core-lens 0.1.dev180__tar.gz → 0.1.dev183__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 (94) hide show
  1. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/PKG-INFO +1 -1
  2. core_lens-0.1.dev183/SKILLS.md +242 -0
  3. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/docs/source/export.md +1 -6
  4. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/docs/source/plugins.md +2 -0
  5. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/docs/source/queries.md +2 -7
  6. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/docs/source/quickstart.md +2 -6
  7. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/docs/source/stats.md +10 -18
  8. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/_version.py +2 -2
  9. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/schema/detection.py +19 -1
  10. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/schema/profile.py +11 -0
  11. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/usage.md +12 -13
  12. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/uv.lock +485 -392
  13. core_lens-0.1.dev180/SKILLS.md +0 -259
  14. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/.github/ISSUE_TEMPLATE/blank-proposal.yaml +0 -0
  15. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/.github/ISSUE_TEMPLATE/bug-report.yaml +0 -0
  16. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/.github/ISSUE_TEMPLATE/feature-request.yaml +0 -0
  17. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/.github/pull_request_template.md +0 -0
  18. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/.github/workflows/ci.yml +0 -0
  19. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/.github/workflows/gh-pages.yml +0 -0
  20. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/.github/workflows/pre-release.yml +0 -0
  21. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/.github/workflows/release.yml +0 -0
  22. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/.gitignore +0 -0
  23. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/.gitmessage +0 -0
  24. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/.pre-commit-config.yaml +0 -0
  25. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/.python-version +0 -0
  26. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/CONTRIBUTING.md +0 -0
  27. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/LICENSE +0 -0
  28. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/README.md +0 -0
  29. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/benchmarks/README.md +0 -0
  30. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/benchmarks/bench_aoi.py +0 -0
  31. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/benchmarks/bench_entity.py +0 -0
  32. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/benchmarks/bench_export.py +0 -0
  33. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/benchmarks/bench_polars_utils.py +0 -0
  34. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/benchmarks/bench_result.py +0 -0
  35. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/benchmarks/bench_schema.py +0 -0
  36. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/benchmarks/bench_season.py +0 -0
  37. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/benchmarks/bench_spatial.py +0 -0
  38. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/benchmarks/bench_view.py +0 -0
  39. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/benchmarks/run_all.sh +0 -0
  40. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/docs/Makefile +0 -0
  41. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/docs/make.bat +0 -0
  42. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/docs/source/concepts.md +0 -0
  43. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/docs/source/conf.py +0 -0
  44. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/docs/source/index.rst +0 -0
  45. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/docs/source/intro.md +0 -0
  46. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/docs/source/logging.md +0 -0
  47. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/docs/source/plots.md +0 -0
  48. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/examples/demo_mws.py +0 -0
  49. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/examples/demo_tehsil.py +0 -0
  50. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/hooks/mypy.sh +0 -0
  51. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/hooks/no-parquet-outside-fixtures.sh +0 -0
  52. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/hooks/pytest.sh +0 -0
  53. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/pyproject.toml +0 -0
  54. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/__init__.py +0 -0
  55. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/__main__.py +0 -0
  56. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/aoi.py +0 -0
  57. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/base/__init__.py +0 -0
  58. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/base/entity.py +0 -0
  59. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/base/namespaces/__init__.py +0 -0
  60. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/base/namespaces/plot.py +0 -0
  61. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/base/namespaces/stats.py +0 -0
  62. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/base/result.py +0 -0
  63. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/base/view.py +0 -0
  64. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/entities/__init__.py +0 -0
  65. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/entities/mws.py +0 -0
  66. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/entities/tehsil.py +0 -0
  67. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/entities/waterbody.py +0 -0
  68. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/export/__init__.py +0 -0
  69. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/export/formats.py +0 -0
  70. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/py.typed +0 -0
  71. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/schema/__init__.py +0 -0
  72. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/utils/__init__.py +0 -0
  73. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/utils/paths.py +0 -0
  74. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/utils/polars_utils.py +0 -0
  75. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/utils/season.py +0 -0
  76. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/src/core_lens/utils/spatial.py +0 -0
  77. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/tests/fixtures/generate_fixtures.py +0 -0
  78. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/tests/unit/conftest.py +0 -0
  79. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/tests/unit/test_aoi.py +0 -0
  80. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/tests/unit/test_entities.py +0 -0
  81. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/tests/unit/test_entity.py +0 -0
  82. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/tests/unit/test_export.py +0 -0
  83. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/tests/unit/test_main.py +0 -0
  84. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/tests/unit/test_plot.py +0 -0
  85. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/tests/unit/test_polars_utils.py +0 -0
  86. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/tests/unit/test_profile.py +0 -0
  87. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/tests/unit/test_result.py +0 -0
  88. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/tests/unit/test_schema_detection.py +0 -0
  89. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/tests/unit/test_schema_profile.py +0 -0
  90. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/tests/unit/test_season.py +0 -0
  91. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/tests/unit/test_season_config.py +0 -0
  92. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/tests/unit/test_spatial.py +0 -0
  93. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/tests/unit/test_stats.py +0 -0
  94. {core_lens-0.1.dev180 → core_lens-0.1.dev183}/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.dev180
