Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
5a492fd
Add ability to check if a type accepts an whole number index
jhp-lanl Sep 12, 2025
02a7396
Add safeSet() and safeGet() helpers
jhp-lanl Sep 12, 2025
8895d80
Add safeGet and safeSet that don't take numerical indices
jhp-lanl Sep 12, 2025
58a920b
Switch to safeGet and safeSet for indexable types
jhp-lanl Sep 12, 2025
fce8f7c
Whoops... forgot comment
jhp-lanl Sep 12, 2025
4959500
Clang format
jhp-lanl Sep 12, 2025
c2b1dbb
Update changelog
jhp-lanl Sep 12, 2025
589e1dc
Update doc
jhp-lanl Sep 12, 2025
fd74c25
Make functions PORTABLE and add required get/set
jhp-lanl Sep 12, 2025
a3d71fc
Make code more DRY and rename things a bit
jhp-lanl Sep 13, 2025
856531f
Add int index check
jhp-lanl Sep 13, 2025
220df73
Rename safeGet/Set to SafeGet/Set and remove direct indexing or regular
jhp-lanl Sep 13, 2025
271e52e
Make indexer const correct
jhp-lanl Sep 13, 2025
63d5a39
Rename safeGet/Set
jhp-lanl Sep 13, 2025
8a893a7
Clang format
jhp-lanl Sep 13, 2025
d6a9f68
Switch to template-based decision to use integer index or not
jhp-lanl Sep 13, 2025
cf8bf93
Whoops... forgot to return
jhp-lanl Sep 13, 2025
2d4153a
Clang format
jhp-lanl Sep 13, 2025
0a4baa1
Add docs for SafeMustGet() and SafeMustSet()
jhp-lanl Sep 13, 2025
4c89631
Get rid of Get and have wrappers use GetSet. Also update comments and…
jhp-lanl Sep 17, 2025
9d52960
Switch Get for Safe versions and expand tests
jhp-lanl Sep 17, 2025
69b56ab
Merge branch 'main' of github.com:lanl/singularity-eos into jhp/spine…
jhp-lanl Sep 17, 2025
0feb8e4
Remove last Get in favor of SafeSet
jhp-lanl Sep 17, 2025
c245728
Remove a few more instances of Get in favor of the Safe variety for I…
jhp-lanl Sep 17, 2025
734d55b
Clang format
jhp-lanl Sep 17, 2025
efd9a9f
Whoops... void was a bad choice for a type index
jhp-lanl Sep 17, 2025
d8dbccc
Let's try an unreachable return to make decltype(auto) happy
jhp-lanl Sep 17, 2025
bfacaac
Add dependent_false_v for if constexpr static asserts
jhp-lanl Sep 17, 2025
36d4286
Move throw into SafeGet/SafeSet wrappers and provide helpful compile …
jhp-lanl Sep 17, 2025
c783623
Can't test for a runtime throw when compile-time error will be used
jhp-lanl Sep 17, 2025
232ecca
Small doc tweaks
jhp-lanl Sep 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

