Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Min-utility best response from rideHailMaster. #3728

Merged
merged 13 commits into from
May 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/main/resources/beam-template.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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 = [{
Expand Down
77 changes: 72 additions & 5 deletions src/main/scala/beam/agentsim/agents/ridehail/RideHailMaster.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) =>
Expand Down Expand Up @@ -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
}

}
}

Expand Down
6 changes: 6 additions & 0 deletions src/main/scala/beam/sim/config/BeamConfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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{}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}

Expand Down
56 changes: 53 additions & 3 deletions test/input/beamville/beam-multiple-rhm.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
]
2 changes: 1 addition & 1 deletion test/input/beamville/populationAttributes.xml
Git LFS file not shown