core-lens 0.1.dev172__tar.gz → 0.1.dev175__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.dev172 → core_lens-0.1.dev175}/PKG-INFO +1 -1
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/benchmarks/README.md +0 -1
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/benchmarks/bench_entity.py +0 -1
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/benchmarks/bench_polars_utils.py +1 -1
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/benchmarks/bench_result.py +5 -2
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/benchmarks/bench_schema.py +4 -2
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/benchmarks/bench_spatial.py +0 -1
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/benchmarks/bench_view.py +8 -5
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/benchmarks/run_all.sh +1 -1
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/_version.py +2 -2
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/base/namespaces/plot.py +32 -18
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/base/result.py +13 -2
- core_lens-0.1.dev172/benchmarks/bench_paths.py +0 -217
- core_lens-0.1.dev172/parse_scalene.py +0 -50
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/.github/ISSUE_TEMPLATE/blank-proposal.yaml +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/.github/ISSUE_TEMPLATE/bug-report.yaml +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/.github/ISSUE_TEMPLATE/feature-request.yaml +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/.github/pull_request_template.md +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/.github/workflows/ci.yml +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/.github/workflows/gh-pages.yml +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/.github/workflows/pre-release.yml +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/.github/workflows/release.yml +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/.gitignore +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/.gitmessage +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/.pre-commit-config.yaml +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/.python-version +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/CONTRIBUTING.md +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/LICENSE +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/README.md +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/SKILLS.md +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/benchmarks/bench_aoi.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/benchmarks/bench_export.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/benchmarks/bench_season.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/docs/Makefile +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/docs/make.bat +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/docs/source/concepts.md +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/docs/source/conf.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/docs/source/export.md +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/docs/source/index.rst +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/docs/source/intro.md +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/docs/source/logging.md +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/docs/source/plots.md +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/docs/source/plugins.md +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/docs/source/queries.md +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/docs/source/quickstart.md +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/docs/source/stats.md +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/examples/demo_mws.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/examples/demo_tehsil.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/hooks/mypy.sh +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/hooks/no-parquet-outside-fixtures.sh +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/hooks/pytest.sh +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/pyproject.toml +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/__init__.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/__main__.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/aoi.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/base/__init__.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/base/entity.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/base/namespaces/__init__.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/base/namespaces/stats.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/base/view.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/entities/__init__.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/entities/mws.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/entities/tehsil.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/entities/waterbody.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/export/__init__.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/export/formats.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/py.typed +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/schema/__init__.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/schema/detection.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/schema/profile.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/utils/__init__.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/utils/paths.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/utils/polars_utils.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/utils/season.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/utils/spatial.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/tests/fixtures/generate_fixtures.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/tests/unit/conftest.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/tests/unit/test_aoi.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/tests/unit/test_entities.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/tests/unit/test_entity.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/tests/unit/test_export.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/tests/unit/test_main.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/tests/unit/test_plot.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/tests/unit/test_polars_utils.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/tests/unit/test_profile.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/tests/unit/test_result.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/tests/unit/test_schema_detection.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/tests/unit/test_schema_profile.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/tests/unit/test_season.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/tests/unit/test_season_config.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/tests/unit/test_spatial.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/tests/unit/test_stats.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/tests/unit/test_view.py +0 -0
- {core_lens-0.1.dev172 → core_lens-0.1.dev175}/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.dev175
|
|
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
|
|
@@ -59,7 +59,6 @@ uv run python benchmarks/bench_spatial.py
|
|
|
59
59
|
| `bench_aoi.py` | `AoI.register`, `AoI.__init__` (bbox/geometry/named/seasons), `__getattr__`, `current_season/year`, `registered_entities`, `deregister` |
|
|
60
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
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
62
|
| `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
63
|
| `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
64
|
| `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 |
|
|
@@ -191,7 +191,7 @@ print(
|
|
|
191
191
|
# ── 7. cached_read_schema() — cold vs cached ──────────────────────────────────
|
|
192
192
|
_section("7. cached_read_schema() [cold vs functools.cache warm]")
|
|
193
193
|
from core_lens.utils.polars_utils import cached_read_schema # noqa: E402
|
|
194
|
-
from core_lens.utils.polars_utils import _cached_schema_internal #
|
|
194
|
+
from core_lens.utils.polars_utils import _cached_schema_internal # noqa: E402
|
|
195
195
|
|
|
196
196
|
# Warm the cache first (may already be warm from module import chain).
|
|
197
197
|
cached_read_schema(static_path)
|
|
@@ -188,7 +188,8 @@ print(
|
|
|
188
188
|
|
|
189
189
|
# ── 10. Result.aggregate(by='season') / by='month' / by='year_month' ─────────
|
|
190
190
|
_section("10. Result.aggregate() sub_annual groupings")
|
|
191
|
-
from core_lens.base.view import Season
|
|
191
|
+
from core_lens.base.view import Season # noqa: E402
|
|
192
|
+
|
|
192
193
|
result_fn = aoi_small.mws.between(season=Season.KHARIF, year=(2018, 2022)).sub_annual
|
|
193
194
|
|
|
194
195
|
data_col = result_fn.columns[-1] # last column (data column)
|
|
@@ -197,7 +198,9 @@ for by_key in ("month", "season", "year_month", "season_year", "year"):
|
|
|
197
198
|
t0 = time.perf_counter()
|
|
198
199
|
agg_r = result_fn.aggregate(pl.first(data_col), by=by_key)
|
|
199
200
|
t1 = time.perf_counter()
|
|
200
|
-
print(
|
|
201
|
+
print(
|
|
202
|
+
f"aggregate(by={by_key!r:12}) : {(t1 - t0) * 1000:.2f} ms shape={agg_r.df().shape}"
|
|
203
|
+
)
|
|
201
204
|
|
|
202
205
|
|
|
203
206
|
# ── 11. Result.aggregate() error paths ──────────────────────────────────
|
|
@@ -221,7 +221,7 @@ else:
|
|
|
221
221
|
|
|
222
222
|
# ── 9. _find_lon_companion() ─────────────────────────────────────────────────
|
|
223
223
|
_section("9. _find_lon_companion() [lat→lon heuristic — O(candidates)]")
|
|
224
|
-
import polars as pl # noqa: E401 (already imported; re-stated for Scalene trace clarity)
|
|
224
|
+
import polars as pl # noqa: E401, E402 (already imported; re-stated for Scalene trace clarity)
|
|
225
225
|
|
|
226
226
|
# Build a synthetic schema with lat + lon columns to exercise the positive branch.
|
|
227
227
|
synthetic_schema = pl.Schema(
|
|
@@ -265,7 +265,9 @@ print("_require_cols (ok) : no error (expect no error)")
|
|
|
265
265
|
|
|
266
266
|
# ── 11. SchemaProfile.from_file() — JSON round-trip ──────────────────────────
|
|
267
267
|
_section("11. SchemaProfile.from_file() [JSON round-trip]")
|
|
268
|
-
import json
|
|
268
|
+
import json # noqa: E402
|
|
269
|
+
import tempfile # noqa: E402
|
|
270
|
+
import pathlib # noqa: E402
|
|
269
271
|
|
|
270
272
|
profile2 = entity2.schema_profile
|
|
271
273
|
profile_dict = profile2.model_dump()
|
|
@@ -193,7 +193,6 @@ print(f"spatial_filter (geom) : {(t1 - t0) * 1000:.2f} ms")
|
|
|
193
193
|
|
|
194
194
|
# ── 9. execute_spatial_join() — cross-entity agg join ────────────────────────
|
|
195
195
|
_section("9. execute_spatial_join() [cross-entity agg on small bbox]")
|
|
196
|
-
import polars as pl # already imported at module top — re-imported for clarity
|
|
197
196
|
from core_lens.utils.spatial import execute_spatial_join # noqa: E402
|
|
198
197
|
|
|
199
198
|
# Use the small-AoI static data as primary and the same entity as secondary.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
=======================================================
|
|
3
3
|
Targets:
|
|
4
4
|
- View.where() attribute filter (Polars semi-join on static file)
|
|
5
|
-
- View.spatial_filter()
|
|
5
|
+
- View.spatial_filter() vectorized centroid test (or STRtree for area)
|
|
6
6
|
- View.between() time-filter dict construction (no I/O)
|
|
7
7
|
- View.between(season=CURRENT) current-season resolution
|
|
8
8
|
- View.between(year=tuple) year-range season filter
|
|
@@ -147,7 +147,9 @@ print(f"Shape : {result_annual.df().shape}")
|
|
|
147
147
|
# ── 11. sub_annual materialisation ───────────────────────────────────────────
|
|
148
148
|
_section("11. aoi_small.mws.sub_annual [sub_annual materialisation]")
|
|
149
149
|
t0 = time.perf_counter()
|
|
150
|
-
result_fn = aoi_small.mws.between(
|
|
150
|
+
result_fn = aoi_small.mws.between(
|
|
151
|
+
season=Season.KHARIF, year=2022
|
|
152
|
+
).sub_annual.materialise()
|
|
151
153
|
t1 = time.perf_counter()
|
|
152
154
|
print(f"sub_annual : {(t1 - t0) * 1000:.2f} ms")
|
|
153
155
|
print(f"Shape : {result_fn.df().shape}")
|
|
@@ -179,7 +181,6 @@ print(
|
|
|
179
181
|
|
|
180
182
|
# ── 14. View.between() error paths ───────────────────────────────────────────
|
|
181
183
|
_section("14. View.between() error paths [guard branches — no I/O]")
|
|
182
|
-
import traceback
|
|
183
184
|
|
|
184
185
|
errors_caught = 0
|
|
185
186
|
|
|
@@ -218,7 +219,7 @@ print(f"Error guards raised : {errors_caught}/5 (expect 5)")
|
|
|
218
219
|
|
|
219
220
|
# ── 15. View.spatial_join() — deferred cross-entity join spec ─────────────────
|
|
220
221
|
_section("15. View.spatial_join() [deferred spec — no I/O]")
|
|
221
|
-
from core_lens.entities.mws import MWSEntity as _MWS2
|
|
222
|
+
from core_lens.entities.mws import MWSEntity as _MWS2 # noqa: E402
|
|
222
223
|
|
|
223
224
|
other_entity = _MWS2(data_root=DATA_ROOT)
|
|
224
225
|
REPS_SJ = 10_000
|
|
@@ -234,7 +235,9 @@ print(
|
|
|
234
235
|
# 15b. Double spatial_join raises ValueError
|
|
235
236
|
duplicate_raised = False
|
|
236
237
|
try:
|
|
237
|
-
view_all.spatial_join(other_entity, {"x": "sum"}).spatial_join(
|
|
238
|
+
view_all.spatial_join(other_entity, {"x": "sum"}).spatial_join(
|
|
239
|
+
other_entity, {"y": "mean"}
|
|
240
|
+
)
|
|
238
241
|
except ValueError:
|
|
239
242
|
duplicate_raised = True
|
|
240
243
|
print(f"Double join guard : {duplicate_raised} (expect True)")
|
|
@@ -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.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 1, '
|
|
21
|
+
__version__ = version = '0.1.dev175'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 'dev175')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -731,31 +731,45 @@ class PlotNamespace:
|
|
|
731
731
|
)
|
|
732
732
|
|
|
733
733
|
entities = df.get_column(key_col).unique().to_list()
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
for c in hover_cols:
|
|
740
|
-
if c in row.columns:
|
|
741
|
-
lst = row.get_column(c).to_list()
|
|
742
|
-
if row.schema[c].is_numeric():
|
|
743
|
-
lst = [float("nan") if v is None else v for v in lst]
|
|
744
|
-
source_data[c] = lst
|
|
745
|
-
|
|
746
|
-
source = ColumnDataSource(source_data) # type: ignore[arg-type]
|
|
734
|
+
|
|
735
|
+
if len(entities) > 20:
|
|
736
|
+
# Fast path: single dataset, single colour, no legend
|
|
737
|
+
pdf = df.drop_nulls(subset=[x, y_col]).select(hover_cols).to_pandas()
|
|
738
|
+
source = ColumnDataSource(pdf)
|
|
747
739
|
fig.scatter(
|
|
748
740
|
x=x,
|
|
749
741
|
y=y_col,
|
|
750
742
|
source=source,
|
|
751
|
-
color=_color_for(
|
|
752
|
-
legend_label=str(entity),
|
|
743
|
+
color=_color_for(0),
|
|
753
744
|
size=8,
|
|
754
745
|
alpha=0.7,
|
|
755
746
|
)
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
747
|
+
else:
|
|
748
|
+
for i, entity in enumerate(entities):
|
|
749
|
+
row = df.filter(pl.col(key_col) == entity)
|
|
750
|
+
|
|
751
|
+
# Replace None with NaN for numeric columns to prevent Bokeh JS rendering crashes
|
|
752
|
+
source_data = {}
|
|
753
|
+
for c in hover_cols:
|
|
754
|
+
if c in row.columns:
|
|
755
|
+
lst = row.get_column(c).to_list()
|
|
756
|
+
if row.schema[c].is_numeric():
|
|
757
|
+
lst = [float("nan") if v is None else v for v in lst]
|
|
758
|
+
source_data[c] = lst
|
|
759
|
+
|
|
760
|
+
source = ColumnDataSource(source_data) # type: ignore[arg-type]
|
|
761
|
+
fig.scatter(
|
|
762
|
+
x=x,
|
|
763
|
+
y=y_col,
|
|
764
|
+
source=source,
|
|
765
|
+
color=_color_for(i),
|
|
766
|
+
legend_label=str(entity),
|
|
767
|
+
size=8,
|
|
768
|
+
alpha=0.7,
|
|
769
|
+
)
|
|
770
|
+
if fig.legend:
|
|
771
|
+
fig.legend.click_policy = "hide"
|
|
772
|
+
fig.add_layout(fig.legend[0], "right")
|
|
759
773
|
_apply_theme(fig, self.result, "Scatter Distribution")
|
|
760
774
|
return fig
|
|
761
775
|
|
|
@@ -279,12 +279,23 @@ class Result:
|
|
|
279
279
|
)
|
|
280
280
|
|
|
281
281
|
if by is None:
|
|
282
|
-
|
|
282
|
+
group_cols = self.key_cols
|
|
283
283
|
else:
|
|
284
284
|
# Temporal grouping columns are expected to already exist on the
|
|
285
285
|
# frame (added by the materialisation layer from the time column).
|
|
286
286
|
group_cols = self.key_cols + [by]
|
|
287
|
-
|
|
287
|
+
|
|
288
|
+
# Polars group_by throws DuplicateError if an aggregation expression
|
|
289
|
+
# outputs a column name that is already present in the grouping keys.
|
|
290
|
+
# Filter them out to prevent crashes when users iterate over all columns.
|
|
291
|
+
safe_exprs = [
|
|
292
|
+
expr
|
|
293
|
+
for expr in exprs
|
|
294
|
+
if getattr(expr, "meta", None) is None
|
|
295
|
+
or expr.meta.output_name() not in group_cols
|
|
296
|
+
]
|
|
297
|
+
|
|
298
|
+
new_data = self.data.group_by(group_cols).agg(*safe_exprs)
|
|
288
299
|
|
|
289
300
|
return self._replace(data=new_data)
|
|
290
301
|
|
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
"""bench_paths.py — Benchmark for core_lens.utils.paths
|
|
2
|
-
======================================================
|
|
3
|
-
Targets:
|
|
4
|
-
- is_cloud_uri() URI scheme prefix check (hot path helper)
|
|
5
|
-
- resolve_fs_and_path() URI → (FileSystem, path) resolution
|
|
6
|
-
- path_exists() local filesystem existence check
|
|
7
|
-
- join_uri() URI-agnostic path join (local + cloud)
|
|
8
|
-
|
|
9
|
-
Scalene focuses on:
|
|
10
|
-
- string prefix scanning in is_cloud_uri (Python vs C in str.startswith)
|
|
11
|
-
- pyarrow.fs.FileSystem.from_uri overhead for cloud URIs
|
|
12
|
-
- pathlib.Path normalisation cost in resolve_fs_and_path / join_uri
|
|
13
|
-
- pyarrow.fs.get_file_info cost for path_exists on local paths
|
|
14
|
-
|
|
15
|
-
Usage:
|
|
16
|
-
uv run scalene run --html --outfile benchmarks/profiles/bench_paths.html benchmarks/bench_paths.py
|
|
17
|
-
uv run python benchmarks/bench_paths.py
|
|
18
|
-
"""
|
|
19
|
-
|
|
20
|
-
from __future__ import annotations
|
|
21
|
-
|
|
22
|
-
import pathlib
|
|
23
|
-
import time
|
|
24
|
-
|
|
25
|
-
from core_lens.utils.paths import (
|
|
26
|
-
is_cloud_uri,
|
|
27
|
-
join_uri,
|
|
28
|
-
path_exists,
|
|
29
|
-
resolve_fs_and_path,
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
# ── Config ────────────────────────────────────────────────────────────────────
|
|
33
|
-
DATA_ROOT = "data/"
|
|
34
|
-
LOCAL_PATH = str(pathlib.Path(DATA_ROOT).resolve())
|
|
35
|
-
|
|
36
|
-
CLOUD_URIS = [
|
|
37
|
-
"s3://geo-benchmark-data/converted/mws/static",
|
|
38
|
-
]
|
|
39
|
-
LOCAL_URIS = [
|
|
40
|
-
LOCAL_PATH,
|
|
41
|
-
"/tmp/nonexistent_path",
|
|
42
|
-
"relative/path",
|
|
43
|
-
]
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def _section(title: str) -> None:
|
|
47
|
-
print(f"\n{'─' * 60}")
|
|
48
|
-
print(f" {title}")
|
|
49
|
-
print("─" * 60)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
# ── 1. is_cloud_uri() — positive path (cloud URIs) ────────────────────────────
|
|
53
|
-
_section("1. is_cloud_uri() [cloud URIs — positive match]")
|
|
54
|
-
REPS = 1_000_000
|
|
55
|
-
|
|
56
|
-
t0 = time.perf_counter()
|
|
57
|
-
for _ in range(REPS):
|
|
58
|
-
for uri in CLOUD_URIS:
|
|
59
|
-
is_cloud_uri(uri)
|
|
60
|
-
t1 = time.perf_counter()
|
|
61
|
-
per_call = (t1 - t0) / (REPS * len(CLOUD_URIS))
|
|
62
|
-
print(
|
|
63
|
-
f"is_cloud_uri (positive) ×{REPS * len(CLOUD_URIS)}: "
|
|
64
|
-
f"{(t1 - t0) * 1000:.2f} ms total ({per_call * 1e9:.1f} ns/call)"
|
|
65
|
-
)
|
|
66
|
-
for uri in CLOUD_URIS:
|
|
67
|
-
print(f" {uri[:40]!r:42} → {is_cloud_uri(uri)}")
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
# ── 2. is_cloud_uri() — negative path (local paths) ──────────────────────────
|
|
71
|
-
_section("2. is_cloud_uri() [local paths — negative match]")
|
|
72
|
-
t0 = time.perf_counter()
|
|
73
|
-
for _ in range(REPS):
|
|
74
|
-
for uri in LOCAL_URIS:
|
|
75
|
-
is_cloud_uri(uri)
|
|
76
|
-
t1 = time.perf_counter()
|
|
77
|
-
per_call = (t1 - t0) / (REPS * len(LOCAL_URIS))
|
|
78
|
-
print(
|
|
79
|
-
f"is_cloud_uri (negative) ×{REPS * len(LOCAL_URIS)}: "
|
|
80
|
-
f"{(t1 - t0) * 1000:.2f} ms total ({per_call * 1e9:.1f} ns/call)"
|
|
81
|
-
)
|
|
82
|
-
for uri in LOCAL_URIS:
|
|
83
|
-
print(f" {uri!r:42} → {is_cloud_uri(uri)}")
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
# ── 3. resolve_fs_and_path() — local path ─────────────────────────────────────
|
|
87
|
-
_section("3. resolve_fs_and_path() [local path — pathlib normalise]")
|
|
88
|
-
REPS_FS = 10_000
|
|
89
|
-
t0 = time.perf_counter()
|
|
90
|
-
for _ in range(REPS_FS):
|
|
91
|
-
resolve_fs_and_path(LOCAL_PATH)
|
|
92
|
-
t1 = time.perf_counter()
|
|
93
|
-
print(
|
|
94
|
-
f"resolve_fs (local) ×{REPS_FS}: {(t1 - t0) * 1000:.2f} ms total "
|
|
95
|
-
f"({(t1 - t0) / REPS_FS * 1e6:.2f} µs/call)"
|
|
96
|
-
)
|
|
97
|
-
fs, path = resolve_fs_and_path(LOCAL_PATH)
|
|
98
|
-
print(f" fs type : {type(fs).__name__}")
|
|
99
|
-
print(f" path : {path!r}")
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
# ── 4. resolve_fs_and_path() — cloud URI ──────────────────────────────────────
|
|
103
|
-
_section("4. resolve_fs_and_path() [cloud URI — lru_cache cold vs warm]")
|
|
104
|
-
from core_lens.utils.paths import _resolve_fs_and_path_cached # noqa: E402
|
|
105
|
-
|
|
106
|
-
cloud_uri = CLOUD_URIS[0] # s3:// URI
|
|
107
|
-
|
|
108
|
-
# Cold: flush cache, measure the first call (pafs.FileSystem.from_uri cost).
|
|
109
|
-
_resolve_fs_and_path_cached.cache_clear()
|
|
110
|
-
t0 = time.perf_counter()
|
|
111
|
-
resolve_fs_and_path(cloud_uri)
|
|
112
|
-
t1 = time.perf_counter()
|
|
113
|
-
t_cold_us = (t1 - t0) * 1e6
|
|
114
|
-
print(f"resolve_fs cloud (cold) : {t_cold_us:.2f} µs ← pafs.FileSystem.from_uri")
|
|
115
|
-
|
|
116
|
-
# Warm: same URI, now served from lru_cache.
|
|
117
|
-
REPS_CFS = 100_000
|
|
118
|
-
t0 = time.perf_counter()
|
|
119
|
-
for _ in range(REPS_CFS):
|
|
120
|
-
resolve_fs_and_path(cloud_uri)
|
|
121
|
-
t1 = time.perf_counter()
|
|
122
|
-
t_warm_us = (t1 - t0) / REPS_CFS * 1e6
|
|
123
|
-
print(
|
|
124
|
-
f"resolve_fs cloud (warm) ×{REPS_CFS}: {(t1 - t0) * 1000:.2f} ms total "
|
|
125
|
-
f"({t_warm_us:.3f} µs/call)"
|
|
126
|
-
)
|
|
127
|
-
print(f"Speedup (cold→warm) : {t_cold_us / t_warm_us:.0f}×")
|
|
128
|
-
print(f"Cache info : {_resolve_fs_and_path_cached.cache_info()}")
|
|
129
|
-
|
|
130
|
-
fs_c, path_c = resolve_fs_and_path(cloud_uri)
|
|
131
|
-
print(f" fs type : {type(fs_c).__name__}")
|
|
132
|
-
print(f" path : {path_c!r}")
|
|
133
|
-
|
|
134
|
-
# All four cloud URI schemes — each is a separate cache entry.
|
|
135
|
-
_section("4b. All cloud URI schemes [each scheme = distinct cache entry]")
|
|
136
|
-
for uri in CLOUD_URIS:
|
|
137
|
-
_resolve_fs_and_path_cached.cache_clear()
|
|
138
|
-
t0 = time.perf_counter()
|
|
139
|
-
resolve_fs_and_path(uri)
|
|
140
|
-
t1 = time.perf_counter()
|
|
141
|
-
print(f" cold {uri[:45]!r:47} : {(t1 - t0) * 1e6:.1f} µs")
|
|
142
|
-
print(f"Cache info (after 4 URIs): {_resolve_fs_and_path_cached.cache_info()}")
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
# ── 5. path_exists() — local path (exists) ────────────────────────────────────
|
|
146
|
-
_section("5. path_exists() [local — directory exists]")
|
|
147
|
-
REPS_PE = 1_000
|
|
148
|
-
t0 = time.perf_counter()
|
|
149
|
-
for _ in range(REPS_PE):
|
|
150
|
-
path_exists(LOCAL_PATH)
|
|
151
|
-
t1 = time.perf_counter()
|
|
152
|
-
print(
|
|
153
|
-
f"path_exists (exists) ×{REPS_PE}: {(t1 - t0) * 1000:.2f} ms total "
|
|
154
|
-
f"({(t1 - t0) / REPS_PE * 1e6:.2f} µs/call)"
|
|
155
|
-
)
|
|
156
|
-
print(f" exists: {path_exists(LOCAL_PATH)!r}")
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
# ── 6. path_exists() — local path (absent) ────────────────────────────────────
|
|
160
|
-
_section("6. path_exists() [local — path does not exist]")
|
|
161
|
-
t0 = time.perf_counter()
|
|
162
|
-
for _ in range(REPS_PE):
|
|
163
|
-
path_exists("/tmp/core_lens_nonexistent_xyz_123")
|
|
164
|
-
t1 = time.perf_counter()
|
|
165
|
-
print(
|
|
166
|
-
f"path_exists (absent) ×{REPS_PE}: {(t1 - t0) * 1000:.2f} ms total "
|
|
167
|
-
f"({(t1 - t0) / REPS_PE * 1e6:.2f} µs/call)"
|
|
168
|
-
)
|
|
169
|
-
print(f" exists: {path_exists('/tmp/core_lens_nonexistent_xyz_123')!r}")
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
# ── 7. join_uri() — local root ────────────────────────────────────────────────
|
|
173
|
-
_section("7. join_uri() [local root — pathlib join]")
|
|
174
|
-
REPS_JU = 1_000_000
|
|
175
|
-
t0 = time.perf_counter()
|
|
176
|
-
for _ in range(REPS_JU):
|
|
177
|
-
join_uri(LOCAL_PATH, "mws/static")
|
|
178
|
-
t1 = time.perf_counter()
|
|
179
|
-
print(
|
|
180
|
-
f"join_uri (local) ×{REPS_JU}: {(t1 - t0) * 1000:.2f} ms total "
|
|
181
|
-
f"({(t1 - t0) / REPS_JU * 1e9:.1f} ns/call)"
|
|
182
|
-
)
|
|
183
|
-
print(f" result: {join_uri(LOCAL_PATH, 'mws/static')!r}")
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
# ── 8. join_uri() — cloud root ────────────────────────────────────────────────
|
|
187
|
-
_section("8. join_uri() [cloud root — string concat]")
|
|
188
|
-
cloud_root = "s3://my-bucket/data"
|
|
189
|
-
t0 = time.perf_counter()
|
|
190
|
-
for _ in range(REPS_JU):
|
|
191
|
-
join_uri(cloud_root, "mws/static")
|
|
192
|
-
t1 = time.perf_counter()
|
|
193
|
-
print(
|
|
194
|
-
f"join_uri (cloud) ×{REPS_JU}: {(t1 - t0) * 1000:.2f} ms total "
|
|
195
|
-
f"({(t1 - t0) / REPS_JU * 1e9:.1f} ns/call)"
|
|
196
|
-
)
|
|
197
|
-
print(f" result: {join_uri(cloud_root, 'mws/static')!r}")
|
|
198
|
-
|
|
199
|
-
# Head-to-head: local vs cloud join.
|
|
200
|
-
t_local = time.perf_counter()
|
|
201
|
-
for _ in range(REPS_JU):
|
|
202
|
-
join_uri(LOCAL_PATH, "mws/static")
|
|
203
|
-
t_local = time.perf_counter() - t_local
|
|
204
|
-
|
|
205
|
-
t_cloud = time.perf_counter()
|
|
206
|
-
for _ in range(REPS_JU):
|
|
207
|
-
join_uri(cloud_root, "mws/static")
|
|
208
|
-
t_cloud = time.perf_counter() - t_cloud
|
|
209
|
-
|
|
210
|
-
print(
|
|
211
|
-
f"\nHead-to-head ×{REPS_JU}: local={t_local * 1000:.2f} ms "
|
|
212
|
-
f"cloud={t_cloud * 1000:.2f} ms "
|
|
213
|
-
f"(cloud is {t_cloud / t_local:.2f}× {'faster' if t_cloud < t_local else 'slower'})"
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
print("\n✓ bench_paths.py complete")
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import glob
|
|
3
|
-
import os
|
|
4
|
-
|
|
5
|
-
profiles = glob.glob('/home/akashyap54/workstation/Projects/benchmarks/profiles/*_profile.json')
|
|
6
|
-
|
|
7
|
-
for profile in profiles:
|
|
8
|
-
name = os.path.basename(profile).replace('_profile.json', '')
|
|
9
|
-
try:
|
|
10
|
-
d = json.load(open(profile))
|
|
11
|
-
except Exception as e:
|
|
12
|
-
print(f"Error loading {name}: {e}")
|
|
13
|
-
continue
|
|
14
|
-
|
|
15
|
-
print(f"\n# {name.upper()}")
|
|
16
|
-
print("-" * 40)
|
|
17
|
-
|
|
18
|
-
line_stats = []
|
|
19
|
-
|
|
20
|
-
for filename, f_data in d.get('files', {}).items():
|
|
21
|
-
short_filename = os.path.basename(filename)
|
|
22
|
-
for line_data in f_data.get('lines', []):
|
|
23
|
-
cpu_percent = line_data.get('n_cpu_percent_c', 0) + line_data.get('n_cpu_percent_python', 0)
|
|
24
|
-
malloc_mb = line_data.get('n_malloc_mb', 0)
|
|
25
|
-
peak_mb = line_data.get('n_peak_mb', 0)
|
|
26
|
-
line_str = line_data.get('line', '').strip()
|
|
27
|
-
lineno = line_data.get('lineno', 0)
|
|
28
|
-
|
|
29
|
-
if cpu_percent > 0 or malloc_mb > 0 or peak_mb > 0:
|
|
30
|
-
line_stats.append({
|
|
31
|
-
'file': short_filename,
|
|
32
|
-
'lineno': lineno,
|
|
33
|
-
'line': line_str[:60] + ('...' if len(line_str) > 60 else ''),
|
|
34
|
-
'cpu': cpu_percent,
|
|
35
|
-
'malloc_mb': malloc_mb,
|
|
36
|
-
'peak_mb': peak_mb
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
# Top 5 by CPU
|
|
40
|
-
print(f"Top 5 lines by CPU %:")
|
|
41
|
-
top_cpu = sorted(line_stats, key=lambda x: x['cpu'], reverse=True)[:5]
|
|
42
|
-
for x in top_cpu:
|
|
43
|
-
print(f" {x['file']}:{x['lineno']} | CPU: {x['cpu']:.1f}% | Line: {x['line']}")
|
|
44
|
-
|
|
45
|
-
# Top 5 by Malloc
|
|
46
|
-
print(f"\nTop 5 lines by Memory Allocation (MB):")
|
|
47
|
-
top_mem = sorted(line_stats, key=lambda x: x['malloc_mb'], reverse=True)[:5]
|
|
48
|
-
for x in top_mem:
|
|
49
|
-
if x['malloc_mb'] > 0:
|
|
50
|
-
print(f" {x['file']}:{x['lineno']} | Malloc: {x['malloc_mb']:.1f} MB | Line: {x['line']}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|