title: Indexing numpy array with half-values efficiently
body: <p>I would like to index a <code>numpy</code> array with integer and half values. This is roughly what I have in mind:</p>
  <pre class="lang-py prettyprint-override"><code>&gt;&gt;&gt; a = HalfIndexedArray([[0,1,2,3],[10,11,12,13],[20,21,22,23]])
  &gt;&gt;&gt; print(a[0,0])
  0
  &gt;&gt;&gt; print(a[0.5,0.5])
  11
  &gt;&gt;&gt; print(a[0,1+0.5])
  3
  </code></pre>
  <p>Only half-values would be used as indices, so I imagine some kind of wrapper could be constructed that stores the values at integer indices that are accessed by multiplying the given fractional index.</p>
  <p>One could construct some kind of helper function that can get and set the values accordingly; however, it would be even better if the native numpy indexing functionality (slices etc.) could be still used. Additionally, significant overhead is not really acceptable, as these arrays would be used for numerical computations.</p>
  <p>It is basically some efficient syntactic sugar that I am looking for: is this something that can be achieved in numpy (or more broadly, in Python)?</p>
  <p><strong>Edit:</strong> To make it clearer: my goal is to use both the integer and the half indices. The whole point is to make the code cleaner, as the half-indices would correspond to half-steps during time-stepping in a numerical solver (where integer-steps also exist). E.g. see <a href="https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods#Derivation_of_the_Runge%E2%80%93Kutta_fourth-order_method" rel="nofollow noreferrer">here</a> as a simple example where fractional indices are routinely used in mathematical notation.</p>
  <p><strong>Edit #2</strong>: Per @matszwecja's suggestion, I tried reimplementing <code>numpy.ndarray</code>'s <code>__(get/set)item__</code> functions like this:</p>
  <pre class="lang-py prettyprint-override"><code>import numpy as np
  
  class half_indexed_ndarray(np.ndarray):
      def __getitem__(self, key):
          print('Getting by key {0}'.format(key))
          if isinstance(key, tuple):
              tuple_double = tuple(int(i*2) for i in key)
              return super(half_indexed_ndarray, self).__getitem__(tuple_double)
          if isinstance(key, int) or isinstance(key, float):
              return super(half_indexed_ndarray, self).__getitem__(int(key * 2))
      
      def __setitem__(self, key, value):
          print('Setting by key {0}'.format(key))
          if isinstance(key, tuple):
              tuple_double = tuple(int(i*2) for i in key)
              return super(half_indexed_ndarray, self).__setitem__(tuple_double, value)
          if isinstance(key, int) or isinstance(key, float):
              return super(half_indexed_ndarray, self).__setitem__(int(key * 2), value)
  </code></pre>
  <p>For simple indexing, this does work:</p>
  <pre class="lang-py prettyprint-override"><code>a = half_indexed_ndarray((3,3))
  a[0,0]=1
  a[0,0.5]=5
  a[0.5,0.5]=505
  assert a[0,0]==1
  assert a[0,0.5]==5
  assert a[0.5,0.5]==505
  </code></pre>
  <p>However, indexing ranges does not work yet, and numpy's behaviour is a bit puzzling. For example:</p>
  <pre class="lang-py prettyprint-override"><code>&gt;&gt;&gt; print(a[-3,])
  
  Getting by key 0
  Getting by key (-3,)
  
  ---------------------------------------------------------------------------
  IndexError                                Traceback (most recent call last)
  &lt;ipython-input-239-23821ec76b06&gt; in &lt;module&gt;
  ----&gt; 1 print(a[0])
  
  ~/anaconda3/lib/python3.8/site-packages/numpy/core/arrayprint.py in _array_str_implementation(a, max_line_width, precision, suppress_small, array2string)
     1504         return _guarded_repr_or_str(np.ndarray.__getitem__(a, ()))
     1505 
  -&gt; 1506     return array2string(a, max_line_width, precision, suppress_small, ' ', &quot;&quot;)
     1507 
     1508 
  
  &lt;several more calls omitted by me&gt;
  
  &lt;ipython-input-233-eb1d93dc766b&gt; in __getitem__(self, key)
        6         if isinstance(key, tuple):
        7             tuple_double = tuple(int(i*2) for i in key)
  ----&gt; 8             return super(half_indexed_ndarray, self).__getitem__(tuple_double)
        9         if isinstance(key, int) or isinstance(key, float):
       10             return super(half_indexed_ndarray, self).__getitem__(int(key * 2))
  
  IndexError: index -6 is out of bounds for axis 0 with size 3
  </code></pre>
  <p>My interpretation is that numpy translates the tuple index <code>(0,)</code> to <code>(-3,)</code> for some reason and then calls <code>__getitem__</code> again. However, during both of these calls, the index is multiplied by two, when it only should be multiplied once. Not sure how this could be circumvented.</p>