Skip to content

dc_ohms_from_percent fails on single-Array ModelChain with list input #2829

Description

@mokazemi

Describe the bug

ModelChain._run_from_effective_irrad (and the dc_ohmic_model step it calls, dc_ohms_from_percent) determines whether self.results.dc should be treated as a tuple based solely on whether the input data passed to run_model_from_poa() / run_model_from_effective_irradiance() was a list/tuple, rather than on PVSystem.num_arrays.

Meanwhile, PVSystem.dc_ohms_from_percent() is decorated with the _unwrap_single_value helper, which returns a scalar float (not a tuple) whenever the PVSystem has exactly one Array, regardless of how the input data was shaped.

When a user passes a length-1 list/tuple of POA (or effective irradiance) DataFrames into a ModelChain built from a PVSystem with a single Array, this mismatch causes:

  • self.results.dc to be stored internally as a 1-element tuple (because the input was a list), while
  • Rw = self.system.dc_ohms_from_percent() to be returned as a plain float (because num_arrays == 1).

dc_ohms_from_percent() in modelchain.py then does:

if isinstance(self.results.dc, tuple):
    self.results.dc_ohmic_losses = tuple(
        pvsystem.dc_ohmic_losses(Rw, df['i_mp'])
        for Rw, df in zip(Rw, self.results.dc)
    )

Since Rw is a float, zip(Rw, self.results.dc) fails with:

TypeError: 'float' object is not iterable

This only reproduces for single-Array PVSystems whose POA/effective irradiance data happens to be passed as a list/tuple rather than a bare DataFrame; with multiple Arrays it works fine because both code paths agree on "tuple," and with a bare DataFrame input on a single-Array system it also works fine because both code paths agree on "scalar."

To Reproduce

Steps to reproduce the behavior:

  1. Build a PVSystem with a single Array using a Sandia module from pvlib.pvsystem.retrieve_sam, and set array_losses_parameters={"dc_ohmic_percent": <some value>} on that Array.

  2. Build a ModelChain for this system with dc_ohmic_model="dc_ohms_from_percent".

  3. Prepare a single POA DataFrame poa_df, then wrap it in a one-element list: poa_input = [poa_df].

  4. Call mc.run_model_from_poa(poa_input).

  5. See error:

    TypeError: 'float' object is not iterable

    raised from ModelChain.dc_ohms_from_percent at the zip(Rw, self.results.dc)
    line in modelchain.py.

Minimal, runnable reproduction:

    import pandas as pd
    from pvlib.location import Location
    from pvlib.modelchain import ModelChain
    from pvlib.pvsystem import PVSystem, Array, FixedMount, retrieve_sam
    from pvlib.temperature import TEMPERATURE_MODEL_PARAMETERS

    sandia_modules = retrieve_sam("SandiaMod")
    module = sandia_modules["Canadian_Solar_CS5P_220M___2009_"]
    cec_inverters = retrieve_sam("cecinverter")
    inverter = cec_inverters["ABB__MICRO_0_25_I_OUTD_US_208__208V_"]
    temp_params = TEMPERATURE_MODEL_PARAMETERS["sapm"]["open_rack_glass_glass"]

    array = Array(
        mount=FixedMount(surface_tilt=30, surface_azimuth=180),
        module_parameters=module,
        temperature_model_parameters=temp_params,
        modules_per_string=2,
        strings=3,
        array_losses_parameters={"dc_ohmic_percent": 1.5},
    )
    system = PVSystem(arrays=[array], inverter_parameters=inverter)
    location = Location(35.6889, 51.3897, tz="Asia/Tehran", altitude=1040)
    mc = ModelChain(system, location, aoi_model="physical",
                     dc_ohmic_model="dc_ohms_from_percent")

    index = pd.date_range("2025-06-01", periods=3, freq="1h", tz="Asia/Tehran")
    poa_df = pd.DataFrame({
        "poa_global": [500.0, 600.0, 550.0],
        "poa_direct": [400.0, 480.0, 440.0],
        "poa_diffuse": [100.0, 120.0, 110.0],
    }, index=index)

    mc.run_model_from_poa([poa_df])  # fails with TypeError
    # mc.run_model_from_poa(poa_df)   # succeeds

Expected behavior

ModelChain.run_model_from_poa (and run_model_from_effective_irradiance) should produce consistent, correct results regardless of whether the caller wraps a single Array's data in a one-element list/tuple or passes it as a bare DataFrame. Either:
(a) ModelChain should normalize/unwrap single-element list input to match num_arrays == 1 before running the rest of the chain, or
(b) dc_ohms_from_percent's tuple/scalar decision should be based on self.results.dc's actual type consistently with how Rw is computed, e.g. by wrapping Rw in a tuple whenever self.results.dc is a tuple (and vice versa), so that no TypeError occurs and ohmic losses are computed correctly either way.

Screenshots

N/A (traceback only):

pv_farm_sim/simulate.py:59: in run_system_model
    mc.run_model_from_poa(poa_list)
.venv/lib/python3.14/site-packages/pvlib/modelchain.py:1807: in run_model_from_poa
    self._run_from_effective_irrad(data)
.venv/lib/python3.14/site-packages/pvlib/modelchain.py:1834: in _run_from_effective_irrad
    self.dc_ohmic_model()
.venv/lib/python3.14/site-packages/pvlib/modelchain.py: in dc_ohms_from_percent
    for Rw, df in zip(Rw, self.results.dc)
TypeError: 'float' object is not iterable

Versions:

  • pvlib.__version__: 0.15.2
  • pandas.__version__: 3.0.3
  • python: 3.14

Additional context

  • Workaround: only pass a list/tuple to run_model_from_poa/ run_model_from_effective_irradiance when len(pv_system.arrays) > 1; pass the bare DataFrame otherwise. This avoids the mismatch entirely.
  • The bug is specific to the combination of (single Array) + (list/tuple input) + (dc_ohmic_model="dc_ohms_from_percent"). Other DC/loss models in the chain don't perform this tuple-vs-scalar cross-check, so they don't
    surface the inconsistency — which is why removing dc_ohmic_model="dc_ohms_from_percent" "fixes" the symptom without fixing
    the underlying shape mismatch.
  • Related but distinct multi-array issues: Multiple arrays in a PV system #1067 (multi-array support), AttributeError when using ModelChain with multiple Arrays #1759 (AttributeError with multiple Arrays) — this report concerns a narrower single-Array-with-list-input edge case that doesn't appear to be tracked elsewhere.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions