Skip to content

Commit 02980eb

Browse files
committed
improve ExplicitAts API
1 parent 3e54433 commit 02980eb

8 files changed

Lines changed: 348 additions & 223 deletions

File tree

umbi/ats/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
write,
1717
)
1818
from .entity_class import EntityClass
19-
from .explicit_ats import ExplicitAts, TimeType
19+
from .explicit_ats import Branch, Choice, ExplicitAts, TimeType
2020
from .model_info import ModelInfo
2121
from .variable_valuations import EntityClassValuations, EntityValuation, EntityValuations, Variable, VariableValuations
2222

@@ -30,6 +30,8 @@
3030
# model_info
3131
"ModelInfo",
3232
# explicit_ats
33+
"Branch",
34+
"Choice",
3335
"TimeType",
3436
"ExplicitAts",
3537
# variable_valuations

umbi/ats/annotations.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,19 +121,19 @@ def observation_values(self) -> list[Scalar]:
121121
def player_values(self) -> list[Scalar]:
122122
return self.get_values_for(EntityClass.PLAYERS)
123123

124-
def set_state_values(self, values: list[Scalar]):
124+
def set_state_values(self, values: Sequence[Scalar]):
125125
self.set_values_for(EntityClass.STATES, values)
126126

127-
def set_choice_values(self, values: list[Scalar]):
127+
def set_choice_values(self, values: Sequence[Scalar]):
128128
self.set_values_for(EntityClass.CHOICES, values)
129129

130-
def set_branch_values(self, values: list[Scalar]):
130+
def set_branch_values(self, values: Sequence[Scalar]):
131131
self.set_values_for(EntityClass.BRANCHES, values)
132132

133-
def set_observation_values(self, values: list[Scalar]):
133+
def set_observation_values(self, values: Sequence[Scalar]):
134134
self.set_values_for(EntityClass.OBSERVATIONS, values)
135135

136-
def set_player_values(self, values: list[Scalar]):
136+
def set_player_values(self, values: Sequence[Scalar]):
137137
self.set_values_for(EntityClass.PLAYERS, values)
138138

139139
def unset_state_values(self):

umbi/ats/ats_to_umb.py

Lines changed: 61 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ def explicit_umb_to_explicit_ats(umb: umbi.umb.ExplicitUmb) -> ExplicitAts:
1818
ats = ExplicitAts()
1919

2020
## index
21-
# skip format_version, format_revision and file_data
21+
# strip format_version, format_revision and file_data
22+
# load umb.index.model_data
2223
md = umb.index.model_data
2324
if md is not None:
2425
ats.model_info = ModelInfo(
@@ -30,33 +31,51 @@ def explicit_umb_to_explicit_ats(umb: umbi.umb.ExplicitUmb) -> ExplicitAts:
3031
doi=md.doi,
3132
url=md.url,
3233
)
34+
3335
# load index.transition_system
3436
ts = umb.index.transition_system
3537
ats.time = TimeType(ts.time)
3638
ats.num_players = ts.num_players
37-
ats.num_states = ts.num_states
3839
ats.num_initial_states = ts.num_initial_states
39-
ats.num_choices = ts.num_choices
4040
ats.num_choice_actions = ts.num_choice_actions
41-
ats.num_branches = ts.num_branches
4241
ats.num_branch_actions = ts.num_branch_actions
4342
ats.player_to_name = ts.player_to_name
4443

4544
## values
4645
ats.state_is_initial = umb.state_is_initial
47-
ats.state_to_choice = umb.state_to_choices
4846
ats.state_to_player = umb.state_to_player
4947

5048
ats.state_is_markovian = umb.state_is_markovian
5149
ats.state_to_exit_rate = umb.state_to_exit_rate
5250

