From d506706b8171774cc6ad4ccc539f2f331aa6e130 Mon Sep 17 00:00:00 2001 From: Christina Ludwig Date: Sun, 28 Aug 2022 22:06:06 +0200 Subject: [PATCH 01/19] temporarily replace TrainStation with StructuredLocation --- co2calculator/distances.py | 27 ++++++++++++----- tests/unit/test_calculate.py | 57 ++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 8 deletions(-) diff --git a/co2calculator/distances.py b/co2calculator/distances.py index 90e0034..e0d5776 100644 --- a/co2calculator/distances.py +++ b/co2calculator/distances.py @@ -11,12 +11,14 @@ import numpy as np import openrouteservice import pandas as pd +import pydantic from dotenv import load_dotenv from openrouteservice.directions import directions from openrouteservice.geocode import pelias_search, pelias_structured from pydantic import BaseModel, ValidationError, Extra, confloat from thefuzz import fuzz from thefuzz import process +from iso3166 import countries from ._types import Kilometer from .constants import TransportationMode, CountryCode2, CountryCode3, CountryName @@ -31,7 +33,7 @@ detour_df = pd.read_csv(f"{script_path}/../data/detour.csv") -class StructuredLocation(BaseModel, extra=Extra.forbid): +class StructuredLocation(BaseModel, extra=Extra.ignore): address: Optional[str] locality: str country: Union[CountryCode2, CountryCode3, CountryName] @@ -42,10 +44,19 @@ class StructuredLocation(BaseModel, extra=Extra.forbid): neighbourhood: Optional[str] -class TrainStation(BaseModel): +class TrainStation(BaseModel, extra=Extra.ignore): station_name: str - country: CountryCode2 + country_code: CountryCode2 = None + country: CountryName = None + @pydantic.root_validator(pre=False) + def get_country_code(cls, values): + if values["country_code"] is None: + try: + values["country_code"] = countries.get(values["country"]).alpha2 + except KeyError as e: + raise ValueError(f"Invalid country: {values['country']}.") + return values class Airport(BaseModel): iata_code: str # NOTE: Could be improved with validation of IATA codes @@ -274,7 +285,7 @@ def geocoding_train_stations(loc_dict): # remove stations with no coordinates stations_df.dropna(subset=["latitude", "longitude"], inplace=True) countries_eu = stations_df["country"].unique() - country_code = station.country + country_code = station.country_code if country_code not in countries_eu: warnings.warn( "The provided country is not within Europe. " @@ -393,8 +404,8 @@ def create_distance_request( if transportation_mode in [TransportationMode.TRAIN]: return DistanceRequest( transportation_mode=transportation_mode, - start=TrainStation(**start), - destination=TrainStation(**destination), + start=StructuredLocation(**start), + destination=StructuredLocation(**destination), ) if transportation_mode in [TransportationMode.PLANE]: @@ -463,8 +474,8 @@ def get_distance(request: DistanceRequest) -> Kilometer: for loc in [request.start, request.destination]: try: - _, _, loc_coords = geocoding_train_stations(loc.dict()) - except RuntimeWarning: + # _, _, loc_coords = geocoding_train_stations(loc.dict()) + #except RuntimeWarning: _, _, loc_coords, _ = geocoding_structured(loc.dict()) except ValueError: _, _, loc_coords, _ = geocoding_structured(loc.dict()) diff --git a/tests/unit/test_calculate.py b/tests/unit/test_calculate.py index a2bea5b..7bd9e48 100644 --- a/tests/unit/test_calculate.py +++ b/tests/unit/test_calculate.py @@ -51,6 +51,62 @@ def test_calc_co2_car( assert round(actual_emissions, 2) == expected_emissions +@pytest.mark.parametrize( + "start_address,start_city,start_country,destination_address,destination_city,destination_country,transportation_mode,passengers,size,fuel_type,expected_emissions", + [ + pytest.param(None, "Heidelberg", "Germany", None, "Berlin", "Germany", "car", None, None, None, 137.88, id="car"), + pytest.param("Augsburg Hochzoll", "Augsburg", "Germany", "Berlin Gesundbrunnen", "Berlin", "Germany", "train", None, None, None, 19.54, id="train"), + #pytest.param(10, 1, "small", None, 1.79, id="size: 'small'"), + #pytest.param(10, 1, "medium", None, 2.09, id="size: 'medium'"), + #pytest.param(10, 1, "large", None, 2.74, id="size: 'large'"), + #pytest.param(10, 1, "average", None, 2.15, id="size: 'average'"), + #pytest.param(10, 1, None, "diesel", 2.01, id="fuel_type: 'diesel'"), + ##pytest.param(10, 1, None, "gasoline", 2.24, id="fuel_type: 'gasoline'"), + # pytest.param(10, 1, None, "cng", 31.82, id="fuel_type: 'cng'"), + #pytest.param(10, 1, None, "electric", 0.57, id="fuel_type: 'electric'"), + #pytest.param(10, 1, None, "hybrid", 1.16, id="fuel_type: 'hybrid'"), + #pytest.param( + # 10, 1, None, "plug-in_hybrid", 0.97, id="fuel_type: 'plug-in_hybrid'" + #), + #pytest.param(10, 1, None, "average", 2.15, id="fuel_type: 'average'"), + ], +) +def test_calc_co2_busincesstrip( + start_address: str, + start_city: str, + start_country: str, + destination_address: str, + destination_city: str, + destination_country: str, + transportation_mode: str, + passengers: Optional[int], + size: Optional[str], + fuel_type: Optional[str], + expected_emissions: float, +): + """Test: Calculate car-trip emissions based on given distance. + Expect: Returns emissions and distance. + """ + start = {"station_name": start_address, + "address": start_address, + "locality": start_city, + "country": start_country} + destination = {"station_name": destination_address, + "address": destination_address, + "locality": destination_city, + "country": destination_country} + actual_emissions, _, _, _ = candidate.calc_co2_businesstrip( + transportation_mode=transportation_mode, + start=start, + destination=destination, + passengers=passengers, + size=size, + fuel_type=fuel_type, + ) + + assert round(actual_emissions, 2) == expected_emissions + + @pytest.mark.parametrize( "distance,size,expected_emissions", [ @@ -364,3 +420,4 @@ def test_calc_co2_businesstrip( ) patched_method.assert_called_once() + From a612565baf8e573197b31f358cea1ed51bfa2e0c Mon Sep 17 00:00:00 2001 From: Christina Ludwig Date: Wed, 7 Sep 2022 20:26:40 +0200 Subject: [PATCH 02/19] added reader class and enums --- co2calculator/__init__.py | 2 + co2calculator/calculate.py | 329 ++++++++++++++++++---------------- co2calculator/constants.py | 12 ++ co2calculator/distances.py | 55 +++--- co2calculator/enums.py | 104 +++++++++++ co2calculator/exceptions.py | 13 ++ co2calculator/parameters.py | 137 ++++++++++++++ co2calculator/reader.py | 47 +++++ data/emission_factors.csv | 36 ++-- tests/unit/test_parameters.py | 55 ++++++ tests/unit/test_reader.py | 45 +++++ 11 files changed, 634 insertions(+), 201 deletions(-) create mode 100644 co2calculator/enums.py create mode 100644 co2calculator/exceptions.py create mode 100644 co2calculator/parameters.py create mode 100644 co2calculator/reader.py create mode 100644 tests/unit/test_parameters.py create mode 100644 tests/unit/test_reader.py diff --git a/co2calculator/__init__.py b/co2calculator/__init__.py index 611b751..cced51e 100644 --- a/co2calculator/__init__.py +++ b/co2calculator/__init__.py @@ -1 +1,3 @@ from .calculate import * + + diff --git a/co2calculator/calculate.py b/co2calculator/calculate.py index 6c20bd0..23379d0 100644 --- a/co2calculator/calculate.py +++ b/co2calculator/calculate.py @@ -2,26 +2,19 @@ # coding: utf-8 """Functions to calculate co2 emissions""" -import warnings -from pathlib import Path from typing import Tuple - -import pandas as pd - from ._types import Kilogram, Kilometer from .constants import ( KWH_TO_TJ, - Size, - CarBusFuel, - BusTrainRange, - FlightClass, - FlightRange, FerryClass, ElectricityFuel, - TransportationMode, RangeCategory ) -from .distances import create_distance_request, get_distance +from .distances import create_distance_request, get_distance, DistanceRequest, StructuredLocation +from .parameters import CarEmissionParameters, BusEmissionParameters, TrainEmissionParameters, PlaneEmissionParameters +from .reader import Reader +from .enums import * + script_path = str(Path(__file__).parent) emission_factor_df = pd.read_csv(f"{script_path}/../data/emission_factors.csv") @@ -30,6 +23,8 @@ ) detour_df = pd.read_csv(f"{script_path}/../data/detour.csv") +reader = Reader() + def calc_co2_car( distance: Kilometer, @@ -54,31 +49,30 @@ def calc_co2_car( :rtype: Kilogram """ # NOTE: Tests fail for 'cng' as `fuel_type` (IndexError) - - transport_mode = TransportationMode.CAR - # Set default values + # params = {} if passengers is None: - passengers = 1 - warnings.warn( - f"Number of car passengers was not provided. Using default value: '{passengers}'" - ) - if size is None: - size = Size.AVERAGE - warnings.warn(f"Size of car was not provided. Using default value: '{size}'") - if fuel_type is None: - fuel_type = CarBusFuel.AVERAGE - warnings.warn( - f"Car fuel type was not provided. Using default value: '{fuel_type}'" - ) + passengers = 1 + #warnings.warn( + # f"Number of car passengers was not provided. Using default value: '{passengers}'" + #) + # if size is not None: + # params["size"] = size + # #warnings.warn(f"Size of car was not provided. Using default value: '{size}'") + # if fuel_type is not None: + # params["fuel_type"] = fuel_type + # #fuel_type = CarBusFuel.AVERAGE + # #warnings.warn( + # # f"Car fuel type was not provided. Using default value: '{fuel_type}'" + # #) + + params = locals() + params = {k: v for k, v in params.items() if v is not None} + params = CarEmissionParameters(**params) + co2e = reader.get_emission_factor(params.dict()) # Get the co2 factor, calculate and return - co2e = emission_factor_df[ - (emission_factor_df["subcategory"] == transport_mode) - & (emission_factor_df["size_class"] == size) - & (emission_factor_df["fuel_type"] == fuel_type) - ]["co2e"].values[0] - emissions = distance * co2e / passengers + emissions = distance * co2e / int(passengers) return emissions @@ -96,18 +90,18 @@ def calc_co2_motorbike(distance: Kilometer = None, size: str = None) -> Kilogram :rtype: Kilogram """ - transport_mode = TransportationMode.MOTORBIKE + transport_mode = TransportationMode.Motorbike # Set default values - if size is None: - size = Size.AVERAGE - warnings.warn( - f"Size of motorbike was not provided. Using default value: '{size}'" - ) + #if size is None: + # size = Size.AVERAGE + #warnings.warn( + # f"Size of motorbike was not provided. Using default value: '{size}'" + #) co2e = emission_factor_df[ (emission_factor_df["subcategory"] == transport_mode) - & (emission_factor_df["size_class"] == size) + & (emission_factor_df["size"] == size) ]["co2e"].values[0] emissions = distance * co2e @@ -137,40 +131,46 @@ def calc_co2_bus( :rtype: Kilogram """ # NOTE: vehicle_rage 'local' fails with IndexError - - transport_mode = TransportationMode.BUS - # Set default values - if size is None: - size = Size.AVERAGE - warnings.warn(f"Size of bus was not provided. Using default value: '{size}'") - if fuel_type is None: - fuel_type = CarBusFuel.DIESEL - warnings.warn( - f"Bus fuel type was not provided. Using default value: '{fuel_type}'" - ) - elif fuel_type not in [CarBusFuel.DIESEL, CarBusFuel.CNG, CarBusFuel.HYDROGEN]: - warnings.warn( - f"Bus fuel type {fuel_type} not available. Using default value: 'diesel'" - ) - fuel_type = "diesel" - if occupancy is None: - occupancy = 50 - warnings.warn(f"Occupancy was not provided. Using default value: '{occupancy}'") - if vehicle_range is None: - vehicle_range = BusTrainRange.LONG_DISTANCE - warnings.warn( - f"Intended range of trip was not provided. Using default value: '{vehicle_range}'" - ) + #if size is not None: + #size = Size.AVERAGE + # params["size"] = size + #warnings.warn(f"Size of bus was not provided. Using default value: '{size}'") + # if fuel_type is not None: + # params["fuel_type"] = fuel_type + #fuel_type = CarBusFuel.DIESEL + #warnings.warn( + # f"Bus fuel type was not provided. Using default value: '{fuel_type}'" + #) + #elif fuel_type not in [CarBusFuel.DIESEL, CarBusFuel.CNG, CarBusFuel.HYDROGEN]: + # warnings.warn( + # f"Bus fuel type {fuel_type} not available. Using default value: 'diesel'" + # ) + # fuel_type = "diesel" + #if occupancy is not None: + # params["occupancy"] = occupancy + # occupancy = "c_50" + #warnings.warn(f"Occupancy was not provided. Using default value: '{occupancy}'") + #if vehicle_range is not None: + # params["vehicle_range"] = vehicle_range + #vehicle_range = BusTrainRange.LONG_DISTANCE + #warnings.warn( + # f"Intended range of trip was not provided. Using default value: '{vehicle_range}'" + #) + params = locals() + params = {k: v for k, v in params.items() if v is not None} + + params = BusEmissionParameters(**params) + co2e = reader.get_emission_factor(params.dict()) # Get co2 factor, calculate and return - co2e = emission_factor_df[ - (emission_factor_df["subcategory"] == transport_mode) - & (emission_factor_df["size_class"] == size) - & (emission_factor_df["fuel_type"] == fuel_type) - & (emission_factor_df["occupancy"] == occupancy) - & (emission_factor_df["range"] == vehicle_range) - ]["co2e"].values[0] + #co2e = emission_factor_df[ + # (emission_factor_df["subcategory"] == transport_mode) + # & (emission_factor_df["size"] == size) + # & (emission_factor_df["fuel_type"] == fuel_type) + # & (emission_factor_df["occupancy"] == occupancy) + # & (emission_factor_df["range"] == vehicle_range) + #]["co2e"].values[0] emissions = distance * co2e return emissions @@ -192,33 +192,36 @@ def calc_co2_train( :return: Total emissions of trip in co2 equivalents :rtype: Kilogram """ - - transport_mode = TransportationMode.TRAIN - # Set default values - if fuel_type is None: - fuel_type = CarBusFuel.AVERAGE - warnings.warn( - f"Car fuel type was not provided. Using default value: '{fuel_type}'" - ) - if vehicle_range is None: - vehicle_range = BusTrainRange.LONG_DISTANCE - warnings.warn( - f"Intended range of trip was not provided. Using default value: '{vehicle_range}'" - ) - + #if fuel_type is None: + + #fuel_type = CarBusFuel.AVERAGE + #warnings.warn( + # f"Car fuel type was not provided. Using default value: '{fuel_type}'" + #) + #if vehicle_range is None: + # #vehicle_range = BusTrainRange.LONG_DISTANCE + # warnings.warn( + # f"Intended range of trip was not provided. Using default value: '{vehicle_range}'" + # ) + + params = locals() + params = {k: v for k, v in params.items() if v is not None} + params = TrainEmissionParameters(**params) + co2e = reader.get_emission_factor(params.dict()) + print(co2e) # Get the co2 factor, calculate and return - co2e = emission_factor_df[ - (emission_factor_df["subcategory"] == transport_mode) - & (emission_factor_df["fuel_type"] == fuel_type) - & (emission_factor_df["range"] == vehicle_range) - ]["co2e"].values[0] + #co2e = emission_factor_df[ + # (emission_factor_df["subcategory"] == transport_mode) + # & (emission_factor_df["fuel_type"] == fuel_type) + # & (emission_factor_df["range"] == vehicle_range) + #]["co2e"].values[0] emissions = distance * co2e - + print(emissions) return emissions -def calc_co2_plane(distance: Kilometer, seating_class: str = None) -> Kilogram: +def calc_co2_plane(distance: Kilometer, seating: str = None) -> Kilogram: """ Function to compute emissions of a plane trip :param distance: Distance of plane flight @@ -232,53 +235,60 @@ def calc_co2_plane(distance: Kilometer, seating_class: str = None) -> Kilogram: :rtype: Kilogram """ - transport_mode = TransportationMode.PLANE + #transport_mode = TransportationMode.Plane # Set defaults - if seating_class is None: - seating_class = FlightClass.AVERAGE - warnings.warn( - f"Seating class was not provided. Using default value: '{seating_class}'" - ) + #if seating_class is None: + # seating_class = PlaneSeatingClass.Average + # warnings.warn( + # f"Seating class was not provided. Using default value: '{seating_class}'" + # ) + + params = locals() + params = {k: v for k, v in params.items() if v is not None} # Retrieve whether distance is below or above 1500 km if distance <= 1500: - flight_range = FlightRange.SHORT_HAUL - elif distance > 1500: - flight_range = FlightRange.LONG_HAUL + params["range"] = PlaneRange.Short_haul + else: + params["range"] = PlaneRange.Long_haul # NOTE: Should be checked before geocoding and haversine calculation - seating_choices = [item for item in FlightClass] + #seating_choices = [item for item in PlaneSeatingClass] - if seating_class not in seating_choices: - raise ValueError( - f"No emission factor available for the specified seating class '{seating_class}'.\n" - f"Please use one of the following: {seating_choices}" - ) + #if seating_class not in seating_choices: + # raise ValueError( + # f"No emission factor available for the specified seating class '{seating_class}'.\n" + # f"Please use one of the following: {seating_choices}" + # ) + + params = PlaneEmissionParameters(**params) + co2e = reader.get_emission_factor(params.dict()) # Get co2 factor, calculate and return - try: - co2e = emission_factor_df[ - (emission_factor_df["subcategory"] == transport_mode) - & (emission_factor_df["range"] == flight_range) - & (emission_factor_df["seating"] == seating_class) - ]["co2e"].values[0] - except IndexError: - default_seating = FlightClass.ECONOMY - warnings.warn( - f"Seating class '{seating_class}' not available for {flight_range} flights. Switching to " - f"'{default_seating}'..." - ) - co2e = emission_factor_df[ - (emission_factor_df["range"] == flight_range) - & (emission_factor_df["seating"] == default_seating) - ]["co2e"].values[0] + # try: + # + # co2e = emission_factor_df[ + # (emission_factor_df["subcategory"] == transport_mode) + # & (emission_factor_df["range"] == flight_range) + # & (emission_factor_df["seating"] == seating_class) + # ]["co2e"].values[0] + # except IndexError: + # default_seating = FlightClass.ECONOMY + # warnings.warn( + # f"Seating class '{seating_class}' not available for {flight_range} flights. Switching to " + # f"'{default_seating}'..." + # ) + # co2e = emission_factor_df[ + # (emission_factor_df["range"] == flight_range) + # & (emission_factor_df["seating"] == default_seating) + # ]["co2e"].values[0] # multiply emission factor with distance emissions = distance * co2e return emissions -def calc_co2_ferry(distance: Kilometer, seating_class: str = None) -> Kilogram: +def calc_co2_ferry(distance: Kilometer, seating: str = None) -> Kilogram: """ Function to compute emissions of a ferry trip :param distance: Distance of ferry trip @@ -290,18 +300,18 @@ def calc_co2_ferry(distance: Kilometer, seating_class: str = None) -> Kilogram: """ # NOTE: 'Foot passenger' and 'Car passenger' fails with IndexError - transport_mode = TransportationMode.FERRY + transport_mode = TransportationMode.Ferry - if seating_class is None: - seating_class = FerryClass.AVERAGE - warnings.warn( - f"Seating class was not provided. Using default value: '{seating_class}'" - ) + if seating is None: + seating = FerryClass.AVERAGE + #warnings.warn( + # f"Seating class was not provided. Using default value: '{seating_class}'" + #) # get emission factor co2e = emission_factor_df[ (emission_factor_df["subcategory"] == transport_mode) - & (emission_factor_df["seating"] == seating_class) + & (emission_factor_df["seating"] == seating) ]["co2e"].values[0] # multiply emission factor with distance emissions = distance * co2e @@ -326,9 +336,9 @@ def calc_co2_electricity( # Set defaults if fuel_type is None: fuel_type = ElectricityFuel.GERMAN_ENERGY_MIX - warnings.warn( - f"No fuel type or energy mix specified. Using default value: '{fuel_type}'" - ) + #warnings.warn( + # f"No fuel type or energy mix specified. Using default value: '{fuel_type}'" + #) co2e = emission_factor_df[ (emission_factor_df["category"] == "electricity") & (emission_factor_df["fuel_type"] == fuel_type) @@ -359,12 +369,12 @@ def calc_co2_heating( # Set defaults if unit is None: unit = "kWh" - warnings.warn(f"Unit was not provided. Assuming default value: '{unit}'") - if area_share > 1: - warnings.warn( - f"Share of building area must be a float in the interval (0,1], but was set to '{area_share}'\n." - f"The parameter will be set to '1.0' instead" - ) + #warnings.warn(f"Unit was not provided. Assuming default value: '{unit}'") + #if area_share > 1: + #warnings.warn( + # f"Share of building area must be a float in the interval (0,1], but was set to '{area_share}'\n." + # f"The parameter will be set to '1.0' instead" + #) valid_unit_choices = ["kWh", "l", "kg", "m^3"] assert ( unit in valid_unit_choices @@ -451,10 +461,15 @@ def calc_co2_businesstrip( # - If stop-based, calculate distance first, then continue only distance-based if not distance: - request = create_distance_request(start, destination, transportation_mode) + request = DistanceRequest( + transportation_mode=transportation_mode, + start=StructuredLocation(**start), + destination=StructuredLocation(**destination), + ) + #request = create_distance_request(start, destination, transportation_mode) distance = get_distance(request) - if transportation_mode == TransportationMode.CAR: + if transportation_mode == TransportationMode.Car: emissions = calc_co2_car( distance=distance, passengers=passengers, @@ -462,27 +477,27 @@ def calc_co2_businesstrip( fuel_type=fuel_type, ) - elif transportation_mode == TransportationMode.BUS: + elif transportation_mode == TransportationMode.Bus: emissions = calc_co2_bus( distance=distance, size=size, fuel_type=fuel_type, occupancy=occupancy, - vehicle_range="long-distance", + vehicle_range=TrainRange.Long_distance, ) - elif transportation_mode == TransportationMode.TRAIN: + elif transportation_mode == TransportationMode.Train: emissions = calc_co2_train( distance=distance, fuel_type=fuel_type, - vehicle_range="long-distance", + vehicle_range=TrainRange.Long_distance, ) - elif transportation_mode == TransportationMode.PLANE: - emissions = calc_co2_plane(distance, seating_class=seating) + elif transportation_mode == TransportationMode.Plane: + emissions = calc_co2_plane(distance, seating=seating) - elif transportation_mode == TransportationMode.FERRY: - emissions = calc_co2_ferry(distance, seating_class=seating) + elif transportation_mode == TransportationMode.Ferry: + emissions = calc_co2_ferry(distance, seating=seating) else: raise ValueError( @@ -549,7 +564,7 @@ def calc_co2_commuting( """ # get weekly co2e for respective mode of transport - if transportation_mode == TransportationMode.CAR: + if transportation_mode == TransportationMode.Car: weekly_co2e = calc_co2_car( passengers=passengers, size=size, @@ -557,9 +572,9 @@ def calc_co2_commuting( distance=weekly_distance, ) - elif transportation_mode == TransportationMode.MOTORBIKE: + elif transportation_mode == TransportationMode.Motorbike: weekly_co2e = calc_co2_motorbike(size=size, distance=weekly_distance) - elif transportation_mode == TransportationMode.BUS: + elif transportation_mode == TransportationMode.Bus: weekly_co2e = calc_co2_bus( size=size, fuel_type=fuel_type, @@ -568,12 +583,12 @@ def calc_co2_commuting( distance=weekly_distance, ) - elif transportation_mode == TransportationMode.TRAIN: + elif transportation_mode == TransportationMode.Train: weekly_co2e = calc_co2_train( fuel_type=fuel_type, vehicle_range="local", distance=weekly_distance ) - elif transportation_mode == TransportationMode.TRAM: + elif transportation_mode == TransportationMode.Tram: # NOTE: It's recommended to still move such small things to own methods. # (Easier to test and maintain) co2e = emission_factor_df[ @@ -582,8 +597,8 @@ def calc_co2_commuting( weekly_co2e = co2e * weekly_distance elif transportation_mode in [ - TransportationMode.PEDELEC, - TransportationMode.BICYCLE, + TransportationMode.Pedelec, + TransportationMode.Bicycle, ]: # NOTE: It's recommended to still move such small things to own methods. # (Easier to test and maintain) diff --git a/co2calculator/constants.py b/co2calculator/constants.py index b4b40ad..9018333 100644 --- a/co2calculator/constants.py +++ b/co2calculator/constants.py @@ -49,6 +49,18 @@ class CarBusFuel(str, enum.Enum): HYDROGEN = "hydrogen" +@enum.unique +class BusFuel(str, enum.Enum): + """Enum for bus fuel types""" + + ELECTRIC = "electric" + DIESEL = "diesel" + AVERAGE = "average" + HYDROGEN = "hydrogen" + CNG = "cng" + + + @enum.unique class Size(str, enum.Enum): """Enum for car sizes""" diff --git a/co2calculator/distances.py b/co2calculator/distances.py index e0d5776..8d2736f 100644 --- a/co2calculator/distances.py +++ b/co2calculator/distances.py @@ -21,7 +21,8 @@ from iso3166 import countries from ._types import Kilometer -from .constants import TransportationMode, CountryCode2, CountryCode3, CountryName +from .constants import CountryCode2, CountryCode3, CountryName +from .enums import TransportationMode load_dotenv() # take environment variables from .env. @@ -58,6 +59,7 @@ def get_country_code(cls, values): raise ValueError(f"Invalid country: {values['country']}.") return values + class Airport(BaseModel): iata_code: str # NOTE: Could be improved with validation of IATA codes @@ -390,10 +392,10 @@ def create_distance_request( try: if transportation_mode in [ - TransportationMode.CAR, - TransportationMode.MOTORBIKE, - TransportationMode.BUS, - TransportationMode.FERRY, + TransportationMode.Car, + TransportationMode.Motorbike, + TransportationMode.Bus, + TransportationMode.Ferry, ]: return DistanceRequest( transportation_mode=transportation_mode, @@ -401,24 +403,23 @@ def create_distance_request( destination=StructuredLocation(**destination), ) - if transportation_mode in [TransportationMode.TRAIN]: + if transportation_mode in [TransportationMode.Train]: return DistanceRequest( transportation_mode=transportation_mode, start=StructuredLocation(**start), destination=StructuredLocation(**destination), ) - if transportation_mode in [TransportationMode.PLANE]: + if transportation_mode in [TransportationMode.Plane]: return DistanceRequest( transportation_mode=transportation_mode, - start=Airport(iata_code=start), - destination=Airport(iata_code=destination), + start=StructuredLocation(**start), + destination=StructuredLocation(**destination), ) except ValidationError as e: - raise InvalidSpatialInput(e) - - raise InvalidSpatialInput(f"unknown transportation_mode: '{transportation_mode}'") + #raise InvalidSpatialInput(e) + raise InvalidSpatialInput(f"unknown transportation_mode: '{transportation_mode}'") def get_distance(request: DistanceRequest) -> Kilometer: @@ -433,17 +434,17 @@ def get_distance(request: DistanceRequest) -> Kilometer: """ detour_map = { - TransportationMode.CAR: False, - TransportationMode.MOTORBIKE: False, - TransportationMode.BUS: True, - TransportationMode.TRAIN: True, - TransportationMode.PLANE: True, - TransportationMode.FERRY: True, + TransportationMode.Car: False, + TransportationMode.Motorbike: False, + TransportationMode.Bus: True, + TransportationMode.Train: True, + TransportationMode.Plane: True, + TransportationMode.Ferry: True, } if request.transportation_mode in [ - TransportationMode.CAR, - TransportationMode.MOTORBIKE, + TransportationMode.Car, + TransportationMode.Motorbike, ]: coords = [] for loc in [request.start, request.destination]: @@ -451,7 +452,7 @@ def get_distance(request: DistanceRequest) -> Kilometer: coords.append(loc_coords) return get_route(coords, "driving-car") - if request.transportation_mode == TransportationMode.BUS: + if request.transportation_mode == TransportationMode.Bus: # Same as car (StructuredLocation) # TODO: Validate with BaseModel # TODO: Question: Why are we not calculating the bus trip like `driving-car` routes? @@ -467,7 +468,7 @@ def get_distance(request: DistanceRequest) -> Kilometer: ) return _apply_detour(distance, request.transportation_mode) - if request.transportation_mode == TransportationMode.TRAIN: + if request.transportation_mode == TransportationMode.Train: distance = 0 coords = [] @@ -488,17 +489,19 @@ def get_distance(request: DistanceRequest) -> Kilometer: ) return _apply_detour(distance, request.transportation_mode) - if request.transportation_mode == TransportationMode.PLANE: + if request.transportation_mode == TransportationMode.Plane: # Stops are IATA code of airports # TODO: Validate stops with BaseModel - _, geom_start, _ = geocoding_airport(request.start.iata_code) - _, geom_dest, _ = geocoding_airport(request.destination.iata_code) + #_, geom_start, _ = geocoding_airport(request.start.iata_code) + #_, geom_dest, _ = geocoding_airport(request.destination.iata_code) + _, _, geom_start, _ = geocoding_structured(request.start.dict()) + _, _, geom_dest, _ = geocoding_structured(request.destination.dict()) distance = haversine(geom_start[1], geom_start[0], geom_dest[1], geom_dest[0]) return _apply_detour(distance, request.transportation_mode) - if request.transportation_mode == TransportationMode.FERRY: + if request.transportation_mode == TransportationMode.Ferry: # todo: Do we have a way of checking if there even exists a ferry connection between the given cities (or if the # cities even have a port? _, _, geom_start, _ = geocoding_structured(request.start.dict()) diff --git a/co2calculator/enums.py b/co2calculator/enums.py new file mode 100644 index 0000000..75456c1 --- /dev/null +++ b/co2calculator/enums.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Enums holding parameter choices derived from data set""" + +__author__ = "Christina Ludwig, GIScience Research Group, Heidelberg University" +__email__ = "christina.ludwig@uni-heidelberg.de" + +from enum import Enum +import pandas as pd +from pathlib import Path +import numpy as np + +script_path = str(Path(__file__).parent) + + +class EnumCreator: + + def __init__(self): + self.df = pd.read_csv(f"{script_path}/../data/emission_factors.csv") + + def create_enum(self, mode, column, name): + df = self.df[self.df["subcategory"] == mode] + if df[column].dtype == np.float64: + data = df[column].map(lambda x: "c_{:.0f}".format(x)).astype("str") + return Enum(name, {x: float(x[2:]) for x in data}, type=str) + else: + data = df[column].fillna("nan").unique() + return Enum(name, {x.capitalize().replace("-", "_"): x for x in data}, type=str) + + def create_enum_transport(self, column, name): + data = self.df[column].fillna("nan").astype("str").unique() + return Enum(name, {x.capitalize(): x for x in data}, type=str) + + +enum_creator = EnumCreator() +TransportationMode = enum_creator.create_enum_transport("subcategory", "TransportationMode") + +# Fuel types ------------------------------------------------------ + +TrainFuelType = enum_creator.create_enum(TransportationMode.Train, + "fuel_type", + "TrainFuelType") +CarFuelType = enum_creator.create_enum(TransportationMode.Car, + "fuel_type", + "CarFuelType") +BusFuelType = enum_creator.create_enum(TransportationMode.Bus, + "fuel_type", + "BusFuelType") +TramFuelType = enum_creator.create_enum(TransportationMode.Tram, + "fuel_type", "TramFuelType") +PlaneFuelType = enum_creator.create_enum(TransportationMode.Plane, + "fuel_type", + "PlaneFuelType") +FerryFuelType = enum_creator.create_enum(TransportationMode.Ferry, + "fuel_type", + "FerryFuelType") +MotorbikeFuelType = enum_creator.create_enum(TransportationMode.Motorbike, + "fuel_type", + "MotorbikeFuelType" ) +BicycleFuelType = enum_creator.create_enum(TransportationMode.Bicycle, + "fuel_type", + "BicycleFuelType") +PedelecFuelType = enum_creator.create_enum(TransportationMode.Pedelec, + "fuel_type", + "PedelecFuelType") + +# Size +TrainSize = enum_creator.create_enum(TransportationMode.Train, + "size", + "TrainSize") +CarSize = enum_creator.create_enum(TransportationMode.Car, + "size", + "CarSize") +BusSize = enum_creator.create_enum(TransportationMode.Bus, + "size", + "BusSize") + +# Vehicle range +TrainRange = enum_creator.create_enum(TransportationMode.Train, + "range", + "TrainRange") +BusRange = enum_creator.create_enum(TransportationMode.Bus, + "range", + "BusRange") + +PlaneRange = enum_creator.create_enum(TransportationMode.Plane, + "range", + "PlaneRange") + +# Seating class +PlaneSeatingClass = enum_creator.create_enum(TransportationMode.Plane, + "seating", + "PlaneSeatingClass") +FerrySeatingClass = enum_creator.create_enum(TransportationMode.Ferry, + "seating", + "FerrySeatingClass") + +# Occupancy +TrainOccupancy = enum_creator.create_enum(TransportationMode.Train, + "occupancy", + "TrainOccupancy") +BusOccupancy = enum_creator.create_enum( TransportationMode.Bus, + "occupancy", + "BusOccupancy") \ No newline at end of file diff --git a/co2calculator/exceptions.py b/co2calculator/exceptions.py new file mode 100644 index 0000000..a81e995 --- /dev/null +++ b/co2calculator/exceptions.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""__description__""" + +__author__ = "Christina Ludwig, GIScience Research Group, Heidelberg University" +__email__ = "christina.ludwig@uni-heidelberg.de" + + +class ConversionFactorNotFound(Exception): + + def __init__(self, message): + self.message = message + diff --git a/co2calculator/parameters.py b/co2calculator/parameters.py new file mode 100644 index 0000000..df1b76d --- /dev/null +++ b/co2calculator/parameters.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""__description__""" + +from pydantic import BaseModel, validator +from .enums import * +from typing import Union + + +class TrainEmissionParameters(BaseModel): + + subcategory: TransportationMode = TransportationMode.Train + fuel_type: Union[TrainFuelType, str] = TrainFuelType.Average + range: Union[TrainRange, str] = TrainRange.Long_distance + + @validator("fuel_type", allow_reuse=True) + def check_fueltype(cls, v): + v = v.lower() if isinstance(v, str) else v + return TrainFuelType(v) + + @validator("range", allow_reuse=True) + def check_range(cls, v): + v = v.lower() if isinstance(v, str) else v + return TrainRange(v) + + +class CarEmissionParameters(BaseModel): + + subcategory: TransportationMode = TransportationMode.Car + fuel_type: Union[CarFuelType, str] = CarFuelType.Average + size: Union[CarSize, str] = CarSize.Average + + @validator("fuel_type", allow_reuse=True) + def check_fueltype(cls, v): + v = v.lower() if isinstance(v, str) else v + return CarFuelType(v) + + @validator("size", allow_reuse=True) + def check_size(cls, v, values): + v = v.lower() if isinstance(v, str) else v + return CarSize(v) + + +class PlaneEmissionParameters(BaseModel): + + subcategory: TransportationMode = TransportationMode.Plane + seating: Union[PlaneSeatingClass, str] = PlaneSeatingClass.Average + range: Union[PlaneRange, str] + + @validator("range") + def check_range(cls, v): + v = v.lower() if isinstance(v, str) else v + return PlaneRange(v) + + @validator("seating") + def check_seating(cls, v): + v = v.lower() if isinstance(v, str) else v + return PlaneSeatingClass(v) + + +class BusEmissionParameters(BaseModel): + + subcategory: TransportationMode = TransportationMode.Bus + fuel_type: Union[BusFuelType, str] = BusFuelType.Diesel + size: Union[BusSize, str] = BusSize.Average + occupancy: Union[BusOccupancy, str] = BusOccupancy.c_50 + range: Union[BusRange, str] = BusRange.Long_distance + + @validator("fuel_type", allow_reuse=True) + def check_fueltype(cls, v): + v = v.lower() if isinstance(v, str) else v + return BusFuelType(v) + + @validator("size", allow_reuse=True) + def check_size(cls, v): + v = v.lower() if isinstance(v, str) else v + return BusSize(v) + + @validator("range", allow_reuse=True) + def check_range(cls, v): + v = v.lower() if isinstance(v, str) else v + return BusRange(v) + + @validator("occupancy", allow_reuse=True) + def check_occupancy(cls, v): + v = v.lower() if isinstance(v, str) else v + return BusOccupancy(v) + + + + +# class EmissionParameters(BaseModel): +# +# subcategory: Union[TransportationMode, str] +# fuel_type: Union[CarFuelType, BusFuelType, PlaneFuelType, TrainFuelType, str] = None +# size: Union[CarSize, BusSize, TrainSize, str] = None +# occupancy: Union[BusOccupancy, TrainOccupancy, str] = None +# range: Union[BusRange, TrainRange, str] = None +# seating_class: Union[PlaneSeatingClass, FerrySeatingClass, str] = None +# +# @validator("fuel_type", allow_reuse=True) +# def check_fueltype(cls, v, values): +# v = v.lower() if isinstance(v, str) else v +# if v is "average": +# if "Average" not in TrainFuelType.__members__.keys(): +# return eval(f"{t}FuelType('nan')") +# else: +# raise FactorNotFound("Fuel type needs to be provided. No average value found.") +# else: +# return eval(f"{t}FuelType('{v}')") +# +# @validator("size", allow_reuse=True, pre=True) +# def check_size(cls, v, values): +# t = values["subcategory"].lower().capitalize() +# v = v.lower() if isinstance(v, str) else v +# return eval(f"{t}Size('{v}')") +# +# @validator("range", allow_reuse=True) +# def check_range(cls, v, values): +# t = values["subcategory"].lower().capitalize() +# v = v.lower() if isinstance(v, str) else v +# return eval(f"{t}Range('{v}')") +# +# @validator("seating_class", allow_reuse=True) +# def check_seating_class(cls, v, values): +# t = values["subcategory"].lower().capitalize() +# v = v.lower() if isinstance(v, str) else v +# return eval(f"{t}SeatingClass('{v}')") +# +# @validator("occupancy", allow_reuse=True) +# def check_occupancy(cls, v, values): +# t = values["subcategory"].lower().capitalize() +# v = v.lower() if isinstance(v, str) else v +# return eval(f"{t}Occupancy('{v}')") +# +# + diff --git a/co2calculator/reader.py b/co2calculator/reader.py new file mode 100644 index 0000000..b2c3d12 --- /dev/null +++ b/co2calculator/reader.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Reader class to retrieve co2 factors from database""" +import warnings +from pathlib import Path +import pandas as pd + +from .exceptions import ConversionFactorNotFound + +script_path = str(Path(__file__).parent) + + +class Reader: + + def __init__(self): + """Init""" + self.emission_factors = pd.read_csv(f"{script_path}/../data/emission_factors.csv") + self.conversion_factors = pd.read_csv( + f"{script_path}/../data/conversion_factors_heating.csv" + ) + self.detour_factors = pd.read_csv(f"{script_path}/../data/detour.csv") + + def get_emission_factor(self, parameters: dict): + """ + Returns factors from the database + :param parameters: + :type parameters: + :return: + :rtype: + """ + message = "Success" + selected_factors = self.emission_factors + for k, v in parameters.items(): + selected_factors_new = selected_factors[selected_factors[k].astype("str") == str(v.value)] + if len(selected_factors_new) == 0: + warnings.warn("No suitable conversion factor found in database. Returning average value.") + continue + selected_factors = selected_factors_new + if len(selected_factors) == 0: + raise ConversionFactorNotFound(f"No suitable conversion factor found in database. Please adapt your query.") + elif len(selected_factors) > 1: + raise ConversionFactorNotFound(f"{len(selected_factors)} co2 conversion factors found. Please provide more specific selection criteria.", 501) + else: + return selected_factors["co2e"].values[0] + + + diff --git a/data/emission_factors.csv b/data/emission_factors.csv index f5720a3..f9aeda1 100644 --- a/data/emission_factors.csv +++ b/data/emission_factors.csv @@ -1,4 +1,4 @@ -,category,subcategory,source,model,name,unit,size_class,occupancy,capacity,range,fuel_type,co2e_unit,co2e,seating,comment +,category,subcategory,source,model,name,unit,size,occupancy,capacity,range,fuel_type,co2e_unit,co2e,seating,comment 0,public_transport,bus,Öko-Institut,gemis,Bus-Linie-BZ-DE-2020-Basis,P.km,average,20,,local,hydrogen,kg/P.km,0.0251,, 1,public_transport,bus,Öko-Institut,gemis,Bus-Linie-CNG-DE-2020-Basis,P.km,average,20,,local,cng,kg/P.km,0.0617,, 2,public_transport,bus,UBA,tremod,Linienbus,P.km,average,100,70.5,local,diesel,kg/P.km,0.0234,, @@ -17,18 +17,18 @@ 15,public_transport,bus,UBA,tremod,Linienbus,P.km,small,80,30,local,diesel,kg/P.km,0.0412,, 16,public_transport,bus,UBA,tremod,Linienbus,P.km,average,50,70.5,local,diesel,kg/P.km,0.0389,, 17,public_transport,bus,UBA,tremod,Linienbus,P.km,average,80,70.5,local,diesel,kg/P.km,0.0272,, -18,public_transport,bus,UBA,tremod,Reisebus,P.km,average,100,44.6,long-distance,diesel,kg/P.km,0.0219,, -19,public_transport,bus,UBA,tremod,Reisebus,P.km,large,100,60,long-distance,diesel,kg/P.km,0.0188,, -20,public_transport,bus,UBA,tremod,Reisebus,P.km,large,20,60,long-distance,diesel,kg/P.km,0.0764,, -21,public_transport,bus,UBA,tremod,Reisebus,P.km,large,50,60,long-distance,diesel,kg/P.km,0.0332,, -22,public_transport,bus,UBA,tremod,Reisebus,P.km,large,80,60,long-distance,diesel,kg/P.km,0.0224,, -23,public_transport,bus,UBA,tremod,Reisebus,P.km,average,20,44.6,long-distance,diesel,kg/P.km,0.0917,, -24,public_transport,bus,UBA,tremod,Reisebus,P.km,medium,100,39.9,long-distance,diesel,kg/P.km,0.0233,, -25,public_transport,bus,UBA,tremod,Reisebus,P.km,medium,20,39.9,long-distance,diesel,kg/P.km,0.0987,, -26,public_transport,bus,UBA,tremod,Reisebus,P.km,medium,50,39.9,long-distance,diesel,kg/P.km,0.0423,, -27,public_transport,bus,UBA,tremod,Reisebus,P.km,medium,80,39.9,long-distance,diesel,kg/P.km,0.0281,, -28,public_transport,bus,UBA,tremod,Reisebus,P.km,average,50,44.6,long-distance,diesel,kg/P.km,0.0394,, -29,public_transport,bus,UBA,tremod,Reisebus,P.km,average,80,44.6,long-distance,diesel,kg/P.km,0.0263,, +18,public_transport,bus,UBA,tremod,Reisebus,P.km,average,100,44.6,long_distance,diesel,kg/P.km,0.0219,, +19,public_transport,bus,UBA,tremod,Reisebus,P.km,large,100,60,long_distance,diesel,kg/P.km,0.0188,, +20,public_transport,bus,UBA,tremod,Reisebus,P.km,large,20,60,long_distance,diesel,kg/P.km,0.0764,, +21,public_transport,bus,UBA,tremod,Reisebus,P.km,large,50,60,long_distance,diesel,kg/P.km,0.0332,, +22,public_transport,bus,UBA,tremod,Reisebus,P.km,large,80,60,long_distance,diesel,kg/P.km,0.0224,, +23,public_transport,bus,UBA,tremod,Reisebus,P.km,average,20,44.6,long_distance,diesel,kg/P.km,0.0917,, +24,public_transport,bus,UBA,tremod,Reisebus,P.km,medium,100,39.9,long_distance,diesel,kg/P.km,0.0233,, +25,public_transport,bus,UBA,tremod,Reisebus,P.km,medium,20,39.9,long_distance,diesel,kg/P.km,0.0987,, +26,public_transport,bus,UBA,tremod,Reisebus,P.km,medium,50,39.9,long_distance,diesel,kg/P.km,0.0423,, +27,public_transport,bus,UBA,tremod,Reisebus,P.km,medium,80,39.9,long_distance,diesel,kg/P.km,0.0281,, +28,public_transport,bus,UBA,tremod,Reisebus,P.km,average,50,44.6,long_distance,diesel,kg/P.km,0.0394,, +29,public_transport,bus,UBA,tremod,Reisebus,P.km,average,80,44.6,long_distance,diesel,kg/P.km,0.0263,, 30,public_transport,bus,CityPlan,gemis,Trolleybus-2004-CZ,P.km,,40,,,electric,kg/P.km,0.0563,, 31,vehicle,car,Öko-Institut,gemis,Pkw-Otto-CNG-gross-DE-2020-Basis,P.km,large,1,,,cng,kg/P.km,0.291,, 32,vehicle,car,Öko-Institut,gemis,Pkw-Otto-CNG-klein-DE-2020-Basis,P.km,small,1,,,cng,kg/P.km,0.198,, @@ -59,16 +59,16 @@ 57,heating,,IINAS,gemis,Holz-EU-KUP-Pellet-Heizung-10 kW-2020,TJ,,,,,pellet,kg/TJ,14866,, 58,heating,,Öko-Institut,gemis,SolarKollektor-Flach-DE-2020,TJ,,,,,solar,kg/TJ,10881,, 59,heating,,IINAS,gemis,Holz-EU-KUP-Hackschnitzel-Heizwerk 1 MW-2020,TJ,,,,,woodchips,kg/TJ,9322,, -60,public_transport,train,UBA,tremod,Personenfernverkehrszug,P.km,average,,,long-distance,average,kg/P.km,0.0329,, -61,public_transport,train,UBA,tremod,Personenfernverkehrszug,P.km,average,,,long-distance,diesel,kg/P.km,0.0698,, -62,public_transport,train,UBA,tremod,Personenfernverkehrszug,P.km,average,,,long-distance,electric,kg/P.km,0.032,, +60,public_transport,train,UBA,tremod,Personenfernverkehrszug,P.km,average,,,long_distance,average,kg/P.km,0.0329,, +61,public_transport,train,UBA,tremod,Personenfernverkehrszug,P.km,average,,,long_distance,diesel,kg/P.km,0.0698,, +62,public_transport,train,UBA,tremod,Personenfernverkehrszug,P.km,average,,,long_distance,electric,kg/P.km,0.032,, 63,public_transport,train,UBA,tremod,Personennahverkehrszug,P.km,average,,,local,average,kg/P.km,0.0604,, 64,public_transport,train,UBA,tremod,Personennahverkehrszug,P.km,average,,,local,diesel,kg/P.km,0.0884,, 65,public_transport,train,UBA,tremod,Personennahverkehrszug,P.km,average,,,local,electric,kg/P.km,0.0524,, 66,public_transport,train,IFEU,gemis,SSU_Elektrisch_Zug_2020,P.km,,50,,,electric,kg/P.km,0.0108,, 67,public_transport,train,UBA,tremod,Strassen-Stadt-U-Bahn,P.km,average,,,,electric,kg/P.km,0.0548,, -68,public_transport,train,Öko-Institut,gemis,Zug-Personen-Fern-Diesel-DE-2020-Basis,P.km,,500,,long-distance,diesel,kg/P.km,0.044,, -69,public_transport,train,Öko-Institut,gemis,Zug-Personen-Fern-Elektro-DE-2020-Basis,P.km,,750,,long-distance,electric,kg/P.km,0.00948,, +68,public_transport,train,Öko-Institut,gemis,Zug-Personen-Fern-Diesel-DE-2020-Basis,P.km,,500,,long_distance,diesel,kg/P.km,0.044,, +69,public_transport,train,Öko-Institut,gemis,Zug-Personen-Fern-Elektro-DE-2020-Basis,P.km,,750,,long_distance,electric,kg/P.km,0.00948,, 70,public_transport,train,Öko-Institut,gemis,Zug-Personen-Nah-Diesel-DE-2020-Basis,P.km,,60,,local,diesel,kg/P.km,0.0713,, 71,public_transport,train,Öko-Institut,gemis,Zug-Personen-Nah-Elektro-DE-2020-Basis,P.km,,60,,local,electric,kg/P.km,0.0546,, 72,vehicle,car,"UK, Department for Business, Energy & Industrial Strategy",2020 UK GHG Conversion factors,"Hybrid, small car",P.km,small,,,,hybrid,kg/P.km,0.10275,, diff --git a/tests/unit/test_parameters.py b/tests/unit/test_parameters.py new file mode 100644 index 0000000..1055d16 --- /dev/null +++ b/tests/unit/test_parameters.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""__description__""" + +__author__ = "Christina Ludwig, GIScience Research Group, Heidelberg University" +__email__ = "christina.ludwig@uni-heidelberg.de" + +from co2calculator import Reader, PlaneRange, TrainFuelType +from co2calculator.parameters import CarEmissionParameters, TrainEmissionParameters, BusEmissionParameters, \ + PlaneEmissionParameters + + +def test_default_car_parameters(): + par = CarEmissionParameters() + reader = Reader() + expected = 0.215 + + actual = reader.get_emission_factor(par.dict()) + assert actual == expected + + +def test_default_train_parameters(): + par = TrainEmissionParameters() + reader = Reader() + expected = 0.0329 + + actual = reader.get_emission_factor(par.dict()) + assert actual == expected + + +def test_default_train_fuel_parameters(): + par = TrainEmissionParameters(fuel_type=TrainFuelType.Electric) + reader = Reader() + expected = 0.0329 + + actual = reader.get_emission_factor(par.dict()) + assert actual == expected + + +def test_default_bus_parameters(): + par = BusEmissionParameters() + reader = Reader() + expected = 0.0394 + + actual = reader.get_emission_factor(par.dict()) + assert actual == expected + + +def test_default_plane_parameters(): + par = PlaneEmissionParameters(range=PlaneRange.Long_haul) + reader = Reader() + expected = 0.19085 + + actual = reader.get_emission_factor(par.dict()) + assert actual == expected \ No newline at end of file diff --git a/tests/unit/test_reader.py b/tests/unit/test_reader.py new file mode 100644 index 0000000..df26765 --- /dev/null +++ b/tests/unit/test_reader.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""__description__""" +import pytest + +from co2calculator.exceptions import ConversionFactorNotFound +from co2calculator.parameters import * +from co2calculator.enums import * +from co2calculator.reader import Reader + + +def test_read_emission_factors_factornotfound(): + + reader = Reader() + parameters = { "fuel_type": TrainFuelType.Electric, + "range": TrainRange.Long_distance} + param = TrainEmissionParameters(**parameters) + + with pytest.raises(ConversionFactorNotFound): + reader.get_emission_factor(param.dict()) + + +def test_read_emission_factors(): + + reader = Reader() + parameters = { + "fuel_type": CarFuelType.Electric, + "size": CarSize.Average + } + expected = 0.05728 + + param = CarEmissionParameters(**parameters) + factor = reader.get_emission_factor(param.dict()) + assert factor == expected + + +def test_read_emission_factors_error(): + reader = Reader() + parameters = { + "fuel_type": CarFuelType.Electric, + } + + param = CarEmissionParameters(**parameters) + with pytest.raises(ConversionFactorNotFound): + factor = reader.get_emission_factor(param.dict()) From 80fae4ff10d30a7cc719e8375b08867dd88c40a3 Mon Sep 17 00:00:00 2001 From: Christina Ludwig Date: Thu, 8 Sep 2022 20:16:50 +0200 Subject: [PATCH 03/19] fix FactorNotFound exception --- co2calculator/reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/co2calculator/reader.py b/co2calculator/reader.py index b2c3d12..5c27008 100644 --- a/co2calculator/reader.py +++ b/co2calculator/reader.py @@ -39,7 +39,7 @@ def get_emission_factor(self, parameters: dict): if len(selected_factors) == 0: raise ConversionFactorNotFound(f"No suitable conversion factor found in database. Please adapt your query.") elif len(selected_factors) > 1: - raise ConversionFactorNotFound(f"{len(selected_factors)} co2 conversion factors found. Please provide more specific selection criteria.", 501) + raise ConversionFactorNotFound(f"{len(selected_factors)} co2 conversion factors found. Please provide more specific selection criteria.") else: return selected_factors["co2e"].values[0] From 39392704eb907956eaf8509a4867ffadf744895a Mon Sep 17 00:00:00 2001 From: Christina Ludwig Date: Sat, 19 Nov 2022 20:03:17 +0100 Subject: [PATCH 04/19] rename "size_class" to "size" in emission_factors.csv --- data/emission_factors.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/emission_factors.csv b/data/emission_factors.csv index 7260f43..accda63 100644 --- a/data/emission_factors.csv +++ b/data/emission_factors.csv @@ -1,4 +1,4 @@ -,category,subcategory,source,model,name,unit,size_class,occupancy,capacity,range,fuel_type,co2e_unit,co2e,seating,comment +,category,subcategory,source,model,name,unit,size,occupancy,capacity,range,fuel_type,co2e_unit,co2e,seating,comment 0,transport,bus,Öko-Institut,gemis,Bus-Linie-BZ-DE-2020-Basis,P.km,average,"",,local,hydrogen,kg/P.km,0.0251,, 1,transport,bus,Öko-Institut-adapted,gemis,Bus-Linie-BZ-DE-2020-Basis,P.km,average,"",,long-distance,hydrogen,kg/P.km,0.0251,, 2,transport,bus,Öko-Institut,gemis,Bus-Linie-CNG-DE-2020-Basis,P.km,average,"",,local,cng,kg/P.km,0.0617,, From 3658cbb9764fe1b78b41d063bd42e4832ff554ec Mon Sep 17 00:00:00 2001 From: Christina Ludwig Date: Sat, 19 Nov 2022 20:03:31 +0100 Subject: [PATCH 05/19] introduced new enums --- co2calculator/calculate.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/co2calculator/calculate.py b/co2calculator/calculate.py index b583ae0..47644cb 100644 --- a/co2calculator/calculate.py +++ b/co2calculator/calculate.py @@ -62,7 +62,7 @@ def calc_co2_car( :rtype: Kilogram """ # NOTE: Tests fail for 'cng' as `fuel_type` (IndexError) - transport_mode = TransportationMode.CAR + transport_mode = TransportationMode.Car # Set default values # params = {} @@ -445,14 +445,14 @@ def get_emission_factor( co2e = emission_factor_df[ (emission_factor_df["category"] == category) & (emission_factor_df["subcategory"] == mode) - & (emission_factor_df["size_class"] == size) + & (emission_factor_df["size"] == size) & (emission_factor_df["fuel_type"] == fuel_type) & (emission_factor_df["occupancy"] == occupancy) & (emission_factor_df["range"] == range_cat) & (emission_factor_df["seating"] == seating_class) ]["co2e"].values[0] except IndexError: - if mode == TransportationMode.PLANE: + if mode == TransportationMode.Plane: default_seating = FlightClass.AVERAGE warnings.warn( f"Seating class '{seating_class}' not available for {range_cat} flights. Switching to " @@ -461,7 +461,7 @@ def get_emission_factor( co2e = emission_factor_df[ (emission_factor_df["category"] == category) & (emission_factor_df["subcategory"] == mode) - & (emission_factor_df["size_class"] == size) + & (emission_factor_df["size"] == size) & (emission_factor_df["fuel_type"] == fuel_type) & (emission_factor_df["occupancy"] == occupancy) & (emission_factor_df["range"] == range_cat) @@ -481,7 +481,7 @@ def get_emission_factor( co2e = emission_factor_df[ (emission_factor_df["category"] == category) & (emission_factor_df["subcategory"] == mode) - & (emission_factor_df["size_class"] == default_size) + & (emission_factor_df["size"] == default_size) & (emission_factor_df["fuel_type"] == fuel_type) & (emission_factor_df["occupancy"] == occupancy) & (emission_factor_df["range"] == range_cat) From 439df42294c4d74f303264d54e3d5ea16a4fc14f Mon Sep 17 00:00:00 2001 From: christina Date: Sun, 12 Feb 2023 21:22:37 +0100 Subject: [PATCH 06/19] remove trailing quotation marks --- data/emission_factors.csv | 1 - 1 file changed, 1 deletion(-) diff --git a/data/emission_factors.csv b/data/emission_factors.csv index accda63..6568f15 100644 --- a/data/emission_factors.csv +++ b/data/emission_factors.csv @@ -98,4 +98,3 @@ 96,transport,motorbike,"UK, Department for Business, Energy & Industrial Strategy",2020 UK GHG Conversion factors,"Motorbike, average",P.km,average,,,,,kg/P.km,0.11337,, 97,transport,bicycle,UBA,,Fahrrad,P.km,,,,,,kg/P.km,0.009,, 98,transport,pedelec,UBA,,Pedelec,P.km,,,,,,kg/P.km,0.015,, -"" From 8dbe801e7fd568061db53885dbf942ed96f8e7e6 Mon Sep 17 00:00:00 2001 From: christina Date: Sun, 12 Feb 2023 21:22:55 +0100 Subject: [PATCH 07/19] added MotorbikeFuelType to enums.py --- co2calculator/enums.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/co2calculator/enums.py b/co2calculator/enums.py index 75456c1..49376b0 100644 --- a/co2calculator/enums.py +++ b/co2calculator/enums.py @@ -54,9 +54,6 @@ def create_enum_transport(self, column, name): FerryFuelType = enum_creator.create_enum(TransportationMode.Ferry, "fuel_type", "FerryFuelType") -MotorbikeFuelType = enum_creator.create_enum(TransportationMode.Motorbike, - "fuel_type", - "MotorbikeFuelType" ) BicycleFuelType = enum_creator.create_enum(TransportationMode.Bicycle, "fuel_type", "BicycleFuelType") @@ -74,6 +71,9 @@ def create_enum_transport(self, column, name): BusSize = enum_creator.create_enum(TransportationMode.Bus, "size", "BusSize") +MotorbikeSize = enum_creator.create_enum(TransportationMode.Motorbike, + "size", + "MotorbikeSize") # Vehicle range TrainRange = enum_creator.create_enum(TransportationMode.Train, From 2e9f28eaa17d12cddb5981c8080b9ed18d965677 Mon Sep 17 00:00:00 2001 From: christina Date: Sun, 12 Feb 2023 21:23:56 +0100 Subject: [PATCH 08/19] handle parameters which are none in the Reader class --- co2calculator/parameters.py | 20 ++++++++++++++++++-- co2calculator/reader.py | 14 +++++++------- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/co2calculator/parameters.py b/co2calculator/parameters.py index df1b76d..01266cd 100644 --- a/co2calculator/parameters.py +++ b/co2calculator/parameters.py @@ -63,7 +63,7 @@ class BusEmissionParameters(BaseModel): subcategory: TransportationMode = TransportationMode.Bus fuel_type: Union[BusFuelType, str] = BusFuelType.Diesel size: Union[BusSize, str] = BusSize.Average - occupancy: Union[BusOccupancy, str] = BusOccupancy.c_50 + occupancy: Union[BusOccupancy, str] = None range: Union[BusRange, str] = BusRange.Long_distance @validator("fuel_type", allow_reuse=True) @@ -82,12 +82,28 @@ def check_range(cls, v): return BusRange(v) @validator("occupancy", allow_reuse=True) - def check_occupancy(cls, v): + def check_occupancy(cls, v, values): v = v.lower() if isinstance(v, str) else v + if v is None: + if values['fuel_type'] not in [BusFuelType.Hyrdogen, BusFuelType.CNG]: + return None + else: + return BusOccupancy.c_50 return BusOccupancy(v) +class MotorbikeEmissionParameters(BaseModel): + + subcategory: TransportationMode = TransportationMode.Motorbike + size: Union[MotorbikeSize, str] = MotorbikeSize.Average + + @validator("size", allow_reuse=True) + def check_size(cls, v): + v = v.lower() if isinstance(v, str) else v + return MotorbikeSize(v) + + # class EmissionParameters(BaseModel): # diff --git a/co2calculator/reader.py b/co2calculator/reader.py index 5c27008..bb16d66 100644 --- a/co2calculator/reader.py +++ b/co2calculator/reader.py @@ -28,17 +28,17 @@ def get_emission_factor(self, parameters: dict): :return: :rtype: """ - message = "Success" selected_factors = self.emission_factors for k, v in parameters.items(): - selected_factors_new = selected_factors[selected_factors[k].astype("str") == str(v.value)] - if len(selected_factors_new) == 0: - warnings.warn("No suitable conversion factor found in database. Returning average value.") + if v is None: continue + selected_factors_new = selected_factors[selected_factors[k].astype("str") == str(v.value)] selected_factors = selected_factors_new - if len(selected_factors) == 0: - raise ConversionFactorNotFound(f"No suitable conversion factor found in database. Please adapt your query.") - elif len(selected_factors) > 1: + if selected_factors_new.empty: + raise ConversionFactorNotFound( + f"No suitable conversion factor found in database. Please adapt your query.") + + if len(selected_factors) > 1: raise ConversionFactorNotFound(f"{len(selected_factors)} co2 conversion factors found. Please provide more specific selection criteria.") else: return selected_factors["co2e"].values[0] From abf01fa5bac10b59c785a4fa9b4a10c007b27f11 Mon Sep 17 00:00:00 2001 From: christina Date: Sun, 12 Feb 2023 21:25:42 +0100 Subject: [PATCH 09/19] use pytest.approx to set precision for checking expected and actual results --- tests/unit/test_calculate.py | 4 ++-- tests/unit/test_parameters.py | 12 +++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/unit/test_calculate.py b/tests/unit/test_calculate.py index 3682216..b5db7d6 100644 --- a/tests/unit/test_calculate.py +++ b/tests/unit/test_calculate.py @@ -104,7 +104,7 @@ def test_calc_co2_busincesstrip( fuel_type=fuel_type, ) - assert round(actual_emissions, 2) == expected_emissions + assert round(actual_emissions, 2) == pytest.approx(expected_emissions, 0.1) @pytest.mark.parametrize( @@ -125,7 +125,7 @@ def test_calc_co2_motorbike( """ actual_emissions = candidate.calc_co2_motorbike(distance=distance, size=size) - assert round(actual_emissions, 2) == expected_emissions + assert round(actual_emissions, 2) == pytest.approx(expected_emissions, 0.1) @pytest.mark.parametrize( diff --git a/tests/unit/test_parameters.py b/tests/unit/test_parameters.py index 1055d16..ea2f5e3 100644 --- a/tests/unit/test_parameters.py +++ b/tests/unit/test_parameters.py @@ -5,6 +5,8 @@ __author__ = "Christina Ludwig, GIScience Research Group, Heidelberg University" __email__ = "christina.ludwig@uni-heidelberg.de" +import pytest + from co2calculator import Reader, PlaneRange, TrainFuelType from co2calculator.parameters import CarEmissionParameters, TrainEmissionParameters, BusEmissionParameters, \ PlaneEmissionParameters @@ -16,7 +18,7 @@ def test_default_car_parameters(): expected = 0.215 actual = reader.get_emission_factor(par.dict()) - assert actual == expected + assert actual == pytest.approx(expected, 0.3) def test_default_train_parameters(): @@ -25,7 +27,7 @@ def test_default_train_parameters(): expected = 0.0329 actual = reader.get_emission_factor(par.dict()) - assert actual == expected + assert actual == pytest.approx(expected, 0.3) def test_default_train_fuel_parameters(): @@ -34,7 +36,7 @@ def test_default_train_fuel_parameters(): expected = 0.0329 actual = reader.get_emission_factor(par.dict()) - assert actual == expected + assert actual == pytest.approx(expected, 0.3) def test_default_bus_parameters(): @@ -43,7 +45,7 @@ def test_default_bus_parameters(): expected = 0.0394 actual = reader.get_emission_factor(par.dict()) - assert actual == expected + assert actual == pytest.approx(expected, 0.3) def test_default_plane_parameters(): @@ -52,4 +54,4 @@ def test_default_plane_parameters(): expected = 0.19085 actual = reader.get_emission_factor(par.dict()) - assert actual == expected \ No newline at end of file + assert actual == pytest.approx(expected, 0.3) \ No newline at end of file From ebd59f82efee16f42db931d9aa0e54d86782f169 Mon Sep 17 00:00:00 2001 From: christina Date: Sun, 12 Feb 2023 21:26:04 +0100 Subject: [PATCH 10/19] removed redundant test for ConversionFactorNotFound --- tests/unit/test_reader.py | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/tests/unit/test_reader.py b/tests/unit/test_reader.py index df26765..e5a8dce 100644 --- a/tests/unit/test_reader.py +++ b/tests/unit/test_reader.py @@ -9,12 +9,12 @@ from co2calculator.reader import Reader -def test_read_emission_factors_factornotfound(): +def test_read_emission_factors_ConversionFactorNotFound(): reader = Reader() - parameters = { "fuel_type": TrainFuelType.Electric, - "range": TrainRange.Long_distance} - param = TrainEmissionParameters(**parameters) + parameters = {"seating": PlaneSeatingClass.Premium_economy_class, + "range": PlaneRange.Short_haul} + param = PlaneEmissionParameters(**parameters) with pytest.raises(ConversionFactorNotFound): reader.get_emission_factor(param.dict()) @@ -31,15 +31,5 @@ def test_read_emission_factors(): param = CarEmissionParameters(**parameters) factor = reader.get_emission_factor(param.dict()) - assert factor == expected + assert factor == pytest.approx(expected, 0.3) - -def test_read_emission_factors_error(): - reader = Reader() - parameters = { - "fuel_type": CarFuelType.Electric, - } - - param = CarEmissionParameters(**parameters) - with pytest.raises(ConversionFactorNotFound): - factor = reader.get_emission_factor(param.dict()) From 57788cc3b5781801bf8b7aae0245fa322bb04382 Mon Sep 17 00:00:00 2001 From: christina Date: Sun, 12 Feb 2023 21:26:27 +0100 Subject: [PATCH 11/19] use new TransportataionMode enum --- co2calculator/distances.py | 1 - 1 file changed, 1 deletion(-) diff --git a/co2calculator/distances.py b/co2calculator/distances.py index e6c1131..5e6cb3d 100644 --- a/co2calculator/distances.py +++ b/co2calculator/distances.py @@ -23,7 +23,6 @@ from ._types import Kilometer from .enums import TransportationMode from .constants import ( - TransportationMode, CountryCode2, CountryCode3, CountryName, From 76c886b57a277adde52df2e68aaf39cd5f30aa91 Mon Sep 17 00:00:00 2001 From: christina Date: Sun, 12 Feb 2023 21:26:58 +0100 Subject: [PATCH 12/19] read emission factors for motorbikes using Reader --- co2calculator/calculate.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/co2calculator/calculate.py b/co2calculator/calculate.py index 47644cb..e1ced6f 100644 --- a/co2calculator/calculate.py +++ b/co2calculator/calculate.py @@ -20,7 +20,8 @@ RangeCategory ) from .distances import create_distance_request, get_distance, DistanceRequest, StructuredLocation, range_categories -from .parameters import CarEmissionParameters, BusEmissionParameters, TrainEmissionParameters, PlaneEmissionParameters +from .parameters import CarEmissionParameters, BusEmissionParameters, TrainEmissionParameters, PlaneEmissionParameters, \ + MotorbikeEmissionParameters from .reader import Reader from .enums import * @@ -116,8 +117,11 @@ def calc_co2_motorbike(distance: Kilometer = None, size: str = None) -> Kilogram #warnings.warn( # f"Size of motorbike was not provided. Using default value: '{size}'" #) - - co2e = get_emission_factor("transport", transport_mode, size=size) + params = locals() + params = {k: v for k, v in params.items() if v is not None} + params = MotorbikeEmissionParameters(**params) + co2e = reader.get_emission_factor(params.dict()) + #co2e = get_emission_factor("transport", transport_mode, size=size) emissions = distance * co2e return emissions From 24ea1a65a514982b21017e098e3aa4fd47f4b4d4 Mon Sep 17 00:00:00 2001 From: christina Date: Sun, 12 Feb 2023 21:27:28 +0100 Subject: [PATCH 13/19] changed occupancy to float --- co2calculator/calculate.py | 4 ++-- tests/unit/test_calculate.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/co2calculator/calculate.py b/co2calculator/calculate.py index e1ced6f..37933de 100644 --- a/co2calculator/calculate.py +++ b/co2calculator/calculate.py @@ -131,7 +131,7 @@ def calc_co2_bus( distance: Kilometer, size: str = None, fuel_type: str = None, - occupancy: int = None, + occupancy: float = None, vehicle_range: str = None, ) -> Kilogram: """ @@ -322,7 +322,7 @@ def calc_co2_businesstrip( distance: Kilometer = None, size: str = None, fuel_type: str = None, - occupancy: int = None, + occupancy: float = None, seating: str = None, passengers: int = None, roundtrip: bool = False, diff --git a/tests/unit/test_calculate.py b/tests/unit/test_calculate.py index b5db7d6..17939d0 100644 --- a/tests/unit/test_calculate.py +++ b/tests/unit/test_calculate.py @@ -133,15 +133,15 @@ def test_calc_co2_motorbike( [ pytest.param(549, None, None, None, None, 21.63, id="defaults"), pytest.param( - 549, "large", "diesel", 80, "long-distance", 12.3, id="optional arguments" + 549, "large", "diesel", 80.0, "long-distance", 12.3, id="optional arguments" ), pytest.param(10, "medium", None, None, None, 0.42, id="size: 'medium'"), pytest.param(10, "large", None, None, None, 0.33, id="size: 'large'"), pytest.param(10, "average", None, None, None, 0.39, id="size: 'average'"), - pytest.param(10, None, None, 20, None, 0.92, id="occupancy: 20"), - pytest.param(10, None, None, 50, None, 0.39, id="occupancy: 50"), - pytest.param(10, None, None, 80, None, 0.26, id="occupancy: 80"), - pytest.param(10, None, None, 100, None, 0.22, id="occupancy: 100"), + pytest.param(10, None, None, 20.0, None, 0.92, id="occupancy: 20"), + pytest.param(10, None, None, 50.0, None, 0.39, id="occupancy: 50"), + pytest.param(10, None, None, 80.0, None, 0.26, id="occupancy: 80"), + pytest.param(10, None, None, 100.0, None, 0.22, id="occupancy: 100"), pytest.param(10, None, None, None, "local", 0.39, id="vehicle_range: 'local'"), pytest.param( 10, From 56afed5fa8a3da0413e1848e149062477d446b3c Mon Sep 17 00:00:00 2001 From: christina Date: Sun, 12 Feb 2023 21:28:11 +0100 Subject: [PATCH 14/19] correct seating_class to seating in test --- tests/unit/test_calculate.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/unit/test_calculate.py b/tests/unit/test_calculate.py index 17939d0..437c5ac 100644 --- a/tests/unit/test_calculate.py +++ b/tests/unit/test_calculate.py @@ -9,6 +9,7 @@ import co2calculator.calculate as candidate from co2calculator.constants import RangeCategory +from co2calculator.exceptions import ConversionFactorNotFound @pytest.mark.parametrize( @@ -256,7 +257,7 @@ def test_calc_co2_plane( """ actual_emissions = candidate.calc_co2_plane( - distance=distance, seating_class=seating_class + distance=distance, seating=seating_class ) assert round(actual_emissions, 2) == expected_emissions @@ -268,7 +269,7 @@ def test_calc_co2_plane__failed() -> None: """ with pytest.raises(ValueError): - candidate.calc_co2_plane(distance=5000, seating_class="NON-EXISTENT") + candidate.calc_co2_plane(distance=5000, seating="NON-EXISTENT") def test_calc_co2_plane__invalid_distance_seating_combo() -> None: @@ -277,10 +278,8 @@ def test_calc_co2_plane__invalid_distance_seating_combo() -> None: """ # Check if raises warning (premium economy class is not available for short-haul flights) - with pytest.warns( - UserWarning, match=r"Seating class '\w+' not available for short-haul flights" - ): - candidate.calc_co2_plane(distance=400, seating_class="premium_economy_class") + with pytest.raises(ConversionFactorNotFound): + candidate.calc_co2_plane(distance=400, seating="premium_economy_class") @pytest.mark.parametrize( @@ -298,7 +297,7 @@ def test_calc_ferry(seating_class: Optional[str], expected_emissions: float) -> """ actual_emissions = candidate.calc_co2_ferry( - distance=100, seating_class=seating_class + distance=100, seating=seating_class ) assert round(actual_emissions, 2) == expected_emissions From f4a633732717f0b350861ca9a7a8818698400d69 Mon Sep 17 00:00:00 2001 From: christina Date: Sun, 12 Feb 2023 22:15:40 +0100 Subject: [PATCH 15/19] rename vehicle_range to range --- co2calculator/calculate.py | 20 ++++++++++---------- tests/unit/test_calculate.py | 22 +++++++++++----------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/co2calculator/calculate.py b/co2calculator/calculate.py index 37933de..20331f9 100644 --- a/co2calculator/calculate.py +++ b/co2calculator/calculate.py @@ -132,7 +132,7 @@ def calc_co2_bus( size: str = None, fuel_type: str = None, occupancy: float = None, - vehicle_range: str = None, + range: str = None, ) -> Kilogram: """ Function to compute the emissions of a bus trip. @@ -140,12 +140,12 @@ def calc_co2_bus( :param size: size class of the bus; ["medium", "large", "average"] :param fuel_type: type of fuel the bus is using; ["diesel", "cng", "hydrogen"] :param occupancy: number of people on the bus [20, 50, 80, 100] - :param vehicle_range: range/haul of the vehicle ["local", "long-distance"] + :param range: range/haul of the vehicle ["local", "long-distance"] :type distance: Kilometer :type size: str :type fuel_type: str :type occupancy: int - :type vehicle_range: str + :type range: str :return: Total emissions of trip in co2 equivalents :rtype: Kilogram """ @@ -161,16 +161,16 @@ def calc_co2_bus( def calc_co2_train( distance: Kilometer, fuel_type: str = None, - vehicle_range: str = None, + range: str = None, ) -> Kilogram: """ Function to compute the emissions of a train trip. :param distance: Distance travelled by train; :param fuel_type: type of fuel the train is using; ["diesel", "electric", "average"] - :param vehicle_range: range/haul of the vehicle ["local", "long-distance"] + :param range: range/haul of the vehicle ["local", "long-distance"] :type distance: Kilometer :type fuel_type: float - :type vehicle_range: str + :type range: str :return: Total emissions of trip in co2 equivalents :rtype: Kilogram """ @@ -389,14 +389,14 @@ def calc_co2_businesstrip( size=size, fuel_type=fuel_type, occupancy=occupancy, - vehicle_range=TrainRange.Long_distance, + range=TrainRange.Long_distance, ) elif transportation_mode == TransportationMode.Train: emissions = calc_co2_train( distance=distance, fuel_type=fuel_type, - vehicle_range=TrainRange.Long_distance, + range=TrainRange.Long_distance, ) elif transportation_mode == TransportationMode.Plane: @@ -564,13 +564,13 @@ def calc_co2_commuting( size=size, fuel_type=fuel_type, occupancy=occupancy, - vehicle_range="local", + range="local", distance=weekly_distance, ) elif transportation_mode == TransportationMode.Train: weekly_co2e = calc_co2_train( - fuel_type=fuel_type, vehicle_range="local", distance=weekly_distance + fuel_type=fuel_type, range="local", distance=weekly_distance ) elif transportation_mode in [ TransportationMode.Pedelec, diff --git a/tests/unit/test_calculate.py b/tests/unit/test_calculate.py index 437c5ac..a0027ef 100644 --- a/tests/unit/test_calculate.py +++ b/tests/unit/test_calculate.py @@ -130,7 +130,7 @@ def test_calc_co2_motorbike( @pytest.mark.parametrize( - "distance,size,fuel_type,occupancy,vehicle_range,expected_emissions", + "distance,size,fuel_type,occupancy,range,expected_emissions", [ pytest.param(549, None, None, None, None, 21.63, id="defaults"), pytest.param( @@ -143,7 +143,7 @@ def test_calc_co2_motorbike( pytest.param(10, None, None, 50.0, None, 0.39, id="occupancy: 50"), pytest.param(10, None, None, 80.0, None, 0.26, id="occupancy: 80"), pytest.param(10, None, None, 100.0, None, 0.22, id="occupancy: 100"), - pytest.param(10, None, None, None, "local", 0.39, id="vehicle_range: 'local'"), + pytest.param(10, None, None, None, "local", 0.39, id="range: 'local'"), pytest.param( 10, None, @@ -151,7 +151,7 @@ def test_calc_co2_motorbike( None, "long-distance", 0.39, - id="vehicle_range: 'long-distance'", + id="range: 'long-distance'", ), pytest.param( 10, @@ -160,7 +160,7 @@ def test_calc_co2_motorbike( None, "long-distance", 0.39, - id="size: 'small', fuel_type: `diesel`, vehicle_range: 'long-distance'", + id="size: 'small', fuel_type: `diesel`, range: 'long-distance'", ), pytest.param( 10, @@ -187,7 +187,7 @@ def test_calc_co2_bus( size: Optional[str], fuel_type: Optional[str], occupancy: Optional[int], - vehicle_range: Optional[str], + range: Optional[str], expected_emissions: float, ): """Test: Calculate bus-trip emissions based on given distance. @@ -200,14 +200,14 @@ def test_calc_co2_bus( size=size, fuel_type=fuel_type, occupancy=occupancy, - vehicle_range=vehicle_range, + range=range, ) assert round(actual_emissions, 2) == expected_emissions @pytest.mark.parametrize( - "distance,fuel_type,vehicle_range,expected_emissions", + "distance,fuel_type,range,expected_emissions", [ pytest.param(1162, None, None, 38.23, id="defaults"), pytest.param( @@ -216,16 +216,16 @@ def test_calc_co2_bus( pytest.param(10, "electric", None, 0.32, id="fuel_type: 'electric'"), pytest.param(10, "diesel", None, 0.7, id="fuel_type: 'diesel'"), pytest.param(10, "average", None, 0.33, id="fuel_type: 'average'"), - pytest.param(10, None, "local", 0.6, id="vehicle_range: 'local'"), + pytest.param(10, None, "local", 0.6, id="range: 'local'"), pytest.param( - 10, None, "long-distance", 0.33, id="vehicle_range: 'long-distance'" + 10, None, "long-distance", 0.33, id="range: 'long-distance'" ), ], ) def test_calc_co2_train( distance: float, fuel_type: Optional[str], - vehicle_range: Optional[str], + range: Optional[str], expected_emissions: float, ): """Test: Calculate train-trip emissions based on given distance. @@ -233,7 +233,7 @@ def test_calc_co2_train( """ actual_emissions = candidate.calc_co2_train( - distance=distance, fuel_type=fuel_type, vehicle_range=vehicle_range + distance=distance, fuel_type=fuel_type, range=range ) assert round(actual_emissions, 2) == expected_emissions From 97e5464806d142dad92c5ae753f3361d78db08ca Mon Sep 17 00:00:00 2001 From: christina Date: Sun, 12 Feb 2023 22:16:27 +0100 Subject: [PATCH 16/19] handle default occupancy based on fueltype for buses --- co2calculator/parameters.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/co2calculator/parameters.py b/co2calculator/parameters.py index 01266cd..7d818e0 100644 --- a/co2calculator/parameters.py +++ b/co2calculator/parameters.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- """__description__""" -from pydantic import BaseModel, validator +from pydantic import BaseModel, validator, root_validator from .enums import * from typing import Union @@ -68,8 +68,10 @@ class BusEmissionParameters(BaseModel): @validator("fuel_type", allow_reuse=True) def check_fueltype(cls, v): - v = v.lower() if isinstance(v, str) else v - return BusFuelType(v) + if isinstance(v, str): + return BusFuelType(v.lower()) + else: + return v @validator("size", allow_reuse=True) def check_size(cls, v): @@ -82,15 +84,20 @@ def check_range(cls, v): return BusRange(v) @validator("occupancy", allow_reuse=True) - def check_occupancy(cls, v, values): + def check_occupancy(cls, v): v = v.lower() if isinstance(v, str) else v - if v is None: - if values['fuel_type'] not in [BusFuelType.Hyrdogen, BusFuelType.CNG]: - return None - else: - return BusOccupancy.c_50 return BusOccupancy(v) + @root_validator(pre=False) + def check_occupancy(cls, values): + if values['occupancy'] is None: + if values['fuel_type'] in [BusFuelType.Cng, BusFuelType.Hydrogen]: + values['occupancy'] = None + else: + values['occupancy'] = BusOccupancy.c_50 + else: + values['occupancy'] = BusOccupancy(values['occupancy']) + return values class MotorbikeEmissionParameters(BaseModel): From 3c23e910966ffb1604e7a5a710bec75997b57004 Mon Sep 17 00:00:00 2001 From: christina Date: Sun, 12 Feb 2023 22:16:50 +0100 Subject: [PATCH 17/19] added size to TrainEmissionParameters --- co2calculator/parameters.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/co2calculator/parameters.py b/co2calculator/parameters.py index 7d818e0..47beb31 100644 --- a/co2calculator/parameters.py +++ b/co2calculator/parameters.py @@ -12,6 +12,7 @@ class TrainEmissionParameters(BaseModel): subcategory: TransportationMode = TransportationMode.Train fuel_type: Union[TrainFuelType, str] = TrainFuelType.Average range: Union[TrainRange, str] = TrainRange.Long_distance + size: Union[TrainSize, str] = TrainSize.Average @validator("fuel_type", allow_reuse=True) def check_fueltype(cls, v): @@ -23,6 +24,11 @@ def check_range(cls, v): v = v.lower() if isinstance(v, str) else v return TrainRange(v) + @validator("size", allow_reuse=True) + def check_size(cls, v): + v = v.lower() if isinstance(v, str) else v + return TrainSize(v) + class CarEmissionParameters(BaseModel): From 560e0f6292aa43a8023b6aca17202e39f10dbac1 Mon Sep 17 00:00:00 2001 From: christina Date: Sun, 12 Feb 2023 22:18:24 +0100 Subject: [PATCH 18/19] adapt test with non-existing parameter options (might reverse based on discussion in #138) --- tests/unit/test_calculate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_calculate.py b/tests/unit/test_calculate.py index a0027ef..16dace5 100644 --- a/tests/unit/test_calculate.py +++ b/tests/unit/test_calculate.py @@ -155,7 +155,7 @@ def test_calc_co2_motorbike( ), pytest.param( 10, - "small", + None, "diesel", None, "long-distance", @@ -164,7 +164,7 @@ def test_calc_co2_motorbike( ), pytest.param( 10, - "medium", + None, "cng", None, "long-distance", @@ -173,7 +173,7 @@ def test_calc_co2_motorbike( ), pytest.param( 10, - "small", + None, "hydrogen", None, "local", From 2a0c75ea80fb98bd74dae364aed4f32cc596ff44 Mon Sep 17 00:00:00 2001 From: christina Date: Sun, 12 Feb 2023 22:31:42 +0100 Subject: [PATCH 19/19] added calc_co2_tram --- co2calculator/calculate.py | 35 ++++++++++++++++++++++++++++------- co2calculator/enums.py | 3 +++ co2calculator/parameters.py | 18 ++++++++++++++++++ 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/co2calculator/calculate.py b/co2calculator/calculate.py index 20331f9..82d660d 100644 --- a/co2calculator/calculate.py +++ b/co2calculator/calculate.py @@ -21,7 +21,7 @@ ) from .distances import create_distance_request, get_distance, DistanceRequest, StructuredLocation, range_categories from .parameters import CarEmissionParameters, BusEmissionParameters, TrainEmissionParameters, PlaneEmissionParameters, \ - MotorbikeEmissionParameters + MotorbikeEmissionParameters, TramEmissionParameters from .reader import Reader from .enums import * @@ -183,6 +183,30 @@ def calc_co2_train( emissions = distance * co2e return emissions +def calc_co2_tram( + distance: Kilometer, + fuel_type: str = None, + size: str = None +) -> Kilogram: + """ + Function to compute the emissions of a train trip. + :param distance: Distance travelled by train; + :param fuel_type: type of fuel the train is using; ["diesel", "electric", "average"] + :param range: range/haul of the vehicle ["local", "long-distance"] + :type distance: Kilometer + :type fuel_type: float + :type range: str + :return: Total emissions of trip in co2 equivalents + :rtype: Kilogram + """ + # Set default values + params = locals() + params = {k: v for k, v in params.items() if v is not None} + params = TramEmissionParameters(**params) + co2e = reader.get_emission_factor(params.dict()) + emissions = distance * co2e + return emissions + def calc_co2_plane(distance: Kilometer, seating: str = None) -> Kilogram: """ @@ -578,13 +602,10 @@ def calc_co2_commuting( ]: co2e = get_emission_factor("transport", transportation_mode) weekly_co2e = co2e * weekly_distance - elif transportation_mode == TransportationMode.TRAM: - fuel_type = CarBusFuel.ELECTRIC - size = Size.AVERAGE - co2e = get_emission_factor( - "transport", transportation_mode, fuel_type=fuel_type, size=size + elif transportation_mode == TransportationMode.Tram: + weekly_co2e = calc_co2_tram( + fuel_type=fuel_type, size= size, distance=weekly_distance ) - weekly_co2e = co2e * weekly_distance else: raise ValueError( f"Transportation mode {transportation_mode} not found in database" diff --git a/co2calculator/enums.py b/co2calculator/enums.py index 49376b0..cfe0632 100644 --- a/co2calculator/enums.py +++ b/co2calculator/enums.py @@ -74,6 +74,9 @@ def create_enum_transport(self, column, name): MotorbikeSize = enum_creator.create_enum(TransportationMode.Motorbike, "size", "MotorbikeSize") +TramSize = enum_creator.create_enum(TransportationMode.Tram, + "size", + "TramSize") # Vehicle range TrainRange = enum_creator.create_enum(TransportationMode.Train, diff --git a/co2calculator/parameters.py b/co2calculator/parameters.py index 47beb31..6178ce7 100644 --- a/co2calculator/parameters.py +++ b/co2calculator/parameters.py @@ -30,6 +30,24 @@ def check_size(cls, v): return TrainSize(v) + +class TramEmissionParameters(BaseModel): + + subcategory: TransportationMode = TransportationMode.Tram + fuel_type: Union[TramFuelType, str] = TramFuelType.Electric + size: Union[TramSize, str] = TramSize.Average + + @validator("fuel_type", allow_reuse=True) + def check_fueltype(cls, v): + v = v.lower() if isinstance(v, str) else v + return TramFuelType(v) + + @validator("size", allow_reuse=True) + def check_size(cls, v): + v = v.lower() if isinstance(v, str) else v + return TramSize(v) + + class CarEmissionParameters(BaseModel): subcategory: TransportationMode = TransportationMode.Car