I'm personally not particularly interested in this direction. Because there are so many options, you basically have 6 separate functions that you have to document in one docstring, and each one is a 1- or 2-liner with `arange`. That isn't enough semantic compression to justify a new function, for me.
It is also the case that there is still an ambiguity in what `closed` means in the case of a larger `step`; you excluded the option that I would have picked (`arange(start, stop+1, step)` behaves the way I would want in such cases; constant step, all values <= stop). `arange` is a flexible primitive, with semantics shared by the language itself. There are lots of ways to use that primitive to get a wide variety of results, which is what makes it a good primitive. Documenting those 1- or 2-liners in our docs would be preferable to having a new function, IMO. I would be mildly more open to one additional, focused function that *just* does a closed range (with `arange(start, stop+1, step)`) but still would prefer not to expand our API that way. On Thu, May 4, 2023 at 10:56 AM <cameron.pinne...@gmail.com> wrote: > Getting a closed interval is awkward. Let's say you want an array of the > integers from a to b, inclusive at both ends. e.g. for a=10 b=15, you want: > > [10, 11, 12, 13, 14, 15] > > Neither linspace nor arange makes this easy. > > arange(a, b) # wrong, it only goes up to 14 > arange(a, b+1) # right, but awkward. you have to remember to add one > linspace(a, b, b-a) # wrong, will produce fractions > linspace(a, b, b-a, endpoint=False) # wrong, only goes up to 14 > linspace(a, b, b-a+1) # right, awkward. also it gives you floats by default > > Both ways have the potential for an off-by-one error. I forget the right > way to do it and have to look it up, pretty much every time. > > It has been proposed before to add an step parameter to linspace or an > endpoint parameter to arange: https://github.com/numpy/numpy/issues/630 > but it was rejected on the grounds that it's too hard / impossible to make > it work properly with floats. > > But that's not a problem if the function is restricted to only give > integers in the first place. There's a few ways this can be done; one is to > make a function "crange" (closed range): > > crange(10, 15) # 10, 11, 12, 13, 14, 15 > > Like arange it would take a step parameter, defaulting to 1. Unlike > arange, the start, stop, and step parameters must all be integers. > > This raises the question of what to do when the step size doesn't evenly > divide the range. What should crange(10, 15, 3) return? There's no clear > answer. There are three constraints we might be interested in: > > 1: for 0 <= i < len(array)-1: array[i] + step == array[i+1] > 2: for x in array: start <= x <= stop > 3: array[0] == start, array[-1] == stop > > But for crange(10, 15, 3) we can only choose two: > > [10, 13, 15] # violates 1, satisfies 2 and 3 > [10, 13] # violates 2, satisfies 1 and 3 > [10, 13, 16] # violates 3, satisfies 1 and 2 > > Depending on the situation you might plausibly want any one of these. > Furthermore, there is the symmetric set of possibilities where we fix the > stop bound, and step backwards from it, letting the start bound vary: > > [10, 12, 15] # violates 1, satisfies 2 and 3 > [9, 12, 15] # violates 2, satisfies 1 and 3 > [12, 15] # violates 3, satisfies 1 and 2 > > There are two axes of behaviour here: which bound we should "anchor", and > what to do with the other bound. We can "anchor" either the start or the > stop, and with the other bound we have three choices: "open", "closed", or > "exact". Since "crange" now seems wrongly named, call it "intrange": > > def intrange(start: int, stop: int, step: int = 1, bound: str = "open", > anchor: str = "start", dtype=None): ... > > Would behave like: > > intrange(10, 15, 3) > [10, 13] # same as arange > intrange(10, 15, 3, bound="closed") > [10, 13, 16] > intrange(10, 15, 3, bound="exact") > [10, 13, 15] > intrange(10, 15, 3, bound="open", anchor="stop") > [12, 15] > intrange(10, 15, 3, bound="closed", anchor="stop") > [9, 12, 15] > intrange(10, 15, 3, bound="exact", anchor="stop") > [10, 12, 15] > > Anyway, the two most common kinds of integer range you'd want to make are > easy .. mostly you just want step=1, inclusive at the start, and either > exclusive or inclusive at the end. > > intrange(10, 15) > [10, 11, 12, 13, 14] # same as arange > intrange(10, 15, bound="closed") > [10, 11, 12, 13, 14, 15] # easy to remember, intuitive, no off-by-one-error > > >From there, you can make inclusive floating point ranges by scaling: > > intrange(10, 15, bound="closed") / 10 > [1.0, 1.1, 1.2, 1.3, 1.4, 1.5] > > This way, we don't have to worry about floating point imprecision screwing > up the bounds calculation. > > Sketch implementation: > > def intrange(start, stop, step=1, bound="open", anchor="start"): > match (anchor, bound): > case ("start", "open"): > return np.arange(start, stop, step) > case ("start", "closed"): > return np.arange(start, stop + step, step) > case ("start", "exact"): > result = np.arange(start, stop + step, step) > result[-1] = stop > return result > case ("stop", "open"): > return np.flip(np.arange(stop, start, -step)) > case ("stop", "closed"): > return np.flip(np.arange(stop, start - step, -step)) > case ("stop", "exact"): > result = np.flip(np.arange(stop, start - step, -step)) > result[0] = start > return result > case _: > assert False > _______________________________________________ > NumPy-Discussion mailing list -- numpy-discussion@python.org > To unsubscribe send an email to numpy-discussion-le...@python.org > https://mail.python.org/mailman3/lists/numpy-discussion.python.org/ > Member address: robert.k...@gmail.com > -- Robert Kern
_______________________________________________ NumPy-Discussion mailing list -- numpy-discussion@python.org To unsubscribe send an email to numpy-discussion-le...@python.org https://mail.python.org/mailman3/lists/numpy-discussion.python.org/ Member address: arch...@mail-archive.com