Skip to content

Commit

Permalink
Merge pull request #187 from BerkeleyLearnVerify/dynamics-refactor
Browse files Browse the repository at this point in the history
Refactor dynamics.py; fix message for rejected simulations
  • Loading branch information
dfremont authored Oct 1, 2023
2 parents dc8f524 + 27f577e commit 3af563d
Show file tree
Hide file tree
Showing 13 changed files with 599 additions and 550 deletions.
6 changes: 4 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,8 @@ def maketable(items, columns=5, gap=4):
)
from sphinx.ext.autodoc import ClassDocumenter, FunctionDocumenter

from scenic.core.dynamics import Behavior, DynamicScenario, Monitor
from scenic.core.dynamics.behaviors import Behavior, Monitor
from scenic.core.dynamics.scenarios import DynamicScenario


class ScenicBehavior(PyFunction):
Expand Down Expand Up @@ -557,7 +558,8 @@ def key(entry):

from sphinx.ext.autodoc import ClassDocumenter

from scenic.core.dynamics import Behavior, DynamicScenario
from scenic.core.dynamics.behaviors import Behavior
from scenic.core.dynamics.scenarios import DynamicScenario

orig_add_directive_header = ClassDocumenter.add_directive_header

Expand Down
18 changes: 18 additions & 0 deletions src/scenic/core/dynamics/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""Support for dynamic behaviors and modular scenarios.
A few classes are exposed here for external use, including:
* `Action`;
* `GuardViolation`, `InvariantViolation`, and `PreconditionViolation`;
* `StuckBehaviorWarning`.
Everything else defined in the submodules is an implementation detail and
should not be used outside of Scenic (it may change at any time).
"""

from .actions import Action
from .guards import GuardViolation, InvariantViolation, PreconditionViolation
from .utils import RejectSimulationException, StuckBehaviorWarning

#: Timeout in seconds after which a `StuckBehaviorWarning` will be raised.
stuckBehaviorWarningTimeout = 10
65 changes: 65 additions & 0 deletions src/scenic/core/dynamics/actions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""Actions taken by dynamic agents."""

import abc


class Action(abc.ABC):
"""An :term:`action` which can be taken by an agent for one step of a simulation."""

def canBeTakenBy(self, agent):
"""Whether this action is allowed to be taken by the given agent.
The default implementation always returns True.
"""
return True

@abc.abstractmethod
def applyTo(self, agent, simulation):
"""Apply this action to the given agent in the given simulation.
This method should call simulator APIs so that the agent will take this action
during the next simulated time step. Depending on the simulator API, it may be
necessary to batch each agent's actions into a single update: in that case you
can have this method set some state on the agent, then apply the actual update
in an overridden implementation of `Simulation.executeActions`. For examples,
see the CARLA interface: `scenic.simulators.carla.actions` has some CARLA-specific
actions which directly call CARLA APIs, while the generic steering and braking
actions from `scenic.domains.driving.actions` are implemented using the batching
approach (see for example the ``setThrottle`` method of the class
`scenic.simulators.carla.model.Vehicle`, which sets state later read by
``CarlaSimulation.executeActions`` in `scenic.simulators.carla.simulator`).
"""
raise NotImplementedError


class _EndSimulationAction(Action):
"""Special action indicating it is time to end the simulation.
Only for internal use.
"""

def __init__(self, line):
self.line = line

def __str__(self):
return f'"terminate simulation" executed on line {self.line}'

def applyTo(self, agent, simulation):
assert False


class _EndScenarioAction(Action):
"""Special action indicating it is time to end the current scenario.
Only for internal use.
"""

def __init__(self, scenario, line):
self.scenario = scenario
self.line = line

def __str__(self):
return f'"terminate" executed on line {self.line}'

def applyTo(self, agent, simulation):
assert False
156 changes: 156 additions & 0 deletions src/scenic/core/dynamics/behaviors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
"""Behaviors and monitors."""

import functools
import inspect
import itertools
import sys
import warnings

from scenic.core.distributions import Samplable, toDistribution
import scenic.core.dynamics as dynamics
from scenic.core.errors import InvalidScenarioError
from scenic.core.type_support import CoercionFailure
from scenic.core.utils import alarm

from .invocables import Invocable
from .utils import StuckBehaviorWarning


class Behavior(Invocable, Samplable):
"""Dynamic behaviors of agents.
Behavior statements are translated into definitions of subclasses of this class.
"""

_noActionsMsg = (
'does not take any actions (perhaps you forgot to use "take" or "do"?)'
)

def __init_subclass__(cls):
if "__signature__" in cls.__dict__:
# We're unpickling a behavior; skip this step.
return

if cls.__module__ is not __name__:
import scenic.syntax.veneer as veneer

