From ab0342824a5de027e32347756596cc79ec2a3335 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] feat(PropertyInterpreter): Generic structure for custom property interpretation --- .../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 | 59 +++++++ 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 | 53 ++++++ engine/tests/Utils/DistributionTest.cpp | 160 ++++++++++++++++++ engine/tests/Utils/JsonParserTest.cpp | 106 ------------ engine/tests/Utils/JsonParsingTest.cpp | 80 +++++++++ .../tests/Utils/PropertyInterpreterTest.cpp | 142 ++++++++++++++++ 39 files changed, 1288 insertions(+), 535 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/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 db616255..03502ba3 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -219,10 +219,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 20fd7859..4712dfd1 100644 --- a/engine/cmake/generated_files.cmake +++ b/engine/cmake/generated_files.cmake @@ -239,6 +239,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 ) @@ -606,5 +609,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..6e4c0ec0 --- /dev/null +++ b/engine/src/Utils/JsonParsing.cpp @@ -0,0 +1,59 @@ +/******************************************************************************* + * 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; + } + + for (const std::string& search : {""", "'"}) + { + 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 \ No newline at end of file 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..19105902 --- /dev/null +++ b/engine/src/Utils/PropertyInterpreter.h @@ -0,0 +1,53 @@ +/******************************************************************************** + * 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 "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); + + /// 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 \ No newline at end of file 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..c284ee42 --- /dev/null +++ b/engine/tests/Utils/PropertyInterpreterTest.cpp @@ -0,0 +1,142 @@ +/******************************************************************************** + * 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"); +} \ No newline at end of file -- GitLab