3
+ Version: 0.1.dev183
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
@@ -0,0 +1,242 @@
1
+ ---
2
+ name: core-lens
3
+ description: >
4
+ A Python library for querying, analysing, and visualising geospatial Earth science data.
5
+ Provides a unified interface over microwatersheds (MWS), administrative boundaries, and pluggable domain entities.
6
+ Use this skill to understand how to interact with AoI (Area of Interest), Views, Results, and Statistical analysis.
7
+ ---
8
+
9
+ # Agent Instructions: Core-Lens Library
10
+
11
+ You are an AI Agent tasked with writing or debugging code using the `core-lens` library. `core-lens` is a high-performance Python library designed for querying, filtering, and manipulating large-scale geospatial datasets using deferred execution with **Polars**.
12
+
13
+ **Read these instructions carefully before writing any code.**
14
+
15
+ ## 1. Core Architectural Constraints
16
+
17
+ - **Area of Interest (AoI) is the Primary Entry Point**: Never load parquet files directly. Always create an `AoI` object to define the geographical scope and directory context.
18
+ - **Lazy Evaluation**: `core-lens` is lazy by default. Accessing an entity like `aoi.mws` creates a `View`. No data is loaded into memory until you call a materialisation method (`.static`, `.annual`, or `.sub_annual`).
19
+ - **Polars Over Pandas**: `core-lens` uses Polars for zero-copy operations. Only convert to GeoPandas (`.gdf()`) when strictly necessary for heavy geometric processing.
20
+ - **Separation of Concerns**:
21
+ - `AoI`: Filters boundaries, resolves geography, scopes entities, and handles time constraints.
22
+ - `Result`: Produced by materializing a view. Handles all computation, spatial joins, aggregations, stats, and plotting.
23
+ - `Export`: Separate namespace (`from core_lens.export import geoparquet`).
24
+
25
+ ## 1.5 Logging and Tracing
26
+
27
+ `core_lens` uses Loguru and is disabled by default. If you need to debug spatial resolution issues or trace validation errors, enable the internal logger:
28
+
29
+ ```python
30
+ import sys
31
+ from loguru import logger
32
+
33
+ logger.enable("core_lens")
34
+ logger.add(sys.stdout, level="DEBUG")
35
+ ```
36
+
37
+ ## 2. Initialization and Setup
38
+
39
+ Before querying, you MUST register the specific entities you plan to use.
40
+
41
+ ```python
42
+ from core_lens import AoI, SeasonConfig
43
+ from core_lens.entities import MWSEntity, TehsilEntity, DistrictEntity
44
+
45
+ # 1. Register entities FIRST
46
+ AoI.register(MWSEntity)
47
+ AoI.register(DistrictEntity)
48
+ # ... register others as needed
49
+
50
+ # 2. Scoping the AoI (Choose ONE mode)
51
+ DATA_ROOT = "data/"
52
+ # Mode A: By Administrative Hierarchy
53
+ aoi = AoI(DATA_ROOT, tehsil="Pangi", district="Chamba")
54
+ # Mode B: By Bounding Box
55
+ aoi = AoI(DATA_ROOT, bbox=(76.0, 31.0, 78.0, 33.0))
56
+ # Mode C: By specific Entity ID
57
+ aoi = AoI(DATA_ROOT, mws_id="13_551")
58
+
59
+ # Optional: Override default seasons (kharif, rabi, zaid)
60
+ custom_seasons = SeasonConfig(kharif=("06-01", "10-15"), rabi=("10-16", "02-28"), zaid=("03-01", "05-31"))
61
+ aoi_custom = AoI(DATA_ROOT, tehsil="Pangi", seasons=custom_seasons)
62
+ ```
63
+
64
+ ## 3. Querying & Filtering (Views)
65
+
66
+ Entities on the `AoI` object are accessed as **Views**. Views are lazy and can be chained.
67
+
68
+ ```python
69
+ # Access the view
70
+ mws_view = aoi.mws
71
+
72
+ # Attribute Filtering
73
+ filtered = mws_view.where(state="Himachal Pradesh")
74
+
75
+ # Spatial Filtering (inside the AoI)
76
+ spatial_filtered = mws_view.spatial_filter(bbox=(76.5, 31.5, 77.5, 32.5))
77
+
78
+ # Temporal Filtering
79
+ # Note: You can filter by exact years, a range of years, or predefined seasons.
80
+ from core_lens.base.view import Season
81
+ temporal_view = mws_view.between(year=(2020, 2022), season=Season.KHARIF)
82
+ current_view = mws_view.between(season=Season.CURRENT)
83
+ ```
84
+
85
+ ## 4. Materialization (Loading Data)
86
+
87
+ You must materialize the view into a `Result` to perform computations.
88
+ Choose the resolution based on what you need:
89
+ - `.static`: One row per entity, includes `geometry`.
90
+ - `.annual`: Time-series, aggregated annually. (NO geometry by default).
91
+ - `.sub_annual`: High-frequency time-series. (NO geometry by default).
92
+
93
+ ```python
94
+ static_result = filtered.static.materialise()
95
+ annual_result = temporal_view.annual.materialise()
96
+ ```
97
+ *Note: Depending on library version, you may only need `.static` instead of `.static.materialise()`. If `.static` returns a Result directly, use that.*
98
+
99
+ ```python
100
+ # To access underlying data structures:
101
+ df = static_result.df() # Polars DataFrame (Zero-copy)
102
+ lazy_df = static_result.lazy() # Polars LazyFrame
103
+ gdf = static_result.gdf() # GeoPandas GeoDataFrame (Heavy!)
104
+ ```
105
+
106
+ ## 5. Computation and Data Manipulation (Result API)
107
+
108
+ All data manipulation happens on the `Result` object.
109
+
110
+ ```python
111
+ import polars as pl
112
+
113
+ # Derive new columns
114
+ res = annual_result.derive("water_shortage", pl.col("rainfall") - pl.col("storage"))
115
+
116
+ # Aggregation (Supported 'by' keys: "year", "month", "season", "season_year")
117
+ agg_res = res.aggregate(pl.mean("ndvi").alias("avg_ndvi"), by="year")
118
+
119
+ # Reattaching Geometry
120
+ # Time-series results drop geometry to save memory. Reattach it if you need to map them:
121
+ geo_result = agg_res.with_geometry()
122
+ ```
123
+
124
+ ## 6. Spatial Joins
125
+
126
+ Spatial joins are lazy and enrich a primary entity with aggregated data from a secondary entity.
127
+
128
+ ```python
129
+ # Calculate the total forest tree_cover inside each MWS
130
+ joined_view = aoi.mws.spatial_join(
131
+ aoi.forest, agg={"tree_cover": "sum", "canopy_density": "mean"}
132
+ )
133
+ # Resulting columns are prefixed, e.g., 'forest_tree_cover', 'forest_canopy_density'
134
+ ```
135
+
136
+ ## 7. Statistical Analysis (`Result.stats`)
137
+
138
+ All stats methods are available under `result.stats` and return a NEW `Result`.
139
+ - Computed data is in `result.data` (which is a Polars DataFrame)
140
+ - Parameters and testing metrics are stored in `result.metadata`
141
+
142
+ ```python
143
+ # Descriptive stats
144
+ result.stats.describe(columns=["ndvi", "rainfall"])
145
+
146
+ # Correlation (pearson, spearman, kendall)
147
+ from core_lens.base.namespaces.stats import CorrelateMethod
148
+ result.stats.correlate(columns=["ndvi", "rainfall"], method=CorrelateMethod.PEARSON, across="entity")
149
+
150
+ # Hypothesis Testing (t-test, mann-whitney, wilcoxon, ks, chi-square)
151
+ from core_lens.base.namespaces.stats import TestMethod
152
+ result.stats.test(column="cropping_intensity", groups="temperature_zone", method=TestMethod.MANN_WHITNEY)
153
+
154
+ # Change Detection (absolute, percentage, trend)
155
+ from core_lens.base.namespaces.stats import ChangeMethod
156
+ result.stats.change(column="tree_cover", from_period=2018, to_period=2023, method=ChangeMethod.PERCENTAGE)
157
+
158
+ # Anomaly Detection
159
+ from core_lens.base.namespaces.stats import AnomalyTsMethod, AnomalyCrossMethod
160
+ # Mode 1: Cross-sectional (zscore, iqr, percentile, threshold)
161
+ result.stats.anomaly(column="ndvi", mode="cross_sectional", method=AnomalyCrossMethod.ZSCORE, baseline=(2010, 2020))
162
+ # Mode 2: Time-series (stl, cusum, mad)
163
+ result.stats.anomaly(column="ndvi", mode="timeseries", method=AnomalyTsMethod.STL, baseline=(2010, 2018))
164
+
165
+ # Similarity Matching (euclidean, cosine, mahalanobis, manhattan)
166
+ from core_lens.base.namespaces.stats import SimilarityMethod
167
+ result.stats.similarity(
168
+ target="13_551",
169
+ columns={"rainfall": ("annual", {"year": 2018})},
170
+ method=SimilarityMethod.EUCLIDEAN,
171
+ top_n=10
172
+ )
173
+ ```
174
+
175
+ ## 8. Plotting and Visualization (`Result.plot`)
176
+
177
+ Plots are explicitly accessed via the `result.plot` namespace. Maps use Lonboard (requires geometry); charts use Plotly. By default, charts render the top 10 entities.
178
+
179
+ ```python
180
+ # Map plotting (Requires result.with_geometry() to have been called for time-series)
181
+ result.plot.choropleth("ndvi", subplot_on="season_year")
182
+
183
+ # Charts
184
+ result.plot.timeseries(x="year", y="ndvi", top_n=10)
185
+ result.plot.scatter(x="rainfall", y="ndvi", top_n=10)
186
+ result.plot.distribution(x="ndvi", top_n=10)
187
+ result.plot.correlation(columns=["ndvi", "rainfall"], top_n=10)
188
+ result.plot.heatmap(x="year", y="month", value="ndvi", top_n=10)
189
+ result.plot.matrix()
190
+
191
+ # Plotting the AoI boundary itself
192
+ aoi.plot()
193
+ aoi.plot(overlay=result)
194
+ ```
195
+
196
+ ## 9. Exporting Results
197
+
198
+ Exporting uses standalone functions. Do not export massive datasets (like country-wide `mws`) to GeoJSON as it will bloat the file size. Use GeoParquet instead.
199
+
200
+ ```python
201
+ from core_lens.export import geoparquet, parquet, geojson
202
+
203
+ # For results with geometry
204
+ geoparquet(result, "output.geoparquet")
205
+
206
+ # For tabular results without geometry
207
+ parquet(result, "output.parquet")
208
+ ```
209
+
210
+ ## 10. Plugin System (Custom Entities)
211
+
212
+ You can extend core-lens with custom entities by subclassing `BaseEntity`.
213
+
214
+ ```python
215
+ from core_lens.base import BaseEntity
216
+
217
+ class CustomEntity(BaseEntity):
218
+ @property
219
+ def key_cols(self) -> list[str]:
220
+ return ["custom_id"]
221
+
222
+ @property
223
+ def geometry_col(self) -> str:
224
+ return "geometry"
225
+
226
+ @property
227
+ def static_path(self) -> str:
228
+ return "custom/static.geoparquet"
229
+
230
+ @property
231
+ def annual_path(self) -> str | None:
232
+ return "custom/annual.parquet"
233
+
234
+ AoI.register(CustomEntity)
235
+ ```
236
+
237
+ ## Agent Reminders & Pitfalls to Avoid
238
+ - **DO NOT** attempt to read parquet files directly using `pandas.read_parquet` or `polars.read_parquet`. Always construct an `AoI` and use the library's materialized `Result` output.
239
+ - **DO NOT** assume `annual` or `sub_annual` results contain geometry. You must call `.with_geometry()` to reattach it.
240
+ - **DO NOT** forget to import and register entities before initializing the `AoI`.
241
+ - **DO** use Polars expressions (`import polars as pl`) inside `.derive()` and `.aggregate()` methods for maximum performance.
242
+ - **DO** always read parameters/testing metrics from `result.metadata` and underlying stats from `result.data` after running any methods in `result.stats`.
@@ -32,12 +32,7 @@ from core_lens.export import geoparquet, geojson
32
32
  spatial_res = aoi.mws.annual.with_geometry()
