diff --git a/docs/img/documentation_images/photosynthesis_read_cropreporter/6_APH-frames.png b/docs/img/documentation_images/photosynthesis_read_cropreporter/6_APH-frames.png new file mode 100644 index 000000000..8ed10f609 Binary files /dev/null and b/docs/img/documentation_images/photosynthesis_read_cropreporter/6_APH-frames.png differ diff --git a/docs/img/documentation_images/photosynthesis_read_cropreporter/6_aph_frames.png b/docs/img/documentation_images/photosynthesis_read_cropreporter/6_aph_frames.png deleted file mode 100644 index 81e20228a..000000000 Binary files a/docs/img/documentation_images/photosynthesis_read_cropreporter/6_aph_frames.png and /dev/null differ diff --git a/docs/photosynthesis_read_cropreporter.md b/docs/photosynthesis_read_cropreporter.md index bf00d2521..af9789bda 100644 --- a/docs/photosynthesis_read_cropreporter.md +++ b/docs/photosynthesis_read_cropreporter.md @@ -22,7 +22,7 @@ PSII_data instance containing [xarray DataArrays](http://xarray.pydata.org/en/st - Measurements from chlorophyll fluorescence are stored in the attribute `chlorophyll` and include a chlorophyll fluorescence frame (Chl) stored as NumPy array/ndarray. The Fdark frame, if collected, is not stored. - Green fluorescence protein (GFP) measurements are stored in the attribute `gfp` and include frames for dark fluorescence (Fdark), GFP fluorescence (GFP, 525 nm), and autofluorescence (Auto, 585 nm). - Red fluorescence protein (RFP) measurements are stored in the attribute `rfp` and include frames for dark fluorescence (Fdark) and RFP fluorescence (585 nm). - - Alpha light absorption coefficient (APH) measurements are stored in the attribute `aph` and include reflected light frames for red (640 nm) and far-red (732 nm) wavelengths. + - Alpha light absorption coefficient (APH) measurements are stored in the attribute `aph` and include reflected light frames for red (640 nm) and far-red (732 nm) wavelengths. `ps.aph` is a NumPy array/ndarray with shape (height, width, 2), where index 0 is the red frame and index 1 is the far-red frame. The Fdark frame, if collected, is not stored. - Spectral measurements are stored as a PlantCV [Spectral_data](Spectral_data.md) object in the attribute `spectral`. Frames are stored by reflectance wavelength and can include: blue (475nm), green (550nm), red (640nm), green2 (540nm), far-red (710nm), and near-infrared (770nm). @@ -81,6 +81,6 @@ ps.ojip_dark.plot(col='frame_label', col_wrap=4) **Alpha light absorption coefficient (APH) measurements** -![Screenshot](img/documentation_images/photosynthesis_read_cropreporter/6_aph_frames.png) +![Screenshot](img/documentation_images/photosynthesis_read_cropreporter/6_APH-frames.png) **Source Code:** [Here](https://github.com/danforthcenter/plantcv/blob/main/plantcv/plantcv/photosynthesis/read_cropreporter.py) diff --git a/plantcv/plantcv/photosynthesis/read_cropreporter.py b/plantcv/plantcv/photosynthesis/read_cropreporter.py index 49785d008..392fb0fd8 100644 --- a/plantcv/plantcv/photosynthesis/read_cropreporter.py +++ b/plantcv/plantcv/photosynthesis/read_cropreporter.py @@ -579,8 +579,7 @@ def _process_rfp_data(ps, metadata): def _process_aph_data(ps, metadata): - """ - Create an xarray DataArray for an APH dataset. + """Read APH dataset and keep the Red and FarRed frames as a NumPy array. Parameters ---------- @@ -588,33 +587,39 @@ def _process_aph_data(ps, metadata): PSII_data instance. metadata : dict INF file metadata dictionary. - """ bin_filepath = _dat_filepath(dataset="APH", datapath=ps.datapath, filename=ps.filename) + if os.path.exists(bin_filepath): - img_cube, frame_labels, frame_nums = _read_dat_file(dataset="APH", filename=bin_filepath, - height=int(metadata["ImageRows"]), - width=int(metadata["ImageCols"])) - frame_labels = ["Red", "FarRed"] - aph = xr.DataArray( - data=img_cube, - dims=('x', 'y', 'frame_label'), - coords={'frame_label': frame_labels, - 'frame_num': ('frame_label', frame_nums)}, - name='aph' - ) - aph.attrs["long_name"] = "Alpha Light absorption coefficient (Reflection) (640nm Red, 732nm FarRed)" - aph.attrs["dark_comp_on"] = int(metadata.get("AphDarkCompOn", metadata.get("AlphaDarkCompOn", "0"))) - aph.attrs["gain_red"] = float(metadata.get("AphGainRed", metadata.get("AlphaGainRed", "nan"))) - aph.attrs["gain_farred"] = float(metadata.get("AphGainFarRed", metadata.get("AlphaGainFarRed", "nan"))) - ps.add_data(aph) + # Read the raw data cube (contains Fdark, Red, and FarRed or just Red and FarRed) + img_cube, _, _ = _read_dat_file(dataset="APH", filename=bin_filepath, + height=int(metadata["ImageRows"]), + width=int(metadata["ImageCols"])) - _debug( - visual=ps.aph, - filename=os.path.join(params.debug_outdir, f"{str(params.device)}_APH-frames.png"), - col='frame_label', - col_wrap=int(np.ceil(ps.aph.frame_label.size / 4)) + # The APH file typically has: index 0 = Fdark, index 1 = Red, index 2 = FarRed. + # Some acquisitions may only contain two frames (e.g. no dark frame). + # Select the Red and FarRed frames based on the number of frames present. + # Use the last 2 frames as Red and FarRed: + # - When there are 3 frames, indices are [0]=Fdark, [1]=Red, [2]=FarRed -> use indices 1 and 2. + # - When there are 2 frames, indices are [0]=Red, [1]=FarRed -> use indices 0 and 1. + num_frames = img_cube.shape[2] + if num_frames < 2: + raise RuntimeError(f"APH DAT file contains {num_frames} frame(s); expected at least 2 (Red and FarRed).") + aph_frames = img_cube[:, :, num_frames - 2:num_frames] + + # Store as a standard attribute + ps.aph = aph_frames + + # Debugging — wrap in a temporary DataArray so frame labels appear in the plot + aph_debug = xr.DataArray( + data=ps.aph, + dims=('x', 'y', 'frame_label'), + coords={'frame_label': ['Red', 'FarRed']} ) + _debug(visual=aph_debug, + filename=os.path.join(params.debug_outdir, f"{str(params.device)}_APH-frames.png"), + col='frame_label', + col_wrap=2) def _dat_filepath(dataset, datapath, filename): diff --git a/tests/plantcv/photosynthesis/test_read_cropreporter.py b/tests/plantcv/photosynthesis/test_read_cropreporter.py index b0c283ad9..6b03a4ea5 100644 --- a/tests/plantcv/photosynthesis/test_read_cropreporter.py +++ b/tests/plantcv/photosynthesis/test_read_cropreporter.py @@ -139,6 +139,28 @@ def test_read_cropreporter_aph_only(photosynthesis_test_data, tmpdir): assert ps.aph.shape[2] == 2 # Red + FarRed +def test_read_cropreporter_aph_insufficient_frames(photosynthesis_test_data, tmpdir, monkeypatch): + """Test that APH import raises RuntimeError when DAT file contains fewer than 2 frames.""" + cache_dir = tmpdir.mkdir("sub_aph_err") + inf_dest = os.path.join(cache_dir, "HDR_2025-12-12_tob1_20251212205712029.INF") + dat_dest = os.path.join(cache_dir, "APH_2025-12-12_tob1_20251212205712029.DAT") + shutil.copyfile(photosynthesis_test_data.cropreporter_aph, inf_dest) + aph_dat = photosynthesis_test_data.cropreporter_aph.replace("HDR", "APH").replace("INF", "DAT") + shutil.copyfile(aph_dat, dat_dest) + # Override image dimensions so monkeypatched data is the right size + with open(inf_dest, "a") as f: + f.write("\nImageRows=10") + f.write("\nImageCols=10") + # Return only 1 frame worth of data (10 * 10 * 1 = 100) to trigger the error + monkeypatch.setattr(np, "fromfile", lambda *args, **kwargs: np.ones(100, dtype=np.uint16)) + error_raised = False + try: + read_cropreporter(filename=inf_dest) + except RuntimeError: + error_raised = True + assert error_raised + + def test_read_cropreporter_pmt_only_9_labels(photosynthesis_test_data, tmpdir): """Test PMT (PAM Time) import with 9 frames.""" # Create a test tmp directory