sim-tools 1.0.4__tar.gz → 1.2.0__tar.gz
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.
- {sim_tools-1.0.4 → sim_tools-1.2.0}/PKG-INFO +1 -1
- {sim_tools-1.0.4 → sim_tools-1.2.0}/sim_tools/__init__.py +1 -1
- {sim_tools-1.0.4 → sim_tools-1.2.0}/sim_tools/distributions.py +224 -56
- {sim_tools-1.0.4 → sim_tools-1.2.0}/sim_tools/time_dependent.py +4 -0
- {sim_tools-1.0.4 → sim_tools-1.2.0}/.gitignore +0 -0
- {sim_tools-1.0.4 → sim_tools-1.2.0}/LICENSE +0 -0
- {sim_tools-1.0.4 → sim_tools-1.2.0}/README.md +0 -0
- {sim_tools-1.0.4 → sim_tools-1.2.0}/pyproject.toml +0 -0
- {sim_tools-1.0.4 → sim_tools-1.2.0}/sim_tools/_validation.py +0 -0
- {sim_tools-1.0.4 → sim_tools-1.2.0}/sim_tools/data/nspp_example1.csv +0 -0
- {sim_tools-1.0.4 → sim_tools-1.2.0}/sim_tools/datasets.py +0 -0
- {sim_tools-1.0.4 → sim_tools-1.2.0}/sim_tools/output_analysis.py +0 -0
- {sim_tools-1.0.4 → sim_tools-1.2.0}/sim_tools/ovs/__init__.py +0 -0
- {sim_tools-1.0.4 → sim_tools-1.2.0}/sim_tools/ovs/evaluation.py +0 -0
- {sim_tools-1.0.4 → sim_tools-1.2.0}/sim_tools/ovs/fixed_budget.py +0 -0
- {sim_tools-1.0.4 → sim_tools-1.2.0}/sim_tools/ovs/indifference_zone.py +0 -0
- {sim_tools-1.0.4 → sim_tools-1.2.0}/sim_tools/ovs/toy_models.py +0 -0
- {sim_tools-1.0.4 → sim_tools-1.2.0}/sim_tools/trace.py +0 -0
|
@@ -100,6 +100,7 @@ from typing import (
|
|
|
100
100
|
Dict,
|
|
101
101
|
runtime_checkable,
|
|
102
102
|
TypeVar,
|
|
103
|
+
Callable,
|
|
103
104
|
)
|
|
104
105
|
|
|
105
106
|
import numpy as np
|
|
@@ -257,7 +258,7 @@ class DistributionRegistry:
|
|
|
257
258
|
>>> exp_dist = DistributionRegistry.create("Exponential", mean=2.0)
|
|
258
259
|
>>> exp_dist.sample(5) # Generate 5 samples
|
|
259
260
|
|
|
260
|
-
Create multiple distributions from configuration:
|
|
261
|
+
Create multiple distributions from a flat configuration:
|
|
261
262
|
|
|
262
263
|
>>> config = {
|
|
263
264
|
... "arrivals": {
|
|
@@ -270,22 +271,53 @@ class DistributionRegistry:
|
|
|
270
271
|
... }
|
|
271
272
|
... }
|
|
272
273
|
>>> distributions = DistributionRegistry.create_batch(config,
|
|
273
|
-
|
|
274
|
+
... main_seed=12345)
|
|
274
275
|
>>> arrivals = distributions["arrivals"]
|
|
275
276
|
>>> service_times = distributions["service_times"]
|
|
276
277
|
|
|
278
|
+
Create distributions from a nested configuration:
|
|
279
|
+
|
|
280
|
+
>>> config = {
|
|
281
|
+
... "call": {
|
|
282
|
+
... "C1": {"class_name": "Exponential", "params": {"mean": 10.0}},
|
|
283
|
+
... "C2": {"class_name": "Exponential", "params": {"mean": 8.0}},
|
|
284
|
+
... },
|
|
285
|
+
... "response_time": {
|
|
286
|
+
... "C1": {
|
|
287
|
+
... "class_name": "Lognormal",
|
|
288
|
+
... "params": {"mean": 30.0, "stdev": 5.0},
|
|
289
|
+
... },
|
|
290
|
+
... },
|
|
291
|
+
... }
|
|
292
|
+
>>> dists = DistributionRegistry.create_batch(
|
|
293
|
+
... config, main_seed=42, preserve_structure=True
|
|
294
|
+
... )
|
|
295
|
+
>>> dists["call"]["C1"].sample(5)
|
|
296
|
+
>>> dists["response_time"]["C1"].sample(5)
|
|
297
|
+
|
|
277
298
|
Notes
|
|
278
299
|
-----
|
|
279
300
|
When creating distributions in batch with a main_seed, each distribution
|
|
280
301
|
receives its own statistically independent seed derived from the main seed.
|
|
281
302
|
This ensures proper statistical independence between random number streams
|
|
282
303
|
while maintaining overall reproducibility through the main seed.
|
|
304
|
+
|
|
305
|
+
When `preserve_structure=True`, the output mirrors the shape of the
|
|
306
|
+
input configuration, so nested configs produce nested dicts of instances.
|
|
307
|
+
When `preserve_structure=False` (default), all leaf distributions are
|
|
308
|
+
flattened into a single dict whose keys are the path components joined by
|
|
309
|
+
underscores (e.g. `"call_C1"`).
|
|
310
|
+
|
|
311
|
+
With `sort=True` (default), keys at every nesting level are sorted
|
|
312
|
+
alphabetically before seeds are assigned. This ensures that seed
|
|
313
|
+
assignment is stable even if the insertion order of keys in the config
|
|
314
|
+
dict changes between runs.
|
|
283
315
|
"""
|
|
284
316
|
|
|
285
|
-
_registry = {}
|
|
317
|
+
_registry: Dict[str, type] = {}
|
|
286
318
|
|
|
287
319
|
@classmethod
|
|
288
|
-
def register(cls, name: Optional[str] = None):
|
|
320
|
+
def register(cls, name: Optional[str] = None) -> Callable:
|
|
289
321
|
"""
|
|
290
322
|
Decorator to register a distribution class in the registry.
|
|
291
323
|
|
|
@@ -310,7 +342,7 @@ class DistributionRegistry:
|
|
|
310
342
|
return decorator
|
|
311
343
|
|
|
312
344
|
@classmethod
|
|
313
|
-
def get(cls, name: str):
|
|
345
|
+
def get(cls, name: str) -> type:
|
|
314
346
|
"""
|
|
315
347
|
Get a distribution class by name.
|
|
316
348
|
|
|
@@ -353,43 +385,146 @@ class DistributionRegistry:
|
|
|
353
385
|
distribution_class = cls.get(name)
|
|
354
386
|
return distribution_class(**params)
|
|
355
387
|
|
|
388
|
+
@classmethod
|
|
389
|
+
def _is_dist_config(cls, obj) -> bool:
|
|
390
|
+
"""
|
|
391
|
+
Return True if `obj` is a valid distribution config leaf.
|
|
392
|
+
|
|
393
|
+
A valid leaf is a dict with exactly the keys `'class_name'` and
|
|
394
|
+
`'params'` and no others.
|
|
395
|
+
|
|
396
|
+
Parameters
|
|
397
|
+
----------
|
|
398
|
+
obj : object
|
|
399
|
+
The object to test.
|
|
400
|
+
|
|
401
|
+
Returns
|
|
402
|
+
-------
|
|
403
|
+
bool
|
|
404
|
+
"""
|
|
405
|
+
return (
|
|
406
|
+
isinstance(obj, dict)
|
|
407
|
+
and set(obj.keys()) == {"class_name", "params"}
|
|
408
|
+
)
|
|
409
|
+
|
|
356
410
|
@classmethod
|
|
357
411
|
def create_batch(
|
|
358
412
|
cls,
|
|
359
413
|
config: Union[List[Dict], Dict[str, Dict]],
|
|
360
414
|
main_seed: Optional[int] = None,
|
|
361
|
-
sort: Optional[bool] = True
|
|
415
|
+
sort: Optional[bool] = True,
|
|
416
|
+
preserve_structure: bool = False,
|
|
362
417
|
) -> Union[List, Dict]:
|
|
363
418
|
"""
|
|
364
419
|
Create multiple distributions from a configuration dictionary or list.
|
|
365
420
|
|
|
421
|
+
Accepts both flat and arbitrarily nested dict configurations. Every
|
|
422
|
+
leaf must be a dict with exactly the keys `'class_name'` and
|
|
423
|
+
`'params'`.
|
|
424
|
+
|
|
366
425
|
Parameters
|
|
367
426
|
----------
|
|
368
427
|
config : Union[List[Dict], Dict[str, Dict]]
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
428
|
+
One of:
|
|
429
|
+
|
|
430
|
+
- A **list** of distribution configs, each with `'class_name'`
|
|
431
|
+
and `'params'`. Returns a list of instances.
|
|
432
|
+
- A **flat dict** mapping names to distribution configs:
|
|
433
|
+
|
|
434
|
+
{
|
|
435
|
+
"arrivals": {
|
|
436
|
+
"class_name": "Exponential",
|
|
437
|
+
"params": {"mean": 5.0},
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
- A **nested dict** where intermediate keys group distributions
|
|
442
|
+
and leaves are distribution configs:
|
|
443
|
+
|
|
444
|
+
{
|
|
445
|
+
"call": {
|
|
446
|
+
"C1": {
|
|
447
|
+
"class_name": "Exponential",
|
|
448
|
+
"params": {"mean": 10.0},
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
373
453
|
main_seed : Optional[int], default=None
|
|
374
454
|
Master seed to generate individual seeds for each distribution.
|
|
375
455
|
If None, random seeds will still be generated for independence.
|
|
376
456
|
sort : Optional[bool], default=True
|
|
377
|
-
If True
|
|
378
|
-
|
|
379
|
-
|
|
457
|
+
If True, keys at every nesting level are sorted alphabetically
|
|
458
|
+
before seeds are assigned. This ensures deterministic seed
|
|
459
|
+
assignment even if config key insertion order changes between
|
|
460
|
+
runs. Not relevant for top-level lists.
|
|
461
|
+
preserve_structure : Optional[bool], default=False
|
|
462
|
+
Controls the shape of the returned dict when `config` is a dict.
|
|
463
|
+
|
|
464
|
+
- `False` (default): all distributions are returned in a single
|
|
465
|
+
flat dict whose keys are the path components of each leaf joined
|
|
466
|
+
by underscores, e.g. `"call_C1"`.
|
|
467
|
+
- `True`: the returned dict mirrors the nesting of the input
|
|
468
|
+
config, so `result["call"]["C1"]` gives the distribution
|
|
469
|
+
directly.
|
|
470
|
+
|
|
471
|
+
Has no effect when `config` is a list.
|
|
380
472
|
|
|
381
473
|
Returns
|
|
382
474
|
-------
|
|
383
475
|
Union[List, Dict]
|
|
384
|
-
|
|
385
|
-
- A
|
|
386
|
-
|
|
476
|
+
- A list of distribution instances if `config` was a list.
|
|
477
|
+
- A flat dict of distribution instances if `config` was a dict
|
|
478
|
+
and `preserve_structure=False`.
|
|
479
|
+
- A nested dict of distribution instances if `config` was a dict
|
|
480
|
+
and `preserve_structure=True`.
|
|
387
481
|
|
|
388
482
|
Raises
|
|
389
483
|
------
|
|
390
484
|
TypeError
|
|
391
|
-
If config is neither a list nor a dictionary
|
|
392
|
-
|
|
485
|
+
If `config` is neither a list nor a dictionary.
|
|
486
|
+
ValueError
|
|
487
|
+
If any distribution config is malformed, if a required key is
|
|
488
|
+
missing or an unexpected key is present, or if the config
|
|
489
|
+
contains values that are neither dicts, lists, nor valid
|
|
490
|
+
distribution configs.
|
|
491
|
+
"""
|
|
492
|
+
flat_items = []
|
|
493
|
+
|
|
494
|
+
def walk(node, path=()):
|
|
495
|
+
if cls._is_dist_config(node):
|
|
496
|
+
flat_items.append((path, node))
|
|
497
|
+
return
|
|
498
|
+
|
|
499
|
+
if isinstance(node, dict):
|
|
500
|
+
# Detect a malformed distribution config: has one of the
|
|
501
|
+
# expected keys but not the exact right set, which most
|
|
502
|
+
# likely means a typo or missing key in a leaf config.
|
|
503
|
+
if "class_name" in node or "params" in node:
|
|
504
|
+
expected = {"class_name", "params"}
|
|
505
|
+
raise ValueError(
|
|
506
|
+
f"Distribution config at path {path!r} must have "
|
|
507
|
+
f"ONLY the keys {expected}. "
|
|
508
|
+
f"Found keys: {set(node.keys())}"
|
|
509
|
+
)
|
|
510
|
+
items = node.items()
|
|
511
|
+
if sort:
|
|
512
|
+
items = sorted(items, key=lambda kv: kv[0])
|
|
513
|
+
for key, value in items:
|
|
514
|
+
walk(value, path + (key,))
|
|
515
|
+
return
|
|
516
|
+
|
|
517
|
+
if isinstance(node, list):
|
|
518
|
+
for i, value in enumerate(node):
|
|
519
|
+
walk(value, path + (i,))
|
|
520
|
+
return
|
|
521
|
+
|
|
522
|
+
raise ValueError(
|
|
523
|
+
f"Expected a distribution config dict, a nested grouping "
|
|
524
|
+
f"dict, or a list at path {path!r}. "
|
|
525
|
+
f"Got {type(node).__name__!r}."
|
|
526
|
+
)
|
|
527
|
+
|
|
393
528
|
if isinstance(config, list):
|
|
394
529
|
seeds = spawn_seeds(len(config), main_seed)
|
|
395
530
|
return [
|
|
@@ -397,33 +532,48 @@ class DistributionRegistry:
|
|
|
397
532
|
for i, dist_config in enumerate(config)
|
|
398
533
|
]
|
|
399
534
|
|
|
400
|
-
if isinstance(config, dict):
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
535
|
+
if not isinstance(config, dict):
|
|
536
|
+
raise TypeError(
|
|
537
|
+
"Configuration must be a list or dictionary, "
|
|
538
|
+
f"got {type(config).__name__!r}."
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
walk(config)
|
|
542
|
+
seeds = spawn_seeds(len(flat_items), main_seed)
|
|
543
|
+
|
|
544
|
+
created = [
|
|
545
|
+
(path, cls._validate_and_create(dist_config, seeds[i]))
|
|
546
|
+
for i, (path, dist_config) in enumerate(flat_items)
|
|
547
|
+
]
|
|
409
548
|
|
|
410
|
-
|
|
549
|
+
if not preserve_structure:
|
|
550
|
+
return {"_".join(map(str, path)): obj for path, obj in created}
|
|
551
|
+
|
|
552
|
+
result: Dict = {}
|
|
553
|
+
for path, obj in created:
|
|
554
|
+
cursor = result
|
|
555
|
+
for part in path[:-1]:
|
|
556
|
+
cursor = cursor.setdefault(part, {})
|
|
557
|
+
cursor[path[-1]] = obj
|
|
558
|
+
return result
|
|
411
559
|
|
|
412
560
|
@classmethod
|
|
413
561
|
def _validate_and_create(cls, dist_config, seed):
|
|
414
562
|
"""
|
|
415
|
-
Validate
|
|
416
|
-
|
|
417
|
-
|
|
563
|
+
Validate a distribution config, inject a random seed, and instantiate.
|
|
564
|
+
|
|
565
|
+
Checks that `dist_config` has exactly the keys `'class_name'` and
|
|
566
|
+
`'params'`, injects `random_seed` into the params, then creates
|
|
567
|
+
and returns the distribution instance.
|
|
418
568
|
|
|
419
569
|
Parameters
|
|
420
570
|
----------
|
|
421
571
|
dist_config : dict
|
|
422
572
|
Dictionary specifying the distribution configuration. Must have
|
|
423
|
-
keys 'class_name' (str) and 'params' (dict), and no others.
|
|
573
|
+
keys `'class_name'` (str) and `'params'` (dict), and no others.
|
|
424
574
|
seed : int
|
|
425
575
|
The seed to include in the distribution's parameters (as
|
|
426
|
-
'random_seed').
|
|
576
|
+
`'random_seed'`).
|
|
427
577
|
|
|
428
578
|
Returns
|
|
429
579
|
-------
|
|
@@ -451,48 +601,53 @@ class DistributionRegistry:
|
|
|
451
601
|
|
|
452
602
|
# Copy params and inject the random seed.
|
|
453
603
|
params = dist_config["params"].copy()
|
|
454
|
-
|
|
604
|
+
|
|
605
|
+
# Only inject random seed if class constructor accepts it
|
|
606
|
+
distribution_class = cls.get(dist_config["class_name"])
|
|
607
|
+
sig = inspect.signature(distribution_class.__init__)
|
|
608
|
+
if "random_seed" in sig.parameters:
|
|
609
|
+
params["random_seed"] = seed
|
|
455
610
|
|
|
456
611
|
# Instantiate and return the distribution object.
|
|
457
612
|
return cls.create(dist_config["class_name"], **params)
|
|
458
613
|
|
|
459
614
|
@classmethod
|
|
460
|
-
def get_template(cls, format="json", indent=2):
|
|
615
|
+
def get_template(cls, format: str = "json", indent: int = 2) -> Union[Dict, str]:
|
|
461
616
|
"""
|
|
462
617
|
Generate a template configuration containing all registered
|
|
463
618
|
distributions.
|
|
464
619
|
|
|
465
620
|
This helper method creates a template that includes all registered
|
|
466
621
|
distribution types with appropriate dummy parameters. Users can modify
|
|
467
|
-
this template and pass it directly to create_batch() to instantiate
|
|
622
|
+
this template and pass it directly to `create_batch()` to instantiate
|
|
468
623
|
their distributions.
|
|
469
624
|
|
|
470
625
|
Parameters
|
|
471
626
|
----------
|
|
472
627
|
format : str, default="json"
|
|
473
|
-
Output format: 'dict' for Python dictionary or 'json'
|
|
474
|
-
string
|
|
628
|
+
Output format: `'dict'` for a Python dictionary or `'json'`
|
|
629
|
+
for a JSON string.
|
|
475
630
|
indent : int, default=2
|
|
476
|
-
Indentation for JSON formatting (
|
|
631
|
+
Indentation for JSON formatting (only used when
|
|
632
|
+
`format='json'`).
|
|
477
633
|
|
|
478
634
|
Returns
|
|
479
635
|
-------
|
|
480
636
|
Union[Dict, str]
|
|
481
|
-
Either a dictionary (if
|
|
482
|
-
format='json') containing template configurations for all
|
|
483
|
-
registered distributions
|
|
637
|
+
Either a dictionary (if`format='dict'`) or a JSON string (if
|
|
638
|
+
`format='json'`) containing template configurations for all
|
|
639
|
+
registered distributions.
|
|
484
640
|
|
|
485
641
|
Examples
|
|
486
642
|
--------
|
|
487
643
|
>>> template = DistributionRegistry.get_template(format='dict')
|
|
488
|
-
>>> print(template.keys())
|
|
489
|
-
|
|
490
|
-
'Uniform_example', ...])
|
|
644
|
+
>>> print(list(template.keys()))
|
|
645
|
+
['Exponential_example', 'Normal_example', 'Uniform_example', ...]
|
|
491
646
|
|
|
492
647
|
>>> template = DistributionRegistry.get_template(format='json')
|
|
493
648
|
>>> print(template[:70])
|
|
494
649
|
{
|
|
495
|
-
|
|
650
|
+
"Exponential_example": {
|
|
496
651
|
"class_name": "Exponential",
|
|
497
652
|
"params": {
|
|
498
653
|
"""
|
|
@@ -1080,18 +1235,32 @@ class CombinationDistribution:
|
|
|
1080
1235
|
sample a combination of values from multiple distributions.
|
|
1081
1236
|
"""
|
|
1082
1237
|
|
|
1083
|
-
def __init__(self, *
|
|
1238
|
+
def __init__(self, *args: Distribution, dists=None):
|
|
1084
1239
|
"""
|
|
1085
|
-
|
|
1240
|
+
Initialise a combination distribution.
|
|
1241
|
+
|
|
1242
|
+
Distributions can be passed either as positional arguments or via the
|
|
1243
|
+
`dists` keyword argument, but not both. The keyword form is required
|
|
1244
|
+
when creating instances through the `DistributionRegistry` class,
|
|
1245
|
+
which passes parameters by name.
|
|
1086
1246
|
|
|
1087
1247
|
Parameters
|
|
1088
1248
|
----------
|
|
1089
|
-
*
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1249
|
+
*args : Distribution
|
|
1250
|
+
Distribution objects to combine, passed as positional arguments.
|
|
1251
|
+
E.g. `CombinationDistribution(d1, d2)`.
|
|
1252
|
+
Cannot be used together with `dists`.
|
|
1253
|
+
dists : Sequence[Distribution], optional
|
|
1254
|
+
Distribution objects to combine, passed as a keyword argument.
|
|
1255
|
+
E.g. `CombinationDistribution(dists=[d1, d2])`.
|
|
1256
|
+
Cannot be used together with `*args`.
|
|
1257
|
+
"""
|
|
1258
|
+
if args and dists is not None:
|
|
1259
|
+
raise ValueError(
|
|
1260
|
+
"Pass distributions either as positional arguments or as "
|
|
1261
|
+
"'dists', not both."
|
|
1262
|
+
)
|
|
1263
|
+
self.dists = dists if dists is not None else args
|
|
1095
1264
|
|
|
1096
1265
|
def __repr__(self):
|
|
1097
1266
|
dist_reprs = [repr(dist) for dist in self.dists]
|
|
@@ -2195,7 +2364,7 @@ class DiscreteEmpirical:
|
|
|
2195
2364
|
"""
|
|
2196
2365
|
|
|
2197
2366
|
# convert to array first
|
|
2198
|
-
self.values = np.asarray(values)
|
|
2367
|
+
self.values = np.asarray(list(values))
|
|
2199
2368
|
self.freq = np.asarray(freq)
|
|
2200
2369
|
|
|
2201
2370
|
validate(self.freq, "freq", is_positive_array)
|
|
@@ -2244,7 +2413,6 @@ class DiscreteEmpirical:
|
|
|
2244
2413
|
- A numpy array of values with shape determined by size parameter
|
|
2245
2414
|
"""
|
|
2246
2415
|
sample = self.rng.choice(self.values, p=self.probabilities, size=size)
|
|
2247
|
-
|
|
2248
2416
|
if size is None:
|
|
2249
2417
|
return sample.item()
|
|
2250
2418
|
return sample
|
|
@@ -10,7 +10,11 @@ import pandas as pd
|
|
|
10
10
|
import matplotlib.pyplot as plt
|
|
11
11
|
import itertools
|
|
12
12
|
|
|
13
|
+
from sim_tools.distributions import DistributionRegistry
|
|
14
|
+
|
|
15
|
+
|
|
13
16
|
# pylint: disable=too-few-public-methods
|
|
17
|
+
@DistributionRegistry.register()
|
|
14
18
|
class NSPPThinning:
|
|
15
19
|
"""
|
|
16
20
|
Non Stationary Poisson Process via Thinning.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|