Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Symbolic signal/property references #86

Merged
merged 10 commits into from
May 13, 2019
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
.idea
__pycache__
tts.wav
keys*
keys.yml
dist/*
build/*
Expand All @@ -15,4 +16,4 @@ ros2/build
ros2/install
ros2/log
venv/*
mkdocs.yml
mkdocs.yml
11 changes: 11 additions & 0 deletions modules/ravestate/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from .activation import *
from .argparser import *
from .causal import *
from .config import *
from .constraint import *
from .context import *
from .module import *
from .property import *
from .receptor import *
from .spike import *
from .state import *
14 changes: 7 additions & 7 deletions modules/ravestate/activation.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def resources(self) -> Set[str]:
Return's the set of the activation's write-access property names.
"""
if self.state_to_activate.write_props:
return set(self.state_to_activate.write_props)
return self.state_to_activate.get_write_props_ids()
else:
# Return a dummy resource that will be "consumed" by the activation.
# This allows CausalGroup to track the spike acquisitions for this
Expand Down Expand Up @@ -236,7 +236,7 @@ def update(self) -> bool:
self.death_clock = None

# Ask each spike's causal group for activation consent
spikes_for_conjunct = set((sig.spike, sig.detached) for sig in conjunction.signals())
spikes_for_conjunct = set((sig.spike, sig.detached_value) for sig in conjunction.signals())
consenting_causal_groups = set()
all_consented = True
for spike, detached in spikes_for_conjunct:
Expand All @@ -255,7 +255,7 @@ def update(self) -> bool:

# Gather payloads for all spikes
self.spike_payloads = {
spike.name(): spike.payload()
spike.id(): spike.payload()
for spike, _ in spikes_for_conjunct if spike.payload()}

# Notify all consenting causal groups that activation is going forward
Expand Down Expand Up @@ -334,17 +334,17 @@ def _run_private(self):

# Process state function result
if isinstance(result, Emit):
if self.state_to_activate.signal():
if self.state_to_activate.signal:
self.ctx.emit(
self.state_to_activate.signal(),
self.state_to_activate.signal,
parents=self.parent_spikes,
wipe=result.wipe)
else:
logger.error(f"Attempt to emit spike from state {self.name}, which does not specify a signal name!")

elif isinstance(result, Wipe):
if self.state_to_activate.signal():
self.ctx.wipe(self.state_to_activate.signal())
if self.state_to_activate.signal:
self.ctx.wipe(self.state_to_activate.signal)
else:
logger.error(f"Attempt to wipe spikes from state {self.name}, which does not specify a signal name!")

Expand Down
43 changes: 38 additions & 5 deletions modules/ravestate/causal.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,9 +249,9 @@ def rejected(self, spike: 'ISpike', rejected_by: IActivation, reason: int) -> No
Called by a state activation, to notify the group that a member spike
is no longer being referenced for the given state's write props.
This may be because ... <br>
... the state activation's dereference function was called. (reason=0) <br>
... the activation's dereference function was called. (reason=0) <br>
... the spike got too old. (reason=1) <br>
... the activation is happening and dereferencing it's spikes. (reason=2)
... the activation happened and is dereferencing it's spikes. (reason=2)

* `spike`: The member spike whose ref-set should be reduced.

Expand Down Expand Up @@ -281,7 +281,7 @@ def _decrement_refcount(refcount_for_act_for_spike):

self._change_effect_causes(rejected_by, -1)

logger.debug(f"{self}.rejected({spike} by {rejected_by}): ")
logger.debug(f"{self}.rejected({spike} by {rejected_by})")

def consent(self, ready_suitor: IActivation) -> bool:
"""
Expand Down Expand Up @@ -437,8 +437,8 @@ def _change_effect_causes(self, act: IActivation, change: Optional[int]):
if self._uncaused_spikes[sig][act] <= 0:
del self._uncaused_spikes[sig][act]
if len(self._uncaused_spikes[sig]) == 0:
for act in set(self._non_detached_activations()):
act.effect_not_caused(self, sig.name)
for act_to_notify in set(self._non_detached_activations()):
act_to_notify.effect_not_caused(self, sig.id())
del self._uncaused_spikes[sig]

def _non_detached_activations(self) -> Generator[IActivation, None, None]:
Expand All @@ -450,3 +450,36 @@ def _non_detached_activations(self) -> Generator[IActivation, None, None]:
yielded_activations.add(act)
yield act

def check_reference_sanity(self) -> bool:
"""
Make sure, that the refcount-per-act-per-spike-per-resource value sum
is equal to the number of spikes from this causal group acquired per activation
for each activation in the index.
:return: True if the criterion is fulfilled, False otherwise.
"""
result = True
signals_with_cause = set()
for act in self._non_detached_activations():
sum_refcount = sum(
refc_per_act[act]
for resource in act.resources()
for refc_per_act in self._ref_index[resource].values() if act in refc_per_act)
sum_spikes = sum(
len(act.resources())
for signal in act.constraint.signals() if signal.spike and signal.spike.causal_group() == self)
if sum_spikes != sum_refcount:
logger.error(f"Mutual refcount mismatch: {self} -> {sum_refcount} : {sum_spikes} <- {act}")
result = False
for sig in act.possible_signals():
signals_with_cause.add(sig)
if sig in self._uncaused_spikes:
causes = sum_spikes / len(act.resources())
if self._uncaused_spikes[sig][act] != causes:
logger.error(f"Signal cause mismatch for {sig} by {act}: is {self._uncaused_spikes[sig][act]}, "
f"should be {causes}")
result = False
for signal in set(self._uncaused_spikes.keys())-signals_with_cause:
logger.error(f"Signal cause mismatch for {signal}: is {self._uncaused_spikes[signal]}, "
f"should be ZERO")
result = False
return result
112 changes: 72 additions & 40 deletions modules/ravestate/constraint.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import copy
from typing import List, Set, Generator, Optional, Tuple, Union, Callable, Any
from ravestate.spike import Spike
from ravestate.iactivation import IActivation, ICausalGroup
from ravestate.threadlocal import ravestate_thread_local


from reggol import get_logger
logger = get_logger(__name__)
Expand All @@ -16,25 +19,6 @@ def __init__(self, key: str):
self.key = key


def s(signal_name: str, *, min_age: Union[float, ConfigurableAge] = 0., max_age: Union[float, ConfigurableAge] = 5.,
detached: bool = False) -> 'Signal':
"""
Alias to call Signal-constructor

* `signal_name`: Name of the Signal

* `min_age`: Minimum age for the signal, in seconds. Can also be ConfigurableAge that gets the age from the config.

* `max_age`: Maximum age for the signal, in seconds. Can also be ConfigurableAge that gets the age from the config.
Set to less-than zero for unrestricted age.

* `detached`: Flag which indicates, whether spikes that fulfill this signal
are going to have a separate causal group from spikes that
are generated by a state that uses this signal as a constraint.
"""
return Signal(signal_name, min_age=min_age, max_age=max_age, detached=detached)


class Constraint:
"""
Superclass for Signal, Conjunct and Disjunct
Expand Down Expand Up @@ -77,13 +61,16 @@ def effect_not_caused(self, act: IActivation, group: ICausalGroup, effect: str)

class Signal(Constraint):
"""
Class that represents a Signal
Class that represents a Signal. Should be constructed in a `with Module(..)`
context, such that it's module scope is set automatically.
"""

name: str
spike: Optional[Spike]
min_age: float
max_age: float
detached: bool
min_age_value: float
max_age_value: float
detached_value: bool
module_name: str

# tells whether this signal has been completed, and may therefore
# not introduce a new causal group into the parent conjunct.
Expand All @@ -99,19 +86,25 @@ class Signal(Constraint):

_min_age_ticks: int # written on acquire, when act.secs_to_ticks is available

def __init__(self, name: str, *, min_age=0., max_age=5., detached=False):
def __init__(self, name: str, *, min_age=0., max_age=5., detached=False, _skip_module_context=False):
self.name = name
self.min_age = min_age
self.max_age = max_age
self.min_age_value = min_age
self.max_age_value = max_age
self.spike = None
self.detached = detached
self.detached_value = detached
self.completed_by = None
self.is_completion = False
self._min_age_ticks = 0
self.module_name = ""
# TODO: Deal with ConfigurableAge
# if min_age > max_age and max_age > .0:
# logger.warning(f"{self}: max_age={max_age} < min_age={min_age}!")

# add signal to module in current `with Module(...)` clause
module_under_construction = getattr(ravestate_thread_local, 'module_under_construction', None)
if module_under_construction:
module_under_construction.add(self)

def __or__(self, other):
if isinstance(other, Signal):
return Disjunct(Conjunct(self), Conjunct(other))
Expand All @@ -132,31 +125,37 @@ def __and__(self, other):
return Disjunct(*conjunct_list)

def __eq__(self, other):
return (isinstance(other, Signal) and self.name == other.name) or\
(isinstance(other, str) and self.name == other)
return (isinstance(other, Signal) and self.id() == other.id()) or \
(isinstance(other, str) and self.id() == other)

def __hash__(self):
return hash(self.name)
return hash(self.id())

def __repr__(self):
return f"Signal({self.name}, {self.min_age}, {self.max_age}, {self.detached})"
return f"Signal({self.id()}, {self.min_age_value}, {self.max_age_value}, {self.detached_value})"

def __str__(self):
return self.id()

def id(self):
return f'{self.module_name}:{self.name}'

def signals(self) -> Generator['Signal', None, None]:
yield self

def conjunctions(self, filter_detached=False) -> Generator['Conjunct', None, None]:
if not filter_detached or not self.detached:
if not filter_detached or not self.detached_value:
yield Conjunct(self)

def acquire(self, spike: Spike, act: IActivation):
if not self.spike and self.name == spike.name() and (self.max_age < 0 or spike.age() <= act.secs_to_ticks(self.max_age)):
if not self.spike and self.id() == spike.id() and (self.max_age_value < 0 or spike.age() <= act.secs_to_ticks(self.max_age_value)):
assert not spike.is_wiped()
with spike.causal_group() as cg:
# Causal group might refuse acquisition, if one of act's state's write-props is unavailable.
if not cg.acquired(spike, act, self.detached):
if not cg.acquired(spike, act, self.detached_value):
return False
self._min_age_ticks = act.secs_to_ticks(self.min_age)
self.spike = spike
self.spike = spike
self._min_age_ticks = act.secs_to_ticks(self.min_age_value)
return True
return False

Expand Down Expand Up @@ -184,7 +183,7 @@ def dereference(self, *,

def update(self, act: IActivation) -> Generator['Signal', None, None]:
# Reject spike, once it has become too old
if self.spike and self.max_age >= 0 and self.spike.age() > act.secs_to_ticks(self.max_age):
if self.spike and self.max_age_value >= 0 and self.spike.age() > act.secs_to_ticks(self.max_age_value):
with self.spike.causal_group() as cg:
cg.rejected(self.spike, act, reason=1)
self.spike = None
Expand All @@ -207,7 +206,40 @@ def effect_not_caused(self, act: IActivation, group: ICausalGroup, effect: str)
cause.spike = None
yield cause

def __str__(self):
def min_age(self, min_age: Union[float, ConfigurableAge]) -> 'Signal':
new_sig = copy.deepcopy(self)
new_sig.min_age_value = min_age
return new_sig

def max_age(self, max_age: Union[float, ConfigurableAge]) -> 'Signal':
new_sig = copy.deepcopy(self)
new_sig.max_age_value = max_age
return new_sig

def detached(self) -> 'Signal':
new_sig = copy.deepcopy(self)
new_sig.detached_value = True
return new_sig


class SignalRef(Signal):
"""
Signal reference. Almost the same as a signal, except that
it will not try to auto-discover it's module out of thread-local context
(module_name will stay unfilled). Needed, because sometimes
you need to reference a singal within a module scope
without assigning that signal to the contextual module.
"""

def __init__(self, name: str, *, min_age=0., max_age=5., detached=False):
super().__init__(
name,
min_age=min_age,
max_age=max_age,
detached=detached,
_skip_module_context=True)

def id(self):
return self.name


Expand All @@ -231,7 +263,7 @@ def __init__(self, *args):
logger.error("Conjunct can only be constructed with Signals.")
raise ValueError
self._signals = set(args)
self._hash = hash(tuple(sorted(sig.name for sig in self._signals)))
self._hash = hash(tuple(sorted(sig.id() for sig in self._signals)))
self._allowed_causal_groups = set()

def __iter__(self):
Expand Down Expand Up @@ -272,7 +304,7 @@ def signals(self) -> Generator['Signal', None, None]:
def conjunctions(self, filter_detached=False) -> Generator['Conjunct', None, None]:
result = self
if filter_detached:
result = Conjunct(*(sig for sig in self._signals if not sig.detached))
result = Conjunct(*(sig for sig in self._signals if not sig.detached_value))
if result._signals:
yield result

Expand Down
4 changes: 2 additions & 2 deletions modules/ravestate/consumable.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# Dummy resource which allowed CausalGroups to track acquisitions
# for states that don't have any write-props.

from ravestate.property import PropertyBase
from ravestate.property import Property

from reggol import get_logger
logger = get_logger(__name__)


class Consumable(PropertyBase):
class Consumable(Property):
"""
Dummy resource which allows CausalGroups to track acquisitions
for states that don't have any write-props.
Expand Down
Loading