Coverage for moptipyapps / prodsched / simulation.py: 94%

237 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2025-12-30 03:25 +0000

1""" 

2A simulator for production scheduling. 

3 

4For simulation a production system, we can build on the class 

5:class:`~Simulation`. This base class offers the support to implement almost 

6arbitrarily complex production system scheduling logic. 

7The simulations here a fully deterministic and execute a given MFC scenario 

8given as an :mod:`~moptipyapps.prodsched.instance`, i.e., an object of type 

9:class:`~moptipyapps.prodsched.instance.Instance`. 

10 

11The :class:`~Simulation` class offers a core backbone of a priority-queue 

12based discrete event simulation. These events are strictly related to the 

13production scheduling task, which allows for an efficient implementation. 

14The :class:`~Simulation` is driven by the data of an 

15:mod:`~moptipyapps.prodsched.instance`. This instance prescribes when new 

16demands (:class:`~moptipyapps.prodsched.instance.Demand`) enter the system, 

17how long certain production steps take, and which route each product type 

18takes through the system, i.e., by which work stations it is processed in 

19which order. 

20This is the core logic that drives the simulation. 

21 

22A simulation is executed by invoking the method :meth:`~Simulation.ctrl_run`. 

23Then, the event loop begins and it invokes the `event_*` methods as need be. 

24For example, when a customer demand for a certain product comes in or if some 

25units of a given product become available, the method 

26:meth:`~Simulation.event_product` is invoked. 

27You can overwrite this method to decide what to do in such cases. 

28For example, you could invoke :meth:`~Simulation.act_demand_satisfied` to mark 

29a :class:`~moptipyapps.prodsched.instance.Demand` as satisfied, you could 

30invoke :meth:`~Simulation.act_produce` to tell the factory to produce some 

31units of a given product, you could invoke 

32:meth:`~Simulation.act_store_in_warehouse` to store some units of a product in 

33the warehouse or use :meth:`~Simulation.act_take_from_warehouse` to take some 

34out. 

35In other words, in the `event_*` methods, you implement the logic, the 

36operating system for your factory. 

37Their default implementations in this class just produce product units on 

38demand and do not really perform predictive production. 

39 

40Each :class:`~Simulation` also needs an instance of :class:`~Listener`. 

41The :class:`~Listener` is informed about what happens and sees all the 

42production events. It is used to gather statistics and information -- 

43independently of how you implement the `event_*` methods. This allows us 

44to compare factories that run on very different logic. 

45 

46Simulations have three groups of methods: 

47 

48- Methods starting with `ctrl_*` are for starting and resetting the 

49 simulation so that it can be started again. You may override them if you 

50 have additional need for initialization or clean-up. 

51- Methods that start with `event_*` are methods that are invoked by the 

52 simulator to notify you about an event in the simulation. You can overwrite 

53 these methods to implement the logic of your production scheduling method. 

54- Methods that start with `act_*` are actions that you can invoke inside the 

55 `event_*` methods. The tell the simulator or stations what to do. 

56 

57An example of such specialized simulations is the 

58:class:`~moptipyapps.prodsched.rop_simulation.ROPSimulation`, 

59which simulates the behavior of a system that uses re-order points (ROPs) to 

60decide what to produce and when. 

61In such a simulation, the `event_*`-methods are overwritten to invoke the 

62`act_*`-methods according to their needs. 

63Here, in the base class :class:`~Simulation`, they are implemented such to 

64order the production of product units directly upon the arrival of customer 

65demands. In the :class:`~moptipyapps.prodsched.rop_simulation.ROPSimulation` 

66on the other hand, products are produced base on re-order points. 

67 

68 

69**`ctrl_*` Methods:** 

70 

71We have the following `ctrl_*` methods, which are invoked from outside to 

72start, stop, or reset the simulation. 

73 

74- :meth:`~Simulation.ctrl_run` runs the simulation. 

75- :meth:`~Simulation.ctrl_reset` resets the simulator so that we can start it 

76 again. If you want to re-use a simulation, you need to first invoke 

77 :meth:`~Simulation.ctrl_reset` to clear the internal state. 

78 

79 

80**`event_*` Methods:** 

81 

82We have the following `event_*` methods, which implement the core logic of 

83the factory. They can be overwritten to ralize different production 

84scenarios. 

85 

86- :meth:`~Simulation.event_product` is invoked by the simulation if one of the 

87 following three things happened: 

88 

89 1. An amount of a product has been produced (`amount > 0`). 

90 2. An amount of a product has been made available at the start of the 

91 simulation to form the initial amount in the warehouse 

92 (`amount > 0`). 

93 3. A customer demand for the product has appeared in the system. 

94 

95 In this method, you can store product into the warehouse, remove product 

96 from the warehouse, and/or mark a demand as completed. 

97 

98- :meth:`~Simulation.event_station` is invoked by the simulation if a work 

99 station became idle *and* at least one production :class:`~Job` is queued 

100 at the station. 

101 Now you can decide which of the queued to jobs to execute next by invoking 

102 :meth:`~Simulation.act_exec_job`. The job you pass into this method will 

103 then immediately begin production at the work station. 

104 

105 

106**`act_*` Methods:** 

107 

108The `act_*` methods are invoked from inside the `event_*` methods. 

109They cause the production system to perform certain actions. 

110The following `act_*` methods exist: 

111 

112- :meth:`~Simulation.act_store_in_warehouse` can be invoked from inside 

113 :meth:`~Simulation.event_product`. It tells the system to *add* a certain 

114 amount of units of a given product to the warehouse. 

115 Notice that these units of product must have come from somewhere. 

116 They could be the result of a completed production job or could have 

117 occurred at the simulation startup as initial warehouse contents. 

118 You cannot just "make up" new product units without violating the integrity 

119 of the simulation. 

120 

121- :meth:`~Simulation.act_take_from_warehouse` can be invoked from inside 

122 :meth:`~Simulation.event_product`. It tells the system to take a certain 

123 amount of units *out* of the warehouse. You cannot take out more units from 

124 the warehouse than currently stored inside it nor can you take out a 

125 negative amount of units. 

126 

127- :meth:`~Simulation.act_demand_satisfied` can be invoked from inside 

128 :meth:`~Simulation.event_product`. It tells the system that a certain 

129 :class:`~moptipyapps.prodsched.instance.Demand` has been fulfilled. 

130 If you do that, you must make sure to remove the corresponding amount of 

131 product units from the system. They could have just been produced or they 

132 could have been taken from the warehouse. However, demands can only be 

133 satisfied using actually existing units of product. If you just "make up" 

134 product units, you will destroy the integrity of the simulation. 

135 

136- :meth:`~Simulation.act_produce` can be invoked from inside 

137 :meth:`~Simulation.event_product`. This instructs the system to begin 

138 producing a certain amount of a given product. This will lead to the 

139 creation of a :class:`~Job` record. This record will enter the queue of 

140 the first work station that the product should pass through. It will appear 

141 in :meth:`~Simulation.event_station` once this work station gets idle (or 

142 right away, if it currently is idle). 

143 

144- :meth:`~Simulation.act_exec_job` is invoked from inside 

145 :meth:`~Simulation.event_station`. Basically, 

146 :meth:`~Simulation.event_station` gets called if there are one or multiple 

147 production :class:`~Job` records queued at a work station and the work 

148 station is idle (or becomes idle). Then, you can decide which of the jobs to 

149 begin working on next on that work station. You will pass the corresponding 

150 :class:`~Job` record to :meth:`~Simulation.act_exec_job`. 

151 Once that job is completed on the current work station, it will re-appear in 

152 the :meth:`~Simulation.event_station` invocation for the *next* work station 

153 it needs to pass through. Once it completes at its last work station, its 

154 produced :attr:`~Job.amount` of product :attr:`~Job.product_id` will appear 

155 in a :meth:`~Simulation.event_product` invocation for the corresponding 

156 product. 

157 

158Through this simple interface, we can control a relatively complex material 

159flow simulation. 

160In :mod:`~moptipyapps.prodsched.rop_simulation`, we extend this simulation 

161class by implementing a re-order point based approach. 

162Matter of fact, we can extend this basic simulation using all kinds of 

163production logic to drive our simulated factory. 

164 

165The question then is: If all these approaches overwrite the `event_*` methods 

166in different ways, how can we get a clear picture of their performance? 

167No problem: 

168For this purpose, the class :class:`~Listener` exists. 

169Its methods are automatically invoked by the simulation and allow us to track 

170exactly what happens and when, independently of the actual simulation logic. 

171The class :class:`~PrintingListener` implements these methods to print the 

172events to the standard output. 

173In :mod:`~moptipyapps.prodsched.statistics_collector`, we implement the class 

174:class:`~moptipyapps.prodsched.statistics_collector.StatisticsCollector` which 

175instead uses the events to fill a 

176:class:`~moptipyapps.prodsched.statistics.Statistics` record (see module 

177:mod:`~moptipyapps.prodsched.statistics`). 

178This record collects all the performance data that is relevant for judging the 

179efficiency of a production scheduling approach. 

180 

181**Examples:** 

182 

183Let's now look at some basic examples of the production scheduling / material 

184flow control simulation. 

185 

186Here we have a very easy production scheduling instance. 

187There is 1 product that passes through 2 stations. 

188First it passes through station 0, then through station 1. 

189The per-unit production time is always 10 time units on station 0 and 30 time 

190units on station 2. 

191There is one customer demand, for 10 units of this product, which enters the 

192system at time unit 20. 

193The warehouse is initially empty. 

194 

195>>> instance = Instance( 

196... name="test1", n_products=1, n_customers=1, n_stations=2, n_demands=1, 

197... time_end_warmup=10, time_end_measure=4000, 

198... routes=[[0, 1]], 

199... demands=[[0, 0, 0, 10, 20, 100]], 

200... warehous_at_t0=[0], 

201... station_product_unit_times=[[[10.0, 10000.0]], 

202... [[30.0, 10000.0]]]) 

203 

204The simulation will see that the customer demand for 10 units of product 0 

205appears at time unit 20. 

206It will issue a production order for these 10 units at station 0. 

207Since station 0 is not occupied, it can immediately begin with the production. 

208It will finish the production after 10*10 time units, i.e., at time unit 120. 

209The product is then routed to station 1, which is also idle and can 

210immediately begin producing. 

211It needs 10*30 time units, meaning that it finishes after 300 time units. 

212The demanded product amount is completed after 420 time units and the demand 0 

213can be fulfilled. 

214 

215>>> simulation = Simulation(instance, PrintingListener(print_time=False)) 

216>>> simulation.ctrl_run() 

217start 

218T=0.0: product=0, amount=0, in_warehouse=0, in_production=0, 0 pending demands 

219T=20.0! product=0, amount=0, in_warehouse=0, in_production=0,\ 

220 1 pending demands 

221T=20.0! station=0, 1 jobs queued 

222T=20.0! start j(id: 0, p: 0, am: 10, ar: 20, me: T, c: F, st: 20, sp: 0)\ 

223 at station 0 

224T=120.0! finished j(id: 0, p: 0, am: 10, ar: 20, me: T, c: F, st: 20, sp: 0)\ 

225 at station 0 

226T=120.0! station=1, 1 jobs queued 

227T=120.0! start j(id: 0, p: 0, am: 10, ar: 20, me: T, c: F, st: 120, sp: 1)\ 

228 at station 1 

229T=420.0! finished j(id: 0, p: 0, am: 10, ar: 20, me: T, c: T, st: 120, sp: 1)\ 

230 at station 1 

231T=420.0! product=0, amount=10, in_warehouse=0, in_production=0,\ 

232 1 pending demands 

233T=420.0! d(id: 0, p: 0, c: 0, am: 10, ar: 20, dl: 100, me: T) statisfied 

234T=420.0 -- finished 

235 

236>>> instance = Instance( 

237... name="test2", n_products=2, n_customers=1, n_stations=2, n_demands=3, 

238... time_end_warmup=21, time_end_measure=10000, 

239... routes=[[0, 1], [1, 0]], 

240... demands=[[0, 0, 1, 10, 20, 90], [1, 0, 0, 5, 22, 200], 

241... [2, 0, 1, 7, 30, 200]], 

242... warehous_at_t0=[2, 1], 

243... station_product_unit_times=[[[10.0, 50.0, 15.0, 100.0], 

244... [ 5.0, 20.0, 7.0, 35.0, 4.0, 50.0]], 

245... [[ 5.0, 24.0, 7.0, 80.0], 

246... [ 3.0, 21.0, 6.0, 50.0,]]]) 

247 

248>>> instance.name 

249'test2' 

250 

251>>> simulation = Simulation(instance, PrintingListener(print_time=False)) 

252>>> simulation.ctrl_run() 

253start 

254T=0.0: product=0, amount=2, in_warehouse=0, in_production=0, 0 pending demands 

255T=0.0: 2 units of product 0 in warehouse 

256T=0.0: product=1, amount=1, in_warehouse=0, in_production=0, 0 pending demands 

257T=0.0: 1 units of product 1 in warehouse 

258T=20.0: product=1, amount=0, in_warehouse=1, in_production=0,\ 

259 1 pending demands 

260T=20.0: station=1, 1 jobs queued 

261T=20.0: start j(id: 0, p: 1, am: 9, ar: 20, me: F, c: F, st: 20, sp: 0)\ 

262 at station 1 

263T=22.0! product=0, amount=0, in_warehouse=2, in_production=0,\ 

264 1 pending demands 

265T=22.0! station=0, 1 jobs queued 

266T=22.0! start j(id: 1, p: 0, am: 3, ar: 22, me: T, c: F, st: 22, sp: 0)\ 

267 at station 0 

268T=30.0! product=1, amount=0, in_warehouse=1, in_production=9,\ 

269 2 pending demands 

270T=52.0! finished j(id: 1, p: 0, am: 3, ar: 22, me: T, c: F, st: 22, sp: 0)\ 

271 at station 0 

272T=62.0: finished j(id: 0, p: 1, am: 9, ar: 20, me: F, c: F, st: 20, sp: 0)\ 

273 at station 1 

274T=62.0! station=1, 2 jobs queued 

275T=62.0! start j(id: 2, p: 1, am: 7, ar: 30, me: T, c: F, st: 30, sp: 0)\ 

276 at station 1 

277T=62.0! station=0, 1 jobs queued 

278T=62.0: start j(id: 0, p: 1, am: 9, ar: 20, me: F, c: F, st: 62, sp: 1)\ 

279 at station 0 

280T=95.0! finished j(id: 2, p: 1, am: 7, ar: 30, me: T, c: F, st: 30, sp: 0)\ 

281 at station 1 

282T=95.0! station=1, 1 jobs queued 

283T=95.0! start j(id: 1, p: 0, am: 3, ar: 22, me: T, c: F, st: 52, sp: 1)\ 

284 at station 1 

285T=107.0: finished j(id: 0, p: 1, am: 9, ar: 20, me: F, c: T, st: 62, sp: 1)\ 

286 at station 0 

287T=107.0! station=0, 1 jobs queued 

288T=107.0! start j(id: 2, p: 1, am: 7, ar: 30, me: T, c: F, st: 95, sp: 1)\ 

289 at station 0 

290T=107.0! product=1, amount=9, in_warehouse=1, in_production=7,\ 

291 2 pending demands 

292T=107.0: d(id: 0, p: 1, c: 0, am: 10, ar: 20, dl: 90, me: F) statisfied 

293T=107.0! 0 units of product 1 in warehouse 

294T=112.0! finished j(id: 1, p: 0, am: 3, ar: 22, me: T, c: T, st: 52, sp: 1)\ 

295 at station 1 

296T=112.0! product=0, amount=3, in_warehouse=2, in_production=0,\ 

297 1 pending demands 

298T=112.0! d(id: 1, p: 0, c: 0, am: 5, ar: 22, dl: 200, me: T) statisfied 

299T=112.0! 0 units of product 0 in warehouse 

300T=144.0! finished j(id: 2, p: 1, am: 7, ar: 30, me: T, c: T, st: 95, sp: 1)\ 

301 at station 0 

302T=144.0! product=1, amount=7, in_warehouse=0, in_production=0,\ 

303 1 pending demands 

304T=144.0! d(id: 2, p: 1, c: 0, am: 7, ar: 30, dl: 200, me: T) statisfied 

305T=144.0 -- finished 

306 

307 

308>>> simulation.ctrl_reset() 

309>>> simulation.ctrl_run() 

310start 

311T=0.0: product=0, amount=2, in_warehouse=0, in_production=0, 0 pending demands 

312T=0.0: 2 units of product 0 in warehouse 

313T=0.0: product=1, amount=1, in_warehouse=0, in_production=0, 0 pending demands 

314T=0.0: 1 units of product 1 in warehouse 

315T=20.0: product=1, amount=0, in_warehouse=1, in_production=0,\ 

316 1 pending demands 

317T=20.0: station=1, 1 jobs queued 

318T=20.0: start j(id: 0, p: 1, am: 9, ar: 20, me: F, c: F, st: 20, sp: 0)\ 

319 at station 1 

320T=22.0! product=0, amount=0, in_warehouse=2, in_production=0,\ 

321 1 pending demands 

322T=22.0! station=0, 1 jobs queued 

323T=22.0! start j(id: 1, p: 0, am: 3, ar: 22, me: T, c: F, st: 22, sp: 0)\ 

324 at station 0 

325T=30.0! product=1, amount=0, in_warehouse=1, in_production=9,\ 

326 2 pending demands 

327T=52.0! finished j(id: 1, p: 0, am: 3, ar: 22, me: T, c: F, st: 22, sp: 0)\ 

328 at station 0 

329T=62.0: finished j(id: 0, p: 1, am: 9, ar: 20, me: F, c: F, st: 20, sp: 0)\ 

330 at station 1 

331T=62.0! station=1, 2 jobs queued 

332T=62.0! start j(id: 2, p: 1, am: 7, ar: 30, me: T, c: F, st: 30, sp: 0)\ 

333 at station 1 

334T=62.0! station=0, 1 jobs queued 

335T=62.0: start j(id: 0, p: 1, am: 9, ar: 20, me: F, c: F, st: 62, sp: 1)\ 

336 at station 0 

337T=95.0! finished j(id: 2, p: 1, am: 7, ar: 30, me: T, c: F, st: 30, sp: 0)\ 

338 at station 1 

339T=95.0! station=1, 1 jobs queued 

340T=95.0! start j(id: 1, p: 0, am: 3, ar: 22, me: T, c: F, st: 52, sp: 1)\ 

341 at station 1 

342T=107.0: finished j(id: 0, p: 1, am: 9, ar: 20, me: F, c: T, st: 62, sp: 1)\ 

343 at station 0 

344T=107.0! station=0, 1 jobs queued 

345T=107.0! start j(id: 2, p: 1, am: 7, ar: 30, me: T, c: F, st: 95, sp: 1)\ 

346 at station 0 

347T=107.0! product=1, amount=9, in_warehouse=1, in_production=7,\ 

348 2 pending demands 

349T=107.0: d(id: 0, p: 1, c: 0, am: 10, ar: 20, dl: 90, me: F) statisfied 

350T=107.0! 0 units of product 1 in warehouse 

351T=112.0! finished j(id: 1, p: 0, am: 3, ar: 22, me: T, c: T, st: 52, sp: 1)\ 

352 at station 1 

353T=112.0! product=0, amount=3, in_warehouse=2, in_production=0,\ 

354 1 pending demands 

355T=112.0! d(id: 1, p: 0, c: 0, am: 5, ar: 22, dl: 200, me: T) statisfied 

356T=112.0! 0 units of product 0 in warehouse 

357T=144.0! finished j(id: 2, p: 1, am: 7, ar: 30, me: T, c: T, st: 95, sp: 1)\ 

358 at station 0 

359T=144.0! product=1, amount=7, in_warehouse=0, in_production=0,\ 

360 1 pending demands 

361T=144.0! d(id: 2, p: 1, c: 0, am: 7, ar: 30, dl: 200, me: T) statisfied 

362T=144.0 -- finished 

363 

364 

365Now we want to stop the simulation measurement period before the last 

366job completes. Notice that the last production jobs after time unit 

36781 are no longer performed, because their end falls outside of the 

368measurement period. 

369 

370>>> instance = Instance( 

371... name="test3", n_products=2, n_customers=1, n_stations=2, n_demands=3, 

372... time_end_warmup=21, time_end_measure=100, 

373... routes=[[0, 1], [1, 0]], 

374... demands=[[0, 0, 1, 10, 20, 90], [1, 0, 0, 5, 22, 200], 

375... [2, 0, 1, 7, 30, 200]], 

376... warehous_at_t0=[2, 1], 

377... station_product_unit_times=[[[10.0, 50.0, 15.0, 100.0], 

378... [ 5.0, 20.0, 7.0, 35.0, 4.0, 50.0]], 

379... [[ 5.0, 24.0, 7.0, 80.0], 

380... [ 3.0, 21.0, 6.0, 50.0,]]]) 

381 

382>>> instance.name 

383'test3' 

384 

385>>> simulation = Simulation(instance, PrintingListener(print_time=False)) 

386>>> simulation.ctrl_run() 

387start 

388T=0.0: product=0, amount=2, in_warehouse=0, in_production=0, 0 pending demands 

389T=0.0: 2 units of product 0 in warehouse 

390T=0.0: product=1, amount=1, in_warehouse=0, in_production=0, 0 pending demands 

391T=0.0: 1 units of product 1 in warehouse 

392T=20.0: product=1, amount=0, in_warehouse=1, in_production=0,\ 

393 1 pending demands 

394T=20.0: station=1, 1 jobs queued 

395T=20.0: start j(id: 0, p: 1, am: 9, ar: 20, me: F, c: F, st: 20, sp: 0)\ 

396 at station 1 

397T=22.0! product=0, amount=0, in_warehouse=2, in_production=0,\ 

398 1 pending demands 

399T=22.0! station=0, 1 jobs queued 

400T=22.0! start j(id: 1, p: 0, am: 3, ar: 22, me: T, c: F, st: 22, sp: 0)\ 

401 at station 0 

402T=30.0! product=1, amount=0, in_warehouse=1, in_production=9,\ 

403 2 pending demands 

404T=52.0! finished j(id: 1, p: 0, am: 3, ar: 22, me: T, c: F, st: 22, sp: 0)\ 

405 at station 0 

406T=62.0: finished j(id: 0, p: 1, am: 9, ar: 20, me: F, c: F, st: 20, sp: 0)\ 

407 at station 1 

408T=62.0! station=1, 2 jobs queued 

409T=62.0! start j(id: 2, p: 1, am: 7, ar: 30, me: T, c: F, st: 30, sp: 0)\ 

410 at station 1 

411T=62.0! station=0, 1 jobs queued 

412T=62.0: start j(id: 0, p: 1, am: 9, ar: 20, me: F, c: F, st: 62, sp: 1)\ 

413 at station 0 

414T=95.0! finished j(id: 2, p: 1, am: 7, ar: 30, me: T, c: F, st: 30, sp: 0)\ 

415 at station 1 

416T=95.0! station=1, 1 jobs queued 

417T=95.0! start j(id: 1, p: 0, am: 3, ar: 22, me: T, c: F, st: 52, sp: 1)\ 

418 at station 1 

419T=95.0 -- finished 

420 

421>>> simulation.ctrl_reset() 

422>>> simulation.ctrl_run() 

423start 

424T=0.0: product=0, amount=2, in_warehouse=0, in_production=0, 0 pending demands 

425T=0.0: 2 units of product 0 in warehouse 

426T=0.0: product=1, amount=1, in_warehouse=0, in_production=0, 0 pending demands 

427T=0.0: 1 units of product 1 in warehouse 

428T=20.0: product=1, amount=0, in_warehouse=1, in_production=0,\ 

429 1 pending demands 

430T=20.0: station=1, 1 jobs queued 

431T=20.0: start j(id: 0, p: 1, am: 9, ar: 20, me: F, c: F, st: 20, sp: 0)\ 

432 at station 1 

433T=22.0! product=0, amount=0, in_warehouse=2, in_production=0,\ 

434 1 pending demands 

435T=22.0! station=0, 1 jobs queued 

436T=22.0! start j(id: 1, p: 0, am: 3, ar: 22, me: T, c: F, st: 22, sp: 0)\ 

437 at station 0 

438T=30.0! product=1, amount=0, in_warehouse=1, in_production=9,\ 

439 2 pending demands 

440T=52.0! finished j(id: 1, p: 0, am: 3, ar: 22, me: T, c: F, st: 22, sp: 0)\ 

441 at station 0 

442T=62.0: finished j(id: 0, p: 1, am: 9, ar: 20, me: F, c: F, st: 20, sp: 0)\ 

443 at station 1 

444T=62.0! station=1, 2 jobs queued 

445T=62.0! start j(id: 2, p: 1, am: 7, ar: 30, me: T, c: F, st: 30, sp: 0)\ 

446 at station 1 

447T=62.0! station=0, 1 jobs queued 

448T=62.0: start j(id: 0, p: 1, am: 9, ar: 20, me: F, c: F, st: 62, sp: 1)\ 

449 at station 0 

450T=95.0! finished j(id: 2, p: 1, am: 7, ar: 30, me: T, c: F, st: 30, sp: 0)\ 

451 at station 1 

452T=95.0! station=1, 1 jobs queued 

453T=95.0! start j(id: 1, p: 0, am: 3, ar: 22, me: T, c: F, st: 52, sp: 1)\ 

454 at station 1 

455T=95.0 -- finished 

456""" 

