core-lens 0.1.dev164__tar.gz → 0.1.dev168__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 (92) hide show
  1. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/CONTRIBUTING.md +2 -2
  2. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/PKG-INFO +4 -2
  3. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/SKILLS.md +7 -7
  4. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/benchmarks/README.md +1 -1
  5. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/benchmarks/bench_result.py +2 -2
  6. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/benchmarks/bench_schema.py +10 -10
  7. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/benchmarks/bench_season.py +13 -13
  8. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/benchmarks/bench_view.py +6 -6
  9. core_lens-0.1.dev168/docs/source/concepts.md +7 -0
  10. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/docs/source/logging.md +1 -1
  11. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/docs/source/queries.md +7 -7
  12. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/docs/source/stats.md +1 -1
  13. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/examples/demo_mws.py +12 -12
  14. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/examples/demo_tehsil.py +2 -2
  15. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/pyproject.toml +3 -1
  16. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/src/core_lens/_version.py +2 -2
  17. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/src/core_lens/aoi.py +3 -3
  18. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/src/core_lens/base/entity.py +13 -13
  19. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/src/core_lens/base/namespaces/plot.py +30 -1
  20. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/src/core_lens/base/namespaces/stats.py +38 -15
  21. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/src/core_lens/base/result.py +10 -10
  22. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/src/core_lens/base/view.py +15 -15
  23. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/src/core_lens/entities/mws.py +4 -4
  24. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/src/core_lens/entities/tehsil.py +1 -1
  25. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/src/core_lens/schema/detection.py +18 -18
  26. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/src/core_lens/schema/profile.py +8 -8
  27. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/src/core_lens/utils/season.py +2 -2
  28. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/tests/fixtures/generate_fixtures.py +7 -7
  29. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/tests/unit/conftest.py +16 -16
  30. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/tests/unit/test_aoi.py +3 -3
  31. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/tests/unit/test_entities.py +2 -2
  32. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/tests/unit/test_entity.py +2 -2
  33. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/tests/unit/test_plot.py +1 -1
  34. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/tests/unit/test_profile.py +1 -1
  35. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/tests/unit/test_result.py +8 -8
  36. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/tests/unit/test_schema_detection.py +7 -7
  37. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/tests/unit/test_schema_profile.py +7 -7
  38. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/tests/unit/test_spatial.py +3 -3
  39. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/tests/unit/test_stats.py +6 -6
  40. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/tests/unit/test_view.py +31 -61
  41. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/uv.lock +569 -249
  42. core_lens-0.1.dev164/docs/source/concepts.md +0 -7
  43. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/.github/ISSUE_TEMPLATE/blank-proposal.yaml +0 -0
  44. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/.github/ISSUE_TEMPLATE/bug-report.yaml +0 -0
  45. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/.github/ISSUE_TEMPLATE/feature-request.yaml +0 -0
  46. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/.github/pull_request_template.md +0 -0
  47. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/.github/workflows/ci.yml +0 -0
  48. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/.github/workflows/gh-pages.yml +0 -0
  49. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/.github/workflows/pre-release.yml +0 -0
  50. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/.github/workflows/release.yml +0 -0
  51. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/.gitignore +0 -0
  52. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/.gitmessage +0 -0
  53. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/.pre-commit-config.yaml +0 -0
  54. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/.python-version +0 -0
  55. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/LICENSE +0 -0
  56. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/README.md +0 -0
  57. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/benchmarks/bench_aoi.py +0 -0
  58. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/benchmarks/bench_export.py +0 -0
  59. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/benchmarks/bench_polars_utils.py +0 -0
  60. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/benchmarks/bench_spatial.py +0 -0
  61. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/benchmarks/run_all.sh +0 -0
  62. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/docs/Makefile +0 -0
  63. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/docs/make.bat +0 -0
  64. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/docs/source/conf.py +0 -0
  65. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/docs/source/export.md +0 -0
  66. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/docs/source/index.rst +0 -0
  67. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/docs/source/intro.md +0 -0
  68. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/docs/source/plots.md +0 -0
  69. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/docs/source/plugins.md +0 -0
  70. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/docs/source/quickstart.md +0 -0
  71. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/hooks/mypy.sh +0 -0
  72. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/hooks/no-parquet-outside-fixtures.sh +0 -0
  73. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/hooks/pytest.sh +0 -0
  74. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/src/core_lens/__init__.py +0 -0
  75. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/src/core_lens/__main__.py +0 -0
  76. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/src/core_lens/base/__init__.py +0 -0
  77. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/src/core_lens/base/namespaces/__init__.py +0 -0
  78. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/src/core_lens/entities/__init__.py +0 -0
  79. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/src/core_lens/entities/waterbody.py +0 -0
  80. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/src/core_lens/export/__init__.py +0 -0
  81. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/src/core_lens/export/formats.py +0 -0
  82. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/src/core_lens/py.typed +0 -0
  83. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/src/core_lens/schema/__init__.py +0 -0
  84. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/src/core_lens/utils/__init__.py +0 -0
  85. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/src/core_lens/utils/paths.py +0 -0
  86. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/src/core_lens/utils/polars_utils.py +0 -0
  87. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/src/core_lens/utils/spatial.py +0 -0
  88. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/tests/unit/test_export.py +0 -0
  89. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/tests/unit/test_main.py +0 -0
  90. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/tests/unit/test_polars_utils.py +0 -0
  91. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/tests/unit/test_season.py +0 -0
  92. {core_lens-0.1.dev164 → core_lens-0.1.dev168}/tests/unit/test_season_config.py +0 -0
