Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/development' into T98_estimate_b…
Browse files Browse the repository at this point in the history
…ug_fix
  • Loading branch information
martin-springer committed Aug 13, 2024
2 parents 7fe6d6f + e0987b4 commit 311b70b
Show file tree
Hide file tree
Showing 12 changed files with 681 additions and 4 deletions.
6 changes: 6 additions & 0 deletions docs/source/_autosummary/pvdeg.symbolic.calc_df_symbolic.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pvdeg.symbolic.calc\_df\_symbolic
=================================

.. currentmodule:: pvdeg.symbolic

.. autofunction:: calc_df_symbolic
6 changes: 6 additions & 0 deletions docs/source/_autosummary/pvdeg.symbolic.calc_kwarg_floats.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pvdeg.symbolic.calc\_kwarg\_floats
==================================

.. currentmodule:: pvdeg.symbolic

.. autofunction:: calc_kwarg_floats
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pvdeg.symbolic.calc\_kwarg\_timeseries
======================================

.. currentmodule:: pvdeg.symbolic

.. autofunction:: calc_kwarg_timeseries
69 changes: 69 additions & 0 deletions docs/source/_autosummary/pvdeg.symbolic.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
.. Please when editing this file make sure to keep it matching the
docs in ../configuration.rst:reference_to_examples
pvdeg.symbolic
==============

.. automodule:: pvdeg.symbolic

.. this is crazy
Function Overview
-----------------

.. autosummary::
:toctree:
:nosignatures:


pvdeg.symbolic.calc_df_symbolic
pvdeg.symbolic.calc_kwarg_floats
pvdeg.symbolic.calc_kwarg_timeseries




.. this is crazy
..
Functions
---------


.. autofunction:: calc_df_symbolic

.. _sphx_glr_backref_pvdeg.symbolic.calc_df_symbolic:

.. minigallery:: pvdeg.symbolic.calc_df_symbolic
:add-heading:

.. autofunction:: calc_kwarg_floats

.. _sphx_glr_backref_pvdeg.symbolic.calc_kwarg_floats:

.. minigallery:: pvdeg.symbolic.calc_kwarg_floats
:add-heading:

.. autofunction:: calc_kwarg_timeseries

.. _sphx_glr_backref_pvdeg.symbolic.calc_kwarg_timeseries:

.. minigallery:: pvdeg.symbolic.calc_kwarg_timeseries
:add-heading:










1 change: 1 addition & 0 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Modules, methods, classes and attributes are explained here.
geospatial
spectral
standards
symbolic
temperature
utilities
weather
10 changes: 7 additions & 3 deletions docs/source/whatsnew/releases/v0.4.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,21 @@ Enhancements
---------
* Autotemplating system for geospatial analysis using `pvdeg.geospatial.autotemplate`
* New module `pvdeg.decorators` that contains `pvdeg` specific decorator functions.
* Implemented `geospatial_result_type` decorator to update functions and preform runtime introspection to determine if a function is autotemplate-able.
* Implemented `geospatial_result_type` decorator to update functions and preform runtime introspection to determine if a function is autotemplate-able.
* `Geospatial Templates.ipynb` notebook to showcase new and old templating functionality for users.
* symbolic equation solver for simple models.
* notebook tutorial `Custom-Functions-Nopython.ipynb`

Bug Fixes
---------
* Added type hinting to many `pvdeg` functions
* Replaced deprecated numba `jit(nopython=True)` calls with `njit`
* Fixed whatsnew `v0.3.5` author

Requirements
------------
* `sympy` package required for `pvdeg.symbolic` functions and notebook. Not added to dependency list.

Contributors
~~~~~~~~~~~~
* Tobin Ford (:ghuser:`tobin-ford`)