33
33
 
34
34
  # Export to GeoParquet with DuckDB COPY options (e.g., partitioning)
35
- geoparquet(
36
- spatial_res,
37
- "output.geoparquet",
38
- compression="ZSTD",
39
- partition_by="year"
40
- )
35
+ geoparquet(spatial_res, "output.geoparquet", compression="ZSTD", partition_by="year")
41
36
 
42
37
  # Export to GeoJSON
43
38
  geojson(spatial_res, "output.json")
@@ -5,6 +5,7 @@ You can extend CoreLens by defining your own entities. Simply subclass `BaseEnti
5
5
  ```python
6
6
  from core_lens.base import BaseEntity
7
7
 
8
+
8
9
  class CustomEntity(BaseEntity):
9
10
  @property
10
11
  def key_cols(self) -> list[str]:
@@ -22,6 +23,7 @@ class CustomEntity(BaseEntity):
22
23
  def annual_path(self) -> str | None:
23
24
  return "custom/annual.parquet"
24
25
 
26
+
25
27
  # Register before use
26
28
  AoI.register(CustomEntity)
27
29
  ```
@@ -27,8 +27,7 @@ import polars as pl
27
27
  # 1. Derive new columns (works on any Result)
28
28
  res_annual = aoi.mws.annual
29
29
  derived_res = res_annual.derive(
30
- "drought_flag",
31
- pl.when(pl.col("rainfall") < 500).then(1).otherwise(0)
30
+ "drought_flag", pl.when(pl.col("rainfall") < 500).then(1).otherwise(0)
32
31
  )
33
32
 
34
33
  # 2. Aggregate (temporal grouping like "month" requires sub-annual, but "year" works on annual too)
@@ -47,11 +46,7 @@ Spatially join data from two different entities (e.g. Microwatersheds and Forest
47
46
 
48
47
  # Join forest metrics onto MWS geometries
49
48
  joined_view = aoi.mws.spatial_join(
50
- aoi.forest,
51
- agg={
52
- "tree_cover": "area",
53
- "canopy_density": "mean"
54
- }
49
+ aoi.forest, agg={"tree_cover": "area", "canopy_density": "mean"}
55
50
  )
56
51
 
57
52
  # Materialise the joined view
@@ -29,15 +29,11 @@ Create an `AoI` (Area of Interest) by specifying the `data_root` (which can be a
29
29
 
30
30
  ```python
