Custom Test Setups
The built-in test_setup presets (e2848_default, bifi_e2848_etotal,
bifi_power_tc_meas_tbom, bifi_power_tc_calc_tbom, e2848_spec_corrected_poa)
cover the most common capacity-test configurations. When a project calls for a
different regression equation or a non-standard column calculation, pvcaptest lets
you supply your own reg_cols_meas and reg_cols_sim dicts and user-defined
parameter calculation functions without modifying the package.
This page explains the structure of those dicts, shows how the built-in
captest.calcparams functions plug into them, and describes the three
ways to wire a custom dict into a CapTest.
The regression column dictionary grammar
Each key in reg_cols_meas or reg_cols_sim maps a regression term (such
as "power" or "poa") to one of three node forms.
The simplest node is a plain string matching a column name in the data attribute.
For example, this is the approach used in the built-in test setups for the
PVsyst output:
"poa": "GlobInc"
A simple aggregation is a two-element tuple of the column-group id and
an aggregation function name. This will be aggregated using the CapData.agg_group
method. This example assumes that you have a key in your CapData.column_groups called
irr_poa that points to a list of columns, which contain measurements from POA
irradiance sensors. See the Column Grouping section of the CapData
workflow documentation page for additional explanation.
"poa": ("irr_poa", "mean")
Note
Aggregation uses pandas.DataFrame.agg which accepts a wide range of aggregation methods.
A calculated column is a two-element tuple of a callable and a dict mapping the callable’s keyword arguments to column names, aggregation tuples, or nested calculated-column tuples:
"poa": (
e_total,
{
"poa": ("irr_poa", "mean"),
"rpoa": ("irr_rpoa", "mean"),
},
)
Nesting is allowed to any depth. During
setup(), pvcaptest recursively walks the tree bottom-up:
each (func, kwargs_dict) tuple creates a new column named
func.__name__ in CapData.data, and that name is passed upward as input
to any parent tuple. For the example above, e_total is a callable (a function from
the calcparams module) and processing this portion of the dictionary adds a column
to the data attribute with the name e_total. Also, the dictionary is updated
so that poa now points to e_total.
Using calcparams functions
The functions in captest.calcparams are the public building blocks
for calculated columns. Import the ones you need:
from captest.calcparams import (
e_total,
bom_temp,
cell_temp,
power_temp_correct,
rpoa_pvsyst,
scale,
)
Using custom functions
If you need to calculate a parameter which does not have a function in the calcparms
module, you can write your own.
The dictionary accepts plain Python functions that are not part of
captest.calcparams, as long as they follow the same signature
convention:
First positional argument must be
data, the source DataFrame.Remaining arguments are column names passed as strings.
The function returns a
pandas.Seriesindexed likedata.The function must be defined with
def(not alambda) so it has a__name__.
def my_adjusted_poa(data, poa=None, adjustment=1.0, verbose=True):
"""Scale a POA column by a site-specific adjustment factor."""
if verbose:
print(f"Calculating my_adjusted_poa as {poa} * {adjustment}")
return data[poa] * adjustment
my_meas_cols = {
"poa": (
my_adjusted_poa,
{"poa": ("irr_poa", "mean"), "adjustment": 1.05},
),
...
}
Adding a verbose kwarg to print an explanation of the calculation is not required, but strongly recommended as it makes the calculation traceable for a reviewing party.
Note
Each function name must be unique within a single reg_cols_meas or
reg_cols_sim dict because the column added to CapData.data is
always named func.__name__. If two nodes call functions with the same
name, the second call overwrites the column produced by the first.
Creating a Custom Regression Columns Dictionary
The example below builds measured and modeled column dicts that compute temperature-corrected power from raw power, back-of-module temperature (estimated from POA, ambient temperature, and wind speed), and cell temperature:
from captest.calcparams import bom_temp, cell_temp, power_temp_correct
my_meas_cols = {
"power": (
power_temp_correct,
{
"power": ("real_pwr_mtr", "sum"),
"cell_temp": (
cell_temp,
{
"poa": ("irr_poa", "mean"),
"bom": (
bom_temp,
{
"poa": ("irr_poa", "mean"),
"temp_amb": ("temp_amb", "mean"),
"wind_speed": ("wind_speed", "mean"),
},
),
},
),
},
),
"poa": ("irr_poa", "mean"),
}
my_sim_cols = {
"power": (
power_temp_correct,
{"power": "E_Grid", "cell_temp": "TArray"},
),
"poa": "GlobInc",
}
Scalar auto-injection
Scalar parameters such as power_temp_coeff, base_temp,
bifaciality, and spectral_module_type do not need to appear in the
dict. When a captest.calcparams function has a keyword argument
whose name matches an attribute on the CapData instance, pvcaptest injects
that value automatically. CapTest propagates
these scalars onto both CapData instances during
setup(), so setting them on the
CapTest instance is sufficient:
ct = CapTest.from_params(
test_setup="custom",
reg_cols_meas=my_meas_cols,
reg_cols_sim=my_sim_cols,
reg_fml="power ~ poa + I(poa * poa) + I(poa * t_amb) + I(poa * w_vel) - 1",
meas=meas,
sim=sim,
ac_nameplate=6_000_000,
power_temp_coeff=-0.36, # injected automatically into power_temp_correct
base_temp=25, # injected automatically into power_temp_correct
)
To override the auto-injected value for a specific node, include the scalar explicitly in that node’s kwarg dict.
This approach is recommended because it ensures values that should be consistent between
the measured data and simulated data, like bifaciality, match.
Wiring a custom dict into CapTest
There are three equivalent ways to supply custom column dicts.
Route 1 — fully custom setup. Pass test_setup='custom' with all three
required overrides. scatter_plots and rep_conditions default to
scatter_default and an empty dict if omitted:
from captest import CapTest
ct = CapTest.from_params(
test_setup="custom",
reg_cols_meas=my_meas_cols,
reg_cols_sim=my_sim_cols,
reg_fml="power ~ poa + I(poa * poa) + I(poa * t_amb) + I(poa * w_vel) - 1",
meas=meas,
sim=sim,
ac_nameplate=6_000_000,
test_tolerance="- 4",
)
Route 2 — override a named preset. If a built-in preset’s formula is
correct but the column mappings need to change, pass reg_cols_meas and / or
reg_cols_sim alongside a named test_setup. The preset’s formula,
scatter plot, and reporting conditions are inherited:
ct = CapTest.from_params(
test_setup="e2848_default",
reg_cols_meas=my_meas_cols,
reg_cols_sim=my_sim_cols,
meas=meas,
sim=sim,
ac_nameplate=6_000_000,
)
Route 3 — assign directly. Attributes can be set on the instance before
calling setup():
ct = CapTest(test_setup="custom", ac_nameplate=6_000_000)
ct.meas = meas
ct.sim = sim
ct.reg_cols_meas = my_meas_cols
ct.reg_cols_sim = my_sim_cols
ct.reg_fml = "power ~ poa + I(poa * poa) + I(poa * t_amb) + I(poa * w_vel) - 1"
ct.setup()