calibration: an option to hold the detector tilt fixed
--mode calibration fits five parameters - beam centre, distance and the two PONI tilts - and a program that cannot express a tilted detector has nowhere to put the last two. Dropping them after the fact is worse than never fitting them: the centre and the distance of a tilted fit have already absorbed the tilt, so the flattened geometry is right nowhere. rugnux --no-refine-tilt, the "Refine detector tilt" tick box on the viewer's Calib page and RingOptimizer's refine_tilt argument hold rot1/rot2 at the value the geometry came in with and fit the remaining three. That is the best flat-detector answer, and the one such a program would refine to itself. Measured on a five-distance calibrant series. At short distance the tilt is real and reproducible - three independent fits agreeing to 0.01 deg, radial rms 1.4 -> 0.4 px - and its direct beam agrees with the background beam-centre estimator to 0.05 px, so the tilted model is the physically right one. The pinned fit then displaces the centre 2.6 px to absorb the tilt and lands within 0.03 px of the same place at every distance. Past ~300 mm, where only two rings reach the detector, the tilt is instead under-determined: it comes out with the opposite sign to every short-distance fit and drags the PONI 28 px while the rms does not move (0.960 against 0.962). The existing degeneracy guard only fires on a strictly single ring, so it does not catch that; declining a tilt that does not pay for itself in rms is left for a separate change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MxrrPcxodNiXzhNiECCVp5
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
# Changelog
|
||||
## 1.0.0
|
||||
### 1.0.0-rc.166
|
||||
* `rugnux --mode calibration --no-refine-tilt`, and the "Refine detector tilt" tick box in the viewer's calibration panel, fit the beam centre and the distance with the detector tilt held fixed, for a calibration meant for a program that cannot express a tilted detector.
|
||||
* A miniCBF sweep takes the mounting from the imgCIF axis table its header carries: which laboratory direction the image's columns and rows run along, and which the spindle turns about. Where there is no table, a `+SLOW` on the `Oscillation_axis` line still says the spindle runs along the image's slow direction. All three were previously assumed, and two instruments are not what was assumed.
|
||||
* Naming a miniCBF frame with no directory - a frame in the working directory - finds its sweep instead of reporting that no images were found.
|
||||
* A detector swung out on a 2theta arm is placed where it stands, from the depends_on transformation chain of an NXmx master or the `Detector_2theta` line of a miniCBF header; both were previously read and then ignored.
|
||||
|
||||
@@ -588,7 +588,7 @@ Only pass 2 is written, as the canonical `<prefix>_*` output. Pass 1's merge exi
|
||||
|
||||
Everything above fits the geometry to *Bragg* data, where the beam centre is the weakest parameter: it is gauge-coupled to the crystal orientation, which is why §7.5 restrains it toward the header value and commits only a sub-1 % move. A **powder ring has no orientation to be coupled to**. Where it falls on the detector depends on the geometry and on nothing else, which makes a calibrant — LaB₆, silver behenate, CeO₂, silicon — or even ice an independent constraint on exactly the quantity Bragg data cannot pin.
|
||||
|
||||
The ring positions are matched to the observed rings and the geometry is refined (Ceres, five parameters: beam centre, distance, and the two detector tilts) so that the $|s|$ predicted at each observed ring point matches the ring it belongs to.
|
||||
The ring positions are matched to the observed rings and the geometry is refined (Ceres, five parameters: beam centre, distance, and the two detector tilts) so that the $|s|$ predicted at each observed ring point matches the ring it belongs to. The two tilts can be held fixed (`rugnux --no-refine-tilt`, the viewer's *Refine detector tilt* tick box), leaving a three-parameter fit: a tilt a downstream program cannot express is better left out of the fit than refined and then dropped, since the centre and the distance of a tilted fit have already absorbed it.
|
||||
|
||||
**Calibrants.** LaB₆, silver behenate, CeO₂ and silicon are held as unit cells and their rings enumerated from them. Ice is held as the hexagonal-ice ring positions of §3.3 instead — measured to 1.522 Å, calculated below it — because hexagonal ice is $P6_3/mmc$ with oxygen on $4f$ and enumerating $hkl$ from its cell would emit rings the oxygen sublattice extinguishes. A calibrant is therefore a list of ring $q$ values throughout, not a cell.
|
||||
|
||||
|
||||
@@ -48,6 +48,10 @@ install it.
|
||||
so a fit that sits a couple of pixels off every ring can still report a small one. *Rings* needs
|
||||
the run to be integrated in azimuthal sectors — with the AzInt page's *Azimuthal bins* below 4 the
|
||||
calibration run raises it to 32, as `rugnux --mode calibration` does, and says so.
|
||||
*Refine detector tilt* is ticked by default and fits the two tilts along with the centre and the
|
||||
distance; unticking it holds them where they are, for a calibration meant for a program that
|
||||
cannot express a tilted detector (`rugnux --no-refine-tilt`). It applies to both buttons and to
|
||||
*Analyze dataset*.
|
||||
- **Settings** panel for the geometry, unit cell, spot finding, indexing, azimuthal integration,
|
||||
Bragg integration, scaling, powder calibration and a reference dataset — the same settings the
|
||||
CLI takes.
|
||||
|
||||
@@ -894,6 +894,12 @@ standard error that implies on the centre. That error is *formal*: it measures t
|
||||
points, not whether the rings themselves are trustworthy, so it stays small when a fit goes wrong
|
||||
for a structural reason — one visible ring, or ice that is textured rather than smooth.
|
||||
|
||||
`--no-refine-tilt` holds rot1/rot2 where the header put them and fits only the centre and the
|
||||
distance. The tilt is real and worth measuring, but a program that has nowhere to put one — XDS
|
||||
takes a detector normal to the beam — is better given a geometry that was measured with the tilt
|
||||
pinned than one that was measured tilted and then flattened, because in the tilted fit the centre
|
||||
and the distance have already absorbed the tilt.
|
||||
|
||||
Both the PONI (the point of normal incidence, which is what a `.poni` file stores) and the direct
|
||||
beam (where the beam lands, which is what most other programs call the beam centre) are printed.
|
||||
They differ by *distance* × tan(rot) once the detector is tilted, which on a 0.3° tilt at 300 mm is
|
||||
@@ -949,6 +955,7 @@ Calibration (`--mode calibration`):
|
||||
| --- | --- |
|
||||
| `--calibrant <name>` | Powder standard: `lab6` \| `agbh` \| `ceo2` \| `si` \| `ice` (default `lab6`, case-insensitive) |
|
||||
| `--calibration <txt>` | How the rings are measured: `rings` \| `spots` (default `rings`; see above). `rings` defaults `--azim-phi-bins` to 32 |
|
||||
| `--no-refine-tilt` | Do not refine the detector tilt: hold rot1/rot2 at the header value and fit only the beam centre and the distance, for a calibration handed to a program that cannot express a tilted detector (XDS) |
|
||||
|
||||
Detector mask:
|
||||
|
||||
|
||||
@@ -239,7 +239,8 @@ std::vector<RingClusters> GuessInitialGeometry(DiffractionGeometry &geom, const
|
||||
return cluster_annot;
|
||||
}
|
||||
|
||||
void GuessGeometry(DiffractionGeometry &geom, const std::vector<SpotToSave> &v, const std::vector<float> &ring_q) {
|
||||
void GuessGeometry(DiffractionGeometry &geom, const std::vector<SpotToSave> &v, const std::vector<float> &ring_q,
|
||||
bool refine_tilt) {
|
||||
if (ring_q.empty())
|
||||
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "No calibrant rings given");
|
||||
auto cluster_annot = GuessInitialGeometry(geom, v, 2 * PI / ring_q[0]);
|
||||
@@ -273,7 +274,7 @@ void GuessGeometry(DiffractionGeometry &geom, const std::vector<SpotToSave> &v,
|
||||
}
|
||||
}
|
||||
|
||||
RingOptimizer optimizer(geom);
|
||||
RingOptimizer optimizer(geom, refine_tilt);
|
||||
geom = optimizer.Run(optimizer_input);
|
||||
}
|
||||
|
||||
@@ -294,7 +295,8 @@ std::vector<RingOptimizerInput> AssignSpotsToRings(const DiffractionGeometry &ge
|
||||
return optimizer_input;
|
||||
}
|
||||
|
||||
void OptimizeGeometry(DiffractionGeometry &geom, const std::vector<SpotToSave> &v, const std::vector<float> &ring_q) {
|
||||
RingOptimizer optimizer(geom);
|
||||
void OptimizeGeometry(DiffractionGeometry &geom, const std::vector<SpotToSave> &v, const std::vector<float> &ring_q,
|
||||
bool refine_tilt) {
|
||||
RingOptimizer optimizer(geom, refine_tilt);
|
||||
geom = optimizer.Run(AssignSpotsToRings(geom, v, ring_q));
|
||||
}
|
||||
|
||||
@@ -50,8 +50,11 @@ float GuessDetectorDistance(const DiffractionGeometry& geom, float ring_radius_p
|
||||
// measured rather than enumerated from a cell - hexagonal ice - can be calibrated against as well.
|
||||
// GuessInitialGeometry needs only the outermost d-spacing, to seed the distance from the innermost ring.
|
||||
std::vector<RingClusters> GuessInitialGeometry(DiffractionGeometry &geom, const std::vector<SpotToSave> &v, float largest_ring_d_A);
|
||||
void GuessGeometry(DiffractionGeometry &geom, const std::vector<SpotToSave> &v, const std::vector<float> &ring_q);
|
||||
void OptimizeGeometry(DiffractionGeometry &geom, const std::vector<SpotToSave> &v, const std::vector<float> &ring_q);
|
||||
// refine_tilt = false pins rot1/rot2 where the geometry already has them (see RingOptimizer).
|
||||
void GuessGeometry(DiffractionGeometry &geom, const std::vector<SpotToSave> &v, const std::vector<float> &ring_q,
|
||||
bool refine_tilt = true);
|
||||
void OptimizeGeometry(DiffractionGeometry &geom, const std::vector<SpotToSave> &v, const std::vector<float> &ring_q,
|
||||
bool refine_tilt = true);
|
||||
|
||||
// Each spot paired with the calibrant ring nearest its observed q, as the points RingOptimizer fits.
|
||||
// Spots more than 0.1 1/A from every ring are dropped rather than forced onto the closest one.
|
||||
|
||||
@@ -69,7 +69,8 @@ struct RingResidual {
|
||||
double det_m00, det_m01, det_m10, det_m11;
|
||||
};
|
||||
|
||||
RingOptimizer::RingOptimizer(const DiffractionGeometry& geom) : reference(geom) {}
|
||||
RingOptimizer::RingOptimizer(const DiffractionGeometry& geom, bool refine_tilt)
|
||||
: reference(geom), refine_tilt(refine_tilt) {}
|
||||
|
||||
DiffractionGeometry RingOptimizer::Run(const std::vector<RingOptimizerInput> &input) {
|
||||
// Initial guess for the parameters
|
||||
@@ -107,7 +108,7 @@ DiffractionGeometry RingOptimizer::Run(const std::vector<RingOptimizerInput> &in
|
||||
&& std::all_of(input.begin(), input.end(), [&](const RingOptimizerInput &p) {
|
||||
return p.q_expected == input.front().q_expected;
|
||||
});
|
||||
if (single_ring) {
|
||||
if (single_ring || !refine_tilt) {
|
||||
problem.SetParameterBlockConstant(&rot1);
|
||||
problem.SetParameterBlockConstant(&rot2);
|
||||
}
|
||||
|
||||
@@ -14,8 +14,13 @@ struct RingOptimizerInput {
|
||||
|
||||
class RingOptimizer {
|
||||
DiffractionGeometry reference;
|
||||
bool refine_tilt;
|
||||
public:
|
||||
RingOptimizer(const DiffractionGeometry& geom);
|
||||
// refine_tilt = false holds the detector tilt (rot1/rot2) at the value the geometry came in with
|
||||
// and fits only the beam centre and the distance. A tilted PONI is exact but not every consumer
|
||||
// accepts one - XDS has no place to put it - so a calibration meant for such a program is better
|
||||
// measured with the tilt pinned than with it refined and then dropped.
|
||||
RingOptimizer(const DiffractionGeometry& geom, bool refine_tilt = true);
|
||||
DiffractionGeometry Run(const std::vector<RingOptimizerInput> &input);
|
||||
};
|
||||
|
||||
|
||||
+4
-2
@@ -2857,7 +2857,8 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b
|
||||
logger.Info("Powder calibration from {} pooled spots", calibration_spot_list.size());
|
||||
result.calibration = CalibrateFromSpots(calibration_spot_list,
|
||||
experiment_.GetDiffractionGeometry(),
|
||||
config_.calibrant_ring_q);
|
||||
config_.calibrant_ring_q,
|
||||
config_.calibration_refine_tilt);
|
||||
} else {
|
||||
const auto *summed = plots.GetAzIntProfileObject();
|
||||
if (!summed)
|
||||
@@ -2865,7 +2866,8 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b
|
||||
"No azimuthal profile was accumulated");
|
||||
result.calibration = CalibrateFromProfile(summed->GetResult(), mapping,
|
||||
experiment_.GetDiffractionGeometry(),
|
||||
config_.calibrant_ring_q);
|
||||
config_.calibrant_ring_q,
|
||||
config_.calibration_refine_tilt);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,10 @@ struct ProcessConfig {
|
||||
// The library takes the ring list, not a calibrant name - the CLI resolves it (CalibrantRings).
|
||||
CalibrationMethod calibration_method = CalibrationMethod::Rings;
|
||||
std::vector<float> calibrant_ring_q;
|
||||
// Fit the detector tilt (PONI rot1/rot2) along with the beam centre and the distance. False holds
|
||||
// the tilt where the header put it, for a calibration destined for a program that cannot express
|
||||
// one - XDS.
|
||||
bool calibration_refine_tilt = true;
|
||||
|
||||
// Measure the spot budget instead of taking experiment.GetMaxSpotCount() as given (FullAnalysis,
|
||||
// rotation only; the rugnux CLI sets it unless --max-spots pinned the budget). The first pass reads
|
||||
|
||||
@@ -56,23 +56,25 @@ CalibrationResult Summarize(const DiffractionGeometry &fitted,
|
||||
CalibrationResult CalibrateFromProfile(const std::vector<float> &profile,
|
||||
const AzimuthalIntegrationMapping &mapping,
|
||||
const DiffractionGeometry &geom,
|
||||
const std::vector<float> &calibrant_ring_q) {
|
||||
const std::vector<float> &calibrant_ring_q,
|
||||
bool refine_tilt) {
|
||||
const auto points = RingsFromAzimuthalProfile(profile, mapping, geom, calibrant_ring_q);
|
||||
if (points.empty())
|
||||
throw JFJochException(JFJochExceptionCategory::CalibrationError,
|
||||
"No powder ring found in the summed azimuthal profile");
|
||||
return Summarize(RingOptimizer(geom).Run(points), points);
|
||||
return Summarize(RingOptimizer(geom, refine_tilt).Run(points), points);
|
||||
}
|
||||
|
||||
CalibrationResult CalibrateFromSpots(const std::vector<SpotToSave> &spots,
|
||||
const DiffractionGeometry &geom,
|
||||
const std::vector<float> &calibrant_ring_q) {
|
||||
const std::vector<float> &calibrant_ring_q,
|
||||
bool refine_tilt) {
|
||||
DiffractionGeometry fitted = geom;
|
||||
// From scratch (Hough circle centre + ring clustering), then refined: the guess pins the centre to a
|
||||
// whole pixel and only sees the spots its clustering kept, so the refine re-matches every spot at
|
||||
// that geometry.
|
||||
GuessGeometry(fitted, spots, calibrant_ring_q);
|
||||
OptimizeGeometry(fitted, spots, calibrant_ring_q);
|
||||
GuessGeometry(fitted, spots, calibrant_ring_q, refine_tilt);
|
||||
OptimizeGeometry(fitted, spots, calibrant_ring_q, refine_tilt);
|
||||
return Summarize(fitted, AssignSpotsToRings(fitted, spots, calibrant_ring_q));
|
||||
}
|
||||
|
||||
|
||||
@@ -27,17 +27,24 @@ struct CalibrationResult {
|
||||
double beam_sigma_pxl = 0.0;
|
||||
};
|
||||
|
||||
// Both fits take the detector tilt as a free parameter unless refine_tilt is false, which holds
|
||||
// rot1/rot2 at the value `geom` came in with and fits only the beam centre and the distance. The
|
||||
// tilt is real and a PONI carries it, but a program that has nowhere to put it - XDS - is better
|
||||
// given a geometry measured with the tilt pinned than one measured tilted and then flattened.
|
||||
|
||||
// Fit the geometry to the rings found in a run-summed azimuthal profile (CalibrationMethod::Rings).
|
||||
CalibrationResult CalibrateFromProfile(const std::vector<float> &profile,
|
||||
const AzimuthalIntegrationMapping &mapping,
|
||||
const DiffractionGeometry &geom,
|
||||
const std::vector<float> &calibrant_ring_q);
|
||||
const std::vector<float> &calibrant_ring_q,
|
||||
bool refine_tilt = true);
|
||||
|
||||
// Fit the geometry to a pooled spot list (CalibrationMethod::Spots): the beam centre from scratch off
|
||||
// the Hough circle centre, then the same ring fit.
|
||||
CalibrationResult CalibrateFromSpots(const std::vector<SpotToSave> &spots,
|
||||
const DiffractionGeometry &geom,
|
||||
const std::vector<float> &calibrant_ring_q);
|
||||
const std::vector<float> &calibrant_ring_q,
|
||||
bool refine_tilt = true);
|
||||
|
||||
// Write the geometry as a pyFAI PONI file, the interchange format every azimuthal-integration tool
|
||||
// reads. Throws if the file cannot be written.
|
||||
|
||||
@@ -96,6 +96,8 @@ std::string RugnuxCommandLine(const ProcessConfig &config,
|
||||
if (!calibrant_name.empty())
|
||||
add("--calibrant", calibrant_name);
|
||||
add("--calibration", config.calibration_method == CalibrationMethod::Rings ? "rings" : "spots");
|
||||
if (!config.calibration_refine_tilt)
|
||||
args.emplace_back("--no-refine-tilt");
|
||||
// The rings method reads the ring at every azimuth, and the GUI may have raised the sector
|
||||
// count for this run, so emit the value actually used rather than relying on the CLI default.
|
||||
if (config.calibration_method == CalibrationMethod::Rings)
|
||||
|
||||
+12
-3
@@ -100,6 +100,7 @@ void print_usage() {
|
||||
std::cout << " Calibration (--mode calibration)" << std::endl;
|
||||
std::cout << " --calibrant <name> Powder standard: lab6|agbh|ceo2|si|ice (default: lab6, case-insensitive)" << std::endl;
|
||||
std::cout << " --calibration <txt> How the rings are measured: rings|spots (default: rings). rings sums the (q x azimuth) azimuthal profile over every processed image and fits the ring arcs in it; spots pools the found spots and fits those. -s/-e/--stride select the images; rings defaults --azim-phi-bins to 32" << std::endl;
|
||||
std::cout << " --no-refine-tilt Do not refine the detector tilt: hold rot1/rot2 at the header value and fit only the beam centre and the distance. For a calibration handed to a program that cannot express a tilted detector, e.g. XDS" << std::endl;
|
||||
std::cout << std::endl;
|
||||
|
||||
std::cout << " Detector mask" << std::endl;
|
||||
@@ -298,7 +299,8 @@ enum {
|
||||
OPT_DETECTOR_MIRROR_Y,
|
||||
OPT_DETECTOR_QUARTER_TURNS,
|
||||
OPT_FFT_MIN_UNIT_CELL,
|
||||
OPT_POLARIZATION
|
||||
OPT_POLARIZATION,
|
||||
OPT_NO_REFINE_TILT
|
||||
};
|
||||
|
||||
static option long_options[] = {
|
||||
@@ -323,6 +325,7 @@ static option long_options[] = {
|
||||
{"mode", required_argument, nullptr, OPT_MODE},
|
||||
{"calibrant", required_argument, nullptr, OPT_CALIBRANT},
|
||||
{"calibration", required_argument, nullptr, OPT_CALIBRATION},
|
||||
{"no-refine-tilt", no_argument, nullptr, OPT_NO_REFINE_TILT},
|
||||
{"no-merge", no_argument, nullptr, OPT_NO_MERGE},
|
||||
{"no-p1-crosscheck", no_argument, nullptr, OPT_NO_P1_CROSSCHECK},
|
||||
{"scale-fulls", no_argument, nullptr, OPT_SCALE_FULLS},
|
||||
@@ -661,6 +664,7 @@ static int RunRugnux(int argc, char **argv) {
|
||||
RugnuxMode mode = RugnuxMode::MX; // --mode
|
||||
std::string calibrant = "lab6"; // --calibrant
|
||||
CalibrationMethod calibration_method = CalibrationMethod::Rings; // --calibration
|
||||
bool calibration_refine_tilt = true; // --no-refine-tilt
|
||||
bool rotation_indexing = false;
|
||||
bool force_still = false; // --force-still: process a rotation dataset as stills (indexing + scaling)
|
||||
bool two_pass_rotation = true;
|
||||
@@ -1071,6 +1075,9 @@ static int RunRugnux(int argc, char **argv) {
|
||||
return 1;
|
||||
}
|
||||
break;
|
||||
case OPT_NO_REFINE_TILT:
|
||||
calibration_refine_tilt = false;
|
||||
break;
|
||||
case OPT_NO_MERGE:
|
||||
run_scaling = false;
|
||||
break;
|
||||
@@ -1973,6 +1980,7 @@ static int RunRugnux(int argc, char **argv) {
|
||||
ProcessConfig config;
|
||||
config.mode = ProcessMode::Calibration;
|
||||
config.calibration_method = calibration_method;
|
||||
config.calibration_refine_tilt = calibration_refine_tilt;
|
||||
config.calibrant_ring_q = CalibrantRings(calibrant);
|
||||
config.start_image = start_image;
|
||||
config.end_image = end_image;
|
||||
@@ -1999,10 +2007,11 @@ static int RunRugnux(int argc, char **argv) {
|
||||
config.spot_finding.low_resolution_limit = d_max_spot_finding;
|
||||
experiment.MaxSpotCount(max_spot_count_override.value_or(RUGNUX_MAX_SPOT_COUNT));
|
||||
|
||||
logger.Info("Powder calibration: calibrant {} ({} rings), method {}, {} azimuthal bins",
|
||||
logger.Info("Powder calibration: calibrant {} ({} rings), method {}, {} azimuthal bins, tilt {}",
|
||||
calibrant, config.calibrant_ring_q.size(),
|
||||
calibration_method == CalibrationMethod::Rings ? "rings" : "spots",
|
||||
experiment.GetAzimuthalIntegrationSettings().GetAzimuthalBinCount());
|
||||
experiment.GetAzimuthalIntegrationSettings().GetAzimuthalBinCount(),
|
||||
calibration_refine_tilt ? "refined" : "fixed");
|
||||
|
||||
Rugnux process(reader, experiment, *dataset->pixel_mask, config);
|
||||
g_active_process = &process;
|
||||
|
||||
@@ -266,3 +266,39 @@ TEST_CASE("DetGeomCalib_OptimizeGeometry") {
|
||||
CHECK(geom_i.GetPoniRot1_rad() == Catch::Approx(geom.GetPoniRot1_rad()));
|
||||
CHECK(geom_i.GetPoniRot2_rad() == Catch::Approx(geom.GetPoniRot2_rad()));
|
||||
}
|
||||
|
||||
// The tilt is held where it was given, and the beam centre and distance are still fitted. This is what
|
||||
// a calibration destined for a program with no way to express a tilted detector asks for.
|
||||
TEST_CASE("DetGeomCalib_RingOptimizerFixedTilt") {
|
||||
DiffractionGeometry geom;
|
||||
geom.Wavelength_A(1.0).BeamX_pxl(100.0).BeamY_pxl(200.0).DetectorDistance_mm(100);
|
||||
|
||||
const float lab6_a = LAB6_CELL_A;
|
||||
|
||||
std::vector<RingOptimizerInput> spots;
|
||||
for (int d = 1; d < 4; d++) {
|
||||
for (int i = 0; i < 30; i++) {
|
||||
auto [x, y] = geom.ResPhiToPxl(lab6_a / sqrtf(d), i * M_PI * 2.0 / 30.0);
|
||||
spots.push_back(RingOptimizerInput{.x = x, .y = y,
|
||||
.q_expected = sqrtf(d) * M_PI * 2.0 / lab6_a});
|
||||
}
|
||||
}
|
||||
|
||||
DiffractionGeometry geom_i;
|
||||
geom_i.Wavelength_A(1.0).BeamX_pxl(105).BeamY_pxl(195).DetectorDistance_mm(110);
|
||||
|
||||
DiffractionGeometry geom_o = RingOptimizer(geom_i, false).Run(spots);
|
||||
|
||||
CHECK(geom_o.GetPoniRot1_rad() == 0.0f);
|
||||
CHECK(geom_o.GetPoniRot2_rad() == 0.0f);
|
||||
CHECK(geom_o.GetBeamX_pxl() == Catch::Approx(geom.GetBeamX_pxl()));
|
||||
CHECK(geom_o.GetBeamY_pxl() == Catch::Approx(geom.GetBeamY_pxl()));
|
||||
CHECK(geom_o.GetDetectorDistance_mm() == Catch::Approx(geom.GetDetectorDistance_mm()));
|
||||
|
||||
// A tilt already in the geometry stays exactly where it is rather than being refined away.
|
||||
DiffractionGeometry geom_tilted(geom_i);
|
||||
geom_tilted.PoniRot1_rad(0.1).PoniRot2_rad(0.05);
|
||||
DiffractionGeometry geom_t = RingOptimizer(geom_tilted, false).Run(spots);
|
||||
CHECK(geom_t.GetPoniRot1_rad() == Catch::Approx(0.1));
|
||||
CHECK(geom_t.GetPoniRot2_rad() == Catch::Approx(0.05));
|
||||
}
|
||||
|
||||
@@ -545,7 +545,7 @@ void JFJochImageReadingWorker::Analyze() {
|
||||
EmitImageLoaded_i(current_image_ptr);
|
||||
}
|
||||
|
||||
void JFJochImageReadingWorker::FindCenter(QVector<float> ring_q, bool guess) {
|
||||
void JFJochImageReadingWorker::FindCenter(QVector<float> ring_q, bool guess, bool refine_tilt) {
|
||||
QMutexLocker locker(&m);
|
||||
if (!current_image_ptr)
|
||||
return;
|
||||
@@ -555,9 +555,9 @@ void JFJochImageReadingWorker::FindCenter(QVector<float> ring_q, bool guess) {
|
||||
const std::vector<float> ring_Q(ring_q.begin(), ring_q.end());
|
||||
try {
|
||||
if (guess)
|
||||
GuessGeometry(geom, current_image_ptr->ImageData().spots, ring_Q);
|
||||
GuessGeometry(geom, current_image_ptr->ImageData().spots, ring_Q, refine_tilt);
|
||||
else
|
||||
OptimizeGeometry(geom, current_image_ptr->ImageData().spots, ring_Q);
|
||||
OptimizeGeometry(geom, current_image_ptr->ImageData().spots, ring_Q, refine_tilt);
|
||||
} catch (const JFJochException &e) {
|
||||
logger.ErrorException(e);
|
||||
return;
|
||||
|
||||
@@ -232,7 +232,7 @@ public slots:
|
||||
|
||||
// The calibrant as its ring q values (2*pi/d, 1/A) rather than a cell, so a standard whose rings
|
||||
// are measured rather than enumerated - hexagonal ice - can be calibrated against too.
|
||||
void FindCenter(QVector<float> ring_q, bool guess);
|
||||
void FindCenter(QVector<float> ring_q, bool guess, bool refine_tilt);
|
||||
|
||||
void Analyze();
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ signals:
|
||||
void downloadROIs();
|
||||
void uploadROIs();
|
||||
void maskFromROI(QString name, bool add);
|
||||
void findBeamCenter(QVector<float> ring_q, bool guess);
|
||||
void findBeamCenter(QVector<float> ring_q, bool guess, bool refine_tilt);
|
||||
void analyze();
|
||||
|
||||
void imageLoaded(std::shared_ptr<const JFJochReaderImage> image);
|
||||
|
||||
@@ -53,7 +53,7 @@ signals:
|
||||
void braggChanged(BraggIntegrationSettings settings);
|
||||
void scalingChanged(ScalingSettings settings);
|
||||
void experimentChanged(const DiffractionExperiment &experiment);
|
||||
void findBeamCenter(QVector<float> ring_q, bool guess);
|
||||
void findBeamCenter(QVector<float> ring_q, bool guess, bool refine_tilt);
|
||||
void ringsFromCalibration(QVector<float> rings);
|
||||
void referenceSelected(QString path, QString column); // user picked a reference MTZ / column
|
||||
void reanalyzeImage(bool armed); // "Analyze image" toggle
|
||||
|
||||
@@ -25,6 +25,16 @@ PowderCalibrationWidget::PowderCalibrationWidget(QWidget *parent) : QWidget(pare
|
||||
"(q x azimuth) azimuthal profile over every image and fits the ring arcs in it; "
|
||||
"spots pools the found spots and fits those.");
|
||||
|
||||
// The detector tilt is a free parameter of every one of these fits. Unticking it holds rot1/rot2
|
||||
// where the geometry already has them - what a calibration handed to a program with no way to
|
||||
// express a tilted detector (XDS) needs. It applies to the two buttons and to "Analyze dataset".
|
||||
refineTiltCheck = new QCheckBox("Refine detector tilt", this);
|
||||
refineTiltCheck->setChecked(true);
|
||||
refineTiltCheck->setToolTip("Fit the detector tilt (PONI rot1/rot2) together with the beam centre "
|
||||
"and the distance. Untick to hold the tilt where it is and fit only the "
|
||||
"beam centre and the distance - for a calibration that will be given to "
|
||||
"a program which cannot express a tilted detector, such as XDS.");
|
||||
|
||||
auto findBeamCenterButton = new QPushButton("Guess detector calibration", this);
|
||||
connect(findBeamCenterButton, &QPushButton::clicked,this, &PowderCalibrationWidget::findBeamCenterClicked);
|
||||
|
||||
@@ -48,6 +58,8 @@ PowderCalibrationWidget::PowderCalibrationWidget(QWidget *parent) : QWidget(pare
|
||||
|
||||
refine_row->addWidget(calibrantRingsButton,2, 0);
|
||||
|
||||
refine_row->addWidget(refineTiltCheck,2, 1);
|
||||
|
||||
refine_row->addWidget(new QLabel("Dataset method:"),3,0);
|
||||
refine_row->addWidget(methodCombo,3,1);
|
||||
|
||||
@@ -75,7 +87,8 @@ std::vector<float> PowderCalibrationWidget::GetCalibrantRings() const {
|
||||
|
||||
CalibrationSelection PowderCalibrationWidget::Selection() const {
|
||||
return {calibrantCombo->currentText(), GetCalibrantRings(),
|
||||
static_cast<CalibrationMethod>(methodCombo->currentData().toInt())};
|
||||
static_cast<CalibrationMethod>(methodCombo->currentData().toInt()),
|
||||
refineTiltCheck->isChecked()};
|
||||
}
|
||||
|
||||
void PowderCalibrationWidget::updateCalibrantList() {
|
||||
@@ -102,10 +115,10 @@ void PowderCalibrationWidget::loadImage(std::shared_ptr<const JFJochReaderImage>
|
||||
|
||||
void PowderCalibrationWidget::findBeamCenterClicked() {
|
||||
const std::vector<float> rings = GetCalibrantRings();
|
||||
emit findBeamCenter(QVector<float>(rings.begin(), rings.end()), true);
|
||||
emit findBeamCenter(QVector<float>(rings.begin(), rings.end()), true, refineTiltCheck->isChecked());
|
||||
}
|
||||
|
||||
void PowderCalibrationWidget::optimizeBeamCenterClicked() {
|
||||
const std::vector<float> rings = GetCalibrantRings();
|
||||
emit findBeamCenter(QVector<float>(rings.begin(), rings.end()), false);
|
||||
emit findBeamCenter(QVector<float>(rings.begin(), rings.end()), false, refineTiltCheck->isChecked());
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QVector>
|
||||
#include "../../common/UnitCell.h"
|
||||
@@ -16,6 +17,7 @@ struct CalibrationSelection {
|
||||
QString name;
|
||||
std::vector<float> ring_q; // q = 2*pi/d [1/A], ascending
|
||||
CalibrationMethod method = CalibrationMethod::Rings;
|
||||
bool refine_tilt = true; // false pins rot1/rot2 where they are (a calibration meant for XDS)
|
||||
};
|
||||
|
||||
// Detector geometry from a powder standard: the calibrant, and how its rings are measured. The two
|
||||
@@ -27,13 +29,14 @@ class PowderCalibrationWidget : public QWidget {
|
||||
|
||||
QComboBox* calibrantCombo{nullptr}; // stores current calibrant selection
|
||||
QComboBox* methodCombo{nullptr}; // rings / spots, for the whole-dataset run
|
||||
QCheckBox* refineTiltCheck{nullptr};// fit the detector tilt, or hold it where it is
|
||||
|
||||
std::vector<float> GetCalibrantRings() const;
|
||||
|
||||
void updateCalibrantList();
|
||||
signals:
|
||||
void ringsFromCalibration(QVector<float> v);
|
||||
void findBeamCenter(QVector<float> ring_q, bool guess);
|
||||
void findBeamCenter(QVector<float> ring_q, bool guess, bool refine_tilt);
|
||||
public:
|
||||
PowderCalibrationWidget(QWidget* parent);
|
||||
CalibrationSelection Selection() const;
|
||||
|
||||
@@ -301,6 +301,8 @@ int JFJochProcessingJobsWindow::askJob(const ReprocessingInputs &inputs, JobSpec
|
||||
.arg(spec.calibration.name)
|
||||
.arg(spec.calibration.ring_q.size())
|
||||
.arg(rings ? "rings" : "spots");
|
||||
if (!spec.calibration.refine_tilt)
|
||||
text += ", detector tilt held fixed";
|
||||
if (rings)
|
||||
text += panel_bins < 4
|
||||
? QStringLiteral("\nAzimuthal bins: %1 is too few to locate a ring — using %2")
|
||||
@@ -376,6 +378,7 @@ ProcessConfig JFJochProcessingJobsWindow::buildConfig(const JobSpec &spec, const
|
||||
config.fit_spindle = spec.estimate_beam_center;
|
||||
if (spec.mode == ProcessMode::Calibration) {
|
||||
config.calibration_method = spec.calibration.method;
|
||||
config.calibration_refine_tilt = spec.calibration.refine_tilt;
|
||||
config.calibrant_ring_q = spec.calibration.ring_q;
|
||||
// Spot finding for the spots method. Indexing is off: a calibration wants the spot positions and
|
||||
// nothing else, and a calibrant is a powder with no lattice to index.
|
||||
|
||||
Reference in New Issue
Block a user