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 29 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
21 changes: 20 additions & 1 deletion docs/BuildConfiguration.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ Sections follow that describe particular settings in more depth.
|       source | `uri` | The unique URI pointing to the data file. | *Required* | | 2.2 |
| 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 |
| [digitransitEmissions](#digitransitEmissions) | `object` | Configure properties for emissions file. | *Optional* | | na |
|    carAvgCo2 | `integer` | The average CO2 emissions of a car. | *Optional* | `170` | na |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind using CO₂ in comments and documentation, please (using native character )?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't mind - made the changes.

|    carAvgOccupancy | `double` | The average number of passengers in a car. | *Optional* | `1.3` | na |
| [elevationBucket](#elevationBucket) | `object` | Used to download NED elevation tiles from the given AWS S3 bucket. | *Optional* | | na |
| [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 |
Expand Down Expand Up @@ -697,6 +700,18 @@ values are defined in meters in the source data. If, for example, decimetres are
in the source data, this should be set to 0.1.


<h3 id="digitransitEmissions">digitransitEmissions</h3>

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

Configure properties for emissions file.

By specifying a URL to fetch emissions data, the program gains access to carbon dioxide (CO2)
emissions associated with transportation modes. This data is then used
to perform emission calculations for public transport modes and car travel.


<h3 id="elevationBucket">elevationBucket</h3>

**Since version:** `na` ∙ **Type:** `object` ∙ **Cardinality:** `Optional`
Expand Down Expand Up @@ -1163,7 +1178,11 @@ case where this is not the case.
"enabled" : true
}
}
]
],
"digitransitEmissions" : {
"carAvgCo2PerKm" : 170,
"carAvgOccupancy" : 1.3
}
}
```

Expand Down
1 change: 1 addition & 0 deletions docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,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 emissions calculation and data handling. | | ✓️ |
| `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
61 changes: 61 additions & 0 deletions docs/sandbox/DigitransitEmissions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Digitransit CO2 Emissions calculation

## Contact Info

- Digitransit Team

## Documentation

Graph build import of CO2 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 properties:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Emissions data is located in an emissions.txt file within a gtfs package and has the following properties:
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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are pros/cons for including this in the GTFS files.

  • It is easier to maintain consistency, linking to route/trips
    +/- It possible simplify the data pipeline
  • The GTFS files must be assembled if they have different sources (planning system/emission estimation system)
  • It can not be used with NeTEx
  • The GTFS files are not following the specification any more, probably good to keep it outside until it is adopted.

I do not have a strong opinion here, and I am not worried about the NeTEx integration, because it would be easy to fix.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The naming avg_co2_per_vehicle_per_km a bit long, repeat context and does not have the complet unit. Route id clearly indicate that this is per vehicle, and _per_km is missing the mass unit(gram?). So, instead I would keep the names short like avg_co2 and document the unit in the documentation.

A quick search also told me that there is probably a number of other parameters that we might have to add like CH4, CO, CO2, EC, HONO, N2O, NH3, NH4, NO, NO2, NO3, NOx, PM10, PM25, SO2, THC, TOG, and VOC, so keeping the name a bit shorter is a good thing :-) https://ipeagit.github.io/gtfs2emis/

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Route id clearly indicate that this is per vehicle" I don't think this is 100% correct, we thought we had to include the per vehicle in the name to differentiate total emissions of a route vs emissions per passenger. The fields used to have shorter names but the problem is that if this file is included in the GTFS packages, people will find the file without having any context as it's a custom extension and doesn't have any documentation on the GTFS website. Therefore, we felt like the fields should have names that really explain the values but seems like we have overlooked inclusion of the unit in the name (which would make it even longer).


