diff --git a/aws/src/main/python/beam_lambda/lambda_function.py b/aws/src/main/python/beam_lambda/lambda_function.py index 16441940e75..ad4c820bcd1 100755 --- a/aws/src/main/python/beam_lambda/lambda_function.py +++ b/aws/src/main/python/beam_lambda/lambda_function.py @@ -169,7 +169,6 @@ fi exit 0; path: /home/ubuntu/check_simulation_result.sh - runcmd: - sudo chmod +x /home/ubuntu/install-and-run-helics-scripts.sh - sudo chmod +x /home/ubuntu/write-cpu-ram-usage.sh @@ -192,7 +191,6 @@ - RESOLVED_COMMIT=$COMMIT - fi - echo "Resolved commit is $RESOLVED_COMMIT" - - 'echo "sudo git fetch"' - sudo git fetch - 'echo "GIT_LFS_SKIP_SMUDGE=1 sudo git checkout $BRANCH $(date)"' @@ -203,7 +201,6 @@ - sudo git lfs pull - echo "sudo git checkout -qf ..." - GIT_LFS_SKIP_SMUDGE=1 sudo git checkout -qf $COMMIT - - production_data_submodules=$(git submodule | awk '{ print $2 }') - for i in $production_data_submodules - do @@ -227,19 +224,17 @@ - esac - done - done - - if [ "$RUN_JUPYTER" = "True" ] - then - echo "Starting Jupyter" - sudo ./gradlew jupyterStart -Puser=root -PjupyterToken=$JUPYTER_TOKEN - fi - + - if [ "$RUN_BEAM" = "True" ] - then - - echo "-------------------Starting Beam Sim----------------------" - echo $(date +%s) > /tmp/.starttime - - rm -rf /home/ubuntu/git/beam/test/input/sf-light/r5/network.dat + - rm -rf /home/ubuntu/git/beam/test/input/sf-light/r5/network.dat - hello_msg=$(printf "Run Started \\n Run Name** $TITLED** \\n Instance ID %s \\n Instance type **%s** \\n Host name **%s** \\n Web browser ** http://%s:8000 ** \\n Region $REGION \\n Batch $UID \\n Branch **$BRANCH** \\n Commit $COMMIT" $(ec2metadata --instance-id) $(ec2metadata --instance-type) $(ec2metadata --public-hostname) $(ec2metadata --public-hostname)) - start_json=$(printf "{ \\"command\\":\\"add\\", @@ -274,7 +269,6 @@ - crontab /tmp/cron_jobs - crontab -l - echo "notification scheduled..." - - 'echo "gradlew assemble: $(date)"' - ./gradlew assemble - 'echo "sudo chown -R ubuntu:ubuntu ."' @@ -286,7 +280,6 @@ - export GOOGLE_API_KEY="$GOOGLE_API_KEY" - echo $MAXRAM - /tmp/slack.sh "$hello_msg" - - s3p="" - for cf in $CONFIG - do @@ -299,7 +292,7 @@ - do - export $metric=$count - done < RunHealthAnalysis.txt - + - curl -H "Authorization:Bearer $SLACK_TOKEN" -F file=@RunHealthAnalysis.txt -F initial_comment="Beam Health Analysis" -F channels="$SLACK_CHANNEL" "https://slack.com/api/files.upload" - s3glip="" - if [ "$S3_PUBLISH" = "True" ] @@ -956,4 +949,4 @@ def lambda_handler(event, context): if command_id in instance_operations: return instance_handler(event) - return "Operation {command} not supported, please specify one of the supported operations (deploy | start | stop | terminate | log). ".format(command=command_id) + return "Operation {command} not supported, please specify one of the supported operations (deploy | start | stop | terminate | log). ".format(command=command_id) \ No newline at end of file diff --git a/build.gradle b/build.gradle index 043eea43513..d948c6baad7 100755 --- a/build.gradle +++ b/build.gradle @@ -720,4 +720,4 @@ jmh { jvmArgs = ['-Djmh.separateClasspathJAR=true', '-Dbeam.home=' + project.projectDir] duplicateClassesStrategy = 'exclude' zip64 = true -} +} \ No newline at end of file diff --git a/docs/inputs.rst b/docs/inputs.rst index 5ae57e3e393..06dc4bfd891 100755 --- a/docs/inputs.rst +++ b/docs/inputs.rst @@ -238,17 +238,17 @@ Routing Configuration departureWindow = "double | 15.0" numberOfSamples = "int | 1" osmMapdbFile = ${beam.routing.r5.directory}"/osm.mapdb" - mNetBuilder.fromCRS = "EPSG:4326" # WGS84 - mNetBuilder.toCRS = "EPSG:26910" # UTM10N - travelTimeNoiseFraction = 0.0 - maxDistanceLimitByModeInMeters { - bike = 40000 - } - bikeLaneScaleFactor = 1.0 - bikeLaneLinkIdsFilePath = "" - } - startingIterationForTravelTimesMSA = 0 - overrideNetworkTravelTimesUsingSkims = false + mNetBuilder.fromCRS = "EPSG:4326" # WGS84 + mNetBuilder.toCRS = "EPSG:26910" # UTM10N + travelTimeNoiseFraction = 0.0 + maxDistanceLimitByModeInMeters { + bike = 40000 + } + bikeLaneScaleFactor = 1.0 + bikeLaneLinkIdsFilePath = "" + } + startingIterationForTravelTimesMSA = 0 + overrideNetworkTravelTimesUsingSkims = false # Set a lower bound on travel times that can possibly be used to override the network-based # travel time in the route.This is used to prevent unrealistically fast trips or negative diff --git a/production/sfbay b/production/sfbay index 786b20833d3..87ccd9b0303 160000 --- a/production/sfbay +++ b/production/sfbay @@ -1 +1 @@ -Subproject commit 786b20833d34d1dfb57cff531717c31e7ea43e8c +Subproject commit 87ccd9b03035c947a94a8e417f80b496c853238d diff --git a/src/main/scala/beam/agentsim/agents/PersonAgent.scala b/src/main/scala/beam/agentsim/agents/PersonAgent.scala index f40742a837b..90cb514fce6 100755 --- a/src/main/scala/beam/agentsim/agents/PersonAgent.scala +++ b/src/main/scala/beam/agentsim/agents/PersonAgent.scala @@ -5,6 +5,7 @@ import akka.actor.{ActorRef, FSM, Props, Stash, Status} import beam.agentsim.Resource._ import beam.agentsim.agents.BeamAgent._ import beam.agentsim.agents.PersonAgent._ +import beam.agentsim.agents.choice.mode.TourModeChoiceMultinomialLogit import beam.agentsim.agents.freight.input.FreightReader.PAYLOAD_WEIGHT_IN_KG import beam.agentsim.agents.household.HouseholdActor.ReleaseVehicle import beam.agentsim.agents.household.HouseholdCAVDriverAgent @@ -17,6 +18,8 @@ import beam.agentsim.agents.parking.ChoosesParking.{ ChoosingParkingSpot, ReleasingParkingSpot } +import beam.agentsim.agents.planning.BeamPlan.atHome +import beam.agentsim.agents.planning.Strategy.{TourModeChoiceStrategy, TripModeChoiceStrategy} import beam.agentsim.agents.planning.{BeamPlan, Tour} import beam.agentsim.agents.ridehail.RideHailManager.TravelProposal import beam.agentsim.agents.ridehail._ @@ -34,6 +37,7 @@ import beam.agentsim.scheduler.BeamAgentScheduler.{CompletionNotice, IllegalTrig import beam.agentsim.scheduler.Trigger.TriggerWithId import beam.agentsim.scheduler.{BeamAgentSchedulerTimer, Trigger} import beam.router.Modes.BeamMode +import beam.router.TourModes.BeamTourMode import beam.router.Modes.BeamMode.{ CAR, CAV, @@ -84,6 +88,7 @@ object PersonAgent { services: BeamServices, beamScenario: BeamScenario, modeChoiceCalculator: ModeChoiceCalculator, + tourModeChoiceCalculator: TourModeChoiceMultinomialLogit, transportNetwork: TransportNetwork, tollCalculator: TollCalculator, router: ActorRef, @@ -105,6 +110,7 @@ object PersonAgent { services, beamScenario, modeChoiceCalculator, + tourModeChoiceCalculator, transportNetwork, router, rideHailManager, @@ -188,8 +194,9 @@ object PersonAgent { currentTrip: Option[EmbodiedBeamTrip] = None, restOfCurrentTrip: List[EmbodiedBeamLeg] = List(), currentVehicle: VehicleStack = Vector(), - currentTourMode: Option[BeamMode] = None, - currentTourPersonalVehicle: Option[Id[BeamVehicle]] = None, + currentTripMode: Option[BeamMode] = None, // We might not need this here any more if it's kept in the plan + currentTourMode: Option[BeamTourMode] = None, // "" + currentTourPersonalVehicle: Option[Id[BeamVehicle]] = None, // "" passengerSchedule: PassengerSchedule = PassengerSchedule(), currentLegPassengerScheduleIndex: Int = 0, hasDeparted: Boolean = false, @@ -292,6 +299,7 @@ class PersonAgent( val beamServices: BeamServices, val beamScenario: BeamScenario, val modeChoiceCalculator: ModeChoiceCalculator, + val tourModeChoiceCalculator: TourModeChoiceMultinomialLogit, val transportNetwork: TransportNetwork, val router: ActorRef, val rideHailManager: ActorRef, @@ -406,7 +414,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 @@ -469,6 +477,30 @@ class PersonAgent( } } + def isFirstTripWithinTour(nextAct: Activity): Boolean = { + val (tripIndexOfElement: Int, _) = currentTripIndexWithinTour(nextAct) + tripIndexOfElement == 0 + } + + def isLastTripWithinTour(nextAct: Activity): Boolean = { + val (tripIndexOfElement: Int, lastTripIndex: Int) = currentTripIndexWithinTour(nextAct) + tripIndexOfElement == lastTripIndex + } + + def isFirstOrLastTripWithinTour(nextAct: Activity): Boolean = { + val (tripIndexOfElement: Int, lastTripIndex: Int) = currentTripIndexWithinTour(nextAct) + tripIndexOfElement == 0 || tripIndexOfElement == lastTripIndex + } + + def currentTripIndexWithinTour(nextAct: Activity): (Int, Int) = { + val tour = _experiencedBeamPlan.getTourContaining(nextAct) + 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) @@ -571,24 +603,17 @@ class PersonAgent( case Some(nextAct) => logDebug(s"wants to go to ${nextAct.getType} @ $tick") holdTickAndTriggerId(tick, triggerId) - val indexOfNextActivity = _experiencedBeamPlan.getPlanElements.indexOf(nextAct) - val modeOfNextLeg = _experiencedBeamPlan.getPlanElements.get(indexOfNextActivity - 1) match { - case leg: Leg => BeamMode.fromString(leg.getMode) - case _ => None - } + val modeOfNextLeg = _experiencedBeamPlan.getTripStrategy[TripModeChoiceStrategy](nextAct).mode + val currentTourModeChoiceStrategy = _experiencedBeamPlan.getTourStrategy[TourModeChoiceStrategy](nextAct) val currentCoord = currentActivity(data).getCoord 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 current tour mode is defined in _experiencedBeamPlan.getTourStrategy + // If we have the currentTourPersonalVehicle then we should use it + // use the mode of the next leg as the new trip mode. + currentTripMode = modeOfNextLeg, + currentTourMode = None, // currentTourModeChoiceStrategy.tourMode, NOTE: Not sure why this is needed... numberOfReplanningAttempts = 0, failedTrips = IndexedSeq.empty, enrouteData = EnrouteData() @@ -605,7 +630,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) @@ -619,7 +644,7 @@ class PersonAgent( case Event( TriggerWithId(TeleportationEndsTrigger(tick), triggerId), - data @ BasePersonData(_, Some(currentTrip), _, _, maybeCurrentTourMode, _, _, _, true, _, _, _, _, _) + data @ BasePersonData(_, Some(currentTrip), _, _, maybeCurrentTripMode, _, _, _, _, true, _, _, _, _, _) ) => holdTickAndTriggerId(tick, triggerId) @@ -632,7 +657,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) + currentTripMode = data.currentTripMode.map(_.value) ) eventsManager.processEvent(teleportationEvent) @@ -651,7 +676,7 @@ class PersonAgent( */ case Event( TriggerWithId(PersonDepartureTrigger(tick), triggerId), - data @ BasePersonData(_, Some(currentTrip), _, _, _, _, _, _, false, _, _, _, _, _) + data @ BasePersonData(_, Some(currentTrip), _, _, _, _, _, _, _, false, _, _, _, _, _) ) => endActivityAndDepart(tick, currentTrip, data) @@ -660,7 +685,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}") @@ -711,7 +736,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 @@ -730,7 +755,7 @@ class PersonAgent( // TRANSIT FAILURE case Event( ReservationResponse(Left(firstErrorResponse), _), - data @ BasePersonData(_, _, nextLeg :: _, _, _, _, _, _, _, _, _, _, _, _) + data @ BasePersonData(_, _, nextLeg :: _, _, _, _, _, _, _, _, _, _, _, _, _) ) => logDebug(s"replanning because ${firstErrorResponse.errorCode}") @@ -804,7 +829,7 @@ class PersonAgent( // RIDE HAIL FAILURE case Event( response @ RideHailResponse(_, _, Some(error), _, _), - data @ BasePersonData(_, _, _, _, _, _, _, _, _, _, _, _, _, _) + data: BasePersonData ) => handleFailedRideHailReservation(error, response, data) } @@ -815,7 +840,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)) @@ -848,7 +873,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") @@ -925,6 +950,7 @@ class PersonAgent( _, _, _, + _, currentCost, _, _, @@ -965,10 +991,16 @@ class PersonAgent( ) val currentCoord = beamServices.geo.wgs2Utm(basePersonData.restOfCurrentTrip.head.beamLeg.travelPath.startPoint).loc - val nextCoord = nextActivity(basePersonData).get.getCoord + val nextAct = nextActivity(basePersonData).get + val nextCoord = nextAct.getCoord + // Have to give up my mode as well, perhaps there's no option left for driving. + _experiencedBeamPlan.putStrategy(nextAct, TripModeChoiceStrategy(mode = None)) + val updatedTourMode: Option[BeamTourMode] = if (isFirstOrLastTripWithinTour(nextAct)) { None } + else { basePersonData.currentTourMode } 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, + currentTourMode = updatedTourMode, currentTourPersonalVehicle = None, numberOfReplanningAttempts = basePersonData.numberOfReplanningAttempts + 1 ), @@ -1065,6 +1097,7 @@ class PersonAgent( _, _, _, + _, _ ) ) if nextLeg.asDriver => @@ -1158,7 +1191,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 @@ -1173,7 +1206,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 = @@ -1181,7 +1214,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, @@ -1192,7 +1225,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) @@ -1220,7 +1253,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 @@ -1235,7 +1268,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 = @@ -1244,7 +1277,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, @@ -1264,7 +1297,8 @@ class PersonAgent( _, _, _, - currentTourMode @ Some(HOV2_TELEPORTATION | HOV3_TELEPORTATION), + Some(HOV2_TELEPORTATION | HOV3_TELEPORTATION), + _, _, _, _, @@ -1304,7 +1338,7 @@ class PersonAgent( currentTrip = None, restOfCurrentTrip = List(), currentTourPersonalVehicle = None, - currentTourMode = if (activity.getType.equals("Home")) None else currentTourMode, + currentTripMode = None, hasDeparted = false ) case None => @@ -1321,7 +1355,8 @@ class PersonAgent( Some(currentTrip), _, _, - currentTourMode, + _, + _, currentTourPersonalVehicle, _, _, @@ -1406,7 +1441,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) @@ -1417,7 +1452,7 @@ class PersonAgent( case None => None }, - currentTourMode = if (activity.getType.equals("Home")) None else currentTourMode, + currentTripMode = None, hasDeparted = false ) case None => @@ -1507,7 +1542,7 @@ class PersonAgent( } def getReplanningReasonFrom(data: BasePersonData, prefix: String): String = { - data.currentTourMode + data.currentTripMode .collect { case mode => s"$prefix $mode" } @@ -1581,7 +1616,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/choice/logit/TourModeChoiceModel.scala b/src/main/scala/beam/agentsim/agents/choice/logit/TourModeChoiceModel.scala new file mode 100644 index 00000000000..6e507d28c13 --- /dev/null +++ b/src/main/scala/beam/agentsim/agents/choice/logit/TourModeChoiceModel.scala @@ -0,0 +1,27 @@ +package beam.agentsim.agents.choice.logit + +import beam.sim.config.BeamConfig + +object TourModeChoiceModel { + def apply(beamConfig: BeamConfig) = new TourModeChoiceModel(beamConfig) + + sealed trait TourModeParameters + + object TourModeParameters { + final case object ExpectedMaxUtility extends TourModeParameters with Serializable + final case object Intercept extends TourModeParameters with Serializable + } + + type TourModeMNLConfig = Map[TourModeParameters, UtilityFunctionOperation] +} + +class TourModeChoiceModel( + val beamConfig: BeamConfig +) { + + val DefaultMNLParameters: TourModeChoiceModel.TourModeMNLConfig = Map( + TourModeChoiceModel.TourModeParameters.ExpectedMaxUtility -> UtilityFunctionOperation.Multiplier(1.0), + TourModeChoiceModel.TourModeParameters.Intercept -> UtilityFunctionOperation.Intercept(1.0) + ) + +} diff --git a/src/main/scala/beam/agentsim/agents/choice/mode/ModeChoiceMultinomialLogit.scala b/src/main/scala/beam/agentsim/agents/choice/mode/ModeChoiceMultinomialLogit.scala index fda51c6e2ed..4e2875c42be 100755 --- a/src/main/scala/beam/agentsim/agents/choice/mode/ModeChoiceMultinomialLogit.scala +++ b/src/main/scala/beam/agentsim/agents/choice/mode/ModeChoiceMultinomialLogit.scala @@ -44,6 +44,8 @@ class ModeChoiceMultinomialLogit( override lazy val beamConfig: BeamConfig = beamConfigHolder.beamConfig + override val modeChoiceLogit: MultinomialLogit[BeamMode, String] = modeModel + var expectedMaximumUtility: Double = 0.0 val modalBehaviors: ModalBehaviors = beamConfig.beam.agentsim.agents.modalBehaviors diff --git a/src/main/scala/beam/agentsim/agents/choice/mode/TourModeChoiceMultinomialLogit.scala b/src/main/scala/beam/agentsim/agents/choice/mode/TourModeChoiceMultinomialLogit.scala new file mode 100644 index 00000000000..757c4cad648 --- /dev/null +++ b/src/main/scala/beam/agentsim/agents/choice/mode/TourModeChoiceMultinomialLogit.scala @@ -0,0 +1,111 @@ +package beam.agentsim.agents.choice.mode + +import beam.agentsim.agents.choice.logit.{MultinomialLogit, TourModeChoiceModel, UtilityFunctionOperation} +import beam.agentsim.agents.choice.logit.TourModeChoiceModel.{TourModeMNLConfig, TourModeParameters} +import beam.router.Modes.BeamMode +import beam.router.TourModes.BeamTourMode +import beam.router.skim.core.ODSkimmer.ODSkimmerTimeCostTransfer +import beam.sim.population.AttributesOfIndividual + +import scala.collection.mutable +import scala.util.Random + +class TourModeChoiceMultinomialLogit( + val attributesOfIndividual: AttributesOfIndividual, + val tourModeChoiceModel: TourModeChoiceModel +) { + val rnd: Random = new scala.util.Random(System.currentTimeMillis()) + + val tourModeLogit = MultinomialLogit[BeamTourMode, TourModeChoiceModel.TourModeParameters]( + Map.empty[BeamTourMode, Map[TourModeParameters, UtilityFunctionOperation]], + tourModeChoiceModel.DefaultMNLParameters + ) + + def chooseTourMode( + tourModeCosts: Seq[Map[BeamMode, ODSkimmerTimeCostTransfer]], + modeLogit: MultinomialLogit[BeamMode, String], + modeToTourMode: Map[BeamTourMode, Seq[BeamMode]], + firstAndLastTripModeToTourModeOption: Option[Map[BeamTourMode, Seq[BeamMode]]] + ): Option[BeamTourMode] = { + val tourUtility = + tourExpectedMaxUtility(tourModeCosts, modeLogit, modeToTourMode, firstAndLastTripModeToTourModeOption) + tourModeChoice(tourUtility, tourModeLogit) + } + + def tripExpectedMaxUtility( + tripModeCosts: Map[BeamMode, ODSkimmerTimeCostTransfer], + modeLogit: MultinomialLogit[BeamMode, String], + modeToTourMode: Map[BeamTourMode, Seq[BeamMode]] + ): Map[BeamTourMode, Double] = { + modeToTourMode map { case (beamTourMode, beamModes) => + val modeChoice = beamModes.map { beamMode => + val skims = tripModeCosts.getOrElse(beamMode, ODSkimmerTimeCostTransfer()) + val timeCost = attributesOfIndividual.getVOT(skims.timeInHours) + val monetaryCost = skims.cost + beamMode -> (Map("cost" -> (timeCost + monetaryCost)) ++ Map( + "transfers" -> skims.numTransfers.toDouble + )) + }.toMap + beamTourMode -> modeLogit.getExpectedMaximumUtility(modeChoice).getOrElse(Double.NegativeInfinity) + } + } + + def tourExpectedMaxUtility( + tourModeCosts: Seq[Map[BeamMode, ODSkimmerTimeCostTransfer]], + modeLogit: MultinomialLogit[BeamMode, String], + modeToTourMode: Map[BeamTourMode, Seq[BeamMode]], + firstAndLastTripModeToTourModeOption: Option[Map[BeamTourMode, Seq[BeamMode]]] = None + ): Map[BeamTourMode, Double] = { + val tourModeToExpectedUtility = mutable.Map[BeamTourMode, Double]() + tourModeCosts.zipWithIndex foreach { case (modeCosts, idx) => + if (idx == 0 | idx == tourModeCosts.length - 1) { + // Allow inclusion of private vehicles in first/last trips, e.g. for DRIVE_TRANSIT + // Default to normal modeToTourMode if the other mapping isn't defined, though + tripExpectedMaxUtility(modeCosts, modeLogit, firstAndLastTripModeToTourModeOption.getOrElse(modeToTourMode)) + .map { case (tourMode, util) => + tourModeToExpectedUtility += (tourMode -> (tourModeToExpectedUtility.getOrElse(tourMode, 0.0) + util)) + } + } else { + tripExpectedMaxUtility(modeCosts, modeLogit, modeToTourMode).map { case (tourMode, util) => + tourModeToExpectedUtility += (tourMode -> (tourModeToExpectedUtility.getOrElse(tourMode, 0.0) + util)) + } + } + + } + tourModeToExpectedUtility.toMap + } + + def tourModeChoice( + tourModeUtility: Map[BeamTourMode, Double], + tourModeLogit: MultinomialLogit[BeamTourMode, TourModeParameters] + ): Option[BeamTourMode] = { + val tourModeChoiceData: Map[BeamTourMode, Map[TourModeParameters, Double]] = tourModeUtility.map { + case (tourMode, util) => + tourMode -> Map[TourModeParameters, Double]( + TourModeParameters.ExpectedMaxUtility -> util, + TourModeParameters.Intercept -> 0 + ) + } + tourModeLogit.sampleAlternative(tourModeChoiceData, rnd) match { + case Some(sample) => Some(sample.alternativeType) + case None => None + } + } + + def apply( + tourModeCosts: Seq[Map[BeamMode, ODSkimmerTimeCostTransfer]], + modeLogit: MultinomialLogit[BeamMode, String], + modeToTourMode: Map[BeamTourMode, Seq[BeamMode]], + firstAndLastTripModeToTourModeOption: Option[Map[BeamTourMode, Seq[BeamMode]]] + ): Option[BeamTourMode] = { + chooseTourMode(tourModeCosts, modeLogit, modeToTourMode, firstAndLastTripModeToTourModeOption) + } + + def apply( + tourModeCosts: Seq[Map[BeamMode, ODSkimmerTimeCostTransfer]], + modeLogit: MultinomialLogit[BeamMode, String], + modeToTourMode: Map[BeamTourMode, Seq[BeamMode]] + ): Option[BeamTourMode] = { + chooseTourMode(tourModeCosts, modeLogit, modeToTourMode, None) + } +} diff --git a/src/main/scala/beam/agentsim/agents/household/HouseholdActor.scala b/src/main/scala/beam/agentsim/agents/household/HouseholdActor.scala index a8ad0686018..69d5ee9299b 100755 --- a/src/main/scala/beam/agentsim/agents/household/HouseholdActor.scala +++ b/src/main/scala/beam/agentsim/agents/household/HouseholdActor.scala @@ -7,6 +7,8 @@ import akka.util.Timeout import beam.agentsim.Resource.NotifyVehicleIdle import beam.agentsim.agents.BeamAgent.Finish import beam.agentsim.agents._ +import beam.agentsim.agents.choice.logit.TourModeChoiceModel +import beam.agentsim.agents.choice.mode.TourModeChoiceMultinomialLogit import beam.agentsim.agents.modalbehaviors.ChoosesMode.{CavTripLegsRequest, CavTripLegsResponse} import beam.agentsim.agents.modalbehaviors.DrivesVehicle.VehicleOrToken import beam.agentsim.agents.modalbehaviors.ModeChoiceCalculator @@ -373,6 +375,8 @@ object HouseholdActor { household.members.foreach { person => val attributes = person.getCustomAttributes.get("beam-attributes").asInstanceOf[AttributesOfIndividual] val modeChoiceCalculator = modeChoiceCalculatorFactory(attributes) + val tourModeChoiceCalculator = + new TourModeChoiceMultinomialLogit(attributes, new TourModeChoiceModel(beamScenario.beamConfig)) val selectedPlan = person.getSelectedPlan // Set zero endTime for plans with one activity. In other case agent sim will be started // before all InitializeTrigger's are completed @@ -389,6 +393,7 @@ object HouseholdActor { beamServices, beamScenario, modeChoiceCalculator, + tourModeChoiceCalculator, transportNetwork, tollCalculator, router, diff --git a/src/main/scala/beam/agentsim/agents/modalbehaviors/ChoosesMode.scala b/src/main/scala/beam/agentsim/agents/modalbehaviors/ChoosesMode.scala index 12deb81e42e..d83c4a90c6a 100755 --- a/src/main/scala/beam/agentsim/agents/modalbehaviors/ChoosesMode.scala +++ b/src/main/scala/beam/agentsim/agents/modalbehaviors/ChoosesMode.scala @@ -8,6 +8,7 @@ import beam.agentsim.agents._ import beam.agentsim.agents.household.HouseholdActor.{MobilityStatusInquiry, MobilityStatusResponse, ReleaseVehicle} import beam.agentsim.agents.modalbehaviors.ChoosesMode._ import beam.agentsim.agents.modalbehaviors.DrivesVehicle.{ActualVehicle, Token, VehicleOrToken} +import beam.agentsim.agents.planning.Strategy.{TourModeChoiceStrategy, TripModeChoiceStrategy} import beam.agentsim.agents.ridehail.{RideHailInquiry, RideHailRequest, RideHailResponse} import beam.agentsim.agents.vehicles.AccessErrorCodes.RideHailNotRequestedError import beam.agentsim.agents.vehicles.EnergyEconomyAttributes.Powertrain @@ -20,12 +21,14 @@ import beam.agentsim.infrastructure.{ParkingInquiry, ParkingInquiryResponse, Zon import beam.agentsim.scheduler.BeamAgentScheduler.{CompletionNotice, ScheduleTrigger} import beam.router.BeamRouter._ import beam.router.Modes.BeamMode -import beam.router.Modes.BeamMode.{WALK, _} +import beam.router.Modes.BeamMode._ +import beam.router.TourModes.BeamTourMode +import beam.router.TourModes.BeamTourMode._ import beam.router.model.{BeamLeg, EmbodiedBeamLeg, EmbodiedBeamTrip} import beam.router.skim.core.ODSkimmer import beam.router.skim.event.ODSkimmerFailedTripEvent import beam.router.skim.readonly.ODSkims -import beam.router.{Modes, RoutingWorker} +import beam.router.{Modes, RoutingWorker, TourModes} import beam.sim.population.AttributesOfIndividual import beam.sim.{BeamServices, Geofence} import beam.utils.logging.pattern.ask @@ -41,6 +44,7 @@ import scala.collection.JavaConverters import scala.concurrent.duration._ import scala.concurrent.{ExecutionContext, Future} + /** * BEAM */ @@ -132,137 +136,53 @@ trait ChoosesMode { def bodyVehiclePersonId: PersonIdWithActorRef = PersonIdWithActorRef(id, self) 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 currentTourStrategy = _experiencedBeamPlan.getTourStrategy[TourModeChoiceStrategy](nextAct) + val currentTripMode = _experiencedBeamPlan.getTripStrategy[TripModeChoiceStrategy](nextAct).mode + 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 && + ( + currentTripMode.exists(mode => mode == CAR || mode == BIKE) || + currentTripMode.exists(mode => mode == DRIVE_TRANSIT || mode == BIKE_TRANSIT) + && isLastTripWithinTour(nextAct) + ) => + self ! MobilityStatusResponse( + Vector(beamVehicles(data.personData.currentTourPersonalVehicle.get)), + getCurrentTriggerIdOrGenerate + ) + // Create teleportation vehicle if we are told to use teleportation + case data: ChoosesModeData if choosesModeData.personData.currentTripMode.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 => implicit val executionContext: ExecutionContext = context.system.dispatcher - plansModeOption match { + currentTripMode 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 } @@ -308,28 +228,15 @@ 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)) - // 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 availableModes: Seq[BeamMode] = availableModesForPerson(matsimPlan.getPerson, choosesModeData.excludeModes) + val personData = choosesModeData.personData + val nextAct = nextActivity(personData).get val bodyStreetVehicle = createBodyStreetVehicle(currentPersonLocation) - val nextAct = nextActivity(choosesModeData.personData).get val departTime = _currentTick.get var availablePersonalStreetVehicles = - correctedCurrentTourMode match { + personData.currentTripMode 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)) @@ -337,11 +244,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(nextAct)) { newlyAvailableBeamVehicles } else { Vector() @@ -350,6 +253,49 @@ trait ChoosesMode { Vector() } + val chosenCurrentTourMode: Option[BeamTourMode] = personData.currentTourMode match { + case Some(tourMode) => Some(tourMode) + case None => + val availablePersonalVehicleModes = availablePersonalStreetVehicles.map(x => x.streetVehicle.mode).distinct + val availableFirstAndLastLegModes = + availablePersonalVehicleModes.flatMap(x => BeamTourMode.enabledModes.get(x)).flatten + val modesToQuery = + (availablePersonalVehicleModes ++ BeamMode.nonPersonalVehicleModes ++ availableFirstAndLastLegModes).distinct + .intersect(availableModes) + val dummyVehicleType = beamScenario.vehicleTypes(dummyRHVehicle.vehicleTypeId) + val currentTour = _experiencedBeamPlan.getTourContaining(nextAct) + val tourModeCosts = beamServices.skims.od_skimmer.getTourModeCosts( + modesToQuery, + currentTour, + dummyRHVehicle.vehicleTypeId, + dummyVehicleType, + beamScenario.fuelTypePrices(dummyVehicleType.primaryFuelType) + ) + val modeToTourMode = + BeamTourMode.values.map(tourMode => tourMode -> tourMode.allowedBeamModes.intersect(modesToQuery)).toMap + val firstAndLastTripModeToTourModeOption = BeamTourMode.values + .map(tourMode => tourMode -> tourMode.allowedBeamModesForFirstAndLastLeg.intersect(modesToQuery)) + .toMap + tourModeChoiceCalculator( + tourModeCosts, + modeChoiceCalculator.modeChoiceLogit, + modeToTourMode, + Some(firstAndLastTripModeToTourModeOption) + ) + } + + _experiencedBeamPlan.putStrategy( + _experiencedBeamPlan.getTourContaining(nextAct), + TourModeChoiceStrategy(chosenCurrentTourMode) + ) + + val availableModesGivenTourMode = getAvailableModesGivenTourMode( + availableModes, + availablePersonalStreetVehicles, + chosenCurrentTourMode, + isFirstOrLastTripWithinTour(nextAct) + ) + def makeRequestWith( withTransit: Boolean, vehicles: Vector[StreetVehicle], @@ -408,7 +354,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) @@ -418,14 +364,13 @@ trait ChoosesMode { } } - val hasRideHail = availableModes.contains(RIDE_HAIL) + val hasRideHail = availableModesGivenTourMode.contains(RIDE_HAIL) var responsePlaceholders = ChoosesModeResponsePlaceholders() var requestId: Option[Int] = None // Form and send requests - var householdVehiclesWereNotAvailable = false // to replan when personal vehicles are not available - correctedCurrentTourMode match { + personData.currentTripMode match { case None => if (hasRideHail) { responsePlaceholders = makeResponsePlaceholders( @@ -441,9 +386,18 @@ trait ChoosesMode { responsePlaceholders = makeResponsePlaceholders(withRouting = true) requestId = None } + + val availableStreetVehiclesGivenTourMode = newlyAvailableBeamVehicles.map { vehicleOrToken => + chosenCurrentTourMode match { + case Some(BIKE_BASED) if !vehicleOrToken.vehicle.isSharedVehicle => vehicleOrToken.streetVehicle + case Some(CAR_BASED) if !vehicleOrToken.vehicle.isSharedVehicle => vehicleOrToken.streetVehicle + case _ => vehicleOrToken.streetVehicle + } + } :+ bodyStreetVehicle + makeRequestWith( - withTransit = availableModes.exists(_.isTransit), - newlyAvailableBeamVehicles.map(_.streetVehicle) :+ bodyStreetVehicle, + withTransit = availableModesGivenTourMode.exists(_.isTransit), + availableStreetVehiclesGivenTourMode, possibleEgressVehicles = dummySharedVehicles ) case Some(WALK) => @@ -454,7 +408,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) @@ -521,13 +475,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(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) @@ -539,7 +490,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, @@ -575,8 +526,8 @@ trait ChoosesMode { logDebug(m.toString) } val newPersonData = choosesModeData.copy( - personData = choosesModeData.personData - .copy(currentTourMode = if (householdVehiclesWereNotAvailable) None else correctedCurrentTourMode), + personData = personData + .copy(currentTripMode = choosesModeData.personData.currentTripMode, currentTourMode = chosenCurrentTourMode), availablePersonalStreetVehicles = availablePersonalStreetVehicles, allAvailableStreetVehicles = newlyAvailableBeamVehicles, routingResponse = responsePlaceholders.routingResponse, @@ -837,6 +788,24 @@ trait ChoosesMode { ) } using completeChoiceIfReady) + private def correctCurrentTripModeAccordingToRules( + currentTripMode: Option[BeamMode], + personData: BasePersonData, + availableModes: Seq[BeamMode] + ): Option[BeamMode] = { + val replanningIsAvailable = + personData.numberOfReplanningAttempts < beamServices.beamConfig.beam.agentsim.agents.modalBehaviors.maximumNumberOfReplanningAttempts + currentTripMode 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 + } + } + private def makeParkingInquiries( choosesModeData: ChoosesModeData, itineraries: Seq[EmbodiedBeamTrip] @@ -1123,7 +1092,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 @@ -1160,6 +1129,30 @@ trait ChoosesMode { } } + def getAvailableModesGivenTourMode( + availableModes: Seq[BeamMode], + availablePersonalStreetVehicles: Vector[VehicleOrToken], + currentTourMode: Option[BeamTourMode], + isFirstOrLastTrip: Boolean = false + ): Seq[BeamMode] = { + availableModes.intersect(currentTourMode match { + case Some(WALK_BASED) => + val enabledModes = availablePersonalStreetVehicles + .flatMap(veh => + if (veh.vehicle.isSharedVehicle) { BeamTourMode.enabledModes.get(veh.streetVehicle.mode) } + else None + ) + .flatten + val walkBasedModes = + if (isFirstOrLastTrip) WALK_BASED.allowedBeamModesForFirstAndLastLeg + else WALK_BASED.allowedBeamModes + walkBasedModes ++ enabledModes + case Some(tourMode) => tourMode.allowedBeamModesGivenAvailableVehicles(availablePersonalStreetVehicles, isFirstOrLastTrip) + case None => BeamMode.allModes + }) + } + + def mustBeDrivenHome(vehicle: VehicleOrToken): Boolean = { vehicle match { case ActualVehicle(beamVehicle) => @@ -1216,7 +1209,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, @@ -1258,28 +1251,25 @@ 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) ) case _ => } + // TODO: available vehicles + val availableModesForTrips = getAvailableModesGivenTourMode( + availableModesForPerson(matsimPlan.getPerson, choosesModeData.excludeModes), + choosesModeData.availablePersonalStreetVehicles, + choosesModeData.personData.currentTourMode, + isFirstOrLastTripWithinTour(nextAct) + ) - val availableModesForTrips: Seq[BeamMode] = availableModesForPerson(matsimPlan.getPerson) - .filterNot(mode => choosesModeData.excludeModes.contains(mode)) - - 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(nextAct), personData.hasDeparted) match { + case (true, false) => combinedItinerariesForChoice.filter(_.tripClassifier == mode) case _ => combinedItinerariesForChoice.filter(trip => @@ -1298,7 +1288,7 @@ trait ChoosesMode { combinedItinerariesForChoice.filter(_.tripClassifier == mode) case _ => combinedItinerariesForChoice - } + }) val itinerariesOfCorrectMode = filteredItinerariesForChoice.filter(itin => availableModesForTrips.contains(itin.tripClassifier)) @@ -1309,6 +1299,13 @@ trait ChoosesMode { .asInstanceOf[AttributesOfIndividual] val availableAlts = Some(itinerariesOfCorrectMode.map(_.tripClassifier).mkString(":")) + def gotoFinishingModeChoice(chosenTrip: EmbodiedBeamTrip) = { + goto(FinishingModeChoice) using choosesModeData.copy( + pendingChosenTrip = Some(chosenTrip), + availableAlternatives = availableAlts + ) + } + modeChoiceCalculator( itinerariesOfCorrectMode, attributesOfIndividual, @@ -1317,27 +1314,9 @@ trait ChoosesMode { Some(matsimPlan.getPerson) ) match { case Some(chosenTrip) => - filteredItinerariesForChoice.foreach { - case possibleTrip - if (possibleTrip != chosenTrip) && - beamScenario.beamConfig.beam.exchange.output.sendNonChosenTripsToSkimmer => - generateSkimData( - possibleTrip.legs.lastOption.map(_.beamLeg.endTime).getOrElse(_currentTick.get), - possibleTrip, - failedTrip = false, - personData.currentActivityIndex, - currentActivity(personData), - nextActivity(personData) - ) - case _ => - } - 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 @@ -1359,10 +1338,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, @@ -1371,49 +1347,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, availableModesForTrips) + if (correctedTripMode != personData.currentTripMode) { + //give another chance to make a choice without predefined mode + //TODO: Do we need to do anything with tour mode here? + 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, @@ -1522,26 +1510,30 @@ trait ChoosesMode { _experiencedBeamPlan.activities(data.personData.currentActivityIndex).getAttributes.getAttribute("trip_id") ).getOrElse("").toString + val nextAct = nextActivity(data.personData).get + + val tourMode = data.personData.currentTourMode + val modeChoiceEvent = new ModeChoiceEvent( tick, id, chosenTrip.tripClassifier.value, - data.personData.currentTourMode.map(_.value).getOrElse(""), + tourMode.map(_.value).getOrElse(""), data.expectedMaxUtilityOfLatestChoice.getOrElse[Double](Double.NaN), _experiencedBeamPlan.activities(data.personData.currentActivityIndex).getLinkId.toString, data.availableAlternatives.get, data.availablePersonalStreetVehicles.nonEmpty, chosenTrip.legs.view.map(_.beamLeg.travelPath.distanceInM).sum, - _experiencedBeamPlan.tourIndexOfElement(nextActivity(data.personData).get), + _experiencedBeamPlan.tourIndexOfElement(nextAct), chosenTrip, _experiencedBeamPlan.activities(data.personData.currentActivityIndex).getType, - nextActivity(data.personData).get.getType, + nextAct.getType, tripId ) eventsManager.processEvent(modeChoiceEvent) - data.personData.currentTourMode match { - case Some(HOV2_TELEPORTATION | HOV3_TELEPORTATION) => + data.personData.currentTripMode match { + case Some(mode) if mode.isHovTeleportation => scheduler ! CompletionNotice( triggerId, Vector( @@ -1584,17 +1576,20 @@ trait ChoosesMode { ) ) ) + val currentTourPersonalVehicle = + if (isCurrentPersonalVehicleVoided) + vehiclesUsed.headOption.filter(mustBeDrivenHome).map(_.id) + else + data.personData.currentTourPersonalVehicle + .orElse(vehiclesUsed.headOption.filter(mustBeDrivenHome).map(_.id)) + val updatedTourStrategy = + TourModeChoiceStrategy(data.personData.currentTourMode, currentTourPersonalVehicle) + _experiencedBeamPlan.putStrategy(_experiencedBeamPlan.getTourContaining(nextAct), updatedTourStrategy) goto(WaitingForDeparture) using data.personData.copy( currentTrip = Some(chosenTrip), restOfCurrentTrip = chosenTrip.legs.toList, - currentTourMode = data.personData.currentTourMode - .orElse(Some(chosenTrip.tripClassifier)), - currentTourPersonalVehicle = - if (isCurrentPersonalVehicleVoided) - vehiclesUsed.headOption.filter(mustBeDrivenHome).map(_.id) - else - data.personData.currentTourPersonalVehicle - .orElse(vehiclesUsed.headOption.filter(mustBeDrivenHome).map(_.id)), + currentTripMode = Some(chosenTrip.tripClassifier), + currentTourPersonalVehicle = currentTourPersonalVehicle, failedTrips = data.personData.failedTrips ++ data.personData.currentTrip ) } diff --git a/src/main/scala/beam/agentsim/agents/modalbehaviors/DrivesVehicle.scala b/src/main/scala/beam/agentsim/agents/modalbehaviors/DrivesVehicle.scala index 9b240ca788d..bc9ad6793c7 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/agentsim/agents/modalbehaviors/ModeChoiceCalculator.scala b/src/main/scala/beam/agentsim/agents/modalbehaviors/ModeChoiceCalculator.scala index 82a5fe263b2..cd91f4994b2 100755 --- a/src/main/scala/beam/agentsim/agents/modalbehaviors/ModeChoiceCalculator.scala +++ b/src/main/scala/beam/agentsim/agents/modalbehaviors/ModeChoiceCalculator.scala @@ -1,11 +1,14 @@ package beam.agentsim.agents.modalbehaviors -import beam.agentsim.agents.choice.logit.LatentClassChoiceModel +import beam.agentsim.agents.choice.logit +import beam.agentsim.agents.choice.logit.{LatentClassChoiceModel, UtilityFunctionOperation} import beam.agentsim.agents.choice.logit.LatentClassChoiceModel.Mandatory +import beam.agentsim.agents.choice.logit.TourModeChoiceModel.TourModeParameters import beam.agentsim.agents.choice.mode._ import beam.agentsim.agents.vehicles.BeamVehicleType import beam.router.Modes.BeamMode import beam.router.Modes.BeamMode._ +import beam.router.TourModes.BeamTourMode import beam.router.model.{EmbodiedBeamLeg, EmbodiedBeamTrip} import beam.sim.BeamServices import beam.sim.config.{BeamConfig, BeamConfigHolder} @@ -24,6 +27,15 @@ trait ModeChoiceCalculator { val beamConfig: BeamConfig + val commonUtility: Map[String, UtilityFunctionOperation] = Map( + "cost" -> UtilityFunctionOperation("multiplier", -1) + ) + + val modeChoiceLogit = new logit.MultinomialLogit[BeamMode, String]( + Map.empty[BeamMode, Option[Map[String, UtilityFunctionOperation]]], + commonUtility + ) + lazy val random: Random = new Random( beamConfig.matsim.modules.global.randomSeed ) diff --git a/src/main/scala/beam/agentsim/agents/planning/BeamPlan.scala b/src/main/scala/beam/agentsim/agents/planning/BeamPlan.scala index 26fb0da164b..591024838bb 100755 --- a/src/main/scala/beam/agentsim/agents/planning/BeamPlan.scala +++ b/src/main/scala/beam/agentsim/agents/planning/BeamPlan.scala @@ -1,15 +1,21 @@ package beam.agentsim.agents.planning -import java.{lang, util} +import beam.agentsim.agents.modalbehaviors.ChoosesMode.ChoosesModeData +import beam.agentsim.agents.modalbehaviors.DrivesVehicle.VehicleOrToken +import beam.agentsim.agents.planning.BeamPlan.atHome -import beam.agentsim.agents.planning.Strategy.{ModeChoiceStrategy, Strategy} -import beam.router.Modes.BeamMode +import java.{lang, util} +import beam.agentsim.agents.planning.Strategy.{Strategy, TourModeChoiceStrategy, TripModeChoiceStrategy} +import beam.router.Modes.{BeamMode, isPersonalVehicleMode} +import beam.router.TourModes.BeamTourMode +import beam.router.TourModes.BeamTourMode._ import org.matsim.api.core.v01.population._ import org.matsim.core.population.PopulationUtils import org.matsim.utils.objectattributes.attributable.Attributes import scala.collection.JavaConverters._ import scala.collection.mutable +import scala.reflect.ClassTag /** * BeamPlan @@ -30,11 +36,19 @@ object BeamPlan { def apply(matsimPlan: Plan): BeamPlan = { val beamPlan = new BeamPlan beamPlan.setPerson(matsimPlan.getPerson) - matsimPlan.getPlanElements.asScala.foreach { - case activity: Activity => - beamPlan.addActivity(activity) - case leg: Leg => - beamPlan.addLeg(leg) + matsimPlan.getPlanElements.asScala.headOption match { + case Some(a1: Activity) => beamPlan.addActivity(a1) + case _ => + } + matsimPlan.getPlanElements.asScala.sliding(2).foreach { + case mutable.Buffer(_: Activity, a2: Activity) => + beamPlan.addLeg(PopulationUtils.createLeg("")) + beamPlan.addActivity(a2) + case mutable.Buffer(a1: Activity, l1: Leg) => + beamPlan.addLeg(l1) + case mutable.Buffer(l1: Leg, a1: Activity) => + beamPlan.addActivity(a1) + case _ => } beamPlan.setScore(matsimPlan.getScore) beamPlan.setType(matsimPlan.getType) @@ -85,6 +99,8 @@ object BeamPlan { newPlan.setScore(plan.getScore) newPlan } + + def atHome(activity: Activity): Boolean = activity.getType.equalsIgnoreCase("home") } class BeamPlan extends Plan { @@ -115,9 +131,11 @@ class BeamPlan extends Plan { case activity: Activity => val nextTrip = Trip(activity, nextLeg, nextTour) nextTour.addTrip(nextTrip) - if (activity.getType.equalsIgnoreCase("home")) { + if (atHome(activity)) { + // TODO: Also trigger this if we return to a location already present in the tour tours = tours :+ nextTour - nextTour = new Tour + putStrategy(nextTour, TourModeChoiceStrategy(getTourModeFromTourLegs(nextTour))) + nextTour = new Tour(originActivity = Some(activity)) } case leg: Leg => nextLeg = Some(leg) @@ -126,7 +144,7 @@ class BeamPlan extends Plan { indexBeamPlan() actsLegs.foreach { case l: Leg => - putStrategy(actsLegToTrip(l), ModeChoiceStrategy(BeamMode.fromString(l.getMode))) + putStrategy(actsLegToTrip(l), TripModeChoiceStrategy(BeamMode.fromString(l.getMode))) case _ => } } @@ -145,24 +163,37 @@ class BeamPlan extends Plan { } def putStrategy(planElement: PlanElement, strategy: Strategy): Unit = { - if (!strategies.contains(planElement)) { - strategies.put(planElement, mutable.Map[Class[_ <: Strategy], Strategy]()) - } - strategies(planElement).put(strategy.getClass, strategy) + val planElementMap = strategies.getOrElseUpdate(planElement, mutable.Map.empty[Class[_ <: Strategy], Strategy]) + planElementMap.put(strategy.getClass, strategy) - planElement match { - case tour: Tour => - tour.trips.foreach(trip => putStrategy(trip, strategy)) - case trip: Trip => - putStrategy(trip.activity, strategy) - trip.leg.foreach(theLeg => putStrategy(theLeg, strategy)) + (strategy, planElement) match { + case (tripModeChoiceStrategy: TripModeChoiceStrategy, tour: Tour) => + tripModeChoiceStrategy.tripStrategies(tour, this).foreach { case (trip, _) => + putStrategy(trip, tripModeChoiceStrategy) + } + case (tripModeChoiceStrategy: TripModeChoiceStrategy, trip: Trip) => + putStrategy(trip.activity, tripModeChoiceStrategy) // I don't think this gets used + trip.leg.foreach(theLeg => putStrategy(theLeg, tripModeChoiceStrategy)) + case (_: TourModeChoiceStrategy, _: Trip) => + throw new RuntimeException("Can only set tour mode strategy from within a tour") case _ => - // Already dealt with Acts and Legs } } - def getStrategy(planElement: PlanElement, forClass: Class[_ <: Strategy]): Option[Strategy] = { - strategies.getOrElse(planElement, mutable.Map()).get(forClass) + def getStrategy[T <: Strategy: ClassTag](planElement: PlanElement): T = { + val forClass: Class[T] = implicitly[ClassTag[T]].runtimeClass.asInstanceOf[Class[T]] + strategies + .getOrElse(planElement, Map.empty[Class[_ <: Strategy], Strategy]) + .getOrElse(forClass, forClass.getConstructor().newInstance()) + .asInstanceOf[T] + } + + def getTripStrategy[T <: Strategy: ClassTag](activity: Activity): T = { + getStrategy(actsLegToTrip(activity)) + } + + def getTourStrategy[T <: Strategy: ClassTag](activity: Activity): T = { + getStrategy(getTourContaining(activity)) } def isLastElementInTour(planElement: PlanElement): Boolean = { @@ -204,6 +235,31 @@ class BeamPlan extends Plan { } } + def getTourContaining(index: Int): Tour = { + getTourContaining(activities(index)) + } + + def getTripContaining(index: Int): Trip = { + getTripContaining(activities(index)) + } + + def getTourModeFromTourLegs(tour: Tour): Option[BeamTourMode] = { + // TODO: Should this just look at the first/last mode of legs? + var tourMode: Option[BeamTourMode] = None + if (tour.trips.exists(trip => trip.leg.isDefined)) { + tour.trips.foreach(trip => + trip.leg match { + case Some(leg) if leg.getMode.equalsIgnoreCase("car") => tourMode = Some(CAR_BASED) + case Some(leg) if leg.getMode.equalsIgnoreCase("bike") && !tourMode.contains(CAR_BASED)=> + tourMode = Some(BIKE_BASED) + case Some(_) => tourMode = Some(WALK_BASED) + case _ => + } + ) + } + tourMode + } + ////////////////////////////////////////////////////////////////////// // Supporting legacy interface ////////////////////////////////////////////////////////////////////// diff --git a/src/main/scala/beam/agentsim/agents/planning/Strategy.scala b/src/main/scala/beam/agentsim/agents/planning/Strategy.scala index a96d7a93e48..d8f1c1fdad5 100755 --- a/src/main/scala/beam/agentsim/agents/planning/Strategy.scala +++ b/src/main/scala/beam/agentsim/agents/planning/Strategy.scala @@ -1,14 +1,76 @@ package beam.agentsim.agents.planning +import beam.agentsim.agents.planning.BeamPlan.atHome +import beam.agentsim.agents.vehicles.BeamVehicle import beam.router.Modes.BeamMode +import beam.router.Modes.BeamMode._ +import beam.router.TourModes.BeamTourMode +import org.matsim.api.core.v01.Id +import org.matsim.api.core.v01.population.Activity /** * BEAM */ object Strategy { - sealed trait Strategy + trait Strategy { - case class ModeChoiceStrategy(mode: Option[BeamMode]) extends Strategy + /** + * When a tour strategy is set this method of that strategy is called. It allows to set strategies for the trips + * that this tour consists of + * @param tour the tour that this strategy is set for + * @param beamPlan the whole beam plan + * @return trips with strategies that needs to be set + */ + def tripStrategies(tour: Tour, beamPlan: BeamPlan): Seq[(Trip, Strategy)] = Seq.empty + } + + case class TourModeChoiceStrategy(tourMode: Option[BeamTourMode] = None, tourVehicle: Option[Id[BeamVehicle]] = None) + extends Strategy { + def this() = this(None) + } + + case class TripModeChoiceStrategy(mode: Option[BeamMode] = None) extends Strategy { + def this() = this(None) + + override def tripStrategies(tour: Tour, beamPlan: BeamPlan): Seq[(Trip, Strategy)] = { + val tourStrategy = this + + mode match { + case Some(CAR | BIKE) => + tour.trips.zip(Seq.fill(tour.trips.size)(tourStrategy)) + case Some(DRIVE_TRANSIT | BIKE_TRANSIT) => + //replace both ends with DRIVE_TRANSIT or BIKE_TRANSIT + val firstTrip = tour.trips.head + val lastTrip = tour.trips.last + Seq(firstTrip -> tourStrategy, lastTrip -> tourStrategy) + case Some(HOV2_TELEPORTATION | HOV3_TELEPORTATION) => + //we need to replace all CAR_HOV modes to this tour mode + //because these CAR_HOV modes shouldn't be used in this type of tour + tour.trips + .withFilter { trip => + beamPlan.getStrategy[TripModeChoiceStrategy](trip).mode match { + case Some(CAR_HOV2 | CAR_HOV3) => true + case _ => false + } + } + .map(_ -> tourStrategy) + case _ => super.tripStrategies(tour, beamPlan) + } + } + + def tourStrategy(beamPlan: BeamPlan, curAct: Activity, nextAct: Activity): TripModeChoiceStrategy = { + val currentTourModeOpt = beamPlan.getTourStrategy[TripModeChoiceStrategy](nextAct).mode + val newTourMode = currentTourModeOpt match { + case Some(_) if mode.get.isHovTeleportation => mode + case Some(DRIVE_TRANSIT | BIKE_TRANSIT) => currentTourModeOpt + case _ if atHome(curAct) => mode + case Some(_) => currentTourModeOpt + case None => mode + } + TripModeChoiceStrategy(newTourMode) + } + + } } diff --git a/src/main/scala/beam/agentsim/agents/planning/Tour.scala b/src/main/scala/beam/agentsim/agents/planning/Tour.scala index 64ce976f211..68f5cd889ef 100755 --- a/src/main/scala/beam/agentsim/agents/planning/Tour.scala +++ b/src/main/scala/beam/agentsim/agents/planning/Tour.scala @@ -1,9 +1,12 @@ package beam.agentsim.agents.planning -import org.matsim.api.core.v01.population.PlanElement +import org.matsim.api.core.v01.population.{Activity, PlanElement} import org.matsim.utils.objectattributes.attributable.Attributes -class Tour(private var tripsInternal: Vector[Trip] = Vector()) extends PlanElement { +class Tour( + private var tripsInternal: Vector[Trip] = Vector(), + val originActivity: Option[Activity] = None +) extends PlanElement { def trips: Seq[Trip] = tripsInternal diff --git a/src/main/scala/beam/agentsim/events/PathTraversalEvent.scala b/src/main/scala/beam/agentsim/events/PathTraversalEvent.scala index 7969d7018c9..851833f2cf6 100644 --- a/src/main/scala/beam/agentsim/events/PathTraversalEvent.scala +++ b/src/main/scala/beam/agentsim/events/PathTraversalEvent.scala @@ -41,7 +41,7 @@ case class PathTraversalEvent( amountPaid: Double, fromStopIndex: Option[Int], toStopIndex: Option[Int], - currentTourMode: Option[String], + currentTripMode: Option[String], /*, linkIdsToLaneOptions: IndexedSeq[(Int, Option[Int])], linkIdsToSpeedOptions: IndexedSeq[(Int, Option[Double])], @@ -96,7 +96,7 @@ case class PathTraversalEvent( attr.put(ATTRIBUTE_TOLL_PAID, amountPaid.toString) attr.put(ATTRIBUTE_FROM_STOP_INDEX, fromStopIndex.map(_.toString).getOrElse("")) attr.put(ATTRIBUTE_TO_STOP_INDEX, toStopIndex.map(_.toString).getOrElse("")) - attr.put(ATTRIBUTE_CURRENT_TOUR_MODE, currentTourMode.getOrElse("")) + attr.put(ATTRIBUTE_CURRENT_TRIP_MODE, currentTripMode.getOrElse("")) /* attr.put(ATTRIBUTE_LINKID_WITH_LANE_MAP, linkIdsToLaneOptions.map{case ((linkId, laneOption)) => s"$linkId:${laneOption.getOrElse(0)}"}.mkString(",")) attr.put(ATTRIBUTE_LINKID_WITH_SPEED_MAP, linkIdsToSpeedOptions.map{case ((linkId, speedOption)) => s"$linkId:${speedOption.getOrElse(0)}"}.mkString(",")) @@ -123,7 +123,7 @@ object PathTraversalEvent { val ATTRIBUTE_PRIMARY_FUEL: String = "primaryFuel" val ATTRIBUTE_SECONDARY_FUEL: String = "secondaryFuel" val ATTRIBUTE_NUM_PASS: String = "numPassengers" - val ATTRIBUTE_CURRENT_TOUR_MODE: String = "currentTourMode" + val ATTRIBUTE_CURRENT_TRIP_MODE: String = "currentTripMode" val ATTRIBUTE_LINK_IDS: String = "links" val ATTRIBUTE_LINK_TRAVEL_TIME: String = "linkTravelTime" @@ -163,7 +163,7 @@ object PathTraversalEvent { vehicleType: BeamVehicleType, numPass: Int, beamLeg: BeamLeg, - currentTourMode: Option[String], + currentTripMode: Option[String], primaryFuelConsumed: Double, secondaryFuelConsumed: Double, endLegPrimaryFuelLevel: Double, @@ -207,7 +207,7 @@ object PathTraversalEvent { amountPaid = amountPaid, fromStopIndex = beamLeg.travelPath.transitStops.map(_.fromIdx), toStopIndex = beamLeg.travelPath.transitStops.map(_.toIdx), - currentTourMode = currentTourMode, + currentTripMode = currentTripMode, /* linkIdsToLaneOptions = linkIdsToLaneOptions, linkIdsToSpeedOptions = linkIdsToSpeedOptions, @@ -258,8 +258,8 @@ object PathTraversalEvent { attr.get(ATTRIBUTE_FROM_STOP_INDEX).flatMap(Option(_)).flatMap(x => if (x == "") None else Some(x.toInt)) val toStopIndex: Option[Int] = attr.get(ATTRIBUTE_TO_STOP_INDEX).flatMap(Option(_)).flatMap(x => if (x == "") None else Some(x.toInt)) - val currentTourMode: Option[String] = - attr.get(ATTRIBUTE_CURRENT_TOUR_MODE).flatMap(x => if (x == "") None else Some(x)) + val currentTripMode: Option[String] = + attr.get(ATTRIBUTE_CURRENT_TRIP_MODE).flatMap(x => if (x == "") None else Some(x)) /* val linkIdsToLaneOptions = attr(ATTRIBUTE_LINKID_WITH_LANE_MAP).split(",").map(x=>{ val linkIdToLaneSplit = x.split(":") @@ -321,7 +321,7 @@ object PathTraversalEvent { amountPaid, fromStopIndex, toStopIndex, - currentTourMode, + currentTripMode, /*, linkIdsToLaneOptions, linkIdsToSpeedOptions, diff --git a/src/main/scala/beam/agentsim/events/TeleportationEvent.scala b/src/main/scala/beam/agentsim/events/TeleportationEvent.scala index afdc5a2b980..00652e73129 100644 --- a/src/main/scala/beam/agentsim/events/TeleportationEvent.scala +++ b/src/main/scala/beam/agentsim/events/TeleportationEvent.scala @@ -16,7 +16,7 @@ case class TeleportationEvent( startY: Double, endX: Double, endY: Double, - currentTourMode: Option[String] + currentTripMode: Option[String] ) extends Event(time) with ScalaEvent { import TeleportationEvent._ @@ -38,7 +38,7 @@ case class TeleportationEvent( attr.put(ATTRIBUTE_START_COORDINATE_Y, startY.toString) attr.put(ATTRIBUTE_END_COORDINATE_X, endX.toString) attr.put(ATTRIBUTE_END_COORDINATE_Y, endY.toString) - attr.put(ATTRIBUTE_CURRENT_TOUR_MODE, currentTourMode.getOrElse("")) + attr.put(ATTRIBUTE_CURRENT_TRIP_MODE, currentTripMode.getOrElse("")) filledAttrs.set(attr) attr @@ -49,7 +49,7 @@ case class TeleportationEvent( object TeleportationEvent { val EVENT_TYPE: String = "TeleportationEvent" - val ATTRIBUTE_CURRENT_TOUR_MODE: String = "currentTourMode" + val ATTRIBUTE_CURRENT_TRIP_MODE: String = "currentTripMode" val ATTRIBUTE_DEPARTURE_TIME: String = "departureTime" val ATTRIBUTE_PERSON: String = "person" diff --git a/src/main/scala/beam/router/Modes.scala b/src/main/scala/beam/router/Modes.scala index aa73937123f..47549ec6dc5 100755 --- a/src/main/scala/beam/router/Modes.scala +++ b/src/main/scala/beam/router/Modes.scala @@ -1,6 +1,7 @@ package beam.router -import beam.router.Modes.BeamMode.{BIKE, CAR, CAR_HOV2, CAR_HOV3, CAV, WALK} +import beam.agentsim.agents.modalbehaviors.DrivesVehicle.VehicleOrToken +import beam.agentsim.agents.vehicles.VehicleCategory._ import com.conveyal.r5.api.util.{LegMode, TransitModes} import com.conveyal.r5.profile.StreetMode import enumeratum.values._ @@ -34,6 +35,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 +126,10 @@ object Modes { val chainBasedModes = Seq(CAR, BIKE) + val personalVehicleModes = Seq(CAR, BIKE, DRIVE_TRANSIT, BIKE_TRANSIT) + + val nonPersonalVehicleModes = Seq(WALK, RIDE_HAIL, RIDE_HAIL_POOLED, RIDE_HAIL_TRANSIT, WALK_TRANSIT) + val transitModes = Seq(BUS, FUNICULAR, GONDOLA, CABLE_CAR, FERRY, TRAM, TRANSIT, RAIL, SUBWAY) @@ -161,6 +167,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 @@ -197,11 +205,11 @@ object Modes { } def toR5StreetMode(mode: BeamMode): StreetMode = mode match { - case BIKE => StreetMode.BICYCLE - case WALK => StreetMode.WALK - case CAR => StreetMode.CAR - case CAV => StreetMode.CAR - case _ => throw new IllegalArgumentException + case BeamMode.BIKE => StreetMode.BICYCLE + case BeamMode.WALK => StreetMode.WALK + case BeamMode.CAR => StreetMode.CAR + case BeamMode.CAV => StreetMode.CAR + case _ => throw new IllegalArgumentException } def toR5StreetMode(mode: LegMode): StreetMode = mode match { @@ -240,3 +248,85 @@ object Modes { } } + +object TourModes { + import beam.router.Modes.BeamMode + import beam.router.Modes.BeamMode._ + + sealed abstract class BeamTourMode( + val value: String, + val vehicleCategory: VehicleCategory, + val allowedBeamModes: Seq[BeamMode], + val allowedBeamModesForFirstAndLastLeg: Seq[BeamMode] + ) extends StringEnumEntry { + + import BeamTourMode._ + + + def allowedBeamModesGivenAvailableVehicles( + vehicles: Vector[VehicleOrToken], + firstOrLastLeg: Boolean + ): Seq[BeamMode] = { + val relevantModes = if (firstOrLastLeg) { allowedBeamModesForFirstAndLastLeg } + else allowedBeamModes + if ( + vehicles.exists(vehOrToken => + !vehOrToken.vehicle.isSharedVehicle && vehOrToken.streetVehicle.mode.in(relevantModes) + ) + ) { relevantModes } + else { Seq.empty[BeamMode] } + } + + def isVehicleBased: Boolean = this match { + case WALK_BASED => false + case _ => true + } + } + + object BeamTourMode extends StringEnum[BeamTourMode] with StringCirceEnum[BeamTourMode] { + + override val values: immutable.IndexedSeq[BeamTourMode] = findValues + + val enabledModes: Map[BeamMode, Seq[BeamMode]] = + Map[BeamMode, Seq[BeamMode]](CAR -> Seq(DRIVE_TRANSIT), BIKE -> Seq(BIKE_TRANSIT)) + + // TODO: Also allow use of shared bikes/cars in walk based tours + case object WALK_BASED + extends BeamTourMode( + "walk_based", + Body, + Seq[BeamMode]( + WALK, + WALK_TRANSIT, + RIDE_HAIL, + RIDE_HAIL_POOLED, + RIDE_HAIL_TRANSIT, + HOV2_TELEPORTATION, + HOV3_TELEPORTATION + ), + Seq[BeamMode]( + WALK, + WALK_TRANSIT, + RIDE_HAIL, + RIDE_HAIL_POOLED, + RIDE_HAIL_TRANSIT, + DRIVE_TRANSIT, + BIKE_TRANSIT, + HOV2_TELEPORTATION, + HOV3_TELEPORTATION + ) + ) + + case object CAR_BASED + extends BeamTourMode( + "car_based", + Car, + Seq[BeamMode](CAR, CAR_HOV2, CAR_HOV3), + Seq[BeamMode](CAR, CAR_HOV2, CAR_HOV3) + ) + + case object BIKE_BASED extends BeamTourMode("bike_based", Bike, Seq[BeamMode](BIKE), Seq[BeamMode](BIKE)) + + } + +} diff --git a/src/main/scala/beam/router/skim/core/ODSkimmer.scala b/src/main/scala/beam/router/skim/core/ODSkimmer.scala index d52d1a09f3b..b660d88c2f0 100644 --- a/src/main/scala/beam/router/skim/core/ODSkimmer.scala +++ b/src/main/scala/beam/router/skim/core/ODSkimmer.scala @@ -16,6 +16,7 @@ import org.matsim.core.controler.MatsimServices import org.matsim.core.controler.events.IterationEndsEvent import org.apache.commons.lang3.math.NumberUtils +import scala.util.Try import scala.util.control.NonFatal class ODSkimmer @Inject() (matsimServices: MatsimServices, beamScenario: BeamScenario, beamConfig: BeamConfig) @@ -381,6 +382,28 @@ object ODSkimmer extends LazyLogging { override def toCsv: String = hour + "," + mode + "," + origin + "," + destination } + case class ODSkimmerTimeCostTransfer( + timeInHours: Double = 0.0, + cost: Double = 0.0, + numTransfers: Int = 0, + crowdingLevel: Double = 0.0 + ) { + + def +(other: ODSkimmerTimeCostTransfer): ODSkimmerTimeCostTransfer = { + ODSkimmerTimeCostTransfer( + this.timeInHours + other.timeInHours, + this.cost + other.cost, + this.numTransfers + other.numTransfers, + if (this.timeInHours <= 0) { other.crowdingLevel } + else if (other.timeInHours <= 0) { this.crowdingLevel } + else { + (this.crowdingLevel / this.timeInHours + other.crowdingLevel / other.timeInHours) * + (this.timeInHours + other.timeInHours) + } + ) + } + } + def fromCsv( row: scala.collection.Map[String, String] ): (AbstractSkimmerKey, AbstractSkimmerInternal) = { diff --git a/src/main/scala/beam/router/skim/readonly/ODSkims.scala b/src/main/scala/beam/router/skim/readonly/ODSkims.scala index 0e80ed3ba3f..b043c61527e 100644 --- a/src/main/scala/beam/router/skim/readonly/ODSkims.scala +++ b/src/main/scala/beam/router/skim/readonly/ODSkims.scala @@ -1,6 +1,7 @@ package beam.router.skim.readonly import beam.agentsim.agents.choice.mode.DrivingCost +import beam.agentsim.agents.planning.Tour import beam.agentsim.agents.vehicles.BeamVehicleType import beam.agentsim.infrastructure.taz.TAZ import beam.router.BeamRouter @@ -17,11 +18,13 @@ import beam.router.Modes.BeamMode.{ TRANSIT, WALK_TRANSIT } +import beam.router.TourModes.BeamTourMode import beam.router.skim.SkimsUtils.{distanceAndTime, getRideHailCost, timeToBin} import beam.router.skim.core.AbstractSkimmerReadOnly -import beam.router.skim.core.ODSkimmer.{ExcerptData, ODSkimmerInternal, ODSkimmerKey, Skim} +import beam.router.skim.core.ODSkimmer.{ExcerptData, ODSkimmerInternal, ODSkimmerKey, ODSkimmerTimeCostTransfer, Skim} import beam.sim.config.BeamConfig import beam.sim.{BeamHelper, BeamScenario, BeamServices} +import org.matsim.api.core.v01.population.Activity import org.matsim.api.core.v01.{Coord, Id} import scala.collection.immutable @@ -97,6 +100,49 @@ class ODSkims(beamConfig: BeamConfig, beamScenario: BeamScenario) extends Abstra (timeFactor, costFactor) } + def getTourModeCosts( + modes: Seq[BeamMode], + tour: Tour, + vehicleTypeId: Id[BeamVehicleType], + vehicleType: BeamVehicleType, + fuelPrice: Double + ): Seq[Map[BeamMode, ODSkimmerTimeCostTransfer]] = { + tour.originActivity match { + case Some(originActivity) => + Seq(getSkimInfo(originActivity, tour.trips.head.activity, modes, vehicleTypeId, vehicleType, fuelPrice)) ++ + tour.trips + .sliding(2) + .map { case Seq(trip1, trip2) => + getSkimInfo(trip1.activity, trip2.activity, modes, vehicleTypeId, vehicleType, fuelPrice) + } + .toSeq + + case _ => Seq[Map[BeamMode, ODSkimmerTimeCostTransfer]]() + } + } + + def getSkimInfo( + activity1: Activity, + activity2: Activity, + modes: Iterable[BeamMode], + vehicleTypeId: Id[BeamVehicleType], + vehicleType: BeamVehicleType, + fuelPrice: Double + ): Map[BeamMode, ODSkimmerTimeCostTransfer] = { + modes.map { mode => + val skim = getTimeDistanceAndCost( + activity1.getCoord, + activity2.getCoord, + activity1.getEndTime.toInt, + mode, + vehicleTypeId, + vehicleType, + fuelPrice + ) + mode -> ODSkimmerTimeCostTransfer(skim.generalizedTime / 3600.0, skim.cost, 0, 0) + }.toMap + } + def getTimeDistanceAndCost( originUTM: Location, destinationUTM: Location, 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/agentsim/agents/PersonAgentSpec.scala b/src/test/scala/beam/agentsim/agents/PersonAgentSpec.scala index 34e4004a6e3..dc8044bd44d 100644 --- a/src/test/scala/beam/agentsim/agents/PersonAgentSpec.scala +++ b/src/test/scala/beam/agentsim/agents/PersonAgentSpec.scala @@ -4,7 +4,7 @@ import akka.actor.{Actor, ActorRef, ActorSystem, PoisonPill, Props} import akka.testkit.TestActors.ForwardActor import akka.testkit.{ImplicitSender, TestActorRef, TestFSMRef, TestKitBase, TestProbe} import beam.agentsim.agents.PersonTestUtil._ -import beam.agentsim.agents.choice.mode.ModeChoiceUniformRandom +import beam.agentsim.agents.choice.mode.{ModeChoiceUniformRandom, TourModeChoiceMultinomialLogit} import beam.agentsim.agents.household.HouseholdActor.HouseholdActor import beam.agentsim.agents.modalbehaviors.DrivesVehicle.{AlightVehicleTrigger, BoardVehicleTrigger} import beam.agentsim.agents.ridehail.{RideHailRequest, RideHailResponse} @@ -39,7 +39,7 @@ import org.matsim.households.{Household, HouseholdsFactoryImpl} import org.scalatest.BeforeAndAfter import org.scalatest.funspec.AnyFunSpecLike -import scala.collection.{mutable, JavaConverters} +import scala.collection.{JavaConverters, mutable} class PersonAgentSpec extends AnyFunSpecLike @@ -68,6 +68,8 @@ class PersonAgentSpec private lazy val modeChoiceCalculator = new ModeChoiceUniformRandom(beamConfig) + private lazy val tourModeChoiceCalculator = new TourModeChoiceMultinomialLogit(attributesOfIndividual, tourModeChoiceModel) + // Mock a transit driver (who has to be a child of a mock router) private lazy val transitDriverProps = Props(new ForwardActor(self)) @@ -114,6 +116,7 @@ class PersonAgentSpec services, beamScenario, modeChoiceCalculator, + tourModeChoiceCalculator, beamScenario.transportNetwork, self, self, diff --git a/src/test/scala/beam/agentsim/agents/TeleportationSpec.scala b/src/test/scala/beam/agentsim/agents/TeleportationSpec.scala index 115c6366d5d..530cc2c226c 100644 --- a/src/test/scala/beam/agentsim/agents/TeleportationSpec.scala +++ b/src/test/scala/beam/agentsim/agents/TeleportationSpec.scala @@ -56,9 +56,9 @@ class TeleportationSpec extends AnyFunSpecLike with Matchers with BeamHelper wit { case _: TeleportationEvent => teleportationEvents = teleportationEvents + 1 - case e: PathTraversalEvent if e.currentTourMode.contains("car_hov3") && e.mode == BeamMode.CAR => + case e: PathTraversalEvent if e.currentTripMode.contains("car_hov3") && e.mode == BeamMode.CAR => carHov3passengers.add(e.numberOfPassengers) - case e: PathTraversalEvent if e.currentTourMode.contains("car_hov2") && e.mode == BeamMode.CAR => + case e: PathTraversalEvent if e.currentTripMode.contains("car_hov2") && e.mode == BeamMode.CAR => carHov2passengers.add(e.numberOfPassengers) case e: ActivityStartEvent if e.getPersonId.toString == "2" => activitiesOfPerson2.append((e.getLinkId.toString, e.getTime, e.getActType)) diff --git a/src/test/scala/beam/agentsim/agents/freight/FreightReplannerSpec.scala b/src/test/scala/beam/agentsim/agents/freight/FreightReplannerSpec.scala index eb8a1ea2d2b..98a52939999 100644 --- a/src/test/scala/beam/agentsim/agents/freight/FreightReplannerSpec.scala +++ b/src/test/scala/beam/agentsim/agents/freight/FreightReplannerSpec.scala @@ -127,12 +127,12 @@ class FreightReplannerSpec extends AnyWordSpecLike with Matchers with BeamHelper plan.getPlanElements should have size 9 plan.getPlanElements.get(0) shouldBe a[Activity] val activity0 = plan.getPlanElements.get(0).asInstanceOf[Activity] - activity0.getType should be("Warehouse") + activity0.getType should be("Home") activity0.getEndTime should be(11915.0) plan.getPlanElements.get(2).asInstanceOf[Activity].getType should be("Unloading") plan.getPlanElements.get(4).asInstanceOf[Activity].getType should be("Unloading") plan.getPlanElements.get(6).asInstanceOf[Activity].getType should be("Loading") - plan.getPlanElements.get(8).asInstanceOf[Activity].getType should be("Warehouse") + plan.getPlanElements.get(8).asInstanceOf[Activity].getType should be("Home") } } diff --git a/src/test/scala/beam/agentsim/agents/freight/input/GenericFreightReaderSpec.scala b/src/test/scala/beam/agentsim/agents/freight/input/GenericFreightReaderSpec.scala index ace55ead817..eae38e6d8aa 100644 --- a/src/test/scala/beam/agentsim/agents/freight/input/GenericFreightReaderSpec.scala +++ b/src/test/scala/beam/agentsim/agents/freight/input/GenericFreightReaderSpec.scala @@ -158,7 +158,7 @@ class GenericFreightReaderSpec extends AnyWordSpecLike with Matchers { plan4.getPlanElements.get(2).asInstanceOf[Activity].getCoord should be( new Coord(169900.11498160253, 3510.2356380579545) ) - plan4.getPlanElements.get(4).asInstanceOf[Activity].getType should be("Warehouse") + plan4.getPlanElements.get(4).asInstanceOf[Activity].getType should be("Home") } } diff --git a/src/test/scala/beam/agentsim/planning/BeamPlanSpec.scala b/src/test/scala/beam/agentsim/planning/BeamPlanSpec.scala index f72479fafe4..9584169840b 100755 --- a/src/test/scala/beam/agentsim/planning/BeamPlanSpec.scala +++ b/src/test/scala/beam/agentsim/planning/BeamPlanSpec.scala @@ -1,7 +1,7 @@ package beam.agentsim.planning import beam.agentsim.agents.planning.BeamPlan -import beam.agentsim.agents.planning.Strategy.ModeChoiceStrategy +import beam.agentsim.agents.planning.Strategy.TripModeChoiceStrategy import beam.router.Modes.BeamMode.CAR import beam.sim.BeamHelper import org.matsim.api.core.v01.Coord @@ -37,7 +37,7 @@ class BeamPlanSpec extends AnyWordSpecLike with Matchers with BeamHelper { PopulationUtils.createAndAddLeg(matsimPlan, "car") PopulationUtils.createAndAddActivityFromCoord(matsimPlan, "Home", new Coord(0.0, 0.0)) - val strat = ModeChoiceStrategy(Some(CAR)) + val strat = TripModeChoiceStrategy(Some(CAR)) "should contain the same activities and legs as the MATSimn plan used in creation" in { val beamPlan = BeamPlan(matsimPlan) @@ -52,47 +52,49 @@ class BeamPlanSpec extends AnyWordSpecLike with Matchers with BeamHelper { val beamPlan = BeamPlan(matsimPlan) val act = beamPlan.activities.head beamPlan.putStrategy(act, strat) - beamPlan.getStrategy(act, classOf[ModeChoiceStrategy]) should be(Some(strat)) + beamPlan.getStrategy[TripModeChoiceStrategy](act) should be(Some(strat)) } "should attach a strategy to a leg" in { val beamPlan = BeamPlan(matsimPlan) val leg = beamPlan.legs.head beamPlan.putStrategy(leg, strat) - beamPlan.getStrategy(leg, classOf[ModeChoiceStrategy]) should be(Some(strat)) + beamPlan.getStrategy[TripModeChoiceStrategy](leg) should be(Some(strat)) } "should attach a strategy to a trip" in { val beamPlan = BeamPlan(matsimPlan) val trip = beamPlan.trips.head beamPlan.putStrategy(trip, strat) - beamPlan.getStrategy(trip, classOf[ModeChoiceStrategy]) should be(Some(strat)) + beamPlan.getStrategy[TripModeChoiceStrategy](trip) should be(Some(strat)) } "should attach a strategy to a tour" in { val beamPlan = BeamPlan(matsimPlan) val tour = beamPlan.tours.head beamPlan.putStrategy(tour, strat) - beamPlan.getStrategy(tour, classOf[ModeChoiceStrategy]) should be(Some(strat)) + beamPlan.getStrategy[TripModeChoiceStrategy](tour) should be(Some(strat)) } "should attach a strategy to a trip and the trip's activity and leg" in { val beamPlan = BeamPlan(matsimPlan) val trip = beamPlan.trips.head beamPlan.putStrategy(trip, strat) - beamPlan.getStrategy(trip.activity, classOf[ModeChoiceStrategy]) should be(Some(strat)) + beamPlan.getStrategy[TripModeChoiceStrategy](trip.activity) should be(Some(strat)) trip.leg match { case Some(leg) => - beamPlan.getStrategy(leg, classOf[ModeChoiceStrategy]) should be(Some(strat)) + beamPlan.getStrategy[TripModeChoiceStrategy](leg) should be(Some(strat)) case None => } } - "should attach a strategy to a tour and the tour's trips, activities, and trips" in { + "should not attach a strategy to tour's trips, activities, and legs" in { val beamPlan = BeamPlan(matsimPlan) - val tour = beamPlan.tours.head - beamPlan.putStrategy(tour, strat) + val tour = beamPlan.tours(1) + val strategy = TripModeChoiceStrategy(None) + beamPlan.putStrategy(tour, strategy) + beamPlan.getStrategy[TripModeChoiceStrategy](tour) should be(Some(strategy)) tour.trips.foreach { trip => - beamPlan.getStrategy(trip, classOf[ModeChoiceStrategy]) should be(Some(strat)) - beamPlan.getStrategy(trip.activity, classOf[ModeChoiceStrategy]) should be(Some(strat)) + beamPlan.getStrategy[TripModeChoiceStrategy](trip) should be(Some(strat)) + beamPlan.getStrategy[TripModeChoiceStrategy](trip.activity) should be(Some(strat)) trip.leg match { case Some(leg) => - beamPlan.getStrategy(leg, classOf[ModeChoiceStrategy]) should be(Some(strat)) + beamPlan.getStrategy[TripModeChoiceStrategy](leg) should be(Some(strat)) case None => } } diff --git a/src/test/scala/beam/analysis/cartraveltime/StudyAreaTripFilterTest.scala b/src/test/scala/beam/analysis/cartraveltime/StudyAreaTripFilterTest.scala index 34877841bc8..e6550bd00f0 100644 --- a/src/test/scala/beam/analysis/cartraveltime/StudyAreaTripFilterTest.scala +++ b/src/test/scala/beam/analysis/cartraveltime/StudyAreaTripFilterTest.scala @@ -52,7 +52,7 @@ class StudyAreaTripFilterTest extends AnyFunSuite with Matchers { vehicleType = vehicleType, numPass = 1, beamLeg = beamLeg, - currentTourMode = None, + currentTripMode = None, primaryFuelConsumed = 1.0, secondaryFuelConsumed = 0.0, endLegPrimaryFuelLevel = 1.0, diff --git a/src/test/scala/beam/integration/SingleModeSpec.scala b/src/test/scala/beam/integration/SingleModeSpec.scala index f29744f7c47..43e4faafad9 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} @@ -154,8 +154,10 @@ class SingleModeSpec val newPlanElements = person.getSelectedPlan.getPlanElements.asScala.collect { case activity: Activity if activity.getType == "Home" => Seq(activity, scenario.getPopulation.getFactory.createLeg("drive_transit")) - case activity: Activity => Seq(activity) - case _: Leg => Nil + case activity: Activity => + Seq(activity) + Seq(activity, scenario.getPopulation.getFactory.createLeg("")) + case _: Leg => Nil }.flatten if (newPlanElements.last.isInstanceOf[Leg]) { newPlanElements.remove(newPlanElements.size - 1) @@ -205,7 +207,9 @@ class SingleModeSpec val regularPersonEvents = filterOutProfessionalDriversAndCavs(personDepartureEvents) val (driveTransit, others) = regularPersonEvents.map(_.getLegMode).partition(_ == "drive_transit") //router gives too little 'drive transit' trips, most of the persons chooses 'car' in this case - others.count(_ == "walk_transit") should be < (0.2 * driveTransit.size).toInt + withClue("When transit is available majority of agents should use drive_transit") { + others.count(_ == "walk_transit") should be < MathUtils.doubleToInt(0.2 * driveTransit.size) + } val eventsByPerson = events.groupBy(_.getAttributes.get("person")) @@ -269,8 +273,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) + } } } diff --git a/src/test/scala/beam/utils/SimRunnerForTest.scala b/src/test/scala/beam/utils/SimRunnerForTest.scala index 509f4143ef0..c5e530d217f 100644 --- a/src/test/scala/beam/utils/SimRunnerForTest.scala +++ b/src/test/scala/beam/utils/SimRunnerForTest.scala @@ -1,13 +1,16 @@ package beam.utils import java.io.File - import akka.actor.ActorSystem +import beam.agentsim.agents.choice.logit.TourModeChoiceModel import beam.agentsim.agents.modalbehaviors.ModeChoiceCalculator import beam.agentsim.events.eventbuilder.{ComplexEventBuilder, EventBuilderActor} import beam.api.{BeamCustomizationAPI, DefaultAPIImplementation} +import beam.router.Modes.BeamMode import beam.sim.config.{BeamConfig, BeamConfigHolder, MatSimBeamConfigBuilder} +import beam.sim.population.{AttributesOfIndividual, HouseholdAttributes} import beam.sim.{BeamHelper, BeamScenario, BeamServices, BeamServicesImpl, RunBeam} +import com.conveyal.r5.api.util.{LegMode, TransitModes} import com.google.inject.Injector import org.matsim.core.api.experimental.events.EventsManager import org.matsim.core.config.Config @@ -27,6 +30,8 @@ trait SimRunnerForTest extends BeamHelper with BeforeAndAfterAll with BeforeAndA // Next things are pretty cheap in initialization, so let it be non-lazy val beamConfig: BeamConfig = BeamConfig(config) + val attributesOfIndividual: AttributesOfIndividual = AttributesOfIndividual(householdAttributes = HouseholdAttributes("", 0.0,0,0,0), Option(""), false, Seq(BeamMode.CAR), valueOfTime = 0.0, age = Option(0), income = Option(0)) + val tourModeChoiceModel: TourModeChoiceModel = TourModeChoiceModel(beamConfig) val matsimConfig: Config = new MatSimBeamConfigBuilder(config).buildMatSimConf() matsimConfig.controler.setOutputDirectory(outputDirPath) matsimConfig.controler.setOverwriteFileSetting(OverwriteFileSetting.overwriteExistingFiles) diff --git a/test/input/sf-light/freight/freight-depots.csv.gz b/test/input/sf-light/freight/freight-depots.csv.gz index 814699157f6..129ba1951aa 100644 --- a/test/input/sf-light/freight/freight-depots.csv.gz +++ b/test/input/sf-light/freight/freight-depots.csv.gz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2fcd7271e8bae1d84c0c591ef9aa4dbba403284d47e0e11aa6eca910fd7fc980 -size 30826 +oid sha256:d6c455cf72f372ec73b09778eae08a7c8aaf40a38fdacf37010e7f8a6b5bdef4 +size 35777