31
31
  # Create an AoI scoped to a specific Tehsil using a local path
32
- aoi = AoI(
33
- data_root="/path/to/data",
34
- tehsil="Pangi"
35
- )
32
+ aoi = AoI(data_root="/path/to/data", tehsil="Pangi")
36
33
 
37
34
  # Or scope it directly to a specific list of entity IDs using an S3 bucket
38
35
  aoi_specific = AoI(
39
- data_root="s3://my-cloud-bucket/core-lens-data",
40
- mws_id=["13_001", "13_002"]
36
+ data_root="s3://my-cloud-bucket/core-lens-data", mws_id=["13_001", "13_002"]
41
37
  )
42
38
 
43
39
  # You can now access entities scoped to this AoI:
@@ -21,8 +21,8 @@ from core_lens.base.namespaces.stats import CorrelateMethod
21
21
 
22
22
  corr = res.stats.correlate(
23
23
  columns=["ndvi", "rainfall", "temperature"],
24
- method=CorrelateMethod.PEARSON, # or SPEARMAN, KENDALL
25
- across="entity" # correlate across entities or time
24
+ method=CorrelateMethod.PEARSON, # or SPEARMAN, KENDALL
25
+ across="entity", # correlate across entities or time
26
26
  )
27
27
  ```
28
28
 
@@ -37,14 +37,12 @@ from core_lens.base.namespaces.stats import TestMethod
37
37
  test_res = res.stats.test(
38
38
  column="cropping_intensity",
39
39
  groups="temperature_zone",
40
- method=TestMethod.MANN_WHITNEY
40
+ method=TestMethod.MANN_WHITNEY,
41
41
  )
42
42
 
43
43
  # Period-based testing
44
44
  test_period = res.stats.test(
45
- column="ndvi",
46
- periods=[(2010, 2015), (2016, 2023)],
47
- method=TestMethod.T_TEST
45
+ column="ndvi", periods=[(2010, 2015), (2016, 2023)], method=TestMethod.T_TEST
48
46
  )
49
47
  ```
