core-lens 0.1.dev169__tar.gz → 0.1.dev172__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 (96) hide show
  1. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/PKG-INFO +1 -1
  2. core_lens-0.1.dev172/benchmarks/README.md +77 -0
  3. core_lens-0.1.dev172/benchmarks/bench_entity.py +308 -0
  4. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/benchmarks/bench_export.py +65 -2
  5. core_lens-0.1.dev172/benchmarks/bench_paths.py +217 -0
  6. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/benchmarks/bench_polars_utils.py +28 -0
  7. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/benchmarks/bench_result.py +92 -3
  8. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/benchmarks/bench_schema.py +102 -0
  9. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/benchmarks/bench_spatial.py +69 -0
  10. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/benchmarks/bench_view.py +96 -5
  11. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/benchmarks/run_all.sh +2 -0
  12. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/docs/source/queries.md +3 -3
  13. core_lens-0.1.dev172/parse_scalene.py +50 -0
  14. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/pyproject.toml +3 -0
  15. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/__init__.py +2 -0
  16. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/__main__.py +12 -0
  17. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/_version.py +2 -2
  18. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/aoi.py +21 -1
  19. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/base/entity.py +2 -2
  20. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/base/namespaces/__init__.py +2 -0
  21. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/base/namespaces/plot.py +26 -0
  22. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/base/namespaces/stats.py +5 -0
  23. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/base/result.py +12 -0
  24. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/base/view.py +15 -0
  25. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/entities/__init__.py +2 -0
  26. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/entities/mws.py +2 -0
  27. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/entities/tehsil.py +2 -0
  28. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/entities/waterbody.py +2 -0
  29. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/export/__init__.py +2 -0
  30. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/export/formats.py +23 -8
  31. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/utils/__init__.py +2 -0
  32. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/utils/paths.py +72 -8
  33. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/utils/polars_utils.py +1 -1
  34. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/utils/season.py +23 -6
  35. core_lens-0.1.dev172/tests/unit/test_season.py +251 -0
  36. core_lens-0.1.dev169/benchmarks/README.md +0 -60
  37. core_lens-0.1.dev169/tests/unit/test_season.py +0 -130
  38. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/.github/ISSUE_TEMPLATE/blank-proposal.yaml +0 -0
  39. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/.github/ISSUE_TEMPLATE/bug-report.yaml +0 -0
  40. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/.github/ISSUE_TEMPLATE/feature-request.yaml +0 -0
  41. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/.github/pull_request_template.md +0 -0
  42. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/.github/workflows/ci.yml +0 -0
  43. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/.github/workflows/gh-pages.yml +0 -0
  44. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/.github/workflows/pre-release.yml +0 -0
  45. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/.github/workflows/release.yml +0 -0
  46. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/.gitignore +0 -0
  47. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/.gitmessage +0 -0
  48. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/.pre-commit-config.yaml +0 -0
  49. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/.python-version +0 -0
  50. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/CONTRIBUTING.md +0 -0
  51. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/LICENSE +0 -0
  52. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/README.md +0 -0
  53. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/SKILLS.md +0 -0
  54. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/benchmarks/bench_aoi.py +0 -0
  55. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/benchmarks/bench_season.py +0 -0
  56. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/docs/Makefile +0 -0
  57. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/docs/make.bat +0 -0
  58. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/docs/source/concepts.md +0 -0
  59. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/docs/source/conf.py +0 -0
  60. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/docs/source/export.md +0 -0
  61. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/docs/source/index.rst +0 -0
  62. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/docs/source/intro.md +0 -0
  63. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/docs/source/logging.md +0 -0
  64. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/docs/source/plots.md +0 -0
  65. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/docs/source/plugins.md +0 -0
  66. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/docs/source/quickstart.md +0 -0
  67. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/docs/source/stats.md +0 -0
  68. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/examples/demo_mws.py +0 -0
  69. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/examples/demo_tehsil.py +0 -0
  70. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/hooks/mypy.sh +0 -0
  71. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/hooks/no-parquet-outside-fixtures.sh +0 -0
  72. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/hooks/pytest.sh +0 -0
  73. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/base/__init__.py +0 -0
  74. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/py.typed +0 -0
  75. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/schema/__init__.py +0 -0
  76. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/schema/detection.py +0 -0
  77. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/schema/profile.py +0 -0
  78. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/utils/spatial.py +0 -0
  79. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/tests/fixtures/generate_fixtures.py +0 -0
  80. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/tests/unit/conftest.py +0 -0
  81. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/tests/unit/test_aoi.py +0 -0
  82. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/tests/unit/test_entities.py +0 -0
  83. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/tests/unit/test_entity.py +0 -0
  84. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/tests/unit/test_export.py +0 -0
  85. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/tests/unit/test_main.py +0 -0
  86. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/tests/unit/test_plot.py +0 -0
  87. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/tests/unit/test_polars_utils.py +0 -0
  88. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/tests/unit/test_profile.py +0 -0
  89. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/tests/unit/test_result.py +0 -0
  90. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/tests/unit/test_schema_detection.py +0 -0
  91. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/tests/unit/test_schema_profile.py +0 -0
  92. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/tests/unit/test_season_config.py +0 -0
  93. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/tests/unit/test_spatial.py +0 -0
  94. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/tests/unit/test_stats.py +0 -0
  95. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/tests/unit/test_view.py +0 -0
  96. {core_lens-0.1.dev169 → core_lens-0.1.dev172}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: core-lens
