prompt: 
  We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  regression: iam.physical returns nan for aoi > 90° when n = 1
  **Describe the bug**
  For pvlib==0.9.5, when n = 1 (no reflection) and aoi > 90°, we get nan as result.
  
  **To Reproduce**
  ```python
  import pvlib
  pvlib.iam.physical(aoi=100, n=1)
  ```
  returns `nan`.
  
  **Expected behavior**
  The result should be `0`, as it was for pvlib <= 0.9.4.
  
  
  **Versions:**
   - ``pvlib.__version__``: '0.9.5'
   - ``pandas.__version__``:  '1.5.3'
   - python: 3.10.4
  
  
  --- END ISSUE ---
  
  Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
  
  --- BEGIN FILE ---
  ```
  ### pvlib/iam.py
  r"""
  The ``iam`` module contains functions that implement models for the incidence
  angle modifier (IAM). The IAM quantifies the fraction of direct irradiance on
  a module's front surface that is transmitted through the module materials to
  the cells. Stated differently, the quantity 1 - IAM is the fraction of direct
  irradiance that is reflected away or absorbed by the module's front materials.
  IAM is typically a function of the angle of incidence (AOI) of the direct
  irradiance to the module's surface.
  """
  
  import numpy as np
  import pandas as pd
  import functools
  from pvlib.tools import cosd, sind
  
  # a dict of required parameter names for each IAM model
  # keys are the function names for the IAM models
  _IAM_MODEL_PARAMS = {
      'ashrae': {'b'},
      'physical': {'n', 'K', 'L'},
      'martin_ruiz': {'a_r'},
      'sapm': {'B0', 'B1', 'B2', 'B3', 'B4', 'B5'},
      'interp': set()
  }
  
  
  def ashrae(aoi, b=0.05):
      r"""
      Determine the incidence angle modifier using the ASHRAE transmission
      model.
  
      The ASHRAE (American Society of Heating, Refrigeration, and Air
      Conditioning Engineers) transmission model is developed in
      [1]_, and in [2]_. The model has been used in software such as PVSyst [3]_.
  
      Parameters
      ----------
      aoi : numeric
          The angle of incidence (AOI) between the module normal vector and the
          sun-beam vector in degrees. Angles of nan will result in nan.
  
      b : float, default 0.05
          A parameter to adjust the incidence angle modifier as a function of
          angle of incidence. Typical values are on the order of 0.05 [3].
  
      Returns
      -------
      iam : numeric
          The incident angle modifier (IAM). Returns zero for all abs(aoi) >= 90
          and for all ``iam`` values that would be less than 0.
  
      Notes
      -----
      The incidence angle modifier is calculated as
  
      .. math::
  
          IAM = 1 - b (\sec(aoi) - 1)
  
      As AOI approaches 90 degrees, the model yields negative values for IAM;
      negative IAM values are set to zero in this implementation.
  
      References
      ----------
      .. [1] Souka A.F., Safwat H.H., "Determination of the optimum
         orientations for the double exposure flat-plate collector and its
         reflections". Solar Energy vol .10, pp 170-174. 1966.
  
      .. [2] ASHRAE standard 93-77
  
      .. [3] PVsyst Contextual Help.
         https://files.pvsyst.com/help/index.html?iam_loss.htm retrieved on
         October 14, 2019
  
      See Also
      --------
      pvlib.iam.physical
      pvlib.iam.martin_ruiz
      pvlib.iam.interp
      """
  
      iam = 1 - b * (1 / np.cos(np.radians(aoi)) - 1)
      aoi_gte_90 = np.full_like(aoi, False, dtype='bool')
      np.greater_equal(np.abs(aoi), 90, where=~np.isnan(aoi), out=aoi_gte_90)
      iam = np.where(aoi_gte_90, 0, iam)
      iam = np.maximum(0, iam)
  
      if isinstance(aoi, pd.Series):
          iam = pd.Series(iam, index=aoi.index)
  
      return iam
  
  
  def physical(aoi, n=1.526, K=4.0, L=0.002, *, n_ar=None):
      r"""
      Determine the incidence angle modifier using refractive index ``n``,
      extinction coefficient ``K``, glazing thickness ``L`` and refractive
      index ``n_ar`` of an optional anti-reflective coating.
  
      ``iam.physical`` calculates the incidence angle modifier as described in
      [1]_, Section 3, with additional support of an anti-reflective coating.
      The calculation is based on a physical model of reflections, absorption,
      and transmission through a transparent cover.
  
      Parameters
      ----------
      aoi : numeric
          The angle of incidence between the module normal vector and the
          sun-beam vector in degrees. Angles of nan will result in nan.
  
      n : numeric, default 1.526
          The effective index of refraction (unitless). Reference [1]_
          indicates that a value of 1.526 is acceptable for glass.
  
      K : numeric, default 4.0
          The glazing extinction coefficient in units of 1/meters.
          Reference [1] indicates that a value of 4 is reasonable for
          "water white" glass.
  
      L : numeric, default 0.002
          The glazing thickness in units of meters. Reference [1]_
          indicates that 0.002 meters (2 mm) is reasonable for most
          glass-covered PV panels.
  
      n_ar : numeric, optional
          The effective index of refraction of the anti-reflective (AR) coating
          (unitless). If n_ar is None (default), no AR coating is applied.
          A typical value for the effective index of an AR coating is 1.29.
  
      Returns
      -------
      iam : numeric
          The incident angle modifier
  
      Notes
      -----
      The pvlib python authors believe that Eqn. 14 in [1]_ is
      incorrect, which presents :math:`\theta_{r} = \arcsin(n \sin(AOI))`.
      Here, :math:`\theta_{r} = \arcsin(1/n \times \sin(AOI))`
  
      References
      ----------
      .. [1] W. De Soto et al., "Improvement and validation of a model for
         photovoltaic array performance", Solar Energy, vol 80, pp. 78-88,
         2006.
  
      .. [2] Duffie, John A. & Beckman, William A.. (2006). Solar Engineering
         of Thermal Processes, third edition. [Books24x7 version] Available
         from http://common.books24x7.com/toc.aspx?bookid=17160.
  
      See Also
      --------
      pvlib.iam.martin_ruiz
      pvlib.iam.ashrae
      pvlib.iam.interp
      pvlib.iam.sapm
      """
      n1, n3 = 1, n
      if n_ar is None or np.allclose(n_ar, n1):
          # no AR coating
          n2 = n
      else:
          n2 = n_ar
  
      # incidence angle
      costheta = np.maximum(0, cosd(aoi))  # always >= 0
      sintheta = np.sqrt(1 - costheta**2)  # always >= 0
      n1costheta1 = n1 * costheta
      n2costheta1 = n2 * costheta
  
      # refraction angle of first interface
      sintheta = n1 / n2 * sintheta
      costheta = np.sqrt(1 - sintheta**2)
      n1costheta2 = n1 * costheta
      n2costheta2 = n2 * costheta
  
      # reflectance of s-, p-polarized, and normal light by the first interface
      rho12_s = ((n1costheta1 - n2costheta2) / (n1costheta1 + n2costheta2)) ** 2
      rho12_p = ((n1costheta2 - n2costheta1) / (n1costheta2 + n2costheta1)) ** 2
      rho12_0 = ((n1 - n2) / (n1 + n2)) ** 2
  
      # transmittance through the first interface
      tau_s = 1 - rho12_s
      tau_p = 1 - rho12_p
      tau_0 = 1 - rho12_0
  
      if not np.allclose(n3, n2):  # AR coated glass
          n3costheta2 = n3 * costheta
          # refraction angle of second interface
          sintheta = n2 / n3 * sintheta
          costheta = np.sqrt(1 - sintheta**2)
          n2costheta3 = n2 * costheta
          n3costheta3 = n3 * costheta
  
          # reflectance by the second interface
          rho23_s = (
              (n2costheta2 - n3costheta3) / (n2costheta2 + n3costheta3)
          ) ** 2
          rho23_p = (
              (n2costheta3 - n3costheta2) / (n2costheta3 + n3costheta2)
          ) ** 2
          rho23_0 = ((n2 - n3) / (n2 + n3)) ** 2
  
          # transmittance through the coating, including internal reflections
          # 1 + rho23*rho12 + (rho23*rho12)^2 + ... = 1/(1 - rho23*rho12)
          tau_s *= (1 - rho23_s) / (1 - rho23_s * rho12_s)
          tau_p *= (1 - rho23_p) / (1 - rho23_p * rho12_p)
          tau_0 *= (1 - rho23_0) / (1 - rho23_0 * rho12_0)
  
      # transmittance after absorption in the glass
      tau_s *= np.exp(-K * L / costheta)
      tau_p *= np.exp(-K * L / costheta)
      tau_0 *= np.exp(-K * L)
  
      # incidence angle modifier
      iam = (tau_s + tau_p) / 2 / tau_0
  
      return iam
  
  
  def martin_ruiz(aoi, a_r=0.16):
      r'''
      Determine the incidence angle modifier (IAM) using the Martin
      and Ruiz incident angle model.
  
      Parameters
      ----------
      aoi : numeric, degrees
          The angle of incidence between the module normal vector and the
          sun-beam vector in degrees.
  
      a_r : numeric
          The angular losses coefficient described in equation 3 of [1]_.
          This is an empirical dimensionless parameter. Values of ``a_r`` are
          generally on the order of 0.08 to 0.25 for flat-plate PV modules.
  
      Returns
      -------
      iam : numeric
          The incident angle modifier(s)
  
      Notes
      -----
      `martin_ruiz` calculates the incidence angle modifier (IAM) as described in
      [1]_. The information required is the incident angle (AOI) and the angular
      losses coefficient (a_r). Note that [1]_ has a corrigendum [2]_ which
      clarifies a mix-up of 'alpha's and 'a's in the former.
  
      The incident angle modifier is defined as
  
      .. math::
  
         IAM = \frac{1 - \exp(-\frac{\cos(aoi)}{a_r})}
         {1 - \exp(\frac{-1}{a_r})}
  
      which is presented as :math:`AL(\alpha) = 1 - IAM` in equation 4 of [1]_,
      with :math:`\alpha` representing the angle of incidence AOI. Thus IAM = 1
      at AOI = 0, and IAM = 0 at AOI = 90.  This equation is only valid for
      -90 <= aoi <= 90, therefore `iam` is constrained to 0.0 outside this
      interval.
  
      References
      ----------
      .. [1] N. Martin and J. M. Ruiz, "Calculation of the PV modules angular
         losses under field conditions by means of an analytical model", Solar
         Energy Materials & Solar Cells, vol. 70, pp. 25-38, 2001.
  
      .. [2] N. Martin and J. M. Ruiz, "Corrigendum to 'Calculation of the PV
         modules angular losses under field conditions by means of an
         analytical model'", Solar Energy Materials & Solar Cells, vol. 110,
         pp. 154, 2013.
  
      See Also
      --------
      pvlib.iam.martin_ruiz_diffuse
      pvlib.iam.physical
      pvlib.iam.ashrae
      pvlib.iam.interp
      pvlib.iam.sapm
      '''
      # Contributed by Anton Driesse (@adriesse), PV Performance Labs. July, 2019
  
      aoi_input = aoi
  
      aoi = np.asanyarray(aoi)
      a_r = np.asanyarray(a_r)
  
      if np.any(np.less_equal(a_r, 0)):
          raise ValueError("The parameter 'a_r' cannot be zero or negative.")
  
      with np.errstate(invalid='ignore'):
          iam = (1 - np.exp(-cosd(aoi) / a_r)) / (1 - np.exp(-1 / a_r))
          iam = np.where(np.abs(aoi) >= 90.0, 0.0, iam)
  
      if isinstance(aoi_input, pd.Series):
          iam = pd.Series(iam, index=aoi_input.index)
  
      return iam
  
  
  def martin_ruiz_diffuse(surface_tilt, a_r=0.16, c1=0.4244, c2=None):
      '''
      Determine the incidence angle modifiers (iam) for diffuse sky and
      ground-reflected irradiance using the Martin and Ruiz incident angle model.
  
      Parameters
      ----------
      surface_tilt: float or array-like, default 0
          Surface tilt angles in decimal degrees.
          The tilt angle is defined as degrees from horizontal
          (e.g. surface facing up = 0, surface facing horizon = 90)
          surface_tilt must be in the range [0, 180]
  
      a_r : numeric
          The angular losses coefficient described in equation 3 of [1]_.
          This is an empirical dimensionless parameter. Values of a_r are
          generally on the order of 0.08 to 0.25 for flat-plate PV modules.
          a_r must be greater than zero.
  
      c1 : float
          First fitting parameter for the expressions that approximate the
          integral of diffuse irradiance coming from different directions.
          c1 is given as the constant 4 / 3 / pi (0.4244) in [1]_.
  
      c2 : float
          Second fitting parameter for the expressions that approximate the
          integral of diffuse irradiance coming from different directions.
          If c2 is None, it will be calculated according to the linear
          relationship given in [3]_.
  
      Returns
      -------
      iam_sky : numeric
          The incident angle modifier for sky diffuse
  
      iam_ground : numeric
          The incident angle modifier for ground-reflected diffuse
  
      Notes
      -----
      Sky and ground modifiers are complementary: iam_sky for tilt = 30 is
      equal to iam_ground for tilt = 180 - 30.  For vertical surfaces,
      tilt = 90, the two factors are equal.
  
      References
      ----------
      .. [1] N. Martin and J. M. Ruiz, "Calculation of the PV modules angular
         losses under field conditions by means of an analytical model", Solar
         Energy Materials & Solar Cells, vol. 70, pp. 25-38, 2001.
  
      .. [2] N. Martin and J. M. Ruiz, "Corrigendum to 'Calculation of the PV
         modules angular losses under field conditions by means of an
         analytical model'", Solar Energy Materials & Solar Cells, vol. 110,
         pp. 154, 2013.
  
      .. [3] "IEC 61853-3 Photovoltaic (PV) module performance testing and energy
         rating - Part 3: Energy rating of PV modules". IEC, Geneva, 2018.
  
      See Also
      --------
      pvlib.iam.martin_ruiz
      pvlib.iam.physical
      pvlib.iam.ashrae
      pvlib.iam.interp
      pvlib.iam.sapm
      '''
      # Contributed by Anton Driesse (@adriesse), PV Performance Labs. Oct. 2019
  
      if isinstance(surface_tilt, pd.Series):
          out_index = surface_tilt.index
      else:
          out_index = None
  
      surface_tilt = np.asanyarray(surface_tilt)
  
      # avoid undefined results for horizontal or upside-down surfaces
      zeroang = 1e-06
  
      surface_tilt = np.where(surface_tilt == 0, zeroang, surface_tilt)
      surface_tilt = np.where(surface_tilt == 180, 180 - zeroang, surface_tilt)
  
      if c2 is None:
          # This equation is from [3] Sect. 7.2
          c2 = 0.5 * a_r - 0.154
  
      beta = np.radians(surface_tilt)
      sin = np.sin
      pi = np.pi
      cos = np.cos
  
      # avoid RuntimeWarnings for <, sin, and cos with nan
      with np.errstate(invalid='ignore'):
          # because sin(pi) isn't exactly zero
          sin_beta = np.where(surface_tilt < 90, sin(beta), sin(pi - beta))
  
          trig_term_sky = sin_beta + (pi - beta - sin_beta) / (1 + cos(beta))
          trig_term_gnd = sin_beta +      (beta - sin_beta) / (1 - cos(beta))  # noqa: E222 E261 E501
  
      iam_sky = 1 - np.exp(-(c1 + c2 * trig_term_sky) * trig_term_sky / a_r)
      iam_gnd = 1 - np.exp(-(c1 + c2 * trig_term_gnd) * trig_term_gnd / a_r)
  
      if out_index is not None:
          iam_sky = pd.Series(iam_sky, index=out_index, name='iam_sky')
          iam_gnd = pd.Series(iam_gnd, index=out_index, name='iam_ground')
  
      return iam_sky, iam_gnd
  
  
  def interp(aoi, theta_ref, iam_ref, method='linear', normalize=True):
      r'''
      Determine the incidence angle modifier (IAM) by interpolating a set of
      reference values, which are usually measured values.
  
      Parameters
      ----------
      aoi : numeric
          The angle of incidence between the module normal vector and the
          sun-beam vector [degrees].
  
      theta_ref : numeric
          Vector of angles at which the IAM is known [degrees].
  
      iam_ref : numeric
          IAM values for each angle in ``theta_ref`` [unitless].
  
      method : str, default 'linear'
          Specifies the interpolation method.
          Useful options are: 'linear', 'quadratic', 'cubic'.
          See scipy.interpolate.interp1d for more options.
  
      normalize : boolean, default True
          When true, the interpolated values are divided by the interpolated
          value at zero degrees.  This ensures that ``iam=1.0`` at normal
          incidence.
  
      Returns
      -------
      iam : numeric
          The incident angle modifier(s) [unitless]
  
      Notes
      -----
      ``theta_ref`` must have two or more points and may span any range of
      angles. Typically there will be a dozen or more points in the range 0-90
      degrees. Beyond the range of ``theta_ref``, IAM values are extrapolated,
      but constrained to be non-negative.
  
      The sign of ``aoi`` is ignored; only the magnitude is used.
  
      See Also
      --------
      pvlib.iam.physical
      pvlib.iam.ashrae
      pvlib.iam.martin_ruiz
      pvlib.iam.sapm
      '''
      # Contributed by Anton Driesse (@adriesse), PV Performance Labs. July, 2019
  
      from scipy.interpolate import interp1d
  
      # Scipy doesn't give the clearest feedback, so check number of points here.
      MIN_REF_VALS = {'linear': 2, 'quadratic': 3, 'cubic': 4, 1: 2, 2: 3, 3: 4}
  
      if len(theta_ref) < MIN_REF_VALS.get(method, 2):
          raise ValueError("Too few reference points defined "
                           "for interpolation method '%s'." % method)
  
      if np.any(np.less(iam_ref, 0)):
          raise ValueError("Negative value(s) found in 'iam_ref'. "
                           "This is not physically possible.")
  
      interpolator = interp1d(theta_ref, iam_ref, kind=method,
                              fill_value='extrapolate')
      aoi_input = aoi
  
      aoi = np.asanyarray(aoi)
      aoi = np.abs(aoi)
      iam = interpolator(aoi)
      iam = np.clip(iam, 0, None)
  
      if normalize:
          iam /= interpolator(0)
  
      if isinstance(aoi_input, pd.Series):
          iam = pd.Series(iam, index=aoi_input.index)
  
      return iam
  
  
  def sapm(aoi, module, upper=None):
      r"""
      Determine the incidence angle modifier (IAM) using the SAPM model.
  
      Parameters
      ----------
      aoi : numeric
          Angle of incidence in degrees. Negative input angles will return
          zeros.
  
      module : dict-like
          A dict or Series with the SAPM IAM model parameters.
          See the :py:func:`sapm` notes section for more details.
  
      upper : None or float, default None
          Upper limit on the results.
  
      Returns
      -------
      iam : numeric
          The SAPM angle of incidence loss coefficient, termed F2 in [1]_.
  
      Notes
      -----
      The SAPM [1]_ traditionally does not define an upper limit on the AOI
      loss function and values slightly exceeding 1 may exist for moderate
      angles of incidence (15-40 degrees). However, users may consider
      imposing an upper limit of 1.
  
      References
      ----------
      .. [1] King, D. et al, 2004, "Sandia Photovoltaic Array Performance
         Model", SAND Report 3535, Sandia National Laboratories, Albuquerque,
         NM.
  
      .. [2] B.H. King et al, "Procedure to Determine Coefficients for the
         Sandia Array Performance Model (SAPM)," SAND2016-5284, Sandia
         National Laboratories (2016).
  
      .. [3] B.H. King et al, "Recent Advancements in Outdoor Measurement
         Techniques for Angle of Incidence Effects," 42nd IEEE PVSC (2015).
         DOI: 10.1109/PVSC.2015.7355849
  
      See Also
      --------
      pvlib.iam.physical
      pvlib.iam.ashrae
      pvlib.iam.martin_ruiz
      pvlib.iam.interp
      """
  
      aoi_coeff = [module['B5'], module['B4'], module['B3'], module['B2'],
                   module['B1'], module['B0']]
  
      iam = np.polyval(aoi_coeff, aoi)
      iam = np.clip(iam, 0, upper)
      # nan tolerant masking
      aoi_lt_0 = np.full_like(aoi, False, dtype='bool')
      np.less(aoi, 0, where=~np.isnan(aoi), out=aoi_lt_0)
      iam = np.where(aoi_lt_0, 0, iam)
  
      if isinstance(aoi, pd.Series):
          iam = pd.Series(iam, aoi.index)
  
      return iam
  
  
  def marion_diffuse(model, surface_tilt, **kwargs):
      """
      Determine diffuse irradiance incidence angle modifiers using Marion's
      method of integrating over solid angle.
  
      Parameters
      ----------
      model : str
          The IAM function to evaluate across solid angle. Must be one of
          `'ashrae', 'physical', 'martin_ruiz', 'sapm', 'schlick'`.
  
      surface_tilt : numeric
          Surface tilt angles in decimal degrees.
          The tilt angle is defined as degrees from horizontal
          (e.g. surface facing up = 0, surface facing horizon = 90).
  
      **kwargs
          Extra parameters passed to the IAM function.
  
      Returns
      -------
      iam : dict
          IAM values for each type of diffuse irradiance:
  
              * 'sky': radiation from the sky dome (zenith <= 90)
              * 'horizon': radiation from the region of the sky near the horizon
                (89.5 <= zenith <= 90)
              * 'ground': radiation reflected from the ground (zenith >= 90)
  
          See [1]_ for a detailed description of each class.
  
      See Also
      --------
      pvlib.iam.marion_integrate
  
      References
      ----------
      .. [1] B. Marion "Numerical method for angle-of-incidence correction
         factors for diffuse radiation incident photovoltaic modules",
         Solar Energy, Volume 147, Pages 344-348. 2017.
         DOI: 10.1016/j.solener.2017.03.027
  
      Examples
      --------
      >>> marion_diffuse('physical', surface_tilt=20)
      {'sky': 0.9539178294437575,
       'horizon': 0.7652650139134007,
       'ground': 0.6387140117795903}
  
      >>> marion_diffuse('ashrae', [20, 30], b=0.04)
      {'sky': array([0.96748999, 0.96938408]),
       'horizon': array([0.86478428, 0.91825792]),
       'ground': array([0.77004435, 0.8522436 ])}
      """
  
      models = {
          'physical': physical,
          'ashrae': ashrae,
          'sapm': sapm,
          'martin_ruiz': martin_ruiz,
          'schlick': schlick,
      }
  
      try:
          iam_model = models[model]
      except KeyError:
          raise ValueError('model must be one of: ' + str(list(models.keys())))
  
      iam_function = functools.partial(iam_model, **kwargs)
      iam = {}
      for region in ['sky', 'horizon', 'ground']:
          iam[region] = marion_integrate(iam_function, surface_tilt, region)
  
      return iam
  
  
  def marion_integrate(function, surface_tilt, region, num=None):
      """
      Integrate an incidence angle modifier (IAM) function over solid angle
      to determine a diffuse irradiance correction factor using Marion's method.
  
      This lower-level function actually performs the IAM integration for the
      specified solid angle region.
  
      Parameters
      ----------
      function : callable(aoi)
          The IAM function to evaluate across solid angle. The function must
          be vectorized and take only one parameter, the angle of incidence in
          degrees.
  
      surface_tilt : numeric
          Surface tilt angles in decimal degrees.
          The tilt angle is defined as degrees from horizontal
          (e.g. surface facing up = 0, surface facing horizon = 90).
  
      region : {'sky', 'horizon', 'ground'}
          The region to integrate over. Must be one of:
  
              * 'sky': radiation from the sky dome (zenith <= 90)
              * 'horizon': radiation from the region of the sky near the horizon
                (89.5 <= zenith <= 90)
              * 'ground': radiation reflected from the ground (zenith >= 90)
  
          See [1]_ for a detailed description of each class.
  
      num : int, optional
          The number of increments in the zenith integration.
          If not specified, N will follow the values used in [1]_:
  
              * 'sky' or 'ground': num = 180
              * 'horizon': num = 1800
  
      Returns
      -------
      iam : numeric
          AOI diffuse correction factor for the specified region.
  
      See Also
      --------
      pvlib.iam.marion_diffuse
  
      References
      ----------
      .. [1] B. Marion "Numerical method for angle-of-incidence correction
         factors for diffuse radiation incident photovoltaic modules",
         Solar Energy, Volume 147, Pages 344-348. 2017.
         DOI: 10.1016/j.solener.2017.03.027
  
      Examples
      --------
      >>> marion_integrate(pvlib.iam.ashrae, 20, 'sky')
      0.9596085829811408
  
      >>> from functools import partial
      >>> f = partial(pvlib.iam.physical, n=1.3)
      >>> marion_integrate(f, [20, 30], 'sky')
      array([0.96225034, 0.9653219 ])
      """
  
      if num is None:
          if region in ['sky', 'ground']:
              num = 180
          elif region == 'horizon':
              num = 1800
          else:
              raise ValueError(f'Invalid region: {region}')
  
      beta = np.radians(surface_tilt)
      if isinstance(beta, pd.Series):
          # convert Series to np array for broadcasting later
          beta = beta.values
      ai = np.pi/num  # angular increment
  
      phi_range = np.linspace(0, np.pi, num, endpoint=False)
      psi_range = np.linspace(0, 2*np.pi, 2*num, endpoint=False)
  
      # the pseudocode in [1] do these checks at the end, but it's
      # faster to do this criteria check up front instead of later.
      if region == 'sky':
          mask = phi_range + ai <= np.pi/2
      elif region == 'horizon':
          lo = 89.5 * np.pi/180
          hi = np.pi/2
          mask = (lo <= phi_range) & (phi_range + ai <= hi)
      elif region == 'ground':
          mask = (phi_range >= np.pi/2)
      else:
          raise ValueError(f'Invalid region: {region}')
      phi_range = phi_range[mask]
  
      # fast Cartesian product of phi and psi
      angles = np.array(np.meshgrid(phi_range, psi_range)).T.reshape(-1, 2)
      # index with single-element lists to maintain 2nd dimension so that
      # these angle arrays broadcast across the beta array
      phi_1 = angles[:, [0]]
      psi_1 = angles[:, [1]]
      phi_2 = phi_1 + ai
      # psi_2 = psi_1 + ai  # not needed
      phi_avg = phi_1 + 0.5*ai
      psi_avg = psi_1 + 0.5*ai
      term_1 = np.cos(beta) * np.cos(phi_avg)
      # The AOI formula includes a term based on the difference between
      # panel azimuth and the photon azimuth, but because we assume each class
      # of diffuse irradiance is isotropic and we are integrating over all
      # angles, it doesn't matter what panel azimuth we choose (i.e., the
      # system is rotationally invariant).  So we choose gamma to be zero so
      # that we can omit it from the cos(psi_avg) term.
      # Marion's paper mentions this in the Section 3 pseudocode:
      # "set gamma to pi (or any value between 0 and 2pi)"
      term_2 = np.sin(beta) * np.sin(phi_avg) * np.cos(psi_avg)
      cosaoi = term_1 + term_2
      aoi = np.arccos(cosaoi)
      # simplify Eq 8, (psi_2 - psi_1) is always ai
      dAs = ai * (np.cos(phi_1) - np.cos(phi_2))
      cosaoi_dAs = cosaoi * dAs
      # apply the final AOI check, zeroing out non-passing points
      mask = aoi < np.pi/2
      cosaoi_dAs = np.where(mask, cosaoi_dAs, 0)
      numerator = np.sum(function(np.degrees(aoi)) * cosaoi_dAs, axis=0)
      denominator = np.sum(cosaoi_dAs, axis=0)
  
      with np.errstate(invalid='ignore'):
          # in some cases, no points pass the criteria
          # (e.g. region='ground', surface_tilt=0), so we override the division
          # by zero to set Fd=0.  Also, preserve nans in beta.
          Fd = np.where((denominator != 0) | ~np.isfinite(beta),
                        numerator / denominator,
                        0)
  
      # preserve input type
      if np.isscalar(surface_tilt):
          Fd = Fd.item()
      elif isinstance(surface_tilt, pd.Series):
          Fd = pd.Series(Fd, surface_tilt.index)
  
      return Fd
  
  
  def schlick(aoi):
      """
      Determine incidence angle modifier (IAM) for direct irradiance using the
      Schlick approximation to the Fresnel equations.
  
      The Schlick approximation was proposed in [1]_ as a computationally
      efficient alternative to computing the Fresnel factor in computer
      graphics contexts.  This implementation is a normalized form of the
      equation in [1]_ so that it can be used as a PV IAM model.
      Unlike other IAM models, this model has no ability to describe
      different reflection profiles.
  
      In PV contexts, the Schlick approximation has been used as an analytically
      integrable alternative to the Fresnel equations for estimating IAM
      for diffuse irradiance [2]_.
  
      Parameters
      ----------
      aoi : numeric
          The angle of incidence (AOI) between the module normal vector and the
          sun-beam vector. Angles of nan will result in nan. [degrees]
  
      Returns
      -------
      iam : numeric
          The incident angle modifier.
  
      References
      ----------
      .. [1] Schlick, C. An inexpensive BRDF model for physically-based
         rendering. Computer graphics forum 13 (1994).
  
      .. [2] Xie, Y., M. Sengupta, A. Habte, A. Andreas, "The 'Fresnel Equations'
         for Diffuse radiation on Inclined photovoltaic Surfaces (FEDIS)",
         Renewable and Sustainable Energy Reviews, vol. 161, 112362. June 2022.
         :doi:`10.1016/j.rser.2022.112362`
  
      See Also
      --------
      pvlib.iam.schlick_diffuse
      """
      iam = 1 - (1 - cosd(aoi)) ** 5
      iam = np.where(np.abs(aoi) >= 90.0, 0.0, iam)
  
      # preserve input type
      if np.isscalar(aoi):
          iam = iam.item()
      elif isinstance(aoi, pd.Series):
          iam = pd.Series(iam, aoi.index)
  
      return iam
  
  
  def schlick_diffuse(surface_tilt):
      """
      Determine the incidence angle modifiers (IAM) for diffuse sky and
      ground-reflected irradiance on a tilted surface using the Schlick
      incident angle model.
  
      The diffuse iam values are calculated using an analytical integration
      of the Schlick equation [1]_ over the portion of an isotropic sky and
      isotropic foreground that is visible from the tilted surface [2]_.
  
      Parameters
      ----------
      surface_tilt : numeric
          Surface tilt angle measured from horizontal (e.g. surface facing
          up = 0, surface facing horizon = 90). [degrees]
  
      Returns
      -------
      iam_sky : numeric
          The incident angle modifier for sky diffuse.
  
      iam_ground : numeric
          The incident angle modifier for ground-reflected diffuse.
  
      References
      ----------
      .. [1] Schlick, C. An inexpensive BRDF model for physically-based
         rendering. Computer graphics forum 13 (1994).
  
      .. [2] Xie, Y., M. Sengupta, A. Habte, A. Andreas, "The 'Fresnel Equations'
         for Diffuse radiation on Inclined photovoltaic Surfaces (FEDIS)",
         Renewable and Sustainable Energy Reviews, vol. 161, 112362. June 2022.
         :doi:`10.1016/j.rser.2022.112362`
  
      See Also
      --------
      pvlib.iam.schlick
      """
      # these calculations are as in [2]_, but with the refractive index
      # weighting coefficient w set to 1.0 (so it is omitted)
  
      # relative transmittance of sky diffuse radiation by PV cover:
      cosB = cosd(surface_tilt)
      sinB = sind(surface_tilt)
      cuk = (2 / (np.pi * (1 + cosB))) * (
          (30/7)*np.pi - (160/21)*np.radians(surface_tilt) - (10/3)*np.pi*cosB
          + (160/21)*cosB*sinB - (5/3)*np.pi*cosB*sinB**2 + (20/7)*cosB*sinB**3
          - (5/16)*np.pi*cosB*sinB**4 + (16/105)*cosB*sinB**5
      )  # Eq 4 in [2]
  
      # relative transmittance of ground-reflected radiation by PV cover:
      with np.errstate(divide='ignore', invalid='ignore'):  # Eq 6 in [2]
          cug = 40 / (21 * (1 - cosB)) - (1 + cosB) / (1 - cosB) * cuk
  
      cug = np.where(surface_tilt < 1e-6, 0, cug)
  
      # respect input types:
      if np.isscalar(surface_tilt):
          cuk = cuk.item()
          cug = cug.item()
      elif isinstance(surface_tilt, pd.Series):
          cuk = pd.Series(cuk, surface_tilt.index)
          cug = pd.Series(cug, surface_tilt.index)
  
      return cuk, cug
  
  ```
  --- END FILE ---
  
  Please first localize the bug based on the issue statement, and then generate *SEARCH/REPLACE* edits to fix the issue.
  
  Every *SEARCH/REPLACE* edit must use this format:
  1. The file path
  2. The start of search block: <<<<<<< SEARCH
  3. A contiguous chunk of lines to search for in the existing source code
  4. The dividing line: =======
  5. The lines to replace into the source code
  6. The end of the replace block: >>>>>>> REPLACE
  
  Here is an example:
  
  ```python
  ### mathweb/flask/app.py
  <<<<<<< SEARCH
  from flask import Flask
  =======
  import math
  from flask import Flask
  >>>>>>> REPLACE
  ```
  
  Please note that the *SEARCH/REPLACE* edit REQUIRES PROPER INDENTATION. If you would like to add the line '        print(x)', you must fully write that out, with all those spaces before the code!
  Wrap the *SEARCH/REPLACE* edit in blocks ```python...```.
  
