Skip to content

Adding LinSpace, LogSpace, and Arange prototypes #18200

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: master
Choose a base branch
from

Conversation

edfink234
Copy link

@edfink234 edfink234 commented Mar 30, 2025

This Pull request:

Changes or fixes:

Adds linSpace, logSpace, and arange functions to ROOT::VecOps namespace

Checklist:

  • tested changes locally
  • updated the docs (if necessary)

Fixes #17855

Copy link
Member

@dpiparo dpiparo left a comment

Choose a reason for hiding this comment

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

Thanks for the good work you did to put together this PR. I proposed some changes which can be applied without much effort, or so I hope, and that will make the new functions (templates) even more useful.

@dpiparo dpiparo changed the title Adding linSpace, logSpace, and arange prototypes Adding LinSpace, LogSpace, and Arange prototypes Mar 31, 2025
@dpiparo dpiparo self-assigned this Mar 31, 2025
@dpiparo
Copy link
Member

dpiparo commented Mar 31, 2025

Ah, and also, the equality of double precision floating point is not working too well because of the very meaning of equality of floating point ;-)

Copy link

github-actions bot commented Mar 31, 2025

Test Results

0 tests   0 ✅  0s ⏱️
0 suites  0 💤
0 files    0 ❌

Results for commit b5b5620.

♻️ This comment has been updated with latest results.

@edfink234
Copy link
Author

edfink234 commented Apr 1, 2025

Hi @dpiparo

Thank you for the quick response and feedback!

For this next commit, I’ve updated the functions as follows:

  • LinSpace, LogSpace, and Arange are now templated to deduce a common arithmetic type that defaults to double when necessary
  • I’ve adjusted parameter naming to match VecOps conventions by using a lowercase 'n' with a default power-of-two value (128).
  • The Doxygen documentation has been expanded to fully detail the template parameters, function behavior, usage examples, and to clarify key implementation details such as the check against LLONG_MAX.
  • I’ve updated the tests to use approximate (tolerance-based) comparisons for floating-point values with a CheckNear function

@dpiparo
Copy link
Member

dpiparo commented Apr 1, 2025

Dear @edfink234 thanks. I am reviewing the code. One thing which we will need is to provide a clean commit, properly formatted with all the changes - it is fine to have some "history" when crafting PRs but we try to keep the history of the repo as clean as possible.

Copy link
Member

@dpiparo dpiparo left a comment

Choose a reason for hiding this comment

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

Good job. Once the changes are addressed and the tests pass, we are ready to merge.

@edfink234
Copy link
Author

Thanks for the quick response @dpiparo! I will get to these hopefully today (Tuesday PST) if not at some point this week. It's almost 2 am where I am atm 😅...

@edfink234
Copy link
Author

@dpiparo I made another commit to address the new comments and modified the tests a bit to hopefully have them pass.

@dpiparo
Copy link
Member

dpiparo commented Apr 2, 2025

Thanks @edfink234 . Let's see how this goes.

Copy link
Member

@vepadulano vepadulano left a comment

Choose a reason for hiding this comment

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

This is a quite useful contribution, thanks a lot! I have left some comments for discussion

Comment on lines 3286 to 3287
template <typename T1 = double, typename T2 = double, typename T3 = double, typename Common_t = std::conditional_t<std::is_floating_point_v<std::common_type_t<T1, T2, T3>>, std::common_type_t<T1, T2, T3>, double>>
inline RVec<Common_t> Linspace(T1 start, T2 end, unsigned long long n = 128)
Copy link
Member

Choose a reason for hiding this comment

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

The template parameter T3 is not used anywhere in the template, so I guess it can be removed. Also, from what I understand the return type will always be an RVec of floating point type. I personally do not see the reason to not always just have RVec<double>.

Copy link
Author

Choose a reason for hiding this comment

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

I think the benefit could be if the user wants float or some other floating point type, they have some options on how they call these functions (Linspace, Logspace, Arange) to yield the desired floating point type. For example (after the changes from the most recent commit to this PR), to illustrate some of this flexibility below:

