Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dev symbolic #104

Merged
merged 7 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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