A clamp function inside math to put an interval

Hi, I’d like to propose adding math.clamp(value, min_val, max_val) to the standard library.

Motivation

I was working on my project and I had to reuse the following code as an util simplemax(lower, min(x, upper)). Many other languages provide a clamp() function for this common operation.

Examples

>>> math.clamp(5, 0, 10)
5
>>> math.clamp(-5, 0, 10) 
0
>>> math.clamp(15, 0, 10)
10

No idea if this is needed.

But if deemed so, then it would be nice if naming was taken from Python world - i.e. math.clip.

The version of this I have in my code: sorted((lower, val, upper))[1] but yes I’d like a builtin clamp. I’m neutral on the “should min > max raise an error” but would need answering. And prefer clamp to clip.

There’s an indepth previous discussion Mailman 3 Consider adding clip or clamp function to math - Python-ideas - python.org

And at least one other discussion Math.Clamp/Clip proposal - #4 by dirn that didn’t get very far

This has been proposed before. I don’t have a link, but you should search the forum (for “clamp” should be enough) and you’ll find the previous discussion.

As the function isn’t in Python, it’s likely that it was either considered insufficiently useful to justify being in the stdlib, or the existing ways of doing this were viewed as enough. In any case, whatever the previous objections were, you’ll need to address them if you want to make progress here.

As you already showed, max(lower, min(x, upper)) appears to be too trivial to be written as a dedicated stdlib function.

I think I disagree. Zen - “There should be one-- and preferably only one --obvious way to do it.”. There are multiple ways to write a clamp function, all with their own edge cases around handling of NaN and comparison between lower and upper bound.

I like my sorted approach as it doesn’t matter the ordering of upper/lower, it gives the middle value, but NaN breaks stuff:

from math import nan

print(sorted((nan, 1, 3)))
print(sorted((1, nan, 3)))
print(sorted((1, 3, nan)))
print(sorted((nan, 3, 1)))
print(sorted((3, nan, 1)))
print(sorted((3, 1, nan)))

gives:

[nan, 1, 3]
[1, nan, 3]
[1, 3, nan]
[nan, 1, 3]
[3, nan, 1]
[1, 3, nan]

and likewise the min/max approach doesn’t behave consistently if NaN is involved:

from math import nan

print(max(1, min(nan, 3)))
print(max(1, min(3, nan)))
print(max(nan, min(1, 3)))
print(max(nan, min(3, 1)))

gives

1
3
nan
nan

So I’d be in favour of having it in the stdlib even if it’s just so that it’s obvious what the hard bit is.

Looking through the previous mailman discussion it sounds like people were interested, I get the impression that nothing came of it because of the debate around NaN.

In terms usefulness, I’ve used probably about half of the functionality in the math module at one point or another and would probably use clamp more.

I’m in favour, a previous comment said a single function wouldn’t require a PEP, but I think this would to explain the decision of how NaN is handled and maybe ordering of different types. I’m fine with it not needing a PEP, but given how much discussion this previously prompted, I might be the minority.

1 Like

Pretty much all libraries that implement tensor have this. Although this is does not mean that math needs to as well, but given it is applicable to scalar value and there are enough use cases, it might be worthwhile consideration.

The fact that not all min/max combinations are robust adds a bit of weight to it, although forms:

max(min(value, a_max), a_min)
min(max(value, a_min), a_max)

seem to handle nans ok?


Github has quite a few hits on:

1. /max([^)]*min(/ Language:Python 200K+

2. /min([^)]*max(/ Language:Python 200K+

But not too much. However, this can take quite a few different forms.


I am currently slightly negative on this.

However, calling 2 functions for such a fairly trivial operation could be costly. If there are cases where this could improve performance in some non-trivial places it would add a bit more weight.

Right now I am very confused with the conclusion,
I am very much new to contribution to opensource, and I personally came by thinking to solve one simple issue which I am facing, but ngl

yes as @blhsing it is trivial again as @alwaysmpe it should be part of the library as most of the other languages to contain it natively.

Now myself I do not know how to go forward with it…

I think it’d need a spec writing of what it would do and what the edge cases would be. Alas, the best way to get a thing like this done is to do it yourself. But you could have a look through the mailman discussion and try to work out what the conclusion was for the desired behaviour, outline that, write a new post and see what people think. Numpy also has an implementation so would be good to look at their handling. I’d support it being added, but I’m also just one random person in the community. You could also look at creating a fork of cpython and creating an implementation. Also not easy if you’re new to the project, but people are happy to help.

For a specification I suggest following the Array API:

A function like clip makes more sense for arrays than for scalars. There is value in having these mathematical APIs across different modules and libraries be consistent with one another in so far as possible though.

The spec is the hard part since you get edge cases around NaN and type promotion. In this case because clamp looks so trivial, there are more than one ways to skin a cat and this is where things can’t get everyone happy. The value of clamp is not in its implementation (which is too trivial in itself) but in providing a spec around the special cases.

But judging from this thread I don’t think any consensus has been definitively reached by the community on the use cases: most users (including myself) rarely have to care which way max and min go first, or whether it calls __lt__ or __gt__to do the operations — but for those who do have to care, a stdlib implementation will likely be too restrictive.

Following the Python Array API sounds good to me.

  • :cross_mark: Python N/A
    • :paperclip: Python Array API clip(x, /, min, max) Spec
    • :paperclip: numpy numpy.clip(a, a_min, a_max) Docs
    • :clamp: PyTorch torch.clamp(input, min, max) Docs
      • :paperclip: torch.clip(input, min, max) is an alias Docs
    • :paperclip: TensorFlow tf.clip_by_value(t, clip_value_min, clip_value_max) Docs
    • :paperclip: JAX jax.numpy.clip(arr, /, min, max) Docs
      • :clamp: jax.lax.clamp(min, x, max) Docs min first! :lion:
    • :paperclip: CuPy cupy.clip(arr, min, max) Docs
    • :paperclip: Dask dask.array.clip(a, a_min, a_max) Docs
    • :paperclip: PyData/Sparse sparse.clip(a, min, max) Docs
    • :paperclip: Pandas pandas.DataFrame.clip(lower, upper) Docs
  • :paperclip: Matlab clip(X, lower, upper) Docs
  • :paperclip: Wolfram Clip[x, {min, max}] Docs
  • :cross_mark: R N/A
    • :paperclip: ramify clip(x, .min, .max) Docs
    • :clamp: raster clamp(x, lower, upper) Docs
  • :clamp: Ruby 123.clamp(min, max) Docs
  • :clamp: .NET System.Math.Clamp(value, min, max) Docs
  • :clamp: Java Math.clamp(value, min, max) Docs
  • :cross_mark: JavaScript N/A
  • :cross_mark: C N/A
  • :clamp: C++ std::clamp(v, lo, hi) Reference
  • :cross_mark: Rust N/A
    • :clamp: num num::clamp(input, min, max) Docs
  • :cross_mark: CUDA N/A
  • :clamp: GLSL OpenGL / OpenCL / Vulkan clamp(x, minVal, maxVal) Reference
  • :clamp: Direct3D HLSL clamp(x, min, max) Docs
  • :clamp: Apple Metal MSL clamp(x, minval, maxval) Spec
  • :clamp: WebGPU WGSL clamp(e, low, high) Spec
  • :clamp: OSL clamp(x, minval, maxval) Docs
  • :clamp: ISPC clamp(v, low, high) Docs
  • :clamp: CSS clamp(min, val, max) Docs min first! :lion:
4 Likes