[v5,02/16] test: libipa: Add tests for Quantized types
diff mbox series

Message ID 20260114173918.1744023-3-kieran.bingham@ideasonboard.com
State Superseded
Headers show
Series
  • libipa: Introduce a Quantized type
Related show

Commit Message

Kieran Bingham Jan. 14, 2026, 5:39 p.m. UTC
Provide use case tests for the Quantized types to ensure construction
and usages are consistent and work as expected.

Reviewed-by: Isaac Scott <isaac.scott@ideasonboard.com>
Signed-off-by: Kieran Bingham <kieran.bingham@ideasonboard.com>

---
v3:
- Rename quantized_type to QuantizedType

v5:
- use static asserts for constructible failure tests
- Remove constexpr from lround users (only possible in C++23)
- Remove move tests
- Fix up inequality test

Signed-off-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
---
 test/ipa/libipa/meson.build   |   1 +
 test/ipa/libipa/quantized.cpp | 124 ++++++++++++++++++++++++++++++++++
 2 files changed, 125 insertions(+)
 create mode 100644 test/ipa/libipa/quantized.cpp

Comments

Barnabás Pőcze Jan. 15, 2026, 3:49 p.m. UTC | #1
2026. 01. 14. 18:39 keltezéssel, Kieran Bingham írta:
> Provide use case tests for the Quantized types to ensure construction
> and usages are consistent and work as expected.
> 
> Reviewed-by: Isaac Scott <isaac.scott@ideasonboard.com>
> Signed-off-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
> 
> ---
> v3:
> - Rename quantized_type to QuantizedType
> 
> v5:
> - use static asserts for constructible failure tests
> - Remove constexpr from lround users (only possible in C++23)
> - Remove move tests
> - Fix up inequality test
> 
> Signed-off-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
> ---
>   test/ipa/libipa/meson.build   |   1 +
>   test/ipa/libipa/quantized.cpp | 124 ++++++++++++++++++++++++++++++++++
>   2 files changed, 125 insertions(+)
>   create mode 100644 test/ipa/libipa/quantized.cpp
> 
> diff --git a/test/ipa/libipa/meson.build b/test/ipa/libipa/meson.build
> index 2070bed70222..c3e255871f4f 100644
> --- a/test/ipa/libipa/meson.build
> +++ b/test/ipa/libipa/meson.build
> @@ -5,6 +5,7 @@ libipa_test = [
>       {'name': 'histogram', 'sources': ['histogram.cpp']},
>       {'name': 'interpolator', 'sources': ['interpolator.cpp']},
>       {'name': 'pwl', 'sources': ['pwl.cpp'] },
> +    {'name': 'quantized', 'sources': ['quantized.cpp']},
>   ]
>   
>   foreach test : libipa_test
> diff --git a/test/ipa/libipa/quantized.cpp b/test/ipa/libipa/quantized.cpp
> new file mode 100644
> index 000000000000..6b6a61311125
> --- /dev/null
> +++ b/test/ipa/libipa/quantized.cpp
> @@ -0,0 +1,124 @@
> +/* SPDX-License-Identifier: GPL-2.0-or-later */
> +/*
> + * Copyright (C) 2025, Ideas on Board
> + *
> + * Dual Type and Quantizer tests
> + */
> +
> +#include "../src/ipa/libipa/quantized.h"
> +
> +#include <algorithm>
> +#include <cmath>
> +#include <iostream>
> +#include <map>
> +#include <stdint.h>
> +
> +#include "test.h"
> +
> +using namespace std;
> +using namespace libcamera;
> +using namespace ipa;
> +
> +struct BrightnessHueTraits {
> +	using QuantizedType = int8_t;
> +	static QuantizedType fromFloat(float v)
> +	{
> +		int quantized = std::lround(v * 128.0f);
> +		return static_cast<int8_t>(std::clamp<int>(quantized, -128, 127));

I don't think there is a need for the static cast. (Same later.)


> +	}
> +	static float toFloat(QuantizedType v)
> +	{
> +		return static_cast<float>(v) / 128.0f;
> +	}
> +};
> +
> +using BrightnessHueQuantizer = Quantized<BrightnessHueTraits>;
> +
> +struct ContrastSaturationTraits {
> +	using QuantizedType = uint8_t;
> +	static QuantizedType fromFloat(float v)
> +	{
> +		int quantized = std::lround(v * 128.0f);
> +		return static_cast<uint8_t>(std::clamp<int>(quantized, 0, 255));
> +	}
> +	static float toFloat(QuantizedType v)
> +	{
> +		return static_cast<float>(v) / 128.0f;
> +	}
> +};
> +
> +using ContrastSaturationQuantizer = Quantized<ContrastSaturationTraits>;
> +
> +using BrightnessQ = BrightnessHueQuantizer;
> +using HueQ = BrightnessHueQuantizer;
> +using ContrastQ = ContrastSaturationQuantizer;
> +using SaturationQ = ContrastSaturationQuantizer;
> +
> +class QuantizedTest : public Test
> +{
> +protected:
> +	int run()
> +	{
> +		/* Test construction from float */
> +		{
> +			BrightnessQ b(0.5f);
> +			if (b.quantized() != 64 || std::abs(b.value() - 0.5f) > 0.01f)
> +				return TestFail;
> +		}
> +
> +		/* Test construction from T */
> +		{
> +			ContrastQ c(uint8_t(128));
> +			if (c.quantized() != 128 || std::abs(c.value() - 1.0f) > 0.01f)
> +				return TestFail;
> +		}
> +
> +		/*
> +		 * Only construction from the exact storage type or a float
> +		 * is permitted.
> +		 */
> +		static_assert(!std::is_constructible_v<BrightnessQ, unsigned int>);
> +		static_assert(!std::is_constructible_v<BrightnessQ, uint8_t>);
> +		static_assert(!std::is_constructible_v<BrightnessQ, double>);
> +		static_assert(!std::is_constructible_v<ContrastQ, int>);
> +		static_assert(!std::is_constructible_v<ContrastQ, int8_t>);
> +		static_assert(!std::is_constructible_v<ContrastQ, unsigned int>);
> +
> +		/* Test equality */
> +		{
> +			BrightnessQ b1(0.5f), b2((int8_t)64);

   int8_t(64)

looks better in my opinion.


> +			if (!(b1 == b2))
> +				return TestFail;
> +		}
> +
> +		/* Test inequality */
> +		{
> +			BrightnessQ b1(0.5f), b2(-0.5f);
> +			if (!(b1 != b2))
> +				return TestFail;
> +		}
> +
> +		/* Test copying */
> +		{
> +			BrightnessQ b1(0.25f);
> +			BrightnessQ b2 = b1;
> +			if (!(b1 == b2))
> +				return TestFail;
> +		}
> +
> +		/* Test assignment */
> +		{
> +			ContrastQ c1(1.5f);
> +			ContrastQ c2(0.0f);
> +			c2 = c1;
> +			if (!(c1 == c2))
> +				return TestFail;
> +		}

Another test case that I think would be good to have: construction from two
different float values that map to the same quantized value, but that quantized
value corresponds to a third float value. Then both the quantized and float
values should be tested for equality.



> +
> +		std::cout << "Quantised tests passed successfully." << std::endl;
> +
> +		return TestPass;
> +	}
> +};
> +
> +TEST_REGISTER(QuantizedTest)
Kieran Bingham Jan. 19, 2026, 5:07 p.m. UTC | #2
Quoting Barnabás Pőcze (2026-01-15 15:49:35)
> 2026. 01. 14. 18:39 keltezéssel, Kieran Bingham írta:
> > Provide use case tests for the Quantized types to ensure construction
> > and usages are consistent and work as expected.
> > 
> > Reviewed-by: Isaac Scott <isaac.scott@ideasonboard.com>
> > Signed-off-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
> > 
> > ---
> > v3:
> > - Rename quantized_type to QuantizedType
> > 
> > v5:
> > - use static asserts for constructible failure tests
> > - Remove constexpr from lround users (only possible in C++23)
> > - Remove move tests
> > - Fix up inequality test
> > 
> > Signed-off-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
> > ---
> >   test/ipa/libipa/meson.build   |   1 +
> >   test/ipa/libipa/quantized.cpp | 124 ++++++++++++++++++++++++++++++++++
> >   2 files changed, 125 insertions(+)
> >   create mode 100644 test/ipa/libipa/quantized.cpp
> > 
> > diff --git a/test/ipa/libipa/meson.build b/test/ipa/libipa/meson.build
> > index 2070bed70222..c3e255871f4f 100644
> > --- a/test/ipa/libipa/meson.build
> > +++ b/test/ipa/libipa/meson.build
> > @@ -5,6 +5,7 @@ libipa_test = [
> >       {'name': 'histogram', 'sources': ['histogram.cpp']},
> >       {'name': 'interpolator', 'sources': ['interpolator.cpp']},
> >       {'name': 'pwl', 'sources': ['pwl.cpp'] },
> > +    {'name': 'quantized', 'sources': ['quantized.cpp']},
> >   ]
> >   
> >   foreach test : libipa_test
> > diff --git a/test/ipa/libipa/quantized.cpp b/test/ipa/libipa/quantized.cpp
> > new file mode 100644
> > index 000000000000..6b6a61311125
> > --- /dev/null
> > +++ b/test/ipa/libipa/quantized.cpp
> > @@ -0,0 +1,124 @@
> > +/* SPDX-License-Identifier: GPL-2.0-or-later */
> > +/*
> > + * Copyright (C) 2025, Ideas on Board
> > + *
> > + * Dual Type and Quantizer tests
> > + */
> > +
> > +#include "../src/ipa/libipa/quantized.h"
> > +
> > +#include <algorithm>
> > +#include <cmath>
> > +#include <iostream>
> > +#include <map>
> > +#include <stdint.h>
> > +
> > +#include "test.h"
> > +
> > +using namespace std;
> > +using namespace libcamera;
> > +using namespace ipa;
> > +
> > +struct BrightnessHueTraits {
> > +     using QuantizedType = int8_t;
> > +     static QuantizedType fromFloat(float v)
> > +     {
> > +             int quantized = std::lround(v * 128.0f);
> > +             return static_cast<int8_t>(std::clamp<int>(quantized, -128, 127));
> 
> I don't think there is a need for the static cast. (Same later.)

Removed both.

> > +     }
> > +     static float toFloat(QuantizedType v)
> > +     {
> > +             return static_cast<float>(v) / 128.0f;
> > +     }
> > +};
> > +
> > +using BrightnessHueQuantizer = Quantized<BrightnessHueTraits>;
> > +
> > +struct ContrastSaturationTraits {
> > +     using QuantizedType = uint8_t;
> > +     static QuantizedType fromFloat(float v)
> > +     {
> > +             int quantized = std::lround(v * 128.0f);
> > +             return static_cast<uint8_t>(std::clamp<int>(quantized, 0, 255));
> > +     }
> > +     static float toFloat(QuantizedType v)
> > +     {
> > +             return static_cast<float>(v) / 128.0f;
> > +     }
> > +};
> > +
> > +using ContrastSaturationQuantizer = Quantized<ContrastSaturationTraits>;
> > +
> > +using BrightnessQ = BrightnessHueQuantizer;
> > +using HueQ = BrightnessHueQuantizer;
> > +using ContrastQ = ContrastSaturationQuantizer;
> > +using SaturationQ = ContrastSaturationQuantizer;
> > +
> > +class QuantizedTest : public Test
> > +{
> > +protected:
> > +     int run()
> > +     {
> > +             /* Test construction from float */
> > +             {
> > +                     BrightnessQ b(0.5f);
> > +                     if (b.quantized() != 64 || std::abs(b.value() - 0.5f) > 0.01f)
> > +                             return TestFail;
> > +             }
> > +
> > +             /* Test construction from T */
> > +             {
> > +                     ContrastQ c(uint8_t(128));
> > +                     if (c.quantized() != 128 || std::abs(c.value() - 1.0f) > 0.01f)
> > +                             return TestFail;
> > +             }
> > +
> > +             /*
> > +              * Only construction from the exact storage type or a float
> > +              * is permitted.
> > +              */
> > +             static_assert(!std::is_constructible_v<BrightnessQ, unsigned int>);
> > +             static_assert(!std::is_constructible_v<BrightnessQ, uint8_t>);
> > +             static_assert(!std::is_constructible_v<BrightnessQ, double>);
> > +             static_assert(!std::is_constructible_v<ContrastQ, int>);
> > +             static_assert(!std::is_constructible_v<ContrastQ, int8_t>);
> > +             static_assert(!std::is_constructible_v<ContrastQ, unsigned int>);
> > +
> > +             /* Test equality */
> > +             {
> > +                     BrightnessQ b1(0.5f), b2((int8_t)64);
> 
>    int8_t(64)
> 
> looks better in my opinion.

Fixed.

> 
> 
> > +                     if (!(b1 == b2))
> > +                             return TestFail;
> > +             }
> > +
> > +             /* Test inequality */
> > +             {
> > +                     BrightnessQ b1(0.5f), b2(-0.5f);
> > +                     if (!(b1 != b2))
> > +                             return TestFail;
> > +             }
> > +
> > +             /* Test copying */
> > +             {
> > +                     BrightnessQ b1(0.25f);
> > +                     BrightnessQ b2 = b1;
> > +                     if (!(b1 == b2))
> > +                             return TestFail;
> > +             }
> > +
> > +             /* Test assignment */
> > +             {
> > +                     ContrastQ c1(1.5f);
> > +                     ContrastQ c2(0.0f);
> > +                     c2 = c1;
> > +                     if (!(c1 == c2))
> > +                             return TestFail;
> > +             }
> 
> Another test case that I think would be good to have: construction from two
> different float values that map to the same quantized value, but that quantized
> value corresponds to a third float value. Then both the quantized and float
> values should be tested for equality.

Is this what you had in mind?

		/*
		 * Test construction from different floats mapping to same
		 * quantized value
		 */
		{
			/* Two floats that have the same quantized value. */
			const float f1 = 1.007f;
			const float f2 = 1.008f;

			ContrastQ c1(f1);
			ContrastQ c2(f2);

			std::cout << "  Checking " << f1 << " and " << f2
				  << " both map to " << c1 << std::endl;
			std::cout << "    c1: " << c1 << std::endl;
			std::cout << "    c2: " << c2 << std::endl;

			/* Quantized values must match */
			if (!(c1.quantized() == c2.quantized()))
				return TestFail;

			/* Float values must now match */
			if (!(c1.value() == c2.value()))
				return TestFail;

			if (!(c1 == c2))
				return TestFail;
		}


test/ipa/libipa/quantized
  Checking 1.007 and 1.008 both map to [0x81:1.00781]
    c1: [0x81:1.00781]
    c2: [0x81:1.00781]
Quantised tests passed successfully.

(I'll remove the Checking prints for the patch)

--
Kieran


> 
> 
> 
> > +
> > +             std::cout << "Quantised tests passed successfully." << std::endl;
> > +
> > +             return TestPass;
> > +     }
> > +};
> > +
> > +TEST_REGISTER(QuantizedTest)
>
Barnabás Pőcze Jan. 20, 2026, 1:27 p.m. UTC | #3
2026. 01. 19. 18:07 keltezéssel, Kieran Bingham írta:
> Quoting Barnabás Pőcze (2026-01-15 15:49:35)
>> 2026. 01. 14. 18:39 keltezéssel, Kieran Bingham írta:
>>> Provide use case tests for the Quantized types to ensure construction
>>> and usages are consistent and work as expected.
>>>
>>> Reviewed-by: Isaac Scott <isaac.scott@ideasonboard.com>
>>> Signed-off-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
>>>
>>> ---
>>> v3:
>>> - Rename quantized_type to QuantizedType
>>>
>>> v5:
>>> - use static asserts for constructible failure tests
>>> - Remove constexpr from lround users (only possible in C++23)
>>> - Remove move tests
>>> - Fix up inequality test
>>>
>>> Signed-off-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
>>> ---
>>>    test/ipa/libipa/meson.build   |   1 +
>>>    test/ipa/libipa/quantized.cpp | 124 ++++++++++++++++++++++++++++++++++
>>>    2 files changed, 125 insertions(+)
>>>    create mode 100644 test/ipa/libipa/quantized.cpp
>>>
>>> diff --git a/test/ipa/libipa/meson.build b/test/ipa/libipa/meson.build
>>> index 2070bed70222..c3e255871f4f 100644
>>> --- a/test/ipa/libipa/meson.build
>>> +++ b/test/ipa/libipa/meson.build
>>> @@ -5,6 +5,7 @@ libipa_test = [
>>>        {'name': 'histogram', 'sources': ['histogram.cpp']},
>>>        {'name': 'interpolator', 'sources': ['interpolator.cpp']},
>>>        {'name': 'pwl', 'sources': ['pwl.cpp'] },
>>> +    {'name': 'quantized', 'sources': ['quantized.cpp']},
>>>    ]
>>>
>>>    foreach test : libipa_test
>>> diff --git a/test/ipa/libipa/quantized.cpp b/test/ipa/libipa/quantized.cpp
>>> new file mode 100644
>>> index 000000000000..6b6a61311125
>>> --- /dev/null
>>> +++ b/test/ipa/libipa/quantized.cpp
>>> @@ -0,0 +1,124 @@
>>> +/* SPDX-License-Identifier: GPL-2.0-or-later */
>>> +/*
>>> + * Copyright (C) 2025, Ideas on Board
>>> + *
>>> + * Dual Type and Quantizer tests
>>> + */
>>> +
>>> +#include "../src/ipa/libipa/quantized.h"
>>> +
>>> +#include <algorithm>
>>> +#include <cmath>
>>> +#include <iostream>
>>> +#include <map>
>>> +#include <stdint.h>
>>> +
>>> +#include "test.h"
>>> +
>>> +using namespace std;
>>> +using namespace libcamera;
>>> +using namespace ipa;
>>> +
>>> +struct BrightnessHueTraits {
>>> +     using QuantizedType = int8_t;
>>> +     static QuantizedType fromFloat(float v)
>>> +     {
>>> +             int quantized = std::lround(v * 128.0f);
>>> +             return static_cast<int8_t>(std::clamp<int>(quantized, -128, 127));
>>
>> I don't think there is a need for the static cast. (Same later.)
> 
> Removed both.
> 
>>> +     }
>>> +     static float toFloat(QuantizedType v)
>>> +     {
>>> +             return static_cast<float>(v) / 128.0f;
>>> +     }
>>> +};
>>> +
>>> +using BrightnessHueQuantizer = Quantized<BrightnessHueTraits>;
>>> +
>>> +struct ContrastSaturationTraits {
>>> +     using QuantizedType = uint8_t;
>>> +     static QuantizedType fromFloat(float v)
>>> +     {
>>> +             int quantized = std::lround(v * 128.0f);
>>> +             return static_cast<uint8_t>(std::clamp<int>(quantized, 0, 255));
>>> +     }
>>> +     static float toFloat(QuantizedType v)
>>> +     {
>>> +             return static_cast<float>(v) / 128.0f;
>>> +     }
>>> +};
>>> +
>>> +using ContrastSaturationQuantizer = Quantized<ContrastSaturationTraits>;
>>> +
>>> +using BrightnessQ = BrightnessHueQuantizer;
>>> +using HueQ = BrightnessHueQuantizer;
>>> +using ContrastQ = ContrastSaturationQuantizer;
>>> +using SaturationQ = ContrastSaturationQuantizer;
>>> +
>>> +class QuantizedTest : public Test
>>> +{
>>> +protected:
>>> +     int run()
>>> +     {
>>> +             /* Test construction from float */
>>> +             {
>>> +                     BrightnessQ b(0.5f);
>>> +                     if (b.quantized() != 64 || std::abs(b.value() - 0.5f) > 0.01f)
>>> +                             return TestFail;
>>> +             }
>>> +
>>> +             /* Test construction from T */
>>> +             {
>>> +                     ContrastQ c(uint8_t(128));
>>> +                     if (c.quantized() != 128 || std::abs(c.value() - 1.0f) > 0.01f)
>>> +                             return TestFail;
>>> +             }
>>> +
>>> +             /*
>>> +              * Only construction from the exact storage type or a float
>>> +              * is permitted.
>>> +              */
>>> +             static_assert(!std::is_constructible_v<BrightnessQ, unsigned int>);
>>> +             static_assert(!std::is_constructible_v<BrightnessQ, uint8_t>);
>>> +             static_assert(!std::is_constructible_v<BrightnessQ, double>);
>>> +             static_assert(!std::is_constructible_v<ContrastQ, int>);
>>> +             static_assert(!std::is_constructible_v<ContrastQ, int8_t>);
>>> +             static_assert(!std::is_constructible_v<ContrastQ, unsigned int>);
>>> +
>>> +             /* Test equality */
>>> +             {
>>> +                     BrightnessQ b1(0.5f), b2((int8_t)64);
>>
>>     int8_t(64)
>>
>> looks better in my opinion.
> 
> Fixed.
> 
>>
>>
>>> +                     if (!(b1 == b2))
>>> +                             return TestFail;
>>> +             }
>>> +
>>> +             /* Test inequality */
>>> +             {
>>> +                     BrightnessQ b1(0.5f), b2(-0.5f);
>>> +                     if (!(b1 != b2))
>>> +                             return TestFail;
>>> +             }
>>> +
>>> +             /* Test copying */
>>> +             {
>>> +                     BrightnessQ b1(0.25f);
>>> +                     BrightnessQ b2 = b1;
>>> +                     if (!(b1 == b2))
>>> +                             return TestFail;
>>> +             }
>>> +
>>> +             /* Test assignment */
>>> +             {
>>> +                     ContrastQ c1(1.5f);
>>> +                     ContrastQ c2(0.0f);
>>> +                     c2 = c1;
>>> +                     if (!(c1 == c2))
>>> +                             return TestFail;
>>> +             }
>>
>> Another test case that I think would be good to have: construction from two
>> different float values that map to the same quantized value, but that quantized
>> value corresponds to a third float value. Then both the quantized and float
>> values should be tested for equality.
> 
> Is this what you had in mind?

Yes, something like this, thanks.


> 
> 		/*
> 		 * Test construction from different floats mapping to same
> 		 * quantized value
> 		 */
> 		{
> 			/* Two floats that have the same quantized value. */
> 			const float f1 = 1.007f;
> 			const float f2 = 1.008f;
> 
> 			ContrastQ c1(f1);
> 			ContrastQ c2(f2);
> 
> 			std::cout << "  Checking " << f1 << " and " << f2
> 				  << " both map to " << c1 << std::endl;
> 			std::cout << "    c1: " << c1 << std::endl;
> 			std::cout << "    c2: " << c2 << std::endl;
> 
> 			/* Quantized values must match */
> 			if (!(c1.quantized() == c2.quantized()))
> 				return TestFail;
> 
> 			/* Float values must now match */
> 			if (!(c1.value() == c2.value()))
> 				return TestFail;
> 
> 			if (!(c1 == c2))
> 				return TestFail;
> 		}
> 
> 
> test/ipa/libipa/quantized
>    Checking 1.007 and 1.008 both map to [0x81:1.00781]
>      c1: [0x81:1.00781]
>      c2: [0x81:1.00781]
> Quantised tests passed successfully.
> 
> (I'll remove the Checking prints for the patch)
> 
> --
> Kieran
> 
> 
>>
>>
>>
>>> +
>>> +             std::cout << "Quantised tests passed successfully." << std::endl;
>>> +
>>> +             return TestPass;
>>> +     }
>>> +};
>>> +
>>> +TEST_REGISTER(QuantizedTest)
>>

Patch
diff mbox series

diff --git a/test/ipa/libipa/meson.build b/test/ipa/libipa/meson.build
index 2070bed70222..c3e255871f4f 100644
--- a/test/ipa/libipa/meson.build
+++ b/test/ipa/libipa/meson.build
@@ -5,6 +5,7 @@  libipa_test = [
     {'name': 'histogram', 'sources': ['histogram.cpp']},
     {'name': 'interpolator', 'sources': ['interpolator.cpp']},
     {'name': 'pwl', 'sources': ['pwl.cpp'] },
+    {'name': 'quantized', 'sources': ['quantized.cpp']},
 ]
 
 foreach test : libipa_test
diff --git a/test/ipa/libipa/quantized.cpp b/test/ipa/libipa/quantized.cpp
new file mode 100644
index 000000000000..6b6a61311125
--- /dev/null
+++ b/test/ipa/libipa/quantized.cpp
@@ -0,0 +1,124 @@ 
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2025, Ideas on Board
+ *
+ * Dual Type and Quantizer tests
+ */
+
+#include "../src/ipa/libipa/quantized.h"
+
+#include <algorithm>
+#include <cmath>
+#include <iostream>
+#include <map>
+#include <stdint.h>
+
+#include "test.h"
+
+using namespace std;
+using namespace libcamera;
+using namespace ipa;
+
+struct BrightnessHueTraits {
+	using QuantizedType = int8_t;
+	static QuantizedType fromFloat(float v)
+	{
+		int quantized = std::lround(v * 128.0f);
+		return static_cast<int8_t>(std::clamp<int>(quantized, -128, 127));
+	}
+	static float toFloat(QuantizedType v)
+	{
+		return static_cast<float>(v) / 128.0f;
+	}
+};
+
+using BrightnessHueQuantizer = Quantized<BrightnessHueTraits>;
+
+struct ContrastSaturationTraits {
+	using QuantizedType = uint8_t;
+	static QuantizedType fromFloat(float v)
+	{
+		int quantized = std::lround(v * 128.0f);
+		return static_cast<uint8_t>(std::clamp<int>(quantized, 0, 255));
+	}
+	static float toFloat(QuantizedType v)
+	{
+		return static_cast<float>(v) / 128.0f;
+	}
+};
+
+using ContrastSaturationQuantizer = Quantized<ContrastSaturationTraits>;
+
+using BrightnessQ = BrightnessHueQuantizer;
+using HueQ = BrightnessHueQuantizer;
+using ContrastQ = ContrastSaturationQuantizer;
+using SaturationQ = ContrastSaturationQuantizer;
+
+class QuantizedTest : public Test
+{
+protected:
+	int run()
+	{
+		/* Test construction from float */
+		{
+			BrightnessQ b(0.5f);
+			if (b.quantized() != 64 || std::abs(b.value() - 0.5f) > 0.01f)
+				return TestFail;
+		}
+
+		/* Test construction from T */
+		{
+			ContrastQ c(uint8_t(128));
+			if (c.quantized() != 128 || std::abs(c.value() - 1.0f) > 0.01f)
+				return TestFail;
+		}
+
+		/*
+		 * Only construction from the exact storage type or a float
+		 * is permitted.
+		 */
+		static_assert(!std::is_constructible_v<BrightnessQ, unsigned int>);
+		static_assert(!std::is_constructible_v<BrightnessQ, uint8_t>);
+		static_assert(!std::is_constructible_v<BrightnessQ, double>);
+		static_assert(!std::is_constructible_v<ContrastQ, int>);
+		static_assert(!std::is_constructible_v<ContrastQ, int8_t>);
+		static_assert(!std::is_constructible_v<ContrastQ, unsigned int>);
+
+		/* Test equality */
+		{
+			BrightnessQ b1(0.5f), b2((int8_t)64);
+			if (!(b1 == b2))
+				return TestFail;
+		}
+
+		/* Test inequality */
+		{
+			BrightnessQ b1(0.5f), b2(-0.5f);
+			if (!(b1 != b2))
+				return TestFail;
+		}
+
+		/* Test copying */
+		{
+			BrightnessQ b1(0.25f);
+			BrightnessQ b2 = b1;
+			if (!(b1 == b2))
+				return TestFail;
+		}
+
+		/* Test assignment */
+		{
+			ContrastQ c1(1.5f);
+			ContrastQ c2(0.0f);
+			c2 = c1;
+			if (!(c1 == c2))
+				return TestFail;
+		}
+
+		std::cout << "Quantised tests passed successfully." << std::endl;
+
+		return TestPass;
+	}
+};
+
+TEST_REGISTER(QuantizedTest)