Skip to content

Car wash queue

Chesney's Clean and Wax hand car wash, a shadow account of the revenue of its workers' labour.

  • Name: car_wash
  • Field: economics/markets
  • Formalism: agent-based model
  • References: Chesney 2021

Factory

@register_model(
    field='economics/markets',
    tags=('queue', 'agents', 'labour'),
    references=('Chesney 2021',),
    defaults={'until': 600},
)
def car_wash(
    n_workers: int = 4,
    n_cars: int = 1,
    *,
    p_pass: float = 0.33,
    p_enter: float = 0.005,
    p_valet: float = 0.5,
    wash_work: float = 525.0,
    valet_work: float = 1750.0,
    wash_price: float = 4.0,
    valet_price: float = 10.0,
    balk: int = 3,
) -> System:
    """Return Chesney's Clean and Wax hand car wash, a shadow account of the revenue of its workers' labour.

    Every step is one second. Passing cars turn in with a probability and queue for the wash, washed cars may queue
    for a valet, and the workers serve the front car of each area, two at a time if they can. The wash opens with
    cars already queuing.

    Args:
        n_workers:
            The number of workers.

        n_cars:
            The number of cars queuing for the wash at the start, at least one.

        p_pass:
            The probability that a car passes in a second.

        p_enter:
            The probability that a passing car turns in.

        p_valet:
            The probability that a washed car stays for a valet.

        wash_work:
            The person-seconds of a wash.

        valet_work:
            The person-seconds of a valet.

        wash_price:
            The price of a wash.

        valet_price:
            The price of a wash with a valet.

        balk:
            The length of the wash queue at which drivers no longer turn in.

    Returns:
        system:
            The model, with the groups `workers`, `cars`, and `till`.

    Examples:
        >>> from skcomplex.models.economics.markets import car_wash
        >>> car_wash(5).elements[0].check().prepare().size_
        5
    """
    operation = CarWashOperationInteraction(
        'wash',
        cars='cars',
        till='till',
        p_pass=p_pass,
        p_enter=p_enter,
        p_valet=p_valet,
        wash_work=wash_work,
        valet_work=valet_work,
        wash_price=wash_price,
        valet_price=valet_price,
        balk=float(balk),
    )
    return System(
        elements=[
            GroupElement('workers', element=WasherElement, size=n_workers, variable=True),
            GroupElement('cars', element=CarElement, size=n_cars, values={'work': wash_work}, variable=True),
            GroupElement('till', element=TillElement, size=1, variable=True),
        ],
        interactions=[operation],
    )

Components

class WasherElement(Element):
    """Worker who joins the front car of the wash or of the valet while it has fewer than two workers.

    A worker who finds both areas staffed helps at a random one of them. One second of work removes one
    person-second from the car.

    Args:
        label:
            The label of the agent or group.

    Examples:
        >>> from skcomplex.core.elements import GroupElement
        >>> from skcomplex.models.economics.markets import WasherElement
        >>> GroupElement('workers', element=WasherElement, size=4).size
        4
    """

    cars: ClassVar[str] = 'cars'

    def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
        """Return the work rule.

        Args:
            s:
                The namespace of the symbols the laws can use.

        Returns:
            laws:
                The laws of the component.
        """
        return [RuleLaw(self.work)]

    def work(self, agent: AgentView, model: ModelView) -> None:
        """Work one second on the front car of an area.

        Args:
            agent:
                The worker.

            model:
                The model.
        """
        cars = model.agents(self.cars, CarView)
        fronts = [
            car for car in (WasherElement._front(cars, WASH), WasherElement._front(cars, VALET)) if car is not None
        ]
        if not fronts:
            return
        short = [car for car in fronts if car.crew < CREW_SIZE]
        car = short[0] if short else fronts[int(model.rng.integers(len(fronts)))]
        car.crew += 1
        car.work -= 1.0

    @staticmethod
    def _front(cars: list[CarView], stage: int) -> CarView | None:
        """Return the car that came first among the cars at a stage, or `None` if there is none."""
        waiting = [car for car in cars if car.stage == stage]
        return min(waiting, key=lambda car: car.id) if waiting else None

class CarElement(Element):
    """Car at the wash, waiting for or receiving a wash or a valet.

    Args:
        label:
            The label of the agent or group.

        visual:
            How the element is drawn, or `None` for `default_visual_`.

        stage:
            0 for the wash and 1 for the valet.

        work:
            The person-seconds of work left at the current stage.

        crew:
            The number of workers on the car in the current second.

    Examples:
        >>> from skcomplex.core.elements import GroupElement
        >>> from skcomplex.models.economics.markets import CarElement
        >>> GroupElement('cars', element=CarElement, size=1).size
        1
    """

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = None,
        *,
        stage: Annotated[float, StateSpec(domain=NN)] = WASH,
        work: Annotated[float, StateSpec()] = 525.0,
        crew: Annotated[float, StateSpec(domain=NN)] = 0,
    ) -> None:
        super().__init__(label, visual=visual)
        self.stage = stage
        self.work = work
        self.crew = crew

    @classmethod
    def _get_view(cls) -> type[CarView]:
        """Return the view of a car, with its stage, work, and crew."""
        return CarView

    def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
        """Return no laws, since the workers and the till act on the cars.

        Args:
            s:
                The namespace of the symbols the laws can use.

        Returns:
            laws:
                The laws of the component.
        """
        return []

