Module probabilistic_automata.pa
Code for modeling a probablistic automaton.
Expand source code
"""Code for modeling a probablistic automaton."""
from __future__ import annotations
import random
from collections import defaultdict
from typing import Callable, Mapping, Set
import attr
import funcy as fn
from dfa import DFA, SupAlphabet, ProductAlphabet
from dfa.dfa import Alphabet, Letter, State
from probabilistic_automata.distributions import (
Action, ExplicitDistribution, ProductDistribution, EnvDist,
Distribution, prod_dist, uniform
)
def _dict2dist(env_dist) -> EnvDist:
@fn.memoize
def env_dist2(state, action):
dist = env_dist(state, action)
if isinstance(dist, Distribution.__args__):
return dist
return ExplicitDistribution(dist)
return env_dist2
@attr.s(frozen=True, auto_attribs=True)
class PDFA:
"""A DFA over a product alphabet where the first value
is non-deterministic and the second value is set according
to a state indexed stationary distribution.
"""
dfa: DFA = attr.ib()
env_dist: EnvDist = attr.ib(converter=_dict2dist)
@dfa.validator
def _check_product_lang(self, _, dfa):
if isinstance(dfa.inputs, ProductAlphabet):
return
assert all(isinstance(i, tuple) and len(i) == 2 for i in dfa.inputs)
@property
def env_inputs(self):
"""Accesses the set of environment inputs."""
if isinstance(self.dfa.inputs, ProductAlphabet):
return self.dfa.inputs.right
return set(fn.pluck(1, self.dfa.inputs))
@property
def inputs(self):
"""Accesses the set of (non-environment) inputs."""
if isinstance(self.dfa.inputs, ProductAlphabet):
return self.dfa.inputs.left
return set(fn.pluck(0, self.dfa.inputs))
@property
def outputs(self):
"""Accesses the set of possible outputs."""
return self.dfa.outputs
@property
def start(self):
"""Accesses the start state."""
return self.dfa.start
def states(self):
"""Computes the set of reachable states from start."""
return self.dfa.states()
def _label(self, state):
return self.dfa.label((), start=state)
def run(self, *, start=None, seed=None, label=False):
"""Co-routine interface for simulating runs of the automaton.
- Users can send system actions (elements of self.inputs).
- Co-routine yields the current state.
If label is True, then state labels are returned instead
of states.
Example:
=======
machine: PDFA = ..
my_input: Action = .. # Element of machine.inputs.
sim = machine.run() # Start co-routine.
state1 = sim.send(my_input)
state2 = sim.send(my_input)
"""
if seed is not None:
random.seed(seed)
labeler = self.dfa._label if label else lambda x: x
machine = self.dfa.run(start=start)
state = machine.send(None)
while True:
sys_action = yield labeler(state)
env_action = self.env_dist(state, sys_action).sample()
state = machine.send((sys_action, env_action))
trace = DFA.trace
transition = DFA.transition
transduce = DFA.transduce
label = DFA.label
@fn.memoize
def support(self, state, action) -> Set[State]:
"""Returns the set of reachable states given (state, action)."""
return set(self.transition_probs(state, action).keys())
def _probs(self, start, action):
for e, p in self.env_dist(start, action).items():
end = self.dfa._transition(start, (action, e))
yield (end, p)
def transition_probs(self, state, action) -> Mapping[State, float]:
"""Returns distribution over states given (state, action)"""
probs = defaultdict(lambda: 0)
for end, prob in self._probs(state, action):
probs[end] += prob
return probs
def prob(self, start: State, end: State, action: Action) -> float:
"""
Returns the probability of transitioning from start to end
given action.
"""
return sum(p for s, p in self._probs(start, action) if s == end)
def __or__(self, other: PDFA) -> PDFA:
def transition(composite_state, composite_action):
state_l, state_r = composite_state
(input_l, input_r), (env_l, env_r) = composite_action
state2_l = self.dfa._transition(state_l, (input_l, env_l))
state2_r = other.dfa._transition(state_r, (input_r, env_r))
return (state2_l, state2_r)
return pdfa(
start=(self.start, other.start),
label=lambda s: (self.dfa._label(s[0]), other.dfa._label(s[1])),
transition=transition,
env_dist=prod_dist(self.env_dist, other.env_dist),
inputs=ProductAlphabet(self.inputs, other.inputs),
env_inputs=ProductAlphabet(self.env_inputs, other.env_inputs),
outputs=ProductAlphabet(self.outputs, other.outputs),
)
def __rshift__(self, other: PDFA) -> PDFA:
def transition(composite_state, composite_action):
state_l, state_r = composite_state
input_l, (env_l, env_r) = composite_action
input_r = self.dfa._label(state_l)
state2_l = self.dfa._transition(state_l, (input_l, env_l))
state2_r = other.dfa._transition(state_r, (input_r, env_r))
return (state2_l, state2_r)
def env_dist(composite_state, input_l):
state_l, state_r = composite_state
input_r = self.dfa._label(state_l)
return ProductDistribution(
left=self.env_dist(state_l, input_l),
right=other.env_dist(state_r, input_r),
)
return pdfa(
start=(self.start, other.start),
label=lambda s: other.dfa._label(s[1]),
transition=transition,
env_dist=env_dist,
inputs=self.inputs,
env_inputs=ProductAlphabet(self.env_inputs, other.env_inputs),
outputs=other.outputs,
)
def __lshift__(self, other: PDFA) -> PDFA:
return other >> self
def pdfa(
start: State,
label: Callable[[State], Letter],
transition: Callable[[State, Action], State],
env_dist: EnvDist,
inputs: Alphabet = None,
env_inputs: Alphabet = None,
outputs: Alphabet = None
) -> PDFA:
"""Main entrypoint for construction a Probablistic Automaton."""
if inputs is None:
inputs = SupAlphabet()
if outputs is None:
outputs = {True, False}
if env_inputs is None:
env_inputs = {None}
inputs = ProductAlphabet(inputs, env_inputs)
return PDFA(
env_dist=env_dist,
dfa=DFA(
start=start, label=label,
inputs=inputs, outputs=outputs,
transition=transition,
),
)
def lift(dyn: DFA) -> PDFA:
"""Lifts a DFA into a deterministic PDFA."""
return PDFA(
dfa=attr.evolve(
dyn,
inputs=ProductAlphabet(dyn.inputs, {None}),
transition=lambda s, c: dyn._transition(s, c[0]),
),
env_dist=uniform({None}),
)
def randomize(dyn: DFA) -> PDFA:
"""Lifts a DFA into a PDFA where original inputs are applied
uniformly at random.
"""
return PDFA(
dfa=attr.evolve(
dyn,
inputs=ProductAlphabet({None}, dyn.inputs),
transition=lambda s, c: dyn._transition(s, c[1]),
),
env_dist=uniform(dyn.inputs),
)
Functions
def lift(dyn)-
Lifts a DFA into a deterministic PDFA.
Expand source code
def lift(dyn: DFA) -> PDFA: """Lifts a DFA into a deterministic PDFA.""" return PDFA( dfa=attr.evolve( dyn, inputs=ProductAlphabet(dyn.inputs, {None}), transition=lambda s, c: dyn._transition(s, c[0]), ), env_dist=uniform({None}), ) def pdfa(start, label, transition, env_dist, inputs=None, env_inputs=None, outputs=None)-
Main entrypoint for construction a Probablistic Automaton.
Expand source code
def pdfa( start: State, label: Callable[[State], Letter], transition: Callable[[State, Action], State], env_dist: EnvDist, inputs: Alphabet = None, env_inputs: Alphabet = None, outputs: Alphabet = None ) -> PDFA: """Main entrypoint for construction a Probablistic Automaton.""" if inputs is None: inputs = SupAlphabet() if outputs is None: outputs = {True, False} if env_inputs is None: env_inputs = {None} inputs = ProductAlphabet(inputs, env_inputs) return PDFA( env_dist=env_dist, dfa=DFA( start=start, label=label, inputs=inputs, outputs=outputs, transition=transition, ), ) def randomize(dyn)-
Lifts a DFA into a PDFA where original inputs are applied uniformly at random.
Expand source code
def randomize(dyn: DFA) -> PDFA: """Lifts a DFA into a PDFA where original inputs are applied uniformly at random. """ return PDFA( dfa=attr.evolve( dyn, inputs=ProductAlphabet({None}, dyn.inputs), transition=lambda s, c: dyn._transition(s, c[1]), ), env_dist=uniform(dyn.inputs), )
Classes
class PDFA (dfa, env_dist)-
A DFA over a product alphabet where the first value is non-deterministic and the second value is set according to a state indexed stationary distribution.
Expand source code
class PDFA: """A DFA over a product alphabet where the first value is non-deterministic and the second value is set according to a state indexed stationary distribution. """ dfa: DFA = attr.ib() env_dist: EnvDist = attr.ib(converter=_dict2dist) @dfa.validator def _check_product_lang(self, _, dfa): if isinstance(dfa.inputs, ProductAlphabet): return assert all(isinstance(i, tuple) and len(i) == 2 for i in dfa.inputs) @property def env_inputs(self): """Accesses the set of environment inputs.""" if isinstance(self.dfa.inputs, ProductAlphabet): return self.dfa.inputs.right return set(fn.pluck(1, self.dfa.inputs)) @property def inputs(self): """Accesses the set of (non-environment) inputs.""" if isinstance(self.dfa.inputs, ProductAlphabet): return self.dfa.inputs.left return set(fn.pluck(0, self.dfa.inputs)) @property def outputs(self): """Accesses the set of possible outputs.""" return self.dfa.outputs @property def start(self): """Accesses the start state.""" return self.dfa.start def states(self): """Computes the set of reachable states from start.""" return self.dfa.states() def _label(self, state): return self.dfa.label((), start=state) def run(self, *, start=None, seed=None, label=False): """Co-routine interface for simulating runs of the automaton. - Users can send system actions (elements of self.inputs). - Co-routine yields the current state. If label is True, then state labels are returned instead of states. Example: ======= machine: PDFA = .. my_input: Action = .. # Element of machine.inputs. sim = machine.run() # Start co-routine. state1 = sim.send(my_input) state2 = sim.send(my_input) """ if seed is not None: random.seed(seed) labeler = self.dfa._label if label else lambda x: x machine = self.dfa.run(start=start) state = machine.send(None) while True: sys_action = yield labeler(state) env_action = self.env_dist(state, sys_action).sample() state = machine.send((sys_action, env_action)) trace = DFA.trace transition = DFA.transition transduce = DFA.transduce label = DFA.label @fn.memoize def support(self, state, action) -> Set[State]: """Returns the set of reachable states given (state, action).""" return set(self.transition_probs(state, action).keys()) def _probs(self, start, action): for e, p in self.env_dist(start, action).items(): end = self.dfa._transition(start, (action, e)) yield (end, p) def transition_probs(self, state, action) -> Mapping[State, float]: """Returns distribution over states given (state, action)""" probs = defaultdict(lambda: 0) for end, prob in self._probs(state, action): probs[end] += prob return probs def prob(self, start: State, end: State, action: Action) -> float: """ Returns the probability of transitioning from start to end given action. """ return sum(p for s, p in self._probs(start, action) if s == end) def __or__(self, other: PDFA) -> PDFA: def transition(composite_state, composite_action): state_l, state_r = composite_state (input_l, input_r), (env_l, env_r) = composite_action state2_l = self.dfa._transition(state_l, (input_l, env_l)) state2_r = other.dfa._transition(state_r, (input_r, env_r)) return (state2_l, state2_r) return pdfa( start=(self.start, other.start), label=lambda s: (self.dfa._label(s[0]), other.dfa._label(s[1])), transition=transition, env_dist=prod_dist(self.env_dist, other.env_dist), inputs=ProductAlphabet(self.inputs, other.inputs), env_inputs=ProductAlphabet(self.env_inputs, other.env_inputs), outputs=ProductAlphabet(self.outputs, other.outputs), ) def __rshift__(self, other: PDFA) -> PDFA: def transition(composite_state, composite_action): state_l, state_r = composite_state input_l, (env_l, env_r) = composite_action input_r = self.dfa._label(state_l) state2_l = self.dfa._transition(state_l, (input_l, env_l)) state2_r = other.dfa._transition(state_r, (input_r, env_r)) return (state2_l, state2_r) def env_dist(composite_state, input_l): state_l, state_r = composite_state input_r = self.dfa._label(state_l) return ProductDistribution( left=self.env_dist(state_l, input_l), right=other.env_dist(state_r, input_r), ) return pdfa( start=(self.start, other.start), label=lambda s: other.dfa._label(s[1]), transition=transition, env_dist=env_dist, inputs=self.inputs, env_inputs=ProductAlphabet(self.env_inputs, other.env_inputs), outputs=other.outputs, ) def __lshift__(self, other: PDFA) -> PDFA: return other >> selfInstance variables
var env_inputs-
Accesses the set of environment inputs.
Expand source code
@property def env_inputs(self): """Accesses the set of environment inputs.""" if isinstance(self.dfa.inputs, ProductAlphabet): return self.dfa.inputs.right return set(fn.pluck(1, self.dfa.inputs)) var inputs-
Accesses the set of (non-environment) inputs.
Expand source code
@property def inputs(self): """Accesses the set of (non-environment) inputs.""" if isinstance(self.dfa.inputs, ProductAlphabet): return self.dfa.inputs.left return set(fn.pluck(0, self.dfa.inputs)) var outputs-
Accesses the set of possible outputs.
Expand source code
@property def outputs(self): """Accesses the set of possible outputs.""" return self.dfa.outputs var start-
Accesses the start state.
Expand source code
@property def start(self): """Accesses the start state.""" return self.dfa.start
Methods
def label(self, word, *, start=None)-
Returns the label of the input word.
Expand source code
def label(self, word, *, start=None): """Returns the label of the input word.""" output = self._label(self.transition(word, start=start)) assert (self.outputs is None) or (output in self.outputs) return output def prob(self, start, end, action)-
Returns the probability of transitioning from start to end given action.
Expand source code
def prob(self, start: State, end: State, action: Action) -> float: """ Returns the probability of transitioning from start to end given action. """ return sum(p for s, p in self._probs(start, action) if s == end) def run(self, *, start=None, seed=None, label=False)-
Co-routine interface for simulating runs of the automaton.
- Users can send system actions (elements of self.inputs).
- Co-routine yields the current state.
If label is True, then state labels are returned instead of states.
Example:
machine: PDFA = .. my_input: Action = .. # Element of machine.inputs.
sim = machine.run() # Start co-routine.
state1 = sim.send(my_input) state2 = sim.send(my_input)
Expand source code
def run(self, *, start=None, seed=None, label=False): """Co-routine interface for simulating runs of the automaton. - Users can send system actions (elements of self.inputs). - Co-routine yields the current state. If label is True, then state labels are returned instead of states. Example: ======= machine: PDFA = .. my_input: Action = .. # Element of machine.inputs. sim = machine.run() # Start co-routine. state1 = sim.send(my_input) state2 = sim.send(my_input) """ if seed is not None: random.seed(seed) labeler = self.dfa._label if label else lambda x: x machine = self.dfa.run(start=start) state = machine.send(None) while True: sys_action = yield labeler(state) env_action = self.env_dist(state, sys_action).sample() state = machine.send((sys_action, env_action)) def states(self)-
Computes the set of reachable states from start.
Expand source code
def states(self): """Computes the set of reachable states from start.""" return self.dfa.states() def support(self, state, action)-
Returns the set of reachable states given (state, action).
Expand source code
@fn.memoize def support(self, state, action) -> Set[State]: """Returns the set of reachable states given (state, action).""" return set(self.transition_probs(state, action).keys()) def trace(self, word, *, start=None)-
Creates a generator that yields the sequence of states that results from reading the word.
Expand source code
def trace(self, word, *, start=None): """Creates a generator that yields the sequence of states that results from reading the word. """ machine = self.run(start=start) for char in fn.concat([None], word): state = machine.send(char) yield state def transduce(self, word, *, start=None)-
Returns the labels of the states traced by the input word.
Expand source code
def transduce(self, word, *, start=None): """Returns the labels of the states traced by the input word.""" return tuple(map(self._label, self.trace(word, start=start)))[:-1] def transition(self, word, *, start=None)-
Returns the state the input word accesses.
Expand source code
def transition(self, word, *, start=None): """Returns the state the input word accesses.""" return fn.last(self.trace(word, start=start)) def transition_probs(self, state, action)-
Returns distribution over states given (state, action)
Expand source code
def transition_probs(self, state, action) -> Mapping[State, float]: """Returns distribution over states given (state, action)""" probs = defaultdict(lambda: 0) for end, prob in self._probs(state, action): probs[end] += prob return probs