From 3e798c9c01386a52254a175d6b8c7fac90b6dcb5 Mon Sep 17 00:00:00 2001 From: rakow Date: Fri, 26 Jan 2024 14:45:34 +0100 Subject: [PATCH] Railsim: Moving blocks & Deadlock avoidance (#3075) * first step to move reroute and link request into one method * prepare test and enum for moving blocks * start restructure rail resources * add approved dist to state, prepare some restructuring * change reservation logic to be based on distance, no exceptions for now, but slight difference in some tests still need fixing * disposition can now give an approved speed, update speed calculation to include approved speed from disposition, fixes the failing tests * overhaul of the resource management, removed completely from RailLink and put into separate classes * change resource interfaces to be distance based as well * use current train position for checking reservation distance * worked on moving block implementation, still WIP * worked on moving block implementation, still WIP * worked on moving block implementation, still WIP * add track number to reservation interface, this will allow for more fine-grained control by disposition * reworked track assignment in moving block * worked on resources, test partially working now * moving block train following test now working, prepare to add more tests and scenarion * add a multi-track test for moving block * add case when dist is 0 to calc target speed * adding another network and test case * small fixed, more complex mixed test case now working * fixed situation where train blocks a link that has the same length as itself, all moving block tests now working * prepare interfaces and network for deadlock tests * worked on DeadlockAvoidance interface and tests * allow to specify if one track for the opposite direction always needs to be reserved * implemented non blocking reserve mode for fixed blocks as well, improved interfaces, update tests * improved interfaces, first draft for simple deadlock avoidance * tests for simple deadlock avoidance * update deadlock tests, improve network * added events for stuck trains, reworked dead lock avoidance interface and simple strategy * update test cases, improve deadlock avoidance * fixed dead lock avoidance for moving blocks, enabled re-routing again, but still wip * deadlock avoidance checks for re-routing, fixed remaining issues in tests * add two more deadlock tests * update to junit5 --- contribs/railsim/pom.xml | 5 + .../matsim/contrib/railsim/RailsimUtils.java | 18 ++ .../railsim/analysis/RailsimCsvWriter.java | 3 +- .../RailsimLinkStateChangeEventMapper.java | 5 +- .../railsim/events/RailsimDetourEvent.java | 14 +- .../events/RailsimLinkStateChangeEvent.java | 18 +- .../railsim/qsimengine/FuzzyUtils.java | 7 +- .../railsim/qsimengine/RailResource.java | 62 ---- .../qsimengine/RailResourceManager.java | 200 ------------ .../railsim/qsimengine/RailsimCalc.java | 137 +++++---- .../railsim/qsimengine/RailsimEngine.java | 208 +++++++------ .../railsim/qsimengine/RailsimQSimEngine.java | 3 +- .../railsim/qsimengine/RailsimQSimModule.java | 6 +- .../qsimengine/RailsimTransitDriverAgent.java | 1 + .../contrib/railsim/qsimengine/TrainInfo.java | 4 +- .../railsim/qsimengine/TrainPosition.java | 86 ++++++ .../railsim/qsimengine/TrainState.java | 89 +++++- .../railsim/qsimengine/UpdateEvent.java | 2 + .../deadlocks/DeadlockAvoidance.java | 55 ++++ .../deadlocks/NoDeadlockAvoidance.java | 36 +++ .../deadlocks/SimpleDeadlockAvoidance.java | 117 +++++++ .../qsimengine/disposition/Detour.java | 13 + .../disposition/DispositionResponse.java | 11 + .../disposition/SimpleDisposition.java | 144 +++++++-- .../disposition/TrainDisposition.java | 26 +- .../resources/FixedBlockResource.java | 203 +++++++++++++ .../resources/MovingBlockResource.java | 286 ++++++++++++++++++ .../qsimengine/{ => resources}/RailLink.java | 115 ++----- .../qsimengine/resources/RailResource.java | 37 +++ .../resources/RailResourceInternal.java | 36 +++ .../resources/RailResourceManager.java | 215 +++++++++++++ .../ResourceState.java} | 15 +- .../qsimengine/resources/ResourceType.java | 15 + .../qsimengine/router/TrainRouter.java | 26 +- .../railsim/qsimengine/EventsAssert.java | 13 + .../railsim/qsimengine/RailsimCalcTest.java | 20 ++ .../qsimengine/RailsimDeadlockTest.java | 229 ++++++++++++++ .../RailsimEngineMovingBlockTest.java | 187 ++++++++++++ .../railsim/qsimengine/RailsimEngineTest.java | 4 +- .../railsim/qsimengine/RailsimTestUtils.java | 21 ++ .../resources/ReserveResourceTest.java | 81 +++++ .../microStationRerouting/trainNetwork.xml | 12 +- .../railsim/qsimengine/networkDeadlocks.xml | 171 +++++++++++ .../railsim/qsimengine/networkMesoUni.xml | 4 +- .../railsim/qsimengine/networkMixedTypes.xml | 197 ++++++++++++ .../qsimengine/networkMovingBlocks.xml | 99 ++++++ 46 files changed, 2660 insertions(+), 596 deletions(-) delete mode 100644 contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailResource.java delete mode 100644 contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailResourceManager.java create mode 100644 contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/TrainPosition.java create mode 100644 contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/deadlocks/DeadlockAvoidance.java create mode 100644 contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/deadlocks/NoDeadlockAvoidance.java create mode 100644 contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/deadlocks/SimpleDeadlockAvoidance.java create mode 100644 contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/disposition/Detour.java create mode 100644 contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/disposition/DispositionResponse.java create mode 100644 contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/resources/FixedBlockResource.java create mode 100644 contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/resources/MovingBlockResource.java rename contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/{ => resources}/RailLink.java (56%) create mode 100644 contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/resources/RailResource.java create mode 100644 contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/resources/RailResourceInternal.java create mode 100644 contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/resources/RailResourceManager.java rename contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/{TrackState.java => resources/ResourceState.java} (87%) create mode 100644 contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/resources/ResourceType.java create mode 100644 contribs/railsim/src/test/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimDeadlockTest.java create mode 100644 contribs/railsim/src/test/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimEngineMovingBlockTest.java create mode 100644 contribs/railsim/src/test/java/ch/sbb/matsim/contrib/railsim/qsimengine/resources/ReserveResourceTest.java create mode 100644 contribs/railsim/test/input/ch/sbb/matsim/contrib/railsim/qsimengine/networkDeadlocks.xml create mode 100644 contribs/railsim/test/input/ch/sbb/matsim/contrib/railsim/qsimengine/networkMixedTypes.xml create mode 100644 contribs/railsim/test/input/ch/sbb/matsim/contrib/railsim/qsimengine/networkMovingBlocks.xml diff --git a/contribs/railsim/pom.xml b/contribs/railsim/pom.xml index 345b4fd0cea..b6172ab206c 100644 --- a/contribs/railsim/pom.xml +++ b/contribs/railsim/pom.xml @@ -12,6 +12,11 @@ railsim + + it.unimi.dsi + fastutil + + org.assertj assertj-core diff --git a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/RailsimUtils.java b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/RailsimUtils.java index 6eff2e945f2..297e2c99da3 100644 --- a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/RailsimUtils.java +++ b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/RailsimUtils.java @@ -20,12 +20,14 @@ package ch.sbb.matsim.contrib.railsim; import ch.sbb.matsim.contrib.railsim.config.RailsimConfigGroup; +import ch.sbb.matsim.contrib.railsim.qsimengine.resources.ResourceType; import org.matsim.api.core.v01.network.Link; import org.matsim.vehicles.VehicleType; import java.math.BigDecimal; import java.math.RoundingMode; + /** * Utility class for working with Railsim and its specific attributes. * @@ -40,6 +42,7 @@ public final class RailsimUtils { public static final String LINK_ATTRIBUTE_MINIMUM_TIME = "railsimMinimumTime"; public static final String VEHICLE_ATTRIBUTE_ACCELERATION = "railsimAcceleration"; public static final String VEHICLE_ATTRIBUTE_DECELERATION = "railsimDeceleration"; + public static final String RESOURCE_TYPE = "railsimResourceType"; private RailsimUtils() { } @@ -155,4 +158,19 @@ public static void setTrainAcceleration(VehicleType vehicle, double acceleration vehicle.getAttributes().putAttribute(VEHICLE_ATTRIBUTE_ACCELERATION, acceleration); } + /** + * Return the resource type for a link, if not set, fixed block is assumed. + */ + public static ResourceType getResourceType(Link link) { + Object attr = link.getAttributes().getAttribute(RESOURCE_TYPE); + return attr != null ? ResourceType.valueOf(attr.toString()) : ResourceType.fixedBlock; + } + + /** + * Sets the resource type for the link. + */ + public static void setResourceType(Link link, ResourceType type) { + link.getAttributes().putAttribute(RESOURCE_TYPE, type.toString()); + } + } diff --git a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/analysis/RailsimCsvWriter.java b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/analysis/RailsimCsvWriter.java index 61462ddebbd..82beea8bb11 100644 --- a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/analysis/RailsimCsvWriter.java +++ b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/analysis/RailsimCsvWriter.java @@ -45,7 +45,7 @@ private RailsimCsvWriter() { * Write {@link RailsimLinkStateChangeEvent} to a csv file. */ public static void writeLinkStatesCsv(List events, String filename) throws UncheckedIOException { - String[] header = {"link", "time", "state", "vehicle", "track"}; + String[] header = {"link", "time", "state", "vehicle"}; try (CSVPrinter csv = new CSVPrinter(IOUtils.getBufferedWriter(filename), CSVFormat.DEFAULT.builder().setHeader(header).build())) { for (RailsimLinkStateChangeEvent event : events) { @@ -53,7 +53,6 @@ public static void writeLinkStatesCsv(List events, csv.print(event.getTime()); csv.print(event.getState().toString()); csv.print(event.getVehicleId() != null ? event.getVehicleId().toString() : ""); - csv.print(event.getTrack()); csv.println(); } } catch (IOException e) { diff --git a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/eventmappers/RailsimLinkStateChangeEventMapper.java b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/eventmappers/RailsimLinkStateChangeEventMapper.java index 97488e97e1d..cb1cc71c73b 100644 --- a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/eventmappers/RailsimLinkStateChangeEventMapper.java +++ b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/eventmappers/RailsimLinkStateChangeEventMapper.java @@ -20,7 +20,7 @@ package ch.sbb.matsim.contrib.railsim.eventmappers; import ch.sbb.matsim.contrib.railsim.events.RailsimLinkStateChangeEvent; -import ch.sbb.matsim.contrib.railsim.qsimengine.TrackState; +import ch.sbb.matsim.contrib.railsim.qsimengine.resources.ResourceState; import org.matsim.api.core.v01.Id; import org.matsim.api.core.v01.events.GenericEvent; import org.matsim.api.core.v01.network.Link; @@ -38,8 +38,7 @@ public RailsimLinkStateChangeEvent apply(GenericEvent event) { event.getTime(), asId(attributes.get(RailsimLinkStateChangeEvent.ATTRIBUTE_LINK), Link.class), asId(attributes.get(RailsimLinkStateChangeEvent.ATTRIBUTE_VEHICLE), Vehicle.class), - TrackState.valueOf(attributes.get(RailsimLinkStateChangeEvent.ATTRIBUTE_STATE)), - Integer.parseInt(attributes.get(RailsimLinkStateChangeEvent.ATTRIBUTE_TRACK)) + ResourceState.valueOf(attributes.get(RailsimLinkStateChangeEvent.ATTRIBUTE_STATE)) ); } diff --git a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/events/RailsimDetourEvent.java b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/events/RailsimDetourEvent.java index f5a84b0e8ff..70b2fddd1c1 100644 --- a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/events/RailsimDetourEvent.java +++ b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/events/RailsimDetourEvent.java @@ -39,17 +39,17 @@ public class RailsimDetourEvent extends Event implements HasVehicleId { public static final String EVENT_TYPE = "railsimDetourEvent"; private final Id vehicleId; - private final Id entry; - private final Id exit; + private final Id start; + private final Id end; private final List> detour; private final Id newStop; - public RailsimDetourEvent(double time, Id vehicleId, Id entry, Id exit, List> detour, + public RailsimDetourEvent(double time, Id vehicleId, Id start, Id end, List> detour, Id newStop) { super(time); this.vehicleId = vehicleId; - this.entry = entry; - this.exit = exit; + this.start = start; + this.end = end; this.detour = detour; this.newStop = newStop; } @@ -70,8 +70,8 @@ public Map getAttributes() { Map attributes = super.getAttributes(); attributes.put(HasVehicleId.ATTRIBUTE_VEHICLE, vehicleId.toString()); - attributes.put("entry", entry.toString()); - attributes.put("exit", exit.toString()); + attributes.put("start", start.toString()); + attributes.put("end", end.toString()); attributes.put("detour", detour.stream().map(Object::toString).collect(Collectors.joining(","))); attributes.put("newStop", Objects.toString(newStop)); diff --git a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/events/RailsimLinkStateChangeEvent.java b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/events/RailsimLinkStateChangeEvent.java index 55a73fdf6d7..04d2610406f 100644 --- a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/events/RailsimLinkStateChangeEvent.java +++ b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/events/RailsimLinkStateChangeEvent.java @@ -19,7 +19,7 @@ package ch.sbb.matsim.contrib.railsim.events; -import ch.sbb.matsim.contrib.railsim.qsimengine.TrackState; +import ch.sbb.matsim.contrib.railsim.qsimengine.resources.ResourceState; import org.matsim.api.core.v01.Id; import org.matsim.api.core.v01.events.Event; import org.matsim.api.core.v01.events.HasLinkId; @@ -30,26 +30,23 @@ import java.util.Map; /** - * Event thrown when the {@link ch.sbb.matsim.contrib.railsim.qsimengine.TrackState} of a {@link Link} changes. + * Event thrown when the {@link ResourceState} of a {@link Link} changes. */ public final class RailsimLinkStateChangeEvent extends Event implements HasLinkId, HasVehicleId { public static final String EVENT_TYPE = "railsimLinkStateChangeEvent"; public static final String ATTRIBUTE_STATE = "state"; - public static final String ATTRIBUTE_TRACK = "track"; private final Id linkId; private final Id vehicleId; - private final TrackState state; - private final int track; + private final ResourceState state; - public RailsimLinkStateChangeEvent(double time, Id linkId, Id vehicleId, TrackState state, int track) { + public RailsimLinkStateChangeEvent(double time, Id linkId, Id vehicleId, ResourceState state) { super(time); this.linkId = linkId; this.vehicleId = vehicleId; this.state = state; - this.track = track; } @Override @@ -67,21 +64,16 @@ public Id getVehicleId() { return this.vehicleId; } - public TrackState getState() { + public ResourceState getState() { return state; } - public int getTrack() { - return track; - } - @Override public Map getAttributes() { Map attr = super.getAttributes(); attr.put(ATTRIBUTE_LINK, this.linkId.toString()); attr.put(ATTRIBUTE_VEHICLE, this.vehicleId.toString()); attr.put(ATTRIBUTE_STATE, this.state.toString()); - attr.put(ATTRIBUTE_TRACK, String.valueOf(track)); return attr; } } diff --git a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/FuzzyUtils.java b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/FuzzyUtils.java index 3e27651c038..df3aaa9fdef 100644 --- a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/FuzzyUtils.java +++ b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/FuzzyUtils.java @@ -24,7 +24,12 @@ */ final class FuzzyUtils { - private static final double EPSILON = 1E-5; + /** + * The allowed deviation for small numbers to be considered equal. + * Contrary to intuition, this value should not be too large. + * It might happen that trains are moved too earlier over links. + */ + private static final double EPSILON = 1E-6; private FuzzyUtils() { } diff --git a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailResource.java b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailResource.java deleted file mode 100644 index ded83867f7d..00000000000 --- a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailResource.java +++ /dev/null @@ -1,62 +0,0 @@ -/* *********************************************************************** * - * project: org.matsim.* - * * - * *********************************************************************** * - * * - * copyright : (C) 2023 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 ch.sbb.matsim.contrib.railsim.qsimengine; - -import org.matsim.core.mobsim.framework.MobsimDriverAgent; - -import java.util.HashSet; -import java.util.List; -import java.util.Set; - - -/** - * A resource representing multiple {@link RailLink}. - */ -public class RailResource { - - /** - * Links belonging to this resource. - */ - final List links; - - /** - * Agents holding this resource exclusively. - */ - final Set reservations; - - /** - * Maximum number of reservations. - */ - int capacity; - - public RailResource(List links) { - this.links = links; - this.reservations = new HashSet<>(); - this.capacity = links.stream().mapToInt(RailLink::getNumberOfTracks).min().orElseThrow(); - } - - /** - * Whether an agent is able to block this resource. - */ - boolean hasCapacity() { - return reservations.size() < capacity; - } - -} diff --git a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailResourceManager.java b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailResourceManager.java deleted file mode 100644 index 6f00f25b9d6..00000000000 --- a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailResourceManager.java +++ /dev/null @@ -1,200 +0,0 @@ -/* *********************************************************************** * - * project: org.matsim.* - * * - * *********************************************************************** * - * * - * copyright : (C) 2023 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 ch.sbb.matsim.contrib.railsim.qsimengine; - -import ch.sbb.matsim.contrib.railsim.config.RailsimConfigGroup; -import ch.sbb.matsim.contrib.railsim.events.RailsimLinkStateChangeEvent; -import jakarta.inject.Inject; -import org.matsim.api.core.v01.Id; -import org.matsim.api.core.v01.IdMap; -import org.matsim.api.core.v01.network.Link; -import org.matsim.api.core.v01.network.Network; -import org.matsim.core.api.experimental.events.EventsManager; -import org.matsim.core.config.ConfigUtils; -import org.matsim.core.mobsim.framework.MobsimDriverAgent; -import org.matsim.core.mobsim.qsim.QSim; - -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * Class responsible for managing and blocking resources and segments of links. - */ -public final class RailResourceManager { - - private final EventsManager eventsManager; - - /** - * Rail links. - */ - private final Map, RailLink> links; - - private final Map, RailResource> resources; - - @Inject - public RailResourceManager(QSim qsim) { - this(qsim.getEventsManager(), ConfigUtils.addOrGetModule(qsim.getScenario().getConfig(), RailsimConfigGroup.class), qsim.getScenario().getNetwork()); - } - - /** - * Construct resources from network. - */ - public RailResourceManager(EventsManager eventsManager, RailsimConfigGroup config, Network network) { - this.eventsManager = eventsManager; - this.links = new IdMap<>(Link.class, network.getLinks().size()); - - Set modes = config.getNetworkModes(); - for (Map.Entry, ? extends Link> e : network.getLinks().entrySet()) { - if (e.getValue().getAllowedModes().stream().anyMatch(modes::contains)) - this.links.put(e.getKey(), new RailLink(e.getValue())); - } - - Map, List> collect = links.values().stream() - .filter(l -> l.resource != null) - .collect(Collectors.groupingBy(l -> l.resource, Collectors.toList()) - ); - - resources = new IdMap<>(RailResource.class, collect.size()); - for (Map.Entry, List> e : collect.entrySet()) { - resources.put(e.getKey(), new RailResource(e.getValue())); - } - } - - /** - * Get single link that belongs to an id. - */ - public RailLink getLink(Id id) { - return links.get(id); - } - - /** - * Return the resource for a given id. - */ - public RailResource getResource(Id id) { - if (id == null) return null; - return resources.get(id); - } - - /** - * Try to block a resource for a specific driver. - * - * @return true if the resource is now blocked or was blocked for this driver already. - */ - private boolean tryBlockResource(RailResource resource, MobsimDriverAgent driver) { - - if (resource.reservations.contains(driver)) - return true; - - if (resource.hasCapacity()) { - resource.reservations.add(driver); - return true; - } - - return false; - } - - /** - * Try to release a resource, but only if none of the links are blocked anymore by this driver. - * - * @return whether driver is still blocking this resource. - */ - private boolean tryReleaseResource(RailResource resource, MobsimDriverAgent driver) { - - if (resource.links.stream().noneMatch(l -> l.isBlockedBy(driver))) { - resource.reservations.remove(driver); - return true; - } - - return false; - } - - /** - * Try to block a track and the underlying resource and return whether it was successful. - */ - public boolean tryBlockTrack(double time, MobsimDriverAgent driver, RailLink link) { - - if (link.isBlockedBy(driver)) - return true; - - Id resourceId = link.getResourceId(); - if (resourceId != null) { - - RailResource resource = getResource(resourceId); - - // resource is required - if (!tryBlockResource(resource, driver)) { - return false; - } - } - - if (link.hasFreeTrack()) { - int track = link.blockTrack(driver); - eventsManager.processEvent(new RailsimLinkStateChangeEvent(Math.ceil(time), link.getLinkId(), - driver.getVehicle().getId(), TrackState.BLOCKED, track)); - - return true; - } - - return false; - } - - /** - * Checks whether a link or underlying resource has remaining capacity. - */ - public boolean hasCapacity(Id link) { - - RailLink l = getLink(link); - - if (!l.hasFreeTrack()) - return false; - - RailResource res = getResource(l.getResourceId()); - if (res != null) { - return res.hasCapacity(); - } - - return true; - } - - /** - * Whether a driver already reserved a link or would be able to reserve it. - */ - public boolean isBlockedBy(RailLink link, MobsimDriverAgent driver) { - // If a link is blocked, the resource must be blocked as well - return link.isBlockedBy(driver); - } - - /** - * Release a non-free track to be free again. - */ - public void releaseTrack(double time, MobsimDriverAgent driver, RailLink link) { - int track = link.releaseTrack(driver); - eventsManager.processEvent(new RailsimLinkStateChangeEvent(Math.ceil(time), link.getLinkId(), driver.getVehicle().getId(), - TrackState.FREE, track)); - - // Release held resources - if (link.getResourceId() != null) { - tryReleaseResource(getResource(link.getResourceId()), driver); - } - - } -} diff --git a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimCalc.java b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimCalc.java index 62437d0bfa7..117947728d3 100644 --- a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimCalc.java +++ b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimCalc.java @@ -19,13 +19,15 @@ package ch.sbb.matsim.contrib.railsim.qsimengine; +import ch.sbb.matsim.contrib.railsim.qsimengine.resources.RailLink; + import java.util.ArrayList; import java.util.List; /** * Utility class holding static calculation methods related to state (updates). */ -final class RailsimCalc { +public final class RailsimCalc { private RailsimCalc() { } @@ -49,6 +51,7 @@ static double solveTraveledDist(double speed, double dist, double acceleration) /** * Calculate time needed to advance distance {@code dist}. Depending on acceleration and max speed. + * If dist can never be reached, will return time needed to stop. */ static double calcRequiredTime(TrainState state, double dist) { @@ -84,7 +87,7 @@ static double calcRequiredTime(TrainState state, double dist) { } else if (dist <= max) { return solveTraveledDist(state.speed, dist, state.acceleration); } else - return Double.POSITIVE_INFINITY; + return decelTime; } } @@ -100,6 +103,12 @@ static SpeedTarget calcTargetSpeed(double dist, double acceleration, double dece return new SpeedTarget(finalSpeed, Double.POSITIVE_INFINITY); } + // Distance could be zero, speeds must be already equal then + if (FuzzyUtils.equals(dist, 0)) { + assert FuzzyUtils.equals(currentSpeed, finalSpeed) : "Current speed must be equal to allowed speed"; + return new SpeedTarget(finalSpeed, 0); + } + double timeDecel = (allowedSpeed - finalSpeed) / deceleration; double distDecel = calcTraveledDist(allowedSpeed, timeDecel, -deceleration); @@ -162,83 +171,92 @@ static double calcTargetSpeedForStop(double dist, double acceleration, double de } /** - * Calculate when the reservation function should be triggered. - * Should return {@link Double#POSITIVE_INFINITY} if this distance is far in the future and can be checked at later point. - * - * @param state current train state - * @param currentLink the link where the train head is on - * @return travel distance after which reservations should be updated. + * Calculate the minimum distance that needs to be reserved for the train, such that it can stop safely. */ - static double nextLinkReservation(TrainState state, RailLink currentLink) { - - // on way to pt stop, no need to reserve anymore - if (state.isStop(currentLink.getLinkId()) && FuzzyUtils.lessThan(state.headPosition, currentLink.length)) - return Double.POSITIVE_INFINITY; + static double calcReservationDistance(TrainState state, RailLink currentLink) { double assumedSpeed = calcPossibleMaxSpeed(state); - // time needed for full stop - double stopTime = assumedSpeed / state.train.deceleration(); + // stop at end of link + if (state.isStop(currentLink.getLinkId())) + return currentLink.length - state.headPosition; - assert stopTime > 0 : "Stop time can not be negative"; + double stopTime = assumedSpeed / state.train.deceleration(); // safety distance double safety = RailsimCalc.calcTraveledDist(assumedSpeed, stopTime, -state.train.deceleration()); + double distToNextStop = currentLink.length - state.headPosition; int idx = state.routeIdx; - double dist = currentLink.length - state.headPosition; - - RailLink nextLink = null; - // need to check beyond safety distance - while (FuzzyUtils.lessEqualThan(dist, safety * 2) && idx < state.route.size()) { - nextLink = state.route.get(idx++); - if (!nextLink.isBlockedBy(state.driver)) - return dist - safety; + while (idx < state.route.size()) { + RailLink nextLink = state.route.get(idx++); + distToNextStop += nextLink.length; - // No reservation beyond pt stop if (state.isStop(nextLink.getLinkId())) break; - - dist += nextLink.length; } - // No reservation needed after the end - if (idx == state.route.size() || (nextLink != null && state.isStop(nextLink.getLinkId()))) - return Double.POSITIVE_INFINITY; + return Math.min(safety, distToNextStop); + } + + /** + * Calculate the projected driven distance, based on current position and state. + */ + public static double projectedDistance(double time, TrainPosition position) { + + if (!(position instanceof TrainState state)) + throw new IllegalArgumentException("Position must be a TrainState."); + + double elapsed = time - state.timestamp; + + if (elapsed == 0) + return 0; + + double accelTime = (state.targetSpeed - state.speed) / state.acceleration; + + double dist; + if (state.acceleration == 0) { + dist = state.speed * elapsed; + + } else if (accelTime < elapsed) { + // Travelled distance under constant acceleration + dist = RailsimCalc.calcTraveledDist(state.speed, accelTime, state.acceleration); - return dist - safety; + // Remaining time at constant speed + if (state.acceleration > 0) + dist += RailsimCalc.calcTraveledDist(state.targetSpeed, elapsed - accelTime, 0); + + } else { + // Acceleration was constant the whole time + dist = RailsimCalc.calcTraveledDist(state.speed, elapsed, state.acceleration); + } + + return dist; } /** * Links that need to be blocked or otherwise stop needs to be initiated. + * This method is only valid for fixed block resources. */ - static List calcLinksToBlock(TrainState state, RailLink currentLink) { + public static List calcLinksToBlock(TrainPosition position, RailLink currentLink, double reserveDist) { List result = new ArrayList<>(); - // Currently driving to pt stop - if (state.isStop(currentLink.getLinkId()) && FuzzyUtils.lessThan(state.headPosition, currentLink.length)) - return result; + // Assume current distance left on link is already reserved (only for fixed block) + double dist = currentLink.length - position.getHeadPosition(); - double assumedSpeed = calcPossibleMaxSpeed(state); - double stopTime = assumedSpeed / state.train.deceleration(); - - // safety distance - double safety = RailsimCalc.calcTraveledDist(assumedSpeed, stopTime, -state.train.deceleration()); - - int idx = state.routeIdx; + int idx = position.getRouteIndex(); - // dist to next - double dist = currentLink.length - state.headPosition; + // This function always needs to provide more reserve distance than requested (except when it will stop) + while (FuzzyUtils.lessEqualThan(dist, reserveDist) && idx < position.getRouteSize()) { + RailLink nextLink = position.getRoute(idx++); + dist += nextLink.length; - while (FuzzyUtils.lessEqualThan(dist, safety) && idx < state.route.size()) { - RailLink nextLink = state.route.get(idx++); result.add(nextLink); - dist += nextLink.length; - // Beyond pt stop links don't need to be reserved - if (state.isStop(nextLink.getLinkId())) + // Don't block beyond stop + if (position.isStop(nextLink.getLinkId())) break; } @@ -246,18 +264,27 @@ static List calcLinksToBlock(TrainState state, RailLink currentLink) { } /** - * Whether re-routing should be tried. - * - * @param upcoming the upcoming links the train tried to block. + * Calculate distance to the next stop. */ - static boolean considerReRouting(List upcoming, RailLink currentLink) { - return currentLink.isEntryLink() || upcoming.stream().anyMatch(RailLink::isEntryLink); + public static double calcDistToNextStop(TrainPosition position, RailLink currentLink) { + double dist = currentLink.length - position.getHeadPosition(); + + int idx = position.getRouteIndex(); + while (idx < position.getRouteSize()) { + RailLink nextLink = position.getRoute(idx++); + dist += nextLink.length; + + if (position.isStop(nextLink.getLinkId())) + break; + } + + return dist; } /** * Maximum speed of the next upcoming links. */ - private static double calcPossibleMaxSpeed(TrainState state) { + static double calcPossibleMaxSpeed(TrainState state) { double safety = RailsimCalc.calcTraveledDist(state.train.maxVelocity(), state.train.maxVelocity() / state.train.deceleration(), -state.train.deceleration()); double maxSpeed = state.allowedMaxSpeed; diff --git a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimEngine.java b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimEngine.java index 6573ed1b47e..ff98123051a 100644 --- a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimEngine.java +++ b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimEngine.java @@ -26,14 +26,13 @@ import java.util.Queue; import java.util.stream.Collectors; +import ch.sbb.matsim.contrib.railsim.qsimengine.disposition.DispositionResponse; +import ch.sbb.matsim.contrib.railsim.qsimengine.resources.RailLink; +import ch.sbb.matsim.contrib.railsim.qsimengine.resources.RailResourceManager; 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.events.Event; -import org.matsim.api.core.v01.events.LinkEnterEvent; -import org.matsim.api.core.v01.events.LinkLeaveEvent; -import org.matsim.api.core.v01.events.PersonEntersVehicleEvent; -import org.matsim.api.core.v01.events.VehicleEntersTrafficEvent; +import org.matsim.api.core.v01.events.*; import org.matsim.api.core.v01.network.Link; import org.matsim.core.api.experimental.events.EventsManager; import org.matsim.core.mobsim.framework.MobsimDriverAgent; @@ -52,6 +51,11 @@ */ final class RailsimEngine implements Steppable { + /** + * Additional safety distance in meter that is added to the reservation distance. + * Ensure that trains always have enough distance to progress. + */ + private static double SAFETY_DIST = 10; private static final Logger log = LogManager.getLogger(RailsimEngine.class); private final EventsManager eventsManager; private final RailsimConfigGroup config; @@ -210,8 +214,8 @@ private void checkTrackReservation(double time, UpdateEvent event) { boolean allBlocked = blockLinkTracks(time, state); - // Driver can advance if the next link is already free - if (allBlocked || (nextLink != null && nextLink.isBlockedBy(state.driver))) { + // Driver can advance if there is approved distance + if (allBlocked || FuzzyUtils.greaterThan(state.approvedDist, 0)) { if (allBlocked) event.checkReservation = -1; @@ -219,8 +223,11 @@ private void checkTrackReservation(double time, UpdateEvent event) { event.checkReservation = time + config.pollInterval; } - // Train already waits at the end of previous link - if (event.waitingForLink) { + // Train already waits at the end of previous link, next link is already blocked + if (event.waitingForLink && + nextLink != null && + resources.isBlockedBy(nextLink, state) && + FuzzyUtils.equals(resources.getLink(state.headLink).length, state.headPosition)) { enterLink(time, event); event.waitingForLink = false; @@ -238,7 +245,7 @@ private void checkTrackReservation(double time, UpdateEvent event) { event.checkReservation = time + config.pollInterval; // If train is already standing still and waiting, there is no update needed. - if (event.waitingForLink) { + if (event.waitingForLink && FuzzyUtils.equals(state.speed, 0)) { event.plannedTime = time + config.pollInterval; } else { decideNextUpdate(event); @@ -259,7 +266,7 @@ private void updateDeparture(double time, UpdateEvent event) { state.tailPosition = firstLink.length - state.train.length(); // reserve links and start if first one is free - if (blockLinkTracks(time, state) || resources.isBlockedBy(firstLink, state.driver)) { + if (blockLinkTracks(time, state) || resources.isBlockedBy(firstLink, state)) { createEvent(new PersonEntersVehicleEvent(time, state.driver.getId(), state.driver.getVehicle().getId())); createEvent(new VehicleEntersTrafficEvent(time, state.driver.getId(), @@ -296,74 +303,48 @@ private void updateDeparture(double time, UpdateEvent event) { */ private boolean blockLinkTracks(double time, TrainState state) { - List links = RailsimCalc.calcLinksToBlock(state, resources.getLink(state.headLink)); + double reserveDist = RailsimCalc.calcReservationDistance(state, resources.getLink(state.headLink)); - if (links.isEmpty()) - return true; + DispositionResponse response = disposition.requestNextSegment(time, state, reserveDist); - if (state.pt != null && RailsimCalc.considerReRouting(links, resources.getLink(state.headLink))) { + // Disposition assigned a detour + if (state.pt != null && response.detour() != null) { - int start = -1; - int end = -1; - RailLink entry = null; - RailLink exit = null; + List subRoute = state.route.subList(response.detour().startIdx(), response.detour().endIdx()); - for (int i = Math.max(0, state.routeIdx - 1); i < state.route.size(); i++) { - RailLink l = state.route.get(i); - - if (l.isEntryLink()) { - entry = l; - start = i; - } else if (start > -1 && l.isBlockedBy(state.driver)) { - // check if any link beyond entry is already blocked - // if that is the case re-route is not possible anymore - break; - } else if (start > -1 && l.isExitLink()) { - exit = l; - end = i; - break; - } - } + TransitStopFacility newStop = state.pt.addDetour(subRoute, response.detour().newRoute()); - // there might be no exit link if this is the end of the route - // exit will be set to null if re-route is too late - // network could be wrong as well, but hard to verify - if (exit != null) { + subRoute.clear(); + subRoute.addAll(response.detour().newRoute()); - // check if this route is different - List subRoute = state.route.subList(start + 1, end); - - List detour = disposition.requestRoute(time, state.pt, subRoute, entry, exit); - - if (detour != null && !subRoute.equals(detour)) { - - TransitStopFacility newStop = state.pt.addDetour(subRoute, detour); + if (newStop != null) { + state.nextStop = newStop; + } - subRoute.clear(); - subRoute.addAll(detour); + createEvent(new RailsimDetourEvent( + time, state.driver.getVehicle().getId(), + response.detour().startLink(), response.detour().endLink(), + response.detour().newRoute().stream().map(RailLink::getLinkId).toList(), + newStop != null ? newStop.getId() : null + )); - if (newStop != null) { - state.nextStop = newStop; - } + // reserve the assigned links from the detour + response = disposition.requestNextSegment(time, state, reserveDist); + } - createEvent(new RailsimDetourEvent( - time, state.driver.getVehicle().getId(), - entry.getLinkId(), exit.getLinkId(), - detour.stream().map(RailLink::getLinkId).toList(), - newStop != null ? newStop.getId() : null - )); + state.approvedDist = response.approvedDist(); - // Block links again using the updated route - links = RailsimCalc.calcLinksToBlock(state, resources.getLink(state.headLink)); + assert FuzzyUtils.greaterEqualThan(state.approvedDist, 0) : "Approved distance must be positive, but was " + response.approvedDist(); - } - } - } - - List blocked = disposition.blockRailSegment(time, state.driver, links); + // At the moment the approved speed from the disposition is not used + // Stop when the approved distance is not enough + if (FuzzyUtils.lessThan(state.approvedDist, reserveDist + SAFETY_DIST)) + state.approvedSpeed = 0; + else + state.approvedSpeed = Double.POSITIVE_INFINITY; - // Only continue successfully if all requested link have been blocked - return links.size() == blocked.size(); + // Return whether the train has to stop + return state.approvedSpeed != 0; } private void enterLink(double time, UpdateEvent event) { @@ -393,7 +374,7 @@ private void enterLink(double time, UpdateEvent event) { // Free all reservations for (RailLink link : state.route) { - if (link.isBlockedBy(state.driver)) { + if (resources.isBlockedBy(link, state)) { disposition.unblockRailLink(time, state.driver, link); } } @@ -412,7 +393,7 @@ private void enterLink(double time, UpdateEvent event) { RailLink currentLink = state.route.get(state.routeIdx); // If this linked is blocked the driver can continue - if (!currentLink.isBlockedBy(state.driver)) { + if (!resources.isBlockedBy(currentLink, state)) { event.waitingForLink = true; event.type = UpdateEvent.Type.WAIT_FOR_RESERVATION; event.plannedTime = time + config.pollInterval; @@ -424,15 +405,16 @@ private void enterLink(double time, UpdateEvent event) { createEvent(new LinkLeaveEvent(time, state.driver.getVehicle().getId(), state.headLink)); // Get link and increment - state.headPosition = 0; state.headLink = state.route.get(state.routeIdx++).getLinkId(); - state.driver.notifyMoveOverNode(state.headLink); - createEvent(new LinkEnterEvent(time, state.driver.getVehicle().getId(), state.headLink)); + assert resources.isBlockedBy(resources.getLink(state.headLink), state) : "Link has to be blocked by driver when entered"; - RailLink link = resources.getLink(state.headLink); + state.headPosition = 0; + // Reset waiting flag + event.waitingForLink = false; - assert link.isBlockedBy(state.driver) : "Link has to be blocked by driver when entered"; + state.driver.notifyMoveOverNode(state.headLink); + createEvent(new LinkEnterEvent(time, state.driver.getVehicle().getId(), state.headLink)); decideTargetSpeed(event, state); @@ -529,18 +511,20 @@ private void updatePosition(double time, UpdateEvent event) { state.headPosition += dist; state.tailPosition += dist; + state.approvedDist -= dist; if (Double.isFinite(state.targetDecelDist)) { state.targetDecelDist -= dist; } // When trains are put into the network their tail may be longer than the current link - // this assertion may not hold depending on the network, should possibly be removed - assert state.routeIdx <= 2 || FuzzyUtils.greaterEqualThan(state.tailPosition, 0) : "Illegal state update. Tail position should not be negative"; + // this assertion may not hold depending on the network +// assert state.routeIdx <= 2 || FuzzyUtils.greaterEqualThan(state.tailPosition, 0) : "Illegal state update. Tail position should not be negative"; assert FuzzyUtils.lessEqualThan(state.headPosition, resources.getLink(state.headLink).length) : "Illegal state update. Head position must be smaller than link length"; assert FuzzyUtils.greaterEqualThan(state.headPosition, 0) : "Head position must be positive"; assert FuzzyUtils.lessEqualThan(state.speed, state.allowedMaxSpeed) : "Speed must be less equal than the allowed speed"; + assert FuzzyUtils.greaterEqualThan(state.approvedDist, 0) : "Approved distance must be positive"; state.timestamp = time; @@ -587,19 +571,24 @@ private void decideNextUpdate(UpdateEvent event) { // (3) next link needs reservation double reserveDist = Double.POSITIVE_INFINITY; if (!state.isRouteAtEnd() && !event.isAwaitingReservation()) { - reserveDist = RailsimCalc.nextLinkReservation(state, currentLink); + // Check first if more distance than for the next stop is needed + double nextStop = RailsimCalc.calcDistToNextStop(state, currentLink); + + if (!FuzzyUtils.equals(state.approvedDist, nextStop)) { + double requiredDist = RailsimCalc.calcReservationDistance(state, currentLink); + // when the approved distance is equal to the required distance, new reservation needs to be made + reserveDist = state.approvedDist - requiredDist; + } + + // With moving blocks the reserve distance might not be set to 0, but rather the accel or decel distance if (reserveDist < 0) reserveDist = 0; // Outside of block track the reserve distance is always greater 0 // infinite loops would occur otherwise if (!(event.type != UpdateEvent.Type.BLOCK_TRACK || FuzzyUtils.greaterThan(reserveDist, 0))) { - // There are here for debugging - List tmp = RailsimCalc.calcLinksToBlock(state, currentLink); - double r = RailsimCalc.nextLinkReservation(state, currentLink); - - throw new AssertionError("Reserve distance must be positive, but was " + r); + throw new AssertionError("Reserve distance must be positive, but was " + reserveDist); } } @@ -615,7 +604,18 @@ private void decideNextUpdate(UpdateEvent event) { // Find the earliest required update double dist; - if (reserveDist <= accelDist && reserveDist <= decelDist && reserveDist <= tailDist && reserveDist <= headDist) { + if (FuzzyUtils.lessEqualThan(tailDist, decelDist) && + FuzzyUtils.lessEqualThan(tailDist, reserveDist) && + FuzzyUtils.lessEqualThan(tailDist, headDist) && + FuzzyUtils.lessEqualThan(tailDist, accelDist)) { + + // leave link has priority, and is ensured to be executed before others at same time step + // otherwise train might block links of same length as itself + + dist = tailDist; + event.type = UpdateEvent.Type.LEAVE_LINK; + + } else if (reserveDist <= accelDist && reserveDist <= decelDist && reserveDist <= tailDist && reserveDist <= headDist) { dist = reserveDist; event.type = UpdateEvent.Type.BLOCK_TRACK; } else if (accelDist <= decelDist && accelDist <= reserveDist && accelDist <= tailDist && accelDist <= headDist) { @@ -624,9 +624,6 @@ private void decideNextUpdate(UpdateEvent event) { } else if (decelDist <= accelDist && decelDist <= reserveDist && decelDist <= tailDist && decelDist <= headDist) { dist = decelDist; event.type = UpdateEvent.Type.SPEED_CHANGE; - } else if (tailDist <= decelDist && tailDist <= reserveDist && tailDist <= headDist) { - dist = tailDist; - event.type = UpdateEvent.Type.LEAVE_LINK; } else { dist = headDist; event.type = UpdateEvent.Type.ENTER_LINK; @@ -674,7 +671,28 @@ private void decideTargetSpeed(UpdateEvent event, TrainState state) { state.targetSpeed = state.allowedMaxSpeed; state.targetDecelDist = Double.POSITIVE_INFINITY; - for (int i = state.routeIdx; i <= state.route.size(); i++) { + boolean stop = false; + + // Set speed approved by disposition + if (state.approvedSpeed < minAllowed) { + + RailsimCalc.SpeedTarget target = RailsimCalc.calcTargetSpeed(state.approvedDist, + state.train.acceleration(), state.train.deceleration(), + state.speed, state.allowedMaxSpeed, state.approvedSpeed); + + assert FuzzyUtils.greaterEqualThan(target.decelDist(), 0) : "Decel dist is " + target.decelDist() + ", stopping is not possible"; + + if (FuzzyUtils.equals(target.decelDist(), 0)) { + state.targetSpeed = state.approvedSpeed; + state.targetDecelDist = Double.POSITIVE_INFINITY; + stop = true; + } else { + state.targetSpeed = target.targetSpeed(); + state.targetDecelDist = target.decelDist(); + } + } + + for (int i = state.routeIdx; i <= state.route.size() && !stop; i++) { RailLink link; double allowed; @@ -688,8 +706,6 @@ private void decideTargetSpeed(UpdateEvent event, TrainState state) { // train stops at the very end of a link if (i > 0 && state.isStop(state.route.get(i - 1).getLinkId())) allowed = 0; - else if (!resources.isBlockedBy(link, state.driver)) - allowed = 0; else allowed = link.getAllowedFreespeed(state.driver); } @@ -723,7 +739,7 @@ else if (!resources.isBlockedBy(link, state.driver)) if (link != null) dist += link.length; - if (dist >= window) + if (FuzzyUtils.greaterEqualThan(dist, window)) break; minAllowed = allowed; @@ -761,4 +777,16 @@ private double retrieveAllowedMaxSpeed(TrainState state) { return maxSpeed; } + /** + * Remove all trains from simulation and generate events at the end of the day. + * + * @param now end of day time + */ + void clearTrains(double now) { + + for (TrainState train : activeTrains) { + eventsManager.processEvent(new VehicleAbortsEvent(now, train.driver.getVehicle().getId(), train.headLink)); + eventsManager.processEvent(new PersonStuckEvent(now, train.driver.getId(), train.headLink, train.driver.getMode())); + } + } } diff --git a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimQSimEngine.java b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimQSimEngine.java index 88ee0b22cbc..2e65080b4a1 100644 --- a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimQSimEngine.java +++ b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimQSimEngine.java @@ -21,6 +21,7 @@ import ch.sbb.matsim.contrib.railsim.config.RailsimConfigGroup; import ch.sbb.matsim.contrib.railsim.qsimengine.disposition.TrainDisposition; +import ch.sbb.matsim.contrib.railsim.qsimengine.resources.RailResourceManager; import com.google.inject.Inject; import org.matsim.api.core.v01.Id; import org.matsim.api.core.v01.network.Link; @@ -79,7 +80,7 @@ public void onPrepareSim() { @Override public void afterSim() { - + engine.clearTrains(qsim.getSimTimer().getTimeOfDay()); } @Override diff --git a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimQSimModule.java b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimQSimModule.java index 77e4e04133b..5add9a20252 100644 --- a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimQSimModule.java +++ b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimQSimModule.java @@ -19,8 +19,11 @@ package ch.sbb.matsim.contrib.railsim.qsimengine; +import ch.sbb.matsim.contrib.railsim.qsimengine.deadlocks.DeadlockAvoidance; +import ch.sbb.matsim.contrib.railsim.qsimengine.deadlocks.SimpleDeadlockAvoidance; import ch.sbb.matsim.contrib.railsim.qsimengine.disposition.SimpleDisposition; import ch.sbb.matsim.contrib.railsim.qsimengine.disposition.TrainDisposition; +import ch.sbb.matsim.contrib.railsim.qsimengine.resources.RailResourceManager; import ch.sbb.matsim.contrib.railsim.qsimengine.router.TrainRouter; import com.google.inject.multibindings.OptionalBinder; import org.matsim.core.mobsim.qsim.AbstractQSimModule; @@ -47,8 +50,9 @@ protected void configureQSim() { bind(TrainRouter.class).asEagerSingleton(); bind(RailResourceManager.class).asEagerSingleton(); - // This Interface might be replaced with other implementations + // These interfaces might be replaced with other implementations bind(TrainDisposition.class).to(SimpleDisposition.class).asEagerSingleton(); + bind(DeadlockAvoidance.class).to(SimpleDeadlockAvoidance.class).asEagerSingleton(); addQSimComponentBinding(COMPONENT_NAME).to(RailsimQSimEngine.class); diff --git a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimTransitDriverAgent.java b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimTransitDriverAgent.java index be2ebcb6ffb..8768ce90029 100644 --- a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimTransitDriverAgent.java +++ b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimTransitDriverAgent.java @@ -19,6 +19,7 @@ package ch.sbb.matsim.contrib.railsim.qsimengine; +import ch.sbb.matsim.contrib.railsim.qsimengine.resources.RailLink; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.matsim.api.core.v01.Id; diff --git a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/TrainInfo.java b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/TrainInfo.java index 9d395d785d0..e48aebaad18 100644 --- a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/TrainInfo.java +++ b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/TrainInfo.java @@ -27,7 +27,7 @@ /** * Non-mutable static information for a single train. */ -record TrainInfo( +public record TrainInfo( Id id, double length, double maxVelocity, @@ -47,7 +47,7 @@ record TrainInfo( ); } - public void checkConsistency() { + void checkConsistency() { if (!Double.isFinite(maxVelocity) || maxVelocity <= 0) throw new IllegalArgumentException("Train of type " + id + " does not have a finite maximumVelocity."); diff --git a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/TrainPosition.java b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/TrainPosition.java new file mode 100644 index 00000000000..2e99d3903bc --- /dev/null +++ b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/TrainPosition.java @@ -0,0 +1,86 @@ +package ch.sbb.matsim.contrib.railsim.qsimengine; + +import ch.sbb.matsim.contrib.railsim.qsimengine.resources.RailLink; +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.network.Link; +import org.matsim.core.mobsim.framework.MobsimDriverAgent; + +import javax.annotation.Nullable; +import java.util.List; + +/** + * Interface allowing to query current train position and information. + */ +public interface TrainPosition { + + /** + * The driver, which can also be used as identifier. + */ + MobsimDriverAgent getDriver(); + + /** + * Get transit driver agent, if this is a pt transit. + */ + @Nullable + RailsimTransitDriverAgent getPt(); + + /** + * The train type. + */ + TrainInfo getTrain(); + + /** + * The link the where the head of the train is on. Can be null if not yet departed. + */ + @Nullable + Id getHeadLink(); + + /** + * The link the where the tail of the train is on. Can be null if not yet departed. + */ + @Nullable + Id getTailLink(); + + /** + * The position of the head of the train on the link, in meters. + */ + double getHeadPosition(); + + /** + * The position of the tail of the train on the link, in meters. + */ + double getTailPosition(); + + /** + * Current route index. + */ + int getRouteIndex(); + + /** + * Total route size. + */ + int getRouteSize(); + + /** + * Get part of the route. + */ + RailLink getRoute(int idx); + + /** + * Get part of the route. + * + * @param from from index, inclusive + * @param to to index, exclusive + */ + List getRoute(int from, int to); + + /** + * Return the route until the next stop based on the current position. + */ + List getRouteUntilNextStop(); + + /** + * Check whether to stop at certain link. + */ + boolean isStop(Id link); +} diff --git a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/TrainState.java b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/TrainState.java index 1c0f66cb969..acd544c74e4 100644 --- a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/TrainState.java +++ b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/TrainState.java @@ -20,6 +20,7 @@ package ch.sbb.matsim.contrib.railsim.qsimengine; import ch.sbb.matsim.contrib.railsim.events.RailsimTrainStateEvent; +import ch.sbb.matsim.contrib.railsim.qsimengine.resources.RailLink; import org.matsim.api.core.v01.Id; import org.matsim.api.core.v01.network.Link; import org.matsim.core.mobsim.framework.MobsimDriverAgent; @@ -31,7 +32,7 @@ /** * Stores the mutable current state of a train. */ -final class TrainState { +final class TrainState implements TrainPosition { /** * Driver of the train. @@ -103,10 +104,20 @@ final class TrainState { double headPosition; /** - * * Distance in meters away from the {@code tailLink}s {@code fromNode}. + * Distance in meters away from the {@code tailLink}s {@code fromNode}. */ double tailPosition; + /** + * Distance in meters, which are approved by dispatcher. + */ + double approvedDist; + + /** + * Speed in m/s, which is approved after reaching {@link #approvedDist}. + */ + double approvedSpeed; + /** * Speed in m/s. */ @@ -140,6 +151,7 @@ public String toString() { ", allowedMaxSpeed=" + allowedMaxSpeed + ", headPosition=" + headPosition + ", tailPosition=" + tailPosition + + ", approvedDist=" + approvedDist + ", speed=" + speed + ", acceleration=" + acceleration + '}'; @@ -150,10 +162,8 @@ boolean isRouteAtEnd() { return routeIdx >= route.size(); } - /** - * Check whether to stop at certain link. - */ - boolean isStop(Id link) { + @Override + public boolean isStop(Id link) { return nextStop != null && nextStop.getLinkId().equals(link); } @@ -164,4 +174,71 @@ RailsimTrainStateEvent asEvent(double time) { speed, acceleration, targetSpeed); } + @Override + public MobsimDriverAgent getDriver() { + return driver; + } + + @Override + @Nullable + public RailsimTransitDriverAgent getPt() { + return pt; + } + + @Override + public TrainInfo getTrain() { + return train; + } + + @Override + public Id getHeadLink() { + return headLink; + } + + @Override + public Id getTailLink() { + return tailLink; + } + + @Override + public double getHeadPosition() { + return headPosition; + } + + @Override + public double getTailPosition() { + return tailPosition; + } + + @Override + public int getRouteIndex() { + return routeIdx; + } + + @Override + public int getRouteSize() { + return route.size(); + } + + @Override + public RailLink getRoute(int idx) { + return route.get(idx); + } + + @Override + public List getRoute(int from, int to) { + return route.subList(from, to); + } + + @Override + public List getRouteUntilNextStop() { + int from = routeIdx; + + for (int i = 0; i < route.size(); i++) { + if (isStop(route.get(i).getLinkId())) { + return route.subList(from, i + 1); + } + } + return route.subList(from, route.size()); + } } diff --git a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/UpdateEvent.java b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/UpdateEvent.java index 52776792c9b..a8a2c246ddd 100644 --- a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/UpdateEvent.java +++ b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/UpdateEvent.java @@ -19,6 +19,8 @@ package ch.sbb.matsim.contrib.railsim.qsimengine; +import ch.sbb.matsim.contrib.railsim.qsimengine.resources.RailLink; + import java.util.Objects; /** diff --git a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/deadlocks/DeadlockAvoidance.java b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/deadlocks/DeadlockAvoidance.java new file mode 100644 index 00000000000..513b55df61a --- /dev/null +++ b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/deadlocks/DeadlockAvoidance.java @@ -0,0 +1,55 @@ +package ch.sbb.matsim.contrib.railsim.qsimengine.deadlocks; + +import ch.sbb.matsim.contrib.railsim.qsimengine.TrainPosition; +import ch.sbb.matsim.contrib.railsim.qsimengine.resources.RailLink; +import ch.sbb.matsim.contrib.railsim.qsimengine.resources.RailResource; +import org.matsim.api.core.v01.Id; +import org.matsim.core.mobsim.framework.MobsimDriverAgent; + +import java.util.List; +import java.util.Map; + +/** + * Interface for deadlock prevention strategies. + * Strategies should aim to prevent deadlocks, but is not required to guarantee it. + */ +public interface DeadlockAvoidance { + + /** + * Let the strategy known about the resources that are available. + */ + default void initResources(Map, RailResource> resources) { + } + + + /** + * Check if reserving this link may produce a deadlock. + * @return true if the link can be reserved, false otherwise. + */ + boolean checkLink(double time, RailLink link, TrainPosition position); + + /** + * Check if performing this re-route may produce a deadlock. + * @param subRoute the original route to be changed + * @param detour new detour route + * @return true if the rerouting can be performed, false otherwise. + */ + boolean checkReroute(double time, RailLink start, RailLink end, List subRoute, List detour, TrainPosition position); + + /** + * Called when a resource was reserved. + */ + void onReserve(double time, RailResource resource, TrainPosition position); + + /** + * Called when a resource was released. + */ + void onRelease(double time, RailResource resource, MobsimDriverAgent driver); + + /** + * Called when a link was released. + */ + default void onReleaseLink(double time, RailLink link, MobsimDriverAgent driver) { + } + +} diff --git a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/deadlocks/NoDeadlockAvoidance.java b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/deadlocks/NoDeadlockAvoidance.java new file mode 100644 index 00000000000..d5fe67ca604 --- /dev/null +++ b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/deadlocks/NoDeadlockAvoidance.java @@ -0,0 +1,36 @@ +package ch.sbb.matsim.contrib.railsim.qsimengine.deadlocks; + +import ch.sbb.matsim.contrib.railsim.qsimengine.TrainPosition; +import ch.sbb.matsim.contrib.railsim.qsimengine.resources.RailLink; +import ch.sbb.matsim.contrib.railsim.qsimengine.resources.RailResource; +import org.matsim.core.mobsim.framework.MobsimDriverAgent; + +import java.util.List; + +/** + * Does not prevent deadlocks and permits every request. + */ +public class NoDeadlockAvoidance implements DeadlockAvoidance { + + + @Override + public void onReserve(double time, RailResource resource, TrainPosition position) { + } + + + @Override + public boolean checkLink(double time, RailLink link, TrainPosition position) { + return true; + } + + @Override + public boolean checkReroute(double time, RailLink start, RailLink end, List subRoute, List detour, TrainPosition position) { + return true; + } + + @Override + public void onRelease(double time, RailResource resource, MobsimDriverAgent driver) { + } + + +} diff --git a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/deadlocks/SimpleDeadlockAvoidance.java b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/deadlocks/SimpleDeadlockAvoidance.java new file mode 100644 index 00000000000..6ad95f76623 --- /dev/null +++ b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/deadlocks/SimpleDeadlockAvoidance.java @@ -0,0 +1,117 @@ +package ch.sbb.matsim.contrib.railsim.qsimengine.deadlocks; + +import ch.sbb.matsim.contrib.railsim.qsimengine.TrainPosition; +import ch.sbb.matsim.contrib.railsim.qsimengine.resources.RailLink; +import ch.sbb.matsim.contrib.railsim.qsimengine.resources.RailResource; +import jakarta.inject.Inject; +import org.matsim.api.core.v01.Id; +import org.matsim.core.mobsim.framework.MobsimDriverAgent; + +import javax.annotation.Nullable; +import java.util.*; + +/** + * A simple deadlock avoidance strategy that backtracks conflicting points in the schedule of trains. + * This implementation does not guarantee deadlock freedom, but can avoid many cases. + */ +public class SimpleDeadlockAvoidance implements DeadlockAvoidance { + + /** + * Stores conflict points of trains. + */ + private final Map conflictPoints = new HashMap<>(); + + @Inject + public SimpleDeadlockAvoidance() { + } + + /** + * This strategy only safeguards sections with single capacity. + */ + private static boolean isConflictPoint(RailResource resource) { + return resource.getTotalCapacity() == 1; + } + + @Override + public void onReserve(double time, RailResource resource, TrainPosition position) { + + boolean resourceFound = false; + + int idx = Math.max(0, position.getRouteIndex() - 1); + for (int i = idx; i < position.getRouteSize(); i++) { + + RailLink link = position.getRoute(i); + RailResource r = link.getResource(); +// // Iterate through route until requested resource is present + + if (r == resource) { + resourceFound = true; + } + + if (!resourceFound) + continue; + + if (isConflictPoint(r)) { + + Reservation reservation = conflictPoints.computeIfAbsent(r, k -> new Reservation()); + + // Reserve non conflict points, otherwise stop + if (reservation.direction != null && reservation.direction != link) + break; + + reservation.direction = link; + reservation.trains.add(position.getDriver()); + + } else + break; + + } + } + + @Override + public boolean checkLink(double time, RailLink link, TrainPosition position) { + + // Passing points can be ignored + if (!isConflictPoint(link.getResource())) + return true; + + Reservation other = conflictPoints.get(link.getResource()); + + // not reserved or reserved by same direction + return other == null || other.direction == null || other.direction == link; + } + + @Override + public boolean checkReroute(double time, RailLink start, RailLink end, List subRoute, List detour, TrainPosition position) { + + // rerouting is always allowed, but reservations needs to be removed + for (RailLink link : subRoute) { + onRelease(time, link.getResource(), position.getDriver()); + } + + return true; + } + + @Override + public void onRelease(double time, RailResource resource, MobsimDriverAgent driver) { + + Reservation reservation = conflictPoints.get(resource); + if (reservation != null) { + reservation.trains.remove(driver); + + // this direction is free again + if (reservation.trains.isEmpty()) + reservation.direction = null; + } + } + + /** + * Holds current direction and drivers holding a reservation. + */ + private static final class Reservation { + + private RailLink direction; + private final Set trains = new LinkedHashSet<>(); + + } +} diff --git a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/disposition/Detour.java b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/disposition/Detour.java new file mode 100644 index 00000000000..728324b49d9 --- /dev/null +++ b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/disposition/Detour.java @@ -0,0 +1,13 @@ +package ch.sbb.matsim.contrib.railsim.qsimengine.disposition; + +import ch.sbb.matsim.contrib.railsim.qsimengine.resources.RailLink; +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.network.Link; + +import java.util.List; + +/** + * Instruction to detour from original route. + */ +public record Detour(int startIdx, int endIdx, Id startLink, Id endLink, List newRoute) { +} diff --git a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/disposition/DispositionResponse.java b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/disposition/DispositionResponse.java new file mode 100644 index 00000000000..3514a2f66b7 --- /dev/null +++ b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/disposition/DispositionResponse.java @@ -0,0 +1,11 @@ +package ch.sbb.matsim.contrib.railsim.qsimengine.disposition; + +/** + * Response from {@link TrainDisposition}. + * @param approvedDist distance cleared for the train + * @param approvedSpeed speed cleared after approved distance, if 0 the train needs to stop. + * @param detour detour to be taken, if any. + */ +public record DispositionResponse(double approvedDist, double approvedSpeed, Detour detour) { + +} diff --git a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/disposition/SimpleDisposition.java b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/disposition/SimpleDisposition.java index 99418a7547b..7bb0642bfad 100644 --- a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/disposition/SimpleDisposition.java +++ b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/disposition/SimpleDisposition.java @@ -19,15 +19,15 @@ package ch.sbb.matsim.contrib.railsim.qsimengine.disposition; -import ch.sbb.matsim.contrib.railsim.qsimengine.RailLink; -import ch.sbb.matsim.contrib.railsim.qsimengine.RailResourceManager; -import ch.sbb.matsim.contrib.railsim.qsimengine.RailsimTransitDriverAgent; +import ch.sbb.matsim.contrib.railsim.qsimengine.resources.RailLink; +import ch.sbb.matsim.contrib.railsim.qsimengine.resources.RailResource; +import ch.sbb.matsim.contrib.railsim.qsimengine.resources.RailResourceManager; +import ch.sbb.matsim.contrib.railsim.qsimengine.RailsimCalc; +import ch.sbb.matsim.contrib.railsim.qsimengine.TrainPosition; import ch.sbb.matsim.contrib.railsim.qsimengine.router.TrainRouter; import jakarta.inject.Inject; import org.matsim.core.mobsim.framework.MobsimDriverAgent; -import javax.annotation.Nullable; -import java.util.ArrayList; import java.util.List; /** @@ -38,6 +38,15 @@ public class SimpleDisposition implements TrainDisposition { private final RailResourceManager resources; private final TrainRouter router; + /** + * Whether re-routing should be tried. + * + * @param upcoming the upcoming links the train tried to block. + */ + static boolean considerReRouting(List upcoming, RailLink currentLink) { + return currentLink.isEntryLink() || upcoming.stream().anyMatch(RailLink::isEntryLink); + } + @Inject public SimpleDisposition(RailResourceManager resources, TrainRouter router) { this.resources = resources; @@ -49,41 +58,132 @@ public void onDeparture(double time, MobsimDriverAgent driver, List ro // Nothing to do. } - @Nullable @Override - public List requestRoute(double time, RailsimTransitDriverAgent driver, List segment, RailLink entry, RailLink exit) { + public DispositionResponse requestNextSegment(double time, TrainPosition position, double dist) { - // Only re-routes if the link segment is occupied - for (RailLink link : segment) { - if (!resources.isBlockedBy(link, driver) && !resources.hasCapacity(link.getLinkId())) - return router.calcRoute(entry, exit); + RailLink currentLink = resources.getLink(position.getHeadLink()); + List segment = RailsimCalc.calcLinksToBlock(position, currentLink, dist); + + // Check for re routing + Detour detour = checkDetour(time, segment, position); + + if (detour != null) { + // train needs to integrate the detour and request a new route + return new DispositionResponse(0, 0, detour); } - return null; - } + double reserveDist = resources.tryBlockLink(time, currentLink, RailResourceManager.ANY_TRACK_NON_BLOCKING, position); - @Override - public List blockRailSegment(double time, MobsimDriverAgent driver, List segment) { + if (reserveDist == RailResource.NO_RESERVATION) + return new DispositionResponse(0, 0, null); + + // current link only partial reserved + if (reserveDist < currentLink.length) { + return new DispositionResponse(reserveDist - position.getHeadPosition(), 0, null); + } - List blocked = new ArrayList<>(); + // remove already used distance + reserveDist -= position.getHeadPosition(); + boolean stop = false; // Iterate all links that need to be blocked for (RailLink link : segment) { - // Check if single link can be reserved - if (resources.tryBlockTrack(time, driver, link)) { - blocked.add(link); - } else + // first link does not need to be blocked again + if (link == currentLink) + continue; + + dist = resources.tryBlockLink(time, link, RailResourceManager.ANY_TRACK_NON_BLOCKING, position); + + if (dist == RailResource.NO_RESERVATION) { + stop = true; + break; + } + + // partial reservation + reserveDist += dist; + + // If the link is not fully reserved then stop + // there might be a better advised speed (speed of train in-front) + if (dist < link.getLength()) { + stop = true; break; + } } - return blocked; + return new DispositionResponse(reserveDist, stop ? 0 : Double.POSITIVE_INFINITY, detour); } + private Detour checkDetour(double time, List segment, TrainPosition position) { + + if (position.getPt() != null && considerReRouting(segment, resources.getLink(position.getHeadLink()))) { + + int start = -1; + int end = -1; + RailLink entry = null; + RailLink exit = null; + + for (int i = Math.max(0, position.getRouteIndex() - 1); i < position.getRouteSize(); i++) { + RailLink l = position.getRoute(i); + + if (l.isEntryLink()) { + entry = l; + start = i; + } else if (start > -1 && resources.isBlockedBy(l, position)) { + // check if any link beyond entry is already blocked + // if that is the case re-route is not possible anymore + break; + } else if (start > -1 && l.isExitLink()) { + exit = l; + end = i; + break; + } + } + + // there might be no exit link if this is the end of the route + // exit will be set to null if re-route is too late + // network could be wrong as well, but hard to verify + if (exit != null) { + + List subRoute = position.getRoute(start + 1, end); + + List newRoute = reroute(time, subRoute, position, entry, exit); + + if (newRoute != null) + return new Detour(start + 1, end, entry.getLinkId(), exit.getLinkId(), newRoute); + } + } + + return null; + } + + private List reroute(double time, List subRoute, TrainPosition position, RailLink entry, RailLink exit) { + + // Only re-routes if the link segment is occupied + for (RailLink link : subRoute) { + if (!resources.isBlockedBy(link, position) && + !resources.hasCapacity(time, link.getLinkId(), RailResourceManager.ANY_TRACK_NON_BLOCKING, position)) { + + List detour = router.calcRoute(position, entry, exit); + + if (subRoute.equals(detour)) + return null; + + if (!resources.checkReroute(time, entry, exit, subRoute, detour, position)) + return null; + + return detour; + } + } + + return null; + } + + @Override public void unblockRailLink(double time, MobsimDriverAgent driver, RailLink link) { // put resource handling into release track - resources.releaseTrack(time, driver, link); + resources.releaseLink(time, link, driver); } } diff --git a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/disposition/TrainDisposition.java b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/disposition/TrainDisposition.java index eb178dc663c..3cd2cdeb11a 100644 --- a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/disposition/TrainDisposition.java +++ b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/disposition/TrainDisposition.java @@ -19,11 +19,10 @@ package ch.sbb.matsim.contrib.railsim.qsimengine.disposition; -import ch.sbb.matsim.contrib.railsim.qsimengine.RailLink; -import ch.sbb.matsim.contrib.railsim.qsimengine.RailsimTransitDriverAgent; +import ch.sbb.matsim.contrib.railsim.qsimengine.resources.RailLink; +import ch.sbb.matsim.contrib.railsim.qsimengine.TrainPosition; import org.matsim.core.mobsim.framework.MobsimDriverAgent; -import javax.annotation.Nullable; import java.util.List; /** @@ -37,27 +36,16 @@ public interface TrainDisposition { void onDeparture(double time, MobsimDriverAgent driver, List route); /** - * Called by the driver when an entry link is within stop distance. - * - * @param segment the original link segment between entry and exit - * @return the route change, or null if nothing should be changed + * Request the next segment to be reserved. + * @param time current time + * @param position position information + * @param dist distance in meter the train is requesting */ - @Nullable - default List requestRoute(double time, RailsimTransitDriverAgent driver, List segment, - RailLink entry, RailLink exit) { - return null; - } + DispositionResponse requestNextSegment(double time, TrainPosition position, double dist); - /** - * Train is reaching the given links and is trying to block them. - * - * @return links of the request that are exclusively blocked for the train. - */ - List blockRailSegment(double time, MobsimDriverAgent driver, List segment); /** * Inform the resource manager that the train has passed a link that can now be unblocked. - * This needs to be called after track states have been updated already. */ void unblockRailLink(double time, MobsimDriverAgent driver, RailLink link); diff --git a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/resources/FixedBlockResource.java b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/resources/FixedBlockResource.java new file mode 100644 index 00000000000..eefec6b7a29 --- /dev/null +++ b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/resources/FixedBlockResource.java @@ -0,0 +1,203 @@ +/* *********************************************************************** * + * project: org.matsim.* + * * + * *********************************************************************** * + * * + * copyright : (C) 2023 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 ch.sbb.matsim.contrib.railsim.qsimengine.resources; + +import ch.sbb.matsim.contrib.railsim.qsimengine.TrainPosition; +import org.matsim.api.core.v01.Id; +import org.matsim.core.mobsim.framework.MobsimDriverAgent; + +import java.util.*; + + +/** + * Fixed block where each track is exclusively reserved by one agent. + */ +final class FixedBlockResource implements RailResourceInternal { + + private final Id id; + + /** + * Links belonging to this resource. + */ + private final List links; + + /** + * Reservations per link and per track. + */ + private final Map tracks; + + /** + * Tracks drivers that have at least one reservation on any link os this resource. + * The link a driver used to enter the resource is stored for each. + */ + private final Map reservations; + + /** + * Maximum number of reservations. + */ + private final int capacity; + + FixedBlockResource(Id id, List links) { + this.id = id; + this.links = links; + this.capacity = links.stream().mapToInt(l -> l.tracks).min().orElseThrow(); + this.reservations = new HashMap<>(capacity); + this.tracks = new HashMap<>(links.size()); + + for (RailLink link : links) { + tracks.put(link, new MobsimDriverAgent[link.tracks]); + } + } + + @Override + public Id getId() { + return id; + } + + @Override + public ResourceType getType() { + return ResourceType.fixedBlock; + } + + @Override + public List getLinks() { + return links; + } + + @Override + public int getTotalCapacity() { + return capacity; + } + + @Override + public ResourceState getState(RailLink link) { + if (reservations.isEmpty()) + return ResourceState.EMPTY; + if (reservations.size() < capacity) + return ResourceState.IN_USE; + + return ResourceState.EXHAUSTED; + } + + @Override + public boolean hasCapacity(double time, RailLink link, int track, TrainPosition position) { + + if (track >= 0) + throw new IllegalArgumentException("Fixed block does not support choosing individual tracks."); + + // there is no need to check the individual tracks here + if (reservations.containsKey(position.getDriver())) { + return true; + } + + // Can return any free track + if (track == RailResourceManager.ANY_TRACK) + return reservations.size() < capacity; + + assert track == RailResourceManager.ANY_TRACK_NON_BLOCKING; + + boolean samePresent = false; + for (RailLink enterLink : reservations.values()) { + if (enterLink == link) { + samePresent = true; + break; + } + } + + // if same direction is already used, one extra track needs to be available + return samePresent ? reservations.size() < capacity - 1 : reservations.size() < capacity; + } + + + @Override + public double getReservedDist(RailLink link, TrainPosition position) { + MobsimDriverAgent[] state = tracks.get(link); + for (MobsimDriverAgent reserved : state) { + if (reserved == position.getDriver()) { + return link.length; + } + } + + return RailResourceInternal.NO_RESERVATION; + } + + @Override + public double reserve(double time, RailLink link, int track, TrainPosition position) { + + if (track >= 0) + throw new IllegalArgumentException("Fixed block does not support choosing individual tracks."); + + assert position.getDriver() != null: "Driver must not be null."; + + // store the link the driver used to enter the resource + if (!reservations.containsKey(position.getDriver())) + reservations.put(position.getDriver(), link); + + if (reservations.size() > capacity) { + throw new IllegalStateException("Too many reservations. Capacity needs to be checked before calling reserve."); + } + + MobsimDriverAgent[] state = tracks.get(link); + for (int i = 0; i < state.length; i++) { + if (state[i] == null) { + state[i] = position.getDriver(); + return link.length; + } + } + + throw new IllegalStateException("No track was free."); + } + + @Override + public boolean release(RailLink link, MobsimDriverAgent driver) { + + MobsimDriverAgent[] state = tracks.get(link); + int track = -1; + for (int i = 0; i < state.length; i++) { + if (state[i] == driver) { + state[i] = null; + track = i; + break; + } + } + + if (track == -1) + throw new AssertionError("Driver " + driver + " has not reserved the track."); + + boolean allFree = true; + for (MobsimDriverAgent[] others : tracks.values()) { + for (MobsimDriverAgent other : others) { + if (other == driver) { + allFree = false; + break; + } + } + } + + // if the driver has no more reservations, remove it from the set + if (allFree) { + reservations.remove(driver); + return true; + } + + return false; + } + +} diff --git a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/resources/MovingBlockResource.java b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/resources/MovingBlockResource.java new file mode 100644 index 00000000000..126c90b1f2f --- /dev/null +++ b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/resources/MovingBlockResource.java @@ -0,0 +1,286 @@ +package ch.sbb.matsim.contrib.railsim.qsimengine.resources; + +import ch.sbb.matsim.contrib.railsim.qsimengine.RailsimCalc; +import ch.sbb.matsim.contrib.railsim.qsimengine.TrainPosition; +import it.unimi.dsi.fastutil.ints.Int2DoubleArrayMap; +import it.unimi.dsi.fastutil.ints.Int2DoubleMap; +import org.matsim.api.core.v01.Id; +import org.matsim.core.mobsim.framework.MobsimDriverAgent; + +import javax.annotation.Nullable; +import java.util.*; + +/** + * A moving block, which allows multiple trains and needs to make sure that they don't collide. + */ +final class MovingBlockResource implements RailResourceInternal { + + private final Id id; + private final List links; + private final Track[] tracks; + private final int capacity; + + /** + * Links that are reserved by trains. + */ + private final Map> moving = new HashMap<>(); + + /** + * Map driver on segment to their incoming link. + */ + private final Map reservations; + + MovingBlockResource(Id id, List links) { + this.id = id; + this.links = links; + + this.capacity = links.stream().mapToInt(l -> l.tracks).min().orElseThrow(); + this.tracks = new Track[capacity]; + + for (int i = 0; i < capacity; i++) { + tracks[i] = new Track(); + } + + this.reservations = new HashMap<>(); + + for (RailLink link : links) { + moving.put(link, new HashSet<>()); + } + } + + @Override + public Id getId() { + return id; + } + + @Override + public ResourceType getType() { + return ResourceType.movingBlock; + } + + @Override + public List getLinks() { + return links; + } + + @Override + public int getTotalCapacity() { + return capacity; + } + + @Override + public ResourceState getState(RailLink link) { + // All links have the same state + int used = 0; + for (Track track : tracks) { + if (track.incoming != null) + used++; + } + + if (used == 0) + return ResourceState.EMPTY; + + return ResourceState.IN_USE; + } + + @Override + public boolean hasCapacity(double time, RailLink link, int track, TrainPosition position) { + + TrainEntry entry = reservations.get(position.getDriver()); + + // No entry yet + if (entry == null && track < 0) { + track = chooseTrack(link, track); + + // No track was available + if (track < 0) + return false; + } else if (entry != null && track < 0) { + track = entry.track; + } else if (entry != null && track != entry.track) { + throw new IllegalStateException("Train is already on a different track."); + } + + // entry should store track + double dist = checkReserve(time, link, track, entry, position); + + return dist > 0; + } + + @Override + public double getReservedDist(RailLink link, TrainPosition position) { + + TrainEntry entry = reservations.get(position.getDriver()); + if (entry == null) + return NO_RESERVATION; + + return entry.reservedDistance.getOrDefault(link.getLinkId().index(), NO_RESERVATION); + } + + @Override + public double reserve(double time, RailLink link, int track, TrainPosition position) { + + moving.get(link).add(position.getDriver()); + + // store trains incoming link + if (!reservations.containsKey(position.getDriver())) { + + if (track < 0) + track = chooseTrack(link, track); + + TrainEntry e = new TrainEntry(track, position, links.size()); + Track t = tracks[track]; + + reservations.put(position.getDriver(), e); + + // Mark this as used by this direction + if (t.incoming == null) + t.incoming = link; + + t.queue.add(e); + } + + TrainEntry self = reservations.get(position.getDriver()); + + double dist = checkReserve(time, link, self.track, self, position); + self.reservedDistance.put(link.getLinkId().index(), dist); + + return dist; + } + + /** + * Chooses a track for trains that don't have one yet. + */ + private int chooseTrack(RailLink link, int mode) { + int same = -1; + int available = -1; + int free = 0; + + assert mode < 0: "Can not give a specific track at this point"; + + for (int i = 0; i < tracks.length; i++) { + if (tracks[i].incoming == link) + same = i; + else if (tracks[i].incoming == null) { + available = i; + free++; + } + } + + // Always try to keep a track in reserve in non blocking mode + int newTrack = mode == RailResourceManager.ANY_TRACK_NON_BLOCKING ? 1 : 0; + + // If there is more than one free track, a new one is opened + if (same != -1) // there is a train in the same direction already + return free > newTrack ? available : same; + + return available; + } + + /** + * Compute the distance that can be reserved without actually reserving it. + */ + private double checkReserve(double time, RailLink link, int track, + @Nullable TrainEntry entry, TrainPosition position) { + + List queue = tracks[track].queue; + + // Approve whole link length for the first train in the queue + int idx = entry != null ? queue.indexOf(entry) : queue.size(); + if (idx == 0) { + return link.length; + } + + // The train in front of this one + TrainEntry inFront = queue.get(idx - 1); + + // tail is on the same link + if (Objects.equals(inFront.position.getTailLink(), link.getLinkId())) { + + // available distance can be at most the link length + return Math.min( + link.length, + inFront.position.getTailPosition() + RailsimCalc.projectedDistance(time, inFront.position) + ); + } + + // if no train is on it, whole link is available + if (moving.get(link).isEmpty()) + return link.length; + + // assume link is fully occupied + // this might not be 100% accurate, when states are not up-to-date + // should be fine for most use-cases + return 0; + } + + @Override + public boolean release(RailLink link, MobsimDriverAgent driver) { + + moving.get(link).remove(driver); + + boolean allFree = true; + for (Set others : moving.values()) { + if (others.contains(driver)) { + allFree = false; + break; + } + } + + assert reservations.containsKey(driver) : "Driver does not has a reservation."; + + // Remove this train from the incoming map + if (allFree) { + + TrainEntry entry = reservations.remove(driver); + + Track track = tracks[entry.track]; + track.queue.removeIf(p -> p.position.getDriver() == driver); + + // This track is completely free again + if (track.queue.isEmpty()) + track.incoming = null; + + return true; + } + + return false; + } + + /** + * Class to keep track of train positions and reservations. + */ + private static final class TrainEntry { + + final int track; + final TrainPosition position; + + /** + * Stores the reserved distance per link. (index of link id) + */ + final Int2DoubleMap reservedDistance; + + public TrainEntry(int track, TrainPosition position, int links) { + this.track = track; + this.position = position; + this.reservedDistance = new Int2DoubleArrayMap(links); + } + } + + /** + * One instance for each available track. + */ + private static final class Track { + + /** + * Link used to enter this track, which indicates the direction. + */ + private RailLink incoming; + + /** + * Train currently on this track. + */ + private final List queue = new ArrayList<>(); + + } +} diff --git a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailLink.java b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/resources/RailLink.java similarity index 56% rename from contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailLink.java rename to contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/resources/RailLink.java index d59ae3801a2..06be3f5556e 100644 --- a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailLink.java +++ b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/resources/RailLink.java @@ -17,7 +17,7 @@ * * * *********************************************************************** */ -package ch.sbb.matsim.contrib.railsim.qsimengine; +package ch.sbb.matsim.contrib.railsim.qsimengine.resources; import ch.sbb.matsim.contrib.railsim.RailsimUtils; import org.matsim.api.core.v01.Id; @@ -25,52 +25,34 @@ import org.matsim.api.core.v01.network.Link; import org.matsim.core.mobsim.framework.MobsimDriverAgent; -import javax.annotation.Nullable; -import java.util.Arrays; import java.util.Objects; /** * Rail links which can has multiple tracks and corresponds to exactly one link. */ public final class RailLink implements HasLinkId { - private final Id id; - - /** - * States per track. - */ - private final TrackState[] state; - private final boolean isEntryLink; private final boolean isExitLink; - /** - * Drivers of each blocked track. - */ - private final MobsimDriverAgent[] blocked; - - final double length; - final double freeSpeed; - final double minimumHeadwayTime; + public final double length; + public final double minimumHeadwayTime; + public final double freeSpeed; + final int tracks; /** - * ID of the resource this link belongs to. + * Resource this link belongs to. */ - @Nullable - final Id resource; + RailResourceInternal resource; public RailLink(Link link) { - id = link.getId(); - state = new TrackState[RailsimUtils.getTrainCapacity(link)]; - Arrays.fill(state, TrackState.FREE); - blocked = new MobsimDriverAgent[state.length]; - length = link.getLength(); - freeSpeed = link.getFreespeed(); - minimumHeadwayTime = RailsimUtils.getMinimumHeadwayTime(link); - String resourceId = RailsimUtils.getResourceId(link); - resource = resourceId != null ? Id.create(resourceId, RailResource.class) : null; - isEntryLink = RailsimUtils.isEntryLink(link); - isExitLink = RailsimUtils.isExitLink(link); + this.id = link.getId(); + this.length = link.getLength(); + this.tracks = RailsimUtils.getTrainCapacity(link); + this.freeSpeed = link.getFreespeed(); + this.minimumHeadwayTime = RailsimUtils.getMinimumHeadwayTime(link); + this.isEntryLink = RailsimUtils.isEntryLink(link); + this.isExitLink = RailsimUtils.isExitLink(link); } @Override @@ -78,16 +60,15 @@ public Id getLinkId() { return id; } - @Nullable - public Id getResourceId() { - return resource; + void setResource(RailResourceInternal resource) { + this.resource = resource; } /** - * Number of tracks on this link. + * Access to the underlying resource. */ - public int getNumberOfTracks() { - return state.length; + public RailResource getResource() { + return resource; } /** @@ -97,57 +78,6 @@ public double getAllowedFreespeed(MobsimDriverAgent driver) { return Math.min(freeSpeed, driver.getVehicle().getVehicle().getType().getMaximumVelocity()); } - /** - * Check if driver has already reserved this link. - */ - public boolean isBlockedBy(MobsimDriverAgent driver) { - for (MobsimDriverAgent reservation : blocked) { - if (reservation == driver) - return true; - } - return false; - } - - /** - * Whether at least one track is free. - */ - boolean hasFreeTrack() { - for (TrackState trackState : state) { - if (trackState == TrackState.FREE) - return true; - } - return false; - } - - /** - * Block a track that was previously reserved. - */ - int blockTrack(MobsimDriverAgent driver) { - for (int i = 0; i < state.length; i++) { - if (state[i] == TrackState.FREE) { - blocked[i] = driver; - state[i] = TrackState.BLOCKED; - return i; - } - } - throw new IllegalStateException("No track was free."); - } - - /** - * Release a non-free track to be free again. - */ - int releaseTrack(MobsimDriverAgent driver) { - for (int i = 0; i < state.length; i++) { - if (blocked[i] == driver) { - state[i] = TrackState.FREE; - blocked[i] = null; - return i; - } - } - throw new AssertionError("Driver " + driver + " has not reserved the track."); - } - - /** * Entry link of a station relevant for re-routing. */ @@ -162,6 +92,13 @@ public boolean isExitLink() { return isExitLink; } + /** + * Length in meter. + */ + public double getLength() { + return length; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/resources/RailResource.java b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/resources/RailResource.java new file mode 100644 index 00000000000..409b8e39e8a --- /dev/null +++ b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/resources/RailResource.java @@ -0,0 +1,37 @@ +package ch.sbb.matsim.contrib.railsim.qsimengine.resources; + +import org.matsim.api.core.v01.Identifiable; + +import java.util.List; + +/** + * A resource representing multiple {@link RailLink}. + */ +public interface RailResource extends Identifiable { + + /** + * Constant returned if no reservation is present. + */ + double NO_RESERVATION = -1; + + /** + * Type of resource. + */ + ResourceType getType(); + + /** + * The links that are represented by this resource. + */ + List getLinks(); + + /** + * The total capacity of this resource, i.e number of tracks. + */ + int getTotalCapacity(); + + /** + * State of a specific link. + */ + ResourceState getState(RailLink link); + +} diff --git a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/resources/RailResourceInternal.java b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/resources/RailResourceInternal.java new file mode 100644 index 00000000000..96a0e63132a --- /dev/null +++ b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/resources/RailResourceInternal.java @@ -0,0 +1,36 @@ +package ch.sbb.matsim.contrib.railsim.qsimengine.resources; + +import ch.sbb.matsim.contrib.railsim.qsimengine.TrainPosition; +import org.matsim.core.mobsim.framework.MobsimDriverAgent; + +/** + * Internal rail resource interface, which allows modifying the state. + * Disposition should only interact with resources via {@link RailResourceManager}. + */ +interface RailResourceInternal extends RailResource { + + /** + * Whether an agent is able to block this resource. + */ + boolean hasCapacity(double time, RailLink link, int track, TrainPosition position); + + /** + * The reserved distance on this link for an agent. Returns 0 if the agent has no reservation. + * @return the reserved distance, -1 if there is no reservation. A reservation with 0 dist could be possible. + */ + double getReservedDist(RailLink link, TrainPosition position); + + /** + * Reserves this resource for the given agent. + * + * @return the reserved distance on this link + */ + double reserve(double time, RailLink link, int track, TrainPosition position); + + /** + * Releases the link on this resource for the given agent. + * @return if the resource was released, i.e. no more links are occupied. + */ + boolean release(RailLink link, MobsimDriverAgent driver); + +} diff --git a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/resources/RailResourceManager.java b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/resources/RailResourceManager.java new file mode 100644 index 00000000000..9a04894bf00 --- /dev/null +++ b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/resources/RailResourceManager.java @@ -0,0 +1,215 @@ +/* *********************************************************************** * + * project: org.matsim.* + * * + * *********************************************************************** * + * * + * copyright : (C) 2023 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 ch.sbb.matsim.contrib.railsim.qsimengine.resources; + +import ch.sbb.matsim.contrib.railsim.RailsimUtils; +import ch.sbb.matsim.contrib.railsim.config.RailsimConfigGroup; +import ch.sbb.matsim.contrib.railsim.events.RailsimLinkStateChangeEvent; +import ch.sbb.matsim.contrib.railsim.qsimengine.TrainPosition; +import ch.sbb.matsim.contrib.railsim.qsimengine.deadlocks.DeadlockAvoidance; +import jakarta.inject.Inject; +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.IdMap; +import org.matsim.api.core.v01.network.Link; +import org.matsim.api.core.v01.network.Network; +import org.matsim.core.api.experimental.events.EventsManager; +import org.matsim.core.config.ConfigUtils; +import org.matsim.core.mobsim.framework.MobsimDriverAgent; +import org.matsim.core.mobsim.qsim.QSim; + +import java.util.*; + +/** + * Class responsible for managing and blocking resources and segments of links. + */ +public final class RailResourceManager { + + /** + * Constant that can be used as track number to indicate that any track is allowed. + */ + public static final int ANY_TRACK = -1; + + /** + * Constant to indicate than any track is allowed as long as the opposing direction is not blocked. + */ + public static final int ANY_TRACK_NON_BLOCKING = -2; + + private final EventsManager eventsManager; + + /** + * Rail links. + */ + private final Map, RailLink> links; + + private final Map, RailResource> resources; + + private final DeadlockAvoidance dla; + + /** + * Retrieve source id of a link. + */ + public static Id getResourceId(Link link) { + String id = RailsimUtils.getResourceId(link); + if (id == null) + return Id.create(link.getId().toString(), RailResource.class); + else + return Id.create(id, RailResource.class); + } + + @Inject + public RailResourceManager(QSim qsim, DeadlockAvoidance dla) { + this(qsim.getEventsManager(), + ConfigUtils.addOrGetModule(qsim.getScenario().getConfig(), RailsimConfigGroup.class), + qsim.getScenario().getNetwork(), + dla); + } + + /** + * Construct resources from network. + */ + public RailResourceManager(EventsManager eventsManager, RailsimConfigGroup config, + Network network, DeadlockAvoidance dla) { + this.eventsManager = eventsManager; + this.dla = dla; + this.links = new IdMap<>(Link.class, network.getLinks().size()); + + // Mapping for resources to be created + Map, List> resourceMapping = new HashMap<>(); + + Set modes = config.getNetworkModes(); + for (Map.Entry, ? extends Link> e : network.getLinks().entrySet()) { + if (e.getValue().getAllowedModes().stream().anyMatch(modes::contains)) { + + RailLink link = new RailLink(e.getValue()); + resourceMapping.computeIfAbsent(getResourceId(e.getValue()), k -> new ArrayList<>()).add(link); + this.links.put(e.getKey(), link); + } + } + + resources = new IdMap<>(RailResource.class, resourceMapping.size()); + for (Map.Entry, List> e : resourceMapping.entrySet()) { + + // use type of the first link + RailLink link = e.getValue().get(0); + ResourceType type = RailsimUtils.getResourceType(network.getLinks().get(link.getLinkId())); + + RailResourceInternal r = switch (type) { + case fixedBlock -> new FixedBlockResource(e.getKey(), e.getValue()); + case movingBlock -> new MovingBlockResource(e.getKey(), e.getValue()); + }; + + e.getValue().forEach(l -> l.setResource(r)); + + resources.put(e.getKey(), r); + } + + dla.initResources(resources); + } + + /** + * All available resources. + */ + public Collection getResources() { + return resources.values(); + } + + /** + * Get single link that belongs to an id. + */ + public RailLink getLink(Id id) { + return links.get(id); + } + + + /** + * Try to block a track and the underlying resource and return the allowed distance. + */ + public double tryBlockLink(double time, RailLink link, int track, TrainPosition position) { + + double reservedDist = link.resource.getReservedDist(link, position); + + // return only fully reserved links + if (reservedDist != RailResourceInternal.NO_RESERVATION && reservedDist == link.length) { + return reservedDist; + } + + if (link.resource.hasCapacity(time, link, track, position)) { + + if (!dla.checkLink(time, link, position)) { + assert reservedDist == RailResourceInternal.NO_RESERVATION : "Link should not be reserved already."; + + return RailResourceInternal.NO_RESERVATION; + } + + double dist = link.resource.reserve(time, link, track, position); + eventsManager.processEvent(new RailsimLinkStateChangeEvent(Math.ceil(time), link.getLinkId(), + position.getDriver().getVehicle().getId(), link.resource.getState(link))); + + dla.onReserve(time, link.resource, position); + + assert dist >= 0 : "Reserved distance must be equal or larger than 0."; + + return dist; + } + + // may be previously reserved dist, or no reservation + return reservedDist; + } + + /** + * Checks whether a link or underlying resource has remaining capacity. + */ + public boolean hasCapacity(double time, Id link, int track, TrainPosition position) { + RailLink l = getLink(link); + return l.resource.hasCapacity(time, l, track, position); + } + + /** + * Whether a driver already reserved a link. + */ + public boolean isBlockedBy(RailLink link, TrainPosition position) { + return link.resource.getReservedDist(link, position) > RailResourceInternal.NO_RESERVATION; + } + + /** + * Release a non-free track to be free again. + */ + public void releaseLink(double time, RailLink link, MobsimDriverAgent driver) { + + boolean release = link.resource.release(link, driver); + + dla.onReleaseLink(time, link, driver); + + if (release) { + dla.onRelease(time, link.resource, driver); + } + + eventsManager.processEvent(new RailsimLinkStateChangeEvent(Math.ceil(time), link.getLinkId(), driver.getVehicle().getId(), + link.resource.getState(link))); + } + + /** + * Check if a re-route is allowed. + * @see DeadlockAvoidance#checkReroute(double, RailLink, RailLink, List, List, TrainPosition) + */ + public boolean checkReroute(double time, RailLink start, RailLink end, List subRoute, List detour, TrainPosition position) { + return dla.checkReroute(time, start, end, subRoute, detour, position); + } +} diff --git a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/TrackState.java b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/resources/ResourceState.java similarity index 87% rename from contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/TrackState.java rename to contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/resources/ResourceState.java index 976d2d2e9ad..cff904dffbf 100644 --- a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/TrackState.java +++ b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/resources/ResourceState.java @@ -17,16 +17,15 @@ * * * *********************************************************************** */ -package ch.sbb.matsim.contrib.railsim.qsimengine; +package ch.sbb.matsim.contrib.railsim.qsimengine.resources; /** - * Current state of a track. + * Current state resource. */ -public enum TrackState { - FREE, +public enum ResourceState { + EMPTY, - /** - * Blocked tracks that are exclusively available for trains. - */ - BLOCKED + IN_USE, + + EXHAUSTED, } diff --git a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/resources/ResourceType.java b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/resources/ResourceType.java new file mode 100644 index 00000000000..409b0514551 --- /dev/null +++ b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/resources/ResourceType.java @@ -0,0 +1,15 @@ +package ch.sbb.matsim.contrib.railsim.qsimengine.resources; + +/** + * Describes the type of resource. + */ +public enum ResourceType { + + fixedBlock, + + /** + * A block that allows driving similar to ETCS Level 2 without signals. + */ + movingBlock + +} diff --git a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/router/TrainRouter.java b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/router/TrainRouter.java index bb25b2ac3b2..889d80d84fb 100644 --- a/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/router/TrainRouter.java +++ b/contribs/railsim/src/main/java/ch/sbb/matsim/contrib/railsim/qsimengine/router/TrainRouter.java @@ -19,14 +19,16 @@ package ch.sbb.matsim.contrib.railsim.qsimengine.router; -import ch.sbb.matsim.contrib.railsim.qsimengine.RailLink; -import ch.sbb.matsim.contrib.railsim.qsimengine.RailResourceManager; +import ch.sbb.matsim.contrib.railsim.qsimengine.TrainPosition; +import ch.sbb.matsim.contrib.railsim.qsimengine.resources.RailLink; +import ch.sbb.matsim.contrib.railsim.qsimengine.resources.RailResourceManager; import jakarta.inject.Inject; import org.matsim.api.core.v01.network.Link; import org.matsim.api.core.v01.network.Network; import org.matsim.api.core.v01.network.Node; import org.matsim.api.core.v01.population.Person; import org.matsim.core.mobsim.qsim.QSim; +import org.matsim.core.router.DijkstraFactory; import org.matsim.core.router.speedy.SpeedyALTFactory; import org.matsim.core.router.util.LeastCostPathCalculator; import org.matsim.core.router.util.TravelDisutility; @@ -44,6 +46,8 @@ public final class TrainRouter { private final RailResourceManager resources; private final LeastCostPathCalculator lpc; + private final DisUtility disutility = new DisUtility(); + @Inject public TrainRouter(QSim qsim, RailResourceManager resources) { this(qsim.getScenario().getNetwork(), resources); @@ -54,26 +58,36 @@ public TrainRouter(Network network, RailResourceManager resources) { this.resources = resources; // uses the full network, which should not be slower than filtered network as long as dijkstra is used - this.lpc = new SpeedyALTFactory().createPathCalculator(network, new DisUtility(), new FreeSpeedTravelTime()); + this.lpc = new DijkstraFactory().createPathCalculator(network, disutility, new FreeSpeedTravelTime()); } /** - * Calculate the shortest path between two links. + * Calculate the shortest path between two links. This method is not thread-safe, because of mutable state in the disutility. */ - public List calcRoute(RailLink from, RailLink to) { + public List calcRoute(TrainPosition position, RailLink from, RailLink to) { Node fromNode = network.getLinks().get(from.getLinkId()).getToNode(); Node toNode = network.getLinks().get(to.getLinkId()).getFromNode(); + disutility.setPosition(position); + LeastCostPathCalculator.Path path = lpc.calcLeastCostPath(fromNode, toNode, 0, null, null); return path.links.stream().map(l -> resources.getLink(l.getId())).toList(); } private final class DisUtility implements TravelDisutility { + + private TrainPosition position; + + public void setPosition(TrainPosition position) { + this.position = position; + } + @Override public double getLinkTravelDisutility(Link link, double time, Person person, Vehicle vehicle) { - return resources.hasCapacity(link.getId()) ? 0 : 1; + // only works with fixed block + return resources.hasCapacity(time, link.getId(), RailResourceManager.ANY_TRACK, position) ? 0 : 1; } @Override diff --git a/contribs/railsim/src/test/java/ch/sbb/matsim/contrib/railsim/qsimengine/EventsAssert.java b/contribs/railsim/src/test/java/ch/sbb/matsim/contrib/railsim/qsimengine/EventsAssert.java index 726b229ba80..a6fb8b7be16 100644 --- a/contribs/railsim/src/test/java/ch/sbb/matsim/contrib/railsim/qsimengine/EventsAssert.java +++ b/contribs/railsim/src/test/java/ch/sbb/matsim/contrib/railsim/qsimengine/EventsAssert.java @@ -45,6 +45,19 @@ public EventsAssert hasTrainState(String veh, double time, double headPosition, ); } + public EventsAssert hasTrainState(String veh, double time, String headLink, double speed) { + return haveAtLeast(1, + new Condition<>(event -> + (event instanceof RailsimTrainStateEvent ev) + && ev.getVehicleId().toString().equals(veh) + && FuzzyUtils.equals(ev.getTime(), time) + && ev.getHeadLink().toString().equals(headLink) + && FuzzyUtils.equals(ev.getSpeed(), speed), + String.format("event with veh %s time %.0f link: %s speed: %.2f", veh, time, headLink, speed)) + ); + } + + @Override protected EventAssert toAssert(Event value, String description) { return null; diff --git a/contribs/railsim/src/test/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimCalcTest.java b/contribs/railsim/src/test/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimCalcTest.java index bf7b63411e9..32e77a080c2 100644 --- a/contribs/railsim/src/test/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimCalcTest.java +++ b/contribs/railsim/src/test/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimCalcTest.java @@ -147,4 +147,24 @@ void testCalcTargetSpeedForStop() { .isCloseTo(1000, Offset.offset(0.0001)); } + + @Test + public void testCalcRequiredTime() { + + TrainState state = new TrainState(null, null, 0, null, null); + + state.speed = 0; + state.acceleration = 0; + + assertThat(RailsimCalc.calcRequiredTime(state, 1000)) + .isEqualTo(Double.POSITIVE_INFINITY); + + state.speed = 10; + state.acceleration = -1; + + // Comes to stop after 10s + assertThat(RailsimCalc.calcRequiredTime(state, 10000)) + .isEqualTo(10); + + } } diff --git a/contribs/railsim/src/test/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimDeadlockTest.java b/contribs/railsim/src/test/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimDeadlockTest.java new file mode 100644 index 00000000000..ddd998ee3f1 --- /dev/null +++ b/contribs/railsim/src/test/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimDeadlockTest.java @@ -0,0 +1,229 @@ +package ch.sbb.matsim.contrib.railsim.qsimengine; + +import ch.sbb.matsim.contrib.railsim.RailsimUtils; +import ch.sbb.matsim.contrib.railsim.config.RailsimConfigGroup; +import ch.sbb.matsim.contrib.railsim.qsimengine.deadlocks.DeadlockAvoidance; +import ch.sbb.matsim.contrib.railsim.qsimengine.deadlocks.NoDeadlockAvoidance; +import ch.sbb.matsim.contrib.railsim.qsimengine.deadlocks.SimpleDeadlockAvoidance; +import ch.sbb.matsim.contrib.railsim.qsimengine.disposition.SimpleDisposition; +import ch.sbb.matsim.contrib.railsim.qsimengine.resources.RailResourceManager; +import ch.sbb.matsim.contrib.railsim.qsimengine.resources.ResourceType; +import ch.sbb.matsim.contrib.railsim.qsimengine.router.TrainRouter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.matsim.api.core.v01.network.Link; +import org.matsim.api.core.v01.network.Network; +import org.matsim.core.api.experimental.events.EventsManager; +import org.matsim.core.events.EventsUtils; +import org.matsim.core.network.NetworkUtils; +import org.matsim.testcases.MatsimTestUtils; + +import javax.annotation.Nullable; +import java.io.File; +import java.util.Set; +import java.util.function.Consumer; + +public class RailsimDeadlockTest { + + @RegisterExtension + public MatsimTestUtils utils = new MatsimTestUtils(); + + private EventsManager eventsManager; + private RailsimTestUtils.EventCollector collector; + + @BeforeEach + public void setUp() { + eventsManager = EventsUtils.createEventsManager(); + collector = new RailsimTestUtils.EventCollector(); + + eventsManager.addHandler(collector); + eventsManager.initProcessing(); + } + + private RailsimTestUtils.Holder getTestEngine(String network, DeadlockAvoidance dla, @Nullable Consumer f) { + Network net = NetworkUtils.readNetwork(new File(utils.getPackageInputDirectory(), network).toString()); + RailsimConfigGroup config = new RailsimConfigGroup(); + + collector.clear(); + + if (f != null) { + for (Link link : net.getLinks().values()) { + f.accept(link); + } + } + + RailResourceManager res = new RailResourceManager(eventsManager, config, net, dla); + TrainRouter router = new TrainRouter(net, res); + + return new RailsimTestUtils.Holder(new RailsimEngine(eventsManager, config, res, new SimpleDisposition(res, router)), net); + } + + @Test + public void deadlock() { + + RailsimTestUtils.Holder test = getTestEngine("networkDeadlocks.xml", new NoDeadlockAvoidance(), null); + RailsimTestUtils.createDeparture(test, TestVehicle.Regio, "regio", 0, "AB", "EF"); + RailsimTestUtils.createDeparture(test, TestVehicle.Regio, "regio2", 0, "HG", "DC"); + + test.doSimStepUntil(250); +// test.debugFiles(collector, "deadLock"); + + RailsimTestUtils.assertThat(collector) + .hasTrainState("regio", 240, "y1y", 0) + .hasTrainState("regio2", 240, "zy", 0); + + } + + @Test + public void deadLockAvoidancePoint() { + + Set increased = Set.of("y1y", "yy1"); + + // This avoidance point is too small for these trains and will lead to a deadlock + // this test is also an example where the simple deadlock avoidance fails currently + + RailsimTestUtils.Holder test = getTestEngine("networkDeadlocks.xml", new SimpleDeadlockAvoidance(), l -> { + String id = l.getId().toString(); + if (increased.contains(id)) { + RailsimUtils.setTrainCapacity(l, 2); + } + }); + RailsimTestUtils.createDeparture(test, TestVehicle.Regio, "regio", 0, "AB", "EF"); + RailsimTestUtils.createDeparture(test, TestVehicle.Regio, "regio2", 0, "HG", "CD"); + + test.doSimStepUntil(250); +// test.debugFiles(collector, "deadLockAvoidancePoint"); + + RailsimTestUtils.assertThat(collector) + .hasTrainState("regio", 240, "y1y", 0) + .hasTrainState("regio2", 240, "yy1", 0); + + } + + @Test + public void avoidancePoint() { + + // There is an avoidance point which is also large enough for the train + Set increased = Set.of("y1y", "yy1"); + + RailsimTestUtils.Holder test = getTestEngine("networkDeadlocks.xml", new SimpleDeadlockAvoidance(), l -> { + String id = l.getId().toString(); + if (increased.contains(id)) { + RailsimUtils.setTrainCapacity(l, 2); + l.setLength(500); + } + }); + RailsimTestUtils.createDeparture(test, TestVehicle.Regio, "regio", 0, "AB", "EF"); + RailsimTestUtils.createDeparture(test, TestVehicle.Regio, "regio2", 0, "HG", "CD"); + + test.doSimStepUntil(800); + test.debugFiles(collector, "avoidancePoint"); + + RailsimTestUtils.assertThat(collector) + .hasTrainState("regio", 420, "EF", 0) + .hasTrainState("regio2", 410, "CD", 0); + + } + + @Test + public void tooSmall() { + + Set increased = Set.of("xB", "Bx", "yx", "xy", "AB", "BA"); + + // Increase some capacity, but one of the segment is too small for multiple trains + RailsimTestUtils.Holder test = getTestEngine("networkDeadlocks.xml", new SimpleDeadlockAvoidance(), l -> { + String id = l.getId().toString(); + if (increased.contains(id)) + RailsimUtils.setTrainCapacity(l, 2); + + l.setFreespeed(5); + }); + + RailsimTestUtils.createDeparture(test, TestVehicle.Regio, "regio1a", 0, "AB", "EF"); + RailsimTestUtils.createDeparture(test, TestVehicle.Regio, "regio1b", 0, "AB", "EF"); + + RailsimTestUtils.createDeparture(test, TestVehicle.Regio, "regio2a", 0, "HG", "CD"); + RailsimTestUtils.createDeparture(test, TestVehicle.Regio, "regio2b", 0, "HG", "CD"); + + test.doSimStepUntil(1500); +// test.debugFiles(collector, "tooSmall"); + + + RailsimTestUtils.assertThat(collector) + .hasTrainState("regio1a", 1110, "EF", 0) + .hasTrainState("regio1b", 1360, "EF", 0) + .hasTrainState("regio2a", 670, "CD", 0) + .hasTrainState("regio2b", 980, "CD", 0); + + } + + @Test + public void oneWay() { + + RailsimTestUtils.Holder test = getTestEngine("networkDeadlocks.xml", new SimpleDeadlockAvoidance(), null); + RailsimTestUtils.createDeparture(test, TestVehicle.Regio, "regio", 0, "AB", "EF"); + RailsimTestUtils.createDeparture(test, TestVehicle.Regio, "regio2", 0, "HG", "CD"); + + test.doSimStepUntil(800); +// test.debugFiles(collector, "oneWay"); + + RailsimTestUtils.assertThat(collector) + .hasTrainState("regio", 350, "EF", 0) + .hasTrainState("regio2", 520, "CD", 0); + + } + + @Test + public void twoWay() { + + RailsimTestUtils.Holder test = getTestEngine("networkDeadlocks.xml", new SimpleDeadlockAvoidance(), l -> RailsimUtils.setTrainCapacity(l, 2)); + RailsimTestUtils.createDeparture(test, TestVehicle.Regio, "regio1a", 0, "AB", "EF"); + RailsimTestUtils.createDeparture(test, TestVehicle.Regio, "regio1b", 0, "AB", "EF"); + + RailsimTestUtils.createDeparture(test, TestVehicle.Regio, "regio2a", 0, "HG", "CD"); + RailsimTestUtils.createDeparture(test, TestVehicle.Regio, "regio2b", 0, "HG", "CD"); + + test.doSimStepUntil(800); +// test.debugFiles(collector, "twoWay"); + + RailsimTestUtils.assertThat(collector) + .hasTrainState("regio1a", 350, "EF", 0) + .hasTrainState("regio1b", 510, "EF", 0) + .hasTrainState("regio2a", 350, "CD", 0) + .hasTrainState("regio2b", 510, "CD", 0); + + } + + @Test + public void movingBlock() { + + // Keep start and end as fixed block + Set fixed = Set.of("AB", "BA", "CD", "DC", "EF", "FE", "HG", "GH"); + + RailsimTestUtils.Holder test = getTestEngine("networkDeadlocks.xml", new SimpleDeadlockAvoidance(), l -> { + String id = l.getId().toString(); + if (!fixed.contains(id)) { + RailsimUtils.setResourceType(l, ResourceType.movingBlock); + } + + l.setFreespeed(5); + }); + + RailsimTestUtils.createDeparture(test, TestVehicle.Regio, "regio1a", 0, "AB", "EF"); + RailsimTestUtils.createDeparture(test, TestVehicle.Regio, "regio1b", 0, "AB", "EF"); + + RailsimTestUtils.createDeparture(test, TestVehicle.Regio, "regio2a", 0, "HG", "CD"); + RailsimTestUtils.createDeparture(test, TestVehicle.Regio, "regio2b", 0, "HG", "CD"); + + test.doSimStepUntil(1500); +// test.debugFiles(collector, "movingBlock"); + + RailsimTestUtils.assertThat(collector) + .hasTrainState("regio1a", 670, "EF", 0) + .hasTrainState("regio1b", 790, "EF", 0) + .hasTrainState("regio2a", 1120, "CD", 0) + .hasTrainState("regio2b", 1243, "CD", 0); + + } +} diff --git a/contribs/railsim/src/test/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimEngineMovingBlockTest.java b/contribs/railsim/src/test/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimEngineMovingBlockTest.java new file mode 100644 index 00000000000..ae31cac7539 --- /dev/null +++ b/contribs/railsim/src/test/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimEngineMovingBlockTest.java @@ -0,0 +1,187 @@ +package ch.sbb.matsim.contrib.railsim.qsimengine; + +import ch.sbb.matsim.contrib.railsim.RailsimUtils; +import ch.sbb.matsim.contrib.railsim.config.RailsimConfigGroup; +import ch.sbb.matsim.contrib.railsim.qsimengine.deadlocks.NoDeadlockAvoidance; +import ch.sbb.matsim.contrib.railsim.qsimengine.disposition.SimpleDisposition; +import ch.sbb.matsim.contrib.railsim.qsimengine.resources.RailResourceManager; +import ch.sbb.matsim.contrib.railsim.qsimengine.router.TrainRouter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.matsim.api.core.v01.network.Link; +import org.matsim.api.core.v01.network.Network; +import org.matsim.core.api.experimental.events.EventsManager; +import org.matsim.core.events.EventsUtils; +import org.matsim.core.network.NetworkUtils; +import org.matsim.testcases.MatsimTestUtils; + +import javax.annotation.Nullable; +import java.io.File; +import java.util.function.Consumer; + +/** + * Tests for moving block logic. + */ +public class RailsimEngineMovingBlockTest { + + @RegisterExtension + public MatsimTestUtils utils = new MatsimTestUtils(); + + private EventsManager eventsManager; + private RailsimTestUtils.EventCollector collector; + + @BeforeEach + public void setUp() { + eventsManager = EventsUtils.createEventsManager(); + collector = new RailsimTestUtils.EventCollector(); + + eventsManager.addHandler(collector); + eventsManager.initProcessing(); + } + + private RailsimTestUtils.Holder getTestEngine(String network, @Nullable Consumer f) { + Network net = NetworkUtils.readNetwork(new File(utils.getPackageInputDirectory(), network).toString()); + RailsimConfigGroup config = new RailsimConfigGroup(); + + collector.clear(); + + if (f != null) { + for (Link link : net.getLinks().values()) { + f.accept(link); + } + } + RailResourceManager res = new RailResourceManager(eventsManager, config, net, new NoDeadlockAvoidance()); + TrainRouter router = new TrainRouter(net, res); + + return new RailsimTestUtils.Holder(new RailsimEngine(eventsManager, config, res, new SimpleDisposition(res, router)), net); + } + + private RailsimTestUtils.Holder getTestEngine(String network) { + return getTestEngine(network, null); + } + + @Test + public void multipleTrains() { + + RailsimTestUtils.Holder test = getTestEngine("networkMovingBlocks.xml"); + RailsimTestUtils.createDeparture(test, TestVehicle.Regio, "regio", 0, "l1-2", "l6-7"); + RailsimTestUtils.createDeparture(test, TestVehicle.Cargo, "cargo", 60, "l1-2", "l6-7"); + RailsimTestUtils.createDeparture(test, TestVehicle.Sprinter, "sprinter", 120, "l1-2", "l6-7"); + + test.doSimStepUntil(5_000); + +// test.debugFiles(collector, "movingBlock"); + + RailsimTestUtils.assertThat(collector) + .hasTrainState("regio", 2370, 200, 0) + .hasTrainState("cargo", 3268, 200, 0) + .hasTrainState("sprinter", 3345, 200, 0); + + test = getTestEngine("networkMovingBlocks.xml"); + RailsimTestUtils.createDeparture(test, TestVehicle.Regio, "regio", 0, "l1-2", "l6-7"); + RailsimTestUtils.createDeparture(test, TestVehicle.Cargo, "cargo", 60, "l1-2", "l6-7"); + RailsimTestUtils.createDeparture(test, TestVehicle.Sprinter, "sprinter", 120, "l1-2", "l6-7"); + + test.doStateUpdatesUntil(5_000, 1); + + RailsimTestUtils.assertThat(collector) + .hasTrainState("regio", 2370, 200, 0) + .hasTrainState("cargo", 3268, 200, 0) + .hasTrainState("sprinter", 3345, 200, 0); + + } + + @Test + public void opposite() { + + RailsimTestUtils.Holder test = getTestEngine("networkMovingBlocks.xml"); + RailsimTestUtils.createDeparture(test, TestVehicle.Sprinter, "sprinter", 0, "l1-2", "l6-7"); + RailsimTestUtils.createDeparture(test, TestVehicle.Sprinter, "sprinter2", 400, "l6-5", "l2-1"); + + test.doSimStepUntil(2_000); +// test.debugFiles(collector, "opposite"); + + RailsimTestUtils.assertThat(collector) + .hasTrainState("sprinter2", 1368, 200, 0) + .hasTrainState("sprinter", 1559, 200, 0); + + test = getTestEngine("networkMovingBlocks.xml"); + RailsimTestUtils.createDeparture(test, TestVehicle.Sprinter, "sprinter", 0, "l1-2", "l6-7"); + RailsimTestUtils.createDeparture(test, TestVehicle.Sprinter, "sprinter2", 400, "l6-5", "l2-1"); + + test.doStateUpdatesUntil(2_000, 5); +// test.debugFiles(collector, "opposite_detailed"); + + RailsimTestUtils.assertThat(collector) + .hasTrainState("sprinter2", 1368, 200, 0) + .hasTrainState("sprinter", 1559, 200, 0); + + } + + @Test + public void multiTrack() { + + // This test increased capacity + RailsimTestUtils.Holder test = getTestEngine("networkMovingBlocks.xml", l -> RailsimUtils.setTrainCapacity(l, 3)); + + RailsimTestUtils.createDeparture(test, TestVehicle.Regio, "regio", 0, "l1-2", "l6-7"); + RailsimTestUtils.createDeparture(test, TestVehicle.Cargo, "cargo", 60, "l1-2", "l6-7"); + RailsimTestUtils.createDeparture(test, TestVehicle.Sprinter, "sprinter", 120, "l1-2", "l6-7"); + + test.doSimStepUntil(5_000); +// test.debugFiles(collector, "multiTrack"); + + RailsimTestUtils.assertThat(collector) + .hasTrainState("regio", 2370, 200, 0) + .hasTrainState("cargo", 3268, 200, 0) + .hasTrainState("sprinter", 1984, 200, 0); + + test = getTestEngine("networkMovingBlocks.xml", l -> RailsimUtils.setTrainCapacity(l, 3)); + + RailsimTestUtils.createDeparture(test, TestVehicle.Regio, "regio", 0, "l1-2", "l6-7"); + RailsimTestUtils.createDeparture(test, TestVehicle.Cargo, "cargo", 60, "l1-2", "l6-7"); + RailsimTestUtils.createDeparture(test, TestVehicle.Sprinter, "sprinter", 120, "l1-2", "l6-7"); + + test.doStateUpdatesUntil(5_000, 1); + + RailsimTestUtils.assertThat(collector) + .hasTrainState("regio", 2370, 200, 0) + .hasTrainState("cargo", 3268, 200, 0) + .hasTrainState("sprinter", 1984, 200, 0); + + } + + + @Test + public void mixed() { + + RailsimTestUtils.Holder test = getTestEngine("networkMixedTypes.xml"); + RailsimTestUtils.createDeparture(test, TestVehicle.Regio, "regio", 0, "1-2", "20-21"); + RailsimTestUtils.createDeparture(test, TestVehicle.Cargo, "cargo", 0, "1-2", "20-21"); + RailsimTestUtils.createDeparture(test, TestVehicle.Sprinter, "sprinter", 0, "1-2", "20-21"); + + test.doSimStepUntil(2_000); + test.debugFiles(collector, "mixed"); + + RailsimTestUtils.assertThat(collector) + .hasTrainState("regio", 1418, 1000, 0) + .hasTrainState("cargo", 1241, 1000, 0) + .hasTrainState("sprinter", 1324, 1000, 0); + + test = getTestEngine("networkMixedTypes.xml"); + RailsimTestUtils.createDeparture(test, TestVehicle.Regio, "regio", 0, "1-2", "20-21"); + RailsimTestUtils.createDeparture(test, TestVehicle.Cargo, "cargo", 0, "1-2", "20-21"); + RailsimTestUtils.createDeparture(test, TestVehicle.Sprinter, "sprinter", 0, "1-2", "20-21"); + + test.doStateUpdatesUntil(2_000, 1); + + RailsimTestUtils.assertThat(collector) + .hasTrainState("regio", 1418, 1000, 0) + .hasTrainState("cargo", 1241, 1000, 0) + .hasTrainState("sprinter", 1324, 1000, 0); + + } + + +} diff --git a/contribs/railsim/src/test/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimEngineTest.java b/contribs/railsim/src/test/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimEngineTest.java index 8b5aee8695e..6d0d67ad81b 100644 --- a/contribs/railsim/src/test/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimEngineTest.java +++ b/contribs/railsim/src/test/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimEngineTest.java @@ -21,7 +21,9 @@ import ch.sbb.matsim.contrib.railsim.RailsimUtils; import ch.sbb.matsim.contrib.railsim.config.RailsimConfigGroup; +import ch.sbb.matsim.contrib.railsim.qsimengine.deadlocks.NoDeadlockAvoidance; import ch.sbb.matsim.contrib.railsim.qsimengine.disposition.SimpleDisposition; +import ch.sbb.matsim.contrib.railsim.qsimengine.resources.RailResourceManager; import ch.sbb.matsim.contrib.railsim.qsimengine.router.TrainRouter; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -65,7 +67,7 @@ private RailsimTestUtils.Holder getTestEngine(String network, @Nullable Consumer f.accept(link); } } - RailResourceManager res = new RailResourceManager(eventsManager, config, net); + RailResourceManager res = new RailResourceManager(eventsManager, config, net, new NoDeadlockAvoidance()); TrainRouter router = new TrainRouter(net, res); return new RailsimTestUtils.Holder(new RailsimEngine(eventsManager, config, res, new SimpleDisposition(res, router)), net); diff --git a/contribs/railsim/src/test/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimTestUtils.java b/contribs/railsim/src/test/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimTestUtils.java index 63f5249cef6..fa2e7744e69 100644 --- a/contribs/railsim/src/test/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimTestUtils.java +++ b/contribs/railsim/src/test/java/ch/sbb/matsim/contrib/railsim/qsimengine/RailsimTestUtils.java @@ -19,9 +19,11 @@ package ch.sbb.matsim.contrib.railsim.qsimengine; +import ch.sbb.matsim.contrib.railsim.RailsimUtils; import ch.sbb.matsim.contrib.railsim.analysis.RailsimCsvWriter; import ch.sbb.matsim.contrib.railsim.events.RailsimLinkStateChangeEvent; import ch.sbb.matsim.contrib.railsim.events.RailsimTrainStateEvent; +import ch.sbb.matsim.contrib.railsim.qsimengine.resources.RailLink; import org.matsim.api.core.v01.Id; import org.matsim.api.core.v01.events.Event; import org.matsim.api.core.v01.network.Link; @@ -36,6 +38,8 @@ import org.matsim.core.router.costcalculators.OnlyTimeDependentTravelDisutility; import org.matsim.core.router.util.LeastCostPathCalculator; import org.matsim.core.trafficmonitoring.FreeSpeedTravelTime; +import org.matsim.utils.objectattributes.attributable.Attributes; +import org.matsim.utils.objectattributes.attributable.AttributesImpl; import org.matsim.vehicles.MatsimVehicleReader; import org.matsim.vehicles.Vehicle; import org.matsim.vehicles.VehicleType; @@ -97,10 +101,27 @@ public static void createDeparture(Holder test, TestVehicle type, String veh, do Mockito.when(mobVeh.getVehicle()).thenReturn(vehicle); Mockito.when(mobVeh.getId()).thenReturn(vehicleId); Mockito.when(driver.getVehicle()).thenReturn(mobVeh); + Mockito.when(driver.getId()).thenReturn(Id.createPersonId("driver_" + veh)); test.engine.handleDeparture(time, driver, route.getStartLinkId(), route); } + /** + * Create a RailLink for testing. + */ + public static RailLink createLink(double length, int trainCapacity) { + + Link link = Mockito.mock(Link.class, Answers.RETURNS_MOCKS); + + AttributesImpl attr = new AttributesImpl(); + Mockito.when(link.getAttributes()).thenReturn(attr); + Mockito.when(link.getLength()).thenReturn(length); + + RailsimUtils.setTrainCapacity(link, trainCapacity); + + return new RailLink(link); + } + /** * Collect events during testing */ diff --git a/contribs/railsim/src/test/java/ch/sbb/matsim/contrib/railsim/qsimengine/resources/ReserveResourceTest.java b/contribs/railsim/src/test/java/ch/sbb/matsim/contrib/railsim/qsimengine/resources/ReserveResourceTest.java new file mode 100644 index 00000000000..e8af037225a --- /dev/null +++ b/contribs/railsim/src/test/java/ch/sbb/matsim/contrib/railsim/qsimengine/resources/ReserveResourceTest.java @@ -0,0 +1,81 @@ +package ch.sbb.matsim.contrib.railsim.qsimengine.resources; + +import ch.sbb.matsim.contrib.railsim.qsimengine.RailsimTestUtils; +import ch.sbb.matsim.contrib.railsim.qsimengine.TrainPosition; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.matsim.api.core.v01.Id; +import org.mockito.Mockito; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ReserveResourceTest { + + private TrainPosition d1 = Mockito.mock(TrainPosition.class, Mockito.RETURNS_DEEP_STUBS); + private TrainPosition d2 = Mockito.mock(TrainPosition.class, Mockito.RETURNS_DEEP_STUBS); + private TrainPosition d3 = Mockito.mock(TrainPosition.class, Mockito.RETURNS_DEEP_STUBS); + + static List params() { + RailLink l1 = RailsimTestUtils.createLink(10, 2); + RailLink l2 = RailsimTestUtils.createLink(10, 2); + return List.of( + Arguments.of(l1, l2, new FixedBlockResource(Id.create("r", RailResource.class), List.of(l1, l2))), + Arguments.of(l1, l2, new MovingBlockResource(Id.create("r", RailResource.class), List.of(l1, l2))) + ); + } + + @ParameterizedTest + @MethodSource("params") + public void anyTrack(RailLink l1, RailLink l2, RailResourceInternal r) { + + assertThat(r.hasCapacity(0, l1, RailResourceManager.ANY_TRACK, d1)) + .isTrue(); + + double reserved = r.reserve(0, l1, RailResourceManager.ANY_TRACK, d1); + assertThat(reserved).isEqualTo(10); + + reserved = r.reserve(0, l1, RailResourceManager.ANY_TRACK, d2); + assertThat(reserved).isEqualTo(10); + + assertThat(r.hasCapacity(0, l1, RailResourceManager.ANY_TRACK, d3)) + .isFalse(); + + r.release(l1, d1.getDriver()); + + assertThat(r.hasCapacity(0, l1, RailResourceManager.ANY_TRACK, d3)) + .isTrue(); + + } + + @ParameterizedTest + @MethodSource("params") + public void anyTrackNonBlocking(RailLink l1, RailLink l2, RailResourceInternal r) { + + assertThat(r.hasCapacity(0, l1, RailResourceManager.ANY_TRACK_NON_BLOCKING, d1)) + .isTrue(); + + double reserved = r.reserve(0, l1, RailResourceManager.ANY_TRACK_NON_BLOCKING, d1); + assertThat(reserved).isEqualTo(10); + + assertThat(r.hasCapacity(0, l1, RailResourceManager.ANY_TRACK_NON_BLOCKING, d2)) + .isFalse(); + + assertThat(r.hasCapacity(0, l2, RailResourceManager.ANY_TRACK_NON_BLOCKING, d2)) + .isTrue(); + + assertThat(r.reserve(0, l2, RailResourceManager.ANY_TRACK_NON_BLOCKING, d2)) + .isEqualTo(10); + + r.release(l1, d1.getDriver()); + + assertThat(r.hasCapacity(0, l1, RailResourceManager.ANY_TRACK_NON_BLOCKING, d3)) + .isTrue(); + + assertThat(r.hasCapacity(0, l2, RailResourceManager.ANY_TRACK_NON_BLOCKING, d1)) + .isFalse(); + + } +} diff --git a/contribs/railsim/test/input/ch/sbb/matsim/contrib/railsim/integration/microStationRerouting/trainNetwork.xml b/contribs/railsim/test/input/ch/sbb/matsim/contrib/railsim/integration/microStationRerouting/trainNetwork.xml index 1ebe818b2c5..7125765a18b 100644 --- a/contribs/railsim/test/input/ch/sbb/matsim/contrib/railsim/integration/microStationRerouting/trainNetwork.xml +++ b/contribs/railsim/test/input/ch/sbb/matsim/contrib/railsim/integration/microStationRerouting/trainNetwork.xml @@ -61,12 +61,12 @@ - 2 + 3 - 2 + 3 true @@ -81,25 +81,25 @@ - 2 + 3 true - 2 + 3 - 2 + 3 true - 2 + 3 diff --git a/contribs/railsim/test/input/ch/sbb/matsim/contrib/railsim/qsimengine/networkDeadlocks.xml b/contribs/railsim/test/input/ch/sbb/matsim/contrib/railsim/qsimengine/networkDeadlocks.xml new file mode 100644 index 00000000000..de496846f1b --- /dev/null +++ b/contribs/railsim/test/input/ch/sbb/matsim/contrib/railsim/qsimengine/networkDeadlocks.xml @@ -0,0 +1,171 @@ + + + + + + Atlantis + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AB + + + + + AB + + + + + Bx + + + + + Bx + + + + + + CD + + + + + CD + + + + + Cx + + + + + Cx + + + + + + + xy + + + + + xy + + + + + + yy + + + + + yy + + + + + + yz + + + + + yz + + + + + + zE + + + + + zE + + + + + EF + + + + + EF + + + + + + Gz + + + + + Gz + + + + + GH + + + + + GH + + + + + + + + diff --git a/contribs/railsim/test/input/ch/sbb/matsim/contrib/railsim/qsimengine/networkMesoUni.xml b/contribs/railsim/test/input/ch/sbb/matsim/contrib/railsim/qsimengine/networkMesoUni.xml index e1296d25fea..22cb9b5fb24 100644 --- a/contribs/railsim/test/input/ch/sbb/matsim/contrib/railsim/qsimengine/networkMesoUni.xml +++ b/contribs/railsim/test/input/ch/sbb/matsim/contrib/railsim/qsimengine/networkMesoUni.xml @@ -33,7 +33,7 @@ - 2 + 3 @@ -43,7 +43,7 @@ - 5 + 6 diff --git a/contribs/railsim/test/input/ch/sbb/matsim/contrib/railsim/qsimengine/networkMixedTypes.xml b/contribs/railsim/test/input/ch/sbb/matsim/contrib/railsim/qsimengine/networkMixedTypes.xml new file mode 100644 index 00000000000..fe3c72e3416 --- /dev/null +++ b/contribs/railsim/test/input/ch/sbb/matsim/contrib/railsim/qsimengine/networkMixedTypes.xml @@ -0,0 +1,197 @@ + + + + + + Atlantis + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 5 + + + + + 3 + l23 + movingBlock + + + + + 3 + l23 + movingBlock + + + + + 1 + l45 + movingBlock + + + + + 1 + l45 + movingBlock + + + + + 3 + l67 + + + + + 3 + l67 + + + + + 1 + l89 + movingBlock + + + + + + 1 + l89 + movingBlock + + + + + 1 + l89 + movingBlock + + + + + 1 + l89 + movingBlock + + + + + 1 + movingBlock + + + + + 1 + movingBlock + + + + + 1 + movingBlock + + + + + 1 + movingBlock + + + + + + 1 + + + + + 1 + + + + + 1 + movingBlock + + + + + + 1 + movingBlock + + + + + + 5 + + + + + + + diff --git a/contribs/railsim/test/input/ch/sbb/matsim/contrib/railsim/qsimengine/networkMovingBlocks.xml b/contribs/railsim/test/input/ch/sbb/matsim/contrib/railsim/qsimengine/networkMovingBlocks.xml new file mode 100644 index 00000000000..b4b9a8b5dd5 --- /dev/null +++ b/contribs/railsim/test/input/ch/sbb/matsim/contrib/railsim/qsimengine/networkMovingBlocks.xml @@ -0,0 +1,99 @@ + + + + + + Atlantis + + + + + + + + + + + + + + + + + + + + + + + + + + l23 + movingBlock + + + + + l23 + movingBlock + + + + + + + + + + + + l45 + movingBlock + + + + + l45 + movingBlock + + + + + + l56 + movingBlock + + + + + l56 + movingBlock + + + + + + + + + + + +