Новосибирский институт органической химии им. Н.Н. Ворожцова СО РАН

Лаборатория изучения механизмов органических реакций

average


#!/usr/bin/perl -ws

our($h,$help, $auto, $i, $B, $T, $kcal, $kJ);
(my $program = $0) =~ s/^.*[\/\\]//;

if ($h || $help) {
  print <<HELP;

Usage: $program 'averagings' file1.xyz file2.xyz ... > multifile.xyz
       cat file1.xyz file2.xyz ... | $program 'averagings'  > multifile.xyz
       $program -i file1.xyz file2.xyz ...
       $program -B file1.xyz file2.xyz ... > file.xyz
Зависимости: perl, symmetry для опции -auto (опционально)

Усреднение свойств в молденовских файлах (например, хим. сдвигов в *.xyzppm)

averagings - перечисление групп  (через пробел) 
                          атомов (через запятую и/или дефис)
Если перед номером стоит буква A, то это водороды, связанные с этим атомом
Примеры:
'3-5,7-9 1,6,10' - усреднить хим. сдвиги атомов 3,4,5,7,8,9 и (отдельно) 1,6,10
A15-A17 - усреднить хим. сдвиги протонов, связанных с атомами 15,16,17

Если вместо 'averagings' будет -auto, то автоматически будут усреднены
хим. сдвиги протонов метилов и эквивалентных атомов (для последнего действия
требуется программа symmetry).

С опцией -i, если программе даны файлы в качестве аргументов, они будут 
отредактированы по месту.

С опцией -B будет усреднение хим. сдвигов по Больцману. Во 2-й строке xyz-файлов
  д.б. энергия. Нумерация атомов во всех xyz-файлах д.б. согласованной. 
  Напечатается геометрия самого стабильного конформера с хим. сдвигами, 
  представляющими собой взвешенное среднее по всем. Веса - мольные доли,
  вычисляются из энергий.
Опции, относящиеся к -B:
-T=298    Температура
       По умолчанию энергии в a.u. (hartree)
-kkal  Энергии в ккал/моль
-kJ    Энергии в кДж/моль

HELP
  exit;
}

if ($B) {
  $T ||= 298.15;
  my $factor = 627.5; # default energy unit is a.u. (hartree)
  if    ($kcal) {$factor = 1;}
  elsif ($kJ)   {$factor = 1/4.184}
  my @mols = @ARGV ? readmolden(@ARGV) : readmolden();
  my $mol = bolzman(\@mols, $T, $factor);
  writemolden($mol);
  exit;
}

if (! $auto && ! @ARGV ) {
  die "Нужно указать, что усреднять, либо задать опцию -auto. $program -h for help\n";
}

$replace = 1 if $i;

if ($auto) {
  
  $symmetry = '/usr/local/bin/symmetry';
  $symmetry = '' unless (-x $symmetry);
  #die "Program symmetry not found\n" unless (-x $symmetry);

  # Вгоняем таблицу Менделеева
  @ATOM = qw (XX
              H                                                  He
              Li Be                               B  C  N  O  F  Ne
              Na Mg                               Al Si P  S  Cl Ar
              K  Ca Sc Ti V  Cr Mn Fe Co Ni Cu Zn Ga Ge As Se Br Kr
              Rb Sr Y  Zr Nb Mo Tc Ru Rh Pd Ag Cd In Sn Sb Te I  Xe
              Cs Ba La
                      Ce Pr Nd Pm Sm Eu Gd Tb Dy Ho Er Tm Yb Lu
                       Hf Ta W  Re Os Ir Pt Au Hg Tl Pb Bi Po At Rn
              Fr Ra Ac
                      Th Pa U  Np Pu Am Cm Bk Cf Es Fm Md No Lr
             );
  for ($i=0; $i<@ATOM; $i++) { $ATOM{$ATOM[$i]} = $i };

  $replace = 0 unless @ARGV;
  
  while (&read_molden) {

    # Усредняем протоны метильных групп
    for ($i=1; $i<=$N; $i++) {
      if ( $atom[$i] eq 'C' ) {
        $CH[$i] = [];
        for ($j=1; $j<=$N; $j++) {
          if ( $atom[$j] eq 'H' &&
               ($x[$i]-$x[$j])**2+($y[$i]-$y[$j])**2+($z[$i]-$z[$j])**2 < 1.44
              ) {
            push @{$CH[$i]}, $j;
          }
        }
      }
    }
    
    for ($i=1; $i<=$N; $i++) {
      if ($atom[$i] eq 'C' && @{$CH[$i]}==3) {
        my $aver = ($ppm[$CH[$i][0]]+$ppm[$CH[$i][1]]+$ppm[$CH[$i][2]])/3;
        $ppm[$CH[$i][0]] = $ppm[$CH[$i][1]] = $ppm[$CH[$i][2]] = $aver;
      }
    }

    if ($symmetry) {
      open TINP, ">symm.TINP" || warn "Can't write temporary file: $!\n" , next;
      print TINP " $N\n";
      for ($i=1; $i<=$N; $i++) {
        print TINP " $ATOM{$atom[$i]}   $x[$i]  $y[$i]  $z[$i]\n";
      }
      close TINP;

      $generator = undef;
      my $prim = 0.05;
      my $final = 0.2;
      open OUT, '-|', "$symmetry -primary $prim -final $final symm.TINP" or
                      warn "Can't run $symmetry: $!\n", return undef;
      #open OUT, "$symmetry -final 0.009999 symm.TINP |";
      while (<OUT>) {
        if (/^Molecule has the following abelian symmetry generators: (\S+)/) {
          $generator = $1;
          last;
        }
      }
      while (<OUT>) {
        if (m/^Abelian symmetrized coordinates of all atoms$/) {
          <OUT>;
          $N = <OUT>; chomp $N;
          for ($i=1; $i<=$N; $i++) {
            ($natom,$x[$i],$y[$i],$z[$i]) = split ' ', <OUT>;
            $atom[$i] = $ATOM[$natom];
          }
        }
      }
      close OUT;
      unlink 'symm.TINP';

      @symm_atoms = symm_atoms($generator);
      SYMM_ATOMS:
      foreach (@symm_atoms) {
        #print "@{$_}\n";
        my $sum = 0;
        foreach (@{$_}) {
          next SYMM_ATOMS if $ppm[$_] eq '';
          $sum += $ppm[$_];
        }
        my $aver = $sum/@{$_};
        $ppm[shift @{$_}] = $aver;
        foreach (@{$_}) {
          $ppm[$_] = '';
        }
      }
    }
    
    &write_molden;
  }
  exit;
  
  sub symm_atoms {
    my $generator = shift;
    my @symm_atoms;
    unless ($generator) {
      @symm_atoms = map {[$_]} 1..$N;
      return @symm_atoms;
    }
    my %condition = (
      'z'        => '$x[$i]==$x[$j] && $y[$i]==$y[$j] && $z[$i]==-$z[$j]',
      'x,y'      => 'abs($x[$i])==abs($x[$j]) && 
                     abs($y[$i])==abs($y[$j]) && 
                     $z[$i]==$z[$j]',
      'xy'       => '$x[$i]==-$x[$j] && $y[$i]==-$y[$j] && $z[$i]==$z[$j]',
      'x,y,z'    => 'abs($x[$i])==abs($x[$j]) && 
                     abs($y[$i])==abs($y[$j]) && 
                     abs($z[$i])==abs($z[$j])',
      'xyz'      => '$x[$i]==-$x[$j] && $y[$i]==-$y[$j] && $z[$i]==-$z[$j]',
      'xy,z'     => '($x[$i]==-$x[$j] && $y[$i]==-$y[$j] && $z[$i]==$z[$j]) || 
                     ($x[$i]==$x[$j] && $y[$i]==$y[$j] && $z[$i]==-$z[$j])',
      'xy,xz,yz' => '($x[$i]==-$x[$j] && $y[$i]==-$y[$j] && $z[$i]==$z[$j]) ||
                     ($x[$i]==-$x[$j] && $y[$i]==$y[$j] && $z[$i]==-$z[$j]) ||
                     ($x[$i]==$x[$j] && $y[$i]==-$y[$j] && $z[$i]==-$z[$j])'
    );
    my @at = @atom;
    for ($i=1; $i<=$N; $i++) {
      next unless $at[$i];
      push @symm_atoms, [($i)];
      #$at[$i] = undef;
      for ($j=$i+1; $j<=$N; $j++) {
        next unless $at[$j];
        if (exists $condition{$generator} && eval "$condition{$generator}") {
            $at[$j] = undef;
            push @{$symm_atoms[-1]}, $j;
        }
      }
    }
    return @symm_atoms;
  }
  
}


$i = 0;
foreach my $av (split ' ', shift) {
  unless ($av =~ /^(A?\d+(-A?\d+)?,)*A?\d+(-A?\d+)?$/g) {
    die "Not valid numbers: $av\n";
  }

  foreach (split /,/, $av) {
    if (/^(\d+)-(\d+)$/) { # Раскрываем интервалы (i-n)
      my @a = $1<=$2 ? $1..$2 : reverse $2..$1;
      push @{$averagings[$i]}, @a;
    }
    elsif (/^A(\d+)-A(\d+)$/) { # Раскрываем интервалы (Ai-An)
      my @a = $1<=$2 ? $1..$2 : reverse $2..$1;
      @a = map {"A$_"} @a;
      push @{$averagings[$i]}, @a;
    }
    elsif (/^A(\d+)-(\d+)$/ || /^(\d+)-A(\d+)$/) { # Проверка
      die "Not valid numbers: $_\n";
    }
    else {push @{$averagings[$i]}, $_}
  }
  $i++; 
}

#for (@averagings) {print "@{$_}\n"}
#exit;

$replace = 0 unless @ARGV;

while (&read_molden) {
  foreach (@averagings) {
    my @a = @{$_};
    if ($a[0] =~ /^A/ ) {
      @a = map {s/^A//; $_} @a;
      #print "    a @a\n";
      undef @new_a;
      foreach $A (@a) {
        for ($i=1; $i<=$N; $i++) {
          if ($atom[$i] eq 'H' && $i != $A) {
            $r = ($x[$i]-$x[$A])**2 + ($y[$i]-$y[$A])**2 + ($z[$i]-$z[$A])**2;
            push @new_a, $i if $r < 1.44;
          }
        }
      }
      @a = @new_a;
      #print "new_a @a\n";
    }
    my $sum = 0;
    foreach (@a) {
      die "Not valid number @_\n" if $_>$N;
      $sum += $ppm[$_];
    }
    $sum /= $#a+1;
    $ppm[$_] = $sum foreach @a;
  }

  &write_molden;
}


sub read_molden {
  return undef if eof();
  # 1-st string
  if (<>=~/^\s*(\d+)\s*$/) {
    ($N,$energy,@atom,@x,@y,@z,@ppm) = undef;
    $N = $1;
  } else {
    return undef;
  }
  # 2-nd string
  ($energy) = <>=~/(-?\d+\.\d+)/;
  # Geometry
  for ($i=1;$i<=$N;$i++) {
    ($atom[$i],$x[$i],$y[$i],$z[$i],$ppm[$i],undef) = split ' ', <>;
    return undef unless $atom[$i]=~/^\w\w?/ &&
                        $x[$i]=~/^-?\d+\.\d+/ &&
                        $y[$i]=~/^-?\d+\.\d+/ &&
                        $z[$i]=~/^-?\d+\.\d+/;
  }
  return $N;
}

sub write_molden {
  if (@atom && @x && @y && @z) {
    if ($replace) {
      open F, '>', "$ARGV.TMP" or die "Can't open $ARGV.TMP: $!\n";
      select F;
    }
    print " $#atom\n";
    print " Energy $energy" if $energy;
    print "\n";
    for ($i=1; $i<=$#atom; $i++) {
      if ($atom[$i] && $x[$i] && $y[$i] && $z[$i]) {
        printf " %-2s %12.8f %12.8f %12.8f",$atom[$i],$x[$i],$y[$i],$z[$i];
        printf $atom[$i] eq 'H' ? " %10.3f" : " %9.2f"  , $ppm[$i] if $ppm[$i];
        print "\n";
      }
    }
    if ($replace) {
      close F;
      rename "$ARGV.TMP", $ARGV or die "Can't rename $ARGV.TMP: $!\n";
    }
  }
}

sub bolzman {
  my @mols = @{$_[0]};
  my $T = $_[1] || 298.15;
  my $factor = $_[2] || 627.5;
  my $R = 1.987;
  @mols = grep {defined($_->[0]{Energy})} @mols;
  my $mol = $mols[0];
  foreach (@mols) {
    $mol = $_ if $_->[0]{Energy} < $mol->[0]{Energy};
  }
  $mol = copy_mol($mol);
  for (my $i=1; $i<@$mol; $i++) {
    undef $mol->[$i][4];
  }
  my @gamma = map {exp(-($_->[0]{Energy}-$mol->[0]{Energy})*$factor*1000/$R/$T)} @mols;
  my $sum = 0;
  $sum += $_ foreach @gamma;
  @gamma = map {$_/$sum} @gamma;
  #print "@gamma\n";
  ATOM:
  for (my $i=1; $i<@$mol; $i++) { # by atoms
    my $ppm = 0;
    for (my $j=0; $j<@mols; $j++) { # by mols
      next ATOM unless defined($mols[$j][$i][4]) && $mols[$j][$i][4]=~/^-?\d+(?:\.\d+)?$/;
      $ppm += $gamma[$j]*$mols[$j][$i][4];
    }
    $mol->[$i][4] = $ppm;
  }
  return $mol;
}

sub readmolden {
 ############################################################################
 ## Молекула -- ссылка на массив, 0-й элемент -- свойства, следующие - атомы.
 ## Свойства -- ссылка на хэш с ключами Energy, Symmetry
 ## Атом -- ссылка на массив [atom, x, y, z, ppm] (ppm может не быть).
 ##
 ## Читает xyz. Параметры - имена xyz-файлов. Если параметров нет, то <>.
 ## Возвращает массив найденных молекул.
 ############################################################################
  local @ARGV = @_ ? @_ : @ARGV;
  my $num = qr/-?\d+(?:\.\d+)?/;
  my @mols;
  my $line;
  LOOP:
  while ($line || defined($line = <>)) {
    #print $line;
    if ($line =~/^\s*(\d+)\s*$/) {
      my @mol;
      my $N = $1;
      last LOOP if eof();
      next LOOP if eof(ARGV);
      $line = <>;
      ($mol[0]{Energy}) = $line =~ /($num)/o;
      ($mol[0]{Symmetry}) = $line =~ /symm\S*\s+(\S+)/i;
      for (my $i=1; $i<=$N; $i++) {
        last LOOP if eof();
        next LOOP if eof(ARGV);
        $line = <>;
        #print $line;
        if ($line =~ /^\s*([A-Z]{1,2})\s+($num)\s+($num)\s+($num)\s*(.*)/io) {
          $mol[$i] = [$1,$2,$3,$4,$5];
          if ($mol[$i][4]=~/^($num)\s*(.*)/io) {
            $mol[$i][4] = $1;
            $mol[$i][5] = $2 if $2;
          }
        } else {
          next LOOP;
        }
      }
      push @mols, \@mol;
      last LOOP if eof();
    } else {
      undef $line;
    }
  }
  return @mols;
}

sub writemolden {
  foreach my $mol (@_) {;
    my $N = $#{$mol};
    print " $N\n";
    print " Energy $mol->[0]{Energy}" if $mol->[0]{Energy};
    print "  Point $mol->[0]{Point}" if $mol->[0]{Point};
    print "  Symmetry $mol->[0]{Symmetry}" if $mol->[0]{Symmetry};
    print "  Ellips $mol->[0]{Ellips}" if $mol->[0]{Ellips};
    print "\n";
    for (my $i=1; $i<=$N; $i++) {
      printf " %-2s %12.8f %12.8f %12.8f", $mol->[$i][0],$mol->[$i][1],$mol->[$i][2],$mol->[$i][3];
      printf " %8.2f", $mol->[$i][4] if defined($mol->[$i][4]) && $mol->[$i][4]=~/-?\d+(?:\.\d+)?/;
      print "  $mol->[$i][5]" if defined($mol->[$i][5]);
      print "\n";
    }
  }
}

sub copy_mol {
  my $mol = shift;
  my $new_mol;
  my $N = $#{$mol};
  $new_mol->[0] = $mol->[0] if $mol->[0];
  for (my $i=1; $i<=$N; $i++) {
    $new_mol->[$i] = [@{$mol->[$i]}];
  }
  return $new_mol;
}