Skip to content

Commit 009678d

Browse files
committed
speedup test
1 parent 3c4d081 commit 009678d

14 files changed

Lines changed: 424 additions & 1197 deletions

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ dependencies = [
1515
"marshmallow_oneofschema",
1616
"bitstring",
1717
]
18-
requires-python = ">=3.11"
18+
requires-python = ">=3.12"
1919

2020
[project.urls]
2121
Homepage = "https://github.com/randriu/umbi"
@@ -87,7 +87,7 @@ exclude = [
8787
"**/__pycache__",
8888
"umbi.egg-info",
8989
]
90-
pythonVersion = "3.11"
90+
pythonVersion = "3.12"
9191
typeCheckingMode = "standard"
9292
reportMissingImports = true
9393
reportMissingTypeStubs = false

umbi/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
help="logging level",
1919
)
2020
@click.option("--import-umb", type=click.Path(), required=False, help=".umb filepath to import as ExplicitUmb")
21-
@click.option("--import-ats", type=click.Path(), required=False, help=".umb filepath to import as ExplicitAts")
21+
@click.option("--import-ats", type=click.Path(), required=False, help=".umb filepath to import as SimpleAts")
2222
@click.option("--export", type=click.Path(), required=False, help=".umb filepath to export")
2323
def main(log_level, import_umb, import_ats, export):
2424
umbi.setup_logging(level=log_level)

umbi/ats/__init__.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@
1616
write,
1717
)
1818
from .entity_class import EntityClass
19-
from .explicit_ats import Branch, Choice, ExplicitAts, TimeType
2019
from .model_info import ModelInfo
20+
from .simple_ats import SimpleAts
21+
from .time_type import TimeType
2122
from .variable_valuations import (
2223
EntityClassValuations,
2324
EntityValuation,
@@ -35,11 +36,10 @@
3536
"RewardAnnotation",
3637
# model_info
3738
"ModelInfo",
38-
# explicit_ats
39-
"Branch",
40-
"Choice",
39+
# time_type
4140
"TimeType",
42-
"ExplicitAts",
41+
# explicit_ats
42+
"SimpleAts",
4343
# variable_valuations
4444
"EntityVariableValuations",
4545
"Variable",

umbi/ats/annotations.py

Lines changed: 153 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
)
1515

1616
from .entity_class import EntityClass
17+
from .entity_space import EntityMapping, EntitySpace, OptionalMappingManager
1718

1819
logger = logging.getLogger(__name__)
1920

@@ -23,133 +24,200 @@ class Annotation:
2324
"""General annotation."""
2425

2526
name: str
27+
entity_spaces: dict[EntityClass, EntitySpace]
2628
alias: str | None = None
2729
description: str | None = None
28-
_entity_class_to_values: dict[EntityClass, list[Scalar]] = field(default_factory=dict[EntityClass, list[Scalar]])
30+
_entity_managers: dict[EntityClass, OptionalMappingManager] = field(default_factory=dict)
2931

3032
@classmethod
3133
def entity_class_enabled(cls, entity_class: EntityClass) -> bool:
3234
"""Check if the given entity class is enabled for this annotation type. Can be overridden by subclasses to restrict which entity classes the annotation subclass can be applied to."""
3335
return True
3436

35-
def __str__(self) -> str:
36-
s = f"Annotation(name={self.name!r}, alias={self.alias!r}, description={self.description!r}, values={{\n"
37-
indent_str = " "
38-
for entity_class, values in self._entity_class_to_values.items():
39-
s += f"{indent_str}{entity_class}: {values}\n"
40-
s += "})"
41-
return s
37+
def __post_init__(self):
38+
for entity_class in EntityClass:
39+
assert entity_class in self.entity_spaces, f"Entity space for {entity_class} must be provided"
40+
self._entity_managers[entity_class] = OptionalMappingManager[Scalar](
41+
name=f"{self.name}::{entity_class.value}_to_value",
42+
domain=self.entity_spaces[entity_class],
43+
can_have_mapping=lambda entity_class=entity_class: self.entity_class_enabled(entity_class),
44+
)
45+
self.entity_spaces[entity_class]._subscribe(
46+
self, lambda *args: self._entity_managers[entity_class].auto_manage()
47+
)
4248

4349
@property
44-
def mappings(self) -> dict[EntityClass, list[Scalar]]:
45-
"""Alias for entity_class_to_values."""
46-
return self._entity_class_to_values
50+
def _state_values_manager(self) -> OptionalMappingManager[Scalar]:
51+
return self._entity_managers[EntityClass.STATES]
4752

4853
@property
49-
def has_values(self) -> bool:
50-
"""Check if the annotation has any values set."""
51-
return len(self._entity_class_to_values) > 0
54+
def _choice_values_manager(self) -> OptionalMappingManager[Scalar]:
55+
return self._entity_managers[EntityClass.CHOICES]
5256

5357
@property
54-
def entity_classes(self) -> set[EntityClass]:
55-
"""Get the set of entity classes for which this annotation has values."""
56-
return set(self._entity_class_to_values.keys())
58+
def _branch_values_manager(self) -> OptionalMappingManager[Scalar]:
59+
return self._entity_managers[EntityClass.BRANCHES]
5760

58-
def has_values_for(self, entity_class: EntityClass) -> bool:
59-
"""Check if the annotation has values for the given entity class."""
60-
return self._entity_class_to_values.get(entity_class) is not None
61+
@property
62+
def _player_values_manager(self) -> OptionalMappingManager[Scalar]:
63+
return self._entity_managers[EntityClass.PLAYERS]
6164

62-
def get_values_for(self, entity_class: EntityClass) -> list[Scalar]:
63-
"""
64-
Get the values for the given entity class.
65-
:raises KeyError: if no values are set for the given entity class
66-
"""
67-
if not self.has_values_for(entity_class):
68-
raise KeyError(f"Annotation has no values for entity class {entity_class}")
69-
return self._entity_class_to_values[entity_class]
65+
@property
66+
def _observation_values_manager(self) -> OptionalMappingManager[Scalar]:
67+
return self._entity_managers[EntityClass.OBSERVATIONS]
7068

71-
def set_values_for(self, entity_class: EntityClass, values: Sequence[Scalar]) -> None:
72-
"""Set the values for the given entity class."""
73-
if not self.entity_class_enabled(entity_class):
74-
raise ValueError(f"Entity class {entity_class} is not enabled for this annotation type")
75-
self._entity_class_to_values[entity_class] = list(values)
69+
def add_state_values(self):
70+
self._state_values_manager.create_mapping()
7671

77-
def unset_values_for(self, entity_class: EntityClass) -> None:
78-
"""Unset the values for the given entity class."""
79-
if not self.has_values_for(entity_class):
80-
raise KeyError(f"Annotation has no values for entity class {entity_class}")
81-
del self._entity_class_to_values[entity_class]
72+
def remove_state_values(self):
73+
self._state_values_manager.remove_mapping()
8274

83-
### Convenience properties and methods for each entity class ###
8475
@property
8576
def has_state_values(self) -> bool:
86-
return self.has_values_for(EntityClass.STATES)
77+
return self._state_values_manager.has_mapping
78+
79+
@property
80+
def state_values(self) -> EntityMapping[Scalar]:
81+
return self._state_values_manager.mapping
82+
83+
@state_values.setter
84+
def state_values(self, values: Sequence[Scalar] | None) -> None:
85+
self._state_values_manager.mapping = values
86+
87+
def add_choice_values(self):
88+
self._choice_values_manager.create_mapping()
89+
90+
def remove_choice_values(self):
91+
self._choice_values_manager.remove_mapping()
8792

8893
@property
8994
def has_choice_values(self) -> bool:
90-
return self.has_values_for(EntityClass.CHOICES)
95+
return self._choice_values_manager.has_mapping
96+
97+
@property
98+
def choice_values(self) -> EntityMapping[Scalar]:
99+
return self._choice_values_manager.mapping
100+
101+
@choice_values.setter
102+
def choice_values(self, values: Sequence[Scalar] | None) -> None:
103+
self._choice_values_manager.mapping = values
104+
105+
def add_branch_values(self):
106+
self._branch_values_manager.create_mapping()
107+
108+
def remove_branch_values(self):
109+
self._branch_values_manager.remove_mapping()
91110

92111
@property
93112
def has_branch_values(self) -> bool:
94-
return self.has_values_for(EntityClass.BRANCHES)
113+
return self._branch_values_manager.has_mapping
95114

96115
@property
97-
def has_observation_values(self) -> bool:
98-
return self.has_values_for(EntityClass.OBSERVATIONS)
116+
def branch_values(self) -> EntityMapping[Scalar]:
117+
return self._branch_values_manager.mapping
118+
119+
@branch_values.setter
120+
def branch_values(self, values: Sequence[Scalar] | None) -> None:
121+
self._branch_values_manager.mapping = values
122+
123+
def add_player_values(self):
124+
self._player_values_manager.create_mapping()
125+
126+
def remove_player_values(self):
127+
self._player_values_manager.remove_mapping()
99128

100129
@property
101130
def has_player_values(self) -> bool:
102-
return self.has_values_for(EntityClass.PLAYERS)
131+
return self._player_values_manager.has_mapping
103132

104133
@property
105-
def state_values(self) -> list[Scalar]:
106-
return self.get_values_for(EntityClass.STATES)
134+
def player_values(self) -> EntityMapping[Scalar]:
135+
return self._player_values_manager.mapping
107136

108-
@property
109-
def choice_values(self) -> list[Scalar]:
110-
return self.get_values_for(EntityClass.CHOICES)
137+
@player_values.setter
138+
def player_values(self, values: Sequence[Scalar] | None) -> None:
139+
self._player_values_manager.mapping = values
111140

112-
@property
113-
def branch_values(self) -> list[Scalar]:
114-
return self.get_values_for(EntityClass.BRANCHES)
141+
def add_observation_values(self):
142+
self._observation_values_manager.create_mapping()
143+
144+
def remove_observation_values(self):
145+
self._observation_values_manager.remove_mapping()
115146

116147
@property
117-
def observation_values(self) -> list[Scalar]:
118-
return self.get_values_for(EntityClass.OBSERVATIONS)
148+
def has_observation_values(self) -> bool:
149+
return self._observation_values_manager.has_mapping
119150

120151
@property
121-
def player_values(self) -> list[Scalar]:
122-
return self.get_values_for(EntityClass.PLAYERS)
152+
def observation_values(self) -> EntityMapping[Scalar]:
153+
return self._observation_values_manager.mapping
123154

124-
def set_state_values(self, values: Sequence[Scalar]):
125-
self.set_values_for(EntityClass.STATES, values)
155+
@observation_values.setter
156+
def observation_values(self, values: Sequence[Scalar] | None) -> None:
157+
self._observation_values_manager.mapping = values
126158

127-
def set_choice_values(self, values: Sequence[Scalar]):
128-
self.set_values_for(EntityClass.CHOICES, values)
159+
def validate(self):
160+
for manager in self._entity_managers.values():
161+
manager.validate()
129162

130-
def set_branch_values(self, values: Sequence[Scalar]):
131-
self.set_values_for(EntityClass.BRANCHES, values)
163+
def __str__(self) -> str:
164+
s = f"Annotation(name={self.name!r}, alias={self.alias!r}, description={self.description!r}, values={{\n"
165+
indent_str = " "
166+
for entity_class, values in self._entity_class_to_values.items():
167+
s += f"{indent_str}{entity_class}: {values}\n"
168+
s += "})"
169+
return s
132170

133-
def set_observation_values(self, values: Sequence[Scalar]):
134-
self.set_values_for(EntityClass.OBSERVATIONS, values)
171+
### legacy methods
135172

136-
def set_player_values(self, values: Sequence[Scalar]):
137-
self.set_values_for(EntityClass.PLAYERS, values)
173+
@property
174+
def _entity_class_to_values(self) -> dict[EntityClass, EntityMapping[Scalar]]:
175+
"""Get a mapping from entity class to the corresponding values mapping for this annotation."""
176+
entity_class_to_values = {}
177+
for entity_class, manager in self._entity_managers.items():
178+
if manager.has_mapping:
179+
entity_class_to_values[entity_class] = manager.mapping
180+
return entity_class_to_values
181+
182+
# @property
183+
# def mappings(self) -> dict[EntityClass, EntityMapping[Scalar]]:
184+
# """Alias for entity_class_to_values."""
185+
# return self._entity_class_to_values
138186

139-
def unset_state_values(self):
140-
self.unset_values_for(EntityClass.STATES)
187+
@property
188+
def has_values(self) -> bool:
189+
"""Check if the annotation has any values set."""
190+
return len(self._entity_class_to_values) > 0
141191

142-
def unset_choice_values(self):
143-
self.unset_values_for(EntityClass.CHOICES)
192+
@property
193+
def entity_classes(self) -> set[EntityClass]:
194+
"""Get the set of entity classes for which this annotation has values."""
195+
return set(self._entity_class_to_values.keys())
144196

145-
def unset_branch_values(self):
146-
self.unset_values_for(EntityClass.BRANCHES)
197+
def has_values_for(self, entity_class: EntityClass) -> bool:
198+
"""Check if the annotation has values for the given entity class."""
199+
return self._entity_class_to_values.get(entity_class) is not None
147200