457 

458from dataclasses import dataclass, field 

459from heapq import heappop, heappush 

460from time import time_ns 

461from typing import Any, Callable, Final 

462 

463import numpy as np 

464from pycommons.strings.string_conv import bool_to_str, float_to_str 

465from pycommons.types import type_error 

466 

467from moptipyapps.prodsched.instance import ( 

468 Demand, 

469 Instance, 

470 compute_finish_time, 

471) 

472 

473 

474@dataclass(order=True, frozen=True) 

475class _Event: 

476 """The internal record for events in the simulation.""" 

477 

478 #: When does the event happen? 

479 when: float 

480 #: Which function to call? 

481 call: Callable = field(compare=False) 

482 #: The arguments to pass to the function 

483 args: tuple = field(compare=False) 

484 

485 

486@dataclass(order=True, frozen=True) 

487class Job: 

488 """The record for a production job.""" 

489 

490 #: the unique job id 

491 job_id: int 

492 #: the ID of the product to be produced. 

493 product_id: int 

494 #: the amount to produce 

495 amount: int 

496 #: the time when the job was issued 

497 arrival: float 

498 #: should the job be considered during measurement? 

499 measure: bool 

500 #: is the job completed? 

501 completed: bool = False 

502 #: the time when the job arrived at the queue of the current station. 

