├── .clang-format ├── .clang-tidy ├── .cmake-format.py ├── .editorconfig ├── .github └── workflows │ ├── linux.yml │ ├── macos.yml │ ├── sanitizer.yml │ ├── static-analysis.yml │ ├── unit-tests.yml │ └── windows.yml ├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── LICENSE-3rdParty ├── MANIFEST.in ├── README.md ├── cmake ├── Cache.cmake ├── Codecov.cmake ├── CompilerWarnings.cmake ├── FindSphinx.cmake ├── PreventInSourceBuilds.cmake ├── StandardProjectSettings.cmake ├── StaticAnalyzers.cmake ├── ThirdPartyDeps.cmake ├── Version.cmake ├── launch-c.in ├── launch-cxx.in ├── signaltlConfig.cmake.in └── test_std_filesystem.cc ├── codecov.yml ├── docs ├── CMakeLists.txt ├── conf.py ├── cpp_lib.rst ├── cpp_lib │ ├── core.rst │ ├── parsing.rst │ ├── robustness.rst │ └── signal_tl.rst └── index.rst ├── examples ├── CMakeLists.txt ├── basic_formulas.cc ├── basic_parsing.cc ├── basic_robustness.cc ├── basic_robustness.py ├── basic_signal.cc └── basic_signal.py ├── pyproject.toml ├── python_bindings ├── CMakeLists.txt ├── bindings.hpp ├── init.py.in ├── pyast.cc ├── pyrobustness.cc ├── pysignal.cc └── pysignal_tl.cc ├── requirements-dev.txt ├── scripts └── run_coverage.sh ├── setup.cfg ├── setup.py ├── signal_tl ├── __init__.py └── _version.py ├── src ├── CMakeLists.txt ├── core │ ├── ast.cc │ └── signal.cc ├── include │ └── signal_tl │ │ ├── ast.hpp │ │ ├── exception.hpp │ │ ├── fmt.hpp │ │ ├── internal │ │ ├── filesystem.hpp │ │ └── utils.hpp │ │ ├── parser.hpp │ │ ├── robustness.hpp │ │ ├── signal.hpp │ │ └── signal_tl.hpp ├── parser │ ├── actions.hpp │ ├── error_messages.hpp │ ├── grammar.hpp │ └── parser.cc └── robust_semantics │ ├── classic_robustness.cc │ ├── minmax.cc │ ├── minmax.hpp │ └── mono_wedge.h └── tests ├── CMakeLists.txt ├── formulas ├── formula0.stl-spec ├── formula1.stl-spec ├── formula2.stl-spec ├── formula3.stl-spec ├── formulafail1.stl-spec ├── formulafail2.stl-spec └── formulafail3.stl-spec ├── signaltl_tests.cc ├── test_append_error.cc ├── test_parser.cc └── test_signals.cc /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | AccessModifierOffset: '-1' 3 | AlignAfterOpenBracket: AlwaysBreak 4 | AlignConsecutiveMacros: 'true' 5 | AlignConsecutiveAssignments: 'true' 6 | AlignConsecutiveDeclarations: 'false' 7 | AlignEscapedNewlines: Left 8 | AlignOperands: 'true' 9 | AlignTrailingComments: 'true' 10 | AllowAllParametersOfDeclarationOnNextLine: 'false' 11 | AllowShortBlocksOnASingleLine: 'true' 12 | AllowShortCaseLabelsOnASingleLine: 'false' 13 | AllowShortFunctionsOnASingleLine: Empty 14 | AllowShortIfStatementsOnASingleLine: Never 15 | AllowShortLambdasOnASingleLine: Inline 16 | AllowShortLoopsOnASingleLine: 'true' 17 | AlwaysBreakAfterReturnType: None 18 | AlwaysBreakBeforeMultilineStrings: 'true' 19 | AlwaysBreakTemplateDeclarations: 'Yes' 20 | BinPackArguments: 'false' 21 | BinPackParameters: 'false' 22 | BreakBeforeBinaryOperators: None 23 | BreakBeforeBraces: Attach 24 | BreakBeforeTernaryOperators: 'true' 25 | BreakConstructorInitializers: AfterColon 26 | BreakStringLiterals: 'false' 27 | ColumnLimit: '88' 28 | CompactNamespaces: 'false' 29 | ConstructorInitializerAllOnOneLineOrOnePerLine: 'true' 30 | ConstructorInitializerIndentWidth: '4' 31 | ContinuationIndentWidth: '4' 32 | Cpp11BracedListStyle: 'true' 33 | DerivePointerAlignment: 'false' 34 | DisableFormat: 'false' 35 | IndentCaseLabels: 'true' 36 | IndentWidth: '2' 37 | IndentWrappedFunctionNames: 'false' 38 | KeepEmptyLinesAtTheStartOfBlocks: 'false' 39 | Language: Cpp 40 | MaxEmptyLinesToKeep: '1' 41 | NamespaceIndentation: None 42 | PointerAlignment: Left 43 | Standard: Cpp11 44 | TabWidth: '2' 45 | UseTab: Never 46 | 47 | ... 48 | -------------------------------------------------------------------------------- /.clang-tidy: -------------------------------------------------------------------------------- 1 | --- 2 | # NOTE there must be no spaces before the '-', so put the comma last. 3 | Checks: '-*, 4 | bugprone-*, 5 | cppcoreguidelines-*, 6 | -cppcoreguidelines-owning-memory, 7 | -cppcoreguidelines-pro-bounds-array-to-pointer-decay, 8 | -cppcoreguidelines-pro-bounds-constant-array-index, 9 | -cppcoreguidelines-pro-bounds-pointer-arithmetic, 10 | -cppcoreguidelines-pro-type-cstyle-cast, 11 | -cppcoreguidelines-pro-type-reinterpret-cast, 12 | -cppcoreguidelines-pro-type-static-cast-downcast, 13 | -cppcoreguidelines-pro-type-union-access, 14 | -cppcoreguidelines-pro-type-vararg, 15 | -cppcoreguidelines-special-member-functions, 16 | hicpp-exception-baseclass, 17 | hicpp-avoid-goto, 18 | modernize-*, 19 | -modernize-use-trailing-return-type, 20 | performance-*, 21 | -performance-noexcept-move-constructor, 22 | ' 23 | WarningsAsErrors: '*' 24 | HeaderFilterRegex: '' 25 | FormatStyle: 'file' 26 | ... 27 | -------------------------------------------------------------------------------- /.cmake-format.py: -------------------------------------------------------------------------------- 1 | with section("parse"): 2 | additional_commands = { 3 | "pybind11_add_module": { 4 | "pargs": "1+", 5 | "flags": [], 6 | "kwargs": { 7 | "MODULE": 1, 8 | "SHARED": 1, 9 | "EXCLUDE_FROM_ALL": 1, 10 | "NO_EXTRAS": 1, 11 | "SYSTEM": 1, 12 | "THIN_LTO": 1, 13 | "OPT_SIZE": 1, 14 | }, 15 | }, 16 | "setup_target_for_coverage_lcov": { 17 | "pargs": 0, 18 | "flags": ["NO_DEMANGLE"], 19 | "kwargs": { 20 | "NAME": 1, 21 | "BASE_DIRECTORY": 1, 22 | "EXCLUDE": "*", 23 | "EXECUTABLE": "*", 24 | "EXECUTABLE_ARGS": "*", 25 | "DEPENDENCIES": "*", 26 | "LCOV_ARGS": "*", 27 | "GENHTML_ARGS": "*", 28 | }, 29 | }, 30 | "setup_target_for_coverage_gcovr_xml": { 31 | "pargs": 0, 32 | "flags": [], 33 | "kwargs": { 34 | "NAME": 1, 35 | "BASE_DIRECTORY": 1, 36 | "EXCLUDE": "*", 37 | "EXECUTABLE": "*", 38 | "EXECUTABLE_ARGS": "*", 39 | "DEPENDENCIES": "*", 40 | }, 41 | }, 42 | "setup_target_for_coverage_gcovr_html": { 43 | "pargs": 0, 44 | "flags": [], 45 | "kwargs": { 46 | "NAME": 1, 47 | "BASE_DIRECTORY": 1, 48 | "EXCLUDE": "*", 49 | "EXECUTABLE": "*", 50 | "EXECUTABLE_ARGS": "*", 51 | "DEPENDENCIES": "*", 52 | }, 53 | }, 54 | "conan_cmake_run": { 55 | "pargs": "*", 56 | "flags": [ 57 | "BASIC_SETUP", 58 | "CMAKE_TARGETS", 59 | "UPDATE", 60 | "KEEP_RPATHS", 61 | "NO_LOAD", 62 | "NO_OUTPUT_DIRS", 63 | "OUTPUT_QUIET", 64 | "NO_IMPORTS", 65 | "SKIP_STD", 66 | ], 67 | "kwargs": { 68 | "CONANFILE": 1, 69 | "ARCH": 1, 70 | "BUILD": 1, 71 | "REQUIRES": "*", 72 | "OPTIONS": "*", 73 | }, 74 | }, 75 | } 76 | 77 | with section("lint"): 78 | disabled_codes = [ 79 | "C0113", 80 | ] 81 | 82 | with section("format"): 83 | dangle_parens = True 84 | line_ending = "unix" 85 | line_width = 80 86 | tab_size = 2 87 | keyword_case = "upper" 88 | 89 | with section("markup"): 90 | bullet_char = "-" 91 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | indent_size = 2 10 | indent_style = space 11 | 12 | [*.py] 13 | charset = utf-8 14 | indent_size = 4 15 | 16 | [Makefile] 17 | indent_style = tab 18 | 19 | -------------------------------------------------------------------------------- /.github/workflows/linux.yml: -------------------------------------------------------------------------------- 1 | name: Linux Compiler Testing 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | SCCACHE_DIR: ${{ github.workspace }}/.cache/sccache 7 | 8 | jobs: 9 | linux: 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | compiler: 14 | - g++-8 15 | - g++-9 16 | - g++-10 17 | - clang++-8 18 | - clang++-9 19 | - clang++-10 20 | build_type: [Debug, Release] 21 | 22 | runs-on: ubuntu-20.04 23 | 24 | env: 25 | CXX: ${{ matrix.compiler }} 26 | 27 | steps: 28 | - uses: actions/checkout@v2 29 | - uses: actions/setup-python@v2 30 | with: 31 | python-version: '3.x' 32 | - name: Cache dependencies 33 | uses: actions/cache@v2 34 | env: 35 | cache-name: cache-deps 36 | with: 37 | path: .cache/deps 38 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/ThirdPartyDeps.cmake') }} 39 | restore-keys: | 40 | ${{ runner.os }}-build-${{ env.cache-name }}- 41 | ${{ runner.os }}-build- 42 | ${{ runner.os }}- 43 | - name: Cache build 44 | uses: actions/cache@v2 45 | env: 46 | cache-name: cache-build 47 | with: 48 | path: .cache/sccache 49 | key: ${{ runner.os }}-build-${{ env.cache-name }} 50 | - name: Install sccache 51 | run: | 52 | curl -SL https://github.com/mozilla/sccache/releases/download/v0.2.15/sccache-v0.2.15-x86_64-unknown-linux-musl.tar.gz | tar xvz --strip-components=1 --wildcards "*/sccache" 53 | chmod +x sccache 54 | - run: cmake -E make_directory build 55 | 56 | - working-directory: build/ 57 | run: cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} 58 | 59 | - working-directory: build/ 60 | run: cmake --build . 61 | 62 | - working-directory: build/ 63 | run: ctest --output-on-failure 64 | 65 | -------------------------------------------------------------------------------- /.github/workflows/macos.yml: -------------------------------------------------------------------------------- 1 | name: macOS 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | SCCACHE_DIR: ${{ github.workspace }}/.cache/sccache 7 | 8 | jobs: 9 | xcode: 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | xcode: ['11', '12'] 14 | build_type: [Debug, Release] 15 | 16 | runs-on: macos-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - uses: actions/setup-python@v2 21 | with: 22 | python-version: '3.x' 23 | - name: Cache dependencies 24 | uses: actions/cache@v2 25 | env: 26 | cache-name: cache-deps 27 | with: 28 | path: .cache/deps 29 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/ThirdPartyDeps.cmake') }} 30 | restore-keys: | 31 | ${{ runner.os }}-build-${{ env.cache-name }}- 32 | ${{ runner.os }}-build- 33 | ${{ runner.os }}- 34 | - name: Cache build 35 | uses: actions/cache@v2 36 | env: 37 | cache-name: cache-build 38 | with: 39 | path: .cache/sccache 40 | key: ${{ runner.os }}-build-${{ env.cache-name }} 41 | - uses: maxim-lobanov/setup-xcode@v1 42 | with: 43 | xcode-version: ${{ matrix.xcode }} 44 | - name: Install sccache 45 | run: brew install sccache 46 | - name: CMake configure 47 | run: cmake -B build 48 | 49 | - working-directory: build/ 50 | run: cmake --build . --config ${{ matrix.build_type }} 51 | 52 | - working-directory: build/ 53 | run: ctest --config ${{ matrix.build_type }} --output-on-failure 54 | -------------------------------------------------------------------------------- /.github/workflows/sanitizer.yml: -------------------------------------------------------------------------------- 1 | name: Sanitizer 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | SCCACHE_DIR: ${{ github.workspace }}/.cache/sccache 7 | 8 | jobs: 9 | sanitizer: 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | cxx: [g++, clang++] 14 | sanitizer: [address, undefined] 15 | 16 | runs-on: ubuntu-20.04 17 | 18 | env: 19 | CXX: ${{ matrix.cxx }} 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - uses: actions/setup-python@v2 24 | with: 25 | python-version: '3.x' 26 | - name: Cache dependencies 27 | uses: actions/cache@v2 28 | env: 29 | cache-name: cache-deps 30 | with: 31 | path: .cache/deps 32 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/ThirdPartyDeps.cmake') }} 33 | restore-keys: | 34 | ${{ runner.os }}-build-${{ env.cache-name }}- 35 | ${{ runner.os }}-build- 36 | ${{ runner.os }}- 37 | - name: Cache build 38 | uses: actions/cache@v2 39 | env: 40 | cache-name: cache-build 41 | with: 42 | path: .cache/sccache 43 | key: ${{ runner.os }}-build-${{ env.cache-name }} 44 | - name: Install sccache 45 | run: | 46 | curl -SL https://github.com/mozilla/sccache/releases/download/v0.2.15/sccache-v0.2.15-x86_64-unknown-linux-musl.tar.gz | tar xvz --strip-components=1 --wildcards "*/sccache" 47 | chmod +x sccache 48 | - run: cmake -E make_directory build 49 | 50 | - working-directory: build/ 51 | run: cmake $GITHUB_WORKSPACE -DBUILD_EXAMPLES=OFF -DCMAKE_CXX_FLAGS="-fsanitize=${{ matrix.sanitizer }}" 52 | 53 | - working-directory: build/ 54 | run: cmake --build . 55 | 56 | - working-directory: build/ 57 | run: ctest --output-on-failure 58 | -------------------------------------------------------------------------------- /.github/workflows/static-analysis.yml: -------------------------------------------------------------------------------- 1 | name: Static Analysis 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | SCCACHE_DIR: ${{ github.workspace }}/.cache/sccache 7 | 8 | jobs: 9 | static-analysis: 10 | runs-on: ubuntu-20.04 11 | env: 12 | CC: clang-10 13 | CXX: clang++-10 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/setup-python@v2 17 | with: 18 | python-version: '3.x' 19 | - name: Cache dependencies 20 | uses: actions/cache@v2 21 | env: 22 | cache-name: cache-deps 23 | with: 24 | path: .cache/deps 25 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/ThirdPartyDeps.cmake') }} 26 | restore-keys: | 27 | ${{ runner.os }}-build-${{ env.cache-name }}- 28 | ${{ runner.os }}-build- 29 | ${{ runner.os }}- 30 | - name: Cache build 31 | uses: actions/cache@v2 32 | env: 33 | cache-name: cache-build 34 | with: 35 | path: .cache/sccache 36 | key: ${{ runner.os }}-build-${{ env.cache-name }} 37 | - name: Install sccache 38 | run: | 39 | curl -SL https://github.com/mozilla/sccache/releases/download/v0.2.15/sccache-v0.2.15-x86_64-unknown-linux-musl.tar.gz | tar xvz --strip-components=1 --wildcards "*/sccache" 40 | chmod +x sccache 41 | - name: Install install static analyzers 42 | run: sudo apt install clang-tidy iwyu 43 | - name: CMake configure 44 | run: cmake -B build -DCMAKE_BUILD_TYPE=Debug -DENABLE_COVERAGE=OFF -DENABLE_TESTING=OFF -DENABLE_STATIC_ANALYSIS=ON 45 | - name: CMake build 46 | run: cmake --build build --config Debug -j --clean-first 47 | -------------------------------------------------------------------------------- /.github/workflows/unit-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Unit Tests and Coverage 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | SCCACHE_DIR: ${{ github.workspace }}/.cache/sccache 7 | 8 | jobs: 9 | code-coverage: 10 | runs-on: ubuntu-20.04 11 | env: 12 | CC: gcc-9 13 | CXX: g++-9 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/setup-python@v2 17 | with: 18 | python-version: '3.x' 19 | - name: Cache dependencies 20 | uses: actions/cache@v2 21 | env: 22 | cache-name: cache-deps 23 | with: 24 | path: .cache/deps 25 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/ThirdPartyDeps.cmake') }} 26 | restore-keys: | 27 | ${{ runner.os }}-build-${{ env.cache-name }}- 28 | ${{ runner.os }}-build- 29 | ${{ runner.os }}- 30 | - name: Cache build 31 | uses: actions/cache@v2 32 | env: 33 | cache-name: cache-build 34 | with: 35 | path: .cache/sccache 36 | key: ${{ runner.os }}-build-${{ env.cache-name }} 37 | - name: Install sccache 38 | run: | 39 | curl -SL https://github.com/mozilla/sccache/releases/download/v0.2.15/sccache-v0.2.15-x86_64-unknown-linux-musl.tar.gz | tar xvz --strip-components=1 --wildcards "*/sccache" 40 | chmod +x sccache 41 | - name: Install Lcov 42 | run: sudo apt install lcov ninja-build 43 | - name: CMake configure 44 | run: cmake -B build -DCMAKE_BUILD_TYPE=Debug -DENABLE_COVERAGE=ON -DENABLE_TESTING=ON 45 | - name: CMake build 46 | run: cmake --build build --config Debug -j --clean-first 47 | - name: CTest 48 | working-directory: build/ 49 | run: ctest --output-on-failure 50 | - name: Generate Coverage Report 51 | run: bash ./scripts/run_coverage.sh 52 | - name: Upload coverage to Codecov 53 | run: bash <(curl -s https://codecov.io/bash) -f coverage.info || echo "Codecov did not collect coverage reports" 54 | 55 | 56 | -------------------------------------------------------------------------------- /.github/workflows/windows.yml: -------------------------------------------------------------------------------- 1 | name: Windows 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | SCCACHE_DIR: ${{ github.workspace }}/.cache/sccache 7 | 8 | jobs: 9 | vs2019: 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | build_type: [Debug, Release] 14 | 15 | runs-on: windows-latest 16 | 17 | env: 18 | CXX: ${{ matrix.compiler }} 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: actions/setup-python@v2 23 | with: 24 | python-version: '3.x' 25 | - name: Cache dependencies 26 | uses: actions/cache@v2 27 | env: 28 | cache-name: cache-deps 29 | with: 30 | path: .cache/deps 31 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/ThirdPartyDeps.cmake') }} 32 | restore-keys: | 33 | ${{ runner.os }}-build-${{ env.cache-name }}- 34 | ${{ runner.os }}-build- 35 | ${{ runner.os }}- 36 | - name: Cache build 37 | uses: actions/cache@v2 38 | env: 39 | cache-name: cache-build 40 | with: 41 | path: .cache/sccache 42 | key: ${{ runner.os }}-build-${{ env.cache-name }} 43 | - name: Install sccache 44 | run: choco install sccache 45 | - run: cmake -E make_directory build 46 | 47 | - shell: bash 48 | working-directory: build/ 49 | run: cmake $GITHUB_WORKSPACE -G "Visual Studio 16 2019" 50 | 51 | - working-directory: build/ 52 | run: cmake --build . --config ${{ matrix.build_type }} 53 | 54 | - working-directory: build/ 55 | run: ctest -C ${{ matrix.build_type }} --output-on-failure 56 | 57 | vs2019-clang: 58 | strategy: 59 | fail-fast: false 60 | matrix: 61 | build_type: [Debug, Release] 62 | 63 | runs-on: windows-latest 64 | 65 | steps: 66 | - uses: actions/checkout@v2 67 | - uses: actions/setup-python@v2 68 | with: 69 | python-version: '3.x' 70 | - name: Cache dependencies 71 | uses: actions/cache@v2 72 | env: 73 | cache-name: cache-deps 74 | with: 75 | path: .cache/deps 76 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/ThirdPartyDeps.cmake') }} 77 | restore-keys: | 78 | ${{ runner.os }}-build-${{ env.cache-name }}- 79 | ${{ runner.os }}-build- 80 | ${{ runner.os }}- 81 | - name: Cache build 82 | uses: actions/cache@v2 83 | env: 84 | cache-name: cache-build 85 | with: 86 | path: .cache/sccache 87 | key: ${{ runner.os }}-build-${{ env.cache-name }} 88 | - name: Install sccache 89 | run: choco install sccache 90 | - run: cmake -E make_directory build 91 | 92 | - shell: bash 93 | working-directory: build/ 94 | run: cmake $GITHUB_WORKSPACE -G "Visual Studio 16 2019" -T ClangCL 95 | 96 | - working-directory: build/ 97 | run: cmake --build . --config ${{ matrix.build_type }} 98 | 99 | - working-directory: build/ 100 | run: ctest -C ${{ matrix.build_type }} --output-on-failure 101 | 102 | vs2017: 103 | strategy: 104 | fail-fast: false 105 | matrix: 106 | visual_studio: 107 | - 'Visual Studio 15 2017' 108 | - 'Visual Studio 15 2017 Win64' 109 | build_type: [Debug, Release] 110 | 111 | runs-on: windows-2016 112 | 113 | steps: 114 | - uses: actions/checkout@v2 115 | - uses: actions/setup-python@v2 116 | with: 117 | python-version: '3.x' 118 | - name: Cache dependencies 119 | uses: actions/cache@v2 120 | env: 121 | cache-name: cache-deps 122 | with: 123 | path: .cache/deps 124 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/ThirdPartyDeps.cmake') }} 125 | restore-keys: | 126 | ${{ runner.os }}-build-${{ env.cache-name }}- 127 | ${{ runner.os }}-build- 128 | ${{ runner.os }}- 129 | - name: Cache build 130 | uses: actions/cache@v2 131 | env: 132 | cache-name: cache-build 133 | with: 134 | path: .cache/sccache 135 | key: ${{ runner.os }}-build-${{ env.cache-name }} 136 | - name: Install sccache 137 | run: choco install sccache 138 | - run: cmake -E make_directory build 139 | 140 | - shell: bash 141 | working-directory: build/ 142 | run: cmake $GITHUB_WORKSPACE -G "${{ matrix.visual_studio }}" 143 | 144 | - working-directory: build/ 145 | run: cmake --build . --config ${{ matrix.build_type }} 146 | 147 | - working-directory: build/ 148 | run: ctest -C ${{ matrix.build_type }} --output-on-failure 149 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache/ 2 | .deps/ 3 | coverage.info 4 | wheelhouse/ 5 | .ccls-cache/ 6 | .clangd/ 7 | # Created by https://www.gitignore.io/api/c++,python,cmake 8 | # Edit at https://www.gitignore.io/?templates=c++,python,cmake 9 | 10 | ### C++ ### 11 | # Prerequisites 12 | *.d 13 | 14 | # Compiled Object files 15 | *.slo 16 | *.lo 17 | *.o 18 | *.obj 19 | 20 | # Precompiled Headers 21 | *.gch 22 | *.pch 23 | 24 | # Compiled Dynamic libraries 25 | *.so 26 | *.dylib 27 | *.dll 28 | 29 | # Fortran module files 30 | *.mod 31 | *.smod 32 | 33 | # Compiled Static libraries 34 | *.lai 35 | *.la 36 | *.a 37 | *.lib 38 | 39 | # Executables 40 | *.exe 41 | *.out 42 | *.app 43 | 44 | ### CMake ### 45 | CMakeLists.txt.user 46 | CMakeCache.txt 47 | CMakeFiles 48 | CMakeScripts 49 | Testing 50 | Makefile 51 | cmake_install.cmake 52 | install_manifest.txt 53 | compile_commands.json 54 | CTestTestfile.cmake 55 | _deps 56 | 57 | ### CMake Patch ### 58 | # External projects 59 | *-prefix/ 60 | 61 | ### Python ### 62 | # Byte-compiled / optimized / DLL files 63 | __pycache__/ 64 | *.py[cod] 65 | *$py.class 66 | 67 | # C extensions 68 | 69 | # Distribution / packaging 70 | .Python 71 | build/ 72 | develop-eggs/ 73 | dist/ 74 | downloads/ 75 | eggs/ 76 | .eggs/ 77 | lib/ 78 | lib64/ 79 | parts/ 80 | sdist/ 81 | var/ 82 | wheels/ 83 | pip-wheel-metadata/ 84 | share/python-wheels/ 85 | *.egg-info/ 86 | .installed.cfg 87 | *.egg 88 | MANIFEST 89 | 90 | # PyInstaller 91 | # Usually these files are written by a python script from a template 92 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 93 | *.manifest 94 | *.spec 95 | 96 | # Installer logs 97 | pip-log.txt 98 | pip-delete-this-directory.txt 99 | 100 | # Unit test / coverage reports 101 | htmlcov/ 102 | .tox/ 103 | .nox/ 104 | .coverage 105 | .coverage.* 106 | .cache 107 | nosetests.xml 108 | coverage.xml 109 | *.cover 110 | .hypothesis/ 111 | .pytest_cache/ 112 | 113 | # Translations 114 | *.mo 115 | *.pot 116 | 117 | # Scrapy stuff: 118 | .scrapy 119 | 120 | # Sphinx documentation 121 | docs/_build/ 122 | 123 | # PyBuilder 124 | target/ 125 | 126 | # pyenv 127 | .python-version 128 | 129 | # pipenv 130 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 131 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 132 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 133 | # install all needed dependencies. 134 | #Pipfile.lock 135 | 136 | # celery beat schedule file 137 | celerybeat-schedule 138 | 139 | # SageMath parsed files 140 | *.sage.py 141 | 142 | # Spyder project settings 143 | .spyderproject 144 | .spyproject 145 | 146 | # Rope project settings 147 | .ropeproject 148 | 149 | # Mr Developer 150 | .mr.developer.cfg 151 | .project 152 | .pydevproject 153 | 154 | # mkdocs documentation 155 | /site 156 | 157 | # mypy 158 | .mypy_cache/ 159 | .dmypy.json 160 | dmypy.json 161 | 162 | # Pyre type checker 163 | .pyre/ 164 | 165 | # End of https://www.gitignore.io/api/c++,python,cmake 166 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.11) 2 | 3 | list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") 4 | 5 | # Check if signaltl is being used directly or via add_subdirectory 6 | set(SIGNALTL_MASTER_PROJECT OFF) 7 | if(CMAKE_CURRENT_LIST_DIR STREQUAL CMAKE_SOURCE_DIR) 8 | set(SIGNALTL_MASTER_PROJECT ON) 9 | endif() 10 | 11 | include(cmake/Version.cmake) 12 | message(STATUS "signaltl: v${SIGNALTL_FULL_VERSION}") 13 | 14 | project( 15 | signaltl 16 | VERSION "${SIGNALTL_VERSION}" 17 | DESCRIPTION 18 | "A library for efficiently working with Signal Temporal Logic (STL) and its quantitative semantics" 19 | LANGUAGES CXX 20 | ) 21 | 22 | include(CMakeDependentOption) 23 | 24 | # Options 25 | option(BUILD_CORE_ONLY "Build only the core AST and Signal" OFF) 26 | cmake_dependent_option( 27 | BUILD_PARSER "Don't build the parser" ON "NOT BUILD_CORE_ONLY" OFF 28 | ) 29 | cmake_dependent_option( 30 | BUILD_ROBUSTNESS "Don't build the robust semantics for STL" ON 31 | "NOT BUILD_CORE_ONLY" OFF 32 | ) 33 | 34 | option(BUILD_DOCS "Build the documentation?" OFF) 35 | option(BUILD_EXAMPLES "Build the examples?" ${SIGNALTL_MASTER_PROJECT}) 36 | 37 | # TODO: Turn this on once the library is stable. 38 | option(BUILD_PYTHON_BINDINGS "Build the Python extension?" 39 | OFF 40 | ) 41 | 42 | option(ENABLE_CACHE "Enable cache if available" ${SIGNALTL_MASTER_PROJECT}) 43 | option(ENABLE_TESTING "Build signaltl test suite?" ${SIGNALTL_MASTER_PROJECT}) 44 | # Coverage needs to be explicitly turned on, but available only if Testing is 45 | # enabled. 46 | cmake_dependent_option( 47 | ENABLE_COVERAGE "Generate coverage.xml for test suite?" OFF "ENABLE_TESTING" 48 | OFF 49 | ) 50 | option(ENABLE_STATIC_ANALYSIS "Enable clang-tidy and include-what-you-use" OFF) 51 | 52 | set(_SIGNALTL_BUILD_THE_TESTS 53 | OFF 54 | CACHE INTERNAL "Easy option to build the tests" 55 | ) 56 | if((SIGNALTL_MASTER_PROJECT AND ENABLE_TESTING) AND BUILD_TESTING) 57 | set(_SIGNALTL_BUILD_THE_TESTS 58 | ON 59 | CACHE INTERNAL "Easy option to build the tests" 60 | ) 61 | endif() 62 | 63 | if(NOT BUILD_CORE_ONLY) 64 | if(NOT NO_BUILD_PARSER) 65 | set(BUILD_PARSER 66 | ON 67 | CACHE BOOL "Build the parser" 68 | ) 69 | endif() 70 | if(NOT NO_BUILD_ROBUSTNESS) 71 | set(BUILD_ROBUSTNESS 72 | ON 73 | CACHE BOOL "Build the robustness" 74 | ) 75 | endif() 76 | endif() 77 | 78 | # ############################################################################## 79 | # Include CMake Modules # 80 | # ############################################################################## 81 | 82 | include(StandardProjectSettings) 83 | include(PreventInSourceBuilds) 84 | include(CompilerWarnings) 85 | include(Cache) 86 | include(StaticAnalyzers) 87 | 88 | include(CTest) 89 | include(Codecov) 90 | 91 | # ############################################################################## 92 | # Some Global Configuration # 93 | # ############################################################################## 94 | 95 | set(CMAKE_CXX_STANDARD 17) 96 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 97 | set(CMAKE_CXX_EXTENSIONS ON) 98 | set(CMAKE_POSITION_INDEPENDENT_CODE ON) 99 | 100 | set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib) 101 | set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib) 102 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin) 103 | 104 | # ############################################################################## 105 | # Third Party dependencies # 106 | # ############################################################################## 107 | 108 | include(ThirdPartyDeps) 109 | 110 | # ############################################################################## 111 | # Sources # 112 | # ############################################################################## 113 | 114 | add_subdirectory(src) 115 | add_library(signaltl::signaltl ALIAS signaltl) 116 | add_subdirectory(python_bindings) 117 | 118 | if(BUILD_EXAMPLES) 119 | add_subdirectory(examples) 120 | endif() 121 | 122 | if(ENABLE_TESTING) 123 | add_subdirectory(tests) 124 | coverage_evaluate() 125 | endif() 126 | 127 | if(BUILD_DOCS) 128 | add_subdirectory(docs) 129 | endif() 130 | 131 | # ############################################################################## 132 | # Installation # 133 | # ############################################################################## 134 | 135 | include(GNUInstallDirs) 136 | include(CMakePackageConfigHelpers) 137 | set(SIGNALTL_CMAKECONFIG_INSTALL_DIR 138 | "${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}" 139 | CACHE STRING "install path for signaltlConfig.cmake" 140 | ) 141 | set(INSTALL_CONFIGDIR ${SIGNALTL_CMAKECONFIG_INSTALL_DIR}) 142 | 143 | install( 144 | TARGETS signaltl 145 | EXPORT signaltl-targets 146 | LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} 147 | ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} 148 | ) 149 | 150 | install(DIRECTORY ${SIGNALTL_INCLUDE_DIRS} TYPE INCLUDE) 151 | 152 | install( 153 | EXPORT signaltl-targets 154 | FILE signaltlTargets.cmake 155 | NAMESPACE signaltl:: 156 | DESTINATION ${INSTALL_CONFIGDIR} 157 | ) 158 | 159 | # Create a ConfigVersion.cmake file 160 | write_basic_package_version_file( 161 | ${CMAKE_CURRENT_BINARY_DIR}/signaltlConfigVersion.cmake 162 | VERSION ${PROJECT_VERSION} 163 | COMPATIBILITY AnyNewerVersion 164 | ) 165 | 166 | configure_package_config_file( 167 | ${CMAKE_CURRENT_LIST_DIR}/cmake/signaltlConfig.cmake.in 168 | ${CMAKE_CURRENT_BINARY_DIR}/signaltlConfig.cmake 169 | INSTALL_DESTINATION ${INSTALL_CONFIGDIR} 170 | ) 171 | 172 | # Install the config, configversion and custom find modules 173 | install(FILES ${CMAKE_CURRENT_BINARY_DIR}/signaltlConfig.cmake 174 | ${CMAKE_CURRENT_BINARY_DIR}/signaltlConfigVersion.cmake 175 | DESTINATION ${INSTALL_CONFIGDIR} 176 | ) 177 | 178 | export( 179 | EXPORT signaltl-targets 180 | FILE ${CMAKE_CURRENT_BINARY_DIR}/signaltlTargets.cmake 181 | NAMESPACE signaltl:: 182 | ) 183 | 184 | # Register package in user's package registry 185 | export(PACKAGE signaltl) 186 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Anand Balakrishnan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE-3rdParty: -------------------------------------------------------------------------------- 1 | This projects either includes source code derived from the following projects 2 | or directly depends on these projects: 3 | 4 | pybind11 5 | -------- 6 | 7 | Copyright (c) 2016 Wenzel Jakob , All rights reserved. 8 | 9 | Redistribution and use in source and binary forms, with or without 10 | modification, are permitted provided that the following conditions are met: 11 | 12 | 1. Redistributions of source code must retain the above copyright notice, this 13 | list of conditions and the following disclaimer. 14 | 15 | 2. Redistributions in binary form must reproduce the above copyright notice, 16 | this list of conditions and the following disclaimer in the documentation 17 | and/or other materials provided with the distribution. 18 | 19 | 3. Neither the name of the copyright holder nor the names of its contributors 20 | may be used to endorse or promote products derived from this software 21 | without specific prior written permission. 22 | 23 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 24 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 25 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 26 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 27 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 28 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 29 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 31 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 32 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | 34 | Please also refer to the file CONTRIBUTING.md, which clarifies licensing of 35 | external contributions to this project including patches, pull requests, etc. 36 | 37 | 38 | Breach Matlab Toolbox 39 | --------------------- 40 | 41 | Copyright (c) 2017, Decyphir, Inc 42 | All rights reserved. 43 | 44 | Redistribution and use in source and binary forms, with or without 45 | modification, are permitted provided that the following conditions are met: 46 | 47 | * Redistributions of source code must retain the above copyright notice, this 48 | list of conditions and the following disclaimer. 49 | 50 | * Redistributions in binary form must reproduce the above copyright notice, 51 | this list of conditions and the following disclaimer in the documentation 52 | and/or other materials provided with the distribution. 53 | 54 | * Neither the name of Decyphir, Inc, nor the name of the University of 55 | California, Berkeley, nor the name of Verimag, nor the names of its 56 | contributors may be used to endorse or promote products derived from this 57 | software without specific prior written permission. 58 | 59 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 60 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 61 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 62 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 63 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 64 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 65 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 66 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 67 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 68 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 69 | 70 | Lemire-Fenn algorithm for Rolling Min/Max 71 | ----------------------------------------- 72 | 73 | MIT License 74 | 75 | Copyright (c) 2016 Evan Balster 76 | 77 | Permission is hereby granted, free of charge, to any person obtaining a copy 78 | of this software and associated documentation files (the "Software"), to deal 79 | in the Software without restriction, including without limitation the rights 80 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 81 | copies of the Software, and to permit persons to whom the Software is 82 | furnished to do so, subject to the following conditions: 83 | 84 | The above copyright notice and this permission notice shall be included in all 85 | copies or substantial portions of the Software. 86 | 87 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 88 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 89 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 90 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 91 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 92 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 93 | SOFTWARE. 94 | 95 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | global-exclude __pycache__ 2 | global-exclude *.py[co] 3 | 4 | include README.md LICENSE LICENSE-3rdParty MANIFEST.in VERSION 5 | global-include *.md 6 | global-include CMakeLists.txt *.cmake 7 | global-include conanfile.txt 8 | 9 | graft cmake 10 | graft examples 11 | graft tests 12 | graft include 13 | graft signal_tl 14 | graft src 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This repository has been archived in favor of my other package [argus](https://github.com/anand-bala/argus) 2 | 3 | [![pypi](https://badge.fury.io/py/signal-temporal-logic.svg)](https://badge.fury.io/py/signal-temporal-logic) 4 | [![codecov](https://codecov.io/gh/anand-bala/signal-temporal-logic/branch/master/graph/badge.svg)](https://codecov.io/gh/anand-bala/signal-temporal-logic) 5 | 6 | # Signal Temporal Logic Monitoring 7 | 8 | This package provides an interface to define offline monitoring for Signal Temporal 9 | Logic (STL) specifications. The library is written in C++ (and can be used with [CMake]) 10 | and has been wrapped for Python usage using [pybind11]. 11 | 12 | The library is inspired by the following projects: 13 | 14 | - [py-metric-temporal-logic] is a tool written in pure Python, and provides an elegant 15 | interface for evaluating discrete time signals using Metric Temporal Logic (MTL). 16 | - [Breach] and [S-TaLiRo] are [Matlab] toolboxes designed for falsification and 17 | simulation-based testing of cyber-physical systems with STL and MTL specifications, 18 | respectively. One of their various features includes the ability to evaluate the 19 | robustness of signals against STL/MTL. 20 | 21 | The `signal-temporal-logic` library aims to be different from the above in the following 22 | ways: 23 | 24 | - Written for speed and targets Python. 25 | - While [py-metric-temporal-logic] is written in Python, it doesn't perform 26 | computations efficiently and quickly. 27 | - Support for multiple quantitative semantics. 28 | - All the above tools have their own way of computing the quantitative semantics for 29 | STL/MTL specifications. 30 | - This tool will try to support common ways of computing the robustness, but will 31 | also have support for other quantitative semantics of STL. 32 | 33 | 34 | [CMake]: https://cmake.org/ 35 | [pybind11]: https://pybind11.readthedocs.io/en/stable/ 36 | [py-metric-temporal-logic]: https://github.com/mvcisback/py-metric-temporal-logic/ 37 | [Matlab]: https://www.mathworks.com/products/matlab.html 38 | [Breach]: https://github.com/decyphir/breach 39 | [S-TaLiRo]: https://sites.google.com/a/asu.edu/s-taliro/s-taliro 40 | 41 | ## List of Quantitative Semantics 42 | 43 | - [Classic Robustness](http://www-verimag.imag.fr/~maler/Papers/sensiform.pdf) 44 | - A. Donzé and O. Maler, "Robust Satisfaction of Temporal Logic over Real-Valued 45 | Signals," in Formal Modeling and Analysis of Timed Systems, Berlin, Heidelberg, 46 | 2010, pp. 92–106. 47 | - [Temporal Logic as Filtering](https://arxiv.org/abs/1510.08079) 48 | - A. Rodionova, E. Bartocci, D. Nickovic, and R. Grosu, "Temporal Logic As 49 | Filtering," in Proceedings of the 19th International Conference on Hybrid Systems: 50 | Computation and Control, New York, NY, USA, 2016, pp. 11–20. 51 | - [Smooth Cumulative Semantics](https://arxiv.org/abs/1904.11611) 52 | - I. Haghighi, N. Mehdipour, E. Bartocci, and C. Belta, "Control from Signal 53 | Temporal Logic Specifications with Smooth Cumulative Quantitative Semantics," 54 | arXiv:1904.11611 [cs], Apr. 2019. 55 | 56 | # Installing Python package 57 | 58 | ## Using `pip` 59 | 60 | ```shell 61 | $ python3 -m pip install signal-temporal-logic 62 | ``` 63 | 64 | ## Build from source 65 | 66 | **Requirements:** `cmake` >= 3.5, `git` and a C++ compiler that supports C++17. 67 | 68 | First clone the repository: 69 | ```shell 70 | $ git clone https://github.com/anand-bala/signal-temporal-logic 71 | ``` 72 | 73 | Then install using `pip`, install the package: 74 | ```shell 75 | $ python3 -m pip install -U . 76 | ``` 77 | 78 | # Usage 79 | 80 | See the [examples/ directory](examples/) for some usage examples in C++ and Python. 81 | 82 | 83 | -------------------------------------------------------------------------------- /cmake/Cache.cmake: -------------------------------------------------------------------------------- 1 | if(NOT ENABLE_CACHE) 2 | return() 3 | endif() 4 | 5 | set(CACHE_OPTION_VALUES "sccache" "ccache") 6 | 7 | message(STATUS "Compiler caching enabled") 8 | message(CHECK_START "Finding suitable compiler caching tool") 9 | foreach(cache_option ${CACHE_OPTION_VALUES}) 10 | # Try to find a suitable cache compiler. Provide PROJECT_SOURCE_DIR and 11 | # FETCHCONTENT_BASE_DIR as hints as we will download the Linux sccache 12 | # binaries there in the CI. 13 | find_program( 14 | CACHE_BINARY ${cache_option} HINTS "${PROJECT_SOURCE_DIR}" 15 | "${FETCHCONTENT_BASE_DIR}" 16 | ) 17 | if(CACHE_BINARY) 18 | break() 19 | endif() 20 | endforeach() 21 | 22 | if(NOT CACHE_BINARY) 23 | message(CHECK_FAIL "not found") 24 | message(WARNING "Compiler caching is enabled but not using it.") 25 | return() 26 | endif() 27 | 28 | message(CHECK_PASS "found ${CACHE_BINARY} (enabled)") 29 | 30 | set(C_LAUNCHER "${CACHE_BINARY}") 31 | set(CXX_LAUNCHER "${CACHE_BINARY}") 32 | 33 | if(CMAKE_GENERATOR STREQUAL "Xcode") 34 | configure_file(${CMAKE_CURRENT_LIST_DIR}/launch-c.in launch-c) 35 | configure_file(${CMAKE_CURRENT_LIST_DIR}/launch-cxx.in launch-cxx) 36 | 37 | execute_process( 38 | COMMAND chmod a+rx "${CMAKE_CURRENT_BINARY_DIR}/launch-c" 39 | "${CMAKE_CURRENT_BINARY_DIR}/launch-cxx" 40 | ) 41 | 42 | # Set Xcode project attributes to route compilation and linking through our 43 | # scripts 44 | set(CMAKE_XCODE_ATTRIBUTE_CC 45 | "${CMAKE_CURRENT_BINARY_DIR}/launch-c" 46 | CACHE INTERNAL "" 47 | ) 48 | set(CMAKE_XCODE_ATTRIBUTE_CXX 49 | "${CMAKE_CURRENT_BINARY_DIR}/launch-cxx" 50 | CACHE INTERNAL "" 51 | ) 52 | set(CMAKE_XCODE_ATTRIBUTE_LD 53 | "${CMAKE_CURRENT_BINARY_DIR}/launch-c" 54 | CACHE INTERNAL "" 55 | ) 56 | set(CMAKE_XCODE_ATTRIBUTE_LDPLUSPLUS 57 | "${CMAKE_CURRENT_BINARY_DIR}/launch-cxx" 58 | CACHE INTERNAL "" 59 | ) 60 | else() 61 | # Support Unix Makefiles and Ninja 62 | set(CMAKE_C_COMPILER_LAUNCHER 63 | "${C_LAUNCHER}" 64 | CACHE INTERNAL "" 65 | ) 66 | set(CMAKE_CXX_COMPILER_LAUNCHER 67 | "${CXX_LAUNCHER}" 68 | CACHE INTERNAL "" 69 | ) 70 | endif() 71 | -------------------------------------------------------------------------------- /cmake/Codecov.cmake: -------------------------------------------------------------------------------- 1 | # This adapted from part of https://github.com/RWTH-HPC/CMake-codecov. 2 | 3 | set(CXX_COVERAGE_COMPILE_FLAGS 4 | -g -O0 --coverage -fprofile-arcs -ftest-coverage 5 | CACHE INTERNAL "" 6 | ) 7 | set(CXX_COVERAGE_LINK_FLAGS 8 | --coverage 9 | CACHE INTERNAL "" 10 | ) 11 | 12 | # Helper function to get the relative path of the source file destination path. 13 | # This path is needed by FindGcov and FindLcov cmake files to locate the 14 | # captured data. 15 | function(codecov_path_of_source FILE RETURN_VAR) 16 | string(REGEX MATCH "TARGET_OBJECTS:([^ >]+)" _source ${FILE}) 17 | 18 | # If expression was found, SOURCEFILE is a generator-expression for an object 19 | # library. Currently we found no way to call this function automatic for the 20 | # referenced target, so it must be called in the directoryso of the object 21 | # library definition. 22 | if(NOT "${_source}" STREQUAL "") 23 | set(${RETURN_VAR} 24 | "" 25 | PARENT_SCOPE 26 | ) 27 | return() 28 | endif() 29 | 30 | string(REPLACE "${CMAKE_CURRENT_BINARY_DIR}/" "" FILE "${FILE}") 31 | if(IS_ABSOLUTE ${FILE}) 32 | file(RELATIVE_PATH FILE ${CMAKE_CURRENT_SOURCE_DIR} ${FILE}) 33 | endif() 34 | 35 | # get the right path for file 36 | string(REPLACE ".." "__" PATH "${FILE}") 37 | 38 | set(${RETURN_VAR} 39 | "${PATH}" 40 | PARENT_SCOPE 41 | ) 42 | endfunction() 43 | 44 | # Add coverage flags for the given target. 45 | function(add_coverage_flags target) 46 | if(ENABLE_COVERAGE) 47 | message(DEBUG "Enabling coverage for target: ${target}") 48 | # Add required flags (GCC & LLVM/Clang) 49 | target_compile_options(${target} PRIVATE ${CXX_COVERAGE_COMPILE_FLAGS}) 50 | if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.13) 51 | target_link_options(${target} PRIVATE ${CXX_COVERAGE_LINK_FLAGS}) 52 | else() 53 | target_link_libraries(${target} PRIVATE ${CXX_COVERAGE_LINK_FLAGS}) 54 | endif() 55 | 56 | get_target_property(tsources ${target} SOURCES) 57 | set(target_compiler "") 58 | set(additional_files "") 59 | foreach(source_file ${tsources}) 60 | list(APPEND additional_files "${source_file}.gcno") 61 | list(APPEND additional_files "${source_file}.gcda") 62 | endforeach() 63 | 64 | # Add gcov files generated by compiler to clean target. 65 | set(clean_files "") 66 | foreach(file ${additional_files}) 67 | codecov_path_of_source(${file} file) 68 | list(APPEND clean_files 69 | "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/${target}.dir/${file}" 70 | ) 71 | endforeach() 72 | set_target_properties( 73 | ${target} PROPERTIES ADDITIONAL_CLEAN_FILES "${clean_files}" 74 | ADDITIONAL_MAKE_CLEAN_FILES "${clean_files}" 75 | ) 76 | 77 | message( 78 | DEBUG 79 | "Adding following files to target (${target}) for cleaning:\n\t\t ---${clean_files}" 80 | ) 81 | 82 | endif() 83 | endfunction() 84 | 85 | # Add coverage support for the given target and register target for coverage 86 | # evaluation. If coverage is disabled or not supported, this function will 87 | # simply do nothing. 88 | function(add_coverage target) 89 | if(ENABLE_COVERAGE) 90 | message(STATUS "Adding coverage flags for target: ${target}") 91 | add_coverage_flags(${target}) 92 | endif() 93 | endfunction() 94 | 95 | # Add global target to gather coverage information after all targets have been 96 | # added. Other evaluation functions could be added here, after checks for the 97 | # specific module have been passed. 98 | function(coverage_evaluate) 99 | # add lcov evaluation 100 | if(LCOV_FOUND AND ENABLE_COVERAGE) 101 | lcov_capture_initial() 102 | lcov_capture() 103 | endif() 104 | endfunction() 105 | -------------------------------------------------------------------------------- /cmake/CompilerWarnings.cmake: -------------------------------------------------------------------------------- 1 | # from here: 2 | # 3 | # https://github.com/lefticus/cppbestpractices/blob/master/02-Use_the_Tools_Available.md 4 | 5 | function(set_project_warnings project_name) 6 | option(WARNINGS_AS_ERRORS "Treat compiler warnings as errors" TRUE) 7 | 8 | set(MSVC_WARNINGS 9 | /W4 # Baseline reasonable warnings 10 | /w14640 # Enable warning on thread un-safe static member initialization 11 | /permissive- # standards conformance mode for MSVC compiler. 12 | ) 13 | 14 | if(CMAKE_BUILD_TYPE STREQUAL "Debug") 15 | list(APPEND MSVC_WARNINGS /Z7) 16 | endif() 17 | 18 | set(CLANG_WARNINGS 19 | -Wall 20 | -Wextra # reasonable and standard 21 | -Wshadow # warn the user if a variable declaration shadows one from a 22 | # parent context 23 | -Wnon-virtual-dtor # warn the user if a class with virtual functions has a 24 | # non-virtual destructor. This helps catch hard to 25 | # track down memory errors 26 | -Wold-style-cast # warn for c-style casts 27 | -Wunused # warn on anything being unused 28 | -Wpedantic # warn if non-standard C++ is used 29 | -Wformat=2 # warn on security issues around functions that format output 30 | # (ie printf) 31 | ) 32 | 33 | if(WARNINGS_AS_ERRORS) 34 | set(CLANG_WARNINGS ${CLANG_WARNINGS} -Werror) 35 | set(MSVC_WARNINGS ${MSVC_WARNINGS} /WX) 36 | endif() 37 | 38 | set(GCC_WARNINGS ${CLANG_WARNINGS}) 39 | 40 | if(MSVC) 41 | set(PROJECT_WARNINGS ${MSVC_WARNINGS}) 42 | elseif(CMAKE_CXX_COMPILER_ID MATCHES ".*Clang") 43 | set(PROJECT_WARNINGS ${CLANG_WARNINGS}) 44 | elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") 45 | set(PROJECT_WARNINGS ${GCC_WARNINGS}) 46 | else() 47 | message( 48 | AUTHOR_WARNING 49 | "No compiler warnings set for '${CMAKE_CXX_COMPILER_ID}' compiler." 50 | ) 51 | endif() 52 | 53 | target_compile_options(${project_name} PRIVATE ${PROJECT_WARNINGS}) 54 | 55 | endfunction() 56 | -------------------------------------------------------------------------------- /cmake/FindSphinx.cmake: -------------------------------------------------------------------------------- 1 | # Look for an executable called sphinx-build 2 | find_program( 3 | SPHINX_EXECUTABLE 4 | NAMES sphinx-build 5 | DOC "Path to sphinx-build executable" 6 | ) 7 | 8 | include(FindPackageHandleStandardArgs) 9 | 10 | # Handle standard arguments to find_package like REQUIRED and QUIET 11 | find_package_handle_standard_args( 12 | Sphinx "Failed to find sphinx-build executable" SPHINX_EXECUTABLE 13 | ) 14 | -------------------------------------------------------------------------------- /cmake/PreventInSourceBuilds.cmake: -------------------------------------------------------------------------------- 1 | # This function will prevent in-source builds 2 | function(AssureOutOfSourceBuilds) 3 | # make sure the user doesn't play dirty with symlinks 4 | get_filename_component(srcdir "${CMAKE_SOURCE_DIR}" REALPATH) 5 | get_filename_component(bindir "${CMAKE_BINARY_DIR}" REALPATH) 6 | 7 | # disallow in-source builds 8 | if("${srcdir}" STREQUAL "${bindir}") 9 | message("######################################################") 10 | message("Warning: in-source builds are disabled") 11 | message("Please create a separate build directory and run cmake from there") 12 | message("######################################################") 13 | message(FATAL_ERROR "Quitting configuration") 14 | endif() 15 | endfunction() 16 | 17 | assureoutofsourcebuilds() 18 | -------------------------------------------------------------------------------- /cmake/StandardProjectSettings.cmake: -------------------------------------------------------------------------------- 1 | include(CMakeParseArguments) 2 | 3 | # Set a default build type if none was specified 4 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 5 | message(STATUS "Setting build type to 'Debug' as none was specified.") 6 | set(CMAKE_BUILD_TYPE 7 | Debug 8 | CACHE STRING "Choose the type of build." FORCE 9 | ) 10 | # Set the possible values of build type for cmake-gui, ccmake 11 | set_property( 12 | CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" 13 | "RelWithDebInfo" 14 | ) 15 | endif() 16 | 17 | if(PROJECT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) 18 | set(CMAKE_EXPORT_COMPILE_COMMANDS 19 | ON 20 | CACHE BOOL "Enable/Disable output of compile commands during generation." 21 | FORCE 22 | ) 23 | endif() 24 | 25 | option(ENABLE_LTO "Enable link time optimization?" OFF) 26 | 27 | include(CheckIPOSupported) 28 | check_ipo_supported(RESULT result) 29 | if(result) 30 | set(ENABLE_LTO 31 | ON 32 | CACHE BOOL "Enable link time optimization?" FORCE 33 | ) 34 | endif() 35 | 36 | # Set some default options for target 37 | function(set_default_compile_options target) 38 | 39 | if(PROJECT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) 40 | # Force color in compiler output as it will be easier to debug... 41 | if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") 42 | # using Clang 43 | target_compile_options(${target} PRIVATE -fcolor-diagnostics) 44 | elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") 45 | # using GCC 46 | target_compile_options(${target} PRIVATE -fdiagnostics-color=always) 47 | endif() 48 | endif() 49 | 50 | if(ENABLE_LTO) 51 | set_target_properties( 52 | ${target} PROPERTIES INTERPROCEDURAL_OPTIMIZATION TRUE 53 | ) 54 | endif() 55 | 56 | if(MSVC) 57 | target_compile_options(${target} PRIVATE /Zc:__cplusplus) 58 | endif() 59 | 60 | endfunction() 61 | 62 | # Set compile and link flags for std::filesystem 63 | function(set_std_filesystem_options target) 64 | message(CHECK_START "Checking compiler flags for std::filesystem") 65 | # Check if we need to add -lstdc++fs or -lc++fs or nothing 66 | if(MSVC) 67 | set(std_fs_no_lib_needed TRUE) 68 | else() 69 | try_compile( 70 | std_fs_no_lib_needed ${CMAKE_CURRENT_BINARY_DIR} 71 | SOURCES ${PROJECT_SOURCE_DIR}/cmake/test_std_filesystem.cc 72 | COMPILE_DEFINITIONS -std=c++17 73 | ) 74 | try_compile( 75 | std_fs_needs_stdcxxfs ${CMAKE_CURRENT_BINARY_DIR} 76 | SOURCES ${PROJECT_SOURCE_DIR}/cmake/test_std_filesystem.cc 77 | COMPILE_DEFINITIONS -std=c++17 78 | LINK_LIBRARIES stdc++fs 79 | ) 80 | try_compile( 81 | std_fs_needs_cxxfs ${CMAKE_CURRENT_BINARY_DIR} 82 | SOURCES ${PROJECT_SOURCE_DIR}/cmake/test_std_filesystem.cc 83 | COMPILE_DEFINITIONS -std=c++17 84 | LINK_LIBRARIES c++fs 85 | ) 86 | endif() 87 | 88 | if(${std_fs_needs_stdcxxfs}) 89 | set(std_fs_lib stdc++fs) 90 | message(CHECK_PASS "${std_fs_lib}") 91 | elseif(${std_fs_needs_cxxfs}) 92 | set(std_fs_lib c++fs) 93 | message(CHECK_PASS "${std_fs_lib}") 94 | elseif(${std_fs_no_lib_needed}) 95 | set(std_fs_lib "") 96 | message(CHECK_PASS "No flags needed") 97 | else() 98 | message(CHECK_FAIL "unknown compiler, not passing -lstdc++fs or -lc++fs") 99 | set(std_fs_lib "") 100 | endif() 101 | 102 | target_link_libraries(${target} PUBLIC ${std_fs_lib}) 103 | endfunction() 104 | -------------------------------------------------------------------------------- /cmake/StaticAnalyzers.cmake: -------------------------------------------------------------------------------- 1 | option(ENABLE_CLANG_TIDY "Enable static analysis with clang-tidy" 2 | ${ENABLE_STATIC_ANALYSIS} 3 | ) 4 | option(ENABLE_INCLUDE_WHAT_YOU_USE 5 | "Enable static analysis with include-what-you-use" 6 | ${ENABLE_STATIC_ANALYSIS} 7 | ) 8 | 9 | include(CMakeParseArguments) 10 | 11 | if(ENABLE_CLANG_TIDY) 12 | message(CHECK_START "Finding clang-tidy") 13 | find_program(CLANGTIDY clang-tidy) 14 | if(CLANGTIDY) 15 | message(CHECK_PASS "found: ${CLANGTIDY}") 16 | set(CLANG_TIDY_CMD 17 | ${CLANGTIDY} -extra-arg=-Wno-unknown-warning-option 18 | CACHE INTERNAL "Command for clang-tidy" 19 | ) 20 | else() 21 | message(CHECK_FAIL "not found") 22 | message(SEND_ERROR "clang-tidy requested but executable not found") 23 | endif() 24 | endif() 25 | 26 | function(enable_clang_tidy target) 27 | message(STATUS "Enabling clang-tidy checks for ${target}") 28 | 29 | if(ENABLE_CLANG_TIDY) 30 | set_target_properties( 31 | ${target} PROPERTIES CXX_CLANG_TIDY "${CLANG_TIDY_CMD}" 32 | ) 33 | endif() 34 | endfunction() 35 | 36 | if(ENABLE_INCLUDE_WHAT_YOU_USE) 37 | message(CHECK_START "Finding include-what-you-use") 38 | find_program(INCLUDE_WHAT_YOU_USE include-what-you-use) 39 | if(INCLUDE_WHAT_YOU_USE) 40 | message(CHECK_PASS "found: ${INCLUDE_WHAT_YOU_USE}") 41 | set(INCLUDE_WHAT_YOU_USE_CMD 42 | ${INCLUDE_WHAT_YOU_USE} 43 | CACHE INTERNAL "Command for include-what-you-use" 44 | ) 45 | else() 46 | message(CHECK_FAIL "not found") 47 | message( 48 | SEND_ERROR "include-what-you-use requested but executable not found" 49 | ) 50 | endif() 51 | endif() 52 | 53 | function(enable_include_what_you_use target) 54 | message(STATUS "Enabling include-what-you-use checks for ${target}") 55 | 56 | if(ENABLE_INCLUDE_WHAT_YOU_USE) 57 | set_target_properties( 58 | ${target} PROPERTIES CXX_INCLUDE_WHAT_YOU_USE 59 | "${INCLUDE_WHAT_YOU_USE_CMD}" 60 | ) 61 | endif() 62 | endfunction() 63 | -------------------------------------------------------------------------------- /cmake/ThirdPartyDeps.cmake: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.11 FATAL_ERROR) 2 | 3 | include(FetchContent) 4 | include(CMakePrintHelpers) 5 | 6 | if(SIGNALTL_MASTER_PROJECT) 7 | get_filename_component( 8 | fc_base "./.cache/deps" REALPATH BASE_DIR "${PROJECT_SOURCE_DIR}" 9 | ) 10 | set(FETCHCONTENT_BASE_DIR ${fc_base}) 11 | endif() 12 | 13 | # ############################################################################## 14 | # Core Dependencies 15 | # ############################################################################## 16 | 17 | if(NOT BUILD_CORE_ONLY) 18 | if(NOT NO_BUILD_ROBUSTNESS) 19 | set(_BUILD_PARSER ON) 20 | endif() 21 | if(NOT NO_BUILD_ROBUSTNESS) 22 | set(_BUILD_ROBUSTNESS ON) 23 | endif() 24 | endif() 25 | 26 | unset(CMAKE_CXX_CLANG_TIDY) 27 | unset(CMAKE_CXX_INCLUDE_WHAT_YOU_USE) 28 | 29 | message(CHECK_START "Looking for fmtlib/fmt") 30 | find_package(fmt QUIET) 31 | if(NOT fmt_FOUND) 32 | message(CHECK_FAIL "system library not found (using fetched version).") 33 | FetchContent_Declare( 34 | fmt 35 | GIT_REPOSITORY https://github.com/fmtlib/fmt.git 36 | GIT_TAG 7.1.3 37 | GIT_PROGRESS ON 38 | ) 39 | 40 | FetchContent_GetProperties(fmt) 41 | if(NOT fmt_POPULATED) 42 | FetchContent_Populate(fmt) 43 | set(FMT_INSTALL 44 | ON 45 | CACHE BOOL "Generate the install target for fmtlib." FORCE 46 | ) 47 | add_subdirectory(${fmt_SOURCE_DIR} ${fmt_BINARY_DIR}) 48 | endif() 49 | else() 50 | message(CHECK_PASS "system library found.") 51 | endif() 52 | 53 | if(BUILD_PARSER) 54 | message(CHECK_START "Looking for taocpp/pegtl") 55 | find_package(pegtl QUIET) 56 | if(NOT pegtl_FOUND) 57 | message(CHECK_FAIL "system library not found (using fetched version).") 58 | FetchContent_Declare( 59 | pegtl 60 | GIT_REPOSITORY https://github.com/taocpp/PEGTL.git 61 | GIT_TAG f34a1170afba0207a3c29ec08959591e3e7068c5 62 | GIT_PROGRESS ON 63 | ) 64 | 65 | FetchContent_GetProperties(pegtl) 66 | if(NOT pegtl_POPULATED) 67 | FetchContent_Populate(pegtl) 68 | add_subdirectory(${pegtl_SOURCE_DIR} ${pegtl_BINARY_DIR}) 69 | endif() 70 | else() 71 | message(CHECK_PASS "system library found.") 72 | endif() 73 | endif() 74 | 75 | # ############################################################################## 76 | # Python Bindings 77 | # ############################################################################## 78 | 79 | if(BUILD_PYTHON_BINDINGS) 80 | message(CHECK_START "Looking for pybind11/pybind11") 81 | find_package(pybind11 QUIET) 82 | if(NOT pybind11_FOUND) 83 | message(CHECK_FAIL "system library not found (using fetched version).") 84 | FetchContent_Declare( 85 | pybind11 86 | GIT_REPOSITORY https://github.com/pybind/pybind11.git 87 | GIT_TAG v2.6.1 88 | GIT_PROGRESS ON 89 | ) 90 | 91 | FetchContent_GetProperties(pybind11) 92 | if(NOT pybind11_POPULATED) 93 | FetchContent_Populate(pybind11) 94 | set(PYBIND11_INSTALL 95 | ON 96 | CACHE BOOL "Install pybind11 header files" FORCE 97 | ) 98 | add_subdirectory(${pybind11_SOURCE_DIR} ${pybind11_BINARY_DIR}) 99 | endif() 100 | else() 101 | message(CHECK_PASS "system library found.") 102 | endif() 103 | endif() 104 | 105 | # ############################################################################## 106 | # Testing Dependencies 107 | # ############################################################################## 108 | 109 | if(ENABLE_TESTING) 110 | message(CHECK_START "Looking for catchorg/Catch2") 111 | find_package(Catch2 QUIET) 112 | if(NOT Catch2_FOUND) 113 | message(CHECK_FAIL "system library not found (using fetched version).") 114 | FetchContent_Declare( 115 | catch2 116 | GIT_REPOSITORY https://github.com/catchorg/Catch2.git 117 | GIT_TAG v2.13.4 118 | GIT_PROGRESS ON 119 | ) 120 | 121 | FetchContent_GetProperties(catch2) 122 | if(NOT catch2_POPULATED) 123 | FetchContent_Populate(catch2) 124 | cmake_print_variables(catch2_SOURCE_DIR) 125 | add_subdirectory(${catch2_SOURCE_DIR} ${catch2_BINARY_DIR}) 126 | include(${catch2_SOURCE_DIR}/contrib/Catch.cmake) 127 | endif() 128 | else() 129 | message(CHECK_PASS "system library found.") 130 | endif() 131 | endif() 132 | -------------------------------------------------------------------------------- /cmake/Version.cmake: -------------------------------------------------------------------------------- 1 | set(SIGNALTL_VERSION_MAJOR 0) 2 | set(SIGNALTL_VERSION_MINOR 1) 3 | set(SIGNALTL_VERSION_PATCH 4) 4 | set(SIGNALTL_VERSION 5 | "${SIGNALTL_VERSION_MAJOR}.${SIGNALTL_VERSION_MINOR}.${SIGNALTL_VERSION_PATCH}" 6 | CACHE STRING "signaltl library version" 7 | ) 8 | 9 | set(SIGNALTL_FULL_VERSION 10 | "" 11 | CACHE STRING "signaltl library version (with pre-release metadata)" 12 | ) 13 | 14 | message(CHECK_START "Finding working git") 15 | find_program(GIT_EXE git REQUIRED) 16 | if(NOT GIT_EXE) 17 | message(CHECK_FAIL "not found") 18 | return() 19 | endif() 20 | 21 | message(CHECK_PASS "found ${GIT_EXE}") 22 | execute_process( 23 | COMMAND ${GIT_EXE} -C ${CMAKE_CURRENT_SOURCE_DIR} describe --match *.*.* 24 | --tags 25 | RESULT_VARIABLE EXIT_STATUS 26 | OUTPUT_VARIABLE GIT_DESCRIBE 27 | OUTPUT_STRIP_TRAILING_WHITESPACE 28 | ) 29 | if(EXIT_STATUS EQUAL "0") 30 | message(STATUS "Attempting to parse for tagged release version.") 31 | if(GIT_DESCRIBE MATCHES "^v?([0-9]+\\.[0-9]+\\.[0-9]+)$") 32 | # Tagged release version. 33 | set(GIT_TAG ${CMAKE_MATCH_1}) 34 | if(NOT GIT_TAG VERSION_EQUAL SIGNALTL_VERSION) 35 | message( 36 | SEND_ERROR 37 | "signaltl version (${SIGNALTL_VERSION}) does not match Git tag (${GIT_TAG})." 38 | ) 39 | endif() 40 | elseif(GIT_DESCRIBE MATCHES 41 | "^v?([0-9]+\\.[0-9]+\\.[0-9]+)(-[a-z0-9]+)?-([0-9]+)-g(.+)$" 42 | ) 43 | # Untagged pre-release. The Signaltl version is updated to include the 44 | # number of commits since the last tagged version and the commit hash. The 45 | # version is formatted in accordance with the https://semver.org 46 | # specification. 47 | set(GIT_TAG ${CMAKE_MATCH_1}) 48 | set(GIT_COMMITS_AFTER_TAG ${CMAKE_MATCH_3}) 49 | set(GIT_COMMIT ${CMAKE_MATCH_4}) 50 | if(NOT SIGNALTL_VERSION VERSION_GREATER GIT_TAG) 51 | message( 52 | SEND_ERROR 53 | "Signaltl version (${SIGNALTL_VERSION}) must be greater than tagged ancestor (${GIT_TAG})." 54 | ) 55 | endif() 56 | set(SIGNALTL_FULL_VERSION 57 | "${SIGNALTL_VERSION}-dev${GIT_COMMITS_AFTER_TAG}+${GIT_COMMIT}" 58 | CACHE STRING "signaltl library version (with pre-release metadata)" 59 | FORCE 60 | ) 61 | else() 62 | message(SEND_ERROR "Failed to parse version from output of `git describe`.") 63 | endif() 64 | endif() 65 | 66 | -------------------------------------------------------------------------------- /cmake/launch-c.in: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Xcode generator doesn't include the compiler as the 4 | # first argument, Ninja and Makefiles do. Handle both cases. 5 | if [ "$1" = "${CMAKE_C_COMPILER}" ] ; then 6 | shift 7 | fi 8 | 9 | exec "${C_LAUNCHER}" "${CMAKE_C_COMPILER}" "$@" 10 | -------------------------------------------------------------------------------- /cmake/launch-cxx.in: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Xcode generator doesn't include the compiler as the 4 | # first argument, Ninja and Makefiles do. Handle both cases. 5 | if [ "$1" = "${CMAKE_CXX_COMPILER}" ] ; then 6 | shift 7 | fi 8 | 9 | ${CCCACHE_EXPORTS} 10 | 11 | exec "${CXX_LAUNCHER}" "${CMAKE_CXX_COMPILER}" "$@" 12 | -------------------------------------------------------------------------------- /cmake/signaltlConfig.cmake.in: -------------------------------------------------------------------------------- 1 | @PACKAGE_INIT@ 2 | 3 | include("${CMAKE_CURRENT_LIST_DIR}/signaltlTargets.cmake") 4 | 5 | set_and_check( 6 | signaltl_INCLUDE_DIR "${PACKAGE_PREFIX_DIR}/@CMAKE_INSTALL_INCLUDEDIR@" 7 | ) 8 | set_and_check(signaltl_LIB_DIR "${PACKAGE_PREFIX_DIR}/@CMAKE_INSTALL_LIBDIR@") 9 | set(signaltl_LIBRARIES signaltl::signalt:) 10 | set(signaltl_VERSION_TYPE "@signaltl_VERSION_TYPE@") 11 | 12 | if(NOT signaltl_FIND_QUIETLY) 13 | message( 14 | STATUS 15 | "Found signaltl: ${signaltl_INCLUDE_DIR} (found version \"${signaltl_VERSION}\" ${signaltl_VERSION_TYPE})" 16 | ) 17 | endif() 18 | -------------------------------------------------------------------------------- /cmake/test_std_filesystem.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #if __cplusplus >= 201703L 4 | 5 | #if __has_include() 6 | #include 7 | namespace fs = std::filesystem; 8 | #elif __has_include() 9 | #include 10 | namespace fs = std::experimental::filesystem; 11 | #else 12 | #error "Missing the and headers." 13 | #endif 14 | 15 | #else 16 | #error "Need to compile with C++17 support" 17 | #endif 18 | 19 | int main() { 20 | try { 21 | fs::path path("/root"); 22 | (void)path; 23 | } catch (const fs::filesystem_error& e) { 24 | std::cerr << "filesystem_error workaround: " << e.what() << std::endl; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "tests/*" 3 | - "examples/*" 4 | - "python_bindings/*" 5 | -------------------------------------------------------------------------------- /docs/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | if(NOT BUILD_DOCS) 2 | return() 3 | endif() 4 | 5 | find_package(Doxygen REQUIRED) 6 | 7 | set(DOXYGEN_PROJECT_NAME "Signal Temporal Logic") 8 | set(DOXYGEN_PROJECT_NUMBER "${SIGNALTL_FULL_VERSION}") 9 | set(DOXYGEN_INPUT_DIR ${PROJECT_SOURCE_DIR}/src) 10 | set(DOXYGEN_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) 11 | 12 | set(DOXYGEN_QUIET YES) 13 | # set(DOXYGEN_EXTRACT_ALL YES) 14 | set(DOXYGEN_EXCLUDE_PATTERNS "*/internal/*" "fmt.hpp") 15 | 16 | set(DOXYGEN_GENERATE_XML YES) 17 | set(DOXYGEN_GENERATE_MAN YES) 18 | set(DOXYGEN_GENERATE_HTML YES) 19 | 20 | doxygen_add_docs( 21 | cpp_docs ${DOXYGEN_INPUT_DIR} COMMENT "Generate docs for C++" 22 | ) 23 | 24 | set(DOXYGEN_INDEX_FILE ${DOXYGEN_OUTPUT_DIRECTORY}/xml/index.xml) 25 | 26 | find_package(Sphinx REQUIRED) 27 | set(SPHINX_SOURCE ${CMAKE_CURRENT_SOURCE_DIR}) 28 | set(SPHINX_BUILD ${CMAKE_CURRENT_BINARY_DIR}/sphinx) 29 | set(SPHINX_INDEX_FILE ${SPHINX_BUILD}/index.html) 30 | file(GLOB_RECURSE SPHINX_RST_SOURCES "*.rst") 31 | 32 | file(GLOB_RECURSE SPHINX_MD_SOURCES "*.md") 33 | 34 | set(SPHINX_DEPENDS 35 | ${PROJECT_SOURCE_DIR}/README.md ${DOXYGEN_INDEX_FILE} ${SPHINX_RST_SOURCES} 36 | ${SPHINX_MD_SOURCES} ${SPHINX_SOURCE}/conf.py 37 | ) 38 | 39 | # Only regenerate Sphinx when: 40 | # 41 | # - Doxygen has rerun 42 | # - Our doc files have been updated 43 | # - The Sphinx config has been updated 44 | add_custom_target( 45 | sphinx_docs 46 | BYPRODUCTS ${SPHINX_INDEX_FILE} 47 | COMMAND 48 | ${SPHINX_EXECUTABLE} -b html 49 | # Tell Breathe where to find the Doxygen output 50 | -Dbreathe_projects.signal_tl=${DOXYGEN_OUTPUT_DIRECTORY}/xml ${SPHINX_SOURCE} 51 | ${SPHINX_BUILD} 52 | WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} 53 | DEPENDS ${SPHINX_DEPENDS} 54 | COMMENT "Generating documentation with Sphinx" 55 | ALL 56 | ) 57 | add_dependencies(sphinx_docs cpp_docs) 58 | 59 | # Nice named target so we can run the job easily 60 | add_custom_target(docs ALL) 61 | add_dependencies(docs sphinx_docs cpp_docs) 62 | 63 | # Add an install target to install the docs 64 | include(GNUInstallDirs) 65 | install(DIRECTORY ${SPHINX_BUILD} DESTINATION ${CMAKE_INSTALL_DOCDIR}) 66 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | 16 | sys.path.insert(0, os.path.abspath("..")) 17 | 18 | 19 | # -- Project information ----------------------------------------------------- 20 | 21 | project = "Signal Temporal Logic" 22 | copyright = "2021, Anand Balakrishnan" 23 | author = "Anand Balakrishnan" 24 | 25 | 26 | # -- General configuration --------------------------------------------------- 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be 29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 30 | # ones. 31 | extensions = [ 32 | "breathe", 33 | "recommonmark", 34 | "sphinx.ext.autodoc", 35 | "sphinx.ext.intersphinx", 36 | "sphinx.ext.viewcode", 37 | ] 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ["_templates"] 41 | 42 | # List of patterns, relative to source directory, that match files and 43 | # directories to ignore when looking for source files. 44 | # This pattern also affects html_static_path and html_extra_path. 45 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 46 | 47 | 48 | html_theme = "sphinx_book_theme" 49 | 50 | breathe_default_project = "signal_tl" 51 | 52 | source_suffix = { 53 | ".rst": "restructuredtext", 54 | ".txt": "markdown", 55 | ".md": "markdown", 56 | } 57 | -------------------------------------------------------------------------------- /docs/cpp_lib.rst: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anand-bala/signal-temporal-logic/9efcb838336d73565d1710456894f52687944349/docs/cpp_lib.rst -------------------------------------------------------------------------------- /docs/cpp_lib/core.rst: -------------------------------------------------------------------------------- 1 | Core STL Expressions and Signals 2 | ================================ 3 | 4 | Reference 5 | --------- 6 | 7 | .. doxygennamespace:: signal_tl::ast 8 | 9 | .. doxygennamespace:: signal_tl::signal 10 | -------------------------------------------------------------------------------- /docs/cpp_lib/parsing.rst: -------------------------------------------------------------------------------- 1 | Signal Temporal Logic Specification File Parsing 2 | ================================================ 3 | 4 | Reference 5 | --------- 6 | 7 | .. doxygenfile:: parser.hpp 8 | -------------------------------------------------------------------------------- /docs/cpp_lib/robustness.rst: -------------------------------------------------------------------------------- 1 | Signal Temporal Logic Semantics 2 | =============================== 3 | 4 | Reference 5 | --------- 6 | 7 | .. doxygennamespace:: signal_tl::semantics 8 | -------------------------------------------------------------------------------- /docs/cpp_lib/signal_tl.rst: -------------------------------------------------------------------------------- 1 | Signal Temporal Logic (C++ API) 2 | =============================== 3 | 4 | Modules 5 | ------- 6 | 7 | .. toctree:: 8 | :maxdepth: 3 9 | 10 | core 11 | robustness 12 | parsing 13 | 14 | 15 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to Signal Temporal Logic's documentation! 2 | ================================================= 3 | 4 | .. note:: 5 | TODO: Add general introduction on how to use the library and the executable. 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | :caption: Contents: 10 | 11 | cpp_lib/signal_tl 12 | 13 | -------------------------------------------------------------------------------- /examples/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | message(STATUS "Building Examples in ${CMAKE_CURRENT_LIST_DIR}") 2 | 3 | unset(CMAKE_CXX_CLANG_TIDY) 4 | unset(CMAKE_CXX_INCLUDE_WHAT_YOU_USE) 5 | 6 | add_custom_target(examples) 7 | 8 | function(add_example TARGET) 9 | add_executable(${TARGET} ${ARGN}) 10 | target_link_libraries(${TARGET} PUBLIC signaltl::signaltl) 11 | set_default_compile_options(${TARGET}) 12 | add_coverage_flags(${TARGET}) 13 | add_dependencies(examples ${TARGET}) 14 | endfunction() 15 | 16 | add_example(basic_formulas ${CMAKE_CURRENT_LIST_DIR}/basic_formulas.cc) 17 | add_example(basic_signal ${CMAKE_CURRENT_LIST_DIR}/basic_signal.cc) 18 | add_example(basic_robustness ${CMAKE_CURRENT_LIST_DIR}/basic_robustness.cc) 19 | 20 | if(BUILD_PARSER) 21 | add_example(basic_parsing basic_parsing.cc) 22 | target_link_libraries(basic_parsing PRIVATE taocpp::pegtl) 23 | endif() 24 | -------------------------------------------------------------------------------- /examples/basic_formulas.cc: -------------------------------------------------------------------------------- 1 | #include "signal_tl/ast.hpp" // for Predicate, signal_tl 2 | #include "signal_tl/fmt.hpp" // IWYU pragma: keep 3 | 4 | #include // for print 5 | #include // for allocator 6 | 7 | namespace stl = signal_tl; 8 | 9 | int main() { 10 | fmt::print("phi := {}\n", stl::ast::Predicate("a")); 11 | 12 | fmt::print("----------------\n"); 13 | 14 | fmt::print("phi := {}\n", stl::Predicate("a") & stl::Predicate("b")); 15 | 16 | return 0; 17 | } 18 | -------------------------------------------------------------------------------- /examples/basic_parsing.cc: -------------------------------------------------------------------------------- 1 | #include "signal_tl/fmt.hpp" 2 | #include "signal_tl/internal/filesystem.hpp" 3 | #include "signal_tl/parser.hpp" 4 | #include "signal_tl/signal_tl.hpp" 5 | 6 | #include 7 | 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | namespace pegtl = tao::pegtl; 16 | 17 | int main(int argc, char* argv[]) { 18 | if (argc != 2) { 19 | std::cerr << "Invalid number of arguments. Need input file" << std::endl; 20 | return 1; 21 | } 22 | 23 | pegtl::file_input in(argv[1]); 24 | auto input_path = stdfs::path(argv[1]); 25 | try { 26 | // signal_tl::grammar::internal::trace_from_file(input_path); 27 | auto spec = signal_tl::parser::from_file(input_path); 28 | std::cout << "Parsed formulas are:" << std::endl; 29 | for (const auto& [expr_id, expr] : spec->formulas) { 30 | fmt::print(std::cout, "\t{} := {}\n", expr_id, expr); 31 | } 32 | } catch (const pegtl::parse_error& e) { 33 | const auto p = e.positions().front(); 34 | std::cerr << e.what() << '\n' 35 | << in.line_at(p) << '\n' 36 | << std::setw(static_cast(p.column)) << '^' << std::endl; 37 | return 1; 38 | } 39 | 40 | return 0; 41 | } 42 | -------------------------------------------------------------------------------- /examples/basic_robustness.cc: -------------------------------------------------------------------------------- 1 | #include "signal_tl/signal_tl.hpp" // for Predicate, Signal, compute_robust... 2 | 3 | #include "signal_tl/fmt.hpp" // IWYU pragma: keep 4 | #include // for print 5 | 6 | #include // for make_shared, __shared_ptr_access 7 | #include // for visit 8 | #include // for vector 9 | 10 | namespace stl = signal_tl; 11 | using namespace signal_tl::signal; 12 | 13 | int main() { 14 | const auto phi = stl::Predicate("a") | stl::Predicate("b"); 15 | 16 | fmt::print("phi := {}\n", phi); 17 | 18 | auto xs = std::make_shared( 19 | std::vector{0, 2, 1, -2, -1}, std::vector{0, 2.5, 4.5, 6.5, 9}); 20 | auto ys = std::make_shared( 21 | std::vector{0, -2, 2, 1, -1.5}, std::vector{0, 2, 6, 8.5, 11}); 22 | fmt::print("xs:\t{}\n", *xs); 23 | fmt::print("ys:\t{}\n", *ys); 24 | 25 | { 26 | const auto trace1 = Trace{{"a", xs}, {"b", ys}}; 27 | const auto rob1 = stl::compute_robustness(phi, trace1); 28 | fmt::print("unsynched robustness:\t{}\n", *rob1); 29 | } 30 | { 31 | const auto [xs_, ys_] = synchronize(xs, ys); 32 | const auto trace1 = Trace{{"a", xs_}, {"b", ys_}}; 33 | const auto rob1 = stl::compute_robustness(phi, trace1, true); 34 | 35 | fmt::print("synched robustness:\t{}\n", *rob1); 36 | } 37 | 38 | return 0; 39 | } 40 | -------------------------------------------------------------------------------- /examples/basic_robustness.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import signal_tl as stl 4 | 5 | a = stl.Predicate("a") > 0 6 | b = stl.Predicate("b") <= 0.5 7 | 8 | phi = stl.Until(a, b) 9 | 10 | t = np.linspace(0, 50, 201) 11 | x = np.cos(t) 12 | y = np.sin(t) 13 | 14 | trace = { 15 | "a": stl.Signal(x, t), 16 | "b": stl.Signal(x, t), 17 | } 18 | 19 | rob = stl.compute_robustness(phi, trace) 20 | print(rob.at(0)) 21 | -------------------------------------------------------------------------------- /examples/basic_signal.cc: -------------------------------------------------------------------------------- 1 | #include // for transform 2 | #include // for back_insert_iterator, back_inserter 3 | #include // for __shared_ptr_access, make_shared 4 | #include // for tuple_element<>::type 5 | #include // for vector 6 | 7 | #include // for print, join 8 | 9 | #include "signal_tl/fmt.hpp" // IWYU pragma: keep 10 | #include "signal_tl/signal.hpp" // for Signal, Sample, synchronize, signal 11 | 12 | using namespace signal_tl::signal; 13 | 14 | int main() { 15 | fmt::print("\n===== Basic Signals =====\n"); 16 | 17 | auto xs = std::make_shared( 18 | std::vector{0, 2, 1, -2, -1}, std::vector{0, 2.5, 4.5, 6.5, 9}); 19 | auto ys = std::make_shared( 20 | std::vector{0, -2, 2, 1, -1.5}, std::vector{0, 2, 6, 8.5, 11}); 21 | fmt::print("xs:\t{}\n", *xs); 22 | fmt::print("ys:\t{}\n", *ys); 23 | 24 | { 25 | fmt::print("\n===== Unsynched timestamps =====\n"); 26 | auto xs_t = std::vector{}; 27 | std::transform( 28 | xs->begin(), xs->end(), std::back_inserter(xs_t), [](const Sample& s) { 29 | return s.time; 30 | }); 31 | auto ys_t = std::vector{}; 32 | std::transform( 33 | ys->begin(), ys->end(), std::back_inserter(ys_t), [](const Sample& s) { 34 | return s.time; 35 | }); 36 | 37 | fmt::print("xs time:\t{}\n", fmt::join(xs_t, ", ")); 38 | fmt::print("ys time:\t{}\n", fmt::join(ys_t, ", ")); 39 | } 40 | 41 | { 42 | fmt::print("\n===== Synched Signals =====\n"); 43 | const auto [xs_, ys_] = synchronize(xs, ys); 44 | fmt::print("xs sync:\t{}\n", *xs_); 45 | fmt::print("ys sync:\t{}\n", *ys_); 46 | 47 | fmt::print("\n===== Synched timestamps =====\n"); 48 | auto xs_t = std::vector{}; 49 | std::transform( 50 | xs_->begin(), xs_->end(), std::back_inserter(xs_t), [](const Sample& s) { 51 | return s.time; 52 | }); 53 | auto ys_t = std::vector{}; 54 | std::transform( 55 | ys_->begin(), ys_->end(), std::back_inserter(ys_t), [](const Sample& s) { 56 | return s.time; 57 | }); 58 | 59 | fmt::print("xs sync time:\t{}\n", fmt::join(xs_t, ", ")); 60 | fmt::print("ys sync time:\t{}\n", fmt::join(ys_t, ", ")); 61 | } 62 | return 0; 63 | } 64 | -------------------------------------------------------------------------------- /examples/basic_signal.py: -------------------------------------------------------------------------------- 1 | import signal_tl as stl 2 | 3 | # Signal(sample_values, sample_times) 4 | xs = stl.Signal([0, 2, 1, -2, -1], [0, 2.5, 4.5, 6.5, 9]) 5 | ys = stl.Signal([0, -2, 2, 1, -1.5], [0, 2, 6, 8.5, 11]) 6 | 7 | # Synchronizing signals 8 | xs_, ys_ = stl.synchronize(xs, ys) 9 | 10 | print("x: {}".format(list(xs))) 11 | print("y: {}".format(list(ys))) 12 | print("\nSyncronizing the times") 13 | print("xs_.time: {}".format(list(map(lambda s: s.time, xs_)))) 14 | print("ys_.time: {}".format(list(map(lambda s: s.time, ys_)))) 15 | 16 | print("\nSynchronized signals (With piecewise linear interpolation)") 17 | print("xs_: {}".format(list(xs_))) 18 | print("ys_: {}".format(list(ys_))) 19 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=47.0.0", "wheel", "cmake>=3.11.0"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /python_bindings/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | if(NOT BUILD_PYTHON_BINDINGS) 2 | return() 3 | endif() 4 | 5 | # We will setup __init__.py with the full version string (PEP440 compatible). 6 | configure_file(init.py.in ${PROJECT_SOURCE_DIR}/signal_tl/__init__.py @ONLY) 7 | 8 | set(BINDINGS_SOURCES pyast.cc pyrobustness.cc pysignal.cc pysignal_tl.cc) 9 | 10 | pybind11_add_module(_cext MODULE ${BINDINGS_SOURCES}) 11 | target_include_directories(_cext PRIVATE ${CMAKE_CURRENT_LISTS_DIR}) 12 | target_link_libraries(_cext PUBLIC signaltl::signaltl) 13 | add_coverage_flags(_cext) 14 | -------------------------------------------------------------------------------- /python_bindings/bindings.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef SIGNAL_TL_PYTHON_BINDINGS_HPP 4 | #define SIGNAL_TL_PYTHON_BINDINGS_HPP 5 | 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | #include 15 | 16 | using namespace pybind11::literals; 17 | namespace py = pybind11; 18 | 19 | void init_ast_module(py::module&); 20 | void init_signal_module(py::module&); 21 | void init_robustness_module(py::module&); 22 | 23 | #endif 24 | -------------------------------------------------------------------------------- /python_bindings/init.py.in: -------------------------------------------------------------------------------- 1 | __version__ = "@SIGNALTL_FULL_VERSION@" 2 | 3 | # isort: split 4 | 5 | from signal_tl._cext import (Always, And, Const, Eventually, Not, Or, 6 | Predicate, Until) 7 | from signal_tl._cext.semantics import compute_robustness 8 | from signal_tl._cext.signal import Sample, Signal, Trace, synchronize 9 | 10 | F = Eventually 11 | G = Always 12 | U = Until 13 | 14 | TOP = Const(True) 15 | BOT = Const(False) 16 | -------------------------------------------------------------------------------- /python_bindings/pyast.cc: -------------------------------------------------------------------------------- 1 | #include "bindings.hpp" // for init_ast_module 2 | #include "signal_tl/ast.hpp" // for Expr, Until, Const, Always, Even... 3 | #include "signal_tl/fmt.hpp" // IWYU pragma: keep 4 | 5 | #include // for format 6 | 7 | #include // for operator""_a, arg 8 | #include // for constexpr_first, ignore_unused 9 | #include // for operator+ 10 | #include // for self, self_t, operator<, operator<= 11 | #include // for class_, init, module, module_ 12 | #include // for getattr, reinterpret_steal, sequ... 13 | 14 | #include // for array 15 | #include // for __shared_ptr_access 16 | #include // for nullopt, optional 17 | #include // for basic_string, string 18 | #include // for pair 19 | #include // for visit 20 | #include // for vector 21 | 22 | using namespace signal_tl; 23 | namespace { 24 | 25 | template 26 | ast::Expr and_op(const T lhs, const ast::Expr& rhs) { 27 | return ast::Expr{lhs} & rhs; 28 | } 29 | 30 | template 31 | ast::Expr or_op(const T& lhs, const ast::Expr& rhs) { 32 | return ast::Expr{lhs} | rhs; 33 | } 34 | 35 | template 36 | ast::Expr not_op(const T& expr) { 37 | return ~ast::Expr{expr}; 38 | } 39 | 40 | } // namespace 41 | 42 | void init_ast_module(py::module& parent) { 43 | parent.def("Const", &Const, "value"_a); 44 | parent.def("Predicate", &Predicate, "name"_a); 45 | parent.def("Not", &Not, "arg"_a); 46 | parent.def("And", &And, "args"_a); 47 | parent.def("Or", &Or, "args"_a); 48 | parent.def("Always", &Always, "arg"_a, "interval"_a = std::nullopt); 49 | parent.def("Eventually", &Eventually, "arg"_a, "interval"_a = std::nullopt); 50 | parent.def("Until", &Until, "arg0"_a, "arg1"_a, "interval"_a = std::nullopt); 51 | 52 | auto m = parent.def_submodule("ast", "Define the AST nodes for STL"); 53 | py::class_(m, "Const") 54 | .def(py::init(), "value"_a) 55 | .def_readonly("value", &ast::Const::value) 56 | .def("__and__", &and_op) 57 | .def("__or__", &or_op) 58 | .def("__invert__", ¬_op) 59 | .def("__repr__", [](const ast::Const& e) { return fmt::format("{}", e); }); 60 | 61 | py::class_(m, "Predicate") 62 | .def(py::init(), "name"_a) 63 | .def_readonly("name", &ast::Predicate::name) 64 | .def("__and__", &and_op) 65 | .def("__or__", &or_op) 66 | .def("__invert__", ¬_op) 67 | .def(py::self < double()) 68 | .def(py::self <= double()) 69 | .def(py::self > double()) 70 | .def(py::self >= double()) 71 | .def("__repr__", [](const ast::Predicate& e) { return fmt::format("{}", e); }); 72 | 73 | py::class_(m, "Not") 74 | .def(py::init(), "arg"_a) 75 | .def_readonly("arg", &ast::Not::arg) 76 | .def("__and__", &and_op) 77 | .def("__or__", &or_op) 78 | .def("__invert__", ¬_op) 79 | .def("__repr__", [](const ast::NotPtr& e) { return fmt::format("{}", *e); }); 80 | 81 | py::class_(m, "And") 82 | .def(py::init&>(), "args"_a) 83 | .def_readonly("args", &ast::And::args) 84 | .def("__and__", &and_op) 85 | .def("__or__", &or_op) 86 | .def("__invert__", ¬_op) 87 | .def("__repr__", [](const ast::AndPtr& e) { return fmt::format("{}", *e); }); 88 | 89 | py::class_(m, "Or") 90 | .def(py::init&>(), "args"_a) 91 | .def_readonly("args", &ast::Or::args) 92 | .def("__and__", &and_op) 93 | .def("__or__", &or_op) 94 | .def("__invert__", ¬_op) 95 | .def("__repr__", [](const ast::Or& e) { return fmt::format("{}", e); }); 96 | 97 | py::class_(m, "Eventually") 98 | .def( 99 | py::init>(), 100 | "arg"_a, 101 | "interval"_a = std::nullopt) 102 | .def_readonly("arg", &ast::Eventually::arg) 103 | .def_readonly("interval", &ast::Eventually::arg) 104 | .def("__and__", &and_op) 105 | .def("__or__", &or_op) 106 | .def("__invert__", ¬_op) 107 | .def("__repr__", [](const ast::Eventually& e) { return fmt::format("{}", e); }); 108 | 109 | py::class_(m, "Always") 110 | .def( 111 | py::init>(), 112 | "arg"_a, 113 | "interval"_a = std::nullopt) 114 | .def_readonly("arg", &ast::Always::arg) 115 | .def_readonly("interval", &ast::Always::arg) 116 | .def("__and__", &and_op) 117 | .def("__or__", &or_op) 118 | .def("__invert__", ¬_op) 119 | .def("__repr__", [](const ast::Always& e) { return fmt::format("{}", e); }); 120 | 121 | py::class_(m, "Until") 122 | .def( 123 | py::init>(), 124 | "arg0"_a, 125 | "arg1"_a, 126 | "interval"_a = std::nullopt) 127 | .def_readonly("args", &ast::Until::args) 128 | .def_readonly("interval", &ast::Until::args) 129 | .def("__and__", &and_op) 130 | .def("__or__", &or_op) 131 | .def("__invert__", ¬_op) 132 | .def("__repr__", [](const ast::Until& e) { return fmt::format("{}", e); }); 133 | } 134 | -------------------------------------------------------------------------------- /python_bindings/pyrobustness.cc: -------------------------------------------------------------------------------- 1 | #include "bindings.hpp" // for init_robustness_module 2 | #include "signal_tl/ast.hpp" // for Expr, signal_tl 3 | #include "signal_tl/robustness.hpp" // for compute_robustness, semantics 4 | #include "signal_tl/signal.hpp" // for Trace, signal 5 | 6 | #include "signal_tl/fmt.hpp" // IWYU pragma: keep 7 | 8 | #include // for operator""_a, arg 9 | #include // for constexpr_first, ignore_unused 10 | #include // for operator+ 11 | #include // for module, module_ 12 | #include // for dict 13 | 14 | #include // for shared_ptr 15 | #include // for vector 16 | 17 | using namespace signal_tl; 18 | using namespace semantics; 19 | using namespace signal; 20 | 21 | void init_robustness_module(py::module& parent) { 22 | auto m = parent.def_submodule("semantics", "Robustness semantics for STL"); 23 | 24 | m.def( 25 | "compute_robustness", 26 | [](const ast::Expr& phi, const Trace& trace, bool synchronized) { 27 | return compute_robustness(phi, trace, synchronized); 28 | }, 29 | "phi"_a, 30 | "trace"_a, 31 | "synchronized"_a = false); 32 | } 33 | -------------------------------------------------------------------------------- /python_bindings/pysignal.cc: -------------------------------------------------------------------------------- 1 | #include "bindings.hpp" // for init_signal_module 2 | #include "signal_tl/ast.hpp" // for signal_tl 3 | #include "signal_tl/fmt.hpp" // IWYU pragma: keep 4 | #include "signal_tl/signal.hpp" // for Sample, Trace, Signal, SignalPtr 5 | 6 | #include // for array 7 | #include // for size_t 8 | #include // for exception 9 | #include // for format 10 | #include // for operator==, map, operator!= 11 | #include // for allocator, operator<<, __shared_... 12 | #include // for buffer_protocol, keep_alive 13 | #include // for operator""_a, handle::cast, cast_op 14 | #include // for ignore_unused, constexpr_first 15 | #include // for operator+ 16 | #include // for self, self_t, operator<, operator<= 17 | #include // for class_, init, make_iterator, mod... 18 | #include // for getattr, iterable, sequence, dict 19 | #include // for bind_vector, bind_map 20 | #include // for basic_string 21 | #include // for vector 22 | 23 | using namespace signal_tl; 24 | 25 | void init_signal_module(py::module& parent) { 26 | using namespace signal; 27 | 28 | auto m = parent.def_submodule("signal", "A general class of signals (PWL, etc.)"); 29 | py::bind_vector>(m, "DoubleList", py::buffer_protocol()); 30 | py::bind_vector>(m, "SampleList"); 31 | py::bind_map(m, "Trace").def(py::init(), "other"_a); 32 | 33 | py::class_(m, "Sample") 34 | .def(py::init()) 35 | .def_readonly("time", &Sample::time) 36 | .def_readonly("value", &Sample::value) 37 | .def_readonly("derivative", &Sample::value) 38 | .def(py::self < py::self) 39 | .def(py::self > py::self) 40 | .def(py::self >= py::self) 41 | .def(py::self <= py::self) 42 | .def("__repr__", [](const Sample& e) { return fmt::format("{}", e); }); 43 | 44 | py::class_>(m, "Signal") 45 | .def(py::init<>()) 46 | .def(py::init(), "other"_a) 47 | .def(py::init&>()) 48 | .def( 49 | py::init&, const std::vector&>(), 50 | "points"_a, 51 | "times"_a) 52 | .def_property_readonly("begin_time", &Signal::begin_time) 53 | .def_property_readonly("end_time", &Signal::end_time) 54 | .def("simplify", &Signal::simplify) 55 | .def("resize", &Signal::resize, "start"_a, "end"_a, "fill"_a) 56 | .def("shift", &Signal::shift, "dt"_a) 57 | .def("__repr__", [](const Signal& e) { return fmt::format("{}", e); }) 58 | .def( 59 | "__iter__", 60 | [](const Signal& s) { return py::make_iterator(s.begin(), s.end()); }, 61 | py::keep_alive<0, 1>()) 62 | .def( 63 | "__getitem__", 64 | [](const SignalPtr& s, size_t i) { return s->at_idx(i).value; }) 65 | .def("__len__", &Signal::size) 66 | .def("at", [](const SignalPtr& s, double t) { return s->at(t).value; }); 67 | 68 | m.def("synchronize", &synchronize, "x"_a, "y"_a); 69 | } 70 | -------------------------------------------------------------------------------- /python_bindings/pysignal_tl.cc: -------------------------------------------------------------------------------- 1 | #include "bindings.hpp" 2 | #include "signal_tl/exception.hpp" 3 | 4 | #include // for PYBIND11_MODULE 5 | #include // for module_, register_exception 6 | #include // for str_attr_accessor 7 | #include // for PyErr_SetString, PyExc_NotImplem... 8 | 9 | PYBIND11_MODULE(_cext, m) { // NOLINT 10 | py::register_exception( 11 | m, "FunctionNotImplemented", PyExc_NotImplementedError); 12 | 13 | m.doc() = "Signal Temporal Logic library."; 14 | init_ast_module(m); 15 | init_signal_module(m); 16 | init_robustness_module(m); 17 | } 18 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | breathe~=4.26.1 2 | myst-parser~=0.13.3 3 | sphinx-book-theme 4 | -------------------------------------------------------------------------------- /scripts/run_coverage.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P) 4 | project_dir=$(readlink -f $script_dir/..) 5 | 6 | setup_colors() { 7 | if [[ -t 2 ]] && [[ -z "${NO_COLOR-}" ]] && [[ "${TERM-}" != "dumb" ]]; then 8 | NOFORMAT='\033[0m' RED='\033[0;31m' GREEN='\033[0;32m' ORANGE='\033[0;33m' BLUE='\033[0;34m' PURPLE='\033[0;35m' CYAN='\033[0;36m' YELLOW='\033[1;33m' 9 | else 10 | NOFORMAT='' RED='' GREEN='' ORANGE='' BLUE='' PURPLE='' CYAN='' YELLOW='' 11 | fi 12 | } 13 | 14 | msg() { 15 | echo >&2 -e "${1-}" 16 | } 17 | 18 | relpath(){ 19 | python3 -c "import os.path; print(os.path.relpath('$1','${2:-$PWD}'))" 20 | } 21 | 22 | setup_colors 23 | 24 | cd $project_dir 25 | coverage_file="${project_dir}/coverage.info" 26 | proj_relpath=$(relpath $project_dir) 27 | src_relpath=$(relpath $project_dir/src) 28 | 29 | msg "${GREEN}Capturing lcov data and saving to:${NOFORMAT} ${coverage_file}" 30 | lcov --capture --directory . --output-file $coverage_file 31 | msg "-- ${YELLOW}Extracting coverage information for directory:${NOFORMAT} ${src_relpath}" 32 | lcov --extract $coverage_file "${project_dir}/src/*" --output-file $coverage_file 33 | lcov --list $coverage_file 34 | 35 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = signal-temporal-logic 3 | version = attr:signal_tl.__version__ 4 | description = A library for efficiently working with Signal Temporal Logic (STL) and its quantitative semantics. 5 | long_description = file: README.md 6 | long_description_content_type = text/markdown 7 | license_files = LICENSE, LICENSE-3rdParty 8 | 9 | author = Anand Balakrishnan 10 | author_email = anandbala1597@gmail.com 11 | url = https://github.com/anand-bala/signal-temporal-logic 12 | 13 | classifiers = 14 | Programming Language :: Python :: 3 :: Only 15 | Programming Language :: Python :: 3.5 16 | Programming Language :: Python :: 3.6 17 | Programming Language :: Python :: 3.7 18 | Programming Language :: Python :: 3.8 19 | Programming Language :: C++ 20 | License :: OSI Approved :: MIT License 21 | Intended Audience :: Science/Research 22 | 23 | [options] 24 | zip_safe = False 25 | packages = find: 26 | python_requires = >= 3.5, <= 3.8 27 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | import shutil 4 | import subprocess 5 | import sys 6 | from distutils.dir_util import copy_tree 7 | from distutils.file_util import copy_file 8 | from distutils.version import LooseVersion 9 | 10 | from setuptools import Extension, setup 11 | from setuptools.command.build_ext import build_ext 12 | 13 | 14 | class CMakeExtension(Extension): 15 | def __init__(self, name, sourcedir=""): 16 | Extension.__init__(self, name, sources=[]) 17 | self.sourcedir = os.path.abspath(sourcedir) 18 | 19 | 20 | class CMakeBuild(build_ext): 21 | def run(self): 22 | try: 23 | out = subprocess.check_output(["cmake", "--version"]) 24 | except OSError: 25 | raise RuntimeError( 26 | "CMake must be installed to build the following extensions: " 27 | + ", ".join(e.name for e in self.extensions) 28 | ) 29 | for ext in self.extensions: 30 | self.build_extension(ext) 31 | if self.inplace: 32 | self.copy_extensions_to_source() 33 | 34 | def build_extension(self, ext): 35 | print("CMake Library Output Directory = {}".format(self.build_lib)) 36 | cmake_args = [ 37 | "-DPYTHON_EXECUTABLE=" + sys.executable, 38 | "-DBUILD_EXAMPLES=OFF", 39 | "-DBUILD_PYTHON_BINDINGS=ON", 40 | "-DENABLE_TESTING=OFF", 41 | "-DENABLE_COVERAGE=OFF", 42 | "-DENABLE_STATIC_ANALYSIS=OFF", 43 | "-DINSTALL_PACKAGE=OFF", 44 | ] 45 | 46 | cfg = "Debug" if self.debug else "Release" 47 | build_args = ["--config", cfg] 48 | 49 | if self.parallel: 50 | if platform.system() == "Windows": 51 | if sys.maxsize > 2 ** 32: 52 | cmake_args += ["-A", "x64"] 53 | build_args += ["--", "/MP{}".format(self.parallel)] 54 | else: 55 | build_args += ["--", "-j{}".format] 56 | 57 | if not os.path.exists(self.build_lib): 58 | os.makedirs(self.build_lib) 59 | subprocess.check_call(["cmake", ext.sourcedir] + cmake_args, cwd=self.build_lib) 60 | subprocess.check_call( 61 | ["cmake", "--build", "."] + build_args, cwd=self.build_lib 62 | ) 63 | 64 | def copy_extensions_to_source(self): 65 | build_py = self.get_finalized_command("build_py") 66 | for ext in self.extensions: 67 | fullname = self.get_ext_fullname(ext.name) 68 | print("Package fullname = {}".format(fullname)) 69 | filename = self.get_ext_filename(fullname) 70 | print("Package filename = {}".format(filename)) 71 | modpath = fullname.split(".") 72 | package = ".".join(modpath[:-1]) 73 | package_dir = build_py.get_package_dir(package) 74 | print("Package directory = {}".format(package_dir)) 75 | dest_dir = os.path.join(package_dir) 76 | src_dir = os.path.join(self.build_lib, "lib") 77 | 78 | # Always copy, even if source is older than destination, to ensure 79 | # that the right extensions for the current Python/platform are 80 | # used. 81 | copy_tree(src_dir, dest_dir, verbose=self.verbose, dry_run=self.dry_run) 82 | if ext._needs_stub: 83 | self.write_stub(package_dir or os.curdir, ext, True) 84 | 85 | 86 | setup( 87 | ext_modules=[CMakeExtension("signal_tl.c_extension")], 88 | cmdclass=dict(build_ext=CMakeBuild), 89 | ) 90 | -------------------------------------------------------------------------------- /signal_tl/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.4-dev55+9d981d4" 2 | 3 | # isort: split 4 | 5 | from signal_tl._cext import (Always, And, Const, Eventually, Not, Or, 6 | Predicate, Until) 7 | from signal_tl._cext.semantics import compute_robustness 8 | from signal_tl._cext.signal import Sample, Signal, Trace, synchronize 9 | 10 | F = Eventually 11 | G = Always 12 | U = Until 13 | 14 | TOP = Const(True) 15 | BOT = Const(False) 16 | -------------------------------------------------------------------------------- /signal_tl/_version.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anand-bala/signal-temporal-logic/9efcb838336d73565d1710456894f52687944349/signal_tl/_version.py -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # The trailing slash is important for the installing part 2 | set(SIGNALTL_INCLUDE_DIRS 3 | ${CMAKE_CURRENT_LIST_DIR}/include/ 4 | CACHE PATH "Path to the signaltl include directory" 5 | ) 6 | 7 | set(SIGNALTL_SRCS core/signal.cc core/ast.cc) 8 | 9 | if(BUILD_PARSER) 10 | list(APPEND SIGNALTL_SRCS parser/error_messages.hpp parser/actions.hpp 11 | parser/parser.cc parser/grammar.hpp 12 | ) 13 | else() 14 | message(STATUS "Not building parser") 15 | endif() 16 | 17 | if(BUILD_ROBUSTNESS) 18 | list(APPEND SIGNALTL_SRCS robust_semantics/classic_robustness.cc 19 | robust_semantics/minmax.cc robust_semantics/minmax.hpp 20 | ) 21 | else() 22 | message(STATUS "Not building robust semantics") 23 | endif() 24 | 25 | add_library(signaltl ${SIGNALTL_SRCS}) 26 | 27 | target_link_libraries(signaltl PUBLIC fmt::fmt) 28 | target_include_directories( 29 | signaltl PUBLIC $ 30 | $ 31 | ) 32 | target_compile_features(signaltl PUBLIC cxx_std_17) 33 | set_default_compile_options(signaltl) 34 | set_project_warnings(signaltl) 35 | enable_clang_tidy(signaltl) 36 | enable_include_what_you_use(signaltl) 37 | add_coverage(signaltl) 38 | set_target_properties(signaltl PROPERTIES POSITION_INDEPENDENT_CODE ON) 39 | 40 | if(BUILD_PARSER) 41 | target_link_libraries(signaltl PRIVATE taocpp::pegtl) 42 | target_include_directories(signaltl PRIVATE parser/) 43 | set_std_filesystem_options(signaltl) 44 | endif() 45 | if(BUILD_ROBUSTNESS) 46 | target_include_directories(signaltl PRIVATE robust_semantics) 47 | endif() 48 | -------------------------------------------------------------------------------- /src/core/ast.cc: -------------------------------------------------------------------------------- 1 | #include "signal_tl/ast.hpp" 2 | #include "signal_tl/internal/utils.hpp" 3 | 4 | #include 5 | #include 6 | 7 | namespace signal_tl { 8 | 9 | namespace ast { 10 | 11 | Predicate operator>(const Predicate& lhs, const double bound) { 12 | return Predicate{lhs.name, ComparisonOp::GT, bound}; 13 | } 14 | 15 | Predicate operator>=(const Predicate& lhs, const double bound) { 16 | return Predicate{lhs.name, ComparisonOp::GE, bound}; 17 | } 18 | 19 | Predicate operator<(const Predicate& lhs, const double bound) { 20 | return Predicate{lhs.name, ComparisonOp::LT, bound}; 21 | } 22 | 23 | Predicate operator<=(const Predicate& lhs, const double bound) { 24 | return Predicate{lhs.name, ComparisonOp::LE, bound}; 25 | } 26 | 27 | using utils::overloaded; 28 | 29 | namespace { 30 | 31 | Expr AndHelper(const AndPtr& lhs, const Expr& rhs) { 32 | std::vector args{lhs->args}; 33 | 34 | std::visit( 35 | overloaded{ 36 | [&args](const auto e) { args.push_back(e); }, 37 | [&args](const Const e) { 38 | if (!e.value) { 39 | args = {Expr{e}}; 40 | } 41 | }, 42 | [&args](const AndPtr& e) { 43 | args.reserve( 44 | args.size() + 45 | static_cast(std::distance(e->args.begin(), e->args.end()))); 46 | args.insert(args.end(), e->args.begin(), e->args.end()); 47 | }}, 48 | rhs); 49 | return std::make_shared(args); 50 | } 51 | 52 | Expr OrHelper(const OrPtr& lhs, const Expr& rhs) { 53 | std::vector args{lhs->args}; 54 | 55 | std::visit( 56 | overloaded{ 57 | [&args](const auto e) { args.push_back(e); }, 58 | [&args](const Const e) { 59 | if (e.value) { 60 | args = {Expr{e}}; 61 | } 62 | }, 63 | [&args](const OrPtr& e) { 64 | args.reserve( 65 | args.size() + 66 | static_cast(std::distance(e->args.begin(), e->args.end()))); 67 | args.insert(args.end(), e->args.begin(), e->args.end()); 68 | }}, 69 | rhs); 70 | return std::make_shared(args); 71 | } 72 | 73 | } // namespace 74 | 75 | Expr operator&(const Expr& lhs, const Expr& rhs) { 76 | if (const auto c_ptr = std::get_if(&lhs)) { 77 | return (c_ptr->value) ? rhs : *c_ptr; 78 | } else if (const AndPtr* e_ptr = std::get_if(&lhs)) { 79 | return AndHelper(*e_ptr, rhs); 80 | } 81 | return std::make_shared(std::vector{lhs, rhs}); 82 | } 83 | 84 | Expr operator|(const Expr& lhs, const Expr& rhs) { 85 | if (const auto c_ptr = std::get_if(&lhs)) { 86 | return (!c_ptr->value) ? rhs : *c_ptr; 87 | } else if (const OrPtr* e_ptr = std::get_if(&lhs)) { 88 | return OrHelper(*e_ptr, rhs); 89 | } 90 | return std::make_shared(std::vector{lhs, rhs}); 91 | } 92 | 93 | Expr operator~(const Expr& expr) { 94 | if (const auto e = std::get_if(&expr)) { 95 | return Const{!(*e).value}; 96 | } 97 | return std::make_shared(expr); 98 | } 99 | 100 | } // namespace ast 101 | 102 | ast::Const Const(bool value) { 103 | return ast::Const{value}; 104 | } 105 | 106 | ast::Predicate Predicate(std::string name) { 107 | return ast::Predicate{std::move(name)}; 108 | } 109 | 110 | Expr Not(Expr arg) { 111 | if (const auto e = std::get_if(&arg)) { 112 | return ast::Const{!(*e).value}; 113 | } 114 | return std::make_shared(std::move(arg)); 115 | } 116 | 117 | Expr And(std::vector args) { 118 | return std::make_shared(std::move(args)); 119 | } 120 | 121 | Expr Or(std::vector args) { 122 | return std::make_shared(std::move(args)); 123 | } 124 | 125 | Expr Implies(const Expr& x, const Expr& y) { 126 | return (~x) | (y); 127 | } 128 | 129 | Expr Xor(const Expr& x, const Expr& y) { 130 | return (x | y) & (~x | ~y); 131 | } 132 | 133 | Expr Iff(const Expr& x, const Expr& y) { 134 | return (x & y) | (~x & ~y); 135 | } 136 | 137 | Expr Always(Expr arg) { 138 | return std::make_shared(std::move(arg)); 139 | } 140 | 141 | Expr Always(Expr arg, ast::Interval interval) { 142 | return std::make_shared(std::move(arg), interval); 143 | } 144 | 145 | Expr Eventually(Expr arg) { 146 | return std::make_shared(std::move(arg)); 147 | } 148 | 149 | Expr Eventually(Expr arg, ast::Interval interval) { 150 | return std::make_shared(std::move(arg), interval); 151 | } 152 | 153 | Expr Until(Expr arg1, Expr arg2) { 154 | return std::make_shared(std::move(arg1), std::move(arg2)); 155 | } 156 | 157 | Expr Until(Expr arg1, Expr arg2, ast::Interval interval) { 158 | return std::make_shared(std::move(arg1), std::move(arg2), interval); 159 | } 160 | 161 | } // namespace signal_tl 162 | -------------------------------------------------------------------------------- /src/core/signal.cc: -------------------------------------------------------------------------------- 1 | #include "signal_tl/signal.hpp" // for Sample, Signal, SignalPtr, synchronize 2 | #include "signal_tl/fmt.hpp" // IWYU pragma: keep 3 | 4 | #include // for lower_bound, max 5 | #include // for format 6 | #include // for prev, next 7 | #include // for shared_ptr, __shared_ptr_access, mak... 8 | #include // for invalid_argument 9 | #include // for make_tuple, tuple 10 | #include // for vector 11 | 12 | namespace signal_tl::signal { 13 | 14 | Sample Signal::at(double t) const { 15 | if (this->begin_time() > t && this->end_time() < t) { 16 | throw std::invalid_argument( 17 | fmt::format("Signal is undefined for given time instance {}", t)); 18 | } 19 | constexpr auto comp_time = [](const Sample& a, const Sample& b) -> bool { 20 | return a.time < b.time; 21 | }; 22 | 23 | auto it = std::lower_bound( 24 | this->begin(), this->end(), Sample{t, 0.0}, comp_time); // it->time >= t 25 | if (it->time == t) { 26 | return *it; 27 | } 28 | return Sample{t, it->interpolate(t), it->derivative}; 29 | } 30 | 31 | void Signal::push_back(Sample sample) { 32 | if (!this->samples.empty()) { 33 | if (sample.time <= this->end_time()) { 34 | throw std::invalid_argument(fmt::format( 35 | "Trying to append a Sample timestamped at or before the Signal end_time," 36 | "i.e., time is not strictly monotonically increasing." 37 | "Current end_time is {}, given Sample is {}.", 38 | this->end_time(), 39 | sample)); 40 | } 41 | const auto t = this->samples.back().time; 42 | const auto v = this->samples.back().value; 43 | auto& last = this->samples.back(); 44 | 45 | last.derivative = (sample.value - v) / (sample.time - t); 46 | } 47 | this->samples.push_back(Sample{sample.time, sample.value, 0.0}); 48 | } 49 | 50 | void Signal::push_back(double time, double value) { 51 | this->push_back(Sample{time, value, 0.0}); 52 | } 53 | 54 | SignalPtr Signal::simplify() const { 55 | auto sig = std::make_shared(); 56 | for (const auto& s : this->samples) { 57 | const auto [t, v, d] = s; 58 | if ((sig->empty()) || 59 | (sig->back().interpolate(t) != v || sig->back().derivative != d)) { 60 | sig->push_back(s); 61 | } 62 | } 63 | if (this->end_time() != sig->end_time()) { 64 | sig->push_back(this->back()); 65 | } 66 | return sig; 67 | } 68 | 69 | SignalPtr Signal::resize(double start, double end, double fill) const { 70 | auto sig = std::make_shared(); 71 | 72 | // Check if begin_time > start, then add filled value 73 | if (this->begin_time() > start) { 74 | sig->push_back(Sample{start, fill, 0.0}); 75 | } 76 | 77 | // Truncate the discard all samples where sample.time < start 78 | for (auto i = this->begin(); i != this->end(); i++) { 79 | const double t = i->time; 80 | // If current sample is timed below start, ... 81 | if (std::next(i) != this->end() && std::next(i)->time > start) { 82 | // and next sample is timed after `start`, append an intermediate value 83 | sig->push_back(Sample{start, i->interpolate(start)}); 84 | } else if (start <= t && t <= end) { 85 | // If the samples are within the desired time range, keep the samples. 86 | sig->push_back(*i); 87 | } else if (t > end) { 88 | // If we are out of the range, ... 89 | if (i != this->begin() && std::prev(i)->time < end) { 90 | // and the previous sample is within the range, interpolate from the last. 91 | sig->push_back(Sample{end, std::prev(i)->interpolate(end)}); 92 | } else { 93 | // TODO(anand): Does it make sense to terminate early? 94 | break; 95 | } 96 | } 97 | } 98 | 99 | return sig; 100 | } 101 | 102 | SignalPtr Signal::shift(double dt) const { 103 | auto sig = std::make_shared(this->samples); 104 | for (auto& s : sig->samples) { s.time += dt; } 105 | 106 | return sig; 107 | } 108 | 109 | SignalPtr Signal::resize_shift(double start, double end, double fill, double dt) const { 110 | auto out = this->resize(start, end, fill); 111 | for (auto& s : out->samples) { s.time += dt; } 112 | return out; 113 | } 114 | 115 | std::tuple, std::shared_ptr> 116 | synchronize(const std::shared_ptr& x, const std::shared_ptr& y) { 117 | const double begin_time = std::max(x->begin_time(), y->begin_time()); 118 | // const double end_time = std::min(x->end_time(), y->end_time()); 119 | 120 | // These will store the new series of Samples, containing a sample for every 121 | // time instance in x and y, and the time points where they intersect. 122 | auto xv = std::vector{}; 123 | auto yv = std::vector{}; 124 | 125 | // Iterator to the first element where element.time <= begin_time. 126 | // If the iterator begins after begin_time, get the prev (if it exists) and 127 | // interpolate from it. 128 | constexpr auto comp_time = [](const Sample& a, const Sample& b) -> bool { 129 | return a.time < b.time; 130 | }; 131 | auto i = std::lower_bound(x->begin(), x->end(), Sample{begin_time, 0.0}, comp_time); 132 | if (i->time > begin_time && i != x->begin()) { 133 | xv.push_back(Sample{ 134 | begin_time, std::prev(i)->interpolate(begin_time), std::prev(i)->derivative}); 135 | } 136 | auto j = std::lower_bound(y->begin(), y->end(), Sample{begin_time, 0.0}, comp_time); 137 | if (j->time > begin_time && j != y->begin()) { 138 | yv.push_back(Sample{ 139 | begin_time, std::prev(j)->interpolate(begin_time), std::prev(i)->derivative}); 140 | } 141 | 142 | // Now, we have to track the timestamps. 143 | while (i != x->end() && j != y->end()) { 144 | if (i->time == j->time) { 145 | // The samples remain in both. 146 | xv.push_back(*i); 147 | yv.push_back(*j); 148 | i++; 149 | j++; 150 | } else if (i->time < j->time) { 151 | // Add the current point 152 | xv.push_back(*i); 153 | yv.push_back(Sample{i->time, std::prev(j)->interpolate(i->time)}); 154 | // TODO: Intercept? 155 | // We need to "catch up". 156 | i++; 157 | } else if (j->time < i->time) { 158 | yv.push_back(*j); 159 | xv.push_back(Sample{j->time, std::prev(i)->interpolate(j->time)}); 160 | j++; 161 | } 162 | } 163 | 164 | if (const double t = xv.back().time; yv.back().time < t) { 165 | yv.push_back(Sample{xv.back().time, yv.back().interpolate(t)}); 166 | } 167 | 168 | if (const double t = yv.back().time; xv.back().time < t) { 169 | xv.push_back(Sample{yv.back().time, xv.back().interpolate(t)}); 170 | } 171 | 172 | return std::make_tuple(std::make_shared(xv), std::make_shared(yv)); 173 | } 174 | 175 | } // namespace signal_tl::signal 176 | -------------------------------------------------------------------------------- /src/include/signal_tl/ast.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef SIGNAL_TEMPORAL_LOGIC_AST_HPP 4 | #define SIGNAL_TEMPORAL_LOGIC_AST_HPP 5 | 6 | #include // for isinf 7 | #include // for numeric_limits 8 | #include // for shared_ptr 9 | #include // for invalid_argument 10 | #include // for string, operator==, basic_string 11 | #include // for remove_reference<>::type 12 | #include // for move, make_pair, pair 13 | #include // for get, get_if, visit, variant 14 | #include // for vector 15 | 16 | namespace signal_tl { 17 | namespace ast { 18 | 19 | struct Const; 20 | struct Predicate; 21 | struct Not; 22 | struct And; 23 | struct Or; 24 | struct Always; 25 | struct Eventually; 26 | struct Until; 27 | 28 | using ConstPtr = std::shared_ptr; 29 | using PredicatePtr = std::shared_ptr; 30 | using NotPtr = std::shared_ptr; 31 | using AndPtr = std::shared_ptr; 32 | using OrPtr = std::shared_ptr; 33 | using AlwaysPtr = std::shared_ptr; 34 | using EventuallyPtr = std::shared_ptr; 35 | using UntilPtr = std::shared_ptr; 36 | 37 | /* Define Syntax Tree */ 38 | 39 | /// Boolean constant AST nodes. 40 | /// 41 | /// Used to represent `true` or `false` values efficiently. 42 | struct Const { 43 | bool value = false; 44 | 45 | // TODO: pybind11 doesn't like deleted default constructors 46 | Const() = default; 47 | Const(bool boolean_value) : value{boolean_value} {}; 48 | 49 | inline bool operator==(const Const& other) const { 50 | return this->value == other.value; 51 | }; 52 | 53 | inline bool operator!=(const Const& other) const { 54 | return !(*this == other); 55 | }; 56 | }; 57 | 58 | /// Valid comparison operations within a predicate. 59 | /// 60 | /// NOTE(anand): I personally think it never makes sense to check if some 61 | /// signal value is _exactly_ equal to some value, as that is impossible with 62 | /// real-valued signals. 63 | enum class ComparisonOp { GT, GE, LT, LE }; 64 | 65 | /// A Predicate AST node. 66 | /// 67 | /// It simply holds the expression `x ~ c`, where `x` is some signal identifier, `~` is 68 | /// a valid comparison operator, and `c` is some constant (double). 69 | struct Predicate { 70 | std::string name; 71 | ComparisonOp op = ComparisonOp::GE; 72 | double rhs = 0.0; 73 | 74 | // Predicate() = delete; 75 | Predicate( 76 | std::string ap_name, 77 | ComparisonOp operation = ComparisonOp::GE, 78 | double constant_val = 0.0) : 79 | name{std::move(ap_name)}, op{operation}, rhs{constant_val} {}; 80 | 81 | inline bool operator==(const Predicate& other) const { 82 | return (name == other.name) && (op == other.op) && (rhs == other.rhs); 83 | }; 84 | 85 | inline bool operator!=(const Predicate& other) const { 86 | return !(*this == other); 87 | }; 88 | }; 89 | 90 | /// A valid expression is one of the following: 91 | /// 92 | /// - A Boolean constant; 93 | /// - A Predicate expression; 94 | /// - A unary Not, or n-ary And/Or; and 95 | /// - The temporal operators, Always, Eventually, and Until. 96 | using Expr = std::variant< 97 | Const, 98 | Predicate, 99 | NotPtr, 100 | AndPtr, 101 | OrPtr, 102 | AlwaysPtr, 103 | EventuallyPtr, 104 | UntilPtr>; 105 | using ExprPtr = std::shared_ptr; 106 | 107 | struct Not { 108 | Expr arg; 109 | 110 | // Not() = delete; 111 | Not(Expr sub_expr) : arg(std::move(sub_expr)) {} 112 | }; 113 | 114 | struct And { 115 | std::vector args; 116 | 117 | // And() = delete; 118 | And(std::vector operands) : args(std::move(operands)) { 119 | if (args.size() < 2) { 120 | throw std::invalid_argument( 121 | "It doesn't make sense to have an And operator with < 2 operands"); 122 | } 123 | } 124 | }; 125 | 126 | struct Or { 127 | std::vector args; 128 | 129 | // Or() = delete; 130 | Or(std::vector operands) : args(std::move(operands)) { 131 | if (args.size() < 2) { 132 | throw std::invalid_argument( 133 | "It doesn't make sense to have an Or operator with < 2 operands"); 134 | } 135 | } 136 | }; 137 | 138 | /* Modal Logic */ 139 | using IntervalType = std::pair; 140 | 141 | /// A simple interval type for temporal operators. 142 | /// 143 | /// Currently, it can only encode a pair of numbers (either `unsigned long long 144 | /// int` or `double`), and enforced that any `double`s need to be positive at 145 | /// runtime. 146 | /// 147 | /// TODO 148 | /// ---- 149 | /// 150 | /// - Support open and closed intervals, along with interval operations. Maybe 151 | /// we will have to move this to a separate header file like `Signal`. 152 | struct Interval { 153 | using Num = std::variant; 154 | 155 | Num low; 156 | Num high; 157 | 158 | Interval() : low{0.0}, high{std::numeric_limits::infinity()} {}; 159 | Interval(unsigned long long int a, unsigned long long int b) : low{a}, high{b} {} 160 | Interval(double a, double b) : low{a}, high{b} { 161 | if (a < 0 || b < 0) { 162 | throw std::invalid_argument("Interval cannot have negative values"); 163 | } else if (b <= a) { 164 | throw std::invalid_argument("Interval [a,b] cannot have b <= a"); 165 | } 166 | } 167 | 168 | /// Return the (low, high) pair as `double`s. 169 | [[nodiscard]] std::pair as_double() const { 170 | double a = 0.0, b = 0.0; 171 | if (auto a_pval = std::get_if(&this->low)) { 172 | a = static_cast(*a_pval); 173 | } else { 174 | a = std::get(this->low); 175 | } 176 | 177 | if (auto b_pval = std::get_if(&this->high)) { 178 | b = static_cast(*b_pval); 179 | } else { 180 | b = std::get(this->high); 181 | } 182 | return {a, b}; 183 | } 184 | 185 | /// Check if the interval is [0, inf). 186 | [[nodiscard]] bool is_zero_to_inf() const { 187 | bool start_zero = std::visit([](auto&& start) { return start == 0; }, this->low); 188 | bool end_inf = std::visit( 189 | [](auto&& end) { return std::isinf(static_cast(end)); }, this->high); 190 | return start_zero && end_inf; 191 | } 192 | 193 | /// Check if the interval is [0, inf). 194 | /// 195 | /// NOTE: Purely for backwards compatibility to a version of signal_tl that 196 | /// dealt with intervals as `std::pair`, and the interval was 197 | /// stored inside an `std::optional` field in the Temporal operators. 198 | [[nodiscard]] bool has_value() const { 199 | return this->is_zero_to_inf(); 200 | } 201 | }; 202 | 203 | struct Always { 204 | Expr arg; 205 | Interval interval; 206 | 207 | // Always() = delete; 208 | Always(Expr operand) : arg{std::move(operand)}, interval{} {} 209 | Always(Expr operand, Interval time_interval) : 210 | arg(std::move(operand)), interval(time_interval) {} 211 | }; 212 | 213 | struct Eventually { 214 | Expr arg; 215 | Interval interval; 216 | 217 | // Eventually() = delete; 218 | Eventually(Expr operand) : arg{std::move(operand)}, interval{} {} 219 | Eventually(Expr operand, Interval time_interval) : 220 | arg(std::move(operand)), interval(time_interval) {} 221 | }; 222 | 223 | struct Until { 224 | std::pair args; 225 | Interval interval; 226 | 227 | // Until() = delete; 228 | Until(Expr arg0, Expr arg1) : 229 | args{std::make_pair(std::move(arg0), std::move(arg1))}, interval{} {} 230 | Until(Expr arg0, Expr arg1, Interval time_interval) : 231 | args{std::make_pair(std::move(arg0), std::move(arg1))}, interval{time_interval} {} 232 | }; 233 | 234 | Predicate operator>(const Predicate& lhs, const double bound); 235 | Predicate operator>=(const Predicate& lhs, const double bound); 236 | Predicate operator<(const Predicate& lhs, const double bound); 237 | Predicate operator<=(const Predicate& lhs, const double bound); 238 | 239 | Expr operator~(const Expr& e); 240 | Expr operator&(const Expr& lhs, const Expr& rhs); 241 | Expr operator|(const Expr& lhs, const Expr& rhs); 242 | Expr operator>>(const Expr& lhs, const Expr& rhs); 243 | 244 | } // namespace ast 245 | 246 | using ast::Expr; 247 | 248 | // These are helper functions to automatically wrap the correct ast Node into a 249 | // `std::shared_ptr` and then return that as an `Expr`. 250 | ast::Const Const(bool value); 251 | ast::Predicate Predicate(std::string name); 252 | Expr Not(Expr arg); 253 | Expr And(std::vector args); 254 | Expr Or(std::vector args); 255 | Expr Implies(const Expr& arg1, const Expr& arg2); 256 | Expr Xor(const Expr& arg1, const Expr& arg2); 257 | Expr Iff(const Expr& arg1, const Expr& arg2); 258 | Expr Always(Expr arg); 259 | Expr Always(Expr arg, ast::Interval interval); 260 | Expr Eventually(Expr arg); 261 | Expr Eventually(Expr arg, ast::Interval interval); 262 | Expr Until(Expr arg1, Expr arg2); 263 | Expr Until(Expr arg1, Expr arg2, ast::Interval interval); 264 | 265 | } // namespace signal_tl 266 | 267 | #endif 268 | -------------------------------------------------------------------------------- /src/include/signal_tl/exception.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef SIGNAL_TEMPORAL_LOGIC_EXCEPTION_HPP 4 | #define SIGNAL_TEMPORAL_LOGIC_EXCEPTION_HPP 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | // LCOV_EXCL_START 11 | namespace signal_tl { 12 | 13 | struct not_implemented_error : public std::exception { 14 | private: 15 | const std::string reason; 16 | 17 | public: 18 | not_implemented_error(std::string what_arg) : reason{std::move(what_arg)} {} 19 | not_implemented_error(const char* what_arg) : reason{what_arg} {} 20 | 21 | not_implemented_error(const not_implemented_error& other) = default; 22 | 23 | [[nodiscard]] const char* what() const noexcept override { 24 | return this->reason.c_str(); 25 | } 26 | }; 27 | 28 | } // namespace signal_tl 29 | // LCOV_EXCL_STOP 30 | 31 | #endif /* end of include guard: __SIGNAL_TEMPORAL_LOGIC_EXCEPTION_HH__ */ 32 | -------------------------------------------------------------------------------- /src/include/signal_tl/fmt.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef SIGNAL_TEMPORAL_LOGIC_FMT_HPP 4 | #define SIGNAL_TEMPORAL_LOGIC_FMT_HPP 5 | 6 | #include 7 | #include 8 | 9 | #include "signal_tl/ast.hpp" 10 | #include "signal_tl/signal.hpp" 11 | 12 | #include "signal_tl/internal/utils.hpp" 13 | 14 | std::ostream& operator<<(std::ostream& os, const signal_tl::ast::Expr& expr); 15 | 16 | namespace signal_tl::ast { 17 | 18 | template 19 | struct formatter { 20 | constexpr auto parse(fmt::format_parse_context& ctx) { 21 | return ctx.begin(); 22 | } 23 | }; 24 | 25 | } // namespace signal_tl::ast 26 | 27 | template <> 28 | struct fmt::formatter 29 | : signal_tl::ast::formatter { 30 | template 31 | auto format(const signal_tl::ast::Const& e, FormatContext& ctx) { 32 | return format_to(ctx.out(), "{}", e.value); 33 | } 34 | }; 35 | 36 | template <> 37 | struct fmt::formatter 38 | : signal_tl::ast::formatter { 39 | template 40 | auto format(const signal_tl::ast::Predicate& e, FormatContext& ctx) { 41 | std::string op = ">="; 42 | switch (e.op) { 43 | case signal_tl::ast::ComparisonOp::GE: 44 | op = ">="; 45 | break; 46 | case signal_tl::ast::ComparisonOp::GT: 47 | op = ">"; 48 | break; 49 | case signal_tl::ast::ComparisonOp::LE: 50 | op = "<="; 51 | break; 52 | case signal_tl::ast::ComparisonOp::LT: 53 | op = "<"; 54 | break; 55 | } 56 | return format_to(ctx.out(), "({} {} {})", e.name, op, e.rhs); 57 | } 58 | }; 59 | 60 | template <> 61 | struct fmt::formatter 62 | : signal_tl::ast::formatter { 63 | template 64 | auto format(const signal_tl::ast::Not& e, FormatContext& ctx) { 65 | return format_to(ctx.out(), "~{}", e.arg); 66 | } 67 | }; 68 | 69 | template <> 70 | struct fmt::formatter 71 | : signal_tl::ast::formatter { 72 | template 73 | auto format(const signal_tl::ast::And& e, FormatContext& ctx) { 74 | return format_to(ctx.out(), "({})", fmt::join(e.args, " & ")); 75 | } 76 | }; 77 | 78 | template <> 79 | struct fmt::formatter 80 | : signal_tl::ast::formatter { 81 | template 82 | auto format(const signal_tl::ast::Or& e, FormatContext& ctx) { 83 | return format_to(ctx.out(), "({})", fmt::join(e.args, " | ")); 84 | } 85 | }; 86 | 87 | template <> 88 | struct fmt::formatter 89 | : signal_tl::ast::formatter { 90 | template 91 | auto format(const signal_tl::ast::Always& e, FormatContext& ctx) { 92 | if (e.interval.has_value()) { 93 | const auto [a, b] = e.interval.as_double(); 94 | if (std::isinf(b)) { 95 | return format_to(ctx.out(), "G[{}, int) {}", a, e.arg); 96 | } 97 | return format_to(ctx.out(), "G[{},{}] {}", a, b, e.arg); 98 | } 99 | return format_to(ctx.out(), "G {}", e.arg); 100 | } 101 | }; 102 | 103 | template <> 104 | struct fmt::formatter 105 | : signal_tl::ast::formatter { 106 | template 107 | auto format(const signal_tl::ast::Eventually& e, FormatContext& ctx) { 108 | if (e.interval.has_value()) { 109 | const auto [a, b] = e.interval.as_double(); 110 | if (std::isinf(b)) { 111 | return format_to(ctx.out(), "F[{}, inf] {}", a, e.arg); 112 | } 113 | return format_to(ctx.out(), "F[{},{}] {}", a, b, e.arg); 114 | } 115 | return format_to(ctx.out(), "F {}", e.arg); 116 | } 117 | }; 118 | 119 | template <> 120 | struct fmt::formatter 121 | : signal_tl::ast::formatter { 122 | template 123 | auto format(const signal_tl::ast::Until& e, FormatContext& ctx) { 124 | const auto [e1, e2] = e.args; 125 | if (e.interval.has_value()) { 126 | const auto [a, b] = e.interval.as_double(); 127 | if (std::isinf(b)) { 128 | return format_to(ctx.out(), "{} U[{}, inf) {}", e1, a, e2); 129 | } 130 | return format_to(ctx.out(), "{} U[{},{}] {}", e1, a, b, e2); 131 | } 132 | return format_to(ctx.out(), "{} U {}", e1, e2); 133 | } 134 | }; 135 | 136 | inline std::ostream& operator<<(std::ostream& os, const signal_tl::ast::Expr& expr) { 137 | std::string s = std::visit( 138 | signal_tl::utils::overloaded{ 139 | [](const signal_tl::ast::Const& e) { return fmt::to_string(e); }, 140 | [](const signal_tl::ast::Predicate& e) { return fmt::to_string(e); }, 141 | [](const auto e) { 142 | return fmt::to_string(*e); 143 | }}, 144 | expr); 145 | 146 | return os << s; 147 | } 148 | 149 | template <> 150 | struct fmt::formatter 151 | : signal_tl::ast::formatter { 152 | template 153 | auto format(const signal_tl::ast::Expr& expr, FormatContext& ctx) { 154 | return std::visit( 155 | signal_tl::utils::overloaded{ 156 | [&](const signal_tl::ast::Const& e) { 157 | return format_to(ctx.out(), "{}", e); 158 | }, 159 | [&](const signal_tl::ast::Predicate& e) { 160 | return format_to(ctx.out(), "{}", e); 161 | }, 162 | [&](const auto& e) { 163 | return format_to(ctx.out(), "{}", *e); 164 | }}, 165 | expr); 166 | } 167 | }; 168 | 169 | template <> 170 | struct fmt::formatter { 171 | constexpr auto parse(format_parse_context& ctx) { 172 | return ctx.begin(); 173 | } 174 | 175 | template 176 | auto format(const signal_tl::signal::Sample& s, FormatContext& ctx) { 177 | return format_to(ctx.out(), "({}, {})", s.time, s.value); 178 | } 179 | }; 180 | 181 | template <> 182 | struct fmt::formatter { 183 | constexpr auto parse(format_parse_context& ctx) { 184 | return ctx.begin(); 185 | } 186 | 187 | template 188 | auto format(const signal_tl::signal::Signal& s, FormatContext& ctx) { 189 | return format_to(ctx.out(), "[{}]", fmt::join(s, ", ")); 190 | } 191 | }; 192 | 193 | #endif 194 | -------------------------------------------------------------------------------- /src/include/signal_tl/internal/filesystem.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef SIGNALTL_INTERNAL_FILESYSTEM_HPP 4 | #define SIGNALTL_INTERNAL_FILESYSTEM_HPP 5 | 6 | // First, we will check if the source is being compiled with 7 | #if __cplusplus >= 201703L 8 | 9 | #if __has_include() 10 | #include // IWYU pragma: export 11 | namespace stdfs = std::filesystem; 12 | #elif __has_include() 13 | #include // IWYU pragma: export 14 | namespace stdfs = std::experimental::filesystem; 15 | #else 16 | #error "Missing the and headers." 17 | #endif 18 | 19 | #else 20 | #error "Need to compile with C++17 support" 21 | #endif 22 | 23 | #endif 24 | -------------------------------------------------------------------------------- /src/include/signal_tl/internal/utils.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef SIGNAL_TEMPORAL_LOGIC_UTILS_HPP 4 | #define SIGNAL_TEMPORAL_LOGIC_UTILS_HPP 5 | 6 | #include 7 | #include 8 | 9 | namespace signal_tl::utils { 10 | 11 | /** 12 | * Visit helper for a set of visitor lambdas. 13 | * 14 | * @see https://en.cppreference.com/w/cpp/utility/variant/visit 15 | */ 16 | template 17 | struct overloaded : Ts... { 18 | using Ts::operator()...; 19 | }; 20 | template 21 | overloaded(Ts...) -> overloaded; 22 | 23 | /** 24 | * Python-style enumerate function for range-based for loops. 25 | * 26 | * @see http://www.reedbeta.com/blog/python-like-enumerate-in-cpp17/ 27 | */ 28 | template < 29 | typename T, 30 | typename TIter = decltype(std::begin(std::declval())), 31 | typename = decltype(std::end(std::declval()))> 32 | constexpr auto enumerate(T&& iterable) { 33 | struct iterator { 34 | size_t i; 35 | TIter iter; 36 | bool operator!=(const iterator& other) const { 37 | return iter != other.iter; 38 | } 39 | void operator++() { 40 | ++i; 41 | ++iter; 42 | } 43 | auto operator*() const { 44 | return std::tie(i, *iter); 45 | } 46 | }; 47 | struct iterable_wrapper { 48 | T iterable; 49 | auto begin() { 50 | return iterator{0, std::begin(iterable)}; 51 | } 52 | auto end() { 53 | return iterator{0, std::end(iterable)}; 54 | } 55 | }; 56 | return iterable_wrapper{std::forward(iterable)}; 57 | } 58 | 59 | /** 60 | * Python-style reversed iterator 61 | */ 62 | template < 63 | typename T, 64 | typename = decltype(std::rbegin(std::declval())), 65 | typename = decltype(std::rend(std::declval()))> 66 | constexpr auto reversed(T&& iterable) { 67 | struct iterable_wrapper { 68 | T iterable; 69 | auto begin() { 70 | return std::rbegin(iterable); 71 | } 72 | auto end() { 73 | return std::rend(iterable); 74 | } 75 | }; 76 | return iterable_wrapper{std::forward(iterable)}; 77 | } 78 | 79 | } // namespace signal_tl::utils 80 | 81 | #endif 82 | -------------------------------------------------------------------------------- /src/include/signal_tl/parser.hpp: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | 4 | #ifndef SIGNAL_TEMPORAL_LOGIC_PARSER_HPP 5 | #define SIGNAL_TEMPORAL_LOGIC_PARSER_HPP 6 | 7 | #include "signal_tl/ast.hpp" // for Expr 8 | #include "signal_tl/internal/filesystem.hpp" 9 | 10 | #include // for size_t 11 | #include // for map 12 | #include // for unique_ptr 13 | #include // for string 14 | #include // for string_view 15 | #include // for move 16 | 17 | namespace signal_tl { 18 | 19 | /// Holds the concrete `Specification` that is read from a file. 20 | /// 21 | /// A specification file is a list of commands/declarations which will be used 22 | /// to build monitors for signals. See the documentation for the specification 23 | /// file for more details. 24 | struct Specification { 25 | std::map formulas; 26 | std::map assertions; 27 | 28 | Specification() = default; 29 | Specification( 30 | std::map _formulas, 31 | std::map _assertions) : 32 | formulas{std::move(_formulas)}, assertions{std::move(_assertions)} {} 33 | 34 | void add_formula(const std::string&, ast::Expr); 35 | void add_assertion(const std::string&, ast::Expr); 36 | 37 | ast::Expr get_formula(std::string_view); 38 | ast::Expr get_assertion(std::string_view); 39 | }; 40 | 41 | namespace parser { 42 | 43 | /// Given a `string_view` of the actual specification (typically read from the 44 | /// specification script file), this fuction will return the parsed contents. 45 | /// 46 | /// Note that by returning a `std::unique_ptr`, the `signal_tl` library gives 47 | /// up ownership of the `Specification`. The user of the library can manipulate 48 | /// the `Specification` struct however they like, but the specification may 49 | /// lose its meaning once you do. 50 | std::unique_ptr from_string(std::string_view); 51 | 52 | /// Given a `std::filesystem::path` to the specification file, this function 53 | /// reads the file and creates a concrete `Specification` from it. 54 | /// 55 | /// Note that by returning a `std::unique_ptr`, the `signal_tl` library gives 56 | /// up ownership of the `Specification`. The user of the library can manipulate 57 | /// the `Specification` struct however they like, but the specification may 58 | /// lose its meaning once you do. 59 | std::unique_ptr from_file(const stdfs::path&); 60 | 61 | } // namespace parser 62 | 63 | // LCOV_EXCL_START 64 | namespace grammar::internal { 65 | 66 | /// **INTERNAL USE ONLY** 67 | /// 68 | /// This is used to call `tao::pagtl::contrib::analyze`, a function that 69 | /// analyzes the parser grammar for construction errors like unresolved cycles, 70 | /// etc. Used in the tests to check the grammar and is useful only for 71 | /// developers of this library. 72 | size_t analyze(int verbose = 1); 73 | 74 | bool trace_from_file(const stdfs::path&); 75 | 76 | } // namespace grammar::internal 77 | // LCOV_EXCL_STOP 78 | 79 | } // namespace signal_tl 80 | 81 | #endif /* end of include guard: SIGNAL_TEMPORAL_LOGIC_PARSER_HPP */ 82 | -------------------------------------------------------------------------------- /src/include/signal_tl/robustness.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef SIGNAL_TEMPORAL_LOGIC_ROBUSTNESS_HPP 4 | #define SIGNAL_TEMPORAL_LOGIC_ROBUSTNESS_HPP 5 | 6 | #include "signal_tl/ast.hpp" 7 | #include "signal_tl/signal.hpp" 8 | 9 | #include 10 | #include 11 | 12 | namespace signal_tl::semantics { 13 | 14 | signal::SignalPtr compute_robustness( 15 | const ast::Expr& phi, 16 | const signal::Trace& trace, 17 | bool synchronized = false); 18 | 19 | } // namespace signal_tl::semantics 20 | 21 | #endif 22 | -------------------------------------------------------------------------------- /src/include/signal_tl/signal.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef SIGNAL_TEMPORAL_LOGIC_SIGNAL_HPP 4 | #define SIGNAL_TEMPORAL_LOGIC_SIGNAL_HPP 5 | 6 | #include // for lower_bound 7 | #include // for size_t 8 | #include // for next, prev 9 | #include // for map 10 | #include // for shared_ptr, allocator_traits<>::value_type 11 | #include // for invalid_argument 12 | #include // for string 13 | #include // for tuple 14 | #include // for declval 15 | #include // for vector 16 | 17 | namespace signal_tl::signal { 18 | 19 | struct Sample { 20 | double time; 21 | double value; 22 | double derivative = 0.0; 23 | 24 | /** 25 | * Linear interpolate the Sample (given the derivative) to get the value at time `t`. 26 | */ 27 | [[nodiscard]] constexpr double interpolate(double t) const { 28 | return value + derivative * (t - time); 29 | } 30 | 31 | /** 32 | * Get the time point at which the lines associated with this Sample and the given 33 | * Sample intersect. 34 | */ 35 | [[nodiscard]] constexpr double time_intersect(const Sample& point) const { 36 | return (value - point.value + (point.derivative * point.time) - 37 | (derivative * time)) / 38 | (point.derivative - derivative); 39 | } 40 | 41 | [[nodiscard]] constexpr double area(double t) const { 42 | if (t > time) { 43 | return (value + this->interpolate(t)) * (t - time) / 2; 44 | } else { 45 | return 0; 46 | } 47 | } 48 | 49 | }; // namespace signal 50 | 51 | constexpr bool operator<(const Sample& lhs, const Sample& rhs) { 52 | return lhs.value < rhs.value; 53 | } 54 | constexpr bool operator>(const Sample& lhs, const Sample& rhs) { 55 | return rhs < lhs; 56 | } 57 | constexpr bool operator<=(const Sample& lhs, const Sample& rhs) { 58 | return !(lhs > rhs); 59 | } 60 | constexpr bool operator>=(const Sample& lhs, const Sample& rhs) { 61 | return !(lhs < rhs); 62 | } 63 | 64 | constexpr Sample operator-(const Sample& other) { 65 | return {other.time, -other.value, -other.derivative}; 66 | } 67 | 68 | /** 69 | * Piecewise-linear, right-continuous signal 70 | */ 71 | struct Signal { 72 | private: 73 | std::vector samples; 74 | 75 | public: 76 | [[nodiscard]] double begin_time() const { 77 | return (samples.empty()) ? 0.0 : samples.front().time; 78 | } 79 | 80 | [[nodiscard]] double end_time() const { 81 | return (samples.empty()) ? 0.0 : samples.back().time; 82 | } 83 | 84 | [[nodiscard]] double interpolate(double t, size_t idx) const { 85 | return this->samples.at(idx).interpolate(t); 86 | } 87 | 88 | [[nodiscard]] double time_intersect(const Sample& point, size_t idx) const { 89 | return this->samples.at(idx).time_intersect(point); 90 | } 91 | 92 | [[nodiscard]] double area(double t, size_t idx) const { 93 | return this->samples.at(idx).area(t); 94 | } 95 | 96 | [[nodiscard]] Sample front() const { 97 | return this->samples.front(); 98 | } 99 | 100 | [[nodiscard]] Sample back() const { 101 | return this->samples.back(); 102 | } 103 | 104 | [[nodiscard]] Sample at_idx(size_t i) const { 105 | return this->samples.at(i); 106 | } 107 | 108 | /** 109 | * Get the sample at time `t`. 110 | * 111 | * Does a binary search for the given time instance, and interpolates from 112 | * the closest sample less than `t` if necessary. 113 | */ 114 | [[nodiscard]] Sample at(double t) const; 115 | /** 116 | * Get const_iterator to the start of the signal 117 | */ 118 | [[nodiscard]] auto begin() const { 119 | return this->samples.cbegin(); 120 | } 121 | 122 | /** 123 | * Get const_iterator to the end of the signal 124 | */ 125 | [[nodiscard]] auto end() const { 126 | return this->samples.cend(); 127 | } 128 | 129 | /** 130 | * Get const_iterator to the first element of the signal that is timed at or after 131 | * `s` 132 | */ 133 | [[nodiscard]] auto begin_at(double s) const { 134 | if (this->begin_time() >= s) 135 | return this->begin(); 136 | 137 | constexpr auto comp_op = [](const Sample& a, const Sample& b) { 138 | return a.time < b.time; 139 | }; 140 | return std::lower_bound(this->begin(), this->end(), Sample{s, 0.0}, comp_op); 141 | } 142 | 143 | /** 144 | * Get const_iterator to the element after the last element of the signal 145 | * that is timed at or before `t` 146 | */ 147 | [[nodiscard]] auto end_at(double t) const { 148 | if (this->end_time() <= t) 149 | return this->end(); 150 | 151 | auto it = this->end(); 152 | while (it->time > t) it = std::prev(it); 153 | // Now we have the pointer to the first element from the back whose .time <= t. 154 | // So increment by 1 and return 155 | return std::next(it); 156 | } 157 | 158 | /** 159 | * Get const reverse_iterator to the samples. 160 | */ 161 | [[nodiscard]] auto rbegin() const { 162 | return this->samples.crbegin(); 163 | } 164 | 165 | /** 166 | * Get const reverse_iterator to the samples. 167 | */ 168 | [[nodiscard]] auto rend() const { 169 | return this->samples.crend(); 170 | } 171 | 172 | [[nodiscard]] size_t size() const { 173 | return this->samples.size(); 174 | } 175 | 176 | [[nodiscard]] bool empty() const { 177 | return this->samples.empty(); 178 | } 179 | 180 | /** 181 | * Add a Sample to the back of the Signal 182 | */ 183 | void push_back(Sample s); 184 | void push_back(double time, double value); 185 | 186 | /** 187 | * Remove sampling points where (y, dy) is continuous 188 | */ 189 | [[nodiscard]] std::shared_ptr simplify() const; 190 | /** 191 | * Restrict/extend the signal to [s,t] with default value v where not defined. 192 | */ 193 | [[nodiscard]] std::shared_ptr 194 | resize(double start, double end, double fill) const; 195 | /** 196 | * Shift the signal by dt time units 197 | */ 198 | [[nodiscard]] std::shared_ptr shift(double dt) const; 199 | 200 | /** 201 | * Resize and shift a signal without creating copies. We use this often, so it makes 202 | * sense to combine it. 203 | */ 204 | [[nodiscard]] std::shared_ptr 205 | resize_shift(double start, double end, double fill, double dt) const; 206 | 207 | Signal() : samples{} {} 208 | 209 | Signal(const Signal& other) { 210 | this->samples.reserve(other.size()); 211 | for (const auto& s : other) { this->push_back(s); } 212 | } 213 | 214 | /** 215 | * Create a Signal from a sequence of amples 216 | */ 217 | template < 218 | typename T, 219 | typename = decltype(std::begin(std::declval())), 220 | typename = decltype(std::end(std::declval()))> 221 | Signal(const T& data) { 222 | this->samples.reserve(data.size()); 223 | for (const auto& s : data) { this->push_back(s); } 224 | } 225 | 226 | /** 227 | * Create a Signal from a sequence of data points and time stamps 228 | */ 229 | Signal(const std::vector& points, const std::vector& times) { 230 | if (points.size() != times.size()) { 231 | throw std::invalid_argument( 232 | "Number of sample points and time points need to be equal."); 233 | } 234 | 235 | size_t n = points.size(); 236 | this->samples.reserve(n); 237 | for (size_t i = 0; i < n; i++) { this->push_back(times.at(i), points.at(i)); } 238 | } 239 | 240 | /** 241 | * Create a Signal from the given iterators 242 | */ 243 | template < 244 | typename T, 245 | typename TIter = decltype(std::begin(std::declval())), 246 | typename = decltype(std::end(std::declval()))> 247 | Signal(TIter&& start, TIter&& end) { 248 | for (auto i = start; i != end; i++) { this->push_back(*i); } 249 | } 250 | }; 251 | 252 | /** 253 | * Synchronize two signals by making sure that one is explicitely defined for all the 254 | * time instances the other is defined. 255 | * 256 | * The output signals are confined to the time range where both of them are defined, 257 | * thus can truncate a signal if the other isn't defined there. 258 | */ 259 | std::tuple, std::shared_ptr> 260 | synchronize(const std::shared_ptr& x, const std::shared_ptr& y); 261 | 262 | using SignalPtr = std::shared_ptr; 263 | using Trace = std::map; 264 | 265 | } // namespace signal_tl::signal 266 | 267 | #endif 268 | -------------------------------------------------------------------------------- /src/include/signal_tl/signal_tl.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef SIGNAL_TEMPORAL_LOGIC_SIGNALTL_HPP 4 | #define SIGNAL_TEMPORAL_LOGIC_SIGNALTL_HPP 5 | 6 | // IWYU pragma: begin_exports 7 | #include "signal_tl/ast.hpp" 8 | #include "signal_tl/exception.hpp" 9 | #include "signal_tl/robustness.hpp" 10 | #include "signal_tl/signal.hpp" 11 | // IWYU pragma: end_exports 12 | 13 | namespace signal_tl { 14 | using namespace signal; 15 | using namespace semantics; 16 | } // namespace signal_tl 17 | 18 | #endif 19 | -------------------------------------------------------------------------------- /src/parser/actions.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef SIGNALTL_PARSER_ACTIONS_HPP 4 | #define SIGNALTL_PARSER_ACTIONS_HPP 5 | 6 | #include "grammar.hpp" 7 | #include "signal_tl/ast.hpp" 8 | #include // IWYU pragma: keep 9 | 10 | #include 11 | 12 | // #define NDEBUG 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | namespace signal_tl::parser { 21 | 22 | namespace actions { 23 | /// Here, we will define the custom actions for the PEG parser that will 24 | /// convert each rule into a valid AST class. 25 | namespace peg = tao::pegtl; 26 | using namespace signal_tl::grammar; 27 | 28 | using PrimitiveState = std::variant; 29 | 30 | enum struct PredicateType { ONE, TWO }; 31 | 32 | /// This encodes local state of the push-down parser. 33 | /// 34 | /// Essentially, this is a stack element, and a new one is pushed down 35 | /// whenerver we encounter a Term, as that is the only recursive rule in our 36 | /// grammar. This keeps track of whatever is required to make a Term, without 37 | /// polluting the context of parent rules in the AST. 38 | struct ParserState { 39 | /// Purely here for debugging purposes. 40 | unsigned long long int level; 41 | 42 | /// Whenever an Expression is completed, this field gets populared. Later, 43 | /// within the action for the Term rule, we move the result onto the vector 44 | /// `terms` to allow for the parent expression to easily combine it with 45 | /// their list of `terms`. 46 | std::optional result; 47 | 48 | /// When parsing a primitive (boolean or numeral), we add the result here so 49 | /// that the parent rule can immediately use it to construct either a 50 | /// BooleanLiteral or a Numeral (integer or double). 51 | std::optional primitive_result; 52 | 53 | /// A list of identifiers as parsed by the rule. 54 | /// 55 | /// In most cases, this is immediately used by the parent rule (either in 56 | /// Term or in some Command) to either create new formulas/assertions or to 57 | /// map existing formulas/assertions to a valid `ast::Expr`. 58 | std::vector identifiers; 59 | 60 | /// Informs the immediate parent rule (only useful for PredicateTerm) whether 61 | /// the Predicate is of the form `id ~ c` or `c ~ id`, essentially to change 62 | /// it to the canonical form `id ~ c`. 63 | std::optional predicate_type; 64 | /// Informs the parent PredicateTerm what comparison operation is used in the 65 | /// predicate. It is used in conjunction with the `predicate_type` field to 66 | /// construct a valid `ast::Predicate`. 67 | std::optional comparison_type; 68 | 69 | /// A list of Terms parsed in the local context. For example, for an N-ary 70 | /// operation like And and Or, we expect the list to have at least 2 valid 71 | /// `ast::Expr`. This is populated when a local context is popped off the 72 | /// stack of a Term within the current rule. 73 | std::vector terms; 74 | }; 75 | 76 | /// This maintains the global list of formulas and assertions that have been 77 | /// parsed within the specification. 78 | struct GlobalParserState { 79 | std::map formulas; 80 | std::map assertions; 81 | }; 82 | 83 | template 84 | struct action : peg::nothing {}; 85 | 86 | /// For each rule, we will assume that the top level function that calls this 87 | /// action passes a reference to a `Specification` that we can populate, along 88 | /// with other internal states. 89 | 90 | template <> 91 | struct action { 92 | template 93 | static void apply(const ActionInput& in, GlobalParserState&, ParserState& state) { 94 | auto id = std::string(in.string()); 95 | state.identifiers.push_back(id); 96 | } 97 | }; 98 | 99 | template <> 100 | struct action { 101 | static void apply0(GlobalParserState&, ParserState& state) { 102 | state.primitive_result = PrimitiveState(true); 103 | } 104 | }; 105 | 106 | template <> 107 | struct action { 108 | static void apply0(GlobalParserState&, ParserState& state) { 109 | state.primitive_result = PrimitiveState(false); 110 | } 111 | }; 112 | 113 | template <> 114 | struct action { 115 | static void apply0(GlobalParserState&, ParserState& state) { 116 | // This function is called only if the BooleanLiteral rule passes, 117 | // otherwise it is a parsing failure. 118 | if (state.primitive_result.has_value()) { 119 | bool primitive_val = std::get(state.primitive_result.value()); 120 | state.result = Const(primitive_val); 121 | state.primitive_result = std::nullopt; 122 | } 123 | } 124 | }; 125 | 126 | template <> 127 | struct action { 128 | template 129 | static void apply(const ActionInput& in, GlobalParserState&, ParserState& state) { 130 | long int val = std::stol(in.string()); 131 | state.primitive_result = PrimitiveState(val); 132 | } 133 | }; 134 | 135 | template <> 136 | struct action { 137 | template 138 | static void apply(const ActionInput& in, GlobalParserState&, ParserState& state) { 139 | double val = std::stod(in.string()); 140 | state.primitive_result = PrimitiveState(val); 141 | } 142 | }; 143 | 144 | template <> 145 | struct action { 146 | static void apply0(GlobalParserState&, ParserState& state) { 147 | state.comparison_type = ast::ComparisonOp::LT; 148 | } 149 | }; 150 | 151 | template <> 152 | struct action { 153 | static void apply0(GlobalParserState&, ParserState& state) { 154 | state.comparison_type = ast::ComparisonOp::LE; 155 | } 156 | }; 157 | 158 | template <> 159 | struct action { 160 | static void apply0(GlobalParserState&, ParserState& state) { 161 | state.comparison_type = ast::ComparisonOp::GT; 162 | } 163 | }; 164 | 165 | template <> 166 | struct action { 167 | static void apply0(GlobalParserState&, ParserState& state) { 168 | state.comparison_type = ast::ComparisonOp::GE; 169 | } 170 | }; 171 | 172 | template <> 173 | struct action { 174 | static void apply0(GlobalParserState&, ParserState& state) { 175 | state.predicate_type = PredicateType::ONE; 176 | } 177 | }; 178 | 179 | template <> 180 | struct action { 181 | static void apply0(GlobalParserState&, ParserState& state) { 182 | state.predicate_type = PredicateType::TWO; 183 | } 184 | }; 185 | 186 | template <> 187 | struct action { 188 | static void apply0(GlobalParserState&, ParserState& state) { 189 | // Here, we assume that the parser succeeded in parsing 1 identifier and 1 190 | // numeral in the subtree, and now we can get their results from state. 191 | // Moreover, we assume that the sub-expressions set the predicate type and 192 | // the comparison type. 193 | 194 | if (state.identifiers.empty()) { 195 | throw std::logic_error("Predicate sub-parser did not parse any identifiers"); 196 | } 197 | auto id = state.identifiers.back(); // Get the last identifier. 198 | state.identifiers.pop_back(); // Remove it from the list. 199 | auto comparison_type = state.comparison_type.value(); 200 | state.comparison_type = std::nullopt; 201 | 202 | double const_val = 0.0; 203 | if (auto pval = std::get_if(&state.primitive_result.value())) { 204 | const_val = static_cast(*pval); 205 | } else { 206 | const_val = std::get(state.primitive_result.value()); 207 | } 208 | state.primitive_result = std::nullopt; 209 | 210 | if (state.predicate_type == PredicateType::TWO) { 211 | // For Type TWO, we need to flip the direction of the comparison 212 | switch (state.comparison_type.value()) { 213 | case ast::ComparisonOp::LT: 214 | comparison_type = ast::ComparisonOp::GT; 215 | break; 216 | case ast::ComparisonOp::LE: 217 | comparison_type = ast::ComparisonOp::GE; 218 | break; 219 | case ast::ComparisonOp::GT: 220 | comparison_type = ast::ComparisonOp::LT; 221 | break; 222 | case ast::ComparisonOp::GE: 223 | comparison_type = ast::ComparisonOp::LE; 224 | break; 225 | } 226 | } 227 | state.predicate_type = std::nullopt; 228 | 229 | state.result = ast::Predicate{id, comparison_type, const_val}; 230 | } 231 | }; 232 | 233 | template <> 234 | struct action { 235 | static void apply0(GlobalParserState&, ParserState& state) { 236 | assert(!state.terms.empty()); 237 | // Here we assume that the NotTerm rule matched a single Term as a 238 | // subexpression. This implies that there should be exactly 1 Expr in 239 | // states.terms 240 | state.result = ::signal_tl::Not(state.terms.back()); 241 | // We will remove that term after using it, and set Not(term) as the resultant 242 | // state. 243 | state.terms.pop_back(); 244 | // There should be exactly 0 terms left now. 245 | assert(state.terms.empty()); 246 | } 247 | }; 248 | 249 | template <> 250 | struct action : peg::require_apply { 251 | template 252 | static void apply(const ActionInput& in, GlobalParserState&, ParserState& state) { 253 | if (state.terms.size() < 2) { 254 | throw peg::parse_error( 255 | std::string("expected at least 2 terms, got ") + 256 | std::to_string(state.terms.size()), 257 | in); 258 | } 259 | // We expect the state to contain at least 2 terms due to pegtl::rep_min 260 | assert(state.terms.size() >= 2); 261 | // Then, we just std::move the vector of terms into the And expression. 262 | state.result = ::signal_tl::And(std::move(state.terms)); 263 | // There should be exactly 0 terms left now. 264 | assert(state.terms.empty()); 265 | } 266 | }; 267 | 268 | template <> 269 | struct action : peg::require_apply { 270 | template 271 | static void apply(const ActionInput& in, GlobalParserState&, ParserState& state) { 272 | if (state.terms.size() < 2) { 273 | throw peg::parse_error( 274 | std::string("expected at least 2 terms, got ") + 275 | std::to_string(state.terms.size()), 276 | in); 277 | } 278 | // We expect the state to contain at least 2 terms due to pegtl::rep_min 279 | assert(state.terms.size() >= 2); 280 | // Then, we just std::move the vector of terms into the Or expression. 281 | state.result = ::signal_tl::Or(std::move(state.terms)); 282 | // There should be exactly 0 terms left now. 283 | assert(state.terms.empty()); 284 | } 285 | }; 286 | 287 | template <> 288 | struct action { 289 | static void apply0(GlobalParserState&, ParserState& state) { 290 | // We expect the state to contain exactly 2 terms 291 | assert(state.terms.size() == 2); 292 | // TODO(anand): Verify the order of the Terms. 293 | auto rhs = state.terms.back(); 294 | state.terms.pop_back(); 295 | auto lhs = state.terms.back(); 296 | state.terms.pop_back(); 297 | state.result = ::signal_tl::Implies(lhs, rhs); 298 | // There should be exactly 0 terms left now. 299 | assert(state.terms.empty()); 300 | } 301 | }; 302 | 303 | template <> 304 | struct action { 305 | static void apply0(GlobalParserState&, ParserState& state) { 306 | // We expect the state to contain exactly 2 terms 307 | assert(state.terms.size() == 2); 308 | auto rhs = state.terms.back(); 309 | state.terms.pop_back(); 310 | auto lhs = state.terms.back(); 311 | state.terms.pop_back(); 312 | state.result = ::signal_tl::Iff(lhs, rhs); 313 | // There should be exactly 0 terms left now. 314 | assert(state.terms.empty()); 315 | } 316 | }; 317 | 318 | template <> 319 | struct action { 320 | static void apply0(GlobalParserState&, ParserState& state) { 321 | // We expect the state to contain exactly 2 terms 322 | assert(state.terms.size() == 2); 323 | auto rhs = state.terms.back(); 324 | state.terms.pop_back(); 325 | auto lhs = state.terms.back(); 326 | state.terms.pop_back(); 327 | state.result = ::signal_tl::Xor(lhs, rhs); 328 | // There should be exactly 0 terms left now. 329 | assert(state.terms.empty()); 330 | } 331 | }; 332 | 333 | template <> 334 | struct action { 335 | static void apply0(GlobalParserState&, ParserState& state) { 336 | assert(state.terms.size() == 1); 337 | // Here we assume that the AlwaysTerm rule matched a single Term as a 338 | // subexpression. This implies that there should be exactly 1 Expr in 339 | // states.terms 340 | // TODO(anand): Add support for intervals. 341 | state.result = ::signal_tl::Always(state.terms.back()); 342 | // We will remove that term after using it, and set Not(term) as the resultant 343 | // state. 344 | state.terms.pop_back(); 345 | // There should be exactly 0 terms left now. 346 | assert(state.terms.empty()); 347 | } 348 | }; 349 | 350 | template <> 351 | struct action { 352 | static void apply0(GlobalParserState&, ParserState& state) { 353 | assert(state.terms.size() == 1); 354 | // Here we assume that the EventuallyTerm rule matched a single Term as a 355 | // subexpression. This implies that there should be exactly 1 Expr in 356 | // states.terms 357 | // TODO(anand): Add support for intervals. 358 | state.result = ::signal_tl::Eventually(state.terms.back()); 359 | // We will remove that term after using it, and set Not(term) as the resultant 360 | // state. 361 | state.terms.pop_back(); 362 | // There should be exactly 0 terms left now. 363 | assert(state.terms.empty()); 364 | } 365 | }; 366 | 367 | template <> 368 | struct action { 369 | static void apply0(GlobalParserState&, ParserState& state) { 370 | // We expect the state to contain exactly 2 terms 371 | assert(state.terms.size() == 2); 372 | auto rhs = state.terms.back(); 373 | state.terms.pop_back(); 374 | auto lhs = state.terms.back(); 375 | state.terms.pop_back(); 376 | state.result = ::signal_tl::Until(lhs, rhs); 377 | // There should be exactly 0 terms left now. 378 | assert(state.terms.empty()); 379 | } 380 | }; 381 | 382 | template <> 383 | struct action { 384 | template < 385 | typename Rule, 386 | peg::apply_mode A, 387 | peg::rewind_mode M, 388 | template 389 | class Action, 390 | template 391 | class Control, 392 | typename ParseInput> 393 | static bool 394 | match(ParseInput& in, GlobalParserState& global_state, ParserState& state) { 395 | // Here, we implement a push-down parser. Essentially, the new_state was 396 | // pushed onto the stack before the parser entered the rule for Term, and 397 | // now we have to pop the top of the stack (new_state) and merge the top 398 | // smartly with the old_state. 399 | 400 | // Create a new layer on the stack 401 | ParserState new_local_state{}; 402 | new_local_state.level = state.level + 1; 403 | 404 | // Parse the input with the new state. 405 | bool ret = tao::pegtl::match( 406 | in, global_state, new_local_state); 407 | // Once we are done parsing, we need to reset the states. 408 | // After the apply0 for Term was completed, new_state should have 1 Term in 409 | // the vector. This term needs to be moved onto the Terms vector in the 410 | // old_state. 411 | state.terms.insert( 412 | std::end(state.terms), 413 | std::begin(new_local_state.terms), 414 | std::end(new_local_state.terms)); 415 | return ret; 416 | } 417 | 418 | static void apply0(GlobalParserState& global_state, ParserState& state) { 419 | // Here, we have two possibilities: 420 | // 421 | // 1. The Term was a valid expression; or 422 | // 2. The Term was an identifier. 423 | // 424 | // In the first case, once we have a resultant Expression, we should move 425 | // it to a vector so that it can be used by parent nodes in the AST. 426 | // 427 | // In the second case, we have to copy the expression pointed by the 428 | // identifier onto the terms vector. 429 | 430 | // If we have a Expression as the sub-result. 431 | if (state.result.has_value()) { 432 | // We move the result onto the vector of terms. 433 | state.terms.push_back(*state.result); 434 | // And invalidate the sub-result 435 | state.result = std::nullopt; 436 | } else if (!state.identifiers.empty()) { // And if we have an id 437 | // Copy the pointer to the formula with the corresponding id 438 | state.terms.push_back(global_state.formulas.at(state.identifiers.back())); 439 | state.identifiers.pop_back(); 440 | } else { 441 | // Otherwise, it doesn't make sense that there are no results, as this 442 | // means that the parser failed. This should be unreachable. 443 | // LCOV_EXCL_START 444 | throw std::logic_error( 445 | "Should be unreachable, but looks like a Term has no sub expression or identifier."); 446 | // LCOV_EXCL_STOP 447 | } 448 | } 449 | }; 450 | 451 | template <> 452 | struct action { 453 | template 454 | static void 455 | apply(const ActionInput& in, GlobalParserState& global_state, ParserState& state) { 456 | // For an assertion statement, we essentially will have 1 identifier, and 1 457 | // term. Thus, the identifier will be in the result, and the number of 458 | // terms must be 1. 459 | assert(state.terms.size() == 1); 460 | assert(!state.identifiers.empty()); 461 | 462 | auto id = state.identifiers.back(); // Must be there 463 | state.identifiers.pop_back(); 464 | auto expr = state.terms.back(); // Must be there 465 | state.terms.pop_back(); 466 | 467 | const auto [it, check] = global_state.assertions.insert({id, expr}); 468 | if (!check) { // Unsuccessful insert. Probably due to id already being there 469 | throw peg::parse_error( 470 | fmt::format("possible redefinition of Assertion with id: \"{}\"", it->first), 471 | in); 472 | } 473 | 474 | assert(state.terms.empty()); 475 | } 476 | }; 477 | 478 | template <> 479 | struct action { 480 | template 481 | static void 482 | apply(const ActionInput& in, GlobalParserState& global_state, ParserState& state) { 483 | // For an define-formula command, we essentially will have 1 identifier, 484 | // and 1 term. Thus, the identifier will be in the result, and the number 485 | // of terms must be 1. 486 | assert(state.terms.size() == 1); 487 | assert(!state.identifiers.empty()); 488 | 489 | auto id = state.identifiers.back(); // Must be there 490 | state.identifiers.pop_back(); 491 | auto expr = state.terms.back(); // Must be there 492 | state.terms.pop_back(); 493 | 494 | const auto [it, check] = global_state.formulas.insert({id, expr}); 495 | if (!check) { // Unsuccessful insert. Probably due to id already being there 496 | throw peg::parse_error( 497 | fmt::format("possible redefinition of Formula with id: \"{}\"", it->first), 498 | in); 499 | } 500 | 501 | assert(state.terms.empty()); 502 | } 503 | }; 504 | 505 | } // namespace actions 506 | 507 | using actions::action; 508 | } // namespace signal_tl::parser 509 | 510 | #endif /* end of include guard: SIGNALTL_PARSER_ACTIONS_HPP */ 511 | -------------------------------------------------------------------------------- /src/parser/error_messages.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef SIGNALTL_PARSER_ERRORS_HPP 4 | #define SIGNALTL_PARSER_ERRORS_HPP 5 | 6 | #include "grammar.hpp" 7 | 8 | namespace signal_tl::parser { 9 | namespace peg = tao::pegtl; 10 | 11 | namespace error_messages { 12 | 13 | /// In this file we define the exceptions/error messages that must be 14 | /// thrown/shown when some rule encounters a parsing error. 15 | using namespace signal_tl::grammar; 16 | 17 | /// The default error message should be nothing. Then, we will override the 18 | /// messages for specific rules. 19 | template 20 | inline constexpr const char* error_message = nullptr; 21 | 22 | template <> 23 | inline constexpr auto error_message = "expected whitespace or comment"; 24 | 25 | // template <> 26 | // inline constexpr auto error_message = "expected identifier"; 27 | 28 | template <> 29 | inline constexpr auto error_message = "expected an identifier"; 30 | 31 | template <> 32 | inline constexpr auto error_message< 33 | peg::seq>, peg::plus>> = 34 | "expected some (signed) number after exponent"; 35 | 36 | template <> 37 | inline constexpr auto error_message = 38 | "expected an identifier followed by a numeral or vice-versa"; 39 | 40 | template <> 41 | inline constexpr auto error_message = "expected a list of at least 2 Terms"; 42 | 43 | template <> 44 | inline constexpr auto error_message = 45 | "expected a list of at exactly 2 Terms"; 46 | 47 | template <> 48 | inline constexpr auto error_message = 49 | "expected an expression followed by a closing parenthesis ')'"; 50 | 51 | template <> 52 | inline constexpr auto error_message = "expected a Term"; 53 | 54 | template <> 55 | inline constexpr auto error_message = 56 | "top-level command does not match list of known commands"; 57 | 58 | template <> 59 | inline constexpr auto error_message = 60 | "expected ( ...). Maybe you have an unclosed S-expression command."; 61 | 62 | template <> 63 | inline constexpr auto error_message = "invalid top-level item"; 64 | } // namespace error_messages 65 | 66 | struct error { 67 | template 68 | static constexpr auto message = error_messages::error_message; 69 | 70 | /// This is used to prevent local failues in the Term rule from becoming 71 | /// global failures. 72 | template 73 | static constexpr bool raise_on_failure = false; 74 | }; 75 | 76 | template 77 | using control = peg::must_if::control; 78 | 79 | } // namespace signal_tl::parser 80 | 81 | #endif /* end of include guard: SIGNALTL_PARSER_ERRORS_HPP */ 82 | -------------------------------------------------------------------------------- /src/parser/grammar.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef SIGNALTL_GRAMMAR_HPP 4 | #define SIGNALTL_GRAMMAR_HPP 5 | 6 | #include // IWYU pragma: keep 7 | 8 | namespace signal_tl::grammar { 9 | /// Here, we define the grammar for a Lisp-y specification format using 10 | /// S-expressions. This is inspired, in part, by the SMT-LIB. 11 | namespace peg = tao::pegtl; 12 | 13 | /// Line comments that starts using the character ';' 14 | struct LineComment : peg::disable, peg::until> {}; 15 | 16 | /// Any horizontal white space or line comment 17 | /// NOTE(anand): I am also going to include EOL as a a skippable comment. 18 | struct Sep : peg::disable> {}; 19 | struct Skip : peg::disable> {}; 20 | struct EndOfWord : peg::seq, Skip> {}; 21 | 22 | struct Identifier : peg::seq {}; 23 | 24 | /// Some symbols 25 | template 26 | struct Symbol : peg::seq {}; 27 | struct LParen : Symbol> {}; 28 | struct RParen : Symbol> {}; 29 | struct LtSymbol : Symbol, peg::not_at'>, peg::not_at', '='>>> {}; 32 | struct GeSymbol : Symbol=")> {}; 33 | 34 | /// Keywords are one the following. 35 | template 36 | struct Keyword : peg::seq {}; 37 | 38 | struct KwTrue : Keyword {}; 39 | struct KwFalse : Keyword {}; 40 | 41 | struct KwNot : Keyword {}; 42 | struct KwAnd : Keyword {}; 43 | struct KwOr : Keyword {}; 44 | struct KwImplies : Keyword {}; 45 | struct KwIff : Keyword {}; 46 | struct KwXor : Keyword {}; 47 | struct KwAlways : Keyword {}; 48 | struct KwEventually : Keyword {}; 49 | struct KwUntil : Keyword {}; 50 | 51 | struct KwDefineFormula : Keyword {}; 52 | struct KwAssert : Keyword {}; 53 | 54 | // First we define some helpers 55 | struct decimal_seq : peg::plus {}; 56 | 57 | template 58 | struct exponent 59 | : peg::if_must>, peg::plus>> {}; 60 | 61 | struct DoubleLiteral : peg::seq< 62 | peg::sor< 63 | peg::seq< 64 | decimal_seq, 65 | peg::one<'.'>, 66 | decimal_seq, 67 | peg::opt>>>, 68 | peg::seq< 69 | decimal_seq, 70 | peg::opt>, 71 | exponent>>>, 72 | Skip> {}; 73 | struct IntegerLiteral : peg::seq {}; 74 | struct BooleanLiteral : peg::sor {}; 75 | 76 | /// Primitive types are one of `double`, `int`, or `bool`. 77 | /// 78 | /// NOTE: 79 | /// 80 | /// The rule for Double must come before Integer as the latter partially 81 | /// matches the former. 82 | struct Numeral : peg::sor {}; 83 | struct Constant : peg::sor {}; 84 | 85 | /// Forward Declaration of Term fore recursion in Expression. 86 | struct Term; 87 | 88 | /// A predicate term is an expression of the form `(~ x c)` or `(~ c x) where, 89 | /// `x` is a signal identifier, `~` is a relational operation, and `c` is a 90 | /// numeric constant (double or integer). 91 | /// 92 | /// TODO(anand): No support for arithmetic expressions of signals. Must be 93 | /// implemented in userland. 94 | struct ComparisonSymbol : peg::sor {}; 95 | struct PredicateForm1 : peg::seq {}; 96 | struct PredicateForm2 : peg::seq {}; 97 | struct PredicateForm : peg::sor {}; 98 | struct PredicateTerm 99 | : peg::if_must, PredicateForm> {}; 100 | 101 | struct NotTerm : peg::if_must {}; 102 | 103 | /// Tail for N-ary operations like AND and OR. 104 | /// 105 | /// NOTE 106 | /// ---- 107 | /// 108 | /// 2021-01-21 (anand): 109 | /// 110 | /// - Currently, this is weirdly bugged. When the rule fails to match the third 111 | /// Term, it throws a global error. 112 | /// 113 | /// 2021-01-22 (anand): 114 | /// 115 | /// - I guess I should have reworded the above note by saying that when the 116 | /// internal peg::star combinator reaches the last Term and fails to find the 117 | /// next Term, then what should be a success (because that is how peg::star 118 | /// operates) it becomes a global error. 119 | /// - Another interesting issue is that when the error happens, it is not an 120 | /// issue with the peg::must condition in AndTerm/OrTerm, but rather within 121 | /// the peg::sor condition in Term. This implies that the peg::sor is 122 | /// becoming a global error. 123 | /// - HACK: I edited the match function for Term to return true if there is at 124 | /// least 1 Term in the old stack. Then, the parent rule (And/Or) can check 125 | /// if there are enough Terms. 126 | /// - Above hack doesn't work. Because I am an idiot. See [PEGTL errors] for 127 | /// the detailt about when a rule contains a custom error message, local 128 | /// erros are automatically converted to global errors even if there is no 129 | /// peg::must rule. This is something simple that I overlooked! 130 | struct NaryTail : peg::plus {}; 131 | struct BinaryTail : peg::seq {}; 132 | 133 | struct AndTerm : peg::if_must {}; 134 | struct OrTerm : peg::if_must {}; 135 | struct ImpliesTerm : peg::if_must {}; 136 | struct IffTerm : peg::if_must {}; 137 | struct XorTerm : peg::if_must {}; 138 | 139 | // TODO(anand): Temporal operations must allow an optional Interval argument. 140 | struct AlwaysTerm : peg::if_must {}; 141 | struct EventuallyTerm : peg::if_must {}; 142 | struct UntilTerm : peg::if_must {}; 143 | 144 | /// An Expression must be a valid STL formula (without specific functions for 145 | /// the predicates). So, we will hard code the syntax and we don't have to 146 | /// worry about precedence as S-expressions FTW! 147 | using Expression = peg::sor< 148 | PredicateTerm, 149 | NotTerm, 150 | AndTerm, 151 | OrTerm, 152 | ImpliesTerm, 153 | IffTerm, 154 | XorTerm, 155 | AlwaysTerm, 156 | EventuallyTerm, 157 | UntilTerm, 158 | Term>; 159 | 160 | using TermTail = peg::until; 161 | struct Term : peg::sor, BooleanLiteral, Identifier> {}; 162 | 163 | struct Assertion : peg::if_must {}; 164 | struct DefineFormula : peg::if_must {}; 165 | 166 | /// The list of commands includes 167 | /// 168 | /// - Assertion: `(assert )` 169 | /// 170 | /// Here, we will use the `` to refer to the assertion rule 171 | /// (essentially the semantics) from the program. This is different from most 172 | /// other languages that have an `assert` statement, where it is a runtime 173 | /// thing. 174 | /// 175 | /// NOTE: We should eventually have this file run as a script. 176 | /// 177 | /// - Define Formula `(define-formula )` 178 | /// 179 | /// This is straightforward enough: we define a formula (named using 180 | /// ``) as some expression. 181 | using AllowedCommands = peg::sor; 182 | struct Command : peg::must {}; 183 | 184 | /// Commands are top level S-expressions with the syntax: 185 | /// 186 | /// ``` 187 | /// ::= '(' * ')' 188 | /// ``` 189 | /// We will hard code the top level commands, like `define-formula` and 190 | /// `assert` as that will allow us to directly reason about them. A command is 191 | /// essentially intrinsic to the specification language. 192 | using AnyCommandTail = peg::until; 193 | struct AnyCommand : peg::if_must {}; 194 | 195 | struct StatementList : peg::seq> {}; 196 | 197 | /// A specification essentially consists for a list of top level commands, 198 | /// andwe are just gonna ignore all horizontal spaces 199 | /* struct TopLevel : peg::pad {}; */ 200 | struct SpecificationFile : peg::must {}; 201 | 202 | } // namespace signal_tl::grammar 203 | 204 | #endif /* end of include guard: SIGNALTL_GRAMMAR_HPP */ 205 | -------------------------------------------------------------------------------- /src/parser/parser.cc: -------------------------------------------------------------------------------- 1 | #include "signal_tl/parser.hpp" 2 | #include "actions.hpp" 3 | #include "error_messages.hpp" // for control 4 | #include "grammar.hpp" // for SpecificationFile 5 | 6 | #include // for analyze 7 | #include // for standard_trace 8 | #include // for nothing 9 | #include // for parse 10 | 11 | #include // for logic_error 12 | 13 | // #define NDEBUG 14 | #include 15 | 16 | namespace { 17 | 18 | namespace peg = tao::pegtl; 19 | using namespace signal_tl; 20 | 21 | template 22 | std::unique_ptr _parse(ParseInput&& input) { 23 | // bool success = 24 | // peg::parse(input); 25 | auto global_state = parser::actions::GlobalParserState{}; 26 | auto top_local_state = parser::actions::ParserState{}; 27 | top_local_state.level = 0; 28 | bool success = 29 | peg::parse( 30 | input, global_state, top_local_state); 31 | if (success) { 32 | assert(top_local_state.level == 0); 33 | auto spec = std::make_unique( 34 | std::move(global_state.formulas), std::move(global_state.assertions)); 35 | return spec; 36 | } else { 37 | // LCOV_EXCL_START 38 | throw std::logic_error( 39 | "Local error thrown during parsing of input. This is most likely a bug."); 40 | // LCOV_EXCL_STOP 41 | } 42 | } 43 | } // namespace 44 | 45 | namespace signal_tl::parser { 46 | 47 | std::unique_ptr from_string(std::string_view input) { 48 | tao::pegtl::string_input in(input, "from_content"); 49 | return _parse(in); 50 | } 51 | 52 | std::unique_ptr from_file(const stdfs::path& input) { 53 | tao::pegtl::file_input in(input); 54 | return _parse(in); 55 | } 56 | 57 | } // namespace signal_tl::parser 58 | 59 | // LCOV_EXCL_START 60 | namespace signal_tl::grammar::internal { 61 | 62 | size_t analyze(int verbose) { 63 | return peg::analyze(verbose); 64 | } 65 | 66 | bool trace_from_file(const stdfs::path& input_path) { 67 | peg::file_input in(input_path); 68 | auto global_state = parser::actions::GlobalParserState{}; 69 | auto top_local_state = parser::actions::ParserState{}; 70 | top_local_state.level = 0; 71 | return peg::standard_trace( 72 | in, global_state, top_local_state); 73 | } 74 | // LCOV_EXCL_STOP 75 | 76 | } // namespace signal_tl::grammar::internal 77 | -------------------------------------------------------------------------------- /src/robust_semantics/classic_robustness.cc: -------------------------------------------------------------------------------- 1 | #include "signal_tl/ast.hpp" 2 | #include "signal_tl/exception.hpp" 3 | #include "signal_tl/robustness.hpp" 4 | #include "signal_tl/signal.hpp" 5 | 6 | #include "minmax.hpp" 7 | 8 | #include // for max, min, transform, for_each 9 | #include // for assert 10 | #include // for isinf 11 | #include // for negate 12 | #include // for back_insert_iterator, back_inserter 13 | #include // for numeric_limits 14 | #include // for operator!= 15 | #include // for __shared_ptr_access, make_shared 16 | #include // for logic_error 17 | #include // for string 18 | #include // for make_tuple, tuple_element<>::type 19 | #include // for tuple_element<>::type, pair 20 | #include // for visit 21 | #include // for vector 22 | 23 | namespace signal_tl::semantics { 24 | using namespace signal; 25 | using namespace minmax; 26 | 27 | namespace { 28 | constexpr double TOP = std::numeric_limits::infinity(); 29 | constexpr double BOTTOM = -TOP; 30 | 31 | SignalPtr compute_until(const SignalPtr& input_x, const SignalPtr& input_y) { 32 | const auto [x, y] = synchronize(input_x, input_y); 33 | assert(x->size() == y->size()); 34 | assert(x->begin_time() == y->begin_time()); 35 | assert(x->end_time() == y->end_time()); 36 | 37 | auto sigstack = std::vector(); 38 | 39 | // TODO(anand): This doesn't handle crossing signals well... 40 | 41 | double prev = TOP; 42 | double max_right = BOTTOM; 43 | 44 | for (auto [i, j] = std::make_tuple(x->rbegin(), y->rbegin()); 45 | i != x->rend() && j != y->rend(); 46 | i++, j++) { 47 | max_right = std::max(max_right, j->value); 48 | prev = std::max({j->value, std::min(i->value, prev), -max_right}); 49 | sigstack.push_back({i->time, prev}); 50 | } 51 | std::reverse(sigstack.begin(), sigstack.end()); 52 | auto out = std::make_shared(sigstack); 53 | return out; 54 | } 55 | 56 | SignalPtr compute_until(const SignalPtr&, const SignalPtr&, double, double) { 57 | throw not_implemented_error("Bounded compute_until has not been implemented yet."); 58 | } 59 | 60 | struct RobustnessOp { 61 | double min_time = 0.0; 62 | double max_time = std::numeric_limits::infinity(); 63 | Trace trace = {}; 64 | 65 | RobustnessOp() = default; 66 | 67 | SignalPtr operator()(const ast::Const e) const; 68 | SignalPtr operator()(const ast::Predicate& e) const; 69 | SignalPtr operator()(const ast::NotPtr& e) const; 70 | SignalPtr operator()(const ast::AndPtr& e) const; 71 | SignalPtr operator()(const ast::OrPtr& e) const; 72 | SignalPtr operator()(const ast::EventuallyPtr& e) const; 73 | SignalPtr operator()(const ast::AlwaysPtr& e) const; 74 | SignalPtr operator()(const ast::UntilPtr& e) const; 75 | }; 76 | 77 | SignalPtr compute(const ast::Expr& phi, const RobustnessOp& rob) { 78 | return std::visit([&](auto&& e) { return rob(e); }, phi); 79 | } 80 | 81 | } // namespace 82 | 83 | SignalPtr compute_robustness(const ast::Expr& phi, const signal::Trace& trace, bool) { 84 | // Compute the start and end of the trace. 85 | struct MinMaxTime { 86 | double begin{TOP}; 87 | double end{BOTTOM}; 88 | void operator()(const std::pair& entry) { 89 | auto s = entry.second; 90 | begin = std::min(begin, s->begin_time()); 91 | end = std::max(end, s->end_time()); 92 | } 93 | }; 94 | 95 | const MinMaxTime minmaxtime = 96 | std::for_each(trace.cbegin(), trace.cend(), MinMaxTime{}); 97 | double min_time = minmaxtime.begin; 98 | double max_time = minmaxtime.end; 99 | 100 | auto rob = RobustnessOp{min_time, max_time, trace}; 101 | 102 | SignalPtr out = compute(phi, rob); 103 | 104 | return out; 105 | } 106 | 107 | SignalPtr RobustnessOp::operator()(const ast::Const e) const { 108 | const double val = (e.value) ? static_cast(TOP) : static_cast(BOTTOM); 109 | auto samples = std::vector{{min_time, val, 0.0}, {max_time, val, 0.0}}; 110 | return std::make_shared(samples); 111 | } 112 | 113 | SignalPtr RobustnessOp::operator()(const ast::Predicate& e) const { 114 | const auto& x = trace.at(e.name); 115 | auto y = std::make_shared(); 116 | for (const auto& sample : *x) { 117 | const double t = sample.time; 118 | const double v = sample.value; 119 | switch (e.op) { 120 | case ast::ComparisonOp::GE: 121 | case ast::ComparisonOp::GT: 122 | y->push_back(t, v - e.rhs); 123 | break; 124 | case ast::ComparisonOp::LE: 125 | case ast::ComparisonOp::LT: 126 | y->push_back(t, e.rhs - v); 127 | break; 128 | } 129 | } 130 | return y; 131 | } 132 | 133 | SignalPtr RobustnessOp::operator()(const ast::NotPtr& e) const { 134 | auto x = compute(e->arg, *this); 135 | auto vec = std::vector{}; 136 | vec.reserve(x->size()); 137 | std::transform(x->begin(), x->end(), std::back_inserter(vec), std::negate<>()); 138 | return std::make_shared(vec); 139 | } 140 | 141 | SignalPtr RobustnessOp::operator()(const ast::AndPtr& e) const { 142 | auto ys = std::vector{}; 143 | ys.reserve(e->args.size()); 144 | std::transform( 145 | e->args.begin(), e->args.end(), std::back_inserter(ys), [this](const auto arg) { 146 | return compute(arg, *this); 147 | }); 148 | assert(ys.size() == e->args.size()); 149 | return compute_elementwise_min(ys); 150 | } 151 | 152 | SignalPtr RobustnessOp::operator()(const ast::OrPtr& e) const { 153 | auto ys = std::vector{}; 154 | ys.reserve(e->args.size()); 155 | std::transform( 156 | e->args.begin(), e->args.end(), std::back_inserter(ys), [this](const auto arg) { 157 | return compute(arg, *this); 158 | }); 159 | assert(ys.size() == e->args.size()); 160 | return compute_elementwise_max(ys); 161 | } 162 | 163 | SignalPtr RobustnessOp::operator()(const ast::EventuallyPtr& e) const { 164 | auto y = compute(e->arg, *this); 165 | if (!e->interval.has_value()) { 166 | return compute_max_seq(y); 167 | } 168 | 169 | const auto [a, b] = e->interval.as_double(); 170 | if (b - a < 0) { 171 | throw std::logic_error("Eventually operator: b < a in interval [a,b]"); 172 | } else if (b - a == 0) { 173 | return y; 174 | } else if (b - a >= y->end_time() - y->begin_time()) { 175 | return compute_max_seq(y); 176 | } else { 177 | return compute_max_seq(y, a, b); 178 | } 179 | } 180 | 181 | SignalPtr RobustnessOp::operator()(const ast::AlwaysPtr& e) const { 182 | auto y = compute(e->arg, *this); 183 | if (!e->interval.has_value()) { 184 | return compute_min_seq(y); 185 | } 186 | 187 | const auto [a, b] = e->interval.as_double(); 188 | if (b - a < 0) { 189 | throw std::logic_error("Always operator: b < a in interval [a,b]"); 190 | } else if (b - a == 0) { 191 | return y; 192 | } else if (b - a >= y->end_time() - y->begin_time()) { 193 | return compute_min_seq(y); 194 | } else { 195 | return compute_min_seq(y, a, b); 196 | } 197 | } 198 | 199 | SignalPtr RobustnessOp::operator()(const ast::UntilPtr& e) const { 200 | auto y1 = compute(e->args.first, *this); 201 | auto y2 = compute(e->args.second, *this); 202 | if (!e->interval.has_value()) { 203 | return compute_until(y1, y2); 204 | } 205 | 206 | const auto [a, b] = e->interval.as_double(); 207 | if (std::isinf(b) && a == 0) { 208 | return compute_until(y1, y2); 209 | } else { 210 | return compute_until(y1, y2, a, b); 211 | } 212 | } 213 | 214 | } // namespace signal_tl::semantics 215 | -------------------------------------------------------------------------------- /src/robust_semantics/minmax.cc: -------------------------------------------------------------------------------- 1 | #include "minmax.hpp" 2 | #include "mono_wedge.h" // for mono_wedge_update 3 | 4 | #include // for max, reverse 5 | #include // for _Deque_iterator, deque, operator- 6 | #include // for greater_equal, less_equal 7 | #include // for prev, next, begin 8 | #include // for numeric_limits 9 | #include // for __shared_ptr_access, make_shared 10 | #include // for accumulate 11 | #include // for make_tuple, tuple_element<>::type 12 | #include // for tuple_element<>::type 13 | 14 | #include // for assert 15 | 16 | namespace signal_tl::minmax { 17 | using namespace signal; 18 | 19 | template 20 | SignalPtr compute_minmax_pair( 21 | const SignalPtr& input_x, 22 | const SignalPtr& input_y, 23 | Compare comp, 24 | bool synchronized) { 25 | const auto [x, y] = (synchronized) ? std::make_tuple(input_x, input_y) 26 | : synchronize(input_x, input_y); 27 | assert(x->size() == y->size()); 28 | assert(x->begin_time() == y->begin_time()); 29 | 30 | assert(x->end_time() == y->end_time()); 31 | 32 | // Used to keep track of the signal from which the last minmax winner was chosen. 33 | enum struct Chosen { X, Y, NONE }; 34 | Chosen last_chosen = Chosen::NONE; 35 | 36 | auto out = std::make_shared(); 37 | 38 | for (auto [i, j] = std::make_tuple(x->begin(), y->begin()); 39 | i != x->end() && j != y->end(); 40 | i++, j++) { 41 | if (comp(*i, *j)) { 42 | if (last_chosen == Chosen::Y) { 43 | double intercept_time = std::prev(j)->time_intersect(*std::prev(i)); 44 | if (intercept_time > out->end_time() && intercept_time != i->time) { 45 | out->push_back( 46 | Sample{intercept_time, std::prev(j)->interpolate(intercept_time)}); 47 | } 48 | } 49 | out->push_back(*i); 50 | last_chosen = Chosen::X; 51 | } else { 52 | if (last_chosen == Chosen::X) { 53 | double intercept_time = std::prev(i)->time_intersect(*std::prev(j)); 54 | if (intercept_time > out->end_time() && intercept_time != j->time) { 55 | out->push_back( 56 | Sample{intercept_time, std::prev(i)->interpolate(intercept_time)}); 57 | } 58 | } 59 | out->push_back(*j); 60 | last_chosen = Chosen::Y; 61 | } 62 | } 63 | 64 | return out; 65 | } 66 | 67 | template 68 | SignalPtr 69 | compute_minmax_pair(const std::vector& xs, Compare comp, bool synchronized) { 70 | if (xs.empty()) { 71 | auto out = std::make_shared(); 72 | out->push_back(0, -std::numeric_limits::infinity()); 73 | return out; 74 | } else if (xs.size() == 1) { 75 | return xs.at(0); 76 | } else if (xs.size() == 2) { 77 | return compute_minmax_pair(xs[0], xs[1], comp, synchronized); 78 | } 79 | 80 | // TODO(anand): Parallel execution policy? 81 | SignalPtr out = std::accumulate( 82 | std::next(xs.cbegin()), 83 | xs.cend(), 84 | xs.at(0), 85 | [&comp, &synchronized](const SignalPtr a, const SignalPtr b) { 86 | return compute_minmax_pair(a, b, comp, synchronized); 87 | }); 88 | return out; 89 | } 90 | 91 | template 92 | SignalPtr compute_minmax_seq(const SignalPtr& x, Compare comp) { 93 | auto opt = x->back(); 94 | auto z = std::vector{}; 95 | z.reserve(2 * x->size()); 96 | z.push_back(x->back()); 97 | 98 | for (auto i = std::next(x->rbegin()); i != x->rend(); i++) { 99 | opt = (comp(*i, opt)) ? *i : opt; 100 | z.push_back({i->time, opt.value}); 101 | } 102 | 103 | std::reverse(z.begin(), z.end()); 104 | return std::make_shared(z); 105 | } 106 | 107 | template 108 | SignalPtr compute_minmax_seq(const SignalPtr& x, double a, double b, Compare comp) { 109 | const auto width = b - a; 110 | const auto begin_time = x->begin_time(); 111 | const auto end_time = x->end_time(); 112 | 113 | auto x_ = (a == 0) 114 | ? x 115 | : x->resize_shift( 116 | begin_time + width, end_time + width, x->back().value, -width); 117 | 118 | auto z = std::make_shared(); 119 | auto samples = std::deque{x_->begin(), x_->end()}; 120 | auto window = std::deque{}; 121 | 122 | auto i = std::begin(samples); 123 | // -- Read in values in [0, width) 124 | for (; i->time < x->begin_time() + width; i++) { 125 | mono_wedge::mono_wedge_update(window, *i, comp); 126 | } 127 | 128 | // -- Stream in the rest of the signal. 129 | for (; i != samples.end(); i++) { 130 | if (i->time - a > window.front().time) { 131 | // Add the sample for time M.front + a 132 | i = samples.insert( 133 | i, 134 | Sample{ 135 | window.front().time + a, 136 | std::prev(i)->interpolate(window.front().time + a), 137 | 0.0}); 138 | } 139 | mono_wedge::mono_wedge_update(window, *i, comp); 140 | while (window.front().time <= i->time - a) { window.pop_front(); } 141 | z->push_back(i->time, i->value); 142 | } 143 | 144 | return z->simplify(); 145 | } 146 | 147 | SignalPtr 148 | compute_elementwise_min(const SignalPtr& x, const SignalPtr& y, bool synchronized) { 149 | return compute_minmax_pair(x, y, std::less_equal<>(), synchronized); 150 | } 151 | 152 | SignalPtr 153 | compute_elementwise_max(const SignalPtr& x, const SignalPtr& y, bool synchronized) { 154 | return compute_minmax_pair(x, y, std::greater_equal<>(), synchronized); 155 | } 156 | 157 | SignalPtr compute_elementwise_min(const std::vector& xs, bool synchronized) { 158 | return compute_minmax_pair(xs, std::less_equal<>(), synchronized); 159 | } 160 | 161 | SignalPtr compute_elementwise_max(const std::vector& xs, bool synchronized) { 162 | return compute_minmax_pair(xs, std::greater_equal<>(), synchronized); 163 | } 164 | 165 | SignalPtr compute_max_seq(const SignalPtr& x) { 166 | return compute_minmax_seq(x, std::greater_equal<>()); 167 | } 168 | 169 | SignalPtr compute_min_seq(const SignalPtr& x) { 170 | return compute_minmax_seq(x, std::less_equal<>()); 171 | } 172 | 173 | SignalPtr compute_max_seq(const SignalPtr& x, double a, double b) { 174 | return compute_minmax_seq(x, a, b, std::greater_equal<>()); 175 | } 176 | 177 | SignalPtr compute_min_seq(const SignalPtr& x, double a, double b) { 178 | return compute_minmax_seq(x, a, b, std::less_equal<>()); 179 | } 180 | 181 | } // namespace signal_tl::minmax 182 | -------------------------------------------------------------------------------- /src/robust_semantics/minmax.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SIGNAL_TEMPORAL_LOGIC_MINMAX_HPP 2 | #define SIGNAL_TEMPORAL_LOGIC_MINMAX_HPP 3 | 4 | #include "signal_tl/signal.hpp" 5 | 6 | #include 7 | 8 | namespace signal_tl::minmax { 9 | 10 | /** 11 | * Compute the element-wise minimum/maximum (depending on value of Compare) between 12 | * two signals. 13 | */ 14 | template 15 | signal::SignalPtr compute_minmax_pair( 16 | const signal::SignalPtr& input_x, 17 | const signal::SignalPtr& input_y, 18 | Compare comp, 19 | bool synchronized = false); 20 | 21 | signal::SignalPtr compute_elementwise_min( 22 | const signal::SignalPtr& x, 23 | const signal::SignalPtr& y, 24 | bool synchronized = false); 25 | 26 | signal::SignalPtr compute_elementwise_max( 27 | const signal::SignalPtr& x, 28 | const signal::SignalPtr& y, 29 | bool synchronized = false); 30 | 31 | /** 32 | * Compute the element-wise minimum/maximum (depending on value of Compare) between 33 | * multiple signals. 34 | */ 35 | template 36 | signal::SignalPtr compute_minmax_pair( 37 | const std::vector& xs, 38 | Compare comp, 39 | bool synchronized = false); 40 | 41 | signal::SignalPtr compute_elementwise_min( 42 | const std::vector& xs, 43 | bool synchronized = false); 44 | 45 | signal::SignalPtr compute_elementwise_max( 46 | const std::vector& xs, 47 | bool synchronized = false); 48 | 49 | /** 50 | * Compute the rolling min/max of a signal, i.e., at time t, the min/max value is the 51 | * sample with min/max value in the window [t, t + inf). 52 | */ 53 | template 54 | signal::SignalPtr compute_minmax_seq(const signal::SignalPtr& x, Compare comp); 55 | 56 | signal::SignalPtr compute_max_seq(const signal::SignalPtr& x); 57 | signal::SignalPtr compute_min_seq(const signal::SignalPtr& x); 58 | 59 | /** 60 | * Compute the windowed min/max of a signal, i.e., at time t, the min/max value is the 61 | * sample with min/max value in the window [t + a, t + b]. 62 | */ 63 | template 64 | signal::SignalPtr 65 | compute_minmax_seq(const signal::SignalPtr& x, double a, double b, Compare comp); 66 | 67 | signal::SignalPtr compute_max_seq(const signal::SignalPtr& x, double a, double b); 68 | signal::SignalPtr compute_min_seq(const signal::SignalPtr& x, double a, double b); 69 | 70 | } // namespace signal_tl::minmax 71 | 72 | #endif 73 | -------------------------------------------------------------------------------- /src/robust_semantics/mono_wedge.h: -------------------------------------------------------------------------------- 1 | /* 2 | This code is available under the MIT license: 3 | 4 | Copyright (c) 2016 Evan Balster 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 7 | software and associated documentation files (the "Software"), to deal in the 8 | Software without restriction, including without limitation the rights to use, copy, 9 | modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 10 | and to permit persons to whom the Software is furnished to do so, subject to the 11 | following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 17 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 20 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 21 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | 24 | The original Lemire algorithm is "patent-free". For more information on the Lemire 25 | algorithm: 26 | 27 | Code: https://github.com/lemire/runningmaxmin 28 | 29 | Paper: https://arxiv.org/abs/cs/0610046 30 | */ 31 | 32 | #ifndef MONOTONIC_WEDGE_H 33 | #define MONOTONIC_WEDGE_H 34 | 35 | #include 36 | #include 37 | 38 | #if __cplusplus > 199711L 39 | #include // For std::forward 40 | #endif 41 | 42 | /* 43 | This header presents algorithms for fast running minimum and maximum using 44 | the Daniel Lemire monotonic wedge algorithm with enhancements proposed 45 | by Ethan Fenn. 46 | 47 | The algorithm here is modeled on the C++ STL style and meant to be used 48 | with vector, deque or ring-buffer structures. 49 | 50 | The amortized complexity of the update operation is constant; 51 | IE, N updates can be performed in linear time on a given wedge. 52 | The worst-case complexity for a single update is below log2(N). 53 | 54 | 55 | Usage recommendations: 56 | 57 | This algorithm is most useful for "rolling" min / max evaluation. 58 | Most applications will thus prefer deques or ring-buffers as wedges. 59 | 60 | Generally, values beyond a certain age should be popped to limit the 61 | size of the wedge, though amortized complexity will remain linear even 62 | if this is not done. 63 | 64 | The wedge must be monotonic at all times with respect to Compare, 65 | EG. by only modifying the structure with wedge_update and pop_front. 66 | 67 | 68 | See bottom for (MIT) license and IP remarks. 69 | */ 70 | 71 | namespace mono_wedge { 72 | /* 73 | mono_wedge_search(begin, end, value, comp) 74 | 75 | Search routine used to determine deletion range in mono_wedge_update. 76 | 77 | Similar to std::lower_bound, returns first element in range for which 78 | comp(value, element) returns false. 79 | 80 | Range must be sorted with regard to comp. 81 | Iterator must be a random access iterator. 82 | Complexity is below log2(N) with respect to wedge size. 83 | Facilitates amortized constant complexity in mono_wedge_update. 84 | */ 85 | template 86 | Iterator mono_wedge_search(Iterator begin, Iterator end, const T& value, Compare comp) { 87 | auto size = static_cast(std::distance(begin, end)); 88 | if (size <= 0ul) 89 | return end; 90 | 91 | // Linear search through at most J elements, where J = log2(N-J). 92 | Iterator search_pos = end; 93 | --search_pos; 94 | size_t i = 1ul; 95 | for (; ((size - i) >> i) > 0ul; ++i, --search_pos) { 96 | if (comp(*search_pos, value)) 97 | return ++search_pos; 98 | } 99 | 100 | // Afterwards run a binary search (use std::lower_bound) 101 | return std::lower_bound(begin, ++search_pos, value, comp); 102 | } 103 | 104 | /* 105 | mono_wedge_update(wedge, value, comp) 106 | 107 | Update a monotonic wedge with a new value. 108 | 109 | Erases values which do not satisfy comp(element, value), then 110 | appends value to the wedge via push_back. 111 | 112 | Complexity is less than log2(N) with respect to wedge size. 113 | Complexity of N calls is O(N), if wedge is initially empty. 114 | Thus, amortized complexity over many calls is constant. 115 | 116 | Wedge type must: 117 | - Produce random access iterators via begin/end. 118 | - Support push_back. 119 | 120 | A "less" comparator yields a min-wedge. 121 | A "greater" comparator yields a max-wedge. 122 | */ 123 | template 124 | void mono_wedge_update(Wedge& wedge, const T& value, Compare comp) { 125 | auto i = mono_wedge_search(wedge.begin(), wedge.end(), value, comp); 126 | wedge.erase(i, wedge.end()); 127 | wedge.push_back(value); 128 | } 129 | 130 | /* 131 | min_wedge_search(wedge, value) 132 | min_wedge_search(wedge, value) 133 | 134 | Convenience variants of mono_wedge_search for min and max wedges. 135 | These will use std::greater/less, which default to operator >/<. 136 | */ 137 | template 138 | Iterator min_wedge_search(Iterator begin, Iterator end, const T& value) { 139 | return mono_wedge_search(begin, end, value, std::less()); 140 | } 141 | 142 | template 143 | Iterator max_wedge_search(Iterator begin, Iterator end, const T& value) { 144 | return mono_wedge_search(begin, end, value, std::greater()); 145 | } 146 | 147 | /* 148 | min_wedge_update(wedge, value) 149 | min_wedge_update(wedge, value) 150 | 151 | Convenience variants of mono_wedge_update for min and max wedges. 152 | These will use std::greater/less, which default to operator >/<. 153 | */ 154 | template 155 | void min_wedge_update(Wedge& wedge, const T& value) { 156 | return mono_wedge_update(wedge, value, std::less()); 157 | } 158 | 159 | template 160 | void max_wedge_update(Wedge& wedge, const T& value) { 161 | return mono_wedge_update(wedge, value, std::greater()); 162 | } 163 | 164 | #if __cplusplus > 199711L 165 | /* 166 | C++11 variants of mono_wedge_update supporting rvalue references. 167 | */ 168 | 169 | template 170 | void mono_wedge_update(Wedge& wedge, T&& value, Compare comp) { 171 | typename Wedge::iterator i = 172 | mono_wedge_search(wedge.begin(), wedge.end(), value, comp); 173 | auto erase_count = static_cast(std::distance(i, wedge.end())); 174 | while (erase_count--) wedge.pop_back(); 175 | wedge.push_back(std::forward(value)); 176 | } 177 | 178 | template 179 | void min_wedge_update(Wedge& wedge, T&& value) { 180 | mono_wedge_update(wedge, std::forward(value), std::less()); 181 | } 182 | 183 | template 184 | void max_wedge_update(Wedge& wedge, T&& value) { 185 | mono_wedge_update(wedge, std::forward(value), std::greater()); 186 | } 187 | #endif 188 | } // namespace mono_wedge 189 | 190 | #endif // MONOTONIC_WEDGE_H 191 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | message(STATUS "Building Tests in ${CMAKE_CURRENT_LIST_DIR}") 2 | 3 | function(add_test_executable TARGET) 4 | add_executable(${TARGET} ${ARGN}) 5 | target_include_directories(${TARGET} PRIVATE ${CMAKE_CURRENT_LIST_DIR}) 6 | target_link_libraries(${TARGET} PUBLIC signaltl::signaltl Catch2::Catch2) 7 | set_default_compile_options(${TARGET}) 8 | add_coverage_flags(${TARGET}) 9 | catch_discover_tests(${TARGET}) 10 | endfunction() 11 | 12 | add_test_executable( 13 | signaltl_tests signaltl_tests.cc test_append_error.cc test_signals.cc 14 | ) 15 | 16 | if(BUILD_PARSER) 17 | add_test_executable(parser_tests signaltl_tests.cc test_parser.cc) 18 | 19 | # HACK(anand): Hard coding the path to the directory with examples so that 20 | # Catch2 can load it. I am not sure how to do it otherwise... 21 | # 22 | # NOTE: The double quotes must be escaped correctly. 23 | set_target_properties( 24 | parser_tests PROPERTIES COMPILE_DEFINITIONS 25 | "SIGNALTL_TESTS_DIR=\"${CMAKE_CURRENT_LIST_DIR}\"" 26 | ) 27 | endif() 28 | -------------------------------------------------------------------------------- /tests/formulas/formula0.stl-spec: -------------------------------------------------------------------------------- 1 | ; Comment 2 | 3 | -------------------------------------------------------------------------------- /tests/formulas/formula1.stl-spec: -------------------------------------------------------------------------------- 1 | ; vim: ft=lisp 2 | 3 | ; Here, `phi1` is the name of the formula (which should be fetchable from some 4 | ; hash table or something) and `x` is some signal value. Here we are defining 5 | ; `always (x > 0)`. 6 | (define-formula phi1 (always (> x 0))) 7 | 8 | 9 | ; Now we will define some regular formulas. 10 | (define-formula phi2 (< p 0)) 11 | (define-formula phi3 (> q 0)) 12 | (define-formula phi4 (and phi2 phi3)) 13 | (define-formula phi5 (eventually phi4)) 14 | (define-formula phi6 (always phi5)) 15 | 16 | (assert monitor phi6) 17 | 18 | ; alwas 19 | -------------------------------------------------------------------------------- /tests/formulas/formula2.stl-spec: -------------------------------------------------------------------------------- 1 | ; vim: set ft=lisp 2 | 3 | (define-formula phi0 (< p 10.0)) 4 | (define-formula phi1 true) 5 | (define-formula phi2 (not phi1)) 6 | (define-formula phi3 (and phi1 phi2 phi0)) 7 | -------------------------------------------------------------------------------- /tests/formulas/formula3.stl-spec: -------------------------------------------------------------------------------- 1 | ; vim: set ft=lisp 2 | 3 | (define-formula phi0 (implies (< p 0) (> s 10))) 4 | ; (define-formula phi1 (or (< p 0) (and (> q 0) (< r 0)))) 5 | -------------------------------------------------------------------------------- /tests/formulas/formulafail1.stl-spec: -------------------------------------------------------------------------------- 1 | ; vim: ft=lisp 2 | ; Here, `phi1` is the name of the formula (which should be fetchable from some 3 | ; hash table or something) and `x` is some signal value. Here we are defining 4 | ; `always (x > 0)`. 5 | (define-formula phi1 (always (> x 0))) 6 | 7 | ; It should fail here 8 | alwas 9 | 10 | -------------------------------------------------------------------------------- /tests/formulas/formulafail2.stl-spec: -------------------------------------------------------------------------------- 1 | ; vim: ft=lisp 2 | ; It should fail here 3 | ; ↓ 4 | (define-formulaphi1) 5 | -------------------------------------------------------------------------------- /tests/formulas/formulafail3.stl-spec: -------------------------------------------------------------------------------- 1 | ; vim: ft=lisp 2 | 3 | ; It should fail here 4 | ; ↓ 5 | (define-formula phi1 (and (< x 0))) 6 | 7 | -------------------------------------------------------------------------------- /tests/signaltl_tests.cc: -------------------------------------------------------------------------------- 1 | #define CATCH_CONFIG_MAIN 2 | 3 | #include "catch2/catch.hpp" 4 | -------------------------------------------------------------------------------- /tests/test_append_error.cc: -------------------------------------------------------------------------------- 1 | #include "signal_tl/signal_tl.hpp" // for Signal, Predicate, compute_robust... 2 | 3 | #include // for operator""_catch_sr, SourceLineInfo 4 | 5 | #include // for make_shared, shared_ptr, allocator 6 | #include // for vector 7 | 8 | namespace stl = signal_tl; 9 | using namespace signal_tl::signal; 10 | using signal_tl::ast::Expr; 11 | 12 | namespace { 13 | Expr get_phi() { 14 | auto theta1 = stl::Predicate("theta") <= 1.0; 15 | auto theta2 = stl::Predicate("theta") < 0.25; 16 | auto x_dot = stl::Predicate("x_dot") < 0.25; 17 | auto x = stl::Predicate("x") < 1; 18 | auto phi = stl::Always(theta1 & x) & stl::Eventually(stl::Always(theta2 & x_dot)); 19 | return phi; 20 | } 21 | 22 | Trace get_trace1() { 23 | auto t = std::vector{0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 24 | 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 25 | 18.0, 19.0, 20.0, 21.0, 22.0, 23.0, 24.0}; 26 | 27 | auto theta = std::vector{ 28 | 0.8072115182876587, 0.6253616213798523, 0.5759530067443848, 2.1681711673736572, 29 | 1.3756893873214722, 3.254326105117798, 0.9760392904281616, 0.3995759189128876, 30 | 0.4058665931224823, 1.9370567798614502, 0.19959034025669098, 1.7020440101623535, 31 | 0.8671382069587708, 0.4280993342399597, 1.0938061475753784, 0.42080727219581604, 32 | 2.25722336769104, 0.7912396192550659, 1.7364906072616577, 0.518024206161499, 33 | 2.300281286239624, 1.2479541301727295, 0.377531498670578, 0.06242965906858444, 34 | 0.49801141023635864}; 35 | 36 | auto x = std::vector{ 37 | 0.667906641960144, 0.4194730520248413, 0.058425962924957275, 38 | 1.093833088874817, 1.294434666633606, 0.038136839866638184, 39 | 0.8808276057243347, 0.4610064923763275, 0.6509886384010315, 40 | 0.288589745759964, 1.0443519353866577, 0.847595751285553, 41 | 0.5976979732513428, 1.4771767854690552, 1.362302303314209, 42 | 0.2924133539199829, 0.6099613308906555, 0.8487048745155334, 43 | 1.218008041381836, 0.46537500619888306, 0.5752330422401428, 44 | 0.12523308396339417, 0.5752788186073303, 0.5430198907852173, 45 | 1.007282853126525}; 46 | 47 | auto x_dot = 48 | std::vector{0.32892680168151855, 1.3858317136764526, 0.7090339660644531, 49 | 2.7325704097747803, 0.25496241450309753, 0.6217708587646484, 50 | 0.5633045434951782, 0.15813271701335907, 0.480469673871994, 51 | 1.7805505990982056, 0.016603171825408936, 0.6469761729240417, 52 | 0.37351396679878235, 2.1860337257385254, 1.4297616481781006, 53 | 0.37450167536735535, 1.1249395608901978, 0.9770298004150391, 54 | 0.46279963850975037, 1.7110997438430786, 0.2733519673347473, 55 | 1.7005544900894165, 1.4978992938995361, 0.2856500744819641, 56 | 1.3224234580993652}; 57 | 58 | return Trace{ 59 | {"theta", std::make_shared(theta, t)}, 60 | {"x", std::make_shared(x, t)}, 61 | {"x_dot", std::make_shared(x_dot, t)}}; 62 | } 63 | 64 | Trace get_trace2() { 65 | auto t = std::vector{48.0, 49.0, 50.0, 51.0, 52.0, 53.0, 54.0, 55.0, 56.0, 66 | 57.0, 58.0, 59.0, 60.0, 61.0, 62.0, 63.0, 64.0, 65.0, 67 | 66.0, 67.0, 68.0, 69.0, 70.0, 71.0, 72.0}; 68 | 69 | auto theta = 70 | std::vector{0.44065698981285095, 0.5513322949409485, 0.5247364044189453, 71 | 0.43430134654045105, 0.32265782356262207, 0.5241603255271912, 72 | 0.469169020652771, 0.46336737275123596, 0.5579506754875183, 73 | 0.3699119985103607, 0.46935901045799255, 0.4902954399585724, 74 | 0.4584878087043762, 0.30844229459762573, 0.4374590218067169, 75 | 0.6623110175132751, 0.6576758027076721, 0.8176819682121277, 76 | 0.6190072894096375, 0.5651955604553223, 0.4377847909927368, 77 | 0.4374394118785858, 0.1870441734790802, 0.46698999404907227, 78 | 0.5973787903785706}; 79 | 80 | auto x = 81 | std::vector{0.45926108956336975, 0.5118712186813354, 0.5310275554656982, 82 | 0.5383917689323425, 0.5208474397659302, 0.44286447763442993, 83 | 0.524522602558136, 0.48844966292381287, 0.48231762647628784, 84 | 0.509438693523407, 0.4990404546260834, 0.5225937962532043, 85 | 0.49739140272140503, 0.557137131690979, 0.5001983642578125, 86 | 0.5057839155197144, 0.49534863233566284, 0.46751928329467773, 87 | 0.5088319778442383, 0.5121058821678162, 0.5232639312744141, 88 | 0.4922287166118622, 0.48807886242866516, 0.470917671918869, 89 | 0.46549665927886963}; 90 | 91 | auto x_dot = std::vector{ 92 | 0.057910606265068054, 0.5042053461074829, 1.0982192754745483, 93 | 0.7265422940254211, 0.5069679617881775, 0.062289945781230927, 94 | 1.1192792654037476, 0.564326822757721, 1.3101483583450317, 95 | 0.05849301815032959, 1.1456730365753174, 0.24640771746635437, 96 | 0.12858152389526367, 0.1601352095603943, 0.6358292698860168, 97 | 0.7501578330993652, 0.8025932312011719, 0.09620770812034607, 98 | 0.5078790783882141, 0.5271714925765991, 1.5782673358917236, 99 | 0.4009734094142914, 0.8590595126152039, 0.824567973613739, 100 | 0.5973787903785706}; 101 | 102 | return Trace{ 103 | {"theta", std::make_shared(theta, t)}, 104 | {"x", std::make_shared(x, t)}, 105 | {"x_dot", std::make_shared(x_dot, t)}}; 106 | } 107 | 108 | Trace get_trace3() { 109 | auto t = std::vector{36.0, 37.0, 38.0, 39.0, 40.0, 41.0, 42.0, 43.0, 44.0, 110 | 45.0, 46.0, 47.0, 48.0, 49.0, 50.0, 51.0, 52.0, 53.0, 111 | 54.0, 55.0, 56.0, 57.0, 58.0, 59.0, 60.0}; 112 | 113 | auto theta = std::vector{ 114 | 0.5072104334831238, 0.5413619875907898, 0.46047237515449524, 0.587928831577301, 115 | 0.6958177089691162, 0.465532124042511, 0.3703998029232025, 0.5294902324676514, 116 | 0.5152763724327087, 0.3972248136997223, 0.5539292693138123, 0.7565155029296875, 117 | 0.7385683655738831, 0.8336687684059143, 0.7297738790512085, 0.761920154094696, 118 | 0.5655747056007385, 0.5533368587493896, 0.3833700716495514, 0.4904012084007263, 119 | 0.6297431588172913, 0.5226778388023376, 0.5340427756309509, 0.5696533918380737, 120 | 0.5347775816917419}; 121 | 122 | auto x = 123 | std::vector{0.49088460206985474, 0.46030473709106445, 0.46780791878700256, 124 | 0.5537748336791992, 0.615630567073822, 0.506172776222229, 125 | 0.504558265209198, 0.5294902324676514, 0.5679064393043518, 126 | 0.562709391117096, 0.5367719531059265, 0.5054753422737122, 127 | 0.49388623237609863, 0.4934890568256378, 0.466511994600296, 128 | 0.4677998721599579, 0.6733770370483398, 0.5938720107078552, 129 | 0.4902632236480713, 0.4863351583480835, 0.4031582176685333, 130 | 0.46352311968803406, 0.5801506042480469, 0.45827946066856384, 131 | 0.5205367207527161}; 132 | 133 | auto x_dot = std::vector{ 134 | 0.2561785876750946, 0.7974404096603394, 0.28822001814842224, 0.7067545652389526, 135 | 0.3675220012664795, 1.8011497259140015, 2.357013702392578, 0.9592754244804382, 136 | 0.16032743453979492, 1.4524136781692505, 0.5566926002502441, 0.669854998588562, 137 | 1.120910882949829, 1.1875431537628174, 1.0968633890151978, 1.3454444408416748, 138 | 1.2306058406829834, 1.8302419185638428, 0.7071495652198792, 0.08202195167541504, 139 | 1.1206265687942505, 0.7472844123840332, 1.7585080862045288, 1.749678373336792, 140 | 1.4623041152954102}; 141 | 142 | return Trace{ 143 | {"theta", std::make_shared(theta, t)}, 144 | {"x", std::make_shared(x, t)}, 145 | {"x_dot", std::make_shared(x_dot, t)}}; 146 | } 147 | 148 | } // namespace 149 | 150 | TEST_CASE( 151 | "Computing robustness does not mess up the timestamps", 152 | "[signal][robustness]") { 153 | const auto phi = get_phi(); 154 | { 155 | const auto trace = get_trace1(); 156 | 157 | REQUIRE_NOTHROW(stl::compute_robustness(phi, trace, true)); 158 | } 159 | 160 | { 161 | const auto trace = get_trace2(); 162 | 163 | REQUIRE_NOTHROW(stl::compute_robustness(phi, trace, true)); 164 | } 165 | 166 | { 167 | const auto trace = get_trace3(); 168 | 169 | REQUIRE_NOTHROW(stl::compute_robustness(phi, trace, true)); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /tests/test_parser.cc: -------------------------------------------------------------------------------- 1 | #include "signal_tl/internal/filesystem.hpp" 2 | #include "signal_tl/parser.hpp" 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #ifndef SIGNALTL_TESTS_DIR 12 | #error "SIGNALTL_TESTS_DIR has not been defined in the preprocessor stage" 13 | #endif 14 | 15 | TEST_CASE("PEG Grammar has no issues", "[parser][grammar]") { 16 | size_t issues = signal_tl::grammar::internal::analyze(1); 17 | INFO("Number of issues = " << issues); 18 | REQUIRE(issues == 0); 19 | } 20 | 21 | TEST_CASE("Parsing of string input specifications", "[parser][string-input]") { 22 | auto valid_spec = GENERATE(values({ 23 | "(define-formula phi1 (< p 0))\n" 24 | "(assert monitor phi1)\n", 25 | "; Here, `phi1` is the name of the formula (which should be fetchable from some\n" 26 | "; hash table or something) and `x` is some signal value. Here we are defining\n" 27 | "; `always (x > 0)`.\n" 28 | "(define-formula phi1 (always (> x 0)))\n" 29 | "; Now we will define some regular formulas.\n" 30 | "(define-formula phi2 (< p 0))\n" 31 | "(define-formula phi3 (> q 0))\n" 32 | "(define-formula phi4 (and phi2 phi3))\n" 33 | "(define-formula phi5 (eventually phi4))\n" 34 | "(define-formula phi6 (always phi5))\n" 35 | "(assert monitor phi6)\n", 36 | })); 37 | 38 | SECTION("Valid specifications are parsed") { 39 | REQUIRE_NOTHROW(signal_tl::parser::from_string(valid_spec)); 40 | } 41 | } 42 | 43 | TEST_CASE("Parsing of file input specifications", "[parser][file-input]") { 44 | const auto specification_dir = stdfs::path(SIGNALTL_TESTS_DIR) / "formulas"; 45 | if (!stdfs::exists(specification_dir) || !stdfs::is_directory(specification_dir)) { 46 | FAIL("Directory with testing formulas doesn't exist: " << specification_dir); 47 | } 48 | 49 | for (auto& p : stdfs::directory_iterator(specification_dir)) { 50 | if (stdfs::is_regular_file(p)) { 51 | SECTION(std::string("Parsing file: ") + p.path().string()) { 52 | INFO(std::string("Parsing file: ") + p.path().string()); 53 | if (p.path().string().find("fail") != std::string::npos) { 54 | // If filename contains "fail" then it should fail to parse 55 | REQUIRE_THROWS(signal_tl::parser::from_file(p)); 56 | } else { 57 | // Otherwise, it should succeed 58 | REQUIRE_NOTHROW(signal_tl::parser::from_file(p)); 59 | } 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/test_signals.cc: -------------------------------------------------------------------------------- 1 | #include "signal_tl/signal.hpp" // for Sample, Signal, signal 2 | 3 | #include // for Approx, operator==, SourceLineInfo 4 | 5 | #include // for __shared_ptr_access, shared_ptr, all... 6 | #include // for default_random_engine, random_device 7 | #include // for vector 8 | 9 | using namespace signal_tl::signal; 10 | 11 | namespace { 12 | class MonotonicIncreasingTimestampedSignal 13 | : public Catch::Generators::IGenerator { 14 | std::default_random_engine rng; 15 | std::uniform_real_distribution<> dist; 16 | double current_end_time{0.0}; 17 | Sample current_last_sample; 18 | 19 | public: 20 | MonotonicIncreasingTimestampedSignal( 21 | double interval_size = 10.0, // NOLINT(cppcoreguidelines-avoid-magic-numbers) 22 | // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) 23 | double delta = 0.1) : 24 | rng(std::random_device{}()), dist(delta, interval_size) { 25 | current_last_sample = 26 | Sample{current_end_time, 10.0}; // NOLINT(cppcoreguidelines-avoid-magic-numbers) 27 | } 28 | 29 | [[nodiscard]] Sample const& get() const override; 30 | 31 | bool next() override { 32 | current_end_time += dist(rng); 33 | current_last_sample.time = current_end_time; 34 | return true; 35 | } 36 | }; 37 | 38 | Catch::Generators::GeneratorWrapper mono_increase_sample( 39 | double interval_size = 10.0, // NOLINT(cppcoreguidelines-avoid-magic-numbers) 40 | double delta = 0.1) { // NOLINT(cppcoreguidelines-avoid-magic-numbers) 41 | return Catch::Generators::GeneratorWrapper( 42 | std::unique_ptr>( 43 | new MonotonicIncreasingTimestampedSignal(interval_size, delta))); 44 | } 45 | 46 | } // namespace 47 | 48 | Sample const& MonotonicIncreasingTimestampedSignal::get() const { 49 | return current_last_sample; 50 | } 51 | 52 | TEST_CASE("Signals are monotonically increasing", "[signal]") { 53 | SECTION("Manually created signal") { 54 | auto points = std::vector{ 55 | 25.0, 15.0, 22.0, -1.0}; // NOLINT(cppcoreguidelines-avoid-magic-numbers) 56 | auto time_pts = std::vector{ 57 | 0, 0.25, 5, 6.25}; // NOLINT(cppcoreguidelines-avoid-magic-numbers) 58 | auto sig = std::make_shared(points, time_pts); 59 | 60 | REQUIRE(sig->size() == 4); 61 | REQUIRE(sig->begin_time() == Approx(0)); 62 | REQUIRE( 63 | sig->end_time() == 64 | Approx(6.25)); // NOLINT(cppcoreguidelines-avoid-magic-numbers) 65 | 66 | auto time_point = GENERATE( 67 | take(100, random(0.0, 6.25))); // NOLINT(cppcoreguidelines-avoid-magic-numbers) 68 | REQUIRE_THROWS(sig->push_back(time_point, 0.0)); 69 | } 70 | 71 | SECTION("Automatically generated signals") { 72 | auto samples = GENERATE(take(1000, chunk(50, mono_increase_sample()))); 73 | REQUIRE_NOTHROW(Signal{samples}); 74 | } 75 | } 76 | --------------------------------------------------------------------------------