instance: {"FAIL_TO_PASS": "[\"pvlib/tests/test_iam.py::test_physical_n1_L0\"]", "PASS_TO_PASS": "[\"pvlib/tests/test_iam.py::test_ashrae\", \"pvlib/tests/test_iam.py::test_ashrae_scalar\", \"pvlib/tests/test_iam.py::test_physical\", \"pvlib/tests/test_iam.py::test_physical_ar\", \"pvlib/tests/test_iam.py::test_physical_noar\", \"pvlib/tests/test_iam.py::test_physical_scalar\", \"pvlib/tests/test_iam.py::test_martin_ruiz\", \"pvlib/tests/test_iam.py::test_martin_ruiz_exception\", \"pvlib/tests/test_iam.py::test_martin_ruiz_diffuse\", \"pvlib/tests/test_iam.py::test_iam_interp\", \"pvlib/tests/test_iam.py::test_sapm[45-0.9975036250000002]\", \"pvlib/tests/test_iam.py::test_sapm[aoi1-expected1]\", \"pvlib/tests/test_iam.py::test_sapm[aoi2-expected2]\", \"pvlib/tests/test_iam.py::test_sapm_limits\", \"pvlib/tests/test_iam.py::test_marion_diffuse_model\", \"pvlib/tests/test_iam.py::test_marion_diffuse_kwargs\", \"pvlib/tests/test_iam.py::test_marion_diffuse_invalid\", \"pvlib/tests/test_iam.py::test_marion_integrate_scalar[sky-180-0.9596085829811408]\", \"pvlib/tests/test_iam.py::test_marion_integrate_scalar[horizon-1800-0.8329070417832541]\", \"pvlib/tests/test_iam.py::test_marion_integrate_scalar[ground-180-0.719823559106309]\", \"pvlib/tests/test_iam.py::test_marion_integrate_list[sky-180-expected0]\", \"pvlib/tests/test_iam.py::test_marion_integrate_list[horizon-1800-expected1]\", \"pvlib/tests/test_iam.py::test_marion_integrate_list[ground-180-expected2]\", \"pvlib/tests/test_iam.py::test_marion_integrate_series[sky-180-expected0]\", \"pvlib/tests/test_iam.py::test_marion_integrate_series[horizon-1800-expected1]\", \"pvlib/tests/test_iam.py::test_marion_integrate_series[ground-180-expected2]\", \"pvlib/tests/test_iam.py::test_marion_integrate_ground_flat\", \"pvlib/tests/test_iam.py::test_marion_integrate_invalid\", \"pvlib/tests/test_iam.py::test_schlick\", \"pvlib/tests/test_iam.py::test_schlick_diffuse\"]", "base_commit": "40e9e978c170bdde4eeee1547729417665dbc34c", "created_at": "2023-03-24T10:46:42Z", "environment_setup_commit": "6072e0982c3c0236f532ddfa48fbf461180d834e", "hints_text": "", "instance_id": "pvlib__pvlib-python-1707", "patch": "diff --git a/pvlib/iam.py b/pvlib/iam.py\n--- a/pvlib/iam.py\n+++ b/pvlib/iam.py\n@@ -175,8 +175,12 @@ def physical(aoi, n=1.526, K=4.0, L=0.002, *, n_ar=None):\n     n2costheta2 = n2 * costheta\n \n     # reflectance of s-, p-polarized, and normal light by the first interface\n-    rho12_s = ((n1costheta1 - n2costheta2) / (n1costheta1 + n2costheta2)) ** 2\n-    rho12_p = ((n1costheta2 - n2costheta1) / (n1costheta2 + n2costheta1)) ** 2\n+    with np.errstate(divide='ignore', invalid='ignore'):\n+        rho12_s = \\\n+            ((n1costheta1 - n2costheta2) / (n1costheta1 + n2costheta2)) ** 2\n+        rho12_p = \\\n+            ((n1costheta2 - n2costheta1) / (n1costheta2 + n2costheta1)) ** 2\n+\n     rho12_0 = ((n1 - n2) / (n1 + n2)) ** 2\n \n     # transmittance through the first interface\n@@ -208,13 +212,22 @@ def physical(aoi, n=1.526, K=4.0, L=0.002, *, n_ar=None):\n         tau_0 *= (1 - rho23_0) / (1 - rho23_0 * rho12_0)\n \n     # transmittance after absorption in the glass\n-    tau_s *= np.exp(-K * L / costheta)\n-    tau_p *= np.exp(-K * L / costheta)\n+    with np.errstate(divide='ignore', invalid='ignore'):\n+        tau_s *= np.exp(-K * L / costheta)\n+        tau_p *= np.exp(-K * L / costheta)\n+\n     tau_0 *= np.exp(-K * L)\n \n     # incidence angle modifier\n     iam = (tau_s + tau_p) / 2 / tau_0\n \n+    # for light coming from behind the plane, none can enter the module\n+    # when n2 > 1, this is already the case\n+    if np.isclose(n2, 1).any():\n+        iam = np.where(aoi >= 90, 0, iam)\n+        if isinstance(aoi, pd.Series):\n+            iam = pd.Series(iam, index=aoi.index)\n+\n     return iam\n \n \n", "problem_statement": "regression: iam.physical returns nan for aoi > 90\u00b0 when n = 1\n**Describe the bug**\r\nFor pvlib==0.9.5, when n = 1 (no reflection) and aoi > 90\u00b0, we get nan as result.\r\n\r\n**To Reproduce**\r\n```python\r\nimport pvlib\r\npvlib.iam.physical(aoi=100, n=1)\r\n```\r\nreturns `nan`.\r\n\r\n**Expected behavior**\r\nThe result should be `0`, as it was for pvlib <= 0.9.4.\r\n\r\n\r\n**Versions:**\r\n - ``pvlib.__version__``: '0.9.5'\r\n - ``pandas.__version__``:  '1.5.3'\r\n - python: 3.10.4\r\n\n", "repo": "pvlib/pvlib-python", "test_patch": "diff --git a/pvlib/tests/test_iam.py b/pvlib/tests/test_iam.py\n--- a/pvlib/tests/test_iam.py\n+++ b/pvlib/tests/test_iam.py\n@@ -51,6 +51,18 @@ def test_physical():\n     assert_series_equal(iam, expected)\n \n \n+def test_physical_n1_L0():\n+    aoi = np.array([0, 22.5, 45, 67.5, 90, 100, np.nan])\n+    expected = np.array([1, 1, 1, 1, 0, 0, np.nan])\n+    iam = _iam.physical(aoi, n=1, L=0)\n+    assert_allclose(iam, expected, equal_nan=True)\n+\n+    aoi = pd.Series(aoi)\n+    expected = pd.Series(expected)\n+    iam = _iam.physical(aoi, n=1, L=0)\n+    assert_series_equal(iam, expected)\n+\n+\n def test_physical_ar():\n     aoi = np.array([0, 22.5, 45, 67.5, 90, 100, np.nan])\n     expected = np.array([1, 0.99944171, 0.9917463, 0.91506158, 0, 0, np.nan])\n", "version": "0.9"}
parsed_commit_content: {"file_diffs":[{"old_file_content":"r\"\"\"\nThe ``iam`` module contains functions that implement models for the incidence\nangle modifier (IAM). The IAM quantifies the fraction of direct irradiance on\na module's front surface that is transmitted through the module materials to\nthe cells. Stated differently, the quantity 1 - IAM is the fraction of direct\nirradiance that is reflected away or absorbed by the module's front materials.\nIAM is typically a function of the angle of incidence (AOI) of the direct\nirradiance to the module's surface.\n\"\"\"\n\nimport numpy as np\nimport pandas as pd\nimport functools\nfrom pvlib.tools import cosd, sind\n\n# a dict of required parameter names for each IAM model\n# keys are the function names for the IAM models\n_IAM_MODEL_PARAMS = {\n    'ashrae': {'b'},\n    'physical': {'n', 'K', 'L'},\n    'martin_ruiz': {'a_r'},\n    'sapm': {'B0', 'B1', 'B2', 'B3', 'B4', 'B5'},\n    'interp': set()\n}\n\n\ndef ashrae(aoi, b=0.05):\n    r\"\"\"\n    Determine the incidence angle modifier using the ASHRAE transmission\n    model.\n\n    The ASHRAE (American Society of Heating, Refrigeration, and Air\n    Conditioning Engineers) transmission model is developed in\n    [1]_, and in [2]_. The model has been used in software such as PVSyst [3]_.\n\n    Parameters\n    ----------\n    aoi : numeric\n        The angle of incidence (AOI) between the module normal vector and the\n        sun-beam vector in degrees. Angles of nan will result in nan.\n\n    b : float, default 0.05\n        A parameter to adjust the incidence angle modifier as a function of\n        angle of incidence. Typical values are on the order of 0.05 [3].\n\n    Returns\n    -------\n    iam : numeric\n        The incident angle modifier (IAM). Returns zero for all abs(aoi) >= 90\n        and for all ``iam`` values that would be less than 0.\n\n    Notes\n    -----\n    The incidence angle modifier is calculated as\n\n    .. math::\n\n        IAM = 1 - b (\\sec(aoi) - 1)\n\n    As AOI approaches 90 degrees, the model yields negative values for IAM;\n    negative IAM values are set to zero in this implementation.\n\n    References\n    ----------\n    .. [1] Souka A.F., Safwat H.H., \"Determination of the optimum\n       orientations for the double exposure flat-plate collector and its\n       reflections\". Solar Energy vol .10, pp 170-174. 1966.\n\n    .. [2] ASHRAE standard 93-77\n\n    .. [3] PVsyst Contextual Help.\n       https://files.pvsyst.com/help/index.html?iam_loss.htm retrieved on\n       October 14, 2019\n\n    See Also\n    --------\n    pvlib.iam.physical\n    pvlib.iam.martin_ruiz\n    pvlib.iam.interp\n    \"\"\"\n\n    iam = 1 - b * (1 / np.cos(np.radians(aoi)) - 1)\n    aoi_gte_90 = np.full_like(aoi, False, dtype='bool')\n    np.greater_equal(np.abs(aoi), 90, where=~np.isnan(aoi), out=aoi_gte_90)\n    iam = np.where(aoi_gte_90, 0, iam)\n    iam = np.maximum(0, iam)\n\n    if isinstance(aoi, pd.Series):\n        iam = pd.Series(iam, index=aoi.index)\n\n    return iam\n\n\ndef physical(aoi, n=1.526, K=4.0, L=0.002, *, n_ar=None):\n    r\"\"\"\n    Determine the incidence angle modifier using refractive index ``n``,\n    extinction coefficient ``K``, glazing thickness ``L`` and refractive\n    index ``n_ar`` of an optional anti-reflective coating.\n\n    ``iam.physical`` calculates the incidence angle modifier as described in\n    [1]_, Section 3, with additional support of an anti-reflective coating.\n    The calculation is based on a physical model of reflections, absorption,\n    and transmission through a transparent cover.\n\n    Parameters\n    ----------\n    aoi : numeric\n        The angle of incidence between the module normal vector and the\n        sun-beam vector in degrees. Angles of nan will result in nan.\n\n    n : numeric, default 1.526\n        The effective index of refraction (unitless). Reference [1]_\n        indicates that a value of 1.526 is acceptable for glass.\n\n    K : numeric, default 4.0\n        The glazing extinction coefficient in units of 1/meters.\n        Reference [1] indicates that a value of 4 is reasonable for\n        \"water white\" glass.\n\n    L : numeric, default 0.002\n        The glazing thickness in units of meters. Reference [1]_\n        indicates that 0.002 meters (2 mm) is reasonable for most\n        glass-covered PV panels.\n\n    n_ar : numeric, optional\n        The effective index of refraction of the anti-reflective (AR) coating\n        (unitless). If n_ar is None (default), no AR coating is applied.\n        A typical value for the effective index of an AR coating is 1.29.\n\n    Returns\n    -------\n    iam : numeric\n        The incident angle modifier\n\n    Notes\n    -----\n    The pvlib python authors believe that Eqn. 14 in [1]_ is\n    incorrect, which presents :math:`\\theta_{r} = \\arcsin(n \\sin(AOI))`.\n    Here, :math:`\\theta_{r} = \\arcsin(1/n \\times \\sin(AOI))`\n\n    References\n    ----------\n    .. [1] W. De Soto et al., \"Improvement and validation of a model for\n       photovoltaic array performance\", Solar Energy, vol 80, pp. 78-88,\n       2006.\n\n    .. [2] Duffie, John A. & Beckman, William A.. (2006). Solar Engineering\n       of Thermal Processes, third edition. [Books24x7 version] Available\n       from http://common.books24x7.com/toc.aspx?bookid=17160.\n\n    See Also\n    --------\n    pvlib.iam.martin_ruiz\n    pvlib.iam.ashrae\n    pvlib.iam.interp\n    pvlib.iam.sapm\n    \"\"\"\n    n1, n3 = 1, n\n    if n_ar is None or np.allclose(n_ar, n1):\n        # no AR coating\n        n2 = n\n    else:\n        n2 = n_ar\n\n    # incidence angle\n    costheta = np.maximum(0, cosd(aoi))  # always >= 0\n    sintheta = np.sqrt(1 - costheta**2)  # always >= 0\n    n1costheta1 = n1 * costheta\n    n2costheta1 = n2 * costheta\n\n    # refraction angle of first interface\n    sintheta = n1 / n2 * sintheta\n    costheta = np.sqrt(1 - sintheta**2)\n    n1costheta2 = n1 * costheta\n    n2costheta2 = n2 * costheta\n\n    # reflectance of s-, p-polarized, and normal light by the first interface\n    rho12_s = ((n1costheta1 - n2costheta2) / (n1costheta1 + n2costheta2)) ** 2\n    rho12_p = ((n1costheta2 - n2costheta1) / (n1costheta2 + n2costheta1)) ** 2\n    rho12_0 = ((n1 - n2) / (n1 + n2)) ** 2\n\n    # transmittance through the first interface\n    tau_s = 1 - rho12_s\n    tau_p = 1 - rho12_p\n    tau_0 = 1 - rho12_0\n\n    if not np.allclose(n3, n2):  # AR coated glass\n        n3costheta2 = n3 * costheta\n        # refraction angle of second interface\n        sintheta = n2 / n3 * sintheta\n        costheta = np.sqrt(1 - sintheta**2)\n        n2costheta3 = n2 * costheta\n        n3costheta3 = n3 * costheta\n\n        # reflectance by the second interface\n        rho23_s = (\n            (n2costheta2 - n3costheta3) / (n2costheta2 + n3costheta3)\n        ) ** 2\n        rho23_p = (\n            (n2costheta3 - n3costheta2) / (n2costheta3 + n3costheta2)\n        ) ** 2\n        rho23_0 = ((n2 - n3) / (n2 + n3)) ** 2\n\n        # transmittance through the coating, including internal reflections\n        # 1 + rho23*rho12 + (rho23*rho12)^2 + ... = 1/(1 - rho23*rho12)\n        tau_s *= (1 - rho23_s) / (1 - rho23_s * rho12_s)\n        tau_p *= (1 - rho23_p) / (1 - rho23_p * rho12_p)\n        tau_0 *= (1 - rho23_0) / (1 - rho23_0 * rho12_0)\n\n    # transmittance after absorption in the glass\n    tau_s *= np.exp(-K * L / costheta)\n    tau_p *= np.exp(-K * L / costheta)\n    tau_0 *= np.exp(-K * L)\n\n    # incidence angle modifier\n    iam = (tau_s + tau_p) / 2 / tau_0\n\n    return iam\n\n\ndef martin_ruiz(aoi, a_r=0.16):\n    r'''\n    Determine the incidence angle modifier (IAM) using the Martin\n    and Ruiz incident angle model.\n\n    Parameters\n    ----------\n    aoi : numeric, degrees\n        The angle of incidence between the module normal vector and the\n        sun-beam vector in degrees.\n\n    a_r : numeric\n        The angular losses coefficient described in equation 3 of [1]_.\n        This is an empirical dimensionless parameter. Values of ``a_r`` are\n        generally on the order of 0.08 to 0.25 for flat-plate PV modules.\n\n    Returns\n    -------\n    iam : numeric\n        The incident angle modifier(s)\n\n    Notes\n    -----\n    `martin_ruiz` calculates the incidence angle modifier (IAM) as described in\n    [1]_. The information required is the incident angle (AOI) and the angular\n    losses coefficient (a_r). Note that [1]_ has a corrigendum [2]_ which\n    clarifies a mix-up of 'alpha's and 'a's in the former.\n\n    The incident angle modifier is defined as\n\n    .. math::\n\n       IAM = \\frac{1 - \\exp(-\\frac{\\cos(aoi)}{a_r})}\n       {1 - \\exp(\\frac{-1}{a_r})}\n\n    which is presented as :math:`AL(\\alpha) = 1 - IAM` in equation 4 of [1]_,\n    with :math:`\\alpha` representing the angle of incidence AOI. Thus IAM = 1\n    at AOI = 0, and IAM = 0 at AOI = 90.  This equation is only valid for\n    -90 <= aoi <= 90, therefore `iam` is constrained to 0.0 outside this\n    interval.\n\n    References\n    ----------\n    .. [1] N. Martin and J. M. Ruiz, \"Calculation of the PV modules angular\n       losses under field conditions by means of an analytical model\", Solar\n       Energy Materials & Solar Cells, vol. 70, pp. 25-38, 2001.\n\n    .. [2] N. Martin and J. M. Ruiz, \"Corrigendum to 'Calculation of the PV\n       modules angular losses under field conditions by means of an\n       analytical model'\", Solar Energy Materials & Solar Cells, vol. 110,\n       pp. 154, 2013.\n\n    See Also\n    --------\n    pvlib.iam.martin_ruiz_diffuse\n    pvlib.iam.physical\n    pvlib.iam.ashrae\n    pvlib.iam.interp\n    pvlib.iam.sapm\n    '''\n    # Contributed by Anton Driesse (@adriesse), PV Performance Labs. July, 2019\n\n    aoi_input = aoi\n\n    aoi = np.asanyarray(aoi)\n    a_r = np.asanyarray(a_r)\n\n    if np.any(np.less_equal(a_r, 0)):\n        raise ValueError(\"The parameter 'a_r' cannot be zero or negative.\")\n\n    with np.errstate(invalid='ignore'):\n        iam = (1 - np.exp(-cosd(aoi) / a_r)) / (1 - np.exp(-1 / a_r))\n        iam = np.where(np.abs(aoi) >= 90.0, 0.0, iam)\n\n    if isinstance(aoi_input, pd.Series):\n        iam = pd.Series(iam, index=aoi_input.index)\n\n    return iam\n\n\ndef martin_ruiz_diffuse(surface_tilt, a_r=0.16, c1=0.4244, c2=None):\n    '''\n    Determine the incidence angle modifiers (iam) for diffuse sky and\n    ground-reflected irradiance using the Martin and Ruiz incident angle model.\n\n    Parameters\n    ----------\n    surface_tilt: float or array-like, default 0\n        Surface tilt angles in decimal degrees.\n        The tilt angle is defined as degrees from horizontal\n        (e.g. surface facing up = 0, surface facing horizon = 90)\n        surface_tilt must be in the range [0, 180]\n\n    a_r : numeric\n        The angular losses coefficient described in equation 3 of [1]_.\n        This is an empirical dimensionless parameter. Values of a_r are\n        generally on the order of 0.08 to 0.25 for flat-plate PV modules.\n        a_r must be greater than zero.\n\n    c1 : float\n        First fitting parameter for the expressions that approximate the\n        integral of diffuse irradiance coming from different directions.\n        c1 is given as the constant 4 / 3 / pi (0.4244) in [1]_.\n\n    c2 : float\n        Second fitting parameter for the expressions that approximate the\n        integral of diffuse irradiance coming from different directions.\n        If c2 is None, it will be calculated according to the linear\n        relationship given in [3]_.\n\n    Returns\n    -------\n    iam_sky : numeric\n        The incident angle modifier for sky diffuse\n\n    iam_ground : numeric\n        The incident angle modifier for ground-reflected diffuse\n\n    Notes\n    -----\n    Sky and ground modifiers are complementary: iam_sky for tilt = 30 is\n    equal to iam_ground for tilt = 180 - 30.  For vertical surfaces,\n    tilt = 90, the two factors are equal.\n\n    References\n    ----------\n    .. [1] N. Martin and J. M. Ruiz, \"Calculation of the PV modules angular\n       losses under field conditions by means of an analytical model\", Solar\n       Energy Materials & Solar Cells, vol. 70, pp. 25-38, 2001.\n\n    .. [2] N. Martin and J. M. Ruiz, \"Corrigendum to 'Calculation of the PV\n       modules angular losses under field conditions by means of an\n       analytical model'\", Solar Energy Materials & Solar Cells, vol. 110,\n       pp. 154, 2013.\n\n    .. [3] \"IEC 61853-3 Photovoltaic (PV) module performance testing and energy\n       rating - Part 3: Energy rating of PV modules\". IEC, Geneva, 2018.\n\n    See Also\n    --------\n    pvlib.iam.martin_ruiz\n    pvlib.iam.physical\n    pvlib.iam.ashrae\n    pvlib.iam.interp\n    pvlib.iam.sapm\n    '''\n    # Contributed by Anton Driesse (@adriesse), PV Performance Labs. Oct. 2019\n\n    if isinstance(surface_tilt, pd.Series):\n        out_index = surface_tilt.index\n    else:\n        out_index = None\n\n    surface_tilt = np.asanyarray(surface_tilt)\n\n    # avoid undefined results for horizontal or upside-down surfaces\n    zeroang = 1e-06\n\n    surface_tilt = np.where(surface_tilt == 0, zeroang, surface_tilt)\n    surface_tilt = np.where(surface_tilt == 180, 180 - zeroang, surface_tilt)\n\n    if c2 is None:\n        # This equation is from [3] Sect. 7.2\n        c2 = 0.5 * a_r - 0.154\n\n    beta = np.radians(surface_tilt)\n    sin = np.sin\n    pi = np.pi\n    cos = np.cos\n\n    # avoid RuntimeWarnings for <, sin, and cos with nan\n    with np.errstate(invalid='ignore'):\n        # because sin(pi) isn't exactly zero\n        sin_beta = np.where(surface_tilt < 90, sin(beta), sin(pi - beta))\n\n        trig_term_sky = sin_beta + (pi - beta - sin_beta) / (1 + cos(beta))\n        trig_term_gnd = sin_beta +      (beta - sin_beta) / (1 - cos(beta))  # noqa: E222 E261 E501\n\n    iam_sky = 1 - np.exp(-(c1 + c2 * trig_term_sky) * trig_term_sky / a_r)\n    iam_gnd = 1 - np.exp(-(c1 + c2 * trig_term_gnd) * trig_term_gnd / a_r)\n\n    if out_index is not None:\n        iam_sky = pd.Series(iam_sky, index=out_index, name='iam_sky')\n        iam_gnd = pd.Series(iam_gnd, index=out_index, name='iam_ground')\n\n    return iam_sky, iam_gnd\n\n\ndef interp(aoi, theta_ref, iam_ref, method='linear', normalize=True):\n    r'''\n    Determine the incidence angle modifier (IAM) by interpolating a set of\n    reference values, which are usually measured values.\n\n    Parameters\n    ----------\n    aoi : numeric\n        The angle of incidence between the module normal vector and the\n        sun-beam vector [degrees].\n\n    theta_ref : numeric\n        Vector of angles at which the IAM is known [degrees].\n\n    iam_ref : numeric\n        IAM values for each angle in ``theta_ref`` [unitless].\n\n    method : str, default 'linear'\n        Specifies the interpolation method.\n        Useful options are: 'linear', 'quadratic', 'cubic'.\n        See scipy.interpolate.interp1d for more options.\n\n    normalize : boolean, default True\n        When true, the interpolated values are divided by the interpolated\n        value at zero degrees.  This ensures that ``iam=1.0`` at normal\n        incidence.\n\n    Returns\n    -------\n    iam : numeric\n        The incident angle modifier(s) [unitless]\n\n    Notes\n    -----\n    ``theta_ref`` must have two or more points and may span any range of\n    angles. Typically there will be a dozen or more points in the range 0-90\n    degrees. Beyond the range of ``theta_ref``, IAM values are extrapolated,\n    but constrained to be non-negative.\n\n    The sign of ``aoi`` is ignored; only the magnitude is used.\n\n    See Also\n    --------\n    pvlib.iam.physical\n    pvlib.iam.ashrae\n    pvlib.iam.martin_ruiz\n    pvlib.iam.sapm\n    '''\n    # Contributed by Anton Driesse (@adriesse), PV Performance Labs. July, 2019\n\n    from scipy.interpolate import interp1d\n\n    # Scipy doesn't give the clearest feedback, so check number of points here.\n    MIN_REF_VALS = {'linear': 2, 'quadratic': 3, 'cubic': 4, 1: 2, 2: 3, 3: 4}\n\n    if len(theta_ref) < MIN_REF_VALS.get(method, 2):\n        raise ValueError(\"Too few reference points defined \"\n                         \"for interpolation method '%s'.\" % method)\n\n    if np.any(np.less(iam_ref, 0)):\n        raise ValueError(\"Negative value(s) found in 'iam_ref'. \"\n                         \"This is not physically possible.\")\n\n    interpolator = interp1d(theta_ref, iam_ref, kind=method,\n                            fill_value='extrapolate')\n    aoi_input = aoi\n\n    aoi = np.asanyarray(aoi)\n    aoi = np.abs(aoi)\n    iam = interpolator(aoi)\n    iam = np.clip(iam, 0, None)\n\n    if normalize:\n        iam /= interpolator(0)\n\n    if isinstance(aoi_input, pd.Series):\n        iam = pd.Series(iam, index=aoi_input.index)\n\n    return iam\n\n\ndef sapm(aoi, module, upper=None):\n    r\"\"\"\n    Determine the incidence angle modifier (IAM) using the SAPM model.\n\n    Parameters\n    ----------\n    aoi : numeric\n        Angle of incidence in degrees. Negative input angles will return\n        zeros.\n\n    module : dict-like\n        A dict or Series with the SAPM IAM model parameters.\n        See the :py:func:`sapm` notes section for more details.\n\n    upper : None or float, default None\n        Upper limit on the results.\n\n    Returns\n    -------\n    iam : numeric\n        The SAPM angle of incidence loss coefficient, termed F2 in [1]_.\n\n    Notes\n    -----\n    The SAPM [1]_ traditionally does not define an upper limit on the AOI\n    loss function and values slightly exceeding 1 may exist for moderate\n    angles of incidence (15-40 degrees). However, users may consider\n    imposing an upper limit of 1.\n\n    References\n    ----------\n    .. [1] King, D. et al, 2004, \"Sandia Photovoltaic Array Performance\n       Model\", SAND Report 3535, Sandia National Laboratories, Albuquerque,\n       NM.\n\n    .. [2] B.H. King et al, \"Procedure to Determine Coefficients for the\n       Sandia Array Performance Model (SAPM),\" SAND2016-5284, Sandia\n       National Laboratories (2016).\n\n    .. [3] B.H. King et al, \"Recent Advancements in Outdoor Measurement\n       Techniques for Angle of Incidence Effects,\" 42nd IEEE PVSC (2015).\n       DOI: 10.1109/PVSC.2015.7355849\n\n    See Also\n    --------\n    pvlib.iam.physical\n    pvlib.iam.ashrae\n    pvlib.iam.martin_ruiz\n    pvlib.iam.interp\n    \"\"\"\n\n    aoi_coeff = [module['B5'], module['B4'], module['B3'], module['B2'],\n                 module['B1'], module['B0']]\n\n    iam = np.polyval(aoi_coeff, aoi)\n    iam = np.clip(iam, 0, upper)\n    # nan tolerant masking\n    aoi_lt_0 = np.full_like(aoi, False, dtype='bool')\n    np.less(aoi, 0, where=~np.isnan(aoi), out=aoi_lt_0)\n    iam = np.where(aoi_lt_0, 0, iam)\n\n    if isinstance(aoi, pd.Series):\n        iam = pd.Series(iam, aoi.index)\n\n    return iam\n\n\ndef marion_diffuse(model, surface_tilt, **kwargs):\n    \"\"\"\n    Determine diffuse irradiance incidence angle modifiers using Marion's\n    method of integrating over solid angle.\n\n    Parameters\n    ----------\n    model : str\n        The IAM function to evaluate across solid angle. Must be one of\n        `'ashrae', 'physical', 'martin_ruiz', 'sapm', 'schlick'`.\n\n    surface_tilt : numeric\n        Surface tilt angles in decimal degrees.\n        The tilt angle is defined as degrees from horizontal\n        (e.g. surface facing up = 0, surface facing horizon = 90).\n\n    **kwargs\n        Extra parameters passed to the IAM function.\n\n    Returns\n    -------\n    iam : dict\n        IAM values for each type of diffuse irradiance:\n\n            * 'sky': radiation from the sky dome (zenith <= 90)\n            * 'horizon': radiation from the region of the sky near the horizon\n              (89.5 <= zenith <= 90)\n            * 'ground': radiation reflected from the ground (zenith >= 90)\n\n        See [1]_ for a detailed description of each class.\n\n    See Also\n    --------\n    pvlib.iam.marion_integrate\n\n    References\n    ----------\n    .. [1] B. Marion \"Numerical method for angle-of-incidence correction\n       factors for diffuse radiation incident photovoltaic modules\",\n       Solar Energy, Volume 147, Pages 344-348. 2017.\n       DOI: 10.1016/j.solener.2017.03.027\n\n    Examples\n    --------\n    >>> marion_diffuse('physical', surface_tilt=20)\n    {'sky': 0.9539178294437575,\n     'horizon': 0.7652650139134007,\n     'ground': 0.6387140117795903}\n\n    >>> marion_diffuse('ashrae', [20, 30], b=0.04)\n    {'sky': array([0.96748999, 0.96938408]),\n     'horizon': array([0.86478428, 0.91825792]),\n     'ground': array([0.77004435, 0.8522436 ])}\n    \"\"\"\n\n    models = {\n        'physical': physical,\n        'ashrae': ashrae,\n        'sapm': sapm,\n        'martin_ruiz': martin_ruiz,\n        'schlick': schlick,\n    }\n\n    try:\n        iam_model = models[model]\n    except KeyError:\n        raise ValueError('model must be one of: ' + str(list(models.keys())))\n\n    iam_function = functools.partial(iam_model, **kwargs)\n    iam = {}\n    for region in ['sky', 'horizon', 'ground']:\n        iam[region] = marion_integrate(iam_function, surface_tilt, region)\n\n    return iam\n\n\ndef marion_integrate(function, surface_tilt, region, num=None):\n    \"\"\"\n    Integrate an incidence angle modifier (IAM) function over solid angle\n    to determine a diffuse irradiance correction factor using Marion's method.\n\n    This lower-level function actually performs the IAM integration for the\n    specified solid angle region.\n\n    Parameters\n    ----------\n    function : callable(aoi)\n        The IAM function to evaluate across solid angle. The function must\n        be vectorized and take only one parameter, the angle of incidence in\n        degrees.\n\n    surface_tilt : numeric\n        Surface tilt angles in decimal degrees.\n        The tilt angle is defined as degrees from horizontal\n        (e.g. surface facing up = 0, surface facing horizon = 90).\n\n    region : {'sky', 'horizon', 'ground'}\n        The region to integrate over. Must be one of:\n\n            * 'sky': radiation from the sky dome (zenith <= 90)\n            * 'horizon': radiation from the region of the sky near the horizon\n              (89.5 <= zenith <= 90)\n            * 'ground': radiation reflected from the ground (zenith >= 90)\n\n        See [1]_ for a detailed description of each class.\n\n    num : int, optional\n        The number of increments in the zenith integration.\n        If not specified, N will follow the values used in [1]_:\n\n            * 'sky' or 'ground': num = 180\n            * 'horizon': num = 1800\n\n    Returns\n    -------\n    iam : numeric\n        AOI diffuse correction factor for the specified region.\n\n    See Also\n    --------\n    pvlib.iam.marion_diffuse\n\n    References\n    ----------\n    .. [1] B. Marion \"Numerical method for angle-of-incidence correction\n       factors for diffuse radiation incident photovoltaic modules\",\n       Solar Energy, Volume 147, Pages 344-348. 2017.\n       DOI: 10.1016/j.solener.2017.03.027\n\n    Examples\n    --------\n    >>> marion_integrate(pvlib.iam.ashrae, 20, 'sky')\n    0.9596085829811408\n\n    >>> from functools import partial\n    >>> f = partial(pvlib.iam.physical, n=1.3)\n    >>> marion_integrate(f, [20, 30], 'sky')\n    array([0.96225034, 0.9653219 ])\n    \"\"\"\n\n    if num is None:\n        if region in ['sky', 'ground']:\n            num = 180\n        elif region == 'horizon':\n            num = 1800\n        else:\n            raise ValueError(f'Invalid region: {region}')\n\n    beta = np.radians(surface_tilt)\n    if isinstance(beta, pd.Series):\n        # convert Series to np array for broadcasting later\n        beta = beta.values\n    ai = np.pi/num  # angular increment\n\n    phi_range = np.linspace(0, np.pi, num, endpoint=False)\n    psi_range = np.linspace(0, 2*np.pi, 2*num, endpoint=False)\n\n    # the pseudocode in [1] do these checks at the end, but it's\n    # faster to do this criteria check up front instead of later.\n    if region == 'sky':\n        mask = phi_range + ai <= np.pi/2\n    elif region == 'horizon':\n        lo = 89.5 * np.pi/180\n        hi = np.pi/2\n        mask = (lo <= phi_range) & (phi_range + ai <= hi)\n    elif region == 'ground':\n        mask = (phi_range >= np.pi/2)\n    else:\n        raise ValueError(f'Invalid region: {region}')\n    phi_range = phi_range[mask]\n\n    # fast Cartesian product of phi and psi\n    angles = np.array(np.meshgrid(phi_range, psi_range)).T.reshape(-1, 2)\n    # index with single-element lists to maintain 2nd dimension so that\n    # these angle arrays broadcast across the beta array\n    phi_1 = angles[:, [0]]\n    psi_1 = angles[:, [1]]\n    phi_2 = phi_1 + ai\n    # psi_2 = psi_1 + ai  # not needed\n    phi_avg = phi_1 + 0.5*ai\n    psi_avg = psi_1 + 0.5*ai\n    term_1 = np.cos(beta) * np.cos(phi_avg)\n    # The AOI formula includes a term based on the difference between\n    # panel azimuth and the photon azimuth, but because we assume each class\n    # of diffuse irradiance is isotropic and we are integrating over all\n    # angles, it doesn't matter what panel azimuth we choose (i.e., the\n    # system is rotationally invariant).  So we choose gamma to be zero so\n    # that we can omit it from the cos(psi_avg) term.\n    # Marion's paper mentions this in the Section 3 pseudocode:\n    # \"set gamma to pi (or any value between 0 and 2pi)\"\n    term_2 = np.sin(beta) * np.sin(phi_avg) * np.cos(psi_avg)\n    cosaoi = term_1 + term_2\n    aoi = np.arccos(cosaoi)\n    # simplify Eq 8, (psi_2 - psi_1) is always ai\n    dAs = ai * (np.cos(phi_1) - np.cos(phi_2))\n    cosaoi_dAs = cosaoi * dAs\n    # apply the final AOI check, zeroing out non-passing points\n    mask = aoi < np.pi/2\n    cosaoi_dAs = np.where(mask, cosaoi_dAs, 0)\n    numerator = np.sum(function(np.degrees(aoi)) * cosaoi_dAs, axis=0)\n    denominator = np.sum(cosaoi_dAs, axis=0)\n\n    with np.errstate(invalid='ignore'):\n        # in some cases, no points pass the criteria\n        # (e.g. region='ground', surface_tilt=0), so we override the division\n        # by zero to set Fd=0.  Also, preserve nans in beta.\n        Fd = np.where((denominator != 0) | ~np.isfinite(beta),\n                      numerator / denominator,\n                      0)\n\n    # preserve input type\n    if np.isscalar(surface_tilt):\n        Fd = Fd.item()\n    elif isinstance(surface_tilt, pd.Series):\n        Fd = pd.Series(Fd, surface_tilt.index)\n\n    return Fd\n\n\ndef schlick(aoi):\n    \"\"\"\n    Determine incidence angle modifier (IAM) for direct irradiance using the\n    Schlick approximation to the Fresnel equations.\n\n    The Schlick approximation was proposed in [1]_ as a computationally\n    efficient alternative to computing the Fresnel factor in computer\n    graphics contexts.  This implementation is a normalized form of the\n    equation in [1]_ so that it can be used as a PV IAM model.\n    Unlike other IAM models, this model has no ability to describe\n    different reflection profiles.\n\n    In PV contexts, the Schlick approximation has been used as an analytically\n    integrable alternative to the Fresnel equations for estimating IAM\n    for diffuse irradiance [2]_.\n\n    Parameters\n    ----------\n    aoi : numeric\n        The angle of incidence (AOI) between the module normal vector and the\n        sun-beam vector. Angles of nan will result in nan. [degrees]\n\n    Returns\n    -------\n    iam : numeric\n        The incident angle modifier.\n\n    References\n    ----------\n    .. [1] Schlick, C. An inexpensive BRDF model for physically-based\n       rendering. Computer graphics forum 13 (1994).\n\n    .. [2] Xie, Y., M. Sengupta, A. Habte, A. Andreas, \"The 'Fresnel Equations'\n       for Diffuse radiation on Inclined photovoltaic Surfaces (FEDIS)\",\n       Renewable and Sustainable Energy Reviews, vol. 161, 112362. June 2022.\n       :doi:`10.1016/j.rser.2022.112362`\n\n    See Also\n    --------\n    pvlib.iam.schlick_diffuse\n    \"\"\"\n    iam = 1 - (1 - cosd(aoi)) ** 5\n    iam = np.where(np.abs(aoi) >= 90.0, 0.0, iam)\n\n    # preserve input type\n    if np.isscalar(aoi):\n        iam = iam.item()\n    elif isinstance(aoi, pd.Series):\n        iam = pd.Series(iam, aoi.index)\n\n    return iam\n\n\ndef schlick_diffuse(surface_tilt):\n    \"\"\"\n    Determine the incidence angle modifiers (IAM) for diffuse sky and\n    ground-reflected irradiance on a tilted surface using the Schlick\n    incident angle model.\n\n    The diffuse iam values are calculated using an analytical integration\n    of the Schlick equation [1]_ over the portion of an isotropic sky and\n    isotropic foreground that is visible from the tilted surface [2]_.\n\n    Parameters\n    ----------\n    surface_tilt : numeric\n        Surface tilt angle measured from horizontal (e.g. surface facing\n        up = 0, surface facing horizon = 90). [degrees]\n\n    Returns\n    -------\n    iam_sky : numeric\n        The incident angle modifier for sky diffuse.\n\n    iam_ground : numeric\n        The incident angle modifier for ground-reflected diffuse.\n\n    References\n    ----------\n    .. [1] Schlick, C. An inexpensive BRDF model for physically-based\n       rendering. Computer graphics forum 13 (1994).\n\n    .. [2] Xie, Y., M. Sengupta, A. Habte, A. Andreas, \"The 'Fresnel Equations'\n       for Diffuse radiation on Inclined photovoltaic Surfaces (FEDIS)\",\n       Renewable and Sustainable Energy Reviews, vol. 161, 112362. June 2022.\n       :doi:`10.1016/j.rser.2022.112362`\n\n    See Also\n    --------\n    pvlib.iam.schlick\n    \"\"\"\n    # these calculations are as in [2]_, but with the refractive index\n    # weighting coefficient w set to 1.0 (so it is omitted)\n\n    # relative transmittance of sky diffuse radiation by PV cover:\n    cosB = cosd(surface_tilt)\n    sinB = sind(surface_tilt)\n    cuk = (2 / (np.pi * (1 + cosB))) * (\n        (30/7)*np.pi - (160/21)*np.radians(surface_tilt) - (10/3)*np.pi*cosB\n        + (160/21)*cosB*sinB - (5/3)*np.pi*cosB*sinB**2 + (20/7)*cosB*sinB**3\n        - (5/16)*np.pi*cosB*sinB**4 + (16/105)*cosB*sinB**5\n    )  # Eq 4 in [2]\n\n    # relative transmittance of ground-reflected radiation by PV cover:\n    with np.errstate(divide='ignore', invalid='ignore'):  # Eq 6 in [2]\n        cug = 40 / (21 * (1 - cosB)) - (1 + cosB) / (1 - cosB) * cuk\n\n    cug = np.where(surface_tilt < 1e-6, 0, cug)\n\n    # respect input types:\n    if np.isscalar(surface_tilt):\n        cuk = cuk.item()\n        cug = cug.item()\n    elif isinstance(surface_tilt, pd.Series):\n        cuk = pd.Series(cuk, surface_tilt.index)\n        cug = pd.Series(cug, surface_tilt.index)\n\n    return cuk, cug\n","new_file_content":"r\"\"\"\nThe ``iam`` module contains functions that implement models for the incidence\nangle modifier (IAM). The IAM quantifies the fraction of direct irradiance on\na module's front surface that is transmitted through the module materials to\nthe cells. Stated differently, the quantity 1 - IAM is the fraction of direct\nirradiance that is reflected away or absorbed by the module's front materials.\nIAM is typically a function of the angle of incidence (AOI) of the direct\nirradiance to the module's surface.\n\"\"\"\n\nimport numpy as np\nimport pandas as pd\nimport functools\nfrom pvlib.tools import cosd, sind\n\n# a dict of required parameter names for each IAM model\n# keys are the function names for the IAM models\n_IAM_MODEL_PARAMS = {\n    'ashrae': {'b'},\n    'physical': {'n', 'K', 'L'},\n    'martin_ruiz': {'a_r'},\n    'sapm': {'B0', 'B1', 'B2', 'B3', 'B4', 'B5'},\n    'interp': set()\n}\n\n\ndef ashrae(aoi, b=0.05):\n    r\"\"\"\n    Determine the incidence angle modifier using the ASHRAE transmission\n    model.\n\n    The ASHRAE (American Society of Heating, Refrigeration, and Air\n    Conditioning Engineers) transmission model is developed in\n    [1]_, and in [2]_. The model has been used in software such as PVSyst [3]_.\n\n    Parameters\n    ----------\n    aoi : numeric\n        The angle of incidence (AOI) between the module normal vector and the\n        sun-beam vector in degrees. Angles of nan will result in nan.\n\n    b : float, default 0.05\n        A parameter to adjust the incidence angle modifier as a function of\n        angle of incidence. Typical values are on the order of 0.05 [3].\n\n    Returns\n    -------\n    iam : numeric\n        The incident angle modifier (IAM). Returns zero for all abs(aoi) >= 90\n        and for all ``iam`` values that would be less than 0.\n\n    Notes\n    -----\n    The incidence angle modifier is calculated as\n\n    .. math::\n\n        IAM = 1 - b (\\sec(aoi) - 1)\n\n    As AOI approaches 90 degrees, the model yields negative values for IAM;\n    negative IAM values are set to zero in this implementation.\n\n    References\n    ----------\n    .. [1] Souka A.F., Safwat H.H., \"Determination of the optimum\n       orientations for the double exposure flat-plate collector and its\n       reflections\". Solar Energy vol .10, pp 170-174. 1966.\n\n    .. [2] ASHRAE standard 93-77\n\n    .. [3] PVsyst Contextual Help.\n       https://files.pvsyst.com/help/index.html?iam_loss.htm retrieved on\n       October 14, 2019\n\n    See Also\n    --------\n    pvlib.iam.physical\n    pvlib.iam.martin_ruiz\n    pvlib.iam.interp\n    \"\"\"\n\n    iam = 1 - b * (1 / np.cos(np.radians(aoi)) - 1)\n    aoi_gte_90 = np.full_like(aoi, False, dtype='bool')\n    np.greater_equal(np.abs(aoi), 90, where=~np.isnan(aoi), out=aoi_gte_90)\n    iam = np.where(aoi_gte_90, 0, iam)\n    iam = np.maximum(0, iam)\n\n    if isinstance(aoi, pd.Series):\n        iam = pd.Series(iam, index=aoi.index)\n\n    return iam\n\n\ndef physical(aoi, n=1.526, K=4.0, L=0.002, *, n_ar=None):\n    r\"\"\"\n    Determine the incidence angle modifier using refractive index ``n``,\n    extinction coefficient ``K``, glazing thickness ``L`` and refractive\n    index ``n_ar`` of an optional anti-reflective coating.\n\n    ``iam.physical`` calculates the incidence angle modifier as described in\n    [1]_, Section 3, with additional support of an anti-reflective coating.\n    The calculation is based on a physical model of reflections, absorption,\n    and transmission through a transparent cover.\n\n    Parameters\n    ----------\n    aoi : numeric\n        The angle of incidence between the module normal vector and the\n        sun-beam vector in degrees. Angles of nan will result in nan.\n\n    n : numeric, default 1.526\n        The effective index of refraction (unitless). Reference [1]_\n        indicates that a value of 1.526 is acceptable for glass.\n\n    K : numeric, default 4.0\n        The glazing extinction coefficient in units of 1/meters.\n        Reference [1] indicates that a value of 4 is reasonable for\n        \"water white\" glass.\n\n    L : numeric, default 0.002\n        The glazing thickness in units of meters. Reference [1]_\n        indicates that 0.002 meters (2 mm) is reasonable for most\n        glass-covered PV panels.\n\n    n_ar : numeric, optional\n        The effective index of refraction of the anti-reflective (AR) coating\n        (unitless). If n_ar is None (default), no AR coating is applied.\n        A typical value for the effective index of an AR coating is 1.29.\n\n    Returns\n    -------\n    iam : numeric\n        The incident angle modifier\n\n    Notes\n    -----\n    The pvlib python authors believe that Eqn. 14 in [1]_ is\n    incorrect, which presents :math:`\\theta_{r} = \\arcsin(n \\sin(AOI))`.\n    Here, :math:`\\theta_{r} = \\arcsin(1/n \\times \\sin(AOI))`\n\n    References\n    ----------\n    .. [1] W. De Soto et al., \"Improvement and validation of a model for\n       photovoltaic array performance\", Solar Energy, vol 80, pp. 78-88,\n       2006.\n\n    .. [2] Duffie, John A. & Beckman, William A.. (2006). Solar Engineering\n       of Thermal Processes, third edition. [Books24x7 version] Available\n       from http://common.books24x7.com/toc.aspx?bookid=17160.\n\n    See Also\n    --------\n    pvlib.iam.martin_ruiz\n    pvlib.iam.ashrae\n    pvlib.iam.interp\n    pvlib.iam.sapm\n    \"\"\"\n    n1, n3 = 1, n\n    if n_ar is None or np.allclose(n_ar, n1):\n        # no AR coating\n        n2 = n\n    else:\n        n2 = n_ar\n\n    # incidence angle\n    costheta = np.maximum(0, cosd(aoi))  # always >= 0\n    sintheta = np.sqrt(1 - costheta**2)  # always >= 0\n    n1costheta1 = n1 * costheta\n    n2costheta1 = n2 * costheta\n\n    # refraction angle of first interface\n    sintheta = n1 / n2 * sintheta\n    costheta = np.sqrt(1 - sintheta**2)\n    n1costheta2 = n1 * costheta\n    n2costheta2 = n2 * costheta\n\n    # reflectance of s-, p-polarized, and normal light by the first interface\n    with np.errstate(divide='ignore', invalid='ignore'):\n        rho12_s = \\\n            ((n1costheta1 - n2costheta2) / (n1costheta1 + n2costheta2)) ** 2\n        rho12_p = \\\n            ((n1costheta2 - n2costheta1) / (n1costheta2 + n2costheta1)) ** 2\n\n    rho12_0 = ((n1 - n2) / (n1 + n2)) ** 2\n\n    # transmittance through the first interface\n    tau_s = 1 - rho12_s\n    tau_p = 1 - rho12_p\n    tau_0 = 1 - rho12_0\n\n    if not np.allclose(n3, n2):  # AR coated glass\n        n3costheta2 = n3 * costheta\n        # refraction angle of second interface\n        sintheta = n2 / n3 * sintheta\n        costheta = np.sqrt(1 - sintheta**2)\n        n2costheta3 = n2 * costheta\n        n3costheta3 = n3 * costheta\n\n        # reflectance by the second interface\n        rho23_s = (\n            (n2costheta2 - n3costheta3) / (n2costheta2 + n3costheta3)\n        ) ** 2\n        rho23_p = (\n            (n2costheta3 - n3costheta2) / (n2costheta3 + n3costheta2)\n        ) ** 2\n        rho23_0 = ((n2 - n3) / (n2 + n3)) ** 2\n\n        # transmittance through the coating, including internal reflections\n        # 1 + rho23*rho12 + (rho23*rho12)^2 + ... = 1/(1 - rho23*rho12)\n        tau_s *= (1 - rho23_s) / (1 - rho23_s * rho12_s)\n        tau_p *= (1 - rho23_p) / (1 - rho23_p * rho12_p)\n        tau_0 *= (1 - rho23_0) / (1 - rho23_0 * rho12_0)\n\n    # transmittance after absorption in the glass\n    with np.errstate(divide='ignore', invalid='ignore'):\n        tau_s *= np.exp(-K * L / costheta)\n        tau_p *= np.exp(-K * L / costheta)\n\n    tau_0 *= np.exp(-K * L)\n\n    # incidence angle modifier\n    iam = (tau_s + tau_p) / 2 / tau_0\n\n    # for light coming from behind the plane, none can enter the module\n    # when n2 > 1, this is already the case\n    if np.isclose(n2, 1).any():\n        iam = np.where(aoi >= 90, 0, iam)\n        if isinstance(aoi, pd.Series):\n            iam = pd.Series(iam, index=aoi.index)\n\n    return iam\n\n\ndef martin_ruiz(aoi, a_r=0.16):\n    r'''\n    Determine the incidence angle modifier (IAM) using the Martin\n    and Ruiz incident angle model.\n\n    Parameters\n    ----------\n    aoi : numeric, degrees\n        The angle of incidence between the module normal vector and the\n        sun-beam vector in degrees.\n\n    a_r : numeric\n        The angular losses coefficient described in equation 3 of [1]_.\n        This is an empirical dimensionless parameter. Values of ``a_r`` are\n        generally on the order of 0.08 to 0.25 for flat-plate PV modules.\n\n    Returns\n    -------\n    iam : numeric\n        The incident angle modifier(s)\n\n    Notes\n    -----\n    `martin_ruiz` calculates the incidence angle modifier (IAM) as described in\n    [1]_. The information required is the incident angle (AOI) and the angular\n    losses coefficient (a_r). Note that [1]_ has a corrigendum [2]_ which\n    clarifies a mix-up of 'alpha's and 'a's in the former.\n\n    The incident angle modifier is defined as\n\n    .. math::\n\n       IAM = \\frac{1 - \\exp(-\\frac{\\cos(aoi)}{a_r})}\n       {1 - \\exp(\\frac{-1}{a_r})}\n\n    which is presented as :math:`AL(\\alpha) = 1 - IAM` in equation 4 of [1]_,\n    with :math:`\\alpha` representing the angle of incidence AOI. Thus IAM = 1\n    at AOI = 0, and IAM = 0 at AOI = 90.  This equation is only valid for\n    -90 <= aoi <= 90, therefore `iam` is constrained to 0.0 outside this\n    interval.\n\n    References\n    ----------\n    .. [1] N. Martin and J. M. Ruiz, \"Calculation of the PV modules angular\n       losses under field conditions by means of an analytical model\", Solar\n       Energy Materials & Solar Cells, vol. 70, pp. 25-38, 2001.\n\n    .. [2] N. Martin and J. M. Ruiz, \"Corrigendum to 'Calculation of the PV\n       modules angular losses under field conditions by means of an\n       analytical model'\", Solar Energy Materials & Solar Cells, vol. 110,\n       pp. 154, 2013.\n\n    See Also\n    --------\n    pvlib.iam.martin_ruiz_diffuse\n    pvlib.iam.physical\n    pvlib.iam.ashrae\n    pvlib.iam.interp\n    pvlib.iam.sapm\n    '''\n    # Contributed by Anton Driesse (@adriesse), PV Performance Labs. July, 2019\n\n    aoi_input = aoi\n\n    aoi = np.asanyarray(aoi)\n    a_r = np.asanyarray(a_r)\n\n    if np.any(np.less_equal(a_r, 0)):\n        raise ValueError(\"The parameter 'a_r' cannot be zero or negative.\")\n\n    with np.errstate(invalid='ignore'):\n        iam = (1 - np.exp(-cosd(aoi) / a_r)) / (1 - np.exp(-1 / a_r))\n        iam = np.where(np.abs(aoi) >= 90.0, 0.0, iam)\n\n    if isinstance(aoi_input, pd.Series):\n        iam = pd.Series(iam, index=aoi_input.index)\n\n    return iam\n\n\ndef martin_ruiz_diffuse(surface_tilt, a_r=0.16, c1=0.4244, c2=None):\n    '''\n    Determine the incidence angle modifiers (iam) for diffuse sky and\n    ground-reflected irradiance using the Martin and Ruiz incident angle model.\n\n    Parameters\n    ----------\n    surface_tilt: float or array-like, default 0\n        Surface tilt angles in decimal degrees.\n        The tilt angle is defined as degrees from horizontal\n        (e.g. surface facing up = 0, surface facing horizon = 90)\n        surface_tilt must be in the range [0, 180]\n\n    a_r : numeric\n        The angular losses coefficient described in equation 3 of [1]_.\n        This is an empirical dimensionless parameter. Values of a_r are\n        generally on the order of 0.08 to 0.25 for flat-plate PV modules.\n        a_r must be greater than zero.\n\n    c1 : float\n        First fitting parameter for the expressions that approximate the\n        integral of diffuse irradiance coming from different directions.\n        c1 is given as the constant 4 / 3 / pi (0.4244) in [1]_.\n\n    c2 : float\n        Second fitting parameter for the expressions that approximate the\n        integral of diffuse irradiance coming from different directions.\n        If c2 is None, it will be calculated according to the linear\n        relationship given in [3]_.\n\n    Returns\n    -------\n    iam_sky : numeric\n        The incident angle modifier for sky diffuse\n\n    iam_ground : numeric\n        The incident angle modifier for ground-reflected diffuse\n\n    Notes\n    -----\n    Sky and ground modifiers are complementary: iam_sky for tilt = 30 is\n    equal to iam_ground for tilt = 180 - 30.  For vertical surfaces,\n    tilt = 90, the two factors are equal.\n\n    References\n    ----------\n    .. [1] N. Martin and J. M. Ruiz, \"Calculation of the PV modules angular\n       losses under field conditions by means of an analytical model\", Solar\n       Energy Materials & Solar Cells, vol. 70, pp. 25-38, 2001.\n\n    .. [2] N. Martin and J. M. Ruiz, \"Corrigendum to 'Calculation of the PV\n       modules angular losses under field conditions by means of an\n       analytical model'\", Solar Energy Materials & Solar Cells, vol. 110,\n       pp. 154, 2013.\n\n    .. [3] \"IEC 61853-3 Photovoltaic (PV) module performance testing and energy\n       rating - Part 3: Energy rating of PV modules\". IEC, Geneva, 2018.\n\n    See Also\n    --------\n    pvlib.iam.martin_ruiz\n    pvlib.iam.physical\n    pvlib.iam.ashrae\n    pvlib.iam.interp\n    pvlib.iam.sapm\n    '''\n    # Contributed by Anton Driesse (@adriesse), PV Performance Labs. Oct. 2019\n\n    if isinstance(surface_tilt, pd.Series):\n        out_index = surface_tilt.index\n    else:\n        out_index = None\n\n    surface_tilt = np.asanyarray(surface_tilt)\n\n    # avoid undefined results for horizontal or upside-down surfaces\n    zeroang = 1e-06\n\n    surface_tilt = np.where(surface_tilt == 0, zeroang, surface_tilt)\n    surface_tilt = np.where(surface_tilt == 180, 180 - zeroang, surface_tilt)\n\n    if c2 is None:\n        # This equation is from [3] Sect. 7.2\n        c2 = 0.5 * a_r - 0.154\n\n    beta = np.radians(surface_tilt)\n    sin = np.sin\n    pi = np.pi\n    cos = np.cos\n\n    # avoid RuntimeWarnings for <, sin, and cos with nan\n    with np.errstate(invalid='ignore'):\n        # because sin(pi) isn't exactly zero\n        sin_beta = np.where(surface_tilt < 90, sin(beta), sin(pi - beta))\n\n        trig_term_sky = sin_beta + (pi - beta - sin_beta) / (1 + cos(beta))\n        trig_term_gnd = sin_beta +      (beta - sin_beta) / (1 - cos(beta))  # noqa: E222 E261 E501\n\n    iam_sky = 1 - np.exp(-(c1 + c2 * trig_term_sky) * trig_term_sky / a_r)\n    iam_gnd = 1 - np.exp(-(c1 + c2 * trig_term_gnd) * trig_term_gnd / a_r)\n\n    if out_index is not None:\n        iam_sky = pd.Series(iam_sky, index=out_index, name='iam_sky')\n        iam_gnd = pd.Series(iam_gnd, index=out_index, name='iam_ground')\n\n    return iam_sky, iam_gnd\n\n\ndef interp(aoi, theta_ref, iam_ref, method='linear', normalize=True):\n    r'''\n    Determine the incidence angle modifier (IAM) by interpolating a set of\n    reference values, which are usually measured values.\n\n    Parameters\n    ----------\n    aoi : numeric\n        The angle of incidence between the module normal vector and the\n        sun-beam vector [degrees].\n\n    theta_ref : numeric\n        Vector of angles at which the IAM is known [degrees].\n\n    iam_ref : numeric\n        IAM values for each angle in ``theta_ref`` [unitless].\n\n    method : str, default 'linear'\n        Specifies the interpolation method.\n        Useful options are: 'linear', 'quadratic', 'cubic'.\n        See scipy.interpolate.interp1d for more options.\n\n    normalize : boolean, default True\n        When true, the interpolated values are divided by the interpolated\n        value at zero degrees.  This ensures that ``iam=1.0`` at normal\n        incidence.\n\n    Returns\n    -------\n    iam : numeric\n        The incident angle modifier(s) [unitless]\n\n    Notes\n    -----\n    ``theta_ref`` must have two or more points and may span any range of\n    angles. Typically there will be a dozen or more points in the range 0-90\n    degrees. Beyond the range of ``theta_ref``, IAM values are extrapolated,\n    but constrained to be non-negative.\n\n    The sign of ``aoi`` is ignored; only the magnitude is used.\n\n    See Also\n    --------\n    pvlib.iam.physical\n    pvlib.iam.ashrae\n    pvlib.iam.martin_ruiz\n    pvlib.iam.sapm\n    '''\n    # Contributed by Anton Driesse (@adriesse), PV Performance Labs. July, 2019\n\n    from scipy.interpolate import interp1d\n\n    # Scipy doesn't give the clearest feedback, so check number of points here.\n    MIN_REF_VALS = {'linear': 2, 'quadratic': 3, 'cubic': 4, 1: 2, 2: 3, 3: 4}\n\n    if len(theta_ref) < MIN_REF_VALS.get(method, 2):\n        raise ValueError(\"Too few reference points defined \"\n                         \"for interpolation method '%s'.\" % method)\n\n    if np.any(np.less(iam_ref, 0)):\n        raise ValueError(\"Negative value(s) found in 'iam_ref'. \"\n                         \"This is not physically possible.\")\n\n    interpolator = interp1d(theta_ref, iam_ref, kind=method,\n                            fill_value='extrapolate')\n    aoi_input = aoi\n\n    aoi = np.asanyarray(aoi)\n    aoi = np.abs(aoi)\n    iam = interpolator(aoi)\n    iam = np.clip(iam, 0, None)\n\n    if normalize:\n        iam /= interpolator(0)\n\n    if isinstance(aoi_input, pd.Series):\n        iam = pd.Series(iam, index=aoi_input.index)\n\n    return iam\n\n\ndef sapm(aoi, module, upper=None):\n    r\"\"\"\n    Determine the incidence angle modifier (IAM) using the SAPM model.\n\n    Parameters\n    ----------\n    aoi : numeric\n        Angle of incidence in degrees. Negative input angles will return\n        zeros.\n\n    module : dict-like\n        A dict or Series with the SAPM IAM model parameters.\n        See the :py:func:`sapm` notes section for more details.\n\n    upper : None or float, default None\n        Upper limit on the results.\n\n    Returns\n    -------\n    iam : numeric\n        The SAPM angle of incidence loss coefficient, termed F2 in [1]_.\n\n    Notes\n    -----\n    The SAPM [1]_ traditionally does not define an upper limit on the AOI\n    loss function and values slightly exceeding 1 may exist for moderate\n    angles of incidence (15-40 degrees). However, users may consider\n    imposing an upper limit of 1.\n\n    References\n    ----------\n    .. [1] King, D. et al, 2004, \"Sandia Photovoltaic Array Performance\n       Model\", SAND Report 3535, Sandia National Laboratories, Albuquerque,\n       NM.\n\n    .. [2] B.H. King et al, \"Procedure to Determine Coefficients for the\n       Sandia Array Performance Model (SAPM),\" SAND2016-5284, Sandia\n       National Laboratories (2016).\n\n    .. [3] B.H. King et al, \"Recent Advancements in Outdoor Measurement\n       Techniques for Angle of Incidence Effects,\" 42nd IEEE PVSC (2015).\n       DOI: 10.1109/PVSC.2015.7355849\n\n    See Also\n    --------\n    pvlib.iam.physical\n    pvlib.iam.ashrae\n    pvlib.iam.martin_ruiz\n    pvlib.iam.interp\n    \"\"\"\n\n    aoi_coeff = [module['B5'], module['B4'], module['B3'], module['B2'],\n                 module['B1'], module['B0']]\n\n    iam = np.polyval(aoi_coeff, aoi)\n    iam = np.clip(iam, 0, upper)\n    # nan tolerant masking\n    aoi_lt_0 = np.full_like(aoi, False, dtype='bool')\n    np.less(aoi, 0, where=~np.isnan(aoi), out=aoi_lt_0)\n    iam = np.where(aoi_lt_0, 0, iam)\n\n    if isinstance(aoi, pd.Series):\n        iam = pd.Series(iam, aoi.index)\n\n    return iam\n\n\ndef marion_diffuse(model, surface_tilt, **kwargs):\n    \"\"\"\n    Determine diffuse irradiance incidence angle modifiers using Marion's\n    method of integrating over solid angle.\n\n    Parameters\n    ----------\n    model : str\n        The IAM function to evaluate across solid angle. Must be one of\n        `'ashrae', 'physical', 'martin_ruiz', 'sapm', 'schlick'`.\n\n    surface_tilt : numeric\n        Surface tilt angles in decimal degrees.\n        The tilt angle is defined as degrees from horizontal\n        (e.g. surface facing up = 0, surface facing horizon = 90).\n\n    **kwargs\n        Extra parameters passed to the IAM function.\n\n    Returns\n    -------\n    iam : dict\n        IAM values for each type of diffuse irradiance:\n\n            * 'sky': radiation from the sky dome (zenith <= 90)\n            * 'horizon': radiation from the region of the sky near the horizon\n              (89.5 <= zenith <= 90)\n            * 'ground': radiation reflected from the ground (zenith >= 90)\n\n        See [1]_ for a detailed description of each class.\n\n    See Also\n    --------\n    pvlib.iam.marion_integrate\n\n    References\n    ----------\n    .. [1] B. Marion \"Numerical method for angle-of-incidence correction\n       factors for diffuse radiation incident photovoltaic modules\",\n       Solar Energy, Volume 147, Pages 344-348. 2017.\n       DOI: 10.1016/j.solener.2017.03.027\n\n    Examples\n    --------\n    >>> marion_diffuse('physical', surface_tilt=20)\n    {'sky': 0.9539178294437575,\n     'horizon': 0.7652650139134007,\n     'ground': 0.6387140117795903}\n\n    >>> marion_diffuse('ashrae', [20, 30], b=0.04)\n    {'sky': array([0.96748999, 0.96938408]),\n     'horizon': array([0.86478428, 0.91825792]),\n     'ground': array([0.77004435, 0.8522436 ])}\n    \"\"\"\n\n    models = {\n        'physical': physical,\n        'ashrae': ashrae,\n        'sapm': sapm,\n        'martin_ruiz': martin_ruiz,\n        'schlick': schlick,\n    }\n\n    try:\n        iam_model = models[model]\n    except KeyError:\n        raise ValueError('model must be one of: ' + str(list(models.keys())))\n\n    iam_function = functools.partial(iam_model, **kwargs)\n    iam = {}\n    for region in ['sky', 'horizon', 'ground']:\n        iam[region] = marion_integrate(iam_function, surface_tilt, region)\n\n    return iam\n\n\ndef marion_integrate(function, surface_tilt, region, num=None):\n    \"\"\"\n    Integrate an incidence angle modifier (IAM) function over solid angle\n    to determine a diffuse irradiance correction factor using Marion's method.\n\n    This lower-level function actually performs the IAM integration for the\n    specified solid angle region.\n\n    Parameters\n    ----------\n    function : callable(aoi)\n        The IAM function to evaluate across solid angle. The function must\n        be vectorized and take only one parameter, the angle of incidence in\n        degrees.\n\n    surface_tilt : numeric\n        Surface tilt angles in decimal degrees.\n        The tilt angle is defined as degrees from horizontal\n        (e.g. surface facing up = 0, surface facing horizon = 90).\n\n    region : {'sky', 'horizon', 'ground'}\n        The region to integrate over. Must be one of:\n\n            * 'sky': radiation from the sky dome (zenith <= 90)\n            * 'horizon': radiation from the region of the sky near the horizon\n              (89.5 <= zenith <= 90)\n            * 'ground': radiation reflected from the ground (zenith >= 90)\n\n        See [1]_ for a detailed description of each class.\n\n    num : int, optional\n        The number of increments in the zenith integration.\n        If not specified, N will follow the values used in [1]_:\n\n            * 'sky' or 'ground': num = 180\n            * 'horizon': num = 1800\n\n    Returns\n    -------\n    iam : numeric\n        AOI diffuse correction factor for the specified region.\n\n    See Also\n    --------\n    pvlib.iam.marion_diffuse\n\n    References\n    ----------\n    .. [1] B. Marion \"Numerical method for angle-of-incidence correction\n       factors for diffuse radiation incident photovoltaic modules\",\n       Solar Energy, Volume 147, Pages 344-348. 2017.\n       DOI: 10.1016/j.solener.2017.03.027\n\n    Examples\n    --------\n    >>> marion_integrate(pvlib.iam.ashrae, 20, 'sky')\n    0.9596085829811408\n\n    >>> from functools import partial\n    >>> f = partial(pvlib.iam.physical, n=1.3)\n    >>> marion_integrate(f, [20, 30], 'sky')\n    array([0.96225034, 0.9653219 ])\n    \"\"\"\n\n    if num is None:\n        if region in ['sky', 'ground']:\n            num = 180\n        elif region == 'horizon':\n            num = 1800\n        else:\n            raise ValueError(f'Invalid region: {region}')\n\n    beta = np.radians(surface_tilt)\n    if isinstance(beta, pd.Series):\n        # convert Series to np array for broadcasting later\n        beta = beta.values\n    ai = np.pi/num  # angular increment\n\n    phi_range = np.linspace(0, np.pi, num, endpoint=False)\n    psi_range = np.linspace(0, 2*np.pi, 2*num, endpoint=False)\n\n    # the pseudocode in [1] do these checks at the end, but it's\n    # faster to do this criteria check up front instead of later.\n    if region == 'sky':\n        mask = phi_range + ai <= np.pi/2\n    elif region == 'horizon':\n        lo = 89.5 * np.pi/180\n        hi = np.pi/2\n        mask = (lo <= phi_range) & (phi_range + ai <= hi)\n    elif region == 'ground':\n        mask = (phi_range >= np.pi/2)\n    else:\n        raise ValueError(f'Invalid region: {region}')\n    phi_range = phi_range[mask]\n\n    # fast Cartesian product of phi and psi\n    angles = np.array(np.meshgrid(phi_range, psi_range)).T.reshape(-1, 2)\n    # index with single-element lists to maintain 2nd dimension so that\n    # these angle arrays broadcast across the beta array\n    phi_1 = angles[:, [0]]\n    psi_1 = angles[:, [1]]\n    phi_2 = phi_1 + ai\n    # psi_2 = psi_1 + ai  # not needed\n    phi_avg = phi_1 + 0.5*ai\n    psi_avg = psi_1 + 0.5*ai\n    term_1 = np.cos(beta) * np.cos(phi_avg)\n    # The AOI formula includes a term based on the difference between\n    # panel azimuth and the photon azimuth, but because we assume each class\n    # of diffuse irradiance is isotropic and we are integrating over all\n    # angles, it doesn't matter what panel azimuth we choose (i.e., the\n    # system is rotationally invariant).  So we choose gamma to be zero so\n    # that we can omit it from the cos(psi_avg) term.\n    # Marion's paper mentions this in the Section 3 pseudocode:\n    # \"set gamma to pi (or any value between 0 and 2pi)\"\n    term_2 = np.sin(beta) * np.sin(phi_avg) * np.cos(psi_avg)\n    cosaoi = term_1 + term_2\n    aoi = np.arccos(cosaoi)\n    # simplify Eq 8, (psi_2 - psi_1) is always ai\n    dAs = ai * (np.cos(phi_1) - np.cos(phi_2))\n    cosaoi_dAs = cosaoi * dAs\n    # apply the final AOI check, zeroing out non-passing points\n    mask = aoi < np.pi/2\n    cosaoi_dAs = np.where(mask, cosaoi_dAs, 0)\n    numerator = np.sum(function(np.degrees(aoi)) * cosaoi_dAs, axis=0)\n    denominator = np.sum(cosaoi_dAs, axis=0)\n\n    with np.errstate(invalid='ignore'):\n        # in some cases, no points pass the criteria\n        # (e.g. region='ground', surface_tilt=0), so we override the division\n        # by zero to set Fd=0.  Also, preserve nans in beta.\n        Fd = np.where((denominator != 0) | ~np.isfinite(beta),\n                      numerator / denominator,\n                      0)\n\n    # preserve input type\n    if np.isscalar(surface_tilt):\n        Fd = Fd.item()\n    elif isinstance(surface_tilt, pd.Series):\n        Fd = pd.Series(Fd, surface_tilt.index)\n\n    return Fd\n\n\ndef schlick(aoi):\n    \"\"\"\n    Determine incidence angle modifier (IAM) for direct irradiance using the\n    Schlick approximation to the Fresnel equations.\n\n    The Schlick approximation was proposed in [1]_ as a computationally\n    efficient alternative to computing the Fresnel factor in computer\n    graphics contexts.  This implementation is a normalized form of the\n    equation in [1]_ so that it can be used as a PV IAM model.\n    Unlike other IAM models, this model has no ability to describe\n    different reflection profiles.\n\n    In PV contexts, the Schlick approximation has been used as an analytically\n    integrable alternative to the Fresnel equations for estimating IAM\n    for diffuse irradiance [2]_.\n\n    Parameters\n    ----------\n    aoi : numeric\n        The angle of incidence (AOI) between the module normal vector and the\n        sun-beam vector. Angles of nan will result in nan. [degrees]\n\n    Returns\n    -------\n    iam : numeric\n        The incident angle modifier.\n\n    References\n    ----------\n    .. [1] Schlick, C. An inexpensive BRDF model for physically-based\n       rendering. Computer graphics forum 13 (1994).\n\n    .. [2] Xie, Y., M. Sengupta, A. Habte, A. Andreas, \"The 'Fresnel Equations'\n       for Diffuse radiation on Inclined photovoltaic Surfaces (FEDIS)\",\n       Renewable and Sustainable Energy Reviews, vol. 161, 112362. June 2022.\n       :doi:`10.1016/j.rser.2022.112362`\n\n    See Also\n    --------\n    pvlib.iam.schlick_diffuse\n    \"\"\"\n    iam = 1 - (1 - cosd(aoi)) ** 5\n    iam = np.where(np.abs(aoi) >= 90.0, 0.0, iam)\n\n    # preserve input type\n    if np.isscalar(aoi):\n        iam = iam.item()\n    elif isinstance(aoi, pd.Series):\n        iam = pd.Series(iam, aoi.index)\n\n    return iam\n\n\ndef schlick_diffuse(surface_tilt):\n    \"\"\"\n    Determine the incidence angle modifiers (IAM) for diffuse sky and\n    ground-reflected irradiance on a tilted surface using the Schlick\n    incident angle model.\n\n    The diffuse iam values are calculated using an analytical integration\n    of the Schlick equation [1]_ over the portion of an isotropic sky and\n    isotropic foreground that is visible from the tilted surface [2]_.\n\n    Parameters\n    ----------\n    surface_tilt : numeric\n        Surface tilt angle measured from horizontal (e.g. surface facing\n        up = 0, surface facing horizon = 90). [degrees]\n\n    Returns\n    -------\n    iam_sky : numeric\n        The incident angle modifier for sky diffuse.\n\n    iam_ground : numeric\n        The incident angle modifier for ground-reflected diffuse.\n\n    References\n    ----------\n    .. [1] Schlick, C. An inexpensive BRDF model for physically-based\n       rendering. Computer graphics forum 13 (1994).\n\n    .. [2] Xie, Y., M. Sengupta, A. Habte, A. Andreas, \"The 'Fresnel Equations'\n       for Diffuse radiation on Inclined photovoltaic Surfaces (FEDIS)\",\n       Renewable and Sustainable Energy Reviews, vol. 161, 112362. June 2022.\n       :doi:`10.1016/j.rser.2022.112362`\n\n    See Also\n    --------\n    pvlib.iam.schlick\n    \"\"\"\n    # these calculations are as in [2]_, but with the refractive index\n    # weighting coefficient w set to 1.0 (so it is omitted)\n\n    # relative transmittance of sky diffuse radiation by PV cover:\n    cosB = cosd(surface_tilt)\n    sinB = sind(surface_tilt)\n    cuk = (2 / (np.pi * (1 + cosB))) * (\n        (30/7)*np.pi - (160/21)*np.radians(surface_tilt) - (10/3)*np.pi*cosB\n        + (160/21)*cosB*sinB - (5/3)*np.pi*cosB*sinB**2 + (20/7)*cosB*sinB**3\n        - (5/16)*np.pi*cosB*sinB**4 + (16/105)*cosB*sinB**5\n    )  # Eq 4 in [2]\n\n    # relative transmittance of ground-reflected radiation by PV cover:\n    with np.errstate(divide='ignore', invalid='ignore'):  # Eq 6 in [2]\n        cug = 40 / (21 * (1 - cosB)) - (1 + cosB) / (1 - cosB) * cuk\n\n    cug = np.where(surface_tilt < 1e-6, 0, cug)\n\n    # respect input types:\n    if np.isscalar(surface_tilt):\n        cuk = cuk.item()\n        cug = cug.item()\n    elif isinstance(surface_tilt, pd.Series):\n        cuk = pd.Series(cuk, surface_tilt.index)\n        cug = pd.Series(cug, surface_tilt.index)\n\n    return cuk, cug\n","header":{"file":{"path":"pvlib/iam.py"},"misc_line":null},"index_line":null,"is_binary_file":false,"binary_line":null,"minus_file":{"path":"a/pvlib/iam.py"},"plus_file":{"path":"b/pvlib/iam.py"},"hunks":[{"descriptor":{"old_range":{"start":175,"length":8},"new_range":{"start":175,"length":12},"section":"def physical(aoi, n=1.526, K=4.0, L=0.002, *, n_ar=None):"},"line_group":{"all_lines":[{"content":"    n2costheta2 = n2 * costheta","type":"context"},{"content":"","type":"context"},{"content":"    # reflectance of s-, p-polarized, and normal light by the first interface","type":"context"},{"content":"    rho12_s = ((n1costheta1 - n2costheta2) / (n1costheta1 + n2costheta2)) ** 2","type":"deleted"},{"content":"    rho12_p = ((n1costheta2 - n2costheta1) / (n1costheta2 + n2costheta1)) ** 2","type":"deleted"},{"content":"    with np.errstate(divide='ignore', invalid='ignore'):","type":"added"},{"content":"        rho12_s = \\","type":"added"},{"content":"            ((n1costheta1 - n2costheta2) / (n1costheta1 + n2costheta2)) ** 2","type":"added"},{"content":"        rho12_p = \\","type":"added"},{"content":"            ((n1costheta2 - n2costheta1) / (n1costheta2 + n2costheta1)) ** 2","type":"added"},{"content":"","type":"added"},{"content":"    rho12_0 = ((n1 - n2) / (n1 + n2)) ** 2","type":"context"},{"content":"","type":"context"},{"content":"    # transmittance through the first interface","type":"context"}]},"modified_entities":[],"added_entities":[],"deleted_entities":[]},{"descriptor":{"old_range":{"start":208,"length":13},"new_range":{"start":212,"length":22},"section":"def physical(aoi, n=1.526, K=4.0, L=0.002, *, n_ar=None):"},"line_group":{"all_lines":[{"content":"        tau_0 *= (1 - rho23_0) / (1 - rho23_0 * rho12_0)","type":"context"},{"content":"","type":"context"},{"content":"    # transmittance after absorption in the glass","type":"context"},{"content":"    tau_s *= np.exp(-K * L / costheta)","type":"deleted"},{"content":"    tau_p *= np.exp(-K * L / costheta)","type":"deleted"},{"content":"    with np.errstate(divide='ignore', invalid='ignore'):","type":"added"},{"content":"        tau_s *= np.exp(-K * L / costheta)","type":"added"},{"content":"        tau_p *= np.exp(-K * L / costheta)","type":"added"},{"content":"","type":"added"},{"content":"    tau_0 *= np.exp(-K * L)","type":"context"},{"content":"","type":"context"},{"content":"    # incidence angle modifier","type":"context"},{"content":"    iam = (tau_s + tau_p) / 2 / tau_0","type":"context"},{"content":"","type":"context"},{"content":"    # for light coming from behind the plane, none can enter the module","type":"added"},{"content":"    # when n2 > 1, this is already the case","type":"added"},{"content":"    if np.isclose(n2, 1).any():","type":"added"},{"content":"        iam = np.where(aoi >= 90, 0, iam)","type":"added"},{"content":"        if isinstance(aoi, pd.Series):","type":"added"},{"content":"            iam = pd.Series(iam, index=aoi.index)","type":"added"},{"content":"","type":"added"},{"content":"    return iam","type":"context"},{"content":"","type":"context"},{"content":"","type":"context"},{"content":"","type":"context"}]},"modified_entities":[],"added_entities":[],"deleted_entities":[]}]}],"old_commit_hash":"40e9e978c170bdde4eeee1547729417665dbc34c","new_commit_hash":"d03704c72329577bff6c9e986383aadfec1c17c1","commit_message":"whatsnew","commit_date":"2023-06-12T11:58:04-04:00","metadata":{}}
problem_statement: regression: iam.physical returns nan for aoi > 90° when n = 1
  **Describe the bug**
  For pvlib==0.9.5, when n = 1 (no reflection) and aoi > 90°, we get nan as result.
  
  **To Reproduce**
  ```python
  import pvlib
  pvlib.iam.physical(aoi=100, n=1)
  ```
  returns `nan`.
  
  **Expected behavior**
  The result should be `0`, as it was for pvlib <= 0.9.4.
  
  
  **Versions:**
   - ``pvlib.__version__``: '0.9.5'
   - ``pandas.__version__``:  '1.5.3'
   - python: 3.10.4
  
  
