Skip to content

Commit

Permalink
feat: add a calculation function that works generically for all trips (
Browse files Browse the repository at this point in the history
…#184)

* add a calculation function that works generically for all trips

* add settings.json to vscode

* add tests

* remove leftover business trip function

* bump version
  • Loading branch information
codingfabi authored May 5, 2024
1 parent ebf4dbb commit 7b0cba7
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 249 deletions.
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]
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "co2calculator"
version = "0.1.3b"
version = "0.1.5b"
description = "Calculates CO2e emissions from travel and commuting as well as heating and electricity consumption of buildings"
authors = ["pledge4future Team <info@pledge4future.org>"]
license = "GPL 3"
Expand Down
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

0 comments on commit 7b0cba7

Please sign in to comment.