Skip to content
2 changes: 1 addition & 1 deletion experiments/generate_missions.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def generate(num_missions: int,
config = build_config(speedup)
mission_generator = RandomMissionGenerator(sut, initial, environment, config, max_num_commands=max_num_commands)
resource_limits = ResourceLimits(num_missions)
missions = mission_generator.generate(None, resource_limits)
missions = mission_generator.generate(seed, resource_limits)
with open(output_file, "w") as f:
mission_descriptions = list(map(Mission.to_dict, missions))
json.dump(mission_descriptions, f, indent=2)
Expand Down
115 changes: 115 additions & 0 deletions experiments/generate_missions_templatebased.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#!/usr/bin/env python3
import json
import yaml
import logging
import argparse
import random

from houston.mission import Mission
from houston.generator.template_based import TemplateBasedMissionGenerator
from houston.generator.resources import ResourceLimits

from settings import sut, initial, environment, build_config


logger = logging.getLogger("houston") # type: logging.Logger
logger.setLevel(logging.DEBUG)


def setup_logging(verbose: bool = False) -> None:
log_to_stdout = logging.StreamHandler()
log_to_stdout.setLevel(logging.DEBUG if verbose else logging.INFO)
logging.getLogger('houston').addHandler(log_to_stdout)


def parse_args():
parser = argparse.ArgumentParser(description='Generates missions')
parser.add_argument('number_of_mission', type=int, action='store',
help='number of missions to be generated.')
parser.add_argument('max_num_commands', type=int, action='store',
help='maximum number of commands in a single mission.')
parser.add_argument('-t', type=str, action='store',
help='template of the mission.')
parser.add_argument('-f', type=str, action='store',
help='yaml file of mutants nad their templates.')
parser.add_argument('--speedup', action='store', type=int,
default=1,
help='simulation speedup that should be used')
parser.add_argument('--seed', action='store', type=int,
default=1000,
help='random seed to be used by random generator.')
parser.add_argument('--output', action='store', type=str,
default='missions.json',
help='the file where the results will be stored')
parser.add_argument('--verbose', action='store_true',
default=False,
help='verbose logging.')
return parser.parse_args()


def generate(num_missions: int,
max_num_commands: int,
seed: int,
speedup: int,
template: str
) -> None:
config = build_config(speedup)
mission_generator = TemplateBasedMissionGenerator(sut, initial, environment, config, max_num_commands=max_num_commands)
resource_limits = ResourceLimits(num_missions)
missions = mission_generator.generate(seed, resource_limits,
template=template)
return missions



if __name__ == "__main__":
args = parse_args()
setup_logging(args.verbose)
random.seed(args.seed)
with open(args.output, "w") as f:
pass
if args.t:
missions = generate(num_missions=args.number_of_mission,
max_num_commands=args.max_num_commands,
seed=args.seed,
speedup=args.speedup,
template=args.t)
mission_descriptions = list(map(Mission.to_dict, missions))
elif args.f:
templates = []
with open(args.f, "r") as f:
mutants = yaml.load(f, Loader=yaml.FullLoader)
for m in mutants:
template = m.get('mission-template')
if not template:
continue
if isinstance(template, str):
templates.append((template, args.number_of_mission, m['uid']))
elif isinstance(template, list):
n_missions = args.number_of_mission
for i, t in enumerate(template):
if i == len(template)-1:
templates.append((t, n_missions, m['uid']))
else:
r = random.randint(0, n_missions)
templates.append((t, r, m['uid']))
n_missions -= r
logger.info("Number of templates: %d", len(templates))
logger.info("AAAA %s", templates)
mission_descriptions = []
for template, num, uid in templates:
missions = generate(num_missions=num,
max_num_commands=args.max_num_commands,
seed=args.seed,
speedup=args.speedup,
template=template)
for n in missions:
new_dict = n.to_dict()
new_dict['mutant'] = uid
mission_descriptions.append(new_dict)
else:
raise Exception("Provide either -t or -f")

logger.info("Total number of missions: %d", len(mission_descriptions))
with open(args.output, "w") as f:
json.dump(mission_descriptions, f, indent=2)
8 changes: 6 additions & 2 deletions houston/ardu/command_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,12 @@ def circle_based_generator(cls: Type[Command],
destination = dist.destination(origin, heading)

prob_zero = 0.1 # the probability of generating 0.0 as the parameter value
params['lat'] = destination.latitude if rng.random() >= prob_zero else 0.0
params['lon'] = destination.longitude if rng.random() >= prob_zero else 0.0
if rng.random() >= prob_zero:
params['lat'] = destination.latitude
params['lon'] = destination.longitude
else:
params['lat'] = 0.0
params['lon'] = 0.0

command = cls(**params)
return command
Expand Down
15 changes: 15 additions & 0 deletions houston/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,21 @@ def generate(cls, rng: random.Random) -> 'Command':
command = cls(**params)
return command

@classmethod
def generate_fixed_params(cls,
fixed_params: Dict[str, Any],
rng: random.Random
) -> 'Command':
new_cmd = cls.generate(rng)
params = {}
for p in new_cmd:
if p.name in fixed_params.keys():
params[p.name] = fixed_params[p.name]
else:
params[p.name] = new_cmd[p.name]
command = cls(**params)
return command


@attr.s(frozen=True)
class CommandOutcome(object):
Expand Down
9 changes: 5 additions & 4 deletions houston/generator/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def __iter__(self):
"""
return self

def __next__(self):
def __next__(self, **kwargs):
"""
Requests the next mission from the mission generator.
"""
Expand All @@ -38,7 +38,7 @@ def __next__(self):
g.tick()
if g.exhausted():
raise StopIteration
mission = self.__generator.generate_mission()
mission = self.__generator.generate_mission(**kwargs)
g.tick()
g.resource_usage.num_missions += 1
mission_num = g.resource_usage.num_missions
Expand Down Expand Up @@ -205,7 +205,8 @@ def record_outcome(self, mission, outcome, coverage=None):

def generate(self,
seed: int,
resource_limits: ResourceLimits
resource_limits: ResourceLimits,
**kwargs
) -> List[Mission]:
"""
Generate missions and return them
Expand All @@ -219,7 +220,7 @@ def generate(self,
self.tick()
# TODO use threads
while True:
mission = stream.__next__()
mission = stream.__next__(**kwargs)
missions.append(mission)
except StopIteration:
logger.info("Done with generating missions")
Expand Down
192 changes: 192 additions & 0 deletions houston/generator/template_based.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
from typing import Type, Dict, Callable, Optional, Any
import logging
import attr
import re

from .base import MissionGenerator
from ..mission import Mission
from ..system import System
from ..state import State
from ..environment import Environment
from ..configuration import Configuration
from ..command import Command
from ..exceptions import HoustonException

logger = logging.getLogger(__name__) # type: logging.Logger
logger.setLevel(logging.DEBUG)


class FailedMissionGenerationException(HoustonException):
"""
Thrown whenever the mission generation fails to follow the
template.
"""


@attr.s
class CommandTemplate:
"""
A CommandTemplate describes template for a set of commands.
"""

cmd = attr.ib(type=str)
repeats = attr.ib(type=int)
params = attr.ib(type=Optional[Dict[str, Any]], default=None)

@staticmethod
def from_str(template_str: str) -> 'CommandTemplate':
"""
Creates a CommandTemplate based on provided template
string. The template should take the following format:
`<COMMAND NAME>(<FIXED PARAMETERS>)^<NUMBER OF REPEATS>`
Example:
`TAKEOFF(alt: 10.3)^2` which means template is for
two `TAKEOFF` commands with parameter `alt` set to 10.3
Providing the parameters and number of repeats are
optional. By default, number of repeats is set to 1. If `*`
is provided as number of repeates, it will be dynamically
decided. If command name is provided as `.` it means any
possible command.
"""

regex = r"(?P<cmd>[a-zA-Z\.\_]+)(?P<params>\(.*\))?(?P<repeats>\^[\d\*]*)?" # noqa: pycodestyle
matched = re.fullmatch(regex, template_str.strip())
if not matched:
logger.error("Template is wrong %s", template_str)
return None
groups = matched.groupdict()
cmd = groups['cmd']
params = None
repeats = 1
if groups['params']:
pairs = groups['params'].strip()[1:-1].split(",")
params = {}
for pair in pairs:
splited = pair.split(":")
assert len(splited) == 2
params[splited[0].strip()] = eval(splited[1])
if groups['repeats']:
r = groups['repeats'][1:].strip()
if r == '*':
repeats = -1
else:
repeats = int(r)
return CommandTemplate(cmd, repeats, params)

def __str__(self):
s = "cmd: {}, params:{}, repeats: {}"
return s.format(self.cmd,
self.params,
self.repeats)


class TemplateBasedMissionGenerator(MissionGenerator):
"""
Given a template for intended missions, this generator
generates missions.
"""

def __init__(self,
system: Type[System],
initial_state: State,
env: Environment,
config: Configuration,
threads: int = 1,
command_generators: Optional[Dict[str, Callable]] = None,
max_num_commands: int = 10
) -> None:
super().__init__(system, threads, command_generators, max_num_commands)
self.__initial_state = initial_state
self.__env = env
self.__configuration = config

@property
def initial_state(self):
"""
The initial state used by all missions produced by this generator.
"""
return self.__initial_state

@property
def env(self):
"""
The environment used by all missions produced by this generator.
"""
return self.__env

def generate_command(self,
command_class: Type[Command],
params: Dict[str, Any]
) -> Command:
generator = self.command_generator(command_class)
if generator is None:
return command_class.generate_fixed_params(params,
self.rng)
# g = generator.generate_action_without_state
# return g(self.system, self.__env, self.rng)
return g(self.rng)

def generate_mission(self, template: str):
"""
A list of CommandTemplate strings separated by `-`
should be provided as template.
Example:
TAKEOFF(alt: 10.3)-.^*-LAND-.^1

raises:
FailedMissionGenerationException:
It failed to generate a mission for this
template.
"""
command_templates = [CommandTemplate.from_str(t)
for t in template.split("-")]
cmds_len = self.max_num_commands
cmds_len -= sum([c.repeats for c in command_templates
if c.repeats > 0])
command_classes = list(self.system.commands.values())
for tries in range(50):
# try at most 50 times to generate a mission
commands = []
max_nonfixed_commands = cmds_len
try:
for ct in command_templates:
r = ct.repeats
if r <= 0:
# number of repeats between 0 and max allowed
r = self.rng.randint(0, max_nonfixed_commands)
for _ in range(r):
if commands:
next_allowed = commands[-1].__class__.get_next_allowed(self.system) # noqa: pycodestyle
else:
# everything is allowed
next_allowed = [cc for cc in command_classes]

if ct.cmd != '.':
next_allowed = [cc for cc in next_allowed
if ct.cmd in cc.uid]

if not next_allowed:
if ct.repeats > 0:
# logger.debug("So far %s", commands)
commands = []
raise FailedMissionGenerationException
else:
break
params = ct.params or {}
command_class = self.rng.choice(next_allowed)
commands.append(self.generate_command(command_class,
params))
if ct.repeats <= 0:
max_nonfixed_commands -= 1
break
except FailedMissionGenerationException:
logger.debug("Try %d failed", tries)
continue
if not commands:
raise FailedMissionGenerationException("Mission generation failed")
logger.debug("Generated mission: %s", commands)
return Mission(self.__configuration,
self.__env,
self.__initial_state,
commands,
self.system)