Skip to content

14 - Photosynthesis with SIF Simulation

This chapter combines dynamic leaf temperature, Farquhar photosynthesis, and sun-induced chlorophyll fluorescence (SIF). A scene may contain mesh leaves, TurbidBoundary vegetation, or both. The returned products provide results for the corresponding mesh primitives, statistical vegetation components, and temperature-enabled terrain surfaces.

Required properties

A leaf component included in dynamic photosynthesis and SIF calculations needs:

Attribute domain Common attributes Purpose
optical Fluspect Leaf reflection, transmission, absorption and fluorescence spectra
thermal ThermalProperty Initial temperature and long-wave emissivity
biophysical BiophysicalProperty Leaf width, stomatal surface, heat and water vapor exchange
physiological Farquhar An, gs and dynamic Phi_f
plant.set_property(
    "leaves",
    less.Fluspect(
        N=1.5, cab=45, car=10, cw=0.012, cm=0.006,
        fqe=0.012,
    ),
)
plant.set_property(
    "leaves",
    less.ThermalProperty(
        temperature="air",
        emissivity=0.98,
    ),
)
plant.set_property(
    "leaves",
    less.BiophysicalProperty(
        leaf_width=0.05,
        stomata_side="bottom",
    ),
)
plant.set_property(
    "leaves",
    less.Farquhar(Vcmax25=60.0),
)

Fluspect.fqe controls static SIF. Dynamic SIF uses Phi_f calculated from the photosynthetic state.

Choosing the FvCB formulation

less.Farquhar() now uses formulation="scope" and absorption_component="chlorophyll" by default, so both arguments may be omitted. The default follows the SCOPE C3 equations.

The historical LESS3 equations remain available explicitly:

plant.set_property(
    "leaves",
    less.Farquhar(Vcmax25=60.0, formulation="jmax"),
)

For formulation="jmax" only, an omitted Jmax25 resolves to 2 * Vcmax25. The Scope formulation does not use Jmax and therefore rejects Jmax25. Remove Jmax25 to use Scope, or select formulation="jmax" when a Jmax capacity is intended. Selecting Jmax does not change the absorbed-light component: chlorophyll absorption remains the default in both formulations.

Choosing the absorbed-light component

Farquhar.absorption_component selects which absorber drives photosynthesis. It defaults to "chlorophyll". Prospect and Fluspect provide their wavelength-dependent biochemical absorption partitions automatically, so the usual leaf models need no additional partition data. Select absorption_component="total" explicitly when total leaf absorption should drive the Farquhar calculation:

plant.set_property(
    "leaves",
    less.Farquhar(Vcmax25=60.0, absorption_component="total"),
)

A MeasuredSpectrum does not infer pigment absorption from a chlorophyll content value alone. Supply either fractions measured on the same wavelength grid or a named partition model:

measured = less.MeasuredSpectrum(
    wavelengths=[400.0, 550.0, 700.0],
    reflectance=[0.08, 0.12, 0.18],
    transmittance=[0.04, 0.08, 0.12],
    absorption_partition=less.ProspectPartition(cab=45.0),
)

For pigment-sensitive APAR comparisons, use approximately 1 nm spectral sampling across PAR. Coarser bands can bias the wavelength-dependent chlorophyll fraction. This recommendation concerns spectral absorption; it does not correct differences caused by the chosen physiology formulation.

Environment units and result metadata

Microclimate.pressure is in Pa, Microclimate.oxygen is in mmol mol-1, and Microclimate.ca is CO2 in umol mol-1. LESS derives actual vapor pressure in Pa from air temperature and humidity. During a Scope leaf-temperature solve, that actual vapor pressure stays fixed while leaf relative humidity is recomputed at the solved leaf temperature.

Photosynthesis products keep formulation, absorption_component, pressure, oxygen, actual vapor pressure, CO2, and the scientific environment fingerprint aligned with each output row. APAR remains the selected-component value and APAR_total remains total leaf-absorbed PAR.

When reading legacy serialized Farquhar records that do not contain the two selectors, LESS migrates them to formulation="jmax" plus absorption_component="total", preserving their historical meaning. Newly serialized records store both fields explicitly.

microclimate = less.Microclimate(
    air_temperature=25.0,  # degrees Celsius
    humidity=60.0,         # relative humidity, percent
    wind_speed=2.0,
    ca=400.0,
)

state = scene.solve(
    microclimate=microclimate,
    modules=["energy_balance", "photosynthesis", "sif"],
)

photo = state.photosynthesis
print("An:", photo.An)
print("gs:", photo.gs)
print("Phi_f:", photo.Phi_f)
print("leaf temperature:", photo.T_leaf)

thermal = scene.simulate(
    less.ThermalImager(
        less.Orthographic(image_size=512),
        bands=[10600.0],
        quality=128,
    )
)

sif = scene.simulate(
    less.SIFImager(
        less.Orthographic(image_size=512),
        bands=[687.0, 740.0, 760.0],
        quality=256,
    )
)

