diff --git a/image_analysis/IndexAndRefine.cpp b/image_analysis/IndexAndRefine.cpp index 4952cb71..5d4c5c4b 100644 --- a/image_analysis/IndexAndRefine.cpp +++ b/image_analysis/IndexAndRefine.cpp @@ -14,9 +14,18 @@ #include "scale_merge/ReindexAmbiguity.h" #include "scale_merge/ScaleOnTheFly.h" -IndexAndRefine::IndexAndRefine(const DiffractionExperiment &x, IndexerThreadPool *indexer, bool retain_outcomes) +namespace { + // Iterations the offline geometry refinement is allowed, standing in for the 40 ms the online path + // spends. Ceres' own default is 50; the per-image problem is small and converges well inside that, + // so this bounds the pathological case rather than the normal one. + constexpr int OFFLINE_REFINE_ITERATIONS = 50; +} + +IndexAndRefine::IndexAndRefine(const DiffractionExperiment &x, IndexerThreadPool *indexer, + bool retain_outcomes, bool real_time) : index_ice_rings(x.GetIndexingSettings().GetIndexIceRings()), retain_outcomes_(retain_outcomes), + real_time(real_time), experiment(x), geom_(x.GetDiffractionGeometry()), indexer_(indexer), @@ -258,7 +267,11 @@ void IndexAndRefine::RefineGeometryIfNeeded(DataMessage &msg, IndexAndRefine::In // enters the fit at the loose first tolerance is arbitrarily indexed noise. Weight every spot by // how strong it is for its resolution so those contribute without dragging the orientation. .weight_spots_by_confidence = true, - .max_time = 0.04 // 40 ms is max allowed time for the operation + // Online: 40 ms is the real budget per image, so the wall clock is the right bound even though + // it makes the answer depend on machine load. Offline: bound the same refinement by iterations + // instead, so reprocessing the same file twice gives the same lattice. + .max_time = 0.04, + .max_iterations = real_time ? 0 : OFFLINE_REFINE_ITERATIONS }; @@ -346,7 +359,8 @@ void IndexAndRefine::RefineGeometryIfNeeded(DataMessage &msg, IndexAndRefine::In .refine_unit_cell = false, .refine_rotation_axis = false, .index_ice_rings = experiment.GetIndexingSettings().GetIndexIceRings(), - .max_time = 0.02 + .max_time = 0.02, + .max_iterations = real_time ? 0 : OFFLINE_REFINE_ITERATIONS / 2 }; XtalOptimizerRotationOnly(data_extra, msg.spots, 0.1); el = data_extra.latt; diff --git a/image_analysis/IndexAndRefine.h b/image_analysis/IndexAndRefine.h index 54d09b51..7c9268de 100644 --- a/image_analysis/IndexAndRefine.h +++ b/image_analysis/IndexAndRefine.h @@ -34,6 +34,7 @@ class IndexAndRefine { // whole-run integration_outcome vector is not retained (viewer live/interactive use, which never // scales the accumulated run). rugnux/receiver keep it true so ScaleAllImages/merge have the data. const bool retain_outcomes_; + const bool real_time; // see the constructor const DiffractionExperiment& experiment; const DiffractionGeometry geom_; @@ -92,7 +93,12 @@ class IndexAndRefine { std::optional RotationAngle(int64_t image) const; // mid-exposure angle for the indexer public: - IndexAndRefine(const DiffractionExperiment &x, IndexerThreadPool *indexer, bool retain_outcomes = true); + // real_time: bound the per-image geometry refinement by WALL CLOCK, as online acquisition must - + // it has a real per-image budget. Offline (rugnux, the viewer) passes false and the refinement is + // bounded by iteration count instead, so the same file reprocesses to the same answer regardless of + // what else the machine was doing. + IndexAndRefine(const DiffractionExperiment &x, IndexerThreadPool *indexer, bool retain_outcomes = true, + bool real_time = false); void AddImageToRotationIndexer(DataMessage &msg); void ForceRotationIndexerLattice(const CrystalLattice& lattice); diff --git a/image_analysis/geom_refinement/XtalOptimizer.cpp b/image_analysis/geom_refinement/XtalOptimizer.cpp index e35d4e8e..c769e871 100644 --- a/image_analysis/geom_refinement/XtalOptimizer.cpp +++ b/image_analysis/geom_refinement/XtalOptimizer.cpp @@ -345,7 +345,10 @@ bool XtalOptimizerInternal(XtalOptimizerData &data, ceres::Solver::Options options; options.linear_solver_type = ceres::DENSE_QR; options.minimizer_progress_to_stdout = false; - options.max_solver_time_in_seconds = data.max_time; + if (data.max_iterations > 0) + options.max_num_iterations = data.max_iterations; + else + options.max_solver_time_in_seconds = data.max_time; options.logging_type = ceres::LoggingType::SILENT; options.num_threads = num_threads; // usually 1 (called from many threads); caller may raise it ceres::Solver::Summary summary; @@ -477,7 +480,10 @@ bool XtalOptimizerRotationOnly(XtalOptimizerData &data, ceres::Solver::Options options; options.linear_solver_type = ceres::DENSE_QR; options.minimizer_progress_to_stdout = false; - options.max_solver_time_in_seconds = data.max_time; + if (data.max_iterations > 0) + options.max_num_iterations = data.max_iterations; + else + options.max_solver_time_in_seconds = data.max_time; options.logging_type = ceres::LoggingType::SILENT; options.num_threads = 1; diff --git a/image_analysis/geom_refinement/XtalOptimizer.h b/image_analysis/geom_refinement/XtalOptimizer.h index 105a2faf..35375be6 100644 --- a/image_analysis/geom_refinement/XtalOptimizer.h +++ b/image_analysis/geom_refinement/XtalOptimizer.h @@ -35,7 +35,12 @@ struct XtalOptimizerData { // spot list they have already selected, it is the per-image refinement that gets the raw list. bool weight_spots_by_confidence = false; + // Stopping rule. max_iterations > 0 bounds the solver by ITERATIONS, which is reproducible; + // otherwise it is bounded by max_time, wall-clock seconds, which is not - the same image refines + // to a different answer on a busier machine. Online acquisition needs the wall-clock bound because + // its budget is real; offline reprocessing wants the reproducible one. float max_time = 1.0; + int max_iterations = 0; std::optional axis; diff --git a/receiver/JFJochReceiver.cpp b/receiver/JFJochReceiver.cpp index 59bec65d..17ef151d 100644 --- a/receiver/JFJochReceiver.cpp +++ b/receiver/JFJochReceiver.cpp @@ -31,7 +31,7 @@ JFJochReceiver::JFJochReceiver(const DiffractionExperiment &in_experiment, pixel_mask(in_pixel_mask), // retain_outcomes=false for now: online scaling/merge at the end is not enabled yet, so the // whole-run integration_outcome is unused here. Flip back to true when online scaling lands. - indexer(experiment, indexing_thread_pool, false) { + indexer(experiment, indexing_thread_pool, false, /*real_time=*/true) { logger.Info("Initializing receiver"); // Ensure there is nothing running for now if (!image_buffer.Finalize(std::chrono::seconds(1)))