Skip to content

GSoC 2026: Standardizing Diffuse Irradiance Components and Extending ModelChain Optical Loss Modeling #2750

Open

Description

@cbcrespo

This issue tracks my work for Google Summer of Code 2026 on improving diffuse irradiance handling and optical loss modeling in pvlib. This project will be carried out under the supervision of @AdamRJensen and @ramaroesilva, and has two main goals:

  1. Standardizing the outputs of diffuse transposition models
  2. Extending ModelChain to support component-specific optical loss calculations, via component-specific incident angle modifiers (IAM)

If you're interested, you can follow regular updates and discussions on my blog. Official project description on the GSoC website here.

Motivation

Currently, diffuse transposition models in pvlib.irradiance provide inconsistent outputs. Some models (e.g., Perez) can return individual components (circumsolar, isotropic, horizon), while others only return total diffuse irradiance. This was previously identified and discussed in issue #1553.

This inconsistency limits the ModelChain workflow, which:

  • Applies a single angle of incidence (AOI) model
  • Uses one IAM value for total irradiance
  • Cannot distinguish between irradiance components. Since optical losses depend strongly on incidence angle, this reduces physical accuracy.

Proposed Changes

Phase 1: Standardize diffuse component outputs

  • Introduce a consistent return_components parameter across all diffuse transposition models. PR Adapts irradiance.isotropic to return_components framework. #2527 has begun addressing this by modifying irradiance.isotropic to support this parameter, as previously implemented in irradiance.perez.
  • Ensure all models return:
    • Circumsolar
    • Isotropic
    • Horizon
    • Total diffuse irradiance
  • For models that lack explicit components, return zeros for missing components
  • Follow the structure used in pvlib.irradiance.perez

Phase 2: Extend ModelChain for component-specific IAM

  • Consider plane-of-array (POA) components instead of global irradiance
  • Add use of component-specific IAM when calculating effective $G_{POA}$
  • Allow users to pass a dictionary to aoi_model, specifying IAM model for each component
  • Maintain full backward compatibility

My name is Carolina Crespo, I am a PhD student at the University of Lisbon working on firm power in Iberia using PyPSA-Eur. I am a regular user of pvlib, which I teach (although at a superficial level) to 1st-year Bachelors students.

Any feedback on this plan will be very welcome, especially since this is my first time contributing to an open source project. Looking forward to getting started!

List of issues and PRs

Phase 1

Phase 2

