TrajectoryHandler.java

package org.matsim.episim;

import com.google.common.collect.ImmutableMap;
import com.google.inject.Inject;
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.ActivityEndEvent;
import org.matsim.api.core.v01.events.ActivityStartEvent;
import org.matsim.api.core.v01.events.PersonEntersVehicleEvent;
import org.matsim.api.core.v01.events.PersonLeavesVehicleEvent;
import org.matsim.api.core.v01.population.Person;
import org.matsim.episim.model.ContactModel;
import org.matsim.episim.policy.Restriction;
import org.matsim.facilities.ActivityFacility;
import org.matsim.vehicles.Vehicle;

import javax.inject.Named;
import java.time.DayOfWeek;
import java.util.Iterator;
import java.util.Map;
import java.util.SplittableRandom;
import java.util.function.Predicate;

/**
 * Executes trajectory of a person using the events.
 */
final class TrajectoryHandler {

	private static final Logger log = LogManager.getLogger(TrajectoryHandler.class);

	private final EpisimConfigGroup episimConfig;
	private final EpisimReporting reporting;
	private final ContactModel contactModel;
	private final Map<Id<Person>, EpisimPerson> personMap;
	private final Map<Id<Vehicle>, InfectionEventHandler.EpisimVehicle> vehicleMap;
	private final Map<Id<ActivityFacility>, InfectionEventHandler.EpisimFacility> pseudoFacilityMap;

	/**
	 * The "local" random instance, used for all submodels.
	 */
	private final SplittableRandom rnd;

	private int iteration = 0;
	private DayOfWeek day;

	@Inject
	public TrajectoryHandler(EpisimConfigGroup episimConfig, EpisimReporting reporting, ContactModel model, SplittableRandom rnd,
	                         @Named("personMap") Map<Id<Person>, EpisimPerson> personMap,
	                         @Named("vehicleMap") Map<Id<Vehicle>, InfectionEventHandler.EpisimVehicle> vehicleMap,
	                         @Named("pseudoFacilityMap") Map<Id<ActivityFacility>, InfectionEventHandler.EpisimFacility> pseudoFacilityMap) {
		this.rnd = rnd;
		this.episimConfig = episimConfig;
		this.reporting = reporting;
		this.contactModel = model;
		this.personMap = personMap;
		this.vehicleMap = vehicleMap;
		this.pseudoFacilityMap = pseudoFacilityMap;
	}

	SplittableRandom getRnd() {
		return rnd;
	}

	void setRestrictionsForIteration(int iteration, ImmutableMap<String, Restriction> im) {
		this.iteration = iteration;
		this.day = EpisimUtils.getDayOfWeek(episimConfig, iteration);
		contactModel.setRestrictionsForIteration(iteration, im);
	}

	/**
	 * @see ContactModel#getNumContacts()
	 */
	int getNumContacts() {
		return contactModel.getNumContacts();
	}

	/**
	 * Handle plans with "holes" in their trajectory.
	 * <p>
	 * In the data, activity start events at the beginning can be missing.
	 * Likewise, activity end events at the end of a day might be missing.
	 * This leads to implicitly given first and last containers of the day.
	 *
	 * @param day         day that is about to start
	 * @param responsible used for partitioning of trajectory handlers
	 */
	@Deprecated
	void checkAndHandleEndOfNonCircularTrajectory(EpisimPerson person, DayOfWeek day, Predicate<Id<?>> responsible) {

		Id<ActivityFacility> lastFacilityId = person.getLastFacilityId(day.minus(1));
		Id<ActivityFacility> firstFacilityId = person.getFirstFacilityId(day);

		// now is the start of current day, when this is called iteration still has the value of the last day
		double now = EpisimUtils.getCorrectedTime(episimConfig.getStartOffset(), 0, iteration + 1);

		// TODO: are unclosed trajectories with PT possible?

		if (!lastFacilityId.equals(firstFacilityId)) {
			InfectionEventHandler.EpisimFacility lastFacility = this.pseudoFacilityMap.get(lastFacilityId);

			// index of last activity at previous day
			String actType = person.getActivity(day.minus(1), 24 * 3600.).actType();
			double timeSpent = now - lastFacility.getContainerEnteringTime(person.getPersonId());
			person.addSpentTime(actType, timeSpent);

			if (iteration > 1 && timeSpent > 86400 && !actType.equals("home")) {
				// there might be some implausible trajectories
				log.trace("{} spent {} outside home", person, timeSpent);
			}

			if (responsible.test(lastFacilityId)) {
				contactModel.infectionDynamicsFacility(person, lastFacility, now);
				lastFacility.removePerson(person);
			}

			if (responsible.test(firstFacilityId)) {
				InfectionEventHandler.EpisimFacility firstFacility = this.pseudoFacilityMap.get(firstFacilityId);
				firstFacility.addPerson(person, now, person.getFirstActivity(day));

				contactModel.notifyEnterFacility(person, firstFacility, now);
			}


		} else {
			// TODO: check if still needed
			InfectionEventHandler.EpisimFacility firstFacility = this.pseudoFacilityMap.get(firstFacilityId);

			if (responsible.test(firstFacility.getContainerId())) {
				firstFacility.addPerson(person, now, person.getFirstActivity(day));
				contactModel.notifyEnterFacility(person, firstFacility, now);
			}
		}
	}

	/**
	 * Called of start of day before any handleEvent method.
	 *
	 * @param responsible predicate for checking if the handler is responsible for a certain facility
	 */
	public void onStartDay(Predicate<Id<ActivityFacility>> responsibleFacility,
	                       Predicate<Id<Vehicle>> responsibleVehicle) {

		double now = EpisimUtils.getCorrectedTime(episimConfig.getStartOffset(), 0, iteration);
		DayOfWeek day = EpisimUtils.getDayOfWeek(episimConfig, iteration);

		// need to use previous as in config
		DayOfWeek prevDay = EpisimUtils.getDayOfWeek(episimConfig, iteration - 1);

		for (InfectionEventHandler.EpisimFacility facility : pseudoFacilityMap.values()) {
			if (!responsibleFacility.test(facility.getContainerId()))
				continue;

			facility.resetContagiousCounter();

			Iterator<EpisimPerson> it = facility.getPersons().iterator();

			while (it.hasNext()) {

				EpisimPerson person = it.next();

				assert facility.getContainerId().equals(person.getLastFacilityId(prevDay)) :
						String.format("Person %s needs to be in its last facility (%s) at the end of the day, but is in %s",
								person.getPersonId(), person.getLastFacilityId(prevDay), facility.getContainerId());

				// person needs to be at a different container and is removed here
				if (person.getStaysInContainer(prevDay) && !person.getLastFacilityId(prevDay).equals(person.getFirstFacilityId(day))) {

					EpisimPerson.PerformedActivity lastActivity = facility.getPerformedActivity(person.getPersonId());

					double timeSpent = now - facility.getContainerEnteringTime(person.getPersonId());
					person.addSpentTime(lastActivity.actType(), timeSpent);

					contactModel.infectionDynamicsFacility(person, facility, now);
					facility.removePerson(person, it);
				} else if (person.infectedButNotSerious())
					facility.countContagious(1);
			}
		}

		// all persons still in vehicles are removed at the end of the day
		for (InfectionEventHandler.EpisimVehicle vehicle : vehicleMap.values()) {
			if (!responsibleVehicle.test(vehicle.getContainerId()))
				continue;

			Iterator<EpisimPerson> it = vehicle.getPersons().iterator();
			while (it.hasNext()) {
				EpisimPerson person = it.next();
				contactModel.infectionDynamicsVehicle(person, vehicle, now);
				vehicle.removePerson(person, it);
			}
		}


		for (EpisimPerson person : personMap.values()) {

			Id<ActivityFacility> firstFacilityId = person.getFirstFacilityId(day);
			InfectionEventHandler.EpisimFacility firstFacility = pseudoFacilityMap.get(firstFacilityId);

			if (!responsibleFacility.test(firstFacilityId))
				continue;

			if (!person.checkFirstActivity(day, 0))
				continue;

			if (!person.getStaysInContainer(prevDay) || !person.getLastFacilityId(prevDay).equals(firstFacilityId)) {
				firstFacility.addPerson(person, now, person.getFirstActivity(day));
				contactModel.notifyEnterFacility(person, firstFacility, now);
			}
		}
	}

	/**
	 * Checks whether this person does perform the activity at {@code time}
	 */
	private boolean checkParticipation(EpisimPerson person, double time) {
		if (episimConfig.getActivityHandling() == EpisimConfigGroup.ActivityHandling.duringContact)
			return true;

		return person.checkActivity(day, time);
	}

	private boolean checkVehicleUsage(EpisimPerson person, double time) {
		if (episimConfig.getActivityHandling() == EpisimConfigGroup.ActivityHandling.duringContact)
			return true;

		return person.checkActivity(day, time) && person.checkNextActivity(day, time);
	}

	public void handleEvent(ActivityStartEvent activityStartEvent) {
//		double now = activityStartEvent.getTime();
		double now = EpisimUtils.getCorrectedTime(episimConfig.getStartOffset(), activityStartEvent.getTime(), iteration);

		// find the person:
		EpisimPerson episimPerson = this.personMap.get(activityStartEvent.getPersonId());

		if (!checkParticipation(episimPerson, activityStartEvent.getTime()))
			return;

		reporting.handleEvent(activityStartEvent);

		// create pseudo facility id that includes the activity type:
		Id<ActivityFacility> episimFacilityId = activityStartEvent.getFacilityId();

		// find the facility
		InfectionEventHandler.EpisimFacility episimFacility = this.pseudoFacilityMap.get(episimFacilityId);

		// add person to facility
		episimFacility.addPerson(episimPerson, now, episimPerson.getActivity(day, activityStartEvent.getTime()));

		contactModel.notifyEnterFacility(episimPerson, episimFacility, now);
	}

