diff --git a/src/main/scala/beam/agentsim/agents/PersonAgent.scala b/src/main/scala/beam/agentsim/agents/PersonAgent.scala index 9ee5e284d48..1df2f842243 100755 --- a/src/main/scala/beam/agentsim/agents/PersonAgent.scala +++ b/src/main/scala/beam/agentsim/agents/PersonAgent.scala @@ -189,6 +189,7 @@ object PersonAgent { restOfCurrentTrip: List[EmbodiedBeamLeg] = List(), currentVehicle: VehicleStack = Vector(), currentTourMode: Option[BeamMode] = None, + currentTripMode: Option[BeamMode] = None, currentTourPersonalVehicle: Option[Id[BeamVehicle]] = None, passengerSchedule: PassengerSchedule = PassengerSchedule(), currentLegPassengerScheduleIndex: Int = 0, @@ -285,6 +286,8 @@ object PersonAgent { case basePersonData: BasePersonData => Some(basePersonData) case _ => None } + + def atHome(activity: Activity): Boolean = activity.getType.equalsIgnoreCase("home") } class PersonAgent( @@ -406,7 +409,7 @@ class PersonAgent( // which is used in place of our real remaining tour distance of 0.0 // which should help encourage residential end-of-day charging val tomorrowFirstLegDistance = - if (nextAct.getType.toLowerCase == "home") { + if (atHome(nextAct)) { findFirstCarLegOfTrip(personData) match { case Some(carLeg) => carLeg.beamLeg.travelPath.distanceInM @@ -475,6 +478,30 @@ class PersonAgent( } } + def isFirstTripWithinTour(personData: BasePersonData, nextAct: Activity): Boolean = { + val (tripIndexOfElement: Int, _) = currentTripIndexWithinTour(personData, nextAct) + tripIndexOfElement == 0 + } + + def isLastTripWithinTour(personData: BasePersonData, nextAct: Activity): Boolean = { + val (tripIndexOfElement: Int, lastTripIndex: Int) = currentTripIndexWithinTour(personData, nextAct) + tripIndexOfElement == lastTripIndex + } + + def isFirstOrLastTripWithinTour(personData: BasePersonData, nextAct: Activity): Boolean = { + val (tripIndexOfElement: Int, lastTripIndex: Int) = currentTripIndexWithinTour(personData, nextAct) + tripIndexOfElement == 0 || tripIndexOfElement == lastTripIndex + } + + def currentTripIndexWithinTour(personData: BasePersonData, nextAct: Activity): (Int, Int) = { + val tour = currentTour(personData) + val lastTripIndex = tour.trips.size - 1 + val tripIndexOfElement = tour + .tripIndexOfElement(nextAct) + .getOrElse(throw new IllegalArgumentException(s"Element [$nextAct] not found")) + (tripIndexOfElement, lastTripIndex) + } + def currentActivity(data: BasePersonData): Activity = _experiencedBeamPlan.activities(data.currentActivityIndex) @@ -586,15 +613,10 @@ class PersonAgent( val nextCoord = nextActivity(data).get.getCoord goto(ChoosingMode) using ChoosesModeData( personData = data.copy( - // If the mode of the next leg is defined and is CAV, use it, otherwise, - // If we don't have a current tour mode (i.e. are not on a tour aka at home), - // use the mode of the next leg as the new tour mode. - currentTourMode = modeOfNextLeg match { - case Some(CAV) => - Some(CAV) - case _ => - data.currentTourMode.orElse(modeOfNextLeg) - }, + // We do not stick to current tour mode + // If we have the currentTourPersonalVehicle then we should use it + // use the mode of the next leg as the new trip mode. + currentTripMode = modeOfNextLeg, numberOfReplanningAttempts = 0, failedTrips = IndexedSeq.empty, enrouteData = EnrouteData() @@ -611,7 +633,7 @@ class PersonAgent( when(Teleporting) { case Event( TriggerWithId(PersonDepartureTrigger(tick), triggerId), - data @ BasePersonData(_, Some(currentTrip), _, _, _, _, _, _, false, _, _, _, _, _) + data @ BasePersonData(_, Some(currentTrip), _, _, _, _, _, _, _, false, _, _, _, _, _) ) => endActivityAndDepart(tick, currentTrip, data) @@ -625,7 +647,7 @@ class PersonAgent( case Event( TriggerWithId(TeleportationEndsTrigger(tick), triggerId), - data @ BasePersonData(_, Some(currentTrip), _, _, maybeCurrentTourMode, _, _, _, true, _, _, _, _, _) + data @ BasePersonData(_, Some(currentTrip), _, _, _, maybeCurrentTripMode, _, _, _, true, _, _, _, _, _) ) => holdTickAndTriggerId(tick, triggerId) @@ -638,7 +660,7 @@ class PersonAgent( startY = currentTrip.legs.head.beamLeg.travelPath.startPoint.loc.getY, endX = currentTrip.legs.last.beamLeg.travelPath.endPoint.loc.getX, endY = currentTrip.legs.last.beamLeg.travelPath.endPoint.loc.getY, - currentTourMode = maybeCurrentTourMode.map(_.value) + currentTourMode = maybeCurrentTripMode.map(_.value) ) eventsManager.processEvent(teleportationEvent) @@ -657,7 +679,7 @@ class PersonAgent( */ case Event( TriggerWithId(PersonDepartureTrigger(tick), triggerId), - data @ BasePersonData(_, Some(currentTrip), _, _, _, _, _, _, false, _, _, _, _, _) + data @ BasePersonData(_, Some(currentTrip), _, _, _, _, _, _, _, false, _, _, _, _, _) ) => endActivityAndDepart(tick, currentTrip, data) @@ -666,7 +688,7 @@ class PersonAgent( case Event( TriggerWithId(PersonDepartureTrigger(tick), triggerId), - BasePersonData(_, _, restOfCurrentTrip, _, _, _, _, _, true, _, _, _, _, _) + BasePersonData(_, _, restOfCurrentTrip, _, _, _, _, _, _, true, _, _, _, _, _) ) => // We're coming back from replanning, i.e. we are already on the trip, so we don't throw a departure event logDebug(s"replanned to leg ${restOfCurrentTrip.head}") @@ -722,7 +744,7 @@ class PersonAgent( val currentCoord = beamServices.geo.wgs2Utm(data.restOfCurrentTrip.head.beamLeg.travelPath.startPoint).loc val nextCoord = nextActivity(data).get.getCoord goto(ChoosingMode) using ChoosesModeData( - data.copy(currentTourMode = None, numberOfReplanningAttempts = data.numberOfReplanningAttempts + 1), + data.copy(currentTripMode = None, numberOfReplanningAttempts = data.numberOfReplanningAttempts + 1), currentLocation = SpaceTime( currentCoord, tick @@ -741,7 +763,7 @@ class PersonAgent( // TRANSIT FAILURE case Event( ReservationResponse(Left(firstErrorResponse), _), - data @ BasePersonData(_, _, nextLeg :: _, _, _, _, _, _, _, _, _, _, _, _) + data @ BasePersonData(_, _, nextLeg :: _, _, _, _, _, _, _, _, _, _, _, _, _) ) => logDebug(s"replanning because ${firstErrorResponse.errorCode}") @@ -815,7 +837,7 @@ class PersonAgent( // RIDE HAIL FAILURE case Event( response @ RideHailResponse(_, _, Some(error), _, _), - data @ BasePersonData(_, _, _, _, _, _, _, _, _, _, _, _, _, _) + data: BasePersonData ) => handleFailedRideHailReservation(error, response, data) } @@ -826,7 +848,7 @@ class PersonAgent( */ case Event( TriggerWithId(BoardVehicleTrigger(tick, vehicleToEnter), triggerId), - data @ BasePersonData(_, _, currentLeg :: _, currentVehicle, _, _, _, _, _, _, _, _, _, _) + data @ BasePersonData(_, _, currentLeg :: _, currentVehicle, _, _, _, _, _, _, _, _, _, _, _) ) => logDebug(s"PersonEntersVehicle: $vehicleToEnter @ $tick") eventsManager.processEvent(new PersonEntersVehicleEvent(tick, id, vehicleToEnter)) @@ -859,7 +881,7 @@ class PersonAgent( */ case Event( TriggerWithId(AlightVehicleTrigger(tick, vehicleToExit, energyConsumedOption), triggerId), - data @ BasePersonData(_, _, _ :: restOfCurrentTrip, currentVehicle, _, _, _, _, _, _, _, _, _, _) + data @ BasePersonData(_, _, _ :: restOfCurrentTrip, currentVehicle, _, _, _, _, _, _, _, _, _, _, _) ) if vehicleToExit.equals(currentVehicle.head) => updateFuelConsumed(energyConsumedOption) logDebug(s"PersonLeavesVehicle: $vehicleToExit @ $tick") @@ -936,6 +958,7 @@ class PersonAgent( _, _, _, + _, currentCost, _, _, @@ -980,6 +1003,7 @@ class PersonAgent( goto(ChoosingMode) using ChoosesModeData( basePersonData.copy( currentTourMode = None, // Have to give up my mode as well, perhaps there's no option left for driving. + currentTripMode = None, currentTourPersonalVehicle = None, numberOfReplanningAttempts = basePersonData.numberOfReplanningAttempts + 1 ), @@ -1076,6 +1100,7 @@ class PersonAgent( _, _, _, + _, _ ) ) if nextLeg.asDriver => @@ -1170,7 +1195,7 @@ class PersonAgent( nextState // TRANSIT but too late - case Event(StateTimeout, data @ BasePersonData(_, _, nextLeg :: _, _, _, _, _, _, _, _, _, _, _, _)) + case Event(StateTimeout, data @ BasePersonData(_, _, nextLeg :: _, _, _, _, _, _, _, _, _, _, _, _, _)) if nextLeg.beamLeg.mode.isTransit && nextLeg.beamLeg.startTime < _currentTick.get => // We've missed the bus. This occurs when something takes longer than planned (based on the // initial inquiry). So we replan but change tour mode to WALK_TRANSIT since we've already done our non-transit @@ -1185,7 +1210,7 @@ class PersonAgent( val nextCoord = nextActivity(data).get.getCoord goto(ChoosingMode) using ChoosesModeData( personData = data - .copy(currentTourMode = Some(WALK_TRANSIT), numberOfReplanningAttempts = data.numberOfReplanningAttempts + 1), + .copy(currentTripMode = Some(WALK_TRANSIT), numberOfReplanningAttempts = data.numberOfReplanningAttempts + 1), currentLocation = SpaceTime(currentCoord, _currentTick.get), isWithinTripReplanning = true, excludeModes = @@ -1193,7 +1218,7 @@ class PersonAgent( else Vector(BeamMode.RIDE_HAIL, BeamMode.CAR, BeamMode.CAV) ) // TRANSIT - case Event(StateTimeout, BasePersonData(_, _, nextLeg :: _, _, _, _, _, _, _, _, _, _, _, _)) + case Event(StateTimeout, BasePersonData(_, _, nextLeg :: _, _, _, _, _, _, _, _, _, _, _, _, _)) if nextLeg.beamLeg.mode.isTransit => val resRequest = TransitReservationRequest( nextLeg.beamLeg.travelPath.transitStops.get.fromIdx, @@ -1204,7 +1229,7 @@ class PersonAgent( TransitDriverAgent.selectByVehicleId(nextLeg.beamVehicleId) ! resRequest goto(WaitingForReservationConfirmation) // RIDE_HAIL - case Event(StateTimeout, BasePersonData(_, _, nextLeg :: tailOfCurrentTrip, _, _, _, _, _, _, _, _, _, _, _)) + case Event(StateTimeout, BasePersonData(_, _, nextLeg :: tailOfCurrentTrip, _, _, _, _, _, _, _, _, _, _, _, _)) if nextLeg.isRideHail => val legSegment = nextLeg :: tailOfCurrentTrip.takeWhile(leg => leg.beamVehicleId == nextLeg.beamVehicleId) @@ -1232,7 +1257,7 @@ class PersonAgent( goto(WaitingForReservationConfirmation) // CAV but too late // TODO: Refactor so it uses literally the same code block as transit - case Event(StateTimeout, data @ BasePersonData(_, _, nextLeg :: _, _, _, _, _, _, _, _, _, _, _, _)) + case Event(StateTimeout, data @ BasePersonData(_, _, nextLeg :: _, _, _, _, _, _, _, _, _, _, _, _, _)) if nextLeg.beamLeg.startTime < _currentTick.get => // We've missed the CAV. This occurs when something takes longer than planned (based on the // initial inquiry). So we replan but change tour mode to WALK_TRANSIT since we've already done our non-transit @@ -1247,7 +1272,7 @@ class PersonAgent( val nextCoord = nextActivity(data).get.getCoord goto(ChoosingMode) using ChoosesModeData( personData = data - .copy(currentTourMode = Some(WALK_TRANSIT), numberOfReplanningAttempts = data.numberOfReplanningAttempts + 1), + .copy(currentTripMode = Some(WALK_TRANSIT), numberOfReplanningAttempts = data.numberOfReplanningAttempts + 1), currentLocation = SpaceTime(currentCoord, _currentTick.get), isWithinTripReplanning = true, excludeModes = @@ -1256,7 +1281,7 @@ class PersonAgent( ) // CAV // TODO: Refactor so it uses literally the same code block as transit - case Event(StateTimeout, BasePersonData(_, _, nextLeg :: tailOfCurrentTrip, _, _, _, _, _, _, _, _, _, _, _)) => + case Event(StateTimeout, BasePersonData(_, _, nextLeg :: tailOfCurrentTrip, _, _, _, _, _, _, _, _, _, _, _, _)) => val legSegment = nextLeg :: tailOfCurrentTrip.takeWhile(leg => leg.beamVehicleId == nextLeg.beamVehicleId) val resRequest = ReservationRequest( legSegment.head.beamLeg, @@ -1276,7 +1301,8 @@ class PersonAgent( _, _, _, - currentTourMode @ Some(HOV2_TELEPORTATION | HOV3_TELEPORTATION), + _, + Some(HOV2_TELEPORTATION | HOV3_TELEPORTATION), _, _, _, @@ -1316,7 +1342,8 @@ class PersonAgent( currentTrip = None, restOfCurrentTrip = List(), currentTourPersonalVehicle = None, - currentTourMode = if (activity.getType.equals("Home")) None else currentTourMode, + currentTourMode = if (atHome(activity)) None else data.currentTourMode, + currentTripMode = None, hasDeparted = false ) case None => @@ -1334,6 +1361,7 @@ class PersonAgent( _, _, currentTourMode, + _, currentTourPersonalVehicle, _, _, @@ -1404,7 +1432,7 @@ class PersonAgent( currentTourPersonalVehicle = currentTourPersonalVehicle match { case Some(personalVehId) => val personalVeh = beamVehicles(personalVehId).asInstanceOf[ActualVehicle].vehicle - if (activity.getType.equals("Home")) { + if (atHome(activity)) { potentiallyChargingBeamVehicles.put(personalVeh.id, beamVehicles(personalVeh.id)) beamVehicles -= personalVeh.id personalVeh.getManager.get ! ReleaseVehicle(personalVeh, triggerId) @@ -1415,7 +1443,8 @@ class PersonAgent( case None => None }, - currentTourMode = if (activity.getType.equals("Home")) None else currentTourMode, + currentTourMode = if (atHome(activity)) None else currentTourMode, + currentTripMode = None, hasDeparted = false ) case None => @@ -1489,7 +1518,7 @@ class PersonAgent( } def getReplanningReasonFrom(data: BasePersonData, prefix: String): String = { - data.currentTourMode + data.currentTripMode .collect { case mode => s"$prefix $mode" } @@ -1563,7 +1592,7 @@ class PersonAgent( handleBoardOrAlightOutOfPlace case Event( TriggerWithId(BoardVehicleTrigger(_, vehicleId), triggerId), - BasePersonData(_, _, _, currentVehicle, _, _, _, _, _, _, _, _, _, _) + BasePersonData(_, _, _, currentVehicle, _, _, _, _, _, _, _, _, _, _, _) ) if currentVehicle.nonEmpty && currentVehicle.head.equals(vehicleId) => log.debug("Person {} in state {} received Board for vehicle that he is already on, ignoring...", id, stateName) stay() replying CompletionNotice(triggerId, Vector()) diff --git a/src/main/scala/beam/agentsim/agents/household/HouseholdActor.scala b/src/main/scala/beam/agentsim/agents/household/HouseholdActor.scala index 1c25958ce19..1af1da550b8 100755 --- a/src/main/scala/beam/agentsim/agents/household/HouseholdActor.scala +++ b/src/main/scala/beam/agentsim/agents/household/HouseholdActor.scala @@ -197,8 +197,8 @@ object HouseholdActor { val homeCoordFromPlans = household.members .flatMap(person => person.getSelectedPlan.getPlanElements.asScala.flatMap { - case act: Activity if act.getType == "Home" => Some(act.getCoord) - case _ => None + case act: Activity if PersonAgent.atHome(act) => Some(act.getCoord) + case _ => None } ) .headOption diff --git a/src/main/scala/beam/agentsim/agents/household/HouseholdFleetManager.scala b/src/main/scala/beam/agentsim/agents/household/HouseholdFleetManager.scala index ebd7062290b..a0caffb9bad 100644 --- a/src/main/scala/beam/agentsim/agents/household/HouseholdFleetManager.scala +++ b/src/main/scala/beam/agentsim/agents/household/HouseholdFleetManager.scala @@ -7,6 +7,7 @@ import akka.util.Timeout import beam.agentsim.Resource.NotifyVehicleIdle import beam.agentsim.agents.BeamAgent.Finish import beam.agentsim.agents.InitializeTrigger +import beam.agentsim.agents.PersonAgent.atHome import beam.agentsim.agents.household.HouseholdActor._ import beam.agentsim.agents.household.HouseholdFleetManager.ResolvedParkingResponses import beam.agentsim.agents.modalbehaviors.DrivesVehicle.ActualVehicle @@ -104,7 +105,7 @@ class HouseholdFleetManager( case GetVehicleTypes(triggerId) => sender() ! VehicleTypesResponse(vehicles.values.map(_.beamVehicleType).toSet, triggerId) - case MobilityStatusInquiry(personId, whenWhere, _, requireVehicleCategoryAvailable, triggerId) => + case MobilityStatusInquiry(personId, whenWhere, originActivity, requireVehicleCategoryAvailable, triggerId) => { for { neededVehicleCategory <- requireVehicleCategoryAvailable @@ -157,13 +158,15 @@ class HouseholdFleetManager( } }.getOrElse { availableVehicles = availableVehicles match { - case firstVehicle :: rest => + //in case of replanning because of TRANSIT failure WALK_TRANSIT is used + //but we may want to introduce maxWalkingDistance and check that the agent is close enough to the vehicle + case firstVehicle :: rest if atHome(originActivity) => logger.debug("Vehicle {} is now taken", firstVehicle.id) firstVehicle.becomeDriver(sender) sender() ! MobilityStatusResponse(Vector(ActualVehicle(firstVehicle)), triggerId) rest - case Nil => - logger.debug(s"Not returning vehicle because no default is defined") + case _ => + logger.debug(s"Not returning vehicle because no default is defined or agent is not at home") sender() ! MobilityStatusResponse(Vector(), triggerId) Nil } diff --git a/src/main/scala/beam/agentsim/agents/modalbehaviors/ChoosesMode.scala b/src/main/scala/beam/agentsim/agents/modalbehaviors/ChoosesMode.scala index 3f200e75ee1..9307b6537ab 100755 --- a/src/main/scala/beam/agentsim/agents/modalbehaviors/ChoosesMode.scala +++ b/src/main/scala/beam/agentsim/agents/modalbehaviors/ChoosesMode.scala @@ -128,165 +128,57 @@ trait ChoosesMode { def bodyVehiclePersonId: PersonIdWithActorRef = PersonIdWithActorRef(id, self) - def currentTourBeamVehicle: Option[BeamVehicle] = { - stateData match { - case data: ChoosesModeData => - data.personData.currentTourPersonalVehicle match { - case Some(personalVehicle) => - Option( - beamVehicles(personalVehicle) - .asInstanceOf[ActualVehicle] - .vehicle - ) - case _ => None - } - case data: BasePersonData => - data.currentTourPersonalVehicle match { - case Some(personalVehicle) => - Option( - beamVehicles(personalVehicle) - .asInstanceOf[ActualVehicle] - .vehicle - ) - case _ => None - } - case _ => - None - } - } - onTransition { case _ -> ChoosingMode => + val choosesModeData: ChoosesModeData = nextStateData.asInstanceOf[ChoosesModeData] + val availableModes: Seq[BeamMode] = availableModesForPerson(matsimPlan.getPerson, choosesModeData.excludeModes) + val nextAct = nextActivity(choosesModeData.personData).get + val correctedCurrentTripMode = correctCurrentTripModeAccordingToRules( + choosesModeData.personData.currentTripMode, + choosesModeData.personData, + nextAct, + availableModes + ) nextStateData match { // If I am already on a tour in a vehicle, only that vehicle is available to me - case ChoosesModeData( - BasePersonData(_, _, _, _, _, Some(vehicle), _, _, _, _, _, _, _, _), - _, - _, - _, - _, - _, - _, - _, - _, - _, - _, - _, - _, - _, - _, - _, - _, - _, - _, - _, - _, - _ - ) => - self ! MobilityStatusResponse(Vector(beamVehicles(vehicle)), getCurrentTriggerIdOrGenerate) - // Only need to get available street vehicles if our mode requires such a vehicle - case ChoosesModeData( - BasePersonData( - _, - _, - _, - _, - Some(HOV2_TELEPORTATION | HOV3_TELEPORTATION), - _, - _, - _, - _, - _, - _, - _, - _, - _ - ), - currentLocation, - _, - _, - _, - _, - _, - _, - _, - _, - _, - _, - _, - _, - _, - _, - _, - _, - _, - _, - _, - _ - ) => - val teleportationVehicle = createSharedTeleportationVehicle(currentLocation) + case data: ChoosesModeData + if data.personData.currentTourPersonalVehicle.isDefined && + ( + data.personData.currentTourMode.exists(mode => mode == CAR || mode == BIKE) || + data.personData.currentTourMode.exists(mode => mode == DRIVE_TRANSIT || mode == BIKE_TRANSIT) + && isLastTripWithinTour(data.personData, nextAct) + ) => + self ! MobilityStatusResponse( + Vector(beamVehicles(data.personData.currentTourPersonalVehicle.get)), + getCurrentTriggerIdOrGenerate + ) + // Create teleportation vehicle if we are told to use teleportation + case data: ChoosesModeData if correctedCurrentTripMode.exists(_.isHovTeleportation) => + val teleportationVehicle = createSharedTeleportationVehicle(data.currentLocation) val vehicles = Vector(ActualVehicle(teleportationVehicle)) self ! MobilityStatusResponse(vehicles, getCurrentTriggerIdOrGenerate) // Only need to get available street vehicles if our mode requires such a vehicle - case ChoosesModeData( - BasePersonData( - currentActivityIndex, - _, - _, - _, - plansModeOption @ (None | Some(CAR | BIKE | DRIVE_TRANSIT | BIKE_TRANSIT)), - _, - _, - _, - _, - _, - _, - _, - _, - _ - ), - currentLocation, - _, - _, - _, - _, - _, - _, - _, - _, - _, - _, - _, - _, - _, - _, - _, - _, - _, - _, - _, - _ - ) => + case data: ChoosesModeData if correctedCurrentTripMode.forall(Modes.isPersonalVehicleMode) => implicit val executionContext: ExecutionContext = context.system.dispatcher - plansModeOption match { + correctedCurrentTripMode match { case Some(CAR | DRIVE_TRANSIT) => requestAvailableVehicles( vehicleFleets, - currentLocation, - _experiencedBeamPlan.activities(currentActivityIndex), + data.currentLocation, + currentActivity(data.personData), Some(VehicleCategory.Car) ) pipeTo self case Some(BIKE | BIKE_TRANSIT) => requestAvailableVehicles( vehicleFleets, - currentLocation, - _experiencedBeamPlan.activities(currentActivityIndex), + data.currentLocation, + currentActivity(data.personData), Some(VehicleCategory.Bike) ) pipeTo self case _ => requestAvailableVehicles( vehicleFleets, - currentLocation, - _experiencedBeamPlan.activities(currentActivityIndex) + data.currentLocation, + currentActivity(data.personData) ) pipeTo self } @@ -332,28 +224,18 @@ trait ChoosesMode { case Event(MobilityStatusResponse(newlyAvailableBeamVehicles, triggerId), choosesModeData: ChoosesModeData) => beamVehicles ++= newlyAvailableBeamVehicles.map(v => v.id -> v) val currentPersonLocation = choosesModeData.currentLocation - val availableModes: Seq[BeamMode] = availableModesForPerson( - matsimPlan.getPerson - ).filterNot(mode => choosesModeData.excludeModes.contains(mode)) + val availableModes: Seq[BeamMode] = availableModesForPerson(matsimPlan.getPerson, choosesModeData.excludeModes) + val personData = choosesModeData.personData + val nextAct = nextActivity(personData).get // Make sure the current mode is allowable - val replanningIsAvailable = - choosesModeData.personData.numberOfReplanningAttempts < beamServices.beamConfig.beam.agentsim.agents.modalBehaviors.maximumNumberOfReplanningAttempts - val correctedCurrentTourMode = choosesModeData.personData.currentTourMode match { - case Some(mode @ (HOV2_TELEPORTATION | HOV3_TELEPORTATION)) - if availableModes.contains(CAR) && replanningIsAvailable => - Some(mode) - case Some(mode) if availableModes.contains(mode) && replanningIsAvailable => Some(mode) - case Some(mode) if availableModes.contains(mode) => Some(WALK) - case None if !replanningIsAvailable => Some(WALK) - case _ => None - } + val correctedCurrentTripMode = + correctCurrentTripModeAccordingToRules(personData.currentTripMode, personData, nextAct, availableModes) val bodyStreetVehicle = createBodyStreetVehicle(currentPersonLocation) - val nextAct = nextActivity(choosesModeData.personData).get val departTime = _currentTick.get var availablePersonalStreetVehicles = - correctedCurrentTourMode match { + correctedCurrentTripMode match { case None | Some(CAR | BIKE) => // In these cases, a personal vehicle will be involved, but filter out teleportation vehicles newlyAvailableBeamVehicles.filterNot(v => BeamVehicle.isSharedTeleportationVehicle(v.id)) @@ -361,11 +243,7 @@ trait ChoosesMode { // In these cases, also include teleportation vehicles newlyAvailableBeamVehicles case Some(DRIVE_TRANSIT | BIKE_TRANSIT) => - val tour = _experiencedBeamPlan.getTourContaining(nextAct) - val tripIndexOfElement = tour - .tripIndexOfElement(nextAct) - .getOrElse(throw new IllegalArgumentException(s"Element [$nextAct] not found")) - if (tripIndexOfElement == 0 || tripIndexOfElement == tour.trips.size - 1) { + if (isFirstOrLastTripWithinTour(personData, nextAct)) { newlyAvailableBeamVehicles } else { Vector() @@ -432,7 +310,7 @@ trait ChoosesMode { streetVehicles: Vector[StreetVehicle], byMode: BeamMode ): Vector[StreetVehicle] = { - choosesModeData.personData.currentTourPersonalVehicle match { + personData.currentTourPersonalVehicle match { case Some(personalVeh) => // We already have a vehicle we're using on this tour, so filter down to that streetVehicles.filter(_.id == personalVeh) @@ -448,7 +326,7 @@ trait ChoosesMode { var requestId: Option[Int] = None // Form and send requests - correctedCurrentTourMode match { + correctedCurrentTripMode match { case None => if (hasRideHail) { responsePlaceholders = makeResponsePlaceholders( @@ -477,7 +355,7 @@ trait ChoosesMode { makeRequestWith(withTransit = true, Vector(bodyStreetVehicle)) case Some(CAV) => // Request from household the trip legs to put into trip - householdRef ! CavTripLegsRequest(bodyVehiclePersonId, currentActivity(choosesModeData.personData)) + householdRef ! CavTripLegsRequest(bodyVehiclePersonId, currentActivity(personData)) responsePlaceholders = makeResponsePlaceholders(withPrivateCAV = true) case Some(HOV2_TELEPORTATION) => val vehicles = filterStreetVehiclesForQuery(newlyAvailableBeamVehicles.map(_.streetVehicle), CAR) @@ -529,13 +407,10 @@ trait ChoosesMode { } case Some(mode @ (DRIVE_TRANSIT | BIKE_TRANSIT)) => val vehicleMode = Modes.getAccessVehicleMode(mode) - val LastTripIndex = currentTour(choosesModeData.personData).trips.size - 1 - val tripIndexOfElement = currentTour(choosesModeData.personData) - .tripIndexOfElement(nextAct) - .getOrElse(throw new IllegalArgumentException(s"Element [$nextAct] not found")) + val (tripIndexOfElement: Int, lastTripIndex: Int) = currentTripIndexWithinTour(personData, nextAct) ( tripIndexOfElement, - choosesModeData.personData.currentTourPersonalVehicle + personData.currentTourPersonalVehicle ) match { case (0, _) if !choosesModeData.isWithinTripReplanning => // We use our car if we are not replanning, otherwise we end up doing a walk transit (catch-all below) @@ -547,7 +422,7 @@ trait ChoosesMode { :+ bodyStreetVehicle ) responsePlaceholders = makeResponsePlaceholders(withRouting = true) - case (LastTripIndex, Some(currentTourPersonalVehicle)) => + case (`lastTripIndex`, Some(currentTourPersonalVehicle)) => // At the end of the tour, only drive home a vehicle that we have also taken away from there. makeRequestWith( withTransit = true, @@ -583,7 +458,7 @@ trait ChoosesMode { logDebug(m.toString) } val newPersonData = choosesModeData.copy( - personData = choosesModeData.personData.copy(currentTourMode = correctedCurrentTourMode), + personData = personData.copy(currentTripMode = correctedCurrentTripMode), availablePersonalStreetVehicles = availablePersonalStreetVehicles, allAvailableStreetVehicles = newlyAvailableBeamVehicles, routingResponse = responsePlaceholders.routingResponse, @@ -820,6 +695,32 @@ trait ChoosesMode { ) } using completeChoiceIfReady) + private def correctCurrentTripModeAccordingToRules( + currentTripMode: Option[BeamMode], + personData: BasePersonData, + nextAct: Activity, + availableModes: Seq[BeamMode] + ): Option[BeamMode] = { + val replanningIsAvailable = + personData.numberOfReplanningAttempts < beamServices.beamConfig.beam.agentsim.agents.modalBehaviors.maximumNumberOfReplanningAttempts + (currentTripMode, personData.currentTourMode) match { + case (_, Some(CAR | BIKE)) if personData.currentTourPersonalVehicle.isDefined => personData.currentTourMode + case (_, Some(DRIVE_TRANSIT | BIKE_TRANSIT)) + if personData.currentTourPersonalVehicle.isDefined && isLastTripWithinTour(personData, nextAct) => + personData.currentTourMode + case (Some(mode @ (HOV2_TELEPORTATION | HOV3_TELEPORTATION)), _) + if availableModes.contains(CAR) && replanningIsAvailable => + Some(mode) + case (Some(CAR_HOV2 | CAR_HOV3), Some(tourMode @ (HOV2_TELEPORTATION | HOV3_TELEPORTATION))) + if availableModes.contains(CAR) && replanningIsAvailable => + Some(tourMode) + case (Some(mode), _) if availableModes.contains(mode) && replanningIsAvailable => Some(mode) + case (Some(mode), _) if availableModes.contains(mode) => Some(WALK) + case (None, _) if !replanningIsAvailable => Some(WALK) + case _ => None + } + } + private def makeParkingInquiries( choosesModeData: ChoosesModeData, itineraries: Seq[EmbodiedBeamTrip] @@ -1110,7 +1011,7 @@ trait ChoosesMode { ): Seq[EmbodiedBeamTrip] = { itineraries.map { itin => itin.tripClassifier match { - case CAR | DRIVE_TRANSIT | BIKE_TRANSIT | BIKE => + case mode if Modes.isPersonalVehicleMode(mode) => // find parking legs (the subsequent leg of the same vehicle) val parkingLegs = itin.legs.zip(itin.legs.tail).collect { case (leg1, leg2) if leg1.beamVehicleId == leg2.beamVehicleId && legVehicleHasParkingBehavior(leg2) => leg2 @@ -1203,7 +1104,7 @@ trait ChoosesMode { ) <= beamScenario.beamConfig.beam.agentsim.agents.rideHail.allocationManager.maxWaitingTimeInSec => val origLegs = travelProposal.toEmbodiedBeamLegsForCustomer(bodyVehiclePersonId) (travelProposal.poolingInfo match { - case Some(poolingInfo) if !choosesModeData.personData.currentTourMode.contains(RIDE_HAIL) => + case Some(poolingInfo) if !choosesModeData.personData.currentTripMode.contains(RIDE_HAIL) => val pooledLegs = origLegs.map { origLeg => origLeg.copy( cost = origLeg.cost * poolingInfo.costFactor, @@ -1245,7 +1146,7 @@ trait ChoosesMode { def isAvailable(mode: BeamMode): Boolean = combinedItinerariesForChoice.exists(_.tripClassifier == mode) - choosesModeData.personData.currentTourMode match { + choosesModeData.personData.currentTripMode match { case Some(expectedMode) if expectedMode.isTransit && !isAvailable(expectedMode) => eventsManager.processEvent( createFailedTransitODSkimmerEvent(currentPersonLocation.loc, nextAct.getCoord, expectedMode) @@ -1253,20 +1154,13 @@ trait ChoosesMode { case _ => } - val availableModesForTrips: Seq[BeamMode] = availableModesForPerson(matsimPlan.getPerson) - .filterNot(mode => choosesModeData.excludeModes.contains(mode)) + val availableModesForTrips: Seq[BeamMode] = + availableModesForPerson(matsimPlan.getPerson, choosesModeData.excludeModes) - val filteredItinerariesForChoice = (choosesModeData.personData.currentTourMode match { + val filteredItinerariesForChoice = (choosesModeData.personData.currentTripMode match { case Some(mode) if mode == DRIVE_TRANSIT || mode == BIKE_TRANSIT => - val LastTripIndex = currentTour(choosesModeData.personData).trips.size - 1 - val tripIndexOfElement = currentTour(choosesModeData.personData) - .tripIndexOfElement(nextAct) - .getOrElse(throw new IllegalArgumentException(s"Element [$nextAct] not found")) - ( - tripIndexOfElement, - personData.hasDeparted - ) match { - case (0 | LastTripIndex, false) => + (isFirstOrLastTripWithinTour(personData, nextAct), personData.hasDeparted) match { + case (true, false) => combinedItinerariesForChoice.filter(_.tripClassifier == mode) case _ => combinedItinerariesForChoice.filter(trip => @@ -1293,6 +1187,13 @@ trait ChoosesMode { .asInstanceOf[AttributesOfIndividual] val availableAlts = Some(filteredItinerariesForChoice.map(_.tripClassifier).mkString(":")) + def gotoFinishingModeChoice(chosenTrip: EmbodiedBeamTrip) = { + goto(FinishingModeChoice) using choosesModeData.copy( + pendingChosenTrip = Some(chosenTrip), + availableAlternatives = availableAlts + ) + } + modeChoiceCalculator( filteredItinerariesForChoice, attributesOfIndividual, @@ -1300,13 +1201,9 @@ trait ChoosesMode { Some(matsimPlan.getPerson) ) match { case Some(chosenTrip) => - val dataForNextStep = choosesModeData.copy( - pendingChosenTrip = Some(chosenTrip), - availableAlternatives = availableAlts - ) - goto(FinishingModeChoice) using dataForNextStep + gotoFinishingModeChoice(chosenTrip) case None => - choosesModeData.personData.currentTourMode match { + choosesModeData.personData.currentTripMode match { case Some(CAV) => // Special case, if you are using household CAV, no choice was necessary you just use this mode // Construct the embodied trip to allow for processing by FinishingModeChoice and scoring @@ -1328,10 +1225,7 @@ trait ChoosesMode { body.beamVehicleType.id ) val cavTrip = EmbodiedBeamTrip(walk1 +: cavTripLegs.legs.toVector :+ walk2) - goto(FinishingModeChoice) using choosesModeData.copy( - pendingChosenTrip = Some(cavTrip), - availableAlternatives = availableAlts - ) + gotoFinishingModeChoice(cavTrip) } else { val bushwhackingTrip = RoutingWorker.createBushwackingTrip( choosesModeData.currentLocation.loc, @@ -1340,49 +1234,61 @@ trait ChoosesMode { body.toStreetVehicle, geo ) - goto(FinishingModeChoice) using choosesModeData.copy( - pendingChosenTrip = Some(bushwhackingTrip), - availableAlternatives = availableAlts - ) + gotoFinishingModeChoice(bushwhackingTrip) } case Some(_) => - //give another chance to make a choice without predefined mode - self ! MobilityStatusResponse(choosesModeData.allAvailableStreetVehicles, getCurrentTriggerId.get) - stay() using ChoosesModeData( - personData = personData.copy(currentTourMode = None), - currentLocation = choosesModeData.currentLocation, - excludeModes = choosesModeData.excludeModes - ) + val correctedTripMode = + correctCurrentTripModeAccordingToRules(None, personData, nextAct, availableModesForTrips) + if (correctedTripMode != personData.currentTripMode) { + //give another chance to make a choice without predefined mode + gotoChoosingModeWithoutPredefinedMode(choosesModeData) + } else { + val expensiveWalkTrip = createExpensiveWalkTrip(currentPersonLocation, nextAct, routingResponse) + gotoFinishingModeChoice(expensiveWalkTrip) + } case _ => // Bad things happen but we want them to continue their day, so we signal to downstream that trip should be made to be expensive - val originalWalkTripLeg = - routingResponse.itineraries.find(_.tripClassifier == WALK) match { - case Some(originalWalkTrip) => - originalWalkTrip.legs.head - case None => - RoutingWorker - .createBushwackingTrip( - currentPersonLocation.loc, - nextAct.getCoord, - _currentTick.get, - body.toStreetVehicle, - beamServices.geo - ) - .legs - .head - } - val expensiveWalkTrip = EmbodiedBeamTrip( - Vector(originalWalkTripLeg.copy(replanningPenalty = 10.0)) - ) - - goto(FinishingModeChoice) using choosesModeData.copy( - pendingChosenTrip = Some(expensiveWalkTrip), - availableAlternatives = availableAlts - ) + val expensiveWalkTrip = createExpensiveWalkTrip(currentPersonLocation, nextAct, routingResponse) + gotoFinishingModeChoice(expensiveWalkTrip) } } } + private def createExpensiveWalkTrip( + currentPersonLocation: SpaceTime, + nextAct: Activity, + routingResponse: RoutingResponse + ) = { + val originalWalkTripLeg = + routingResponse.itineraries.find(_.tripClassifier == WALK) match { + case Some(originalWalkTrip) => + originalWalkTrip.legs.head + case None => + RoutingWorker + .createBushwackingTrip( + currentPersonLocation.loc, + nextAct.getCoord, + _currentTick.get, + body.toStreetVehicle, + beamServices.geo + ) + .legs + .head + } + val expensiveWalkTrip = EmbodiedBeamTrip( + Vector(originalWalkTripLeg.copy(replanningPenalty = 10.0)) + ) + expensiveWalkTrip + } + + private def gotoChoosingModeWithoutPredefinedMode(choosesModeData: ChoosesModeData) = { + goto(ChoosingMode) using choosesModeData.copy( + personData = choosesModeData.personData.copy(currentTripMode = None), + currentLocation = choosesModeData.currentLocation, + excludeModes = choosesModeData.excludeModes + ) + } + private def createFailedTransitODSkimmerEvent( originLocation: Location, destinationLocation: Location, @@ -1495,7 +1401,7 @@ trait ChoosesMode { tick, id, chosenTrip.tripClassifier.value, - data.personData.currentTourMode.map(_.value).getOrElse(""), + data.personData.currentTripMode.map(_.value).getOrElse(""), data.expectedMaxUtilityOfLatestChoice.getOrElse[Double](Double.NaN), _experiencedBeamPlan.activities(data.personData.currentActivityIndex).getLinkId.toString, data.availableAlternatives.get, @@ -1509,7 +1415,7 @@ trait ChoosesMode { ) eventsManager.processEvent(modeChoiceEvent) - data.personData.currentTourMode match { + data.personData.currentTripMode match { case Some(HOV2_TELEPORTATION | HOV3_TELEPORTATION) => scheduler ! CompletionNotice( triggerId, @@ -1523,6 +1429,7 @@ trait ChoosesMode { goto(Teleporting) using data.personData.copy( currentTrip = Some(chosenTrip), + currentTourMode = data.personData.currentTripMode, restOfCurrentTrip = List() ) @@ -1556,8 +1463,8 @@ trait ChoosesMode { goto(WaitingForDeparture) using data.personData.copy( currentTrip = Some(chosenTrip), restOfCurrentTrip = chosenTrip.legs.toList, - currentTourMode = data.personData.currentTourMode - .orElse(Some(chosenTrip.tripClassifier)), + currentTourMode = data.personData.currentTourMode.orElse(Some(chosenTrip.tripClassifier)), + currentTripMode = data.personData.currentTripMode.orElse(Some(chosenTrip.tripClassifier)), currentTourPersonalVehicle = if (isCurrentPersonalVehicleVoided) vehiclesUsed.headOption.filter(mustBeDrivenHome).map(_.id) diff --git a/src/main/scala/beam/agentsim/agents/modalbehaviors/DrivesVehicle.scala b/src/main/scala/beam/agentsim/agents/modalbehaviors/DrivesVehicle.scala index ef633a7bdb7..dc19f806e0b 100755 --- a/src/main/scala/beam/agentsim/agents/modalbehaviors/DrivesVehicle.scala +++ b/src/main/scala/beam/agentsim/agents/modalbehaviors/DrivesVehicle.scala @@ -222,7 +222,7 @@ trait DrivesVehicle[T <: DrivingData] extends BeamAgent[T] with Stash with Expon case Event( TriggerWithId(EndLegTrigger(tick), triggerId), LiterallyDrivingData(data: BasePersonData, _, _) - ) if data.currentTourMode.contains(HOV2_TELEPORTATION) || data.currentTourMode.contains(HOV3_TELEPORTATION) => + ) if data.currentTripMode.contains(HOV2_TELEPORTATION) || data.currentTripMode.contains(HOV3_TELEPORTATION) => updateLatestObservedTick(tick) val dataForNextLegOrActivity: BasePersonData = data.copy( @@ -331,7 +331,6 @@ trait DrivesVehicle[T <: DrivingData] extends BeamAgent[T] with Stash with Expon } val numberOfPassengers: Int = calculateNumberOfPassengersBasedOnCurrentTourMode(data, currentLeg, riders) - val currentTourMode: Option[String] = getCurrentTourMode(data) val pte = PathTraversalEvent( tick, currentVehicleUnderControl, @@ -339,7 +338,7 @@ trait DrivesVehicle[T <: DrivingData] extends BeamAgent[T] with Stash with Expon currentBeamVehicle.beamVehicleType, numberOfPassengers, currentLeg, - currentTourMode, + getCurrentTripMode(data), fuelConsumed.primaryFuel, fuelConsumed.secondaryFuel, currentBeamVehicle.primaryFuelLevelInJoules, @@ -489,16 +488,8 @@ trait DrivesVehicle[T <: DrivingData] extends BeamAgent[T] with Stash with Expon stay() } - private def getCurrentTourMode(data: DrivingData): Option[String] = { - data match { - case bpd: BasePersonData => - bpd.currentTourMode match { - case Some(mode: BeamMode) => Some(mode.value) - case _ => None - } - case _ => None - } - } + private def getCurrentTripMode(data: DrivingData): Option[String] = + findPersonData(data).flatMap(_.currentTripMode).map(_.value) private def calculateNumberOfPassengersBasedOnCurrentTourMode( data: DrivingData, @@ -507,7 +498,7 @@ trait DrivesVehicle[T <: DrivingData] extends BeamAgent[T] with Stash with Expon ): Int = { val numberOfPassengers = data match { case bpd: BasePersonData => - (bpd.currentTourMode, currentLeg.mode) match { + (bpd.currentTripMode, currentLeg.mode) match { // can't directly check HOV2/3 because the equals in BeamMode is overridden case (Some(mode @ BeamMode.CAR), BeamMode.CAR) if mode.value == BeamMode.CAR_HOV2.value => riders.size + 1 case (Some(mode @ BeamMode.CAR), BeamMode.CAR) if mode.value == BeamMode.CAR_HOV3.value => riders.size + 2 @@ -547,7 +538,6 @@ trait DrivesVehicle[T <: DrivingData] extends BeamAgent[T] with Stash with Expon tollsAccumulated += tollOnCurrentLeg val numberOfPassengers: Int = calculateNumberOfPassengersBasedOnCurrentTourMode(data, partiallyCompletedBeamLeg, riders) - val currentTourMode: Option[String] = getCurrentTourMode(data) val pte = PathTraversalEvent( updatedStopTick, currentVehicleUnderControl, @@ -555,7 +545,7 @@ trait DrivesVehicle[T <: DrivingData] extends BeamAgent[T] with Stash with Expon currentBeamVehicle.beamVehicleType, numberOfPassengers, partiallyCompletedBeamLeg, - currentTourMode, + getCurrentTripMode(data), fuelConsumed.primaryFuel, fuelConsumed.secondaryFuel, currentBeamVehicle.primaryFuelLevelInJoules, diff --git a/src/main/scala/beam/router/Modes.scala b/src/main/scala/beam/router/Modes.scala index aa73937123f..c610f65a285 100755 --- a/src/main/scala/beam/router/Modes.scala +++ b/src/main/scala/beam/router/Modes.scala @@ -34,6 +34,7 @@ object Modes { def isTransit: Boolean = isR5TransitMode(this) def isMassTransit: Boolean = this == SUBWAY || this == RAIL || this == FERRY || this == TRAM def isRideHail: Boolean = this == RIDE_HAIL + def isHovTeleportation: Boolean = this == HOV2_TELEPORTATION || this == HOV3_TELEPORTATION } object BeamMode extends StringEnum[BeamMode] with StringCirceEnum[BeamMode] { @@ -124,6 +125,8 @@ object Modes { val chainBasedModes = Seq(CAR, BIKE) + val personalVehicleModes = Seq(CAR, BIKE, DRIVE_TRANSIT, BIKE_TRANSIT) + val transitModes = Seq(BUS, FUNICULAR, GONDOLA, CABLE_CAR, FERRY, TRAM, TRANSIT, RAIL, SUBWAY) @@ -161,6 +164,8 @@ object Modes { def isChainBasedMode(beamMode: BeamMode): Boolean = BeamMode.chainBasedModes.contains(beamMode) + def isPersonalVehicleMode(beamMode: BeamMode): Boolean = BeamMode.personalVehicleModes.contains(beamMode) + implicit def beamMode2R5Mode(beamMode: BeamMode): Either[LegMode, TransitModes] = beamMode.r5Mode.get diff --git a/src/main/scala/beam/utils/plan/sampling/AvailableModeUtils.scala b/src/main/scala/beam/utils/plan/sampling/AvailableModeUtils.scala index 74e96c718aa..6218ba63057 100644 --- a/src/main/scala/beam/utils/plan/sampling/AvailableModeUtils.scala +++ b/src/main/scala/beam/utils/plan/sampling/AvailableModeUtils.scala @@ -60,6 +60,10 @@ object AvailableModeUtils extends LazyLogging { getPersonCustomAttributes(person).map(_.availableModes).getOrElse(Seq.empty) } + def availableModesForPerson(person: Person, excludedModes: Seq[BeamMode]): Seq[BeamMode] = { + getPersonCustomAttributes(person).map(_.availableModes).getOrElse(Seq.empty).filterNot(excludedModes.contains) + } + /** * Sets the available modes for the given person in the population * diff --git a/src/test/scala/beam/integration/SingleModeSpec.scala b/src/test/scala/beam/integration/SingleModeSpec.scala index f29744f7c47..0764c304fbe 100755 --- a/src/test/scala/beam/integration/SingleModeSpec.scala +++ b/src/test/scala/beam/integration/SingleModeSpec.scala @@ -11,7 +11,7 @@ import beam.router.RouteHistory import beam.sflight.RouterForTest import beam.sim.common.GeoUtilsImpl import beam.sim.{BeamHelper, BeamMobsim, RideHailFleetInitializerProvider} -import beam.utils.SimRunnerForTest +import beam.utils.{MathUtils, SimRunnerForTest} import beam.utils.TestConfigUtils.testConfig import com.typesafe.config.ConfigFactory import org.matsim.api.core.v01.events.{ActivityEndEvent, Event, PersonDepartureEvent, PersonEntersVehicleEvent} @@ -269,8 +269,10 @@ class SingleModeSpec val personDepartureEvents = events.collect { case event: PersonDepartureEvent => event } personDepartureEvents should not be empty val regularPersonEvents = filterOutProfessionalDriversAndCavs(personDepartureEvents) - val (drive, others) = regularPersonEvents.map(_.getLegMode).partition(_ == "car") - others.size should be < (0.02 * drive.size).toInt + val othersCount = regularPersonEvents.count(_.getLegMode != "car") + withClue("Majority of agents should use cars. Other modes take place when no car available.") { + othersCount should be < MathUtils.doubleToInt(0.02 * regularPersonEvents.size) + } } }