captest 0.13.3rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
captest/prtest.py ADDED
@@ -0,0 +1,416 @@
1
+ import warnings
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+ import param
6
+
7
+ from captest import capdata
8
+
9
+
10
+ emp_heat_coeff = {
11
+ "open_rack": {
12
+ "glass_cell_glass": {"a": -3.47, "b": -0.0594, "del_tcnd": 3},
13
+ "glass_cell_poly": {"a": -3.56, "b": -0.0750, "del_tcnd": 3},
14
+ "poly_tf_steel": {"a": -3.58, "b": -0.1130, "del_tcnd": 3},
15
+ },
16
+ "close_roof_mount": {"glass_cell_glass": {"a": -2.98, "b": -0.0471, "del_tcnd": 1}},
17
+ "insulated_back": {"glass_cell_poly": {"a": -2.81, "b": -0.0455, "del_tcnd": 0}},
18
+ }
19
+
20
+
21
+ def get_common_timestep(data, units="m", string_output=True):
22
+ """
23
+ Get the most commonly occuring timestep of data as frequency string.
24
+ Parameters
25
+ ----------
26
+ data : Series or DataFrame
27
+ Data with a DateTimeIndex.
28
+ units : str, default 'm'
29
+ String representing date/time unit, such as (D)ay, (M)onth, (Y)ear,
30
+ (h)ours, (m)inutes, or (s)econds.
31
+ string_output : bool, default True
32
+ Set to False to return a numeric value.
33
+ Returns
34
+ -------
35
+ str
36
+ frequency string
37
+ """
38
+ units_abbrev = {
39
+ "D": "Day",
40
+ "M": "Months",
41
+ "Y": "Year",
42
+ "h": "hours",
43
+ "m": "minutes",
44
+ "s": "seconds",
45
+ }
46
+ common_timestep = data.index.to_series().diff().mode().values[0]
47
+ common_timestep_tdelta = common_timestep.astype("timedelta64[m]")
48
+ freq = common_timestep_tdelta / np.timedelta64(1, units)
49
+ if string_output:
50
+ return str(freq) + " " + units_abbrev[units]
51
+ else:
52
+ return freq
53
+
54
+
55
+ def temp_correct_power(power, power_temp_coeff, cell_temp, base_temp=25):
56
+ """Apply temperature correction to PV power.
57
+
58
+ Divides `power` by the temperature correction, so low power values that
59
+ are above `base_temp` will be increased and high power values that are
60
+ below the `base_temp` will be decreased.
61
+
62
+ Parameters
63
+ ----------
64
+ power : numeric or Series
65
+ PV power (in watts) to correct to the `base_temp`.
66
+ power_temp_coeff : numeric
67
+ Module power temperature coefficient as percent per degree celsius.
68
+ Ex. -0.36
69
+ cell_temp : numeric or Series
70
+ Cell temperature (in Celsius) used to calculate temperature
71
+ differential from the `base_temp`.
72
+ base_temp : numeric, default 25
73
+ Base temperature (in Celsius) to correct power to. Default is the
74
+ STC of 25 degrees Celsius.
75
+
76
+ Returns
77
+ -------
78
+ type matches `power`
79
+ Power corrected for temperature.
80
+ """
81
+ corr_power = power / (1 + ((power_temp_coeff / 100) * (cell_temp - base_temp)))
82
+ return corr_power
83
+
84
+
85
+ def back_of_module_temp(
86
+ poa, temp_amb, wind_speed, module_type="glass_cell_poly", racking="open_rack"
87
+ ):
88
+ """Calculate back of module temperature from measured weather data.
89
+
90
+ Calculate back of module temperature from POA irradiance, ambient
91
+ temperature, wind speed (at height of 10 meters), and empirically
92
+ derived heat transfer coefficients.
93
+
94
+ Equation from NREL Weather Corrected Performance Ratio Report.
95
+
96
+ Parameters
97
+ ----------
98
+ poa : numeric or Series
99
+ POA irradiance in W/m^2.
100
+ temp_amb : numeric or Series
101
+ Ambient temperature in degrees C.
102
+ wind_speed : numeric or Series
103
+ Measured wind speed (m/sec) corrected to measurement height of
104
+ 10 meters.
105
+ module_type : str, default 'glass_cell_poly'
106
+ Any of glass_cell_poly, glass_cell_glass, or 'poly_tf_steel'.
107
+ racking: str, default 'open_rack'
108
+ Any of 'open_rack', 'close_roof_mount', or 'insulated_back'
109
+
110
+ Returns
111
+ -------
112
+ numeric or Series
113
+ Back of module temperatures.
114
+ """
115
+ a = emp_heat_coeff[racking][module_type]["a"]
116
+ b = emp_heat_coeff[racking][module_type]["b"]
117
+ return poa * np.exp(a + b * wind_speed) + temp_amb
118
+
119
+
120
+ def cell_temp(bom, poa, module_type="glass_cell_poly", racking="open_rack"):
121
+ """Calculate cell temp from BOM temp, POA, and heat transfer coefficient.
122
+
123
+ Equation from NREL Weather Corrected Performance Ratio Report.
124
+
125
+ Parameters
126
+ ----------
127
+ bom : numeric or Series
128
+ Back of module temperature (degrees C). Strictly followin the NREL
129
+ procedure this value would be obtained from the `back_of_module_temp`
130
+ function.
131
+
132
+ Alternatively, a measured BOM temperature may be used.
133
+
134
+ Refer to p.7 of NREL Weather Corrected Performance Ratio Report.
135
+ poa : numeric or Series
136
+ POA irradiance in W/m^2.
137
+ module_type : str, default 'glass_cell_poly'
138
+ Any of glass_cell_poly, glass_cell_glass, or 'poly_tf_steel'.
139
+ racking: str, default 'open_rack'
140
+ Any of 'open_rack', 'close_roof_mount', or 'insulated_back'
141
+
142
+ Returns
143
+ -------
144
+ numeric or Series
145
+ Cell temperature(s).
146
+ """
147
+ return bom + (poa / 1000) * emp_heat_coeff[racking][module_type]["del_tcnd"]
148
+
149
+
150
+ def avg_typ_cell_temp(poa, cell_temp):
151
+ """Calculate irradiance weighted cell temperature.
152
+
153
+ Parameters
154
+ ----------
155
+ poa : Series
156
+ POA irradiance (W/m^2).
157
+ cell_temp : Series
158
+ Cell temperature for each interval (degrees C).
159
+
160
+ Returns
161
+ -------
162
+ float
163
+ Average irradiance-weighted cell temperature.
164
+ """
165
+ return (poa * cell_temp).sum() / poa.sum()
166
+
167
+
168
+ """DIRECTLY BELOW DRAFT PR FUNCTION TO DO ALL VERSIONS OF CALC
169
+ DECIDED TO BREAK INTO SMALLER FUNCTIONS, LEAVE TEMPORARILY"""
170
+
171
+
172
+ def perf_ratio_inputs_ok(ac_energy, dc_nameplate, poa, availability=1):
173
+ """Check types of perf_ratio arguments.
174
+
175
+ Parameters
176
+ ----------
177
+ ac_energy : Series
178
+ Measured energy production (Wh) from system meter.
179
+ dc_nameplate : numeric
180
+ Summation of nameplate ratings (W) for all installed modules of system
181
+ under test.
182
+ poa : Series
183
+ POA irradiance (W/m^2) for each time interval of the test.
184
+ availability : numeric or Series, default 1
185
+ Apply an adjustment for plant availability to the expected power
186
+ (denominator).
187
+ """
188
+ if not isinstance(ac_energy, pd.Series):
189
+ warnings.warn("ac_energy must be a Pandas Series.")
190
+ return False
191
+ elif not isinstance(poa, pd.Series):
192
+ warnings.warn("poa must be a Pandas Series.")
193
+ return False
194
+ elif not ac_energy.index.equals(poa.index):
195
+ warnings.warn("indices of poa and ac_energy must match.")
196
+ return False
197
+ elif isinstance(availability, pd.Series):
198
+ if not availability.index.equals(poa.index):
199
+ warnings.warn(
200
+ "Index of availability must match the index of the poa and ac_energy."
201
+ )
202
+ return False
203
+ else:
204
+ return True
205
+ else:
206
+ return True
207
+
208
+
209
+ def perf_ratio(
210
+ ac_energy,
211
+ dc_nameplate,
212
+ poa,
213
+ unit_adj=1,
214
+ degradation=0,
215
+ year=1,
216
+ availability=1,
217
+ ):
218
+ """Calculate performance ratio.
219
+
220
+ Parameters
221
+ ----------
222
+ ac_energy : Series
223
+ Measured energy production (Wh) from system meter.
224
+ dc_nameplate : numeric
225
+ Summation of nameplate ratings (W) for all installed modules of system
226
+ under test.
227
+ poa : Series
228
+ POA irradiance (W/m^2) for each time interval of the test.
229
+ unit_adj : numeric, default 1
230
+ Scale factor to adjust units of `ac_energy`. For exmaple pass 1000
231
+ to convert measured energy from kWh to Wh within PR calculation.
232
+ degradation : numeric, default None
233
+ Apply a derate (percent, Ex: 0.5%) for degradation to the expected
234
+ power (denominator). Must also pass specify a value for the `year`
235
+ argument.
236
+ NOTE: Percent is divided by 100 to convert to decimal within function.
237
+ year : numeric
238
+ Year of operation to use in degradation calculation.
239
+ availability : numeric or Series, default 1
240
+ Apply an adjustment for plant availability to the expected power
241
+ (denominator).
242
+
243
+ Returns
244
+ -------
245
+ PrResults
246
+ Instance of class PrResults.
247
+ """
248
+ if not perf_ratio_inputs_ok(
249
+ ac_energy, dc_nameplate, poa, availability=availability
250
+ ):
251
+ return
252
+
253
+ timestep = get_common_timestep(poa, units="h", string_output=False)
254
+ timestep_str = get_common_timestep(poa, units="h", string_output=True)
255
+
256
+ expected_dc = (
257
+ availability
258
+ * dc_nameplate
259
+ * poa
260
+ / 1000
261
+ * (1 - degradation / 100) ** year
262
+ * timestep
263
+ )
264
+ pr = ac_energy.sum() * unit_adj / expected_dc.sum()
265
+
266
+ input_cd = capdata.CapData("input_cd")
267
+ input_cd.data = pd.concat([poa, ac_energy], axis=1)
268
+
269
+ pr_per_timestep = ac_energy * unit_adj / expected_dc
270
+ results_data = pd.concat([ac_energy, expected_dc, pr_per_timestep], axis=1)
271
+ results_data.columns = ["ac_energy", "expected_dc", "pr_per_timestep"]
272
+
273
+ results = PrResults(
274
+ timestep=(timestep, timestep_str),
275
+ pr=pr,
276
+ dc_nameplate=dc_nameplate,
277
+ input_data=input_cd,
278
+ results_data=results_data,
279
+ )
280
+ return results
281
+
282
+
283
+ def perf_ratio_temp_corr_nrel(
284
+ ac_energy,
285
+ dc_nameplate,
286
+ poa,
287
+ power_temp_coeff=None,
288
+ temp_amb=None,
289
+ wind_speed=None,
290
+ base_temp=25,
291
+ module_type="glass_cell_poly",
292
+ racking="open_rack",
293
+ unit_adj=1,
294
+ degradation=None,
295
+ year=None,
296
+ availability=1,
297
+ ):
298
+ """Calculate performance ratio.
299
+
300
+ Parameters
301
+ ----------
302
+ ac_energy : Series
303
+ Measured energy production (kWh) from system meter.
304
+ dc_nameplate : numeric
305
+ Summation of nameplate ratings (W) for all installed modules of system
306
+ under test.
307
+ poa : Series
308
+ POA irradiance (W/m^2) for each time interval of the test.
309
+ power_temp_coeff : numeric, default None
310
+ Module power temperature coefficient as percent per degree celsius.
311
+ Ex. -0.36
312
+ temp_amb : Series
313
+ Ambient temperature (degrees C) measurements.
314
+ wind_speed : Series
315
+ Measured wind speed (m/sec) corrected to measurement height of
316
+ 10 meters.
317
+ base_temp : numeric, default 25
318
+ Base temperature (in Celsius) to correct power to. Default is the
319
+ STC of 25 degrees Celsius. The NREL Weather-Corrected Performance
320
+ Ratio technical report uses the term 'Tcell_typ_avg' for this value.
321
+ module_type : str, default 'glass_cell_poly'
322
+ Any of glass_cell_poly, glass_cell_glass, or 'poly_tf_steel'.
323
+ racking: str, default 'open_rack'
324
+ Any of 'open_rack', 'close_roof_mount', or 'insulated_back'
325
+ unit_adj : numeric, default 1
326
+ Scale factor to adjust units of `ac_energy`. For exmaple pass 1000
327
+ to convert measured energy from kWh to Wh within PR calculation.
328
+ degradation : numeric, default None
329
+ NOT IMPLEMENTED
330
+ Apply a derate for degradation to the expected power (denominator).
331
+ Must also pass specify a value for the `year` argument.
332
+ year : numeric
333
+ NOT IMPLEMENTED
334
+ Year of operation to use in degradation calculation.
335
+ availability : numeric or Series, default 1
336
+ NOT IMPLEMENTED
337
+ Apply an adjustment for plant availability to the expected power
338
+ (denominator).
339
+
340
+ Returns
341
+ -------
342
+ """
343
+ timestep = get_common_timestep(poa, units="h", string_output=False)
344
+ timestep_str = get_common_timestep(poa, units="h", string_output=True)
345
+
346
+ temp_bom = back_of_module_temp(poa, temp_amb, wind_speed, module_type, racking)
347
+ temp_cell = cell_temp(temp_bom, poa, module_type, racking)
348
+ dc_nameplate_temp_corr = temp_correct_power(
349
+ dc_nameplate, power_temp_coeff, temp_cell, base_temp=base_temp
350
+ )
351
+ # below is same as the perf_ratio function
352
+ # move to a separate function?
353
+ expected_dc = (
354
+ availability
355
+ * dc_nameplate_temp_corr
356
+ * poa
357
+ / 1000
358
+ # * (1 - degradation / 100)**year
359
+ * timestep
360
+ )
361
+ pr = ac_energy.sum() * unit_adj / expected_dc.sum()
362
+
363
+ input_cd = capdata.CapData("input_cd")
364
+ input_cd.data = pd.concat([poa, ac_energy], axis=1)
365
+
366
+ pr_per_timestep = ac_energy * unit_adj / expected_dc
367
+ results_data = pd.concat([ac_energy, expected_dc, pr_per_timestep], axis=1)
368
+ results_data.columns = ["ac_energy", "expected_dc", "pr_per_timestep"]
369
+
370
+ results = PrResults(
371
+ timestep=(timestep, timestep_str),
372
+ pr=pr,
373
+ dc_nameplate=dc_nameplate,
374
+ input_data=input_cd,
375
+ results_data=results_data,
376
+ )
377
+ return results
378
+
379
+
380
+ class PrResults(param.Parameterized):
381
+ """
382
+ Results from a PR calculation.
383
+ """
384
+
385
+ dc_nameplate = param.Number(
386
+ bounds=(0, None),
387
+ doc=("Summation of nameplate ratings (W) for all installed modules of system."),
388
+ )
389
+ pr = param.Number(doc="Performance ratio result decimal fraction.")
390
+ timestep = param.Tuple(doc="Timestep of series.")
391
+ expected_pr = param.Number(
392
+ bounds=(0, 1), doc="Expected Performance ratio result decimal fraction."
393
+ )
394
+ input_data = param.ClassSelector(capdata.CapData)
395
+ results_data = param.ClassSelector(pd.DataFrame)
396
+
397
+ def print_pr_result(self):
398
+ """Print summary of PR result - passing / failing and by how much"""
399
+ if self.pr >= self.expected_pr:
400
+ print(
401
+ "The test is PASSING with a measured PR of {:.2f}, "
402
+ "which is {:.2f} above the expected PR of {:.2f}".format(
403
+ self.pr * 100,
404
+ (self.pr - self.expected_pr) * 100,
405
+ self.expected_pr * 100,
406
+ )
407
+ )
408
+ else:
409
+ print(
410
+ "The test is FAILING with a measured PR of {:.2f}, "
411
+ "which is {:.2f} below the expected PR of {:.2f}".format(
412
+ self.pr * 100,
413
+ (self.expected_pr - self.pr) * 100,
414
+ self.expected_pr * 100,
415
+ )
416
+ )
captest/util.py ADDED
@@ -0,0 +1,134 @@
1
+ import re
2
+ import json
3
+ import yaml
4
+ import numpy as np
5
+ import pandas as pd
6
+
7
+
8
+ def read_json(path):
9
+ with open(path) as f:
10
+ json_data = json.load(f)
11
+ return json_data
12
+
13
+
14
+ def read_yaml(path):
15
+ with open(path, "r") as stream:
16
+ try:
17
+ data = yaml.safe_load(stream)
18
+ except yaml.YAMLError as exc:
19
+ print(exc)
20
+ return data
21
+
22
+
23
+ def get_common_timestep(data, units="m", string_output=True):
24
+ """
25
+ Get the most commonly occuring timestep of data as frequency string.
26
+
27
+ Parameters
28
+ ----------
29
+ data : Series or DataFrame
30
+ Data with a DateTimeIndex.
31
+ units : str, default 'm'
32
+ String representing date/time unit, such as (D)ay, (M)onth, (Y)ear,
33
+ (h)ours, (m)inutes, or (s)econds.
34
+ string_output : bool, default True
35
+ Set to False to return a numeric value.
36
+
37
+ Returns
38
+ -------
39
+ str or numeric
40
+ If the `string_output` is True and the most common timestep is an integer
41
+ in the specified units then a valid pandas frequency or offset alias is
42
+ returned.
43
+ If `string_output` is false, then a numeric value is returned.
44
+ """
45
+ units_abbrev = {"D": "D", "M": "M", "Y": "Y", "h": "H", "m": "min", "s": "S"}
46
+ common_timestep = data.index.to_series().diff().mode().values[0]
47
+ common_timestep_tdelta = common_timestep.astype("timedelta64[m]")
48
+ freq = common_timestep_tdelta / np.timedelta64(1, units)
49
+ if string_output:
50
+ try:
51
+ return str(int(freq)) + units_abbrev[units]
52
+ except Exception:
53
+ return str(freq) + units_abbrev[units]
54
+ else:
55
+ return freq
56
+
57
+
58
+ def reindex_datetime(data, report=False):
59
+ """
60
+ Find dataframe index frequency and reindex to add any missing intervals.
61
+
62
+ Sorts index of passed dataframe before reindexing.
63
+
64
+ Parameters
65
+ ----------
66
+ data : DataFrame
67
+ DataFrame to be reindexed.
68
+
69
+ Returns
70
+ -------
71
+ Reindexed DataFrame
72
+ """
73
+ data_index_length = data.shape[0]
74
+ df = data.copy()
75
+ df.sort_index(inplace=True)
76
+
77
+ freq_str = get_common_timestep(data, string_output=True)
78
+ full_ix = pd.date_range(start=df.index[0], end=df.index[-1], freq=freq_str)
79
+ df = df.reindex(index=full_ix)
80
+ df_index_length = df.shape[0]
81
+ missing_intervals = df_index_length - data_index_length
82
+
83
+ if report:
84
+ print("Frequency determined to be " + freq_str + " minutes.")
85
+ print("{:,} intervals added to index.".format(missing_intervals))
86
+ print("")
87
+
88
+ return df, missing_intervals, freq_str
89
+
90
+
91
+ def generate_irr_distribution(lowest_irr, highest_irr, rng=np.random.default_rng(82)):
92
+ """
93
+ Create a list of increasing values similar to POA irradiance data.
94
+
95
+ Default parameters result in increasing values where the difference
96
+ between each subsquent value is randomly chosen from the typical range
97
+ of steps for a POA tracker.
98
+
99
+ Parameters
100
+ ----------
101
+ lowest_irr : numeric
102
+ Lowest value in the list of values returned.
103
+ highest_irr : numeric
104
+ Highest value in the list of values returned.
105
+ rng : Numpy Random Generator
106
+ Instance of the default Generator.
107
+
108
+ Returns
109
+ -------
110
+ irr_values : list
111
+ """
112
+ irr_values = [
113
+ lowest_irr,
114
+ ]
115
+ possible_steps = rng.integers(1, high=8, size=10000) + rng.random(size=10000) - 1
116
+ below_max = True
117
+ while below_max:
118
+ next_val = irr_values[-1] + rng.choice(possible_steps, replace=False)
119
+ if next_val >= highest_irr:
120
+ below_max = False
121
+ else:
122
+ irr_values.append(next_val)
123
+ return irr_values
124
+
125
+
126
+ def tags_by_regex(tag_list, regex_str):
127
+ regex = re.compile(regex_str, re.IGNORECASE)
128
+ return [tag for tag in tag_list if regex.search(tag) is not None]
129
+
130
+
131
+ def append_tags(sel_tags, tags, regex_str):
132
+ new_list = sel_tags.copy()
133
+ new_list.extend(tags_by_regex(tags, regex_str))
134
+ return new_list