feat(eos): work translating runtime eos code

most (though not all) of the runtime eos code is now present in the new version. What remains is the templated try_evaluate method along with both the templated and non tempalted try_partial_derivative methods.
This commit is contained in:
2026-09-15 15:45:17 -04:00
parent d1f59d6d70
commit 7b4f30d945
8 changed files with 506 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
#pragma once
#include <string_view>
#include <type_traits>
#include "serif/dimensions/quantities.hpp"
#include "serif/dimensions/runtime/runtime.hpp"
namespace serif::dimensions::runtime {
template <typename Quantity>
concept RuntimeIdentifiedThermodynamicQuantity =
ThermodynamicQuantityType<Quantity> &&
requires {{Quantity::identifier} -> std::convertible_to<std::string_view>; } &&
(std::string_view{Quantity::identifier}.size() > 0);
template <RuntimeIdentifiedThermodynamicQuantity Quantity>
inline constexpr ThermodynamicQuantityID thermodynamicQuantityID{std::string_view{Quantity::identifier}};
}

View File

@@ -0,0 +1,31 @@
#pragma once
#include <string_view>
#include <span>
#include <cstdint>
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<const ThermodynamicQuantityID> inputQuantities;
std::uint64_t partialDerivativeMask;
[[nodiscard]] constexpr bool hasPartialDerivative(const std::size_t inputIndex) const noexcept;
};
}

View File

@@ -0,0 +1,18 @@
#pragma once
#include "serif/eos/models/concepts.hpp"
#include "serif/eos/runtime/details.hpp"
namespace serif::eos::runtime {
template <typename Candidate>
concept RuntimeEOSModel = eos::models::EOSModel<Candidate> &&
RuntimeCatalogIsSupported<typename std::remove_cvref_t<Candidate>::Relations>::value;
template <RuntimeEOSModel EOSCandidate>
using RuntimeEOSAdapter = RuntimeCatalogDispatch<EOSCandidate, typename EOSCandidate::Relations>;
template <RuntimeEOSModel EOSCandidate>
[[nodiscard]] constexpr std::span<const dimensions::runtime::RuntimeRelationDescriptor> runtimeRelationDescriptors() noexcept {
return RuntimeCatalogStorage<EOSCandidate, typename EOSCandidate::Relations>::descriptors;
}
}

View File