50
48
 
@@ -57,10 +55,7 @@ from core_lens.base.namespaces.stats import ChangeMethod
57
55
 
58
56
  # Trend over time
59
57
  trend = res.stats.change(
60
- column="ndvi",
61
- from_period=2010,
62
- to_period=2023,
63
- method=ChangeMethod.TREND
58
+ column="ndvi", from_period=2010, to_period=2023, method=ChangeMethod.TREND
64
59
  )
65
60
 
66
61
  # Absolute or percentage change
@@ -68,7 +63,7 @@ pct_change = res.stats.change(
68
63
  column="tree_cover",
69
64
  from_period=2018,
70
65
  to_period=2023,
71
- method=ChangeMethod.PERCENTAGE
66
+ method=ChangeMethod.PERCENTAGE,
72
67
  )
73
68
  ```
74
69
 
@@ -81,10 +76,7 @@ from core_lens.base.namespaces.stats import AnomalyTsMethod, AnomalyCrossMethod
81
76
 
82
77
  # Timeseries anomaly against its own history
83
78
  ts_anomalies = res.stats.anomaly(
84
- column="ndvi",
85
- mode="timeseries",
86
- method=AnomalyTsMethod.STL,
87
- baseline=(2010, 2018)
79
+ column="ndvi", mode="timeseries", method=AnomalyTsMethod.STL, baseline=(2010, 2018)
88
80
  )
89
81
 
90
82
  # Cross-sectional anomaly against other entities
@@ -92,7 +84,7 @@ cross_anomalies = res.stats.anomaly(
92
84
  column="ndvi",
93
85
  mode="cross_sectional",
94
86
  method=AnomalyCrossMethod.ZSCORE,
95
- baseline=(2010, 2020)
87
+ baseline=(2010, 2020),
96
88
  )
97
89
  ```