### Fixed (Repair bugs, etc)
- [[PR561]](https://github.com/lanl/singularity-eos/pull/561) Fix logic for kokkos-kernels in spackage so that it is only required for closure models on GPU
- [[PR564]](https://github.com/lanl/singularity-eos/pull/564) Fix logic for numerical vs type indices by adding safeGet() and safeSet() helpers

### Changed (changing behavior/API/variables/...)

Expand Down
80 changes: 74 additions & 6 deletions doc/sphinx/src/using-eos.rst
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,8 @@ of the ``[]`` operator that takes your type. For example:

class MyLambda_t {
public:
// Enable recognition that this is type-indexable
constexpr static bool is_type_indexable = true;
MyLambda_t() = default;
PORTABLE_FORCEINLINE_FUNCTION
Real &operator[](const std::size_t idx) const {
Expand All @@ -724,17 +726,83 @@ which might be used as
where ``MeanIonizationState`` is shorthand for index 2, since you
defined that overload. Note that the ``operator[]`` must be marked
``const``. To more easily enable mixing and matching integer-based
indexing with type-based indexing, the function
indexing with type-based indexing, the functions

.. code-block:: cpp

template<typename Name_t, typename Indexer_t>
template <typename T, typename Indexer_t>
PORTABLE_FORCEINLINE_FUNCTION
Real &Get(Indexer_t &&lambda, std::size_t idx = 0);
bool SafeGet(Indexer_t const &lambda, std::size_t const idx, Real &out);

will return a reference to the value at named index ``Name_t()`` if
that overload is defined in ``Indexer_t`` and otherwise return a
reference at index ``idx``.
.. code-block:: cpp

template <typename T, typename Indexer_t>
PORTABLE_FORCEINLINE_FUNCTION
bool SafeGet(Indexer_t const &lambda, Real &out);

.. code-block:: cpp

template <typename T, typename Indexer_t>
PORTABLE_FORCEINLINE_FUNCTION
Real SafeMustGet(Indexer_t const &lambda, std::size_t const idx)

.. code-block:: cpp

template <typename T, typename Indexer_t>
PORTABLE_FORCEINLINE_FUNCTION
Real SafeMustGet(Indexer_t const &lambda)

will update the value of ``out`` with the value at either the appropriate
type-based index, ``T``, or the numerical index, ``idx``, if the ``Indexer_t``
doesn't accept type-based indexing. If the ``Indexer_t`` **does** accept
type-based indexing but **doesn't** have the requested index, then the
``out`` value is not updated. The same is true for when ``Indexer_t`` is the
``nullptr``. The overload that doesn't take a numerical index will *only*
return the value at a type-based index. The ``SafeMustGet()`` version
is intended to generate errors if a value can't be retrieved. In the case of
type-based indexing, the error will be at compile time if the type isn't located
in the indexer. A runtime abort will occur if either the null pointer is passed
or if integer indexing isn't allowed for some reason.

Similarly, the functions

.. code-block:: cpp

template <typename T, typename Indexer_t>
PORTABLE_FORCEINLINE_FUNCTION
inline bool SafeSet(Indexer_t &lambda, std::size_t const idx, Real const in);

.. code-block:: cpp

template <typename T, typename Indexer_t>
PORTABLE_FORCEINLINE_FUNCTION
inline bool SafeSet(Indexer_t &lambda, Real const in)

.. code-block:: cpp

template <typename T, typename Indexer_t>
PORTABLE_FORCEINLINE_FUNCTION
inline bool SafeMustSet(Indexer_t &lambda, std::size_t const idx, Real const in);

.. code-block:: cpp

template <typename T, typename Indexer_t>
PORTABLE_FORCEINLINE_FUNCTION
inline bool SafeMustSet(Indexer_t &lambda, Real const in)

can modify the values in the ``Indexer_t`` and behave the same way. In this way,
if a type-based index isn't present in the container, then another index won't
be overwritten. Again, the ``SafeMustSet()`` version will compile-time fail
or runtime abort if the lambda value can't be modified for the same reasons as
``SafeMustGet()``.

.. note::

Previous versions defined a ``Get()`` function that was "unsafe" in the
sense that it would fall back on the numerical index even if a type-based
indexer was used. This could result in retrieving and overwriting incorrect
values in the indexer. We recommend not using this function and instead using
the "safe" versions.

As a convenience tool, the struct

Expand Down
222 changes: 220 additions & 2 deletions singularity-eos/base/indexable_types.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,210 @@
#include <utility>

#include <ports-of-call/portability.hpp>
#include <ports-of-call/portable_errors.hpp>
#include <singularity-eos/base/variadic_utils.hpp>

namespace singularity {
namespace IndexerUtils {
// Convenience function for accessing an indexer by either type or
// natural number index depending on what is available

// Identifies an indexer as a type-based indexer
template <class, class = void>
struct is_type_indexer : std::false_type {};
template <class Indexer_t>
struct is_type_indexer<Indexer_t,
std::void_t<decltype(std::decay_t<Indexer_t>::is_type_indexable)>>
: std::bool_constant<std::decay_t<Indexer_t>::is_type_indexable> {};
template <class Indexer_t>
constexpr bool is_type_indexer_v = is_type_indexer<Indexer_t>::value;

namespace impl {

// Simple way to switch between pure type indexing or also allowing intergers
enum class AllowedIndexing { Numeric, TypeOnly };

// The "safe" version of Get(). This function will ONLY return a value IF that
// type-based index is present in the Indexer OR if the Indexer doesn't support
// type-based indexing.
template <AllowedIndexing AI, typename T, typename Indexer_t>
PORTABLE_FORCEINLINE_FUNCTION bool SafeGet(Indexer_t const &lambda, std::size_t const idx,
Real &out) {
// If null then nothing happens
if (variadic_utils::is_nullptr(lambda)) {
return false;
}

// Return value if type index is available
if constexpr (variadic_utils::is_indexable_v<Indexer_t, T>) {
out = lambda[T{}];
return true;
}

// Do nothing if lambda has type indexing BUT doesn't have this type index
if constexpr (is_type_indexer_v<Indexer_t>) {
return false;
}

// Fall back to numeric indexing if allowed
if constexpr (AI == AllowedIndexing::Numeric) {
if constexpr (variadic_utils::has_int_index_v<Indexer_t>) {
out = lambda[idx];
return true;
}
}

// Something else...
return false;
}

// Same as above but causes an error condition (static or dynamic) if the value
// can't be obtained
template <AllowedIndexing AI, typename T, typename Indexer_t>
PORTABLE_FORCEINLINE_FUNCTION Real SafeMustGet(Indexer_t const &lambda,
std::size_t const idx) {
// Error on null pointer
PORTABLE_ALWAYS_REQUIRE(!variadic_utils::is_nullptr(lambda),
"Indexer can't be nullptr");

// Return type-based index. Static assert that type MUST exist in indexer
if constexpr (is_type_indexer_v<Indexer_t>) {
static_assert(variadic_utils::is_indexable_v<Indexer_t, T>);
return lambda[T{}];
}

// Fall back to numerical indexing if allowed
if constexpr (AI == AllowedIndexing::Numeric) {
if constexpr (variadic_utils::has_int_index<Indexer_t>::value) {
return lambda[idx];
}
}

// Something else...
PORTABLE_ALWAYS_THROW_OR_ABORT("Cannot obtain value from unknown indexer");
}

// Break out "Set" functionality from "Get". The original "Get()" did both, but
// the "safe" version needs to separate that functionality for setting the
// values in a lambda
template <AllowedIndexing AI, typename T, typename Indexer_t>
PORTABLE_FORCEINLINE_FUNCTION bool SafeSet(Indexer_t &lambda, std::size_t const idx,
Real const in) {
// If null then nothing happens
if (variadic_utils::is_nullptr(lambda)) {
return false;
}

// Return value if type index is available
if constexpr (variadic_utils::is_indexable_v<Indexer_t, T>) {
lambda[T{}] = in;
return true;
}

// Do nothing if lambda has type indexing BUT doesn't have this type index
if constexpr (is_type_indexer_v<Indexer_t>) {
return false;
}

// Fall back to numeric indexing if allowed
if constexpr (AI == AllowedIndexing::Numeric) {
if constexpr (variadic_utils::has_int_index_v<Indexer_t>) {
lambda[idx] = in;
return true;
}
}

// Something else...
return false;
}

// Same as above but causes an error condition (static or dynamic) if the value
// can't be obtained
template <AllowedIndexing AI, typename T, typename Indexer_t>
PORTABLE_FORCEINLINE_FUNCTION void SafeMustSet(Indexer_t &lambda, std::size_t const idx,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could the safe Must functions be unified and return a reference? It doesn't seem like there's a good reason to split them out and that would be a bit more DRY?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point. I'll do that

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay after a bit of a const correctness rabbit hole, I think I have something that works. It leverages decltype(auto) to essentially do the right thing. The value category preservation of lambda through std::forward is admittedly a bit overkill here, but maybe we someday use an on-the-fly indexer for some reason and R-value references are important. True, we could cross this bridge then, but I figured I'd be as extensible as possible now since this is behind an impl namespace anyway.

Copy link
Collaborator Author

@jhp-lanl jhp-lanl Sep 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha... well I thought I had something that worked. Let me fix the CI...

Copy link
Collaborator Author

@jhp-lanl jhp-lanl Sep 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ChatGPT:

You ran into a classic “auto/decltype(auto) + if constexpr + always-throw” corner case.

Classic...

Anyway, after some more rabbit holes with if constexpr and deduced return type, I discovered dependent_false_v<T> can be used with static_assert in if constexpr expressions to essentially give a conditional compile-time error.. So no more PORTABLE_ALWAYS_THROW_OR_ABORT in favor of a nice compile-time error.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! I think the solution you landed on is quite nice in the end.

Real const in) {
// Error on null pointer
PORTABLE_ALWAYS_REQUIRE(!variadic_utils::is_nullptr(lambda),
"Indexer can't be nullptr");

// Return type-based index. Static assert that type MUST exist in indexer
if constexpr (is_type_indexer_v<Indexer_t>) {
static_assert(variadic_utils::is_indexable_v<Indexer_t, T>);
lambda[T{}] = in;
return;
}

// Fall back to numerical indexing if allowed
if constexpr (AI == AllowedIndexing::Numeric) {
if constexpr (variadic_utils::has_int_index<Indexer_t>::value) {
lambda[idx] = in;
return;
}
}

// Something else...
PORTABLE_ALWAYS_THROW_OR_ABORT("Cannot obtain value from unknown indexer");
}

} // namespace impl

// Overload when numerical index is provided
template <typename T, typename Indexer_t>
PORTABLE_FORCEINLINE_FUNCTION bool SafeGet(Indexer_t const &lambda, std::size_t const idx,
Real &out) {
return impl::SafeGet<impl::AllowedIndexing::Numeric, T>(lambda, idx, out);
}

// Overload when numerical index isn't provided
template <typename T, typename Indexer_t>
PORTABLE_FORCEINLINE_FUNCTION bool SafeGet(Indexer_t const &lambda, Real &out) {
std::size_t idx = 0;
return impl::SafeGet<impl::AllowedIndexing::TypeOnly, T>(lambda, idx, out);
}

// Overload when numerical index is provided
template <typename T, typename Indexer_t>
PORTABLE_FORCEINLINE_FUNCTION bool SafeSet(Indexer_t &lambda, std::size_t const idx,
Real const in) {
return impl::SafeSet<impl::AllowedIndexing::Numeric, T>(lambda, idx, in);
}

// Overload when numerical index isn't provided
template <typename T, typename Indexer_t>
PORTABLE_FORCEINLINE_FUNCTION bool SafeSet(Indexer_t &lambda, Real const in) {
std::size_t idx = 0;
return impl::SafeSet<impl::AllowedIndexing::TypeOnly, T>(lambda, idx, in);
}

// Overload when numerical index is provided
template <typename T, typename Indexer_t>
PORTABLE_FORCEINLINE_FUNCTION Real SafeMustGet(Indexer_t const &lambda,
std::size_t const idx) {
return impl::SafeMustGet<impl::AllowedIndexing::Numeric, T>(lambda, idx);
}

// Overload when numerical index isn't provided
template <typename T, typename Indexer_t>
PORTABLE_FORCEINLINE_FUNCTION Real SafeMustGet(Indexer_t const &lambda) {
std::size_t idx = 0;
return impl::SafeMustGet<impl::AllowedIndexing::TypeOnly, T>(lambda, idx);
}

// Overload when numerical index is provided
template <typename T, typename Indexer_t>
PORTABLE_FORCEINLINE_FUNCTION void SafeMustSet(Indexer_t &lambda, std::size_t const idx,
Real const in) {
return impl::SafeMustSet<impl::AllowedIndexing::Numeric, T>(lambda, idx, in);
}

// Overload when numerical index isn't provided
template <typename T, typename Indexer_t>
PORTABLE_FORCEINLINE_FUNCTION void SafeMustSet(Indexer_t &lambda, Real const in) {
std::size_t idx = 0;
return impl::SafeMustSet<impl::AllowedIndexing::TypeOnly, T>(lambda, idx, in);
}

// NOTE: this Get is "unsafe" because it can allow you to overwrite a type-based
// index since it automatically falls back to numeric indexing if the type
// index isn't present.
template <typename T, typename Indexer_t>
PORTABLE_FORCEINLINE_FUNCTION auto &Get(Indexer_t &&lambda, std::size_t idx = 0) {
if constexpr (variadic_utils::is_indexable_v<Indexer_t, T>) {
Expand All @@ -40,25 +238,44 @@ PORTABLE_FORCEINLINE_FUNCTION auto &Get(Indexer_t &&lambda, std::size_t idx = 0)
template <typename Data_t, typename... Ts>
class VariadicIndexerBase {
public:
// Any class that wants to be recognized as indexable (so that we don't
// accidentally fall back to integer indexing when we don't want to) needs to
// include this.
constexpr static bool is_type_indexable = true;

// JHP: another option for the `is_type_indexable` flag is to take the ADL
// route. Essentially this would involve defining a friend function that
// could be defined in an appropriate namesapce so that theoretically a host
// code could use a TPL container with type-based indexing and allow that
// container to be flagged in our code as acceptable. This seems like a bit
// of a heavy hammer for what we need here though. We can easily change this
// if a TPL provides a type that is being used for this purpose.

VariadicIndexerBase() = default;

PORTABLE_FORCEINLINE_FUNCTION
VariadicIndexerBase(const Data_t &data) : data_(data) {}

template <typename T,
typename = std::enable_if_t<variadic_utils::contains<T, Ts...>::value>>
PORTABLE_FORCEINLINE_FUNCTION Real &operator[](const T &t) {
constexpr std::size_t idx = variadic_utils::GetIndexInTL<T, Ts...>();
return data_[idx];
}

PORTABLE_FORCEINLINE_FUNCTION
Real &operator[](const std::size_t idx) { return data_[idx]; }

template <typename T,
typename = std::enable_if_t<variadic_utils::contains<T, Ts...>::value>>
PORTABLE_FORCEINLINE_FUNCTION const Real &operator[](const T &t) const {
constexpr std::size_t idx = variadic_utils::GetIndexInTL<T, Ts...>();
return data_[idx];
}

PORTABLE_FORCEINLINE_FUNCTION
const Real &operator[](const std::size_t idx) const { return data_[idx]; }

static inline constexpr std::size_t size() { return sizeof...(Ts); }

private:
Expand All @@ -70,6 +287,7 @@ using VariadicIndexer = VariadicIndexerBase<std::array<Real, sizeof...(Ts)>, Ts.
// uses a Real*
template <typename... Ts>
using VariadicPointerIndexer = VariadicIndexerBase<Real *, Ts...>;

} // namespace IndexerUtils

namespace IndexableTypes {
Expand Down
9 changes: 9 additions & 0 deletions singularity-eos/base/variadic_utils.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,15 @@ struct is_indexable<T, Index,
template <typename T, typename Index>
constexpr bool is_indexable_v = is_indexable<T, Index>::value;

// Check if a type can accept an int index
template <class T, class = void>
struct has_int_index : std::false_type {};
template <class T>
struct has_int_index<T, std::void_t<decltype(std::declval<T>()[std::declval<int>()])>>
: std::true_type {};
template <typename T>
constexpr bool has_int_index_v = has_int_index<T>::value;

// this flattens a typelist of typelists to a single typelist

// first parameter - accumulator
Expand Down
4 changes: 2 additions & 2 deletions singularity-eos/eos/eos_electrons.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,8 @@ class IdealElectrons : public EosBase<IdealElectrons> {
private:
template <typename Indexer_t = Real *>
PORTABLE_INLINE_FUNCTION Real _Cv(Indexer_t &&lambda) const {
const Real Z =
IndexerUtils::Get<IndexableTypes::MeanIonizationState>(lambda, Lambda::Zi);
Real Z = IndexerUtils::SafeMustGet<IndexableTypes::MeanIonizationState>(lambda,
Lambda::Zi);
return _Cvbase * std::max(Z, static_cast<Real>(0.0));
}

Expand Down
Loading