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

[Feature Request] Time/State-Dependent Baulking #234

Closed
galenseilis opened this issue Dec 18, 2023 · 3 comments
Closed

[Feature Request] Time/State-Dependent Baulking #234

galenseilis opened this issue Dec 18, 2023 · 3 comments

Comments

@galenseilis
Copy link
Contributor

galenseilis commented Dec 18, 2023

I noticed that arrival_node.ArrivalNode.decide_baulk is responsible for making a decision about whether to baulk an instance of Individual.

Here is the implementation:

    def decide_baulk(self, next_node, next_individual):
        """
        Either makes an individual baulk, or sends the individual
        to the next node.
        """
        if next_node.baulking_functions[self.next_class] is None:
            self.send_individual(next_node, next_individual)
        else:
            rnd_num = random()
            if rnd_num < next_node.baulking_functions[self.next_class](next_node.number_of_individuals):
                self.record_baulk(next_node, next_individual)
                self.simulation.nodes[-1].accept(next_individual, completed=False)
            else:
                self.send_individual(next_node, next_individual)

Baulking is conventionally thought of as customers deciding not to join the queue if it is too long, which is may be why it was implemented this way. I first learn from the docs that the baulking function just takes the length of the queue and returns a probability of baulking.

Here is the example from the documentation:

def probability_of_baulking(n):
    if n < 3:
        return 0.0
    if n < 7:
        return 0.5
    return 1.0

That function actually uses n. We could of course make baulking functions that just ignore n. Here is a frivolous example that we would not be interested in practice. In this case there is a probability of 1 that the individual will baulk if the int of the unix time is prime, otherwise there is a 0.5 probability of baulking.

import math
import time

def is_prime(n):
    if n % 2 == 0 and n > 2: 
        return False
    return all(n % i for i in range(3, int(math.sqrt(n)) + 1, 2))

def probability_of_baulking(n):
    int_unix_time = int(time.time())
    
    if is_prime(int_unix_time):
        return 1.0

    return 0.5

But what is of practical interest to me is baulking that depends on time, or the individual's properties, or other properties of next_node, or even some entirely non-local properties. And I don't want to patch this unless I have to.

How feasible is replacing

next_node.baulking_functions[self.next_class](next_node.number_of_individuals)

with

next_node.baulking_functions[self.next_class](next_node, next_individual)?

Having next_node available is a handy default behaviour and otherwise anything about time or state can be accessed via next_individual.simulation. It would allow for a form of "generalized baulking" where the reason for not joining the queue could be for reasons other than the length of the waitlist.

I realize there pretty much the same thing could be achieved via state-dependent routing.

@geraintpalmer
Copy link
Member

Hi, I love this idea. In fact, the baulking function could just take in the simulation object itself, and then have access to any property of the system it needs, including the time?

e.g.

def probability_of_baulking(Q, node_id):
    if len(Q.nodes[node_id].all_customers) < 3:
        return 0.0
    if len(Q.nodes[node_id].all_customers) < 7:
        return 0.5
    return 1.0

What do you think?

@galenseilis
Copy link
Contributor Author

galenseilis commented Dec 19, 2023

Hi, I love this idea. In fact, the baulking function could just take in the simulation object itself, and then have access to any property of the system it needs, including the time?

e.g.

def probability_of_baulking(Q, node_id):
    if len(Q.nodes[node_id].all_customers) < 3:
        return 0.0
    if len(Q.nodes[node_id].all_customers) < 7:
        return 0.5
    return 1.0

What do you think?

Thank you for considering my ideas! 😊

I can see how passing the simulation to probability_of_baulking naturally gives access to the top-level. From there the time and state can be accessed. I'm wondering, how would probability_of_baulking know which individual is being considered for baulking. That would be important for individual-dependent baulking behaviour. Would there be a natural way to access that information if the function signature was probability_of_baulking(Q, node_id)?

Here is my thinking for the function signature I suggested. Using the probability_of_baulking(next_node, next_individual) signature would communicate the pair of node and individual in consideration. In my opinion this is a natural default. However, if the time was needed, one could always use next_individual.simulation.current_time since every individual has a reference of simulation as an instance attribute. Similarly, next_individual.simulation.nodes should give access to arbitrary state information.

I suppose another option would be something like probability_of_baulking(Q, node_id, ind_id), but that means performing a search for the instance of the next node if we wanted it. It doesn't seem like a good option.

I know, I am slightly modifying the term type signature to a related note I am terming "function signature". I really am just talking about what parameters the function has.

Further discussion on this is welcome! I am still unfamiliar with most of the mechanisms in Ciw, so I may be missing something.

@geraintpalmer
Copy link
Member

Implemented here: 9d00262

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants