Skip to content

Commit 7e55add

Browse files
Merge pull request #5347 from springfall2008/fix/pv-band-follows-raw-when-calibration-off-5345
2 parents 41b7360 + a2960a0 commit 7e55add

7 files changed

Lines changed: 541 additions & 65 deletions

File tree

‎apps/predbat/solar_model.py‎

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535

3636
# Applied when no ensemble P10 data is available
3737
_DEFAULT_P10_FALLBACK = 0.7
38+
_DEFAULT_P90_FALLBACK = 1.3
3839

3940

4041
def pvwatts_cell_temperature(poa_global, temp_air, wind_speed):
@@ -68,7 +69,7 @@ def _temperature_efficiency(gti, temp, wind):
6869
return max(0.5, min(1.1, 1.0 - _TEMP_COEFF * (t_cell - _STC_TEMP_C)))
6970

7071

71-
def gti_hourly_to_period_kwh(times, gti_values, temp_values, wind_values, kwp, system_loss, shading_factors=None, p10_instant=None, p10_fallback=_DEFAULT_P10_FALLBACK):
72+
def gti_hourly_to_period_kwh(times, gti_values, temp_values, wind_values, kwp, system_loss, shading_factors=None, band_ratio=None, p10_fallback=_DEFAULT_P10_FALLBACK, p90_fallback=_DEFAULT_P90_FALLBACK):
7273
"""Convert hourly GTI samples into per-hour PV energy for a single array.
7374
7475
Open-Meteo returns point-in-time irradiance (W/m2) at the start of each hour, so the
@@ -83,11 +84,13 @@ def gti_hourly_to_period_kwh(times, gti_values, temp_values, wind_values, kwp, s
8384
kwp: array peak power in kW
8485
system_loss: fractional system loss, e.g. 0.05 for 95% efficiency
8586
shading_factors: optional list of 12 per-month multipliers
86-
p10_instant: optional dict of timestamp string to raw P10 kW, before temperature derate
87-
p10_fallback: multiplier applied to P50 when p10_instant has no entry
87+
band_ratio: optional dict of timestamp string to (p10_ratio, p90_ratio), each relative to P50
88+
p10_fallback: multiplier applied to P50 when band_ratio has no entry
89+
p90_fallback: multiplier applied to P50 when band_ratio is given but has no entry
8890
8991
Returns:
90-
dict of tz-aware UTC hour-start datetime to {"pv_estimate": kWh, "pv_estimate10": kWh}
92+
dict of tz-aware UTC hour-start datetime to {"pv_estimate": kWh, "pv_estimate10": kWh},
93+
plus "pv_estimate90": kWh when band_ratio is given
9194
"""
9295
instant_kw = {}
9396
instant_stamps = []
@@ -102,15 +105,16 @@ def gti_hourly_to_period_kwh(times, gti_values, temp_values, wind_values, kwp, s
102105
wind = wind_values[idx] if idx < len(wind_values) and wind_values[idx] is not None else _DEFAULT_WIND_MS
103106
eta_temp = _temperature_efficiency(gti, temp, wind)
104107
pv50_inst = dp4((gti / 1000.0) * kwp * eta_temp * (1.0 - system_loss))
105-
raw_p10 = p10_instant.get(ts) if p10_instant else None
106-
# p10_instant was computed without temperature derating; apply eta_temp now
107-
pv10_inst = dp4(min(raw_p10 * eta_temp, pv50_inst) if raw_p10 is not None else pv50_inst * p10_fallback)
108+
# The ratios scale the already derated P50, held to the side of it they belong on
109+
p10_ratio, p90_ratio = band_ratio.get(ts, (p10_fallback, p90_fallback)) if band_ratio else (p10_fallback, p90_fallback)
110+
pv10_inst = dp4(pv50_inst * min(max(p10_ratio, 0.0), 1.0))
111+
pv90_inst = dp4(pv50_inst * max(p90_ratio, 1.0))
108112
try:
109113
stamp = datetime.strptime(ts, "%Y-%m-%dT%H:%M")
110114
stamp = stamp.replace(tzinfo=pytz.utc)
111115
except (ValueError, TypeError):
112116
continue
113-
instant_kw[stamp] = (pv50_inst, pv10_inst)
117+
instant_kw[stamp] = (pv50_inst, pv10_inst, pv90_inst)
114118
instant_stamps.append(stamp)
115119

116120
period_data = {}
@@ -119,16 +123,20 @@ def gti_hourly_to_period_kwh(times, gti_values, temp_values, wind_values, kwp, s
119123
next_stamp = instant_stamps[i + 1]
120124
if (next_stamp - stamp) != timedelta(hours=1):
121125
continue
122-
pv50_start, pv10_start = instant_kw[stamp]
123-
pv50_end, pv10_end = instant_kw[next_stamp]
126+
pv50_start, pv10_start, pv90_start = instant_kw[stamp]
127+
pv50_end, pv10_end, pv90_end = instant_kw[next_stamp]
124128
pv50 = dp4(0.5 * (pv50_start + pv50_end))
125129
pv10 = dp4(0.5 * (pv10_start + pv10_end))
130+
pv90 = dp4(0.5 * (pv90_start + pv90_end))
126131

127132
if shading_factors and len(shading_factors) == 12:
128133
shading_month = shading_factors[stamp.month - 1]
129134
pv50 = dp4(pv50 * shading_month)
130135
pv10 = dp4(pv10 * shading_month)
136+
pv90 = dp4(pv90 * shading_month)
131137

132138
period_data[stamp] = {"pv_estimate": pv50, "pv_estimate10": pv10}
139+
if band_ratio is not None:
140+
period_data[stamp]["pv_estimate90"] = pv90
133141

134142
return period_data

0 commit comments

Comments
 (0)