53-
ats.choice_to_branches = umb.choice_to_branches
54-
ats.branch_to_target = umb.branch_to_target
55-
ats.branch_to_probability = umb.branch_to_probability
51+
for state_index in range(ts.num_states):
52+
state = ats.add_state()
53+
assert state == state_index
54+
55+
state_to_choices = (
56+
umb.state_to_choices if umb.state_to_choices is not None else [s for s in range(ats.num_states + 1)]
57+
)
58+
if ts.num_choices > 0:
59+
for state in range(ats.num_states):
60+
for choice_idx in range(state_to_choices[state], state_to_choices[state + 1]):
61+
choice = ats.add_state_choice(state=state)
62+
if ts.num_branches > 0:
63+
assert umb.choice_to_branches is not None, "num_branches > 0 but choice_to_branches is None"
64+
for branch_idx in range(umb.choice_to_branches[choice_idx], umb.choice_to_branches[choice_idx + 1]):
65+
assert umb.branch_to_target is not None, "num_branches > 0 but branch_to_target is None"
66+
target = umb.branch_to_target[branch_idx]
67+
prob = umb.branch_to_probability[branch_idx] if umb.branch_to_probability is not None else None
68+
branch_action = (
69+
umb.branch_to_branch_action[branch_idx] if umb.branch_to_branch_action is not None else None
70+
)
71+
choice.add_branch(target=target, prob=prob, action=branch_action)
72+
if ts.num_choice_actions > 0:
73+
assert umb.choice_to_choice_action is not None, (
74+
"num_choice_actions > 0 but choice_to_choice_action is None"
75+
)
76+
choice.action = umb.choice_to_choice_action[choice_idx]
5677

57-
ats.choice_to_choice_action = umb.choice_to_choice_action
5878
ats.choice_action_to_name = umb.choice_action_to_string
59-
ats.branch_to_branch_action = umb.branch_to_branch_action
6079
ats.branch_action_to_name = umb.branch_action_to_string
6180

6281
# load annotations
@@ -167,7 +186,6 @@ def explicit_ats_to_explicit_umb(ats: ExplicitAts) -> umbi.umb.ExplicitUmb:
167186
# warning: this does not copy the values, but directly uses the lists from ats.
168187
# this is fine as long as we don't modify them
169188
umb.state_is_initial = ats.state_is_initial
170-
umb.state_to_choices = ats.state_to_choice
171189
umb.state_to_player = ats.state_to_player
172190

173191
umb.state_is_markovian = ats.state_is_markovian
@@ -178,19 +196,40 @@ def explicit_ats_to_explicit_umb(ats: ExplicitAts) -> umbi.umb.ExplicitUmb:
178196
umb.index.transition_system.exit_rate_type = SizedType.for_type(target_type)
179197
umb.state_to_exit_rate = vector # type: ignore
180198

181-
umb.choice_to_branches = ats.choice_to_branches
182-
umb.branch_to_target = ats.branch_to_target
183-
184-
if ats.branch_to_probability is not None:
185-
# promote
186-
target_type, vector = umbi.datatypes.promote_scalars(ats.branch_to_probability)
187-
assert isinstance(target_type, umbi.datatypes.NumericType), "branch probabilities must be numeric"
188-
umb.index.transition_system.branch_probability_type = SizedType.for_type(target_type)
189-
umb.branch_to_probability = vector # type: ignore
199+
umb.state_to_choices = []
200+
num_choices = 0
201+
for state in range(ats.num_states):
202+
umb.state_to_choices.append(num_choices)
203+
num_choices += ats.num_state_choices(state)
204+
umb.state_to_choices.append(num_choices)
205+
206+
if ats.num_choices > 0:
207+
if ats.num_choice_actions > 0:
208+
umb.choice_to_choice_action = [choice.action for choice in ats.choices] # type: ignore
209+
210+
branches = []
211+
umb.choice_to_branches = []
212+
for choice in ats.choices:
213+
umb.choice_to_branches.append(len(branches))
214+
branches.extend(choice.branches)
215+
umb.choice_to_branches.append(len(branches))
216+
if len(branches) > 0:
217+
umb.branch_to_target = [branch.target for branch in branches]
218+
if any(branch.prob is not None for branch in branches):
219+
# has branch probabilities
220+
branch_to_probability = [branch.prob if branch.prob is not None else 1 for branch in branches]
221+
# promote
222+
target_type, vector = umbi.datatypes.promote_scalars(branch_to_probability)
223+
# assert isinstance(target_type, umbi.datatypes.NumericType), "branch probabilities must be numeric"
224+
umb.index.transition_system.branch_probability_type = SizedType.for_type(target_type)
225+
umb.branch_to_probability = vector # type: ignore
226+
if ats.num_branch_actions > 0:
227+
branch_actions = [branch.action for branch in branches]
228+
if any(action is None for action in branch_actions):
229+
raise ValueError("if num_branch_actions > 0, all branches must have an action")
230+
umb.branch_to_branch_action = branch_actions # type: ignore
190231

