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