503 station_time: float = -1.0 

504 #: the current job step, starts at 0. 

505 step: int = -1 

506 

507 def __str__(self) -> str: 

508 """ 

509 Get a string representation of this job. 

510 

511 :return: the string representation 

512 

513 >>> str(Job(0, 1, 10, 0.5, True, False, 1.0, 0)) 

514 'j(id: 0, p: 1, am: 10, ar: 0.5, me: T, c: F, st: 1, sp: 0)' 

515 """ 

516 fts: Final[Callable] = float_to_str 

517 return (f"j(id: {self.job_id}, p: {self.product_id}, " 

518 f"am: {self.amount}, ar: {fts(self.arrival)}, " 

519 f"me: {bool_to_str(self.measure)}, " 

520 f"c: {bool_to_str(self.completed)}, " 

521 f"st: {fts(self.station_time)}, sp: {self.step})") 

522 

523 

524class Listener: 

525 """A listener for simulation events.""" 

526 

527 def start(self) -> None: 

528 """Get notification that the simulation is starting.""" 

529 

530 def product_in_warehouse( 

531 self, time: float, product_id: int, amount: int, 

532 is_in_measure_period: bool) -> None: 

533 """ 

534 Report a change of the amount of products in the warehouse. 

535 

536 :param time: the current time 

537 :param product_id: the product ID 

538 :param amount: the new absolute total amount of that product in the 

539 warehouse 

540 :param is_in_measure_period: is this event inside the measurement 

541 period? 

542 """ 

