Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Development #1

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open

Development #1

wants to merge 15 commits into from

Conversation

rsjeffers
Copy link

PR to ask questions

The observation class defines the basic structure to implement the observation tree. It's still a WIP. Therefore, the entire class is commented out.
@CLAassistant
Copy link

CLAassistant commented Apr 26, 2024

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
0 out of 2 committers have signed the CLA.

❌ ID-Akash
❌ rakow
You have signed the CLA already but the status is still pending? Let us recheck it.

@munterfi
Copy link
Collaborator

munterfi commented May 8, 2024

Hi @ID-Akash and @rsjeffers

I tried to address the TODOs in the TreeObservation class:

package ch.sbb.matsim.contrib.railsim.observation;

import ch.sbb.matsim.contrib.railsim.qsimengine.RailsimCalc;
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 ch.sbb.matsim.contrib.railsim.qsimengine.resources.RailResourceManager;
import ch.sbb.matsim.contrib.railsim.qsimengine.resources.ResourceState;
import org.matsim.api.core.v01.network.Link;
import org.matsim.api.core.v01.network.Network;
import org.matsim.api.core.v01.network.Node;
// TODO: What is the time resolution of the environment? 1 iteration is equal to home many seconds in rlsim?
// The time resolution of matsim is seconds, same for railsim. But there are seconds where nothing happens, no events in update queue.
import java.util.List;
import java.util.ArrayList;

class OtherAgent {
	// Distance of agent to other agent's tail position if the train is moving in the same direction
	//	Otherwise, distance to BufferTip position if moving in opposite direction
	double distance;
	List<Double> bufferTipPosition;
	List<Double> tailPosition;
	double speed;

	public OtherAgent(double distance, List<Double> bufferTipPosition, List<Double> tailPosition, double speed, boolean sameDirection) {
		this.distance = distance;
		this.bufferTipPosition = bufferTipPosition;
		this.tailPosition = tailPosition;
		this.speed = speed;
		this.sameDirection = sameDirection;
	}

	boolean sameDirection;

}

class ObservationNode {
	Node node;
	double distNodeAgent;
	double distNodeStop;
	boolean isSwitchable;
	OtherAgent sameDirAgent;
//	OtherAgent oppDirAgent;
}

public class TreeObservation {
	private final TrainPosition position;
	private final RailResourceManager resources;
	private final Network network;
	private List<ObservationNode> observation;

	public TreeObservation(TrainPosition position, RailResourceManager resources, Network network) {
		this.resources = resources;
		this.position = position;
		this.network = network;
		createTreeObs();
	}

	public RailLink getBufferTip() {
		double reserveDist = RailsimCalc.calcReservationDistance(position, resources.getLink(position.getHeadLink()));
		RailLink currentLink = resources.getLink(position.getHeadLink());
		List<RailLink> reservedSegment = RailsimCalc.calcLinksToBlock(position, currentLink, reserveDist);
		// TODO Verify if the links are added in the list in the sequence of occurence.
		RailLink bufferTip = reservedSegment.get(reservedSegment.size() - 1);

		return bufferTip;
	}

	private void createTreeObs() {
		int depth = 3;
		createTreeObs(depth);
	}

	// TODO: Handle cases when the agent should also see the next 2 halts. - But this may not be needed as the agent ca be penalised
//  if the agent is unable to reach the next station.
	private void createTreeObs(int depth) {
		// Get the link of the tip of the buffer
		RailLink bufferTipLink = getBufferTip();

		// Get the toNode of the bufferTipLink
		Node toNode = getToNode(bufferTipLink);
		List<Node> exploreQueue = new ArrayList<Node>();

		exploreQueue.addLast(toNode);
		for (int i = 0; i < depth; i++) {
			// Depth traversal: Making an observation Tree of fixed depth
			int lenExploreQueue = exploreQueue.size();
			while (lenExploreQueue > 0) {
				// Level traversal
				Node curNode = exploreQueue.getFirst();
				exploreQueue.remove(0);

				// Look for switches/intersections/stops on the branches stemming out of the current switch
				List<Node> nextNodes = getNextNodes(curNode);

				for (Node nextNode : nextNodes) {
					exploreQueue.addLast(nextNode);
					TrainPosition trainF = getClosestTrainOnPathF(curNode, nextNode);
					TrainPosition trainR = getClosestTrainOnPathR(curNode, nextNode);
					observation.add(createObservatioNode(nextNode, trainF, trainR));
				}
				lenExploreQueue -= 1;
			}
		}
	}

