示例#1
0
    def post(self):
        logger.info("GenerateConstraints post")
        iterations = int(self.get_argument('iterations'))
        generator_type = self.get_argument('generator')
        if not generator_type in ['planes', 'rectangles', 'quads']:
            return self._json_error("Invalid generator set", 400)

        result = self._getResultData(self._get_session('current_result', None))
        if result is None:
            return self._json_error("Unable to load result data", 500)

        smt2interface, generator = self.make_gen(generator_type)
        new_samples, unsat = yield self.executor.submit(
            self.analyze, smt2interface, generator, iterations)

        if len(new_samples) == 0 and len(unsat) == 0:
            return self._json_error("SMT solver did not return an answer")

        raise NotImplementedError()
        samples = self._get_session('samples',
                                    InstantiationResultDict())  # FIXME
        # Clear all regions, resumption not supported (yet)
        constraints = []  #self._get_session('regions', [])

        samples.update(new_samples)
        constraints += unsat

        self._set_session('samples', samples)
        self._set_session('regions', constraints)

        return self._json_ok({
            'sat': _jsonSamples(new_samples),
            'unsat': unsat
        })
示例#2
0
    def post(self):
        logger.debug("Samples post request")
        #print(self.request.body)
        sampling_information = json_decode(self.request.body)
        #print(sampling_information)
        coordinates = sampling_information

        # Get the current prism file and save it temporarily
        prism_files = self._get_session("prism-files", {})
        #assert sampling_information["prism_file"] in prism_files
        #prism_file = PrismFile(prism_files[sampling_information["prism_file"]])
        if coordinates is None:
            return self._json_error("Unable to read coordinates", 400)
        result = self._getResultData(self._get_session('current_result', None))
        samples = self._get_session(
            'samples', InstantiationResultDict(parameters=result.parameters))
        socket = self._get_socket()
        sampling_interface = getSampler(self._get_session('sampler'), result)
        coordinates = [Point(Rational(x), Rational(y)) for x, y in coordinates]
        sample_points = result.parameters.instantiate(coordinates)
        new_samples = sampling_interface.perform_sampling(sample_points)
        if socket is not None:
            socket.send_samples(new_samples)

        samples.update(new_samples)
        self._set_session('samples', samples)

        return self._json_ok(_jsonSamples(new_samples))
示例#3
0
 def get(self):
     logger.debug("Samples get request")
     result = self._getResultData(self._get_session('current_result', None))
     flattenedsamples = _jsonSamples(
         self._get_session(
             'samples',
             InstantiationResultDict(parameters=result.parameters)))
     return self._json_ok(flattenedsamples)
 def __init__(self, sampler, parameters, region, samples=None):
     """
     @param sampler Sampler used to generate new samples
     @param parameters VariableOrder
     @param samples SampleDict pre-existing samples, which is copied.
         None is allowed
     """
     self.sampler = sampler
     self.parameters = parameters
     self.region = region
     self.samples = samples.copy() if samples else InstantiationResultDict(
         parameters=parameters)
     assert prophesy.config is not None
     self.distance = prophesy.config.configuration.get_sampling_min_distance(
     )
示例#5
0
 def perform_sampling(self, sample_points, surely_welldefined=False):
     if not surely_welldefined:
         logger.warning(
             "Sampling assumes (without any checks) that the point is welldefined"
         )
     # Perform sampling with model instantiator
     logger.debug("Call stormpy for sampling")
     parameter_mapping = self.get_parameter_mapping(
         sample_points[0].get_parameters())
     samples = InstantiationResultDict({
         p: self.sample_single_point(p, parameter_mapping)
         for p in sample_points
     })
     logger.debug("Sampling with stormpy successfully finished")
     return samples
示例#6
0
    def analyze(self, smt2interface, generator, iterations=-1):
        if iterations == 0:
            return ({}, [])

        socket = self._get_socket()

        #smt2interface.run()

        unsat = []
        result = self._getResultData(self._get_session('current_result', None))

        new_samples = InstantiationResultDict(parameters=result.parameters)
        for result in generator:
            (check_result, data) = result
            if check_result is RegionCheckResult.Satisfied:
                (poly, safe) = data
                unsat.append((_jsonPoly(poly), bool(safe)))
                if socket is not None:
                    socket.send_constraints([unsat[-1]])
            elif check_result is RegionCheckResult.CounterExample:
                (sample, safe) = data
                new_samples[sample.pt] = sample.val
                if socket is not None:
                    socket.send_samples({sample.pt: sample.val})
            else:
                assert check_result is RegionCheckResult.Unknown
                print("Check result 'unknown' not considered.")
                #TODO refine
                pass

            if self._check_canceled():
                break

            iterations -= 1
            if iterations == 0:
                break

        smt2interface.stop()

        return (new_samples, unsat)