148-
def unset_observation_values(self):
149-
self.unset_values_for(EntityClass.OBSERVATIONS)
201+
def get_values_for(self, entity_class: EntityClass) -> EntityMapping[Scalar]:
202+
"""
203+
Get the values for the given entity class.
204+
:raises KeyError: if no values are set for the given entity class
205+
"""
206+
if not self.has_values_for(entity_class):
207+
raise KeyError(f"Annotation has no values for entity class {entity_class}")
208+
return self._entity_class_to_values[entity_class]
209+
210+
def set_values_for(self, entity_class: EntityClass, values: Sequence[Scalar]) -> None:
211+
"""Set the values for the given entity class."""
212+
if not self.entity_class_enabled(entity_class):
213+
raise ValueError(f"Entity class {entity_class} is not enabled for this annotation type")
214+
self._entity_managers[entity_class].mapping = values
150215

151-
def unset_player_values(self):
152-
self.unset_values_for(EntityClass.PLAYERS)
216+
def remove_values_for(self, entity_class: EntityClass) -> None:
217+
"""Remove the values for the given entity class."""
218+
if not self.has_values_for(entity_class):
219+
raise KeyError(f"Annotation has no values for entity class {entity_class}")
220+
del self._entity_class_to_values[entity_class]
153221

154222
def __eq__(self, other: object) -> bool:
155223
if not isinstance(other, Annotation):
@@ -168,22 +236,6 @@ def get_common_type(self) -> ScalarType:
168236
"""
169237
return scalar_promotion_type_of(itertools.chain.from_iterable(self._entity_class_to_values.values()))
170238

171-
def validate(self):
172-
"""Validate the annotation data."""
173-
return
174-
175-
def _permute_entities(self, entity_class: EntityClass, new_to_old: list[int]) -> None:
176-
"""Reorder the values for the given entity class according to the new order.
177-
178-
:param entity_class: the entity class for which to reorder values
179-
:param new_to_old: a list mapping new indices to old indices, where new_to_old[new_index] = old_index
180-
"""
181-
if not self.has_values_for(entity_class):
182-
return
183-
values = self._entity_class_to_values[entity_class]
184-
# we don't check the order
185-
self._entity_class_to_values[entity_class] = [values[i] for i in new_to_old]
186-
187239

188240
class RewardAnnotation(Annotation):
189241
"""Reward annotation is a numeric annotation that can be associated with states, choices, or branches."""
@@ -221,9 +273,10 @@ def validate(self):
221273
class ObservationAnnotation(Annotation):
222274
"""Observation annotation is an integer annotation that can only be associated with either states or branches."""
223275

224-
def __init__(self, num_observations: int) -> None:
225-
super().__init__(name="observation")
226-
self.num_observations = num_observations
276+
def __init__(self, **kwargs) -> None:
277+
super().__init__(name="observation", **kwargs)
278+
self._entity_managers[EntityClass.STATES].codomain = self.entity_spaces[EntityClass.OBSERVATIONS]
279+
self._entity_managers[EntityClass.BRANCHES].codomain = self.entity_spaces[EntityClass.OBSERVATIONS]
227280

228281
@classmethod
229282
def entity_class_enabled(cls, entity_class: EntityClass) -> bool:
@@ -248,15 +301,11 @@ def set_values_for(self, entity_class: EntityClass, values: Sequence[Scalar]) ->
248301

249302
def validate(self) -> None:
250303
super().validate()
251-
if not (0 < self.num_observations):
252-
raise ValueError(f"num_observations must be a positive integer, got {self.num_observations}")
253-
if self.get_common_type() != NumericPrimitiveType.INT:
254-
raise ValueError(f"Observation annotation type must be INT, got {self.get_common_type()}")
255-
for item, obs in enumerate(self.values):
256-
if not 0 <= obs < self.num_observations: # type: ignore
257-
raise ValueError(f"observation mapping[{item}] = {obs} is out of range [0, {self.num_observations})")
304+
datatype = self.get_common_type()
305+
if datatype != NumericPrimitiveType.INT:
306+
raise ValueError(f"Observation annotation type must be INT, got {datatype}")
258307

259308
def __eq__(self, other: object) -> bool:
260309
if not isinstance(other, ObservationAnnotation):
261310
return False
262-
return super().__eq__(other) and self.num_observations == other.num_observations
311+
return super().__eq__(other)

0 commit comments

Comments
 (0)