title: Get x-values corresponding to y-value on non-monotonic curves
body: <p>First, I'm not sure SO is the good place to ask this kind of question.<br>
  If not please tell me where to ask and I'll remove it.</p>
  
  <p>However, since I not found any post that address my problem, my question and my code below may be useful to other people ...</p>
  
  <p>The code part is a bit long, although I tried to keep only the minimum.
  However, this part should only serves to show that I have done some research and that I am looking for something better.</p>
  
  <p><strong>The question:</strong></p>
  
  <p>From a x-values and y-values lists, I want to find (or guess) the x-values which corresponds to a given y-value.</p>
  
  <p>When the curve defined by X and Y values is <strong>monotonic</strong>, I can simply interpolate the function <code>x = f(y)</code> :</p>
  
  <pre><code>import numpy as np
  from scipy import interpolate
  
  class AxisCam:
      def __init__(self, x=None, y=None):
          self.x = x if x else []
          self.y = y if y else []
  
          if len(self.x):
              self.xMin = min(self.x)
              self.xMax = max(self.x)
          else:
              self.xMin = None
              self.xMax = None
  
          if len(self.y):
              self.yMin = min(self.y)
              self.yMax = max(self.y)
          else:
              self.yMin = None
              self.yMax = None
  
          self._interpolX, self._interpolY = self.setInterpolator()
  
      def setInterpolator(self, interpolator=interpolate.interp1d):
          """
          Define the interpolator to use to approximate the axis cam positions
          :param interpolator: interpolator function to use, default is scipy.interpolate.interp1d
          :return: a tuple with the interpolator functions for x and y values
          """
          if len(self.x) &lt;= 0 or len(self.y) &lt;= 0:
              return None, None
          with np.errstate(divide='ignore', invalid='ignore'):  # silent the warnings caused by the interpolator
              self._interpolX = interpolator(self.y, self.x)  # x = f(y)
              self._interpolY = interpolator(self.x, self.y)  # y = f(x)
          return self._interpolX, self._interpolY
  
      def getX(self, yValue):
          """
          Return x-value corresponding to a y-value using the interpolator
          :param yValue: y-value we want to know the corresponding x-value
          :return: x-value corresponding to the given y-value
          """
          if yValue &lt; self.yMin:
              raise ValueError("value should be greater than the minimum y-value")
          elif yValue &gt; self.yMax:
              raise ValueError("value should be lesser than the maximum y-value")
          return float(self._interpolX(yValue))
  
      def getY(self, value):
          """
          Return a y-value corresponding to a x-value using the interpolator
          :param value: x-value we want to know the corresponding y-value
          :return: the y-value corresponding to the given x-value
          """
          if value &lt; self.xMin:
              raise ValueError("value should be greater than the minimum x-value")
          elif value &gt; self.xMax:
              raise ValueError("value should be lesser than the maximum x-value")
          return float(self._interpolY(value))
  
  x = [0, 0.351906, 0.703812, 1.055718]  # The 1024 values for X and Y can be retrieved here : https://pastebin.com/5eHsRjZ3
  y = [0.0, 0.000306, 0.002419, 0.008111]
  ac = AxisCam(x, y)
  print(ac.getX(100))  # returns 30.124163768271398
  </code></pre>
  
  <p>However, when the curve is <strong>non-monotonic</strong>, I can't. An exception is raised</p>
  
  <blockquote>
    <p>ValueError: x must be strictly increasing</p>
  </blockquote>
  
  <p>So, for now, I splitted the curve into monotonics part using <code>getMonotonicParts</code> method below and I can interpolate the function <code>x = f(y)</code> on each monotonic parts.</p>
  
  <pre><code>import numpy as np
  from scipy import interpolate
  
  class AxisCam:
      def __init__(self, x=None, y=None):
          self.x = x if x else []
          self.y = y if y else []
  
          if len(self.y):
              self.yMin = min(self.y)
              self.yMax = max(self.y)
          else:
              self.yMin = None
              self.yMax = None
  
          self._monotonicParts = self.getMonotonicParts()
  
      def getMonotonicParts(self, interpolator=interpolate.interp1d):
          parts = []
          prevY = None  # will store the previous value of y to compare with at each iteration
          startIdx = None  # will store the index of self.x and self.y where the monotonic part start from
          direction = 0  # 0: Unknown - 1 : constant - 2: ascending - 3: descending
          lenY = len(self.y)
          for i, (x, y) in enumerate(zip(self.x, self.y)):
              if prevY is None:
                  prevY = y
              if startIdx is None:
                  startIdx = i
  
              prevDir = direction
              direction = 1 if y == prevY else 2 if y &gt; prevY else 3
              if prevDir != 0 and prevDir != direction:  # Direction has changed =&gt; we have a new monotonic part
                  endIdx = i - 1
                  if direction == 3:  # y values are increasing =&gt; we can interpolate on it
                      interp_func = interpolator(self.y[startIdx:endIdx], self.x[startIdx:endIdx])
                  elif direction == 1:  # y values are decreasing =&gt; we need to reverse it to interpolate on it
                      xValues = self.x[startIdx:endIdx]
                      xValues.reverse()
                      yValues = self.y[startIdx:endIdx]
                      yValues.reverse()
                      interp_func = interpolator(yValues, xValues)
                  else:  # y values are the same on the range =&gt; return one of these
                      def interp_func(value): return self.y[startIdx]
                  parts.append({'start': startIdx,
                                'end': endIdx,
                                'x0': self.x[startIdx],
                                'y0': self.y[startIdx],
                                'x1': self.x[endIdx],
                                'y1': self.y[endIdx],
                                'interp': interp_func})
                  startIdx = i
              elif i == lenY - 1:  # Add element on the last iteration
                  endIdx = i
                  if direction == 2:
                      interp = interpolator(self.y[startIdx:endIdx], self.x[startIdx:endIdx])
                  else:
                      interp = None
                  parts.append({'start': startIdx,
                                'end': endIdx,
                                'x0': self.x[startIdx],
                                'y0': self.y[startIdx],
                                'x1': self.x[endIdx],
                                'y1': self.y[endIdx],
                                'interp': interp})
              prevY = y
          return parts
  
      def getX(self, yValue):
          """
          Return a list of x-values corresponding to a y-value using the interpolator
          :param yValue: y-value we want to know the corresponding x-value
          :return: a list of x-values corresponding to the given y-value
          """
          if yValue &lt; self.yMin:
              raise ValueError("value should be greater than the minimum y-value")
          elif yValue &gt; self.yMax:
              raise ValueError("value should be lesser than the maximum y-value")
          xValues = []
          for part in self._monotonicParts:
              if part['y0'] &lt;= yValue &lt;= part['y1'] or part['y0'] &gt;= yValue &gt;= part['y1']:
                  xValues.append(float(part['interp'](yValue)))
          return xValues
  
  x = []  # The 1024 values for X and Y can be retrieved here : https://pastebin.com/SL9RYYxY
  y = []  # /!\ It is not the same values that the previous example /!\
  ac = AxisCam(x, y)
  print(ac.getX(100))  # returns [122.96996037206237, 207.6239552142487]
  </code></pre>
  
  <p>My solution works quite well but seems a bit far-fetched to me and I'm wondering if there is another better methods to do this.</p>