5 # Created by Toshi Nagata.
6 # Copyright 2008 Toshi Nagata. All rights reserved.
8 # This program is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation version 2 of the License.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
23 raise MolbyError, "cannot load crd; the molecule is empty"
25 fp = open(filename, "rb")
28 coords = (0...natoms).collect { Vector3D[0, 0, 0] }
29 periodic = (self.box && self.box[4].any? { |n| n != 0 })
30 show_progress_panel("Loading AMBER crd file...")
31 # puts "sframe = #{sframe}, pos = #{fp.pos}"
34 values = line.scan(/......../)
36 # The first line should be skipped. However, if this line seems to contain
37 # coordinates, then try reading them
38 if values.size != 10 && (values.size > 10 || values.size != natoms * 3)
39 next # Wrong number of coordinates
41 if values.each { |v| Float(v) rescue break } == nil
42 # The line contains non-number
45 puts "Loadcrd: The title line seems to be missing"
47 if count + values.size > natoms * 3
48 raise MolbyError, sprintf("crd format error - too many values at line %d in file %s; number of atoms = %d, current frame = %d", fp.lineno, fp.path, natoms, frame)
51 coords[count / 3][count % 3] = Float(v)
54 if count == natoms * 3
56 if frame == 0 && atoms.all? { |ap| ap.r.x == 0.0 && ap.r.y == 0.0 && ap.r.z == 0.0 }
57 # Do not create a new frame
58 atoms.each_with_index { |ap, i| ap.r = coords[i] }
60 create_frame([coords])
63 # Should have box information
65 if line == nil || (values = line.chomp.scan(/......../)).length != 3
66 # Periodic but no cell information
67 puts "Loadcrd: the molecule has a periodic cell but the crd file does not contain cell info"
71 self.cell = [Float(values[0]), Float(values[1]), Float(values[2]), 90, 90, 90]
76 set_progress_message("Loading AMBER crd file...\n(#{frame} frames completed)")
82 raise MolbyError, sprintf("crd format error - file ended in the middle of frame %d", frame)
86 self.frame = self.nframes - 1
95 raise MolbyError, "cannot save crd; the molecule is empty"
97 fp = open(filename, "wb")
98 show_progress_panel("Saving AMBER crd file...")
99 fp.printf("TITLE: %d atoms\n", natoms)
101 nframes = self.nframes
103 self.update_enabled = false
105 (0...nframes).each { |i|
108 while (j < natoms * 3)
109 w = atoms[j / 3].r[j % 3]
110 fp.printf(" %7.3f", w)
111 fp.print("\n") if (j % 10 == 9 || j == natoms * 3 - 1)
115 set_progress_message("Saving AMBER crd file...\n(#{i} frames completed)")
119 self.update_enabled = true
127 alias :loadmdcrd :loadcrd
128 alias :savemdcrd :savecrd
130 def sub_load_gamess_log_basis_set(lines, lineno)
132 while (line = lines[ln])
134 break if line =~ /SHELL\s+TYPE\s+PRIMITIVE/
139 sym = -10 # undefined
142 while (line = lines[ln])
144 break if line =~ /TOTAL NUMBER OF BASIS SET/
147 add_gaussian_orbital_shell(i, sym, nprims)
155 ln += 1 # Skip the blank line
157 elsif a.length == 5 || a.length == 6
173 raise MolbyError, "Unknown gaussian shell type at line #{lineno + ln}"
177 if (a.length == 5 && sym == -1) || (a.length == 6 && sym != -1)
178 raise MolbyError, "Wrong format in gaussian shell information at line #{lineno + ln}"
182 csp = Float(a[5] || 0.0)
183 add_gaussian_primitive_coefficients(exp, c, csp)
186 raise MolbyError, "Error in reading basis set information at line #{lineno + ln}"
192 def sub_load_gamess_log_mo_coefficients(lines, lineno, ncomps)
196 while (line = lines[ln]) != nil
198 if line =~ /BETA SET/
202 if line =~ /------------/ || line =~ /EIGENVECTORS/ || line =~ /\*\*\*\* (ALPHA|BETA) SET/
205 next unless line =~ /^\s*\d/
206 mo_labels = line.split # MO numbers (1-based)
207 mo_energies = lines[ln].split
208 mo_symmetries = lines[ln + 1].split
209 # puts "mo #{mo_labels.inspect}"
211 mo = mo_labels.map { [] } # array of *independent* empty arrays
212 while (line = lines[ln]) != nil
214 break unless line =~ /^\s*\d/
216 s = line[15 + 11 * i, 11].chomp
217 break if s =~ /^\s*$/
218 mo[i].push(Float(s)) rescue print "line = #{line}, s = #{s}"
219 # line[15..-1].split.each_with_index { |s, i|
220 # mo[i].push(Float(s))
223 mo.each_with_index { |m, i|
224 idx = Integer(mo_labels[i])
225 set_mo_coefficients(idx + (alpha ? 0 : ncomps), Float(mo_energies[i]), m)
226 # if mo_labels[i] % 8 == 1
227 # puts "set_mo_coefficients #{idx}, #{mo_energies[i]}, [#{m[0]}, ..., #{m[-1]}]"
238 def sub_load_gamess_log(fp)
245 self.update_enabled = false
246 mes = "Loading GAMESS log file"
247 show_progress_panel(mes)
249 ne_alpha = ne_beta = 0 # number of electrons
250 rflag = nil # 0, UHF; 1, RHF; 2, ROHF
252 search_mode = 0 # 0, no search; 1, optimize; 2, irc
253 ncomps = 0 # Number of AO terms per one MO (sum of the number of components over all shells)
254 alpha_beta = nil # Flag to read alpha/beta MO appropriately
255 nsearch = 0 # Search number for optimization
259 # frame = nframes - 1
268 if line =~ /ATOM\s+ATOMIC\s+COORDINATES/ || line =~ /COORDINATES OF ALL ATOMS ARE/ || line =~ /COORDINATES \(IN ANGSTROM\) FOR \$DATA GROUP ARE/
269 set_progress_message(mes + "\nReading atomic coordinates...")
273 # next # Skip initial atomic coordinates unless loading into an empty molecule
275 line = fp.gets # Skip one line
279 if line =~ /COORDINATES OF ALL ATOMS ARE/
280 line = fp.gets # Skip one line
286 while (line = fp.gets) != nil
287 break if line =~ /^\s*$/ || line =~ /END OF ONE/ || line =~ /\$END/
288 next if line =~ /-----/
289 name, charge, x, y, z = line.split
290 v = Vector3D[x, y, z]
291 coords.push(v * (first_line ? 0.529177 : 1.0)) # Bohr to angstrom
292 names.push([name, charge])
295 # Build a new molecule
296 names.each_index { |i|
297 ap = add_atom(names[i][0])
298 ap.atomic_number = names[i][1].to_i
299 ap.atom_type = ap.element
306 # (j + 1 ... natoms).each { |k|
307 # if calc_bond(j, k) < 1.7
316 if (search_mode == 1 && nsearch == 1) || first_line
317 # The input coordinate and the first frame for geometry search
318 # can have the same coordinate as the last frame; if this is the case, then
319 # do not create the new frame
320 select_frame(nframes - 1)
323 if (ap.r - coords[ap.index]).length2 > 1e-8
330 create_frame([coords]) # Should not be (coords)
333 set_property("energy", energy) if energy
334 elsif line =~ /BEGINNING GEOMETRY SEARCH POINT/
335 energy = nil # New search has begun, so clear the energy
337 elsif line =~ /CONSTRAINED OPTIMIZATION POINT/
338 energy = nil # New search has begun, so clear the energy
340 elsif line =~ /FINAL .* ENERGY IS *([-.0-9]+) AFTER/
343 set_property("energy", energy)
345 elsif line =~ /TOTAL ENERGY += +([-.0-9]+)/
347 elsif false && line =~ /EQUILIBRIUM GEOMETRY LOCATED/i
348 set_progress_message(mes + "\nReading optimized coordinates...")
349 fp.gets; fp.gets; fp.gets
351 while (line = fp.gets) != nil
352 break if line =~ /^\s*$/
354 atom, an, x, y, z = line.split
356 ap.r = Vector3D[x, y, z]
360 # if ne_alpha > 0 && ne_beta > 0
361 # # Allocate basis set record again, to update the atomic coordinates
362 # allocate_basis_set_record(rflag, ne_alpha, ne_beta)
364 elsif line =~ /ATOMIC BASIS SET/
367 while (line = fp.gets)
368 break if line =~ /TOTAL NUMBER OF BASIS SET/
372 ncomps = sub_load_gamess_log_basis_set(lines, lineno)
373 elsif line =~ /NUMBER OF OCCUPIED ORBITALS/
381 elsif line =~ /SCFTYP=(\w+)/
383 if ne_alpha > 0 || ne_beta > 0
392 elsif line =~ /(ALPHA|BETA)\s*SET/
394 elsif line =~ /^\s*(EIGENVECTORS|MOLECULAR ORBITALS)\s*$/
396 clear_mo_coefficients
397 set_mo_info(:type=>["UHF", "RHF", "ROHF"][rflag], :alpha=>ne_alpha, :beta=>ne_beta)
400 line = fp.gets; line = fp.gets
403 set_progress_message(mes + "\nReading MO coefficients...")
404 while (line = fp.gets)
405 break if line =~ /\.\.\.\.\.\./ || line =~ /----------------/
409 sub_load_gamess_log_mo_coefficients(lines, lineno, ncomps)
410 set_progress_message(mes)
411 elsif line =~ /N A T U R A L B O N D O R B I T A L A N A L Y S I S/
413 while (line = fp.gets) != nil
414 break if line =~ /done with NBO analysis/
417 import_nbo_log(nbo_lines)
422 select_frame(nframes - 1)
424 if energy && energy != 0.0
425 set_property("energy", energy)
428 self.update_enabled = true
429 (n > 0 ? true : false)
432 def sub_load_gaussian_log(fp)
446 use_input_orientation = false
447 show_progress_panel("Loading Gaussian out file...")
454 if line =~ /(Input|Standard) orientation/
457 use_input_orientation = true if nf == 0
458 next if !use_input_orientation
460 next if use_input_orientation
462 4.times { line = fp.gets } # Skip four lines
466 while (line = fp.gets) != nil
467 break if line =~ /-----/
468 num, charge, type, x, y, z = line.split
469 coords.push(Vector3D[x, y, z])
474 # Build a new molecule
475 anums.each_index { |i|
477 ap.atomic_number = anums[i]
478 ap.atom_type = ap.element
479 ap.name = sprintf("%s%d", ap.element, i)
485 # (j + 1 ... natoms).each { |k|
486 # if calc_bond(j, k) < 1.7
495 create_frame([coords]) # Should not be (coords)
498 # TODO: to ensure whether the energy output line comes before
499 # or after the atomic coordinates.
500 set_property("energy", energy)
503 elsif line =~ /SCF Done: *E\(\w+\) *= *([-.0-9]+)/
508 set_property("energy", energy)
511 (n > 0 ? true : false)
514 def sub_load_psi4_log(fp)
524 show_progress_panel("Loading Psi4 output file...")
526 getline = lambda { @lineno += 1; return fp.gets }
528 # Import coordinates and energies
531 first_frame = nframes
536 while line = getline.call
537 if line =~ /==> Geometry <==/
538 # Skip until line containing "------"
539 while line = getline.call
540 break if line =~ /------/
544 # Read atom positions
545 while line = getline.call
547 break if line =~ /^\s*$/
548 tokens = line.split(' ')
549 if natoms > 0 && first_frame == nframes
550 if index >= natoms || tokens[0] != atoms[index].element
552 raise MolbyError, "The atom list does not match the current structure at line #{@lineno}"
555 vecs.push(Vector3D[Float(tokens[1]), Float(tokens[2]), Float(tokens[3])])
562 # Create molecule from the initial geometry
563 ats.each_with_index { |aname, i|
567 ap.atom_type = ap.element
568 ap.name = sprintf("%s%d", aname, i)
573 if vecs.length != natoms
574 break # Log file is incomplete
576 # Does this geometry differ from the last one?
577 vecs.length.times { |i|
578 if (atoms[i].r - vecs[i]).length2 > 1.0e-14
579 # Create a new frame and break
581 vecs.length.times { |j|
589 elsif line =~ /Final Energy: +([-.0-9]+)/
590 # Energy for this geometry
592 set_property("energy", energy)
600 elsif line =~ /^ *Nalpha *= *(\d+)/
602 elsif line =~ /^ *Nbeta *= *(\d+)/
608 clear_mo_coefficients
609 set_mo_info(:type => hf_type, :alpha => nalpha, :beta => nbeta)
613 # mol.set_mo_info should be set before calling this function
614 # Optional label is for importing JANPA output: "NAO" or "CPLO"
615 # If label is not nil, then returns a hash containing the following key/value pairs:
616 # :atoms => an array of [element_symbol, seq_num, atomic_num, x, y, z] (angstrom)
617 # :gto => an array of an array of [sym, [ex0, c0, ex1, c1, ...]]
618 # :moinfo => an array of [sym, energy, spin (0 or 1), occ]
619 # :mo => an array of [c0, c1, ...]
620 def sub_load_molden(fp, label = nil)
621 getline = lambda { @lineno += 1; return fp.gets }
622 bohr = 0.529177210903
624 ncomps = 0 # Number of components (AOs)
625 occ_alpha = 0 # Number of occupied alpha orbitals
626 occ_beta = 0 # Number of occupied beta orbitals
631 while line = getline.call
632 if line =~ /^\[Atoms\]/
634 while line = getline.call
636 # element, index, atomic_number, x, y, z (in AU)
639 (hash[:atoms] ||= []).push([a[0], Integer(a[1]), Integer(a[2]), Float(a[3]) * bohr, Float(a[4]) * bohr, Float(a[5]) * bohr])
641 if atoms[i].atomic_number != Integer(a[2]) ||
642 (atoms[i].x - Float(a[3]) * bohr).abs > 1e-4 ||
643 (atoms[i].y - Float(a[4]) * bohr).abs > 1e-4 ||
644 (atoms[i].z - Float(a[5]) * bohr).abs > 1e-4
645 errmsg = "The atom list does not match the current molecule."
654 redo # The next line will be the beginning of the next block
655 elsif line =~ /^\[GTO\]/
662 while line = getline.call
665 break if a.length != 2
667 while line = getline.call
668 # type, no_of_primitives, 1.00?
670 break if a.length != 3 # Terminated by a blank line
677 sym = 2; n = 6 # TODO: handle both spherical and cartesian
683 raise MolbyError, "Unknown gaussian shell type '#{a[0]}' at line #{@lineno} in MOLDEN file"
685 nprimitives = Integer(a[1])
690 nprimitives.times { |i|
691 line = getline.call # exponent, contraction
694 gtoline[1].push(Float(b[0]), Float(b[1]))
696 add_gaussian_primitive_coefficients(Float(b[0]), Float(b[1]), 0.0)
700 add_gaussian_orbital_shell(atom_index, sym, nprimitives)
708 hash[:gto].push(gtos)
711 redo # The next line will be the beginning of the next block
712 elsif line =~ /^\[MO\]/
714 idx_alpha = 1 # set_mo_coefficients() accepts 1-based index of MO
725 sym = nil # Not used in Molby
728 while line = getline.call
729 if line =~ /^ *Sym= *(\w+)/
731 elsif line =~ /^ *Ene= *([-+.0-9e]+)/
733 elsif line =~ /^ *Spin= *(\w+)/
735 elsif line =~ /^ *Occup= *([-+.0-9e]+)/
745 hash[:moinfo].push([sym, ene, (spin == "Alpha" ? 0 : 1), occ])
747 elsif line =~ /^ *([0-9]+) +([-+.0-9e]+)/
758 set_mo_coefficients(idx, ene, m)
760 hash[:mo].push(m.dup)
768 break if i < ncomps # no MO info was found
775 message_box("The MOLDEN file was found but not imported. " + errmsg, "Psi4 import info", :ok)
776 return (label ? nil : false)
778 return (label ? hash : true)
781 # Import the JANPA log and related molden files
782 # Files: inppath.{NAO.molden,CLPO.molden,janpa.log}
783 def sub_load_janpa_log(inppath)
785 fp = File.open(inppath + ".janpa.log", "rt") rescue fp = nil
787 message_box("Cannot open JANPA log file #{inppath + '.janpa.log'}: " + $!.to_s)
790 print("Importing #{inppath}.janpa.log.\n")
792 getline = lambda { lineno += 1; return fp.gets }
795 while line = getline.call
798 while line = getline.call
799 break if line !~ /^\s*[1-9]/
800 num = Integer(line[0, 5])
802 occ = Float(line[26, 11])
804 # atom_number, occupied?, shell_number, orb_sym, angular_number
805 name =~ /\s*[A-Z]+([0-9]+)(\*?):\s* R([0-9]+)\*([a-z]+)\(([-0-9]+)\)/
808 shell_num = Integer($3)
810 ang_num = Integer($5)
811 h["NAO"].push([num, anum, occupied, shell_num, orb_sym, ang_num, occ])
813 elsif line =~ /^\s*CLPO\s+D e s c r i p t i o n\s+Occupancy\s+Composition/
815 while line = getline.call
816 break if line =~ /^\s*$/
817 num = Integer(line[0, 5])
818 label1 = line[5, 6].strip
819 desc = line[11, 30].strip
820 desc =~ /\s*([-A-Za-z0-9]+)(,\s*(.*$))?/
823 if desc2 =~ /^(.*)*\(NB\)\s*$/ && label1 == ""
827 # like ["(BD)", "C1-H3", "Io = 0.2237"]
828 h["CLPO"][num - 1] = [label1, desc1, desc2]
830 elsif line =~ /^ -NAO_Molden_File: (\S*)/
832 elsif line =~ /^ -LHO_Molden_File: (\S*)/
834 elsif line =~ /^ -CLPO_Molden_File: (\S*)/
836 elsif line =~ /^ -PNAO_Molden_File: (\S*)/
838 elsif line =~ /^ -AHO_Molden_File: (\S*)/
840 elsif line =~ /^ -LPO_Molden_File: (\S*)/
846 mfiles.each { |key, value|
847 fp = Kernel.open(value, "rt") rescue fp = nil
849 print("Importing #{value}.\n")
850 res = sub_load_molden(fp, key)
852 # Some kind of orbital based on AO
853 h["AO/#{key}"] = LAMatrix.new(res[:mo])
861 $stderr.write(e.message + "\n")
862 $stderr.write(e.backtrace.inspect + "\n")
866 def loadout(filename)
868 fp = open(filename, "rb")
874 retval = sub_load_gaussian_log(fp)
877 retval = sub_load_gamess_log(fp)
880 retval = sub_load_psi4_log(fp)
882 # If .molden file exists, then try to read it
883 namepath = filename.gsub(/\.\w*$/, "")
884 mname = "#{namepath}.molden"
885 if File.exists?(mname)
886 fp2 = open(mname, "rb")
888 flag = sub_load_molden(fp2)
890 status = (flag ? 0 : -1)
893 if File.exists?("#{namepath}.janpa.log")
894 flag = sub_load_janpa_log(namepath)
895 status = (flag ? 0 : -1)
909 alias :loadlog :loadout
911 def loadxyz(filename)
912 fp = open(filename, "rb")
925 if coords.length == 0
926 # Allow "number of atoms" line or "number of atoms and crystallographic cell parameter"
927 # (Chem3D xyz format)
928 next if toks.length == 1
930 cell = toks[1..6].map { |s| Float(s.sub(/\(\d+\)/, "")) } # Remove (xx) and convert to float
934 name, x, y, z = line.split
936 x = Float(x.sub(/\(\d+\)/, ""))
937 y = Float(y.sub(/\(\d+\)/, ""))
938 z = Float(z.sub(/\(\d+\)/, ""))
939 r = Vector3D[x, y, z]
947 celltr = self.cell_transform
949 names.each_index { |i|
950 ap = add_atom(names[i])
951 names[i] =~ /^([A-Za-z]{1,2})/
952 element = $1.capitalize
954 ap.atom_type = element
956 ap.r = celltr * coords[i]
965 # (j + 1 ... natoms).each { |k|
966 # if calc_bond(j, k) < 1.7
967 # # create_bond(j, k)
971 # self.undo_enabled = save_undo_enabled
972 (n > 0 ? true : false)
975 def savexyz(filename)
976 open(filename, "wb") { |fp|
977 fp.printf "%d\n", self.natoms
979 fp.printf "%s %.5f %.5f %.5f\n", ap.element, ap.x, ap.y, ap.z
985 def loadzmat(filename)
987 open(filename, "rb") { |fp|
988 while (line = fp.gets)
991 an = Molecule.guess_atomic_number_from_name(a[0])
992 elm = Parameter.builtin.elements[an].name
993 base1 = a[1].to_i - 1
994 base2 = a[3].to_i - 1
995 base3 = a[5].to_i - 1
996 base1 = nil if base1 < 0
997 base2 = nil if base2 < 0
998 base3 = nil if base3 < 0
999 add_atom(a[0], elm, elm, a[2].to_f, base1, a[4].to_f, base2, a[6].to_f, base3)
1005 def savezmat(filename)
1008 def loadcom(filename)
1009 # save_undo_enabled = self.undo_enabled?
1010 # self.undo_enabled = false
1012 fp = open(filename, "rb")
1014 while (line = fp.gets)
1017 section = 1 if line =~ /^\#/
1019 elsif section == 1 || section == 2
1020 section += 1 if line =~ /^\s*$/
1023 # The first line is skipped (charge and multiplicity)
1024 while (line = fp.gets)
1026 break if line =~ /^\s*$/
1027 a = line.split(/\s*[ \t,\/]\s*/)
1028 r = Vector3D[Float(a[1]), Float(a[2]), Float(a[3])]
1030 a[0] =~ /^([A-Za-z]{1,2})/
1031 element = $1.capitalize
1032 ap.element = element
1033 ap.atom_type = element
1041 # self.undo_enabled = save_undo_enabled
1045 def loadinp(filename)
1046 # save_undo_enabled = self.undo_enabled?
1047 # self.undo_enabled = false
1049 fp = open(filename, "rb")
1052 while (line = fp.gets)
1053 if line =~ /\A \$BASIS/
1057 next if line !~ /\A \$DATA/
1058 line = fp.gets # Title line
1059 line = fp.gets # Symmetry line
1060 while (line = fp.gets)
1063 break if line =~ /\$END/
1066 ap.atomic_number = Integer(a[1])
1067 ap.atom_type = ap.element
1068 r = Vector3D[Float(a[2]), Float(a[3]), Float(a[4])]
1071 # Skip until one blank line is detected
1072 while (line = fp.gets) && line =~ /\S/
1080 # self.undo_enabled = save_undo_enabled
1084 def saveinp(filename)
1086 raise MolbyError, "cannot save GAMESS input; the molecule is empty"
1088 fp = open(filename, "wb")
1090 fp.print <<end_of_header
1092 ! Generated by Molby at #{now}
1093 $CONTRL COORD=UNIQUE EXETYP=RUN ICHARG=0
1094 ICUT=20 INTTYP=HONDO ITOL=30
1095 MAXIT=200 MOLPLT=.T. MPLEVL=0
1096 MULT=1 QMTTOL=1e-08 RUNTYP=OPTIMIZE
1097 SCFTYP=RHF UNITS=ANGS $END
1098 $SCF CONV=1.0E-06 DIRSCF=.T. $END
1099 $STATPT NSTEP=400 OPTTOL=1.0E-06 $END
1100 $SYSTEM MEMDDI=0 MWORDS=16 TIMLIM=50000 $END
1101 $BASIS GBASIS=N31 NDFUNC=1 NGAUSS=6 $END
1102 $GUESS GUESS=HUCKEL $END
1109 next if ap.atomic_number == 0
1110 fp.printf " %-6s %4d %10.6f %10.6f %10.6f\n", ap.name, ap.atomic_number, ap.r.x, ap.r.y, ap.r.z
1117 def savecom(filename)
1119 raise MolbyError, "cannot save Gaussian input; the molecule is empty"
1121 fp = open(filename, "wb")
1122 base = File.basename(filename, ".*")
1123 fp.print <<end_of_header
1127 #{name}; created by Molby at #{Time.now.to_s}
1132 next if ap.atomic_number == 0
1133 fp.printf "%-6s %10.6f %10.6f %10.6f\n", ap.element, ap.r.x, ap.r.y, ap.r.z
1140 alias :loadgjf :loadcom
1141 alias :savegjf :savecom
1143 def loadcif(filename)
1146 while @tokens.length == 0
1151 while line = fp.gets
1152 break if line[0] == ?;
1158 line = line[1...line.length]
1161 line.scan(/'[^\']*'|"[^\"]*"|[^#\s]+|#.*/) do |s|
1163 if s =~ /^data_|loop_|global_|save_|stop_/i
1164 s = "#" + s # Label for reserved words
1169 return @tokens.shift
1171 def float_strip_rms(str)
1172 str =~ /^(-?)(\d*)(\.(\d*))?(\((\d+)\))?$/
1173 sgn, i, frac, rms = $1, $2, $4, $6
1176 base = 0.1 ** frac.length
1177 i = i + frac.to_f * base
1182 rms = rms.to_f * base
1191 def parse_symmetry_operation(str)
1194 elsif (str =~ /(\d+)_(\d)(\d)(\d)/) || (str =~ /(\d+) +(\d)(\d)(\d)/)
1195 sym = [Integer($1) - 1, Integer($2) - 5, Integer($3) - 5, Integer($4) - 5]
1196 elsif (str =~ /^(\d+)$/)
1197 sym = [Integer($1) - 1, 0, 0, 0]
1199 if sym && (sym[0] == 0 && sym[1] == 0 && sym[2] == 0 && sym[3] == 0)
1204 def find_atom_by_name(mol, name)
1205 name = name.delete(" ()")
1206 ap = mol.atoms[name] rescue ap = nil
1209 selfname = self.name
1210 fp = open(filename, "rb")
1211 data_identifier = nil
1217 bond_defined = false
1218 special_positions = []
1221 cell_trans = cell_trans_inv = Transform.identity
1222 token = getciftoken(fp)
1223 pardigits_re = /\(\d+\)/
1224 calculated_atoms = []
1226 if token =~ /^\#data_/i
1227 if data_identifier == nil || mol.natoms == 0
1228 # First block or no atoms yet
1229 # Continue processing of this molecule
1230 data_identifier = token
1231 token = getciftoken(fp)
1234 # Description of another molecule begins here
1235 data_identifier = token
1238 elsif token =~ /^_cell/
1239 val = getciftoken(fp)
1240 if token == "_cell_length_a"
1241 cell[0], cell[6] = float_strip_rms(val)
1242 elsif token == "_cell_length_b"
1243 cell[1], cell[7] = float_strip_rms(val)
1244 elsif token == "_cell_length_c"
1245 cell[2], cell[8] = float_strip_rms(val)
1246 elsif token == "_cell_angle_alpha"
1247 cell[3], cell[9] = float_strip_rms(val)
1248 elsif token == "_cell_angle_beta"
1249 cell[4], cell[10] = float_strip_rms(val)
1250 elsif token == "_cell_angle_gamma"
1251 cell[5], cell[11] = float_strip_rms(val)
1253 if cell.length == 12 && cell.all?
1255 puts "Unit cell is set to #{mol.cell.inspect}." if verbose
1257 cell_trans = mol.cell_transform
1258 cell_trans_inv = cell_trans.inverse
1260 token = getciftoken(fp)
1262 elsif token.casecmp("#loop_") == 0
1264 while (token = getciftoken(fp)) && token[0] == ?_
1267 if labels[0] =~ /symmetry_equiv_pos|space_group_symop|atom_site_label|atom_site_aniso_label|geom_bond/
1268 hlabel = Hash.new(-10000000)
1269 labels.each_with_index { |lb, i|
1276 break if token == nil || token[0] == ?_ || token[0] == ?#
1282 token = getciftoken(fp)
1284 if labels[0] =~ /^_symmetry_equiv_pos/ || labels[0] =~ /^_space_group_symop/
1286 symstr = d[hlabel["_symmetry_equiv_pos_as_xyz"]] || d[hlabel["_space_group_symop_operation_xyz"]]
1287 symstr.delete("\"\'")
1288 exps = symstr.split(/,/)
1289 sym = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
1290 exps.each_with_index { |s, i|
1291 terms = s.scan(/([-+]?)(([.0-9]+)(\/([0-9]+))?([xXyYzZ])?|([xXyYzZ]))/)
1293 # a[0]: sign, a[2]: numerator, a[4]: denometer
1295 # The number part is a[2]/a[4]
1296 num = Float(a[2])/Float(a[4])
1298 # The number part is either integer or a floating point
1303 num = -num if a[0][0] == ?-
1304 xyz = (a[5] || a[6])
1305 if xyz == "x" || xyz == "X"
1307 elsif xyz == "y" || xyz == "Y"
1309 elsif xyz == "z" || xyz == "Z"
1316 puts "symmetry operation #{sym.inspect}" if verbose
1317 mol.add_symmetry(Transform.new(sym))
1319 puts "#{mol.nsymmetries} symmetry operations are added" if verbose
1320 elsif labels[0] =~ /^_atom_site_label/
1323 name = d[hlabel["_atom_site_label"]]
1324 elem = d[hlabel["_atom_site_type_symbol"]]
1325 fx = d[hlabel["_atom_site_fract_x"]]
1326 fy = d[hlabel["_atom_site_fract_y"]]
1327 fz = d[hlabel["_atom_site_fract_z"]]
1328 uiso = d[hlabel["_atom_site_U_iso_or_equiv"]]
1329 biso = d[hlabel["_atom_site_B_iso_or_equiv"]]
1330 occ = d[hlabel["_atom_site_occupancy"]]
1331 calc = d[hlabel["_atom_site_calc_flag"]]
1332 name = name.delete(" ()")
1333 if elem == nil || elem == ""
1334 if name =~ /[A-Za-z]{1,2}/
1335 elem = $&.capitalize
1340 ap = mol.add_atom(name, elem, elem)
1341 ap.fract_x, ap.sigma_x = float_strip_rms(fx)
1342 ap.fract_y, ap.sigma_y = float_strip_rms(fy)
1343 ap.fract_z, ap.sigma_z = float_strip_rms(fz)
1345 ap.temp_factor, sig = float_strip_rms(biso)
1347 ap.temp_factor, sig = float_strip_rms(uiso)
1348 ap.temp_factor *= 78.9568352087149 # 8*pi*pi
1350 ap.occupancy, sig = float_strip_rms(occ)
1351 if calc == "c" || calc == "calc"
1352 calculated_atoms.push(ap.index)
1354 # Guess special positions
1355 (1...mol.nsymmetries).each { |isym|
1357 sr = (mol.transform_for_symop(isym) * sr) - sr;
1358 nx = (sr.x + 0.5).floor
1359 ny = (sr.y + 0.5).floor
1360 nz = (sr.z + 0.5).floor
1361 if (Vector3D[sr.x - nx, sr.y - ny, sr.z - nz].length2 < 1e-6)
1362 # [isym, -nx, -ny, -nz] transforms this atom to itself
1363 # The following line is equivalent to:
1364 # if special_positions[ap.index] == nil; special_positions[ap.index] = []; end;
1365 # special_positions[ap.index].push(...)
1366 (special_positions[ap.index] ||= []).push([isym, -nx, -ny, -nz])
1369 if verbose && special_positions[ap.index]
1370 puts "#{name} is on the special position: #{special_positions[ap.index].inspect}"
1373 puts "#{mol.natoms} atoms are created." if verbose
1374 elsif labels[0] =~ /^_atom_site_aniso_label/
1375 # Set anisotropic parameters
1378 name = d[hlabel["_atom_site_aniso_label"]]
1379 ap = find_atom_by_name(mol, name)
1381 u11 = d[hlabel["_atom_site_aniso_U_11"]]
1384 u11, usig[0] = float_strip_rms(u11)
1385 u22, usig[1] = float_strip_rms(d[hlabel["_atom_site_aniso_U_22"]])
1386 u33, usig[2] = float_strip_rms(d[hlabel["_atom_site_aniso_U_33"]])
1387 u12, usig[3] = float_strip_rms(d[hlabel["_atom_site_aniso_U_12"]])
1388 u13, usig[4] = float_strip_rms(d[hlabel["_atom_site_aniso_U_13"]])
1389 u23, usig[5] = float_strip_rms(d[hlabel["_atom_site_aniso_U_23"]])
1390 ap.aniso = [u11, u22, u33, u12, u13, u23, 8] + usig
1394 puts "#{c} anisotropic parameters are set." if verbose
1395 elsif labels[0] =~ /^_geom_bond/
1399 n1 = d[hlabel["_geom_bond_atom_site_label_1"]]
1400 n2 = d[hlabel["_geom_bond_atom_site_label_2"]]
1401 sym1 = d[hlabel["_geom_bond_site_symmetry_1"]] || "."
1402 sym2 = d[hlabel["_geom_bond_site_symmetry_2"]] || "."
1403 n1 = find_atom_by_name(mol, n1)
1404 n2 = find_atom_by_name(mol, n2)
1405 next if n1 == nil || n2 == nil
1408 sym1 = parse_symmetry_operation(sym1)
1409 sym2 = parse_symmetry_operation(sym2)
1411 exbonds.push([n1, n2, sym1, sym2])
1413 mol.create_bond(n1, n2)
1415 tr1 = (sym1 ? mol.transform_for_symop(sym1) : Transform.identity)
1416 tr2 = (sym2 ? mol.transform_for_symop(sym2) : Transform.identity)
1417 if special_positions[n1]
1418 # Add extra bonds for equivalent positions of n1
1419 special_positions[n1].each { |symop|
1420 sym2x = mol.symop_for_transform(tr1 * mol.transform_for_symop(symop) * tr1.inverse * tr2)
1421 exbonds.push([n1, n2, sym1, sym2x])
1424 if special_positions[n2]
1425 # Add extra bonds n2-n1.symop, where symop transforms n2 to self
1426 tr = (sym1 ? mol.transform_for_symop(sym1) : Transform.identity)
1427 special_positions[n2].each { |symop|
1428 sym1x = mol.symop_for_transform(tr2 * mol.transform_for_symop(symop) * tr2.inverse * tr1)
1429 exbonds.push([n2, n1, sym2, sym1x])
1436 puts "#{mol.nbonds} bonds are created." if verbose
1437 if calculated_atoms.length > 0
1438 # Guess bonds for calculated hydrogen atoms
1440 calculated_atoms.each { |ai|
1441 if mol.atoms[ai].connects.length == 0
1442 as = mol.find_close_atoms(ai)
1444 mol.create_bond(ai, aj)
1449 puts "#{n1} bonds are guessed." if verbose
1451 if exbonds.length > 0
1452 h = Dialog.run("CIF Import: Symmetry Expansion") {
1454 item(:text, :title=>"There are bonds including symmetry related atoms.\nWhat do you want to do?"),
1455 item(:radio, :title=>"Expand only atoms that are included in those extra bonds.", :tag=>"atoms_only"),
1456 item(:radio, :title=>"Expand fragments having atoms included in the extra bonds.", :tag=>"fragment", :value=>1),
1457 item(:radio, :title=>"Ignore these extra bonds.", :tag=>"ignore")
1459 radio_group("atoms_only", "fragment", "ignore")
1461 if h[:status] == 0 && h["ignore"] == 0
1462 atoms_only = (h["atoms_only"] != 0)
1465 mol.each_fragment { |f| fragments.push(f) }
1469 if debug; puts "extra bond #{ex[0]}(#{ex[2].inspect}) - #{ex[1]}(#{ex[3].inspect})"; end
1474 ex[i + 2] = ex[i - 2]
1476 if debug; puts " symop = #{symop.inspect}"; end
1477 # Expand the atom or the fragment including the atom
1479 ig = IntGroup[ex[i - 2]]
1482 ig = fragments.find { |f| f.include?(ex[i - 2]) }
1483 ig.each_with_index { |n, ii| if n == ex[i - 2]; idx = ii; break; end }
1485 symop[4] = ex[i - 2] # Base atom
1486 if debug; puts " expanding #{ig} by #{symop.inspect}"; end
1487 a = mol.expand_by_symmetry(ig, symop[0], symop[1], symop[2], symop[3])
1488 ex[i + 2] = a[idx] # Index of the expanded atom
1491 if ex[4] && ex[5] && ex[4] != ex[5]
1492 if debug; puts " creating bond #{ex[4]} - #{ex[5]}"; end
1493 mol.create_bond(ex[4], ex[5])
1498 puts "#{mol.nbonds} bonds are created." if verbose
1502 # puts "Loop beginning with #{labels[0]} is skipped"
1506 token = getciftoken(fp)
1508 # Skip tokens until next tag or reserved word is detected
1509 while token != nil && token[0] != ?_ && token[0] != ?#
1510 token = getciftoken(fp)
1517 if token != nil && token == data_identifier
1518 # Process next molecule: open a new molecule and start adding atom on that
1521 (@aux_mols ||= []).push(mol)
1527 # self.undo_enabled = save_undo_enabled
1531 def dump(group = nil)
1536 str = str.gsub(/%/, "%%")
1537 str.gsub!(/ /, "%20")
1538 str.gsub!(/\t/, "%09")
1542 group = atom_group(group ? group : 0...natoms)
1546 s += sprintf("%4d %-7s %-4s %-4s %-2s %7.3f %7.3f %7.3f %6.3f [%s]\n",
1547 ap.index, sprintf("%3s.%d", ap.res_name, ap.res_seq),
1548 quote(ap.name), quote(ap.atom_type), quote(ap.element),
1549 ap.r.x, ap.r.y, ap.r.z, ap.charge,
1550 ap.connects.join(","))
1557 raise "Molecule must be empty"
1559 format = "index residue name atom_type element rx ry rz charge connects"
1563 # arg can be either a String or an array of String.
1564 # Iterates for each line in the string or each member of the array.
1565 if arg.is_a?(String)
1566 arg = arg.split("\n")
1570 format = line[1..-1]
1574 keys = format.split(" ").collect { |key| key.to_sym }
1576 values = line.chomp.split(" ")
1577 next if values == nil || values.length == 0
1578 ap = create_atom(sprintf("X%03d", natoms))
1579 r = Vector3D[0, 0, 0]
1580 keys.each_index { |i|
1581 break if (value = values[i]) == nil
1585 value.gsub(/%09/, "\t")
1586 value.gsub(/%20/, " ")
1587 value.gsub(/%%/, "%")
1591 if resAtoms[value] == nil
1592 resAtoms[value] = []
1594 resAtoms[value].push(ap.index)
1601 elsif key == :connects
1602 value.scan(/\d+/).each { |i|
1605 newBonds.push(ap.index)
1612 ap.set_attr(key, value)
1617 resAtoms.each_key { |key|
1618 assign_residue(atom_group(resAtoms[key]), key)
1620 create_bond(*newBonds)
1624 # Plug-in for loading mbsf
1625 def loadmbsf_plugin(s, lineno)
1629 # Plug-in for saving mbsf