├── .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 | [![Build Status](https://travis-ci.org/stanleybak/hylaa.svg?branch=master)](https://travis-ci.org/stanleybak/hylaa) 2 | [![PyPI version](https://badge.fury.io/py/hylaa.svg)](https://badge.fury.io/py/hylaa) 3 | 4 | # Hylaa # 5 | 6 |

Hylaa Logo

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 | --------------------------------------------------------------------------------