Skip to content

Commit 5db53ab

Browse files
authored
update for pvlib 0.9.0a4 (#124)
* update for pvlib 0.9.0a4 a4 because pvlib/pvlib-python#1162 is required along with a tbd issue for computing from effective irradiance closes #119 closes #109 also adds real tests of the pvlib modelchain to catch errors there * mypy * test more modelchain configurations * fix when missing poa global or solar position for from_effective_irr * make sure poa_global is NaN not None * use pvlib 0.9.0a4 * comment update
1 parent 532fe83 commit 5db53ab

File tree

6 files changed

+174
-96
lines changed

6 files changed

+174
-96
lines changed

api/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ prometheus-fastapi-instrumentator==5.5.1
2626
pandas==1.1.4
2727
pymysql==0.10.1
2828
sqlalchemy==1.3.20
29-
pvlib==0.9.0a1
29+
pvlib==0.9.0a4
3030
numpy==1.19.4
3131
scipy==1.5.4
3232
requests==2.24.0

api/solarperformanceinsight_api/compute.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -222,8 +222,8 @@ def process_single_modelchain(
222222
).to_frame() # type: ignore
223223
weather_sum = pd.DataFrame(
224224
{
225-
"poa_global": 0, # type: ignore
226225
"effective_irradiance": 0, # type: ignore
226+
"poa_global": 0, # type: ignore
227227
"cell_temperature": 0, # type: ignore
228228
},
229229
index=performance.index,
@@ -233,13 +233,17 @@ def process_single_modelchain(
233233
num_arrays = len(mc.system.arrays)
234234
out = []
235235
for i in range(num_arrays):
236-
array_weather: pd.DataFrame = _get_index(results, "total_irrad", i)[
237-
["poa_global"]
238-
].copy() # type: ignore
239-
# copy avoids setting values on a copy of slice later
240-
array_weather.loc[:, "effective_irradiance"] = _get_index(
236+
array_weather: pd.DataFrame = _get_index(
241237
results, "effective_irradiance", i
238+
).to_frame(
239+
"effective_irradiance"
242240
) # type: ignore
241+
# total irrad empty if effective irradiance supplied initially
242+
array_weather.loc[:, "poa_global"] = _get_index(
243+
results, "total_irrad", i
244+
).get( # type: ignore
245+
"poa_global", float("NaN")
246+
)
243247
array_weather.loc[:, "cell_temperature"] = _get_index(
244248
results, "cell_temperature", i
245249
) # type: ignore
@@ -254,9 +258,19 @@ def process_single_modelchain(
254258
)
255259
# mean
256260
weather_avg: pd.DataFrame = weather_sum / num_arrays # type: ignore
257-
adjusted_zenith: pd.DataFrame = adjust(
258-
results.solar_position[["zenith"]]
259-
) # type: ignore
261+
adjusted_zenith: pd.DataFrame
262+
# not calculated if effective irradiance is provided
263+
if results.solar_position is not None:
264+
adjusted_zenith = adjust(results.solar_position[["zenith"]]) # type: ignore
265+
else:
266+
# calculate solar position making sure to shift times and shift back
267+
# modelchain passes through air temperature and pressure, but that only
268+
# affects apparent_zenith
269+
adjusted_zenith = adjust(
270+
mc.location.get_solarposition(
271+
weather_avg.index.shift(freq=tshift) # type: ignore
272+
)[["zenith"]]
273+
) # type: ignore
260274
summary_frame = pd.concat(
261275
[
262276
performance,

api/solarperformanceinsight_api/conftest.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
from base64 import b64decode
22
from contextlib import contextmanager
3+
from copy import deepcopy
34
import datetime as dt
45
from uuid import UUID, uuid1
56

67

78
from fakeredis import FakeRedis # type: ignore
89
from fastapi.testclient import TestClient
910
import httpx
11+
from pvlib.pvsystem import PVSystem # type: ignore
12+
from pvlib.tracking import SingleAxisTracker # type: ignore
1013
import pymysql
1114
import pytest
1215
from rq import Queue # type: ignore
@@ -314,3 +317,32 @@ def async_queue(mock_redis, mocker):
314317
q = Queue("jobs", connection=mock_redis)
315318
mocker.patch.object(queuing, "_get_queue", return_value=q)
316319
return q
320+
321+
322+
@pytest.fixture()
323+
def fixed_tracking():
324+
return models.FixedTracking(tilt=32, azimuth=180.9)
325+
326+
327+
@pytest.fixture()
328+
def single_axis_tracking():
329+
return models.SingleAxisTracking(
330+
axis_tilt=0, axis_azimuth=179.8, backtracking=False, gcr=1.8
331+
)
332+
333+
334+
@pytest.fixture(params=["fixed_axis", "single_axis", "multi_array_fixed"])
335+
def either_tracker(request, system_def, fixed_tracking, single_axis_tracking):
336+
inv = system_def.inverters[0]
337+
if request.param == "fixed_axis":
338+
inv.arrays[0].tracking = fixed_tracking
339+
return inv, PVSystem, False
340+
elif request.param == "multi_array_fixed":
341+
inv.arrays[0].tracking = fixed_tracking
342+
arr1 = deepcopy(inv.arrays[0])
343+
arr1.name = "Array 2"
344+
inv.arrays.append(arr1)
345+
return inv, PVSystem, True
346+
else:
347+
inv.arrays[0].tracking = single_axis_tracking
348+
return inv, SingleAxisTracker, False

api/solarperformanceinsight_api/models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ class PVWattsInverterParameters(BaseModel):
339339
eta_inv_ref: float = Field(
340340
0.9637, description="Reference inverter efficiency, unitless"
341341
)
342-
_modelchain_ac_model: str = PrivateAttr("pvwatts_multi")
342+
_modelchain_ac_model: str = PrivateAttr("pvwatts")
343343

344344

345345
class SandiaInverterParameters(BaseModel):
@@ -402,7 +402,7 @@ class SandiaInverterParameters(BaseModel):
402402
" (i.e., night tare), W"
403403
),
404404
)
405-
_modelchain_ac_model: str = PrivateAttr("sandia_multi")
405+
_modelchain_ac_model: str = PrivateAttr("sandia")
406406

407407

408408
class AOIModelEnum(str, Enum):

api/solarperformanceinsight_api/tests/test_compute.py

Lines changed: 115 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55

66

77
import pandas as pd
8+
from pvlib.location import Location
9+
from pvlib.modelchain import ModelChain
810
import pytest
911

1012

11-
from solarperformanceinsight_api import compute, storage, models
13+
from solarperformanceinsight_api import compute, storage, models, pvmodeling
1214

1315

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

350352

351-
def test_process_single_modelchain(mocker):
353+
# pytest param ids are helpful finding combos that fail
354+
@pytest.mark.parametrize(
355+
"tempcols",
356+
(
357+
# pytest parm ids are
358+
pytest.param(["temp_air", "wind_speed"], id="standard_temp"),
359+
pytest.param(["temp_air"], id="air_temp_only"),
360+
pytest.param(["module_temperature"], id="module_temp"),
361+
pytest.param(["cell_temperature"], id="cell_temp"),
362+
pytest.param(["module_temperature", "wind_speed"], id="module_temp+ws"),
363+
pytest.param(["cell_temperature", "wind_speed"], id="cell_temp+ws"),
364+
),
365+
)
366+
@pytest.mark.parametrize(
367+
"method,colmap",
368+
(
369+
pytest.param("run_model", {}, id="run_model"),
370+
pytest.param(
371+
"run_model_from_poa",
372+
{"ghi": "poa_global", "dni": "poa_direct", "dhi": "poa_diffuse"},
373+
id="run_model_poa",
374+
),
375+
pytest.param(
376+
"run_model_from_effective_irradiance",
377+
{"ghi": "effective_irradiance", "dni": "noped", "dhi": "nah"},
378+
id="run_model_eff",
379+
),
380+
),
381+
)
382+
def test_process_single_modelchain(
383+
system_def, either_tracker, method, colmap, tempcols
384+
):
385+
# full run through a modelchain with a fixed tilt single array,
386+
# fixed tilt two array, and single axis tracker single array
352387
tshift = dt.timedelta(minutes=5)
353-
df = pd.DataFrame({"poa_global": [1.0]}, index=[pd.Timestamp("2020-01-01T12:00")])
354-
df.index.name = "time"
355-
shifted = df.shift(freq=-tshift)
356-
357-
class Res:
358-
ac = df["poa_global"]
359-
total_irrad = (df, df)
360-
effective_irradiance = (df, df)
361-
cell_temperature = (df, df)
362-
solar_position = pd.DataFrame({"zenith": 91.0}, index=df.index)
363-
364-
class Sys:
365-
arrays = [0, 1]
366-
367-
class MC:
368-
results = Res()
369-
system = Sys()
370-
371-
def run_model(self, data):
372-
pd.testing.assert_frame_equal(df, data[0])
373-
return self
374-
375-
with pytest.raises(AttributeError):
376-
compute.process_single_modelchain(MC(), [df], "run_from_poa", tshift, 0)
388+
index = pd.DatetimeIndex([pd.Timestamp("2020-01-01T12:00:00-07:00")], name="time")
389+
tempdf = pd.DataFrame(
390+
{
391+
"temp_air": [25.0],
392+
"wind_speed": [10.0],
393+
"module_temperature": [30.0],
394+
"cell_temperature": [32.0],
395+
"poa_global": [1100.0],
396+
},
397+
index=index,
398+
)[tempcols]
399+
irrad = pd.DataFrame(
400+
{
401+
"ghi": [1100.0],
402+
"dni": [1000.0],
403+
"dhi": [100.0],
404+
},
405+
index=index,
406+
).rename(columns=colmap)
407+
df = pd.concat([irrad, tempdf], axis=1)
408+
inv, _, multi = either_tracker
409+
location = Location(latitude=32.1, longitude=-110.8, altitude=2000, name="test")
410+
pvsys = pvmodeling.construct_pvsystem(inv)
411+
mc = ModelChain(system=pvsys, location=location, **dict(inv._modelchain_models))
412+
weather = [df]
413+
if multi:
414+
weather.append(df)
377415

378416
# shifted (df - 5min) goes in, and shifted right (df) goes to be processed
379-
dblist, summary = compute.process_single_modelchain(
380-
MC(), [shifted], "run_model", tshift, 0
381-
)
382-
pd.testing.assert_frame_equal(
383-
summary,
384-
pd.DataFrame(
385-
{
386-
"performance": [1.0],
387-
"poa_global": [1.0],
388-
"effective_irradiance": [1.0],
389-
"cell_temperature": [1.0],
390-
"zenith": [91.0],
391-
},
392-
index=shifted.index,
393-
),
394-
)
417+
dblist, summary = compute.process_single_modelchain(mc, weather, method, tshift, 0)
418+
assert summary.performance.iloc[0] == 250.0
419+
assert set(summary.columns) == {
420+
"performance",
421+
"poa_global",
422+
"effective_irradiance",
423+
"cell_temperature",
424+
"zenith",
425+
}
426+
pd.testing.assert_index_equal(summary.index, df.index)
395427

396428
# performance for the inverter, and weather for each array
397-
assert {d.schema_path for d in dblist} == {
398-
"/inverters/0",
399-
"/inverters/0/arrays/0",
400-
"/inverters/0/arrays/1",
401-
}
402-
inv0arr0_weather = pd.read_feather(BytesIO(dblist[1].data))
403-
exp_weather = shifted.copy()
404-
exp_weather.loc[:, "effective_irradiance"] = 1.0
405-
exp_weather.loc[:, "cell_temperature"] = 1.0
429+
if multi:
430+
assert {d.schema_path for d in dblist} == {
431+
"/inverters/0",
432+
"/inverters/0/arrays/0",
433+
"/inverters/0/arrays/1",
434+
}
435+
else:
436+
assert {d.schema_path for d in dblist} == {
437+
"/inverters/0",
438+
"/inverters/0/arrays/0",
439+
}
440+
441+
inv_perf = list(
442+
filter(
443+
lambda x: x.type == "performance data" and x.schema_path == "/inverters/0",
444+
dblist,
445+
)
446+
)[0]
406447
pd.testing.assert_frame_equal(
407-
inv0arr0_weather, exp_weather.astype("float32").reset_index()
448+
pd.read_feather(BytesIO(inv_perf.data)),
449+
pd.DataFrame(
450+
{"performance": [250.0]}, dtype="float32", index=df.index
451+
).reset_index(),
452+
)
453+
arr0_weather_df = pd.read_feather(
454+
BytesIO(
455+
list(
456+
filter(
457+
lambda x: x.type == "weather data"
458+
and x.schema_path == "/inverters/0/arrays/0",
459+
dblist,
460+
)
461+
)[0].data
462+
)
408463
)
464+
assert set(arr0_weather_df.columns) == {
465+
"poa_global",
466+
"effective_irradiance",
467+
"cell_temperature",
468+
"time",
469+
}
470+
# pvlib>0.9.0a2
471+
assert not pd.isna(arr0_weather_df.cell_temperature).any()
409472

410473

411474
def test_run_performance_job(stored_job, auth0_id, nocommit_transaction, mocker):

api/solarperformanceinsight_api/tests/test_pvmodeling.py

Lines changed: 1 addition & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
1-
from copy import deepcopy
21
from inspect import signature
32

43

54
from pvlib.location import Location
65
from pvlib.modelchain import ModelChain
76
from pvlib.pvsystem import PVSystem
87
from pvlib.tracking import SingleAxisTracker
9-
import pytest
108

119

12-
from solarperformanceinsight_api import models, pvmodeling
10+
from solarperformanceinsight_api import pvmodeling
1311

1412

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

2018

21-
@pytest.fixture()
22-
def fixed_tracking():
23-
return models.FixedTracking(tilt=32, azimuth=180.9)
24-
25-
26-
@pytest.fixture()
27-
def single_axis_tracking():
28-
return models.SingleAxisTracking(
29-
axis_tilt=0, axis_azimuth=179.8, backtracking=False, gcr=1.8
30-
)
31-
32-
33-
@pytest.fixture(params=["fixed", "single", "multi_fixed"])
34-
def either_tracker(request, system_def, fixed_tracking, single_axis_tracking):
35-
inv = system_def.inverters[0]
36-
if request.param == "fixed":
37-
inv.arrays[0].tracking = fixed_tracking
38-
return inv, PVSystem, False
39-
elif request.param == "multi_fixed":
40-
inv.arrays[0].tracking = fixed_tracking
41-
arr1 = deepcopy(inv.arrays[0])
42-
arr1.name = "Array 2"
43-
inv.arrays.append(arr1)
44-
return inv, PVSystem, True
45-
else:
46-
inv.arrays[0].tracking = single_axis_tracking
47-
return inv, SingleAxisTracker, False
48-
49-
5019
def test_construct_pvsystem(either_tracker):
5120
inv, cls, multi = either_tracker
5221
out = pvmodeling.construct_pvsystem(inv)

0 commit comments

Comments
 (0)