salabim 24.0.12__py3-none-any.whl → 24.0.14.post3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
salabim/salabim.py CHANGED
@@ -1,13 +1,13 @@
1
- # _ _ _ ____ _ _ ___ _ ____
2
- # ___ __ _ | | __ _ | |__ (_) _ __ ___ |___ \ | || | / _ \ / ||___ \
3
- # / __| / _` || | / _` || '_ \ | || '_ ` _ \ __) || || |_ | | | | | | __) |
4
- # \__ \| (_| || || (_| || |_) || || | | | | | / __/ |__ _| _ | |_| | _ | | / __/
5
- # |___/ \__,_||_| \__,_||_.__/ |_||_| |_| |_| |_____| |_| (_) \___/ (_)|_||_____|
1
+ # _ _ _ ____ _ _ ___ _ _ _
2
+ # ___ __ _ | | __ _ | |__ (_) _ __ ___ |___ \ | || | / _ \ / || || |
3
+ # / __| / _` || | / _` || '_ \ | || '_ ` _ \ __) || || |_ | | | | | || || |_
4
+ # \__ \| (_| || || (_| || |_) || || | | | | | / __/ |__ _| _ | |_| | _ | ||__ _|
5
+ # |___/ \__,_||_| \__,_||_.__/ |_||_| |_| |_| |_____| |_| (_) \___/ (_)|_| |_|
6
6
  # discrete event simulation
7
7
  #
8
8
  # see www.salabim.org for more information, the documentation and license information
9
9
 
10
- __version__ = "24.0.12"
10
+ __version__ = "24.0.14"
11
11
 
12
12
  import heapq
13
13
  import random
