Skip to content

Commit b3ad7b4

Browse files
SKETCH-2763: Generate complement strand for nucleic acids
1 parent f253631 commit b3ad7b4

11 files changed

Lines changed: 692 additions & 12 deletions

File tree

src/schrodinger/rdkit_extensions/helm/monomer_coordgen.cpp

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,18 +50,6 @@ namespace rdkit_extensions
5050

5151
using schrodinger::rdkit_extensions::Direction;
5252

53-
// empty space gap between a monomer and the one following it in the chain. This
54-
// will be the length of the visibile bond line connecting the two.
55-
constexpr double SIDE_TO_SIDE_DISTANCE = 0.70;
56-
// when a monomer size is not specified or its value is lower than this, this
57-
// value is used as the minimum size
58-
constexpr double MONOMER_MINIMUM_SIZE = 0.80;
59-
60-
// total distance from the center of one monomer to the center of the following
61-
// in the chain
62-
constexpr double MONOMER_BOND_LENGTH =
63-
SIDE_TO_SIDE_DISTANCE + MONOMER_MINIMUM_SIZE;
64-
6553
// maximum allowed bond length as a multiple of the ideal bond length. Any bond
6654
// longer than this will be considered "stretched"
6755
constexpr double MAX_BOND_STRETCH = 3.0;

src/schrodinger/rdkit_extensions/helm/monomer_coordgen.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ namespace rdkit_extensions
2121
// measured in scene units
2222
const std::string MONOMER_ITEM_SIZE{"monomerItemSize"};
2323

24+
// Empty-space gap between a monomer and the next in the chain (the length of
25+
// the visible bond line connecting them).
26+
constexpr double SIDE_TO_SIDE_DISTANCE = 0.70;
27+
// Minimum monomer size, used when a size is unspecified or smaller than this.
28+
constexpr double MONOMER_MINIMUM_SIZE = 0.80;
29+
// Center-to-center distance between adjacent monomers in a chain.
30+
constexpr double MONOMER_BOND_LENGTH =
31+
SIDE_TO_SIDE_DISTANCE + MONOMER_MINIMUM_SIZE;
32+
2433
/**
2534
* Information about a turn in a snaking or coiling chain layout.
2635
*/

src/schrodinger/sketcher/menu/monomer_context_menu.cpp

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,50 @@ namespace schrodinger
1616
namespace sketcher
1717
{
1818

19+
// True when every selected monomer is part of a nucleic acid chain (no
20+
// peptides, no CHEM) AND at least one is an NA base — the gate for the
21+
// "Add Complementary Sequence" action visibility.
22+
static bool na_selection_with_at_least_one_base(
23+
const std::unordered_set<const RDKit::Atom*>& atoms)
24+
{
25+
// Any non-NA monomer type (peptide, CHEM, or an unknown future variant)
26+
// disqualifies the whole selection.
27+
auto is_na_monomer = [](const RDKit::Atom* a) {
28+
switch (get_monomer_type(a)) {
29+
case MonomerType::NA_BASE:
30+
case MonomerType::NA_SUGAR:
31+
case MonomerType::NA_PHOSPHATE:
32+
return true;
33+
default:
34+
return false;
35+
}
36+
};
37+
auto is_na_base = [](const RDKit::Atom* a) {
38+
return get_monomer_type(a) == MonomerType::NA_BASE;
39+
};
40+
// any_of is false for an empty selection, so no explicit empty guard.
41+
return std::ranges::all_of(atoms, is_na_monomer) &&
42+
std::ranges::any_of(atoms, is_na_base);
43+
}
44+
45+
// Filter the menu's selection down to NA bases whose symbol has a
46+
// Watson-Crick complement. Used to drive both the "enabled" state of
47+
// the action and the payload of the emitted signal.
48+
static std::unordered_set<const RDKit::Atom*>
49+
complementable_bases(const std::unordered_set<const RDKit::Atom*>& atoms)
50+
{
51+
std::unordered_set<const RDKit::Atom*> out;
52+
for (const auto* a : atoms) {
53+
if (get_monomer_type(a) != MonomerType::NA_BASE) {
54+
continue;
55+
}
56+
if (na_base_has_complement(get_monomer_res_name(a))) {
57+
out.insert(a);
58+
}
59+
}
60+
return out;
61+
}
62+
1963
MonomerContextMenu::MonomerContextMenu(QWidget* parent) :
2064
AbstractContextMenu(parent)
2165
{
@@ -25,6 +69,7 @@ MonomerContextMenu::MonomerContextMenu(QWidget* parent) :
2569
createProtonateAction();
2670
createMutateBaseSubMenu();
2771
createSugarToggleAction();
72+
createAddComplementaryStrandAction();
2873
createDeleteAction();
2974
}
3075

@@ -174,6 +219,19 @@ void MonomerContextMenu::createSugarToggleAction()
174219
});
175220
}
176221

