1 package uk.ac.cam.ch.wwmm.opsin; 2 3 import java.util.ArrayList; 4 import java.util.Collection; 5 import java.util.Collections; 6 import java.util.HashMap; 7 import java.util.Iterator; 8 import java.util.LinkedHashMap; 9 import java.util.LinkedHashSet; 10 import java.util.List; 11 import java.util.Map; 12 import java.util.Set; 13 import java.util.regex.Matcher; 14 15 import static uk.ac.cam.ch.wwmm.opsin.OpsinTools.*; 16 import static uk.ac.cam.ch.wwmm.opsin.XmlDeclarations.*; 17 18 /**A fragment of a molecule, holds bonds and atoms. 19 * 20 * @author ptc24 21 * @author dl387 22 * 23 */ 24 class Fragment implements Iterable<Atom> { 25 26 /**A mapping between IDs and the atoms in this fragment, by default is ordered by the order atoms are added to the fragment*/ 27 private final Map<Integer, Atom> atomMapFromId = new LinkedHashMap<Integer, Atom>(); 28 29 /**Equivalent to and synced to atomMapFromId.values() */ 30 private final Collection<Atom> atomCollection = atomMapFromId.values(); 31 32 /**A mapping between locants and the atoms in this fragment*/ 33 private final Map<String, Atom> atomMapFromLocant = new HashMap<String, Atom>(); 34 35 /**The bonds in the fragment*/ 36 private final Set<Bond> bondSet = new LinkedHashSet<Bond>(); 37 38 /**The associated token element*/ 39 private Element tokenEl; 40 41 /**The atoms that are used when this fragment is connected to another fragment. Unused outAtoms means that the fragment is a radical or an error has occurred 42 * Initially empty */ 43 private final List<OutAtom> outAtoms = new ArrayList<OutAtom>(); 44 45 /**The atoms that are used on this fragment to form things like esters 46 * Initially empty */ 47 private final List<FunctionalAtom> functionalAtoms = new ArrayList<FunctionalAtom>(); 48 49 /**The atom that fragments connecting to this fragment should connect to in preference 50 * e.g. for amino acids the alpha amino group 51 * Null by default*/ 52 private Atom defaultInAtom = null; 53 54 /**The atoms in the fragment that have been indicated to have hydrogen at the SMILES level.*/ 55 private final List<Atom> indicatedHydrogen = new ArrayList<Atom>(); 56 57 /**Pseudo atoms indicating start and end of polymer structure repeat unit*/ 58 private List<Atom> polymerAttachmentPoints = null; 59 60 /** 61 * DO NOT CALL DIRECTLY EXCEPT FOR TESTING 62 * Makes an empty Fragment associated with the given tokenEl 63 * @param tokenEl 64 */ Fragment(Element tokenEl)65 Fragment(Element tokenEl) { 66 this.tokenEl = tokenEl; 67 } 68 69 /** 70 * DO NOT CALL DIRECTLY EXCEPT FOR TESTING 71 * Makes an empty Fragment with the given type 72 * 73 * @param type 74 */ Fragment(String type)75 Fragment(String type) { 76 this.tokenEl = new TokenEl(""); 77 this.tokenEl.addAttribute(TYPE_ATR, type); 78 } 79 80 /**Adds an atom to the fragment and associates it with this fragment*/ addAtom(Atom atom)81 void addAtom(Atom atom) { 82 List<String> locants =atom.getLocants(); 83 for (String locant: locants) { 84 atomMapFromLocant.put(locant, atom); 85 } 86 atomMapFromId.put(atom.getID(), atom); 87 atom.setFrag(this); 88 } 89 90 /** 91 * Return the number of atoms in the fragment 92 * @return 93 */ getAtomCount()94 int getAtomCount() { 95 return atomCollection.size(); 96 } 97 98 /** 99 * Returns a copy of the fragment's atoms 100 * @return 101 */ getAtomList()102 List<Atom> getAtomList() { 103 return new ArrayList<Atom>(atomCollection); 104 } 105 106 /** 107 * Adds a bond to the fragment. 108 * @param bond 109 */ addBond(Bond bond)110 void addBond(Bond bond) { 111 bondSet.add(bond); 112 } 113 114 /**Removes a bond to the fragment if it is present. 115 * @param bond 116 * @return*/ removeBond(Bond bond)117 boolean removeBond(Bond bond) { 118 return bondSet.remove(bond); 119 } 120 121 /**Gets bondSet.*/ getBondSet()122 Set<Bond> getBondSet() { 123 return Collections.unmodifiableSet(bondSet); 124 } 125 126 /**Gets the id of the atom in the fragment with the specified locant. 127 * 128 * @param locant The locant to look for 129 * @return The id of the found atom, or 0 if it is not found 130 */ getIDFromLocant(String locant)131 int getIDFromLocant(String locant) { 132 Atom a = getAtomByLocant(locant); 133 if (a != null){ 134 return a.getID(); 135 } 136 return 0; 137 } 138 139 /**Gets the id of the atom in the fragment with the specified locant, throwing if this fails. 140 * 141 * @param locant The locant to look for 142 * @return The id of the found atom 143 * @throws StructureBuildingException 144 */ getIDFromLocantOrThrow(String locant)145 int getIDFromLocantOrThrow(String locant) throws StructureBuildingException { 146 int id = getIDFromLocant(locant); 147 if(id == 0) { 148 throw new StructureBuildingException("Couldn't find id from locant " + locant + "."); 149 } 150 return id; 151 } 152 153 /**Gets the atom in the fragment with the specified locant. 154 * 155 * @param locant The locant to look for 156 * @return The found atom, or null if it is not found 157 */ getAtomByLocant(String locant)158 Atom getAtomByLocant(String locant) { 159 Atom a =atomMapFromLocant.get(locant); 160 if (a != null){ 161 return a; 162 } 163 Matcher m =MATCH_AMINOACID_STYLE_LOCANT.matcher(locant); 164 if (m.matches()){//e.g. N5 165 Atom backboneAtom =atomMapFromLocant.get(m.group(3));//the atom corresponding to the numeric or greek component 166 if (backboneAtom==null){ 167 return null; 168 } 169 a = FragmentTools.getAtomByAminoAcidStyleLocant(backboneAtom, m.group(1), m.group(2)); 170 if (a != null){ 171 return a; 172 } 173 } 174 return null; 175 } 176 177 /**Gets the atom in the fragment with the specified locant, throwing if this fails. 178 * 179 * @param locant The locant to look for 180 * @return The found atom 181 * @throws StructureBuildingException 182 */ getAtomByLocantOrThrow(String locant)183 Atom getAtomByLocantOrThrow(String locant) throws StructureBuildingException { 184 Atom a = getAtomByLocant(locant); 185 if(a == null) { 186 throw new StructureBuildingException("Could not find the atom with locant " + locant + "."); 187 } 188 return a; 189 } 190 191 /**Gets the atom in the fragment with the specified ID. 192 * 193 * @param id The id of the atom. 194 * @return The found atom, or null. 195 */ getAtomByID(int id)196 Atom getAtomByID(int id) { 197 return atomMapFromId.get(id); 198 } 199 200 /**Gets the atom in the fragment with the specified ID, throwing if this fails. 201 * 202 * @param id The id of the atom. 203 * @return The found atom 204 * @throws StructureBuildingException 205 */ getAtomByIDOrThrow(int id)206 Atom getAtomByIDOrThrow(int id) throws StructureBuildingException { 207 Atom a = getAtomByID(id); 208 if(a == null) { 209 throw new StructureBuildingException("Couldn't find atom with id " + id + "."); 210 } 211 return a; 212 } 213 214 /**Finds a bond between two specified atoms the first of which must be within the fragment 215 * 216 * @param ID1 The id of one atom 217 * @param ID2 The id of the other atom 218 * @return The bond found, or null 219 */ findBond(int ID1, int ID2)220 Bond findBond(int ID1, int ID2) { 221 Atom a = atomMapFromId.get(ID1); 222 if (a != null){ 223 for (Bond b : a.getBonds()) { 224 if((b.getFrom() == ID1 && b.getTo() == ID2) || 225 (b.getTo() == ID1 && b.getFrom() == ID2)) { 226 return b; 227 } 228 } 229 } 230 return null; 231 } 232 233 /**Finds a bond between two specified atoms the first of which must be within the fragment, throwing if it fails. 234 * 235 * @param ID1 The id of one atom 236 * @param ID2 The id of the other atom 237 * @return The bond found 238 * @throws StructureBuildingException 239 */ findBondOrThrow(int ID1, int ID2)240 Bond findBondOrThrow(int ID1, int ID2) throws StructureBuildingException { 241 Bond b = findBond(ID1, ID2); 242 if(b == null) { 243 throw new StructureBuildingException("Couldn't find specified bond"); 244 } 245 return b; 246 } 247 248 /**Works out how many atoms there are in the fragment there are 249 * with consecutive locants, starting from 1 that are in a chain 250 * 251 * @return The number of atoms in the locant chain 252 */ getChainLength()253 int getChainLength() { 254 int length = 0; 255 Atom next = getAtomByLocant(Integer.toString(length + 1)); 256 Atom previous = null; 257 while (next != null){ 258 if (previous != null && previous.getBondToAtom(next) == null){ 259 break; 260 } 261 length++; 262 previous = next; 263 next = getAtomByLocant(Integer.toString(length + 1)); 264 } 265 return length; 266 } 267 268 /** 269 * Gets the type of the corresponding tokenEl 270 * Returns "" if undefined 271 * @return 272 */ getType()273 String getType() { 274 String type = tokenEl.getAttributeValue(TYPE_ATR); 275 return type != null ? type : ""; 276 } 277 278 /** 279 * Gets the subType of the corresponding tokenEl 280 * Returns "" if undefined 281 * @return 282 */ getSubType()283 String getSubType() { 284 String subType = tokenEl.getAttributeValue(SUBTYPE_ATR); 285 return subType != null ? subType : ""; 286 } 287 288 /** 289 * Gets the associate tokenEl 290 * Whether or not this is a real token can be tested by whether it has a parent 291 * @return 292 */ getTokenEl()293 Element getTokenEl() { 294 return tokenEl; 295 } 296 297 /** 298 * Sets the associated tokenEl 299 * Type/subType are inherited from the tokenEl 300 * @param tokenEl 301 */ setTokenEl(Element tokenEl)302 void setTokenEl(Element tokenEl) { 303 this.tokenEl = tokenEl; 304 } 305 306 /** 307 * How many OutAtoms (i.e. radicals) are associated with this fragment 308 * @return 309 */ getOutAtomCount()310 int getOutAtomCount() { 311 return outAtoms.size(); 312 } 313 314 /** 315 * Gets the outAtom at a specific index of the outAtoms linkedList 316 * @param i 317 * @return 318 */ getOutAtom(int i)319 OutAtom getOutAtom(int i) { 320 return outAtoms.get(i); 321 } 322 323 /** 324 * Adds an outAtom 325 * @param id 326 * @param valency 327 * @param setExplicitly 328 * @throws StructureBuildingException 329 */ addOutAtom(int id, int valency, Boolean setExplicitly)330 void addOutAtom(int id, int valency, Boolean setExplicitly) throws StructureBuildingException { 331 addOutAtom(getAtomByIDOrThrow(id), valency, setExplicitly); 332 } 333 334 /** 335 * Adds an outAtom 336 * @param atom 337 * @param valency 338 * @param setExplicitly 339 */ addOutAtom(Atom atom, int valency, Boolean setExplicitly)340 void addOutAtom(Atom atom, int valency, Boolean setExplicitly) { 341 outAtoms.add(new OutAtom(atom, valency, setExplicitly)); 342 } 343 344 /** 345 * Includes the OutAtoms of a given fragment into this fragment 346 * Note that no OutAtoms are created in doing this 347 * @param frag 348 */ incorporateOutAtoms(Fragment frag)349 void incorporateOutAtoms(Fragment frag) { 350 outAtoms.addAll(frag.outAtoms); 351 } 352 353 /** 354 * Removes the outAtom at a specific index of the outAtom linkedList 355 * @param i 356 */ removeOutAtom(int i)357 void removeOutAtom(int i) { 358 OutAtom removedOutAtom = outAtoms.remove(i); 359 if (removedOutAtom.isSetExplicitly()){ 360 removedOutAtom.getAtom().addOutValency(-removedOutAtom.getValency()); 361 } 362 } 363 364 /** 365 * Removes the specified outAtom from the outAtoms linkedList 366 * @param outAtom 367 */ removeOutAtom(OutAtom outAtom)368 void removeOutAtom(OutAtom outAtom) { 369 if (outAtoms.remove(outAtom) && outAtom.isSetExplicitly()){ 370 outAtom.getAtom().addOutValency(-outAtom.getValency()); 371 } 372 } 373 374 /** 375 * How many functionalAtoms (i.e. locations that can form esters) are associated with this fragment 376 * @return 377 */ getFunctionalAtomCount()378 int getFunctionalAtomCount() { 379 return functionalAtoms.size(); 380 } 381 382 /** 383 * Gets the functionalAtom at a specific index of the functionalAtoms linkedList 384 * @param i 385 * @return 386 */ getFunctionalAtom(int i)387 FunctionalAtom getFunctionalAtom(int i) { 388 return functionalAtoms.get(i); 389 } 390 391 /**Adds a functionalAtom 392 * @param atom*/ addFunctionalAtom(Atom atom)393 void addFunctionalAtom(Atom atom) { 394 functionalAtoms.add(new FunctionalAtom(atom)); 395 } 396 397 /** 398 * Includes the FunctionalAtoms of a given fragment into this fragment 399 * Note that no FunctionalAtoms are created in doing this 400 * @param frag 401 */ incorporateFunctionalAtoms(Fragment frag)402 void incorporateFunctionalAtoms(Fragment frag) { 403 functionalAtoms.addAll(frag.functionalAtoms); 404 } 405 406 /** 407 * Removes the functionalAtom at a specific index of the functionalAtoms linkedList 408 * @param i 409 * @return 410 */ removeFunctionalAtom(int i)411 FunctionalAtom removeFunctionalAtom(int i) { 412 return functionalAtoms.remove(i); 413 } 414 415 /** 416 * Removes the specified functionalAtom from the functionalAtoms linkedList 417 * @param functionalAtom 418 */ removeFunctionalAtom(FunctionalAtom functionalAtom)419 void removeFunctionalAtom(FunctionalAtom functionalAtom) { 420 functionalAtoms.remove(functionalAtom); 421 } 422 getPolymerAttachmentPoints()423 List<Atom> getPolymerAttachmentPoints() { 424 return polymerAttachmentPoints; 425 } 426 setPolymerAttachmentPoints(List<Atom> polymerAttachmentPoints)427 void setPolymerAttachmentPoints(List<Atom> polymerAttachmentPoints) { 428 this.polymerAttachmentPoints = polymerAttachmentPoints; 429 } 430 431 /**Gets a list of atoms in the fragment that connect to a specified atom 432 * 433 * @param atom The reference atom 434 * @return The list of atoms connected to the atom 435 */ getIntraFragmentAtomNeighbours(Atom atom)436 List<Atom> getIntraFragmentAtomNeighbours(Atom atom) { 437 List<Atom> results = new ArrayList<Atom>(atom.getBondCount()); 438 for(Bond b : atom.getBonds()) { 439 Atom otherAtom = b.getOtherAtom(atom); 440 if (otherAtom == null) { 441 throw new RuntimeException("OPSIN Bug: A bond associated with an atom does not involve it"); 442 } 443 //If the other atom is in atomMapFromId then it is in this fragment 444 if (atomMapFromId.get(otherAtom.getID()) != null) { 445 results.add(otherAtom); 446 } 447 } 448 return results; 449 } 450 451 /**Calculates the number of bonds connecting to the atom, excluding bonds to implicit 452 * hydrogens. Double bonds count as 453 * two bonds, etc. Eg ethene - both C's have an incoming valency of 2. 454 * 455 * Only bonds to atoms within the fragment are counted. Suffix atoms are excluded 456 * 457 * @param atom 458 * @return Incoming Valency 459 * @throws StructureBuildingException 460 */ getIntraFragmentIncomingValency(Atom atom)461 int getIntraFragmentIncomingValency(Atom atom) throws StructureBuildingException { 462 int v = 0; 463 for(Bond b : atom.getBonds()) { 464 //recalled atoms will be null if they are not part of this fragment 465 if(b.getFromAtom() == atom) { 466 Atom a =getAtomByID(b.getTo()); 467 if (a != null && !a.getType().equals(SUFFIX_TYPE_VAL)){ 468 v += b.getOrder(); 469 } 470 } else if(b.getToAtom() == atom) { 471 Atom a =getAtomByID(b.getFrom()); 472 if (a != null && !a.getType().equals(SUFFIX_TYPE_VAL)){ 473 v += b.getOrder(); 474 } 475 } 476 else{ 477 throw new StructureBuildingException("A bond associated with an atom does not involve it"); 478 } 479 } 480 return v; 481 } 482 483 /** 484 * Checks valencies are sensible 485 * @throws StructureBuildingException 486 */ checkValencies()487 void checkValencies() throws StructureBuildingException { 488 for(Atom a : atomCollection) { 489 if(!ValencyChecker.checkValency(a)) { 490 throw new StructureBuildingException("Atom is in unphysical valency state! Element: " + a.getElement() + " valency: " + a.getIncomingValency()); 491 } 492 } 493 } 494 495 /** 496 * Removes an atom from this fragment 497 * @param atom 498 */ removeAtom(Atom atom)499 void removeAtom(Atom atom) { 500 int atomID =atom.getID(); 501 atomMapFromId.remove(atomID); 502 for (String l : atom.getLocants()) { 503 atomMapFromLocant.remove(l); 504 } 505 if (defaultInAtom == atom){ 506 defaultInAtom = null; 507 } 508 } 509 /** 510 * Retrieves the overall charge of the fragment by querying all its atoms 511 * @return 512 */ getCharge()513 int getCharge() { 514 int charge=0; 515 for (Atom a : atomCollection) { 516 charge+=a.getCharge(); 517 } 518 return charge; 519 } 520 getDefaultInAtom()521 Atom getDefaultInAtom() { 522 return defaultInAtom; 523 } 524 setDefaultInAtom(Atom inAtom)525 void setDefaultInAtom(Atom inAtom) { 526 this.defaultInAtom = inAtom; 527 } 528 getDefaultInAtomOrFirstAtom()529 Atom getDefaultInAtomOrFirstAtom() { 530 return defaultInAtom != null ? defaultInAtom : getFirstAtom(); 531 } 532 533 /** 534 * Adds a mapping between the locant and atom object 535 * @param locant A locant as a string 536 * @param a An atom 537 */ addMappingToAtomLocantMap(String locant, Atom a)538 void addMappingToAtomLocantMap(String locant, Atom a){ 539 atomMapFromLocant.put(locant, a); 540 } 541 542 /** 543 * Removes a mapping between a locant 544 * @param locant A locant as a string 545 */ removeMappingFromAtomLocantMap(String locant)546 void removeMappingFromAtomLocantMap(String locant){ 547 atomMapFromLocant.remove(locant); 548 } 549 550 /** 551 * Checks to see whether a locant is present on this fragment 552 * @param locant 553 * @return 554 */ hasLocant(String locant)555 boolean hasLocant(String locant) { 556 return getAtomByLocant(locant) != null; 557 } 558 559 560 /** 561 * Returns an unmodifiable list of the locants associated with this fragment 562 * @return 563 */ getLocants()564 Set<String> getLocants() { 565 return Collections.unmodifiableSet(atomMapFromLocant.keySet()); 566 } 567 getIndicatedHydrogen()568 List<Atom> getIndicatedHydrogen() { 569 return indicatedHydrogen; 570 } 571 addIndicatedHydrogen(Atom atom)572 void addIndicatedHydrogen(Atom atom) { 573 indicatedHydrogen.add(atom); 574 } 575 576 /** 577 * Returns the id of the first atom in the fragment 578 * @return 579 * @throws StructureBuildingException 580 */ getIdOfFirstAtom()581 int getIdOfFirstAtom() { 582 return getFirstAtom().getID(); 583 } 584 585 /** 586 * Returns the the first atom in the fragment or null if it has no atoms 587 * Typically the first atom will be the first atom that was added to the fragment 588 * @return firstAtom 589 */ getFirstAtom()590 Atom getFirstAtom(){ 591 Iterator<Atom> atomIterator =atomCollection.iterator(); 592 if (atomIterator.hasNext()){ 593 return atomIterator.next(); 594 } 595 return null; 596 } 597 598 /** 599 * Clears and recreates atomMapFromId (and hence AtomCollection) using the order of the atoms in atomList 600 * @param atomList 601 * @throws StructureBuildingException 602 */ reorderAtomCollection(List<Atom> atomList)603 void reorderAtomCollection(List<Atom> atomList) throws StructureBuildingException { 604 if (atomMapFromId.size() != atomList.size()){ 605 throw new StructureBuildingException("atom list is not the same size as the number of atoms in the fragment"); 606 } 607 atomMapFromId.clear(); 608 for (Atom atom : atomList) { 609 atomMapFromId.put(atom.getID(), atom); 610 } 611 } 612 613 /** 614 * Reorders the fragment's internal atomList by the value of the first locant of the atoms 615 * e.g. 1,2,3,3a,3b,4 616 * Used for assuring the correct order of atom iteration when performing ring fusion 617 * @throws StructureBuildingException 618 */ sortAtomListByLocant()619 void sortAtomListByLocant() throws StructureBuildingException { 620 List<Atom> atomList =getAtomList(); 621 Collections.sort(atomList, new FragmentTools.SortByLocants()); 622 reorderAtomCollection(atomList); 623 } 624 625 @Override iterator()626 public Iterator<Atom> iterator() { 627 return atomCollection.iterator(); 628 } 629 } 630 631 632 633