diff --git a/Car wash SimPy.py b/Car wash SimPy.py index ae4e956..46ab571 100644 --- a/Car wash SimPy.py +++ b/Car wash SimPy.py @@ -31,7 +31,7 @@ class Street(sim.Component): def process(self): while True: - self.nextarrival=sim.now()+street_iat.sample() + self.nextarrival=env.now()+street_iat.sample() wakeup.trigger() yield self.hold(till=self.nextarrival) @@ -45,7 +45,7 @@ def process(self): # OK, now wait to get out onto the street; every time a new car # arrives in cross traffic, it will signal us to check the new # next arrival time - while sim.now()+time_to_exit >= street.nextarrival: + while env.now()+time_to_exit >= street.nextarrival: yield self.wait(wakeup,mode='wait for wakeup') yield self.hold(time_to_exit,mode='exit') self.release(bufferfront) diff --git a/DejaVuSansMono.ttf b/DejaVuSansMono.ttf new file mode 100644 index 0000000..8b7bb2a Binary files /dev/null and b/DejaVuSansMono.ttf differ diff --git a/Demo animate 1.py b/Demo animate 1.py index 32b4674..4e4a79e 100644 --- a/Demo animate 1.py +++ b/Demo animate 1.py @@ -1,26 +1,26 @@ -# Demo animate 1 -import salabim as sim - -class AnimateMovingText(sim.Animate): - def __init__(self): - super().__init__(text='',x0=100,x1=1000,y0=100,t1=env.now()+10) - - def x(self,t): - return sim.interpolate(sim.interpolate(t,self.t0,self.t1,0,1)**2,0,1,self.x0,self.x1) - - def y(self,t): - return int(t)*50 - - def text(self,t): - return '{:0.1f}'.format(t) - - -1env=sim.Environment() - -env.animation_parameters() - -AnimateMovingText() - -sim.run() - - +# Demo animate 1 +import salabim as sim + +class AnimateMovingText(sim.Animate): + def __init__(self): + super().__init__(text='',x0=100,x1=1000,y0=100,t1=env.now()+10) + + def x(self,t): + return sim.interpolate(sim.interpolate(t,self.t0,self.t1,0,1)**2,0,1,self.x0,self.x1) + + def y(self,t): + return int(t)*50 + + def text(self,t): + return '{:0.1f}'.format(t) + + +env=sim.Environment() + +env.animation_parameters() + +AnimateMovingText() + +env.run() + + diff --git a/Demo animate 2.py b/Demo animate 2.py index eea494b..d5e938c 100644 --- a/Demo animate 2.py +++ b/Demo animate 2.py @@ -1,50 +1,53 @@ -# Demo animate 2.py -import salabim as sim - -class AnimateWaitSquare(sim.Animate): - def __init__(self,i): - self.i=i - super().__init__(rectangle0=(-10,-10,10,10),x0=300-30*i,y0=100,fillcolor0='red',linewidth0=0) - - def visible(self,t): - return q[self.i] is not None - -class AnimateWaitText(sim.Animate): - def __init__(self,i): - self.i=i - super().__init__(text='',x0=300-30*i,y0=100,textcolor0='white') - - def text(self,t): - component_i=q[self.i] - - if component_i is None: - return '' - else: - return component_i.name() - -def do_animation(): - env.animation_parameters() - for i in range(10): - AnimateWaitSquare(i) - AnimateWaitText(i) - show_length=sim.Animate(text='',x0=330,y0=100,textcolor0='black',anchor='w') - show_length.text=lambda t: 'Length= '+str(len(q)) - -class Person(sim.Component): - def process(self): - self.enter(q) - yield self.hold(15) - self.leave(q) - -env=sim.Environment(trace=True) - -q=sim.Queue('q') -for i in range(15): - Person(name='{:02d}'.format(i),at=i*1) - - -do_animation() - - - -env.run() +# Demo animate 2.py +import salabim as sim + + +class AnimateWaitSquare(sim.Animate): + def __init__(self, i): + self.i = i + super().__init__( + rectangle0=(-10, -10, 10, 10), x0=300 - 30*i, y0=100, fillcolor0='red', linewidth0=0) + + def visible(self, t): + return q[self.i] is not None + + +class AnimateWaitText(sim.Animate): + def __init__(self, i): + self.i = i + super().__init__(text='', x0=300-30*i, y0=100, textcolor0='white') + + def text(self, t): + component_i = q[self.i] + + if component_i is None: + return '' + else: + return component_i.name() + + +def do_animation(): + env.animation_parameters() + for i in range(10): + AnimateWaitSquare(i) + AnimateWaitText(i) + show_length=sim.Animate(text='',x0=330,y0=100,textcolor0='black',anchor='w') + show_length.text = lambda t: 'Length= ' + str(len(q)) + + +class Person(sim.Component): + def process(self): + self.enter(q) + yield self.hold(15) + self.leave(q) + +env = sim.Environment(trace=True) + +q = sim.Queue('q') +for i in range(15): + Person(name='{:02d}'.format(i), at=i*1) + + +do_animation() + +env.run() diff --git a/Dining philosophers animated.py b/Dining philosophers animated.py index 3a7ef8c..3f8c90c 100644 --- a/Dining philosophers animated.py +++ b/Dining philosophers animated.py @@ -47,8 +47,7 @@ def angle(self, t): def do_animation(): global nphilosophers, eatingtime_mean, thinkingtime_mean global nphilosophers_last - a=1/0 - sim.animation_parameters(x0=-50 * env.width / env.height, y0=-50, x1=+50 * env.width / env.height, + env.animation_parameters(x0=-50 * env.width / env.height, y0=-50, x1=+50 * env.width / env.height, modelname='Dining philosophers', speed=8) for i in philosopher: @@ -79,7 +78,7 @@ def set_nphilosophers(val): nphilosophers = int(val) if nphilosophers != nphilosophers_last: nphilosophers_last = nphilosophers - sim.main().activate() + env.main().activate() class Philosopher(sim.Component): diff --git a/Elevator animated.py b/Elevator animated.py index 3bac21e..23776d9 100644 --- a/Elevator animated.py +++ b/Elevator animated.py @@ -157,31 +157,31 @@ def do_animation(): x += xvisitor_dim ncars_last = ncars - ui_ncars = sim.AnimateSlider(x=540, y=env.height, width=90, height=20, + ui_ncars = sim.AnimateSlider(x=510, y=env.height, width=90, height=20, vmin=1, vmax=5, resolution=1, v=ncars, label='#elevators', action=set_ncars) topfloor_last = topfloor - ui_topfloor = sim.AnimateSlider(x=640, y=env.height, width=90, height=20, + ui_topfloor = sim.AnimateSlider(x=610, y=env.height, width=90, height=20, vmin=5, vmax=20, resolution=1, v=topfloor, label='top floor', action=set_topfloor) capacity_last = capacity - ui_capacity = sim.AnimateSlider(x=740, y=env.height, width=90, height=20, + ui_capacity = sim.AnimateSlider(x=710, y=env.height, width=90, height=20, vmin=2, vmax=6, resolution=1, v=capacity, label='capacity', action=set_capacity) - ui_load_0_n = sim.AnimateSlider(x=540, y=env.height - 50, width=90, height=25, + ui_load_0_n = sim.AnimateSlider(x=510, y=env.height - 50, width=90, height=25, vmin=0, vmax=400, resolution=25, v=load_0_n, label='Load 0->n', action=set_load_0_n) - ui_load_n_n = sim.AnimateSlider(x=640, y=env.height - 50, width=90, height=25, + ui_load_n_n = sim.AnimateSlider(x=610, y=env.height - 50, width=90, height=25, vmin=0, vmax=400, resolution=25, v=load_n_n, label='Load n->n', action=set_load_n_n) - ui_load_n_0 = sim.AnimateSlider(x=740, y=env.height - 50, width=90, height=25, + ui_load_n_0 = sim.AnimateSlider(x=710, y=env.height - 50, width=90, height=25, vmin=0, vmax=400, resolution=25, v=load_n_0, label='Load n->0', action=set_load_n_0) if make_video: - sim.animation_parameters(modelname='Elevator',speed=32,video='Elevator.mp4', + env.animation_parameters(modelname='Elevator',speed=32,video='Elevator.mp4', show_speed=False,show_fps=False) else: - sim.animation_parameters(modelname='Elevator', speed=32) + env.animation_parameters(modelname='Elevator', speed=32) def set_load_0_n(val): @@ -211,7 +211,7 @@ def set_capacity(val): capacity = int(val) if capacity != capacity_last: capacity_last = capacity - sim.main().activate() + env.main().activate() def set_ncars(val): @@ -220,7 +220,7 @@ def set_ncars(val): ncars = int(val) if ncars != ncars_last: ncars_last = ncars - sim.main().activate() + env.main().activate() def set_topfloor(val): @@ -229,7 +229,7 @@ def set_topfloor(val): topfloor = int(val) if topfloor != topfloor_last: topfloor_last = topfloor - sim.main().activate() + env.main().activate() def direction_color(direction): diff --git a/Elevator.py b/Elevator.py index 4e63438..bd895d5 100644 --- a/Elevator.py +++ b/Elevator.py @@ -190,7 +190,7 @@ def getdirection(fromfloor, tofloor): env.run(1000) env.trace(False) for floor in floors.values(): - floor.visitors.reset() + floor.visitors.reset_monitors() env.run(50000) print('Floor n length length_of_stay') diff --git a/Example - bank, 1 clerk.py b/Example - bank, 1 clerk.py index 82f246c..7bbdf59 100644 --- a/Example - bank, 1 clerk.py +++ b/Example - bank, 1 clerk.py @@ -33,4 +33,5 @@ def process(self): waitingline = sim.Queue('waitingline') env.run(till=50) -waitingline.print_statistics() \ No newline at end of file +print() +waitingline.print_statistics() diff --git a/Example - bank, 3 clerks (resources).py b/Example - bank, 3 clerks (resources).py index a2358a1..97b6eba 100644 --- a/Example - bank, 3 clerks (resources).py +++ b/Example - bank, 3 clerks (resources).py @@ -19,9 +19,14 @@ def process(self): env = sim.Environment(trace=False) CustomerGenerator() clerks = sim.Resource('clerk', 3) -clerks.name('piet') + env.run(till=50000) -clerks.requesters().length.print_histogram(30, 1, 0) + +clerks.requesters().length.print_histogram(30, 0, 1) print() -clerks.requesters().length_of_stay.print_histogram(30, 10, 0) +clerks.requesters().length_of_stay.print_histogram(30, 0, 10) + clerks.print_statistics() +clerks.print_info() +print(clerks.claimed_quantity()) + diff --git a/Example - bank, 3 clerks (standby).py b/Example - bank, 3 clerks (standby).py index 3ec8de7..513dc31 100644 --- a/Example - bank, 3 clerks (standby).py +++ b/Example - bank, 3 clerks (standby).py @@ -1,4 +1,4 @@ -# Example - bank, 3 clerks.py +# Example - bank, 3 clerks (standby).py import salabim as sim @@ -27,9 +27,8 @@ def process(self): env = sim.Environment(trace=False) CustomerGenerator() -clerks = sim.Queue('clerks') for i in range(3): - Clerk().enter(clerks) + Clerk() waitingline = sim.Queue('waitingline') env.run(till=50000) diff --git a/Example - bank, 3 clerks (state).py b/Example - bank, 3 clerks (state).py new file mode 100644 index 0000000..580c7bc --- /dev/null +++ b/Example - bank, 3 clerks (state).py @@ -0,0 +1,40 @@ +# Example - bank, 3 clerks (state).py +import salabim as sim + + +class CustomerGenerator(sim.Component): + def process(self): + while True: + Customer() + yield self.hold(sim.Uniform(5, 15).sample()) + + +class Customer(sim.Component): + def process(self): + self.enter(waitingline) + worktodo.trigger(max=1) + yield self.passivate() + + +class Clerk(sim.Component): + def process(self): + while True: + if len(waitingline) == 0: + yield self.wait(worktodo) + self.customer = waitingline.pop() + yield self.hold(30) + self.customer.activate() + + +env = sim.Environment(trace=False) +CustomerGenerator() +for i in range(3): + Clerk() +waitingline = sim.Queue('waitingline') +worktodo = sim.State('worktodo') + +env.run(till=50000) +waitingline.length.print_histogram(30, 0, 1) +print() +waitingline.length_of_stay.print_histogram(30, 0, 10) +worktodo.print_statistics() diff --git a/Example - bank, 3 clerks, reneging.py b/Example - bank, 3 clerks, reneging.py index 7520093..7c65142 100644 --- a/Example - bank, 3 clerks, reneging.py +++ b/Example - bank, 3 clerks, reneging.py @@ -59,6 +59,6 @@ def process(self): print('number reneged', env.number_reneged) print('number balked', env.number_balked) import matplotlib.pyplot as plt -plt.plot(*waitingline.length.tx(), 'bo') +plt.plot(*waitingline.length.tx(exoff=True), 'bo') plt.ylabel('length of ' + waitingline.name()) plt.show() diff --git a/Example - bank, 3 clerks.py b/Example - bank, 3 clerks.py index d7764ba..a1b50b8 100644 --- a/Example - bank, 3 clerks.py +++ b/Example - bank, 3 clerks.py @@ -35,13 +35,16 @@ def process(self): for i in range(3): Clerk().enter(clerks) waitingline = sim.Queue('waitingline') -waitingline.print_statistics() -waitingline.length.print_histogram(30,1,0) + env.run(till=50000) +waitingline.print_info() waitingline.print_statistics() -waitingline.length.print_histogram(30, 1, 0) +waitingline.length.print_histogram(30, 0, 1) print() -waitingline.length_of_stay.print_histogram(30, 10, 0) +waitingline.length_of_stay.print_histogram(30, 0, 10) + +waitingline.length_of_stay.print_statistics() +waitingline.length.print_statistics() diff --git a/Example - basic.py b/Example - basic.py index ba78d42..2d20154 100644 --- a/Example - basic.py +++ b/Example - basic.py @@ -7,7 +7,6 @@ def process(self): while True: yield self.hold(1) - env = sim.Environment(trace=True) Car() env.run(till=5) diff --git a/Lock animated.py b/Lock animated.py index 990927a..80b3a54 100644 --- a/Lock animated.py +++ b/Lock animated.py @@ -161,7 +161,7 @@ def do_animation(): xbound = {left: -1 * locklength, right: 1 * locklength} yspace = 5 - sim.animation_parameters( + env.animation_parameters( x0=xbound[left], y0=-waterdepth, x1=xbound[right], modelname='Lock', speed=8) for side in [left, right]: @@ -186,21 +186,21 @@ def do_animation(): a = sim.Animate(rectangle0=(0, 0, 0, 0), fillcolor0='black', linewidth0=0) a.rectangle = lock_door_right_rectangle - a = sim.Animate(text='', x0=300, y0=650, screen_coordinates=True, - fontsize0=15, font='DejaVuSansMono', anchor='w') + a = sim.Animate(text='', x0=10, y0=650, screen_coordinates=True, + fontsize0=15, font='narrow', anchor='w') a.text = lambda t: 'mean waiting left : {:5.1f} (n={})'.\ format(wait[left].length_of_stay.mean(), wait[left].length_of_stay.number_of_entries()) - a = sim.Animate(text='', x0=300, y0=630, screen_coordinates=True, - fontsize0=15, font='DejaVuSansMono', anchor='w') + a = sim.Animate(text='', x0=10, y0=630, screen_coordinates=True, + fontsize0=15, font='narrow', anchor='w') a.text = lambda t: 'mean waiting right: {:5.1f} (n={})'.\ format(wait[right].length_of_stay.mean(), wait[right].length_of_stay.number_of_entries()) - a = sim.Animate(text='xx=12.34', x0=300, y0=610, screen_coordinates=True, - fontsize0=15, font='DejaVuSansMono', anchor='w') + a = sim.Animate(text='xx=12.34', x0=10, y0=610, screen_coordinates=True, + fontsize0=15, font='narrow', anchor='w') a.text = lambda t: ' nr waiting left : {:3d}'.format(wait[left].length()) - a = sim.Animate(text='xx=12.34', x0=300, y0=590, screen_coordinates=True, - fontsize0=15, font='DejaVuSansMono', anchor='w') + a = sim.Animate(text='xx=12.34', x0=10, y0=590, screen_coordinates=True, + fontsize0=15, font='narrow', anchor='w') a.text = lambda t: ' nr waiting right: {:3d}'.format(wait[right].length()) sim.AnimateSlider(x=520, y=env.height, width=100, height=20, diff --git a/Lock with resources animated.py b/Lock with resources animated.py index 2b7fe16..8ec3452 100644 --- a/Lock with resources animated.py +++ b/Lock with resources animated.py @@ -161,7 +161,7 @@ def do_animation(): xbound = {left: -1 * locklength, right: 1 * locklength} yspace = 5 - sim.animation_parameters( + env.animation_parameters( x0=xbound[left], y0=-waterdepth, x1=xbound[right], modelname='Lock', speed=8) for side in [left, right]: @@ -188,22 +188,22 @@ def do_animation(): a = sim.Animate(rectangle0=(0, 0, 0, 0), fillcolor0='black', linewidth0=0) a.rectangle = lock_door_right_rectangle - a = sim.Animate(text='', x0=300, y0=650, screen_coordinates=True, - fontsize0=15, font='DejaVuSansMono', anchor='w') + a = sim.Animate(text='', x0=10, y0=650, screen_coordinates=True, + fontsize0=15, font='narrow', anchor='w') a.text = lambda t: 'mean waiting left : {:5.1f} (n={})'.\ format(key_in[left].requesters().length_of_stay.mean(), key_in[left].requesters().length_of_stay.number_of_entries()) - a = sim.Animate(text='', x0=300, y0=630, screen_coordinates=True, - fontsize0=15, font='DejaVuSansMono', anchor='w') + a = sim.Animate(text='', x0=10, y0=630, screen_coordinates=True, + fontsize0=15, font='narrow', anchor='w') a.text = lambda t: 'mean waiting right: {:5.1f} (n={})'.\ format(key_in[right].requesters().length_of_stay.mean(), key_in[right].requesters().length_of_stay.number_of_entries()) - a = sim.Animate(text='xx=12.34', x0=300, y0=610, screen_coordinates=True, - fontsize0=15, font='DejaVuSansMono', anchor='w') + a = sim.Animate(text='xx=12.34', x0=10, y0=610, screen_coordinates=True, + fontsize0=15, font='narrow', anchor='w') a.text = lambda t: ' nr waiting left : {:3d}'.format( key_in[left].requesters().length()) - a = sim.Animate(text='xx=12.34', x0=300, y0=590, screen_coordinates=True, - fontsize0=15, font='DejaVuSansMono', anchor='w') + a = sim.Animate(text='xx=12.34', x0=10, y0=590, screen_coordinates=True, + fontsize0=15, font='narrow', anchor='w') a.text = lambda t: ' nr waiting right: {:3d}'.format( key_in[right].requesters().length()) diff --git a/Machine shop animated.py b/Machine shop animated.py index 464af94..6b491a3 100644 --- a/Machine shop animated.py +++ b/Machine shop animated.py @@ -59,7 +59,7 @@ def fillcolor(self, t): class MachineTextAnimate(sim.Animate): def __init__(self, machine): self.machine = machine - super().__init__(x0=10, y0=100 + self.machine.n * 30, text='', anchor='sw') + super().__init__(x0=10, y0=100 + self.machine.n * 30, text='', anchor='sw', font='narrow', fontsize0=15) def text(self, t): return '{} {:4d}'.format(self.machine.ident, self.machine.parts_made) @@ -126,7 +126,7 @@ class RepairTextAnimate(sim.Animate): def __init__(self, i): self.i = i super().__init__(y0=10 + 3, text='', - textcolor0='white', fontsize0=20, anchor='sw') + textcolor0='white', font='narrow', fontsize0=15, anchor='sw') def x(self, t): return xrepairman(self.i, t) + 2 diff --git a/arial.ttf b/arial.ttf new file mode 100644 index 0000000..24b9cf2 Binary files /dev/null and b/arial.ttf differ diff --git a/calibri.ttf b/calibri.ttf new file mode 100644 index 0000000..82dceba Binary files /dev/null and b/calibri.ttf differ diff --git a/changelog.txt b/changelog.txt index 7c787c7..8d657b7 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,72 @@ salabim changelog +version 2.2.5 2017-09-27 +========================= +Queue.reset() has been renamed to Queue.reset_monitors() +Resource.reset() has been renamed to Resource.reset_monitors() +New method: State.reset_monitors() + +In Component.wait(), if the test value contains a $, the $ +sign is now replaced by state.value() instead of str(state.value()) +This means that you can now say + self.wait('light','$ in ("green","yellow")') + +Monitor can now store the tallied values in an array instead of a list. +this results in less memory usage and faster execution speed. +The array may be either integer, unsigned integer or float. +Integer and unsigned integer is available in 1, 2, 4 or 8 byte versions. +When type='any' is given (default), the tallied values will be stored in a list. +Note that the monitor for Queue.length_of_stay is a float array now. +For list monitors the x-value in x() returns a numpy array +where the values are converted to a numeric (value 0 if not possible) +unless overridden with force_numeric=False. + +MonitorTimestamp will now store the timestamps in a float array. +The tallied values can be stored in an array instead of a list. This results in less memory usage and +faster execution speed. +The array may be either integer, unsigned integer or float. +Integer and unsigned integer is available in 1, 2, 4 or 8 byte versions. +When type='any' is given, the tallied values will be stored in a list. +Monitor off is now tallied now with the attribute off of the timestamped monitor, +which is dependent on the type. +Note that the tallied values for Queue.length are in an uint32 array. +The tallied values for Resource.capacity, Resource.claimed_quantity and +Resource.available_quantity are in a float array, +The tallied values for State.value are in a list, unless a type is specified at init time. +The MonitorTimestamp.x() method returns a numpy array for integer and float monitors. +For 'any' timestamped monitors the x-value in xduration() is a numpy array +where the values are converted to a numeric (value 0 if not possible) +unless overridden with force_numeric=False. + +The Monitor.x() and MonitorTimestamped.xduration() methods now uses caching to impove performance. + +The function type in Component.wait now uses three arguments instead of a tuple of value, +component and state. + +Redesigned font searching in order to support animation on Linux and to guarantee a consistent +appearance over different platforms. +Therefore, a small set of ttf fonts is included in the distribution. These should reside in +the same directory where salabim.py is located (PyPI automatically takes care of that). +These fonts will be first searched. As of this moment, salabim is shipped with: +- calibri.ttf The preferred proportional font. Also accessible with font='std' or font='' +- arial.ttf +- cour.ttf +- DejaVuSansMono.ttf The preferred monospaced font. Also accessible with font='mono' +- mplus-1m-regular.ttf The preferred narrow monospaced font. Also accessible with font='narrow' +If salabim is not able to find any matching font, the PIL.ImageFont.load_default() will be called, now. + +Internal optimizations by using defaultdict instead of dict where useful. + +Outphased: + salabim.run() + salabim.main() + salabim.trace() + salabim.now() + salabim.animation_parameters() + salabim.current_component() +Use the equivalent default environment method instead, like + env.run() + version 2.2.4 2017-09-12 ========================= Automatic naming of components, queues, etc. results in shorter names now. @@ -17,8 +84,6 @@ Monitor and MonitorTimestamp names are now serialized. Please have a look at the much improved manual. -Salabim development is powered by Wing IDE (www.wingware.com). - version 2.2.3 2017-09-05 ========================= PIL 4.2.1 (shipped with the latest WinPython distribution), has a bug when @@ -357,7 +422,7 @@ Component.activate() has an additional parameter keep_request, which controls wh pending requests are to be kept upon an activate. Environment.stop_run() and stop_run() has been phased out. -Use the much more logical Environment.main().activate() or sim.main().activate() instead. +Use the much more logical Environment.main().activate() or env.main().activate() instead. This allows the user to specify the time (including inf) of the main reactivation. The models 'Dining philosophers animated.py' and 'Elevator animated.py' have been updated accordingly. @@ -531,7 +596,7 @@ or just one method or class by (e.g.) import salabim as sim help(Animate) help(Component.enter) - help(sim.now) + help(env.now) Bug fixes. @@ -568,7 +633,7 @@ All sample programs are defined in such a way that they give reproducible result main is no longer a global variable. You can use either the main property in Component or Environment classes or just main() from the -salabim module, e.g. with sim.main() +salabim module, e.g. with env.main() The default environment can be queried with the function default_env(), e.g. de=sim.default_env(). @@ -617,7 +682,7 @@ The attribute passive_reason has been phased out, as mode offers similar functio When the mode is set, either by an assignment or one of the above methods, the property mode_time is set to now. This is particularly useful for hold, in order to assess when the component started the hold, like in - fraction=sim.interpolate(sim.now,comp.mode_time,comp.scheduled_time,0,1) + fraction=sim.interpolate(env.now,comp.mode_time,comp.scheduled_time,0,1) This technique is used in the sample models 'Lock animated' and 'Elevator animated'. A bug in PIL caused non black texts to have a kind of outline around the characters in Animate(text=...). @@ -835,7 +900,7 @@ instead of In all examples, we now use import salabim as sim If you use the latter form, all salabim items have to be preceeded by sim. , -like sim.Component, sim.Resource, sim.main, sim.passive, sim.now(), sim.inf. +like sim.Component, sim.Resource, env.main, sim.passive, env.now(), sim.inf. If you want to use a salabim item without prefixing, use something like from salabim import inf,main along with diff --git a/cour.ttf b/cour.ttf new file mode 100644 index 0000000..1f5c35c Binary files /dev/null and b/cour.ttf differ diff --git a/crossing.py b/crossing.py index 3200549..44b4bfd 100644 --- a/crossing.py +++ b/crossing.py @@ -47,5 +47,5 @@ def process(self): TrafficLight() CarGenerator() -sim.run(500) +env.run(500) diff --git a/install.py b/install.py index 8a9097b..512dc07 100644 --- a/install.py +++ b/install.py @@ -1,6 +1,7 @@ import numpy import os import shutil +import glob import platform Pythonista = (platform.system() == 'Darwin') @@ -19,11 +20,17 @@ if not os.path.isdir(path): os.makedirs(path) -if os.path.isfile('salabim.py'): - shutil.copy( - 'salabim.py', - path + ('/' if Pythonista else '\\') + 'salabim.py') - with open(path + ('/' if Pythonista else '\\') + '__init__.py', 'w') as initfile: + +files = glob.iglob("*.*") + +ok=False +for file in files: + if (file in ('salabim.py', 'changelog.txt', 'license.txt')) or (file.endswith('.ttf')): + if file=='salabim.py': + ok=True + shutil.copy(file, path + os.sep + file) +if ok: + with open(path + os.sep + '__init__.py', 'w') as initfile: initfile.write('from .salabim import *\n') print('salabim succesfully installed') else: diff --git a/mplus-1m-regular.ttf b/mplus-1m-regular.ttf new file mode 100644 index 0000000..f28fc25 Binary files /dev/null and b/mplus-1m-regular.ttf differ diff --git a/salabim.py b/salabim.py index 7eb20ab..d16c127 100644 --- a/salabim.py +++ b/salabim.py @@ -26,13 +26,11 @@ see www.salabim.org for more information, the manual and updates. ''' -import platform -Pythonista = (platform.system() == 'Darwin') - import heapq import random import time import math +import array import collections import itertools import functools @@ -42,6 +40,9 @@ import numpy as np from numpy import inf, nan +import platform +Pythonista = (platform.system() == 'Darwin') + try: import cv2 cv2_installed = True @@ -64,13 +65,12 @@ except: tkinter_installed = False - if Pythonista: import scene import ui import objc_util -__version__ = '2.2.4' +__version__ = '2.2.5' class SalabimException(Exception): @@ -99,12 +99,29 @@ class Monitor(object): it is possible to control monitoring later, with the monitor method + type : str + specifies how tallied values are to be stored + - 'any' (default) stores values in a list. This allows + non numeric values. In calculations the values are + forced to a numeric value (0 if not possible) + - 'bool' (True, False) Actually integer >= 0 <= 255 1 byte + - 'uint8' integer >= 0 <= 255 1 byte + - 'int16' integer >= -32768 <= 32767 2 bytes + - 'uint16' integer >= 0 <= 65535 2 bytes + - 'int32' integer >= -2147483648<= 2147483647 4 bytes + - 'uint32' integer >= 0 <= 4294967295 4 bytes + - 'int64' integer >= -9223372036854775808 <= 9223372036854775807 8 bytes + - 'uint64' integer >= 0 <= 18446744073709551615 8 bytes + - 'float' float 8 bytes + env : Environment environment where the monitor is defined |n| - if omitted, default_env will be used + if omitted, default_env will be used ''' - def __init__(self, name, monitor=True, env=None): + cached_x = [(0, 0), (0, 0)] # index=ex0, value=[hash,x] + + def __init__(self, name, monitor=True, type='any', env=None): if env is None: self.env = _default_env else: @@ -112,6 +129,10 @@ def __init__(self, name, monitor=True, env=None): if name is None: name = 'monitor.' self.name(name) + if type in _lookup_arraytype: + self._type = type + else: + raise AssertionError('type (' + type + ') not recognized') self._timestamp = False self.reset(monitor) @@ -128,7 +149,10 @@ def reset(self, monitor=None): if monitor is None: monitor = self._monitor - self._x = [] + if self._type == 'any': + self._x = [] + else: + self._x = array.array(_lookup_arraytype[self._type]) self.monitor(monitor) def monitor(self, value=None): @@ -360,8 +384,8 @@ def histogram(self, bins=10, range=None, ex0=False): ------- numpy histogram : see numpy documentation - Notes - ----- + Note + ---- The numpy definition of a histogram is different from the salabim print_histogram! ''' @@ -383,9 +407,9 @@ def print_statistics(self, show_header=True, show_legend=True, do_indent=False): primarily for internal use ''' if do_indent: - l=45 + l = 45 else: - l=0 + l = 0 indent=pad('',l) if show_header: @@ -398,37 +422,37 @@ def print_statistics(self, show_header=True, show_legend=True, do_indent=False): pad('-' * (l-1)+' ',l) + '-------------- ------------ ------------ ------------') if self._timestamp: - if self.duration()==0: - print(pad(self.name(),l)+'no data') + if self.duration() == 0: + print(pad(self.name(), l)+'no data') return else: - print(pad(self.name(),l) + 'duration {:13.3f}{:13.3f}{:13.3f}'. - format(self.duration(), - self.duration(ex0=True), self.duration_zero())) + print(pad(self.name(),l) + 'duration {}{}{}'. + format(fn(self.duration(),13,3), + fn(self.duration(ex0=True),13,3), fn(self.duration_zero(),13,3))) else: if self.number_of_entries() == 0: - print(pad(self.name(),l)+'no entries') + print(pad(self.name(), l)+'no entries') return else: - print(pad(self.name(),l) + 'entries {:13d}{:13d}{:13d}'. - format(self.number_of_entries(), - self.number_of_entries(ex0=True), self.number_of_entries_zero())) + print(pad(self.name(), l) + 'entries {}{}{}'. + format(fn(self.number_of_entries(),13,3), + fn(self.number_of_entries(ex0=True),13,3), fn(self.number_of_entries_zero(),13,3))) - print(indent + 'mean {:13.3f}{:13.3f}'. - format(self.mean(), self.mean(ex0=True))) - print(indent + 'std.deviation {:13.3f}{:13.3f}'. - format(self.std(), self.std(ex0=True))) + print(indent + 'mean {}{}'. + format(fn(self.mean(),13,3), fn(self.mean(ex0=True),13,3))) + print(indent + 'std.deviation {}{}'. + format(fn(self.std(),13,3), fn(self.std(ex0=True),13,3))) print() - print(indent + 'minimum {:13.3f}{:13.3f}'. - format(self.minimum(), self.minimum(ex0=True))) - print(indent + 'median {:13.3f}{:13.3f}'. - format(self.percentile(50), self.percentile(50, ex0=True))) - print(indent + '90% percentile{:13.3f}{:13.3f}'. - format(self.percentile(90), self.percentile(90, ex0=True))) - print(indent + '95% percentile{:13.3f}{:13.3f}'. - format(self.percentile(95), self.percentile(95, ex0=True))) - print(indent + 'maximum {:13.3f}{:13.3f}'. - format(self.maximum(), self.maximum(ex0=True))) + print(indent + 'minimum {}{}'. + format(fn(self.minimum(),13,3), fn(self.minimum(ex0=True),13,3))) + print(indent + 'median {}{}'. + format(fn(self.percentile(50),13,3),fn(self.percentile(50, ex0=True),13,3))) + print(indent + '90% percentile{}{}'. + format(fn(self.percentile(90),13,3), fn(self.percentile(90, ex0=True),13,3))) + print(indent + '95% percentile{}{}'. + format(fn(self.percentile(95),13,3), fn(self.percentile(95, ex0=True),13,3))) + print(indent + 'maximum {}{}'. + format(fn(self.maximum(),13,3), fn(self.maximum(ex0=True),13,3))) def print_histogram(self, number_of_bins=30, lowerbound=0, bin_width=1, ex0=False): @@ -455,10 +479,10 @@ def print_histogram(self, number_of_bins=30, lowerbound=0, bin_width=1, ex0=Fals else: x = self.x(ex0=ex0) weights = np.ones(len(x)) - weight_total = np.sum(weights) + weight_total = np.sum(weights) print('Histogram of', self.name()) - if weight_total==0: + if weight_total == 0: print() if self._timestamp: print('no data') @@ -498,10 +522,10 @@ def print_histogram(self, number_of_bins=30, lowerbound=0, bin_width=1, ex0=Fals s = ('*' * n) + (' ' * (scale - n)) s = s[:ncum - 1] + '|' + s[ncum + 1:] - print('{:13.3f} {:13.3f}{:6.1f}{:6.1f} {}'. - format(ub, count, perc * 100, cumperc * 100, s)) + print('{} {}{}{} {}'. + format(fn(ub, 13, 3), fn(count, 13, 3), fn(perc * 100, 6, 1), fn(cumperc * 100, 6, 1), s)) - def x(self, ex0=False): + def x(self, ex0=False, force_numeric=True): ''' array of tallied values @@ -510,17 +534,41 @@ def x(self, ex0=False): ex0 : bool if False (default), include zeroes. if True, exclude zeroes + convert_to_numeric : bool + if True (default), convert non numeric tallied values numeric if possible, otherwise assume 0 |n| + if False, do not interpret x-values, return as list if type is list + Returns ------- - all tallied values : array + all tallied values : array/list ''' - - x = list_to_numeric_array(self._x) + thishash = hash((tuple(self._x), force_numeric)) + + if Monitor.cached_x[ex0][0] == thishash: + return Monitor.cached_x[ex0][1] + + if self._type == 'any': + if force_numeric: + x = list_to_numeric_array(self._x) + else: + if ex0: + x = [] + for el in self._x: + if el != 0: + x.append(el) + return x + else: + x = self._x + Monitor.cached_x[ex0] = (hash, x) + return x + else: + x = np.array(self._x) if ex0: - return x[np.where(x != 0)] - else: - return x + x = x[np.where(x != 0)] + + Monitor.cached_x[ex0] = (hash, x) + return x class MonitorTimestamp(Monitor): @@ -541,13 +589,32 @@ class MonitorTimestamp(Monitor): if False, monitoring is disabled |n| it is possible to control monitoring later, with the monitor method - + + type : str + specifies how tallied values are to be stored + Using a int, uint of float type results in less memory usage and better + performance. Note that the getter should never return the number not to use + as this is used to indicate 'off' + + - 'any' (default) stores values in a list. This allows for + non numeric values. In calculations the values are + forced to a numeric value (0 if not possible) do not use -inf + - 'bool' bool (False, True). Actually integer >= 0 <= 254 1 byte do not use 255 + - 'uint8' integer >= 0 <= 254 1 byte do not use 255 + - 'int16' integer >= -32767 <= 32767 2 bytes do not use -32768 + - 'uint16' integer >= 0 <= 65534 2 bytes do not use 65535 + - 'int32' integer >= -2147483647 <= 2147483647 4 bytes do not use -2147483648 + - 'uint32' integer >= 0 <= 4294967294 4 bytes do not use 4294967295 + - 'int64' integer >= -9223372036854775807 <= 9223372036854775807 8 bytes do not use -9223372036854775808 + - 'uint64' integer >= 0 <= 18446744073709551614 8 bytes do not use 18446744073709551615 + - 'float' float 8 bytes do not use -inf + env : Environment environment where the monitor is defined |n| if omitted, default_env will be used - Notes - ----- + Note + ---- A MonitorTimestamp collects both the value and the time. All statistics are based on the durations as weights. @@ -568,13 +635,20 @@ class MonitorTimestamp(Monitor): And thus a mean of (10*50+11*20+12*10+10*20)/(50+20+10+20) ''' - def __init__(self, name, getter, monitor=True, env=None): + cached_xduration = [(0, ()), (0, ())] # index=ex0, value=[hash,(x,duration)] + + def __init__(self, name, getter, monitor=True, type='any', env=None): if name is None: name = 'monitortimestamp.' if env is None: self.env = _default_env else: self.env = env + if type in _lookup_arraytype: + self._type = type + else: + raise AssertionError('type (' + str(type) + ') nor recognized') + self.off = _lookup_off[_lookup_arraytype[type]] self.name(name) self._timestamp = True self._getter = getter @@ -596,11 +670,16 @@ def reset(self, monitor=None): ''' if monitor is not None: self._monitor = monitor + if self._type == 'any': + self._x = [] + else: + self._x = array.array(_lookup_arraytype[self._type]) if self._monitor: - self._x = [self._getter()] + self._x.append(self._getter()) else: - self._x = [nan] - self._t = [self.env._now] + self._x.append(self.off) + self._t = array.array('d') + self._t.append(self.env._now) def monitor(self, value=None): ''' @@ -623,7 +702,7 @@ def monitor(self, value=None): if self._monitor: self.tally() else: - self._tally_nan() + self._tally_off() return self.monitor def tally(self): @@ -639,12 +718,12 @@ def tally(self): self._x.append(x) self._t.append(t) - def _tally_nan(self): + def _tally_off(self): t = self.env._now if self._t[-1] == t: - self._x[-1] = nan + self._x[-1] = self.off else: - self._x.append(nan) + self._x.append(self.off) self._t.append(t) def name(self, txt=None): @@ -835,7 +914,7 @@ def histogram(self, bins=10, range=None, ex0=False): ------- numpy histogram : see numpy documentation - Notes + Note ----- The numpy definition of a histogram is different from the salabim print_histogram! @@ -843,7 +922,7 @@ def histogram(self, bins=10, range=None, ex0=False): x, duration = self.xduration(ex0=ex0) return np.histogram(x, bins=bins, range=range, weights=duration) - def xduration(self, ex0=False): + def xduration(self, ex0=False, force_numeric=True): ''' tuple of array with x-values and array with durations @@ -852,86 +931,136 @@ def xduration(self, ex0=False): ex0 : bool if False (default), include zeroes. if True, exclude zeroes + force_numeric : bool + if True (default), convert non numeric tallied values numeric if possible, otherwise assume 0 |n| + if False, do not interpret x-values, return as list if type is list + Returns ------- - array with x-values and array with durations : tuple + array/list with x-values and array with durations : tuple ''' - x = list_to_numeric_array(self._x) - duration = np.zeros(len(self._x)) + + thishash = hash((tuple(self._x), tuple(self._t), force_numeric)) + + if MonitorTimestamp.cached_xduration[ex0][0] == thishash: + return MonitorTimestamp.cached_xduration[ex0][1] + + duration = np.zeros(len(self._t)) for i, t in enumerate(self._t): if i != 0: duration[i - 1] = t - lastt lastt = t duration[-1] = self.env._now - lastt - filter_not_isnan = np.where(~np.isnan(x)) - duration = duration[filter_not_isnan] - x = x[filter_not_isnan] + + if self._type == 'any': + if force_numeric: + x = list_to_numeric_array(self._x) + else: + x = [] + duration = array.array('d') + + for thisx, thisduration in zip(self._x, duration): + if (not ex0) or (thisx != 0): + x.append(thisx) + duration.append(thisduration) + MonitorTimestamp.cached_xduration[ex0] = (thishash, (x, duration)) + return x, duration + + else: + x = np.array(self._x) + filter_not_off = np.where(x != self.off) + duration = duration[filter_not_off] + x = x[filter_not_off] if ex0: filter_ex0 = np.where(x != 0) - return x[filter_ex0], duration[filter_ex0] - else: - return x, duration + x = x[filter_ex0] + duration = duration[filter_ex0] + + MonitorTimestamp.cached_xduration[ex0] = (thishash, (x, duration)) + return x, duration - def xt(self, ex0=False, exnan=False): + def xt(self, ex0=False, exoff=False, force_numeric=True): ''' - tuple of array with x-values and array with timestamps + tuple of array/list with x-values and array with timestamp Parameters ---------- ex0 : bool if False (default), include zeroes. if True, exclude zeroes - exnan : bool - if False (default), include nan. if True, exclude nans - + exoff : bool + if False (default), include self.off. if True, exclude self.off's + + force_numeric : bool + if True (default), convert non numeric tallied values numeric if possible, otherwise assume 0 |n| + if False, do not interpret x-values, return as list if type is list + Returns ------- - array with x-values and array with timestamps : tuple + array/list with x-values and array with timestamps : tuple - Notes - ----- - The value nan is stored when monitoring is turned off + Note + ---- + The value self.off is stored when monitoring is turned off ''' - x = list_to_numeric_array(self._x) + if self._type == 'any': + if force_numeric: + x = list_to_numeric_array(self._x) + else: + x = [] + t = array.array('d') + for thisx, thist in zip(self._x, self._t): + if (not ex0) or (thisx != 0): + if (not exoff) or (thisx != self.off): + x.append(thisx) + t.appent(thist) + return x, t + else: + x = np.array(self._x) + t = np.array(self._t) if ex0: filter_ex0 = np.where(x != 0) x = x[filter_ex0] t = t[filter_ex0] - if exnan: - filter_exnan = np.where(~np.isnan(x)) - x = x[filter_exnan] - t = t[filter_exnan] + if exoff: + filter_exoff = np.where(x != self.off) + x = x[filter_exoff] + t = t[filter_exoff] return x, t - def tx(self, ex0=False, exnan=False): + def tx(self, ex0=False, exoff=False, force_numeric=False): ''' - tuple of array with timestamps and array with x-values + tuple of array with timestamps and array/list with x-values Parameters ---------- ex0 : bool if False (default), include zeroes. if True, exclude zeroes - exnan : bool - if False (default), include nan. if True, exclude nans - + exoff : bool + if False (default), include self.off. if True, exclude self.off's + + force_numeric : bool + if True (default), convert non numeric tallied values numeric if possible, otherwise assume 0 |n| + if False, do not interpret x-values, return as list if type is list + Returns ------- - array with timestamps and array with x-values : tuple + array with timestamps and array/list with x-values : tuple - Notes - ----- - The value nan is stored when monitoring is turned off + Note + ---- + The value self.off is stored when monitoring is turned off ''' - return tuple(reversed(self.xt(ex0=ex0, exnan=exnan))) + return tuple(reversed(self.xt(ex0=ex0, exoff=exoff, force_numeric=force_numeric))) def print_statistics(self, show_header=True, show_legend=True, do_indent=False): ''' - print timestamp monitor statistics + print timestamped monitor statistics Parameters ---------- @@ -1028,15 +1157,15 @@ def draw(self): touchvalues = self.touches.values() capture_image = Image.new('RGB', - (an_env.width, an_env.height), colorspec_to_tuple(an_env.background_color)) + (an_env.width, an_env.height), colorspec_to_tuple(an_env.background_color)) for ao in an_env.an_objects: ao.make_pil_image(an_env.t) if ao._image_visible: capture_image.paste(ao._image, - (int(ao._image_x), - int(an_env.height - ao._image_y - ao._image.size[1])), - ao._image) + (int(ao._image_x), + int(an_env.height - ao._image_y - ao._image.size[1])), + ao._image) ims = scene.load_pil_image(capture_image) scene.image(ims, 0, 0, *capture_image.size) @@ -1175,18 +1304,18 @@ def __init__(self, name=None, monitor=True, env=None, _isinternal=False): self._iter_sequence = 0 self._iter_touched = {} self.length = MonitorTimestamp( - ('Length of ',self), getter=self._getlength, monitor=monitor, env=self.env) + ('Length of ',self), getter=self._getlength, monitor=monitor, type='uint32', env=self.env) self.length_of_stay = Monitor( - ('Length of stay in ',self), monitor=monitor) + ('Length of stay in ', self), monitor=monitor, type='float') if not _isinternal: self.env.print_trace('', '', self.name() + ' create') def _getlength(self): return self._length - def reset(self, monitor=None): + def reset_monitors(self, monitor=None): ''' - resets queue monitor length_of_stay and time stamped monitr length + resets queue monitor length_of_stay and time stamped monitor length Parameters ---------- @@ -1194,8 +1323,8 @@ def reset(self, monitor=None): if True (default}, monitoring will be on. |n| if False, monitoring is disabled - Notes - ----- + Note + ---- it is possible to reset individual monitoring with length_of_stay.reset() and length.reset() ''' self.length.reset(monitor=monitor) @@ -1212,8 +1341,8 @@ def monitor(self, value=None): if False, monitoring is disabled |n| if not specified, no change - Notes - ----- + Note + ---- it is possible to individually control monitoring with length_of_stay.monitor() and length.monitor() ''' @@ -1297,8 +1426,8 @@ def add(self, component): component to be added to the tail of the queue |n| may not be member of the queue yet - Notes - ----- + Note + ---- the priority will be set to the priority of the tail of the queue, if any or 0 if queue is empty @@ -1316,8 +1445,8 @@ def add_at_head(self, component): component to be added to the head of the queue |n| may not be member of the queue yet - Notes - ----- + Note + ---- the priority will be set to the priority of the head of the queue, if any or 0 if queue is empty @@ -1338,8 +1467,8 @@ def add_in_front_of(self, component, poscomponent): component in front of which component will be inserted |n| must be member of the queue - Notes - ----- + Note + ---- the priority of component will be set to the priority of poscomponent ''' component.enter_in_front_off(self, poscomponent) @@ -1358,9 +1487,9 @@ def add_behind(self, component, poscomponent): component behind which component will be inserted |n| must be member of the queue - Notes - ----- - the priority of component will be set to the priority of poscomponent + Note + ---- + the priority of component will be set to the priority of poscomponent ''' component.enter_behind(self, poscomponent) @@ -1378,8 +1507,8 @@ def add_sorted(self, component, priority): priority : float priority of the component|n| - Notes - ----- + Note + ---- component will be placed just after the last component with a priority <= priority ''' @@ -1403,8 +1532,8 @@ def head(self): ------- the head component of the queue, if any. None otherwise : Component - Notes - ----- + Note + ---- q[0] is a more Pythonic way to access the head of the queue ''' return self._head.successor.component @@ -1415,7 +1544,7 @@ def tail(self): ------- the tail component of the queue, if any. None otherwise : Component - Notes + Note ----- q[-1] is a more Pythonic way to access the tail of the queue ''' @@ -1636,8 +1765,8 @@ def union(self, q, name): ------- queue containing all elements of self and q : Queue - Notes - ----- + Note + ---- the priority will be set to 0 for all components in the resulting queue |n| the order of the resulting queue is as follows: |n| @@ -1807,7 +1936,8 @@ class Environment(object): the seed for random, equivalent to random.seed() |n| if None, a purely random value (based on the current time) will be used (not reproducable) |n| - if omitted, the no action on random is taken + if the null string (''), no action on random is taken |n| + if omitted, 1234567 will be used. name : str name of the environment |n| @@ -1820,8 +1950,8 @@ class Environment(object): if False, no change |n| if omitted, this environment becomes the default environment |n| - Notes - ----- + Note + ---- The trace may be switched on/off later with trace |n| The seed may be later set with random_seed() |n| Initially, the random stream will be seeded with the value 1234567. @@ -1855,7 +1985,6 @@ def __init__(self, trace=False, random_seed=1234567, name=None, is_default_env=T self._current_component = self._main self.ui_objects = [] self.print_trace('{:10.3f}'.format(self._now), 'main', 'current') - self.font_cache = {} self._nameserializeQueue = {} self._nameserializeComponent = {} self._nameserializeResource = {} @@ -2050,8 +2179,8 @@ def animation_parameters(self, The video has to have a .mp4 etension |n| This requires installation of numpy and opencv (cv2). - Notes - ----- + Note + ---- The y-coordinate of the upper right corner is determined automatically in such a way that the x and scaling are the same. |n| @@ -2144,8 +2273,10 @@ def trace(self, value=None): Note ---- - If you want to test the status, always include parentheses, like |n| - if env.trace(): + If you want to test the status, always include + parentheses, like + + ``if env.trace():`` ''' if value is not None: self._trace = value @@ -2247,7 +2378,7 @@ def run(self, duration=None, till=None): if Pythonista: raise AssertionError( 'video production is not supported under Pythonista.') - else: + else: raise AssertionError( 'cv2 required for video production. Run pip install opencv_python.') @@ -2439,7 +2570,7 @@ def an_system_clocktext(self): may be overridden to change the standard behaviour. ''' ao = Animate(x0=self.width, y0=self.height - 5, fillcolor0='black', - text='', fontsize0=15, font='DejaVuSansMono', anchor='ne', + text='', fontsize0=15, font='narrow', anchor='ne', screen_coordinates=True, env=self) self.system_an_objects.append(ao) ao.text = clocktext @@ -2492,44 +2623,11 @@ def set_start_animation(self): self.start_animation_time = self.t self.start_animation_clocktime = time.time() - @functools.lru_cache() - def fonts(self): - return _fonts() - - @functools.lru_cache() - def getfont(self, fontname, fontsize): # fontsize in screen_coordinates! - ''' - internal funtion to get and cache fonts - ''' - if isinstance(fontname, str): - fontlist = (fontname,) - else: - fontlist = fontname - - font = None - for ifont in itertools.chain(fontlist, ('calibri', 'arial', 'arialmt')): - try: - font = ImageFont.truetype(font=ifont, size=int(fontsize)) - break - except: - pass - ifont = self.fonts().get(normalize(ifont)) - if ifont is not None: - try: - font = ImageFont.truetype(font=ifont, size=int(fontsize)) - break - except: - pass - - if font is None: - raise AssertionError('no matching fonts found for ', fontname) - return font - def getwidth(self, text, font='', fontsize=20, screen_coordinates=False): if not screen_coordinates: fontsize = fontsize * self.scale f = self.getfont(font, fontsize) - if text == '': # necessary because of bug in PIL >= 4.2.1 + if text == '': # necessary because of bug in PIL >= 4.2.1 thiswidth, thisheight = (0, 0) else: thiswidth, thisheight = f.getsize(text) @@ -2555,7 +2653,7 @@ def getfontsize_to_fit(self, text, width, font='', screen_coordinates=False): return fontsize else: return fontsize / self.scale - + def name(self, txt=None): ''' Parameters @@ -2608,9 +2706,9 @@ def print_trace(self, s1='', s2='', s3='', s4=''): s4 : str part 4 (usually formatted now) - Notes - ----- - if the current component's suppress_trace is True, nothing is printed + Note + ---- + if the current component's suppress_trace is True, nothing is printed ''' if self._trace: if hasattr(self, '_current_component'): @@ -2773,8 +2871,8 @@ class Animate(object): width1 : float width of the image to be displayed at time t1 (default: width0) |n| - Notes - ----- + Note + ---- one (and only one) of the following parameters is required: - circle0 - image @@ -2792,14 +2890,14 @@ class Animate(object): hexnames may be either 3 of 4 bytes long (RGB or RGBA) both colornames and hexnames may be given as a tuple with an additional alpha between 0 and 255, - e.g. ``('red',127)`` or ``('#ff00ff',128)`` + e.g. ``(255,0,255,128)``, ('red',127)`` or ``('#ff00ff',128)`` Permitted parameters ====================== ========= ========= ========= ========= ========= ========= parameter circle image line polygon rectangle text ====================== ========= ========= ========= ========= ========= ========= - parent - - - - - -## + parent - - - - - - layer - - - - - - keep - - - - - - scree_coordinates - - - - - - @@ -3065,11 +3163,10 @@ def update(self, layer=None, keep=None, visible=None, width1 : float width of the image to be displayed at time t1 (default: width0) |n| - Notes - ----- - the type of the animation cannot be changed with this method. - - the default value of most of the paramaters is the current value (at time now) + Note + ---- + The type of the animation cannot be changed with this method. |n| + The default value of most of the parameters is the current value (at time now) ''' t = self.env._now @@ -3152,17 +3249,31 @@ def update(self, layer=None, keep=None, visible=None, def remove(self): ''' - removes the animation object - - the animation object is removed from the animation queue, - so effectively ending this animation + removes the animation object from the animation queue, + so effectively ending this animation. - note that it might be still updated, if required + Note + ---- + The animation object might be still updated, if required ''' if self in self.env.an_objects: self.env.an_objects.remove(self) def x(self, t=None): + ''' + x-position of an animate object. May be overridden. + + Parameters + ---------- + t : float + current time + + Returns + ------- + x : float + default behaviour: linear interpolation betwen self.x0 and self.x1 + + ''' return interpolate((self.env._now if t is None else t), self.t0, self.t1, self.x0, self.x1) @@ -3241,7 +3352,7 @@ def image(self, t=None): ''' returns image and a serial number at time t use the function spec_to_image to change the image here |n| - if there's a change in the image, a new serial numbder should be returned + if there's a change in the image, a new serial number should be returned if there's no change, do not update the serial number ''' return self.image0, self.image_serial0 @@ -3467,7 +3578,7 @@ def make_pil_image(self, t): fontsize = self.fontsize(t) angle = self.angle(t) anchor = self.anchor(t) - fontname=self.font(t) + fontname = self.font(t) text = self.text(t) if self.screen_coordinates: @@ -3483,10 +3594,10 @@ def make_pil_image(self, t): self._image_ident = ( text, fontname, fontsize, angle, textcolor) if self._image_ident != self._image_ident_prev: - font = self.env.getfont(fontname, fontsize) + font = getfont(fontname, fontsize) if text == '': # this code is a workarond for a bug in PIL >= 4.2.1 im = Image.new( - 'RGBA', (0,0), (0, 0, 0, 0)) + 'RGBA', (0, 0), (0, 0, 0, 0)) else: width, height = font.getsize(text) im = Image.new( @@ -3594,10 +3705,10 @@ class AnimateButton(object): executed when the button is pressed (default None) the function should have no arguments |n| - Notes - ----- - On CPython platforms, the tkinter functionality is used, - on Pythonista, this is emulated by salabim + Note + ---- + On CPython platforms, the tkinter functionality is used. + On Pythonista, this is emulated by salabim ''' def __init__(self, x=0, y=0, width=80, height=30, @@ -3650,8 +3761,7 @@ def install(self): def remove(self): ''' - removes the button object - + removes the button object. |n| the ui object is removed from the ui queue, so effectively ending this ui ''' @@ -3716,16 +3826,16 @@ class AnimateSlider(object): fontsize : int fontsize of the text (default 12) - action ; function + action : function function executed when the slider value is changed (default None) |n| the function should one arguments, being the new value |n| if None (default), no action - Notes - ----- + Note + ---- The current value of the slider is the v attibute of the slider. |n| - On CPython platforms, the tkinter functionality is used, - on Pythonista, this is emulated by salabim + On CPython platforms, the tkinter functionality is used. |n| + On Pythonista, this is emulated by salabim ''' def __init__(self, layer=0, x=0, y=0, width=100, height=20, @@ -3818,9 +3928,8 @@ def install(self): def remove(self): ''' - removes the slider object - - the ui object is removed from the ui queue, + removes the slider object |n| + The ui object is removed from the ui queue, so effectively ending this ui ''' if self in self.env.ui_objects: @@ -3888,7 +3997,7 @@ class Component(object): ''' def __init__(self, name=None, at=None, delay=0, urgent=False, - process='*', suppress_trace=False, mode=None, env=None, *args, **kwargs): + process='*', suppress_trace=False, mode=None, env=None, *args, **kwargs): if env is None: self.env = _default_env else: @@ -3899,8 +4008,8 @@ def __init__(self, name=None, at=None, delay=0, urgent=False, self._qmembers = {} self._process = None self._status = data - self._requests = {} - self._claims = {} + self._requests = collections.defaultdict(float) + self._claims = collections.defaultdict(float) self._waits = [] self._on_event_list = False self._scheduled_time = inf @@ -3921,6 +4030,23 @@ def __init__(self, name=None, at=None, delay=0, urgent=False, self.setup(*args, **kwargs) def setup(self, *args, **kwargs): + ''' + called immediately after initialization of a component. + + by default this is a dummy method, but it can be overridden. + + Example + ------- + class Car(sim.Component): + def setup(self, color): + self.color = color + + def process(self): + ... + + redcar=Car(color='red') |n| + bluecar=Car(color='blue') + ''' pass def __repr__(self): @@ -4005,7 +4131,7 @@ def _check_fail(self): self._leave(r._requesters) if r._requesters._length == 0: r._minq = inf - self._requests = {} + self._requests = collections.defaultdict(float) self._failed = True if len(self._waits) != 0: @@ -4079,11 +4205,11 @@ def activate(self, at=None, delay=0, urgent=False, process=None, if nothing specified, the mode will be unchanged.|n| also mode_time will be set to now, if mode is set. - Notes - ----- + Note + ---- if to be applied for the current component, use yield self.activate(). |n| if both at and delay are specified, the component becomes current at the sum - ofw the two values. + of the two values. ''' p = None if process is None: @@ -4109,7 +4235,7 @@ def activate(self, at=None, delay=0, urgent=False, process=None, if self._status != current: self._remove() if p is None: - if not (keep_request or keep_wait): + if not (keep_request or keep_wait): self._check_fail() else: self._check_fail() @@ -4156,9 +4282,9 @@ def hold(self, duration=None, till=None, urgent=False, mode='*'): if nothing specified, the mode will be unchanged.|n| also mode_time will be set to now, if mode is set. - Notes + Note ---- - if to be used for the current component, use `yield self.hold(...)``. |n| + if to be used for the current component, use ``yield self.hold(...)``. |n| if both duration and till are specified, the component will become current at the sum of these two. @@ -4191,13 +4317,13 @@ def passivate(self, mode='*'): mode : str preferred mode |n| - will be used in trace and can be used in animations|n| - if nothing specified, the mode will be unchanged.|n| + will be used in trace and can be used in animations |n| + if nothing is specified, the mode will be unchanged.|n| also mode_time will be set to now, if mode is set. Note ---- - if to be used for the current component (nearly always the case), use `yield self.passivate(...)`. + if to be used for the current component (nearly always the case), use ``yield self.passivate(...)``. ''' if self._status != current: self._checkisnotdata() @@ -4226,7 +4352,7 @@ def cancel(self, mode='*'): Note ---- - if to be used for the current component, use `yield self.cancel(...)`. + if to be used for the current component, use ``yield self.cancel(...)``. ''' if self._status != current: self._checkisnotdata() @@ -4253,13 +4379,13 @@ def standby(self, mode='*'): if nothing specified, the mode will be unchanged.|n| also mode_time will be set to now, if mode is set. - Notes - ----- + Note + ---- Not allowed for data components or main. if to be used for the current component (which will be nearly always the case), - use `yield self.standby(...)`. + use ``yield self.standby(...)``. ''' if self._status != current: self._checkisnotdata() @@ -4308,17 +4434,19 @@ def request(self, *args, fail_at=None, fail_delay=None, mode='*'): if nothing specified, the mode will be unchanged. |n| also mode_time will be set to now, if mode is set. - Notes - ----- + Note + ---- Not allowed for data components or main. - if to be used for the current component + If to be used for the current component (which will be nearly always the case), - use `yield self.request(...)``. + use ``yield self.request(...)``. - it is not allowed to claim a resource more than once by the same component |n| - the requested quantity may exceed the current capacity of a resource |n| - the parameter failed will be reset by a calling request + If the same resource is specified more that once, the quantities are summed |n| + + The requested quantity may exceed the current capacity of a resource |n| + + The parameter failed will be reset by a calling request or wait Example ------- @@ -4372,14 +4500,10 @@ def request(self, *args, fail_at=None, fail_delay=None, mode='*'): else: raise AssertionError('incorrect specifier', argsi) - if r in self._requests: - raise AssertionError(resource.name() + ' requested twice') if q <= 0: raise AssertionError('quantity ' + str(q) + ' <=0') - self._requests[r] = q - if q waits for s1.value()==False or s2.value=='on' or s3.value()==True s1 is at the tail of waiters, because of the set priority yield self.wait(s1,s2,all=True) |n| - --> waits for s1.value()==True and s2.value==True |n| + --> waits for s1.value()==True and s2.value==True |n| ''' if self._status != current: self._checkisnotdata() @@ -4633,8 +4759,8 @@ def wait(self, *args, fail_at=None, fail_delay=None, all=False, mode='*'): else: self._waits.append((state, value, 0)) - if len(self._waits)==0: - raise AssertionError ('no states specified') + if len(self._waits) == 0: + raise AssertionError('no states specified') self._trywait() if len(self._waits) != 0: @@ -4649,11 +4775,11 @@ def _trywait(self): honored = False break elif valuetype == 1: - if eval(value.replace('$',str(state._value))): + if eval(value.replace('$', 'state.value()')): honored = False break elif valuetype == 2: - if not value((state._value,self,state)): + if not value(state._value, self, state): honored = False break @@ -4665,11 +4791,11 @@ def _trywait(self): honored = True break elif valuetype == 1: - if eval(value.replace('$',str(state._value))): + if eval(value.replace('$', str(state._value))): honored = True break elif valuetype == 2: - if value((state._value,self,state)): + if value(state._value, self, state): honored = True break @@ -4683,9 +4809,8 @@ def _trywait(self): return honored - - def claimed_quanity(self): - '''' + def claimed_quantity(self): + ''' Parameters ---------- resource : Resoure @@ -4696,10 +4821,7 @@ def claimed_quanity(self): the claimed quantity from a resource : float or int if the resource is not claimed, 0 will be returned ''' - if resource in self._claims: - return self._claims[resource] - else: - return 0 + return self._claims.get(resource, 0) def claimed_resources(self): ''' @@ -4729,16 +4851,13 @@ def requested_quantity(self, resource): the requested (not yet honored) quantity from a resource : float or int if there is no request for the resource, 0 will be returned ''' - if resource in self._requests: - return self._requests[resource] - else: - return 0 + return self._requests.get(resource, 0) def failed(self): ''' Returns ------- - True, if the latest request has failed (either by timeout or external) : bool + True, if the latest request/wait has failed (either by timeout or external) : bool False, otherwise ''' return self._failed @@ -4821,7 +4940,7 @@ def mode(self, value=None): Returns ------- - mode of the component. any, usually str + mode of the component : any, usually str the mode is useful for tracing and animations. |n| Usually the mode will be set in a call to passivate, hold, activate, request or standby. ''' @@ -4954,8 +5073,8 @@ def enter(self, q): q : Queue queue to enter - Notes - ----- + Note + ---- the priority will be set to the priority of the tail component of the queue, if any or 0 if queue is empty @@ -4973,8 +5092,8 @@ def enter_at_head(self, q): q : Queue queue to enter - Notes - ----- + Note + ---- the priority will be set to the priority of the head component of the queue, if any or 0 if queue is empty @@ -4996,8 +5115,8 @@ def enter_in_front_of(self, q, poscomponent): poscomponent : Component component to be entered in front of - Notes - ----- + Note + ---- the priority will be set to the priority of poscomponent ''' @@ -5018,8 +5137,8 @@ def enter_behind(self, q, poscomponent): poscomponent : Component component to be entered behind - Notes - ----- + Note + ---- the priority will be set to the priority of poscomponent ''' @@ -5040,8 +5159,8 @@ def enter_sorted(self, q, priority): priority: float priority in the queue - Notes - ----- + Note + ---- The component is placed just before the first component with a priority > given priority ''' @@ -5072,8 +5191,8 @@ def leave(self, q): q : Queue queue to leave - Notes - ----- + Note + ---- statistics are updated accordingly ''' @@ -5093,7 +5212,7 @@ def leave(self, q): def priority(self, q, priority=None): ''' - gest/sets the priority of a component in a queue + gets/sets the priority of a component in a queue Parameters ---------- @@ -5106,10 +5225,10 @@ def priority(self, q, priority=None): Returns ------- - the priority of the component in the queue + the priority of the component in the queue : float - Notes - ----- + Note + ---- if you change the priority, the order of the queue may change ''' @@ -5214,25 +5333,22 @@ def status(self): returns the status of a component possible values are - - data - - passive - - scheduled - - requesting - - current - - standby + - data + - passive + - scheduled + - requesting + - current + - standby ''' if len(self._requests) > 0: return requesting - if len(self._waits) > 0 : + if len(self._waits) > 0: return waiting return self._status def _member(self, q): - try: - return self._qmembers[q] - except: - return None + return self._qmembers.get(q,None) def _checknotinqueue(self, q): mx = self._member(q) @@ -5565,20 +5681,21 @@ class Cdf(_Distribution): ---------- spec : list or tuple list with x-values and corresponding cumulative density - (x1,c1,x2,c2, ...xn,cn) - + (x1,c1,x2,c2, ...xn,cn) |n| + Requirements: + + x1<=x2<= ...<=xn |n| + c1<=c2<=cn |n| + c1=0 |n| + cn>0 |n| + all cumulative densities are auto scaled according to cn, + so no need to set cn to 1 or 100. + randomstream: randomstream if omitted, random will be used |n| if used as random.Random(12299) it defines a new stream with the specified seed - requirements: - x1<=x2<= ...<=xn |n| - c1<=c2<=cn |n| - c1=0 |n| - cn>0 |n| - all cumulative densities are auto scaled according to cn, - so no need to set cn to 1 or 100. ''' def __init__(self, spec, randomstream=None): @@ -5654,21 +5771,22 @@ class Pdf(_Distribution): ''' Probability distribution function - Pdf(spec,spec2,seed) + Pdf(spec,probabilities,seed) Parameters ---------- spec : list or tuple - list with x-values and corresponding probability |n| - (x1,p1,x2,p2, ...xn,pn) |n| - |n| - or, if spec2 is present: |n| - |n| - list with x-values - - spec2 : list, tuple or float + either + + - if no possibilities specified: |n| + list with x-values and corresponding probability + (x0, p0, x1, p1, ...xn,pn) |n| + - if probabilities is specified: |n| + list with x-values + + probabilities : list, tuple or float if omitted, spec contains the probabilities |n| - the list contains the probabilities of the corresponding + the list (p0, p1, ...pn) contains the probabilities of the corresponding x-values from spec. |n| alternatively, if a float is given (e.g. 1), all x-values have equal probability. The value is not important. @@ -5678,16 +5796,17 @@ class Pdf(_Distribution): if used as random.Random(12299) it assigns a new stream with the specified seed - requirements: - p1+p2=...+pn>0 |n| - all densities are auto scaled according to the sum of p1 to pn, - so no need to have p1 to pn add up to 1 or 100. |n| - the x-values may be any type. If it is a salabim distribution, - not the distribution, but a sample will be returned when - calling sample. + Note + ---- + p0+p1=...+pn>0 |n| + all densities are auto scaled according to the sum of p0 to pn, + so no need to have p0 to pn add up to 1 or 100. |n| + The x-values may be any type. |n| + If it is a salabim distribution, not the distribution, + but a sample will be returned when calling sample. ''' - def __init__(self, spec1, spec2=None, randomstream=None): + def __init__(self, spec1, probabilities=None, randomstream=None): self._x = [0] # just a place holder self._cum = [0] if randomstream is None: @@ -5794,20 +5913,20 @@ class Distribution(_Distribution): ---------- spec : str - string containing a valid salabim distribution, where only the first - letters are relevant and casing is not important + letters are relevant and casing is not important - string containing one float (c1), resulting in Constant(c1) - string containing two floats seperated by a comma (c1,c2), - resulting in a Uniform(c1,c2) + resulting in a Uniform(c1,c2) - string containing three floats, separated by commas (c1,c2,c3), - resulting in a Triangular(c1,c2,c3) + resulting in a Triangular(c1,c2,c3) randomstream : randomstream if omitted, random will be used |n| if used as random.Random(12299) it assigns a new stream with the specified seed |n| - Notes - ----- + Note + ---- The randomstream in the specifying string is ignored. |n| It is possible to use expressions in the specification, as long these are valid within the context of the salabim module, which usually implies @@ -5874,7 +5993,7 @@ def __repr__(self): return self._distribution.__repr__() def print_info(self): - self._distribution.print_info() + self._distribution.print_info() def sample(self): ''' @@ -5914,11 +6033,30 @@ class State(object): if True (default) , the waiters queue and the value are monitored |n| if False, monitoring is disabled. + type : str + specifies how the state values are monitored. Using a + int, uint of float type results in less memory usage and better + performance. Note that you avoid the number not to use + as this is used to indicate 'off' + + - 'any' (default) stores values in a list. This allows for + non numeric values. In calculations the values are + forced to a numeric value (0 if not possible) do not use -inf + - 'bool' bool (False, True). Actually integer >= 0 <= 254 1 byte do not use 255 + - 'uint8' integer >= 0 <= 254 1 byte do not use 255 + - 'int16' integer >= -32767 <= 32767 2 bytes do not use -32768 + - 'uint16' integer >= 0 <= 65534 2 bytes do not use 65535 + - 'int32' integer >= -2147483647 <= 2147483647 4 bytes do not use -2147483648 + - 'uint32' integer >= 0 <= 4294967294 4 bytes do not use 4294967295 + - 'int64' integer >= -9223372036854775807 <= 9223372036854775807 8 bytes do not use -9223372036854775808 + - 'uint64' integer >= 0 <= 18446744073709551614 8 bytes do not use 18446744073709551615 + - 'float' float 8 bytes do not use -inf + env : Environment environment to be used |n| if omitted, _default_env is used ''' - def __init__(self, name=None, value=False, env=None, monitor=True): + def __init__(self, name=None, value=False, type='any', monitor=True, env=None): if env is None: self.env = _default_env else: @@ -5928,11 +6066,11 @@ def __init__(self, name=None, value=False, env=None, monitor=True): self.name(name) self._value = value self._waiters = Queue( - name=('waiters of ',self), + name=('waiters of ', self), monitor=monitor, env=self.env, _isinternal=True) self.value = MonitorTimestamp( - name=('Value of ',self), - getter=self._get_value, monitor=monitor, env=self.env) + name=('Value of ', self), + getter=self._get_value, monitor=monitor, type=type, env=self.env) self.env.print_trace( '', '', self.name() + ' create', 'value= ' + str(self._value)) @@ -5959,7 +6097,7 @@ def print_info(self): values = values + ', ' values = values + str(value) - print(' ' + pad(c.name(), 20),' value(s): '+values) + print(' ' + pad(c.name(), 20), ' value(s): '+values) def __call__(self): return self._value @@ -5985,8 +6123,8 @@ def set(self, value=True): if there is a change, the waiters queue will be checked to see whether there are waiting components to be honored - Notes - ----- + Note + ---- This method is identical to reset, except the default value is True. ''' self.env.print_trace('', '', self.name()+' set', 'value = ' + str(value)) @@ -6006,8 +6144,8 @@ def reset(self, value=False): if there is a change, the waiters queue will be checked to see whether there are waiting components to be honored - Notes - ----- + Note + ---- This method is identical to set, except the default value is False. ''' self.env.print_trace('', '', self.name()+' reset', 'value = ' + str(value)) @@ -6033,9 +6171,9 @@ def trigger(self, value=True, value_after=None, max=inf): maximum number of components to be honored for the trigger value |n| default: inf - Notes - ----- - The value of the state will be set to value, then at most + Note + ---- + The value of the state will be set to value, then at most max waiting components for this state will be honored and next the value will be set to value_after and again checked for possible honors. @@ -6073,13 +6211,29 @@ def monitor(self, value=None): if False, monitoring is disabled |n| if not specified, no change - Notes - ----- + Note + ---- it is possible to individually control requesters().monitor(), value.monitor() ''' self.requesters().monitor(value) self.value.monitor(value) + + def reset_monitors(monitor=None): + ''' + resets the timestamped monitor for the state’s value + + Parameters + ---------- + monitor : bool + if True (default}, monitoring will be on. |n| + if False, monitoring is disabled + + Note + ---- + Equivalent to ``state.value.reset()`` + ''' + self.value.reset() def _get_value(self): return self._value @@ -6136,7 +6290,7 @@ def waiters(self): ''' Returns ------- - queue containing all components waiting for this state + queue containing all components waiting for this state : Queue ''' return self._waiters @@ -6163,14 +6317,14 @@ class Resource(object): if the resource is actually just a level. |n| if False, claims belong to a component. - env : Environment - environment to be used |n| - if omitted, _default_env is used - monitor : bool if True (default) , the requesters queue, the claimers queue, the capacity, the available_quantity and the claimed_quantity are monitored |n| if False, monitoring is disabled. + + env : Environment + environment to be used |n| + if omitted, _default_env is used ''' def __init__(self, name=None, capacity=1, @@ -6191,21 +6345,21 @@ def __init__(self, name=None, capacity=1, monitor=monitor, env=self.env, _isinternal=True) self._claimed_quantity = 0 self._anonymous = anonymous - self._minq=inf + self._minq = inf self.capacity = MonitorTimestamp( - ('Capacity of ',self), - getter=self._get_capacity, monitor=monitor, env=self.env) + ('Capacity of ', self), + getter=self._get_capacity, monitor=monitor, type='float', env=self.env) self.claimed_quantity = MonitorTimestamp( - ('Claimed quantity of ',self), - getter=self._get_claimed_quantity, monitor=monitor, env=self.env) + ('Claimed quantity of ', self), + getter=self._get_claimed_quantity, monitor=monitor, type='float', env=self.env) self.available_quantity = MonitorTimestamp( - ('Available quantity of ',self), - getter=self._get_available_quantity, monitor=monitor, env=self.env) + ('Available quantity of ', self), + getter=self._get_available_quantity, monitor=monitor, type='float', env=self.env) self.env.print_trace( '', '', self.name() + ' create', 'capacity=' + str(self._capacity) + (' anonymous' if self._anonymous else '')) - def reset(monitor=None): + def reset_monitors(monitor=None): ''' resets the resource monitors and timestamped monitors @@ -6215,14 +6369,18 @@ def reset(monitor=None): if True (default}, monitoring will be on. |n| if False, monitoring is disabled - Notes - ----- - it is possible to reset individual monitoring with claimers().reset() and requesters().reset, - capacity.reset(), available_quantity.reset() or claimed_quantity.reset() + Note + ---- + it is possible to reset individual monitoring with + claimers().reset_monitors(), + requesters().reset_monitors, + capacity.reset(), + available_quantity.reset() or + claimed_quantity.reset() ''' - self.requesters().reset(monitor) - self.claimers().reset(monitor) + self.requesters().reset_monitors(monitor) + self.claimers().reset_monitors(monitor) self.capacity.reset(monitor) self.available_quantity.reset(monitor) self.claimed_quantity.reset(monitor) @@ -6255,10 +6413,11 @@ def monitor(self, value=None): if False, monitoring is disabled |n| if not specified, no change - Notes - ----- - it is possible to individually control monitoring with claimers().monitor() and requesters().monitor(), - capacity.monitor(), available_quantity.monitor) or claimed_quantity.monitor() + Note + ---- + it is possible to individually control monitoring with claimers().monitor() + and requesters().monitor(), capacity.monitor(), available_quantity.monitor) + or claimed_quantity.monitor() ''' self.requesters().monitor(value) self.claimers().monitor(value) @@ -6319,8 +6478,8 @@ def release(self, quantity=None): for non-anonymous resources, all components claiming from this resource will be released. - Notes - ----- + Note + ---- quantity may not be specified for a non-anomymous resoure ''' @@ -6352,7 +6511,7 @@ def requesters(self): ''' Return ------ - queue containing all components with not yet honored requests. + queue containing all components with not yet honored requests: Queue ''' return self._requesters @@ -6360,8 +6519,8 @@ def claimers(self): ''' Returns ------- - queue with all components claiming from the resource. |n| - will be an empty queue for an anonymous resource + queue with all components claiming from the resource: Queue + will be an empty queue for an anonymous resource ''' return self._claimers @@ -6579,8 +6738,8 @@ def colorinterpolate(t, t0, t1, v0, v1): ------- f(t) : float - Notes - ----- + Note + ---- Note that no extrapolation is done, i.e f(t)=v0 for tt1. |n| This function is heavily used during animation. @@ -6615,8 +6774,8 @@ def interpolate(t, t0, t1, v0, v1): ------- f(t) : float - Notes - ----- + Note + ---- Note that no extrapolation is done, i.e f(t)=v0 for tt1. |n| This function is heavily used during animation. @@ -6682,8 +6841,8 @@ def tracetext(): def _set_name(name, _nameserialize, object): oldname = getattr(object, '_base_name', None) - if isinstance(name,tuple): - auto=False + if isinstance(name, tuple): + auto = False else: auto = (('*' + name)[-1] == '.') # * added to allow for null string @@ -6714,17 +6873,19 @@ def _set_name(name, _nameserialize, object): object._base_name = name object._sequence_number = sequence_number + def _decode_name(name): - if isinstance(name,tuple): + if isinstance(name, tuple): return name[0]+name[1].name() else: return name + def _decode_base_name(basename): - if isinstance(name,tuple): - return basename[0]+basename[1].basename() + if isinstance(name, tuple): + return basename[0] + basename[1].basename() else: - return basename + return basename def pad(txt, n): @@ -6736,25 +6897,66 @@ def pad(txt, n): def rpad(txt, n): return txt.rjust(n)[:n] + + +def fn(x, l, d): + if math.isnan(x): + return ('{:' + str(l) + 's}').format('') + if x >= 10**(l - d - 1): + return ('{:' + str(l) + '.' + str(l-d-3)+'e}').format(x) + if x == int(x): + return ('{:' + str(l-d-1) + 'd}{:' + str(d+1) + 's}').format(int(x), '') + return ('{:' + str(l) + '.' + str(d) + 'f}').format(x) + +_lookup_arraytype = { + 'bool': 'B', + 'int8': 'b', + 'uint8': 'B', + 'int16': 'h', + 'uint16': 'H', + 'int32': 'i', + 'uint32': 'I', + 'int64': 'l', + 'uint64': 'L', + 'float': 'd', + 'double': 'd', + 'any': 'any' + } + +_lookup_off = { + 'b': -128, + 'B': 255, + 'h': -32768, + 'H': 65535, + 'i': -2147483648, + 'I': 4294967295, + 'l': -9223372036854775808, + 'L': 18446744073709551615, + 'd': -np.inf, + 'any': -np.inf + } def list_to_numeric_array(l): x = np.array(l) - if x.dtype not in (np.float, np.int): - x=[] + if not str(x.dtype) in _lookup_arraytype: + x = [] for v in l: try: - v = float(v) - except ValueError: - v = 0 - vint = int(v) - if v == vint: + vint = int(v) + except: + vint = 0 + try: + vfloat = float(v) + except: + vfloat = 0 + if vint == vfloat: x.append(vint) else: - x.append(v) + x.append(vfloat) x = np.array(x) return x - + def normalize(s): res = '' @@ -7152,13 +7354,62 @@ def _pythonista_fonts(): return font_dict -def _fonts(): +@functools.lru_cache() +def fonts(): if Pythonista: return _pythonista_fonts() else: return _ttf_fonts() +@functools.lru_cache() +def getfont(fontname, fontsize): # fontsize in screen_coordinates! + ''' + internal funtion to get and cache fonts + ''' + standardfonts={ + '': 'calibri', + 'std': 'calibri', + 'mono': 'DejaVuSansMono', + 'narrow': 'mplus-1m-regular'} + if fontname.lower() in standardfonts: + fontname = standardfonts[fontname.lower()] + + salabim_dir = os.path.dirname(__file__) + os.sep + + if isinstance(fontname, str): + fontlist = (fontname,) + else: + fontlist = fontname + + font = None + for ifont in itertools.chain(fontlist, ('calibri', 'arial', 'arialmt')): + try: + font = ImageFont.truetype(font=ifont, size=int(fontsize)) + break + except: + pass + try: + font = ImageFont.truetype(font=salabim_dir + ifont +'.ttf', + size=int(fontsize)) + break + except: + pass + + ifont = fonts().get(normalize(ifont)) + if ifont is not None: + try: + font = ImageFont.truetype(font=ifont, size=int(fontsize)) + break + except: + pass + + if font is None: # last resort + font = ImageFont.load_default() + + return font + + def _show_pythonista_fonts(): fontnames = sorted(_pythonista_fonts().values(), key=str.lower) for font in fontnames: @@ -7179,7 +7430,7 @@ def _show_ttf_fonts(): def show_fonts(): ''' - show (prints) all available fonts on this machine + show (print) all available fonts on this machine ''' if Pythonista: @@ -7190,7 +7441,7 @@ def show_fonts(): def show_colornames(): ''' - show (prints) all available colours and their value + show (print) all available color names and their value. ''' names = sorted(colornames().keys()) @@ -7206,170 +7457,6 @@ def default_env(): ''' return _default_env - -def main(): - ''' - Returns - ------- - main component of the default environment : Component - ''' - return _default_env._main - - -def now(): - ''' - Returns - ------- - the current simulation time of the default environment : float - ''' - return _default_env._now - - -def trace(value=None): - ''' - trace status of the default environment - - Parameters - ---------- - value : bool - new trace status |n| - if omitted, no change - - Returns - ------- - trace status : bool - - Note - ---- - If you want to test the status, always include parentheses, like |n| - if sim.trace(): - ''' - if value is not None: - self._default_env._trace = value - return _default_env._trace - - -def current_component(): - ''' - Returns - ------- - the current_component of the default environment : Component - ''' - return _default_env._current_component - - -def run(*args, **kwargs): - ''' - start execution of the simulation for the default environment - - Parameters - ---------- - duration : float - schedule with a delay of duration |n| - if 0, now is used - - till : float - schedule time |n| - if omitted, 0 is assumed - - Note - ---- - only issue run() from the main level run for the default environment - ''' - _default_env.run(*args, **kwargs) - - -def animation_parameters(*args, **kwargs): - ''' - set animation_parameters for the default environment - - Parameters - ---------- - animate : bool - animate indicator |n| - if omitted, True, i.e. animation |n| - Installation of PIL is required for animation. - - speed : float - speed |n| - specifies how much faster or slower than real time the animation will run. - e.g. if 2, 2 simulation time units will be displayed per second. - - width : int - width of the animation in screen coordinates |n| - if omitted, no change. At init of the environment, the width will be - set to 1024 for CPython and the current screen width for Pythonista. - - height : int - height of the animation in screen coordinates |n| - if omitted, no change. At init of the environment, the height will be - set to 768 for CPython and the current screen height for Pythonista. - - x0 : float - user x-coordinate of the lower left corner |n| - if omitted, no change. At init of the environment, x0 will be set to 0. - - y0 : float - user y_coordinate of the lower left corner |n| - if omitted, no change. At init of the environment, y0 will be set to 0. - - x1 : float - user x-coordinate of the lower right corner |n| - if omitted, no change. At init of the environment, x1 will be set to 1024 - for CPython and the current screen width for Pythonista. - - background_color : colorspec - color of the background |n| - if omitted, no change. At init of the environment, this will be set to white. - - fps : float - number of frames per second - - modelname : str - name of model to be shown in upper left corner, - along with text 'a salabim model' |n| - if omitted, no change. At init of the environment, this will be set - to the null string, which implies suppression of this feature. - - use_toplevel : bool - if salabim animation is used in parallel with - other modules using tkinter, it might be necessary to - initialize the root with tkinter.TopLevel(). - In that case, set this parameter to True. |n| - if False (default), the root will be initialized with tkinter.Tk() - - show_fps : bool - if True, show the number of frames per second (default)|n| - if False, do not show the number of frames per second - - show_speed: bool - if True, show the animation speed (default)|n| - if False, do not show the animation speed - - show_time: bool - if True, show the time (default)|n| - if False, do not show the time - - video : str - if video is not omitted, a mp4 format video with the name video - will be created. |n| - The video has to have a .mp4 etension |n| - This requires installation of numpy and opencv (cv2). - - Notes - ----- - The y-coordinate of the upper right corner is determined automatically - in such a way that the x and scaling are the same. |n| - - Note that changing the parameters x0, x1, y0, width, height, background_color, modelname, - use_toplevelmand video, animate has no effect on the current animation. - So to avoid confusion, do not use change these parameters when an animation is running. |n| - On the other hand, changing speed, show_fps, show_time, show_speed and fps can be useful in - a running animation. - ''' - _default_env.animation_parameters(*args, **kwargs) - - if __name__ == '__main__': try: import salabim_test diff --git a/salabim_test.py b/salabim_test.py index 21ddc3b..5cfdb3b 100644 --- a/salabim_test.py +++ b/salabim_test.py @@ -6,7 +6,11 @@ Pythonista=(platform.system()=='Darwin') def test(): - test34() + test35() + +def test35(): + env=sim.Environment() + sim.getfont('cour',10) def test34(): @@ -44,7 +48,7 @@ def process(self): x=X() y=Y() z=Z() - sim.run(10) + env.run(10) s1.print_statistics() @@ -63,13 +67,13 @@ def process(self): class Y(sim.Component): def process(self): while True: - yield self.wait((s1,lambda x: x[0]/2>self.env.now())) + yield self.wait((s1,lambda x, component, state: x/2>self.env.now())) yield self.hold(1.5) class Z(sim.Component): def process(self): while True: - yield self.wait((s2,lambda x: x[0] in ("red","yellow"))) + yield self.wait((s2,lambda x, component, state: x in ("red","yellow"))) yield self.hold(1.5) @@ -82,7 +86,9 @@ def process(self): x=X() y=Y() z=Z() - sim.run(10) + env.run(10) + sim.show_fonts() + print(sim.fonts()) def test32(): @@ -108,7 +114,7 @@ def process(self): go=sim.State() x=X() y=Y() - sim.run() + env.run() def test31(): @@ -138,7 +144,7 @@ def process(self): q=sim.Queue('q.') x=X() y=Y() - sim.run(10) + env.run(10) print('value at ',env.now(),s1.get()) print (s1.value.xduration()) print(s1.value.tx()) @@ -215,10 +221,10 @@ def _get_a(self): def _now(self): return self.env._now - def process(self): - + m2.tally() yield self.hold(1) + self.a=4 m2.tally() m2.monitor(True) print('3',m2.xt()) @@ -236,21 +242,24 @@ def process(self): de=sim.Environment() - m1=sim.Monitor('m1') + m1=sim.Monitor('m1',type='uint8') print (m1.mean()) m1.tally(10) m1.tally(15) m1.tally(20) - m1.tally(20) + m1.tally(92) + m1.tally(0) + m1.tally(12) m1.tally(0) + print ('m1.x()',m1.x(force_numeric=False)) print ('m1',m1.mean(),m1.std(),m1.percentile(50)) print ('m1 ex0',m1.mean(ex0=True),m1.std(ex0=True),m1.percentile(50,ex0=True)) x=X() x.a=10 - m2=sim.MonitorTimestamp('m2',getter=x._get_a) + m2=sim.MonitorTimestamp('m2',getter=x._get_a,type='int8') print('1',m2.xt()) - m2.monitor(False) + m2.monitor(True) m2.tally() print('a',m2.xt()) # m2.monitor(True) @@ -367,7 +376,7 @@ def action2(self,param): class Monitor(sim.Component): def process(self): - while sim.now()<30: + while env.now()<30: yield self.standby() de=sim.Environment(trace=True) @@ -391,7 +400,7 @@ def process(self): y=Y(name='y') # y.activate(at=20) - sim.run(till=35) + env.run(till=35) # env.run(4) @@ -523,7 +532,7 @@ def process(self): y.append(c) z=Z(name='z') - sim.run(till=1000) + env.run(till=1000) def test5(): print('test5') @@ -582,7 +591,7 @@ def process(self): y=Y(name='y') - sim.run(till=21) + env.run(till=21) def test6(): print('test6') @@ -598,9 +607,9 @@ def process(self): q.name('Rij.') print (q.name()) q.clear() - sim.run(till=10) + env.run(till=10) x.reactivate() - sim.run() + env.run() def test7(): print('test7') @@ -647,9 +656,9 @@ def process(self): - sim.run(10) + env.run(10) r2.capacity(2) - sim.run(20) + env.run(20) print(sim.default_env) @@ -794,9 +803,9 @@ def process(self): # assert False height=768 - sim.animation_parameters(modelname='Salabim test') - sim.run(15) - sim.run(till=30) + env.animation_parameters(modelname='Salabim test') + env.run(15) + env.run(till=30) print('THE END') @@ -822,7 +831,7 @@ def process(self): de=sim.Environment(trace=True) x=X() y=Y() - sim.run(till=6) + env.run(till=6) def test10(): @@ -909,7 +918,7 @@ class X(sim.Component): print('---') - sim.run(till=100) + env.run(till=100) def test12(): @@ -921,7 +930,7 @@ def process(self): yield self.hold(1) de=sim.Environment(trace=True) - sim.animation_parameters(speed=1) + env.animation_parameters(speed=1) a=sim.Environment(name='piet.') b=sim.Environment(name='piet.') c=sim.Environment(name='piet.') @@ -933,10 +942,10 @@ def process(self): X(auto_start=False) X(auto_start=False) X() - sim.animation_parameters(speed=0.1,video='x.mp4') - sim.run(4) - sim.run(2) - sim.run(4) + env.animation_parameters(speed=0.1,video='x.mp4') + env.run(4) + env.run(2) + env.run(4) def test13(): @@ -978,10 +987,10 @@ def test15(): def test16(): de=sim.Environment() - sim.animation_parameters() + env.animation_parameters() a=sim.Animate(text='Test',x0=100,y0=100,fontsize0=30,fillcolor0='red') a=sim.Animate(line0=(0,0,500,500),linecolor0='white',linewidth0=6) - sim.run() + env.run() def test17(): @@ -1001,11 +1010,11 @@ def actionb(): sl=sim.AnimateSlider(x=300,y=700,width=300) sim.Animate(text='Text',x0=700,y0=750,font='Times NEWRomian Italic',fontsize0=30) de.animation_parameters(animate=True) - sim.run(5) - sim.animation_parameters(animate=False) - sim.run(100) - sim.animation_parameters(animate=True,background_color='yellow') - sim.run(10) + env.run(5) + env.animation_parameters(animate=False) + env.run(100) + env.animation_parameters(animate=True,background_color='yellow') + env.run(10) def test18(): for j in range(2): @@ -1036,7 +1045,7 @@ def test20(): y=y-50 de.animation_parameters(animate=True) - sim.run() + env.run() def test21(): @@ -1057,13 +1066,13 @@ class Y(sim.Component): def process(self): while True: yield self.hold(1) - print('status of x=',sim.now(),x.status()()) + print('status of x=',env.now(),x.status()()) de=sim.Environment() x=X() Y() - sim.run(12) + env.run(12) def test23(): sim.a=100