Search code examples
pythonnumpy

Range or numpy Arange with end limit include


I am looking to get :

input:

arange(0.0,0.6,0.2)

output:

0.,0.4

I want

0.,0.2,0.4,0.6

how do i achieve using range or arange. If not what is alternate ?


Solution

  • Update 2023-04-21

    Had a bug in the code for stop - start being a non-integer number of steps => fixed

    In short / TLDR

    unexpected behavior:

    >>> np.arange(1, 1.3, .1)  # UNEXPECTED
    array([1. , 1.1, 1.2, 1.3])
    

    fix:

    >>> from arange_cust import *
    >>> np_arange_closed(1, 1.3, .1)
    array([1. , 1.1, 1.2, 1.3])
    >>> np_arange_open(1, 1.3, .1)
    array([1. , 1.1, 1.2])
    

    Background information

    I had your problem a few times as well. I usually quick-fixed it with adding a small value to stop. As mentioned by Kasrâmvd in the comments, the issue is a bit more complex, as floating point rounding errors can occur in numpy.arange (see here and here).

    Unexpected behavior can be found in this example:

    >>> np.arange(1, 1.3, 0.1)
    array([1. , 1.1, 1.2, 1.3])
    

    To clear up things a bit for myself, I decided to be very careful with np.arange.

    Code

    arange_cust.py:

    import numpy as np
    
    def np_arange_cust(
            *args, rtol: float=1e-05, atol: float=1e-08, include_start: bool=True, include_stop: bool = False, **kwargs
    ):
        """
        Combines numpy.arange and numpy.isclose to mimic open, half-open and closed intervals.
    
        Avoids also floating point rounding errors as with
        >>> np.arange(1, 1.3, 0.1)
        array([1., 1.1, 1.2, 1.3])
    
        Parameters
        ----------
        *args : float
            passed to np.arange
        rtol : float
            if last element of array is within this relative tolerance to stop and include[0]==False, it is skipped
        atol : float
            if last element of array is within this relative tolerance to stop and include[1]==False, it is skipped
        include_start: bool
            if first element is included in the returned array
        include_stop: bool
            if last elements are included in the returned array if stop equals last element
        kwargs :
            passed to np.arange
    
        Returns
        -------
        np.ndarray :
            as np.arange but eventually with first and last element stripped/added
        """
        # process arguments
        if len(args) == 1:
            start = 0
            stop = args[0]
            step = 1
        elif len(args) == 2:
            start, stop = args
            step = 1
        else:
            assert len(args) == 3
            start, stop, step = tuple(args)
    
        arr = np.arange(start, stop, step, **kwargs)
        if not include_start:
            arr = np.delete(arr, 0)
    
        if include_stop:
            if np.isclose(arr[-1] + step, stop, rtol=rtol, atol=atol):
                arr = np.c_[arr, arr[-1] + step]
        else:
            if np.isclose(arr[-1], stop, rtol=rtol, atol=atol):
                arr = np.delete(arr, -1)
        return arr
    
    def np_arange_closed(*args, **kwargs):
        return np_arange_cust(*args, **kwargs, include_start=True, include_stop=True)
    
    def np_arange_open(*args, **kwargs):
        return np_arange_cust(*args, **kwargs, include_start=True, include_stop=False)
    

    Pytests

    To avoid bugs in future, here is a testing module. In case we find something again, lets add a testcase. test_arange_cust.py:

    import numpy as np
    from arange_cust import np_arange_cust, np_arange_closed, np_arange_open
    import pytest
    
    class Test_np_arange_cust:
        paras_minimal_working_example = {
            "arange simple": {
                "start": 0, "stop": 7, "step": 1, "include_start": True, "include_stop": False,
                "res_exp": np.array([0, 1, 2, 3, 4, 5, 6])
            },
            "stop not on grid": {
                "start": 0, "stop": 6.5, "step": 1, "include_start": True, "include_stop": False,
                "res_exp": np.array([0, 1, 2, 3, 4, 5, 6])
            },
            "arange failing example: stop excl": {
                "start": 1, "stop": 1.3, "step": .1, "include_start": True, "include_stop": False,
                "res_exp": np.array([1., 1.1, 1.2])
            },
            "arange failing example: stop incl": {
                "start": 1, "stop": 1.3, "step": .1, "include_start": True, "include_stop": True,
                "res_exp": np.array([1., 1.1, 1.2, 1.3])
            },
            "arange failing example: stop excl + start excl": {
                "start": 1, "stop": 1.3, "step": .1, "include_start": False, "include_stop": False,
                "res_exp": np.array([1.1, 1.2])
            },
            "arange failing example: stop incl + start excl": {
                "start": 1, "stop": 1.3, "step": .1, "include_start": False, "include_stop": True,
                "res_exp": np.array([1.1, 1.2, 1.3])
            },
    
        }
    
        @pytest.mark.parametrize(
            argnames=next(iter(paras_minimal_working_example.values())).keys(),
            argvalues=[tuple(paras.values()) for paras in paras_minimal_working_example.values()],
            ids=paras_minimal_working_example.keys(),
        )
        def test_minimal_working_example(self, start, stop, step, include_start, include_stop, res_exp):
            res = np_arange_cust(start, stop, step, include_start=include_start, include_stop=include_stop)
            assert np.allclose(res, res_exp), f"Unexpected result: {res=}, {res_exp=}"