543 

544 def produce_at_begin( 

545 self, time: float, station_id: int, job: Job) -> None: 

546 """ 

547 Report the start of the production of a certain product at a station. 

548 

549 :param time: the current time 

550 :param station_id: the station ID 

551 :param job: the production job 

552 """ 

553 

554 def produce_at_end( 

555 self, time: float, station_id: int, job: Job) -> None: 

556 """ 

557 Report the completion of the production of a product at a station. 

558 

559 :param time: the current time 

560 :param station_id: the station ID 

561 :param job: the production job 

562 """ 

563 

564 def demand_satisfied( 

565 self, time: float, demand: Demand) -> None: 

566 """ 

567 Report that a given demand has been satisfied. 

568 

569 :param time: the time index when the demand was satisfied 

570 :param demand: the demand that was satisfied 

571 """ 

572 

573 def event_product(self, time: float, # pylint: disable=R0913,R0917 

574 product_id: int, amount: int, 

575 in_warehouse: int, in_production: int, 

576 pending_demands: tuple[Demand, ...], 

577 is_in_measure_period: bool) -> None: 

578 """ 

579 Get notified right before :meth:`Simulation.event_product`. 

580 

581 :param time: the current system time 

582 :param product_id: the id of the product 

583 :param amount: the amount of the product that appears 

584 :param in_warehouse: the amount of the product currently in the 

585 warehouse 

586 :param in_production: the amounf of product currently under production 

587 :param pending_demands: the pending orders for the product 

588 :param is_in_measure_period: is this event inside the measurement 

589 period? 

590 """ 

