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

Calculate CO₂ emissions of itineraries #5278

Merged
merged 58 commits into from
Nov 7, 2023
Merged
Show file tree
Hide file tree
Changes from 54 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
1faf8a0
load co2 data for digitransit
Antiik91 Jun 16, 2023
689d98c
remove sout
Antiik91 Jun 16, 2023
46c479b
calculate co2 emissions
sharhio Jul 5, 2023
9f27798
Merge conflict fix
Antiik91 Jul 19, 2023
b7cd138
updated configuration from placeholder values. Adjusted emissions mod…
Antiik91 Jul 21, 2023
06f7f79
initial documentation
Antiik91 Jul 21, 2023
2df7d32
improved docs
Antiik91 Jul 21, 2023
72c03b1
Fixed typo
Antiik91 Jul 25, 2023
9af1ba5
Fixed typos from tests and docs
Antiik91 Aug 1, 2023
2b0d663
Merge remote-tracking branch 'otp/dev-2.x' into co2
sharhio Aug 1, 2023
a075ffa
Emissions logic refined, more tests + fixes
sharhio Aug 3, 2023
740e0c8
generated documentation fixed
sharhio Aug 3, 2023
4f56079
Implementation using dagger injection
sharhio Aug 31, 2023
af88c7d
Merge remote-tracking branch 'otp/dev-2.x' into co2
sharhio Sep 4, 2023
7207521
test fixed
sharhio Sep 4, 2023
ab6efe8
emissions added to datafetchers
sharhio Sep 4, 2023
571757e
emissions data from gtfs.zip
sharhio Sep 14, 2023
241e784
Merge remote-tracking branch 'otp/dev-2.x' into co2
sharhio Sep 14, 2023
7ec48e4
data should use comma separator
sharhio Sep 14, 2023
bd7285b
documentations fixed, comment removed
sharhio Sep 14, 2023
ccecb7e
also read nonzipped gtfs
sharhio Sep 14, 2023
ab48452
Emissions data simplification
sharhio Sep 21, 2023
b13b7db
Emissions tests and documentation fix
sharhio Sep 21, 2023
b87b7e5
merge dev-2.x
sharhio Oct 3, 2023
d9447e3
Documentation and test fixes
sharhio Oct 3, 2023
1750e15
More tests for emissions, documentation fixed
sharhio Oct 3, 2023
d904701
emissions test fixes, cleanup
sharhio Oct 4, 2023
db30d39
emissions use feedScopedId, use meters instead of km, refactoring
sharhio Oct 4, 2023
72088fe
emissions use double instead of float, better naming
sharhio Oct 4, 2023
bf74026
support decimals in passenger count
sharhio Oct 10, 2023
12c8e21
documentation fixes
sharhio Oct 10, 2023
9c6d5a1
Use emissions data model in build
sharhio Oct 11, 2023
4a00f67
Emissions unit, type, service and package updated
sharhio Oct 18, 2023
c0f4bd8
Update src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls
sharhio Oct 19, 2023
8afd628
Emissions naming fixed, emissions use record
sharhio Oct 19, 2023
0d2634f
Emissions grams scalar, refactoring, documentation, tests
sharhio Oct 24, 2023
643e545
Update src/ext-test/java/org/opentripplanner/ext/emissions/EmissionsT…
sharhio Oct 26, 2023
5e9bb96
Update src/ext-test/java/org/opentripplanner/ext/emissions/EmissionsT…
sharhio Oct 26, 2023
d380b77
Update src/main/java/org/opentripplanner/api/model/ApiItinerary.java
sharhio Oct 26, 2023
e39fb2a
Update src/main/java/org/opentripplanner/framework/model/Grams.java
sharhio Oct 26, 2023
9d95379
Update src/ext-test/java/org/opentripplanner/ext/emissions/EmissionsT…
sharhio Oct 26, 2023
7116132
Update src/ext-test/java/org/opentripplanner/ext/emissions/EmissionsT…
sharhio Oct 26, 2023
1741481
Update src/ext-test/java/org/opentripplanner/ext/emissions/EmissionsT…
sharhio Oct 26, 2023
d99d903
Update src/ext-test/java/org/opentripplanner/ext/emissions/EmissionsT…
sharhio Oct 26, 2023
5f33345
Update src/ext/java/org/opentripplanner/ext/emissions/EmissionsDataMo…
sharhio Oct 26, 2023
ffb7adf
Update src/ext/java/org/opentripplanner/ext/emissions/EmissionsDataSe…
sharhio Oct 26, 2023
cf1d627
Emissions refactoring, documentations
sharhio Oct 30, 2023
bab72fc
emissions service final field
sharhio Oct 30, 2023
70c8b2a
Merge remote-tracking branch 'otp/dev-2.x' into co2
sharhio Oct 30, 2023
471e0eb
Update src/ext/java/org/opentripplanner/ext/emissions/EmissionsFilter…
sharhio Oct 30, 2023
7dc9b2a
emissions refactoring
sharhio Oct 30, 2023
0b7e414
Emissions refactoring and test fixes
sharhio Oct 30, 2023
f12803e
Emissions file fix
sharhio Oct 31, 2023
8eb34b5
Emissions issssue type and import fixes
sharhio Nov 1, 2023
3ab8b67
More logging and Merge remote-tracking branch 'otp/dev-2.x' into co2
sharhio Nov 2, 2023
aa5f235
Update src/main/java/org/opentripplanner/model/plan/Itinerary.java
sharhio Nov 2, 2023
0f331e9
Emissions model config and doc generation fix
sharhio Nov 6, 2023
95e5532
Merge branch 'co2' of github.com:HSLdevcom/OpenTripPlanner into co2
sharhio Nov 6, 2023
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
54 changes: 54 additions & 0 deletions doc-templates/Emissions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# CO₂ Emissions calculation