root [1] Linspace(1, 10, 10)
(ROOT::VecOps::RVec<double>) { 1.0000000, 2.0000000, 3.0000000, 4.0000000, 5.0000000, 6.0000000, 7.0000000, 8.0000000, 9.0000000, 10.000000 }
root [2] Linspace(1.0f, 10.0f, 10)
(ROOT::VecOps::RVec<float>) { 1.00000f, 2.00000f, 3.00000f, 4.00000f, 5.00000f, 6.00000f, 7.00000f, 8.00000f, 9.00000f, 10.0000f }
root [3] Linspace<float, float>(1, 10, 10)
(ROOT::VecOps::RVec<float>) { 1.00000f, 2.00000f, 3.00000f, 4.00000f, 5.00000f, 6.00000f, 7.00000f, 8.00000f, 9.00000f, 10.0000f }
root [4] Linspace<long double, long double>(1, 10, 10)
(ROOT::VecOps::RVec<long double>) { 1.0000000L, 2.0000000L, 3.0000000L, 4.0000000L, 5.0000000L, 6.0000000L, 7.0000000L, 8.0000000L, 9.0000000L, 10.000000L }
root [5] Logspace(1.0f, 5.0f, 5, 10.0f)
(ROOT::VecOps::RVec<float>) { 10.0000f, 100.000f, 1000.00f, 10000.0f, 100000.f }
root [6] Logspace<float, float, float>(1, 5, 20)
(ROOT::VecOps::RVec<float>) { 10.0000f, 16.2378f, 26.3665f, 42.8133f, 69.5193f, 112.884f, 183.298f, 297.635f, 483.293f, 784.760f, 1274.28f, 2069.14f, 3359.82f, 5455.60f, 8858.67f, 14384.5f, 23357.2f, 37926.9f, 61584.8f, 100000.f }
root [7] Arange<float, float, float>(1, 10, 3)
(ROOT::VecOps::RVec<float>) { 1.00000f, 4.00000f, 7.00000f }
root [8] Arange<long double, long double, long double>(3, 15, 3.1)
(ROOT::VecOps::RVec<long double>) { 3.0000000L, 6.1000000L, 9.2000000L, 12.300000L }

Copy link
Collaborator

@ferdymercury ferdymercury Apr 10, 2025

Choose a reason for hiding this comment

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

I think we could get the desired functionality by simplifying this as follows:

template <typename T, typename Ret = double> Ret Linspace(T start, T end, unsigned long long n = 50, const bool endpoint=true) {
   const long double step = static_cast<long double>(end - start) / (n - endpoint);
   tmp.push_back(std::is_floating_point_v<Ret> ? start + i * step : std::floor(start + i * step));
}

Comment on lines 3246 to 3250
* @brief Produce RVec with N evenly-spaced entries from start to end inclusive.
*
* This function generates a vector of evenly spaced values, starting at @p start and ending at @p end,
* with exactly @p n elements. The spacing is computed as
* \f$\text{step} = \frac{\text{end} - \text{start}}{n-1}\f$, so that the vector includes both endpoints.
Copy link
Member

Choose a reason for hiding this comment

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

Throughout the ROOT codebase, the Doxygen token used as sequence beginner is \ rather than @. See for example the docs of Construct from above: \begin, \tparam, \p etc. Please move all the @ instances to \ instead.

Comment on lines 3351 to 3386
template <typename T1 = double, typename T2 = double, typename T3 = double, typename Common_t = std::conditional_t<std::is_floating_point_v<std::common_type_t<T1, T2, T3>>, std::common_type_t<T1, T2, T3>, double>>
inline RVec<Common_t> Logspace(T1 start, T2 end, unsigned long long n = 128, T3 base = 10.0)
{
RVec<Common_t> temp;

if (!n || (n > std::numeric_limits<long long>::max())) // Check for invalid or absurd n.
{
return temp;
}
Copy link
Member

Choose a reason for hiding this comment

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

Similar comments that I made for LinSpace also apply here

@ferdymercury
Copy link
Collaborator

ferdymercury commented Apr 6, 2025

Thanks a lot for this!

I have some suggestions, since I think it would be good to mimick python numpy more:

  • Linspace/Logspace should accept function argument const bool endpoint = false/true; // for lin/log, see https://numpy.org/doc/2.1/reference/generated/numpy.linspace.html https://numpy.org/doc/2.1/reference/generated/numpy.logspace.html
  • when dtype=int, we should template-specialize, ie not static_cast the result, but rather "round towards -inf", as explained in Numpy docu.
  • Could Lin/Arange's default value of Common_t be automatically inferred based on the input args, as done in Numpy?. This would make it easier to do for(unsigned int i : Linspace<unsigned int>(0, 10, 11)) instead of currently for(unsigned int i : Linspace<unsigned int, unsigned int, unsigned int>(0, 10, 11)). If this is not possible, I would add a doxygen \note saying that this is different from Numpy.
  • Related to the previous point, in the Doxy docu, we could say that with C++23, you could just do a python-like enumerate for loop:
for (auto const [index, val] : std::views::enumerate(ROOT::VecOps::Linspace(6,10,16)))

@edfink234
Copy link
Author

Thanks a lot for this!

I have some suggestions, since I think it would be good to mimick python numpy more:

  • Linspace/Logspace should accept function argument const bool endpoint = false/true; // for lin/log, see https://numpy.org/doc/2.1/reference/generated/numpy.linspace.html https://numpy.org/doc/2.1/reference/generated/numpy.logspace.html
  • when dtype=int, we should template-specialize, ie not static_cast the result, but rather "round towards -inf", as explained in Numpy docu.
  • Could Lin/Arange's default value of Common_t be automatically inferred based on the input args, as done in Numpy?. This would make it easier to do for(unsigned int i : Linspace<unsigned int>(0, 10, 11)) instead of currently for(unsigned int i : Linspace<unsigned int, unsigned int, unsigned int>(0, 10, 11)). If this is not possible, I would add a doxygen \note saying that this is different from Numpy.
  • Related to the previous point, in the Doxy docu, we could say that with C++23, you could just do a python-like enumerate for loop:
for (auto const [index, val] : std::views::enumerate(ROOT::VecOps::Linspace(6,10,16)))

I tried my best to address these, though for the second and third points, I don't know how to cleanly add a specialization without incurring an ambiguous compiler error wrt being unable to deduce the correct overload.

@ferdymercury
Copy link
Collaborator

I tried my best to address these, though for the second and third points, I don't know how to cleanly add a specialization without incurring an ambiguous compiler error wrt being unable to deduce the correct overload.

Not sure, maybe it's easier to use just a ternary operator such as:

double result = std::is_floating_point ? static_cast : floor ;

@ferdymercury ferdymercury force-pushed the linSpacelogSpaceArange branch from deed8c2 to 1a2bbfd Compare April 10, 2025 10:47
@edfink234
Copy link
Author

Hi @dpiparo @vepadulano @ferdymercury

I added the changes suggested by @vepadulano and @ferdymercury. I'm not sure how I feel about the suggestion of @ferdymercury for step if I understand the need for all these casts and what the floor does exactly, but ok, I changed it and checked that it yields the expected output.

template <typename T = double, typename Ret_t = std::conditional_t<std::is_floating_point_v<T>, T, double>>
inline RVec<Ret_t> Arange(T start, T end, T step)
{
unsigned long long n = std::ceil(static_cast<Ret_t>(end-start)/static_cast<Ret_t>(step)); // Ensure floating-point division.

This comment was marked as outdated.

static_cast<long double>(end - start) / (n - endpoint);

RVec<Ret_t> temp(n);
temp[0] = static_cast<Ret_t>(start);

This comment was marked as outdated.

(static_cast<Ret_t>(end_c - start_c) / static_cast<Ret_t>(n - endpoint)) :
static_cast<long double>(end - start) / (n - endpoint);

temp[0] = static_cast<Ret_t>(std::pow(base_c, start_c));

This comment was marked as outdated.


RVec<Ret_t> temp(n);

Ret_t start_c = static_cast<Ret_t>(start);

This comment was marked as outdated.


long double step = std::is_floating_point_v<Ret_t> ?
(static_cast<Ret_t>(end) - static_cast<Ret_t>(start)) / static_cast<Ret_t>(n - endpoint) :
static_cast<long double>(end - start) / (n - endpoint);

This comment was marked as outdated.

{
for (unsigned long long i = 1; i < n; i++)
{
temp[i] = static_cast<Ret_t>(start) + static_cast<Ret_t>(i) * step;

This comment was marked as outdated.


long double step = std::is_floating_point_v<Ret_t> ?
(static_cast<Ret_t>(end_c - start_c) / static_cast<Ret_t>(n - endpoint)) :
static_cast<long double>(end - start) / (n - endpoint);

This comment was marked as outdated.

Comment on lines 3494 to 3496
long double step_c = std::is_floating_point_v<Ret_t> ?
static_cast<Ret_t>(step) :
static_cast<long double>(step);

This comment was marked as outdated.

Comment on lines 3396 to 3397
Ret_t end_c = static_cast<Ret_t>(end);
Ret_t base_c = static_cast<Ret_t>(base);

This comment was marked as outdated.

}

long double step = std::is_floating_point_v<Ret_t> ?
(static_cast<Ret_t>(end) - static_cast<Ret_t>(start)) / static_cast<Ret_t>(n - endpoint) :

This comment was marked as outdated.

@edfink234
Copy link
Author

Hi @ferdymercury

I just pushed the new commit with these changes!

Comment on lines 3395 to 3397
Ret_t start_c = static_cast<Ret_t>(start);
Ret_t end_c = static_cast<Ret_t>(end);
Ret_t base_c = static_cast<Ret_t>(base);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
Ret_t start_c = static_cast<Ret_t>(start);
Ret_t end_c = static_cast<Ret_t>(end);
Ret_t base_c = static_cast<Ret_t>(base);
long double start_c = start;
long double end_c = end;
long double base_c = base;

Comment on lines 3399 to 3401
long double step = std::is_floating_point_v<Ret_t> ?
(end_c - start_c) / static_cast<long double>(n - endpoint) :
(end >= start ? static_cast<long double>(end - start) / (n - endpoint) : (static_cast<long double>(end) - start) / (n - endpoint));
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
long double step = std::is_floating_point_v<Ret_t> ?
(end_c - start_c) / static_cast<long double>(n - endpoint) :
(end >= start ? static_cast<long double>(end - start) / (n - endpoint) : (static_cast<long double>(end) - start) / (n - endpoint));
long double step = (end_c - start_c) / (n - endpoint);

Comment on lines 3411 to 3412
Ret_t exponent = start_c + i * step;
temp[i] = std::pow(base_c, exponent);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
Ret_t exponent = start_c + i * step;
temp[i] = std::pow(base_c, exponent);
auto exponent = start_c + i * step;
temp[i] = static_cast<Ret_t>(std::pow(base_c, exponent));


if (!n || (n > std::numeric_limits<long long>::max())) // Check for invalid or absurd n.
{
return RVec<Ret_t>{};
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
return RVec<Ret_t>{};
return {};

{
if (!n || (n > std::numeric_limits<long long>::max())) // Check for invalid or absurd n.
{
return RVec<Ret_t>{};
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
return RVec<Ret_t>{};
return {};

{
if (!n || (n > std::numeric_limits<long long>::max())) // Check for invalid or absurd n.
{
return RVec<Ret_t>{};
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
return RVec<Ret_t>{};
return {};

{
for (unsigned long long i = 1; i < n; i++)
{
Ret_t exponent = start_c + i * step;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
Ret_t exponent = start_c + i * step;
auto exponent = start_c + i * step;


RVec<Ret_t> temp(n);

Ret_t start_c = std::is_floating_point_v<Ret_t> ? static_cast<Ret_t>(start) : std::floor(start);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
Ret_t start_c = std::is_floating_point_v<Ret_t> ? static_cast<Ret_t>(start) : std::floor(start);
long double start_c = start;


long double step_c = step;

temp[0] = start_c;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
temp[0] = start_c;
temp[0] = std::is_floating_point_v<Ret_t> ? static_cast<Ret_t>(start) : std::floor(start);

{
for (unsigned long long i = 1; i < n; i++)
{
temp[i] = start_c + i * step_c;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
temp[i] = start_c + i * step_c;
temp[i] = static_cast<Ret_t>(start_c + i * step_c);

@edfink234
Copy link
Author

Hi @ferdymercury @dpiparo @vepadulano! I made a commit with the most recent changes suggested by @ferdymercury.

Copy link
Collaborator

@ferdymercury ferdymercury left a comment

Choose a reason for hiding this comment

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

Thanks a lot for the changes!

I have now some corrections for the documentation,
and one issue with the int-Linspace, I am getting locally different results than yours, please cross-check.

* cout << Linspace(3, 12, 5, false) << "\n";
* // { 3, 4.8, 6.6, 8.4, 10.2 }
* Linspace<int, int>(1, 10, 3) << "\n";
* // { 1, 5, 9 }
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
* // { 1, 5, 9 }
* // { 1, 5, 10 }

Copy link
Collaborator

Choose a reason for hiding this comment

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

I get 10 with the current version of the code. You get sth different ?

Copy link
Author

Choose a reason for hiding this comment

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

No, you are right, that is indeed a typo. Thanks for the catch!

* - n does not exceed std::numeric_limits<long long>::max(), which would indicate that a negative range (or other arithmetic issue)
* has resulted in an extremely large unsigned value, thereby preventing an attempt to reserve an absurd
* amount of memory.
* \note If the template parameter \c Ret_t is explicitly overridden with an integral type, the arithmetic may cause rounding issues. Consequently, the resulting sequence may not end exactly at \p end. To ensure that the sequence ends exactly at \p end, consider casting the result as follows: `RVec<integral_type>(Linspace(...))` (which is equivalent in numpy to `np.linspace(...).astype(integral_type)`). This behavior is different than NumPy in Python.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
* \note If the template parameter \c Ret_t is explicitly overridden with an integral type, the arithmetic may cause rounding issues. Consequently, the resulting sequence may not end exactly at \p end. To ensure that the sequence ends exactly at \p end, consider casting the result as follows: `RVec<integral_type>(Linspace(...))` (which is equivalent in numpy to `np.linspace(...).astype(integral_type)`). This behavior is different than NumPy in Python.
* \note If the template parameter \c Ret_t is explicitly overridden with an integral type, the returned results are rounded towards negative (std::floor) and then cast to the integer type. This is equivalent to setting `dtype = int` in numpy.linspace. To cast to integer without rounding, use instead `RVec<integral_type>(Linspace(...))`, which would be equivalent to .astype(integral_type) in numpy.

* - n does not exceed std::numeric_limits<long long>::max(), which would indicate that a negative range (or other arithmetic issue)
* has resulted in an extremely large unsigned value, thereby preventing an attempt to reserve an absurd
* amount of memory.
* \note If the template parameter \c Ret_t is explicitly overridden with an integral type, the arithmetic may introduce rounding errors, and as a consequence, the sequence may not end exactly at \f$base^{end}\f>. To ensure that the final element is exactly \f$base^{end}\f>, consider casting the result as follows: `RVec<integral_type>(Logspace(...))` (which is equivalent in numpy to `np.logspace(...).astype(integral_type)`). This behavior is different than NumPy in Python.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
* \note If the template parameter \c Ret_t is explicitly overridden with an integral type, the arithmetic may introduce rounding errors, and as a consequence, the sequence may not end exactly at \f$base^{end}\f>. To ensure that the final element is exactly \f$base^{end}\f>, consider casting the result as follows: `RVec<integral_type>(Logspace(...))` (which is equivalent in numpy to `np.logspace(...).astype(integral_type)`). This behavior is different than NumPy in Python.
* \note If the template parameter \c Ret_t is explicitly overridden with an integral type, the returned results are rounded towards negative (std::floor) and then cast to the integer type. This is equivalent to setting `dtype = int` in numpy.linspace. To cast to integer without rounding, use instead `RVec<integral_type>(Logspace(...))`, which would be equivalent to .astype(integral_type) in numpy.

* - n does not exceed std::numeric_limits<long long>::max(), which would indicate that a negative range (or other arithmetic issue)
* has resulted in an extremely large unsigned value, thereby preventing an attempt to reserve an absurd
* amount of memory.
* \note If the template parameter \c Ret_t is explicitly overridden with an integral type, the arithmetic may introduce rounding errors, and as a consequence, the produced sequence may not strictly adhere to the intended progression. If an exact sequence is desired, consider casting the result as follows: `RVec<integral_type>(Arange(...))` (which is equivalent in numpy to `np.arange(...).astype(integral_type)`). This behavior is different than NumPy in Python.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
* \note If the template parameter \c Ret_t is explicitly overridden with an integral type, the arithmetic may introduce rounding errors, and as a consequence, the produced sequence may not strictly adhere to the intended progression. If an exact sequence is desired, consider casting the result as follows: `RVec<integral_type>(Arange(...))` (which is equivalent in numpy to `np.arange(...).astype(integral_type)`). This behavior is different than NumPy in Python.
* \note If the template parameter \c Ret_t is explicitly overridden with an integral type, the returned results are rounded towards negative (std::floor) and then cast to the integer type. This is equivalent to setting `dtype = int` in numpy. To cast to integer without rounding, use instead `RVec<integral_type>(Arange(...))`, which would be equivalent to .astype(integral_type) in numpy.

template <typename T = double, typename Ret_t = std::conditional_t<std::is_floating_point_v<T>, T, double>>
inline RVec<Ret_t> Arange(T start, T end, T step)
{
unsigned long long n = std::ceil(static_cast<Ret_t>(end-start)/static_cast<long double>(step)); // Ensure floating-point division.
Copy link
Collaborator

@ferdymercury ferdymercury May 12, 2025

Choose a reason for hiding this comment

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

Suggested change
unsigned long long n = std::ceil(static_cast<Ret_t>(end-start)/static_cast<long double>(step)); // Ensure floating-point division.
unsigned long long n = std::ceil(( end >= start ? (end - start) : static_cast<long double>(end)-start)/static_cast<long double>(step)); // Ensure floating-point division.

Copy link
Collaborator

@ferdymercury ferdymercury May 12, 2025

Choose a reason for hiding this comment

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

no need for Ret_t here. And we need split, to prevent issues if end<start and type is unsigned int.

Comment on lines 3340 to 3341
* The function is templated to allow for different arithmetic types. The return type \c Ret_t, if not explicitly specified,
* iis determined as follows: if \p T is a floating point type, that type is used; otherwise, the arithmetic is performed using \c double.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
* The function is templated to allow for different arithmetic types. The return type \c Ret_t, if not explicitly specified,
* iis determined as follows: if \p T is a floating point type, that type is used; otherwise, the arithmetic is performed using \c double.
* The function is templated to allow for different return types. The return type \c Ret_t, if not explicitly specified,
* is determined as follows: if \p T is a floating point type, that type is used; otherwise, the return type is \c double.

* iis determined as follows: if \p T is a floating point type, that type is used; otherwise, the arithmetic is performed using \c double.
*
* \tparam T Type of the start and end exponents and the base. Default is double.
* \tparam Ret_t Deduced type used for arithmetic, which, if not explicitly specified, is \p T if that is a floating point type, or double otherwise.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
* \tparam Ret_t Deduced type used for arithmetic, which, if not explicitly specified, is \p T if that is a floating point type, or double otherwise.
* \tparam Ret_t Deduced type used for return type, which, if not explicitly specified, is \p T if that is a floating point type, or double otherwise.

Comment on lines 3255 to 3261
* The function is templated to allow for different arithmetic types. The return type \c Ret_t, if
* not explicitly specified, is determined as follows: if \p T is a floating point type, that type is used;
* otherwise, the arithmetic is performed using \c double.
*
* \tparam T Type of the start and end value. Default is double.
* \tparam Ret_t Return type used for arithmetic, which, if not explicitly specified
* in the template, is \p T if that is a floating point type, or double otherwise.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
* The function is templated to allow for different arithmetic types. The return type \c Ret_t, if
* not explicitly specified, is determined as follows: if \p T is a floating point type, that type is used;
* otherwise, the arithmetic is performed using \c double.
*
* \tparam T Type of the start and end value. Default is double.
* \tparam Ret_t Return type used for arithmetic, which, if not explicitly specified
* in the template, is \p T if that is a floating point type, or double otherwise.
* The function is templated to allow for different return types. The return type \c Ret_t, if
* not explicitly specified, is determined as follows: if \p T is a floating point type, that type is used;
* otherwise, the return type is \c double.
*
* \tparam T Type of the start and end value. Default is double.
* \tparam Ret_t Return type used, which, if not explicitly specified
* in the template, is \p T if that is a floating point type, or double otherwise.

Comment on lines 3436 to 3442
* The function is templated to allow for different arithmetic types. The deduced type \c Ret_t, if not
* explicitly specified, is determined as follows: if \p T is a floating point type, that type is used;
* otherwise, the arithmetic is performed using \c double.
*
* \tparam T Type of the start, end, and step values. Default is double.
* \tparam Ret_t Deduced type used for arithmetic, which, if not explicitly
* specified, is \p T if that is a floating point type, or double otherwise.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
* The function is templated to allow for different arithmetic types. The deduced type \c Ret_t, if not
* explicitly specified, is determined as follows: if \p T is a floating point type, that type is used;
* otherwise, the arithmetic is performed using \c double.
*
* \tparam T Type of the start, end, and step values. Default is double.
* \tparam Ret_t Deduced type used for arithmetic, which, if not explicitly
* specified, is \p T if that is a floating point type, or double otherwise.
* The function is templated to allow for different return types. The return type \c Ret_t, if not
* explicitly specified, is determined as follows: if \p T is a floating point type, that type is used;
* otherwise, the return type is \c double.
*
* \tparam T Type of the start, end, and step values. Default is double.
* \tparam Ret_t Return type, which, if not explicitly
* specified, is \p T if that is a floating point type, or double otherwise.

CheckNear(Linspace(3, 12, 5, false), RVecD{ 3, 4.8, 6.6, 8.4, 10.2 });
CheckNear(Linspace(3, 12, 1, false), RVecD{ 3 });
CheckNear(Linspace(3, 12, 1, true), RVecD{ 3 });
CheckEqual(Linspace<int, int>(1, 10, 3), RVecI{ 1, 5, 9 });
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
CheckEqual(Linspace<int, int>(1, 10, 3), RVecI{ 1, 5, 9 });
CheckEqual(Linspace<int, int>(1, 10, 3), RVecI{ 1, 5, 10 });

Copy link
Member

@vepadulano vepadulano left a comment

Choose a reason for hiding this comment

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

Some more minor comments, I feel like we're getting very close to the conclusion, thanks for bearing through!

static_cast<Ret_t>(std::pow(base_c, start_c)) :
std::floor(std::pow(base_c, start_c));

if (std::is_floating_point_v<Ret_t>)
Copy link
Member

Choose a reason for hiding this comment

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

Can't this be

Suggested change
if (std::is_floating_point_v<Ret_t>)
if constexpr (std::is_floating_point_v<Ret_t>)

Copy link
Author

Choose a reason for hiding this comment

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

Does this turn it into a compile-time instruction (I've never seen it before 😅)

long double step_c = step;

temp[0] = std::is_floating_point_v<Ret_t> ? static_cast<Ret_t>(start) : std::floor(start);
if (std::is_floating_point_v<Ret_t>)
Copy link
Member

Choose a reason for hiding this comment

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

similar comment


RVec<Ret_t> temp(n);
temp[0] = std::is_floating_point_v<Ret_t> ? static_cast<Ret_t>(start) : std::floor(start);
if (std::is_floating_point_v<Ret_t>)
Copy link
Member

Choose a reason for hiding this comment

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

similar comment

Comment on lines +3278 to +3285
* \par C++23 Enumerate Support:
* With C++23, you can use the range-based enumerate view to iterate over the resulting vector with both the index
* and the value, similar to Python's `enumerate`. For example:
* ~~~{.cpp}
* for (auto const [index, val] : std::views::enumerate(ROOT::VecOps::Linspace(6, 10, 16))) {
* // Process index and val.
* }
* ~~~
Copy link
Member

Choose a reason for hiding this comment

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

Why is this section needed? Looks unrelated to this feature. Any std::vector produced in any other way will support the same syntax, it's not specific to the new functions you're introducing

Copy link
Author

Choose a reason for hiding this comment

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

I think @ferdymercury recommended this... @ferdymercury?

Copy link
Collaborator

@ferdymercury ferdymercury May 19, 2025

Choose a reason for hiding this comment

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

I thought to put it here since we are dealing with Python equivalent functions in C++, but yes, it's not really related, I agree. My vote is still to keep this information, since many people are unaware of it. But I am fine with removing it, too.

@edfink234
Copy link
Author

@ferdymercury @vepadulano @dpiparo I made the updates!

I also just wanted to share an updated benchmark using the actual ROOT RVec type instead of std::vector. Running the attached cpp file with the compilation directive g++ -std=c++20 -o linspaceTestAlternateRVec linspaceTestAlternateRVec.cpp -O2 -fno-inline-functions $(root-config --libs --cflags)

RVec

Linspace benchmark Time elapsed = 0.108864
Logspace benchmark time elapsed = 0.853611
Arange benchmark time elapsed = 0.0218328

This is better than I get with std::vector:

Linspace benchmark Time elapsed = 0.541573
Logspace benchmark time elapsed = 1.34327
Arange benchmark time elapsed = 0.0794047

vs Python numpy (python linspaceTest.py):

Linspace time elapsed = 2.097444 seconds
Logspace time elapsed = 4.336540 seconds
Arange time elapsed = 0.135273 seconds

LinspaceLogspaceArangeNumpyVsRVec.zip

Copy link
Collaborator

@ferdymercury ferdymercury left a comment

Choose a reason for hiding this comment

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

Thanks a lot! LGTM, awesome work.

As a final comment, it's inconsistent that endpoint is const but not n, start and end. Same when you define n in Arange function within the function code. I think they should be either all const or none. (I guess neither choice will affect your nice benchmark speed if compiled with -O2).

@edfink234
Copy link
Author

Thanks a lot! LGTM, awesome work.

As a final comment, it's inconsistent that endpoint is const but not n, start and end. Same when you define n in Arange function within the function code. I think they should be either all const or none. (I guess neither choice will affect your nice benchmark speed if compiled with -O2).

Thanks for the comment. I was looking into marking pass-by-value parameters as const vs non-const and stumbled upon this lengthy Stack Overflow post with mixed opinions... I looked at the RVec.hxx file and it seems like the convention is to only mark parameters with default arguments as const and just leave regular pass-by-value parameters without the const specifier...

@edfink234
Copy link
Author

@vepadulano @dpiparo Any updates? I wasn't sure from the discussion about std::views if it was wanted to delete this documentation about it...

@edfink234
Copy link
Author

@vepadulano @dpiparo Any updates? I wasn't sure from the discussion about std::views if it was wanted to delete this documentation about it...

@edfink234
Copy link
Author

@vepadulano @dpiparo Any updates? I wasn't sure from the discussion about std::views if it was wanted to delete this documentation about it.

2 similar comments
@edfink234
Copy link
Author

@vepadulano @dpiparo Any updates? I wasn't sure from the discussion about std::views if it was wanted to delete this documentation about it.

@edfink234
Copy link
Author

@vepadulano @dpiparo Any updates? I wasn't sure from the discussion about std::views if it was wanted to delete this documentation about it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add linspace, regspace, and logspace to ROOT::VecOps
5 participants