most-queue 2.6__py3-none-any.whl → 2.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
most_queue/sim/base.py CHANGED
@@ -4,13 +4,14 @@ Simulation model of QS GI/G/n/r and GI/G/n
4
4
 
5
5
  import time
6
6
 
7
- import numpy as np
8
7
  from colorama import Fore, Style, init
9
8
  from tqdm import tqdm
10
9
 
11
10
  from most_queue.constants import DEFAULT_NUM_STATES
12
11
  from most_queue.random.utils.create import create_distribution
13
12
  from most_queue.random.utils.load import calc_qs_load
13
+ from most_queue.sim.base_core import BaseSimulationCore
14
+ from most_queue.sim.utils.events import EventScheduler
14
15
  from most_queue.sim.utils.qs_queue import QsQueueDeque, QsQueueList
15
16
  from most_queue.sim.utils.servers import Server
16
17
  from most_queue.sim.utils.stats_update import refresh_moments_stat
@@ -20,7 +21,7 @@ from most_queue.structs import QueueResults
20
21
  init()
21
22
 
22
23
 
23
- class QsSim:
24
+ class QsSim(BaseSimulationCore):
24
25
  """
25
26
  Base class for Queueing System Simulator
26
27
  """
@@ -33,22 +34,19 @@ class QsSim:
33
34
  :param verbose: bool : whether to print detailed information during simulation
34
35
  :param buffer_type: str : type of the buffer, "list" or "deque"
35
36
  """
37
+ super().__init__()
38
+
36
39
  self.n = num_of_channels
37
40
  self.buffer = buffer
38
41
  self.verbose = verbose
39
42
 
40
- self.generator = np.random.default_rng()
41
-
42
43
  self.free_channels = self.n
43
44
  self.num_of_states = DEFAULT_NUM_STATES
44
45
  self.load = 0 # utilization factor
45
46
 
46
47
  # to track the length of the continuous channel occupancy period:
47
48
  self.start_busy = 0
48
- self.busy = [0, 0, 0]
49
- self.busy_moments = 0
50
49
 
51
- self.ttek = 0 # current simulation time
52
50
  self.total = 0
53
51
 
54
52
  self.w = [0, 0, 0, 0] # raw moments of waiting time in the QS
@@ -85,18 +83,14 @@ class QsSim:
85
83
  self.is_set_source_params = False
86
84
  self.is_set_server_params = False
87
85
 
88
- self.time_spent = 0
89
-
90
86
  self.zero_wait_arrivals_num = 0
91
87
 
92
- # Cache for server with minimum time
93
- self._min_server_time = 1e16
94
- self._min_server_idx = -1
95
- self._servers_time_changed = True
96
-
97
88
  # Set of free server indices for O(1) access
98
89
  self._free_servers = set(range(self.n))
99
90
 
91
+ # Event scheduler for managing simulation events
92
+ self.event_scheduler = EventScheduler()
93
+
100
94
  def set_sources(self, params, kendall_notation: str = "M"):
101
95
  """
102
96
  Specifies the type and parameters of source time distribution.
@@ -139,7 +133,7 @@ class QsSim:
139
133
  )
140
134
  for _i in range(self.n)
141
135
  ]
142
- self._servers_time_changed = True
136
+ self._mark_servers_time_changed()
143
137
  self._free_servers = set(range(self.n))
144
138
 
145
139
  def calc_load(self) -> float:
@@ -180,7 +174,7 @@ class QsSim:
180
174
  self.servers[server_idx].start_service(tsk, self.ttek, is_warm_start)
181
175
  self._free_servers.remove(server_idx)
182
176
  self.free_channels -= 1
183
- self._servers_time_changed = True
177
+ self._mark_servers_time_changed()
184
178
 
185
179
  # Проверям, не наступил ли ПНЗ:
186
180
  if self.free_channels == 0:
@@ -199,6 +193,10 @@ class QsSim:
199
193
  if new_tsk is None:
200
194
  new_tsk = Task(self.ttek)
201
195
  new_tsk.start_waiting_time = self.ttek
196
+ else:
197
+ # If task already exists but start_waiting_time is not set, set it now
198
+ if new_tsk.start_waiting_time < 0:
199
+ new_tsk.start_waiting_time = self.ttek
202
200
 
203
201
  if self.buffer is None: # queue length is not specified, i.e. infinite queue
204
202
 