class TillElement(Element):
    """Till of the car wash, which counts the revenue and the cars served and turned away.

    Args:
        label:
            The label of the agent or group.

        visual:
            How the element is drawn, or `None` for `default_visual_`.

        revenue:
            The money taken.

        washes:
            The number of cars that left after a wash only.

        valets:
            The number of cars that left after a valet.

        turned_away:
            The number of drivers who did not turn in because of the queue.

    Examples:
        >>> from skcomplex.core.elements import GroupElement
        >>> from skcomplex.models.economics.markets import TillElement
        >>> GroupElement('till', element=TillElement, size=1).size
        1
    """

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = None,
        *,
        revenue: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
        washes: Annotated[float, StateSpec(domain=NN)] = 0,
        valets: Annotated[float, StateSpec(domain=NN)] = 0,
        turned_away: Annotated[float, StateSpec(domain=NN)] = 0,
    ) -> None:
        super().__init__(label, visual=visual)
        self.revenue = revenue
        self.washes = washes
        self.valets = valets
        self.turned_away = turned_away

    @classmethod
    def _get_view(cls) -> type[TillView]:
        """Return the view of the till, with its revenue and counts."""
        return TillView

    def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
        """Return no laws, since the car wash books the payments.

        Args:
            s:
                The namespace of the symbols the laws can use.

        Returns:
            laws:
                The laws of the component.
        """
        return []

class CarWashOperationInteraction(Interaction):
    """Arrival of passing cars, completion of washes and valets, and payment, once every second.

    A passing car turns in with a probability, unless the wash queue holds `balk` cars or more. A washed car stays for
    a valet with a probability, and pays the wash price or the valet price when it leaves.

    Args:
        label:
            The label of the interaction.

        cars:
            The path of the group of cars.

        till:
            The path of the group that holds the till.

        p_pass:
            The probability that a car passes in a second.

        p_enter:
            The probability that a passing car turns in.

        p_valet:
            The probability that a washed car stays for a valet.

        wash_work:
            The person-seconds of a wash.

        valet_work:
            The person-seconds of a valet.

        wash_price:
            The price of a wash.

        valet_price:
            The price of a wash with a valet.

        balk:
            The length of the wash queue at which drivers no longer turn in.

    Examples:
        >>> from skcomplex.models.economics.markets import CarWashOperationInteraction
        >>> CarWashOperationInteraction('wash', cars='cars', till='till', p_enter=0.01).p_enter
        0.01
    """

    def __init__(
        self,
        label: str,
        *,
        cars: Annotated[str, RoleSpec()],
        till: Annotated[str, RoleSpec()],
        p_pass: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.33,
        p_enter: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.005,
        p_valet: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.5,
        wash_work: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 525.0,
        valet_work: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1750.0,
        wash_price: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 4.0,
        valet_price: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 10.0,
        balk: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 3.0,
    ) -> None:
        super().__init__(label)
        self.cars = cars
        self.till = till
        self.p_pass = p_pass
        self.p_enter = p_enter
        self.p_valet = p_valet
        self.wash_work = wash_work
        self.valet_work = valet_work
        self.wash_price = wash_price
        self.valet_price = valet_price
        self.balk = balk

    def _finish(self, car: CarView, till: TillView, model: ModelView, values: dict[str, float]) -> None:
        """Move a car whose work is done to the valet, or take its payment and let it leave."""
        if car.stage == WASH and model.rng.random() < values['p_valet']:
            car.stage, car.work = VALET, values['valet_work']
            return
        if car.stage == WASH:
            till.revenue += values['wash_price']
            till.washes += 1
        else:
            till.revenue += values['valet_price']
            till.valets += 1
        car.remove()

    def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
        """Return the rule of one second.

        Args:
            s:
                The namespace of the symbols the laws can use.

        Returns:
            laws:
                The laws of the component.
        """
        return [RuleLaw(self.operate)]

    def operate(self, model: ModelView) -> None:
        """Finish the cars whose work is done, reset the crews, and let a passing car turn in or drive on.

        Args:
            model:
                The model.
        """
        values = {name: float(model.parameter(name)) for name in _PARAMETERS}
        till = model.agents('till', TillView)[0]
        for car in model.agents('cars', CarView):
            car.crew = 0
            if car.work <= 0:
                self._finish(car, till, model, values)
        if model.rng.random() >= values['p_pass'] * values['p_enter']:
            return
        if sum(car.stage == WASH for car in model.agents('cars', CarView)) >= values['balk']:
            till.turned_away += 1
            return
        model.create('cars', stage=WASH, work=values['wash_work'])