buildingdata 0.2.0__tar.gz → 0.2.2__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.2.0 → buildingdata-0.2.2}/PKG-INFO +25 -6
- {buildingdata-0.2.0 → buildingdata-0.2.2}/README.md +25 -6
- {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/cache.py +53 -2
- buildingdata-0.2.2/buildingdata/exceptions.py +29 -0
- buildingdata-0.2.2/buildingdata/gcs.py +132 -0
- buildingdata-0.2.2/buildingdata/reference/census.py +71 -0
- {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/reference/diagnosis.py +27 -11
- {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/reference/districts.py +5 -11
- {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/reference/elmas.py +5 -11
- {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/reference/enedis.py +6 -11
- {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/reference/gas_network.py +5 -11
- {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/reference/occupant_diaries.py +5 -12
- {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/reference/ore.py +5 -12
- buildingdata-0.2.2/buildingdata/simulation/bdtopo.py +216 -0
- {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/simulation/era5.py +148 -48
- {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/tests/test_cache.py +25 -0
- {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/tests/test_reference.py +31 -1
- buildingdata-0.2.2/buildingdata/tests/test_simulation.py +369 -0
- buildingdata-0.2.2/buildingdata/validation.py +77 -0
- {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata.egg-info/PKG-INFO +25 -6
- {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata.egg-info/SOURCES.txt +1 -0
- {buildingdata-0.2.0 → buildingdata-0.2.2}/pyproject.toml +5 -1
- buildingdata-0.2.0/buildingdata/exceptions.py +0 -17
- buildingdata-0.2.0/buildingdata/gcs.py +0 -82
- buildingdata-0.2.0/buildingdata/reference/census.py +0 -48
- buildingdata-0.2.0/buildingdata/simulation/bdtopo.py +0 -87
- buildingdata-0.2.0/buildingdata/tests/test_simulation.py +0 -184
- {buildingdata-0.2.0 → buildingdata-0.2.2}/LICENSE +0 -0
- {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/__init__.py +0 -0
- {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/_cli.py +0 -0
- {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/config.py +0 -0
- {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/reference/__init__.py +0 -0
- {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/simulation/__init__.py +0 -0
- {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/tests/__init__.py +0 -0
- {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/tests/conftest.py +0 -0
- {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/tests/test_config.py +0 -0
- {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata/tests/test_public_api.py +0 -0
- {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata.egg-info/dependency_links.txt +0 -0
- {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata.egg-info/entry_points.txt +0 -0
- {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata.egg-info/requires.txt +0 -0
- {buildingdata-0.2.0 → buildingdata-0.2.2}/buildingdata.egg-info/top_level.txt +0 -0
- {buildingdata-0.2.0 → buildingdata-0.2.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: buildingdata
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
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
|
|
@@ -88,13 +88,32 @@ uses CRS **EPSG:2154 (Lambert-93)**.
|
|
|
88
88
|
|
|
89
89
|
## Configuration
|
|
90
90
|
|
|
91
|
-
Settings are resolved
|
|
92
|
-
|
|
91
|
+
Settings are resolved in the following precedence order:
|
|
92
|
+
1. Explicit function arguments / CLI options
|
|
93
|
+
2. Environment variables (`BUILDINGDATA_BUCKET`, `BUILDINGDATA_CACHE_DIR`, `GOOGLE_APPLICATION_CREDENTIALS`)
|
|
94
|
+
3. Global configuration file (`~/.config/buildingdata/config.ini`)
|
|
95
|
+
4. Dynamic defaults
|
|
93
96
|
|
|
94
|
-
|
|
97
|
+
Configurable settings:
|
|
98
|
+
- **bucket** — GCS bucket holding reference datasets (default: `building-inference-data`)
|
|
95
99
|
- **cache directory** — where downloaded data is stored locally
|
|
96
|
-
- **credentials** — path to a GCS service-account JSON (omit for anonymous access
|
|
97
|
-
|
|
100
|
+
- **credentials** — path to a GCS service-account JSON (omit for anonymous access to the public bucket)
|
|
101
|
+
|
|
102
|
+
### Cache Behavior & Multi-Project Sharing
|
|
103
|
+
|
|
104
|
+
- **Default (Unconfigured)**: The cache directory is namespaced per installation (`~/.local/share/buildingdata/cache/<install-id>` on Linux/macOS). Each virtualenv or package installation receives its own unique cache subfolder to prevent collisions between environments.
|
|
105
|
+
- **Sharing Across Projects**: To share a single cache directory across multiple repositories, virtualenvs, or Snakemake pipelines, set the `BUILDINGDATA_CACHE_DIR` environment variable or write a global configuration file:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
# Via CLI (writes ~/.config/buildingdata/config.ini):
|
|
109
|
+
buildingdata configure --cache-dir /path/to/shared/cache
|
|
110
|
+
|
|
111
|
+
# Or via environment variable:
|
|
112
|
+
export BUILDINGDATA_CACHE_DIR="/path/to/shared/cache"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Because `~/.config/buildingdata/config.ini` is stored in your home directory, configuring it once applies globally to all projects and virtual environments for your user account.
|
|
116
|
+
|
|
98
117
|
|
|
99
118
|
## Installation extras
|
|
100
119
|
|
|
@@ -55,13 +55,32 @@ uses CRS **EPSG:2154 (Lambert-93)**.
|
|
|
55
55
|
|
|
56
56
|
## Configuration
|
|
57
57
|
|
|
58
|
-
Settings are resolved
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
Settings are resolved in the following precedence order:
|
|
59
|
+
1. Explicit function arguments / CLI options
|
|
60
|
+
2. Environment variables (`BUILDINGDATA_BUCKET`, `BUILDINGDATA_CACHE_DIR`, `GOOGLE_APPLICATION_CREDENTIALS`)
|
|
61
|
+
3. Global configuration file (`~/.config/buildingdata/config.ini`)
|
|
62
|
+
4. Dynamic defaults
|
|
63
|
+
|
|
64
|
+
Configurable settings:
|
|
65
|
+
- **bucket** — GCS bucket holding reference datasets (default: `building-inference-data`)
|
|
62
66
|
- **cache directory** — where downloaded data is stored locally
|
|
63
|
-
- **credentials** — path to a GCS service-account JSON (omit for anonymous access
|
|
64
|
-
|
|
67
|
+
- **credentials** — path to a GCS service-account JSON (omit for anonymous access to the public bucket)
|
|
68
|
+
|
|
69
|
+
### Cache Behavior & Multi-Project Sharing
|
|
70
|
+
|
|
71
|
+
- **Default (Unconfigured)**: The cache directory is namespaced per installation (`~/.local/share/buildingdata/cache/<install-id>` on Linux/macOS). Each virtualenv or package installation receives its own unique cache subfolder to prevent collisions between environments.
|
|
72
|
+
- **Sharing Across Projects**: To share a single cache directory across multiple repositories, virtualenvs, or Snakemake pipelines, set the `BUILDINGDATA_CACHE_DIR` environment variable or write a global configuration file:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Via CLI (writes ~/.config/buildingdata/config.ini):
|
|
76
|
+
buildingdata configure --cache-dir /path/to/shared/cache
|
|
77
|
+
|
|
78
|
+
# Or via environment variable:
|
|
79
|
+
export BUILDINGDATA_CACHE_DIR="/path/to/shared/cache"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Because `~/.config/buildingdata/config.ini` is stored in your home directory, configuring it once applies globally to all projects and virtual environments for your user account.
|
|
83
|
+
|
|
65
84
|
|
|
66
85
|
## Installation extras
|
|
67
86
|
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
+
import fcntl
|
|
2
3
|
import json
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
import uuid
|
|
7
|
+
from contextlib import contextmanager
|
|
3
8
|
from datetime import datetime, timezone
|
|
4
9
|
from pathlib import Path
|
|
5
10
|
|
|
@@ -7,6 +12,46 @@ from .config import get_cache_dir
|
|
|
7
12
|
from .exceptions import CacheError
|
|
8
13
|
|
|
9
14
|
|
|
15
|
+
@contextmanager
|
|
16
|
+
def file_lock(lock_path, timeout=600):
|
|
17
|
+
"""Context manager for acquiring an inter-process file lock.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
lock_path (pathlib.Path or str): Path to the lock file.
|
|
21
|
+
timeout (int or float): Maximum seconds to wait for lock acquisition.
|
|
22
|
+
Defaults to 600 seconds.
|
|
23
|
+
|
|
24
|
+
Raises:
|
|
25
|
+
CacheError: If lock acquisition times out.
|
|
26
|
+
"""
|
|
27
|
+
lock_path = Path(lock_path)
|
|
28
|
+
lock_path.parent.mkdir(parents=True, exist_ok=True)
|
|
29
|
+
fd = os.open(str(lock_path), os.O_CREAT | os.O_RDWR, 0o666)
|
|
30
|
+
start_time = time.monotonic()
|
|
31
|
+
acquired = False
|
|
32
|
+
try:
|
|
33
|
+
while True:
|
|
34
|
+
try:
|
|
35
|
+
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
36
|
+
acquired = True
|
|
37
|
+
break
|
|
38
|
+
except (BlockingIOError, OSError):
|
|
39
|
+
if time.monotonic() - start_time >= timeout:
|
|
40
|
+
raise CacheError(f"Timed out waiting for lock on {lock_path}") from None
|
|
41
|
+
time.sleep(0.1)
|
|
42
|
+
yield
|
|
43
|
+
finally:
|
|
44
|
+
if acquired:
|
|
45
|
+
try:
|
|
46
|
+
fcntl.flock(fd, fcntl.LOCK_UN)
|
|
47
|
+
except OSError:
|
|
48
|
+
pass
|
|
49
|
+
try:
|
|
50
|
+
os.close(fd)
|
|
51
|
+
except OSError:
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
10
55
|
def cache_path(name):
|
|
11
56
|
"""Return the local Path where a named artifact should be cached.
|
|
12
57
|
|
|
@@ -59,7 +104,7 @@ def needs_refresh(name, blob):
|
|
|
59
104
|
|
|
60
105
|
|
|
61
106
|
def write_sidecar(name, blob):
|
|
62
|
-
"""Write a .meta.json sidecar after a successful download.
|
|
107
|
+
"""Write a .meta.json sidecar after a successful download atomically.
|
|
63
108
|
|
|
64
109
|
Args:
|
|
65
110
|
name (str): logical artifact name.
|
|
@@ -71,10 +116,15 @@ def write_sidecar(name, blob):
|
|
|
71
116
|
"generation": blob.generation,
|
|
72
117
|
"downloaded_at": datetime.now(timezone.utc).isoformat(),
|
|
73
118
|
}
|
|
119
|
+
pid_suffix = f".tmp.{os.getpid()}_{uuid.uuid4().hex[:6]}"
|
|
120
|
+
tmp_sidecar = sidecar.with_name(sidecar.name + pid_suffix)
|
|
74
121
|
try:
|
|
75
|
-
with open(
|
|
122
|
+
with open(tmp_sidecar, "w") as f:
|
|
76
123
|
json.dump(meta, f, indent=2)
|
|
124
|
+
tmp_sidecar.replace(sidecar)
|
|
77
125
|
except OSError as e:
|
|
126
|
+
if tmp_sidecar.exists():
|
|
127
|
+
tmp_sidecar.unlink(missing_ok=True)
|
|
78
128
|
raise CacheError(f"Failed to write cache sidecar {sidecar}: {e}") from e
|
|
79
129
|
|
|
80
130
|
|
|
@@ -90,3 +140,4 @@ def ensure_subdir(subdir):
|
|
|
90
140
|
path = get_cache_dir() / subdir
|
|
91
141
|
path.mkdir(parents=True, exist_ok=True)
|
|
92
142
|
return path
|
|
143
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BuildingDataError(Exception):
|
|
5
|
+
"""Base exception for buildingdata errors."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CacheError(BuildingDataError):
|
|
9
|
+
"""Raised when a cache read or write operation fails."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RemoteNotAvailableError(BuildingDataError):
|
|
13
|
+
"""Raised when a remote endpoint returns a non-success status."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ConfigurationError(BuildingDataError):
|
|
17
|
+
"""Raised when required configuration (credentials, API key) is missing."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SchemaValidationError(BuildingDataError):
|
|
21
|
+
"""Raised when a fetched dataset violates its expected schema/unit contract.
|
|
22
|
+
|
|
23
|
+
Signals that the upstream file's columns, dtypes or value ranges no longer
|
|
24
|
+
match what this package documents and what its consumers rely on -- e.g. a
|
|
25
|
+
portal revised its schema, a join-key format changed, or a column switched
|
|
26
|
+
units (kWh vs MWh, fraction vs percent). Failing fast here is deliberate:
|
|
27
|
+
a clear error on a revised upstream file beats a silent NaN or a
|
|
28
|
+
wrong-scale value propagating into a simulation.
|
|
29
|
+
"""
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from tqdm import tqdm
|
|
5
|
+
|
|
6
|
+
from .config import get_bucket, get_credentials_file
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_client():
|
|
10
|
+
"""Return a google.cloud.storage.Client.
|
|
11
|
+
|
|
12
|
+
Uses the credentials file from config if one is set. Otherwise returns
|
|
13
|
+
an anonymous client, which can read public buckets without any
|
|
14
|
+
authentication or Google Cloud project.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
google.cloud.storage.Client: storage client.
|
|
18
|
+
"""
|
|
19
|
+
from google.cloud import storage
|
|
20
|
+
|
|
21
|
+
creds_file = get_credentials_file()
|
|
22
|
+
if creds_file is not None:
|
|
23
|
+
return storage.Client.from_service_account_json(str(creds_file))
|
|
24
|
+
return storage.Client.create_anonymous_client()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_blob(name):
|
|
28
|
+
"""Return the Blob object for a given name, or None if not found.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
name (str): blob name inside the configured bucket.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
google.cloud.storage.Blob or None.
|
|
35
|
+
"""
|
|
36
|
+
client = get_client()
|
|
37
|
+
bucket = client.bucket(get_bucket())
|
|
38
|
+
blob = bucket.blob(name)
|
|
39
|
+
blob.reload()
|
|
40
|
+
return blob if blob.exists() else None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def download_blob(name, dest_path, show_progress=True):
|
|
44
|
+
"""Download a blob from the configured bucket to dest_path.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
name (str): blob name inside the bucket, e.g. "census_latest.parquet".
|
|
48
|
+
dest_path (pathlib.Path): local destination file path.
|
|
49
|
+
show_progress (bool): display a tqdm progress bar. Defaults to True.
|
|
50
|
+
"""
|
|
51
|
+
import os
|
|
52
|
+
import uuid
|
|
53
|
+
|
|
54
|
+
from google.cloud import storage
|
|
55
|
+
|
|
56
|
+
dest_path = Path(dest_path)
|
|
57
|
+
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
|
|
59
|
+
client = get_client()
|
|
60
|
+
bucket = client.bucket(get_bucket())
|
|
61
|
+
blob = bucket.blob(name)
|
|
62
|
+
blob.reload()
|
|
63
|
+
|
|
64
|
+
total = blob.size or 0
|
|
65
|
+
pid_suffix = f".tmp.{os.getpid()}_{uuid.uuid4().hex[:6]}"
|
|
66
|
+
tmp_path = dest_path.with_name(dest_path.name + pid_suffix)
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
with open(tmp_path, "wb") as f:
|
|
70
|
+
if show_progress:
|
|
71
|
+
with tqdm(
|
|
72
|
+
total=total,
|
|
73
|
+
unit="B",
|
|
74
|
+
unit_scale=True,
|
|
75
|
+
desc=name,
|
|
76
|
+
leave=True,
|
|
77
|
+
) as bar:
|
|
78
|
+
def _callback(chunk):
|
|
79
|
+
bar.update(len(chunk))
|
|
80
|
+
|
|
81
|
+
blob.download_to_file(f, checksum="md5", raw_download=True)
|
|
82
|
+
bar.update(total - bar.n)
|
|
83
|
+
else:
|
|
84
|
+
blob.download_to_file(f)
|
|
85
|
+
|
|
86
|
+
tmp_path.replace(dest_path)
|
|
87
|
+
finally:
|
|
88
|
+
if tmp_path.exists():
|
|
89
|
+
tmp_path.unlink(missing_ok=True)
|
|
90
|
+
|
|
91
|
+
return blob
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def ensure_blob_cached(blob_name, refresh=False, get_blob_fn=None, download_blob_fn=None):
|
|
95
|
+
"""Ensure a named GCS blob is downloaded and up-to-date locally, synchronized across processes.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
blob_name (str): name of the blob in GCS, e.g. "census_latest.parquet".
|
|
99
|
+
refresh (bool): force re-download even if cached locally. Defaults to False.
|
|
100
|
+
get_blob_fn (callable, optional): function to retrieve Blob. Defaults to get_blob.
|
|
101
|
+
download_blob_fn (callable, optional): function to download Blob. Defaults to download_blob.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
pathlib.Path: absolute path to the cached file.
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
RemoteNotAvailableError: if the blob does not exist in GCS.
|
|
108
|
+
"""
|
|
109
|
+
from .cache import cache_path, file_lock, is_cached, needs_refresh, write_sidecar
|
|
110
|
+
from .exceptions import RemoteNotAvailableError
|
|
111
|
+
|
|
112
|
+
if get_blob_fn is None:
|
|
113
|
+
get_blob_fn = get_blob
|
|
114
|
+
if download_blob_fn is None:
|
|
115
|
+
download_blob_fn = download_blob
|
|
116
|
+
|
|
117
|
+
dest = cache_path(blob_name)
|
|
118
|
+
lock_file = dest.with_name(dest.name + ".lock")
|
|
119
|
+
|
|
120
|
+
with file_lock(lock_file):
|
|
121
|
+
if not is_cached(blob_name) or refresh:
|
|
122
|
+
blob = get_blob_fn(blob_name)
|
|
123
|
+
if blob is None:
|
|
124
|
+
raise RemoteNotAvailableError(f"Blob {blob_name!r} not found in GCS bucket.")
|
|
125
|
+
if not is_cached(blob_name) or needs_refresh(blob_name, blob):
|
|
126
|
+
download_blob_fn(blob_name, dest)
|
|
127
|
+
write_sidecar(blob_name, blob)
|
|
128
|
+
|
|
129
|
+
return dest
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import polars as pl
|
|
3
|
+
|
|
4
|
+
from ..exceptions import RemoteNotAvailableError
|
|
5
|
+
from ..gcs import download_blob, ensure_blob_cached, get_blob
|
|
6
|
+
from ..validation import require_columns
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_BLOB_NAME = "census_latest.parquet"
|
|
11
|
+
|
|
12
|
+
# Columns the processed INSEE census must expose. ``district``/``city_group``
|
|
13
|
+
# are the join keys ``building_eload`` uses to attach census records to IRIS
|
|
14
|
+
# districts; the dwelling-attribute columns feed ``buildingmodel``'s inference
|
|
15
|
+
# (occupancy/heating/cooking/construction) and the ``ipondl`` weight is the
|
|
16
|
+
# census sampling weight every aggregate is summed over. Missing any of these
|
|
17
|
+
# means the wrong file or a revised INSEE schema.
|
|
18
|
+
_REQUIRED_COLUMNS = (
|
|
19
|
+
"district",
|
|
20
|
+
"city_group",
|
|
21
|
+
"ipondl",
|
|
22
|
+
"occupancy_type",
|
|
23
|
+
"occupant_status",
|
|
24
|
+
"occupant_count",
|
|
25
|
+
"living_area_class",
|
|
26
|
+
"heating_system",
|
|
27
|
+
"residential_type",
|
|
28
|
+
"construction_year_class",
|
|
29
|
+
"main_cooking_energy",
|
|
30
|
+
"secondary_cooking_energy",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_census(city_group_list=None, refresh=False):
|
|
35
|
+
"""Return the processed INSEE census DataFrame, optionally pre-filtered.
|
|
36
|
+
|
|
37
|
+
Downloads census_latest.parquet from GCS to the local cache on first call
|
|
38
|
+
(or when refresh=True). Filtering by city_group_list uses Polars lazy scan
|
|
39
|
+
so only the needed rows are loaded into memory.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
city_group_list (list of str, optional): EPCI codes to filter on. If
|
|
43
|
+
None, the full national census is returned. Defaults to None.
|
|
44
|
+
refresh (bool): force re-download even if the cache is warm.
|
|
45
|
+
Defaults to False.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
polars.DataFrame: census records with columns occupancy_type,
|
|
49
|
+
occupant_status, living_area_class, occupant_count, heating_system,
|
|
50
|
+
residential_type, construction_year_class, district, city,
|
|
51
|
+
city_group, department, region, main_cooking_energy,
|
|
52
|
+
secondary_cooking_energy, ipondl. ``district``/``city_group`` are
|
|
53
|
+
string INSEE codes (IRIS / EPCI); ``ipondl`` is the INSEE sampling
|
|
54
|
+
weight every aggregate is summed over.
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
RemoteNotAvailableError: if the blob is not found in the GCS bucket.
|
|
58
|
+
SchemaValidationError: if the fetched frame is missing a required
|
|
59
|
+
join-key or dwelling-attribute column (see ``_REQUIRED_COLUMNS``).
|
|
60
|
+
"""
|
|
61
|
+
dest = ensure_blob_cached(
|
|
62
|
+
_BLOB_NAME, refresh=refresh, get_blob_fn=get_blob, download_blob_fn=download_blob
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
lf = pl.scan_parquet(dest)
|
|
67
|
+
|
|
68
|
+
require_columns(lf.collect_schema(), _REQUIRED_COLUMNS, "INSEE census")
|
|
69
|
+
if city_group_list is not None:
|
|
70
|
+
lf = lf.filter(pl.col("city_group").is_in(city_group_list))
|
|
71
|
+
return lf.collect()
|
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
import polars as pl
|
|
3
3
|
|
|
4
|
-
from ..
|
|
5
|
-
from ..
|
|
6
|
-
|
|
4
|
+
from ..gcs import download_blob, ensure_blob_cached, get_blob
|
|
5
|
+
from ..validation import require_columns
|
|
6
|
+
|
|
7
7
|
|
|
8
8
|
_BLOB_NAME = "energy_performance_diagnosis_latest.parquet"
|
|
9
9
|
|
|
10
|
+
|
|
10
11
|
# Heating/DHW energies excluded from inference (no meaningful DPE data for coal)
|
|
11
12
|
_EXCLUDED_ENERGIES = ["Charbon"]
|
|
12
13
|
|
|
14
|
+
# Columns this accessor operates on directly (coal filter + dtype casts). If any
|
|
15
|
+
# is absent the raw polars error is opaque; validating up front names the DPE
|
|
16
|
+
# schema drift explicitly. ``heating_system`` + ``region`` are the join keys
|
|
17
|
+
# ``buildingmodel``'s energy-system inference keys on; ``backup_heating_energy``
|
|
18
|
+
# / ``dhw_energy`` carry the fuel labels the coal filter reads.
|
|
19
|
+
_REQUIRED_COLUMNS = (
|
|
20
|
+
"heating_system",
|
|
21
|
+
"region",
|
|
22
|
+
"backup_heating_energy",
|
|
23
|
+
"dhw_energy",
|
|
24
|
+
)
|
|
25
|
+
|
|
13
26
|
|
|
14
27
|
def get_diagnosis(refresh=False):
|
|
15
28
|
"""Return the cleaned DPE energy performance diagnosis DataFrame.
|
|
@@ -26,19 +39,22 @@ def get_diagnosis(refresh=False):
|
|
|
26
39
|
polars.DataFrame: DPE records with columns heating_system (Categorical),
|
|
27
40
|
region (Int64), backup_heating_energy, dhw_energy, and all U-value
|
|
28
41
|
and efficiency columns used by inference/building_attributes.py.
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
RemoteNotAvailableError: if the blob is not found in the GCS bucket.
|
|
45
|
+
SchemaValidationError: if the fetched frame is missing one of the
|
|
46
|
+
columns this accessor operates on (see ``_REQUIRED_COLUMNS``).
|
|
29
47
|
"""
|
|
30
|
-
dest =
|
|
48
|
+
dest = ensure_blob_cached(
|
|
49
|
+
_BLOB_NAME, refresh=refresh, get_blob_fn=get_blob, download_blob_fn=download_blob
|
|
50
|
+
)
|
|
31
51
|
|
|
32
|
-
if not is_cached(_BLOB_NAME) or refresh:
|
|
33
|
-
blob = get_blob(_BLOB_NAME)
|
|
34
|
-
if blob is None:
|
|
35
|
-
raise RemoteNotAvailableError(f"Blob {_BLOB_NAME!r} not found in GCS bucket.")
|
|
36
|
-
if not is_cached(_BLOB_NAME) or needs_refresh(_BLOB_NAME, blob):
|
|
37
|
-
download_blob(_BLOB_NAME, dest)
|
|
38
|
-
write_sidecar(_BLOB_NAME, blob)
|
|
39
52
|
|
|
40
53
|
df = pl.read_parquet(dest)
|
|
41
54
|
|
|
55
|
+
|
|
56
|
+
require_columns(df, _REQUIRED_COLUMNS, "DPE/EPC diagnosis")
|
|
57
|
+
|
|
42
58
|
# Remove records with coal heating/DHW — no useful inference data
|
|
43
59
|
df = df.filter(
|
|
44
60
|
~pl.col("backup_heating_energy").is_in(_EXCLUDED_ENERGIES)
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
import geopandas as gpd
|
|
3
3
|
|
|
4
|
-
from ..
|
|
5
|
-
from ..exceptions import RemoteNotAvailableError
|
|
6
|
-
from ..gcs import download_blob, get_blob
|
|
4
|
+
from ..gcs import download_blob, ensure_blob_cached, get_blob
|
|
7
5
|
|
|
8
6
|
_BLOB_NAME = "districts_latest.parquet"
|
|
9
7
|
|
|
@@ -25,17 +23,13 @@ def get_districts(bbox=None, refresh=False):
|
|
|
25
23
|
district, city, city_name, city_group, department, region,
|
|
26
24
|
has_network_city_level, has_network_grdf_data, geometry.
|
|
27
25
|
"""
|
|
28
|
-
dest =
|
|
26
|
+
dest = ensure_blob_cached(
|
|
27
|
+
_BLOB_NAME, refresh=refresh, get_blob_fn=get_blob, download_blob_fn=download_blob
|
|
28
|
+
)
|
|
29
29
|
|
|
30
|
-
if not is_cached(_BLOB_NAME) or refresh:
|
|
31
|
-
blob = get_blob(_BLOB_NAME)
|
|
32
|
-
if blob is None:
|
|
33
|
-
raise RemoteNotAvailableError(f"Blob {_BLOB_NAME!r} not found in GCS bucket.")
|
|
34
|
-
if not is_cached(_BLOB_NAME) or needs_refresh(_BLOB_NAME, blob):
|
|
35
|
-
download_blob(_BLOB_NAME, dest)
|
|
36
|
-
write_sidecar(_BLOB_NAME, blob)
|
|
37
30
|
|
|
38
31
|
kwargs = {}
|
|
39
32
|
if bbox is not None:
|
|
40
33
|
kwargs["bbox"] = bbox
|
|
41
34
|
return gpd.read_parquet(dest, **kwargs)
|
|
35
|
+
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
import polars as pl
|
|
3
3
|
|
|
4
|
-
from ..
|
|
5
|
-
from ..exceptions import RemoteNotAvailableError
|
|
6
|
-
from ..gcs import download_blob, get_blob
|
|
4
|
+
from ..gcs import download_blob, ensure_blob_cached, get_blob
|
|
7
5
|
|
|
8
6
|
# The ELMAS non-residential dataset (Chiche/Duquesne et al., Sci. Data 2023,
|
|
9
7
|
# 10.1038/s41597-023-02542-z) ships as several tables. Each is cached as its
|
|
@@ -56,14 +54,10 @@ def get_elmas(table="time_series", refresh=False):
|
|
|
56
54
|
)
|
|
57
55
|
|
|
58
56
|
blob_name = _BLOB_NAMES[table]
|
|
59
|
-
dest =
|
|
57
|
+
dest = ensure_blob_cached(
|
|
58
|
+
blob_name, refresh=refresh, get_blob_fn=get_blob, download_blob_fn=download_blob
|
|
59
|
+
)
|
|
60
60
|
|
|
61
|
-
if not is_cached(blob_name) or refresh:
|
|
62
|
-
blob = get_blob(blob_name)
|
|
63
|
-
if blob is None:
|
|
64
|
-
raise RemoteNotAvailableError(f"Blob {blob_name!r} not found in GCS bucket.")
|
|
65
|
-
if not is_cached(blob_name) or needs_refresh(blob_name, blob):
|
|
66
|
-
download_blob(blob_name, dest)
|
|
67
|
-
write_sidecar(blob_name, blob)
|
|
68
61
|
|
|
69
62
|
return pl.read_parquet(dest)
|
|
63
|
+
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
import polars as pl
|
|
3
3
|
|
|
4
|
-
from ..
|
|
5
|
-
from ..exceptions import RemoteNotAvailableError
|
|
6
|
-
from ..gcs import download_blob, get_blob
|
|
4
|
+
from ..gcs import download_blob, ensure_blob_cached, get_blob
|
|
7
5
|
|
|
8
6
|
# Enedis "conso-inf36" (<= 36 kVA) measured aggregated consumption / average
|
|
9
7
|
# load curves, used by building_eload.core.validation as measured ground truth.
|
|
@@ -14,17 +12,14 @@ _REGIONAL_BLOB_NAME = "enedis_regional_conso_inf36_latest.parquet"
|
|
|
14
12
|
|
|
15
13
|
|
|
16
14
|
def _fetch(blob_name, refresh):
|
|
17
|
-
dest =
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if blob is None:
|
|
21
|
-
raise RemoteNotAvailableError(f"Blob {blob_name!r} not found in GCS bucket.")
|
|
22
|
-
if not is_cached(blob_name) or needs_refresh(blob_name, blob):
|
|
23
|
-
download_blob(blob_name, dest)
|
|
24
|
-
write_sidecar(blob_name, blob)
|
|
15
|
+
dest = ensure_blob_cached(
|
|
16
|
+
blob_name, refresh=refresh, get_blob_fn=get_blob, download_blob_fn=download_blob
|
|
17
|
+
)
|
|
25
18
|
return pl.read_parquet(dest)
|
|
26
19
|
|
|
27
20
|
|
|
21
|
+
|
|
22
|
+
|
|
28
23
|
def get_enedis_national(refresh=False):
|
|
29
24
|
"""Return Enedis national ``conso-inf36`` measured consumption (polars).
|
|
30
25
|
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
import geopandas as gpd
|
|
3
3
|
|
|
4
|
-
from ..
|
|
5
|
-
from ..exceptions import RemoteNotAvailableError
|
|
6
|
-
from ..gcs import download_blob, get_blob
|
|
4
|
+
from ..gcs import download_blob, ensure_blob_cached, get_blob
|
|
7
5
|
|
|
8
6
|
_BLOB_NAME = "gas_network_route_latest.gpkg"
|
|
9
7
|
|
|
@@ -24,17 +22,13 @@ def get_gas_network(bbox=None, refresh=False):
|
|
|
24
22
|
Returns:
|
|
25
23
|
geopandas.GeoDataFrame: gas pipeline routes in EPSG:2154.
|
|
26
24
|
"""
|
|
27
|
-
dest =
|
|
25
|
+
dest = ensure_blob_cached(
|
|
26
|
+
_BLOB_NAME, refresh=refresh, get_blob_fn=get_blob, download_blob_fn=download_blob
|
|
27
|
+
)
|
|
28
28
|
|
|
29
|
-
if not is_cached(_BLOB_NAME) or refresh:
|
|
30
|
-
blob = get_blob(_BLOB_NAME)
|
|
31
|
-
if blob is None:
|
|
32
|
-
raise RemoteNotAvailableError(f"Blob {_BLOB_NAME!r} not found in GCS bucket.")
|
|
33
|
-
if not is_cached(_BLOB_NAME) or needs_refresh(_BLOB_NAME, blob):
|
|
34
|
-
download_blob(_BLOB_NAME, dest)
|
|
35
|
-
write_sidecar(_BLOB_NAME, blob)
|
|
36
29
|
|
|
37
30
|
kwargs = {}
|
|
38
31
|
if bbox is not None:
|
|
39
32
|
kwargs["bbox"] = bbox
|
|
40
33
|
return gpd.read_file(dest, **kwargs)
|
|
34
|
+
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
import polars as pl
|
|
3
3
|
|
|
4
|
-
from ..
|
|
5
|
-
from ..exceptions import RemoteNotAvailableError
|
|
6
|
-
from ..gcs import download_blob, get_blob
|
|
4
|
+
from ..gcs import download_blob, ensure_blob_cached, get_blob
|
|
7
5
|
|
|
8
6
|
_BLOB_NAME = "occupant_diaries_latest.parquet"
|
|
9
7
|
|
|
@@ -35,14 +33,9 @@ def get_occupant_diaries(refresh=False):
|
|
|
35
33
|
Raises:
|
|
36
34
|
RemoteNotAvailableError: if the blob is not found in the GCS bucket.
|
|
37
35
|
"""
|
|
38
|
-
dest =
|
|
36
|
+
dest = ensure_blob_cached(
|
|
37
|
+
_BLOB_NAME, refresh=refresh, get_blob_fn=get_blob, download_blob_fn=download_blob
|
|
38
|
+
)
|
|
39
|
+
return pl.read_parquet(dest)
|
|
39
40
|
|
|
40
|
-
if not is_cached(_BLOB_NAME) or refresh:
|
|
41
|
-
blob = get_blob(_BLOB_NAME)
|
|
42
|
-
if blob is None:
|
|
43
|
-
raise RemoteNotAvailableError(f"Blob {_BLOB_NAME!r} not found in GCS bucket.")
|
|
44
|
-
if not is_cached(_BLOB_NAME) or needs_refresh(_BLOB_NAME, blob):
|
|
45
|
-
download_blob(_BLOB_NAME, dest)
|
|
46
|
-
write_sidecar(_BLOB_NAME, blob)
|
|
47
41
|
|
|
48
|
-
return pl.read_parquet(dest)
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
import polars as pl
|
|
3
3
|
|
|
4
|
-
from ..
|
|
5
|
-
from ..exceptions import RemoteNotAvailableError
|
|
6
|
-
from ..gcs import download_blob, get_blob
|
|
4
|
+
from ..gcs import download_blob, ensure_blob_cached, get_blob
|
|
7
5
|
|
|
8
6
|
_BLOB_NAME = "ore_annual_consumption_latest.parquet"
|
|
9
7
|
|
|
@@ -38,14 +36,9 @@ def get_ore(refresh=False):
|
|
|
38
36
|
Raises:
|
|
39
37
|
RemoteNotAvailableError: if the blob is not found in the GCS bucket.
|
|
40
38
|
"""
|
|
41
|
-
dest =
|
|
39
|
+
dest = ensure_blob_cached(
|
|
40
|
+
_BLOB_NAME, refresh=refresh, get_blob_fn=get_blob, download_blob_fn=download_blob
|
|
41
|
+
)
|
|
42
|
+
return pl.read_parquet(dest)
|
|
42
43
|
|
|
43
|
-
if not is_cached(_BLOB_NAME) or refresh:
|
|
44
|
-
blob = get_blob(_BLOB_NAME)
|
|
45
|
-
if blob is None:
|
|
46
|
-
raise RemoteNotAvailableError(f"Blob {_BLOB_NAME!r} not found in GCS bucket.")
|
|
47
|
-
if not is_cached(_BLOB_NAME) or needs_refresh(_BLOB_NAME, blob):
|
|
48
|
-
download_blob(_BLOB_NAME, dest)
|
|
49
|
-
write_sidecar(_BLOB_NAME, blob)
|
|
50
44
|
|
|
51
|
-
return pl.read_parquet(dest)
|