@@ -220,11 +218,8 @@ class QsSim:
220
218
  """
221
219
 
222
220
  self.arrived += 1
223
- # Ensure p array is large enough
224
- if self.in_sys >= len(self.p):
225
- # Extend p array if needed
226
- self.p.extend([0.0] * (self.in_sys - len(self.p) + 1))
227
- self.p[self.in_sys] += self.arrival_time - self.ttek
221
+ # Update state probabilities
222
+ self._update_state_probs(self.ttek, self.arrival_time, self.in_sys)
228
223
 
229
224
  if moment:
230
225
  self.ttek = moment
@@ -258,14 +253,11 @@ class QsSim:
258
253
  """
259
254
  time_to_end = self.servers[c].time_to_end_service
260
255
  end_ts = self.servers[c].end_service()
261
- self._servers_time_changed = True
256
+ self._mark_servers_time_changed()
262
257
  self._free_servers.add(c) # Server is now free
263
258
 
264
- # Ensure p array is large enough
265
- if self.in_sys >= len(self.p):
266
- # Extend p array if needed
267
- self.p.extend([0.0] * (self.in_sys - len(self.p) + 1))
268
- self.p[self.in_sys] += time_to_end - self.ttek
259
+ # Update state probabilities
260
+ self._update_state_probs(self.ttek, time_to_end, self.in_sys)
269
261
 
270
262
  self.ttek = time_to_end
271
263
  self.served += 1
@@ -326,23 +318,154 @@ class QsSim:
326
318
  self._servers_time_changed = False
327
319
  return self._min_server_idx, self._min_server_time
328
320
 
329
- def run_one_step(self) -> None:
321
+ def _update_state_probs(self, old_time: float, new_time: float, state: int):
330
322
  """
331
- Execute one step of the simulation (either an arrival or service completion event).
323
+ Update state probabilities array.
324
+
325
+ Args:
326
+ old_time: Previous time
327
+ new_time: New time
328
+ state: Current system state
332
329
  """