591 

592 def event_station(self, time: float, station_id: int, 

593 queue: tuple[Job, ...], 

594 is_in_measure_period: bool) -> None: 

595 """ 

596 Get notified right before :meth:`Simulation.event_station`. 

597 

598 If this event happens, the station is not busy. It could process a job 

599 and there is at least one job that it could process. You can now 

600 select the job to be executed from the `queue` and pass it to 

601 :meth:`~Simulation.act_exec_job`. 

602 

603 :param time: the current time 

604 :param station_id: the station ID 

605 :param queue: the job queue for this station 

606 :param is_in_measure_period: is this event inside the measurement 

607 period? 

608 """ 

609 

610 def finished(self, time: float) -> None: 

611 """ 

612 Be notified that the simulation has been finished. 

613 

614 :param time: the time when we are finished 

615 """ 

616 

617 

618class Simulation: # pylint: disable=R0902 

619 """A simulator for production scheduling.""" 

620 

621 def __init__(self, instance: Instance, listener: Listener) -> None: 

622 """ 

623 Initialize the simulator. 

624 

625 :param instance: the instance 

626 :param listener: the listener 

627 """ 

628 if not isinstance(instance, Instance): 

629 raise type_error(instance, "instance", Instance) 

630 if not isinstance(listener, Listener): 

631 raise type_error(listener, "listener", Listener) 

632 

633 #: the instance whose data is simulated 

634 self.instance: Final[Instance] = instance 

635 #: the product routes 

636 self.__routes: Final[tuple[tuple[int, ...], ...]] = instance.routes 

637 #: the station-product-unit-times 

638 self.__mput: Final[tuple[tuple[np.ndarray, ...], ...]] = ( 

639 instance.station_product_unit_times) 

640 #: the end of the warmup period 

641 self.__warmup: Final[float] = instance.time_end_warmup 

642 #: the end of the measurement period 

643 self.__measure: Final[float] = instance.time_end_measure 

644 

645 #: the start event function 

646 self.__l_start: Final[Callable[[], None]] = listener.start 

647 #: the product-level-in-warehouse-changed event function 

