1.3.72
 
Loading...
Searching...
No Matches
FluspectB.cpp
Go to the documentation of this file.
1
3#include "FluspectB.h"
4#include "Context.h"
5#include "global.h"
6
7#include <algorithm>
8#include <cmath>
9#include <limits>
10
11namespace helios {
12
13 // ------------------------------------------------------------------------
14 // Internal helpers
15 // ------------------------------------------------------------------------
16 namespace {
17
18 // Exponential integral E_1(x) for x > 0. Port of the Chebyshev / NAG S13AAF
19 // approximation used in LeafOptics::transmittance() (plugins/leafoptics/src/
20 // LeafOptics.cpp), rearranged to return E_1 directly rather than the
21 // (1 - k)·exp(-k) + k²·E_1(k) composite.
22 //
23 // Accuracy is approximately 1e-12 relative for x in (0, 85).
24 double expint(double x) {
25 if (x <= 0.0) {
26 // E_1 diverges at 0 and is undefined for x < 0. Caller guards
27 // non-positive Kall values before invoking this.
28 return std::numeric_limits<double>::infinity();
29 }
30 if (x < 4.0) {
31 double xx = 0.5 * x - 1.0;
32 double yy;
33 // Chebyshev polynomial (ported verbatim from LeafOptics).
34 yy = (((((((((((((((-3.60311230482612224e-13L * xx + 3.46348526554087424e-12L) * xx - 2.99627399604128973e-11L) * xx + 2.57747807106988589e-10L) * xx - 2.09330568435488303e-9L) * xx + 1.59501329936987818e-8L) *
35 xx -
36 1.13717900285428895e-7L) *
37 xx +
38 7.55292885309152956e-7L) *
39 xx -
40 4.64980751480619431e-6L) *
41 xx +
42 2.63830365675408129e-5L) *
43 xx -
44 1.37089870978830576e-4L) *
45 xx +
46 6.47686503728103400e-4L) *
47 xx -
48 2.76060141343627983e-3L) *
49 xx +
50 1.05306034687449505e-2L) *
51 xx -
52 3.57191348753631956e-2L) *
53 xx +
54 1.07774527938978692e-1L) *
55 xx -
56 2.96997075145080963e-1L;
57 yy = (yy * xx + 8.64664716763387311e-1L) * xx + 7.42047691268006429e-1L;
58 return yy - std::log(x);
59 }
60 if (x < 85.0) {
61 double xx = 14.5 / (x + 3.25) - 1.0;
62 double yy;
63 yy = (((((((((((((((-1.62806570868460749e-12L * xx - 8.95400579318284288e-13L) * xx - 4.08352702838151578e-12L) * xx - 1.45132988248537498e-11L) * xx - 8.35086918940757852e-11L) * xx - 2.13638678953766289e-10L) * xx -
64 1.10302431467069770e-9L) *
65 xx -
66 3.67128915633455484e-9L) *
67 xx -
68 1.66980544304104726e-8L) *
69 xx -
70 6.11774386401295125e-8L) *
71 xx -
72 2.70306163610271497e-7L) *
73 xx -
74 1.05565006992891261e-6L) *
75 xx -
76 4.72090467203711484e-6L) *
77 xx -
78 1.95076375089955937e-5L) *
79 xx -
80 9.16450482931221453e-5L) *
81 xx -
82 4.05892130452128677e-4L) *
83 xx -
84 2.14213055000334718e-3L;
85 yy = ((yy * xx - 1.06374875116569657e-2L) * xx - 8.50699154984571871e-2L) * xx + 9.23755307807784058e-1L;
86 return std::exp(-x) * yy / x;
87 }
88 return 0.0;
89 }
90
91 // Angular-average transmittivity of a dielectric interface with refractive
92 // index `nr`, averaged from normal to `alpha` degrees. Port of calctav.m
93 // (Stern 1964, Allen 1973; used internally by Fluspect/PROSPECT).
94 double calctav(double alpha_deg, double nr) {
95 const double rd = M_PI / 180.0;
96 const double n2 = nr * nr;
97 const double np = n2 + 1.0;
98 const double nm = n2 - 1.0;
99 const double a = (nr + 1.0) * (nr + 1.0) / 2.0;
100 const double k = -nm * nm / 4.0;
101 const double sa = std::sin(alpha_deg * rd);
102 double b1 = 0.0;
103 if (alpha_deg != 90.0) {
104 b1 = std::sqrt((sa * sa - np / 2.0) * (sa * sa - np / 2.0) + k);
105 }
106 const double b2 = sa * sa - np / 2.0;
107 const double b = b1 - b2;
108 const double b3 = b * b * b;
109 const double a3 = a * a * a;
110 const double ts = (k * k / (6.0 * b3) + k / b - b / 2.0) - (k * k / (6.0 * a3) + k / a - a / 2.0);
111 const double tp1 = -2.0 * n2 * (b - a) / (np * np);
112 const double tp2 = -2.0 * n2 * np * std::log(b / a) / (nm * nm);
113 const double tp3 = n2 * (1.0 / b - 1.0 / a) / 2.0;
114 const double tp4 = 16.0 * n2 * n2 * (n2 * n2 + 1.0) * std::log((2.0 * np * b - nm * nm) / (2.0 * np * a - nm * nm)) / (np * np * np * nm * nm);
115 const double tp5 = 16.0 * n2 * n2 * n2 * (1.0 / (2.0 * np * b - nm * nm) - 1.0 / (2.0 * np * a - nm * nm)) / (np * np * np);
116 const double tp = tp1 + tp2 + tp3 + tp4 + tp5;
117 return (ts + tp) / (2.0 * sa * sa);
118 }
119
120 // Linear interpolation of `y(x_src)` onto `x_dst`. Requires monotonically
121 // increasing x_src. Out-of-range x_dst values clamp to endpoints.
122 std::vector<double> interp1(const std::vector<float> &x_src, const std::vector<double> &y_src, const std::vector<float> &x_dst) {
123 std::vector<double> y_dst(x_dst.size());
124 size_t j = 0;
125 for (size_t i = 0; i < x_dst.size(); ++i) {
126 const double x = x_dst[i];
127 if (x <= x_src.front()) {
128 y_dst[i] = y_src.front();
129 continue;
130 }
131 if (x >= x_src.back()) {
132 y_dst[i] = y_src.back();
133 continue;
134 }
135 while (j + 1 < x_src.size() && x_src[j + 1] < x) {
136 ++j;
137 }
138 const double x0 = x_src[j];
139 const double x1 = x_src[j + 1];
140 const double y0 = y_src[j];
141 const double y1 = y_src[j + 1];
142 y_dst[i] = y0 + (y1 - y0) * (x - x0) / (x1 - x0);
143 }
144 return y_dst;
145 }
146
147 // Load one globaldata_vec2 spectrum from the given XML file into a vector of
148 // (wavelength, value) pairs. Throws on missing/invalid entries.
149 void loadCoefficientSeries(Context &ctx, const std::string &xml_path, const std::string &label, std::vector<float> &wavelengths_nm, std::vector<float> &values) {
150 if (!ctx.doesGlobalDataExist(label.c_str())) {
151 ctx.loadXML(xml_path.c_str(), true);
152 }
153 if (!ctx.doesGlobalDataExist(label.c_str())) {
154 helios_runtime_error("ERROR (FluspectB::loadFluspectOptipar): Coefficient '" + label + "' not found in " + xml_path);
155 }
156 if (ctx.getGlobalDataType(label.c_str()) != HELIOS_TYPE_VEC2) {
157 helios_runtime_error("ERROR (FluspectB::loadFluspectOptipar): Coefficient '" + label + "' is not of type HELIOS_TYPE_VEC2.");
158 }
159 std::vector<vec2> spectrum;
160 ctx.getGlobalData(label.c_str(), spectrum);
161 if (spectrum.empty()) {
162 helios_runtime_error("ERROR (FluspectB::loadFluspectOptipar): Coefficient '" + label + "' is empty.");
163 }
164 const bool reserve_wavelengths = wavelengths_nm.empty();
165 if (reserve_wavelengths) {
166 wavelengths_nm.reserve(spectrum.size());
167 } else if (wavelengths_nm.size() != spectrum.size()) {
168 helios_runtime_error("ERROR (FluspectB::loadFluspectOptipar): Coefficient '" + label + "' has " + std::to_string(spectrum.size()) + " samples but previous coefficients have " + std::to_string(wavelengths_nm.size()) +
169 " - all must share the same grid.");
170 }
171 values.clear();
172 values.reserve(spectrum.size());
173 for (size_t i = 0; i < spectrum.size(); ++i) {
174 if (reserve_wavelengths) {
175 wavelengths_nm.push_back(spectrum[i].x);
176 } else if (std::abs(wavelengths_nm[i] - spectrum[i].x) > 1e-3f) {
177 helios_runtime_error("ERROR (FluspectB::loadFluspectOptipar): Coefficient '" + label + "' wavelength mismatch at index " + std::to_string(i));
178 }
179 values.push_back(spectrum[i].y);
180 }
181 }
182
183 } // namespace
184
185 // ------------------------------------------------------------------------
186 // Public API
187 // ------------------------------------------------------------------------
188
189 void loadFluspectOptipar(const std::string &xml_path, FluspectOptipar &optipar) {
190 // Temporary Context just to load the XML globaldata. Using a local Context
191 // keeps the Optipar data self-contained and independent of the user's scene.
192 Context tmp_ctx;
193
194 optipar.wavelengths_nm.clear();
195 loadCoefficientSeries(tmp_ctx, xml_path, "fluspect_optipar_nr", optipar.wavelengths_nm, optipar.nr);
196 loadCoefficientSeries(tmp_ctx, xml_path, "fluspect_optipar_Kab", optipar.wavelengths_nm, optipar.Kab);
197 loadCoefficientSeries(tmp_ctx, xml_path, "fluspect_optipar_Kca", optipar.wavelengths_nm, optipar.Kca);
198 loadCoefficientSeries(tmp_ctx, xml_path, "fluspect_optipar_KcaV", optipar.wavelengths_nm, optipar.KcaV);
199 loadCoefficientSeries(tmp_ctx, xml_path, "fluspect_optipar_KcaZ", optipar.wavelengths_nm, optipar.KcaZ);
200 loadCoefficientSeries(tmp_ctx, xml_path, "fluspect_optipar_Kdm", optipar.wavelengths_nm, optipar.Kdm);
201 loadCoefficientSeries(tmp_ctx, xml_path, "fluspect_optipar_Kw", optipar.wavelengths_nm, optipar.Kw);
202 loadCoefficientSeries(tmp_ctx, xml_path, "fluspect_optipar_Ks", optipar.wavelengths_nm, optipar.Ks);
203 loadCoefficientSeries(tmp_ctx, xml_path, "fluspect_optipar_Kant", optipar.wavelengths_nm, optipar.Kant);
204 loadCoefficientSeries(tmp_ctx, xml_path, "fluspect_optipar_Kp", optipar.wavelengths_nm, optipar.Kp);
205 loadCoefficientSeries(tmp_ctx, xml_path, "fluspect_optipar_Kcbc", optipar.wavelengths_nm, optipar.Kcbc);
206 loadCoefficientSeries(tmp_ctx, xml_path, "fluspect_optipar_phi", optipar.wavelengths_nm, optipar.phi);
207 }
208
209 FluspectKernel computeFluspectKernel(const FluspectBiochemistry &biochem, const FluspectOptipar &optipar, float excitation_step_nm) {
210 if (excitation_step_nm <= 0.f) {
211 helios_runtime_error("ERROR (computeFluspectKernel): excitation_step_nm must be > 0.");
212 }
213 const size_t nw = optipar.wavelengths_nm.size();
214 if (nw < 2) {
215 helios_runtime_error("ERROR (computeFluspectKernel): Optipar wavelength grid is empty or degenerate.");
216 }
217
218 // ---- Choose Kca (V2Z linear combination of KcaV and KcaZ) ----
219 // MATLAB: if V2Z == -999 use legacy optipar.Kca, else (1-V2Z)*KcaV + V2Z*KcaZ.
220 // We always use the V2Z combination (there's no -999 sentinel in our API).
221 std::vector<double> Kca(nw);
222 for (size_t i = 0; i < nw; ++i) {
223 Kca[i] = (1.0 - biochem.V2Z) * optipar.KcaV[i] + biochem.V2Z * optipar.KcaZ[i];
224 }
225
226 // ---- PROSPECT calculations ----
227 // Kall = (Cab*Kab + Cca*Kca + ...) / N
228 std::vector<double> Kall(nw);
229 for (size_t i = 0; i < nw; ++i) {
230 Kall[i] = (biochem.Cab * optipar.Kab[i] + biochem.Cca * Kca[i] + biochem.Cdm * optipar.Kdm[i] + biochem.Cw * optipar.Kw[i] + biochem.Cs * optipar.Ks[i] + biochem.Cant * optipar.Kant[i] + biochem.Cp * optipar.Kp[i] +
231 biochem.Cbc * optipar.Kcbc[i]) /
232 biochem.N;
233 }
234
235 // tau = (1-Kall)*exp(-Kall) + Kall^2 * E_1(Kall) for Kall > 0, else 1.
236 // kChlrel = Cab*Kab / (Kall * N) for Kall > 0, else 0.
237 std::vector<double> tau(nw, 1.0);
238 std::vector<double> kChlrel(nw, 0.0);
239 for (size_t i = 0; i < nw; ++i) {
240 const double K = Kall[i];
241 if (K > 0.0) {
242 tau[i] = (1.0 - K) * std::exp(-K) + K * K * expint(K);
243 kChlrel[i] = biochem.Cab * optipar.Kab[i] / (K * biochem.N);
244 }
245 }
246
247 // Interface transmittivities: talf=tav(59), t12=tav(90), t21=t12/n^2.
248 std::vector<double> talf(nw), ralf(nw), t12(nw), r12(nw), t21(nw), r21(nw);
249 for (size_t i = 0; i < nw; ++i) {
250 talf[i] = calctav(59.0, optipar.nr[i]);
251 ralf[i] = 1.0 - talf[i];
252 t12[i] = calctav(90.0, optipar.nr[i]);
253 r12[i] = 1.0 - t12[i];
254 t21[i] = t12[i] / (optipar.nr[i] * optipar.nr[i]);
255 r21[i] = 1.0 - t21[i];
256 }
257
258 // Top-surface and bottom-surface layer transmittance/reflectance.
259 // Stokes solution for N-layer stack.
260 std::vector<double> refl(nw), tran(nw);
261 std::vector<double> r_mes(nw), t_mes(nw); // mesophyll r/t after interface removal
262 for (size_t i = 0; i < nw; ++i) {
263 const double denom0 = 1.0 - r21[i] * r21[i] * tau[i] * tau[i];
264 const double Ta = talf[i] * tau[i] * t21[i] / denom0;
265 const double Ra = ralf[i] + r21[i] * tau[i] * Ta;
266
267 const double t_bot = t12[i] * tau[i] * t21[i] / denom0;
268 const double r_bot = r12[i] + r21[i] * tau[i] * t_bot;
269
270 double r = r_bot;
271 double t = t_bot;
272
273 double D = std::sqrt((1.0 + r + t) * (1.0 + r - t) * (1.0 - r + t) * (1.0 - r - t));
274 const double rq = r * r;
275 const double tq = t * t;
276 double a = (1.0 + rq - tq + D) / (2.0 * r);
277 double b = (1.0 - rq + tq + D) / (2.0 * t);
278
279 const double bNm1 = std::pow(b, biochem.N - 1.0);
280 const double bN2 = bNm1 * bNm1;
281 const double a2 = a * a;
282 double denom1 = a2 * bN2 - 1.0;
283 double Rsub = a * (bN2 - 1.0) / denom1;
284 double Tsub = bNm1 * (a2 - 1.0) / denom1;
285 if (r + t >= 1.0) {
286 // Zero-absorption limit
287 Tsub = t / (t + (1.0 - t) * (biochem.N - 1.0));
288 Rsub = 1.0 - Tsub;
289 }
290
291 const double denom2 = 1.0 - Rsub * r;
292 tran[i] = Ta * Tsub / denom2;
293 refl[i] = Ra + Ta * Rsub * t / denom2;
294 }
295
296 // ---- Remove leaf-air interfaces to get mesophyll-only rho, tau. ----
297 // Rb = (refl - ralf) / (talf*t21 + (refl - ralf)*r21)
298 // Z = tran*(1 - Rb*r21) / (talf*t21)
299 // rho = (Rb - r21*Z^2) / (1 - (r21*Z)^2)
300 // tau_m = (1 - Rb*r21) / (1 - (r21*Z)^2) * Z
301 std::vector<double> rho_m(nw), tau_m(nw), Rb_full(nw);
302 for (size_t i = 0; i < nw; ++i) {
303 const double Rb = (refl[i] - ralf[i]) / (talf[i] * t21[i] + (refl[i] - ralf[i]) * r21[i]);
304 const double Z = tran[i] * (1.0 - Rb * r21[i]) / (talf[i] * t21[i]);
305 double rho_val = (Rb - r21[i] * Z * Z) / (1.0 - (r21[i] * Z) * (r21[i] * Z));
306 double tau_val = (1.0 - Rb * r21[i]) / (1.0 - (r21[i] * Z) * (r21[i] * Z)) * Z;
307 if (rho_val < 0.0) rho_val = 0.0; // Avoid negative r (MATLAB max(rho,0))
308 rho_m[i] = rho_val;
309 tau_m[i] = tau_val;
310 r_mes[i] = rho_val;
311 t_mes[i] = tau_val;
312 // Rb used later with interfaces reintroduced: Rb = rho + tau^2*r21/(1-rho*r21)
313 Rb_full[i] = rho_val + tau_val * tau_val * r21[i] / (1.0 - rho_val * r21[i]);
314 }
315
316 // ---- Kubelka-Munk s and k for mesophyll layer ----
317 std::vector<double> s(nw), k_km(nw), kChl(nw);
318 for (size_t i = 0; i < nw; ++i) {
319 const double r = r_mes[i];
320 const double t = t_mes[i];
321 double a = 1.0;
322 double b = 1.0;
323 if (r + t < 1.0) {
324 const double D = std::sqrt((1.0 + r + t) * (1.0 + r - t) * (1.0 - r + t) * (1.0 - r - t));
325 a = (1.0 + r * r - t * t + D) / (2.0 * r);
326 b = (1.0 - r * r + t * t + D) / (2.0 * t);
327 }
328 double s_val = r / t;
329 double k_val = std::log(b);
330 if (a > 1.0 && std::isfinite(a)) {
331 s_val = 2.0 * a / (a * a - 1.0) * std::log(b);
332 k_val = (a - 1.0) / (a + 1.0) * std::log(b);
333 }
334 s[i] = s_val;
335 k_km[i] = k_val;
336 kChl[i] = kChlrel[i] * k_val;
337 }
338
339 // ---- Prepare output kernel struct ----
340 FluspectKernel out;
341 out.wavelengths_nm = optipar.wavelengths_nm;
342 out.refl.assign(refl.begin(), refl.end());
343 out.tran.assign(tran.begin(), tran.end());
344
345 // Skip fluorescence matrices if fqe == 0.
346 if (biochem.fqe <= 0.f) {
347 return out;
348 }
349
350 // ---- Excitation and emission grids ----
351 // wle = 400 : excitation_step_nm : 750 (inclusive)
352 const double int_step = excitation_step_nm;
353 std::vector<float> wle;
354 for (double w = 400.0; w <= 750.0 + 1e-9; w += int_step) {
355 wle.push_back(static_cast<float>(w));
356 }
357 // wlf = 640:4:850 — MATLAB yields 640, 644, ..., 848 (53 entries).
358 std::vector<float> wlf;
359 for (double w = 640.0; w <= 850.0 - 1e-9; w += 4.0) {
360 wlf.push_back(static_cast<float>(w));
361 }
362 const size_t n_wle = wle.size();
363 const size_t n_wlf = wlf.size();
364
365 out.wle = wle;
366 out.wlf = wlf;
367
368 // Interpolate per-wavelength quantities onto excitation grid
369 auto k_iwle = interp1(optipar.wavelengths_nm, k_km, wle);
370 auto s_iwle = interp1(optipar.wavelengths_nm, s, wle);
371 auto kChl_iwle = interp1(optipar.wavelengths_nm, kChl, wle);
372 auto r21_iwle = interp1(optipar.wavelengths_nm, r21, wle);
373 auto rho_iwle = interp1(optipar.wavelengths_nm, rho_m, wle);
374 auto tau_iwle = interp1(optipar.wavelengths_nm, tau_m, wle);
375 auto talf_iwle = interp1(optipar.wavelengths_nm, talf, wle);
376 auto Rb_iwle = interp1(optipar.wavelengths_nm, Rb_full, wle);
377
378 // Locate wlf wavelengths in the full optipar grid (equivalent to MATLAB's
379 // intersect(wlp, wlf)). We require exact matches within 0.5 nm since both
380 // grids are on integer nm.
381 std::vector<size_t> Iwlf(n_wlf);
382 size_t guess = 0;
383 for (size_t i = 0; i < n_wlf; ++i) {
384 const float target = wlf[i];
385 bool found = false;
386 for (size_t j = guess; j < optipar.wavelengths_nm.size(); ++j) {
387 if (std::abs(optipar.wavelengths_nm[j] - target) < 0.5f) {
388 Iwlf[i] = j;
389 guess = j;
390 found = true;
391 break;
392 }
393 }
394 if (!found) {
395 helios_runtime_error("ERROR (computeFluspectKernel): emission wavelength " + std::to_string(target) + " nm not present in Optipar grid.");
396 }
397 }
398
399 // Values at wlf indices
400 std::vector<double> k_wlf(n_wlf), s_wlf(n_wlf), phi_wlf(n_wlf), r21_wlf(n_wlf), t21_wlf(n_wlf), tau_wlf(n_wlf), rho_wlf(n_wlf), Rb_wlf(n_wlf);
401 for (size_t i = 0; i < n_wlf; ++i) {
402 const size_t j = Iwlf[i];
403 k_wlf[i] = k_km[j];
404 s_wlf[i] = s[j];
405 phi_wlf[i] = optipar.phi[j];
406 r21_wlf[i] = r21[j];
407 t21_wlf[i] = t21[j];
408 tau_wlf[i] = tau_m[j];
409 rho_wlf[i] = rho_m[j];
410 Rb_wlf[i] = Rb_full[j];
411 }
412
413 // ---- Doubling initialisation ----
414 // ndub = 15 doublings, eps = 2^(-ndub) is the initial layer-thickness scale.
415 const int ndub = 15;
416 const double eps_step = std::ldexp(1.0, -ndub); // 2^-15
417
418 std::vector<double> te(n_wle), re(n_wle);
419 for (size_t j = 0; j < n_wle; ++j) {
420 te[j] = 1.0 - (k_iwle[j] + s_iwle[j]) * eps_step;
421 re[j] = s_iwle[j] * eps_step;
422 }
423 std::vector<double> tf(n_wlf), rf(n_wlf);
424 for (size_t i = 0; i < n_wlf; ++i) {
425 tf[i] = 1.0 - (k_wlf[i] + s_wlf[i]) * eps_step;
426 rf[i] = s_wlf[i] * eps_step;
427 }
428
429 // sigmoid[wlf_idx][wle_idx] = 1 / (1 + exp(-wlf/10) * exp(wle/10))
430 // Damps emission at wavelengths shorter than the excitation (Stokes shift).
431 std::vector<std::vector<double>> sigmoid(n_wlf, std::vector<double>(n_wle));
432 for (size_t i = 0; i < n_wlf; ++i) {
433 const double em_factor = std::exp(-static_cast<double>(wlf[i]) / 10.0);
434 for (size_t j = 0; j < n_wle; ++j) {
435 const double ex_factor = std::exp(static_cast<double>(wle[j]) / 10.0);
436 sigmoid[i][j] = 1.0 / (1.0 + em_factor * ex_factor);
437 }
438 }
439
440 // Initial Mf, Mb matrices (both start equal):
441 // M[i][j] = int_step * fqe * 0.5 * phi(wlf_i) * eps * kChl(wle_j) * sigmoid[i][j]
442 std::vector<std::vector<double>> Mf(n_wlf, std::vector<double>(n_wle));
443 std::vector<std::vector<double>> Mb(n_wlf, std::vector<double>(n_wle));
444 const double init_scale = int_step * biochem.fqe * 0.5 * eps_step;
445 for (size_t i = 0; i < n_wlf; ++i) {
446 for (size_t j = 0; j < n_wle; ++j) {
447 const double v = init_scale * phi_wlf[i] * kChl_iwle[j] * sigmoid[i][j];
448 Mf[i][j] = v;
449 Mb[i][j] = v;
450 }
451 }
452
453 // ---- Doubling loop ----
454 std::vector<std::vector<double>> Mfn(n_wlf, std::vector<double>(n_wle));
455 std::vector<std::vector<double>> Mbn(n_wlf, std::vector<double>(n_wle));
456 for (int iter = 0; iter < ndub; ++iter) {
457 // Excitation-side updates (column per wle_j)
458 std::vector<double> xe(n_wle), ten(n_wle), ren(n_wle);
459 for (size_t j = 0; j < n_wle; ++j) {
460 xe[j] = te[j] / (1.0 - re[j] * re[j]);
461 ten[j] = te[j] * xe[j];
462 ren[j] = re[j] * (1.0 + ten[j]);
463 }
464 // Emission-side updates (row per wlf_i)
465 std::vector<double> xf(n_wlf), tfn(n_wlf), rfn(n_wlf);
466 for (size_t i = 0; i < n_wlf; ++i) {
467 xf[i] = tf[i] / (1.0 - rf[i] * rf[i]);
468 tfn[i] = tf[i] * xf[i];
469 rfn[i] = rf[i] * (1.0 + tfn[i]);
470 }
471
472 // Apply: Mfn = Mf*A11 + Mb*A12; Mbn = Mb*A21 + Mf*A22 (element-wise)
473 // A11 = xf[i] + xe[j]
474 // A12 = xf[i]*xe[j] * (rf[i] + re[j])
475 // A21 = 1 + xf[i]*xe[j] * (1 + rf[i]*re[j])
476 // A22 = xf[i]*rf[i] + xe[j]*re[j]
477 for (size_t i = 0; i < n_wlf; ++i) {
478 for (size_t j = 0; j < n_wle; ++j) {
479 const double A11 = xf[i] + xe[j];
480 const double A12 = xf[i] * xe[j] * (rf[i] + re[j]);
481 const double A21 = 1.0 + xf[i] * xe[j] * (1.0 + rf[i] * re[j]);
482 const double A22 = xf[i] * rf[i] + xe[j] * re[j];
483 Mfn[i][j] = Mf[i][j] * A11 + Mb[i][j] * A12;
484 Mbn[i][j] = Mb[i][j] * A21 + Mf[i][j] * A22;
485 }
486 }
487
488 te = ten;
489 re = ren;
490 tf = tfn;
491 rf = rfn;
492 Mf.swap(Mfn);
493 Mb.swap(Mbn);
494 }
495
496 // ---- Re-add leaf-air interfaces to Mf/Mb ----
497 // Rb_iwle = Rb_full interpolated to wle grid
498 // Xe[i][j] = talf_iwle[j] / (1 - r21_iwle[j] * Rb_iwle[j])
499 // Xf[i][j] = t21_wlf[i] / (1 - r21_wlf[i] * Rb_wlf[i])
500 // Ye[i][j] = tau_iwle[j] * r21_iwle[j] / (1 - rho_iwle[j] * r21_iwle[j])
501 // Yf[i][j] = tau_wlf[i] * r21_wlf[i] / (1 - rho_wlf[i] * r21_wlf[i])
502 // A = Xe * (1 + Ye*Yf) * Xf
503 // B = Xe * (Ye + Yf) * Xf
504 // Mb_out = A*Mb_init + B*Mf_init
505 // Mf_out = A*Mf_init + B*Mb_init
506 std::vector<double> xe_vec(n_wle), ye_vec(n_wle);
507 for (size_t j = 0; j < n_wle; ++j) {
508 xe_vec[j] = talf_iwle[j] / (1.0 - r21_iwle[j] * Rb_iwle[j]);
509 ye_vec[j] = tau_iwle[j] * r21_iwle[j] / (1.0 - rho_iwle[j] * r21_iwle[j]);
510 }
511 std::vector<double> xf_vec(n_wlf), yf_vec(n_wlf);
512 for (size_t i = 0; i < n_wlf; ++i) {
513 xf_vec[i] = t21_wlf[i] / (1.0 - r21_wlf[i] * Rb_wlf[i]);
514 yf_vec[i] = tau_wlf[i] * r21_wlf[i] / (1.0 - rho_wlf[i] * r21_wlf[i]);
515 }
516
517 out.Mf.assign(n_wlf, std::vector<float>(n_wle));
518 out.Mb.assign(n_wlf, std::vector<float>(n_wle));
519 for (size_t i = 0; i < n_wlf; ++i) {
520 for (size_t j = 0; j < n_wle; ++j) {
521 const double A = xe_vec[j] * (1.0 + ye_vec[j] * yf_vec[i]) * xf_vec[i];
522 const double B = xe_vec[j] * (ye_vec[j] + yf_vec[i]) * xf_vec[i];
523 const double g = Mb[i][j]; // post-doubling Mb
524 const double f = Mf[i][j]; // post-doubling Mf
525 const double gn = A * g + B * f;
526 const double fn = A * f + B * g;
527 out.Mb[i][j] = static_cast<float>(gn);
528 out.Mf[i][j] = static_cast<float>(fn);
529 }
530 }
531
532 return out;
533 }
534
535} // namespace helios