// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #pragma once #include #include "gemmi/model.hpp" #include "gemmi/scaling.hpp" // Fit the overall scale, the anisotropic B and the flat bulk solvent of a model to observed // amplitudes, writing the answer into `scaling` (k_overall, b_star, k_sol, b_sol) so that // gemmi's own scale_data() can then apply it. // // The caller decides which reflections are fitted: FitModelScale reads scaling.points, which // prepare_points() filled. Pass the WORKING set to keep the free reflections out of the scale // model - that is a property of the call site, not of this function. // // Replaces gemmi's fit_isotropic_b_approximately() + fit_parameters(), which is an unbounded // Levenberg-Marquardt: nothing there stops k_sol and b_sol reaching values a flat solvent model // is meaningless at (b_sol of 1707 A^2 was measured on this corpus). Here k_sol and b_sol come // from a grid search over the physical ranges, so an unphysical answer cannot be represented. // The box the solvent parameters are searched in. The defaults are the physically reasonable // range reported by Fokine & Urzhumtsev and adopted as phenix's default grid: k_sol in (0.1, 0.8) // and b_sol in (10, 80) A^2, of which phenix searches b_sol 10-80 in steps of 5. A model with high // atomic B gets a higher b_sol ceiling (SolventBMax). struct ModelScaleBox { double k_lo = 0.10, k_hi = 0.60; double b_lo = 10.0, b_hi = 80.0; }; // The b_sol ceiling for `model`: 80 A^2, or twice the model's mean atomic B where that is higher. // b_sol is how far the solvent's edge is smeared, and the edge can be no sharper than the atoms it // lies against: a model at B of 300 A^2 beside a solvent held at 80 gets a solvent term that reaches // far too deep in resolution, which only a too-small k_sol can then offset. Fokine & Urzhumtsev's // 10-80 A^2 is for models at the B of ordinary entries (their mean b_sol 46 A^2), where the ceiling // stays at 80. double SolventBMax(const gemmi::Model &model); struct ModelScaleReport { int n_points = 0; // reflections the fit actually used int n_grid = 0; // grid points evaluated double r_work_fit = 1.0; // R on the fitted reflections, at the chosen solution }; // nthreads: the grid points are fitted in parallel on up to this many threads; the answer does not // depend on it. ModelScaleReport FitModelScale(gemmi::Scaling &scaling, ModelScaleBox box = {}, size_t nthreads = 1);