222+
void MonomerContextMenu::createAddComplementaryStrandAction()
223+
{
224+
m_add_complement_action =
225+
addAction("Add Complementary Sequence", this, [this]() {
226+
// Reuse the set computed in updateActions() rather than walking
227+
// the selection a third time.
228+
if (m_complement_bases.empty()) {
229+
return;
230+
}
231+
emit addComplementaryStrandRequested(m_complement_bases);
232+
});
233+
}
234+
177235
void MonomerContextMenu::createDeleteAction()
178236
{
179237
addAction("Delete", this, [this]() { emit deleteRequested(m_atoms); });
@@ -194,6 +252,14 @@ void MonomerContextMenu::updateActions()
194252
m_mutate_base_menu->menuAction()->setVisible(all_na_base);
195253
m_sugar_toggle_action->setVisible(all_na_sugar);
196254

255+
m_complement_bases = complementable_bases(m_atoms);
256+
const bool na_with_base = na_selection_with_at_least_one_base(m_atoms);
257+
m_add_complement_action->setVisible(na_with_base);
258+
// Enabled iff at least one selected base has a Watson-Crick complement
259+
// symbol; DB-level validation happens model-side.
260+
m_add_complement_action->setEnabled(na_with_base &&
261+
!m_complement_bases.empty());
262+
197263
if (all_peptide) {
198264
bool any_d_form_toggleable = false;
199265
for (const auto* a : m_atoms) {

src/schrodinger/sketcher/menu/monomer_context_menu.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ class SKETCHER_API MonomerContextMenu : public AbstractContextMenu
4747
void mutateMonomerRequested(std::vector<MonomerMutation> mutations,
4848
QString description);
4949

50+
// `bases` holds raw atom pointers into the live molecule, so the receiver
51+
// must consume them before it starts mutating the molecule.
52+
void addComplementaryStrandRequested(
53+
const std::unordered_set<const RDKit::Atom*>& bases);
54+
5055
protected:
5156
void updateActions() override;
5257

@@ -56,13 +61,20 @@ class SKETCHER_API MonomerContextMenu : public AbstractContextMenu
5661
void createProtonateAction();
5762
void createMutateBaseSubMenu();
5863
void createSugarToggleAction();
64+
void createAddComplementaryStrandAction();
5965
void createDeleteAction();
6066

6167
QMenu* m_mutate_residue_menu = nullptr;
6268
QAction* m_set_d_form_action = nullptr;
6369
QAction* m_protonate_action = nullptr;
6470
QMenu* m_mutate_base_menu = nullptr;
6571
QAction* m_sugar_toggle_action = nullptr;
72+
QAction* m_add_complement_action = nullptr;
73+
74+
// Bases from the current selection that have a Watson-Crick complement.
75+
// Computed once per updateActions() and reused both to set the action's
76+
// enabled state and as the payload when the action is triggered.
77+
std::unordered_set<const RDKit::Atom*> m_complement_bases;
6678
};
6779

6880
} // namespace sketcher

0 commit comments

Comments
 (0)