3
- Version: 0.1.dev169
3
+ Version: 0.1.dev172
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,77 @@
1
+ # core-lens Benchmarks
2
+
3
+ Focused Scalene profiling scripts — one per module/subsystem.
4
+
5
+ ## Structure
6
+
7
+ ```
8
+ benchmarks/
9
+ ├── README.md
10
+ ├── run_all.sh # Run all benchmarks in sequence
11
+ ├── bench_aoi.py # AoI init, register, named-boundary resolution, current_season/year
12
+ ├── bench_entity.py # BaseEntity._resolve, where, spatial_filter, spatial_join, caches [NEW]
13
+ ├── bench_spatial.py # build_bbox_index, bbox_intersects_geometry, exact_spatial_filter, execute_spatial_join
14
+ ├── bench_paths.py # is_cloud_uri, resolve_fs_and_path, path_exists, join_uri [NEW]
15
+ ├── bench_view.py # View.where, spatial_filter, between (all modes + errors), spatial_join, materialise
16
+ ├── bench_result.py # Result.derive, aggregate (all groupings + errors), with_geometry, df/gdf, stats/plot
17
+ ├── bench_season.py # resolve_time_filter, add_temporal_columns, SeasonConfig.season_for
18
+ ├── bench_schema.py # schema detection (_infer_* helpers, _is_year_col, _find_lon_companion, from_file)
19
+ ├── bench_polars_utils.py # scan_with_key_filter, collect_lf, cached_read_schema (CPU path)
20
+ └── bench_export.py # parquet, json, csv, geoparquet, geojson, GeoJSONSeq, error paths
21
+ ```
22
+
23
+ ## Running
24
+
25
+ ### Single benchmark
26
+
27
+ ```bash
28
+ uv run scalene --html --outfile benchmarks/profiles/bench_spatial.html benchmarks/bench_spatial.py
29
+ ```
30
+
31
+ ### All benchmarks (sequential, JSON output via Scalene)
32
+
33
+ ```bash
34
+ chmod +x benchmarks/run_all.sh
35
+ ./benchmarks/run_all.sh
36
+ ```
37
+
38
+ Profiles land in `benchmarks/profiles/`.
39
+
40
+ ### Quick timing-only run (no Scalene overhead)
41
+
42
+ ```bash
43
+ uv run python benchmarks/bench_spatial.py
44
+ ```
45
+
46
+ ## What to look for in Scalene output
47
+
48
+ | Signal | Meaning |
49
+ |---|---|
50
+ | High **CPU %** on line | Compute bottleneck — vectorise or push into Polars expr |
51
+ | High **memory** spike | Possible copy-on-write or large intermediate frame |
52
+ | `[native]` vs `[Python]` | Native = C ext (fast). Python = pure Python (optimise) |
53
+ | **GPU %** column | cudf path hit or not |
54
+
55
+ ## Coverage map
56
+
57
+ | File | Functions / paths covered |
58
+ |---|---|
59
+ | `bench_aoi.py` | `AoI.register`, `AoI.__init__` (bbox/geometry/named/seasons), `__getattr__`, `current_season/year`, `registered_entities`, `deregister` |
60
+ | `bench_entity.py` | `_resolve` (local/cloud), `where` (single/composite/list/error), `spatial_filter` (bbox/geometry/area/error), `spatial_join`, `schema_profile` cache, `_index` cache, `geometry_lazy`, `_so_key`, `_entity_name`, `_cached_detect`, `EntityValidationError` |
61
+ | `bench_spatial.py` | `build_bbox_index`, `bbox_intersects_geometry`, `exact_spatial_filter` (centroid/area/all-India), `execute_spatial_join`, `_bbox_sidecar_path`, `_read_bbox_sidecar`, error path |
62
+ | `bench_paths.py` | `is_cloud_uri` (positive/negative), `resolve_fs_and_path` (local/cloud), `path_exists` (exists/absent), `join_uri` (local/cloud) |
63
+ | `bench_view.py` | `where` (single/composite), `spatial_filter` (bbox), `between` (dates/season/CURRENT/year-range/errors), `spatial_join` (spec/double-error), `static`/`annual`/`sub_annual` materialisation |
64
+ | `bench_result.py` | `df`, `lazy`, `gdf` (WKB decode), `materialise`, `derive` (single/chain), `with_geometry` (noop/annual), `aggregate` (no-by/year/month/season/year_month/season_year/errors), `gdf` TypeError guard, `stats`/`plot` accessors |
65
+ | `bench_season.py` | `SeasonConfig.__post_init__` (default/custom), `season_for`, `resolve_time_filter` (date-range/kharif/rabi/year-range), `add_temporal_columns` (first/second noop call), Polars filter execution |
66
+ | `bench_schema.py` | `_read_schema`, `_infer_geometry_type`, `_infer_bbox_cols`, `_infer_time_col`, `_is_year_col_from_schema`, `_find_lon_companion`, `_require_cols` (ok/error), `detect` (static/full), `SchemaProfile.from_file`, `entity.schema_profile` (cold/warm) |
67
+ | `bench_polars_utils.py` | `_gpu_available`, `scan_with_key_filter` (build/+time_expr), `collect_lf` (1-key/small/all), `cached_read_schema` (warm/cold) |
68
+ | `bench_export.py` | `_get_duckdb_conn` (cold/cached), `parquet`/`json`/`csv`/`geoparquet`/`geojson` (small/all), `geojson(GeoJSONSeq)`, all error paths, format size comparison |
69
+
70
+ ## Prerequisites
71
+
72
+ ```bash
73
+ uv sync --group dev
74
+ ```
75
+
76
+ Each script needs `DATA_ROOT` pointing at a directory with the expected entity Parquet files.
77
+ Edit the `DATA_ROOT` constant at the top of each file to match your local data path.
@@ -0,0 +1,308 @@
1
+ """bench_entity.py — Benchmark for core_lens.base.entity.BaseEntity
2
+ ===================================================================
3
+ Targets:
4
+ - BaseEntity._resolve() absolute / relative / cloud URI resolution
5
+ - BaseEntity.where() attribute filter (single col, composite, list val)
6
+ - BaseEntity.where() entity-kwarg spatial lookup path
7
+ - BaseEntity.spatial_filter() full pipeline (bbox + geometry mode)
8
+ - BaseEntity.spatial_join() deferred join spec construction
9
+ - BaseEntity.schema_profile instance-level cache (hasattr guard)
10
+ - BaseEntity._index process-level cache (_cached_build_index)
11
+ - BaseEntity.geometry_lazy lazy geometry scan construction
12
+ - _so_key() storage_options → hashable tuple
13
+ - _entity_name() entity class name → string
14
+ - _cached_detect() process-level schema cache (functools.cache)
15
+ - _cached_build_index() process-level bbox index cache
16
+ - EntityValidationError exception class (import overhead)
17
+
18
+ Scalene focuses on:
19
+ - _resolve() pathlib.Path ops vs cloud URI fast-path
20
+ - cached_read_schema() in where() — hot path schema lookup
21
+ - bbox_intersects_geometry() → exact_spatial_filter() inside where()
22
+ - functools.cache lookup overhead in _cached_detect / _cached_build_index
23
+
24
+ Usage:
25
+ uv run scalene run --html --outfile benchmarks/profiles/bench_entity.html benchmarks/bench_entity.py
26
+ uv run python benchmarks/bench_entity.py
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import time
32
+
33
+ import shapely.geometry as sgeom
34
+
35
+ from core_lens import AoI
36
+ from core_lens.entities.mws import MWSEntity
37
+ from core_lens.base.entity import (
38
+ _so_key,
39
+ _entity_name,
40
+ _cached_detect,
41
+ _cached_build_index,
42
+ EntityValidationError,
43
+ )
44
+
45
+ # ── Config ────────────────────────────────────────────────────────────────────
46
+ DATA_ROOT = "data/"
47
+ INDIA_BBOX: tuple[float, float, float, float] = (68.0, 6.0, 98.0, 38.0)
48
+ SMALL_BBOX: tuple[float, float, float, float] = (86.0, 21.0, 87.5, 22.0)
49
+ MWS_ID = "12_322881" # update to a valid mws_id in your dataset
50
+
51
+ AoI.register(MWSEntity)
52
+ entity = MWSEntity(data_root=DATA_ROOT)
53
+ static_path = entity._resolve(entity.static_path)
54
+
55
+
56
+ def _section(title: str) -> None:
57
+ print(f"\n{'─' * 60}")
58
+ print(f" {title}")
59
+ print("─" * 60)
60
+
61
+
62
+ # ── 1. _so_key() — storage_options → hashable tuple ──────────────────────────
63
+ _section("1. _so_key() [dict → sorted tuple — O(n log n)]")
64
+ REPS = 1_000_000
65
+
66
+ so_empty: dict[str, str] = {}
67
+ so_aws: dict[str, str] = {"region": "ap-south-1", "access_key": "AKIAIOSFODNN7EXAMPLE"}
68
+
69
+ t0 = time.perf_counter()
70
+ for _ in range(REPS):
71
+ _so_key(so_empty)
72
+ t1 = time.perf_counter()
73
+ print(
74
+ f"_so_key (empty) ×{REPS}: {(t1 - t0) * 1000:.2f} ms total "
75
+ f"({(t1 - t0) / REPS * 1e9:.1f} ns/call)"
76
+ )
77
+
78
+ t0 = time.perf_counter()
79
+ for _ in range(REPS):
80
+ _so_key(so_aws)
81
+ t1 = time.perf_counter()
82
+ print(
83
+ f"_so_key (2 keys) ×{REPS}: {(t1 - t0) * 1000:.2f} ms total "
84
+ f"({(t1 - t0) / REPS * 1e9:.1f} ns/call)"
85
+ )
86
+ print(f" _so_key(aws): {_so_key(so_aws)}")
87
+
88
+
89
+ # ── 2. _entity_name() — class → string ───────────────────────────────────────
90
+ _section("2. _entity_name() [class → name string, 'Entity' suffix strip]")
91
+ REPS_EN = 1_000_000
92
+ t0 = time.perf_counter()
93
+ for _ in range(REPS_EN):
94
+ _entity_name(MWSEntity)
95
+ t1 = time.perf_counter()
96
+ print(
97
+ f"_entity_name ×{REPS_EN}: {(t1 - t0) * 1000:.2f} ms total "
98
+ f"({(t1 - t0) / REPS_EN * 1e9:.1f} ns/call)"
99
+ )
100
+ print(f" _entity_name(MWSEntity): {_entity_name(MWSEntity)!r} (expect 'mws')")
101
+
102
+
103
+ # ── 3. BaseEntity._resolve() — local relative path ───────────────────────────
104
+ _section("3. BaseEntity._resolve() [relative → absolute local path]")
105
+ REPS_R = 100_000
106
+ t0 = time.perf_counter()
107
+ for _ in range(REPS_R):
108
+ entity._resolve(entity.static_path)
109
+ t1 = time.perf_counter()
110
+ print(
111
+ f"_resolve (relative) ×{REPS_R}: {(t1 - t0) * 1000:.2f} ms total "
112
+ f"({(t1 - t0) / REPS_R * 1e6:.2f} µs/call)"
113
+ )
114
+ print(f" resolved: {entity._resolve(entity.static_path)!r}")
115
+
116
+ # Cloud URI fast-path (no pathlib.Path, no existence check).
117
+ entity_cloud_sim = MWSEntity.__new__(MWSEntity) # bypass __init__ for testing
118
+ entity_cloud_sim._data_root = "s3://my-bucket/data"
119
+ entity_cloud_sim._storage_options = {}
120
+ REPS_CR = 100_000
121
+ t0 = time.perf_counter()
122
+ for _ in range(REPS_CR):
123
+ entity_cloud_sim._resolve("s3://my-bucket/data/mws/static/")
124
+ t1 = time.perf_counter()
125
+ print(
126
+ f"_resolve (cloud abs) ×{REPS_CR}: {(t1 - t0) * 1000:.2f} ms total "
127
+ f"({(t1 - t0) / REPS_CR * 1e6:.2f} µs/call)"
128
+ )
129
+
130
+
131
+ # ── 4. BaseEntity.schema_profile — instance cache ────────────────────────────
132
+ _section("4. BaseEntity.schema_profile [hasattr guard — instance-level cache]")
133
+ entity2 = MWSEntity(data_root=DATA_ROOT)
134
+ t0 = time.perf_counter()
135
+ entity2.schema_profile # cold — triggers _cached_detect
136
+ t1 = time.perf_counter()
137
+ print(f"schema_profile (cold) : {(t1 - t0) * 1000:.2f} ms")
138
+
139
+ REPS_SP = 100_000
140
+ t0 = time.perf_counter()
141
+ for _ in range(REPS_SP):
142
+ entity2.schema_profile # warm — hasattr guard short-circuits
143
+ t1 = time.perf_counter()
144
+ print(
145
+ f"schema_profile (warm) ×{REPS_SP}: {(t1 - t0) * 1000:.2f} ms total "
146
+ f"({(t1 - t0) / REPS_SP * 1e6:.3f} µs/call)"
147
+ )
148
+
149
+
150
+ # ── 5. _cached_detect() — process-level functools.cache ──────────────────────
151
+ _section("5. _cached_detect() [process-level functools.cache lookup]")
152
+ profile = entity.schema_profile
153
+ key = (
154
+ static_path,
155
+ tuple(entity.key_cols),
156
+ entity.geometry_col,
157
+ entity._resolve(entity.annual_path) if entity.annual_path else None,
158
+ entity._resolve(entity.sub_annual_path) if entity.sub_annual_path else None,
159
+ _so_key(entity._storage_options),
160
+ )
161
+ # Prime the cache.
162
+ _cached_detect(*key)
163
+
164
+ REPS_CD = 100_000
165
+ t0 = time.perf_counter()
166
+ for _ in range(REPS_CD):
167
+ _cached_detect(*key)
168
+ t1 = time.perf_counter()
169
+ print(
170
+ f"_cached_detect (warm) ×{REPS_CD}: {(t1 - t0) * 1000:.2f} ms total "
171
+ f"({(t1 - t0) / REPS_CD * 1e6:.3f} µs/call)"
172
+ )
173
+ print(f"Cache info: {_cached_detect.cache_info()}")
174
+
175
+
176
+ # ── 6. BaseEntity._index — process-level bbox cache ──────────────────────────
177
+ _section("6. BaseEntity._index [process-level bbox index cache]")
178
+ t0 = time.perf_counter()
179
+ idx = entity._index # may be warm from earlier modules
180
+ t1 = time.perf_counter()
181
+ print(f"_index (first access) : {(t1 - t0) * 1000:.2f} ms")
182
+ print(f"Index shape : {idx.shape}")
183
+
184
+ REPS_IX = 100_000
185
+ t0 = time.perf_counter()
186
+ for _ in range(REPS_IX):
187
+ entity._index # warm — hasattr guard short-circuits
188
+ t1 = time.perf_counter()
189
+ print(
190
+ f"_index (cached) ×{REPS_IX}: {(t1 - t0) * 1000:.2f} ms total "
191
+ f"({(t1 - t0) / REPS_IX * 1e6:.3f} µs/call)"
192
+ )
193
+
194
+
195
+ # ── 7. BaseEntity.geometry_lazy — lazy geometry LazyFrame ────────────────────
196
+ _section("7. BaseEntity.geometry_lazy [lazy scan construction + hasattr cache]")
197
+ entity3 = MWSEntity(data_root=DATA_ROOT)
198
+ t0 = time.perf_counter()
199
+ lf = entity3.geometry_lazy # cold — builds pl.scan_parquet plan
200
+ t1 = time.perf_counter()
201
+ print(f"geometry_lazy (cold) : {(t1 - t0) * 1000:.2f} ms")
202
+
203
+ REPS_GL = 100_000
204
+ t0 = time.perf_counter()
205
+ for _ in range(REPS_GL):
206
+ entity3.geometry_lazy # warm — hasattr guard short-circuits
207
+ t1 = time.perf_counter()
208
+ print(
209
+ f"geometry_lazy (warm) ×{REPS_GL}: {(t1 - t0) * 1000:.2f} ms total "
210
+ f"({(t1 - t0) / REPS_GL * 1e6:.3f} µs/call)"
211
+ )
212
+
213
+
214
+ # ── 8. BaseEntity.where() — single attribute filter ──────────────────────────
215
+ _section("8. BaseEntity.where(state='Bihar') [single attribute filter]")
216
+ t0 = time.perf_counter()
217
+ view_attr = entity.where(state="Bihar")
218
+ t1 = time.perf_counter()
219
+ print(f"where(state) : {(t1 - t0) * 1000:.2f} ms")
220
+ print(f"Bihar keys : {view_attr.keys.collect().shape}")
221
+
222
+
223
+ # ── 9. BaseEntity.where() — composite attribute filter ───────────────────────
224
+ _section("9. BaseEntity.where(state=..., district=..., tehsil=...) [composite AND]")
225
+ t0 = time.perf_counter()
226
+ view_composite = entity.where(state="Bihar", district="Banka", tehsil="Banka")
227
+ t1 = time.perf_counter()
228
+ print(f"where(3 cols) : {(t1 - t0) * 1000:.2f} ms")
229
+ print(f"Banka keys : {view_composite.keys.collect().shape}")
230
+
231
+
232
+ # ── 10. BaseEntity.where() — list value (is_in) ──────────────────────────────
233
+ _section("10. BaseEntity.where(state=['Bihar', 'Jharkhand']) [list value → is_in]")
234
+ t0 = time.perf_counter()
235
+ view_list = entity.where(state=["Bihar", "Jharkhand"])
236
+ t1 = time.perf_counter()
237
+ print(f"where(list) : {(t1 - t0) * 1000:.2f} ms")
238
+ print(f"Bihar+Jharkhand keys : {view_list.keys.collect().shape}")
239
+
240
+
241
+ # ── 11. BaseEntity.where() — unknown kwarg error ─────────────────────────────
242
+ _section("11. BaseEntity.where() [unknown kwarg → ValueError guard]")
243
+ err_raised = False
244
+ try:
245
+ entity.where(totally_unknown_column="xyz")
246
+ except ValueError:
247
+ err_raised = True
248
+ print(f"ValueError guard : {err_raised} (expect True)")
249
+
250
+
251
+ # ── 12. BaseEntity.spatial_filter() — bbox mode ──────────────────────────────
252
+ _section("12. BaseEntity.spatial_filter(bbox=SMALL_BBOX) [full pipeline]")
253
+ t0 = time.perf_counter()
254
+ view_sf = entity.spatial_filter(bbox=SMALL_BBOX)
255
+ t1 = time.perf_counter()
256
+ print(f"spatial_filter (bbox): {(t1 - t0) * 1000:.2f} ms")
257
+ print(f"Keys : {view_sf.keys.collect().shape}")
258
+
259
+ # geometry mode
260
+ small_poly = sgeom.box(*SMALL_BBOX)
261
+ t0 = time.perf_counter()
262
+ view_sf2 = entity.spatial_filter(geometry=small_poly)
263
+ t1 = time.perf_counter()
264
+ print(f"spatial_filter (geom): {(t1 - t0) * 1000:.2f} ms")
265
+
266
+ # area relationship
267
+ t0 = time.perf_counter()
268
+ view_sf_area = entity.spatial_filter(
269
+ bbox=SMALL_BBOX, relationship="area", threshold=0.3
270
+ )
271
+ t1 = time.perf_counter()
272
+ print(
273
+ f"spatial_filter (area): {(t1 - t0) * 1000:.2f} ms keys={view_sf_area.keys.collect().shape}"
274
+ )
275
+
276
+
277
+ # ── 13. BaseEntity.spatial_join() — deferred spec ────────────────────────────
278
+ _section("13. BaseEntity.spatial_join() [deferred spec construction — no I/O]")
279
+ REPS_SJ = 10_000
280
+ t0 = time.perf_counter()
281
+ for _ in range(REPS_SJ):
282
+ entity.spatial_join(other=entity, agg={"area_in_ha": "sum"})
283
+ t1 = time.perf_counter()
284
+ print(
285
+ f"spatial_join (spec) ×{REPS_SJ}: {(t1 - t0) * 1000:.2f} ms total "
286
+ f"({(t1 - t0) / REPS_SJ * 1e6:.2f} µs/call)"
287
+ )
288
+
289
+
290
+ # ── 14. EntityValidationError — import / raise overhead ──────────────────────
291
+ _section("14. EntityValidationError [raise + catch overhead]")
292
+ REPS_EV = 100_000
293
+ t0 = time.perf_counter()
294
+ for _ in range(REPS_EV):
295
+ try:
296
+ raise EntityValidationError("bench test")
297
+ except EntityValidationError:
298
+ pass
299
+ t1 = time.perf_counter()
300
+ print(
301
+ f"raise+catch ×{REPS_EV}: {(t1 - t0) * 1000:.2f} ms total "
302
+ f"({(t1 - t0) / REPS_EV * 1e6:.2f} µs/call)"
303
+ )
304
+
305
+
306
+ # ── Cleanup ───────────────────────────────────────────────────────────────────
307
+ AoI.deregister(MWSEntity)
308
+ print("\n✓ bench_entity.py complete")
@@ -42,8 +42,8 @@ AoI.register(MWSEntity)
42
42
  aoi_all = AoI(DATA_ROOT, bbox=INDIA_BBOX)
43
43
  aoi_small = AoI(DATA_ROOT, bbox=SMALL_BBOX)
44
44
 
45
- result_all = aoi_all.mws.static # full dataset — geometry present
46
- result_small = aoi_small.mws.static # small subset
45
+ result_all = aoi_all.mws.static.materialise() # full dataset — geometry present
46
+ result_small = aoi_small.mws.static.materialise() # small subset
47
47
 
48
48
  print(f"All result : {result_all.df().shape}")
49
49
  print(f"Small result : {result_small.df().shape}")
@@ -205,6 +205,69 @@ with tempfile.TemporaryDirectory() as tmpdir:
205
205
  f"Ratio : geojson is {t_gj / t_gpq:.2f}× {'slower' if t_gj > t_gpq else 'faster'}"
206
206
  )
207
207
 
208
+ # ── 8. Export error paths — geometry guards ───────────────────────────────
209
+ _section("8. Export error paths [TypeError guards — no write, O(1)]")
210
+
211
+ errs = 0
212
+
213
+ # parquet() with geometry raises TypeError.
214
+ try:
215
+ parquet(result_small, tmp / "err.parquet")
216
+ except TypeError:
217
+ errs += 1
218
+
219
+ # json() with geometry raises TypeError.
220
+ try:
221
+ json(result_small, tmp / "err.json")
222
+ except TypeError:
223
+ errs += 1
224
+
225
+ # csv() with geometry raises TypeError.
226
+ try:
227
+ csv(result_small, tmp / "err.csv")
228
+ except TypeError:
229
+ errs += 1
230
+
231
+ # geoparquet() without geometry raises TypeError.
232
+ try:
233
+ geoparquet(result_no_geom, tmp / "err.geoparquet")
234
+ except TypeError:
235
+ errs += 1
236
+
237
+ # geojson() without geometry raises TypeError.
238
+ try:
239
+ geojson(result_no_geom, tmp / "err.geojson")
240
+ except TypeError:
241
+ errs += 1
242
+
243
+ print(f"TypeError guards raised : {errs}/5 (expect 5)")
244
+
245
+ # ── 9. GeoJSONSeq (newline-delimited GeoJSON) driver ─────────────────────
246
+ _section("9. geojson(driver='GeoJSONSeq') [streaming newline-delimited GeoJSON]")
247
+ out_gjseq = tmp / "bench.geojsonl"
248
+ t0 = time.perf_counter()
249
+ for _ in range(REPS_IO):
250
+ geojson(result_small, out_gjseq, driver="GeoJSONSeq")
251
+ t1 = time.perf_counter()
252
+ sz = out_gjseq.stat().st_size
253
+ print(
254
+ f"GeoJSONSeq (small) ×{REPS_IO}: {(t1 - t0) * 1000:.2f} ms total "
255
+ f"({(t1 - t0) / REPS_IO * 1000:.2f} ms/call) → {sz // 1024} KB"
256
+ )
257
+
258
+ # ── 10. Format comparison summary (small dataset) ─────────────────────────
259
+ _section("10. Format size comparison [small dataset]")
260
+ sizes = {
261
+ "parquet": (tmp / "bench.parquet").stat().st_size,
262
+ "json": (tmp / "bench.json").stat().st_size,
263
+ "csv": (tmp / "bench.csv").stat().st_size,
264
+ "geoparquet": (tmp / "bench.geoparquet").stat().st_size,
265
+ "geojson": (tmp / "bench.geojson").stat().st_size,
266
+ "geojsonseq": (tmp / "bench.geojsonl").stat().st_size,
267
+ }
268
+ for fmt, sz_bytes in sorted(sizes.items(), key=lambda x: x[1]):
269
+ print(f" {fmt:12} : {sz_bytes // 1024:6} KB")
270
+
208
271
 
209
272
  # ── Cleanup ───────────────────────────────────────────────────────────────────
210
273
  AoI.deregister(MWSEntity)