@@ -0,0 +1,275 @@
#pragma once
#include <array>
#include <type_traits>
#include <tuple>
#include <expected>
#include "serif/dimensions/runtime/concepts.hpp"
#include "serif/eos/exceptions.hpp"
#include "serif/eos/relations/catalog.hpp"
namespace serif::eos::runtime {
template <typename RelationT>
struct HasRuntimeQuantityIdentifiers : std::false_type {};
template <typename OutputT, typename... InputTs>
struct HasRuntimeQuantityIdentifiers<relations::Relation<OutputT, InputTs...>>
: std::bool_constant<
dimensions::runtime::RuntimeIdentifiedThermodynamicQuantity<OutputT> &&
(dimensions::runtime::RuntimeIdentifiedThermodynamicQuantity<InputTs> && ...)> {};
template <typename RelationT>
struct RuntimeRelationQuantities;
template <typename OutputT, typename... InputTs>
struct RuntimeRelationQuantities<relations::Relation<OutputT, InputTs...>> {
using Type = std::tuple<OutputT, InputTs...>;
};
template <typename... RelationTs>
using RuntimeCatalogQuantityTuple = decltype(std::tuple_cat(std::declval<typename RuntimeRelationQuantities<RelationTs>::Type>()...));
template <typename FirstQuantityT, typename SecondQuantityT>
[[nodiscard]] consteval bool runtimeQuantityIdentifiersAreCompatible() {
if constexpr (std::same_as<FirstQuantityT, SecondQuantityT>) {
return true;
} else {
return dimensions::runtime::thermodynamicQuantityID<FirstQuantityT> !=
dimensions::runtime::thermodynamicQuantityID<SecondQuantityT>;
}
}
template <typename QuantityTupleT, std::size_t First, std::size_t... Offsets>
[[nodiscard]] consteval bool runtimeQuantityIdentifierIsUnambiguous(std::index_sequence<Offsets...>) {
return (
runtimeQuantityIdentifiersAreCompatible<
std::tuple_element_t<First, QuantityTupleT>,
std::tuple_element_t<First + 1 + Offsets, QuantityTupleT>>() && ...
);
}
template <typename QuantityTupleT, std::size_t... Indices>
[[nodiscard]] consteval bool runtimeQuantityIdentifiersAreUnambiguous(std::index_sequence<Indices...>) {
return (
runtimeQuantityIdentifierIsUnambiguous<QuantityTupleT, Indices>(std::make_index_sequence<std::tuple_size_v<QuantityTupleT> - Indices - 1>{}) && ...
);
}
template <bool QuantitiesAreIdentified, typename... RelationTs>
struct RuntimeRelationsAreSupported : std::false_type {};
template <typename... RelationTs>
struct RuntimeRelationsAreSupported<true, RelationTs...> :
std::bool_constant<runtimeQuantityIdentifiersAreUnambiguous<RuntimeCatalogQuantityTuple<RelationTs...>>(
std::make_index_sequence<std::tuple_size_v<RuntimeCatalogQuantityTuple<RelationTs...>>>{}
)> {};
template <typename CatalogT>
struct RuntimeCatalogIsSupported : std::false_type {};
template <typename... RelationTs>
struct RuntimeCatalogIsSupported<relations::RelationCatalog<RelationTs...>> :
RuntimeRelationsAreSupported<(HasRuntimeQuantityIdentifiers<RelationTs>::value && ...), RelationTs...> {};
template <typename EOS, typename RelationT>
struct RuntimeRelationStorage;
template <typename EOS, typename OutputT, typename... InputTs>
struct RuntimeRelationStorage<EOS, relations::Relation<OutputT, InputTs...>> {
using RelationType = relations::Relation<OutputT, InputTs...>;
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<dimensions::runtime::ThermodynamicQuantityID, sizeof...(InputTs)> inputQuantityIDs {
dimensions::runtime::thermodynamicQuantityID<InputTs>...
};
template <std::size_t... Indices>
[[nodiscard]] static consteval std::uint64_t makePartialDerivativeMask(std::index_sequence<Indices...>) {
using InputTuple = std::tuple<InputTs...>;
/* 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 <int, float, int, int> 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<EOS, RelationType, std::tuple_element_t<Indices, InputTuple>> ? (std::uint64_t{1} << Indices) : std::uint64_t{0}));
}
inline static constexpr std::uint64_t partialDerivativeMask = makePartialDerivativeMask(std::index_sequence_for<InputTs...>{});
inline static constexpr dimensions::runtime::RuntimeRelationDescriptor descriptor {
.outputQuantity = dimensions::runtime::thermodynamicQuantityID<OutputT>,
.inputQuantities = std::span<const dimensions::runtime::ThermodynamicQuantityID>{inputQuantityIDs},
.partialDerivativeMask = partialDerivativeMask
};
};
template <typename EOS, typename CatalogT>
struct RuntimeCatalogStorage;
template <typename EOS, typename... RelationTs>
struct RuntimeCatalogStorage<EOS, relations::RelationCatalog<RelationTs...>> {
inline static constexpr std::array descriptors {
RuntimeRelationStorage<EOS, RelationTs>::descriptor...
};
};
[[nodiscard]] inline std::expected<double, EOSEvaluationError> runtimeEvaluationFailure(
const EOSEvaluationErrorCode code,
std::string message
) {
return std::unexpected<EOSEvaluationError>{EOSEvaluationError{code, std::move(message)}};
}
template <typename EOS, typename OutputT, typename... InputTs>
[[nodiscard]] std::expected<double, EOSEvaluationError> evaluateRuntimeRelation(
const EOS& eos,
relations::Relation<OutputT, InputTs...> /*relation*/,
const std::span<const dimensions::runtime::RuntimeQuantityValue> 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::size_t... Indices>(std::index_sequence<Indices...>) {
return eos::evaluate<OutputT>(eos, dimensions::QuantityValue<InputTs>{inputValues[Indices].value}...);
};
try {
return invoke_evaluate(std::make_index_sequence<sizeof...(InputTs)>{});
} 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<EOSEvaluationError>{e};
}
}
template <typename InputQuantityT, typename EOS, typename OutputT, typename... InputTs>
[[nodiscard]] bool tryRuntimePartialDerivative(
const EOS& eos,
relations::Relation<OutputT, InputTs...> /*relation*/,
const dimensions::runtime::ThermodynamicQuantityID withRespectTo,
const std::span<const dimensions::runtime::RuntimeQuantityValue> inputValues,
std::expected<double, EOSEvaluationError>& result
) {
if (withRespectTo != dimensions::runtime::thermodynamicQuantityID<InputQuantityT>) {
return false;
}
if constexpr (models::EOSSupportingPartialDerivative<EOS, relations::Relation<OutputT, InputTs...>, InputQuantityT>) {
const auto invoke_evaluate_partial_derivative = [&]<std::size_t... Indices>(std::index_sequence<Indices...>) {
return eos::partial_derivative<OutputT, InputQuantityT>(eos, dimensions::QuantityValue<InputTs>{inputValues[Indices].value}...).value();
};
try {
result = invoke_evaluate_partial_derivative(std::index_sequence_for<InputTs...>{});
} catch (const EOSEvaluationError& e) {
result = std::unexpected<EOSEvaluationError>{e};
}
} else {
result = runtimeEvaluationFailure(EOSEvaluationErrorCode::unsupported_derivative, "The requested EOS partial derivative is not available in the selected EOS.");
}
return true;
}
template <typename EOS, typename OutputT, typename... InputTs>
[[nodiscard]] std::expected<double, EOSEvaluationError> evaluateRuntimePartialDerivative (
const EOS& eos,
relations::Relation<OutputT, InputTs...> relation,
const dimensions::runtime::ThermodynamicQuantityID withRespectTo,
const std::span<const dimensions::runtime::RuntimeQuantityValue> inputValues
) {
std::expected<double, EOSEvaluationError> result = runtimeEvaluationFailure(
EOSEvaluationErrorCode::unsupported_derivative,
"The requested quantity is not an input to the EOS relation."
);
[[maybe_unused]] const bool matched = (tryRuntimePartialDerivative<InputTs>(eos, relation, withRespectTo, inputValues, result) || ...); // Here we fold over all input and try to find one that we can evaluate.
return result;
}
template <typename EOS, typename RelationT>
[[nodiscard]] bool runtimeRelationMatches(
const dimensions::runtime::ThermodynamicQuantityID outputQuantityID,
const std::span<const dimensions::runtime::RuntimeQuantityValue> inputValues
) {
const dimensions::runtime::RuntimeRelationDescriptor& descriptor = RuntimeRelationStorage<EOS, RelationT>::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 <typename EOS, typename CatalogT>
struct RuntimeCatalogDispatch;
template <typename EOS, typename...RelationTs>
struct RuntimeCatalogDispatch<EOS, relations::RelationCatalog<RelationTs...>> {
[[nodiscard]] static std::expected<double, EOSEvaluationError> evaluate(
const void *object,
const dimensions::runtime::ThermodynamicQuantityID& outputQuantity,
const std::span<const dimensions::runtime::RuntimeQuantityValue> inputValues
) {
const auto &eos = *static_cast<const EOS*>(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<double, EOSEvaluationError> 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<EOS, RelationTs>(outputQuantity, inputValues) ? (result = evaluateRuntimeRelation(eos, RelationTs{}, inputValues), true) : false) || ...);
return result;
}
[[nodiscard]] static std::expected<double, EOSEvaluationError> partial_derivative(
const void *object,
const dimensions::runtime::ThermodynamicQuantityID& outputQuantity,
const dimensions::runtime::ThermodynamicQuantityID& withRespectTo,
const std::span<const dimensions::runtime::RuntimeQuantityValue> inputValues
) {
const auto &eos = *static_cast<const EOS*>(object); // Same Comment as above
std::expected<double, EOSEvaluationError> result = runtimeEvaluationFailure(
EOSEvaluationErrorCode::unsupported_relation, "The requested EOS relation is not available in the current equation of state."
);
[[maybe_unused]] const bool matched = ((runtimeRelationMatches<EOS, RelationTs>(outputQuantity, inputValues) ? (result = evaluateRuntimePartialDerivative(eos, RelationTs{}, withRespectTo, inputValues), true) : false) || ...);
return result;
}
};
}

