From 551861b44a39e78ccd8cab1358e9ffbf525a915c Mon Sep 17 00:00:00 2001 From: Josh Sumner <51797700+joshqsumner@users.noreply.github.com> Date: Wed, 12 Aug 2026 09:37:24 -0500 Subject: [PATCH 1/2] analyze etr --- docs/analyze_etr.md | 30 ++++++++++++ docs/updating.md | 5 ++ mkdocs.yml | 1 + plantcv/plantcv/analyze/__init__.py | 3 +- plantcv/plantcv/analyze/alphaL.py | 22 +++++---- plantcv/plantcv/analyze/etr.py | 50 ++++++++++++++++++++ tests/plantcv/analyze/test_etr.py | 72 +++++++++++++++++++++++++++++ 7 files changed, 172 insertions(+), 11 deletions(-) create mode 100644 docs/analyze_etr.md create mode 100644 plantcv/plantcv/analyze/etr.py create mode 100644 tests/plantcv/analyze/test_etr.py diff --git a/docs/analyze_etr.md b/docs/analyze_etr.md new file mode 100644 index 0000000000..3afaf060a3 --- /dev/null +++ b/docs/analyze_etr.md @@ -0,0 +1,30 @@ +## Analyze Electron Transport Rate + +Estimate mean and median electron transport rate (ETR). +Calculates `(Fq’/Fm’) * AlphaL * PSI/PSIIratio * Actinic_light`. This requires [calculating alphaL](analyze_alphaL.md) and [calculating Fq'/Fm'](analyze_yii.md) before ETR can be calculated. + +**plantcv.analyze.etr**(*actinic_light, psi_psii_ratio=0.5*) + +**returns** None + +- **Parameters:** + - actinic_light - Light intensity in PAR + - psi_psii_ratio - Light absorption ratio between photosynthesis 1 and 2. Defaults to 0.5. + +- **Context:** + - Used to calculate ETR after YII and AlphaL are calculated. This requires APH frames and the frames input to [`analyze.yii`](analyze_yii.md). + +- **Example use:** + - Below + +- **Output data stored:** Data (mean_etr, median_etr) are stored to the [`Outputs` class](outputs.md) when this function is run. + +```python +from plantcv import plantcv as pcv + +# calculate ETR +pcv.analyze.etr(actinic_light=10) +# check results +pcv.outputs.observations["plant_1"]["mean_etr"]["values"] + +``` diff --git a/docs/updating.md b/docs/updating.md index 59e64c509f..e852beec59 100644 --- a/docs/updating.md +++ b/docs/updating.md @@ -376,6 +376,11 @@ pages for more details on the input and output variable types. * pre v4.2.1: NA * post v4.2.1: dist_chart = **plantcv.analyze.distribution**(*labeled_mask, n_labels=1, direction="down", bin_size=100, hist_range="absolute", label=None*) +#### plantcv.analyze.etr + +* pre v5.0: NA +* post v5.0: _ = **plantcv.analyze.etr**(*actinic_light, psi_psii_ratio=0.5*) + #### plantcv.analyze.grayscale * pre v4.0: (see plantcv.analyze_nir_intensity) diff --git a/mkdocs.yml b/mkdocs.yml index 37c85a49f8..7d6da13ed4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -42,6 +42,7 @@ nav: - 'Analyze Spectral Index': analyze_spectral_index.md - 'Analyze YII': analyze_yii.md - 'Analyze NPQ': analyze_npq.md + - 'Analyze ETR': analyze_etr.md - 'Annotation Tools': - 'Points': Points.md - 'Apply Mask': apply_mask.md diff --git a/plantcv/plantcv/analyze/__init__.py b/plantcv/plantcv/analyze/__init__.py index 7146b14223..941716b5be 100644 --- a/plantcv/plantcv/analyze/__init__.py +++ b/plantcv/plantcv/analyze/__init__.py @@ -9,8 +9,9 @@ from plantcv.plantcv.analyze.yii import yii from plantcv.plantcv.analyze.npq import npq from plantcv.plantcv.analyze.alphaL import alphaL +from plantcv.plantcv.analyze.etr import etr from plantcv.plantcv.analyze.distribution import distribution from plantcv.plantcv.analyze.texture import texture __all__ = ["color", "bound_horizontal", "bound_vertical", "grayscale", "size", "thermal", "spectral_reflectance", - "spectral_index", "yii", "npq", "alphaL", "distribution", "texture"] + "spectral_index", "yii", "npq", "alphaL", "etr", "distribution", "texture"] diff --git a/plantcv/plantcv/analyze/alphaL.py b/plantcv/plantcv/analyze/alphaL.py index d1e57aa699..16dc69946c 100644 --- a/plantcv/plantcv/analyze/alphaL.py +++ b/plantcv/plantcv/analyze/alphaL.py @@ -47,9 +47,11 @@ def alphaL(ps, labeled_mask, n_labels=1, label=None, min_bin=-1, max_bin=1): if getattr(ps, "aph", None) is None: fatal_error("`ps` must be a PSII_Data object with APH data present.") # calculate alphaL for each masked object - aph = _iterate_analysis(img=ps, + aph = _iterate_analysis(img=np.zeros(ps.aph.red.shape[0:2]), labeled_mask=labeled_mask, n_labels=n_labels, label=labels, - function=_analyze_alphaL, **{"min_bin": min_bin, "max_bin": max_bin}) + function=_analyze_alphaL, + **{"min_bin": min_bin, "max_bin": max_bin, + "red": ps.aph.red, "farred": ps.aph.farred}) return aph @@ -73,13 +75,12 @@ def _alphaL_calc(red, farred, mask): return aph -def _analyze_alphaL(img, mask, label, min_bin, max_bin): +def _analyze_alphaL(img, mask, label, min_bin, max_bin, red, farred): """Analyze Alpha L in _iterate_analysis Parameters ---------- - ps = plantcv.plantcv.classes.PSII_data - Photosynthesis data as read by plantcv.plantcv.photosynthesis.read_cropreporter - Must include the aph frame. + img = numpy.ndarray + Empty mask to write alphaL on in _iterate_analysis mask = numpy.ndarray, Binary mask of objects. label = str, @@ -88,15 +89,16 @@ def _analyze_alphaL(img, mask, label, min_bin, max_bin): Minimum bin value (default = -1). max_bin = int, Maximum bin value (default = 1). + red = numpy.ndarray, + ps.aph.red frame + farred = numpy.ndarray + ps.aph.farred frame Returns ------- alphaL = numpy.ndarray, alphaL matrix """ - ps = img - red = ps.aph.red - farred = ps.aph.farred # Calculate alphaL alphaL_mat = _alphaL_calc(red, farred, mask) # Store mean, median, min, max, and histogram of alphaL @@ -150,4 +152,4 @@ def _analyze_alphaL(img, mask, label, min_bin, max_bin): value=hist_df['proportion of pixels (%)'].values.tolist(), label=np.around(hist_df["counts"].values.tolist(), decimals=2).tolist()) - return alphaL_mat + return img + alphaL_mat diff --git a/plantcv/plantcv/analyze/etr.py b/plantcv/plantcv/analyze/etr.py new file mode 100644 index 0000000000..303c9421dc --- /dev/null +++ b/plantcv/plantcv/analyze/etr.py @@ -0,0 +1,50 @@ +"""Analyze electron transport rate""" + +import re +from plantcv.plantcv._globals import outputs +from plantcv.plantcv.fatal_error import fatal_error + + +def etr(actinic_light, psi_psii_ratio=0.5): + """Calculate electron transport rate from yii and alphaL outputs and add to outputs. + + Parameters + ---------- + actinic_light = int or float, + Light intensity in PAR + psi_psii_ratio = float, + PSI/PSII ratio. This should generally be left as 0.5 + + Returns + ------- + None, etr values are added to outputs + """ + obs = outputs.observations + for label, _ in obs.items(): + labs = [label2 for label2, _ in obs[label].items() if re.search("yii_mean.*[fqfm|t1]$", label2)] + labs2 = [label2 for label2, _ in obs[label].items() if re.search("yii_median.*[fqfm|t1]$", label2)] + aph_labs = [label3 for label3, _ in obs[label].items() if re.search("alphaL", label3)] + if len(labs) < 1: + fatal_error("YII mean data must be present in outputs," + + "run plantcv.plantcv.analyze.yii before plantcv.plantcv.analyze.etr.") + yii_mean_val = obs[label][labs[0]]["value"] + yii_median_val = obs[label][labs2[0]]["value"] + if not bool(aph_labs): + fatal_error("AlphaL mean data must be present in outputs," + + "run plantcv.plantcv.analyze.alphaL before plantcv.plantcv.analyze.etr.") + alphaL_mean_val = obs[label]["alphaL_mean"]["value"] + alphaL_median_val = obs[label]["alphaL_median"]["value"] + # calculate ETR + etr_mean_val = yii_mean_val * alphaL_mean_val * psi_psii_ratio * actinic_light + etr_median_val = yii_median_val * alphaL_median_val * psi_psii_ratio * actinic_light + # store outputs + outputs.add_observation(sample=label, + variable = 'mean_etr', trait='mean electron transport rate', + method='plantcv.plantcv.analyze.etr', + scale='none', datatype=float, + value=etr_mean_val, label='none') + outputs.add_observation(sample=label, + variable = 'median_etr', trait='median electron transport rate', + method='plantcv.plantcv.analyze.etr', + scale='none', datatype=float, + value=etr_median_val, label='none') diff --git a/tests/plantcv/analyze/test_etr.py b/tests/plantcv/analyze/test_etr.py new file mode 100644 index 0000000000..07a772ee17 --- /dev/null +++ b/tests/plantcv/analyze/test_etr.py @@ -0,0 +1,72 @@ +import pytest +from plantcv.plantcv._globals import outputs +from plantcv.plantcv.analyze.etr import etr + + +def test_etr(): + """Test for PlantCV.""" + outputs.clear() + outputs.add_observation(sample="label1", + variable = 'yii_mean_t1', + trait='dummy yii', + method='dummy.method', + scale='none', datatype=float, + value=10, label='none') + outputs.add_observation(sample="label1", + variable = 'alphaL_mean', + trait='dummy alpha', + method='dummy.alpha.method', + scale='none', datatype=float, + value=5, label='none') + outputs.add_observation(sample="label1", + variable = 'yii_median_t1', + trait='dummy yii', + method='dummy.method', + scale='none', datatype=float, + value=10, label='none') + outputs.add_observation(sample="label1", + variable = 'alphaL_median', + trait='dummy alpha', + method='dummy.alpha.method', + scale='none', datatype=float, + value=5, label='none') + etr(10) + assert outputs.observations["label1"]["mean_etr"]["value"] == 250 + + +def test_etr_no_yii(): + """Test for PlantCV.""" + outputs.clear() + outputs.add_observation(sample="label1", + variable = 'alphaL_mean', + trait='dummy alpha', + method='dummy.alpha.method', + scale='none', datatype=float, + value=5, label='none') + outputs.add_observation(sample="label1", + variable = 'alphaL_median', + trait='dummy alpha', + method='dummy.alpha.method', + scale='none', datatype=float, + value=5, label='none') + with pytest.raises(RuntimeError): + etr(10) + + +def test_etr_no_alphaL(): + """Test for PlantCV.""" + outputs.clear() + outputs.add_observation(sample="label1", + variable = 'yii_mean_t1', + trait='dummy yii', + method='dummy.method', + scale='none', datatype=float, + value=10, label='none') + outputs.add_observation(sample="label1", + variable = 'yii_median_t1', + trait='dummy yii', + method='dummy.method', + scale='none', datatype=float, + value=10, label='none') + with pytest.raises(RuntimeError): + etr(10) From 8bd9f6c62e7db1662ef619480dc43789336414e2 Mon Sep 17 00:00:00 2001 From: Josh Sumner <51797700+joshqsumner@users.noreply.github.com> Date: Wed, 12 Aug 2026 09:45:59 -0500 Subject: [PATCH 2/2] whitespace --- plantcv/plantcv/analyze/etr.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plantcv/plantcv/analyze/etr.py b/plantcv/plantcv/analyze/etr.py index 303c9421dc..ddc63f84b6 100644 --- a/plantcv/plantcv/analyze/etr.py +++ b/plantcv/plantcv/analyze/etr.py @@ -39,12 +39,12 @@ def etr(actinic_light, psi_psii_ratio=0.5): etr_median_val = yii_median_val * alphaL_median_val * psi_psii_ratio * actinic_light # store outputs outputs.add_observation(sample=label, - variable = 'mean_etr', trait='mean electron transport rate', + variable='mean_etr', trait='mean electron transport rate', method='plantcv.plantcv.analyze.etr', scale='none', datatype=float, value=etr_mean_val, label='none') outputs.add_observation(sample=label, - variable = 'median_etr', trait='median electron transport rate', + variable='median_etr', trait='median electron transport rate', method='plantcv.plantcv.analyze.etr', scale='none', datatype=float, value=etr_median_val, label='none')