diff --git a/Makefile b/Makefile
index dcef008e..45056218 100644
--- a/Makefile
+++ b/Makefile
@@ -131,6 +131,13 @@ $p/berlin-$V-network-with-pt.xml.gz: $p/berlin-$V-network.xml.gz
--merge-stops mergeToParentAndRouteTypes\
--shp $p/pt-area/pt-area.shp
+ $(sc) prepare endless-circle-line\
+ --network $p/berlin-$V-network-with-pt.xml.gz\
+ --transit-schedule $p/berlin-$V-transitSchedule.xml.gz\
+ --transit-vehicles $p/berlin-$V-transitVehicles.xml.gz\
+ --output-transit-schedule $p/berlin-$V-transitSchedule.xml.gz\
+ --output-transit-vehicles $p/berlin-$V-transitVehicles.xml.gz
+
$p/berlin-$V-counts-vmz.xml.gz: $p/berlin-$V-network.xml.gz
$(sc) prepare counts-from-vmz\
--excel ../shared-svn/projects/matsim-berlin/berlin-v5.5/original_data/vmz_counts_2018/Datenexport_2018_TU_Berlin.xlsx\
diff --git a/pom.xml b/pom.xml
index 2a1a83a4..ed1b6e22 100644
--- a/pom.xml
+++ b/pom.xml
@@ -77,6 +77,10 @@
xerces
xercesImpl
+
+ com.github.matsim-org
+ gtfs2matsim
+
@@ -128,7 +132,7 @@
com.github.matsim-org
gtfs2matsim
- 19f1676fc6
+ 45689bf834
org.matsim
diff --git a/src/main/java/org/matsim/prepare/RunOpenBerlinCalibration.java b/src/main/java/org/matsim/prepare/RunOpenBerlinCalibration.java
index 9edfb1da..95559b7f 100644
--- a/src/main/java/org/matsim/prepare/RunOpenBerlinCalibration.java
+++ b/src/main/java/org/matsim/prepare/RunOpenBerlinCalibration.java
@@ -62,6 +62,7 @@
import org.matsim.prepare.opt.RunCountOptimization;
import org.matsim.prepare.opt.SelectPlansFromIndex;
import org.matsim.prepare.population.*;
+import org.matsim.prepare.transit.EndlessCircleLineScheduleModifier;
import org.matsim.run.Activities;
import org.matsim.run.OpenBerlinScenario;
import org.matsim.run.scoring.AdvancedScoringConfigGroup;
@@ -94,7 +95,7 @@
GenerateSmallScaleCommercialTrafficDemand.class, CreateDataDistributionOfStructureData.class,
RunCountOptimization.class, SelectPlansFromIndex.class, ExtractPlanIndexFromType.class, AssignReferencePopulation.class,
ExtractRelevantFreightTrips.class, CheckCarAvailability.class, FixSubtourModes.class, ComputeTripChoices.class, ComputePlanChoices.class,
- ApplyNetworkParams.class, SetCarAvailabilityByAge.class, CreateDrtVehicles.class
+ ApplyNetworkParams.class, SetCarAvailabilityByAge.class, CreateDrtVehicles.class, EndlessCircleLineScheduleModifier.class
})
public class RunOpenBerlinCalibration extends MATSimApplication {
diff --git a/src/main/java/org/matsim/prepare/transit/EndlessCircleLineScheduleModifier.java b/src/main/java/org/matsim/prepare/transit/EndlessCircleLineScheduleModifier.java
new file mode 100644
index 00000000..25cd5355
--- /dev/null
+++ b/src/main/java/org/matsim/prepare/transit/EndlessCircleLineScheduleModifier.java
@@ -0,0 +1,314 @@
+/* *********************************************************************** *
+ * project: org.matsim.*
+ * *
+ * *********************************************************************** *
+ * *
+ * copyright : (C) 2024 by the members listed in the COPYING, *
+ * LICENSE and WARRANTY file. *
+ * email : info at matsim dot org *
+ * *
+ * *********************************************************************** *
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation; either version 2 of the License, or *
+ * (at your option) any later version. *
+ * See also COPYING, LICENSE and WARRANTY file *
+ * *
+ * *********************************************************************** */
+
+package org.matsim.prepare.transit;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.matsim.api.core.v01.Id;
+import org.matsim.api.core.v01.Scenario;
+import org.matsim.api.core.v01.network.Link;
+import org.matsim.application.MATSimAppCommand;
+import org.matsim.core.config.ConfigUtils;
+import org.matsim.core.network.io.MatsimNetworkReader;
+import org.matsim.core.population.routes.NetworkRoute;
+import org.matsim.core.population.routes.RouteUtils;
+import org.matsim.core.scenario.ScenarioUtils;
+import org.matsim.core.utils.collections.Tuple;
+import org.matsim.pt.transitSchedule.api.*;
+import org.matsim.pt.utils.TransitScheduleValidator;
+import org.matsim.vehicles.*;
+import picocli.CommandLine;
+
+import java.nio.file.Path;
+import java.util.*;
+
+// other schedule modifier in the make file implement Consumer. But here we modify schedule and vehicles, so we cannot use that.
+@CommandLine.Command(name = "endless-circle-line", description = "Modifies Berlin S41 and S42 to run circle multiple times.")
+public class EndlessCircleLineScheduleModifier implements MATSimAppCommand {
+
+ private static final Logger log = LogManager.getLogger(EndlessCircleLineScheduleModifier.class);
+
+ @CommandLine.Option(names = {"--transit-schedule"}, description = "Path to the transit schedule file", required = true)
+ private String transitSchedulePath;
+ @CommandLine.Option(names = {"--transit-vehicles"}, description = "Path to the transit vehicles file", required = true)
+ private String transitVehiclesPath;
+ @CommandLine.Option(names = {"--network"}, description = "Path to the network file (for validation only)", required = false)
+ private String networkPath;
+ @CommandLine.Option(names = "--output-transit-schedule", description = "Path to the output transit schedule file", required = true)
+ private Path outputTransitSchedule;
+ @CommandLine.Option(names = "--output-transit-vehicles", description = "Path to the output transit vehicles file", required = true)
+ private Path outputTransitVehicles;
+
+ private TransitSchedule schedule;
+ private Vehicles transitVehicles;
+ private TransitScheduleFactory transitScheduleFactory;
+
+ public static void main(String[] args) {
+ new EndlessCircleLineScheduleModifier().execute(args);
+ }
+
+ @Override
+ public Integer call() throws Exception {
+ replaceS41S42With2LoopingRoutesEach();
+ return 0;
+ }
+
+ @SuppressWarnings("rawtypes")
+ private void replaceS41S42With2LoopingRoutesEach() {
+ /*
+ * basic service pattern of the circle lines S41 and S42 is a 10 min headway service from 4:00 to 24:00 plus additional trains every 10 min
+ * from circa. 5:30 to 20:20 reinforcing to a 5 min headway.
+ *
+ * Here as an example values from a 2023 timetable:
+ * first S41---4715_0 departure at 4:08:24 in Beusselstr., then 10 min headway until 5:08:24, then 5 min until 20:18:24,
+ * then 10 min headway until 23:58:24.
+ *
+ * S42 was similar to S41 in December 2024, but was more complex in 2023:
+ * S42---5444_0 operated from 3:58:18 to,5:28:18 every 10 min, then every 5 min until 20:18:18, then every 10 min until 21:08:18.
+ * Then 5444_1 every 10 min from 21:18.18 until 22:08:18 (65 min loop time!) and 5444_5 from 22:23:18 every 10 min until 24:33:18.
+ *
+ * For simplification implement 2 looping TransitRoutes per circle line, both every 10 min with approximated first and last departure times.
+ */
+ double loopingTravelTime = 60 * 60.0;
+ double firstDepartureTime = 3 * 60 * 60. + 50 * 60.;
+ double lastDepartureTime = 24 * 60 * 60. + 30 * 60.;
+ double baseHeadway = 10 * 60.;
+ double peakHeadwayStart = 5 * 60 * 60. + 20 * 60.;
+ double peakHeadwayEnd = 20 * 60 * 60. + 20 * 60.;
+
+ Scenario scenario = ScenarioUtils.createScenario(ConfigUtils.createConfig());
+ schedule = scenario.getTransitSchedule();
+ transitScheduleFactory = schedule.getFactory();
+ transitVehicles = scenario.getTransitVehicles();
+ TransitScheduleReader scheduleReader = new TransitScheduleReader(scenario);
+ scheduleReader.readFile(transitSchedulePath);
+ MatsimVehicleReader vehicleReader = new MatsimVehicleReader(transitVehicles);
+ vehicleReader.readFile(transitVehiclesPath);
+
+ // attempt to find ids automatically (change with every new input gtfs file)
+ Tuple, Id> lineRouteS41 = findSingleLoopTransitRouteToCopy("S41");
+ Tuple, Id> lineRouteS42 = findSingleLoopTransitRouteToCopy("S42");
+
+ double typicalDepartureSecondS41 = findTypicalDepartureSecond(lineRouteS41.getFirst(), lineRouteS41.getSecond());
+ double typicalDepartureSecondS42 = findTypicalDepartureSecond(lineRouteS42.getFirst(), lineRouteS42.getSecond());
+
+ VehicleType vehicleType = findTypicalVehicleType(lineRouteS41.getFirst(), lineRouteS41.getSecond());
+
+ // S41
+ createLoopingTransitRoute(lineRouteS41.getFirst(), lineRouteS41.getSecond(),
+ Id.create(lineRouteS41.getFirst() + "_loop", TransitRoute.class),
+ loopingTravelTime, getNumberOfLoopings(firstDepartureTime, lastDepartureTime, loopingTravelTime),
+ baseHeadway, firstDepartureTime + typicalDepartureSecondS41, vehicleType);
+
+ createLoopingTransitRoute(lineRouteS41.getFirst(), lineRouteS41.getSecond(),
+ Id.create(lineRouteS41.getFirst() + "_loop_peak", TransitRoute.class),
+ loopingTravelTime, getNumberOfLoopings(peakHeadwayStart, peakHeadwayEnd, loopingTravelTime),
+ baseHeadway, peakHeadwayStart + typicalDepartureSecondS41 + baseHeadway / 2, vehicleType);
+
+ // add early morning service on following day
+ createLoopingTransitRoute(lineRouteS41.getFirst(), lineRouteS41.getSecond(),
+ Id.create(lineRouteS41.getFirst() + "_loop+24h", TransitRoute.class),
+ loopingTravelTime, getNumberOfLoopings(firstDepartureTime + 24 * 3600., 30 * 3600., loopingTravelTime),
+ baseHeadway, firstDepartureTime + 24 * 3600. + typicalDepartureSecondS41, vehicleType);
+
+ // delete old non-looping transit route
+ removeOldTransitRouteAndItsVehicles(lineRouteS41.getFirst(), lineRouteS41.getSecond());
+
+ // S42
+ createLoopingTransitRoute(lineRouteS42.getFirst(), lineRouteS42.getSecond(),
+ Id.create(lineRouteS42.getFirst() + "_loop", TransitRoute.class),
+ loopingTravelTime, getNumberOfLoopings(firstDepartureTime, lastDepartureTime, loopingTravelTime),
+ baseHeadway, firstDepartureTime + typicalDepartureSecondS42, vehicleType);
+
+ createLoopingTransitRoute(lineRouteS42.getFirst(), lineRouteS42.getSecond(),
+ Id.create(lineRouteS42.getFirst() + "_loop_peak", TransitRoute.class),
+ loopingTravelTime, getNumberOfLoopings(peakHeadwayStart, peakHeadwayEnd, loopingTravelTime),
+ baseHeadway, peakHeadwayStart + typicalDepartureSecondS42 + baseHeadway / 2, vehicleType);
+
+ // add early morning service on following day
+ createLoopingTransitRoute(lineRouteS42.getFirst(), lineRouteS42.getSecond(),
+ Id.create(lineRouteS42.getFirst() + "_loop+24h", TransitRoute.class),
+ loopingTravelTime, getNumberOfLoopings(firstDepartureTime + 24 * 3600., 30 * 3600., loopingTravelTime),
+ baseHeadway, firstDepartureTime + 24 * 3600. + typicalDepartureSecondS42, vehicleType);
+
+ removeOldTransitRouteAndItsVehicles(lineRouteS42.getFirst(), lineRouteS42.getSecond());
+
+ if (networkPath != null && !networkPath.isEmpty()) {
+ MatsimNetworkReader networkReader = new MatsimNetworkReader(scenario.getNetwork());
+ networkReader.readFile(networkPath);
+ TransitScheduleValidator.ValidationResult validationResult = TransitScheduleValidator.validateAll(schedule, scenario.getNetwork());
+ if (validationResult.isValid()) {
+ log.info("TransitSchedule is valid according to TransitScheduleValidator.");
+ } else {
+ log.error("TransitSchedule is invalid according to TransitScheduleValidator.");
+ for (TransitScheduleValidator.ValidationResult.ValidationIssue issue : validationResult.getIssues()) {
+ log.error(issue.getMessage());
+ }
+ throw new IllegalStateException("invalid output schedule");
+ }
+ }
+
+ TransitScheduleWriter transitScheduleWriter = new TransitScheduleWriter(schedule);
+ transitScheduleWriter.writeFile(outputTransitSchedule.toString());
+ MatsimVehicleWriter vehicleWriter = new MatsimVehicleWriter(transitVehicles);
+ vehicleWriter.writeFile(outputTransitVehicles.toString());
+ }
+
+ private void createLoopingTransitRoute(Id transitLineId, Id singleLoopingToCopyTransitRouteId,
+ Id loopingTransitRouteId,
+ double loopingTravelTime, long numberLoopings,
+ double headway, double firstDepartureTime,
+ VehicleType vehicleType) {
+
+ TransitLine lineToModify = schedule.getTransitLines().get(transitLineId);
+ TransitRoute routeToCopy = lineToModify.getRoutes().get(singleLoopingToCopyTransitRouteId);
+
+ List> loopingNetworkRouteLinks = new ArrayList<>();
+ List transitRouteStops = new ArrayList<>();
+ // add first stop manually
+ TransitRouteStop firstRouteStop = transitScheduleFactory.createTransitRouteStop(
+ routeToCopy.getStops().getLast().getStopFacility(),
+ routeToCopy.getStops().getFirst().getArrivalOffset(),
+ routeToCopy.getStops().getFirst().getDepartureOffset());
+ firstRouteStop.setAwaitDepartureTime(true);
+ transitRouteStops.add(firstRouteStop);
+
+ loopingNetworkRouteLinks.add(firstRouteStop.getStopFacility().getLinkId());
+
+ for (int loopingsDone = 0; loopingsDone < numberLoopings; loopingsDone++) {
+ loopingNetworkRouteLinks.addAll(routeToCopy.getRoute().getLinkIds());
+ loopingNetworkRouteLinks.add(routeToCopy.getRoute().getEndLinkId());
+
+ // skip first and last stop and add merged stop instead to avoid stopping twice at loopStartTransitStopId
+ for (TransitRouteStop stop : routeToCopy.getStops().subList(1, routeToCopy.getStops().size() - 1)) {
+ TransitRouteStop transitRouteStop = transitScheduleFactory.createTransitRouteStop(stop.getStopFacility(),
+ stop.getArrivalOffset().seconds() + loopingsDone * loopingTravelTime,
+ stop.getDepartureOffset().seconds() + loopingsDone * loopingTravelTime);
+ transitRouteStop.setAwaitDepartureTime(true);
+ transitRouteStops.add(transitRouteStop);
+ }
+ // add last stop of this looping which is first stop of next looping
+ TransitRouteStop lastRouteStop = transitScheduleFactory.createTransitRouteStop(
+ routeToCopy.getStops().getLast().getStopFacility(),
+ routeToCopy.getStops().getLast().getArrivalOffset().seconds() + loopingsDone * loopingTravelTime,
+ routeToCopy.getStops().getFirst().getDepartureOffset().seconds() + (loopingsDone + 1) * loopingTravelTime);
+ lastRouteStop.setAwaitDepartureTime(true);
+ transitRouteStops.add(lastRouteStop);
+ }
+ // at least for S41 and S42 last link in network route ends at same node as first link -> continuous
+ NetworkRoute networkRoute = RouteUtils.createNetworkRoute(loopingNetworkRouteLinks);
+ TransitRoute loopingRoute = transitScheduleFactory.createTransitRoute(loopingTransitRouteId, networkRoute, transitRouteStops, "multiple loopings in one route");
+ loopingRoute.setTransportMode(routeToCopy.getTransportMode());
+
+ int departureIdCounter = 0;
+ for (double departureTime = firstDepartureTime; departureTime < firstDepartureTime + loopingTravelTime; departureTime = departureTime + headway) {
+ Id departureId = Id.create(loopingTransitRouteId + "_" + departureIdCounter, Departure.class);
+ Departure departure = transitScheduleFactory.createDeparture(departureId, departureTime);
+ // create new vehicles and delete unused old ones later.
+ Id vehicleId = Id.createVehicleId("pt_" + loopingRoute.getId().toString() + "_" + departureIdCounter);
+ Vehicle vehicle = transitVehicles.getFactory().createVehicle(vehicleId, vehicleType);
+ transitVehicles.addVehicle(vehicle);
+ departure.setVehicleId(vehicleId);
+ loopingRoute.addDeparture(departure);
+ departureIdCounter++;
+ }
+
+ lineToModify.addRoute(loopingRoute);
+ }
+
+ private VehicleType findTypicalVehicleType(Id lineId, Id routeId) {
+ TransitRoute route = schedule.getTransitLines().get(lineId).getRoutes().get(routeId);
+ Optional exampleDepartureOptional = route.getDepartures().values().stream()
+ // find a typical VehicleType, avoid early hours short train
+ .filter(dep -> dep.getDepartureTime() > 8 * 60 * 60 && dep.getDepartureTime() < 20 * 60 * 60)
+ .findFirst();
+ if (exampleDepartureOptional.isEmpty()) {
+ log.error("No suitable Departure found in line {}, route {} to use as an example for the VehicleType to be used on the new endless looping TransitRoute.",
+ lineId, routeId);
+ throw new IllegalStateException("No suitable Departure found to define VehicleType.");
+ }
+ return transitVehicles.getVehicles().get(exampleDepartureOptional.get().getVehicleId()).getType();
+ }
+
+ private Tuple, Id> findSingleLoopTransitRouteToCopy(String lineName) {
+ List, Id>> candidates = new ArrayList<>();
+ for (TransitLine line : schedule.getTransitLines().values()) {
+ if (line.getAttributes().getAttribute("gtfs_route_short_name").equals(lineName)) {
+ for (TransitRoute route : line.getRoutes().values()) {
+ if (route.getStops().getFirst().getStopFacility().getStopAreaId().equals(
+ route.getStops().getLast().getStopFacility().getStopAreaId()) &&
+ route.getStops().size() == 28 &&
+ route.getStops().getLast().getArrivalOffset().seconds() > 58 * 60 &&
+ route.getStops().getLast().getArrivalOffset().seconds() < 60 * 60 &&
+ route.getDepartures().size() > 100) {
+ // is looping, has all stops and has travel time ca. 60 min (not 65 min) and a significant number of departures
+ // usually there is only one looping TransitRoute *_0 with > 200 departures or two looping TransitRoutes, of which one has > 150 departures and operates all day and the other has < 30 departures.
+ candidates.add(new Tuple<>(line.getId(), route.getId()));
+ }
+ }
+ }
+ }
+
+ switch (candidates.size()) {
+ case 0:
+ log.error("No suitable circle line and route found that loops with the correct number of stops and travel time for line {}. Check for construction work and timetable changes. A day with disturbed circle line is a bad choice.", lineName);
+ throw new IllegalStateException("No suitable line and route found that loops with the correct number of stops and travel time for line " + lineName);
+ case 1:
+ return candidates.getFirst();
+ default:
+ log.error("Found multiple circle line candidates for {}. This is unusual. Please check manually which is the best fit. Listing candidates here ", lineName);
+ for (Tuple, Id> tuple : candidates) {
+ log.error("line {}, route {}", tuple.getFirst(), tuple.getSecond());
+ }
+ throw new IllegalStateException("Aborting.");
+ }
+ }
+
+ /**
+ * Find typical departure time offset from hour. This is important to keep waiting times and headways in respect to other lines similar.
+ */
+ private double findTypicalDepartureSecond(Id transitLineId, Id transitRouteId) {
+ TransitRoute transitRoute = schedule.getTransitLines().get(transitLineId).getRoutes().get(transitRouteId);
+ Map departureSecond2Count = new HashMap<>();
+ for (Departure departure : transitRoute.getDepartures().values()) {
+ // usually operates every 10 or 5 minutes, so % 3600 reduces by hours and % 600 reduces by 10 min headway
+ double offsetFromHour = (departure.getDepartureTime() % 3600) % 600;
+ departureSecond2Count.put(offsetFromHour, departureSecond2Count.getOrDefault(offsetFromHour, 0) + 1);
+ }
+ Optional> mostFrequentDepartureSecond = departureSecond2Count.entrySet().stream()
+ .max(Comparator.comparingInt(Map.Entry::getValue));
+ // most frequent offset should be the one found during both 5 min and 10 min headways
+ if (mostFrequentDepartureSecond.isEmpty() || mostFrequentDepartureSecond.get().getValue() < 50) {
+ log.error("Could not determine typical departure time for {}, {}", transitLineId, transitRouteId);
+ throw new IllegalStateException("Could not determine typical departure time.");
+ }
+ return mostFrequentDepartureSecond.get().getKey();
+ }
+
+ private long getNumberOfLoopings(double firstDepartureTime, double lastDepartureTime, double loopTravelTime) {
+ return Math.round((lastDepartureTime - firstDepartureTime - loopTravelTime) / loopTravelTime);
+ }
+
+ private void removeOldTransitRouteAndItsVehicles(Id transitLineId, Id transitRouteId) {
+ TransitRoute oldNonLoopingTransitRouteToDelete = schedule.getTransitLines().get(transitLineId).getRoutes().get(transitRouteId);
+ oldNonLoopingTransitRouteToDelete.getDepartures().values().forEach(dep -> transitVehicles.removeVehicle(dep.getVehicleId()));
+ schedule.getTransitLines().get(transitLineId).removeRoute(oldNonLoopingTransitRouteToDelete);
+ }
+}
diff --git a/src/main/java/org/matsim/run/OpenBerlinScenario.java b/src/main/java/org/matsim/run/OpenBerlinScenario.java
index 8fdf8b5a..0a6afdfd 100644
--- a/src/main/java/org/matsim/run/OpenBerlinScenario.java
+++ b/src/main/java/org/matsim/run/OpenBerlinScenario.java
@@ -143,7 +143,6 @@ protected void prepareScenario(Scenario scenario) {
// add hbefa link attributes.
HbefaRoadTypeMapping roadTypeMapping = OsmHbefaMapping.build();
roadTypeMapping.addHbefaMappings(scenario.getNetwork());
-
}
@Override