-
Notifications
You must be signed in to change notification settings - Fork 1k
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
Changes from 29 commits
1faf8a0
689d98c
46c479b
9f27798
b7cd138
06f7f79
2df7d32
72c03b1
9af1ba5
2b0d663
a075ffa
740e0c8
4f56079
af88c7d
7207521
ab6efe8
571757e
241e784
7ec48e4
bd7285b
ccecb7e
ab48452
b13b7db
b87b7e5
d9447e3
1750e15
d904701
db30d39
72088fe
bf74026
12c8e21
9c6d5a1
4a00f67
c0f4bd8
8afd628
0d2634f
643e545
5e9bb96
d380b77
e39fb2a
9d95379
7116132
1741481
d99d903
5f33345
ffb7adf
cf1d627
bab72fc
70c8b2a
471e0eb
7dc9b2a
0b7e414
f12803e
8eb34b5
3ab8b67
aa5f235
0f331e9
95e5532
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
`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. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are pros/cons for including this in the GTFS files.
I do not have a strong opinion here, and I am not worried about the NeTEx integration, because it would be easy to fix. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The naming 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/ There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
---|---|---|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
} |
There was a problem hiding this comment.
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₂
)?There was a problem hiding this comment.
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.