Activity

  1. added
    GSoCContributions related to Google Summer of Code.
    on May 6, 2026
  2. markcampanelli commented on May 6, 2026

    @markcampanelli
    Contributor

    Thanks for taking this on @cbcrespo.

    I posted this fixed-tilt-focused code on the Google groups page a while back. I am interested in a clean and performant way of extending the IAM corrections for diffuse POA components to trackers.

    https://groups.google.com/g/pvlib-python/c/Jue2gKzaXnU/m/ndtf6HQABAAJ

  3. cwhanse commented on May 6, 2026

    @cwhanse
    Member

    For models that lack explicit components, return zeros for missing components

    We'll need to agree on rules for assigning the missing diffuse irradiance components to the components that are returned. E.g., if a model does not separately quantify circumsolar diffuse, we include that portion of diffuse irradiance with direct normal irradiance.

  4. adriesse commented on May 6, 2026

    @adriesse
    Member

    Welcome @cbcrespo !!

    Another option I would raise for consideration is to do this in a way that does not hard-code these three specific categories of diffuse irradiance. So one or two could simply be missing (rather than set to zero), or a new and different one could be added or substituted by a new transposition model.

    At least, evaluate the consequences of the chosen code design if a new model comes along with more diffuse categories.

  5. asdai110 commented on May 9, 2026

    @asdai110

    Hello Carolina, your plan to standardize diffuse components and extend IAM support is a massive leap for pvlib's physical accuracy.
    ​A quick thought from a materials perspective: As you extend ModelChain for component-specific IAM, have you considered the 'Spectral Redshift' or 'Frequency Drift' occurring in next-gen thin-film materials?
    Specifically, at high incidence angles, the non-linear thermal gradient across the cell (especially in 2nm-structured substrates) can alter the effective IAM beyond traditional geometric models. Since you're already breaking down the G_{POA} components, adding a 'Thermal-Optical Coupling' parameter might be the next frontier for your PhD research. Would love to see how your framework handles these non-linearities."

  6. ramaroesilva commented on May 13, 2026

    @ramaroesilva
    Contributor

    Hi everyone, thank you for the great input for @cbcrespo activitiy.

    My two cents about the "setting to zero" part (in particular for @cwhanse, @adriesse, @kandersolar):

    • I agree with the disadvantage of hard-coding too much, in particular for the IAM calculations as it would make it unnecessarily less flexible for possible future decomposition approaches
    • I also agree having "component is not considered => 0-filled column/key" as default would be exaggerated, since it could be that the sky diffuse is simply segmented in a different manner and not that a given component is ignored
    • however, for the the decomposition models currently implemented within pvlib it seems solid to me that "absence means 0" and, personally, would prefer to see this explicitly in the output rather than implicitly (giving room for individual interpretation)

    While this is my point of view, naturally it is open for discussion :-)

  7. cbcrespo commented on May 13, 2026

    @cbcrespo
    ContributorAuthor

    @markcampanelli could you expand on what you mean by expanding this to trackers? Would that not already be possible under the current implementation? IAM functions already accept either the AOI or the surface tilt, which can be an array.

  8. cbcrespo commented on Jun 23, 2026

    @cbcrespo
    ContributorAuthor

    I added a new blog post last week!

  9. markcampanelli commented on Jun 23, 2026

    @markcampanelli
    Contributor

    @markcampanelli could you expand on what you mean by expanding this to trackers? Would that not already be possible under the current implementation? IAM functions already accept either the AOI or the surface tilt, which can be an array.

    @cbcrespo My apologies if I sent you on a bit of a savager hunt to nowhere. I think you are correct that the low-level functions are sufficiently vectorized to allow for the tracker calculations to be made. I think a big part of my original concern was if this could be done more efficiently and compactly (and potentially also with the elimination of duplicate calculations that likely happen between the sky and ground diffuse calculations). Also, the use of the Marion method of computing the IAM-corrected diffuse effective irradiance (which assumes sky, horizon, and ground diffuse components) does not seem to be supported in the ModelChain, see

    def effective_irradiance_model(self):
    . I think it is worth thinking about how the varied components being returned from the different diffuse irradiance models can "flow forward" efficiently into a Marion-like IAM correction. Thanks for considering, even if this is out of scope for your immediate project.

  10. markcampanelli commented on Jun 28, 2026

    @markcampanelli
    Contributor

    @markcampanelli could you expand on what you mean by expanding this to trackers? Would that not already be possible under the current implementation? IAM functions already accept either the AOI or the surface tilt, which can be an array.

    It turns out that just doing the "naive" computation here is a huge memory hog (enough to take down my expensive laptop!). Instead, I have found that first doing an interpolation (e.g., scipy.interpolate.PchipInterpolator) of the Marion integral results for each diffuse component and then computing using this interpolant for the SAT tilt angles (for 30- or 60-minute data over 20-30 years) is more efficient. (This could probably be further optimized because many of the SAT tilt angles repeat and could be cached.) I haven't looked into yet where the memory sink happens in the integrations.

    Image
  11. adriesse commented on Jun 29, 2026

    @adriesse
    Member

    I think I mentioned it somewhere before, but I don't think it's a good idea (and also illogical) to include the sum of the components as a separate component.

  12. markcampanelli commented on Jul 1, 2026

    @markcampanelli
    Contributor

    @cbcrespo I may have missed this, but was there any discussion about indicating if a given model concentrates the circumsolar and/or horizon brightening in the center of the sun disk and horizon ring, respectively.

    For example, IIRC at one point the Perez model changed to provide “equivalent” concentrated values for both circumsolar and the horizon, and this affects how subsequent algorithms (like Marion’s) can/should compute the IAMs for the diffuse components.

  13. ramaroesilva commented on Jul 4, 2026

    @ramaroesilva
    Contributor

    It turns out that just doing the "naive" computation here is a huge memory hog (enough to take down my expensive laptop!). Instead, I have found that first doing an interpolation

    @markcampanelli could be worth to create a separate issue about this, and even better if in it you could provide some insights on the memory sink source

  14. kandersolar commented on Jul 4, 2026

    @kandersolar
    Member

    Discussion about memory usage in the Marion IAM functions can go to #1402

  15. cwhanse commented on Jul 10, 2026

    @cwhanse
    Member

    The various sky diffuse functions are being modified to return either a numeric output sky_diffuse, or a DataFrame output diffuse_components containing 'poa_sky_diffuse', 'poa_isotropic', 'poa_circumsolar', and/or 'poa_horizon' depending on what components apply for that model.

    The description of these outputs is not uniform among the current sky diffuse functions. I think we should make it uniform throughout this GSoC project's sequence of PRs. My suggestions below.

    sky_diffuse: The sky diffuse component of irradiance on a tilted plane. [Wm⁻²]
    poa_sky_diffuse: The sky diffuse component of irradiance on a tilted plane. [Wm⁻²]
    poa_isotropic: The portion of sky diffuse irradiance on a tilted plane from the isotropic sky dome. [Wm⁻²]
    poa_circumsolar: The circumsolar portion of sky diffuse irradiance on a tilted plane. [Wm⁻²]
    poa_horizon: The horizon portion of sky diffuse irradiance on a tilted plane. [Wm⁻²]

  16. ramaroesilva commented on Jul 12, 2026

    @ramaroesilva
    Contributor

    My suggestions below.

    sky_diffuse: The sky diffuse component of irradiance on a tilted plane. [Wm⁻²] poa_sky_diffuse: The sky diffuse component of irradiance on a tilted plane. [Wm⁻²] poa_isotropic: The portion of sky diffuse irradiance on a tilted plane from the isotropic sky dome. [Wm⁻²] poa_circumsolar: The circumsolar portion of sky diffuse irradiance on a tilted plane. [Wm⁻²] poa_horizon: The horizon portion of sky diffuse irradiance on a tilted plane. [Wm⁻²]

    Overall seems good to me, but small variation below that would make structure between components more similar:

    • poa_isotropic: as originally proposed
    • poa_circumsolar: The portion of sky diffuse irradiance on a tilted plane from the circumsolar region. [Wm⁻²]
    • poa_horizon: The portion of sky diffuse irradiance on a tilted plane from the horizon. [Wm⁻²]

    Also, not sure about having both sky_diffuse and poa_sky_diffuse, as the naming differentiation might be confusing. Why not keep only poa_sky_diffuse?

  17. cwhanse commented on Jul 12, 2026

    @cwhanse
    Member

    about having both sky_diffuse and poa_sky_diffuse

    sky_diffuse is the name of the returned quantity (array or Series).
    poa_sky_diffuse is the key (column) in a returned DataFrame diffuse_components.

    From a user's point of view, sky_diffuse and poa_sky_diffuse are two different things:

    sky_diffuse = isotropic(..., return_components=False)
    
    diffuse_components = isotropic(..., return_components=True)
    sky_diffuse = diffuse_components['poa_sky_diffuse']
    

    I'd be OK dropping "poa_" from all those keys. But we do need to be mindful of prior naming and the complexity of deprecating keys in returned DataFrames. I don't know if those concerns apply here.

  18. adriesse commented on Jul 13, 2026

    @adriesse
    Member

    I'd be OK dropping "poa_" from all those keys. But we do need to be mindful of prior naming and the complexity of deprecating keys in returned DataFrames. I don't know if those concerns apply here.

    I'd be ok with dropping the prefix as well.

    Let's not forget #2801, which needs a decision (and perhaps further discussion) as well.

  19. ramaroesilva commented on Jul 14, 2026

    @ramaroesilva
    Contributor

    Agree with @adriesse alert on #2801, as it impacts what we're discussing here.

    If the decision in #2801 is to keep the total value as a dict key, personally, I don't think it's best to differentiate an array variable from a dict key - with the same timeseries associated - based on a poa_ prefix. If the decision is to drop the total from the dict key list, I would suggest adding poa_ to the output of of the return_components=False case.

  20. adriesse commented on Jul 14, 2026

    @adriesse
    Member

    .... I would suggest adding poa_ to the output of of the return_components=False case.

    In this case the output doesn't actually have a name. A name for the output only exists inside the function and in the documentation.

  21. ramaroesilva commented on Jul 14, 2026

    @ramaroesilva
    Contributor

    Agreed, but what's the advantage of having it different of the dict key leading to exactly the same timeseries for when users don't want components?

  22. adriesse commented on Jul 14, 2026

    @adriesse
    Member

    Agreed, but what's the advantage of having it different of the dict key leading to exactly the same timeseries for when users don't want components?

    No advantage that I can think of.

  23. cwhanse commented on Jul 14, 2026

    @cwhanse
    Member

    I concur with @ramaroesilva edits to the definitions I proposed.

    For consistency among names, I'm ok with changing the returned Series name to poa_sky_diffuse. To avoid breaking changes, I retract my suggestion to drop poa_ from the component key names.

  24. cbcrespo commented on Jul 15, 2026

    @cbcrespo
    ContributorAuthor

    @cwhanse @adriesse @ramaroesilva

    In addition to those, for functions like get_total_irradiance, I propose:

    • poa_global: Total irradiance on a tilted plane. [Wm⁻²]
    • poa_direct: Direct irradiance on a tilted plane. [Wm⁻²]
    • poa_ground_diffuse: The ground diffuse component of irradiance on a tilted plane. [Wm⁻²]
  25. AdamRJensen commented on Jul 23, 2026

    @AdamRJensen
    Member

    @cbcrespo Could you create a PR that updates the naming to be consistent for all the transposition models and in the nomenclature?

    I see the issue of whether to keep the total value #2801 as a separate issue. For now, you can follow the current practice, and if we decide to change, then we can update all the functions at that time.

    Edit: I see that you already started that here: #2827

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    GSoCContributions related to Google Summer of Code.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions