diff --git a/.gitignore b/.gitignore index 5beb133a..4ce94a32 100644 --- a/.gitignore +++ b/.gitignore @@ -205,3 +205,8 @@ pyrightconfig.json /docs/content/examples/tutorials/ /docs/content/examples/examples/ /docs/content/examples/advanced/ + +.claude +.superpowers + +.uv-cache \ No newline at end of file diff --git a/CHANGES.md b/CHANGES.md index 3c0686c5..fb0cde46 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +* Added `representative_time` database metadata as the default timing source: `TimexLCA` now maps background databases to points in time by reading their Brightway metadata (as written by premise >= 2.4.9.2), making `database_dates` optional ([#217](https://github.com/brightway-lca/bw_timex/issues/217)) +* Added `set_database_metadata` to record what a database represents (`representative_time`, and scenario fields such as `iam_model` or `pathway`) for databases that don't bring the metadata themselves +* Added `TimexLCA(scenario={...})` to select one background scenario when a project holds several; `TimexLCA` raises and lists the scenarios it found if the choice is ambiguous +* Added `UnmappedDatabaseError`, raised by `build_timeline()` when the graph traversal reaches a database that is mapped to no point in time - typically a second foreground database that neither holds the functional unit (which is marked `"dynamic"` automatically) nor was marked itself. This previously surfaced as a bare `KeyError` on a node id; the error now names the database, an affected process, and how to map it ([#217](https://github.com/brightway-lca/bw_timex/issues/217)) +* Fixed `TimexLCA(scenario={...})` silently falling back to a plain (non-time-explicit) LCA when the filter matched no database at all, e.g. a typo in a key or value; it now raises a `ValueError` naming the filter and what each of its keys is actually declared as across the project's databases. Also reworded the `database_dates`-specific error messages in `validation.py`, `timeline_builder.py`, and `edge_extractor.py` to also credit `representative_time` metadata as a source of timing ([#217](https://github.com/brightway-lca/bw_timex/issues/217)) ## [1.2.1] - 2026-08-14 * Fixed `ShapeMismatch` in `lci()` for processes with more than one biosphere exchange, by sizing the biosphere `flip_array` to the number of matrix entries (only raised with `bw_processing` >= 1.5; no numeric results change) ([#213](https://github.com/brightway-lca/bw_timex/pull/213)) diff --git a/bw_timex/__init__.py b/bw_timex/__init__.py index 6ea64abc..cf857177 100644 --- a/bw_timex/__init__.py +++ b/bw_timex/__init__.py @@ -5,8 +5,10 @@ ) from ._lci_cache import clear_background_lci_cache +from .database_metadata import set_database_metadata from .dynamic_biosphere_builder import DynamicBiosphereBuilder from .edge_extractor import EdgeExtractor +from .errors import UnmappedDatabaseError from .helper_classes import SetList from .matrix_modifier import MatrixModifier from .timeline_builder import TimelineBuilder @@ -35,6 +37,8 @@ "DynamicBiosphereBuilder", "EdgeExtractor", "SetList", + # errors + "UnmappedDatabaseError", # utils "add_flows_to_characterization_functions", "add_temporal_distribution_to_exchange", @@ -44,4 +48,5 @@ "get_temporal_evolution_factor", "interactive_td_widget", "plot_characterized_inventory_as_waterfall", + "set_database_metadata", ] diff --git a/bw_timex/database_metadata.py b/bw_timex/database_metadata.py new file mode 100644 index 00000000..ad36ff56 --- /dev/null +++ b/bw_timex/database_metadata.py @@ -0,0 +1,315 @@ +"""Read and write what a Brightway database represents. + +`bw_timex` needs to know which point in time each background database stands +for. That information is stored in the database's own Brightway metadata +(`bw2data.databases[name]`), where premise also writes it when it exports a +prospective database: + +```python +{ + "premise_version": "2.4.9.2", + "iam_model": "remind", + "pathway": "SSP2-PkBudg500", + "representative_time": "2050-01-01T00:00:00", + "ecoinvent_version": "3.10.1", + "system_model": "cutoff", +} +``` + +premise writes this metadata from version 2.4.9.2 onwards. Databases exported by +an earlier premise carry none of it, and need `set_database_metadata`. + +Brightway stores this mapping as JSON, so dates are kept as ISO 8601 strings. +""" + +from __future__ import annotations + +import json +from collections import defaultdict +from datetime import datetime +from typing import Any + +import bw2data as bd +from loguru import logger + +from .validation import DatabaseMetadataInputs + +REPRESENTATIVE_TIME = "representative_time" +SCENARIOS = "scenarios" +DYNAMIC = "dynamic" + +#: Metadata keys that identify the scenario a database represents. Two +#: databases differing in any of these represent different scenarios. +#: `premise_version` is deliberately absent: re-running premise on the same +#: pathway must not look like a second scenario. +SCENARIO_SIGNATURE_KEYS = ( + "iam_model", + "pathway", + "system_model", + "ecoinvent_version", + "external_scenarios", +) + +#: Keys Brightway maintains itself, filtered out when reporting to the user +#: which metadata a project's databases carry. +BRIGHTWAY_METADATA_KEYS = frozenset( + { + "backend", + "depends", + "dirty", + "format", + "geocollections", + "modified", + "number", + "processed", + "searchable", + } +) + + +def _database_name(database: Any) -> str: + """The name of a database given either as a name or as a `bd.Database`.""" + name = getattr(database, "name", database) + if not isinstance(name, str): + raise ValueError( + f"database must be a database name or a bw2data Database, got " + f"{type(database).__name__}." + ) + return name + + +def _normalize_representative_time(value: Any, database: str) -> datetime | str: + """Turn a stored `representative_time` into a datetime or `"dynamic"`.""" + if isinstance(value, datetime): + return value + if isinstance(value, str): + if value == DYNAMIC: + return DYNAMIC + try: + return datetime.fromisoformat(value) + except ValueError: + raise ValueError( + f"Database '{database}' has an invalid `{REPRESENTATIVE_TIME}` " + f"metadata value: {value!r}. Expected an ISO 8601 datetime string " + f"(e.g. '2030-01-01'), a datetime, or '{DYNAMIC}'." + ) from None + raise ValueError( + f"Database '{database}' has an invalid `{REPRESENTATIVE_TIME}` metadata " + f"value of type {type(value).__name__}: {value!r}. Expected an ISO 8601 " + f"datetime string, a datetime, or '{DYNAMIC}'." + ) + + +def set_database_metadata(database: str | bd.Database, **metadata) -> dict: + """ + Store what a database represents in its Brightway metadata. + + Use this for databases that don't bring the metadata themselves, e.g. + databases you built yourself or that were exported by premise < 2.4.9.2, + which is the first version writing this metadata. `TimexLCA` reads + `representative_time` from all databases of the project to map them to + points in time, so this replaces passing `database_dates`. + + Parameters + ---------- + database : str or bw2data.Database + Name of the database, or the database itself. Must be registered. + **metadata : + Metadata to store. `representative_time` accepts a `datetime`, an ISO + 8601 string, or `"dynamic"` and is always stored as a string, because + Brightway serializes database metadata to JSON. Any other key is stored + as given and must be JSON-serializable. Keys that premise (>= 2.4.9.2) + writes, and that `TimexLCA(scenario=...)` can select on, are + `iam_model`, `pathway`, `system_model`, `ecoinvent_version` and + `premise_version`. + + Returns + ------- + dict + The database's metadata after the update. + + Examples + -------- + ```python + set_database_metadata("db_2030", representative_time=datetime(2030, 1, 1)) + set_database_metadata( + "my_2050_variant", + representative_time="2050-01-01", + iam_model="remind", + pathway="SSP2-PkBudg500", + ) + ``` + """ + name = _database_name(database) + DatabaseMetadataInputs(database=name, metadata=metadata) + + if name not in bd.databases: + raise ValueError( + f"Database '{name}' is not registered in this Brightway project. " + f"Available databases: {sorted(bd.databases)}." + ) + + serialized = {} + for key, value in metadata.items(): + if key == REPRESENTATIVE_TIME: + normalized = _normalize_representative_time(value, name) + if isinstance(value, str): + # already a string (an ISO 8601 date or "dynamic"): store as given + serialized[key] = value + else: + serialized[key] = normalized.isoformat() + continue + try: + json.dumps(value) + except TypeError: + raise ValueError( + f"Metadata value for '{key}' is not JSON-serializable: {value!r}. " + f"Brightway stores database metadata as JSON." + ) from None + serialized[key] = value + + bd.databases[name].update(serialized) + bd.databases.flush() + return bd.databases[name] + + +def _candidate_databases() -> dict[str, dict]: + """Registered databases that declare a `representative_time`. + + Multi-scenario databases (superstructure and scenario-array exports, which + carry a `scenarios` list) are skipped: `bw_timex` needs one technosphere + per point in time and cannot pick a scenario out of such a database. They + can still be used by naming them in `database_dates`. + """ + candidates = {} + for name in bd.databases: + metadata = bd.databases[name] + if REPRESENTATIVE_TIME not in metadata: + continue + if metadata.get(SCENARIOS): + logger.info( + f"Skipping database '{name}': it holds " + f"{len(metadata[SCENARIOS])} scenarios, so the point in time it " + f"represents is ambiguous. Map it explicitly with `database_dates` " + f"if you want to use it anyway." + ) + continue + candidates[name] = metadata + return candidates + + +def _as_set(value: Any) -> set: + """Compare list-valued metadata (e.g. `external_scenarios`) order-insensitively.""" + if isinstance(value, (list, tuple, set)): + return {str(item) for item in value} + return {str(value)} + + +def _values_match(declared: Any, wanted: Any) -> bool: + if isinstance(declared, (list, tuple, set)) or isinstance(wanted, (list, tuple, set)): + return _as_set(declared) == _as_set(wanted) + return str(declared) == str(wanted) + + +def _check_filter_keys(scenario: dict, candidates: dict[str, dict]) -> None: + """Reject filter keys no database declares, instead of silently matching nothing.""" + declared = set() + for metadata in candidates.values(): + declared.update(set(metadata) - BRIGHTWAY_METADATA_KEYS) + unknown = sorted(set(scenario) - declared) + if not unknown: + return + available = ", ".join(sorted(declared)) or "none" + raise ValueError( + f"No database in this project declares the metadata key(s) " + f"{unknown}. Keys declared by the databases of this project: {available}. " + f"Add the metadata with `bw_timex.set_database_metadata`, or check the " + f"spelling of your `scenario` filter." + ) + + +def _scenario_signature(metadata: dict) -> tuple: + return tuple( + (key, tuple(sorted(_as_set(metadata[key]))) if key in metadata else None) + for key in SCENARIO_SIGNATURE_KEYS + ) + + +def _format_scenario_sets(groups: dict[tuple, list[str]]) -> str: + """One line per scenario set, naming only the keys that actually differ.""" + differing = [ + key + for index, key in enumerate(SCENARIO_SIGNATURE_KEYS) + if len({signature[index][1] for signature in groups}) > 1 + ] + lines = [] + for signature, names in groups.items(): + values = dict(signature) + description = ", ".join( + f"{key}={', '.join(values[key]) if values[key] else 'not set'}" + for key in differing + ) + lines.append(f" {description}: {', '.join(sorted(names))}") + return "\n".join(lines) + + +def _check_unambiguous(candidates: dict[str, dict]) -> None: + groups = defaultdict(list) + for name, metadata in candidates.items(): + if any(key in metadata for key in SCENARIO_SIGNATURE_KEYS): + groups[_scenario_signature(metadata)].append(name) + if len(groups) <= 1: + return + raise ValueError( + f"Several background scenarios found in this project:\n" + f"{_format_scenario_sets(groups)}\n" + f"Select one, e.g. scenario={{'pathway': '...'}}, or map the databases " + f"explicitly with `database_dates`." + ) + + +def resolve_database_dates_from_metadata( + scenario: dict | None = None, +) -> dict[str, datetime | str]: + """ + Map the databases of the current project to the points in time they represent. + + Reads the `representative_time` metadata of every registered database (see + [`set_database_metadata`][bw_timex.database_metadata.set_database_metadata]). + + If the project holds databases from more than one scenario (differing in + any of `SCENARIO_SIGNATURE_KEYS`, e.g. two premise pathways), this raises + a `ValueError` unless `scenario` narrows the selection down to one. + + Parameters + ---------- + scenario : dict, optional + Metadata a database must match to be included, e.g. + `{"iam_model": "remind", "pathway": "SSP2-PkBudg500"}`. Databases that + don't declare a filtered key at all are kept, so a filter narrows down + an ambiguous project without excluding databases that carry no + scenario metadata (e.g. a dynamic foreground). Raises `ValueError` if + a filter key is not declared by any database in the project. + + Returns + ------- + dict + Mapping of database name to `datetime` or `"dynamic"`, ready to be used + as `TimexLCA.database_dates`. + """ + candidates = _candidate_databases() + if scenario: + _check_filter_keys(scenario, candidates) + candidates = { + name: metadata + for name, metadata in candidates.items() + if all( + key not in metadata or _values_match(metadata[key], wanted) + for key, wanted in scenario.items() + ) + } + _check_unambiguous(candidates) + return { + name: _normalize_representative_time(metadata[REPRESENTATIVE_TIME], name) + for name, metadata in candidates.items() + } diff --git a/bw_timex/edge_extractor.py b/bw_timex/edge_extractor.py index 45202374..ce5921f5 100644 --- a/bw_timex/edge_extractor.py +++ b/bw_timex/edge_extractor.py @@ -319,7 +319,8 @@ def _candidate_databases_for_node(self, node_id: int) -> dict: f"one database at {date:%Y-%m-%d}: '{candidates[date]}' and " f"'{db_name}'. bw_timex cannot tell which one to use. Give " "the copy a distinct name, reference product or location, " - "or remove one of the two databases from `database_dates`." + "or remove one of the two databases from `database_dates` or " + "its `representative_time` metadata." ) candidates[date] = db_name diff --git a/bw_timex/errors.py b/bw_timex/errors.py new file mode 100644 index 00000000..94f3fc5b --- /dev/null +++ b/bw_timex/errors.py @@ -0,0 +1,15 @@ +"""Errors raised by `bw_timex`.""" + +from __future__ import annotations + + +class UnmappedDatabaseError(ValueError): + """A database reached by the graph traversal is missing from the mapping. + + `bw_timex` places every traversed process in time via the database it + lives in, so each of them must either represent a point in time or be + marked as `"dynamic"`. Databases holding the functional unit are treated + as dynamic automatically; every other database has to say what it + represents, through its `representative_time` metadata or through + `database_dates`. + """ diff --git a/bw_timex/timeline_builder.py b/bw_timex/timeline_builder.py index 252882cc..9b070542 100644 --- a/bw_timex/timeline_builder.py +++ b/bw_timex/timeline_builder.py @@ -10,6 +10,7 @@ from loguru import logger from .edge_extractor import Edge, EdgeExtractor, EdgeExtractorBFS +from .errors import UnmappedDatabaseError from .utils import ( convert_date_string_to_datetime, extract_date_as_integer, @@ -306,6 +307,8 @@ def build_timeline(self) -> pd.DataFrame: grouped_edges = self._drop_edges_of_unsupplied_consumers(grouped_edges) + self._check_traversed_databases_are_mapped(grouped_edges) + # add new processes to activity_time_mapping static_dbs = set(self.database_dates_static.keys()) if self.traverse_background else set() for row in grouped_edges.itertuples(): @@ -381,6 +384,70 @@ def build_timeline(self) -> pd.DataFrame: # underlying functions called by build_timeline() # ################################################### + def _check_traversed_databases_are_mapped(self, grouped_edges: pd.DataFrame) -> None: + """ + Check that every traversed process lives in a database that is mapped. + + `bw_timex` places a process in time via the database it lives in, so + every database the traversal reaches must either represent a point in + time or be marked as `"dynamic"`. Only the databases holding the + functional unit are treated as dynamic automatically - a foreground + split across several databases has to mark the other ones itself. A + database that is mapped nowhere has no node metadata loaded for it, + which would otherwise surface as a bare `KeyError` on a node id. + + Parameters + ---------- + grouped_edges : pd.DataFrame + The timeline edges, with `producer` and `consumer` node ids. + + Returns + ------- + None + + Raises + ------ + UnmappedDatabaseError + If any traversed process is in a database that is not mapped. + """ + node_ids = set(grouped_edges["producer"]).union(grouped_edges["consumer"]) + unmapped = sorted( + node_id + for node_id in node_ids + if node_id != -1 and node_id not in self.nodes + ) + if not unmapped: + return + + examples = {} + for node_id in unmapped: + node = bd.get_node(id=node_id) + examples.setdefault(node["database"], []).append(node["name"]) + + databases = ", ".join( + f"'{database}' (e.g. '{names[0]}'" + + (f", and {len(names) - 1} more" if len(names) > 1 else "") + + ")" + for database, names in examples.items() + ) + first = next(iter(examples)) + raise UnmappedDatabaseError( + f"The graph traversal reached processes in database(s) that are not " + f"mapped to a point in time: {databases}. `bw_timex` places every " + f"traversed process in time via its database, and only the " + f"database(s) holding the functional unit are treated as 'dynamic' " + f"automatically, so a foreground split across several databases has " + f"to mark the other ones itself.\n" + f"If '{first}' is part of your foreground, mark it as dynamic:\n" + f" bw_timex.set_database_metadata('{first}', representative_time='dynamic')\n" + f"If it represents a point in time, give it that date instead:\n" + f" bw_timex.set_database_metadata('{first}', " + f"representative_time=datetime(2030, 1, 1))\n" + f"Databases mapped for this calculation: " + f"{sorted(self.database_dates)}. When passing `database_dates` " + f"explicitly, it must list every database the traversal reaches." + ) + def check_database_names(self) -> None: """ Check that the strings of the databases exist in the databases of the Brightway project. @@ -578,7 +645,8 @@ def candidate_databases_for_producers(self, producers: set) -> dict: f"at {date:%Y-%m-%d}: '{already}' and '{node['database']}'. " "bw_timex cannot tell which one its temporal market should use. " "Give the copy a distinct name, reference product or location, " - "or remove one of the two databases from `database_dates`." + "or remove one of the two databases from `database_dates` or " + "its `representative_time` metadata." ) candidates[producer][date] = node["database"] matches[producer][node["database"]] = node.id diff --git a/bw_timex/timex_lca.py b/bw_timex/timex_lca.py index 6c1c7068..52d4f26e 100644 --- a/bw_timex/timex_lca.py +++ b/bw_timex/timex_lca.py @@ -31,6 +31,7 @@ from ._lci_cache import BACKGROUND_UNIT_LCI_CACHE, LCI_SOLVE_CACHE, NODES_CACHE FACTORIZE_SOLVES_THRESHOLD = 8 +from .database_metadata import resolve_database_dates_from_metadata from .dynamic_biosphere_builder import DynamicBiosphereBuilder from .helper_classes import InterDatabaseMapping, LazyActivity, TimeMappingDict from .matrix_modifier import MatrixModifier @@ -82,19 +83,32 @@ class TimexLCA: Examples -------- ```python + from bw_timex import TimexLCA, set_database_metadata + demand = {("my_foreground_database", "my_process"): 1} method = ("some_method_family", "some_category", "some_method") - database_dates = { - "my_background_database_one": datetime.strptime("2020", "%Y"), - "my_background_database_two": datetime.strptime("2030", "%Y"), - "my_background_database_three": datetime.strptime("2040", "%Y"), - # Several databases may share the same date, e.g. to keep your own - # modified copies of background processes in their own database: - "my_modified_background_2020": datetime.strptime("2020", "%Y"), - "my_foreground_database": "dynamic", - } - - tlca = TimexLCA(demand, method, database_dates) + + # Databases exported by premise >= 2.4.9.2 already know the point in + # time they represent. For your own databases, say so once: + set_database_metadata("my_background_database_one", representative_time=datetime(2020, 1, 1)) + set_database_metadata("my_background_database_two", representative_time=datetime(2030, 1, 1)) + + tlca = TimexLCA(demand, method) + + # ... or map the databases explicitly, which then replaces the metadata: + tlca = TimexLCA( + demand, + method, + database_dates={ + "my_background_database_one": datetime(2020, 1, 1), + "my_background_database_two": datetime(2030, 1, 1), + # Several databases may share the same date, e.g. to keep your own + # modified copies of background processes in their own database: + "my_modified_background_2020": datetime(2020, 1, 1), + "my_foreground_database": "dynamic", + }, + ) + tlca.build_timeline() # has many optional arguments tlca.lci() tlca.static_lcia() @@ -110,6 +124,7 @@ def __init__( demand: dict, method: tuple, database_dates: dict = None, + scenario: dict = None, use_global_lci_cache: bool = True, ) -> None: """ @@ -126,10 +141,31 @@ def __init__( Tuple defining the LCIA method, such as `('foo', 'bar')` or default methods, such as `("EF v3.1", "climate change", "global warming potential (GWP100)")` database_dates : dict, optional - Dictionary mapping database names to dates. Several databases may - share the same date, e.g. to keep your own modified copies of - background processes in their own database instead of writing - them into the shared background database for that vintage. + Fallback for mapping the databases yourself instead of letting + `bw_timex` read their metadata - useful for databases written by + premise < 2.4.9.2, which carry no metadata, or when you want to + override what the metadata says. Dictionary mapping database names + to the point in time they represent, as a `datetime`, or to + `"dynamic"` for databases whose processes are distributed over + time (typically the foreground). + Several databases may share the same date, e.g. to keep your own + modified copies of background processes in their own database + instead of writing them into the shared background database for + that vintage. If not given, the mapping is read from the + databases' own `representative_time` metadata (which premise + >= 2.4.9.2 writes when exporting, and which you can set yourself + with `bw_timex.set_database_metadata`). Passing this argument replaces + the metadata entirely: only the databases listed here are used. + scenario : dict, optional + Metadata a background database must match to be used, e.g. + `{"iam_model": "remind", "pathway": "SSP2-PkBudg500"}`. Reads the + scenario metadata written by premise >= 2.4.9.2 (or by you, with + `bw_timex.set_database_metadata`), so it does nothing for + databases that carry none. Only needed when the project + holds several scenarios - `TimexLCA` + raises and lists them otherwise. Databases that don't declare the + filtered key (your foreground, a hand-built vintage) are always + kept. Cannot be combined with `database_dates`. use_global_lci_cache : bool, optional If True (default), background unit LCI matrices are cached at module level and reused across `TimexLCA` objects within the @@ -146,17 +182,16 @@ def __init__( self.demand = demand self.method = method - self.database_dates = database_dates - - if not self.database_dates: - logger.info( - "No database_dates provided. Treating the databases containing the functional \ - unit as dynamic. No remapping of inventories to time explicit databases will be done." - ) - self.database_dates = {key[0]: "dynamic" for key in demand.keys()} + self.scenario = scenario + self.database_dates = self._resolve_database_dates( + demand=demand, database_dates=database_dates, scenario=scenario + ) TimexLCAInputs( - demand=self.demand, method=self.method, database_dates=self.database_dates + demand=self.demand, + method=self.method, + database_dates=self.database_dates, + scenario=self.scenario, ) # Filled in by `prepare_base_lca_inputs`: the databases the base LCA @@ -231,6 +266,79 @@ def __init__( logger.info("TimexLCA initialized.") + @staticmethod + def _resolve_database_dates( + demand: dict, database_dates: dict | None, scenario: dict | None + ) -> dict: + """Map databases to the points in time they represent. + + Either from the explicit `database_dates` argument, which is then the + whole mapping, or from the databases' own `representative_time` + metadata. Databases holding the demand default to `"dynamic"`. + + Raises + ------ + ValueError + If both `database_dates` and `scenario` are given (`scenario` + only selects among databases resolved from metadata, so it makes + no sense once `database_dates` already gives the whole mapping), + or if `scenario` is given but no surviving database positively + declares one of its keys - almost always a typo in one of its + keys or values, since a filter that legitimately excludes + everything would leave nothing for `TimexLCA` to compute with. + A database that doesn't declare a filtered key at all is kept by + the filter (see `resolve_database_dates_from_metadata`), so + checking whether the *resolved mapping* is empty is not enough: + it stays non-empty whenever such a database happens to be + present, even though the filter matched none of the databases it + was meant to select among. + """ + if database_dates is not None: + if scenario: + raise ValueError( + "`scenario` selects background databases by their metadata and " + "only applies when `database_dates` is not given. Pass one or " + "the other." + ) + return dict(database_dates) + + resolved = resolve_database_dates_from_metadata(scenario) + + filter_matched = scenario and any( + key in bd.databases[name] for name in resolved for key in scenario + ) + + if scenario and not filter_matched: + declared = {} + for name in bd.databases: + metadata = bd.databases[name] + for key in scenario: + if key in metadata: + declared.setdefault(key, set()).add(str(metadata[key])) + details = "; ".join( + f"'{key}': " + f"{sorted(declared[key]) if key in declared else 'not declared by any database'}" + for key in scenario + ) + raise ValueError( + f"scenario={scenario!r} matched no database in this project. " + f"Values actually declared for its key(s) by this project's " + f"databases: {details}. Check for a typo in the filter." + ) + elif not resolved: + logger.info( + "No database_dates provided, and no database in this project carries " + "`representative_time` metadata. Treating the databases containing the " + "functional unit as dynamic. No remapping of inventories to time " + "explicit databases will be done." + ) + + for key in demand: + database = bd.get_node(id=get_id(key))["database"] + resolved.setdefault(database, "dynamic") + + return resolved + ######################################## # Main functions to be called by users # ######################################## diff --git a/bw_timex/validation.py b/bw_timex/validation.py index 215dd81c..0db07e9a 100644 --- a/bw_timex/validation.py +++ b/bw_timex/validation.py @@ -18,6 +18,7 @@ class TimexLCAInputs(BaseModel): demand: dict method: tuple database_dates: Optional[dict] = None + scenario: Optional[dict] = None @field_validator("demand") @classmethod @@ -71,6 +72,26 @@ def validate_database_dates(cls, v: Optional[dict]) -> Optional[dict]: ) return v + @field_validator("scenario") + @classmethod + def validate_scenario(cls, v: Optional[dict]) -> Optional[dict]: + if v is None: + return v + if not v: + raise ValueError("scenario must be a non-empty dictionary if provided.") + for key, value in v.items(): + if not isinstance(key, str): + raise ValueError( + f"scenario keys must be strings (database metadata keys), got " + f"{type(key).__name__}." + ) + if not isinstance(value, (str, int, float, bool, list, tuple)): + raise ValueError( + f"scenario values must be scalars or lists of scalars, got " + f"{type(value).__name__} for key '{key}'." + ) + return v + @model_validator(mode="after") def validate_demand_in_dynamic_databases(self) -> "TimexLCAInputs": if self.database_dates is None: @@ -84,7 +105,8 @@ def validate_demand_in_dynamic_databases(self) -> "TimexLCAInputs": if act["database"] not in dynamic_database_names: raise ValueError( f"Demand activity {act} from database {act['database']}: " - f"This database is not marked as 'dynamic' in database_dates. " + f"This database is mapped to a date rather than 'dynamic' " + f"(via `database_dates` or its `representative_time` metadata). " f"Please check." ) return self @@ -243,3 +265,27 @@ def validate_bio_flows(cls, v: list) -> list: f"bio_flows must contain integer database IDs, got {type(item).__name__}: {item}." ) return v + + +class DatabaseMetadataInputs(BaseModel): + """Validates inputs to set_database_metadata""" + + model_config = {"arbitrary_types_allowed": True} + + database: str + metadata: dict + + @field_validator("metadata") + @classmethod + def validate_metadata(cls, v: dict) -> dict: + if not v: + raise ValueError( + "Provide at least one metadata field, e.g. " + "`representative_time=datetime(2030, 1, 1)`." + ) + for key in v: + if not isinstance(key, str): + raise ValueError( + f"Metadata keys must be strings, got {type(key).__name__}: {key}." + ) + return v diff --git a/docs/api/database_metadata.md b/docs/api/database_metadata.md new file mode 100644 index 00000000..c8e5ba9b --- /dev/null +++ b/docs/api/database_metadata.md @@ -0,0 +1,13 @@ +--- +icon: lucide/calendar-clock +tags: + - api +--- + +# Database metadata + +Reading and writing what a Brightway database represents: the point in time +(`representative_time`) and, for prospective databases, the scenario it was built +for. + +::: bw_timex.database_metadata diff --git a/docs/api/errors.md b/docs/api/errors.md new file mode 100644 index 00000000..42c5edd4 --- /dev/null +++ b/docs/api/errors.md @@ -0,0 +1,12 @@ +--- +icon: lucide/triangle-alert +tags: + - api +--- + +# Errors + +Errors raised by `bw_timex`, importable from the top level (e.g. +`from bw_timex import UnmappedDatabaseError`). + +::: bw_timex.errors diff --git a/docs/api/index.md b/docs/api/index.md index a242c0e0..9bd56183 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -15,4 +15,6 @@ The main user-facing class is [`TimexLCA`](timex_lca.md). It orchestrates the ot - [`dynamic_biosphere_builder`](dynamic_biosphere_builder.md) — builds the dynamic biosphere matrix carrying emission timing. - [`edge_extractor`](edge_extractor.md) — extracts and convolves temporal distributions during graph traversal. - [`helper_classes`](helper_classes.md) — supporting data structures used across the package. +- [`database_metadata`](database_metadata.md) — reads and writes what a database represents: its `representative_time` and its scenario. +- [`errors`](errors.md) — the errors `bw_timex` raises. - [`utils`](utils.md) — utility functions. diff --git a/docs/content/getting_started/adding_temporal_information.md b/docs/content/getting_started/adding_temporal_information.md index 76a08acc..f7b4c0c2 100644 --- a/docs/content/getting_started/adding_temporal_information.md +++ b/docs/content/getting_started/adding_temporal_information.md @@ -247,23 +247,40 @@ end ) ``` -So, as you can see, the processes at specific time steps reside within a separate normal Brightway database. To hand them to `bw_timex`, we just need to define a dictionary that maps the names of time-specific databases to the point in time that they represent: +So, as you can see, the processes at specific time steps reside within a separate normal +Brightway database. `bw_timex` picks these up automatically, as long as each database +says which point in time it represents: ```python from datetime import datetime +from bw_timex import set_database_metadata -# Note: The foreground does not represent a specific point in time, but should -# later be dynamically distributed over time -database_dates = { - "background": datetime.strptime("2020", "%Y"), - "background_2030": datetime.strptime("2030", "%Y"), - "foreground": "dynamic", -} +set_database_metadata("background", representative_time=datetime(2020, 1, 1)) +set_database_metadata("background_2030", representative_time=datetime(2030, 1, 1)) ``` +You only do this once per database - it is stored in your Brightway project. Databases +exported by [premise](https://premise.readthedocs.io/en/latest/introduction.html) +**>= 2.4.9.2** bring this metadata with them, so there is nothing to do for those; +for databases from an earlier premise, set it yourself as above. The foreground doesn't +represent a specific point in time and is distributed over time instead; `bw_timex` +treats the databases holding your functional unit that way automatically. + +!!! tip "Foreground split across several databases" + + Only the databases holding the functional unit become dynamic automatically. Mark + any other foreground database yourself: + + ```python + set_database_metadata("my_intermediate_foreground", representative_time="dynamic") + ``` + + Otherwise `build_timeline()` raises an `UnmappedDatabaseError`, naming the database + it could not place in time. + !!! tip "Data sources" - You can use whatever data source you want for the time-specific process data. A nice package from the Brightway cosmos that can help you is [premise](https://premise.readthedocs.io/en/latest/introduction.html). + You can use whatever data source you want for the time-specific process data. [premise](https://premise.readthedocs.io/en/latest/introduction.html) is a nice package from the Brightway cosmos, but you can also use any custom scenario. ### Several databases for the same point in time @@ -272,13 +289,10 @@ background processes: keep the modified copies in your own database per point in time, instead of writing them into ecoinvent or premise. ```python -database_dates = { - "ecoinvent_2020": datetime.strptime("2020", "%Y"), - "ecoinvent_2030": datetime.strptime("2030", "%Y"), - "my_background_2020": datetime.strptime("2020", "%Y"), # your modified copies - "my_background_2030": datetime.strptime("2030", "%Y"), - "foreground": "dynamic", -} +set_database_metadata("ecoinvent_2020", representative_time=datetime(2020, 1, 1)) +set_database_metadata("ecoinvent_2030", representative_time=datetime(2030, 1, 1)) +set_database_metadata("my_background_2020", representative_time=datetime(2020, 1, 1)) +set_database_metadata("my_background_2030", representative_time=datetime(2030, 1, 1)) ``` For each process, `bw_timex` interpolates only between the databases that actually diff --git a/docs/content/getting_started/build_process_timeline.md b/docs/content/getting_started/build_process_timeline.md index ebedea1f..00d800ab 100644 --- a/docs/content/getting_started/build_process_timeline.md +++ b/docs/content/getting_started/build_process_timeline.md @@ -8,7 +8,9 @@ tags: # Step 2 - Building the process timeline -With all the temporal information prepared, we can now instantiate our TimexLCA object. This is very similar to a normal Brightway LCA object, but with the additional argument of our `database_dates`: +With all the temporal information prepared, we can now instantiate our TimexLCA object. +This is just like a normal Brightway LCA object - the timing of the background databases +comes from their metadata: ```python from bw_timex import TimexLCA @@ -16,10 +18,42 @@ from bw_timex import TimexLCA tlca = TimexLCA( demand={("foreground", "A"): 1}, method=("our", "method"), - database_dates=database_dates, ) ``` +The metadata is written by premise >= 2.4.9.2, or by you with +`set_database_metadata` (see [Step 1](adding_temporal_information.md)). + +If your project holds several IAM scenarios, say which one you want - `bw_timex` lists +what it found rather than guessing: + +```python +tlca = TimexLCA( + demand={("foreground", "A"): 1}, + method=("our", "method"), + scenario={"pathway": "SSP2-PkBudg500"}, +) +``` + +Any metadata key filters (`iam_model`, `pathway`, `system_model`, ...), and databases +that don't carry it - your foreground, your own vintages - are kept. Comparing scenarios +is the same script in a loop over filters. + +!!! tip "Mapping the databases by hand" + + `database_dates` maps database names to dates yourself and replaces the metadata + entirely: + + ```python + tlca = TimexLCA(demand, method, database_dates={ + "background": datetime(2020, 1, 1), + "background_2030": datetime(2030, 1, 1), + "foreground": "dynamic", + }) + ``` + + Handy to restrict a calculation to a subset of your project. + Using our new `tlca` object, we can now build the timeline of processes that leads to our functional unit "A". If not specified otherwise, it's assumed that the demand occurs in the current year. In our case, we're specifying the time of demand to the year 2024, with the attribute 'starting_datetime`.. Building the timeline is very simple: ```python tlca.build_timeline(starting_datetime=datetime.strptime("2024-01-01", "%Y-%m-%d")) diff --git a/docs/content/getting_started/quickstart.md b/docs/content/getting_started/quickstart.md index 61e62ed9..c1b292d4 100644 --- a/docs/content/getting_started/quickstart.md +++ b/docs/content/getting_started/quickstart.md @@ -6,7 +6,7 @@ tags: # Quick Start -A condensed reference for using `bw_timex`. For a step-by-step introduction, see the [Walkthrough](index.md). For the underlying framework, see the [Theory](../theory.md) page. For more, see the [Examples](../examples/index.md) or the [API Reference](../../api/index.md). +Condensed reference for `bw_timex`. For a step-by-step introduction, see the [Walkthrough](index.md). For the underlying framework, see the [Theory](../theory.md) page. For more, see the [Examples](../examples/index.md) or the [API Reference](../../api/index.md). --- @@ -38,6 +38,7 @@ from bw_timex import ( TemporalDistribution, TimexLCA, add_temporal_distribution_to_exchange, + set_database_metadata, ) # 1. Set up Brightway project @@ -55,18 +56,15 @@ add_temporal_distribution_to_exchange( output_database="foreground", ) -# 3. Map your time-specific background databases to points in time -database_dates = { - "background": datetime.strptime("2020", "%Y"), - "background_2030": datetime.strptime("2030", "%Y"), - "foreground": "dynamic", # gets distributed over time -} +# 3. Say what your time-specific background databases represent +# (databases from premise >= 2.4.9.2 already know - skip this for them) +set_database_metadata("background", representative_time=datetime(2020, 1, 1)) +set_database_metadata("background_2030", representative_time=datetime(2030, 1, 1)) # 4. Create the TimexLCA object tlca = TimexLCA( demand={("foreground", "A"): 1}, method=("our", "method"), - database_dates=database_dates, ) # 5. Build the process timeline @@ -93,7 +91,7 @@ tlca.plot_dynamic_characterized_inventory() | What you want to express | How | Where | |---|---|---| | *When* an exchange happens | `temporal_distribution` on the exchange | any foreground exchange (and background ones, if you traverse the background) | -| *How the background changes* over time | one database per point in time + `database_dates` | background databases | +| *How the background changes* over time | one database per point in time, each with `representative_time` metadata | background databases | | *How a foreground exchange changes* over time | `temporal_evolution_factors` / `temporal_evolution_amounts` on the exchange (`bw_timex>0.3.4`) | foreground exchanges | ```python @@ -128,8 +126,11 @@ add_temporal_evolution_to_exchange( Absolute dates (`dtype="datetime64[s]"`) are also allowed in a `TemporalDistribution`, e.g. for the timing of the functional unit itself. Relative dates (`dtype="timedelta64[Y]"`) are relative to the consuming process. Several databases may -share the same date in `database_dates`, e.g. if you keep modified copies of background -processes in their own database instead of writing them into the shared vintage. +represent the same point in time, e.g. if you keep modified copies of background +processes in their own database instead of writing them into the shared vintage. See +[Step 1](adding_temporal_information.md) for the database metadata, and +[Step 2](build_process_timeline.md) for scenario selection and for mapping databases +explicitly with `database_dates`. --- @@ -141,10 +142,16 @@ processes in their own database instead of writing them into the shared vintage. TimexLCA( demand={("foreground", "A"): 1}, # Node, (database, code) tuple, or int id method=("our", "method"), - database_dates=database_dates, # optional, but usually what you want + database_dates=None, # fallback: map the databases yourself, overriding metadata + scenario=None, # pick one scenario, when the project holds several ) ``` +Both are optional. `scenario` narrows down what `bw_timex` reads from the database +metadata (written by premise >= 2.4.9.2, or by you with `set_database_metadata`); +`database_dates` is the fallback for when you'd rather write the mapping out yourself, +and it overrides the metadata entirely. + ### `build_timeline()` | Argument | Default | Description | diff --git a/docs/superpowers/plans/2026-08-21-representative-time-metadata.md b/docs/superpowers/plans/2026-08-21-representative-time-metadata.md new file mode 100644 index 00000000..e23a9643 --- /dev/null +++ b/docs/superpowers/plans/2026-08-21-representative-time-metadata.md @@ -0,0 +1,1510 @@ +# Representative time as database metadata — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** `TimexLCA` learns the point in time each background database represents from that database's own Brightway metadata (`representative_time`, as written by premise), so `database_dates` becomes an optional explicit override instead of a required argument. + +**Architecture:** A new module `bw_timex/database_metadata.py` owns everything about database metadata: writing it (`set_database_metadata`) and resolving a `{database: datetime | "dynamic"}` mapping out of the project (`resolve_database_dates_from_metadata`), including scenario filtering and the ambiguity error. `TimexLCA.__init__` calls it in one place and is otherwise untouched: everything downstream still consumes `self.database_dates`. + +**Tech Stack:** Python 3.10+, `bw2data` (database metadata lives in `bd.databases[name]`, a JSON-serialized dict), `pydantic` (input validation, see `bw_timex/validation.py`), `loguru`, `pytest` with `bw2data.tests.bw2test` fixtures. + +**Spec:** `docs/superpowers/specs/2026-08-21-representative-time-metadata-design.md` + +## Global Constraints + +- Run everything with the project venv: `.venv/bin/python`, `.venv/bin/pytest`. +- `database_dates` semantics do not change. When it is passed, it is the whole mapping and metadata is never read. +- `bd.databases` is serialized to JSON. A `datetime` written into it breaks `bd.databases.flush()`. Every date stored in metadata is an ISO 8601 string. +- Metadata keys, exactly as premise writes them: `representative_time`, `iam_model`, `pathway`, `system_model`, `ecoinvent_version`, `premise_version`, `external_scenarios`, `scenarios`. +- Scenario identity keys (the ambiguity signature) are exactly: `("iam_model", "pathway", "system_model", "ecoinvent_version", "external_scenarios")`. `premise_version` is deliberately not one of them. +- `notebooks/examples/paper_case_study.ipynb` must not be modified by any task. +- Commit messages carry no AI attribution and no `Co-Authored-By` trailer. + +--- + +### Task 1: `database_metadata` module — writing metadata + +**Files:** +- Create: `bw_timex/database_metadata.py` +- Modify: `bw_timex/validation.py` (append a `DatabaseMetadataInputs` model) +- Modify: `bw_timex/__init__.py` (export `set_database_metadata`) +- Create: `tests/test_database_metadata.py` + +**Interfaces:** +- Consumes: nothing from earlier tasks. +- Produces: + - `bw_timex.database_metadata.set_database_metadata(database: str | bd.Database, **metadata) -> dict` + - constants `REPRESENTATIVE_TIME: str = "representative_time"`, `SCENARIOS: str = "scenarios"`, `DYNAMIC: str = "dynamic"`, `SCENARIO_SIGNATURE_KEYS: tuple[str, ...]`, `BRIGHTWAY_METADATA_KEYS: frozenset[str]` + - `bw_timex.database_metadata._normalize_representative_time(value, database: str) -> datetime | str` + +- [ ] **Step 1: Write the failing tests** + +Create `tests/test_database_metadata.py`: + +```python +"""Tests for reading and writing what a Brightway database represents.""" + +from datetime import datetime + +import bw2data as bd +import pytest + +from bw_timex import set_database_metadata + +# ─── Tests for set_database_metadata ─── + + +@pytest.mark.usefixtures("temporal_grouping_db_monthly") +class TestSetDatabaseMetadata: + + def test_datetime_is_stored_as_iso_string(self): + set_database_metadata("db_2022", representative_time=datetime(2022, 1, 1)) + assert bd.databases["db_2022"]["representative_time"] == "2022-01-01T00:00:00" + + def test_iso_string_is_stored_as_given(self): + set_database_metadata("db_2022", representative_time="2022-01-01") + assert bd.databases["db_2022"]["representative_time"] == "2022-01-01" + + def test_dynamic_is_allowed(self): + set_database_metadata("foreground", representative_time="dynamic") + assert bd.databases["foreground"]["representative_time"] == "dynamic" + + def test_scenario_fields_are_stored(self): + set_database_metadata( + "db_2022", + representative_time=datetime(2022, 1, 1), + iam_model="remind", + pathway="SSP2-PkBudg500", + ) + assert bd.databases["db_2022"]["iam_model"] == "remind" + assert bd.databases["db_2022"]["pathway"] == "SSP2-PkBudg500" + + def test_database_object_is_accepted(self): + set_database_metadata( + bd.Database("db_2022"), representative_time=datetime(2022, 1, 1) + ) + assert bd.databases["db_2022"]["representative_time"] == "2022-01-01T00:00:00" + + def test_existing_metadata_is_kept(self): + before = bd.databases["db_2022"]["backend"] + set_database_metadata("db_2022", representative_time=datetime(2022, 1, 1)) + assert bd.databases["db_2022"]["backend"] == before + + def test_survives_flush_and_reload(self): + set_database_metadata("db_2022", representative_time=datetime(2022, 1, 1)) + bd.databases.__init__() # re-read from disk + assert bd.databases["db_2022"]["representative_time"] == "2022-01-01T00:00:00" + + def test_unregistered_database_raises(self): + with pytest.raises(ValueError, match="not registered"): + set_database_metadata("no_such_db", representative_time=datetime(2022, 1, 1)) + + def test_unparseable_representative_time_raises(self): + with pytest.raises(ValueError, match="representative_time"): + set_database_metadata("db_2022", representative_time="whenever") + + def test_non_serializable_value_raises(self): + with pytest.raises(ValueError, match="JSON"): + set_database_metadata("db_2022", pathway=object()) + + def test_no_metadata_raises(self): + with pytest.raises(ValueError, match="at least one"): + set_database_metadata("db_2022") +``` + +- [ ] **Step 2: Run the tests to verify they fail** + +Run: `.venv/bin/pytest tests/test_database_metadata.py -v` +Expected: FAIL — `ImportError: cannot import name 'set_database_metadata' from 'bw_timex'` + +- [ ] **Step 3: Write the module** + +Create `bw_timex/database_metadata.py`: + +```python +"""Read and write what a Brightway database represents. + +`bw_timex` needs to know which point in time each background database stands +for. That information is stored in the database's own Brightway metadata +(`bw2data.databases[name]`), where premise also writes it when it exports a +prospective database: + +```python +{ + "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", +} +``` + +Brightway stores this mapping as JSON, so dates are kept as ISO 8601 strings. +""" + +from __future__ import annotations + +import json +from datetime import datetime +from typing import Any + +import bw2data as bd + +REPRESENTATIVE_TIME = "representative_time" +SCENARIOS = "scenarios" +DYNAMIC = "dynamic" + +#: Metadata keys that identify the scenario a database represents. Two +#: databases differing in any of these represent different scenarios. +#: `premise_version` is deliberately absent: re-running premise on the same +#: pathway must not look like a second scenario. +SCENARIO_SIGNATURE_KEYS = ( + "iam_model", + "pathway", + "system_model", + "ecoinvent_version", + "external_scenarios", +) + +#: Keys Brightway maintains itself, filtered out when reporting to the user +#: which metadata a project's databases carry. +BRIGHTWAY_METADATA_KEYS = frozenset( + { + "backend", + "depends", + "dirty", + "format", + "geocollections", + "modified", + "number", + "processed", + "searchable", + } +) + + +def _database_name(database: Any) -> str: + """The name of a database given either as a name or as a `bd.Database`.""" + name = getattr(database, "name", database) + if not isinstance(name, str): + raise ValueError( + f"database must be a database name or a bw2data Database, got " + f"{type(database).__name__}." + ) + return name + + +def _normalize_representative_time(value: Any, database: str) -> datetime | str: + """Turn a stored `representative_time` into a datetime or `"dynamic"`.""" + if isinstance(value, datetime): + return value + if isinstance(value, str): + if value == DYNAMIC: + return DYNAMIC + try: + return datetime.fromisoformat(value) + except ValueError: + raise ValueError( + f"Database '{database}' has an invalid `{REPRESENTATIVE_TIME}` " + f"metadata value: {value!r}. Expected an ISO 8601 datetime string " + f"(e.g. '2030-01-01'), a datetime, or '{DYNAMIC}'." + ) from None + raise ValueError( + f"Database '{database}' has an invalid `{REPRESENTATIVE_TIME}` metadata " + f"value of type {type(value).__name__}: {value!r}. Expected an ISO 8601 " + f"datetime string, a datetime, or '{DYNAMIC}'." + ) + + +def set_database_metadata(database: str | bd.Database, **metadata) -> dict: + """ + Store what a database represents in its Brightway metadata. + + Use this for databases that don't bring the metadata themselves, e.g. + databases you built yourself or that were exported by a premise version + older than the one writing scenario metadata. `TimexLCA` reads + `representative_time` from all databases of the project to map them to + points in time, so this replaces passing `database_dates`. + + Parameters + ---------- + database : str or bw2data.Database + Name of the database, or the database itself. Must be registered. + **metadata : + Metadata to store. `representative_time` accepts a `datetime`, an ISO + 8601 string, or `"dynamic"` and is always stored as a string, because + Brightway serializes database metadata to JSON. Any other key is stored + as given and must be JSON-serializable. Keys that premise writes, and + that `TimexLCA(scenario=...)` can select on, are `iam_model`, + `pathway`, `system_model`, `ecoinvent_version` and `premise_version`. + + Returns + ------- + dict + The database's metadata after the update. + + Examples + -------- + ```python + set_database_metadata("db_2030", representative_time=datetime(2030, 1, 1)) + set_database_metadata( + "my_2050_variant", + representative_time="2050-01-01", + iam_model="remind", + pathway="SSP2-PkBudg500", + ) + ``` + """ + from .validation import DatabaseMetadataInputs + + name = _database_name(database) + DatabaseMetadataInputs(database=name, metadata=metadata) + + if name not in bd.databases: + raise ValueError( + f"Database '{name}' is not registered in this Brightway project. " + f"Available databases: {sorted(bd.databases)}." + ) + + serialized = {} + for key, value in metadata.items(): + if key == REPRESENTATIVE_TIME: + normalized = _normalize_representative_time(value, name) + serialized[key] = ( + normalized if normalized == DYNAMIC else normalized.isoformat() + ) + continue + try: + json.dumps(value) + except TypeError: + raise ValueError( + f"Metadata value for '{key}' is not JSON-serializable: {value!r}. " + f"Brightway stores database metadata as JSON." + ) from None + serialized[key] = value + + bd.databases[name].update(serialized) + bd.databases.flush() + return bd.databases[name] +``` + +- [ ] **Step 4: Add the validation model** + +Append to `bw_timex/validation.py`: + +```python +class DatabaseMetadataInputs(BaseModel): + """Validates inputs to set_database_metadata""" + + model_config = {"arbitrary_types_allowed": True} + + database: str + metadata: dict + + @field_validator("metadata") + @classmethod + def validate_metadata(cls, v: dict) -> dict: + if not v: + raise ValueError( + "Provide at least one metadata field, e.g. " + "`representative_time=datetime(2030, 1, 1)`." + ) + for key in v: + if not isinstance(key, str): + raise ValueError( + f"Metadata keys must be strings, got {type(key).__name__}: {key}." + ) + return v +``` + +- [ ] **Step 5: Export it** + +In `bw_timex/__init__.py`, add the import next to the other helper imports and the name to `__all__` (in the `# utils` block, alphabetically after `plot_characterized_inventory_as_waterfall`): + +```python +from .database_metadata import set_database_metadata +``` + +```python + "set_database_metadata", +``` + +- [ ] **Step 6: Run the tests to verify they pass** + +Run: `.venv/bin/pytest tests/test_database_metadata.py -v` +Expected: PASS (12 tests) + +- [ ] **Step 7: Commit** + +```bash +git add bw_timex/database_metadata.py bw_timex/validation.py bw_timex/__init__.py tests/test_database_metadata.py +git commit -m "feat: add set_database_metadata to store what a database represents" +``` + +--- + +### Task 2: Resolve database dates from metadata + +**Files:** +- Modify: `bw_timex/database_metadata.py` +- Modify: `tests/test_database_metadata.py` + +**Interfaces:** +- Consumes: `REPRESENTATIVE_TIME`, `SCENARIOS`, `DYNAMIC`, `_normalize_representative_time`, `set_database_metadata` from Task 1. +- Produces: `resolve_database_dates_from_metadata(scenario: dict | None = None) -> dict[str, datetime | str]` — every registered database carrying `representative_time`, mapped to a `datetime` or `"dynamic"`. Multi-scenario databases are excluded. Scenario filtering and the ambiguity error come in Task 3; this task's version accepts the argument and ignores it. + +- [ ] **Step 1: Write the failing tests** + +Append to `tests/test_database_metadata.py`: + +```python +from bw_timex.database_metadata import resolve_database_dates_from_metadata + +# ─── Tests for resolving database dates from metadata ─── + + +@pytest.mark.usefixtures("temporal_grouping_db_monthly") +class TestResolveFromMetadata: + + def test_empty_project_metadata_resolves_to_nothing(self): + assert resolve_database_dates_from_metadata() == {} + + def test_iso_strings_resolve_to_datetimes(self): + set_database_metadata("db_2022", representative_time="2022-01-01") + set_database_metadata("db_2024", representative_time="2024-01-01") + assert resolve_database_dates_from_metadata() == { + "db_2022": datetime(2022, 1, 1), + "db_2024": datetime(2024, 1, 1), + } + + def test_dynamic_metadata_resolves_to_dynamic(self): + set_database_metadata("db_2022", representative_time="2022-01-01") + set_database_metadata("foreground", representative_time="dynamic") + resolved = resolve_database_dates_from_metadata() + assert resolved["foreground"] == "dynamic" + assert resolved["db_2022"] == datetime(2022, 1, 1) + + def test_databases_without_metadata_are_ignored(self): + set_database_metadata("db_2022", representative_time="2022-01-01") + assert set(resolve_database_dates_from_metadata()) == {"db_2022"} + + def test_multi_scenario_database_is_skipped(self): + set_database_metadata("db_2022", representative_time="2022-01-01") + set_database_metadata( + "db_2024", + representative_time="2024-01-01", + scenarios=[ + {"pathway": "SSP2-Base", "representative_time": "2024-01-01"}, + {"pathway": "SSP2-PkBudg500", "representative_time": "2024-01-01"}, + ], + ) + assert set(resolve_database_dates_from_metadata()) == {"db_2022"} + + def test_invalid_metadata_value_raises_naming_the_database(self): + bd.databases["db_2022"]["representative_time"] = "whenever" + bd.databases.flush() + with pytest.raises(ValueError, match="db_2022"): + resolve_database_dates_from_metadata() +``` + +- [ ] **Step 2: Run the tests to verify they fail** + +Run: `.venv/bin/pytest tests/test_database_metadata.py::TestResolveFromMetadata -v` +Expected: FAIL — `ImportError: cannot import name 'resolve_database_dates_from_metadata'` + +- [ ] **Step 3: Implement discovery** + +Append to `bw_timex/database_metadata.py` (and add `from loguru import logger` to the imports): + +```python +def _candidate_databases() -> dict[str, dict]: + """Registered databases that declare a `representative_time`. + + Multi-scenario databases (superstructure and scenario-array exports, which + carry a `scenarios` list) are skipped: `bw_timex` needs one technosphere + per point in time and cannot pick a scenario out of such a database. They + can still be used by naming them in `database_dates`. + """ + candidates = {} + for name in bd.databases: + metadata = bd.databases[name] + if REPRESENTATIVE_TIME not in metadata: + continue + if metadata.get(SCENARIOS): + logger.info( + f"Skipping database '{name}': it holds " + f"{len(metadata[SCENARIOS])} scenarios, so the point in time it " + f"represents is ambiguous. Map it explicitly with `database_dates` " + f"if you want to use it anyway." + ) + continue + candidates[name] = metadata + return candidates + + +def resolve_database_dates_from_metadata( + scenario: dict | None = None, +) -> dict[str, datetime | str]: + """ + Map the databases of the current project to the points in time they represent. + + Reads the `representative_time` metadata of every registered database (see + [`set_database_metadata`][bw_timex.database_metadata.set_database_metadata]). + + Parameters + ---------- + scenario : dict, optional + Metadata a database must match to be included, e.g. + `{"iam_model": "remind", "pathway": "SSP2-PkBudg500"}`. Databases that + don't declare a filtered key at all are kept. + + Returns + ------- + dict + Mapping of database name to `datetime` or `"dynamic"`, ready to be used + as `TimexLCA.database_dates`. + """ + candidates = _candidate_databases() + return { + name: _normalize_representative_time(metadata[REPRESENTATIVE_TIME], name) + for name, metadata in candidates.items() + } +``` + +- [ ] **Step 4: Run the tests to verify they pass** + +Run: `.venv/bin/pytest tests/test_database_metadata.py -v` +Expected: PASS (18 tests) + +- [ ] **Step 5: Commit** + +```bash +git add bw_timex/database_metadata.py tests/test_database_metadata.py +git commit -m "feat: resolve database dates from representative_time metadata" +``` + +--- + +### Task 3: Scenario filtering and the ambiguity error + +**Files:** +- Modify: `bw_timex/database_metadata.py` +- Modify: `tests/test_database_metadata.py` + +**Interfaces:** +- Consumes: `resolve_database_dates_from_metadata`, `_candidate_databases`, `SCENARIO_SIGNATURE_KEYS`, `BRIGHTWAY_METADATA_KEYS` from Tasks 1–2. +- Produces: `resolve_database_dates_from_metadata(scenario)` now filters, and raises `ValueError` on an unknown filter key or on several scenario sets. No new public names. + +- [ ] **Step 1: Write the failing tests** + +Append to `tests/test_database_metadata.py`: + +```python +# ─── Tests for scenario selection ─── + + +@pytest.mark.usefixtures("temporal_grouping_db_monthly") +class TestScenarioSelection: + + @pytest.fixture(autouse=True) + def two_scenarios(self): + """db_2022 and db_2024 hold the same year in two different pathways.""" + set_database_metadata( + "db_2022", + representative_time="2022-01-01", + iam_model="remind", + pathway="SSP2-PkBudg500", + premise_version="2.4.9.1", + ) + set_database_metadata( + "db_2024", + representative_time="2024-01-01", + iam_model="remind", + pathway="SSP2-Base", + premise_version="2.4.9.1", + ) + + def test_two_scenario_sets_without_selection_raises(self): + with pytest.raises(ValueError, match="Several background scenarios"): + resolve_database_dates_from_metadata() + + def test_error_names_the_differing_key_and_values(self): + with pytest.raises(ValueError) as excinfo: + resolve_database_dates_from_metadata() + message = str(excinfo.value) + assert "pathway" in message + assert "SSP2-PkBudg500" in message + assert "SSP2-Base" in message + # iam_model is identical in both sets, so it isn't part of the report + assert "iam_model" not in message + + def test_scenario_selects_one_set(self): + resolved = resolve_database_dates_from_metadata( + scenario={"pathway": "SSP2-Base"} + ) + assert resolved == {"db_2024": datetime(2024, 1, 1)} + + def test_databases_without_scenario_metadata_survive_the_filter(self): + set_database_metadata("foreground", representative_time="dynamic") + resolved = resolve_database_dates_from_metadata( + scenario={"pathway": "SSP2-Base"} + ) + assert resolved == { + "db_2024": datetime(2024, 1, 1), + "foreground": "dynamic", + } + + def test_several_filter_keys_are_combined(self): + resolved = resolve_database_dates_from_metadata( + scenario={"iam_model": "remind", "pathway": "SSP2-Base"} + ) + assert set(resolved) == {"db_2024"} + + def test_filter_matching_nothing_resolves_to_nothing(self): + assert resolve_database_dates_from_metadata( + scenario={"pathway": "SSP2-PkBudg1150"} + ) == {} + + def test_unknown_filter_key_raises_listing_available_keys(self): + with pytest.raises(ValueError) as excinfo: + resolve_database_dates_from_metadata(scenario={"pathwya": "SSP2-Base"}) + message = str(excinfo.value) + assert "pathwya" in message + assert "pathway" in message + + def test_same_scenario_from_two_premise_versions_is_not_ambiguous(self): + set_database_metadata("db_2024", pathway="SSP2-PkBudg500") + set_database_metadata("db_2024", premise_version="2.4.9.2") + assert set(resolve_database_dates_from_metadata()) == {"db_2022", "db_2024"} +``` + +- [ ] **Step 2: Run the tests to verify they fail** + +Run: `.venv/bin/pytest tests/test_database_metadata.py::TestScenarioSelection -v` +Expected: FAIL — no error is raised, `resolve_database_dates_from_metadata` currently ignores `scenario` + +- [ ] **Step 3: Implement filtering and the ambiguity check** + +In `bw_timex/database_metadata.py`, add `from collections import defaultdict` to the imports and insert before `resolve_database_dates_from_metadata`: + +```python +def _as_set(value: Any) -> set: + """Compare list-valued metadata (e.g. `external_scenarios`) order-insensitively.""" + if isinstance(value, (list, tuple, set)): + return {str(item) for item in value} + return {str(value)} + + +def _values_match(declared: Any, wanted: Any) -> bool: + if isinstance(declared, (list, tuple, set)) or isinstance(wanted, (list, tuple, set)): + return _as_set(declared) == _as_set(wanted) + return str(declared) == str(wanted) + + +def _check_filter_keys(scenario: dict, candidates: dict[str, dict]) -> None: + """Reject filter keys no database declares, instead of silently matching nothing.""" + declared = set() + for metadata in candidates.values(): + declared.update(set(metadata) - BRIGHTWAY_METADATA_KEYS) + unknown = sorted(set(scenario) - declared) + if not unknown: + return + available = ", ".join(sorted(declared)) or "none" + raise ValueError( + f"No database in this project declares the metadata key(s) " + f"{unknown}. Keys declared by the databases of this project: {available}. " + f"Add the metadata with `bw_timex.set_database_metadata`, or check the " + f"spelling of your `scenario` filter." + ) + + +def _scenario_signature(metadata: dict) -> tuple: + return tuple( + (key, tuple(sorted(_as_set(metadata[key]))) if key in metadata else None) + for key in SCENARIO_SIGNATURE_KEYS + ) + + +def _format_scenario_sets(groups: dict[tuple, list[str]]) -> str: + """One line per scenario set, naming only the keys that actually differ.""" + differing = [ + key + for index, key in enumerate(SCENARIO_SIGNATURE_KEYS) + if len({signature[index][1] for signature in groups}) > 1 + ] + lines = [] + for signature, names in groups.items(): + values = dict(signature) + description = ", ".join( + f"{key}={', '.join(values[key]) if values[key] else 'not set'}" + for key in differing + ) + lines.append(f" {description}: {', '.join(sorted(names))}") + return "\n".join(lines) + + +def _check_unambiguous(candidates: dict[str, dict]) -> None: + groups = defaultdict(list) + for name, metadata in candidates.items(): + if any(key in metadata for key in SCENARIO_SIGNATURE_KEYS): + groups[_scenario_signature(metadata)].append(name) + if len(groups) <= 1: + return + raise ValueError( + f"Several background scenarios found in this project:\n" + f"{_format_scenario_sets(groups)}\n" + f"Select one, e.g. scenario={{'pathway': '...'}}, or map the databases " + f"explicitly with `database_dates`." + ) +``` + +Then replace the body of `resolve_database_dates_from_metadata` with: + +```python + candidates = _candidate_databases() + if scenario: + _check_filter_keys(scenario, candidates) + candidates = { + name: metadata + for name, metadata in candidates.items() + if all( + key not in metadata or _values_match(metadata[key], wanted) + for key, wanted in scenario.items() + ) + } + _check_unambiguous(candidates) + return { + name: _normalize_representative_time(metadata[REPRESENTATIVE_TIME], name) + for name, metadata in candidates.items() + } +``` + +- [ ] **Step 4: Run the tests to verify they pass** + +Run: `.venv/bin/pytest tests/test_database_metadata.py -v` +Expected: PASS (26 tests) + +- [ ] **Step 5: Commit** + +```bash +git add bw_timex/database_metadata.py tests/test_database_metadata.py +git commit -m "feat: select background scenarios by database metadata" +``` + +--- + +### Task 4: Wire it into `TimexLCA` + +**Files:** +- Modify: `bw_timex/timex_lca.py` (imports, class docstring `Examples` block, `__init__` signature + docstring, the `database_dates` fallback block at `timex_lca.py:146-160`) +- Modify: `bw_timex/validation.py` (`TimexLCAInputs`) +- Modify: `tests/test_database_metadata.py` + +**Interfaces:** +- Consumes: `resolve_database_dates_from_metadata(scenario)` from Task 3. +- Produces: `TimexLCA(demand, method, database_dates=None, scenario=None, use_global_lci_cache=True)`; `TimexLCA.scenario` holds the filter that was used; `TimexLCA.database_dates` is the resolved mapping, exactly as before for callers who pass `database_dates`. + +- [ ] **Step 1: Write the failing tests** + +Append to `tests/test_database_metadata.py`: + +```python +from bw_timex import TimexLCA + +# ─── Tests for TimexLCA using database metadata ─── + + +@pytest.mark.usefixtures("temporal_grouping_db_monthly") +class TestTimexLCAFromMetadata: + + @pytest.fixture + def fu(self): + return bd.get_node(database="foreground", code="A") + + def test_no_arguments_uses_metadata(self, fu): + set_database_metadata("db_2022", representative_time="2022-01-01") + set_database_metadata("db_2024", representative_time="2024-01-01") + tlca = TimexLCA(demand={fu.key: 1}, method=("GWP", "example")) + assert tlca.database_dates == { + "db_2022": datetime(2022, 1, 1), + "db_2024": datetime(2024, 1, 1), + "foreground": "dynamic", + } + + def test_demand_database_metadata_is_respected(self, fu): + set_database_metadata("db_2022", representative_time="2022-01-01") + set_database_metadata("foreground", representative_time="dynamic") + tlca = TimexLCA(demand={fu.key: 1}, method=("GWP", "example")) + assert tlca.database_dates["foreground"] == "dynamic" + + def test_scenario_is_forwarded(self, fu): + set_database_metadata( + "db_2022", representative_time="2022-01-01", pathway="SSP2-Base" + ) + set_database_metadata( + "db_2024", representative_time="2024-01-01", pathway="SSP2-PkBudg500" + ) + tlca = TimexLCA( + demand={fu.key: 1}, + method=("GWP", "example"), + scenario={"pathway": "SSP2-Base"}, + ) + assert tlca.database_dates == { + "db_2022": datetime(2022, 1, 1), + "foreground": "dynamic", + } + + def test_database_dates_is_exclusive(self, fu): + set_database_metadata("db_2022", representative_time="2022-01-01") + set_database_metadata("db_2024", representative_time="2024-01-01") + tlca = TimexLCA( + demand={fu.key: 1}, + method=("GWP", "example"), + database_dates={ + "db_2024": datetime(2024, 1, 1), + "foreground": "dynamic", + }, + ) + assert tlca.database_dates == { + "db_2024": datetime(2024, 1, 1), + "foreground": "dynamic", + } + + def test_database_dates_with_scenario_raises(self, fu): + with pytest.raises(ValueError, match="only applies when"): + TimexLCA( + demand={fu.key: 1}, + method=("GWP", "example"), + database_dates={"foreground": "dynamic"}, + scenario={"pathway": "SSP2-Base"}, + ) + + def test_no_metadata_anywhere_falls_back_to_dynamic_demand(self, fu): + tlca = TimexLCA(demand={fu.key: 1}, method=("GWP", "example")) + assert tlca.database_dates == {"foreground": "dynamic"} + + def test_metadata_and_database_dates_give_the_same_score(self, fu): + explicit = TimexLCA( + demand={fu.key: 1}, + method=("GWP", "example"), + database_dates={ + "db_2022": datetime(2022, 1, 1), + "db_2024": datetime(2024, 1, 1), + "foreground": "dynamic", + }, + ) + explicit.build_timeline(starting_datetime=datetime(2024, 1, 2)) + explicit.lci() + explicit.static_lcia() + + set_database_metadata("db_2022", representative_time="2022-01-01") + set_database_metadata("db_2024", representative_time="2024-01-01") + from_metadata = TimexLCA(demand={fu.key: 1}, method=("GWP", "example")) + from_metadata.build_timeline(starting_datetime=datetime(2024, 1, 2)) + from_metadata.lci() + from_metadata.static_lcia() + + assert from_metadata.static_score == pytest.approx(explicit.static_score) +``` + +- [ ] **Step 2: Run the tests to verify they fail** + +Run: `.venv/bin/pytest tests/test_database_metadata.py::TestTimexLCAFromMetadata -v` +Expected: FAIL — `TypeError: TimexLCA.__init__() got an unexpected keyword argument 'scenario'`, and `test_no_arguments_uses_metadata` fails because only the demand database is mapped + +- [ ] **Step 3: Change the signature and resolution** + +In `bw_timex/timex_lca.py`, add to the imports: + +```python +from .database_metadata import resolve_database_dates_from_metadata +``` + +Change the signature: + +```python + def __init__( + self, + demand: dict, + method: tuple, + database_dates: dict = None, + scenario: dict = None, + use_global_lci_cache: bool = True, + ) -> None: +``` + +Replace the `database_dates` docstring entry and add one for `scenario`: + +``` + database_dates : dict, optional + Dictionary mapping database names to the point in time they + represent, as a `datetime`, or to `"dynamic"` for databases whose + processes are distributed over time (typically the foreground). + Several databases may share the same date, e.g. to keep your own + modified copies of background processes in their own database + instead of writing them into the shared background database for + that vintage. If not given, the mapping is read from the + databases' own `representative_time` metadata (which premise + writes when exporting, and which you can set yourself with + `bw_timex.set_database_metadata`). Passing this argument replaces + the metadata entirely: only the databases listed here are used. + scenario : dict, optional + Metadata a background database must match to be used, e.g. + `{"iam_model": "remind", "pathway": "SSP2-PkBudg500"}`. Only + needed when the project holds several scenarios - `TimexLCA` + raises and lists them otherwise. Databases that don't declare the + filtered key (your foreground, a hand-built vintage) are always + kept. Cannot be combined with `database_dates`. +``` + +Replace the fallback block (`self.database_dates = database_dates` through the `if not self.database_dates:` block) with: + +```python + self.scenario = scenario + self.database_dates = self._resolve_database_dates( + demand=demand, database_dates=database_dates, scenario=scenario + ) +``` + +Add the method right after `__init__`: + +```python + @staticmethod + def _resolve_database_dates( + demand: dict, database_dates: dict | None, scenario: dict | None + ) -> dict: + """Map databases to the points in time they represent. + + Either from the explicit `database_dates` argument, which is then the + whole mapping, or from the databases' own `representative_time` + metadata. Databases holding the demand default to `"dynamic"`. + """ + if database_dates: + if scenario: + raise ValueError( + "`scenario` selects background databases by their metadata and " + "only applies when `database_dates` is not given. Pass one or " + "the other." + ) + return dict(database_dates) + + resolved = resolve_database_dates_from_metadata(scenario) + + if not resolved: + logger.info( + "No database_dates provided, and no database in this project carries " + "`representative_time` metadata. Treating the databases containing the " + "functional unit as dynamic. No remapping of inventories to time " + "explicit databases will be done." + ) + + for key in demand: + database = bd.get_node(id=get_id(key))["database"] + resolved.setdefault(database, "dynamic") + + return resolved +``` + +- [ ] **Step 4: Accept `scenario` in the input validation** + +In `bw_timex/validation.py`, add the field and its validator to `TimexLCAInputs`: + +```python + scenario: Optional[dict] = None +``` + +```python + @field_validator("scenario") + @classmethod + def validate_scenario(cls, v: Optional[dict]) -> Optional[dict]: + if v is None: + return v + if not v: + raise ValueError("scenario must be a non-empty dictionary if provided.") + for key, value in v.items(): + if not isinstance(key, str): + raise ValueError( + f"scenario keys must be strings (database metadata keys), got " + f"{type(key).__name__}." + ) + if not isinstance(value, (str, int, float, bool, list, tuple)): + raise ValueError( + f"scenario values must be scalars or lists of scalars, got " + f"{type(value).__name__} for key '{key}'." + ) + return v +``` + +And pass it in `timex_lca.py`, where `TimexLCAInputs` is instantiated: + +```python + TimexLCAInputs( + demand=self.demand, + method=self.method, + database_dates=self.database_dates, + scenario=self.scenario, + ) +``` + +- [ ] **Step 5: Update the class docstring example** + +In the `Examples` block of the `TimexLCA` class docstring, put the metadata path first and keep the explicit mapping as the alternative: + +```python + from bw_timex import TimexLCA, set_database_metadata + + demand = {("my_foreground_database", "my_process"): 1} + method = ("some_method_family", "some_category", "some_method") + + # Databases exported by premise already know the point in time they + # represent. For your own databases, say so once: + set_database_metadata("my_background_database_one", representative_time=datetime(2020, 1, 1)) + set_database_metadata("my_background_database_two", representative_time=datetime(2030, 1, 1)) + + tlca = TimexLCA(demand, method) + + # ... or map the databases explicitly, which then replaces the metadata: + tlca = TimexLCA( + demand, + method, + database_dates={ + "my_background_database_one": datetime(2020, 1, 1), + "my_background_database_two": datetime(2030, 1, 1), + # Several databases may share the same date, e.g. to keep your own + # modified copies of background processes in their own database: + "my_modified_background_2020": datetime(2020, 1, 1), + "my_foreground_database": "dynamic", + }, + ) + + tlca.build_timeline() # has many optional arguments + tlca.lci() + tlca.static_lcia() + print(tlca.static_score) + # also available: "GWP", "pGWP", "pGTP", "prospective_radiative_forcing" + tlca.dynamic_lcia(metric="radiative_forcing") + print(tlca.dynamic_score) +``` + +- [ ] **Step 6: Run the new tests** + +Run: `.venv/bin/pytest tests/test_database_metadata.py -v` +Expected: PASS (33 tests) + +- [ ] **Step 7: Run the whole suite to prove nothing regressed** + +Run: `.venv/bin/pytest -x -q` +Expected: PASS, same count as on `main` plus the new tests. Every existing test passes `database_dates`, so the exclusive branch must keep them green. + +- [ ] **Step 8: Commit** + +```bash +git add bw_timex/timex_lca.py bw_timex/validation.py tests/test_database_metadata.py +git commit -m "feat: read database timing from metadata by default in TimexLCA" +``` + +--- + +### Task 5: Documentation + +**Files:** +- Create: `docs/content/background_database_metadata.md` +- Create: `docs/api/database_metadata.md` +- Modify: `zensical.toml` (User Guide nav, API nav) +- Modify: `docs/content/getting_started/quickstart.md:58-70`, `:96`, `:128-132` +- Modify: `docs/content/getting_started/adding_temporal_information.md:250-282` +- Modify: `docs/content/getting_started/build_process_timeline.md:11-21` +- Modify: `CHANGES.md` + +**Interfaces:** +- Consumes: `set_database_metadata`, `TimexLCA(scenario=...)` from Tasks 1–4. +- Produces: no code. + +- [ ] **Step 1: Write the new reference page** + +Create `docs/content/background_database_metadata.md`: + +````markdown +--- +icon: lucide/calendar-clock +tags: + - background databases +--- + +# What a database represents + +`bw_timex` needs to know which point in time each background database stands for. +That information lives in the database's own Brightway metadata, so it only has to +be recorded once - not in every script. + +```python +import bw2data as bd + +bd.databases["ei_cutoff_3.10.1_remind_SSP2-PkBudg500_2050"] +``` + +```python +{ + # written by brightway + "format": "Ecoinvent XML", "backend": "sqlite", "number": 43648, ..., + # 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", +} +``` + +Only `representative_time` is required. `TimexLCA` reads it from every database of +your project, so a study on premise databases needs no timing argument at all: + +```python +tlca = TimexLCA(demand={("foreground", "A"): 1}, method=("our", "method")) +``` + +!!! info "premise version" + + premise writes this metadata from the version following 2.4.9.2 onwards. For + databases written by an earlier version, set it yourself as shown below - it is + a one-liner per database. + +## Setting it yourself + +For databases you built yourself, use +[`set_database_metadata`][bw_timex.database_metadata.set_database_metadata]: + +```python +from datetime import datetime +from bw_timex import set_database_metadata + +set_database_metadata("background_2020", representative_time=datetime(2020, 1, 1)) +set_database_metadata("background_2030", representative_time=datetime(2030, 1, 1)) +``` + +The value is stored as an ISO 8601 string, because Brightway keeps database +metadata as JSON. You only do this once per database: it is stored in the project, +not in your script. + +Your foreground doesn't represent a point in time - its processes get distributed +over time. `TimexLCA` treats the databases holding your functional unit as +`"dynamic"` automatically, but you can also say so explicitly: + +```python +set_database_metadata("foreground", representative_time="dynamic") +``` + +## Several databases for the same point in time + +More than one database may carry the same date. This is useful when you modify +background processes: keep the modified copies in your own database per point in +time, instead of writing them into ecoinvent or premise. + +```python +set_database_metadata("my_background_2020", representative_time=datetime(2020, 1, 1)) +set_database_metadata("my_background_2030", representative_time=datetime(2030, 1, 1)) +``` + +For each process, `bw_timex` interpolates only between the databases that actually +contain it, matched on `name`, `reference product` and `location`. + +## Choosing a scenario + +A project often holds more than one IAM scenario. `bw_timex` refuses to guess and +tells you what it found: + +``` +Several background scenarios found in this project: + pathway=SSP2-PkBudg500: ei_..._2030, ei_..._2040, ei_..._2050 + pathway=SSP2-Base: ei_..._2030, ei_..._2040, ei_..._2050 +Select one, e.g. scenario={'pathway': '...'}, or map the databases explicitly with +`database_dates`. +``` + +Pick one with the `scenario` argument, which filters the databases on their +metadata: + +```python +tlca = TimexLCA( + demand={("foreground", "A"): 1}, + method=("our", "method"), + scenario={"pathway": "SSP2-PkBudg500"}, +) +``` + +Any metadata key works - `iam_model`, `pathway`, `system_model`, +`ecoinvent_version`, `premise_version`, or anything you set yourself. Databases +that don't carry the key at all (your foreground, your own vintages) are never +filtered out. + +Comparing scenarios is then a loop over filters: + +```python +scores = {} +for pathway in ("SSP2-Base", "SSP2-PkBudg500"): + tlca = TimexLCA(demand, method, scenario={"pathway": pathway}) + tlca.build_timeline() + tlca.lci() + tlca.static_lcia() + scores[pathway] = tlca.static_score +``` + +!!! warning "Superstructure databases" + + Databases holding several scenarios at once (premise superstructure or + scenario-array exports) are skipped: they have no single technosphere per point + in time. Use one database per scenario and year. + +## Mapping the databases explicitly + +`database_dates` still does what it always did, and takes over completely: when you +pass it, metadata is not read at all and only the databases you list are used. + +```python +tlca = TimexLCA( + demand={("foreground", "A"): 1}, + method=("our", "method"), + database_dates={ + "background": datetime(2020, 1, 1), + "background_2030": datetime(2030, 1, 1), + "foreground": "dynamic", + }, +) +``` + +Use it when you want to restrict a calculation to a subset of the databases in your +project, or when a database's metadata is wrong and you don't want to change it. +```` + +- [ ] **Step 2: Add both pages to the nav** + +In `zensical.toml`, add the User Guide entry after the Walkthrough block (after the line `]},` that closes `Walkthrough`, before `{ "What LCA should I do?" ...`): + +```toml + { "What a database represents" = "content/background_database_metadata.md" }, +``` + +And in the API nav block, next to the other API pages: + +```toml + { "Database metadata" = "api/database_metadata.md" }, +``` + +Create `docs/api/database_metadata.md`, following `docs/api/utils.md`: + +```markdown +--- +icon: lucide/calendar-clock +tags: + - api +--- + +# Database metadata + +Reading and writing what a Brightway database represents: the point in time +(`representative_time`) and, for prospective databases, the scenario it was built +for. + +::: bw_timex.database_metadata +``` + +- [ ] **Step 3: Update the quickstart** + +In `docs/content/getting_started/quickstart.md`, replace step 3 and the `TimexLCA` call: + +```python +# 3. Say what your time-specific background databases represent +# (premise databases already know - skip this for them) +set_database_metadata("background", representative_time=datetime(2020, 1, 1)) +set_database_metadata("background_2030", representative_time=datetime(2030, 1, 1)) + +# 4. Create the TimexLCA object +tlca = TimexLCA( + demand={("foreground", "A"): 1}, + method=("our", "method"), +) +``` + +Add `set_database_metadata` to the `from bw_timex import ...` line at the top of that +code block. In the cheat sheet, change the background row to: + +``` +| *How the background changes* over time | one database per point in time, each with `representative_time` metadata | background databases | +``` + +And replace the trailing paragraph about `database_dates` (lines 128-132) with: + +```markdown +Absolute dates (`dtype="datetime64[s]"`) are also allowed in a `TemporalDistribution`, +e.g. for the timing of the functional unit itself. Relative dates +(`dtype="timedelta64[Y]"`) are relative to the consuming process. Several databases may +represent the same point in time, e.g. if you keep modified copies of background +processes in their own database instead of writing them into the shared vintage. See +[Time-specific background databases](../background_database_metadata.md) for scenario selection +and for mapping databases explicitly with `database_dates`. +``` + +- [ ] **Step 4: Update walkthrough step 1** + +In `docs/content/getting_started/adding_temporal_information.md`, replace the paragraph and code block at lines 250-262 with: + +````markdown +So, as you can see, the processes at specific time steps reside within a separate normal +Brightway database. `bw_timex` picks these up automatically, as long as each database +says which point in time it represents: + +```python +from datetime import datetime +from bw_timex import set_database_metadata + +set_database_metadata("background", representative_time=datetime(2020, 1, 1)) +set_database_metadata("background_2030", representative_time=datetime(2030, 1, 1)) +``` + +You only do this once per database - it is stored in your Brightway project. Databases +exported by [premise](https://premise.readthedocs.io/en/latest/introduction.html) bring +this metadata with them, so there is nothing to do for those. The foreground doesn't +represent a specific point in time and is distributed over time instead; `bw_timex` +treats the databases holding your functional unit that way automatically. +```` + +Replace the code block in the "Several databases for the same point in time" section +(lines 274-282) with: + +```python +set_database_metadata("ecoinvent_2020", representative_time=datetime(2020, 1, 1)) +set_database_metadata("ecoinvent_2030", representative_time=datetime(2030, 1, 1)) +set_database_metadata("my_background_2020", representative_time=datetime(2020, 1, 1)) +set_database_metadata("my_background_2030", representative_time=datetime(2030, 1, 1)) +``` + +- [ ] **Step 5: Update walkthrough step 2** + +In `docs/content/getting_started/build_process_timeline.md`, replace lines 11-21 with: + +````markdown +With all the temporal information prepared, we can now instantiate our TimexLCA object. +This is just like a normal Brightway LCA object - the timing of the background databases +comes from their metadata: + +```python +from bw_timex import TimexLCA + +tlca = TimexLCA( + demand={("foreground", "A"): 1}, + method=("our", "method"), +) +``` + +If your project holds several scenarios, select one with +`scenario={"pathway": "SSP2-PkBudg500"}`; to map the databases by hand instead, pass +`database_dates`. Both are covered in +[Time-specific background databases](../background_database_metadata.md). +```` + +- [ ] **Step 6: Add the changelog entry** + +Under `## [Unreleased]` in `CHANGES.md`: + +```markdown +* Added `representative_time` database metadata as the default timing source: `TimexLCA` now maps background databases to points in time by reading their Brightway metadata (as written by premise), making `database_dates` optional ([#217](https://github.com/brightway-lca/bw_timex/issues/217)) +* Added `set_database_metadata` to record Time-specific background databases (`representative_time`, and scenario fields such as `iam_model` or `pathway`) for databases that don't bring the metadata themselves +* Added `TimexLCA(scenario={...})` to select one background scenario when a project holds several; `TimexLCA` raises and lists the scenarios it found if the choice is ambiguous +``` + +- [ ] **Step 7: Verify the docs build** + +Run: `.venv/bin/python -m zensical build 2>&1 | tail -20` +Expected: build succeeds, no warning about `background_database_metadata.md` or `api/database_metadata.md` being missing from the nav. If `zensical` is not installed in the venv, run `.venv/bin/python -c "import tomllib, pathlib; tomllib.loads(pathlib.Path('zensical.toml').read_text())"` to at least prove the nav edit is valid TOML, and say in the commit that the build was not run. + +- [ ] **Step 8: Commit** + +```bash +git add docs zensical.toml CHANGES.md +git commit -m "docs: document representative_time database metadata" +``` + +--- + +### Task 6: Tutorial notebooks + +**Files:** +- Modify: `notebooks/tutorials/1_getting_started.ipynb` +- Modify: `notebooks/tutorials/2_electric_vehicle_from_scratch.ipynb` +- Modify: `notebooks/tutorials/3_dynamic_characterization.ipynb` +- Modify: `notebooks/tutorials/4_import_model_from_excel.ipynb` + +**Interfaces:** +- Consumes: `set_database_metadata`, `TimexLCA()` without `database_dates` from Tasks 1–4. +- Produces: no code. + +These notebooks build their own small databases, so they can be re-executed. + +- [ ] **Step 1: Find every occurrence** + +Run: `grep -n "database_dates" notebooks/tutorials/*.ipynb` +Note which cells build the mapping and which pass it to `TimexLCA`. + +- [ ] **Step 2: Edit the cells** + +In each notebook, use `NotebookEdit` to: +1. Replace the cell that builds `database_dates` with `set_database_metadata` calls, one per background database, keeping the surrounding markdown explanation in sync (it must no longer say "we define a dictionary that maps databases to dates"). +2. Drop the `database_dates=database_dates` argument from the `TimexLCA(...)` call. +3. Add `set_database_metadata` to the `from bw_timex import ...` cell. + +Pattern: + +```python +# before +database_dates = { + "db_2020": datetime.strptime("2020", "%Y"), + "db_2030": datetime.strptime("2030", "%Y"), + "foreground": "dynamic", +} +tlca = TimexLCA(demand={fu.key: 1}, method=method, database_dates=database_dates) + +# after +set_database_metadata("db_2020", representative_time=datetime(2020, 1, 1)) +set_database_metadata("db_2030", representative_time=datetime(2030, 1, 1)) +tlca = TimexLCA(demand={fu.key: 1}, method=method) +``` + +- [ ] **Step 3: Re-execute each notebook** + +Run, one notebook at a time: + +```bash +.venv/bin/jupyter nbconvert --to notebook --execute --inplace notebooks/tutorials/1_getting_started.ipynb +``` + +Expected: completes without error. If a notebook needs data that isn't in the repo, do +not execute it — leave the stored outputs, and note that in the commit message. + +- [ ] **Step 4: Check the diff for accidental churn** + +Run: `git diff --stat notebooks/tutorials` +Expected: only the edited cells plus their re-executed outputs. If execution rewrote +every cell id or bumped unrelated metadata, restore and re-run with +`--ClearMetadataPreprocessor.enabled=True` off, keeping the diff readable. + +- [ ] **Step 5: Commit** + +```bash +git add notebooks/tutorials +git commit -m "docs: use database metadata instead of database_dates in the tutorials" +``` + +--- + +### Task 7: Remaining notebooks + +**Files:** +- Modify: `notebooks/advanced/background_temporal_distributions.ipynb` +- Modify: `notebooks/advanced/uncertainty_with_datapackages.ipynb` +- Modify: `notebooks/advanced/background_temporal_distributions_premise.ipynb` +- Modify: `notebooks/teaching/ev_walkthrough_premise.ipynb` +- Modify: `notebooks/teaching/exercise_ev_vs_petrol_solutions.ipynb` +- Modify: `notebooks/examples/electric_vehicle_premise.ipynb` +- Modify: `notebooks/examples/electric_vehicle_premise_detailed.ipynb` +- Modify: `notebooks/development/benchmarking.ipynb` +- **Do not touch:** `notebooks/examples/paper_case_study.ipynb` + +**Interfaces:** +- Consumes: `set_database_metadata`, `TimexLCA(scenario=...)` from Tasks 1–4. +- Produces: no code. + +The first two build their own databases and can be re-executed. The rest need premise +or ecoinvent databases that aren't in the repo: edit the cell sources only and leave +the stored outputs alone. + +- [ ] **Step 1: Edit the two self-contained notebooks** + +`background_temporal_distributions.ipynb` and `uncertainty_with_datapackages.ipynb`: +same replacement as Task 6 Step 2, then re-execute: + +```bash +.venv/bin/jupyter nbconvert --to notebook --execute --inplace notebooks/advanced/background_temporal_distributions.ipynb +.venv/bin/jupyter nbconvert --to notebook --execute --inplace notebooks/advanced/uncertainty_with_datapackages.ipynb +``` + +- [ ] **Step 2: Edit the premise/ecoinvent notebooks** + +For each of the six remaining notebooks, replace the `database_dates` cell. These use +premise databases, which carry the metadata already, so the mapping usually +disappears entirely: + +```python +# before +database_dates = { + "ei312_REMIND-EU_SSP2_NDC_2020": datetime.strptime("2020", "%Y"), + "ei312_REMIND-EU_SSP2_NDC_2030": datetime.strptime("2030", "%Y"), + "foreground": "dynamic", +} +tlca = TimexLCA(demand={fu.key: 1}, method=method, database_dates=database_dates) + +# after +# The premise databases carry the point in time they represent in their +# metadata, so bw_timex finds them by itself. +tlca = TimexLCA(demand={fu.key: 1}, method=method) +``` + +Two things to get right per notebook: +- If the notebook creates its own modified copies of background processes in extra + databases (the electric-vehicle notebooks do, e.g. `..., without EOL` copies), those + copies need `set_database_metadata(..., representative_time=...)` with the same date + as the vintage they were copied from, or they drop out of the mapping. +- If the notebook's project could hold more than one pathway, show the `scenario` + argument in the markdown right below, e.g. + `scenario={"pathway": "SSP2-PkBudg500"}`. + +Update the surrounding markdown text wherever it explains `database_dates`. + +- [ ] **Step 3: Verify no notebook lost its outputs** + +Run: `git diff --stat notebooks` +Expected: for the six premise notebooks, only source cells change - no `outputs` churn. + +- [ ] **Step 4: Confirm the paper case study is untouched** + +Run: `git status --porcelain notebooks/examples/paper_case_study.ipynb` +Expected: no output. + +- [ ] **Step 5: Commit** + +```bash +git add notebooks +git commit -m "docs: use database metadata instead of database_dates in the notebooks" +``` + +--- + +### Task 8: Final verification + +**Files:** none + +- [ ] **Step 1: Full test suite** + +Run: `.venv/bin/pytest -q` +Expected: all pass. + +- [ ] **Step 2: Nothing still teaches the old default** + +Run: `grep -rn "database_dates" --include="*.md" --include="*.ipynb" docs notebooks | grep -v paper_case_study | grep -v superpowers` +Expected: only the places that deliberately document `database_dates` as the explicit +override — `background_database_metadata.md`, the quickstart's closing paragraph, and +step 2's pointer. Anything else is a leftover. + +- [ ] **Step 3: Public API test still describes the namespace** + +Run: `.venv/bin/pytest tests/test_public_api.py -v` +Expected: PASS. If it asserts an exact `__all__`, add `set_database_metadata` to it. + +- [ ] **Step 4: Commit any fixes and push the branch** + +```bash +git add -A +git commit -m "fix: address leftovers from the metadata migration" +git push -u origin feat/representative-time-metadata +``` diff --git a/docs/superpowers/specs/2026-08-21-representative-time-metadata-design.md b/docs/superpowers/specs/2026-08-21-representative-time-metadata-design.md new file mode 100644 index 00000000..773202ed --- /dev/null +++ b/docs/superpowers/specs/2026-08-21-representative-time-metadata-design.md @@ -0,0 +1,284 @@ +# Representative time as database metadata + +## Problem + +`TimexLCA` learns what a background database represents in one way only: the +`database_dates` argument the user hand-writes at every call site. + +```python +database_dates = { + "ei310_remind_SSP2-PkBudg500_2030": datetime(2030, 1, 1), + "ei310_remind_SSP2-PkBudg500_2040": datetime(2040, 1, 1), + "ei310_remind_SSP2-PkBudg500_2050": datetime(2050, 1, 1), + "foreground": "dynamic", +} +``` + +The information is already in the database — a premise export knows the year it was +built for — but it lives only in the database *name*, so every study re-types it, and +a typo either raises (`Database 'x' not available`) or, worse, silently maps a vintage +to the wrong year. + +premise [PR #303](https://github.com/polca/premise/pull/303) (merged to `master`, not +in 2.4.9.2) closes the gap on the producing side: exported Brightway databases now +carry what they represent in their `bd.databases[name]` metadata. + +```python +{ + # written by brightway + "format": "Ecoinvent XML", "depends": [...], "backend": "sqlite", + "number": 43648, "modified": "...", "processed": "...", + # 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", +} +``` + +Multi-scenario exports (superstructure, scenario arrays) instead carry a `scenarios` +list of such mappings, and a top-level `representative_time` only when all their +scenarios share a year. User (external) scenarios are listed under +`external_scenarios`. + +## Goal + +`TimexLCA` reads the databases' own metadata by default, so the common case needs no +timing argument at all: + +```python +tlca = TimexLCA(demand={("foreground", "A"): 1}, method=("GWP", "example")) +``` + +A project holding several IAM scenarios stays unambiguous: `TimexLCA` refuses to guess +and tells the user how to pick. + +```python +tlca = TimexLCA( + demand={("foreground", "A"): 1}, + method=("GWP", "example"), + scenario={"pathway": "SSP2-PkBudg500"}, +) +``` + +Databases that carry no metadata (hand-built vintages, the foreground) get it from a +one-line helper instead of a repeated argument. + +## Non-goals + +- Removing or deprecating `database_dates`. It stays, unchanged in meaning, and + scripts that pass it behave exactly as they do today. +- Reading anything from database *names*. No year parsing, no naming convention. +- Making superstructure / scenario-array databases usable in `TimexLCA`. They are + recognised and skipped, not supported. +- Writing metadata on import of ecoinvent or any other database. Only the explicit + helper writes. +- Changing how a resolved `database_dates` mapping is used downstream. Everything + after resolution — timeline, temporal markets, matrix modification — is untouched. + +## Design + +### Public interface + +```python +TimexLCA( + demand: dict, + method: tuple, + database_dates: dict = None, + scenario: dict = None, + use_global_lci_cache: bool = True, +) +``` + +`scenario` is a mapping of database metadata key to required value. Any key that +appears in database metadata is allowed — `iam_model`, `pathway`, `system_model`, +`ecoinvent_version`, `premise_version`, and whatever premise adds later. It is a dict +rather than a set of explicit keywords so that the signature stays closed (a +misspelled `use_global_lci_cache` raises `TypeError` instead of being swallowed as a +filter), the call site reads as background selection, and one dict can be reused +across a comparison loop. + +```python +from bw_timex import set_database_metadata + +set_database_metadata("db_2030", representative_time=datetime(2030, 1, 1)) +set_database_metadata( + "my_2050_variant", + representative_time="2050-01-01", + iam_model="remind", + pathway="SSP2-PkBudg500", +) +``` + +### Resolution + +`TimexLCA.__init__` resolves `self.database_dates` before anything else, in +`_resolve_database_dates`. Two mutually exclusive branches: + +**`database_dates` given.** It is the whole mapping. Metadata is not read, `scenario` +must be `None` (passing both raises `ValueError`), and demand databases missing from +it raise in validation as they do today. This keeps every existing script +bit-for-bit unchanged: a legacy call in a project that also holds ten premise +vintages must not silently pull those ten in. + +**`database_dates` not given.** Resolve from metadata: + +1. **Candidates.** Every database in `bd.databases` whose metadata has a + `representative_time`. +2. **Skip multi-scenario databases.** A candidate that also has a non-empty + `scenarios` list is dropped with a `logger.info` naming it. `bw_timex` needs one + technosphere per point in time and cannot pick a scenario out of a superstructure + database. Such a database can still be used by naming it in `database_dates`. +3. **Filter by `scenario`.** A candidate is dropped only if it *declares* a filtered + key with a different value. A candidate that does not declare the key at all is + kept — a hand-built 2020 database, an untouched ecoinvent, or the foreground has no + `pathway`, and filtering it out would break every mixed setup. + `external_scenarios` (a list) compares order-insensitively as a set; all other + values compare with `==` after `str` coercion of both sides. +4. **Ambiguity check.** Over the surviving candidates that declare at least one + scenario key, build a signature from + `("iam_model", "pathway", "system_model", "ecoinvent_version", "external_scenarios")` + (missing key → `None`). Bookkeeping keys such as `premise_version` are deliberately + not part of the signature: re-running premise must not look like a second scenario. + More than one distinct signature raises `ValueError`, reporting only the keys whose + values actually differ: + + ``` + Several background scenarios found in this project: + pathway='SSP2-PkBudg500': ei310_remind_SSP2-PkBudg500_2030, + ei310_remind_SSP2-PkBudg500_2040, + ei310_remind_SSP2-PkBudg500_2050 + pathway='SSP2-Base': ei310_remind_SSP2-Base_2030, + ei310_remind_SSP2-Base_2040, + ei310_remind_SSP2-Base_2050 + Select one, e.g. scenario={'pathway': 'SSP2-PkBudg500'}, or map the databases + explicitly with database_dates. + ``` + + Databases that declare no scenario key at all never appear in this check and are + always kept. +5. **Normalize values.** `datetime` passes through; a string parses with + `datetime.fromisoformat`; the literal `"dynamic"` passes through. Anything else + raises `ValueError` naming the database, the key and the offending value. +6. **Demand databases.** Every database holding a demand key that is not already + mapped is added as `"dynamic"`. +7. **Nothing found.** If no database carries `representative_time`, log the existing + "no remapping will be done" message and fall back to today's behaviour: demand + databases marked `"dynamic"`. + +An unknown filter key — one that no candidate database declares — raises rather than +filtering everything away, listing the keys and values present in the project. That is +what buys back the autocomplete a dict does not give. + +### Module layout + +Discovery, filtering and the setter live in a new module, +`bw_timex/database_metadata.py`. `timex_lca.py` is already large and this is a +self-contained responsibility with its own tests; `utils.py` holds exchange- and +plot-level helpers. `TimexLCA` imports `resolve_database_dates_from_metadata` from it, +and `set_database_metadata` is re-exported from the `bw_timex` top-level namespace. + +### Setter helper + +`bw_timex.database_metadata.set_database_metadata(database, **metadata)`, re-exported +from `bw_timex`: + +- `database` may be a name or a `bd.Database`; unregistered → `ValueError`. +- `representative_time` accepts a `datetime` (serialized with `.isoformat()`), an ISO + string (validated by round-tripping through `fromisoformat`), or `"dynamic"`. + Brightway metadata is stored as JSON, so a `datetime` object left in it breaks + `bd.databases.flush()`; converting is the point of the helper. +- Any other key is written as given, after a JSON-serializability check. +- Writes into `bd.databases[name]` and calls `bd.databases.flush()`, so the value + survives a project reload. +- Returns the resulting metadata mapping. + +### Validation + +`TimexLCAInputs` gains `scenario: Optional[dict]`, validating that keys are strings +and values are scalars or lists of scalars, and that `scenario` and `database_dates` +are not both given. The metadata-side errors (unparseable value, ambiguity, unknown +filter key) are raised in `_resolve_database_dates`, which owns the metadata, not in +the pydantic model. + +`set_database_metadata` gets its own `DatabaseMetadataInputs` model in +`validation.py`, matching how the other user-facing helpers validate. + +## Interactions and limits + +- **Several databases per date** ([#205](https://github.com/brightway-lca/bw_timex/pull/205)) + still works: metadata discovery can map two databases to the same + `representative_time`, which is exactly the modified-copy case. Two full ecoinvent + copies of the same vintage (e.g. from two premise runs) collide on process identity + and raise there, as designed; the fix is a `scenario` filter on `premise_version` or + an explicit `database_dates`. +- **Setup cost.** `TimexLCA.__init__` loads node metadata for every database in + `database_dates`, so auto-discovery costs one node-metadata load per matching + database. A project holding vintages from an unrelated study pays for them; the + escape hatches are `scenario` or `database_dates`. +- **premise version.** The metadata is written by premise `master` (post-2.4.9.2). + Databases written by older premise carry nothing, and the docs say so; those users + either write metadata with the helper or keep using `database_dates`. + +## Documentation + +- `docs/content/getting_started/quickstart.md`: step 3 becomes "the databases already + know when they are"; `database_dates` shown once as the explicit alternative; the + cheat-sheet row for background timing updated. +- `docs/content/getting_started/adding_temporal_information.md` and + `build_process_timeline.md`: update the passages that name `database_dates`. +- New section in `docs/content/getting_started/` on what a database represents: + the metadata keys, `set_database_metadata`, scenario selection and its error, the + premise-version caveat, and `database_dates` as the explicit override. +- New `docs/api/database_metadata.md` (`::: bw_timex.database_metadata`), added to the + API nav in `zensical.toml`. +- `CHANGES.md`: entry under `[Unreleased]`. + +## Notebooks + +Every notebook that builds its own databases writes metadata with +`set_database_metadata` and drops the `database_dates` argument; the premise notebooks +rely on premise-written metadata and show `scenario` where a project holds more than +one pathway. + +- `notebooks/tutorials/1_getting_started.ipynb` +- `notebooks/tutorials/2_electric_vehicle_from_scratch.ipynb` +- `notebooks/tutorials/3_dynamic_characterization.ipynb` +- `notebooks/tutorials/4_import_model_from_excel.ipynb` +- `notebooks/advanced/background_temporal_distributions.ipynb` +- `notebooks/advanced/background_temporal_distributions_premise.ipynb` +- `notebooks/advanced/uncertainty_with_datapackages.ipynb` +- `notebooks/teaching/ev_walkthrough_premise.ipynb` +- `notebooks/teaching/exercise_ev_vs_petrol_solutions.ipynb` +- `notebooks/examples/electric_vehicle_premise.ipynb` +- `notebooks/examples/electric_vehicle_premise_detailed.ipynb` +- `notebooks/development/benchmarking.ipynb` + +`notebooks/examples/paper_case_study.ipynb` is **not** touched: it reproduces a +published study and must keep its exact code. + +## Testing + +New `tests/test_database_metadata.py`, on the existing small fixtures: + +- Timing resolved from `representative_time` metadata with no `database_dates`. +- ISO string and `datetime` metadata values both resolve; `"dynamic"` in metadata + marks a database dynamic; a garbage value raises naming the database. +- Demand database defaults to `"dynamic"` when its metadata says nothing. +- `database_dates` is exclusive: a project full of metadata-carrying databases plus an + explicit `database_dates` resolves to exactly that mapping. +- `database_dates` together with `scenario` raises. +- `scenario` filter selects one pathway out of two; databases without scenario + metadata survive the filter. +- Two scenario sets and no `scenario` raises, and the message names the differing key + and both values. +- Same scenario written by two premise versions does not raise (bookkeeping keys are + outside the signature). +- A database carrying `scenarios` is skipped, and named in the log. +- An unknown filter key raises listing the available keys. +- `set_database_metadata` round-trips through `bd.databases.flush()` and a re-read; + a `datetime` lands as an ISO string; an unregistered database raises. +- End-to-end: an existing scenario test rewritten to use metadata gives the same + score as the `database_dates` version. diff --git a/notebooks/advanced/background_temporal_distributions.ipynb b/notebooks/advanced/background_temporal_distributions.ipynb index acd8fee4..1614071e 100644 --- a/notebooks/advanced/background_temporal_distributions.ipynb +++ b/notebooks/advanced/background_temporal_distributions.ipynb @@ -57,10 +57,10 @@ "id": "942222ef", "metadata": { "execution": { - "iopub.execute_input": "2026-06-22T08:08:47.650263Z", - "iopub.status.busy": "2026-06-22T08:08:47.650130Z", - "iopub.status.idle": "2026-06-22T08:08:48.990960Z", - "shell.execute_reply": "2026-06-22T08:08:48.990545Z" + "iopub.execute_input": "2026-08-21T10:31:55.868733Z", + "iopub.status.busy": "2026-08-21T10:31:55.868550Z", + "iopub.status.idle": "2026-08-21T10:31:57.383744Z", + "shell.execute_reply": "2026-08-21T10:31:57.383219Z" } }, "outputs": [ @@ -77,21 +77,21 @@ "output_type": "stream", "text": [ "\r", - "100%|██████████| 1/1 [00:00<00:00, 10754.63it/s]" + "100%|██████████| 1/1 [00:00<00:00, 10951.19it/s]" ] }, { "name": "stdout", "output_type": "stream", "text": [ - "\u001b[2m10:08:48+0200\u001b[0m [\u001b[32m\u001b[1minfo \u001b[0m] \u001b[1mVacuuming database \u001b[0m\n" + "\u001b[2m12:31:57+0200\u001b[0m [\u001b[32m\u001b[1minfo \u001b[0m] \u001b[1mVacuuming database \u001b[0m\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ - "\u001b[2m10:08:48+0200\u001b[0m [\u001b[33m\u001b[1mwarning \u001b[0m] \u001b[1mNot able to determine geocollections for all datasets. This database is not ready for regionalization.\u001b[0m\n" + "\u001b[2m12:31:57+0200\u001b[0m [\u001b[33m\u001b[1mwarning \u001b[0m] \u001b[1mNot able to determine geocollections for all datasets. This database is not ready for regionalization.\u001b[0m\n" ] }, { @@ -114,21 +114,21 @@ "output_type": "stream", "text": [ "\r", - "100%|██████████| 2/2 [00:00<00:00, 40920.04it/s]" + "100%|██████████| 2/2 [00:00<00:00, 34521.02it/s]" ] }, { "name": "stdout", "output_type": "stream", "text": [ - "\u001b[2m10:08:48+0200\u001b[0m [\u001b[32m\u001b[1minfo \u001b[0m] \u001b[1mVacuuming database \u001b[0m\n" + "\u001b[2m12:31:57+0200\u001b[0m [\u001b[32m\u001b[1minfo \u001b[0m] \u001b[1mVacuuming database \u001b[0m\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ - "\u001b[2m10:08:48+0200\u001b[0m [\u001b[33m\u001b[1mwarning \u001b[0m] \u001b[1mNot able to determine geocollections for all datasets. This database is not ready for regionalization.\u001b[0m\n" + "\u001b[2m12:31:57+0200\u001b[0m [\u001b[33m\u001b[1mwarning \u001b[0m] \u001b[1mNot able to determine geocollections for all datasets. This database is not ready for regionalization.\u001b[0m\n" ] }, { @@ -151,14 +151,14 @@ "output_type": "stream", "text": [ "\r", - "100%|██████████| 1/1 [00:00<00:00, 10866.07it/s]" + "100%|██████████| 1/1 [00:00<00:00, 30615.36it/s]" ] }, { "name": "stdout", "output_type": "stream", "text": [ - "\u001b[2m10:08:48+0200\u001b[0m [\u001b[32m\u001b[1minfo \u001b[0m] \u001b[1mVacuuming database \u001b[0m\n" + "\u001b[2m12:31:57+0200\u001b[0m [\u001b[32m\u001b[1minfo \u001b[0m] \u001b[1mVacuuming database \u001b[0m\n" ] }, { @@ -256,10 +256,10 @@ "id": "d2660de7", "metadata": { "execution": { - "iopub.execute_input": "2026-06-22T08:08:48.992384Z", - "iopub.status.busy": "2026-06-22T08:08:48.992289Z", - "iopub.status.idle": "2026-06-22T08:08:49.032820Z", - "shell.execute_reply": "2026-06-22T08:08:49.032374Z" + "iopub.execute_input": "2026-08-21T10:31:57.385045Z", + "iopub.status.busy": "2026-08-21T10:31:57.384951Z", + "iopub.status.idle": "2026-08-21T10:31:57.455766Z", + "shell.execute_reply": "2026-08-21T10:31:57.455229Z" } }, "outputs": [ @@ -267,7 +267,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "\u001b[2m10:08:48+0200\u001b[0m [\u001b[33m\u001b[1mwarning \u001b[0m] \u001b[1mNot able to determine geocollections for all datasets. This database is not ready for regionalization.\u001b[0m\n" + "\u001b[2m12:31:57+0200\u001b[0m [\u001b[33m\u001b[1mwarning \u001b[0m] \u001b[1mNot able to determine geocollections for all datasets. This database is not ready for regionalization.\u001b[0m\n" ] }, { @@ -283,14 +283,14 @@ "output_type": "stream", "text": [ "\r", - "100%|██████████| 2/2 [00:00<00:00, 48489.06it/s]" + "100%|██████████| 2/2 [00:00<00:00, 60787.01it/s]" ] }, { "name": "stdout", "output_type": "stream", "text": [ - "\u001b[2m10:08:48+0200\u001b[0m [\u001b[32m\u001b[1minfo \u001b[0m] \u001b[1mVacuuming database \u001b[0m\n" + "\u001b[2m12:31:57+0200\u001b[0m [\u001b[32m\u001b[1minfo \u001b[0m] \u001b[1mVacuuming database \u001b[0m\n" ] }, { @@ -334,7 +334,7 @@ "id": "c4d74bee", "metadata": {}, "source": [ - "As in Getting Started, we record which background database represents which point in time:" + "As in [Getting Started](../tutorials/1_getting_started.ipynb), we record which point in time each background database represents, using `set_database_metadata` — `TimexLCA` will read it automatically:" ] }, { @@ -343,21 +343,39 @@ "id": "e21503fa", "metadata": { "execution": { - "iopub.execute_input": "2026-06-22T08:08:49.034110Z", - "iopub.status.busy": "2026-06-22T08:08:49.034022Z", - "iopub.status.idle": "2026-06-22T08:08:49.036177Z", - "shell.execute_reply": "2026-06-22T08:08:49.035812Z" + "iopub.execute_input": "2026-08-21T10:31:57.456900Z", + "iopub.status.busy": "2026-08-21T10:31:57.456834Z", + "iopub.status.idle": "2026-08-21T10:31:57.727620Z", + "shell.execute_reply": "2026-08-21T10:31:57.727191Z" } }, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "{'depends': ['biosphere'],\n", + " 'backend': 'sqlite',\n", + " 'number': 2,\n", + " 'modified': '2026-08-21T12:31:57.386123',\n", + " 'geocollections': [],\n", + " 'searchable': True,\n", + " 'processed': '2026-08-21T12:31:57.451530',\n", + " 'dirty': False,\n", + " 'representative_time': '2040-01-01T00:00:00'}" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "from datetime import datetime\n", "\n", - "database_dates = {\n", - " \"background_2020\": datetime.strptime(\"2020\", \"%Y\"),\n", - " \"background_2040\": datetime.strptime(\"2040\", \"%Y\"),\n", - " \"foreground\": \"dynamic\",\n", - "}" + "from bw_timex import set_database_metadata\n", + "\n", + "set_database_metadata(\"background_2020\", representative_time=datetime(2020, 1, 1))\n", + "set_database_metadata(\"background_2040\", representative_time=datetime(2040, 1, 1))" ] }, { @@ -398,10 +416,10 @@ "id": "ab591abf", "metadata": { "execution": { - "iopub.execute_input": "2026-06-22T08:08:49.037461Z", - "iopub.status.busy": "2026-06-22T08:08:49.037363Z", - "iopub.status.idle": "2026-06-22T08:08:49.313368Z", - "shell.execute_reply": "2026-06-22T08:08:49.313021Z" + "iopub.execute_input": "2026-08-21T10:31:57.728861Z", + "iopub.status.busy": "2026-08-21T10:31:57.728781Z", + "iopub.status.idle": "2026-08-21T10:31:57.737286Z", + "shell.execute_reply": "2026-08-21T10:31:57.736915Z" } }, "outputs": [ @@ -409,14 +427,14 @@ "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-06-22 10:08:49.308\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.utils\u001b[0m:\u001b[36madd_temporal_distribution_to_exchange\u001b[0m:\u001b[36m604\u001b[0m - \u001b[1mAdded temporal distribution to exchange Exchange: 2 None 'C' (None, somewhere, None) to 'B' (None, somewhere, None).\u001b[0m\n" + "\u001b[32m2026-08-21 12:31:57.732\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.utils\u001b[0m:\u001b[36madd_temporal_distribution_to_exchange\u001b[0m:\u001b[36m670\u001b[0m - \u001b[1mAdded temporal distribution to exchange Exchange: 2 None 'C' (None, somewhere, None) to 'B' (None, somewhere, None).\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-06-22 10:08:49.311\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.utils\u001b[0m:\u001b[36madd_temporal_distribution_to_exchange\u001b[0m:\u001b[36m604\u001b[0m - \u001b[1mAdded temporal distribution to exchange Exchange: 2 None 'C' (None, somewhere, None) to 'B' (None, somewhere, None).\u001b[0m\n" + "\u001b[32m2026-08-21 12:31:57.735\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.utils\u001b[0m:\u001b[36madd_temporal_distribution_to_exchange\u001b[0m:\u001b[36m670\u001b[0m - \u001b[1mAdded temporal distribution to exchange Exchange: 2 None 'C' (None, somewhere, None) to 'B' (None, somewhere, None).\u001b[0m\n" ] } ], @@ -456,10 +474,10 @@ "id": "d44daabf", "metadata": { "execution": { - "iopub.execute_input": "2026-06-22T08:08:49.314829Z", - "iopub.status.busy": "2026-06-22T08:08:49.314709Z", - "iopub.status.idle": "2026-06-22T08:08:49.352568Z", - "shell.execute_reply": "2026-06-22T08:08:49.352155Z" + "iopub.execute_input": "2026-08-21T10:31:57.738406Z", + "iopub.status.busy": "2026-08-21T10:31:57.738335Z", + "iopub.status.idle": "2026-08-21T10:31:57.791869Z", + "shell.execute_reply": "2026-08-21T10:31:57.791482Z" } }, "outputs": [ @@ -467,49 +485,77 @@ "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-06-22 10:08:49.315\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m136\u001b[0m - \u001b[1mInitializing TimexLCA object...\u001b[0m\n" + "\u001b[32m2026-08-21 12:31:57.738\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m174\u001b[0m - \u001b[1mInitializing TimexLCA object...\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[32m2026-08-21 12:31:57.740\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m194\u001b[0m - \u001b[1mCalculating base LCA...\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-06-22 10:08:49.316\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m153\u001b[0m - \u001b[1mCalculating base LCA...\u001b[0m\n" + "\u001b[32m2026-08-21 12:31:57.740\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mclean_databases\u001b[0m:\u001b[36m1355\u001b[0m - \u001b[1mReprocessing 2 modified database(s) before calculating: background_2020, background_2040. This can take a while for large databases.\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-06-22 10:08:49.331\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m170\u001b[0m - \u001b[1mCollecting node infos...\u001b[0m\n" + "\u001b[32m2026-08-21 12:31:57.747\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mclean_databases\u001b[0m:\u001b[36m1361\u001b[0m - \u001b[1mDone reprocessing modified databases.\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-06-22 10:08:49.332\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mbuild_timeline\u001b[0m:\u001b[36m336\u001b[0m - \u001b[1mNo edge filter function provided. Skipping all edges in background databases.\u001b[0m\n" + "\u001b[32m2026-08-21 12:31:57.758\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m211\u001b[0m - \u001b[1mCollecting node infos...\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-06-22 10:08:49.333\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mbuild_timeline\u001b[0m:\u001b[36m357\u001b[0m - \u001b[1mCreating activity time mapping...\u001b[0m\n" + "\u001b[32m2026-08-21 12:31:57.759\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m223\u001b[0m - \u001b[1mLoading node metadata from 3 database(s)...\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-06-22 10:08:49.333\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timeline_builder\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m112\u001b[0m - \u001b[1mTraversing supply chain graph...\u001b[0m\n" + "\u001b[32m2026-08-21 12:31:57.760\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m260\u001b[0m - \u001b[1mTimexLCA initialized.\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-06-22 10:08:49.336\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timeline_builder\u001b[0m:\u001b[36mbuild_timeline\u001b[0m:\u001b[36m186\u001b[0m - \u001b[1mBuilding timeline...\u001b[0m\n" + "\u001b[32m2026-08-21 12:31:57.761\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mbuild_timeline\u001b[0m:\u001b[36m453\u001b[0m - \u001b[1mNo edge filter function provided. Skipping all edges in background databases.\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[32m2026-08-21 12:31:57.761\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mbuild_timeline\u001b[0m:\u001b[36m474\u001b[0m - \u001b[1mCreating activity time mapping...\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[32m2026-08-21 12:31:57.762\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timeline_builder\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m112\u001b[0m - \u001b[1mTraversing supply chain graph...\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[32m2026-08-21 12:31:57.767\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timeline_builder\u001b[0m:\u001b[36mbuild_timeline\u001b[0m:\u001b[36m183\u001b[0m - \u001b[1mBuilding timeline...\u001b[0m\n" ] }, { @@ -517,7 +563,7 @@ "output_type": "stream", "text": [ "Starting graph traversal\n", - "Calculation count: 1\n" + "Calculation count: 2\n" ] }, { @@ -552,6 +598,7 @@ " consumer\n", " consumer_name\n", " amount\n", + " cumulative_amount\n", " temporal_market_shares\n", " temporal_evolution\n", " temporal_evolution_reference\n", @@ -561,16 +608,17 @@ " \n", " 0\n", " 2024\n", - " 327359257374097410\n", + " 349138551637430274\n", " 2024-01-01\n", - " 327359257227296768\n", + " 349138551499018240\n", " B\n", " 2024\n", - " 327359257374097411\n", + " 349138551637430275\n", " 2024-01-01\n", - " 327359257286017024\n", + " 349138551557738496\n", " A\n", " 3.0\n", + " 3.0\n", " {'background_2020': 0.8, 'background_2040': 0.2}\n", " None\n", " producer\n", @@ -578,9 +626,9 @@ " \n", " 1\n", " 2024\n", - " 327359257374097411\n", + " 349138551637430275\n", " 2024-01-01\n", - " 327359257286017024\n", + " 349138551557738496\n", " A\n", " 2024\n", " -1\n", @@ -588,6 +636,7 @@ " -1\n", " -1\n", " 1.0\n", + " 1.0\n", " None\n", " None\n", " producer\n", @@ -598,16 +647,16 @@ ], "text/plain": [ " hash_producer time_mapped_producer date_producer producer \\\n", - "0 2024 327359257374097410 2024-01-01 327359257227296768 \n", - "1 2024 327359257374097411 2024-01-01 327359257286017024 \n", + "0 2024 349138551637430274 2024-01-01 349138551499018240 \n", + "1 2024 349138551637430275 2024-01-01 349138551557738496 \n", "\n", " producer_name hash_consumer time_mapped_consumer date_consumer \\\n", - "0 B 2024 327359257374097411 2024-01-01 \n", + "0 B 2024 349138551637430275 2024-01-01 \n", "1 A 2024 -1 2024-01-01 \n", "\n", - " consumer consumer_name amount \\\n", - "0 327359257286017024 A 3.0 \n", - "1 -1 -1 1.0 \n", + " consumer consumer_name amount cumulative_amount \\\n", + "0 349138551557738496 A 3.0 3.0 \n", + "1 -1 -1 1.0 1.0 \n", "\n", " temporal_market_shares temporal_evolution \\\n", "0 {'background_2020': 0.8, 'background_2040': 0.2} None \n", @@ -629,7 +678,6 @@ "tlca_static_bg = TimexLCA(\n", " demand={(\"foreground\", \"A\"): 1},\n", " method=(\"our\", \"method\"),\n", - " database_dates=database_dates,\n", ")\n", "tlca_static_bg.build_timeline(starting_datetime=\"2024-01-01\")\n", "tlca_static_bg.timeline" @@ -649,10 +697,10 @@ "id": "e4b25e4c", "metadata": { "execution": { - "iopub.execute_input": "2026-06-22T08:08:49.353869Z", - "iopub.status.busy": "2026-06-22T08:08:49.353795Z", - "iopub.status.idle": "2026-06-22T08:08:49.370227Z", - "shell.execute_reply": "2026-06-22T08:08:49.369872Z" + "iopub.execute_input": "2026-08-21T10:31:57.793072Z", + "iopub.status.busy": "2026-08-21T10:31:57.792984Z", + "iopub.status.idle": "2026-08-21T10:31:57.810484Z", + "shell.execute_reply": "2026-08-21T10:31:57.809829Z" } }, "outputs": [ @@ -660,14 +708,14 @@ "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-06-22 10:08:49.359\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mlci\u001b[0m:\u001b[36m507\u001b[0m - \u001b[1mExpanding matrices...\u001b[0m\n" + "\u001b[32m2026-08-21 12:31:57.798\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mlci\u001b[0m:\u001b[36m634\u001b[0m - \u001b[1mExpanding matrices...\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-06-22 10:08:49.361\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mlci\u001b[0m:\u001b[36m526\u001b[0m - \u001b[1mCalculating dynamic inventory...\u001b[0m\n" + "\u001b[32m2026-08-21 12:31:57.800\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mlci\u001b[0m:\u001b[36m653\u001b[0m - \u001b[1mCalculating dynamic inventory...\u001b[0m\n" ] }, { @@ -703,10 +751,10 @@ "id": "936fb39b", "metadata": { "execution": { - "iopub.execute_input": "2026-06-22T08:08:49.371504Z", - "iopub.status.busy": "2026-06-22T08:08:49.371429Z", - "iopub.status.idle": "2026-06-22T08:08:49.395760Z", - "shell.execute_reply": "2026-06-22T08:08:49.395276Z" + "iopub.execute_input": "2026-08-21T10:31:57.811833Z", + "iopub.status.busy": "2026-08-21T10:31:57.811740Z", + "iopub.status.idle": "2026-08-21T10:31:57.838387Z", + "shell.execute_reply": "2026-08-21T10:31:57.838055Z" } }, "outputs": [ @@ -714,49 +762,63 @@ "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-06-22 10:08:49.371\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m136\u001b[0m - \u001b[1mInitializing TimexLCA object...\u001b[0m\n" + "\u001b[32m2026-08-21 12:31:57.812\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m174\u001b[0m - \u001b[1mInitializing TimexLCA object...\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-06-22 10:08:49.372\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m153\u001b[0m - \u001b[1mCalculating base LCA...\u001b[0m\n" + "\u001b[32m2026-08-21 12:31:57.813\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m194\u001b[0m - \u001b[1mCalculating base LCA...\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-06-22 10:08:49.378\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m170\u001b[0m - \u001b[1mCollecting node infos...\u001b[0m\n" + "\u001b[32m2026-08-21 12:31:57.818\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m211\u001b[0m - \u001b[1mCollecting node infos...\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-06-22 10:08:49.379\u001b[0m | \u001b[33m\u001b[1mWARNING \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mbuild_timeline\u001b[0m:\u001b[36m303\u001b[0m - \u001b[33m\u001b[1mtraverse_background=True with graph_traversal='priority': non-referenced background variants are not placed on the priority heap; each variant subtree is walked in full via proxy reads when its parent edge is reached. The referenced-system heap exploration order is unchanged and explored amounts are exact (identical to graph_traversal='bfs' for these subtrees).\u001b[0m\n" + "\u001b[32m2026-08-21 12:31:57.820\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m223\u001b[0m - \u001b[1mLoading node metadata from 3 database(s)...\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-06-22 10:08:49.380\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mbuild_timeline\u001b[0m:\u001b[36m357\u001b[0m - \u001b[1mCreating activity time mapping...\u001b[0m\n" + "\u001b[32m2026-08-21 12:31:57.820\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m260\u001b[0m - \u001b[1mTimexLCA initialized.\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-06-22 10:08:49.380\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timeline_builder\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m112\u001b[0m - \u001b[1mTraversing supply chain graph...\u001b[0m\n" + "\u001b[32m2026-08-21 12:31:57.820\u001b[0m | \u001b[33m\u001b[1mWARNING \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mbuild_timeline\u001b[0m:\u001b[36m420\u001b[0m - \u001b[33m\u001b[1mtraverse_background=True with graph_traversal='priority': non-referenced background variants are not placed on the priority heap; each variant subtree is walked in full via proxy reads when its parent edge is reached. The referenced-system heap exploration order is unchanged and explored amounts are exact (identical to graph_traversal='bfs' for these subtrees).\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-06-22 10:08:49.384\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timeline_builder\u001b[0m:\u001b[36mbuild_timeline\u001b[0m:\u001b[36m186\u001b[0m - \u001b[1mBuilding timeline...\u001b[0m\n" + "\u001b[32m2026-08-21 12:31:57.820\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mbuild_timeline\u001b[0m:\u001b[36m474\u001b[0m - \u001b[1mCreating activity time mapping...\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[32m2026-08-21 12:31:57.821\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timeline_builder\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m112\u001b[0m - \u001b[1mTraversing supply chain graph...\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[32m2026-08-21 12:31:57.825\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timeline_builder\u001b[0m:\u001b[36mbuild_timeline\u001b[0m:\u001b[36m183\u001b[0m - \u001b[1mBuilding timeline...\u001b[0m\n" ] }, { @@ -860,7 +922,6 @@ "tlca_dynamic_bg = TimexLCA(\n", " demand={(\"foreground\", \"A\"): 1},\n", " method=(\"our\", \"method\"),\n", - " database_dates=database_dates,\n", ")\n", "tlca_dynamic_bg.build_timeline(\n", " starting_datetime=\"2024-01-01\",\n", @@ -887,10 +948,10 @@ "id": "bcd18cf7", "metadata": { "execution": { - "iopub.execute_input": "2026-06-22T08:08:49.396974Z", - "iopub.status.busy": "2026-06-22T08:08:49.396899Z", - "iopub.status.idle": "2026-06-22T08:08:49.414524Z", - "shell.execute_reply": "2026-06-22T08:08:49.414221Z" + "iopub.execute_input": "2026-08-21T10:31:57.839590Z", + "iopub.status.busy": "2026-08-21T10:31:57.839522Z", + "iopub.status.idle": "2026-08-21T10:31:57.858164Z", + "shell.execute_reply": "2026-08-21T10:31:57.857638Z" } }, "outputs": [ @@ -898,14 +959,14 @@ "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-06-22 10:08:49.401\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mlci\u001b[0m:\u001b[36m507\u001b[0m - \u001b[1mExpanding matrices...\u001b[0m\n" + "\u001b[32m2026-08-21 12:31:57.844\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mlci\u001b[0m:\u001b[36m634\u001b[0m - \u001b[1mExpanding matrices...\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-06-22 10:08:49.405\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mlci\u001b[0m:\u001b[36m526\u001b[0m - \u001b[1mCalculating dynamic inventory...\u001b[0m\n" + "\u001b[32m2026-08-21 12:31:57.848\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mlci\u001b[0m:\u001b[36m653\u001b[0m - \u001b[1mCalculating dynamic inventory...\u001b[0m\n" ] }, { @@ -984,10 +1045,10 @@ "id": "b2d881bb", "metadata": { "execution": { - "iopub.execute_input": "2026-06-22T08:08:49.415999Z", - "iopub.status.busy": "2026-06-22T08:08:49.415925Z", - "iopub.status.idle": "2026-06-22T08:08:49.445670Z", - "shell.execute_reply": "2026-06-22T08:08:49.445208Z" + "iopub.execute_input": "2026-08-21T10:31:57.859348Z", + "iopub.status.busy": "2026-08-21T10:31:57.859284Z", + "iopub.status.idle": "2026-08-21T10:31:57.936962Z", + "shell.execute_reply": "2026-08-21T10:31:57.936627Z" } }, "outputs": [ @@ -995,56 +1056,84 @@ "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-06-22 10:08:49.420\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.utils\u001b[0m:\u001b[36madd_temporal_distribution_to_exchange\u001b[0m:\u001b[36m604\u001b[0m - \u001b[1mAdded temporal distribution to exchange Exchange: 3 None 'B' (None, somewhere, None) to 'A' (None, somewhere, None).\u001b[0m\n" + "\u001b[32m2026-08-21 12:31:57.905\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.utils\u001b[0m:\u001b[36madd_temporal_distribution_to_exchange\u001b[0m:\u001b[36m670\u001b[0m - \u001b[1mAdded temporal distribution to exchange Exchange: 3 None 'B' (None, somewhere, None) to 'A' (None, somewhere, None).\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[32m2026-08-21 12:31:57.905\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m174\u001b[0m - \u001b[1mInitializing TimexLCA object...\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[32m2026-08-21 12:31:57.906\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m194\u001b[0m - \u001b[1mCalculating base LCA...\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[32m2026-08-21 12:31:57.907\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mclean_databases\u001b[0m:\u001b[36m1355\u001b[0m - \u001b[1mReprocessing 1 modified database(s) before calculating: foreground. This can take a while for large databases.\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-06-22 10:08:49.420\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m136\u001b[0m - \u001b[1mInitializing TimexLCA object...\u001b[0m\n" + "\u001b[32m2026-08-21 12:31:57.911\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mclean_databases\u001b[0m:\u001b[36m1361\u001b[0m - \u001b[1mDone reprocessing modified databases.\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-06-22 10:08:49.420\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m153\u001b[0m - \u001b[1mCalculating base LCA...\u001b[0m\n" + "\u001b[32m2026-08-21 12:31:57.916\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m211\u001b[0m - \u001b[1mCollecting node infos...\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-06-22 10:08:49.429\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m170\u001b[0m - \u001b[1mCollecting node infos...\u001b[0m\n" + "\u001b[32m2026-08-21 12:31:57.918\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m223\u001b[0m - \u001b[1mLoading node metadata from 3 database(s)...\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-06-22 10:08:49.430\u001b[0m | \u001b[33m\u001b[1mWARNING \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mbuild_timeline\u001b[0m:\u001b[36m303\u001b[0m - \u001b[33m\u001b[1mtraverse_background=True with graph_traversal='priority': non-referenced background variants are not placed on the priority heap; each variant subtree is walked in full via proxy reads when its parent edge is reached. The referenced-system heap exploration order is unchanged and explored amounts are exact (identical to graph_traversal='bfs' for these subtrees).\u001b[0m\n" + "\u001b[32m2026-08-21 12:31:57.918\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m260\u001b[0m - \u001b[1mTimexLCA initialized.\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-06-22 10:08:49.431\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mbuild_timeline\u001b[0m:\u001b[36m357\u001b[0m - \u001b[1mCreating activity time mapping...\u001b[0m\n" + "\u001b[32m2026-08-21 12:31:57.918\u001b[0m | \u001b[33m\u001b[1mWARNING \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mbuild_timeline\u001b[0m:\u001b[36m420\u001b[0m - \u001b[33m\u001b[1mtraverse_background=True with graph_traversal='priority': non-referenced background variants are not placed on the priority heap; each variant subtree is walked in full via proxy reads when its parent edge is reached. The referenced-system heap exploration order is unchanged and explored amounts are exact (identical to graph_traversal='bfs' for these subtrees).\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-06-22 10:08:49.431\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timeline_builder\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m112\u001b[0m - \u001b[1mTraversing supply chain graph...\u001b[0m\n" + "\u001b[32m2026-08-21 12:31:57.918\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mbuild_timeline\u001b[0m:\u001b[36m474\u001b[0m - \u001b[1mCreating activity time mapping...\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-06-22 10:08:49.435\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timeline_builder\u001b[0m:\u001b[36mbuild_timeline\u001b[0m:\u001b[36m186\u001b[0m - \u001b[1mBuilding timeline...\u001b[0m\n" + "\u001b[32m2026-08-21 12:31:57.919\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timeline_builder\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m112\u001b[0m - \u001b[1mTraversing supply chain graph...\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[32m2026-08-21 12:31:57.923\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timeline_builder\u001b[0m:\u001b[36mbuild_timeline\u001b[0m:\u001b[36m183\u001b[0m - \u001b[1mBuilding timeline...\u001b[0m\n" ] }, { @@ -1087,6 +1176,7 @@ " consumer\n", " consumer_name\n", " amount\n", + " cumulative_amount\n", " temporal_market_shares\n", " temporal_evolution\n", " temporal_evolution_reference\n", @@ -1096,16 +1186,17 @@ " \n", " 0\n", " 2024\n", - " 327359257374097410\n", + " 349138551637430274\n", " 2024-01-01\n", - " 327359257227296768\n", + " 349138551499018240\n", " B\n", " 2024\n", - " 327359257374097412\n", + " 349138551637430276\n", " 2024-01-01\n", - " 327359257286017024\n", + " 349138551557738496\n", " A\n", " 2.1\n", + " 2.1\n", " None\n", " None\n", " producer\n", @@ -1113,16 +1204,17 @@ " \n", " 1\n", " 2024\n", - " 327359257374097411\n", + " 349138551637430275\n", " 2024-01-01\n", - " 327359257227296769\n", + " 349138551499018241\n", " C\n", " 2024\n", - " 327359257374097410\n", + " 349138551637430274\n", " 2024-01-01\n", - " 327359257227296768\n", + " 349138551499018240\n", " B\n", " 1.2\n", + " 2.52\n", " {'background_2020': 0.8, 'background_2040': 0.2}\n", " None\n", " producer\n", @@ -1130,9 +1222,9 @@ " \n", " 2\n", " 2024\n", - " 327359257374097412\n", + " 349138551637430276\n", " 2024-01-01\n", - " 327359257286017024\n", + " 349138551557738496\n", " A\n", " 2024\n", " -1\n", @@ -1140,6 +1232,7 @@ " -1\n", " -1\n", " 1.0\n", + " 1.0\n", " None\n", " None\n", " producer\n", @@ -1147,16 +1240,17 @@ " \n", " 3\n", " 2030\n", - " 327359257374097413\n", + " 349138551637430277\n", " 2030-01-01\n", - " 327359257227296768\n", + " 349138551499018240\n", " B\n", " 2024\n", - " 327359257374097412\n", + " 349138551637430276\n", " 2024-01-01\n", - " 327359257286017024\n", + " 349138551557738496\n", " A\n", " 0.9\n", + " 0.9\n", " None\n", " None\n", " producer\n", @@ -1164,16 +1258,17 @@ " \n", " 4\n", " 2030\n", - " 327359257374097414\n", + " 349138551637430278\n", " 2030-01-01\n", - " 327359257227296769\n", + " 349138551499018241\n", " C\n", " 2030\n", - " 327359257374097413\n", + " 349138551637430277\n", " 2030-01-01\n", - " 327359257227296768\n", + " 349138551499018240\n", " B\n", " 1.2\n", + " 1.08\n", " {'background_2020': 0.5, 'background_2040': 0.5}\n", " None\n", " producer\n", @@ -1181,16 +1276,17 @@ " \n", " 5\n", " 2034\n", - " 327359257374097415\n", + " 349138551637430279\n", " 2034-01-01\n", - " 327359257227296769\n", + " 349138551499018241\n", " C\n", " 2024\n", - " 327359257374097410\n", + " 349138551637430274\n", " 2024-01-01\n", - " 327359257227296768\n", + " 349138551499018240\n", " B\n", " 0.8\n", + " 1.68\n", " {'background_2020': 0.3, 'background_2040': 0.7}\n", " None\n", " producer\n", @@ -1198,16 +1294,17 @@ " \n", " 6\n", " 2040\n", - " 327359257374097416\n", + " 349138551637430280\n", " 2040-01-01\n", - " 327359257227296769\n", + " 349138551499018241\n", " C\n", " 2030\n", - " 327359257374097413\n", + " 349138551637430277\n", " 2030-01-01\n", - " 327359257227296768\n", + " 349138551499018240\n", " B\n", " 0.8\n", + " 0.72\n", " {'background_2040': 1}\n", " None\n", " producer\n", @@ -1218,31 +1315,31 @@ ], "text/plain": [ " hash_producer time_mapped_producer date_producer producer \\\n", - "0 2024 327359257374097410 2024-01-01 327359257227296768 \n", - "1 2024 327359257374097411 2024-01-01 327359257227296769 \n", - "2 2024 327359257374097412 2024-01-01 327359257286017024 \n", - "3 2030 327359257374097413 2030-01-01 327359257227296768 \n", - "4 2030 327359257374097414 2030-01-01 327359257227296769 \n", - "5 2034 327359257374097415 2034-01-01 327359257227296769 \n", - "6 2040 327359257374097416 2040-01-01 327359257227296769 \n", + "0 2024 349138551637430274 2024-01-01 349138551499018240 \n", + "1 2024 349138551637430275 2024-01-01 349138551499018241 \n", + "2 2024 349138551637430276 2024-01-01 349138551557738496 \n", + "3 2030 349138551637430277 2030-01-01 349138551499018240 \n", + "4 2030 349138551637430278 2030-01-01 349138551499018241 \n", + "5 2034 349138551637430279 2034-01-01 349138551499018241 \n", + "6 2040 349138551637430280 2040-01-01 349138551499018241 \n", "\n", " producer_name hash_consumer time_mapped_consumer date_consumer \\\n", - "0 B 2024 327359257374097412 2024-01-01 \n", - "1 C 2024 327359257374097410 2024-01-01 \n", + "0 B 2024 349138551637430276 2024-01-01 \n", + "1 C 2024 349138551637430274 2024-01-01 \n", "2 A 2024 -1 2024-01-01 \n", - "3 B 2024 327359257374097412 2024-01-01 \n", - "4 C 2030 327359257374097413 2030-01-01 \n", - "5 C 2024 327359257374097410 2024-01-01 \n", - "6 C 2030 327359257374097413 2030-01-01 \n", + "3 B 2024 349138551637430276 2024-01-01 \n", + "4 C 2030 349138551637430277 2030-01-01 \n", + "5 C 2024 349138551637430274 2024-01-01 \n", + "6 C 2030 349138551637430277 2030-01-01 \n", "\n", - " consumer consumer_name amount \\\n", - "0 327359257286017024 A 2.1 \n", - "1 327359257227296768 B 1.2 \n", - "2 -1 -1 1.0 \n", - "3 327359257286017024 A 0.9 \n", - "4 327359257227296768 B 1.2 \n", - "5 327359257227296768 B 0.8 \n", - "6 327359257227296768 B 0.8 \n", + " consumer consumer_name amount cumulative_amount \\\n", + "0 349138551557738496 A 2.1 2.1 \n", + "1 349138551499018240 B 1.2 2.52 \n", + "2 -1 -1 1.0 1.0 \n", + "3 349138551557738496 A 0.9 0.9 \n", + "4 349138551499018240 B 1.2 1.08 \n", + "5 349138551499018240 B 0.8 1.68 \n", + "6 349138551499018240 B 0.8 0.72 \n", "\n", " temporal_market_shares temporal_evolution \\\n", "0 None None \n", @@ -1283,7 +1380,6 @@ "tlca_both = TimexLCA(\n", " demand={(\"foreground\", \"A\"): 1},\n", " method=(\"our\", \"method\"),\n", - " database_dates=database_dates,\n", ")\n", "tlca_both.build_timeline(starting_datetime=\"2024-01-01\", traverse_background=True)\n", "tlca_both.timeline" @@ -1303,10 +1399,10 @@ "id": "f62d8e8f", "metadata": { "execution": { - "iopub.execute_input": "2026-06-22T08:08:49.446963Z", - "iopub.status.busy": "2026-06-22T08:08:49.446878Z", - "iopub.status.idle": "2026-06-22T08:08:49.466326Z", - "shell.execute_reply": "2026-06-22T08:08:49.465882Z" + "iopub.execute_input": "2026-08-21T10:31:57.938366Z", + "iopub.status.busy": "2026-08-21T10:31:57.938273Z", + "iopub.status.idle": "2026-08-21T10:31:57.959100Z", + "shell.execute_reply": "2026-08-21T10:31:57.958588Z" } }, "outputs": [ @@ -1314,14 +1410,14 @@ "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-06-22 10:08:49.452\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mlci\u001b[0m:\u001b[36m507\u001b[0m - \u001b[1mExpanding matrices...\u001b[0m\n" + "\u001b[32m2026-08-21 12:31:57.943\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mlci\u001b[0m:\u001b[36m634\u001b[0m - \u001b[1mExpanding matrices...\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-06-22 10:08:49.456\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mlci\u001b[0m:\u001b[36m526\u001b[0m - \u001b[1mCalculating dynamic inventory...\u001b[0m\n" + "\u001b[32m2026-08-21 12:31:57.948\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mlci\u001b[0m:\u001b[36m653\u001b[0m - \u001b[1mCalculating dynamic inventory...\u001b[0m\n" ] }, { @@ -1373,7 +1469,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.13.9" + "version": "3.12.12" } }, "nbformat": 4, diff --git a/notebooks/advanced/background_temporal_distributions_premise.ipynb b/notebooks/advanced/background_temporal_distributions_premise.ipynb index 1d81b93b..838e4b94 100644 --- a/notebooks/advanced/background_temporal_distributions_premise.ipynb +++ b/notebooks/advanced/background_temporal_distributions_premise.ipynb @@ -344,21 +344,12 @@ }, "outputs": [], "source": [ - "from datetime import datetime\n", - "\n", "method = (\n", " \"ecoinvent-3.12\",\n", " \"EF v3.1\",\n", " \"climate change\",\n", " \"global warming potential (GWP100)\",\n", - ")\n", - "\n", - "database_dates = {\n", - " \"ei312_REMIND-EU_SSP2_NDC_2020\": datetime.strptime(\"2020\", \"%Y\"),\n", - " \"ei312_REMIND-EU_SSP2_NDC_2030\": datetime.strptime(\"2030\", \"%Y\"),\n", - " \"ei312_REMIND-EU_SSP2_NDC_2040\": datetime.strptime(\"2040\", \"%Y\"),\n", - " \"foreground\": \"dynamic\",\n", - "}" + ")" ] }, { @@ -403,8 +394,10 @@ "\n", "# This single line runs a full static ecoinvent LCA of the demand under the hood\n", "# (the \"base LCA\"), which is the slowest step of the whole notebook (~30 s on a\n", - "# cold cache). We build exactly ONE TimexLCA and reuse it below.\n", - "tlca = TimexLCA({A: 1}, method, database_dates)" + "# cold cache). We build exactly ONE TimexLCA and reuse it below. premise >= 2.4.9.2\n", + "# writes each background database's representative_time as its own metadata, so no\n", + "# database_dates mapping is needed here.\n", + "tlca = TimexLCA({A: 1}, method)" ] }, { diff --git a/notebooks/advanced/uncertainty_with_datapackages.ipynb b/notebooks/advanced/uncertainty_with_datapackages.ipynb index 18c9f60e..d0495e6d 100644 --- a/notebooks/advanced/uncertainty_with_datapackages.ipynb +++ b/notebooks/advanced/uncertainty_with_datapackages.ipynb @@ -28,10 +28,10 @@ "id": "4a51bde3", "metadata": { "execution": { - "iopub.execute_input": "2026-06-22T09:58:47.666306Z", - "iopub.status.busy": "2026-06-22T09:58:47.666230Z", - "iopub.status.idle": "2026-06-22T09:58:49.178820Z", - "shell.execute_reply": "2026-06-22T09:58:49.178284Z" + "iopub.execute_input": "2026-08-21T10:33:10.087083Z", + "iopub.status.busy": "2026-08-21T10:33:10.086889Z", + "iopub.status.idle": "2026-08-21T10:33:11.519297Z", + "shell.execute_reply": "2026-08-21T10:33:11.518861Z" } }, "outputs": [], @@ -47,10 +47,10 @@ "id": "2c185b3e", "metadata": { "execution": { - "iopub.execute_input": "2026-06-22T09:58:49.180409Z", - "iopub.status.busy": "2026-06-22T09:58:49.180313Z", - "iopub.status.idle": "2026-06-22T09:58:49.202938Z", - "shell.execute_reply": "2026-06-22T09:58:49.202471Z" + "iopub.execute_input": "2026-08-21T10:33:11.521035Z", + "iopub.status.busy": "2026-08-21T10:33:11.520936Z", + "iopub.status.idle": "2026-08-21T10:33:11.538879Z", + "shell.execute_reply": "2026-08-21T10:33:11.538520Z" } }, "outputs": [], @@ -65,10 +65,10 @@ "id": "9ad772c8", "metadata": { "execution": { - "iopub.execute_input": "2026-06-22T09:58:49.204427Z", - "iopub.status.busy": "2026-06-22T09:58:49.204347Z", - "iopub.status.idle": "2026-06-22T09:58:49.270249Z", - "shell.execute_reply": "2026-06-22T09:58:49.269606Z" + "iopub.execute_input": "2026-08-21T10:33:11.540024Z", + "iopub.status.busy": "2026-08-21T10:33:11.539954Z", + "iopub.status.idle": "2026-08-21T10:33:11.630086Z", + "shell.execute_reply": "2026-08-21T10:33:11.629704Z" } }, "outputs": [ @@ -76,14 +76,23 @@ "name": "stderr", "output_type": "stream", "text": [ - "100%|██████████| 1/1 [00:00<00:00, 5370.43it/s]" + "\r", + " 0%| | 0/1 [00:00,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ]" + "[,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ]" ] }, "execution_count": 24, @@ -773,73 +857,73 @@ "id": "1757427b", "metadata": { "execution": { - "iopub.execute_input": "2026-06-22T09:58:50.086145Z", - "iopub.status.busy": "2026-06-22T09:58:50.086040Z", - "iopub.status.idle": "2026-06-22T09:58:50.089261Z", - "shell.execute_reply": "2026-06-22T09:58:50.088659Z" + "iopub.execute_input": "2026-08-21T10:33:12.280693Z", + "iopub.status.busy": "2026-08-21T10:33:12.280626Z", + "iopub.status.idle": "2026-08-21T10:33:12.283286Z", + "shell.execute_reply": "2026-08-21T10:33:12.282900Z" } }, "outputs": [ { "data": { "text/plain": [ - "{(('background_2020', 'glider'), 202001): 327395989872181248,\n", - " (('background_2030', 'glider'), 203001): 327395989914124288,\n", - " (('background_2040', 'glider'), 204001): 327395989947678720,\n", - " (('background_2020', 'powertrain'), 202001): 327395989981233152,\n", - " (('background_2030', 'powertrain'), 203001): 327395990014787584,\n", - " (('background_2040', 'powertrain'), 204001): 327395990044147712,\n", - " (('background_2020', 'battery'), 202001): 327395990077702144,\n", - " (('background_2030', 'battery'), 203001): 327395990111256576,\n", - " (('background_2040', 'battery'), 204001): 327395990144811008,\n", - " (('background_2020', 'electricity'), 202001): 327395990178365440,\n", - " (('background_2030', 'electricity'), 203001): 327395990207725568,\n", - " (('background_2040', 'electricity'), 204001): 327395990241280000,\n", - " (('background_2020', 'glider_eol'), 202001): 327395990270640128,\n", - " (('background_2030', 'glider_eol'), 203001): 327395990304194560,\n", - " (('background_2040', 'glider_eol'), 204001): 327395990337748992,\n", - " (('background_2020', 'powertrain_eol'), 202001): 327395990367109120,\n", - " (('background_2030', 'powertrain_eol'), 203001): 327395990404857856,\n", - " (('background_2040', 'powertrain_eol'), 204001): 327395990438412288,\n", - " (('background_2020', 'battery_eol'), 202001): 327395990467772416,\n", - " (('background_2030', 'battery_eol'), 203001): 327395990501326848,\n", - " (('background_2040', 'battery_eol'), 204001): 327395990534881280,\n", - " (('foreground', 'ev_production'), 'dynamic'): 327395990769762304,\n", - " (('foreground', 'driving'), 'dynamic'): 327395990794928128,\n", - " (('foreground', 'used_ev'), 'dynamic'): 327395990836871168,\n", - " (('temporalized', 'glider'), 202404): 327395990836871169,\n", - " (('temporalized', 'glider'), 202405): 327395990836871170,\n", - " (('temporalized', 'glider'), 202504): 327395990836871171,\n", - " (('temporalized', 'powertrain'), 202504): 327395990836871172,\n", - " (('temporalized', 'battery'), 202504): 327395990836871173,\n", - " (('temporalized', 'glider'), 202505): 327395990836871174,\n", - " (('temporalized', 'powertrain'), 202505): 327395990836871175,\n", - " (('temporalized', 'battery'), 202505): 327395990836871176,\n", - " (('temporalized', 'glider'), 202604): 327395990836871177,\n", - " (('temporalized', 'ev_production'), 202604): 327395990836871178,\n", - " (('temporalized', 'glider'), 202605): 327395990836871179,\n", - " (('temporalized', 'ev_production'), 202605): 327395990836871180,\n", - " (('temporalized', 'electricity'), 202607): 327395990836871181,\n", - " (('temporalized', 'driving'), 202607): 327395990836871182,\n", - " (('temporalized', 'electricity'), 202707): 327395990836871183,\n", - " (('temporalized', 'electricity'), 202807): 327395990836871184,\n", - " (('temporalized', 'electricity'), 202907): 327395990836871185,\n", - " (('temporalized', 'electricity'), 203007): 327395990836871186,\n", - " (('temporalized', 'electricity'), 203107): 327395990836871187,\n", - " (('temporalized', 'electricity'), 203207): 327395990836871188,\n", - " (('temporalized', 'electricity'), 203307): 327395990836871189,\n", - " (('temporalized', 'electricity'), 203407): 327395990836871190,\n", - " (('temporalized', 'electricity'), 203507): 327395990836871191,\n", - " (('temporalized', 'electricity'), 203607): 327395990836871192,\n", - " (('temporalized', 'electricity'), 203707): 327395990836871193,\n", - " (('temporalized', 'electricity'), 203807): 327395990836871194,\n", - " (('temporalized', 'electricity'), 203907): 327395990836871195,\n", - " (('temporalized', 'electricity'), 204007): 327395990836871196,\n", - " (('temporalized', 'electricity'), 204107): 327395990836871197,\n", - " (('temporalized', 'used_ev'), 204207): 327395990836871198,\n", - " (('temporalized', 'glider_eol'), 204210): 327395990836871199,\n", - " (('temporalized', 'powertrain_eol'), 204210): 327395990836871200,\n", - " (('temporalized', 'battery_eol'), 204210): 327395990836871201}" + "{(('background_2020', 'glider'), 202001): 349138863036841984,\n", + " (('background_2020', 'powertrain'), 202001): 349138863129116672,\n", + " (('background_2020', 'battery'), 202001): 349138863213002752,\n", + " (('background_2020', 'electricity'), 202001): 349138863296888832,\n", + " (('background_2020', 'glider_eol'), 202001): 349138863380774912,\n", + " (('background_2020', 'powertrain_eol'), 202001): 349138863468855296,\n", + " (('background_2020', 'battery_eol'), 202001): 349138863561129984,\n", + " (('background_2030', 'glider'), 203001): 349138863070396416,\n", + " (('background_2030', 'powertrain'), 203001): 349138863158476800,\n", + " (('background_2030', 'battery'), 203001): 349138863242362880,\n", + " (('background_2030', 'electricity'), 203001): 349138863326248960,\n", + " (('background_2030', 'glider_eol'), 203001): 349138863405940736,\n", + " (('background_2030', 'powertrain_eol'), 203001): 349138863498215424,\n", + " (('background_2030', 'battery_eol'), 203001): 349138863594684416,\n", + " (('background_2040', 'glider'), 204001): 349138863099756544,\n", + " (('background_2040', 'powertrain'), 204001): 349138863183642624,\n", + " (('background_2040', 'battery'), 204001): 349138863267528704,\n", + " (('background_2040', 'electricity'), 204001): 349138863351414784,\n", + " (('background_2040', 'glider_eol'), 204001): 349138863439495168,\n", + " (('background_2040', 'powertrain_eol'), 204001): 349138863527575552,\n", + " (('background_2040', 'battery_eol'), 204001): 349138863624044544,\n", + " (('foreground', 'ev_production'), 'dynamic'): 349138863716319232,\n", + " (('foreground', 'driving'), 'dynamic'): 349138863728902144,\n", + " (('foreground', 'used_ev'), 'dynamic'): 349138863741485056,\n", + " (('temporalized', 'glider'), 202406): 349138863741485057,\n", + " (('temporalized', 'glider'), 202407): 349138863741485058,\n", + " (('temporalized', 'glider'), 202506): 349138863741485059,\n", + " (('temporalized', 'powertrain'), 202506): 349138863741485060,\n", + " (('temporalized', 'battery'), 202506): 349138863741485061,\n", + " (('temporalized', 'glider'), 202507): 349138863741485062,\n", + " (('temporalized', 'powertrain'), 202507): 349138863741485063,\n", + " (('temporalized', 'battery'), 202507): 349138863741485064,\n", + " (('temporalized', 'glider'), 202606): 349138863741485065,\n", + " (('temporalized', 'ev_production'), 202606): 349138863741485066,\n", + " (('temporalized', 'glider'), 202607): 349138863741485067,\n", + " (('temporalized', 'ev_production'), 202607): 349138863741485068,\n", + " (('temporalized', 'electricity'), 202609): 349138863741485069,\n", + " (('temporalized', 'driving'), 202609): 349138863741485070,\n", + " (('temporalized', 'electricity'), 202709): 349138863741485071,\n", + " (('temporalized', 'electricity'), 202809): 349138863741485072,\n", + " (('temporalized', 'electricity'), 202909): 349138863741485073,\n", + " (('temporalized', 'electricity'), 203009): 349138863741485074,\n", + " (('temporalized', 'electricity'), 203109): 349138863741485075,\n", + " (('temporalized', 'electricity'), 203209): 349138863741485076,\n", + " (('temporalized', 'electricity'), 203309): 349138863741485077,\n", + " (('temporalized', 'electricity'), 203409): 349138863741485078,\n", + " (('temporalized', 'electricity'), 203509): 349138863741485079,\n", + " (('temporalized', 'electricity'), 203609): 349138863741485080,\n", + " (('temporalized', 'electricity'), 203709): 349138863741485081,\n", + " (('temporalized', 'electricity'), 203809): 349138863741485082,\n", + " (('temporalized', 'electricity'), 203909): 349138863741485083,\n", + " (('temporalized', 'electricity'), 204009): 349138863741485084,\n", + " (('temporalized', 'electricity'), 204109): 349138863741485085,\n", + " (('temporalized', 'used_ev'), 204209): 349138863741485086,\n", + " (('temporalized', 'glider_eol'), 204212): 349138863741485087,\n", + " (('temporalized', 'powertrain_eol'), 204212): 349138863741485088,\n", + " (('temporalized', 'battery_eol'), 204212): 349138863741485089}" ] }, "execution_count": 25, @@ -865,17 +949,17 @@ "id": "334e70ad", "metadata": { "execution": { - "iopub.execute_input": "2026-06-22T09:58:50.090600Z", - "iopub.status.busy": "2026-06-22T09:58:50.090508Z", - "iopub.status.idle": "2026-06-22T09:58:50.092733Z", - "shell.execute_reply": "2026-06-22T09:58:50.092275Z" + "iopub.execute_input": "2026-08-21T10:33:12.284258Z", + "iopub.status.busy": "2026-08-21T10:33:12.284196Z", + "iopub.status.idle": "2026-08-21T10:33:12.286016Z", + "shell.execute_reply": "2026-08-21T10:33:12.285706Z" } }, "outputs": [ { "data": { "text/plain": [ - "327395990836871182" + "349138863741485070" ] }, "execution_count": 26, @@ -884,7 +968,11 @@ } ], "source": [ - "id_driving = tlca.activity_time_mapping[(('temporalized', 'driving'), 202607)]\n", + "id_driving = next(\n", + " matrix_id\n", + " for ((database, name), time), matrix_id in tlca.activity_time_mapping.items()\n", + " if database == \"temporalized\" and name == \"driving\"\n", + ")\n", "id_driving" ] }, @@ -894,17 +982,17 @@ "id": "65284e77", "metadata": { "execution": { - "iopub.execute_input": "2026-06-22T09:58:50.094081Z", - "iopub.status.busy": "2026-06-22T09:58:50.093998Z", - "iopub.status.idle": "2026-06-22T09:58:50.096271Z", - "shell.execute_reply": "2026-06-22T09:58:50.095818Z" + "iopub.execute_input": "2026-08-21T10:33:12.287070Z", + "iopub.status.busy": "2026-08-21T10:33:12.287003Z", + "iopub.status.idle": "2026-08-21T10:33:12.289051Z", + "shell.execute_reply": "2026-08-21T10:33:12.288726Z" } }, "outputs": [ { "data": { "text/plain": [ - "327395990836871186" + "349138863741485074" ] }, "execution_count": 27, @@ -913,7 +1001,11 @@ } ], "source": [ - "id_electricity = tlca.activity_time_mapping[(('temporalized', 'electricity'), 203007)] # the electricity \"temporal market\" seen by driving in 2030\n", + "id_electricity = next(\n", + " matrix_id\n", + " for ((database, name), time), matrix_id in tlca.activity_time_mapping.items()\n", + " if database == \"temporalized\" and name == \"electricity\" and time // 100 == 2030\n", + ") # the electricity \"temporal market\" seen by driving in 2030\n", "id_electricity" ] }, @@ -936,10 +1028,10 @@ "id": "4410b8c8", "metadata": { "execution": { - "iopub.execute_input": "2026-06-22T09:58:50.098074Z", - "iopub.status.busy": "2026-06-22T09:58:50.097995Z", - "iopub.status.idle": "2026-06-22T09:58:50.099800Z", - "shell.execute_reply": "2026-06-22T09:58:50.099286Z" + "iopub.execute_input": "2026-08-21T10:33:12.290144Z", + "iopub.status.busy": "2026-08-21T10:33:12.290078Z", + "iopub.status.idle": "2026-08-21T10:33:12.291792Z", + "shell.execute_reply": "2026-08-21T10:33:12.291457Z" } }, "outputs": [], @@ -953,10 +1045,10 @@ "id": "0c0ad4ea", "metadata": { "execution": { - "iopub.execute_input": "2026-06-22T09:58:50.101050Z", - "iopub.status.busy": "2026-06-22T09:58:50.100967Z", - "iopub.status.idle": "2026-06-22T09:58:50.103058Z", - "shell.execute_reply": "2026-06-22T09:58:50.102624Z" + "iopub.execute_input": "2026-08-21T10:33:12.292676Z", + "iopub.status.busy": "2026-08-21T10:33:12.292615Z", + "iopub.status.idle": "2026-08-21T10:33:12.294117Z", + "shell.execute_reply": "2026-08-21T10:33:12.293806Z" } }, "outputs": [], @@ -970,10 +1062,10 @@ "id": "afcdeb2e", "metadata": { "execution": { - "iopub.execute_input": "2026-06-22T09:58:50.104683Z", - "iopub.status.busy": "2026-06-22T09:58:50.104602Z", - "iopub.status.idle": "2026-06-22T09:58:50.106663Z", - "shell.execute_reply": "2026-06-22T09:58:50.106008Z" + "iopub.execute_input": "2026-08-21T10:33:12.295240Z", + "iopub.status.busy": "2026-08-21T10:33:12.295162Z", + "iopub.status.idle": "2026-08-21T10:33:12.296645Z", + "shell.execute_reply": "2026-08-21T10:33:12.296368Z" } }, "outputs": [], @@ -995,10 +1087,10 @@ "id": "d686892a", "metadata": { "execution": { - "iopub.execute_input": "2026-06-22T09:58:50.107861Z", - "iopub.status.busy": "2026-06-22T09:58:50.107773Z", - "iopub.status.idle": "2026-06-22T09:58:50.109584Z", - "shell.execute_reply": "2026-06-22T09:58:50.109100Z" + "iopub.execute_input": "2026-08-21T10:33:12.297543Z", + "iopub.status.busy": "2026-08-21T10:33:12.297483Z", + "iopub.status.idle": "2026-08-21T10:33:12.298957Z", + "shell.execute_reply": "2026-08-21T10:33:12.298621Z" } }, "outputs": [], @@ -1020,10 +1112,10 @@ "id": "d7a6820d", "metadata": { "execution": { - "iopub.execute_input": "2026-06-22T09:58:50.110912Z", - "iopub.status.busy": "2026-06-22T09:58:50.110842Z", - "iopub.status.idle": "2026-06-22T09:58:50.112459Z", - "shell.execute_reply": "2026-06-22T09:58:50.112125Z" + "iopub.execute_input": "2026-08-21T10:33:12.299920Z", + "iopub.status.busy": "2026-08-21T10:33:12.299858Z", + "iopub.status.idle": "2026-08-21T10:33:12.301259Z", + "shell.execute_reply": "2026-08-21T10:33:12.301001Z" } }, "outputs": [], @@ -1042,10 +1134,10 @@ "id": "a4b37318", "metadata": { "execution": { - "iopub.execute_input": "2026-06-22T09:58:50.113611Z", - "iopub.status.busy": "2026-06-22T09:58:50.113542Z", - "iopub.status.idle": "2026-06-22T09:58:50.115030Z", - "shell.execute_reply": "2026-06-22T09:58:50.114605Z" + "iopub.execute_input": "2026-08-21T10:33:12.302155Z", + "iopub.status.busy": "2026-08-21T10:33:12.302091Z", + "iopub.status.idle": "2026-08-21T10:33:12.303488Z", + "shell.execute_reply": "2026-08-21T10:33:12.303189Z" } }, "outputs": [], @@ -1067,10 +1159,10 @@ "id": "5bcf08f3", "metadata": { "execution": { - "iopub.execute_input": "2026-06-22T09:58:50.116390Z", - "iopub.status.busy": "2026-06-22T09:58:50.116278Z", - "iopub.status.idle": "2026-06-22T09:58:50.118346Z", - "shell.execute_reply": "2026-06-22T09:58:50.117908Z" + "iopub.execute_input": "2026-08-21T10:33:12.304545Z", + "iopub.status.busy": "2026-08-21T10:33:12.304488Z", + "iopub.status.idle": "2026-08-21T10:33:12.306002Z", + "shell.execute_reply": "2026-08-21T10:33:12.305660Z" } }, "outputs": [], @@ -1099,10 +1191,10 @@ "id": "3b427d9a", "metadata": { "execution": { - "iopub.execute_input": "2026-06-22T09:58:50.119761Z", - "iopub.status.busy": "2026-06-22T09:58:50.119643Z", - "iopub.status.idle": "2026-06-22T09:58:50.121422Z", - "shell.execute_reply": "2026-06-22T09:58:50.121042Z" + "iopub.execute_input": "2026-08-21T10:33:12.306865Z", + "iopub.status.busy": "2026-08-21T10:33:12.306797Z", + "iopub.status.idle": "2026-08-21T10:33:12.308301Z", + "shell.execute_reply": "2026-08-21T10:33:12.307994Z" } }, "outputs": [], @@ -1116,10 +1208,10 @@ "id": "309d820a", "metadata": { "execution": { - "iopub.execute_input": "2026-06-22T09:58:50.123000Z", - "iopub.status.busy": "2026-06-22T09:58:50.122869Z", - "iopub.status.idle": "2026-06-22T09:58:50.154074Z", - "shell.execute_reply": "2026-06-22T09:58:50.153468Z" + "iopub.execute_input": "2026-08-21T10:33:12.309269Z", + "iopub.status.busy": "2026-08-21T10:33:12.309204Z", + "iopub.status.idle": "2026-08-21T10:33:12.337079Z", + "shell.execute_reply": "2026-08-21T10:33:12.336691Z" } }, "outputs": [], @@ -1134,10 +1226,10 @@ "id": "da2ed768", "metadata": { "execution": { - "iopub.execute_input": "2026-06-22T09:58:50.156072Z", - "iopub.status.busy": "2026-06-22T09:58:50.155925Z", - "iopub.status.idle": "2026-06-22T09:58:50.196688Z", - "shell.execute_reply": "2026-06-22T09:58:50.196121Z" + "iopub.execute_input": "2026-08-21T10:33:12.338028Z", + "iopub.status.busy": "2026-08-21T10:33:12.337960Z", + "iopub.status.idle": "2026-08-21T10:33:12.377360Z", + "shell.execute_reply": "2026-08-21T10:33:12.377075Z" } }, "outputs": [ @@ -1145,10 +1237,10 @@ "name": "stdout", "output_type": "stream", "text": [ - "baseline 15261.765039012731\n", - "reduction 15050.671288882346\n", - "heavy reduction 14930.046288807838\n", - "zero 14809.421288733332\n" + "baseline 15066.797664957096\n", + "reduction 14858.307039782376\n", + "heavy reduction 14739.169539682538\n", + "zero 14620.0320395827\n" ] } ], @@ -1175,7 +1267,14 @@ "cell_type": "code", "execution_count": 38, "id": "97ce71ed", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-21T10:33:12.378504Z", + "iopub.status.busy": "2026-08-21T10:33:12.378444Z", + "iopub.status.idle": "2026-08-21T10:33:12.380384Z", + "shell.execute_reply": "2026-08-21T10:33:12.380101Z" + } + }, "outputs": [], "source": [ "import stats_arrays as sa\n", @@ -1211,15 +1310,22 @@ "cell_type": "code", "execution_count": 39, "id": "d47e6edb", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-21T10:33:12.381581Z", + "iopub.status.busy": "2026-08-21T10:33:12.381507Z", + "iopub.status.idle": "2026-08-21T10:33:14.713573Z", + "shell.execute_reply": "2026-08-21T10:33:14.713135Z" + } + }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "mean 15260.4\n", - "std 58.8\n", - "5-95% 15169.4 - 15359.2\n" + "mean 15073.7\n", + "std 57.9\n", + "5-95% 14973.3 - 15162.1\n" ] } ], @@ -1267,7 +1373,14 @@ "cell_type": "code", "execution_count": 40, "id": "8c04ae6b", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-21T10:33:14.715048Z", + "iopub.status.busy": "2026-08-21T10:33:14.714953Z", + "iopub.status.idle": "2026-08-21T10:33:14.718604Z", + "shell.execute_reply": "2026-08-21T10:33:14.718196Z" + } + }, "outputs": [ { "name": "stdout", @@ -1352,15 +1465,22 @@ "cell_type": "code", "execution_count": 41, "id": "faa49834", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-21T10:33:14.719792Z", + "iopub.status.busy": "2026-08-21T10:33:14.719723Z", + "iopub.status.idle": "2026-08-21T10:33:17.137677Z", + "shell.execute_reply": "2026-08-21T10:33:17.137237Z" + } + }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "mean 15260.4\n", - "std 324.8\n", - "5-95% 14718.3 - 15737.5\n" + "mean 15085.6\n", + "std 305.1\n", + "5-95% 14578.6 - 15574.3\n" ] } ], @@ -1406,7 +1526,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.13.9" + "version": "3.12.12" } }, "nbformat": 4, diff --git a/notebooks/development/benchmarking.ipynb b/notebooks/development/benchmarking.ipynb index 56750df9..4c82aab6 100644 --- a/notebooks/development/benchmarking.ipynb +++ b/notebooks/development/benchmarking.ipynb @@ -337,15 +337,23 @@ "metadata": {}, "outputs": [], "source": [ - "def _database_dates(db_name):\n", - " database_dates = {\n", - " \"ei_cutoff_3.11_image_SSP2-M_2020 2025-05-30\": datetime.strptime(\"2020\", \"%Y\"),\n", - " \"ei_cutoff_3.11_image_SSP2-M_2030 2025-05-30\": datetime.strptime(\"2030\", \"%Y\"),\n", - " \"ei_cutoff_3.11_image_SSP2-M_2040 2025-05-30\": datetime.strptime(\"2040\", \"%Y\"),\n", - " \"ei_cutoff_3.11_image_SSP2-M_2050 2025-05-30\": datetime.strptime(\"2050\", \"%Y\"),\n", - " db_name: \"dynamic\",\n", - " }\n", - " return database_dates" + "from bw_timex import set_database_metadata\n", + "\n", + "# The background databases represent fixed points in time, so we only need to record\n", + "# this once. Each test database created below holds the functional unit, so bw_timex\n", + "# treats it as \"dynamic\" automatically - no per-database mapping needed in the loop.\n", + "set_database_metadata(\n", + " \"ei_cutoff_3.11_image_SSP2-M_2020 2025-05-30\", representative_time=datetime.strptime(\"2020\", \"%Y\")\n", + ")\n", + "set_database_metadata(\n", + " \"ei_cutoff_3.11_image_SSP2-M_2030 2025-05-30\", representative_time=datetime.strptime(\"2030\", \"%Y\")\n", + ")\n", + "set_database_metadata(\n", + " \"ei_cutoff_3.11_image_SSP2-M_2040 2025-05-30\", representative_time=datetime.strptime(\"2040\", \"%Y\")\n", + ")\n", + "set_database_metadata(\n", + " \"ei_cutoff_3.11_image_SSP2-M_2050 2025-05-30\", representative_time=datetime.strptime(\"2050\", \"%Y\")\n", + ")" ] }, { @@ -1088,7 +1096,6 @@ " nr_tiers.append(n_tier)\n", " # Create a unique database name based on the number of tiers and processes\n", " db_name = f\"test_{n_tier}tiers_{n_process}processes\"\n", - " database_dates = _database_dates(db_name)\n", " test_db_names.append(db_name)\n", " # print(f\"Processing database {db_name} with {n_tier} tiers and {n_process} processes per tier...\")\n", " # Check if the database already exists\n", @@ -1117,7 +1124,6 @@ " t0 = time.time()\n", " tlca = TimexLCA(demand={FU_node: 1},\n", " method=method,\n", - " database_dates=database_dates,\n", " )\n", " t1 = time.time()\n", " time_initialize.append(t1 - t0)\n", diff --git a/notebooks/examples/electric_vehicle_premise.ipynb b/notebooks/examples/electric_vehicle_premise.ipynb index 81b6d6c0..65ea0a4d 100644 --- a/notebooks/examples/electric_vehicle_premise.ipynb +++ b/notebooks/examples/electric_vehicle_premise.ipynb @@ -67,7 +67,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "cell-4", "metadata": { "docs_summary": "Show the code that builds the ev system", @@ -88,6 +88,10 @@ "source": [ "# Standard brightway modelling of the ev system - nothing time-explicit yet.\n", "\n", + "from datetime import datetime\n", + "\n", + "from bw_timex import set_database_metadata\n", + "\n", "ELECTRICITY_CONSUMPTION = 0.2 # kWh/km\n", "MILEAGE = 150_000 # km\n", "LIFETIME = 15 # years\n", @@ -159,6 +163,11 @@ "\n", " modified_db.process()\n", "\n", + " # These copies represent the same point in time as the premise database they were\n", + " # copied from, so we record that here too - otherwise bw_timex would not know when\n", + " # they occur and drop them from the temporal mapping.\n", + " set_database_metadata(modified_name, representative_time=datetime.strptime(year, \"%Y\"))\n", + "\n", "# Background processes our foreground links to\n", "ev_background_2020 = modified_dbs[db_2020.name]\n", "glider_production = ev_background_2020.get(code=\"glider_production_without_eol\")\n", @@ -362,15 +371,15 @@ "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-08-04 07:10:04.975\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.utils\u001b[0m:\u001b[36madd_temporal_distribution_to_exchange\u001b[0m:\u001b[36m670\u001b[0m - \u001b[1mAdded temporal distribution to exchange Exchange: 840 kilogram 'glider production, passenger car, without EOL' (kilogram, GLO, None) to 'production of an electric vehicle' (unit, GLO, None).\u001b[0m\n", - "\u001b[32m2026-08-04 07:10:04.980\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.utils\u001b[0m:\u001b[36madd_temporal_distribution_to_exchange\u001b[0m:\u001b[36m670\u001b[0m - \u001b[1mAdded temporal distribution to exchange Exchange: 80 kilogram 'powertrain production, for electric passenger car, without EOL' (kilogram, GLO, None) to 'production of an electric vehicle' (unit, GLO, None).\u001b[0m\n", - "\u001b[32m2026-08-04 07:10:04.985\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.utils\u001b[0m:\u001b[36madd_temporal_distribution_to_exchange\u001b[0m:\u001b[36m670\u001b[0m - \u001b[1mAdded temporal distribution to exchange Exchange: 280 kilogram 'battery production, Li-ion, LiMn2O4, rechargeable, without EOL' (kilogram, GLO, None) to 'production of an electric vehicle' (unit, GLO, None).\u001b[0m\n", - "\u001b[32m2026-08-04 07:10:04.989\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.utils\u001b[0m:\u001b[36madd_temporal_distribution_to_exchange\u001b[0m:\u001b[36m670\u001b[0m - \u001b[1mAdded temporal distribution to exchange Exchange: 1 unit 'production of an electric vehicle' (unit, GLO, None) to 'driving an electric vehicle' (transport over an ev lifetime, GLO, None).\u001b[0m\n", - "\u001b[32m2026-08-04 07:10:05.120\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.utils\u001b[0m:\u001b[36madd_temporal_distribution_to_exchange\u001b[0m:\u001b[36m670\u001b[0m - \u001b[1mAdded temporal distribution to exchange Exchange: 30000.0 kilowatt hour 'market group for electricity, low voltage' (kilowatt hour, DEU, None) to 'driving an electric vehicle' (transport over an ev lifetime, GLO, None).\u001b[0m\n", - "\u001b[32m2026-08-04 07:10:05.125\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.utils\u001b[0m:\u001b[36madd_temporal_distribution_to_exchange\u001b[0m:\u001b[36m670\u001b[0m - \u001b[1mAdded temporal distribution to exchange Exchange: -1 unit 'used electric vehicle' (unit, GLO, None) to 'driving an electric vehicle' (transport over an ev lifetime, GLO, None).\u001b[0m\n", - "\u001b[32m2026-08-04 07:10:05.252\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.utils\u001b[0m:\u001b[36madd_temporal_distribution_to_exchange\u001b[0m:\u001b[36m670\u001b[0m - \u001b[1mAdded temporal distribution to exchange Exchange: -840 kilogram 'treatment of used glider, passenger car, shredding' (kilogram, GLO, None) to 'used electric vehicle' (unit, GLO, None).\u001b[0m\n", - "\u001b[32m2026-08-04 07:10:05.378\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.utils\u001b[0m:\u001b[36madd_temporal_distribution_to_exchange\u001b[0m:\u001b[36m670\u001b[0m - \u001b[1mAdded temporal distribution to exchange Exchange: -80 kilogram 'treatment of used powertrain for electric passenger car, manual dismantling' (kilogram, GLO, None) to 'used electric vehicle' (unit, GLO, None).\u001b[0m\n", - "\u001b[32m2026-08-04 07:10:05.541\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.utils\u001b[0m:\u001b[36madd_temporal_distribution_to_exchange\u001b[0m:\u001b[36m670\u001b[0m - \u001b[1mAdded temporal distribution to exchange Exchange: -280 kilogram 'market for used Li-ion battery' (kilogram, GLO, None) to 'used electric vehicle' (unit, GLO, None).\u001b[0m\n" + "\u001b[32m2026-08-14 14:45:12.822\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.utils\u001b[0m:\u001b[36madd_temporal_distribution_to_exchange\u001b[0m:\u001b[36m670\u001b[0m - \u001b[1mAdded temporal distribution to exchange Exchange: 840 kilogram 'glider production, passenger car, without EOL' (kilogram, GLO, None) to 'production of an electric vehicle' (unit, GLO, None).\u001b[0m\n", + "\u001b[32m2026-08-14 14:45:12.826\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.utils\u001b[0m:\u001b[36madd_temporal_distribution_to_exchange\u001b[0m:\u001b[36m670\u001b[0m - \u001b[1mAdded temporal distribution to exchange Exchange: 80 kilogram 'powertrain production, for electric passenger car, without EOL' (kilogram, GLO, None) to 'production of an electric vehicle' (unit, GLO, None).\u001b[0m\n", + "\u001b[32m2026-08-14 14:45:12.831\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.utils\u001b[0m:\u001b[36madd_temporal_distribution_to_exchange\u001b[0m:\u001b[36m670\u001b[0m - \u001b[1mAdded temporal distribution to exchange Exchange: 280 kilogram 'battery production, Li-ion, LiMn2O4, rechargeable, without EOL' (kilogram, GLO, None) to 'production of an electric vehicle' (unit, GLO, None).\u001b[0m\n", + "\u001b[32m2026-08-14 14:45:12.836\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.utils\u001b[0m:\u001b[36madd_temporal_distribution_to_exchange\u001b[0m:\u001b[36m670\u001b[0m - \u001b[1mAdded temporal distribution to exchange Exchange: 1 unit 'production of an electric vehicle' (unit, GLO, None) to 'driving an electric vehicle' (transport over an ev lifetime, GLO, None).\u001b[0m\n", + "\u001b[32m2026-08-14 14:45:12.962\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.utils\u001b[0m:\u001b[36madd_temporal_distribution_to_exchange\u001b[0m:\u001b[36m670\u001b[0m - \u001b[1mAdded temporal distribution to exchange Exchange: 30000.0 kilowatt hour 'market group for electricity, low voltage' (kilowatt hour, DEU, None) to 'driving an electric vehicle' (transport over an ev lifetime, GLO, None).\u001b[0m\n", + "\u001b[32m2026-08-14 14:45:12.967\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.utils\u001b[0m:\u001b[36madd_temporal_distribution_to_exchange\u001b[0m:\u001b[36m670\u001b[0m - \u001b[1mAdded temporal distribution to exchange Exchange: -1 unit 'used electric vehicle' (unit, GLO, None) to 'driving an electric vehicle' (transport over an ev lifetime, GLO, None).\u001b[0m\n", + "\u001b[32m2026-08-14 14:45:13.093\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.utils\u001b[0m:\u001b[36madd_temporal_distribution_to_exchange\u001b[0m:\u001b[36m670\u001b[0m - \u001b[1mAdded temporal distribution to exchange Exchange: -840 kilogram 'treatment of used glider, passenger car, shredding' (kilogram, GLO, None) to 'used electric vehicle' (unit, GLO, None).\u001b[0m\n", + "\u001b[32m2026-08-14 14:45:13.221\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.utils\u001b[0m:\u001b[36madd_temporal_distribution_to_exchange\u001b[0m:\u001b[36m670\u001b[0m - \u001b[1mAdded temporal distribution to exchange Exchange: -80 kilogram 'treatment of used powertrain for electric passenger car, manual dismantling' (kilogram, GLO, None) to 'used electric vehicle' (unit, GLO, None).\u001b[0m\n", + "\u001b[32m2026-08-14 14:45:13.344\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.utils\u001b[0m:\u001b[36madd_temporal_distribution_to_exchange\u001b[0m:\u001b[36m670\u001b[0m - \u001b[1mAdded temporal distribution to exchange Exchange: -280 kilogram 'market for used Li-ion battery' (kilogram, GLO, None) to 'used electric vehicle' (unit, GLO, None).\u001b[0m\n" ] } ], @@ -450,12 +459,12 @@ "source": [ "## Time-explicit LCA\n", "\n", - "Besides the functional unit and the impact assessment method, `bw_timex` needs to know which point in time each background database represents. Databases that carry temporal distributions - here our foreground - are flagged as `\"dynamic\"`." + "Besides the functional unit and the impact assessment method, `bw_timex` needs to know which point in time each background database represents. premise >= 2.4.9.2 writes this as `representative_time` metadata on the databases it creates, so `bw_timex` finds it automatically; for an earlier premise, set it yourself with `set_database_metadata` - which is what we did for our own `ev_background_` copies above, since they represent the same points in time. You can also skip metadata entirely and pass `database_dates` to `TimexLCA` yourself, which then takes over completely. The database holding the functional unit - here our foreground - is flagged as `\"dynamic\"` automatically. Any other foreground database needs `set_database_metadata(db, representative_time=\"dynamic\")`." ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "cell-12", "metadata": { "execution": { @@ -467,21 +476,9 @@ }, "outputs": [], "source": [ - "from datetime import datetime\n", - "\n", "functional_unit = {driving: 1} # transport over 1 ev lifetime\n", "\n", - "method = (\"ecoinvent-3.12\", \"EF v3.1\", \"climate change\", \"global warming potential (GWP100)\")\n", - "\n", - "database_dates = {\n", - " db_2020.name: datetime.strptime(\"2020\", \"%Y\"),\n", - " db_2030.name: datetime.strptime(\"2030\", \"%Y\"),\n", - " db_2040.name: datetime.strptime(\"2040\", \"%Y\"),\n", - " \"ev_background_2020\": datetime.strptime(\"2020\", \"%Y\"),\n", - " \"ev_background_2030\": datetime.strptime(\"2030\", \"%Y\"),\n", - " \"ev_background_2040\": datetime.strptime(\"2040\", \"%Y\"),\n", - " \"foreground\": \"dynamic\",\n", - "}" + "method = (\"ecoinvent-3.12\", \"EF v3.1\", \"climate change\", \"global warming potential (GWP100)\")" ] }, { @@ -489,12 +486,12 @@ "id": "cell-13", "metadata": {}, "source": [ - "With that, we can set up a `TimexLCA`. It works like a normal `bw2calc.LCA`, with `database_dates` as the extra argument:" + "With that, we can set up a `TimexLCA`. It works like a normal `bw2calc.LCA`:" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "id": "cell-14", "metadata": { "execution": { @@ -504,27 +501,11 @@ "shell.execute_reply": "2026-08-03T19:56:48.510756Z" } }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "\u001b[32m2026-08-04 07:10:08.209\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m142\u001b[0m - \u001b[1mInitializing TimexLCA object...\u001b[0m\n", - "\u001b[32m2026-08-04 07:10:08.210\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m163\u001b[0m - \u001b[1mCalculating base LCA...\u001b[0m\n", - "\u001b[32m2026-08-04 07:10:08.210\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mclean_databases\u001b[0m:\u001b[36m1214\u001b[0m - \u001b[1mReprocessing 1 modified database(s) before calculating: foreground. This can take a while for large databases.\u001b[0m\n", - "\u001b[32m2026-08-04 07:10:08.219\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mclean_databases\u001b[0m:\u001b[36m1220\u001b[0m - \u001b[1mDone reprocessing modified databases.\u001b[0m\n", - "/Users/timodiepers/Documents/Coding/bw_timex/.venv/lib/python3.12/site-packages/scikits/umfpack/umfpack.py:737: UmfpackWarning: (almost) singular matrix! (estimated cond. number: 3.90e+13)\n", - " warnings.warn(msg, UmfpackWarning)\n", - "\u001b[32m2026-08-04 07:10:08.947\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m180\u001b[0m - \u001b[1mCollecting node infos...\u001b[0m\n", - "\u001b[32m2026-08-04 07:10:08.985\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m192\u001b[0m - \u001b[1mLoading node metadata from 7 database(s)...\u001b[0m\n", - "\u001b[32m2026-08-04 07:10:09.793\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m229\u001b[0m - \u001b[1mTimexLCA initialized.\u001b[0m\n" - ] - } - ], + "outputs": [], "source": [ "from bw_timex import TimexLCA\n", "\n", - "tlca = TimexLCA(functional_unit, method, database_dates)" + "tlca = TimexLCA(functional_unit, method)" ] }, { @@ -552,11 +533,11 @@ "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-08-04 07:10:23.270\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mbuild_timeline\u001b[0m:\u001b[36m358\u001b[0m - \u001b[1mNo edge filter function provided. Skipping all edges in background databases.\u001b[0m\n", - "\u001b[32m2026-08-04 07:10:29.312\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mbuild_timeline\u001b[0m:\u001b[36m379\u001b[0m - \u001b[1mCreating activity time mapping...\u001b[0m\n", - "\u001b[32m2026-08-04 07:10:29.413\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timeline_builder\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m112\u001b[0m - \u001b[1mTraversing supply chain graph...\u001b[0m\n", - "\u001b[32m2026-08-04 07:10:29.437\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timeline_builder\u001b[0m:\u001b[36mbuild_timeline\u001b[0m:\u001b[36m186\u001b[0m - \u001b[1mBuilding timeline...\u001b[0m\n", - "\u001b[32m2026-08-04 07:10:29.536\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timeline_builder\u001b[0m:\u001b[36mget_weights_for_interpolation_between_nearest_years\u001b[0m:\u001b[36m705\u001b[0m - \u001b[1mReference date 2040-08-01 00:00:00 is higher than all provided dates. Data will be taken from the closest lower year.\u001b[0m\n" + "\u001b[32m2026-08-14 14:45:15.012\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mbuild_timeline\u001b[0m:\u001b[36m362\u001b[0m - \u001b[1mNo edge filter function provided. Skipping all edges in background databases.\u001b[0m\n", + "\u001b[32m2026-08-14 14:45:18.897\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mbuild_timeline\u001b[0m:\u001b[36m383\u001b[0m - \u001b[1mCreating activity time mapping...\u001b[0m\n", + "\u001b[32m2026-08-14 14:45:18.994\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timeline_builder\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m112\u001b[0m - \u001b[1mTraversing supply chain graph...\u001b[0m\n", + "\u001b[32m2026-08-14 14:45:19.016\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timeline_builder\u001b[0m:\u001b[36mbuild_timeline\u001b[0m:\u001b[36m183\u001b[0m - \u001b[1mBuilding timeline...\u001b[0m\n", + "\u001b[32m2026-08-14 14:45:19.087\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timeline_builder\u001b[0m:\u001b[36mget_weights_for_interpolation_between_nearest_years\u001b[0m:\u001b[36m754\u001b[0m - \u001b[1mReference date 2040-08-01 00:00:00 is higher than all provided dates. Data will be taken from the closest lower year.\u001b[0m\n" ] }, { @@ -1034,8 +1015,8 @@ "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-08-04 07:10:32.439\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mlci\u001b[0m:\u001b[36m529\u001b[0m - \u001b[1mExpanding matrices...\u001b[0m\n", - "\u001b[32m2026-08-04 07:10:32.457\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mlci\u001b[0m:\u001b[36m548\u001b[0m - \u001b[1mCalculating dynamic inventory...\u001b[0m\n", + "\u001b[32m2026-08-14 14:45:19.548\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mlci\u001b[0m:\u001b[36m543\u001b[0m - \u001b[1mExpanding matrices...\u001b[0m\n", + "\u001b[32m2026-08-14 14:45:19.565\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mlci\u001b[0m:\u001b[36m562\u001b[0m - \u001b[1mCalculating dynamic inventory...\u001b[0m\n", "/Users/timodiepers/Documents/Coding/bw_timex/.venv/lib/python3.12/site-packages/scikits/umfpack/umfpack.py:737: UmfpackWarning: (almost) singular matrix! (estimated cond. number: 4.97e+12)\n", " warnings.warn(msg, UmfpackWarning)\n", "/Users/timodiepers/Documents/Coding/bw_timex/.venv/lib/python3.12/site-packages/scikits/umfpack/umfpack.py:737: UmfpackWarning: (almost) singular matrix! (estimated cond. number: 4.97e+12)\n", @@ -1217,7 +1198,7 @@ ], "metadata": { "kernelspec": { - "display_name": ".venv (3.12.12)", + "display_name": ".venv (3.12.12.final.0)", "language": "python", "name": "python3" }, diff --git a/notebooks/examples/electric_vehicle_premise_detailed.ipynb b/notebooks/examples/electric_vehicle_premise_detailed.ipynb index 5eb41ba7..0c135557 100644 --- a/notebooks/examples/electric_vehicle_premise_detailed.ipynb +++ b/notebooks/examples/electric_vehicle_premise_detailed.ipynb @@ -216,6 +216,10 @@ "# location) with the copies this notebook creates below in ev_background_ - same\n", "# date, different database. bw_timex would then refuse to resolve the resulting\n", "# ambiguity. This cleanup is safe: these nodes were created by this notebook, not premise.\n", + "from datetime import datetime\n", + "\n", + "from bw_timex import set_database_metadata\n", + "\n", "for db in [db_2020, db_2030, db_2040]:\n", " for code in [\"glider_production_without_eol\", \"powertrain_production_without_eol\", \"battery_production_without_eol\"]:\n", " try:\n", @@ -263,7 +267,12 @@ " # For the battery, some waste treatment is buried in the process \"battery cell production, Li-ion, \n", " # LiMn2O4\" - but not for the whole mass of the battery(?). For simplicity, we just leave it in there.\n", "\n", - " modified_db.process()" + " modified_db.process()\n", + "\n", + " # These copies represent the same point in time as the premise database they were\n", + " # copied from, so we record that here too - otherwise bw_timex would not know when\n", + " # they occur and drop them from the temporal mapping.\n", + " set_database_metadata(modified_name, representative_time=datetime.strptime(year, \"%Y\"))" ] }, { @@ -694,40 +703,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "`bw_timex` also needs to know the representative time of the databases:" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": { - "execution": { - "iopub.execute_input": "2026-08-03T20:00:28.316357Z", - "iopub.status.busy": "2026-08-03T20:00:28.316297Z", - "iopub.status.idle": "2026-08-03T20:00:28.318261Z", - "shell.execute_reply": "2026-08-03T20:00:28.317973Z" - } - }, - "outputs": [], - "source": [ - "from datetime import datetime\n", - "\n", - "database_dates = {\n", - " db_2020.name: datetime.strptime(\"2020\", \"%Y\"),\n", - " db_2030.name: datetime.strptime(\"2030\", \"%Y\"),\n", - " db_2040.name: datetime.strptime(\"2040\", \"%Y\"),\n", - " \"ev_background_2020\": datetime.strptime(\"2020\", \"%Y\"),\n", - " \"ev_background_2030\": datetime.strptime(\"2030\", \"%Y\"),\n", - " \"ev_background_2040\": datetime.strptime(\"2040\", \"%Y\"),\n", - " \"foreground\": \"dynamic\", # flag databases that should be temporally distributed with \"dynamic\"\n", - "}" + "premise >= 2.4.9.2 writes the point in time each background database represents as `representative_time` metadata on the database itself, so `bw_timex` finds it automatically - no mapping needed in this script. For databases from an earlier premise, set it yourself with `set_database_metadata`, as we did for our own `ev_background_` copies above, since they represent the same points in time. `database_dates` is the same fallback, passed straight to `TimexLCA`, and it also lets you override what the metadata says for any database." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Now, we can instantiate a `TimexLCA`. It's structure is similar to a normal `bw2calc.LCA`, but with the additional argument `database_dates`.\n", + "Now, we can instantiate a `TimexLCA`. It's structure is similar to a normal `bw2calc.LCA`.\n", "\n", "Not sure about the required inputs? Check the documentation using `?`. All our classes and methods have docstrings!" ] @@ -807,7 +790,7 @@ } ], "source": [ - "tlca = TimexLCA({driving: 1}, method, database_dates)" + "tlca = TimexLCA({driving: 1}, method)" ] }, { diff --git a/notebooks/teaching/ev_walkthrough_premise.ipynb b/notebooks/teaching/ev_walkthrough_premise.ipynb index 70430bf9..3c8295e4 100644 --- a/notebooks/teaching/ev_walkthrough_premise.ipynb +++ b/notebooks/teaching/ev_walkthrough_premise.ipynb @@ -382,25 +382,13 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Specify prospective background database dates\n", + "### Prospective background databases\n", "\n", - "Created with [`premise`](https://github.com/polca/premise) following the REMIND-EU SSP2 NDC scenario.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [], - "source": [ - "from datetime import datetime\n", - "\n", - "database_dates = {\n", - " \"ei312_REMIND-EU_SSP2_NDC_2020\": datetime.strptime(\"2020\", \"%Y\"),\n", - " \"ei312_REMIND-EU_SSP2_NDC_2030\": datetime.strptime(\"2030\", \"%Y\"),\n", - " \"ei312_REMIND-EU_SSP2_NDC_2040\": datetime.strptime(\"2040\", \"%Y\"),\n", - " \"foreground\": \"dynamic\", # Doesn't have a fixed date, but will be distributed over time\n", - "}" + "Created with [`premise`](https://github.com/polca/premise) following the REMIND-EU SSP2 NDC scenario.\n", + "premise >= 2.4.9.2 writes the point in time each database represents as `representative_time`\n", + "metadata on the database itself, so `bw_timex` finds it automatically - no mapping needed in this\n", + "script. For databases from an earlier premise, set it yourself with `set_database_metadata`, or\n", + "pass `database_dates` to `TimexLCA` to map (or override) it directly." ] }, { @@ -430,7 +418,7 @@ "source": [ "from bw_timex import TimexLCA\n", "\n", - "tlca = TimexLCA({driving: 1}, method, database_dates)" + "tlca = TimexLCA({driving: 1}, method)" ] }, { @@ -1604,7 +1592,7 @@ } ], "source": [ - "tlca = TimexLCA(demand={driving: 1}, method=method, database_dates=database_dates)\n", + "tlca = TimexLCA(demand={driving: 1}, method=method)\n", "tlca.build_timeline(starting_datetime=\"2025-01-01\", temporal_grouping=\"month\", graph_traversal=\"bfs\")\n", "tlca.lci()\n", "tlca.static_lcia()\n", diff --git a/notebooks/teaching/exercise_ev_vs_petrol_solutions.ipynb b/notebooks/teaching/exercise_ev_vs_petrol_solutions.ipynb index 6fbf578f..f33322d5 100644 --- a/notebooks/teaching/exercise_ev_vs_petrol_solutions.ipynb +++ b/notebooks/teaching/exercise_ev_vs_petrol_solutions.ipynb @@ -602,25 +602,9 @@ "source": [ "Now we can start using `bw_timex` to build the process timeline, build the time-explicit inventory and calculate the time explicit scores.\n", "\n", - "Start by creating a dictionary mapping the respective databases to the relevant timestamps. \n", - "Then instantiate your timexLCA object, build the timeline, calculate the LCI, and the LCIA for both options.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from datetime import datetime\n", + "premise >= 2.4.9.2 writes the point in time each background database represents as `representative_time` metadata on the database itself, so `bw_timex` finds it automatically - no `database_dates` mapping needed here (for databases from an earlier premise, set it yourself with `set_database_metadata`). But this project holds *two* IAM scenarios (NPi and PkBudg650), so we need to pick one with the `scenario` argument, e.g. `scenario={\"pathway\": \"SSP2-NPi\"}` - it filters on that same premise >= 2.4.9.2 metadata, so an earlier premise leaves no `pathway` key to filter on either.\n", "\n", - "database_dates = {\n", - " db_2020_NPi.name: datetime.strptime(\"2020\", \"%Y\"),\n", - " db_2030_NPi.name: datetime.strptime(\"2030\", \"%Y\"),\n", - " db_2040_NPi.name: datetime.strptime(\"2040\", \"%Y\"),\n", - " db_2050_NPi.name: datetime.strptime(\"2050\", \"%Y\"),\n", - " foreground.name: \"dynamic\", # flag databases that should be temporally distributed with \"dynamic\"\n", - "}" + "Then instantiate your timexLCA object, build the timeline, calculate the LCI, and the LCIA for both options.\n" ] }, { @@ -631,7 +615,7 @@ "source": [ "from bw_timex import TimexLCA\n", "\n", - "tlca_BEV = TimexLCA({LC_BEV: 1}, method, database_dates)\n", + "tlca_BEV = TimexLCA({LC_BEV: 1}, method, scenario={\"pathway\": \"SSP2-NPi\"})\n", "tlca_BEV.build_timeline(temporal_grouping=\"year\") # build timeline with yearly steps" ] }, @@ -668,7 +652,7 @@ "metadata": {}, "outputs": [], "source": [ - "tlca_ICEC = TimexLCA({LC_ICEC: 1}, method, database_dates)\n", + "tlca_ICEC = TimexLCA({LC_ICEC: 1}, method, scenario={\"pathway\": \"SSP2-NPi\"})\n", "tlca_ICEC.build_timeline(temporal_grouping=\"year\") # build timeline with yearly steps\n", "tlca_ICEC.lci() # calculate the dynamic inventory\n", "tlca_ICEC.dynamic_lcia(metric=\"GWP\", time_horizon=100, fixed_time_horizon=True)" diff --git a/notebooks/tutorials/1_getting_started.ipynb b/notebooks/tutorials/1_getting_started.ipynb index a5529b55..97ab91d1 100644 --- a/notebooks/tutorials/1_getting_started.ipynb +++ b/notebooks/tutorials/1_getting_started.ipynb @@ -61,6 +61,12 @@ "cell_type": "code", "execution_count": 1, "metadata": { + "execution": { + "iopub.execute_input": "2026-08-21T10:19:45.627532Z", + "iopub.status.busy": "2026-08-21T10:19:45.627194Z", + "iopub.status.idle": "2026-08-21T10:19:46.995674Z", + "shell.execute_reply": "2026-08-21T10:19:46.995268Z" + }, "jupyter": { "source_hidden": true } @@ -70,54 +76,97 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/timodiepers/Documents/Coding/bw_timex/.venv/lib/python3.13/site-packages/bw2calc/__init__.py:53: UserWarning: \n", - "It seems like you have an ARM architecture, but haven't installed scikit-umfpack:\n", - "\n", - " https://pypi.org/project/scikit-umfpack/\n", - "\n", - "Installing it could give you much faster calculations.\n", - "\n", - " warnings.warn(UMFPACK_WARNING)\n", - "100%|██████████| 1/1 [00:00<00:00, 12633.45it/s]" + "\r", + " 0%| | 0/1 [00:00\n", " \n", " 0\n", - " 2024-05-01\n", + " 2024-06-01\n", " glider\n", - " 2026-05-01\n", + " 2026-06-01\n", " production of an electric vehicle\n", " 588.0\n", - " {'background_2020': 0.567, 'background_2030': ...\n", + " {'background_2020': 0.558, 'background_2030': ...\n", " \n", " \n", " 1\n", - " 2024-06-01\n", + " 2024-07-01\n", " glider\n", - " 2026-06-01\n", + " 2026-07-01\n", " production of an electric vehicle\n", " 588.0\n", - " {'background_2020': 0.558, 'background_2030': ...\n", + " {'background_2020': 0.55, 'background_2030': 0...\n", " \n", " \n", " 2\n", - " 2025-05-01\n", + " 2025-06-01\n", " glider\n", - " 2026-05-01\n", + " 2026-06-01\n", " production of an electric vehicle\n", " 84.0\n", - " {'background_2020': 0.467, 'background_2030': ...\n", + " {'background_2020': 0.459, 'background_2030': ...\n", " \n", " \n", " 3\n", - " 2025-05-01\n", + " 2025-06-01\n", " powertrain\n", - " 2026-05-01\n", + " 2026-06-01\n", " production of an electric vehicle\n", " 80.0\n", - " {'background_2020': 0.467, 'background_2030': ...\n", + " {'background_2020': 0.459, 'background_2030': ...\n", " \n", " \n", " 4\n", - " 2025-05-01\n", + " 2025-06-01\n", " battery\n", - " 2026-05-01\n", + " 2026-06-01\n", " production of an electric vehicle\n", " 280.0\n", - " {'background_2020': 0.467, 'background_2030': ...\n", + " {'background_2020': 0.459, 'background_2030': ...\n", " \n", " \n", " 5\n", - " 2025-06-01\n", + " 2025-07-01\n", " glider\n", - " 2026-06-01\n", + " 2026-07-01\n", " production of an electric vehicle\n", " 84.0\n", - " {'background_2020': 0.459, 'background_2030': ...\n", + " {'background_2020': 0.45, 'background_2030': 0...\n", " \n", " \n", " 6\n", - " 2025-06-01\n", + " 2025-07-01\n", " powertrain\n", - " 2026-06-01\n", + " 2026-07-01\n", " production of an electric vehicle\n", " 80.0\n", - " {'background_2020': 0.459, 'background_2030': ...\n", + " {'background_2020': 0.45, 'background_2030': 0...\n", " \n", " \n", " 7\n", - " 2025-06-01\n", + " 2025-07-01\n", " battery\n", - " 2026-06-01\n", + " 2026-07-01\n", " production of an electric vehicle\n", " 280.0\n", - " {'background_2020': 0.459, 'background_2030': ...\n", + " {'background_2020': 0.45, 'background_2030': 0...\n", " \n", " \n", " 8\n", - " 2026-05-01\n", + " 2026-06-01\n", " glider\n", - " 2026-05-01\n", + " 2026-06-01\n", " production of an electric vehicle\n", " 168.0\n", - " {'background_2020': 0.367, 'background_2030': ...\n", + " {'background_2020': 0.359, 'background_2030': ...\n", " \n", " \n", " 9\n", - " 2026-05-01\n", + " 2026-06-01\n", " production of an electric vehicle\n", - " 2026-08-01\n", + " 2026-09-01\n", " driving an electric vehicle\n", " 0.2\n", " None\n", " \n", " \n", " 10\n", - " 2026-06-01\n", + " 2026-07-01\n", " glider\n", - " 2026-06-01\n", + " 2026-07-01\n", " production of an electric vehicle\n", " 168.0\n", - " {'background_2020': 0.359, 'background_2030': ...\n", + " {'background_2020': 0.35, 'background_2030': 0...\n", " \n", " \n", " 11\n", - " 2026-06-01\n", + " 2026-07-01\n", " production of an electric vehicle\n", - " 2026-08-01\n", + " 2026-09-01\n", " driving an electric vehicle\n", " 0.8\n", " None\n", " \n", " \n", " 12\n", - " 2026-08-01\n", + " 2026-09-01\n", " electricity\n", - " 2026-08-01\n", + " 2026-09-01\n", " driving an electric vehicle\n", " 1875.0\n", - " {'background_2020': 0.342, 'background_2030': ...\n", + " {'background_2020': 0.333, 'background_2030': ...\n", " \n", " \n", " 13\n", - " 2026-08-01\n", + " 2026-09-01\n", " driving an electric vehicle\n", - " 2026-08-01\n", + " 2026-09-01\n", " -1\n", " 1.0\n", " None\n", " \n", " \n", " 14\n", - " 2027-08-01\n", + " 2027-09-01\n", " electricity\n", - " 2026-08-01\n", + " 2026-09-01\n", " driving an electric vehicle\n", " 1875.0\n", - " {'background_2020': 0.242, 'background_2030': ...\n", + " {'background_2020': 0.234, 'background_2030': ...\n", " \n", " \n", " 15\n", - " 2028-08-01\n", + " 2028-09-01\n", " electricity\n", - " 2026-08-01\n", + " 2026-09-01\n", " driving an electric vehicle\n", " 1875.0\n", - " {'background_2020': 0.142, 'background_2030': ...\n", + " {'background_2020': 0.133, 'background_2030': ...\n", " \n", " \n", " 16\n", - " 2029-08-01\n", + " 2029-09-01\n", " electricity\n", - " 2026-08-01\n", + " 2026-09-01\n", " driving an electric vehicle\n", " 1875.0\n", - " {'background_2020': 0.042, 'background_2030': ...\n", + " {'background_2020': 0.033, 'background_2030': ...\n", " \n", " \n", " 17\n", - " 2030-08-01\n", + " 2030-09-01\n", " electricity\n", - " 2026-08-01\n", + " 2026-09-01\n", " driving an electric vehicle\n", " 1875.0\n", - " {'background_2030': 0.942, 'background_2040': ...\n", + " {'background_2030': 0.933, 'background_2040': ...\n", " \n", " \n", " 18\n", - " 2031-08-01\n", + " 2031-09-01\n", " electricity\n", - " 2026-08-01\n", + " 2026-09-01\n", " driving an electric vehicle\n", " 1875.0\n", - " {'background_2030': 0.842, 'background_2040': ...\n", + " {'background_2030': 0.834, 'background_2040': ...\n", " \n", " \n", " 19\n", - " 2032-08-01\n", + " 2032-09-01\n", " electricity\n", - " 2026-08-01\n", + " 2026-09-01\n", " driving an electric vehicle\n", " 1875.0\n", - " {'background_2030': 0.742, 'background_2040': ...\n", + " {'background_2030': 0.733, 'background_2040': ...\n", " \n", " \n", " 20\n", - " 2033-08-01\n", + " 2033-09-01\n", " electricity\n", - " 2026-08-01\n", + " 2026-09-01\n", " driving an electric vehicle\n", " 1875.0\n", - " {'background_2030': 0.642, 'background_2040': ...\n", + " {'background_2030': 0.633, 'background_2040': ...\n", " \n", " \n", " 21\n", - " 2034-08-01\n", + " 2034-09-01\n", " electricity\n", - " 2026-08-01\n", + " 2026-09-01\n", " driving an electric vehicle\n", " 1875.0\n", - " {'background_2030': 0.542, 'background_2040': ...\n", + " {'background_2030': 0.533, 'background_2040': ...\n", " \n", " \n", " 22\n", - " 2035-08-01\n", + " 2035-09-01\n", " electricity\n", - " 2026-08-01\n", + " 2026-09-01\n", " driving an electric vehicle\n", " 1875.0\n", - " {'background_2030': 0.442, 'background_2040': ...\n", + " {'background_2030': 0.433, 'background_2040': ...\n", " \n", " \n", " 23\n", - " 2036-08-01\n", + " 2036-09-01\n", " electricity\n", - " 2026-08-01\n", + " 2026-09-01\n", " driving an electric vehicle\n", " 1875.0\n", - " {'background_2030': 0.342, 'background_2040': ...\n", + " {'background_2030': 0.333, 'background_2040': ...\n", " \n", " \n", " 24\n", - " 2037-08-01\n", + " 2037-09-01\n", " electricity\n", - " 2026-08-01\n", + " 2026-09-01\n", " driving an electric vehicle\n", " 1875.0\n", - " {'background_2030': 0.242, 'background_2040': ...\n", + " {'background_2030': 0.233, 'background_2040': ...\n", " \n", " \n", " 25\n", - " 2038-08-01\n", + " 2038-09-01\n", " electricity\n", - " 2026-08-01\n", + " 2026-09-01\n", " driving an electric vehicle\n", " 1875.0\n", - " {'background_2030': 0.142, 'background_2040': ...\n", + " {'background_2030': 0.133, 'background_2040': ...\n", " \n", " \n", " 26\n", - " 2039-08-01\n", + " 2039-09-01\n", " electricity\n", - " 2026-08-01\n", + " 2026-09-01\n", " driving an electric vehicle\n", " 1875.0\n", - " {'background_2030': 0.042, 'background_2040': ...\n", + " {'background_2030': 0.033, 'background_2040': ...\n", " \n", " \n", " 27\n", - " 2040-08-01\n", + " 2040-09-01\n", " electricity\n", - " 2026-08-01\n", + " 2026-09-01\n", " driving an electric vehicle\n", " 1875.0\n", " {'background_2040': 1}\n", " \n", " \n", " 28\n", - " 2041-08-01\n", + " 2041-09-01\n", " electricity\n", - " 2026-08-01\n", + " 2026-09-01\n", " driving an electric vehicle\n", " 1875.0\n", " {'background_2040': 1}\n", " \n", " \n", " 29\n", - " 2042-08-01\n", + " 2042-09-01\n", " used electric vehicle\n", - " 2026-08-01\n", + " 2026-09-01\n", " driving an electric vehicle\n", " -1.0\n", " None\n", " \n", " \n", " 30\n", - " 2042-11-01\n", + " 2042-12-01\n", " glider_eol\n", - " 2042-08-01\n", + " 2042-09-01\n", " used electric vehicle\n", " -840.0\n", " {'background_2040': 1}\n", " \n", " \n", " 31\n", - " 2042-11-01\n", + " 2042-12-01\n", " powertrain_eol\n", - " 2042-08-01\n", + " 2042-09-01\n", " used electric vehicle\n", " -80.0\n", " {'background_2040': 1}\n", " \n", " \n", " 32\n", - " 2042-11-01\n", + " 2042-12-01\n", " battery_eol\n", - " 2042-08-01\n", + " 2042-09-01\n", " used electric vehicle\n", " -280.0\n", " {'background_2040': 1}\n", @@ -992,39 +1010,39 @@ ], "text/plain": [ " date_producer producer_name date_consumer \\\n", - "0 2024-05-01 glider 2026-05-01 \n", - "1 2024-06-01 glider 2026-06-01 \n", - "2 2025-05-01 glider 2026-05-01 \n", - "3 2025-05-01 powertrain 2026-05-01 \n", - "4 2025-05-01 battery 2026-05-01 \n", - "5 2025-06-01 glider 2026-06-01 \n", - "6 2025-06-01 powertrain 2026-06-01 \n", - "7 2025-06-01 battery 2026-06-01 \n", - "8 2026-05-01 glider 2026-05-01 \n", - "9 2026-05-01 production of an electric vehicle 2026-08-01 \n", - "10 2026-06-01 glider 2026-06-01 \n", - "11 2026-06-01 production of an electric vehicle 2026-08-01 \n", - "12 2026-08-01 electricity 2026-08-01 \n", - "13 2026-08-01 driving an electric vehicle 2026-08-01 \n", - "14 2027-08-01 electricity 2026-08-01 \n", - "15 2028-08-01 electricity 2026-08-01 \n", - "16 2029-08-01 electricity 2026-08-01 \n", - "17 2030-08-01 electricity 2026-08-01 \n", - "18 2031-08-01 electricity 2026-08-01 \n", - "19 2032-08-01 electricity 2026-08-01 \n", - "20 2033-08-01 electricity 2026-08-01 \n", - "21 2034-08-01 electricity 2026-08-01 \n", - "22 2035-08-01 electricity 2026-08-01 \n", - "23 2036-08-01 electricity 2026-08-01 \n", - "24 2037-08-01 electricity 2026-08-01 \n", - "25 2038-08-01 electricity 2026-08-01 \n", - "26 2039-08-01 electricity 2026-08-01 \n", - "27 2040-08-01 electricity 2026-08-01 \n", - "28 2041-08-01 electricity 2026-08-01 \n", - "29 2042-08-01 used electric vehicle 2026-08-01 \n", - "30 2042-11-01 glider_eol 2042-08-01 \n", - "31 2042-11-01 powertrain_eol 2042-08-01 \n", - "32 2042-11-01 battery_eol 2042-08-01 \n", + "0 2024-06-01 glider 2026-06-01 \n", + "1 2024-07-01 glider 2026-07-01 \n", + "2 2025-06-01 glider 2026-06-01 \n", + "3 2025-06-01 powertrain 2026-06-01 \n", + "4 2025-06-01 battery 2026-06-01 \n", + "5 2025-07-01 glider 2026-07-01 \n", + "6 2025-07-01 powertrain 2026-07-01 \n", + "7 2025-07-01 battery 2026-07-01 \n", + "8 2026-06-01 glider 2026-06-01 \n", + "9 2026-06-01 production of an electric vehicle 2026-09-01 \n", + "10 2026-07-01 glider 2026-07-01 \n", + "11 2026-07-01 production of an electric vehicle 2026-09-01 \n", + "12 2026-09-01 electricity 2026-09-01 \n", + "13 2026-09-01 driving an electric vehicle 2026-09-01 \n", + "14 2027-09-01 electricity 2026-09-01 \n", + "15 2028-09-01 electricity 2026-09-01 \n", + "16 2029-09-01 electricity 2026-09-01 \n", + "17 2030-09-01 electricity 2026-09-01 \n", + "18 2031-09-01 electricity 2026-09-01 \n", + "19 2032-09-01 electricity 2026-09-01 \n", + "20 2033-09-01 electricity 2026-09-01 \n", + "21 2034-09-01 electricity 2026-09-01 \n", + "22 2035-09-01 electricity 2026-09-01 \n", + "23 2036-09-01 electricity 2026-09-01 \n", + "24 2037-09-01 electricity 2026-09-01 \n", + "25 2038-09-01 electricity 2026-09-01 \n", + "26 2039-09-01 electricity 2026-09-01 \n", + "27 2040-09-01 electricity 2026-09-01 \n", + "28 2041-09-01 electricity 2026-09-01 \n", + "29 2042-09-01 used electric vehicle 2026-09-01 \n", + "30 2042-12-01 glider_eol 2042-09-01 \n", + "31 2042-12-01 powertrain_eol 2042-09-01 \n", + "32 2042-12-01 battery_eol 2042-09-01 \n", "\n", " consumer_name amount \\\n", "0 production of an electric vehicle 588.0 \n", @@ -1062,33 +1080,33 @@ "32 used electric vehicle -280.0 \n", "\n", " temporal_market_shares \n", - "0 {'background_2020': 0.567, 'background_2030': ... \n", - "1 {'background_2020': 0.558, 'background_2030': ... \n", - "2 {'background_2020': 0.467, 'background_2030': ... \n", - "3 {'background_2020': 0.467, 'background_2030': ... \n", - "4 {'background_2020': 0.467, 'background_2030': ... \n", - "5 {'background_2020': 0.459, 'background_2030': ... \n", - "6 {'background_2020': 0.459, 'background_2030': ... \n", - "7 {'background_2020': 0.459, 'background_2030': ... \n", - "8 {'background_2020': 0.367, 'background_2030': ... \n", + "0 {'background_2020': 0.558, 'background_2030': ... \n", + "1 {'background_2020': 0.55, 'background_2030': 0... \n", + "2 {'background_2020': 0.459, 'background_2030': ... \n", + "3 {'background_2020': 0.459, 'background_2030': ... \n", + "4 {'background_2020': 0.459, 'background_2030': ... \n", + "5 {'background_2020': 0.45, 'background_2030': 0... \n", + "6 {'background_2020': 0.45, 'background_2030': 0... \n", + "7 {'background_2020': 0.45, 'background_2030': 0... \n", + "8 {'background_2020': 0.359, 'background_2030': ... \n", "9 None \n", - "10 {'background_2020': 0.359, 'background_2030': ... \n", + "10 {'background_2020': 0.35, 'background_2030': 0... \n", "11 None \n", - "12 {'background_2020': 0.342, 'background_2030': ... \n", + "12 {'background_2020': 0.333, 'background_2030': ... \n", "13 None \n", - "14 {'background_2020': 0.242, 'background_2030': ... \n", - "15 {'background_2020': 0.142, 'background_2030': ... \n", - "16 {'background_2020': 0.042, 'background_2030': ... \n", - "17 {'background_2030': 0.942, 'background_2040': ... \n", - "18 {'background_2030': 0.842, 'background_2040': ... \n", - "19 {'background_2030': 0.742, 'background_2040': ... \n", - "20 {'background_2030': 0.642, 'background_2040': ... \n", - "21 {'background_2030': 0.542, 'background_2040': ... \n", - "22 {'background_2030': 0.442, 'background_2040': ... \n", - "23 {'background_2030': 0.342, 'background_2040': ... \n", - "24 {'background_2030': 0.242, 'background_2040': ... \n", - "25 {'background_2030': 0.142, 'background_2040': ... \n", - "26 {'background_2030': 0.042, 'background_2040': ... \n", + "14 {'background_2020': 0.234, 'background_2030': ... \n", + "15 {'background_2020': 0.133, 'background_2030': ... \n", + "16 {'background_2020': 0.033, 'background_2030': ... \n", + "17 {'background_2030': 0.933, 'background_2040': ... \n", + "18 {'background_2030': 0.834, 'background_2040': ... \n", + "19 {'background_2030': 0.733, 'background_2040': ... \n", + "20 {'background_2030': 0.633, 'background_2040': ... \n", + "21 {'background_2030': 0.533, 'background_2040': ... \n", + "22 {'background_2030': 0.433, 'background_2040': ... \n", + "23 {'background_2030': 0.333, 'background_2040': ... \n", + "24 {'background_2030': 0.233, 'background_2040': ... \n", + "25 {'background_2030': 0.133, 'background_2040': ... \n", + "26 {'background_2030': 0.033, 'background_2040': ... \n", "27 {'background_2040': 1} \n", "28 {'background_2040': 1} \n", "29 None \n", @@ -1121,10 +1139,10 @@ "execution_count": 9, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T07:51:30.375893Z", - "iopub.status.busy": "2026-08-04T07:51:30.375778Z", - "iopub.status.idle": "2026-08-04T07:51:30.438287Z", - "shell.execute_reply": "2026-08-04T07:51:30.437578Z" + "iopub.execute_input": "2026-08-21T10:20:25.850819Z", + "iopub.status.busy": "2026-08-21T10:20:25.850755Z", + "iopub.status.idle": "2026-08-21T10:20:25.903806Z", + "shell.execute_reply": "2026-08-21T10:20:25.903420Z" } }, "outputs": [ @@ -1132,14 +1150,14 @@ "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-08-04 09:51:30.382\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mlci\u001b[0m:\u001b[36m529\u001b[0m - \u001b[1mExpanding matrices...\u001b[0m\n" + "\u001b[32m2026-08-21 12:20:25.855\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mlci\u001b[0m:\u001b[36m634\u001b[0m - \u001b[1mExpanding matrices...\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-08-04 09:51:30.399\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mlci\u001b[0m:\u001b[36m548\u001b[0m - \u001b[1mCalculating dynamic inventory...\u001b[0m\n" + "\u001b[32m2026-08-21 12:20:25.869\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mlci\u001b[0m:\u001b[36m653\u001b[0m - \u001b[1mCalculating dynamic inventory...\u001b[0m\n" ] } ], @@ -1159,17 +1177,17 @@ "execution_count": 10, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T07:51:30.440155Z", - "iopub.status.busy": "2026-08-04T07:51:30.440037Z", - "iopub.status.idle": "2026-08-04T07:51:30.443133Z", - "shell.execute_reply": "2026-08-04T07:51:30.442698Z" + "iopub.execute_input": "2026-08-21T10:20:25.904961Z", + "iopub.status.busy": "2026-08-21T10:20:25.904894Z", + "iopub.status.idle": "2026-08-21T10:20:25.907281Z", + "shell.execute_reply": "2026-08-21T10:20:25.906957Z" } }, "outputs": [ { "data": { "text/plain": [ - "15166.963039459766" + "15066.797664957092" ] }, "execution_count": 10, @@ -1194,10 +1212,10 @@ "execution_count": 11, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T07:51:30.444727Z", - "iopub.status.busy": "2026-08-04T07:51:30.444645Z", - "iopub.status.idle": "2026-08-04T07:51:30.446842Z", - "shell.execute_reply": "2026-08-04T07:51:30.446413Z" + "iopub.execute_input": "2026-08-21T10:20:25.908315Z", + "iopub.status.busy": "2026-08-21T10:20:25.908252Z", + "iopub.status.idle": "2026-08-21T10:20:25.910008Z", + "shell.execute_reply": "2026-08-21T10:20:25.909688Z" } }, "outputs": [ @@ -1230,17 +1248,17 @@ "execution_count": 12, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T07:51:30.447993Z", - "iopub.status.busy": "2026-08-04T07:51:30.447929Z", - "iopub.status.idle": "2026-08-04T07:51:30.458169Z", - "shell.execute_reply": "2026-08-04T07:51:30.457714Z" + "iopub.execute_input": "2026-08-21T10:20:25.910938Z", + "iopub.status.busy": "2026-08-21T10:20:25.910881Z", + "iopub.status.idle": "2026-08-21T10:20:25.918840Z", + "shell.execute_reply": "2026-08-21T10:20:25.918531Z" } }, "outputs": [ { "data": { "text/plain": [ - "np.float64(15166.963039459766)" + "np.float64(15066.797664957092)" ] }, "execution_count": 12, @@ -1271,16 +1289,16 @@ "execution_count": 13, "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T07:51:30.459429Z", - "iopub.status.busy": "2026-08-04T07:51:30.459353Z", - "iopub.status.idle": "2026-08-04T07:51:30.574216Z", - "shell.execute_reply": "2026-08-04T07:51:30.573700Z" + "iopub.execute_input": "2026-08-21T10:20:25.919972Z", + "iopub.status.busy": "2026-08-21T10:20:25.919899Z", + "iopub.status.idle": "2026-08-21T10:20:26.016856Z", + "shell.execute_reply": "2026-08-21T10:20:26.016514Z" } }, "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAABggAAAJFCAYAAAAI43xTAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAArxtJREFUeJzs3QmYjeX7wPHb2KbMjH039qUUUgrZlyRLtoiEIlkSEtkqClmiyJr6SYQkeyQhWbK1WMqvlHWshQajxjb+1/38rnP+58ycGbM5Z855vp/rOteZec/zbud5jzme+33uO11kZORNAQAAAAAAAAAAVgny9QEAAAAAAAAAAADvI0AAAAAAAAAAAICFCBAAAAAAAAAAAGAhAgQAAAAAAAAAAFiIAAEAAAAAAAAAABYiQAAAAAAAAAAAgIUIEAAAAAAAAAAAYCECBAAAAAAAAAAAWIgAAQAAAAAAAAAAFiJAAAAAAAAAAACAhQgQwKPo6Gg5dOiQebYF52wH+tkO9LMd6OfARx/bgX62A/1sB/rZDvRz4LOxjwGbESBAvG7cuGHdu8M524F+tgP9bAf6OfDRx3agn+1AP9uBfrYD/Rz4bOxjwFYECAAAAAAAAAAAsBABAgAAAAAAAAAALESAAAAAAAAAAAAACxEgAAAAAAAAAADAQgQIAAAAAAAAAACwEAECAAAAAAAAAAAsRIAAAAAAAAAAAAALESAAAAAAAAAAAMBCBAgAAAAAAAAAALAQAQIAAAAAAAAAACxEgAAAAAAAAAAAAAsRIAAAAAAAAAAAwEIECAAAAAAAAAAAsBABAgAAAAAAAAAALESAAAAAAAAAAAAACxEgAAAAAAAAAADAQgQIAAAAAAAAAACwEAECAAAAAAAAAAAsRIAAAAAAAAAAAAALZfD1AQAAAAAAAABIXffdd5+cOHEiWevevHlT0qVLl+x9FyxYUHbv3p3s9QF4DwECAAAAAAAAIMD8E3VJsmXL5rN9A/APBAgAAAAAAACAAFOpUD5ZUSSjT/b9+NFrPtkvgKQjQAAAAAAAAICAVji8oFy8dNkn+w4LzSLHIpKX6icl/k2XXh7cdjTJ6910STGU3CRD2QqGJ3NNAN5GgAAAAAAAAAABrWrFYjKrb4hP9t15YpRP9rv0m03JWi86OloiIiIkPDxcgoODU/24AKQtQb4+AAAAAAAAAAAA4H0ECAAAAAAAAAAAsBAphgAAAAAAACxiYz7+M+eipHafw+ILWcJy+2S/AJAYBAgAAAAAAAAsYmM+/o1bdyd7XXLyAwhkpBgCAAAAAAAAAMBCBAgAAAAAAAAAALAQKYYAAAAAAIDVfJWTn3z8AABfI0AAAAAAAACs5quc/OTjBwD4GimGAAAAAAAAAACwEAECAAAAAAAAAAAsRIAAAAAAAAAAAAALUYMAAAAAAABYW7BXnTkXJbX7HPb6frOE5fb6PgEAcEWAAAAAAAAAWFuwV23cujtZ60VHR0tERISEh4dLcHBwqh8XAAC3GymGAAAAAAAAAACwEAECAAAAAAAAAAAsRIAAAAAAAAAAAAALESAAAAAAAAAAAMBCFCkGAAAAACAehcMLysVLl73+/oSFZpFjESfEF86ci5LafQ57fb9ZwnJ7fZ8AANiOAAEAAAAAAPGoWrGYzOob4vX3p/PEKPGVjVt3J2u96OhoiYiIkPDwcAkODk714wIAAKmPFEMAAAAAAAAAAFiIAAEAAAAAAAAAABYiQAAAAAAAAAAAgIUIEAAAAAAAAAAAYCGKFAMAAAAAEqVweEG5eOmy19+tsNAscizihPjCmXNRUrvPYa/vN0tYbq/vEwAA2IcAAQAAAAAgUapWLCaz+oZ4/d3qPDFKfGXj1t3JWi86OloiIiIkPDxcgoODU/24AAAArEkxtHDhQunbt6/Url1b8uTJI9myZZN58+Ylat0jR45IwYIFzTovvfRSvO0+++wzqVu3rhQoUECKFCkiTz75pOzeHf8XwR9//FFat24thQsXNuvUr19fli5dGm/706dPS69evaRMmTKSN29eqVSpkowfP16uXbuWqPMAAAAAAAAAAMC6GQQjR440d17kzJnTDK7rz4kRExMjPXr0uGU7HajXfeidHc8++6xERUXJkiVL5NFHH5Xly5dLlSpV3Npv2rRJWrVqZe4CadmypYSEhMiKFSvMusePH5cXX3zRrf2ZM2dMAOHEiRPSpEkTKVGihGzdutXs84cffpD58+dLunTpkviuAAAAAAAAAAAQ4DMIJk+eLHv37pWDBw9K586dE73e1KlTZdeuXTJ06NB42+g2x4wZIyVLlpQtW7bIqFGjZNKkSbJq1Srzep8+fUygweH69etmWVBQkGmjbXUdXVe3MWLECDl27JjbPoYNG2YCBxMmTJC5c+fK8OHDZe3atSbI8OWXX8rixYuT9b4AAAAAAAAAABDQAQJNLaSpfJLiwIEDZuBe0wqVK1cu3naaqkgH/V9++WXJmjWrc3n58uXNAP5vv/0m27Ztc5s9cPjwYXniiSdMGwddt1+/fnL16lVZsGCBc/mlS5dM6qGiRYuaGQYOOmNAAwfq448/TtK5AQAAAAAAAABgRYAgqW7cuGFSCxUvXlwGDBiQYFu9819p/YHY6tWrZ541HVBy2+sMhitXrkidOnXipBHSoEepUqVkx44d5pgBAAAAAAAAAPAWv6hBkFTvvPOO7NmzR9atWyeZMmVKsK2mGNIaAlrbIDatFeBo49re9TVXug3d1qFDh+K012CFJ7r8999/N3UVdJZBQqKjo8VbdCaE67MNOGc70M92oJ/tQD8HPvrYDvSzf4m5edNn+/Xm/4dSA9e2HehnO9jWz746X631CcD7Ai5AsG/fPhk3bpz07t1b7rvvvlu2v3jxouTOndvja6Ghoc42ru1VWFhYvOt4au+avsiVYzsXLly45bGePHnS6zMNtMCybThnO9DPdqCf7UA/Bz762A7+2M91a9eSS5f/8fp+Q7PcKRs2fiu+cPJMpNTuc8Tr+810RzZzU5U/8sdrO6U4ZzvQz4HPm32cPn36eG+uBXB7BVSAQCObjtRCAwcOlEBToEABr76X+odAZ0XcahZGoOCc6edAxbXNtR2ouLYD/9qmjwO/j/29n6vcX0xmv/S/m4q86Zl3L0l4eLj4wvpNP1jXz8nFOdPPgYprO/CvbRv7GLBZhkBLLbR//35Zu3atZM6cOVHr6B38rnf8u9ICw442ru1VQutky5Yt0TMEbjXDwNdTrfQPgW1TvDhnO9DPdqCf7UA/Bz762A7+2M9BsWqMeXO//vZe+XM/pxTnbAf62Q629bNt5wvYKqCKFO/du1diYmKkfv36ZpDe8WjatKl5/aOPPjK/P/XUU851tJZAVFSUx2lTnuoNeKpL4KDb0G25TolytHetS+BKl+s/uIUKFUrBmQMAAAAAAAAAYPEMgjp16kjOnDk9DtzrrILSpUtL5cqVpXz58s7XqlWrJjt37pQNGzZIu3bt3NZbv369s41re52poO1btWp1y/aVKlUyAYBvvvlGbt68Kelc7jI6duyYKVBco0YNyZAhoLoCAAAAAAAAAJDGBdSodNeuXT0u37x5swkQ6MD9u+++6/Za+/btZfLkyTJhwgRp1KiRM9WPzkZYvHixlClTRqpWrepsX6tWLSlatKh8/vnn0q1bN2ewQVMIaeBAgwFt27Z1SzHUsmVL+fTTT80Mhs6dO5vlGix48803zc+dOnW6De8GAAAAAAAAAAB+HiCYM2eObNu2zfysNQbU3LlzZcuWLeZnHcDv2LFjsrZdsmRJGTRokIwcOVKqV68ujz/+uEkTtGTJEvP6pEmTJCjo/zMx6Z3+7733npk90LhxYzP4HxISIitWrJCIiAgZMWKEFClSxG0fw4cPN8f68ssvy8aNG00Koq1bt8quXbukYcOGcWYiAAAAAAAAAABwu/lFgECDAwsWLHBbtn37dvNwSG6AQPXv318KFy4s06dPl1mzZknGjBlN0GHIkCFy3333xWlfs2ZNWbNmjYwePVqWLl0q165dk7Jly8obb7xhAgax5cuXT9atW2eCEDqTQdcNDw+XoUOHSp8+fdzSDgEAAAAAAAAA4A1+ESDQgXt9JJfm+I+MjEywTZs2bcwjsR544AGTZiixNEgwZcqURLcHAAAAAAAAAEBsDxAAAAAASNsKhxeUi5cue32/YaFZ5FjECfGFM+eipHafw17fb5aw3F7fJwAAAAITAQIAAAAAKVa1YjGZ1TfE6+9k54lR4isbt+5O1nrR0dGmfpmmHQ0ODk714wIAAAAS6/+r7wIAAAAAAAAAAGsQIAAAAAAAAAAAwEIECAAAAAAAAAAAsBABAgAAAAAAAAAALESAAAAAAAAAAAAACxEgAAAAAAAAAADAQgQIAAAAAAAAAACwEAECAAAAAAAAAAAslMHXBwAAAAAEmsLhBeXipcs+2XdYaBY5FnHC6/s9cy5Kavc57PX9ZgnL7fV9AgAAAIGCAAEAAACQyqpWLCaz+ob45H3tPDHKJ/vduHV3staLjo6WiIgICQ8Pl+Dg4FQ/LgAAAADxI8UQAAAAAAAAAAAWIkAAAAAAAAAAAICFCBAAAAAAAAAAAGAhAgQAAAAAAAAAAFiIAAEAAAAAAAAAABYiQAAAAAAAAAAAgIUIEAAAAAAAAAAAYCECBAAAAAAAAAAAWCiDrw8AAAAAga1weEG5eOmyT/YdFppFjkWc8Pp+z5yLktp9DosvZAnL7ZP9AgAAAPA/BAgAAABwW1WtWExm9Q3xybvceWKUT/a7cevuZK0XHR0tEREREh4eLsHBwal+XAAAAADgihRDAAAAAAAAAABYiAABAAAAAAAAAAAWIkAAAAAAAAAAAICFCBAAAAAAAAAAAGAhAgQAAAAAAAAAAFiIAAEAAAAAAAAAABYiQAAAAAAAAAAAgIUIEAAAAAAAAAAAYCECBAAAAAAAAAAAWIgAAQAAAAAAAAAAFsrg6wMAAACwSeHwgnLx0mWf7DssNIscizjh9f2eORcltfscFl/IEpbbJ/sFAAAAAH9AgAAAAMCLqlYsJrP6hvjkPe88Mcon+924dXey1ouOjpaIiAgJDw+X4ODgVD8uAAAAALAdKYYAAAAAAAAAALAQAQIAAAAAAAAAACxEgAAAAAAAAAAAAAsRIAAAAAAAAAAAwEIECAAAAAAAAAAAsBABAgAAAAAAAAAALESAAAAAAAAAAAAACxEgAAAAAAAAAADAQgQIAAAAAAAAAACwEAECAAAAAAAAAAAsRIAAAAAAAAAAAAALZfD1AQAAAHsVDi8oFy9d9sm+w0KzyLGIE17f75lzUVK7z2HxhSxhuX2yXwAAAABA2uQXAYKFCxfKtm3bZPfu3bJ//365evWqTJ06Vdq3b+/W7tq1a7J69Wr58ssv5ccff5QTJ05IunTppEyZMvLUU0/JM888I+nTp/e4j88++0xmzJghv/76q2TMmFGqVKkigwcPlvvuu89je93+6NGjZceOHXL9+nUpW7asvPDCC9KiRQuP7U+fPi0jR46Ur7/+WiIjIyU8PFzatm0rffr0MfsDAMBGVSsWk1l9Q3yy784To3yy341bdyd73ejoaImIiDDfI4KDg1P1uAAAAAAA9vGLAIEOrOt/hnPmzCl58+Y1P3ty+PBh6dSpk4SEhEjNmjXlsccek4sXL8qaNWvk5ZdflrVr18qnn35qggauxo8fb/ah/9l+9tlnJSoqSpYsWSKPPvqoLF++3AQLXG3atElatWpl/mPesmVLs78VK1aYdY8fPy4vvviiW/szZ85I/fr1TcCiSZMmUqJECdm6davZ5w8//CDz58+Pc0wAAAAAAAAAAIjtAYLJkydL8eLFpXDhwvLuu+/KG2+84bGdDtTrYH+7du0kS5YszuU6EK8D81999ZUZ8G/evLnztYMHD8qYMWOkZMmSsn79esmaNatZ3qVLF3nkkUfMHf46eyEo6H/lGnS2gC7T31etWiXly5c3y1955RWpV6+ejBgxQpo1a2aO1WHYsGEmcPDOO+9I586dzbKbN2/Kc889J4sXLzaPJ5544ja9ewAAAAAAAAAA+GmR4tq1a7sNuMenQIECZtDdNTig9HdN/6P0zn1X8+bNM4P+OsPAERxQOvCvswR+++03EyBwnT2gMxV0QN8RHFC6br9+/Uz6owULFjiXX7p0SZYuXSpFixY1MwwcdMaABg7Uxx9/nMR3BAAAAAAAAAAACwIEqcGR5z92DYItW7aY57p168ZZR2cExA4qJLX9rl275MqVK1KnTp04aYQ06FGqVClTx+DGjRspOj8AAAAAAAAAAAIuxVBq+OSTTzwO7GuKIU1NpLUNYtNaAY42ru1dX3Ol29BtHTp0KE57TZHkiS7//fffTV0FnWVwq8KE3qIzIVyfbcA524F+tgP97D9ibt706b69+bc1Ndh2bdt2vopztgP9bAf62Q70sx1s62dfna/W+gTgfVYECGbPni1ff/21KVzcoEEDt9e0iHHu3Lk9rhcaGups49pehYWFxbuOp/au6YtcObZz4cKFW57HyZMnvT7TQAss24ZztgP9bAf6Oe3TNH++3LcG6P2Rbde2beerOGc70M92oJ/tQD/bwbZ+9ub5asaP+G6uBXB7BXyAYM2aNTJgwAAJDw+XmTNnij/TGgveolFi/UOgsyIyZcokNuCc6edAxbXNtZ2WZciQwaf71u8H/sS2z7Nt56s4Z/o5UHFtc20HKq5tru1AZON1DdgsoAMEa9eulU6dOkmePHlk5cqVki9fPo938Lve8e9KCww72ri2Vwmtky1btkTPELjVDANfT7XSPwS2TfHinO1AP9uBfk77gmLV5/H2vv31b5xt17Zt56s4ZzvQz3agn+1AP9vBtn627XwBWwVskeKvvvpKOnToIDlz5jTBgfjy+2stgaioKI/TpjzVG/BUl8BBt6Hbcp0S5WjvWpfAlS7Xf3ALFSqU5HMEAAAAAAAAACC5ggI1ONCxY0fJnj27CQ4klMOsWrVq5nnDhg1xXlu/fr1bm+S0r1SpkgkAfPPNN3IzViHGY8eOmQLFlStX9mmKBQAAAAAAAACAfQIuQKDFiDU4oGl+NDjgeve/J+3btzeD8xMmTHBLA7R3715ZvHixlClTRqpWrepcXqtWLTMb4fPPPzdtHHTdd955xwQD2rZt65ZiqGXLlnLkyBH56KOPnMs1WPDmm2+anzUNEgAAAAAAAAAA3uQXt63PmTNHtm3bZn7ev3+/eZ47d65s2bLF/KwD+BoUOHDggDz99NNy5coVqV69uhnEj61w4cImKOBQsmRJGTRokIwcOdKs8/jjj5s0QUuWLDGvT5o0SYKC/j+OosGE9957T1q1aiWNGzc2g/8hISGyYsUKiYiIkBEjRkiRIkXc9jl8+HBzrC+//LJs3LjRzGjYunWr7Nq1Sxo2bGi2BQCA+TsVXlAuXrrs9TcjLDSLHIs44fX9njkXJbX7HBZfyBKW2yf7BQAAAAAgrfCLAIEGBxYsWOC2bPv27ebhoAECrQGgwQGld/97oul/XAMEqn///iZwMH36dJk1a5ZkzJjRBB2GDBki9913X5xt1KxZU9asWSOjR4+WpUuXyrVr16Rs2bLyxhtvmIBBbFoced26dSYIoYWTdd3w8HAZOnSo9OnTR9L5sEAjACBtqVqxmMzqG+L1/XaeGCW+sHHr7mSvGx0dbYLz+jeV4mkAAAAAAARogEAH7vVxKzVq1JDIyMhk7aNNmzbmkVgPPPCAxxkK8dEgwZQpU5J1bAAAAAAAAAAApLaAq0EAAAAAAAAAAABujQABAAAAAAAAAAAWIkAAAAAAAAAAAICFCBAAAAAAAAAAAGAhAgQAAAAAAAAAAFiIAAEAAAAAAAAAABYiQAAAAAAAAAAAgIUIEAAAAAAAAAAAYCECBAAAAAAAAAAAWIgAAQAAAAAAAAAAFiJAAAAAAAAAAACAhQgQAAAAAAAAAABgoQy+PgAAAOJTOLygXLx02etvUFhoFjkWcUJ84cy5KKnd57DX95slLLfX9wkAAAAAAHyLAAEAIM2qWrGYzOob4vX9dp4YJb6ycevuZK0XHR0tEREREh4eLsHBwal+XAAAAAAAIPCQYggAAAAAAAAAAAsRIAAAAAAAAAAAwEKJSjE0duzYVN/xwIEDU32bAAAAAAAAAAAgFQMEY8aMkXTp0klqIkAAAAAAAAAAAIAfFCnOnTu31KtXL8U7XLdunZw9ezbF2wEAAAAAAAAAAF4IEBQvXlymTZsmKfXYY48RIAAAAAAAAAAAwLYixTdv3vT2LgEAAAAAAAAAQHJmEEydOlXy5MkjqaF///7y559/psq2AAAAAAAAAADAbQwQPPXUU5Ja6tevn2rbAgAAAAAAAAAAfpJiCAAAAAAAAAAA+B4BAgAAAAAAAAAALJSoFEOebN26NVUOoFq1aqmyHQAAAAAAAAAA4IUAQZMmTSRdunSSErr+uXPnUrQNAAAAAAAAAADgxQBBoUKFzAD/qVOn5Pr16//bWIYMkiNHDjl//rxzWcaMGSVfvnzJ3Q0AAAAAAAAAAEhLNQj27dtnZhHcvHlTOnXqJNu3b5czZ87Ib7/9Zp71d10eExMjTZs2lb1793p8AAAAAAAAAAAAP5pB8Mknn8iMGTNk4sSJ0rFjR7fXgoKCpEyZMua1+++/X/r27Stly5aV9u3bp8YxA4CVCocXlIuXLnt9v2GhWeRYxAnxhTPnoqR2n8Ne32+WsNxe3ycAAAAAIG3QG54vXrwo165d8/WhAEmmWX/CwsIkU6ZMtzdAMGvWLJM6KHZwIDZ9fcyYMaY9AQIASL6qFYvJrL4hXn8LO0+MEl/ZuHV3staLjo6WiIgICQ8Pl+Dg4FQ/LgAAAABAYLp69apERkZK1qxZzSOlNVgBXwS4tO6vXr+JCRIkO8XQ77//Lvnz509UW2134MCB5O4KAAAAAAAAAG67S5cuSc6cOSVz5swEB+CXNLuPXsM6CyZR7VOyo4MHD95yqo2+ru2ItgEAAAAAAABI63dfp0+f3teHAaSIjt1r7eBEtU3uTrS2gEYhhg0blmC74cOHy4ULF6RSpUrJ3RUAAAAAAAAAAEhlya5B0K9fP/n2229NoeJt27ZJ165d5e6775bcuXPLX3/9Jb/++qvMnDlT9uzZYyIWL730UuoeOQAAAAAAAAAA8H6AoEaNGjJx4kQZMGCA7N69W3r16hWnjU5jyJgxo4wbN860BwAAAAAAAAAAfh4gUB07dpQqVarIe++9J+vWrZMzZ844X8ubN6/Ur1/fBA7uuuuu1DhWAAAAAAAAAPCJ5h2ek+PnIn327hfKmU2Wzf3QZ/tHYEpRgECVLl1apkyZYn7WmgRRUVESEhIiYWFhqXF8AAAAAAAAAOBzGhz4o/Uk3x3Aoj5is59++knWrFkjPXr0kGzZsqXqtjdv3ixNmzaVlStXpvlMOKl9rMkuUuyJBgUKFChAcAAAAAAAAAAAkKoBgrFjx8qFCxdS/V2tUKGCfP311+bZNimeQaCuX79u6hCcOHFC/vnnH2nXrl1qbBYAAAAAAAAAYDGtc6vjz8lZR+vjJvbG9wcffFBslKIZBPpGjx8/XkqVKiUNGjSQZ599Vl544QW3Nr179zaRl8OHD6f0WAEAAAAAAAAASTR69GiTlmfv3r3SsmVLkwWmRIkSMnDgQImOjna2++uvv8z4ro735smTxwyaT5s2zYwDK30uVqyYjBgxwrnO6dOnzbbLlSvntk/dT4sWLZy/x8TEmFT1WtNWt12yZEkzdhwZ6V7XQbf16quvytSpU6VixYqSK1cumT9/vvTr18+8rmPN2kYfR48ejXedLVu2mHMbMmSIPPzww1KoUCGzz2bNmsmPP/4YJ21PtmzZzLND48aNpXbt2vLdd99JvXr1JF++fPLAAw/I7Nmzk3z96XGMHDnSHJuee9myZeW1116TK1euuLW71fufpmYQ6EFpkeJVq1aZ38PDw+Xvv/82NQhc6Zs3d+5c+eKLL+TFF19M+REDAAAAAAAAAJLs6aeflrZt20qvXr1kx44dMmHCBDl37px8+OGHcvnyZWnUqJH8+eefZrC9ePHi8uWXX5oB9lOnTpmgQLp06aR69eqyadMm5za//fZbueOOOyQiIsLcJK4BhKtXr8r27dtlwIABznY9e/aUpUuXmjHiatWqmfajRo2Sn3/+WdauXSsZMvz/UPXixYulYMGCMmzYMMmaNasJZvTp00cmTZpkxpp1sF45nuNbRwfgtW6uBhfy5s1rxq4XLlwoDRs2NMd99913J/h+acYc3a8GMjTA8Mknn0jfvn3Ne1OzZs1Evec3btyQNm3amAw8L7/8stx3333yyy+/mKDNoUOHZN68eaZdYt7/NBUgWLBggRn01wOdNWuWidw89thj5sJyVb9+fQkKCjI5nAgQAAAAAAAAAIBv6EC1DjirunXrmgH/MWPGSP/+/c0d97///rt8/vnnZkzXcfO3ppSfPn26ubNdB+R1YHzQoEFm4F1T82iwQNvv2bPHDLprgGDnzp1mPccguo4Zf/rpp/LOO+9I586dncejd/TroLgW3HWdbaABhmXLlklISIhzWZEiRcxz+fLlnT+78rSO0lkLroP1jzzyiFSuXFk+/vhjc+4J0eCJbvOee+4xv+tMhA0bNphgRGIDBEuWLDHvkev7WqtWLcmRI4d0795dfvjhBzMzQcfbE/P+p5kUQxot0QvoP//5T4LFG7JkyWI67MCBA8ndFQAAAAAAAAAghVq1auXxd02jowECHbR2DE476IwDzefvuDFcB8Z1oH3r1q3md03LowPe+tAAgdJnDR7o3fJKbx5Pnz692Z9uy/F46KGHJDQ01Lkthzp16sQZ6L+V+NbR4MOjjz4qRYsWlZw5c5r0QwcPHpQ//vjjltvUrDmO4IDKnDmzCWocP3480cel567pgjRdkeu56+C/cpx7Yt//NDODQKdBaK4qRycnRN/4pLxpAAAAAAAAAIDUpWl2XOXOnds8nz9/3qSP93SHumOZtlFlypQxy/SueE3Rc+zYMRMc0LQ+gwcPNqnpNWigaYQ0KKA0bY4GFTzd+e+4Uz+h40zOuamvvvrKpMl/4oknTGogPV89Js108++//95ym9mzZ4+zLFOmTG51G25Fz10fGphI6NwT+/6nmQCB5m+Kr0Nj0zdMoysAAAAAAAAAAN84c+aMuUvdtSiu0mU6GK71AGLTIsSONg46i8CRw19vIteiujpj4OzZsya9kKbNefPNN53tdV2tMbBmzRpn0CChgXjNXJNUntbRVEA6c+CDDz5wW66D8RrQ8AY99/z585tCy57o7AKVlPc/TQQI9MCPHDmSqOCATtfQjkguLRyxbds2U8hh//79Jp+UVqRu3769x/aa/0rzR61YscJEZzR61Lx5c1OV29M0E62grReJ5p3SwhCaFkmnfGgl6fiOe/369aaIh1b+1otP0yxp0Q2Nlnmi74FWqtbImuaN0iIZmm9LH8m54AHbFQ4vKBcvXfb6fsNCs8ixiBPiC2fORUntPoe9vt8sYf+7mwAAAAAAAPg3HTDXAriuvyu921/HSDXfvubY1/oEDp999pkZ3Ne8/Q41atSQRYsWmfX1Z6VjsDq7QIvvXrt2zS1Hv+b9nzhxoglIaB3b5NA791VS7t7XcVjX4sfqm2++McWHUzJenRR67vq+6nGUK1cu3nZa/Dmx73+aCBBoQQY9OH1ocYv4fPTRR6bTHBdKcujAula11lRFeqHpz/HRas+NGzeWffv2mTdSp4/oIP7kyZNNPqfVq1dLcHCw2zo6vWTOnDkm4tWtWzdTFdrRGevWrTOD+bEDFtpOp4W0a9fOLNMK3BqEmD17tjRr1syt/a+//ioNGjQw74O20YiRVubWqtX62ttvv53s9wawVdWKxWRW36TloksNnSdGia9s3Lo7Wevpvz3676bmzYv97x8AAAAAALCHjuUGBQVJ1apVTU778ePHS+vWrc3Avo4bvP/++9KlSxcZOnSoFC9eXL788ktTi1ZT8rimv9HBf00lpLMIpk2b5rZcb8TWVD5ly5Z1LtcAhI6jPv/882ZcVQe7dcBf09Jv3LjR3AiuN2wn5K677jLPun0dj86YMaOpD+AIHMQ3OP/FF1/IK6+8YsaMdSxWb/rWWQ/e0rp1a1OguWXLlqbQsN5orsEYHavR+gR6k7qe21NPPZXo9z9NBAh69OhhLijHXflabTo2HXR/4403TGdp5yeXDu7rG1K4cGF59913zTbjM2nSJBMc0EH/4cOHO5frzxql0gu2X79+zuV6R78epwY8NCjguKC04/ShswK00rRDZGSkuaA0WKEfgIIFC5rluj/9AOi2NTChxTUcdJnOatComl6USjtZAwl6Qet+tCAHAAAAAAAAgLSpUM5sIov6+Hb/KTRv3jwZNmyYTJkyxdxEqNlNHKmA7rzzTnNztY6janaWCxcumLvsR40aJT179nTbjqae19c0w4zrTAFHgEDvho+dNUXHZStVqmTGYnX/ele8jq3qOrFv0PZE1+3fv78ZMJ81a5YZZN+zZ0+CafC1/sDJkyfNOrpfDVro8Y0bN068JUOGDGZcWMe4NVCgMyw0Hb+Odes4smPgPynvf6oeX3JX1EiHHqxeUE8//bS5s99R2KFJkyYmGqOFEzSSpCek1Z2T61bRIwfd19y5c03AQgf2XenvH374obkQXAME+rtjwN412qQD+Xoh6ywCx523SoMI2jlacMMRHFD6c9euXc25alTKMbNAUwtpFXCdQeEIDijdl+5T3ytNbUSAAAAAAAAAAEi7ls39UPxdsWLF3G6Gjk3v/NfU7omh6eBja9q0qbnB2hMNGOjd8fpISHzrK02P5Joi6Vbr6D51HFcfrmKnia9Ro0acbaxatcrjNuNbnhAdC9ZsMvpISGLef0/HmhJBKVm5d+/e8p///McMjmuxBB0410F6TeWj1Zc1+qERGZ024g0HDx406YF0iorWEXClv+tyjWrp1BWHLVu2mNeqVKkSZ3v16tUzz3o+ru2Vax6o5LbXqTy6b9f2AAAAAAAAAAB4Q7JnEDho7iRNlfP999+bKssavdBBb52uoQPgml7IWzRAoDQdkSe6XIsLa7tChQqZegUa2NBj9VQ927Edx3Zdf/Y07cWxzFN7T8ek+9QpMDrb4vr163EKZsSWlAIcKaWFoF2fbcA5+5eYmzd9tl9vfhZTA9e2HehnO9jWz7adr+Kc7UA/24F+tgP9bAfb+tlX50vNPPij69evJ/i61nvQR0AHCBwD3Xp3/u2qpJxYmudfZc2a1ePrYWFhbu0cz47lt2p/q3UcdQc8tY/vmHQdzZcVFRUl2bIlnEdM82XduHFDvOnMmTNiG845MP4Bvp37TahQelrGtW0H+tkOtvWzbeerOGc70M92oJ/tQD/bwbZ+9ub56thifDf8InV4SrODlMuVK1eCr2v93rT+vqdKgMDVX3/9Ze6udeTsR+rxZnVtjRLrHwKtLZFQJfBAwjn7Vz/fasbN7dyvv/37xrXtX9d2ctHP9HMg4rrmug5UXNtc24GKa5trO1DZdm3bdr5ASnzzzTcJvu4oQJyWpfoIm1aG3rlzp6lB4G2Ou/q1FoInse/+9zRDIKH2sdfJkSOHW/tLly7F2z6+Y9J1tFiGFlZOi1Ot9A+BbVO8OGf/EJQunc/266+fCa5tO9DPdrCtn207X8U524F+tgP9bAf62Q629bNt5wskR8WKFcXf3ZYESFqo2BccNQAOHTrk8XXHckc7rZWgUZyjR496TN0Tu318dQYSqk+Q0DHpPnXfWofAV3dDAwAAAAAAAADslLYrJCSRDsbnz59fduzYYQoQu9LfdbkOxmuBYodq1aqZ17Zv3x5ne1rQWD388MNu7dWGDRvibe9oc6v227ZtM/t2bQ8AAAAAAAAAgDcEVIBAU/V06NDBFPx9++233V7T33V5p06d3JY7fh81apRbdfavv/5atmzZInXr1pXChQs7l7do0cKkDZo5c6acOHHCuVx//uCDDyRnzpzSpEkT5/JSpUqZAMPmzZvNNh10X7pPR1omAAAAAAAAAAC8yS/y2syZM8fcba/2799vnufOnWsG8FXVqlWdg+x9+vSR1atXy8SJE2Xv3r1SoUIF2bNnj7mD//7775cePXq4bbtmzZpmXd1HrVq1pEGDBnL69GlZunSpZM+eXcaNG+fWPlu2bCbY0K1bN9NeAwZK258/f14++ugjCQ0NdVtnwoQJ8uijj0r79u1Ne01rtHbtWvnvf/8rXbt2lcqVK9/Gdw8AAAAAAAAAAC8ECLT+QGrXINDgwIIFC9yWaUog17RAjgCB1hVYtWqVjBkzRlauXGnu3Neq67169ZKBAwfKHXfcEWf7GkwoW7asfPzxxzJjxgyzDZ0F8Nprr0mxYsXitH/yySfNTAEd+J8/f76ZuaCBiAEDBkjt2rXjtL/77rtN+qGRI0eawMA///xj0iGNHz9eunTpkkrvEgAAAAAAAAAAPgwQrFmzJrU3KdOnTzePxMqaNauMHj3aPBIjKChIunfvbh6JVb9+ffNILE01pAEIAAAAAAAAAADSAr9IMQQAAAAAAAAAvvR0y6flr+N/+mz/uQvlkU+WfCK2+umnn8zN6ZpCXtPAp6bNmzdL06ZNTUaaGjVqiE0IEAAAAAAAAADALWhwoNGZBj57n1bLWrGZBgjGjh0rTz31VKoHCCpUqCBff/21lClTRmyTrACBFgo+cOCA3HnnnfLQQw/dskPmzZsnx48fNzUAAAAAAAAAAABIDK13e/369WStkzFjxkS1DwsLkwcffNDKDglKSuNTp07JY489JtWrV5fOnTtL27ZtTVRl8ODB8u+//8a73ieffGKiOwAAAAAAAAAA79JarXqT9969e6Vly5ZSoEABKVGihLmhOzo62tnur7/+khdeeMHUU82TJ48ZNJ82bZoZcFf6XKxYMRkxYoRzndOnT5ttlytXzm2fup8WLVo4f4+JiZEpU6ZIlSpVzLZLliwpvXv3lsjISLf1dFuvvvqqTJ06VSpWrCi5cuWS+fPnS79+/Zx3+2sbfRw9ejTedbZs2WLObciQIfLwww9LoUKFzD6bNWsmP/74Y5wUQ9myZTPPDo0bN5batWvLd999J/Xq1ZN8+fLJAw88ILNnzxYrZxDom9m8eXP5/fffnReEunr1qrz//vvyzTffyKeffipFixa9XccKAAAAAAAAAEimp59+2tz03atXL9mxY4dMmDBBzp07Jx9++KFcvnxZGjVqJH/++acZbC9evLh8+eWXZoBdbxzXoEC6dOnMzeObNm1ybvPbb7+VO+64QyIiIuTw4cMmgKBjxtu3b5cBAwY42/Xs2VOWLl0qL774olSrVs20HzVqlPz888+ydu1ayZDh/4eqFy9eLAULFpRhw4ZJ1qxZTTCjT58+MmnSJJk7d64ZrFeO5/jWuXLlily8eNEEF/LmzStRUVGycOFCadiwoTnuu+++O8H368SJE2a/GsjQAIPeCN+3b1/z3tSsWdOuAMGsWbNMWqHs2bOb2QCPPPKICRosWrRI3n77bfntt99MVGXFihXmzQfgPSVLFpdz5/5O5toa8EuXrDVz5swuf/xxSHzhzLkoqd3nsNf3myUst9f3CQAAAAAAkBratGljBvxV3bp1zYD/mDFjpH///uaOe705/PPPP5f69eubNnrn/D///CPTp083Mwt0QF4HxgcNGmQG3jU1jwYLtP2ePXvMoLsGCHbu3GnWcwyiazBCby5/5513TGYaB72jX4MSWhzYdbaBBhiWLVsmISEhzmVFihQxz+XLl3f+7MrTOkpnLTjcuHHDjGtXrlxZPv74Y3PuCdHgiW7znnvuMb/rTIQNGzaYYIR1AQLtJL1g9GJ49NFHncs14vP4449L+/bt5ZdffnEGCUqXLn27jhlALEVLF5Z2bbxfYX3bZ/+bxuULG7fuTtZ6GtjUCHV4eLgEBwen+nEBAAAAAACkVa1atYrzuw6SaxodDRDkyJHDGRxw0BkHmuJHB/k1PY8OjOtA+9atW006ek3Lo3fYa4oeDRA888wz5lmDB/fdd5/ZhhYATp8+vdmfaz0BrW8bGhpqtuUaIKhTp06cgf5biW8dHdfWIIHe4O6azkgDGbei40eO4IDKnDmzCWpovV3rahD897//NbmhXIMDDhqx+eqrr8zUkDNnzkjTpk1NewAAAAAAAABA2qBpdlzlzv2/TAnnz5+Xv//+2y1lj4NjmbZRWpNWl+nMgSNHjsixY8ekVq1aJnCgwQJNT6/POlasQQGlaYs0qKDjyFofwPVx6dIlc6d+QseZnHNTOmbdsWNHKVy4sLnxfd26dSZV/r333ptgTV0HzaYTW6ZMmdzqNlgzg0BzUCWUOihLlixm+slTTz1l3mSdVbB8+XIpW7Zsah0rAAAAAAAAACCZ9OZunSXgWpRY6TIdDNd6ALFpEWJHGwcNBjhy+GvBYy1qrDMGzp49a9IL/fDDD/Lmm2862+u6WmNgzZo1zqBBQgPxmskmqTyto6mAtGbuBx984LZcgyFapwBJmEGgU0RiR3Ji03QdmktK8zjpxaBTTjxdVAAAAAAAAAAA79IBc0+/693+WnxYx381x76rzz77zAzua95+hxo1apgMMrq+/uy4g19nF4wePVquXbvmlqNfx4s1tZAGJCpWrBjnoYP4t6J37quk3L2vdRBcix8rvbldiw8jiTMItKbAtm3bzHQQTTWUUEfNmzfPTN3QiJAGCbSKNQAAAAAAAADAd3SwPygoSKpWrWpqCowfP15at25tBvY13/77778vXbp0kaFDh0rx4sXlyy+/lE8++cTUoXVNP6SD/5pKSGcRTJs2zW253q2vqYtcM8toAKJdu3by/PPPS7du3UywQceRNZf/xo0bTX3b2rVrJ3jsd911l3nW7Wux5YwZM5r6AI7AgScamPjiiy/klVdeMbVzf/31V5kwYYKZ9YAkBgj0otEAgUaFevTokWBb7Zy5c+eaghSrVq1K1pQQAAAAAAAAAEgrchfKI6tlrU/3n1J6Y/ewYcNM0V7NBtO5c2dnKqA777xTVq9eLcOHDzeFiy9cuGDu7B81apT07NnTbTtaS0Bf0xoErjMFHAECnY0Qe0xYAwmVKlWSOXPmmP3rnf0FCxY06ySU2t5B1+3fv78JWMyaNUtiYmJkz5495ljiozexnzx50qyj+9WghR7fuHHjkvHuWR4gaNiwoYmuzJgxw0R6POWKcttwhgzy8ccfm7ZLliwhSAAAAAAAAADAb32y5BPxd8WKFTNjtfHRO/+nTp2aqG3t3r07zrKmTZtKZGSkx/YaMNDZCfpISHzrq1dffdU8EruO7nPw4MHm4UqLKruqUaNGnG3oje+exLc84AMEGqFZtmyZ+VkrPIeEhNxyHQ0iaESmUaNGcuXKlZQdKQAAAAAAAAAA8H6AwFNkJTE0p1WrVq2SvB4AAAAAAAAAALh9gm7jtgEAAAAAAAAAPqYpdjSFTmKywsAuSZpBENv58+dl+/btptDDxYsXJSwszFSA1irUOXPmTL2jBAAAAAAAAAAAvg8Q7Nq1y1Sy/uabb+JtU6dOHRk4cKA89NBDKTk+AAAAAAAAAACQFlIMvfvuu/LYY4+Z4MDNmzfNIzQ0VPLnz2+eHcs2bNhgihOPHz/+dhw3AAAAAAAAAADwVoDg/ffflzfffFNu3LghDz74oMyaNUv++OMPOXr0qPzyyy/mWX/X5ZpmSNu99dZbMmPGjJQcIwAAAAAAAAAA8FWAQOsMDB8+XNKlS2eev/rqK2nRokWcWgP6uy5fs2aNvP7662Y2wRtvvGHWBwAAAAAAAAAAfhYg0FkB0dHR0qVLF+nTp0+i1nnppZdMe11P1wcAAAAAAAAAAH4WIFi/fr0EBQXJgAEDkrQDba+zDnR9AAAAAAAAAACQNmRIbEOtL1CiRAnJkydPknaQN29es56uDwAAAAAAAAD+qEOHDnL27Fmf7T9Xrlwyd+7c27Z9Hb+tUKGCTJ06Vdq3b2+W9ejRQ7Zs2SL79u1LcN3NmzdL06ZNZeXKlVKjRg3xV+XKlZPq1avL9OnTxRaJDhBERUVJ8eLFk7WTsLAwOXbsWLLWBQAAAAAAAABf0+BAy5Ytfbb/JUuWeH2fr7zyinTv3t3r+0UaDBDkyJFDTp8+nayd6HrZs2dP1roAAAAAAAAAAO8rVqyYT972K1euSObMmX2yb9skugbB3XffLSdPnrzldJLY9u7da9bT9QEAAAAAAAAA3rd8+XKpUqWKSSFfqVIlmTdvnkkhpGl14uPp9ePHj5sURAUKFJCiRYvKCy+8IBcvXvS4/ldffSWPPfaYFCxY0DyeeOIJ2b9/v1ubxo0bS+3atWXDhg1Sp04dk7J+3LhxiT6vxOxDzZw5UypXrmzOv2TJkmZmxOlk3hBvZYCgYcOGcvPmTRk6dKjExMQkap0bN27IkCFDTJFi7SQAAAAAAAAAgHdt2rRJnnnmGSlUqJB8/PHHMnjwYHnvvfdM7YCkuHz5sjz++OOyfft2eeutt+TDDz+U69evm1REsWm9hCeffNLs8z//+Y/MmDFD/v77bzNOHDsdfUREhPTp08cc4+eff26CBomR2H2MHDnSHKMGCObPn2/GuNeuXWvaxRfcsEWiUwx16tRJJk6caIpSPP300zJlyhSTdig+2hE9e/aUrVu3Sr58+aRjx46pdcwAAAAAAAAAgETSwXxNF7Rw4UJJnz69WVa1alW57777zB37ibVgwQI5dOiQmY1Qq1Yts6x+/frSokULOXHihFsg4dVXXzU1Gz744APnci1gXLFiRZk0aZJMmDDBufzcuXPm2HRmQ2Ildh86Tj158mTTToMiDqVKlZImTZqYgMmLL74otkr0DILg4GATEcqYMaOsWbPGvMkDBgwwlal/+eUXOXLkiHnW33W5vq7TOzJlymQ6SNcHAAAAAAAAAHiPZnn54YcfpGnTps7ggNIUQQ899FCStqU3g+fMmdMZHHDQtD6udu3aJRcuXDB39+sMA8cjJCTE7FO340pvME9KcCAp+9B2WtOgTZs2butXr17dzDzQG+JtlugZBKpatWqyaNEiee655+Svv/4y0zb04YmmI8qdO7cJDuibDQAAAAAAAADwLr07/9q1a5IrV644r2k+/qNHjyZ6W+fPn/c44yD2sj///NM86+C9JzpunND6iZHYfegMAkcQIrZ8+fKZc7JZkgIEqmbNmrJz504zm2Dx4sXy66+/xmlTpkwZEzXSQEK2bNlS61gBAAAAAAAAAEmgd/xrVpizZ8/GO8ieWJpy/ueff46z/MyZM3HaKU1ZX6FChTjtM2RwH5bWGrZJldh9ZM+e3Tx7Kkh8+vRpueeee8RmSQ4QKB3079+/v3lERkaa/FJRUVFm+oZOTXG86QAAAAAAAAAA39G0Qg888IBJDf/666870wzpmK7eCJ6Uu/c1w8zSpUvl22+/dUszpIWFXWkx4LCwMPn9999N4eHbIbH7ePDBByVz5szmGBs2bOhc/t1338nx48elW7duYrNkBQhiBwuYJQAAAAAAAAAAadOQIUOkWbNmJh2PZn35559/ZOzYsSbFUFBQosvUSrt27WTatGnSuXNnE2woWLCgSUmvg/SuQkNDTWHk3r17mxQ+jRo1MjeV64wFDUroTeb6Wkokdh+6TJ/ffvttc4O71mKIiIiQESNGSNGiRaVTp05isyQFCLQQsUaWNDJTrly5W7bX6SZaKEKLPRQpUiQlxwkAAAAAAAAAPqM5/JcsWeLT/SeXpo3/6KOPZPTo0dKhQwcJDw+Xvn37ypo1a8xd9ImVJUsWWb58uQwcOFAGDx5sUhc1adLEBBvat2/v1vbpp58248KTJk2SF154wRQK1tkKWoy4efPmyT6X5Oxj6NChpiaB1tOdN2+eGd+uX7++vPHGG+ZnmyU6QKCFLFq0aCHHjh2TBQsWJCpAcPLkSWnbtq2ULFlStm/fnqRoFAAAAAAAAACkFXPnzhV/pgPmroPmFy9elJEjRzrT7ugN3ppO3tX06dPjbEeDC/Pnz4+zPPa6qnbt2uaRkFWrViXpPJKzD/X888+bR0L27dsntkn0iL12lM4g0IhQgwYNErWOttOpK3/88UeKOxoAAAAAAAAAkHRXr16VPn36yLJly2TLli3y2WefmWCBZn/p3r07b6nFEj2DQItYaDVpnaqRFNpeL7wVK1aY/E4AAAAAAAAAAO/RzC6ap1/TAp09e1buuOMOZ+HismXLprmuiImJMY/46Di1o9gyvBQg+OmnnyRr1qzy0EMPJWkHeqFpEeMff/wxOccHAAAAAAAAAEiBDBky+FWKJL3pXNPcx0fTHNmYDsinAYIzZ85I8eLFk7wDjeZooYjDhw8neV0AAAAAAAAAgF0GDRqUYL2ATJkyefV4AlmiAwQ3btwwVamTtZMMGeT69evJWhdIqjoP15HDR5MXkLoZEyPpkllMu1iRYvLNd9+IL5w6flo+eesPr+/3zuAQr+8TAAAAAAAAgU0LJusDaShAkCNHDjl58mSydnLq1CmzPuANma5nlJcy9PL6m736+lrxlV/2/pqs9aKjoyUiIsJMywoODk714wIAAAAAAACQdiX6Vum7775b/vzzT/ntt9+StINff/3VpCe66667knN8AAAAAAAAAADAlwGCOnXqyM2bN2XChAlJ2sH48eNNHYK6desm5/gAAAAAAAAAAIAvAwQdO3aUrFmzyueff57oIIEGBxYvXixhYWFmfQAAAAAAAAAAkDYkOkCgg/zjxo0zswhGjRoljRs3lpUrV0pkZKRbO/19xYoV0qhRI3nrrbfM7IExY8aY9QEAAAAAAAAAgJ8FCFSbNm1McCAoKEi2bdsmnTp1kuLFi0vRokXl3nvvNc/6+zPPPGNe13ZvvvmmtG3bVrxJgxgapGjSpImUKVNG8ufPL5UqVZK+ffvKkSNH4rS/ePGiDBkyxJxDnjx5pFy5cvLaa69JVFSUx+3HxMTI+++/Lw8//LDky5dPSpQoIV26dPG4bYf169eboEmhQoVMQVg9tm+//TZVzxsAAAAAAAAAgMTKIEnUs2dPeeihh2T06NGyYcMGs+zChQvm4UprDgwaNEgefPBB8bZXX31Vpk6dagbvdaZDaGio/Pzzz/Lxxx+blEdfffWVlC1b1rS9fPmyabNv3z5zzE888YTs3btXJk+eLFu3bpXVq1dLcHCw2/Y10DBnzhxTuLlbt25y6tQpWbZsmXk/1q1bZwIGrhYuXGja5cqVS9q1a2eWLV26VJo3by6zZ8+WZs2aefHdAQAAAAAAAJBU7Tu0lTPnTvvsjcubM5/Mm/vpbdv+0aNHpUKFCmZctX379mZZjx49ZMuWLWbsNCGbN2+Wpk2bmowzNWrUuG3HiDQQIFB6N74OtJ87d87MFDh58qRcunTJDMQXKFBAqlatKjlz5hRfOHPmjEyfPt3cpa8Xr9ZNcNCLe+jQoeZZH2rSpEnmAtdB/+HDhzvb6s8TJ06UadOmSb9+/ZzLN23aZIIDOntAgwKZMmUyy1u3bm0eAwYMkCVLlrilXHrllVfM+6EzBgoWLGiW6/5q1qxptq2BCX3vAAAAAAAAAKRNGhyo2qaIz/a/7bOjXt+njmt2797d6/tFGk0xFJsOemuqnOeff15efvll86y/+yo4oI4dO2ZSAFWpUsUtOKAaNmxons+ePetMRTR37lwJCQkxA/uu9HddrsEAV47fNdDgCA6oRx55RKpXr25mEURERDiXaxBBZ1foe+MIDij9uWvXribI8sUXX6TqewAAAAAAAAAAKVWsWDEzq8Dbrly54vV92ipFAYK0SNP76MD99u3bTW0BV2vWrDHPtWrVMs8HDx406YEqV64sWbJkcWurv+tyrStw/Phx53KdlaCvaQAitnr16plnTU3k2l7pLIHEtAcAAAAAAACA1LZ8+XIzpqk1WDVDzLx580wKIa3HGh9Pr+tYqaYg0kwyWpP2hRdeiDMO66Cp3h977DFzs7Q+NL37/v373dpo+vfatWubG6/r1KkjefPmlXHjxqXSWeO2pBhKy3LkyCHDhg0zdQi0VoIWBnbUIND0QM8995y5m98RIFBaWNkTXa7FhbWdFhfWegWnT5829QvSp0/vsb3rdl1/jl2XwHWZa/uEREdHi7dcvXrV7dmf6MwQX+3Xm31kez8nF+dsB/rZDvRz4KOP7UA/24F+tgP9bAf6OfD5qo9j1wBF6tJx0WeeecbcsKxjp//8848ZhNfxznTp0iV6O9r+8ccfNxlT3nrrLTNmumjRIpOKKDbN3PLiiy+atOx9+vSRa9eumZTuGjDQmgWFCxd2ttWMLNqmf//+Zow19s3cuH0CLkCgNGqlEazevXvLrFmznMu1NoJGqTJk+N9pOyJbsVMROYSFhbm1czw7lt+q/a3WcdQdiC/CFpvWerhx44Z4u6aDv7l+7brP9uuaXsqf+GM/pxTnbAf62Q70c+Cjj+1AP9uBfrYD/WwH+jnwebOP9Ubc+G7gRerQwXxNF7Rw4ULnjc86VnrfffeZO/YTa8GCBXLo0CEzG8GRpaV+/frSokULOXHihFsgQW/gbtmypXzwwQfO5VrAuGLFiqYu7IQJE5zLNQ27HpvObIB3BWSAYOzYsTJ+/HgZMmSItGnTxgQAtBCx/q41ErSOgM4s8Dca9PAWjRLrHwL9B8K11oI/yJAxg8/2q8Wx/Yk/93Nycc70c6Di2ubaDkRc11zXgYprm2s7UHFtc20HKtuubdvO1wZ6w/EPP/wgPXv2dMuKomONmoHl6NHEFz/WVOlaf9YRHHDQm7K/+eYb5++7du0yswyefPJJuX79/2/m1Zqvus/YKdfz5ctHcMBHAi5AsHHjRhk9erS54F966SXnco2IffrppyYqptErDRA47urXi9WT2Hf/e5ohkFD72Oto+iNXly5ditM+rU210j8E/jbFKynTolJ7v/72XvlzP6cU52wH+tkO9HPgo4/tQD/bgX62A/1sB/o58NnYx4FK787X9D65cuWK85rWI0hKgOD8+fMeZxzEXvbnn3+aZw0QeJI7d+4E14f3BFyA4Ouvv3ZOV4lNL7RSpUrJ3r17JSoqylkDQKfFeOJY7minua80mqUfGo28xa5DELu94+effvrJ1BmIHSBIqD4BAAAAAAAAAKSU3vGfMWNGOXv2bJzXHAP5iaXjm1rr9VYpqRzjoFpzoEKFCnHaO1LA+/qGX4gEBdqb4Cig4umCd0TMgoKCzIdCB+bz588vO3bsMHmxXOnvurxIkSKm2IZDtWrVzGvbt2+Ps20taKwefvhht/ZKq3DH197RBgAAAAAAAABSk97k/MADD8jKlSvd6ptqzYCdO3cmaVs6jqnjq99++63b8s8//9zt98qVK5usKb///rupORD7Ua5cuRSeFVJLwAUIqlSpYp6nTZsWJ3WQFizWC1/zXGXOnNlEpjp06GBmE7z99ttubfV3Xd6pUye35Y7fR40a5VbNXWcubNmyRerWretWgVsLdOiHYebMmW6FOvRnLdChETytiwAAAAAAAAAAt4PWZj18+LBJ+bNmzRpZsmSJKSCsKYb0ZurEateunSko3blzZ/n4449l3bp10q1bNxMIcBUaGmoKI0+fPl169OhhghM6dqr7HTRokLz33nu34SyRHAGXYqh58+byn//8R7777jtT2OKxxx4zRYr37NkjmzZtkjvuuMMM7jv06dNHVq9ebaa7aOohnfKibfWO//vvv99cwK5q1qwpHTt2NIWOtRhHgwYN5PTp07J06VLJnj27jBs3zq19tmzZTLBBPyjaXgMGSttrzq6PPvrIfGAAAAAAAAAApF15c+aTbZ8d9en+k0vHNHUcUmu36g3T4eHh0rdvXxMsOH78eKK3oynYly9fLgMHDpTBgwebLC168/PYsWOlffv2bm2ffvppk5ll0qRJ8sILL8iVK1dMCngds9UxXPh5gKBp06ZJmsai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", + "image/png": "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mWBQUs2fP9/MQNDZCE4a94YNG0qrVq3k+PHjsQIJmuGldevW8v7777uWawHjypUry8SJE2X8+PGu5VrrVo9Na9/Cu4IkwGzcuNFMldECwDpNRosBaxFiveA//fRTyZw5s7k43e/q14iXJ3Hv/vc0QyCx9jdaRws7x20PAAAAAAAAAGlFU5Z///330rx581hp0zVbiaZoTw6tpap1aZ3ggEOztrjbtWuXGXN99NFH5dq1a66HjtPqPnU77goUKEBwwEcCbgbB119/7YpGxaXRsDJlypicV5GRka4aABr18sRZ7rTTqS16sR45csR8sOLWIYjb3vn5xx9/NHUGtD6Cu8TqEwAAAAAAAABAaund+ZreJ0+ePPFey5cvnxnrTKqzZ896nHEQd9lff/1lnjVA4EnevHkTXR/eE3ABAqfC+unTpxP8QAQFBZmZBDowX7BgQdmxY4eZ9uKe20p/1+XFihUzubQcNWrUkEWLFsn27dvNz+60oLG6//77Y7X//PPPZf369VK1alWP7eNuBwAAAAAAAADSgt7xr2OhnsZLnYH8pNIboH/++ecbFrV2bpTWmgOVKlWK196pEevr9NkIwBRD1apVM89Tp06NlzpICxZrLiydxpI1a1Zz4XXq1MnMJnjrrbditdXfdXmXLl1iLXd+HzVqlCsY4cxc0Ire9evXj1VgQ/NvaQqhGTNmxMrDpT9r/i39gGpdBAAAAAAAAABIa5oF5Z577pEVK1aYrCju45M7d+5M1rb0Rme9Afubb76JtVxvkHZ33333mTHR33//3dQciPuoUKFCKs8KaSXgZhC0bNlSPvzwQ/n2229N3iqtiq1Fivfs2WOqdd9yyy1mcN+h1bFXrVplolmaekgjWtpW7/i/++67pVevXrG2X7t2bencubMpdKy5tho1aiQnT56UJUuWSM6cOU31b3c5cuQwwYYePXqY9howUNpep+R89NFHEhoa6qV3BwAAAAAAAIBthg4dKi1atDApf5566in5999/ZezYsSbFkGZbSaoOHTqYG7O7du0qr776qqn/unDhQhMIcKfjnVoYuV+/fmYMtEmTJmbsVGcsaFBC6x/oa/C9TIEYEdPBd71Q9VmjV3qnv17s7dq1k+eff17KlSvnaq9phVauXCljxowxUbTNmzebnFd9+vSRwYMHm4BCXBpMKF++vMyaNUumT59utqGz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", "text/plain": [ "
" ] diff --git a/notebooks/tutorials/3_dynamic_characterization.ipynb b/notebooks/tutorials/3_dynamic_characterization.ipynb index e983d080..34327a1b 100644 --- a/notebooks/tutorials/3_dynamic_characterization.ipynb +++ b/notebooks/tutorials/3_dynamic_characterization.ipynb @@ -18,10 +18,10 @@ "id": "5d3622ff", "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T07:25:46.074356Z", - "iopub.status.busy": "2026-08-04T07:25:46.074157Z", - "iopub.status.idle": "2026-08-04T07:25:47.680720Z", - "shell.execute_reply": "2026-08-04T07:25:47.680287Z" + "iopub.execute_input": "2026-08-21T10:20:58.631769Z", + "iopub.status.busy": "2026-08-21T10:20:58.631399Z", + "iopub.status.idle": "2026-08-21T10:20:59.869756Z", + "shell.execute_reply": "2026-08-21T10:20:59.869258Z" } }, "outputs": [ @@ -29,7 +29,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "\u001b[2m09:25:47+0200\u001b[0m [\u001b[33m\u001b[1mwarning \u001b[0m] \u001b[1mRemoving project from project timex_example_dynamic_characterization list, but not deleting data; if you switch to this project again you will have the same data again. To delete data permanently, pass `(..., delete_dir=True)`.\u001b[0m\n" + "\u001b[2m12:20:59+0200\u001b[0m [\u001b[33m\u001b[1mwarning \u001b[0m] \u001b[1mRemoving project from project timex_example_dynamic_characterization list, but not deleting data; if you switch to this project again you will have the same data again. To delete data permanently, pass `(..., delete_dir=True)`.\u001b[0m\n" ] }, { @@ -45,21 +45,21 @@ "output_type": "stream", "text": [ "\r", - "100%|██████████| 1/1 [00:00<00:00, 1726.76it/s]" + "100%|██████████| 1/1 [00:00<00:00, 2807.43it/s]" ] }, { "name": "stdout", "output_type": "stream", "text": [ - "\u001b[2m09:25:47+0200\u001b[0m [\u001b[32m\u001b[1minfo \u001b[0m] \u001b[1mVacuuming database \u001b[0m\n" + "\u001b[2m12:20:59+0200\u001b[0m [\u001b[32m\u001b[1minfo \u001b[0m] \u001b[1mVacuuming database \u001b[0m\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ - "\u001b[2m09:25:47+0200\u001b[0m [\u001b[33m\u001b[1mwarning \u001b[0m] \u001b[1mNot able to determine geocollections for all datasets. This database is not ready for regionalization.\u001b[0m\n" + "\u001b[2m12:20:59+0200\u001b[0m [\u001b[33m\u001b[1mwarning \u001b[0m] \u001b[1mNot able to determine geocollections for all datasets. This database is not ready for regionalization.\u001b[0m\n" ] }, { @@ -82,14 +82,14 @@ "output_type": "stream", "text": [ "\r", - "100%|██████████| 1/1 [00:00<00:00, 21732.15it/s]" + "100%|██████████| 1/1 [00:00<00:00, 22795.13it/s]" ] }, { "name": "stdout", "output_type": "stream", "text": [ - "\u001b[2m09:25:47+0200\u001b[0m [\u001b[32m\u001b[1minfo \u001b[0m] \u001b[1mVacuuming database \u001b[0m\n" + "\u001b[2m12:20:59+0200\u001b[0m [\u001b[32m\u001b[1minfo \u001b[0m] \u001b[1mVacuuming database \u001b[0m\n" ] }, { @@ -173,10 +173,10 @@ "id": "8d9405d9", "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T07:25:47.682211Z", - "iopub.status.busy": "2026-08-04T07:25:47.682115Z", - "iopub.status.idle": "2026-08-04T07:25:47.683912Z", - "shell.execute_reply": "2026-08-04T07:25:47.683616Z" + "iopub.execute_input": "2026-08-21T10:20:59.871200Z", + "iopub.status.busy": "2026-08-21T10:20:59.871109Z", + "iopub.status.idle": "2026-08-21T10:20:59.872822Z", + "shell.execute_reply": "2026-08-21T10:20:59.872506Z" } }, "outputs": [], @@ -191,10 +191,10 @@ "id": "71bba776", "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T07:25:47.685048Z", - "iopub.status.busy": "2026-08-04T07:25:47.684983Z", - "iopub.status.idle": "2026-08-04T07:25:47.990358Z", - "shell.execute_reply": "2026-08-04T07:25:47.989987Z" + "iopub.execute_input": "2026-08-21T10:20:59.873983Z", + "iopub.status.busy": "2026-08-21T10:20:59.873915Z", + "iopub.status.idle": "2026-08-21T10:20:59.884638Z", + "shell.execute_reply": "2026-08-21T10:20:59.884333Z" } }, "outputs": [ @@ -202,42 +202,42 @@ "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-08-04 09:25:47.974\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m142\u001b[0m - \u001b[1mInitializing TimexLCA object...\u001b[0m\n" + "\u001b[32m2026-08-21 12:20:59.874\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m174\u001b[0m - \u001b[1mInitializing TimexLCA object...\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-08-04 09:25:47.975\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m149\u001b[0m - \u001b[1mNo database_dates provided. Treating the databases containing the functional unit as dynamic. No remapping of inventories to time explicit databases will be done.\u001b[0m\n" + "\u001b[32m2026-08-21 12:20:59.874\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m_resolve_database_dates\u001b[0m:\u001b[36m312\u001b[0m - \u001b[1mNo database_dates provided, and no database in this project carries `representative_time` metadata. Treating the databases containing the functional unit as dynamic. No remapping of inventories to time explicit databases will be done.\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-08-04 09:25:47.976\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m163\u001b[0m - \u001b[1mCalculating base LCA...\u001b[0m\n" + "\u001b[32m2026-08-21 12:20:59.875\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m194\u001b[0m - \u001b[1mCalculating base LCA...\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-08-04 09:25:47.987\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m180\u001b[0m - \u001b[1mCollecting node infos...\u001b[0m\n" + "\u001b[32m2026-08-21 12:20:59.882\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m211\u001b[0m - \u001b[1mCollecting node infos...\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-08-04 09:25:47.988\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m192\u001b[0m - \u001b[1mLoading node metadata from 1 database(s)...\u001b[0m\n" + "\u001b[32m2026-08-21 12:20:59.882\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m223\u001b[0m - \u001b[1mLoading node metadata from 1 database(s)...\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-08-04 09:25:47.988\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m229\u001b[0m - \u001b[1mTimexLCA initialized.\u001b[0m\n" + "\u001b[32m2026-08-21 12:20:59.883\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m260\u001b[0m - \u001b[1mTimexLCA initialized.\u001b[0m\n" ] } ], @@ -253,10 +253,10 @@ "id": "c40754e8", "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T07:25:47.991593Z", - "iopub.status.busy": "2026-08-04T07:25:47.991413Z", - "iopub.status.idle": "2026-08-04T07:25:48.040452Z", - "shell.execute_reply": "2026-08-04T07:25:48.040043Z" + "iopub.execute_input": "2026-08-21T10:20:59.885983Z", + "iopub.status.busy": "2026-08-21T10:20:59.885894Z", + "iopub.status.idle": "2026-08-21T10:20:59.903919Z", + "shell.execute_reply": "2026-08-21T10:20:59.903494Z" } }, "outputs": [ @@ -264,35 +264,35 @@ "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-08-04 09:25:47.992\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mbuild_timeline\u001b[0m:\u001b[36m358\u001b[0m - \u001b[1mNo edge filter function provided. Skipping all edges in background databases.\u001b[0m\n" + "\u001b[32m2026-08-21 12:20:59.886\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mbuild_timeline\u001b[0m:\u001b[36m453\u001b[0m - \u001b[1mNo edge filter function provided. Skipping all edges in background databases.\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-08-04 09:25:47.992\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mbuild_timeline\u001b[0m:\u001b[36m379\u001b[0m - \u001b[1mCreating activity time mapping...\u001b[0m\n" + "\u001b[32m2026-08-21 12:20:59.886\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mbuild_timeline\u001b[0m:\u001b[36m474\u001b[0m - \u001b[1mCreating activity time mapping...\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-08-04 09:25:47.992\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timeline_builder\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m112\u001b[0m - \u001b[1mTraversing supply chain graph...\u001b[0m\n" + "\u001b[32m2026-08-21 12:20:59.886\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timeline_builder\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m112\u001b[0m - \u001b[1mTraversing supply chain graph...\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-08-04 09:25:47.994\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timeline_builder\u001b[0m:\u001b[36mbuild_timeline\u001b[0m:\u001b[36m186\u001b[0m - \u001b[1mBuilding timeline...\u001b[0m\n" + "\u001b[32m2026-08-21 12:20:59.888\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timeline_builder\u001b[0m:\u001b[36mbuild_timeline\u001b[0m:\u001b[36m183\u001b[0m - \u001b[1mBuilding timeline...\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-08-04 09:25:48.033\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timeline_builder\u001b[0m:\u001b[36madd_column_temporal_market_shares_to_timeline\u001b[0m:\u001b[36m587\u001b[0m - \u001b[1mNo time-explicit databases are provided. Mapping to time-explicit databases is not possible.\u001b[0m\n" + "\u001b[32m2026-08-21 12:20:59.898\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timeline_builder\u001b[0m:\u001b[36madd_column_temporal_market_shares_to_timeline\u001b[0m:\u001b[36m627\u001b[0m - \u001b[1mNo time-explicit databases are provided. Mapping to time-explicit databases is not possible.\u001b[0m\n" ] }, { @@ -369,10 +369,10 @@ "id": "1c833eff", "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T07:25:48.041573Z", - "iopub.status.busy": "2026-08-04T07:25:48.041490Z", - "iopub.status.idle": "2026-08-04T07:25:48.054419Z", - "shell.execute_reply": "2026-08-04T07:25:48.053994Z" + "iopub.execute_input": "2026-08-21T10:20:59.905124Z", + "iopub.status.busy": "2026-08-21T10:20:59.905051Z", + "iopub.status.idle": "2026-08-21T10:20:59.916014Z", + "shell.execute_reply": "2026-08-21T10:20:59.915648Z" } }, "outputs": [ @@ -380,14 +380,14 @@ "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-08-04 09:25:48.045\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mlci\u001b[0m:\u001b[36m529\u001b[0m - \u001b[1mExpanding matrices...\u001b[0m\n" + "\u001b[32m2026-08-21 12:20:59.908\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mlci\u001b[0m:\u001b[36m634\u001b[0m - \u001b[1mExpanding matrices...\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-08-04 09:25:48.048\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mlci\u001b[0m:\u001b[36m548\u001b[0m - \u001b[1mCalculating dynamic inventory...\u001b[0m\n" + "\u001b[32m2026-08-21 12:20:59.910\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mlci\u001b[0m:\u001b[36m653\u001b[0m - \u001b[1mCalculating dynamic inventory...\u001b[0m\n" ] } ], @@ -401,10 +401,10 @@ "id": "4a51cd8a", "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T07:25:48.055579Z", - "iopub.status.busy": "2026-08-04T07:25:48.055510Z", - "iopub.status.idle": "2026-08-04T07:25:48.057993Z", - "shell.execute_reply": "2026-08-04T07:25:48.057687Z" + "iopub.execute_input": "2026-08-21T10:20:59.917251Z", + "iopub.status.busy": "2026-08-21T10:20:59.917178Z", + "iopub.status.idle": "2026-08-21T10:20:59.919509Z", + "shell.execute_reply": "2026-08-21T10:20:59.919216Z" } }, "outputs": [ @@ -458,10 +458,10 @@ "id": "b86341cc", "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T07:25:48.059254Z", - "iopub.status.busy": "2026-08-04T07:25:48.059152Z", - "iopub.status.idle": "2026-08-04T07:25:48.060765Z", - "shell.execute_reply": "2026-08-04T07:25:48.060464Z" + "iopub.execute_input": "2026-08-21T10:20:59.920807Z", + "iopub.status.busy": "2026-08-21T10:20:59.920741Z", + "iopub.status.idle": "2026-08-21T10:20:59.922216Z", + "shell.execute_reply": "2026-08-21T10:20:59.921921Z" } }, "outputs": [], @@ -483,10 +483,10 @@ "id": "c01ea15d", "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T07:25:48.061741Z", - "iopub.status.busy": "2026-08-04T07:25:48.061666Z", - "iopub.status.idle": "2026-08-04T07:25:48.063447Z", - "shell.execute_reply": "2026-08-04T07:25:48.063152Z" + "iopub.execute_input": "2026-08-21T10:20:59.923364Z", + "iopub.status.busy": "2026-08-21T10:20:59.923304Z", + "iopub.status.idle": "2026-08-21T10:20:59.925223Z", + "shell.execute_reply": "2026-08-21T10:20:59.924822Z" } }, "outputs": [], @@ -502,10 +502,10 @@ "id": "246683b4", "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T07:25:48.064502Z", - "iopub.status.busy": "2026-08-04T07:25:48.064431Z", - "iopub.status.idle": "2026-08-04T07:25:48.077250Z", - "shell.execute_reply": "2026-08-04T07:25:48.076922Z" + "iopub.execute_input": "2026-08-21T10:20:59.926245Z", + "iopub.status.busy": "2026-08-21T10:20:59.926173Z", + "iopub.status.idle": "2026-08-21T10:20:59.935312Z", + "shell.execute_reply": "2026-08-21T10:20:59.934902Z" } }, "outputs": [ @@ -541,36 +541,36 @@ " 0\n", " 2038-01-01 05:49:12\n", " 1.922234e-13\n", - " 342931108592119808\n", - " 342931108659228673\n", + " 349135793641672704\n", + " 349135793700392961\n", " \n", " \n", " 1\n", " 2039-01-01 11:38:24\n", " 1.766044e-13\n", - " 342931108592119808\n", - " 342931108659228673\n", + " 349135793641672704\n", + " 349135793700392961\n", " \n", " \n", " 2\n", " 2040-01-01 17:27:36\n", " 1.622546e-13\n", - " 342931108592119808\n", - " 342931108659228673\n", + " 349135793641672704\n", + " 349135793700392961\n", " \n", " \n", " 3\n", " 2040-12-31 23:16:48\n", " 1.490707e-13\n", - " 342931108592119808\n", - " 342931108659228673\n", + " 349135793641672704\n", + " 349135793700392961\n", " \n", " \n", " 4\n", " 2042-01-01 05:06:00\n", " 1.369581e-13\n", - " 342931108592119808\n", - " 342931108659228673\n", + " 349135793641672704\n", + " 349135793700392961\n", " \n", " \n", " ...\n", @@ -583,36 +583,36 @@ " 84\n", " 2122-01-01 14:42:00\n", " 1.556748e-16\n", - " 342931108592119808\n", - " 342931108659228673\n", + " 349135793641672704\n", + " 349135793700392961\n", " \n", " \n", " 85\n", " 2123-01-01 20:31:12\n", " 1.430256e-16\n", - " 342931108592119808\n", - " 342931108659228673\n", + " 349135793641672704\n", + " 349135793700392961\n", " \n", " \n", " 86\n", " 2124-01-02 02:20:24\n", " 1.314042e-16\n", - " 342931108592119808\n", - " 342931108659228673\n", + " 349135793641672704\n", + " 349135793700392961\n", " \n", " \n", " 87\n", " 2125-01-01 08:09:36\n", " 1.207270e-16\n", - " 342931108592119808\n", - " 342931108659228673\n", + " 349135793641672704\n", + " 349135793700392961\n", " \n", " \n", " 88\n", " 2126-01-01 13:58:48\n", " 1.109175e-16\n", - " 342931108592119808\n", - " 342931108659228673\n", + " 349135793641672704\n", + " 349135793700392961\n", " \n", " \n", "\n", @@ -621,17 +621,17 @@ ], "text/plain": [ " date amount flow activity\n", - "0 2038-01-01 05:49:12 1.922234e-13 342931108592119808 342931108659228673\n", - "1 2039-01-01 11:38:24 1.766044e-13 342931108592119808 342931108659228673\n", - "2 2040-01-01 17:27:36 1.622546e-13 342931108592119808 342931108659228673\n", - "3 2040-12-31 23:16:48 1.490707e-13 342931108592119808 342931108659228673\n", - "4 2042-01-01 05:06:00 1.369581e-13 342931108592119808 342931108659228673\n", + "0 2038-01-01 05:49:12 1.922234e-13 349135793641672704 349135793700392961\n", + "1 2039-01-01 11:38:24 1.766044e-13 349135793641672704 349135793700392961\n", + "2 2040-01-01 17:27:36 1.622546e-13 349135793641672704 349135793700392961\n", + "3 2040-12-31 23:16:48 1.490707e-13 349135793641672704 349135793700392961\n", + "4 2042-01-01 05:06:00 1.369581e-13 349135793641672704 349135793700392961\n", ".. ... ... ... ...\n", - "84 2122-01-01 14:42:00 1.556748e-16 342931108592119808 342931108659228673\n", - "85 2123-01-01 20:31:12 1.430256e-16 342931108592119808 342931108659228673\n", - "86 2124-01-02 02:20:24 1.314042e-16 342931108592119808 342931108659228673\n", - "87 2125-01-01 08:09:36 1.207270e-16 342931108592119808 342931108659228673\n", - "88 2126-01-01 13:58:48 1.109175e-16 342931108592119808 342931108659228673\n", + "84 2122-01-01 14:42:00 1.556748e-16 349135793641672704 349135793700392961\n", + "85 2123-01-01 20:31:12 1.430256e-16 349135793641672704 349135793700392961\n", + "86 2124-01-02 02:20:24 1.314042e-16 349135793641672704 349135793700392961\n", + "87 2125-01-01 08:09:36 1.207270e-16 349135793641672704 349135793700392961\n", + "88 2126-01-01 13:58:48 1.109175e-16 349135793641672704 349135793700392961\n", "\n", "[89 rows x 4 columns]" ] @@ -655,10 +655,10 @@ "id": "5868fb38", "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T07:25:48.078267Z", - "iopub.status.busy": "2026-08-04T07:25:48.078199Z", - "iopub.status.idle": "2026-08-04T07:25:48.210035Z", - "shell.execute_reply": "2026-08-04T07:25:48.209537Z" + "iopub.execute_input": "2026-08-21T10:20:59.936433Z", + "iopub.status.busy": "2026-08-21T10:20:59.936360Z", + "iopub.status.idle": "2026-08-21T10:21:00.005854Z", + "shell.execute_reply": "2026-08-21T10:21:00.005514Z" } }, "outputs": [ @@ -691,10 +691,10 @@ "id": "40c30c15", "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T07:25:48.211241Z", - "iopub.status.busy": "2026-08-04T07:25:48.211152Z", - "iopub.status.idle": "2026-08-04T07:25:48.213039Z", - "shell.execute_reply": "2026-08-04T07:25:48.212647Z" + "iopub.execute_input": "2026-08-21T10:21:00.007107Z", + "iopub.status.busy": "2026-08-21T10:21:00.007037Z", + "iopub.status.idle": "2026-08-21T10:21:00.008985Z", + "shell.execute_reply": "2026-08-21T10:21:00.008606Z" } }, "outputs": [ @@ -726,10 +726,10 @@ "id": "7fdfc3c7", "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T07:25:48.214579Z", - "iopub.status.busy": "2026-08-04T07:25:48.214508Z", - "iopub.status.idle": "2026-08-04T07:25:48.219142Z", - "shell.execute_reply": "2026-08-04T07:25:48.218700Z" + "iopub.execute_input": "2026-08-21T10:21:00.010257Z", + "iopub.status.busy": "2026-08-21T10:21:00.010186Z", + "iopub.status.idle": "2026-08-21T10:21:00.013918Z", + "shell.execute_reply": "2026-08-21T10:21:00.013515Z" } }, "outputs": [ @@ -768,10 +768,10 @@ "id": "bc445e2a", "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T07:25:48.220319Z", - "iopub.status.busy": "2026-08-04T07:25:48.220247Z", - "iopub.status.idle": "2026-08-04T07:25:48.282452Z", - "shell.execute_reply": "2026-08-04T07:25:48.282076Z" + "iopub.execute_input": "2026-08-21T10:21:00.015127Z", + "iopub.status.busy": "2026-08-21T10:21:00.015055Z", + "iopub.status.idle": "2026-08-21T10:21:00.118518Z", + "shell.execute_reply": "2026-08-21T10:21:00.118105Z" } }, "outputs": [ @@ -866,10 +866,10 @@ "id": "d66dd743", "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T07:25:48.284115Z", - "iopub.status.busy": "2026-08-04T07:25:48.284017Z", - "iopub.status.idle": "2026-08-04T07:25:48.289432Z", - "shell.execute_reply": "2026-08-04T07:25:48.289011Z" + "iopub.execute_input": "2026-08-21T10:21:00.120083Z", + "iopub.status.busy": "2026-08-21T10:21:00.119994Z", + "iopub.status.idle": "2026-08-21T10:21:00.124879Z", + "shell.execute_reply": "2026-08-21T10:21:00.124465Z" } }, "outputs": [ @@ -907,10 +907,10 @@ "id": "eecd5073", "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T07:25:48.290573Z", - "iopub.status.busy": "2026-08-04T07:25:48.290488Z", - "iopub.status.idle": "2026-08-04T07:25:48.295083Z", - "shell.execute_reply": "2026-08-04T07:25:48.294772Z" + "iopub.execute_input": "2026-08-21T10:21:00.125934Z", + "iopub.status.busy": "2026-08-21T10:21:00.125871Z", + "iopub.status.idle": "2026-08-21T10:21:00.130095Z", + "shell.execute_reply": "2026-08-21T10:21:00.129698Z" } }, "outputs": [ @@ -951,10 +951,10 @@ "id": "e075e0f9", "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T07:25:48.296266Z", - "iopub.status.busy": "2026-08-04T07:25:48.296192Z", - "iopub.status.idle": "2026-08-04T07:25:48.299830Z", - "shell.execute_reply": "2026-08-04T07:25:48.299417Z" + "iopub.execute_input": "2026-08-21T10:21:00.131242Z", + "iopub.status.busy": "2026-08-21T10:21:00.131173Z", + "iopub.status.idle": "2026-08-21T10:21:00.134639Z", + "shell.execute_reply": "2026-08-21T10:21:00.134300Z" } }, "outputs": [], @@ -1049,13 +1049,20 @@ "id": "15b6f25d", "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T07:25:48.300870Z", - "iopub.status.busy": "2026-08-04T07:25:48.300804Z", - "iopub.status.idle": "2026-08-04T07:25:48.345136Z", - "shell.execute_reply": "2026-08-04T07:25:48.344834Z" + "iopub.execute_input": "2026-08-21T10:21:00.135710Z", + "iopub.status.busy": "2026-08-21T10:21:00.135637Z", + "iopub.status.idle": "2026-08-21T10:21:00.185094Z", + "shell.execute_reply": "2026-08-21T10:21:00.184658Z" } }, "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[2m12:21:00+0200\u001b[0m [\u001b[33m\u001b[1mwarning \u001b[0m] \u001b[1mRemoving project from project __test_database1__ list, but not deleting data; if you switch to this project again you will have the same data again. To delete data permanently, pass `(..., delete_dir=True)`.\u001b[0m\n" + ] + }, { "name": "stderr", "output_type": "stream", @@ -1069,21 +1076,21 @@ "output_type": "stream", "text": [ "\r", - "100%|██████████| 3/3 [00:00<00:00, 37008.56it/s]" + "100%|██████████| 3/3 [00:00<00:00, 33288.13it/s]" ] }, { "name": "stdout", "output_type": "stream", "text": [ - "\u001b[2m09:25:48+0200\u001b[0m [\u001b[32m\u001b[1minfo \u001b[0m] \u001b[1mVacuuming database \u001b[0m\n" + "\u001b[2m12:21:00+0200\u001b[0m [\u001b[32m\u001b[1minfo \u001b[0m] \u001b[1mVacuuming database \u001b[0m\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ - "\u001b[2m09:25:48+0200\u001b[0m [\u001b[33m\u001b[1mwarning \u001b[0m] \u001b[1mNot able to determine geocollections for all datasets. This database is not ready for regionalization.\u001b[0m\n" + "\u001b[2m12:21:00+0200\u001b[0m [\u001b[33m\u001b[1mwarning \u001b[0m] \u001b[1mNot able to determine geocollections for all datasets. This database is not ready for regionalization.\u001b[0m\n" ] }, { @@ -1106,14 +1113,14 @@ "output_type": "stream", "text": [ "\r", - "100%|██████████| 1/1 [00:00<00:00, 10305.42it/s]" + "100%|██████████| 1/1 [00:00<00:00, 11618.57it/s]" ] }, { "name": "stdout", "output_type": "stream", "text": [ - "\u001b[2m09:25:48+0200\u001b[0m [\u001b[32m\u001b[1minfo \u001b[0m] \u001b[1mVacuuming database \u001b[0m\n" + "\u001b[2m12:21:00+0200\u001b[0m [\u001b[32m\u001b[1minfo \u001b[0m] \u001b[1mVacuuming database \u001b[0m\n" ] }, { @@ -1142,10 +1149,10 @@ "id": "6ef97c64", "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T07:25:48.346390Z", - "iopub.status.busy": "2026-08-04T07:25:48.346317Z", - "iopub.status.idle": "2026-08-04T07:25:48.381016Z", - "shell.execute_reply": "2026-08-04T07:25:48.380549Z" + "iopub.execute_input": "2026-08-21T10:21:00.186207Z", + "iopub.status.busy": "2026-08-21T10:21:00.186141Z", + "iopub.status.idle": "2026-08-21T10:21:00.218153Z", + "shell.execute_reply": "2026-08-21T10:21:00.217707Z" } }, "outputs": [ @@ -1153,99 +1160,99 @@ "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-08-04 09:25:48.346\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m142\u001b[0m - \u001b[1mInitializing TimexLCA object...\u001b[0m\n" + "\u001b[32m2026-08-21 12:21:00.186\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m174\u001b[0m - \u001b[1mInitializing TimexLCA object...\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-08-04 09:25:48.347\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m149\u001b[0m - \u001b[1mNo database_dates provided. Treating the databases containing the functional unit as dynamic. No remapping of inventories to time explicit databases will be done.\u001b[0m\n" + "\u001b[32m2026-08-21 12:21:00.186\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m_resolve_database_dates\u001b[0m:\u001b[36m312\u001b[0m - \u001b[1mNo database_dates provided, and no database in this project carries `representative_time` metadata. Treating the databases containing the functional unit as dynamic. No remapping of inventories to time explicit databases will be done.\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-08-04 09:25:48.347\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m163\u001b[0m - \u001b[1mCalculating base LCA...\u001b[0m\n" + "\u001b[32m2026-08-21 12:21:00.187\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m194\u001b[0m - \u001b[1mCalculating base LCA...\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-08-04 09:25:48.352\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m180\u001b[0m - \u001b[1mCollecting node infos...\u001b[0m\n" + "\u001b[32m2026-08-21 12:21:00.191\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m211\u001b[0m - \u001b[1mCollecting node infos...\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-08-04 09:25:48.353\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m192\u001b[0m - \u001b[1mLoading node metadata from 1 database(s)...\u001b[0m\n" + "\u001b[32m2026-08-21 12:21:00.192\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m223\u001b[0m - \u001b[1mLoading node metadata from 1 database(s)...\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-08-04 09:25:48.354\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m229\u001b[0m - \u001b[1mTimexLCA initialized.\u001b[0m\n" + "\u001b[32m2026-08-21 12:21:00.192\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m260\u001b[0m - \u001b[1mTimexLCA initialized.\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-08-04 09:25:48.354\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mbuild_timeline\u001b[0m:\u001b[36m358\u001b[0m - \u001b[1mNo edge filter function provided. Skipping all edges in background databases.\u001b[0m\n" + "\u001b[32m2026-08-21 12:21:00.193\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mbuild_timeline\u001b[0m:\u001b[36m453\u001b[0m - \u001b[1mNo edge filter function provided. Skipping all edges in background databases.\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-08-04 09:25:48.354\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mbuild_timeline\u001b[0m:\u001b[36m379\u001b[0m - \u001b[1mCreating activity time mapping...\u001b[0m\n" + "\u001b[32m2026-08-21 12:21:00.193\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mbuild_timeline\u001b[0m:\u001b[36m474\u001b[0m - \u001b[1mCreating activity time mapping...\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-08-04 09:25:48.354\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timeline_builder\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m112\u001b[0m - \u001b[1mTraversing supply chain graph...\u001b[0m\n" + "\u001b[32m2026-08-21 12:21:00.193\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timeline_builder\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m112\u001b[0m - \u001b[1mTraversing supply chain graph...\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-08-04 09:25:48.356\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timeline_builder\u001b[0m:\u001b[36mbuild_timeline\u001b[0m:\u001b[36m186\u001b[0m - \u001b[1mBuilding timeline...\u001b[0m\n" + "\u001b[32m2026-08-21 12:21:00.195\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timeline_builder\u001b[0m:\u001b[36mbuild_timeline\u001b[0m:\u001b[36m183\u001b[0m - \u001b[1mBuilding timeline...\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-08-04 09:25:48.366\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timeline_builder\u001b[0m:\u001b[36madd_column_temporal_market_shares_to_timeline\u001b[0m:\u001b[36m587\u001b[0m - \u001b[1mNo time-explicit databases are provided. Mapping to time-explicit databases is not possible.\u001b[0m\n" + "\u001b[32m2026-08-21 12:21:00.203\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timeline_builder\u001b[0m:\u001b[36madd_column_temporal_market_shares_to_timeline\u001b[0m:\u001b[36m627\u001b[0m - \u001b[1mNo time-explicit databases are provided. Mapping to time-explicit databases is not possible.\u001b[0m\n" ] }, { - "name": "stderr", + "name": "stdout", "output_type": "stream", "text": [ - "\u001b[32m2026-08-04 09:25:48.371\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mlci\u001b[0m:\u001b[36m529\u001b[0m - \u001b[1mExpanding matrices...\u001b[0m\n" + "Starting graph traversal\n", + "Calculation count: 0\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "\u001b[32m2026-08-04 09:25:48.374\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mlci\u001b[0m:\u001b[36m548\u001b[0m - \u001b[1mCalculating dynamic inventory...\u001b[0m\n" + "\u001b[32m2026-08-21 12:21:00.209\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mlci\u001b[0m:\u001b[36m634\u001b[0m - \u001b[1mExpanding matrices...\u001b[0m\n" ] }, { - "name": "stdout", + "name": "stderr", "output_type": "stream", "text": [ - "Starting graph traversal\n", - "Calculation count: 0\n" + "\u001b[32m2026-08-21 12:21:00.211\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36mlci\u001b[0m:\u001b[36m653\u001b[0m - \u001b[1mCalculating dynamic inventory...\u001b[0m\n" ] } ], @@ -1275,10 +1282,10 @@ "id": "f6ff1eb3", "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T07:25:48.382353Z", - "iopub.status.busy": "2026-08-04T07:25:48.382270Z", - "iopub.status.idle": "2026-08-04T07:25:48.384529Z", - "shell.execute_reply": "2026-08-04T07:25:48.384192Z" + "iopub.execute_input": "2026-08-21T10:21:00.219399Z", + "iopub.status.busy": "2026-08-21T10:21:00.219328Z", + "iopub.status.idle": "2026-08-21T10:21:00.221486Z", + "shell.execute_reply": "2026-08-21T10:21:00.221206Z" } }, "outputs": [], @@ -1304,10 +1311,10 @@ "id": "e3b6c6b4", "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T07:25:48.385687Z", - "iopub.status.busy": "2026-08-04T07:25:48.385615Z", - "iopub.status.idle": "2026-08-04T07:25:48.447661Z", - "shell.execute_reply": "2026-08-04T07:25:48.447223Z" + "iopub.execute_input": "2026-08-21T10:21:00.222709Z", + "iopub.status.busy": "2026-08-21T10:21:00.222642Z", + "iopub.status.idle": "2026-08-21T10:21:00.283087Z", + "shell.execute_reply": "2026-08-21T10:21:00.282626Z" } }, "outputs": [ @@ -1348,10 +1355,10 @@ "id": "4668245f", "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T07:25:48.449664Z", - "iopub.status.busy": "2026-08-04T07:25:48.449521Z", - "iopub.status.idle": "2026-08-04T07:25:48.508544Z", - "shell.execute_reply": "2026-08-04T07:25:48.508087Z" + "iopub.execute_input": "2026-08-21T10:21:00.284483Z", + "iopub.status.busy": "2026-08-21T10:21:00.284388Z", + "iopub.status.idle": "2026-08-21T10:21:00.342640Z", + "shell.execute_reply": "2026-08-21T10:21:00.342270Z" } }, "outputs": [ @@ -1384,10 +1391,10 @@ "id": "9cae721c", "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T07:25:48.509809Z", - "iopub.status.busy": "2026-08-04T07:25:48.509710Z", - "iopub.status.idle": "2026-08-04T07:25:48.561832Z", - "shell.execute_reply": "2026-08-04T07:25:48.561066Z" + "iopub.execute_input": "2026-08-21T10:21:00.344006Z", + "iopub.status.busy": "2026-08-21T10:21:00.343910Z", + "iopub.status.idle": "2026-08-21T10:21:00.395186Z", + "shell.execute_reply": "2026-08-21T10:21:00.394669Z" } }, "outputs": [ @@ -1435,10 +1442,10 @@ "id": "a2027c21", "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T07:25:48.563316Z", - "iopub.status.busy": "2026-08-04T07:25:48.563207Z", - "iopub.status.idle": "2026-08-04T07:25:48.619445Z", - "shell.execute_reply": "2026-08-04T07:25:48.618938Z" + "iopub.execute_input": "2026-08-21T10:21:00.396421Z", + "iopub.status.busy": "2026-08-21T10:21:00.396334Z", + "iopub.status.idle": "2026-08-21T10:21:00.447183Z", + "shell.execute_reply": "2026-08-21T10:21:00.446833Z" } }, "outputs": [], @@ -1498,10 +1505,10 @@ "id": "f62763da", "metadata": { "execution": { - "iopub.execute_input": "2026-08-04T07:25:48.620785Z", - "iopub.status.busy": "2026-08-04T07:25:48.620708Z", - "iopub.status.idle": "2026-08-04T07:25:48.623869Z", - "shell.execute_reply": "2026-08-04T07:25:48.623461Z" + "iopub.execute_input": "2026-08-21T10:21:00.448493Z", + "iopub.status.busy": "2026-08-21T10:21:00.448421Z", + "iopub.status.idle": "2026-08-21T10:21:00.451482Z", + "shell.execute_reply": "2026-08-21T10:21:00.451096Z" } }, "outputs": [ diff --git a/notebooks/tutorials/4_import_model_from_excel.ipynb b/notebooks/tutorials/4_import_model_from_excel.ipynb index f3c33b8c..e2700d2a 100644 --- a/notebooks/tutorials/4_import_model_from_excel.ipynb +++ b/notebooks/tutorials/4_import_model_from_excel.ipynb @@ -398,34 +398,19 @@ { "cell_type": "markdown", "metadata": {}, - "source": [ - "`bw_timex` needs to know the representative time of the databases:" - ] + "source": "`bw_timex` needs to know the representative time of the databases. We record it once, as `representative_time` metadata on each database, using `set_database_metadata`:" }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "from datetime import datetime\n", - "\n", - "database_dates = {\n", - " \"background_2020\": datetime.strptime(\"2020\", \"%Y\"),\n", - " \"background_2030\": datetime.strptime(\"2030\", \"%Y\"),\n", - " \"background_2040\": datetime.strptime(\"2040\", \"%Y\"),\n", - " \"foreground\": \"dynamic\", # flag databases that should be temporally distributed with \"dynamic\"\n", - "}" - ] + "source": "from datetime import datetime\n\nfrom bw_timex import set_database_metadata\n\nset_database_metadata(\"background_2020\", representative_time=datetime(2020, 1, 1))\nset_database_metadata(\"background_2030\", representative_time=datetime(2030, 1, 1))\nset_database_metadata(\"background_2040\", representative_time=datetime(2040, 1, 1))\n# the \"foreground\" database, which holds our demand, is treated as \"dynamic\" automatically" }, { "cell_type": "markdown", "metadata": {}, - "source": [ - "Now, we can instantiate a `TimexLCA`. It's structure is similar to a normal `bw2calc.LCA`, but with the additional argument `database_dates`.\n", - "\n", - "Not sure about the required inputs? Check the documentation using `?`. All our classes and methods have docstrings!" - ] + "source": "Now, we can instantiate a `TimexLCA`. It's structure is similar to a normal `bw2calc.LCA` - `TimexLCA` reads the metadata we just set automatically, so no timing argument is needed.\n\nNot sure about the required inputs? Check the documentation using `?`. All our classes and methods have docstrings!" }, { "cell_type": "code", @@ -456,25 +441,8 @@ "cell_type": "code", "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "\u001b[32m2026-06-25 17:01:01.438\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m115\u001b[0m - \u001b[1mInitializing TimexLCA object...\u001b[0m\n", - "\u001b[32m2026-06-25 17:01:01.438\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m131\u001b[0m - \u001b[1mCalculating base LCA...\u001b[0m\n", - "\u001b[32m2026-06-25 17:01:01.447\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mbw_timex.timex_lca\u001b[0m:\u001b[36m__init__\u001b[0m:\u001b[36m148\u001b[0m - \u001b[1mCollecting node infos...\u001b[0m\n" - ] - } - ], - "source": [ - "# intialize the TimexLCA object with the functional unit, method, and database dates\n", - "tlca = TimexLCA({driving: 1}, method, database_dates)\n", - "# build the timeline with a temporal grouping of \"month\"\n", - "tlca.build_timeline(temporal_grouping=\"month\")\n", - "# calculate the time-explicit LCI\n", - "tlca.lci()" - ] + "outputs": [], + "source": "# intialize the TimexLCA object with the functional unit and method\ntlca = TimexLCA({driving: 1}, method)\n# build the timeline with a temporal grouping of \"month\"\ntlca.build_timeline(temporal_grouping=\"month\")\n# calculate the time-explicit LCI\ntlca.lci()" }, { "cell_type": "markdown", diff --git a/tests/conftest.py b/tests/conftest.py index d66c76d9..54a6d45b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -47,3 +47,4 @@ zero_weight_first_background_td_db, ) from .fixtures.vehicle_explicit_db_fixture import vehicle_explicit_db +from .fixtures.split_foreground_db_fixture import split_foreground_db diff --git a/tests/fixtures/split_foreground_db_fixture.py b/tests/fixtures/split_foreground_db_fixture.py new file mode 100644 index 00000000..fb305666 --- /dev/null +++ b/tests/fixtures/split_foreground_db_fixture.py @@ -0,0 +1,93 @@ +import bw2data as bd +import numpy as np +import pytest +from bw2data.tests import bw2test +from bw_timex import TemporalDistribution + + +@pytest.fixture +@bw2test +def split_foreground_db(): + """A foreground split across two databases, only one of which holds the FU. + + ``foreground`` holds the functional unit, which consumes an intermediate + process living in a *second* foreground database, + ``intermediate_foreground``. That second database represents no point in + time either, but nothing marks it as dynamic automatically: only the + database holding the functional unit gets that treatment. Unless the user + marks it (or lists it in ``database_dates``), it is missing from the + mapping and its nodes cannot be placed in time. + """ + bd.Database("bio").write( + {("bio", "co2"): {"name": "carbon dioxide", "unit": "kg", "type": "emission"}} + ) + bd.Method(("GWP", "example")).write([(("bio", "co2"), 1.0)]) + + for year, co2 in (("2020", 10), ("2030", 5)): + bd.Database(f"background_{year}").write( + { + (f"background_{year}", "electricity"): { + "name": "electricity", + "unit": "kWh", + "location": "GLO", + "reference product": "electricity", + "exchanges": [ + { + "input": (f"background_{year}", "electricity"), + "amount": 1, + "type": "production", + }, + {"input": ("bio", "co2"), "amount": co2, "type": "biosphere"}, + ], + } + } + ) + + bd.Database("intermediate_foreground").write( + { + ("intermediate_foreground", "assembly"): { + "name": "assembly", + "unit": "unit", + "location": "GLO", + "reference product": "assembly", + "exchanges": [ + { + "input": ("intermediate_foreground", "assembly"), + "amount": 1, + "type": "production", + }, + { + "input": ("background_2020", "electricity"), + "amount": 2, + "type": "technosphere", + }, + ], + } + } + ) + + bd.Database("foreground").write( + { + ("foreground", "fu"): { + "name": "fu", + "unit": "unit", + "location": "GLO", + "reference product": "fu", + "exchanges": [ + {"input": ("foreground", "fu"), "amount": 1, "type": "production"}, + { + "input": ("intermediate_foreground", "assembly"), + "amount": 1, + "type": "technosphere", + "temporal_distribution": TemporalDistribution( + date=np.array([5], dtype="timedelta64[Y]"), + amount=np.array([1.0]), + ), + }, + ], + } + } + ) + + for db in bd.databases: + bd.Database(db).process() diff --git a/tests/test_database_metadata.py b/tests/test_database_metadata.py new file mode 100644 index 00000000..31c58483 --- /dev/null +++ b/tests/test_database_metadata.py @@ -0,0 +1,425 @@ +"""Tests for reading and writing what a Brightway database represents.""" + +from datetime import datetime + +import bw2data as bd +import pytest +from loguru import logger + +from bw_timex import TimexLCA, set_database_metadata +from bw_timex.database_metadata import resolve_database_dates_from_metadata +from bw_timex.validation import TimexLCAInputs + +# ─── Tests for set_database_metadata ─── + + +@pytest.mark.usefixtures("temporal_grouping_db_monthly") +class TestSetDatabaseMetadata: + + def test_datetime_is_stored_as_iso_string(self): + set_database_metadata("db_2022", representative_time=datetime(2022, 1, 1)) + assert bd.databases["db_2022"]["representative_time"] == "2022-01-01T00:00:00" + + def test_iso_string_is_stored_as_given(self): + set_database_metadata("db_2022", representative_time="2022-01-01") + assert bd.databases["db_2022"]["representative_time"] == "2022-01-01" + + def test_dynamic_is_allowed(self): + set_database_metadata("foreground", representative_time="dynamic") + assert bd.databases["foreground"]["representative_time"] == "dynamic" + + def test_scenario_fields_are_stored(self): + set_database_metadata( + "db_2022", + representative_time=datetime(2022, 1, 1), + iam_model="remind", + pathway="SSP2-PkBudg500", + ) + assert bd.databases["db_2022"]["iam_model"] == "remind" + assert bd.databases["db_2022"]["pathway"] == "SSP2-PkBudg500" + + def test_database_object_is_accepted(self): + set_database_metadata( + bd.Database("db_2022"), representative_time=datetime(2022, 1, 1) + ) + assert bd.databases["db_2022"]["representative_time"] == "2022-01-01T00:00:00" + + def test_existing_metadata_is_kept(self): + before = bd.databases["db_2022"]["backend"] + set_database_metadata("db_2022", representative_time=datetime(2022, 1, 1)) + assert bd.databases["db_2022"]["backend"] == before + + def test_survives_flush_and_reload(self): + set_database_metadata("db_2022", representative_time=datetime(2022, 1, 1)) + bd.databases.__init__() # re-read from disk + assert bd.databases["db_2022"]["representative_time"] == "2022-01-01T00:00:00" + + def test_unregistered_database_raises(self): + with pytest.raises(ValueError, match="not registered"): + set_database_metadata("no_such_db", representative_time=datetime(2022, 1, 1)) + + def test_unparseable_representative_time_raises(self): + with pytest.raises(ValueError, match="representative_time"): + set_database_metadata("db_2022", representative_time="whenever") + + def test_non_serializable_value_raises(self): + with pytest.raises(ValueError, match="JSON"): + set_database_metadata("db_2022", pathway=object()) + + def test_no_metadata_raises(self): + with pytest.raises(ValueError, match="at least one"): + set_database_metadata("db_2022") + + +# ─── Tests for resolving database dates from metadata ─── + + +@pytest.mark.usefixtures("temporal_grouping_db_monthly") +class TestResolveFromMetadata: + + def test_empty_project_metadata_resolves_to_nothing(self): + assert resolve_database_dates_from_metadata() == {} + + def test_iso_strings_resolve_to_datetimes(self): + set_database_metadata("db_2022", representative_time="2022-01-01") + set_database_metadata("db_2024", representative_time="2024-01-01") + assert resolve_database_dates_from_metadata() == { + "db_2022": datetime(2022, 1, 1), + "db_2024": datetime(2024, 1, 1), + } + + def test_dynamic_metadata_resolves_to_dynamic(self): + set_database_metadata("db_2022", representative_time="2022-01-01") + set_database_metadata("foreground", representative_time="dynamic") + resolved = resolve_database_dates_from_metadata() + assert resolved["foreground"] == "dynamic" + assert resolved["db_2022"] == datetime(2022, 1, 1) + + def test_databases_without_metadata_are_ignored(self): + set_database_metadata("db_2022", representative_time="2022-01-01") + assert set(resolve_database_dates_from_metadata()) == {"db_2022"} + + def test_multi_scenario_database_is_skipped(self): + set_database_metadata("db_2022", representative_time="2022-01-01") + set_database_metadata( + "db_2024", + representative_time="2024-01-01", + scenarios=[ + {"pathway": "SSP2-Base", "representative_time": "2024-01-01"}, + {"pathway": "SSP2-PkBudg500", "representative_time": "2024-01-01"}, + ], + ) + assert set(resolve_database_dates_from_metadata()) == {"db_2022"} + + def test_invalid_metadata_value_raises_naming_the_database(self): + bd.databases["db_2022"]["representative_time"] = "whenever" + bd.databases.flush() + with pytest.raises(ValueError, match="db_2022"): + resolve_database_dates_from_metadata() + + def test_multi_scenario_database_is_named_in_the_log(self): + set_database_metadata("db_2022", representative_time="2022-01-01") + set_database_metadata( + "db_2024", + representative_time="2024-01-01", + scenarios=[ + {"pathway": "SSP2-Base", "representative_time": "2024-01-01"}, + {"pathway": "SSP2-PkBudg500", "representative_time": "2024-01-01"}, + ], + ) + messages = [] + sink_id = logger.add(messages.append, level="INFO") + try: + resolve_database_dates_from_metadata() + finally: + logger.remove(sink_id) + assert any("db_2024" in message for message in messages) + + +# ─── Tests for order-insensitive `external_scenarios` comparison ─── + + +@pytest.mark.usefixtures("temporal_grouping_db_monthly") +class TestExternalScenariosOrderInsensitive: + + def test_ambiguity_signature_is_order_insensitive(self): + """Same `external_scenarios`, listed in a different order, must not + look like two different scenarios.""" + set_database_metadata( + "db_2022", + representative_time="2022-01-01", + external_scenarios=["scenario_a", "scenario_b"], + ) + set_database_metadata( + "db_2024", + representative_time="2024-01-01", + external_scenarios=["scenario_b", "scenario_a"], + ) + # Would raise "Several background scenarios found" if the signature + # depended on list order. + resolved = resolve_database_dates_from_metadata() + assert set(resolved) == {"db_2022", "db_2024"} + + def test_filter_value_is_order_insensitive(self): + set_database_metadata( + "db_2022", + representative_time="2022-01-01", + external_scenarios=["scenario_a", "scenario_b"], + ) + resolved = resolve_database_dates_from_metadata( + scenario={"external_scenarios": ["scenario_b", "scenario_a"]} + ) + assert resolved == {"db_2022": datetime(2022, 1, 1)} + + +# ─── Tests for scenario selection ─── + + +@pytest.mark.usefixtures("temporal_grouping_db_monthly") +class TestScenarioSelection: + + @pytest.fixture(autouse=True) + def two_scenarios(self, temporal_grouping_db_monthly): + """db_2022 and db_2024 hold the same year in two different pathways.""" + set_database_metadata( + "db_2022", + representative_time="2022-01-01", + iam_model="remind", + pathway="SSP2-PkBudg500", + premise_version="2.4.9.1", + ) + set_database_metadata( + "db_2024", + representative_time="2024-01-01", + iam_model="remind", + pathway="SSP2-Base", + premise_version="2.4.9.1", + ) + + def test_two_scenario_sets_without_selection_raises(self): + with pytest.raises(ValueError, match="Several background scenarios"): + resolve_database_dates_from_metadata() + + def test_error_names_the_differing_key_and_values(self): + with pytest.raises(ValueError) as excinfo: + resolve_database_dates_from_metadata() + message = str(excinfo.value) + assert "pathway" in message + assert "SSP2-PkBudg500" in message + assert "SSP2-Base" in message + # iam_model is identical in both sets, so it isn't part of the report + assert "iam_model" not in message + + def test_scenario_selects_one_set(self): + resolved = resolve_database_dates_from_metadata( + scenario={"pathway": "SSP2-Base"} + ) + assert resolved == {"db_2024": datetime(2024, 1, 1)} + + def test_databases_without_scenario_metadata_survive_the_filter(self): + set_database_metadata("foreground", representative_time="dynamic") + resolved = resolve_database_dates_from_metadata( + scenario={"pathway": "SSP2-Base"} + ) + assert resolved == { + "db_2024": datetime(2024, 1, 1), + "foreground": "dynamic", + } + + def test_several_filter_keys_are_combined(self): + resolved = resolve_database_dates_from_metadata( + scenario={"iam_model": "remind", "pathway": "SSP2-Base"} + ) + assert set(resolved) == {"db_2024"} + + def test_filter_matching_nothing_resolves_to_nothing(self): + assert resolve_database_dates_from_metadata( + scenario={"pathway": "SSP2-PkBudg1150"} + ) == {} + + def test_unknown_filter_key_raises_listing_available_keys(self): + with pytest.raises(ValueError) as excinfo: + resolve_database_dates_from_metadata(scenario={"pathwya": "SSP2-Base"}) + message = str(excinfo.value) + assert "pathwya" in message + assert "pathway" in message + + def test_same_scenario_from_two_premise_versions_is_not_ambiguous(self): + set_database_metadata("db_2024", pathway="SSP2-PkBudg500") + set_database_metadata("db_2024", premise_version="2.4.9.2") + assert set(resolve_database_dates_from_metadata()) == {"db_2022", "db_2024"} + + +# ─── Tests for TimexLCA using database metadata ─── + + +@pytest.mark.usefixtures("temporal_grouping_db_monthly") +class TestTimexLCAFromMetadata: + + @pytest.fixture + def fu(self): + return bd.get_node(database="foreground", code="A") + + def test_no_arguments_uses_metadata(self, fu): + set_database_metadata("db_2022", representative_time="2022-01-01") + set_database_metadata("db_2024", representative_time="2024-01-01") + tlca = TimexLCA(demand={fu.key: 1}, method=("GWP", "example")) + assert tlca.database_dates == { + "db_2022": datetime(2022, 1, 1), + "db_2024": datetime(2024, 1, 1), + "foreground": "dynamic", + } + + def test_demand_database_metadata_is_respected(self, fu): + set_database_metadata("db_2022", representative_time="2022-01-01") + set_database_metadata("foreground", representative_time="dynamic") + tlca = TimexLCA(demand={fu.key: 1}, method=("GWP", "example")) + assert tlca.database_dates["foreground"] == "dynamic" + + def test_scenario_is_forwarded(self, fu): + set_database_metadata( + "db_2022", representative_time="2022-01-01", pathway="SSP2-Base" + ) + set_database_metadata( + "db_2024", representative_time="2024-01-01", pathway="SSP2-PkBudg500" + ) + tlca = TimexLCA( + demand={fu.key: 1}, + method=("GWP", "example"), + scenario={"pathway": "SSP2-Base"}, + ) + assert tlca.database_dates == { + "db_2022": datetime(2022, 1, 1), + "foreground": "dynamic", + } + + def test_typo_in_scenario_value_raises(self, fu): + set_database_metadata( + "db_2022", representative_time="2022-01-01", pathway="SSP2-Base" + ) + set_database_metadata( + "db_2024", representative_time="2024-01-01", pathway="SSP2-PkBudg500" + ) + with pytest.raises(ValueError, match="SSP2-Basee") as excinfo: + TimexLCA( + demand={fu.key: 1}, + method=("GWP", "example"), + scenario={"pathway": "SSP2-Basee"}, + ) + message = str(excinfo.value) + # The filter that matched nothing, and what's actually declared for + # that key, must both be in the error so a typo is obvious. + assert "SSP2-Base" in message + assert "SSP2-PkBudg500" in message + + def test_typo_in_scenario_value_raises_even_with_a_non_scenario_survivor(self, fu): + """A database that declares no scenario keys survives every filter, + so the resolved mapping is non-empty even though the filter matched + none of the scenario databases it was meant to select among. The + error must still fire - checking whether `resolved` is empty is not + enough. + """ + set_database_metadata("db_2022", representative_time="2022-01-01") + set_database_metadata( + "db_2024", representative_time="2024-01-01", pathway="SSP2-Base" + ) + with pytest.raises(ValueError, match="SSP2-Basee") as excinfo: + TimexLCA( + demand={fu.key: 1}, + method=("GWP", "example"), + scenario={"pathway": "SSP2-Basee"}, + ) + message = str(excinfo.value) + assert "SSP2-Base" in message + + def test_database_dates_is_exclusive(self, fu): + set_database_metadata("db_2022", representative_time="2022-01-01") + set_database_metadata("db_2024", representative_time="2024-01-01") + tlca = TimexLCA( + demand={fu.key: 1}, + method=("GWP", "example"), + database_dates={ + "db_2024": datetime(2024, 1, 1), + "foreground": "dynamic", + }, + ) + assert tlca.database_dates == { + "db_2024": datetime(2024, 1, 1), + "foreground": "dynamic", + } + + def test_database_dates_with_scenario_raises(self, fu): + with pytest.raises(ValueError, match="only applies when"): + TimexLCA( + demand={fu.key: 1}, + method=("GWP", "example"), + database_dates={"foreground": "dynamic"}, + scenario={"pathway": "SSP2-Base"}, + ) + + def test_empty_database_dates_dict_raises(self, fu): + """An empty dict is falsy but not `None`: it must still be treated as + an explicit (if invalid) `database_dates`, not fall through to + metadata resolution. + """ + set_database_metadata("db_2022", representative_time="2022-01-01") + with pytest.raises(ValueError, match="non-empty dictionary"): + TimexLCA( + demand={fu.key: 1}, + method=("GWP", "example"), + database_dates={}, + ) + + def test_no_metadata_anywhere_falls_back_to_dynamic_demand(self, fu): + tlca = TimexLCA(demand={fu.key: 1}, method=("GWP", "example")) + assert tlca.database_dates == {"foreground": "dynamic"} + + def test_metadata_and_database_dates_give_the_same_score(self, fu): + explicit = TimexLCA( + demand={fu.key: 1}, + method=("GWP", "example"), + database_dates={ + "db_2022": datetime(2022, 1, 1), + "db_2024": datetime(2024, 1, 1), + "foreground": "dynamic", + }, + ) + explicit.build_timeline(starting_datetime=datetime(2024, 1, 2)) + explicit.lci() + explicit.static_lcia() + + set_database_metadata("db_2022", representative_time="2022-01-01") + set_database_metadata("db_2024", representative_time="2024-01-01") + from_metadata = TimexLCA(demand={fu.key: 1}, method=("GWP", "example")) + from_metadata.build_timeline(starting_datetime=datetime(2024, 1, 2)) + from_metadata.lci() + from_metadata.static_lcia() + + assert from_metadata.static_score == pytest.approx(explicit.static_score) + + +# ─── Tests for TimexLCAInputs.validate_scenario ─── + + +@pytest.mark.usefixtures("temporal_grouping_db_monthly") +class TestValidateScenario: + + @pytest.fixture + def fu(self): + return bd.get_node(database="foreground", code="A") + + def test_non_string_key_raises(self, fu): + with pytest.raises(ValueError, match="scenario keys must be strings"): + TimexLCAInputs( + demand={fu.key: 1}, + method=("GWP", "example"), + scenario={123: "SSP2-Base"}, + ) + + def test_non_scalar_value_raises(self, fu): + with pytest.raises(ValueError, match="scenario values must be scalars"): + TimexLCAInputs( + demand={fu.key: 1}, + method=("GWP", "example"), + scenario={"pathway": {"nested": "dict"}}, + ) diff --git a/tests/test_timex_lca.py b/tests/test_timex_lca.py index f2851b51..9b01d00d 100644 --- a/tests/test_timex_lca.py +++ b/tests/test_timex_lca.py @@ -535,7 +535,7 @@ def test_no_database_dates_defaults_to_dynamic(self): def test_demand_not_in_dynamic_db_raises(self): """Test error when demand activity's db is not marked dynamic (line 988).""" fu = bd.get_node(database="foreground", code="A") - with pytest.raises(ValueError, match="not marked as 'dynamic'"): + with pytest.raises(ValueError, match="mapped to a date rather than 'dynamic'"): TimexLCA( demand={fu.key: 1}, method=("GWP", "example"), diff --git a/tests/test_unmapped_database.py b/tests/test_unmapped_database.py new file mode 100644 index 00000000..5a6e3dd7 --- /dev/null +++ b/tests/test_unmapped_database.py @@ -0,0 +1,87 @@ +"""A database that is reached by the traversal but is missing from the mapping. + +Only the databases holding the functional unit are treated as `"dynamic"` +automatically. A second foreground database - an intermediate one, which does +not hold the functional unit - is therefore missing from the mapping unless the +user marks it, and its nodes cannot be placed in time. +""" + +from datetime import datetime + +import pytest + +from bw_timex import TimexLCA, set_database_metadata +from bw_timex.errors import UnmappedDatabaseError + +DATABASE_DATES = { + "foreground": "dynamic", + "background_2020": datetime(2020, 1, 1), + "background_2030": datetime(2030, 1, 1), +} + + +def _set_background_metadata(): + set_database_metadata("background_2020", representative_time=datetime(2020, 1, 1)) + set_database_metadata("background_2030", representative_time=datetime(2030, 1, 1)) + + +@pytest.mark.usefixtures("split_foreground_db") +class TestUnmappedDatabase: + + def test_explicit_database_dates_raise_unmapped_database_error(self): + tlca = TimexLCA( + demand={("foreground", "fu"): 1}, + method=("GWP", "example"), + database_dates=DATABASE_DATES, + ) + with pytest.raises(UnmappedDatabaseError) as excinfo: + tlca.build_timeline() + assert "intermediate_foreground" in str(excinfo.value) + + def test_error_names_an_affected_process(self): + tlca = TimexLCA( + demand={("foreground", "fu"): 1}, + method=("GWP", "example"), + database_dates=DATABASE_DATES, + ) + with pytest.raises(UnmappedDatabaseError, match="assembly"): + tlca.build_timeline() + + def test_error_points_at_the_fix(self): + tlca = TimexLCA( + demand={("foreground", "fu"): 1}, + method=("GWP", "example"), + database_dates=DATABASE_DATES, + ) + with pytest.raises(UnmappedDatabaseError, match="set_database_metadata"): + tlca.build_timeline() + + def test_metadata_resolution_raises_the_same_error(self): + _set_background_metadata() + tlca = TimexLCA(demand={("foreground", "fu"): 1}, method=("GWP", "example")) + with pytest.raises(UnmappedDatabaseError, match="intermediate_foreground"): + tlca.build_timeline() + + def test_marking_the_database_dynamic_fixes_it(self): + _set_background_metadata() + set_database_metadata("intermediate_foreground", representative_time="dynamic") + tlca = TimexLCA(demand={("foreground", "fu"): 1}, method=("GWP", "example")) + tlca.build_timeline() + assert "assembly" in set(tlca.timeline["producer_name"]) + + def test_listing_the_database_in_database_dates_fixes_it(self): + tlca = TimexLCA( + demand={("foreground", "fu"): 1}, + method=("GWP", "example"), + database_dates={**DATABASE_DATES, "intermediate_foreground": "dynamic"}, + ) + tlca.build_timeline() + assert "assembly" in set(tlca.timeline["producer_name"]) + + def test_unmapped_database_error_is_a_value_error(self): + assert issubclass(UnmappedDatabaseError, ValueError) + + def test_unmapped_database_error_is_exposed_at_top_level(self): + import bw_timex + + assert bw_timex.UnmappedDatabaseError is UnmappedDatabaseError diff --git a/zensical.toml b/zensical.toml index becaee26..b84763f7 100644 --- a/zensical.toml +++ b/zensical.toml @@ -59,6 +59,8 @@ nav = [ { "Edge Extractor" = "api/edge_extractor.md" }, { "Helper Classes" = "api/helper_classes.md" }, { Utils = "api/utils.md" }, + { "Database metadata" = "api/database_metadata.md" }, + { Errors = "api/errors.md" }, ]}, ]