5 # Created by Toshi Nagata.
6 # Copyright 2012 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.
17 # Definition for use in ORTEP
26 symcode = (sym[1] + 5) * 10000 + (sym[2] + 5) * 1000 + (sym[3] + 5) * 100 + sym[0] + 1
34 def symmetry_to_string(sym)
35 # Sym is a Transform object defined in crystallographic coordinates
47 if (a - (n = (a + 0.5).floor)).abs < 1e-4
50 elsif ((a * 2) - (n = (a * 2 + 0.5).floor)).abs < 1e-4
53 elsif ((a * 3) - (n = (a * 3 + 0.5).floor)).abs < 1e-4
56 elsif ((a * 4) - (n = (a * 4 + 0.5).floor)).abs < 1e-4
59 elsif ((a * 6) - (n = (a * 6 + 0.5).floor)).abs < 1e-4
63 s += sprintf("%.4f", a)
66 ["x", "y", "z"].each_with_index { |t, i|
70 elsif (a - 1.0).abs < 1e-4
71 s += (s == "" ? t : "+" + t)
72 elsif (a + 1.0).abs < 1e-4
80 s += sprintf("%.4f", a.abs) + t
91 def string_to_symmetry(str)
93 sary = str.split(/, */)
94 raise if sary.count != 3
96 sary.each_with_index { |s, j|
101 raise if s !~ /^([-+][.0-9\/]*)([xyzXYZ]?)/
102 sa = Regexp.last_match[1]
103 st = Regexp.last_match[2]
104 s = Regexp.last_match.post_match
115 raise if a[i * 3 + j] != nil
121 sa0, sa1 = sa.split("/")
122 aa = sa0.to_f / sa1.to_f
132 return Transform.new(a)
134 raise "Cannot convert to symmetry operation: #{str}"
142 # Export ortep to File fp
143 # If attr is a hash, it represents options for drawing.
144 # "atoms"=>[[group, type, color, rad], [group, type, color, rad], ...]
145 # group is an IntGroup, representing a group of atoms.
146 # type is an atom type: 0 = boundary, 1 = boundary + principal, 2 = 1 + axes, 3 = 2 + shades, 4 = fixed size
147 # color is 0..7 (0,1=black, 2=red, 3=green, 4=blue, 5=cyan, 6=magenta, 7=yellow)
148 # rad is the radius of the atom (in Angstrom) whose type is fixed size
149 # If an atom appears in multiple entries, the later entry is valid.
150 # "bonds"=>[[group1, group2, type, color], [group1, group2, type, color], ...]
151 # group1 and group2 are IntGroups, reprensenting the atoms constituting the bonds.
152 # type is a bond type: 0 to 4 for bonds with no shades to 4 shades.
153 # color is 0..7 as in atoms.
154 # If a bond appears in multiple entries, the later entry is valid.
155 def export_ortep(fp, attr = nil)
158 hidden = atom_group { |ap| !is_atom_visible(ap.index) }
159 hydrogen = self.show_hydrogens
160 expanded = self.show_expanded
161 atomlist = atom_group { |ap|
162 (ap.element != "H" || hydrogen) &&
163 (ap.symop == nil || expanded) &&
164 (!hidden.include?(ap.index))
168 fp.printf "%-78.78s\n", self.name + ": generated by Molby at " + Time.now.to_s
173 cp = [1, 1, 1, 90, 90, 90]
175 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]
177 # Symmetry operations
178 syms = self.symmetries
179 if syms == nil || syms.length == 0
180 fp.print "1 0 1 0 0 0 0 1 0 0 0 0 1\n"
182 syms.each_with_index { |s, i|
184 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]
188 # Atoms (all symmetry unique atoms regardless they are visible or not)
190 aattr = (attr ? attr["atoms"] : nil)
192 break if ap.symop != nil
193 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
196 aattr.reverse_each { |at|
197 if at[0].include?(ap.index)
210 eigval = ap.aniso_eigenvalues
211 if eigval && (eigval[0] < 0.0 || eigval[1] < 0.0 || eigval[2] < 0.0)
212 # Non positive-definite anisotropic factor: fallback to isotropic
216 if an != nil && rad < 0.0
217 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
222 rad = 1.2 if rad <= 0
225 fp.printf " %8.3f%9g%9g%9g%9g%9g%9d\n", rad, 0.0, 0.0, 0.0, 0.0, 0.0, type
231 # Special points to specify cartesian axes
232 axis, angle = self.get_view_rotation
233 tr = Transform.rotation(axis, -angle)
234 org = self.get_view_center
235 x = org + tr.column(0)
236 y = org + tr.column(1)
237 tr = self.cell_transform
241 tr = Transform.identity
246 fp.printf " CNTR %9.4f%9.4f%9.4f 0\n", org.x, org.y, org.z
247 fp.printf " %8.3f%9g%9g%9g%9g%9g%9d\n", 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 6
248 fp.printf " X %9.4f%9.4f%9.4f 0\n", x.x, x.y, x.z
249 fp.printf " %8.3f%9g%9g%9g%9g%9g%9d\n", 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 6
250 fp.printf " Y %9.4f%9.4f%9.4f 0\n", y.x, y.y, y.z
251 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
259 # Paper size, margin, viewing distance
260 fp.print " 301 6.6 6.6 0 0.0\n"
262 # Sort atoms by symop and index
264 ig = IntGroup.new # Used for collecting contiguous atoms
265 each_atom(atomlist) { |ap|
266 idx, symcode = ap.to_adc
267 acode = symcode * self.natoms + idx - 1
268 acodes[acode] = ap.index
271 index2an = [] # Index in Molecule to Index in ORTEP
272 ig.each_with_index { |acode, i|
273 index2an[acodes[acode]] = i + 1
277 while (r = ig.range_at(i)) != nil
279 s = (s / self.natoms) + (s % self.natoms + 1) * 100000 # Rebuild true ADC (atom index is in the upper digit)
281 e = (e / self.natoms) + (e % self.natoms + 1) * 100000
293 adcs.each_with_index { |a, i|
299 if i == adcs.length - 1 || k == 6 || (k == 5 && i < adcs.length - 2 && adcs[i + 2] < 0)
306 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
307 # fp.print " 502 1 0.0 2 0.0 3 0.0\n"
310 fp.print " 604 1.538\n"
313 bond_inst = Array.new(35) { [] } # Bonds for types 0 to 4 and colors 1 to 7
314 battr = (attr ? attr["bonds"] : nil)
316 next if !atomlist.include?(b[0]) || !atomlist.include?(b[1])
319 battr.reverse_each { |at|
320 if (at[0].include?(b[0]) && at[1].include?(b[1])) || (at[0].include?(b[1]) && at[1].include?(b[0]))
322 bcol = (at[3] != 0 ? at[3] - 1 : at[3]) % 7
329 an1 = atoms[b[0]].atomic_number
330 an2 = atoms[b[1]].atomic_number
331 if an1 == 1 || an2 == 1
333 elsif an1 <= 8 && an2 <= 8
339 bond_inst[btype * 7 + bcol].push(b[0], b[1])
342 # Output bond specifications
343 # Avoid including too many ADCs in a single 811/821 instruction
344 # (Upper limit is 140. Here we divide at every 36 ADCs)
345 output_bonds = lambda { |icode|
346 bond_inst.each_with_index { |inst, ii|
347 next if inst.length == 0
349 if icode / 10 == 81 # 811 instructions
350 fp.printf " 204%9d\n", ii % 7 + 1 # Pen color
352 inst.each_with_index { |b, i|
354 fp.printf " %d %3s", (i >= inst.length - 6 || i % 36 == 30 ? 2 : 1), (i % 36 == 0 ? icode.to_s : "")
356 idx, scode = atoms[b].to_adc
357 fp.printf "%9d", idx * 100000 + scode
358 if i % 6 == 5 || i == inst.length - 1
360 if i == inst.length - 1 || i % 36 == 35
361 fp.printf "%21s%3d%12s%6.3f\n", "", btype, "", 0.05
368 fp.print " 0 1001 0.02\n" # Activate hidden line removal
369 output_bonds.call(821)
372 # Atom type 0=714 (boundary), 1=712 (+principal), 2=716 (+axes), 3=711 (+shades)
373 atom_inst = Array.new(28) { IntGroup.new }
374 aattr = (attr ? attr["atoms"] : nil)
378 aattr.reverse_each { |at|
381 acol = (at[2] != 0 ? at[2] - 1 : at[2]) % 7
388 an1 = atoms[i].atomic_number
397 idx, scode = atoms[i].to_adc
398 atom_inst[atype * 7 + acol].add(idx)
400 (atom_inst.count - 1).downto(0) { |ii|
402 next if inst.count == 0
403 fp.printf " 204%9d\n", ii % 7 + 1 # Pen color
405 atype = [714, 712, 716, 711][ii / 7]
406 while (r = inst.range_at(i)) != nil
407 fp.printf " 1 %3d\n", atype
408 fp.printf "%27s%9d%9d\n", "", r.first, r.last
413 output_bonds.call(811)
421 def savetep(filename)
423 raise MolbyError, "cannot save ORTEP input; the molecule is empty"
425 fp = open(filename, "wb")
434 # Ref. W. C. Hamilton, Acta Cryst. 1961, 14, 185-189
435 # T. Ito, Acta Cryst 1981, A37, 621-624
437 # An object to describe the best-fit plane
439 # Contains the plane coefficients and the constant (a, b, c, d for ax + by + cz + d = 0),
440 # the error matrix, and the metric tensor.
443 attr_accessor :molecule, :group, :coeff, :const, :error_matrix, :metric_tensor
444 def initialize(mol, group, coeff, const, err, met)
454 [@coeff.x, @coeff.x, @coeff.z, @const]
457 [sqrt_safe(@error_matrix[0, 0]), sqrt_safe(@error_matrix[1, 1]), sqrt_safe(@error_matrix[2, 2]), sqrt_safe(@error_matrix[3, 3])]
460 s = sprintf("Molby::Plane[\n coeff, const = [[%f, %f, %f], %f],\n", @coeff.x, @coeff.y, @coeff.z, @const)
461 s += sprintf(" sigma = [[%10.4e, %10.4e, %10.4e], %10.4e],\n", *self.sigma)
463 s += (i == 0 ? " error_matrix = [" : " ")
465 s += sprintf("%12.6e%s", @error_matrix[j, i], (j == i ? (i == 3 ? "],\n" : ",\n") : ","))
468 s += sprintf(" molecule = %s\n", @molecule.inspect)
469 s += sprintf(" group = %s\n", @group.inspect)
471 s += (i == 0 ? " metric_tensor = [" : " ")
473 s += sprintf("%12.6e%s", @metric_tensor[j, i], (j == 3 ? (i == 3 ? "]]\n" : ",\n") : ","))
484 sig = Vector3D[0, 0, 0]
486 d = fr.dot(@coeff) + @const
487 sig1 = (@coeff.x * sig.x) ** 2 + (@coeff.y * sig.y) ** 2 + (@coeff.z * sig.z) ** 2
488 sig2 = LAMatrix.multiply("t", fr, @error_matrix, fr)[0, 0]
489 if ap.is_a?(AtomRef) && ap.molecule == @molecule && @group.include?(ap.index)
490 # The atom defines the plane
492 sig0 = 0.0 if sig0 < 0.0
496 return d, sqrt_safe(sig0)
499 e1 = @error_matrix.submatrix(0, 0, 3, 3)
500 e2 = plane.error_matrix.submatrix(0, 0, 3, 3)
501 m = @metric_tensor.submatrix(0, 0, 3, 3)
503 cos_t = plane.coeff.dot(m * @coeff)
510 sig_t = (m * e1).trace + (m * e2).trace
513 sig_t = w * w * (c.dot(LAMatrix.multiply(m, e1, m, c)) + @coeff.dot(LAMatrix.multiply(m, e2, m, @coeff)))
516 sig_t = sqrt_safe(sig_t) * 180.0 / PI
523 # Calculate best-fit plane for the given atoms
524 # Return value: a Molby::Plane object
531 # Positional parameters and standard deviations
535 each_atom(group) { |ap|
538 if (s = ap.sigma_x) > 0.0 && s < sig_min
541 if (s = ap.sigma_y) > 0.0 && s < sig_min
544 if (s = ap.sigma_z) > 0.0 && s < sig_min
552 s.x = sig_min if s.x < sig_min
553 s.y = sig_min if s.y < sig_min
554 s.z = sig_min if s.z < sig_min
557 # The metric tensor of the reciprocal lattice
558 # g[j, i] = (ai*).dot(aj*), where ai* and aj* are the reciprocal axis vectors
559 t = self.cell_transform
561 t = Transform.identity
567 g = LAMatrix.multiply("t", g2, g2)
569 # The variance-covariance matrices of the atomic parameters
570 # mm[k][n] is a 3x3 matrix describing the correlation between the atoms k and n,
571 # and its components are defined as: sigma_k[i] * sigma_n[j] * corr[k, i, n, j],
572 # where corr(k, i, n, j) is the correlation coefficients between the atomic parameters
574 mm = Array.new(dim) { Array.new(dim) }
575 zero = LAMatrix.zero(3, 3)
578 mkn = LAMatrix.new(3, 3)
583 mkn[j, i] = sig[k][i] * sig[n][j]
585 # Inter-coordinate correlation should be implemented here
590 # Inter-atomic correlation should be implemented here
592 mm[k][n] = (mkn == zero ? zero : mkn)
596 # The variance-covariance matrix of the atom-plance distances
597 # m[j, i] = v.transpose * mm[i][j] * v, where v is the plane coefficient vector
598 # The inverse of m is the weight matrix
599 m = LAMatrix.new(dim, dim)
601 # The matrix representation of the atomic coordinates
602 # y[j, i] = x[i][j] (for j = 0..2), -1 (for j = 3)
603 # y * LAMatrix[a, b, c, d] gives the atom-plane distances for each atom
604 y = LAMatrix.new(4, dim)
612 # The coefficients to be determined
613 n0 = LAMatrix[1, 1, 1, 0]
614 v = LAMatrix[1, 1, 1] # The coefficient part
621 # Set zero to the "constant" part, and normalize the "coefficient" part
624 n0.multiply!(1.0 / n0.fnorm)
626 3.times { |i| v[0, i] = n0[0, i] }
628 # Build the variance-covariance matrix
631 m[j, i] = LAMatrix.multiply("t", v, mm[i][j], v)[0, 0]
634 c = LAMatrix.multiply("t", y, "i", m, y)
636 # Invert c: only the inverse is used in the following, so c is inversed destructively
641 # Determine the tentative solution, which is given by the eigenvector of cinv * g
642 # for the largest eigenvalue
643 evals, evecs = (cinv * g).eigenvalues
644 4.times { |i| n0[0, i] = evecs[3, i] }
648 # Convert the coefficient vector to the reciprocal space
651 # Determine multiplier
652 # In this implementation, the sign of delta-n is opposite from that used in
654 lam = 1.0 / (LAMatrix.multiply("t", h, cinv, h)[0, 0])
656 # Solve the linearized equation
657 # (Is the equation 21 in the reference really correct? Shouldn't it read
658 # B = 1 - lambda * C.inverse * H* ? )
659 b = LAMatrix.multiply(lam, cinv, g)
660 b.sub!(LAMatrix.identity(4))
665 break if dn[0, 0] ** 2 + dn[0, 1] ** 2 + dn[0, 2] ** 2 < 1e-9
670 # Error matrix = b * cinv * b.transpose
671 em = LAMatrix.multiply(b, cinv, "t", b)
672 coeff = Vector3D[n0[0, 0], n0[0, 1], n0[0, 2]]
675 return Molby::Plane.new(self, group, coeff, const, em, g)
680 plane_settings = @plane_settings || Hash.new
682 mol.open_auxiliary_window("Best-Fit Planes", :has_close_box=>true) {
683 refresh_proc = lambda { |it|
684 n = it[:tag][/\d/].to_i
685 g = plane_settings["group#{n}"]
687 str = g.inspect.sub!("IntGroup[", "").sub!("]", "")
688 set_value("group#{n}", str)
690 p = mol.plane(g) rescue p = nil
691 plane_settings["plane#{n}"] = p
696 aps = (n == 1 ? "" : "'")
697 str = sprintf("a%s = %f(%f)\nb%s = %f(%f)\nc%s = %f(%f)\nd%s = %f(%f)",
698 aps, coeff.x, sig[0],
699 aps, coeff.y, sig[1],
700 aps, coeff.z, sig[2],
702 set_value("result#{n}", str)
704 set_value("result#{n}", "")
706 p1 = plane_settings["plane1"]
707 p2 = plane_settings["plane2"]
709 t, sig = p1.dihedral(p2)
710 str = sprintf("%f(%f)", t, sig)
711 set_value("dihedral", str)
713 set_value("dihedral", "")
716 p = plane_settings["plane1"]
719 mol.each_atom(g) { |ap|
720 d, sig = p.distance(ap)
721 str += sprintf("%s %f(%f)\n", ap.name, d, sig)
727 set_value("result#{n}", str)
730 set_value("group#{n}", "")
731 set_value("result#{n}", "")
734 set_proc = lambda { |it|
735 n = it[:tag][/\d/].to_i
738 str = sel.inspect.sub!("IntGroup[", "").sub!("]", "")
739 set_value("group#{n}", str)
740 plane_settings["group#{n}"] = sel
742 plane_settings["group#{n}"] = nil
744 refresh_proc.call(it)
746 text_proc = lambda { |it|
747 n = it[:tag][/\d/].to_i
748 str = it[:value].gsub(/[^-.,0-9]/, "") # Remove unsane characters
749 g = eval("IntGroup[#{str}]") rescue g = nil
750 plane_settings["group#{n}"] = g
751 refresh_proc.call(it)
754 item(:text, :title=>"Plane 1 (ax + by + cz + d = 0)"),
756 item(:text, :title=>"Atoms"),
757 item(:textfield, :width=>240, :height=>32, :tag=>"group1", :action=>text_proc),
758 item(:button, :title=>"Set Current Selection", :tag=>"button1", :action=>set_proc),
759 item(:text, :title=>"Results"),
760 item(:textview, :width=>240, :height=>68, :editable=>false, :tag=>"result1"),
761 item(:button, :title=>"Recalculate", :tag=>"refresh1", :action=>refresh_proc),
764 item(:text, :title=>"Plane 2 (a'x + b'y + c'z + d' = 0)"),
766 item(:text, :title=>"Atoms"),
767 item(:textfield, :width=>240, :height=>32, :tag=>"group2", :action=>text_proc),
768 item(:button, :title=>"Set Current Selection", :tag=>"button2", :action=>set_proc),
769 item(:text, :title=>"Results"),
770 item(:textview, :width=>240, :height=>68, :editable=>false, :tag=>"result2"),
771 item(:button, :title=>"Recalculate", :tag=>"refresh2", :action=>refresh_proc),
772 item(:text, :title=>"Dihedral angle with Plane 1"), -1, -1,
774 item(:textfield, :width=>240, :height=>20, :tag=>"dihedral"), -1,
777 item(:text, :title=>"Distance from Plane 1"), -1, -1,
778 item(:text, :title=>"Atoms"),
779 item(:textfield, :width=>240, :height=>32, :tag=>"group3", :action=>text_proc),
780 item(:button, :title=>"Set Current Selection", :tag=>"button3", :action=>set_proc),
781 item(:text, :title=>"Results"),
782 item(:textview, :width=>240, :height=>68, :editable=>false, :tag=>"result3"),
783 item(:button, :title=>"Recalculate", :tag=>"refresh3", :action=>refresh_proc)
785 refresh_proc.call(item_with_tag("refresh1"))
786 refresh_proc.call(item_with_tag("refresh2"))
787 refresh_proc.call(item_with_tag("refresh3"))
790 @plane_settings = plane_settings
793 # Calculate bond length and angles with standard deviations
794 # args can be a single IntGroup (calculate bonds and angles including those atoms)
795 # or arrays of 2 or 3 integers (explicitly specifying bonds and angles)
796 def bond_angle_with_sigma(*args)
798 if args.length >= 2 || (args.length == 1 && !args[0].is_a?(IntGroup))
799 # Bonds and angles are explicitly specified
803 if arg.length == 2 && arg[0].is_a?(Integer) && arg[1].is_a?(Integer)
805 elsif arg.length == 3 && arg[0].is_a?(Integer) && arg[1].is_a?(Integer) && arg[2].is_a?(Integer)
808 raise MolbyError, "Invalid argument #{arg.inspect}"
817 bonds = self.bonds.select { |b|
818 (g == nil || (g.include?(b[0]) && g.include?(b[1]))) &&
819 (atoms[b[0]].symop == nil || atoms[b[1]].symop == nil)
821 angles = self.angles.select { |ang|
822 (g == nil || (g.include?(ang[0]) && g.include?(ang[1]) && g.include?(ang[2]))) &&
823 (atoms[ang[0]].symop == nil || atoms[ang[1]].symop == nil || atoms[ang[2]].symop == nil)
827 # A list of interatomic distance, its differential coefficients, and other
829 # The hash tag is a list [i, j] (i and j are the atom indices) or [i, j, k]
830 # (i, j, k are the atom indices, and r(ijk) is the distance between the atom i
831 # and the center point between the atoms j and k).
832 # The value is a list of following quantities:
833 # index 0: rij or r(ijk)
834 # index 1-9: d(rij)/d(xij), d(rij)/d(yij), d(rij)/d(zij),
835 # d(rij)/da, d(rij)/db, d(rij)/dc, d(rij)/d(alpha), d(rij)/d(beta), d(rij)/d(gamma)
836 # index 10: the list of the "base atom"
837 # index 11: the list of the transform matrices
840 # A list of fractional coordinates and sigmas
844 fract.push(ap.fract_r)
848 # A list of base atoms (for symmetry-related atoms) and transform matrices
852 trans_i = Transform.identity
855 bases.push(sym ? sym[4] : ap.index)
857 tr = transform_for_symop(sym).transpose
858 tr[3, 0] = tr[3, 1] = tr[3, 2] = 0.0
863 symcode.push(sym ? sprintf("%d_%d%d%d", sym[0] + 1, sym[1] + 5, sym[2] + 5, sym[3] + 5) : ".")
866 # Unit cell parameter
869 cell = [1, 1, 1, 90, 90, 90, 0, 0, 0, 0, 0, 0]
870 elsif cell.length == 6
871 cell.push(0.0, 0.0, 0.0, 0.0, 0.0, 0.0) # No sigma information
873 # $a, $b, $c, $alpha, $beta, $gamma, $sig_a, $sig_b, $sig_c, $sig_alpha, $sig_beta, $sig_gamma = self.cell
874 cos_a = cos(cell[3] * PI / 180.0)
875 cos_b = cos(cell[4] * PI / 180.0)
876 cos_c = cos(cell[5] * PI / 180.0)
877 abc = cell[0] * cell[1] * cos_c
878 bca = cell[1] * cell[2] * cos_a
879 cab = cell[2] * cell[0] * cos_b
880 aa = cell[0] * cell[0]
881 bb = cell[1] * cell[1]
882 cc = cell[2] * cell[2]
884 get_dlist = lambda { |_i, _j, _k|
889 _p = dlist[[_i, _j, _k]]
890 return _p if _p != nil
891 _p = (dlist[[_i, _j, _k]] = [])
892 _vij = fract[_i] - (fract[_j] + fract[_k]) * 0.5
893 _p[10] = [bases[_i], bases[_j], bases[_k]]
894 _p[11] = [trans[_i], trans[_j], trans[_k]]
900 return _p if _p != nil
901 _p = (dlist[[_i, _j]] = [])
902 _vij = fract[_i] - fract[_j]
903 _p[10] = [bases[_i], bases[_j]]
904 _p[11] = [trans[_i], trans[_j]]
909 _dij = sqrt_safe(aa * _xij * _xij + bb * _yij * _yij + cc * _zij * _zij + 2 * bca * _yij * _zij + 2 * cab * _zij * _xij + 2 * abc * _xij * _yij)
911 _p[1] = (aa * _xij + abc * _yij + cab * _zij) / _dij
912 _p[2] = (bb * _yij + bca * _zij + abc * _xij) / _dij
913 _p[3] = (cc * _zij + cab * _xij + bca * _yij) / _dij
914 _p[4] = (cell[0] * _xij * _xij + cell[2] * cos_b * _zij * _xij + cell[1] * cos_c * _xij * _yij) / _dij
915 _p[5] = (cell[1] * _yij * _yij + cell[0] * cos_c * _xij * _yij + cell[2] * cos_a * _yij * _zij) / _dij
916 _p[6] = (cell[2] * _zij * _zij + cell[1] * cos_a * _yij * _zij + cell[0] * cos_b * _zij * _xij) / _dij
917 _p[7] = (-cell[1] * cell[2] * sin(cell[3] * PI / 180.0) * _yij * _zij) * (PI / 180.0) / _dij
918 _p[8] = (-cell[2] * cell[0] * sin(cell[4] * PI / 180.0) * _zij * _xij) * (PI / 180.0) / _dij
919 _p[9] = (-cell[0] * cell[1] * sin(cell[5] * PI / 180.0) * _xij * _yij) * (PI / 180.0) / _dij
923 diff_by_rn = lambda { |_dl, _n, _ijk|
925 # return value: Vector3D[ d(rij)/d(xn), d(rij)/d(yn), d(rij)/d(zn) ]
926 # If ijk is true, then dl is dlist(i, j, k)
927 _dv = Vector3D[_dl[1], _dl[2], _dl[3]]
928 _c = Vector3D[0, 0, 0]
930 _c += _dl[11][0] * _dv
934 _c -= _dl[11][1] * _dv * 0.5
937 _c -= _dl[11][2] * _dv * 0.5
941 _c -= _dl[11][1] * _dv
947 notate_with_sigma = lambda { |_val, _sig|
949 return sprintf "%.4f", _val
951 _lg = log(_sig.abs / 1.95) / log(10.0)
953 _val2 = (_val * (10 ** _n) + 0.5).floor * (0.1 ** _n)
954 return sprintf "%.#{_n}f(%d)", _val2, (_sig * (10 ** _n) + 0.5).floor
959 c = Vector3D[0, 0, 0]
966 p = get_dlist.call(i, j, nil)
968 p[10].uniq.each { |k|
971 c = diff_by_rn.call(p, k, false)
977 apk.fract_r = r + Vector3D[0.00001, 0, 0]
978 amend_by_symmetry(p[10])
980 apk.fract_r = r + Vector3D[0, 0.00001, 0]
981 amend_by_symmetry(p[10])
983 apk.fract_r = r + Vector3D[0, 0, 0.00001]
984 amend_by_symmetry(p[10])
987 amend_by_symmetry(p[10])
988 printf " dr/dv[%d] cal %10.5g %10.5g %10.5g\n", k, c[0], c[1], c[2]
989 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
991 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]
994 sig += (p[4 + n] * cell[6 + n]) ** 2
997 results.push([[i, j], notate_with_sigma.call(p[0], sig), symcode[i], symcode[j], nil])
1004 p0 = get_dlist.call(i, j, nil)
1005 p1 = get_dlist.call(j, k, nil)
1006 p2 = get_dlist.call(i, k, nil)
1007 p3 = get_dlist.call(j, i, k)
1008 t123 = acos_safe((p0[0] ** 2 + p1[0] ** 2 - p2[0] ** 2) / (2 * p0[0] * p1[0]))
1009 t124 = acos_safe((p0[0] ** 2 + p3[0] ** 2 - p2[0] ** 2 * 0.25) / (2 * p0[0] * p3[0]))
1010 t324 = acos_safe((p1[0] ** 2 + p3[0] ** 2 - p2[0] ** 2 * 0.25) / (2 * p1[0] * p3[0]))
1011 dtdr12 = -(p0[0] ** 2 - p3[0] ** 2 + p2[0] ** 2 * 0.25) / (2 * p3[0] * (p0[0] ** 2) * sin(t124))
1012 dtdr23 = -(p1[0] ** 2 - p3[0] ** 2 + p2[0] ** 2 * 0.25) / (2 * p3[0] * (p1[0] ** 2) * sin(t324))
1013 dtdr13 = p2[0] / (sin(t124) * 4 * p0[0] * p3[0]) + p2[0] / (sin(t324) * 4 * p1[0] * p3[0])
1014 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))
1015 pp = (p0[10] + p1[10] + p2[10] + p3[10]).uniq
1020 c = Vector3D[0, 0, 0]
1021 c += diff_by_rn.call(p0, n, false) * (dtdr12 * 180.0 / PI)
1022 c += diff_by_rn.call(p1, n, false) * (dtdr23 * 180.0 / PI)
1023 c += diff_by_rn.call(p2, n, false) * (dtdr13 * 180.0 / PI)
1024 c += diff_by_rn.call(p3, n, true) * (dtdr24 * 180.0 / PI)
1025 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]
1027 dd = dtdr12 * p0[4] + dtdr23 * p1[4] + dtdr13 * p2[4] + dtdr24 * p3[4]
1028 sig += dd * dd * cell[6] * cell[6]
1029 dd = dtdr12 * p0[5] + dtdr23 * p1[5] + dtdr13 * p2[5] + dtdr24 * p3[5]
1030 sig += dd * dd * cell[7] * cell[7]
1031 dd = dtdr12 * p0[6] + dtdr23 * p1[6] + dtdr13 * p2[6] + dtdr24 * p3[6]
1032 sig += dd * dd * cell[8] * cell[8]
1033 dd = dtdr12 * p0[7] + dtdr23 * p1[7] + dtdr13 * p2[7] + dtdr24 * p3[7]
1034 sig += dd * dd * cell[9] * cell[9]
1035 dd = dtdr12 * p0[8] + dtdr23 * p1[8] + dtdr13 * p2[8] + dtdr24 * p3[8]
1036 sig += dd * dd * cell[10] * cell[10]
1037 dd = dtdr12 * p0[9] + dtdr23 * p1[9] + dtdr13 * p2[9] + dtdr24 * p3[9]
1038 sig += dd * dd * cell[11] * cell[11]
1039 sig = sqrt_safe(sig)
1040 results.push([[i, j, k], notate_with_sigma.call(t123*180/PI, sig), symcode[i], symcode[j], symcode[k]])
1045 def cmd_bond_angle_with_sigma
1047 mol.open_auxiliary_window("Bond & Angle with Sigma", :has_close_box=>true) {
1051 on_add_bond = lambda { |it|
1052 values.push([nil, nil, "-", nil, nil, nil, "-", 0])
1053 item_with_tag("table")[:refresh] = true
1054 item_with_tag("table")[:selection] = IntGroup[values.count - 1]
1055 item_with_tag("dump")[:enabled] = true
1057 on_add_angle = lambda { |it|
1058 values.push([nil, nil, nil, nil, nil, nil, nil, 0])
1059 item_with_tag("table")[:refresh] = true
1060 item_with_tag("table")[:selection] = IntGroup[values.count - 1]
1061 item_with_tag("dump")[:enabled] = true
1063 on_get_value = lambda { |it, row, col|
1064 v = values[row][col]
1067 v = mol.atoms[v].name
1071 elsif (col >= 4 && col <= 6) && (vv = values[row][col - 4]).is_a?(Integer)
1072 if (s = mol.atoms[vv].symop) != nil
1073 v = sprintf("%d_%d%d%d", s[0] + 1, s[1] + 5, s[2] + 5, s[3] + 5)
1080 on_selection_changed = lambda { |it|
1083 set_attr("delete", :enabled=>true)
1084 set_attr("dump", :enabled=>true)
1086 set_attr("delete", :enabled=>false)
1087 set_attr("dump", :enabled=>false)
1090 on_delete = lambda { |it|
1091 s = attr("table", :selection)
1093 s.reverse_each { |idx|
1094 values.delete_at(idx)
1097 tbl = item_with_tag("table")
1098 tbl[:selection] = IntGroup[]
1099 tbl[:refresh] = true
1100 on_selection_changed.call(tbl)
1102 atom_name = lambda { |ap|
1103 (ap.molecule.nresidues >= 2 ? "#{ap.res_seq}:" : "") + ap.name
1106 item(:table, :width=>500, :height=>300, :tag=>"table",
1107 :columns=>[["atom1", 60], ["atom2", 60], ["atom3", 60],
1108 ["value(sigma)", 80], ["symop1", 80], ["symop2", 80], ["symop3", 80]],
1109 :on_count=> lambda { |it| values.count },
1110 :on_get_value=>on_get_value,
1111 :on_selection_changed=>on_selection_changed),
1112 item(:text, :title=>"(1) Hit 'New Bond' or 'New Angle', and (2) click on the atoms to measure in the main window.",
1115 item(:button, :title=>"New Bond", :width=>80, :font=>[10], :action=>on_add_bond),
1116 item(:button, :title=>"New Angle", :width=>80, :font=>[10], :action=>on_add_angle),
1117 item(:button, :title=>"Delete", :width=>80, :font=>[10], :action=>on_delete,
1118 :tag=>"delete", :enabled=>false),
1119 :padding=>5, :margin=>0),
1121 item(:view, :width=>480, :height=>1), -1,
1122 item(:button, :title=>"Export to Clipboard", :tag=>"dump",
1123 :action=>lambda { |item|
1125 sel = attr("table", :selection)
1126 return if sel == nil || sel.count == 0
1130 sss = on_get_value.call(item_with_tag("table"), j, i).to_s
1131 ss += (sss == "-" ? "" : sss) + (i == 6 ? "\n" : " ")
1135 export_to_clipboard(s)
1138 [item(:button, :title=>"Close", :action=>lambda { |item| hide }), {:align=>:right}])
1140 @on_document_modified = lambda { |*d|
1141 newsel = mol.selection - sel
1143 row = (item_with_tag("table")[:selection] || [nil])[-1]
1145 if newsel.count == 1
1146 # Add new atom to the selected row
1148 lim = (values[row][2] == "-" ? 2 : 3)
1151 if values[row][i] == nil
1152 values[row][i] = val
1158 (lim - 1).times { |j|
1159 values[row][j] = values[row][j + 1]
1161 values[row][lim - 1] = val
1166 val1 = mol.bond_angle_with_sigma(values[row][0...lim])[0][1]
1168 values[row][3] = val1
1169 item_with_tag("table")[:refresh] = true
1172 # listen(mol, "documentModified", on_document_modified)
1173 # listen(mol, "documentWillClose", lambda { |*d| hide } )
1174 @on_document_modified.call
1179 def find_expanded_atom(base, symop)
1180 return base unless symop
1181 symopb = symop + [base]
1182 self.each_atom { |ap|
1183 if ap.symop == symopb
1190 def complete_by_symmetry
1192 raise "Unit cell should be given"
1195 avec, bvec, cvec = self.box
1197 self.nsymmetries.times { |n|
1198 syms.push(transform_for_symop([n, 0, 0, 0], true))
1201 self.each_fragment { |f|
1205 self.each_atom { |ap|
1206 # Find if ap is contained in expansion
1207 next if ap.symop != nil # Already expanded
1208 rad = Parameter.builtin.elements[ap.atomic_number].radius
1209 syms.each_with_index { |sym, sym_idx|
1212 dy = (n / 3) % 3 - 1
1213 dz = (n / 9) % 3 - 1
1214 next if dx == 0 && dy == 0 && dz == 0 && sym_idx == 0
1215 symop = [sym_idx, dx, dy, dz]
1216 next if self.find_expanded_atom(ap.index, symop)
1217 r = sym * ap.r + avec * dx + bvec * dy + cvec * dz
1218 close_atoms = self.find_close_atoms(r, 1.2, rad)
1219 if close_atoms.length > 0
1220 # ap * [sym, dx, dy, dz] is included in the expansion
1221 close_atoms.each { |ca|
1222 next if ca > ap.index
1223 i1 = frags.index { |f| f.include?(ca) }
1224 i2 = frags.index { |f| f.include?(ap.index) }
1225 pp = close_pairs.find { |p1| p1[0] == i1 && p1[1] == i2 && p1[2] == symop }
1227 pp = [i1, i2, symop, [], [], [], []]
1228 close_pairs.push(pp)
1230 if (r - atoms[ca].r).length2 > 0.0001
1231 # Normal case (bond between ca and ap)
1233 pp[4].push(ap.index)
1237 pp[6].push(ap.index)
1244 puts "close_pairs = #{close_pairs}" if verbose
1245 expand_pairs = lambda { |i1, symop, depth|
1246 # Find expanded fragment that is close to fragment [i1, symop]
1247 next [] if depth > 16
1249 close_pairs.each { |pp|
1250 next if i1 != nil && pp[0] != i1
1251 next if pp[3].count == 0 # No real expansion
1252 # Multiply two symops
1254 symop2 = symop_for_transform(transform_for_symop(pp[2]) * transform_for_symop(symop))
1255 puts "multiply two symops: #{pp[2].inspect} * #{symop.inspect} -> #{symop2.inspect}" if verbose
1261 if symop2[1] == 0 && symop2[2] == 0 && symop2[3] == 0
1262 # No expansion, but pp[3] and pp[4] are still needed to make bonds
1265 # Expand the atoms only at the marginal
1266 retval.push([nil, symop, symop2, pp[3], pp[4], pp[5], pp[6]])
1269 retval.push([pp[1], symop, symop2, pp[3], pp[4], pp[5], pp[6]])
1270 retval.concat(expand_pairs.call(pp[1], symop2, depth + 1))
1275 ex_pairs = expand_pairs.call(nil, nil, 0)
1276 puts "ex_pairs = #{ex_pairs}" if verbose
1280 ex_pairs.each { |ex|
1281 # ex[0]: fragment to expand, ex[1], ex[2]: symop,
1282 # ex[3], ex[4]: atoms to make bonds, ex[5], ex[6]: duplicate atoms
1290 self.expand_by_symmetry(f, ex[2][0], ex[2][1], ex[2][2], ex[2][3], true)
1291 puts "expand(#{f}, #{ex[2]}) -> atoms[#{n1}..#{self.natoms - n1}]" if verbose
1293 ex[3].each_with_index { |x, i|
1294 x1 = self.find_expanded_atom(x, ex[1])
1296 x2 = f.index(ex[4][i]) + n1 # New index of the expanded atom
1298 x2 = ex[4][i] # Base atom
1301 puts "create_bond(#{x1}, #{x2})" if verbose
1303 # Register duplicate atoms
1305 ex[5].each_with_index { |x, i|
1306 x1 = self.find_expanded_atom(x, ex[1])
1307 x2 = f.index(ex[6][i]) + n1
1308 duplicates.each { |d|
1316 duplicates.push([x1, x2])
1321 puts "duplicates = #{duplicates}" if verbose
1323 # Remove duplicate atoms
1325 duplicates.each { |d|
1326 d.each_with_index { |dn, i|
1329 self.atoms[dn].connects.each { |nn|
1330 create_bond(d[0], nn)
1331 puts "create_bond(#{d[0]}, #{nn})" if verbose
1337 puts "remove(#{rem})" if verbose
1341 def create_packing_diagram
1343 error_message_box "Unit cell is not defined."
1346 expansion_box = (@expansion_box ||= [0.0, 1.0, 0.0, 1.0, 0.0, 1.0])
1347 h = Dialog.run("Create Packing Diagram") {
1349 item(:text, :title=>"Specify the expansion limits for each axis:"),
1351 item(:text, :title=>"x"),
1352 item(:textfield, :width=>80, :tag=>"xmin", :value=>sprintf("%.1f", expansion_box[0].to_f)),
1353 item(:text, :title=>"..."),
1354 item(:textfield, :width=>80, :tag=>"xmax", :value=>sprintf("%.1f", expansion_box[1].to_f)),
1355 item(:text, :title=>"y"),
1356 item(:textfield, :width=>80, :tag=>"ymin", :value=>sprintf("%.1f", expansion_box[2].to_f)),
1357 item(:text, :title=>"..."),
1358 item(:textfield, :width=>80, :tag=>"ymax", :value=>sprintf("%.1f", expansion_box[3].to_f)),
1359 item(:text, :title=>"z"),
1360 item(:textfield, :width=>80, :tag=>"zmin", :value=>sprintf("%.1f", expansion_box[4].to_f)),
1361 item(:text, :title=>"..."),
1362 item(:textfield, :width=>80, :tag=>"zmax", :value=>sprintf("%.1f", expansion_box[5].to_f)))
1365 @expansion_box[0] = h["xmin"].to_f
1366 @expansion_box[1] = h["xmax"].to_f
1367 @expansion_box[2] = h["ymin"].to_f
1368 @expansion_box[3] = h["ymax"].to_f
1369 @expansion_box[4] = h["zmin"].to_f
1370 @expansion_box[5] = h["zmax"].to_f
1375 # Enumerate all atoms within the expansion box
1376 syms = self.symmetries
1378 xmin = @expansion_box[0]
1379 xmax = @expansion_box[1]
1380 ymin = @expansion_box[2]
1381 ymax = @expansion_box[3]
1382 zmin = @expansion_box[4]
1383 zmax = @expansion_box[5]
1385 xmax_d = xmax.floor - 1
1387 ymax_d = ymax.floor - 1
1389 zmax_d = zmax.floor - 1
1390 syms.each_with_index { |sym, i|
1392 fr = sym * ap.fract_r
1399 symopi = i * 1000000 + (50 - dx) * 10000 + (50 - dy) * 100 + 50 - dz
1400 (h[symopi] ||= []).push(ap.index)
1401 xmin_d.upto(xmax_d) { |xd|
1402 next if fr.x + xd < xmin || fr.x + xd > xmax
1403 ymin_d.upto(ymax_d) { |yd|
1404 next if fr.y + yd < ymin || fr.y + yd > ymax
1405 zmin_d.upto(zmax_d) { |zd|
1406 next if fr.z + zd < zmin || fr.z + zd > zmax
1407 next if xd == 0 && yd == 0 && zd == 0
1408 symopid = symopi + xd * 10000 + yd * 100 + zd
1409 (h[symopid] ||= []).push(ap.index)
1415 h.keys.sort.each { |key|
1417 dx = key % 1000000 / 10000 - 50
1418 dy = key % 10000 / 100 - 50
1420 next if sym == 0 && dx == 0 && dy == 0 && dz == 0
1421 # puts "[#{sym},#{dx},#{dy},#{dz}]: #{h[key].inspect}"
1422 expand_by_symmetry(IntGroup[h[key]], sym, dx, dy, dz)
1429 tmp = create_temp_dir("ortep", mol.name)
1431 error_message_box($!.to_s)
1434 tepexe = "#{ResourcePath}/ortep3/ortep3"
1435 if $platform == "win"
1439 tepbounds = [0, 0, 400, 400]
1442 ["all", "Shaded", "black", ""],
1443 ["Li-Ar", "Principals", "black", ""],
1444 ["C", "Boundary", "black", ""],
1445 ["H", "Fixed", "black", "0.1"]
1448 ["all", "all", "4 Shades", "black"],
1449 ["Li-Ar", "Li-Ar", "3 Shades", "black"],
1450 ["C", "Li-Ar", "2 Shades", "black"],
1451 ["H", "all", "Open", "black"]
1454 mol.open_auxiliary_window("Show ORTEP", :resizable=>true, :has_close_box=>true) {
1455 tepview = nil # Forward declaration
1458 "Atoms"=>[["atom list", 65], ["type", 80], ["color", 40], ["radius", 50]],
1459 "Bonds"=>[["list1", 65], ["list2", 65], ["type", 80], ["color", 40]]
1461 atom_types = ["Boundary", "Principals", "Axes", "Shaded", "Fixed"]
1462 bond_types = ["Open", "1 Shade", "2 Shades", "3 Shades", "4 Shades"]
1463 colors = ["black", "red", "green", "blue", "cyan", "magenta", "yellow"]
1465 get_ortep_attr = lambda {
1467 make_atom_list = lambda { |s, ln|
1468 ss = s.scan(/[-.0-9A-Za-z]+/)
1470 ss.inject(IntGroup[]) { |r, it|
1474 elsif it =~ /-|(\.\.)/
1475 s0 = Regexp.last_match.pre_match
1476 s1 = Regexp.last_match.post_match
1483 if s1 !~ /^[0-9]+$/ || (s1 = s1.to_i) < s0
1484 raise "Bad atom list specification: #{s} in line #{ln}"
1489 s0 = Parameter.builtin.elements.find_index { |p| p.name == s0.capitalize }
1490 s1 = Parameter.builtin.elements.find_index { |p| p.name == s1.capitalize }
1491 if s0 == nil || s1 == nil
1492 raise "Bad element symbol specification: #{s} in line #{ln}"
1494 (s0..s1).each { |an|
1495 r.add(mol.atom_group { |ap| ap.atomic_number == an } )
1503 descs["Atoms"].each_with_index { |d, idx|
1504 list = make_atom_list.call(d[0], idx)
1505 type = atom_types.find_index { |it| it == d[1] } || 0
1506 col = (colors.find_index { |it| it == d[2] } || 0) + 1
1508 at.push([list, type, col, rad])
1513 descs["Bonds"].each_with_index { |d, idx|
1514 list1 = make_atom_list.call(d[0], idx)
1515 list2 = make_atom_list.call(d[1], idx)
1516 type = bond_types.find_index { |it| it == d[2] } || 0
1517 col = (colors.find_index { |it| it == d[3] } || 0) + 1
1518 bd.push([list1, list2, type, col])
1523 on_update_ortep = lambda { |it|
1525 attr = get_ortep_attr.call
1526 # Create ORTEP input in the temporary directory
1527 open(tmp + "/TEP.IN", "w") { |fp| mol.export_ortep(fp, attr) }
1531 if FileTest.exist?("TEP001.PRN")
1532 File.unlink("TEP001.PRN")
1534 pid = call_subprocess([tepexe], "Running ORTEP", nil, "NUL", "NUL")
1535 if FileTest.exist?("TEP001.PRN")
1536 File.rename("TEP001.PRN", "TEP001.ps")
1540 msg = "ORTEP execution in #{tmp} failed with status #{pid}."
1541 message_box(msg, "ORTEP Failed", :ok, :warning)
1542 elsif !FileTest.exist?(tmp + "/TEP001.ps")
1543 msg = "ORTEP execution in #{tmp} failed with unknown error."
1544 message_box(msg, "ORTEP Failed", :ok, :warning)
1546 open(tmp + "/TEP001.ps", "r") { |fp|
1548 tepbounds = [100000, 100000, -100000, -100000]
1552 if ln =~ /setrgbcolor/
1553 tepdata.push(["color", x.to_f, y.to_f, c.to_f])
1555 elsif ln =~ /setgray/
1557 tepdata.push(["color", x, x, x])
1563 tepdata.push([x, y])
1565 tepdata[-1].push(x, y)
1569 tepbounds[0] = x if x < tepbounds[0]
1570 tepbounds[1] = y if y < tepbounds[1]
1571 tepbounds[2] = x if x > tepbounds[2]
1572 tepbounds[3] = y if y > tepbounds[3]
1574 # [x, y, width, height]
1575 tepbounds[2] -= tepbounds[0]
1576 tepbounds[3] -= tepbounds[1]
1578 tepview.refresh_rect(tepview[:frame])
1581 on_draw_ortep = lambda { |it|
1584 brush(:color=>[1, 1, 1, 1])
1585 pen(:color=>[1, 1, 1, 1])
1586 draw_rectangle(frame)
1587 pen(:color=>[0, 0, 0, 1])
1588 rx = (frame[2] - 10.0) / tepbounds[2]
1589 ry = (frame[3] - 10.0) / tepbounds[3]
1591 dx = (frame[2] - tepbounds[2] * rx) * 0.5
1592 dy = (frame[3] + tepbounds[3] * rx) * 0.5
1595 pen(:color=>[d[1], d[2], d[3], 1])
1598 x0 = (dx + (d[0] - tepbounds[0]) * rx).to_i
1599 y0 = (dy - (d[1] - tepbounds[1]) * rx).to_i
1600 (d.length / 2 - 1).times { |i|
1601 x1 = (dx + (d[i * 2 + 2] - tepbounds[0]) * rx).to_i
1602 y1 = (dy - (d[i * 2 + 3] - tepbounds[1]) * rx).to_i
1603 draw_line(x0, y0, x1, y1)
1609 on_export_eps = lambda { |fname|
1610 frame = item_with_tag("ortep")[:frame].dup
1612 rx = (frame[2] - dx * 2) / tepbounds[2]
1613 ry = (frame[3] - dy * 2) / tepbounds[3]
1615 frame[2] = (rx * tepbounds[2] + dx * 2).to_i
1616 frame[3] = (rx * tepbounds[3] + dy * 2).to_i
1617 # Assumes A4 paper and center the bounding box
1618 frame[0] = (595 - frame[2]) / 2
1619 frame[1] = (842 - frame[3]) / 2
1620 xmax = frame[0] + frame[2]
1621 ymax = frame[1] + frame[3]
1622 open(fname, "w") { |fp|
1623 fp.print "%!PS-Adobe-3.0 EPSF-3.0
1625 %%BoundingBox: #{frame[0]} #{frame[1]} #{xmax} #{ymax}
1627 %%Orientation: Landscape
1634 0 setgray 1 setlinecap 1 setlinewidth
1639 fp.printf "%.3f %.3f %.3f setrgbcolor\n", d[1], d[2], d[3]
1642 x0 = frame[0] + dx + (d[0] - tepbounds[0]) * rx
1643 y0 = frame[1] + dy + (d[1] - tepbounds[1]) * rx
1644 fp.printf "%.2f %.2f m\n", x0, y0
1645 (d.length / 2 - 1).times { |i|
1646 x1 = frame[0] + dx + (d[i * 2 + 2] - tepbounds[0]) * rx
1647 y1 = frame[1] + dy + (d[i * 2 + 3] - tepbounds[1]) * rx
1648 fp.printf "%.2f %.2f l\n", x1, y1
1654 fp.print "showpage\n%%Trailer\n%%EOF\n"
1657 on_export_bitmap = lambda { |fname|
1658 frame = item_with_tag("ortep")[:frame].dup
1661 rx = (frame[2] - dx * 2) * scale / tepbounds[2]
1662 ry = (frame[3] - dy * 2) * scale / tepbounds[3]
1664 frame[2] = (rx * tepbounds[2] + dx * 2).to_i
1665 frame[3] = (rx * tepbounds[3] + dy * 2).to_i
1666 bmp = Bitmap.new(frame[2], frame[3], 32)
1669 pen(:color=>[0, 0, 0, 1], :width=>scale)
1672 pen(:color=>[d[1], d[2], d[3], 1])
1675 x0 = (dx + (d[0] - tepbounds[0]) * rx).to_i
1676 y0 = (dy + (tepbounds[3] - d[1] + tepbounds[1]) * rx).to_i
1677 (d.length / 2 - 1).times { |i|
1678 x1 = (dx + (d[i * 2 + 2] - tepbounds[0]) * rx).to_i
1679 y1 = (dy + (tepbounds[3] - d[i * 2 + 3] + tepbounds[1]) * rx).to_i
1680 draw_line(x0, y0, x1, y1)
1686 bmp.save_to_file(fname)
1688 on_export = lambda { |it|
1689 basename = (mol.path ? File.basename(mol.path, ".*") : mol.name)
1690 fname = Dialog.save_panel("Export ORTEP file:", mol.dir, basename + ".eps",
1691 "Encapsulated PostScript (*.eps)|*.eps|PNG (*.png)|*.png|TIFF (*.tif)|*.tif|ORTEP Instruction (*.tep)|*.tep|All files|*.*")
1693 ext = File.extname(fname).downcase
1695 on_export_eps.call(fname)
1696 elsif ext == ".png" || ext == ".tif" || ext == ".tiff"
1697 on_export_bitmap.call(fname)
1699 filecopy("#{tmp}/TEP.IN", fname)
1702 @on_document_modified = lambda { |m|
1704 # Close handler (called when the close box is pressed or the document is closed)
1705 @on_close = lambda { |*d|
1710 on_select_tab = lambda { |it|
1711 tab = ["Atoms", "Bonds"][it[:value]]
1712 # puts "tab = #{tab}"
1713 table = item_with_tag("table")
1714 table[:columns] = columns[tab]
1715 table[:selection] = IntGroup[]
1716 table[:refresh] = true
1718 get_count = lambda { |it| descs[tab].count }
1719 get_value = lambda { |it, row, col| descs[tab][row][col] }
1720 has_popup_menu = lambda { |it, row, col|
1727 elsif tab == "Bonds"
1736 on_selection_changed = lambda { |it|
1737 sel = it[:selection]
1738 item_with_tag("remove")[:enabled] = (sel == nil || sel.count == 0 ? false : true)
1740 is_item_editable = lambda { |it, row, col|
1741 return has_popup_menu.call(it, row, col) == nil
1743 popup_menu_selected = lambda { |it, row, col, sel|
1744 titles = has_popup_menu.call(it, row, col)
1745 return nil if titles == nil
1746 descs[tab][row][col] = titles[sel]
1749 set_value = lambda { |it, row, col, val|
1750 descs[tab][row][col] = val
1753 add_to_table = lambda { |it|
1754 table = item_with_tag("table")
1755 d = descs[tab][-1].dup
1757 table[:refresh] = true
1758 table[:selection] = IntGroup[descs[tab].count - 1]
1759 # puts descs[tab].inspect
1761 remove_from_table = lambda { |it|
1762 table = item_with_tag("table")
1763 sel = table[:selection]
1764 sel.reverse_each { |n|
1765 descs[tab][n, 1] = []
1767 table[:refresh] = true
1771 item(:popup, :subitems=>["Atoms", "Bonds"], :action=>on_select_tab),
1772 item(:table, :width=>240, :height=>380, :flex=>[0,0,0,0,1,1], :tag=>"table",
1773 :columns=>columns["Atoms"],
1774 :on_count=>get_count,
1775 :on_get_value=>get_value,
1776 :on_set_value=>set_value,
1777 :on_selection_changed=>on_selection_changed,
1778 :is_item_editable=>is_item_editable,
1779 :has_popup_menu=>has_popup_menu,
1780 :on_popup_menu_selected=>popup_menu_selected),
1782 item(:button, :title=>"Add", :tag=>"add", :action=>add_to_table),
1783 item(:button, :title=>"Remove", :tag=>"remove", :action=>remove_from_table, :enabled=>false),
1784 :flex=>[0,1,0,0,0,0]),
1785 :flex=>[0,0,0,0,0,1]),
1787 item(:view, :width=>400, :height=>400, :tag=>"ortep", :on_paint=>on_draw_ortep, :flex=>[0,0,0,0,1,1]),
1789 item(:button, :title=>"Refresh", :action=>on_update_ortep, :align=>:left),
1790 item(:button, :title=>"Export as...", :action=>on_export, :align=>:right),
1791 :flex=>[0,1,0,0,0,0]),
1792 :flex=>[0,0,0,0,1,1]),
1793 :flex=>[0,0,0,0,1,1]
1795 set_min_size(480, 200)
1796 tepview = item_with_tag("ortep")
1797 # listen(mol, "documentModified", on_document_modified)
1798 # listen(mol, "documentWillClose", lambda { |m| close } )
1799 @on_document_modified.call(nil)
1800 on_update_ortep.call(nil)
1808 hash = Dialog.run("Unit Cell") {
1811 @box = (@box ? @box.dup : nil)
1814 def set_cell_value(item1)
1815 alpha = value("alpha").to_f * PI / 180.0
1816 beta = value("beta").to_f * PI / 180.0
1817 gamma = value("gamma").to_f * PI / 180.0
1818 if alpha.abs < 1e-8 || beta.abs < 1e-8 || gamma.abs < 1e-8
1822 @box = [Vector3D[1,0,0],Vector3D[0,1,0],Vector3D[0,0,1],Vector3D[0,0,0],[1,1,1]]
1824 av = Vector3D[1, 0, 0]
1825 bv = Vector3D[cos(gamma), sin(gamma), 0]
1827 cy = (cos(alpha) - cos(beta) * cos(gamma)) / sin(gamma)
1828 cz = sqrt_safe(1.0 - cx * cx - cy * cy)
1829 cv = Vector3D[cx, cy, cz]
1830 x0 = @box[0].normalize
1831 z0 = (@box[0].cross(@box[1])).normalize
1832 y0 = (z0.cross(x0)).normalize
1833 tr = Transform[x0, y0, z0, Vector3D[0, 0, 0]]
1834 av = (tr * av) * value("a").to_f
1835 bv = (tr * bv) * value("b").to_f
1836 cv = (tr * cv) * value("c").to_f
1842 if @box[i][j].abs < 1e-8
1847 update_values(item1)
1850 def set_box_value(item1)
1853 ["o0", "o1", "o2", "a0", "a1", "a2", "b0", "b1", "b2", "c0", "c1", "c2"].each { |k|
1856 if s == nil || s == ""
1860 h[k] = Float(eval(s))
1861 set_value(k, h[k].to_s)
1864 mes = "Cannot evaluate #{value(k)}: " + $!.to_s
1865 Dialog.run("Value Error") {
1866 layout(1, item(:text, :title=>mes))
1867 set_attr(1, :hidden=>true)
1873 ax = Vector3D[h["a0"], h["a1"], h["a2"]]
1874 bx = Vector3D[h["b0"], h["b1"], h["b2"]]
1875 cx = Vector3D[h["c0"], h["c1"], h["c2"]]
1876 ox = Vector3D[h["o0"], h["o1"], h["o2"]]
1878 if ax.length2 < 1e-8
1880 ax = Vector3D[1, 0, 0]
1882 if bx.length2 < 1e-8
1884 bx = Vector3D[0, 1, 0]
1886 if cx.length2 < 1e-8
1888 cx = Vector3D[0, 0, 1]
1890 @box = [ax, bx, cx, ox, fx]
1894 update_values(item1)
1896 def clear_box(item1)
1899 set_attr("restore", :enabled=>true)
1900 update_values(item1)
1902 def restore_box(item1)
1903 @box = @box_save.dup
1904 set_attr("restore", :enabled=>false)
1905 update_values(item1)
1908 acos(v1.dot(v2)/(v1.length * v2.length)) * 180.0 / PI
1910 def update_values(it)
1911 ["a0", "a1", "a2", "b0", "b1", "b2", "c0", "c1", "c2", "o0", "o1", "o2",
1912 "a", "b", "c", "alpha", "beta", "gamma"].each_with_index { |tag, i|
1917 val = @box[i / 3][i % 3]
1920 val = @box[i - 12].length
1923 val = angle(@box[(i - 14) % 3], @box[(i - 13) % 3])
1926 set_value(tag, sprintf(fmt, val))
1932 item(:text, :title=>"Cell parameters:"),
1935 item(:text, :title=>"a/b/c"),
1936 item(:textfield, :width=>140, :tag=>"a"),
1937 item(:textfield, :width=>140, :tag=>"b"),
1938 item(:textfield, :width=>140, :tag=>"c"),
1940 item(:text, :title=>"α/β/γ"),
1941 item(:textfield, :width=>140, :tag=>"alpha"),
1942 item(:textfield, :width=>140, :tag=>"beta"),
1943 item(:textfield, :width=>140, :tag=>"gamma"),
1946 item(:button, :title=>"Set Cell Parameters", :align=>:right, :action=>:set_cell_value),
1950 item(:text, :title=>"Axis vectors:"),
1953 item(:text, :title=>"origin"),
1954 item(:textfield, :width=>140, :tag=>"o0"),
1955 item(:textfield, :width=>140, :tag=>"o1"),
1956 item(:textfield, :width=>140, :tag=>"o2"),
1958 item(:text, :title=>"a-axis"),
1959 item(:textfield, :width=>140, :tag=>"a0"),
1960 item(:textfield, :width=>140, :tag=>"a1"),
1961 item(:textfield, :width=>140, :tag=>"a2"),
1963 item(:text, :title=>"b-axis"),
1964 item(:textfield, :width=>140, :tag=>"b0"),
1965 item(:textfield, :width=>140, :tag=>"b1"),
1966 item(:textfield, :width=>140, :tag=>"b2"),
1968 item(:text, :title=>"c-axis"),
1969 item(:textfield, :width=>140, :tag=>"c0"),
1970 item(:textfield, :width=>140, :tag=>"c1"),
1971 item(:textfield, :width=>140, :tag=>"c2"),
1974 item(:button, :title=>"Set Axis Vectors", :align=>:right, :action=>:set_box_value),
1978 item(:text, :title=>"(Ruby expressions are allowed as the values; e.g. 12.0*cos(60*PI/180))"),
1981 item(:button, :title=>"Clear Cell", :tag=>"clear", :action=>:clear_box),
1982 item(:button, :title=>"Restore Cell", :tag=>"restore", :action=>:restore_box, :enabled=>false),
1984 set_attr(0, :action=>lambda { |it|
1985 if set_box_value(it)
1992 if hash[:status] == 0
1994 h = Dialog.run(" ") {
1996 item(:text, :title=>"Do you want to keep the fractional coordinates?"),
1997 item(:radio, :title=>"Yes, convert the cartesian coordinates so that the fractional coordinates will be unchanged.",
1998 :tag=>"yes", :value=>1),
1999 item(:radio, :title=>"No, keep the cartesian coordinates unchanged.", :tag=>"no"))
2000 radio_group("yes", "no")
2014 # For debug; check the symmetry data in space_groups.txt for self-consistency
2015 def Molecule.check_space_group
2016 zero_transform = Transform.zero
2017 group_member_p = lambda { |g, tr|
2018 g.each_with_index { |tr2, idx|
2020 # Wrap the translational part to [-0.5..0.5]
2021 3.times { |k| tr2[3, k] -= (tr2[3, k] + 0.5).floor }
2022 if tr2.nearly_equal?(zero_transform)
2028 @@space_groups.each { |g|
2038 err += "The element #{i + 1} is not regular transform; #{trx ? symmetry_to_string(trx) : 'nil'}\n"
2041 3.times { |k| trx[3, k] -= trx[3, k].floor } # Wrap the translation part to [0..1]
2042 if !group_member_p.call(group, trx)
2043 err += "The element #{i + 1} has no inverse element; #{symmetry_to_string(trx)}\n"
2049 3.times { |k| trx[3, k] -= trx[3, k].floor } # Wrap the translation part to [0..1]
2050 if !group_member_p.call(group, trx)
2051 err += "The element #{i + 1} * #{j + 1} is not in the group; "
2052 err += "#{symmetry_to_string(tri)} * #{symmetry_to_string(trj)} = #{symmetry_to_string(trx)}\n"
2057 msg = "#{name} is a complete group\n"
2059 msg = "#{name} is NOT a complete group\n" + err
2060 error_message_box(msg)
2062 # break if err != ""
2066 def Molecule.setup_space_groups
2067 if defined?(@@space_groups)
2068 return @@space_groups
2071 # The debug flag causes self-check of the space group table
2076 @@space_groups = [] # Sequential table of space groups (array of [name, array_of_transform])
2077 @@space_group_list = [] # [["Triclinic", -> Crystal system
2078 # ["#1: P1", -> Space group family
2079 # [["P1", 0]]],-> Variations
2084 # [["Pc", 6], ["Pa", 7], ["Pn", 8]]],
2087 Kernel.open(ScriptPath + "/space_groups.txt", "r") { |fp|
2089 lines.push(ln) if debug
2090 if ln =~ /\[(\w+)\]/
2092 @@space_group_list.push([label, []])
2095 if ln =~ /^(\d+) +(.*)$/
2096 group_code = $1.to_i
2098 name = "#" + $1 + ": " + $2
2102 lines.push(ln.dup) if debug
2107 lattice = ln.scan(/((^\+)|,)\(([\/,0-9]+)\)/).map {
2108 |a| a[2].split(/, */).map {
2109 |x| if x =~ /^(\d+)\/(\d+)$/
2114 lattice.each { |lat|
2124 # Rectify the shorthand descriptions
2125 ln1 = ln.dup if debug
2127 mod = ln.gsub!(/(\d)(\d)/, "\\1/\\2")
2128 mod = ln.gsub!(/(^|[^\/])(\d)([^\/]|$)/, "\\11/\\2\\3") || mod
2129 mod = ln.gsub!(/([0-9xyzXYZ])([xyzXYZ])/, "\\1+\\2") || mod
2131 puts "#{fp.lineno}: #{ln1} -> #{ln}\n"
2134 group.push(string_to_symmetry(ln))
2136 lines[-1] = symmetry_to_string(group[-1]).gsub(/ +/, "") + "\n"
2139 error_message_box($!.message + "(line #{fp.lineno})")
2143 group.concat(group_gen)
2144 @@space_groups.push([name, group])
2145 group_index = @@space_groups.count - 1
2146 if last_group_code != group_code
2147 idx = (name.index(" (") || 0) - 1 # Remove parenthesized phrase
2148 group_family_name = name[0..idx]
2149 @@space_group_list[-1][1].push([group_family_name, []])
2150 last_group_code = group_code
2152 @@space_group_list[-1][1][-1][1].push([group_name, group_index])
2157 Molecule.check_space_group
2158 fn = Dialog.save_panel("Save modified space_group.txt:")
2160 Kernel.open(fn, "w") { |fp|
2167 return @@space_groups
2170 def cmd_symmetry_operations
2173 syms = mol.symmetries
2175 if !defined?(@@space_groups)
2176 Molecule.setup_space_groups
2180 crystal_systems = @@space_group_list.map { |m| m[0] }
2181 space_groups = @@space_group_list[0][1].map { |m| m[0] }
2182 variations = @@space_group_list[0][1][0][1].map { |m| m[0] }
2184 # The index (for @@space_groups) of the current space group (an integer or nil)
2185 current_space_group = nil
2187 # Find current space group
2188 guess_space_group = lambda {
2189 current_space_group = nil
2190 s = syms.map { |tr| tr = tr.dup; 3.times { |k| tr[3, k] -= tr[3, k].floor }; tr }
2191 @@space_groups.each_with_index { |g, i|
2192 next if g[1].count != s.count
2195 idx = ss.find_index { |tr2| tr.nearly_equal?(tr2) }
2200 current_space_group = i
2206 select_space_group = lambda { |it|
2208 h = Dialog.run("Select Space Group") {
2209 update_operation = lambda { |*d|
2210 it = item_with_tag("operations")
2211 cval = item_with_tag("crystal_system")[:value]
2212 sval = item_with_tag("space_group")[:value]
2213 vval = item_with_tag("variation")[:value]
2214 idx = @@space_group_list[cval][1][sval][1][vval][1]
2216 @@space_groups[idx][1].each { |tr|
2217 op += symmetry_to_string(tr) + "\n"
2221 crystal_system_popup_handler = lambda { |it|
2223 space_groups = @@space_group_list[val][1].map { |m| m[0] }
2224 variations = @@space_group_list[val][1][0][1].map { |m| m[0] }
2225 item_with_tag("space_group")[:subitems] = space_groups
2226 item_with_tag("space_group")[:value] = 0
2227 item_with_tag("variation")[:subitems] = variations
2228 item_with_tag("variation")[:value] = 0
2229 update_operation.call
2231 space_group_popup_handler = lambda { |it|
2233 cval = item_with_tag("crystal_system")[:value]
2234 variations = @@space_group_list[cval][1][val][1].map { |m| m[0] }
2235 item_with_tag("variation")[:subitems] = variations
2236 item_with_tag("variation")[:value] = 0
2237 update_operation.call
2239 variation_popup_handler = lambda { |it|
2240 update_operation.call
2243 item(:text, :title=>"Crystal System"),
2244 item(:popup, :subitems=>crystal_systems, :tag=>"crystal_system", :action=>crystal_system_popup_handler),
2245 item(:text, :title=>"Space Group"),
2246 item(:popup, :subitems=>space_groups, :tag=>"space_group", :action=>space_group_popup_handler),
2247 item(:text, :title=>"Variation"),
2248 item(:popup, :subitems=>variations, :tag=>"variation", :action=>variation_popup_handler),
2249 item(:textview, :width=>360, :height=>200, :editable=>false, :tag=>"operations"),
2251 if current_space_group
2253 @@space_group_list.each_with_index { |m, i|
2254 m[1].each_with_index { |mm, j|
2255 mm[1].each_with_index { |mmm, k|
2256 if mmm[1] == current_space_group
2257 (it = item_with_tag("crystal_system"))[:value] = i
2258 crystal_system_popup_handler.call(it)
2259 (it = item_with_tag("space_group"))[:value] = j
2260 space_group_popup_handler.call(it)
2261 (it = item_with_tag("variation"))[:value] = k
2262 variation_popup_handler.call(it)
2272 cval = h["crystal_system"]
2273 sval = h["space_group"]
2274 vval = h["variation"]
2275 idx = @@space_group_list[cval][1][sval][1][vval][1]
2276 syms = @@space_groups[idx][1].dup
2280 h = Dialog.run("Symmetry Operations") {
2281 update_space_group_text = lambda {
2282 guess_space_group.call
2283 if current_space_group
2284 label = @@space_groups[current_space_group][0]
2288 item_with_tag("space_group")[:value] = label
2292 item(:text, :title=>"Space Group:"),
2293 item(:textfield, :width=>200, :editable=>false, :tag=>"space_group")),
2294 item(:button, :title=>"Select...", :action=>lambda { |it|
2295 select_space_group.call(it)
2296 item_with_tag("sym_table")[:refresh] = true
2297 update_space_group_text.call }, :align=>:right),
2298 item(:table, :tag=>"sym_table", :width=>300, :height=>300,
2299 :columns=>[["Symmetry Operations", 280]],
2300 :on_count=>lambda { |it| syms.count },
2301 :on_get_value=>lambda { |it, row, col| symmetry_to_string(syms[row]) },
2302 :on_set_value=>lambda { |it, row, col, val|
2303 syms[row] = string_to_symmetry(val)
2304 update_space_group_text.call },
2305 :is_item_editable=>lambda { |it, row, col| true },
2306 :on_selection_changed=>lambda { |it|
2307 item_with_tag("delete")[:enabled] = (it[:selection].count > 0) } ),
2309 item(:togglebutton, :title=>"+", :width=>40, :tag=>"add",
2310 :action=>lambda { |it|
2311 syms.push(Transform.new); item_with_tag("sym_table")[:refresh] = true
2312 update_space_group_text.call } ),
2313 item(:togglebutton, :title=>"-", :width=>40, :tag=>"delete", :enabled=>false,
2314 :action=>lambda { |it|
2315 item_with_tag("sym_table")[:selection].reverse_each { |n| syms.delete_at(n) }
2316 item_with_tag("sym_table")[:refresh] = true
2317 update_space_group_text.call } ),
2318 :padding=>0, :margin=>0 ) )
2319 item_with_tag("sym_table")[:refresh] = true
2320 update_space_group_text.call
2323 self.remove_symmetries(nil)
2325 self.add_symmetry(s)
2330 def cmd_show_periodic_image
2332 hash = Dialog.run("Show Periodic Image") {
2334 def set_periodic_image(it)
2336 ["amin", "amax", "bmin", "bmax", "cmin", "cmax"].each_with_index { |k, i|
2338 if s == nil || s == ""
2344 @mol.show_periodic_image(a)
2345 @mol.show_periodic_image = (attr("show_flag", :value) == 1 ? true : false)
2347 pimage = @mol.show_periodic_image
2348 flag = @mol.show_periodic_image?
2350 item(:checkbox, :title=>"Show Periodic Image", :tag=>"show_flag", :value=>(flag ? 1 : 0),
2351 :action=>lambda { |it| @mol.show_periodic_image = (it[:value] == 1 ? true : false) } ),
2353 item(:text, :title=>"a-axis"),
2354 item(:textfield, :width=>80, :tag=>"amin", :value=>pimage[0].to_s),
2355 item(:text, :title=>"to"),
2356 item(:textfield, :width=>80, :tag=>"amax", :value=>pimage[1].to_s),
2357 item(:text, :title=>"b-axis"),
2358 item(:textfield, :width=>80, :tag=>"bmin", :value=>pimage[2].to_s),
2359 item(:text, :title=>"to"),
2360 item(:textfield, :width=>80, :tag=>"bmax", :value=>pimage[3].to_s),
2361 item(:text, :title=>"c-axis"),
2362 item(:textfield, :width=>80, :tag=>"cmin", :value=>pimage[4].to_s),
2363 item(:text, :title=>"to"),
2364 item(:textfield, :width=>80, :tag=>"cmax", :value=>pimage[5].to_s),
2365 item(:button, :title=>"Set", :action=>:set_periodic_image))
2366 set_attr(0, :action=>lambda { |it| set_periodic_image(it); end_modal(it) } )
2370 def has_expanded_atoms
2371 atoms.find { |ap| ap.symop != nil }
2374 def cmd_remove_expanded_atoms
2376 return unless mol.has_expanded_atoms
2377 hash = Dialog.run("Remove Expanded Atoms") {
2379 item(:radio, :title=>"Remove all expanded atoms.", :tag=>"all", :value=>1),
2380 item(:radio, :title=>"Keep the expanded atoms that are in the same fragment with original atoms.", :tag=>"partial"))
2381 radio_group("all", "partial")
2383 if hash[:status] == 0
2385 rm = mol.atom_group { |ap| ap.symop != nil }
2388 mol.each_fragment { |f|
2389 if !f.find { |i| mol.atoms[i].symop == nil }
2400 require_cell = lambda { |m| m && m.cell != nil }
2402 register_menu("Xtal\tUnit Cell...", :cmd_unit_cell)
2403 register_menu("Xtal\tSymmetry Operations...", :cmd_symmetry_operations)
2404 register_menu("Xtal\t-", nil)
2405 register_menu("Xtal\tComplete by Symmetry", :complete_by_symmetry, lambda { |m| m && (m.cell != nil || m.nsymmetries > 1) } )
2406 register_menu("Xtal\tCreate Packing Diagram...", :create_packing_diagram, require_cell)
2407 register_menu("Xtal\tShow Periodic Image...", :cmd_show_periodic_image, require_cell)
2408 register_menu("Xtal\tRemove Expanded Atoms...", :cmd_remove_expanded_atoms, lambda { |m| m && m.has_expanded_atoms } )
2409 register_menu("Xtal\t-", nil)
2410 register_menu("Xtal\tBonds and Angles with Sigma...", :cmd_bond_angle_with_sigma, :non_empty)
2411 register_menu("Xtal\tBest-fit Planes...", :cmd_plane, :non_empty)
2412 register_menu("Xtal\tShow ORTEP...", :cmd_show_ortep, :non_empty)