## Contact Info

- Digitransit Team

## Documentation

Graph build import of CO₂ Emissions from GTFS data sets (through custom emissions.txt extension)
and the ability to attach them to itineraries by Digitransit team.
The emissions are represented in grams per kilometer (g/Km) unit.

Emissions data is located in an emissions.txt file within a gtfs package and has the following columns:

`route_id`: route id

`avg_co2_per_vehicle_per_km`: Average carbon dioxide equivalent value for the vehicles used on the route at grams/Km units.

`avg_passenger_count`: Average passenger count for the vehicles on the route.

For example:
```csv
route_id,avg_co2_per_vehicle_per_km,avg_passenger_count
1234,123,20
2345,0,0
3456,12.3,20.0
```

Emissions data is loaded from the gtfs package and embedded into the graph during the build process.


### Configuration
To enable this functionality, you need to enable the "Co2Emissions" feature in the
`otp-config.json` file.

```JSON
//otp-config.json
{
"Co2Emissions": true
}

```
Include the `emissions` object in the
`build-config.json` file. The `emissions` object should contain parameters called
`carAvgCo2PerKm` and `carAvgOccupancy`. The `carAvgCo2PerKm` provides the average emissions value for a car in g/km and
the `carAvgOccupancy` provides the average number of passengers in a car.

<!-- INSERT: config -->

## Changelog

### OTP 2.5

