Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
2 changes: 1 addition & 1 deletion api/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ prometheus-fastapi-instrumentator==5.5.1
pandas==1.1.4
pymysql==0.10.1
sqlalchemy==1.3.20
pvlib==0.9.0a1
pvlib==0.9.0a4
numpy==1.19.4
scipy==1.5.4
requests==2.24.0
Expand Down
30 changes: 21 additions & 9 deletions api/solarperformanceinsight_api/compute.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,8 +222,8 @@ def process_single_modelchain(
).to_frame() # type: ignore
weather_sum = pd.DataFrame(
{
"poa_global": 0, # type: ignore
"effective_irradiance": 0, # type: ignore
"poa_global": 0, # type: ignore
"cell_temperature": 0, # type: ignore
},
index=performance.index,
Expand All @@ -233,13 +233,17 @@ def process_single_modelchain(
num_arrays = len(mc.system.arrays)
out = []
for i in range(num_arrays):
array_weather: pd.DataFrame = _get_index(results, "total_irrad", i)[
["poa_global"]
].copy() # type: ignore
# copy avoids setting values on a copy of slice later
array_weather.loc[:, "effective_irradiance"] = _get_index(
array_weather: pd.DataFrame = _get_index(
results, "effective_irradiance", i
).to_frame(
"effective_irradiance"
) # type: ignore
# total irrad empty if effective irradiance supplied initially
array_weather.loc[:, "poa_global"] = _get_index(
results, "total_irrad", i
).get( # type: ignore
"poa_global", float("NaN")
)
array_weather.loc[:, "cell_temperature"] = _get_index(
results, "cell_temperature", i
) # type: ignore
Expand All @@ -254,9 +258,17 @@ def process_single_modelchain(
)
# mean
weather_avg: pd.DataFrame = weather_sum / num_arrays # type: ignore
adjusted_zenith: pd.DataFrame = adjust(
results.solar_position[["zenith"]]
) # type: ignore
adjusted_zenith: pd.DataFrame
# not calculate if effective irradiance is provided
if results.solar_position is not None:
adjusted_zenith = adjust(results.solar_position[["zenith"]]) # type: ignore
else:
# calculate solar position making sure to shift times and shift back
adjusted_zenith = adjust(
mc.location.get_solarposition(
weather_avg.index.shift(freq=tshift) # type: ignore
)[["zenith"]]
) # type: ignore
summary_frame = pd.concat(
[
performance,
Expand Down
32 changes: 32 additions & 0 deletions api/solarperformanceinsight_api/conftest.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
from base64 import b64decode
from contextlib import contextmanager
from copy import deepcopy
import datetime as dt
from uuid import UUID, uuid1


from fakeredis import FakeRedis # type: ignore
from fastapi.testclient import TestClient
import httpx
from pvlib.pvsystem import PVSystem # type: ignore
from pvlib.tracking import SingleAxisTracker # type: ignore
import pymysql
import pytest
from rq import Queue # type: ignore
Expand Down Expand Up @@ -314,3 +317,32 @@ def async_queue(mock_redis, mocker):
q = Queue("jobs", connection=mock_redis)
mocker.patch.object(queuing, "_get_queue", return_value=q)
return q


@pytest.fixture()
def fixed_tracking():
return models.FixedTracking(tilt=32, azimuth=180.9)


@pytest.fixture()
def single_axis_tracking():
return models.SingleAxisTracking(
axis_tilt=0, axis_azimuth=179.8, backtracking=False, gcr=1.8
)


@pytest.fixture(params=["fixed_axis", "single_axis", "multi_array_fixed"])
def either_tracker(request, system_def, fixed_tracking, single_axis_tracking):
inv = system_def.inverters[0]
if request.param == "fixed_axis":
inv.arrays[0].tracking = fixed_tracking
return inv, PVSystem, False
elif request.param == "multi_array_fixed":
inv.arrays[0].tracking = fixed_tracking
arr1 = deepcopy(inv.arrays[0])
arr1.name = "Array 2"
inv.arrays.append(arr1)
return inv, PVSystem, True
else:
inv.arrays[0].tracking = single_axis_tracking
return inv, SingleAxisTracker, False
4 changes: 2 additions & 2 deletions api/solarperformanceinsight_api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ class PVWattsInverterParameters(BaseModel):
eta_inv_ref: float = Field(
0.9637, description="Reference inverter efficiency, unitless"
)
_modelchain_ac_model: str = PrivateAttr("pvwatts_multi")
_modelchain_ac_model: str = PrivateAttr("pvwatts")


class SandiaInverterParameters(BaseModel):
Expand Down Expand Up @@ -379,7 +379,7 @@ class SandiaInverterParameters(BaseModel):
" (i.e., night tare), W"
),
)
_modelchain_ac_model: str = PrivateAttr("sandia_multi")
_modelchain_ac_model: str = PrivateAttr("sandia")


class AOIModelEnum(str, Enum):
Expand Down
167 changes: 115 additions & 52 deletions api/solarperformanceinsight_api/tests/test_compute.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@


import pandas as pd
from pvlib.location import Location
from pvlib.modelchain import ModelChain
import pytest


from solarperformanceinsight_api import compute, storage, models
from solarperformanceinsight_api import compute, storage, models, pvmodeling


pytestmark = pytest.mark.usefixtures("add_example_db_data")
Expand Down Expand Up @@ -348,64 +350,125 @@ class Res:
assert pd.isna(nans).all()


def test_process_single_modelchain(mocker):
# pytest param ids are helpful finding combos that fail
@pytest.mark.parametrize(
"tempcols",
(
# pytest parm ids are
pytest.param(["temp_air", "wind_speed"], id="standard_temp"),
pytest.param(["temp_air"], id="air_temp_only"),
pytest.param(["module_temperature"], id="module_temp"),
pytest.param(["cell_temperature"], id="cell_temp"),
pytest.param(["module_temperature", "wind_speed"], id="module_temp+ws"),
pytest.param(["cell_temperature", "wind_speed"], id="cell_temp+ws"),
),
)
@pytest.mark.parametrize(
"method,colmap",
(
pytest.param("run_model", {}, id="run_model"),
pytest.param(
"run_model_from_poa",
{"ghi": "poa_global", "dni": "poa_direct", "dhi": "poa_diffuse"},
id="run_model_poa",
),
pytest.param(
"run_model_from_effective_irradiance",
{"ghi": "effective_irradiance", "dni": "noped", "dhi": "nah"},
id="run_model_eff",
),
),
)
def test_process_single_modelchain(
system_def, either_tracker, method, colmap, tempcols
):
# full run through a modelchain with a fixed tilt single array,
# fixed tilt two array, and single axis tracker single array
tshift = dt.timedelta(minutes=5)
df = pd.DataFrame({"poa_global": [1.0]}, index=[pd.Timestamp("2020-01-01T12:00")])
df.index.name = "time"
shifted = df.shift(freq=-tshift)

class Res:
ac = df["poa_global"]
total_irrad = (df, df)
effective_irradiance = (df, df)
cell_temperature = (df, df)
solar_position = pd.DataFrame({"zenith": 91.0}, index=df.index)

class Sys:
arrays = [0, 1]

class MC:
results = Res()
system = Sys()

def run_model(self, data):
pd.testing.assert_frame_equal(df, data[0])
return self

with pytest.raises(AttributeError):
compute.process_single_modelchain(MC(), [df], "run_from_poa", tshift, 0)
index = pd.DatetimeIndex([pd.Timestamp("2020-01-01T12:00:00-07:00")], name="time")
tempdf = pd.DataFrame(
{
"temp_air": [25.0],
"wind_speed": [10.0],
"module_temperature": [30.0],
"cell_temperature": [32.0],
"poa_global": [1100.0],
},
index=index,
)[tempcols]
irrad = pd.DataFrame(
{
"ghi": [1100.0],
"dni": [1000.0],
"dhi": [100.0],
},
index=index,
).rename(columns=colmap)
df = pd.concat([irrad, tempdf], axis=1)
inv, _, multi = either_tracker
location = Location(latitude=32.1, longitude=-110.8, altitude=2000, name="test")
pvsys = pvmodeling.construct_pvsystem(inv)
mc = ModelChain(system=pvsys, location=location, **dict(inv._modelchain_models))
weather = [df]
if multi:
weather.append(df)

# shifted (df - 5min) goes in, and shifted right (df) goes to be processed
dblist, summary = compute.process_single_modelchain(
MC(), [shifted], "run_model", tshift, 0
)
pd.testing.assert_frame_equal(
summary,
pd.DataFrame(
{
"performance": [1.0],
"poa_global": [1.0],
"effective_irradiance": [1.0],
"cell_temperature": [1.0],
"zenith": [91.0],
},
index=shifted.index,
),
)
dblist, summary = compute.process_single_modelchain(mc, weather, method, tshift, 0)
assert summary.performance.iloc[0] == 250.0
assert set(summary.columns) == {
"performance",
"poa_global",
"effective_irradiance",
"cell_temperature",
"zenith",
}
pd.testing.assert_index_equal(summary.index, df.index)

# performance for the inverter, and weather for each array
assert {d.schema_path for d in dblist} == {
"/inverters/0",
"/inverters/0/arrays/0",
"/inverters/0/arrays/1",
}
inv0arr0_weather = pd.read_feather(BytesIO(dblist[1].data))
exp_weather = shifted.copy()
exp_weather.loc[:, "effective_irradiance"] = 1.0
exp_weather.loc[:, "cell_temperature"] = 1.0
if multi:
assert {d.schema_path for d in dblist} == {
"/inverters/0",
"/inverters/0/arrays/0",
"/inverters/0/arrays/1",
}
else:
assert {d.schema_path for d in dblist} == {
"/inverters/0",
"/inverters/0/arrays/0",
}

inv_perf = list(
filter(
lambda x: x.type == "performance data" and x.schema_path == "/inverters/0",
dblist,
)
)[0]
pd.testing.assert_frame_equal(
inv0arr0_weather, exp_weather.astype("float32").reset_index()
pd.read_feather(BytesIO(inv_perf.data)),
pd.DataFrame(
{"performance": [250.0]}, dtype="float32", index=df.index
).reset_index(),
)
arr0_weather_df = pd.read_feather(
BytesIO(
list(
filter(
lambda x: x.type == "weather data"
and x.schema_path == "/inverters/0/arrays/0",
dblist,
)
)[0].data
)
)
assert set(arr0_weather_df.columns) == {
"poa_global",
"effective_irradiance",
"cell_temperature",
"time",
}
# pvlib>0.9.0a2
assert not pd.isna(arr0_weather_df.cell_temperature).any()


def test_run_performance_job(stored_job, auth0_id, nocommit_transaction, mocker):
Expand Down
33 changes: 1 addition & 32 deletions api/solarperformanceinsight_api/tests/test_pvmodeling.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
from copy import deepcopy
from inspect import signature


from pvlib.location import Location
from pvlib.modelchain import ModelChain
from pvlib.pvsystem import PVSystem
from pvlib.tracking import SingleAxisTracker
import pytest


from solarperformanceinsight_api import models, pvmodeling
from solarperformanceinsight_api import pvmodeling


def test_construct_location(system_def):
Expand All @@ -18,35 +16,6 @@ def test_construct_location(system_def):
assert isinstance(pvmodeling.construct_location(system_def), Location)


@pytest.fixture()
def fixed_tracking():
return models.FixedTracking(tilt=32, azimuth=180.9)


@pytest.fixture()
def single_axis_tracking():
return models.SingleAxisTracking(
axis_tilt=0, axis_azimuth=179.8, backtracking=False, gcr=1.8
)


@pytest.fixture(params=["fixed", "single", "multi_fixed"])
def either_tracker(request, system_def, fixed_tracking, single_axis_tracking):
inv = system_def.inverters[0]
if request.param == "fixed":
inv.arrays[0].tracking = fixed_tracking
return inv, PVSystem, False
elif request.param == "multi_fixed":
inv.arrays[0].tracking = fixed_tracking
arr1 = deepcopy(inv.arrays[0])
arr1.name = "Array 2"
inv.arrays.append(arr1)
return inv, PVSystem, True
else:
inv.arrays[0].tracking = single_axis_tracking
return inv, SingleAxisTracker, False


def test_construct_pvsystem(either_tracker):
inv, cls, multi = either_tracker
out = pvmodeling.construct_pvsystem(inv)
Expand Down