@@ -143,7 +143,7 @@ Examples:
143
143
  ```text
144
144
  feature/stats-similarity
145
145
  feature/plugin-validation
146
- fix/version-clash-fortnightly
146
+ fix/version-clash-sub-annual
147
147
  ```
148
148
 
149
149
  Workflow:
@@ -204,7 +204,7 @@ Examples:
204
204
 
205
205
  ```text
206
206
  feat: add stats.similarity()
207
- fix: resolve VersionClashError on fortnightly
207
+ fix: resolve VersionClashError on sub-annual
208
208
  docs: add plugin guide
209
209
  test: add aggregate validation fixtures
210
210
  chore: update ruff
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: core-lens
3
- Version: 0.1.dev164
3
+ Version: 0.1.dev168
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
@@ -10,10 +10,12 @@ License-File: LICENSE
10
10
  Classifier: Development Status :: 2 - Pre-Alpha
11
11
  Classifier: Operating System :: POSIX :: Linux
12
12
  Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
13
15
  Classifier: Topic :: Scientific/Engineering
14
16
  Classifier: Topic :: Scientific/Engineering :: GIS
15
17
  Classifier: Topic :: Scientific/Engineering :: Hydrology
16
- Requires-Python: >=3.13
18
+ Requires-Python: >=3.12
17
19
  Requires-Dist: core-lens[core]
18
20
  Requires-Dist: core-lens[spatial]
19
21
  Provides-Extra: core
@@ -47,7 +47,7 @@ view.between(season="kharif", year=2020)
47
47
  # Materialise into a Result object
48
48
  static_result = view.static # has geometry
49
49
  annual_result = view.annual # no geometry
50
- fortnightly_result = view.fortnightly # no geometry
50
+ sub_annual_result = view.sub_annual # no geometry
51
51
  ```
52
52
 
53
53
  ## 3. Operations on Results (Compute & Aggregation)
