v1.0.0-rc.176 (#86)
Build Packages / Create release (push) Successful in 9s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 5m8s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 11m12s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 16m43s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 20m56s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 22m4s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 26m57s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 28m24s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 27m58s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 29m29s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 18m23s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 19m55s
Build Packages / Generate python client (push) Successful in 32s
Build Packages / Build documentation (push) Successful in 1m32s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 27m20s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 29m27s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 32m41s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 34m46s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 37m25s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 35m56s
Build Packages / Unit tests (push) Successful in 2h8m44s
Build Packages / Validation Battery (push) Successful in 33m35s
Build Packages / Create release (push) Successful in 9s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 5m8s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 11m12s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 16m43s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 20m56s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 22m4s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 26m57s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 28m24s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 27m58s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 29m29s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 18m23s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 19m55s
Build Packages / Generate python client (push) Successful in 32s
Build Packages / Build documentation (push) Successful in 1m32s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 27m20s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 29m27s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 32m41s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 34m46s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 37m25s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 35m56s
Build Packages / Unit tests (push) Successful in 2h8m44s
Build Packages / Validation Battery (push) Successful in 33m35s
* jfjoch_broker: Optional live Rugnux analysis of DECTRIS rotation collections inside the broker, with merged data, maps and the placed model written beside the master file, the rotation lattice in the scan result, and a "Rugnux live" tab and dashboard row in the web frontend. * Rugnux: Faster processing, with molecular replacement and model validation spread over several GPUs and run on the Apple GPU on macOS. * Rugnux: The polarization stated in miniCBF and d*TREK headers is used, twinned crystals' space groups are decided more reliably, and a run that finds no lattice still reports every image. Reviewed-on: #86 Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
This commit was merged in pull request #86.
This commit is contained in:
1 parent
19b135186d
commit
8261b7354e
377 files changed
+114248
-1922
No files matched your search
+150
-57
@@ -31,6 +31,7 @@
|
||||
#endif
|
||||
#include "../image_analysis/structure_refinement/SigmaA.h"
|
||||
#include "../writer/WriteModel.h"
|
||||
#include "../writer/RugnuxModelFile.h"
|
||||
#include "../image_analysis/scale_merge/ReindexAmbiguity.h"
|
||||
|
||||
namespace {
|
||||
@@ -107,6 +108,10 @@ ATOM 2 C . CB GLY A 1 12.000 14.000 16.000 1.00 20.00
|
||||
f.close();
|
||||
return name;
|
||||
}
|
||||
|
||||
RugnuxModel ReadModel(const std::string &path) {
|
||||
return ReadRugnuxModelFile(path);
|
||||
}
|
||||
}
|
||||
|
||||
// --model used to call gemmi::read_pdb unconditionally, so a deposited model handed over as mmCIF -
|
||||
@@ -162,6 +167,55 @@ TEST_CASE("ModelValidation_GPUPlanOnCardSizes", "[ModelValidation]") {
|
||||
CHECK(large.may_run_beside);
|
||||
}
|
||||
|
||||
TEST_CASE("ModelValidation_GPUPlanEnginesOnOtherCards", "[ModelValidation]") {
|
||||
// Four cards of 15.6 GB each: this card cannot take a second rigid-body engine within half of its 12.48 GB
|
||||
// budget (4.42 + 1.88 GB > 6.24 GB), and the other nine go three to a card on the other three.
|
||||
const size_t card = 15600000000;
|
||||
const ModelValidationGPUPlan alone = PlanModelValidationGPU(LargeCellNeed(card));
|
||||
ModelValidationGPUNeed need = LargeCellNeed(card);
|
||||
need.other_cards = {card, card, card};
|
||||
const ModelValidationGPUPlan plan = PlanModelValidationGPU(need);
|
||||
// This card's parts are those of the card alone.
|
||||
CHECK(plan.budget == alone.budget);
|
||||
CHECK(plan.bytes == alone.bytes);
|
||||
CHECK(plan.structure_factors == alone.structure_factors);
|
||||
CHECK(plan.null_structure_factors == alone.null_structure_factors);
|
||||
CHECK(plan.scale_fit_engines == alone.scale_fit_engines);
|
||||
REQUIRE(plan.rigid_body_per_card.size() == 4);
|
||||
CHECK(plan.rigid_body_per_card[0] == alone.rigid_body_engines);
|
||||
CHECK(alone.rigid_body_engines == 1);
|
||||
CHECK(plan.rigid_body_per_card == std::vector<size_t>{1, 3, 3, 3});
|
||||
CHECK(plan.rigid_body_engines == 10);
|
||||
CHECK(plan.most_on_a_card == 3 * need.rigid_body);
|
||||
CHECK(plan.may_run_beside);
|
||||
// Each other card within half of its own budget, less what a validation beside holds there.
|
||||
need.beside = plan.most_on_a_card;
|
||||
const ModelValidationGPUPlan beside = PlanModelValidationGPU(need);
|
||||
for (size_t k = 1; k < 4; k++)
|
||||
CHECK(beside.rigid_body_per_card[k] * need.rigid_body
|
||||
<= std::min(plan.budget / 2, plan.budget - need.beside));
|
||||
// What is resident is counted on every card: a merge holding most of each leaves no room for extra engines.
|
||||
need.beside = 0;
|
||||
need.resident = 8 * GB;
|
||||
const ModelValidationGPUPlan full = PlanModelValidationGPU(need);
|
||||
CHECK(full.rigid_body_per_card == std::vector<size_t>{1, 1, 1, 1});
|
||||
// One card alone: the plan as it was.
|
||||
CHECK(alone.rigid_body_per_card == std::vector<size_t>{1});
|
||||
CHECK(alone.most_on_a_card == alone.bytes);
|
||||
|
||||
// Engines this card alone has room for still go on every card in turn: ten of 0.1 GB on four cards, from the
|
||||
// next card and this one last.
|
||||
need = LargeCellNeed(card);
|
||||
need.other_cards = {card, card, card};
|
||||
need.rigid_body = 100000000;
|
||||
const ModelValidationGPUPlan small = PlanModelValidationGPU(need);
|
||||
CHECK(small.rigid_body_per_card == std::vector<size_t>{3, 3, 2, 2});
|
||||
CHECK(small.rigid_body_engines == 10);
|
||||
CHECK(small.scale_fit_engines == 4);
|
||||
need.other_cards.clear();
|
||||
CHECK(PlanModelValidationGPU(need).rigid_body_per_card == std::vector<size_t>{10});
|
||||
}
|
||||
|
||||
TEST_CASE("ModelValidation_GPUPlanPartThatDoesNotFitGoesToTheCPU", "[ModelValidation]") {
|
||||
ModelValidationGPUNeed need = LargeCellNeed(8 * GB);
|
||||
need.structure_factors = 7 * GB; // over the 6.4 GB budget on its own
|
||||
@@ -212,8 +266,8 @@ TEST_CASE("ModelValidation_ReadsPdbAndMmcif", "[ModelValidation]") {
|
||||
const auto pdb = WriteTemp("model_validation_test.pdb", kPdb);
|
||||
const auto cif = WriteTemp("model_validation_test.cif", kMmcif);
|
||||
|
||||
const auto from_pdb = ModelReferenceIntensities(pdb, {}, {}, 4.0, logger);
|
||||
const auto from_cif = ModelReferenceIntensities(cif, {}, {}, 4.0, logger);
|
||||
const auto from_pdb = ModelReferenceIntensities(ReadModel(pdb), {}, {}, 4.0, logger);
|
||||
const auto from_cif = ModelReferenceIntensities(ReadModel(cif), {}, {}, 4.0, logger);
|
||||
|
||||
REQUIRE_FALSE(from_pdb.empty());
|
||||
REQUIRE_FALSE(from_cif.empty());
|
||||
@@ -222,7 +276,7 @@ TEST_CASE("ModelValidation_ReadsPdbAndMmcif", "[ModelValidation]") {
|
||||
|
||||
// The extension is not what decides: the same mmCIF under a .pdb name still reads.
|
||||
const auto misnamed = WriteTemp("model_validation_test_misnamed.pdb", kMmcif);
|
||||
CHECK_FALSE(ModelReferenceIntensities(misnamed, {}, {}, 4.0, logger).empty());
|
||||
CHECK_FALSE(ModelReferenceIntensities(ReadModel(misnamed), {}, {}, 4.0, logger).empty());
|
||||
|
||||
std::filesystem::remove(pdb);
|
||||
std::filesystem::remove(cif);
|
||||
@@ -256,8 +310,8 @@ TEST_CASE("ModelValidation_ExpandsStrictNcs", "[ModelValidation]") {
|
||||
const auto ncs_path = WriteTemp("model_validation_test_ncs.pdb", with_ncs);
|
||||
const auto full_path = WriteTemp("model_validation_test_ncs_full.pdb", expanded);
|
||||
|
||||
const auto from_ncs = ModelReferenceIntensities(ncs_path, {}, {}, 4.0, logger);
|
||||
const auto from_full = ModelReferenceIntensities(full_path, {}, {}, 4.0, logger);
|
||||
const auto from_ncs = ModelReferenceIntensities(ReadModel(ncs_path), {}, {}, 4.0, logger);
|
||||
const auto from_full = ModelReferenceIntensities(ReadModel(full_path), {}, {}, 4.0, logger);
|
||||
REQUIRE(from_ncs.size() == from_full.size());
|
||||
REQUIRE_FALSE(from_ncs.empty());
|
||||
for (size_t i = 0; i < from_ncs.size(); i++) {
|
||||
@@ -275,22 +329,22 @@ TEST_CASE("ModelValidation_ExpandsStrictNcs", "[ModelValidation]") {
|
||||
TEST_CASE("ModelValidation_UnusableModelGivesAReason", "[ModelValidation]") {
|
||||
Logger logger("ModelValidation_UnusableModelGivesAReason");
|
||||
|
||||
// A file that holds no model is refused where it is read, before anything runs.
|
||||
const auto empty_file = WriteTemp("model_validation_test_bogus.pdb", "not a coordinate file\n");
|
||||
CHECK_THROWS(ReadModel(empty_file));
|
||||
CHECK_THROWS(ReadModel("model_validation_test_does_not_exist.pdb"));
|
||||
std::filesystem::remove(empty_file);
|
||||
|
||||
// A model with atoms but no crystal cell to put them in.
|
||||
RugnuxModel no_cell;
|
||||
no_cell.name = "no_cell.pdb";
|
||||
no_cell.structure.models.emplace_back(1);
|
||||
const auto result = ValidateAgainstModel({}, UnitCell{.a = 40, .b = 50, .c = 60,
|
||||
.alpha = 90, .beta = 90, .gamma = 90},
|
||||
empty_file, "", logger);
|
||||
no_cell, logger);
|
||||
CHECK_FALSE(result.ok);
|
||||
CHECK_FALSE(result.failure_reason.empty());
|
||||
CHECK_THAT(result.failure_reason, Catch::Matchers::ContainsSubstring(empty_file));
|
||||
|
||||
const auto missing = ValidateAgainstModel({}, UnitCell{.a = 40, .b = 50, .c = 60,
|
||||
.alpha = 90, .beta = 90, .gamma = 90},
|
||||
"model_validation_test_does_not_exist.pdb", "", logger);
|
||||
CHECK_FALSE(missing.ok);
|
||||
CHECK_FALSE(missing.failure_reason.empty());
|
||||
|
||||
std::filesystem::remove(empty_file);
|
||||
CHECK_THAT(result.failure_reason, Catch::Matchers::ContainsSubstring("no_cell.pdb"));
|
||||
}
|
||||
|
||||
// A rigid-body step is only worth taking if it can find a shift it was not told about. The check is
|
||||
@@ -306,7 +360,7 @@ TEST_CASE("ModelValidation_RigidBodyRecoversASmallShift", "[ModelValidation]") {
|
||||
|
||||
// "Observed" amplitudes: the model's own structure factors, so the target's minimum is exactly
|
||||
// where the model started.
|
||||
const auto ref = ModelReferenceIntensities(path, {}, {}, 3.0, logger);
|
||||
const auto ref = ModelReferenceIntensities(ReadModel(path), {}, {}, 3.0, logger);
|
||||
REQUIRE_FALSE(ref.empty());
|
||||
gemmi::AsuData<gemmi::ValueSigma<float>> fobs;
|
||||
fobs.unit_cell_ = st.cell;
|
||||
@@ -353,7 +407,7 @@ TEST_CASE("ModelValidation_RigidBodyLeavesThePolarDirectionAlone", "[ModelValida
|
||||
REQUIRE(sg != nullptr);
|
||||
st.setup_cell_images();
|
||||
|
||||
const auto ref = ModelReferenceIntensities(path, {}, {}, 3.0, logger);
|
||||
const auto ref = ModelReferenceIntensities(ReadModel(path), {}, {}, 3.0, logger);
|
||||
REQUIRE_FALSE(ref.empty());
|
||||
gemmi::AsuData<gemmi::ValueSigma<float>> fobs;
|
||||
fobs.unit_cell_ = st.cell;
|
||||
@@ -400,7 +454,7 @@ TEST_CASE("ModelValidation_RigidBodySolventStaysPhysical", "[ModelValidation]")
|
||||
REQUIRE(sg != nullptr);
|
||||
st.setup_cell_images();
|
||||
|
||||
const auto ref = ModelReferenceIntensities(path, {}, {}, 3.0, logger);
|
||||
const auto ref = ModelReferenceIntensities(ReadModel(path), {}, {}, 3.0, logger);
|
||||
REQUIRE_FALSE(ref.empty());
|
||||
gemmi::AsuData<gemmi::ValueSigma<float>> fobs;
|
||||
fobs.unit_cell_ = st.cell;
|
||||
@@ -487,7 +541,7 @@ TEST_CASE("ModelValidation_FindsTheDatasDescriptionOfTheLattice", "[ModelValidat
|
||||
|
||||
// The data: the cluster's own |F|^2 in its own frame, 30 x 34 x 38, P 21 21 21.
|
||||
const auto data_model = WriteTemp("cob_data_test.pdb", ClusterPdb().c_str());
|
||||
auto obs = ModelReferenceIntensities(data_model, {}, {}, 2.5, logger);
|
||||
auto obs = ModelReferenceIntensities(ReadModel(data_model), {}, {}, 2.5, logger);
|
||||
REQUIRE(obs.size() > 500);
|
||||
for (size_t i = 0; i < obs.size(); i++) {
|
||||
obs[i].F = std::sqrt(std::max(0.0f, obs[i].I));
|
||||
@@ -495,7 +549,6 @@ TEST_CASE("ModelValidation_FindsTheDatasDescriptionOfTheLattice", "[ModelValidat
|
||||
}
|
||||
const UnitCell data_cell{.a = 30, .b = 34, .c = 38, .alpha = 90, .beta = 90, .gamma = 90};
|
||||
const gemmi::SpaceGroup *data_sg = gemmi::find_spacegroup_by_name("P 21 21 21");
|
||||
const std::string prefix = (std::filesystem::temp_directory_path() / "cob_test").string();
|
||||
|
||||
// The same structure as a deposition on permuted axes, a' = b, b' = c, c' = a. The cell is
|
||||
// orthogonal, so the Cartesian coordinates permute with the axes.
|
||||
@@ -518,9 +571,13 @@ TEST_CASE("ModelValidation_FindsTheDatasDescriptionOfTheLattice", "[ModelValidat
|
||||
}
|
||||
const auto other_setting = WriteTemp("cob_model_test.pdb", permuted.c_str());
|
||||
|
||||
const auto moved = ValidateAgainstModel(obs, data_cell, other_setting, prefix, logger, data_sg,
|
||||
const auto moved = ValidateAgainstModel(obs, data_cell, ReadModel(other_setting), logger, data_sg,
|
||||
/*probe_indexing_ambiguity=*/false, 1, 1.0);
|
||||
REQUIRE(moved.ok);
|
||||
// The maps come back in memory: their coefficients always, the maps themselves only when asked for.
|
||||
REQUIRE(moved.maps);
|
||||
CHECK(moved.maps->coefficients.columns.size() == 12);
|
||||
CHECK_FALSE(moved.maps->map_2fofc);
|
||||
logger.Info("Change of basis test: took {} out of {}, R-work {:.4f}",
|
||||
moved.change_of_basis_op.triplet(), moved.setting_as_read, moved.r_work);
|
||||
CHECK_FALSE(moved.change_of_basis_op == gemmi::Op::identity());
|
||||
@@ -529,15 +586,13 @@ TEST_CASE("ModelValidation_FindsTheDatasDescriptionOfTheLattice", "[ModelValidat
|
||||
// The isomorphous case, which is what nearly every run is: the model already describes the
|
||||
// lattice the way the data do, the only operators that map its cell are its own symmetry, and
|
||||
// nothing is scored or moved.
|
||||
const auto same = ValidateAgainstModel(obs, data_cell, data_model, prefix, logger, data_sg,
|
||||
const auto same = ValidateAgainstModel(obs, data_cell, ReadModel(data_model), logger, data_sg,
|
||||
false, 1, 1.0);
|
||||
REQUIRE(same.ok);
|
||||
CHECK(same.change_of_basis_op == gemmi::Op::identity());
|
||||
CHECK(same.setting_as_read.empty());
|
||||
CHECK(same.r_work < 0.15);
|
||||
|
||||
for (const char *suffix : {"_2fofc.ccp4", "_fofc.ccp4", "_anom.ccp4", "_maps.mtz"})
|
||||
std::filesystem::remove(prefix + suffix);
|
||||
std::filesystem::remove(data_model);
|
||||
std::filesystem::remove(other_setting);
|
||||
}
|
||||
@@ -551,7 +606,7 @@ TEST_CASE("ModelValidation_ReindexesTheDataIntoTheModelsIndexing", "[ModelValida
|
||||
// Point group 4 on a tetragonal lattice (4/mmm): one alternative indexing.
|
||||
const auto model = WriteTemp("reidx_model_test.pdb",
|
||||
ClusterPdb("CRYST1 34.000 34.000 38.000 90.00 90.00 90.00 P 4 4\n").c_str());
|
||||
auto own = ModelReferenceIntensities(model, {}, {}, 2.5, logger);
|
||||
auto own = ModelReferenceIntensities(ReadModel(model), {}, {}, 2.5, logger);
|
||||
REQUIRE(own.size() > 500);
|
||||
for (size_t i = 0; i < own.size(); i++) {
|
||||
own[i].F = std::sqrt(std::max(0.0f, own[i].I));
|
||||
@@ -561,12 +616,11 @@ TEST_CASE("ModelValidation_ReindexesTheDataIntoTheModelsIndexing", "[ModelValida
|
||||
const gemmi::SpaceGroup *p4 = gemmi::find_spacegroup_by_name("P 4");
|
||||
const auto laws = ReindexAmbiguityOperators(cell, *p4);
|
||||
REQUIRE(laws.size() == 1);
|
||||
const std::string prefix = (std::filesystem::temp_directory_path() / "reidx_test").string();
|
||||
|
||||
// The data as a run that picked the other indexing would have merged them.
|
||||
const auto obs = ReindexReflections(own, laws.front());
|
||||
|
||||
const auto to_model = ValidateAgainstModel(obs, cell, model, prefix, logger, p4,
|
||||
const auto to_model = ValidateAgainstModel(obs, cell, ReadModel(model), logger, p4,
|
||||
/*probe_indexing_ambiguity=*/true, 1, 1.0);
|
||||
REQUIRE(to_model.ok);
|
||||
CHECK(to_model.change_of_basis_op == gemmi::Op::identity());
|
||||
@@ -575,7 +629,7 @@ TEST_CASE("ModelValidation_ReindexesTheDataIntoTheModelsIndexing", "[ModelValida
|
||||
CHECK(to_model.r_work < 0.15);
|
||||
|
||||
// A reference already fixed the data's indexing: the data stay, and the model is moved.
|
||||
const auto fixed = ValidateAgainstModel(obs, cell, model, prefix, logger, p4, false, 1, 1.0);
|
||||
const auto fixed = ValidateAgainstModel(obs, cell, ReadModel(model), logger, p4, false, 1, 1.0);
|
||||
REQUIRE(fixed.ok);
|
||||
CHECK_FALSE(fixed.change_of_basis_op == gemmi::Op::identity());
|
||||
CHECK(fixed.indexing_op == gemmi::Op::identity());
|
||||
@@ -599,7 +653,7 @@ TEST_CASE("ModelValidation_ReindexesTheDataIntoTheModelsIndexing", "[ModelValida
|
||||
r.F = std::sqrt(std::max(0.0f, r.I));
|
||||
}
|
||||
}
|
||||
const auto twin = ValidateAgainstModel(twinned, cell, model, prefix, logger, p4, true, 1, 1.0);
|
||||
const auto twin = ValidateAgainstModel(twinned, cell, ReadModel(model), logger, p4, true, 1, 1.0);
|
||||
REQUIRE(twin.ok);
|
||||
CHECK_FALSE(twin.indexing_decided);
|
||||
CHECK(twin.indexing_op == gemmi::Op::identity());
|
||||
@@ -607,7 +661,7 @@ TEST_CASE("ModelValidation_ReindexesTheDataIntoTheModelsIndexing", "[ModelValida
|
||||
|
||||
// Data already in the model's indexing: nothing moves. The null is run all the same, as on every
|
||||
// validation that judges its model (ModelValidationSchedule::fit_test), and the model fits.
|
||||
const auto same = ValidateAgainstModel(own, cell, model, prefix, logger, p4, true, 1, 1.0);
|
||||
const auto same = ValidateAgainstModel(own, cell, ReadModel(model), logger, p4, true, 1, 1.0);
|
||||
REQUIRE(same.ok);
|
||||
CHECK(same.change_of_basis_op == gemmi::Op::identity());
|
||||
CHECK(same.indexing_op == gemmi::Op::identity());
|
||||
@@ -615,8 +669,6 @@ TEST_CASE("ModelValidation_ReindexesTheDataIntoTheModelsIndexing", "[ModelValida
|
||||
CHECK(same.model_fits);
|
||||
CHECK(same.r_work < 0.15);
|
||||
|
||||
for (const char *suffix : {"_2fofc.ccp4", "_fofc.ccp4", "_anom.ccp4", "_maps.mtz"})
|
||||
std::filesystem::remove(prefix + suffix);
|
||||
std::filesystem::remove(model);
|
||||
}
|
||||
|
||||
@@ -736,7 +788,7 @@ TEST_CASE("ModelValidation_CCModelFollowsTheSignalByShell", "[ModelValidation]")
|
||||
Logger logger("ModelValidation_CCModelFollowsTheSignalByShell");
|
||||
|
||||
const auto path = WriteTemp("cc_model_test.pdb", ClusterPdb().c_str());
|
||||
auto obs = ModelReferenceIntensities(path, {}, {}, 2.5, logger);
|
||||
auto obs = ModelReferenceIntensities(ReadModel(path), {}, {}, 2.5, logger);
|
||||
REQUIRE(obs.size() > 1000);
|
||||
|
||||
// The shells the correlation is reported on, coarse to fine, and the noise each one gets as a
|
||||
@@ -773,11 +825,10 @@ TEST_CASE("ModelValidation_CCModelFollowsTheSignalByShell", "[ModelValidation]")
|
||||
obs[i].rfree_flag = (i % 20) == 0;
|
||||
}
|
||||
|
||||
const std::string prefix = (std::filesystem::temp_directory_path() / "cc_model_test").string();
|
||||
const auto result =
|
||||
ValidateAgainstModel(obs, UnitCell{.a = 30, .b = 34, .c = 38,
|
||||
.alpha = 90, .beta = 90, .gamma = 90},
|
||||
path, prefix, logger, gemmi::find_spacegroup_by_name("P 21 21 21"),
|
||||
ReadModel(path), logger, gemmi::find_spacegroup_by_name("P 21 21 21"),
|
||||
/*probe_indexing_ambiguity=*/false, 1, 1.0, shells);
|
||||
REQUIRE(result.ok);
|
||||
REQUIRE(result.cc_model_shells.size() == shells.size());
|
||||
@@ -805,13 +856,11 @@ TEST_CASE("ModelValidation_CCModelFollowsTheSignalByShell", "[ModelValidation]")
|
||||
const auto no_shells =
|
||||
ValidateAgainstModel(obs, UnitCell{.a = 30, .b = 34, .c = 38,
|
||||
.alpha = 90, .beta = 90, .gamma = 90},
|
||||
path, prefix, logger, gemmi::find_spacegroup_by_name("P 21 21 21"),
|
||||
ReadModel(path), logger, gemmi::find_spacegroup_by_name("P 21 21 21"),
|
||||
false, 1, 1.0);
|
||||
CHECK(no_shells.ok);
|
||||
CHECK(no_shells.cc_model_shells.empty());
|
||||
|
||||
for (const char *suffix : {"_2fofc.ccp4", "_fofc.ccp4", "_anom.ccp4", "_maps.mtz"})
|
||||
std::filesystem::remove(prefix + suffix);
|
||||
std::filesystem::remove(path);
|
||||
}
|
||||
|
||||
@@ -827,22 +876,19 @@ TEST_CASE("ModelValidation_WrongModelInAGroupOffTheDataMetric", "[ModelValidatio
|
||||
const auto data_path = WriteTemp("off_metric_data.pdb", ClusterPdb().c_str());
|
||||
const auto wrong_path = WriteTemp("off_metric_wrong.pdb",
|
||||
ClusterPdb("CRYST1 40.000 40.000 40.000 90.00 90.00 90.00 I 21 3 24\n", 7).c_str());
|
||||
auto obs = ModelReferenceIntensities(data_path, {}, {}, 2.0, logger);
|
||||
auto obs = ModelReferenceIntensities(ReadModel(data_path), {}, {}, 2.0, logger);
|
||||
REQUIRE(obs.size() > 1000);
|
||||
for (size_t i = 0; i < obs.size(); i++) {
|
||||
obs[i].sigma = std::max(1.0f, 0.05f * obs[i].I);
|
||||
obs[i].F = std::sqrt(std::max(0.0f, obs[i].I));
|
||||
obs[i].rfree_flag = (i % 20) == 0;
|
||||
}
|
||||
const std::string prefix = (std::filesystem::temp_directory_path() / "off_metric_test").string();
|
||||
const auto result =
|
||||
ValidateAgainstModel(obs, UnitCell{.a = 30, .b = 34, .c = 38, .alpha = 90, .beta = 90, .gamma = 90},
|
||||
wrong_path, prefix, logger, gemmi::find_spacegroup_by_name("P 21 21 21"));
|
||||
ReadModel(wrong_path), logger, gemmi::find_spacegroup_by_name("P 21 21 21"));
|
||||
CHECK(result.ok);
|
||||
CHECK(!result.adopted_model_enantiomorph);
|
||||
|
||||
for (const char *suffix : {"_2fofc.ccp4", "_fofc.ccp4", "_anom.ccp4", "_maps.mtz"})
|
||||
std::filesystem::remove(prefix + suffix);
|
||||
std::filesystem::remove(data_path);
|
||||
std::filesystem::remove(wrong_path);
|
||||
}
|
||||
@@ -854,7 +900,7 @@ TEST_CASE("ModelValidation_CCModelUnrelatedModelConfirmsNothing", "[ModelValidat
|
||||
Logger logger("ModelValidation_CCModelUnrelatedModelConfirmsNothing");
|
||||
const auto data_path = WriteTemp("cc_null_data.pdb", ClusterPdb().c_str());
|
||||
const auto wrong_path = WriteTemp("cc_null_wrong.pdb", ClusterPdb(kCryst, 7).c_str());
|
||||
auto obs = ModelReferenceIntensities(data_path, {}, {}, 2.0, logger);
|
||||
auto obs = ModelReferenceIntensities(ReadModel(data_path), {}, {}, 2.0, logger);
|
||||
std::erase_if(obs, [](const MergedReflection &r) { return r.d > 8.0f; });
|
||||
REQUIRE(obs.size() > 1000);
|
||||
for (size_t i = 0; i < obs.size(); i++) {
|
||||
@@ -867,10 +913,9 @@ TEST_CASE("ModelValidation_CCModelUnrelatedModelConfirmsNothing", "[ModelValidat
|
||||
obs[i].rfree_flag = (i % 20) == 0;
|
||||
}
|
||||
const std::vector<float> shells{4.0f, 3.0f, 2.5f, 2.2f, 2.0f};
|
||||
const std::string prefix = (std::filesystem::temp_directory_path() / "cc_null_test").string();
|
||||
const auto result =
|
||||
ValidateAgainstModel(obs, UnitCell{.a = 30, .b = 34, .c = 38, .alpha = 90, .beta = 90, .gamma = 90},
|
||||
wrong_path, prefix, logger, gemmi::find_spacegroup_by_name("P 21 21 21"),
|
||||
ReadModel(wrong_path), logger, gemmi::find_spacegroup_by_name("P 21 21 21"),
|
||||
/*probe_indexing_ambiguity=*/false, 1, 1.0, shells);
|
||||
REQUIRE(result.ok);
|
||||
REQUIRE(result.cc_model_shells.size() == shells.size());
|
||||
@@ -885,8 +930,6 @@ TEST_CASE("ModelValidation_CCModelUnrelatedModelConfirmsNothing", "[ModelValidat
|
||||
// The trap is there: on independent reflections some shell would have read as significant.
|
||||
CHECK(fisher_max > 3.0);
|
||||
|
||||
for (const char *suffix : {"_2fofc.ccp4", "_fofc.ccp4", "_anom.ccp4", "_maps.mtz"})
|
||||
std::filesystem::remove(prefix + suffix);
|
||||
std::filesystem::remove(data_path);
|
||||
std::filesystem::remove(wrong_path);
|
||||
}
|
||||
@@ -1003,7 +1046,7 @@ TEST_CASE("ModelValidation_RigidBodySameOnAnyNumberOfThreads", "[ModelValidation
|
||||
REQUIRE(sg != nullptr);
|
||||
st.setup_cell_images();
|
||||
|
||||
const auto ref = ModelReferenceIntensities(path, {}, {}, 3.0, logger);
|
||||
const auto ref = ModelReferenceIntensities(ReadModel(path), {}, {}, 3.0, logger);
|
||||
REQUIRE_FALSE(ref.empty());
|
||||
gemmi::AsuData<gemmi::ValueSigma<float>> fobs;
|
||||
fobs.unit_cell_ = st.cell;
|
||||
@@ -1033,8 +1076,10 @@ TEST_CASE("ModelValidation_RigidBodySameOnAnyNumberOfThreads", "[ModelValidation
|
||||
}
|
||||
|
||||
// The rigid body puts each probe placement on the grid with its own parallel copy of gemmi's gridding
|
||||
// (density, solvent mask and their symmetrization). It must give gemmi's grids bit for bit, on any
|
||||
// number of threads, for isotropic and anisotropic atoms and for groups with and without centring.
|
||||
// (density, solvent mask and their symmetrization). The mask must be gemmi's bit for bit; the density
|
||||
// gemmi's but for the last bits of its single-precision walk (a point at a radius' edge can also fall on
|
||||
// the other side, adding or losing a term at the cutoff). Both must be the same on any number of threads,
|
||||
// for isotropic and anisotropic atoms and for groups with and without centring.
|
||||
TEST_CASE("ModelValidation_ParallelGriddingMatchesGemmi", "[ModelValidation]") {
|
||||
const char *crysts[] = {
|
||||
"CRYST1 40.000 50.000 60.000 90.00 90.00 90.00 P 1 1\n",
|
||||
@@ -1060,6 +1105,15 @@ TEST_CASE("ModelValidation_ParallelGriddingMatchesGemmi", "[ModelValidation]") {
|
||||
return a.data.size() == b.data.size() &&
|
||||
std::memcmp(a.data.data(), b.data.data(), a.data.size() * sizeof(float)) == 0;
|
||||
};
|
||||
// gemmi's density to rounding: relative 1e-5, and the few cutoff-sized terms at a radius' edge.
|
||||
const auto close = [](const gemmi::Grid<float> &a, const gemmi::Grid<float> &b, float cutoff) {
|
||||
if (a.data.size() != b.data.size())
|
||||
return false;
|
||||
for (size_t i = 0; i < a.data.size(); i++)
|
||||
if (std::fabs(a.data[i] - b.data[i]) > 1e-5f * std::fabs(b.data[i]) + 4 * cutoff)
|
||||
return false;
|
||||
return true;
|
||||
};
|
||||
for (double d_min : {6.0, 3.5}) {
|
||||
gemmi::DensityCalculator<gemmi::IT92<float>, float> gemmi_dc;
|
||||
gemmi_dc.d_min = d_min;
|
||||
@@ -1075,6 +1129,7 @@ TEST_CASE("ModelValidation_ParallelGriddingMatchesGemmi", "[ModelValidation]") {
|
||||
gemmi_mask.set_size_from_spacing(gemmi_dc.requested_grid_spacing(), gemmi::GridSizeRounding::Up);
|
||||
gemmi::SolventMasker(gemmi::AtomicRadiiSet::Refmac).put_mask_on_grid(gemmi_mask, st.models[0]);
|
||||
|
||||
std::vector<float> one_thread;
|
||||
for (size_t nthreads : {1, 4}) {
|
||||
const std::vector<size_t> leaders = OrbitLeaders(gemmi_mask, nthreads);
|
||||
|
||||
@@ -1085,7 +1140,11 @@ TEST_CASE("ModelValidation_ParallelGriddingMatchesGemmi", "[ModelValidation]") {
|
||||
dc.grid.spacegroup = sg;
|
||||
dc.set_refmac_compatible_blur(st.models[0]);
|
||||
PutModelDensityOnGrid(dc, st.models[0], leaders, nthreads);
|
||||
CHECK(same(dc.grid, gemmi_dc.grid));
|
||||
CHECK(close(dc.grid, gemmi_dc.grid, gemmi_dc.cutoff));
|
||||
if (one_thread.empty())
|
||||
one_thread = dc.grid.data;
|
||||
else
|
||||
CHECK(dc.grid.data == one_thread);
|
||||
|
||||
gemmi::Grid<float> mask;
|
||||
mask.unit_cell = st.cell;
|
||||
@@ -1130,7 +1189,9 @@ TEST_CASE("ModelValidation_GriddingAnAtomWhoseRadiusStartsAtTheCutoff", "[ModelV
|
||||
gemmi_dc.put_model_density_on_grid(st.models[0]);
|
||||
PutModelDensityOnGrid(dc, st.models[0], {}, 4);
|
||||
REQUIRE(dc.grid.data.size() == gemmi_dc.grid.data.size());
|
||||
CHECK(std::memcmp(dc.grid.data.data(), gemmi_dc.grid.data.data(), dc.grid.data.size() * sizeof(float)) == 0);
|
||||
// gemmi's density but for the last bits of the single-precision walk (see above).
|
||||
for (size_t i = 0; i < dc.grid.data.size(); i++)
|
||||
CHECK(std::fabs(dc.grid.data[i] - gemmi_dc.grid.data[i]) <= 1e-5f * std::fabs(gemmi_dc.grid.data[i]) + 4 * dc.cutoff);
|
||||
CHECK(std::all_of(dc.grid.data.begin(), dc.grid.data.end(), [](float v) { return std::isfinite(v); }));
|
||||
}
|
||||
|
||||
@@ -1282,7 +1343,7 @@ namespace {
|
||||
size_t engines, Logger &logger) {
|
||||
auto pool = RigidBodyGPUPool::Plan(model, cell, sg, d_min, observations, logger);
|
||||
REQUIRE(pool);
|
||||
pool->Reserve(engines);
|
||||
pool->Reserve({engines});
|
||||
return pool;
|
||||
}
|
||||
}
|
||||
@@ -1298,7 +1359,7 @@ namespace {
|
||||
gemmi::AsuData<gemmi::ValueSigma<float>> OwnAmplitudes(const char *cryst, const gemmi::Structure &st,
|
||||
double d_min, Logger &logger) {
|
||||
const auto path = WriteTemp("rigid_body_own_amplitudes.pdb", ClusterPdb(cryst).c_str());
|
||||
const auto ref = ModelReferenceIntensities(path, {}, {}, 3.0, logger);
|
||||
const auto ref = ModelReferenceIntensities(ReadModel(path), {}, {}, 3.0, logger);
|
||||
std::filesystem::remove(path);
|
||||
gemmi::AsuData<gemmi::ValueSigma<float>> fobs;
|
||||
fobs.unit_cell_ = st.cell;
|
||||
@@ -1327,7 +1388,7 @@ namespace {
|
||||
const gemmi::SpaceGroup *sg = st.find_spacegroup();
|
||||
// "Observed" amplitudes: the model's own, as placed at q = 0.
|
||||
const auto path = WriteTemp("rigid_body_jacobian_test.pdb", ClusterPdb(cryst).c_str());
|
||||
const auto ref = ModelReferenceIntensities(path, {}, {}, 3.0, logger);
|
||||
const auto ref = ModelReferenceIntensities(ReadModel(path), {}, {}, 3.0, logger);
|
||||
std::filesystem::remove(path);
|
||||
REQUIRE_FALSE(ref.empty());
|
||||
|
||||
@@ -1455,7 +1516,7 @@ TEST_CASE("ModelValidation_RigidBodyReportsTheRotationItApplied", "[ModelValidat
|
||||
const gemmi::SpaceGroup *sg = st.find_spacegroup();
|
||||
REQUIRE(sg != nullptr);
|
||||
st.setup_cell_images();
|
||||
const auto ref = ModelReferenceIntensities(path, {}, {}, 3.0, logger);
|
||||
const auto ref = ModelReferenceIntensities(ReadModel(path), {}, {}, 3.0, logger);
|
||||
std::filesystem::remove(path);
|
||||
REQUIRE_FALSE(ref.empty());
|
||||
gemmi::AsuData<gemmi::ValueSigma<float>> fobs;
|
||||
@@ -1655,6 +1716,38 @@ TEST_CASE("RigidBodyGPU_Deterministic", "[ModelValidation][gpu]") {
|
||||
}
|
||||
}
|
||||
|
||||
// An engine's calls run on the engine's card and leave the calling thread on its own: every engine of a pool with
|
||||
// one on each card, all taken at once from one thread, evaluated in turn. A thread left on the last engine's card
|
||||
// would run what it does next - a search's function, memory it allocates - on that card, beside data on another.
|
||||
TEST_CASE("RigidBodyGPU_CallsLeaveTheCallerOnItsCard", "[ModelValidation][gpu]") {
|
||||
if (get_gpu_count() == 0)
|
||||
SKIP("No GPU");
|
||||
Logger logger("RigidBodyGPU_CallsLeaveTheCallerOnItsCard");
|
||||
const char *cryst = kRigidBodyCrysts[4];
|
||||
const gemmi::Structure st = AnisoCluster(cryst);
|
||||
const gemmi::SpaceGroup *sg = st.find_spacegroup();
|
||||
const auto fobs = OwnAmplitudes(cryst, st, 3.5, logger);
|
||||
const size_t cards = get_gpu_count();
|
||||
std::vector<gemmi::Model> models(cards, st.models[0]);
|
||||
auto pool = RigidBodyGPUPool::Plan(models[0], st.cell, *sg, 3.5, fobs.v.size(), logger);
|
||||
REQUIRE(pool);
|
||||
pool->Reserve(std::vector<size_t>(cards, 1));
|
||||
const int caller = RigidBodyGPUEngine::CurrentDevice();
|
||||
std::vector<std::unique_ptr<RigidBodyTargetGPU>> targets;
|
||||
for (size_t k = 0; k < cards; k++)
|
||||
targets.push_back(std::make_unique<RigidBodyTargetGPU>(*pool, models[k], st.cell, *sg, 4));
|
||||
CHECK(RigidBodyGPUEngine::CurrentDevice() == caller);
|
||||
const double q0[6] = {0.12, -0.09, 0.07, 0.15, -0.10, 0.05};
|
||||
for (auto &t : targets) {
|
||||
t->SetZone(fobs, 3.5);
|
||||
CHECK(RigidBodyGPUEngine::CurrentDevice() == caller);
|
||||
std::vector<double> r(t->NumObservations()), j(6 * t->NumObservations());
|
||||
REQUIRE(t->Residuals(q0, r.data()));
|
||||
REQUIRE(t->Jacobian(q0, j.data()));
|
||||
CHECK(RigidBodyGPUEngine::CurrentDevice() == caller);
|
||||
}
|
||||
}
|
||||
|
||||
// The model's structure factors on the GPU against the CPU path model validation takes - gemmi's density and
|
||||
// mask on the symmetrized grid, transformed by FFTW and read off by prepare_asu_data() - in the five groups,
|
||||
// at a resolution where the mask's shrink step does nothing and at one where it does. The same reflections
|
||||
|
||||
Reference in new issue
Block a user