djordon / queueing-tool

Simulator for queueing networks written in Python
http://queueing-tool.readthedocs.org/
MIT License
64 stars 9 forks source link

Agent recognition from data #106

Open marco-create opened 10 months ago

marco-create commented 10 months ago

Hello @djordon :)
I have to simulate a one-node M/D/1 system where requests arrive with the arrival time defined as follows: def slow_arr(t): return t + random.expovariate(1.5)

and service time: def ser(t): return t + 0.5

During the simulation I need to simulate the arriving of additional requests that have lower arrival time: def fast_arr(t): return t + random.expovariate(0.15)

From what I got from the docs, I defined two Agents (requests) as follows:

ag_slow = qt.Agent(
    agent_id = (1, 1),   # edge 1, agent 1
    arrival_f = slow_arr,
)

ag_fast = qt.Agent(
    agent_id = (1, 2),
    arrival_f = fast_arr
)

and the queue (which is only one in my simulation):

q = qt.QueueServer(
    num_servers = 1,
    collect_data = True,
    service_f = ser,
)

Now, I injected the first Agent into the server and simulate 10 events and checked the data (just in case)

q.set_active()  # accept agents
ag_slow.queue_action(queue=q)
q.simulate(n=10)
q.fetch_data()

Everything seems right.
Then I injected the second Agent (the requests with lower arrival time):

ag_fast.queue_action(queue=q)
q.simulate(n=10)

Then I port the data into a dataframe and that's the result:

arrival service departure num_queued num_total q_id index
0 1.967303 1.967303 2.467303 0.0 1.0 0.0 0
1 2.093401 2.467303 2.967303 1.0 2.0 0.0 1
2 5.400844 5.400844 5.900844 0.0 1.0 0.0 2
3 5.765601 5.900844 6.400844 1.0 2.0 0.0 3
4 8.493998 8.493998 8.993998 0.0 1.0 0.0 4
5 9.035278 9.035278 9.535278 0.0 1.0 0.0 5
6 9.874867 9.874867 10.374867 0.0 1.0 0.0 6
7 10.542173 10.542173 11.042173 0.0 1.0 0.0 7
8 11.993661 11.993661 12.493661 0.0 1.0 0.0 8
9 13.107135 13.107135 0.000000 0.0 1.0 0.0 9
10 13.565815 0.000000 0.000000 1.0 2.0 0.0 10

Now I have two questions (or more?).

Thanks for the help.

djordon commented 9 months ago

Hi @marco-create , sorry for the long delay in getting to this.

First of all, is the way I simulate the network okay? (please don't roast me).

Hmmm, I don't think this will work, although I'm not entirely sure at the moment. It sounds as though you want to queue to have two different types of agents where each has it's own arrival time. Is that right? If so, then I think you have a couple of options:

  1. Create an agent factor that returns the different agent types at the right proportions. Then create a queue with your special AgentFactory.
  2. Create a simple network with three queues. Queue 1 creates agents of type 1 and is connected to Queue 3, Queue 2 creates agents if type 2 and is connected to Queue 3. The service functions for queues 1 and 2 is the identity function (def service_f(t): return t) and the service function for queue 3 is as you described above.

If you want to have different service times for the different types of agents, then you need to subclass QueueServer.

Second, is there a way to get the info about the agents? (like, "is this an agent from those with lower arrival time?"). Maybe should be implemented such a thing? Like a label.

Yes, there is. For QueueServer there is the data attribute. Check the attributes section for the QueueServer in the docs for a description of it's contents, but a short summary is that it's a dictionary where the keys are the agent ids and the values are data about the agent. When creating your new custom agent class, just add the agent class name to the agent_id (the agent id is a 2-tuple, just add name at the end making it a 3-tuple). That way the data attribute will have all the information you want.

marco-create commented 9 months ago

Thank you for replying this. As you suggested, I'm simulating a one-node system where requests at low arrival rate enter the system and leave; in the meantime, I inject another type of requests at very fast arrival rate that simulate an overload in the system and then just leave. Something like this:

---
title: One-node M/D/1 system
---
  graph LR;
  A("Entry point")
  B("Leaving point")
  λ0("slow λ")
  λ1("fast λ")

      λ0 --> A
      λ1 --> node
      A --> node --> B

The two agents are defined as follows:

def ag_slow(label):
    return qt.Agent(
        arrival_f = lambda t: t + random.expovariate(lambd=125),
        agent_id= (0, 0, 'slow')
)

def ag_fast(label):
    return qt.Agent(
        arrival_f = lambda t: t + random.expovariate(lambd=500),
        agent_id= (0, 0, 'fast')
)

but I'm then struggling with the adjacency list and edge list. If I define them like this:

adja_list = {
    0: [1],    # entry point for slow requests
    1: [3],    # 3 is leaving point
    2: [3]    # entry poiny for fast requests which leave at 3
}
edge_list = {
    0: {1: 1},
    1: {3: 3},
    2: {3: 3}
}

and start the simulation:

qn.initialize()
qn.start_collecting_data()
qn.simulate(n=200)
dat = qn.get_agent_data(return_header=True)

for k, v in dat[0].items():
    print(k)
    print(v) 

The results that I obtain seem confusing.

(0, 0, 'slow')
[[ 0.66643346  0.66643346  0.66643346  0.          1.          0.        ]
 [ 0.66643346  0.66643346  0.66755346  0.          1.          1.        ]
 [ 0.66755346  0.          0.          0.          0.          3.        ]
 ...
 [62.21592812  0.          0.          0.          0.          3.        ]
 [64.99198248 64.99198248  0.          0.          1.          1.        ]
 [64.99198248 64.99198248 64.99198248  0.          1.          0.        ]]

I would expect to get something where is visible the the slow requests enter, go into node 3, the waiting queue gets overloaded by the fast paced requests but then, eventually the slow requests leave.

Please let me know what I do wrong. Thanks!

djordon commented 9 months ago

Nice mermaid graph, it helped me see what you were thinking.

Still, it's a little hard to know for sure without a full example since I can't reproduce what you're doing. Would you mind posting a more full set of code so that I can see what you see and direct you toward a fix?

marco-create commented 9 months ago

You can find the full example I posted, here.

djordon commented 9 months ago

Okay great, I think I know what you were going after. One thing that stuck out so far is that the arrival_f is supposed to be given to the queues, not the agents. Also, your agent factory needs to be a little different. And although your network is fine, I'm going to do a slightly different network (since I think it's easier to explain):

