diff --git a/experiments/generate_missions.py b/experiments/generate_missions.py index 5a2e3f6c..098449a5 100755 --- a/experiments/generate_missions.py +++ b/experiments/generate_missions.py @@ -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) diff --git a/experiments/generate_missions_templatebased.py b/experiments/generate_missions_templatebased.py new file mode 100755 index 00000000..174b350c --- /dev/null +++ b/experiments/generate_missions_templatebased.py @@ -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) diff --git a/houston/ardu/command_factory.py b/houston/ardu/command_factory.py index 6e549211..ef978c45 100644 --- a/houston/ardu/command_factory.py +++ b/houston/ardu/command_factory.py @@ -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 diff --git a/houston/command.py b/houston/command.py index 6b97a049..fe5443d3 100644 --- a/houston/command.py +++ b/houston/command.py @@ -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): diff --git a/houston/generator/base.py b/houston/generator/base.py index b7a452bb..80fd01d2 100644 --- a/houston/generator/base.py +++ b/houston/generator/base.py @@ -29,7 +29,7 @@ def __iter__(self): """ return self - def __next__(self): + def __next__(self, **kwargs): """ Requests the next mission from the mission generator. """ @@ -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 @@ -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 @@ -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") diff --git a/houston/generator/template_based.py b/houston/generator/template_based.py new file mode 100644 index 00000000..e884c868 --- /dev/null +++ b/houston/generator/template_based.py @@ -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: + `()^` + 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[a-zA-Z\.\_]+)(?P\(.*\))?(?P\^[\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)