Coverage for appearance/ciecam16.py: 73%
119 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 22:49 +1300
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 22:49 +1300
1"""
2CIECAM16 Colour Appearance Model
3================================
5Define the *CIECAM16* colour appearance model for predicting perceptual colour
6attributes under varying viewing conditions.
8- :class:`colour.appearance.InductionFactors_CIECAM16`
9- :attr:`colour.VIEWING_CONDITIONS_CIECAM16`
10- :class:`colour.CAM_Specification_CIECAM16`
11- :func:`colour.XYZ_to_CIECAM16`
12- :func:`colour.CIECAM16_to_XYZ`
14References
15----------
16- :cite:`CIEDivision12022` : CIE Division 1 & CIE Division 8. (2022).
17 CIE 248:2022 The CIE 2016 Colour Appearance Model for Colour Management
18 Systems: CIECAM16. Commission Internationale de l'Eclairage.
19 ISBN:978-3-902842-94-7
20"""
22from __future__ import annotations
24from dataclasses import astuple, dataclass, field
26import numpy as np
28from colour.algebra import spow, vecmul
29from colour.appearance.cam16 import MATRIX_16, MATRIX_INVERSE_16
30from colour.appearance.ciecam02 import (
31 VIEWING_CONDITIONS_CIECAM02,
32 InductionFactors_CIECAM02,
33 P,
34 achromatic_response_forward,
35 achromatic_response_inverse,
36 brightness_correlate,
37 chroma_correlate,
38 colourfulness_correlate,
39 degree_of_adaptation,
40 eccentricity_factor,
41 hue_angle,
42 hue_quadrature,
43 lightness_correlate,
44 matrix_post_adaptation_non_linear_response_compression,
45 opponent_colour_dimensions_forward,
46 opponent_colour_dimensions_inverse,
47 post_adaptation_non_linear_response_compression_forward,
48 saturation_correlate,
49 temporary_magnitude_quantity_inverse,
50 viewing_conditions_dependent_parameters,
51)
52from colour.hints import ( # noqa: TC001
53 Annotated,
54 ArrayLike,
55 Domain100,
56 NDArrayFloat,
57 Range100,
58)
59from colour.utilities import (
60 CanonicalMapping,
61 MixinDataclassArithmetic,
62 MixinDataclassIterable,
63 as_float,
64 as_float_array,
65 from_range_100,
66 from_range_degrees,
67 has_only_nan,
68 ones,
69 to_domain_100,
70 to_domain_degrees,
71 tsplit,
72)
74__author__ = "Colour Developers"
75__copyright__ = "Copyright 2013 Colour Developers"
76__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
77__maintainer__ = "Colour Developers"
78__email__ = "colour-developers@colour-science.org"
79__status__ = "Production"
81__all__ = [
82 "InductionFactors_CIECAM16",
83 "VIEWING_CONDITIONS_CIECAM16",
84 "CAM_Specification_CIECAM16",
85 "XYZ_to_CIECAM16",
86 "CIECAM16_to_XYZ",
87 "f_e_forward",
88 "f_e_inverse",
89 "f_q",
90 "d_f_q",
91]
94@dataclass(frozen=True)
95class InductionFactors_CIECAM16(MixinDataclassIterable):
96 """
97 Define the *CIECAM16* colour appearance model induction factors.
99 Parameters
100 ----------
101 F
102 Maximum degree of adaptation :math:`F`.
103 c
104 Exponential non-linearity :math:`c`.
105 N_c
106 Chromatic induction factor :math:`N_c`.
108 Notes
109 -----
110 - The *CIECAM16* colour appearance model induction factors are the same
111 as *CIECAM02* colour appearance model.
113 References
114 ----------
115 :cite:`CIEDivision12022`
116 """
118 F: float
119 c: float
120 N_c: float
123VIEWING_CONDITIONS_CIECAM16: CanonicalMapping = CanonicalMapping(
124 VIEWING_CONDITIONS_CIECAM02
125)
126VIEWING_CONDITIONS_CIECAM16.__doc__ = """
127Define the reference *CIECAM16* colour appearance model viewing conditions.
129References
130----------
131:cite:`CIEDivision12022`
132"""
135@dataclass
136class CAM_Specification_CIECAM16(MixinDataclassArithmetic):
137 """
138 Define the *CIECAM16* colour appearance model specification.
140 Parameters
141 ----------
142 J
143 Correlate of *lightness* :math:`J`.
144 C
145 Correlate of *chroma* :math:`C`.
146 h
147 *Hue* angle :math:`h` in degrees.
148 s
149 Correlate of *saturation* :math:`s`.
150 Q
151 Correlate of *brightness* :math:`Q`.
152 M
153 Correlate of *colourfulness* :math:`M`.
154 H
155 *Hue* :math:`h` quadrature :math:`H`.
156 HC
157 *Hue* :math:`h` composition :math:`H^C`.
159 References
160 ----------
161 :cite:`CIEDivision12022`
162 """
164 J: float | NDArrayFloat | None = field(default_factory=lambda: None)
165 C: float | NDArrayFloat | None = field(default_factory=lambda: None)
166 h: float | NDArrayFloat | None = field(default_factory=lambda: None)
167 s: float | NDArrayFloat | None = field(default_factory=lambda: None)
168 Q: float | NDArrayFloat | None = field(default_factory=lambda: None)
169 M: float | NDArrayFloat | None = field(default_factory=lambda: None)
170 H: float | NDArrayFloat | None = field(default_factory=lambda: None)
171 HC: float | NDArrayFloat | None = field(default_factory=lambda: None)
174def XYZ_to_CIECAM16(
175 XYZ: Domain100,
176 XYZ_w: Domain100,
177 L_A: ArrayLike,
178 Y_b: ArrayLike,
179 surround: (
180 InductionFactors_CIECAM02 | InductionFactors_CIECAM16
181 ) = VIEWING_CONDITIONS_CIECAM16["Average"],
182 discount_illuminant: bool = False,
183 compute_H: bool = True,
184) -> Annotated[CAM_Specification_CIECAM16, (100, 100, 360, 100, 100, 100, 400)]:
185 """
186 Compute the *CIECAM16* colour appearance model correlates from the
187 specified *CIE XYZ* tristimulus values.
189 Parameters
190 ----------
191 XYZ
192 *CIE XYZ* tristimulus values of test sample / stimulus.
193 XYZ_w
194 *CIE XYZ* tristimulus values of reference white.
195 L_A
196 Adapting field *luminance* :math:`L_A` in :math:`cd/m^2`, (often
197 taken to be 20% of the luminance of a white object in the scene).
198 Y_b
199 Luminous factor of background :math:`Y_b` such as
200 :math:`Y_b = 100 \\times L_b / L_w` where :math:`L_w` is the
201 luminance of the light source and :math:`L_b` is the luminance of
202 the background. For viewing images, :math:`Y_b` can be the average
203 :math:`Y` value for the pixels in the entire image, or frequently,
204 a :math:`Y` value of 20, approximate an :math:`L^*` of 50 is used.
205 surround
206 Surround viewing conditions induction factors.
207 discount_illuminant
208 Truth value indicating if the illuminant should be discounted.
209 compute_H
210 Whether to compute *Hue* :math:`h` quadrature :math:`H`.
211 :math:`H` is rarely used, and expensive to compute.
213 Returns
214 -------
215 :class:`colour.CAM_Specification_CIECAM16`
216 *CIECAM16* colour appearance model specification.
218 Notes
219 -----
220 +---------------------+-----------------------+---------------+
221 | **Domain** | **Scale - Reference** | **Scale - 1** |
222 +=====================+=======================+===============+
223 | ``XYZ`` | 100 | 1 |
224 +---------------------+-----------------------+---------------+
225 | ``XYZ_w`` | 100 | 1 |
226 +---------------------+-----------------------+---------------+
228 +---------------------+-----------------------+---------------+
229 | **Range** | **Scale - Reference** | **Scale - 1** |
230 +=====================+=======================+===============+
231 | ``specification.J`` | 100 | 1 |
232 +---------------------+-----------------------+---------------+
233 | ``specification.C`` | 100 | 1 |
234 +---------------------+-----------------------+---------------+
235 | ``specification.h`` | 360 | 1 |
236 +---------------------+-----------------------+---------------+
237 | ``specification.s`` | 100 | 1 |
238 +---------------------+-----------------------+---------------+
239 | ``specification.Q`` | 100 | 1 |
240 +---------------------+-----------------------+---------------+
241 | ``specification.M`` | 100 | 1 |
242 +---------------------+-----------------------+---------------+
243 | ``specification.H`` | 400 | 1 |
244 +---------------------+-----------------------+---------------+
246 References
247 ----------
248 :cite:`CIEDivision12022`
250 Examples
251 --------
252 >>> XYZ = np.array([19.01, 20.00, 21.78])
253 >>> XYZ_w = np.array([95.05, 100.00, 108.88])
254 >>> L_A = 318.31
255 >>> Y_b = 20.0
256 >>> surround = VIEWING_CONDITIONS_CIECAM16["Average"]
257 >>> XYZ_to_CIECAM16(XYZ, XYZ_w, L_A, Y_b, surround) # doctest: +ELLIPSIS
258 CAM_Specification_CIECAM16(J=41.7312079..., C=0.1033557..., \
259h=217.0679597..., s=2.3450150..., Q=195.3717089..., M=0.1074367..., \
260H=275.5949861..., HC=None)
261 """
263 XYZ = to_domain_100(XYZ)
264 XYZ_w = to_domain_100(XYZ_w)
265 _X_w, Y_w, _Z_w = tsplit(XYZ_w)
266 L_A = as_float_array(L_A)
267 Y_b = as_float_array(Y_b)
269 # Step 0
270 # Converting *CIE XYZ* tristimulus values to sharpened *RGB* values.
271 RGB_w = vecmul(MATRIX_16, XYZ_w)
273 # Computing degree of adaptation :math:`D`.
274 D = (
275 np.clip(degree_of_adaptation(surround.F, L_A), 0, 1)
276 if not discount_illuminant
277 else ones(L_A.shape)
278 )
280 n, F_L, N_bb, N_cb, z = viewing_conditions_dependent_parameters(Y_b, Y_w, L_A)
282 D_RGB = D[..., None] * 100 / RGB_w + 1 - D[..., None]
283 RGB_wc = D_RGB * RGB_w
285 # Applying forward post-adaptation non-linear response compression.
286 RGB_aw = post_adaptation_non_linear_response_compression_forward(RGB_wc, F_L)
288 # Computing achromatic responses for the whitepoint.
289 A_w = achromatic_response_forward(RGB_aw, N_bb)
291 # Step 1
292 # Converting *CIE XYZ* tristimulus values to sharpened *RGB* values.
293 RGB = vecmul(MATRIX_16, XYZ)
295 # Step 2
296 RGB_c = D_RGB * RGB
298 # Step 3
299 # Applying forward post-adaptation non-linear response compression.
300 RGB_a = f_e_forward(RGB_c, F_L) + 0.1
302 # Step 4
303 # Converting to preliminary cartesian coordinates.
304 a, b = tsplit(opponent_colour_dimensions_forward(RGB_a))
306 # Computing the *hue* angle :math:`h`.
307 h = hue_angle(a, b)
309 # Step 5
310 # Computing eccentricity factor *e_t*.
311 e_t = eccentricity_factor(h)
313 # Computing hue :math:`h` quadrature :math:`H`.
314 H = hue_quadrature(h) if compute_H else np.full(h.shape, np.nan)
315 # TODO: Compute hue composition.
317 # Step 6
318 # Computing achromatic responses for the stimulus.
319 A = achromatic_response_forward(RGB_a, N_bb)
321 # Step 7
322 # Computing the correlate of *Lightness* :math:`J`.
323 J = lightness_correlate(A, A_w, surround.c, z)
325 # Step 8
326 # Computing the correlate of *brightness* :math:`Q`.
327 Q = brightness_correlate(surround.c, J, A_w, F_L)
329 # Step 9
330 # Computing the correlate of *chroma* :math:`C`.
331 C = chroma_correlate(J, n, surround.N_c, N_cb, e_t, a, b, RGB_a)
333 # Computing the correlate of *colourfulness* :math:`M`.
334 M = colourfulness_correlate(C, F_L)
336 # Computing the correlate of *saturation* :math:`s`.
337 s = saturation_correlate(M, Q)
339 return CAM_Specification_CIECAM16(
340 J=as_float(from_range_100(J)),
341 C=as_float(from_range_100(C)),
342 h=as_float(from_range_degrees(h)),
343 s=as_float(from_range_100(s)),
344 Q=as_float(from_range_100(Q)),
345 M=as_float(from_range_100(M)),
346 H=as_float(from_range_degrees(H, 400)),
347 HC=None,
348 )
351def CIECAM16_to_XYZ(
352 specification: Annotated[
353 CAM_Specification_CIECAM16, (100, 100, 360, 100, 100, 100, 400)
354 ],
355 XYZ_w: Domain100,
356 L_A: ArrayLike,
357 Y_b: ArrayLike,
358 surround: (
359 InductionFactors_CIECAM02 | InductionFactors_CIECAM16
360 ) = VIEWING_CONDITIONS_CIECAM16["Average"],
361 discount_illuminant: bool = False,
362) -> Range100:
363 """
364 Convert the *CIECAM16* colour appearance model specification to *CIE XYZ*
365 tristimulus values.
367 Parameters
368 ----------
369 specification
370 *CIECAM16* colour appearance model specification. Correlate of
371 *lightness* :math:`J`, correlate of *chroma* :math:`C` or correlate of
372 *colourfulness* :math:`M` and *hue* angle :math:`h` in degrees must be
373 specified, e.g., :math:`JCh` or :math:`JMh`.
374 XYZ_w
375 *CIE XYZ* tristimulus values of reference white.
376 L_A
377 Adapting field *luminance* :math:`L_A` in :math:`cd/m^2`, (often taken
378 to be 20% of the luminance of a white object in the scene).
379 Y_b
380 Luminous factor of background :math:`Y_b` such as
381 :math:`Y_b = 100 \\times L_b / L_w` where :math:`L_w` is the luminance
382 of the light source and :math:`L_b` is the luminance of the background.
383 For viewing images, :math:`Y_b` can be the average :math:`Y` value for
384 the pixels in the entire image, or frequently, a :math:`Y` value of 20,
385 approximating an :math:`L^*` of 50 is used.
386 surround
387 Surround viewing conditions.
388 discount_illuminant
389 Discount the illuminant.
391 Returns
392 -------
393 :class:`numpy.ndarray`
394 *CIE XYZ* tristimulus values.
396 Raises
397 ------
398 ValueError
399 If neither :math:`C` or :math:`M` correlates have been defined in the
400 ``specification`` argument.
402 Notes
403 -----
404 +---------------------+-----------------------+---------------+
405 | **Domain** | **Scale - Reference** | **Scale - 1** |
406 +=====================+=======================+===============+
407 | ``specification.J`` | 100 | 1 |
408 +---------------------+-----------------------+---------------+
409 | ``specification.C`` | 100 | 1 |
410 +---------------------+-----------------------+---------------+
411 | ``specification.h`` | 360 | 1 |
412 +---------------------+-----------------------+---------------+
413 | ``specification.s`` | 100 | 1 |
414 +---------------------+-----------------------+---------------+
415 | ``specification.Q`` | 100 | 1 |
416 +---------------------+-----------------------+---------------+
417 | ``specification.M`` | 100 | 1 |
418 +---------------------+-----------------------+---------------+
419 | ``specification.H`` | 360 | 1 |
420 +---------------------+-----------------------+---------------+
421 | ``XYZ_w`` | 100 | 1 |
422 +---------------------+-----------------------+---------------+
424 +---------------------+-----------------------+---------------+
425 | **Range** | **Scale - Reference** | **Scale - 1** |
426 +=====================+=======================+===============+
427 | ``XYZ`` | 100 | 1 |
428 +---------------------+-----------------------+---------------+
430 References
431 ----------
432 :cite:`CIEDivision12022`
434 Examples
435 --------
436 >>> specification = CAM_Specification_CIECAM16(
437 ... J=41.731207905126638, C=0.103355738709070, h=217.067959767393010
438 ... )
439 >>> XYZ_w = np.array([95.05, 100.00, 108.88])
440 >>> L_A = 318.31
441 >>> Y_b = 20.0
442 >>> CIECAM16_to_XYZ(specification, XYZ_w, L_A, Y_b) # doctest: +ELLIPSIS
443 array([ 19.01..., 20... , 21.78...])
444 """
446 J, C, h, _s, _Q, M, _H, _HC = astuple(specification)
448 J = to_domain_100(J)
449 C = to_domain_100(C)
450 h = to_domain_degrees(h)
451 M = to_domain_100(M)
452 L_A = as_float_array(L_A)
453 XYZ_w = to_domain_100(XYZ_w)
454 _X_w, Y_w, _Z_w = tsplit(XYZ_w)
456 # Step 0
457 # Converting *CIE XYZ* tristimulus values to sharpened *RGB* values.
458 RGB_w = vecmul(MATRIX_16, XYZ_w)
460 # Computing degree of adaptation :math:`D`.
461 D = (
462 np.clip(degree_of_adaptation(surround.F, L_A), 0, 1)
463 if not discount_illuminant
464 else ones(L_A.shape)
465 )
467 n, F_L, N_bb, N_cb, z = viewing_conditions_dependent_parameters(Y_b, Y_w, L_A)
469 D_RGB = D[..., None] * 100 / RGB_w + 1 - D[..., None]
470 RGB_wc = D_RGB * RGB_w
472 # Applying forward post-adaptation non-linear response compression.
473 RGB_aw = post_adaptation_non_linear_response_compression_forward(RGB_wc, F_L)
475 # Computing achromatic responses for the whitepoint.
476 A_w = achromatic_response_forward(RGB_aw, N_bb)
478 # Step 1
479 if has_only_nan(C) and not has_only_nan(M):
480 C = M / spow(F_L, 0.25)
481 elif has_only_nan(C):
482 error = (
483 'Either "C" or "M" correlate must be defined in '
484 'the "CAM_Specification_CIECAM16" argument!'
485 )
487 raise ValueError(error)
489 # Step 2
490 # Computing temporary magnitude quantity :math:`t`.
491 t = temporary_magnitude_quantity_inverse(C, J, n)
493 # Computing eccentricity factor *e_t*.
494 e_t = eccentricity_factor(h)
496 # Computing achromatic response :math:`A` for the stimulus.
497 A = achromatic_response_inverse(A_w, J, surround.c, z)
499 # Computing *P_1* to *P_3*.
500 P_n = P(surround.N_c, N_cb, e_t, t, A, N_bb)
501 _P_1, P_2, _P_3 = tsplit(P_n)
503 # Step 3
504 # Computing opponent colour dimensions :math:`a` and :math:`b`.
505 ab = opponent_colour_dimensions_inverse(P_n, h)
506 a, b = tsplit(ab) * np.where(t == 0, 0, 1)
508 # Step 4
509 # Applying post-adaptation non-linear response compression matrix.
510 RGB_a = matrix_post_adaptation_non_linear_response_compression(P_2, a, b)
512 # Step 5
513 # Applying inverse post-adaptation non-linear response compression.
514 RGB_c = f_e_inverse(RGB_a - 0.1, F_L)
516 # Step 6
517 RGB = RGB_c / D_RGB
519 # Step 7
520 XYZ = vecmul(MATRIX_INVERSE_16, RGB)
522 return from_range_100(XYZ)
525def f_e_forward(RGB_c: ArrayLike, F_L: ArrayLike) -> NDArrayFloat:
526 """
527 Compute the post-adaptation cone responses.
529 Parameters
530 ----------
531 RGB_c
532 *CMCCAT2000* transform sharpened :math:`RGB_c` array.
533 F_L
534 *Luminance* level adaptation factor :math:`F_L`.
536 Returns
537 -------
538 :class:`numpy.ndarray`
539 Compressed *CMCCAT2000* transform sharpened :math:`RGB_a` array.
541 Notes
542 -----
543 - This definition is different from :cite:`Li2017` and provides linear
544 extensions under 0.26 and above 150. It also omits the 0.1 offset
545 that is now part of the general model.
547 Examples
548 --------
549 >>> RGB_c = np.array([19.99693975, 20.00186123, 20.01350530])
550 >>> F_L = 1.16754446415
551 >>> f_e_forward(RGB_c, F_L)
552 ... # doctest: +ELLIPSIS
553 array([ 7.8463202..., 7.8471152..., 7.8489959...])
554 """
556 RGB_c = as_float_array(RGB_c)
557 F_L = as_float_array(F_L)
558 q_L, q_U = 0.26, 150
560 f_q_F_L_q_U = f_q(F_L, q_U)[..., None]
561 f_q_F_L_q_L = f_q(F_L, q_L)[..., None]
562 f_q_F_L_RGB_c = f_q(F_L[..., None], RGB_c)
563 d_f_q_F_L_q_U = d_f_q(F_L, q_U)[..., None]
565 return np.select(
566 [
567 RGB_c > q_U,
568 np.logical_and(q_L <= RGB_c, RGB_c <= q_U),
569 RGB_c < q_L,
570 ],
571 [
572 f_q_F_L_q_U + d_f_q_F_L_q_U * (RGB_c - q_U),
573 f_q_F_L_RGB_c,
574 f_q_F_L_q_L * RGB_c / q_L,
575 ],
576 )
579def f_e_inverse(RGB_a: ArrayLike, F_L: ArrayLike) -> NDArrayFloat:
580 """
581 Compute the inverse of the forward eccentricity factor modified cone-like
582 responses.
584 Parameters
585 ----------
586 RGB_a
587 *CMCCAT2000* transform sharpened :math:`RGB_a` array.
588 F_L
589 *Luminance* level adaptation factor :math:`F_L`.
591 Returns
592 -------
593 :class:`numpy.ndarray`
594 Compressed *CMCCAT2000* transform sharpened :math:`RGB_c` array.
596 Notes
597 -----
598 - This definition is different from :cite:`Li2017` and provides linear
599 extensions under 0.26 and above 150. It also omits the 0.1 offset
600 that is now part of the general model.
602 Examples
603 --------
604 >>> RGB_a = np.array([7.8463202, 7.84711528, 7.84899595])
605 >>> F_L = 1.16754446415
606 >>> f_e_inverse(RGB_a, F_L)
607 ... # doctest: +ELLIPSIS
608 array([ 19.9969397..., 20.0018612..., 20.0135052...])
609 """
611 RGB_a = as_float_array(RGB_a)
612 F_L = as_float_array(F_L)
613 q_L, q_U = 0.26, 150
615 f_q_F_L_q_U = f_q(F_L, q_U)[..., None]
616 f_q_F_L_q_L = f_q(F_L, q_L)[..., None]
617 d_f_q_F_L_q_U = d_f_q(F_L, q_U)[..., None]
619 return np.select(
620 [
621 RGB_a > f_q_F_L_q_U,
622 np.logical_and(f_q_F_L_q_L <= RGB_a, RGB_a <= f_q_F_L_q_U),
623 RGB_a < f_q_F_L_q_L,
624 ],
625 [
626 q_U + (RGB_a - f_q_F_L_q_U) / d_f_q_F_L_q_U,
627 100 / F_L[..., None] * spow((27.13 * RGB_a) / (400 - RGB_a), 1 / 0.42),
628 q_L * RGB_a / f_q_F_L_q_L,
629 ],
630 )
633def f_q(F_L: ArrayLike, q: ArrayLike) -> NDArrayFloat:
634 """
635 Evaluate the :math:`f(q)` function for chromatic adaptation.
637 Parameters
638 ----------
639 F_L
640 *Luminance* level adaptation factor :math:`F_L`.
641 q
642 :math:`q` parameter.
644 Returns
645 -------
646 :class:`numpy.ndarray`
647 Evaluated :math:`f(q)` function result.
649 Examples
650 --------
651 >>> f_q(1.17, 0.26) # doctest: +ELLIPSIS
652 1.2886520...
653 """
655 F_L = as_float_array(F_L)
656 q = as_float_array(q)
658 F_L_q_100 = spow((F_L * q) / 100, 0.42)
660 return (400 * F_L_q_100) / (27.13 + F_L_q_100)
663def d_f_q(F_L: ArrayLike, q: ArrayLike) -> NDArrayFloat:
664 """
665 Compute the :math:`f'(q)` function derivative.
667 Parameters
668 ----------
669 F_L
670 *Luminance* level adaptation factor :math:`F_L`.
671 q
672 :math:`q` parameter.
674 Returns
675 -------
676 :class:`numpy.ndarray`
677 Evaluated :math:`f'(q)` function derivative.
679 Examples
680 --------
681 >>> d_f_q(1.17, 0.26) # doctest: +ELLIPSIS
682 2.0749623...
683 """
685 F_L = as_float_array(F_L)
686 q = as_float_array(q)
688 F_L_q_100 = (F_L * q) / 100
690 return (1.68 * 27.13 * F_L * spow(F_L_q_100, -0.58)) / (
691 27.13 + spow(F_L_q_100, 0.42)
692 ) ** 2