diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..2dffa0db8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,52 @@ +# Repository instructions + +## Building and testing + +This project is CMake-based but uses the Elements build system. Never run CMake +commands directly; use the project's Makefile through Elements instead. + +Run the following commands from the repository root as needed: + +- `make` — build the project. +- `make install` — install the build products. +- `make tests` — build and run the tests. + +Always run `make install` before integration runs such as executing `sourcextractor++` +in `tests/sim12`, and use the installed executable and environment. + +## Coding guidelines + +- Follow `.editorconfig`: UTF-8, LF line endings, spaces, two-space indentation, + a maximum line length of 120 characters, no trailing whitespace, and a final + newline. Python uses four-space indentation; Makefiles use tabs. +- Match the surrounding code when conventions vary. Keep changes focused and + avoid reformatting unrelated code. +- In C++, use `PascalCase` for classes, `camelCase` for methods and functions, + `snake_case` for local variables and parameters, and `m_snake_case` for data + members. Match existing public API names. +- Put opening braces on the same line as class, function, and control-flow + declarations. Use spaces after control-flow keywords (`if (...)`, `for (...)`). + Keep namespace contents and class access labels unindented, as in nearby code. +- Always use braces `{}` around C++ `if`, `else if`, `else`, and `for` bodies, + even for a single statement. Do not use unbraced conditionals or `for` loops, + even when nearby code does so. +- Follow the module layout: public headers in `//`, implementation + files in `/src/lib/`, and tests in `/tests/src/`. Use `.h` and + `.cpp` files named after the class where applicable. +- Use header guards and module-qualified project includes such as + `"SEFramework/Task/SourceTask.h"`. Follow nearby include grouping and guard names. + Keep code in its existing namespace (`SourceXtractor`, `ModelFitting`, etc.). +- Use constructor initializer lists for members, `explicit` for single-argument + constructors where implicit conversion is not intended, and `override` for + overridden virtual methods. Mark read-only member functions `const` and use + const references for read-only access to nontrivial objects. +- Follow existing ownership patterns with `std::unique_ptr` and `std::shared_ptr`; + use RAII for resource management. Use `auto` where the type is clear from context. +- Preserve numerical types and precision choices in the surrounding code, + including the aliases in `SEUtils/Types.h` where used. +- Preserve existing license headers. Use Doxygen comments for public C++ API + documentation and explain non-obvious algorithms, assumptions, and units. +- Follow the existing Boost.Test style for C++ tests: `*_test.cpp` files, + `BOOST_AUTO_TEST_SUITE`, and test cases or fixtures matching nearby tests. +- In Python, use `snake_case` functions and variables, `PascalCase` classes, and + docstrings consistent with the surrounding module. diff --git a/SEImplementation/CMakeLists.txt b/SEImplementation/CMakeLists.txt index 02cc4bafc..f953b1bf2 100644 --- a/SEImplementation/CMakeLists.txt +++ b/SEImplementation/CMakeLists.txt @@ -187,6 +187,10 @@ elements_add_unit_test(JacobianSource_test tests/src/Plugin/Jacobian/JacobianSou LINK_LIBRARIES SEImplementation TYPE Boost) if (NOT WITHOUT_MODELFITTING) +elements_add_unit_test(FlexibleModelFittingWeight_test + tests/src/Plugin/FlexibleModelFitting/FlexibleModelFittingWeight_test.cpp + LINK_LIBRARIES SEImplementation + TYPE Boost) elements_add_unit_test(MoffatModelFitting_test tests/src/Plugin/MoffatModelFitting/MoffatModelFitting_test.cpp LINK_LIBRARIES SEImplementation TYPE Boost) diff --git a/SEImplementation/SEImplementation/Plugin/FlexibleModelFitting/FlexibleModelFittingIterativeTask.h b/SEImplementation/SEImplementation/Plugin/FlexibleModelFitting/FlexibleModelFittingIterativeTask.h index 839c2ef51..39815d0ed 100644 --- a/SEImplementation/SEImplementation/Plugin/FlexibleModelFitting/FlexibleModelFittingIterativeTask.h +++ b/SEImplementation/SEImplementation/Plugin/FlexibleModelFitting/FlexibleModelFittingIterativeTask.h @@ -73,6 +73,7 @@ class FlexibleModelFittingIterativeTask : public GroupTask { void computeProperties(SourceGroupInterface& group) const override; private: + friend struct FlexibleModelFittingWeightFixture; struct SourceState { std::unordered_map parameters_initial_values; std::unordered_map parameters_values; diff --git a/SEImplementation/SEImplementation/Plugin/FlexibleModelFitting/FlexibleModelFittingTask.h b/SEImplementation/SEImplementation/Plugin/FlexibleModelFitting/FlexibleModelFittingTask.h index ad948a8c3..ae81c5dfa 100644 --- a/SEImplementation/SEImplementation/Plugin/FlexibleModelFitting/FlexibleModelFittingTask.h +++ b/SEImplementation/SEImplementation/Plugin/FlexibleModelFitting/FlexibleModelFittingTask.h @@ -54,6 +54,7 @@ class FlexibleModelFittingTask : public GroupTask { void computeProperties(SourceGroupInterface& group) const override; private: + friend struct FlexibleModelFittingWeightFixture; bool isFrameValid(SourceGroupInterface& group, int frame_index) const; diff --git a/SEImplementation/SEImplementation/Plugin/MoffatModelFitting/MoffatModelFittingTask.h b/SEImplementation/SEImplementation/Plugin/MoffatModelFitting/MoffatModelFittingTask.h index 1520b57ac..34107e3cd 100644 --- a/SEImplementation/SEImplementation/Plugin/MoffatModelFitting/MoffatModelFittingTask.h +++ b/SEImplementation/SEImplementation/Plugin/MoffatModelFitting/MoffatModelFittingTask.h @@ -25,6 +25,7 @@ #define _SEIMPLEMENTATION_PLUGIN_MOFFATMODELFITTING_MOFFATMODELFITTINGTASK_H_ #include "SEFramework/Task/SourceTask.h" +#include "SEFramework/Image/VectorImage.h" #include "SEImplementation/Image/ImagePsf.h" @@ -41,6 +42,9 @@ class MoffatModelFittingTask : public SourceTask { void computeProperties(SourceInterface& source) const override; private: + friend struct MoffatModelFittingWeightFixture; + + std::shared_ptr> createWeightImage(SourceInterface& source) const; std::string m_least_squares_engine; unsigned int m_max_iterations; diff --git a/SEImplementation/src/lib/Plugin/FlexibleModelFitting/FlexibleModelFittingIterativeTask.cpp b/SEImplementation/src/lib/Plugin/FlexibleModelFitting/FlexibleModelFittingIterativeTask.cpp index 19b64a5eb..d4d7c8cfa 100644 --- a/SEImplementation/src/lib/Plugin/FlexibleModelFitting/FlexibleModelFittingIterativeTask.cpp +++ b/SEImplementation/src/lib/Plugin/FlexibleModelFitting/FlexibleModelFittingIterativeTask.cpp @@ -308,7 +308,7 @@ std::shared_ptr> FlexibleModelFittingIterativeTask::createW auto ref_coordinates = source.getProperty().getCoordinateSystem(); auto frame_image = frame_images.getLockedImage(LayerSubtractedImage); - auto frame_image_thresholded = frame_images.getLockedImage(LayerThresholdedImage); + auto original_image = frame_images.getLockedImage(LayerOriginalImage); auto variance_map = frame_images.getLockedImage(LayerVarianceMap); const auto& frame_info = source.getProperty(frame_index); @@ -356,7 +356,9 @@ std::shared_ptr> FlexibleModelFittingIterativeTask::createW auto dx = x + rect.getTopLeft().m_x - ellipse.m_x; auto dy = y + rect.getTopLeft().m_y - ellipse.m_y; - if (saturation > 0 && pixel_val > saturation) { + // Saturation is defined before background subtraction, in the original image units. + if (saturation > 0 && original_image->getValue( + rect.getTopLeft().m_x + x, rect.getTopLeft().m_y + y) >= saturation) { weight->at(x, y) = 0; } else if (gain > 0.0 && pixel_val > 0.0) { weight->at(x, y) = sqrt(1.0 / (back_var + pixel_val / gain)); diff --git a/SEImplementation/src/lib/Plugin/FlexibleModelFitting/FlexibleModelFittingTask.cpp b/SEImplementation/src/lib/Plugin/FlexibleModelFitting/FlexibleModelFittingTask.cpp index 4c6555c5e..ead277c03 100644 --- a/SEImplementation/src/lib/Plugin/FlexibleModelFitting/FlexibleModelFittingTask.cpp +++ b/SEImplementation/src/lib/Plugin/FlexibleModelFitting/FlexibleModelFittingTask.cpp @@ -95,7 +95,7 @@ std::shared_ptr> FlexibleModelFittingTask::createWeightImag const auto& frame_images = group.begin()->getProperty(frame_index); auto frame_image = frame_images.getLockedImage(LayerSubtractedImage); - auto frame_image_thresholded = frame_images.getLockedImage(LayerThresholdedImage); + auto original_image = frame_images.getLockedImage(LayerOriginalImage); auto variance_map = frame_images.getLockedImage(LayerVarianceMap); const auto& frame_info = group.begin()->getProperty(frame_index); @@ -109,7 +109,9 @@ std::shared_ptr> FlexibleModelFittingTask::createWeightImag for (int x = 0; x < rect.getWidth(); x++) { auto back_var = variance_map->getValue(rect.getTopLeft().m_x + x, rect.getTopLeft().m_y + y); auto pixel_val = frame_image->getValue(rect.getTopLeft().m_x + x, rect.getTopLeft().m_y + y); - if (saturation > 0 && pixel_val > saturation) { + // Saturation is defined before background subtraction, in the original image units. + if (saturation > 0 && original_image->getValue( + rect.getTopLeft().m_x + x, rect.getTopLeft().m_y + y) >= saturation) { weight->at(x, y) = 0; } else if (gain > 0.0 && pixel_val > 0.0) { diff --git a/SEImplementation/src/lib/Plugin/MoffatModelFitting/MoffatModelFittingTask.cpp b/SEImplementation/src/lib/Plugin/MoffatModelFitting/MoffatModelFittingTask.cpp index 9a315aacc..7e12bfceb 100644 --- a/SEImplementation/src/lib/Plugin/MoffatModelFitting/MoffatModelFittingTask.cpp +++ b/SEImplementation/src/lib/Plugin/MoffatModelFitting/MoffatModelFittingTask.cpp @@ -63,6 +63,7 @@ #include "ModelFitting/Engine/DataVsModelResiduals.h" #include "SEImplementation/Plugin/DetectionFrameSourceStamp/DetectionFrameSourceStamp.h" +#include "SEImplementation/Plugin/DetectionFrameImages/DetectionFrameImages.h" #include "SEImplementation/Plugin/PixelCentroid/PixelCentroid.h" #include "SEImplementation/Property/PixelCoordinateList.h" @@ -150,10 +151,60 @@ struct SourceModel { } +std::shared_ptr> MoffatModelFittingTask::createWeightImage(SourceInterface& source) const { + const auto& stamp = source.getProperty(); + const auto& source_stamp = stamp.getStamp(); + const auto& variance_stamp = stamp.getVarianceStamp(); + const auto& thresholded_stamp = stamp.getThresholdedStamp(); + auto stamp_top_left = stamp.getTopLeft(); + const auto& pixel_coordinates = source.getProperty().getCoordinateList(); + + auto weight = VectorImage::create(source_stamp.getWidth(), source_stamp.getHeight()); + std::fill(weight->getData().begin(), weight->getData().end(), 1); + + for (int y = 0; y < source_stamp.getHeight(); y++) { + for (int x = 0; x < source_stamp.getWidth(); x++) { + weight->at(x, y) = (thresholded_stamp.getValue(x, y) >= 0) ? 0 : 1; + } + } + + for (auto pixel : pixel_coordinates) { + pixel -= stamp_top_left; + weight->at(pixel.m_x, pixel.m_y) = 1; + } + + const auto& detection_frame_info = source.getProperty(); + SeFloat gain = detection_frame_info.getGain(); + SeFloat saturation = detection_frame_info.getSaturation(); + + std::shared_ptr> original_stamp; + if (saturation > 0) { + const auto& frame_images = source.getProperty(); + original_stamp = frame_images.getImageChunk(LayerOriginalImage, + stamp_top_left.m_x, stamp_top_left.m_y, source_stamp.getWidth(), source_stamp.getHeight()); + } + + for (int y = 0; y < source_stamp.getHeight(); y++) { + for (int x = 0; x < source_stamp.getWidth(); x++) { + auto back_var = variance_stamp.getValue(x, y); + // The threshold is defined in original-image units, before background subtraction. + if (saturation > 0 && original_stamp->getValue(x, y) >= saturation) { + weight->at(x, y) = 0; + } else if (weight->at(x, y)>0) { + if (gain > 0.0) { + weight->at(x, y) = sqrt(1.0 / (back_var + source_stamp.getValue(x, y) / gain)); + } else { + weight->at(x, y) = sqrt(1.0 / back_var); // infinite gain + } + } + } + } + + return weight; +} + void MoffatModelFittingTask::computeProperties(SourceInterface& source) const { auto& source_stamp = source.getProperty().getStamp(); - auto& variance_stamp = source.getProperty().getVarianceStamp(); - auto& thresholded_stamp = source.getProperty().getThresholdedStamp(); auto& threshold_map_stamp = source.getProperty().getThresholdMapStamp(); PixelCoordinate stamp_top_left = source.getProperty().getTopLeft(); @@ -207,38 +258,7 @@ void MoffatModelFittingTask::computeProperties(SourceInterface& source) const { std::move(extended_models) }; - auto weight = VectorImage::create(source_stamp.getWidth(), source_stamp.getHeight()); - std::fill(weight->getData().begin(), weight->getData().end(), 1); - - for (int y=0; y < source_stamp.getHeight(); y++) { - for (int x=0; x < source_stamp.getWidth(); x++) { - weight->at(x, y) = (thresholded_stamp.getValue(x, y) >= 0) ? 0 : 1; - } - } - - for (auto pixel : pixel_coordinates) { - pixel -= stamp_top_left; - weight->at(pixel.m_x, pixel.m_y) = 1; - } - - const auto& detection_frame_info = source.getProperty(); - SeFloat gain = detection_frame_info.getGain(); - SeFloat saturation = detection_frame_info.getSaturation(); - - for (int y=0; y < source_stamp.getHeight(); y++) { - for (int x=0; x < source_stamp.getWidth(); x++) { - auto back_var = variance_stamp.getValue(x, y); - if (saturation > 0 && source_stamp.getValue(x, y) >= saturation) { - weight->at(x, y) = 0; - } else if (weight->at(x, y)>0) { - if (gain > 0.0) { - weight->at(x, y) = sqrt(1.0 / (back_var + source_stamp.getValue(x, y) / gain)); - } else { - weight->at(x, y) = sqrt(1.0 / back_var); // infinite gain - } - } - } - } + auto weight = createWeightImage(source); // FIXME we should be able to use the source_stamp Image interface directly auto image = VectorImage::create(source_stamp); diff --git a/SEImplementation/tests/src/Plugin/FlexibleModelFitting/FlexibleModelFittingWeight_test.cpp b/SEImplementation/tests/src/Plugin/FlexibleModelFitting/FlexibleModelFittingWeight_test.cpp new file mode 100644 index 000000000..fc32dd2ae --- /dev/null +++ b/SEImplementation/tests/src/Plugin/FlexibleModelFitting/FlexibleModelFittingWeight_test.cpp @@ -0,0 +1,132 @@ +/** Copyright © 2019 Université de Genève, LMU Munich - Faculty of Physics, IAP-CNRS/Sorbonne Université + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3.0 of the License, or (at your option) + * any later version. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +#include + +#include + +#include "SEFramework/Source/SimpleSource.h" +#include "SEFramework/Source/SimpleSourceGroup.h" +#include "SEImplementation/Plugin/FlexibleModelFitting/FlexibleModelFittingIterativeTask.h" +#include "SEImplementation/Plugin/FlexibleModelFitting/FlexibleModelFittingTask.h" +#include "SEImplementation/Plugin/MeasurementFrameCoordinates/MeasurementFrameCoordinates.h" +#include "SEImplementation/Plugin/MeasurementFrameGroupRectangle/MeasurementFrameGroupRectangle.h" +#include "SEImplementation/Plugin/MeasurementFrameImages/MeasurementFrameImages.h" +#include "SEImplementation/Plugin/MeasurementFrameInfo/MeasurementFrameInfo.h" +#include "SEImplementation/Plugin/MeasurementFrameRectangle/MeasurementFrameRectangle.h" +#include "SEImplementation/Plugin/ReferenceCoordinates/ReferenceCoordinates.h" +#include "SEImplementation/Plugin/WorldCentroid/WorldCentroid.h" + +namespace SourceXtractor { +namespace { + +class WeightTestCoordinates : public CoordinateSystem { +public: + WorldCoordinate imageToWorld(ImageCoordinate coord) const override { + return WorldCoordinate(coord.m_x, coord.m_y); + } + + ImageCoordinate worldToImage(WorldCoordinate) const override { + return ImageCoordinate(2, 2); + } +}; + +} + +struct FlexibleModelFittingWeightFixture { + void checkWeights(bool iterative, SeFloat saturation, SeFloat scale, bool varying_background, SeFloat gain, SeFloat background_level = 30) { + const int size = 5; + auto image = VectorImage::create(size, size); + auto background = VectorImage::create(size, size); + auto variance = VectorImage::create(size, size); + variance->fillValue(4 * scale * scale); + + // Below, exactly at, and above saturation; also exercise nonpositive signal. + const SeFloat pixels[] = {99, 100, 101, 20, 10}; + for (int y = 0; y < size; ++y) { + for (int x = 0; x < size; ++x) { + image->at(x, y) = pixels[x] * scale; + background->at(x, y) = (background_level + (varying_background ? y * 5 : 0)) * scale; + } + } + + auto coordinates = std::make_shared(); + auto frame = std::make_shared( + image, variance, 1e6, coordinates, gain, saturation, 0); + frame->setBackgroundLevel(background, 2 * scale); + auto source = std::make_unique(); + source->setIndexedProperty(0, frame, size, size); + source->setIndexedProperty(0, size, size, gain, saturation, 1e6, 2 * scale); + source->setIndexedProperty(0, coordinates); + source->setProperty(coordinates); + source->setProperty(2, 2); + source->setIndexedProperty(0, ImageCoordinate(1, 1), ImageCoordinate(3, 3)); + + std::shared_ptr> weights; + if (iterative) { + FlexibleModelFittingIterativeTask task("", 1, 1, {}, {}, {}, {}); + weights = task.createWeightImage(*source, 0); + } else { + SimpleSourceGroup group; + group.addSource(std::move(source)); + group.setIndexedProperty(0, PixelCoordinate(0, 0), PixelCoordinate(4, 4)); + FlexibleModelFittingTask task("", 1, 1, {}, {}, {}); + weights = task.createWeightImage(group, 0); + } + + BOOST_REQUIRE_EQUAL(weights->getWidth(), size); + BOOST_REQUIRE_EQUAL(weights->getHeight(), size); + for (int y = 0; y < size; ++y) { + for (int x = 0; x < size; ++x) { + if (saturation > 0 && x >= 1 && x <= 2) { + BOOST_CHECK_EQUAL(weights->getValue(x, y), 0); + } else { + // Shot noise continues to use the background-subtracted signal. + double signal = image->getValue(x, y) - background->getValue(x, y); + double expected_variance = 4 * scale * scale; + if (gain > 0 && signal > 0) { + expected_variance += signal / gain; + } + BOOST_CHECK_CLOSE(weights->getValue(x, y), 1 / std::sqrt(expected_variance), 0.001); + } + } + } + } +}; + +BOOST_FIXTURE_TEST_SUITE(FlexibleModelFittingWeight_test, FlexibleModelFittingWeightFixture) + +BOOST_AUTO_TEST_CASE(saturation_before_background_subtraction) { + for (bool iterative : {true, false}) { + checkWeights(iterative, 100, 1, false, 2); + checkWeights(iterative, 100, 1, false, 2, 0); + checkWeights(iterative, 100, 1, false, 2, -30); + checkWeights(iterative, 100, 1, true, 2); + checkWeights(iterative, 100, 1, true, 0); + checkWeights(iterative, 200, 2, true, 1); + } +} + +BOOST_AUTO_TEST_CASE(disabled_saturation) { + for (bool iterative : {true, false}) { + checkWeights(iterative, 0, 1, true, 2); + checkWeights(iterative, -1, 1, true, 2); + } +} + +BOOST_AUTO_TEST_SUITE_END() + +} diff --git a/SEImplementation/tests/src/Plugin/MoffatModelFitting/MoffatModelFitting_test.cpp b/SEImplementation/tests/src/Plugin/MoffatModelFitting/MoffatModelFitting_test.cpp index 0018fc7ea..ff126e625 100644 --- a/SEImplementation/tests/src/Plugin/MoffatModelFitting/MoffatModelFitting_test.cpp +++ b/SEImplementation/tests/src/Plugin/MoffatModelFitting/MoffatModelFitting_test.cpp @@ -40,6 +40,7 @@ #include "SEImplementation/Plugin/DetectionFrameSourceStamp/DetectionFrameSourceStamp.h" #include "SEImplementation/Plugin/DetectionFrameCoordinates/DetectionFrameCoordinates.h" #include "SEImplementation/Plugin/DetectionFrameInfo/DetectionFrameInfo.h" +#include "SEImplementation/Plugin/DetectionFrameImages/DetectionFrameImages.h" #include "SEImplementation/Plugin/IsophotalFlux/IsophotalFlux.h" #include "SEFramework/Property/DetectionFrame.h" @@ -101,6 +102,7 @@ BOOST_FIXTURE_TEST_CASE(modelfitting_test, MoffatModelFittingFixture) { source->setProperty(500., 0., 1., 0.); source->setProperty(pixel_coordinates); source->setProperty(detection_frame); + source->setProperty(detection_frame, 20, 20); source->setProperty(std::make_shared()); source->setProperty(20, 20, 1, 65000, 1e6, 1); @@ -117,3 +119,65 @@ BOOST_FIXTURE_TEST_CASE(modelfitting_test, MoffatModelFittingFixture) { BOOST_AUTO_TEST_SUITE_END () + +namespace SourceXtractor { + +struct MoffatModelFittingWeightFixture { + void checkWeights(SeFloat saturation, SeFloat background_level) { + auto original = VectorImage::create(12, 10); + original->fillValue(0); + auto stamp = VectorImage::create(4, 3); + auto variance = VectorImage::create(4, 3); + variance->fillValue(4); + auto thresholded = VectorImage::create(4, 3); + thresholded->fillValue(-1); + // Put the stamp away from the image origin to exercise coordinate alignment. + PixelCoordinate top_left(5, 4); + const SeFloat pixels[] = {99, 100, 101, 80}; + for (int y = 0; y < 3; ++y) { + for (int x = 0; x < 4; ++x) { + original->at(top_left.m_x + x, top_left.m_y + y) = pixels[x]; + stamp->at(x, y) = pixels[x] - background_level - y; + } + } + // A neighbour remains excluded; source membership must not override saturation. + thresholded->at(3, 2) = 1; + std::vector source_pixels {PixelCoordinate(6, 4)}; + thresholded->at(1, 0) = 1; + auto frame = std::make_shared(original, std::make_shared()); + SimpleSource source; + source.setProperty(frame, 12, 10); + source.setProperty(stamp, stamp, thresholded, top_left, variance, variance); + source.setProperty(12, 10, 2, saturation, 1e6, 2); + source.setProperty(source_pixels); + MoffatModelFittingTask task("", 1); + auto weights = task.createWeightImage(source); + for (int y = 0; y < 3; ++y) { + for (int x = 0; x < 4; ++x) { + if ((saturation > 0 && (x == 1 || x == 2)) || (x == 3 && y == 2)) { + BOOST_CHECK_EQUAL(weights->getValue(x, y), 0); + } else { + double expected = std::sqrt(1.0 / (4 + stamp->getValue(x, y) / 2)); + BOOST_CHECK_CLOSE(weights->getValue(x, y), expected, 0.001); + } + } + } + } +}; + +BOOST_FIXTURE_TEST_SUITE(MoffatModelFittingWeight_test, MoffatModelFittingWeightFixture) + +BOOST_AUTO_TEST_CASE(original_pixels_determine_saturation) { + checkWeights(100, 30); + checkWeights(100, 0); + checkWeights(100, -30); +} + +BOOST_AUTO_TEST_CASE(disabled_saturation_preserves_weights) { + checkWeights(0, 30); + checkWeights(-1, 30); +} + +BOOST_AUTO_TEST_SUITE_END() + +}