patch: diff --git a/pvlib/iam.py b/pvlib/iam.py
  --- a/pvlib/iam.py
  +++ b/pvlib/iam.py
  @@ -175,8 +175,12 @@ def physical(aoi, n=1.526, K=4.0, L=0.002, *, n_ar=None):
       n2costheta2 = n2 * costheta
   
       # reflectance of s-, p-polarized, and normal light by the first interface
  -    rho12_s = ((n1costheta1 - n2costheta2) / (n1costheta1 + n2costheta2)) ** 2
  -    rho12_p = ((n1costheta2 - n2costheta1) / (n1costheta2 + n2costheta1)) ** 2
  +    with np.errstate(divide='ignore', invalid='ignore'):
  +        rho12_s = \
  +            ((n1costheta1 - n2costheta2) / (n1costheta1 + n2costheta2)) ** 2
  +        rho12_p = \
  +            ((n1costheta2 - n2costheta1) / (n1costheta2 + n2costheta1)) ** 2
  +
       rho12_0 = ((n1 - n2) / (n1 + n2)) ** 2
   
       # transmittance through the first interface
  @@ -208,13 +212,22 @@ def physical(aoi, n=1.526, K=4.0, L=0.002, *, n_ar=None):
           tau_0 *= (1 - rho23_0) / (1 - rho23_0 * rho12_0)
   
       # transmittance after absorption in the glass
  -    tau_s *= np.exp(-K * L / costheta)
  -    tau_p *= np.exp(-K * L / costheta)
  +    with np.errstate(divide='ignore', invalid='ignore'):
  +        tau_s *= np.exp(-K * L / costheta)
  +        tau_p *= np.exp(-K * L / costheta)
  +
       tau_0 *= np.exp(-K * L)
   
       # incidence angle modifier
       iam = (tau_s + tau_p) / 2 / tau_0
   
  +    # for light coming from behind the plane, none can enter the module
  +    # when n2 > 1, this is already the case
  +    if np.isclose(n2, 1).any():
  +        iam = np.where(aoi >= 90, 0, iam)
  +        if isinstance(aoi, pd.Series):
  +            iam = pd.Series(iam, index=aoi.index)
  +
       return iam
   
   
   
  
file_context: {"pvlib/iam.py": "r\"\"\"\nThe ``iam`` module contains functions that implement models for the incidence\nangle modifier (IAM). The IAM quantifies the fraction of direct irradiance on\na module's front surface that is transmitted through the module materials to\nthe cells. Stated differently, the quantity 1 - IAM is the fraction of direct\nirradiance that is reflected away or absorbed by the module's front materials.\nIAM is typically a function of the angle of incidence (AOI) of the direct\nirradiance to the module's surface.\n\"\"\"\n\nimport numpy as np\nimport pandas as pd\nimport functools\nfrom pvlib.tools import cosd, sind\n\n# a dict of required parameter names for each IAM model\n# keys are the function names for the IAM models\n_IAM_MODEL_PARAMS = {\n    'ashrae': {'b'},\n    'physical': {'n', 'K', 'L'},\n    'martin_ruiz': {'a_r'},\n    'sapm': {'B0', 'B1', 'B2', 'B3', 'B4', 'B5'},\n    'interp': set()\n}\n\n\ndef ashrae(aoi, b=0.05):\n    r\"\"\"\n    Determine the incidence angle modifier using the ASHRAE transmission\n    model.\n\n    The ASHRAE (American Society of Heating, Refrigeration, and Air\n    Conditioning Engineers) transmission model is developed in\n    [1]_, and in [2]_. The model has been used in software such as PVSyst [3]_.\n\n    Parameters\n    ----------\n    aoi : numeric\n        The angle of incidence (AOI) between the module normal vector and the\n        sun-beam vector in degrees. Angles of nan will result in nan.\n\n    b : float, default 0.05\n        A parameter to adjust the incidence angle modifier as a function of\n        angle of incidence. Typical values are on the order of 0.05 [3].\n\n    Returns\n    -------\n    iam : numeric\n        The incident angle modifier (IAM). Returns zero for all abs(aoi) >= 90\n        and for all ``iam`` values that would be less than 0.\n\n    Notes\n    -----\n    The incidence angle modifier is calculated as\n\n    .. math::\n\n        IAM = 1 - b (\\sec(aoi) - 1)\n\n    As AOI approaches 90 degrees, the model yields negative values for IAM;\n    negative IAM values are set to zero in this implementation.\n\n    References\n    ----------\n    .. [1] Souka A.F., Safwat H.H., \"Determination of the optimum\n       orientations for the double exposure flat-plate collector and its\n       reflections\". Solar Energy vol .10, pp 170-174. 1966.\n\n    .. [2] ASHRAE standard 93-77\n\n    .. [3] PVsyst Contextual Help.\n       https://files.pvsyst.com/help/index.html?iam_loss.htm retrieved on\n       October 14, 2019\n\n    See Also\n    --------\n    pvlib.iam.physical\n    pvlib.iam.martin_ruiz\n    pvlib.iam.interp\n    \"\"\"\n\n    iam = 1 - b * (1 / np.cos(np.radians(aoi)) - 1)\n    aoi_gte_90 = np.full_like(aoi, False, dtype='bool')\n    np.greater_equal(np.abs(aoi), 90, where=~np.isnan(aoi), out=aoi_gte_90)\n    iam = np.where(aoi_gte_90, 0, iam)\n    iam = np.maximum(0, iam)\n\n    if isinstance(aoi, pd.Series):\n        iam = pd.Series(iam, index=aoi.index)\n\n    return iam\n\n\ndef physical(aoi, n=1.526, K=4.0, L=0.002, *, n_ar=None):\n    r\"\"\"\n    Determine the incidence angle modifier using refractive index ``n``,\n    extinction coefficient ``K``, glazing thickness ``L`` and refractive\n    index ``n_ar`` of an optional anti-reflective coating.\n\n    ``iam.physical`` calculates the incidence angle modifier as described in\n    [1]_, Section 3, with additional support of an anti-reflective coating.\n    The calculation is based on a physical model of reflections, absorption,\n    and transmission through a transparent cover.\n\n    Parameters\n    ----------\n    aoi : numeric\n        The angle of incidence between the module normal vector and the\n        sun-beam vector in degrees. Angles of nan will result in nan.\n\n    n : numeric, default 1.526\n        The effective index of refraction (unitless). Reference [1]_\n        indicates that a value of 1.526 is acceptable for glass.\n\n    K : numeric, default 4.0\n        The glazing extinction coefficient in units of 1/meters.\n        Reference [1] indicates that a value of 4 is reasonable for\n        \"water white\" glass.\n\n    L : numeric, default 0.002\n        The glazing thickness in units of meters. Reference [1]_\n        indicates that 0.002 meters (2 mm) is reasonable for most\n        glass-covered PV panels.\n\n    n_ar : numeric, optional\n        The effective index of refraction of the anti-reflective (AR) coating\n        (unitless). If n_ar is None (default), no AR coating is applied.\n        A typical value for the effective index of an AR coating is 1.29.\n\n    Returns\n    -------\n    iam : numeric\n        The incident angle modifier\n\n    Notes\n    -----\n    The pvlib python authors believe that Eqn. 14 in [1]_ is\n    incorrect, which presents :math:`\\theta_{r} = \\arcsin(n \\sin(AOI))`.\n    Here, :math:`\\theta_{r} = \\arcsin(1/n \\times \\sin(AOI))`\n\n    References\n    ----------\n    .. [1] W. De Soto et al., \"Improvement and validation of a model for\n       photovoltaic array performance\", Solar Energy, vol 80, pp. 78-88,\n       2006.\n\n    .. [2] Duffie, John A. & Beckman, William A.. (2006). Solar Engineering\n       of Thermal Processes, third edition. [Books24x7 version] Available\n       from http://common.books24x7.com/toc.aspx?bookid=17160.\n\n    See Also\n    --------\n    pvlib.iam.martin_ruiz\n    pvlib.iam.ashrae\n    pvlib.iam.interp\n    pvlib.iam.sapm\n    \"\"\"\n    n1, n3 = 1, n\n    if n_ar is None or np.allclose(n_ar, n1):\n        # no AR coating\n        n2 = n\n    else:\n        n2 = n_ar\n\n    # incidence angle\n    costheta = np.maximum(0, cosd(aoi))  # always >= 0\n    sintheta = np.sqrt(1 - costheta**2)  # always >= 0\n    n1costheta1 = n1 * costheta\n    n2costheta1 = n2 * costheta\n\n    # refraction angle of first interface\n    sintheta = n1 / n2 * sintheta\n    costheta = np.sqrt(1 - sintheta**2)\n    n1costheta2 = n1 * costheta\n    n2costheta2 = n2 * costheta\n\n    # reflectance of s-, p-polarized, and normal light by the first interface\n    rho12_s = ((n1costheta1 - n2costheta2) / (n1costheta1 + n2costheta2)) ** 2\n    rho12_p = ((n1costheta2 - n2costheta1) / (n1costheta2 + n2costheta1)) ** 2\n    rho12_0 = ((n1 - n2) / (n1 + n2)) ** 2\n\n    # transmittance through the first interface\n    tau_s = 1 - rho12_s\n    tau_p = 1 - rho12_p\n    tau_0 = 1 - rho12_0\n\n    if not np.allclose(n3, n2):  # AR coated glass\n        n3costheta2 = n3 * costheta\n        # refraction angle of second interface\n        sintheta = n2 / n3 * sintheta\n        costheta = np.sqrt(1 - sintheta**2)\n        n2costheta3 = n2 * costheta\n        n3costheta3 = n3 * costheta\n\n        # reflectance by the second interface\n        rho23_s = (\n            (n2costheta2 - n3costheta3) / (n2costheta2 + n3costheta3)\n        ) ** 2\n        rho23_p = (\n            (n2costheta3 - n3costheta2) / (n2costheta3 + n3costheta2)\n        ) ** 2\n        rho23_0 = ((n2 - n3) / (n2 + n3)) ** 2\n\n        # transmittance through the coating, including internal reflections\n        # 1 + rho23*rho12 + (rho23*rho12)^2 + ... = 1/(1 - rho23*rho12)\n        tau_s *= (1 - rho23_s) / (1 - rho23_s * rho12_s)\n        tau_p *= (1 - rho23_p) / (1 - rho23_p * rho12_p)\n        tau_0 *= (1 - rho23_0) / (1 - rho23_0 * rho12_0)\n\n    # transmittance after absorption in the glass\n    tau_s *= np.exp(-K * L / costheta)\n    tau_p *= np.exp(-K * L / costheta)\n    tau_0 *= np.exp(-K * L)\n\n    # incidence angle modifier\n    iam = (tau_s + tau_p) / 2 / tau_0\n\n    return iam\n\n\ndef martin_ruiz(aoi, a_r=0.16):\n    r'''\n    Determine the incidence angle modifier (IAM) using the Martin\n    and Ruiz incident angle model.\n\n    Parameters\n    ----------\n    aoi : numeric, degrees\n        The angle of incidence between the module normal vector and the\n        sun-beam vector in degrees.\n\n    a_r : numeric\n        The angular losses coefficient described in equation 3 of [1]_.\n        This is an empirical dimensionless parameter. Values of ``a_r`` are\n        generally on the order of 0.08 to 0.25 for flat-plate PV modules.\n\n    Returns\n    -------\n    iam : numeric\n        The incident angle modifier(s)\n\n    Notes\n    -----\n    `martin_ruiz` calculates the incidence angle modifier (IAM) as described in\n    [1]_. The information required is the incident angle (AOI) and the angular\n    losses coefficient (a_r). Note that [1]_ has a corrigendum [2]_ which\n    clarifies a mix-up of 'alpha's and 'a's in the former.\n\n    The incident angle modifier is defined as\n\n    .. math::\n\n       IAM = \\frac{1 - \\exp(-\\frac{\\cos(aoi)}{a_r})}\n       {1 - \\exp(\\frac{-1}{a_r})}\n\n    which is presented as :math:`AL(\\alpha) = 1 - IAM` in equation 4 of [1]_,\n    with :math:`\\alpha` representing the angle of incidence AOI. Thus IAM = 1\n    at AOI = 0, and IAM = 0 at AOI = 90.  This equation is only valid for\n    -90 <= aoi <= 90, therefore `iam` is constrained to 0.0 outside this\n    interval.\n\n    References\n    ----------\n    .. [1] N. Martin and J. M. Ruiz, \"Calculation of the PV modules angular\n       losses under field conditions by means of an analytical model\", Solar\n       Energy Materials & Solar Cells, vol. 70, pp. 25-38, 2001.\n\n    .. [2] N. Martin and J. M. Ruiz, \"Corrigendum to 'Calculation of the PV\n       modules angular losses under field conditions by means of an\n       analytical model'\", Solar Energy Materials & Solar Cells, vol. 110,\n       pp. 154, 2013.\n\n    See Also\n    --------\n    pvlib.iam.martin_ruiz_diffuse\n    pvlib.iam.physical\n    pvlib.iam.ashrae\n    pvlib.iam.interp\n    pvlib.iam.sapm\n    '''\n    # Contributed by Anton Driesse (@adriesse), PV Performance Labs. July, 2019\n\n    aoi_input = aoi\n\n    aoi = np.asanyarray(aoi)\n    a_r = np.asanyarray(a_r)\n\n    if np.any(np.less_equal(a_r, 0)):\n        raise ValueError(\"The parameter 'a_r' cannot be zero or negative.\")\n\n    with np.errstate(invalid='ignore'):\n        iam = (1 - np.exp(-cosd(aoi) / a_r)) / (1 - np.exp(-1 / a_r))\n        iam = np.where(np.abs(aoi) >= 90.0, 0.0, iam)\n\n    if isinstance(aoi_input, pd.Series):\n        iam = pd.Series(iam, index=aoi_input.index)\n\n    return iam\n\n\ndef martin_ruiz_diffuse(surface_tilt, a_r=0.16, c1=0.4244, c2=None):\n    '''\n    Determine the incidence angle modifiers (iam) for diffuse sky and\n    ground-reflected irradiance using the Martin and Ruiz incident angle model.\n\n    Parameters\n    ----------\n    surface_tilt: float or array-like, default 0\n        Surface tilt angles in decimal degrees.\n        The tilt angle is defined as degrees from horizontal\n        (e.g. surface facing up = 0, surface facing horizon = 90)\n        surface_tilt must be in the range [0, 180]\n\n    a_r : numeric\n        The angular losses coefficient described in equation 3 of [1]_.\n        This is an empirical dimensionless parameter. Values of a_r are\n        generally on the order of 0.08 to 0.25 for flat-plate PV modules.\n        a_r must be greater than zero.\n\n    c1 : float\n        First fitting parameter for the expressions that approximate the\n        integral of diffuse irradiance coming from different directions.\n        c1 is given as the constant 4 / 3 / pi (0.4244) in [1]_.\n\n    c2 : float\n        Second fitting parameter for the expressions that approximate the\n        integral of diffuse irradiance coming from different directions.\n        If c2 is None, it will be calculated according to the linear\n        relationship given in [3]_.\n\n    Returns\n    -------\n    iam_sky : numeric\n        The incident angle modifier for sky diffuse\n\n    iam_ground : numeric\n        The incident angle modifier for ground-reflected diffuse\n\n    Notes\n    -----\n    Sky and ground modifiers are complementary: iam_sky for tilt = 30 is\n    equal to iam_ground for tilt = 180 - 30.  For vertical surfaces,\n    tilt = 90, the two factors are equal.\n\n    References\n    ----------\n    .. [1] N. Martin and J. M. Ruiz, \"Calculation of the PV modules angular\n       losses under field conditions by means of an analytical model\", Solar\n       Energy Materials & Solar Cells, vol. 70, pp. 25-38, 2001.\n\n    .. [2] N. Martin and J. M. Ruiz, \"Corrigendum to 'Calculation of the PV\n       modules angular losses under field conditions by means of an\n       analytical model'\", Solar Energy Materials & Solar Cells, vol. 110,\n       pp. 154, 2013.\n\n    .. [3] \"IEC 61853-3 Photovoltaic (PV) module performance testing and energy\n       rating - Part 3: Energy rating of PV modules\". IEC, Geneva, 2018.\n\n    See Also\n    --------\n    pvlib.iam.martin_ruiz\n    pvlib.iam.physical\n    pvlib.iam.ashrae\n    pvlib.iam.interp\n    pvlib.iam.sapm\n    '''\n    # Contributed by Anton Driesse (@adriesse), PV Performance Labs. Oct. 2019\n\n    if isinstance(surface_tilt, pd.Series):\n        out_index = surface_tilt.index\n    else:\n        out_index = None\n\n    surface_tilt = np.asanyarray(surface_tilt)\n\n    # avoid undefined results for horizontal or upside-down surfaces\n    zeroang = 1e-06\n\n    surface_tilt = np.where(surface_tilt == 0, zeroang, surface_tilt)\n    surface_tilt = np.where(surface_tilt == 180, 180 - zeroang, surface_tilt)\n\n    if c2 is None:\n        # This equation is from [3] Sect. 7.2\n        c2 = 0.5 * a_r - 0.154\n\n    beta = np.radians(surface_tilt)\n    sin = np.sin\n    pi = np.pi\n    cos = np.cos\n\n    # avoid RuntimeWarnings for <, sin, and cos with nan\n    with np.errstate(invalid='ignore'):\n        # because sin(pi) isn't exactly zero\n        sin_beta = np.where(surface_tilt < 90, sin(beta), sin(pi - beta))\n\n        trig_term_sky = sin_beta + (pi - beta - sin_beta) / (1 + cos(beta))\n        trig_term_gnd = sin_beta +      (beta - sin_beta) / (1 - cos(beta))  # noqa: E222 E261 E501\n\n    iam_sky = 1 - np.exp(-(c1 + c2 * trig_term_sky) * trig_term_sky / a_r)\n    iam_gnd = 1 - np.exp(-(c1 + c2 * trig_term_gnd) * trig_term_gnd / a_r)\n\n    if out_index is not None:\n        iam_sky = pd.Series(iam_sky, index=out_index, name='iam_sky')\n        iam_gnd = pd.Series(iam_gnd, index=out_index, name='iam_ground')\n\n    return iam_sky, iam_gnd\n\n\ndef interp(aoi, theta_ref, iam_ref, method='linear', normalize=True):\n    r'''\n    Determine the incidence angle modifier (IAM) by interpolating a set of\n    reference values, which are usually measured values.\n\n    Parameters\n    ----------\n    aoi : numeric\n        The angle of incidence between the module normal vector and the\n        sun-beam vector [degrees].\n\n    theta_ref : numeric\n        Vector of angles at which the IAM is known [degrees].\n\n    iam_ref : numeric\n        IAM values for each angle in ``theta_ref`` [unitless].\n\n    method : str, default 'linear'\n        Specifies the interpolation method.\n        Useful options are: 'linear', 'quadratic', 'cubic'.\n        See scipy.interpolate.interp1d for more options.\n\n    normalize : boolean, default True\n        When true, the interpolated values are divided by the interpolated\n        value at zero degrees.  This ensures that ``iam=1.0`` at normal\n        incidence.\n\n    Returns\n    -------\n    iam : numeric\n        The incident angle modifier(s) [unitless]\n\n    Notes\n    -----\n    ``theta_ref`` must have two or more points and may span any range of\n    angles. Typically there will be a dozen or more points in the range 0-90\n    degrees. Beyond the range of ``theta_ref``, IAM values are extrapolated,\n    but constrained to be non-negative.\n\n    The sign of ``aoi`` is ignored; only the magnitude is used.\n\n    See Also\n    --------\n    pvlib.iam.physical\n    pvlib.iam.ashrae\n    pvlib.iam.martin_ruiz\n    pvlib.iam.sapm\n    '''\n    # Contributed by Anton Driesse (@adriesse), PV Performance Labs. July, 2019\n\n    from scipy.interpolate import interp1d\n\n    # Scipy doesn't give the clearest feedback, so check number of points here.\n    MIN_REF_VALS = {'linear': 2, 'quadratic': 3, 'cubic': 4, 1: 2, 2: 3, 3: 4}\n\n    if len(theta_ref) < MIN_REF_VALS.get(method, 2):\n        raise ValueError(\"Too few reference points defined \"\n                         \"for interpolation method '%s'.\" % method)\n\n    if np.any(np.less(iam_ref, 0)):\n        raise ValueError(\"Negative value(s) found in 'iam_ref'. \"\n                         \"This is not physically possible.\")\n\n    interpolator = interp1d(theta_ref, iam_ref, kind=method,\n                            fill_value='extrapolate')\n    aoi_input = aoi\n\n    aoi = np.asanyarray(aoi)\n    aoi = np.abs(aoi)\n    iam = interpolator(aoi)\n    iam = np.clip(iam, 0, None)\n\n    if normalize:\n        iam /= interpolator(0)\n\n    if isinstance(aoi_input, pd.Series):\n        iam = pd.Series(iam, index=aoi_input.index)\n\n    return iam\n\n\ndef sapm(aoi, module, upper=None):\n    r\"\"\"\n    Determine the incidence angle modifier (IAM) using the SAPM model.\n\n    Parameters\n    ----------\n    aoi : numeric\n        Angle of incidence in degrees. Negative input angles will return\n        zeros.\n\n    module : dict-like\n        A dict or Series with the SAPM IAM model parameters.\n        See the :py:func:`sapm` notes section for more details.\n\n    upper : None or float, default None\n        Upper limit on the results.\n\n    Returns\n    -------\n    iam : numeric\n        The SAPM angle of incidence loss coefficient, termed F2 in [1]_.\n\n    Notes\n    -----\n    The SAPM [1]_ traditionally does not define an upper limit on the AOI\n    loss function and values slightly exceeding 1 may exist for moderate\n    angles of incidence (15-40 degrees). However, users may consider\n    imposing an upper limit of 1.\n\n    References\n    ----------\n    .. [1] King, D. et al, 2004, \"Sandia Photovoltaic Array Performance\n       Model\", SAND Report 3535, Sandia National Laboratories, Albuquerque,\n       NM.\n\n    .. [2] B.H. King et al, \"Procedure to Determine Coefficients for the\n       Sandia Array Performance Model (SAPM),\" SAND2016-5284, Sandia\n       National Laboratories (2016).\n\n    .. [3] B.H. King et al, \"Recent Advancements in Outdoor Measurement\n       Techniques for Angle of Incidence Effects,\" 42nd IEEE PVSC (2015).\n       DOI: 10.1109/PVSC.2015.7355849\n\n    See Also\n    --------\n    pvlib.iam.physical\n    pvlib.iam.ashrae\n    pvlib.iam.martin_ruiz\n    pvlib.iam.interp\n    \"\"\"\n\n    aoi_coeff = [module['B5'], module['B4'], module['B3'], module['B2'],\n                 module['B1'], module['B0']]\n\n    iam = np.polyval(aoi_coeff, aoi)\n    iam = np.clip(iam, 0, upper)\n    # nan tolerant masking\n    aoi_lt_0 = np.full_like(aoi, False, dtype='bool')\n    np.less(aoi, 0, where=~np.isnan(aoi), out=aoi_lt_0)\n    iam = np.where(aoi_lt_0, 0, iam)\n\n    if isinstance(aoi, pd.Series):\n        iam = pd.Series(iam, aoi.index)\n\n    return iam\n\n\ndef marion_diffuse(model, surface_tilt, **kwargs):\n    \"\"\"\n    Determine diffuse irradiance incidence angle modifiers using Marion's\n    method of integrating over solid angle.\n\n    Parameters\n    ----------\n    model : str\n        The IAM function to evaluate across solid angle. Must be one of\n        `'ashrae', 'physical', 'martin_ruiz', 'sapm', 'schlick'`.\n\n    surface_tilt : numeric\n        Surface tilt angles in decimal degrees.\n        The tilt angle is defined as degrees from horizontal\n        (e.g. surface facing up = 0, surface facing horizon = 90).\n\n    **kwargs\n        Extra parameters passed to the IAM function.\n\n    Returns\n    -------\n    iam : dict\n        IAM values for each type of diffuse irradiance:\n\n            * 'sky': radiation from the sky dome (zenith <= 90)\n            * 'horizon': radiation from the region of the sky near the horizon\n              (89.5 <= zenith <= 90)\n            * 'ground': radiation reflected from the ground (zenith >= 90)\n\n        See [1]_ for a detailed description of each class.\n\n    See Also\n    --------\n    pvlib.iam.marion_integrate\n\n    References\n    ----------\n    .. [1] B. Marion \"Numerical method for angle-of-incidence correction\n       factors for diffuse radiation incident photovoltaic modules\",\n       Solar Energy, Volume 147, Pages 344-348. 2017.\n       DOI: 10.1016/j.solener.2017.03.027\n\n    Examples\n    --------\n    >>> marion_diffuse('physical', surface_tilt=20)\n    {'sky': 0.9539178294437575,\n     'horizon': 0.7652650139134007,\n     'ground': 0.6387140117795903}\n\n    >>> marion_diffuse('ashrae', [20, 30], b=0.04)\n    {'sky': array([0.96748999, 0.96938408]),\n     'horizon': array([0.86478428, 0.91825792]),\n     'ground': array([0.77004435, 0.8522436 ])}\n    \"\"\"\n\n    models = {\n        'physical': physical,\n        'ashrae': ashrae,\n        'sapm': sapm,\n        'martin_ruiz': martin_ruiz,\n        'schlick': schlick,\n    }\n\n    try:\n        iam_model = models[model]\n    except KeyError:\n        raise ValueError('model must be one of: ' + str(list(models.keys())))\n\n    iam_function = functools.partial(iam_model, **kwargs)\n    iam = {}\n    for region in ['sky', 'horizon', 'ground']:\n        iam[region] = marion_integrate(iam_function, surface_tilt, region)\n\n    return iam\n\n\ndef marion_integrate(function, surface_tilt, region, num=None):\n    \"\"\"\n    Integrate an incidence angle modifier (IAM) function over solid angle\n    to determine a diffuse irradiance correction factor using Marion's method.\n\n    This lower-level function actually performs the IAM integration for the\n    specified solid angle region.\n\n    Parameters\n    ----------\n    function : callable(aoi)\n        The IAM function to evaluate across solid angle. The function must\n        be vectorized and take only one parameter, the angle of incidence in\n        degrees.\n\n    surface_tilt : numeric\n        Surface tilt angles in decimal degrees.\n        The tilt angle is defined as degrees from horizontal\n        (e.g. surface facing up = 0, surface facing horizon = 90).\n\n    region : {'sky', 'horizon', 'ground'}\n        The region to integrate over. Must be one of:\n\n            * 'sky': radiation from the sky dome (zenith <= 90)\n            * 'horizon': radiation from the region of the sky near the horizon\n              (89.5 <= zenith <= 90)\n            * 'ground': radiation reflected from the ground (zenith >= 90)\n\n        See [1]_ for a detailed description of each class.\n\n    num : int, optional\n        The number of increments in the zenith integration.\n        If not specified, N will follow the values used in [1]_:\n\n            * 'sky' or 'ground': num = 180\n            * 'horizon': num = 1800\n\n    Returns\n    -------\n    iam : numeric\n        AOI diffuse correction factor for the specified region.\n\n    See Also\n    --------\n    pvlib.iam.marion_diffuse\n\n    References\n    ----------\n    .. [1] B. Marion \"Numerical method for angle-of-incidence correction\n       factors for diffuse radiation incident photovoltaic modules\",\n       Solar Energy, Volume 147, Pages 344-348. 2017.\n       DOI: 10.1016/j.solener.2017.03.027\n\n    Examples\n    --------\n    >>> marion_integrate(pvlib.iam.ashrae, 20, 'sky')\n    0.9596085829811408\n\n    >>> from functools import partial\n    >>> f = partial(pvlib.iam.physical, n=1.3)\n    >>> marion_integrate(f, [20, 30], 'sky')\n    array([0.96225034, 0.9653219 ])\n    \"\"\"\n\n    if num is None:\n        if region in ['sky', 'ground']:\n            num = 180\n        elif region == 'horizon':\n            num = 1800\n        else:\n            raise ValueError(f'Invalid region: {region}')\n\n    beta = np.radians(surface_tilt)\n    if isinstance(beta, pd.Series):\n        # convert Series to np array for broadcasting later\n        beta = beta.values\n    ai = np.pi/num  # angular increment\n\n    phi_range = np.linspace(0, np.pi, num, endpoint=False)\n    psi_range = np.linspace(0, 2*np.pi, 2*num, endpoint=False)\n\n    # the pseudocode in [1] do these checks at the end, but it's\n    # faster to do this criteria check up front instead of later.\n    if region == 'sky':\n        mask = phi_range + ai <= np.pi/2\n    elif region == 'horizon':\n        lo = 89.5 * np.pi/180\n        hi = np.pi/2\n        mask = (lo <= phi_range) & (phi_range + ai <= hi)\n    elif region == 'ground':\n        mask = (phi_range >= np.pi/2)\n    else:\n        raise ValueError(f'Invalid region: {region}')\n    phi_range = phi_range[mask]\n\n    # fast Cartesian product of phi and psi\n    angles = np.array(np.meshgrid(phi_range, psi_range)).T.reshape(-1, 2)\n    # index with single-element lists to maintain 2nd dimension so that\n    # these angle arrays broadcast across the beta array\n    phi_1 = angles[:, [0]]\n    psi_1 = angles[:, [1]]\n    phi_2 = phi_1 + ai\n    # psi_2 = psi_1 + ai  # not needed\n    phi_avg = phi_1 + 0.5*ai\n    psi_avg = psi_1 + 0.5*ai\n    term_1 = np.cos(beta) * np.cos(phi_avg)\n    # The AOI formula includes a term based on the difference between\n    # panel azimuth and the photon azimuth, but because we assume each class\n    # of diffuse irradiance is isotropic and we are integrating over all\n    # angles, it doesn't matter what panel azimuth we choose (i.e., the\n    # system is rotationally invariant).  So we choose gamma to be zero so\n    # that we can omit it from the cos(psi_avg) term.\n    # Marion's paper mentions this in the Section 3 pseudocode:\n    # \"set gamma to pi (or any value between 0 and 2pi)\"\n    term_2 = np.sin(beta) * np.sin(phi_avg) * np.cos(psi_avg)\n    cosaoi = term_1 + term_2\n    aoi = np.arccos(cosaoi)\n    # simplify Eq 8, (psi_2 - psi_1) is always ai\n    dAs = ai * (np.cos(phi_1) - np.cos(phi_2))\n    cosaoi_dAs = cosaoi * dAs\n    # apply the final AOI check, zeroing out non-passing points\n    mask = aoi < np.pi/2\n    cosaoi_dAs = np.where(mask, cosaoi_dAs, 0)\n    numerator = np.sum(function(np.degrees(aoi)) * cosaoi_dAs, axis=0)\n    denominator = np.sum(cosaoi_dAs, axis=0)\n\n    with np.errstate(invalid='ignore'):\n        # in some cases, no points pass the criteria\n        # (e.g. region='ground', surface_tilt=0), so we override the division\n        # by zero to set Fd=0.  Also, preserve nans in beta.\n        Fd = np.where((denominator != 0) | ~np.isfinite(beta),\n                      numerator / denominator,\n                      0)\n\n    # preserve input type\n    if np.isscalar(surface_tilt):\n        Fd = Fd.item()\n    elif isinstance(surface_tilt, pd.Series):\n        Fd = pd.Series(Fd, surface_tilt.index)\n\n    return Fd\n\n\ndef schlick(aoi):\n    \"\"\"\n    Determine incidence angle modifier (IAM) for direct irradiance using the\n    Schlick approximation to the Fresnel equations.\n\n    The Schlick approximation was proposed in [1]_ as a computationally\n    efficient alternative to computing the Fresnel factor in computer\n    graphics contexts.  This implementation is a normalized form of the\n    equation in [1]_ so that it can be used as a PV IAM model.\n    Unlike other IAM models, this model has no ability to describe\n    different reflection profiles.\n\n    In PV contexts, the Schlick approximation has been used as an analytically\n    integrable alternative to the Fresnel equations for estimating IAM\n    for diffuse irradiance [2]_.\n\n    Parameters\n    ----------\n    aoi : numeric\n        The angle of incidence (AOI) between the module normal vector and the\n        sun-beam vector. Angles of nan will result in nan. [degrees]\n\n    Returns\n    -------\n    iam : numeric\n        The incident angle modifier.\n\n    References\n    ----------\n    .. [1] Schlick, C. An inexpensive BRDF model for physically-based\n       rendering. Computer graphics forum 13 (1994).\n\n    .. [2] Xie, Y., M. Sengupta, A. Habte, A. Andreas, \"The 'Fresnel Equations'\n       for Diffuse radiation on Inclined photovoltaic Surfaces (FEDIS)\",\n       Renewable and Sustainable Energy Reviews, vol. 161, 112362. June 2022.\n       :doi:`10.1016/j.rser.2022.112362`\n\n    See Also\n    --------\n    pvlib.iam.schlick_diffuse\n    \"\"\"\n    iam = 1 - (1 - cosd(aoi)) ** 5\n    iam = np.where(np.abs(aoi) >= 90.0, 0.0, iam)\n\n    # preserve input type\n    if np.isscalar(aoi):\n        iam = iam.item()\n    elif isinstance(aoi, pd.Series):\n        iam = pd.Series(iam, aoi.index)\n\n    return iam\n\n\ndef schlick_diffuse(surface_tilt):\n    \"\"\"\n    Determine the incidence angle modifiers (IAM) for diffuse sky and\n    ground-reflected irradiance on a tilted surface using the Schlick\n    incident angle model.\n\n    The diffuse iam values are calculated using an analytical integration\n    of the Schlick equation [1]_ over the portion of an isotropic sky and\n    isotropic foreground that is visible from the tilted surface [2]_.\n\n    Parameters\n    ----------\n    surface_tilt : numeric\n        Surface tilt angle measured from horizontal (e.g. surface facing\n        up = 0, surface facing horizon = 90). [degrees]\n\n    Returns\n    -------\n    iam_sky : numeric\n        The incident angle modifier for sky diffuse.\n\n    iam_ground : numeric\n        The incident angle modifier for ground-reflected diffuse.\n\n    References\n    ----------\n    .. [1] Schlick, C. An inexpensive BRDF model for physically-based\n       rendering. Computer graphics forum 13 (1994).\n\n    .. [2] Xie, Y., M. Sengupta, A. Habte, A. Andreas, \"The 'Fresnel Equations'\n       for Diffuse radiation on Inclined photovoltaic Surfaces (FEDIS)\",\n       Renewable and Sustainable Energy Reviews, vol. 161, 112362. June 2022.\n       :doi:`10.1016/j.rser.2022.112362`\n\n    See Also\n    --------\n    pvlib.iam.schlick\n    \"\"\"\n    # these calculations are as in [2]_, but with the refractive index\n    # weighting coefficient w set to 1.0 (so it is omitted)\n\n    # relative transmittance of sky diffuse radiation by PV cover:\n    cosB = cosd(surface_tilt)\n    sinB = sind(surface_tilt)\n    cuk = (2 / (np.pi * (1 + cosB))) * (\n        (30/7)*np.pi - (160/21)*np.radians(surface_tilt) - (10/3)*np.pi*cosB\n        + (160/21)*cosB*sinB - (5/3)*np.pi*cosB*sinB**2 + (20/7)*cosB*sinB**3\n        - (5/16)*np.pi*cosB*sinB**4 + (16/105)*cosB*sinB**5\n    )  # Eq 4 in [2]\n\n    # relative transmittance of ground-reflected radiation by PV cover:\n    with np.errstate(divide='ignore', invalid='ignore'):  # Eq 6 in [2]\n        cug = 40 / (21 * (1 - cosB)) - (1 + cosB) / (1 - cosB) * cuk\n\n    cug = np.where(surface_tilt < 1e-6, 0, cug)\n\n    # respect input types:\n    if np.isscalar(surface_tilt):\n        cuk = cuk.item()\n        cug = cug.item()\n    elif isinstance(surface_tilt, pd.Series):\n        cuk = pd.Series(cuk, surface_tilt.index)\n        cug = pd.Series(cug, surface_tilt.index)\n\n    return cuk, cug\n"}
filelevel_patches: {"pvlib/iam.py": "@@ -175,8 +175,12 @@ def physical(aoi, n=1.526, K=4.0, L=0.002, *, n_ar=None):\n     n2costheta2 = n2 * costheta\n \n     # reflectance of s-, p-polarized, and normal light by the first interface\n-    rho12_s = ((n1costheta1 - n2costheta2) / (n1costheta1 + n2costheta2)) ** 2\n-    rho12_p = ((n1costheta2 - n2costheta1) / (n1costheta2 + n2costheta1)) ** 2\n+    with np.errstate(divide='ignore', invalid='ignore'):\n+        rho12_s = \\\n+            ((n1costheta1 - n2costheta2) / (n1costheta1 + n2costheta2)) ** 2\n+        rho12_p = \\\n+            ((n1costheta2 - n2costheta1) / (n1costheta2 + n2costheta1)) ** 2\n+\n     rho12_0 = ((n1 - n2) / (n1 + n2)) ** 2\n \n     # transmittance through the first interface\n@@ -208,13 +212,22 @@ def physical(aoi, n=1.526, K=4.0, L=0.002, *, n_ar=None):\n         tau_0 *= (1 - rho23_0) / (1 - rho23_0 * rho12_0)\n \n     # transmittance after absorption in the glass\n-    tau_s *= np.exp(-K * L / costheta)\n-    tau_p *= np.exp(-K * L / costheta)\n+    with np.errstate(divide='ignore', invalid='ignore'):\n+        tau_s *= np.exp(-K * L / costheta)\n+        tau_p *= np.exp(-K * L / costheta)\n+\n     tau_0 *= np.exp(-K * L)\n \n     # incidence angle modifier\n     iam = (tau_s + tau_p) / 2 / tau_0\n \n+    # for light coming from behind the plane, none can enter the module\n+    # when n2 > 1, this is already the case\n+    if np.isclose(n2, 1).any():\n+        iam = np.where(aoi >= 90, 0, iam)\n+        if isinstance(aoi, pd.Series):\n+            iam = pd.Series(iam, index=aoi.index)\n+\n     return iam"}
messages.content: 
  We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  regression: iam.physical returns nan for aoi > 90° when n = 1
  **Describe the bug**
  For pvlib==0.9.5, when n = 1 (no reflection) and aoi > 90°, we get nan as result.
  
  **To Reproduce**
  ```python
  import pvlib
  pvlib.iam.physical(aoi=100, n=1)
  ```
  returns `nan`.
  
  **Expected behavior**
  The result should be `0`, as it was for pvlib <= 0.9.4.
  
  
  **Versions:**
   - ``pvlib.__version__``: '0.9.5'
   - ``pandas.__version__``:  '1.5.3'
   - python: 3.10.4
  
  
  --- END ISSUE ---
  
  Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
  
  --- BEGIN FILE ---
  ```
  ### pvlib/iam.py
  r"""
  The ``iam`` module contains functions that implement models for the incidence
  angle modifier (IAM). The IAM quantifies the fraction of direct irradiance on
  a module's front surface that is transmitted through the module materials to
  the cells. Stated differently, the quantity 1 - IAM is the fraction of direct
  irradiance that is reflected away or absorbed by the module's front materials.
  IAM is typically a function of the angle of incidence (AOI) of the direct
  irradiance to the module's surface.
  """
  
  import numpy as np
  import pandas as pd
  import functools
  from pvlib.tools import cosd, sind
  
  # a dict of required parameter names for each IAM model
  # keys are the function names for the IAM models
  _IAM_MODEL_PARAMS = {
      'ashrae': {'b'},
      'physical': {'n', 'K', 'L'},
      'martin_ruiz': {'a_r'},
      'sapm': {'B0', 'B1', 'B2', 'B3', 'B4', 'B5'},
      'interp': set()
  }
  
  
  def ashrae(aoi, b=0.05):
      r"""
      Determine the incidence angle modifier using the ASHRAE transmission
      model.
  
      The ASHRAE (American Society of Heating, Refrigeration, and Air
      Conditioning Engineers) transmission model is developed in
      [1]_, and in [2]_. The model has been used in software such as PVSyst [3]_.
  
      Parameters
      ----------
      aoi : numeric
          The angle of incidence (AOI) between the module normal vector and the
          sun-beam vector in degrees. Angles of nan will result in nan.
  
      b : float, default 0.05
          A parameter to adjust the incidence angle modifier as a function of
          angle of incidence. Typical values are on the order of 0.05 [3].
  
      Returns
      -------
      iam : numeric
          The incident angle modifier (IAM). Returns zero for all abs(aoi) >= 90
          and for all ``iam`` values that would be less than 0.
  
      Notes
      -----
      The incidence angle modifier is calculated as
  
      .. math::
  
          IAM = 1 - b (\sec(aoi) - 1)
  
      As AOI approaches 90 degrees, the model yields negative values for IAM;
      negative IAM values are set to zero in this implementation.
  
      References
      ----------
      .. [1] Souka A.F., Safwat H.H., "Determination of the optimum
         orientations for the double exposure flat-plate collector and its
         reflections". Solar Energy vol .10, pp 170-174. 1966.
  
      .. [2] ASHRAE standard 93-77
  
      .. [3] PVsyst Contextual Help.
         https://files.pvsyst.com/help/index.html?iam_loss.htm retrieved on
         October 14, 2019
  
      See Also
      --------
      pvlib.iam.physical
      pvlib.iam.martin_ruiz
      pvlib.iam.interp
      """
  
      iam = 1 - b * (1 / np.cos(np.radians(aoi)) - 1)
      aoi_gte_90 = np.full_like(aoi, False, dtype='bool')
      np.greater_equal(np.abs(aoi), 90, where=~np.isnan(aoi), out=aoi_gte_90)
      iam = np.where(aoi_gte_90, 0, iam)
      iam = np.maximum(0, iam)
  
      if isinstance(aoi, pd.Series):
          iam = pd.Series(iam, index=aoi.index)
  
      return iam
  
  
  def physical(aoi, n=1.526, K=4.0, L=0.002, *, n_ar=None):
      r"""
      Determine the incidence angle modifier using refractive index ``n``,
      extinction coefficient ``K``, glazing thickness ``L`` and refractive
      index ``n_ar`` of an optional anti-reflective coating.
  
      ``iam.physical`` calculates the incidence angle modifier as described in
      [1]_, Section 3, with additional support of an anti-reflective coating.
      The calculation is based on a physical model of reflections, absorption,
      and transmission through a transparent cover.
  
      Parameters
      ----------
      aoi : numeric
          The angle of incidence between the module normal vector and the
          sun-beam vector in degrees. Angles of nan will result in nan.
  
      n : numeric, default 1.526
          The effective index of refraction (unitless). Reference [1]_
          indicates that a value of 1.526 is acceptable for glass.
  
      K : numeric, default 4.0
          The glazing extinction coefficient in units of 1/meters.
          Reference [1] indicates that a value of 4 is reasonable for
          "water white" glass.
  
      L : numeric, default 0.002
          The glazing thickness in units of meters. Reference [1]_
          indicates that 0.002 meters (2 mm) is reasonable for most
          glass-covered PV panels.
  
      n_ar : numeric, optional
          The effective index of refraction of the anti-reflective (AR) coating
          (unitless). If n_ar is None (default), no AR coating is applied.
          A typical value for the effective index of an AR coating is 1.29.
  
      Returns
      -------
      iam : numeric
          The incident angle modifier
  
      Notes
      -----
      The pvlib python authors believe that Eqn. 14 in [1]_ is
      incorrect, which presents :math:`\theta_{r} = \arcsin(n \sin(AOI))`.
      Here, :math:`\theta_{r} = \arcsin(1/n \times \sin(AOI))`
  
      References
      ----------
      .. [1] W. De Soto et al., "Improvement and validation of a model for
         photovoltaic array performance", Solar Energy, vol 80, pp. 78-88,
         2006.
  
      .. [2] Duffie, John A. & Beckman, William A.. (2006). Solar Engineering
         of Thermal Processes, third edition. [Books24x7 version] Available
         from http://common.books24x7.com/toc.aspx?bookid=17160.
  
      See Also
      --------
      pvlib.iam.martin_ruiz
      pvlib.iam.ashrae
      pvlib.iam.interp
      pvlib.iam.sapm
      """
      n1, n3 = 1, n
      if n_ar is None or np.allclose(n_ar, n1):
          # no AR coating
          n2 = n
      else:
          n2 = n_ar
  
      # incidence angle
      costheta = np.maximum(0, cosd(aoi))  # always >= 0
      sintheta = np.sqrt(1 - costheta**2)  # always >= 0
      n1costheta1 = n1 * costheta
      n2costheta1 = n2 * costheta
  
      # refraction angle of first interface
      sintheta = n1 / n2 * sintheta
      costheta = np.sqrt(1 - sintheta**2)
      n1costheta2 = n1 * costheta
      n2costheta2 = n2 * costheta
  
      # reflectance of s-, p-polarized, and normal light by the first interface
      rho12_s = ((n1costheta1 - n2costheta2) / (n1costheta1 + n2costheta2)) ** 2
      rho12_p = ((n1costheta2 - n2costheta1) / (n1costheta2 + n2costheta1)) ** 2
      rho12_0 = ((n1 - n2) / (n1 + n2)) ** 2
  
      # transmittance through the first interface
      tau_s = 1 - rho12_s
      tau_p = 1 - rho12_p
      tau_0 = 1 - rho12_0
  
      if not np.allclose(n3, n2):  # AR coated glass
          n3costheta2 = n3 * costheta
          # refraction angle of second interface
          sintheta = n2 / n3 * sintheta
          costheta = np.sqrt(1 - sintheta**2)
          n2costheta3 = n2 * costheta
          n3costheta3 = n3 * costheta
  
          # reflectance by the second interface
          rho23_s = (
              (n2costheta2 - n3costheta3) / (n2costheta2 + n3costheta3)
          ) ** 2
          rho23_p = (
              (n2costheta3 - n3costheta2) / (n2costheta3 + n3costheta2)
          ) ** 2
          rho23_0 = ((n2 - n3) / (n2 + n3)) ** 2
  
          # transmittance through the coating, including internal reflections
          # 1 + rho23*rho12 + (rho23*rho12)^2 + ... = 1/(1 - rho23*rho12)
          tau_s *= (1 - rho23_s) / (1 - rho23_s * rho12_s)
          tau_p *= (1 - rho23_p) / (1 - rho23_p * rho12_p)
          tau_0 *= (1 - rho23_0) / (1 - rho23_0 * rho12_0)
  
      # transmittance after absorption in the glass
      tau_s *= np.exp(-K * L / costheta)
      tau_p *= np.exp(-K * L / costheta)
      tau_0 *= np.exp(-K * L)
  
      # incidence angle modifier
      iam = (tau_s + tau_p) / 2 / tau_0
  
      return iam
  
  
  def martin_ruiz(aoi, a_r=0.16):
      r'''
      Determine the incidence angle modifier (IAM) using the Martin
      and Ruiz incident angle model.
  
      Parameters
      ----------
      aoi : numeric, degrees
          The angle of incidence between the module normal vector and the
          sun-beam vector in degrees.
  
      a_r : numeric
          The angular losses coefficient described in equation 3 of [1]_.
          This is an empirical dimensionless parameter. Values of ``a_r`` are
          generally on the order of 0.08 to 0.25 for flat-plate PV modules.
  
      Returns
      -------
      iam : numeric
          The incident angle modifier(s)
  
      Notes
      -----
      `martin_ruiz` calculates the incidence angle modifier (IAM) as described in
      [1]_. The information required is the incident angle (AOI) and the angular
      losses coefficient (a_r). Note that [1]_ has a corrigendum [2]_ which
      clarifies a mix-up of 'alpha's and 'a's in the former.
  
      The incident angle modifier is defined as
  
      .. math::
  
         IAM = \frac{1 - \exp(-\frac{\cos(aoi)}{a_r})}
         {1 - \exp(\frac{-1}{a_r})}
  
      which is presented as :math:`AL(\alpha) = 1 - IAM` in equation 4 of [1]_,
      with :math:`\alpha` representing the angle of incidence AOI. Thus IAM = 1
      at AOI = 0, and IAM = 0 at AOI = 90.  This equation is only valid for
      -90 <= aoi <= 90, therefore `iam` is constrained to 0.0 outside this
      interval.
  
      References
      ----------
      .. [1] N. Martin and J. M. Ruiz, "Calculation of the PV modules angular
         losses under field conditions by means of an analytical model", Solar
         Energy Materials & Solar Cells, vol. 70, pp. 25-38, 2001.
  
      .. [2] N. Martin and J. M. Ruiz, "Corrigendum to 'Calculation of the PV
         modules angular losses under field conditions by means of an
         analytical model'", Solar Energy Materials & Solar Cells, vol. 110,
         pp. 154, 2013.
  
      See Also
      --------
      pvlib.iam.martin_ruiz_diffuse
      pvlib.iam.physical
      pvlib.iam.ashrae
      pvlib.iam.interp
      pvlib.iam.sapm
      '''
      # Contributed by Anton Driesse (@adriesse), PV Performance Labs. July, 2019
  
      aoi_input = aoi
  
      aoi = np.asanyarray(aoi)
      a_r = np.asanyarray(a_r)
  
      if np.any(np.less_equal(a_r, 0)):
          raise ValueError("The parameter 'a_r' cannot be zero or negative.")
  
      with np.errstate(invalid='ignore'):
          iam = (1 - np.exp(-cosd(aoi) / a_r)) / (1 - np.exp(-1 / a_r))
          iam = np.where(np.abs(aoi) >= 90.0, 0.0, iam)
  
      if isinstance(aoi_input, pd.Series):
          iam = pd.Series(iam, index=aoi_input.index)
  
      return iam
  
  
  def martin_ruiz_diffuse(surface_tilt, a_r=0.16, c1=0.4244, c2=None):
      '''
      Determine the incidence angle modifiers (iam) for diffuse sky and
      ground-reflected irradiance using the Martin and Ruiz incident angle model.
  
      Parameters
      ----------
      surface_tilt: float or array-like, default 0
          Surface tilt angles in decimal degrees.
          The tilt angle is defined as degrees from horizontal
          (e.g. surface facing up = 0, surface facing horizon = 90)
          surface_tilt must be in the range [0, 180]
  
      a_r : numeric
          The angular losses coefficient described in equation 3 of [1]_.
          This is an empirical dimensionless parameter. Values of a_r are
          generally on the order of 0.08 to 0.25 for flat-plate PV modules.
          a_r must be greater than zero.
  
      c1 : float
          First fitting parameter for the expressions that approximate the
          integral of diffuse irradiance coming from different directions.
          c1 is given as the constant 4 / 3 / pi (0.4244) in [1]_.
  
      c2 : float
          Second fitting parameter for the expressions that approximate the
          integral of diffuse irradiance coming from different directions.
          If c2 is None, it will be calculated according to the linear
          relationship given in [3]_.
  
      Returns
      -------
      iam_sky : numeric
          The incident angle modifier for sky diffuse
  
      iam_ground : numeric
          The incident angle modifier for ground-reflected diffuse
  
      Notes
      -----
      Sky and ground modifiers are complementary: iam_sky for tilt = 30 is
      equal to iam_ground for tilt = 180 - 30.  For vertical surfaces,
      tilt = 90, the two factors are equal.
  
      References
      ----------
      .. [1] N. Martin and J. M. Ruiz, "Calculation of the PV modules angular
         losses under field conditions by means of an analytical model", Solar
         Energy Materials & Solar Cells, vol. 70, pp. 25-38, 2001.
  
      .. [2] N. Martin and J. M. Ruiz, "Corrigendum to 'Calculation of the PV
         modules angular losses under field conditions by means of an
         analytical model'", Solar Energy Materials & Solar Cells, vol. 110,
         pp. 154, 2013.
  
      .. [3] "IEC 61853-3 Photovoltaic (PV) module performance testing and energy
         rating - Part 3: Energy rating of PV modules". IEC, Geneva, 2018.
  
      See Also
      --------
      pvlib.iam.martin_ruiz
      pvlib.iam.physical
      pvlib.iam.ashrae
      pvlib.iam.interp
      pvlib.iam.sapm
      '''
      # Contributed by Anton Driesse (@adriesse), PV Performance Labs. Oct. 2019
  
      if isinstance(surface_tilt, pd.Series):
          out_index = surface_tilt.index
      else:
          out_index = None
  
      surface_tilt = np.asanyarray(surface_tilt)
  
      # avoid undefined results for horizontal or upside-down surfaces
      zeroang = 1e-06
  
      surface_tilt = np.where(surface_tilt == 0, zeroang, surface_tilt)
      surface_tilt = np.where(surface_tilt == 180, 180 - zeroang, surface_tilt)
  
      if c2 is None:
          # This equation is from [3] Sect. 7.2
          c2 = 0.5 * a_r - 0.154
  
      beta = np.radians(surface_tilt)
      sin = np.sin
      pi = np.pi
      cos = np.cos
  
      # avoid RuntimeWarnings for <, sin, and cos with nan
      with np.errstate(invalid='ignore'):
          # because sin(pi) isn't exactly zero
          sin_beta = np.where(surface_tilt < 90, sin(beta), sin(pi - beta))
  
          trig_term_sky = sin_beta + (pi - beta - sin_beta) / (1 + cos(beta))
          trig_term_gnd = sin_beta +      (beta - sin_beta) / (1 - cos(beta))  # noqa: E222 E261 E501
  
      iam_sky = 1 - np.exp(-(c1 + c2 * trig_term_sky) * trig_term_sky / a_r)
      iam_gnd = 1 - np.exp(-(c1 + c2 * trig_term_gnd) * trig_term_gnd / a_r)
  
      if out_index is not None:
          iam_sky = pd.Series(iam_sky, index=out_index, name='iam_sky')
          iam_gnd = pd.Series(iam_gnd, index=out_index, name='iam_ground')
  
      return iam_sky, iam_gnd
  
  
  def interp(aoi, theta_ref, iam_ref, method='linear', normalize=True):
      r'''
      Determine the incidence angle modifier (IAM) by interpolating a set of
      reference values, which are usually measured values.
  
      Parameters
      ----------
      aoi : numeric
          The angle of incidence between the module normal vector and the
          sun-beam vector [degrees].
  
      theta_ref : numeric
          Vector of angles at which the IAM is known [degrees].
  
      iam_ref : numeric
          IAM values for each angle in ``theta_ref`` [unitless].
  
      method : str, default 'linear'
          Specifies the interpolation method.
          Useful options are: 'linear', 'quadratic', 'cubic'.
          See scipy.interpolate.interp1d for more options.
  
      normalize : boolean, default True
          When true, the interpolated values are divided by the interpolated
          value at zero degrees.  This ensures that ``iam=1.0`` at normal
          incidence.
  
      Returns
      -------
      iam : numeric
          The incident angle modifier(s) [unitless]
  
      Notes
      -----
      ``theta_ref`` must have two or more points and may span any range of
      angles. Typically there will be a dozen or more points in the range 0-90
      degrees. Beyond the range of ``theta_ref``, IAM values are extrapolated,
      but constrained to be non-negative.
  
      The sign of ``aoi`` is ignored; only the magnitude is used.
  
      See Also
      --------
      pvlib.iam.physical
      pvlib.iam.ashrae
      pvlib.iam.martin_ruiz
      pvlib.iam.sapm
      '''
      # Contributed by Anton Driesse (@adriesse), PV Performance Labs. July, 2019
  
      from scipy.interpolate import interp1d
  
      # Scipy doesn't give the clearest feedback, so check number of points here.
      MIN_REF_VALS = {'linear': 2, 'quadratic': 3, 'cubic': 4, 1: 2, 2: 3, 3: 4}
  
      if len(theta_ref) < MIN_REF_VALS.get(method, 2):
          raise ValueError("Too few reference points defined "
                           "for interpolation method '%s'." % method)
  
      if np.any(np.less(iam_ref, 0)):
          raise ValueError("Negative value(s) found in 'iam_ref'. "
                           "This is not physically possible.")
  
      interpolator = interp1d(theta_ref, iam_ref, kind=method,
                              fill_value='extrapolate')
      aoi_input = aoi
  
      aoi = np.asanyarray(aoi)
      aoi = np.abs(aoi)
      iam = interpolator(aoi)
      iam = np.clip(iam, 0, None)
  
      if normalize:
          iam /= interpolator(0)
  
      if isinstance(aoi_input, pd.Series):
          iam = pd.Series(iam, index=aoi_input.index)
  
      return iam
  
  
  def sapm(aoi, module, upper=None):
      r"""
      Determine the incidence angle modifier (IAM) using the SAPM model.
  
      Parameters
      ----------
      aoi : numeric
          Angle of incidence in degrees. Negative input angles will return
          zeros.
  
      module : dict-like
          A dict or Series with the SAPM IAM model parameters.
          See the :py:func:`sapm` notes section for more details.
  
      upper : None or float, default None
          Upper limit on the results.
  
      Returns
      -------
      iam : numeric
          The SAPM angle of incidence loss coefficient, termed F2 in [1]_.
  
      Notes
      -----
      The SAPM [1]_ traditionally does not define an upper limit on the AOI
      loss function and values slightly exceeding 1 may exist for moderate
      angles of incidence (15-40 degrees). However, users may consider
      imposing an upper limit of 1.
  
      References
      ----------
      .. [1] King, D. et al, 2004, "Sandia Photovoltaic Array Performance
         Model", SAND Report 3535, Sandia National Laboratories, Albuquerque,
         NM.
  
      .. [2] B.H. King et al, "Procedure to Determine Coefficients for the
         Sandia Array Performance Model (SAPM)," SAND2016-5284, Sandia
         National Laboratories (2016).
  
      .. [3] B.H. King et al, "Recent Advancements in Outdoor Measurement
         Techniques for Angle of Incidence Effects," 42nd IEEE PVSC (2015).
         DOI: 10.1109/PVSC.2015.7355849
  
      See Also
      --------
      pvlib.iam.physical
      pvlib.iam.ashrae
      pvlib.iam.martin_ruiz
      pvlib.iam.interp
      """
  
      aoi_coeff = [module['B5'], module['B4'], module['B3'], module['B2'],
                   module['B1'], module['B0']]
  
      iam = np.polyval(aoi_coeff, aoi)
      iam = np.clip(iam, 0, upper)
      # nan tolerant masking
      aoi_lt_0 = np.full_like(aoi, False, dtype='bool')
      np.less(aoi, 0, where=~np.isnan(aoi), out=aoi_lt_0)
      iam = np.where(aoi_lt_0, 0, iam)
  
      if isinstance(aoi, pd.Series):
          iam = pd.Series(iam, aoi.index)
  
      return iam
  
  
  def marion_diffuse(model, surface_tilt, **kwargs):
      """
      Determine diffuse irradiance incidence angle modifiers using Marion's
      method of integrating over solid angle.
  
      Parameters
      ----------
      model : str
          The IAM function to evaluate across solid angle. Must be one of
          `'ashrae', 'physical', 'martin_ruiz', 'sapm', 'schlick'`.
  
      surface_tilt : numeric
          Surface tilt angles in decimal degrees.
          The tilt angle is defined as degrees from horizontal
          (e.g. surface facing up = 0, surface facing horizon = 90).
  
      **kwargs
          Extra parameters passed to the IAM function.
  
      Returns
      -------
      iam : dict
          IAM values for each type of diffuse irradiance:
  
              * 'sky': radiation from the sky dome (zenith <= 90)
              * 'horizon': radiation from the region of the sky near the horizon
                (89.5 <= zenith <= 90)
              * 'ground': radiation reflected from the ground (zenith >= 90)
  
          See [1]_ for a detailed description of each class.
  
      See Also
      --------
      pvlib.iam.marion_integrate
  
      References
      ----------
      .. [1] B. Marion "Numerical method for angle-of-incidence correction
         factors for diffuse radiation incident photovoltaic modules",
         Solar Energy, Volume 147, Pages 344-348. 2017.
         DOI: 10.1016/j.solener.2017.03.027
  
      Examples
      --------
      >>> marion_diffuse('physical', surface_tilt=20)
      {'sky': 0.9539178294437575,
       'horizon': 0.7652650139134007,
       'ground': 0.6387140117795903}
  
      >>> marion_diffuse('ashrae', [20, 30], b=0.04)
      {'sky': array([0.96748999, 0.96938408]),
       'horizon': array([0.86478428, 0.91825792]),
       'ground': array([0.77004435, 0.8522436 ])}
      """
  
      models = {
          'physical': physical,
          'ashrae': ashrae,
          'sapm': sapm,
          'martin_ruiz': martin_ruiz,
          'schlick': schlick,
      }
  
      try:
          iam_model = models[model]
      except KeyError:
          raise ValueError('model must be one of: ' + str(list(models.keys())))
  
      iam_function = functools.partial(iam_model, **kwargs)
      iam = {}
      for region in ['sky', 'horizon', 'ground']:
          iam[region] = marion_integrate(iam_function, surface_tilt, region)
  
      return iam
  
  
  def marion_integrate(function, surface_tilt, region, num=None):
      """
      Integrate an incidence angle modifier (IAM) function over solid angle
      to determine a diffuse irradiance correction factor using Marion's method.
  
      This lower-level function actually performs the IAM integration for the
      specified solid angle region.
  
      Parameters
      ----------
      function : callable(aoi)
          The IAM function to evaluate across solid angle. The function must
          be vectorized and take only one parameter, the angle of incidence in
          degrees.
  
      surface_tilt : numeric
          Surface tilt angles in decimal degrees.
          The tilt angle is defined as degrees from horizontal
          (e.g. surface facing up = 0, surface facing horizon = 90).
  
      region : {'sky', 'horizon', 'ground'}
          The region to integrate over. Must be one of:
  
              * 'sky': radiation from the sky dome (zenith <= 90)
              * 'horizon': radiation from the region of the sky near the horizon
                (89.5 <= zenith <= 90)
              * 'ground': radiation reflected from the ground (zenith >= 90)
  
          See [1]_ for a detailed description of each class.
  
      num : int, optional
          The number of increments in the zenith integration.
          If not specified, N will follow the values used in [1]_:
  
              * 'sky' or 'ground': num = 180
              * 'horizon': num = 1800
  
      Returns
      -------
      iam : numeric
          AOI diffuse correction factor for the specified region.
  
      See Also
      --------
      pvlib.iam.marion_diffuse
  
      References
      ----------
      .. [1] B. Marion "Numerical method for angle-of-incidence correction
         factors for diffuse radiation incident photovoltaic modules",
         Solar Energy, Volume 147, Pages 344-348. 2017.
         DOI: 10.1016/j.solener.2017.03.027
  
      Examples
      --------
      >>> marion_integrate(pvlib.iam.ashrae, 20, 'sky')
      0.9596085829811408
  
      >>> from functools import partial
      >>> f = partial(pvlib.iam.physical, n=1.3)
      >>> marion_integrate(f, [20, 30], 'sky')
      array([0.96225034, 0.9653219 ])
      """
  
      if num is None:
          if region in ['sky', 'ground']:
              num = 180
          elif region == 'horizon':
              num = 1800
          else:
              raise ValueError(f'Invalid region: {region}')
  
      beta = np.radians(surface_tilt)
      if isinstance(beta, pd.Series):
          # convert Series to np array for broadcasting later
          beta = beta.values
      ai = np.pi/num  # angular increment
  
      phi_range = np.linspace(0, np.pi, num, endpoint=False)
      psi_range = np.linspace(0, 2*np.pi, 2*num, endpoint=False)
  
      # the pseudocode in [1] do these checks at the end, but it's
      # faster to do this criteria check up front instead of later.
      if region == 'sky':
          mask = phi_range + ai <= np.pi/2
      elif region == 'horizon':
          lo = 89.5 * np.pi/180
          hi = np.pi/2
          mask = (lo <= phi_range) & (phi_range + ai <= hi)
      elif region == 'ground':
          mask = (phi_range >= np.pi/2)
      else:
          raise ValueError(f'Invalid region: {region}')
      phi_range = phi_range[mask]
  
      # fast Cartesian product of phi and psi
      angles = np.array(np.meshgrid(phi_range, psi_range)).T.reshape(-1, 2)
      # index with single-element lists to maintain 2nd dimension so that
      # these angle arrays broadcast across the beta array
      phi_1 = angles[:, [0]]
      psi_1 = angles[:, [1]]
      phi_2 = phi_1 + ai
      # psi_2 = psi_1 + ai  # not needed
      phi_avg = phi_1 + 0.5*ai
      psi_avg = psi_1 + 0.5*ai
      term_1 = np.cos(beta) * np.cos(phi_avg)
      # The AOI formula includes a term based on the difference between
      # panel azimuth and the photon azimuth, but because we assume each class
      # of diffuse irradiance is isotropic and we are integrating over all
      # angles, it doesn't matter what panel azimuth we choose (i.e., the
      # system is rotationally invariant).  So we choose gamma to be zero so
      # that we can omit it from the cos(psi_avg) term.
      # Marion's paper mentions this in the Section 3 pseudocode:
      # "set gamma to pi (or any value between 0 and 2pi)"
      term_2 = np.sin(beta) * np.sin(phi_avg) * np.cos(psi_avg)
      cosaoi = term_1 + term_2
      aoi = np.arccos(cosaoi)
      # simplify Eq 8, (psi_2 - psi_1) is always ai
      dAs = ai * (np.cos(phi_1) - np.cos(phi_2))
      cosaoi_dAs = cosaoi * dAs
      # apply the final AOI check, zeroing out non-passing points
      mask = aoi < np.pi/2
      cosaoi_dAs = np.where(mask, cosaoi_dAs, 0)
      numerator = np.sum(function(np.degrees(aoi)) * cosaoi_dAs, axis=0)
      denominator = np.sum(cosaoi_dAs, axis=0)
  
      with np.errstate(invalid='ignore'):
          # in some cases, no points pass the criteria
          # (e.g. region='ground', surface_tilt=0), so we override the division
          # by zero to set Fd=0.  Also, preserve nans in beta.
          Fd = np.where((denominator != 0) | ~np.isfinite(beta),
                        numerator / denominator,
                        0)
  
      # preserve input type
      if np.isscalar(surface_tilt):
          Fd = Fd.item()
      elif isinstance(surface_tilt, pd.Series):
          Fd = pd.Series(Fd, surface_tilt.index)
  
      return Fd
  
  
  def schlick(aoi):
      """
      Determine incidence angle modifier (IAM) for direct irradiance using the
      Schlick approximation to the Fresnel equations.
  
      The Schlick approximation was proposed in [1]_ as a computationally
      efficient alternative to computing the Fresnel factor in computer
      graphics contexts.  This implementation is a normalized form of the
      equation in [1]_ so that it can be used as a PV IAM model.
      Unlike other IAM models, this model has no ability to describe
      different reflection profiles.
  
      In PV contexts, the Schlick approximation has been used as an analytically
      integrable alternative to the Fresnel equations for estimating IAM
      for diffuse irradiance [2]_.
  
      Parameters
      ----------
      aoi : numeric
          The angle of incidence (AOI) between the module normal vector and the
          sun-beam vector. Angles of nan will result in nan. [degrees]
  
      Returns
      -------
      iam : numeric
          The incident angle modifier.
  
      References
      ----------
      .. [1] Schlick, C. An inexpensive BRDF model for physically-based
         rendering. Computer graphics forum 13 (1994).
  
      .. [2] Xie, Y., M. Sengupta, A. Habte, A. Andreas, "The 'Fresnel Equations'
         for Diffuse radiation on Inclined photovoltaic Surfaces (FEDIS)",
         Renewable and Sustainable Energy Reviews, vol. 161, 112362. June 2022.
         :doi:`10.1016/j.rser.2022.112362`
  
      See Also
      --------
      pvlib.iam.schlick_diffuse
      """
      iam = 1 - (1 - cosd(aoi)) ** 5
      iam = np.where(np.abs(aoi) >= 90.0, 0.0, iam)
  
      # preserve input type
      if np.isscalar(aoi):
          iam = iam.item()
      elif isinstance(aoi, pd.Series):
          iam = pd.Series(iam, aoi.index)
  
      return iam
  
  
  def schlick_diffuse(surface_tilt):
      """
      Determine the incidence angle modifiers (IAM) for diffuse sky and
      ground-reflected irradiance on a tilted surface using the Schlick
      incident angle model.
  
      The diffuse iam values are calculated using an analytical integration
      of the Schlick equation [1]_ over the portion of an isotropic sky and
      isotropic foreground that is visible from the tilted surface [2]_.
  
      Parameters
      ----------
      surface_tilt : numeric
          Surface tilt angle measured from horizontal (e.g. surface facing
          up = 0, surface facing horizon = 90). [degrees]
  
      Returns
      -------
      iam_sky : numeric
          The incident angle modifier for sky diffuse.
  
      iam_ground : numeric
          The incident angle modifier for ground-reflected diffuse.
  
      References
      ----------
      .. [1] Schlick, C. An inexpensive BRDF model for physically-based
         rendering. Computer graphics forum 13 (1994).
  
      .. [2] Xie, Y., M. Sengupta, A. Habte, A. Andreas, "The 'Fresnel Equations'
         for Diffuse radiation on Inclined photovoltaic Surfaces (FEDIS)",
         Renewable and Sustainable Energy Reviews, vol. 161, 112362. June 2022.
         :doi:`10.1016/j.rser.2022.112362`
  
      See Also
      --------
      pvlib.iam.schlick
      """
      # these calculations are as in [2]_, but with the refractive index
      # weighting coefficient w set to 1.0 (so it is omitted)
  
      # relative transmittance of sky diffuse radiation by PV cover:
      cosB = cosd(surface_tilt)
      sinB = sind(surface_tilt)
      cuk = (2 / (np.pi * (1 + cosB))) * (
          (30/7)*np.pi - (160/21)*np.radians(surface_tilt) - (10/3)*np.pi*cosB
          + (160/21)*cosB*sinB - (5/3)*np.pi*cosB*sinB**2 + (20/7)*cosB*sinB**3
          - (5/16)*np.pi*cosB*sinB**4 + (16/105)*cosB*sinB**5
      )  # Eq 4 in [2]
  
      # relative transmittance of ground-reflected radiation by PV cover:
      with np.errstate(divide='ignore', invalid='ignore'):  # Eq 6 in [2]
          cug = 40 / (21 * (1 - cosB)) - (1 + cosB) / (1 - cosB) * cuk
  
      cug = np.where(surface_tilt < 1e-6, 0, cug)
  
      # respect input types:
      if np.isscalar(surface_tilt):
          cuk = cuk.item()
          cug = cug.item()
      elif isinstance(surface_tilt, pd.Series):
          cuk = pd.Series(cuk, surface_tilt.index)
          cug = pd.Series(cug, surface_tilt.index)
  
      return cuk, cug
  
  ```
  --- END FILE ---
  
  Please first localize the bug based on the issue statement, and then generate *SEARCH/REPLACE* edits to fix the issue.
  
  Every *SEARCH/REPLACE* edit must use this format:
  1. The file path
  2. The start of search block: <<<<<<< SEARCH
  3. A contiguous chunk of lines to search for in the existing source code
  4. The dividing line: =======
  5. The lines to replace into the source code
  6. The end of the replace block: >>>>>>> REPLACE
  
  Here is an example:
  
  ```python
  ### mathweb/flask/app.py
  <<<<<<< SEARCH
  from flask import Flask
  =======
  import math
  from flask import Flask
  >>>>>>> REPLACE
  ```
  
  Please note that the *SEARCH/REPLACE* edit REQUIRES PROPER INDENTATION. If you would like to add the line '        print(x)', you must fully write that out, with all those spaces before the code!
  Wrap the *SEARCH/REPLACE* edit in blocks ```python...```.
  
