001/* 002 * BioJava development code 003 * 004 * This code may be freely distributed and modified under the 005 * terms of the GNU Lesser General Public Licence. This should 006 * be distributed with the code. If you do not have a copy, 007 * see: 008 * 009 * http://www.gnu.org/copyleft/lesser.html 010 * 011 * Copyright for this code is held jointly by the individual 012 * authors. These should be listed in @author doc comments. 013 * 014 * For more information on the BioJava project and its aims, 015 * or to join the biojava-l mailing list, visit the home page 016 * at: 017 * 018 * http://www.biojava.org/ 019 * 020 * Created on 12.03.2004 021 * @author Andreas Prlic 022 * 023 */ 024package org.biojava.nbio.structure; 025 026 027import org.biojava.nbio.structure.chem.ChemComp; 028import org.biojava.nbio.structure.chem.ChemCompGroupFactory; 029import org.biojava.nbio.structure.chem.PolymerType; 030import org.biojava.nbio.structure.io.FileConvert; 031import org.biojava.nbio.core.exceptions.CompoundNotFoundException; 032import org.biojava.nbio.core.sequence.ProteinSequence; 033import org.biojava.nbio.core.sequence.compound.AminoAcidCompound; 034import org.biojava.nbio.core.sequence.template.Sequence; 035import org.slf4j.Logger; 036import org.slf4j.LoggerFactory; 037 038import java.util.*; 039 040 041/** 042 * A Chain in a PDB file. It contains several groups which can be of 043 * one of the types defined in the {@link GroupType} constants. 044 * 045 * @author Andreas Prlic 046 * @author Jules Jacobsen 047 * @since 1.4 048 */ 049public class ChainImpl implements Chain { 050 051 private final static Logger logger = LoggerFactory.getLogger(ChainImpl.class); 052 053 private static final long serialVersionUID = 1990171805277911840L; 054 055 /** 056 * The default chain identifier used to be an empty space 057 */ 058 private static final String DEFAULT_CHAIN_ID = "A"; 059 060 private String authId; // the 'public' chain identifier as assigned by authors in PDB files 061 private String asymId; // the 'internal' chain identifier as used in mmCIF files 062 063 private List<Group> groups; 064 private List<Group> seqResGroups; 065 066 private EntityInfo entity; 067 private Structure parent; 068 069 private final Map<String, Integer> pdbResnumMap; 070 071 private List<SeqMisMatch> seqMisMatches; 072 073 /** 074 * Constructs a ChainImpl object. 075 */ 076 public ChainImpl() { 077 super(); 078 079 authId = DEFAULT_CHAIN_ID; 080 groups = new ArrayList<>() ; 081 082 seqResGroups = new ArrayList<>(); 083 pdbResnumMap = new HashMap<>(); 084 asymId = null; 085 086 } 087 088 @Override 089 public String getId() { 090 return asymId; 091 } 092 093 @Override 094 public void setId(String asymId) { 095 this.asymId = asymId; 096 } 097 098 @Override 099 public String getName() { return authId; } 100 101 @Override 102 public void setName(String authId) { this.authId = authId; } 103 104 @Override 105 public void setStructure(Structure parent){ 106 this.parent = parent; 107 } 108 109 @Override 110 public Structure getStructure() { 111 112 return parent; 113 } 114 115 @Override 116 public Object clone() { 117 118 // go through all groups and add to new Chain. 119 ChainImpl n = new ChainImpl(); 120 // copy chain data: 121 122 n.setId(getId()); 123 n.setName(getName()); 124 125 // NOTE the EntityInfo will be reset at the parent level (Structure) if cloning is happening from parent level 126 // here we don't deep-copy it and just keep the same reference, in case the cloning is happening at the Chain level only 127 n.setEntityInfo(this.entity); 128 129 130 for (Group group : groups) { 131 Group g = (Group) group.clone(); 132 n.addGroup(g); 133 g.setChain(n); 134 } 135 136 if (seqResGroups!=null){ 137 138 List<Group> tmpSeqRes = new ArrayList<>(); 139 140 // cloning seqres and atom groups is ugly, due to their 141 // nested relationship (some of the atoms can be in the seqres, but not all) 142 143 for (Group seqResGroup : seqResGroups) { 144 145 if (seqResGroup==null) { 146 tmpSeqRes.add(null); 147 continue; 148 } 149 150 int i = groups.indexOf(seqResGroup); 151 152 Group g ; 153 154 if (i!=-1) { 155 // group found in atom groups, we get the equivalent reference from the newly cloned atom groups 156 g = n.getAtomGroup(i); 157 } else { 158 // group not found in atom groups, we clone the seqres group 159 g = (Group) seqResGroup.clone(); 160 } 161 g.setChain(n); 162 tmpSeqRes.add(g); 163 } 164 165 n.setSeqResGroups(tmpSeqRes); 166 } 167 168 return n ; 169 } 170 171 @Override 172 public void setEntityInfo(EntityInfo mol) { 173 this.entity = mol; 174 } 175 176 @Override 177 public EntityInfo getEntityInfo() { 178 return this.entity; 179 } 180 181 @Override 182 public void addGroup(Group group) { 183 184 group.setChain(this); 185 186 // Set the altlocs chain as well 187 for(Group g : group.getAltLocs()) { 188 g.setChain(this); 189 } 190 191 groups.add(group); 192 193 // store the position internally for quick access of this group 194 195 String pdbResnum = null ; 196 ResidueNumber resNum = group.getResidueNumber(); 197 if ( resNum != null) 198 pdbResnum = resNum.toString(); 199 if ( pdbResnum != null) { 200 Integer pos = groups.size() - 1; 201 // ARGH sometimes numbering in PDB files is confusing. 202 // e.g. PDB: 1sfe 203 /* 204 * ATOM 620 N GLY 93 -24.320 -6.591 4.210 1.00 46.82 N 205 * ATOM 621 CA GLY 93 -24.960 -6.849 5.497 1.00 47.35 C 206 * ATOM 622 C GLY 93 -26.076 -5.873 5.804 1.00 47.24 C 207 * ATOM 623 O GLY 93 -26.382 -4.986 5.006 1.00 47.56 O 208 * and ... 209 * HETATM 1348 O HOH 92 -21.853 -16.886 19.138 1.00 66.92 O 210 * HETATM 1349 O HOH 93 -26.126 1.226 29.069 1.00 71.69 O 211 * HETATM 1350 O HOH 94 -22.250 -18.060 -6.401 1.00 61.97 O 212 */ 213 214 // this check is to give in this case the entry priority that is an AminoAcid / comes first... 215 // a good example of same residue number for 2 residues is 3th3, chain T, residue 201 (a LYS and a sugar BGC covalently attached to it) - JD 2016-03-09 216 if ( pdbResnumMap.containsKey(pdbResnum)) { 217 218 logger.warn("Adding residue {}({}) to chain {} but a residue with same residue number is already present: {}({}). Will add only the aminoacid residue (if any) to the lookup, lookups for that residue number won't work properly.", 219 pdbResnum, group.getPDBName(), getId(), groups.get(pdbResnumMap.get(pdbResnum)).getResidueNumber(), groups.get(pdbResnumMap.get(pdbResnum)).getPDBName()); 220 if ( group instanceof AminoAcid) 221 pdbResnumMap.put(pdbResnum,pos); 222 } else 223 pdbResnumMap.put(pdbResnum,pos); 224 } 225 226 } 227 228 @Override 229 public Group getAtomGroup(int position) { 230 231 return groups.get(position); 232 } 233 234 @Override 235 public List<Group> getAtomGroups(GroupType type){ 236 237 List<Group> tmp = new ArrayList<>() ; 238 for (Group g : groups) { 239 if (g.getType().equals(type)) { 240 tmp.add(g); 241 } 242 } 243 244 return tmp ; 245 } 246 247 @Override 248 public List<Group> getAtomGroups(){ 249 return groups ; 250 } 251 252 @Override 253 public void setAtomGroups(List<Group> groups){ 254 for (Group g:groups){ 255 g.setChain(this); 256 } 257 this.groups = groups; 258 } 259 260 @Override 261 public Group[] getGroupsByPDB(ResidueNumber start, ResidueNumber end, boolean ignoreMissing) 262 throws StructureException { 263 // Short-circut for include all groups 264 if(start == null && end == null) { 265 return groups.toArray(new Group[0]); 266 } 267 268 List<Group> retlst = new ArrayList<>(); 269 270 boolean adding, foundStart; 271 if( start == null ) { 272 // start with first group 273 adding = true; 274 foundStart = true; 275 } else { 276 adding = false; 277 foundStart = false; 278 } 279 280 for (Group g: groups){ 281 282 // Check for start 283 if (!adding && start.equalsPositional(g.getResidueNumber())) { 284 adding = true; 285 foundStart = true; 286 } 287 288 // Check if past start 289 if ( ignoreMissing && ! (foundStart && adding) ) { 290 ResidueNumber pos = g.getResidueNumber(); 291 292 if ( start != null && start.compareToPositional(pos) <= 0) { 293 foundStart = true; 294 adding = true; 295 } 296 } 297 298 if ( adding) 299 retlst.add(g); 300 301 // check for end 302 if ( end != null && end.equalsPositional(g.getResidueNumber())) { 303 if ( ! adding) 304 throw new StructureException("did not find start PDB residue number " + start + " in chain " + authId); 305 adding = false; 306 break; 307 } 308 // check if past end 309 if ( ignoreMissing && adding && end != null){ 310 311 ResidueNumber pos = g.getResidueNumber(); 312 if ( end.compareToPositional(pos) <= 0) { 313 adding = false; 314 break; 315 } 316 317 } 318 } 319 320 if ( ! foundStart){ 321 throw new StructureException("did not find start PDB residue number " + start + " in chain " + authId); 322 } 323 if ( end != null && adding && !ignoreMissing) { 324 throw new StructureException("did not find end PDB residue number " + end + " in chain " + authId); 325 } 326 327 //not checking if the end has been found in this case... 328 329 return retlst.toArray(new Group[0]); 330 } 331 332 @Override 333 public Group getGroupByPDB(ResidueNumber resNum) throws StructureException { 334 String pdbresnum = resNum.toString(); 335 if ( pdbResnumMap.containsKey(pdbresnum)) { 336 Integer pos = pdbResnumMap.get(pdbresnum); 337 return groups.get(pos); 338 } else { 339 throw new StructureException("unknown PDB residue number " + pdbresnum + " in chain " + authId); 340 } 341 } 342 343 @Override 344 public Group[] getGroupsByPDB(ResidueNumber start, ResidueNumber end) 345 throws StructureException { 346 return getGroupsByPDB(start, end, false); 347 } 348 349 @Override 350 public int getSeqResLength() { 351 //new method returns the length of the sequence defined in the SEQRES records 352 return seqResGroups.size(); 353 } 354 355 @Override 356 public String toString(){ 357 String newline = System.getProperty("line.separator"); 358 StringBuilder str = new StringBuilder(); 359 str.append("Chain asymId:").append(getId()).append(" authId:").append(getName()).append(newline); 360 if ( entity != null ){ 361 if ( entity.getDescription() != null){ 362 str.append(entity.getDescription()).append(newline); 363 } 364 } 365 str.append("total SEQRES length: ").append(getSeqResGroups().size()).append(" total ATOM length:") 366 .append(getAtomLength()).append(" residues ").append(newline); 367 368 return str.toString() ; 369 } 370 371 @Override 372 public Sequence<?> getBJSequence() { 373 374 String seq = getSeqResSequence(); 375 376 Sequence<AminoAcidCompound> s = null; 377 378 try { 379 s = new ProteinSequence(seq); 380 } catch (CompoundNotFoundException e) { 381 logger.error("Could not create sequence object from seqres sequence. Some unknown compound: {}",e.getMessage()); 382 } 383 384 //TODO: return a DNA sequence if the content is DNA... 385 return s; 386 } 387 388 @Override 389 public String getAtomSequence(){ 390 391 List<Group> groups = getAtomGroups(); 392 StringBuilder sequence = new StringBuilder() ; 393 394 for ( Group g: groups){ 395 ChemComp cc = g.getChemComp(); 396 397 if ( PolymerType.PROTEIN_ONLY.contains(cc.getPolymerType()) || 398 PolymerType.POLYNUCLEOTIDE_ONLY.contains(cc.getPolymerType())){ 399 // an amino acid residue.. use for alignment 400 String oneLetter= ChemCompGroupFactory.getOneLetterCode(cc); 401 if ( oneLetter == null) 402 oneLetter = Character.toString(StructureTools.UNKNOWN_GROUP_LABEL); 403 sequence.append(oneLetter); 404 } 405 406 } 407 return sequence.toString(); 408 } 409 410 @Override 411 public String getSeqResSequence(){ 412 413 StringBuilder str = new StringBuilder(); 414 for (Group g : seqResGroups) { 415 ChemComp cc = g.getChemComp(); 416 if ( cc == null) { 417 logger.warn("Could not load ChemComp for group: {}", g); 418 str.append(StructureTools.UNKNOWN_GROUP_LABEL); 419 } else if ( PolymerType.PROTEIN_ONLY.contains(cc.getPolymerType()) || 420 PolymerType.POLYNUCLEOTIDE_ONLY.contains(cc.getPolymerType())){ 421 // an amino acid residue.. use for alignment 422 String oneLetter= ChemCompGroupFactory.getOneLetterCode(cc); 423 // AB oneLetter.length() should be one. e.g. in 1EMA it is 3 and this makes mapping residue to sequence impossible. 424 if ( oneLetter == null || oneLetter.isEmpty() || oneLetter.equals("?")) { 425 oneLetter = Character.toString(StructureTools.UNKNOWN_GROUP_LABEL); 426 } 427 str.append(oneLetter); 428 } else { 429 str.append(StructureTools.UNKNOWN_GROUP_LABEL); 430 } 431 } 432 return str.toString(); 433 } 434 435 /** 436 * Get the one letter sequence so that Sequence is guaranteed to 437 * be the same length as seqResGroups. 438 * Method related to https://github.com/biojava/biojava/issues/457 439 * @return a string of the sequence guaranteed to be the same length 440 * as seqResGroups. 441 */ 442 public String getSeqResOneLetterSeq(){ 443 444 StringBuilder str = new StringBuilder(); 445 for (Group g : seqResGroups) { 446 ChemComp cc = g.getChemComp(); 447 if ( cc == null) { 448 logger.warn("Could not load ChemComp for group: {}", g); 449 str.append(StructureTools.UNKNOWN_GROUP_LABEL); 450 } else if ( PolymerType.PROTEIN_ONLY.contains(cc.getPolymerType()) || 451 PolymerType.POLYNUCLEOTIDE_ONLY.contains(cc.getPolymerType())){ 452 // an amino acid residue.. use for alignment 453 String oneLetter= ChemCompGroupFactory.getOneLetterCode(cc); 454 // AB oneLetter.length() should be one. e.g. in 1EMA it is 3 and this makes mapping residue to sequence impossible. 455 if ( oneLetter == null || oneLetter.isEmpty() || oneLetter.equals("?") || oneLetter.length()!=1) { 456 oneLetter = Character.toString(StructureTools.UNKNOWN_GROUP_LABEL); 457 } 458 str.append(oneLetter); 459 } else { 460 str.append(StructureTools.UNKNOWN_GROUP_LABEL); 461 } 462 } 463 return str.toString(); 464 } 465 466 @Override 467 public Group getSeqResGroup(int position) { 468 return seqResGroups.get(position); 469 } 470 471 @Override 472 public List<Group> getSeqResGroups(GroupType type) { 473 List<Group> tmp = new ArrayList<>() ; 474 for (Group g : seqResGroups) { 475 if (g.getType().equals(type)) { 476 tmp.add(g); 477 } 478 } 479 480 return tmp ; 481 } 482 483 @Override 484 public List<Group> getSeqResGroups() { 485 return seqResGroups; 486 } 487 488 @Override 489 public void setSeqResGroups(List<Group> groups){ 490 for (Group g: groups){ 491 if (g != null) { 492 g.setChain(this); 493 } 494 } 495 this.seqResGroups = groups; 496 } 497 498 @Override 499 public int getAtomLength() { 500 501 return groups.size(); 502 } 503 504 @Override 505 public String toPDB() { 506 return FileConvert.toPDB(this); 507 } 508 509 @Override 510 public String toMMCIF() { 511 return FileConvert.toMMCIF(this); 512 } 513 514 @Override 515 public void setSeqMisMatches(List<SeqMisMatch> seqMisMatches) { 516 this.seqMisMatches = seqMisMatches; 517 } 518 519 @Override 520 public List<SeqMisMatch> getSeqMisMatches() { 521 return seqMisMatches; 522 } 523 524 @Override 525 public EntityType getEntityType() { 526 if (getEntityInfo()==null) return null; 527 return getEntityInfo().getType(); 528 } 529 530 @Override 531 public boolean isWaterOnly() { 532 for (Group g : getAtomGroups()) { 533 if (!g.isWater()) 534 return false; 535 } 536 return true; 537 } 538 539 @Override 540 public boolean isPureNonPolymer() { 541 for (Group g : getAtomGroups()) { 542 543 //ChemComp cc = g.getChemComp(); 544 545 if ( g.isPolymeric() && 546 !g.isHetAtomInFile() ) { 547 548 // important: the aminoacid or nucleotide residue can be in Atom records 549 550 return false; 551 } 552 553 } 554 return true; 555 } 556 557 @Override 558 public GroupType getPredominantGroupType(){ 559 560 double ratioResiduesToTotal = StructureTools.RATIO_RESIDUES_TO_TOTAL; 561 562 int sizeAminos = getAtomGroups(GroupType.AMINOACID).size(); 563 int sizeNucleotides = getAtomGroups(GroupType.NUCLEOTIDE).size(); 564 List<Group> hetAtoms = getAtomGroups(GroupType.HETATM); 565 int sizeHetatoms = hetAtoms.size(); 566 int sizeWaters = 0; 567 for (Group g : hetAtoms) { 568 if (g.isWater()) 569 sizeWaters++; 570 } 571 int sizeHetatomsWithoutWater = sizeHetatoms - sizeWaters; 572 573 int fullSize = sizeAminos + sizeNucleotides + sizeHetatomsWithoutWater; 574 575 if ((double) sizeAminos / (double) fullSize > ratioResiduesToTotal) 576 return GroupType.AMINOACID; 577 578 if ((double) sizeNucleotides / (double) fullSize > ratioResiduesToTotal) 579 return GroupType.NUCLEOTIDE; 580 581 if ((double) (sizeHetatomsWithoutWater) / (double) fullSize > ratioResiduesToTotal) 582 return GroupType.HETATM; 583 584 // finally if neither condition works, we try based on majority, but log 585 // it 586 GroupType max; 587 if (sizeNucleotides > sizeAminos) { 588 if (sizeNucleotides > sizeHetatomsWithoutWater) { 589 max = GroupType.NUCLEOTIDE; 590 } else { 591 max = GroupType.HETATM; 592 } 593 } else { 594 if (sizeAminos > sizeHetatomsWithoutWater) { 595 max = GroupType.AMINOACID; 596 } else { 597 max = GroupType.HETATM; 598 } 599 } 600 logger.debug( 601 "Ratio of residues to total for chain with asym_id {} is below {}. Assuming it is a {} chain. " 602 + "Counts: # aa residues: {}, # nuc residues: {}, # non-water het residues: {}, # waters: {}, " 603 + "ratio aa/total: {}, ratio nuc/total: {}", 604 getId(), ratioResiduesToTotal, max, sizeAminos, 605 sizeNucleotides, sizeHetatomsWithoutWater, sizeWaters, 606 (double) sizeAminos / (double) fullSize, 607 (double) sizeNucleotides / (double) fullSize); 608 609 return max; 610 } 611 612 @Override 613 public boolean isProtein() { 614 return getPredominantGroupType() == GroupType.AMINOACID; 615 } 616 617 @Override 618 public boolean isNucleicAcid() { 619 return getPredominantGroupType() == GroupType.NUCLEOTIDE; 620 } 621 622 623} 624