- Initial implementation of the emissions calculation.
21 changes: 20 additions & 1 deletion docs/BuildConfiguration.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ Sections follow that describe particular settings in more depth.
| demDefaults | `object` | Default properties for DEM extracts. | *Optional* | | 2.3 |
|    [elevationUnitMultiplier](#demDefaults_elevationUnitMultiplier) | `double` | Specify a multiplier to convert elevation units from source to meters. | *Optional* | `1.0` | 2.3 |
| [elevationBucket](#elevationBucket) | `object` | Used to download NED elevation tiles from the given AWS S3 bucket. | *Optional* | | na |
| [emissions](#emissions) | `object` | Emissions configuration. | *Optional* | | na |
|    carAvgCo2PerKm | `integer` | The average CO₂ emissions of a car in grams per kilometer. | *Optional* | `170` | na |
|    carAvgOccupancy | `double` | The average number of passengers in a car. | *Optional* | `1.3` | na |
leonardehrenfried marked this conversation as resolved.
Show resolved Hide resolved
| [fares](sandbox/Fares.md) | `object` | Fare configuration. | *Optional* | | 2.0 |
| gtfsDefaults | `object` | The gtfsDefaults section allows you to specify default properties for GTFS files. | *Optional* | | 2.3 |
|    blockBasedInterlining | `boolean` | Whether to create stay-seated transfers in between two trips with the same block id. | *Optional* | `true` | 2.3 |
Expand Down Expand Up @@ -723,6 +726,18 @@ for the next graph build operation. You should add the `--cache <directory>` com
to specify your NED tile cache location.


<h3 id="emissions">emissions</h3>

**Since version:** `na` ∙ **Type:** `object` ∙ **Cardinality:** `Optional`
**Path:** /

Emissions configuration.

By specifying the average CO₂ emissions of a car in grams per kilometer as well as
the average number of passengers in a car the program is able to to perform emission
calculations for car travel.


leonardehrenfried marked this conversation as resolved.
Show resolved Hide resolved
<h3 id="gd_discardMinTransferTimes">discardMinTransferTimes</h3>

**Since version:** `2.3` ∙ **Type:** `boolean` ∙ **Cardinality:** `Optional` ∙ **Default value:** `false`
Expand Down Expand Up @@ -1163,7 +1178,11 @@ case where this is not the case.
"enabled" : true
}
}
]
],
"emissions" : {
"carAvgCo2PerKm" : 170,
"carAvgOccupancy" : 1.3
}
leonardehrenfried marked this conversation as resolved.
Show resolved Hide resolved
}
```

Expand Down
1 change: 1 addition & 0 deletions docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ Here is a list of all features which can be toggled on/off and their default val
| `TransferConstraints` | Enforce transfers to happen according to the _transfers.txt_(GTFS) and Interchanges(NeTEx). Turing this _off_ will increase the routing performance a little. | ✓️ | |
| `ActuatorAPI` | Endpoint for actuators (service health status). | | ✓️ |
| `AsyncGraphQLFetchers` | Whether the @async annotation in the GraphQL schema should lead to the fetch being executed asynchronously. This allows batch or alias queries to run in parallel at the cost of consuming extra threads. | | |
| `Co2Emissions` | Enable the emissions sandbox module. | | ✓️ |
| `DataOverlay` | Enable usage of data overlay when calculating costs for the street network. | | ✓️ |
| `FaresV2` | Enable import of GTFS-Fares v2 data. | | ✓️ |
| `FlexRouting` | Enable FLEX routing. | | ✓️ |
Expand Down
79 changes: 79 additions & 0 deletions docs/sandbox/Emissions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# CO₂ Emissions calculation

## Contact Info

- Digitransit Team

## Documentation

Graph build import of CO₂ Emissions from GTFS data sets (through custom emissions.txt extension)
and the ability to attach them to itineraries by Digitransit team.
The emissions are represented in grams per kilometer (g/Km) unit.

Emissions data is located in an emissions.txt file within a gtfs package and has the following columns:

`route_id`: route id

`avg_co2_per_vehicle_per_km`: Average carbon dioxide equivalent value for the vehicles used on the route at grams/Km units.

`avg_passenger_count`: Average passenger count for the vehicles on the route.

For example:
```csv
route_id,avg_co2_per_vehicle_per_km,avg_passenger_count
1234,123,20
2345,0,0
3456,12.3,20.0
```

Emissions data is loaded from the gtfs package and embedded into the graph during the build process.


### Configuration
To enable this functionality, you need to enable the "Co2Emissions" feature in the
`otp-config.json` file.

```JSON
//otp-config.json
{
"Co2Emissions": true
}