For example:
```csv
route_id,avg_co2_per_vehicle_per_km,avg_passenger_count
1234,123,20
2345,0,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 `digitransitEmissions` object in the
`build-config.json` file. The `digitransitEmissions` object should contain parameters called
`carAvgCo2PerKm` and `carAvgOccupancy`. The `carAvgCo2PerKm` provides the average emissions value for a car and
the `carAvgOccupancy` provides the average number of passengers in a car.

```json
//build-config.json
{
"digitransitEmissions": {
"carAvgCo2PerKm": 170,
"carAvgOccupancy": 1.3
}
}
```
## Changelog

### OTP 2.5

- Initial implementation of the emissions calculation.


Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package org.opentripplanner.ext.emissions.digitransit;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
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.ext.digitransitemissions.DigitransitEmissions;
import org.opentripplanner.ext.digitransitemissions.DigitransitEmissionsService;
import org.opentripplanner.model.StopTime;
import org.opentripplanner.model.plan.Itinerary;
import org.opentripplanner.model.plan.Leg;
import org.opentripplanner.model.plan.ScheduledTransitLeg;
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 EmissionsServiceTest {

private DigitransitEmissionsService eService;

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, DigitransitEmissions> digitransitEmissions = new HashMap<>();
digitransitEmissions.put(
new FeedScopedId("F", "1"),
DigitransitEmissions.newDigitransitEmissions(0.12, 12)
);
digitransitEmissions.put(
new FeedScopedId("F", "2"),
DigitransitEmissions.newDigitransitEmissions(0, 0)
);
this.eService = new DigitransitEmissionsService(digitransitEmissions, 0.131);
}

@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();
List<Leg> legs = new ArrayList<>();
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 ScheduledTransitLeg(
new TripTimes(trip, stoptimes, new Deduplicator()),
pattern,
0,
2,
TIME,
TIME.plusMinutes(10),
TIME.toLocalDate(),
ZoneIds.BERLIN,
null,
null,
100,
null
);
legs.add(leg);
Itinerary i = new Itinerary(legs);
assertEquals(2223.902, eService.getEmissionsForItinerary(i));
}

@Test
void testGetEmissionsForCarRoute() {
List<Leg> legs = new ArrayList<>();
var leg = StreetLeg
.create()
.withMode(TraverseMode.CAR)
.withDistanceMeters(214.4)
.withStartTime(TIME)
.withEndTime(TIME.plus(1, ChronoUnit.HOURS))
.build();
legs.add(leg);
Itinerary i = new Itinerary(legs);
assertEquals(28.0864, eService.getEmissionsForItinerary(i));
}

@Test
void testNoEmissionsForFeedWithoutEmissionsConfigured() {
Map<FeedScopedId, DigitransitEmissions> digitransitEmissions = new HashMap<>();
digitransitEmissions.put(
new FeedScopedId("G", "1"),
DigitransitEmissions.newDigitransitEmissions(0.12, 12)
);
this.eService = new DigitransitEmissionsService(digitransitEmissions, 0.131);

var route = TransitModelForTest.route(id("1")).withAgency(subject).build();
List<Leg> legs = new ArrayList<>();
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 ScheduledTransitLeg(
new TripTimes(trip, stoptimes, new Deduplicator()),
pattern,
0,
2,
TIME,
TIME.plusMinutes(10),
TIME.toLocalDate(),
ZoneIds.BERLIN,
null,
null,
100,
null
);
legs.add(leg);
Itinerary i = new Itinerary(legs);
assertNull(eService.getEmissionsForItinerary(i));
}

@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();
List<Leg> legs = new ArrayList<>();
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 ScheduledTransitLeg(
new TripTimes(trip, stoptimes, new Deduplicator()),
pattern,
0,
2,
TIME,
TIME.plusMinutes(10),
TIME.toLocalDate(),
ZoneIds.BERLIN,
null,
null,
100,
null
);
legs.add(leg);
Itinerary i = new Itinerary(legs);
assertEquals(0, eService.getEmissionsForItinerary(i));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.opentripplanner._support.time.ZoneIds;
import org.opentripplanner.ext.digitransitemissions.DefaultEmissionsService;
import org.opentripplanner.ext.digitransitemissions.DefaultEmissionsServiceRepository;
import org.opentripplanner.ext.transmodelapi.TransmodelRequestContext;
import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore;
import org.opentripplanner.model.calendar.CalendarServiceData;
Expand Down Expand Up @@ -125,6 +127,7 @@ public class TripRequestMapperTest implements PlanTestConstants {
new DefaultWorldEnvelopeService(new DefaultWorldEnvelopeRepository()),
new DefaultVehiclePositionService(),
new DefaultVehicleRentalService(),
new DefaultEmissionsService(new DefaultEmissionsServiceRepository()),
RouterConfig.DEFAULT.flexConfig(),
List.of(),
null
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.opentripplanner.ext.digitransitemissions;

import jakarta.inject.Inject;
import java.io.Serializable;
import java.util.Optional;
import org.opentripplanner.model.plan.Itinerary;

public class DefaultEmissionsService implements Serializable, EmissionsService {

private EmissionsServiceRepository repository = null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know what the repository should actually be. It seems the standards differ a bit between different services. We could discuss in a dev meeting what the repository actually is.


@Inject
public DefaultEmissionsService(EmissionsServiceRepository repository) {
this.repository = repository;
}

@Override
public Double getEmissionsForItinerary(Itinerary itinerary) {
Optional<DigitransitEmissionsService> service = repository.retrieveEmissionsService();
if (service.isPresent()) {
return service.get().getEmissionsForItinerary(itinerary);
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.opentripplanner.ext.digitransitemissions;

import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import java.io.Serializable;
import java.util.Optional;
import javax.annotation.Nonnull;

@Singleton
public class DefaultEmissionsServiceRepository implements EmissionsServiceRepository, Serializable {

private volatile DigitransitEmissionsService emissionsService = null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, the relationship between service and repository is a bit confusing. Not sure if we should the service as a volatile field in a serialized(?) class.


@Inject
public DefaultEmissionsServiceRepository() {}

@Override
public Optional<DigitransitEmissionsService> retrieveEmissionsService() {
return Optional.ofNullable(emissionsService);
}

@Override
public void saveEmissionsService(@Nonnull DigitransitEmissionsService emissionsService) {
this.emissionsService = emissionsService;
}
}
Loading