From 2448ab2384ae7706992f236fc0b806fc34f8fed7 Mon Sep 17 00:00:00 2001 From: romainsacchi Date: Thu, 11 Jul 2024 21:48:45 +0200 Subject: [PATCH] Add battery update function --- dev/Untitled1.ipynb | 92 +++++++--- premise/activity_maps.py | 42 +++-- premise/battery.py | 157 ++++++++++++++++++ premise/data/battery/energy_density.yaml | 65 ++++++++ premise/data/utils/logging/logconfig.yaml | 13 ++ premise/data/utils/logging/reporting.yaml | 43 +++++ .../transport_roadfreight_variables.yaml | 1 - premise/new_database.py | 2 + premise/report.py | 1 + premise/transport.py | 16 +- 10 files changed, 385 insertions(+), 47 deletions(-) create mode 100644 premise/battery.py create mode 100644 premise/data/battery/energy_density.yaml diff --git a/dev/Untitled1.ipynb b/dev/Untitled1.ipynb index 72e691e6..762517d1 100644 --- a/dev/Untitled1.ipynb +++ b/dev/Untitled1.ipynb @@ -80,13 +80,6 @@ "- Extracting source database\n", "- Extracting inventories\n", "- Fetching IAM data\n", - "dict_items([('train, electric', 'ES|Transport|Freight|Rail|Electric'), ('train, diesel-electric', ['ES|Transport|Freight|Rail|Liquids|Fossil', 'ES|Transport|Freight|Rail|Liquids|Biomass']), ('train, fuel cell', 'ES|Transport|Freight|Rail|Liquids|Hydrogen')])\n", - "1113 set()\n", - "1113 set()\n", - "1113 set()\n", - "1113 set()\n", - "1113 set()\n", - "1113 set()\n", "Done!\n" ] } @@ -112,35 +105,83 @@ { "cell_type": "code", "execution_count": 5, - "id": "3e88c655-ddb1-409d-b84f-c2c4e1976542", + "id": "0c80994c-cbac-4143-81ee-1de1531a6f95", "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Processing scenarios: 100%|█████████████| 1/1 [01:37<00:00, 97.61s/it]" + ] + }, { "name": "stdout", "output_type": "stream", "text": [ - "Generate scenario report.\n", - "Report saved under /Users/romain/GitHub/premise/dev/export/scenario_report.\n" + "Done!\n", + "\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" ] } ], "source": [ - "ndb.generate_scenario_report()" + "ndb.update()" ] }, { "cell_type": "code", - "execution_count": null, - "id": "0c80994c-cbac-4143-81ee-1de1531a6f95", + "execution_count": 6, + "id": "3b0e71e7-2fbb-4247-abcf-60ebb1909d4a", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "gearbox, for lorry 201.6\n", + "transmission, for lorry 291.2\n", + "treatment of used lorry, 40 metric ton -1\n", + "market for power distribution unit, for electric passenger car 4\n", + "assembly operation, for lorry 29223.50287857302\n", + "market for converter, for electric passenger car 25\n", + "market for inverter, for electric passenger car 41.19999946653843\n", + "suspension, for lorry 3440\n", + "power electronics, for lorry 265\n", + "tires and wheels, for lorry 1422\n", + "market for battery management system, for Li-ion battery 125.73132358067981\n", + "glider lightweighting 268.7599962415552\n", + "cabin, for lorry 1153\n", + "other components, for electric lorry 1059\n", + "maintenance, lorry 40 metric ton 1\n", + "frame, blanks and saddle, for lorry 5539\n", + "market for electric motor, electric passenger car 600\n", + "market for used Li-ion battery -314.32832768712774\n", + "retarder, for lorry 67.2\n", + "market for battery cell, Li-ion, NMC622 188.597004106448\n", + "heavy duty truck, battery electric, NMC-622 battery, 40t gross weight, long haul 1\n" + ] + } + ], "source": [ - "ndb.update()" + "from premise.utils import load_database\n", + "ndb.scenarios[0] = load_database(ndb.scenarios[0])\n", + "\n", + "for ds in ndb.scenarios[0][\"database\"]:\n", + " if ds[\"name\"] == \"heavy duty truck, battery electric, NMC-622 battery, 40t gross weight, long haul\":\n", + " for e in ds[\"exchanges\"]:\n", + " print(e[\"name\"], e[\"amount\"])" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "dff4efb1-69ab-4bd5-8d52-4038df180a85", "metadata": {}, "outputs": [ @@ -159,7 +200,7 @@ "text": [ "Writing activities to SQLite3 database:\n", "0% [##############################] 100% | ETA: 00:00:00\n", - "Total time elapsed: 00:00:32\n" + "Total time elapsed: 00:00:28\n" ] }, { @@ -167,16 +208,21 @@ "output_type": "stream", "text": [ "Title: Writing activities to SQLite3 database:\n", - " Started: 07/01/2024 17:19:47\n", - " Finished: 07/01/2024 17:20:20\n", - " Total time elapsed: 00:00:32\n", - " CPU %: 94.20\n", - " Memory %: 15.94\n" + " Started: 07/11/2024 21:39:26\n", + " Finished: 07/11/2024 21:39:54\n", + " Total time elapsed: 00:00:28\n", + " CPU %: 92.10\n", + " Memory %: 19.69\n", + "Created database: trspt 4\n", + "Generate scenario report.\n", + "Report saved under /Users/romain/GitHub/premise/dev/export/scenario_report.\n", + "Generate change report.\n", + "Report saved under /Users/romain/GitHub/premise/dev.\n" ] } ], "source": [ - "ndb.write_db_to_brightway()" + "ndb.write_db_to_brightway(\"trspt 4\")" ] }, { diff --git a/premise/activity_maps.py b/premise/activity_maps.py index 5b435767..3023ef69 100644 --- a/premise/activity_maps.py +++ b/premise/activity_maps.py @@ -275,54 +275,66 @@ def generate_transport_map(self, transport_type: str) -> dict: Rerurns a dictionary with transport type as keys (see below) and a set of related ecoinvent activities' names as values. """ + mapping = {} if transport_type == "car": - return self.generate_sets_from_filters( - get_mapping(filepath=PASSENGER_CARS, var="ecoinvent_aliases") + mapping = self.generate_sets_from_filters( + get_mapping(filepath=PASSENGER_CARS, var="ecoinvent_aliases", model=self.model) ) elif transport_type == "two-wheeler": - return self.generate_sets_from_filters( - get_mapping(filepath=TWO_WHEELERS, var="ecoinvent_aliases") + mapping = self.generate_sets_from_filters( + get_mapping(filepath=TWO_WHEELERS, var="ecoinvent_aliases", model=self.model) ) elif transport_type == "bus": - return self.generate_sets_from_filters( - get_mapping(filepath=BUSES, var="ecoinvent_aliases") + mapping = self.generate_sets_from_filters( + get_mapping(filepath=BUSES, var="ecoinvent_aliases", model=self.model) ) elif transport_type == "truck": - return self.generate_sets_from_filters( - get_mapping(filepath=TRUCKS, var="ecoinvent_aliases") + mapping = self.generate_sets_from_filters( + get_mapping(filepath=TRUCKS, var="ecoinvent_aliases", model=self.model) ) elif transport_type == "train": - return self.generate_sets_from_filters( - get_mapping(filepath=TRAINS, var="ecoinvent_aliases") + mapping = self.generate_sets_from_filters( + get_mapping(filepath=TRAINS, var="ecoinvent_aliases", model=self.model) ) + # remove empty values + mapping = {key: val for key, val in mapping.items() if len(val) > 0} + + return mapping + def generate_vehicle_fuel_map(self, transport_type: str) -> dict: """ Filter ecoinvent processes related to transport fuels. Rerurns a dictionary with transport type as keys (see below) and a set of related ecoinvent activities' names as values. """ + mapping = {} if transport_type == "car": - return self.generate_sets_from_filters( + mapping = self.generate_sets_from_filters( get_mapping(filepath=PASSENGER_CARS, var="ecoinvent_fuel_aliases") ) elif transport_type == "two-wheeler": - return self.generate_sets_from_filters( + mapping = self.generate_sets_from_filters( get_mapping(filepath=TWO_WHEELERS, var="ecoinvent_fuel_aliases") ) elif transport_type == "bus": - return self.generate_sets_from_filters( + mapping = self.generate_sets_from_filters( get_mapping(filepath=BUSES, var="ecoinvent_fuel_aliases") ) elif transport_type == "truck": - return self.generate_sets_from_filters( + mapping = self.generate_sets_from_filters( get_mapping(filepath=TRUCKS, var="ecoinvent_fuel_aliases") ) elif transport_type == "train": - return self.generate_sets_from_filters( + mapping = self.generate_sets_from_filters( get_mapping(filepath=TRAINS, var="ecoinvent_fuel_aliases") ) + # remove empty values + mapping = {key: val for key, val in mapping.items() if len(val) > 0} + + return mapping + def generate_sets_from_filters(self, filtr: dict, database=None) -> dict: """ Generate a dictionary with sets of activity names for diff --git a/premise/battery.py b/premise/battery.py new file mode 100644 index 00000000..ac04a371 --- /dev/null +++ b/premise/battery.py @@ -0,0 +1,157 @@ +""" +module to adjust the battery inputs to reflect progress in +terms of cell energy density. + +""" + +import yaml + +from .logger import create_logger +from .transformation import ( + BaseTransformation, + IAMDataCollection, + List, + np, + ws, +) +from .filesystem_constants import DATA_DIR + +logger = create_logger("battery") + + +def load_cell_energy_density(): + """ + Load cell energy density data. + """ + with open(DATA_DIR / "battery/energy_density.yaml", "r") as file: + data = yaml.load(file, Loader=yaml.FullLoader) + + return {x["ecoinvent_aliases"]["name"]: x["target"] for x in data.values()} + + +def _update_battery(scenario, version, system_model): + battery = Battery( + database=scenario["database"], + iam_data=scenario["iam data"], + model=scenario["model"], + pathway=scenario["pathway"], + year=scenario["year"], + version=version, + system_model=system_model, + cache=scenario.get("cache"), + index=scenario.get("index"), + ) + + battery.adjust_battery_mass() + + scenario["database"] = battery.database + scenario["index"] = battery.index + scenario["cache"] = battery.cache + + return scenario + + +class Battery(BaseTransformation): + """ + Class that modifies the battery market to reflect progress + in terms of cell energy density. + + """ + + def __init__( + self, + database: List[dict], + iam_data: IAMDataCollection, + model: str, + pathway: str, + year: int, + version: str, + system_model: str, + cache: dict = None, + index: dict = None, + ) -> None: + super().__init__( + database, + iam_data, + model, + pathway, + year, + version, + system_model, + cache, + index, + ) + self.system_model = system_model + + def adjust_battery_mass(self) -> None: + """ + Adjust vehicle components (e.g., battery). + Adjust the battery mass to reflect progress in battery technology. + Specifically, we adjust the battery mass to reflect progress in + terms of cell energy density. + We leave the density unchanged after 2050. + """ + + energy_density = load_cell_energy_density() + + filters = [ + ws.contains("name", x) + for x in energy_density + ] + + for ds in ws.get_many( + self.database, + ws.exclude( + ws.either( + *[ + ws.contains("name", x) + for x in [ + "market for battery", + "battery production", + "battery cell production", + "cell module production", + ] + ] + ) + ), + ): + + for exc in ws.technosphere( + ds, ws.either(*filters) + ): + name = [x for x in energy_density if x in exc["name"]][0] + + scaling_factor = ( + energy_density[name][2020] / np.clip( + np.interp( + self.year, + list(energy_density[name].keys()), + list(energy_density[name].values()), + ), + 0.1, + 0.5 + ) + ) + + if "log parameters" not in ds: + ds["log parameters"] = {} + + ds["log parameters"]["battery input"] = exc["name"] + ds["log parameters"]["old battery mass"] = exc["amount"] + exc["amount"] *= scaling_factor + ds["log parameters"]["new battery mass"] = exc["amount"] + + self.write_log(ds, status="modified") + + def write_log(self, dataset, status="created"): + """ + Write log file. + """ + + logger.info( + f"{status}|{self.model}|{self.scenario}|{self.year}|" + f"{dataset['name']}|{dataset['location']}|" + f"{dataset.get('log parameters', {}).get('battery input', '')}|" + f"{dataset.get('log parameters', {}).get('old battery mass', '')}|" + f"{dataset.get('log parameters', {}).get('new battery mass', '')}" + ) diff --git a/premise/data/battery/energy_density.yaml b/premise/data/battery/energy_density.yaml new file mode 100644 index 00000000..7a43f234 --- /dev/null +++ b/premise/data/battery/energy_density.yaml @@ -0,0 +1,65 @@ +# Battery cell energy densities, in kWh/kg cell +--- + +Li-ion: + ecoinvent_aliases: + name: market for battery, Li-ion + target: + 2020: 0.2 + 2050: 0.5 + +NMC111: + ecoinvent_aliases: + name: market for battery cell, Li-ion, NMC111 + target: + 2020: 0.2 + 2050: 0.5 + +NMC622: + ecoinvent_aliases: + name: market for battery cell, Li-ion, NMC622 + target: + 2020: 0.2 + 2050: 0.5 + +NMC811: + ecoinvent_aliases: + name: market for battery cell, Li-ion, NMC811 + target: + 2020: 0.2 + 2050: 0.5 + +NCA: + ecoinvent_aliases: + name: market for battery cell, Li-ion, NCA + target: + 2020: 0.2 + 2050: 0.5 + +LFP: + ecoinvent_aliases: + name: market for battery cell, Li-ion, LFP + target: + 2020: 0.2 + 2050: 0.5 + +LTO: + ecoinvent_aliases: + name: market for battery cell, Li-ion, LTO + target: + 2020: 0.2 + 2050: 0.5 + +LiMn2O4: + ecoinvent_aliases: + name: market for battery cell, Li-ion, LiMn2O4 + target: + 2020: 0.2 + 2050: 0.5 + +BoP: + ecoinvent_aliases: + name: market for battery management system + target: + 2020: 0.2 + 2050: 0.5 diff --git a/premise/data/utils/logging/logconfig.yaml b/premise/data/utils/logging/logconfig.yaml index 0037baa8..a4b328c8 100644 --- a/premise/data/utils/logging/logconfig.yaml +++ b/premise/data/utils/logging/logconfig.yaml @@ -72,6 +72,14 @@ handlers: formatter: simple mode: a + file_battery: + class: logging.FileHandler + level: INFO + filename: "export/logs/premise_battery.log" + encoding: utf8 + formatter: simple + mode: a + file_transport: class: logging.FileHandler level: INFO @@ -140,6 +148,11 @@ loggers: handlers: [ file_fuel ] propagate: False + battery: + level: INFO + handlers: [ file_battery ] + propagate: False + heat: level: INFO handlers: [ file_heat ] diff --git a/premise/data/utils/logging/reporting.yaml b/premise/data/utils/logging/reporting.yaml index 74392c39..c13e23cc 100644 --- a/premise/data/utils/logging/reporting.yaml +++ b/premise/data/utils/logging/reporting.yaml @@ -462,6 +462,49 @@ premise_heat: unit: kg CO2/reference flow tab: Heat +premise_battery: + columns: + timestamp: + name: timestamp + description: Timestamp of the log entry + module: + name: module + description: Module name + level: + name: level + description: Log level + status: + name: status + description: Status of the dataset + model: + name: model + description: IAM model name + pathway: + name: pathway + description: Pathway name + year: + name: year + description: Year + dataset: + name: dataset + description: Dataset name + region: + name: region + description: Region name + battery input: + name: Battery provider + description: Name of the battery provider + unit: unitless + old battery mass: + name: Old battery mass + description: Mass of the battery before adjustment + unit: kg + new battery mass: + name: New battery mass + description: Mass of the battery after adjustment + unit: kg + tab: Battery + premise_transport: columns: timestamp: diff --git a/premise/iam_variables_mapping/transport_roadfreight_variables.yaml b/premise/iam_variables_mapping/transport_roadfreight_variables.yaml index bcf6be71..07f1d483 100644 --- a/premise/iam_variables_mapping/transport_roadfreight_variables.yaml +++ b/premise/iam_variables_mapping/transport_roadfreight_variables.yaml @@ -132,7 +132,6 @@ truck, fuel cell electric, 18t: fltr: - hydrogen - truck, diesel, 18t: iam_aliases: remind: ES|Transport|Freight|Road|Truck (18t)|Liquids diff --git a/premise/new_database.py b/premise/new_database.py index 33ec9267..6be589af 100644 --- a/premise/new_database.py +++ b/premise/new_database.py @@ -22,6 +22,7 @@ from .data_collection import IAMDataCollection from .direct_air_capture import _update_dac from .electricity import _update_electricity +from .battery import _update_battery from .emissions import _update_emissions from .export import ( Export, @@ -867,6 +868,7 @@ def update(self, sectors: [str, list, None] = None) -> None: "steel": {"func": _update_steel, "args": (self.version, self.system_model)}, "fuels": {"func": _update_fuels, "args": (self.version, self.system_model)}, "heat": {"func": _update_heat, "args": (self.version, self.system_model)}, + "battery": {"func": _update_battery, "args": (self.version, self.system_model)}, "emissions": { "func": _update_emissions, "args": (self.version, self.system_model, self.gains_scenario), diff --git a/premise/report.py b/premise/report.py index 5073cf15..fbc89ba1 100644 --- a/premise/report.py +++ b/premise/report.py @@ -568,6 +568,7 @@ def generate_change_report(source, version, source_type, system_model): "premise_electricity", "premise_fuel", "premise_heat", + "premise_battery", "premise_transport", "premise_steel", "premise_metal", diff --git a/premise/transport.py b/premise/transport.py index 8eb36c08..749ed658 100644 --- a/premise/transport.py +++ b/premise/transport.py @@ -10,6 +10,7 @@ import xarray as xr import yaml from wurst import searching as ws +import numpy as np from .activity_maps import InventorySet from .filesystem_constants import DATA_DIR, IAM_OUTPUT_DIR @@ -29,7 +30,6 @@ def _update_vehicles(scenario, vehicle_type, version, system_model): - has_fleet = False if vehicle_type == "car": if hasattr(scenario["iam data"], "passenger_car_markets"): @@ -485,14 +485,13 @@ def create_vehicle_markets(self) -> list: if (name, loc) in list_created_trucks: exc["name"] = name else: - exc["name"] = ( - f"transport, freight, lorry, unspecified, long haul" - ) - + exc[ + "name" + ] = f"transport, freight, lorry, unspecified, long haul" else: - exc["name"] = ( - "transport, freight, lorry, unspecified, long haul" - ) + exc[ + "name" + ] = "transport, freight, lorry, unspecified, long haul" exc["product"] = "transport, freight, lorry" exc["location"] = self.geo.ecoinvent_to_iam_location( @@ -552,6 +551,7 @@ def adjust_transport_efficiency(self, dataset): return dataset + def write_log(self, dataset, status="created"): """ Write log file.