98
90
 
@@ -107,9 +99,9 @@ similar = res.stats.similarity(
107
99
  target="13_551",
108
100
  columns={
109
101
  "rainfall": ("annual", {"year": 2018}),
110
- "ndvi": ("sub_annual", {"season": "kharif", "year": 2020})
102
+ "ndvi": ("sub_annual", {"season": "kharif", "year": 2020}),
111
103
  },
112
104
  method=SimilarityMethod.EUCLIDEAN,
113
- top_n=10
105
+ top_n=10,
114
106
  )
115
107
  ```
@@ -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.dev180'
22
- __version_tuple__ = version_tuple = (0, 1, 'dev180')
21
+ __version__ = version = '0.1.dev183'
22
+ __version_tuple__ = version_tuple = (0, 1, 'dev183')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -102,10 +102,12 @@ def detect(
102
102
  if lon_col:
103
103
  reserved_static.add(lon_col)
104
104
  extra_static_cols = [c for c in static_schema if c not in reserved_static]
105
+ col_types_static = {col: str(dtype) for col, dtype in static_schema.items()}
105
106
 
106
107
  annual_time_col: str | None = None
107
108
  extra_annual_cols: list[str] = []
108
109
  annual_is_year_col: bool | None = None
110
+ col_types_annual: dict[str, str] = {}
109
111
  if annual_path:
110
112
  annual_schema = _read_schema(
111
113
  annual_path, label="annual", storage_options=_so or None
@@ -116,10 +118,12 @@ def detect(
116
118
  )
117
119
  extra_annual_cols = [c for c in annual_schema if c not in reserved_annual]
118
120
  annual_is_year_col = _is_year_col_from_schema(annual_schema, annual_time_col)
121
+ col_types_annual = {col: str(dtype) for col, dtype in annual_schema.items()}
119
122
 
120
123
  sub_annual_time_col: str | None = None
121
124
  extra_sub_annual_cols: list[str] = []
122
125
  sub_annual_is_year_col: bool | None = None
126
+ col_types_sub_annual: dict[str, str] = {}
123
127
  if sub_annual_path:
124
128
  sub_annual_schema = _read_schema(
125
129
  sub_annual_path, label="sub_annual", storage_options=_so or None
@@ -132,6 +136,9 @@ def detect(
132
136
  sub_annual_is_year_col = _is_year_col_from_schema(
133
137
  sub_annual_schema, sub_annual_time_col
134
138
  )
139
+ col_types_sub_annual = {
140
+ col: str(dtype) for col, dtype in sub_annual_schema.items()
141
+ }
135
142
 
136
143
  return SchemaProfile(
137
144
  key_cols=key_cols,
@@ -143,6 +150,9 @@ def detect(
143
150
  extra_static_cols=extra_static_cols,
144
151
  extra_annual_cols=extra_annual_cols,
145
152
  extra_sub_annual_cols=extra_sub_annual_cols,
153
+ col_types_static=col_types_static,
154
+ col_types_annual=col_types_annual,
155
+ col_types_sub_annual=col_types_sub_annual,
146
156
  annual_is_year_col=annual_is_year_col,
147
157
  sub_annual_is_year_col=sub_annual_is_year_col,
148
158
  )
@@ -153,10 +163,18 @@ def _read_schema(
153
163
  label: str,
154
164
  storage_options: dict[str, Any] | None = None,
155
165
  ) -> pl.Schema:
166
+ import os
167
+
156
168
  _so = storage_options or {}
169
+ # When path is a directory, use a glob to read only .parquet files.
170
+ # This prevents polars from raising InvalidOperationError when the directory
171
+ # contains mixed-extension files (e.g. data_dictionary.csv alongside .parquet).
172
+ scan_path = path
173
+ if os.path.isdir(path):
174
+ scan_path = os.path.join(path, "**", "*.parquet")
157
175
  try:
158
176
  return pl.scan_parquet(
159
- path,
177
+ scan_path,
160
178
  hive_partitioning=True,
161
179
  storage_options=_so or None,
162
180
  ).collect_schema()
@@ -56,6 +56,12 @@ class SchemaProfile(BaseModel):
56
56
  is used for validation and documentation only.
57
57
  extra_annual_cols: Additional attribute columns in the annual file.
58
58
  extra_sub_annual_cols: Additional attribute columns in the sub_annual file.
59
+ col_types_static: Mapping of **all** column names in the static file to
60
+ their Polars dtype string (e.g. ``{"basin_id": "Int32", "geometry": "Binary"}``).
61
+ Populated by the auto-detection layer; ``{}`` when the profile is
62
+ constructed manually.
63
+ col_types_annual: Same as ``col_types_static`` but for the annual file.
64
+ col_types_sub_annual: Same as ``col_types_static`` but for the sub_annual file.
59
65
 
60
66
  """
61
67
 
@@ -70,6 +76,11 @@ class SchemaProfile(BaseModel):
70
76
  extra_static_cols: list[str] = []
71
77
  extra_annual_cols: list[str] = []
72
78
  extra_sub_annual_cols: list[str] = []
79
+ # Column name → Polars dtype string for every column in each resolution.
80
+ # Populated by the auto-detection layer; empty dict when built manually.
81
+ col_types_static: dict[str, str] = {}
82
+ col_types_annual: dict[str, str] = {}
83
+ col_types_sub_annual: dict[str, str] = {}
73
84
  # Whether the time column is an integer-year column (True), a Date/Datetime
74
85
  # column (False), or absent/unknown (None). Determined at detect() time from
75
86
  # the Parquet schema — eliminates a redundant collect_schema() call in
@@ -89,8 +89,8 @@ spatial_view = mws_view.spatial_filter(bbox=(76.5, 31.5, 77.5, 32.5))
89
89
  from core_lens.base.view import Season
90
90
 
91
91
  temporal_view = mws_view.between(
92
- year=(2020, 2022), # Inclusive year range
93
- season=Season.KHARIF # Filter to specific season
92
+ year=(2020, 2022), # Inclusive year range
93
+ season=Season.KHARIF, # Filter to specific season
94
94
  )
95
95
  ```