1 change: 1 addition & 0 deletions pvdeg/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from . import montecarlo
from . import scenario
from . import spectral
from . import symbolic
from . import standards
from . import temperature
from . import utilities
Expand Down
2 changes: 1 addition & 1 deletion pvdeg/spectral.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def poa_irradiance(
-------
poa : pandas.DataFrame
Contains keys/columns 'poa_global', 'poa_direct', 'poa_diffuse',
'poa_sky_diffuse', 'poa_ground_diffuse'.
'poa_sky_diffuse', 'poa_ground_diffuse'. [W/m2]
"""

# TODO: change for handling HSAT tracking passed or requested
Expand Down
124 changes: 124 additions & 0 deletions pvdeg/symbolic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
"""
Collections of functions to enable arbitrary symbolic expression evaluation for simple models
"""

import sympy as sp
import pandas as pd
import numpy as np

# from latex2sympy2 import latex2sympy # this potentially useful but if someone has to use this then they proboably wont be able to figure out the rest
# parse: latex -> sympy using latex2sympy2 if nessesscary


def calc_kwarg_floats(
expr: sp.core.mul.Mul,
kwarg: dict,
) -> float:
"""
Calculate a symbolic sympy expression using a dictionary of values
Parameters:
----------
expr: sp.core.mul.Mul
symbolic sympy expression to calculate values on.
kwarg: dict
dictionary of kwarg values for the function, keys must match
sympy symbols.
Returns:
--------
res: float
calculated value from symbolic equation
"""
res = expr.subs(kwarg).evalf()
return res


def calc_df_symbolic(
expr: sp.core.mul.Mul,
df: pd.DataFrame,
) -> pd.Series:
"""
Calculate the expression over the entire dataframe.
Parameters:
----------
expr: sp.core.mul.Mul
symbolic sympy expression to calculate values on.
df: pd.DataFrame
pandas dataframe containing column names matching the sympy symbols.
"""
variables = set(map(str, list(expr.free_symbols)))
if not variables.issubset(df.columns.values):
raise ValueError(f"""
all expression variables need to be in dataframe cols
expr symbols : {expr.free_symbols}")
dataframe cols : {df.columns.values}
""")

res = df.apply(lambda row: calc_kwarg_floats(expr, row.to_dict()), axis=1)
return res


def _have_same_indices(series_list):
if not series_list:
return True

if not isinstance(series_list, pd.Series):
return False

first_index = series_list[0].index

same_indicies = all(s.index.equals(first_index) for s in series_list[1:])
all_series = all(isinstance(value, pd.Series) for value in series_list)

return same_indicies and all_series


def _have_same_length(series_list):
if not series_list:
return True

first_length = series_list[0].shape[0]
return all(s.shape[0] == first_length for s in series_list[1:])


def calc_kwarg_timeseries(
expr,
kwarg,
):
# check for equal length among timeseries. no nesting loops allowed, no functions can be dependent on their previous results values
numerics, timeseries, series_length = {}, {}, 0
for key, val in kwarg.items():
if isinstance(val, (pd.Series, np.ndarray)):
timeseries[key] = val
series_length = len(val)
elif isinstance(val, (int, float)):
numerics[key] = val
else:
raise ValueError(f"only simple numerics or timeseries allowed")

if not _have_same_length(list(timeseries.values())):
raise NotImplementedError(
f"arrays/series are different lengths. fix mismatched length. otherwise arbitrary symbolic solution is too complex for solver. nested loops or loops dependent on previous results not supported."
)

# calculate the expression. we will seperately calculate all values and store then in a timeseries of the same shape. if a user wants to sum the values then they can
if _have_same_indices(list(timeseries.values())):
index = list(timeseries.values())[0].index
else:
index = pd.RangeIndex(start=0, stop=series_length)
res = pd.Series(index=index, dtype=float)

for i in range(series_length):
# calculate at each point and save value
iter_dict = {
key: value.values[i] for key, value in timeseries.items()
} # pandas indexing will break like this in future versions, we could only

iter_dict = {**numerics, **iter_dict}

# we are still getting timeseries at this point
res.iloc[i] = float(expr.subs(iter_dict).evalf())

return res
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ docs = [
test = [
"pytest",
"pytest-cov",
"sympy",
]
books = [
"jupyter-book",
Expand Down
116 changes: 116 additions & 0 deletions tests/test_symbolic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import pytest
import os
import json
import numpy as np
import pandas as pd
import sympy as sp # not a dependency, may cause issues
import pvdeg
from pvdeg import TEST_DATA_DIR

WEATHER = pd.read_csv(
os.path.join(TEST_DATA_DIR, r"weather_day_pytest.csv"),
index_col=0,
parse_dates=True
)

with open(os.path.join(TEST_DATA_DIR, "meta.json"), "r") as file:
META = json.load(file)

# D = k_d * E * Ileak
# degradation rate, D
# degradation constant, k_d

# electric field, E = Vbias / d
# Vbias, potential diference between cells and frame
# d, encapsulant thickness

# leakage current, Ileak = Vbias / Rencap
# Vbias, potential diference between cells and frame
# Rencap, resistance of encapsulant

k_d, Vbias, Rencap, d = sp.symbols('k_d Vbias Rencap d')
pid = k_d * (Vbias / d) * (Vbias / Rencap)

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

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

assert res == pytest.approx(2e-9)

def test_symbolic_df():
pid_df = pd.DataFrame([pid_kwarg] * 5)

res_series = pvdeg.symbolic.calc_df_symbolic(
expr=pid,
df=pid_df
)

pid_values = pd.Series([2e-9]*5)

pd.testing.assert_series_equal(res_series, pid_values, check_dtype=False)


def test_symbolic_timeseries():
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) )

module_temps = pvdeg.temperature.module(
weather_df=WEATHER,
meta=META,
conf="open_rack_glass_glass"
)
poa_irradiance = pvdeg.spectral.poa_irradiance(
weather_df=WEATHER,
meta=META
)

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]

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
).sum()

assert res == pytest.approx(6.5617e-09)

def test_calc_df_symbolic_bad():
expr = sp.symbols('not_in_columns')
df = pd.DataFrame([[1,2,3,5]],columns=['a','b','c','d'])

with pytest.raises(ValueError):
pvdeg.symbolic.calc_df_symbolic(expr=expr, df=df)

def test_calc_kwarg_timeseries_bad_type():
# try passing an invalid argument type
with pytest.raises(ValueError, match="only simple numerics or timeseries allowed"):
pvdeg.symbolic.calc_kwarg_timeseries(expr=None, kwarg={'bad':pd.DataFrame()})

def test_calc_kwarg_timeseries_bad_mismatch_lengths():
# arrays of different lengths
with pytest.raises(NotImplementedError, match="arrays/series are different lengths. fix mismatched length. otherwise arbitrary symbolic solution is too complex for solver. nested loops or loops dependent on previous results not supported."):
pvdeg.symbolic.calc_kwarg_timeseries(expr=None, kwarg={'len1':np.zeros((5,)), 'len2':np.zeros(10,)})

def test_calc_kwarg_timeseries_no_index():

v1, v2 = sp.symbols('v1 v2')
expr = v1 * v2
Loading

0 comments on commit 311b70b

Please sign in to comment.