	private TrainPosition getClosestTrainOnPathR(Node curNode, Node nextNode) {
		// complete route to current position of the train from schedule, if needed?
		List<RailLink> previousRoute = position.getRoute(0, position.getRouteIndex());

		// TODO: If all opposite trains are needed, follow the inLinks of curNode until the nextNode is reached. Store the of the path links.
		List<Link> path = null;

		// check for each link if there is capacity
		for (Link link : path) {
			RailLink railLink = resources.getLink(link.getId());
			RailResource resource = railLink.getResource();
			ResourceState state = resource.getState(railLink);
			// TODO: Ask Christian how we get the train position of the nearest train.
		}

		return null;
	}

	private TrainPosition getClosestTrainOnPathF(Node curNode, Node nextNode) {
		// complete route from current position of the train from schedule, if needed?
		List<RailLink> upcomingRoute = position.getRoute(position.getRouteIndex(), position.getRouteSize());

		// TODO: If all opposite trains are needed, follow the outLinks of curNode until the nextNode is reached. Store the of the path links.
		List<Link> path = null;

		// same procedure as above...

		return null;
	}

	private List<Node> getNextNodes(Node curNode) {
		// next switches
		List<Node> switchNodes = new ArrayList<>();

		// check all outgoing links from the current node
		for (Link outLink : curNode.getOutLinks().values()) {
			Node nextNode = outLink.getToNode();

			// follow nodes with only one outgoing link until a switch is reached
			while (nextNode.getOutLinks().size() == 1) {
				// get the single outgoing link and follow it
				// TODO: Handle end of network, will throw NoSuchElementException at the moment
				nextNode = nextNode.getOutLinks().values().iterator().next().getToNode();
			}

			// if the next node has more than one outgoing link, it's a switch
			if (nextNode.getOutLinks().size() > 1) {
				switchNodes.add(nextNode);
			}
		}

		return switchNodes;
	}

	private Node getToNode(RailLink bufferTipLink) {
		return network.getLinks().get(bufferTipLink.getLinkId()).getToNode();
	}

	public List<ObservationNode> getObservation() {
		return observation;
	}
}

The TreeObservation needs to have access to the Network, therefore i added it to the constructor.

To get access to the TrainPositions of the opponent trains, we should consult Christian (@rakow), as a simple method to extract this object was not obvious to me.

Best, M.

@rakow
Copy link

rakow commented May 8, 2024

The disposition is given the TrainPosition on each request, as well as for departing trains. It should be able to keep track of the state and update it accordingly on every change. The information always goes from the railsim engine to other components, and can not be directly requested. Another option would be to let the RailResourceManager keep track of some state that is commonly used in multiple dispoisition strategies.

@rsjeffers
Copy link
Author

@rakow thank you for your message, I didn't fully understand could you elaborate a little on what you're proposing? For each train we need to be able to observe the trains near to it in order to make an informed decision about which direction to take

@munterfi
Copy link
Collaborator

munterfi commented May 16, 2024

Maybe I got it completely wrong, but I interpreted @rakow answer as follows:

package ch.sbb.matsim.contrib.railsim.rl;

import ch.sbb.matsim.contrib.railsim.config.RailsimConfigGroup;
import ch.sbb.matsim.contrib.railsim.qsimengine.RailsimEngine;
import ch.sbb.matsim.contrib.railsim.qsimengine.deadlocks.NoDeadlockAvoidance;
import ch.sbb.matsim.contrib.railsim.qsimengine.resources.RailResourceManager;
import org.matsim.api.core.v01.Id;
import org.matsim.api.core.v01.Scenario;
import org.matsim.api.core.v01.population.Person;
import org.matsim.core.api.experimental.events.EventsManager;
import org.matsim.core.events.EventsUtils;

public class RLEnvironment {
	private final Scenario scenario;
	private final RailsimEngine engine;
	private final EventsManager eventsManager;
	private final RLDisposition disposition;
	private final RailResourceManager resourceManager;
	private double time = 0;

	public RLEnvironment(Scenario scenario) {
		RailsimConfigGroup config = new RailsimConfigGroup();
		this.scenario = scenario;
		this.eventsManager = EventsUtils.createEventsManager();
		// TODO: Add a BasicEventHandler implementation that handles or tracks events as needed
		// eventsManager.addHandler(BasicEventHandler);
		// eventsManager.initProcessing();
		this.resourceManager = new RailResourceManager(eventsManager, config, scenario.getNetwork(), new NoDeadlockAvoidance());
		this.disposition = new RLDisposition(scenario.getNetwork(), resourceManager); // pass in disposition whatever needed
		engine = new RailsimEngine(eventsManager, config, resourceManager, disposition);
	}

	public TreeObservation step(Id<Person> agent) {
		// wait until agent with agentId calls rl disposition
		// while (true) {
		//	engine.doSimStep(time++);
		// }

		// TODO: Generate tree observation internally in the disposition and let the agent decide based on it, see RLDisposition::requestNextSegment
		//  Maybe some synchronization needed... semaphore or something in the disposition?

		// TODO: wait until action has manifested for agent, how dis this defined? Fixed time threshold, next link enter event, arrival at next station, ...
		// while (true) {
		// 	engine.doSimStep(time++);
		// }

		// gather new tree observation for agent
		return disposition.getTreeObservation(agent);
	}

	public static void main(String[] args) {
		// TODO: load the scenario
		Scenario scenario;
		RLEnvironment env = new RLEnvironment(scenario);
		// step through the simulation
		env.step(Id.createPersonId("a1"));
	}

}

And track all the train positions in the disposition:

package ch.sbb.matsim.contrib.railsim.rl;

import ch.sbb.matsim.contrib.railsim.qsimengine.TrainPosition;
import ch.sbb.matsim.contrib.railsim.qsimengine.disposition.DispositionResponse;
import ch.sbb.matsim.contrib.railsim.qsimengine.disposition.TrainDisposition;
import ch.sbb.matsim.contrib.railsim.qsimengine.resources.RailLink;
import ch.sbb.matsim.contrib.railsim.qsimengine.resources.RailResourceManager;
import org.matsim.api.core.v01.Id;
import org.matsim.api.core.v01.network.Link;
import org.matsim.api.core.v01.network.Network;
import org.matsim.api.core.v01.population.Person;
import org.matsim.core.mobsim.framework.MobsimDriverAgent;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class RLDisposition implements TrainDisposition {

	private final Network network;
	private final RailResourceManager resourceManager;
	// maybe more is needed here...

	private final Map<Id<Person>, TrainPosition> trainPositions = new HashMap<>();
	private final Map<Id<Link>, TrainPosition> trainPositionsToLink = new HashMap<>();
	// note: ignores the case of multiple trains on the same link...

	public RLDisposition(Network network, RailResourceManager resourceManager) {
		this.network = network;
		this.resourceManager = resourceManager;
	}

	@Override
	public void onDeparture(double time, MobsimDriverAgent driver, List<RailLink> route) {
		// TODO: Mark trains as active, store initial route if needed
	}

	@Override
	public DispositionResponse requestNextSegment(double time, TrainPosition position, double dist) {
		// track all trains in the network
		trainPositions.put(position.getDriver().getId(), position);
		trainPositionsToLink.put(position.getHeadLink(), position);
		// create tree observation for current agent
		TreeObservation observation = getTreeObservation(position.getDriver().getId());
		// TODO: gRPC call to python with tree observation
		//  Then return the disposition response from python
		return null;
	}

	@Override
	public void unblockRailLink(double time, MobsimDriverAgent driver, RailLink link) {
		// TODO: Release resource in resource manager
	}

	public TreeObservation getTreeObservation(Id<Person> agent) {
		TrainPosition currentAgentPosition = trainPositions.get(agent);
		// TODO: Generate the tree observations here:
		//  Follow the network from the current node
		//  lookup in the trainPositionsToLink map if there are any trains on the link
		//  create TreeObservation object and return it
		return null;
	}
}

The approach is for sure not complete, since I am a bit confused now if we use gRPC or directly Python to control Java...

@munterfi
Copy link
Collaborator

@rsjeffers Can you give us write permissions on the fork, then we don't have to copy the code into the comments? Thanks 😄

@munterfi
Copy link
Collaborator

munterfi commented May 21, 2024

And track all the train positions in the disposition:

package ch.sbb.matsim.contrib.railsim.rl;

