Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add a calculation function that works generically for all trips #184

Merged
merged 6 commits into from
May 5, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ __pycache__/
# C extensions
*.so

#.vscode
settings.json

# Distribution / packaging
.Python
build/
Expand Down
106 changes: 22 additions & 84 deletions co2calculator/calculate.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
calc_co2_train,
calc_co2_tram,
)
from co2calculator.util import get_calc_function_from_transport_mode

from ._types import Kilogram, Kilometer
from .constants import (
Expand Down Expand Up @@ -115,95 +116,32 @@ def calc_co2_heating(
return consumption_kwh * area_share / KWH_TO_TJ * co2e


def calc_co2_businesstrip(
def calc_co2_trip(
distance: Kilometer | None,
transportation_mode: TransportationMode,
start=None,
destination=None,
distance: Kilometer = None,
size: Size = None,
fuel_type: CarFuel | BusFuel | TrainFuel = None,
occupancy: int = None,
seating: FlightClass | FerryClass = None,
passengers: int = None,
roundtrip: bool = False,
) -> Tuple[Kilogram, Kilometer, str, str]:
"""Function to compute emissions for business trips based on transportation mode and trip specifics

:param transportation_mode: mode of transport [car, bus, train, plane, ferry]
:param start: Start of the trip (alternatively, distance can be provided)
:param destination: Destination of the trip (alternatively, distance can be provided)
:param distance: Distance travelled in km (alternatively, start and destination can be provided)
:param size: Size class of the vehicle [small, medium, large, average] - only used for car and bus
:param fuel_type: Fuel type of the vehicle
[average, cng, diesel, electric, gasoline, hybrid, hydrogen, plug-in_hybrid]
- only used for car, bus and train
:param occupancy: Occupancy of the vehicle in % [20, 50, 80, 100] - only used for bus
:param seating: seating class ["average", "Economy class", "Premium economy class", "Business class", "First class"]
- only used for plane
:param passengers: Number of passengers in the vehicle (including the participant), number from 1 to 9
- only used for car
:param roundtrip: whether the trip is a round trip or not [True, False]
:type transportation_mode: str
:type distance: Kilometer
:type size: str
:type fuel_type: str
:type occupancy: int
:type seating: str
:type passengers: int
:type roundtrip: bool
:return: Emissions of the business trip in co2 equivalents,
Distance of the business trip,
Range category of the business trip [very short haul, short haul, medium haul, long haul]
Range description (i.e., what range of distances does to category correspond to)
:rtype: tuple[Kilogram, Kilometer, str, str]
"""

# Evaluate if distance- or stop-based request.
# Rules:
# - `distance` is dominant;
# - if distance not provided, take stops;
# - if stops not available, raise error;
# In general:
# - If stop-based, calculate distance first, then continue only distance-based

if not distance:
request = create_distance_request(start, destination, transportation_mode)
distance = get_distance(request)

if transportation_mode == TransportationMode.CAR:
emissions = calc_co2_car(
distance=distance,
options={},
)
elif transportation_mode == TransportationMode.BUS:
emissions = calc_co2_bus(
distance=distance,
options={},
)

elif transportation_mode == TransportationMode.TRAIN:
emissions = calc_co2_train(
distance=distance,
options={},
)

elif transportation_mode == TransportationMode.PLANE:
emissions = calc_co2_plane(distance, options={})
custom_emission_factor: Kilogram | None = None,
options: dict = None,
) -> Kilogram:
"""Function to compute emissions for a trip based on distance

elif transportation_mode == TransportationMode.FERRY:
emissions = calc_co2_ferry(distance, options={})
:param distance: Distance travelled in km
:param transportation_mode: mode of transport. For options, see TransportationMode enum.
:param custom_emission_factor: custom emission factor in kg/km. If provided, this will be used instead of the included emission factors.
:param options: options for the trip. Type must match transportation mode.

:return: Emissions of the business trip in co2 equivalents.
"""
if custom_emission_factor is not None:
print("Ignoring transportation mode as custom emission factor is set")
return distance * custom_emission_factor
else:
raise ValueError(
f"No emission factor available for the specified mode of transport '{transportation_mode}'."
# check for invalid transportation mode
assert transportation_mode.lower() in (
item.value for item in TransportationMode
)
if roundtrip is True:
emissions *= 2

# categorize according to distance (range)
range_category, range_description = range_categories(distance)

return emissions, distance, range_category, range_description
# pass the distance and options to the respective function
calc_function = get_calc_function_from_transport_mode(transportation_mode)
return calc_function(distance, options)


def calc_co2_commuting(
Expand Down
29 changes: 29 additions & 0 deletions co2calculator/util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Generic collection of util functions and maps."""

from co2calculator.constants import TransportationMode
from co2calculator.mobility.calculate_mobility import (
calc_co2_bicycle,
calc_co2_bus,
calc_co2_car,
calc_co2_ferry,
calc_co2_motorbike,
calc_co2_pedelec,
calc_co2_train,
calc_co2_tram,
)


def get_calc_function_from_transport_mode(
transport_mode: TransportationMode,
) -> callable:
transportation_mode_calc_function_map = {
TransportationMode.CAR: calc_co2_car,
TransportationMode.MOTORBIKE: calc_co2_motorbike,
TransportationMode.BUS: calc_co2_bus,
TransportationMode.TRAIN: calc_co2_train,
TransportationMode.BICYCLE: calc_co2_bicycle,
TransportationMode.TRAM: calc_co2_tram,
TransportationMode.FERRY: calc_co2_ferry,
TransportationMode.PEDELEC: calc_co2_pedelec,
}
return transportation_mode_calc_function_map[transport_mode]
119 changes: 0 additions & 119 deletions tests/functional/test_calculate.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,125 +24,6 @@
# TODO: Mock all calls to openrouteservice.org (or openrouteservice package)


class TestCalculateBusinessTrip:
"""Functional testing of `calc_co2_businesstrip` calls from backend"""

@pytest.mark.parametrize(
"transportation_mode, expected_emissions",
[
pytest.param("car", 9.03, id="transportation_mode: 'car'"),
pytest.param("bus", 1.65, id="transportation_mode: 'bus'"),
pytest.param("train", 1.38, id="transportation_mode: 'train'"),
],
)
def test_calc_co2_business_trip__distance_based(
self, transportation_mode: str, expected_emissions: float
) -> None:
"""Scenario: Backend asks for business trip calculation with distance input.
Test: co2 calculation for business trip
Expect: Happy path
"""
actual_emissions, _, _, _ = candidate.calc_co2_businesstrip(
transportation_mode=transportation_mode,
start=None,
destination=None,
distance=42.0,
size=None,
fuel_type=None,
occupancy=None,
seating=None,
passengers=None,
roundtrip=False,
)

assert round(actual_emissions, 2) == expected_emissions

@pytest.mark.parametrize(
"transportation_mode, start, destination, expected_emissions",
[
pytest.param(
"car",
{
"address": "Im Neuenheimer Feld 348",
"locality": "Heidelberg",
"country": "Germany",
},
{
"country": "Germany",
"locality": "Berlin",
"address": "Alexanderplatz 1",
},
134.71,
id="transportation_mode: 'car'",
),
pytest.param(
"bus",
{
"address": "Im Neuenheimer Feld 348",
"locality": "Heidelberg",
"country": "Germany",
},
{
"country": "Germany",
"locality": "Berlin",
"address": "Alexanderplatz 1",
},
28.3,
id="transportation_mode: 'bus'",
),
pytest.param(
"train",
{"station_name": "Heidelberg Hbf", "country": "DE"},
{"station_name": "Berlin Hbf", "country": "DE"},
24.66,
id="transportation_mode: 'train'",
),
pytest.param(
"plane",
"FRA",
"BER",
129.16,
id="transportation_mode: 'plane'",
),
pytest.param(
"ferry",
{"locality": "Friedrichshafen", "country": "DE"},
{"locality": "Konstanz", "country": "DE"},
2.57,
id="transportation_mode: 'ferry'",
),
],
)
@pytest.mark.skip(reason="API Key missing for test setup. TODO: Mock Response")
def test_calc_co2_business_trip__stops_based(
self,
transportation_mode: str,
start: Dict,
destination: Dict,
expected_emissions: float,
) -> None:
"""Scenario: Backend asks for business trip calculation with distance input.
Test: co2 calculation for business trip
Expect: Happy path
"""
# NOTE: IMPORTANT - Test currently makes real web calls!
# TODO: Record responses and mock external calls!
actual_emissions, _, _, _ = candidate.calc_co2_businesstrip(
transportation_mode=transportation_mode,
start=start,
destination=destination,
distance=None,
size=None,
fuel_type=None,
occupancy=None,
seating=None,
passengers=None,
roundtrip=False,
)

assert round(actual_emissions, 2) == expected_emissions


class TestCalculateCommuting:
"""Functional testing of `calc_co2_commuting` calls from backend"""

Expand Down
Loading
Loading