@@ -60,7 +60,7 @@ PyDroid = sys.platform == "linux" and any("pydroid" in v for v in os.environ.val
60
60
  PyPy = platform.python_implementation() == "PyPy"
61
61
  Chromebook = "penguin" in platform.uname()
62
62
  PythonInExcel = not ("__file__" in globals())
63
- AnacondaCode = sys.platform=="emscripten"
63
+ AnacondaCode = sys.platform == "emscripten"
64
64
 
65
65
 
66
66
  def a_log(*args):
@@ -154,8 +154,9 @@ nan = float("nan")
154
154
  if Pythonista or AnacondaCode or PythonInExcel:
155
155
  _yieldless = False
156
156
  else:
157
- _yieldless=True
158
-
157
+ _yieldless = True
158
+
159
+
159
160
  class QueueFullError(Exception):
160
161
  pass
161
162
 
@@ -4144,7 +4145,7 @@ if Pythonista:
4144
4145
  try:
4145
4146
  ims = scene.load_pil_image(capture_image)
4146
4147
  except SystemError:
4147
- im_file = "temp.png" # hack for Pythonista 3.4 ***
4148
+ im_file = "temp.png" # hack for Pythonista 3.4
4148
4149
  capture_image.save(im_file, "PNG")
4149
4150
  ims = scene.load_image_file(im_file)
4150
4151
  scene.image(ims, 0, 0, *capture_image.size)
@@ -5717,7 +5718,7 @@ class Queue:
5717
5718
 
5718
5719
  class Store(Queue):
5719
5720
  def __init__(self, name: str = None, monitor: Any = True, fill: Iterable = None, capacity: float = inf, env: "Environment" = None, *args, **kwargs) -> None:
5720
- super().__init__(name=name, monitor=monitor,fill=None,capacity=capacity, env=env, *args, **kwargs)
5721
+ super().__init__(name=name, monitor=monitor, fill=None, capacity=capacity, env=env, *args, **kwargs)
5721
5722
 
5722
5723
  with self.env.suppress_trace():
5723
5724
  self._to_store_requesters = Queue(f"{name}.to_store_requesters", env=env)
@@ -7981,6 +7982,10 @@ by adding:
7981
7982
  ----
7982
7983
  Only if yieldless is False: if to be used for the current component, use ``yield self.cancel()``.
7983
7984
  """
7985
+ if self.status.value == data:
7986
+ if self.env._trace:
7987
+ self.env.print_trace("", "", "cancel (on data component) " + self.name() + " " + self._modetxt())
7988
+ return
7984
7989
  if self.status.value != current:
7985
7990
  self._checkisnotdata()
7986
7991
  self._remove()
@@ -9048,10 +9053,7 @@ by adding:
9048
9053
  self.status._value = waiting
9049
9054
  self._reschedule(scheduled_time, schedule_priority, urgent, "wait", cap_now)
9050
9055
  else:
9051
- return # ***
9052
- if self.env._yieldless:
9053
- if self is self.env._current_component:
9054
- self.env._glet.switch()
9056
+ return
9055
9057
 
9056
9058
  def _trywait(self):
9057
9059
  if self.status.value == interrupted:
@@ -10000,6 +10002,211 @@ by adding:
10000
10002
  return None
10001
10003
 
10002
10004
 
10005
+ class Event(Component):
10006
+ """
10007
+ Event object
10008
+
10009
+ An event object is a specialized Component that is usually not subclassed.
10010
+
10011
+ Apart from the usual Component parameters it has an action parameter, to specifies what should
10012
+ happen after becoming active. This action is usually a lambda function.
10013
+
10014
+ Parameters
10015
+ ----------
10016
+ action : callable
10017
+ function called when the component becomes current.
10018
+
10019
+ action_string : str
10020
+ string to be printed in trace when action gets executed (default: "action")
10021
+
10022
+ name : str
10023
+ name of the component.
10024
+
10025
+ if the name ends with a period (.),
10026
+ auto serializing will be applied
10027
+
10028
+ if the name end with a comma,
10029
+ auto serializing starting at 1 will be applied
10030
+
10031
+ if omitted, the name will be derived from the class
10032
+ it is defined in (lowercased)
10033
+
10034
+ at : float or distribution
10035
+ schedule time
10036
+
10037
+ if omitted, now is used
10038
+
10039
+ if distribution, the distribution is sampled
10040
+
10041
+ delay : float or distributiom
10042
+ schedule with a delay
10043
+
10044
+ if omitted, no delay
10045
+
10046
+ if distribution, the distribution is sampled
10047
+
10048
+ priority : float
10049
+ priority
10050
+
10051
+ default: 0
10052
+
10053
+ if a component has the same time on the event list, this component is sorted accoring to
10054
+ the priority.
10055
+
10056
+ urgent : bool
10057
+ urgency indicator
10058
+
10059
+ if False (default), the component will be scheduled
10060
+ behind all other components scheduled
10061
+ for the same time and priority
10062
+
10063
+ if True, the component will be scheduled
10064
+ in front of all components scheduled
10065
+ for the same time and priority
10066
+
10067
+ suppress_trace : bool
10068
+ suppress_trace indicator
10069
+
10070
+ if True, this component will be excluded from the trace
10071
+
10072
+ If False (default), the component will be traced
10073
+
10074
+ Can be queried or set later with the suppress_trace method.
10075
+
10076
+ suppress_pause_at_step : bool
10077
+ suppress_pause_at_step indicator
10078
+
10079
+ if True, if this component becomes current, do not pause when stepping
10080
+
10081
+ If False (default), the component will be paused when stepping
10082
+
10083
+ Can be queried or set later with the suppress_pause_at_step method.
10084
+
10085
+ skip_standby : bool
10086
+ skip_standby indicator
10087
+
10088
+ if True, after this component became current, do not activate standby components
10089
+
10090
+ If False (default), after the component became current activate standby components
10091
+
10092
+ Can be queried or set later with the skip_standby method.
10093
+
10094
+ mode : str preferred
10095
+ mode
10096
+
10097
+ will be used in trace and can be used in animations
10098
+
10099
+ if omitted, the mode will be "".
10100
+
10101
+ also mode_time will be set to now.
10102
+
10103
+ cap_now : bool
10104
+ indicator whether times (at, delay) in the past are allowed. If, so now() will be used.
10105
+ default: sys.default_cap_now(), usualy False
10106
+
10107
+ env : Environment
10108
+ environment where the component is defined
10109
+
10110
+ if omitted, default_env will be used
10111
+ """
10112
+
10113
+ def __init__(
10114
+ self,
10115
+ action: Callable,
10116
+ action_string="action",
10117
+ name: str = None,
10118
+ at: Union[float, Callable] = None,
10119
+ delay: Union[float, Callable] = None,
10120
+ priority: float = None,
10121
+ urgent: bool = None,
10122
+ suppress_trace: bool = False,
10123
+ suppress_pause_at_step: bool = False,
10124
+ skip_standby: bool = False,
10125
+ mode: str = "",
10126
+ cap_now: bool = None,
10127
+ env: "Environment" = None,
10128
+ **kwargs,
10129
+ ):
10130
+ self._action = action
10131
+ self._action_string = action_string
10132
+ self._action_taken = False
10133
+ if env is None:
10134
+ env = g.default_env
10135
+ super().__init__(
10136
+ name=name,
10137
+ at=at,
10138
+ delay=delay,
10139
+ priority=priority,
10140
+ urgent=urgent,
10141
+ suppress_trace=suppress_trace,
10142
+ suppress_pause_at_step=suppress_pause_at_step,
10143
+ skip_standby=skip_standby,
10144
+ mode=mode,
10145
+ cap_now=cap_now,
10146
+ env=env,
10147
+ process="process" if env._yieldless else "process_yield",
10148
+ **kwargs,
10149
+ )
10150
+
10151
+ def process_yield(self):
10152
+ self.env.print_trace("", "", self._action_string, "")
10153
+ self._action()
10154
+ self._action_taken = True
10155
+ return
10156
+ yield 1 # just to make it a generator
10157
+
10158
+ def process(self):
10159
+ self.env.print_trace("", "", self._action_string, "")
10160
+ self._action()
10161
+ self._action_taken = True
10162
+
10163
+ def action(self, value=None):
10164
+ """
10165
+ action
10166
+
10167
+ Parameters
10168
+ ----------
10169
+ value : callable
10170
+ new action callable
10171
+
10172
+ Returns
10173
+ -------
10174
+ current action : callable
10175
+ """
10176
+ if value is not None:
10177
+ self._action = value
10178
+ return self._action
10179
+
10180
+ def action_string(self, value=None):
10181
+ """
10182
+ action_string
10183
+
10184
+ Parameters
10185
+ ----------
10186
+ value : string
10187
+ new action_string
10188
+
10189
+ Returns
10190
+ -------
10191
+ current action_string : string
10192
+ """
10193
+
10194
+ if value is not None:
10195
+ self._action_string = value
10196
+ return self._action_string
10197
+
10198
+ def action_taken(self):
10199
+ """
10200
+ action_taken
10201
+
10202
+ Returns
10203
+ -------
10204
+ action taken: bool
10205
+ True if action has been taken, False if not
10206
+ """
10207
+ return self._action_taken
10208
+
10209
+
10003
10210
  class Environment:
10004
10211
  """
10005
10212
  environment object
@@ -10791,7 +10998,7 @@ class Environment:
10791
10998
  if c.overridden_lineno:
10792
10999
  self.print_trace(self.time_to_str(self._now - self._offset), c.name(), "current", s0=un_na(c.overridden_lineno))
10793
11000
  else:
10794
- self.print_trace(self.time_to_str(self._now - self._offset), c.name(), "current", s0=un_na(c.lineno_txt())) # ***
11001
+ self.print_trace(self.time_to_str(self._now - self._offset), c.name(), "current", s0=un_na(c.lineno_txt()))
10795
11002
  if c == self._main:
10796
11003
  self.running = False
10797
11004
  return
@@ -11141,13 +11348,13 @@ class Environment:
11141
11348
 
11142
11349
  if width is not None:
11143
11350
  if self._width != width:
11144
- self._width = width
11351
+ self._width = int(width)
11145
11352
  frame_changed = True
11146
11353
  width_changed = True
11147
11354
 
11148
11355
  if height is not None:
11149
11356
  if self._height != height:
11150
- self._height = height
11357
+ self._height = int(height)
11151
11358
  frame_changed = True
11152
11359
  height_changed = True
11153
11360
 
@@ -11506,7 +11713,7 @@ class Environment:
11506
11713
  if self._video_pingpong:
11507
11714
  self._images.extend(self._images[::-1])
11508
11715
  if self._video_repeat == 1: # in case of repeat == 1, loop should not be specified (otherwise, it might show twice)
11509
- if PythonInExcel or AnacondaCode: # ***
11716
+ if PythonInExcel or AnacondaCode:
11510
11717
  with b64_file_handler(self._video_name, mode="b", result=_pie_result) as f:
11511
11718
  self._images[0].save(
11512
11719
  f,
@@ -11527,7 +11734,7 @@ class Environment:
11527
11734
  optimize=False,
11528
11735
  )
11529
11736
  else:
11530
- if PythonInExcel or AnacondaCode: # ***
11737
+ if PythonInExcel or AnacondaCode:
11531
11738
  with b64_file_handler(self._video_name, mode="b", result=_pie_result) as f:
11532
11739
  self._images[0].save(
11533
11740
  f,
@@ -11596,7 +11803,8 @@ class Environment:
11596
11803
  for ao in an_objects:
11597
11804
  ao.make_pil_image(self.t())
11598
11805
  if ao._image_visible and (include_topleft or not ao.getattr("in_topleft", False)):
11599
- image.paste(ao._image, (int(ao._image_x), int(self._height - ao._image_y - ao._image.size[1])), ao._image)
11806
+ image.paste(ao._image, (int(ao._image_x), int(self._height - ao._image_y - ao._image.size[1])),ao._image.convert("RGBA"),)
11807
+
11600
11808
 
11601
11809
  return image.convert(mode)
11602
11810
 
@@ -12993,7 +13201,7 @@ class Environment:
12993
13201
  if co is None:
12994
13202
  if len(g.canvas_objects) >= self._maximum_number_of_bitmaps:
12995
13203
  if overflow_image is None:
12996
- overflow_image = Image.new("RGBA", (self._width, self._height), (0, 0, 0, 0))
13204
+ overflow_image = Image.new("RGBA", (int(self._width), int(self._height)), (0, 0, 0, 0))
12997
13205
  overflow_image.paste(ao._image, (int(ao._image_x), int(self._height - ao._image_y - ao._image.size[1])), ao._image)
12998
13206
  ao.canvas_object = None
12999
13207
  else:
@@ -15218,10 +15426,20 @@ class Animate2dBase(DynamicClass):
15218
15426
  spec = self.image(t)
15219
15427
  image_container = ImageContainer(spec)
15220
15428
  width = self.width(t)
15429
+ height = self.height(t)
15430
+
15221
15431
  if width is None:
15222
- width = image_container.images[0].size[0]
15432
+ if height is None:
15433
+ width = image_container.images[0].size[0]
15434
+ height = image_container.images[0].size[1]
15435
+ else:
15436
+ width = height * image_container.images[0].size[0] / image_container.images[0].size[1]
15437
+ else:
15438
+ if height is None:
15439
+ height = width * image_container.images[0].size[1] / image_container.images[0].size[0]
15440
+ else:
15441
+ ...
15223
15442
 
15224
- height = width * image_container.images[0].size[1] / image_container.images[0].size[0]
15225
15443
  if not self.screen_coordinates:
15226
15444
  width *= self.env._scale
15227
15445
  height *= self.env._scale
@@ -15240,7 +15458,6 @@ class Animate2dBase(DynamicClass):
15240
15458
  offsety = offsety * self.env._scale
15241
15459
 
15242
15460
  alpha = int(self.alpha(t))
15243
-
15244
15461
  image, id = image_container.get_image(
15245
15462
  (t - self.animation_start(t)) * self.animation_speed(t),
15246
15463
  repeat=self.animation_repeat(t),
@@ -15248,6 +15465,7 @@ class Animate2dBase(DynamicClass):
15248
15465
  t_from=self.animation_from(t),
15249
15466
  t_to=self.animation_to(t),
15250
15467
  )
15468
+
15251
15469
  self._image_ident = (spec, id, width, height, angle, alpha, flip_horizontal, flip_vertical)
15252
15470
 
15253
15471
  if self._image_ident != self._image_ident_prev:
@@ -15312,8 +15530,6 @@ class Animate2dBase(DynamicClass):
15312
15530
 
15313
15531
  if not self.screen_coordinates:
15314
15532
  fontsize = fontsize * self.env._scale
15315
- # offsetx = offsetx * self.env._scale # ***
15316
- # offsety = offsety * self.env._scale # ***
15317
15533
  text_anchor = self.text_anchor(t)
15318
15534
 
15319
15535
  if self.attached_to:
@@ -15347,6 +15563,7 @@ class Animate2dBase(DynamicClass):
15347
15563
  qx = (x - self.env._x0) * self.env._scale
15348
15564
  qy = (y - self.env._y0) * self.env._scale
15349
15565
  max_lines = self.max_lines(t)
15566
+
15350
15567
  self._image_ident = (text, fontname, fontsize, angle, textcolor, max_lines)
15351
15568
  if self._image_ident != self._image_ident_prev:
15352
15569
  font, heightA = getfont(fontname, fontsize)
@@ -15529,6 +15746,9 @@ class AnimateClassic(Animate2dBase):
15529
15746
  def width(self, t):
15530
15747
  return self.master.width(t)
15531
15748
 
15749
+ def height(self, t):
15750
+ return self.master.height(t)
15751
+
15532
15752
  def anchor(self, t):
15533
15753
  return self.master.anchor(t)
15534
15754
 
@@ -15756,6 +15976,11 @@ class Animate:
15756
15976
 
15757
15977
  if omitted or None, no scaling
15758
15978
 
15979
+ height0 : float
15980
+ width of the image to be displayed at time t0
15981
+
15982
+ if omitted or None, no scaling
15983
+
15759
15984
  t1 : float
15760
15985
  time of end of the animation (default inf)
15761
15986
 
@@ -15824,6 +16049,9 @@ class Animate:
15824
16049
  width1 : float
15825
16050
  width of the image to be displayed at time t1 (default: width0)
15826
16051
 
16052
+ height1 : float
16053
+ width of the image to be displayed at time t1 (default: height0)
16054
+
15827
16055
  over3d : bool
15828
16056
  if True, this object will be rendered to the OpenGL window
15829
16057
 
@@ -15892,6 +16120,7 @@ class Animate:
15892
16120
  font -
15893
16121
  fontsize0,fontsize1 -
15894
16122
  width0,width1 -
16123
+ height0,height1 -
15895
16124
  ====================== ========= ========= ========= ========= ========= =========
15896
16125
  """
15897
16126
 
@@ -15927,6 +16156,7 @@ class Animate:
15927
16156
  alpha0: float = 255,
15928
16157
  fontsize0: float = 20,
15929
16158
  width0: float = None,
16159
+ height0: float = None,
15930
16160
  t1: float = None,
15931
16161
  x1: float = None,
15932
16162
  y1: float = None,
@@ -15945,6 +16175,7 @@ class Animate:
15945
16175
  alpha1: float = None,
15946
16176
  fontsize1: float = None,
15947
16177
  width1: float = None,
16178
+ height1: float = None,
15948
16179
  xy_anchor: str = "",
15949
16180
  over3d: bool = None,
15950
16181
  flip_horizontal: bool = False,
@@ -15991,10 +16222,12 @@ class Animate:
15991
16222
  self.text0 = text
15992
16223
 
15993
16224
  if image is None:
15994
- self.width0 = 0 # just to be able to interpolate
16225
+ self.width0 = 0 # just to be able to interpolat
16226
+ self.height0 = 0
15995
16227
  else:
15996
16228
  self.image0 = image
15997
16229
  self.width0 = width0 # None means original size
16230
+ self.height0 = height0
15998
16231
 
15999
16232
  self.as_points0 = as_points
16000
16233
  self.font0 = font
@@ -16056,7 +16289,7 @@ class Animate:
16056
16289
  self.alpha1 = self.alpha0 if alpha1 is None else alpha1
16057
16290
  self.fontsize1 = self.fontsize0 if fontsize1 is None else fontsize1
16058
16291
  self.width1 = self.width0 if width1 is None else width1
16059
-
16292
+ self.height1 = self.height0 if height1 is None else height1
16060
16293
  self.t1 = inf if t1 is None else t1
16061
16294
  if self.env._animate_debug:
16062
16295
  self.caller = self.env._frame_to_lineno(_get_caller_frame(), add_filename=True)
@@ -16115,6 +16348,7 @@ class Animate:
16115
16348
  alpha0=None,
16116
16349
  fontsize0=None,
16117
16350
  width0=None,
16351
+ height0=None,
16118
16352
  xy_anchor1=None,
16119
16353
  as_points=None,
16120
16354
  t1=None,
@@ -16135,6 +16369,7 @@ class Animate:
16135
16369
  alpha1=None,
16136
16370
  fontsize1=None,
16137
16371
  width1=None,
16372
+ height1=None,
16138
16373
  flip_horizontal=None,
16139
16374
  flip_vertical=None,
16140
16375
  animation_start=None,
@@ -16287,6 +16522,11 @@ class Animate:
16287
16522
 
16288
16523
  if None, the original width of the image will be used
16289
16524
 
16525
+ height0 : float
16526
+ height of the image to be displayed at time t0 (default see below)
16527
+
16528
+ if None, the original height of the image will be used
16529
+
16290
16530
  t1 : float
16291
16531
  time of end of the animation (default: inf)
16292
16532
 
@@ -16353,6 +16593,8 @@ class Animate:
16353
16593
  width1 : float
16354
16594
  width of the image to be displayed at time t1 (default: width0)
16355
16595
 
16596
+ height1 : float
16597
+ height of the image to be displayed at time t1 (default: height0)
16356
16598
 
16357
16599
  Note
16358
16600
  ----
@@ -16388,6 +16630,8 @@ class Animate:
16388
16630
  self.max_lines0 = max_lines
16389
16631
 
16390
16632
  self.width0 = self.width() if width0 is None else width0
16633
+ self.height0 = self.height() if height0 is None else height0
16634
+
16391
16635
  if image is not None:
16392
16636
  self.image0 = image
16393
16637
 
@@ -16434,6 +16678,7 @@ class Animate:
16434
16678
  self.alpha1 = self.alpha0 if alpha1 is None else alpha1
16435
16679
  self.fontsize1 = self.fontsize0 if fontsize1 is None else fontsize1
16436
16680
  self.width1 = self.width0 if width1 is None else width1
16681
+ self.height1 = self.height0 if height1 is None else height1
16437
16682
  self.xy_anchor1 = self.xy_anchor0 if xy_anchor1 is None else xy_anchor1
16438
16683
 
16439
16684
  self.t1 = inf if t1 is None else t1
@@ -16731,7 +16976,7 @@ class Animate:
16731
16976
 
16732
16977
  def width(self, t=None):
16733
16978
  """
16734
- width position of an animated image object. May be overridden.
16979
+ width of an animated image object. May be overridden.
16735
16980
 
16736
16981
  Parameters
16737
16982
  ----------
@@ -16756,6 +17001,33 @@ class Animate:
16756
17001
 
16757
17002
  return interpolate((self.env._now if t is None else t), self.t0, self.t1, width0, width1)
16758
17003
 
17004
+ def height(self, t=None):
17005
+ """
17006
+ height of an animated image object. May be overridden.
17007
+
17008
+ Parameters
17009
+ ----------
17010
+ t : float
17011
+ current time
17012
+
17013
+ Returns
17014
+ -------
17015
+ height : float
17016
+ default behaviour: linear interpolation between self.height0 and self.height1
17017
+
17018
+ if None, the original height of the image will be used
17019
+ """
17020
+ height0 = self.height0
17021
+ height1 = self.height1
17022
+ if height0 is None and height1 is None:
17023
+ return None
17024
+ if height0 is None:
17025
+ height0 = ImageContainer(self.image0).images[0].size[0]
17026
+ if height1 is None:
17027
+ height1 = ImageContainer(self.image1).images[0].size[0]
17028
+
17029
+ return interpolate((self.env._now if t is None else t), self.t0, self.t1, height0, height1)
17030
+
16759
17031
  def fontsize(self, t=None):
16760
17032
  """
16761
17033
  fontsize of an animate object. May be overridden.
@@ -19449,6 +19721,8 @@ class AnimateImage(Animate2dBase):
19449
19721
  width : float
19450
19722
  width of the image (default: None = no scaling)
19451
19723
 
19724
+ heighth : float
19725
+ height of the image (default: None = no scaling)
19452
19726
 
19453
19727
  text : str, tuple or list
19454
19728
  the text to be displayed
@@ -19584,6 +19858,7 @@ class AnimateImage(Animate2dBase):
19584
19858
  x: Union[float, Callable] = None,
19585
19859
  y: Union[float, Callable] = None,
19586
19860
  width: Union[float, Callable] = None,
19861
+ height: Union[float, Callable] = None,
19587
19862
  text: Union[str, Callable] = None,
19588
19863
  fontsize: Union[float, Callable] = None,
19589
19864
  textcolor: Union[ColorType, Callable] = None,
@@ -19626,6 +19901,7 @@ class AnimateImage(Animate2dBase):
19626
19901
  x=0,
19627
19902
  y=0,
19628
19903
  width=None,
19904
+ height=None,
19629
19905
  text="",
19630
19906
  fontsize=15,
19631
19907
  textcolor="bg",
@@ -19708,10 +19984,10 @@ class ComponentGenerator(Component):
19708
19984
 
19709
19985
  Parameters
19710
19986
  ----------
19711
- component_class : callable, usually a subclass of Component or Pdf or Cdf distribution
19987
+ component_class : callable, usually a subclass of Component or Pdf/Pmf or Cdf distribution
19712
19988
  the type of components to be generated
19713
19989
 
19714
- in case of a distribution, the Pdf or Cdf should return a callable
19990
+ in case of a distribution, the Pdf/Pmf or Cdf should return a callable
19715
19991
 
19716
19992
  generator_name : str
19717
19993
  name of the component generator.
@@ -19822,6 +20098,13 @@ class ComponentGenerator(Component):
19822
20098
 
19823
20099
  e.g. env.main().activate()
19824
20100
 
20101
+ moments : iterable
20102
+ specifies the moments when the components have to be generated. It is not required that these are sorted.
20103
+
20104
+ note that the moments are specified in the current time unit
20105
+
20106
+ cannot be used together with at, delay, till, duration, number, iat,force_at, force_till, disturbance or equidistant
20107
+
19825
20108
  env : Environment
19826
20109
  environment where the component is defined
19827
20110
 
@@ -19850,6 +20133,7 @@ class ComponentGenerator(Component):
19850
20133
  disturbance: Callable = None,
19851
20134
  equidistant: bool = False,
19852
20135
  at_end: Callable = None,
20136
+ moments: Iterable = None,
19853
20137
  env: "Environment" = None,
19854
20138
  **kwargs,
19855
20139
  ):
@@ -19867,6 +20151,15 @@ class ComponentGenerator(Component):
19867
20151
 
19868
20152
  if not callable(component_class):
19869
20153
  raise ValueError("component_class must be a callable")
20154
+ if moments is not None:
20155
+ if any(prop for prop in (at, delay, till, duration, number, iat, force_at, force_till, disturbance, equidistant)):
20156
+ raise ValueError(
20157
+ "specifying at, delay, till,duration, number, iat,force_at, force_till, disturbance or equidistant is not allowed, if moments is specified"
20158
+ )
20159
+ if callable(moments):
20160
+ moments = moments()
20161
+ moments = sorted([env.spec_to_time(moment) for moment in moments])
20162
+
19870
20163
  self.component_class = component_class
19871
20164
  self.iat = iat
19872
20165
  self.disturbance = disturbance
@@ -19909,25 +20202,26 @@ class ComponentGenerator(Component):
19909
20202
  at = None
19910
20203
  process = ""
19911
20204
  else:
19912
- if self.iat is None and not equidistant:
19913
- if till == inf or self.number == inf:
19914
- raise ValueError("iat not specified --> till and number need to be specified")
19915
- if disturbance is not None:
19916
- raise ValueError("iat not specified --> disturbance not allowed")
19917
-
19918
- samples = sorted([Uniform(at, till)() for _ in range(self.number)])
19919
- if force_at or force_till:
19920
- if number == 1:
19921
- if force_at and force_till:
19922
- raise ValueError("force_at and force_till does not allow number=1")
19923
- samples = [at] if force_at else [till]
19924
- else:
19925
- v_at = at if force_at else samples[0]
19926
- v_till = till if force_till else samples[-1]
19927
- min_sample = samples[0]
19928
- max_sample = samples[-1]
19929
- samples = [interpolate(sample, min_sample, max_sample, v_at, v_till) for sample in samples]
19930
- self.intervals = [t1 - t0 for t0, t1 in zip([0] + samples, samples)]
20205
+ if (self.iat is None and not equidistant) or moments:
20206
+ if not moments:
20207
+ if till == inf or self.number == inf:
20208
+ raise ValueError("iat not specified --> till and number need to be specified")
20209
+ if disturbance is not None:
20210
+ raise ValueError("iat not specified --> disturbance not allowed")
20211
+
20212
+ moments = sorted([Uniform(at, till)() for _ in range(self.number)])
20213
+ if force_at or force_till:
20214
+ if number == 1:
20215
+ if force_at and force_till:
20216
+ raise ValueError("force_at and force_till does not allow number=1")
20217
+ moments = [at] if force_at else [till]
20218
+ else:
20219
+ v_at = at if force_at else moments[0]
20220
+ v_till = till if force_till else moments[-1]
20221
+ min_moment = moments[0]
20222
+ max_moment = moments[-1]
20223
+ moments = [interpolate(moment, min_moment, max_moment, v_at, v_till) for moment in moments]
20224
+ self.intervals = [t1 - t0 for t0, t1 in zip([0] + moments, moments)]
19931
20225
  at = self.intervals[0]
19932
20226
  self.intervals[0] = 0
19933
20227
  process = "do_spread_yieldless" if env._yieldless else "do_spread"
@@ -20253,7 +20547,7 @@ class _Distribution:
20253
20547
  If, after number_of_tries retries, the sampled value is still not within the given bounds,
20254
20548
  fail_value will be returned
20255
20549
 
20256
- Samples that cannot be converted (only possible with Pdf and CumPdf) to float
20550
+ Samples that cannot be converted (only possible with /Pmf and CumPdf/CumPmf) to float
20257
20551
  are assumed to be within the bounds.
20258
20552
  """
20259
20553
  return Bounded(self, lowerbound, upperbound, fail_value, number_of_retries, include_lowerbound, include_upperbound).sample()
@@ -20493,7 +20787,7 @@ class Bounded(_Distribution):
20493
20787
  If, after number_of_tries retries, the sampled value is still not within the given bounds,
20494
20788
  fail_value will be returned
20495
20789
 
20496
- Samples that cannot be converted to float (only possible with Pdf and CumPdf)
20790
+ Samples that cannot be converted to float (only possible with Pdf/Pmf and CumPdf)
20497
20791
  are assumed to be within the bounds.
20498
20792
  """
20499
20793
 
@@ -21989,6 +22283,9 @@ class Pdf(_Distribution):
21989
22283
 
21990
22284
  If it is a salabim distribution, not the distribution,
21991
22285
  but a sample will be returned when calling sample.
22286
+
22287
+
22288
+ This method is also available under the name Pmf
21992
22289
  """
21993
22290
 
21994
22291
  def __init__(self, spec: Union[Iterable, Dict], probabilities=None, time_unit: str = None, randomstream: Any = None, env: "Environment" = None):
@@ -22133,9 +22430,151 @@ class Pdf(_Distribution):
22133
22430
  return self._mean
22134
22431
 
22135
22432
 
22433
+ class Pmf(Pdf):
22434
+ """
22435
+ Probability mass function
22436
+
22437
+ Parameters
22438
+ ----------
22439
+ spec : list, tuple or dict
22440
+ either
22441
+
22442
+ - if no probabilities specified:
22443
+
22444
+ list/tuple with x-values and corresponding probability
22445
+ dict where the keys are re x-values and the values are probabilities
22446
+ (x0, p0, x1, p1, ...xn,pn)
22447
+
22448
+ - if probabilities is specified:
22449
+
22450
+ list with x-values
22451
+
22452
+ probabilities : iterable or float
22453
+ if omitted, spec contains the probabilities
22454
+
22455
+ the iterable (p0, p1, ...pn) contains the probabilities of the corresponding
22456
+ x-values from spec.
22457
+
22458
+ alternatively, if a float is given (e.g. 1), all x-values
22459
+ have equal probability. The value is not important.
22460
+
22461
+ time_unit : str
22462
+ specifies the time unit
22463
+
22464
+ must be one of "years", "weeks", "days", "hours", "minutes", "seconds", "milliseconds", "microseconds"
22465
+
22466
+ default : no conversion
22467
+
22468
+
22469
+ randomstream : randomstream
22470
+ if omitted, random will be used
22471
+
22472
+ if used as random.Random(12299)
22473
+ it assigns a new stream with the specified seed
22474
+
22475
+ env : Environment
22476
+ environment where the distribution is defined
22477
+
22478
+ if omitted, default_env will be used
22479
+
22480
+ Note
22481
+ ----
22482
+ p0+p1=...+pn>0
22483
+
22484
+ all densities are auto scaled according to the sum of p0 to pn,
22485
+ so no need to have p0 to pn add up to 1 or 100.
22486
+
22487
+ The x-values can be any type.
22488
+
22489
+ If it is a salabim distribution, not the distribution,
22490
+ but a sample will be returned when calling sample.
22491
+
22492
+ This method is also available under the name Pdf
22493
+
22494
+ """
22495
+
22496
+ def __repr__(self):
22497
+ return "Pmf"
22498
+
22499
+ def print_info(self, as_str: bool = False, file: TextIO = None) -> str:
22500
+ """
22501
+ prints information about the distribution
22502
+
22503
+ Parameters
22504
+ ----------
22505
+ as_str: bool
22506
+ if False (default), print the info
22507
+ if True, return a string containing the info
22508
+
22509
+ file: file
22510
+ if None(default), all output is directed to stdout
22511
+
22512
+ otherwise, the output is directed to the file
22513
+
22514
+ Returns
22515
+ -------
22516
+ info (if as_str is True) : str
22517
+ """
22518
+ result = []
22519
+ result.append("Pmf distribution " + hex(id(self)))
22520
+ result.append(" randomstream=" + hex(id(self.randomstream)))
22521
+ return return_or_print(result, as_str, file)
22522
+
22523
+ def sample(self, n: int = None) -> Any:
22524
+ """
22525
+ Parameters
22526
+ ----------
22527
+ n : number of samples : int
22528
+ if not specified, specifies just return one sample, as usual
22529
+
22530
+ if specified, return a list of n sampled values from the distribution without replacement.
22531
+ This requires that all probabilities are equal.
22532
+
22533
+ If n > number of values in the Pmf distribution, n is assumed to be the number of values
22534
+ in the distribution.
22535
+
22536
+ If a sampled value is a distribution, a sample from that distribution will be returned.
22537
+
22538
+ Returns
22539
+ -------
22540
+ Sample of the distribution : any (usually float) or list
22541
+ In case n is specified, returns a list of n values
22542
+
22543
+ """
22544
+ if self.supports_n:
22545
+ if n is None:
22546
+ return self.randomstream.sample(self._x, 1)[0]
22547
+ else:
22548
+ if n < 0:
22549
+ raise ValueError("n < 0")
22550
+ n = min(n, len(self._x))
22551
+ xs = self.randomstream.sample(self._x, n)
22552
+ return [x.sample() if isinstance(x, _Distribution) else x for x in xs]
22553
+ else:
22554
+ if n is None:
22555
+ r = self.randomstream.random()
22556
+ for cum, x in zip([0] + self._cum, [0] + self._x):
22557
+ if r <= cum:
22558
+ if isinstance(x, _Distribution):
22559
+ return x.sample()
22560
+ return x
22561
+ else:
22562
+ raise ValueError("not all probabilities are the same")
22563
+
22564
+ def mean(self) -> float:
22565
+ """
22566
+ Returns
22567
+ -------
22568
+ mean of the distribution : float
22569
+ if the mean can't be calculated (if not all x-values are scalars or distributions),
22570
+ nan will be returned.
22571
+ """
22572
+ return self._mean
22573
+
22574
+
22136
22575
  class CumPdf(_Distribution):
22137
22576
  """
22138
- Cumulative Probability distribution function
22577
+ Cumulative Probability mass function
22139
22578
 
22140
22579
  Parameters
22141
22580
  ----------
@@ -22188,6 +22627,8 @@ class CumPdf(_Distribution):
22188
22627
 
22189
22628
  If it is a salabim distribution, not the distribution,
22190
22629
  but a sample will be returned when calling sample.
22630
+
22631
+ This method is also available under the name CumPmf
22191
22632
  """
22192
22633
 
22193
22634
  def __init__(
@@ -22303,6 +22744,116 @@ class CumPdf(_Distribution):
22303
22744
  return self._mean
22304
22745
 
22305
22746
 
22747
+ class CumPmf(CumPdf):
22748
+ """
22749
+ Cumulative Probability mass function
22750
+
22751
+ Parameters
22752
+ ----------
22753
+ spec : list or tuple
22754
+ either
22755
+
22756
+ - if no cumprobabilities specified:
22757
+
22758
+ list with x-values and corresponding cumulative probability
22759
+ (x0, p0, x1, p1, ...xn,pn)
22760
+
22761
+ - if cumprobabilities is specified:
22762
+
22763
+ list with x-values
22764
+
22765
+ cumprobabilities : list, tuple or float
22766
+ if omitted, spec contains the probabilities
22767
+
22768
+ the list (p0, p1, ...pn) contains the cumulative probabilities of the corresponding
22769
+ x-values from spec.
22770
+
22771
+
22772
+ time_unit : str
22773
+ specifies the time unit
22774
+
22775
+ must be one of "years", "weeks", "days", "hours", "minutes", "seconds", "milliseconds", "microseconds"
22776
+
22777
+ default : no conversion
22778
+
22779
+
22780
+ randomstream : randomstream
22781
+ if omitted, random will be used
22782
+
22783
+ if used as random.Random(12299)
22784
+ it assigns a new stream with the specified seed
22785
+
22786
+ env : Environment
22787
+ environment where the distribution is defined
22788
+
22789
+ if omitted, default_env will be used
22790
+
22791
+ Note
22792
+ ----
22793
+ p0<=p1<=..pn>0
22794
+
22795
+ all densities are auto scaled according to pn,
22796
+ so no need to have pn be 1 or 100.
22797
+
22798
+ The x-values can be any type.
22799
+
22800
+ If it is a salabim distribution, not the distribution,
22801
+ but a sample will be returned when calling sample.
22802
+
22803
+ This method is also available under the name CumPdf
22804
+ """
22805
+
22806
+ def __repr__(self):
22807
+ return "CumPmf"
22808
+
22809
+ def print_info(self, as_str: bool = False, file: TextIO = None) -> str:
22810
+ """
22811
+ prints information about the distribution
22812
+
22813
+ Parameters
22814
+ ----------
22815
+ as_str: bool
22816
+ if False (default), print the info
22817
+ if True, return a string containing the info
22818
+
22819
+ file: file
22820
+ if None(default), all output is directed to stdout
22821
+
22822
+ otherwise, the output is directed to the file
22823
+
22824
+ Returns
22825
+ -------
22826
+ info (if as_str is True) : str
22827
+ """
22828
+ result = []
22829
+ result.append("CumPmf distribution " + hex(id(self)))
22830
+ result.append(" randomstream=" + hex(id(self.randomstream)))
22831
+ return return_or_print(result, as_str, file)
22832
+
22833
+ def sample(self) -> Any:
22834
+ """
22835
+ Returns
22836
+ -------
22837
+ Sample of the distribution : any (usually float)
22838
+ """
22839
+ r = self.randomstream.random()
22840
+ for cum, x in zip([0] + self._cum, [0] + self._x):
22841
+ if r <= cum:
22842
+ if isinstance(x, _Distribution):
22843
+ return x.sample()
22844
+ return x
22845
+
22846
+ def mean(self) -> float:
22847
+ """
22848
+ Returns
22849
+ -------
22850
+ mean of the distribution : float
22851
+ if the mean can't be calculated (if not all x-values are scalars or distributions),
22852
+ nan will be returned.
22853
+ """
22854
+ return self._mean
22855
+
22856
+
22306
22857
  class External(_Distribution):
22307
22858
  """
22308
22859
  External distribution function
@@ -23902,7 +24453,7 @@ class _APNG:
23902
24453
 
23903
24454
  def to_bytes(self):
23904
24455
  CHUNK_BEFORE_IDAT = {"cHRM", "gAMA", "iCCP", "sBIT", "sRGB", "bKGD", "hIST", "tRNS", "pHYs", "sPLT", "tIME", "PLTE"}
23905
- PNG_SIGN = b"\x89\x50\x4E\x47\x0D\x0A\x1A\x0A"
24456
+ PNG_SIGN = b"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a"
23906
24457
  out = [PNG_SIGN]
23907
24458
  other_chunks = []
23908
24459
  seq = 0
@@ -26316,7 +26867,7 @@ def fonts():
26316
26867
 
26317
26868
  for dir, recursive in dir_recursives:
26318
26869
  for file_path in dir.glob("**/*.*" if recursive else "*.*"):
26319
- if file_path.suffix.lower() == ".ttf": # ***
26870
+ if file_path.suffix.lower() == ".ttf":
26320
26871
  file = str(file_path)
26321
26872
  fn = os.path.basename(file).split(".")[0]
26322
26873
  if "_std_fonts" in globals() and fn in _std_fonts(): # test for availabiitly, because of minimized version
@@ -26376,7 +26927,8 @@ def getfont(fontname, fontsize):
26376
26927
  return getfont.lookup[(fontname, fontsize)]
26377
26928
  else:
26378
26929
  getfont.lookup = {}
26379
-
26930
+ if fontname == "":
26931
+ a = 1
26380
26932
  if isinstance(fontname, str):
26381
26933
  fontlist1 = [fontname]
26382
26934
  else:
@@ -26409,7 +26961,8 @@ def getfont(fontname, fontsize):
26409
26961
  result = ImageFont.truetype(filename, size=int(fontsize))
26410
26962
  else:
26411
26963
  # refer to https://github.com/python-pillow/Pillow/issues/3730 for explanation (in order to load >= 500 fonts)
26412
- result = ImageFont.truetype(font=io.BytesIO(open(filename, "rb").read()), size=int(fontsize))
26964
+ with open(filename, "rb") as f:
26965
+ result = ImageFont.truetype(font=io.BytesIO(f.read()), size=int(fontsize))
26413
26966
  break
26414
26967
  except Exception:
26415
26968
  raise
@@ -26948,7 +27501,7 @@ def reset() -> None:
26948
27501
  g.tkinter_loaded = "?"
26949
27502
  g.image_container_cache = {}
26950
27503
  g._default_cap_now = False
26951
- g._captured_stdout=[]
27504
+ g._captured_stdout = []
26952
27505
 
26953
27506
  random_seed() # always start with seed 1234567
26954
27507
 
@@ -27181,7 +27734,6 @@ reset()
27181
27734
  set_environment_aliases()
27182
27735
 
27183
27736
  if __name__ == "__main__":
27184
-
27185
27737
  sys.path.insert(0, str(Path(__file__).parent / ".." / "misc"))
27186
27738
  try:
27187
27739
  import salabim_exp
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: salabim
3
- Version: 24.0.12
3
+ Version: 24.0.14.post3
4
4
  Summary: salabim - discrete event simulation in Python
5
5
  Author-email: Ruud van der Ham <rt.van.der.ham@gmail.com>
6
6
  Project-URL: Homepage, https://salabim.org
@@ -0,0 +1,10 @@
1
+ salabim/DejaVuSansMono.ttf,sha256=Z_oIXp5yp1Zaw2y2p3vaxwHhjHpG0MFbmwhxSh4aIEI,335068
2
+ salabim/LICENSE.txt,sha256=qHlBa-POyexatCxDTjSKMlYtkBFQDn9lu-YV_1L6V0U,1106
3
+ salabim/__init__.py,sha256=r7qPLvlmX0dkZDyjuTo8Jo3ex3sD1L4pmK6K5ib9vyw,56
4
+ salabim/calibri.ttf,sha256=RWpf8Uo31RfvGGNaSt9-2sXSuN87AVE_NFMRsV3LhBk,1330156
5
+ salabim/mplus-1m-regular.ttf,sha256=EuFHr90BJjuAn_r5MleJFN-WfkeWJ4tf7DweI5zr8tU,289812
6
+ salabim/salabim.py,sha256=LkXx4KGzg9FaYfe4NUBb2Zm2Iy4v1B2kyHks7cwsWoI,1113664
7
+ salabim-24.0.14.post3.dist-info/METADATA,sha256=4yHEwkcBMUAj2CtFsi3y4s7zkJTzSvLkkm0h3g6vgiI,3456
8
+ salabim-24.0.14.post3.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
9
+ salabim-24.0.14.post3.dist-info/top_level.txt,sha256=UE6zVlbi3F6T5ma1a_5TrojMaF21GYKDt9svvm0U4cQ,8
10
+ salabim-24.0.14.post3.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (73.0.1)
2
+ Generator: setuptools (75.2.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,10 +0,0 @@
1
- salabim/DejaVuSansMono.ttf,sha256=Z_oIXp5yp1Zaw2y2p3vaxwHhjHpG0MFbmwhxSh4aIEI,335068
2
- salabim/LICENSE.txt,sha256=qHlBa-POyexatCxDTjSKMlYtkBFQDn9lu-YV_1L6V0U,1106
3
- salabim/__init__.py,sha256=r7qPLvlmX0dkZDyjuTo8Jo3ex3sD1L4pmK6K5ib9vyw,56
4
- salabim/calibri.ttf,sha256=RWpf8Uo31RfvGGNaSt9-2sXSuN87AVE_NFMRsV3LhBk,1330156
5
- salabim/mplus-1m-regular.ttf,sha256=EuFHr90BJjuAn_r5MleJFN-WfkeWJ4tf7DweI5zr8tU,289812
6
- salabim/salabim.py,sha256=i5fPqlJSk3PRWFFZ7Z6RVsIWRI0EwQIeaN99fu5mCV0,1097596
7
- salabim-24.0.12.dist-info/METADATA,sha256=PlkVJ2Qk6CFySH31iSJDmGZQ2Yrzolam1TIIuwqQN3A,3450
8
- salabim-24.0.12.dist-info/WHEEL,sha256=Mdi9PDNwEZptOjTlUcAth7XJDFtKrHYaQMPulZeBCiQ,91
9
- salabim-24.0.12.dist-info/top_level.txt,sha256=UE6zVlbi3F6T5ma1a_5TrojMaF21GYKDt9svvm0U4cQ,8
10
- salabim-24.0.12.dist-info/RECORD,,