```
Include the `emissions` object in the
`build-config.json` file. The `emissions` object should contain parameters called
`carAvgCo2PerKm` and `carAvgOccupancy`. The `carAvgCo2PerKm` provides the average emissions value for a car in g/km and
the `carAvgOccupancy` provides the average number of passengers in a car.

<!-- config BEGIN -->
<!-- NOTE! This section is auto-generated. Do not change, change doc in code instead. -->

### Example configuration

```JSON
// build-config.json
{
"emissions" : {
"carAvgCo2PerKm" : 170,
"carAvgOccupancy" : 1.3
}
}
```
### Overview

| Config Parameter | Type | Summary | Req./Opt. | Default Value | Since |
|------------------|:---------:|------------------------------------------------------------|:----------:|---------------|:-----:|
| carAvgCo2PerKm | `integer` | The average CO₂ emissions of a car in grams per kilometer. | *Optional* | `170` | na |
| carAvgOccupancy | `double` | The average number of passengers in a car. | *Optional* | `1.3` | na |


### Details


<!-- config END -->

## Changelog

### OTP 2.5

- Initial implementation of the emissions calculation.
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.opentripplanner.ext.emissions;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.io.File;
import org.junit.jupiter.api.Test;
import org.opentripplanner.graph_builder.issue.service.DefaultDataImportIssueStore;
import org.opentripplanner.test.support.ResourceLoader;

public class Co2EmissionsDataReaderTest {

private static final ResourceLoader RES = ResourceLoader.of(Co2EmissionsDataReaderTest.class);
private static final File CO2_GTFS_ZIP = RES.file("emissions-test-gtfs.zip");
private static final File CO2_GTFS = RES.file("emissions-test-gtfs/");
private static final File INVALID_CO2_GTFS = RES.file("emissions-invalid-test-gtfs/");

private Co2EmissionsDataReader co2EmissionsDataReader = new Co2EmissionsDataReader(
new DefaultDataImportIssueStore()
);

@Test
void testCo2EmissionsZipDataReading() {
var emissions = co2EmissionsDataReader.readGtfsZip(CO2_GTFS_ZIP);
assertEquals(6, emissions.size());
}

@Test
void testCo2EmissionsDataReading() {
var emissions = co2EmissionsDataReader.readGtfs(CO2_GTFS);
assertEquals(6, emissions.size());
}

@Test
void testInvalidCo2EmissionsDataReading() {
var emissions = co2EmissionsDataReader.readGtfs(INVALID_CO2_GTFS);
assertEquals(0, emissions.size());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package org.opentripplanner.ext.emissions;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.opentripplanner.transit.model._data.TransitModelForTest.id;

import java.time.OffsetDateTime;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.opentripplanner._support.time.ZoneIds;
import org.opentripplanner.framework.model.Grams;
import org.opentripplanner.model.StopTime;
import org.opentripplanner.model.plan.Itinerary;
import org.opentripplanner.model.plan.ScheduledTransitLeg;
import org.opentripplanner.model.plan.ScheduledTransitLegBuilder;
import org.opentripplanner.model.plan.StreetLeg;
import org.opentripplanner.street.search.TraverseMode;
import org.opentripplanner.transit.model._data.TransitModelForTest;
import org.opentripplanner.transit.model.basic.TransitMode;
import org.opentripplanner.transit.model.framework.Deduplicator;
import org.opentripplanner.transit.model.framework.FeedScopedId;
import org.opentripplanner.transit.model.organization.Agency;
import org.opentripplanner.transit.model.timetable.Trip;
import org.opentripplanner.transit.model.timetable.TripTimes;

class EmissionsTest {

private DefaultEmissionsService eService;
private EmissionsFilter emissionsFilter;

static final ZonedDateTime TIME = OffsetDateTime
.parse("2023-07-20T17:49:06+03:00")
.toZonedDateTime();

private static final Agency subject = Agency
.of(TransitModelForTest.id("F:1"))
.withName("Foo_CO")
.withTimezone("Europe/Helsinki")
.build();

@BeforeEach
void SetUp() {
Map<FeedScopedId, Double> emissions = new HashMap<>();
emissions.put(new FeedScopedId("F", "1"), (0.12 / 12));
emissions.put(new FeedScopedId("F", "2"), 0.0);
EmissionsDataModel emissionsDataModel = new EmissionsDataModel(emissions, 0.131);
this.eService = new DefaultEmissionsService(emissionsDataModel);
this.emissionsFilter = new EmissionsFilter(eService);
}

@Test
void testGetEmissionsForItinerary() {
var stopOne = TransitModelForTest.stopForTest("1:stop1", 60, 25);
var stopTwo = TransitModelForTest.stopForTest("1:stop1", 61, 25);
var stopThree = TransitModelForTest.stopForTest("1:stop1", 62, 25);
var stopPattern = TransitModelForTest.stopPattern(stopOne, stopTwo, stopThree);
var route = TransitModelForTest.route(id("1")).build();
var pattern = TransitModelForTest.tripPattern("1", route).withStopPattern(stopPattern).build();
var stoptime = new StopTime();
var stoptimes = new ArrayList<StopTime>();
stoptimes.add(stoptime);
var trip = Trip
.of(FeedScopedId.parse("FOO:BAR"))
.withMode(TransitMode.BUS)
.withRoute(route)
.build();
var leg = new ScheduledTransitLegBuilder<>()
.withTripTimes(new TripTimes(trip, stoptimes, new Deduplicator()))
.withTripPattern(pattern)
.withBoardStopIndexInPattern(0)
.withAlightStopIndexInPattern(2)
.withStartTime(TIME)
.withEndTime(TIME.plusMinutes(10))
.withServiceDate(TIME.toLocalDate())
.withZoneId(ZoneIds.BERLIN)
.build();
Itinerary i = new Itinerary(List.of(leg));
assertEquals(
new Grams(2223.902),
emissionsFilter.filter(List.of(i)).get(0).getEmissionsPerPerson().getCo2()
);
}

@Test
void testGetEmissionsForCarRoute() {
var leg = StreetLeg
.create()
.withMode(TraverseMode.CAR)
.withDistanceMeters(214.4)
.withStartTime(TIME)
.withEndTime(TIME.plus(1, ChronoUnit.HOURS))
.build();
Itinerary i = new Itinerary(List.of(leg));
assertEquals(
new Grams(28.0864),
emissionsFilter.filter(List.of(i)).get(0).getEmissionsPerPerson().getCo2()
);
}

@Test
void testNoEmissionsForFeedWithoutEmissionsConfigured() {
Map<FeedScopedId, Double> emissions = new HashMap<>();
emissions.put(new FeedScopedId("G", "1"), (0.12 / 12));
EmissionsDataModel emissionsDataModel = new EmissionsDataModel(emissions, 0.131);

this.eService = new DefaultEmissionsService(emissionsDataModel);
this.emissionsFilter = new EmissionsFilter(this.eService);

var route = TransitModelForTest.route(id("1")).withAgency(subject).build();
var pattern = TransitModelForTest
.tripPattern("1", route)
.withStopPattern(TransitModelForTest.stopPattern(3))
.build();
var stoptime = new StopTime();
var stoptimes = new ArrayList<StopTime>();
stoptimes.add(stoptime);
var trip = Trip
.of(FeedScopedId.parse("FOO:BAR"))
.withMode(TransitMode.BUS)
.withRoute(route)
.build();
var leg = new ScheduledTransitLegBuilder<>()
.withTripTimes(new TripTimes(trip, stoptimes, new Deduplicator()))
.withTripPattern(pattern)
.withBoardStopIndexInPattern(0)
.withAlightStopIndexInPattern(2)
.withStartTime(TIME)
.withEndTime(TIME.plusMinutes(10))
.withServiceDate(TIME.toLocalDate())
.withZoneId(ZoneIds.BERLIN)
.build();
Itinerary i = new Itinerary(List.of(leg));
assertEquals(null, emissionsFilter.filter(List.of(i)).get(0).getEmissionsPerPerson());
}

@Test
void testZeroEmissionsForItineraryWithZeroEmissions() {
var stopOne = TransitModelForTest.stopForTest("1:stop1", 60, 25);
var stopTwo = TransitModelForTest.stopForTest("1:stop1", 61, 25);
var stopThree = TransitModelForTest.stopForTest("1:stop1", 62, 25);
var stopPattern = TransitModelForTest.stopPattern(stopOne, stopTwo, stopThree);
var route = TransitModelForTest.route(id("2")).build();
var pattern = TransitModelForTest.tripPattern("1", route).withStopPattern(stopPattern).build();
var stoptime = new StopTime();
var stoptimes = new ArrayList<StopTime>();
stoptimes.add(stoptime);
var trip = Trip
.of(FeedScopedId.parse("FOO:BAR"))
.withMode(TransitMode.BUS)
.withRoute(route)
.build();
var leg = new ScheduledTransitLegBuilder<>()
.withTripTimes(new TripTimes(trip, stoptimes, new Deduplicator()))
.withTripPattern(pattern)
.withBoardStopIndexInPattern(0)
.withAlightStopIndexInPattern(2)
.withStartTime(TIME)
.withEndTime(TIME.plusMinutes(10))
.withServiceDate(TIME.toLocalDate())
.withZoneId(ZoneIds.BERLIN)
.build();
Itinerary i = new Itinerary(List.of(leg));
assertEquals(
new Grams(0.0),
emissionsFilter.filter(List.of(i)).get(0).getEmissionsPerPerson().getCo2()
);
}
}
Loading