330
+ # Ensure p array is large enough
331
+ while state >= len(self.p):
332
+ # Extend p array in chunks to avoid frequent reallocations
333
+ self.p.extend([0.0] * min(1000, max(1, len(self.p) // 10)))
334
+ self.p[state] += new_time - old_time
333
335
 
334
- num_of_server_earlier, serv_earl = self._get_min_server_time()
336
+ # Hook methods for customization
337
+ def _before_arrival(self, moment: float = None, ts=None):
338
+ """
339
+ Hook called before arrival event is processed.
340
+ Override this to add custom logic before arrival.
335
341
 
336
- # Global warm-up is set. Need to track
337
- # including the moment of warm-up end
338
- times = [serv_earl, self.arrival_time]
339
- min_time_num = np.argmin(times)
340
- if min_time_num == 0:
341
- # Serving
342
- self.serving(num_of_server_earlier)
343
- else:
344
- # Arrival
342
+ Args:
343
+ moment: Optional specific arrival moment
344
+ ts: Optional task object
345
+ """
346
+ # Override in subclasses if needed
347
+
348
+ def _after_arrival(self, moment: float = None, ts=None):
349
+ """
350
+ Hook called after arrival event is processed.
351
+ Override this to add custom logic after arrival.
352
+
353
+ Args:
354
+ moment: Optional specific arrival moment
355
+ ts: Optional task object
356
+ """
357
+ # Override in subclasses if needed
358
+
359
+ def _before_serving(self, channel: int, is_network: bool = False):
360
+ """
361
+ Hook called before serving event is processed.
362
+ Override this to add custom logic before serving.
363
+
364
+ Args:
365
+ channel: Channel number that completed service
366
+ is_network: Whether this is part of a network simulation
367
+ """
368
+ # Override in subclasses if needed
369
+
370
+ def _after_serving(self, channel: int, task=None, is_network: bool = False):
371
+ """
372
+ Hook called after serving event is processed.
373
+ Override this to add custom logic after serving.
374
+
375
+ Args:
376
+ channel: Channel number that completed service
377
+ task: Task that completed service
378
+ is_network: Whether this is part of a network simulation
379
+ """
380
+ # Override in subclasses if needed
381
+
382
+ def _get_custom_events(self):
383
+ """
384
+ Get custom events for this simulator.
385
+ Override this method to register custom event types.
386
+
387
+ Returns:
388
+ dict: Dictionary mapping event_type -> event_time
389
+ """
390
+ return {}
391
+
392
+ def _handle_custom_event(self, event_type: str):
393
+ """
394
+ Handle a custom event.
395
+ Override this method to process custom events.
396
+
397
+ Args:
398
+ event_type: Type of the custom event
399
+ """
400
+ raise NotImplementedError(f"Custom event '{event_type}' not handled")
401
+
402
+ def _get_available_events(self):
403
+ """
404
+ Collect all available events (arrival, serving, custom).
405
+ Returns dictionary of event_type -> event_time.
406
+ """
407
+ events = {}
408
+
409
+ # Standard arrival event
410
+ if hasattr(self, "arrival_time") and self.arrival_time < float("inf"):
411
+ events["arrival"] = self.arrival_time
412
+
413
+ # Standard serving event (next service completion)
414
+ server_idx, serv_time = self._get_min_server_time()
415
+ if server_idx >= 0 and serv_time < float("inf"):
416
+ events["serving"] = serv_time
417
+
418
+ # Custom events from subclass
419
+ custom_events = self._get_custom_events()
420
+ events.update(custom_events)
421
+
422
+ return events
423
+
424
+ def _select_next_event(self):
425
+ """
426
+ Select the next event to process based on minimum time.
427
+
428
+ Returns:
429
+ tuple: (event_type, event_time) or (None, None) if no events
430
+ """
431
+ events = self._get_available_events()
432
+ if not events:
433
+ return None, None
434
+
435
+ event_type = min(events.keys(), key=lambda k: events[k])
436
+ return event_type, events[event_type]
437
+
438
+ def _execute_event(self, event_type: str):
439
+ """
440
+ Execute the specified event.
441
+
442
+ Args:
443
+ event_type: Type of event to execute
444
+ """
445
+ if event_type == "arrival":
446
+ self._before_arrival()
345
447
  self.arrival()
448
+ self._after_arrival()
449
+ elif event_type == "serving":
450
+ server_idx, _ = self._get_min_server_time()
451
+ self._before_serving(server_idx)
452
+ task = self.serving(server_idx)
453
+ self._after_serving(server_idx, task)
454
+ else:
455
+ # Custom event
456
+ self._handle_custom_event(event_type)
457
+
458
+ def run_one_step(self) -> None:
459
+ """
460
+ Execute one step of the simulation using event-based approach.
461
+ Automatically handles arrival, serving, and custom events.
462
+ """
463
+ event_type, _event_time = self._select_next_event()
464
+
465
+ if event_type is None:
466
+ raise RuntimeError("No events available to process")
467
+
468
+ self._execute_event(event_type)
346
469
 
347
470
  def run(self, total_served: int) -> QueueResults:
348
471
  """
@@ -386,32 +509,41 @@ class QsSim:
386
509
 
387
510
  return QueueResults(v=v, w=w, p=p, duration=self.time_spent, utilization=self.calc_load())
388
511
 
389
- def refresh_busy_stat(self, new_a: float) -> None:
512
+ def refresh_busy_stat(self, new_a: float, count: int = None) -> None:
390
513
  """
391
514
  Update statistics of the busy period.
392
515
 
393
516
  Args:
394
517
  new_a: New busy period duration to include in statistics.
518
+ count: Number of busy periods (if None, uses self.busy_moments)
395
519
  """
396
- self.busy = refresh_moments_stat(self.busy, new_a, self.busy_moments)
520
+ if count is None:
521
+ count = self.busy_moments
522
+ super().refresh_busy_stat(new_a, count)
397
523
 
398
- def refresh_v_stat(self, new_a: float) -> None:
524
+ def refresh_v_stat(self, new_a: float, count: int = None) -> None:
399
525
  """
400
526
  Update statistics of sojourn times.
401
527
 
402
528
  Args:
403
529
  new_a: New sojourn time value to include in statistics.
530
+ count: Number of served tasks (if None, uses self.served)
404
531
  """
405
- self.v = refresh_moments_stat(self.v, new_a, self.served)
532
+ if count is None:
533
+ count = self.served
534
+ self.v = refresh_moments_stat(self.v, new_a, count)
406
535
 
407
- def refresh_w_stat(self, new_a: float) -> None:
536
+ def refresh_w_stat(self, new_a: float, count: int = None) -> None:
408
537
  """
409
538
  Update statistics of wait times.
410
539
 
411
540
  Args:
412
541
  new_a: New waiting time value to include in statistics.
542
+ count: Number of taken tasks (if None, uses self.taked)
413
543
  """
414
- self.w = refresh_moments_stat(self.w, new_a, self.taked)
544
+ if count is None:
545
+ count = self.taked
546
+ self.w = refresh_moments_stat(self.w, new_a, count)
415
547
 
416
548
  def get_p(self) -> list[float]:
417
549
  """
@@ -0,0 +1,89 @@
1
+ """
2
+ Base simulation core with common functionality for all simulators
3
+ """
4
+
5
+ import numpy as np
6
+
7
+ from most_queue.sim.utils.stats_update import refresh_moments_stat
8
+
9
+
10
+ class BaseSimulationCore:
11
+ """
12
+ Base class for queueing system simulators.
13
+ Contains common attributes and methods shared across different simulator types.
14
+ """
15
+
16
+ def __init__(self):
17
+ """Initialize the base simulation core."""
18
+ self.ttek = 0 # current simulation time
19
+ self.generator = np.random.default_rng()
20
+ self.time_spent = 0
21
+
22
+ # Statistics for busy periods
23
+ self.busy = [0, 0, 0]
24
+ self.busy_moments = 0
25
+
26
+ # Cache for server with minimum time
27
+ self._min_server_time = 1e16
28
+ self._min_server_idx = -1
29
+ self._servers_time_changed = True
30
+
31
+ def refresh_busy_stat(self, new_a: float, count: int = None) -> None:
32
+ """
33
+ Update statistics of the busy period.
34
+
35
+ Args:
36
+ new_a: New busy period duration to include in statistics.
37
+ count: Number of busy periods (if None, uses self.busy_moments)
38
+ """
39
+ if count is None:
40
+ count = self.busy_moments
41
+ self.busy = refresh_moments_stat(self.busy, new_a, count)
42
+
43
+ def refresh_v_stat(self, new_a: float, count: int = None) -> None:
44
+ """
45
+ Update statistics of sojourn times.
46
+
47
+ Args:
48
+ new_a: New sojourn time value to include in statistics.
49
+ count: Number of served tasks (if None, should be overridden by subclass)
50
+ """
51
+ # This is a placeholder - subclasses should override with proper count
52
+ # Initialize v if not exists
53
+ if not hasattr(self, "v"):
54
+ self.v = [0, 0, 0, 0]
55
+ if count is not None:
56
+ self.v = refresh_moments_stat(self.v, new_a, count)
57
+
58
+ def refresh_w_stat(self, new_a: float, count: int = None) -> None:
59
+ """
60
+ Update statistics of wait times.
61
+
62
+ Args:
63
+ new_a: New waiting time value to include in statistics.
64
+ count: Number of taken tasks (if None, should be overridden by subclass)
65
+ """
66
+ # This is a placeholder - subclasses should override with proper count
67
+ # Initialize w if not exists
68
+ if not hasattr(self, "w"):
69
+ self.w = [0, 0, 0, 0]
70
+ if count is not None:
71
+ self.w = refresh_moments_stat(self.w, new_a, count)
72
+
73
+ def _get_min_server_time(self):
74
+ """
75
+ Get server with minimum time to end service.
76
+ Uses caching to avoid O(n) search on every call.
77
+
78
+ This is a base implementation that should be overridden
79
+ if the simulator uses a different server structure.
80
+
81
+ Returns:
82
+ tuple: (server_index, min_time) or (-1, float('inf')) if no servers
83
+ """
84
+ # Base implementation - subclasses should override
85
+ return -1, float("inf")
86
+
87
+ def _mark_servers_time_changed(self):
88
+ """Mark that server times have changed and cache needs refresh."""
89
+ self._servers_time_changed = True
most_queue/sim/batch.py CHANGED
@@ -61,7 +61,7 @@ class QueueingSystemBatchSim(QsSim):
61
61
  self.servers[server_idx].start_service(Task(ttek), ttek, False)
62
62
  self._free_servers.remove(server_idx)
63
63
  self.free_channels -= 1
64
- self._servers_time_changed = True
64
+ self._mark_servers_time_changed()
65
65
 
66
66
  # Check if busy period has started:
67
67
  if self.free_channels == 0:
@@ -85,12 +85,8 @@ class QueueingSystemBatchSim(QsSim):
85
85
 
86
86
  for _tsk in range(batch_size):
87
87
  self.arrived += 1
88
- # Ensure p array is large enough before accessing
89
- # We need space for current in_sys and potentially more
90
- while self.in_sys >= len(self.p):
91
- # Extend p array in chunks to avoid frequent reallocations
92
- self.p.extend([0.0] * min(1000, max(1, len(self.p) // 10)))
93
- self.p[self.in_sys] += self.arrival_time - self.ttek
88
+ # Update state probabilities
89
+ self._update_state_probs(self.ttek, self.arrival_time, self.in_sys)
94
90
  self.in_sys += 1
95
91
 
96
92
  if self.free_channels == 0:
@@ -73,7 +73,8 @@ class QueueingFiniteSourceSim(QsSim):
73
73
  """
74
74
 
75
75
  self.arrived += 1
76
- self.p[self.in_sys] += self.arrival_times[self.arrived_num] - self.ttek
76
+ # Update state probabilities
77
+ self._update_state_probs(self.ttek, self.arrival_times[self.arrived_num], self.in_sys)
77
78
  self.sources_left -= 1
78
79
  self.in_sys += 1
79
80
  self.ttek = self.arrival_times[self.arrived_num]
@@ -85,11 +85,8 @@ class ForkJoinSim(QsSim):
85
85
  """
86
86
 
87
87
  self.arrived += 1
88
- # Ensure p array is large enough
89
- while self.in_sys >= len(self.p):
90
- # Extend p array in chunks to avoid frequent reallocations
91
- self.p.extend([0.0] * min(1000, max(1, len(self.p) // 10)))
92
- self.p[self.in_sys] += self.arrival_time - self.ttek
88
+ # Update state probabilities
89
+ self._update_state_probs(self.ttek, self.arrival_time, self.in_sys)
93
90
  self.ttek = self.arrival_time
94
91
  self.arrival_time = self.ttek + self.source.generate()
95
92
 
@@ -122,7 +119,7 @@ class ForkJoinSim(QsSim):
122
119
  self.servers[i].start_service(t.subtasks[i], self.ttek)
123
120
  self._free_servers.discard(i)
124
121
  self.free_channels -= 1
125
- self._servers_time_changed = True
122
+ self._mark_servers_time_changed()
126
123
  else:
127
124
  self.queues[i].append(t.subtasks[i])
128
125
 
@@ -189,14 +186,11 @@ class ForkJoinSim(QsSim):
189
186
  :param c: int : number of channel where service is completed.
190
187
  """
191
188
  time_to_end = self.servers[c].time_to_end_service
192
- # Ensure p array is large enough
193
- while self.in_sys >= len(self.p):
194
- # Extend p array in chunks to avoid frequent reallocations
195
- self.p.extend([0.0] * min(1000, max(1, len(self.p) // 10)))
196
- self.p[self.in_sys] += time_to_end - self.ttek
189
+ # Update state probabilities
190
+ self._update_state_probs(self.ttek, time_to_end, self.in_sys)
197
191
  end_ts = self.servers[c].end_service()
198
192
  self._free_servers.add(c) # Server is now free
199
- self._servers_time_changed = True
193
+ self._mark_servers_time_changed()
200
194
  self.ttek = time_to_end
201
195
  self.served_subtask_in_task[end_ts.task_id] += 1
202
196
  self.total += 1
@@ -17,6 +17,13 @@ class ForkJoinSimDelta(ForkJoinSim):
17
17
  Simulation of ForkJoin queue with delta.
18
18
  """
19
19
 
20
+ def _handle_custom_event(self, event_type):
21
+ """
22
+ Handle custom events (none needed for this simulator).
23
+ """
24
+ # This simulator doesn't use custom events
25
+ raise NotImplementedError(f"Custom event '{event_type}' not supported by ForkJoinSimDelta")
26
+
20
27
  def __init__(
21
28
  self,
22
29
  num_of_channels,
@@ -52,17 +52,46 @@ class ImpatientQueueSim(QsSim):
52
52
 
53
53
  self.impatience = create_distribution(params, kendall_notation, self.generator)
54
54
 
55
+ def _get_custom_events(self):
56
+ """
57
+ Get custom events (impatient task drops).
58
+ """
59
+ events = {}
60
+ if self.queue.size() > 0:
61
+ min_leave_time = float("inf")
62
+ for tsk in self.queue.queue:
63
+ if tsk.moment_to_leave < min_leave_time:
64
+ min_leave_time = tsk.moment_to_leave
65
+ if min_leave_time < float("inf"):
66
+ events["task_drop"] = min_leave_time
67
+ return events
68
+
69
+ def _handle_custom_event(self, event_type: str):
70
+ """
71
+ Handle custom events (impatient task drops).
72
+ """
73
+ if event_type == "task_drop":
74
+ # Find the task with minimum leave time
75
+ num_of_task_earlier = -1
76
+ moment_to_leave_earlier = float("inf")
77
+ for i, tsk in enumerate(self.queue.queue):
78
+ if tsk.moment_to_leave < moment_to_leave_earlier:
79
+ moment_to_leave_earlier = tsk.moment_to_leave
80
+ num_of_task_earlier = i
81
+
82
+ if num_of_task_earlier >= 0:
83
+ self.drop_task(num_of_task_earlier, moment_to_leave_earlier)
84
+ else:
85
+ super()._handle_custom_event(event_type)
86
+
55
87
  def arrival(self, moment=None, ts=None):
56
88
  """
57
89
  Actions on arrival of a task.
58
90
  """
59
91
 
60
92
  self.arrived += 1
61
- # Ensure p array is large enough
62
- while self.in_sys >= len(self.p):
63
- # Extend p array in chunks to avoid frequent reallocations
64
- self.p.extend([0.0] * min(1000, max(1, len(self.p) // 10)))
65
- self.p[self.in_sys] += self.arrival_time - self.ttek
93
+ # Update state probabilities
94
+ self._update_state_probs(self.ttek, self.arrival_time, self.in_sys)
66
95
 
67
96
  self.in_sys += 1
68
97
  self.ttek = self.arrival_time
@@ -92,7 +121,7 @@ class ImpatientQueueSim(QsSim):
92
121
  self.servers[server_idx].start_service(ImpatientTask(self.ttek, moment_to_leave), self.ttek, False)
93
122
  self._free_servers.remove(server_idx)
94
123
  self.free_channels -= 1
95
- self._servers_time_changed = True
124
+ self._mark_servers_time_changed()
96
125
 
97
126
  # Проверям, не наступил ли ПНЗ:
98
127
  if self.free_channels == 0:
@@ -103,11 +132,8 @@ class ImpatientQueueSim(QsSim):
103
132
  """
104
133
  Drop a task from the queue and update statistics.
105
134
  """
106
- # Ensure p array is large enough
107
- while self.in_sys >= len(self.p):
108
- # Extend p array in chunks to avoid frequent reallocations
109
- self.p.extend([0.0] * min(1000, max(1, len(self.p) // 10)))
110
- self.p[self.in_sys] += moment_to_leave_earlier - self.ttek
135
+ # Update state probabilities
136
+ self._update_state_probs(self.ttek, moment_to_leave_earlier, self.in_sys)
111
137
  self.ttek = moment_to_leave_earlier
112
138
 
113
139
  # Remove task from queue - need to handle deque properly
@@ -132,37 +158,8 @@ class ImpatientQueueSim(QsSim):
132
158
  end_ts.wait_time = self.ttek - end_ts.arr_time
133
159
  self.refresh_w_stat(end_ts.wait_time)
134
160
 
135
- def run_one_step(self):
136
- """
137
- Run one step of the simulation.
138
- """
139
- # Use optimized method from base class
140
- num_of_server_earlier, serv_earl = self._get_min_server_time()
141
-
142
- num_of_task_earlier = -1
143
- moment_to_leave_earlier = 1e10
144
- if self.queue.size() > 0:
145
- for i, tsk in enumerate(self.queue.queue):
146
- if tsk.moment_to_leave < moment_to_leave_earlier:
147
- moment_to_leave_earlier = tsk.moment_to_leave
148
- num_of_task_earlier = i
149
-
150
- # Key moment:
151
-
152
- if self.arrival_time < serv_earl:
153
- if self.arrival_time < moment_to_leave_earlier:
154
- self.arrival()
155
- elif num_of_task_earlier >= 0:
156
- self.drop_task(num_of_task_earlier, moment_to_leave_earlier)
157
- else:
158
- self.arrival()
159
- else:
160
- if serv_earl < moment_to_leave_earlier and num_of_task_earlier >= 0:
161
- self.serving(num_of_server_earlier)
162
- elif num_of_task_earlier >= 0:
163
- self.drop_task(num_of_task_earlier, moment_to_leave_earlier)
164
- else:
165
- self.serving(num_of_server_earlier)
161
+ # run_one_step is now inherited from base class and uses event-based approach
162
+ # Custom events (task drops) are handled via _get_custom_events() and _handle_custom_event()
166
163
 
167
164
  def __str__(self, is_short=False):
168
165
  """