648 self.__l_product_in_warehouse: Final[Callable[[ 

649 float, int, int, bool], None]] = listener.product_in_warehouse 

650 #: the demand satisfied event function 

651 self.__l_demand_satisfied: Final[Callable[[ 

652 float, Demand], None]] \ 

653 = listener.demand_satisfied 

654 #: the listener to be notified if the production of a certain 

655 #: product begins at a certain station. 

656 self.__l_produce_at_begin: Final[Callable[[ 

657 float, int, Job], None]] = listener.produce_at_begin 

658 #: the listener to be notified if the production of a certain 

659 #: product end at a certain station. 

660 self.__l_produce_at_end: Final[Callable[[ 

661 float, int, Job], None]] = listener.produce_at_end 

662 #: the listener to notify about simulation end 

663 self.__l_finished: Final[Callable[[float], None]] = listener.finished 

664 #: the listener to notify about product events 

665 self.__l_event_product: Final[Callable[[ 

666 float, int, int, int, int, tuple[Demand, ...], bool], None]] = \ 

667 listener.event_product 

668 #: the listener to notify about station events 

669 self.__l_event_station: Final[Callable[[ 

670 float, int, tuple[Job, ...], bool], None]] = \ 

671 listener.event_station 

672 

673 #: the current time 

674 self.__time: float = 0.0 

675 #: the internal event queue 

676 self.__queue: Final[list[_Event]] = [] 

677 #: the internal list of pending demands 

678 self.__pending_demands: Final[list[list[Demand]]] = [ 

679 [] for _ in range(instance.n_products)] 

680 #: the internal list of the amount of product currently in production 

681 self.__in_production: Final[list[int]] = [ 

682 0 for _ in range(instance.n_products)] 

683 #: the internal warehouse 

684 self.__warehouse: Final[list[int]] = [0] * instance.n_products 

685 #: the station queues. 

686 self.__mq: Final[list[list[Job]]] = [ 

687 [] for _ in range(instance.n_stations)] 

688 #: whether the stations are busy 

689 self.__mbusy: Final[list[bool]] = [False] * instance.n_stations 

690 #: the job ID counter 

691 self.__job_id: int = 0 

692 

693 def ctrl_reset(self) -> None: 

694 """ 

695 Reset the simulation. 

696 

697 This function sets the time to 0, clears the event queue, clears 

698 the pending orders list, clears the warehouse. 

699 """ 

700 self.__time = 0.0 

701 self.__queue.clear() 

702 for i in range(self.instance.n_products): 

703 self.__warehouse[i] = 0 

704 self.__pending_demands[i].clear() 

705 self.__in_production[i] = 0 

706 for mq in self.__mq: 

707 mq.clear() 

708 for i in range(self.instance.n_stations): 

709 self.__mbusy[i] = False 

710 self.__job_id = 0 

711 

712 def ctrl_run(self) -> None: 

713 """ 

714 Run the simulation. 

715 

716 This function executes the main loop of the simulation. It runs the 

717 central event pump, which is a priority queue. It processes the 

718 simulation events one by one. 

719 """ 

720 self.__l_start() 

721 queue: Final[list[_Event]] = self.__queue 

722 

723 #: fill the warehouse at time index 0 

724 for product_id, amount in enumerate(self.instance.warehous_at_t0): 

725 heappush(queue, _Event(0.0, self.__product_available, ( 

726 product_id, amount))) 

727 #: fill in the customer demands/orders 

728 for demand in self.instance.demands: 

729 heappush(queue, _Event( 

730 demand.arrival, self.__demand_issued, (demand, ))) 

731 

732 while list.__len__(queue): 

733 event: _Event = heappop(queue) 

734 time: float = event.when 

735 if time < self.__time: 

736 raise ValueError(f"Event for {time} at time {self.__time}?") 

737 self.__time = time 

738 event.call(*event.args) 

739 

740 self.__l_finished(self.__time) 

741 

742 def act_demand_satisfied(self, demand: Demand) -> None: 

743 """ 

744 Notify the system that a given demand has been satisfied. 

745 

746 :param demand: the demand that was satisfied 

747 """ 

748 self.__pending_demands[demand.product_id].remove(demand) 

749 self.__l_demand_satisfied(self.__time, demand) 

750 

751 def event_product(self, time: float, # pylint: disable=W0613,R0913,R0917 

752 product_id: int, amount: int, 

753 in_warehouse: int, 

754 in_production: int, # pylint: disable=W0613 

755 pending_demands: tuple[Demand, ...]) -> None: 

756 """ 

757 Take actions when an event regarding a product or demand occurred. 

758 

759 The following events may have occurred: 

760 

761 1. An amount of a product has been produced (`amount > 0`). 

762 2. An amount of a product has been made available at the start of the 

763 simulation to form the initial amount in the warehouse 

764 (`amount > 0`). 

765 3. A customer demand for the product has appeared in the system. If 

766 there is any demand to be fulfilled, then `pending_demands` is not 

767 empty. 

768 

769 You can choose to execute one or multiple of the following actions: 

770 

771 1. :meth:`~Simulation.act_store_in_warehouse` to store a positive 

772 amount of product in the warehouse. 

773 2. :meth:`~Simulation.act_take_from_warehouse` to take a positive 

774 amount of product out of the warehouse (must be `<= in_warehouse`. 

775 3. :meth:`~Simulation.act_produce` to order the production of a 

776 positive amount of the product. 

777 4. :meth:`~Simulation.act_demand_satisfied` to mark one of the demands 

778 from `queue` as satisfied. Notice that in this case, you must make 

779 sure to remove the corresponding amount of product units from the 

780 system. If sufficient units are in `amount`, you would simply not 

781 store these in the warehouse. You could also simply take some units 

782 out of the warehouse with :meth:`~act_take_from_warehouse`. 

783 

784 :param time: the current system time 

785 :param product_id: the id of the product 

786 :param amount: the amount of the product that appears 

787 :param in_warehouse: the amount of the product currently in the 

788 warehouse 

789 :param in_production: the amounf of product currently under production 

790 :param pending_demands: the pending orders for the product 

791 """ 

792 dem_len: int = tuple.__len__(pending_demands) 

793 if dem_len <= 0 < amount: # no demands + positive amount? 

794 self.act_store_in_warehouse(product_id, amount) # store 

795 return # ... and we are done 

796 

797 # Go through the list of demands and satisfy them on a first-come- 

798 # first-serve basis. 

799 total: int = in_warehouse + amount # The available units. 

800 product_needed: int = 0 # The amount needed to satisfy the demands. 