示例#7
0
    def post(self):
        #request = json_decode(self.request.body)
        #safe = bool(request['safe'])
        #coordinates = request['coordinates']

        safe = self.get_argument('constr-mode') == "safe"
        coordinates = json_decode(self.get_argument('coordinates'))
        if coordinates is None:
            return self._json_error("Unable to read coordinates", 400)
        coordinates = [(float(x), float(y)) for x, y in coordinates]
        if coordinates[0] == coordinates[-1]:
            # Strip connecting point if any
            coordinates = coordinates[:-1]

        smt2interface, generator = self.make_gen("poly")
        generator.add_polygon(Polygon(coordinates), safe)
        new_samples, unsat = yield self.executor.submit(
            self.analyze, smt2interface, generator)

        if len(new_samples) == 0 and len(unsat) == 0:
            return self._json_error("SMT solver did not return an answer")

        result = self._getResultData(self._get_session('current_result', None))
        if result is None:
            return self._json_error("Unable to load result data", 500)
        samples = self._get_session(
            'samples', InstantiationResultDict(parameters=result.parameters))
        constraints = self._get_session('regions', [])

        samples.update(new_samples)
        constraints += unsat

        self._set_session('samples', samples)
        self._set_session('regions', constraints)

        return self._json_ok({
            'sat': _jsonSamples(new_samples),
            'unsat': unsat
        })
示例#8
0
    def make_gen(self, type):
        result = self._getResultData(self._get_session('current_result', None))
        if result is None:
            return self._json_error("Unable to load result data", 500)

        samples = self._get_session(
            'samples', InstantiationResultDict(parameters=result.parameters))
        threshold = self._get_session('threshold', Rational("1/2"))

        smt2interface = getSat(self._get_session('sat'))
        smt2interface.run()
        problem_description = ProblemDescription()
        problem_description.solution_function = result.ratfunc
        problem_description.parameters = result.parameters
        problem_description.threshold = threshold

        checker = SolutionFunctionRegionChecker(smt2interface)
        checker.initialize(problem_description)

        if type == 'planes':
            return self._json_error("Planes generator was dropped in v2")
        elif type == 'rectangles':
            return self._json_error(
                "Rectangles generator was temporarily dropped in v2")
        elif type == 'quads':
            generator = HyperRectangleRegions(
                samples, result.parameters, threshold, checker,
                problem_description.welldefined_constraints,
                problem_description.graph_preserving_constraints)
        elif type == 'poly':
            raise NotImplementedError(
                "We do no longer support arbitrary polygons.")
        # generator = ConstraintPolygon(samples, result.parameters, threshold, checker, problem_description.welldefined_constraints, problem_description.graph_preserving_constraints)
        else:
            return self._json_error("Bad generator")
        generator.plot = False

        return (smt2interface, generator)
示例#9
0
    def post(self):
        logger.debug("GenerateSamples post request")
        iterations = int(self.get_argument('iterations'))

        if iterations < 0:
            return self._json_error("Number of iterations must be >= 0", 400)
        threshold = self._get_session('threshold', 0.5)
        threshold = Rational(threshold)
        #TODO make sure threshold is a rational all the time.
        generator_type = self.get_argument('generator')
        if not generator_type in ['uniform', 'linear', 'delaunay']:
            return self._json_error("Invalid generator set " + generator_type,
                                    400)

        if generator_type == 'uniform' and iterations < 2:
            return self._json_error(
                "Number of iterations must be >= 2 for uniform generation",
                400)

        if iterations == 0:
            # Nothing to do
            return self._json_ok(_jsonSamples({}))

        result = self._getResultData(self._get_session('current_result', None))
        if result is None:
            return self._json_error("Unable to load result data", 500)

        socket = self._get_socket()

        parameters = result.parameters

        samples = self._get_session(
            'samples', InstantiationResultDict(parameters=parameters))
        new_samples = InstantiationResultDict(parameters=parameters)
        sampling_interface = getSampler(self._get_session('sampler'), result)
        if generator_type == 'uniform':
            intervals = result.parameters.get_parameter_bounds()
            samples_generator = UniformSampleGenerator(sampling_interface,
                                                       result.parameters,
                                                       samples, iterations)
        elif generator_type == "linear":
            samples_generator = LinearRefinement(sampling_interface,
                                                 result.parameters, samples,
                                                 threshold)
        elif generator_type == "delaunay":
            return self._json_error("Delaunay refinement was dropped in v2",
                                    400)
        else:
            assert False, "Bad generator"

        def generate_samples(samples_generator, iterations):
            for (generated_samples, _) in zip(samples_generator,
                                              range(0, iterations)):
                new_samples.update(generated_samples)
                if socket is not None:
                    socket.send_samples(generated_samples)
                if self._check_canceled():
                    break

            return new_samples

        new_samples = yield self.executor.submit(generate_samples,
                                                 samples_generator, iterations)

        samples.update(new_samples)
        self._set_session('samples', samples)
        return self._json_ok(_jsonSamples(new_samples))
示例#10
0
 def delete(self):
     raise NotImplementedError()
     self._set_session("samples", InstantiationResultDict())  # FIXME
     return self._json_ok()
示例#11
0
def read_samples_file(path, parameters):
    """
    Reads sample files.

    The first line specifies the parameters (with an optional "Result" for the last column).
    The second line optionally specifies a threshold. This is important if we have binary samples,
    (for which we do not know the value, but just whether it is above or below the threshold).
    The remaining lines give the parameter values and the value. This value is either a number or
    "above" or "below".

    :param path:
    :return:
    """
    threshold = None
    with open(path, 'r') as f:
        lines = [l.strip() for l in f.readlines()]
        if len(lines) <= 2:
            raise RuntimeError("Samples file is empty or malformed")

        # read first line with variable names
        parameter_names = lines[0].split()
        if parameter_names[-1] == "Result":
            parameter_names = parameter_names[:-1]
        start = 1

        for par_name, par in zip(parameter_names, parameters):
            if par_name != par.name:
                raise ValueError(
                    "Parameter names {} do not coincide with given parameters {}"
                    .format(parameter_names, parameters))

        #Ignore thresholds
        if lines[1].startswith("Threshold"):
            if len(lines[1].split()) != 2:
                raise IOError("Invalid input on line 2")
            threshold = Rational(lines[1].split()[1])
            start += 1

        samples = InstantiationResultDict(parameters=parameters)
        skip_next = False
        for i, line in enumerate(lines[start:]):
            if skip_next:
                skip_next = False
                continue
            items = line.split()
            if len(items) - 1 != len(parameter_names):
                # Prism reports that probs are negative:
                if line.find("are negative") > 0:
                    coords = map(Rational, items[:len(parameter_names)])
                    samples[ParameterInstantiation.from_point(
                        Point(*coords),
                        parameters)] = InstantiationResultFlag.NOT_WELLDEFINED
                    skip_next = True
                    continue
                logger.error("Invalid input in %s on line %s: '%s'", path,
                             str(i + start), line)
                continue
            if items[-1] == "below":
                #TODO
                raise NotImplementedError(
                    "Inexact sampling results are not yet supported in v2")
                #value = SAMPLE_BELOW
            elif items[-1] == "above":
                #TODO
                raise NotImplementedError(
                    "Inexact sampling results are not yet supported in v2")
            elif items[-1] == "InstantiationResultFlag.NOT_WELLDEFINED":
                value = InstantiationResultFlag.NOT_WELLDEFINED
            else:
                value = Rational(items[-1])
            coords = map(Rational, items[:-1])
            samples[ParameterInstantiation.from_point(Point(*coords),
                                                      parameters)] = value

    logger.debug("Parameters: %s", str(parameters))
    return parameters, threshold, samples
示例#12
0
    def perform_sampling(self, sample_points, surely_welldefined=False):
        logger.info("Perform batch sampling")
        if self.pctlformula is None:
            raise NotEnoughInformationError("pctl formula missing")
        if not self._has_model_set():
            raise NotEnoughInformationError("model missing")

        # create a temporary file for the result.
        ensure_dir_exists(prophesy.config.configuration.get_intermediate_dir())

        def sample_single_point(parameter_instantiation):
            fd, resultfile = tempfile.mkstemp(
                suffix=".txt",
                dir=prophesy.config.configuration.get_intermediate_dir(),
                text=True)
            os.close(fd)

            const_values_string = ",".join([
                "{}={}".format(parameter.name, val)
                for parameter, val in parameter_instantiation.items()
            ])
            constants_string = self.constants.to_key_value_string(
                to_float=False) if self.constants else ""
            if constants_string != "":
                const_values_string = const_values_string + "," + constants_string

            args = [
                self.
                main_location,  # Parametric DRN not supported with main version.
                '--prop',
                str(self.pctlformula),
                "-const",
                const_values_string
            ]
            if self.drnfile:
                args += ['-drn', self.drnfile.location]
            elif self.prismfile:
                args += ['--prism', self.prismfile.location]
                if self.prismfile.model_type == ModelType.CTMC:
                    args += ['-pc']
            if self.bisimulation == BisimulationType.strong:
                args.append('--bisimulation')

            logger.info("Call storm")
            ret_code = run_tool(args, quiet=False, outputfile=resultfile)
            if ret_code != 0:
                logger.debug("Storm output logged in %s", resultfile)
                # Do not crash here
            else:
                logger.info("Storm call finished successfully")
                logger.debug("Storm output logged in %s", resultfile)

            result = None
            with open(resultfile) as f:
                result_in_next_line = False
                for line in f:
                    if result_in_next_line:
                        result = pc.Rational(line)
                        break
                    if "Substitution yielding negative" in line:
                        result = InstantiationResultFlag.NOT_WELLDEFINED
                        ret_code = 0
                        break
                    match = re.search(r"Result (.*):(.*)", line)
                    if match:
                        # Check for exact result
                        match_exact = re.search(r"(.*) \(approx. .*\)",
                                                match.group(2))
                        if match_exact:
                            result = pc.Rational(match_exact.group(1))
                            break
                        else:
                            if match.group(2).strip() == "":
                                result_in_next_line = True
                                continue
                            result = pc.Rational(match.group(2))
                            break
            if ret_code != 0:
                raise RuntimeError("Storm crashed.")
            if result is None:
                raise RuntimeError(
                    "Could not find result from storm in {}".format(
                        resultfile))

            os.remove(resultfile)
            return result

        samples = InstantiationResultDict(
            {p: sample_single_point(p)
             for p in sample_points})

        return samples
    def _analyse_region(self, region, welldefined, safe, check_for_eq = False):
        """
        Analyse the given region.
        :param region: Region.
        :param welldefined: Flag iff the region is welldefined.
        :param safe: Flag iff the region should be considered safe.
        :return: Tuple (RegionCheckResult, (region/counterexample, safe))
        """
        logger.debug("Analyse region %s", region)
        if welldefined == WelldefinednessResult.Illdefined:
            self.ignore_region()
            self.record_illdefined(region)
            return WelldefinednessResult.Illdefined, region

        assert welldefined == WelldefinednessResult.Welldefined
        checkresult, additional = self.checker.analyse_region(region, safe, check_for_eq)
        if checkresult == RegionCheckResult.Satisfied:
            # remove unnecessary samples which are covered already by regions
            # TODO region might contain this info, why not use that.
            self.safe_samples = InstantiationResultDict(
                {k: v for k, v in self.safe_samples.items() if not region.contains(k.get_point(self.parameters))},
                parameters=self.parameters)
            self.bad_samples = InstantiationResultDict(
                {k: v for k, v in self.bad_samples.items() if not region.contains(k.get_point(self.parameters))},
                parameters=self.parameters)

            # update everything
            self.accept_region()
            self.record_accepted(region, safe)
            return checkresult, (region, safe)
        elif checkresult == RegionCheckResult.CounterExample:
            # add new point as counter example to existing regions

            self.reject_region(additional)
            self.record_cex(region, safe, additional)
            return checkresult, (additional, safe)
        elif checkresult == RegionCheckResult.Splitted:
            safe_regions, bad_regions, remaining_regions = additional
            self.record_results(safe_regions, bad_regions)
            self.refine_region(remaining_regions)
            return checkresult, (region, safe) #TODO why do we need to return this?
        elif checkresult == RegionCheckResult.Refined:
            # We refined the existing region.
            # additional should contain the candidate for the counterexample.
            # compute setminus operation to get accepted constraints:

            #check  if additional actually is a list of hyperrects.
            if isinstance(additional, HyperRectangle) and isinstance(region, HyperRectangle):
                accepted_regions = region.setminus(additional) # calculate the accepted regions
                self.refine_region([additional])
                self.record_accepted(accepted_regions, safe)

            return checkresult, (accepted_regions, safe)
        elif checkresult == RegionCheckResult.Homogenous:
            logger.warning("Still have to check that there is at least one sample here.")
            self.safe_samples = InstantiationResultDict(
                {k: v for k, v in self.safe_samples.items() if not region.contains(k.get_point(self.parameters))},
                parameters=self.parameters)
            self.bad_samples = InstantiationResultDict(
                {k: v for k, v in self.bad_samples.items() if not region.contains(k.get_point(self.parameters))},
                parameters=self.parameters)

            self.accept_region()
            self.record_accepted(region, safe)
            return checkresult, (region, safe)
        elif checkresult == RegionCheckResult.Inhomogenous:
            if additional:
                self.reject_region(additional)
                self.record_border(region, additional)
            else:
                self.record_contains_border(region, safe)
                self.fail_region(homogeneity=True)
            return checkresult, (region, safe)
        else:
            self.fail_region(homogeneity=check_for_eq)
            self.record_unknown(region, safe)
            return RegionCheckResult.Unknown, (region, safe)
class RegionGenerator:
    """
    A generator for regions.
    This class acts as an iterable that generates new regions (or counterexamples),
    until the search space is exhausted (which possibly never happens).
    """
    __metaclass__ = ABCMeta

    def __init__(self, samples, parameters, region, threshold, checker, wd_constraints, gp_constraints, generate_plot=True, allow_homogeneity=False):
        """
        Constructor.
        :param samples: List of samples.
        :param parameters: Parameters of the model.
        :param threshold: Threshold.
        :param checker: Region checker.
        :param wd_constraints: Well-defined constraints.
        :param gp_constraints: Graph-preserving constraints.
        """
        self.safe_samples, self.bad_samples, self.illdefined_samples = samples.copy().split(threshold)
        self.parameters = parameters
        self.threshold = threshold

        self.max_area_sum = region.size()

        self.checker = checker

        # Stores all regions as triple ([constraint], polygon representation, bad/safe)
        self.all_polys = []
        self.safe_polys = []
        self.bad_polys = []
        self.illdefined_polys = []
        self.new_samples = {}
        self.wd_constraints = wd_constraints
        self.gp_constraints = gp_constraints

        self._records = []
        self._iteration_timer = None

        # Options for plotting.
        self._plot_candidates = False
        self.plot = generate_plot
        self.plot_source_dir = None
        self._source_index = 1
        if generate_plot and len(self.parameters) > 2:
            logger.warning("Plotting for more than two dimensions not supported")
            self.plot = False
        self.first_pdf = True
        from prophesy.config import configuration
        if self.plot:
            ensure_dir_exists(configuration.get_plots_dir())
            _, self.result_file = tempfile.mkstemp(suffix=".pdf", prefix="result_",
                                               dir=configuration.get_plots_dir())
        self.allow_homogenous_check = allow_homogeneity

    def __iter__(self):
        # Prime the generator
        return next(self)

    def __next__(self):
        self.start_iteration()
        self.start_generation()
        region_info = self.next_region()
        self.stop_generation()
        while region_info is not None:
            polygon, welldefined, area_safe, check_for_eq = region_info
            if self._plot_candidates:
                self.plot_candidate()
            self.start_analysis()
            res = self._analyse_region(polygon, welldefined, area_safe, check_for_eq)
            self.stop_analysis()
            self.stop_iteration()
            yield res
            self.start_iteration()
            # get next constraint depending on algorithm
            self.start_generation()
            region_info = self.next_region()
            self.stop_generation()
        # Remove last record as there is no next region
        self._records.pop(-1)

    def _add_pdf(self, name):
        """
        Add PDF with name to result.pdf in tmp directory.
        """
        from prophesy.config import modules
        if not modules.is_module_available("pypdf2"):
            logging.warning("Module 'PyPDF2' is not available. PDF export is not supported.")
            return

        # Load module as it is available
        from PyPDF2 import PdfFileMerger, PdfFileReader

        if self.first_pdf:
            self.first_pdf = False
            shutil.copyfile(name, self.result_file)
            logger.debug("Plot file located at {0}".format(self.result_file))
        else:
            merger = PdfFileMerger()
            merger.append(PdfFileReader(self.result_file, 'rb'))
            merger.append(PdfFileReader(name, 'rb'))
            merger.write(self.result_file)

    @abstractmethod
    def plot_candidate(self):
        """
        Plot the current candidate.
        """
        raise NotImplementedError("Abstract parent method")

    def plot_results(self, *args, **kwargs):
        """
        Plot results.
        :param args: Arguments.
        :param kwargs: Arguments.
        """

        if not self.plot:
            return


        from prophesy.output.plot import Plot
        from prophesy.config import configuration

        if self.plot_source_dir is None:
            self.plot_source_dir = tempfile.mkdtemp(suffix=None, prefix="src_", dir=configuration.get_plots_dir())


        # Extend arguments
        poly_green = kwargs.get('poly_green', [])
        kwargs['poly_green'] = poly_green + self.safe_polys
        poly_red = kwargs.get('poly_red', [])
        kwargs['poly_red'] = poly_red + self.bad_polys
        poly_black = kwargs.get('poly_black', [])
        kwargs['poly_black'] = poly_black + self.illdefined_polys

        # Split samples appropriately
        samples_green = [instantiation.get_point(self.parameters) for instantiation in self.safe_samples.keys()]
        samples_red = [instantiation.get_point(self.parameters) for instantiation in self.bad_samples.keys()]
        samples_black = [instantiation.get_point(self.parameters) for instantiation in self.illdefined_samples.keys()]


        _, result_tmp_file = tempfile.mkstemp(".pdf", dir=configuration.get_plots_dir())
        _, result_src_file = tempfile.mkstemp(".pgf", prefix="{:03d}_".format(self._source_index), dir=self.plot_source_dir)
        Plot.plot_results(parameters=self.parameters,
                          samples_green=samples_green,
                          samples_red=samples_red,
                          samples_black=samples_black,
                          path_to_pdf=result_tmp_file,
                          path_to_src=result_src_file,
                          *args, **kwargs)
        self._source_index += 1
        self._add_pdf(result_tmp_file)
        os.unlink(result_tmp_file)

    def export_results(self, path):
        logger.debug("Write results to %s", path)
        with open(path, 'w+') as file:
            for idx, r in enumerate(self._records):
                for reg in r.safe_regions:
                    file.write("{}: {}\n".format(reg, "safe"))
                for reg in r.bad_regions:
                    file.write("{}: {}\n".format(reg, "unsafe"))

    @abstractmethod
    def next_region(self):
        """
        Generate a new region.
        :return Tuple (new region, well-definedness, safe/unsafe) or None if no next region exists.
        """
        raise NotImplementedError("Abstract parent method")

    @abstractmethod
    def fail_region(self, homogeneity=False):
        """
        Called after a region could not be checked, usually due to memout or timeout.
        Updates the current set of regions.
        """
        raise NotImplementedError("Abstract parent method")

    @abstractmethod
    def reject_region(self, sample):
        """
        Called after a region is rejected (sample found).
        :param sample: Sample acting as a counterexample for the constraint.
        """
        raise NotImplementedError("Abstract parent method")

    @abstractmethod
    def ignore_region(self):
        """
        Called for a region which is overall ill-defined. Skip it.
        """
        raise NotImplementedError("Abstract parent method")

    @abstractmethod
    def accept_region(self):
        """
        Called after a region is accepted.
        """
        raise NotImplementedError("Abstract parent method")

    def record_results(self, safe_regions, bad_regions):
        logger.info("Partial results for region.")

        if not isinstance(safe_regions, list):
            safe_regions = [safe_regions]
        if not isinstance(bad_regions, list):
            safe_regions = [bad_regions]

        for r in safe_regions:
            self.all_polys.append((r, RegionCheckResult.Satisfied))
            self.safe_polys.append(r)
        for r in bad_regions:
            self.all_polys.append((r, RegionCheckResult.Satisfied))
            self.bad_polys.append(r)
        self._records[-1].set_regions(safe_regions, bad_regions)
        self._records[-1].set_result(RegionCheckResult.Satisfied)

    def record_accepted(self, region, safe):
        """
        Record the accepted region.

        :return:
        """
        logger.info("Region accepted")

        if not isinstance(region, list):
            region = [region]

        for r in region:
            self.all_polys.append((r, RegionCheckResult.Satisfied))
            if safe:
                self.safe_polys.append(r)
            else:
                self.bad_polys.append(r)
        self._records[-1].set_region(region, safe)
        self._records[-1].set_result(RegionCheckResult.Satisfied)

    def record_cex(self, region, safe, additional):
        """
        :param additional: An additional sample.
        :return:
        """
        logger.info("Counterexample found")
        if additional.result >= self.threshold:
            self.safe_samples[additional.instantiation] = additional.result
        else:
            self.bad_samples[additional.instantiation] = additional.result

        self._records[-1].set_region(region, safe)
        self._records[-1].set_result(RegionCheckResult.CounterExample)

    def record_border(self, region, additional):
        logger.info("Border found")
        self.border_samples[additional.instantiation] = additional.result
        self._records[-1].set_region(region, None)
        self._records[-1].set_result(RegionCheckResult.CounterExample)

    def record_contains_border(self, region, safe):
        logger.info("Contains border (no particular point found)")
        self._records[-1].set_region(region, safe)
        self._records[-1].set_result(RegionCheckResult.Unknown)

    def record_unknown(self, region, safe):
        logger.info("No result found")
        self._records[-1].set_region(region, safe)
        self._records[-1].set_result(RegionCheckResult.Unknown)

    def record_illdefined(self, region):
        logger.info("Region is illdefined")
        self.all_polys.append((region, WelldefinednessResult.Illdefined))
        self.illdefined_polys.append(region)

        self._records[-1].set_result(WelldefinednessResult.Illdefined)

    def start_iteration(self):
        logger.info("Start next iteration")

        self._records.append(GenerationRecord())
        self._records[-1].start_iteration_timer()

    def start_analysis(self):
        self._records[-1].start_analysis_timer()

    def stop_analysis(self):
        self._records[-1].stop_analysis_timer()

    def start_generation(self):
        self._records[-1].start_generation_timer()

    def stop_generation(self):
        self._records[-1].stop_generation_timer()

    def stop_iteration(self):
        self._records[-1].stop_iteration_timer()
        logger.debug("Done with iteration: took %s", str(self._records[-1].iteration_time))

    def generate_constraints(self, max_iter=-1, max_area=1, plot_every_n=1, plot_candidates=True,
                             export_statistics=None):
        """
        Iteratively generate new regions, heuristically, attempting to find the largest safe or unsafe area.
        :param max_iter: Number of regions to generate/check at most (not counting SMT failures), -1 for unbounded
        :param max_area: Maximal area percentage that should be covered.
        :param plot_every_n: How often should the plot be appended to the PDF.
        :param plot_candidates: True, iff candidates should be plotted.
        :return Tuple (safe regions, unsafe regions, samples)
        """
        if max_iter == 0:
            return self.safe_polys, self.bad_polys, self.new_samples

        self._plot_candidates = plot_candidates

        for result, additional_info in self:

            if export_statistics:
                self.export_stats(export_statistics, update=True)

            max_iter -= 1
            # Check termination criteria
            area_sum = sum(poly.size() for poly, safe in self.all_polys)
            logger.debug("Current area is {}, remaining number of iterations is {}".format(area_sum, max_iter))

            if area_sum >= max_area * self.max_area_sum:

                break


            if max_iter == 0:
                break

            # Plot intermediate result
            if result != RegionCheckResult.Unknown:  # and len(self.all_polys) % plot_every_n == 0:
                self.plot_results(display=True)

        # Plot the final outcome
        if self.plot:
            self.plot_results(display=False)
            logger.info("Generation complete, plot located at {} and sources at {}".format(self.result_file, self.plot_source_dir))


        # Print results
        logger.info(self.generate_header())
        logger.info(self.generate_stats(update=True))

        return self.safe_polys, self.bad_polys, self.new_samples

    def generate_header(self):
        return "\t".join(
            ["N", "cons. area", "res", "safe", "gentime", "anatime", "ttime", "cov. area", "cov. safe area",
             "cumgentime", "cumanatime", "cumttime"])

    def generate_stats(self, update=False):
        stats = ""

        cov_area = 0.0
        safe_area = 0.0
        cumulative_generation_time = 0.0
        cumulative_analysis_time = 0.0
        cumulative_total_time = 0.0
        for idx, r in enumerate(self._records):
            cov_area += float(r.covered_area)
            safe_area += float(r.covered_safe_area)
            cumulative_generation_time += r.generation_time
            cumulative_analysis_time += r.analysis_time
            cumulative_total_time += r.iteration_time

            if not update or len(self._records) == idx + 1:
                stats += "{}\t{:.5f}\t\t{}\t{}\t{:.2f}\t{:.2f}\t{:.2f}\t{:.2f}\t\t{:.2f}\t\t{:.2f}\t\t{:.2f}\t\t{:.2f}\n".format(
                    idx, float(r.area), r.result, r.safe, r.generation_time, r.analysis_time, r.iteration_time,
                    cov_area, safe_area, cumulative_generation_time, cumulative_analysis_time, cumulative_total_time)
        return stats

    def export_stats(self, filename, update=False):
        logger.debug("Write stats to %s (update == %s)", filename, update)
        with open(filename, 'a') as file:
            if not update or len(self._records) == 1:
                file.write(self.generate_header() + "\n")
            file.write(self.generate_stats(update))

    def _analyse_region(self, region, welldefined, safe, check_for_eq = False):
        """
        Analyse the given region.
        :param region: Region.
        :param welldefined: Flag iff the region is welldefined.
        :param safe: Flag iff the region should be considered safe.
        :return: Tuple (RegionCheckResult, (region/counterexample, safe))
        """
        logger.debug("Analyse region %s", region)
        if welldefined == WelldefinednessResult.Illdefined:
            self.ignore_region()
            self.record_illdefined(region)
            return WelldefinednessResult.Illdefined, region

        assert welldefined == WelldefinednessResult.Welldefined
        checkresult, additional = self.checker.analyse_region(region, safe, check_for_eq)
        if checkresult == RegionCheckResult.Satisfied:
            # remove unnecessary samples which are covered already by regions
            # TODO region might contain this info, why not use that.
            self.safe_samples = InstantiationResultDict(
                {k: v for k, v in self.safe_samples.items() if not region.contains(k.get_point(self.parameters))},
                parameters=self.parameters)
            self.bad_samples = InstantiationResultDict(
                {k: v for k, v in self.bad_samples.items() if not region.contains(k.get_point(self.parameters))},
                parameters=self.parameters)

            # update everything
            self.accept_region()
            self.record_accepted(region, safe)
            return checkresult, (region, safe)
        elif checkresult == RegionCheckResult.CounterExample:
            # add new point as counter example to existing regions

            self.reject_region(additional)
            self.record_cex(region, safe, additional)
            return checkresult, (additional, safe)
        elif checkresult == RegionCheckResult.Splitted:
            safe_regions, bad_regions, remaining_regions = additional
            self.record_results(safe_regions, bad_regions)
            self.refine_region(remaining_regions)
            return checkresult, (region, safe) #TODO why do we need to return this?
        elif checkresult == RegionCheckResult.Refined:
            # We refined the existing region.
            # additional should contain the candidate for the counterexample.
            # compute setminus operation to get accepted constraints:

            #check  if additional actually is a list of hyperrects.
            if isinstance(additional, HyperRectangle) and isinstance(region, HyperRectangle):
                accepted_regions = region.setminus(additional) # calculate the accepted regions
                self.refine_region([additional])
                self.record_accepted(accepted_regions, safe)

            return checkresult, (accepted_regions, safe)
        elif checkresult == RegionCheckResult.Homogenous:
            logger.warning("Still have to check that there is at least one sample here.")
            self.safe_samples = InstantiationResultDict(
                {k: v for k, v in self.safe_samples.items() if not region.contains(k.get_point(self.parameters))},
                parameters=self.parameters)
            self.bad_samples = InstantiationResultDict(
                {k: v for k, v in self.bad_samples.items() if not region.contains(k.get_point(self.parameters))},
                parameters=self.parameters)

            self.accept_region()
            self.record_accepted(region, safe)
            return checkresult, (region, safe)
        elif checkresult == RegionCheckResult.Inhomogenous:
            if additional:
                self.reject_region(additional)
                self.record_border(region, additional)
            else:
                self.record_contains_border(region, safe)
                self.fail_region(homogeneity=True)
            return checkresult, (region, safe)
        else:
            self.fail_region(homogeneity=check_for_eq)
            self.record_unknown(region, safe)
            return RegionCheckResult.Unknown, (region, safe)