Custom Functions with Numba#

import json
import os

import numpy as np
import pandas as pd
import sympy as sp
import matplotlib.pyplot as plt
import pvdeg

Grabbing Weather Data#

We need a timeseries of weather data to feed into the symbolic degradation expression below.

By default this notebook loads a cached NSRDB TMY for Miami, FL that ships with the repo (tutorials/data/psm4_miami.csv). This keeps the notebook reproducible in CI and works offline. Set USE_LIVE_PVGIS = True in the next cell to make a live PVGIS API call for Manhattan, NYC instead.

# For reproducible CI and offline use, default to cached weather data.
# Set USE_LIVE_PVGIS=True to fetch live PVGIS TMY for Manhattan instead.
USE_LIVE_PVGIS = False

if USE_LIVE_PVGIS:
    weather_df, meta_dict = pvdeg.weather.get(
        database="PVGIS",
        id=(40.776676, -73.971321),  # manhattan (latitude, longitude)
    )
else:
    repo_root = os.path.dirname(os.path.dirname(pvdeg.__file__))
    weather_path = os.path.join(repo_root, "tutorials", "data", "psm4_miami.csv")
    meta_path = os.path.join(repo_root, "tutorials", "data", "meta_miami.json")

    weather_df = pd.read_csv(weather_path, index_col=0, parse_dates=True)
    with open(meta_path, "r") as f:
        meta_dict = json.load(f)
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
Cell In[2], line 15
     11     repo_root = os.path.dirname(os.path.dirname(pvdeg.__file__))
     12     weather_path = os.path.join(repo_root, "tutorials", "data", "psm4_miami.csv")
     13     meta_path = os.path.join(repo_root, "tutorials", "data", "meta_miami.json")
     14 
---> 15     weather_df = pd.read_csv(weather_path, index_col=0, parse_dates=True)
     16     with open(meta_path, "r") as f:
     17         meta_dict = json.load(f)

File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/pandas/io/parsers/readers.py:873, in read_csv(filepath_or_buffer, sep, delimiter, header, names, index_col, usecols, dtype, engine, converters, true_values, false_values, skipinitialspace, skiprows, skipfooter, nrows, na_values, keep_default_na, na_filter, skip_blank_lines, parse_dates, date_format, dayfirst, cache_dates, iterator, chunksize, compression, thousands, decimal, lineterminator, quotechar, quoting, doublequote, escapechar, comment, encoding, encoding_errors, dialect, on_bad_lines, low_memory, memory_map, float_precision, storage_options, dtype_backend)
    861 kwds_defaults = _refine_defaults_read(
    862     dialect,
    863     delimiter,
   (...)    869     dtype_backend=dtype_backend,
    870 )
    871 kwds.update(kwds_defaults)
--> 873 return _read(filepath_or_buffer, kwds)

File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/pandas/io/parsers/readers.py:300, in _read(filepath_or_buffer, kwds)
    297 _validate_names(kwds.get("names", None))
    299 # Create the parser.
--> 300 parser = TextFileReader(filepath_or_buffer, **kwds)
    302 if chunksize or iterator:
    303     return parser

File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/pandas/io/parsers/readers.py:1645, in TextFileReader.__init__(self, f, engine, **kwds)
   1642     self.options["has_index_names"] = kwds["has_index_names"]
   1644 self.handles: IOHandles | None = None
-> 1645 self._engine = self._make_engine(f, self.engine)

File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/pandas/io/parsers/readers.py:1904, in TextFileReader._make_engine(self, f, engine)
   1902     if "b" not in mode:
   1903         mode += "b"
-> 1904 self.handles = get_handle(
   1905     f,
   1906     mode,
   1907     encoding=self.options.get("encoding", None),
   1908     compression=self.options.get("compression", None),
   1909     memory_map=self.options.get("memory_map", False),
   1910     is_text=is_text,
   1911     errors=self.options.get("encoding_errors", "strict"),
   1912     storage_options=self.options.get("storage_options", None),
   1913 )
   1914 assert self.handles is not None
   1915 f = self.handles.handle

File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/pandas/io/common.py:930, in get_handle(path_or_buf, mode, encoding, compression, memory_map, is_text, errors, storage_options)
    925 elif isinstance(handle, str):
    926     # Check whether the filename is to be opened in binary mode.
    927     # Binary mode does not support 'encoding' and 'newline'.
    928     if ioargs.encoding and "b" not in ioargs.mode:
    929         # Encoding
--> 930         handle = open(
    931             handle,
    932             ioargs.mode,
    933             encoding=ioargs.encoding,
    934             errors=errors,
    935             newline="",
    936         )
    937     else:
    938         # Binary mode
    939         handle = open(handle, ioargs.mode)

FileNotFoundError: [Errno 2] No such file or directory: '/opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/tutorials/data/psm4_miami.csv'

Calculating Temperature and Irradiance#

Use pvdeg to calculate timeseries temperature for a theoretical module from the previous metorological data.

Update this call after scenario has been merged: dev_scenario_geospatial->development->dev_symbolic

module_temps = pvdeg.temperature.module(
    weather_df=weather_df, meta=meta_dict, conf="open_rack_glass_glass"
)

poa_irradiance = pvdeg.spectral.poa_irradiance(weather_df=weather_df, meta=meta_dict)

location_label = "Manhattan" if USE_LIVE_PVGIS else "Miami"

plt.figure(figsize=(10, 6))
plt.subplot(1, 2, 1)
plt.plot(
    module_temps.values
)  # plotting the values in order because we are using tmy data so the years are not consistent within our data
plt.title(f"TMY Module Temperature, {location_label}")
plt.subplot(1, 2, 2)
plt.plot(poa_irradiance.values)
plt.legend(poa_irradiance.columns)
plt.title(f"TMY Module Irradiance, {location_label}")
plt.show()
The array surface_tilt angle was not provided, therefore the latitude of  25.8 was used.
The array azimuth was not provided, therefore an azimuth of  180.0 was used.
The array surface_tilt angle was not provided, therefore the latitude of  25.8 was used.
The array azimuth was not provided, therefore an azimuth of  180.0 was used.
../_images/12d1da8a1e5f7abe5c31368b6f3c8a890f5e8fde0b8c30f674b4f66df1e00a10.png

Define Custom Expressions from Latex or#

We will use an altered arrhenius equation with an irrandiance relation (ignore the fact that this exists in pvdeg already).

\(R_{D} = R_{0}I^{X} e^{\frac{-{Ea}}{kT}}\) ea is one variable so this may present some issues the raw latex looks like this R_{0}I^{X}  e^{\frac{-{Ea}}{kT}}

lnR_0, I, X, Ea, k, T = sp.symbols("lnR_0 I X Ea k T")

ln_R_D_expr = (
    lnR_0 * I**X * sp.exp((-Ea) / (k * T))
)  # python exponentiation is ** rather than ^

# viewing output
ln_R_D_expr
\[\displaystyle I^{X} lnR_{0} e^{- \frac{Ea}{T k}}\]

Calculating Degradation Expression#

Generally more processing will have to happen outside of these functions than built in pvdeg functions. Here we are defining our arguments and correcting units. When trying to calculate using timeseries we will pass pandas.Series objects to the arguments.

Results should be strictly scrutinized to make sure the calculation is iterating over your series correctly. It will generally be easier to write python code for more complex functions. This is about as complex as we will be able to get using arbitrary symbolic expressions if looping over timeseries data is required.

module_temps_k = module_temps + 273.15  # convert C -> K

poa_global = poa_irradiance[
    "poa_global"
]  # take only the global irradiance series from the total irradiance dataframe
poa_global_kw = poa_global / 1000  # [W/m^2] -> [kW/m^2]

This is quite slow#

values_kwarg = {
    "Ea": 62.08,  # activation energy, [kJ/mol]
    "k": 8.31446e-3,  # boltzmans constant, [kJ/(mol * K)]
    "T": module_temps_k,  # module temperature, [K]
    "I": poa_global_kw,  # module plane of array irradiance, [W/m2]
    "X": 0.0341,  # irradiance relation, [unitless]
    "lnR_0": 13.72,  # prefactor degradation [ln(%/h)]
}

res = pvdeg.symbolic.calc_kwarg_timeseries(expr=ln_R_D_expr, kwarg=values_kwarg)

Total degradation#

To calculate accumulated degradation, we can sum each of the substep values and convert to log scale (need to do this because prefactor was in log scale).

np.log10(res.sum())
np.float64(-5.637887238640398)

Example with Single Values#

leakage current \(I_{leak} = \frac{V_{bias}}{R_{enc}}\)

\(I_{leak}\), leakage current \({V_{bias}}\), potential difference between cells and frame \({R_{enc}}\), resistance of the encapsulant

electric field \(E = \frac{V_{bias}}{d}\)

\(E\), electric field \(d\), thickness of encapsulant

degradation rage \(D = k_{D} * E * I{leak}\)

\(D\), degradation rate \(k_D\), degradation constant

k_d, E, Vbias, Rencap, d = sp.symbols("k_d E Vbias Rencap d")

pid = k_d * (Vbias / d) * (Vbias / Rencap)
import pvdeg

pid_kwarg = {
    "Vbias": 1000,
    "Rencap": 1e9,
    "d": 0.0005,
    "k_d": 1e-9,
}

res = pvdeg.symbolic.calc_kwarg_floats(expr=pid, kwarg=pid_kwarg)

res
\[\displaystyle 2.0 \cdot 10^{-9}\]

Calculate Values in a Dataframe using a symbolic expression#

After creating the symbolic expression in using latex with pvdeg.symbolic.symbolic_from_latex, feed it to the function to calculate the leakage current at each row in the dataframe. This expression calculation only accesses one dataframe row at a time so we cannot do any summations or windowed timeseries averiging within the call. At this point, write your own python function. If you create functions that would be useful, please email us your source code or create an issue and copy and paste your code there. If it is relevant it may be added to the package (credit will be given).

# I_leak_expr = pid_kwarg["Vbias"] / pid_kwarg["Rencap"]
# pvdeg.symbolic.calc_df_symbolic(expr=I_leak_expr, df=values_df)