Files
Jungfraujoch/image_analysis/indexing/IndexerThreadPool.h
T
leonarski_fandClaude Opus 5 89962574ef spindle: the severity no longer rides on the indexing seed or on which indexer is configured
The score gated on 60 spots, but the seed escalation stops at the leanest seed that indexes -
30 spots on precisely the clean frames a grid scan produces - so the value was absent exactly
where beamline automation most needs it, and absence maps to "engage": the protocol would have
fired on every good frame, which degenerates the trigger into "always". The floor itself stays
where it was calibrated; what changes is what it gates. When no escalation pass could answer,
one severity-only pass runs over the full spot list - the row search alone, no reduction, no
refinement - purely to produce the number.

The same was true of the indexer choice: only the FFT family computes a row shortlist, so a
deployment configured with the known-cell indexer - the ordinary online stills path - never
produced the score at all. Where the severity-only pass has no row search to run, the severity
is read off the rows of the winning lattice instead, which any indexer produces: the lattice's
shortest few distinct directions, as many as the FFT shortlist resolves in practice, fed through
the same window and scoring with equal magnitudes. The count parity is load-bearing - a worst
case over every enumerable lattice direction fires on 100% of harmless mounts of a generic
triclinic cell against 74% for this selection at theta_max = 15 deg, and an always-firing
trigger decides nothing - while the diad-detection rate stays 1.00 on the monoclinic classes
either way, a dropped axis row being recovered by the pair normals exactly as an invisible one
is. A frame that neither indexed nor reached the spot floor still reports nothing, which is the
honest answer and maps to the recoverable error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EFEJG6WBQv8th4UJFNe53N
2026-09-04 10:59:05 +02:00

94 lines
3.8 KiB
C++

// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <functional>
#include <future>
#include <vector>
#include <optional>
#include <memory>
#include <latch>
#include "../common/JFJochMessages.h"
#include "../common/DiffractionSpot.h"
#include "../common/DiffractionExperiment.h"
#include "Indexer.h"
// When a worker builds its indexers.
//
// Preconstruct (the default, and what the online service needs): every indexer the requested
// algorithm could resolve to is built while the pool is being constructed, so it is resident and
// planned before the first frame arrives. Building one means a cuFFT plan plus ~0.6 GB of device
// allocation, and the first cuFFT call in a process also pays the library's one-time init (0.3 s
// measured); jfjoch_broker cannot have any of that land on the live data path, so holding an indexer
// the resolved algorithm may never dispatch to is an accepted cost there.
//
// OnFirstUse (offline batch - rugnux, jfjoch_viewer): nothing is latency-critical, so the indexer is
// built on the first frame that needs it. The algorithm is only resolved from the frame's
// DiffractionExperiment (rotation -> FFT, stills with a known cell -> FFBIDX), so preconstructing
// leaves each worker holding a fully allocated FFTIndexerGPU - 0.6 GB of cuFFT plan and histograms -
// that can never be dispatched to.
enum class IndexerConstruction { Preconstruct, OnFirstUse };
// Force cuFFT's one-time library initialisation - 0.3 s measured, paid by whichever call happens to
// be first - so it can be got out of the way on a background thread at startup rather than landing in
// the middle of the first pass with nothing overlapping it. No-op without a GPU.
void WarmUpCuFFT();
class IndexerThread {
struct TaskInput {
const DiffractionExperiment &experiment;
const std::vector<Coord> &recip;
const bool severity_only;
};
// Held by value: with IndexerConstruction::OnFirstUse the worker builds its indexer long after
// the constructor returned, and pools are routinely built from a temporary - for instance
// IndexerThreadPool(experiment.GetIndexingSettings()), which returns by value.
const IndexingSettings settings_;
const IndexerConstruction construction_;
bool stop = false;
enum class TaskState {STARTING, IDLE, READY, RUNNING, COMPLETED, ERROR} state = TaskState::STARTING;
std::mutex m;
std::condition_variable c_running;
std::condition_variable c_start;
std::condition_variable c_done;
std::unique_ptr<IndexerResult> result = nullptr;
std::unique_ptr<TaskInput> task_input = nullptr;
std::thread worker_thread;
void Worker(int threadid);
public:
IndexerThread(const IndexingSettings& settings, int threadid, IndexerConstruction construction);
~IndexerThread();
std::unique_ptr<IndexerResult> Run(const DiffractionExperiment &experiment, const std::vector<Coord> &recip,
bool severity_only = false);
void Finalize();
};
class IndexerThreadPool {
std::mutex m;
std::condition_variable c;
std::vector<uint8_t> worker_busy;
size_t worker_free_count;
std::vector<std::unique_ptr<IndexerThread>> tasks;
const int64_t viable_cell_min_spots;
const bool blocking;
int GetFreeWorker();
public:
IndexerThreadPool(const IndexingSettings& settings,
IndexerConstruction construction = IndexerConstruction::Preconstruct);
// severity_only skips indexing and produces just the spindle severity - see Indexer::Run.
IndexerResult Run(const DiffractionExperiment& experiment, const std::vector<Coord>& recip,
bool severity_only = false);
};