diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a076a4a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,98 @@ +# Project Guidelines for Claude + +## Project Overview + +### Phase 1 +I would like to add more unit testing to the EbsdLib library. Can we put together a TODO.md file stored in the "Code_Review" directory that lists all the candidate classes to create unit tests for? + +### Phase 2 +I would also like to write documentation for this library + +### Phase 3 +I would also like to write more example code that shows how to use the library + +How should we get stated with these tasks? + +## Directory Structure +- `Source/Ebsdlib/` - Core library +- `Source/Apps/` - various test applications +- `src/Test/` - Test files +- `cmake/` - CMake configuration + + +## Directories to Ignore +- `scripts/` - Build/utility scripts +- `conda/` - Conda packaging + +## Coding Standards + +### C++ Style (from .clang-format) +- C++20 standard +- Allman brace style (braces on new lines for classes, control statements, enums, functions, namespaces, structs, before else) +- 200 column limit +- 2-space indentation, no tabs +- Pointer alignment left (`int* ptr` not `int *ptr`) +- No space before parentheses +- Sort includes alphabetically +- No short functions on single line +- Always break template declarations +- Constructor initializers break before comma + +### Naming Conventions (from .clang-tidy) +- C++ header files: `.hpp` extension +- C++ source files: `.cpp` extension +- Namespaces: `lower_case` +- Classes: `CamelCase` +- Structs: `CamelCase` +- Class methods: `camelBack` +- Functions: `camelBack` +- Variables: `camelBack` +- Private members: `m_` prefix + `CamelCase` (e.g., `m_MemberVariable`) +- Global variables: `CamelCase` +- Global constants: `k_` prefix + `CamelCase` (e.g., `k_DefaultValue`) +- Local pointers: `camelBack` + `Ptr` suffix (e.g., `dataPtr`) +- Type aliases: `CamelCase` + `Type` suffix (e.g., `ValueType`) +- Macros: `UPPER_CASE` + + +## Build System +- vcpkg for dependency management +- CMake-based build system + +Example configuring the project +```bash +cd /Users/mjackson/Workspace1/EbsdLib && cmake --preset EbsdLib-Release +``` + +- Build directory is located at "/Users/mjackson/Workspace1/DREAM3D-Build/EbsdLib-Release" + +Example building the project +```bash +cd /Users/mjackson/Workspace5/DREAM3D-Build/EbsdLib-Release && cmake --build . --target all +``` + +- Python anaconda environment 'dream3d' can be used if needed + +## Testing +- Unit tests use the Catch2 framework. +- Use the `ctest` to run unit tests + +### Running Unit Tests +- Always use `ctest` to run unit tests, NOT the test binary directly +- The `ctest` command handles test data extraction and cleanup automatically +- Use the `-R` flag to run specific tests by name pattern + +Example - Running a specific test: +```bash +cd /Users/mjackson/Workspace5/DREAM3D-Build/EbsdLib-Release && ctest -R "EbsdLib::FillBadData" --verbose +``` + +Example - Running all SimplnxCore tests: +```bash +cd /Users/mjackson/Workspace5/DREAM3D-Build/EbsdLib-Release && ctest -R "EbsdLib::" --verbose +``` + +### Printing debug statements in unit tests + +## Additional Notes + diff --git a/Code_Review/TODO.md b/Code_Review/TODO.md index cca0d35..191dbcc 100644 --- a/Code_Review/TODO.md +++ b/Code_Review/TODO.md @@ -59,6 +59,12 @@ There are **12 active test files** compiled into a single `EbsdLibUnitTest` exec **Severity:** MEDIUM - Tests pass even with wildly incorrect values **Issue:** The tolerance check uses `delta < 1.0E6` (one million) instead of `1.0E-6` (one millionth). This means the round-trip conversion tests will pass even if values are off by up to a million, making the tests effectively useless for catching conversion errors. +### Bug 5: `EbsdDataArray::eraseTuples()` - Does not update `m_NumTuples` + +**File:** `Source/EbsdLib/Core/EbsdDataArray.cpp:678` +**Severity:** MEDIUM - `getNumberOfTuples()` returns stale value after eraseTuples +**Issue:** The `eraseTuples()` method updates `m_Size`, `m_MaxId`, and `m_Array` but never updates `m_NumTuples`. After calling `eraseTuples()`, `getNumberOfTuples()` returns the original tuple count instead of the new (reduced) count. `getSize()` correctly returns the new total element count. + --- ## Classes Requiring Unit Tests @@ -70,7 +76,7 @@ These classes have zero or near-zero test coverage and contain non-trivial logic #### 1. `EbsdDataArray` - Core data container - **File:** `Source/EbsdLib/Core/EbsdDataArray.hpp` -- **Status:** No tests +- **Status:** **DONE** - `Source/Test/EbsdDataArrayTest.cpp` (23 test cases) - **Why:** Core template class used by virtually every reader and computation. Wraps raw arrays with lifecycle management. - **Recommended tests:** - Construction (default, sized, from existing pointer) @@ -85,7 +91,7 @@ These classes have zero or near-zero test coverage and contain non-trivial logic #### 2. `Matrix3X1` - 3x1 vector operations - **File:** `Source/EbsdLib/Math/Matrix3X1.hpp` -- **Status:** Only `cosTheta` tested (in QuaternionTest) +- **Status:** **DONE** - `Source/Test/Matrix3X1Test.cpp` (20 test cases) - **Why:** Core math class with known bugs (see Bugs #1 and #2) - **Recommended tests:** - Construction and element access @@ -99,7 +105,7 @@ These classes have zero or near-zero test coverage and contain non-trivial logic #### 3. `Matrix3X3` - 3x3 matrix operations - **File:** `Source/EbsdLib/Math/Matrix3X3.hpp` -- **Status:** Exercised in TextureTest but no assertions on results +- **Status:** **DONE** - `Source/Test/Matrix3X3Test.cpp` (23 test cases) - **Why:** Used in orientation math, coordinate transforms; currently only smoke-tested - **Recommended tests:** - Construction and element access @@ -145,7 +151,7 @@ These classes have zero or near-zero test coverage and contain non-trivial logic #### 7. `OrientationMath` - Crystallographic math - **File:** `Source/EbsdLib/Core/OrientationMath.h` -- **Status:** No direct tests (indirectly exercised by ConvertToFundamentalZoneTest) +- **Status:** **DONE** - `Source/Test/OrientationMathTest.cpp` (12 test cases) - **Why:** Static methods for misorientation calculations, widely used - **Recommended tests:** - `axisAngletoMatrix()`, `quatsToMatrix()` @@ -156,7 +162,7 @@ These classes have zero or near-zero test coverage and contain non-trivial logic #### 8. `EbsdStringUtils` - String utilities - **File:** `Source/EbsdLib/Utilities/EbsdStringUtils.hpp` -- **Status:** No tests +- **Status:** **DONE** - `Source/Test/EbsdStringUtilsTest.cpp` (18 test cases) - **Why:** String parsing utilities used by all readers - **Recommended tests:** - `split()`, `tokenize()` with various delimiters @@ -168,7 +174,7 @@ These classes have zero or near-zero test coverage and contain non-trivial logic #### 9. `EbsdTransform` - Reference frame transformations - **File:** `Source/EbsdLib/Core/EbsdTransform.h` -- **Status:** No tests +- **Status:** **DONE** - `Source/Test/EbsdTransformTest.cpp` (6 test cases) - **Why:** Transforms sample and Euler reference frames; errors here corrupt all downstream analysis - **Recommended tests:** - Sample reference frame transformations (all axis combinations) @@ -179,7 +185,7 @@ These classes have zero or near-zero test coverage and contain non-trivial logic #### 10. `EbsdLibRandom` - PRNG - **File:** `Source/EbsdLib/Math/EbsdLibRandom.h` -- **Status:** No tests +- **Status:** **DONE** - `Source/Test/EbsdLibRandomTest.cpp` (9 test cases) - **Why:** Mersenne Twister wrapper; used for texture generation - **Recommended tests:** - Seeded deterministic output verification @@ -189,7 +195,7 @@ These classes have zero or near-zero test coverage and contain non-trivial logic #### 11. `ArrayHelpers` - Template math helpers - **File:** `Source/EbsdLib/Math/ArrayHelpers.hpp` -- **Status:** No tests +- **Status:** **DONE** - `Source/Test/ArrayHelpersTest.cpp` (13 test cases) - **Why:** Static utility methods used in orientation conversions - **Recommended tests:** - `splat()`, `multiply()`, `scalarMultiply()` @@ -213,43 +219,43 @@ These classes have zero or near-zero test coverage and contain non-trivial logic #### 13. `ColorTable` / `ColorUtilities` - Color handling - **Files:** `Source/EbsdLib/Utilities/ColorTable.h`, `Source/EbsdLib/Utilities/ColorUtilities.h` -- **Status:** No direct tests +- **Status:** **DONE** - `Source/Test/ColorTableTest.cpp` (11 test cases) - **Recommended tests:** `RgbColor` helpers, HSV-to-RGB conversion, color component extraction #### 14. `LambertUtilities` - Square-to-sphere mapping - **File:** `Source/EbsdLib/Utilities/LambertUtilities.h` -- **Status:** No tests +- **Status:** **DONE** - `Source/Test/LambertUtilitiesTest.cpp` (4 test cases) - **Recommended tests:** Square-to-sphere and sphere-to-square conversions, round-trip consistency, boundary values #### 15. `ModifiedLambertProjection` - Lambert projection - **File:** `Source/EbsdLib/Utilities/ModifiedLambertProjection.h` -- **Status:** No tests +- **Status:** **DONE** - `Source/Test/ModifiedLambertProjectionTest.cpp` (7 test cases) - **Recommended tests:** North/south hemisphere projection, `addInterpolatedValues()`, normalization #### 16. `ComputeStereographicProjection` - Stereographic projection utilities - **File:** `Source/EbsdLib/Utilities/ComputeStereographicProjection.h` -- **Status:** No tests +- **Status:** **DONE** - `Source/Test/StereographicProjectionTest.cpp` (6 test cases) - **Recommended tests:** Stereographic-to-spherical and spherical-to-stereographic round-trips #### 17. `TexturePreset` - Texture presets - **File:** `Source/EbsdLib/Texture/TexturePreset.h` -- **Status:** No tests +- **Status:** **DONE** - `Source/Test/TexturePresetTest.cpp` (5 test cases) - **Recommended tests:** Preset value getters, preset registration #### 18. `AngPhase` / `CtfPhase` / `EspritPhase` - Phase data classes - **Files:** `Source/EbsdLib/IO/TSL/AngPhase.h`, `Source/EbsdLib/IO/HKL/CtfPhase.h`, `Source/EbsdLib/IO/BrukerNano/EspritPhase.h` -- **Status:** CtfPhase partially tested via CtfReaderTest; others untested +- **Status:** **DONE** - `Source/Test/PhaseTest.cpp` (18 test cases) - **Recommended tests:** Construction, getter/setter verification, lattice constant parsing #### 19. LaueOps subclasses - Enhanced symmetry tests - **Files:** `Source/EbsdLib/LaueOps/*.h` (11 subclasses) -- **Status:** FZ tests comprehensive; IPF generation and misorientation calculation only smoke-tested +- **Status:** **DONE** - `Source/Test/LaueOpsTest.cpp` (11 test cases) - **Recommended tests:** - `getNumSymOps()` returns expected count for each crystal system - `getIPFColor()` with known orientations against reference values @@ -259,13 +265,13 @@ These classes have zero or near-zero test coverage and contain non-trivial logic #### 20. `ModifiedLambertProjection3D` - 3D Lambert projection - **File:** `Source/EbsdLib/Utilities/ModifiedLambertProjection3D.hpp` -- **Status:** No tests +- **Status:** **DONE** - `Source/Test/ModifiedLambertProjection3DTest.cpp` (7 test cases) - **Recommended tests:** Cube-to-sphere and sphere-to-cube conversions, edge cases at cube boundaries #### 21. `OrientationTransformation` (namespace) - Conversion functions - **File:** `Source/EbsdLib/Core/OrientationTransformation.hpp` -- **Status:** Well-tested via OrientationTest.cpp (round-trip), but not all individual functions are directly tested +- **Status:** **DONE** - `Source/Test/OrientationTransformationTest.cpp` (11 test cases) - **Recommended tests:** Direct tests of each `xx2yy()` function with known analytical values (supplement existing round-trip tests) #### 22. `H5CtfReader` / `H5AngReader` - HDF5 format readers @@ -281,37 +287,37 @@ These classes have zero or near-zero test coverage and contain non-trivial logic #### 23. `ToolTipGenerator` - HTML tooltip builder - **File:** `Source/EbsdLib/Utilities/ToolTipGenerator.h` -- **Status:** No tests +- **Status:** **DONE** - `Source/Test/ToolTipGeneratorTest.cpp` (8 test cases). Also fixed bug where `generateHTML()` and `rowToHTML()` returned empty strings. - **Recommended tests:** `addTitle()`, `addValue()`, output HTML correctness #### 24. `PoleFigureData` - Data holder - **File:** `Source/EbsdLib/Utilities/PoleFigureData.h` -- **Status:** No tests +- **Status:** **DONE** - `Source/Test/PoleFigureDataTest.cpp` (5 test cases) - **Recommended tests:** Construction, getter verification #### 25. `CanvasUtilities` - Visualization helpers - **File:** `Source/EbsdLib/Utilities/CanvasUtilities.hpp` -- **Status:** No tests +- **Status:** **DONE** - `Source/Test/CanvasUtilitiesTest.cpp` (6 test cases) - **Recommended tests:** Only if visual regression testing infrastructure is added #### 26. `ModifiedLambertProjectionArray` - Array variant - **File:** `Source/EbsdLib/Utilities/ModifiedLambertProjectionArray.h` -- **Status:** No tests +- **Status:** **DONE** - `Source/Test/ModifiedLambertProjectionArrayTest.cpp` (16 test cases) - **Recommended tests:** Array construction, element access, resize #### 27. `PoleFigureUtilities` - Pole figure generation - **File:** `Source/EbsdLib/Utilities/PoleFigureUtilities.h` -- **Status:** No tests +- **Status:** **DONE** - `Source/Test/PoleFigureUtilitiesTest.cpp` (4 test cases) - **Recommended tests:** Configuration setup, pole figure generation with known inputs #### 28. `TiffWriter` - TIFF file output - **File:** `Source/EbsdLib/Utilities/TiffWriter.h` -- **Status:** Indirectly tested via IPFLegendTest (only checks return code) +- **Status:** **DONE** - `Source/Test/TiffWriterTest.cpp` (4 test cases) - **Recommended tests:** Write and read-back verification, grayscale and color modes --- diff --git a/Source/EbsdLib/Math/Matrix3X1.hpp b/Source/EbsdLib/Math/Matrix3X1.hpp index 235c4bc..a5c2322 100644 --- a/Source/EbsdLib/Math/Matrix3X1.hpp +++ b/Source/EbsdLib/Math/Matrix3X1.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include diff --git a/Source/EbsdLib/Math/Matrix3X3.hpp b/Source/EbsdLib/Math/Matrix3X3.hpp index 9c830eb..a50087b 100644 --- a/Source/EbsdLib/Math/Matrix3X3.hpp +++ b/Source/EbsdLib/Math/Matrix3X3.hpp @@ -5,6 +5,7 @@ #include +#include #include #include diff --git a/Source/EbsdLib/Utilities/PoleFigureUtilities.h b/Source/EbsdLib/Utilities/PoleFigureUtilities.h index 37e1c35..81f55d9 100644 --- a/Source/EbsdLib/Utilities/PoleFigureUtilities.h +++ b/Source/EbsdLib/Utilities/PoleFigureUtilities.h @@ -148,7 +148,7 @@ class EbsdLib_EXPORT PoleFigureUtilities * @brief The GeneratePoleFigureRgbaImageImpl class is a wrapper around generating the RGBA image (2D UChar Array with 4 Components) from the * intensity image. This should be called from a TBB Task */ -class GeneratePoleFigureRgbaImageImpl +class EbsdLib_EXPORT GeneratePoleFigureRgbaImageImpl { public: GeneratePoleFigureRgbaImageImpl(ebsdlib::DoubleArrayType* intensity, PoleFigureConfiguration_t* config, ebsdlib::UInt8ArrayType* rgba); diff --git a/Source/EbsdLib/Utilities/ToolTipGenerator.cpp b/Source/EbsdLib/Utilities/ToolTipGenerator.cpp index 83d9fa5..c4b6858 100644 --- a/Source/EbsdLib/Utilities/ToolTipGenerator.cpp +++ b/Source/EbsdLib/Utilities/ToolTipGenerator.cpp @@ -122,7 +122,7 @@ std::string ToolTipGenerator::generateHTML() const ss << rowToHTML(spacer); ss << "\n"; ss << ""; - return html; + return ss.str(); } // ----------------------------------------------------------------------------- @@ -144,5 +144,5 @@ std::string ToolTipGenerator::rowToHTML(const RowItem& row) const break; } - return html; + return ss.str(); } diff --git a/Source/Test/ArrayHelpersTest.cpp b/Source/Test/ArrayHelpersTest.cpp new file mode 100644 index 0000000..91401a7 --- /dev/null +++ b/Source/Test/ArrayHelpersTest.cpp @@ -0,0 +1,140 @@ +#include + +#include "EbsdLib/Math/ArrayHelpers.hpp" + +#include +#include + +using VecF = std::vector; +using Helpers = ArrayHelpers; + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ArrayHelpersTest::Splat", "[EbsdLib][ArrayHelpersTest]") +{ + VecF a(5); + Helpers::splat(a, 3.14f); + for(size_t i = 0; i < a.size(); i++) + { + REQUIRE(a[i] == Approx(3.14f)); + } +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ArrayHelpersTest::Multiply", "[EbsdLib][ArrayHelpersTest]") +{ + VecF a = {1.0f, 2.0f, 3.0f}; + VecF b = {4.0f, 5.0f, 6.0f}; + + auto c = Helpers::multiply(a, b); + REQUIRE(c.size() == 3); + REQUIRE(c[0] == Approx(4.0f)); + REQUIRE(c[1] == Approx(10.0f)); + REQUIRE(c[2] == Approx(18.0f)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ArrayHelpersTest::MultiplyWithMax", "[EbsdLib][ArrayHelpersTest]") +{ + VecF a = {1.0f, 2.0f, 3.0f, 4.0f}; + VecF b = {5.0f, 6.0f, 7.0f, 8.0f}; + + auto c = Helpers::multiply(a, b, 2); + REQUIRE(c.size() == 2); + REQUIRE(c[0] == Approx(5.0f)); + REQUIRE(c[1] == Approx(12.0f)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ArrayHelpersTest::ScalarMultiply", "[EbsdLib][ArrayHelpersTest]") +{ + VecF a = {1.0f, 2.0f, 3.0f}; + Helpers::scalarMultiply(a, 2.0f); + REQUIRE(a[0] == Approx(2.0f)); + REQUIRE(a[1] == Approx(4.0f)); + REQUIRE(a[2] == Approx(6.0f)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ArrayHelpersTest::ScalarDivide", "[EbsdLib][ArrayHelpersTest]") +{ + VecF a = {10.0f, 20.0f, 30.0f}; + Helpers::scalarDivide(a, 10.0f); + REQUIRE(a[0] == Approx(1.0f)); + REQUIRE(a[1] == Approx(2.0f)); + REQUIRE(a[2] == Approx(3.0f)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ArrayHelpersTest::Sum", "[EbsdLib][ArrayHelpersTest]") +{ + VecF a = {1.0f, 2.0f, 3.0f, 4.0f}; + REQUIRE(Helpers::sum(a) == Approx(10.0f)); + + // Sum with max + REQUIRE(Helpers::sum(a, 2) == Approx(3.0f)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ArrayHelpersTest::SumOfSquares", "[EbsdLib][ArrayHelpersTest]") +{ + VecF a = {1.0f, 2.0f, 3.0f}; + // 1 + 4 + 9 = 14 + REQUIRE(Helpers::sumofSquares(a) == Approx(14.0f)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ArrayHelpersTest::SqrtSumOfSquares", "[EbsdLib][ArrayHelpersTest]") +{ + VecF a = {3.0f, 4.0f, 0.0f}; + REQUIRE(Helpers::sqrtSumOfSquares(a) == Approx(5.0f)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ArrayHelpersTest::MaxVal", "[EbsdLib][ArrayHelpersTest]") +{ + VecF a = {1.0f, 5.0f, 3.0f, 2.0f}; + REQUIRE(Helpers::maxval(a) == Approx(5.0f)); + + VecF b = {-1.0f, -5.0f, -3.0f}; + REQUIRE(Helpers::maxval(b) == Approx(-1.0f)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ArrayHelpersTest::AbsValue", "[EbsdLib][ArrayHelpersTest]") +{ + VecF a = {-1.0f, 2.0f, -3.0f}; + auto result = Helpers::absValue(a); + REQUIRE(result[0] == Approx(1.0f)); + REQUIRE(result[1] == Approx(2.0f)); + REQUIRE(result[2] == Approx(3.0f)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ArrayHelpersTest::Power", "[EbsdLib][ArrayHelpersTest]") +{ + VecF a = {2.0f, 3.0f, 4.0f}; + Helpers::power(a, 2.0f); + REQUIRE(a[0] == Approx(4.0f)); + REQUIRE(a[1] == Approx(9.0f)); + REQUIRE(a[2] == Approx(16.0f)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ArrayHelpersTest::EmptyArray", "[EbsdLib][ArrayHelpersTest]") +{ + VecF empty; + REQUIRE(Helpers::sum(empty) == Approx(0.0f)); + REQUIRE(Helpers::sumofSquares(empty) == Approx(0.0f)); + REQUIRE(Helpers::sqrtSumOfSquares(empty) == Approx(0.0f)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ArrayHelpersTest::DoubleType", "[EbsdLib][ArrayHelpersTest]") +{ + using VecD = std::vector; + using HelpersD = ArrayHelpers; + + VecD a = {1.0, 2.0, 3.0}; + REQUIRE(HelpersD::sum(a) == Approx(6.0)); + REQUIRE(HelpersD::sumofSquares(a) == Approx(14.0)); +} diff --git a/Source/Test/CMakeLists.txt b/Source/Test/CMakeLists.txt index 4ce5822..737d5cb 100644 --- a/Source/Test/CMakeLists.txt +++ b/Source/Test/CMakeLists.txt @@ -25,6 +25,29 @@ set(EbsdLib_UnitTest_SRCS ${EbsdLibProj_SOURCE_DIR}/Source/Test/ODFTest.cpp ${EbsdLibProj_SOURCE_DIR}/Source/Test/TextureTest.cpp ${EbsdLibProj_SOURCE_DIR}/Source/Test/OrientationConverterTest.cpp + ${EbsdLibProj_SOURCE_DIR}/Source/Test/Matrix3X1Test.cpp + ${EbsdLibProj_SOURCE_DIR}/Source/Test/Matrix3X3Test.cpp + ${EbsdLibProj_SOURCE_DIR}/Source/Test/ArrayHelpersTest.cpp + ${EbsdLibProj_SOURCE_DIR}/Source/Test/EbsdDataArrayTest.cpp + ${EbsdLibProj_SOURCE_DIR}/Source/Test/EbsdStringUtilsTest.cpp + ${EbsdLibProj_SOURCE_DIR}/Source/Test/EbsdTransformTest.cpp + ${EbsdLibProj_SOURCE_DIR}/Source/Test/EbsdLibRandomTest.cpp + ${EbsdLibProj_SOURCE_DIR}/Source/Test/OrientationMathTest.cpp + ${EbsdLibProj_SOURCE_DIR}/Source/Test/ColorTableTest.cpp + ${EbsdLibProj_SOURCE_DIR}/Source/Test/LambertUtilitiesTest.cpp + ${EbsdLibProj_SOURCE_DIR}/Source/Test/StereographicProjectionTest.cpp + ${EbsdLibProj_SOURCE_DIR}/Source/Test/TexturePresetTest.cpp + ${EbsdLibProj_SOURCE_DIR}/Source/Test/PhaseTest.cpp + ${EbsdLibProj_SOURCE_DIR}/Source/Test/LaueOpsTest.cpp + ${EbsdLibProj_SOURCE_DIR}/Source/Test/ModifiedLambertProjection3DTest.cpp + ${EbsdLibProj_SOURCE_DIR}/Source/Test/ModifiedLambertProjectionTest.cpp + ${EbsdLibProj_SOURCE_DIR}/Source/Test/OrientationTransformationTest.cpp + ${EbsdLibProj_SOURCE_DIR}/Source/Test/ToolTipGeneratorTest.cpp + ${EbsdLibProj_SOURCE_DIR}/Source/Test/PoleFigureDataTest.cpp + ${EbsdLibProj_SOURCE_DIR}/Source/Test/CanvasUtilitiesTest.cpp + ${EbsdLibProj_SOURCE_DIR}/Source/Test/ModifiedLambertProjectionArrayTest.cpp + ${EbsdLibProj_SOURCE_DIR}/Source/Test/PoleFigureUtilitiesTest.cpp + ${EbsdLibProj_SOURCE_DIR}/Source/Test/TiffWriterTest.cpp ) @@ -47,6 +70,11 @@ target_link_libraries(${UNIT_TEST_TARGET} Catch2::Catch2 ) +target_include_directories(${UNIT_TEST_TARGET} + PRIVATE + "${EbsdLibProj_SOURCE_DIR}/3rdParty/canvas_ity/src" +) + if(MSVC) set_source_files_properties(${EbsdLibProj_BINARY_DIR}/EbsdLibUnitTest.cpp PROPERTIES COMPILE_FLAGS /bigobj) diff --git a/Source/Test/CanvasUtilitiesTest.cpp b/Source/Test/CanvasUtilitiesTest.cpp new file mode 100644 index 0000000..66cd7ef --- /dev/null +++ b/Source/Test/CanvasUtilitiesTest.cpp @@ -0,0 +1,151 @@ +#include + +#include "EbsdLib/Core/EbsdDataArray.hpp" +#include "EbsdLib/Utilities/CanvasUtilities.hpp" + +#include +#include + +using namespace ebsdlib; + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::CanvasUtilitiesTest::MirrorImage", "[EbsdLib][CanvasUtilitiesTest]") +{ + const int dim = 4; + std::vector cDims = {4}; // RGBA + auto src = EbsdDataArray::CreateArray(dim * dim, cDims, "MirrorTestSrc", true); + + // Set top row (y=0) to red, bottom row (y=3) to blue + for(int x = 0; x < dim; x++) + { + uint8_t red[4] = {255, 0, 0, 255}; + uint8_t blue[4] = {0, 0, 255, 255}; + src->setTuple(0 * dim + x, red); // top row + src->setTuple((dim - 1) * dim + x, blue); // bottom row + } + + auto result = MirrorImage(src.get(), dim); + + // After mirror, top row should be blue, bottom row should be red + uint8_t* topPixel = result->getTuplePointer(0); + REQUIRE(topPixel[0] == 0); // R + REQUIRE(topPixel[2] == 255); // B + + uint8_t* bottomPixel = result->getTuplePointer((dim - 1) * dim); + REQUIRE(bottomPixel[0] == 255); // R + REQUIRE(bottomPixel[2] == 0); // B +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::CanvasUtilitiesTest::RotateImage90About001", "[EbsdLib][CanvasUtilitiesTest]") +{ + const int width = 4; + const int height = 4; + std::vector cDims = {4}; // RGBA + auto src = EbsdDataArray::CreateArray(width * height, cDims, "RotateTestSrc", true); + src->initializeWithZeros(); + + // Place a known pixel at (x=0, y=0) + uint8_t marker[4] = {111, 222, 33, 255}; + src->setTuple(0, marker); + + auto result = RotateImage90About001(src.get(), width, height); + + // After clockwise 90 rotation: (0,0) -> (height-1, 0) in rotated image + // new_x = height - y - 1 = 3, new_y = x = 0, rotWidth = height = 4 + // destIdx = new_y * rotWidth + new_x = 0 * 4 + 3 = 3 + uint8_t* rotPixel = result->getTuplePointer(3); + REQUIRE(rotPixel[0] == 111); + REQUIRE(rotPixel[1] == 222); + REQUIRE(rotPixel[2] == 33); + REQUIRE(rotPixel[3] == 255); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::CanvasUtilitiesTest::ConvertColorOrder", "[EbsdLib][CanvasUtilitiesTest]") +{ + const int dim = 2; + std::vector cDims = {4}; + auto src = EbsdDataArray::CreateArray(dim * dim, cDims, "ConvertColorSrc", true); + + // Set ARGB pixel: [A=10, R=20, G=30, B=40] + uint8_t argb[4] = {10, 20, 30, 40}; + src->setTuple(0, argb); + + auto result = ConvertColorOrder(src.get(), dim); + + // ConvertColorOrder swaps: dest[0]=src[2], dest[1]=src[1], dest[2]=src[0], dest[3]=src[3] + uint8_t* destPixel = result->getTuplePointer(0); + REQUIRE(destPixel[0] == 30); // src[2] = G + REQUIRE(destPixel[1] == 20); // src[1] = R + REQUIRE(destPixel[2] == 10); // src[0] = A + REQUIRE(destPixel[3] == 40); // src[3] = B +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::CanvasUtilitiesTest::RemoveAlphaChannel", "[EbsdLib][CanvasUtilitiesTest]") +{ + std::vector cDims = {4}; + auto src = EbsdDataArray::CreateArray(4, cDims, "RemoveAlphaSrc", true); + + uint8_t rgba[4] = {100, 150, 200, 255}; + src->setTuple(0, rgba); + + auto result = RemoveAlphaChannel(src.get()); + + REQUIRE(result->getNumberOfTuples() == 4); + REQUIRE(result->getNumberOfComponents() == 3); + + uint8_t* destPixel = result->getTuplePointer(0); + REQUIRE(destPixel[0] == 100); + REQUIRE(destPixel[1] == 150); + REQUIRE(destPixel[2] == 200); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::CanvasUtilitiesTest::CropRGBImage", "[EbsdLib][CanvasUtilitiesTest]") +{ + const int width = 8; + const int height = 8; + std::vector cDims = {3}; // RGB + auto src = EbsdDataArray::CreateArray(width * height, cDims, "CropSrc", true); + + // Fill entire image with a gradient based on linear index + for(int i = 0; i < width * height; i++) + { + uint8_t val = static_cast(i); + uint8_t pixel[3] = {val, val, val}; + src->setTuple(i, pixel); + } + + // Crop a 4x4 region starting at col=2, row=2 + auto result = CropRGBImage(src, width, height, 2, 2, 4, 4); + + REQUIRE(result->getNumberOfTuples() == 16); // 4*4 + + // The pixel at (col=2, row=2) in the source is at index 2*8+2=18 + uint8_t* firstPixel = result->getTuplePointer(0); + REQUIRE(firstPixel[0] == 18); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::CanvasUtilitiesTest::GeneratePointsOnUnitCircle", "[EbsdLib][CanvasUtilitiesTest]") +{ + Point3DType direction(0.0, 0.0, 1.0); // Z-axis + int numPoints = 36; + + std::vector points = GeneratePointsOnUnitCircle(direction, numPoints); + + // GeneratePointsOnUnitCircle generates num_points + 1 to close the circle + REQUIRE(points.size() == static_cast(numPoints + 1)); + + for(const auto& pt : points) + { + double magnitude = std::sqrt(pt[0] * pt[0] + pt[1] * pt[1] + pt[2] * pt[2]); + REQUIRE(magnitude == Approx(1.0).margin(1e-10)); + + // Each point should be orthogonal to the direction (dot product ~0) + double dot = pt[0] * direction[0] + pt[1] * direction[1] + pt[2] * direction[2]; + REQUIRE(dot == Approx(0.0).margin(1e-10)); + } +} diff --git a/Source/Test/ColorTableTest.cpp b/Source/Test/ColorTableTest.cpp new file mode 100644 index 0000000..416cc23 --- /dev/null +++ b/Source/Test/ColorTableTest.cpp @@ -0,0 +1,199 @@ +#include + +#include "EbsdLib/Utilities/ColorTable.h" +#include "EbsdLib/Utilities/ColorUtilities.h" + +#include +#include + +using namespace ebsdlib; + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ColorTableTest::RgbColor_dRgb_RoundTrip", "[EbsdLib][ColorTableTest]") +{ + int r = 128, g = 64, b = 32, a = 255; + Rgb color = RgbColor::dRgb(r, g, b, a); + REQUIRE(RgbColor::dRed(color) == r); + REQUIRE(RgbColor::dGreen(color) == g); + REQUIRE(RgbColor::dBlue(color) == b); + REQUIRE(RgbColor::dAlpha(color) == a); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ColorTableTest::RgbColor_dRgb_Extremes", "[EbsdLib][ColorTableTest]") +{ + // Pure black with full alpha + Rgb black = RgbColor::dRgb(0, 0, 0, 255); + REQUIRE(RgbColor::dRed(black) == 0); + REQUIRE(RgbColor::dGreen(black) == 0); + REQUIRE(RgbColor::dBlue(black) == 0); + REQUIRE(RgbColor::dAlpha(black) == 255); + + // Pure white with full alpha + Rgb white = RgbColor::dRgb(255, 255, 255, 255); + REQUIRE(RgbColor::dRed(white) == 255); + REQUIRE(RgbColor::dGreen(white) == 255); + REQUIRE(RgbColor::dBlue(white) == 255); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ColorTableTest::RgbColor_dGray", "[EbsdLib][ColorTableTest]") +{ + // The formula is: (R*11 + G*16 + B*5) / 32 + Rgb color = RgbColor::dRgb(100, 150, 200, 255); + int expected = (100 * 11 + 150 * 16 + 200 * 5) / 32; + REQUIRE(RgbColor::dGray(color) == expected); + + // Pure white should be close to 255 + Rgb white = RgbColor::dRgb(255, 255, 255, 255); + REQUIRE(RgbColor::dGray(white) == 255); + + // Pure black should be 0 + Rgb black = RgbColor::dRgb(0, 0, 0, 255); + REQUIRE(RgbColor::dGray(black) == 0); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ColorTableTest::RgbColor_fRgb", "[EbsdLib][ColorTableTest]") +{ + Rgb color = RgbColor::dRgb(255, 128, 0, 255); + auto [fr, fg, fb] = RgbColor::fRgb(color); + REQUIRE(fr == Approx(1.0f)); + REQUIRE(fg == Approx(128.0f / 255.0f)); + REQUIRE(fb == Approx(0.0f)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ColorTableTest::RgbColor_compare", "[EbsdLib][ColorTableTest]") +{ + Rgb a = RgbColor::dRgb(100, 200, 50, 255); + Rgb b = RgbColor::dRgb(100, 200, 50, 255); + Rgb c = RgbColor::dRgb(100, 200, 51, 255); + + REQUIRE(RgbColor::compare(a, b) == true); + REQUIRE(RgbColor::compare(a, c) == false); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ColorTableTest::GetColorTable", "[EbsdLib][ColorTableTest]") +{ + int numColors = 10; + std::vector colors(3 * numColors, 0.0f); + EbsdColorTable::GetColorTable(numColors, colors); + + // All values should be in [0, 1] range + for(size_t i = 0; i < colors.size(); i++) + { + CHECK(colors[i] >= 0.0f); + CHECK(colors[i] <= 1.0f); + } +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ColorTableTest::GetColorTable_SingleColor", "[EbsdLib][ColorTableTest]") +{ + std::vector colors(3, 0.0f); + EbsdColorTable::GetColorTable(1, colors); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ColorTableTest::ConvertHSVtoRgb_KnownValues", "[EbsdLib][ColorTableTest]") +{ + // Red: h=0, s=1, v=1 + { + Rgb color = ColorUtilities::ConvertHSVtoRgb(0.0f, 1.0f, 1.0f); + REQUIRE(RgbColor::dRed(color) == 255); + REQUIRE(RgbColor::dGreen(color) == 0); + REQUIRE(RgbColor::dBlue(color) == 0); + } + + // Green: h=0.333, s=1, v=1 + { + Rgb color = ColorUtilities::ConvertHSVtoRgb(1.0f / 3.0f, 1.0f, 1.0f); + REQUIRE(RgbColor::dRed(color) == 0); + REQUIRE(RgbColor::dGreen(color) == 255); + CHECK(RgbColor::dBlue(color) <= 1); // allow rounding + } + + // Blue: h=0.667, s=1, v=1 + { + Rgb color = ColorUtilities::ConvertHSVtoRgb(2.0f / 3.0f, 1.0f, 1.0f); + CHECK(RgbColor::dRed(color) <= 1); + REQUIRE(RgbColor::dGreen(color) == 0); + REQUIRE(RgbColor::dBlue(color) == 255); + } + + // White: s=0, v=1 + { + Rgb color = ColorUtilities::ConvertHSVtoRgb(0.0f, 0.0f, 1.0f); + REQUIRE(RgbColor::dRed(color) == 255); + REQUIRE(RgbColor::dGreen(color) == 255); + REQUIRE(RgbColor::dBlue(color) == 255); + } + + // Black: v=0 + { + Rgb color = ColorUtilities::ConvertHSVtoRgb(0.0f, 1.0f, 0.0f); + REQUIRE(RgbColor::dRed(color) == 0); + REQUIRE(RgbColor::dGreen(color) == 0); + REQUIRE(RgbColor::dBlue(color) == 0); + } +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ColorTableTest::Hsv2Rgb_KnownValues", "[EbsdLib][ColorTableTest]") +{ + // Red: h=0 degrees, s=1, v=1 + { + Rgb color = ColorUtilities::Hsv2Rgb(0.0f, 1.0f, 1.0f); + REQUIRE(RgbColor::dRed(color) == 255); + REQUIRE(RgbColor::dGreen(color) == 0); + REQUIRE(RgbColor::dBlue(color) == 0); + } + + // Green: h=120 degrees, s=1, v=1 + { + Rgb color = ColorUtilities::Hsv2Rgb(120.0f, 1.0f, 1.0f); + REQUIRE(RgbColor::dRed(color) == 0); + REQUIRE(RgbColor::dGreen(color) == 255); + CHECK(RgbColor::dBlue(color) <= 1); + } + + // Blue: h=240 degrees, s=1, v=1 + { + Rgb color = ColorUtilities::Hsv2Rgb(240.0f, 1.0f, 1.0f); + CHECK(RgbColor::dRed(color) <= 1); + REQUIRE(RgbColor::dGreen(color) == 0); + REQUIRE(RgbColor::dBlue(color) == 255); + } +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ColorTableTest::GenerateColors", "[EbsdLib][ColorTableTest]") +{ + int count = 12; + auto colors = ColorUtilities::GenerateColors(count); + + REQUIRE(colors.size() == static_cast(count)); + + for(const auto& color : colors) + { + // Alpha should be 255 (opaque) + REQUIRE(RgbColor::dAlpha(color) == 255); + + // RGB values in valid range + CHECK(RgbColor::dRed(color) >= 0); + CHECK(RgbColor::dRed(color) <= 255); + CHECK(RgbColor::dGreen(color) >= 0); + CHECK(RgbColor::dGreen(color) <= 255); + CHECK(RgbColor::dBlue(color) >= 0); + CHECK(RgbColor::dBlue(color) <= 255); + } +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ColorTableTest::GenerateColors_Single", "[EbsdLib][ColorTableTest]") +{ + auto colors = ColorUtilities::GenerateColors(1); + REQUIRE(colors.size() == 1); +} diff --git a/Source/Test/EbsdDataArrayTest.cpp b/Source/Test/EbsdDataArrayTest.cpp new file mode 100644 index 0000000..bf272a2 --- /dev/null +++ b/Source/Test/EbsdDataArrayTest.cpp @@ -0,0 +1,337 @@ +#include + +#include "EbsdLib/Core/EbsdDataArray.hpp" + +#include + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdDataArrayTest::CreateArraySimple", "[EbsdLib][EbsdDataArrayTest]") +{ + auto arr = EbsdDataArray::CreateArray(10, "TestArray", true); + REQUIRE(arr != nullptr); + REQUIRE(arr->getName() == "TestArray"); + REQUIRE(arr->getNumberOfTuples() == 10); + REQUIRE(arr->getNumberOfComponents() == 1); + REQUIRE(arr->getSize() == 10); + REQUIRE(arr->isAllocated() == true); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdDataArrayTest::CreateArrayWithCompDims", "[EbsdLib][EbsdDataArrayTest]") +{ + std::vector compDims = {3}; + auto arr = EbsdDataArray::CreateArray(10, compDims, "VectorArray", true); + REQUIRE(arr != nullptr); + REQUIRE(arr->getNumberOfTuples() == 10); + REQUIRE(arr->getNumberOfComponents() == 3); + REQUIRE(arr->getSize() == 30); + REQUIRE(arr->getComponentDimensions() == compDims); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdDataArrayTest::CreateArrayWithRank", "[EbsdLib][EbsdDataArrayTest]") +{ + size_t dims[1] = {4}; + auto arr = EbsdDataArray::CreateArray(5, 1, dims, "RankArray", true); + REQUIRE(arr != nullptr); + REQUIRE(arr->getNumberOfTuples() == 5); + REQUIRE(arr->getNumberOfComponents() == 4); + REQUIRE(arr->getSize() == 20); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdDataArrayTest::CreateArrayNoAllocate", "[EbsdLib][EbsdDataArrayTest]") +{ + auto arr = EbsdDataArray::CreateArray(10, "NoAlloc", false); + REQUIRE(arr != nullptr); + REQUIRE(arr->getNumberOfTuples() == 10); + REQUIRE(arr->isAllocated() == false); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdDataArrayTest::FromStdVector", "[EbsdLib][EbsdDataArrayTest]") +{ + std::vector vec = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f}; + auto arr = EbsdDataArray::FromStdVector(vec, "FromVec"); + REQUIRE(arr != nullptr); + REQUIRE(arr->getNumberOfTuples() == 5); + REQUIRE(arr->getNumberOfComponents() == 1); + + for(size_t i = 0; i < vec.size(); i++) + { + REQUIRE(arr->getValue(i) == vec[i]); + } +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdDataArrayTest::CopyFromPointer", "[EbsdLib][EbsdDataArrayTest]") +{ + float data[] = {10.0f, 20.0f, 30.0f}; + auto arr = EbsdDataArray::CopyFromPointer(data, 3, "CopyPtr"); + REQUIRE(arr != nullptr); + REQUIRE(arr->getSize() == 3); + REQUIRE(arr->getValue(0) == 10.0f); + REQUIRE(arr->getValue(1) == 20.0f); + REQUIRE(arr->getValue(2) == 30.0f); + + // Verify it's a deep copy + data[0] = 99.0f; + REQUIRE(arr->getValue(0) == 10.0f); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdDataArrayTest::GetSetName", "[EbsdLib][EbsdDataArrayTest]") +{ + auto arr = EbsdDataArray::CreateArray(5, "Original", true); + REQUIRE(arr->getName() == "Original"); + arr->setName("Renamed"); + REQUIRE(arr->getName() == "Renamed"); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdDataArrayTest::GetSetValue", "[EbsdLib][EbsdDataArrayTest]") +{ + auto arr = EbsdDataArray::CreateArray(5, "Values", true); + arr->initializeWithZeros(); + + arr->setValue(0, 1.5f); + arr->setValue(4, 9.9f); + REQUIRE(arr->getValue(0) == 1.5f); + REQUIRE(arr->getValue(1) == 0.0f); + REQUIRE(arr->getValue(4) == 9.9f); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdDataArrayTest::GetSetComponent", "[EbsdLib][EbsdDataArrayTest]") +{ + std::vector compDims = {3}; + auto arr = EbsdDataArray::CreateArray(5, compDims, "CompArray", true); + arr->initializeWithZeros(); + + arr->setComponent(0, 0, 1.0f); + arr->setComponent(0, 1, 2.0f); + arr->setComponent(0, 2, 3.0f); + arr->setComponent(2, 1, 7.5f); + + REQUIRE(arr->getComponent(0, 0) == 1.0f); + REQUIRE(arr->getComponent(0, 1) == 2.0f); + REQUIRE(arr->getComponent(0, 2) == 3.0f); + REQUIRE(arr->getComponent(2, 1) == 7.5f); + REQUIRE(arr->getComponent(1, 0) == 0.0f); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdDataArrayTest::InitializeWithZeros", "[EbsdLib][EbsdDataArrayTest]") +{ + auto arr = EbsdDataArray::CreateArray(10, "Zeros", true); + arr->initializeWithZeros(); + for(size_t i = 0; i < arr->getSize(); i++) + { + REQUIRE(arr->getValue(i) == 0); + } +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdDataArrayTest::InitializeWithValue", "[EbsdLib][EbsdDataArrayTest]") +{ + auto arr = EbsdDataArray::CreateArray(10, "Filled", true); + arr->initializeWithValue(42.0f); + for(size_t i = 0; i < arr->getSize(); i++) + { + REQUIRE(arr->getValue(i) == 42.0f); + } +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdDataArrayTest::ResizeTuples", "[EbsdLib][EbsdDataArrayTest]") +{ + auto arr = EbsdDataArray::CreateArray(5, "Resize", true); + arr->initializeWithValue(1.0f); + REQUIRE(arr->getNumberOfTuples() == 5); + + arr->resizeTuples(10); + REQUIRE(arr->getNumberOfTuples() == 10); + REQUIRE(arr->getSize() == 10); + + // Original values should still be present + REQUIRE(arr->getValue(0) == 1.0f); + REQUIRE(arr->getValue(4) == 1.0f); + + // Shrink + arr->resizeTuples(3); + REQUIRE(arr->getNumberOfTuples() == 3); + REQUIRE(arr->getSize() == 3); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdDataArrayTest::EraseTuples", "[EbsdLib][EbsdDataArrayTest]") +{ + auto arr = EbsdDataArray::CreateArray(5, "Erase", true); + for(size_t i = 0; i < 5; i++) + { + arr->setValue(i, static_cast(i + 1)); + } + + // Erase tuple at index 1 and 3 (values 2.0 and 4.0) + // NOTE: eraseTuples requires sorted indices + std::vector idxs = {1, 3}; + int32_t err = arr->eraseTuples(idxs); + REQUIRE(err >= 0); + + // NOTE: eraseTuples has a known bug where m_NumTuples is not updated, + // so we check getSize() instead of getNumberOfTuples() + REQUIRE(arr->getSize() == 3); + + // Remaining values should be 1, 3, 5 + REQUIRE(arr->getValue(0) == 1.0f); + REQUIRE(arr->getValue(1) == 3.0f); + REQUIRE(arr->getValue(2) == 5.0f); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdDataArrayTest::CopyTuple", "[EbsdLib][EbsdDataArrayTest]") +{ + auto arr = EbsdDataArray::CreateArray(5, "CopyTuple", true); + for(size_t i = 0; i < 5; i++) + { + arr->setValue(i, static_cast(i + 1)); + } + + arr->copyTuple(0, 4); // Copy tuple 0 to position 4 + REQUIRE(arr->getValue(4) == 1.0f); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdDataArrayTest::DeepCopy", "[EbsdLib][EbsdDataArrayTest]") +{ + auto arr = EbsdDataArray::CreateArray(5, "Original", true); + arr->initializeWithValue(7.0f); + + auto copy = arr->deepCopy(); + REQUIRE(copy != nullptr); + REQUIRE(copy->getName() == arr->getName()); + REQUIRE(copy->getNumberOfTuples() == arr->getNumberOfTuples()); + REQUIRE(copy->getSize() == arr->getSize()); + + for(size_t i = 0; i < arr->getSize(); i++) + { + REQUIRE(copy->getValue(i) == arr->getValue(i)); + } + + // Modify copy, original should be unchanged + copy->setValue(0, 99.0f); + REQUIRE(arr->getValue(0) == 7.0f); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdDataArrayTest::CopyFromArray", "[EbsdLib][EbsdDataArrayTest]") +{ + auto src = EbsdDataArray::CreateArray(5, "Source", true); + for(size_t i = 0; i < 5; i++) + { + src->setValue(i, static_cast(i + 10)); + } + + auto dest = EbsdDataArray::CreateArray(10, "Dest", true); + dest->initializeWithZeros(); + + bool result = dest->copyFromArray(3, src, 0, 3); + REQUIRE(result == true); + + REQUIRE(dest->getValue(3) == 10.0f); + REQUIRE(dest->getValue(4) == 11.0f); + REQUIRE(dest->getValue(5) == 12.0f); + REQUIRE(dest->getValue(0) == 0.0f); // Untouched +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdDataArrayTest::GetPointer", "[EbsdLib][EbsdDataArrayTest]") +{ + auto arr = EbsdDataArray::CreateArray(5, "PtrTest", true); + arr->initializeWithValue(3.0f); + + float* ptr = arr->getPointer(0); + REQUIRE(ptr != nullptr); + REQUIRE(ptr[0] == 3.0f); + + float* ptr2 = arr->getPointer(2); + REQUIRE(ptr2 != nullptr); + REQUIRE(ptr2[0] == 3.0f); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdDataArrayTest::SetTuple", "[EbsdLib][EbsdDataArrayTest]") +{ + std::vector compDims = {3}; + auto arr = EbsdDataArray::CreateArray(5, compDims, "SetTuple", true); + arr->initializeWithZeros(); + + float tupleData[3] = {1.0f, 2.0f, 3.0f}; + arr->setTuple(2, tupleData); + + REQUIRE(arr->getComponent(2, 0) == 1.0f); + REQUIRE(arr->getComponent(2, 1) == 2.0f); + REQUIRE(arr->getComponent(2, 2) == 3.0f); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdDataArrayTest::SetTupleFromVector", "[EbsdLib][EbsdDataArrayTest]") +{ + std::vector compDims = {3}; + auto arr = EbsdDataArray::CreateArray(5, compDims, "SetTupleVec", true); + arr->initializeWithZeros(); + + std::vector tupleData = {4.0f, 5.0f, 6.0f}; + arr->setTuple(1, tupleData); + + REQUIRE(arr->getComponent(1, 0) == 4.0f); + REQUIRE(arr->getComponent(1, 1) == 5.0f); + REQUIRE(arr->getComponent(1, 2) == 6.0f); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdDataArrayTest::MultiComponent", "[EbsdLib][EbsdDataArrayTest]") +{ + std::vector compDims = {2, 3}; + auto arr = EbsdDataArray::CreateArray(4, compDims, "MultiComp", true); + REQUIRE(arr->getNumberOfComponents() == 6); + REQUIRE(arr->getSize() == 24); + REQUIRE(arr->getNumberOfTuples() == 4); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdDataArrayTest::IntegerType", "[EbsdLib][EbsdDataArrayTest]") +{ + auto arr = EbsdDataArray::CreateArray(5, "IntArray", true); + arr->initializeWithZeros(); + arr->setValue(0, 42); + arr->setValue(4, -7); + REQUIRE(arr->getValue(0) == 42); + REQUIRE(arr->getValue(4) == -7); + REQUIRE(arr->getValue(2) == 0); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdDataArrayTest::OperatorBracket", "[EbsdLib][EbsdDataArrayTest]") +{ + auto arr = EbsdDataArray::CreateArray(5, "BracketTest", true); + arr->initializeWithZeros(); + + (*arr)[0] = 1.0f; + (*arr)[4] = 5.0f; + REQUIRE((*arr)[0] == 1.0f); + REQUIRE((*arr)[4] == 5.0f); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdDataArrayTest::TypeSize", "[EbsdLib][EbsdDataArrayTest]") +{ + auto floatArr = EbsdDataArray::CreateArray(1, "Float", true); + REQUIRE(floatArr->getTypeSize() == sizeof(float)); + + auto doubleArr = EbsdDataArray::CreateArray(1, "Double", true); + REQUIRE(doubleArr->getTypeSize() == sizeof(double)); + + auto intArr = EbsdDataArray::CreateArray(1, "Int", true); + REQUIRE(intArr->getTypeSize() == sizeof(int32_t)); +} diff --git a/Source/Test/EbsdLibRandomTest.cpp b/Source/Test/EbsdLibRandomTest.cpp new file mode 100644 index 0000000..a161d8e --- /dev/null +++ b/Source/Test/EbsdLibRandomTest.cpp @@ -0,0 +1,152 @@ +#include + +#include "EbsdLib/Math/EbsdLibRandom.h" + +#include +#include + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdLibRandomTest::SeededDeterministic", "[EbsdLib][EbsdLibRandomTest]") +{ + // Two generators seeded the same should produce the same sequence + EbsdLibRandom rg1; + rg1.init_genrand(12345); + + EbsdLibRandom rg2; + rg2.init_genrand(12345); + + for(int i = 0; i < 100; i++) + { + REQUIRE(rg1.genrand_int32() == rg2.genrand_int32()); + } +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdLibRandomTest::DifferentSeeds", "[EbsdLib][EbsdLibRandomTest]") +{ + EbsdLibRandom rg1; + rg1.init_genrand(12345); + + EbsdLibRandom rg2; + rg2.init_genrand(54321); + + // Different seeds should produce different sequences (very high probability) + bool allSame = true; + for(int i = 0; i < 100; i++) + { + if(rg1.genrand_int32() != rg2.genrand_int32()) + { + allSame = false; + break; + } + } + REQUIRE(allSame == false); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdLibRandomTest::GenRandReal1Range", "[EbsdLib][EbsdLibRandomTest]") +{ + EbsdLibRandom rg; + rg.init_genrand(42); + + // genrand_real1() should generate values in [0, 1] + for(int i = 0; i < 1000; i++) + { + double val = rg.genrand_real1(); + REQUIRE(val >= 0.0); + REQUIRE(val <= 1.0); + } +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdLibRandomTest::GenRandReal2Range", "[EbsdLib][EbsdLibRandomTest]") +{ + EbsdLibRandom rg; + rg.init_genrand(42); + + // genrand_real2() should generate values in [0, 1) + for(int i = 0; i < 1000; i++) + { + double val = rg.genrand_real2(); + REQUIRE(val >= 0.0); + REQUIRE(val < 1.0); + } +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdLibRandomTest::GenRandReal3Range", "[EbsdLib][EbsdLibRandomTest]") +{ + EbsdLibRandom rg; + rg.init_genrand(42); + + // genrand_real3() should generate values in (0, 1) + for(int i = 0; i < 1000; i++) + { + double val = rg.genrand_real3(); + REQUIRE(val > 0.0); + REQUIRE(val < 1.0); + } +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdLibRandomTest::GenRandRes53Range", "[EbsdLib][EbsdLibRandomTest]") +{ + EbsdLibRandom rg; + rg.init_genrand(42); + + // genrand_res53() should generate values in [0, 1) + for(int i = 0; i < 1000; i++) + { + double val = rg.genrand_res53(); + REQUIRE(val >= 0.0); + REQUIRE(val < 1.0); + } +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdLibRandomTest::GenRandInt31Range", "[EbsdLib][EbsdLibRandomTest]") +{ + EbsdLibRandom rg; + rg.init_genrand(42); + + // genrand_int31() should generate values in [0, 2^31 - 1] + for(int i = 0; i < 1000; i++) + { + long val = rg.genrand_int31(); + REQUIRE(val >= 0); + REQUIRE(val <= 0x7FFFFFFF); + } +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdLibRandomTest::InitByArray", "[EbsdLib][EbsdLibRandomTest]") +{ + unsigned long initKey[4] = {0x123, 0x234, 0x345, 0x456}; + + EbsdLibRandom rg1; + rg1.init_by_array(initKey, 4); + + EbsdLibRandom rg2; + rg2.init_by_array(initKey, 4); + + // Same init_key should produce same sequence + for(int i = 0; i < 100; i++) + { + REQUIRE(rg1.genrand_int32() == rg2.genrand_int32()); + } +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdLibRandomTest::ZeroSeed", "[EbsdLib][EbsdLibRandomTest]") +{ + EbsdLibRandom rg; + rg.init_genrand(0); + + // Should still produce valid output + for(int i = 0; i < 100; i++) + { + double val = rg.genrand_real1(); + REQUIRE(val >= 0.0); + REQUIRE(val <= 1.0); + } +} diff --git a/Source/Test/EbsdStringUtilsTest.cpp b/Source/Test/EbsdStringUtilsTest.cpp new file mode 100644 index 0000000..fee6fc3 --- /dev/null +++ b/Source/Test/EbsdStringUtilsTest.cpp @@ -0,0 +1,185 @@ +#include + +#include "EbsdLib/Utilities/EbsdStringUtils.hpp" + +#include +#include + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdStringUtilsTest::SplitSingleChar", "[EbsdLib][EbsdStringUtilsTest]") +{ + auto tokens = EbsdStringUtils::split("a,b,c", ','); + REQUIRE(tokens.size() == 3); + REQUIRE(tokens[0] == "a"); + REQUIRE(tokens[1] == "b"); + REQUIRE(tokens[2] == "c"); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdStringUtilsTest::SplitNoDelimiter", "[EbsdLib][EbsdStringUtilsTest]") +{ + auto tokens = EbsdStringUtils::split("hello", ','); + REQUIRE(tokens.size() == 1); + REQUIRE(tokens[0] == "hello"); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdStringUtilsTest::SplitEmptyString", "[EbsdLib][EbsdStringUtilsTest]") +{ + auto tokens = EbsdStringUtils::split("", ','); + REQUIRE(tokens.empty()); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdStringUtilsTest::SplitMultipleDelimiters", "[EbsdLib][EbsdStringUtilsTest]") +{ + auto tokens = EbsdStringUtils::split("a,b;c", {',', ';'}, false); + REQUIRE(tokens.size() == 3); + REQUIRE(tokens[0] == "a"); + REQUIRE(tokens[1] == "b"); + REQUIRE(tokens[2] == "c"); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdStringUtilsTest::SplitConsecutiveDelimiters", "[EbsdLib][EbsdStringUtilsTest]") +{ + auto tokens = EbsdStringUtils::split("a,,b", {','}, true); + REQUIRE(tokens.size() == 3); + REQUIRE(tokens[0] == "a"); + REQUIRE(tokens[1] == ""); + REQUIRE(tokens[2] == "b"); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdStringUtilsTest::SplitIgnoreConsecutive", "[EbsdLib][EbsdStringUtilsTest]") +{ + auto tokens = EbsdStringUtils::split("a,,b", {','}, false); + REQUIRE(tokens.size() == 2); + REQUIRE(tokens[0] == "a"); + REQUIRE(tokens[1] == "b"); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdStringUtilsTest::SpecificSplitIgnoreEmpty", "[EbsdLib][EbsdStringUtilsTest]") +{ + auto tokens = EbsdStringUtils::specific_split(",a,,b,", {','}, EbsdStringUtils::IgnoreEmpty); + REQUIRE(tokens.size() == 2); + REQUIRE(tokens[0] == "a"); + REQUIRE(tokens[1] == "b"); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdStringUtilsTest::SpecificSplitAllowAll", "[EbsdLib][EbsdStringUtilsTest]") +{ + auto tokens = EbsdStringUtils::specific_split(",a,,b,", {','}, EbsdStringUtils::AllowAll); + REQUIRE(tokens.size() == 5); + REQUIRE(tokens[0] == ""); + REQUIRE(tokens[1] == "a"); + REQUIRE(tokens[2] == ""); + REQUIRE(tokens[3] == "b"); + REQUIRE(tokens[4] == ""); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdStringUtilsTest::SpecificSplitOnlyConsecutive", "[EbsdLib][EbsdStringUtilsTest]") +{ + auto tokens = EbsdStringUtils::specific_split(",a,,b,", {','}, EbsdStringUtils::OnlyConsecutive); + // Leading/trailing empty stripped, but consecutive empty kept + REQUIRE(tokens.size() == 3); + REQUIRE(tokens[0] == "a"); + REQUIRE(tokens[1] == ""); + REQUIRE(tokens[2] == "b"); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdStringUtilsTest::Replace", "[EbsdLib][EbsdStringUtilsTest]") +{ + std::string result = EbsdStringUtils::replace("hello world", "world", "there"); + REQUIRE(result == "hello there"); + + // Multiple occurrences + result = EbsdStringUtils::replace("aabaa", "a", "x"); + REQUIRE(result == "xxbxx"); + + // No match + result = EbsdStringUtils::replace("hello", "xyz", "abc"); + REQUIRE(result == "hello"); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdStringUtilsTest::LTrim", "[EbsdLib][EbsdStringUtilsTest]") +{ + REQUIRE(EbsdStringUtils::ltrim(" hello") == "hello"); + REQUIRE(EbsdStringUtils::ltrim("\t\nhello") == "hello"); + REQUIRE(EbsdStringUtils::ltrim("hello ") == "hello "); + REQUIRE(EbsdStringUtils::ltrim("") == ""); + REQUIRE(EbsdStringUtils::ltrim(" ") == ""); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdStringUtilsTest::RTrim", "[EbsdLib][EbsdStringUtilsTest]") +{ + REQUIRE(EbsdStringUtils::rtrim("hello ") == "hello"); + REQUIRE(EbsdStringUtils::rtrim("hello\t\n") == "hello"); + REQUIRE(EbsdStringUtils::rtrim(" hello") == " hello"); + REQUIRE(EbsdStringUtils::rtrim("") == ""); + REQUIRE(EbsdStringUtils::rtrim(" ") == ""); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdStringUtilsTest::Trimmed", "[EbsdLib][EbsdStringUtilsTest]") +{ + REQUIRE(EbsdStringUtils::trimmed(" hello ") == "hello"); + REQUIRE(EbsdStringUtils::trimmed("\t hello \n") == "hello"); + REQUIRE(EbsdStringUtils::trimmed("hello") == "hello"); + REQUIRE(EbsdStringUtils::trimmed("") == ""); + REQUIRE(EbsdStringUtils::trimmed(" ") == ""); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdStringUtilsTest::Chop", "[EbsdLib][EbsdStringUtilsTest]") +{ + REQUIRE(EbsdStringUtils::chop("hello", 1) == "hell"); + REQUIRE(EbsdStringUtils::chop("hello", 3) == "he"); + REQUIRE(EbsdStringUtils::chop("hello", 5) == ""); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdStringUtilsTest::Number", "[EbsdLib][EbsdStringUtilsTest]") +{ + REQUIRE(EbsdStringUtils::number(42) == "42"); + REQUIRE(EbsdStringUtils::number(-7) == "-7"); + REQUIRE(EbsdStringUtils::number(0) == "0"); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdStringUtilsTest::Simplified", "[EbsdLib][EbsdStringUtilsTest]") +{ + // simplified() trims leading/trailing whitespace and replaces interior + // whitespace chars with spaces (but does not collapse consecutive spaces) + REQUIRE(EbsdStringUtils::simplified(" hello world ") == "hello world"); + REQUIRE(EbsdStringUtils::simplified("hello") == "hello"); + REQUIRE(EbsdStringUtils::simplified("") == ""); + REQUIRE(EbsdStringUtils::simplified("\thello\tworld\t") == "hello world"); + REQUIRE(EbsdStringUtils::simplified(" a ") == "a"); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdStringUtilsTest::SplitTabDelimited", "[EbsdLib][EbsdStringUtilsTest]") +{ + auto tokens = EbsdStringUtils::split("col1\tcol2\tcol3", '\t'); + REQUIRE(tokens.size() == 3); + REQUIRE(tokens[0] == "col1"); + REQUIRE(tokens[1] == "col2"); + REQUIRE(tokens[2] == "col3"); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdStringUtilsTest::SplitSpaceDelimited", "[EbsdLib][EbsdStringUtilsTest]") +{ + auto tokens = EbsdStringUtils::split("1.0 2.0 3.0", ' '); + REQUIRE(tokens.size() == 3); + REQUIRE(tokens[0] == "1.0"); + REQUIRE(tokens[1] == "2.0"); + REQUIRE(tokens[2] == "3.0"); +} diff --git a/Source/Test/EbsdTransformTest.cpp b/Source/Test/EbsdTransformTest.cpp new file mode 100644 index 0000000..6c41904 --- /dev/null +++ b/Source/Test/EbsdTransformTest.cpp @@ -0,0 +1,70 @@ +#include + +#include "EbsdLib/Core/EbsdLibConstants.h" +#include "EbsdLib/Core/EbsdTransform.h" + +#include + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdTransformTest::TSLTransformation", "[EbsdLib][EbsdTransformTest]") +{ + // TSL/EDAX: sample=[180, 0, 1, 0], euler=[90, 0, 0, 1] + std::array sampleTransformation = {180.0f, 0.0f, 1.0f, 0.0f}; + std::array eulerTransformation = {90.0f, 0.0f, 0.0f, 1.0f}; + + auto result = EbsdTransform::IdentifyStandardTransformation(sampleTransformation, eulerTransformation); + REQUIRE(result == ebsdlib::TSLdefault); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdTransformTest::HKLTransformation", "[EbsdLib][EbsdTransformTest]") +{ + // HKL: sample=[180, 0, 1, 0], euler=[0, 0, 0, 1] + std::array sampleTransformation = {180.0f, 0.0f, 1.0f, 0.0f}; + std::array eulerTransformation = {0.0f, 0.0f, 0.0f, 1.0f}; + + auto result = EbsdTransform::IdentifyStandardTransformation(sampleTransformation, eulerTransformation); + REQUIRE(result == ebsdlib::HKLdefault); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdTransformTest::HEDMTransformation", "[EbsdLib][EbsdTransformTest]") +{ + // HEDM: sample=[0, 0, 0, 1], euler=[0, 0, 0, 1] + std::array sampleTransformation = {0.0f, 0.0f, 0.0f, 1.0f}; + std::array eulerTransformation = {0.0f, 0.0f, 0.0f, 1.0f}; + + auto result = EbsdTransform::IdentifyStandardTransformation(sampleTransformation, eulerTransformation); + REQUIRE(result == ebsdlib::HEDMdefault); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdTransformTest::UnknownTransformation", "[EbsdLib][EbsdTransformTest]") +{ + // Unknown/custom parameters + std::array sampleTransformation = {45.0f, 1.0f, 0.0f, 0.0f}; + std::array eulerTransformation = {45.0f, 1.0f, 0.0f, 0.0f}; + + auto result = EbsdTransform::IdentifyStandardTransformation(sampleTransformation, eulerTransformation); + REQUIRE(result == ebsdlib::UnknownCoordinateMapping); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdTransformTest::ClassName", "[EbsdLib][EbsdTransformTest]") +{ + REQUIRE(EbsdTransform::ClassName() == "EbsdTransform"); + + EbsdTransform transform; + REQUIRE(transform.getNameOfClass() == "EbsdTransform"); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::EbsdTransformTest::ZeroRotation", "[EbsdLib][EbsdTransformTest]") +{ + // Both zeros but not matching HEDM pattern (different axis) + std::array sampleTransformation = {0.0f, 1.0f, 0.0f, 0.0f}; + std::array eulerTransformation = {0.0f, 0.0f, 0.0f, 1.0f}; + + auto result = EbsdTransform::IdentifyStandardTransformation(sampleTransformation, eulerTransformation); + REQUIRE(result == ebsdlib::UnknownCoordinateMapping); +} diff --git a/Source/Test/LambertUtilitiesTest.cpp b/Source/Test/LambertUtilitiesTest.cpp new file mode 100644 index 0000000..189299b --- /dev/null +++ b/Source/Test/LambertUtilitiesTest.cpp @@ -0,0 +1,67 @@ +#include + +#include "EbsdLib/Math/EbsdLibMath.h" +#include "EbsdLib/Utilities/LambertUtilities.h" + +#include + +using namespace ebsdlib; + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::LambertUtilitiesTest::Origin_NorthHemisphere", "[EbsdLib][LambertUtilitiesTest]") +{ + // Origin (0,0,0) should map to the north pole (0,0,1) + float vert[3] = {0.0f, 0.0f, 0.0f}; + int32_t result = LambertUtilities::LambertSquareVertToSphereVert(vert, LambertUtilities::Hemisphere::North); + REQUIRE(result == 0); + REQUIRE(vert[0] == Approx(0.0f).margin(1.0e-6f)); + REQUIRE(vert[1] == Approx(0.0f).margin(1.0e-6f)); + REQUIRE(vert[2] == Approx(1.0f).margin(1.0e-6f)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::LambertUtilitiesTest::SouthHemisphere_NonZeroInput", "[EbsdLib][LambertUtilitiesTest]") +{ + // For south hemisphere, hemiFactor = 1.0 which produces z = (2a^2/pi*r) - r + // For north hemisphere, hemiFactor = -1.0 which produces z = -((2a^2/pi*r) - r) = r - 2a^2/pi*r + // At the same non-zero input, south should differ from north in z sign + float vertNorth[3] = {0.5f, 0.3f, 0.0f}; + float vertSouth[3] = {0.5f, 0.3f, 0.0f}; + + LambertUtilities::LambertSquareVertToSphereVert(vertNorth, LambertUtilities::Hemisphere::North); + LambertUtilities::LambertSquareVertToSphereVert(vertSouth, LambertUtilities::Hemisphere::South); + + // x and y should be the same, z should have opposite signs + CHECK(vertNorth[0] == Approx(vertSouth[0]).margin(1.0e-5f)); + CHECK(vertNorth[1] == Approx(vertSouth[1]).margin(1.0e-5f)); + CHECK(vertNorth[2] == Approx(-vertSouth[2]).margin(1.0e-5f)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::LambertUtilitiesTest::OutputOnUnitSphere", "[EbsdLib][LambertUtilitiesTest]") +{ + // Test several points to verify output is on the unit sphere + float halfRange = static_cast(std::sqrt(ebsdlib::constants::k_PiD / 2.0)); + float testValues[] = {0.0f, 0.3f, -0.3f, 0.6f, -0.6f, halfRange * 0.5f}; + + for(float x : testValues) + { + for(float y : testValues) + { + float vert[3] = {x, y, 0.0f}; + int32_t result = LambertUtilities::LambertSquareVertToSphereVert(vert, LambertUtilities::Hemisphere::North); + REQUIRE(result == 0); + + float mag = std::sqrt(vert[0] * vert[0] + vert[1] * vert[1] + vert[2] * vert[2]); + CHECK(mag == Approx(1.0f).margin(1.0e-4f)); + } + } +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::LambertUtilitiesTest::ReturnValueValid", "[EbsdLib][LambertUtilitiesTest]") +{ + float vert[3] = {0.5f, 0.5f, 0.0f}; + int32_t result = LambertUtilities::LambertSquareVertToSphereVert(vert, LambertUtilities::Hemisphere::North); + REQUIRE(result == 0); +} diff --git a/Source/Test/LaueOpsTest.cpp b/Source/Test/LaueOpsTest.cpp new file mode 100644 index 0000000..bdbdd52 --- /dev/null +++ b/Source/Test/LaueOpsTest.cpp @@ -0,0 +1,174 @@ +#include + +#include "EbsdLib/LaueOps/CubicLowOps.h" +#include "EbsdLib/LaueOps/CubicOps.h" +#include "EbsdLib/LaueOps/HexagonalLowOps.h" +#include "EbsdLib/LaueOps/HexagonalOps.h" +#include "EbsdLib/LaueOps/LaueOps.h" +#include "EbsdLib/LaueOps/MonoclinicOps.h" +#include "EbsdLib/LaueOps/OrthoRhombicOps.h" +#include "EbsdLib/LaueOps/TetragonalLowOps.h" +#include "EbsdLib/LaueOps/TetragonalOps.h" +#include "EbsdLib/LaueOps/TriclinicOps.h" +#include "EbsdLib/LaueOps/TrigonalLowOps.h" +#include "EbsdLib/LaueOps/TrigonalOps.h" +#include "EbsdLib/Utilities/ColorTable.h" + +#include +#include +#include + +using namespace ebsdlib; + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::LaueOpsTest::GetAllOrientationOps", "[EbsdLib][LaueOpsTest]") +{ + auto ops = LaueOps::GetAllOrientationOps(); + // Should return exactly 12 entries (one for each Laue group index 0-11) + REQUIRE(ops.size() == 12); + + for(size_t i = 0; i < ops.size(); i++) + { + REQUIRE(ops[i] != nullptr); + } +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::LaueOpsTest::GetNumSymOps", "[EbsdLib][LaueOpsTest]") +{ + auto ops = LaueOps::GetAllOrientationOps(); + + // Index order matches CrystalStructure constants: + // 0=Hexagonal_High, 1=Cubic_High, 2=Hexagonal_Low, 3=Cubic_Low + // 4=Triclinic, 5=Monoclinic, 6=OrthoRhombic + // 7=Tetragonal_Low, 8=Tetragonal_High + // 9=Trigonal_Low, 10=Trigonal_High + CHECK(ops[0]->getNumSymOps() == 12); // Hexagonal_High + CHECK(ops[1]->getNumSymOps() == 24); // Cubic_High + CHECK(ops[2]->getNumSymOps() == 6); // Hexagonal_Low + CHECK(ops[3]->getNumSymOps() == 12); // Cubic_Low + CHECK(ops[4]->getNumSymOps() == 1); // Triclinic + CHECK(ops[5]->getNumSymOps() == 2); // Monoclinic + CHECK(ops[6]->getNumSymOps() == 4); // OrthoRhombic + CHECK(ops[7]->getNumSymOps() == 4); // Tetragonal_Low + CHECK(ops[8]->getNumSymOps() == 8); // Tetragonal_High + CHECK(ops[9]->getNumSymOps() == 3); // Trigonal_Low + CHECK(ops[10]->getNumSymOps() == 6); // Trigonal_High +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::LaueOpsTest::GetSymmetryName", "[EbsdLib][LaueOpsTest]") +{ + auto ops = LaueOps::GetAllOrientationOps(); + + for(size_t i = 0; i < ops.size(); i++) + { + std::string name = ops[i]->getSymmetryName(); + CHECK(!name.empty()); + } +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::LaueOpsTest::GetLaueNames", "[EbsdLib][LaueOpsTest]") +{ + auto names = LaueOps::GetLaueNames(); + REQUIRE(names.size() == 12); + + for(const auto& name : names) + { + CHECK(!name.empty()); + } +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::LaueOpsTest::GetHasInversion", "[EbsdLib][LaueOpsTest]") +{ + auto ops = LaueOps::GetAllOrientationOps(); + + for(size_t i = 0; i < ops.size(); i++) + { + // All Laue classes are centrosymmetric + CHECK(ops[i]->getHasInversion() == true); + } +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::LaueOpsTest::CalculateMisorientation_Identity", "[EbsdLib][LaueOpsTest]") +{ + // For CubicOps, misorientation between identical quaternions should be ~0 + CubicOps cubicOps; + QuatD identity = QuatD::identity(); + + auto misori = cubicOps.calculateMisorientation(identity, identity); + // Angle (misori[3]) should be approximately 0 + REQUIRE(misori[3] == Approx(0.0).margin(1.0e-6)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::LaueOpsTest::GenerateIPFColor_CubicOps", "[EbsdLib][LaueOpsTest]") +{ + CubicOps cubicOps; + // Identity Euler angles (0,0,0) with Z reference direction + double eulers[3] = {0.0, 0.0, 0.0}; + double refDir[3] = {0.0, 0.0, 1.0}; + + Rgb color = cubicOps.generateIPFColor(eulers, refDir, false); + + // Should produce a valid non-zero color + int r = RgbColor::dRed(color); + int g = RgbColor::dGreen(color); + int b = RgbColor::dBlue(color); + CHECK((r + g + b) > 0); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::LaueOpsTest::GenerateIPFColor_HexagonalOps", "[EbsdLib][LaueOpsTest]") +{ + HexagonalOps hexOps; + double eulers[3] = {0.0, 0.0, 0.0}; + double refDir[3] = {0.0, 0.0, 1.0}; + + Rgb color = hexOps.generateIPFColor(eulers, refDir, false); + + int r = RgbColor::dRed(color); + int g = RgbColor::dGreen(color); + int b = RgbColor::dBlue(color); + CHECK((r + g + b) > 0); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::LaueOpsTest::FZTypeToString", "[EbsdLib][LaueOpsTest]") +{ + CHECK(!LaueOps::FZTypeToString(LaueOps::FZType::Anorthic).empty()); + CHECK(!LaueOps::FZTypeToString(LaueOps::FZType::Cyclic).empty()); + CHECK(!LaueOps::FZTypeToString(LaueOps::FZType::Dihedral).empty()); + CHECK(!LaueOps::FZTypeToString(LaueOps::FZType::Tetrahedral).empty()); + CHECK(!LaueOps::FZTypeToString(LaueOps::FZType::Octahedral).empty()); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::LaueOpsTest::AxisOrderingTypeToString", "[EbsdLib][LaueOpsTest]") +{ + CHECK(!LaueOps::AxisOrderingTypeToString(LaueOps::AxisOrderingType::None).empty()); + CHECK(!LaueOps::AxisOrderingTypeToString(LaueOps::AxisOrderingType::TwoFold).empty()); + CHECK(!LaueOps::AxisOrderingTypeToString(LaueOps::AxisOrderingType::ThreeFold).empty()); + CHECK(!LaueOps::AxisOrderingTypeToString(LaueOps::AxisOrderingType::FourFold).empty()); + CHECK(!LaueOps::AxisOrderingTypeToString(LaueOps::AxisOrderingType::SixFold).empty()); + CHECK(!LaueOps::AxisOrderingTypeToString(LaueOps::AxisOrderingType::EightFold).empty()); + CHECK(!LaueOps::AxisOrderingTypeToString(LaueOps::AxisOrderingType::TenFold).empty()); + CHECK(!LaueOps::AxisOrderingTypeToString(LaueOps::AxisOrderingType::TwelveFold).empty()); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::LaueOpsTest::GetOrientationOpsFromSpaceGroupNumber", "[EbsdLib][LaueOpsTest]") +{ + // Cubic high: space groups 221-230 + auto cubicHighOps = LaueOps::GetOrientationOpsFromSpaceGroupNumber(225); + REQUIRE(cubicHighOps != nullptr); + CHECK(cubicHighOps->getNumSymOps() == 24); + + // Hexagonal high: space groups 191-194 + auto hexHighOps = LaueOps::GetOrientationOpsFromSpaceGroupNumber(194); + REQUIRE(hexHighOps != nullptr); + CHECK(hexHighOps->getNumSymOps() == 12); +} diff --git a/Source/Test/Matrix3X1Test.cpp b/Source/Test/Matrix3X1Test.cpp new file mode 100644 index 0000000..e3b270f --- /dev/null +++ b/Source/Test/Matrix3X1Test.cpp @@ -0,0 +1,313 @@ +#include + +#include "EbsdLib/Math/Matrix3X1.hpp" + +#include +#include + +using namespace ebsdlib; + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X1Test::DefaultConstruction", "[EbsdLib][Matrix3X1Test]") +{ + Matrix3X1 m; + REQUIRE(m[0] == 0.0f); + REQUIRE(m[1] == 0.0f); + REQUIRE(m[2] == 0.0f); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X1Test::ValueConstruction", "[EbsdLib][Matrix3X1Test]") +{ + Matrix3X1 m(1.0f, 2.0f, 3.0f); + REQUIRE(m[0] == 1.0f); + REQUIRE(m[1] == 2.0f); + REQUIRE(m[2] == 3.0f); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X1Test::PointerConstruction", "[EbsdLib][Matrix3X1Test]") +{ + float data[3] = {4.0f, 5.0f, 6.0f}; + Matrix3X1 m(data); + REQUIRE(m[0] == 4.0f); + REQUIRE(m[1] == 5.0f); + REQUIRE(m[2] == 6.0f); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X1Test::CopyAndMove", "[EbsdLib][Matrix3X1Test]") +{ + Matrix3X1 a(1.0f, 2.0f, 3.0f); + Matrix3X1 b(a); + REQUIRE(b[0] == 1.0f); + REQUIRE(b[1] == 2.0f); + REQUIRE(b[2] == 3.0f); + + Matrix3X1 c = std::move(b); + REQUIRE(c[0] == 1.0f); + REQUIRE(c[1] == 2.0f); + REQUIRE(c[2] == 3.0f); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X1Test::DotProduct", "[EbsdLib][Matrix3X1Test]") +{ + Matrix3X1 a(1.0f, 2.0f, 3.0f); + Matrix3X1 b(4.0f, 5.0f, 6.0f); + + // a dot b = 1*4 + 2*5 + 3*6 = 32 + REQUIRE(a.dot(b) == Approx(32.0f)); + + // Self dot product = sum of squares + REQUIRE(a.dot() == Approx(14.0f)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X1Test::CrossProduct", "[EbsdLib][Matrix3X1Test]") +{ + Matrix3X1 a(1.0f, 0.0f, 0.0f); + Matrix3X1 b(0.0f, 1.0f, 0.0f); + + auto c = a.cross(b); + REQUIRE(c[0] == Approx(0.0f)); + REQUIRE(c[1] == Approx(0.0f)); + REQUIRE(c[2] == Approx(1.0f)); + + // Cross product is anti-commutative: b x a = -(a x b) + auto d = b.cross(a); + REQUIRE(d[0] == Approx(0.0f)); + REQUIRE(d[1] == Approx(0.0f)); + REQUIRE(d[2] == Approx(-1.0f)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X1Test::Magnitude", "[EbsdLib][Matrix3X1Test]") +{ + Matrix3X1 m(3.0f, 4.0f, 0.0f); + REQUIRE(m.magnitude() == Approx(5.0f)); + + Matrix3X1 zero; + REQUIRE(zero.magnitude() == Approx(0.0f)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X1Test::NormalizeMember", "[EbsdLib][Matrix3X1Test]") +{ + Matrix3X1 m(3.0f, 4.0f, 0.0f); + auto n = m.normalize(); + REQUIRE(n[0] == Approx(0.6f)); + REQUIRE(n[1] == Approx(0.8f)); + REQUIRE(n[2] == Approx(0.0f)); + + // Magnitude of normalized vector should be 1 + REQUIRE(n.magnitude() == Approx(1.0f)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X1Test::NormalizeStatic", "[EbsdLib][Matrix3X1Test]") +{ + float i = 3.0f; + float j = 4.0f; + float k = 0.0f; + bool result = Matrix3X1::normalize(i, j, k); + REQUIRE(result == true); + REQUIRE(i == Approx(0.6f)); + REQUIRE(j == Approx(0.8f)); + REQUIRE(k == Approx(0.0f)); + + // Zero vector should return false + float zi = 0.0f; + float zj = 0.0f; + float zk = 0.0f; + result = Matrix3X1::normalize(zi, zj, zk); + REQUIRE(result == false); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X1Test::SortAscending", "[EbsdLib][Matrix3X1Test]") +{ + // Already sorted + { + Matrix3X1 m(1.0f, 2.0f, 3.0f); + auto sorted = m.sortAscending(); + REQUIRE(sorted[0] == 1.0f); + REQUIRE(sorted[1] == 2.0f); + REQUIRE(sorted[2] == 3.0f); + } + + // Reverse sorted + { + Matrix3X1 m(3.0f, 2.0f, 1.0f); + auto sorted = m.sortAscending(); + REQUIRE(sorted[0] == 1.0f); + REQUIRE(sorted[1] == 2.0f); + REQUIRE(sorted[2] == 3.0f); + } + + // Middle element is smallest + { + Matrix3X1 m(2.0f, 1.0f, 3.0f); + auto sorted = m.sortAscending(); + REQUIRE(sorted[0] == 1.0f); + REQUIRE(sorted[1] == 2.0f); + REQUIRE(sorted[2] == 3.0f); + } + + // Last element is smallest + { + Matrix3X1 m(3.0f, 1.0f, 0.0f); + auto sorted = m.sortAscending(); + REQUIRE(sorted[0] == 0.0f); + REQUIRE(sorted[1] == 1.0f); + REQUIRE(sorted[2] == 3.0f); + } + + // All equal + { + Matrix3X1 m(5.0f, 5.0f, 5.0f); + auto sorted = m.sortAscending(); + REQUIRE(sorted[0] == 5.0f); + REQUIRE(sorted[1] == 5.0f); + REQUIRE(sorted[2] == 5.0f); + } + + // Negative values + { + Matrix3X1 m(1.0f, -3.0f, 0.0f); + auto sorted = m.sortAscending(); + REQUIRE(sorted[0] == -3.0f); + REQUIRE(sorted[1] == 0.0f); + REQUIRE(sorted[2] == 1.0f); + } +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X1Test::CosTheta", "[EbsdLib][Matrix3X1Test]") +{ + Matrix3X1 a(1.0, 0.0, 0.0); + Matrix3X1 b(0.0, 1.0, 0.0); + + // Perpendicular vectors -> cosTheta = 0 + REQUIRE(a.cosTheta(b) == Approx(0.0)); + + // Same direction -> cosTheta = 1 + REQUIRE(a.cosTheta(a) == Approx(1.0)); + + // Opposite direction -> cosTheta = -1 + Matrix3X1 c(-1.0, 0.0, 0.0); + REQUIRE(a.cosTheta(c) == Approx(-1.0)); + + // Zero vector -> cosTheta = 1 (by convention) + Matrix3X1 zero; + REQUIRE(a.cosTheta(zero) == Approx(1.0)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X1Test::MaxValueIndex", "[EbsdLib][Matrix3X1Test]") +{ + Matrix3X1 a(5.0f, 3.0f, 1.0f); + REQUIRE(a.maxValueIndex() == 0); + + Matrix3X1 b(1.0f, 5.0f, 3.0f); + REQUIRE(b.maxValueIndex() == 1); + + Matrix3X1 c(1.0f, 3.0f, 5.0f); + REQUIRE(c.maxValueIndex() == 2); + + // Uses absolute value + Matrix3X1 d(-10.0f, 3.0f, 5.0f); + REQUIRE(d.maxValueIndex() == 0); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X1Test::Abs", "[EbsdLib][Matrix3X1Test]") +{ + Matrix3X1 m(-1.0f, 2.0f, -3.0f); + auto a = m.abs(); + REQUIRE(a[0] == 1.0f); + REQUIRE(a[1] == 2.0f); + REQUIRE(a[2] == 3.0f); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X1Test::OperatorAdd", "[EbsdLib][Matrix3X1Test]") +{ + Matrix3X1 a(1.0f, 2.0f, 3.0f); + Matrix3X1 b(4.0f, 5.0f, 6.0f); + + auto c = a + b; + REQUIRE(c[0] == 5.0f); + REQUIRE(c[1] == 7.0f); + REQUIRE(c[2] == 9.0f); + + // Scalar addition + auto d = a + 10.0f; + REQUIRE(d[0] == 11.0f); + REQUIRE(d[1] == 12.0f); + REQUIRE(d[2] == 13.0f); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X1Test::OperatorSubtract", "[EbsdLib][Matrix3X1Test]") +{ + Matrix3X1 a(4.0f, 5.0f, 6.0f); + Matrix3X1 b(1.0f, 2.0f, 3.0f); + + auto c = a - b; + REQUIRE(c[0] == 3.0f); + REQUIRE(c[1] == 3.0f); + REQUIRE(c[2] == 3.0f); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X1Test::OperatorScalarMultiply", "[EbsdLib][Matrix3X1Test]") +{ + Matrix3X1 a(1.0f, 2.0f, 3.0f); + auto b = a * 2.0f; + REQUIRE(b[0] == 2.0f); + REQUIRE(b[1] == 4.0f); + REQUIRE(b[2] == 6.0f); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X1Test::CopyInto", "[EbsdLib][Matrix3X1Test]") +{ + Matrix3X1 m(1.0f, 2.0f, 3.0f); + double dest[3] = {0.0, 0.0, 0.0}; + m.copyInto(dest); + REQUIRE(dest[0] == Approx(1.0)); + REQUIRE(dest[1] == Approx(2.0)); + REQUIRE(dest[2] == Approx(3.0)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X1Test::ToType", "[EbsdLib][Matrix3X1Test]") +{ + Matrix3X1 m(1.5f, 2.5f, 3.5f); + Matrix3X1 d = m.to(); + REQUIRE(d[0] == Approx(1.5)); + REQUIRE(d[1] == Approx(2.5)); + REQUIRE(d[2] == Approx(3.5)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X1Test::DataPointer", "[EbsdLib][Matrix3X1Test]") +{ + Matrix3X1 m(1.0f, 2.0f, 3.0f); + float* ptr = m.data(); + REQUIRE(ptr != nullptr); + REQUIRE(ptr[0] == 1.0f); + REQUIRE(ptr[1] == 2.0f); + REQUIRE(ptr[2] == 3.0f); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X1Test::DoubleType", "[EbsdLib][Matrix3X1Test]") +{ + Matrix3X1D m(1.0, 2.0, 3.0); + REQUIRE(m[0] == 1.0); + REQUIRE(m.magnitude() == Approx(std::sqrt(14.0))); + auto n = m.normalize(); + REQUIRE(n.magnitude() == Approx(1.0)); +} diff --git a/Source/Test/Matrix3X3Test.cpp b/Source/Test/Matrix3X3Test.cpp new file mode 100644 index 0000000..aa88181 --- /dev/null +++ b/Source/Test/Matrix3X3Test.cpp @@ -0,0 +1,374 @@ +#include + +#include "EbsdLib/Math/Matrix3X1.hpp" +#include "EbsdLib/Math/Matrix3X3.hpp" + +#include +#include + +using namespace ebsdlib; + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X3Test::DefaultConstruction", "[EbsdLib][Matrix3X3Test]") +{ + Matrix3X3 m; + for(size_t i = 0; i < 9; i++) + { + REQUIRE(m[i] == 0.0f); + } +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X3Test::ValueConstruction", "[EbsdLib][Matrix3X3Test]") +{ + Matrix3X3 m(1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f); + REQUIRE(m[0] == 1.0f); + REQUIRE(m[1] == 2.0f); + REQUIRE(m[2] == 3.0f); + REQUIRE(m[3] == 4.0f); + REQUIRE(m[4] == 5.0f); + REQUIRE(m[5] == 6.0f); + REQUIRE(m[6] == 7.0f); + REQUIRE(m[7] == 8.0f); + REQUIRE(m[8] == 9.0f); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X3Test::PointerConstruction", "[EbsdLib][Matrix3X3Test]") +{ + float data[9] = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f}; + Matrix3X3 m(data); + for(size_t i = 0; i < 9; i++) + { + REQUIRE(m[i] == data[i]); + } +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X3Test::OperatorParens", "[EbsdLib][Matrix3X3Test]") +{ + Matrix3X3 m(1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f); + REQUIRE(m(0, 0) == 1.0f); + REQUIRE(m(0, 1) == 2.0f); + REQUIRE(m(0, 2) == 3.0f); + REQUIRE(m(1, 0) == 4.0f); + REQUIRE(m(1, 1) == 5.0f); + REQUIRE(m(1, 2) == 6.0f); + REQUIRE(m(2, 0) == 7.0f); + REQUIRE(m(2, 1) == 8.0f); + REQUIRE(m(2, 2) == 9.0f); + + // Out of range should throw + REQUIRE_THROWS_AS(m(3, 0), std::out_of_range); + REQUIRE_THROWS_AS(m(0, 3), std::out_of_range); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X3Test::Identity", "[EbsdLib][Matrix3X3Test]") +{ + auto id = Matrix3X3::Identity(); + REQUIRE(id(0, 0) == 1.0f); + REQUIRE(id(0, 1) == 0.0f); + REQUIRE(id(0, 2) == 0.0f); + REQUIRE(id(1, 0) == 0.0f); + REQUIRE(id(1, 1) == 1.0f); + REQUIRE(id(1, 2) == 0.0f); + REQUIRE(id(2, 0) == 0.0f); + REQUIRE(id(2, 1) == 0.0f); + REQUIRE(id(2, 2) == 1.0f); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X3Test::MatrixMultiply", "[EbsdLib][Matrix3X3Test]") +{ + auto id = Matrix3X3::Identity(); + Matrix3X3 a(1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f); + + // A * I = A + auto result = a * id; + for(size_t i = 0; i < 9; i++) + { + REQUIRE(result[i] == Approx(a[i])); + } + + // I * A = A + result = id * a; + for(size_t i = 0; i < 9; i++) + { + REQUIRE(result[i] == Approx(a[i])); + } + + // Known multiplication + Matrix3X3 b(9.0f, 8.0f, 7.0f, 6.0f, 5.0f, 4.0f, 3.0f, 2.0f, 1.0f); + result = a * b; + // Row 0: [1*9+2*6+3*3, 1*8+2*5+3*2, 1*7+2*4+3*1] = [30, 24, 18] + REQUIRE(result(0, 0) == Approx(30.0f)); + REQUIRE(result(0, 1) == Approx(24.0f)); + REQUIRE(result(0, 2) == Approx(18.0f)); + // Row 1: [4*9+5*6+6*3, 4*8+5*5+6*2, 4*7+5*4+6*1] = [84, 69, 54] + REQUIRE(result(1, 0) == Approx(84.0f)); + REQUIRE(result(1, 1) == Approx(69.0f)); + REQUIRE(result(1, 2) == Approx(54.0f)); + // Row 2: [7*9+8*6+9*3, 7*8+8*5+9*2, 7*7+8*4+9*1] = [138, 114, 90] + REQUIRE(result(2, 0) == Approx(138.0f)); + REQUIRE(result(2, 1) == Approx(114.0f)); + REQUIRE(result(2, 2) == Approx(90.0f)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X3Test::MultiplyInPlace", "[EbsdLib][Matrix3X3Test]") +{ + Matrix3X3 a(1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f); + auto id = Matrix3X3::Identity(); + a.multiplyInPlace(id); + REQUIRE(a(0, 0) == Approx(1.0f)); + REQUIRE(a(1, 1) == Approx(5.0f)); + REQUIRE(a(2, 2) == Approx(9.0f)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X3Test::MatrixVectorMultiply", "[EbsdLib][Matrix3X3Test]") +{ + auto id = Matrix3X3::Identity(); + Matrix3X1 v(1.0f, 2.0f, 3.0f); + + // I * v = v + auto result = id * v; + REQUIRE(result[0] == Approx(1.0f)); + REQUIRE(result[1] == Approx(2.0f)); + REQUIRE(result[2] == Approx(3.0f)); + + // Known multiplication + Matrix3X3 m(1.0f, 0.0f, 0.0f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f); + result = m * v; + // [1*1+0*2+0*3, 0*1+0*2+(-1)*3, 0*1+1*2+0*3] = [1, -3, 2] + REQUIRE(result[0] == Approx(1.0f)); + REQUIRE(result[1] == Approx(-3.0f)); + REQUIRE(result[2] == Approx(2.0f)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X3Test::MatrixArrayMultiply", "[EbsdLib][Matrix3X3Test]") +{ + auto id = Matrix3X3::Identity(); + std::array v = {1.0f, 2.0f, 3.0f}; + + auto result = id * v; + REQUIRE(result[0] == Approx(1.0f)); + REQUIRE(result[1] == Approx(2.0f)); + REQUIRE(result[2] == Approx(3.0f)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X3Test::ScalarMultiply", "[EbsdLib][Matrix3X3Test]") +{ + Matrix3X3 m(1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f); + auto result = m * 2.0f; + REQUIRE(result[0] == Approx(2.0f)); + REQUIRE(result[4] == Approx(10.0f)); + REQUIRE(result[8] == Approx(18.0f)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X3Test::Addition", "[EbsdLib][Matrix3X3Test]") +{ + Matrix3X3 a(1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f); + Matrix3X3 b(9.0f, 8.0f, 7.0f, 6.0f, 5.0f, 4.0f, 3.0f, 2.0f, 1.0f); + + auto c = a + b; + REQUIRE(c[0] == Approx(10.0f)); + REQUIRE(c[4] == Approx(10.0f)); + REQUIRE(c[8] == Approx(10.0f)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X3Test::Subtraction", "[EbsdLib][Matrix3X3Test]") +{ + Matrix3X3 a(5.0f, 5.0f, 5.0f, 5.0f, 5.0f, 5.0f, 5.0f, 5.0f, 5.0f); + Matrix3X3 b(1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f); + + auto c = a - b; + REQUIRE(c[0] == Approx(4.0f)); + REQUIRE(c[4] == Approx(0.0f)); + REQUIRE(c[8] == Approx(-4.0f)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X3Test::Transpose", "[EbsdLib][Matrix3X3Test]") +{ + Matrix3X3 m(1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f); + auto t = m.transpose(); + REQUIRE(t(0, 0) == 1.0f); + REQUIRE(t(0, 1) == 4.0f); + REQUIRE(t(0, 2) == 7.0f); + REQUIRE(t(1, 0) == 2.0f); + REQUIRE(t(1, 1) == 5.0f); + REQUIRE(t(1, 2) == 8.0f); + REQUIRE(t(2, 0) == 3.0f); + REQUIRE(t(2, 1) == 6.0f); + REQUIRE(t(2, 2) == 9.0f); + + // (A^T)^T = A + auto tt = t.transpose(); + for(size_t i = 0; i < 9; i++) + { + REQUIRE(tt[i] == Approx(m[i])); + } +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X3Test::Determinant", "[EbsdLib][Matrix3X3Test]") +{ + // Identity determinant = 1 + auto id = Matrix3X3::Identity(); + REQUIRE(id.determinant() == Approx(1.0f)); + + // Known determinant + Matrix3X3 m(1.0f, 2.0f, 3.0f, 0.0f, 1.0f, 4.0f, 5.0f, 6.0f, 0.0f); + // det = 1*(1*0-4*6) - 2*(0*0-4*5) + 3*(0*6-1*5) = 1*(-24) - 2*(-20) + 3*(-5) = -24+40-15 = 1 + REQUIRE(m.determinant() == Approx(1.0f)); + + // det(A^T) = det(A) + auto t = m.transpose(); + REQUIRE(t.determinant() == Approx(m.determinant())); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X3Test::Invert", "[EbsdLib][Matrix3X3Test]") +{ + Matrix3X3 m(1.0f, 2.0f, 3.0f, 0.0f, 1.0f, 4.0f, 5.0f, 6.0f, 0.0f); + + auto inv = m.invert(); + + // A * A^-1 should be approximately I + auto product = m * inv; + REQUIRE(product(0, 0) == Approx(1.0f).margin(1e-5f)); + REQUIRE(product(0, 1) == Approx(0.0f).margin(1e-5f)); + REQUIRE(product(0, 2) == Approx(0.0f).margin(1e-5f)); + REQUIRE(product(1, 0) == Approx(0.0f).margin(1e-5f)); + REQUIRE(product(1, 1) == Approx(1.0f).margin(1e-5f)); + REQUIRE(product(1, 2) == Approx(0.0f).margin(1e-5f)); + REQUIRE(product(2, 0) == Approx(0.0f).margin(1e-5f)); + REQUIRE(product(2, 1) == Approx(0.0f).margin(1e-5f)); + REQUIRE(product(2, 2) == Approx(1.0f).margin(1e-5f)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X3Test::Minors", "[EbsdLib][Matrix3X3Test]") +{ + Matrix3X3 m(1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f); + auto min = m.minors(); + + // Minor(0,0) = det([[5,6],[8,9]]) = 45-48 = -3 + REQUIRE(min[0] == Approx(-3.0f)); + // Minor(0,1) = det([[4,6],[7,9]]) = 36-42 = -6 + REQUIRE(min[1] == Approx(-6.0f)); + // Minor(0,2) = det([[4,5],[7,8]]) = 32-35 = -3 + REQUIRE(min[2] == Approx(-3.0f)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X3Test::Cofactor", "[EbsdLib][Matrix3X3Test]") +{ + Matrix3X3 m(1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f); + auto cof = m.cofactor(); + + // Cofactor applies sign checkerboard to minors + REQUIRE(cof[0] == Approx(-3.0f)); // +minor[0] + REQUIRE(cof[1] == Approx(6.0f)); // -minor[1] + REQUIRE(cof[2] == Approx(-3.0f)); // +minor[2] +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X3Test::Adjoint", "[EbsdLib][Matrix3X3Test]") +{ + Matrix3X3 m(1.0f, 2.0f, 3.0f, 0.0f, 1.0f, 4.0f, 5.0f, 6.0f, 0.0f); + auto adj = m.adjoint(); + + // adjoint = cofactor transposed + auto cof = m.cofactor(); + auto cofT = cof.transpose(); + for(size_t i = 0; i < 9; i++) + { + REQUIRE(adj[i] == Approx(cofT[i])); + } +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X3Test::ColAndRow", "[EbsdLib][Matrix3X3Test]") +{ + Matrix3X3 m(1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f); + + auto col0 = m.col(0); + REQUIRE(col0[0] == 1.0f); + REQUIRE(col0[1] == 4.0f); + REQUIRE(col0[2] == 7.0f); + + auto col2 = m.col(2); + REQUIRE(col2[0] == 3.0f); + REQUIRE(col2[1] == 6.0f); + REQUIRE(col2[2] == 9.0f); + + auto row0 = m.row(0); + REQUIRE(row0[0] == 1.0f); + REQUIRE(row0[1] == 2.0f); + REQUIRE(row0[2] == 3.0f); + + auto row2 = m.row(2); + REQUIRE(row2[0] == 7.0f); + REQUIRE(row2[1] == 8.0f); + REQUIRE(row2[2] == 9.0f); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X3Test::Normalize", "[EbsdLib][Matrix3X3Test]") +{ + auto id = Matrix3X3::Identity(); + auto norm = id.normalize(); + + // Identity columns already have unit length, so normalize should return identity + REQUIRE(norm(0, 0) == Approx(1.0f)); + REQUIRE(norm(1, 1) == Approx(1.0f)); + REQUIRE(norm(2, 2) == Approx(1.0f)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X3Test::CopyInto", "[EbsdLib][Matrix3X3Test]") +{ + Matrix3X3 m(1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f); + double dest[9] = {}; + m.copyInto(dest); + for(size_t i = 0; i < 9; i++) + { + REQUIRE(dest[i] == Approx(static_cast(m[i]))); + } +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X3Test::DirectStructureMatrix", "[EbsdLib][Matrix3X3Test]") +{ + // Cubic system: a=b=c=1, alpha=beta=gamma=90 + std::array cubicParams = {1.0f, 1.0f, 1.0f, 90.0f, 90.0f, 90.0f}; + auto dsm = Matrix3X3::DirectStructureMatrix(cubicParams); + + // For cubic: DSM should be approximately identity + REQUIRE(dsm(0, 0) == Approx(1.0f).margin(1e-5f)); + REQUIRE(dsm(1, 1) == Approx(1.0f).margin(1e-5f)); + REQUIRE(dsm(2, 2) == Approx(1.0f).margin(1e-5f)); + REQUIRE(dsm(0, 1) == Approx(0.0f).margin(1e-5f)); + REQUIRE(dsm(0, 2) == Approx(0.0f).margin(1e-5f)); + REQUIRE(dsm(1, 0) == Approx(0.0f).margin(1e-5f)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::Matrix3X3Test::DoubleType", "[EbsdLib][Matrix3X3Test]") +{ + auto id = Matrix3X3D::Identity(); + REQUIRE(id.determinant() == Approx(1.0)); + + auto inv = id.invert(); + for(size_t i = 0; i < 9; i++) + { + REQUIRE(inv[i] == Approx(id[i])); + } +} diff --git a/Source/Test/ModifiedLambertProjection3DTest.cpp b/Source/Test/ModifiedLambertProjection3DTest.cpp new file mode 100644 index 0000000..9653389 --- /dev/null +++ b/Source/Test/ModifiedLambertProjection3DTest.cpp @@ -0,0 +1,140 @@ +#include + +#include "EbsdLib/Core/EbsdLibConstants.h" +#include "EbsdLib/Utilities/ModifiedLambertProjection3D.hpp" + +#include +#include + +using namespace ebsdlib; +using MLP3D = ModifiedLambertProjection3D, double>; + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ModifiedLambertProjection3DTest::GetPyramid", "[EbsdLib][ModifiedLambertProjection3DTest]") +{ + // +Z axis -> pyramid 1 + { + std::vector xyz = {0.0, 0.0, 1.0}; + REQUIRE(MLP3D::GetPyramid(xyz) == 1); + } + // -Z axis -> pyramid 2 + { + std::vector xyz = {0.0, 0.0, -1.0}; + REQUIRE(MLP3D::GetPyramid(xyz) == 2); + } + // +X axis -> pyramid 3 + { + std::vector xyz = {1.0, 0.0, 0.0}; + REQUIRE(MLP3D::GetPyramid(xyz) == 3); + } + // -X axis -> pyramid 4 + { + std::vector xyz = {-1.0, 0.0, 0.0}; + REQUIRE(MLP3D::GetPyramid(xyz) == 4); + } + // +Y axis -> pyramid 5 + { + std::vector xyz = {0.0, 1.0, 0.0}; + REQUIRE(MLP3D::GetPyramid(xyz) == 5); + } + // -Y axis -> pyramid 6 + { + std::vector xyz = {0.0, -1.0, 0.0}; + REQUIRE(MLP3D::GetPyramid(xyz) == 6); + } +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ModifiedLambertProjection3DTest::LambertCubeToBall_Origin", "[EbsdLib][ModifiedLambertProjection3DTest]") +{ + std::vector origin = {0.0, 0.0, 0.0}; + int ierr = 0; + auto result = MLP3D::LambertCubeToBall(origin, ierr); + + REQUIRE(ierr == 0); + REQUIRE(result.size() == 3); + CHECK(result[0] == Approx(0.0).margin(1.0e-10)); + CHECK(result[1] == Approx(0.0).margin(1.0e-10)); + CHECK(result[2] == Approx(0.0).margin(1.0e-10)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ModifiedLambertProjection3DTest::LambertCubeToBall_OutOfRange", "[EbsdLib][ModifiedLambertProjection3DTest]") +{ + // ap/2 ≈ 1.0725, so values > ap/2 + epsilon should return ierr=-1 + std::vector outOfRange = {10.0, 0.0, 0.0}; + int ierr = 0; + auto result = MLP3D::LambertCubeToBall(outOfRange, ierr); + + REQUIRE(ierr == -1); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ModifiedLambertProjection3DTest::LambertBallToCube_Origin", "[EbsdLib][ModifiedLambertProjection3DTest]") +{ + std::vector origin = {0.0, 0.0, 0.0}; + int ierr = 0; + auto result = MLP3D::LambertBallToCube(origin, ierr); + + REQUIRE(ierr == 0); + REQUIRE(result.size() == 3); + CHECK(result[0] == Approx(0.0).margin(1.0e-10)); + CHECK(result[1] == Approx(0.0).margin(1.0e-10)); + CHECK(result[2] == Approx(0.0).margin(1.0e-10)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ModifiedLambertProjection3DTest::LambertBallToCube_OutOfRange", "[EbsdLib][ModifiedLambertProjection3DTest]") +{ + // R1 ≈ 1.33, so values with magnitude > R1 should return ierr=-1 + std::vector outOfRange = {10.0, 0.0, 0.0}; + int ierr = 0; + auto result = MLP3D::LambertBallToCube(outOfRange, ierr); + + REQUIRE(ierr == -1); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ModifiedLambertProjection3DTest::RoundTrip_CubeToBallToCube", "[EbsdLib][ModifiedLambertProjection3DTest]") +{ + // Test round-trip for several valid cube points + std::vector> testPoints = { + {0.1, 0.0, 0.5}, {0.0, 0.3, 0.4}, {-0.2, 0.1, 0.6}, {0.3, -0.3, 0.5}, {0.0, 0.0, 0.8}, + }; + + for(const auto& pt : testPoints) + { + int ierr1 = 0; + auto ball = MLP3D::LambertCubeToBall(pt, ierr1); + REQUIRE(ierr1 == 0); + + int ierr2 = 0; + auto cube = MLP3D::LambertBallToCube(ball, ierr2); + REQUIRE(ierr2 == 0); + + CHECK(cube[0] == Approx(pt[0]).margin(1.0e-6)); + CHECK(cube[1] == Approx(pt[1]).margin(1.0e-6)); + CHECK(cube[2] == Approx(pt[2]).margin(1.0e-6)); + } +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ModifiedLambertProjection3DTest::BallOutputBounded", "[EbsdLib][ModifiedLambertProjection3DTest]") +{ + // Ball output should have magnitude <= R1 + std::vector> testPoints = { + {0.5, 0.5, 0.5}, + {-0.5, 0.3, 0.2}, + {0.0, 0.0, 1.0}, + }; + + for(const auto& pt : testPoints) + { + int ierr = 0; + auto ball = MLP3D::LambertCubeToBall(pt, ierr); + REQUIRE(ierr == 0); + + double mag = std::sqrt(ball[0] * ball[0] + ball[1] * ball[1] + ball[2] * ball[2]); + CHECK(mag <= LambertParametersType::R1 + 1.0e-8); + } +} diff --git a/Source/Test/ModifiedLambertProjectionArrayTest.cpp b/Source/Test/ModifiedLambertProjectionArrayTest.cpp new file mode 100644 index 0000000..d35b562 --- /dev/null +++ b/Source/Test/ModifiedLambertProjectionArrayTest.cpp @@ -0,0 +1,184 @@ +#include + +#include "EbsdLib/Utilities/ModifiedLambertProjectionArray.h" + +#include +#include + +using namespace ebsdlib; + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ModifiedLambertProjectionArrayTest::NewReturnsValid", "[EbsdLib][ModifiedLambertProjectionArrayTest]") +{ + auto arr = ModifiedLambertProjectionArray::New(); + REQUIRE(arr != nullptr); + REQUIRE(arr->isAllocated() == true); + REQUIRE(arr->getNumberOfTuples() == 0); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ModifiedLambertProjectionArrayTest::SetGetName", "[EbsdLib][ModifiedLambertProjectionArrayTest]") +{ + auto arr = ModifiedLambertProjectionArray::New(); + arr->setName("TestArray"); + REQUIRE(arr->getName() == "TestArray"); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ModifiedLambertProjectionArrayTest::SetGetPhase", "[EbsdLib][ModifiedLambertProjectionArrayTest]") +{ + auto arr = ModifiedLambertProjectionArray::New(); + arr->setPhase(42); + REQUIRE(arr->getPhase() == 42); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ModifiedLambertProjectionArrayTest::FillArray", "[EbsdLib][ModifiedLambertProjectionArrayTest]") +{ + auto arr = ModifiedLambertProjectionArray::New(); + arr->fillArrayWithNewModifiedLambertProjection(5); + REQUIRE(arr->getNumberOfTuples() == 5); + + for(size_t i = 0; i < 5; i++) + { + REQUIRE(arr->getModifiedLambertProjection(static_cast(i)) != nullptr); + } +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ModifiedLambertProjectionArrayTest::SetGetModifiedLambertProjection", "[EbsdLib][ModifiedLambertProjectionArrayTest]") +{ + auto arr = ModifiedLambertProjectionArray::New(); + arr->fillArrayWithNewModifiedLambertProjection(3); + + auto proj = ModifiedLambertProjection::New(); + arr->setModifiedLambertProjection(1, proj); + + REQUIRE(arr->getModifiedLambertProjection(1) == proj); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ModifiedLambertProjectionArrayTest::OperatorBracket", "[EbsdLib][ModifiedLambertProjectionArrayTest]") +{ + auto arr = ModifiedLambertProjectionArray::New(); + arr->fillArrayWithNewModifiedLambertProjection(3); + + auto proj = ModifiedLambertProjection::New(); + arr->setModifiedLambertProjection(2, proj); + + REQUIRE((*arr)[2] == proj); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ModifiedLambertProjectionArrayTest::ResizeTuples", "[EbsdLib][ModifiedLambertProjectionArrayTest]") +{ + auto arr = ModifiedLambertProjectionArray::New(); + arr->fillArrayWithNewModifiedLambertProjection(3); + REQUIRE(arr->getNumberOfTuples() == 3); + + arr->resizeTuples(10); + REQUIRE(arr->getNumberOfTuples() == 10); + + arr->resizeTuples(2); + REQUIRE(arr->getNumberOfTuples() == 2); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ModifiedLambertProjectionArrayTest::CopyTuple", "[EbsdLib][ModifiedLambertProjectionArrayTest]") +{ + auto arr = ModifiedLambertProjectionArray::New(); + arr->fillArrayWithNewModifiedLambertProjection(4); + + auto proj = ModifiedLambertProjection::New(); + arr->setModifiedLambertProjection(0, proj); + + arr->copyTuple(0, 3); + REQUIRE(arr->getModifiedLambertProjection(3) == proj); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ModifiedLambertProjectionArrayTest::EraseTuples", "[EbsdLib][ModifiedLambertProjectionArrayTest]") +{ + auto arr = ModifiedLambertProjectionArray::New(); + arr->fillArrayWithNewModifiedLambertProjection(5); + REQUIRE(arr->getNumberOfTuples() == 5); + + std::vector idxs = {1, 3}; + int err = arr->eraseTuples(idxs); + REQUIRE(err == 0); + REQUIRE(arr->getNumberOfTuples() == 3); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ModifiedLambertProjectionArrayTest::EraseTuples_InvalidIndex", "[EbsdLib][ModifiedLambertProjectionArrayTest]") +{ + auto arr = ModifiedLambertProjectionArray::New(); + arr->fillArrayWithNewModifiedLambertProjection(3); + + std::vector idxs = {10}; + int err = arr->eraseTuples(idxs); + REQUIRE(err == -100); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ModifiedLambertProjectionArrayTest::DeepCopy", "[EbsdLib][ModifiedLambertProjectionArrayTest]") +{ + auto arr = ModifiedLambertProjectionArray::New(); + arr->fillArrayWithNewModifiedLambertProjection(4); + arr->setName("Original"); + + auto copy = arr->deepCopy(); + REQUIRE(copy != nullptr); + REQUIRE(copy->getNumberOfTuples() == 4); + + // Verify independence: modify original, check copy is unaffected + arr->resizeTuples(1); + REQUIRE(copy->getNumberOfTuples() == 4); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ModifiedLambertProjectionArrayTest::ClearAll", "[EbsdLib][ModifiedLambertProjectionArrayTest]") +{ + auto arr = ModifiedLambertProjectionArray::New(); + arr->fillArrayWithNewModifiedLambertProjection(5); + arr->clearAll(); + REQUIRE(arr->getNumberOfTuples() == 0); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ModifiedLambertProjectionArrayTest::NumberOfComponents", "[EbsdLib][ModifiedLambertProjectionArrayTest]") +{ + auto arr = ModifiedLambertProjectionArray::New(); + REQUIRE(arr->getNumberOfComponents() == 1); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ModifiedLambertProjectionArrayTest::Rank", "[EbsdLib][ModifiedLambertProjectionArrayTest]") +{ + auto arr = ModifiedLambertProjectionArray::New(); + REQUIRE(arr->getRank() == 1); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ModifiedLambertProjectionArrayTest::ComponentDimensions", "[EbsdLib][ModifiedLambertProjectionArrayTest]") +{ + auto arr = ModifiedLambertProjectionArray::New(); + std::vector dims = arr->getComponentDimensions(); + REQUIRE(dims.size() == 1); + REQUIRE(dims[0] == 1); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ModifiedLambertProjectionArrayTest::TypeAsString", "[EbsdLib][ModifiedLambertProjectionArrayTest]") +{ + auto arr = ModifiedLambertProjectionArray::New(); + REQUIRE(arr->getTypeAsString() == "ModifiedLambertProjectionArray"); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ModifiedLambertProjectionArrayTest::NameOfClass", "[EbsdLib][ModifiedLambertProjectionArrayTest]") +{ + auto arr = ModifiedLambertProjectionArray::New(); + REQUIRE(arr->getNameOfClass() == "ModifiedLambertProjectionArray"); + REQUIRE(ModifiedLambertProjectionArray::ClassName() == "ModifiedLambertProjectionArray"); +} diff --git a/Source/Test/ModifiedLambertProjectionTest.cpp b/Source/Test/ModifiedLambertProjectionTest.cpp new file mode 100644 index 0000000..d372b7a --- /dev/null +++ b/Source/Test/ModifiedLambertProjectionTest.cpp @@ -0,0 +1,127 @@ +#include + +#include "EbsdLib/Utilities/ModifiedLambertProjection.h" + +#include + +using namespace ebsdlib; + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ModifiedLambertProjectionTest::InitializeSquares", "[EbsdLib][ModifiedLambertProjectionTest]") +{ + auto mlp = ModifiedLambertProjection::New(); + REQUIRE(mlp != nullptr); + + int dim = 10; + float sphereRadius = 1.0f; + mlp->initializeSquares(dim, sphereRadius); + + REQUIRE(mlp->getDimension() == dim); + REQUIRE(mlp->getSphereRadius() == Approx(sphereRadius)); + CHECK(mlp->getStepSize() > 0.0f); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ModifiedLambertProjectionTest::SquareArrays", "[EbsdLib][ModifiedLambertProjectionTest]") +{ + auto mlp = ModifiedLambertProjection::New(); + int dim = 8; + float sphereRadius = 1.0f; + mlp->initializeSquares(dim, sphereRadius); + + auto northSquare = mlp->getNorthSquare(); + auto southSquare = mlp->getSouthSquare(); + REQUIRE(northSquare != nullptr); + REQUIRE(southSquare != nullptr); + + // Size should be dim * dim + REQUIRE(northSquare->getNumberOfTuples() == static_cast(dim * dim)); + REQUIRE(southSquare->getNumberOfTuples() == static_cast(dim * dim)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ModifiedLambertProjectionTest::SetGetValue", "[EbsdLib][ModifiedLambertProjectionTest]") +{ + auto mlp = ModifiedLambertProjection::New(); + int dim = 8; + mlp->initializeSquares(dim, 1.0f); + + // Set and get a value in the north square + mlp->setValue(ModifiedLambertProjection::NorthSquare, 0, 42.0); + REQUIRE(mlp->getValue(ModifiedLambertProjection::NorthSquare, 0) == Approx(42.0)); + + // Set and get a value in the south square + mlp->setValue(ModifiedLambertProjection::SouthSquare, 5, 99.0); + REQUIRE(mlp->getValue(ModifiedLambertProjection::SouthSquare, 5) == Approx(99.0)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ModifiedLambertProjectionTest::AddValue", "[EbsdLib][ModifiedLambertProjectionTest]") +{ + auto mlp = ModifiedLambertProjection::New(); + int dim = 8; + mlp->initializeSquares(dim, 1.0f); + + mlp->setValue(ModifiedLambertProjection::NorthSquare, 3, 10.0); + mlp->addValue(ModifiedLambertProjection::NorthSquare, 3, 5.0); + REQUIRE(mlp->getValue(ModifiedLambertProjection::NorthSquare, 3) == Approx(15.0)); + + mlp->addValue(ModifiedLambertProjection::NorthSquare, 3, 2.5); + REQUIRE(mlp->getValue(ModifiedLambertProjection::NorthSquare, 3) == Approx(17.5)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ModifiedLambertProjectionTest::NormalizeSquares", "[EbsdLib][ModifiedLambertProjectionTest]") +{ + auto mlp = ModifiedLambertProjection::New(); + int dim = 4; + mlp->initializeSquares(dim, 1.0f); + + // Set some values in both squares + for(int i = 0; i < dim * dim; i++) + { + mlp->setValue(ModifiedLambertProjection::NorthSquare, i, 1.0); + mlp->setValue(ModifiedLambertProjection::SouthSquare, i, 1.0); + } + + mlp->normalizeSquares(); + + // After normalization, each hemisphere sums to 1.0 independently + double northTotal = 0.0; + double southTotal = 0.0; + for(int i = 0; i < dim * dim; i++) + { + northTotal += mlp->getValue(ModifiedLambertProjection::NorthSquare, i); + southTotal += mlp->getValue(ModifiedLambertProjection::SouthSquare, i); + } + REQUIRE(northTotal == Approx(1.0).margin(1.0e-6)); + REQUIRE(southTotal == Approx(1.0).margin(1.0e-6)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ModifiedLambertProjectionTest::GetSquareCoord_NorthPole", "[EbsdLib][ModifiedLambertProjectionTest]") +{ + auto mlp = ModifiedLambertProjection::New(); + int dim = 16; + mlp->initializeSquares(dim, 1.0f); + + // North pole (0,0,1) should return north square (true) + float northPole[3] = {0.0f, 0.0f, 1.0f}; + float sqCoord[2] = {0.0f, 0.0f}; + bool isNorth = mlp->getSquareCoord(northPole, sqCoord); + REQUIRE(isNorth == true); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ModifiedLambertProjectionTest::GetSquareCoord_SouthPole", "[EbsdLib][ModifiedLambertProjectionTest]") +{ + auto mlp = ModifiedLambertProjection::New(); + int dim = 16; + mlp->initializeSquares(dim, 1.0f); + + // South pole (0,0,-1) should return south square (false) + float southPole[3] = {0.0f, 0.0f, -1.0f}; + float sqCoord[2] = {0.0f, 0.0f}; + bool isNorth = mlp->getSquareCoord(southPole, sqCoord); + REQUIRE(isNorth == false); +} diff --git a/Source/Test/OrientationMathTest.cpp b/Source/Test/OrientationMathTest.cpp new file mode 100644 index 0000000..eac353c --- /dev/null +++ b/Source/Test/OrientationMathTest.cpp @@ -0,0 +1,212 @@ +#include + +#include "EbsdLib/Core/OrientationMath.h" +#include "EbsdLib/Math/EbsdLibMath.h" + +#include +#include + +using namespace ebsdlib; + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::OrientationMathTest::MetricTensorCubic", "[EbsdLib][OrientationMathTest]") +{ + // Cubic: a=b=c=1, alpha=beta=gamma=90 degrees + float mt[3][3] = {}; + float alpha = static_cast(M_PI_2); + float beta = static_cast(M_PI_2); + float gamma = static_cast(M_PI_2); + + OrientationMath::MetricTensorFromLatticeParameters(1.0f, 1.0f, 1.0f, alpha, beta, gamma, mt); + + // For cubic: metric tensor = identity + REQUIRE(mt[0][0] == Approx(1.0f).margin(1e-5f)); + REQUIRE(mt[1][1] == Approx(1.0f).margin(1e-5f)); + REQUIRE(mt[2][2] == Approx(1.0f).margin(1e-5f)); + REQUIRE(mt[0][1] == Approx(0.0f).margin(1e-5f)); + REQUIRE(mt[0][2] == Approx(0.0f).margin(1e-5f)); + REQUIRE(mt[1][0] == Approx(0.0f).margin(1e-5f)); + REQUIRE(mt[1][2] == Approx(0.0f).margin(1e-5f)); + REQUIRE(mt[2][0] == Approx(0.0f).margin(1e-5f)); + REQUIRE(mt[2][1] == Approx(0.0f).margin(1e-5f)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::OrientationMathTest::MetricTensorCubicScaled", "[EbsdLib][OrientationMathTest]") +{ + // Cubic with a=b=c=2 + float mt[3][3] = {}; + float alpha = static_cast(M_PI_2); + float beta = static_cast(M_PI_2); + float gamma = static_cast(M_PI_2); + + OrientationMath::MetricTensorFromLatticeParameters(2.0f, 2.0f, 2.0f, alpha, beta, gamma, mt); + + // For cubic a=2: diagonal = a^2 = 4 + REQUIRE(mt[0][0] == Approx(4.0f).margin(1e-5f)); + REQUIRE(mt[1][1] == Approx(4.0f).margin(1e-5f)); + REQUIRE(mt[2][2] == Approx(4.0f).margin(1e-5f)); + REQUIRE(mt[0][1] == Approx(0.0f).margin(1e-5f)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::OrientationMathTest::MetricTensorSymmetric", "[EbsdLib][OrientationMathTest]") +{ + // The metric tensor should always be symmetric + float mt[3][3] = {}; + float alpha = 1.2f; + float beta = 1.3f; + float gamma = 1.1f; + + OrientationMath::MetricTensorFromLatticeParameters(2.0f, 3.0f, 4.0f, alpha, beta, gamma, mt); + + REQUIRE(mt[0][1] == Approx(mt[1][0]).margin(1e-5f)); + REQUIRE(mt[0][2] == Approx(mt[2][0]).margin(1e-5f)); + REQUIRE(mt[1][2] == Approx(mt[2][1]).margin(1e-5f)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::OrientationMathTest::RootTensorCubic", "[EbsdLib][OrientationMathTest]") +{ + float rt[3][3] = {}; + float alpha = static_cast(M_PI_2); + float beta = static_cast(M_PI_2); + float gamma = static_cast(M_PI_2); + + OrientationMath::RootTensorFromLatticeParameters(1.0f, 1.0f, 1.0f, alpha, beta, gamma, rt); + + // For cubic with 90-degree angles: + // rt[0][0] = a*sin(beta)*sin(gamma) = 1*1*1 = 1 + REQUIRE(rt[0][0] == Approx(1.0f).margin(1e-5f)); + // rt[1][1] = b*sin(alpha) = 1*1 = 1 + REQUIRE(rt[1][1] == Approx(1.0f).margin(1e-5f)); + // rt[2][2] = c = 1 + REQUIRE(rt[2][2] == Approx(1.0f).margin(1e-5f)); + // Off-diagonals should be ~0 for cubic + REQUIRE(rt[0][1] == Approx(0.0f).margin(1e-5f)); + REQUIRE(rt[0][2] == Approx(0.0f).margin(1e-5f)); + REQUIRE(rt[1][2] == Approx(0.0f).margin(1e-5f)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::OrientationMathTest::MillerBravaisToMillerDirection", "[EbsdLib][OrientationMathTest]") +{ + // [2 -1 -1 0] -> [2-(-1), -1-(-1), 0] = [3, 0, 0] + int32_t millerBravais[4] = {2, -1, -1, 0}; + int32_t miller[3] = {}; + + OrientationMath::MillerBravaisToMillerDirection(millerBravais, miller); + REQUIRE(miller[0] == 3); + REQUIRE(miller[1] == 0); + REQUIRE(miller[2] == 0); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::OrientationMathTest::MillerToMillerBravaisDirection", "[EbsdLib][OrientationMathTest]") +{ + int32_t miller[3] = {1, 0, 0}; + int32_t millerBravais[4] = {}; + + OrientationMath::MillerToMillerBravaisDirection(miller, millerBravais); + + // U = (2*1 - 0)/3 = 0 (truncated from 0.66) + // V = (2*0 - 1)/3 = 0 (truncated from -0.33) + // T = -(1+0)/3 = 0 (truncated from -0.33) + // W = 0 + // Check that U + V + T = 0 approximately (Miller-Bravais constraint) + REQUIRE(millerBravais[0] + millerBravais[1] + millerBravais[2] == 0); + REQUIRE(millerBravais[3] == 0); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::OrientationMathTest::MillerBravaisToMillerPlane", "[EbsdLib][OrientationMathTest]") +{ + // For planes: (H K I L) -> (H K L), I is dropped + int32_t millerBravais[4] = {1, 0, -1, 2}; + int32_t miller[3] = {}; + + OrientationMath::MillerBravaisToMillerPlane(millerBravais, miller); + REQUIRE(miller[0] == 1); + REQUIRE(miller[1] == 0); + REQUIRE(miller[2] == 2); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::OrientationMathTest::MillerToMillerBravaisPlane", "[EbsdLib][OrientationMathTest]") +{ + int32_t miller[3] = {1, 1, 0}; + int32_t millerBravais[4] = {}; + + OrientationMath::MillerToMillerBravaisPlane(miller, millerBravais); + + REQUIRE(millerBravais[0] == 1); + REQUIRE(millerBravais[1] == 1); + REQUIRE(millerBravais[2] == -2); // -(H + K) = -(1+1) = -2 + REQUIRE(millerBravais[3] == 0); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::OrientationMathTest::MillerBravaisPlaneRoundTrip", "[EbsdLib][OrientationMathTest]") +{ + // Round-trip: Miller -> MillerBravais -> Miller should give back the original + int32_t millerOrig[3] = {1, 2, 3}; + int32_t millerBravais[4] = {}; + int32_t millerRecovered[3] = {}; + + OrientationMath::MillerToMillerBravaisPlane(millerOrig, millerBravais); + OrientationMath::MillerBravaisToMillerPlane(millerBravais, millerRecovered); + + REQUIRE(millerRecovered[0] == millerOrig[0]); + REQUIRE(millerRecovered[1] == millerOrig[1]); + REQUIRE(millerRecovered[2] == millerOrig[2]); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::OrientationMathTest::MillerBravaisPlaneConstraint", "[EbsdLib][OrientationMathTest]") +{ + // The Miller-Bravais plane indices must satisfy H + K + I = 0 + int32_t miller[3] = {2, 1, 0}; + int32_t millerBravais[4] = {}; + + OrientationMath::MillerToMillerBravaisPlane(miller, millerBravais); + + REQUIRE(millerBravais[0] + millerBravais[1] + millerBravais[2] == 0); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::OrientationMathTest::MillerBravaisDirectionKnownValues", "[EbsdLib][OrientationMathTest]") +{ + // Known crystallographic direction: [1 1 -2 0] in MB -> [1-(-2), 1-(-2), 0] = [3, 3, 0] in Miller + int32_t millerBravais[4] = {1, 1, -2, 0}; + int32_t miller[3] = {}; + + OrientationMath::MillerBravaisToMillerDirection(millerBravais, miller); + REQUIRE(miller[0] == 3); + REQUIRE(miller[1] == 3); + REQUIRE(miller[2] == 0); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::OrientationMathTest::MetricTensorHexagonal", "[EbsdLib][OrientationMathTest]") +{ + // Hexagonal: a=b, c different, alpha=beta=90, gamma=120 degrees + float mt[3][3] = {}; + float a = 2.0f; + float c = 3.0f; + float alpha = static_cast(M_PI_2); + float beta = static_cast(M_PI_2); + float gamma = static_cast(2.0 * M_PI / 3.0); // 120 degrees in radians + + OrientationMath::MetricTensorFromLatticeParameters(a, a, c, alpha, beta, gamma, mt); + + // g11 = a^2 = 4 + REQUIRE(mt[0][0] == Approx(4.0f).margin(1e-4f)); + // g22 = b^2 = 4 + REQUIRE(mt[1][1] == Approx(4.0f).margin(1e-4f)); + // g33 = c^2 = 9 + REQUIRE(mt[2][2] == Approx(9.0f).margin(1e-4f)); + // g12 = a*b*cos(gamma) = 4*cos(120) = 4*(-0.5) = -2 + REQUIRE(mt[0][1] == Approx(-2.0f).margin(1e-4f)); + // g13 = a*c*cos(beta) = 2*3*cos(90) = 0 + REQUIRE(mt[0][2] == Approx(0.0f).margin(1e-4f)); +} diff --git a/Source/Test/OrientationTransformationTest.cpp b/Source/Test/OrientationTransformationTest.cpp new file mode 100644 index 0000000..6517636 --- /dev/null +++ b/Source/Test/OrientationTransformationTest.cpp @@ -0,0 +1,195 @@ +#include + +#include "EbsdLib/Orientation/AxisAngle.hpp" +#include "EbsdLib/Orientation/Cubochoric.hpp" +#include "EbsdLib/Orientation/Euler.hpp" +#include "EbsdLib/Orientation/Homochoric.hpp" +#include "EbsdLib/Orientation/OrientationMatrix.hpp" +#include "EbsdLib/Orientation/Quaternion.hpp" +#include "EbsdLib/Orientation/Rodrigues.hpp" + +#include + +using namespace ebsdlib; + +// ============================================================================= +// Euler <-> OrientationMatrix +// ============================================================================= + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::OrientationTransformationTest::eu2om_Identity", "[EbsdLib][OrientationTransformationTest]") +{ + // Identity Euler (0,0,0) should produce identity 3x3 matrix + EulerDType eu(0.0, 0.0, 0.0); + auto om = eu.toOrientationMatrix(); + + // Diagonal should be 1 + CHECK(om[0] == Approx(1.0).margin(1.0e-10)); + CHECK(om[4] == Approx(1.0).margin(1.0e-10)); + CHECK(om[8] == Approx(1.0).margin(1.0e-10)); + + // Off-diagonal should be 0 + CHECK(om[1] == Approx(0.0).margin(1.0e-10)); + CHECK(om[2] == Approx(0.0).margin(1.0e-10)); + CHECK(om[3] == Approx(0.0).margin(1.0e-10)); + CHECK(om[5] == Approx(0.0).margin(1.0e-10)); + CHECK(om[6] == Approx(0.0).margin(1.0e-10)); + CHECK(om[7] == Approx(0.0).margin(1.0e-10)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::OrientationTransformationTest::om2eu_Identity", "[EbsdLib][OrientationTransformationTest]") +{ + // Identity matrix should produce Euler (0,0,0) + OrientationMatrixDType om(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0); + auto eu = om.toEuler(); + + CHECK(eu[0] == Approx(0.0).margin(1.0e-10)); + CHECK(eu[1] == Approx(0.0).margin(1.0e-10)); + CHECK(eu[2] == Approx(0.0).margin(1.0e-10)); +} + +// ============================================================================= +// Euler <-> Quaternion +// ============================================================================= + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::OrientationTransformationTest::eu2qu_Identity", "[EbsdLib][OrientationTransformationTest]") +{ + // Identity Euler -> identity quaternion (0,0,0,1) in VectorScalar layout + EulerDType eu(0.0, 0.0, 0.0); + auto qu = eu.toQuaternion(); + + // VectorScalar: [x, y, z, w] where identity is (0,0,0,1) + CHECK(qu[0] == Approx(0.0).margin(1.0e-10)); + CHECK(qu[1] == Approx(0.0).margin(1.0e-10)); + CHECK(qu[2] == Approx(0.0).margin(1.0e-10)); + CHECK(qu[3] == Approx(1.0).margin(1.0e-10)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::OrientationTransformationTest::qu2eu_Identity", "[EbsdLib][OrientationTransformationTest]") +{ + // Identity quaternion -> Euler (0,0,0) + QuatD qu = QuatD::identity(); + auto eu = qu.toEuler(); + + CHECK(eu[0] == Approx(0.0).margin(1.0e-10)); + CHECK(eu[1] == Approx(0.0).margin(1.0e-10)); + CHECK(eu[2] == Approx(0.0).margin(1.0e-10)); +} + +// ============================================================================= +// Euler -> AxisAngle +// ============================================================================= + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::OrientationTransformationTest::eu2ax_Identity", "[EbsdLib][OrientationTransformationTest]") +{ + // Identity Euler -> axis-angle with angle = 0 + EulerDType eu(0.0, 0.0, 0.0); + auto ax = eu.toAxisAngle(); + + // The angle (4th component) should be 0 + CHECK(ax[3] == Approx(0.0).margin(1.0e-10)); +} + +// ============================================================================= +// AxisAngle -> OrientationMatrix +// ============================================================================= + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::OrientationTransformationTest::ax2om_90degZ", "[EbsdLib][OrientationTransformationTest]") +{ + // 90-degree rotation about Z-axis + // axis = (0,0,1), angle = pi/2 + AxisAngleDType ax(0.0, 0.0, 1.0, M_PI / 2.0); + auto om = ax.toOrientationMatrix(); + + // Expected rotation matrix for passive 90-deg rotation about Z: + // | 0 -1 0 | + // | 1 0 0 | + // | 0 0 1 | + CHECK(om[0] == Approx(0.0).margin(1.0e-10)); + CHECK(om[1] == Approx(-1.0).margin(1.0e-10)); + CHECK(om[2] == Approx(0.0).margin(1.0e-10)); + CHECK(om[3] == Approx(1.0).margin(1.0e-10)); + CHECK(om[4] == Approx(0.0).margin(1.0e-10)); + CHECK(om[5] == Approx(0.0).margin(1.0e-10)); + CHECK(om[6] == Approx(0.0).margin(1.0e-10)); + CHECK(om[7] == Approx(0.0).margin(1.0e-10)); + CHECK(om[8] == Approx(1.0).margin(1.0e-10)); +} + +// ============================================================================= +// Rodrigues -> Homochoric +// ============================================================================= + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::OrientationTransformationTest::ro2ho_Zero", "[EbsdLib][OrientationTransformationTest]") +{ + // Zero Rodrigues vector (identity rotation) -> zero homochoric + RodriguesDType ro(0.0, 0.0, 1.0, 0.0); // axis=(0,0,1), length=0 + auto ho = ro.toHomochoric(); + + CHECK(ho[0] == Approx(0.0).margin(1.0e-10)); + CHECK(ho[1] == Approx(0.0).margin(1.0e-10)); + CHECK(ho[2] == Approx(0.0).margin(1.0e-10)); +} + +// ============================================================================= +// Homochoric -> Cubochoric +// ============================================================================= + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::OrientationTransformationTest::ho2cu_Zero", "[EbsdLib][OrientationTransformationTest]") +{ + // Zero homochoric -> zero cubochoric + HomochoricDType ho(0.0, 0.0, 0.0); + auto cu = ho.toCubochoric(); + + CHECK(cu[0] == Approx(0.0).margin(1.0e-10)); + CHECK(cu[1] == Approx(0.0).margin(1.0e-10)); + CHECK(cu[2] == Approx(0.0).margin(1.0e-10)); +} + +// ============================================================================= +// Round-trip tests with known analytical values +// ============================================================================= + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::OrientationTransformationTest::Euler_OM_RoundTrip", "[EbsdLib][OrientationTransformationTest]") +{ + // Non-trivial Euler angles + EulerDType eu(0.5, 0.7, 1.2); + auto om = eu.toOrientationMatrix(); + auto euBack = om.toEuler(); + + CHECK(euBack[0] == Approx(eu[0]).margin(1.0e-8)); + CHECK(euBack[1] == Approx(eu[1]).margin(1.0e-8)); + CHECK(euBack[2] == Approx(eu[2]).margin(1.0e-8)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::OrientationTransformationTest::Euler_Quat_RoundTrip", "[EbsdLib][OrientationTransformationTest]") +{ + EulerDType eu(1.0, 0.5, 2.0); + auto qu = eu.toQuaternion(); + auto euBack = qu.toEuler(); + + CHECK(euBack[0] == Approx(eu[0]).margin(1.0e-8)); + CHECK(euBack[1] == Approx(eu[1]).margin(1.0e-8)); + CHECK(euBack[2] == Approx(eu[2]).margin(1.0e-8)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::OrientationTransformationTest::Euler_AxisAngle_RoundTrip", "[EbsdLib][OrientationTransformationTest]") +{ + EulerDType eu(0.3, 1.2, 0.8); + auto ax = eu.toAxisAngle(); + auto euBack = ax.toEuler(); + + CHECK(euBack[0] == Approx(eu[0]).margin(1.0e-6)); + CHECK(euBack[1] == Approx(eu[1]).margin(1.0e-6)); + CHECK(euBack[2] == Approx(eu[2]).margin(1.0e-6)); +} diff --git a/Source/Test/PhaseTest.cpp b/Source/Test/PhaseTest.cpp new file mode 100644 index 0000000..2d696f4 --- /dev/null +++ b/Source/Test/PhaseTest.cpp @@ -0,0 +1,207 @@ +#include + +#include "EbsdLib/IO/BrukerNano/EspritPhase.h" +#include "EbsdLib/IO/HKL/CtfPhase.h" +#include "EbsdLib/IO/TSL/AngPhase.h" + +#include +#include + +using namespace ebsdlib; + +// ============================================================================= +// AngPhase Tests +// ============================================================================= + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::PhaseTest::AngPhase_Construction", "[EbsdLib][PhaseTest]") +{ + auto phase = AngPhase::New(); + REQUIRE(phase != nullptr); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::PhaseTest::AngPhase_MaterialName", "[EbsdLib][PhaseTest]") +{ + auto phase = AngPhase::New(); + phase->setMaterialName("Iron"); + REQUIRE(phase->getMaterialName() == "Iron"); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::PhaseTest::AngPhase_Formula", "[EbsdLib][PhaseTest]") +{ + auto phase = AngPhase::New(); + phase->setFormula("Fe"); + REQUIRE(phase->getFormula() == "Fe"); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::PhaseTest::AngPhase_Symmetry", "[EbsdLib][PhaseTest]") +{ + auto phase = AngPhase::New(); + phase->setSymmetry(43); + REQUIRE(phase->getSymmetry() == 43); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::PhaseTest::AngPhase_PhaseIndex", "[EbsdLib][PhaseTest]") +{ + auto phase = AngPhase::New(); + phase->setPhaseIndex(1); + REQUIRE(phase->getPhaseIndex() == 1); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::PhaseTest::AngPhase_LatticeConstants", "[EbsdLib][PhaseTest]") +{ + auto phase = AngPhase::New(); + // Pre-allocate lattice constants vector (the setters index directly) + phase->setLatticeConstants(std::vector(6, 0.0f)); + phase->setLatticeConstantA(3.6f); + phase->setLatticeConstantB(3.6f); + phase->setLatticeConstantC(3.6f); + phase->setLatticeConstantAlpha(90.0f); + phase->setLatticeConstantBeta(90.0f); + phase->setLatticeConstantGamma(90.0f); + + auto lc = phase->getLatticeConstants(); + REQUIRE(lc.size() == 6); + REQUIRE(lc[0] == Approx(3.6f)); + REQUIRE(lc[1] == Approx(3.6f)); + REQUIRE(lc[2] == Approx(3.6f)); + REQUIRE(lc[3] == Approx(90.0f)); + REQUIRE(lc[4] == Approx(90.0f)); + REQUIRE(lc[5] == Approx(90.0f)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::PhaseTest::AngPhase_ParseLatticeConstants", "[EbsdLib][PhaseTest]") +{ + auto phase = AngPhase::New(); + std::vector tokens = {"LatticeConstants", "3.600", "3.600", "3.600", "90.000", "90.000", "90.000"}; + phase->parseLatticeConstants(tokens); + + auto lc = phase->getLatticeConstants(); + REQUIRE(lc.size() == 6); + REQUIRE(lc[0] == Approx(3.6f)); + REQUIRE(lc[1] == Approx(3.6f)); + REQUIRE(lc[2] == Approx(3.6f)); + REQUIRE(lc[3] == Approx(90.0f)); + REQUIRE(lc[4] == Approx(90.0f)); + REQUIRE(lc[5] == Approx(90.0f)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::PhaseTest::AngPhase_ParseHKLFamilies", "[EbsdLib][PhaseTest]") +{ + auto phase = AngPhase::New(); + std::vector tokens = {"hklFamilies", "1", "1", "1", "1", "0.000000", "1"}; + phase->parseHKLFamilies(tokens); + + auto families = phase->getHKLFamilies(); + REQUIRE(families.size() == 1); + REQUIRE(families[0]->h == 1); + REQUIRE(families[0]->k == 1); + REQUIRE(families[0]->l == 1); +} + +// ============================================================================= +// CtfPhase Tests +// ============================================================================= + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::PhaseTest::CtfPhase_Construction", "[EbsdLib][PhaseTest]") +{ + auto phase = CtfPhase::New(); + REQUIRE(phase != nullptr); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::PhaseTest::CtfPhase_PhaseName", "[EbsdLib][PhaseTest]") +{ + auto phase = CtfPhase::New(); + phase->setPhaseName("Nickel"); + REQUIRE(phase->getPhaseName() == "Nickel"); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::PhaseTest::CtfPhase_PhaseIndex", "[EbsdLib][PhaseTest]") +{ + auto phase = CtfPhase::New(); + phase->setPhaseIndex(2); + REQUIRE(phase->getPhaseIndex() == 2); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::PhaseTest::CtfPhase_ParsePhase", "[EbsdLib][PhaseTest]") +{ + auto phase = CtfPhase::New(); + // Format: lattice_constants(;sep) TAB lattice_angles(;sep) TAB name TAB laue_group + std::string line = "3.524;3.524;3.524\t90.000;90.000;90.000\tNickel\t11"; + phase->parsePhase(line); + + REQUIRE(phase->getPhaseName() == "Nickel"); + + auto lc = phase->getLatticeConstants(); + REQUIRE(lc.size() == 6); + REQUIRE(lc[0] == Approx(3.524f)); + REQUIRE(lc[1] == Approx(3.524f)); + REQUIRE(lc[2] == Approx(3.524f)); + REQUIRE(lc[3] == Approx(90.0f)); + REQUIRE(lc[4] == Approx(90.0f)); + REQUIRE(lc[5] == Approx(90.0f)); + + REQUIRE(phase->getLaueGroup() == Ctf::LG_Cubic_High); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::PhaseTest::CtfPhase_SpaceGroup", "[EbsdLib][PhaseTest]") +{ + auto phase = CtfPhase::New(); + phase->setSpaceGroup(225); + REQUIRE(phase->getSpaceGroup() == 225); +} + +// ============================================================================= +// EspritPhase Tests +// ============================================================================= + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::PhaseTest::EspritPhase_Construction", "[EbsdLib][PhaseTest]") +{ + auto phase = EspritPhase::New(); + REQUIRE(phase != nullptr); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::PhaseTest::EspritPhase_Name", "[EbsdLib][PhaseTest]") +{ + auto phase = EspritPhase::New(); + phase->setName("Aluminum"); + REQUIRE(phase->getName() == "Aluminum"); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::PhaseTest::EspritPhase_Formula", "[EbsdLib][PhaseTest]") +{ + auto phase = EspritPhase::New(); + phase->setFormula("Al"); + REQUIRE(phase->getFormula() == "Al"); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::PhaseTest::EspritPhase_SpaceGroup", "[EbsdLib][PhaseTest]") +{ + auto phase = EspritPhase::New(); + phase->setSpaceGroup("Fm-3m"); + REQUIRE(phase->getSpaceGroup() == "Fm-3m"); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::PhaseTest::EspritPhase_PhaseIndex", "[EbsdLib][PhaseTest]") +{ + auto phase = EspritPhase::New(); + phase->setPhaseIndex(3); + REQUIRE(phase->getPhaseIndex() == 3); +} diff --git a/Source/Test/PoleFigureDataTest.cpp b/Source/Test/PoleFigureDataTest.cpp new file mode 100644 index 0000000..02ce179 --- /dev/null +++ b/Source/Test/PoleFigureDataTest.cpp @@ -0,0 +1,104 @@ +#include + +#include "EbsdLib/Utilities/PoleFigureData.h" + +#include + +using namespace ebsdlib; + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::PoleFigureDataTest::DefaultConstruction", "[EbsdLib][PoleFigureDataTest]") +{ + PoleFigureData pfd; + REQUIRE(pfd.imageSize[0] == 0); + REQUIRE(pfd.imageSize[1] == 0); + REQUIRE(pfd.kernelRadius[0] == 3); + REQUIRE(pfd.kernelRadius[1] == 3); + REQUIRE(pfd.xData.empty()); + REQUIRE(pfd.yData.empty()); + REQUIRE(pfd.label.empty()); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::PoleFigureDataTest::ParameterizedConstruction", "[EbsdLib][PoleFigureDataTest]") +{ + std::vector xData = {1.0f, 2.0f, 3.0f}; + std::vector yData = {4.0f, 5.0f, 6.0f}; + int32_t kernelRad[2] = {5, 7}; + int32_t size[2] = {256, 512}; + + PoleFigureData pfd(xData, yData, "TestLabel", kernelRad, size); + + REQUIRE(pfd.xData.size() == 3); + REQUIRE(pfd.yData.size() == 3); + REQUIRE(pfd.xData[0] == Approx(1.0f)); + REQUIRE(pfd.yData[2] == Approx(6.0f)); + REQUIRE(pfd.label == "TestLabel"); + REQUIRE(pfd.kernelRadius[0] == 5); + REQUIRE(pfd.kernelRadius[1] == 7); + REQUIRE(pfd.imageSize[0] == 256); + REQUIRE(pfd.imageSize[1] == 512); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::PoleFigureDataTest::CopyConstruction", "[EbsdLib][PoleFigureDataTest]") +{ + std::vector xData = {1.0f, 2.0f}; + std::vector yData = {3.0f, 4.0f}; + int32_t kernelRad[2] = {5, 7}; + int32_t size[2] = {100, 200}; + + PoleFigureData original(xData, yData, "Original", kernelRad, size); + PoleFigureData copy(original); + + REQUIRE(copy.xData == original.xData); + REQUIRE(copy.yData == original.yData); + REQUIRE(copy.label == original.label); + REQUIRE(copy.imageSize[0] == original.imageSize[0]); + REQUIRE(copy.imageSize[1] == original.imageSize[1]); + REQUIRE(copy.kernelRadius[0] == original.kernelRadius[0]); + REQUIRE(copy.kernelRadius[1] == original.kernelRadius[1]); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::PoleFigureDataTest::AssignmentOperator", "[EbsdLib][PoleFigureDataTest]") +{ + std::vector xData = {10.0f, 20.0f}; + std::vector yData = {30.0f, 40.0f}; + int32_t kernelRad[2] = {2, 4}; + int32_t size[2] = {64, 128}; + + PoleFigureData original(xData, yData, "Assigned", kernelRad, size); + PoleFigureData assigned; + assigned = original; + + REQUIRE(assigned.xData == original.xData); + REQUIRE(assigned.yData == original.yData); + REQUIRE(assigned.label == "Assigned"); + REQUIRE(assigned.imageSize[0] == 64); + REQUIRE(assigned.imageSize[1] == 128); + REQUIRE(assigned.kernelRadius[0] == 2); + REQUIRE(assigned.kernelRadius[1] == 4); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::PoleFigureDataTest::DataIndependenceAfterCopy", "[EbsdLib][PoleFigureDataTest]") +{ + std::vector xData = {1.0f}; + std::vector yData = {2.0f}; + int32_t kernelRad[2] = {3, 3}; + int32_t size[2] = {50, 50}; + + PoleFigureData original(xData, yData, "Original", kernelRad, size); + PoleFigureData copy(original); + + // Modify original + original.xData.push_back(99.0f); + original.label = "Modified"; + original.imageSize[0] = 999; + + // Copy should be unaffected + REQUIRE(copy.xData.size() == 1); + REQUIRE(copy.label == "Original"); + REQUIRE(copy.imageSize[0] == 50); +} diff --git a/Source/Test/PoleFigureUtilitiesTest.cpp b/Source/Test/PoleFigureUtilitiesTest.cpp new file mode 100644 index 0000000..3d87b9e --- /dev/null +++ b/Source/Test/PoleFigureUtilitiesTest.cpp @@ -0,0 +1,138 @@ +#include + +#include "EbsdLib/Core/EbsdDataArray.hpp" +#include "EbsdLib/Utilities/PoleFigureUtilities.h" + +#include +#include + +using namespace ebsdlib; + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::PoleFigureUtilitiesTest::PoleFigureConfiguration_t_Fields", "[EbsdLib][PoleFigureUtilitiesTest]") +{ + PoleFigureConfiguration_t config; + config.eulers = nullptr; + config.imageDim = 256; + config.lambertDim = 64; + config.numColors = 32; + config.minScale = 0.0; + config.maxScale = 1.0; + config.sphereRadius = 1.0f; + config.discrete = false; + config.discreteHeatMap = false; + config.colorMap = "Default"; + config.labels = {"A", "B", "C"}; + config.order = {0, 1, 2}; + config.phaseName = "Phase1"; + config.FlipFinalImage = true; + + REQUIRE(config.imageDim == 256); + REQUIRE(config.lambertDim == 64); + REQUIRE(config.numColors == 32); + REQUIRE(config.minScale == Approx(0.0)); + REQUIRE(config.maxScale == Approx(1.0)); + REQUIRE(config.sphereRadius == Approx(1.0f)); + REQUIRE(config.discrete == false); + REQUIRE(config.discreteHeatMap == false); + REQUIRE(config.colorMap == "Default"); + REQUIRE(config.labels.size() == 3); + REQUIRE(config.order.size() == 3); + REQUIRE(config.phaseName == "Phase1"); + REQUIRE(config.FlipFinalImage == true); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::PoleFigureUtilitiesTest::CreateColorImage_UniformData", "[EbsdLib][PoleFigureUtilitiesTest]") +{ + int width = 16; + int height = 16; + int nColors = 16; + + // Create uniform intensity data (all zeros) + auto data = DoubleArrayType::CreateArray(static_cast(width * height), {1ULL}, "Intensity", true); + data->initializeWithZeros(); + + auto image = PoleFigureUtilities::CreateColorImage(data.get(), width, height, nColors, "TestImage", 0.0, 1.0); + + REQUIRE(image != nullptr); + REQUIRE(image->getNumberOfTuples() == static_cast(width * height)); + REQUIRE(image->getNumberOfComponents() == 4); // RGBA +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::PoleFigureUtilitiesTest::CreateColorImage_GradientData", "[EbsdLib][PoleFigureUtilitiesTest]") +{ + int width = 16; + int height = 16; + int nColors = 16; + + // Create gradient intensity data (linear ramp 0 to 1) + auto data = DoubleArrayType::CreateArray(static_cast(width * height), {1ULL}, "GradientIntensity", true); + double numPixels = static_cast(width * height); + for(size_t i = 0; i < data->getNumberOfTuples(); i++) + { + data->setValue(i, static_cast(i) / numPixels); + } + + auto image = PoleFigureUtilities::CreateColorImage(data.get(), width, height, nColors, "GradientImage", 0.0, 1.0); + + REQUIRE(image != nullptr); + REQUIRE(image->getNumberOfTuples() == static_cast(width * height)); + + // Verify that the image has non-zero content (at least some pixel is colored) + bool hasNonZero = false; + for(size_t i = 0; i < image->getNumberOfTuples(); i++) + { + uint8_t* pixel = image->getTuplePointer(i); + if(pixel[0] != 0 || pixel[1] != 0 || pixel[2] != 0) + { + hasNonZero = true; + break; + } + } + REQUIRE(hasNonZero); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::PoleFigureUtilitiesTest::GeneratePoleFigureRgbaImageImpl_Operator", "[EbsdLib][PoleFigureUtilitiesTest]") +{ + int imageDim = 16; + int nColors = 16; + + auto intensity = DoubleArrayType::CreateArray(static_cast(imageDim * imageDim), {1ULL}, "Intensity", true); + for(size_t i = 0; i < intensity->getNumberOfTuples(); i++) + { + intensity->setValue(i, static_cast(i) / static_cast(intensity->getNumberOfTuples())); + } + + PoleFigureConfiguration_t config; + config.eulers = nullptr; + config.imageDim = imageDim; + config.numColors = nColors; + config.minScale = 0.0; + config.maxScale = 1.0; + config.sphereRadius = 1.0f; + config.discrete = false; + config.discreteHeatMap = false; + + std::vector cDims = {4}; + auto rgba = UInt8ArrayType::CreateArray(static_cast(imageDim * imageDim), cDims, "RGBA", true); + rgba->initializeWithZeros(); + + GeneratePoleFigureRgbaImageImpl impl(intensity.get(), &config, rgba.get()); + impl(); + + // Verify the image was populated (at least some non-zero pixel) + bool hasNonZero = false; + for(size_t i = 0; i < rgba->getNumberOfTuples(); i++) + { + uint8_t* pixel = rgba->getTuplePointer(i); + if(pixel[0] != 0 || pixel[1] != 0 || pixel[2] != 0 || pixel[3] != 0) + { + hasNonZero = true; + break; + } + } + REQUIRE(hasNonZero); +} diff --git a/Source/Test/StereographicProjectionTest.cpp b/Source/Test/StereographicProjectionTest.cpp new file mode 100644 index 0000000..54f177e --- /dev/null +++ b/Source/Test/StereographicProjectionTest.cpp @@ -0,0 +1,103 @@ +#include + +#include "EbsdLib/Math/Matrix3X1.hpp" +#include "EbsdLib/Utilities/ComputeStereographicProjection.h" + +#include +#include + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::StereographicProjectionTest::StereoToSpherical_Origin", "[EbsdLib][StereographicProjectionTest]") +{ + // Origin (0,0) in stereographic should map to north pole (0,0,1) + auto result = stereographic::utils::StereoToSpherical(0.0, 0.0); + REQUIRE(result[0] == Approx(0.0)); + REQUIRE(result[1] == Approx(0.0)); + REQUIRE(result[2] == Approx(1.0)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::StereographicProjectionTest::SphericalToStereo_NorthPole", "[EbsdLib][StereographicProjectionTest]") +{ + // North pole (0,0,1) should map to origin in stereographic + auto result = stereographic::utils::SphericalToStereo(0.0, 0.0, 1.0); + REQUIRE(result[0] == Approx(0.0)); + REQUIRE(result[1] == Approx(0.0)); + REQUIRE(result[2] == Approx(0.0)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::StereographicProjectionTest::RoundTrip_StereoToSphericalToStereo", "[EbsdLib][StereographicProjectionTest]") +{ + // Round-trip: SphericalToStereo(StereoToSpherical(p)) should approximately equal p + std::vector> testPoints = {{0.1, 0.2}, {-0.3, 0.4}, {0.5, -0.1}, {0.0, 0.3}}; + + for(const auto& [x, y] : testPoints) + { + ebsdlib::Matrix3X1 stereo(x, y, 0.0); + auto sphere = stereographic::utils::StereoToSpherical(stereo); + auto backToStereo = stereographic::utils::SphericalToStereo(sphere); + CHECK(backToStereo[0] == Approx(x).margin(1.0e-10)); + CHECK(backToStereo[1] == Approx(y).margin(1.0e-10)); + } +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::StereographicProjectionTest::RoundTrip_SphericalToStereoToSpherical", "[EbsdLib][StereographicProjectionTest]") +{ + // Round-trip for unit sphere points with z > -1 + std::vector> spherePoints = { + {0.0, 0.0, 1.0}, // north pole + {1.0 / std::sqrt(3.0), 1.0 / std::sqrt(3.0), 1.0 / std::sqrt(3.0)}, // (1,1,1)/sqrt(3) + {1.0 / std::sqrt(2.0), 0.0, 1.0 / std::sqrt(2.0)}, // 45 degrees + {std::sqrt(3.0) / 2.0, 0.0, 0.5}, // 60 degrees from pole + }; + + for(const auto& pt : spherePoints) + { + auto stereo = stereographic::utils::SphericalToStereo(pt); + auto backToSphere = stereographic::utils::StereoToSpherical(stereo); + CHECK(backToSphere[0] == Approx(pt[0]).margin(1.0e-10)); + CHECK(backToSphere[1] == Approx(pt[1]).margin(1.0e-10)); + CHECK(backToSphere[2] == Approx(pt[2]).margin(1.0e-10)); + } +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::StereographicProjectionTest::EquatorPoint", "[EbsdLib][StereographicProjectionTest]") +{ + // Equator point (1,0,0): stereo = x/(1+z) = 1/(1+0) = 1, y/(1+z) = 0 + auto stereo = stereographic::utils::SphericalToStereo(1.0, 0.0, 0.0); + REQUIRE(stereo[0] == Approx(1.0)); + REQUIRE(stereo[1] == Approx(0.0)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::StereographicProjectionTest::TransformUnitSphereToStereographicCoords", "[EbsdLib][StereographicProjectionTest]") +{ + std::vector> points; + + // Northern hemisphere point + points.push_back({0.0f, 0.0f, 1.0f}); // north pole + + // Southern hemisphere point + points.push_back({0.0f, 0.0f, -1.0f}); // south pole - z < 0 uses (1-z) denominator + + // Equator + points.push_back({1.0f, 0.0f, 0.0f}); + + auto result = stereographic::utils::TransformUnitSphereToStereographicCoords(points); + REQUIRE(result.size() == 3); + + // North pole -> origin + CHECK(result[0][0] == Approx(0.0f).margin(1.0e-6f)); + CHECK(result[0][1] == Approx(0.0f).margin(1.0e-6f)); + + // South pole with southern projection: x/(1-z) = 0/(1-(-1)) = 0 + CHECK(result[1][0] == Approx(0.0f).margin(1.0e-6f)); + CHECK(result[1][1] == Approx(0.0f).margin(1.0e-6f)); + + // Equator (1,0,0) with z>=0: x/(1+z) = 1/(1+0) = 1 + CHECK(result[2][0] == Approx(1.0f).margin(1.0e-6f)); + CHECK(result[2][1] == Approx(0.0f).margin(1.0e-6f)); +} diff --git a/Source/Test/TexturePresetTest.cpp b/Source/Test/TexturePresetTest.cpp new file mode 100644 index 0000000..5fd2c45 --- /dev/null +++ b/Source/Test/TexturePresetTest.cpp @@ -0,0 +1,88 @@ +#include + +#include "EbsdLib/Core/EbsdLibConstants.h" +#include "EbsdLib/Texture/TexturePreset.h" + +#include + +using namespace ebsdlib; + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::TexturePresetTest::Construction", "[EbsdLib][TexturePresetTest]") +{ + auto preset = TexturePreset::New(CrystalStructure::Cubic_High, "TestPreset", 10.0, 20.0, 30.0); + REQUIRE(preset != nullptr); + REQUIRE(preset->getCrystalStructure() == CrystalStructure::Cubic_High); + REQUIRE(preset->getName() == "TestPreset"); + REQUIRE(preset->getEuler1() == Approx(10.0)); + REQUIRE(preset->getEuler2() == Approx(20.0)); + REQUIRE(preset->getEuler3() == Approx(30.0)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::TexturePresetTest::SetterGetterRoundTrip", "[EbsdLib][TexturePresetTest]") +{ + auto preset = TexturePreset::New(); + REQUIRE(preset != nullptr); + + preset->setCrystalStructure(CrystalStructure::Hexagonal_High); + REQUIRE(preset->getCrystalStructure() == CrystalStructure::Hexagonal_High); + + preset->setName("MyPreset"); + REQUIRE(preset->getName() == "MyPreset"); + + preset->setEuler1(45.0); + REQUIRE(preset->getEuler1() == Approx(45.0)); + + preset->setEuler2(90.0); + REQUIRE(preset->getEuler2() == Approx(90.0)); + + preset->setEuler3(135.0); + REQUIRE(preset->getEuler3() == Approx(135.0)); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::TexturePresetTest::CubicTexturePresets", "[EbsdLib][TexturePresetTest]") +{ + auto textures = CubicTexturePresets::getTextures(); + + // Should return exactly 14 presets + REQUIRE(textures.size() == 14); + + // All should have Cubic_High crystal structure + for(const auto& preset : textures) + { + REQUIRE(preset->getCrystalStructure() == CrystalStructure::Cubic_High); + } + + // Verify known names are present + std::vector expectedNames = {"Brass", "S", "Copper", "Goss", "Cube"}; + for(const auto& expectedName : expectedNames) + { + bool found = false; + for(const auto& preset : textures) + { + if(preset->getName() == expectedName) + { + found = true; + break; + } + } + CHECK(found); + } +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::TexturePresetTest::HexTexturePresets", "[EbsdLib][TexturePresetTest]") +{ + auto textures = HexTexturePresets::getTextures(); + // Currently returns empty vector + REQUIRE(textures.empty()); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::TexturePresetTest::NullPointer", "[EbsdLib][TexturePresetTest]") +{ + auto ptr = TexturePreset::NullPointer(); + REQUIRE(ptr == nullptr); +} diff --git a/Source/Test/TiffWriterTest.cpp b/Source/Test/TiffWriterTest.cpp new file mode 100644 index 0000000..2a3455a --- /dev/null +++ b/Source/Test/TiffWriterTest.cpp @@ -0,0 +1,103 @@ +#include + +#include "EbsdLib/Test/EbsdLibTestFileLocations.h" +#include "EbsdLib/Utilities/TiffWriter.h" + +#include +#include +#include +#include + +namespace fs = std::filesystem; + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::TiffWriterTest::WriteColorImage_RGB", "[EbsdLib][TiffWriterTest]") +{ + std::string outputPath = UnitTest::TestTempDir + "TiffWriterTest_RGB.tif"; + + int32_t width = 4; + int32_t height = 4; + uint16_t samplesPerPixel = 3; + + // Create a solid red image + std::vector data(width * height * samplesPerPixel, 0); + for(int i = 0; i < width * height; i++) + { + data[i * 3 + 0] = 255; // R + data[i * 3 + 1] = 0; // G + data[i * 3 + 2] = 0; // B + } + + auto [errCode, errMsg] = TiffWriter::WriteColorImage(outputPath, width, height, samplesPerPixel, data.data()); + REQUIRE(errCode == 0); + REQUIRE(fs::exists(outputPath)); + REQUIRE(fs::file_size(outputPath) > 0); + +#if REMOVE_TEST_FILES + fs::remove(outputPath); +#endif +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::TiffWriterTest::WriteColorImage_RGBA", "[EbsdLib][TiffWriterTest]") +{ + std::string outputPath = UnitTest::TestTempDir + "TiffWriterTest_RGBA.tif"; + + int32_t width = 4; + int32_t height = 4; + uint16_t samplesPerPixel = 4; + + // Create a solid green image with alpha + std::vector data(width * height * samplesPerPixel, 0); + for(int i = 0; i < width * height; i++) + { + data[i * 4 + 0] = 0; // R + data[i * 4 + 1] = 255; // G + data[i * 4 + 2] = 0; // B + data[i * 4 + 3] = 255; // A + } + + auto [errCode, errMsg] = TiffWriter::WriteColorImage(outputPath, width, height, samplesPerPixel, data.data()); + REQUIRE(errCode == 0); + REQUIRE(fs::exists(outputPath)); + REQUIRE(fs::file_size(outputPath) > 0); + +#if REMOVE_TEST_FILES + fs::remove(outputPath); +#endif +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::TiffWriterTest::WriteGrayScaleImage", "[EbsdLib][TiffWriterTest]") +{ + std::string outputPath = UnitTest::TestTempDir + "TiffWriterTest_Gray.tif"; + + int32_t width = 4; + int32_t height = 4; + + // Create a mid-gray image + std::vector data(width * height, 128); + + auto [errCode, errMsg] = TiffWriter::WriteGrayScaleImage(outputPath, width, height, data.data()); + REQUIRE(errCode == 0); + REQUIRE(fs::exists(outputPath)); + REQUIRE(fs::file_size(outputPath) > 0); + +#if REMOVE_TEST_FILES + fs::remove(outputPath); +#endif +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::TiffWriterTest::WriteColorImage_InvalidPath", "[EbsdLib][TiffWriterTest]") +{ + std::string outputPath = "/nonexistent_directory_xyz/TiffWriterTest_Invalid.tif"; + + int32_t width = 4; + int32_t height = 4; + uint16_t samplesPerPixel = 3; + std::vector data(width * height * samplesPerPixel, 0); + + auto [errCode, errMsg] = TiffWriter::WriteColorImage(outputPath, width, height, samplesPerPixel, data.data()); + REQUIRE(errCode != 0); +} diff --git a/Source/Test/ToolTipGeneratorTest.cpp b/Source/Test/ToolTipGeneratorTest.cpp new file mode 100644 index 0000000..327b1e0 --- /dev/null +++ b/Source/Test/ToolTipGeneratorTest.cpp @@ -0,0 +1,102 @@ +#include + +#include "EbsdLib/Utilities/ToolTipGenerator.h" + +#include + +using namespace ebsdlib; + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ToolTipGeneratorTest::DefaultConstruction", "[EbsdLib][ToolTipGeneratorTest]") +{ + ToolTipGenerator gen; + REQUIRE(gen.getRowColorStr() == "#FFFCEA"); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ToolTipGeneratorTest::SetGetRowColor", "[EbsdLib][ToolTipGeneratorTest]") +{ + ToolTipGenerator gen; + gen.setRowColorStr("#FF0000"); + REQUIRE(gen.getRowColorStr() == "#FF0000"); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ToolTipGeneratorTest::AddTitle_GenerateHTML", "[EbsdLib][ToolTipGeneratorTest]") +{ + ToolTipGenerator gen; + gen.addTitle("Test Title"); + std::string html = gen.generateHTML(); + + REQUIRE_FALSE(html.empty()); + REQUIRE(html.find("") != std::string::npos); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ToolTipGeneratorTest::AddSpacer_GenerateHTML", "[EbsdLib][ToolTipGeneratorTest]") +{ + ToolTipGenerator gen; + gen.addSpacer(); + std::string html = gen.generateHTML(); + + REQUIRE_FALSE(html.empty()); + // Spacer rows have empty td cells + REQUIRE(html.find("") != std::string::npos); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ToolTipGeneratorTest::Clear", "[EbsdLib][ToolTipGeneratorTest]") +{ + ToolTipGenerator gen; + gen.addTitle("Title1"); + gen.addValue("Name", "Value"); + gen.clear(); + std::string html = gen.generateHTML(); + + // After clear, only the trailing spacer should be present (no Title1 or Name) + REQUIRE(html.find("Title1") == std::string::npos); + REQUIRE(html.find("Name") == std::string::npos); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ToolTipGeneratorTest::Append", "[EbsdLib][ToolTipGeneratorTest]") +{ + ToolTipGenerator gen1; + gen1.addTitle("First"); + + ToolTipGenerator gen2; + gen2.addTitle("Second"); + + gen1.append(gen2); + std::string html = gen1.generateHTML(); + + REQUIRE(html.find("First") != std::string::npos); + REQUIRE(html.find("Second") != std::string::npos); +} + +// ----------------------------------------------------------------------------- +TEST_CASE("ebsdlib::ToolTipGeneratorTest::RowColorInHTML", "[EbsdLib][ToolTipGeneratorTest]") +{ + ToolTipGenerator gen; + gen.setRowColorStr("#AABBCC"); + gen.addTitle("Color Test"); + std::string html = gen.generateHTML(); + + REQUIRE(html.find("#AABBCC") != std::string::npos); +}