Port of xy_target_rgbw_model.py `_nnls_solve` — projected-gradient NNLS for a 3x3 system. The Python version prefers scipy.optimize.nnls when available; we use the same projected-gradient fallback (500 iters @ step 0.01) since scipy isn't available in this TU. The reference falls back to this code on platforms lacking scipy, so this fidelity matches the reference's actual portable behavior.
| 731 | // to this code on platforms lacking scipy, so this fidelity matches the |
| 732 | // reference's actual portable behavior. |
| 733 | inline void nnls3(const float M[3][3], const float b[3], float t_out[3], float& residual) { |
| 734 | float t[3] = {0.0f, 0.0f, 0.0f}; |
| 735 | for (int it = 0; it < 500; ++it) { |
| 736 | // r = M·t − b |
| 737 | float r[3]; |
| 738 | for (int i = 0; i < 3; ++i) { |
| 739 | r[i] = M[i][0]*t[0] + M[i][1]*t[1] + M[i][2]*t[2] - b[i]; |
| 740 | } |
| 741 | // grad = Mᵀ·r |
| 742 | float grad[3]; |
| 743 | for (int j = 0; j < 3; ++j) { |
| 744 | grad[j] = M[0][j]*r[0] + M[1][j]*r[1] + M[2][j]*r[2]; |
| 745 | } |
| 746 | // t ← max(t − step·grad, 0) |
| 747 | const float step = 0.01f; |
| 748 | for (int j = 0; j < 3; ++j) { |
| 749 | float v = t[j] - step * grad[j]; |
| 750 | t[j] = v > 0.0f ? v : 0.0f; |
| 751 | } |
| 752 | } |
| 753 | // residual = ‖M·t − b‖₂ |
| 754 | float r[3]; |
| 755 | for (int i = 0; i < 3; ++i) { |
| 756 | r[i] = M[i][0]*t[0] + M[i][1]*t[1] + M[i][2]*t[2] - b[i]; |
| 757 | } |
| 758 | residual = fl::sqrt(r[0]*r[0] + r[1]*r[1] + r[2]*r[2]); |
| 759 | t_out[0] = t[0]; t_out[1] = t[1]; t_out[2] = t[2]; |
| 760 | } |
| 761 | |
| 762 | // Port of `_strict_project_target_xyz_to_led_hull`. When target_xy is outside |
| 763 | // the LED RGB triangle, projects to the nearest in-hull point by trying NNLS |
no test coverage detected