diff --git a/src/main/resources/beam-template.conf b/src/main/resources/beam-template.conf index 0c424c54295..017f3e6482f 100755 --- a/src/main/resources/beam-template.conf +++ b/src/main/resources/beam-template.conf @@ -124,6 +124,7 @@ beam.agentsim.agents.modalBehaviors.multinomialLogit.params.ride_hail_pooled_int beam.agentsim.agents.modalBehaviors.multinomialLogit.params.walk_intercept = "double | 0.0" beam.agentsim.agents.modalBehaviors.multinomialLogit.params.bike_intercept = "double | 0.0" beam.agentsim.agents.modalBehaviors.multinomialLogit.params.bike_transit_intercept = "double | 0.0" +beam.agentsim.agents.modalBehaviors.multinomialLogit.params.ride_hail_subscription = "double | 0.0" beam.agentsim.agents.modalBehaviors.multinomialLogit.utility_scale_factor = "double | 1.0" beam.agentsim.agents.modalBehaviors.lccm.filePath = ${beam.inputDirectory}"/lccm-long.csv" @@ -193,6 +194,9 @@ beam.agentsim.taz.parkingManager.parallel.numberOfClusters = "int | 8" beam.agentsim.toll.filePath = ${beam.inputDirectory}"/toll-prices.csv" # Ride Hailing Params: Options are ALL, MASS, or the individual modes comma separate, e.g. BUS,TRAM beam.agentsim.agents.rideHailTransit.modesToConsider = "MASS" +# Ride Hailing Params: Options are MIN_COST, MIN_UTILITY +beam.agentsim.agents.rideHail.bestResponseType = "MIN_COST" + # Ride Hailing Params beam.agentsim.agents.rideHail.managers = [{ diff --git a/src/main/scala/beam/agentsim/agents/ridehail/RideHailMaster.scala b/src/main/scala/beam/agentsim/agents/ridehail/RideHailMaster.scala index 623fc555c8c..4c22e080f4b 100644 --- a/src/main/scala/beam/agentsim/agents/ridehail/RideHailMaster.scala +++ b/src/main/scala/beam/agentsim/agents/ridehail/RideHailMaster.scala @@ -3,10 +3,14 @@ package beam.agentsim.agents.ridehail import akka.actor.{ActorRef, Props, Terminated} import beam.agentsim.agents.BeamAgent.Finish import beam.agentsim.agents.InitializeTrigger +import beam.agentsim.agents.choice.logit.{MultinomialLogit, UtilityFunctionOperation} import beam.agentsim.agents.ridehail.RideHailManager.ResponseCache +import beam.agentsim.agents.ridehail.RideHailManager.TravelProposal import beam.agentsim.agents.ridehail.RideHailMaster.RequestWithResponses import beam.agentsim.agents.vehicles.AccessErrorCodes.UnknownInquiryIdError -import beam.agentsim.agents.vehicles.VehicleManager +import beam.agentsim.agents.vehicles.{PersonIdWithActorRef, VehicleManager} +import beam.sim.population.AttributesOfIndividual +import beam.sim.population.PopulationAdjustment._ import beam.agentsim.scheduler.BeamAgentScheduler.{CompletionNotice, ScheduleTrigger} import beam.agentsim.scheduler.Trigger.TriggerWithId import beam.router.RouteHistory @@ -84,6 +88,7 @@ class RideHailMaster( private val inquiriesWithResponses: mutable.Map[Int, RequestWithResponses] = mutable.Map.empty private val rideHailResponseCache = new ResponseCache val rand: Random = new Random(beamScenario.beamConfig.matsim.modules.global.randomSeed) + private val bestResponseType: String = beamServices.beamConfig.beam.agentsim.agents.rideHail.bestResponseType override def loggedReceive: Receive = { case TriggerWithId(trigger: InitializeTrigger, triggerId) => @@ -136,17 +141,79 @@ class RideHailMaster( private def findBestProposal(customer: Id[Person], responses: IndexedSeq[RideHailResponse]) = { val responsesInRandomOrder = rand.shuffle(responses) - val withProposals = responses.filter(_.travelProposal.isDefined) + val withProposals = responsesInRandomOrder.filter(_.travelProposal.isDefined) if (withProposals.isEmpty) responsesInRandomOrder.head else - withProposals.minBy { response => - val travelProposal = response.travelProposal.get + bestResponseType match { + case "MIN_COST" => + withProposals.minBy { response => + val travelProposal = response.travelProposal.get + val price = travelProposal.estimatedPrice(customer) + if (travelProposal.poolingInfo.isDefined && response.request.asPooled) + Math.min(price, price * travelProposal.poolingInfo.get.costFactor) + else + price + } + case "MIN_UTILITY" => sampleProposals(customer, withProposals) + } + } + + private def sampleProposals(customer: Id[Person], responses: IndexedSeq[RideHailResponse]): RideHailResponse = { + val proposalsToSample: Map[RideHailResponse, Map[String, Double]] = + proposalsToResponseAlternatives(customer, responses) + val mnlParams = Map( + "cost" -> UtilityFunctionOperation.Multiplier(-1.0), + "subscription" -> UtilityFunctionOperation.Multiplier(1.0) + ) + val mnl: MultinomialLogit[RideHailResponse, String] = MultinomialLogit(Map.empty, mnlParams) + val proposalsWithUtility = mnl.calcAlternativesWithUtility(proposalsToSample) + val chosenProposal = mnl.sampleAlternative(proposalsWithUtility, rand) + chosenProposal.get.alternativeType + } + + private def proposalsToResponseAlternatives( + customer: Id[Person], + responses: IndexedSeq[RideHailResponse] + ): Map[RideHailResponse, Map[String, Double]] = { + val person = beamServices.matsimServices.getScenario.getPopulation.getPersons.get(customer) + val customerAttributes = person.getCustomAttributes.get(BEAM_ATTRIBUTES).asInstanceOf[AttributesOfIndividual] + responses.map { alt => + val cost: Double = { + val travelProposal = alt.travelProposal.get val price = travelProposal.estimatedPrice(customer) - if (travelProposal.poolingInfo.isDefined && response.request.asPooled) + if (travelProposal.poolingInfo.isDefined && alt.request.asPooled) Math.min(price, price * travelProposal.poolingInfo.get.costFactor) else price } + val scaledTime: Double = customerAttributes.getVOT( + getGeneralizedTimeOfProposalInHours(alt.request.customer, alt.travelProposal) + ) + val hasSubscription = + if (alt.request.rideHailServiceSubscription.contains(alt.rideHailManagerName)) 1.0 else 0.0 + + alt -> + Map( + "cost" -> (cost + scaledTime), + "subscription" -> hasSubscription * beamServices.beamConfig.beam.agentsim.agents.modalBehaviors.multinomialLogit.params.ride_hail_subscription + ) + + }.toMap + } + + private def getGeneralizedTimeOfProposalInHours( + passenger: PersonIdWithActorRef, + proposal: Option[TravelProposal] + ): Double = { + // TODO: add walking time once walk-to-point service is implemented + proposal match { + case Some(proposal) => + val wait = proposal.maxWaitingTimeInSec + val duration = proposal.travelTimeForCustomer(passenger) + (duration + (wait * beamServices.beamConfig.beam.agentsim.agents.modalBehaviors.modeVotMultiplier.waiting)) / 3600 + case _ => 0.0 + } + } } diff --git a/src/main/scala/beam/sim/config/BeamConfig.scala b/src/main/scala/beam/sim/config/BeamConfig.scala index 99b12d2592f..7e2c95040b0 100644 --- a/src/main/scala/beam/sim/config/BeamConfig.scala +++ b/src/main/scala/beam/sim/config/BeamConfig.scala @@ -634,6 +634,7 @@ object BeamConfig { drive_transit_intercept: scala.Double, ride_hail_intercept: scala.Double, ride_hail_pooled_intercept: scala.Double, + ride_hail_subscription: scala.Double, ride_hail_transit_intercept: scala.Double, transfer: scala.Double, transit_crowding: scala.Double, @@ -662,6 +663,8 @@ object BeamConfig { ride_hail_pooled_intercept = if (c.hasPathOrNull("ride_hail_pooled_intercept")) c.getDouble("ride_hail_pooled_intercept") else 0.0, + ride_hail_subscription = + if (c.hasPathOrNull("ride_hail_subscription")) c.getDouble("ride_hail_subscription") else 0.0, ride_hail_transit_intercept = if (c.hasPathOrNull("ride_hail_transit_intercept")) c.getDouble("ride_hail_transit_intercept") else 0.0, @@ -965,6 +968,7 @@ object BeamConfig { } case class RideHail( + bestResponseType: java.lang.String, cav: BeamConfig.Beam.Agentsim.Agents.RideHail.Cav, charging: BeamConfig.Beam.Agentsim.Agents.RideHail.Charging, freeSpeedLinkWeightMultiplier: scala.Double, @@ -1507,6 +1511,8 @@ object BeamConfig { def apply(c: com.typesafe.config.Config): BeamConfig.Beam.Agentsim.Agents.RideHail = { BeamConfig.Beam.Agentsim.Agents.RideHail( + bestResponseType = + if (c.hasPathOrNull("bestResponseType")) c.getString("bestResponseType") else "MIN_COST", cav = BeamConfig.Beam.Agentsim.Agents.RideHail.Cav( if (c.hasPathOrNull("cav")) c.getConfig("cav") else com.typesafe.config.ConfigFactory.parseString("cav{}") diff --git a/src/test/scala/beam/integration/ridehail/MultipleRideHailManagerSpec.scala b/src/test/scala/beam/integration/ridehail/MultipleRideHailManagerSpec.scala index 30123cd97e0..2e6f6452272 100755 --- a/src/test/scala/beam/integration/ridehail/MultipleRideHailManagerSpec.scala +++ b/src/test/scala/beam/integration/ridehail/MultipleRideHailManagerSpec.scala @@ -24,7 +24,7 @@ class MultipleRideHailManagerSpec extends AnyWordSpecLike with Matchers with Bea case pte: PathTraversalEvent if pte.vehicleId.toString.startsWith("rideHail") => pte } val groupedByFleetId = rhPTE.groupBy(pte => getFleetId(pte.vehicleId)) - groupedByFleetId.keySet shouldBe Set("Uber", "Lyft") + groupedByFleetId.keySet shouldBe Set("Uber", "Lyft", "Cruise") } } diff --git a/test/input/beamville/beam-multiple-rhm.conf b/test/input/beamville/beam-multiple-rhm.conf index 4881cc30389..692dca368d4 100644 --- a/test/input/beamville/beam-multiple-rhm.conf +++ b/test/input/beamville/beam-multiple-rhm.conf @@ -5,6 +5,8 @@ beam.agentsim.simulationName = "multiplerhm" beam.agentsim.firstIteration = 0 beam.agentsim.lastIteration = 0 +beam.agentsim.agents.modalBehaviors.multinomialLogit.params.ride_hail_subscription = 2 + beam.debug.messageLogging = true beam.debug.maxSimulationStepTimeBeforeConsideredStuckMin = 50 beam.outputs.events.fileOutputFormats = "csv.gz,xml" # valid options: xml(.gz) , csv(.gz), none - DEFAULT: csv.gz @@ -13,6 +15,8 @@ beam.physsim.skipPhysSim = true beam.debug.stuckAgentDetection.enabled = false +beam.agentsim.agents.rideHail.bestResponseType = "MIN_UTILITY" + beam.agentsim.agents.rideHail.managers = [ { name = "Uber" @@ -59,8 +63,12 @@ beam.agentsim.agents.rideHail.managers = [ initialization.initType = "FILE" # If FILE, use this param initialization.filePath = ${beam.inputDirectory}"/rideHailFleet.csv" - defaultCostPerMile = 1.25 - defaultCostPerMinute = 0.75 + defaultBaseCost = 1.0 + defaultCostPerMile = 0.5 + defaultCostPerMinute = 0.14 + pooledBaseCost = 1.1 + pooledCostPerMile = 0.6 + pooledCostPerMinute = 0.01 rideHailManager.radiusInMeters = 5000 # allocationManager(DEFAULT_MANAGER | EV_MANAGER | POOLING_ALONSO_MORA) allocationManager.name = "POOLING_ALONSO_MORA" @@ -91,5 +99,47 @@ beam.agentsim.agents.rideHail.managers = [ allocationManager.repositionLowWaitingTimes.waitingTimeWeight = 4.0 allocationManager.repositionLowWaitingTimes.demandWeight = 4.0 allocationManager.repositionLowWaitingTimes.produceDebugImages = true - } + }, + { + name = "Cruise" + iterationStats.timeBinSizeInSec = 3600 + # Initialization Type(PROCEDURAL | FILE) + initialization.initType = "PROCEDURAL" + # If PROCEDURAL, use these params + # initialization.procedural.initialLocation.name(INITIAL_RIDE_HAIL_LOCATION_HOME | INITIAL_RIDE_HAIL_LOCATION_UNIFORM_RANDOM | INITIAL_RIDE_HAIL_LOCATION_ALL_AT_CENTER | INITIAL_RIDE_HAIL_LOCATION_ALL_IN_CORNER) + initialization.procedural.initialLocation.name = "HOME" + initialization.procedural.initialLocation.home.radiusInMeters = 500 + initialization.procedural.fractionOfInitialVehicleFleet = 0.5 + initialization.procedural.vehicleTypeId = "beamVilleCar" + defaultBaseCost = 10.0 + defaultCostPerMile = 0.5 + defaultCostPerMinute = 0.14 + pooledBaseCost = 1.1 + pooledCostPerMile = 0.6 + pooledCostPerMinute = 0.01 + rideHailManager.radiusInMeters = 5000 + # allocationManager(DEFAULT_MANAGER | EV_MANAGER | POOLING_ALONSO_MORA) + allocationManager.name = "POOLING_ALONSO_MORA" + allocationManager.requestBufferTimeoutInSeconds = 200 + allocationManager.maxWaitingTimeInSec = 900 + allocationManager.maxExcessRideTime = 0.5 # up to +50% + # repositioningManager can be DEFAULT_REPOSITIONING_MANAGER | DEMAND_FOLLOWING_REPOSITIONING_MANAGER | REPOSITIONING_LOW_WAITING_TIMES | INVERSE_SQUARE_DISTANCE_REPOSITIONING_FACTOR + repositioningManager.name = "REPOSITIONING_LOW_WAITING_TIMES" + repositioningManager.timeout = 300 + # REPOSITIONING_LOW_WAITING_TIMES + allocationManager.repositionLowWaitingTimes.percentageOfVehiclesToReposition = 1.0 + allocationManager.repositionLowWaitingTimes.repositionCircleRadiusInMeters = 3000 + allocationManager.repositionLowWaitingTimes.timeWindowSizeInSecForDecidingAboutRepositioning = 1200 + allocationManager.repositionLowWaitingTimes.allowIncreasingRadiusIfDemandInRadiusLow = true + allocationManager.repositionLowWaitingTimes.minDemandPercentageInRadius = 0.1 + allocationManager.repositionLowWaitingTimes.minimumNumberOfIdlingVehiclesThresholdForRepositioning = 1 + # repositioningMethod(TOP_SCORES | KMEANS) + allocationManager.repositionLowWaitingTimes.repositioningMethod = "TOP_SCORES" + allocationManager.repositionLowWaitingTimes.keepMaxTopNScores = 5 + allocationManager.repositionLowWaitingTimes.minScoreThresholdForRepositioning = 0.00001 + allocationManager.repositionLowWaitingTimes.distanceWeight = 0.01 + allocationManager.repositionLowWaitingTimes.waitingTimeWeight = 4.0 + allocationManager.repositionLowWaitingTimes.demandWeight = 4.0 + allocationManager.repositionLowWaitingTimes.produceDebugImages = true + } ] \ No newline at end of file diff --git a/test/input/beamville/populationAttributes.xml b/test/input/beamville/populationAttributes.xml index 138d5f7821d..e714134da2f 100644 --- a/test/input/beamville/populationAttributes.xml +++ b/test/input/beamville/populationAttributes.xml @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:227f90cfcc3354e8ee301abe437ba838254f6f94ea3639be7366c5cbfd66649a +oid sha256:92517b8663ae3a7dc40799bdd323de30fee1b5d066e8b10ebdb6869aa13a4be1 size 13436