---
title: 4-node system
---
  graph LR;
  Node("λ = 0, Only arrivals from λ0 and λ1")

      A("λ0") -- mu = infinity --> Node
      B("λ1") -- mu = infinity --> Node
      Node -- deterministic --> Ether

Roughly speaking, node 0 has arrival rate λ0 with instant departures, node 1 has arrival rate λ1 with instant departures, node 2 doesn't have an arrival rate but receives arrivals from nodes 0 and 1. The Ether node just means agents have left the system. This graph would be represented like so

import queueing_tool as qt

# You can manually add in the Ether node but I didn't here. It get's
# added for us when we create the graph below.
adja_list = {
    0: [2],
    1: [2],
    2: [3],
}
edge_list = {
    0: {2: 1},
    1: {2: 2},
    2: {3: 3},
  }

g = qt.adjacency2graph(adjacency=adja_list, edge_type=edge_list)

q_classes = {1: qt.QueueServer, 2: qt.QueueServer, 2: qt.QueueServer}

# Define the arrival and departure rates
def arr_f1(t):
    return t + np.random.exponential(125)

def arr_f2(t):
    return t + np.random.exponential(500)

def service_f(t):
    # Is this right
    return t + 0.00112

def identity(t):
    return t

class FastAgent(qt.Agent):
    def __init__(self, agent_id=(0, 0)):
        super().__init__(self, agent_id)
        self.agent_id = (agent_id[0], agent_id[1], "fast")

class SlowAgent(qt.Agent):
    def __init__(self, agent_id=(0, 0)):
        super().__init__(self, agent_id)
        self.agent_id = (agent_id[0], agent_id[1], "slow")

# Make a mapping between the edge types and the parameters used to make those
# queues. If a particular parameter is not given then the defaults are used.
q_args = {
    1: {
        "arrival_f": arr_f1,
        "service_f": identity,
        "AgentFactory": FastAgent,
    },
    2: {
        "arrival_f": arr_f2,
        "service_f": identity,
        "AgentFactory": SlowAgent,
    },
    3: {"service_f": service_f}

}

qn = qt.QueueNetwork(g=g, q_classes=q_classes, q_args=q_args)

# Lift the maximum number of agents from the default of 1000 to infinity
qn.max_agents = np.infty

# Before any simulations can take place the network must be initialized to
# allow arrivals from outside the network. This specifies that only edge
# types 1 and 2 accept arrivals from outside the network.
qn.initialize(edge_type=[1, 2])

I haven't tested the above, maybe there is a typo somewhere. Anyway, I think it captures my mermaid graph and what I think you are going after. And hopefully this all helps. Happy to answer any questions.

marco-create commented 9 months ago

Thank you for your immense help. One (last?) question and please you can still find the code here.

Now, having slow and fast requests arriving on the same node, shouldn't make the queue longer? I'm referring to the 4 and 5 columns in the data:

Why it's always 0 (the 4th) or 1 (the 5th)? Shouldn't be a overloading of requests when the fast requests get injected into the system? Or becasue of the FIFO system we don't see an overloading of requests?

Thanks!

djordon commented 9 months ago

Hmmm, I believe it's due to the queue's service function. The mean rate of your fast arrivals is around one over a thousand, whereas your service function operates at a rate of approximately one over a million. You'll have to use a different service function (or different arrival functions) in order to overload the system.