	public void handleEvent(ActivityEndEvent activityEndEvent) {
//		double now = activityEndEvent.getTime();
		double now = EpisimUtils.getCorrectedTime(episimConfig.getStartOffset(), activityEndEvent.getTime(), iteration);

		EpisimPerson episimPerson = this.personMap.get(activityEndEvent.getPersonId());

		// create pseudo facility id that includes the activity type:
		Id<ActivityFacility> episimFacilityId = activityEndEvent.getFacilityId();

		// find the facility
		InfectionEventHandler.EpisimFacility episimFacility = this.pseudoFacilityMap.get(episimFacilityId);

		// person did not perform this activity
		if (episimConfig.getActivityHandling() == EpisimConfigGroup.ActivityHandling.startOfDay && !episimFacility.containsPerson(episimPerson))
			return;

		reporting.handleEvent(activityEndEvent);

		if (episimConfig.getContagiousOptimization() == EpisimConfigGroup.ContagiousOptimization.no ||
				episimFacility.containsContagious()) {
			contactModel.infectionDynamicsFacility(episimPerson, episimFacility, now);
		}

		if (episimConfig.getReportTimeUse() == EpisimConfigGroup.ReportTimeUse.yes) {
			double timeSpent = now - episimFacility.getContainerEnteringTime(episimPerson.getPersonId());
			episimPerson.addSpentTime(activityEndEvent.getActType(), timeSpent);
		}

		episimFacility.removePerson(episimPerson);
	}

	public void handleEvent(PersonEntersVehicleEvent entersVehicleEvent) {
//		double now = entersVehicleEvent.getTime();
		double now = EpisimUtils.getCorrectedTime(episimConfig.getStartOffset(), entersVehicleEvent.getTime(), iteration);

		// find the person:
		EpisimPerson episimPerson = this.personMap.get(entersVehicleEvent.getPersonId());

		if (!checkVehicleUsage(episimPerson, entersVehicleEvent.getTime()))
			return;

		reporting.handleEvent(entersVehicleEvent);

		// find the vehicle:
		InfectionEventHandler.EpisimVehicle episimVehicle = this.vehicleMap.get(entersVehicleEvent.getVehicleId());

		// add person to vehicle and memorize entering time:
		episimVehicle.addPerson(episimPerson, now, EpisimPerson.UNSPECIFIC_ACTIVITY);

		contactModel.notifyEnterVehicle(episimPerson, episimVehicle, now);
	}

	public void handleEvent(PersonLeavesVehicleEvent leavesVehicleEvent) {
//		double now = leavesVehicleEvent.getTime();
		double now = EpisimUtils.getCorrectedTime(episimConfig.getStartOffset(), leavesVehicleEvent.getTime(), iteration);

		// find vehicle:
		InfectionEventHandler.EpisimVehicle episimVehicle = this.vehicleMap.get(leavesVehicleEvent.getVehicleId());

		EpisimPerson episimPerson = this.personMap.get(leavesVehicleEvent.getPersonId());

		// person did not enter the vehicle
		if (episimConfig.getActivityHandling() == EpisimConfigGroup.ActivityHandling.startOfDay && !episimVehicle.containsPerson(episimPerson))
			return;

		reporting.handleEvent(leavesVehicleEvent);

		if (episimConfig.getContagiousOptimization() == EpisimConfigGroup.ContagiousOptimization.no ||
				episimVehicle.containsContagious()) {
			contactModel.infectionDynamicsVehicle(episimPerson, episimVehicle, now);
		}


		if (episimConfig.getReportTimeUse() == EpisimConfigGroup.ReportTimeUse.yes) {
			double timeSpent = now - episimVehicle.getContainerEnteringTime(episimPerson.getPersonId());

			episimPerson.addSpentTime("pt", timeSpent);
		}

		// remove person from vehicle:
		episimVehicle.removePerson(episimPerson);
	}

	public void reportCpuTime(String what, int taskId) {
		reporting.reportCpuTime(iteration, "TrajectoryHandler", what, taskId);
	}

	public InfectionEventHandler.EpisimFacility getEpisimFacility(Id<ActivityFacility> id) {
		return this.pseudoFacilityMap.get(id);
	}

	public InfectionEventHandler.EpisimVehicle getEpisimVehicle(Id<Vehicle> id) {
		return this.vehicleMap.get(id);
	}
}