buildingdata 0.5.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.
- {buildingdata-0.5.0 → buildingdata-0.7.0}/PKG-INFO +1 -1
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata/reference/diagnosis.py +11 -1
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata/simulation/bdtopo.py +53 -7
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata/simulation/bdtopo_bulk.py +13 -12
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata/tests/test_bulk.py +77 -1
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata/tests/test_pipeline_diagnosis.py +88 -3
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata.egg-info/PKG-INFO +1 -1
- {buildingdata-0.5.0 → buildingdata-0.7.0}/pyproject.toml +1 -1
- {buildingdata-0.5.0 → buildingdata-0.7.0}/LICENSE +0 -0
- {buildingdata-0.5.0 → buildingdata-0.7.0}/README.md +0 -0
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata/__init__.py +0 -0
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata/_cli.py +0 -0
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata/bulk.py +0 -0
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata/cache.py +0 -0
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata/config.py +0 -0
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata/exceptions.py +0 -0
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata/gcs.py +0 -0
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata/reference/__init__.py +0 -0
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata/reference/census.py +0 -0
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata/reference/districts.py +0 -0
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata/reference/elecdom.py +0 -0
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata/reference/elmas.py +0 -0
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata/reference/enedis.py +0 -0
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata/reference/gas_network.py +0 -0
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata/reference/occupant_diaries.py +0 -0
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata/reference/ore.py +0 -0
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata/simulation/__init__.py +0 -0
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata/simulation/_epw.py +0 -0
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata/simulation/era5.py +0 -0
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata/simulation/era5_bulk.py +0 -0
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata/tests/__init__.py +0 -0
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata/tests/conftest.py +0 -0
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata/tests/test_cache.py +0 -0
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata/tests/test_config.py +0 -0
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata/tests/test_gcs.py +0 -0
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata/tests/test_public_api.py +0 -0
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata/tests/test_reference.py +0 -0
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata/tests/test_simulation.py +0 -0
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata/validation.py +0 -0
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata.egg-info/SOURCES.txt +0 -0
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata.egg-info/dependency_links.txt +0 -0
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata.egg-info/entry_points.txt +0 -0
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata.egg-info/requires.txt +0 -0
- {buildingdata-0.5.0 → buildingdata-0.7.0}/buildingdata.egg-info/top_level.txt +0 -0
- {buildingdata-0.5.0 → buildingdata-0.7.0}/setup.cfg +0 -0
|
@@ -56,7 +56,7 @@ def get_diagnosis(refresh=False):
|
|
|
56
56
|
``dhw_energy``, ``heating_mode``/``dhw_mode`` (individual /
|
|
57
57
|
collective / mixed), the ``*_efficiency`` and ``*_scop`` pairs,
|
|
58
58
|
``intermittency_factor``, ``backup_heating_share``,
|
|
59
|
-
``dhw_storage_volume``;
|
|
59
|
+
``dhw_storage_volume``, ``heating_emitter_type``;
|
|
60
60
|
* observed performance — ``energy_class``, ``ghg_class`` and the
|
|
61
61
|
``annual_*_per_area`` intensities, for calibrating simulated
|
|
62
62
|
output against the diagnosis itself.
|
|
@@ -69,6 +69,16 @@ def get_diagnosis(refresh=False):
|
|
|
69
69
|
understates heat-pump performance roughly threefold, hence the two
|
|
70
70
|
columns.
|
|
71
71
|
|
|
72
|
+
``heating_system`` stays the coarse, census-joinable class — every
|
|
73
|
+
heat pump collapses to ``"electric heat pump"``. ``heat_pump_type``
|
|
74
|
+
(``air/air``, ``air/water``, ``water/water``, ``brine/water``,
|
|
75
|
+
``geothermal``, ``hybrid``) and ``heat_pump_installation_period``
|
|
76
|
+
recover the configuration detail the census join key cannot carry,
|
|
77
|
+
and are **non-null only for heat-pump records**.
|
|
78
|
+
``heating_emitter_type`` (``air``, ``floor heating``,
|
|
79
|
+
``radiator low/medium``, ``radiator high``) is populated for every
|
|
80
|
+
system.
|
|
81
|
+
|
|
72
82
|
Raises:
|
|
73
83
|
RemoteNotAvailableError: if the blob is not found in the GCS bucket.
|
|
74
84
|
SchemaValidationError: if the fetched frame is missing one of the
|
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
527
|
-
|
|
528
|
-
|
|
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
|
|
540
|
-
|
|
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
|
-
|
|
574
|
-
.
|
|
575
|
-
|
|
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)
|
|
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"):
|
|
@@ -99,6 +99,8 @@ UPSTREAM_DEFAULTS = {
|
|
|
99
99
|
"rendement_regulation_systeme_1": 0.99,
|
|
100
100
|
"i0_systeme_1": 0.9,
|
|
101
101
|
"conso_ch_systeme_1": 8000.0,
|
|
102
|
+
"temp_distribution_ch_systeme_1": "moyenne",
|
|
103
|
+
"type_emission_distribution_systeme_1": "Radiateurs à eau chaude - robinet thermostatique",
|
|
102
104
|
# heating system 2 (a wood stove backing up the boiler)
|
|
103
105
|
"type_energie_systeme_2": "Bois – Bûches",
|
|
104
106
|
"rendement_generation_systeme_2": 0.5,
|
|
@@ -175,6 +177,83 @@ def test_a_heat_pump_gets_a_scop_and_no_efficiency(script):
|
|
|
175
177
|
assert out["main_heating_system_scop"][0] == pytest.approx(3.0 * 0.95 * 0.95 * 0.99)
|
|
176
178
|
|
|
177
179
|
|
|
180
|
+
def test_heat_pump_detail_is_parsed_from_the_generator_label(script):
|
|
181
|
+
out = _run(
|
|
182
|
+
script,
|
|
183
|
+
{
|
|
184
|
+
"type_generateur_ch_systeme_1": "PAC air/eau installée après 2017",
|
|
185
|
+
"rendement_generation_systeme_1": None,
|
|
186
|
+
"scop_systeme_1": 3.0,
|
|
187
|
+
},
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
assert out["heat_pump_type"][0] == "air/water"
|
|
191
|
+
assert out["heat_pump_installation_period"][0] == "[2017, 2100]"
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_a_hybrid_heat_pump_label_is_not_swallowed_by_the_air_water_pattern(script):
|
|
195
|
+
# The 3 hybrid records' label is "pompe à chaleur hybride : partie pompe à
|
|
196
|
+
# chaleur PAC air/eau installée après 2017" (real 11-12-2025 vintage) — it
|
|
197
|
+
# contains "air/eau" too, so "hybride" must be tested first or the hybrid
|
|
198
|
+
# reading is lost.
|
|
199
|
+
out = _run(
|
|
200
|
+
script,
|
|
201
|
+
{
|
|
202
|
+
"type_generateur_ch_systeme_1": (
|
|
203
|
+
"pompe à chaleur hybride : partie pompe à chaleur PAC air/eau"
|
|
204
|
+
" installée après 2017"
|
|
205
|
+
),
|
|
206
|
+
"rendement_generation_systeme_1": None,
|
|
207
|
+
"scop_systeme_1": 3.0,
|
|
208
|
+
},
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
assert out["heat_pump_type"][0] == "hybrid"
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def test_heat_pump_detail_is_null_for_a_non_heat_pump_record(script):
|
|
215
|
+
out = _run(script)
|
|
216
|
+
|
|
217
|
+
assert out["heating_system"][0] != "electric heat pump"
|
|
218
|
+
assert out["heat_pump_type"][0] is None
|
|
219
|
+
assert out["heat_pump_installation_period"][0] is None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def test_heating_emitter_type_is_air_when_there_is_no_distribution_loop(script):
|
|
223
|
+
# The bulk of air/air heat pumps report no distribution network at all.
|
|
224
|
+
out = _run(
|
|
225
|
+
script,
|
|
226
|
+
{
|
|
227
|
+
"type_generateur_ch_systeme_1": "PAC air/air installée à partir de 2015",
|
|
228
|
+
"rendement_generation_systeme_1": None,
|
|
229
|
+
"scop_systeme_1": 3.0,
|
|
230
|
+
"temp_distribution_ch_systeme_1": "abscence de réseau de distribution",
|
|
231
|
+
"type_emission_distribution_systeme_1": "Soufflage d'air chaud",
|
|
232
|
+
},
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
assert out["heating_emitter_type"][0] == "air"
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def test_heating_emitter_type_separates_floor_heating_from_a_radiator_loop(script):
|
|
239
|
+
# Both configurations report "moyenne" in the coarse temperature column —
|
|
240
|
+
# only the fine emission label tells them apart.
|
|
241
|
+
floor, radiator = _run(
|
|
242
|
+
script,
|
|
243
|
+
{"type_emission_distribution_systeme_1": "Plancher chauffant basse température"},
|
|
244
|
+
{"type_emission_distribution_systeme_1": "Radiateurs à eau chaude - robinet thermostatique"},
|
|
245
|
+
)["heating_emitter_type"]
|
|
246
|
+
|
|
247
|
+
assert floor == "floor heating"
|
|
248
|
+
assert radiator == "radiator low/medium"
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def test_heating_emitter_type_maps_the_high_temperature_class(script):
|
|
252
|
+
out = _run(script, {"temp_distribution_ch_systeme_1": "haute"})
|
|
253
|
+
|
|
254
|
+
assert out["heating_emitter_type"][0] == "radiator high"
|
|
255
|
+
|
|
256
|
+
|
|
178
257
|
def test_a_multi_building_gas_plant_is_classified_as_a_heat_network(script):
|
|
179
258
|
# "Chaudière(s) gaz multi bâtiment modélisée comme un réseau de chaleur"
|
|
180
259
|
# matches both a gas and a heat-network pattern. ADEME reports the fuel as
|
|
@@ -273,11 +352,17 @@ def test_every_published_column_is_populated(script):
|
|
|
273
352
|
out = _run(script)
|
|
274
353
|
|
|
275
354
|
assert set(out.columns) == set(script.COLUMN_SELECTION)
|
|
276
|
-
# The SCOP columns
|
|
277
|
-
|
|
355
|
+
# The SCOP columns, and the heat-pump detail columns, are null by
|
|
356
|
+
# construction for the default non-heat-pump record.
|
|
357
|
+
expected_null_columns = {
|
|
358
|
+
"main_heating_system_scop",
|
|
359
|
+
"backup_heating_system_scop",
|
|
360
|
+
"heat_pump_type",
|
|
361
|
+
"heat_pump_installation_period",
|
|
362
|
+
}
|
|
278
363
|
unexpected_nulls = [
|
|
279
364
|
column
|
|
280
365
|
for column in out.columns
|
|
281
|
-
if column not in
|
|
366
|
+
if column not in expected_null_columns and out[column].null_count()
|
|
282
367
|
]
|
|
283
368
|
assert unexpected_nulls == []
|
|
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
|