Skip to content

Conversation

@ramaroesilva
Copy link
Contributor

@ramaroesilva ramaroesilva commented Aug 3, 2025

@ramaroesilva
Copy link
Contributor Author

For now, I only updated irradiance.isotropic as a testbed for the proposed changes.

Made use of the content from perez and haydaviesdocstrings, which were not fully consistent. Also, added the poa_ prefix to the outputs as suggested by @markcampanelli here

If this seems good to everyone, I can replicate this for all functions (adding the return_components and making documentation more consistent across fuctions, whenever each is needed).

@RDaxini RDaxini added this to the v0.13.1 milestone Aug 4, 2025
Copy link
Member

@kandersolar kandersolar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @ramaroesilva! I agree it makes sense to trial it on one function first. Let's wait to do the others until #2529 has some more discussion.

In the meantime, some minor comments below.


return sky_diffuse
if return_components:
diffuse_components = OrderedDict()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor note: no need to use OrderedDict in new code (#1684)

return_components : bool, default False
Flag used to decide whether to return the calculated diffuse components
or not. If `False`, ``sky_diffuse`` is returned. If `True`,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
or not. If `False`, ``sky_diffuse`` is returned. If `True`,
or not. If `False`, ``poa_sky_diffuse`` is returned. If `True`,

Assuming this is in reference to the names in the Returns section

Comment on lines 629 to 633
* sky_diffuse: Total sky diffuse
* isotropic
* circumsolar
* horizon
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update with poa_ prefix

@ramaroesilva
Copy link
Contributor Author

Once more people give feedback on the documentation structuring of this example, I will continue developing the other transposition functions taking into account the OrderedDict comment. All poa_ related changes will move to a separate PR.

diffuse_components['poa_horizon'] = 0

return diffuse_components
else:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to return both poa_sky_diffuse and diffuse_components, if return_componets is True.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me. This "one or the other" is what is implemented in perez at the moment.

For clarity, do you mean returning two dictionaries, which makes the number of outputs depend on return_components, or having still one dictionary buth with an additional sky_diffuse key?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking a tuple with two dicts, when return_components is True.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cwhanse I just noticed that in my latest commit I included in the diffuse_components dict the sky_diffuse as one of the keys. Would this - having a single-dict with components and their sum - make sense?

If instead we have two dictionaries as originally proposed, the one having the sky_diffuse will have a single key... How would we call the variable and the key without being repetitive? total_diffuse["sky"]?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies, I misspoke. Return a tuple (sky_diffuse, components) where sky_diffuse is the current numeric (Series, array, float) and components is a dict.

Copy link
Member

@cwhanse cwhanse Aug 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edited: returning a tuple requires the user to change from

sky_diffuse = isotropic(...)

to

sky_diffuse, _ = isotropic(...)

I was (wrongly) thinking that only the first element of the tuple would be assigned to sky_diffuse. Not thinking clearly.

So now I'm not so sure that returning a tuple is a good idea. Still, it seems better than requiring the user to change to

out = isotropic(...)
sky_diffuse = out['sky_diffuse']

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for clarifying and thinking things through.

Going back to the start:

I would prefer to return both poa_sky_diffuse and diffuse_components, if return_componets is True.

I just remembered that in my latest commit the sky_diffuse is included within diffuse_components, following the workflow used in perez and haydavies. So what we are discussing here is in fact if we should change the current modus operandi of transposition models with return_components.

While what you propose is v0.13.1-friendly for the models not yet returning return_components, it would break previous code for those that already do. Having this said, I would suggest moving this specific topic to a separate issue and raise some discussion aiming for v0.14.

What do you think? If agreed, can you can raise a new issue like @kandersolar did in #2529?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just remembered that in my latest commit the sky_diffuse is included within diffuse_components, following the workflow used in perez and haydavies. So what we are discussing here is in fact if we should change the current modus operandi of transposition models with return_components.

All the irradiance component sets I looked at include both components and sums of components. I think the cleanest way would be (would have been) to include only the lowest level and leave the summation up to the user.

Comment on lines +669 to +672
if isinstance(poa_sky_diffuse, pd.Series):
# follows `perez` worfklow
# shouldn't it include an argument `index=dhi.index`?
diffuse_components = pd.DataFrame(diffuse_components)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any comments about my question here? This code is used in perez.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, should preserve the index from dhi

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would adding this index bit to the diffuse-component-including perez and haydavies functions be considered a breaking change?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that's a breaking change. pvlib strives that a function returns the same data type as the inputs. That's one reason we test functions with different input types. If those functions are returning e.g. Array for Series input, I'd regard that as a bug that needs fixing.

In perez the index wasn't explicitly set because the upstream calculations resulted in sky_diffuse being a Series. I think it's better to be explicit, just in case the calculation uses some numpy function that casts Series to Array.

@ramaroesilva
Copy link
Contributor Author

ramaroesilva commented Dec 21, 2025

Sorry for letting this get stale, work got hectic.

Would like to follow up on @kandersolar initiative who is already including the poa_ prefix in the outputs in #2627 and try to push this PR (after expanding it to other transposition functions) for v0.14. Now we have the freedom of being allowed to introduce breaking changes.

Previous points made:

  • @cwhanse would prefer including both poa_sky_diffuse and diffuse_components if return_components is True.
  • @adriesse suggests to include only the lowest level and leave the summation up to the user.

Within a modelchain, in theory, the optical losses would use the components, whereas the module temperature most often uses the non-effective GTI. If only the components are kept, I guess that a summation would be needed somewhere within the modelchain.

Keeping both the sum and the components seems easier, otherwise we would need to have the code adapt to the user choice regarding return_components.

@ramaroesilva
Copy link
Contributor Author

ramaroesilva commented Dec 21, 2025

If we end up deciding to keep both sum and components, I was thinking if a single dict/DataFrame variable should be produced whether return_components is True or False (i.e., adding more keys/columns to the same base structure).

Pros:

  • keeps same function calling in both cases (addressing one of @cwhanse points in this discussion)
  • avoids duplicating indices in case of DataFrame

Cons:

  • would need to adjust functions in modelchain (and get_total_irradiance maybe) that were receiving either the sum and components
  • would be sending unnecessary data when other functions getting this input ask only for sum/components (increased overhead?)

Could also call the variable as poa_sky_diffuse and have the sum allocate to a total key.

@markcampanelli
Copy link
Contributor

markcampanelli commented Dec 21, 2025

@ramaroesilva Here's my idea, which probably breaks too many things. Immutability opens up caching the sum, for a significant speedup.

"""
Example of a frozen dataclass for PAO diffuse components using cache for components sum.
"""

from dataclasses import dataclass
import functools
import typing

import numpy.typing


@dataclass(frozen=True)
class PoaDiffuseComponents:
    """Contanier for POA diffuse irradiance components."""

    poa_isotropic: numpy.typing.NDArray[numpy.floating[typing.Any]]
    poa_circumsolar: numpy.typing.NDArray[numpy.floating[typing.Any]]
    poa_horizon: numpy.typing.NDArray[numpy.floating[typing.Any]]

    @functools.cached_property
    def poa_sky_diffuse(self) -> numpy.typing.NDArray[numpy.floating[typing.Any]]:
        """Return sum of all POA diffuse components."""

        return self.poa_isotropic + self.poa_circumsolar + self.poa_horizon

    @property
    def isotropic(self) -> numpy.typing.NDArray[numpy.floating[typing.Any]]:
        """Return deprecated POA isotropic diffuse-component."""

        return self.poa_isotropic

    @property
    def circumsolar(self) -> numpy.typing.NDArray[numpy.floating[typing.Any]]:
        """Return deprecated POA circumsolar-diffuse component."""

        return self.poa_circumsolar

    @property
    def horizon(self) -> numpy.typing.NDArray[numpy.floating[typing.Any]]:
        """Return deprecated POA horizon-diffuse component."""

        return self.poa_horizon

    @property
    def sky_diffuse(self) -> numpy.typing.NDArray[numpy.floating[typing.Any]]:
        """Return deprecated sum of all POA diffuse components."""

        return self.poa_sky_diffuse


if __name__ == "__main__":

    import time

    rng = numpy.random.default_rng()

    poa_diffuse_components = PoaDiffuseComponents(
        poa_isotropic=rng.uniform(low=0.0, high=1100.0, size=(8760,)),
        poa_circumsolar=rng.uniform(low=0.0, high=1100.0, size=(8760,)),
        poa_horizon=rng.uniform(low=0.0, high=1100.0, size=(8760,)),
    )

    start_time_ns = time.perf_counter_ns()
    poa_diffuse_components.poa_sky_diffuse
    end_time_ns = time.perf_counter_ns()
    print(f"poa_sky_diffuse uncached time: {end_time_ns - start_time_ns} ns")

    start_time_ns = time.perf_counter_ns()
    poa_diffuse_components.poa_sky_diffuse
    end_time_ns = time.perf_counter_ns()
    print(f"poa_sky_diffuse cached time: {end_time_ns - start_time_ns} ns")

Example output:

poa_sky_diffuse uncached time: 24667 ns
poa_sky_diffuse cached time: 250 ns

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Consider allowing return_components in all transposition model functions

6 participants