191-
umb.choice_to_choice_action = ats.choice_to_choice_action
192232
umb.choice_action_to_string = ats.choice_action_to_name
193-
umb.branch_to_branch_action = ats.branch_to_branch_action
194233
umb.branch_action_to_string = ats.branch_action_to_name
195234

196235
# add annotations

umbi/ats/examples/grid.py

Lines changed: 19 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
1-
"""
2-
umbi demo: Create an ATS from a grid string.
3-
"""
4-
5-
from __future__ import annotations
1+
"""umbi demo: Create an ExplicitAts from a grid string."""
62

73
import logging
84
from fractions import Fraction
95

10-
import umbi
6+
from ..explicit_ats import ExplicitAts, TimeType
7+
from ..variable_valuations import EntityClassValuations, EntityValuations
118

129
logger = logging.getLogger(__name__)
1310

1411

15-
def grid_ats(grid: str) -> umbi.ats.ExplicitAts:
12+
def grid_ats(grid: str) -> ExplicitAts:
1613
"""
1714
Create a simple ATS from a rectangular grid string.
1815
@@ -32,8 +29,8 @@ def grid_ats(grid: str) -> umbi.ats.ExplicitAts:
3229
raise ValueError("The grid must be rectangular (all rows must have the same length).")
3330

3431
# Create ATS
35-
ats = umbi.ats.ExplicitAts()
36-
ats.time = umbi.ats.TimeType.DISCRETE
32+
ats = ExplicitAts()
33+
ats.time = TimeType.DISCRETE
3734
ats.num_players = 1
3835

3936
cell_to_state = {}
@@ -55,13 +52,12 @@ def grid_ats(grid: str) -> umbi.ats.ExplicitAts:
5552
elif c == "g":
5653
goal_states.add(state)
5754

58-
ats.num_states = len(cell_to_state)
5955
ats.set_initial_states(list(initial_states))
6056

6157
if ats.variable_valuations is None:
62-
ats.variable_valuations = umbi.ats.EntityClassValuations()
58+
ats.variable_valuations = EntityClassValuations()
6359
if not ats.variable_valuations.has_state_valuations:
64-
ats.variable_valuations.set_state_valuations(umbi.ats.EntityValuations())
60+
ats.variable_valuations.set_state_valuations(EntityValuations())
6561
state_valuations = ats.variable_valuations.state_valuations
6662
vx = state_valuations.add_variable("x")
6763
vy = state_valuations.add_variable("y")
@@ -76,41 +72,28 @@ def grid_ats(grid: str) -> umbi.ats.ExplicitAts:
7672
"right": (1, 0),
7773
}
7874

79-
ats.state_to_choice = []
80-
ats.choice_to_branches = []
81-
ats.branch_to_target = []
82-
ats.branch_to_probability = []
83-
ats.choice_to_choice_action = []
8475
ats.choice_action_to_name = list(direction_dxdy.keys())
8576
ats.num_choice_actions = len(ats.choice_action_to_name)
8677

8778
for (x, y), state in cell_to_state.items():
88-
ats.state_to_choice.append(len(ats.choice_to_choice_action))
89-
79+
ats.add_state()
9080
for direction, (dx, dy) in direction_dxdy.items():
9181
target = (x + dx, y + dy)
92-
ats.choice_to_choice_action.append(ats.choice_action_to_name.index(direction))
93-
ats.choice_to_branches.append(len(ats.branch_to_target))
82+
choice = ats.add_state_choice(state=state)
83+
choice.action = ats.choice_action_to_name.index(direction)
9484

9585
if target in cell_to_state:
9686
target_state = cell_to_state[target]
97-
ats.branch_to_target.extend([target_state, state])
98-
ats.branch_to_probability.extend([Fraction(9, 10), Fraction(1, 10)])
87+
choice.add_branch(target=target_state, prob=Fraction(9, 10))
88+
choice.add_branch(target=state, prob=Fraction(1, 10))
9989
else:
100-
ats.branch_to_target.append(state)
101-
ats.branch_to_probability.append(1)
102-
103-
ats.state_to_choice.append(len(ats.choice_to_choice_action))
104-
ats.choice_to_branches.append(len(ats.branch_to_target))
105-
ats.num_choices = len(ats.choice_to_choice_action)
106-
ats.num_branches = len(ats.branch_to_target)
90+
choice.add_branch(target=state)
10791

108-
goal_ap = umbi.ats.AtomicPropositionAnnotation(name="goal")
109-
goal_ap.set_state_values([s in goal_states for s in range(ats.num_states)])
110-
ats.add_ap_annotation(goal_ap)
92+
annotation = ats.add_ap_annotation(name="goal")
93+
annotation.set_state_values([s in goal_states for s in range(ats.num_states)])
11194

112-
step_cost_reward = umbi.ats.RewardAnnotation(name="step_cost")
113-
step_cost_reward.set_choice_values([1 for _ in range(ats.num_choices)])
114-
ats.add_reward_annotation(step_cost_reward)
95+
annotation = ats.add_reward_annotation(name="step_cost")
96+
annotation.set_choice_values([1 for _ in range(ats.num_choices)])
11597

98+
ats.validate()
11699
return ats

umbi/ats/examples/random_game.py

Lines changed: 11 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
1-
#!/usr/bin/env python3
21
"""
3-
umbi demo: A random stochastic game generator based on Azeem et al. "Optimistic and Topological Value Iteration for
4-
Simple Stochastic Games".
2+
umbi demo: A random stochastic game generator based on
3+
Azeem et al. "Optimistic and Topological Value Iteration for Simple Stochastic Games".
54
"""
65

7-
from __future__ import annotations
8-
96
import logging
107
import random
118
import time
129

13-
import umbi
10+
from ..explicit_ats import ExplicitAts, TimeType
1411

1512
logger = logging.getLogger(__name__)
1613

@@ -176,7 +173,7 @@ def random_transition_function(
176173
return delta, actions_at_state, all_actions
177174

178175

179-
def random_game_ats(num_states: int, seed: int | None = None) -> umbi.ats.ExplicitAts:
176+
def random_game_ats(num_states: int, seed: int | None = None) -> ExplicitAts:
180177
"""
181178
Generate a random stochastic game connected from initial state.
182179
@@ -187,9 +184,8 @@ def random_game_ats(num_states: int, seed: int | None = None) -> umbi.ats.Explic
187184
delta, actions_at_state, all_actions = random_transition_function(num_states, seed)
188185