messages.role: user
verification_info: {"file_context": {"pvlib/iam.py": "r\"\"\"\nThe ``iam`` module contains functions that implement models for the incidence\nangle modifier (IAM). The IAM quantifies the fraction of direct irradiance on\na module's front surface that is transmitted through the module materials to\nthe cells. Stated differently, the quantity 1 - IAM is the fraction of direct\nirradiance that is reflected away or absorbed by the module's front materials.\nIAM is typically a function of the angle of incidence (AOI) of the direct\nirradiance to the module's surface.\n\"\"\"\n\nimport numpy as np\nimport pandas as pd\nimport functools\nfrom pvlib.tools import cosd, sind\n\n# a dict of required parameter names for each IAM model\n# keys are the function names for the IAM models\n_IAM_MODEL_PARAMS = {\n    'ashrae': {'b'},\n    'physical': {'n', 'K', 'L'},\n    'martin_ruiz': {'a_r'},\n    'sapm': {'B0', 'B1', 'B2', 'B3', 'B4', 'B5'},\n    'interp': set()\n}\n\n\ndef ashrae(aoi, b=0.05):\n    r\"\"\"\n    Determine the incidence angle modifier using the ASHRAE transmission\n    model.\n\n    The ASHRAE (American Society of Heating, Refrigeration, and Air\n    Conditioning Engineers) transmission model is developed in\n    [1]_, and in [2]_. The model has been used in software such as PVSyst [3]_.\n\n    Parameters\n    ----------\n    aoi : numeric\n        The angle of incidence (AOI) between the module normal vector and the\n        sun-beam vector in degrees. Angles of nan will result in nan.\n\n    b : float, default 0.05\n        A parameter to adjust the incidence angle modifier as a function of\n        angle of incidence. Typical values are on the order of 0.05 [3].\n\n    Returns\n    -------\n    iam : numeric\n        The incident angle modifier (IAM). Returns zero for all abs(aoi) >= 90\n        and for all ``iam`` values that would be less than 0.\n\n    Notes\n    -----\n    The incidence angle modifier is calculated as\n\n    .. math::\n\n        IAM = 1 - b (\\sec(aoi) - 1)\n\n    As AOI approaches 90 degrees, the model yields negative values for IAM;\n    negative IAM values are set to zero in this implementation.\n\n    References\n    ----------\n    .. [1] Souka A.F., Safwat H.H., \"Determination of the optimum\n       orientations for the double exposure flat-plate collector and its\n       reflections\". Solar Energy vol .10, pp 170-174. 1966.\n\n    .. [2] ASHRAE standard 93-77\n\n    .. [3] PVsyst Contextual Help.\n       https://files.pvsyst.com/help/index.html?iam_loss.htm retrieved on\n       October 14, 2019\n\n    See Also\n    --------\n    pvlib.iam.physical\n    pvlib.iam.martin_ruiz\n    pvlib.iam.interp\n    \"\"\"\n\n    iam = 1 - b * (1 / np.cos(np.radians(aoi)) - 1)\n    aoi_gte_90 = np.full_like(aoi, False, dtype='bool')\n    np.greater_equal(np.abs(aoi), 90, where=~np.isnan(aoi), out=aoi_gte_90)\n    iam = np.where(aoi_gte_90, 0, iam)\n    iam = np.maximum(0, iam)\n\n    if isinstance(aoi, pd.Series):\n        iam = pd.Series(iam, index=aoi.index)\n\n    return iam\n\n\ndef physical(aoi, n=1.526, K=4.0, L=0.002, *, n_ar=None):\n    r\"\"\"\n    Determine the incidence angle modifier using refractive index ``n``,\n    extinction coefficient ``K``, glazing thickness ``L`` and refractive\n    index ``n_ar`` of an optional anti-reflective coating.\n\n    ``iam.physical`` calculates the incidence angle modifier as described in\n    [1]_, Section 3, with additional support of an anti-reflective coating.\n    The calculation is based on a physical model of reflections, absorption,\n    and transmission through a transparent cover.\n\n    Parameters\n    ----------\n    aoi : numeric\n        The angle of incidence between the module normal vector and the\n        sun-beam vector in degrees. Angles of nan will result in nan.\n\n    n : numeric, default 1.526\n        The effective index of refraction (unitless). Reference [1]_\n        indicates that a value of 1.526 is acceptable for glass.\n\n    K : numeric, default 4.0\n        The glazing extinction coefficient in units of 1/meters.\n        Reference [1] indicates that a value of 4 is reasonable for\n        \"water white\" glass.\n\n    L : numeric, default 0.002\n        The glazing thickness in units of meters. Reference [1]_\n        indicates that 0.002 meters (2 mm) is reasonable for most\n        glass-covered PV panels.\n\n    n_ar : numeric, optional\n        The effective index of refraction of the anti-reflective (AR) coating\n        (unitless). If n_ar is None (default), no AR coating is applied.\n        A typical value for the effective index of an AR coating is 1.29.\n\n    Returns\n    -------\n    iam : numeric\n        The incident angle modifier\n\n    Notes\n    -----\n    The pvlib python authors believe that Eqn. 14 in [1]_ is\n    incorrect, which presents :math:`\\theta_{r} = \\arcsin(n \\sin(AOI))`.\n    Here, :math:`\\theta_{r} = \\arcsin(1/n \\times \\sin(AOI))`\n\n    References\n    ----------\n    .. [1] W. De Soto et al., \"Improvement and validation of a model for\n       photovoltaic array performance\", Solar Energy, vol 80, pp. 78-88,\n       2006.\n\n    .. [2] Duffie, John A. & Beckman, William A.. (2006). Solar Engineering\n       of Thermal Processes, third edition. [Books24x7 version] Available\n       from http://common.books24x7.com/toc.aspx?bookid=17160.\n\n    See Also\n    --------\n    pvlib.iam.martin_ruiz\n    pvlib.iam.ashrae\n    pvlib.iam.interp\n    pvlib.iam.sapm\n    \"\"\"\n    n1, n3 = 1, n\n    if n_ar is None or np.allclose(n_ar, n1):\n        # no AR coating\n        n2 = n\n    else:\n        n2 = n_ar\n\n    # incidence angle\n    costheta = np.maximum(0, cosd(aoi))  # always >= 0\n    sintheta = np.sqrt(1 - costheta**2)  # always >= 0\n    n1costheta1 = n1 * costheta\n    n2costheta1 = n2 * costheta\n\n    # refraction angle of first interface\n    sintheta = n1 / n2 * sintheta\n    costheta = np.sqrt(1 - sintheta**2)\n    n1costheta2 = n1 * costheta\n    n2costheta2 = n2 * costheta\n\n    # reflectance of s-, p-polarized, and normal light by the first interface\n    rho12_s = ((n1costheta1 - n2costheta2) / (n1costheta1 + n2costheta2)) ** 2\n    rho12_p = ((n1costheta2 - n2costheta1) / (n1costheta2 + n2costheta1)) ** 2\n    rho12_0 = ((n1 - n2) / (n1 + n2)) ** 2\n\n    # transmittance through the first interface\n    tau_s = 1 - rho12_s\n    tau_p = 1 - rho12_p\n    tau_0 = 1 - rho12_0\n\n    if not np.allclose(n3, n2):  # AR coated glass\n        n3costheta2 = n3 * costheta\n        # refraction angle of second interface\n        sintheta = n2 / n3 * sintheta\n        costheta = np.sqrt(1 - sintheta**2)\n        n2costheta3 = n2 * costheta\n        n3costheta3 = n3 * costheta\n\n        # reflectance by the second interface\n        rho23_s = (\n            (n2costheta2 - n3costheta3) / (n2costheta2 + n3costheta3)\n        ) ** 2\n        rho23_p = (\n            (n2costheta3 - n3costheta2) / (n2costheta3 + n3costheta2)\n        ) ** 2\n        rho23_0 = ((n2 - n3) / (n2 + n3)) ** 2\n\n        # transmittance through the coating, including internal reflections\n        # 1 + rho23*rho12 + (rho23*rho12)^2 + ... = 1/(1 - rho23*rho12)\n        tau_s *= (1 - rho23_s) / (1 - rho23_s * rho12_s)\n        tau_p *= (1 - rho23_p) / (1 - rho23_p * rho12_p)\n        tau_0 *= (1 - rho23_0) / (1 - rho23_0 * rho12_0)\n\n    # transmittance after absorption in the glass\n    tau_s *= np.exp(-K * L / costheta)\n    tau_p *= np.exp(-K * L / costheta)\n    tau_0 *= np.exp(-K * L)\n\n    # incidence angle modifier\n    iam = (tau_s + tau_p) / 2 / tau_0\n\n    return iam\n\n\ndef martin_ruiz(aoi, a_r=0.16):\n    r'''\n    Determine the incidence angle modifier (IAM) using the Martin\n    and Ruiz incident angle model.\n\n    Parameters\n    ----------\n    aoi : numeric, degrees\n        The angle of incidence between the module normal vector and the\n        sun-beam vector in degrees.\n\n    a_r : numeric\n        The angular losses coefficient described in equation 3 of [1]_.\n        This is an empirical dimensionless parameter. Values of ``a_r`` are\n        generally on the order of 0.08 to 0.25 for flat-plate PV modules.\n\n    Returns\n    -------\n    iam : numeric\n        The incident angle modifier(s)\n\n    Notes\n    -----\n    `martin_ruiz` calculates the incidence angle modifier (IAM) as described in\n    [1]_. The information required is the incident angle (AOI) and the angular\n    losses coefficient (a_r). Note that [1]_ has a corrigendum [2]_ which\n    clarifies a mix-up of 'alpha's and 'a's in the former.\n\n    The incident angle modifier is defined as\n\n    .. math::\n\n       IAM = \\frac{1 - \\exp(-\\frac{\\cos(aoi)}{a_r})}\n       {1 - \\exp(\\frac{-1}{a_r})}\n\n    which is presented as :math:`AL(\\alpha) = 1 - IAM` in equation 4 of [1]_,\n    with :math:`\\alpha` representing the angle of incidence AOI. Thus IAM = 1\n    at AOI = 0, and IAM = 0 at AOI = 90.  This equation is only valid for\n    -90 <= aoi <= 90, therefore `iam` is constrained to 0.0 outside this\n    interval.\n\n    References\n    ----------\n    .. [1] N. Martin and J. M. Ruiz, \"Calculation of the PV modules angular\n       losses under field conditions by means of an analytical model\", Solar\n       Energy Materials & Solar Cells, vol. 70, pp. 25-38, 2001.\n\n    .. [2] N. Martin and J. M. Ruiz, \"Corrigendum to 'Calculation of the PV\n       modules angular losses under field conditions by means of an\n       analytical model'\", Solar Energy Materials & Solar Cells, vol. 110,\n       pp. 154, 2013.\n\n    See Also\n    --------\n    pvlib.iam.martin_ruiz_diffuse\n    pvlib.iam.physical\n    pvlib.iam.ashrae\n    pvlib.iam.interp\n    pvlib.iam.sapm\n    '''\n    # Contributed by Anton Driesse (@adriesse), PV Performance Labs. July, 2019\n\n    aoi_input = aoi\n\n    aoi = np.asanyarray(aoi)\n    a_r = np.asanyarray(a_r)\n\n    if np.any(np.less_equal(a_r, 0)):\n        raise ValueError(\"The parameter 'a_r' cannot be zero or negative.\")\n\n    with np.errstate(invalid='ignore'):\n        iam = (1 - np.exp(-cosd(aoi) / a_r)) / (1 - np.exp(-1 / a_r))\n        iam = np.where(np.abs(aoi) >= 90.0, 0.0, iam)\n\n    if isinstance(aoi_input, pd.Series):\n        iam = pd.Series(iam, index=aoi_input.index)\n\n    return iam\n\n\ndef martin_ruiz_diffuse(surface_tilt, a_r=0.16, c1=0.4244, c2=None):\n    '''\n    Determine the incidence angle modifiers (iam) for diffuse sky and\n    ground-reflected irradiance using the Martin and Ruiz incident angle model.\n\n    Parameters\n    ----------\n    surface_tilt: float or array-like, default 0\n        Surface tilt angles in decimal degrees.\n        The tilt angle is defined as degrees from horizontal\n        (e.g. surface facing up = 0, surface facing horizon = 90)\n        surface_tilt must be in the range [0, 180]\n\n    a_r : numeric\n        The angular losses coefficient described in equation 3 of [1]_.\n        This is an empirical dimensionless parameter. Values of a_r are\n        generally on the order of 0.08 to 0.25 for flat-plate PV modules.\n        a_r must be greater than zero.\n\n    c1 : float\n        First fitting parameter for the expressions that approximate the\n        integral of diffuse irradiance coming from different directions.\n        c1 is given as the constant 4 / 3 / pi (0.4244) in [1]_.\n\n    c2 : float\n        Second fitting parameter for the expressions that approximate the\n        integral of diffuse irradiance coming from different directions.\n        If c2 is None, it will be calculated according to the linear\n        relationship given in [3]_.\n\n    Returns\n    -------\n    iam_sky : numeric\n        The incident angle modifier for sky diffuse\n\n    iam_ground : numeric\n        The incident angle modifier for ground-reflected diffuse\n\n    Notes\n    -----\n    Sky and ground modifiers are complementary: iam_sky for tilt = 30 is\n    equal to iam_ground for tilt = 180 - 30.  For vertical surfaces,\n    tilt = 90, the two factors are equal.\n\n    References\n    ----------\n    .. [1] N. Martin and J. M. Ruiz, \"Calculation of the PV modules angular\n       losses under field conditions by means of an analytical model\", Solar\n       Energy Materials & Solar Cells, vol. 70, pp. 25-38, 2001.\n\n    .. [2] N. Martin and J. M. Ruiz, \"Corrigendum to 'Calculation of the PV\n       modules angular losses under field conditions by means of an\n       analytical model'\", Solar Energy Materials & Solar Cells, vol. 110,\n       pp. 154, 2013.\n\n    .. [3] \"IEC 61853-3 Photovoltaic (PV) module performance testing and energy\n       rating - Part 3: Energy rating of PV modules\". IEC, Geneva, 2018.\n\n    See Also\n    --------\n    pvlib.iam.martin_ruiz\n    pvlib.iam.physical\n    pvlib.iam.ashrae\n    pvlib.iam.interp\n    pvlib.iam.sapm\n    '''\n    # Contributed by Anton Driesse (@adriesse), PV Performance Labs. Oct. 2019\n\n    if isinstance(surface_tilt, pd.Series):\n        out_index = surface_tilt.index\n    else:\n        out_index = None\n\n    surface_tilt = np.asanyarray(surface_tilt)\n\n    # avoid undefined results for horizontal or upside-down surfaces\n    zeroang = 1e-06\n\n    surface_tilt = np.where(surface_tilt == 0, zeroang, surface_tilt)\n    surface_tilt = np.where(surface_tilt == 180, 180 - zeroang, surface_tilt)\n\n    if c2 is None:\n        # This equation is from [3] Sect. 7.2\n        c2 = 0.5 * a_r - 0.154\n\n    beta = np.radians(surface_tilt)\n    sin = np.sin\n    pi = np.pi\n    cos = np.cos\n\n    # avoid RuntimeWarnings for <, sin, and cos with nan\n    with np.errstate(invalid='ignore'):\n        # because sin(pi) isn't exactly zero\n        sin_beta = np.where(surface_tilt < 90, sin(beta), sin(pi - beta))\n\n        trig_term_sky = sin_beta + (pi - beta - sin_beta) / (1 + cos(beta))\n        trig_term_gnd = sin_beta +      (beta - sin_beta) / (1 - cos(beta))  # noqa: E222 E261 E501\n\n    iam_sky = 1 - np.exp(-(c1 + c2 * trig_term_sky) * trig_term_sky / a_r)\n    iam_gnd = 1 - np.exp(-(c1 + c2 * trig_term_gnd) * trig_term_gnd / a_r)\n\n    if out_index is not None:\n        iam_sky = pd.Series(iam_sky, index=out_index, name='iam_sky')\n        iam_gnd = pd.Series(iam_gnd, index=out_index, name='iam_ground')\n\n    return iam_sky, iam_gnd\n\n\ndef interp(aoi, theta_ref, iam_ref, method='linear', normalize=True):\n    r'''\n    Determine the incidence angle modifier (IAM) by interpolating a set of\n    reference values, which are usually measured values.\n\n    Parameters\n    ----------\n    aoi : numeric\n        The angle of incidence between the module normal vector and the\n        sun-beam vector [degrees].\n\n    theta_ref : numeric\n        Vector of angles at which the IAM is known [degrees].\n\n    iam_ref : numeric\n        IAM values for each angle in ``theta_ref`` [unitless].\n\n    method : str, default 'linear'\n        Specifies the interpolation method.\n        Useful options are: 'linear', 'quadratic', 'cubic'.\n        See scipy.interpolate.interp1d for more options.\n\n    normalize : boolean, default True\n        When true, the interpolated values are divided by the interpolated\n        value at zero degrees.  This ensures that ``iam=1.0`` at normal\n        incidence.\n\n    Returns\n    -------\n    iam : numeric\n        The incident angle modifier(s) [unitless]\n\n    Notes\n    -----\n    ``theta_ref`` must have two or more points and may span any range of\n    angles. Typically there will be a dozen or more points in the range 0-90\n    degrees. Beyond the range of ``theta_ref``, IAM values are extrapolated,\n    but constrained to be non-negative.\n\n    The sign of ``aoi`` is ignored; only the magnitude is used.\n\n    See Also\n    --------\n    pvlib.iam.physical\n    pvlib.iam.ashrae\n    pvlib.iam.martin_ruiz\n    pvlib.iam.sapm\n    '''\n    # Contributed by Anton Driesse (@adriesse), PV Performance Labs. July, 2019\n\n    from scipy.interpolate import interp1d\n\n    # Scipy doesn't give the clearest feedback, so check number of points here.\n    MIN_REF_VALS = {'linear': 2, 'quadratic': 3, 'cubic': 4, 1: 2, 2: 3, 3: 4}\n\n    if len(theta_ref) < MIN_REF_VALS.get(method, 2):\n        raise ValueError(\"Too few reference points defined \"\n                         \"for interpolation method '%s'.\" % method)\n\n    if np.any(np.less(iam_ref, 0)):\n        raise ValueError(\"Negative value(s) found in 'iam_ref'. \"\n                         \"This is not physically possible.\")\n\n    interpolator = interp1d(theta_ref, iam_ref, kind=method,\n                            fill_value='extrapolate')\n    aoi_input = aoi\n\n    aoi = np.asanyarray(aoi)\n    aoi = np.abs(aoi)\n    iam = interpolator(aoi)\n    iam = np.clip(iam, 0, None)\n\n    if normalize:\n        iam /= interpolator(0)\n\n    if isinstance(aoi_input, pd.Series):\n        iam = pd.Series(iam, index=aoi_input.index)\n\n    return iam\n\n\ndef sapm(aoi, module, upper=None):\n    r\"\"\"\n    Determine the incidence angle modifier (IAM) using the SAPM model.\n\n    Parameters\n    ----------\n    aoi : numeric\n        Angle of incidence in degrees. Negative input angles will return\n        zeros.\n\n    module : dict-like\n        A dict or Series with the SAPM IAM model parameters.\n        See the :py:func:`sapm` notes section for more details.\n\n    upper : None or float, default None\n        Upper limit on the results.\n\n    Returns\n    -------\n    iam : numeric\n        The SAPM angle of incidence loss coefficient, termed F2 in [1]_.\n\n    Notes\n    -----\n    The SAPM [1]_ traditionally does not define an upper limit on the AOI\n    loss function and values slightly exceeding 1 may exist for moderate\n    angles of incidence (15-40 degrees). However, users may consider\n    imposing an upper limit of 1.\n\n    References\n    ----------\n    .. [1] King, D. et al, 2004, \"Sandia Photovoltaic Array Performance\n       Model\", SAND Report 3535, Sandia National Laboratories, Albuquerque,\n       NM.\n\n    .. [2] B.H. King et al, \"Procedure to Determine Coefficients for the\n       Sandia Array Performance Model (SAPM),\" SAND2016-5284, Sandia\n       National Laboratories (2016).\n\n    .. [3] B.H. King et al, \"Recent Advancements in Outdoor Measurement\n       Techniques for Angle of Incidence Effects,\" 42nd IEEE PVSC (2015).\n       DOI: 10.1109/PVSC.2015.7355849\n\n    See Also\n    --------\n    pvlib.iam.physical\n    pvlib.iam.ashrae\n    pvlib.iam.martin_ruiz\n    pvlib.iam.interp\n    \"\"\"\n\n    aoi_coeff = [module['B5'], module['B4'], module['B3'], module['B2'],\n                 module['B1'], module['B0']]\n\n    iam = np.polyval(aoi_coeff, aoi)\n    iam = np.clip(iam, 0, upper)\n    # nan tolerant masking\n    aoi_lt_0 = np.full_like(aoi, False, dtype='bool')\n    np.less(aoi, 0, where=~np.isnan(aoi), out=aoi_lt_0)\n    iam = np.where(aoi_lt_0, 0, iam)\n\n    if isinstance(aoi, pd.Series):\n        iam = pd.Series(iam, aoi.index)\n\n    return iam\n\n\ndef marion_diffuse(model, surface_tilt, **kwargs):\n    \"\"\"\n    Determine diffuse irradiance incidence angle modifiers using Marion's\n    method of integrating over solid angle.\n\n    Parameters\n    ----------\n    model : str\n        The IAM function to evaluate across solid angle. Must be one of\n        `'ashrae', 'physical', 'martin_ruiz', 'sapm', 'schlick'`.\n\n    surface_tilt : numeric\n        Surface tilt angles in decimal degrees.\n        The tilt angle is defined as degrees from horizontal\n        (e.g. surface facing up = 0, surface facing horizon = 90).\n\n    **kwargs\n        Extra parameters passed to the IAM function.\n\n    Returns\n    -------\n    iam : dict\n        IAM values for each type of diffuse irradiance:\n\n            * 'sky': radiation from the sky dome (zenith <= 90)\n            * 'horizon': radiation from the region of the sky near the horizon\n              (89.5 <= zenith <= 90)\n            * 'ground': radiation reflected from the ground (zenith >= 90)\n\n        See [1]_ for a detailed description of each class.\n\n    See Also\n    --------\n    pvlib.iam.marion_integrate\n\n    References\n    ----------\n    .. [1] B. Marion \"Numerical method for angle-of-incidence correction\n       factors for diffuse radiation incident photovoltaic modules\",\n       Solar Energy, Volume 147, Pages 344-348. 2017.\n       DOI: 10.1016/j.solener.2017.03.027\n\n    Examples\n    --------\n    >>> marion_diffuse('physical', surface_tilt=20)\n    {'sky': 0.9539178294437575,\n     'horizon': 0.7652650139134007,\n     'ground': 0.6387140117795903}\n\n    >>> marion_diffuse('ashrae', [20, 30], b=0.04)\n    {'sky': array([0.96748999, 0.96938408]),\n     'horizon': array([0.86478428, 0.91825792]),\n     'ground': array([0.77004435, 0.8522436 ])}\n    \"\"\"\n\n    models = {\n        'physical': physical,\n        'ashrae': ashrae,\n        'sapm': sapm,\n        'martin_ruiz': martin_ruiz,\n        'schlick': schlick,\n    }\n\n    try:\n        iam_model = models[model]\n    except KeyError:\n        raise ValueError('model must be one of: ' + str(list(models.keys())))\n\n    iam_function = functools.partial(iam_model, **kwargs)\n    iam = {}\n    for region in ['sky', 'horizon', 'ground']:\n        iam[region] = marion_integrate(iam_function, surface_tilt, region)\n\n    return iam\n\n\ndef marion_integrate(function, surface_tilt, region, num=None):\n    \"\"\"\n    Integrate an incidence angle modifier (IAM) function over solid angle\n    to determine a diffuse irradiance correction factor using Marion's method.\n\n    This lower-level function actually performs the IAM integration for the\n    specified solid angle region.\n\n    Parameters\n    ----------\n    function : callable(aoi)\n        The IAM function to evaluate across solid angle. The function must\n        be vectorized and take only one parameter, the angle of incidence in\n        degrees.\n\n    surface_tilt : numeric\n        Surface tilt angles in decimal degrees.\n        The tilt angle is defined as degrees from horizontal\n        (e.g. surface facing up = 0, surface facing horizon = 90).\n\n    region : {'sky', 'horizon', 'ground'}\n        The region to integrate over. Must be one of:\n\n            * 'sky': radiation from the sky dome (zenith <= 90)\n            * 'horizon': radiation from the region of the sky near the horizon\n              (89.5 <= zenith <= 90)\n            * 'ground': radiation reflected from the ground (zenith >= 90)\n\n        See [1]_ for a detailed description of each class.\n\n    num : int, optional\n        The number of increments in the zenith integration.\n        If not specified, N will follow the values used in [1]_:\n\n            * 'sky' or 'ground': num = 180\n            * 'horizon': num = 1800\n\n    Returns\n    -------\n    iam : numeric\n        AOI diffuse correction factor for the specified region.\n\n    See Also\n    --------\n    pvlib.iam.marion_diffuse\n\n    References\n    ----------\n    .. [1] B. Marion \"Numerical method for angle-of-incidence correction\n       factors for diffuse radiation incident photovoltaic modules\",\n       Solar Energy, Volume 147, Pages 344-348. 2017.\n       DOI: 10.1016/j.solener.2017.03.027\n\n    Examples\n    --------\n    >>> marion_integrate(pvlib.iam.ashrae, 20, 'sky')\n    0.9596085829811408\n\n    >>> from functools import partial\n    >>> f = partial(pvlib.iam.physical, n=1.3)\n    >>> marion_integrate(f, [20, 30], 'sky')\n    array([0.96225034, 0.9653219 ])\n    \"\"\"\n\n    if num is None:\n        if region in ['sky', 'ground']:\n            num = 180\n        elif region == 'horizon':\n            num = 1800\n        else:\n            raise ValueError(f'Invalid region: {region}')\n\n    beta = np.radians(surface_tilt)\n    if isinstance(beta, pd.Series):\n        # convert Series to np array for broadcasting later\n        beta = beta.values\n    ai = np.pi/num  # angular increment\n\n    phi_range = np.linspace(0, np.pi, num, endpoint=False)\n    psi_range = np.linspace(0, 2*np.pi, 2*num, endpoint=False)\n\n    # the pseudocode in [1] do these checks at the end, but it's\n    # faster to do this criteria check up front instead of later.\n    if region == 'sky':\n        mask = phi_range + ai <= np.pi/2\n    elif region == 'horizon':\n        lo = 89.5 * np.pi/180\n        hi = np.pi/2\n        mask = (lo <= phi_range) & (phi_range + ai <= hi)\n    elif region == 'ground':\n        mask = (phi_range >= np.pi/2)\n    else:\n        raise ValueError(f'Invalid region: {region}')\n    phi_range = phi_range[mask]\n\n    # fast Cartesian product of phi and psi\n    angles = np.array(np.meshgrid(phi_range, psi_range)).T.reshape(-1, 2)\n    # index with single-element lists to maintain 2nd dimension so that\n    # these angle arrays broadcast across the beta array\n    phi_1 = angles[:, [0]]\n    psi_1 = angles[:, [1]]\n    phi_2 = phi_1 + ai\n    # psi_2 = psi_1 + ai  # not needed\n    phi_avg = phi_1 + 0.5*ai\n    psi_avg = psi_1 + 0.5*ai\n    term_1 = np.cos(beta) * np.cos(phi_avg)\n    # The AOI formula includes a term based on the difference between\n    # panel azimuth and the photon azimuth, but because we assume each class\n    # of diffuse irradiance is isotropic and we are integrating over all\n    # angles, it doesn't matter what panel azimuth we choose (i.e., the\n    # system is rotationally invariant).  So we choose gamma to be zero so\n    # that we can omit it from the cos(psi_avg) term.\n    # Marion's paper mentions this in the Section 3 pseudocode:\n    # \"set gamma to pi (or any value between 0 and 2pi)\"\n    term_2 = np.sin(beta) * np.sin(phi_avg) * np.cos(psi_avg)\n    cosaoi = term_1 + term_2\n    aoi = np.arccos(cosaoi)\n    # simplify Eq 8, (psi_2 - psi_1) is always ai\n    dAs = ai * (np.cos(phi_1) - np.cos(phi_2))\n    cosaoi_dAs = cosaoi * dAs\n    # apply the final AOI check, zeroing out non-passing points\n    mask = aoi < np.pi/2\n    cosaoi_dAs = np.where(mask, cosaoi_dAs, 0)\n    numerator = np.sum(function(np.degrees(aoi)) * cosaoi_dAs, axis=0)\n    denominator = np.sum(cosaoi_dAs, axis=0)\n\n    with np.errstate(invalid='ignore'):\n        # in some cases, no points pass the criteria\n        # (e.g. region='ground', surface_tilt=0), so we override the division\n        # by zero to set Fd=0.  Also, preserve nans in beta.\n        Fd = np.where((denominator != 0) | ~np.isfinite(beta),\n                      numerator / denominator,\n                      0)\n\n    # preserve input type\n    if np.isscalar(surface_tilt):\n        Fd = Fd.item()\n    elif isinstance(surface_tilt, pd.Series):\n        Fd = pd.Series(Fd, surface_tilt.index)\n\n    return Fd\n\n\ndef schlick(aoi):\n    \"\"\"\n    Determine incidence angle modifier (IAM) for direct irradiance using the\n    Schlick approximation to the Fresnel equations.\n\n    The Schlick approximation was proposed in [1]_ as a computationally\n    efficient alternative to computing the Fresnel factor in computer\n    graphics contexts.  This implementation is a normalized form of the\n    equation in [1]_ so that it can be used as a PV IAM model.\n    Unlike other IAM models, this model has no ability to describe\n    different reflection profiles.\n\n    In PV contexts, the Schlick approximation has been used as an analytically\n    integrable alternative to the Fresnel equations for estimating IAM\n    for diffuse irradiance [2]_.\n\n    Parameters\n    ----------\n    aoi : numeric\n        The angle of incidence (AOI) between the module normal vector and the\n        sun-beam vector. Angles of nan will result in nan. [degrees]\n\n    Returns\n    -------\n    iam : numeric\n        The incident angle modifier.\n\n    References\n    ----------\n    .. [1] Schlick, C. An inexpensive BRDF model for physically-based\n       rendering. Computer graphics forum 13 (1994).\n\n    .. [2] Xie, Y., M. Sengupta, A. Habte, A. Andreas, \"The 'Fresnel Equations'\n       for Diffuse radiation on Inclined photovoltaic Surfaces (FEDIS)\",\n       Renewable and Sustainable Energy Reviews, vol. 161, 112362. June 2022.\n       :doi:`10.1016/j.rser.2022.112362`\n\n    See Also\n    --------\n    pvlib.iam.schlick_diffuse\n    \"\"\"\n    iam = 1 - (1 - cosd(aoi)) ** 5\n    iam = np.where(np.abs(aoi) >= 90.0, 0.0, iam)\n\n    # preserve input type\n    if np.isscalar(aoi):\n        iam = iam.item()\n    elif isinstance(aoi, pd.Series):\n        iam = pd.Series(iam, aoi.index)\n\n    return iam\n\n\ndef schlick_diffuse(surface_tilt):\n    \"\"\"\n    Determine the incidence angle modifiers (IAM) for diffuse sky and\n    ground-reflected irradiance on a tilted surface using the Schlick\n    incident angle model.\n\n    The diffuse iam values are calculated using an analytical integration\n    of the Schlick equation [1]_ over the portion of an isotropic sky and\n    isotropic foreground that is visible from the tilted surface [2]_.\n\n    Parameters\n    ----------\n    surface_tilt : numeric\n        Surface tilt angle measured from horizontal (e.g. surface facing\n        up = 0, surface facing horizon = 90). [degrees]\n\n    Returns\n    -------\n    iam_sky : numeric\n        The incident angle modifier for sky diffuse.\n\n    iam_ground : numeric\n        The incident angle modifier for ground-reflected diffuse.\n\n    References\n    ----------\n    .. [1] Schlick, C. An inexpensive BRDF model for physically-based\n       rendering. Computer graphics forum 13 (1994).\n\n    .. [2] Xie, Y., M. Sengupta, A. Habte, A. Andreas, \"The 'Fresnel Equations'\n       for Diffuse radiation on Inclined photovoltaic Surfaces (FEDIS)\",\n       Renewable and Sustainable Energy Reviews, vol. 161, 112362. June 2022.\n       :doi:`10.1016/j.rser.2022.112362`\n\n    See Also\n    --------\n    pvlib.iam.schlick\n    \"\"\"\n    # these calculations are as in [2]_, but with the refractive index\n    # weighting coefficient w set to 1.0 (so it is omitted)\n\n    # relative transmittance of sky diffuse radiation by PV cover:\n    cosB = cosd(surface_tilt)\n    sinB = sind(surface_tilt)\n    cuk = (2 / (np.pi * (1 + cosB))) * (\n        (30/7)*np.pi - (160/21)*np.radians(surface_tilt) - (10/3)*np.pi*cosB\n        + (160/21)*cosB*sinB - (5/3)*np.pi*cosB*sinB**2 + (20/7)*cosB*sinB**3\n        - (5/16)*np.pi*cosB*sinB**4 + (16/105)*cosB*sinB**5\n    )  # Eq 4 in [2]\n\n    # relative transmittance of ground-reflected radiation by PV cover:\n    with np.errstate(divide='ignore', invalid='ignore'):  # Eq 6 in [2]\n        cug = 40 / (21 * (1 - cosB)) - (1 + cosB) / (1 - cosB) * cuk\n\n    cug = np.where(surface_tilt < 1e-6, 0, cug)\n\n    # respect input types:\n    if np.isscalar(surface_tilt):\n        cuk = cuk.item()\n        cug = cug.item()\n    elif isinstance(surface_tilt, pd.Series):\n        cuk = pd.Series(cuk, surface_tilt.index)\n        cug = pd.Series(cug, surface_tilt.index)\n\n    return cuk, cug\n"}, "filelevel_patches": {"pvlib/iam.py": "@@ -175,8 +175,12 @@ def physical(aoi, n=1.526, K=4.0, L=0.002, *, n_ar=None):\n     n2costheta2 = n2 * costheta\n \n     # reflectance of s-, p-polarized, and normal light by the first interface\n-    rho12_s = ((n1costheta1 - n2costheta2) / (n1costheta1 + n2costheta2)) ** 2\n-    rho12_p = ((n1costheta2 - n2costheta1) / (n1costheta2 + n2costheta1)) ** 2\n+    with np.errstate(divide='ignore', invalid='ignore'):\n+        rho12_s = \\\n+            ((n1costheta1 - n2costheta2) / (n1costheta1 + n2costheta2)) ** 2\n+        rho12_p = \\\n+            ((n1costheta2 - n2costheta1) / (n1costheta2 + n2costheta1)) ** 2\n+\n     rho12_0 = ((n1 - n2) / (n1 + n2)) ** 2\n \n     # transmittance through the first interface\n@@ -208,13 +212,22 @@ def physical(aoi, n=1.526, K=4.0, L=0.002, *, n_ar=None):\n         tau_0 *= (1 - rho23_0) / (1 - rho23_0 * rho12_0)\n \n     # transmittance after absorption in the glass\n-    tau_s *= np.exp(-K * L / costheta)\n-    tau_p *= np.exp(-K * L / costheta)\n+    with np.errstate(divide='ignore', invalid='ignore'):\n+        tau_s *= np.exp(-K * L / costheta)\n+        tau_p *= np.exp(-K * L / costheta)\n+\n     tau_0 *= np.exp(-K * L)\n \n     # incidence angle modifier\n     iam = (tau_s + tau_p) / 2 / tau_0\n \n+    # for light coming from behind the plane, none can enter the module\n+    # when n2 > 1, this is already the case\n+    if np.isclose(n2, 1).any():\n+        iam = np.where(aoi >= 90, 0, iam)\n+        if isinstance(aoi, pd.Series):\n+            iam = pd.Series(iam, index=aoi.index)\n+\n     return iam"}}