801 for demand in pending_demands: 

802 demand_needed: int = demand.amount 

803 if demand_needed <= total: 

804 total -= demand_needed 

805 self.act_demand_satisfied(demand) 

806 continue 

807 product_needed += demand_needed 

808 

809 #: Update the warehouse. 

810 if total > in_warehouse: 

811 self.act_store_in_warehouse(product_id, total - in_warehouse) 

812 elif total < in_warehouse: 

813 self.act_take_from_warehouse(product_id, in_warehouse - total) 

814 

815 # Order the production of the product units required to satisfy all 

816 # demands. 

817 product_needed -= total + in_production 

818 if product_needed > 0: 

819 self.act_produce(product_id, product_needed) 

820 

821 def event_station(self, 

822 time: float, # pylint: disable=W0613 

823 station_id: int, # pylint: disable=W0613 

824 queue: tuple[Job, ...]) -> None: 

825 """ 

826 Process an event for a given station. 

827 

828 If this event happens, the station is not busy. It could process a job 

829 and there is at least one job that it could process. You can now 

830 select the job to be executed from the `queue` and pass it to 

831 :meth:`~Simulation.act_exec_job`. 

832 

833 :param time: the current time 

834 :param station_id: the station ID 

835 :param queue: the job queue for this station 

836 """ 

837 self.act_exec_job(queue[0]) 

838 

839 def act_exec_job(self, job: Job) -> None: 

840 """ 

841 Execute the job on its current station. 

842 

843 :param job: the job to be executed 

844 """ 

845 product_id: Final[int] = job.product_id 

846 station_id: Final[int] = self.__routes[product_id][job.step] 

847 time: Final[float] = self.__time 

848 self.__mq[station_id].remove(job) # exception if job is not there 

849 

850 if self.__mbusy[station_id]: 

851 raise ValueError("Cannot execute job on busy station.") 

852 

853 self.__mbusy[station_id] = True 

854 self.__l_produce_at_begin(time, station_id, job) 

855 

856 end_time: float = compute_finish_time( 

857 time, job.amount, self.__mput[station_id][product_id]) 

858 if end_time < self.__measure: # only simulate if within time window 

859 heappush(self.__queue, _Event(end_time, self.__job_step, (job, ))) 

860 

861 def act_store_in_warehouse(self, product_id: int, amount: int) -> None: 

862 """ 

863 Add a certain amount of product to the warehouse. 

864 

865 :param product_id: the product ID 

866 :param amount: the amount 

867 """ 

868 if amount <= 0: 

869 raise ValueError( 

870 f"Cannot add amount {amount} of product {product_id}!") 

871 wh: Final[int] = self.__warehouse[product_id] + amount 

872 self.__warehouse[product_id] = wh 

873 time: Final[float] = self.__time 

874 self.__l_product_in_warehouse( 

875 time, product_id, wh, self.__warmup <= time) 

876 

877 def act_take_from_warehouse(self, product_id: int, amount: int) -> None: 

878 """ 

879 Remove a certain amount of product to the warehouse. 

880 

881 :param product_id: the product ID 

882 :param amount: the amount 

883 """ 

884 if amount <= 0: 

885 raise ValueError( 

886 f"Cannot remove amount {amount} of product {product_id}!") 

887 wh: Final[int] = self.__warehouse[product_id] - amount 

888 if wh < 0: 

889 raise ValueError( 

890 f"Cannot remove {amount} of product {product_id} from " 

891 "warehouse if there are only " 

892 f"{self.__warehouse[product_id]} units in it.") 

893 self.__warehouse[product_id] = wh 

894 time: Final[float] = self.__time 

895 self.__l_product_in_warehouse( 

896 time, product_id, wh, self.__warmup <= time) 

897 

898 def act_produce(self, product_id: int, amount: int) -> None: 

899 """ 

900 Order the production of `amount` units of product. 

901 

902 :param product_id: the product ID 

903 :param amount: the amount that needs to be produced 

904 """ 

905 if amount <= 0: 

906 raise ValueError( 

907 f"Cannot produce {amount} units of product {product_id}.") 

908 time: Final[float] = self.__time 

909 jid: Final[int] = self.__job_id 

910 self.__job_id = jid + 1 

911 self.__job_step(Job(jid, product_id, amount, time, 

912 self.__warmup <= time)) 

913 

914 def __product_available( 

915 self, product_id: int, amount: int) -> None: 

916 """ 

917 Process that an amount of a product enters the warehouse. 

918 

919 :param time: the time when it enters the warehouse 

920 :param product_id: the product ID 

921 :param amount: the amount of the product that enters the warehouse 

922 """ 

923 lst: Final[list[Demand]] = self.__pending_demands[product_id] 

924 tp: Final[tuple] = tuple(lst) if list.__len__(lst) > 0 else () 

925 wh: Final[int] = self.__warehouse[product_id] 

926 ip: Final[int] = self.__in_production[product_id] 

927 time: Final[float] = self.__time 

928 self.__l_event_product(time, product_id, amount, wh, ip, tp, 

929 self.__warmup <= time) 

930 self.event_product(time, product_id, amount, wh, ip, tp) 

931 

932 def __demand_issued(self, demand: Demand) -> None: 

933 """ 

934 Process that a demand was issued by a customer. 

935 

936 :param demand: the demand record 

937 """ 

938 time: float = self.__time 

939 if demand.arrival != time: 

940 raise ValueError( 

941 f"Demand time {demand.arrival} != system time {time}") 

942 product_id: int = demand.product_id 

943 lst: list[Demand] = self.__pending_demands[product_id] 

944 lst.append(demand) 

945 tp: Final[tuple[Demand, ...]] = tuple(lst) 

946 ip: Final[int] = self.__in_production[product_id] 

947 iw: Final[int] = self.__warehouse[product_id] 

948 self.__l_event_product(time, product_id, 0, iw, ip, tp, 

949 self.__warmup <= time) 

950 self.event_product(time, product_id, 0, iw, ip, tp) 

951 

952 def __job_step(self, job: Job) -> None: 

953 """ 

954 Move a job a step forward. 

955 

956 If this job just enters the system, it gets enqueued at its first 

957 station. If it was already running on a station, then that station 

958 becomes idle and can process the next job. Our job now either moves to 

959 the next station and enters the queue of that station OR, if it has 

960 been completed, its produced product amount can enter the warehouse. 

961 

962 :param job: the job 

963 """ 

964 product_id: Final[int] = job.product_id 

965 routes: Final[tuple[int, ...]] = self.__routes[product_id] 