189186
# create ATS
190-
ats = umbi.ats.ExplicitAts(
191-
time=umbi.ats.TimeType.STOCHASTIC,
192-
num_states=num_states,
187+
ats = ExplicitAts(
188+
time=TimeType.STOCHASTIC,
193189
num_players=2,
194190
)
195191

@@ -207,40 +203,25 @@ def random_game_ats(num_states: int, seed: int | None = None) -> umbi.ats.Explic
207203
ats.num_choice_actions = len(all_actions)
208204
ats.choice_action_to_name = all_actions
209205

210-
# count choices and branches
211-
ats.num_choices = sum(len(actions) for actions in actions_at_state.values())
212-
ats.num_branches = sum(
213-
len(delta.get(s, {}).get(action, [])) for s in range(num_states) for action in actions_at_state[s]
214-
)
215-
216206
# build CSR structures
217207
logger.info("building CSR structures ...")
218208
build_start = time.time()
219-
ats.state_to_choice = []
220-
ats.choice_to_choice_action = []
221-
ats.choice_to_branches = []
222-
ats.branch_to_target = []
223-
ats.branch_to_probability = []
224209

225210
for s in range(num_states):
226-
ats.state_to_choice.append(len(ats.choice_to_choice_action))
227-
211+
state = ats.add_state()
228212
for action in sorted(actions_at_state[s]):
229-
ats.choice_to_choice_action.append(action)
230-
ats.choice_to_branches.append(len(ats.branch_to_target))
213+
choice = ats.add_state_choice(state=state)
214+
choice.action = action
231215

232216
# get all branches for this (s, action) pair
233217
branches = delta[s][action]
234218
branches.sort() # sort by target state
235219

236220
for target, prob in branches:
237-
ats.branch_to_target.append(target)
238-
ats.branch_to_probability.append(prob)
239-
240-
ats.state_to_choice.append(len(ats.choice_to_choice_action))
241-
ats.choice_to_branches.append(len(ats.branch_to_target))
221+
choice.add_branch(target=target, prob=prob)
242222

243223
build_time = time.time() - build_start
224+
ats.validate()
244225
logger.info(f"CSR building completed in {build_time:.3f}s")
245226
logger.info(f"generated random game: {num_states} states, {ats.num_choices} choices, {ats.num_branches} branches")
246227
logger.info(f"player0 states: {sum(is_player0_state)}, player1 states: {num_states - sum(is_player0_state)}")

0 commit comments

Comments
 (0)