Skip to content

Commit cbaeaeb

Browse files
committed
Adds Generator to start without dataset, including yaml and batch runner options; Starts adding a priority based Queue reordering.
1 parent 4b850d6 commit cbaeaeb

7 files changed

Lines changed: 273 additions & 24 deletions

File tree

.vscode/extensions.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"github.vscode-github-actions",
66
"fnando.linter",
77
"ms-python.black-formatter",
8-
"virtualplay.vibrant-semantics-dark"
8+
"virtualplay.vibrant-semantics-dark",
9+
"ms-toolsai.datawrangler"
910
]
10-
}
11+
}

datasim/generator.py

Lines changed: 175 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,176 @@
1+
from typing import Any, Dict, Final, Optional, Tuple, Type
2+
from typing_extensions import Literal
3+
from git import List
4+
import numpy as np
5+
6+
from .types import Number, Value
7+
8+
9+
class Sampler:
10+
property: str
11+
12+
def __init__(self, property: str):
13+
self.property = property
14+
15+
@staticmethod
16+
def _from_yaml(property: str, params: Dict) -> "Sampler":
17+
if isinstance(params, Value):
18+
return StaticSampler(property, params, start=params)
19+
elif isinstance(params, Dict):
20+
if "value" in params:
21+
return StaticSampler(
22+
property,
23+
params["value"],
24+
params.get("sample", "independent") == "accumulate",
25+
params.get("start", None),
26+
)
27+
28+
if "distribution" in params:
29+
return DistributionSampler(
30+
property,
31+
params["distribution"],
32+
params.get("parameters", {}),
33+
params.get("sample", "independent"),
34+
)
35+
36+
return StaticSampler(property, None)
37+
38+
def next(self) -> Value:
39+
return None
40+
41+
42+
class StaticSampler(Sampler):
43+
value: Value
44+
accumulate: bool
45+
step: Value
46+
47+
def __init__(
48+
self, property: str, value: Value, accumulate: bool = False, start: Value = None
49+
):
50+
super().__init__(property)
51+
self.accumulate = accumulate
52+
self.value = start if start else 0.0 if isinstance(value, float) else 0
53+
self.step = value
54+
55+
def next(self) -> Value:
56+
if (
57+
self.accumulate
58+
and isinstance(self.value, int | float)
59+
and isinstance(self.step, int | float)
60+
):
61+
self.value += self.step
62+
return self.value
63+
64+
65+
class DistributionSampler(Sampler):
66+
value: float
67+
accumulate: bool
68+
rng: np.random.Generator
69+
np_function: Any
70+
parameters: Dict
71+
72+
def __init__(
73+
self,
74+
property: str,
75+
np_generator: str,
76+
parameters: Dict,
77+
accumulation: Literal["independent", "accumulate"],
78+
start: Optional[float] = None,
79+
):
80+
super().__init__(property)
81+
self.value = start if start else 0.0
82+
self.accumulate = accumulation == "accumulate"
83+
self.rng = np.random.default_rng()
84+
self.np_function = getattr(self.rng, np_generator)
85+
self.parameters = parameters
86+
87+
def next(self) -> Value:
88+
sample = self.np_function(**self.parameters)
89+
if not self.accumulate:
90+
return sample
91+
92+
self.value += sample
93+
return self.value
94+
95+
196
class Generator:
2-
def __init__(self):
3-
pass
97+
id: Final[str]
98+
data_class: Final[str]
99+
subset_key: Final[str]
100+
subsets: Final[Dict]
101+
102+
def __init__(
103+
self, world, id: str, data_class: str, subset_key: str, subsets: List[Dict]
104+
):
105+
self.id = id
106+
self.data_class = data_class
107+
self.subset_key = subset_key
108+
self.subsets = {}
109+
for subset in subsets:
110+
self.subsets[subset[subset_key]] = subset
111+
112+
world.add(self)
113+
114+
@staticmethod
115+
def _from_yaml(world, params: Dict) -> "Generator":
116+
id = list(params.keys())
117+
if len(id) > 1:
118+
raise ValueError(f"Unable to parse yaml: Multiple keys found in {params}")
119+
120+
id = id[0]
121+
params = params[id]
122+
123+
return Generator(world, id, params["class"], params["key"], params["subsets"])
124+
125+
def generate(
126+
self,
127+
type: Type,
128+
limits: Dict[str, Tuple[Literal["<", ">"], Number]] = {},
129+
counts: Dict[Tuple[str, Value], int] = {},
130+
sort: Optional[str] = None,
131+
sort_direction: Literal["asc", "desc"] = "asc",
132+
) -> List:
133+
data = []
134+
id = 0
135+
136+
for subset in self.subsets.values():
137+
samplers: Dict[str, Sampler] = {}
138+
for property, parameters in subset.items():
139+
samplers[property] = Sampler._from_yaml(property, parameters)
140+
141+
count = 0
142+
for (key, value), max in counts.items():
143+
if subset.get(key, None) == value:
144+
count = max
145+
break
146+
147+
while True:
148+
id += 1
149+
next = type()
150+
next.id = f"{id}"
151+
for property, sampler in samplers.items():
152+
setattr(next, property, sampler.next())
153+
154+
for key, (compare, limit) in limits.items():
155+
value = getattr(next, key, None)
156+
if value and (
157+
(compare == ">" and value > limit)
158+
or (compare == "<" and value < limit)
159+
):
160+
break
161+
162+
else:
163+
data.append(next)
164+
165+
if count > 0:
166+
count -= 1
167+
if count == 0:
168+
break
169+
continue
170+
171+
break
172+
173+
if sort:
174+
data.sort(key=lambda o: getattr(o, sort), reverse=sort_direction == "desc")
175+
176+
return data