96
96
 
@@ -101,8 +101,7 @@ You can define a spatial join to enrich one entity with aggregated data from ano
101
101
  ```python
102
102
  # Join MWS with Tehsil entity to calculate the sum of tehsil areas per MWS
103
103
  joined_view = mws_view.spatial_join(
104
- other=TehsilEntity(data_root=DATA_ROOT),
105
- agg={"Shape_Area": "sum"}
104
+ other=TehsilEntity(data_root=DATA_ROOT), agg={"Shape_Area": "sum"}
106
105
  )
107
106
  ```
108
107
 
@@ -127,22 +126,24 @@ A `Result` object provides access to the materialized data. `core-lens` uses zer
127
126
 
128
127
  ```python
129
128
  # 1. Zero-copy access (Instantaneous)
130
- pl_df = result_static.df() # Polars DataFrame
131
- pl_lazy = result_static.lazy() # Polars LazyFrame
129
+ pl_df = result_static.df() # Polars DataFrame
130
+ pl_lazy = result_static.lazy() # Polars LazyFrame
132
131
 
133
132
  # 2. Decode WKB geometries to Shapely (CPU intensive)
134
- gdf = result_static.gdf() # GeoPandas GeoDataFrame
133
+ gdf = result_static.gdf() # GeoPandas GeoDataFrame
135
134
 
136
135
  # 3. Data Transformations (Returns a new Result)
137
136
  # Derive new columns using Polars expressions
138
137
  import polars as pl
139
- result_derived = result_annual.derive("yield_per_ha", pl.col("yield") / pl.col("area_in_ha"))
138
+
139
+ result_derived = result_annual.derive(
140
+ "yield_per_ha", pl.col("yield") / pl.col("area_in_ha")
141
+ )
140
142
 
141
143
  # 4. Fast aggregations (Returns a new Result)
142
144
  # Aggregate by a key column using Polars expressions
143
145
  result_agg = result_derived.aggregate(
144
- pl.col("yield_per_ha").mean().alias("avg_yield_per_ha"),
145
- by="year"
146
+ pl.col("yield_per_ha").mean().alias("avg_yield_per_ha"), by="year"
146
147
  )
147
148
  ```
148
149
 
@@ -173,9 +174,7 @@ By default, `core-lens` uses Indian agricultural seasons. You can inject a custo
173
174
  from core_lens.aoi import SeasonConfig
174
175
 
175
176
  custom_seasons = SeasonConfig(
176
- kharif=("06-01", "10-15"),
177
- rabi=("10-16", "02-28"),
178
- zaid=("03-01", "05-31")
177
+ kharif=("06-01", "10-15"), rabi=("10-16", "02-28"), zaid=("03-01", "05-31")
179
178
  )
180
179
 
181
180
  aoi = AoI(DATA_ROOT, bbox=(...), seasons=custom_seasons)