Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

All notable changes to this project are documented in this file.

## [Unreleased]

### Added
- Databases exported to Brightway now carry scenario metadata in
`bw2data.databases[name]`: `iam_model`, `pathway`, `representative_time`
(ISO 8601), `ecoinvent_version`, `system_model`, `premise_version` and,
if any, `external_scenarios`. Superstructure and scenario-array databases
list their scenarios under `scenarios`.

## [2.4.9.2]

### Added
Expand Down
48 changes: 48 additions & 0 deletions docs/load.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,54 @@ If several databases have been built, the user can give them specific names, lik
biosphere database already registered.


Scenario metadata
*****************

Every database written to brightway carries, in its brightway metadata, a description
of the scenario and of the point in time it represents. It sits next to the fields
brightway maintains itself:

.. code-block:: python

import bw2data as bd

bd.databases["ei_cutoff_3.10.1_remind_SSP2-PkBudg500_2050"]

.. code-block:: python

{
# written by brightway
"format": "Ecoinvent XML",
"depends": ["ecoinvent-3.10.1-biosphere"],
"backend": "sqlite",
"number": 43648,
"modified": "2026-08-14T12:09:25.945746",
"processed": "2026-08-14T12:09:56.124243",
"geocollections": ["world"],
"searchable": True,
# written by premise
"premise_version": "2.4.9.1",
"iam_model": "remind",
"pathway": "SSP2-PkBudg500",
"representative_time": "2050-01-01T00:00:00",
"ecoinvent_version": "3.10.1",
"system_model": "cutoff",
}

.. note::

``bd.databases["db_name"]`` and ``bd.Database("db_name").metadata`` are the same
mapping, so the fields above can be read either way.

``representative_time`` is the ISO 8601 point in time the database is representative of,
and can be read back with ``datetime.fromisoformat()``.

Databases holding several scenarios (superstructure and scenario-array databases)
list them under a ``scenarios`` key instead, and only carry ``representative_time``
at the top level if all their scenarios share the same year.
User (external) scenarios, if any, are listed under ``external_scenarios``.


Superstructure database
***********************

Expand Down
10 changes: 10 additions & 0 deletions premise/brightway2.py
Original file line number Diff line number Diff line change
Expand Up @@ -442,11 +442,20 @@ def _compact_payload_for_fast_write(data: list) -> list:
return data


def _store_database_metadata(name: str, metadata: dict = None) -> None:
"""Attach scenario metadata to a registered Brightway database."""
if not metadata or name not in databases:
return
databases[name].update(metadata)
databases.flush()


def write_brightway_database(
data: list,
name: str,
fast: bool = False,
check_internal: bool = True,
metadata: dict = None,
) -> None:
"""
Write a Brightway2 database from a Wurst database.
Expand Down Expand Up @@ -481,4 +490,5 @@ def write_brightway_database(
else:
databases[name].pop("geocollections", None)
databases.flush()
_store_database_metadata(name, metadata)
_print_database_written(name)
11 changes: 11 additions & 0 deletions premise/brightway25.py
Original file line number Diff line number Diff line change
Expand Up @@ -775,11 +775,20 @@ def iter_technosphere():
_write_search_index_fast(db.filename, data, name)


def _store_database_metadata(name: str, metadata: dict = None) -> None:
"""Attach scenario metadata to a registered Brightway database."""
if not metadata or name not in databases:
return
databases[name].update(metadata)
databases.set_modified(name)


def write_brightway_database(
data: list,
name: str,
fast: bool = False,
check_internal: bool = True,
metadata: dict = None,
) -> None:
for act in data:
act.setdefault("database", name)
Expand All @@ -802,6 +811,7 @@ def write_brightway_database(
_print_database_overwrite(name)
_compact_payload_for_fast_write(data, name)
_write_processed_database_fast(data, name)
_store_database_metadata(name, metadata)
_print_database_written(name)
return

Expand All @@ -811,4 +821,5 @@ def write_brightway_database(

with _fast_sqlite_writes(fast):
BW25Importer(name, data).write_database()
_store_database_metadata(name, metadata)
_print_database_written(name)
22 changes: 22 additions & 0 deletions premise/new_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
from .transport import _update_vehicles
from .utils import (
cache_ref_exists,
database_metadata,
clear_existing_cache,
clear_runtime_caches,
create_scenario_list,
Expand All @@ -76,6 +77,7 @@
end_of_process,
create_cache,
restore_cached_classifications,
scenario_metadata,
)
from .renewables import _update_wind_turbines

Expand Down Expand Up @@ -1291,6 +1293,11 @@ def write_superstructure_db_to_brightway(
name=name,
fast=True,
check_internal=False,
metadata=database_metadata(
self.scenarios,
version=getattr(self, "version", None),
system_model=getattr(self, "system_model", None),
),
)

self._finalize_superstructure_export()
Expand Down Expand Up @@ -1359,6 +1366,11 @@ def write_scenario_array_db_to_brightway(
name=name,
fast=True,
check_internal=False,
metadata=database_metadata(
self.scenarios,
version=getattr(self, "version", None),
system_model=getattr(self, "system_model", None),
),
)

ordered_labels = ["original", *scenario_labels]
Expand Down Expand Up @@ -1562,6 +1574,11 @@ def write_db_to_brightway(self, name: [str, List[str]] = None):
name[s],
fast=True,
check_internal=True,
metadata=scenario_metadata(
scenario,
version=getattr(self, "version", None),
system_model=getattr(self, "system_model", None),
),
)
end_of_process(scenario)
continue
Expand Down Expand Up @@ -1591,6 +1608,11 @@ def write_db_to_brightway(self, name: [str, List[str]] = None):
write_brightway_database(
scenario["database"],
name[s],
metadata=scenario_metadata(
scenario,
version=getattr(self, "version", None),
system_model=getattr(self, "system_model", None),
),
)

end_of_process(scenario)
Expand Down
98 changes: 98 additions & 0 deletions premise/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,104 @@ def eidb_label(
return name


def scenario_metadata(
scenario: Dict[str, Any],
version: str = None,
system_model: str = None,
) -> Dict[str, Any]:
"""Describe the scenario a database represents.