datasim/queue.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -195,18 +195,17 @@ def peek_with_amount(self) -> Tuple[EntityType, Number] | None:
195195

196196
return self.queue[-1]
197197

198-
def prioritize(self, entity: EntityType) -> bool:
199-
"""Pushes an entity to the front of the list.
200-
201-
If the entity was not in the list, it will not be added;
202-
If the entity is in the list more than once, the copy furthest to the back will be put at the front.
198+
def queue_prioritized(self, entity: EntityType, sort_function) -> bool:
199+
"""Pushes an entity to a sorted place in the list
203200
204201
Args:
205202
entity (Entity): _description_
203+
sort_function (lambda): _function that evaluates to a __gt__ comparable type
206204
"""
207205
(_, entry) = [
208206
(i, (e, a)) for i, (e, a) in enumerate(self.queue) if e is entity
209207
][0]
208+
# TODO: change
210209
if self.queue.remove(entry):
211210
self.queue.append(entry)
212211
self.changed_tick = self.world.ticks

datasim/world.py

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@
88
from typing import Any, Dict, Final, List, Optional, Tuple
99

1010
from .constant import Constant
11-
from .output import Output
11+
from .dataset import DataFrameData, Dataset, DataSource
1212
from .entity import Entity
13+
from .generator import Generator
1314
from .logging import log, LogLevel
14-
from .dataset import DataFrameData, Dataset, DataSource
15+
from .output import Output
1516
from .quantity import Quantity
1617
from .queue import Queue
1718
from .resource import Resource
@@ -36,6 +37,7 @@ class World(ABC):
3637
_entity_registry: Final[dict[type, int]] = {}
3738
datasets: Final[Dict[str, Dataset]]
3839
constants: Final[Dict[str, Any]]
40+
generators: Final[Dict[str, Generator]]
3941
resources: Final[Dict[str, Resource]]
4042
queues: Final[Dict[str, Queue]]
4143
quantities: Final[Dict[str, Quantity]]
@@ -119,6 +121,7 @@ def __init__(
119121
self._entity_dict = {}
120122
self.datasets = {}
121123
self.constants = {}
124+
self.generators = {}
122125
self.resources = {}
123126
self.queues = {}
124127
self.quantities = {}
@@ -137,6 +140,10 @@ def __init__(
137140
for constant in definition["constants"]:
138141
Constant._from_yaml(self, constant)
139142

143+
if "generators" in definition:
144+
for generator in definition["generators"]:
145+
Generator._from_yaml(self, generator)
146+
140147
if "resources" in definition:
141148
for resource in definition["resources"]:
142149
Resource._from_yaml(self, resource)
@@ -158,7 +165,7 @@ def reset(self):
158165
"""Reset the World so you can start a different simulation."""
159166
self.active = False
160167

161-
def add(self, obj: Constant | Entity | Resource | Queue | Quantity):
168+
def add(self, obj: Constant | Generator | Entity | Resource | Queue | Quantity):
162169
"""Add an entity to this :class:`World`.
163170
164171
Args:
@@ -176,6 +183,8 @@ def add(self, obj: Constant | Entity | Resource | Queue | Quantity):
176183

