.. _custom_test_setups: Custom Test Setups ================== The built-in ``test_setup`` presets (``e2848_default``, ``bifi_e2848_etotal``, ``bifi_power_tc``, ``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 without modifying the package. This page explains the structure of those dicts, shows how the built-in :py:mod:`captest.calcparams` functions plug into them, and describes the three ways to wire a custom dict into a :py:class:`~captest.captest.CapTest`. The regression column dict 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. A **simple measured column** is a two-element tuple of the column-group id and an aggregation function name: .. code-block:: Python "poa": ("irr_poa", "mean") A **simple modeled column** is a plain string matching a column name in the PVsyst output: .. code-block:: Python "poa": "GlobInc" 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: .. code-block:: Python "poa": ( e_total, { "poa": ("irr_poa", "mean"), "rpoa": ("irr_rpoa", "mean"), }, ) Nesting is allowed to any depth. During :py:meth:`~captest.captest.CapTest.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. Using calcparams functions -------------------------- The functions in :py:mod:`captest.calcparams` are the public building blocks for calculated columns. Import the ones you need: .. code-block:: Python from captest.calcparams import ( e_total, bom_temp, cell_temp, power_temp_correct, rpoa_pvsyst, scale, ) 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: .. code-block:: Python 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"), "t_amb": ("temp_amb", "mean"), "w_vel": ("wind_speed", "mean"), } my_sim_cols = { "power": ( power_temp_correct, {"power": "E_Grid", "cell_temp": "TArray"}, ), "poa": "GlobInc", "t_amb": "T_Amb", "w_vel": "WindVel", } 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 :py:mod:`captest.calcparams` function has a keyword argument whose name matches an attribute on the ``CapData`` instance, pvcaptest injects that value automatically. :py:class:`~captest.captest.CapTest` propagates these scalars onto both ``CapData`` instances during :py:meth:`~captest.captest.CapTest.setup`, so setting them on the :py:class:`~captest.captest.CapTest` instance is sufficient: .. code-block:: Python 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. 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: .. code-block:: Python 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: .. code-block:: Python 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 :py:meth:`~captest.captest.CapTest.setup`: .. code-block:: Python 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() Using custom functions ---------------------- The dict also accepts plain Python functions that are not part of :py:mod:`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 :class:`pandas.Series` indexed like ``data``. - The function must be defined with ``def`` (not a ``lambda``) so it has a ``__name__``. .. code-block:: Python 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}, ), ... } .. 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.