Skip to content

Commit 8a6f17d

Browse files
Merge pull request #173 from CiwPython/update-state-tracking
Update state tracking
2 parents 513550b + 923399e commit 8a6f17d

4 files changed

Lines changed: 201 additions & 20 deletions

File tree

ciw/tests/test_state_tracker.py

Lines changed: 112 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -736,6 +736,118 @@ def test_one_node_deterministic_nodepopulation(self):
736736
]
737737
self.assertEqual(Q.statetracker.history, expected_history)
738738

739+
740+
def test_two_node_deterministic_nodepopulationsubset(self):
741+
N = ciw.create_network(
742+
arrival_distributions=[ciw.dists.Deterministic(0.31), ciw.dists.Deterministic(0.71)],
743+
service_distributions=[ciw.dists.Deterministic(1), ciw.dists.Deterministic(1)],
744+
routing=[[0.0, 1.0], [0.0, 0.0]],
745+
number_of_servers=[1, 1]
746+
)
747+
748+
# First with Both nodes
749+
B = ciw.trackers.NodePopulationSubset([0, 1])
750+
Q = ciw.Simulation(N, tracker=B, exact=26)
751+
Q.simulate_until_max_time(4.5)
752+
expected_history = [
753+
[Decimal('0.0'), (0, 0)],
754+
[Decimal('0.31'), (1, 0)],
755+
[Decimal('0.62'), (2, 0)],
756+
[Decimal('0.71'), (2, 1)],
757+
[Decimal('0.93'), (3, 1)],
758+
[Decimal('1.24'), (4, 1)],
759+
[Decimal('1.31'), (3, 2)],
760+
[Decimal('1.42'), (3, 3)],
761+
[Decimal('1.55'), (4, 3)],
762+
[Decimal('1.71'), (4, 2)],
763+
[Decimal('1.86'), (5, 2)],
764+
[Decimal('2.13'), (5, 3)],
765+
[Decimal('2.17'), (6, 3)],
766+
[Decimal('2.31'), (5, 4)],
767+
[Decimal('2.48'), (6, 4)],
768+
[Decimal('2.71'), (6, 3)],
769+
[Decimal('2.79'), (7, 3)],
770+
[Decimal('2.84'), (7, 4)],
771+
[Decimal('3.10'), (8, 4)],
772+
[Decimal('3.31'), (7, 5)],
773+
[Decimal('3.41'), (8, 5)],
774+
[Decimal('3.55'), (8, 6)],
775+
[Decimal('3.71'), (8, 5)],
776+
[Decimal('3.72'), (9, 5)],
777+
[Decimal('4.03'), (10, 5)],
778+
[Decimal('4.26'), (10, 6)],
779+
[Decimal('4.31'), (9, 7)],
780+
[Decimal('4.34'), (10, 7)]
781+
]
782+
self.assertEqual(Q.statetracker.history, expected_history)
783+
784+
# First with then 1st only
785+
B = ciw.trackers.NodePopulationSubset([0])
786+
Q = ciw.Simulation(N, tracker=B, exact=26)
787+
Q.simulate_until_max_time(4.5)
788+
expected_history = [
789+
[Decimal('0.0'), (0,)],
790+
[Decimal('0.31'), (1,)],
791+
[Decimal('0.62'), (2,)],
792+
[Decimal('0.93'), (3,)],
793+
[Decimal('1.24'), (4,)],
794+
[Decimal('1.31'), (3,)],
795+
[Decimal('1.55'), (4,)],
796+
[Decimal('1.86'), (5,)],
797+
[Decimal('2.17'), (6,)],
798+
[Decimal('2.31'), (5,)],
799+
[Decimal('2.48'), (6,)],
800+
[Decimal('2.79'), (7,)],
801+
[Decimal('3.10'), (8,)],
802+
[Decimal('3.31'), (7,)],
803+
[Decimal('3.41'), (8,)],
804+
[Decimal('3.72'), (9,)],
805+
[Decimal('4.03'), (10,)],
806+
[Decimal('4.31'), (9,)],
807+
[Decimal('4.34'), (10,)]
808+
]
809+
self.assertEqual(Q.statetracker.history, expected_history)
810+
811+
# Then 2nd only
812+
B = ciw.trackers.NodePopulationSubset([1])
813+
Q = ciw.Simulation(N, tracker=B, exact=26)
814+
Q.simulate_until_max_time(4.5)
815+
expected_history = [
816+
[Decimal('0.0'), (0,)],
817+
[Decimal('0.71'), (1,)],
818+
[Decimal('1.31'), (2,)],
819+
[Decimal('1.42'), (3,)],
820+
[Decimal('1.71'), (2,)],
821+
[Decimal('2.13'), (3,)],
822+
[Decimal('2.31'), (4,)],
823+
[Decimal('2.71'), (3,)],
824+
[Decimal('2.84'), (4,)],
825+
[Decimal('3.31'), (5,)],
826+
[Decimal('3.55'), (6,)],
827+
[Decimal('3.71'), (5,)],
828+
[Decimal('4.26'), (6,)],
829+
[Decimal('4.31'), (7,)]
830+
]
831+
self.assertEqual(Q.statetracker.history, expected_history)
832+
833+
def test_no_state_change_when_blocking_subset(self):
834+
N = ciw.create_network(
835+
arrival_distributions=[ciw.dists.Deterministic(1), ciw.dists.NoArrivals()],
836+
service_distributions=[ciw.dists.Deterministic(0.1), ciw.dists.Deterministic(1.2)],
837+
number_of_servers=[1, 1],
838+
routing=[[0.0, 1.0], [0.0, 0.0]],
839+
queue_capacities=[float('Inf'), 0]
840+
)
841+
B = ciw.trackers.NodePopulationSubset([1])
842+
Q = ciw.Simulation(N, tracker=B, exact=26)
843+
Q.simulate_until_max_time(15.5)
844+
expected_history = [
845+
[Decimal('0.0'), (0,)],
846+
[Decimal('1.1'), (1,)]
847+
]
848+
self.assertEqual(Q.statetracker.history, expected_history)
849+
850+
739851
def test_one_node_deterministic_nodeclassmatrix(self):
740852
N = ciw.create_network(
741853
arrival_distributions=[ciw.dists.Sequential([1.5, 0.3, 2.4, 1.1])],
@@ -795,10 +907,8 @@ def test_track_history_two_node_two_class(self):
795907
[0.00, 0],
796908
[0.60, 1],
797909
[1.09, 2],
798-
[1.32, 2],
799910
[1.64, 3],
800911
[1.96, 2],
801-
[2.67, 2],
802912
[2.84, 3],
803913
[3.39, 4],
804914
[3.41, 5],
@@ -822,10 +932,8 @@ def test_track_history_two_node_two_class(self):
822932
[0.00, (0, 0)],
823933
[0.60, (0, 1)],
824934
[1.09, (0, 2)],
825-
[1.32, (0, 2)],
826935
[1.64, (0, 3)],
827936
[1.96, (0, 2)],
828-
[2.67, (0, 2)],
829937
[2.84, (1, 2)],
830938
[3.39, (1, 3)],
831939
[3.41, (2, 3)],
@@ -849,10 +957,8 @@ def test_track_history_two_node_two_class(self):
849957
[0.00, ((0, 0), (0, 0))],
850958
[0.60, ((0, 0), (0, 1))],
851959
[1.09, ((0, 0), (1, 1))],
852-
[1.32, ((0, 0), (1, 1))],
853960
[1.64, ((0, 0), (1, 2))],
854961
[1.96, ((0, 0), (0, 2))],
855-
[2.67, ((0, 0), (0, 2))],
856962
[2.84, ((0, 1), (0, 2))],
857963
[3.39, ((0, 1), (0, 3))],
858964
[3.41, ((0, 2), (0, 3))],

ciw/trackers/state_tracker.py

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ def initialise(self, simulation):
1010
"""
1111
self.simulation = simulation
1212
self.state = None
13-
self.history = []
14-
self.timestamp()
13+
self.history = [[self.simulation.current_time, self.hash_state()]]
1514

1615
def change_state_accept(self, node_id, cust_clss):
1716
"""
@@ -38,7 +37,9 @@ def hash_state(self):
3837
return None
3938

4039
def timestamp(self):
41-
self.history.append([self.simulation.current_time, self.hash_state()])
40+
current_hash_state = self.hash_state()
41+
if current_hash_state != self.history[-1][1]:
42+
self.history.append([self.simulation.current_time, current_hash_state])
4243

4344
def state_probabilities(self, observation_period=(0, float("Inf"))):
4445
"""
@@ -105,8 +106,7 @@ def initialise(self, simulation):
105106
"""
106107
self.simulation = simulation
107108
self.state = 0
108-
self.history = []
109-
self.timestamp()
109+
self.history = [[self.simulation.current_time, self.hash_state()]]
110110

111111
def change_state_accept(self, node_id, cust_clss):
112112
"""
@@ -149,8 +149,7 @@ def initialise(self, simulation):
149149
self.simulation = simulation
150150
self.state = [0 for i in range(
151151
self.simulation.network.number_of_nodes)]
152-
self.history = []
153-
self.timestamp()
152+
self.history = [[self.simulation.current_time, self.hash_state()]]
154153

155154
def change_state_accept(self, node_id, cust_clss):
156155
"""
@@ -177,6 +176,62 @@ def hash_state(self):
177176
return tuple(self.state)
178177

179178

179+
class NodePopulationSubset(StateTracker):
180+
"""
181+
The node population tracker records the number of customers at each node
182+
from a given set of observed nodes.
183+
184+
Example:
185+
(3, 1)
186+
This denotes 3 customers at the first node, and 1 customer at the
187+
second node.
188+
"""
189+
def __init__(self, observed_nodes):
190+
"""
191+
Pre-initialises the object with keyward `observed_nodes`
192+
"""
193+
self.observed_nodes = observed_nodes
194+
195+
def initialise(self, simulation):
196+
"""
197+
Initialises the state tracker class.
198+
"""
199+
self.simulation = simulation
200+
self.state = [0 for i in self.observed_nodes]
201+
self.history = [[self.simulation.current_time, self.hash_state()]]
202+
203+
def change_state_accept(self, node_id, cust_clss):
204+
"""
205+
Changes the state of the system when a customer is accepted.
206+
"""
207+
if node_id-1 in self.observed_nodes:
208+
state_index = self.observed_nodes.index(node_id-1)
209+
self.state[state_index] += 1
210+
211+
def change_state_block(self, node_id, destination, cust_clss):
212+
"""
213+
Changes the state of the system when a customer gets blocked.
214+
"""
215+
pass
216+
217+
def change_state_release(self, node_id, destination, cust_clss, blocked):
218+
"""
219+
Changes the state of the system when a customer is released.
220+
"""
221+
if node_id-1 in self.observed_nodes:
222+
state_index = self.observed_nodes.index(node_id-1)
223+
self.state[state_index] -= 1
224+
225+
def hash_state(self):
226+
"""
227+
Returns a hashable state.
228+
"""
229+
return tuple(self.state)
230+
231+
232+
233+
234+
180235
class NodeClassMatrix(StateTracker):
181236
"""
182237
The node-class matrix tracker records the number of customers of each
@@ -197,8 +252,7 @@ def initialise(self, simulation):
197252
self.state = [[0 for cls in range(
198253
self.simulation.network.number_of_classes)] for i in range(
199254
self.simulation.network.number_of_nodes)]
200-
self.history = []
201-
self.timestamp()
255+
self.history = [[self.simulation.current_time, self.hash_state()]]
202256

203257
def change_state_accept(self, node_id, cust_clss):
204258
"""
@@ -243,8 +297,7 @@ def initialise(self, simulation):
243297
self.simulation = simulation
244298
self.state = [[0, 0] for i in range(
245299
self.simulation.network.number_of_nodes)]
246-
self.history = []
247-
self.timestamp()
300+
self.history = [[self.simulation.current_time, self.hash_state()]]
248301

249302
def change_state_accept(self, node_id, cust_clss):
250303
"""
@@ -301,8 +354,7 @@ def initialise(self, simulation):
301354
self.simulation.network.number_of_nodes)], [0 for i in range(
302355
self.simulation.network.number_of_nodes)]]
303356
self.increment = 1
304-
self.history = []
305-
self.timestamp()
357+
self.history = [[self.simulation.current_time, self.hash_state()]]
306358

307359
def change_state_accept(self, node_id, cust_clss):
308360
"""

docs/Guides/state_trackers.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,9 @@ From this we can obtain the proportion of time the system spend in each state::
5050

5151
So the system was in state :code:`0` (no individuals in the system) 55.4% of the time, in state :code:`1` (one individual in the system) 24.7% of the time, state :code:`2` (two individuals in the system) 13.1% of the time, and state :code:`3` (three individuals in the system) 6.8% of the time.
5252

53+
If a warm up and cool down time is required when calculating the state probabilities, we can put in an observation period. For example, if we with to find the proportion of time the system spend in each state, between dates :code:`50` and :code:`200`, then we can use the following::
54+
55+
>>> Q.statetracker.state_probabilities(observation_period=(50, 200)) # doctest:+SKIP
56+
57+
Note that different trackers represent different states in different ways, see :ref:`refs-statetrackers` for a list of implemented trackers.
5358

54-
Note that different trackers represent different states in different ways, see :ref:`refs-statetrackers` for a list of implemented trackers.

docs/Reference/state_trackers.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Currently Ciw has the following state trackers:
88

99
- :ref:`population`
1010
- :ref:`nodepop`
11+
- :ref:`nodepopsubset`
1112
- :ref:`nodeclssmatrix`
1213
- :ref:`naiveblock`
1314
- :ref:`matrixblock`
@@ -49,6 +50,24 @@ The Simulation object takes in the optional argument :code:`tracker` used as fol
4950
>>> Q = ciw.Simulation(N, tracker=ciw.trackers.NodePopulation()) # doctest:+SKIP
5051

5152

53+
.. _nodepopsubset:
54+
55+
--------------------------------
56+
The NodePopulationSubset Tracker
57+
--------------------------------
58+
59+
The NodePopulationSubset Tracker, similar to the NodePopulation Tracker, records the number of customers at each node. However this allows users to only track a subset of the nodes in the system.
60+
States take the form of list of numbers. An example of tracking a three node queueing network is shown below::
61+
62+
(2, 0, 5)
63+
64+
This denotes that there are two customers at the first observed node, no customers at the second observed node, and five customers at the third observed node.
65+
66+
The Simulation object takes in the optional argument :code:`tracker`, which takes an argument :code:`observed_nodes` a list of node numbers to observe, used as follows (observing the first, second, and fifth nodes)::
67+
68+
>>> Q = ciw.Simulation(N, tracker=ciw.trackers.NodePopulationSubset([0, 1, 4])) # doctest:+SKIP
69+
70+
5271
.. _nodeclssmatrix:
5372

5473
---------------------------

0 commit comments

Comments
 (0)