From 659a5557b3e11c1234618e224f61cf9fd3bd2a5c Mon Sep 17 00:00:00 2001
From: marco <marco@5a81b35b-ba03-0410-adc8-b2c5c5119f08>
Date: Mon, 10 May 2010 05:38:57 +0000
Subject: [PATCH] fix several problems in PDBWriter

Residues with names shorter than 3 letters are left-padded,
we throw an error if we encounter chain, residue and atom
names that are too long (longer than 1, 3 and 4 characters
respectively) and we try to put the atom name into the
correct column.

Oh, and we insert TER records at the correct positions.

All in all the code is much cleaner and faster as it does
not longer rely on boost.format (yes, io performance is
sometimes critical).

git-svn-id: https://dng.biozentrum.unibas.ch/svn/openstructure/trunk@2215 5a81b35b-ba03-0410-adc8-b2c5c5119f08
---
 .gitignore                                 |   1 +
 modules/io/src/CMakeLists.txt              |   1 +
 modules/io/src/formatted_line.hh           | 218 ++++++++++++++++++++
 modules/io/src/mol/pdb_writer.cc           | 227 ++++++++++++++-------
 modules/io/src/mol/pdb_writer.hh           |   5 +-
 modules/io/tests/test_io_pdb.cc            | 157 ++++++++++++++
 modules/io/tests/testfiles/pdb/alt-loc.pdb |   3 +
 modules/io/tests/testfiles/pdb/ter.pdb     |  15 ++
 8 files changed, 553 insertions(+), 74 deletions(-)
 create mode 100644 modules/io/src/formatted_line.hh
 create mode 100644 modules/io/tests/testfiles/pdb/alt-loc.pdb
 create mode 100644 modules/io/tests/testfiles/pdb/ter.pdb

diff --git a/.gitignore b/.gitignore
index 71a47aca6..be2ace828 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,5 +19,6 @@ html
 scratch
 pov_*test.pov
 pov_*test.inc
+*-out.pdb
 CMakeLists.txt.user
 OpenStructure.cbp
diff --git a/modules/io/src/CMakeLists.txt b/modules/io/src/CMakeLists.txt
index e77cfda20..f61c471d3 100644
--- a/modules/io/src/CMakeLists.txt
+++ b/modules/io/src/CMakeLists.txt
@@ -13,6 +13,7 @@ io_utils.hh
 io_exception.hh
 convert.hh
 converting_streams.hh
+formatted_line.hh
 )
 
 set(OST_IO_SOURCES
diff --git a/modules/io/src/formatted_line.hh b/modules/io/src/formatted_line.hh
new file mode 100644
index 000000000..f42bcac73
--- /dev/null
+++ b/modules/io/src/formatted_line.hh
@@ -0,0 +1,218 @@
+#ifndef OST_IO_FORMATTED_LINE_HH
+#define OST_IO_FORMATTED_LINE_HH
+
+#include <ost/string_ref.hh>
+
+/*
+  Author: Marco Biasini
+ */
+
+namespace ost { namespace io {
+
+namespace fmt {
+
+struct LPadded : public StringRef {
+  LPadded(const char* s, size_t l): 
+    StringRef(s, l)
+  { }
+  
+  explicit LPadded(const String& s): StringRef(s.data(), s.size())
+  { }
+  
+  explicit LPadded(const char* s): StringRef(s, strlen(s))
+  { }
+};
+
+struct RPadded : public StringRef {
+  RPadded(const char* s, size_t l): 
+    StringRef(s, l)
+  { }
+  
+  explicit RPadded(const String& s): StringRef(s.data(), s.size())
+  { }
+  
+  explicit RPadded(const char* s): StringRef(s, strlen(s))
+  { }
+};
+
+
+struct LPaddedInt {
+  LPaddedInt(int val) {
+    size_t curr=0;
+    bool minus=val<0;
+    val=std::abs(val);
+    if (minus) {
+      data[curr]='-';
+      ++curr;
+    }
+    do {
+      int m=val%10;
+      data[curr]='0'+m;
+      ++curr;
+      val/=10;
+    } while(val);
+    // swap
+    for (size_t i=0, e=(curr-int(minus))/2; i<e; ++i) {
+      std::swap(data[int(minus)+i], data[curr-i-1]);
+    }
+    data[curr]='\0';
+    len=curr;
+  }
+  char   data[20];
+  size_t len;
+};
+
+// BIG FAT WARNING: Before using this class to output floats with lots of 
+// digits, make sure you indeed get the result you want. The function has 
+// not been tested enough for numerical stability.
+struct LPaddedFloat {
+  LPaddedFloat(Real val, int prec)
+  {
+    double rounded_val=round(val*pow(10, prec))*pow(0.1, prec);
+    size_t curr=0;
+    bool minus=rounded_val<0;
+    rounded_val=std::abs(rounded_val);
+    int int_val=int(rounded_val);
+    if (minus) {
+      data[curr]='-';
+      ++curr;
+    }
+    do {
+      int m=int_val%10;
+      data[curr]='0'+m;
+      ++curr;
+      int_val/=10;
+    } while(int_val);
+    // swap
+    for (size_t i=0, e=(curr-int(minus))/2; i<e; ++i) {
+      std::swap(data[int(minus)+i], data[curr-i-1]);
+    }
+    data[curr]='\0';
+    len=curr;
+    if (prec==0) {
+      return;
+    }
+    data[curr]='.';
+    curr++;
+    rounded_val-=int(rounded_val);
+    while(prec>0) {
+      rounded_val*=10;
+      int m=int(rounded_val);
+      rounded_val-=int(rounded_val);
+      data[curr]='0'+m;
+      curr++;
+      --prec;
+    }
+    data[curr]='\0';
+    len=curr;
+  }
+  char   data[20];
+  size_t len;  
+};
+
+
+
+}
+
+class LineSlice {
+public:
+  LineSlice(char* data, size_t l): data_(data), len_(l) 
+  {
+  }
+
+  LineSlice& operator=(const StringRef& str)
+  {
+    assert(str.length()==len_);
+    memcpy(data_, str.data(), str.size());
+    return *this;
+  }
+  
+  LineSlice& operator=(const fmt::LPadded& str)
+  {
+    assert(str.size()<=len_);
+    memcpy(data_+len_-str.size(), str.data(), str.size());
+    return *this;
+  }
+  
+  LineSlice& operator=(const fmt::RPadded& str)
+  {
+    assert(str.size()==len_);
+    memcpy(data_, str.data(), str.size());
+    return *this;
+  }
+  
+  LineSlice& operator=(const fmt::LPaddedInt& i) 
+  {
+    assert(i.len<=len_);
+    memcpy(data_+len_-i.len, i.data, i.len);
+    return *this;
+  }
+  
+  LineSlice& operator=(const fmt::LPaddedFloat& f)
+  {
+    assert(f.len<=len_);
+    memcpy(data_+len_-f.len, f.data, f.len);
+    return *this;
+  }
+  void Clear()
+  {
+    memset(data_, ' ', len_);
+  }
+private:
+  char* data_;
+  size_t len_;
+};
+
+class FormattedLine {
+public:
+  FormattedLine(size_t width): 
+    data_(new char[width]), len_(width)
+  {
+    this->Clear();
+  }
+  
+  void Clear()
+  {
+    memset(data_, ' ', len_);
+  }
+  ~FormattedLine() { delete[] data_; }
+  
+  LineSlice operator()(int start, int len) 
+  { 
+    assert(start>=0 && start+len<len_);
+    return LineSlice(data_+start, len);
+  }
+  
+  const char* Data() const { return data_; }
+  
+  size_t GetWidth() const { return len_; }
+  
+  char operator[](size_t index) const
+  {
+    assert(index<len_);
+    return data_[index];
+  }
+  
+  char& operator[](size_t index)
+  {
+    assert(index<_len_);
+    return data_[index];
+  }
+private:
+  FormattedLine& operator=(const FormattedLine& rhs);
+  FormattedLine(const FormattedLine& rhs);
+  char*       data_;
+  size_t      len_;
+};
+
+inline std::ostream& operator<<(std::ostream& stream, const FormattedLine& line)
+{
+  stream.write(line.Data(), line.GetWidth()); 
+  stream << std::endl;
+  return stream;
+}
+
+
+}}
+
+#endif
diff --git a/modules/io/src/mol/pdb_writer.cc b/modules/io/src/mol/pdb_writer.cc
index 775bec870..f46f70d04 100644
--- a/modules/io/src/mol/pdb_writer.cc
+++ b/modules/io/src/mol/pdb_writer.cc
@@ -22,6 +22,7 @@
 #include <boost/format.hpp>
 #include <string.h>
 #include <ost/io/io_exception.hh>
+
 #include "pdb_writer.hh"
 
 using boost::format;
@@ -31,63 +32,119 @@ namespace ost { namespace io {
 
 namespace {
 
-void write_atom(std::ostream& ostr, const mol::AtomHandle& atom, int atomnum, 
+// determine whether the element name has to be shifted to to the left by one
+// column.
+bool shift_left(const String& atom_name, bool is_hetatm, 
+                const String& element, float mass) 
+{
+  if (is_hetatm==false) {
+    return false;
+  }
+
+  if (isnumber(atom_name[0]) || atom_name=="UNK" ||
+      atom_name.length()==4) {
+    return true;
+  }
+  if (mass>34) {
+    if (element=="W" || element=="V") {
+      return false;
+    }    
+    return true;
+  }
+  return (element=="K" || element=="CA" || element=="NA" || 
+          element=="MG" || element=="LI");
+}
+
+void write_atom(std::ostream& ostr, FormattedLine& line, 
+                const mol::AtomHandle& atom, int atomnum, 
                 bool is_pqr)
 {
   mol::ResidueHandle res=atom.GetResidue();
   char ins_code=res.GetNumber().GetInsCode();
-  String record_name=atom.GetAtomProps().is_hetatm ? "HETATM" : "ATOM  ";
-  String aname_str=atom.GetName();
-  if(aname_str.length()<4) {
-    aname_str=" "+aname_str;
-  }
+  StringRef record_name(atom.IsHetAtom() ? "HETATM" : "ATOM  ", 6);
   std::vector<String> names=atom.GetAltGroupNames();
+  
+  geom::Vec3 p=atom.GetPos();
+  line( 0, 6)=record_name;
+  line( 6, 5)=fmt::LPaddedInt(atomnum);
+  String atom_name=atom.GetName();
+  if (atom_name.size()>4) {
+    throw IOException("Atom name '"+atom.GetQualifiedName()+
+                      "' is too long for PDB output. At most 4 character "
+                      "are allowed");
+  }
+  if (shift_left(atom_name, atom.IsHetAtom(), atom.GetElement(), 
+                 atom.GetMass())) {
+    line(12, 4)=fmt::RPadded(atom_name);
+  } else {
+    line(13, 3)=fmt::RPadded(atom_name);
+  }
+  if (res.GetKey().size()>3) {
+    throw IOException("Residue name '"+res.GetQualifiedName()+
+                      "' is too long for PDB output. At most 3 characters are "
+                      "allowed");
+  }
+  line(17, 3)=fmt::LPadded(res.GetKey());
+  
+  String chain_name=res.GetChain().GetName();
+  if (chain_name.size()>0) {
+    if (chain_name.size()==1) {
+      line[21]=chain_name[0];
+    } else {
+      throw IOException("PDB format only support single-letter chain names");
+    }
+  }
+  line(22, 4)=fmt::LPaddedInt(res.GetNumber().GetNum());
+  if (ins_code!=0) {
+    line[26]=ins_code;
+  }
+  
+  // deal with alternative atom locations
   if (names.empty()) {
-    geom::Vec3 p=atom.GetPos();
-    String ins_str=(ins_code==0 ? " " : String(1, ins_code));
-    ostr << record_name << format("%5d") % atomnum
-         << format(" %-4s") % aname_str
-         << " "
-         << format("%.3s") % res.GetKey()
-         << format(" %.1s") % res.GetChain().GetName()
-         << format("%4d%s") % res.GetNumber().GetNum() %  ins_str
-         << "   "
-         << format("%8.3f%8.3f%8.3f") % p[0] % p[1] % p[2];
-    if(is_pqr) {
-      ostr << format("%6.2f") % atom.GetAtomProps().charge
-           << format("%6.2f") % atom.GetAtomProps().radius;
+    line(30, 8)=fmt::LPaddedFloat(p[0],  3);
+    line(38, 8)=fmt::LPaddedFloat(p[1],  3);
+    line(46, 8)=fmt::LPaddedFloat(p[2],  3);
+    
+    if (is_pqr) {
+      line(54, 6)=fmt::LPaddedFloat(atom.GetCharge(), 2);
+      line(60, 6)=fmt::LPaddedFloat(atom.GetRadius(), 2);
     } else {
-      ostr << format("%6.2f") % atom.GetAtomProps().occupancy
-           << format("%6.2f") % atom.GetAtomProps().b_factor;
+      line(54, 6)=fmt::LPaddedFloat(atom.GetOccupancy(), 2);
+      line(60, 6)=fmt::LPaddedFloat(atom.GetBFactor(), 2);
     }
-    ostr << format("%10s%2s") % "" % atom.GetAtomProps().element
-         << std::endl
-    ;    
+    
+    line(76, 2)=fmt::LPadded(atom.GetElement());
+    ostr << line;
   } else {
-    for (std::vector<String>::const_iterator i=names.begin(), 
-         e=names.end(); i!=e; ++i) {
-       geom::Vec3 p=atom.GetAltPos(*i);
-       String ins_str=(ins_code==0 ? " " : String(1, ins_code));
-       ostr << record_name << format("%5d") % atomnum
-            << format(" %-4s") % aname_str
-            << *i
-            << format("%.3s") % res.GetKey()
-            << format(" %.1s") % res.GetChain().GetName()
-            << format("%4d%s") % res.GetNumber().GetNum() %  ins_str
-            << "   "
-            << format("%8.3f%8.3f%8.3f") % p[0] % p[1] % p[2];
-       if(is_pqr) {
-         ostr << format("%6.2f") % atom.GetAtomProps().charge
-              << format("%6.2f") % atom.GetAtomProps().radius;
-       } else {
-         ostr << format("%6.2f") % atom.GetAtomProps().occupancy
-              << format("%6.2f") % atom.GetAtomProps().b_factor;
-       }
-       ostr << format("%10s%2s") % "" % atom.GetAtomProps().element
-            << std::endl
-       ;       
+    for (std::vector<String>::const_iterator
+         i=names.begin(), e=names.end(); i!=e; ++i) {
+      p=atom.GetAltPos(*i);
+      line(30, 50).Clear();
+
+      if (i->size()>1) {
+        throw IOException("Alternative atom indicator '"+atom.GetQualifiedName()+
+                          "("+*i+")' too long for PDB output. At most 1 "
+                          "character is allowed");
+      }
+      line[16]=(*i)[0];
+      line(30, 8)=fmt::LPaddedFloat(p[0],  3);
+      line(38, 8)=fmt::LPaddedFloat(p[1],  3);
+      line(46, 8)=fmt::LPaddedFloat(p[2],  3);
+
+      if (is_pqr) {
+       line(54, 6)=fmt::LPaddedFloat(atom.GetCharge(), 2);
+       line(60, 6)=fmt::LPaddedFloat(atom.GetRadius(), 2);
+      } else {
+       line(54, 6)=fmt::LPaddedFloat(atom.GetOccupancy(), 2);
+       line(60, 6)=fmt::LPaddedFloat(atom.GetBFactor(), 2);
+      }
+
+      line(76, 2)=fmt::LPadded(atom.GetElement());
+      ostr << line;
     }
   }
+
+  line.Clear();
 }
 
 void write_conect(std::ostream& ostr, int atom_index,
@@ -109,35 +166,63 @@ void write_conect(std::ostream& ostr, int atom_index,
 
 class PDBWriterImpl : public mol::EntityVisitor {
 public:
-  PDBWriterImpl(std::ostream& ostream, std::map<long,int>& atom_indices)
-    : ostr_(ostream), counter_(0), is_pqr_(false), last_chain_(),
-      atom_indices_(atom_indices) {
+  PDBWriterImpl(std::ostream& ostream, FormattedLine& line, 
+                std::map<long,int>& atom_indices)
+    : ostr_(ostream), counter_(0), is_pqr_(false),
+      atom_indices_(atom_indices), line_(line), peptide_(false) {
   }
 private:
 public:
   virtual bool VisitAtom(const mol::AtomHandle& atom) {
     counter_++;
-    if (last_chain_!=atom.GetResidue().GetChain()) {
-      if (last_chain_.IsValid()) {
-        ostr_ << "TER" << std::endl;
-      }
-      last_chain_=atom.GetResidue().GetChain();
-    }    
-    write_atom(ostr_, atom, counter_, is_pqr_);
+    write_atom(ostr_, line_, atom, counter_, is_pqr_);
     if (atom.GetAtomProps().is_hetatm) {
       atom_indices_[atom.GetHashCode()]=counter_;
     }
     return true;
   }
+  
+  virtual bool VisitResidue(const mol::ResidueHandle& res)
+  {
+    if (res.IsPeptideLinking()) {
+      peptide_=true;
+    } else {
+      if (peptide_) {
+        this->WriteTer(prev_);
+      }
+      peptide_=false;
+    }
+    prev_=res;
+    return true;
+  }
+  
+  void WriteTer(mol::ResidueHandle res)
+  {
+    counter_++;
+    line_(0, 6)=StringRef("TER   ", 6);
+    line_( 6, 5)=fmt::LPaddedInt(counter_);
+    line_(17, 3)=fmt::LPadded(res.GetKey());
+    line_[21]=res.GetChain().GetName()[0];
+    line_(22, 4)=fmt::LPaddedInt(res.GetNumber().GetNum());
+    char ins_code=res.GetNumber().GetInsCode();
+    if (ins_code!=0) {
+      line_[26]=ins_code;
+    }    
+    ostr_ << line_;
+    line_.Clear();
+  }
+  
   void SetIsPQR(bool t) {
     is_pqr_=t;
   }
 private:
-  std::ostream&    ostr_;
-  int              counter_;
-  bool             is_pqr_;
-  mol::ChainHandle last_chain_;
+  std::ostream&       ostr_;
+  int                 counter_;
+  bool                is_pqr_;
   std::map<long,int>& atom_indices_;
+  FormattedLine&      line_;
+  mol::ResidueHandle  prev_;
+  bool                peptide_;
 };
 
 class PDBConectWriterImpl : public mol::EntityVisitor {
@@ -175,15 +260,18 @@ private:
 }
 
 PDBWriter::PDBWriter(std::ostream& stream):
-  outfile_(), outstream_(stream)
-{}
+  outfile_(), outstream_(stream), mol_count_(0), line_(80)
+{
+  
+}
 
 PDBWriter::PDBWriter(const boost::filesystem::path& filename):
-  outfile_(filename.file_string().c_str()), outstream_(outfile_), mol_count_(0)
+  outfile_(filename.file_string().c_str()), outstream_(outfile_), 
+  mol_count_(0), line_(80)
 {}
 
 PDBWriter::PDBWriter(const String& filename):
-  outfile_(filename.c_str()), outstream_(outfile_), mol_count_(0)
+  outfile_(filename.c_str()), outstream_(outfile_), mol_count_(0), line_(80)
 {}
 
 void PDBWriter::WriteModelLeader()
@@ -208,7 +296,7 @@ template <typename H>
 void PDBWriter::WriteModel(H ent)
 {
   this->WriteModelLeader();
-  PDBWriterImpl writer(outstream_,atom_indices_);
+  PDBWriterImpl writer(outstream_,line_, atom_indices_);
   if (PDB::Flags() & PDB::PQR_FORMAT) {
     writer.SetIsPQR(true);
   }
@@ -235,14 +323,7 @@ void PDBWriter::Write(const mol::AtomHandleList& atoms)
   mol::ChainHandle last_chain;
   for (mol::AtomHandleList::const_iterator i=atoms.begin(),
        e=atoms.end(); i!=e; ++i, ++counter) {
-
-    if (last_chain!=(*i).GetResidue().GetChain()) {
-      if (last_chain.IsValid()) {
-        outstream_ << "TER" << std::endl;
-      }
-      last_chain=(*i).GetResidue().GetChain();
-    }
-    write_atom(outstream_, *i, counter, PDB::Flags() & PDB::PQR_FORMAT);
+    write_atom(outstream_, line_, *i, counter, PDB::Flags() & PDB::PQR_FORMAT);
   }
   this->WriteModelTrailer();
 }
diff --git a/modules/io/src/mol/pdb_writer.hh b/modules/io/src/mol/pdb_writer.hh
index 68a9332a9..40ed1cbff 100644
--- a/modules/io/src/mol/pdb_writer.hh
+++ b/modules/io/src/mol/pdb_writer.hh
@@ -28,9 +28,11 @@
 #include <boost/filesystem/fstream.hpp>
 #include <boost/iostreams/filtering_stream.hpp>
 
+#include <ost/mol/mol.hh>
+
 #include <ost/io/module_config.hh>
+#include <ost/io/formatted_line.hh>
 
-#include <ost/mol/mol.hh>
 
 #include "pdb_io.hh"
 
@@ -58,6 +60,7 @@ private:
   std::ostream&   outstream_;
   int mol_count_;
   std::map<long, int> atom_indices_;
+  FormattedLine       line_;
 };
  
 }}
diff --git a/modules/io/tests/test_io_pdb.cc b/modules/io/tests/test_io_pdb.cc
index 811c459a8..053705f01 100644
--- a/modules/io/tests/test_io_pdb.cc
+++ b/modules/io/tests/test_io_pdb.cc
@@ -17,8 +17,10 @@
 // 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 //------------------------------------------------------------------------------
 #include <ost/mol/mol.hh>
+#include <ost/conop/conop.hh>
 #include <ost/io/mol/entity_io_pdb_handler.hh>
 #include <ost/io/pdb_reader.hh>
+#include <ost/io/pdb_writer.hh>
 #include <ost/io/io_exception.hh>
 #define BOOST_TEST_DYN_LINK
 #include <boost/test/unit_test.hpp>
@@ -27,6 +29,36 @@ using boost::unit_test_framework::test_suite;
 using namespace ost;
 using namespace ost::io;
 
+
+bool compare_files(const String& test, const String& gold_standard)
+{
+  std::ifstream test_stream(test.c_str());
+  std::ifstream gold_stream(gold_standard.c_str());
+  String test_line, gold_line;
+  while (true) {
+    bool test_end=std::getline(test_stream, test_line);
+    bool gold_end=std::getline(gold_stream, gold_line);
+    if (!(test_end || gold_end)) {
+      return true;
+    }
+    if (!test_end) {
+      std::cerr << gold_standard << " contains additional line(s):"
+                << std::endl << gold_line;
+      return false;
+    }
+    if (!gold_end) {
+      std::cerr << test << " contains additional line(s):"
+                << std::endl << test_line;
+      return false;
+    }
+    if (gold_line!=test_line) {
+      std::cerr << "line mismatch:" << std::endl << "test: " << test_line 
+                << std::endl << "gold: " << gold_line;
+      return false;
+    }
+  }
+  return true;
+}
 BOOST_AUTO_TEST_SUITE( io )
 
 
@@ -186,7 +218,54 @@ BOOST_AUTO_TEST_CASE(no_endmdl_record)
   PDBReader reader(fname);
   mol::EntityHandle ent=mol::CreateEntity();
   BOOST_CHECK_THROW(reader.Import(ent), IOException);
+}
+
+BOOST_AUTO_TEST_CASE(write_atom)
+{
+  std::stringstream out;
+  PDBWriter writer(out);
+  
+  mol::EntityHandle ent=mol::CreateEntity();
+  mol::XCSEditor edi=ent.RequestXCSEditor();
+  mol::ChainHandle ch=edi.InsertChain("A");
+  mol::ResidueHandle r=edi.AppendResidue(ch, "GLY");
+  mol::AtomProp c_prop;
+  c_prop.element="C";
+  c_prop.occupancy=1.0;
+  c_prop.b_factor=128.0;
+  mol::AtomHandle a=edi.InsertAtom(r, "CA", geom::Vec3(32.0, -128.0, -2.5), 
+                                   c_prop);
+  writer.Write(ent);
+  String s=out.str();
+  BOOST_CHECK_EQUAL(s.substr(0, 54), 
+                    "ATOM      1  CA  GLY A   1      32.000-128.000  -2.500");
+  BOOST_CHECK_EQUAL(s.substr(54, 26), 
+                    "  1.00128.00           C  ");
+}
 
+BOOST_AUTO_TEST_CASE(write_hetatom)
+{
+  std::stringstream out;
+  PDBWriter writer(out);
+  
+  mol::EntityHandle ent=mol::CreateEntity();
+  mol::XCSEditor edi=ent.RequestXCSEditor();
+  mol::ChainHandle ch=edi.InsertChain("A");
+  mol::ResidueHandle r=edi.AppendResidue(ch, "CA");
+  mol::AtomProp c_prop;
+  c_prop.element="CA";
+  c_prop.is_hetatm=true;
+  c_prop.mass=40.01;
+  c_prop.occupancy=1.0;
+  c_prop.b_factor=40.75;
+  mol::AtomHandle a=edi.InsertAtom(r, "CA", geom::Vec3(32.0, -128.0, -2.5), 
+                                   c_prop);
+  writer.Write(ent);
+  String s=out.str();
+  BOOST_CHECK_EQUAL(s.substr(0, 54), 
+                    "HETATM    1 CA    CA A   1      32.000-128.000  -2.500");
+  BOOST_CHECK_EQUAL(s.substr(54, 26), 
+                    "  1.00 40.75          CA  ");
 }
 
 BOOST_AUTO_TEST_CASE(no_endmdl_record_fault_tolerant)
@@ -207,4 +286,82 @@ BOOST_AUTO_TEST_CASE(no_endmdl_record_fault_tolerant)
   PDB::PopFlags();
 }
 
+BOOST_AUTO_TEST_CASE(alt_loc_import_export)
+{
+  String fname("testfiles/pdb/alt-loc.pdb");  
+  // this scope is required to force the writer stream to be closed before 
+  // opening the file again in compare_files. Avoids a race condition.
+  {
+    PDBReader reader(fname);
+    PDBWriter writer(String("testfiles/pdb/alt-loc-out.pdb"));
+    
+    mol::EntityHandle ent=mol::CreateEntity();
+    reader.Import(ent);
+    writer.Write(ent);
+  }
+  BOOST_CHECK(compare_files("testfiles/pdb/alt-loc.pdb", 
+                            "testfiles/pdb/alt-loc-out.pdb"));
+}
+
+BOOST_AUTO_TEST_CASE(write_ter)
+{
+  String fname("testfiles/pdb/ter.pdb");  
+  // this scope is required to force the writer stream to be closed before 
+  // opening the file again in compare_files. Avoids a race condition.
+  {
+    PDBReader reader(fname);
+    PDBWriter writer(String("testfiles/pdb/ter-out.pdb"));
+    
+    mol::EntityHandle ent=mol::CreateEntity();
+    reader.Import(ent);
+    // we use conopology to mark amino acids as peptide-linking. this is require 
+    // for proper TER output
+    conop::Conopology& conop_inst=conop::Conopology::Instance();
+    conop_inst.ConnectAll(conop_inst.GetBuilder(), ent);
+    writer.Write(ent);
+  }
+  BOOST_CHECK(compare_files("testfiles/pdb/ter.pdb", 
+                            "testfiles/pdb/ter-out.pdb"));
+}
+
+
+BOOST_AUTO_TEST_CASE(res_name_too_long)
+{
+  std::stringstream out;
+  PDBWriter writer(out);
+  
+  mol::EntityHandle ent=mol::CreateEntity();
+  mol::XCSEditor edi=ent.RequestXCSEditor();
+  mol::ChainHandle ch=edi.InsertChain("A");
+  mol::ResidueHandle r=edi.AppendResidue(ch, "CALCIUM");
+  mol::AtomHandle a=edi.InsertAtom(r, "CA", geom::Vec3(32.0, -128.0, -2.5));
+  BOOST_CHECK_THROW(writer.Write(ent), IOException);
+}
+
+BOOST_AUTO_TEST_CASE(chain_name_too_long)
+{
+  std::stringstream out;
+  PDBWriter writer(out);
+  
+  mol::EntityHandle ent=mol::CreateEntity();
+  mol::XCSEditor edi=ent.RequestXCSEditor();
+  mol::ChainHandle ch=edi.InsertChain("AB");
+  mol::ResidueHandle r=edi.AppendResidue(ch, "CA");
+  mol::AtomHandle a=edi.InsertAtom(r, "CA", geom::Vec3(32.0, -128.0, -2.5));
+  BOOST_CHECK_THROW(writer.Write(ent), IOException);
+}
+
+BOOST_AUTO_TEST_CASE(atom_name_too_long)
+{
+  std::stringstream out;
+  PDBWriter writer(out);
+  
+  mol::EntityHandle ent=mol::CreateEntity();
+  mol::XCSEditor edi=ent.RequestXCSEditor();
+  mol::ChainHandle ch=edi.InsertChain("A");
+  mol::ResidueHandle r=edi.AppendResidue(ch, "CA");
+  mol::AtomHandle a=edi.InsertAtom(r, "CALCIUM", geom::Vec3(32.0, -128.0, -2.5));
+  BOOST_CHECK_THROW(writer.Write(ent), IOException);
+}
+
 BOOST_AUTO_TEST_SUITE_END()
diff --git a/modules/io/tests/testfiles/pdb/alt-loc.pdb b/modules/io/tests/testfiles/pdb/alt-loc.pdb
new file mode 100644
index 000000000..b08f83459
--- /dev/null
+++ b/modules/io/tests/testfiles/pdb/alt-loc.pdb
@@ -0,0 +1,3 @@
+ATOM      1  N  AMET A   1      16.000  64.000   0.000  0.50  1.00           N  
+ATOM      1  N  BMET A   1       8.000-128.000   0.000  0.50  1.00           N  
+END   
\ No newline at end of file
diff --git a/modules/io/tests/testfiles/pdb/ter.pdb b/modules/io/tests/testfiles/pdb/ter.pdb
new file mode 100644
index 000000000..fb85bb403
--- /dev/null
+++ b/modules/io/tests/testfiles/pdb/ter.pdb
@@ -0,0 +1,15 @@
+ATOM      1  N   THR A 161      29.926 -12.642 -13.047  1.00 25.35           N  
+ATOM      2  CA  THR A 161      29.978 -12.367 -11.602  1.00 25.36           C  
+ATOM      3  C   THR A 161      29.215 -13.430 -10.777  1.00 27.67           C  
+ATOM      4  O   THR A 161      29.065 -13.298  -9.552  1.00 26.09           O  
+ATOM      5  CB  THR A 161      29.363 -11.029 -11.309  1.00 24.99           C  
+ATOM      6  OG1 THR A 161      28.023 -11.041 -11.744  1.00 22.56           O  
+ATOM      7  CG2 THR A 161      29.998  -9.872 -12.148  1.00 23.85           C  
+ATOM      8  N   ALA A 162      28.657 -14.427 -11.463  1.00 28.77           N  
+ATOM      9  CA  ALA A 162      28.111 -15.569 -10.810  1.00 30.99           C  
+ATOM     10  C   ALA A 162      29.301 -16.453 -10.436  1.00 33.52           C  
+ATOM     11  O   ALA A 162      29.230 -17.243  -9.498  1.00 36.04           O  
+ATOM     12  CB  ALA A 162      27.155 -16.295 -11.713  1.00 30.93           C  
+TER      13      ALA A 162                                                      
+HETATM   14  O   HOH A 164      25.516   0.940  27.392  1.00 38.40           O  
+END   
\ No newline at end of file
-- 
GitLab