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 # If mo_path is not nil, then clear existing mo info and load from mo_path
784 def sub_load_janpa_log(inppath, mo_path = nil)
787 fp = File.open(mo_path, "rt") rescue fp = nil
789 hide_progress_panel # Close if it is open
790 message_box("Cannot open MO molden file #{mo_path}: " + $!.to_s)
794 type = get_mo_info(:type)
795 alpha = get_mo_info(:alpha)
796 beta = get_mo_info(:beta)
798 set_mo_info(:type=>type, :alpha=>alpha, :beta=>beta)
799 # mol.@hf_type should be set before calling sub_load_molden
804 fp = File.open(inppath + ".janpa.log", "rt") rescue fp = nil
806 hide_progress_panel # Close if it is open
807 message_box("Cannot open JANPA log file #{inppath + '.janpa.log'}: " + $!.to_s)
810 print("Importing #{inppath}.janpa.log.\n")
812 getline = lambda { lineno += 1; return fp.gets }
815 h["software"] = "JANPA"
816 nao_num = nil # Set later
817 nao_infos = [] # index=atom_index, value=Hash with key "s", "px", "py" etc.
818 # nao_infos[index][key]: array of [nao_num, occupancy], in the reverse order of appearance
819 while line = getline.call
822 while line = getline.call
823 break if line !~ /^\s*[1-9]/
824 num = Integer(line[0, 5])
826 occ = Float(line[26, 11])
828 # atom_number, occupied?, group_number, orb_sym, angular_number
829 name =~ /\s*[A-Z]+([0-9]+)(\*?):\s* R([0-9]+)\*([a-z]+)\(([-0-9]+)\)/
832 group_num = Integer($3)
834 ang_num = Integer($5)
837 orb_desc += ["z", "x", "y"][ang_num + 1]
838 elsif orb_desc == "d"
839 # TODO: handle d, f, g orbitals
841 h["NAO"].push([num, anum, occupied, group_num, orb_desc, occ])
842 nao_num = h["NAO"].length
843 ((nao_infos[anum - 1] ||= Hash.new)[orb_desc] ||= []).unshift([nao_num, occ])
847 nao_infos.each_with_index { |value, atom_index|
848 aname = self.atoms[atom_index].name
849 value.each { |orb_desc, ar|
850 ar.each_with_index { |v, group_index|
858 principle = group_index + 1
859 orb_sym = orb_desc[0]
869 h["NAO_L"][v[0] - 1] = "#{aname} (#{principle}#{orb_desc}) (#{label})"
873 elsif line =~ /^\s*(C?)LPO\s+D e s c r i p t i o n\s+Occupancy\s+Composition/
880 while line = getline.call
881 break if line =~ /^\s*$/
882 num = Integer(line[0, 5])
883 label1 = line[5, 6].strip
884 desc = line[11, 30].strip
885 occ = line[41, 11].strip
886 comp = line[52, 1000].strip
887 desc =~ /\s*([-A-Za-z0-9]+)(,\s*(.*$))?/
890 if desc2 =~ /^(.*)*\(NB\)\s*$/ && label1 == ""
894 atoms = desc1.scan(/[A-Za-z]+(\d+)/) # "C1-H3" -> [["1"], ["3"]]
895 atoms = atoms.map { |a| Integer(a[0]) } # [1, 3]
896 hybrids_a = comp.scan(/h(\d+)@[A-Za-z]+(\d+)/) # "h8@C1...h13@H3" -> "[["8", "1"], ["13", "3"]]
899 i = atoms.find_index(Integer(a[1]))
901 hybrids[i] = Integer(a[0])
904 # like ["(BD)", [1, 3], "Io = 0.2237", occ, [8, 13]]
905 # 1, 3 are the atom indices (1-based)
906 # 8, 13 are the number of hybrid orbitals (1-based)
907 h[key][num - 1] = [label1, atoms, desc2, Float(occ), hybrids]
911 # Also register labels of "LHO"
912 h["LHO_L"] = [""] * nao_num
917 label = "" # The labels for Rydberg orbitals may be replaced later
919 aname1 = self.atoms[val[1][0] - 1].name rescue aname1 = ""
920 aname2 = self.atoms[val[1][1] - 1].name rescue aname2 = ""
922 label = "#{aname1} #{val[0]}"
924 label = "#{aname1}(#{aname2}) #{val[0]}"
927 h[key + "_L"][i] = label
928 if key == "CLPO" && val != nil && val[0] != "(NB)"
930 kind = (val[0] == "(BD)" ? "(val)" : "(lp)")
932 label = "#{aname1} #{kind}"
934 label = "#{aname1}(#{aname2}) #{kind}"
936 h["LHO_L"][hybrids[0] - 1] = label
938 # aname2 should be non-empty
939 label = "#{aname2}(#{aname1}) #{kind}"
940 h["LHO_L"][hybrids[1] - 1] = label
944 elsif line =~ /^ -NAO_Molden_File: (\S*)/
946 elsif line =~ /^ -LHO_Molden_File: (\S*)/
948 elsif line =~ /^ -CLPO_Molden_File: (\S*)/
950 elsif line =~ /^ -PNAO_Molden_File: (\S*)/
952 elsif line =~ /^ -AHO_Molden_File: (\S*)/
954 elsif line =~ /^ -LPO_Molden_File: (\S*)/
960 mfiles.each { |key, value|
961 fp = Kernel.open(value, "rt") rescue fp = nil
963 print("Importing #{value}.\n")
964 res = sub_load_molden(fp, key)
966 # Some kind of orbital based on AO
967 h["AO/#{key}"] = LAMatrix.new(res[:mo])
970 if key == "CLPO" || key == "LPO" || key == "LHO"
971 # Set the label of Rydberg orbitals if possible
972 if h[key + "_L"] != nil
975 label = h[key + "_L"][i]
979 nao_infos.each_with_index { |inf, atom_index|
981 inf.each { |k, v| # k is "s", "px" etc, v is array of [nao_num, occupancy]
982 # Sum for all naos belonging to this atom
984 atomic_contrib += a[i, num_occ[0] - 1] ** 2
987 if atomic_contrib > max_val
988 max_val = atomic_contrib
992 label = self.atoms[max_idx].name + " (ry)"
993 h[key + "_L"][i] = label
1001 if @nbo["AO/NAO"] && @nbo["AO/LHO"] && @nbo["AO/PNAO"]
1002 # Generate PLHO from PNAO, NAO, LHO
1003 # This protocol was suggested by the JANPA author in a private commnunication.
1005 nao2lho = @nbo["AO/NAO"].inverse * @nbo["AO/LHO"]
1006 nao2pnao = @nbo["AO/NAO"].inverse * @nbo["AO/PNAO"]
1007 sign = LAMatrix.diagonal((0...nao2pnao.column_size).map { |i| (nao2pnao[i, i] < 0 ? -1 : 1)})
1008 @nbo["AO/PLHO"] = @nbo["AO/PNAO"] * sign * nao2lho
1010 @nbo["AO/PLHO"] = nil
1015 $stderr.write(e.message + "\n")
1016 $stderr.write(e.backtrace.inspect + "\n")
1020 def loadout(filename)
1022 fp = open(filename, "rb")
1028 retval = sub_load_gaussian_log(fp)
1031 retval = sub_load_gamess_log(fp)
1034 retval = sub_load_psi4_log(fp)
1036 # If .molden file exists, then try to read it
1037 namepath = filename.gsub(/\.\w*$/, "")
1038 mname = "#{namepath}.molden"
1039 if File.exists?(mname)
1040 fp2 = open(mname, "rb")
1042 flag = sub_load_molden(fp2)
1044 status = (flag ? 0 : -1)
1047 if File.exists?("#{namepath}.janpa.log")
1048 flag = sub_load_janpa_log(namepath)
1049 status = (flag ? 0 : -1)
1063 alias :loadlog :loadout
1065 def loadxyz(filename)
1066 fp = open(filename, "rb")
1079 if coords.length == 0
1080 # Allow "number of atoms" line or "number of atoms and crystallographic cell parameter"
1081 # (Chem3D xyz format)
1082 next if toks.length == 1
1084 cell = toks[1..6].map { |s| Float(s.sub(/\(\d+\)/, "")) } # Remove (xx) and convert to float
1088 name, x, y, z = line.split
1090 x = Float(x.sub(/\(\d+\)/, ""))
1091 y = Float(y.sub(/\(\d+\)/, ""))
1092 z = Float(z.sub(/\(\d+\)/, ""))
1093 r = Vector3D[x, y, z]
1101 celltr = self.cell_transform
1103 names.each_index { |i|
1104 ap = add_atom(names[i])
1105 names[i] =~ /^([A-Za-z]{1,2})/
1106 element = $1.capitalize
1107 ap.element = element
1108 ap.atom_type = element
1110 ap.r = celltr * coords[i]
1119 # (j + 1 ... natoms).each { |k|
1120 # if calc_bond(j, k) < 1.7
1121 # # create_bond(j, k)
1125 # self.undo_enabled = save_undo_enabled
1126 (n > 0 ? true : false)
1129 def savexyz(filename)
1130 open(filename, "wb") { |fp|
1131 fp.printf "%d\n", self.natoms
1133 fp.printf "%s %.5f %.5f %.5f\n", ap.element, ap.x, ap.y, ap.z
1139 def loadzmat(filename)
1141 open(filename, "rb") { |fp|
1142 while (line = fp.gets)
1145 an = Molecule.guess_atomic_number_from_name(a[0])
1146 elm = Parameter.builtin.elements[an].name
1147 base1 = a[1].to_i - 1
1148 base2 = a[3].to_i - 1
1149 base3 = a[5].to_i - 1
1150 base1 = nil if base1 < 0
1151 base2 = nil if base2 < 0
1152 base3 = nil if base3 < 0
1153 add_atom(a[0], elm, elm, a[2].to_f, base1, a[4].to_f, base2, a[6].to_f, base3)
1159 def savezmat(filename)
1162 def loadcom(filename)
1163 # save_undo_enabled = self.undo_enabled?
1164 # self.undo_enabled = false
1166 fp = open(filename, "rb")
1168 while (line = fp.gets)
1171 section = 1 if line =~ /^\#/
1173 elsif section == 1 || section == 2
1174 section += 1 if line =~ /^\s*$/
1177 # The first line is skipped (charge and multiplicity)
1178 while (line = fp.gets)
1180 break if line =~ /^\s*$/
1181 a = line.split(/\s*[ \t,\/]\s*/)
1182 r = Vector3D[Float(a[1]), Float(a[2]), Float(a[3])]
1184 a[0] =~ /^([A-Za-z]{1,2})/
1185 element = $1.capitalize
1186 ap.element = element
1187 ap.atom_type = element
1195 # self.undo_enabled = save_undo_enabled
1199 def loadinp(filename)
1200 # save_undo_enabled = self.undo_enabled?
1201 # self.undo_enabled = false
1203 fp = open(filename, "rb")
1206 while (line = fp.gets)
1207 if line =~ /\A \$BASIS/
1211 next if line !~ /\A \$DATA/
1212 line = fp.gets # Title line
1213 line = fp.gets # Symmetry line
1214 while (line = fp.gets)
1217 break if line =~ /\$END/
1220 ap.atomic_number = Integer(a[1])
1221 ap.atom_type = ap.element
1222 r = Vector3D[Float(a[2]), Float(a[3]), Float(a[4])]
1225 # Skip until one blank line is detected
1226 while (line = fp.gets) && line =~ /\S/
1234 # self.undo_enabled = save_undo_enabled
1238 def saveinp(filename)
1240 raise MolbyError, "cannot save GAMESS input; the molecule is empty"
1242 fp = open(filename, "wb")
1244 fp.print <<end_of_header
1246 ! Generated by Molby at #{now}
1247 $CONTRL COORD=UNIQUE EXETYP=RUN ICHARG=0
1248 ICUT=20 INTTYP=HONDO ITOL=30
1249 MAXIT=200 MOLPLT=.T. MPLEVL=0
1250 MULT=1 QMTTOL=1e-08 RUNTYP=OPTIMIZE
1251 SCFTYP=RHF UNITS=ANGS $END
1252 $SCF CONV=1.0E-06 DIRSCF=.T. $END
1253 $STATPT NSTEP=400 OPTTOL=1.0E-06 $END
1254 $SYSTEM MEMDDI=0 MWORDS=16 TIMLIM=50000 $END
1255 $BASIS GBASIS=N31 NDFUNC=1 NGAUSS=6 $END
1256 $GUESS GUESS=HUCKEL $END
1263 next if ap.atomic_number == 0
1264 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
1271 def savecom(filename)
1273 raise MolbyError, "cannot save Gaussian input; the molecule is empty"
1275 fp = open(filename, "wb")
1276 base = File.basename(filename, ".*")
1277 fp.print <<end_of_header
1281 #{name}; created by Molby at #{Time.now.to_s}
1286 next if ap.atomic_number == 0
1287 fp.printf "%-6s %10.6f %10.6f %10.6f\n", ap.element, ap.r.x, ap.r.y, ap.r.z
1294 alias :loadgjf :loadcom
1295 alias :savegjf :savecom
1297 def loadcif(filename)
1300 while @tokens.length == 0
1305 while line = fp.gets
1306 break if line[0] == ?;
1312 line = line[1...line.length]
1315 line.scan(/'[^\']*'|"[^\"]*"|[^#\s]+|#.*/) do |s|
1317 if s =~ /^data_|loop_|global_|save_|stop_/i
1318 s = "#" + s # Label for reserved words
1323 return @tokens.shift
1325 def float_strip_rms(str)
1326 str =~ /^(-?)(\d*)(\.(\d*))?(\((\d+)\))?$/
1327 sgn, i, frac, rms = $1, $2, $4, $6
1330 base = 0.1 ** frac.length
1331 i = i + frac.to_f * base
1336 rms = rms.to_f * base
1345 def parse_symmetry_operation(str)
1348 elsif (str =~ /(\d+)_(\d)(\d)(\d)/) || (str =~ /(\d+) +(\d)(\d)(\d)/)
1349 sym = [Integer($1) - 1, Integer($2) - 5, Integer($3) - 5, Integer($4) - 5]
1350 elsif (str =~ /^(\d+)$/)
1351 sym = [Integer($1) - 1, 0, 0, 0]
1353 if sym && (sym[0] == 0 && sym[1] == 0 && sym[2] == 0 && sym[3] == 0)
1358 def find_atom_by_name(mol, name)
1359 name = name.delete(" ()")
1360 ap = mol.atoms[name] rescue ap = nil
1363 selfname = self.name
1364 fp = open(filename, "rb")
1365 data_identifier = nil
1371 bond_defined = false
1372 special_positions = []
1375 cell_trans = cell_trans_inv = Transform.identity
1376 token = getciftoken(fp)
1377 pardigits_re = /\(\d+\)/
1378 calculated_atoms = []
1380 if token =~ /^\#data_/i
1381 if data_identifier == nil || mol.natoms == 0
1382 # First block or no atoms yet
1383 # Continue processing of this molecule
1384 data_identifier = token
1385 token = getciftoken(fp)
1388 # Description of another molecule begins here
1389 data_identifier = token
1392 elsif token =~ /^_cell/
1393 val = getciftoken(fp)
1394 if token == "_cell_length_a"
1395 cell[0], cell[6] = float_strip_rms(val)
1396 elsif token == "_cell_length_b"
1397 cell[1], cell[7] = float_strip_rms(val)
1398 elsif token == "_cell_length_c"
1399 cell[2], cell[8] = float_strip_rms(val)
1400 elsif token == "_cell_angle_alpha"
1401 cell[3], cell[9] = float_strip_rms(val)
1402 elsif token == "_cell_angle_beta"
1403 cell[4], cell[10] = float_strip_rms(val)
1404 elsif token == "_cell_angle_gamma"
1405 cell[5], cell[11] = float_strip_rms(val)
1407 if cell.length == 12 && cell.all?
1409 puts "Unit cell is set to #{mol.cell.inspect}." if verbose
1411 cell_trans = mol.cell_transform
1412 cell_trans_inv = cell_trans.inverse
1414 token = getciftoken(fp)
1416 elsif token.casecmp("#loop_") == 0
1418 while (token = getciftoken(fp)) && token[0] == ?_
1421 if labels[0] =~ /symmetry_equiv_pos|space_group_symop|atom_site_label|atom_site_aniso_label|geom_bond/
1422 hlabel = Hash.new(-10000000)
1423 labels.each_with_index { |lb, i|
1430 break if token == nil || token[0] == ?_ || token[0] == ?#
1436 token = getciftoken(fp)
1438 if labels[0] =~ /^_symmetry_equiv_pos/ || labels[0] =~ /^_space_group_symop/
1440 symstr = d[hlabel["_symmetry_equiv_pos_as_xyz"]] || d[hlabel["_space_group_symop_operation_xyz"]]
1441 symstr.delete("\"\'")
1442 exps = symstr.split(/,/)
1443 sym = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
1444 exps.each_with_index { |s, i|
1445 terms = s.scan(/([-+]?)(([.0-9]+)(\/([0-9]+))?([xXyYzZ])?|([xXyYzZ]))/)
1447 # a[0]: sign, a[2]: numerator, a[4]: denometer
1449 # The number part is a[2]/a[4]
1450 num = Float(a[2])/Float(a[4])
1452 # The number part is either integer or a floating point
1457 num = -num if a[0][0] == ?-
1458 xyz = (a[5] || a[6])
1459 if xyz == "x" || xyz == "X"
1461 elsif xyz == "y" || xyz == "Y"
1463 elsif xyz == "z" || xyz == "Z"
1470 puts "symmetry operation #{sym.inspect}" if verbose
1471 mol.add_symmetry(Transform.new(sym))
1473 puts "#{mol.nsymmetries} symmetry operations are added" if verbose
1474 elsif labels[0] =~ /^_atom_site_label/
1477 name = d[hlabel["_atom_site_label"]]
1478 elem = d[hlabel["_atom_site_type_symbol"]]
1479 fx = d[hlabel["_atom_site_fract_x"]]
1480 fy = d[hlabel["_atom_site_fract_y"]]
1481 fz = d[hlabel["_atom_site_fract_z"]]
1482 uiso = d[hlabel["_atom_site_U_iso_or_equiv"]]
1483 biso = d[hlabel["_atom_site_B_iso_or_equiv"]]
1484 occ = d[hlabel["_atom_site_occupancy"]]
1485 calc = d[hlabel["_atom_site_calc_flag"]]
1486 name = name.delete(" ()")
1487 if elem == nil || elem == ""
1488 if name =~ /[A-Za-z]{1,2}/
1489 elem = $&.capitalize
1494 ap = mol.add_atom(name, elem, elem)
1495 ap.fract_x, ap.sigma_x = float_strip_rms(fx)
1496 ap.fract_y, ap.sigma_y = float_strip_rms(fy)
1497 ap.fract_z, ap.sigma_z = float_strip_rms(fz)
1499 ap.temp_factor, sig = float_strip_rms(biso)
1501 ap.temp_factor, sig = float_strip_rms(uiso)
1502 ap.temp_factor *= 78.9568352087149 # 8*pi*pi
1504 ap.occupancy, sig = float_strip_rms(occ)
1505 if calc == "c" || calc == "calc"
1506 calculated_atoms.push(ap.index)
1508 # Guess special positions
1509 (1...mol.nsymmetries).each { |isym|
1511 sr = (mol.transform_for_symop(isym) * sr) - sr;
1512 nx = (sr.x + 0.5).floor
1513 ny = (sr.y + 0.5).floor
1514 nz = (sr.z + 0.5).floor
1515 if (Vector3D[sr.x - nx, sr.y - ny, sr.z - nz].length2 < 1e-6)
1516 # [isym, -nx, -ny, -nz] transforms this atom to itself
1517 # The following line is equivalent to:
1518 # if special_positions[ap.index] == nil; special_positions[ap.index] = []; end;
1519 # special_positions[ap.index].push(...)
1520 (special_positions[ap.index] ||= []).push([isym, -nx, -ny, -nz])
1523 if verbose && special_positions[ap.index]
1524 puts "#{name} is on the special position: #{special_positions[ap.index].inspect}"
1527 puts "#{mol.natoms} atoms are created." if verbose
1528 elsif labels[0] =~ /^_atom_site_aniso_label/
1529 # Set anisotropic parameters
1532 name = d[hlabel["_atom_site_aniso_label"]]
1533 ap = find_atom_by_name(mol, name)
1535 u11 = d[hlabel["_atom_site_aniso_U_11"]]
1538 u11, usig[0] = float_strip_rms(u11)
1539 u22, usig[1] = float_strip_rms(d[hlabel["_atom_site_aniso_U_22"]])
1540 u33, usig[2] = float_strip_rms(d[hlabel["_atom_site_aniso_U_33"]])
1541 u12, usig[3] = float_strip_rms(d[hlabel["_atom_site_aniso_U_12"]])
1542 u13, usig[4] = float_strip_rms(d[hlabel["_atom_site_aniso_U_13"]])
1543 u23, usig[5] = float_strip_rms(d[hlabel["_atom_site_aniso_U_23"]])
1544 ap.aniso = [u11, u22, u33, u12, u13, u23, 8] + usig
1548 puts "#{c} anisotropic parameters are set." if verbose
1549 elsif labels[0] =~ /^_geom_bond/
1553 n1 = d[hlabel["_geom_bond_atom_site_label_1"]]
1554 n2 = d[hlabel["_geom_bond_atom_site_label_2"]]
1555 sym1 = d[hlabel["_geom_bond_site_symmetry_1"]] || "."
1556 sym2 = d[hlabel["_geom_bond_site_symmetry_2"]] || "."
1557 n1 = find_atom_by_name(mol, n1)
1558 n2 = find_atom_by_name(mol, n2)
1559 next if n1 == nil || n2 == nil
1562 sym1 = parse_symmetry_operation(sym1)
1563 sym2 = parse_symmetry_operation(sym2)
1565 exbonds.push([n1, n2, sym1, sym2])
1567 mol.create_bond(n1, n2)
1569 tr1 = (sym1 ? mol.transform_for_symop(sym1) : Transform.identity)
1570 tr2 = (sym2 ? mol.transform_for_symop(sym2) : Transform.identity)
1571 if special_positions[n1]
1572 # Add extra bonds for equivalent positions of n1
1573 special_positions[n1].each { |symop|
1574 sym2x = mol.symop_for_transform(tr1 * mol.transform_for_symop(symop) * tr1.inverse * tr2)
1575 exbonds.push([n1, n2, sym1, sym2x])
1578 if special_positions[n2]
1579 # Add extra bonds n2-n1.symop, where symop transforms n2 to self
1580 tr = (sym1 ? mol.transform_for_symop(sym1) : Transform.identity)
1581 special_positions[n2].each { |symop|
1582 sym1x = mol.symop_for_transform(tr2 * mol.transform_for_symop(symop) * tr2.inverse * tr1)
1583 exbonds.push([n2, n1, sym2, sym1x])
1590 puts "#{mol.nbonds} bonds are created." if verbose
1591 if calculated_atoms.length > 0
1592 # Guess bonds for calculated hydrogen atoms
1594 calculated_atoms.each { |ai|
1595 if mol.atoms[ai].connects.length == 0
1596 as = mol.find_close_atoms(ai)
1598 mol.create_bond(ai, aj)
1603 puts "#{n1} bonds are guessed." if verbose
1605 if exbonds.length > 0
1606 h = Dialog.run("CIF Import: Symmetry Expansion") {
1608 item(:text, :title=>"There are bonds including symmetry related atoms.\nWhat do you want to do?"),
1609 item(:radio, :title=>"Expand only atoms that are included in those extra bonds.", :tag=>"atoms_only"),
1610 item(:radio, :title=>"Expand fragments having atoms included in the extra bonds.", :tag=>"fragment", :value=>1),
1611 item(:radio, :title=>"Ignore these extra bonds.", :tag=>"ignore")
1613 radio_group("atoms_only", "fragment", "ignore")
1615 if h[:status] == 0 && h["ignore"] == 0
1616 atoms_only = (h["atoms_only"] != 0)
1619 mol.each_fragment { |f| fragments.push(f) }
1623 if debug; puts "extra bond #{ex[0]}(#{ex[2].inspect}) - #{ex[1]}(#{ex[3].inspect})"; end
1628 ex[i + 2] = ex[i - 2]
1630 if debug; puts " symop = #{symop.inspect}"; end
1631 # Expand the atom or the fragment including the atom
1633 ig = IntGroup[ex[i - 2]]
1636 ig = fragments.find { |f| f.include?(ex[i - 2]) }
1637 ig.each_with_index { |n, ii| if n == ex[i - 2]; idx = ii; break; end }
1639 symop[4] = ex[i - 2] # Base atom
1640 if debug; puts " expanding #{ig} by #{symop.inspect}"; end
1641 a = mol.expand_by_symmetry(ig, symop[0], symop[1], symop[2], symop[3])
1642 ex[i + 2] = a[idx] # Index of the expanded atom
1645 if ex[4] && ex[5] && ex[4] != ex[5]
1646 if debug; puts " creating bond #{ex[4]} - #{ex[5]}"; end
1647 mol.create_bond(ex[4], ex[5])
1652 puts "#{mol.nbonds} bonds are created." if verbose
1656 # puts "Loop beginning with #{labels[0]} is skipped"
1660 token = getciftoken(fp)
1662 # Skip tokens until next tag or reserved word is detected
1663 while token != nil && token[0] != ?_ && token[0] != ?#
1664 token = getciftoken(fp)
1671 if token != nil && token == data_identifier
1672 # Process next molecule: open a new molecule and start adding atom on that
1675 (@aux_mols ||= []).push(mol)
1681 # self.undo_enabled = save_undo_enabled
1685 def dump(group = nil)
1690 str = str.gsub(/%/, "%%")
1691 str.gsub!(/ /, "%20")
1692 str.gsub!(/\t/, "%09")
1696 group = atom_group(group ? group : 0...natoms)
1700 s += sprintf("%4d %-7s %-4s %-4s %-2s %7.3f %7.3f %7.3f %6.3f [%s]\n",
1701 ap.index, sprintf("%3s.%d", ap.res_name, ap.res_seq),
1702 quote(ap.name), quote(ap.atom_type), quote(ap.element),
1703 ap.r.x, ap.r.y, ap.r.z, ap.charge,
1704 ap.connects.join(","))
1711 raise "Molecule must be empty"
1713 format = "index residue name atom_type element rx ry rz charge connects"
1717 # arg can be either a String or an array of String.
1718 # Iterates for each line in the string or each member of the array.
1719 if arg.is_a?(String)
1720 arg = arg.split("\n")
1724 format = line[1..-1]
1728 keys = format.split(" ").collect { |key| key.to_sym }
1730 values = line.chomp.split(" ")
1731 next if values == nil || values.length == 0
1732 ap = create_atom(sprintf("X%03d", natoms))
1733 r = Vector3D[0, 0, 0]
1734 keys.each_index { |i|
1735 break if (value = values[i]) == nil
1739 value.gsub(/%09/, "\t")
1740 value.gsub(/%20/, " ")
1741 value.gsub(/%%/, "%")
1745 if resAtoms[value] == nil
1746 resAtoms[value] = []
1748 resAtoms[value].push(ap.index)
1755 elsif key == :connects
1756 value.scan(/\d+/).each { |i|
1759 newBonds.push(ap.index)
1766 ap.set_attr(key, value)
1771 resAtoms.each_key { |key|
1772 assign_residue(atom_group(resAtoms[key]), key)
1774 create_bond(*newBonds)
1778 # Plug-in for loading mbsf
1779 def loadmbsf_plugin(s, lineno)
1783 # Plug-in for saving mbsf