// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #include "WriteModel.h" #include #include #include // make_mmcif_document #include // write_cif_to_stream #include // write_pdb #include // setup_entities #include "../common/Logger.h" namespace { // Do the coordinates of a model built in `coordinates_group` obey `label` as well? They do when every // operation of `label` is one of theirs, i.e. when `label` is a subgroup: the file then holds more // than one asymmetric unit of it, which is allowed and is what a model always does in P1. They do not // when `label` has an operation of its own, and then the label is a lie a reader acts on - it // generates atoms from that operation, and the crystal it builds is not the one in the file. bool coordinates_obey(const gemmi::SpaceGroup &coordinates_group, const gemmi::SpaceGroup &label) { const std::vector coordinate_ops = coordinates_group.operations().all_ops_sorted(); const std::vector label_ops = label.operations().all_ops_sorted(); return std::includes(coordinate_ops.begin(), coordinate_ops.end(), label_ops.begin(), label_ops.end()); } } // namespace void WritePlacedModel(const gemmi::Structure &placed, const UnitCell &cell, const gemmi::SpaceGroup &space_group, const std::string &output_prefix, Logger &logger) { const std::string cif_path = output_prefix + "_model.cif"; // PDB as well as mmCIF, because the fragment-screening tools this file exists for take a PDB: // PanDDA's per-dataset input is .pdb beside .mtz, and dimple produces the same pair. // Same coordinates, same frame, both formats - a PDB cannot hold every cell, so it is written // when it can be and skipped, with a line saying so, when it cannot. const std::string pdb_path = output_prefix + "_model.pdb"; gemmi::Structure st = placed; // The cell is the data's, taken from the same value the reflection files are written from: the // coordinates were re-fractionalized into it and genuinely sit there. // // The group is the data's too wherever the coordinates obey it, which is the ordinary case and the // one that matters - the label then matches the .mtz beside it, the enantiomorph in particular, // since --model can adopt the model's and that is neither the data's original label nor, // necessarily, the one the input model arrived with. Where they do not obey it the model's own // group is written instead. Data merged in a supergroup of the model's - a run that over-merged // across a pseudo-symmetry twofold, or a hand the model's fit did not earn - would otherwise put // operations on the file that its contents contradict, and a refinement program expands them: the // structure it refines is not the model, and it says so nowhere. A pair whose two files disagree // is worth a warning; a coordinate file that is quietly wrong on its own is not worth writing. const gemmi::SpaceGroup *model_group = st.find_spacegroup(); const bool takes_model_group = model_group != nullptr && !coordinates_obey(*model_group, space_group); const gemmi::SpaceGroup &label = takes_model_group ? *model_group : space_group; st.cell = cell; st.spacegroup_hm = label.xhm(); st.setup_cell_images(); // Fills in entity types and label_asym_id for a model read from a PDB, which carries neither. // Both are no-ops where the input already had them, i.e. for an mmCIF input. // setup_entities and make_mmcif_document can both throw, and this runs BEFORE the reflection files // are written: a convenience deliverable must not be able to take the run's actual output with it. // The map writer does the same. try { gemmi::setup_entities(st); std::ofstream os(cif_path); gemmi::cif::write_cif_to_stream(os, gemmi::make_mmcif_document(st)); if (!os) { logger.Error("Model validation: cannot write the placed model to {}", cif_path); return; } } catch (const std::exception &e) { logger.Warning("Model validation: could not write the placed model to {} ({})", cif_path, e.what()); return; } bool pdb_written = false; try { std::ofstream os(pdb_path); gemmi::write_pdb(st, os); pdb_written = static_cast(os); } catch (const std::exception &e) { logger.Warning("Model validation: the placed model could not also be written as PDB ({}); " "{} has it", e.what(), cif_path); } if (!takes_model_group) { logger.Info("Model validation: the model as placed against these data written to {}{} " "(cell and space group {} as in the reflection files)", cif_path, pdb_written ? " and " + pdb_path : "", space_group.short_name()); } else { logger.Warning("Model validation: the model as placed written to {}{} in ITS OWN space group " "{}, not the {} the reflection files carry - its coordinates do not obey {}, so " "the two files describe the same crystal in different symmetry and cannot be " "refined against each other as they stand", cif_path, pdb_written ? " and " + pdb_path : "", label.short_name(), space_group.short_name(), space_group.short_name()); } }