diff --git a/layer2/ObjectMolecule.cpp b/layer2/ObjectMolecule.cpp index d5fa4f655..ef64dce0a 100644 --- a/layer2/ObjectMolecule.cpp +++ b/layer2/ObjectMolecule.cpp @@ -11208,6 +11208,31 @@ int ObjectMoleculeSetStateOrder(ObjectMolecule * I, int * order, int len) { return false; } +/*========================================================================*/ +pymol::Result<> ObjectMoleculeDeleteStates(ObjectMolecule* I, const std::vector& states) +{ + auto& CSet = I->CSet; + + // first pass, check for invalid states + for (auto state : states) { + if (state < 0 || state >= I->NCSet) { + auto msg = pymol::string_format("Invalid state index: %d", state); + I->G->Feedback->addColored(msg.c_str(), FB_Errors); + return {}; + } + } + + // second pass, delete states + for (auto it = states.rbegin(); it != states.rend(); ++it) { + int state = *it; + DeleteP(CSet[state]); + CSet.erase(state, 1); + } + I->NCSet -= states.size(); + CSet.resize(I->NCSet); + return {}; +} + /*========================================================================*/ ObjectMolecule::~ObjectMolecule() { diff --git a/layer2/ObjectMolecule.h b/layer2/ObjectMolecule.h index d158e7d9b..c6b6dcc45 100644 --- a/layer2/ObjectMolecule.h +++ b/layer2/ObjectMolecule.h @@ -308,6 +308,14 @@ int ObjectMoleculeCheckFullStateSelection(ObjectMolecule * I, int sele, int stat int ObjectMoleculeSetStateOrder(ObjectMolecule * I, int * order, int len); +/** + * @brief Deletes a CoordSet/State from a molecule object + * @param I ObjectMolecule + * @param states state indices (0-based) + * @note This function only works on non-discrete molecular objects + */ +pymol::Result<> ObjectMoleculeDeleteStates(ObjectMolecule* I, const std::vector& state); + int ObjectMoleculeAddPseudoatom(ObjectMolecule * I, int sele_index, const char *name, const char *resn, const char *resi, const char *chain, const char *segi, const char *elem, float vdw, diff --git a/layer3/Executive.cpp b/layer3/Executive.cpp index 3364e2e35..86ed55075 100644 --- a/layer3/Executive.cpp +++ b/layer3/Executive.cpp @@ -14278,6 +14278,30 @@ pymol::Result> ExecutiveDelete(PyMOLGlobals * G, pymol return discardedRecs; } +pymol::Result<> ExecutiveDeleteStates( + PyMOLGlobals* G, std::string_view name, const std::vector& states) +{ + for (auto& rec : ExecutiveGetSpecRecsFromPattern(G, name.data())) { + if (rec.type != cExecObject) { + continue; + } + auto* obj = rec.obj; + if (obj->type != cObjectMolecule) { + continue; + } + auto* mol = static_cast(obj); + if (mol->DiscreteFlag) { + G->Feedback->addColored( + "Error: Cannot delete states from discrete objects.\n", FB_Warnings); + continue; + } + ObjectMoleculeDeleteStates(mol, states); + } + SceneChanged(G); + ExecutiveInvalidatePanelList(G); + return {}; +} + void ExecutiveReAddSpec(PyMOLGlobals* G, std::vector& specs) { auto I = G->Executive; diff --git a/layer3/Executive.h b/layer3/Executive.h index c40a552dd..9a84c043a 100644 --- a/layer3/Executive.h +++ b/layer3/Executive.h @@ -341,6 +341,16 @@ pymol::CObject** ExecutiveFindObjectsByType(PyMOLGlobals * G, int objType); int ExecutiveIterateObject(PyMOLGlobals * G, pymol::CObject ** obj, void **hidden); pymol::Result> ExecutiveDelete(PyMOLGlobals * G, pymol::zstring_view nameView, bool save = false); +/** + * @brief Deletes states from an object + * @param name name of the object + * @param states list of states to be deleted (0-based, must be sorted and + * unique) + * @note Currently only works on non-discrete molecular objects + */ +pymol::Result<> ExecutiveDeleteStates( + PyMOLGlobals* G, std::string_view name, const std::vector& states); + /** * @brief Unregisters the specification record from PyMOL * @param rec specification record to be purged/removed diff --git a/layer4/Cmd.cpp b/layer4/Cmd.cpp index 0764ee6d5..e134a4fa4 100644 --- a/layer4/Cmd.cpp +++ b/layer4/Cmd.cpp @@ -5178,6 +5178,20 @@ static PyObject *CmdDelete(PyObject * self, PyObject * args) return APISuccess(); } +static PyObject *CmdDeleteStates(PyObject * self, PyObject * args) +{ + PyMOLGlobals* G = nullptr; + const char* objName; + PyObject* statesPyList; + API_SETUP_ARGS(G, self, args, "OsO", &self, &objName, &statesPyList); + std::vector states; + PConvFromPyObject(G, statesPyList, states); + API_ASSERT(APIEnterNotModal(G)); + auto result = ExecutiveDeleteStates(G, objName, states); + APIExit(G); + return APIResult(G, result); +} + static PyObject *CmdCartoon(PyObject * self, PyObject * args) { PyMOLGlobals *G = nullptr; @@ -6275,6 +6289,7 @@ static PyMethodDef Cmd_methods[] = { {"del_colorection", CmdDelColorection, METH_VARARGS}, {"fake_drag", CmdFakeDrag, METH_VARARGS}, {"delete", CmdDelete, METH_VARARGS}, + {"delete_states", CmdDeleteStates, METH_VARARGS}, {"dirty", CmdDirty, METH_VARARGS}, {"dirty_wizard", CmdDirtyWizard, METH_VARARGS}, {"dihedral", CmdDihedral, METH_VARARGS}, diff --git a/modules/pymol/api.py b/modules/pymol/api.py index 4b24bb8b6..25335c494 100644 --- a/modules/pymol/api.py +++ b/modules/pymol/api.py @@ -66,6 +66,7 @@ async_, \ cls, \ delete, \ + delete_states, \ do, \ log, \ log_close, \ diff --git a/modules/pymol/commanding.py b/modules/pymol/commanding.py index 470994f33..c3ab2fd0b 100644 --- a/modules/pymol/commanding.py +++ b/modules/pymol/commanding.py @@ -528,6 +528,7 @@ def delete(name, *, _self=cmd): SEE ALSO remove + delete_states ''' r = DEFAULT_ERROR try: @@ -538,6 +539,63 @@ def delete(name, *, _self=cmd): if _self._raising(r,_self): raise pymol.CmdException return r + def delete_states(name: str, states: str, *, _self=cmd): + ''' +DESCRIPTION + +USAGE + + delete_states name, states + +ARGUMENTS + + name = name(s) of object(s), supports wildcards (*) + + states = string: space separated list of state numbers or ranges + +EXAMPLES + + delete_state 1nmr, 1-5 # delete states 1 to 5 from 1nmr + delete_state *, 1-3 10-40 # deletes states 1 to 3 and 10 to 40 from all applicable objects + +PYMOL API + + cmd.delete_states(string name = object-name, string states = states string) + +SEE ALSO + + delete + ''' + + def get_state_list(states_str): + output=[] + input_str = re.sub(r"\s"," ", states_str) + input_str = input_str.replace("-"," -") + input_str = input_str.replace("- ","-") + input_str = input_str.strip().split() + + last = -1 + for x in input_str: + if not x.isdigit(): + if x.startswith("-"): + direction = 1 + current = last + last = int(x[1:]) - 1 + if last < current: + direction = -1 + while current != last: + current += direction + output.append(str(current)) + else: + val = int(x) - 1 + output.append(str(val)) + last = val + return output + + with _self.lockcm: + output = get_state_list(states) + states_list = sorted(set(map(int, output))) + return _cmd.delete_states(_self._COb, name, states_list) class Selection(str): pass diff --git a/modules/pymol/keywords.py b/modules/pymol/keywords.py index fb8c8d022..2e5fc3be6 100644 --- a/modules/pymol/keywords.py +++ b/modules/pymol/keywords.py @@ -59,6 +59,7 @@ def get_command_keywords(self_cmd=cmd): 'create' : [ self_cmd.create , 0 , 0 , '' , parsing.LEGACY ], 'decline' : [ self_cmd.decline , 0 , 0 , '' , parsing.STRICT ], 'delete' : [ self_cmd.delete , 0 , 0 , '' , parsing.STRICT ], + 'delete_states' : [ self_cmd.delete_states , 0 , 0 , '' , parsing.STRICT ], 'def' : [ self_cmd.python_help , 0 , 0 , '' , parsing.PYTHON ], 'del' : [ self_cmd.python_help , 0 , 0 , '' , parsing.PYTHON ], 'deprotect' : [ self_cmd.deprotect , 0 , 0 , '' , parsing.STRICT ], diff --git a/testing/tests/api/commanding.py b/testing/tests/api/commanding.py index e9d671224..b8bd4a541 100644 --- a/testing/tests/api/commanding.py +++ b/testing/tests/api/commanding.py @@ -37,6 +37,12 @@ def testDelete(self): cmd.delete('m1 m2') self.assertEqual(cmd.get_names(), ['m3']) + def testDeleteStates(self): + cmd.pseudoatom('m1', state=10) + self.assertEqual(cmd.count_states('m1'), 10) + cmd.delete_states('m1', '4-8') + self.assertEqual(cmd.count_states('m1'), 5) + def testDo(self): # tested with other methods pass