View File

@@ -0,0 +1,151 @@
#pragma once
#include <span>
#include "serif/eos/runtime/concepts.hpp"
namespace serif::eos::runtime {
class EOSView final {
public:
template <RuntimeEOSModel EOS>
explicit EOSView(EOS& eos) noexcept :
m_object(std::addressof(eos)),
m_relations(&RuntimeEOSAdapter<std::remove_cv_t<EOS>>::evaluate),
m_partialDerivative(&RuntimeEOSAdapter<std::remove_cv_t<EOS>>::partial_derivative) {}
// TODO: This function can be moved to an implantation file, it may need to be to prevent ODR violations
[[nodiscard]] std::span<const dimensions::runtime::RuntimeRelationDescriptor> 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<const dimensions::runtime::ThermodynamicQuantityID> inputQuantities
) const noexcept {
return find_relation(outputQuantity, inputQuantities) != nullptr;
}
[[nodiscard]] std::expected<dimensions::runtime::RuntimeQuantityValue, EOSEvaluationError> try_evaluate(
const dimensions::runtime::ThermodynamicQuantityID outputQuantity,
const std::span<const dimensions::runtime::RuntimeQuantityValue> inputQuantities
) {
const auto validation = validate_relation_request(outputQuantity, inputQuantities);
if (!validation.has_value()) {
return std::unexpected<EOSEvaluationError>{validation.error()};
}
auto result = m_evaluate(m_object, outputQuantity, inputQuantities);
if (!result.has_value()) {
return std::unexpected<EOSEvaluationError>{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<double, EOSEvaluationError> (*)(
const void*,
dimensions::runtime::ThermodynamicQuantityID,
std::span<const dimensions::runtime::RuntimeQuantityValue>
);
using RuntimePartialDerivativeFunction = std::expected<double, EOSEvaluationError> (*)(
const void*,
dimensions::runtime::ThermodynamicQuantityID,
dimensions::runtime::ThermodynamicQuantityID,
std::span<const dimensions::runtime::RuntimeQuantityValue>
);
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<const dimensions::runtime::ThermodynamicQuantityID> 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<const dimensions::runtime::RuntimeRelationDescriptor *, EOSEvaluationError> validate_relation_request(
const dimensions::runtime::ThermodynamicQuantityID outputQuantity,
const std::span<const dimensions::runtime::RuntimeQuantityValue> 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<const dimensions::runtime::RuntimeRelationDescriptor *>(
EOSEvaluationErrorCode::unsupported_relation,
"The requested EOS relation is not available."
);
}
if (!inputCountAvailable) {
return runtime_failure<const dimensions::runtime::RuntimeRelationDescriptor *>(
EOSEvaluationErrorCode::wrong_input_count,
"No EOS relation for given output quantity '" + std::string{outputQuantity.name()} + "' accepts the provided number of inputs."
);
}
return runtime_failure<const dimensions::runtime::RuntimeRelationDescriptor *>(
EOSEvaluationErrorCode::wrong_input_quantity,
"No EOS relation for given output quantity '" + std::string{outputQuantity.name()} + "' accepts the provided input quantities."
);
}
template <typename Value>
[[nodiscard]] static std::expected<Value, EOSEvaluationError> runtime_failure(
const EOSEvaluationErrorCode code,
const std::string message
) {
return std::unexpected<EOSEvaluationError>{
EOSEvaluationError{code, std::move(message)}
};
}
private: // Private members
const void *m_object;
std::span<const dimensions::runtime::RuntimeRelationDescriptor> m_relations;
RuntimeEvaluateFunction m_evaluate;
RuntimePartialDerivativeFunction m_partialDerivative;
};
}

View File

@@ -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;
}
}

View File

@@ -1,6 +1,7 @@
include_dir = include_directories('include') include_dir = include_directories('include')
serif_sources = files( serif_sources = files(
'lib/serif/dimensions/runtime/runtime.cpp',
'lib/serif/discretization/domain/mesh/topology.cpp', 'lib/serif/discretization/domain/mesh/topology.cpp',
'lib/serif/discretization/domain/schema/validation/results.cpp', 'lib/serif/discretization/domain/schema/validation/results.cpp',
'lib/serif/eos/models/polytropic.cpp', 'lib/serif/eos/models/polytropic.cpp',

View File