// Copyright 2020 Global Phasing Ltd. #include "gemmi/assembly.hpp" #include // unique_ptr #include "gemmi/modify.hpp" // transform_pos_and_adp #include "gemmi/neighbor.hpp" // NeighborSearch namespace gemmi { namespace { bool any_subchain_matches(const Chain& chain, const Assembly::Gen& gen) { const std::string* prev_subchain = nullptr; for (const Residue& res : chain.residues) if (prev_subchain == nullptr || res.subchain != *prev_subchain) { if (in_vector(res.subchain, gen.subchains)) return true; prev_subchain = &res.subchain; } return false; } // chain name mappings old->new for each Assembly::Operator used struct ChainMap { bool uses_segments = false; std::string id; std::map names; }; struct AssemblyMapping { HowToNameCopiedChain how; // records subchain name correspondence (new->old), used for updating Entity::subchain std::map sub; std::vector chain_maps; }; void update_address(AtomAddress& a, const ChainMap& chain_map) { auto it = chain_map.names.find(a.chain_name); if (it != chain_map.names.end()) a.chain_name = it->second; } void remove_cras(Model& model, std::vector& vec) { // sort in reverse order, so items can be erased without invalidating pointers std::sort(vec.begin(), vec.end(), [](const CRA& a, const CRA& b) { return std::tie(a.chain, a.residue, a.atom) > std::tie(b.chain, b.residue, b.atom); }); const Atom* prev_a = nullptr; for (CRA& cra : vec) { if (cra.atom == prev_a) continue; prev_a = cra.atom; auto atom_idx = cra.atom - cra.residue->atoms.data(); cra.residue->atoms.erase(cra.residue->atoms.begin() + atom_idx); if (cra.residue->atoms.empty()) { auto res_idx = cra.residue - cra.chain->residues.data(); cra.chain->residues.erase(cra.chain->residues.begin() + res_idx); if (cra.chain->residues.empty()) { auto chain_idx = cra.chain - model.chains.data(); model.chains.erase(model.chains.begin() + chain_idx); } } } } Model make_assembly_(const Assembly& assembly, const Model& model, HowToNameCopiedChain how, const Logger& logger, AssemblyMapping* mapping) { Model new_model(model.num); ChainNameGenerator namegen(how); std::map subs = model.subchain_to_chain(); int counter = 0; for (const Assembly::Gen& gen : assembly.generators) { bool all_chains = (!gen.chains.empty() && gen.chains[0] == "(all)"); for (const Assembly::Operator& oper : gen.operators) { if (logger.callback) { std::string note = cat("Applying ", oper.name, " to"); if (all_chains) note += " all chains"; else if (!gen.chains.empty()) cat_to(note, " chains: ", join_str(gen.chains, ',')); else if (!gen.subchains.empty()) cat_to(note, " subchains: ", join_str(gen.subchains, ',')); logger.note(note); } if (!all_chains) { for (const std::string& chain_name : gen.chains) if (!model.find_chain(chain_name)) logger.err("no chain ", chain_name); for (const std::string& subchain_name : gen.subchains) if (subs.find(subchain_name) == subs.end()) logger.err("no subchain ", subchain_name); } // chains are not merged here, multiple chains may have the same name ChainMap chain_map; if (counter != 0) { chain_map.uses_segments = (how == HowToNameCopiedChain::Dup); chain_map.id = std::to_string(counter); } for (const Chain& chain : model.chains) { // PDB files specify bioassemblies in terms of chains, // mmCIF files in terms of subchains. bool whole_chain = (all_chains || in_vector(chain.name, gen.chains)); if (whole_chain || (!gen.subchains.empty() && any_subchain_matches(chain, gen))) { // try add a new empty chain, but first figure out the name for it auto result = chain_map.names.emplace(chain.name, ""); if (result.second) // insertion happened - generate a new chain name result.first->second = namegen.make_new_name(chain.name, counter+1); new_model.chains.emplace_back(result.first->second); Chain& new_chain = new_model.chains.back(); // add residues to the chain for (const Residue& res : chain.residues) if (whole_chain || in_vector(res.subchain, gen.subchains)) { new_chain.residues.push_back(res); Residue& new_res = new_chain.residues.back(); transform_pos_and_adp(new_res, oper.transform); if (!new_res.subchain.empty()) { // change subchain name for the residue if (how == HowToNameCopiedChain::Short) new_res.subchain = new_chain.name + ":" + new_res.subchain; else if (how == HowToNameCopiedChain::AddNumber) new_res.subchain += new_chain.name.substr(chain.name.size()); if (mapping) mapping->sub.emplace(new_res.subchain, res.subchain); } if (chain_map.uses_segments) new_res.segment = chain_map.id; } } } if (mapping) mapping->chain_maps.push_back(std::move(chain_map)); ++counter; } } return new_model; } void expand_ncs_model_(Model& model, const std::vector& ncs, HowToNameCopiedChain how, AssemblyMapping* mapping) { // For HowToNameCopiedChain::Dup we used to set segment="0" for original // residues. Now it's left blank. Not sure which is better. size_t orig_size = model.chains.size(); ChainNameGenerator namegen(model, how); if (mapping) { // add identities to AssemblyMapping mapping->chain_maps.emplace_back(); ChainMap& chain_map = mapping->chain_maps.back(); // chain_map.id is left empty for (const Chain& chain : model.chains) { chain_map.names.emplace(chain.name, chain.name); for (const ConstResidueSpan& span : chain.subchains()) { const std::string& sub_id = span.subchain_id(); mapping->sub.emplace(sub_id, sub_id); } } } int counter = 0; for (const NcsOp& op : ncs) if (!op.given) { ChainMap chain_map; chain_map.uses_segments = (how == HowToNameCopiedChain::Dup); chain_map.id = op.id; ++counter; for (size_t i = 0; i != orig_size; ++i) { model.chains.push_back(model.chains[i]); Chain& new_chain = model.chains.back(); const std::string& old_name = model.chains[i].name; auto result = chain_map.names.emplace(old_name, ""); if (how != HowToNameCopiedChain::Dup) { if (result.second) // if insertion happened - generate a new chain name result.first->second = namegen.make_new_name(old_name, counter); new_chain.name = result.first->second; } for (Residue& new_res : new_chain.residues) { transform_pos_and_adp(new_res, op.tr); if (!new_res.subchain.empty()) { std::string old_subchain = new_res.subchain; new_res.subchain = new_chain.name + ":" + new_res.subchain; if (mapping) mapping->sub.emplace(new_res.subchain, old_subchain); } if (chain_map.uses_segments) new_res.segment = chain_map.id; } } if (mapping) mapping->chain_maps.push_back(std::move(chain_map)); } } void finalize_expansion(Structure& st, const AssemblyMapping& mapping, double merge_dist, bool expanding_ncs) { if (merge_dist > 0) for (Model& model : st.models) { merge_atoms_in_expanded_model(model, gemmi::UnitCell(), merge_dist); assign_serial_numbers(model); } // update Entity::subchains if (!mapping.sub.empty()) for (Entity& ent : st.entities) { std::vector new_subchains; for (const std::string& s : ent.subchains) for (const auto& new_old : mapping.sub) if (new_old.second == s) new_subchains.push_back(new_old.first); ent.subchains = std::move(new_subchains); } // connections std::vector new_connections; for (const Connection& conn : st.connections) if (expanding_ncs || conn.asu == Asu::Same) { bool first = true; for (const ChainMap& chain_map : mapping.chain_maps) { auto ch1 = chain_map.names.find(conn.partner1.chain_name); auto ch2 = chain_map.names.find(conn.partner2.chain_name); if (ch1 != chain_map.names.end() && ch2 != chain_map.names.end()) { Connection new_conn = conn; new_conn.partner1.chain_name = ch1->second; new_conn.partner2.chain_name = ch2->second; if (chain_map.uses_segments) new_conn.partner1.res_id.segment = new_conn.partner2.res_id.segment = chain_map.id; if (st.models[0].find_atom(new_conn.partner1) && st.models[0].find_atom(new_conn.partner2)) { if (!first) { cat_to(new_conn.name, '.', chain_map.id); first = false; } new_connections.push_back(new_conn); } } } } else { // connections other than 1_555 are lost when making assembly // if it's needed - get in touch } st.connections = std::move(new_connections); if (mapping.how == HowToNameCopiedChain::Dup) return; // cispeps std::vector new_cispeps; new_cispeps.reserve(st.cispeps.size() * mapping.chain_maps.size()); for (const CisPep& cispep : st.cispeps) for (const ChainMap& chain_map : mapping.chain_maps) { new_cispeps.push_back(cispep); update_address(new_cispeps.back().partner_c, chain_map); update_address(new_cispeps.back().partner_n, chain_map); } st.cispeps = std::move(new_cispeps); // secondary structure - helices std::vector new_helices; new_helices.reserve(st.helices.size() * mapping.chain_maps.size()); for (const Helix& helix : st.helices) for (const ChainMap& chain_map : mapping.chain_maps) { new_helices.push_back(helix); update_address(new_helices.back().start, chain_map); update_address(new_helices.back().end, chain_map); } st.helices = std::move(new_helices); // secondary structure - sheets std::vector new_sheets; new_sheets.reserve(st.sheets.size() * mapping.chain_maps.size()); for (const Sheet& sheet : st.sheets) for (const ChainMap& chain_map : mapping.chain_maps) { new_sheets.push_back(sheet); for (Sheet::Strand& strand : new_sheets.back().strands) { update_address(strand.start, chain_map); update_address(strand.end, chain_map); update_address(strand.hbond_atom2, chain_map); update_address(strand.hbond_atom1, chain_map); } } st.sheets = new_sheets; } } // anonymous namespace Model make_assembly(const Assembly& assembly, const Model& model, HowToNameCopiedChain how, const Logger& logging) { return make_assembly_(assembly, model, how, logging, nullptr); } void transform_to_assembly(Structure& st, const std::string& assembly_name, HowToNameCopiedChain how, const Logger& logging, bool keep_spacegroup, double merge_dist) { const Assembly* assembly = st.find_assembly(assembly_name); std::unique_ptr p1_assembly; if (!assembly) { if (assembly_name == "unit_cell") { p1_assembly.reset(new Assembly(pseudo_assembly_for_unit_cell(st.cell))); assembly = p1_assembly.get(); } else if (st.assemblies.empty()) { fail("no bioassemblies are listed for this structure"); } else { fail("wrong assembly name, use one of: " + join_str(st.assemblies, ' ', [](const Assembly& a) { return a.name; })); } } AssemblyMapping mapping; mapping.how = how; AssemblyMapping* mapping_ptr = &mapping; for (Model& model : st.models) { model = make_assembly_(*assembly, model, how, logging, mapping_ptr); mapping_ptr = nullptr; // AssemblyMapping is based only on the first model } finalize_expansion(st, mapping, merge_dist, false); // Should Assembly instructions be kept or removed? Currently - removing. st.assemblies.clear(); if (!keep_spacegroup) { st.spacegroup_hm = "P 1"; // maybe it should be empty? if (assembly_name != "unit_cell") st.cell = UnitCell(); } else { st.cell.images.clear(); } } Model expand_ncs_model(const Model& model, const std::vector& ncs, HowToNameCopiedChain how) { Model model_copy = model; expand_ncs_model_(model_copy, ncs, how, nullptr); return model_copy; } void expand_ncs(Structure& st, HowToNameCopiedChain how, double merge_dist) { AssemblyMapping mapping; mapping.how = how; AssemblyMapping* mapping_ptr = &mapping; for (Model& model : st.models) { expand_ncs_model_(model, st.ncs, how, mapping_ptr); mapping_ptr = nullptr; // AssemblyMapping is based only on the first model } finalize_expansion(st, mapping, merge_dist, true); for (NcsOp& op : st.ncs) op.given = true; st.setup_cell_images(); } void merge_atoms_in_expanded_model(Model& model, const UnitCell& cell, double max_dist, bool compare_serial) { using Mark = NeighborSearch::Mark; NeighborSearch ns(model, cell, 4.0); ns.populate(true); std::vector to_be_deleted; for (int n_ch = 0; n_ch != (int) model.chains.size(); ++n_ch) { Chain& chain = model.chains[n_ch]; for (int n_res = 0; n_res != (int) chain.residues.size(); ++n_res) { Residue& res = chain.residues[n_res]; for (int n_atom = 0; n_atom != (int) res.atoms.size(); ++n_atom) { Atom& atom = res.atoms[n_atom]; std::vector> equiv; ns.for_each_cell(atom.pos, [&](std::vector& marks, const Fractional& fr) { for (Mark& m : marks) { // We look for the same atoms, but copied to a different chain. // First quick check that filters out most of non-matching pairs. if (m.altloc != atom.altloc || m.element != atom.element || m.chain_idx == n_ch || m.atom_idx != n_atom) continue; // Now check if everything else matches. CRA cra = m.to_cra(model); if (cra.atom && (!compare_serial || cra.atom->serial == atom.serial) && cra.atom->name == atom.name && cra.atom->b_iso == atom.b_iso && cra.residue->matches_noseg(res) && m.pos.dist_sq(ns.grid.unit_cell.orthogonalize(fr)) < sq(max_dist)) equiv.emplace_back(cra, m.image_idx); } }); if (!equiv.empty()) { Position pos_sum = atom.pos; for (auto& t : equiv) { CRA& cra = t.first; pos_sum += ns.grid.unit_cell.find_nearest_pbc_position( atom.pos, cra.atom->pos, t.second); // The atoms in equiv are to be discarded later. // Deleting now would invalidate indices in NeighborSearch. to_be_deleted.push_back(cra); // Modify the atoms to avoid processing them again. cra.atom->serial = -1; cra.atom->name.clear(); } size_t n = 1 + equiv.size(); atom.pos = pos_sum / double(n); atom.occ = std::min(1.f, n * atom.occ); } } } } remove_cras(model, to_be_deleted); } void shorten_chain_names(Structure& st) { ChainNameGenerator namegen(HowToNameCopiedChain::Short); Model& model0 = st.models[0]; size_t max_len = model0.chains.size() < 63 ? 1 : 2; for (const Chain& chain : model0.chains) if (chain.name.length() <= max_len) namegen.used_names.push_back(chain.name); for (Chain& chain : model0.chains) if (chain.name.length() > max_len) rename_chain(st, chain.name, namegen.make_short_name(chain.name.substr(0, max_len))); } static std::vector split_chain_by_segments(Chain& orig, ChainNameGenerator& namegen) { std::vector chains; std::vector orig_res; orig_res.swap(orig.residues); int n = 0; for (auto start = orig_res.begin(); start != orig_res.end(); ) { const std::string& seg = start->segment; auto ch = std::find_if(chains.begin(), chains.end(), [&](Chain& c) { return !c.residues.empty() && c.residues[0].segment == seg; }); if (ch == chains.end()) { chains.push_back(orig); ch = chains.end() - 1; // Naming. Here, Dup means chain name + segment. switch (namegen.how) { case HowToNameCopiedChain::Short: ch->name = namegen.make_short_name(ch->name + seg); break; case HowToNameCopiedChain::AddNumber: ch->name = namegen.make_name_with_numeric_postfix(ch->name, ++n); break; case HowToNameCopiedChain::Dup: ch->name += seg; break; } } auto end = std::find_if(start, orig_res.end(), [&](Residue& r) { return r.segment != seg; }); ch->residues.insert(ch->residues.end(), std::make_move_iterator(start), std::make_move_iterator(end)); start = end; } return chains; } void split_chains_by_segments(Model& model, HowToNameCopiedChain how) { ChainNameGenerator namegen(how); std::vector new_chains; for (Chain& chain : model.chains) vector_move_extend(new_chains, split_chain_by_segments(chain, namegen)); model.chains = std::move(new_chains); } } // namespace gemmi