The returned mapping is meant to be attached to the metadata of an exported
database (e.g., ``bw2data.databases[name]``), so that the point in time and
the IAM scenario it represents can be retrieved later on.

:param scenario: Scenario dictionary, with `model`, `pathway` and `year` keys.
:type scenario: dict
:param version: Ecoinvent version the database is based on.
:type version: str
:param system_model: Ecoinvent system model ("cutoff" or "consequential").
:type system_model: str
:return: JSON-serializable scenario metadata.
:rtype: dict
"""

year = int(scenario["year"])

metadata = {
"premise_version": ".".join(str(item) for item in __version__),
"iam_model": scenario["model"],
"pathway": scenario["pathway"],
# ISO 8601 point in time the database is representative of
"representative_time": datetime(year, 1, 1).isoformat(),
}

if version is not None:
metadata["ecoinvent_version"] = str(version)

if system_model is not None:
metadata["system_model"] = system_model

external_scenarios = scenario.get("external scenarios")
if external_scenarios:
metadata["external_scenarios"] = [
ext["scenario"] for ext in external_scenarios if "scenario" in ext
]

return metadata


def database_metadata(
scenarios: List[Dict[str, Any]],
version: str = None,
system_model: str = None,
) -> Dict[str, Any]:
"""Describe the scenario(s) a database represents.

Databases holding a single scenario get a flat description
(see :func:`scenario_metadata`). Databases holding several scenarios
(super-structure or scenario-array databases) get the list of scenarios
under `scenarios`, plus the point in time they all share, if any.

:param scenarios: List of scenario dictionaries.
:type scenarios: list
:param version: Ecoinvent version the database is based on.
:type version: str
:param system_model: Ecoinvent system model ("cutoff" or "consequential").
:type system_model: str
:return: JSON-serializable database metadata.
:rtype: dict
"""

entries = [
scenario_metadata(scenario, version=version, system_model=system_model)
for scenario in scenarios
]

if len(entries) == 1:
return entries[0]

for entry in entries:
# reported once, at the database level
entry.pop("premise_version", None)

metadata = {
"premise_version": ".".join(str(item) for item in __version__),
"scenarios": entries,
}

if version is not None:
metadata["ecoinvent_version"] = str(version)

if system_model is not None:
metadata["system_model"] = system_model

times = {entry["representative_time"] for entry in entries}
if len(times) == 1:
metadata["representative_time"] = times.pop()

return metadata


@lru_cache(maxsize=1)
def load_constants() -> Dict[str, Any]:
"""Load global constants from ``constants.yaml``.
Expand Down
Loading
Loading