966 time: Final[float] = self.__time 

967 

968 # WARNING: We only track jobs with issue time within the measurement 

969 # period! 

970 warm: Final[float] = self.__warmup 

971 time_in_meas: Final[bool] = warm <= time 

972 

973 job_step: Final[int] = job.step 

974 next_step: Final[int] = job_step + 1 

975 completed: Final[bool] = next_step >= tuple.__len__(routes) 

976 

977 if job_step >= 0: # The job was running on a station. 

978 old_station_id: Final[int] = routes[job_step] 

979 if completed: 

980 object.__setattr__(job, "completed", True) 

981 self.__l_produce_at_end(time, old_station_id, job) 

982 self.__mbusy[old_station_id] = False 

983 old_mq: Final[list[Job]] = self.__mq[old_station_id] 

984 if list.__len__(old_mq) > 0: 

985 tupo: Final[tuple[Job, ...]] = tuple(old_mq) 

986 self.__l_event_station(time, old_station_id, tupo, 

987 time_in_meas) 

988 self.event_station(time, old_station_id, tupo) 

989 else: 

990 self.__in_production[product_id] += job.amount 

991 

992 if completed: 

993 self.__in_production[product_id] -= job.amount 

994 self.__product_available(product_id, job.amount) 

995 return 

996 

997 object.__setattr__(job, "step", next_step) 

998 object.__setattr__(job, "station_time", time) 

999 

1000 new_station_id: Final[int] = routes[next_step] 

1001 queue: list[Job] = self.__mq[new_station_id] 

1002 queue.append(job) 

1003 if not self.__mbusy[new_station_id]: 

1004 tupq: Final[tuple[Job, ...]] = tuple(queue) 

1005 self.__l_event_station( 

1006 time, new_station_id, tupq, time_in_meas) 

1007 self.event_station(time, new_station_id, tupq) 

1008 

1009 

1010class PrintingListener(Listener): 

1011 """A listener that just prints simulation events.""" 

1012 

1013 def __init__(self, output: Callable[[str], Any] = print, 

1014 print_time: bool = True) -> None: 

1015 """ 

1016 Initialize the printing listener. 

1017 

1018 :param output: the output callable 

1019 :param print_time: shall we print the time? 

1020 """ 

1021 if not callable(output): 

1022 raise type_error(output, "output", call=True) 

1023 if not isinstance(print_time, bool): 

1024 raise type_error(print_time, "print_time", bool) 

1025 #: the output callable 

1026 self.__output: Final[Callable[[str], Any]] = output 

1027 #: shall we print the time at the end? 

1028 self.__print_time: Final[bool] = print_time 

1029 #: the internal start time 

1030 self.__start_time_ns: int | None = None 

1031 

1032 def start(self) -> None: 

1033 """Print that the simulation begins.""" 

1034 self.__start_time_ns = time_ns() 

1035 self.__output("start") 

1036 

1037 def product_in_warehouse( 

1038 self, time: float, product_id: int, amount: int, 

1039 is_in_measure_period: bool) -> None: 

1040 """Print the product amount in the warehouse.""" 

1041 self.__output(f"T={time}{'!' if is_in_measure_period else ':'} " 

1042 f"{amount} units of product {product_id} in warehouse") 

1043 

1044 def produce_at_begin( 

1045 self, time: float, station_id: int, job: Job) -> None: 

1046 """Print that the production at a given station begun.""" 

1047 self.__output(f"T={time}{'!' if job.measure else ':'} " 

1048 f"start {job} at station {station_id}") 

1049 

1050 def produce_at_end(self, time: float, station_id: int, job: Job) -> None: 

1051 """Print that the production at a given station ended.""" 

1052 self.__output(f"T={time}{'!' if job.measure else ':'} " 

1053 f"finished {job} at station {station_id}") 

1054 

1055 def demand_satisfied(self, time: float, demand: Demand) -> None: 

1056 """Print that a demand was satisfied.""" 

1057 self.__output( 

1058 f"T={time}{'!' if demand.measure else ':'} {demand} statisfied") 

1059 

1060 def event_product(self, time: float, # pylint: disable=R0913,R0917 

1061 product_id: int, amount: int, 

1062 in_warehouse: int, in_production: int, 

1063 pending_demands: tuple[Demand, ...], 

1064 is_in_measure_period: bool) -> None: 

1065 """Print the prouct event.""" 

1066 self.__output(f"T={time}{'!' if is_in_measure_period else ':'} " 

1067 f"product={product_id}, amount={amount}" 

1068 f", in_warehouse={in_warehouse}, in_production=" 

1069 f"{in_production}, {tuple.__len__(pending_demands)} " 

1070 "pending demands") 

1071 

1072 def event_station(self, time: float, station_id: int, 

1073 queue: tuple[Job, ...], 

1074 is_in_measure_period: bool) -> None: 

1075 """Print the station event.""" 

1076 self.__output( 

1077 f"T={time}{'!' if is_in_measure_period else ':'} " 

1078 f"station={station_id}, {tuple.__len__(queue)} jobs queued") 

1079 

1080 def finished(self, time: float) -> None: 

1081 """Print that the simulation has finished.""" 

1082 end: Final[int] = time_ns() 

1083 self.__output(f"T={time} -- finished") 

1084 if self.__print_time and self.__start_time_ns is not None: 

1085 required: float = (end - self.__start_time_ns) / 1_000_000_000 

1086 self.__output(f"Simulation time: {required}s") 

1087 

1088 

1089def warmup() -> None: 

1090 """ 

1091 Perform a warm-up for our simulator. 

1092 

1093 The simulator uses some code implemented in numba etc., which may need to 

1094 be jitted before the actual execution. 

1095 

1096 >>> warmup() 

1097 """ 

1098 instance = Instance( 

1099 name="warmup", n_products=2, n_customers=1, n_stations=2, n_demands=2, 

1100 time_end_warmup=10, time_end_measure=10000, 

1101 routes=[[0, 1], [1, 0]], 

1102 demands=[[0, 0, 1, 10, 20, 90], [1, 0, 0, 5, 22, 200]], 

1103 warehous_at_t0=[2, 1], 

1104 station_product_unit_times=[ 

1105 [[10.0, 50.0, 15.0, 100.0], [5.0, 20.0, 7.0, 35.0, 4.0, 50.0]], 

1106 [[5.0, 24.0, 7.0, 80.0], [3.0, 21.0, 6.0, 50.0]]]) 

1107 Simulation(instance, Listener()).ctrl_run()