thermal.save("dynamic_temperature.tif")
sif.save("dynamic_sif.tif")

This call calculates energy balance, An, gs, and Phi_f together. ThermalImager and SIFImager use the scene's current solved state by default, so you do not need to pass intermediate arrays manually.

Sunlit and shaded leaves

Direct light is not uniform across the canopy. LESS records:

  • sunlit_fraction: fraction of leaf area directly illuminated by the sun;
  • APAR: APAR for the selected absorption_component;
  • APAR_total: total leaf-absorbed PAR before component partitioning;
  • APAR_sunlit, APAR_shaded: conditional selected-component APAR;
  • APAR_total_sunlit, APAR_total_shaded: conditional total APAR;
  • absorption_component: the resolved component for each output element;
  • T_sunlit, T_shaded: conditional sunlit and shaded leaf temperatures;
  • An_sunlit, An_shaded: corresponding net photosynthetic rate.

Diffuse skylight and multiple scattering contribute to both groups. Only the sunlit group receives the unobstructed direct-sun term. The final component or primitive result is weighted by sunlit_fraction:

Both APAR pairs obey this weighting independently. For example,

\[ APAR_\mathrm{total} = p_\mathrm{sunlit} APAR_{\mathrm{total,sunlit}} + (1-p_\mathrm{sunlit}) APAR_{\mathrm{total,shaded}}. \]
\[ \overline{An} = p_\mathrm{sunlit} An_\mathrm{sunlit} + (1-p_\mathrm{sunlit}) An_\mathrm{shaded} \]

Because photosynthesis is nonlinear, LESS solves the sunlit and shaded states separately before averaging their results. Averaging APAR before evaluating the Farquhar model would not give the same result.

Result space hierarchy

print(photo.spatial_mode)
  • A mesh-only scene returns "primitive", with one result for each triangle;
  • Static photosynthesis in a scene containing TurbidBoundary returns "component" top-level arrays for the Turbid components. In a static Mesh+Turbid scene, photo.scene_elements additionally preserves the aligned Mesh and Turbid row identities;
  • A joint dynamic thermo-physiology product returns "mixed" when its public arrays span more than one scene-element kind;
  • Leaf samples used to integrate a statistical vegetation medium do not become permanent voxels or scene geometry;
  • Divide a crown into multiple components when finer spatial summaries are required.

Static SIF without photosynthesis or temperature solving

To simulate fluorescence from a prescribed Fluspect.fqe, calculate static SIF directly:

source = scene.simulate(
    less.SIFProcess(
        mode="static",
        quality="high",
    )
)

image = scene.simulate(
    less.SIFImager(
        less.Orthographic(image_size=512),
        bands=[687.0, 740.0, 760.0],
        quality=256,
        sif_result=source,
    )
)

Static SIF does not run the Farquhar model and does not require a dynamic photosynthesis or temperature calculation. It still requires Fluspect, which provides leaf reflectance, transmittance, absorption, fluorescence excitation, and emission spectra.

Sampling quality

Quality presets are available for dynamic physiological processes:

state = scene.solve(
    microclimate=microclimate,
    modules=["energy_balance", "photosynthesis", "sif"],
    photosynthesis_kwargs={"quality": "high"},
    sif_kwargs={"quality": "high"},
    seed=42,
)

Available values are "preview", "standard", and "high". For scientific studies, repeat the calculation at higher sampling quality and confirm that APAR, leaf temperature, An, gs, and Phi_f are stable. Set seed to make repeated calculations reproducible.

Choosing a backend

The same public API is available with each supported backend:

scene = less.Scene(backend="optix")
scene = less.Scene(backend="vulkan")
scene = less.Scene(backend="embree")

OptiX uses an NVIDIA GPU, Vulkan uses a GPU with Ray Query support, and Embree uses the CPU. Switching backends does not change how solve, ThermalImager, or SIFImager is called.

Check closure and convergence

eb = state.energy_balance.result
print("energy converged:", eb.converged)
print("photo converged:", state.photosynthesis.converged)
print("iterations:", state.photosynthesis.iterations)
print("max residual:", abs(eb.residual[eb.active_mask]).max())

If illumination, microclimate, properties, or geometry change, run scene.solve(...) again. An outdated state cannot be used for a new thermal or SIF image.

Next step

Scenes containing mesh and Turbid vegetation

scene.solve(...) can include mesh leaves, a TurbidBoundary canopy, and terrain in one scene. scene_elements identifies the scene element represented by each row. The result includes leaf area, sunlit fraction, sunlit and shaded leaf temperatures, APAR, net photosynthetic rate, and stomatal conductance. After changing scene properties, illumination, or microclimate, run scene.solve(...) again before creating a new image.

  • less.PhotosynthesisProcess, less.SIFProcess
  • less.Farquhar, less.Fluspect, less.BiophysicalProperty
  • less.Microclimate, less.EnergyBalanceProcess
  • less.SIFImager, less.ThermalImager
  • less.PhotosynthesisProcessProduct, less.SIFProcessProduct