buildingdata 0.6.0__tar.gz → 0.7.0__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 (45) hide show
  1. {buildingdata-0.6.0 → buildingdata-0.7.0}/PKG-INFO +1 -1
  2. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata/simulation/bdtopo.py +53 -7
  3. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata/simulation/bdtopo_bulk.py +13 -12
  4. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata/tests/test_bulk.py +77 -1
  5. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata.egg-info/PKG-INFO +1 -1
  6. {buildingdata-0.6.0 → buildingdata-0.7.0}/pyproject.toml +1 -1
  7. {buildingdata-0.6.0 → buildingdata-0.7.0}/LICENSE +0 -0
  8. {buildingdata-0.6.0 → buildingdata-0.7.0}/README.md +0 -0
  9. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata/__init__.py +0 -0
  10. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata/_cli.py +0 -0
  11. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata/bulk.py +0 -0
  12. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata/cache.py +0 -0
  13. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata/config.py +0 -0
  14. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata/exceptions.py +0 -0
  15. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata/gcs.py +0 -0
  16. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata/reference/__init__.py +0 -0
  17. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata/reference/census.py +0 -0
  18. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata/reference/diagnosis.py +0 -0
  19. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata/reference/districts.py +0 -0
  20. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata/reference/elecdom.py +0 -0
  21. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata/reference/elmas.py +0 -0
  22. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata/reference/enedis.py +0 -0
  23. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata/reference/gas_network.py +0 -0
  24. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata/reference/occupant_diaries.py +0 -0
  25. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata/reference/ore.py +0 -0
  26. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata/simulation/__init__.py +0 -0
  27. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata/simulation/_epw.py +0 -0
  28. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata/simulation/era5.py +0 -0
  29. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata/simulation/era5_bulk.py +0 -0
  30. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata/tests/__init__.py +0 -0
  31. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata/tests/conftest.py +0 -0
  32. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata/tests/test_cache.py +0 -0
  33. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata/tests/test_config.py +0 -0
  34. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata/tests/test_gcs.py +0 -0
  35. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata/tests/test_pipeline_diagnosis.py +0 -0
  36. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata/tests/test_public_api.py +0 -0
  37. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata/tests/test_reference.py +0 -0
  38. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata/tests/test_simulation.py +0 -0
  39. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata/validation.py +0 -0
  40. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata.egg-info/SOURCES.txt +0 -0
  41. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata.egg-info/dependency_links.txt +0 -0
  42. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata.egg-info/entry_points.txt +0 -0
  43. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata.egg-info/requires.txt +0 -0
  44. {buildingdata-0.6.0 → buildingdata-0.7.0}/buildingdata.egg-info/top_level.txt +0 -0
  45. {buildingdata-0.6.0 → buildingdata-0.7.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: buildingdata
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: Data management layer for buildingmodel — reference data download, BDTOPO retrieval, ERA5 weather
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.10
@@ -55,6 +55,47 @@ _TIMEOUT = 120 # seconds
55
55
  # Server-side maximum WFS page size. Requesting more is silently capped here.
56
56
  _PAGE_SIZE = 5000
57
57
 
58
+ # Minimum share of a footprint's area that must fall inside the IRIS polygon for
59
+ # the footprint to belong to that IRIS. At 0.5 a footprint belongs to whichever
60
+ # IRIS holds most of it -- and, in the measure-zero case of an exactly even
61
+ # split, to both, which is the pre-existing behaviour for a shared border
62
+ # building and the safe side of the tie (a strict > 0.5 would drop it from
63
+ # every IRIS).
64
+ #
65
+ # The BBOX query returns everything around the IRIS, so the result has to be
66
+ # filtered down to the IRIS itself. Filtering on `intersects` keeps every
67
+ # footprint straddling the boundary, and a consumer that attributes each
68
+ # footprint to the district holding most of its area then sees a handful of
69
+ # *neighbouring* district codes in what is meant to be one IRIS's stock --
70
+ # enough, in buildingmodel, for one straddling footprint to import a whole
71
+ # neighbouring IRIS's census housing stock (buildingmodel#55). A footprint
72
+ # belongs to the IRIS holding at least half of it, which is the same rule
73
+ # buildingcalibration's BuildingLoader applies downstream.
74
+ _MEMBERSHIP_THRESHOLD = 0.5
75
+
76
+
77
+ def _filter_to_district(gdf, district_geom):
78
+ """Keep the footprints the IRIS polygon holds at least half of.
79
+
80
+ Zero-area geometries (points, lines -- test doubles and the occasional
81
+ degenerate BDTOPO record) have no area share to compute, so they fall back
82
+ to plain intersection.
83
+
84
+ Args:
85
+ gdf (geopandas.GeoDataFrame): candidate buildings, EPSG:2154.
86
+ district_geom (shapely geometry): the IRIS polygon, EPSG:2154.
87
+
88
+ Returns:
89
+ geopandas.GeoDataFrame: the subset belonging to the IRIS.
90
+ """
91
+ area = gdf.geometry.area
92
+ inside = gdf.geometry.intersection(district_geom)
93
+ share = inside.area.where(area > 0.0, 0.0) / area.where(area > 0.0, 1.0)
94
+ belongs = (share >= _MEMBERSHIP_THRESHOLD) | (
95
+ (area <= 0.0) & gdf.geometry.intersects(district_geom)
96
+ )
97
+ return gdf[belongs]
98
+
58
99
 
59
100
  def _fetch_all_wfs_features(base_url, iris_code):
60
101
  """Page a BDTOPO WFS GetFeature query to completion.
@@ -157,8 +198,8 @@ def _fetch_wfs(iris_code, district_geom):
157
198
  district_geom (shapely geometry): the IRIS polygon (EPSG:2154).
158
199
 
159
200
  Returns:
160
- geopandas.GeoDataFrame: buildings intersecting the district polygon,
161
- EPSG:2154, raw French attribute names.
201
+ geopandas.GeoDataFrame: the buildings the district polygon holds at
202
+ least half of, EPSG:2154, raw French attribute names.
162
203
 
163
204
  Raises:
164
205
  RemoteNotAvailableError: on a non-200 WFS status or an empty result.
@@ -182,13 +223,18 @@ def _fetch_wfs(iris_code, district_geom):
182
223
 
183
224
  require_columns(gdf, _REQUIRED_COLUMNS, f"BDTOPO buildings (IRIS {iris_code})")
184
225
 
185
- # Filter to keep only buildings that intersect with the district geometry
186
- return gdf[gdf.geometry.intersects(district_geom)].copy()
226
+ return _filter_to_district(gdf, district_geom).copy()
187
227
 
188
228
 
189
229
  def get_bdtopo(iris_code, refresh=False, source="auto"):
190
230
  """Return BDTOPO buildings for one IRIS, from the bulk cache or the WFS.
191
231
 
232
+ A footprint belongs to the IRIS holding at least half of its area, so a
233
+ footprint straddling the boundary no longer carries a neighbouring
234
+ district code into the result. Note this tightened in 0.7.0 (it was a
235
+ plain ``intersects`` before): a cache written by an earlier version still
236
+ holds the straddlers, pass ``refresh=True`` once to rewrite it.
237
+
192
238
  The GeoDataFrame is cached as a parquet file under
193
239
  ~/.cache/buildingdata/bdtopo/bdtopo_iris_<iris_code>.parquet — the same
194
240
  cache file whichever source produced it. Column names are the raw French
@@ -212,8 +258,8 @@ def get_bdtopo(iris_code, refresh=False, source="auto"):
212
258
  ``nombre_de_logements`` a dwelling count.
213
259
 
214
260
  Returns:
215
- geopandas.GeoDataFrame: BDTOPO buildings in EPSG:2154, raw French
216
- attribute names preserved.
261
+ geopandas.GeoDataFrame: the BDTOPO buildings the IRIS holds at least
262
+ half of, EPSG:2154, raw French attribute names preserved.
217
263
 
218
264
  Raises:
219
265
  ValueError: if ``source`` is not one of "auto", "bulk", "wfs".
@@ -267,7 +313,7 @@ def get_bdtopo(iris_code, refresh=False, source="auto"):
267
313
  # Lock-free bulk read; read_department raises CacheError with the
268
314
  # prefetch hint when source="bulk" and the partition is missing.
269
315
  gdf = bdtopo_bulk.read_department(department, bbox=district_geom.bounds)
270
- gdf = gdf[gdf.geometry.intersects(district_geom)].copy().reset_index(drop=True)
316
+ gdf = _filter_to_district(gdf, district_geom).copy().reset_index(drop=True)
271
317
  require_columns(gdf, _REQUIRED_COLUMNS, f"BDTOPO buildings (IRIS {iris_code})")
272
318
 
273
319
  with file_lock(lock_file):
@@ -44,7 +44,7 @@ from ..exceptions import (
44
44
  RemoteNotAvailableError,
45
45
  )
46
46
  from ..validation import require_columns
47
- from .bdtopo import _REQUIRED_COLUMNS
47
+ from .bdtopo import _REQUIRED_COLUMNS, _filter_to_district
48
48
 
49
49
  # IGN Géoplateforme téléchargement endpoints (confirmed live 2026-08-06):
50
50
  # * catalog: Atom feed at <resource>?zone=D0XX&page=N, paged via the
@@ -523,9 +523,11 @@ def get_bdtopo_bulk(iris_codes):
523
523
  Groups the requested IRIS by département, then per département performs a
524
524
  single bbox-pushdown partition read over the codes' combined bounds and a
525
525
  single spatial join against all the IRIS polygons — sharing the (costly)
526
- file scan across the IRIS instead of paying it once per IRIS. A building
527
- straddling two requested IRIS appears in both results (same semantics as
528
- the per-IRIS ``intersects`` filter of :func:`~.bdtopo.get_bdtopo`).
526
+ file scan across the IRIS instead of paying it once per IRIS. A footprint
527
+ belongs to the IRIS holding at least half of its area, so a building
528
+ straddling two requested IRIS appears in the one holding most of it, and
529
+ in both only when the split is exactly even (same semantics as
530
+ :func:`~.bdtopo.get_bdtopo`).
529
531
 
530
532
  Source: local GeoParquet partitions built by :func:`prefetch_bdtopo` from
531
533
  the IGN départemental editions (vintage recorded in the bulk manifest);
@@ -536,9 +538,9 @@ def get_bdtopo_bulk(iris_codes):
536
538
  départements.
537
539
 
538
540
  Returns:
539
- dict[str, geopandas.GeoDataFrame]: IRIS code -> buildings intersecting
540
- that IRIS, EPSG:2154, raw French attribute names, same schema as the
541
- WFS path (no join helper columns).
541
+ dict[str, geopandas.GeoDataFrame]: IRIS code -> the buildings that IRIS
542
+ holds at least half of, EPSG:2154, raw French attribute names, same
543
+ schema as the WFS path (no join helper columns).
542
544
 
543
545
  Raises:
544
546
  BuildingDataError: if an IRIS code is unknown to the districts layer.
@@ -569,11 +571,10 @@ def get_bdtopo_bulk(iris_codes):
569
571
  joined = gpd.sjoin(gdf, join_frame, how="inner", predicate="intersects")
570
572
  drop = [c for c in ("_iris", "index_right") if c in joined.columns]
571
573
  for code in dep_codes:
572
- sub = (
573
- joined[joined["_iris"] == code]
574
- .drop(columns=drop)
575
- .reset_index(drop=True)
576
- )
574
+ sub = joined[joined["_iris"] == code].drop(columns=drop)
575
+ sub = _filter_to_district(
576
+ sub, dep_sel[dep_sel["district"] == code].geometry.iloc[0]
577
+ ).reset_index(drop=True)
577
578
  require_columns(
578
579
  sub, _REQUIRED_COLUMNS, f"BDTOPO bulk buildings (IRIS {code})"
579
580
  )
@@ -530,7 +530,8 @@ def test_get_bdtopo_bulk_splits_multi_iris_and_shares_border_buildings(
530
530
  assert set(results) == {IRIS_A, IRIS_B}
531
531
  assert sorted(results[IRIS_A]["hauteur"]) == [9.0, 12.0]
532
532
  assert sorted(results[IRIS_B]["hauteur"]) == [6.0, 12.0]
533
- # The border building (hauteur=12) appears in both IRIS frames.
533
+ # The border building (hauteur=12) is split exactly evenly between the two
534
+ # IRIS, so the >= 0.5 membership rule leaves it in both frames.
534
535
  for frame in results.values():
535
536
  assert 12.0 in set(frame["hauteur"])
536
537
  # sjoin helper columns must not leak into the results.
@@ -539,6 +540,39 @@ def test_get_bdtopo_bulk_splits_multi_iris_and_shares_border_buildings(
539
540
  assert frame.crs.to_epsg() == 2154
540
541
 
541
542
 
543
+ def test_get_bdtopo_bulk_gives_a_border_building_to_the_iris_holding_most_of_it(
544
+ monkeypatch, tmp_path
545
+ ):
546
+ """A footprint 70 % inside IRIS_A must not appear in IRIS_B's stock.
547
+
548
+ Filtering on `intersects` used to hand every straddling footprint to both
549
+ IRIS, which lets a consumer read a neighbouring district code out of what
550
+ is meant to be one IRIS's building stock (buildingmodel#55).
551
+ """
552
+ frame = gpd.GeoDataFrame(
553
+ {
554
+ "hauteur": [9.0],
555
+ "nombre_de_logements": [4],
556
+ "usage_1": ["Résidentiel"],
557
+ },
558
+ # 100 m wide, 70 m of it on the IRIS_A side of x = 652000.
559
+ geometry=[box(651930, 6860400, 652030, 6860500)],
560
+ crs="EPSG:2154",
561
+ )
562
+ frame.to_parquet(bdtopo_bulk.partition_path(DEP), write_covering_bbox=True)
563
+ bulk.update_manifest(
564
+ "bdtopo", DEP, {"status": "complete", "vintage": "BDTOPO_TEST_EDITION"}
565
+ )
566
+ monkeypatch.setattr(
567
+ "buildingdata.reference.districts.get_districts", _bulk_districts
568
+ )
569
+
570
+ results = bdtopo_bulk.get_bdtopo_bulk([IRIS_A, IRIS_B])
571
+
572
+ assert sorted(results[IRIS_A]["hauteur"]) == [9.0]
573
+ assert results[IRIS_B].empty
574
+
575
+
542
576
  def test_get_bdtopo_bulk_rejects_unknown_iris(monkeypatch):
543
577
  monkeypatch.setattr(
544
578
  "buildingdata.reference.districts.get_districts", _bulk_districts
@@ -581,6 +615,48 @@ def test_bdtopo_auto_falls_back_to_wfs_without_a_partition(monkeypatch):
581
615
  assert len(gdf) == 1
582
616
 
583
617
 
618
+ def test_bdtopo_wfs_drops_a_footprint_mostly_outside_the_iris(monkeypatch):
619
+ """The BBOX query reaches past the IRIS; only its own footprints come back.
620
+
621
+ A footprint whose majority lies in the neighbouring IRIS used to be kept by
622
+ the `intersects` filter, and downstream carried that neighbour's district
623
+ code into the frame (buildingmodel#55).
624
+ """
625
+ monkeypatch.setattr(bdtopo_mod, "get_districts", _bulk_districts)
626
+ collection = {
627
+ "features": [
628
+ {
629
+ "type": "Feature",
630
+ "geometry": {
631
+ "type": "Polygon",
632
+ # 100 m wide, only 30 m of it on the IRIS_A side.
633
+ "coordinates": [
634
+ [
635
+ [651970.0, 6860400.0],
636
+ [652070.0, 6860400.0],
637
+ [652070.0, 6860500.0],
638
+ [651970.0, 6860500.0],
639
+ [651970.0, 6860400.0],
640
+ ]
641
+ ],
642
+ },
643
+ "properties": {
644
+ "hauteur": 9.0,
645
+ "nombre_de_logements": 4,
646
+ "usage_1": "Résidentiel",
647
+ },
648
+ }
649
+ ]
650
+ }
651
+ monkeypatch.setattr(
652
+ bdtopo_mod.requests, "get", Mock(return_value=FakeResponse(200, collection))
653
+ )
654
+
655
+ gdf = bdtopo_mod.get_bdtopo(IRIS_A, source="wfs")
656
+
657
+ assert gdf.empty
658
+
659
+
584
660
  def test_bdtopo_source_bulk_without_partition_raises_with_prefetch_hint(monkeypatch):
585
661
  monkeypatch.setattr(bdtopo_mod, "get_districts", _bulk_districts)
586
662
  with pytest.raises(CacheError, match="prefetch bdtopo"):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: buildingdata
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: Data management layer for buildingmodel — reference data download, BDTOPO retrieval, ERA5 weather
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.10
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "buildingdata"
7
- version = "0.6.0"
7
+ version = "0.7.0"
8
8
  description = "Data management layer for buildingmodel — reference data download, BDTOPO retrieval, ERA5 weather"
9
9
 
10
10
  readme = "README.md"
File without changes
File without changes
File without changes