diff --git a/src/include/serif/dimensions/runtime/concepts.hpp b/src/include/serif/dimensions/runtime/concepts.hpp new file mode 100644 index 0000000..5b7a442 --- /dev/null +++ b/src/include/serif/dimensions/runtime/concepts.hpp @@ -0,0 +1,17 @@ +#pragma once +#include +#include + +#include "serif/dimensions/quantities.hpp" +#include "serif/dimensions/runtime/runtime.hpp" + +namespace serif::dimensions::runtime { + template + concept RuntimeIdentifiedThermodynamicQuantity = + ThermodynamicQuantityType && + requires {{Quantity::identifier} -> std::convertible_to; } && + (std::string_view{Quantity::identifier}.size() > 0); + + template + inline constexpr ThermodynamicQuantityID thermodynamicQuantityID{std::string_view{Quantity::identifier}}; +} \ No newline at end of file diff --git a/src/include/serif/dimensions/runtime/runtime.hpp b/src/include/serif/dimensions/runtime/runtime.hpp new file mode 100644 index 0000000..a70421e --- /dev/null +++ b/src/include/serif/dimensions/runtime/runtime.hpp @@ -0,0 +1,31 @@ +#pragma once +#include +#include +#include + + +namespace serif::dimensions::runtime { + class ThermodynamicQuantityID final { + public: + explicit constexpr ThermodynamicQuantityID(std::string_view name) noexcept; + + [[nodiscard]] constexpr std::string_view name() const noexcept; + [[nodiscard]] friend constexpr bool operator==(const ThermodynamicQuantityID &, const ThermodynamicQuantityID &) noexcept = default; + private: + std::string_view m_name; // We can use a string view here since the constructor is constexpr and the string view will be valid for the lifetime of the program. + }; + + struct RuntimeQuantityValue final { + ThermodynamicQuantityID id; + double value; + }; + + // TODO: This should be refactored into the eos module rather than the dimensions module + struct RuntimeRelationDescriptor final { + ThermodynamicQuantityID outputQuantity; + std::span inputQuantities; + std::uint64_t partialDerivativeMask; + + [[nodiscard]] constexpr bool hasPartialDerivative(const std::size_t inputIndex) const noexcept; + }; +} \ No newline at end of file diff --git a/src/include/serif/eos/runtime/concepts.hpp b/src/include/serif/eos/runtime/concepts.hpp new file mode 100644 index 0000000..b7e1cbe --- /dev/null +++ b/src/include/serif/eos/runtime/concepts.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "serif/eos/models/concepts.hpp" +#include "serif/eos/runtime/details.hpp" + +namespace serif::eos::runtime { + template + concept RuntimeEOSModel = eos::models::EOSModel && + RuntimeCatalogIsSupported::Relations>::value; + + template + using RuntimeEOSAdapter = RuntimeCatalogDispatch; + + template + [[nodiscard]] constexpr std::span runtimeRelationDescriptors() noexcept { + return RuntimeCatalogStorage::descriptors; + } +} \ No newline at end of file diff --git a/src/include/serif/eos/runtime/details.hpp b/src/include/serif/eos/runtime/details.hpp new file mode 100644 index 0000000..5875f0e --- /dev/null +++ b/src/include/serif/eos/runtime/details.hpp @@ -0,0 +1,275 @@ +#pragma once +#include +#include +#include +#include + +#include "serif/dimensions/runtime/concepts.hpp" +#include "serif/eos/exceptions.hpp" +#include "serif/eos/relations/catalog.hpp" + +namespace serif::eos::runtime { + template + struct HasRuntimeQuantityIdentifiers : std::false_type {}; + + template + struct HasRuntimeQuantityIdentifiers> + : std::bool_constant< + dimensions::runtime::RuntimeIdentifiedThermodynamicQuantity && + (dimensions::runtime::RuntimeIdentifiedThermodynamicQuantity && ...)> {}; + + template + struct RuntimeRelationQuantities; + + template + struct RuntimeRelationQuantities> { + using Type = std::tuple; + }; + + template + using RuntimeCatalogQuantityTuple = decltype(std::tuple_cat(std::declval::Type>()...)); + + template + [[nodiscard]] consteval bool runtimeQuantityIdentifiersAreCompatible() { + if constexpr (std::same_as) { + return true; + } else { + return dimensions::runtime::thermodynamicQuantityID != + dimensions::runtime::thermodynamicQuantityID; + } + } + + template + [[nodiscard]] consteval bool runtimeQuantityIdentifierIsUnambiguous(std::index_sequence) { + return ( + runtimeQuantityIdentifiersAreCompatible< + std::tuple_element_t, + std::tuple_element_t>() && ... + ); + } + + template + [[nodiscard]] consteval bool runtimeQuantityIdentifiersAreUnambiguous(std::index_sequence) { + return ( + runtimeQuantityIdentifierIsUnambiguous(std::make_index_sequence - Indices - 1>{}) && ... + ); + } + + template + struct RuntimeRelationsAreSupported : std::false_type {}; + + template + struct RuntimeRelationsAreSupported : + std::bool_constant>( + std::make_index_sequence>>{} + )> {}; + + template + struct RuntimeCatalogIsSupported : std::false_type {}; + + template + struct RuntimeCatalogIsSupported> : + RuntimeRelationsAreSupported<(HasRuntimeQuantityIdentifiers::value && ...), RelationTs...> {}; + + template + struct RuntimeRelationStorage; + + template + struct RuntimeRelationStorage> { + using RelationType = relations::Relation; + + static_assert(sizeof...(InputTs) <= 64, "Runtime EOS relation descriptors support at most 64 inputs. If you need more than this you will need to implement your own custom RuntimeRelationStorage specialization"); + + inline static constexpr std::array inputQuantityIDs { + dimensions::runtime::thermodynamicQuantityID... + }; + + template + [[nodiscard]] static consteval std::uint64_t makePartialDerivativeMask(std::index_sequence) { + using InputTuple = std::tuple; + + /* This is perhaps overly "clever" and self-indulgent. + * + * Instead of changing that I will provide a brief explanation of what this is doing. + * Effectively we are accumulating a bit mask. std::uint64_t on the left initializes the bit mask to all 0s + * Then we fold over all the indices of the input quantities + * + * As a reminder if you have not run into any of the other folding comments I have left scattered in the code base. C++17 introduced the folding operator, + * what we have here is what is called a right binary fold. Essentially we start with a value then repeatedly logically or it with the result of an iteration + * over the type pack Indices (which is some list of integers from 0 to sizeof...(InputTs) - 1). The | ... | is what indicates this, the first pipe shows we or + * the initial state (a 64 bit integer with all bits set to 0) with the result of the first iteration, then we or that result with the result of the second iteration, and so on until we have iterated over all indices. + * There is persistent confusion I feel with this since we do not have a loop variable as a standard for loop would; however, this is simply the syntax available + * in C++ (as of C++23). The looping is implicit in so far as the compiler implicitly generates the loop over Indices. + * + * We use std::tuple_element_t to get the type of the input quantity at the current index (recall that the fold loop goes over all indices) + * So for example if the tuple is and we are at the second iteration of the folding loop (index 1) then std::tuple_element_t will evaluate to float + * This is contrived though since we are not storing primitives in the tuple but rather types derived from the ThermodynamicQuantity concept. + * + * Note where we evaluate this type. In the template argument for EOSSupportingPartialDerivative. Basically we are asking if, for the given EOS + * and relation type, does it support a partial derivative with respect to the input quantity at the current index. This evaluates at compile + * time to either true or false. There is then a ternary operator that selects a 1 (bit shifted by the current index to fit in the right bitmask location) or a 0 (which we don't need to bitshift) + * + * What that means is when all is said and done for an input type list made of n types there is an n bit integer (padded out to 64 bits) where each bit corresponds + * to whether the EOS supports a partial derivative with respect to the input quantity at that index. The first input quantity corresponds to the least significant bit and the last input quantity corresponds to the most significant bit. + */ + return (std::uint64_t{0} | ... | (models::EOSSupportingPartialDerivative> ? (std::uint64_t{1} << Indices) : std::uint64_t{0})); + } + + inline static constexpr std::uint64_t partialDerivativeMask = makePartialDerivativeMask(std::index_sequence_for{}); + + inline static constexpr dimensions::runtime::RuntimeRelationDescriptor descriptor { + .outputQuantity = dimensions::runtime::thermodynamicQuantityID, + .inputQuantities = std::span{inputQuantityIDs}, + .partialDerivativeMask = partialDerivativeMask + }; + }; + + template + struct RuntimeCatalogStorage; + + template + struct RuntimeCatalogStorage> { + inline static constexpr std::array descriptors { + RuntimeRelationStorage::descriptor... + }; + }; + + [[nodiscard]] inline std::expected runtimeEvaluationFailure( + const EOSEvaluationErrorCode code, + std::string message + ) { + return std::unexpected{EOSEvaluationError{code, std::move(message)}}; + } + + template + [[nodiscard]] std::expected evaluateRuntimeRelation( + const EOS& eos, + relations::Relation /*relation*/, + const std::span inputValues + ) { + /* Once again we find ourself at a use of folding. I reccomend you read the comment earlier in this file regarding folding, or find the C++ docs on folding. + * + * The general premis here is that we call the evaluation function for the EOS with the given relation and input values. We fold over all indicies using the final ... operator. + * This is an implicit loop generated by the compiler over the so called "parameter pack". + */ + const auto invoke_evaluate = [&](std::index_sequence) { + return eos::evaluate(eos, dimensions::QuantityValue{inputValues[Indices].value}...); + }; + + try { + return invoke_evaluate(std::make_index_sequence{}); + } catch (const EOSEvaluationError& e) { // We may want to reconsider using a try-catch for this, but this is good enough for now. Realistically if we want to not use a try catch we will need to change the signature of the EOS evaluation function to return a std::expected instead of throwing an exception. This is a larger change that I don't want to make right now. + return std::unexpected{e}; + } + } + + template + [[nodiscard]] bool tryRuntimePartialDerivative( + const EOS& eos, + relations::Relation /*relation*/, + const dimensions::runtime::ThermodynamicQuantityID withRespectTo, + const std::span inputValues, + std::expected& result + ) { + if (withRespectTo != dimensions::runtime::thermodynamicQuantityID) { + return false; + } + + if constexpr (models::EOSSupportingPartialDerivative, InputQuantityT>) { + const auto invoke_evaluate_partial_derivative = [&](std::index_sequence) { + return eos::partial_derivative(eos, dimensions::QuantityValue{inputValues[Indices].value}...).value(); + }; + + try { + result = invoke_evaluate_partial_derivative(std::index_sequence_for{}); + } catch (const EOSEvaluationError& e) { + result = std::unexpected{e}; + } + } else { + result = runtimeEvaluationFailure(EOSEvaluationErrorCode::unsupported_derivative, "The requested EOS partial derivative is not available in the selected EOS."); + } + + return true; + } + + template + [[nodiscard]] std::expected evaluateRuntimePartialDerivative ( + const EOS& eos, + relations::Relation relation, + const dimensions::runtime::ThermodynamicQuantityID withRespectTo, + const std::span inputValues + ) { + std::expected result = runtimeEvaluationFailure( + EOSEvaluationErrorCode::unsupported_derivative, + "The requested quantity is not an input to the EOS relation." + ); + + [[maybe_unused]] const bool matched = (tryRuntimePartialDerivative(eos, relation, withRespectTo, inputValues, result) || ...); // Here we fold over all input and try to find one that we can evaluate. + return result; + } + + template + [[nodiscard]] bool runtimeRelationMatches( + const dimensions::runtime::ThermodynamicQuantityID outputQuantityID, + const std::span inputValues + ) { + const dimensions::runtime::RuntimeRelationDescriptor& descriptor = RuntimeRelationStorage::descriptor; + + if (descriptor.outputQuantity != outputQuantityID || descriptor.inputQuantities.size() != inputValues.size()) { + return false; + } + + for (std::size_t i = 0; i < inputValues.size(); ++i) { + if (descriptor.inputQuantities[i] != inputValues[i].id) { + return false; + } + } + + return true; + } + + template + struct RuntimeCatalogDispatch; + + template + struct RuntimeCatalogDispatch> { + [[nodiscard]] static std::expected evaluate( + const void *object, + const dimensions::runtime::ThermodynamicQuantityID& outputQuantity, + const std::span inputValues + ) { + const auto &eos = *static_cast(object); // Not sure if polymorphic type erasure is the right tool here. It works but we may want to revisit it if it precent compiler optimizations. This does however let us pass any eos model to the runtime view without needing to know the type at compile time (which is therefore helpful when building extension systems in other languages like python). Further, it may make sense to take the performance hit here and use something like a dynamic_cast to ensure that the object is actually of the correct type. This would be a runtime check but it would be a more robust check than just blindly casting to the expected type. + + + std::expected result = runtimeEvaluationFailure( + EOSEvaluationErrorCode::unsupported_relation, "The requested EOS relation is not available in the current equation of state." + ); + + /* Fold over all relations in the catalog and check if any of them match the requested output quantity and input quantities. If the match is found then + * evaluate the relation and return the result and set matched to true, if no match is found then set matched to false. + */ + [[maybe_unused]] const bool matched = ((runtimeRelationMatches(outputQuantity, inputValues) ? (result = evaluateRuntimeRelation(eos, RelationTs{}, inputValues), true) : false) || ...); + + return result; + } + + [[nodiscard]] static std::expected partial_derivative( + const void *object, + const dimensions::runtime::ThermodynamicQuantityID& outputQuantity, + const dimensions::runtime::ThermodynamicQuantityID& withRespectTo, + const std::span inputValues + ) { + const auto &eos = *static_cast(object); // Same Comment as above + + std::expected result = runtimeEvaluationFailure( + EOSEvaluationErrorCode::unsupported_relation, "The requested EOS relation is not available in the current equation of state." + ); + + [[maybe_unused]] const bool matched = ((runtimeRelationMatches(outputQuantity, inputValues) ? (result = evaluateRuntimePartialDerivative(eos, RelationTs{}, withRespectTo, inputValues), true) : false) || ...); + return result; + } + }; + + + +} \ No newline at end of file diff --git a/src/include/serif/eos/runtime/views.hpp b/src/include/serif/eos/runtime/views.hpp new file mode 100644 index 0000000..ff88dbd --- /dev/null +++ b/src/include/serif/eos/runtime/views.hpp @@ -0,0 +1,151 @@ +#pragma once + +#include + +#include "serif/eos/runtime/concepts.hpp" + +namespace serif::eos::runtime { + class EOSView final { + public: + template + explicit EOSView(EOS& eos) noexcept : + m_object(std::addressof(eos)), + m_relations(&RuntimeEOSAdapter>::evaluate), + m_partialDerivative(&RuntimeEOSAdapter>::partial_derivative) {} + + // TODO: This function can be moved to an implantation file, it may need to be to prevent ODR violations + [[nodiscard]] std::span relations() const noexcept { + return m_relations; + } + + // TODO: This function can be moved to an implantation file, it may need to be to prevent ODR violations + [[nodiscard]] bool supports( + const dimensions::runtime::ThermodynamicQuantityID outputQuantity, + const std::span inputQuantities + ) const noexcept { + return find_relation(outputQuantity, inputQuantities) != nullptr; + } + + [[nodiscard]] std::expected try_evaluate( + const dimensions::runtime::ThermodynamicQuantityID outputQuantity, + const std::span inputQuantities + ) { + const auto validation = validate_relation_request(outputQuantity, inputQuantities); + + if (!validation.has_value()) { + return std::unexpected{validation.error()}; + } + + auto result = m_evaluate(m_object, outputQuantity, inputQuantities); + + if (!result.has_value()) { + return std::unexpected{result.error()}; + } + return dimensions::runtime::RuntimeQuantityValue{outputQuantity, *result}; + } + + // TODO: Still need to translate from the old code the templated version of try_evaluate along with the try_partial_derivative function. The templated version is more user friendly and should be kept, but the non-templated version is needed for the runtime view. + private: // Type aliases + using RuntimeEvaluateFunction = std::expected (*)( + const void*, + dimensions::runtime::ThermodynamicQuantityID, + std::span + ); + + using RuntimePartialDerivativeFunction = std::expected (*)( + const void*, + dimensions::runtime::ThermodynamicQuantityID, + dimensions::runtime::ThermodynamicQuantityID, + std::span + ); + private: // Private methods + // TODO: this can be moved to the implementation file, It may need to be to prevent ODR violations + [[nodiscard]] const dimensions::runtime::RuntimeRelationDescriptor *find_relation( + const dimensions::runtime::ThermodynamicQuantityID outputQuantity, + const std::span inputQuantities + ) const noexcept { + for (const auto& descriptor : m_relations) { + if (descriptor.outputQuantity != outputQuantity || descriptor.inputQuantities.size() != inputQuantities.size()) { + continue; + } + + bool matches = true; + for (std::size_t index = 0; index < inputQuantities.size(); ++index) { + if (descriptor.inputQuantities[index] != inputQuantities[index]) { + matches = false; + break; + } + } + + if (matches) { + return std::addressof(descriptor); + } + } + return nullptr; + } + + [[nodiscard]] std::expected validate_relation_request( + const dimensions::runtime::ThermodynamicQuantityID outputQuantity, + const std::span inputValues + ) const { + bool outputAvailable = false; + bool inputCountAvailable = false; + + for (const auto& descriptor : m_relations) { + if (descriptor.outputQuantity != outputQuantity || descriptor.inputQuantities.size() != inputValues.size()) { + continue; + } + + outputAvailable = true; + inputCountAvailable = true; + bool matches = true; + + for (std::size_t index = 0; index < inputValues.size(); ++index) { + if (descriptor.inputQuantities[index] != inputValues[index].id) { + matches = false; + break; + } + } + + if (matches) { + return std::addressof(descriptor); + } + } + + if (!outputAvailable) { + return runtime_failure( + EOSEvaluationErrorCode::unsupported_relation, + "The requested EOS relation is not available." + ); + } + + if (!inputCountAvailable) { + return runtime_failure( + EOSEvaluationErrorCode::wrong_input_count, + "No EOS relation for given output quantity '" + std::string{outputQuantity.name()} + "' accepts the provided number of inputs." + ); + } + + return runtime_failure( + EOSEvaluationErrorCode::wrong_input_quantity, + "No EOS relation for given output quantity '" + std::string{outputQuantity.name()} + "' accepts the provided input quantities." + ); + } + + template + [[nodiscard]] static std::expected runtime_failure( + const EOSEvaluationErrorCode code, + const std::string message + ) { + return std::unexpected{ + EOSEvaluationError{code, std::move(message)} + }; + } + + private: // Private members + const void *m_object; + std::span m_relations; + RuntimeEvaluateFunction m_evaluate; + RuntimePartialDerivativeFunction m_partialDerivative; + }; +} \ No newline at end of file diff --git a/src/lib/serif/dimensions/runtime/runtime.cpp b/src/lib/serif/dimensions/runtime/runtime.cpp new file mode 100644 index 0000000..4c977ef --- /dev/null +++ b/src/lib/serif/dimensions/runtime/runtime.cpp @@ -0,0 +1,13 @@ +#include "serif/dimensions/runtime/runtime.hpp" + +namespace serif::dimensions::runtime { + constexpr ThermodynamicQuantityID::ThermodynamicQuantityID(const std::string_view name) noexcept : m_name(name) {} + + [[nodiscard]] constexpr std::string_view ThermodynamicQuantityID::name() const noexcept { + return m_name; + } + + [[nodiscard]] constexpr bool RuntimeRelationDescriptor::hasPartialDerivative(const std::size_t inputIndex) const noexcept { + return inputIndex < inputQuantities.size() && (partialDerivativeMask & (std::uint64_t{1} << inputIndex)) != 0; + } +} diff --git a/src/meson.build b/src/meson.build index 9d766a3..d08189a 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,6 +1,7 @@ include_dir = include_directories('include') serif_sources = files( + 'lib/serif/dimensions/runtime/runtime.cpp', 'lib/serif/discretization/domain/mesh/topology.cpp', 'lib/serif/discretization/domain/schema/validation/results.cpp', 'lib/serif/eos/models/polytropic.cpp', diff --git a/tests/sandbox/dimensions_sandbox.cpp b/tests/sandbox/dimensions_sandbox.cpp new file mode 100644 index 0000000..e69de29