if veneer.currentScenario:
veneer.currentScenario._behaviors.append(cls)

target = cls.makeGenerator
target = functools.partial(target, 0, 0) # account for Scenic-inserted args
cls.__signature__ = inspect.signature(target)

def __init__(self, *args, **kwargs):
args = tuple(toDistribution(arg) for arg in args)
kwargs = {name: toDistribution(arg) for name, arg in kwargs.items()}

# Validate arguments to the behavior
sig = inspect.signature(self.makeGenerator)
sig.bind(None, *args, **kwargs) # raises TypeError on incompatible arguments
Samplable.__init__(self, itertools.chain(args, kwargs.values()))
Invocable.__init__(self, *args, **kwargs)

if not inspect.isgeneratorfunction(self.makeGenerator):
raise InvalidScenarioError(f"{self} {self._noActionsMsg}")

@classmethod
def _canCoerceType(cls, ty):
return issubclass(ty, cls) or ty in (type, type(None))

@classmethod
def _coerce(cls, thing):
if thing is None or isinstance(thing, cls):
return thing
elif issubclass(thing, cls):
return thing()
else:
raise CoercionFailure(f"expected type of behavior, got {thing}")

def sampleGiven(self, value):
args = (value[arg] for arg in self._args)
kwargs = {name: value[val] for name, val in self._kwargs.items()}
return type(self)(*args, **kwargs)

def _assignTo(self, agent):
if self._agent and agent is self._agent._dynamicProxy:
# Assigned again (e.g. by override) to same agent; do nothing.
return
if self._isRunning:
raise InvalidScenarioError(
f"tried to reuse behavior object {self} already assigned to {self._agent}"
)
self._start(agent)

def _start(self, agent):
super()._start()
self._agent = agent
self._runningIterator = self.makeGenerator(agent, *self._args, **self._kwargs)
self._checkAllPreconditions()

def _step(self):
import scenic.syntax.veneer as veneer

super()._step()
assert self._runningIterator

def alarmHandler(signum, frame):
if sys.gettrace():
return # skip the warning if we're in the debugger
warnings.warn(
f"the behavior {self} is taking a long time to take an action; "
"maybe you have an infinite loop with no take/wait statements?",
StuckBehaviorWarning,
)

timeout = dynamics.stuckBehaviorWarningTimeout
with veneer.executeInBehavior(self), alarm(timeout, alarmHandler):
try:
actions = self._runningIterator.send(None)
except StopIteration:
actions = () # behavior ended early
return actions

def _stop(self, reason=None):
super()._stop(reason)
self._agent = None
self._runningIterator = None

@property
def _isFinished(self):
return self._runningIterator is None

def _invokeInner(self, agent, subs):
import scenic.syntax.veneer as veneer

assert len(subs) == 1
sub = subs[0]
if not isinstance(sub, Behavior):
raise TypeError(f"expected a behavior, got {sub}")
sub._start(agent)
with veneer.executeInBehavior(sub):
try:
yield from sub._runningIterator
finally:
if sub._isRunning:
sub._stop()

def __repr__(self):
items = itertools.chain(
(repr(arg) for arg in self._args),
(f"{key}={repr(val)}" for key, val in self._kwargs.items()),
)
allArgs = ", ".join(items)
return f"{self.__class__.__name__}({allArgs})"


class Monitor(Behavior):
"""Monitors for dynamic simulations.
Monitor statements are translated into definitions of subclasses of this class.
"""

_noActionsMsg = 'does not take any actions (perhaps you forgot to use "wait"?)'

def _start(self):
return super()._start(None)
42 changes: 42 additions & 0 deletions src/scenic/core/dynamics/guards.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""Preconditions and invariants of behaviors and scenarios."""


class GuardViolation(Exception):
"""Abstract exception raised when a guard of a behavior is violated.
This will never be raised directly; either of the subclasses `PreconditionViolation`
or `InvariantViolation` will be used, as appropriate.
"""

violationType = "guard"

def __init__(self, behavior, lineno):
self.behaviorName = behavior.__class__.__name__
self.lineno = lineno

def __str__(self):
return (
f"violated {self.violationType} of {self.behaviorName} on line {self.lineno}"
)


class PreconditionViolation(GuardViolation):
"""Exception raised when a precondition is violated.
Raised when a precondition is violated when invoking a behavior
or when a precondition encounters a `RejectionException`, so that
rejections count as precondition violations.
"""

violationType = "precondition"


class InvariantViolation(GuardViolation):
"""Exception raised when an invariant is violated.
Raised when an invariant is violated when invoking/resuming a behavior
or when an invariant encounters a `RejectionException`, so that
rejections count as invariant violations.
"""

violationType = "invariant"
Loading

0 comments on commit 3af563d

Please sign in to comment.