3535
3636# Applied when no ensemble P10 data is available
3737_DEFAULT_P10_FALLBACK = 0.7
38+ _DEFAULT_P90_FALLBACK = 1.3
3839
3940
4041def 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