4 # Created by Toshi Nagata.
5 # Copyright 2012 Toshi Nagata. All rights reserved.
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation version 2 of the License.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # Definition for use in ORTEP
25 symcode = (sym[1] + 5) * 10000 + (sym[2] + 5) * 1000 + (sym[3] + 5) * 100 + sym[0] + 1
31 # Used in bond_angle_with_sigma
46 # Export ortep to File fp
47 # If attr is a hash, it represents options for drawing.
48 # "atoms"=>[[group, type, color, rad], [group, type, color, rad], ...]
49 # group is an IntGroup, representing a group of atoms.
50 # type is an atom type: 0 = boundary, 1 = boundary + principal, 2 = 1 + axes, 3 = 2 + shades, 4 = fixed size
51 # color is 0..7 (0,1=black, 2=red, 3=green, 4=blue, 5=cyan, 6=magenta, 7=yellow)
52 # rad is the radius of the atom (in Angstrom) whose type is fixed size
53 # If an atom appears in multiple entries, the later entry is valid.
54 # "bonds"=>[[group1, group2, type, color], [group1, group2, type, color], ...]
55 # group1 and group2 are IntGroups, reprensenting the atoms constituting the bonds.
56 # type is a bond type: 0 to 4 for bonds with no shades to 4 shades.
57 # color is 0..7 as in atoms.
58 # If a bond appears in multiple entries, the later entry is valid.
59 def export_ortep(fp, attr = nil)
62 hidden = atom_group { |ap| !is_atom_visible(ap.index) }
63 hydrogen = self.show_hydrogens
64 expanded = self.show_expanded
65 atomlist = atom_group { |ap|
66 (ap.element != "H" || hydrogen) &&
67 (ap.symop == nil || expanded) &&
68 (!hidden.include?(ap.index))
72 fp.printf "%-78.78s\n", self.name + ": generated by Molby at " + Time.now.to_s
77 cp = [1, 1, 1, 90, 90, 90]
79 fp.printf "%9.3f%9.3f%9.3f%9.3f%9.3f%9.3f\n", cp[0], cp[1], cp[2], cp[3], cp[4], cp[5]
82 syms = self.symmetries
83 if syms == nil || syms.length == 0
84 fp.print "1 0 1 0 0 0 0 1 0 0 0 0 1\n"
86 syms.each_with_index { |s, i|
88 fp.printf "%s%14g%3g%3g%3g%15g%3g%3g%3g%15g%3g%3g%3g\n", (i == syms.length - 1 ? "1" : " "), a[9], a[0], a[1], a[2], a[10], a[3], a[4], a[5], a[11], a[6], a[7], a[8]
92 # Atoms (all symmetry unique atoms regardless they are visible or not)
94 aattr = (attr ? attr["atoms"] : nil)
96 break if ap.symop != nil
97 fp.printf " %4.4s%22s%9.4f%9.4f%9.4f%9d\n", ap.name, "", ap.fract_x, ap.fract_y, ap.fract_z, 0
100 aattr.reverse_each { |at|
101 if at[0].include?(ap.index)
113 if an != nil && rad < 0.0
114 fp.printf " %8.5f%9.6f%9.6f%9.6f%9.6f%9.6f%9d\n", an[0], an[1], an[2], an[3], an[4], an[5], 0
119 rad = 1.2 if rad <= 0
122 fp.printf " %8.3f%9g%9g%9g%9g%9g%9d\n", rad, 0.0, 0.0, 0.0, 0.0, 0.0, type
128 # Special points to specify cartesian axes
129 axis, angle = self.get_view_rotation
130 tr = Transform.rotation(axis, -angle)
131 org = self.get_view_center
132 x = org + tr.column(0)
133 y = org + tr.column(1)
134 tr = self.cell_transform
138 tr = Transform.identity
143 fp.printf " CNTR %9.4f%9.4f%9.4f 0\n", org.x, org.y, org.z
144 fp.printf " %8.3f%9g%9g%9g%9g%9g%9d\n", 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 6
145 fp.printf " X %9.4f%9.4f%9.4f 0\n", x.x, x.y, x.z
146 fp.printf " %8.3f%9g%9g%9g%9g%9g%9d\n", 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 6
147 fp.printf " Y %9.4f%9.4f%9.4f 0\n", y.x, y.y, y.z
148 fp.printf "1%8.3f%9g%9g%9g%9g%9g%9d\n", 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 6
156 # Paper size, margin, viewing distance
157 fp.print " 301 6.6 6.6 0 0.0\n"
159 # Sort atoms by symop and index
161 ig = IntGroup.new # Used for collecting contiguous atoms
162 each_atom(atomlist) { |ap|
163 idx, symcode = ap.to_adc
164 acode = symcode * self.natoms + idx - 1
165 acodes[acode] = ap.index
168 index2an = [] # Index in Molecule to Index in ORTEP
169 ig.each_with_index { |acode, i|
170 index2an[acodes[acode]] = i + 1
174 while (r = ig.range_at(i)) != nil
176 s = (s / self.natoms) + (s % self.natoms + 1) * 100000 # Rebuild true ADC (atom index is in the upper digit)
178 e = (e / self.natoms) + (e % self.natoms + 1) * 100000
190 adcs.each_with_index { |a, i|
196 if i == adcs.length - 1 || k == 6 || (k == 5 && i < adcs.length - 2 && adcs[i + 2] < 0)
203 fp.printf " 501%4d55501%4d55501%4d55501%4d55501%4d55501 1\n", natoms_tep + 1, natoms_tep + 1, natoms_tep + 2, natoms_tep + 1, natoms_tep + 3
204 # fp.print " 502 1 0.0 2 0.0 3 0.0\n"
207 fp.print " 604 1.538\n"
210 bond_inst = Array.new(35) { [] } # Bonds for types 0 to 4 and colors 1 to 7
211 battr = (attr ? attr["bonds"] : nil)
213 next if !atomlist.include?(b[0]) || !atomlist.include?(b[1])
216 battr.reverse_each { |at|
217 if (at[0].include?(b[0]) && at[1].include?(b[1])) || (at[0].include?(b[1]) && at[1].include?(b[0]))
219 bcol = (at[3] != 0 ? at[3] - 1 : at[3]) % 7
226 an1 = atoms[b[0]].atomic_number
227 an2 = atoms[b[1]].atomic_number
228 if an1 == 1 || an2 == 1
230 elsif an1 <= 8 && an2 <= 8
236 bond_inst[btype * 7 + bcol].push(b[0], b[1])
239 # Output bond specifications
240 # Avoid including too many ADCs in a single 811/821 instruction
241 # (Upper limit is 140. Here we divide at every 36 ADCs)
242 output_bonds = lambda { |icode|
243 bond_inst.each_with_index { |inst, ii|
244 next if inst.length == 0
246 if icode / 10 == 81 # 811 instructions
247 fp.printf " 204%9d\n", ii % 7 + 1 # Pen color
249 inst.each_with_index { |b, i|
251 fp.printf " %d %3s", (i >= inst.length - 6 || i % 36 == 30 ? 2 : 1), (i % 36 == 0 ? icode.to_s : "")
253 idx, scode = atoms[b].to_adc
254 fp.printf "%9d", idx * 100000 + scode
255 if i % 6 == 5 || i == inst.length - 1
257 if i == inst.length - 1 || i % 36 == 35
258 fp.printf "%21s%3d%12s%6.3f\n", "", btype, "", 0.05
265 fp.print " 0 1001 0.02\n" # Activate hidden line removal
266 output_bonds.call(821)
269 # Atom type 0=714 (boundary), 1=712 (+principal), 2=716 (+axes), 3=711 (+shades)
270 atom_inst = Array.new(28) { IntGroup.new }
271 aattr = (attr ? attr["atoms"] : nil)
275 aattr.reverse_each { |at|
278 acol = (at[2] != 0 ? at[2] - 1 : at[2]) % 7
285 an1 = atoms[i].atomic_number
294 idx, scode = atoms[i].to_adc
295 atom_inst[atype * 7 + acol].add(idx)
297 (atom_inst.count - 1).downto(0) { |ii|
299 next if inst.count == 0
300 fp.printf " 204%9d\n", ii % 7 + 1 # Pen color
302 atype = [714, 712, 716, 711][ii / 7]
303 while (r = inst.range_at(i)) != nil
304 fp.printf " 1 %3d\n", atype
305 fp.printf "%27s%9d%9d\n", "", r.first, r.last
310 output_bonds.call(811)
318 def savetep(filename)
320 raise MolbyError, "cannot save ORTEP input; the molecule is empty"
322 fp = open(filename, "wb")
332 arg <= 0.0 ? 0.0 : sqrt(arg)
337 # Ref. W. C. Hamilton, Acta Cryst. 1961, 14, 185-189
338 # T. Ito, Acta Cryst 1981, A37, 621-624
340 # An object to describe the best-fit plane
342 # Contains the plane coefficients and the constant (a, b, c, d for ax + by + cz + d = 0),
343 # the error matrix, and the metric tensor.
346 attr_accessor :molecule, :group, :coeff, :const, :error_matrix, :metric_tensor
347 def initialize(mol, group, coeff, const, err, met)
357 [sqrt_safe(@error_matrix[0, 0]), sqrt_safe(@error_matrix[1, 1]), sqrt_safe(@error_matrix[2, 2]), sqrt_safe(@error_matrix[3, 3])]
360 s = sprintf("Molby::Plane[\n coeff, const = [[%f, %f, %f], %f],\n", @coeff.x, @coeff.y, @coeff.z, @const)
361 s += sprintf(" sigma = [[%10.4e, %10.4e, %10.4e], %10.4e],\n", *self.sigma)
363 s += (i == 0 ? " error_matrix = [" : " ")
365 s += sprintf("%12.6e%s", @error_matrix[j, i], (j == i ? (i == 3 ? "],\n" : ",\n") : ","))
368 s += sprintf(" molecule = %s\n", @molecule.inspect)
369 s += sprintf(" group = %s\n", @group.inspect)
371 s += (i == 0 ? " metric_tensor = [" : " ")
373 s += sprintf("%12.6e%s", @metric_tensor[j, i], (j == 3 ? (i == 3 ? "]]\n" : ",\n") : ","))
384 sig = Vector3D[0, 0, 0]
386 d = fr.dot(@coeff) + @const
387 sig1 = (@coeff.x * sig.x) ** 2 + (@coeff.y * sig.y) ** 2 + (@coeff.z * sig.z) ** 2
388 sig2 = LAMatrix.multiply("t", fr, @error_matrix, fr)[0, 0]
389 if ap.is_a?(AtomRef) && ap.molecule == @molecule && @group.include?(ap.index)
390 # The atom defines the plane
392 sig0 = 0.0 if sig0 < 0.0
396 return d, sqrt_safe(sig0)
399 e1 = @error_matrix.submatrix(0, 0, 3, 3)
400 e2 = plane.error_matrix.submatrix(0, 0, 3, 3)
401 m = @metric_tensor.submatrix(0, 0, 3, 3)
403 cos_t = plane.coeff.dot(m * @coeff)
410 sig_t = (m * e1).trace + (m * e2).trace
413 sig_t = w * w * (c.dot(LAMatrix.multiply(m, e1, m, c)) + @coeff.dot(LAMatrix.multiply(m, e2, m, @coeff)))
416 sig_t = sqrt_safe(sig_t) * 180.0 / PI
423 # Calculate best-fit plane for the given atoms
424 # Return value: a Molby::Plane object
431 # Positional parameters and standard deviations
435 each_atom(group) { |ap|
438 if (s = ap.sigma_x) > 0.0 && s < sig_min
441 if (s = ap.sigma_y) > 0.0 && s < sig_min
444 if (s = ap.sigma_z) > 0.0 && s < sig_min
452 s.x = sig_min if s.x < sig_min
453 s.y = sig_min if s.y < sig_min
454 s.z = sig_min if s.z < sig_min
457 # The metric tensor of the reciprocal lattice
458 # g[j, i] = (ai*).dot(aj*), where ai* and aj* are the reciprocal axis vectors
459 t = self.cell_transform
461 t = Transform.identity
467 g = LAMatrix.multiply("t", g2, g2)
469 # The variance-covariance matrices of the atomic parameters
470 # mm[k][n] is a 3x3 matrix describing the correlation between the atoms k and n,
471 # and its components are defined as: sigma_k[i] * sigma_n[j] * corr[k, i, n, j],
472 # where corr(k, i, n, j) is the correlation coefficients between the atomic parameters
474 mm = Array.new(dim) { Array.new(dim) }
475 zero = LAMatrix.zero(3, 3)
478 mkn = LAMatrix.new(3, 3)
483 mkn[j, i] = sig[k][i] * sig[n][j]
485 # Inter-coordinate correlation should be implemented here
490 # Inter-atomic correlation should be implemented here
492 mm[k][n] = (mkn == zero ? zero : mkn)
496 # The variance-covariance matrix of the atom-plance distances
497 # m[j, i] = v.transpose * mm[i][j] * v, where v is the plane coefficient vector
498 # The inverse of m is the weight matrix
499 m = LAMatrix.new(dim, dim)
501 # The matrix representation of the atomic coordinates
502 # y[j, i] = x[i][j] (for j = 0..2), -1 (for j = 3)
503 # y * LAMatrix[a, b, c, d] gives the atom-plane distances for each atom
504 y = LAMatrix.new(4, dim)
512 # The coefficients to be determined
513 n0 = LAMatrix[1, 1, 1, 0]
514 v = LAMatrix[1, 1, 1] # The coefficient part
521 # Set zero to the "constant" part, and normalize the "coefficient" part
524 n0.multiply!(1.0 / n0.fnorm)
526 3.times { |i| v[0, i] = n0[0, i] }
528 # Build the variance-covariance matrix
531 m[j, i] = LAMatrix.multiply("t", v, mm[i][j], v)[0, 0]
534 c = LAMatrix.multiply("t", y, "i", m, y)
536 # Invert c: only the inverse is used in the following, so c is inversed destructively
541 # Determine the tentative solution, which is given by the eigenvector of cinv * g
542 # for the largest eigenvalue
543 evals, evecs = (cinv * g).eigenvalues
544 4.times { |i| n0[0, i] = evecs[3, i] }
548 # Convert the coefficient vector to the reciprocal space
551 # Determine multiplier
552 # In this implementation, the sign of delta-n is opposite from that used in
554 lam = 1.0 / (LAMatrix.multiply("t", h, cinv, h)[0, 0])
556 # Solve the linearized equation
557 # (Is the equation 21 in the reference really correct? Shouldn't it read
558 # B = 1 - lambda * C.inverse * H* ? )
559 b = LAMatrix.multiply(lam, cinv, g)
560 b.sub!(LAMatrix.identity(4))
565 break if dn[0, 0] ** 2 + dn[0, 1] ** 2 + dn[0, 2] ** 2 < 1e-9
570 # Error matrix = b * cinv * b.transpose
571 em = LAMatrix.multiply(b, cinv, "t", b)
572 coeff = Vector3D[n0[0, 0], n0[0, 1], n0[0, 2]]
575 return Molby::Plane.new(self, group, coeff, const, em, g)
580 plane_settings = @plane_settings || Hash.new
582 h = Dialog.new("Best-Fit Planes: " + mol.name, "Close", nil) {
583 refresh_proc = lambda { |it|
584 n = it[:tag][/\d/].to_i
585 g = plane_settings["group#{n}"]
587 str = g.inspect.sub!("IntGroup[", "").sub!("]", "")
588 set_value("group#{n}", str)
590 p = mol.plane(g) rescue p = nil
591 plane_settings["plane#{n}"] = p
596 aps = (n == 1 ? "" : "'")
597 str = sprintf("a%s = %f(%f)\nb%s = %f(%f)\nc%s = %f(%f)\nd%s = %f(%f)",
598 aps, coeff.x, sig[0],
599 aps, coeff.y, sig[1],
600 aps, coeff.z, sig[2],
602 set_value("result#{n}", str)
604 set_value("result#{n}", "")
606 p1 = plane_settings["plane1"]
607 p2 = plane_settings["plane2"]
609 t, sig = p1.dihedral(p2)
610 str = sprintf("%f(%f)", t, sig)
611 set_value("dihedral", str)
613 set_value("dihedral", "")
616 p = plane_settings["plane1"]
619 mol.each_atom(g) { |ap|
620 d, sig = p.distance(ap)
621 str += sprintf("%d %f(%f)\n", ap.index, d, sig)
627 set_value("result#{n}", str)
630 set_value("group#{n}", "")
631 set_value("result#{n}", "")
634 set_proc = lambda { |it|
635 n = it[:tag][/\d/].to_i
638 str = sel.inspect.sub!("IntGroup[", "").sub!("]", "")
639 set_value("group#{n}", str)
640 plane_settings["group#{n}"] = sel
642 plane_settings["group#{n}"] = nil
644 refresh_proc.call(it)
646 text_proc = lambda { |it|
647 n = it[:tag][/\d/].to_i
648 str = it[:value].gsub(/[^-.,0-9]/, "") # Remove unsane characters
649 g = eval("IntGroup[#{str}]") rescue g = nil
650 plane_settings["group#{n}"] = g
651 refresh_proc.call(it)
654 item(:text, :title=>"Plane 1 (ax + by + cz + d = 0)"),
656 item(:text, :title=>"Atoms"),
657 item(:textfield, :width=>240, :height=>32, :tag=>"group1", :action=>text_proc),
658 item(:button, :title=>"Set Current Selection", :tag=>"button1", :action=>set_proc),
659 item(:text, :title=>"Results"),
660 item(:textview, :width=>240, :height=>68, :editable=>false, :tag=>"result1"),
661 item(:button, :title=>"Recalculate", :tag=>"refresh1", :action=>refresh_proc),
664 item(:text, :title=>"Plane 2 (a'x + b'y + c'z + d' = 0)"),
666 item(:text, :title=>"Atoms"),
667 item(:textfield, :width=>240, :height=>32, :tag=>"group2", :action=>text_proc),
668 item(:button, :title=>"Set Current Selection", :tag=>"button2", :action=>set_proc),
669 item(:text, :title=>"Results"),
670 item(:textview, :width=>240, :height=>68, :editable=>false, :tag=>"result2"),
671 item(:button, :title=>"Recalculate", :tag=>"refresh2", :action=>refresh_proc),
672 item(:text, :title=>"Dihedral angle with Plane 1"), -1, -1,
674 item(:textfield, :width=>240, :height=>16, :tag=>"dihedral"), -1,
677 item(:text, :title=>"Distance from Plane 1"), -1, -1,
678 item(:text, :title=>"Atoms"),
679 item(:textfield, :width=>240, :height=>32, :tag=>"group3", :action=>text_proc),
680 item(:button, :title=>"Set Current Selection", :tag=>"button3", :action=>set_proc),
681 item(:text, :title=>"Results"),
682 item(:textview, :width=>240, :height=>68, :editable=>false, :tag=>"result3"),
683 item(:button, :title=>"Recalculate", :tag=>"refresh3", :action=>refresh_proc)
685 refresh_proc.call(item_with_tag("refresh1"))
686 refresh_proc.call(item_with_tag("refresh2"))
687 refresh_proc.call(item_with_tag("refresh3"))
690 @plane_settings = plane_settings
693 # Calculate bond length and angles with standard deviations
694 # args can be a single IntGroup (calculate bonds and angles including those atoms)
695 # or arrays of 2 or 3 integers (explicitly specifying bonds and angles)
696 def bond_angle_with_sigma(*args)
698 if args.length >= 2 || (args.length == 1 && !args[0].is_a?(IntGroup))
699 # Bonds and angles are explicitly specified
703 if arg.length == 2 && arg[0].is_a?(Integer) && arg[1].is_a?(Integer)
705 elsif arg.length == 3 && arg[0].is_a?(Integer) && arg[1].is_a?(Integer) && arg[2].is_a?(Integer)
708 raise MolbyError, "Invalid argument #{arg.inspect}"
717 bonds = self.bonds.select { |b|
718 (g == nil || (g.include?(b[0]) && g.include?(b[1]))) &&
719 (atoms[b[0]].symop == nil || atoms[b[1]].symop == nil)
721 angles = self.angles.select { |ang|
722 (g == nil || (g.include?(ang[0]) && g.include?(ang[1]) && g.include?(ang[2]))) &&
723 (atoms[ang[0]].symop == nil || atoms[ang[1]].symop == nil || atoms[ang[2]].symop == nil)
727 # A list of interatomic distance, its differential coefficients, and other
729 # The hash tag is a list [i, j] (i and j are the atom indices) or [i, j, k]
730 # (i, j, k are the atom indices, and r(ijk) is the distance between the atom i
731 # and the center point between the atoms j and k).
732 # The value is a list of following quantities:
733 # index 0: rij or r(ijk)
734 # index 1-9: d(rij)/d(xij), d(rij)/d(yij), d(rij)/d(zij),
735 # d(rij)/da, d(rij)/db, d(rij)/dc, d(rij)/d(alpha), d(rij)/d(beta), d(rij)/d(gamma)
736 # index 10: the list of the "base atom"
737 # index 11: the list of the transform matrices
740 # A list of fractional coordinates and sigmas
744 fract.push(ap.fract_r)
748 # A list of base atoms (for symmetry-related atoms) and transform matrices
752 trans_i = Transform.identity
755 bases.push(sym ? sym[4] : ap.index)
757 tr = transform_for_symop(sym).transpose
758 tr[3, 0] = tr[3, 1] = tr[3, 2] = 0.0
763 symcode.push(sym ? sprintf("%d_%d%d%d", sym[0] + 1, sym[1] + 5, sym[2] + 5, sym[3] + 5) : ".")
766 # Unit cell parameter
769 cell.push(0.0, 0.0, 0.0, 0.0, 0.0, 0.0) # No sigma information
771 # $a, $b, $c, $alpha, $beta, $gamma, $sig_a, $sig_b, $sig_c, $sig_alpha, $sig_beta, $sig_gamma = self.cell
772 cos_a = cos(cell[3] * PI / 180.0)
773 cos_b = cos(cell[4] * PI / 180.0)
774 cos_c = cos(cell[5] * PI / 180.0)
775 abc = cell[0] * cell[1] * cos_c
776 bca = cell[1] * cell[2] * cos_a
777 cab = cell[2] * cell[0] * cos_b
778 aa = cell[0] * cell[0]
779 bb = cell[1] * cell[1]
780 cc = cell[2] * cell[2]
782 get_dlist = lambda { |_i, _j, _k|
787 _p = dlist[[_i, _j, _k]]
788 return _p if _p != nil
789 _p = (dlist[[_i, _j, _k]] = [])
790 _vij = fract[_i] - (fract[_j] + fract[_k]) * 0.5
791 _p[10] = [bases[_i], bases[_j], bases[_k]]
792 _p[11] = [trans[_i], trans[_j], trans[_k]]
798 return _p if _p != nil
799 _p = (dlist[[_i, _j]] = [])
800 _vij = fract[_i] - fract[_j]
801 _p[10] = [bases[_i], bases[_j]]
802 _p[11] = [trans[_i], trans[_j]]
807 _dij = sqrt_safe(aa * _xij * _xij + bb * _yij * _yij + cc * _zij * _zij + 2 * bca * _yij * _zij + 2 * cab * _zij * _xij + 2 * abc * _xij * _yij)
809 _p[1] = (aa * _xij + abc * _yij + cab * _zij) / _dij
810 _p[2] = (bb * _yij + bca * _zij + abc * _xij) / _dij
811 _p[3] = (cc * _zij + cab * _xij + bca * _yij) / _dij
812 _p[4] = (cell[0] * _xij * _xij + cell[2] * cos_b * _zij * _xij + cell[1] * cos_c * _xij * _yij) / _dij
813 _p[5] = (cell[1] * _yij * _yij + cell[0] * cos_c * _xij * _yij + cell[2] * cos_a * _yij * _zij) / _dij
814 _p[6] = (cell[2] * _zij * _zij + cell[1] * cos_a * _yij * _zij + cell[0] * cos_b * _zij * _xij) / _dij
815 _p[7] = (-cell[1] * cell[2] * sin(cell[3] * PI / 180.0) * _yij * _zij) * (PI / 180.0) / _dij
816 _p[8] = (-cell[2] * cell[0] * sin(cell[4] * PI / 180.0) * _zij * _xij) * (PI / 180.0) / _dij
817 _p[9] = (-cell[0] * cell[1] * sin(cell[5] * PI / 180.0) * _xij * _yij) * (PI / 180.0) / _dij
821 diff_by_rn = lambda { |_dl, _n, _ijk|
823 # return value: Vector3D[ d(rij)/d(xn), d(rij)/d(yn), d(rij)/d(zn) ]
824 # If ijk is true, then dl is dlist(i, j, k)
825 _dv = Vector3D[_dl[1], _dl[2], _dl[3]]
826 _c = Vector3D[0, 0, 0]
828 _c += _dl[11][0] * _dv
832 _c -= _dl[11][1] * _dv * 0.5
835 _c -= _dl[11][2] * _dv * 0.5
839 _c -= _dl[11][1] * _dv
845 notate_with_sigma = lambda { |_val, _sig|
847 return sprintf "%.4f", _val
849 _lg = log(_sig.abs / 1.95) / log(10.0)
851 _val2 = (_val * (10 ** _n) + 0.5).floor * (0.1 ** _n)
852 return sprintf "%.#{_n}f(%d)", _val2, (_sig * (10 ** _n) + 0.5).floor
857 c = Vector3D[0, 0, 0]
864 p = get_dlist.call(i, j, nil)
866 p[10].uniq.each { |k|
869 c = diff_by_rn.call(p, k, false)
875 apk.fract_r = r + Vector3D[0.00001, 0, 0]
876 amend_by_symmetry(p[10])
878 apk.fract_r = r + Vector3D[0, 0.00001, 0]
879 amend_by_symmetry(p[10])
881 apk.fract_r = r + Vector3D[0, 0, 0.00001]
882 amend_by_symmetry(p[10])
885 amend_by_symmetry(p[10])
886 printf " dr/dv[%d] cal %10.5g %10.5g %10.5g\n", k, c[0], c[1], c[2]
887 printf " dr/dv[%d] est %10.5g %10.5g %10.5g\n", k, (d1 - d0) / 0.00001, (d2 - d0) / 0.00001, (d3 - d0) / 0.00001
889 sig += c[0] * c[0] * s[0] * s[0] + c[1] * c[1] * s[1] * s[1] + c[2] * c[2] * s[2] * s[2]
892 sig += (p[4 + n] * cell[6 + n]) ** 2
895 results.push([[i, j], notate_with_sigma.call(p[0], sig), symcode[i], symcode[j], nil])
902 p0 = get_dlist.call(i, j, nil)
903 p1 = get_dlist.call(j, k, nil)
904 p2 = get_dlist.call(i, k, nil)
905 p3 = get_dlist.call(j, i, k)
906 t123 = acos_safe((p0[0] ** 2 + p1[0] ** 2 - p2[0] ** 2) / (2 * p0[0] * p1[0]))
907 t124 = acos_safe((p0[0] ** 2 + p3[0] ** 2 - p2[0] ** 2 * 0.25) / (2 * p0[0] * p3[0]))
908 t324 = acos_safe((p1[0] ** 2 + p3[0] ** 2 - p2[0] ** 2 * 0.25) / (2 * p1[0] * p3[0]))
909 dtdr12 = -(p0[0] ** 2 - p3[0] ** 2 + p2[0] ** 2 * 0.25) / (2 * p3[0] * (p0[0] ** 2) * sin(t124))
910 dtdr23 = -(p1[0] ** 2 - p3[0] ** 2 + p2[0] ** 2 * 0.25) / (2 * p3[0] * (p1[0] ** 2) * sin(t324))
911 dtdr13 = p2[0] / (sin(t124) * 4 * p0[0] * p3[0]) + p2[0] / (sin(t324) * 4 * p1[0] * p3[0])
912 dtdr24 = -(p3[0] ** 2 - p0[0] ** 2 + p2[0] ** 2 * 0.25) / (2 * (p3[0] ** 2) * p0[0] * sin(t124)) - (p3[0] ** 2 - p1[0] ** 2 + p2[0] ** 2 * 0.25) / (2 * (p3[0] ** 2) * p1[0] * sin(t324))
913 pp = (p0[10] + p1[10] + p2[10] + p3[10]).uniq
918 c = Vector3D[0, 0, 0]
919 c += diff_by_rn.call(p0, n, false) * (dtdr12 * 180.0 / PI)
920 c += diff_by_rn.call(p1, n, false) * (dtdr23 * 180.0 / PI)
921 c += diff_by_rn.call(p2, n, false) * (dtdr13 * 180.0 / PI)
922 c += diff_by_rn.call(p3, n, true) * (dtdr24 * 180.0 / PI)
923 sig += c[0] * c[0] * s[0] * s[0] + c[1] * c[1] * s[1] * s[1] + c[2] * c[2] * s[2] * s[2]
925 dd = dtdr12 * p0[4] + dtdr23 * p1[4] + dtdr13 * p2[4] + dtdr24 * p3[4]
926 sig += dd * dd * cell[6] * cell[6]
927 dd = dtdr12 * p0[5] + dtdr23 * p1[5] + dtdr13 * p2[5] + dtdr24 * p3[5]
928 sig += dd * dd * cell[7] * cell[7]
929 dd = dtdr12 * p0[6] + dtdr23 * p1[6] + dtdr13 * p2[6] + dtdr24 * p3[6]
930 sig += dd * dd * cell[8] * cell[8]
931 dd = dtdr12 * p0[7] + dtdr23 * p1[7] + dtdr13 * p2[7] + dtdr24 * p3[7]
932 sig += dd * dd * cell[9] * cell[9]
933 dd = dtdr12 * p0[8] + dtdr23 * p1[8] + dtdr13 * p2[8] + dtdr24 * p3[8]
934 sig += dd * dd * cell[10] * cell[10]
935 dd = dtdr12 * p0[9] + dtdr23 * p1[9] + dtdr13 * p2[9] + dtdr24 * p3[9]
936 sig += dd * dd * cell[11] * cell[11]
938 results.push([[i, j, k], notate_with_sigma.call(t123*180/PI, sig), symcode[i], symcode[j], symcode[k]])
943 def cmd_bond_angle_with_sigma
945 Dialog.new("Bond & Angle with Sigma:" + self.name, nil, nil) {
949 on_get_value = lambda { |it, row, col| values[row][col + 2] }
950 atom_name = lambda { |ap|
951 (ap.molecule.nresidues >= 2 ? "#{ap.res_seq}:" : "") + ap.name
954 item(:table, :width=>480, :height=>300, :tag=>"table",
955 :columns=>[["Bond/Angle", 140], "value(sigma)", "symop1", "symop2", "symop3"],
956 :on_count=> lambda { |it| values.count },
957 :on_get_value=> on_get_value),
959 item(:view, :width=>480, :height=>1), -1,
960 item(:button, :title=>"Export to Clipboard", :tag=>"dump",
961 :action=>lambda { |item|
962 s = ""; values.each { |val| s += val[2..-1].join(" ") + "\n" }; export_to_clipboard(s)
965 [item(:button, :title=>"Close", :action=>lambda { |item| hide }), {:align=>:right}])
967 on_document_modified = lambda { |*d|
968 puts "on_document_modified called: newsel = #{mol.selection}, sel = #{sel}"
969 newsel = mol.selection
976 elsif n1 == 1 && !clicked.include?(newsel[0])
979 clicked[0] = newsel[0]
980 val1 = [[clicked[0]], "---", "", "", ""]
981 elsif n1 == 2 && clicked.length == 1
983 if newsel[0] == clicked[0]
984 clicked[1] = newsel[1]
985 elsif newsel[1] == clicked[0]
986 clicked[1] = newsel[0]
988 clicked[0] = newsel[0]
989 clicked[1] = newsel[1]
991 val1 = mol.bond_angle_with_sigma(clicked)[0]
992 elsif n1 == 3 && clicked.length == 2 && newsel.include?(clicked[0]) && newsel.include?(clicked[1])
994 clicked[2] = (newsel - clicked)[0]
995 val1 = mol.bond_angle_with_sigma(clicked[1..2])[0]
996 val2 = mol.bond_angle_with_sigma(clicked)[0]
1000 [val1, val2].each { |val|
1005 label += atom_name.call(mol.atoms[val[0][i]])
1007 label += (mol.bond_exist?(val[0][i], val[0][i + 1]) ? "-" : "...")
1011 val.unshift(n) # val = [count, indices, label, value(sigma), symop1, symop2, symop3]
1013 if values[-1] && values[-1][0] == 1
1016 values.push(val1) if val1
1017 values.push(val2) if val2
1019 item_with_tag("table")[:refresh] = true
1020 item_with_tag("dump")[:enabled] = (values.length > 0)
1023 listen(mol, "documentModified", on_document_modified)
1024 listen(mol, "documentWillClose", lambda { |*d| hide } )
1025 on_document_modified.call
1030 def find_expanded_atom(base, symop)
1031 return base unless symop
1032 symopb = symop + [base]
1033 self.each_atom { |ap|
1034 if ap.symop == symopb
1041 def complete_by_symmetry
1043 raise "Unit cell should be given"
1046 avec, bvec, cvec = self.box
1048 self.nsymmetries.times { |n|
1049 syms.push(transform_for_symop([n, 0, 0, 0], true))
1052 self.each_fragment { |f|
1056 self.each_atom { |ap|
1057 # Find if ap is contained in expansion
1058 next if ap.symop != nil # Already expanded
1059 rad = Parameter.builtin.elements[ap.atomic_number].radius
1060 syms.each_with_index { |sym, sym_idx|
1063 dy = (n / 3) % 3 - 1
1064 dz = (n / 9) % 3 - 1
1065 next if dx == 0 && dy == 0 && dz == 0 && sym_idx == 0
1066 symop = [sym_idx, dx, dy, dz]
1067 next if self.find_expanded_atom(ap.index, symop)
1068 r = sym * ap.r + avec * dx + bvec * dy + cvec * dz
1069 close_atoms = self.find_close_atoms(r, 1.2, rad)
1070 if close_atoms.length > 0
1071 # ap * [sym, dx, dy, dz] is included in the expansion
1072 close_atoms.each { |ca|
1073 next if ca > ap.index
1074 i1 = frags.index { |f| f.include?(ca) }
1075 i2 = frags.index { |f| f.include?(ap.index) }
1076 pp = close_pairs.find { |p1| p1[0] == i1 && p1[1] == i2 && p1[2] == symop }
1078 pp = [i1, i2, symop, [], [], [], []]
1079 close_pairs.push(pp)
1081 if (r - atoms[ca].r).length2 > 0.0001
1082 # Normal case (bond between ca and ap)
1084 pp[4].push(ap.index)
1088 pp[6].push(ap.index)
1095 puts "close_pairs = #{close_pairs}" if verbose
1096 expand_pairs = lambda { |i1, symop, depth|
1097 # Find expanded fragment that is close to fragment [i1, symop]
1098 next [] if depth > 16
1100 close_pairs.each { |pp|
1101 next if i1 != nil && pp[0] != i1
1102 next if pp[3].count == 0 # No real expansion
1103 # Multiply two symops
1105 symop2 = symop_for_transform(transform_for_symop(pp[2]) * transform_for_symop(symop))
1106 puts "multiply two symops: #{pp[2].inspect} * #{symop.inspect} -> #{symop2.inspect}" if verbose
1112 if symop2[1] == 0 && symop2[2] == 0 && symop2[3] == 0
1113 # No expansion, but pp[3] and pp[4] are still needed to make bonds
1116 # Expand the atoms only at the marginal
1117 retval.push([nil, symop, symop2, pp[3], pp[4], pp[5], pp[6]])
1120 retval.push([pp[1], symop, symop2, pp[3], pp[4], pp[5], pp[6]])
1121 retval.concat(expand_pairs.call(pp[1], symop2, depth + 1))
1126 ex_pairs = expand_pairs.call(nil, nil, 0)
1127 puts "ex_pairs = #{ex_pairs}" if verbose
1131 ex_pairs.each { |ex|
1132 # ex[0]: fragment to expand, ex[1], ex[2]: symop,
1133 # ex[3], ex[4]: atoms to make bonds, ex[5], ex[6]: duplicate atoms
1141 self.expand_by_symmetry(f, ex[2][0], ex[2][1], ex[2][2], ex[2][3], true)
1142 puts "expand(#{f}, #{ex[2]}) -> atoms[#{n1}..#{self.natoms - n1}]" if verbose
1144 ex[3].each_with_index { |x, i|
1145 x1 = self.find_expanded_atom(x, ex[1])
1147 x2 = f.index(ex[4][i]) + n1 # New index of the expanded atom
1149 x2 = ex[4][i] # Base atom
1152 puts "create_bond(#{x1}, #{x2})" if verbose
1154 # Register duplicate atoms
1156 ex[5].each_with_index { |x, i|
1157 x1 = self.find_expanded_atom(x, ex[1])
1158 x2 = f.index(ex[6][i]) + n1
1159 duplicates.each { |d|
1167 duplicates.push([x1, x2])
1172 puts "duplicates = #{duplicates}" if verbose
1174 # Remove duplicate atoms
1176 duplicates.each { |d|
1177 d.each_with_index { |dn, i|
1180 self.atoms[dn].connects.each { |nn|
1181 create_bond(d[0], nn)
1182 puts "create_bond(#{d[0]}, #{nn})" if verbose
1188 puts "remove(#{rem})" if verbose
1192 def create_packing_diagram
1194 error_message_box "Unit cell is not defined."
1197 expansion_box = (@expansion_box ||= [0.0, 1.0, 0.0, 1.0, 0.0, 1.0])
1198 h = Dialog.run("Create Packing Diagram") {
1200 item(:text, :title=>"Specify the expansion limits for each axis:"),
1202 item(:text, :title=>"x"),
1203 item(:textfield, :width=>80, :tag=>"xmin", :value=>sprintf("%.1f", expansion_box[0].to_f)),
1204 item(:text, :title=>"..."),
1205 item(:textfield, :width=>80, :tag=>"xmax", :value=>sprintf("%.1f", expansion_box[1].to_f)),
1206 item(:text, :title=>"y"),
1207 item(:textfield, :width=>80, :tag=>"ymin", :value=>sprintf("%.1f", expansion_box[2].to_f)),
1208 item(:text, :title=>"..."),
1209 item(:textfield, :width=>80, :tag=>"ymax", :value=>sprintf("%.1f", expansion_box[3].to_f)),
1210 item(:text, :title=>"z"),
1211 item(:textfield, :width=>80, :tag=>"zmin", :value=>sprintf("%.1f", expansion_box[4].to_f)),
1212 item(:text, :title=>"..."),
1213 item(:textfield, :width=>80, :tag=>"zmax", :value=>sprintf("%.1f", expansion_box[5].to_f)))
1216 @expansion_box[0] = h["xmin"].to_f
1217 @expansion_box[1] = h["xmax"].to_f
1218 @expansion_box[2] = h["ymin"].to_f
1219 @expansion_box[3] = h["ymax"].to_f
1220 @expansion_box[4] = h["zmin"].to_f
1221 @expansion_box[5] = h["zmax"].to_f
1226 # Enumerate all atoms within the expansion box
1227 syms = self.symmetries
1229 xmin = @expansion_box[0]
1230 xmax = @expansion_box[1]
1231 ymin = @expansion_box[2]
1232 ymax = @expansion_box[3]
1233 zmin = @expansion_box[4]
1234 zmax = @expansion_box[5]
1236 xmax_d = xmax.floor - 1
1238 ymax_d = ymax.floor - 1
1240 zmax_d = zmax.floor - 1
1241 syms.each_with_index { |sym, i|
1243 fr = sym * ap.fract_r
1250 symopi = i * 1000000 + (50 - dx) * 10000 + (50 - dy) * 100 + 50 - dz
1251 (h[symopi] ||= []).push(ap.index)
1252 xmin_d.upto(xmax_d) { |xd|
1253 next if fr.x + xd < xmin || fr.x + xd > xmax
1254 ymin_d.upto(ymax_d) { |yd|
1255 next if fr.y + yd < ymin || fr.y + yd > ymax
1256 zmin_d.upto(zmax_d) { |zd|
1257 next if fr.z + zd < zmin || fr.z + zd > zmax
1258 next if xd == 0 && yd == 0 && zd == 0
1259 symopid = symopi + xd * 10000 + yd * 100 + zd
1260 (h[symopid] ||= []).push(ap.index)
1266 h.keys.sort.each { |key|
1268 dx = key % 1000000 / 10000 - 50
1269 dy = key % 10000 / 100 - 50
1271 next if sym == 0 && dx == 0 && dy == 0 && dz == 0
1272 # puts "[#{sym},#{dx},#{dy},#{dz}]: #{h[key].inspect}"
1273 expand_by_symmetry(IntGroup[h[key]], sym, dx, dy, dz)
1279 tmp = create_temp_dir("ortep", mol.name)
1280 tepexe = "#{ResourcePath}/ortep3/ortep3"
1281 if $platform == "win"
1285 tepbounds = [0, 0, 400, 400]
1288 ["all", "Shaded", "black", ""],
1289 ["Li-Ar", "Principals", "black", ""],
1290 ["C", "Boundary", "black", ""],
1291 ["H", "Fixed", "black", "0.1"]
1294 ["all", "all", "4 Shades", "black"],
1295 ["Li-Ar", "Li-Ar", "3 Shades", "black"],
1296 ["C", "Li-Ar", "2 Shades", "black"],
1297 ["H", "all", "Open", "black"]
1300 Dialog.new("Show ORTEP:" + mol.name, nil, nil, :resizable=>true, :has_close_box=>true) {
1301 tepview = nil # Forward declaration
1304 "Atoms"=>[["atom list", 65], ["type", 80], ["color", 40], ["radius", 50]],
1305 "Bonds"=>[["list1", 65], ["list2", 65], ["type", 80], ["color", 40]]
1307 atom_types = ["Boundary", "Principals", "Axes", "Shaded", "Fixed"]
1308 bond_types = ["Open", "1 Shade", "2 Shades", "3 Shades", "4 Shades"]
1309 colors = ["black", "red", "green", "blue", "cyan", "magenta", "yellow"]
1311 get_ortep_attr = lambda {
1313 make_atom_list = lambda { |s, ln|
1314 ss = s.scan(/[-.0-9A-Za-z]+/)
1316 ss.inject(IntGroup[]) { |r, it|
1320 elsif it =~ /-|(\.\.)/
1321 s0 = Regexp.last_match.pre_match
1322 s1 = Regexp.last_match.post_match
1329 if s1 !~ /^[0-9]+$/ || (s1 = s1.to_i) < s0
1330 raise "Bad atom list specification: #{s} in line #{ln}"
1335 s0 = Parameter.builtin.elements.find_index { |p| p.name == s0.capitalize }
1336 s1 = Parameter.builtin.elements.find_index { |p| p.name == s1.capitalize }
1337 if s0 == nil || s1 == nil
1338 raise "Bad element symbol specification: #{s} in line #{ln}"
1340 (s0..s1).each { |an|
1341 r.add(mol.atom_group { |ap| ap.atomic_number == an } )
1349 descs["Atoms"].each_with_index { |d, idx|
1350 list = make_atom_list.call(d[0], idx)
1351 type = atom_types.find_index { |it| it == d[1] } || 0
1352 col = (colors.find_index { |it| it == d[2] } || 0) + 1
1354 at.push([list, type, col, rad])
1359 descs["Bonds"].each_with_index { |d, idx|
1360 list1 = make_atom_list.call(d[0], idx)
1361 list2 = make_atom_list.call(d[1], idx)
1362 type = bond_types.find_index { |it| it == d[2] } || 0
1363 col = (colors.find_index { |it| it == d[3] } || 0) + 1
1364 bd.push([list1, list2, type, col])
1369 on_update_ortep = lambda { |it|
1371 attr = get_ortep_attr.call
1372 # Create ORTEP input in the temporary directory
1373 open(tmp + "/TEP.IN", "w") { |fp| mol.export_ortep(fp, attr) }
1377 if FileTest.exist?("TEP001.PRN")
1378 File.unlink("TEP001.PRN")
1380 pid = call_subprocess(tepexe, "Running ORTEP", nil, "NUL", "NUL")
1381 if FileTest.exist?("TEP001.PRN")
1382 File.rename("TEP001.PRN", "TEP001.ps")
1386 msg = "ORTEP execution in #{tmp} failed with status #{pid}."
1387 message_box(msg, "ORTEP Failed", :ok, :warning)
1389 open(tmp + "/TEP001.ps", "r") { |fp|
1391 tepbounds = [100000, 100000, -100000, -100000]
1395 if ln =~ /setrgbcolor/
1396 tepdata.push(["color", x.to_f, y.to_f, c.to_f])
1398 elsif ln =~ /setgray/
1400 tepdata.push(["color", x, x, x])
1406 tepdata.push([x, y])
1408 tepdata[-1].push(x, y)
1412 tepbounds[0] = x if x < tepbounds[0]
1413 tepbounds[1] = y if y < tepbounds[1]
1414 tepbounds[2] = x if x > tepbounds[2]
1415 tepbounds[3] = y if y > tepbounds[3]
1417 # [x, y, width, height]
1418 tepbounds[2] -= tepbounds[0]
1419 tepbounds[3] -= tepbounds[1]
1421 tepview.refresh_rect(tepview[:frame])
1424 on_draw_ortep = lambda { |it|
1427 brush(:color=>[1, 1, 1, 1])
1428 pen(:color=>[1, 1, 1, 1])
1429 draw_rectangle(frame)
1430 pen(:color=>[0, 0, 0, 1])
1431 rx = (frame[2] - 10.0) / tepbounds[2]
1432 ry = (frame[3] - 10.0) / tepbounds[3]
1434 dx = (frame[2] - tepbounds[2] * rx) * 0.5
1435 dy = (frame[3] + tepbounds[3] * rx) * 0.5
1438 pen(:color=>[d[1], d[2], d[3], 1])
1441 x0 = (dx + (d[0] - tepbounds[0]) * rx).to_i
1442 y0 = (dy - (d[1] - tepbounds[1]) * rx).to_i
1443 (d.length / 2 - 1).times { |i|
1444 x1 = (dx + (d[i * 2 + 2] - tepbounds[0]) * rx).to_i
1445 y1 = (dy - (d[i * 2 + 3] - tepbounds[1]) * rx).to_i
1446 draw_line(x0, y0, x1, y1)
1452 on_export_eps = lambda { |fname|
1453 frame = item_with_tag("ortep")[:frame].dup
1455 rx = (frame[2] - dx * 2) / tepbounds[2]
1456 ry = (frame[3] - dy * 2) / tepbounds[3]
1458 frame[2] = (rx * tepbounds[2] + dx * 2).to_i
1459 frame[3] = (rx * tepbounds[3] + dy * 2).to_i
1460 # Assumes A4 paper and center the bounding box
1461 frame[0] = (595 - frame[2]) / 2
1462 frame[1] = (842 - frame[3]) / 2
1463 xmax = frame[0] + frame[2]
1464 ymax = frame[1] + frame[3]
1465 open(fname, "w") { |fp|
1466 fp.print "%!PS-Adobe-3.0 EPSF-3.0
1468 %%BoundingBox: #{frame[0]} #{frame[1]} #{xmax} #{ymax}
1470 %%Orientation: Landscape
1477 0 setgray 1 setlinecap 1 setlinewidth
1482 fp.printf "%.3f %.3f %.3f setrgbcolor\n", d[1], d[2], d[3]
1485 x0 = frame[0] + dx + (d[0] - tepbounds[0]) * rx
1486 y0 = frame[1] + dy + (d[1] - tepbounds[1]) * rx
1487 fp.printf "%.2f %.2f m\n", x0, y0
1488 (d.length / 2 - 1).times { |i|
1489 x1 = frame[0] + dx + (d[i * 2 + 2] - tepbounds[0]) * rx
1490 y1 = frame[1] + dy + (d[i * 2 + 3] - tepbounds[1]) * rx
1491 fp.printf "%.2f %.2f l\n", x1, y1
1497 fp.print "showpage\n%%Trailer\n%%EOF\n"
1500 on_export_bitmap = lambda { |fname|
1501 frame = item_with_tag("ortep")[:frame].dup
1504 rx = (frame[2] - dx * 2) * scale / tepbounds[2]
1505 ry = (frame[3] - dy * 2) * scale / tepbounds[3]
1507 frame[2] = (rx * tepbounds[2] + dx * 2).to_i
1508 frame[3] = (rx * tepbounds[3] + dy * 2).to_i
1509 bmp = Bitmap.new(frame[2], frame[3], 32)
1512 pen(:color=>[0, 0, 0, 1], :width=>scale)
1515 pen(:color=>[d[1], d[2], d[3], 1])
1518 x0 = (dx + (d[0] - tepbounds[0]) * rx).to_i
1519 y0 = (dy + (tepbounds[3] - d[1] + tepbounds[1]) * rx).to_i
1520 (d.length / 2 - 1).times { |i|
1521 x1 = (dx + (d[i * 2 + 2] - tepbounds[0]) * rx).to_i
1522 y1 = (dy + (tepbounds[3] - d[i * 2 + 3] + tepbounds[1]) * rx).to_i
1523 draw_line(x0, y0, x1, y1)
1529 bmp.save_to_file(fname)
1531 on_export = lambda { |it|
1532 basename = (mol.path ? File.basename(mol.path, ".*") : mol.name)
1533 fname = Dialog.save_panel("Export ORTEP file:", mol.dir, basename + ".eps",
1534 "Encapsulated PostScript (*.eps)|*.eps|PNG (*.png)|*.png|TIFF (*.tif)|*.tif|ORTEP Instruction (*.tep)|*.tep|All files|*.*")
1536 ext = File.extname(fname).downcase
1538 on_export_eps.call(fname)
1539 elsif ext == ".png" || ext == ".tif" || ext == ".tiff"
1540 on_export_bitmap.call(fname)
1542 filecopy("#{tmp}/TEP.IN", fname)
1545 on_document_modified = lambda { |m|
1547 # Close handler (called when the close box is pressed or the document is closed)
1548 @on_close = lambda { |*d|
1549 cleanup_temp_dir(tmp)
1553 on_select_tab = lambda { |it|
1554 tab = ["Atoms", "Bonds"][it[:value]]
1556 table = item_with_tag("table")
1557 table[:columns] = columns[tab]
1558 table[:selection] = IntGroup[]
1559 table[:refresh] = true
1561 get_count = lambda { |it| descs[tab].count }
1562 get_value = lambda { |it, row, col| descs[tab][row][col] }
1563 has_popup_menu = lambda { |it, row, col|
1570 elsif tab == "Bonds"
1579 on_selection_changed = lambda { |it|
1580 sel = it[:selection]
1581 item_with_tag("remove")[:enabled] = (sel == nil || sel.count == 0 ? false : true)
1583 is_item_editable = lambda { |it, row, col|
1584 return has_popup_menu.call(it, row, col) == nil
1586 popup_menu_selected = lambda { |it, row, col, sel|
1587 titles = has_popup_menu.call(it, row, col)
1588 return nil if titles == nil
1589 descs[tab][row][col] = titles[sel]
1592 set_value = lambda { |it, row, col, val|
1593 descs[tab][row][col] = val
1596 add_to_table = lambda { |it|
1597 table = item_with_tag("table")
1598 d = descs[tab][-1].dup
1600 table[:refresh] = true
1601 table[:selection] = IntGroup[descs[tab].count - 1]
1602 puts descs[tab].inspect
1604 remove_from_table = lambda { |it|
1605 table = item_with_tag("table")
1606 sel = table[:selection]
1607 sel.reverse_each { |n|
1608 descs[tab][n, 1] = []
1610 table[:refresh] = true
1614 item(:popup, :subitems=>["Atoms", "Bonds"], :action=>on_select_tab),
1615 item(:table, :width=>240, :height=>380, :flex=>[0,0,0,0,1,1], :tag=>"table",
1616 :columns=>columns["Atoms"],
1617 :on_count=>get_count,
1618 :on_get_value=>get_value,
1619 :on_set_value=>set_value,
1620 :on_selection_changed=>on_selection_changed,
1621 :is_item_editable=>is_item_editable,
1622 :has_popup_menu=>has_popup_menu,
1623 :on_popup_menu_selected=>popup_menu_selected),
1625 item(:button, :title=>"Add", :tag=>"add", :action=>add_to_table),
1626 item(:button, :title=>"Remove", :tag=>"remove", :action=>remove_from_table, :enabled=>false),
1627 :flex=>[0,1,0,0,0,0]),
1628 :flex=>[0,0,0,0,0,1]),
1630 item(:view, :width=>400, :height=>400, :tag=>"ortep", :on_paint=>on_draw_ortep, :flex=>[0,0,0,0,1,1]),
1632 item(:button, :title=>"Refresh", :action=>on_update_ortep, :align=>:left),
1633 item(:button, :title=>"Export as...", :action=>on_export, :align=>:right),
1634 :flex=>[0,1,0,0,0,0]),
1635 :flex=>[0,0,0,0,1,1]),
1636 :flex=>[0,0,0,0,1,1]
1638 set_min_size(480, 200)
1639 tepview = item_with_tag("ortep")
1640 listen(mol, "documentModified", on_document_modified)
1641 listen(mol, "documentWillClose", lambda { |m| close } )
1642 on_document_modified.call(nil)
1643 on_update_ortep.call(nil)
1648 if lookup_menu("Best-fit Planes...") < 0
1649 register_menu("", "")
1650 register_menu("Best-fit Planes...", :cmd_plane)
1651 register_menu("Bonds and Angles with Sigma...", :cmd_bond_angle_with_sigma)
1652 register_menu("Complete by Symmetry", :complete_by_symmetry)
1653 register_menu("Create Packing Diagram...", :create_packing_diagram)
1654 register_menu("Show ORTEP", :cmd_show_ortep)