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.
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/PKG-INFO +1 -1
- core_lens-0.1.dev172/benchmarks/README.md +77 -0
- core_lens-0.1.dev172/benchmarks/bench_entity.py +308 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/benchmarks/bench_export.py +65 -2
- core_lens-0.1.dev172/benchmarks/bench_paths.py +217 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/benchmarks/bench_polars_utils.py +28 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/benchmarks/bench_result.py +92 -3
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/benchmarks/bench_schema.py +102 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/benchmarks/bench_spatial.py +69 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/benchmarks/bench_view.py +96 -5
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/benchmarks/run_all.sh +2 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/docs/source/queries.md +3 -3
- core_lens-0.1.dev172/parse_scalene.py +50 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/pyproject.toml +3 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/__init__.py +2 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/__main__.py +12 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/_version.py +2 -2
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/aoi.py +21 -1
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/base/entity.py +2 -2
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/base/namespaces/__init__.py +2 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/base/namespaces/plot.py +26 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/base/namespaces/stats.py +5 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/base/result.py +12 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/base/view.py +15 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/entities/__init__.py +2 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/entities/mws.py +2 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/entities/tehsil.py +2 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/entities/waterbody.py +2 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/export/__init__.py +2 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/export/formats.py +23 -8
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/utils/__init__.py +2 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/utils/paths.py +72 -8
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/utils/polars_utils.py +1 -1
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/utils/season.py +23 -6
- core_lens-0.1.dev172/tests/unit/test_season.py +251 -0
- core_lens-0.1.dev169/benchmarks/README.md +0 -60
- core_lens-0.1.dev169/tests/unit/test_season.py +0 -130
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/.github/ISSUE_TEMPLATE/blank-proposal.yaml +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/.github/ISSUE_TEMPLATE/bug-report.yaml +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/.github/ISSUE_TEMPLATE/feature-request.yaml +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/.github/pull_request_template.md +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/.github/workflows/ci.yml +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/.github/workflows/gh-pages.yml +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/.github/workflows/pre-release.yml +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/.github/workflows/release.yml +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/.gitignore +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/.gitmessage +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/.pre-commit-config.yaml +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/.python-version +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/CONTRIBUTING.md +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/LICENSE +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/README.md +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/SKILLS.md +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/benchmarks/bench_aoi.py +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/benchmarks/bench_season.py +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/docs/Makefile +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/docs/make.bat +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/docs/source/concepts.md +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/docs/source/conf.py +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/docs/source/export.md +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/docs/source/index.rst +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/docs/source/intro.md +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/docs/source/logging.md +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/docs/source/plots.md +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/docs/source/plugins.md +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/docs/source/quickstart.md +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/docs/source/stats.md +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/examples/demo_mws.py +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/examples/demo_tehsil.py +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/hooks/mypy.sh +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/hooks/no-parquet-outside-fixtures.sh +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/hooks/pytest.sh +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/base/__init__.py +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/py.typed +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/schema/__init__.py +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/schema/detection.py +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/schema/profile.py +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/src/core_lens/utils/spatial.py +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/tests/fixtures/generate_fixtures.py +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/tests/unit/conftest.py +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/tests/unit/test_aoi.py +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/tests/unit/test_entities.py +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/tests/unit/test_entity.py +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/tests/unit/test_export.py +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/tests/unit/test_main.py +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/tests/unit/test_plot.py +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/tests/unit/test_polars_utils.py +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/tests/unit/test_profile.py +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/tests/unit/test_result.py +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/tests/unit/test_schema_detection.py +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/tests/unit/test_schema_profile.py +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/tests/unit/test_season_config.py +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/tests/unit/test_spatial.py +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/tests/unit/test_stats.py +0 -0
- {core_lens-0.1.dev169 → core_lens-0.1.dev172}/tests/unit/test_view.py +0 -0
- {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.
|
|
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)
|