@@ -129,7 +129,7 @@ result.stats.similarity(
129
129
  target="13_551",
130
130
  columns={
131
131
  "rainfall": ("annual", {"year": 2018}),
132
- "ndvi": ("fortnightly", {"season": "kharif", "year": 2020})
132
+ "ndvi": ("sub_annual", {"season": "kharif", "year": 2020})
133
133
  },
134
134
  method="euclidean",
135
135
  top_n=10
@@ -189,8 +189,8 @@ AoI.register(ForestEntity)
189
189
 
190
190
  ## 9. Execution Environment Context
191
191
 
192
- - **Loading Data**: There is no explicit `AoI.load_data()` method. The `AoI(data_root="...")` constructor acts as the main entry point and defines the directory scope. Data reading happens lazily using Polars `scan_parquet()` when you explicitly call materialization methods like `.static`, `.annual`, or `.fortnightly` on a `View`.
193
- - **Spatial and Temporal Relationship**: To conserve memory, temporal views (`.annual` and `.fortnightly`) drop geometry columns by default. To establish the spatial relationship between the static boundaries (`mws.parquet`) and the temporal time-series (`annual.parquet`), you use the `Result.with_geometry()` method. This triggers an internal join on the entity's primary key (e.g., `mws_id`) bringing the geometry back into the DataFrame for spatial operations and plotting.
192
+ - **Loading Data**: There is no explicit `AoI.load_data()` method. The `AoI(data_root="...")` constructor acts as the main entry point and defines the directory scope. Data reading happens lazily using Polars `scan_parquet()` when you explicitly call materialization methods like `.static`, `.annual`, or `.sub_annual` on a `View`.
193
+ - **Spatial and Temporal Relationship**: To conserve memory, temporal views (`.annual` and `.sub_annual`) drop geometry columns by default. To establish the spatial relationship between the static boundaries (`mws.parquet`) and the temporal time-series (`annual.parquet`), you use the `Result.with_geometry()` method. This triggers an internal join on the entity's primary key (e.g., `mws_id`) bringing the geometry back into the DataFrame for spatial operations and plotting.
194
194
 
195
195
  - **Expected Directory Structure**: When initializing `AoI(data_root="data/")`, the library expects a specific folder structure inside the `data_root` for each registered entity. For example, the `MWSEntity` expects:
196
196
  ```text
@@ -199,7 +199,7 @@ AoI.register(ForestEntity)
199
199
  ├── static/
200
200
  │ └── mws.parquet ← GeoParquet with boundaries
201
201
  ├── annual/ ← Directory of Parquets (often partitioned by basin)
202
- └── fortnightly/ ← Directory of Parquets (partitioned by year/basin)
202
+ └── sub-annual/ ← Directory of Parquets (partitioned by year/basin)
203
203
  ```
204
204
  *(Note: The exact paths for these files/directories are defined within the `BaseEntity` subclasses, e.g., `MWSEntity.static_path`)*
205
205
 
@@ -229,9 +229,9 @@ view = aoi.mws.where(ba_name="Barmer")
229
229
  temporal_view = view.between(season="kharif", year=2021)
230
230
  ```
231
231
 
232
- **5. Materialization:** Trigger the actual Parquet I/O to pull the data into memory by accessing `.static`, `.annual`, or `.fortnightly`.
232
+ **5. Materialization:** Trigger the actual Parquet I/O to pull the data into memory by accessing `.static`, `.annual`, or `.sub_annual`.
233
233
  ```python
234
- result = temporal_view.fortnightly
234
+ result = temporal_view.sub_annual
235
235
  ```
236
236
 
237
237
  **6. Compute & Analysis:** Chain computations, derive metrics, or run statistical tests on the returned `Result`.
@@ -10,7 +10,7 @@ benchmarks/
10
10
  ├── run_all.sh # Run all benchmarks in sequence
11
11
  ├── bench_aoi.py # AoI init, register, named-boundary resolution
12
12
  ├── bench_spatial.py # build_bbox_index, bbox_intersects_geometry, exact_spatial_filter
13
- ├── bench_view.py # View.where, spatial_filter, between, materialise (static/annual/fortnightly)
13
+ ├── bench_view.py # View.where, spatial_filter, between, materialise (static/annual/sub-annual)
14
14
  ├── bench_result.py # Result.derive, aggregate, with_geometry, df/gdf
15
15
  ├── bench_season.py # resolve_time_filter, add_temporal_columns, SeasonConfig.season_for
16
16
  ├── bench_schema.py # schema detection (detect, _infer_* helpers)
@@ -7,7 +7,7 @@ Targets:
7
7
  - Result.with_geometry() static-join to attach geometry to non-static result
8
8
  - Result.derive() with_columns expression append
9
9
  - Result.aggregate() group_by + agg (no temporal grouping)
10
- - Result.aggregate(by=...) temporal grouping (annual / fortnightly)
10
+ - Result.aggregate(by=...) temporal grouping (annual / sub_annual)
11
11
 
12
12
  Scalene focuses on:
13
13
  - shapely.wkb.loads() in gdf() — Python loop over WKB bytes
@@ -130,7 +130,7 @@ print(
130
130
  )
131
131
 
132
132
 
133
- # ── Note: meaningful with_geometry() test requires annual/fortnightly result ──
133
+ # ── Note: meaningful with_geometry() test requires annual/sub_annual result ──
134
134
  # Uncomment with an MWS entity that has annual_path set:
135
135
 
136
136
  result_annual = aoi.mws.annual # no geometry
@@ -36,7 +36,7 @@ DATA_ROOT = "data/"
36
36
  entity = MWSEntity(data_root=DATA_ROOT)
37
37
  static_path = entity._resolve(entity.static_path)
38
38
  annual_path = entity._resolve(entity.annual_path) if entity.annual_path else None
39
- fn_path = entity._resolve(entity.fortnightly_path) if entity.fortnightly_path else None
39
+ fn_path = entity._resolve(entity.sub_annual_path) if entity.sub_annual_path else None
40
40
 
41
41
 
42
42
  def _section(title: str) -> None:
@@ -104,7 +104,7 @@ print(
104
104
  print(f"Detected time col: {_infer_time_col(schema, static_path)!r}")
105
105
 
106
106
 
107
- # ── 5. detect() — cold (no annual/fortnightly) ────────────────────────────────
107
+ # ── 5. detect() — cold (no annual/sub_annual) ────────────────────────────────
108
108
  _section("5. detect(static only) [full detection — cold]")
109
109
  REPS = 10
110
110
  profile = detect( # ensure bound before loop
@@ -112,7 +112,7 @@ profile = detect( # ensure bound before loop
112
112
  key_cols=entity.key_cols,
113
113
  geometry_col=entity.geometry_col,
114
114
  annual_path=None,
115
- fortnightly_path=None,
115
+ sub_annual_path=None,
116
116
  )
117
117
  t0 = time.perf_counter()
118
118
  for _ in range(REPS):
@@ -121,7 +121,7 @@ for _ in range(REPS):
121
121
  key_cols=entity.key_cols,
122
122
  geometry_col=entity.geometry_col,
123
123
  annual_path=None,
124
- fortnightly_path=None,
124
+ sub_annual_path=None,
125
125
  )
126
126
  t1 = time.perf_counter()
127
127
  print(
@@ -133,15 +133,15 @@ print(f"Profile bbox_cols : {profile.bbox_cols!r}")
133
133
  print(f"Profile extra_static : {profile.extra_static_cols}")
134
134
 
135
135
 
136
- # ── 6. detect() — with annual + fortnightly ───────────────────────────────────
136
+ # ── 6. detect() — with annual + sub_annual ───────────────────────────────────
137
137
  if annual_path or fn_path:
138
- _section("6. detect(static + annual + fortnightly) [3× schema reads]")
138
+ _section("6. detect(static + annual + sub_annual) [3× schema reads]")
139
139
  profile_full = detect( # ensure bound before loop
140
140
  static_path=static_path,
141
141
  key_cols=entity.key_cols,
142
142
  geometry_col=entity.geometry_col,
143
143
  annual_path=annual_path,
144
- fortnightly_path=fn_path,
144
+ sub_annual_path=fn_path,
145
145
  )
146
146
  t0 = time.perf_counter()
147
147
  for _ in range(REPS):
@@ -150,7 +150,7 @@ if annual_path or fn_path:
150
150
  key_cols=entity.key_cols,
151
151
  geometry_col=entity.geometry_col,
152
152
  annual_path=annual_path,
153
- fortnightly_path=fn_path,
153
+ sub_annual_path=fn_path,
154
154
  )
155
155
  t1 = time.perf_counter()
156
156
  print(
@@ -158,10 +158,10 @@ if annual_path or fn_path:
158
158
  f"({(t1 - t0) / REPS * 1000:.2f} ms/call)"
159
159
  )
160
160
  print(f"annual_time_col : {profile_full.annual_time_col!r}")
161
- print(f"fortnightly_time_col : {profile_full.fortnightly_time_col!r}")
161
+ print(f"sub_annual_time_col : {profile_full.sub_annual_time_col!r}")
162
162
  else:
163
163
  print(
164
- "\n[skip] No annual/fortnightly paths on TehsilEntity — add an entity with those paths"
164
+ "\n[skip] No annual/sub_annual paths on TehsilEntity — add an entity with those paths"
165
165
  )
166
166
 
167
167
 
@@ -6,7 +6,7 @@ Targets:
6
6
  - resolve_time_filter() dict → Polars Expr build (date-range + season modes)
7
7
  - _date_range_expr() expression build for date ranges
8
8
  - _season_expr() expression build for seasons (year-crossing vs not)
9
- - add_temporal_columns() fortnightly vectorised temporal derivation
9
+ - add_temporal_columns() sub_annual vectorised temporal derivation
10
10
 
11
11
  Scalene focuses on:
12
12
  - Python datetime.strptime calls in __post_init__
@@ -35,22 +35,22 @@ from core_lens.utils.season import add_temporal_columns, resolve_time_filter
35
35
  START_DATE = date(2010, 1, 1)
36
36
  END_DATE = date(2023, 12, 31)
37
37
 
38
- # Number of fortnightly periods between START_DATE and END_DATE (roughly).
39
- N_FORTNIGHTLY = (END_DATE - START_DATE).days // 14
38
+ # Number of sub_annual periods between START_DATE and END_DATE (roughly).
39
+ N_SUB_ANNUAL = (END_DATE - START_DATE).days // 14
40
40
 
41
- # Representative synthetic fortnightly dataframe for add_temporal_columns.
41
+ # Representative synthetic sub_annual dataframe for add_temporal_columns.
42
42
  N_MWS = 500 # number of entity instances (e.g. MWS)
43
- DATES = [START_DATE + timedelta(days=14 * i) for i in range(N_FORTNIGHTLY)]
44
- # Long format: N_MWS × N_FORTNIGHTLY rows
43
+ DATES = [START_DATE + timedelta(days=14 * i) for i in range(N_SUB_ANNUAL)]
44
+ # Long format: N_MWS × N_SUB_ANNUAL rows
45
45
  _all_dates = DATES * N_MWS
46
- FORTNIGHTLY_DF = pl.DataFrame(
46
+ SUB_ANNUAL_DF = pl.DataFrame(
47
47
  {
48
48
  "mws_id": [str(mid) for mid in range(N_MWS) for _ in DATES],
49
49
  "date": _all_dates,
50
- "ndvi": [0.5] * (N_MWS * N_FORTNIGHTLY),
50
+ "ndvi": [0.5] * (N_MWS * N_SUB_ANNUAL),
51
51
  }
52
52
  )
53
- print(f"Synthetic fortnightly frame: {FORTNIGHTLY_DF.shape}")
53
+ print(f"Synthetic sub_annual frame: {SUB_ANNUAL_DF.shape}")
54
54
 
55
55
 
56
56
  def _section(title: str) -> None:
@@ -162,15 +162,15 @@ print(
162
162
  )
163
163
 
164
164
 
165
- # ── 8. add_temporal_columns — large fortnightly DataFrame ─────────────────────
166
- _section(f"8. add_temporal_columns() [{FORTNIGHTLY_DF.shape[0]:,} rows]")
165
+ # ── 8. add_temporal_columns — large sub_annual DataFrame ─────────────────────
166
+ _section(f"8. add_temporal_columns() [{SUB_ANNUAL_DF.shape[0]:,} rows]")
167
167
  t0 = time.perf_counter()
168
- result_df = add_temporal_columns(FORTNIGHTLY_DF, "date", cfg)
168
+ result_df = add_temporal_columns(SUB_ANNUAL_DF, "date", cfg)
169
169
  t1 = time.perf_counter()
170
170
  print(f"add_temporal_columns: {(t1 - t0) * 1000:.2f} ms")
171
171
  print(f"Output shape : {cast(pl.DataFrame, result_df).shape}")
172
172
  print(
173
- f"New columns added : {[c for c in cast(pl.DataFrame, result_df).columns if c not in FORTNIGHTLY_DF.columns]}"
173
+ f"New columns added : {[c for c in cast(pl.DataFrame, result_df).columns if c not in SUB_ANNUAL_DF.columns]}"
174
174
  )
175
175
 
176
176
  # Second call — all columns already present, should be near-zero.
@@ -4,14 +4,14 @@ Targets:
4
4
  - View.where() attribute filter (Polars semi-join on static file)
5
5
  - View.spatial_filter() STRtree filter narrowing existing keys
6
6
  - View.between() time-filter dict construction (no I/O)
7
- - View._materialise() static / annual / fortnightly Parquet reads
7
+ - View._materialise() static / annual / sub_annual Parquet reads
8
8
  - View chaining where → spatial_filter → between → static
9
9
 
10
10
  Scalene focuses on:
11
11
  - scan_with_key_filter() predicate-pushdown LazyFrame
12
12
  - collect_lf() streaming collect (CPU or GPU)
13
13
  - resolve_time_filter() expression build for time predicates
14
- - add_temporal_columns() fortnightly temporal column derivation
14
+ - add_temporal_columns() sub_annual temporal column derivation
15
15
 
16
16
  Usage:
17
17
  uv run scalene run --html --outfile benchmarks/profiles/bench_view.html benchmarks/bench_view.py
@@ -140,12 +140,12 @@ print(f"annual : {(t1 - t0) * 1000:.2f} ms")
140
140
  print(f"Shape : {result_annual.df().shape}")
141
141
 
142
142
 
143
- # ── 11. fortnightly materialisation ───────────────────────────────────────────
144
- _section("11. aoi_small.mws.fortnightly [fortnightly materialisation]")
143
+ # ── 11. sub_annual materialisation ───────────────────────────────────────────
144
+ _section("11. aoi_small.mws.sub_annual [sub_annual materialisation]")
145
145
  t0 = time.perf_counter()
146
- result_fn = aoi_small.mws.between(season=Season.KHARIF, year=2022).fortnightly
146
+ result_fn = aoi_small.mws.between(season=Season.KHARIF, year=2022).sub_annual
147
147
  t1 = time.perf_counter()
148
- print(f"fortnightly : {(t1 - t0) * 1000:.2f} ms")
148
+ print(f"sub_annual : {(t1 - t0) * 1000:.2f} ms")
149
149
  print(f"Shape : {result_fn.df().shape}")
150
150
 
151
151
 
@@ -0,0 +1,7 @@
1
+ # Core Concepts
2
+
3
+ CoreLens is built on three main layers:
4
+
5
+ 1. **AoI (Area of Interest)**: The primary entry point. It represents a spatial boundary and acts as a gateway to all registered entities that intersect that boundary.
6
+ 2. **View**: A lazy, immutable query definition. Filtering by attributes (`where`), space (`spatial_filter`), or time (`between`) returns a new `View` without reading any Parquet data.
7
+ 3. **Result**: The lazy computation graph. Accessing `.static`, `.annual`, or `.sub_annual` on a `View` resolves the query plan and returns a `Result` object wrapping a Polars `LazyFrame`. Data is not actually read until you materialise it by calling `.df()` or `.gdf()`. This delays execution and reduces memory footprint, allowing the Polars streaming engine (automatically routed to the GPU if RAPIDS `cudf-polars` is installed) to optimize the query globally. All statistical operations, aggregations, and plotting are done on `Result` objects.
@@ -27,6 +27,6 @@ aoi = AoI("data/", tehsil="Pangi", district="Chamba")
27
27
 
28
28
  `core_lens` emits logs at different levels depending on the operational context:
29
29
 
30
- - **INFO**: Materialisation events (reading static, annual, or fortnightly Parquet files), entity registrations, AoI initialisation, and cross-entity spatial joins.
30
+ - **INFO**: Materialisation events (reading static, annual, or sub-annual Parquet files), entity registrations, AoI initialisation, and cross-entity spatial joins.
31
31
  - **DEBUG**: Sub-operations such as spatial bounding box index generation, exact geometry relationships, lazy attribute filters, and query generation.
32
32
  - **ERROR**: Incorrect initialisation arguments, failed path validations, missing geometries, unknown schema structures, or conflicting date configurations.
@@ -11,10 +11,10 @@ annual_data = aoi.mws.between("2010-01-01", "2023-12-31").annual
11
11
  # Season-based filtering (defaults to Kharif, Rabi, Zaid)
12
12
  from core_lens.base.view import Season
13
13
 
14
- kharif_2020 = aoi.mws.between(season=Season.KHARIF, year=2020).fortnightly
14
+ kharif_2020 = aoi.mws.between(season=Season.KHARIF, year=2020).sub_annual
15
15
 
16
16
  # Current season based on today's date
17
- current = aoi.mws.between(season=Season.CURRENT).fortnightly
17
+ current = aoi.mws.between(season=Season.CURRENT).sub_annual
18
18
  ```
19
19
 
20
20
  ## Aggregations & Derived Columns
@@ -31,10 +31,10 @@ derived_res = res_annual.derive(
31
31
  pl.when(pl.col("rainfall") < 500).then(1).otherwise(0)
32
32
  )
33
33
 
34
- # 2. Aggregate (temporal grouping like "month" requires fortnightly, but "year" works on annual too)
35
- res_fortnightly = aoi.mws.fortnightly
36
- monthly_avg = res_fortnightly.aggregate(pl.mean("ndvi"), by="month")
37
- seasonal_avg = res_fortnightly.aggregate(pl.mean("ndvi"), by="season_year")
34
+ # 2. Aggregate (temporal grouping like "month" requires sub-annual, but "year" works on annual too)
35
+ res_sub_annual = aoi.mws.sub_annual
36
+ monthly_avg = res_sub_annual.aggregate(pl.mean("ndvi"), by="month")
37
+ seasonal_avg = res_sub_annual.aggregate(pl.mean("ndvi"), by="season_year")
38
38
  ```
39
39
 
40
40
  ## Cross-Entity Spatial Joins
@@ -59,5 +59,5 @@ joined_view = aoi.mws.spatial_join(
59
59
  ```
60
60
 
61
61
  ```{note}
62
- Cross-entity spatial join execution (materialisation) is currently under development and will be added in a subsequent release. Calling materialisation methods (like `.annual`, `.static`, or `.fortnightly`) on a joined view will raise a `NotImplementedError` in the current version.
62
+ Cross-entity spatial join execution (materialisation) is currently under development and will be added in a subsequent release. Calling materialisation methods (like `.annual`, `.static`, or `.sub_annual`) on a joined view will raise a `NotImplementedError` in the current version.
63
63
  ```
@@ -107,7 +107,7 @@ similar = res.stats.similarity(
107
107
  target="13_551",
108
108
  columns={
109
109
  "rainfall": ("annual", {"year": 2018}),
110
- "ndvi": ("fortnightly", {"season": "kharif", "year": 2020})
110
+ "ndvi": ("sub_annual", {"season": "kharif", "year": 2020})
111
111
  },
112
112
  method=SimilarityMethod.EUCLIDEAN,
113
113
  top_n=10
@@ -16,13 +16,13 @@ Data layout expected
16
16
  │ └── mws.parquet ← GeoParquet with WKB geometry in "geom"
17
17
  ├── annual/
18
18
  │ └── ... ← Parquet partitioned by year
19
- └── fortnightly/
19
+ └── sub_annual/
20
20
  └── ... ← Parquet partitioned by basin and year
21
21
 
22
22
  Columns available:
23
23
  - Static: mws_id, tehsil, district, state, area_in_ha, ba_name, geometry, etc.
24
24
  - Annual: mws_id, year, dw_precipitation, dw_et, ci_cropping_intensity, etc.
25
- - Fortnightly: mws_id, date, df_precipitation, df_et, season, etc.
25
+ - SubAnnual: mws_id, date, df_precipitation, df_et, season, etc.
26
26
  """
27
27
 
28
28
  from __future__ import annotations
@@ -122,9 +122,9 @@ print("\nDataFrame head (static):")
122
122
  print(df_static.select(["mws_id", "tehsil", "district", "state", "area_in_ha"]).head(3))
123
123
 
124
124
 
125
- # ── 9. Temporal querying & Materialisation — .annual & .fortnightly ──────────
125
+ # ── 9. Temporal querying & Materialisation — .annual & .sub_annual ──────────
126
126
  # Use .between() to specify a time filter (year range, or season)
127
- # Then use .annual or .fortnightly to materialise
127
+ # Then use .annual or .sub_annual to materialise
128
128
 
129
129
  # Annual data for a range of years
130
130
  view_annual = view_barmer.between("2018-01-01", "2023-12-31")
@@ -133,13 +133,13 @@ print("\nAnnual result columns (first 10):", result_annual.columns[:10])
133
133
  print("Resolution :", result_annual.resolution)
134
134
  print("Has geometry :", result_annual.has_geometry) # False by default
135
135
 
136
- # Fortnightly data for a specific season and year
137
- result_fn = view_barmer.between(season=Season.KHARIF, year=2021).fortnightly
138
- print("\nFortnightly columns (first 10):", result_fn.columns[:10])
136
+ # SubAnnual data for a specific season and year
137
+ result_fn = view_barmer.between(season=Season.KHARIF, year=2021).sub_annual
138
+ print("\nSubAnnual columns (first 10):", result_fn.columns[:10])
139
139
 
140
140
 
141
141
  # ── 10. with_geometry() ──────────────────────────────────────────────────────
142
- # .annual and .fortnightly lack geometry to save memory. Use with_geometry()
142
+ # .annual and .sub_annual lack geometry to save memory. Use with_geometry()
143
143
  # to join geometry from the static table.
144
144
 
145
145
  result_annual_geo = result_annual.with_geometry()
@@ -174,7 +174,7 @@ print(desc_annual_by_entity.df().head(5))
174
174
 
175
175
 
176
176
  # ── 13. Stats — correlate ────────────────────────────────────────────────────
177
- # Pairwise correlations on fortnightly data
177
+ # Pairwise correlations on sub_annual data
178
178
  corr = result_fn.stats.correlate(
179
179
  columns=["df_precipitation", "df_et"],
180
180
  method=CorrelateMethod.PEARSON,
@@ -197,7 +197,7 @@ print(test_result.df())
197
197
 
198
198
 
199
199
  # ── 15. Stats — change ───────────────────────────────────────────────────────
200
- # Computes change over time. Perfect for annual/fortnightly results!
200
+ # Computes change over time. Perfect for annual/sub_annual results!
201
201
 
202
202
  change_result = result_annual.stats.change(
203
203
  column="dw_et",
@@ -302,12 +302,12 @@ print("GeoParquet written: output_mws.geoparquet")
302
302
  final = (
303
303
  aoi.mws.where(ba_name="Barmer")
304
304
  .between(season=Season.KHARIF, year=2021)
305
- .fortnightly.derive(
305
+ .sub_annual.derive(
306
306
  "et_ratio", (pl.col("df_et") / pl.col("df_precipitation")).round(3)
307
307
  )
308
308
  )
309
309
 
310
- print("\nFull pipeline (fortnightly) result shape:", final.data.shape)
310
+ print("\nFull pipeline (sub_annual) result shape:", final.data.shape)
311
311
  print("Columns:", final.columns[:10])
312
312
 
313
313
  top_et = final.df().sort("et_ratio", descending=True).head(5)
@@ -208,7 +208,7 @@ print(test_vs_ref.metadata)
208
208
 
209
209
 
210
210
  # ── 14. Stats — change ────────────────────────────────────────────────────────
211
- # Note: change() is most meaningful on annual/fortnightly data.
211
+ # Note: change() is most meaningful on annual/sub_annual data.
212
212
  # For static entities it still works if the data carries a numeric column
213
213
  # that can be compared between two named groups or derived states.
214
214
  # Here we illustrate absolute change between two derived values stored
@@ -254,7 +254,7 @@ print("Polygon-filtered count :", len(result_poly.df()))
254
254
 
255
255
 
256
256
  # ── 17. with_geometry() ───────────────────────────────────────────────────────
257
- # .static already has geometry. On annual/fortnightly results (other entities)
257
+ # .static already has geometry. On annual/sub_annual results (other entities)
258
258
  # you'd call this to join geometry back in. Demonstrated here for completeness.
259
259
 
260
260
  result_geo = result.with_geometry() # no-op since already has_geometry=True
@@ -4,11 +4,13 @@ dynamic = ["version"]
4
4
  description = "Query, analyse, and visualise CoreStack's microwatershed and Earth science data through a clean, composable Python API."
5
5
  readme = "README.md"
6
6
  authors = [{ name = "Apoorva Kashyap" }]
7
- requires-python = ">=3.13"
7
+ requires-python = ">=3.12"
8
8
  license = "GPL-3.0-or-later"
9
9
  license-files = ["LICEN[CS]E*"]
10
10
  classifiers = [
11
11
  "Programming Language :: Python :: 3",
12
+ "Programming Language :: Python :: 3.12",
13
+ "Programming Language :: Python :: 3.13",
12
14
  "Operating System :: POSIX :: Linux",
13
15
  "Development Status :: 2 - Pre-Alpha",
14
16
  "Topic :: Scientific/Engineering",
@@ -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.dev164'
22
- __version_tuple__ = version_tuple = (0, 1, 'dev164')
21
+ __version__ = version = '0.1.dev168'
22
+ __version_tuple__ = version_tuple = (0, 1, 'dev168')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -272,7 +272,7 @@ class AoI:
272
272
 
273
273
  ``AoI`` holds no data itself. Entity attributes are lazy
274
274
  :class:`~core_lens.base.view.View` objects; no Parquet I/O occurs until a
275
- materialisation property (``.static``, ``.annual``, ``.fortnightly``) is
275
+ materialisation property (``.static``, ``.annual``, ``.sub_annual``) is
276
276
  accessed on a View.
277
277
 
278
278
  **Registration** must happen before any ``AoI`` is constructed::
@@ -685,7 +685,7 @@ def _bbox_to_polygon(
685
685
  def _validate_entity_paths(entity: BaseEntity, name: str) -> None:
686
686
  """Validate an entity's path existence only — no Parquet I/O.
687
687
 
688
- Checks that ``static_path``, ``annual_path``, and ``fortnightly_path``
688
+ Checks that ``static_path``, ``annual_path``, and ``sub_annual_path``
689
689
  resolve to existing local files. Schema validation (key columns,
690
690
  geometry column, geometry type) is intentionally deferred to first data
691
691
  access via :attr:`BaseEntity.schema_profile`, which routes through the
@@ -727,7 +727,7 @@ def _validate_entity_paths(entity: BaseEntity, name: str) -> None:
727
727
  )
728
728
 
729
729
  # --- Temporal path existence checks (no schema read) --------------------
730
- for attr, label in [("annual_path", "annual"), ("fortnightly_path", "fortnightly")]:
730
+ for attr, label in [("annual_path", "annual"), ("sub_annual_path", "sub_annual")]:
731
731
  path = getattr(entity, attr)
732
732
  if path is not None:
733
733
  try:
@@ -77,7 +77,7 @@ def _cached_detect(
77
77
  key_cols: tuple[str, ...],
78
78
  geometry_col: str,
79
79
  annual_path: str | None,
80
- fortnightly_path: str | None,
80
+ sub_annual_path: str | None,
81
81
  storage_options_key: tuple[tuple[str, Any], ...],
82
82
  ) -> "SchemaProfile":
83
83
  """Cached wrapper around :func:`~core_lens.schema.detection.detect`.
@@ -96,7 +96,7 @@ def _cached_detect(
96
96
  key_cols=list(key_cols),
97
97
  geometry_col=geometry_col,
98
98
  annual_path=annual_path,
99
- fortnightly_path=fortnightly_path,
99
+ sub_annual_path=sub_annual_path,
100
100
  storage_options=dict(storage_options_key) or None,
101
101
  )
102
102
 
@@ -150,7 +150,7 @@ class BaseEntity(ABC):
150
150
  Subclasses **may** override:
151
151
 
152
152
  * :attr:`annual_path` — path to the annual time-series Parquet file
153
- * :attr:`fortnightly_path` — path to the fortnightly time-series Parquet file
153
+ * :attr:`sub_annual_path` — path to the sub_annual time-series Parquet file
154
154
  * :attr:`schema_profile` — override auto-detection by returning an
155
155
  explicit :class:`~core_lens.schema.profile.SchemaProfile`
156
156
 
@@ -176,7 +176,7 @@ class BaseEntity(ABC):
176
176
  1. ``static_path`` exists and is readable.
177
177
  2. ``key_cols`` are present and unique in the static file.
178
178
  3. ``geometry_col`` is present and contains valid geometries.
179
- 4. ``annual_path`` and ``fortnightly_path`` exist if declared.
179
+ 4. ``annual_path`` and ``sub_annual_path`` exist if declared.
180
180
 
181
181
  Any failure raises :class:`~core_lens.base.entity.EntityValidationError`.
182
182
  """
@@ -192,7 +192,7 @@ class BaseEntity(ABC):
192
192
  data_root (str | pathlib.Path | None, optional): Root data directory or cloud
193
193
  URI prefix (e.g. ``"s3://bucket/data"``). Relative
194
194
  :attr:`static_path`, :attr:`annual_path`, and
195
- :attr:`fortnightly_path` values are resolved against this
195
+ :attr:`sub_annual_path` values are resolved against this
196
196
  root. Defaults to ``None`` (resolved against cwd).
197
197
  storage_options (dict[str, Any] | None, optional): Cloud credential /
198
198
  configuration options forwarded to ``pyarrow.fs`` and
@@ -322,15 +322,15 @@ class BaseEntity(ABC):
322
322
  return None
323
323
 
324
324
  @property
325
- def fortnightly_path(self) -> str | None:
326
- """Path to the fortnightly time-series Parquet file, or ``None``.
325
+ def sub_annual_path(self) -> str | None:
326
+ """Path to the sub_annual time-series Parquet file, or ``None``.
327
327
 
328
- Override in subclasses that carry fortnightly temporal data. If
328
+ Override in subclasses that carry sub_annual temporal data. If
329
329
  declared, the file must exist at :meth:`AoI.register` time or
330
330
  :class:`EntityValidationError` is raised.
331
331
 
332
332
  Returns:
333
- str | None: A path string, or ``None`` if the entity has no fortnightly data.
333
+ str | None: A path string, or ``None`` if the entity has no sub_annual data.
334
334
 
335
335
  """
336
336
  return None
@@ -369,9 +369,9 @@ class BaseEntity(ABC):
369
369
  if self.annual_path is not None
370
370
  else None
371
371
  ),
372
- fortnightly_path=(
373
- self._resolve(self.fortnightly_path)
374
- if self.fortnightly_path is not None
372
+ sub_annual_path=(
373
+ self._resolve(self.sub_annual_path)
374
+ if self.sub_annual_path is not None
375
375
  else None
376
376
  ),
377
377
  storage_options_key=_so_key(_so),
@@ -659,7 +659,7 @@ class BaseEntity(ABC):
659
659
  r"""Return a lazy :class:`~core_lens.base.view.View` with a cross-entity join pending.
660
660
 
661
661
  The join is recorded in the View's ``join_spec`` and computed only at
662
- materialisation time (``.static``, ``.annual``, or ``.fortnightly``).
662
+ materialisation time (``.static``, ``.annual``, or ``.sub_annual``).
663
663
  Joined columns are namespaced as ``{entity_name}_{column_name}``.
664
664
 
665
665
  Args: