From f248cabd9206b9526d56b74b5661297a0a3f376d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Paris?= <rene.paris@in-tech.com> Date: Wed, 12 Feb 2025 08:08:50 +0100 Subject: [PATCH 1/6] feat(JSON for modern C++): Integrate nlohmann_json library Integrates the commonly used JSON library for convenient parsing of custom properites, carring object data encoded as string. --- NOTICE.md | 6 ++ README.md | 9 +-- engine/BUILD.bazel | 4 +- engine/CMakeLists.txt | 44 +++++++------- engine/WORKSPACE | 12 +--- engine/third_party/dependencies.bzl | 21 +++++++ engine/third_party/deps.bzl | 18 ------ engine/third_party/googletest/BUILD.bazel | 11 ++++ .../googletest/gcc_printer_patch.patch | 22 +++++++ engine/third_party/googletest/googletest.bzl | 20 +++++++ engine/third_party/nlohmann_json/BUILD.bazel | 5 ++ .../nlohmann_json/nlohmann_json.BUILD | 7 +++ .../nlohmann_json/nlohmann_json.bzl | 13 ++++ utils/ci/conan/conanfile.txt | 5 +- .../recipe/nlohmann_json/all/conandata.yml | 21 +++++++ .../recipe/nlohmann_json/all/conanfile.py | 60 +++++++++++++++++++ .../ci/conan/recipe/nlohmann_json/config.yml | 20 +++++++ utils/ci/scripts/20_configure.sh | 1 + 18 files changed, 243 insertions(+), 56 deletions(-) create mode 100644 engine/third_party/dependencies.bzl delete mode 100644 engine/third_party/deps.bzl create mode 100644 engine/third_party/googletest/BUILD.bazel create mode 100644 engine/third_party/googletest/gcc_printer_patch.patch create mode 100644 engine/third_party/googletest/googletest.bzl create mode 100644 engine/third_party/nlohmann_json/BUILD.bazel create mode 100644 engine/third_party/nlohmann_json/nlohmann_json.BUILD create mode 100644 engine/third_party/nlohmann_json/nlohmann_json.bzl create mode 100644 utils/ci/conan/recipe/nlohmann_json/all/conandata.yml create mode 100644 utils/ci/conan/recipe/nlohmann_json/all/conanfile.py create mode 100644 utils/ci/conan/recipe/nlohmann_json/config.yml diff --git a/NOTICE.md b/NOTICE.md index 812ea2b8..c623d498 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -78,6 +78,12 @@ Yase units_nhh * License: MIT +nlohmann (JSON for Modern C++) + * License: MIT License + * Homepage: https://nlohmann.github.io/json/ + * Repository: https://github.com/nlohmann/json + * Version: 3.9.1 + MantleAPI * License: EPL 2.0 diff --git a/README.md b/README.md index c4eebab5..7e6662c2 100644 --- a/README.md +++ b/README.md @@ -255,13 +255,14 @@ Converts `NET_ASAM_OPENSCENARIO::v1_3::ITransitionDynamics` object type to [mant | Dependency | Commit | Version | License | | ---------- | ------ | ------- | ------- | +| [CPM](https://github.com/cpm-cmake/CPM.cmake) | 03705fc | 0.36.0 | MIT License | +| [googletest](https://github.com/google/googletest) | f8d7d77 | 1.14.0 | BSD-3-Clause License | +| [JSON for Modern C++](https://github.com/nlohmann/json) | db78ac1 | 3.9.1 | MIT License | | [MantleAPI](https://gitlab.eclipse.org/eclipse/openpass/mantle-api) | 5541bf1a | 14.0.0 | EPL 2.0 | +| [openpass/stochastics-library](https://gitlab.eclipse.org/eclipse/openpass/stochastics-library) | 6c9dde71 | 0.11.0 | EPL 2.0 | | [OpenSCENARIO API](https://github.com/RA-Consulting-GmbH/openscenario.api.test/) | 5980e88 | 1.4.0 | Apache 2.0 | -| [YASE](https://gitlab.eclipse.org/eclipse/openpass/yase) | d0c0e58d | | EPL 2.0 | | [Units](https://github.com/nholthaus/units) | e27eed9 | 2.3.4 | MIT License | -| [googletest](https://github.com/google/googletest) | f8d7d77 | 1.14.0 | BSD-3-Clause License | -| [CPM](https://github.com/cpm-cmake/CPM.cmake) | 03705fc | 0.36.0 | MIT License | -| [openpass/stochastics-library](https://gitlab.eclipse.org/eclipse/openpass/stochastics-library) | 6c9dde71 | 0.11.0 | EPL 2.0 | +| [YASE](https://gitlab.eclipse.org/eclipse/openpass/yase) | d0c0e58d | | EPL 2.0 | # Issues diff --git a/engine/BUILD.bazel b/engine/BUILD.bazel index cbc396a1..c53cf6dd 100644 --- a/engine/BUILD.bazel +++ b/engine/BUILD.bazel @@ -29,6 +29,7 @@ cc_library( visibility = ["//visibility:public"], deps = [ "@mantle_api", + "@nlohmann_json", "@open_scenario_parser", "@stochastics_library", "@units_nhh", @@ -178,11 +179,12 @@ cc_test( data = [":open_scenario_engine_test_data"], tags = ["test"], deps = [ - ":test_utils", ":open_scenario_engine", ":open_scenario_engine_test_utils", + ":test_utils", "@googletest//:gtest_main", "@mantle_api//:test_utils", + "@nlohmann_json", ], ) diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index 72cdb526..d469f08f 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -23,11 +23,11 @@ if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Release) endif() -# YASE -find_package(Yase REQUIRED) - -# units https://github.com/nholthaus/units (dependency of MantleAPI) +find_package(MantleAPI REQUIRED) +find_package(nlohmann_json REQUIRED) +find_package(Stochastics REQUIRED) find_package(units REQUIRED) +find_package(Yase REQUIRED) # googlemock include(CPM) @@ -39,12 +39,6 @@ CPMAddPackage( OPTIONS "INSTALL_GTEST OFF" "gtest_force_shared_crt ON" ) -# MantleAPI https://gitlab.eclipse.org/eclipse/simopenpass/scenario_api -find_package(MantleAPI REQUIRED) - - -find_package(Stochastics REQUIRED) - # see https://stackoverflow.com/a/58495612 set(CMAKE_INSTALL_RPATH $ORIGIN) @@ -70,12 +64,17 @@ target_include_directories( $<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}/include> ) -target_link_libraries(${PROJECT_NAME} PUBLIC openscenario_api::shared - antlr4_runtime::shared - Yase::agnostic_behavior_tree - units::units - MantleAPI::MantleAPI - PRIVATE Stochastics::Stochastics) +target_link_libraries(${PROJECT_NAME} + PUBLIC + antlr4_runtime::shared + MantleAPI::MantleAPI + units::units + openscenario_api::shared + PRIVATE + nlohmann_json::nlohmann_json + Stochastics::Stochastics + Yase::agnostic_behavior_tree +) if(USE_CCACHE) find_program(CCACHE_FOUND ccache) @@ -250,11 +249,14 @@ target_include_directories( target_link_libraries( ${PROJECT_NAME}Test - PRIVATE ${PROJECT_NAME} - Stochastics::Stochastics - antlr4_runtime::shared - GTest::gmock_main - pthread + PRIVATE + ${PROJECT_NAME} + antlr4_runtime::shared + GTest::gmock_main + nlohmann_json::nlohmann_json + pthread + Stochastics::Stochastics + Yase::agnostic_behavior_tree ) add_test(NAME ${PROJECT_NAME}Test COMMAND ${PROJECT_NAME}Test) diff --git a/engine/WORKSPACE b/engine/WORKSPACE index 91062dc5..5622781f 100644 --- a/engine/WORKSPACE +++ b/engine/WORKSPACE @@ -1,13 +1,5 @@ workspace(name = "open_scenario_engine") -load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") -load("//third_party:deps.bzl", "osc_engine_deps") +load("//third_party:dependencies.bzl", "third_party_deps") -http_archive( - name = "googletest", - sha256 = "81964fe578e9bd7c94dfdb09c8e4d6e6759e19967e397dbea48d1c10e45d0df2", - strip_prefix = "googletest-release-1.12.1", - url = "https://github.com/google/googletest/archive/refs/tags/release-1.12.1.tar.gz", -) - -osc_engine_deps() +third_party_deps() diff --git a/engine/third_party/dependencies.bzl b/engine/third_party/dependencies.bzl new file mode 100644 index 00000000..180f0fdb --- /dev/null +++ b/engine/third_party/dependencies.bzl @@ -0,0 +1,21 @@ +"""A module defining the third party dependencies for instantiating in WORKSPACE""" + +load("@open_scenario_engine//third_party/boost:boost.bzl", "boost_deps") +load("@open_scenario_engine//third_party/googletest:googletest.bzl", "googletest") +load("@open_scenario_engine//third_party/mantle_api:mantle_api.bzl", "mantle_api") +load("@open_scenario_engine//third_party/nlohmann_json:nlohmann_json.bzl", "nlohmann_json") +load("@open_scenario_engine//third_party/open_scenario_parser:open_scenario_parser.bzl", "open_scenario_parser") +load("@open_scenario_engine//third_party/stochastics_library:stochastics_library.bzl", "stochastics_library") +load("@open_scenario_engine//third_party/units:units.bzl", "units_nhh") +load("@open_scenario_engine//third_party/yase:yase.bzl", "yase") + +def third_party_deps(): + """Load dependencies""" + boost_deps() + googletest() + mantle_api() + nlohmann_json() + open_scenario_parser() + stochastics_library() + units_nhh() + yase() diff --git a/engine/third_party/deps.bzl b/engine/third_party/deps.bzl deleted file mode 100644 index 1f832f8a..00000000 --- a/engine/third_party/deps.bzl +++ /dev/null @@ -1,18 +0,0 @@ -load("@//third_party/boost:boost.bzl", "boost_deps") -load("@//third_party/mantle_api:mantle_api.bzl", "mantle_api") -load("@//third_party/open_scenario_parser:open_scenario_parser.bzl", "open_scenario_parser") -load("@//third_party/stochastics_library:stochastics_library.bzl", "stochastics_library") -load("@//third_party/units:units.bzl", "units_nhh") -load("@//third_party/yase:yase.bzl", "yase") - - - -def osc_engine_deps(): - """Load dependencies""" - boost_deps() - mantle_api() - open_scenario_parser() - stochastics_library() - units_nhh() - yase() - diff --git a/engine/third_party/googletest/BUILD.bazel b/engine/third_party/googletest/BUILD.bazel new file mode 100644 index 00000000..5219fee5 --- /dev/null +++ b/engine/third_party/googletest/BUILD.bazel @@ -0,0 +1,11 @@ +alias( + name = "googletest", + actual = "@googletest//:gtest", + visibility = ["//visibility:public"], +) + +alias( + name = "main", + actual = "@googletest//:gtest_main", + visibility = ["//visibility:public"], +) diff --git a/engine/third_party/googletest/gcc_printer_patch.patch b/engine/third_party/googletest/gcc_printer_patch.patch new file mode 100644 index 00000000..a84e6ef6 --- /dev/null +++ b/engine/third_party/googletest/gcc_printer_patch.patch @@ -0,0 +1,22 @@ +--- a/googletest/include/gtest/gtest-printers.h ++++ b/googletest/include/gtest/gtest-printers.h +@@ -205,12 +205,13 @@ struct StreamPrinter { + // Don't accept member pointers here. We'd print them via implicit + // conversion to bool, which isn't useful. + typename = typename std::enable_if< +- !std::is_member_pointer<T>::value>::type, +- // Only accept types for which we can find a streaming operator via +- // ADL (possibly involving implicit conversions). +- typename = decltype(std::declval<std::ostream&>() +- << std::declval<const T&>())> +- static void PrintValue(const T& value, ::std::ostream* os) { ++ !std::is_member_pointer<T>::value>::type> ++ // Only accept types for which we can find a streaming operator via ++ // ADL (possibly involving implicit conversions). ++ // (Use SFINAE via return type, because it seems GCC < 12 doesn't handle name ++ // lookup properly when we do it in the template parameter list.) ++ static auto PrintValue(const T& value, ::std::ostream* os) ++ -> decltype((void)(*os << value)) { + // Call streaming operator found by ADL, possibly with implicit conversions + // of the arguments. + *os << value; diff --git a/engine/third_party/googletest/googletest.bzl b/engine/third_party/googletest/googletest.bzl new file mode 100644 index 00000000..96cf4768 --- /dev/null +++ b/engine/third_party/googletest/googletest.bzl @@ -0,0 +1,20 @@ +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") +load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe") + +_VERSION = "1.12.1" + +def googletest(): + maybe( + http_archive, + name = "googletest", + url = "https://github.com/google/googletest/archive/refs/tags/release-{}.tar.gz".format(_VERSION), + sha256 = "81964fe578e9bd7c94dfdb09c8e4d6e6759e19967e397dbea48d1c10e45d0df2", + strip_prefix = "googletest-release-{}".format(_VERSION), + patches = [ + # Gtest >= 1.11.0 is impacted by a GCC compiler bug regarding template deduction. + # See: https://github.com/google/googletest/issues/3552 + # We backport the upstream fix to this until we reach a gtest version incorporating it by itself. + Label("//:third_party/googletest/gcc_printer_patch.patch"), + ], + patch_args = ["-p1"], + ) diff --git a/engine/third_party/nlohmann_json/BUILD.bazel b/engine/third_party/nlohmann_json/BUILD.bazel new file mode 100644 index 00000000..73a292ee --- /dev/null +++ b/engine/third_party/nlohmann_json/BUILD.bazel @@ -0,0 +1,5 @@ +alias( + name = "nlohmann_json", + actual = "@nlohmann_json", + visibility = ["//visibility:public"], +) diff --git a/engine/third_party/nlohmann_json/nlohmann_json.BUILD b/engine/third_party/nlohmann_json/nlohmann_json.BUILD new file mode 100644 index 00000000..d615d420 --- /dev/null +++ b/engine/third_party/nlohmann_json/nlohmann_json.BUILD @@ -0,0 +1,7 @@ + +cc_library( + name = "nlohmann_json", + hdrs = ["single_include/nlohmann/json.hpp"], + includes = ["single_include"], + visibility = ["//visibility:public"], +) diff --git a/engine/third_party/nlohmann_json/nlohmann_json.bzl b/engine/third_party/nlohmann_json/nlohmann_json.bzl new file mode 100644 index 00000000..5d2c07b9 --- /dev/null +++ b/engine/third_party/nlohmann_json/nlohmann_json.bzl @@ -0,0 +1,13 @@ +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") +load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe") + +_VERSION = "3.9.1" + +def nlohmann_json(): + maybe( + http_archive, + name = "nlohmann_json", + build_file = Label("//:third_party/nlohmann_json/nlohmann_json.BUILD"), + url = "https://github.com/nlohmann/json/releases/download/v{version}/include.zip".format(version = _VERSION), + sha256 = "6bea5877b1541d353bd77bdfbdb2696333ae5ed8f9e8cc22df657192218cad91", + ) diff --git a/utils/ci/conan/conanfile.txt b/utils/ci/conan/conanfile.txt index 2cd59264..059e3c1a 100644 --- a/utils/ci/conan/conanfile.txt +++ b/utils/ci/conan/conanfile.txt @@ -1,9 +1,10 @@ [requires] -units/2.3.4@openscenarioengine/testing mantleapi/v14.0.0@openscenarioengine/testing -yase/d0c0e58d17358044cc9018c74308b45f6097ecfb@openscenarioengine/testing +nlohmann_json/v3.9.1@openscenarioengine/testing openscenario_api/v1.4.0@openscenarioengine/testing stochastics/0.11.0@openscenarioengine/testing +units/2.3.4@openscenarioengine/testing +yase/d0c0e58d17358044cc9018c74308b45f6097ecfb@openscenarioengine/testing [options] diff --git a/utils/ci/conan/recipe/nlohmann_json/all/conandata.yml b/utils/ci/conan/recipe/nlohmann_json/all/conandata.yml new file mode 100644 index 00000000..3a9de20a --- /dev/null +++ b/utils/ci/conan/recipe/nlohmann_json/all/conandata.yml @@ -0,0 +1,21 @@ +################################################################################ +# Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License 2.0 which is available at +# http://www.eclipse.org/legal/epl-2.0. +# +# SPDX-License-Identifier: EPL-2.0 +################################################################################ + +################################################################################ +# conan data file for building nlohmann_json with Conan +################################################################################ + +sources: + "v3.9.1": + url: https://github.com/nlohmann/json/archive/refs/tags/v3.9.1.tar.gz + sha256: "4cf0df69731494668bdd6460ed8cb269b68de9c19ad8c27abc24cd72605b2d5b" + "v3.11.3": + url: https://github.com/nlohmann/json/archive/refs/tags/v3.11.3.tar.gz + sha256: "0d8ef5af7f9794e3263480193c491549b2ba6cc74bb018906202ada498a79406" diff --git a/utils/ci/conan/recipe/nlohmann_json/all/conanfile.py b/utils/ci/conan/recipe/nlohmann_json/all/conanfile.py new file mode 100644 index 00000000..2a50d134 --- /dev/null +++ b/utils/ci/conan/recipe/nlohmann_json/all/conanfile.py @@ -0,0 +1,60 @@ +################################################################################ +# Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License 2.0 which is available at +# http://www.eclipse.org/legal/epl-2.0. +# +# SPDX-License-Identifier: EPL-2.0 +################################################################################ + +################################################################################ +# Install file for building nlohmann_json with Conan +################################################################################ + +from conan import ConanFile +from conan.tools.cmake import CMake, CMakeToolchain +from conan.tools.files import rm, copy, rmdir +from conan.tools.files import get, copy +import os + +required_conan_version = ">=1.53" + +class NlohmannJSONConan(ConanFile): + name = "nlohmann_json" + settings = "os", "compiler", "build_type", "arch" + options = {"shared": [True, False], + "fPIC": [True, False]} + + default_options = {"shared": False, + "fPIC": True} + + exports_sources = "*" + no_copy_source = False + short_paths = True + + def config_options(self): + if self.settings.os == "Windows": + del self.options.fPIC + + def configure(self): + if self.options.shared: + self.options.rm_safe("fPIC") + + def generate(self): + tc = CMakeToolchain(self) + tc.generate() + + def source(self): + get(self, **self.conan_data["sources"][self.version], strip_root=True) + + def build(self): + cmake = CMake(self) + cmake.configure() + + def package(self): + cmake = CMake(self) + cmake.install() + + def package_info(self): + self.cpp_info.set_property("cmake_find_mode", "none") diff --git a/utils/ci/conan/recipe/nlohmann_json/config.yml b/utils/ci/conan/recipe/nlohmann_json/config.yml new file mode 100644 index 00000000..c2ea0f20 --- /dev/null +++ b/utils/ci/conan/recipe/nlohmann_json/config.yml @@ -0,0 +1,20 @@ +################################################################################ +# Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License 2.0 which is available at +# http://www.eclipse.org/legal/epl-2.0. +# +# SPDX-License-Identifier: EPL-2.0 +################################################################################ + +################################################################################ +# config file for building nlohmann_json with Conan +################################################################################ + +versions: + "v3.9.1": + folder: "all" + + "v3.11.3": + folder: "all" diff --git a/utils/ci/scripts/20_configure.sh b/utils/ci/scripts/20_configure.sh index 303502a8..8dc0b896 100755 --- a/utils/ci/scripts/20_configure.sh +++ b/utils/ci/scripts/20_configure.sh @@ -31,6 +31,7 @@ mkdir -p "$MYDIR/../../../../build" cd "$MYDIR/../../../../build" || exit 1 DEPS=( + "$PWD/../deps/direct_deploy/nlohmann_json" "$PWD/../deps/direct_deploy/units" "$PWD/../deps/direct_deploy/mantleapi" "$PWD/../deps/direct_deploy/stochastics" -- GitLab From 6bc0c97f444320d3618249c8796f86be8bdc63d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Paris?= <rene.paris@in-tech.com> Date: Wed, 12 Feb 2025 15:09:31 +0000 Subject: [PATCH 2/6] feat(JsonParser): Add parser for JSON property strings and distribution definitions This commit adds a generic JSON property parser to parse arbitrary strings to a concrete object. For reference, several distributions have been defined, both as json schema and C++ structs. See readme and corresponding test for more information. --- README.md | 11 ++ engine/CMakeLists.txt | 1 + .../Property/Distribution/BaseDistribution.h | 32 ++++++ .../Distribution/ExponentialDistribution.h | 28 +++++ .../Property/Distribution/GammaDistribution.h | 29 +++++ .../Distribution/LogNormalDistribution.h | 29 +++++ .../Distribution/NormalDistribution.h | 29 +++++ .../Distribution/UniformDistribution.h | 28 +++++ engine/src/Utils/Property/Distributions.h | 18 +++ engine/src/Utils/Property/JsonParser.h | 89 +++++++++++++++ .../distributions/base_distribution.json | 20 ++++ .../exponential_distribution.json | 30 +++++ .../distributions/gamma_distribution.json | 34 ++++++ .../distributions/lognormal_distribution.json | 35 ++++++ .../distributions/normal_distrubution.json | 35 ++++++ .../distributions/uniform_distribution.json | 24 ++++ engine/src/Utils/Property/schemas/readme.md | 21 ++++ engine/tests/Utils/JsonParserTest.cpp | 106 ++++++++++++++++++ 18 files changed, 599 insertions(+) create mode 100644 engine/src/Utils/Property/Distribution/BaseDistribution.h create mode 100644 engine/src/Utils/Property/Distribution/ExponentialDistribution.h create mode 100644 engine/src/Utils/Property/Distribution/GammaDistribution.h create mode 100644 engine/src/Utils/Property/Distribution/LogNormalDistribution.h create mode 100644 engine/src/Utils/Property/Distribution/NormalDistribution.h create mode 100644 engine/src/Utils/Property/Distribution/UniformDistribution.h create mode 100644 engine/src/Utils/Property/Distributions.h create mode 100644 engine/src/Utils/Property/JsonParser.h create mode 100644 engine/src/Utils/Property/schemas/distributions/base_distribution.json create mode 100644 engine/src/Utils/Property/schemas/distributions/exponential_distribution.json create mode 100644 engine/src/Utils/Property/schemas/distributions/gamma_distribution.json create mode 100644 engine/src/Utils/Property/schemas/distributions/lognormal_distribution.json create mode 100644 engine/src/Utils/Property/schemas/distributions/normal_distrubution.json create mode 100644 engine/src/Utils/Property/schemas/distributions/uniform_distribution.json create mode 100644 engine/src/Utils/Property/schemas/readme.md create mode 100644 engine/tests/Utils/JsonParserTest.cpp diff --git a/README.md b/README.md index 7e6662c2..8bc1f748 100644 --- a/README.md +++ b/README.md @@ -251,6 +251,17 @@ Converts `NET_ASAM_OPENSCENARIO::v1_3::ITrajectoryRef` object type to [mantle_ap Converts `NET_ASAM_OPENSCENARIO::v1_3::ITransitionDynamics` object type to [mantle_api::TransitionDynamics](https://gitlab.eclipse.org/eclipse/openpass/mantle-api/-/blob/master/include/MantleAPI/Traffic/control_strategy.h) **Note**: Right now the property `followingMode` is not considered. +# Customizations w.r.t ASAM OpenSCENARIO Standard + +## Property Parsing + +The OpenSCENARIO standard does not impose strict requirements on the format of custom properties, allowing for flexibility in their definition. +To facilitate this, the OpenScenarioEngine uses JSON to parse string-type properties into an internal object format. +This approach enables the definition of complex properties, such as distributions, which can be used within actions. +For example, a velocity distribution might be defined and used in a `TrafficAreaAction`. + +For more information on how to define these properties, refer to the provided schemas in [engine/src/Utils/Property/schemas](engine/src/Utils/Property/schemas). + # Dependencies | Dependency | Commit | Version | License | diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index d469f08f..b490fff9 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -226,6 +226,7 @@ target_sources( ${CMAKE_CURRENT_LIST_DIR}/tests/Utils/EllipseTest.cpp ${CMAKE_CURRENT_LIST_DIR}/tests/Utils/EntityCreatorTest.cpp ${CMAKE_CURRENT_LIST_DIR}/tests/Utils/EntityUtilsTest.cpp + ${CMAKE_CURRENT_LIST_DIR}/tests/Utils/JsonParserTest.cpp ) add_custom_command(TARGET ${PROJECT_NAME}Test diff --git a/engine/src/Utils/Property/Distribution/BaseDistribution.h b/engine/src/Utils/Property/Distribution/BaseDistribution.h new file mode 100644 index 00000000..30c7e68d --- /dev/null +++ b/engine/src/Utils/Property/Distribution/BaseDistribution.h @@ -0,0 +1,32 @@ +/******************************************************************************* + * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ + +#pragma once + +#include <nlohmann/json.hpp> +#include <string> + +namespace OpenScenarioEngine::v1_3::Utils::Property::Distribution +{ + +/// @brief Base class for all distribution types +/// +/// Distributions and their values need to be defined by +/// means of string properties. For convenience, they can +/// be deserialized from a JSON into individual distributions. +/// See schemas/distributions/*.json for examples. +struct BaseDistribution +{ + std::string type; ///!< Type of the distribution + double min; ///!< Minimum value + double max; ///!< Maximum value +}; + +} // namespace OpenScenarioEngine::v1_3::Utils::Property::Distribution diff --git a/engine/src/Utils/Property/Distribution/ExponentialDistribution.h b/engine/src/Utils/Property/Distribution/ExponentialDistribution.h new file mode 100644 index 00000000..9fc2b083 --- /dev/null +++ b/engine/src/Utils/Property/Distribution/ExponentialDistribution.h @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ + +#pragma once + +#include <nlohmann/json.hpp> + +#include "BaseDistribution.h" + +namespace OpenScenarioEngine::v1_3::Utils::Property::Distribution +{ + +/// @brief Normal distribution +struct ExponentialDistribution : public BaseDistribution +{ + double lamda; ///!< Rate parameter +}; + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(ExponentialDistribution, type, min, max, lamda); + +} // namespace OpenScenarioEngine::v1_3::Utils::Property::Distribution diff --git a/engine/src/Utils/Property/Distribution/GammaDistribution.h b/engine/src/Utils/Property/Distribution/GammaDistribution.h new file mode 100644 index 00000000..703c3d02 --- /dev/null +++ b/engine/src/Utils/Property/Distribution/GammaDistribution.h @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ + +#pragma once + +#include <nlohmann/json.hpp> + +#include "BaseDistribution.h" + +namespace OpenScenarioEngine::v1_3::Utils::Property::Distribution +{ + +/// @brief Normal distribution +struct GammaDistribution : public BaseDistribution +{ + double shape; ///!< Shape parameter alpha + double scale; ///!< Scale parameter theta +}; + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(GammaDistribution, type, min, max, shape, scale); + +} // namespace OpenScenarioEngine::v1_3::Utils::Property::Distribution diff --git a/engine/src/Utils/Property/Distribution/LogNormalDistribution.h b/engine/src/Utils/Property/Distribution/LogNormalDistribution.h new file mode 100644 index 00000000..a78cb842 --- /dev/null +++ b/engine/src/Utils/Property/Distribution/LogNormalDistribution.h @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ + +#pragma once + +#include <nlohmann/json.hpp> + +#include "BaseDistribution.h" + +namespace OpenScenarioEngine::v1_3::Utils::Property::Distribution +{ + +/// @brief Log-normal distribution +struct LogNormalDistribution : public BaseDistribution +{ + double mu; ///!< Mean value + double sigma; ///!< Standard deviation +}; + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(LogNormalDistribution, type, min, max, mu, sigma); + +} // namespace OpenScenarioEngine::v1_3::Utils::Property::Distribution diff --git a/engine/src/Utils/Property/Distribution/NormalDistribution.h b/engine/src/Utils/Property/Distribution/NormalDistribution.h new file mode 100644 index 00000000..aa25d000 --- /dev/null +++ b/engine/src/Utils/Property/Distribution/NormalDistribution.h @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ + +#pragma once + +#include <nlohmann/json.hpp> + +#include "BaseDistribution.h" + +namespace OpenScenarioEngine::v1_3::Utils::Property::Distribution +{ + +/// @brief Normal distribution +struct NormalDistribution : public BaseDistribution +{ + double mean; ///!< Mean value + double stddev; ///!< Standard deviation +}; + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(NormalDistribution, type, min, max, mean, stddev); + +} // namespace OpenScenarioEngine::v1_3::Utils::Property::Distribution diff --git a/engine/src/Utils/Property/Distribution/UniformDistribution.h b/engine/src/Utils/Property/Distribution/UniformDistribution.h new file mode 100644 index 00000000..f0c465fd --- /dev/null +++ b/engine/src/Utils/Property/Distribution/UniformDistribution.h @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ + +#pragma once + +#include <nlohmann/json.hpp> + +#include "BaseDistribution.h" + +namespace OpenScenarioEngine::v1_3::Utils::Property::Distribution +{ + +/// @brief Uniform distribution +struct UniformDistribution : public BaseDistribution +{ + // No additional fields needed +}; + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(UniformDistribution, type, min, max); + +} // namespace OpenScenarioEngine::v1_3::Utils::Property::Distribution diff --git a/engine/src/Utils/Property/Distributions.h b/engine/src/Utils/Property/Distributions.h new file mode 100644 index 00000000..75b675a3 --- /dev/null +++ b/engine/src/Utils/Property/Distributions.h @@ -0,0 +1,18 @@ +/******************************************************************************* + * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ + +#pragma once + +#include "Distribution/BaseDistribution.h" +#include "Distribution/ExponentialDistribution.h" +#include "Distribution/GammaDistribution.h" +#include "Distribution/LogNormalDistribution.h" +#include "Distribution/NormalDistribution.h" +#include "Distribution/UniformDistribution.h" diff --git a/engine/src/Utils/Property/JsonParser.h b/engine/src/Utils/Property/JsonParser.h new file mode 100644 index 00000000..706368ee --- /dev/null +++ b/engine/src/Utils/Property/JsonParser.h @@ -0,0 +1,89 @@ +/******************************************************************************* + * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ + +#pragma once + +#include <nlohmann/json.hpp> +#include <optional> +#include <regex> +#include <string_view> + +#include "Utils/Logger.h" + +namespace OpenScenarioEngine::v1_3::Utils::Property +{ + +namespace detail +{ + +/// Replace all occurrences of a string in a string +/// @param input String to search in +/// @param search String to search for +/// @param replace String to replace with +/// @return String with all occurrences of search replaced with replace +std::string Replace(std::string_view input, const std::string& search, const std::string& replace) +{ + std::regex search_regex(search); + return std::regex_replace(std::string(input), search_regex, replace); +} + +/// Try to parse a JSON object from a property string +/// +/// In a first attempt, this function tries to parse the string as it is. +/// But as the main use case for the parser is interpreting XML properties +/// as JSON objects and the JSON standard requires double quotes for strings, +/// the XML parser will try to escape double quotes in the XML file using " +/// or try with single quotes (not JSON standard, but we support it). +/// @tparam DeserializedType Type of the object to deserialize (e.g. a distribution) +/// @param property Property to be parsed +/// @return Parsed JSON object or throw if parsing failed +template <typename DeserializedType> +std::optional<DeserializedType> ParseJson(std::string_view property) +{ + if (auto json = nlohmann::json::parse(std::string(property), nullptr, false); + !json.is_discarded()) + { + return json.get<DeserializedType>(); + } + + constexpr const char* DOUBLE_QUOTE{"\""}; + for (const std::string& search : {""", "'"}) + { + if (auto json = nlohmann::json::parse(Replace(property, search, DOUBLE_QUOTE), nullptr, false); + !json.is_discarded()) + { + return json.get<DeserializedType>(); + } + } + + return std::nullopt; +} + +} // namespace detail + +/// Deserialize a property string into a object using nlohmann::json +/// @tparam DeserializedType Type of the object to deserialize (e.g. a distribution) +/// @param property Property string to deserialize +/// @return Deserialized object of type T or empty optional if deserialization failed +template <typename DeserializedType> +std::optional<DeserializedType> ReadFromJson(std::string_view property) +{ + try + { + return detail::ParseJson<DeserializedType>(property); + } + catch (const nlohmann::json::out_of_range&) + { + Logger::Error("JsonParser: Property '" + std::string(property) + "' does not match expected object"); + } + return std::nullopt; +} + +} // namespace OpenScenarioEngine::v1_3::Utils::Property \ No newline at end of file diff --git a/engine/src/Utils/Property/schemas/distributions/base_distribution.json b/engine/src/Utils/Property/schemas/distributions/base_distribution.json new file mode 100644 index 00000000..cc55de3e --- /dev/null +++ b/engine/src/Utils/Property/schemas/distributions/base_distribution.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "BaseDistribution", + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "The type of the distribution, e.g. 'uniform', 'normal', etc." + }, + "min": { + "type": "number", + "description": "The minimum value of the distribution" + }, + "max": { + "type": "number", + "description": "The maximum value of the distribution" + } + }, + "required": ["min", "max"] +} \ No newline at end of file diff --git a/engine/src/Utils/Property/schemas/distributions/exponential_distribution.json b/engine/src/Utils/Property/schemas/distributions/exponential_distribution.json new file mode 100644 index 00000000..60eae64c --- /dev/null +++ b/engine/src/Utils/Property/schemas/distributions/exponential_distribution.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExponentialDistribution", + "allOf": [ + { + "$ref": "base_distribution.json" + }, + { + "if": { + "properties": { + "type": { + "enum": [ + "exponential", "Exponential", "ExponentialDistribution", "exponential_distribution", + "Exponential_Distribution", "exponential-distribution", "Exponential-Distribution", + "exponentialDistribution", "exponentialdistribution"] + } + } + }, + "then": { + "properties": { + "lambda": { + "type": "number", + "description": "Rate parameter of the distribution" + } + }, + "required": ["type", "min", "max", "lambda"] + } + } + ] +} \ No newline at end of file diff --git a/engine/src/Utils/Property/schemas/distributions/gamma_distribution.json b/engine/src/Utils/Property/schemas/distributions/gamma_distribution.json new file mode 100644 index 00000000..c07cb9cf --- /dev/null +++ b/engine/src/Utils/Property/schemas/distributions/gamma_distribution.json @@ -0,0 +1,34 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "GammaDistribution", + "allOf": [ + { + "$ref": "base_distribution.json" + }, + { + "if": { + "properties": { + "type": { + "enum": [ + "gamma", "Gamma", "GammaDistribution", "gamma_distribution", + "Gamma_Distribution", "gamma-distribution", "Gamma-Distribution", + "gammaDistribution", "gammadistribution"] + } + } + }, + "then": { + "properties": { + "shape": { + "type": "number", + "description": "shape parameter alpha" + }, + "scale": { + "type": "number", + "description": "scale parameter theta" + } + }, + "required": ["type", "min", "max", "shape", "scale"] + } + } + ] +} \ No newline at end of file diff --git a/engine/src/Utils/Property/schemas/distributions/lognormal_distribution.json b/engine/src/Utils/Property/schemas/distributions/lognormal_distribution.json new file mode 100644 index 00000000..90ea615d --- /dev/null +++ b/engine/src/Utils/Property/schemas/distributions/lognormal_distribution.json @@ -0,0 +1,35 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "LogNormalDistribution", + "allOf": [ + { + "$ref": "base_distribution.json" + }, + { + "if": { + "properties": { + "type": { + "enum": [ + "lognormal", "LogNormal", "LogNormalDistribution", "lognormal_distribution", + "LogNormal_Distribution", "lognormal-distribution", "LogNormal-Distribution", + "logNormalDistribution", "lognormaldistribution" + ] + } + } + }, + "then": { + "properties": { + "mu": { + "type": "number", + "description": "The mean of the natural logarithm of the distribution" + }, + "sigma": { + "type": "number", + "description": "The standard deviation of the natural logarithm of the distribution" + } + }, + "required": ["type", "min", "max", "mu", "sigma"] + } + } + ] +} \ No newline at end of file diff --git a/engine/src/Utils/Property/schemas/distributions/normal_distrubution.json b/engine/src/Utils/Property/schemas/distributions/normal_distrubution.json new file mode 100644 index 00000000..e4b3be83 --- /dev/null +++ b/engine/src/Utils/Property/schemas/distributions/normal_distrubution.json @@ -0,0 +1,35 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NormalDistribution", + "allOf": [ + { + "$ref": "base_distribution.json" + }, + { + "if": { + "properties": { + "type": { + "enum": [ + "normal", "Normal", "NormalDistribution", "normal_distribution", + "Normal_Distribution", "normal-distribution", "Normal-Distribution", + "normalDistribution", "normaldistribution" + ] + } + } + }, + "then": { + "properties": { + "mean": { + "type": "number", + "description": "The mean value of the distribution" + }, + "stddev": { + "type": "number", + "description": "The standard deviation of the distribution" + } + }, + "required": ["type", "min", "max", "mean", "stddev"] + } + } + ] +} \ No newline at end of file diff --git a/engine/src/Utils/Property/schemas/distributions/uniform_distribution.json b/engine/src/Utils/Property/schemas/distributions/uniform_distribution.json new file mode 100644 index 00000000..c7d19757 --- /dev/null +++ b/engine/src/Utils/Property/schemas/distributions/uniform_distribution.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "UniformDistribution", + "allOf": [ + { + "$ref": "base_distribution.json" + }, + { + "if": { + "properties": { + "type": { + "enum": [ + "uniform", "Uniform", "UniformDistribution", "uniform_distribution", + "Uniform_Distribution", "uniform-distribution", "Uniform-Distribution", + "uniformDistribution", "uniformdistribution"] + } + } + }, + "then": { + "required": ["type", "min", "max"] + } + } + ] +} \ No newline at end of file diff --git a/engine/src/Utils/Property/schemas/readme.md b/engine/src/Utils/Property/schemas/readme.md new file mode 100644 index 00000000..9e2391a1 --- /dev/null +++ b/engine/src/Utils/Property/schemas/readme.md @@ -0,0 +1,21 @@ +# JSON Schemas for Property Strings + +This folder contains JSON schemas for property strings used by the **OpenScenarioEngine**. +While these schemas are not enforced, they serve as a reference for how to properly define properties, such as distributions, in a way that they are correctly parsed. + +## Usage Example + +Here is an example of how to use these schemas within an OpenSCENARIO description: + +```xml +<Property key="MyCustomProperty" value="{'type': 'NormalDistribution', 'min': 0.0, 'max': 10.0, 'mean': 7.5, 'stddev': 4.0 }"> +``` + +In this example, a property with the key `MyCustomProperty` is defined using a JSON string that specifies a `NormalDistribution` with the following parameters: +- `min`: 0.0 +- `max`: 10.0 +- `mean`: 7.5 +- `stddev`: 4.0 + +Refer to the JSON schemas in this folder for more details on how to define other types of distributions and properties. +``` \ No newline at end of file diff --git a/engine/tests/Utils/JsonParserTest.cpp b/engine/tests/Utils/JsonParserTest.cpp new file mode 100644 index 00000000..58ef5dff --- /dev/null +++ b/engine/tests/Utils/JsonParserTest.cpp @@ -0,0 +1,106 @@ +#include <gmock/gmock.h> +#include <gtest/gtest.h> + +#include "Utils/Property/Distributions.h" +#include "Utils/Property/JsonParser.h" + +using testing::DoubleEq; +using testing::Eq; + +using namespace OpenScenarioEngine::v1_3::Utils::Property; + +TEST(JsonParserTest, GivenPropertyString_WhenStringIsValidUniformDistribition_DeserializesToObject) +{ + auto uniformDistribution = ReadFromJson<Distribution::UniformDistribution>( + R"({"type": "uniform", "min": 1.2, "max": 3.4 })"); + + ASSERT_TRUE(uniformDistribution.has_value()); + EXPECT_THAT(uniformDistribution->type, "uniform"); + EXPECT_THAT(uniformDistribution->min, DoubleEq(1.2)); + EXPECT_THAT(uniformDistribution->max, DoubleEq(3.4)); +} + +TEST(JsonParserTest, GivenPropertyString_WhenStringsAreEscaped_DeserializesToObject) +{ + auto uniformDistribution = ReadFromJson<Distribution::UniformDistribution>( + R"({"type": "uniform", "min": 1.2, "max": 3.4 })"); + + ASSERT_TRUE(uniformDistribution.has_value()); +} + +TEST(JsonParserTest, GivenPropertyString_WhenStringsAreInSingleQuotes_DeserializesToObject) +{ + auto uniformDistribution = ReadFromJson<Distribution::UniformDistribution>( + R"({'type': 'uniform', 'min': 1.2, 'max': 3.4 })"); + + ASSERT_TRUE(uniformDistribution.has_value()); +} + +TEST(JsonParserTest, GivenPropertyString_WhenStringIsValidNormalDistribition_DeserializesToObject) +{ + auto normalDistribution = ReadFromJson<Distribution::NormalDistribution>( + R"({"type": "normal", "min": 1.2, "max": 3.4 , "mean": 5.6, "stddev": 7.8 })"); + + ASSERT_TRUE(normalDistribution.has_value()); + EXPECT_THAT(normalDistribution->type, "normal"); + EXPECT_THAT(normalDistribution->min, DoubleEq(1.2)); + EXPECT_THAT(normalDistribution->max, DoubleEq(3.4)); + EXPECT_THAT(normalDistribution->mean, DoubleEq(5.6)); + EXPECT_THAT(normalDistribution->stddev, DoubleEq(7.8)); +} + +TEST(JsonParserTest, GivenPropertyString_WhenStringIsValidLogNormalDistribition_DeserializesToObject) +{ + auto logNormalDistribution = ReadFromJson<Distribution::LogNormalDistribution>( + R"({"type": "lognormal", "min": 1.2, "max": 3.4, "mu": 5.6, "sigma": 7.8 })"); + + ASSERT_TRUE(logNormalDistribution.has_value()); + EXPECT_THAT(logNormalDistribution->type, "lognormal"); + EXPECT_THAT(logNormalDistribution->min, DoubleEq(1.2)); + EXPECT_THAT(logNormalDistribution->max, DoubleEq(3.4)); + EXPECT_THAT(logNormalDistribution->mu, DoubleEq(5.6)); + EXPECT_THAT(logNormalDistribution->sigma, DoubleEq(7.8)); +} + +TEST(JsonParserTest, GivenPropertyString_WhenStringIsValidGammaDistribition_DeserializesToObject) +{ + auto gammaDistribution = ReadFromJson<Distribution::GammaDistribution>( + R"({"type": "gamma", "min": 1.2, "max": 3.4, "shape": 5.6, "scale": 7.8 })"); + + ASSERT_TRUE(gammaDistribution.has_value()); + EXPECT_THAT(gammaDistribution->type, "gamma"); + EXPECT_THAT(gammaDistribution->min, DoubleEq(1.2)); + EXPECT_THAT(gammaDistribution->max, DoubleEq(3.4)); + EXPECT_THAT(gammaDistribution->shape, DoubleEq(5.6)); + EXPECT_THAT(gammaDistribution->scale, DoubleEq(7.8)); +} + +TEST(JsonParserTest, GivenPropertyString_WhenStringIsValidExponentialDistribition_DeserializesToObject) +{ + auto exponentialDistribution = ReadFromJson<Distribution::ExponentialDistribution>( + R"({"type": "exponential", "min": 1.2, "max": 3.4, "lamda": 5.6 })"); + + ASSERT_TRUE(exponentialDistribution.has_value()); + EXPECT_THAT(exponentialDistribution->type, "exponential"); + EXPECT_THAT(exponentialDistribution->min, DoubleEq(1.2)); + EXPECT_THAT(exponentialDistribution->max, DoubleEq(3.4)); + EXPECT_THAT(exponentialDistribution->lamda, DoubleEq(5.6)); +} + +TEST(JsonParserTest, GivenPropertyString_WhenStringIsInvalidJson_ReturnsEmptyOptional) +{ + EXPECT_THAT(ReadFromJson<Distribution::UniformDistribution>("this is not a json"), Eq(std::nullopt)); + EXPECT_THAT(ReadFromJson<Distribution::NormalDistribution>("this is not a json"), Eq(std::nullopt)); + EXPECT_THAT(ReadFromJson<Distribution::LogNormalDistribution>("this is not a json"), Eq(std::nullopt)); + EXPECT_THAT(ReadFromJson<Distribution::GammaDistribution>("this is not a json"), Eq(std::nullopt)); + EXPECT_THAT(ReadFromJson<Distribution::ExponentialDistribution>("this is not a json"), Eq(std::nullopt)); +} + +TEST(JsonParserTest, GivenPropertyString_WhenJsonDoesNotMatch_ReturnsEmptyOptional) +{ + EXPECT_THAT(ReadFromJson<Distribution::UniformDistribution>(R"({"unknown_field": "some_value"})"), Eq(std::nullopt)); + EXPECT_THAT(ReadFromJson<Distribution::NormalDistribution>(R"({"unknown_field": "some_value"})"), Eq(std::nullopt)); + EXPECT_THAT(ReadFromJson<Distribution::LogNormalDistribution>(R"({"unknown_field": "some_value"})"), Eq(std::nullopt)); + EXPECT_THAT(ReadFromJson<Distribution::GammaDistribution>(R"({"unknown_field": "some_value"})"), Eq(std::nullopt)); + EXPECT_THAT(ReadFromJson<Distribution::ExponentialDistribution>(R"({"unknown_field": "some_value"})"), Eq(std::nullopt)); +} -- GitLab From bf23b2ddb81dcb3cf0f7c3d390fa456060fed7c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Paris?= <rene.paris@in-tech.com> Date: Wed, 19 Mar 2025 14:52:11 +0000 Subject: [PATCH 3/6] feat(PropertyInterpreter): Generic structure for custom property interpretation --- .gitignore | 15 +- .../exponential_distribution.json | 20 +++ doc/json_schemas/gamma_distribution.json | 28 +++ doc/json_schemas/lognormal_distribution.json | 20 +++ doc/json_schemas/normal_distrubution.json | 28 +++ .../schemas => doc/json_schemas}/readme.md | 0 doc/json_schemas/uniform_distribution.json | 18 ++ engine/CMakeLists.txt | 4 +- engine/cmake/generated_files.cmake | 15 ++ engine/src/Utils/JsonParsing.cpp | 60 +++++++ engine/src/Utils/JsonParsing.h | 63 +++++++ .../Property/Distribution/BaseDistribution.h | 32 ---- .../Distribution/ExponentialDistribution.h | 28 --- .../Property/Distribution/GammaDistribution.h | 29 --- .../Distribution/LogNormalDistribution.h | 29 --- .../Distribution/NormalDistribution.h | 29 --- engine/src/Utils/Property/JsonParser.h | 89 ---------- .../distributions/base_distribution.json | 20 --- .../exponential_distribution.json | 30 ---- .../distributions/gamma_distribution.json | 34 ---- .../distributions/lognormal_distribution.json | 35 ---- .../distributions/normal_distrubution.json | 35 ---- .../distributions/uniform_distribution.json | 24 --- .../DistributionInterpreter.cpp | 65 +++++++ .../DistributionInterpreter.h | 104 +++++++++++ .../Distributions.h | 11 +- .../Distributions/ExponentialDistribution.h | 31 ++++ .../Distributions/GammaDistribution.h | 39 +++++ .../Distributions/LogNormalDistribution.h | 33 ++++ .../Distributions/NormalDistribution.h | 33 ++++ .../Distributions}/UniformDistribution.h | 17 +- .../PropertyInterpretation/Interpreter.h | 123 +++++++++++++ .../PropertyInterpretation/JsonInterpreter.h | 43 +++++ .../PropertyInterpretation/RawValueRelay.h | 43 +++++ engine/src/Utils/PropertyInterpreter.cpp | 71 ++++++++ engine/src/Utils/PropertyInterpreter.h | 55 ++++++ engine/tests/Utils/DistributionTest.cpp | 160 +++++++++++++++++ engine/tests/Utils/JsonParserTest.cpp | 106 ----------- engine/tests/Utils/JsonParsingTest.cpp | 80 +++++++++ .../tests/Utils/PropertyInterpreterTest.cpp | 165 ++++++++++++++++++ 40 files changed, 1324 insertions(+), 540 deletions(-) create mode 100644 doc/json_schemas/exponential_distribution.json create mode 100644 doc/json_schemas/gamma_distribution.json create mode 100644 doc/json_schemas/lognormal_distribution.json create mode 100644 doc/json_schemas/normal_distrubution.json rename {engine/src/Utils/Property/schemas => doc/json_schemas}/readme.md (100%) create mode 100644 doc/json_schemas/uniform_distribution.json create mode 100644 engine/src/Utils/JsonParsing.cpp create mode 100644 engine/src/Utils/JsonParsing.h delete mode 100644 engine/src/Utils/Property/Distribution/BaseDistribution.h delete mode 100644 engine/src/Utils/Property/Distribution/ExponentialDistribution.h delete mode 100644 engine/src/Utils/Property/Distribution/GammaDistribution.h delete mode 100644 engine/src/Utils/Property/Distribution/LogNormalDistribution.h delete mode 100644 engine/src/Utils/Property/Distribution/NormalDistribution.h delete mode 100644 engine/src/Utils/Property/JsonParser.h delete mode 100644 engine/src/Utils/Property/schemas/distributions/base_distribution.json delete mode 100644 engine/src/Utils/Property/schemas/distributions/exponential_distribution.json delete mode 100644 engine/src/Utils/Property/schemas/distributions/gamma_distribution.json delete mode 100644 engine/src/Utils/Property/schemas/distributions/lognormal_distribution.json delete mode 100644 engine/src/Utils/Property/schemas/distributions/normal_distrubution.json delete mode 100644 engine/src/Utils/Property/schemas/distributions/uniform_distribution.json create mode 100644 engine/src/Utils/PropertyInterpretation/DistributionInterpreter.cpp create mode 100644 engine/src/Utils/PropertyInterpretation/DistributionInterpreter.h rename engine/src/Utils/{Property => PropertyInterpretation}/Distributions.h (63%) create mode 100644 engine/src/Utils/PropertyInterpretation/Distributions/ExponentialDistribution.h create mode 100644 engine/src/Utils/PropertyInterpretation/Distributions/GammaDistribution.h create mode 100644 engine/src/Utils/PropertyInterpretation/Distributions/LogNormalDistribution.h create mode 100644 engine/src/Utils/PropertyInterpretation/Distributions/NormalDistribution.h rename engine/src/Utils/{Property/Distribution => PropertyInterpretation/Distributions}/UniformDistribution.h (56%) create mode 100644 engine/src/Utils/PropertyInterpretation/Interpreter.h create mode 100644 engine/src/Utils/PropertyInterpretation/JsonInterpreter.h create mode 100644 engine/src/Utils/PropertyInterpretation/RawValueRelay.h create mode 100644 engine/src/Utils/PropertyInterpreter.cpp create mode 100644 engine/src/Utils/PropertyInterpreter.h create mode 100644 engine/tests/Utils/DistributionTest.cpp delete mode 100644 engine/tests/Utils/JsonParserTest.cpp create mode 100644 engine/tests/Utils/JsonParsingTest.cpp create mode 100644 engine/tests/Utils/PropertyInterpreterTest.cpp diff --git a/.gitignore b/.gitignore index fc6f72e2..f87582a8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,14 @@ +__pycache__ +.devcontainer .dll +.env +.pytest_cache .so +.venv .vscode -.pytest_cache -__pycache__ -generator.log -*.orig *.bkp -.devcontainer \ No newline at end of file +*.orig +build +deps +generator.log +MODULE.* diff --git a/doc/json_schemas/exponential_distribution.json b/doc/json_schemas/exponential_distribution.json new file mode 100644 index 00000000..1d294b38 --- /dev/null +++ b/doc/json_schemas/exponential_distribution.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExponentialDistribution", + "type": "object", + "properties": { + "lowerLimit": { + "type": "number", + "description": "Lower limit" + }, + "upperLimit": { + "type": "number", + "description": "Upper limit" + }, + "expectedValue": { + "type": "number", + "description": "Reciprocal of the rate parameter lambda" + } + }, + "required": ["lowerLimit", "upperLimit", "expectedValue"] +} \ No newline at end of file diff --git a/doc/json_schemas/gamma_distribution.json b/doc/json_schemas/gamma_distribution.json new file mode 100644 index 00000000..479cb799 --- /dev/null +++ b/doc/json_schemas/gamma_distribution.json @@ -0,0 +1,28 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "GammaDistribution", + "type": "object", + "properties": { + "lowerLimit": { + "type": "number", + "description": "Lower limit", + "minimum": 0.0 + }, + "upperLimit": { + "type": "number", + "description": "Upper limit", + "minimum": 0.0 + }, + "expectedValue": { + "type": "number", + "description": "Expected value (mean) of the distribution, calculated as alpha / beta", + "minimum": 0.0 + }, + "variance": { + "type": "number", + "description": "Variance of the distribution, calculated as alpha / beta^2", + "minimum": 0.0 + } + }, + "required": ["lowerLimit", "upperLimit", "expectedValue", "variance"] +} \ No newline at end of file diff --git a/doc/json_schemas/lognormal_distribution.json b/doc/json_schemas/lognormal_distribution.json new file mode 100644 index 00000000..1d294b38 --- /dev/null +++ b/doc/json_schemas/lognormal_distribution.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExponentialDistribution", + "type": "object", + "properties": { + "lowerLimit": { + "type": "number", + "description": "Lower limit" + }, + "upperLimit": { + "type": "number", + "description": "Upper limit" + }, + "expectedValue": { + "type": "number", + "description": "Reciprocal of the rate parameter lambda" + } + }, + "required": ["lowerLimit", "upperLimit", "expectedValue"] +} \ No newline at end of file diff --git a/doc/json_schemas/normal_distrubution.json b/doc/json_schemas/normal_distrubution.json new file mode 100644 index 00000000..3021ae8e --- /dev/null +++ b/doc/json_schemas/normal_distrubution.json @@ -0,0 +1,28 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NormalDistribution", + "type": "object", + "properties": { + "lowerLimit": { + "type": "number", + "description": "Lower limit", + "minimum": 0.0 + }, + "upperLimit": { + "type": "number", + "description": "Upper limit", + "minimum": 0.0 + }, + "expectedValue": { + "type": "number", + "description": "Expected value", + "minimum": 0.0 + }, + "variance": { + "type": "number", + "description": "Variance (square of standard deviation)", + "minimum": 0.0 + } + }, + "required": ["lowerLimit", "upperLimit", "expectedValue", "variance"] +} \ No newline at end of file diff --git a/engine/src/Utils/Property/schemas/readme.md b/doc/json_schemas/readme.md similarity index 100% rename from engine/src/Utils/Property/schemas/readme.md rename to doc/json_schemas/readme.md diff --git a/doc/json_schemas/uniform_distribution.json b/doc/json_schemas/uniform_distribution.json new file mode 100644 index 00000000..1532d5e2 --- /dev/null +++ b/doc/json_schemas/uniform_distribution.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "UniformDistribution", + "type": "object", + "properties": { + "lowerLimit": { + "type": "number", + "description": "Lower limit", + "minimum": 0.0 + }, + "upperLimit": { + "type": "number", + "description": "Upper limit", + "minimum": 0.0 + } + }, + "required": ["lowerLimit", "upperLimit"] +} \ No newline at end of file diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index b490fff9..74ad6e96 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -223,10 +223,12 @@ target_sources( ${CMAKE_CURRENT_LIST_DIR}/tests/TrafficSignalParsing/TreeGenerationTest.cpp ${CMAKE_CURRENT_LIST_DIR}/tests/Utils/ConstantsTest.cpp ${CMAKE_CURRENT_LIST_DIR}/tests/Utils/ControllerCreatorTest.cpp + ${CMAKE_CURRENT_LIST_DIR}/tests/Utils/DistributionTest.cpp ${CMAKE_CURRENT_LIST_DIR}/tests/Utils/EllipseTest.cpp ${CMAKE_CURRENT_LIST_DIR}/tests/Utils/EntityCreatorTest.cpp ${CMAKE_CURRENT_LIST_DIR}/tests/Utils/EntityUtilsTest.cpp - ${CMAKE_CURRENT_LIST_DIR}/tests/Utils/JsonParserTest.cpp + ${CMAKE_CURRENT_LIST_DIR}/tests/Utils/JsonParsingTest.cpp + ${CMAKE_CURRENT_LIST_DIR}/tests/Utils/PropertyInterpreterTest.cpp ) add_custom_command(TARGET ${PROJECT_NAME}Test diff --git a/engine/cmake/generated_files.cmake b/engine/cmake/generated_files.cmake index 6644cd17..be6db101 100644 --- a/engine/cmake/generated_files.cmake +++ b/engine/cmake/generated_files.cmake @@ -242,6 +242,9 @@ list(APPEND ${PROJECT_NAME}_SOURCES src/Utils/EntityUtils.cpp src/Utils/EventPrioritizer.cpp src/Utils/Logger.cpp + src/Utils/PropertyInterpretation/DistributionInterpreter.cpp + src/Utils/PropertyInterpreter.cpp + src/Utils/JsonParsing.cpp src/Utils/TrafficSignalBuilder.cpp ) @@ -610,5 +613,17 @@ list(APPEND ${PROJECT_NAME}_HEADERS src/Utils/IControllerService.h src/Utils/IEventPrioritizer.h src/Utils/Logger.h + src/Utils/PropertyInterpretation/DistributionInterpreter.h + src/Utils/PropertyInterpretation/Distributions.h + src/Utils/PropertyInterpretation/Distributions/ExponentialDistribution.h + src/Utils/PropertyInterpretation/Distributions/GammaDistribution.h + src/Utils/PropertyInterpretation/Distributions/LogNormalDistribution.h + src/Utils/PropertyInterpretation/Distributions/NormalDistribution.h + src/Utils/PropertyInterpretation/Distributions/UniformDistribution.h + src/Utils/PropertyInterpretation/Interpreter.h + src/Utils/PropertyInterpretation/JsonInterpreter.h + src/Utils/PropertyInterpretation/RawValueRelay.h + src/Utils/PropertyInterpreter.h + src/Utils/JsonParsing.h src/Utils/TrafficSignalBuilder.h ) diff --git a/engine/src/Utils/JsonParsing.cpp b/engine/src/Utils/JsonParsing.cpp new file mode 100644 index 00000000..dc26d3b5 --- /dev/null +++ b/engine/src/Utils/JsonParsing.cpp @@ -0,0 +1,60 @@ +/******************************************************************************* + * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ + +#include "JsonParsing.h" + +#include <regex> + +namespace OpenScenarioEngine::v1_3 +{ + +namespace detail +{ + +/// Replace all occurrences of a string in a string +/// @param input String to search in +/// @param search String to search for +/// @param replace String to replace with +/// @return String with all occurrences of search replaced with replace +std::string Replace(std::string_view input, const std::string& search, const std::string& replace) +{ + std::regex search_regex(search); + return std::regex_replace(std::string(input), search_regex, replace); +} + +} // namespace detail + +namespace json +{ + +std::optional<nlohmann::json> TryParse(std::string_view property) +{ + if (auto j = nlohmann::json::parse(std::string(property), nullptr, false); + !j.is_discarded()) + { + return j; + } + + using namespace std::string_literals; + for (const std::string& search : {"""s, "'"s}) + { + const auto modified_property = detail::Replace(property, search, "\""); + if (auto j = nlohmann::json::parse(modified_property, nullptr, false); + !j.is_discarded()) + { + return j; + } + } + + return std::nullopt; +} + +} // namespace json +} // namespace OpenScenarioEngine::v1_3 diff --git a/engine/src/Utils/JsonParsing.h b/engine/src/Utils/JsonParsing.h new file mode 100644 index 00000000..ba716982 --- /dev/null +++ b/engine/src/Utils/JsonParsing.h @@ -0,0 +1,63 @@ +/******************************************************************************* + * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ + +#pragma once + +#include <nlohmann/json.hpp> +#include <optional> +#include <regex> +#include <string> +#include <string_view> + +#include "Utils/Logger.h" + +namespace OpenScenarioEngine::v1_3 +{ + +namespace json +{ + +/// Try to parse a JSON object from a string +/// +/// In a first attempt, this function tries to parse the string as it is. +/// But as the main use case for the parser is interpreting XML properties +/// as JSON objects and the JSON standard requires double quotes for strings, +/// the XML parser will try to escape double quotes in the XML file using " +/// or try with single quotes (not JSON standard, but we support it). +/// @param value Value to be parsed +/// @return Parsed JSON object or nullopt if parsing failed +std::optional<nlohmann::json> TryParse(std::string_view value); + +/// Deserialize a string into a object using nlohmann::json +/// @tparam DeserializedType Type of the object to deserialize (e.g. a distribution) +/// @param value String to deserialize +/// @return Deserialized object of type T or empty optional if deserialization failed +template <typename DeserializedType> +std::optional<DeserializedType> Deserialize(std::string_view value) +{ + try + { + if (auto j = TryParse(value)) + { + return j->get<DeserializedType>(); + } + Logger::Error("JsonParser: \'" + std::string(value) + "' is not a valid JSON object"); + } + catch (const nlohmann::json::out_of_range&) + { + Logger::Error("JsonParser: \'" + std::string(value) + "' JSON object does not match expected object"); + } + + return std::nullopt; +} + +} // namespace json + +} // namespace OpenScenarioEngine::v1_3 \ No newline at end of file diff --git a/engine/src/Utils/Property/Distribution/BaseDistribution.h b/engine/src/Utils/Property/Distribution/BaseDistribution.h deleted file mode 100644 index 30c7e68d..00000000 --- a/engine/src/Utils/Property/Distribution/BaseDistribution.h +++ /dev/null @@ -1,32 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - *******************************************************************************/ - -#pragma once - -#include <nlohmann/json.hpp> -#include <string> - -namespace OpenScenarioEngine::v1_3::Utils::Property::Distribution -{ - -/// @brief Base class for all distribution types -/// -/// Distributions and their values need to be defined by -/// means of string properties. For convenience, they can -/// be deserialized from a JSON into individual distributions. -/// See schemas/distributions/*.json for examples. -struct BaseDistribution -{ - std::string type; ///!< Type of the distribution - double min; ///!< Minimum value - double max; ///!< Maximum value -}; - -} // namespace OpenScenarioEngine::v1_3::Utils::Property::Distribution diff --git a/engine/src/Utils/Property/Distribution/ExponentialDistribution.h b/engine/src/Utils/Property/Distribution/ExponentialDistribution.h deleted file mode 100644 index 9fc2b083..00000000 --- a/engine/src/Utils/Property/Distribution/ExponentialDistribution.h +++ /dev/null @@ -1,28 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - *******************************************************************************/ - -#pragma once - -#include <nlohmann/json.hpp> - -#include "BaseDistribution.h" - -namespace OpenScenarioEngine::v1_3::Utils::Property::Distribution -{ - -/// @brief Normal distribution -struct ExponentialDistribution : public BaseDistribution -{ - double lamda; ///!< Rate parameter -}; - -NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(ExponentialDistribution, type, min, max, lamda); - -} // namespace OpenScenarioEngine::v1_3::Utils::Property::Distribution diff --git a/engine/src/Utils/Property/Distribution/GammaDistribution.h b/engine/src/Utils/Property/Distribution/GammaDistribution.h deleted file mode 100644 index 703c3d02..00000000 --- a/engine/src/Utils/Property/Distribution/GammaDistribution.h +++ /dev/null @@ -1,29 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - *******************************************************************************/ - -#pragma once - -#include <nlohmann/json.hpp> - -#include "BaseDistribution.h" - -namespace OpenScenarioEngine::v1_3::Utils::Property::Distribution -{ - -/// @brief Normal distribution -struct GammaDistribution : public BaseDistribution -{ - double shape; ///!< Shape parameter alpha - double scale; ///!< Scale parameter theta -}; - -NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(GammaDistribution, type, min, max, shape, scale); - -} // namespace OpenScenarioEngine::v1_3::Utils::Property::Distribution diff --git a/engine/src/Utils/Property/Distribution/LogNormalDistribution.h b/engine/src/Utils/Property/Distribution/LogNormalDistribution.h deleted file mode 100644 index a78cb842..00000000 --- a/engine/src/Utils/Property/Distribution/LogNormalDistribution.h +++ /dev/null @@ -1,29 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - *******************************************************************************/ - -#pragma once - -#include <nlohmann/json.hpp> - -#include "BaseDistribution.h" - -namespace OpenScenarioEngine::v1_3::Utils::Property::Distribution -{ - -/// @brief Log-normal distribution -struct LogNormalDistribution : public BaseDistribution -{ - double mu; ///!< Mean value - double sigma; ///!< Standard deviation -}; - -NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(LogNormalDistribution, type, min, max, mu, sigma); - -} // namespace OpenScenarioEngine::v1_3::Utils::Property::Distribution diff --git a/engine/src/Utils/Property/Distribution/NormalDistribution.h b/engine/src/Utils/Property/Distribution/NormalDistribution.h deleted file mode 100644 index aa25d000..00000000 --- a/engine/src/Utils/Property/Distribution/NormalDistribution.h +++ /dev/null @@ -1,29 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - *******************************************************************************/ - -#pragma once - -#include <nlohmann/json.hpp> - -#include "BaseDistribution.h" - -namespace OpenScenarioEngine::v1_3::Utils::Property::Distribution -{ - -/// @brief Normal distribution -struct NormalDistribution : public BaseDistribution -{ - double mean; ///!< Mean value - double stddev; ///!< Standard deviation -}; - -NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(NormalDistribution, type, min, max, mean, stddev); - -} // namespace OpenScenarioEngine::v1_3::Utils::Property::Distribution diff --git a/engine/src/Utils/Property/JsonParser.h b/engine/src/Utils/Property/JsonParser.h deleted file mode 100644 index 706368ee..00000000 --- a/engine/src/Utils/Property/JsonParser.h +++ /dev/null @@ -1,89 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - *******************************************************************************/ - -#pragma once - -#include <nlohmann/json.hpp> -#include <optional> -#include <regex> -#include <string_view> - -#include "Utils/Logger.h" - -namespace OpenScenarioEngine::v1_3::Utils::Property -{ - -namespace detail -{ - -/// Replace all occurrences of a string in a string -/// @param input String to search in -/// @param search String to search for -/// @param replace String to replace with -/// @return String with all occurrences of search replaced with replace -std::string Replace(std::string_view input, const std::string& search, const std::string& replace) -{ - std::regex search_regex(search); - return std::regex_replace(std::string(input), search_regex, replace); -} - -/// Try to parse a JSON object from a property string -/// -/// In a first attempt, this function tries to parse the string as it is. -/// But as the main use case for the parser is interpreting XML properties -/// as JSON objects and the JSON standard requires double quotes for strings, -/// the XML parser will try to escape double quotes in the XML file using " -/// or try with single quotes (not JSON standard, but we support it). -/// @tparam DeserializedType Type of the object to deserialize (e.g. a distribution) -/// @param property Property to be parsed -/// @return Parsed JSON object or throw if parsing failed -template <typename DeserializedType> -std::optional<DeserializedType> ParseJson(std::string_view property) -{ - if (auto json = nlohmann::json::parse(std::string(property), nullptr, false); - !json.is_discarded()) - { - return json.get<DeserializedType>(); - } - - constexpr const char* DOUBLE_QUOTE{"\""}; - for (const std::string& search : {""", "'"}) - { - if (auto json = nlohmann::json::parse(Replace(property, search, DOUBLE_QUOTE), nullptr, false); - !json.is_discarded()) - { - return json.get<DeserializedType>(); - } - } - - return std::nullopt; -} - -} // namespace detail - -/// Deserialize a property string into a object using nlohmann::json -/// @tparam DeserializedType Type of the object to deserialize (e.g. a distribution) -/// @param property Property string to deserialize -/// @return Deserialized object of type T or empty optional if deserialization failed -template <typename DeserializedType> -std::optional<DeserializedType> ReadFromJson(std::string_view property) -{ - try - { - return detail::ParseJson<DeserializedType>(property); - } - catch (const nlohmann::json::out_of_range&) - { - Logger::Error("JsonParser: Property '" + std::string(property) + "' does not match expected object"); - } - return std::nullopt; -} - -} // namespace OpenScenarioEngine::v1_3::Utils::Property \ No newline at end of file diff --git a/engine/src/Utils/Property/schemas/distributions/base_distribution.json b/engine/src/Utils/Property/schemas/distributions/base_distribution.json deleted file mode 100644 index cc55de3e..00000000 --- a/engine/src/Utils/Property/schemas/distributions/base_distribution.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "BaseDistribution", - "type": "object", - "properties": { - "type": { - "type": "string", - "description": "The type of the distribution, e.g. 'uniform', 'normal', etc." - }, - "min": { - "type": "number", - "description": "The minimum value of the distribution" - }, - "max": { - "type": "number", - "description": "The maximum value of the distribution" - } - }, - "required": ["min", "max"] -} \ No newline at end of file diff --git a/engine/src/Utils/Property/schemas/distributions/exponential_distribution.json b/engine/src/Utils/Property/schemas/distributions/exponential_distribution.json deleted file mode 100644 index 60eae64c..00000000 --- a/engine/src/Utils/Property/schemas/distributions/exponential_distribution.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ExponentialDistribution", - "allOf": [ - { - "$ref": "base_distribution.json" - }, - { - "if": { - "properties": { - "type": { - "enum": [ - "exponential", "Exponential", "ExponentialDistribution", "exponential_distribution", - "Exponential_Distribution", "exponential-distribution", "Exponential-Distribution", - "exponentialDistribution", "exponentialdistribution"] - } - } - }, - "then": { - "properties": { - "lambda": { - "type": "number", - "description": "Rate parameter of the distribution" - } - }, - "required": ["type", "min", "max", "lambda"] - } - } - ] -} \ No newline at end of file diff --git a/engine/src/Utils/Property/schemas/distributions/gamma_distribution.json b/engine/src/Utils/Property/schemas/distributions/gamma_distribution.json deleted file mode 100644 index c07cb9cf..00000000 --- a/engine/src/Utils/Property/schemas/distributions/gamma_distribution.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "GammaDistribution", - "allOf": [ - { - "$ref": "base_distribution.json" - }, - { - "if": { - "properties": { - "type": { - "enum": [ - "gamma", "Gamma", "GammaDistribution", "gamma_distribution", - "Gamma_Distribution", "gamma-distribution", "Gamma-Distribution", - "gammaDistribution", "gammadistribution"] - } - } - }, - "then": { - "properties": { - "shape": { - "type": "number", - "description": "shape parameter alpha" - }, - "scale": { - "type": "number", - "description": "scale parameter theta" - } - }, - "required": ["type", "min", "max", "shape", "scale"] - } - } - ] -} \ No newline at end of file diff --git a/engine/src/Utils/Property/schemas/distributions/lognormal_distribution.json b/engine/src/Utils/Property/schemas/distributions/lognormal_distribution.json deleted file mode 100644 index 90ea615d..00000000 --- a/engine/src/Utils/Property/schemas/distributions/lognormal_distribution.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "LogNormalDistribution", - "allOf": [ - { - "$ref": "base_distribution.json" - }, - { - "if": { - "properties": { - "type": { - "enum": [ - "lognormal", "LogNormal", "LogNormalDistribution", "lognormal_distribution", - "LogNormal_Distribution", "lognormal-distribution", "LogNormal-Distribution", - "logNormalDistribution", "lognormaldistribution" - ] - } - } - }, - "then": { - "properties": { - "mu": { - "type": "number", - "description": "The mean of the natural logarithm of the distribution" - }, - "sigma": { - "type": "number", - "description": "The standard deviation of the natural logarithm of the distribution" - } - }, - "required": ["type", "min", "max", "mu", "sigma"] - } - } - ] -} \ No newline at end of file diff --git a/engine/src/Utils/Property/schemas/distributions/normal_distrubution.json b/engine/src/Utils/Property/schemas/distributions/normal_distrubution.json deleted file mode 100644 index e4b3be83..00000000 --- a/engine/src/Utils/Property/schemas/distributions/normal_distrubution.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "NormalDistribution", - "allOf": [ - { - "$ref": "base_distribution.json" - }, - { - "if": { - "properties": { - "type": { - "enum": [ - "normal", "Normal", "NormalDistribution", "normal_distribution", - "Normal_Distribution", "normal-distribution", "Normal-Distribution", - "normalDistribution", "normaldistribution" - ] - } - } - }, - "then": { - "properties": { - "mean": { - "type": "number", - "description": "The mean value of the distribution" - }, - "stddev": { - "type": "number", - "description": "The standard deviation of the distribution" - } - }, - "required": ["type", "min", "max", "mean", "stddev"] - } - } - ] -} \ No newline at end of file diff --git a/engine/src/Utils/Property/schemas/distributions/uniform_distribution.json b/engine/src/Utils/Property/schemas/distributions/uniform_distribution.json deleted file mode 100644 index c7d19757..00000000 --- a/engine/src/Utils/Property/schemas/distributions/uniform_distribution.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "UniformDistribution", - "allOf": [ - { - "$ref": "base_distribution.json" - }, - { - "if": { - "properties": { - "type": { - "enum": [ - "uniform", "Uniform", "UniformDistribution", "uniform_distribution", - "Uniform_Distribution", "uniform-distribution", "Uniform-Distribution", - "uniformDistribution", "uniformdistribution"] - } - } - }, - "then": { - "required": ["type", "min", "max"] - } - } - ] -} \ No newline at end of file diff --git a/engine/src/Utils/PropertyInterpretation/DistributionInterpreter.cpp b/engine/src/Utils/PropertyInterpretation/DistributionInterpreter.cpp new file mode 100644 index 00000000..479a44cc --- /dev/null +++ b/engine/src/Utils/PropertyInterpretation/DistributionInterpreter.cpp @@ -0,0 +1,65 @@ +/******************************************************************************** + * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ + +#include "DistributionInterpreter.h" + +#include <Stochastics/StochasticsInterface.h> + +#include <algorithm> + +namespace OpenScenarioEngine::v1_3::PropertyInterpretation +{ + +double sample(StochasticsInterface &stochastics, const ExponentialDistribution &distribution) +{ + return std::clamp( + stochastics.GetExponentialDistributed( + distribution.expectedValue != 0.0 ? 1.0 / distribution.expectedValue : 0.0), + distribution.lowerLimit, + distribution.upperLimit); +} + +double sample(StochasticsInterface &stochastics, const GammaDistribution &distribution) +{ + return std::clamp( + stochastics.GetGammaDistributed( + distribution.expectedValue, std::sqrt(std::abs(distribution.variance))), + distribution.lowerLimit, + distribution.upperLimit); +} + +double sample(StochasticsInterface &stochastics, const LogNormalDistribution &distribution) +{ + return std::clamp( + stochastics.GetLogNormalDistributed( + distribution.expectedValue, std::sqrt(std::abs(distribution.variance))), + distribution.lowerLimit, + distribution.upperLimit); +} + +double sample(StochasticsInterface &stochastics, const NormalDistribution &distribution) +{ + return std::clamp( + stochastics.GetNormalDistributed( + distribution.expectedValue, std::sqrt(std::abs(distribution.variance))), + distribution.lowerLimit, + distribution.upperLimit); +} + +double sample(StochasticsInterface &stochastics, const UniformDistribution &distribution) +{ + return std::clamp( + stochastics.GetUniformDistributed( + distribution.lowerLimit, distribution.upperLimit), + distribution.lowerLimit, + distribution.upperLimit); +} + +} // namespace OpenScenarioEngine::v1_3::PropertyInterpretation \ No newline at end of file diff --git a/engine/src/Utils/PropertyInterpretation/DistributionInterpreter.h b/engine/src/Utils/PropertyInterpretation/DistributionInterpreter.h new file mode 100644 index 00000000..7b16b836 --- /dev/null +++ b/engine/src/Utils/PropertyInterpretation/DistributionInterpreter.h @@ -0,0 +1,104 @@ +/******************************************************************************** + * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ + +#pragma once + +#include "Utils/Logger.h" +#include "Utils/PropertyInterpretation/Distributions.h" +#include "Utils/PropertyInterpretation/JsonInterpreter.h" + +class StochasticsInterface; + +namespace OpenScenarioEngine::v1_3::PropertyInterpretation +{ + +/// Sample from an exponential distribution +/// @param stochastics Stochastics interface +/// @param distribution Definition of the distribution +/// @return sampled value +double sample(StochasticsInterface &stochastics, const ExponentialDistribution &distribution); + +/// Sample from a gamma distribution +/// @param stochastics Stochastics interface +/// @param distribution Definition of the distribution +/// @return sampled value +double sample(StochasticsInterface &stochastics, const GammaDistribution &distribution); + +/// Sample from a log normal distribution +/// @param stochastics Stochastics interface +/// @param distribution Definition of the distribution +/// @return sampled value +double sample(StochasticsInterface &stochastics, const LogNormalDistribution &distribution); + +/// Sample from a normal distribution +/// @param stochastics Stochastics interface +/// @param distribution Definition of the distribution +/// @return sampled value +double sample(StochasticsInterface &stochastics, const NormalDistribution &distribution); + +/// Sample from a uniform distribution +/// @param stochastics Stochastics interface +/// @param distribution Definition of the distribution +/// @return sampled value +double sample(StochasticsInterface &stochastics, const UniformDistribution &distribution); + +/// Interpreter for distributions +/// @tparam Distribution Wrapped distribution type +template <typename Distribution> +struct DistributionInterpreter : public JsonInterpreter<Distribution> +{ + using JsonInterpreter<Distribution>::value_; ///!< Internal representation + + /// Binds Interpreter to stochastics interface + /// @param stochastics Stochastics interface + constexpr DistributionInterpreter(StochasticsInterface &stochastics) noexcept + : stochastics_{stochastics} + { + } + + /// Sample from the distribution + /// @return string interpretation of the internal representation, empty if json interpretation failed + std::string to_string() + { + if (value_) + { + return std::to_string(sample(stochastics_, *value_)); + } + Logger::Error("DistributionInterpreter: No distribution value available."); + return ""; + } + + StochasticsInterface &stochastics_; +}; + +/// Interpret a json object using a custom interpreter +/// @note Specialize for custom behavor +/// @tparam T Type of the interpreter +/// @tparam Payload Type of the payload +/// @param interpreter Interpreter to interpret the payload +/// @param payload Payload to interpret +template <typename T, typename Payload> +void interpret(DistributionInterpreter<T> &interpreter, const Payload &payload) +{ + interpreter.interpret(payload); +} + +/// Convert internal representation to string +/// @note Specialize for custom behavor +/// @tparam T Type of the interpreter +/// @param interpreter Interpreter to convert to string +/// @return string interpretation of the internal representation +template <typename T> +std::string to_string(DistributionInterpreter<T> &interpreter) +{ + return interpreter.to_string(); +} + +} // namespace OpenScenarioEngine::v1_3::PropertyInterpretation \ No newline at end of file diff --git a/engine/src/Utils/Property/Distributions.h b/engine/src/Utils/PropertyInterpretation/Distributions.h similarity index 63% rename from engine/src/Utils/Property/Distributions.h rename to engine/src/Utils/PropertyInterpretation/Distributions.h index 75b675a3..c4919f77 100644 --- a/engine/src/Utils/Property/Distributions.h +++ b/engine/src/Utils/PropertyInterpretation/Distributions.h @@ -10,9 +10,8 @@ #pragma once -#include "Distribution/BaseDistribution.h" -#include "Distribution/ExponentialDistribution.h" -#include "Distribution/GammaDistribution.h" -#include "Distribution/LogNormalDistribution.h" -#include "Distribution/NormalDistribution.h" -#include "Distribution/UniformDistribution.h" +#include "Distributions/ExponentialDistribution.h" +#include "Distributions/GammaDistribution.h" +#include "Distributions/LogNormalDistribution.h" +#include "Distributions/NormalDistribution.h" +#include "Distributions/UniformDistribution.h" diff --git a/engine/src/Utils/PropertyInterpretation/Distributions/ExponentialDistribution.h b/engine/src/Utils/PropertyInterpretation/Distributions/ExponentialDistribution.h new file mode 100644 index 00000000..7605671f --- /dev/null +++ b/engine/src/Utils/PropertyInterpretation/Distributions/ExponentialDistribution.h @@ -0,0 +1,31 @@ +/******************************************************************************* + * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ + +#pragma once + +#include <nlohmann/json.hpp> + +namespace OpenScenarioEngine::v1_3::PropertyInterpretation +{ + +/// Exponential distribution +struct ExponentialDistribution +{ + double lowerLimit; ///!< Lower limit + double upperLimit; ///!< Upper limit + double expectedValue; ///!< Reciprocal of the rate parameter lambda +}; + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(ExponentialDistribution, + lowerLimit, + upperLimit, + expectedValue); + +} // namespace OpenScenarioEngine::v1_3::PropertyInterpretation diff --git a/engine/src/Utils/PropertyInterpretation/Distributions/GammaDistribution.h b/engine/src/Utils/PropertyInterpretation/Distributions/GammaDistribution.h new file mode 100644 index 00000000..1e382fe7 --- /dev/null +++ b/engine/src/Utils/PropertyInterpretation/Distributions/GammaDistribution.h @@ -0,0 +1,39 @@ +/******************************************************************************* + * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ + +#pragma once + +#include <nlohmann/json.hpp> + +namespace OpenScenarioEngine::v1_3::PropertyInterpretation +{ + +/// Gamma distribution +/// +/// The gamma distribution is a two-parameter family of continuous probability distributions. +/// It is characterized by a shape parameter (alpha) and a rate parameter (beta). +/// The expected value and variance of the gamma distribution are related to these parameters as follows: +/// - Expected Value = alpha / beta +/// - Variance = alpha / beta^2 +struct GammaDistribution +{ + double lowerLimit; ///!< Lower limit + double upperLimit; ///!< Upper limit + double expectedValue; ///!< Expected value (mean) of the distribution, calculated as alpha / beta + double variance; ///!< Variance of the distribution, calculated as alpha / beta^2 +}; + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(GammaDistribution, + lowerLimit, + upperLimit, + expectedValue, + variance); + +} // namespace OpenScenarioEngine::v1_3::PropertyInterpretation diff --git a/engine/src/Utils/PropertyInterpretation/Distributions/LogNormalDistribution.h b/engine/src/Utils/PropertyInterpretation/Distributions/LogNormalDistribution.h new file mode 100644 index 00000000..1aca4fc4 --- /dev/null +++ b/engine/src/Utils/PropertyInterpretation/Distributions/LogNormalDistribution.h @@ -0,0 +1,33 @@ +/******************************************************************************* + * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ + +#pragma once + +#include <nlohmann/json.hpp> + +namespace OpenScenarioEngine::v1_3::PropertyInterpretation +{ + +/// Normal distribution +struct LogNormalDistribution +{ + double lowerLimit; ///!< Lower limit + double upperLimit; ///!< Upper limit + double expectedValue; ///!< Expected value + double variance; ///!< Variance (square of standard deviation) +}; + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(LogNormalDistribution, + lowerLimit, + upperLimit, + expectedValue, + variance); + +} // namespace OpenScenarioEngine::v1_3 \ No newline at end of file diff --git a/engine/src/Utils/PropertyInterpretation/Distributions/NormalDistribution.h b/engine/src/Utils/PropertyInterpretation/Distributions/NormalDistribution.h new file mode 100644 index 00000000..d2a572f4 --- /dev/null +++ b/engine/src/Utils/PropertyInterpretation/Distributions/NormalDistribution.h @@ -0,0 +1,33 @@ +/******************************************************************************* + * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ + +#pragma once + +#include <nlohmann/json.hpp> + +namespace OpenScenarioEngine::v1_3::PropertyInterpretation +{ + +/// Normal distribution +struct NormalDistribution +{ + double lowerLimit; ///!< Lower limit + double upperLimit; ///!< Upper limit + double expectedValue; ///!< Expected value + double variance; ///!< Variance (square of standard deviation) +}; + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(NormalDistribution, + lowerLimit, + upperLimit, + expectedValue, + variance); + +} // namespace OpenScenarioEngine::v1_3::PropertyInterpretation diff --git a/engine/src/Utils/Property/Distribution/UniformDistribution.h b/engine/src/Utils/PropertyInterpretation/Distributions/UniformDistribution.h similarity index 56% rename from engine/src/Utils/Property/Distribution/UniformDistribution.h rename to engine/src/Utils/PropertyInterpretation/Distributions/UniformDistribution.h index f0c465fd..05b4b145 100644 --- a/engine/src/Utils/Property/Distribution/UniformDistribution.h +++ b/engine/src/Utils/PropertyInterpretation/Distributions/UniformDistribution.h @@ -12,17 +12,18 @@ #include <nlohmann/json.hpp> -#include "BaseDistribution.h" - -namespace OpenScenarioEngine::v1_3::Utils::Property::Distribution +namespace OpenScenarioEngine::v1_3::PropertyInterpretation { -/// @brief Uniform distribution -struct UniformDistribution : public BaseDistribution +/// Uniform distribution +struct UniformDistribution { - // No additional fields needed + double lowerLimit; ///!< Lower limit + double upperLimit; ///!< Upper limit }; -NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(UniformDistribution, type, min, max); +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(UniformDistribution, + lowerLimit, + upperLimit); -} // namespace OpenScenarioEngine::v1_3::Utils::Property::Distribution +} // namespace OpenScenarioEngine::v1_3::PropertyInterpretation diff --git a/engine/src/Utils/PropertyInterpretation/Interpreter.h b/engine/src/Utils/PropertyInterpretation/Interpreter.h new file mode 100644 index 00000000..0df771c4 --- /dev/null +++ b/engine/src/Utils/PropertyInterpretation/Interpreter.h @@ -0,0 +1,123 @@ +/******************************************************************************** + * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ + +#pragma once + +#include <map> +#include <memory> +#include <nlohmann/json.hpp> +#include <string> +#include <utility> +#include <vector> + +namespace OpenScenarioEngine::v1_3::PropertyInterpretation +{ + +/// Type-erased base for custom interpreter +class Interpreter +{ + /// Concept for custom interpreter + struct InterpreterConcept + { + /// Destructor + virtual ~InterpreterConcept() {} + + /// Interpret json object and hide internal representation + /// @param j json object to interpret + virtual void do_interpret(const nlohmann::json &j) = 0; + + /// Convert internal representation to string + virtual std::string do_to_string() = 0; + + /// Clone the interpreter + virtual std::unique_ptr<InterpreterConcept> clone() const = 0; + }; + + /// Model for custom interpreter + /// @tparam InterpreterT Actual interpreter type which shall be type-erased + template <typename InterpreterT> + struct InterpreterModel : public InterpreterConcept + { + InterpreterModel(InterpreterT interpreter) + : interpreter_{std::move(interpreter)} + { + } + + std::unique_ptr<InterpreterConcept> clone() const override + { + return std::make_unique<InterpreterModel>(*this); + } + + /// Relay to free function parse, which must be implemented for each custom interpreter + /// @param j json object to interpret + void do_interpret(const nlohmann::json &j) override + { + interpret(interpreter_, j); + } + + /// Relay to free function to_string, which must be implemented for each custom interpreter + /// @return string interpretation of the internal representation + std::string do_to_string() override + { + return to_string(interpreter_); + } + + InterpreterT interpreter_; ///!< Actual interpreter + }; + + /// Free function to interpret json object (injects interpret into the base class) + /// @param interpreter interpreter to interpret the json object + /// @param j json object to interpret + friend void interpret(Interpreter &interpreter, const nlohmann::json &j) + { + interpreter.pimpl_->do_interpret(j); + } + + /// Free function to convert internal representation to string (injects to_string into the base class) + /// @param interpreter interpreter used to convert to string + /// @return string interpretation of the internal representation + friend std::string to_string(Interpreter &interpreter) + { + return interpreter.pimpl_->do_to_string(); + } + + std::unique_ptr<InterpreterConcept> pimpl_; ///!< Type-erased interpreter + +public: + /// Constructor to create a type erased custom interpreter + /// @tparam InterpreterT Actual interpreter type + /// @param interpreter Actual interpreter + template <typename InterpreterT> + Interpreter(InterpreterT interpreter) + : pimpl_{std::make_unique<InterpreterModel<InterpreterT>>(std::move(interpreter))} + { + } + + /// Copy constructor + /// @param other + Interpreter(const Interpreter &other) + : pimpl_{other.pimpl_->clone()} + { + } + + /// Copy assignment operator + /// @param other + /// @return New interpreter + Interpreter &operator=(const Interpreter &other) + { + other.pimpl_->clone().swap(pimpl_); + return *this; + } + + Interpreter(Interpreter &&) = default; ///!< Move constructor + Interpreter &operator=(Interpreter &&) = default; ///!< Move assignment operator +}; + +} // namespace OpenScenarioEngine::v1_3::PropertyInterpretation diff --git a/engine/src/Utils/PropertyInterpretation/JsonInterpreter.h b/engine/src/Utils/PropertyInterpretation/JsonInterpreter.h new file mode 100644 index 00000000..be2baa76 --- /dev/null +++ b/engine/src/Utils/PropertyInterpretation/JsonInterpreter.h @@ -0,0 +1,43 @@ +/******************************************************************************** + * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ + +#pragma once + +#include <nlohmann/json.hpp> +#include <optional> +#include <string> + +namespace OpenScenarioEngine::v1_3::PropertyInterpretation +{ + +/// Interpreter for json objects +/// @tparam T Type for deserialization +template <typename T> +struct JsonInterpreter +{ + /// Interpret and store the json object + /// @note If the json object cannot be interpreted, the internal value will be std::nullopt + /// @param j json object to interpret + void interpret(const nlohmann::json &j) + { + try + { + value_ = j.get<T>(); + } + catch (const nlohmann::json::out_of_range &) + { + value_ = std::nullopt; + } + } + + std::optional<T> value_; +}; + +} // namespace OpenScenarioEngine::v1_3::PropertyInterpretation \ No newline at end of file diff --git a/engine/src/Utils/PropertyInterpretation/RawValueRelay.h b/engine/src/Utils/PropertyInterpretation/RawValueRelay.h new file mode 100644 index 00000000..26246b48 --- /dev/null +++ b/engine/src/Utils/PropertyInterpretation/RawValueRelay.h @@ -0,0 +1,43 @@ +/******************************************************************************** + * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ + +#pragma once + +#include <string> +#include <utility> + +namespace OpenScenarioEngine::v1_3::PropertyInterpretation +{ + +/// Default interpreter if no custom interpreter is available or can be resolved +struct RawValueRelay +{ + std::string value_; +}; + +/// Void operation needed for type-erased interpreter stage +/// @tparam ...DontCare Catches all parameters, if any +/// @param raw_value_relay Actual intepreter +/// @param ... ignored +template <typename... DontCare> +void interpret(const RawValueRelay &raw_value_relay, DontCare...) +{ + std::ignore = raw_value_relay; +} + +/// Return the internal representation without doing anything +/// @param raw_value_relay Actual interpreter +/// @return stored string +std::string to_string(const RawValueRelay &raw_value_relay) +{ + return raw_value_relay.value_; +} + +} // namespace OpenScenarioEngine::v1_3::PropertyInterpretation \ No newline at end of file diff --git a/engine/src/Utils/PropertyInterpreter.cpp b/engine/src/Utils/PropertyInterpreter.cpp new file mode 100644 index 00000000..4071c912 --- /dev/null +++ b/engine/src/Utils/PropertyInterpreter.cpp @@ -0,0 +1,71 @@ +/******************************************************************************** + * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ + +#include "PropertyInterpreter.h" + +#include <algorithm> + +#include "Utils/JsonParsing.h" +#include "Utils/PropertyInterpretation/DistributionInterpreter.h" +#include "Utils/PropertyInterpretation/RawValueRelay.h" + +namespace OpenScenarioEngine::v1_3 +{ + +using namespace PropertyInterpretation; + +namespace detail +{ +std::pair<std::string, nlohmann::json> GetTypeAndValue(const std::string &property_value) +{ + if (auto j = json::TryParse(property_value); + j.has_value() && j->contains("type") && j->contains("value")) + { + return {j->at("type").get<std::string>(), j->at("value")}; + } + return {"", nlohmann::json{}}; +} + +} // namespace detail + +void PropertyInterpreter::Init( + const std::map<std::string, std::string> &properties) +{ + interpreter_mapping_.clear(); + for (const auto &[property_key, property_value] : properties) + { + const auto [json_type, json_value] = detail::GetTypeAndValue(property_value); + + const auto interpreter_iter = interpreter_prototypes_.find(json_type); + auto [interpreter, _] = interpreter_mapping_.try_emplace( + property_key, + interpreter_iter != interpreter_prototypes_.end() + ? interpreter_iter->second + : RawValueRelay{property_value}); + interpret(interpreter->second, json_value); + } +} + +std::map<std::string, std::string> PropertyInterpreter::GenerateParameters( + const std::map<std::string, std::string> &key_replacements) +{ + std::map<std::string, std::string> parameters; + for (auto &[key, value] : interpreter_mapping_) + { + auto iter = key_replacements.find(key); + parameters.emplace(iter == std::end(key_replacements) + ? key + : iter->second, + to_string(value)); + } + return parameters; +} + +} // namespace OpenScenarioEngine::v1_3 diff --git a/engine/src/Utils/PropertyInterpreter.h b/engine/src/Utils/PropertyInterpreter.h new file mode 100644 index 00000000..e115d630 --- /dev/null +++ b/engine/src/Utils/PropertyInterpreter.h @@ -0,0 +1,55 @@ +/******************************************************************************** + * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ + +#pragma once + +#include <map> +#include <string> +#include <utility> + +#include "Utils/PropertyInterpretation/Interpreter.h" + +namespace OpenScenarioEngine::v1_3 +{ + +/// Utility class to interpret properties, e.g. custom json distribution definitions +/// +/// This class can take a map of properties and interpret them using custom interpreters. +/// Each property is interpreted by a custom interpreter based the value of the property. +/// If a json object is found, the "type" field is used to determine the interpreter. +/// In such a case, the "value" field is relayed to the interpreter for interpretation. +/// If no interpreter is found or the json has no field "type" and "value", the property +/// is treated as a raw value (= string). +class PropertyInterpreter +{ +public: + using Interpreters = std::map<std::string, PropertyInterpretation::Interpreter>; + + /// Constructor to create a property interpreter + /// @param interpreter_prototypes Prototypes for custom interpreters + PropertyInterpreter(Interpreters interpreter_prototypes) + : interpreter_prototypes_{std::move(interpreter_prototypes)} {} + + /// Initialize the property interpreter for a given set of properties + /// @param properties Properties to interpret + void Init(const std::map<std::string, std::string> &properties); + + /// Generate parameters from the interpreted properties, given at initialization + /// @note Might return different values at every call, e.g. for stochastic properties + /// @param key_replacements defines a mapping from original keys to new keys + /// @return interpreted parameter map + std::map<std::string, std::string> GenerateParameters( + const std::map<std::string, std::string> &key_replacements = {}); + +private: + Interpreters interpreter_prototypes_{}; ///!< Available custom interpreters + Interpreters interpreter_mapping_{}; ///!< Property to interpreter relation +}; +} // namespace OpenScenarioEngine::v1_3 diff --git a/engine/tests/Utils/DistributionTest.cpp b/engine/tests/Utils/DistributionTest.cpp new file mode 100644 index 00000000..51c4ce2b --- /dev/null +++ b/engine/tests/Utils/DistributionTest.cpp @@ -0,0 +1,160 @@ +/******************************************************************************** + * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ + +#include <gmock/gmock.h> +#include <gtest/gtest.h> + +#include "TestUtils/MockStochastics.h" +#include "Utils/PropertyInterpretation/DistributionInterpreter.h" +#include "Utils/PropertyInterpretation/Distributions.h" + +using namespace OpenScenarioEngine::v1_3; +using namespace OpenScenarioEngine::v1_3::PropertyInterpretation; +using namespace testing; + +class DistributionTest : public Test +{ +protected: + MockStochastics stochastics; +}; + +struct DistributionTestParams +{ + double lowerLimit; + double upperLimit; + double sampledValue; + double expectedValue; +}; + +class ExponentialDistributionTest : public DistributionTest, public WithParamInterface<DistributionTestParams> +{ +}; + +TEST_P(ExponentialDistributionTest, GivenExponentialDistribution_WhenSampleIsCalled_ThenReturnsClampedValue) +{ + auto params = GetParam(); + ExponentialDistribution distribution{params.lowerLimit, params.upperLimit, 2.0}; + ON_CALL(stochastics, GetExponentialDistributed(1.0 / 2.0)) + .WillByDefault(Return(params.sampledValue)); + + double result = sample(stochastics, distribution); + + EXPECT_THAT(result, DoubleEq(params.expectedValue)); +} + +INSTANTIATE_TEST_SUITE_P( + ExponentialDistributionTests, + ExponentialDistributionTest, + ::testing::Values( + DistributionTestParams{0.0, 10.0, 5.0, 5.0}, // regular test + DistributionTestParams{0.0, 10.0, -1.0, 0.0}, // lowerLimit test + DistributionTestParams{0.0, 10.0, 15.0, 10.0}, // upperLimit test + DistributionTestParams{0.0, 10.0, 0.0, 0.0} // division by zero test for expectedValue + )); + +class GammaDistributionTest : public DistributionTest, public WithParamInterface<DistributionTestParams> +{ +}; + +TEST_P(GammaDistributionTest, GivenGammaDistribution_WhenSampleIsCalled_ThenReturnsClampedValue) +{ + auto params = GetParam(); + GammaDistribution distribution{params.lowerLimit, params.upperLimit, 2.0, 4.0}; + ON_CALL(stochastics, GetGammaDistributed(2.0, std::sqrt(4.0))) + .WillByDefault(Return(params.sampledValue)); + + double result = sample(stochastics, distribution); + + EXPECT_THAT(result, DoubleEq(params.expectedValue)); +} + +INSTANTIATE_TEST_SUITE_P( + GammaDistributionTests, + GammaDistributionTest, + ::testing::Values( + DistributionTestParams{0.0, 10.0, 5.0, 5.0}, // regular test + DistributionTestParams{0.0, 10.0, -1.0, 0.0}, // lowerLimit test + DistributionTestParams{0.0, 10.0, 15.0, 10.0} // upperLimit test + )); + +class LogNormalDistributionTest : public DistributionTest, public WithParamInterface<DistributionTestParams> +{ +}; + +TEST_P(LogNormalDistributionTest, GivenLogNormalDistribution_WhenSampleIsCalled_ThenReturnsClampedValue) +{ + auto params = GetParam(); + LogNormalDistribution distribution{params.lowerLimit, params.upperLimit, 2.0, 4.0}; + ON_CALL(stochastics, GetLogNormalDistributed(2.0, std::sqrt(4.0))) + .WillByDefault(Return(params.sampledValue)); + + double result = sample(stochastics, distribution); + + EXPECT_THAT(result, DoubleEq(params.expectedValue)); +} + +INSTANTIATE_TEST_SUITE_P( + LogNormalDistributionTests, + LogNormalDistributionTest, + ::testing::Values( + DistributionTestParams{0.0, 10.0, 5.0, 5.0}, // regular test + DistributionTestParams{0.0, 10.0, -1.0, 0.0}, // lowerLimit test + DistributionTestParams{0.0, 10.0, 15.0, 10.0} // upperLimit test + )); + +class NormalDistributionTest : public DistributionTest, public WithParamInterface<DistributionTestParams> +{ +}; + +TEST_P(NormalDistributionTest, GivenNormalDistribution_WhenSampleIsCalled_ThenReturnsClampedValue) +{ + auto params = GetParam(); + NormalDistribution distribution{params.lowerLimit, params.upperLimit, 2.0, 4.0}; + ON_CALL(stochastics, GetNormalDistributed(2.0, std::sqrt(4.0))) + .WillByDefault(Return(params.sampledValue)); + + double result = sample(stochastics, distribution); + + EXPECT_THAT(result, DoubleEq(params.expectedValue)); +} + +INSTANTIATE_TEST_SUITE_P( + NormalDistributionTests, + NormalDistributionTest, + ::testing::Values( + DistributionTestParams{0.0, 10.0, 5.0, 5.0}, // regular test + DistributionTestParams{0.0, 10.0, -1.0, 0.0}, // lowerLimit test + DistributionTestParams{0.0, 10.0, 15.0, 10.0} // upperLimit test + )); + +class UniformDistributionTest : public DistributionTest, public WithParamInterface<DistributionTestParams> +{ +}; + +TEST_P(UniformDistributionTest, GivenUniformDistribution_WhenSampleIsCalled_ThenReturnsClampedValue) +{ + auto params = GetParam(); + UniformDistribution distribution{params.lowerLimit, params.upperLimit}; + ON_CALL(stochastics, GetUniformDistributed(params.lowerLimit, params.upperLimit)) + .WillByDefault(Return(params.sampledValue)); + + double result = sample(stochastics, distribution); + + EXPECT_THAT(result, DoubleEq(params.expectedValue)); +} + +INSTANTIATE_TEST_SUITE_P( + UniformDistributionTests, + UniformDistributionTest, + ::testing::Values( + DistributionTestParams{0.0, 10.0, 5.0, 5.0}, // regular test + DistributionTestParams{0.0, 10.0, -1.0, 0.0}, // lowerLimit test + DistributionTestParams{0.0, 10.0, 15.0, 10.0} // upperLimit test + )); diff --git a/engine/tests/Utils/JsonParserTest.cpp b/engine/tests/Utils/JsonParserTest.cpp deleted file mode 100644 index 58ef5dff..00000000 --- a/engine/tests/Utils/JsonParserTest.cpp +++ /dev/null @@ -1,106 +0,0 @@ -#include <gmock/gmock.h> -#include <gtest/gtest.h> - -#include "Utils/Property/Distributions.h" -#include "Utils/Property/JsonParser.h" - -using testing::DoubleEq; -using testing::Eq; - -using namespace OpenScenarioEngine::v1_3::Utils::Property; - -TEST(JsonParserTest, GivenPropertyString_WhenStringIsValidUniformDistribition_DeserializesToObject) -{ - auto uniformDistribution = ReadFromJson<Distribution::UniformDistribution>( - R"({"type": "uniform", "min": 1.2, "max": 3.4 })"); - - ASSERT_TRUE(uniformDistribution.has_value()); - EXPECT_THAT(uniformDistribution->type, "uniform"); - EXPECT_THAT(uniformDistribution->min, DoubleEq(1.2)); - EXPECT_THAT(uniformDistribution->max, DoubleEq(3.4)); -} - -TEST(JsonParserTest, GivenPropertyString_WhenStringsAreEscaped_DeserializesToObject) -{ - auto uniformDistribution = ReadFromJson<Distribution::UniformDistribution>( - R"({"type": "uniform", "min": 1.2, "max": 3.4 })"); - - ASSERT_TRUE(uniformDistribution.has_value()); -} - -TEST(JsonParserTest, GivenPropertyString_WhenStringsAreInSingleQuotes_DeserializesToObject) -{ - auto uniformDistribution = ReadFromJson<Distribution::UniformDistribution>( - R"({'type': 'uniform', 'min': 1.2, 'max': 3.4 })"); - - ASSERT_TRUE(uniformDistribution.has_value()); -} - -TEST(JsonParserTest, GivenPropertyString_WhenStringIsValidNormalDistribition_DeserializesToObject) -{ - auto normalDistribution = ReadFromJson<Distribution::NormalDistribution>( - R"({"type": "normal", "min": 1.2, "max": 3.4 , "mean": 5.6, "stddev": 7.8 })"); - - ASSERT_TRUE(normalDistribution.has_value()); - EXPECT_THAT(normalDistribution->type, "normal"); - EXPECT_THAT(normalDistribution->min, DoubleEq(1.2)); - EXPECT_THAT(normalDistribution->max, DoubleEq(3.4)); - EXPECT_THAT(normalDistribution->mean, DoubleEq(5.6)); - EXPECT_THAT(normalDistribution->stddev, DoubleEq(7.8)); -} - -TEST(JsonParserTest, GivenPropertyString_WhenStringIsValidLogNormalDistribition_DeserializesToObject) -{ - auto logNormalDistribution = ReadFromJson<Distribution::LogNormalDistribution>( - R"({"type": "lognormal", "min": 1.2, "max": 3.4, "mu": 5.6, "sigma": 7.8 })"); - - ASSERT_TRUE(logNormalDistribution.has_value()); - EXPECT_THAT(logNormalDistribution->type, "lognormal"); - EXPECT_THAT(logNormalDistribution->min, DoubleEq(1.2)); - EXPECT_THAT(logNormalDistribution->max, DoubleEq(3.4)); - EXPECT_THAT(logNormalDistribution->mu, DoubleEq(5.6)); - EXPECT_THAT(logNormalDistribution->sigma, DoubleEq(7.8)); -} - -TEST(JsonParserTest, GivenPropertyString_WhenStringIsValidGammaDistribition_DeserializesToObject) -{ - auto gammaDistribution = ReadFromJson<Distribution::GammaDistribution>( - R"({"type": "gamma", "min": 1.2, "max": 3.4, "shape": 5.6, "scale": 7.8 })"); - - ASSERT_TRUE(gammaDistribution.has_value()); - EXPECT_THAT(gammaDistribution->type, "gamma"); - EXPECT_THAT(gammaDistribution->min, DoubleEq(1.2)); - EXPECT_THAT(gammaDistribution->max, DoubleEq(3.4)); - EXPECT_THAT(gammaDistribution->shape, DoubleEq(5.6)); - EXPECT_THAT(gammaDistribution->scale, DoubleEq(7.8)); -} - -TEST(JsonParserTest, GivenPropertyString_WhenStringIsValidExponentialDistribition_DeserializesToObject) -{ - auto exponentialDistribution = ReadFromJson<Distribution::ExponentialDistribution>( - R"({"type": "exponential", "min": 1.2, "max": 3.4, "lamda": 5.6 })"); - - ASSERT_TRUE(exponentialDistribution.has_value()); - EXPECT_THAT(exponentialDistribution->type, "exponential"); - EXPECT_THAT(exponentialDistribution->min, DoubleEq(1.2)); - EXPECT_THAT(exponentialDistribution->max, DoubleEq(3.4)); - EXPECT_THAT(exponentialDistribution->lamda, DoubleEq(5.6)); -} - -TEST(JsonParserTest, GivenPropertyString_WhenStringIsInvalidJson_ReturnsEmptyOptional) -{ - EXPECT_THAT(ReadFromJson<Distribution::UniformDistribution>("this is not a json"), Eq(std::nullopt)); - EXPECT_THAT(ReadFromJson<Distribution::NormalDistribution>("this is not a json"), Eq(std::nullopt)); - EXPECT_THAT(ReadFromJson<Distribution::LogNormalDistribution>("this is not a json"), Eq(std::nullopt)); - EXPECT_THAT(ReadFromJson<Distribution::GammaDistribution>("this is not a json"), Eq(std::nullopt)); - EXPECT_THAT(ReadFromJson<Distribution::ExponentialDistribution>("this is not a json"), Eq(std::nullopt)); -} - -TEST(JsonParserTest, GivenPropertyString_WhenJsonDoesNotMatch_ReturnsEmptyOptional) -{ - EXPECT_THAT(ReadFromJson<Distribution::UniformDistribution>(R"({"unknown_field": "some_value"})"), Eq(std::nullopt)); - EXPECT_THAT(ReadFromJson<Distribution::NormalDistribution>(R"({"unknown_field": "some_value"})"), Eq(std::nullopt)); - EXPECT_THAT(ReadFromJson<Distribution::LogNormalDistribution>(R"({"unknown_field": "some_value"})"), Eq(std::nullopt)); - EXPECT_THAT(ReadFromJson<Distribution::GammaDistribution>(R"({"unknown_field": "some_value"})"), Eq(std::nullopt)); - EXPECT_THAT(ReadFromJson<Distribution::ExponentialDistribution>(R"({"unknown_field": "some_value"})"), Eq(std::nullopt)); -} diff --git a/engine/tests/Utils/JsonParsingTest.cpp b/engine/tests/Utils/JsonParsingTest.cpp new file mode 100644 index 00000000..d6287007 --- /dev/null +++ b/engine/tests/Utils/JsonParsingTest.cpp @@ -0,0 +1,80 @@ +/******************************************************************************** + * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ + +#include <gmock/gmock.h> +#include <gtest/gtest.h> + +#include "Utils/JsonParsing.h" + +using testing::DoubleEq; +using testing::Eq; + +using namespace OpenScenarioEngine::v1_3; + +struct DeserializationTestType +{ + int a; + double b; + std::string c; +}; + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(DeserializationTestType, a, b, c); + +TEST(JsonParsingTest, GivenPropertyString_WhenStringUsesDoubleQuotes_ThenReturnsObject) +{ + auto deserializationTestType = json::Deserialize<DeserializationTestType>( + R"({"a": 1, "b": 2.34, "c": "test"})"); + + ASSERT_TRUE(deserializationTestType.has_value()); + EXPECT_THAT(deserializationTestType->a, Eq(1)); + EXPECT_THAT(deserializationTestType->b, DoubleEq(2.34)); + EXPECT_THAT(deserializationTestType->c, Eq("test")); +} + +TEST(JsonParsingTest, GivenPropertyString_WhenStringUsesSingleQuotes_ThenReturnsObject) +{ + auto deserializationTestType = json::Deserialize<DeserializationTestType>( + R"({'a': 1, 'b': 2.34, 'c': 'test'})"); + + ASSERT_TRUE(deserializationTestType.has_value()); + EXPECT_THAT(deserializationTestType->a, Eq(1)); + EXPECT_THAT(deserializationTestType->b, DoubleEq(2.34)); + EXPECT_THAT(deserializationTestType->c, Eq("test")); +} + +TEST(JsonParsingTest, GivenPropertyString_WhenStringUsesExcapedXmlQuotes_ThenReturnsObject) +{ + auto deserializationTestType = json::Deserialize<DeserializationTestType>( + R"({"a": 1, "b": 2.34, "c": "test"})"); + + ASSERT_TRUE(deserializationTestType.has_value()); + EXPECT_THAT(deserializationTestType->a, Eq(1)); + EXPECT_THAT(deserializationTestType->b, DoubleEq(2.34)); + EXPECT_THAT(deserializationTestType->c, Eq("test")); +} + +TEST(JsonParsingTest, GivenPropertyStringWithMoreFieldsThanNeccessary_WhenParsed_ReturnsObject) +{ + auto deserializationTestType = json::Deserialize<DeserializationTestType>( + R"({"a": 1, "b": 2.34, "c": "test", "d": 4})"); + + ASSERT_TRUE(deserializationTestType.has_value()); + EXPECT_THAT(deserializationTestType->a, Eq(1)); + EXPECT_THAT(deserializationTestType->b, DoubleEq(2.34)); + EXPECT_THAT(deserializationTestType->c, Eq("test")); +} + +TEST(JsonParsingTest, GivenPropertyString_WhenStringIsInvalid_ThenReturnsEmpty) +{ + auto deserializationTestType = json::Deserialize<DeserializationTestType>( + R"({a: 1, b: 2.34, c: test})"); + + ASSERT_FALSE(deserializationTestType.has_value()); +} \ No newline at end of file diff --git a/engine/tests/Utils/PropertyInterpreterTest.cpp b/engine/tests/Utils/PropertyInterpreterTest.cpp new file mode 100644 index 00000000..f17a955f --- /dev/null +++ b/engine/tests/Utils/PropertyInterpreterTest.cpp @@ -0,0 +1,165 @@ +/******************************************************************************** + * Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ + +#include <gmock/gmock.h> +#include <gtest/gtest.h> + +#include "TestUtils/MockStochastics.h" +#include "Utils/PropertyInterpretation/DistributionInterpreter.h" +#include "Utils/PropertyInterpretation/Distributions.h" +#include "Utils/PropertyInterpreter.h" + +using namespace OpenScenarioEngine::v1_3; +using namespace OpenScenarioEngine::v1_3::PropertyInterpretation; +using namespace testing; + +struct PropertyInterpreterTest : public Test +{ + void SetUp() override + { + ON_CALL(stochastics_, GetUniformDistributed(0, 1)).WillByDefault(Return(0.123)); + // mean = 2, stddev = 3 ==> sqrt(variance = 9) + ON_CALL(stochastics_, GetNormalDistributed(2, 3)).WillByDefault(Return(2.345)); + } + + PropertyInterpreter::Interpreters GetInterpreterPrototypes() + { + return { + {"UniformDistribution", DistributionInterpreter<UniformDistribution>{stochastics_}}, + {"NormalDistribution", DistributionInterpreter<NormalDistribution>{stochastics_}}}; + } + + NiceMock<MockStochastics> stochastics_; +}; + +TEST_F(PropertyInterpreterTest, GivenProperties_WhenCallingPropertyInterpreter_ThenInterpretsResults) +{ + PropertyInterpreter property_interpreter(GetInterpreterPrototypes()); + property_interpreter.Init( + {{"prop0", R"({"type": "UniformDistribution", "value": {"lowerLimit": 0, "upperLimit": 1} })"}, + {"prop1", R"({"type": "NormalDistribution", "value": {"lowerLimit": 0, "upperLimit": 10, "expectedValue": 2, "variance": 9} })"}, + {"prop2", "RAW_PROPERTY"}}); + auto result = property_interpreter.GenerateParameters(); + + ASSERT_THAT(result, testing::SizeIs(3)); + EXPECT_EQ(result["prop0"], "0.123000"); + EXPECT_EQ(result["prop1"], "2.345000"); + EXPECT_EQ(result["prop2"], "RAW_PROPERTY"); +} + +TEST_F(PropertyInterpreterTest, GivenPropertiesWithExtendedFields_WhenCalled_ThenInterpretsValues) +{ + std::map<std::string, std::string> test_properties{ + {"extended", R"( + { + "filter": ["extra"], + "mapping": "custom_property", + "type": "UniformDistribution", + "value": {"lowerLimit": 0, "upperLimit": 1} + })"}}; + + PropertyInterpreter property_interpreter(GetInterpreterPrototypes()); + property_interpreter.Init(test_properties); + auto result = property_interpreter.GenerateParameters(); + + ASSERT_THAT(result, testing::SizeIs(1)); + EXPECT_EQ(result["extended"], "0.123000"); +} + +TEST_F(PropertyInterpreterTest, GivenPropertyWithoutType_WhenCallingPropertyInterpreter_ThenReturnsRawValue) +{ + PropertyInterpreter property_interpreter(GetInterpreterPrototypes()); + property_interpreter.Init( + {{"prop0", R"({"value": {"lowerLimit": 0, "upperLimit": 1} })"}}); + auto result = property_interpreter.GenerateParameters(); + + ASSERT_THAT(result, testing::SizeIs(1)); + EXPECT_EQ(result["prop0"], R"({"value": {"lowerLimit": 0, "upperLimit": 1} })"); +} + +TEST_F(PropertyInterpreterTest, GivenPropertyWithoutValue_WhenCallingPropertyInterpreter_ThenReturnsRawValue) +{ + PropertyInterpreter property_interpreter(GetInterpreterPrototypes()); + property_interpreter.Init( + {{"prop0", R"({"type": "UniformDistribution"})"}}); + auto result = property_interpreter.GenerateParameters(); + + ASSERT_THAT(result, testing::SizeIs(1)); + EXPECT_EQ(result["prop0"], R"({"type": "UniformDistribution"})"); +} + +TEST_F(PropertyInterpreterTest, GivenInvalidJson_WhenCallingPropertyInterpreter_ThenReturnsRawValue) +{ + PropertyInterpreter property_interpreter(GetInterpreterPrototypes()); + property_interpreter.Init( + {{"prop0", "invalid_json"}}); + auto result = property_interpreter.GenerateParameters(); + + ASSERT_THAT(result, testing::SizeIs(1)); + EXPECT_EQ(result["prop0"], "invalid_json"); +} + +TEST_F(PropertyInterpreterTest, GivenUnsupportedType_WhenCallingPropertyInterpreter_ThenReturnsRawValue) +{ + PropertyInterpreter property_interpreter(GetInterpreterPrototypes()); + property_interpreter.Init( + {{"prop0", R"({"type": "UnsupportedType", "value": {"lowerLimit": 0, "upperLimit": 1} })"}}); + auto result = property_interpreter.GenerateParameters(); + + ASSERT_THAT(result, testing::SizeIs(1)); + EXPECT_EQ(result["prop0"], R"({"type": "UnsupportedType", "value": {"lowerLimit": 0, "upperLimit": 1} })"); +} + +TEST_F(PropertyInterpreterTest, GivenEmptyProperties_WhenCallingPropertyInterpreter_ThenReturnsEmptyResult) +{ + std::map<std::string, std::string> properties_under_test{}; + + PropertyInterpreter property_interpreter(GetInterpreterPrototypes()); + property_interpreter.Init(properties_under_test); + auto result = property_interpreter.GenerateParameters(); + + ASSERT_THAT(result, testing::IsEmpty()); +} + +TEST_F(PropertyInterpreterTest, GivenKeyReplacements_WhenCallingPropertyInterpreter_ThenReturnsReplacedKeys) +{ + PropertyInterpreter property_interpreter(GetInterpreterPrototypes()); + property_interpreter.Init( + {{"prop0", R"({"type": "UniformDistribution", "value": {"lowerLimit": 0, "upperLimit": 1} })"}}); + + std::map<std::string, std::string> key_replacements{{"prop0", "replaced_key"}}; + auto result = property_interpreter.GenerateParameters(key_replacements); + + ASSERT_THAT(result, testing::SizeIs(1)); + EXPECT_EQ(result["replaced_key"], "0.123000"); +} + +TEST_F(PropertyInterpreterTest, GivenUninitializedInterpreter_WhenCallingGenerate_ThenDoesNothing) +{ + PropertyInterpreter property_interpreter(GetInterpreterPrototypes()); + auto result = property_interpreter.GenerateParameters(); + ASSERT_THAT(result, testing::IsEmpty()); +} + +TEST_F(PropertyInterpreterTest, GivenPropertyInterpreter_WhenCopied_ThenHoldsState) +{ + auto generateInterpreter = [](auto prototypes) -> PropertyInterpreter + { + PropertyInterpreter interpreter(prototypes); + interpreter.Init({{"prop0", "RAW_PROPERTY"}}); + return interpreter; + }; + + auto property_interpreter = generateInterpreter(GetInterpreterPrototypes()); + auto result = property_interpreter.GenerateParameters(); + + ASSERT_THAT(result, testing::SizeIs(1)); + EXPECT_EQ(result["prop0"], "RAW_PROPERTY"); +} \ No newline at end of file -- GitLab From 90239b53c938529bcf4968448f4a0bfc2b80cfdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Paris?= <rene.paris@in-tech.com> Date: Thu, 1 May 2025 08:16:11 +0200 Subject: [PATCH 4/6] doc: add doc for Json properties --- README.md | 2 +- .../exponential_distribution.json | 0 .../gamma_distribution.json | 0 .../lognormal_distribution.json | 0 .../normal_distrubution.json | 0 doc/json_properties/readme.md | 40 +++++++++++++++++++ .../uniform_distribution.json | 0 doc/json_schemas/readme.md | 21 ---------- engine/.gitignore | 1 + 9 files changed, 42 insertions(+), 22 deletions(-) rename doc/{json_schemas => json_properties}/exponential_distribution.json (100%) rename doc/{json_schemas => json_properties}/gamma_distribution.json (100%) rename doc/{json_schemas => json_properties}/lognormal_distribution.json (100%) rename doc/{json_schemas => json_properties}/normal_distrubution.json (100%) create mode 100644 doc/json_properties/readme.md rename doc/{json_schemas => json_properties}/uniform_distribution.json (100%) delete mode 100644 doc/json_schemas/readme.md diff --git a/README.md b/README.md index 8bc1f748..79ebdf6c 100644 --- a/README.md +++ b/README.md @@ -260,7 +260,7 @@ To facilitate this, the OpenScenarioEngine uses JSON to parse string-type proper This approach enables the definition of complex properties, such as distributions, which can be used within actions. For example, a velocity distribution might be defined and used in a `TrafficAreaAction`. -For more information on how to define these properties, refer to the provided schemas in [engine/src/Utils/Property/schemas](engine/src/Utils/Property/schemas). +For more information on how to define these properties, refer to the documentation an provided schemas in [doc/json_properties](./doc/json_properties). # Dependencies diff --git a/doc/json_schemas/exponential_distribution.json b/doc/json_properties/exponential_distribution.json similarity index 100% rename from doc/json_schemas/exponential_distribution.json rename to doc/json_properties/exponential_distribution.json diff --git a/doc/json_schemas/gamma_distribution.json b/doc/json_properties/gamma_distribution.json similarity index 100% rename from doc/json_schemas/gamma_distribution.json rename to doc/json_properties/gamma_distribution.json diff --git a/doc/json_schemas/lognormal_distribution.json b/doc/json_properties/lognormal_distribution.json similarity index 100% rename from doc/json_schemas/lognormal_distribution.json rename to doc/json_properties/lognormal_distribution.json diff --git a/doc/json_schemas/normal_distrubution.json b/doc/json_properties/normal_distrubution.json similarity index 100% rename from doc/json_schemas/normal_distrubution.json rename to doc/json_properties/normal_distrubution.json diff --git a/doc/json_properties/readme.md b/doc/json_properties/readme.md new file mode 100644 index 00000000..6e9be80b --- /dev/null +++ b/doc/json_properties/readme.md @@ -0,0 +1,40 @@ +# JSON Property Strings + +The OpenSCENARIO standard does not impose strict requirements on the format of custom properties, allowing for flexibility in their definition. +To facilitate this, the OpenScenarioEngine uses JSON to parse string-type properties into an internal object format. +This approach enables the definition of complex properties, such as distributions, which can be used within actions. +For example, a velocity distribution might be defined and used in a `TrafficAreaAction`. + +This folder contains JSON schemas for property strings used by the **OpenScenarioEngine**. +While these schemas are not enforced, they serve as a reference for how to properly define properties, such as distributions, in a way that they are correctly parsed. + +## General Usage + +```xml + <Property name="SomeProp" value="{ 'type': 'InterpretableType', 'value': 'InterpretableString'" /> +``` + +## Available Distribution Types + +- [ExponentialDistribution](./exponential_distribution.json) +- [ǸormalDistribution](./normal_distribution.json) +- [GammaDistribution](./gamma_distribution.json) +- [LogNormalDistribution](./lognormal_distribution.json) +- [UniformDistribution](./uniform_distribution.json) + +**Example** + +```xml + <Property + name="velocity" + value="{ + 'type': 'NormalDistribution', + 'value': + { + 'lowerLimit': 19.265, + 'upperLimit': 43.685, + 'expectedValue': 31.475, + 'variance': 37.271025 + } + }" /> +``` diff --git a/doc/json_schemas/uniform_distribution.json b/doc/json_properties/uniform_distribution.json similarity index 100% rename from doc/json_schemas/uniform_distribution.json rename to doc/json_properties/uniform_distribution.json diff --git a/doc/json_schemas/readme.md b/doc/json_schemas/readme.md deleted file mode 100644 index 9e2391a1..00000000 --- a/doc/json_schemas/readme.md +++ /dev/null @@ -1,21 +0,0 @@ -# JSON Schemas for Property Strings - -This folder contains JSON schemas for property strings used by the **OpenScenarioEngine**. -While these schemas are not enforced, they serve as a reference for how to properly define properties, such as distributions, in a way that they are correctly parsed. - -## Usage Example - -Here is an example of how to use these schemas within an OpenSCENARIO description: - -```xml -<Property key="MyCustomProperty" value="{'type': 'NormalDistribution', 'min': 0.0, 'max': 10.0, 'mean': 7.5, 'stddev': 4.0 }"> -``` - -In this example, a property with the key `MyCustomProperty` is defined using a JSON string that specifies a `NormalDistribution` with the following parameters: -- `min`: 0.0 -- `max`: 10.0 -- `mean`: 7.5 -- `stddev`: 4.0 - -Refer to the JSON schemas in this folder for more details on how to define other types of distributions and properties. -``` \ No newline at end of file diff --git a/engine/.gitignore b/engine/.gitignore index 193cf270..f3febcbe 100644 --- a/engine/.gitignore +++ b/engine/.gitignore @@ -4,3 +4,4 @@ build bin bazel-* +external -- GitLab From 72cf9bb0c4509be71f89eb21aab95a3663b77f20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Paris?= <rene.paris@in-tech.com> Date: Fri, 2 May 2025 08:04:32 +0200 Subject: [PATCH 5/6] fix(bazel): establish compatiblity with internal CI Relates to https://gitlab.eclipse.org/eclipse/openpass/openscenario1_engine/-/issues/71 --- engine/BUILD.bazel | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/engine/BUILD.bazel b/engine/BUILD.bazel index c53cf6dd..b3f3450b 100644 --- a/engine/BUILD.bazel +++ b/engine/BUILD.bazel @@ -28,8 +28,8 @@ cc_library( ], visibility = ["//visibility:public"], deps = [ + "@//third_party/nlohmann_json", "@mantle_api", - "@nlohmann_json", "@open_scenario_parser", "@stochastics_library", "@units_nhh", @@ -182,9 +182,9 @@ cc_test( ":open_scenario_engine", ":open_scenario_engine_test_utils", ":test_utils", + "@//third_party/nlohmann_json", "@googletest//:gtest_main", "@mantle_api//:test_utils", - "@nlohmann_json", ], ) -- GitLab From 32c17e81984dd093d2c0855de2bb664442f139fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Paris?= <rene.paris@in-tech.com> Date: Thu, 15 May 2025 07:15:03 +0000 Subject: [PATCH 6/6] fix: copyrights and doxygen documentation --- engine/src/Utils/JsonParsing.h | 2 +- .../Distributions/LogNormalDistribution.h | 2 +- utils/ci/conan/recipe/nlohmann_json/all/conandata.yml | 5 +---- utils/ci/conan/recipe/nlohmann_json/all/conanfile.py | 2 +- utils/ci/conan/recipe/nlohmann_json/config.yml | 5 +---- utils/ci/scripts/20_configure.sh | 2 +- 6 files changed, 6 insertions(+), 12 deletions(-) diff --git a/engine/src/Utils/JsonParsing.h b/engine/src/Utils/JsonParsing.h index ba716982..99435d4e 100644 --- a/engine/src/Utils/JsonParsing.h +++ b/engine/src/Utils/JsonParsing.h @@ -60,4 +60,4 @@ std::optional<DeserializedType> Deserialize(std::string_view value) } // namespace json -} // namespace OpenScenarioEngine::v1_3 \ No newline at end of file +} // namespace OpenScenarioEngine::v1_3 diff --git a/engine/src/Utils/PropertyInterpretation/Distributions/LogNormalDistribution.h b/engine/src/Utils/PropertyInterpretation/Distributions/LogNormalDistribution.h index 1aca4fc4..ba2397ae 100644 --- a/engine/src/Utils/PropertyInterpretation/Distributions/LogNormalDistribution.h +++ b/engine/src/Utils/PropertyInterpretation/Distributions/LogNormalDistribution.h @@ -15,7 +15,7 @@ namespace OpenScenarioEngine::v1_3::PropertyInterpretation { -/// Normal distribution +/// LogNormalDistribution struct LogNormalDistribution { double lowerLimit; ///!< Lower limit diff --git a/utils/ci/conan/recipe/nlohmann_json/all/conandata.yml b/utils/ci/conan/recipe/nlohmann_json/all/conandata.yml index 3a9de20a..346f60a2 100644 --- a/utils/ci/conan/recipe/nlohmann_json/all/conandata.yml +++ b/utils/ci/conan/recipe/nlohmann_json/all/conandata.yml @@ -1,5 +1,5 @@ ################################################################################ -# Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +# Copyright (c) 2024-2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) # # This program and the accompanying materials are made available under the # terms of the Eclipse Public License 2.0 which is available at @@ -16,6 +16,3 @@ sources: "v3.9.1": url: https://github.com/nlohmann/json/archive/refs/tags/v3.9.1.tar.gz sha256: "4cf0df69731494668bdd6460ed8cb269b68de9c19ad8c27abc24cd72605b2d5b" - "v3.11.3": - url: https://github.com/nlohmann/json/archive/refs/tags/v3.11.3.tar.gz - sha256: "0d8ef5af7f9794e3263480193c491549b2ba6cc74bb018906202ada498a79406" diff --git a/utils/ci/conan/recipe/nlohmann_json/all/conanfile.py b/utils/ci/conan/recipe/nlohmann_json/all/conanfile.py index 2a50d134..b3da81e2 100644 --- a/utils/ci/conan/recipe/nlohmann_json/all/conanfile.py +++ b/utils/ci/conan/recipe/nlohmann_json/all/conanfile.py @@ -1,5 +1,5 @@ ################################################################################ -# Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +# Copyright (c) 2024-2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) # # This program and the accompanying materials are made available under the # terms of the Eclipse Public License 2.0 which is available at diff --git a/utils/ci/conan/recipe/nlohmann_json/config.yml b/utils/ci/conan/recipe/nlohmann_json/config.yml index c2ea0f20..84d55e73 100644 --- a/utils/ci/conan/recipe/nlohmann_json/config.yml +++ b/utils/ci/conan/recipe/nlohmann_json/config.yml @@ -1,5 +1,5 @@ ################################################################################ -# Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +# Copyright (c) 2024-2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) # # This program and the accompanying materials are made available under the # terms of the Eclipse Public License 2.0 which is available at @@ -15,6 +15,3 @@ versions: "v3.9.1": folder: "all" - - "v3.11.3": - folder: "all" diff --git a/utils/ci/scripts/20_configure.sh b/utils/ci/scripts/20_configure.sh index 8dc0b896..673e1384 100755 --- a/utils/ci/scripts/20_configure.sh +++ b/utils/ci/scripts/20_configure.sh @@ -1,7 +1,7 @@ #!/bin/bash ################################################################################ -# Copyright (c) 2023-2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +# Copyright (c) 2023-2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) # # This program and the accompanying materials are made available under the # terms of the Eclipse Public License 2.0 which is available at -- GitLab