├── .gitattributes
├── .dockerignore
├── hylaa_logo_small.png
├── examples
├── building
│ ├── build.mat
│ └── building.py
├── space_station
│ ├── iss.mat
│ └── space_station.py
├── gearbox
│ ├── spaceex
│ │ ├── SX_mesh.gif
│ │ └── SX_Mesh.cfg
│ └── gearbox.py
├── run_all_examples.py
├── harmonic_oscillator
│ └── ha.py
├── approx_model
│ └── approx_model.py
├── demo_inputs_reset
│ └── demo_inputs_reset.py
├── demo_reset
│ └── demo_reset.py
├── ha_deaggregation
│ └── ha_deagg.py
├── deaggregation
│ └── deag_example.py
├── drivetrain
│ └── drivetrain.py
├── rendezvous
│ ├── rendezvous_full_passivity.py
│ └── rendezvous.py
└── fuzzy_pd
│ └── fuzzy_pd.py
├── pyproject.toml
├── .gitignore
├── setup.cfg
├── hylaa
├── __init__.py
├── util.py
├── time_elapse.py
├── aggregate.py
├── simulation.py
├── time_elapse_expm.py
├── symbolic.py
├── settings.py
├── kamenev.py
├── timerutil.py
├── guard_opt_data.py
├── deaggregation.py
├── check_trace.py
├── result.py
├── aggstrat.py
└── lpplot.py
├── .travis.yml
├── Dockerfile
├── tests
├── util.py
├── test_deaggregation.py
└── test_misc.py
├── setup.py
├── .github
└── workflows
│ └── publish-package.yml
├── README.md
└── .pylintrc
/.gitattributes:
--------------------------------------------------------------------------------
1 | hylaa/_version.py export-subst
2 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | Dockerfile
2 | .git
3 | **/__pycache__
4 |
--------------------------------------------------------------------------------
/hylaa_logo_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stanleybak/hylaa/HEAD/hylaa_logo_small.png
--------------------------------------------------------------------------------
/examples/building/build.mat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stanleybak/hylaa/HEAD/examples/building/build.mat
--------------------------------------------------------------------------------
/examples/space_station/iss.mat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stanleybak/hylaa/HEAD/examples/space_station/iss.mat
--------------------------------------------------------------------------------
/examples/gearbox/spaceex/SX_mesh.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stanleybak/hylaa/HEAD/examples/gearbox/spaceex/SX_mesh.gif
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools>=61.0.0", "versioneer[toml]==0.28", "wheel"]
3 | build-backend = "setuptools.build_meta"
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.pyc
3 | .directory
4 | TAGS
5 | *.png
6 | *.gv.pdf
7 | *.so
8 | *.mp4
9 | *Debug/
10 | \#*\#
11 | .\#*
12 | gpu_flops
13 | matrixMulCUBLAS
14 | matrixMulCUBLAS.o
15 | test
16 | *_flymake.py
17 | .pytest_cache
18 | .cache
19 | __pycache__
20 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 |
2 | # See the docstring in versioneer.py for instructions. Note that you must
3 | # re-run 'versioneer.py setup' after changing this section, and commit the
4 | # resulting files.
5 |
6 | [versioneer]
7 | VCS = git
8 | style = pep440
9 | versionfile_source = hylaa/_version.py
10 | versionfile_build = hylaa/_version.py
11 | tag_prefix =
12 | parentdir_prefix =
13 |
--------------------------------------------------------------------------------
/hylaa/__init__.py:
--------------------------------------------------------------------------------
1 | '''This file defines which files to import when 'from hylaa import *' is used'''
2 |
3 | __all__ = []
4 |
5 | from . import _version
6 | __version__ = _version.get_versions()['version']
7 | __author__ = "Stanley Bak"
8 | __copyright__ = "Copyright 2023"
9 | __credits__ = [
10 | "Stanley Bak",
11 | "Hoang-Dung Tran",
12 | "Max von Hippel"
13 | ]
14 | __license__ = "GPLv3"
15 | __maintainer__ = "Stanley Bak"
16 | __email__ = "bak2007-DONTSENDMEEMAIL@gmail.com"
17 | __status__ = "Prototype"
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 |
2 | # configuration file for continuous integration testing using travis-ci.org
3 |
4 | sudo: required
5 |
6 | dist:
7 | - trusty
8 |
9 | services:
10 | - docker
11 |
12 | script:
13 | # build Docker container
14 | - docker build -t hylaa .
15 | # run simple example
16 | - docker run hylaa python3 /hylaa/examples/harmonic_oscillator/ha.py
17 | # run unit tests
18 | - docker run hylaa
19 | # run all examples
20 | - docker run hylaa python3 /hylaa/examples/run_all_examples.py
21 |
22 | # to get a shell: docker run -it hylaa bash
23 |
--------------------------------------------------------------------------------
/examples/gearbox/spaceex/SX_Mesh.cfg:
--------------------------------------------------------------------------------
1 | system = mesh
2 | initially = "vx==0 & vy==0 & px==-0.0165 & py==0.003 & I==0 & t==0 "
3 | forbidden = ""
4 | scenario = stc
5 | directions = oct
6 | set-aggregation = "none"
7 | sampling-time = 1
8 | flowpipe-tolerance = 0.0001
9 | flowpipe-tolerance-rel = 0
10 | simu-init-sampling-points = 0
11 | time-horizon = 0.1
12 | iter-max = -1
13 | output-variables = "t,px,py"
14 | output-format = GEN
15 | verbosity = m
16 | output-error = 0.00001
17 | rel-err = 1.0E-12
18 | abs-err = 1.0E-15
19 | ode-rel-tol = 1e-9
20 | ode-abs-tol = 1e-12
21 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Dockerfile for Hylaa v2
2 |
3 | FROM python:3.6
4 |
5 | # ffmpeg is only needed if you want video (.mp4) export
6 | RUN apt-get update && apt-get -qy install ffmpeg
7 |
8 | # install other (required) dependencies
9 | RUN pip3 install pytest numpy scipy sympy matplotlib termcolor swiglpk graphviz
10 |
11 | # set environment variable
12 | ENV PYTHONPATH=$PYTHONPATH:/hylaa
13 |
14 | # copy current directory to docker
15 | COPY . /hylaa
16 |
17 | ### As default command: run the tests ###
18 | CMD python3 -m pytest /hylaa/tests
19 |
20 | # USAGE:
21 | # Build container and name it 'hylaa':
22 | # docker build . -t hylaa
23 |
24 | # # run tests (default command)
25 | # docker run hylaa
26 |
27 | # # get a shell:
28 | # docker run -it hylaa bash
29 | # hylaa is available in /hylaa
30 | # to delete docker container use: docker rm hylaa
31 |
--------------------------------------------------------------------------------
/tests/util.py:
--------------------------------------------------------------------------------
1 | '''
2 | Utilities for testing
3 |
4 | Stanley Bak, 2018
5 | '''
6 |
7 | def pair_almost_in(pair, pair_list, tol=1e-9):
8 | 'check if a pair is in a pair list (up to small tolerance)'
9 |
10 | rv = False
11 |
12 | for a, b in pair_list:
13 | if abs(a - pair[0]) < tol and abs(b - pair[1]) < tol:
14 | rv = True
15 | break
16 |
17 | return rv
18 |
19 | def assert_verts_equals(verts, check_list, tol=1e-5):
20 | '''check that the two lists of vertices are the same'''
21 |
22 | for v in check_list:
23 | assert pair_almost_in(v, verts, tol), "{} was not found in verts: {}".format(v, verts)
24 |
25 | for v in verts:
26 | assert pair_almost_in(v, check_list, tol), "verts contains {}, which was not in check_list: {}".format(
27 | v, check_list)
28 |
29 | def assert_verts_is_box(verts, box, tol=1e-5):
30 | '''check that a list of verts is almost equal to the passed-in box using assertions
31 |
32 | box is [[xmin, xmax], [ymin, ymax]]
33 | '''
34 |
35 | pts = [(box[0][0], box[1][0]), (box[0][1], box[1][0]), (box[0][1], box[1][1]), (box[0][0], box[1][1])]
36 |
37 | assert_verts_equals(verts, pts, tol)
38 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # Based on:
2 | # https://betterscientificsoftware.github.io/
3 | # python-for-hpc/tutorials/python-pypi-packaging/
4 |
5 | from setuptools import setup
6 | import versioneer
7 |
8 | with open("README.md", "r", encoding="utf-8") as fh:
9 | long_description = fh.read()
10 |
11 | setup(
12 | name="hylaa",
13 | version=versioneer.get_version(),
14 | cmdclass=versioneer.get_cmdclass(),
15 | description="Hylaa",
16 | long_description=long_description,
17 | long_description_content_type="text/markdown",
18 | url="https://github.com/stanleybak/hylaa",
19 | author="Stanley Bak",
20 | author_email="bak2007-DONTSENDMEEMAIL@gmail.com",
21 | license="GPLv3",
22 | packages=["hylaa"],
23 | install_requires=[
24 | "ffmpeg-python",
25 | "pytest",
26 | "numpy",
27 | "scipy",
28 | "sympy",
29 | "matplotlib",
30 | "termcolor",
31 | "swiglpk",
32 | "graphviz",
33 | ],
34 | classifiers=[
35 | "Programming Language :: Python :: 3",
36 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
37 | "Operating System :: OS Independent",
38 | "Topic :: Scientific/Engineering :: Artificial Intelligence",
39 | "Intended Audience :: Science/Research",
40 | "Topic :: Scientific/Engineering"
41 | ],
42 | )
43 |
--------------------------------------------------------------------------------
/hylaa/util.py:
--------------------------------------------------------------------------------
1 | '''
2 | General python utilities, which aren't necessary specific to Hylaa's objects.
3 |
4 | Methods / Classes in this one shouldn't require non-standard imports.
5 | '''
6 |
7 | import os
8 | import sys
9 |
10 | class Freezable():
11 | 'a class where you can freeze the fields (prevent new fields from being created)'
12 |
13 | _frozen = False
14 |
15 | def freeze_attrs(self):
16 | 'prevents any new attributes from being created in the object'
17 | self._frozen = True
18 |
19 | def __setattr__(self, key, value):
20 | if self._frozen and not hasattr(self, key):
21 | raise TypeError("{} does not contain attribute '{}' (object was frozen)".format(self, key))
22 |
23 | object.__setattr__(self, key, value)
24 |
25 | def get_script_path(filename):
26 | '''get the path this script, pass in __file__ for the filename'''
27 | return os.path.dirname(os.path.realpath(filename))
28 |
29 | def matrix_to_string(m):
30 | 'get a matrix as a string'
31 |
32 | return "\n".join([", ".join([str(val) for val in row]) for row in m])
33 |
34 | DID_PYTHON3_CHECK = False
35 |
36 | if not DID_PYTHON3_CHECK:
37 | # check that we're using python 3
38 |
39 | if sys.version_info < (3, 0):
40 | sys.stdout.write("Hylaa requires Python 3, but was run with Python {}.{}.\n".format(
41 | sys.version_info[0], sys.version_info[1]))
42 | sys.exit(1)
43 |
--------------------------------------------------------------------------------
/.github/workflows/publish-package.yml:
--------------------------------------------------------------------------------
1 | name: Publish Hylaa
2 |
3 | on: [push]
4 |
5 | jobs:
6 | publish-whl:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | max-parallel: 5
10 |
11 | steps:
12 | - uses: actions/checkout@v2
13 | - name: Set up Python 3.9
14 | uses: actions/setup-python@v2
15 | with:
16 | python-version: 3.9
17 | - name: Create Virtual Python Env
18 | run: |
19 | python -m venv env
20 | source env/bin/activate
21 | echo "VIRTUAL ENV:" $VIRTUAL_ENV
22 | - name: Prepare build requirements
23 | run: >-
24 | source env/bin/activate &&
25 | python -m
26 | pip install
27 | build
28 | - name: Build a binary wheel and a source tarball
29 | run: >-
30 | source env/bin/activate &&
31 | python -m
32 | build
33 | --sdist
34 | --wheel
35 | --outdir dist/
36 | .
37 | - name: Publish distribution 📦 to Test PyPI
38 | if: startsWith(github.ref, 'refs/tags')
39 | uses: pypa/gh-action-pypi-publish@release/v1
40 | with:
41 | password: ${{ secrets.TEST_PYPI_API_TOKEN }}
42 | repository_url: https://test.pypi.org/legacy/
43 | - name: Publish distribution 📦 to PyPI
44 | if: startsWith(github.ref, 'refs/tags')
45 | uses: pypa/gh-action-pypi-publish@release/v1
46 | with:
47 | password: ${{ secrets.PYPI_API_TOKEN }}
48 |
--------------------------------------------------------------------------------
/examples/run_all_examples.py:
--------------------------------------------------------------------------------
1 | '''
2 | Stanley Bak
3 | Run all the examples (useful as a sort of integration test)
4 | April 2017
5 | '''
6 |
7 | import imp
8 | import time
9 | import os
10 |
11 | def main():
12 | 'main code'
13 |
14 | script_dir = os.path.dirname(os.path.realpath(__file__))
15 | python_files = get_files(script_dir, '.py')
16 |
17 | start = time.time()
18 |
19 | for filepath in python_files:
20 | if "counterexample" in filepath: # skip some files
21 | continue
22 |
23 | # this script itself will also be a member of the python_files list... skip it
24 | filename = os.path.split(__file__)[-1]
25 | if filename in os.path.split(filepath):
26 | continue
27 |
28 | print(f"\nRunning example: {filepath}")
29 |
30 | run_example(filepath)
31 |
32 | diff = time.time() - start
33 | print("\nDone! Ran all examples in {:.1f} seconds".format(diff))
34 |
35 | def get_files(filename, extension='.py'):
36 | '''recursively get all the files with the given extension in the passed-in directory'''
37 | rv = []
38 |
39 | if os.path.isdir(filename):
40 | # recursive case
41 | file_list = os.listdir(filename)
42 |
43 | for f in file_list:
44 | rv += get_files(filename + "/" + f)
45 |
46 | elif filename.endswith(extension):
47 | # base case
48 | rv.append(filename)
49 |
50 | return rv
51 |
52 | def run_example(filepath):
53 | 'run a hylaa example at the given path'
54 |
55 | path_parts = os.path.split(filepath)
56 |
57 | mod_name, _ = os.path.splitext(path_parts[-1])
58 | loaded_module = imp.load_source(mod_name, filepath)
59 |
60 | run_hylaa_func = getattr(loaded_module, 'run_hylaa')
61 | #define_settings_func = getattr(loaded_module, 'define_settings')
62 |
63 | #settings = define_settings_func()
64 |
65 | working_dir = os.getcwd()
66 | mod_directory = "/".join(path_parts[:-1])
67 | os.chdir(mod_directory)
68 |
69 | run_hylaa_func()
70 |
71 | os.chdir(working_dir)
72 |
73 | if __name__ == "__main__":
74 | main()
75 |
--------------------------------------------------------------------------------
/examples/harmonic_oscillator/ha.py:
--------------------------------------------------------------------------------
1 | '''
2 | Harmonic Oscillator (with time) Example in Hylaa
3 |
4 | Very simple 2-d example:
5 |
6 | x' == y
7 | y' == -x
8 | '''
9 |
10 | import math
11 |
12 | import numpy as np
13 | from scipy.sparse import csr_matrix
14 |
15 | from hylaa.hybrid_automaton import HybridAutomaton
16 | from hylaa.settings import HylaaSettings, PlotSettings
17 | from hylaa.core import Core
18 | from hylaa.stateset import StateSet
19 | from hylaa import lputil
20 |
21 | def define_ha():
22 | '''make the hybrid automaton'''
23 |
24 | ha = HybridAutomaton()
25 |
26 | # dynamics: x' = y, y' = -x
27 | a_matrix = np.array([[0, 1], [-1, 0]], dtype=float)
28 | a_csr = csr_matrix(a_matrix, dtype=float)
29 |
30 | mode = ha.new_mode('mode')
31 | mode.set_dynamics(a_csr)
32 |
33 | return ha
34 |
35 | def make_init(ha):
36 | '''returns list of initial states'''
37 |
38 | mode = ha.modes['mode']
39 | # init states: x in [-5, -4], y in [0, 1]
40 | init_lpi = lputil.from_box([[-5, -4], [0, 1]], mode)
41 |
42 | init_list = [StateSet(init_lpi, mode)]
43 |
44 | return init_list
45 |
46 | def define_settings():
47 | 'get the hylaa settings object'
48 |
49 | step = math.pi/4
50 | max_time = 1 * math.pi / 2
51 | settings = HylaaSettings(step, max_time)
52 |
53 | plot_settings = settings.plot
54 | plot_settings.plot_mode = PlotSettings.PLOT_IMAGE
55 | plot_settings.xdim_dir = 0
56 | plot_settings.ydim_dir = 1
57 |
58 | #plot_settings.plot_mode = PlotSettings.PLOT_VIDEO
59 | #plot_settings.filename = 'ha.mp4'
60 | #plot_settings.video_fps = 2
61 | #plot_settings.video_extra_frames = 10 # extra frames at the end of a video so it doesn't end so abruptly
62 | #plot_settings.video_pause_frames = 5 # frames to render in video whenever a 'pause' occurs
63 |
64 | plot_settings.label.y_label = '$y$'
65 | plot_settings.label.x_label = '$x$'
66 | plot_settings.label.title = 'Harmonic Oscillator'
67 |
68 | return settings
69 |
70 | def run_hylaa():
71 | 'Runs hylaa with the given settings'
72 |
73 | ha = define_ha()
74 | settings = define_settings()
75 | init_states = make_init(ha)
76 |
77 | Core(ha, settings).run(init_states)
78 |
79 | if __name__ == '__main__':
80 | run_hylaa()
81 |
--------------------------------------------------------------------------------
/hylaa/time_elapse.py:
--------------------------------------------------------------------------------
1 | '''
2 | Time Elapse Computation. This module is primarily responsive for computing
3 | l * e^{At} where l is some direction of interest, and t is a multiple of some time step
4 | '''
5 |
6 | import numpy as np
7 |
8 | from hylaa.util import Freezable
9 | from hylaa.timerutil import Timers
10 | from hylaa.time_elapse_expm import TimeElapseExpmMult
11 |
12 | class TimeElapser(Freezable):
13 | 'Object which computes the time-elapse function for a single mode at multiples of the time step'
14 |
15 | def __init__(self, mode, step_size):
16 | self.mode = mode
17 | self.step_size = step_size
18 | self.dims = self.mode.a_csr.shape[0]
19 | self.inputs = 0 if self.mode.b_csr is None else self.mode.b_csr.shape[1]
20 |
21 | self.time_elapse_obj = None
22 |
23 | self.freeze_attrs()
24 |
25 | def get_basis_matrix(self, step_num):
26 | '''perform the computation for the the basis matrix and input effects matrix at the passed-in step
27 |
28 | returns a tuple (basis_matrix, input_effects matrix)
29 | '''
30 |
31 | if self.time_elapse_obj is None:
32 | Timers.tic('init time_elapse_obj')
33 | self.time_elapse_obj = TimeElapseExpmMult(self)
34 | Timers.toc('init time_elapse_obj')
35 |
36 | Timers.tic('step')
37 | self.time_elapse_obj.assign_basis_matrix(step_num)
38 | Timers.toc('step')
39 |
40 | basis_mat = self.time_elapse_obj.cur_basis_matrix
41 | input_effects_mat = self.time_elapse_obj.cur_input_effects_matrix
42 |
43 | # post-conditions check
44 | assert isinstance(basis_mat, np.ndarray), "cur_basis_mat should be an np.array, " + \
45 | "but it was {}".format(type(basis_mat))
46 |
47 | assert basis_mat.shape == (self.dims, self.dims), \
48 | "cur_basis mat shape({}) should be {}".format(basis_mat.shape, (self.dims, self.dims))
49 |
50 | if self.inputs == 0 or step_num == 0: # 0-th step input should be null
51 | assert input_effects_mat is None
52 | else:
53 | assert isinstance(input_effects_mat, np.ndarray)
54 |
55 | if self.time_elapse_obj.use_lgg:
56 | assert input_effects_mat.shape == (self.dims, self.inputs + self.dims)
57 | else:
58 | assert input_effects_mat.shape == (self.dims, self.inputs)
59 |
60 | return basis_mat, input_effects_mat
61 |
62 | def use_lgg_approx(self):
63 | '''
64 | set this time elapse object to use lgg approximation model
65 | '''
66 |
67 | self.time_elapse_obj.use_lgg_approx()
68 |
--------------------------------------------------------------------------------
/examples/approx_model/approx_model.py:
--------------------------------------------------------------------------------
1 | '''
2 | Harmonic Oscillator Example in Hylaa, demonstrating using
3 | various approximation models for continuous-time reachability
4 |
5 |
6 | dynamics are:
7 | x' = y + u1
8 | y' = -x
9 | starting from [-5, -4], [0, 1]
10 | with u1 in [-0.2, 0.2]
11 | '''
12 |
13 | import math
14 |
15 | import numpy as np
16 | from scipy.sparse import csr_matrix
17 |
18 | from hylaa.hybrid_automaton import HybridAutomaton
19 | from hylaa.settings import HylaaSettings, PlotSettings
20 | from hylaa.core import Core
21 | from hylaa.stateset import StateSet
22 | from hylaa import lputil
23 |
24 | def define_ha():
25 | '''make the hybrid automaton'''
26 |
27 | ha = HybridAutomaton()
28 |
29 | a_matrix = [[0, 1], [-1, 0]]
30 |
31 | b_mat = [[1], [0]]
32 | b_constraints = [[1], [-1]]
33 | b_rhs = [0.2, 0.2]
34 |
35 | mode = ha.new_mode('mode')
36 | mode.set_dynamics(a_matrix)
37 | mode.set_inputs(b_mat, b_constraints, b_rhs)
38 |
39 | return ha
40 |
41 | def make_init(ha):
42 | '''returns list of initial states'''
43 |
44 | mode = ha.modes['mode']
45 | # init states: x in [-5, -4], y in [0, 1]
46 | #init_lpi = lputil.from_box([[-5, -4], [0, 1]], mode)
47 | init_lpi = lputil.from_box([[-5, -5], [0, 0]], mode)
48 |
49 | init_list = [StateSet(init_lpi, mode)]
50 |
51 | return init_list
52 |
53 | def define_settings():
54 | 'get the hylaa settings object'
55 |
56 | step = math.pi/16
57 | max_time = 2*math.pi
58 | settings = HylaaSettings(step, max_time)
59 |
60 | plot_settings = settings.plot
61 | plot_settings.plot_mode = PlotSettings.PLOT_IMAGE
62 | plot_settings.xdim_dir = 0
63 | plot_settings.ydim_dir = 1
64 |
65 | #plot_settings.plot_mode = PlotSettings.PLOT_VIDEO
66 | #plot_settings.filename = 'ha.mp4'
67 | #plot_settings.video_fps = 2
68 | #plot_settings.video_extra_frames = 10 # extra frames at the end of a video so it doesn't end so abruptly
69 | #plot_settings.video_pause_frames = 5 # frames to render in video whenever a 'pause' occurs
70 |
71 | plot_settings.label.y_label = '$y$'
72 | plot_settings.label.x_label = '$x$'
73 | plot_settings.label.title = 'Harmonic Oscillator'
74 |
75 | return settings
76 |
77 | def run_hylaa():
78 | 'Runs hylaa with the given settings'
79 |
80 | ha = define_ha()
81 | settings = define_settings()
82 |
83 | tuples = []
84 | tuples.append((HylaaSettings.APPROX_NONE, "approx_none.png"))
85 | tuples.append((HylaaSettings.APPROX_CHULL, "approx_chull.png"))
86 | tuples.append((HylaaSettings.APPROX_LGG, "approx_lgg.png"))
87 |
88 | for model, filename in tuples:
89 | settings.approx_model, settings.plot.filename = model, filename
90 |
91 | init_states = make_init(ha)
92 | print(f"\nMaking {filename}...")
93 | Core(ha, settings).run(init_states)
94 |
95 | if __name__ == '__main__':
96 | run_hylaa()
97 |
--------------------------------------------------------------------------------
/examples/space_station/space_station.py:
--------------------------------------------------------------------------------
1 | '''
2 | This is the International Space Station example used in the ARCH reachability tools competition in 2018.
3 |
4 | It is a 271-dimensional continuous system, with three time-varying inputs.
5 |
6 | This model demonstrates:
7 | - Loading dynamics from matlab .mat files
8 | - Error condition that is a disjunction
9 | - Plotting with a linear combination of the system states
10 | - Plotting over time without introducing a new variable
11 | '''
12 |
13 | from scipy.io import loadmat
14 |
15 | from hylaa.hybrid_automaton import HybridAutomaton
16 | from hylaa.settings import HylaaSettings, PlotSettings
17 | from hylaa.core import Core
18 | from hylaa.stateset import StateSet
19 | from hylaa import lputil
20 |
21 | def make_automaton():
22 | 'make the hybrid automaton'
23 |
24 | ha = HybridAutomaton()
25 |
26 | mode = ha.new_mode('mode')
27 | dynamics = loadmat('iss.mat')
28 | a_matrix = dynamics['A']
29 | b_matrix = dynamics['B']
30 |
31 | mode.set_dynamics(a_matrix)
32 |
33 | # input bounds
34 | # 0 <= u1 <= 0.1
35 | # 0.8 <= u2 <= 1.0
36 | # 0.9 <= u3 <= 1.0
37 | bounds_mat = [[1, 0, 0], [-1, 0, 0], [0, 1, 0], [0, -1, 0], [0, 0, 1], [0, 0, -1]]
38 | bounds_rhs = [0.1, 0, 1.0, -0.8, 1.0, -0.9]
39 | mode.set_inputs(b_matrix, bounds_mat, bounds_rhs)
40 |
41 | error = ha.new_mode('error')
42 |
43 | # the third output defines the unsafe condition
44 | y3 = dynamics['C'][2]
45 |
46 | limit = 0.0005
47 | #limit = 0.0007
48 |
49 | # Error condition: y3 * x <= -limit OR y3 >= limit
50 | trans1 = ha.new_transition(mode, error)
51 | trans1.set_guard(y3, [-limit])
52 |
53 | trans2 = ha.new_transition(mode, error)
54 | trans2.set_guard(-1 * y3, [-limit])
55 |
56 | return ha
57 |
58 | def make_init(ha):
59 | 'make the initial states'
60 |
61 | # initial set has every variable as [-0.0001, 0.0001]
62 | mode = ha.modes['mode']
63 |
64 | dims = mode.a_csr.shape[0]
65 | init_box = dims * [[-0.0001, 0.0001]]
66 | init_lpi = lputil.from_box(init_box, mode)
67 |
68 | init_list = [StateSet(init_lpi, mode)]
69 |
70 | return init_list
71 |
72 | def make_settings():
73 | 'make the reachability settings object'
74 |
75 | # see hylaa.settings for a list of reachability settings
76 | settings = HylaaSettings(0.1, 20.0) # step size = 0.1, time bound 20.0
77 | settings.plot.plot_mode = PlotSettings.PLOT_NONE
78 | settings.stdout = HylaaSettings.STDOUT_VERBOSE
79 | settings.plot.filename = "space_station.png"
80 |
81 | settings.plot.xdim_dir = None # x dimension will be time
82 |
83 | dynamics = loadmat('iss.mat')
84 | y3 = dynamics['C'][2]
85 | settings.plot.ydim_dir = y3.toarray()[0] # use y3 for the y plot direction
86 |
87 | return settings
88 |
89 | def run_hylaa():
90 | 'main entry point'
91 |
92 | ha = make_automaton()
93 |
94 | init_states = make_init(ha)
95 |
96 | settings = make_settings()
97 |
98 | Core(ha, settings).run(init_states)
99 |
100 | if __name__ == "__main__":
101 | run_hylaa()
102 |
--------------------------------------------------------------------------------
/examples/demo_inputs_reset/demo_inputs_reset.py:
--------------------------------------------------------------------------------
1 | '''
2 | Demonstration model for inputs and resets with Hylaa
3 |
4 | The first mode has the harmonic oscillator dynamics with inputs (x' = y + u1, y' = -x + u2) starting from
5 | [-5, -4], [0, 1], with u1, u2 in [-0.5, 0.5]
6 |
7 | Upon pi/2 time elapsing, there is a time-triggered transition which sets y' := y * -0.5.
8 |
9 | The second mode is the harmonic oscillator in the other direction, with no inputs, x' = -y, y' = x.
10 |
11 | This model demonstrates:
12 | - Time-varying inputs in the dynamics (mode 1)
13 | - Affine variable to add clock dynamics (c' == a with a(t) = 0 for all t)
14 | - Reset upon reaching a transition
15 | - Time-triggered transition (invariant is opposite of guard)
16 | - Modes with different numbers of state variables (mode 1 has four, mode 2 has two)
17 | - Plot output to an image
18 | '''
19 |
20 | import math
21 |
22 | from hylaa.hybrid_automaton import HybridAutomaton
23 | from hylaa.settings import HylaaSettings, PlotSettings
24 | from hylaa.core import Core
25 | from hylaa.stateset import StateSet
26 | from hylaa import lputil
27 |
28 | def make_automaton():
29 | 'make the hybrid automaton'
30 |
31 | ha = HybridAutomaton()
32 |
33 | # mode one: x' = y + u1, y' = -x + u2, c' = 1, a' = 0
34 | m1 = ha.new_mode('m1')
35 | m1.set_dynamics([[0, 1, 0, 0], [-1, 0, 0, 0], [0, 0, 0, 1], [0, 0, 0, 0]])
36 |
37 | b_mat = [[1, 0], [0, 1], [0, 0], [0, 0]]
38 | b_constraints = [[1, 0], [-1, 0], [0, 1], [0, -1]]
39 | b_rhs = [0.5, 0.5, 0.5, 0.5]
40 | m1.set_inputs(b_mat, b_constraints, b_rhs)
41 |
42 | # mode two: x' = -y, y' = x, a' = 0
43 | m2 = ha.new_mode('m2')
44 | m2.set_dynamics([[0, -1], [1, 0]])
45 |
46 | # m1 invariant: c <= pi/2
47 | m1.set_invariant([[0, 0, 1, 0]], [math.pi / 2])
48 |
49 | # guard: c >= pi/2
50 | trans = ha.new_transition(m1, m2)
51 | trans.set_guard([[0, 0, -1, 0]], [-math.pi/2])
52 |
53 | # Assign the reset to the transition
54 | # y *= -1, also the reset is what is used to change the number of system variables (m1 has four vars, m2 has two)
55 |
56 | reset_csr = [[1, 0, 0, 0], [0, -1, 0, 0]]
57 |
58 | # no minkowski sum terms
59 | trans.set_reset(reset_csr)
60 |
61 | return ha
62 |
63 | def make_init(ha):
64 | 'make the initial states'
65 |
66 | # initial set has x0 = [-5, -4], y = [0, 1], c = 0, a = 1
67 | mode = ha.modes['m1']
68 | init_lpi = lputil.from_box([(-5, -4), (0, 1), (0, 0), (1, 1)], mode)
69 |
70 | init_list = [StateSet(init_lpi, mode)]
71 |
72 | return init_list
73 |
74 | def make_settings():
75 | 'make the reachability settings object'
76 |
77 | # see hylaa.settings for a list of reachability settings
78 | settings = HylaaSettings(math.pi / 8, math.pi) # step size = pi/8, time bound pi
79 | settings.plot.plot_mode = PlotSettings.PLOT_IMAGE
80 | settings.stdout = HylaaSettings.STDOUT_NORMAL
81 | settings.plot.filename = "demo_inputs_reset.png"
82 |
83 | return settings
84 |
85 | def run_hylaa():
86 | 'main entry point'
87 |
88 | ha = make_automaton()
89 |
90 | init_states = make_init(ha)
91 |
92 | settings = make_settings()
93 |
94 | Core(ha, settings).run(init_states)
95 |
96 | if __name__ == "__main__":
97 | run_hylaa()
98 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/stanleybak/hylaa)
2 | [](https://badge.fury.io/py/hylaa)
3 |
4 | # Hylaa #
5 |
6 |
7 |
8 | Hylaa (**HY**brid **L**inear **A**utomata **A**nalyzer) is a verification tool for system models with linear ODEs, time-varying inputs, and possibly hybrid dynamics.
9 |
10 | The is version 2 of Hylaa, with support for resets and hybrid automata with time-varying inputs. The focus has shifted from performance to handling more general dynamics.
11 |
12 | The latest version of Hylaa is always available on our github repository at https://github.com/stanleybak/hylaa . A website for Hylaa is maintained at http://stanleybak.com/hylaa .
13 |
14 | The main citation to use for Hylaa is: "HyLAA: A Tool for Computing Simulation-Equivalent Reachability for Linear Systems", S. Bak, P. Duggirala, 20th International Conference on Hybrid Systems: Computation and Control (HSCC 2017)
15 |
16 | The code was mostly written by Stanley Bak (http://stanleybak.com) with input from Parasara Sridhar Duggirala (http://engr.uconn.edu/~psd).
17 |
18 | Hylaa is released under the GPL v3 license (see the LICENSE file). Earlier versions have been approved for public release (DISTRIBUTION A: Approved for public release; distribution unlimited #88ABW-2016-5976, 22 NOV 2016).
19 |
20 | ### Installation ###
21 |
22 | This version of Hylaa runs in `python3` and requires a few other libraries that you can install with `pip3`, the python package manager. You must also set your `PYTHONPATH` environment variable so that it knows where the hylaa source is located. There is a `Dockerfile` in this repository which is used as part of our continuous integration framework that has step by step commands for installing the necessary packages and dependencies. This serves as the installation documentation, as it's always up to date.
23 |
24 | ### Getting Started + Example ###
25 |
26 | The easiest way to get started with Hylaa is to run some of the examples. Once installed and setup, Hylaa models are just python source files you directly with `python3` in a terminal.
27 |
28 | Go to `examples/harmonic_oscillator` and run `ha.py` from the command line (`python ha.py`). This should create `plot.png` in the same folder, which will be an 2-d plot of the reachable set.
29 |
30 | The dynamics in this example are given as x' = **A**x, where **A** is the (potentially sparse) dynamics matrix. This is defined in the `define_ha` function in the `ha.py` source file.
31 |
32 | Initial states and unsafe states are given as conjunctions of linear constraints. These are defined in the `make_init` function.
33 |
34 | Finally, computation settings are given in the `define_settings` function. There are lots of settings that can be adjusted, which can be found in `hylaa/settings.py`, including comments describing what each one does.
35 |
36 | The easiest way to use Hylaa on your example is to copy an example from the examples folder and edit that. Notice that models are python code, which means you can
37 | create the model programatically using loops or by loading the dynamics from a .mat file.
38 |
39 | ### Pending Deprecation Warnings in Test Suite ###
40 |
41 | The test suite produces pending deprecation warnings. This comes from scipy sparse matrices using matrix objects, particularly for computing the matrix exponential. I expect at some point scipy will make an update that will fix these, and then they'll go away. For now, we're ignoring them.
42 |
43 |
--------------------------------------------------------------------------------
/examples/demo_reset/demo_reset.py:
--------------------------------------------------------------------------------
1 | '''
2 | Demonstration model for resets with Hylaa
3 |
4 | 2-d model with x0, y0 both in [0, 0.5], step size 1.0, time bound 10.0
5 | Dynamics in mode 1 are x'= 2, y'=1
6 |
7 | Upon reaching x >= 9.9 (after 5 steps), a reset is taken which sets: x' := [0, 1], y' := y - 10
8 |
9 | Dynamics in mode 2 are x' = 1, y' = 1
10 |
11 | This model demonstrates:
12 | - Affine variable to add clock dynamics (c' == a with a(t) = 0 for all t)
13 | - Reset upon reaching a transition
14 | - Reset has a minkowski sum term, in addition to a reset matrix
15 | - Plot output to an image
16 | '''
17 |
18 | from hylaa.hybrid_automaton import HybridAutomaton
19 | from hylaa.settings import HylaaSettings, PlotSettings
20 | from hylaa.core import Core
21 | from hylaa.stateset import StateSet
22 | from hylaa import lputil
23 |
24 | def make_automaton():
25 | 'make the hybrid automaton'
26 |
27 | ha = HybridAutomaton()
28 |
29 | # mode one: x' = 2, y' = 1, a' = 0
30 | m1 = ha.new_mode('m1')
31 | m1.set_dynamics([[0, 0, 2], [0, 0, 1], [0, 0, 0]])
32 |
33 | # mode two: x' = 1, y' = 1, a' = 0
34 | m2 = ha.new_mode('m2')
35 | m2.set_dynamics([[0, 0, 1], [0, 0, 1], [0, 0, 0]])
36 |
37 | # invariant: x <= 9.9
38 | m1.set_invariant([[1, 0, 0]], [9.9])
39 |
40 | # guard: x >= 9.9
41 | trans = ha.new_transition(m1, m2, 'transition_name')
42 | trans.set_guard([[-1, 0, 0]], [-9.9])
43 |
44 | # Assign the reset to the transition:
45 | #
46 | # def set_reset(self, reset_csr=None, reset_minkowski_csr=None, reset_minkowski_constraints_csr=None,
47 | # reset_minkowski_constraints_rhs=None):
48 | # '''resets are of the form x' = Rx + My, Cy <= rhs, where y are fresh variables
49 | # the reset_minowski variables can be None if no new variables are needed. If unassigned, the identity
50 | # reset is assumed
51 | #
52 | # x' are the new variables
53 | # x are the old variables
54 | # reset_csr is R
55 | # reset_minkowski_csr is M
56 | # reset_minkowski_constraints_csr is C
57 | # reset_minkowski_constraints_rhs is rhs
58 | # '''
59 |
60 | # we want the reset to set x' := [0, 1], y' := y - 10
61 |
62 | reset_csr = [[0, 0, 0], [0, 1, 0], [0, 0, 1]]
63 |
64 | # two new minkowski variables, y0 = [0, 1], y1 = [-10, -10]
65 | minkowski_csr = [[1, 0], [0, 1], [0, 0]]
66 | constraints_csr = [[1, 0], [-1, 0], [0, 1], [0, -1]]
67 | constraints_rhs = [1, 0, -10, 10]
68 |
69 | trans.set_reset(reset_csr, minkowski_csr, constraints_csr, constraints_rhs)
70 |
71 | return ha
72 |
73 | def make_init(ha):
74 | 'make the initial states'
75 |
76 | # initial set has x0 = [0, 0.5], y = [0, 0.5], a = 1
77 | mode = ha.modes['m1']
78 | init_lpi = lputil.from_box([(0, 0.5), (0, 0.5), (1, 1)], mode)
79 |
80 | init_list = [StateSet(init_lpi, mode)]
81 |
82 | return init_list
83 |
84 | def make_settings():
85 | 'make the reachability settings object'
86 |
87 | # see hylaa.settings for a list of reachability settings
88 | settings = HylaaSettings(1.0, 10.0) # step size = 1.0, time bound 10.0
89 | settings.plot.plot_mode = PlotSettings.PLOT_IMAGE
90 | settings.stdout = HylaaSettings.STDOUT_VERBOSE
91 | settings.plot.filename = "demo_reset.png"
92 |
93 | return settings
94 |
95 | def run_hylaa():
96 | 'main entry point'
97 |
98 | ha = make_automaton()
99 |
100 | init_states = make_init(ha)
101 |
102 | settings = make_settings()
103 |
104 | Core(ha, settings).run(init_states)
105 |
106 | if __name__ == "__main__":
107 | run_hylaa()
108 |
--------------------------------------------------------------------------------
/tests/test_deaggregation.py:
--------------------------------------------------------------------------------
1 | '''
2 | Tests for Hylaa deaggregation. Made for use with py.test
3 | '''
4 |
5 | from hylaa.hybrid_automaton import HybridAutomaton
6 | from hylaa.stateset import StateSet
7 | from hylaa.settings import HylaaSettings, PlotSettings
8 | from hylaa import lputil
9 | from hylaa.core import Core
10 | from hylaa.aggdag import OpTransition, OpInvIntersect
11 |
12 | def fail_deagg_counterexample():
13 | 'test that aggregation with a counterexample'
14 | # init: x0, y0 \in [0, 1], step = 1.0
15 | #
16 | # m1 dynamics: x' == 1, y' == 0,
17 | # m1 invariant: x <= 3
18 | # m1 -> m2 guard: True
19 | # m2 dynamics: x' == 0, y' == 1
20 | # m2 -> error: y >= 3
21 |
22 | ha = HybridAutomaton()
23 |
24 | # mode one: x' = 1, y' = 0, a' = 0
25 | m1 = ha.new_mode('m1')
26 | m1.set_dynamics([[0, 0, 1], [0, 0, 0], [0, 0, 0]])
27 |
28 | # mode two: x' = 0, y' = 1, a' = 0
29 | m2 = ha.new_mode('m2')
30 | m2.set_dynamics([[0, 0, 0], [0, 0, 1], [0, 0, 0]])
31 |
32 | # invariant: x <= 3.0
33 | m1.set_invariant([[1, 0, 0]], [3.0])
34 |
35 | # guard: True
36 | trans1 = ha.new_transition(m1, m2, 'trans1')
37 | trans1.set_guard_true()
38 |
39 | error = ha.new_mode('error')
40 | trans2 = ha.new_transition(m2, error, 'trans2')
41 | trans2.set_guard([[0, -1, 0]], [-3]) # y >= 3
42 |
43 | # initial set has x0 = [0, 1], t = [0, 1], a = 1
44 | init_lpi = lputil.from_box([(0, 1), (0, 1), (1, 1)], m1)
45 | init_list = [StateSet(init_lpi, m1)]
46 |
47 | # settings, step size = 1.0
48 | settings = HylaaSettings(1.0, 10.0)
49 | settings.stdout = HylaaSettings.STDOUT_VERBOSE
50 | settings.plot.plot_mode = PlotSettings.PLOT_NONE
51 | settings.aggregation.deaggregation = True
52 |
53 | core = Core(ha, settings)
54 |
55 | core.setup(init_list)
56 |
57 | core.do_step() # pop
58 |
59 | assert core.aggdag.cur_node is not None
60 |
61 | for _ in range(5): # 4 + 1 step to leave invariant
62 | core.do_step() # continuous post in m1
63 |
64 | core.do_step() # pop
65 |
66 | # at this point, the state should be aggregated, but we should maintain one concrete state that's feasible
67 | cur_node = core.aggdag.cur_node
68 | assert cur_node.aggregated_state == core.aggdag.get_cur_state()
69 |
70 | assert cur_node.concrete_state is not None
71 |
72 | assert len(core.aggdag.roots) == 1
73 | root = core.aggdag.roots[0]
74 |
75 | assert root.op_list
76 | assert isinstance(root.op_list[0], OpTransition) and root.op_list[0].step == 1 # transition from x=[1, 2]
77 | assert isinstance(root.op_list[1], OpTransition) and root.op_list[1].step == 2 # transition from x=[2, 3]
78 | assert isinstance(root.op_list[2], OpTransition) and root.op_list[2].step == 3 # transition from x=[3, 4]
79 | assert isinstance(root.op_list[3], OpTransition) and root.op_list[3].step == 4 # transition from x=[4, 5]
80 |
81 | for s in range(4):
82 | assert root.op_list[s].transition == trans1 and root.op_list[s].poststate.is_concrete
83 |
84 | assert isinstance(root.op_list[4], OpInvIntersect)
85 | op4 = root.op_list[4]
86 | assert op4.step == 5 and op4.node == root and op4.i_index == 0 and not op4.is_stronger
87 |
88 | assert isinstance(root.op_list[5], OpTransition) and root.op_list[5].step == 5 # transition from x=[4, 4]
89 |
90 | assert isinstance(root.op_list[6], OpInvIntersect)
91 | op6 = root.op_list[6]
92 | assert op6.step == 6 and op6.node == root and op6.i_index == 0 and op6.is_stronger
93 |
94 | assert len(root.op_list) == 7
95 |
96 | core.run_to_completion()
97 |
98 | assert root.op_list[0].child_node == cur_node
99 | assert root.op_list[3].child_node == cur_node
100 |
101 | #result = core.run(init_list)
102 |
103 | #assert not result.counterexample
104 |
--------------------------------------------------------------------------------
/examples/ha_deaggregation/ha_deagg.py:
--------------------------------------------------------------------------------
1 | '''
2 | Harmonic Oscillator example with deaggreagtion
3 |
4 | 2-d dynamics:
5 |
6 | x' == y
7 | y' == -x
8 |
9 | after a semi-circle, the dynamics in the second mode are y' == -1, x' == 0
10 |
11 | '''
12 |
13 | import math
14 |
15 | from matplotlib import collections
16 |
17 | from hylaa.hybrid_automaton import HybridAutomaton
18 | from hylaa.settings import HylaaSettings, PlotSettings
19 | from hylaa.aggstrat import Aggregated
20 | from hylaa.core import Core
21 | from hylaa.stateset import StateSet
22 | from hylaa import lputil
23 |
24 | def define_ha(unsafe_box):
25 | '''make the hybrid automaton'''
26 |
27 | ha = HybridAutomaton()
28 |
29 | # dynamics: x' = y, y' = -x, t' == a
30 | a_mat = [[0, 1, 0, 0], [-1, 0, 0, 0], [0, 0, 0, 1], [0, 0, 0, 0]]
31 |
32 | one = ha.new_mode('one')
33 | one.set_dynamics(a_mat)
34 | one.set_invariant([[0, 0, 1, 0]], [math.pi - 1e-6]) # t <= pi
35 |
36 | two = ha.new_mode('two')
37 | two.set_dynamics([[0, 0, 0, 0], [0, 0, 0, -1], [0, 0, 0, 1], [0, 0, 0, 0]])
38 | two.set_invariant([[0, -1, 0, 0]], [3]) # y >= -3
39 |
40 | t = ha.new_transition(one, two)
41 | t.set_guard_true()
42 |
43 | error = ha.new_mode('error')
44 | t = ha.new_transition(two, error)
45 |
46 | unsafe_rhs = [-unsafe_box[0][0], unsafe_box[0][1], -unsafe_box[1][0], unsafe_box[1][1]]
47 | t.set_guard([[-1, 0, 0, 0], [1, 0, 0, 0], [0, -1, 0, 0], [0, 1, 0, 0]], unsafe_rhs)
48 |
49 | return ha
50 |
51 | def make_init(ha):
52 | '''returns list of initial states'''
53 |
54 | mode = ha.modes['one']
55 | # init states: x in [-5, -4], y in [0, 1]
56 | init_lpi = lputil.from_box([[-5, -4], [-0.5, 0.5], [0, 0], [1, 1]], mode)
57 |
58 | init_list = [StateSet(init_lpi, mode)]
59 |
60 | return init_list
61 |
62 | def define_settings(unsafe_box):
63 | 'get the hylaa settings object'
64 |
65 | step = math.pi/6
66 | max_time = 3 * math.pi
67 | settings = HylaaSettings(step, max_time)
68 |
69 | settings.process_urgent_guards = True
70 | settings.aggstrat.deaggregate = True # use deaggregation
71 | settings.aggstrat.deagg_preference = Aggregated.DEAGG_LEAVES_FIRST
72 | settings.aggstrat.agg_type = Aggregated.AGG_CONVEX_HULL
73 |
74 | plot_settings = settings.plot
75 | plot_settings.plot_mode = PlotSettings.PLOT_IMAGE
76 | plot_settings.xdim_dir = 0
77 | plot_settings.ydim_dir = 1
78 |
79 | plot_settings.label.x_label = '$x$'
80 | plot_settings.label.y_label = '$y$'
81 |
82 | cols = []
83 | line = [(-10, -3), (10, -3)]
84 | cols.append(collections.LineCollection([line], animated=True, colors=('gray'), linewidths=(2), linestyle='dashed'))
85 |
86 | line = []
87 | line.append((unsafe_box[0][0], unsafe_box[1][0]))
88 | line.append((unsafe_box[0][1], unsafe_box[1][0]))
89 | line.append((unsafe_box[0][1], unsafe_box[1][1]))
90 | line.append((unsafe_box[0][0], unsafe_box[1][1]))
91 | line.append((unsafe_box[0][0], unsafe_box[1][0]))
92 |
93 | cols.append(collections.LineCollection([line], animated=True, colors=('red'), linewidths=(2), linestyle='dashed'))
94 |
95 | settings.plot.extra_collections = cols
96 |
97 | plot_settings.plot_mode = PlotSettings.PLOT_VIDEO
98 | plot_settings.filename = 'ha_deagg.mp4'
99 | plot_settings.video_fps = 4
100 | plot_settings.video_extra_frames = 12 # extra frames at the end of a video so it doesn't end so abruptly
101 | plot_settings.video_pause_frames = 2 # frames to render in video whenever a 'pause' occurs
102 |
103 | plot_settings.label.axes_limits = [-6, 6, -4, 6]
104 |
105 | plot_settings.label.y_label = '$y$'
106 | plot_settings.label.x_label = '$x$'
107 | plot_settings.label.title = 'Deaggregation Demo'
108 |
109 | return settings
110 |
111 | def run_hylaa():
112 | 'Runs hylaa with the given settings'
113 |
114 | unsafe_box = [[0.7, 1.3], [-2, -1]]
115 |
116 | ha = define_ha(unsafe_box)
117 | settings = define_settings(unsafe_box)
118 | init_states = make_init(ha)
119 |
120 | Core(ha, settings).run(init_states)
121 |
122 | if __name__ == '__main__':
123 | run_hylaa()
124 |
--------------------------------------------------------------------------------
/hylaa/aggregate.py:
--------------------------------------------------------------------------------
1 | '''
2 | Stanley Bak
3 |
4 | Implementation of concrete aggregation methods, used by aggdag
5 | '''
6 |
7 | import numpy as np
8 |
9 | from hylaa.stateset import StateSet
10 | from hylaa import lputil
11 |
12 | def aggregate_box_arnoldi(agg_list, op_list, is_box, is_arnoldi, add_guard, print_func):
13 | '''
14 | perform template-based aggregation on the passed-in list of states
15 |
16 | Currently, this can either use box template directions or arnoldi (+box) template directions
17 | '''
18 |
19 | assert is_box or is_arnoldi
20 |
21 | min_step = min([state.cur_steps_since_start[0] for state in agg_list])
22 | max_step = max([state.cur_steps_since_start[1] for state in agg_list])
23 | step_interval = [min_step, max_step]
24 |
25 | print_func("Aggregation time step interval: {}".format(step_interval))
26 |
27 | # create a new state from the aggregation
28 | postmode = agg_list[0].mode
29 | postmode_dims = postmode.a_csr.shape[0]
30 | mid_index = len(agg_list) // 2
31 |
32 | op = op_list[mid_index]
33 |
34 | if is_box or op is None:
35 | agg_dir_mat = np.identity(postmode_dims)
36 | elif is_arnoldi:
37 | # aggregation with a predecessor, use arnoldi directions in predecessor mode in center of
38 | # middle aggregagted state, then project using the reset, and reorthogonalize
39 |
40 | premode = op.parent_node.stateset.mode
41 | t = op.transition
42 | print_func("aggregation point: {}".format(op.premode_center))
43 |
44 | premode_dir_mat = lputil.make_direction_matrix(op.premode_center, premode.a_csr)
45 | print_func("premode dir mat:\n{}".format(premode_dir_mat))
46 |
47 | if t.reset_csr is None:
48 | agg_dir_mat = premode_dir_mat
49 | else:
50 | projected_dir_mat = premode_dir_mat * t.reset_csr.transpose()
51 |
52 | print_func("projected dir mat:\n{}".format(projected_dir_mat))
53 |
54 | # re-orthgohonalize (and create new vectors if necessary)
55 | agg_dir_mat = lputil.reorthogonalize_matrix(projected_dir_mat, postmode_dims)
56 |
57 | # also add box directions in target mode (if they don't already exist)
58 | box_dirs = []
59 | for dim in range(postmode_dims):
60 | direction = [0 if d != dim else 1 for d in range(postmode_dims)]
61 | exists = False
62 |
63 | for row in agg_dir_mat:
64 | if np.allclose(direction, row):
65 | exists = True
66 | break
67 |
68 | if not exists:
69 | box_dirs.append(direction)
70 |
71 | if box_dirs:
72 | agg_dir_mat = np.concatenate((agg_dir_mat, box_dirs), axis=0)
73 |
74 | if op and add_guard:
75 | # add all the guard conditions to the agg_dir_mat
76 |
77 | t = op.transition
78 |
79 | if t.reset_csr is None: # identity reset
80 | guard_dir_mat = t.guard_csr
81 | else:
82 | # multiply each direction in the guard by the reset
83 | guard_dir_mat = t.guard_csr * t.reset_csr.transpose()
84 |
85 | if guard_dir_mat.shape[0] > 0:
86 | agg_dir_mat = np.concatenate((agg_dir_mat, guard_dir_mat.toarray()), axis=0)
87 |
88 | print_func("agg dir mat:\n{}".format(agg_dir_mat))
89 | lpi_list = [state.lpi for state in agg_list]
90 |
91 | new_lpi = lputil.aggregate(lpi_list, agg_dir_mat, postmode)
92 |
93 | return StateSet(new_lpi, agg_list[0].mode, step_interval, op_list, is_concrete=False)
94 |
95 | def aggregate_chull(agg_list, op_list, print_func):
96 | '''
97 | perform template-based aggregation on the passed-in list of states
98 |
99 | Currently, this can either use box template directions or arnoldi (+box) template directions
100 | '''
101 |
102 | min_step = min([state.cur_steps_since_start[0] for state in agg_list])
103 | max_step = max([state.cur_steps_since_start[1] for state in agg_list])
104 | step_interval = [min_step, max_step]
105 |
106 | print_func("Convex hull aggregation time step interval: {}".format(step_interval))
107 |
108 | postmode = agg_list[0].mode
109 | lpi_list = [state.lpi for state in agg_list]
110 |
111 | new_lpi = lputil.aggregate_chull(lpi_list, postmode)
112 |
113 | return StateSet(new_lpi, agg_list[0].mode, step_interval, op_list, is_concrete=False)
114 |
--------------------------------------------------------------------------------
/examples/deaggregation/deag_example.py:
--------------------------------------------------------------------------------
1 | '''
2 | Deaggregation Model from Hylaa
3 |
4 | A simple demo of deaggregation occuring.
5 | '''
6 |
7 | from matplotlib import collections
8 |
9 | from hylaa.hybrid_automaton import HybridAutomaton
10 | from hylaa.settings import HylaaSettings, PlotSettings, LabelSettings
11 | from hylaa.core import Core
12 | from hylaa.aggstrat import Aggregated
13 | from hylaa.stateset import StateSet
14 | from hylaa import lputil
15 |
16 | def make_automaton(unsafe_box):
17 | 'make the hybrid automaton'
18 |
19 | ha = HybridAutomaton('Deaggregation Example')
20 |
21 | # x' = 2
22 | m1 = ha.new_mode('mode0_right')
23 | m1.set_dynamics([[0, 0, 2], [0, 0, 0], [0, 0, 0]])
24 | m1.set_invariant([[1, 0, 0]], [3.5]) # x <= 3.5
25 |
26 | # y' == 2
27 | m2 = ha.new_mode('mode1_up')
28 | m2.set_dynamics([[0, 0, 0], [0, 0, 2], [0, 0, 0]])
29 | m2.set_invariant([0., 1., 0], [3.5]) # y <= 3.5
30 |
31 | # x' == 2
32 | m3 = ha.new_mode('mode2_right')
33 | m3.set_dynamics([[0, 0, 2], [0, 0, -0], [0, 0, 0]])
34 | m3.set_invariant([1., 0, 0], [7]) # x <= 7
35 |
36 | t = ha.new_transition(m1, m2)
37 | t.set_guard_true()
38 |
39 | t = ha.new_transition(m2, m3)
40 | t.set_guard_true()
41 |
42 | error = ha.new_mode('error')
43 | t = ha.new_transition(m3, error)
44 |
45 | unsafe_rhs = [-unsafe_box[0][0], unsafe_box[0][1], -unsafe_box[1][0], unsafe_box[1][1]]
46 |
47 | # x >= 1.1 x <= 1.9, y >= 2.7, y <= 4.3
48 | t.set_guard([[-1, 0, 0], [1, 0, 0], [0, -1, 0], [0, 1, 0]], unsafe_rhs)
49 |
50 | t = ha.new_transition(m2, error)
51 | # x >= 1.1 x <= 1.9, y >= 2.7, y <= 4.3
52 | t.set_guard([[-1, 0, 0], [1, 0, 0], [0, -1, 0], [0, 1, 0]], unsafe_rhs)
53 |
54 | return ha
55 |
56 | def make_init(ha):
57 | 'make the initial states'
58 |
59 | mode = ha.modes['mode0_right']
60 | init_lpi = lputil.from_box([(0, 1), (0, 1.0), (1.0, 1.0)], mode)
61 |
62 | init_list = [StateSet(init_lpi, mode)]
63 |
64 | return init_list
65 |
66 | def make_settings(unsafe_box):
67 | 'make the reachability settings object'
68 |
69 | # see hylaa.settings for a list of reachability settings
70 | settings = HylaaSettings(1.0, 20.0)
71 | settings.process_urgent_guards = True
72 | settings.stdout = HylaaSettings.STDOUT_VERBOSE
73 |
74 | settings.plot.video_pause_frames = 5
75 | settings.plot.video_fps = 5
76 | settings.plot.plot_mode = PlotSettings.PLOT_NONE
77 | settings.plot.interactive_skip_count = 5
78 |
79 | #settings.plot.plot_mode = PlotSettings.PLOT_VIDEO
80 | #settings.plot.filename = "deagg_example.mp4"
81 |
82 | settings.stop_on_aggregated_error = False
83 | settings.aggstrat.deaggregate = True # use deaggregation
84 | settings.aggstrat.deagg_preference = Aggregated.DEAGG_LEAVES_FIRST
85 |
86 | settings.plot.extra_collections = []
87 | settings.plot.label = []
88 |
89 | ls = LabelSettings()
90 | ls.axes_limits = [-1, 12, -1, 6]
91 | settings.plot.label.append(ls)
92 |
93 | ls.big(size=24)
94 |
95 | ls.x_label = '$x$'
96 | ls.y_label = '$y$'
97 |
98 | cols = []
99 |
100 | line = [(3.5, -20), (3.5, 20)]
101 | cols.append(collections.LineCollection([line], animated=True, colors=('gray'), linewidths=(2), linestyle='dashed'))
102 |
103 | line = [(-20, 3.5), (20, 3.5)]
104 | cols.append(collections.LineCollection([line], animated=True, colors=('gray'), linewidths=(2), linestyle='dashed'))
105 |
106 | # x >= 1.1 x <= 1.9, y >= 2.7, y <= 4.3
107 | line = []
108 | line.append((unsafe_box[0][0], unsafe_box[1][0]))
109 | line.append((unsafe_box[0][1], unsafe_box[1][0]))
110 | line.append((unsafe_box[0][1], unsafe_box[1][1]))
111 | line.append((unsafe_box[0][0], unsafe_box[1][1]))
112 | line.append((unsafe_box[0][0], unsafe_box[1][0]))
113 |
114 | cols.append(collections.LineCollection([line], animated=True, colors=('red'), linewidths=(2), linestyle='dashed'))
115 |
116 | settings.plot.extra_collections.append(cols)
117 |
118 | return settings
119 |
120 | def run_hylaa():
121 | 'main entry point'
122 |
123 | unsafe_box = [[5.1, 5.9], [4.1, 4.9]]
124 |
125 | ha = make_automaton(unsafe_box)
126 |
127 | init_states = make_init(ha)
128 |
129 | settings = make_settings(unsafe_box)
130 |
131 | Core(ha, settings).run(init_states)
132 |
133 | if __name__ == "__main__":
134 | run_hylaa()
135 |
--------------------------------------------------------------------------------
/hylaa/simulation.py:
--------------------------------------------------------------------------------
1 | '''
2 | Hylaa Simuation Utilities
3 | '''
4 |
5 | import matplotlib.pyplot as plt
6 |
7 | from hylaa.util import Freezable
8 | from hylaa.settings import PlotSettings
9 |
10 |
11 | class Simulation(Freezable):
12 | 'main simulation container class. Initialize and call run()'
13 |
14 | def __init__(self, ha, settings, init, num_sims):
15 | self.ha = ha # pylint: disable=invalid-name
16 | self.settings = settings
17 | self.init = init
18 |
19 | self.num_sims = num_sims
20 |
21 | self.fig = None
22 | self.axes_list = None
23 | self.num_subplots = None
24 | self.frame_text = None
25 |
26 | self.freeze_attrs()
27 |
28 | def create_plot(self):
29 | 'create the plot'
30 |
31 | if isinstance(self.settings.xdim_dir, list):
32 | assert isinstance(self.settings.ydim_dir, list)
33 | assert len(self.settings.xdim_dir) == len(self.settings.ydim_dir)
34 | else:
35 | self.settings.xdim_dir = [self.settings.xdim_dir]
36 | self.settings.ydim_dir = [self.settings.ydim_dir]
37 |
38 | self.num_subplots = len(self.settings.xdim_dir)
39 |
40 | if not self.settings.plot_mode in [PlotSettings.PLOT_NONE]:
41 | self.fig, axes_list = plt.subplots(nrows=self.num_subplots, ncols=1, figsize=self.settings.plot_size, \
42 | squeeze=False)
43 |
44 | self.axes_list = []
45 |
46 | for row in axes_list:
47 | for axes in row:
48 | self.axes_list.append(axes)
49 |
50 | if not isinstance(self.settings.label, list):
51 | self.settings.label = [self.settings.label]
52 |
53 | # only use title for the first subplot the first plot
54 | title = self.settings.label[0].title
55 | title = title if title is not None else self.ha.name
56 | self.axes_list[0].set_title(title, fontsize=self.settings.label[0].title_size)
57 |
58 | if self.settings.video_show_frame and self.settings.plot_mode == PlotSettings.PLOT_VIDEO:
59 | ax = self.axes_list[0]
60 | self.frame_text = ax.text(0.01, 0.99, '', transform=ax.transAxes, verticalalignment='top')
61 |
62 | for i in range(self.num_subplots):
63 | labels = []
64 | label_settings = [self.settings.xdim_dir[i], self.settings.ydim_dir[i]]
65 | label_strings = [self.settings.label[i].x_label, self.settings.label[i].y_label]
66 |
67 | for label_setting, text in zip(label_settings, label_strings):
68 | if text is not None:
69 | labels.append(text)
70 | elif label_setting is None:
71 | labels.append('Time')
72 | elif isinstance(label_setting, int):
73 | labels.append('$x_{{ {} }}$'.format(label_setting))
74 | else:
75 | labels.append('')
76 |
77 | self.axes_list[i].set_xlabel(labels[0], fontsize=self.settings.label[i].label_size)
78 | self.axes_list[i].set_ylabel(labels[1], fontsize=self.settings.label[i].label_size)
79 |
80 | if self.settings.label[i].axes_limits is not None:
81 | # hardcoded axes limits
82 | xmin, xmax, ymin, ymax = self.settings.label[i].axes_limits
83 |
84 | self.axes_list[i].set_xlim(xmin, xmax)
85 | self.axes_list[i].set_ylim(ymin, ymax)
86 |
87 | if self.settings.grid:
88 | for axes in self.axes_list:
89 | axes.grid(True, linestyle='dashed')
90 |
91 | if self.settings.grid_xtics is not None:
92 | axes.set_xticks(self.settings.grid_xtics)
93 |
94 | if self.settings.grid_ytics is not None:
95 | axes.set_yticks(self.settings.grid_ytics)
96 |
97 | # make the x and y axis animated in case of rescaling
98 | for i, axes in enumerate(self.axes_list):
99 | axes.xaxis.set_animated(True)
100 | axes.yaxis.set_animated(True)
101 |
102 | axes.tick_params(axis='both', which='major', labelsize=self.settings.label[i].tick_label_size)
103 |
104 | plt.tight_layout()
105 |
106 | self.shapes = [DrawnShapes(self, i) for i in range(self.num_subplots)]
107 |
108 | def run(self, box):
109 | 'run simulations from the given initial box'
110 |
111 | self.create_plot()
112 |
--------------------------------------------------------------------------------
/examples/building/building.py:
--------------------------------------------------------------------------------
1 | '''
2 | Building Example in Hylaa. This is the building example from
3 |
4 | ARCH-COMP19 Category Report: Continuous and Hybrid Systems with Linear Continuous Dynamics
5 |
6 | originally from
7 |
8 | H.-D. Tran, L. V. Nguyen, and T. T. Johnson. Large-scale linear systems from order-reduction. In
9 | Proc. of ARCH16. 3rd International Workshop on Applied Verification for Continuous and Hybrid
10 | Systems, 2017.
11 |
12 | This example demonstrates:
13 |
14 | - verification of a linear system with time-varying inputs
15 | - doing plots over time without introducing new variables
16 | - adding custom lines to the plot
17 | '''
18 |
19 | import numpy as np
20 | from scipy.io import loadmat
21 | from scipy.sparse import csr_matrix, csc_matrix
22 |
23 | from matplotlib import collections
24 |
25 | from hylaa.hybrid_automaton import HybridAutomaton
26 | from hylaa.settings import HylaaSettings, PlotSettings
27 | from hylaa.core import Core
28 | from hylaa.stateset import StateSet
29 | from hylaa import lputil
30 |
31 | def define_ha(limit):
32 | '''make the hybrid automaton and return it'''
33 |
34 | ha = HybridAutomaton()
35 |
36 | mode = ha.new_mode('mode')
37 | dynamics = loadmat('build.mat')
38 | a_matrix = dynamics['A']
39 | b_matrix = csc_matrix(dynamics['B'])
40 |
41 | mode.set_dynamics(csr_matrix(a_matrix))
42 |
43 | # 0.8 <= u1 <= 1.0
44 | u_mat = [[1.0], [-1.0]]
45 | u_rhs = [1.0, -0.8]
46 |
47 | mode.set_inputs(b_matrix, u_mat, u_rhs)
48 |
49 | error = ha.new_mode('error')
50 |
51 | y1 = dynamics['C'][0]
52 | mat = csr_matrix(y1, dtype=float)
53 |
54 | trans1 = ha.new_transition(mode, error)
55 | rhs = np.array([-limit], dtype=float) # safe
56 | trans1.set_guard(mat, rhs) # y3 >= limit
57 |
58 | return ha
59 |
60 | def make_init(ha):
61 | '''returns list of initial states'''
62 |
63 | bounds_list = []
64 |
65 | dims = list(ha.modes.values())[0].a_csr.shape[0]
66 |
67 | for dim in range(dims):
68 | if dim < 10:
69 | lb = 0.0002
70 | ub = 0.00025
71 | elif dim == 25:
72 | lb = -0.0001
73 | ub = 0.0001
74 | else:
75 | lb = ub = 0
76 |
77 | bounds_list.append((lb, ub))
78 |
79 | mode = ha.modes['mode']
80 | init_lpi = lputil.from_box(bounds_list, mode)
81 |
82 | init_list = [StateSet(init_lpi, mode)]
83 |
84 | return init_list
85 |
86 | def define_settings(ha, limit):
87 | 'get the hylaa settings object'
88 |
89 | step = 0.0025
90 | max_time = 1.0
91 | settings = HylaaSettings(step, max_time)
92 |
93 | #settings.interval_guard_optimization = False
94 | #settings.time_elapse.scipy_sim.max_step = 0.001
95 |
96 | #settings.time_elapse.scipy_sim.rtol = 1e-9
97 | #settings.time_elapse.scipy_sim.atol = 1e-12
98 |
99 | settings.stdout = stdout = HylaaSettings.STDOUT_VERBOSE
100 |
101 | plot_settings = settings.plot
102 |
103 | plot_settings.plot_mode = PlotSettings.PLOT_IMAGE
104 |
105 | #plot_settings.plot_mode = PlotSettings.PLOT_VIDEO
106 | #plot_settings.filename = 'building.mp4'
107 |
108 | plot_settings.xdim_dir = None
109 | plot_settings.ydim_dir = ha.transitions[0].guard_csr[0].toarray()[0]
110 |
111 | plot_settings.label.y_label = '$y_{1}$'
112 | plot_settings.label.x_label = 'Time'
113 | plot_settings.label.title = 'Building (Uncertain Inputs)'
114 | #plot_settings.label.axes_limits = (0.4, 0.6, -0.0002, -0.0001)
115 | plot_settings.plot_size = (12, 8)
116 | plot_settings.label.big(size=36)
117 |
118 | settings.stop_on_concrete_error = False
119 | settings.make_counterexample = False
120 |
121 | line = [(0.0, -limit), (max_time, -limit)]
122 | lc = collections.LineCollection([line], animated=True, colors=('red'), linewidths=(1), linestyle='dashed')
123 | plot_settings.extra_collections = [lc]
124 |
125 | return settings
126 |
127 | def run_hylaa():
128 | 'Runs hylaa with the given settings'
129 |
130 | #limit = 0.004 # reachable
131 | limit = 0.005 # unreachable
132 |
133 | ha = define_ha(limit)
134 |
135 | tuples = []
136 | tuples.append((HylaaSettings.APPROX_NONE, "approx_none.png"))
137 | tuples.append((HylaaSettings.APPROX_CHULL, "approx_chull.png"))
138 | #tuples.append((HylaaSettings.APPROX_LGG, "approx_lgg.png"))
139 |
140 | for model, filename in tuples:
141 | settings = define_settings(ha, limit)
142 | settings.approx_model, settings.plot.filename = model, filename
143 |
144 | init_states = make_init(ha)
145 | print(f"\nMaking {filename}...")
146 | Core(ha, settings).run(init_states)
147 |
148 | if __name__ == '__main__':
149 | run_hylaa()
150 |
--------------------------------------------------------------------------------
/hylaa/time_elapse_expm.py:
--------------------------------------------------------------------------------
1 | '''
2 | Time-elapse object for matrix exponential and expm-mul methods
3 |
4 | Stanley Bak
5 | April 2018
6 | '''
7 |
8 | from scipy.sparse import csc_matrix
9 | from scipy.sparse.linalg import expm, expm_multiply
10 |
11 | import numpy as np
12 |
13 | from hylaa.util import Freezable
14 | from hylaa.timerutil import Timers
15 |
16 | class TimeElapseExpmMult(Freezable):
17 | 'container object for expm + matrix-vec mult method'
18 |
19 | def __init__(self, time_elapser):
20 | self.time_elapser = time_elapser
21 | self.a_csc = csc_matrix(time_elapser.mode.a_csr)
22 | self.b_csc = None if time_elapser.mode.b_csr is None else csc_matrix(time_elapser.mode.b_csr)
23 | self.dims = time_elapser.dims
24 |
25 | self.cur_step = 0
26 | self.cur_basis_matrix = None
27 | self.cur_input_effects_matrix = None
28 |
29 | self.one_step_matrix_exp = None # one step matrix exponential
30 | self.one_step_input_effects_matrix = None # one step input effects matrix, if inputs exist
31 |
32 | # lgg approximation model vars
33 | self.use_lgg = False
34 |
35 | self.freeze_attrs()
36 |
37 | def init_matrices(self):
38 | 'initialize the one-step basis and input effects matrices'
39 |
40 | dims = self.dims
41 | Timers.tic('expm')
42 | self.one_step_matrix_exp = expm(self.a_csc * self.time_elapser.step_size)
43 | Timers.toc('expm')
44 |
45 | Timers.tic('toarray')
46 | self.one_step_matrix_exp = self.one_step_matrix_exp.toarray()
47 | Timers.toc('toarray')
48 |
49 | if self.b_csc is not None:
50 | self.one_step_input_effects_matrix = np.zeros(self.b_csc.shape, dtype=float)
51 |
52 | for c in range(self.time_elapser.inputs):
53 | # create the a_matrix augmented with a column of the b_matrix as an affine term
54 | indptr = self.b_csc.indptr
55 |
56 | data = np.concatenate((self.a_csc.data, self. b_csc.data[indptr[c]:indptr[c+1]]))
57 | indices = np.concatenate((self.a_csc.indices, self.b_csc.indices[indptr[c]:indptr[c+1]]))
58 | indptr = np.concatenate((self.a_csc.indptr, [len(data)]))
59 |
60 | aug_a_csc = csc_matrix((data, indices, indptr), shape=(dims + 1, dims + 1))
61 |
62 | mat = aug_a_csc * self.time_elapser.step_size
63 |
64 | # the last column of matrix_exp is the same as multiplying it by the initial state [0, 0, ..., 1]
65 | init_state = np.zeros(dims + 1, dtype=float)
66 | init_state[dims] = 1.0
67 | col = expm_multiply(mat, init_state)
68 |
69 | self.one_step_input_effects_matrix[:, c] = col[:dims]
70 |
71 | def assign_basis_matrix(self, step_num):
72 | 'first step matrix exp, other steps matrix multiplication'
73 |
74 | Timers.tic('init_matrices')
75 | if self.one_step_matrix_exp is None:
76 | self.init_matrices()
77 | Timers.toc('init_matrices')
78 |
79 | if step_num == 0: # step zero, basis matrix is identity matrix
80 | self.cur_basis_matrix = np.identity(self.dims, dtype=float)
81 | self.cur_input_effects_matrix = None
82 | elif step_num == 1:
83 | self.cur_basis_matrix = self.one_step_matrix_exp
84 | self.cur_input_effects_matrix = self.one_step_input_effects_matrix
85 |
86 | if self.use_lgg and self.b_csc is not None:
87 | prev_step_mat_exp = np.identity(self.dims, dtype=float)
88 | # make new (wider) input effects matrix
89 | blocks = [self.cur_input_effects_matrix, prev_step_mat_exp]
90 | self.cur_input_effects_matrix = np.concatenate(blocks, axis=1)
91 |
92 | elif step_num == self.cur_step + 1:
93 | Timers.tic('quick_step')
94 | prev_step_mat_exp = self.cur_basis_matrix
95 | self.cur_basis_matrix = np.dot(self.cur_basis_matrix, self.one_step_matrix_exp)
96 |
97 | # inputs
98 | if self.b_csc is not None:
99 | if self.use_lgg:
100 | # cut cur_input_effects matrix into the relevant portion
101 | self.cur_input_effects_matrix = self.cur_input_effects_matrix[:, 0:self.time_elapser.inputs]
102 |
103 | self.cur_input_effects_matrix = np.dot(self.one_step_matrix_exp, self.cur_input_effects_matrix)
104 |
105 | if self.use_lgg:
106 | # make new (wider) input effects matrix
107 | blocks = [self.cur_input_effects_matrix, prev_step_mat_exp]
108 | self.cur_input_effects_matrix = np.concatenate(blocks, axis=1)
109 |
110 | Timers.toc('quick_step')
111 | else:
112 | Timers.tic('slow_step')
113 |
114 | Timers.tic('expm')
115 | # compute one step behind, because this is what's used by input effects matrix
116 | prev_step_mat_exp = expm(self.a_csc * (step_num-1) * self.time_elapser.step_size)
117 | Timers.toc('expm')
118 |
119 | # advance one step to get current basis matrix
120 | self.cur_basis_matrix = np.dot(prev_step_mat_exp.toarray(), self.one_step_matrix_exp)
121 |
122 | # inputs
123 | if self.b_csc is not None:
124 | Timers.tic("input effects")
125 | self.cur_input_effects_matrix = prev_step_mat_exp * self.one_step_input_effects_matrix
126 | Timers.toc("input effects")
127 |
128 | if self.use_lgg:
129 | # make new (wider) input effects matrix
130 | blocks = [self.cur_input_effects_matrix, prev_step_mat_exp]
131 | self.cur_input_effects_matrix = np.concatenate(blocks, axis=1)
132 |
133 | Timers.toc('slow_step')
134 |
135 | self.cur_step = step_num
136 |
137 | def use_lgg_approx(self):
138 | '''
139 | set this time elapse object to use lgg approximation model
140 | '''
141 |
142 | self.use_lgg = True
143 | self.one_step_input_effects_matrix = self.b_csc.toarray() * self.time_elapser.step_size
144 |
--------------------------------------------------------------------------------
/hylaa/symbolic.py:
--------------------------------------------------------------------------------
1 | '''
2 | Hylaa Symoblic dynamics construction
3 |
4 | Construct A matrix and reset matrix / rhs from symbolic expressions
5 |
6 | Stanley Bak
7 | Nov 2018
8 | '''
9 |
10 | import sympy
11 | from sympy.parsing.sympy_parser import parse_expr
12 | from sympy import Mul, Expr, Add, Symbol, Number
13 |
14 | def extract_linear_terms(e, variables, has_affine_variable):
15 | '''extract linear terms from a flat sympy expression
16 |
17 | returns a list of numbers, one for each variable
18 | '''
19 |
20 | rv = [0] * len(variables)
21 |
22 | if has_affine_variable:
23 | rv.append(0)
24 |
25 | if not isinstance(e, Expr):
26 | raise RuntimeError("Expected sympy Expr: " + repr(e))
27 |
28 | try:
29 | _extract_linear_terms_rec(e, variables, rv, has_affine_variable)
30 | except RuntimeError as ex:
31 | raise RuntimeError(str(ex) + ", while parsing " + str(e))
32 |
33 | return rv
34 |
35 | def _extract_linear_terms_rec(e, variables, rv, has_affine_variable):
36 | 'extract linear terms'
37 |
38 | if isinstance(e, Add):
39 | _extract_linear_terms_rec(e.args[0], variables, rv, has_affine_variable)
40 |
41 | for arg in e.args[1:]:
42 | _extract_linear_terms_rec(arg, variables, rv, has_affine_variable)
43 | elif isinstance(e, Number):
44 | val = float(e)
45 |
46 | if val != 0:
47 | if not has_affine_variable:
48 | raise RuntimeError(f"expression has affine variables but has_affine_variable was False: '{e}'")
49 |
50 | rv[-1] += val
51 | elif isinstance(e, Symbol):
52 | try:
53 | index = variables.index(e.name)
54 | except ValueError:
55 | raise RuntimeError(f"variable {e.name} not found in variable list: {variables}")
56 |
57 | rv[index] += 1
58 | elif isinstance(e, Mul):
59 | if len(e.args) != 2:
60 | raise RuntimeError(f"expected multiplication term with exactly two arguments: '{e}'")
61 |
62 | sym_term = None
63 | num_term = None
64 |
65 | if isinstance(e.args[0], Number) and isinstance(e.args[1], Symbol):
66 | num_term = e.args[0]
67 | sym_term = e.args[1]
68 | elif isinstance(e.args[1], Number) and isinstance(e.args[0], Symbol):
69 | num_term = e.args[1]
70 | sym_term = e.args[0]
71 | else:
72 | raise RuntimeError(f"expected multiplication with one number and one variable: '{e}'")
73 |
74 | try:
75 | index = variables.index(sym_term.name)
76 | except ValueError:
77 | raise RuntimeError(f"variable {sym_term.name} not found in variable list: {variables}")
78 |
79 | rv[index] += float(num_term)
80 | else:
81 | raise RuntimeError(f"unsupported term of type {type(e)}: '{e}'")
82 |
83 | def make_reset_mat(variables, resets, constant_dict, has_affine_variable=False):
84 | 'make the matrix for a reset operation'
85 |
86 | mat = make_dynamics_mat(variables, resets, constant_dict, has_affine_variable=has_affine_variable)
87 |
88 | if has_affine_variable:
89 | # affine variable should be identity reset, rather than all 0's as in dynamics
90 | mat[-1][-1] = 1.0
91 |
92 | return mat
93 |
94 | def make_dynamics_mat(variables, derivatives, constant_dict, has_affine_variable=False):
95 | '''make the dynamics A matrix from the list of variables, derivatives, and a dict mapping constants to values
96 |
97 | returns a list of lists (a matrix) of size len(variables) by len(variables)
98 | '''
99 |
100 | rv = []
101 | subs = {}
102 | symbol_dict = {}
103 |
104 | for var in variables:
105 | symbol_dict[var] = sympy.symbols(var)
106 |
107 | for var, value in constant_dict.items():
108 | sym_var = sympy.symbols(var)
109 | subs[sym_var] = value
110 | symbol_dict[var] = sym_var
111 |
112 | for der in derivatives:
113 | sym_der = parse_expr(der, local_dict=symbol_dict)
114 |
115 | sym_der = sym_der.subs(subs)
116 |
117 | row = extract_linear_terms(sym_der, variables, has_affine_variable)
118 | rv.append(row)
119 |
120 | if has_affine_variable:
121 | rv.append([0] * (len(variables) + 1))
122 |
123 | return rv
124 |
125 | def make_condition(variables, condition_list, constant_dict, has_affine_variable=False):
126 | '''make a condition matrix and right-hand-side (rhs) from a set of string conditions.
127 | condition_list is a list of strings with a single '<=' or '>=' condition like 'x - 1 + y <= 2 * x + 3'
128 |
129 | returns a 2-tuple: (mat, rhs)
130 | '''
131 |
132 | assert isinstance(condition_list, list), "condition_list should be a list of string conditions"
133 |
134 | mat = []
135 | rhs = []
136 | subs = {}
137 | symbol_dict = {}
138 |
139 | for var in variables:
140 | symbol_dict[var] = sympy.symbols(var)
141 |
142 | for var, value in constant_dict.items():
143 | sym_var = sympy.symbols(var)
144 | subs[sym_var] = value
145 | symbol_dict[var] = sym_var
146 |
147 | for cond in condition_list:
148 | less_than_count = cond.count('<=')
149 | greater_than_count = cond.count('>=')
150 |
151 | if less_than_count + greater_than_count != 1:
152 | raise RuntimeError(f"Expected condition with single '<=' or '>=': {cond}")
153 |
154 | if greater_than_count == 1:
155 | cond = cond.replace(">=", "<=")
156 |
157 | left, right = cond.split("<=")
158 |
159 | # swap left and right for '>=' expressions
160 | if greater_than_count == 1:
161 | left, right = right, left
162 |
163 | # make the expression: left - (right) <= 0
164 | subtract_cond = f"{left} - ({right})"
165 |
166 | sym_cond = parse_expr(subtract_cond, local_dict=symbol_dict)
167 |
168 | # substitute in constants
169 | sym_cond = sym_cond.subs(subs)
170 |
171 | terms = extract_linear_terms(sym_cond, variables, has_affine_variable=True)
172 | row = terms[:-1]
173 |
174 | if has_affine_variable:
175 | row.append(0)
176 |
177 | mat.append(row)
178 | rhs.append(-1 * terms[-1])
179 |
180 | return mat, rhs
181 |
--------------------------------------------------------------------------------
/hylaa/settings.py:
--------------------------------------------------------------------------------
1 | '''
2 | Hylaa Settings File
3 | Stanley Bak, 2018
4 | '''
5 |
6 | import math
7 |
8 | from matplotlib import animation
9 |
10 | from hylaa.util import Freezable
11 | from hylaa import aggstrat
12 |
13 | # Force matplotlib to not use any Xwindows backend.
14 | #import matplotlib
15 | #matplotlib.use('Agg')
16 |
17 | # Suppress floating point printing overflow/underflow printing in numpy
18 | #import numpy as np
19 | #np.set_printoptions(suppress=True)
20 |
21 | class HylaaSettings(Freezable): # pylint: disable=too-few-public-methods
22 | 'Settings for the computation'
23 |
24 | STDOUT_NONE, STDOUT_NORMAL, STDOUT_VERBOSE, STDOUT_DEBUG = range(4)
25 |
26 | # Approximation Models: None: discrete-time, chull: convex hull only (different simulation semantics),
27 | # lgg: support function method from Le Guernic'10
28 | APPROX_NONE, APPROX_CHULL, APPROX_LGG = range(3)
29 |
30 | def __init__(self, step_size, max_time):
31 | plot_settings = PlotSettings()
32 |
33 | self.step_size = step_size # simulation step size
34 | self.num_steps = int(math.ceil(max_time / step_size))
35 |
36 | self.plot = plot_settings
37 | self.stdout = HylaaSettings.STDOUT_NORMAL
38 | self.stdout_colors = [None, "white", "blue", "yellow"] # colors for each level of printing
39 |
40 | ### SIMULATION-EQUIVALENT SEMANTICS / COMPUTATION PARAMETERS ###
41 | self.process_urgent_guards = False # allow zero continuous-post steps between transitions?
42 | self.do_guard_strengthening = True # add invariants of target modes to each guard?
43 | self.optimize_tt_transitions = True # auto-detect time-triggered transitions and use single-step semantics?
44 | self.approx_model = HylaaSettings.APPROX_NONE
45 | self.skip_zero_dynamics_modes = True
46 |
47 | # what to do when an error appears reachable
48 | self.stop_on_aggregated_error = False # stop whenever any state (aggregated or not) reaches an error mode
49 | self.stop_on_concrete_error = True # stop whenver a concrete state reaches an error
50 | self.make_counterexample = True # save counter-example to data structure / file?
51 |
52 | self.aggstrat = aggstrat.Aggregated() # aggregation strategy class
53 |
54 | # for deterministic random numbers (simulations / color selection)
55 | self.random_seed = 0
56 |
57 | self.freeze_attrs()
58 |
59 | class PlotSettings(Freezable): # pylint: disable=too-few-public-methods,too-many-instance-attributes
60 | 'plot settings container'
61 |
62 | PLOT_NONE = 0 # don't plot (safety checking only; for performance measurement)
63 | PLOT_LIVE = 1 # plot the computation live as we're computing
64 | PLOT_INTERACTIVE = 2 # live plotting with pauses and buttons upon certain events
65 | PLOT_IMAGE = 3 # save the image plot to a file
66 | PLOT_VIDEO = 4 # save a video to a file
67 |
68 | def __init__(self):
69 | self.plot_mode = PlotSettings.PLOT_NONE
70 |
71 | self.store_plot_result = False # store the reachable plot data inside the computation result object?
72 |
73 | self.filename = None # filename to print data to for certain plot modes
74 |
75 | self.plot_size = (8, 8) # inches
76 |
77 | # these settings can be lists, in which case we'll make multiple plots
78 | self.xdim_dir = 0 # plotting x dimension number, direction (np.array), None (time), or dict: mode_name -> dir
79 | self.ydim_dir = 1 # plotting y dimension number, direction (np.array), None (time), or dict: mode_name -> dir
80 | self.label = LabelSettings() # plot title, axis labels, font sizes, ect.
81 | self.extra_collections = None # list of extra animation collections
82 |
83 | self.num_angles = 512 # how many evenly-spaced angles to put into plot_vecs
84 |
85 | self.draw_stride = 1 # draw every n frames (good to inscrease if number of steps is large)
86 |
87 | self.reachable_poly_width = 2 # width of reachable polygon outlines
88 | self.extend_plot_range_ratio = 0.1 # extend plot axis range 10% at a time
89 |
90 | self.sim_line_color = 'black' # simulation line color
91 | self.sim_line_width = 0.2 # width of simulation lines
92 |
93 | self.grid = True
94 | self.grid_xtics = None # override default xtics value, for example np.linspace(0.0, 5.0, 1.0)
95 | self.grid_ytics = None # override default ytics value, for example np.linspace(0.0, 5.0, 1.0)
96 |
97 | self.use_markers_for_small = True # draw markers when the reachable set is tiny instead of (invisible) polygons
98 |
99 | self.show_counterexample = True # draw concrete counter-example in the last frame?
100 |
101 | self.interactive_skip_count = 0 # when using PLOT_INTERACTIVE, auto-click 'next' this many times
102 |
103 | self.video_fps = 40
104 | self.video_extra_frames = 40 # extra frames at the end of a video so it doesn't end so abruptly
105 | self.video_pause_frames = 20 # frames to render in video whenever a 'pause' occurs
106 | self.video_show_frame = True # show the frame counter?
107 |
108 | # function which returns the Writer with the desired settings used to create a video, used for video export
109 | def make_video_writer():
110 | 'returns the Writer to create a video for export'
111 |
112 | writer_class = animation.writers['ffmpeg']
113 | return writer_class(fps=self.video_fps, metadata=dict(artist='Me'), bitrate=1800)
114 |
115 | self.make_video_writer_func = make_video_writer
116 |
117 | self.freeze_attrs()
118 |
119 | class LabelSettings(Freezable):
120 | 'settings for labels such as plot title, plot font size, ect.'
121 |
122 | def __init__(self):
123 | self.x_label = None
124 | self.y_label = None
125 | self.title = None
126 |
127 | self.title_size = 32
128 | self.label_size = 24
129 | self.tick_label_size = 18
130 | self.axes_limits = None # fixed axes limits; a 4-tuple (xmin, xmax, ymin, ymax) or None for auto
131 |
132 | self.freeze_attrs()
133 |
134 | def big(self, size=30):
135 | 'increase sizes of labels'
136 |
137 | self.title_size = size
138 | self.label_size = size
139 | self.tick_label_size = int(0.8 * size)
140 |
141 | def turn_off(self):
142 | 'turn off plot labels'
143 |
144 | self.x_label = ''
145 | self.y_label = ''
146 | self.title = ''
147 |
--------------------------------------------------------------------------------
/hylaa/kamenev.py:
--------------------------------------------------------------------------------
1 | '''
2 | Functions related to Kamenev's method for polytope approximation (the method of refined bounds)
3 |
4 | Stanley Bak
5 | May 16, 2019
6 | '''
7 |
8 | import math
9 |
10 | import numpy as np
11 | import scipy as sp
12 | from scipy.spatial import ConvexHull
13 |
14 | from hylaa.timerutil import Timers
15 |
16 | def _get_orthonormal_rank(vecs, tol=1e-7):
17 | '''
18 | given a list of vecs, return a new vector orthonormal to them and the rank of the matrix
19 | '''
20 |
21 | _, s, v = np.linalg.svd(vecs)
22 |
23 | index = 0
24 |
25 | while index < len(s) and s[index] > tol:
26 | index += 1
27 |
28 | if index == len(v):
29 | rv_vec = None # the vectors span the space
30 | else:
31 | rv_vec = v[index]
32 |
33 | return rv_vec, index
34 |
35 | def _get_rank(vecs, tol=1e-7):
36 | '''get the rank of the passed in matrix'''
37 |
38 | return _get_orthonormal_rank(vecs, tol=tol)[1]
39 |
40 | def _find_two_points(dims, supp_point_func):
41 | '''find two points in the the convex set defined through supp_point_func (which may be degenerate)
42 |
43 | if len(pts) == 1, the convex set is a degenerate set consisting of a single pt
44 | '''
45 |
46 | pts = []
47 |
48 | for d in range(dims):
49 | vec = np.array([-1 if i == d else 0 for i in range(dims)], dtype=float)
50 |
51 | # try min
52 | p1 = supp_point_func(vec)
53 | assert len(p1) == dims, f"support fuction returned {len(p1)}-dimensional point, expected {dims}-d"
54 |
55 | pts = [p1]
56 |
57 | # try max
58 | vec = np.array([1 if i == d else 0 for i in range(dims)], dtype=float)
59 | p2 = supp_point_func(vec)
60 |
61 | if not np.allclose(p1, p2):
62 | pts = [p1, p2]
63 | break
64 |
65 | return pts
66 |
67 | def _find_init_simplex(dims, supp_point_func):
68 | '''
69 | find an n-dimensional initial simplex
70 | '''
71 |
72 | Timers.tic('init_simplex')
73 |
74 | # first, construct the initial simplex and determine a basis for the convex set (it may be degenerate)
75 | init_simplex = _find_two_points(dims, supp_point_func)
76 |
77 | if len(init_simplex) == 2: # S may be a degenerate shape consisting of a single point
78 | init_vec = init_simplex[1] - init_simplex[0]
79 |
80 | spanning_dirs = [init_vec]
81 | degenerate_dirs = []
82 | vecs = [init_vec]
83 |
84 | for _ in range(dims - 1):
85 | new_dir, rank = _get_orthonormal_rank(vecs)
86 |
87 | # min/max in direction v, checking if it increases the rank of vecs
88 | pt = supp_point_func(new_dir)
89 | vecs.append(pt - init_simplex[0])
90 |
91 | if _get_rank(vecs) > rank:
92 | init_simplex.append(pt)
93 | spanning_dirs.append(vecs[-1])
94 | continue
95 |
96 | # rank did not increase with maximize, try minimize
97 | vecs = vecs[0:-1] # pop vec
98 |
99 | pt = supp_point_func(-1 * new_dir)
100 | vecs.append(pt - init_simplex[0])
101 |
102 | if _get_rank(vecs) > rank:
103 | init_simplex.append(pt)
104 | spanning_dirs.append(vecs[-1])
105 | continue
106 |
107 | # rank still didn't increase, new_dir is orthogonal to shape S
108 | vecs = vecs[0:-1] # pop vec
109 |
110 | vecs.append(new_dir) # forces a new orthonormal direction during the next iteration
111 | degenerate_dirs.append(new_dir)
112 |
113 | Timers.toc('init_simplex')
114 |
115 | return init_simplex
116 |
117 | def get_verts(dims, supp_point_func, epsilon=1e-7):
118 | '''
119 | get the n-dimensional vertices of the convex set defined through supp_point_func (which may be degenerate)
120 | '''
121 |
122 | init_simplex = _find_init_simplex(dims, supp_point_func)
123 |
124 | if len(init_simplex) < 3:
125 | return init_simplex # for 0-d and 1-d sets, the init_simplex corners are the only possible extreme points
126 |
127 | rv, _ = _v_h_rep_given_init_simplex(init_simplex, supp_point_func, epsilon=epsilon)
128 |
129 | return rv
130 |
131 | def _v_h_rep_given_init_simplex(init_simplex, supp_point_func, epsilon=1e-7):
132 | '''get all the vertices and hyperplanes of (an epsilon approximation of) the set, defined through supp_point_func
133 |
134 | This function is provided with an initial simplex which spans the space
135 |
136 | this returns verts, equations, where equations is from the Convex Hull's (hull.equations)
137 | '''
138 |
139 | new_pts = init_simplex
140 |
141 | verts = []
142 | iteration = 0
143 | max_error = None
144 |
145 | while new_pts:
146 | iteration += 1
147 | #print(f"\nIteration {iteration}. Verts: {len(verts)}, new_pts: {len(new_pts)}, max_error: {max_error}")
148 |
149 | first_new_index = len(verts)
150 | verts += new_pts
151 | new_pts = []
152 | max_error = 0
153 |
154 | Timers.tic('ConvexHull')
155 | hull = ConvexHull(verts)
156 | Timers.toc('ConvexHull')
157 |
158 | for i, simplex in enumerate(hull.simplices):
159 | is_new = False
160 |
161 | for index in simplex:
162 | if index >= first_new_index:
163 | is_new = True
164 | break
165 |
166 | if not is_new:
167 | continue # skip this simplex
168 |
169 | # get hyperplane for simplex
170 | normal = hull.equations[i, :-1]
171 | rhs = -1 * hull.equations[i, -1]
172 |
173 | Timers.tic('supp_point_func')
174 | supporting_pt = supp_point_func(normal)
175 | Timers.toc('supp_point_func')
176 |
177 | error = np.dot(supporting_pt, normal) - rhs
178 | max_error = max(max_error, error)
179 |
180 | if error <= -1e-4:
181 | print(f"Kamenev Plot warning: supporting point was inside facet? error was {error}")
182 |
183 | if error >= epsilon:
184 | # add the point
185 | already_added = False
186 |
187 | Timers.tic("check if new point")
188 | for pt in new_pts:
189 | if np.allclose(pt, supporting_pt):
190 | already_added = True
191 | break
192 | Timers.toc("check if new point")
193 |
194 | if not already_added:
195 | new_pts.append(supporting_pt)
196 |
197 | return np.array(verts, dtype=float), hull.equations
198 |
--------------------------------------------------------------------------------
/tests/test_misc.py:
--------------------------------------------------------------------------------
1 | '''
2 | tests for misc aspects of hylaa
3 | '''
4 |
5 | import math
6 | import matplotlib.pyplot as plt
7 |
8 | import numpy as np
9 |
10 | from hylaa import symbolic, lputil, lpplot
11 | from hylaa.hybrid_automaton import HybridAutomaton
12 | from hylaa.stateset import StateSet
13 | from hylaa.settings import HylaaSettings
14 |
15 | from util import assert_verts_equals
16 |
17 | def test_step_slow():
18 | 'tests slow-step with non-one step size'
19 |
20 | mode = HybridAutomaton().new_mode('mode_name')
21 | mode.set_dynamics(np.identity(2))
22 | mode.set_inputs([[1, 0], [0, 2]], [[1, 0], [-1, 0], [0, 1], [0, -1]], [10, -1, 10, -1])
23 |
24 | mode.init_time_elapse(0.5)
25 |
26 | # do step 1
27 | _, _ = mode.time_elapse.get_basis_matrix(1)
28 |
29 | # do step 2
30 | basis_mat, input_mat = mode.time_elapse.get_basis_matrix(2)
31 |
32 | # do step 3
33 | _, _ = mode.time_elapse.get_basis_matrix(3)
34 |
35 | # go back to step 2 (slow step) and make sure it matches
36 | slow_basis_mat, slow_input_mat = mode.time_elapse.get_basis_matrix(2)
37 |
38 | assert np.allclose(basis_mat, slow_basis_mat)
39 | assert np.allclose(input_mat, slow_input_mat)
40 |
41 | def test_symbolic_amat():
42 | 'test symbolic dynamics extraction'
43 |
44 | constant_dict = {'alpha': 10}
45 |
46 | variables = ['x', 'y']
47 |
48 | derivatives = ['0', 'x', '-x', 'x + y', 'x - y', '3*y + 2*x', '2*x - y', 'alpha*x', 'alpha**2*y + 1/(2*alpha) * x']
49 | expected = [[0, 0], [1, 0], [-1, 0], [1, 1], [1, -1], [2, 3], [2, -1], [10, 0], [0.05, 100]]
50 |
51 | for der, row in zip(derivatives, expected):
52 | ders = [der, '0']
53 |
54 | a_mat = symbolic.make_dynamics_mat(variables, ders, constant_dict)
55 |
56 | assert np.allclose(a_mat[0], row)
57 |
58 | # check with and without affine term
59 | variables = ['x', 'y']
60 | ders = ['x - alpha * alpha / 2 + 2* y ', 'y']
61 | a_mat = symbolic.make_dynamics_mat(variables, ders, constant_dict, has_affine_variable=True)
62 |
63 | print(f"a_mat:\n{a_mat}")
64 |
65 | expected = np.array([[1, 2, -50], [0, 1, 0], [0, 0, 0]], dtype=float)
66 |
67 | assert np.allclose(a_mat, expected)
68 |
69 | # check errors
70 | try:
71 | symbolic.make_dynamics_mat(['x', 'y'], ['x + y', 'x * 2 * y'], constant_dict)
72 |
73 | assert False, "expected RuntimeError (nonlinear)"
74 | except RuntimeError:
75 | pass
76 |
77 | try:
78 | symbolic.make_dynamics_mat(['x', 'y'], ['x + y', 'x + y + alpha'], constant_dict)
79 |
80 | assert False, "expected RuntimeError (no affine variable)"
81 | except RuntimeError:
82 | pass
83 |
84 | a_mat = symbolic.make_dynamics_mat(['x', 'y'], ['x + y', 'x + y + alpha'], constant_dict, has_affine_variable=True)
85 | expected = np.array([[1, 1, 0], [1, 1, 10], [0, 0, 0]], dtype=float)
86 |
87 | assert np.allclose(a_mat, expected)
88 |
89 | def test_symbolic_condition():
90 | 'test symbolic extraction of a condition A x <= b'
91 |
92 | constant_dict = {'deltap': 0.5}
93 | variables = ['px', 'py']
94 |
95 | orig = "px<=deltap & py<=-px*0.7 & py>=px*0.8 + 5.0"
96 |
97 | cond_list = orig.split('&')
98 |
99 | mat, rhs = symbolic.make_condition(variables, cond_list, constant_dict)
100 | expected_mat = np.array([[1, 0], [0.7, 1], [0.8, -1]], dtype=float)
101 | expected_rhs = [0.5, 0, -5]
102 |
103 | assert np.allclose(mat, expected_mat)
104 | assert np.allclose(rhs, expected_rhs)
105 |
106 | for cond in ["0 <= x <= 1", "0 < x", "0 >= y >= -1", "0 <= x >= 0"]:
107 | try:
108 | symbolic.make_condition(["x", "y"], [cond], {})
109 | assert False, f"expected exception on condition {cond}"
110 | except RuntimeError:
111 | pass
112 |
113 | # try again
114 | cond_list = ['I >= 20']
115 | mat, rhs = symbolic.make_condition(['x', 'I', 'z'], cond_list, constant_dict, has_affine_variable=True)
116 | expected_mat = np.array([[0, -1, 0, 0]], dtype=float)
117 | expected_rhs = [-20]
118 | assert np.allclose(mat, expected_mat)
119 | assert np.allclose(rhs, expected_rhs)
120 |
121 | def test_approx_lgg_inputs():
122 | 'test lgg approximation model with inputs'
123 |
124 | # simple dynamics, x' = 1, y' = 0 + u, a' = 0, u in [0.1, 0.2]
125 | # step size (tau) 0.02
126 | # after one step, the input effect size should by tau*V \oplus beta*B
127 | # we'll manually assign beta to be 0.02, in order to be able to check that the constraints are correct
128 | # A norm is 1
129 |
130 | tau = 0.05
131 |
132 | a_matrix = [[0, 0, 1], [0, 0, 0], [0, 0, 0]]
133 | b_mat = [[0], [1], [0]]
134 | b_constraints = [[1], [-1]]
135 | b_rhs = [0.2, -0.1]
136 |
137 | mode = HybridAutomaton().new_mode('mode')
138 | mode.set_dynamics(a_matrix)
139 | mode.set_inputs(b_mat, b_constraints, b_rhs)
140 |
141 | init_lpi = lputil.from_box([[0, 0], [0, 0], [1, 1]], mode)
142 | assert lputil.compute_radius_inf(init_lpi) == 1
143 |
144 | ss = StateSet(init_lpi, mode)
145 | mode.init_time_elapse(tau)
146 | assert_verts_equals(lpplot.get_verts(ss.lpi), [(0, 0)])
147 |
148 | ss.apply_approx_model(HylaaSettings.APPROX_LGG)
149 |
150 | assert np.linalg.norm(a_matrix, ord=np.inf) == 1.0
151 |
152 | v_set = lputil.from_input_constraints(mode.b_csr, mode.u_constraints_csc, mode.u_constraints_rhs, mode)
153 | assert lputil.compute_radius_inf(v_set) == 0.2
154 | alpha = (math.exp(tau) - 1 - tau) * (1 + 0.2)
155 |
156 | assert_verts_equals(lpplot.get_verts(ss.lpi), \
157 | [(0, 0), (tau-alpha, 0.2*tau + alpha), (tau+alpha, 0.2*tau+alpha), (tau+alpha, 0.1*tau-alpha)])
158 |
159 | # note: c gets bloated by alpha as well
160 | assert (ss.lpi.minimize([0, 0, -1])[ss.lpi.cur_vars_offset + 2]) - (1 + alpha) < 1e-9
161 | assert (ss.lpi.minimize([0, 0, 1])[ss.lpi.cur_vars_offset + 2]) - (1 - alpha) < 1e-9
162 |
163 | # c is actually growing, starting at (1,1) at x=0 and going to [1-alpha, 1+alpha] at x=tau
164 | assert_verts_equals(lpplot.get_verts(ss.lpi, xdim=0, ydim=2), \
165 | [(0, 1), (tau-alpha, 1+alpha), (tau+alpha, 1+alpha), (tau+alpha, 1-alpha), (tau-alpha, 1-alpha)])
166 |
167 | # ready to start
168 | ss.step()
169 |
170 | beta = (math.exp(tau) - 1 - tau) * 0.2
171 |
172 | # note: c gets bloated as well! so now it's [1-epsilon, 1+epsilon], where epsilon=alpha
173 | # so x will grow by [tau * (1 - alpha), tau * (1 + alpha)]
174 | expected = [(tau + beta, -beta + tau * 0.1), \
175 | (tau - beta, -beta + tau * 0.1), \
176 | (tau - beta, beta + tau * 0.2), \
177 | ((tau - alpha) + tau * (1 - alpha) - beta, 2*0.2*tau + alpha + beta), \
178 | ((tau + alpha) + tau * (1 + alpha) + beta, 2*0.2*tau+alpha + beta), \
179 | ((tau + alpha) + tau * (1 + alpha) + beta, 2*0.1*tau-alpha - beta)]
180 |
181 | #xs, ys = zip(*expected)
182 | #plt.plot([x for x in xs] + [xs[0]], [y for y in ys] + [ys[0]], 'r-') # expected is red
183 |
184 | verts = lpplot.get_verts(ss.lpi)
185 | #xs, ys = zip(*verts)
186 | #plt.plot(xs, ys, 'k-+') # computed is black
187 | #plt.show()
188 |
189 | assert_verts_equals(verts, expected)
190 |
191 | # one more step should work without errors
192 | ss.step()
193 |
--------------------------------------------------------------------------------
/hylaa/timerutil.py:
--------------------------------------------------------------------------------
1 | '''
2 | Timer utility functions for Hylaa. Timers are used for performance analysis and
3 | can be refered to statically using Timers.tic(name) and Timers.toc(name)
4 |
5 | Stanley Bak
6 | September 2016
7 | '''
8 |
9 | import time
10 |
11 | from termcolor import cprint
12 |
13 | class TimerData():
14 | 'Performance timer which can be started with tic() and paused with toc()'
15 |
16 | def __init__(self, name, parent):
17 | assert parent is None or isinstance(parent, TimerData)
18 |
19 | self.name = name
20 | self.total_secs = 0
21 | self.num_calls = 0
22 | self.last_start_time = None
23 |
24 | self.parent = parent # parent TimerData, None for top-level timers
25 | self.children = [] # a list of child TimerData
26 |
27 | def get_child(self, name):
28 | 'get a child timer with the given name'
29 |
30 | rv = None
31 |
32 | for child in self.children:
33 | if child.name == name:
34 | rv = child
35 | break
36 |
37 | return rv
38 |
39 | def get_children_recursive(self, name):
40 | 'get all decendants with the given name (returns a list of TimerData)'
41 |
42 | rv = []
43 |
44 | if name == self.name:
45 | rv.append(self)
46 |
47 | for child in self.children:
48 | rv += child.get_children_recursive(name)
49 |
50 | return rv
51 |
52 | def full_name(self):
53 | 'get the full name of the timer (including ancestors)'
54 |
55 | if self.parent is None:
56 | return self.name
57 |
58 | return "{}.{}".format(self.parent.full_name(), self.name)
59 |
60 | def tic(self):
61 | 'start the timer'
62 |
63 | #print "Tic({})".format(self.name)
64 |
65 | if self.last_start_time is not None:
66 | raise RuntimeError("Timer started twice: {}".format(self.name))
67 |
68 | self.num_calls += 1
69 | self.last_start_time = time.perf_counter()
70 |
71 | def toc(self):
72 | 'stop the timer'
73 |
74 | #print "Toc({})".format(self.name)
75 |
76 | if self.last_start_time is None:
77 | raise RuntimeError("Timer stopped without being started: {}".format(self.name))
78 |
79 | self.total_secs += time.perf_counter() - self.last_start_time
80 | self.last_start_time = None
81 |
82 | class Timers():
83 | '''
84 | a static class for doing timer messuarements. Use
85 | Timers.tic(name) and Timers.tic(name) to start and stop timers, use
86 | print_stats to print time statistics
87 | '''
88 |
89 | top_level_timer = None
90 |
91 | stack = [] # stack of currently-running timers, parents at the start, children at the end
92 |
93 | def __init__(self):
94 | raise RuntimeError('Timers is a static class; should not be instantiated')
95 |
96 | @staticmethod
97 | def reset():
98 | 'reset all timers'
99 |
100 | Timers.top_level_timer = None
101 | Timers.stack = []
102 |
103 | @staticmethod
104 | def tic(name):
105 | 'start a timer'
106 |
107 | #print("Tic({})".format(name))
108 |
109 | if not Timers.stack:
110 | top = Timers.top_level_timer
111 |
112 | if top is not None and top.name != name:
113 | # overwrite old top level timer
114 | #print("Overwriting old top-level timer {} with new top-level timer {}".format(top.name, name))
115 | top = Timers.top_level_timer = None
116 |
117 | td = top
118 | else:
119 | td = Timers.stack[-1].get_child(name)
120 |
121 | # create timer object if it doesn't exist
122 | if td is None:
123 | parent = None if not Timers.stack else Timers.stack[-1]
124 | td = TimerData(name, parent)
125 |
126 | if not Timers.stack:
127 | Timers.top_level_timer = td
128 | else:
129 | Timers.stack[-1].children.append(td)
130 |
131 | td.tic()
132 | Timers.stack.append(td)
133 |
134 | @staticmethod
135 | def toc(name):
136 | 'stop a timer'
137 |
138 | #print("Toc({})".format(name))
139 |
140 | assert Timers.stack[-1].name == name, "Out of order toc(). Expected to first stop timer {}".format(
141 | Timers.stack[-1].full_name())
142 |
143 | Timers.stack[-1].toc()
144 | Timers.stack.pop()
145 |
146 | @staticmethod
147 | def print_stats():
148 | 'print statistics about performance timers to stdout'
149 |
150 | Timers.print_stats_recursive(Timers.top_level_timer, 0, None)
151 |
152 | @staticmethod
153 | def print_stats_recursive(td, level, total_time):
154 | 'recursively print information about a timer'
155 |
156 | low_threshold = 5.0
157 | high_threshold = 50.0
158 |
159 | if level == 0:
160 | total_time = td.total_secs
161 |
162 | percent_total = 100 * td.total_secs / total_time
163 |
164 | if percent_total < low_threshold:
165 | def print_func(text):
166 | 'below threshold print function'
167 |
168 | return cprint(text, 'grey')
169 | elif percent_total > high_threshold:
170 | def print_func(text):
171 | 'above threshold print function'
172 |
173 | return cprint(text, None, attrs=['bold'])
174 | else:
175 | def print_func(text):
176 | 'within threshold print function'
177 |
178 | return cprint(text, None)
179 |
180 | if td.last_start_time is not None:
181 | raise RuntimeError("Timer was never stopped: {}".format(td.name))
182 |
183 | if td.parent is None:
184 | percent = 100
185 | percent_str = ""
186 | else:
187 | percent = 100 * td.total_secs / td.parent.total_secs
188 | percent_str = " ({:.1f}%)".format(percent)
189 |
190 | print_func("{}{} Time ({} calls): {:.2f} sec{}".format(" " * level * 2, \
191 | td.name.capitalize(), td.num_calls, td.total_secs, percent_str))
192 |
193 | total_children_secs = 0
194 |
195 | for child in td.children:
196 | total_children_secs += child.total_secs
197 |
198 | Timers.print_stats_recursive(child, level + 1, total_time)
199 |
200 | if td.children:
201 | other = td.total_secs - total_children_secs
202 | other_percent = 100 * other / td.total_secs
203 |
204 | percent_other = other / total_time
205 |
206 | if percent_other < low_threshold:
207 | def other_print_func(text):
208 | 'below threshold print function'
209 |
210 | return cprint(text, 'grey')
211 | elif percent_other > high_threshold:
212 | def other_print_func(text):
213 | 'above threshold print function'
214 |
215 | return cprint(text, None, attrs=['bold'])
216 | else:
217 | def other_print_func(text):
218 | 'within threshold print function'
219 |
220 | return cprint(text, None)
221 |
222 | percent_str = " ({:.1f}%)".format(other_percent)
223 |
224 | other_print_func("{}Other: {:.2f} sec{}".format(" " * (level + 1) * 2, \
225 | other, percent_str))
226 |
--------------------------------------------------------------------------------
/hylaa/guard_opt_data.py:
--------------------------------------------------------------------------------
1 | '''
2 | Stanley Bak
3 | Hylaa-Continuous Guard Optimization Logic
4 | July 2017
5 | '''
6 |
7 | import numpy as np
8 |
9 | from scipy.sparse import csc_matrix
10 |
11 | from hylaa.util import Freezable
12 | from hylaa.hybrid_automaton import LinearAutomatonMode
13 | from hylaa.settings import PlotSettings
14 | from hylaa.glpk_interface import LpInstance
15 | from hylaa.timerutil import Timers
16 |
17 | class GuardOptData(Freezable):
18 | 'Guard optimization data'
19 |
20 | def __init__(self, star, mode, transition_index):
21 | assert isinstance(mode, LinearAutomatonMode)
22 |
23 | self.settings = star.settings
24 | self.mode = mode
25 | self.inputs = star.inputs
26 |
27 | self.star = star
28 | self.transition = mode.transitions[transition_index]
29 | self.num_output_vars = self.transition.guard_matrix_csr.shape[1]
30 |
31 | self.key_dir_offset = 0
32 |
33 | if self.settings.plot.plot_mode != PlotSettings.PLOT_NONE:
34 | if self.settings.plot.xdim_dir is not None:
35 | self.key_dir_offset += 1
36 |
37 | if self.settings.plot.ydim_dir is not None:
38 | self.key_dir_offset += 1
39 |
40 | self.lpi = LpInstance(self.num_output_vars, star.num_init_vars, self.inputs)
41 |
42 | self.lpi.set_init_constraints(star.init_mat, star.init_rhs)
43 | self.lpi.set_output_constraints(self.transition.guard_matrix_csr, self.transition.guard_rhs)
44 |
45 | if star.inputs > 0:
46 | self.lpi.set_input_constraints_csc(csc_matrix(star.mode.u_constraints_csr), star.mode.u_constraints_rhs)
47 |
48 | self.optimized_lp_solution = None
49 |
50 | self.freeze_attrs()
51 |
52 | def update_full_lp(self):
53 | '''update the LP solution and, if it's feasible, get its solution, for GUARD_FULL_LP'''
54 |
55 | cur_basis_mat = self.star.time_elapse.cur_basis_mat
56 |
57 | # start and end rows of key-dir matrices
58 | start = self.key_dir_offset
59 | end = self.key_dir_offset + self.num_output_vars
60 |
61 | self.lpi.update_basis_matrix(cur_basis_mat[start:end])
62 |
63 | # add input effects for the current step (if it exists)
64 | if self.star.time_elapse.cur_input_effects_matrix is not None:
65 | input_effects_mat = self.star.time_elapse.cur_input_effects_matrix
66 | self.lpi.add_input_effects_matrix(input_effects_mat[start:end])
67 |
68 | result_len = self.num_output_vars + self.star.num_init_vars
69 | result_len += self.num_output_vars # total input effect
70 | result_len += self.inputs * (self.star.time_elapse.next_step - 1) # inputs at each step
71 |
72 | result = np.zeros((result_len), dtype=float)
73 | direction = np.zeros((self.num_output_vars,), dtype=float)
74 |
75 | is_feasible = self.lpi.minimize(direction, result, error_if_infeasible=False)
76 |
77 | return result if is_feasible else None
78 |
79 | def get_guard_lpi(self):
80 | '''get the current full lp instance for this guard'''
81 |
82 | return self.lpi
83 |
84 | def get_optimized_lp_solution(self):
85 | '''gets the lp solution without calling an lp solver
86 | this is only possible if the initial set has only range conditions for each initial dimension,
87 | and the output-space has only a single condition
88 |
89 | This returns either an lp solution (np.ndarray) or None if infeasible
90 | '''
91 |
92 | Timers.tic('get_optimized_lp_solution')
93 |
94 | init_ranges = self.star.init_range_tuples
95 | basis_mat = self.star.time_elapse.cur_basis_mat
96 | input_effects_mat = self.star.time_elapse.cur_input_effects_matrix
97 |
98 | assert len(init_ranges) == basis_mat.shape[1]
99 | assert len(self.transition.guard_rhs) == 1
100 | assert self.num_output_vars == 1
101 | assert self.transition.guard_matrix_csr.shape == (1, 1)
102 |
103 | guard_multiplier = self.transition.guard_matrix_csr[0, 0]
104 |
105 | if self.optimized_lp_solution is None:
106 | # +1 for output +1 for total input effects
107 | self.optimized_lp_solution = [0.0] * (len(init_ranges) + 1 + 1)
108 |
109 | result = self.optimized_lp_solution
110 | total_output_index = len(init_ranges)
111 | total_input_index = len(init_ranges) + 1
112 |
113 | guard_threshold = self.transition.guard_rhs[0]
114 | noinput_effect = 0
115 |
116 | Timers.tic("noinput_effects")
117 | for init_index in xrange(len(init_ranges)):
118 | basis_val = basis_mat[0, init_index]
119 | min_init = init_ranges[init_index][0]
120 | max_init = init_ranges[init_index][1]
121 |
122 | mult = basis_val * guard_multiplier
123 | val1 = min_init * mult
124 | val2 = max_init * mult
125 |
126 | # take the minimum of val1 and val2, since guard is CONDITION <= RHS
127 | if val1 < val2:
128 | noinput_effect += val1
129 | result[init_index] = min_init
130 | else:
131 | noinput_effect += val2
132 | result[init_index] = max_init
133 | Timers.toc("noinput_effects")
134 |
135 | # add input effects if they exist
136 | if input_effects_mat is not None:
137 | Timers.tic("input_effects")
138 | input_ranges = self.star.mode.u_range_tuples
139 |
140 | for input_index in xrange(len(input_ranges)):
141 | basis_val = input_effects_mat[0][input_index]
142 | min_input = input_ranges[input_index][0]
143 | max_input = input_ranges[input_index][1]
144 |
145 | val1 = min_input * basis_val * guard_multiplier
146 | val2 = max_input * basis_val * guard_multiplier
147 |
148 | # take the minimum of val1 and val2, since guard is CONDITION <= RHS
149 | if val1 < val2:
150 | result[total_input_index] += val1
151 | result.append(min_input)
152 | else:
153 | result[total_input_index] += val2
154 | result.append(max_input)
155 |
156 | Timers.toc("input_effects")
157 |
158 | result[total_output_index] = noinput_effect + result[total_input_index]
159 |
160 | rv = np.array(result, dtype=float) if result[total_output_index] <= guard_threshold else None
161 |
162 | Timers.toc('get_optimized_lp_solution')
163 |
164 | return rv
165 |
166 | def get_updated_lp_solution(self):
167 | '''update the LP solution and, if it's feasible, get its solution'''
168 |
169 | if self.star.settings.interval_guard_optimization and self.star.init_range_tuples is not None and \
170 | (self.star.inputs == 0 or self.star.mode.u_range_tuples is not None) and self.num_output_vars == 1:
171 | # LP can be decomposed column-by-column (optimization)
172 | rv = self.get_optimized_lp_solution()
173 | else:
174 | rv = self.update_full_lp()
175 |
176 | return rv
177 |
178 | def compute_noinput_effects_split(param):
179 | 'compute noinput effects for part of the basis matrix (used for multithreaded computation)'
180 |
181 | start, end, init_ranges, basis_mat, guard_multiplier = param
182 |
183 | assert len(init_ranges) == basis_mat.shape[1]
184 |
185 | result = np.zeros((len(init_ranges),), dtype=float)
186 | noinput_effect = 0
187 |
188 |
189 |
190 | return result, noinput_effect
191 |
--------------------------------------------------------------------------------
/examples/drivetrain/drivetrain.py:
--------------------------------------------------------------------------------
1 | '''
2 | Drivetrain example used in ARCHCOMP 18.
3 |
4 | Originally generated using the Hyst hylaa2 printer and then manually modified.
5 |
6 | Shows:
7 | - declaring an initial set from a zonotope
8 | - using exact convex hull aggregation (slow)
9 | '''
10 |
11 | from hylaa.hybrid_automaton import HybridAutomaton
12 | from hylaa.settings import HylaaSettings, PlotSettings
13 | from hylaa.aggstrat import Aggregated
14 | from hylaa.core import Core
15 | from hylaa.stateset import StateSet
16 | from hylaa import lputil
17 |
18 | def define_ha():
19 | '''make the hybrid automaton and return it'''
20 |
21 | ha = HybridAutomaton('Drivetrain')
22 |
23 | # dynamics variable order: [x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, t, affine]
24 |
25 | negAngle = ha.new_mode('negAngle')
26 | a_matrix = [ \
27 | [0, 0, 0, 0, 0, 0, 0.0833333333333333, 0, -1, 0, 0, 0, 0], \
28 | [13828.8888888889, -26.6666666666667, 60, 60, 0, 0, -5, -60, 0, 0, 0, 0, 716.666666666667], \
29 | [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], \
30 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5], \
31 | [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0], \
32 | [0, 0, 0, 0, -714.285714285714, -0.04, 0, 0, 0, 714.285714285714, 0, 0, 0], \
33 | [-2777.77777777778, 3.33333333333333, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -83.3333333333333], \
34 | [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0], \
35 | [100, 0, 0, 0, 0, 0, 0, -1000, -0.01, 1000, 0, 0, 3], \
36 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], \
37 | [0, 0, 0, 0, 1000, 0, 0, 1000, 0, -2000, -0.01, 0, 0], \
38 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], \
39 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], \
40 | ]
41 | negAngle.set_dynamics(a_matrix)
42 | # x1 <= -0.03
43 | negAngle.set_invariant([[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ], [-0.03, ])
44 |
45 | deadzone = ha.new_mode('deadzone')
46 | a_matrix = [ \
47 | [0, 0, 0, 0, 0, 0, 0.0833333333333333, 0, -1, 0, 0, 0, 0], \
48 | [-60, -26.6666666666667, 60, 60, 0, 0, -5, -60, 0, 0, 0, 0, 300], \
49 | [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], \
50 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5], \
51 | [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0], \
52 | [0, 0, 0, 0, -714.285714285714, -0.04, 0, 0, 0, 714.285714285714, 0, 0, 0], \
53 | [0, 3.33333333333333, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], \
54 | [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0], \
55 | [0, 0, 0, 0, 0, 0, 0, -1000, -0.01, 1000, 0, 0, 0], \
56 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], \
57 | [0, 0, 0, 0, 1000, 0, 0, 1000, 0, -2000, -0.01, 0, 0], \
58 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], \
59 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], \
60 | ]
61 | deadzone.set_dynamics(a_matrix)
62 | # -0.03 <= x1 & x1 <= 0.03
63 | deadzone.set_invariant([[-1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ], [0.03, 0.03, ])
64 |
65 | posAngle = ha.new_mode('posAngle')
66 | a_matrix = [ \
67 | [0, 0, 0, 0, 0, 0, 0.0833333333333333, 0, -1, 0, 0, 0, 0], \
68 | [13828.8888888889, -26.6666666666667, 60, 60, 0, 0, -5, -60, 0, 0, 0, 0, -116.666666666667], \
69 | [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], \
70 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5], \
71 | [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0], \
72 | [0, 0, 0, 0, -714.285714285714, -0.04, 0, 0, 0, 714.285714285714, 0, 0, 0], \
73 | [-2777.77777777778, 3.33333333333333, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 83.3333333333333], \
74 | [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0], \
75 | [100, 0, 0, 0, 0, 0, 0, -1000, -0.01, 1000, 0, 0, -3], \
76 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], \
77 | [0, 0, 0, 0, 1000, 0, 0, 1000, 0, -2000, -0.01, 0, 0], \
78 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], \
79 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], \
80 | ]
81 | posAngle.set_dynamics(a_matrix)
82 | # 0.03 <= x1
83 | posAngle.set_invariant([[-1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ], [-0.03, ])
84 |
85 | negAngleInit = ha.new_mode('negAngleInit')
86 | a_matrix = [ \
87 | [0, 0, 0, 0, 0, 0, 0.0833333333333333, 0, -1, 0, 0, 0, 0], \
88 | [13828.8888888889, -26.6666666666667, 60, 60, 0, 0, -5, -60, 0, 0, 0, 0, 116.666666666667], \
89 | [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], \
90 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5], \
91 | [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0], \
92 | [0, 0, 0, 0, -714.285714285714, -0.04, 0, 0, 0, 714.285714285714, 0, 0, 0], \
93 | [-2777.77777777778, 3.33333333333333, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -83.3333333333333], \
94 | [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0], \
95 | [100, 0, 0, 0, 0, 0, 0, -1000, -0.01, 1000, 0, 0, 3], \
96 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], \
97 | [0, 0, 0, 0, 1000, 0, 0, 1000, 0, -2000, -0.01, 0, 0], \
98 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], \
99 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], \
100 | ]
101 | negAngleInit.set_dynamics(a_matrix)
102 | # t <= 0.2
103 | negAngleInit.set_invariant([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], ], [0.2, ])
104 |
105 | error = ha.new_mode('error')
106 |
107 | trans = ha.new_transition(negAngleInit, negAngle)
108 | # t >= 0.2
109 | trans.set_guard([[-0, -0, -0, -0, -0, -0, -0, -0, -0, -0, -0, -1, -0], ], [-0.2, ])
110 |
111 | trans = ha.new_transition(negAngle, deadzone)
112 | # x1 >= -0.03
113 | trans.set_guard([[-1, -0, -0, -0, -0, -0, -0, -0, -0, -0, -0, -0, -0], ], [0.03, ])
114 |
115 | trans = ha.new_transition(deadzone, posAngle)
116 | # x1 >= 0.03
117 | trans.set_guard([[-1, -0, -0, -0, -0, -0, -0, -0, -0, -0, -0, -0, -0], ], [-0.03, ])
118 |
119 | trans = ha.new_transition(deadzone, error)
120 | # x1 <= -0.03
121 | trans.set_guard([[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ], [-0.03, ])
122 |
123 | trans = ha.new_transition(posAngle, error)
124 | # x1 <= 0.03
125 | trans.set_guard([[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ], [0.03, ])
126 |
127 | return ha
128 |
129 | def define_init_states(ha):
130 | '''returns a list of StateSet objects'''
131 | # Variable ordering: [x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, t, affine]
132 | rv = []
133 |
134 | mode = ha.modes['negAngleInit']
135 |
136 | #X_0 = {center + alpha * generator, alpha in [-1, 1]}
137 | center = [-0.0432, -11, 0, 30, 0, 30, 360, -0.0013, 30, -0.0013, 30, 0, 1]
138 | generator = [0.0056, 4.67, 0, 10, 0, 10, 120, 0.0006, 10, 0.0006, 10, 0, 0]
139 |
140 | lpi = lputil.from_zonotope(center, [generator], mode)
141 |
142 | rv.append(StateSet(lpi, mode))
143 |
144 | return rv
145 |
146 | def define_settings():
147 | '''get the hylaa settings object
148 | see hylaa/settings.py for a complete list of reachability settings'''
149 |
150 | # step_size = 5.0E-4, max_time = 2.0
151 | settings = HylaaSettings(5.0E-3, 2.0)
152 | settings.stdout = HylaaSettings.STDOUT_VERBOSE
153 | settings.plot.plot_mode = PlotSettings.PLOT_IMAGE
154 | settings.plot.xdim_dir = 0 # [0, None, None]
155 |
156 | #x0_dir = np.array([0, 0, 0, 0, 0, 0, 0.0833333333333333, 0, -1, 0, 0, 0, 0], dtype=float)
157 | #settings.plot.ydim_dir = [2, 6, 8]
158 | settings.plot.ydim_dir = 2
159 |
160 | #settings.stop_on_error = False
161 | #settings.plot.draw_stride = 5
162 | #settings.plot.num_angles = 4096 * 128 # required for convex hull to show up correctly
163 | #settings.aggregation.agg_mode = AggregationSettings.AGG_NONE
164 |
165 | #def custom_pop_func(waiting_list):
166 | # 'custom pop function for aggregation'
167 |
168 | # mode = waiting_list[0].mode
169 |
170 | # state_list = [state for state in waiting_list if state.mode == mode]
171 |
172 | # num = 2 # performing clustering with this number of items
173 |
174 | # return heapq.nsmallest(num, state_list, lambda s: s.cur_steps_since_start)
175 |
176 |
177 | #settings.aggregation.custom_pop_func = custom_pop_func
178 | settings.aggstrat.agg_type = Aggregated.AGG_CONVEX_HULL
179 |
180 | return settings
181 |
182 | def run_hylaa():
183 | 'runs hylaa'
184 |
185 | ha = define_ha()
186 | init = define_init_states(ha)
187 | settings = define_settings()
188 |
189 | result = Core(ha, settings).run(init)
190 |
191 | if __name__ == '__main__':
192 | run_hylaa()
193 |
--------------------------------------------------------------------------------
/hylaa/deaggregation.py:
--------------------------------------------------------------------------------
1 | '''
2 | Deaggregation Management implementation
3 |
4 | Stanley Bak
5 | Nov 2018
6 | '''
7 |
8 | from collections import deque, namedtuple
9 |
10 | from hylaa.util import Freezable
11 | from hylaa.timerutil import Timers
12 |
13 | # Operation types
14 | OpInvIntersect = namedtuple('OpInvIntersect', ['step', 'node', 'i_index', 'is_stronger'])
15 | OpLeftInvariant = namedtuple('OpLeftInvariant', ['step', 'node', 'reached_time_bound'])
16 |
17 | class OpTransition(): # pylint: disable=too-few-public-methods
18 | 'a transition operation'
19 |
20 | def __init__(self, step, parent_node, child_node, transition, poststate):
21 | self.step = step
22 | self.parent_node = parent_node
23 | self.child_node = child_node
24 | self.transition = transition
25 | self.poststate = poststate
26 |
27 | def __str__(self):
28 | return f"[OpTransition({self.step}, {self.transition})]"
29 |
30 | class DeaggregationManager(Freezable):
31 | 'manager for deaggregation data'
32 |
33 | def __init__(self, aggdag):
34 | self.aggdag = aggdag
35 |
36 | self.waiting_nodes = deque() # a deque of 2-tuples (parent, children_list) of AggDagNodes awaiting deaggregation
37 |
38 | # associated with the current computaiton
39 | self.deagg_parent = None # a AggDagNode
40 | self.deagg_children = None # a list of AggDagNode
41 |
42 | # during replay, transition sucessors may have common children nodes that got aggregated
43 | # this maps the overapproximation node to the new unaggregated components (list of stateset and list of ops)
44 | # str(AggDagNode.id()) -> list_of_TransitionOps
45 | self.nodes_to_ops = None # maps (newop.parent, oldop.child) to list of transtion_ops
46 |
47 | self.replay_step = None # current step during a replay
48 |
49 | def doing_replay(self):
50 | 'are we in the middle of a refinement replay?'
51 |
52 | return self.replay_step is not None or self.waiting_nodes
53 |
54 | def plot_replay_deaggregated(self, cur_step_in_mode):
55 | '''plot the updated states during a replay
56 |
57 | child_plot_mask is a list of booleans, indicating which deagg chidlren should be plotted.
58 | plots may be skipped if the child left the invariant on an earlier step
59 |
60 | note: invariant can be false for one step
61 | '''
62 |
63 | plotman = self.aggdag.core.plotman
64 |
65 | # add children plots
66 | plot_child_states = []
67 |
68 | for i, child in enumerate(self.deagg_children):
69 | if not child.node_left_invariant():
70 | plot_child_states.append(child.stateset)
71 | assert child.stateset.cur_step_in_mode == cur_step_in_mode, \
72 | f"expected cur_step_in_mode {cur_step_in_mode}, but it was {child.stateset.cur_step_in_mode}"
73 |
74 | plotman.add_plotted_states(plot_child_states)
75 |
76 | # delete parent plot
77 | plotman.delete_plotted_state(self.deagg_parent.stateset, cur_step_in_mode)
78 |
79 | def init_replay(self):
80 | 'initialize a replay action'
81 |
82 | assert self.replay_step is None
83 |
84 | self.deagg_parent, self.deagg_children = self.waiting_nodes.popleft()
85 | self.replay_step = 0
86 | self.nodes_to_ops = {}
87 |
88 | # pause plot
89 | self.aggdag.core.print_verbose("Pausing due to start of do_step_replay()")
90 | self.aggdag.core.plotman.pause()
91 |
92 | def do_step_replay(self):
93 | 'do a refinement replay step'
94 |
95 | assert self.doing_replay()
96 |
97 | if self.replay_step is None:
98 | self.init_replay()
99 |
100 | assert self.replay_step is not None
101 |
102 | op_list = self.deagg_parent.op_list
103 | op = op_list[self.replay_step]
104 | cur_step_in_mode = op.step
105 |
106 | # This part is a bit complicated due to discrete-time sematnics.
107 | # Basically, in continuous post the operation order is:
108 | # 1. inv intersect
109 | # 2. step()
110 | # 3. check transitions
111 | # 4. plot
112 | #
113 | # in order to recreate this during a replay, we need conditional branching:
114 | # if first op is transition, process all transitions [which does step() first] at the current step,
115 | # but no invariant intersections
116 | # if first op is invariant intersection, process all ops at the current step and transitions at the next step
117 | #
118 | only_process_transitions = isinstance(op_list[self.replay_step], OpTransition)
119 |
120 | parent_left_invariant = False
121 |
122 | for op in op_list[self.replay_step:]:
123 | if op.step > cur_step_in_mode and not only_process_transitions:
124 | cur_step_in_mode += 1
125 | only_process_transitions = True
126 |
127 | if op.step != cur_step_in_mode:
128 | #print(".deagg breaking because next op step is not cur_step_in_mode")
129 | break
130 |
131 | if only_process_transitions and not isinstance(op, OpTransition):
132 | #print(f".deagg breaking because next op is not OpTransition: {op}")
133 | break
134 |
135 | #print(f".deagg replaying step {self.replay_step}/{len(self.deagg_parent.op_list)}: {op}")
136 |
137 | if isinstance(op, OpLeftInvariant):
138 | parent_left_invariant = True
139 |
140 | for child in self.deagg_children:
141 | if not child.node_left_invariant():
142 | child.replay_op(self.deagg_parent.op_list, self.replay_step)
143 | else:
144 | self.aggdag.core.print_verbose("child has left invariant")
145 |
146 | self.replay_step += 1
147 |
148 | # advance the children to the current step if in case we only had inv_intersection operations
149 | for child in self.deagg_children:
150 | if not child.node_left_invariant():
151 | child.stateset.step(cur_step_in_mode)
152 |
153 | if not parent_left_invariant:
154 | # do the plot after the replay actions, as this can affect the plot (for example, invariant intersection)
155 | self.plot_replay_deaggregated(cur_step_in_mode)
156 |
157 | was_last_step = self.replay_step >= len(self.deagg_parent.op_list)
158 |
159 | if was_last_step or all([c.node_left_invariant() for c in self.deagg_children]):
160 | # update recursive children
161 |
162 | for pair, ops in self.nodes_to_ops.items():
163 | _, child = pair
164 | self.aggdag.core.print_verbose(f"making node for recursive deaggregation with t={ops[0].transition}")
165 |
166 | # aggregate all ops into a single node, using same aggregation as before
167 | node = self.aggdag.make_node(ops, child.agg_type_from_parents, child.stateset.aggstring)
168 |
169 | self.waiting_nodes.append((child, [node]))
170 |
171 | self.deagg_parent = self.deagg_children = self.replay_step = self.nodes_to_ops = None
172 |
173 | #print(".deaggregation calling aggdag.save_viz()")
174 | #self.aggdag.save_viz()
175 |
176 | def update_transition_successors(self, old_op, new_op):
177 | '''
178 | during a replay, update a node's successsors recursively
179 | '''
180 |
181 | # aggregation will only be done if both new_op.parent and old_op.child match
182 | parent_node = new_op.parent_node
183 | child_node = old_op.child_node
184 | pair = (parent_node, child_node)
185 |
186 | if pair not in self.nodes_to_ops:
187 | ops = []
188 | self.nodes_to_ops[pair] = ops
189 | else:
190 | ops = self.nodes_to_ops[pair]
191 |
192 | ops.append(new_op)
193 |
194 | def begin_replay(self, node):
195 | 'begin a deaggregation replay with the passed-in node'
196 |
197 | Timers.tic('begin deagg replay')
198 |
199 | # remove all states in the waiting list that come from this node
200 | self.aggdag.remove_node_decendants_from_waiting_list(node)
201 |
202 | # start to populate waiting_nodes
203 | self.waiting_nodes.append((node, node.split()))
204 |
205 | Timers.toc('begin deagg replay')
206 |
--------------------------------------------------------------------------------
/examples/rendezvous/rendezvous_full_passivity.py:
--------------------------------------------------------------------------------
1 | '''
2 | Spacecraft Rendezvous System described in
3 | "Verifying safety of an autonomous spacecraft rendezvous mission" by Nicole Chan and Sayan Mitra
4 |
5 | This model was used in ARCHCOMP-18, specifically the variant with the abort maneuver.
6 |
7 | Stanley Bak, July 2018
8 | '''
9 |
10 | import math
11 |
12 | from matplotlib import collections
13 |
14 | from hylaa.hybrid_automaton import HybridAutomaton
15 | from hylaa.settings import HylaaSettings, PlotSettings, LabelSettings
16 | from hylaa.core import Core
17 | from hylaa.stateset import StateSet
18 | from hylaa import lputil
19 | from hylaa.aggstrat import Aggregated
20 |
21 | def make_automaton(safe):
22 | 'make the hybrid automaton'
23 |
24 | ha = HybridAutomaton('Spacecraft Rendezvous with Abort')
25 |
26 | passive_min_time = 0
27 | passive_max_time = 250
28 |
29 | ############## Modes ##############
30 | p2 = ha.new_mode('Far')
31 | a_mat = [\
32 | [0.0, 0.0, 1.0, 0.0, 0.0, 0], \
33 | [0.0, 0.0, 0.0, 1.0, 0.0, 0], \
34 | [-0.057599765881773, 0.000200959896519766, -2.89995083970656, 0.00877200894463775, 0.0, 0], \
35 | [-0.000174031357370456, -0.0665123984901026, -0.00875351105536225, -2.90300269286856, 0.0, 0.0], \
36 | [0.0, 0.0, 0.0, 0.0, 0.0, 1.0], \
37 | [0, 0, 0, 0, 0, 0]]
38 | inv_mat = [[0.0, 0.0, 0.0, 0.0, 1.0, 0], [1.0, 0.0, 0.0, 0.0, 0.0, 0]]
39 | inv_rhs = [125.0, -100.0]
40 | p2.set_dynamics(a_mat)
41 | p2.set_invariant(inv_mat, inv_rhs)
42 |
43 |
44 | p3 = ha.new_mode('Approaching')
45 | a_mat = [\
46 | [0.0, 0.0, 1.0, 0.0, 0.0, 0], \
47 | [0.0, 0.0, 0.0, 1.0, 0.0, 0], \
48 | [-0.575999943070835, 0.000262486079431672, -19.2299795908647, 0.00876275931760007, 0.0, 0], \
49 | [-0.000262486080737868, -0.575999940191886, -0.00876276068239993, -19.2299765959399, 0.0, 0], \
50 | [0.0, 0.0, 0.0, 0.0, 0.0, 1.],\
51 | [0, 0, 0, 0, 0, 0]]
52 | inv_mat = [\
53 | [-1.0, 0., 0., 0., 0., 0], \
54 | [1.0, 0., 0., 0., 0., 0], \
55 | [0., -1.0, 0., 0., 0., 0], \
56 | [0., 1.0, 0., 0., 0., 0], \
57 | [-1.0, -1.0, 0.0, 0.0, 0.0, 0], \
58 | [1.0, 1.0, 0.0, 0.0, 0.0, 0], \
59 | [-1.0, 1.0, 0., 0., 0., 0], \
60 | [1.0, -1.0, 0., 0., 0., 0], \
61 | [0., 0., 0., 0., 1., 0]]
62 | inv_rhs = [100, 100, 100, 100, 141.1, 141.1, 141.1, 141.1, passive_max_time]
63 | p3.set_dynamics(a_mat)
64 | p3.set_invariant(inv_mat, inv_rhs)
65 |
66 |
67 | passive = ha.new_mode('Abort')
68 | a_mat = [\
69 | [0, 0, 1, 0, 0, 0], \
70 | [0, 0, 0, 1, 0, 0], \
71 | [0.0000575894721132000, 0, 0, 0.00876276, 0, 0], \
72 | [0, 0, -0.00876276, 0, 0, 0], \
73 | [0, 0, 0, 0, 0, 1.], \
74 | [0, 0, 0, 0, 0, 0]]
75 | passive.set_dynamics(a_mat)
76 |
77 | error = ha.new_mode('Error')
78 |
79 | ############## Normal Transitions ##############
80 | t1 = ha.new_transition(p2, p3)
81 | guard_mat = [\
82 | [-1.0, 0., 0., 0., 0., 0], \
83 | [1.0, 0., 0., 0., 0., 0], \
84 | [0., -1.0, 0., 0., 0., 0], \
85 | [0., 1.0, 0., 0., 0., 0], \
86 | [-1.0, -1.0, 0.0, 0.0, 0.0, 0], \
87 | [1.0, 1.0, 0.0, 0.0, 0.0, 0], \
88 | [-1.0, 1.0, 0., 0., 0., 0], \
89 | [1.0, -1.0, 0., 0., 0., 0]]
90 | guard_rhs = [100, 100, 100, 100, 141.1, 141.1, 141.1, 141.1]
91 | t1.set_guard(guard_mat, guard_rhs)
92 |
93 | ha.new_transition(p2, passive).set_guard([[0.0, 0.0, 0.0, 0.0, -1.0, 0]], [-passive_min_time])
94 |
95 | ha.new_transition(p3, passive).set_guard([[0.0, 0.0, 0.0, 0.0, -1.0, 0]], [-passive_min_time])
96 |
97 | ############## Error Transitions ##############
98 | # In the aborting mode, the vehicle must avoid the target, which is modeled as a box B with
99 | # 0.2m edge length and the center placed as the origin
100 | rad = 0.2
101 | t = ha.new_transition(passive, error)
102 | guard_mat = [ \
103 | [1, 0, 0., 0., 0., 0], \
104 | [-1, 0, 0., 0., 0., 0], \
105 | [0, 1., 0., 0., 0., 0], \
106 | [0, -1., 0., 0., 0., 0]]
107 | guard_rhs = [rad, rad, rad, rad]
108 | t.set_guard(guard_mat, guard_rhs)
109 |
110 | #In the rendezvous attempt the spacecraft must remain within the lineof-sight
111 | #cone L = {[x, y]^T | (x >= -100m) AND (y >= x*tan(30)) AND (-y >= x*tan(30))}
112 | ha.new_transition(p3, error).set_guard([[1, 0, 0., 0., 0., 0]], [-100])
113 | ha.new_transition(p3, error).set_guard([[-0.57735, 1, 0, 0., 0., 0]], [0])
114 | ha.new_transition(p3, error).set_guard([[-0.57735, -1, 0., 0., 0., 0]], [0])
115 |
116 | # sqrt(vx^2 + vy^2) should stay below 0.055 m/SECOND (time in model is in MINUTES)
117 | # to make the model unsafe, try changing this to 0.05
118 | meters_per_sec_limit = 0.055 if safe else 0.05
119 | meters_per_min_limit = meters_per_sec_limit * 60
120 | h = meters_per_min_limit * math.cos(math.pi / 8.0)
121 | w = meters_per_min_limit * math.sin(math.pi / 8.0)
122 |
123 | #ha.new_transition(p3, error).set_guard([[0, 0, 1., 0., 0., 0]], [-h])
124 | #ha.new_transition(p3, error).set_guard([[0, 0, -1., 0., 0., 0]], [-h])
125 | #ha.new_transition(p3, error).set_guard([[0, 0, 0., 1., 0., 0]], [-h])
126 | #ha.new_transition(p3, error).set_guard([[0, 0, 0., -1., 0., 0]], [-h])
127 |
128 | #ha.new_transition(p3, error).set_guard([[0, 0, 1., 1., 0., 0]], [-(w + h)])
129 | #ha.new_transition(p3, error).set_guard([[0, 0, -1., 1., 0., 0]], [-(w + h)])
130 | #ha.new_transition(p3, error).set_guard([[0, 0, -1., -1., 0., 0]], [-(w + h)])
131 | #ha.new_transition(p3, error).set_guard([[0, 0, 1., -1., 0., 0]], [-(w + h)])
132 |
133 | return ha
134 |
135 | def make_init(ha):
136 | 'make the initial states'
137 |
138 | p2 = ha.modes['Far']
139 | init_lpi = lputil.from_box([(-925.0, -875.0), (-425.0, -375.0), (0.0, 0.0), (0.0, 0.0), (0.0, 0.0), (1.0, 1.0)], p2)
140 | init_list = [StateSet(init_lpi, p2)]
141 |
142 | return init_list
143 |
144 | class MyAggergated(Aggregated):
145 | 'a custom aggregation strategy'
146 |
147 | def __init__(self):
148 | Aggregated.__init__(self)
149 |
150 | def pop_waiting_list(self, waiting_list):
151 | '''
152 | Get the states to remove from the waiting list based on a score-based method
153 | '''
154 |
155 | states = Aggregated.pop_waiting_list(self, waiting_list)
156 |
157 | if len(states) == 9:
158 | states = states[0:4]
159 |
160 | return states
161 |
162 | def make_settings(safe):
163 | 'make the reachability settings object'
164 |
165 | # see hylaa.settings for a list of reachability settings
166 | settings = HylaaSettings(1.0, 200.0) # step: 0.1, bound: 200.0
167 |
168 | settings.stop_on_aggregated_error = False
169 | settings.process_urgent_guards = True
170 |
171 | #settings.aggstrat = MyAggergated()
172 | settings.aggstrat.deaggregate = True # use deaggregation
173 | settings.aggstrat.deagg_preference = Aggregated.DEAGG_LEAVES_FIRST
174 |
175 | settings.stdout = HylaaSettings.STDOUT_VERBOSE
176 |
177 | settings.plot.plot_mode = PlotSettings.PLOT_NONE
178 | settings.plot.filename = "rendezvous_full_passivity.png"
179 | settings.plot.plot_size = (8, 9)
180 |
181 | #settings.plot.video_pause_frames = 10
182 | #settings.plot.plot_mode = PlotSettings.PLOT_VIDEO
183 | #settings.plot.filename = "rendezvous_full_passivity.mp4"
184 |
185 | settings.plot.xdim_dir = [0] * 3
186 | settings.plot.ydim_dir = [1] * 3
187 | settings.plot.grid = False
188 | settings.plot.label = []
189 | settings.plot.extra_collections = []
190 |
191 | for _ in range(3):
192 | ls = LabelSettings()
193 | settings.plot.label.append(ls)
194 |
195 | ls.big(size=24)
196 |
197 | ls.x_label = '$x$'
198 | ls.y_label = '$y$'
199 |
200 | y = 57.735
201 | line = [(-100, y), (-100, -y), (0, 0), (-100, y)]
202 | c1 = collections.LineCollection([line], animated=True, colors=('gray'), linewidths=(1), linestyle='dashed')
203 |
204 | rad = 0.2
205 | line = [(-rad, -rad), (-rad, rad), (rad, rad), (rad, -rad), (-rad, -rad)]
206 | c2 = collections.LineCollection([line], animated=True, colors=('red'), linewidths=(2))
207 |
208 | settings.plot.extra_collections.append([c1, c2])
209 |
210 | settings.plot.label[0].axes_limits = [-950, 400, -450, 70]
211 | #settings.plot.label[1].axes_limits = [-150, 50, -70, 70]
212 | settings.plot.label[1].axes_limits = [-80, 5, -30, 5]
213 | settings.plot.label[2].axes_limits = [-3, 1.5, -1.5, 1.5]
214 |
215 | return settings
216 |
217 | def run_hylaa():
218 | 'main entry point'
219 |
220 | safe = True
221 |
222 | ha = make_automaton(safe)
223 |
224 | init_states = make_init(ha)
225 |
226 | settings = make_settings(safe)
227 |
228 | Core(ha, settings).run(init_states)
229 |
230 | if __name__ == "__main__":
231 | run_hylaa()
232 |
--------------------------------------------------------------------------------
/.pylintrc:
--------------------------------------------------------------------------------
1 | [MASTER]
2 |
3 | # Specify a configuration file.
4 | #rcfile=
5 |
6 | # Python code to execute, usually for sys.path manipulation such as
7 | # pygtk.require().
8 | #init-hook=
9 |
10 | # Profiled execution.
11 | profile=no
12 |
13 | # Add files or directories to the blacklist. They should be base names, not
14 | # paths.
15 | ignore=CVS
16 |
17 | # Pickle collected data for later comparisons.
18 | persistent=yes
19 |
20 | # List of plugins (as comma separated values of python modules names) to load,
21 | # usually to register additional checkers.
22 | load-plugins=
23 |
24 |
25 | [MESSAGES CONTROL]
26 |
27 | # Enable the message, report, category or checker with the given id(s). You can
28 | # either give multiple identifier separated by comma (,) or put this option
29 | # multiple time. See also the "--disable" option for examples.
30 | #enable=
31 |
32 | # Disable the message, report, category or checker with the given id(s). You
33 | # can either give multiple identifiers separated by comma (,) or put this
34 | # option multiple times (only on the command line, not in the configuration
35 | # file where it should appear only once).You can also use "--disable=all" to
36 | # disable everything first and then reenable specific checks. For example, if
37 | # you want to run only the similarities checker, you can use "--disable=all
38 | # --enable=similarities". If you want to run only the classes checker, but have
39 | # no Warning level messages displayed, use"--disable=all --enable=classes
40 | # --disable=W"
41 |
42 | # R0801 - duplicate code (parsed incorrectly)
43 | # R0921 - abstract class not used (pylint looks at one file at a time)
44 | # R0903 - too few public methods
45 | # C0303 - trailing whitespace
46 | disable=C0303
47 |
48 |
49 | [REPORTS]
50 |
51 | # Set the output format. Available formats are text, parseable, colorized, msvs
52 | # (visual studio) and html. You can also give a reporter class, eg
53 | # mypackage.mymodule.MyReporterClass.
54 | output-format=text
55 |
56 | # Put messages in a separate file for each module / package specified on the
57 | # command line instead of printing them on stdout. Reports (if any) will be
58 | # written in a file name "pylint_global.[txt|html]".
59 | files-output=no
60 |
61 | # Tells whether to display a full report or only the messages
62 | reports=yes
63 |
64 | # Python expression which should return a note less than 10 (10 is the highest
65 | # note). You have access to the variables errors warning, statement which
66 | # respectively contain the number of errors / warnings messages and the total
67 | # number of statements analyzed. This is used by the global evaluation report
68 | # (RP0004).
69 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
70 |
71 | # Add a comment according to your evaluation note. This is used by the global
72 | # evaluation report (RP0004).
73 | comment=no
74 |
75 | # Template used to display messages. This is a python new-style format string
76 | # used to format the message information. See doc for all details
77 | #msg-template=
78 |
79 |
80 | [BASIC]
81 |
82 | # Required attributes for module, separated by a comma
83 | required-attributes=
84 |
85 | # List of builtins function names that should not be used, separated by a comma
86 | bad-functions=map,filter,apply,input
87 |
88 | # Regular expression which should only match correct module names
89 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
90 |
91 | # Regular expression which should only match correct module level names
92 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
93 |
94 | # Regular expression which should only match correct class names
95 | class-rgx=[A-Z_][a-zA-Z0-9]+$
96 |
97 | # Regular expression which should only match correct function names
98 | function-rgx=[a-z_][a-z0-9_]{2,30}$
99 |
100 | # Regular expression which should only match correct method names
101 | method-rgx=[a-z_][a-z0-9_]{2,30}$
102 |
103 | # Regular expression which should only match correct instance attribute names
104 | attr-rgx=[a-z_][a-z0-9_]{2,30}$
105 |
106 | # Regular expression which should only match correct argument names
107 | argument-rgx=[a-z_][a-z0-9_]{0,30}$
108 |
109 | # Regular expression which should only match correct variable names
110 | variable-rgx=[a-z_][a-z0-9_]{0,30}$
111 |
112 | # Regular expression which should only match correct attribute names in class
113 | # bodies
114 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
115 |
116 | # Regular expression which should only match correct list comprehension /
117 | # generator expression variable names
118 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
119 |
120 | # Good variable names which should always be accepted, separated by a comma
121 | good-names=i,j,k,ex,Run,_
122 |
123 | # Bad variable names which should always be refused, separated by a comma
124 | bad-names=foo,bar,baz,toto,tutu,tata
125 |
126 | # Regular expression which should only match function or class names that do
127 | # not require a docstring.
128 | no-docstring-rgx=__.*__
129 |
130 | # Minimum line length for functions/classes that require docstrings, shorter
131 | # ones are exempt.
132 | docstring-min-length=-1
133 |
134 |
135 | [SIMILARITIES]
136 |
137 | # Minimum lines number of a similarity.
138 | min-similarity-lines=4
139 |
140 | # Ignore comments when computing similarities.
141 | ignore-comments=yes
142 |
143 | # Ignore docstrings when computing similarities.
144 | ignore-docstrings=yes
145 |
146 |
147 | [MISCELLANEOUS]
148 |
149 | # List of note tags to take in consideration, separated by a comma.
150 | notes=FIXME,XXX,TODO
151 |
152 |
153 | [FORMAT]
154 |
155 | # Maximum number of characters on a single line.
156 | max-line-length=120
157 |
158 | # Regexp for a line that is allowed to be longer than the limit.
159 | ignore-long-lines=^\s*(# )??$
160 |
161 | # Allow the body of an if to be on the same line as the test if there is no
162 | # else.
163 | single-line-if-stmt=no
164 |
165 | # List of optional constructs for which whitespace checking is disabled
166 | no-space-check=trailing-comma,dict-separator
167 |
168 | # Maximum number of lines in a module
169 | max-module-lines=1000
170 |
171 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
172 | # tab).
173 | indent-string=' '
174 |
175 |
176 | [TYPECHECK]
177 |
178 | # Tells whether missing members accessed in mixin class should be ignored. A
179 | # mixin class is detected if its name ends with "mixin" (case insensitive).
180 | ignore-mixin-members=yes
181 |
182 | # List of classes names for which member attributes should not be checked
183 | # (useful for classes with attributes dynamically set).
184 | ignored-classes=SQLObject
185 |
186 | # When zope mode is activated, add a predefined set of Zope acquired attributes
187 | # to generated-members.
188 | zope=no
189 |
190 | # List of members which are set dynamically and missed by pylint inference
191 | # system, and so shouldn't trigger E0201 when accessed. Python regular
192 | # expressions are accepted.
193 | generated-members=REQUEST,acl_users,aq_parent
194 |
195 | [VARIABLES]
196 |
197 | # Tells whether we should check for unused import in __init__ files.
198 | init-import=no
199 |
200 | # A regular expression matching the beginning of the name of dummy variables
201 | # (i.e. not used).
202 | dummy-variables-rgx=_$|dummy
203 |
204 | # List of additional names supposed to be defined in builtins. Remember that
205 | # you should avoid to define new builtins when possible.
206 | additional-builtins=
207 |
208 |
209 | [IMPORTS]
210 |
211 | # Deprecated modules which should not be used, separated by a comma
212 | deprecated-modules=regsub,TERMIOS,Bastion,rexec
213 |
214 | # Create a graph of every (i.e. internal and external) dependencies in the
215 | # given file (report RP0402 must not be disabled)
216 | import-graph=
217 |
218 | # Create a graph of external dependencies in the given file (report RP0402 must
219 | # not be disabled)
220 | ext-import-graph=
221 |
222 | # Create a graph of internal dependencies in the given file (report RP0402 must
223 | # not be disabled)
224 | int-import-graph=
225 |
226 |
227 | [DESIGN]
228 |
229 | # Maximum number of arguments for function / method
230 | max-args=10
231 |
232 | # Argument names that match this expression will be ignored. Default to name
233 | # with leading underscore
234 | ignored-argument-names=_.*
235 |
236 | # Maximum number of locals for function / method body
237 | max-locals=25
238 |
239 | # Maximum number of return / yield for function / method body
240 | max-returns=6
241 |
242 | # Maximum number of branch for function / method body
243 | max-branches=20
244 |
245 | # Maximum number of statements in function / method body
246 | max-statements=50
247 |
248 | # Maximum number of parents for a class (see R0901).
249 | max-parents=7
250 |
251 | # Maximum number of attributes for a class (see R0902).
252 | max-attributes=15
253 |
254 | # Minimum number of public methods for a class (see R0903).
255 | min-public-methods=2
256 |
257 | # Maximum number of public methods for a class (see R0904).
258 | max-public-methods=20
259 |
260 |
261 | [CLASSES]
262 |
263 | # List of interface methods to ignore, separated by a comma. This is used for
264 | # instance to not check methods defines in Zope's Interface base class.
265 | ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by
266 |
267 | # List of method names used to declare (i.e. assign) instance attributes.
268 | defining-attr-methods=__init__,__new__,setUp
269 |
270 | # List of valid names for the first argument in a class method.
271 | valid-classmethod-first-arg=cls
272 |
273 | # List of valid names for the first argument in a metaclass class method.
274 | valid-metaclass-classmethod-first-arg=mcs
275 |
276 |
277 | [EXCEPTIONS]
278 |
279 | # Exceptions that will emit a warning when being caught. Defaults to
280 | # "Exception"
281 | overgeneral-exceptions=Exception
282 |
--------------------------------------------------------------------------------
/examples/rendezvous/rendezvous.py:
--------------------------------------------------------------------------------
1 | '''
2 | Spacecraft Rendezvous System described in
3 | "Verifying safety of an autonomous spacecraft rendezvous mission" by Nicole Chan and Sayan Mitra
4 |
5 | This model was used in ARCHCOMP-18, specifically the variant with the abort maneuver.
6 |
7 | Stanley Bak, July 2018
8 | '''
9 |
10 | import math
11 |
12 | from matplotlib import collections
13 | from matplotlib.patches import Circle
14 |
15 | from hylaa.hybrid_automaton import HybridAutomaton
16 | from hylaa.settings import HylaaSettings, PlotSettings, LabelSettings
17 | from hylaa.core import Core
18 | from hylaa.stateset import StateSet
19 | from hylaa import lputil, aggstrat
20 |
21 | def make_automaton(safe, passive_time=120):
22 | 'make the hybrid automaton'
23 |
24 | ha = HybridAutomaton(f'Spacecraft Rendezvous (Abort at t={passive_time})')
25 |
26 | passive_time += 1e-4 # ensures switch to passive at a single step intead of at two steps
27 |
28 | ############## Modes ##############
29 | p2 = ha.new_mode('P2')
30 | a_mat = [\
31 | [0.0, 0.0, 1.0, 0.0, 0.0, 0], \
32 | [0.0, 0.0, 0.0, 1.0, 0.0, 0], \
33 | [-0.057599765881773, 0.000200959896519766, -2.89995083970656, 0.00877200894463775, 0.0, 0], \
34 | [-0.000174031357370456, -0.0665123984901026, -0.00875351105536225, -2.90300269286856, 0.0, 0.0], \
35 | [0.0, 0.0, 0.0, 0.0, 0.0, 1.0], \
36 | [0, 0, 0, 0, 0, 0]]
37 | inv_mat = [[0.0, 0.0, 0.0, 0.0, 1.0, 0], [1.0, 0.0, 0.0, 0.0, 0.0, 0], [0., 0., 0., 0., 1., 0]]
38 | inv_rhs = [125.0, -100.0, passive_time]
39 | p2.set_dynamics(a_mat)
40 | p2.set_invariant(inv_mat, inv_rhs)
41 |
42 |
43 | p3 = ha.new_mode('P3')
44 | a_mat = [\
45 | [0.0, 0.0, 1.0, 0.0, 0.0, 0], \
46 | [0.0, 0.0, 0.0, 1.0, 0.0, 0], \
47 | [-0.575999943070835, 0.000262486079431672, -19.2299795908647, 0.00876275931760007, 0.0, 0], \
48 | [-0.000262486080737868, -0.575999940191886, -0.00876276068239993, -19.2299765959399, 0.0, 0], \
49 | [0.0, 0.0, 0.0, 0.0, 0.0, 1.],\
50 | [0, 0, 0, 0, 0, 0]]
51 | inv_mat = [\
52 | [-1.0, 0., 0., 0., 0., 0], \
53 | [1.0, 0., 0., 0., 0., 0], \
54 | [0., -1.0, 0., 0., 0., 0], \
55 | [0., 1.0, 0., 0., 0., 0], \
56 | [-1.0, -1.0, 0.0, 0.0, 0.0, 0], \
57 | [1.0, 1.0, 0.0, 0.0, 0.0, 0], \
58 | [-1.0, 1.0, 0., 0., 0., 0], \
59 | [1.0, -1.0, 0., 0., 0., 0], \
60 | [0., 0., 0., 0., 1., 0]]
61 | inv_rhs = [100, 100, 100, 100, 141.1, 141.1, 141.1, 141.1, passive_time]
62 | p3.set_dynamics(a_mat)
63 | p3.set_invariant(inv_mat, inv_rhs)
64 |
65 |
66 | passive = ha.new_mode('passive')
67 | a_mat = [\
68 | [0, 0, 1, 0, 0, 0], \
69 | [0, 0, 0, 1, 0, 0], \
70 | [0.0000575894721132000, 0, 0, 0.00876276, 0, 0], \
71 | [0, 0, -0.00876276, 0, 0, 0], \
72 | [0, 0, 0, 0, 0, 1.], \
73 | [0, 0, 0, 0, 0, 0]]
74 | passive.set_dynamics(a_mat)
75 |
76 |
77 | error = ha.new_mode('error')
78 |
79 | ############## Normal Transitions ##############
80 | t1 = ha.new_transition(p2, p3)
81 | guard_mat = [\
82 | [-1.0, 0., 0., 0., 0., 0], \
83 | [1.0, 0., 0., 0., 0., 0], \
84 | [0., -1.0, 0., 0., 0., 0], \
85 | [0., 1.0, 0., 0., 0., 0], \
86 | [-1.0, -1.0, 0.0, 0.0, 0.0, 0], \
87 | [1.0, 1.0, 0.0, 0.0, 0.0, 0], \
88 | [-1.0, 1.0, 0., 0., 0., 0], \
89 | [1.0, -1.0, 0., 0., 0., 0]]
90 | guard_rhs = [100, 100, 100, 100, 141.1, 141.1, 141.1, 141.1]
91 | t1.set_guard(guard_mat, guard_rhs)
92 |
93 | ha.new_transition(p2, passive).set_guard([[0.0, 0.0, 0.0, 0.0, -1.0, 0]], [-passive_time])
94 |
95 | ha.new_transition(p3, passive).set_guard([[0.0, 0.0, 0.0, 0.0, -1.0, 0]], [-passive_time])
96 |
97 | ############## Error Transitions ##############
98 | # In the aborting mode, the vehicle must avoid the target, which is modeled as a box B with
99 | # 0.2m edge length and the center placed as the origin
100 | rad = 0.2
101 | t = ha.new_transition(passive, error)
102 | guard_mat = [ \
103 | [1, 0, 0., 0., 0., 0], \
104 | [-1, 0, 0., 0., 0., 0], \
105 | [0, 1., 0., 0., 0., 0], \
106 | [0, -1., 0., 0., 0., 0]]
107 | guard_rhs = [rad, rad, rad, rad]
108 | t.set_guard(guard_mat, guard_rhs)
109 |
110 | #In the rendezvous attempt the spacecraft must remain within the lineof-sight
111 | #cone L = {[x, y]^T | (x >= -100m) AND (y >= x*tan(30)) AND (-y >= x*tan(30))}
112 | ha.new_transition(p3, error, 'los1').set_guard([[1, 0, 0., 0., 0., 0]], [-100])
113 | ha.new_transition(p3, error, 'los2').set_guard([[-0.57735, 1, 0, 0., 0., 0]], [0])
114 | ha.new_transition(p3, error, 'los3').set_guard([[-0.57735, -1, 0., 0., 0., 0]], [0])
115 |
116 | # sqrt(vx^2 + vy^2) should stay below 0.055 m/SECOND (time in model is in MINUTES)
117 | # to make the model unsafe, try changing this to 0.05
118 | meters_per_sec_limit = 0.055 if safe else 0.05
119 | meters_per_min_limit = meters_per_sec_limit * 60
120 | h = meters_per_min_limit * math.cos(math.pi / 8.0)
121 | w = meters_per_min_limit * math.sin(math.pi / 8.0)
122 |
123 | ha.new_transition(p3, error, 'g1').set_guard([[0, 0, 1., 0., 0., 0]], [-h])
124 | ha.new_transition(p3, error, 'g2').set_guard([[0, 0, -1., 0., 0., 0]], [-h])
125 | ha.new_transition(p3, error, 'g3').set_guard([[0, 0, 0., 1., 0., 0]], [-h])
126 | ha.new_transition(p3, error, 'g4').set_guard([[0, 0, 0., -1., 0., 0]], [-h])
127 |
128 | ha.new_transition(p3, error, 'g5').set_guard([[0, 0, 1., 1., 0., 0]], [-(w + h)])
129 | ha.new_transition(p3, error, 'g6').set_guard([[0, 0, -1., 1., 0., 0]], [-(w + h)])
130 | ha.new_transition(p3, error, 'g7').set_guard([[0, 0, -1., -1., 0., 0]], [-(w + h)])
131 | ha.new_transition(p3, error, 'g8').set_guard([[0, 0, 1., -1., 0., 0]], [-(w + h)])
132 |
133 | return ha
134 |
135 | def make_init(ha):
136 | 'make the initial states'
137 |
138 | p2 = ha.modes['P2']
139 |
140 | box = [(-925.0, -875.0), (-425.0, -375.0), (0.0, 0.0), (0.0, 0.0), (0.0, 0.0), (1.0, 1.0)]
141 |
142 | init_lpi = lputil.from_box(box, p2)
143 | init_list = [StateSet(init_lpi, p2)]
144 |
145 | #corners = [[(box[0][0], box[0][0]), (box[1][0], box[1][0]), (0, 0), (0, 0), (0, 0), (1, 1)], \
146 | # [(box[0][1], box[0][1]), (box[1][0], box[1][0]), (0, 0), (0, 0), (0, 0), (1, 1)], \
147 | # [(box[0][1], box[0][1]), (box[1][1], box[1][1]), (0, 0), (0, 0), (0, 0), (1, 1)], \
148 | # [(box[0][0], box[0][0]), (box[1][0], box[1][0]), (0, 0), (0, 0), (0, 0), (1, 1)]]
149 |
150 | #init_list = [StateSet(lputil.from_box(c, p2), p2) for c in corners]
151 |
152 | return init_list
153 |
154 | def make_settings(safe, passive_time=120):
155 | 'make the reachability settings object'
156 |
157 | # see hylaa.settings for a list of reachability settings
158 | settings = HylaaSettings(0.1, 300.0) # step: 0.1, bound: 200.0
159 | settings.plot.plot_mode = PlotSettings.PLOT_NONE #IMAGE
160 | settings.stdout = HylaaSettings.STDOUT_VERBOSE
161 |
162 | #settings.aggstrat = aggstrat.Unaggregated()
163 | #settings.stop_on_concrete_error = False
164 |
165 | settings.stop_on_aggregated_error = False
166 | #settings.process_urgent_guards = True
167 |
168 | settings.aggstrat.deaggregate = True # use deaggregation
169 | settings.aggstrat.deagg_preference = aggstrat.Aggregated.DEAGG_LEAVES_FIRST
170 |
171 | settings.plot.filename = f"rendezvous_t{passive_time:03}.png"
172 | settings.plot.plot_size = (8, 10)
173 |
174 | settings.plot.xdim_dir = [0, 0, 2]
175 | settings.plot.ydim_dir = [1, 1, 3]
176 | settings.plot.label = [LabelSettings(), LabelSettings(), LabelSettings()]
177 |
178 | settings.plot.label[0].big(size=26)
179 | settings.plot.label[0].title_size = 22
180 | settings.plot.label[1].big(size=26)
181 | settings.plot.label[2].big(size=26)
182 |
183 | settings.plot.label[0].x_label = '$x$'
184 | settings.plot.label[0].y_label = '$y$'
185 |
186 | settings.plot.label[1].x_label = '$x$'
187 | settings.plot.label[1].y_label = '$y$'
188 |
189 | settings.plot.label[2].x_label = '$x_{vel}$'
190 | settings.plot.label[2].y_label = '$y_{vel}$'
191 |
192 | settings.plot.label[0].axes_limits = [-950, 200, -450, 200]
193 | #settings.plot.label[1].axes_limits = [-10, 10, -10, 10]
194 | settings.plot.label[1].axes_limits = [-110, 10, -110, 110]
195 | settings.plot.label[2].axes_limits = [-5, 5, -5, 5]
196 |
197 | y = 57.735
198 | line = [(-100, y), (-100, -y), (0, 0), (-100, y)]
199 | c1 = collections.LineCollection([line], animated=True, colors=('gray'), linewidths=(2), linestyle='dashed')
200 | c1_copy = collections.LineCollection([line], animated=True, colors=('gray'), linewidths=(2), linestyle='dashed')
201 |
202 | rad = 0.2
203 | line = [(-rad, -rad), (-rad, rad), (rad, rad), (rad, -rad), (-rad, -rad)]
204 | c2 = collections.LineCollection([line], animated=True, colors=('red'), linewidths=(2))
205 | c2_copy = collections.LineCollection([line], animated=True, colors=('red'), linewidths=(2))
206 |
207 | meters_per_sec_limit = 0.055 if safe else 0.05
208 | meters_per_min_limit = meters_per_sec_limit * 60
209 | x = meters_per_min_limit * math.cos(math.pi / 8.0)
210 | y = meters_per_min_limit * math.sin(math.pi / 8.0)
211 |
212 | line = [(-x, y), (-y, x), (y, x), (x, y), (x, -y), (y, -x), (-y, -x), (-x, -y), (-x, y)]
213 | vel_oct = collections.LineCollection([line], animated=True, colors=('gray'), linewidths=(2), linestyle='dashed')
214 |
215 | r = meters_per_min_limit
216 | patch_col = collections.PatchCollection([Circle((0, 0), r)], alpha=0.25)
217 |
218 | settings.plot.extra_collections = [[c1_copy, c2_copy], [c1, c2], [vel_oct, patch_col]]
219 |
220 | return settings
221 |
222 | def run_hylaa():
223 | 'main entry point'
224 |
225 | safe = True
226 |
227 | for passive_time in [120]: #range(5, 265, 5):
228 | print(passive_time)
229 | ha = make_automaton(safe, passive_time)
230 | init_states = make_init(ha)
231 | settings = make_settings(safe, passive_time)
232 | Core(ha, settings).run(init_states)
233 |
234 | if __name__ == "__main__":
235 | run_hylaa()
236 |
237 |
--------------------------------------------------------------------------------
/hylaa/check_trace.py:
--------------------------------------------------------------------------------
1 | '''
2 | Generate concrete traces from counter-examples found by HyLAA.
3 |
4 | The check() function performs a concrete simulation to find check close
5 | a violation found by HyLAA is to an actual simulation.
6 |
7 | Stanley Bak
8 | December 2016
9 | '''
10 |
11 | import time
12 | import math
13 |
14 | import matplotlib.pyplot as plt
15 | import numpy as np
16 |
17 | from scipy.integrate import odeint
18 | from scipy.sparse import csr_matrix
19 |
20 | from hylaa.util import Freezable
21 |
22 | class CheckTraceData(Freezable):
23 | 'class containing data about the counter-example trace'
24 |
25 | def __init__(self):
26 | self.sim_time = None
27 | self.abs_error = None
28 | self.rel_error = None
29 |
30 | self.freeze_attrs()
31 |
32 | def make_der_func(a_matrix, b_matrix, input_vec):
33 | 'make the derivative function with the given paremeters'
34 |
35 | assert isinstance(a_matrix, csr_matrix)
36 |
37 | if b_matrix is not None:
38 | assert isinstance(b_matrix, csr_matrix)
39 |
40 | input_vec = np.array(input_vec, dtype=float)
41 |
42 | def der_func(state, _):
43 | 'the constructed derivative function'
44 |
45 | no_input = a_matrix * state
46 | no_input.shape = (state.shape[0],)
47 |
48 | if b_matrix is None:
49 | rv = no_input
50 | else:
51 | input_effects = b_matrix * input_vec
52 | input_effects.shape = (state.shape[0],)
53 |
54 | rv = np.add(no_input, input_effects)
55 |
56 | return rv
57 |
58 | return der_func
59 |
60 | def check(a_matrix, b_matrix, step, max_time, start_point, inputs, normal_vec, end_val, \
61 | quick=False, stdout=True, approx_samples=2000):
62 | '''Run a simulation with the given dynamics and inputs to see how close it matches with HyLAA's prediction
63 |
64 | The comparison is printed showing the differences between the simulation's projection onto normal_vec
65 | and expected result (passed in as end_val), both in terms of abs_error and rel_error
66 |
67 | Returns a tuple, (states, times, CheckTraceData) where states is the projected simulation state at each time step
68 | '''
69 |
70 | a_matrix = csr_matrix(a_matrix)
71 |
72 | if b_matrix is not None:
73 | b_matrix = csr_matrix(b_matrix)
74 |
75 | num_dims = a_matrix.shape[0]
76 | assert num_dims > 0
77 | assert a_matrix.shape[0] == a_matrix.shape[1], "expected A matrix to be a square"
78 | assert len(start_point) == num_dims
79 | total_steps = int(round(max_time / step))
80 |
81 | assert abs(total_steps * step - max_time) < 1e-9, "Rounding issue with number of steps"
82 |
83 | if inputs is None:
84 | inputs = [0] * (total_steps)
85 |
86 | assert len(inputs) <= total_steps, "more inputs({}) than steps({})?".format(len(inputs), total_steps)
87 |
88 | data = CheckTraceData()
89 | data.sim_time = max_time
90 |
91 | # we want to roughly get the desired number of sample points, so we may need to do multiple
92 | # samples per input
93 | samples_per_input = approx_samples / max(1, len(inputs))
94 | if samples_per_input < 1:
95 | samples_per_input = 1
96 |
97 | start = time.time()
98 | sim_states = [start_point]
99 | sim_times = [0.0]
100 |
101 | index = 0
102 |
103 | while index < total_steps and index < len(inputs):
104 | num_steps = 1
105 |
106 | # try doing multiple steps at a time (better performance)
107 | while index + num_steps < total_steps and index + num_steps < len(inputs):
108 | if np.allclose(inputs[index], inputs[index + num_steps]):
109 | num_steps += 1
110 | else:
111 | break
112 |
113 | elapsed = time.time() - start
114 | remaining = elapsed / ((1.0+index) / len(inputs)) - elapsed
115 |
116 | if stdout and num_steps > 1 and remaining > 2.0:
117 | print("Combining {} steps (identical inputs)".format(num_steps))
118 |
119 | if stdout and remaining > 2.0:
120 | print("{} / {} (ETA: {:.2f} sec)".format(index, len(inputs), remaining))
121 |
122 | der_func = make_der_func(a_matrix, b_matrix, inputs[index])
123 | new_states, new_times = sim(sim_states[-1], der_func, step * num_steps, samples_per_input * num_steps, quick)
124 |
125 | sim_states += [s for s in new_states]
126 | time_offset = index * step
127 | sim_times += [t + time_offset for t in new_times]
128 |
129 | assert len(sim_states) == len(sim_times), "sim_states len = {}, sim_times len = {}".format(
130 | len(sim_states), len(sim_times))
131 |
132 | index += num_steps
133 |
134 | last_sim_point = sim_states[-1].copy()
135 |
136 | sim_val = np.dot(last_sim_point, normal_vec)
137 | diff = sim_val - end_val
138 |
139 | # normal_vec is -1.0
140 |
141 | data.abs_error = numerator = abs(diff)
142 | denominator = abs(sim_val)
143 |
144 | if denominator == 0:
145 | data.rel_error = 0.0
146 | else:
147 | data.rel_error = numerator / denominator
148 |
149 | if stdout:
150 | print("Final Time: {}".format(index * step))
151 | print("Absolute Error (l-2 norm): {}".format(numerator))
152 | print("Relative Error (l-2 norm): {}".format(data.rel_error))
153 | print("Runtime: {:.2f} seconds".format(time.time() - start))
154 |
155 | return (sim_states, sim_times, data)
156 |
157 | def sim(start, der_func, time_amount, num_steps, quick):
158 | 'simulate for some fixed time, and return the resultant (states, times) tuple'
159 |
160 | tol = 1.49012e-8
161 |
162 | if not quick:
163 | tol /= 1e5 # more accurate simulation
164 |
165 | times = np.linspace(0, time_amount, num=1 + num_steps)
166 | states = odeint(der_func, start, times, col_deriv=True, rtol=tol, atol=tol, mxstep=100000)
167 |
168 | states = states[1:]
169 | times = times[1:]
170 | assert len(states) == num_steps
171 | assert len(times) == num_steps
172 |
173 | return (states, times)
174 |
175 | def plot(sim_states, sim_times, inputs, normal_vec, normal_val, max_time, step, xdim=None, ydim=None):
176 | '''plot the simulation trace in the normal direction
177 |
178 | if xdim and dim are not none, then a 2-d phase portrait plot will be given, otherwise a time-history plot
179 | is provided
180 | '''
181 |
182 | do_2d = xdim is not None and ydim is not None
183 |
184 | total_steps = int(math.ceil(max_time / step))
185 | end_point = sim_states[-1]
186 | end_val = np.dot(end_point, normal_vec)
187 | tol = 1e-6
188 |
189 | if len(end_point) < 10:
190 | print("End Point: {}".format(end_point))
191 |
192 | if end_val - tol <= normal_val:
193 | print("End Point is a violation (within tolerance): {} - {} <= {}".format(end_val, tol, normal_val))
194 | else:
195 | print("End point is NOT a violation: {} - {} (tolerance) > {}".format(end_val, tol, normal_val))
196 |
197 | epsilon = step / 8.0 # to prevent round-off error on the end range
198 | input_times = np.arange(0.0, max_time + epsilon, step)
199 |
200 | if inputs is not None:
201 | _, ax = plt.subplots(2, sharex=not do_2d, figsize=(14, 8))
202 | else:
203 | _, ax = plt.subplots(1, figsize=(14, 5))
204 | ax = [ax]
205 |
206 | if do_2d:
207 | xs = [state[xdim] for state in sim_states]
208 | ys = [state[ydim] for state in sim_states]
209 |
210 | ax[0].plot(xs, ys, 'k-', lw=2, label='Simulation')
211 |
212 | ax[0].plot(xs[-1], ys[-1], 'o', ms=10, mew=3, mec='red', mfc='none', label='End')
213 | ax[0].plot(xs[0], ys[0], '*', ms=10, mew=3, mec='blue', mfc='none', label='Start')
214 |
215 | ax[0].set_ylabel('Dim {}'.format(ydim), fontsize=22)
216 | ax[0].set_xlabel('Dim {}'.format(xdim), fontsize=22)
217 | else:
218 | normal_trace = np.dot(sim_states, normal_vec)
219 | ax[0].plot(sim_times, normal_trace, 'k-', lw=2, label='Simulation')
220 | ax[0].plot(sim_times[-1], normal_trace[-1], 'o', ms=10, mew=3, mec='red', mfc='none')
221 | ax[0].plot([sim_times[0], sim_times[-1]], [normal_val, normal_val], 'k--', lw=2, label='Violation')
222 |
223 | if inputs is None:
224 | ax[0].set_xlabel('Time', fontsize=22)
225 |
226 | ax[0].set_ylabel('Projected State', fontsize=22)
227 | ax[0].set_title('Counter-Example Trace', fontsize=28)
228 |
229 | if inputs is not None and len(inputs) > 0:
230 | inputs.append(inputs[-1]) # there is one less input than time instants
231 | inputs = np.array(inputs, dtype=float)
232 |
233 | flat_inputs = []
234 | flat_times = []
235 |
236 | for i in xrange(total_steps-1):
237 | flat_times += [input_times[i], input_times[i+1]]
238 | flat_inputs += [inputs[i, :], inputs[i, :]]
239 |
240 | # single step edge case
241 | if len(flat_inputs) == 0:
242 | flat_times += [input_times[0], input_times[0]]
243 | flat_inputs += [inputs[0, :], inputs[0, :]]
244 |
245 | for row in xrange(len(flat_inputs[0])):
246 | ax[1].plot(flat_times, [single_input[row] for single_input in flat_inputs], label="$u_{}$".format(row+1))
247 |
248 | ##################
249 | # visual
250 |
251 | ax[0].set_title('Simulation', fontsize=28)
252 | ax[0].tick_params(axis='both', which='major', labelsize=18)
253 |
254 | if inputs is not None:
255 | ax[1].set_ylabel('Input', fontsize=22)
256 | ax[1].set_xlabel('Time', fontsize=22)
257 | ax[1].tick_params(axis='both', which='major', labelsize=18)
258 |
259 | ##################
260 | # legend
261 | for i in xrange(2 if inputs is not None else 1):
262 | legend = ax[i].legend(loc='best', numpoints=1)
263 |
264 | # Set the fontsize
265 | for label in legend.get_texts():
266 | label.set_fontsize('large')
267 |
268 | for label in legend.get_lines():
269 | label.set_linewidth(1.5) # the legend line width
270 |
271 | lim = ax[1 if inputs is not None else 0].get_ylim()
272 | dy = lim[1] - lim[0]
273 | plt.ylim(lim[0] - dy * .1, lim[1] + dy * .1)
274 |
275 | if not do_2d:
276 | plt.xlim(0, max_time * 1.02)
277 | elif len(ax) > 1:
278 | ax[1].set_xlim(0, max_time * 1.02)
279 |
280 | plt.tight_layout()
281 |
282 | plt.show()
283 |
--------------------------------------------------------------------------------
/hylaa/result.py:
--------------------------------------------------------------------------------
1 | '''
2 | Stanley Bak
3 |
4 | Code assocaited with counterexamples.
5 | '''
6 |
7 | from collections import defaultdict
8 | from collections import deque
9 |
10 | import numpy as np
11 |
12 | from sympy import Polygon, Point, Point2D
13 |
14 | from scipy.integrate import odeint
15 | from scipy.sparse import csr_matrix
16 |
17 | from hylaa.aggstrat import get_ancestors
18 | from hylaa.util import Freezable
19 |
20 | class HylaaResult(Freezable): # pylint: disable=too-few-public-methods
21 | 'result object returned by core.run()'
22 |
23 | def __init__(self):
24 | self.top_level_timer = None # TimerData for total time
25 |
26 | # verification result:
27 | self.has_aggregated_error = False
28 | self.has_concrete_error = False
29 |
30 | self.counterexample = [] # if unsafe, a list of CounterExampleSegment objects
31 |
32 | # assigned if setting.plot.store_plot_result is True, an instance of PlotData
33 | self.plot_data = None
34 |
35 | # the last core.cur_state object... used for unit testing
36 | self.last_cur_state = None
37 |
38 | # lines plotted for simulations
39 | # nested lists. first index is plot number, second is segment number,
40 | # third is simulation number, last is list of 2-d points
41 | self.sim_lines = None
42 |
43 | self.freeze_attrs()
44 |
45 | class PlotData(Freezable):
46 | 'used if setting.plot.store_plot_result is True, stores data about the plots'
47 |
48 | def __init__(self, num_plots):
49 | # first index is plot number, second is mode name, result is a list of obj
50 | # each obj is a tuple: (verts, state, cur_step_in_mode, str_description)
51 | self.mode_to_obj_list = [defaultdict(list) for _ in range(num_plots)]
52 |
53 | self.freeze_attrs()
54 |
55 | def get_verts_list(self, mode_name, plot_index=0):
56 | 'get a list of lists for the passed-in mode'
57 |
58 | return self.mode_to_obj_list[plot_index][mode_name][0]
59 |
60 | def add_state(self, state, verts, plot_index):
61 | 'add a plotted state'
62 |
63 | mode_name = state.mode.name
64 |
65 | obj = (verts, state, state.cur_step_in_mode, f"{mode_name} at step {state.cur_step_in_mode}")
66 |
67 | self.mode_to_obj_list[plot_index][mode_name].append(obj)
68 |
69 | def remove_state(self, state, step):
70 | 'remove a state that was previously added'
71 |
72 | found = False
73 |
74 | for mode_to_obj in self.mode_to_obj_list:
75 | obj_list = mode_to_obj[state.mode.name]
76 |
77 | for index, obj in enumerate(obj_list):
78 | _, istate, istep, desc = obj
79 |
80 | if istate is state and step == istep:
81 | found = True
82 |
83 | obj_list.pop(index)
84 | break
85 |
86 | if found:
87 | break
88 |
89 | assert found
90 |
91 | def get_plot_data(self, x, y, subplot=0):
92 | 'get the plot data at x, y, or None if not found'
93 |
94 | rv = None
95 |
96 | mode_to_obj = self.mode_to_obj_list[subplot]
97 | clicked = Point(x, y)
98 |
99 | for mode, obj_list in mode_to_obj.items():
100 |
101 | for obj in obj_list:
102 | verts = obj[0]
103 |
104 | if len(verts) < 4: # need at least 3 points (4 with wrap) to be clicked inside
105 | continue
106 |
107 | verts_2dp = [Point2D(x, y) for x, y in verts[1:]]
108 |
109 | poly = Polygon(*verts_2dp)
110 |
111 | if poly.encloses_point(clicked):
112 | rv = obj
113 | break
114 |
115 | if rv is not None:
116 | break
117 |
118 | return rv
119 |
120 | class CounterExampleSegment(Freezable):
121 | 'a part of a counter-example trace'
122 |
123 | def __init__(self):
124 | self.mode = None # Mode object
125 | self.start = []
126 | self.end = []
127 | self.steps = 0
128 | self.outgoing_transition = None # Transition object
129 | self.reset_minkowski_vars = [] # a list of minkowski variables in the outgoing reset
130 |
131 | self.inputs = deque() # inputs at each step (a deque of m-tuples, where m is the number of inputs)
132 |
133 | self.freeze_attrs()
134 |
135 | def __str__(self):
136 | return "[CE_Segment: {} -> {} in '{}']".format( \
137 | self.start, self.end, "" if self.mode is None else self.mode.name)
138 |
139 | def __repr__(self):
140 | return str(self)
141 |
142 | def make_counterexample(ha, state, transition_to_error, lpi):
143 | '''make and return the result counter-example from the lp solution'''
144 |
145 | lp_solution = lpi.minimize() # resolves the LP to get the full unsafe solution
146 | names = lpi.get_names()
147 |
148 | # first get the number of steps in each mode
149 | num_steps = []
150 |
151 | if state.aggdag_op_list[0] is not None:
152 | for node in get_ancestors(state.aggdag_op_list[0].child_node)[1:]:
153 | assert len(node.parent_ops) == 1
154 | num_steps.append(node.parent_ops[0].step)
155 |
156 | num_steps.append(state.cur_step_in_mode) # add the last mode
157 |
158 | counterexample = []
159 |
160 | for name, value in zip(names, lp_solution):
161 |
162 | # if first initial variable of mode then assign the segment.mode variable
163 | if name.startswith('m') and '_i0' in name:
164 | seg = CounterExampleSegment()
165 | seg.steps = num_steps[len(counterexample)]
166 | counterexample.append(seg)
167 |
168 | parts = name.split('_')
169 |
170 | if len(parts) == 2:
171 | assert len(counterexample) == 1, "only the initial mode should have no predecessor transition"
172 | else:
173 | assert len(parts) == 3
174 |
175 | # assign outgoing transition of previous counterexample segment
176 | transition_index = int(parts[2][1:])
177 | t = counterexample[-2].mode.transitions[transition_index]
178 | counterexample[-2].outgoing_transition = t
179 |
180 | mode_id = int(parts[0][1:])
181 |
182 | for mode in ha.modes.values():
183 | if mode.mode_id == mode_id:
184 | seg.mode = mode
185 | break
186 |
187 | assert seg.mode is not None, "mode id {} not found in automaton".format(mode_id)
188 |
189 | if name.startswith('m'): # mode variable
190 | if '_i' in name:
191 | seg.start.append(value)
192 | elif '_c' in name:
193 | seg.end.append(value)
194 | elif '_I' in name:
195 | if '_I0' in name:
196 | seg.inputs.appendleft([])
197 |
198 | # inputs are in backwards order due to how the LP is constructed, prepend it
199 | seg.inputs[0].append(value)
200 |
201 | elif name.startswith('reset'):
202 | seg.reset_minkowski_vars.append(value)
203 |
204 | # add the final transition which is not encoded in the names of the variables
205 | seg.outgoing_transition = transition_to_error
206 |
207 | return counterexample
208 |
209 |
210 | def replay_counterexample(ce_segment_list, ha, settings):
211 | '''replay the counterexample
212 |
213 | returns a list of points and a list of times
214 | '''
215 |
216 | rv = []
217 | all_times = []
218 |
219 | epsilon = 1e-7
220 | step_size = settings.step_size
221 |
222 | for i, segment in enumerate(ce_segment_list):
223 | inv_list = segment.mode.inv_list
224 | a_mat = segment.mode.a_csr
225 | b_mat = segment.mode.b_csr
226 |
227 | assert b_mat is None, "todo: implement counter-example generation with inputs"
228 |
229 | der_func = make_der_func(a_mat, b_mat, [])
230 |
231 | start = segment.start
232 | times = np.linspace(0, segment.steps * step_size, num=segment.steps)
233 | tol = 1e-9
234 | states = odeint(der_func, start, times, col_deriv=True, rtol=tol, atol=tol, mxstep=int(1e7))
235 |
236 | rv += [s for s in states]
237 | t_offset = 0
238 |
239 | if all_times:
240 | t_offset = all_times[-1]
241 |
242 | all_times += [t + t_offset for t in times]
243 |
244 | # check if in invariant up to the last step
245 | for state in states[:-1]:
246 | for lc in inv_list:
247 | lhs = lc.csr * state
248 |
249 | assert lhs <= lc.rhs + epsilon, "invariant became false during replay counterexample"
250 |
251 | assert np.allclose(states[-1], segment.end)
252 |
253 | # check that last state -> reset -> first state is correct
254 | t = segment.outgoing_transition
255 | lhs = t.guard_csr * states[-1]
256 |
257 | for left, right in zip(lhs, t.guard_rhs):
258 | assert left <= right + epsilon, "guard was not enabled during replay counterexample"
259 |
260 | assert not t.reset_minkowski_csr, "unimplemented: resets with minkowski sums"
261 |
262 | if t.reset_csr is not None:
263 | poststate = t.reset_csr * states[-1]
264 |
265 | if i + 1 < len(ce_segment_list):
266 | next_prestate = ce_segment_list[i+1].start
267 |
268 | assert np.allclose(poststate, next_prestate)
269 | else:
270 | rv.append(poststate)
271 | all_times.append(all_times[-1])
272 |
273 | return rv, all_times
274 |
275 | def make_der_func(a_matrix, b_matrix, input_vec):
276 | 'make the derivative function with the given paremeters'
277 |
278 | assert isinstance(a_matrix, csr_matrix)
279 |
280 | if b_matrix is not None:
281 | assert isinstance(b_matrix, csr_matrix)
282 |
283 | input_vec = np.array(input_vec, dtype=float)
284 |
285 | def der_func(state, _):
286 | 'the constructed derivative function'
287 |
288 | no_input = a_matrix * state
289 | no_input.shape = (state.shape[0],)
290 |
291 | if b_matrix is None:
292 | rv = no_input
293 | else:
294 | input_effects = b_matrix * input_vec
295 | input_effects.shape = (state.shape[0],)
296 |
297 | rv = np.add(no_input, input_effects)
298 |
299 | return rv
300 |
301 | return der_func
302 |
--------------------------------------------------------------------------------
/hylaa/aggstrat.py:
--------------------------------------------------------------------------------
1 | ''''
2 | Stanley Bak
3 | Aggregation Strategy Classes
4 | '''
5 |
6 | from collections import namedtuple
7 |
8 | from hylaa import lputil
9 | from hylaa.util import Freezable
10 |
11 | # container class for description of how to perform a single aggregation
12 | AggType = namedtuple('AggType', ['is_box', 'is_arnoldi_box', 'is_chull', 'add_guard'])
13 |
14 | class AggregationStrategy(Freezable):
15 | '''Aggregation Strategy parent class
16 |
17 | An aggregation strategy controls the evolution of the AggDag. This is done by choosing which transitions create
18 | the current node (aggregation), splitting existing nodes (deaggregation) and deciding if/when to stop the
19 | current node's continuous-post computation (pseudo-transitions).
20 |
21 | The default implementation in this class will perform no aggregation (pop first transition), never split nodes,
22 | and never stop the current computation. This means it's exact, but analysis can require an exponential amount of
23 | computation.
24 | '''
25 |
26 | def __init__(self):
27 | self.freeze_attrs()
28 |
29 | def init_aggdag_data(self, aggdag):
30 | 'initialize data within the aggdag object by directly assigning to aggdag instance'
31 |
32 | pass
33 |
34 | def init_aggdag_node_data(self, aggdag_node):
35 | 'initialize data within every aggdag node by directly assigning to the aggdag_node instance'
36 |
37 | pass
38 |
39 | def pop_waiting_list(self, waiting_list):
40 | '''determine which waiting list elements should be aggregated for the next continuous post computation
41 |
42 | waiting_list is a list of OpTransition
43 |
44 | this function returns a list of OpTransition.
45 | If the list is a single element, no aggregation is performed.
46 | '''
47 |
48 | return [waiting_list[0]]
49 |
50 | def get_deagg_node(self, aggdag):
51 | '''Called before popping a state off the waiting list. Get the aggdag node to deaggregate (if any).
52 | '''
53 |
54 | return None
55 |
56 | def pretransition(self, t, t_lpi, op_transition):
57 | '''event function, called when taking a transition before the reset is applied
58 |
59 | returns True if succeeded, False on LP errors
60 | '''
61 |
62 | # premode_center = lputil.get_box_center(t_lpi)
63 | return True
64 |
65 | def get_agg_type(self, op_list):
66 | '''
67 | Gets the type of aggregation to be performed for the passed in objects.
68 |
69 | This returns an instance of AggType
70 | '''
71 |
72 | raise NotImplementedError("Unaggregated strategy does not implement get_agg_type")
73 |
74 | class Unaggregated(AggregationStrategy):
75 | 'another name for the base implementation of AggregationStrategy'
76 |
77 | class Aggregated(AggregationStrategy):
78 | 'a fully aggregated strategy'
79 |
80 | AGG_BOX, AGG_ARNOLDI_BOX, AGG_CONVEX_HULL = range(3)
81 | POP_LOWEST_MINTIME, POP_LOWEST_AVGTIME, POP_LARGEST_MAXTIME = range(3)
82 | DEAGG_LEAVES_FIRST, DEAGG_ROOT_FIRST, DEAGG_MOST_STATES = range(3)
83 |
84 | def __init__(self, agg_type=AGG_ARNOLDI_BOX, deaggregate=False):
85 | self.agg_type = agg_type
86 | self.pop_type = Aggregated.POP_LOWEST_AVGTIME
87 |
88 | self.add_guard = True # add the guard direction when performing aggregation
89 | self.require_same_path = True
90 | self.deaggregate = deaggregate
91 | self.deagg_preference = Aggregated.DEAGG_MOST_STATES
92 |
93 | self.sim_avoid_modes = [] # list of mode names to try to avoid during simulation
94 |
95 | AggregationStrategy.__init__(self)
96 |
97 | def pop_waiting_list(self, waiting_list):
98 | '''
99 | Get the states to remove from the waiting list based on a score-based method
100 | '''
101 |
102 | # use score method to decide which mode to pop
103 | to_remove_op = waiting_list[0]
104 | to_remove_state = to_remove_op.poststate
105 | to_remove_score = self.pop_score(to_remove_state)
106 |
107 | for op in waiting_list:
108 | state = op.poststate
109 | score = self.pop_score(state)
110 |
111 | if score > to_remove_score or score == to_remove_score and state.mode.name < to_remove_state.mode.name:
112 | to_remove_score = score
113 | to_remove_state = state
114 | to_remove_op = op
115 |
116 | # remove all states for aggregation
117 | op_list = []
118 |
119 | for op in waiting_list:
120 | state = op.poststate
121 | should_add = False
122 |
123 | if state is to_remove_state:
124 | should_add = True
125 | elif state.mode is to_remove_state.mode:
126 | if not self.require_same_path:
127 | should_add = True
128 | else: # require same path; paths are the same if the parent node and transitions match
129 | if to_remove_op is None and op is None:
130 | should_add = True
131 | elif to_remove_op and op:
132 | if to_remove_op.transition is op.transition and \
133 | to_remove_op.parent_node is op.parent_node:
134 | should_add = True
135 |
136 | if should_add:
137 | op_list.append(op)
138 |
139 | return op_list
140 |
141 | def pop_score(self, state):
142 | '''
143 | returns a score used to decide which state to pop off the waiting list. The state with the highest score
144 | will be removed first.
145 | '''
146 |
147 | if self.pop_type == Aggregated.POP_LOWEST_MINTIME:
148 | score = -(state.cur_steps_since_start[0])
149 | elif self.pop_type == Aggregated.POP_LOWEST_AVGTIME:
150 | score = -(state.cur_steps_since_start[0] + state.cur_steps_since_start[1])
151 | elif self.pop_type == Aggregated.POP_LARGEST_MAXTIME:
152 | score = state.cur_steps_since_start[1]
153 | else:
154 | raise RuntimeError("Unknown waiting list pop strategy: {}".format(self.pop_type))
155 |
156 | return score
157 |
158 | def get_simulation_pop_mode(self, sim_waiting_list):
159 | '''
160 | returns the mode to be popped off the simulation waiting list
161 | '''
162 |
163 | # find minimum time mode
164 | min_time_mode = None
165 | min_time_steps = float('inf')
166 |
167 | for mode, _, steps in sim_waiting_list:
168 | if mode.name in self.sim_avoid_modes:
169 | steps = float('inf')
170 |
171 | if steps < min_time_steps:
172 | min_time_mode = mode
173 | min_time_steps = steps
174 |
175 | if min_time_mode is None: # all are avoid modes
176 | min_time_mode = sim_waiting_list[0][0]
177 |
178 | return min_time_mode
179 |
180 | def pretransition(self, t, t_lpi, op_transition):
181 | 'event function, called when taking a transition before the reset is applied'
182 |
183 | rv = True
184 |
185 | if self.agg_type == Aggregated.AGG_ARNOLDI_BOX:
186 | op_transition.premode_center = lputil.get_box_center(t_lpi)
187 |
188 | if op_transition.premode_center is None:
189 | rv = False
190 |
191 | return rv
192 |
193 | def get_agg_type(self, op_list):
194 | '''
195 | Gets the type of aggregation to be performed for the passed in objects.
196 |
197 | This returns an instance of AggType
198 | '''
199 |
200 | return self._get_agg_type()
201 |
202 | def _get_agg_type(self):
203 | '''
204 | Gets the type of aggregation to be performed.
205 | '''
206 |
207 | is_box = self.agg_type == Aggregated.AGG_BOX
208 | is_arnoldi_box = self.agg_type == Aggregated.AGG_ARNOLDI_BOX
209 | is_chull = self.agg_type == Aggregated.AGG_CONVEX_HULL
210 |
211 | return AggType(is_box, is_arnoldi_box, is_chull, self.add_guard)
212 |
213 | def get_deagg_node(self, aggdag):
214 | '''Called before popping a state off the waiting list. Get the aggdag node to deaggregate (if any).
215 | '''
216 |
217 | rv = None
218 |
219 | if self.deaggregate:
220 |
221 | # if nodes have multiple outgoing transitions they will become deaggregated
222 | node_to_transition = {}
223 |
224 | for op in aggdag.waiting_list:
225 | state = op.poststate
226 |
227 | if state.is_concrete: # only try to split non-concrete states
228 | continue
229 |
230 | # highest deaggregation priority: non-concrete states that reach an error mode
231 | if state.mode.is_error():
232 | rv = op.parent_node
233 | #break
234 |
235 | # other deaggregation condition: different outgoing transitions from the same node
236 | if op.parent_node in node_to_transition:
237 | transition = node_to_transition[op.parent_node]
238 |
239 | if op.transition is not transition:
240 | rv = op.parent_node
241 | else:
242 | node_to_transition[op.parent_node] = op.transition
243 |
244 | assert op.parent_node.node_left_invariant()
245 |
246 | # if it's a single state that looks like it reaches the time bound
247 | #if op.parent_node.op_list[-1].reached_time_bound:
248 | # # both a transition and reached time bound... split it
249 | # rv = op.parent_node
250 | # print(f"rv is state with a transition that also reached the time bound")
251 |
252 | # split earlier or latest ancestor, depending on settings
253 | if rv:
254 | ancestors = get_ancestors(rv)
255 |
256 | if self.deagg_preference == Aggregated.DEAGG_LEAVES_FIRST:
257 | ancestors.reverse()
258 | elif self.deagg_preference == Aggregated.DEAGG_MOST_STATES:
259 | # sort ancestors by the number of parent states
260 | count_parent_ops = lambda node: len(node.parent_ops)
261 | ancestors = reversed(sorted(ancestors, key=count_parent_ops))
262 |
263 | rv = None
264 |
265 | for node in ancestors:
266 | if len(node.parent_ops) > 1:
267 | rv = node
268 | break
269 |
270 | assert rv is not None, "didn't find aggregated ancestor?"
271 |
272 | return rv
273 |
274 | def get_ancestors(node):
275 | '''
276 | get an ordered list of all the ancestors, starting from the root to the leaf
277 | '''
278 |
279 | rv = []
280 |
281 | if node.parent_ops[0].parent_node is not None:
282 | rv += get_ancestors(node.parent_ops[0].parent_node)
283 |
284 | rv.append(node)
285 |
286 | return rv
287 |
--------------------------------------------------------------------------------
/hylaa/lpplot.py:
--------------------------------------------------------------------------------
1 | '''
2 | Stanley Bak
3 | Functions related to plotting a set of lp constriants
4 |
5 | get_verts is probably the main one to use
6 |
7 | make_plot_vecs is useful for controlling the accuracy (and decreasing overhead compared w/ not passing it to get_verts)
8 | '''
9 |
10 | import math
11 | import numpy as np
12 | from scipy.spatial import ConvexHull
13 |
14 | import hylaa.kamenev as kamenev
15 | from hylaa.timerutil import Timers
16 |
17 | def pt_to_plot_xy(pt, xdim=0, ydim=1, cur_time=0.0):
18 | '''convert a point to an x/y pair for plotting
19 | xdim and ydim can be either an integer (dimension number), an np.array (direction), or None (time will be used)
20 | '''
21 |
22 | x = y = float(cur_time)
23 |
24 | if isinstance(xdim, int):
25 | x = pt[xdim]
26 | elif xdim:
27 | assert isinstance(xdim, np.ndarray)
28 | x = np.dot(pt, xdim)
29 |
30 | if ydim and isinstance(ydim, int):
31 | y = pt[ydim]
32 | elif ydim:
33 | assert isinstance(ydim, np.ndarray)
34 | y = np.dot(pt, ydim)
35 |
36 | return x, y
37 |
38 | def get_verts(lpi, xdim=0, ydim=1, plot_vecs=None, cur_time=0.0):
39 | '''get the vertices defining (an underapproximation) of the outside of the given linear constraints
40 | These will be usable for plotting, so that rv[0] == rv[-1]. A single point may be returned if the constraints
41 | are (close to) a single point.
42 |
43 | xdim and ydim can be either an integer (dimension number), an np.array (direction), or None (time will be used)
44 |
45 | cur_time can be an integer or a list of length 2 (min/max time)
46 |
47 | plot_vecs is an ordered list of vectors defining all the 2-d directions to optimize in... if None will
48 | construct and use 256 equally spaced vectors
49 | '''
50 |
51 | tol = 1e-9
52 |
53 | if plot_vecs is None:
54 | plot_vecs = make_plot_vecs()
55 |
56 | # make cur_time a 2-tuple: (min_time, max time)
57 | if isinstance(cur_time, (float, int)):
58 | cur_time = [float(cur_time), float(cur_time)]
59 |
60 | #try:
61 | if xdim is None and ydim is None:
62 | if abs(cur_time[0] - cur_time[1]) < tol:
63 | pts = [[cur_time, cur_time]]
64 | else:
65 | pts = []
66 | pts.append([cur_time.min, cur_time.min])
67 | pts.append([cur_time.min, cur_time.max])
68 | pts.append([cur_time.max, cur_time.max])
69 | pts.append([cur_time.max, cur_time.min])
70 |
71 | elif xdim is None:
72 | # plot over time
73 | if isinstance(ydim, int):
74 | ydim = np.array([1.0 if dim == ydim else 0.0 for dim in range(lpi.dims)], dtype=float)
75 |
76 | lpi.set_minimize_direction(ydim)
77 | res = lpi.minimize(columns=[lpi.cur_vars_offset + n for n in range(lpi.dims)])
78 | ymin = np.dot(ydim, res)
79 |
80 | lpi.set_minimize_direction(-1 * ydim)
81 | res = lpi.minimize(columns=[lpi.cur_vars_offset + n for n in range(lpi.dims)])
82 | ymax = np.dot(ydim, res)
83 |
84 | verts = [[cur_time[0], ymin]]
85 |
86 | if abs(ymin - ymax) > tol:
87 | verts.append([cur_time[0], ymax])
88 |
89 | if abs(cur_time[0] - cur_time[1]) > tol:
90 | verts.append([cur_time[1], ymax])
91 |
92 | if abs(cur_time[0] - cur_time[1]) > tol:
93 | verts.append([cur_time[1], ymin])
94 |
95 | elif ydim is None:
96 | # plot over time
97 | if isinstance(xdim, int):
98 | xdim = np.array([1.0 if dim == xdim else 0.0 for dim in range(lpi.dims)], dtype=float)
99 |
100 | lpi.set_minimize_direction(xdim)
101 | res = lpi.minimize(columns=[lpi.cur_vars_offset + n for n in range(lpi.dims)])
102 | xmin = np.dot(xdim, res)
103 |
104 | lpi.set_minimize_direction(-1 * xdim)
105 | res = lpi.minimize(columns=[lpi.cur_vars_offset + n for n in range(lpi.dims)])
106 | xmax = np.dot(xdim, res)
107 |
108 | verts = [[xmin, cur_time[0]]]
109 |
110 | if abs(xmin - xmax) > tol:
111 | verts.append([xmax, cur_time[0]])
112 |
113 | if abs(cur_time[0] - cur_time[1]) > tol:
114 | verts.append([xmax, cur_time[1]])
115 |
116 | if abs(cur_time[0] - cur_time[1]) > tol:
117 | verts.append([xmin, cur_time[1]])
118 | else:
119 | # 2-d plot
120 | bboxw = bbox_widths(lpi, xdim, ydim)
121 |
122 | # use bbox to determine acceptable accuracy... might be better to somehow do this
123 | epsilon = min(bboxw) / 1000.0
124 | dim_list = [xdim, ydim]
125 |
126 | def supp_point_nd(vec):
127 | 'return a supporting point for the given direction (maximize)'
128 |
129 | assert len(vec) == len(dim_list)
130 |
131 | d = np.zeros((lpi.dims,), dtype=float)
132 | # negative here because we want to MAXIMIZE not minimize
133 |
134 | for i, dim_index in enumerate(dim_list):
135 | d[dim_index] = -vec[i]
136 |
137 | lpi.set_minimize_direction(d)
138 |
139 | res = lpi.minimize(columns=[lpi.cur_vars_offset + n for n in range(lpi.dims)])
140 |
141 | rv = []
142 |
143 | for dim in dim_list:
144 | rv.append(res[dim])
145 |
146 | rv = np.array(rv, dtype=float)
147 |
148 | return rv
149 |
150 | Timers.tic('kamenev.get_verts')
151 | verts = kamenev.get_verts(len(dim_list), supp_point_nd, epsilon=epsilon)
152 | Timers.toc('kamenev.get_verts')
153 |
154 | if len(verts) > 2:
155 | # make 2-d convex hull to fix the order
156 | hull = ConvexHull(verts)
157 |
158 | verts = [[verts[i, 0], verts[i, 1]] for i in hull.vertices]
159 |
160 | # old method
161 | #pts = find_boundary_pts(lpi, xdim, ydim, plot_vecs, bboxw)
162 | #verts = [[pt[0], pt[1]] for pt in pts]
163 |
164 | # wrap polygon back to first point
165 | verts.append(verts[0])
166 |
167 | return verts
168 |
169 | def bbox_widths(lpi, xdim, ydim):
170 | 'find and return the bounding box widths of the lp for the passed-in dimensions'
171 |
172 | rv = []
173 | dims = lpi.dims
174 |
175 | for dim in [xdim, ydim]:
176 | col = lpi.cur_vars_offset + dim
177 | min_dir = [1 if i == dim else 0 for i in range(dims)]
178 | max_dir = [-1 if i == dim else 0 for i in range(dims)]
179 |
180 | min_val = lpi.minimize(direction_vec=min_dir, columns=[col])[0]
181 | max_val = lpi.minimize(direction_vec=max_dir, columns=[col])[0]
182 |
183 | dx = max_val - min_val
184 |
185 | if dx < 1e-5:
186 | dx = 1e-5
187 |
188 | rv.append(dx)
189 |
190 | return rv
191 |
192 | def make_plot_vecs(num_angles=256, offset=0.0):
193 | 'make plot_vecs with equally spaced directions, returning the result'
194 |
195 | plot_vecs = []
196 |
197 | for theta in np.linspace(0.0, 2.0*math.pi, num_angles, endpoint=False):
198 | x = math.cos(theta + offset)
199 | y = math.sin(theta + offset)
200 |
201 | vec = np.array([x, y], dtype=float)
202 |
203 | plot_vecs.append(vec)
204 |
205 | return plot_vecs
206 |
207 | def find_boundary_pts(lpi, xdim, ydim, plot_vecs, bboxw):
208 | '''
209 | find points along an LPs boundary by solving several LPs and
210 | returns a list of points on the boundary which maximize each
211 | of the passed-in directions
212 | '''
213 |
214 | rv = []
215 |
216 | assert len(plot_vecs) >= 2
217 |
218 | # optimized approach: do binary search to find changes
219 | point = _minimize(lpi, xdim, ydim, plot_vecs[0], bboxw)
220 | rv.append(point.copy())
221 |
222 | # add it in thirds, to ensure we don't miss anything
223 | third = len(plot_vecs) // 3
224 |
225 | # 0 to 1/3
226 | point = _minimize(lpi, xdim, ydim, plot_vecs[third], bboxw)
227 |
228 | if not np.array_equal(point, rv[-1]):
229 | rv += _binary_search_boundaries(lpi, 0, third, rv[-1], point, xdim, ydim, plot_vecs, bboxw)
230 | rv.append(point.copy())
231 |
232 | # 1/3 to 2/3
233 | point = _minimize(lpi, xdim, ydim, plot_vecs[2*third], bboxw)
234 |
235 | if not np.array_equal(point, rv[-1]):
236 | rv += _binary_search_boundaries(lpi, third, 2*third, rv[-1], point, xdim, ydim, plot_vecs, bboxw)
237 | rv.append(point.copy())
238 |
239 | # 2/3 to end
240 | point = _minimize(lpi, xdim, ydim, plot_vecs[-1], bboxw)
241 |
242 | if not np.array_equal(point, rv[-1]):
243 | rv += _binary_search_boundaries(lpi, 2*third, len(plot_vecs) - 1, rv[-1], point, xdim, ydim, plot_vecs, bboxw)
244 | rv.append(point.copy())
245 |
246 | # pop last point if it's the same as the first point
247 | if len(rv) > 1 and np.array_equal(rv[0], rv[-1]):
248 | rv.pop()
249 |
250 | return rv
251 |
252 | def _minimize(lpi, xdim, ydim, direction, bounding_box_widths):
253 | 'minimize to lp... returning the 2-d point of the minimum'
254 |
255 | dims = 0
256 |
257 | assert not (xdim is None and ydim is None)
258 |
259 | if xdim is not None:
260 | dims = xdim + 1 if isinstance(xdim, int) else len(xdim)
261 |
262 | if ydim is not None:
263 | dims = max(dims, ydim + 1) if isinstance(ydim, int) else max(dims, len(ydim))
264 |
265 | if isinstance(xdim, int):
266 | xdim = np.array([1.0 if dim == xdim else 0.0 for dim in range(dims)], dtype=float)
267 |
268 | if isinstance(ydim, int):
269 | ydim = np.array([1.0 if dim == ydim else 0.0 for dim in range(dims)], dtype=float)
270 |
271 | # xdim and ydim are both arrays or None
272 | optimize_direction = np.zeros(dims, dtype=float)
273 |
274 | if xdim is not None:
275 | optimize_direction += direction[0] * xdim / bounding_box_widths[0]
276 |
277 | if ydim is not None:
278 | optimize_direction += direction[1] * ydim / bounding_box_widths[1]
279 |
280 | lpi.set_minimize_direction(optimize_direction)
281 |
282 | res = lpi.minimize(columns=[lpi.cur_vars_offset + n for n in range(dims)])
283 |
284 | xcoord = 0 if xdim is None else np.dot(res, xdim)
285 | ycoord = 0 if ydim is None else np.dot(res, ydim)
286 |
287 | return np.array([xcoord, ycoord], dtype=float)
288 |
289 | def _binary_search_boundaries(lpi, start, end, start_point, end_point, xdim, ydim, plot_vecs, bboxw):
290 | '''
291 | return all the optimized points in the star for the passed-in directions, between
292 | the start and end indices, exclusive
293 |
294 | points which match start_point or end_point are not returned
295 | '''
296 |
297 | rv = []
298 |
299 | if start + 1 < end:
300 | mid = (start + end) // 2
301 |
302 | mid_point = _minimize(lpi, xdim, ydim, plot_vecs[mid], bboxw)
303 |
304 | not_start = not np.allclose(start_point, mid_point, atol=1e-6)
305 | not_end = not np.allclose(end_point, mid_point, atol=1e-6)
306 |
307 | if not_start:
308 | rv += _binary_search_boundaries(lpi, start, mid, start_point, mid_point, xdim, ydim, plot_vecs, bboxw)
309 |
310 | if not_start and not_end:
311 | rv.append(mid_point)
312 |
313 | if not_end:
314 | rv += _binary_search_boundaries(lpi, mid, end, mid_point, end_point, xdim, ydim, plot_vecs, bboxw)
315 |
316 | return rv
317 |
--------------------------------------------------------------------------------
/examples/gearbox/gearbox.py:
--------------------------------------------------------------------------------
1 | '''
2 | Gearbox benchmark from:
3 | "Motor-Transmission Drive System: a Benchmark Example for Safety Verification"
4 | by Hongxu Chen, Sayan Mitra and Guangyu Tian in ARCH 2015
5 |
6 | This model was also used in ARCHCOMP-17 and 18.
7 |
8 | This model shows how to use hylaa.symbolic to construct the dynamics and reset / guard conditions from
9 | string expressions.
10 |
11 | Unique features:
12 | - Performs both reachability analysis and simulation on the model
13 | - Uses deaggregation to find worst-case meshing time and cumulative impulse force
14 | - Concurrently creates two plots at once (x/y and impuse force/time)
15 |
16 | Stanley Bak, Nov 2018
17 | '''
18 |
19 | import math
20 |
21 | import numpy as np
22 |
23 | from matplotlib import collections
24 |
25 | from hylaa.hybrid_automaton import HybridAutomaton
26 | from hylaa.settings import HylaaSettings, PlotSettings, LabelSettings
27 | from hylaa.core import Core
28 | from hylaa.stateset import StateSet
29 | from hylaa import lputil, aggstrat, symbolic
30 | from hylaa.aggstrat import Aggregated
31 |
32 | def make_automaton(theta_deg, maxi=20):
33 | 'make the hybrid automaton'
34 |
35 | ha = HybridAutomaton('Gearbox')
36 |
37 | # variables
38 | variables = ["px", "py", "vx", "vy", "I"]
39 | derivatives = ["vx", "vy", "Fs/ms", "-Rs*Tf/Jg2", "0"]
40 |
41 | constant_dict = {
42 | "zeta": 0.9,
43 | "ms": 3.4,
44 | "mg2": 18.1,
45 | "Jg2": 0.7,
46 | "Rs": 0.08,
47 | "theta": theta_deg * math.pi / 180, #0.628318530717959,
48 | "deltap": -0.003,
49 | "Fs": 70,
50 | "Tf": 1
51 | }
52 |
53 | ############## Modes ##############
54 | move_free = ha.new_mode('move_free')
55 | #
56 | a_mat = symbolic.make_dynamics_mat(variables, derivatives, constant_dict, has_affine_variable=True)
57 | move_free.set_dynamics(a_mat)
58 |
59 | invariant = f"px<=deltap & py<=-px*0.726542528005361 & py>=px*0.726542528005361 & I <= {maxi}"
60 | mat, rhs = symbolic.make_condition(variables, invariant.split('&'), constant_dict, has_affine_variable=True)
61 | move_free.set_invariant(mat, rhs)
62 |
63 | meshed = ha.new_mode('meshed')
64 | a_mat = np.zeros((6, 6))
65 | meshed.set_dynamics(a_mat)
66 |
67 | # error mode
68 | error = ha.new_mode('error')
69 |
70 | ############## Cyclic Transitions ##############
71 | # t1
72 | t1 = ha.new_transition(move_free, move_free, "t1")
73 | t1_guard = "py>=-px*0.726542528005361 & vx*0.587785252292473+vy*0.809016994374947>=0"
74 | mat, rhs = symbolic.make_condition(variables, t1_guard.split('&'), constant_dict, has_affine_variable=True)
75 | t1.set_guard(mat, rhs)
76 |
77 | # projection of a point onto a plane
78 | # if is the point and is the normal to the plane with mag 1
79 | # proj = - ( dot ) *
80 | nx = math.cos(math.pi / 2 - constant_dict['theta'])
81 | ny = math.sin(math.pi / 2 - constant_dict['theta'])
82 |
83 | i_reset = "I+(vx*0.587785252292473+vy*0.809016994374947)*(zeta+1)*ms*mg2/(ms*(0.809016994374947*0.809016994374947)+mg2*(0.587785252292473*0.587785252292473))"
84 | resets = [f"px - (px*{nx} + py*{ny}) * {nx}",
85 | f"py - (px*{nx} + py*{ny}) * {ny}",
86 | "(vx*(ms*0.809016994374947*0.809016994374947-mg2*zeta*0.587785252292473*0.587785252292473)+vy*(-(zeta+1)*mg2*0.587785252292473*0.809016994374947))/(ms*(0.809016994374947*0.809016994374947)+mg2*(0.587785252292473*0.587785252292473))",
87 | "(vx*(-(zeta+1)*ms*0.587785252292473*0.809016994374947)+vy*(mg2*0.587785252292473*0.587785252292473-ms*zeta*0.809016994374947*0.809016994374947))/(ms*(0.809016994374947*0.809016994374947)+mg2*(0.587785252292473*0.587785252292473))",
88 | i_reset]
89 | reset_mat = symbolic.make_reset_mat(variables, resets, constant_dict, has_affine_variable=True)
90 | t1.set_reset(reset_mat)
91 |
92 | # T1 to error mode
93 | t1_to_error = ha.new_transition(move_free, error, "t1_to_error")
94 | guard = f"{t1_guard} & {i_reset}>={maxi}"
95 | mat, rhs = symbolic.make_condition(variables, guard.split('&'), constant_dict, has_affine_variable=True)
96 | t1_to_error.set_guard(mat, rhs)
97 | t1_to_error.set_reset(reset_mat)
98 |
99 | # t2
100 | t2 = ha.new_transition(move_free, move_free, "t2")
101 | t2_guard = "py<=px*0.726542528005361 & vx*0.587785252292473-vy*0.809016994374947>=0"
102 | mat, rhs = symbolic.make_condition(variables, t2_guard.split('&'), constant_dict, has_affine_variable=True)
103 | t2.set_guard(mat, rhs)
104 |
105 | ny = -ny
106 |
107 | i_reset = "I+(vx*0.587785252292473-vy*0.809016994374947)*(zeta+1)*ms*mg2/(ms*(0.809016994374947*0.809016994374947)+mg2*(0.587785252292473*0.587785252292473))"
108 | resets = [f"px - (px*{nx} + py*{ny}) * {nx}",
109 | f"py - (px*{nx} + py*{ny}) * {ny}",
110 | "(vx*(ms*0.809016994374947*0.809016994374947-mg2*zeta*0.587785252292473*0.587785252292473)+vy*((zeta+1)*mg2*0.587785252292473*0.809016994374947))/(ms*(0.809016994374947*0.809016994374947)+mg2*(0.587785252292473*0.587785252292473))",
111 | "(vx*((zeta+1)*ms*0.587785252292473*0.809016994374947)+vy*(mg2*0.587785252292473*0.587785252292473-ms*zeta*0.809016994374947*0.809016994374947))/(ms*(0.809016994374947*0.809016994374947)+mg2*(0.587785252292473*0.587785252292473))",
112 | i_reset]
113 | reset_mat = symbolic.make_reset_mat(variables, resets, constant_dict, has_affine_variable=True)
114 | t2.set_reset(reset_mat)
115 |
116 | # T2 to error mode
117 | t2_to_error = ha.new_transition(move_free, error, "t2_to_error")
118 | guard = f"{t2_guard} & {i_reset}>={maxi}"
119 | mat, rhs = symbolic.make_condition(variables, guard.split('&'), constant_dict, has_affine_variable=True)
120 | t2_to_error.set_guard(mat, rhs)
121 | t2_to_error.set_reset(reset_mat)
122 |
123 | #### transitions to meshed
124 |
125 | guards = ["px >= deltap & vx >= 0 & vy >= 0",
126 | "px >= deltap & vx >= 0 & vy <= 0",
127 | "px >= deltap & vx <= 0 & vy >= 0",
128 | "px >= deltap & vx <= 0 & vy <= 0"]
129 |
130 | i_resets = ["I+ms*vx+ms*vy", "I+ms*vx-ms*vy", "I-ms*vx+ms*vy", "I-ms*vx-ms*vy"]
131 |
132 | for i, (guard, i_reset) in enumerate(zip(guards, i_resets)):
133 | t3 = ha.new_transition(move_free, meshed, f"meshed_{i}")
134 |
135 | t3_guard = guard + f" & I <= {maxi}"
136 | mat, rhs = symbolic.make_condition(variables, t3_guard.split('&'), constant_dict, has_affine_variable=True)
137 | t3.set_guard(mat, rhs)
138 |
139 | resets = [f"px",
140 | f"py",
141 | "0",
142 | "0",
143 | i_reset]
144 | reset_mat = symbolic.make_reset_mat(variables, resets, constant_dict, has_affine_variable=True)
145 | t3.set_reset(reset_mat)
146 |
147 | # T3 to error mode
148 | t3_to_error = ha.new_transition(move_free, error, f"t3_to_error_{i}")
149 | t3_err_guard = guard + f" & I >= {maxi}"
150 | mat, rhs = symbolic.make_condition(variables, t3_err_guard.split('&'), constant_dict, has_affine_variable=True)
151 | t3_to_error.set_guard(mat, rhs)
152 |
153 | return ha
154 |
155 | def make_init(ha, box):
156 | 'make the initial states'
157 |
158 | mode = ha.modes['move_free']
159 | # px==-0.0165 & py==0.003 & vx==0 & vy==0 & I==0 & affine==1.0
160 | init_lpi = lputil.from_box(box, mode)
161 |
162 |
163 | #init_lpi = lputil.from_box([(-0.02, -0.02), (-0.005, -0.003), (0, 0), (0, 0), (0, 0), (1.0, 1.0)], mode)
164 | #start = [-0.02, -0.004213714568273684, 0.0, 0.0, 0.0, 1.0]
165 | #tol = 1e-7
166 | #init_lpi = lputil.from_box([(x - tol, x + tol) if i < 2 else (x, x) for i, x in enumerate(start)], mode)
167 |
168 | init_list = [StateSet(init_lpi, mode)]
169 |
170 | # why does 0.003-0.005 reach an error with i=30 for roots first but not leaves first?
171 |
172 | return init_list
173 |
174 | def make_settings(theta_deg, box):
175 | 'make the reachability settings object'
176 |
177 | # see hylaa.settings for a list of reachability settings
178 | settings = HylaaSettings(0.001, 0.35) # step: 0.001, bound: 0.1
179 |
180 | #settings.stop_on_aggregated_error = False
181 |
182 | #settings.aggstrat = MyAggergated()
183 | settings.aggstrat.deaggregate = True # use deaggregation
184 | settings.aggstrat.deagg_preference = Aggregated.DEAGG_LEAVES_FIRST
185 |
186 | settings.stdout = HylaaSettings.STDOUT_VERBOSE
187 |
188 | settings.plot.plot_mode = PlotSettings.PLOT_IMAGE
189 | settings.plot.filename = "gearbox.png"
190 | settings.plot.plot_size = (8, 9)
191 |
192 | settings.plot.xdim_dir = 0
193 | settings.plot.ydim_dir = 1
194 |
195 | settings.plot.xdim_dir = [0, None]
196 | settings.plot.ydim_dir = [1, 4]
197 | settings.plot.label = [LabelSettings(), LabelSettings()]
198 |
199 | settings.plot.label[0].title = "Motor-Transmission Drive System"
200 | settings.plot.label[0].title_size = 22
201 |
202 | #settings.plot.label.axes_limits = [-0.02, 0, -0.008, 0.004]
203 | settings.plot.label[0].axes_limits = [-0.02, 0, -0.01, 0.01]
204 | settings.plot.label[0].x_label = "X Position [m]"
205 | settings.plot.label[0].y_label = "Y Position [m]"
206 | settings.plot.label[0].label_size = 20
207 |
208 | settings.plot.label[1].axes_limits = [0.0, 0.35, 0.0, 40.0]
209 | settings.plot.label[1].x_label = "Time [s]"
210 | settings.plot.label[1].y_label = "Impact Impulse [N m]"
211 | settings.plot.label[1].label_size = 20
212 |
213 | lines = []
214 |
215 | # add coords
216 | theta = theta_deg * math.pi / 180
217 | deltap = 0.003
218 | b = 0.01
219 |
220 | y = deltap * math.tan(theta)
221 | x = b / (2 * math.tan(theta))
222 |
223 | p1 = (-x - deltap, y + b / 2)
224 | p2 = (-deltap, y)
225 | p3 = (-deltap, -y)
226 | p4 = (-x - deltap, -y - b / 2)
227 |
228 | lines.append([p1, p2, p3, p4])
229 |
230 | line_col = collections.LineCollection(lines, animated=True, colors=('gray'), linewidths=(2), linestyle='dashed')
231 |
232 | verts = [[box[0][0], box[1][0]], [box[0][1], box[1][0]], [box[0][1], box[1][1]], [box[0][0], box[1][1]]]
233 |
234 | poly_col = collections.PolyCollection([verts], animated=True, edgecolor=None, facecolor=(0., 1.0, 0., 0.5))
235 |
236 | settings.plot.extra_collections = [[line_col, poly_col], []]
237 |
238 | return settings
239 |
240 | def run_hylaa(is_sim=False):
241 | 'main entry point'
242 |
243 | theta_deg = 36
244 | maxi = 60
245 |
246 | ha = make_automaton(theta_deg, maxi)
247 |
248 | box = [(-0.017, -0.016), (-0.005, 0.005), (0, 0), (0, 0), (0, 0), (1.0, 1.0)]
249 |
250 | settings = make_settings(theta_deg, box)
251 |
252 | if not is_sim:
253 | result = Core(ha, settings).run(make_init(ha, box))
254 |
255 | if result.counterexample:
256 | print(f"counterexample start: {result.counterexample[0].start}")
257 | else:
258 | init_mode = ha.modes['move_free']
259 | settings.aggstrat.sim_avoid_modes.append('meshed')
260 | settings.plot.sim_line_width = 1.0
261 | settings.plot.filename = "gearbox_sim.png"
262 | Core(ha, settings, seed=2).simulate(init_mode, box, 100)
263 |
264 | if __name__ == "__main__":
265 | #run(is_sim=False)
266 | run_hylaa(is_sim=True)
267 |
--------------------------------------------------------------------------------
/examples/fuzzy_pd/fuzzy_pd.py:
--------------------------------------------------------------------------------
1 | '''
2 | Fuzzy PD controller example, from Aaron Fifarek's MS Thesis
3 |
4 | D gain is only nonzero near the setpoint
5 |
6 | Model was originally converted with Hyst
7 | '''
8 |
9 | from matplotlib import animation
10 |
11 | from hylaa.hybrid_automaton import HybridAutomaton
12 | from hylaa.settings import HylaaSettings, PlotSettings
13 | from hylaa.core import Core
14 | from hylaa.stateset import StateSet
15 | from hylaa import lputil
16 |
17 | def define_ha():
18 | '''make the hybrid automaton and return it'''
19 |
20 | ha = HybridAutomaton()
21 |
22 | # dynamics variable order: [x1, x2, t, affine]
23 |
24 | loc2 = ha.new_mode('loc2')
25 | a_matrix = [ \
26 | [0, 1, 0, 0], \
27 | [18300, -20, 0, -9562.5], \
28 | [0, 0, 0, 1], \
29 | [0, 0, 0, 0], \
30 | ]
31 | loc2.set_dynamics(a_matrix)
32 | # -x1 > -0.51 & -x1 <= -0.5
33 | loc2.set_invariant([[1, -0, -0, -0], [-1, 0, 0, 0], ], [0.51, -0.5, ])
34 |
35 | loc3 = ha.new_mode('loc3')
36 | a_matrix = [ \
37 | [0, 1, 0, 0], \
38 | [-825, -20, 0, 0], \
39 | [0, 0, 0, 1], \
40 | [0, 0, 0, 0], \
41 | ]
42 | loc3.set_dynamics(a_matrix)
43 | # -x1 > -0.5 & -x1 <= -0.09
44 | loc3.set_invariant([[1, -0, -0, -0], [-1, 0, 0, 0], ], [0.5, -0.09, ])
45 |
46 | loc4 = ha.new_mode('loc4')
47 | a_matrix = [ \
48 | [0, 1, 0, 0], \
49 | [30675, -695, 0, -2835], \
50 | [0, 0, 0, 1], \
51 | [0, 0, 0, 0], \
52 | ]
53 | loc4.set_dynamics(a_matrix)
54 | # -x1 > -0.09 & -x1 <= -0.08
55 | loc4.set_invariant([[1, -0, -0, -0], [-1, 0, 0, 0], ], [0.09, -0.08, ])
56 |
57 | loc1 = ha.new_mode('loc1')
58 | a_matrix = [ \
59 | [0, 1, 0, 0], \
60 | [-450, -20, 0, 0], \
61 | [0, 0, 0, 1], \
62 | [0, 0, 0, 0], \
63 | ]
64 | loc1.set_dynamics(a_matrix)
65 | # -x1 <= -0.51
66 | loc1.set_invariant([[-1, 0, 0, 0], ], [-0.51, ])
67 |
68 | loc5 = ha.new_mode('loc5')
69 | a_matrix = [ \
70 | [0, 1, 0, 0], \
71 | [-4762.5, -95, 0, 0], \
72 | [0, 0, 0, 1], \
73 | [0, 0, 0, 0], \
74 | ]
75 | loc5.set_dynamics(a_matrix)
76 | # -x1 > -0.08 & -x1 < 0.08
77 | loc5.set_invariant([[1, -0, -0, -0], [-1, 0, 0, 0], ], [0.08, 0.08, ])
78 |
79 | loc6 = ha.new_mode('loc6')
80 | a_matrix = [ \
81 | [0, 1, 0, 0], \
82 | [30675, -695, 0, 2835], \
83 | [0, 0, 0, 1], \
84 | [0, 0, 0, 0], \
85 | ]
86 | loc6.set_dynamics(a_matrix)
87 | # -x1 >= 0.08 & -x1 < 0.09
88 | loc6.set_invariant([[1, -0, -0, -0], [-1, 0, 0, 0], ], [-0.08, 0.09, ])
89 |
90 | loc7 = ha.new_mode('loc7')
91 | a_matrix = [ \
92 | [0, 1, 0, 0], \
93 | [-825, -20, 0, 0], \
94 | [0, 0, 0, 1], \
95 | [0, 0, 0, 0], \
96 | ]
97 | loc7.set_dynamics(a_matrix)
98 | # -x1 >= 0.09 & -x1 < 0.5
99 | loc7.set_invariant([[1, -0, -0, -0], [-1, 0, 0, 0], ], [-0.09, 0.5, ])
100 |
101 | loc8 = ha.new_mode('loc8')
102 | a_matrix = [ \
103 | [0, 1, 0, 0], \
104 | [18300, -20, 0, 9562.5], \
105 | [0, 0, 0, 1], \
106 | [0, 0, 0, 0], \
107 | ]
108 | loc8.set_dynamics(a_matrix)
109 | # -x1 >= 0.5 & -x1 < 0.51
110 | loc8.set_invariant([[1, -0, -0, -0], [-1, 0, 0, 0], ], [-0.5, 0.51, ])
111 |
112 | loc9 = ha.new_mode('loc9')
113 | a_matrix = [ \
114 | [0, 1, 0, 0], \
115 | [-450, -20, 0, 0], \
116 | [0, 0, 0, 1], \
117 | [0, 0, 0, 0], \
118 | ]
119 | loc9.set_dynamics(a_matrix)
120 | # -x1 >= 0.51
121 | loc9.set_invariant([[1, -0, -0, -0], ], [-0.51, ])
122 |
123 | trans = ha.new_transition(loc2, loc1)
124 | # -x1 <= -0.51
125 | trans.set_guard([[-1, 0, 0, 0], ], [-0.51, ])
126 |
127 | trans = ha.new_transition(loc2, loc3)
128 | # -x1 > -0.5
129 | trans.set_guard([[1, -0, -0, -0], ], [0.5, ])
130 |
131 | trans = ha.new_transition(loc3, loc4)
132 | # -x1 > -0.09
133 | trans.set_guard([[1, -0, -0, -0], ], [0.09, ])
134 |
135 | trans = ha.new_transition(loc3, loc2)
136 | # -x1 <= -0.5
137 | trans.set_guard([[-1, 0, 0, 0], ], [-0.5, ])
138 |
139 | trans = ha.new_transition(loc4, loc5)
140 | # -x1 > -0.08
141 | trans.set_guard([[1, -0, -0, -0], ], [0.08, ])
142 |
143 | trans = ha.new_transition(loc4, loc3)
144 | # -x1 <= -0.09
145 | trans.set_guard([[-1, 0, 0, 0], ], [-0.09, ])
146 |
147 | trans = ha.new_transition(loc1, loc2)
148 | # -x1 > -0.51
149 | trans.set_guard([[1, -0, -0, -0], ], [0.51, ])
150 |
151 | trans = ha.new_transition(loc5, loc6)
152 | # -x1 >= 0.0801
153 | trans.set_guard([[1, -0, -0, -0], ], [-0.0801, ])
154 |
155 | trans = ha.new_transition(loc5, loc4)
156 | # -x1 <= -0.0801
157 | trans.set_guard([[-1, 0, 0, 0], ], [-0.0801, ])
158 |
159 | trans = ha.new_transition(loc6, loc7)
160 | # -x1 >= 0.09
161 | trans.set_guard([[1, -0, -0, -0], ], [-0.09, ])
162 |
163 | trans = ha.new_transition(loc6, loc5)
164 | # -x1 < 0.08
165 | trans.set_guard([[-1, 0, 0, 0], ], [0.08, ])
166 |
167 | trans = ha.new_transition(loc7, loc8)
168 | # -x1 >= 0.5
169 | trans.set_guard([[1, -0, -0, -0], ], [-0.5, ])
170 |
171 | trans = ha.new_transition(loc7, loc6)
172 | # -x1 < 0.09
173 | trans.set_guard([[-1, 0, 0, 0], ], [0.09, ])
174 |
175 | trans = ha.new_transition(loc8, loc9)
176 | # -x1 >= 0.51
177 | trans.set_guard([[1, -0, -0, -0], ], [-0.51, ])
178 |
179 | trans = ha.new_transition(loc8, loc7)
180 | # -x1 < 0.5
181 | trans.set_guard([[-1, 0, 0, 0], ], [0.5, ])
182 |
183 | trans = ha.new_transition(loc9, loc8)
184 | # -x1 < 0.51
185 | trans.set_guard([[-1, 0, 0, 0], ], [0.51, ])
186 |
187 | return ha
188 |
189 | def define_init_states(ha):
190 | '''returns a list of StateSet objects'''
191 | # Variable ordering: [x1, x2, t, affine]
192 | rv = []
193 |
194 | should_init = lambda name: True
195 |
196 | # -1.0 <= x1 & x1 <= 1.0 & x2 = 0.0 & t = 0.0 & affine = 1.0
197 | mode = ha.modes['loc2']
198 | mat = [[-1, 0, 0, 0], \
199 | [1, 0, 0, 0], \
200 | [0, 1, 0, 0], \
201 | [-0, -1, -0, -0], \
202 | [0, 0, 1, 0], \
203 | [-0, -0, -1, -0], \
204 | [0, 0, 0, 1], \
205 | [-0, -0, -0, -1], ]
206 | rhs = [1, 1, 0, -0, 0, -0, 1, -1, ]
207 |
208 | if should_init(mode.name):
209 | rv.append(StateSet(lputil.from_constraints(mat, rhs, mode), mode))
210 |
211 | # -1.0 <= x1 & x1 <= 1.0 & x2 = 0.0 & t = 0.0 & affine = 1.0
212 | mode = ha.modes['loc3']
213 | mat = [[-1, 0, 0, 0], \
214 | [1, 0, 0, 0], \
215 | [0, 1, 0, 0], \
216 | [-0, -1, -0, -0], \
217 | [0, 0, 1, 0], \
218 | [-0, -0, -1, -0], \
219 | [0, 0, 0, 1], \
220 | [-0, -0, -0, -1], ]
221 | rhs = [1, 1, 0, -0, 0, -0, 1, -1, ]
222 |
223 | if should_init(mode.name):
224 | rv.append(StateSet(lputil.from_constraints(mat, rhs, mode), mode))
225 |
226 | # -1.0 <= x1 & x1 <= 1.0 & x2 = 0.0 & t = 0.0 & affine = 1.0
227 | mode = ha.modes['loc4']
228 | mat = [[-1, 0, 0, 0], \
229 | [1, 0, 0, 0], \
230 | [0, 1, 0, 0], \
231 | [-0, -1, -0, -0], \
232 | [0, 0, 1, 0], \
233 | [-0, -0, -1, -0], \
234 | [0, 0, 0, 1], \
235 | [-0, -0, -0, -1], ]
236 | rhs = [1, 1, 0, -0, 0, -0, 1, -1, ]
237 |
238 | if should_init(mode.name):
239 | rv.append(StateSet(lputil.from_constraints(mat, rhs, mode), mode))
240 |
241 | # -1.0 <= x1 & x1 <= 1.0 & x2 = 0.0 & t = 0.0 & affine = 1.0
242 | mode = ha.modes['loc1']
243 | mat = [[-1, 0, 0, 0], \
244 | [1, 0, 0, 0], \
245 | [0, 1, 0, 0], \
246 | [-0, -1, -0, -0], \
247 | [0, 0, 1, 0], \
248 | [-0, -0, -1, -0], \
249 | [0, 0, 0, 1], \
250 | [-0, -0, -0, -1], ]
251 | rhs = [1, 1, 0, -0, 0, -0, 1, -1, ]
252 |
253 | if should_init(mode.name):
254 | rv.append(StateSet(lputil.from_constraints(mat, rhs, mode), mode))
255 |
256 | # -1.0 <= x1 & x1 <= 1.0 & x2 = 0.0 & t = 0.0 & affine = 1.0
257 | mode = ha.modes['loc5']
258 | mat = [[-1, 0, 0, 0], \
259 | [1, 0, 0, 0], \
260 | [0, 1, 0, 0], \
261 | [-0, -1, -0, -0], \
262 | [0, 0, 1, 0], \
263 | [-0, -0, -1, -0], \
264 | [0, 0, 0, 1], \
265 | [-0, -0, -0, -1], ]
266 | rhs = [1, 1, 0, -0, 0, -0, 1, -1, ]
267 |
268 | if should_init(mode.name):
269 | rv.append(StateSet(lputil.from_constraints(mat, rhs, mode), mode))
270 |
271 | # -1.0 <= x1 & x1 <= 1.0 & x2 = 0.0 & t = 0.0 & affine = 1.0
272 | mode = ha.modes['loc6']
273 | mat = [[-1, 0, 0, 0], \
274 | [1, 0, 0, 0], \
275 | [0, 1, 0, 0], \
276 | [-0, -1, -0, -0], \
277 | [0, 0, 1, 0], \
278 | [-0, -0, -1, -0], \
279 | [0, 0, 0, 1], \
280 | [-0, -0, -0, -1], ]
281 | rhs = [1, 1, 0, -0, 0, -0, 1, -1, ]
282 |
283 | if should_init(mode.name):
284 | rv.append(StateSet(lputil.from_constraints(mat, rhs, mode), mode))
285 |
286 | # -1.0 <= x1 & x1 <= 1.0 & x2 = 0.0 & t = 0.0 & affine = 1.0
287 | mode = ha.modes['loc7']
288 | mat = [[-1, 0, 0, 0], \
289 | [1, 0, 0, 0], \
290 | [0, 1, 0, 0], \
291 | [-0, -1, -0, -0], \
292 | [0, 0, 1, 0], \
293 | [-0, -0, -1, -0], \
294 | [0, 0, 0, 1], \
295 | [-0, -0, -0, -1], ]
296 | rhs = [1, 1, 0, -0, 0, -0, 1, -1, ]
297 |
298 | if should_init(mode.name):
299 | rv.append(StateSet(lputil.from_constraints(mat, rhs, mode), mode))
300 |
301 | # -1.0 <= x1 & x1 <= 1.0 & x2 = 0.0 & t = 0.0 & affine = 1.0
302 | mode = ha.modes['loc8']
303 | mat = [[-1, 0, 0, 0], \
304 | [1, 0, 0, 0], \
305 | [0, 1, 0, 0], \
306 | [-0, -1, -0, -0], \
307 | [0, 0, 1, 0], \
308 | [-0, -0, -1, -0], \
309 | [0, 0, 0, 1], \
310 | [-0, -0, -0, -1], ]
311 | rhs = [1, 1, 0, -0, 0, -0, 1, -1, ]
312 |
313 | if should_init(mode.name):
314 | rv.append(StateSet(lputil.from_constraints(mat, rhs, mode), mode))
315 |
316 | # -1.0 <= x1 & x1 <= 1.0 & x2 = 0.0 & t = 0.0 & affine = 1.0
317 | mode = ha.modes['loc9']
318 | mat = [[-1, 0, 0, 0], \
319 | [1, 0, 0, 0], \
320 | [0, 1, 0, 0], \
321 | [-0, -1, -0, -0], \
322 | [0, 0, 1, 0], \
323 | [-0, -0, -1, -0], \
324 | [0, 0, 0, 1], \
325 | [-0, -0, -0, -1], ]
326 | rhs = [1, 1, 0, -0, 0, -0, 1, -1, ]
327 |
328 | if should_init(mode.name):
329 | rv.append(StateSet(lputil.from_constraints(mat, rhs, mode), mode))
330 |
331 | return rv
332 |
333 |
334 | def define_settings():
335 | '''get the hylaa settings object
336 | see hylaa/settings.py for a complete list of reachability settings'''
337 |
338 | # step_size = 0.001, max_time = 0.75
339 | settings = HylaaSettings(0.001, 0.15)
340 | settings.plot.plot_mode = PlotSettings.PLOT_IMAGE # try PLOT_VIDEO (takes 10 minutes)
341 | settings.plot.xdim_dir = 2
342 | settings.plot.ydim_dir = 0
343 | settings.plot.label.title = "Fuzzy PD Controller"
344 | settings.plot.label.axes_limits = (-0.01, 0.3, -1.1, 1.1)
345 | settings.stdout = HylaaSettings.STDOUT_VERBOSE
346 |
347 | #settings.aggregation.require_same_path=False
348 | #settings.aggregation.pop_strategy=AggregationSettings.POP_LARGEST_MAXTIME
349 | #self.aggstrat = aggstrat.Aggregated()
350 |
351 | # custom settings for video export
352 | def make_video_writer():
353 | 'returns the Writer to create a video for export'
354 |
355 | writer_class = animation.writers['ffmpeg']
356 | return writer_class(fps=50, metadata=dict(artist='Me'), bitrate=1800)
357 |
358 | settings.plot.make_video_writer_func = make_video_writer
359 |
360 | return settings
361 |
362 | def run_hylaa():
363 | 'runs hylaa, returning a HylaaResult object'
364 | ha = define_ha()
365 | init = define_init_states(ha)
366 | settings = define_settings()
367 |
368 | core = Core(ha, settings)
369 | result = core.run(init)
370 |
371 | #core.aggdag.show()
372 |
373 | return result
374 |
375 | if __name__ == '__main__':
376 | run_hylaa()
377 |
--------------------------------------------------------------------------------