177184
if isinstance(obj, Constant):
178185
self.constants[obj.id] = obj
186+
elif isinstance(obj, Generator):
187+
self.generators[obj.id] = obj
179188
elif isinstance(obj, Entity):
180189
self.entities.append(obj)
181190
self._entity_dict[obj.id] = obj
@@ -186,7 +195,9 @@ def add(self, obj: Constant | Entity | Resource | Queue | Quantity):
186195
elif isinstance(obj, Quantity):
187196
self.quantities[obj.id] = obj
188197

189-
def remove(self, obj: Constant | Entity | Resource | Queue | Quantity) -> bool:
198+
def remove(
199+
self, obj: Constant | Generator | Entity | Resource | Queue | Quantity
200+
) -> bool:
190201
"""Remove an entity from this :class:`World`.
191202
192203
Args:
@@ -200,6 +211,8 @@ def remove(self, obj: Constant | Entity | Resource | Queue | Quantity) -> bool:
200211

201212
if isinstance(obj, Constant):
202213
self.constants.pop(obj.id)
214+
elif isinstance(obj, Generator):
215+
self.generators.pop(obj.id)
203216
elif isinstance(obj, Entity):
204217
self.entities.remove(obj)
205218
self._entity_dict.pop(obj.id)
@@ -223,17 +236,28 @@ def _set_variation(self, selector: str, value: Value):
223236
obj_path = selector.split(".")
224237
current = self
225238
for path_part in obj_path[:-1]:
226-
current = getattr(current, path_part)
239+
current = (
240+
current.get(path_part)
241+
if isinstance(current, dict)
242+
else getattr(current, path_part)
243+
)
227244

228-
destination = getattr(current, obj_path[-1])
245+
destination = (
246+
current.get(obj_path[-1])
247+
if isinstance(current, dict)
248+
else getattr(current, obj_path[-1])
249+
)
229250

230251
if (
231252
destination is None
232253
or isinstance(destination, int)
233254
or isinstance(destination, float)
234255
or isinstance(destination, str)
235256
):
236-
setattr(current, obj_path[-1], value)
257+
if isinstance(current, dict):
258+
current[obj_path[-1]] = value
259+
else:
260+
setattr(current, obj_path[-1], value)
237261
elif isinstance(destination, Constant):
238262
destination.value = value
239263

examples/icu/icu.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import pandas as pd
55

6-
from datasim import Quantity, Queue, Resource, StateData, World
6+
from datasim import log, LogLevel, Quantity, Queue, Resource, StateData, World
77
from .patient import DiedPatientState, PatientData, Patient, TreatedPatientState
88

99

@@ -33,12 +33,21 @@ def __init__(
3333
variation_dict=variation_dict,
3434
)
3535

36-
self.load_patient_data("examples/icu/simulatiedata.csv")
36+
# Option: load from CSV or simulate input using Generator
37+
# self.load_patient_data("examples/icu/simulatiedata.csv")
38+
self.generate_patient_data(500)
3739

3840
def load_patient_data(self, filename: str):
3941
self.patients = []
4042
for row in list(reader(open(filename)))[1:]:
4143
self.patients.append(PatientData(row))
44+
log(f"Loaded data for {len(self.patients)} patients", LogLevel.debug)
45+
46+
def generate_patient_data(self, end_enter_time: float):
47+
self.patients = self.generators["patient_generator"].generate(
48+
PatientData, limits={"enter_time": (">", end_enter_time)}, sort="enter_time"
49+
)
50+
log(f"Generated data for {len(self.patients)} patients", LogLevel.debug)
4251

4352
def remove(self, obj):
4453
if isinstance(obj, Patient):
@@ -69,7 +78,11 @@ def before_entities_update(self):
6978
next = self.patients_waiting.peek()
7079

7180
def after_entities_update(self):
72-
if len(self.patients_waiting) == 0 and len(self.entities) == 0:
81+
if (
82+
len(self.patients_waiting) == 0
83+
and len(self.entities) == 0
84+
and len(self.patients) == 0
85+
):
7386
self.stopped = True
7487

7588
def aggregate_data(self) -> Dict[str, pd.DataFrame]:

0 commit comments

Comments
 (0)