import ch.sbb.matsim.contrib.railsim.qsimengine.TrainPosition;
import ch.sbb.matsim.contrib.railsim.qsimengine.disposition.DispositionResponse;
import ch.sbb.matsim.contrib.railsim.qsimengine.disposition.TrainDisposition;
import ch.sbb.matsim.contrib.railsim.qsimengine.resources.RailLink;
import ch.sbb.matsim.contrib.railsim.qsimengine.resources.RailResourceManager;
import org.matsim.api.core.v01.Id;
import org.matsim.api.core.v01.network.Link;
import org.matsim.api.core.v01.network.Network;
import org.matsim.api.core.v01.population.Person;
import org.matsim.core.mobsim.framework.MobsimDriverAgent;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class RLDisposition implements TrainDisposition {

	private final Network network;
	private final RailResourceManager resourceManager;
	// maybe more is needed here...

	private final Map<Id<Person>, TrainPosition> trainPositions = new HashMap<>();
	private final Map<Id<Link>, TrainPosition> trainPositionsToLink = new HashMap<>();

	public RLDisposition(Network network, RailResourceManager resourceManager) {
		this.network = network;
		this.resourceManager = resourceManager;
	}

	@Override
	public void onDeparture(double time, MobsimDriverAgent driver, List<RailLink> route) {
		// TODO: Mark trains as active, store initial route if needed
	}

	@Override
	public DispositionResponse requestNextSegment(double time, TrainPosition position, double dist) {
		// track all trains in the network
		trainPositions.put(position.getDriver().getId(), position);
		trainPositionsToLink.put(position.getHeadLink(), position);
		// create tree observation for current agent
		TreeObservation observation = getTreeObservation(position.getDriver().getId());
		// TODO: gRPC call to python with tree observation
		//  Then return the disposition response from python
		return null;
	}

	@Override
	public void unblockRailLink(double time, MobsimDriverAgent driver, RailLink link) {
		// TODO: Release resource in resource manager
	}

	public TreeObservation getTreeObservation(Id<Person> agent) {
		TrainPosition currentAgentPosition = trainPositions.get(agent);
		// TODO: Generate the tree observations here:
		//  Follow the network from the current node
		//  lookup in the trainPositionsToLink map if there are any trains on the link
		//  create TreeObservation object and return it
		return null;
	}
}

The approach is for sure not complete, since I am a bit confused now if we use gRPC or directly Python to control Java...

There are some still some issues we must solve I guess on the railsim side. The tracked information in the disposition is always the trainState at the moment the disposition was called and not at the time step when the ObservationTree is calculated... I think for the start we can work with this and look for a solution to get the states from the engine from all trains at a specific time step when we have a running RL setup?

A possible solution could be to catch the RailsimTrainsStateEvents in the event handler (interface to be implemented: BasicEventHandler) which is added to the event manager and set the updateInterval in the railsim config to one second:

  public RLEnvironment(Scenario scenario) {
  	RailsimConfigGroup config = new RailsimConfigGroup();
  	this.scenario = scenario;
  	this.eventsManager = EventsUtils.createEventsManager();
  	// TODO: Add a BasicEventHandler implementation that handles or tracks events as needed
  	// eventsManager.addHandler(BasicEventHandler);
  	// eventsManager.initProcessing();
  	// ...

Note: These are events, which are written to the MATSim output events file and not internal events of the railsim engine in the update queue!

sequence_activity_engine_overview_IDLE

@rsjeffers
Copy link
Author

Hi @munterfi you should already have write access, is it working now?

@rakow
Copy link

rakow commented May 22, 2024

@munterfi I think your proposal looks like what I suggested. The disposition is called each time a train needs to make a request, which is fairly often. So it should have a very up-to-date state. Rarely there might be position updates where the disposition may not be involved. You also have to keep in mind the design of the simulation. A train state is updated when necessary and not at a fixed time step. So there might be state update for t=0s and t=10s. If another train requests the disposition t=5s, the exact position of the first train will simply be not available because it has never been calculated.

Enforcing state updates of all other trains may be possible, but I'm not sure if that is a good idea. In reality, you would also have to rely on the last reported position of a train, and we have a fairly low update interval (< 10s). To summarize, I would suggest using the last known position of the other trains instead of enforcing updates. You may also try to play around with reducing the update interval.

Another note: If you need functionality to keep track of train states, I would rather directly implement in the rail sim engine and resource manager instead of creating an event listener. Event listener maybe possible, but it is more difficult to use and implement. The resource manager is already available to all disposition and may be used to query the state. Alternatively, you may create a dedicated class for keeping track of the states.

@munterfi
Copy link
Collaborator

@rakow, thanks for your feedback!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants