@@ -732,9 +732,10 @@ def ashraeiam(aoi, b=0.05):
732732 physicaliam
733733 '''
734734
735- iam = 1 - b * ((1 / np .cos (np .radians (aoi )) - 1 ))
736-
737- iam = np .where (np .abs (aoi ) >= 90 , 0 , iam )
735+ iam = 1 - b * ((1 / np .cos (np .radians (aoi )) - 1 ))
736+ aoi_gte_90 = np .full_like (aoi , False , dtype = 'bool' )
737+ np .greater_equal (np .abs (aoi ), 90 , where = ~ np .isnan (aoi ), out = aoi_gte_90 )
738+ iam = np .where (aoi_gte_90 , 0 , iam )
738739 iam = np .maximum (0 , iam )
739740
740741 if isinstance (iam , pd .Series ):
@@ -836,16 +837,18 @@ def physicaliam(aoi, n=1.526, K=4., L=0.002):
836837 # after deducting the reflected portion of each
837838 iam = ((1 - (rho_para + rho_perp ) / 2 ) / (1 - rho_zero ) * tau / tau_zero )
838839
839- # angles near zero produce nan, but iam is defined as one
840- small_angle = 1e-06
841- iam = np .where (np .abs (aoi ) < small_angle , 1.0 , iam )
840+ with np .errstate (invalid = 'ignore' ):
841+ # angles near zero produce nan, but iam is defined as one
842+ small_angle = 1e-06
843+ iam = np .where (np .abs (aoi ) < small_angle , 1.0 , iam )
842844
843- # angles at 90 degrees can produce tiny negative values, which should be zero
844- # this is a result of calculation precision rather than the physical model
845- iam = np .where (iam < 0 , 0 , iam )
845+ # angles at 90 degrees can produce tiny negative values,
846+ # which should be zero. this is a result of calculation precision
847+ # rather than the physical model
848+ iam = np .where (iam < 0 , 0 , iam )
846849
847- # for light coming from behind the plane, none can enter the module
848- iam = np .where (aoi > 90 , 0 , iam )
850+ # for light coming from behind the plane, none can enter the module
851+ iam = np .where (aoi > 90 , 0 , iam )
849852
850853 if isinstance (aoi_input , pd .Series ):
851854 iam = pd .Series (iam , index = aoi_input .index )
@@ -1296,12 +1299,27 @@ def sapm(effective_irradiance, temp_cell, module):
12961299 q = 1.60218e-19 # Elementary charge in units of coulombs
12971300 kb = 1.38066e-23 # Boltzmann's constant in units of J/K
12981301
1299- Ee = effective_irradiance
1302+ # avoid problem with integer input
1303+ Ee = np .array (effective_irradiance , dtype = 'float64' )
1304+
1305+ # set up masking for 0, positive, and nan inputs
1306+ Ee_gt_0 = np .full_like (Ee , False , dtype = 'bool' )
1307+ Ee_eq_0 = np .full_like (Ee , False , dtype = 'bool' )
1308+ notnan = ~ np .isnan (Ee )
1309+ np .greater (Ee , 0 , where = notnan , out = Ee_gt_0 )
1310+ np .equal (Ee , 0 , where = notnan , out = Ee_eq_0 )
13001311
13011312 Bvmpo = module ['Bvmpo' ] + module ['Mbvmp' ]* (1 - Ee )
13021313 Bvoco = module ['Bvoco' ] + module ['Mbvoc' ]* (1 - Ee )
13031314 delta = module ['N' ] * kb * (temp_cell + 273.15 ) / q
13041315
1316+ # avoid repeated computation
1317+ logEe = np .full_like (Ee , np .nan )
1318+ np .log (Ee , where = Ee_gt_0 , out = logEe )
1319+ logEe = np .where (Ee_eq_0 , - np .inf , logEe )
1320+ # avoid repeated __getitem__
1321+ cells_in_series = module ['Cells_in_Series' ]
1322+
13051323 out = OrderedDict ()
13061324
13071325 out ['i_sc' ] = (
@@ -1312,13 +1330,13 @@ def sapm(effective_irradiance, temp_cell, module):
13121330 (1 + module ['Aimp' ]* (temp_cell - T0 )))
13131331
13141332 out ['v_oc' ] = np .maximum (0 , (
1315- module ['Voco' ] + module [ 'Cells_in_Series' ] * delta * np . log ( Ee ) +
1333+ module ['Voco' ] + cells_in_series * delta * logEe +
13161334 Bvoco * (temp_cell - T0 )))
13171335
13181336 out ['v_mp' ] = np .maximum (0 , (
13191337 module ['Vmpo' ] +
1320- module ['C2' ]* module [ 'Cells_in_Series' ] * delta * np . log ( Ee ) +
1321- module ['C3' ]* module [ 'Cells_in_Series' ] * ((delta * np . log ( Ee ) ) ** 2 ) +
1338+ module ['C2' ] * cells_in_series * delta * logEe +
1339+ module ['C3' ] * cells_in_series * ((delta * logEe ) ** 2 ) +
13221340 Bvmpo * (temp_cell - T0 )))
13231341
13241342 out ['p_mp' ] = out ['i_mp' ] * out ['v_mp' ]
@@ -1518,7 +1536,10 @@ def sapm_aoi_loss(aoi, module, upper=None):
15181536
15191537 aoi_loss = np .polyval (aoi_coeff , aoi )
15201538 aoi_loss = np .clip (aoi_loss , 0 , upper )
1521- aoi_loss = np .where (aoi < 0 , 0 , aoi_loss )
1539+ # nan tolerant masking
1540+ aoi_lt_0 = np .full_like (aoi , False , dtype = 'bool' )
1541+ np .less (aoi , 0 , where = ~ np .isnan (aoi ), out = aoi_lt_0 )
1542+ aoi_loss = np .where (aoi_lt_0 , 0 , aoi_loss )
15221543
15231544 if isinstance (aoi , pd .Series ):
15241545 aoi_loss = pd .Series (aoi_loss , aoi .index )
@@ -1912,9 +1933,11 @@ def v_from_i(resistance_shunt, resistance_series, nNsVth, current,
19121933 # Only compute using LambertW if there are cases with Gsh>0
19131934 if np .any (idx_p ):
19141935 # LambertW argument, cannot be float128, may overflow to np.inf
1915- argW = I0 [idx_p ] / (Gsh [idx_p ]* a [idx_p ]) * \
1916- np .exp ((- I [idx_p ] + IL [idx_p ] + I0 [idx_p ]) /
1917- (Gsh [idx_p ]* a [idx_p ]))
1936+ # overflow is explicitly handled below, so ignore warnings here
1937+ with np .errstate (over = 'ignore' ):
1938+ argW = (I0 [idx_p ] / (Gsh [idx_p ]* a [idx_p ]) *
1939+ np .exp ((- I [idx_p ] + IL [idx_p ] + I0 [idx_p ]) /
1940+ (Gsh [idx_p ]* a [idx_p ])))
19181941
19191942 # lambertw typically returns complex value with zero imaginary part
19201943 # may overflow to np.inf
@@ -2274,22 +2297,39 @@ def adrinverter(v_dc, p_dc, inverter, vtol=0.10):
22742297 mppt_hi = inverter ['MPPTHi' ]
22752298 mppt_low = inverter ['MPPTLow' ]
22762299
2277- v_lim_upper = np .nanmax ([v_max , vdc_max , mppt_hi ])* (1 + vtol )
2278- v_lim_lower = np .nanmax ([v_min , mppt_low ])* (1 - vtol )
2279-
2280- pdc = p_dc / p_nom
2281- vdc = v_dc / v_nom
2282- poly = np .array ([pdc ** 0 , pdc , pdc ** 2 , vdc - 1 , pdc * (vdc - 1 ),
2283- pdc ** 2 * (vdc - 1 ), 1 / vdc - 1 , pdc * (1. / vdc - 1 ),
2284- pdc ** 2 * (1. / vdc - 1 )])
2300+ v_lim_upper = np .nanmax ([v_max , vdc_max , mppt_hi ]) * (1 + vtol )
2301+ v_lim_lower = np .nanmax ([v_min , mppt_low ]) * (1 - vtol )
2302+
2303+ pdc = p_dc / p_nom
2304+ vdc = v_dc / v_nom
2305+ # zero voltage will lead to division by zero, but since power is
2306+ # set to night time value later, these errors can be safely ignored
2307+ with np .errstate (invalid = 'ignore' , divide = 'ignore' ):
2308+ poly = np .array ([pdc ** 0 , # replace with np.ones_like?
2309+ pdc ,
2310+ pdc ** 2 ,
2311+ vdc - 1 ,
2312+ pdc * (vdc - 1 ),
2313+ pdc ** 2 * (vdc - 1 ),
2314+ 1. / vdc - 1 , # divide by 0
2315+ pdc * (1. / vdc - 1 ), # invalid 0./0. --> nan
2316+ pdc ** 2 * (1. / vdc - 1 )]) # divide by 0
22852317 p_loss = np .dot (np .array (ce_list ), poly )
22862318 ac_power = p_nom * (pdc - p_loss )
2287- p_nt = - 1 * np .absolute (p_nt )
2319+ p_nt = - 1 * np .absolute (p_nt )
2320+
2321+ # set output to nan where input is outside of limits
2322+ # errstate silences case where input is nan
2323+ with np .errstate (invalid = 'ignore' ):
2324+ invalid = (v_lim_upper < v_dc ) | (v_dc < v_lim_lower )
2325+ ac_power = np .where (invalid , np .nan , ac_power )
2326+
2327+ # set night values
2328+ ac_power = np .where (vdc == 0 , p_nt , ac_power )
2329+ ac_power = np .maximum (ac_power , p_nt )
22882330
2289- ac_power = np .where ((v_lim_upper < v_dc ) | (v_dc < v_lim_lower ),
2290- np .nan , ac_power )
2291- ac_power = np .where ((ac_power < p_nt ) | (vdc == 0 ), p_nt , ac_power )
2292- ac_power = np .where (ac_power > pac_max , pac_max , ac_power )
2331+ # set max ac output
2332+ ac_power = np .minimum (ac_power , pac_max )
22932333
22942334 if isinstance (p_dc , pd .Series ):
22952335 ac_power = pd .Series (ac_power , index = pdc .index )
0 commit comments