File Coverage

blib/lib/Chemistry/Harmonia.pm
Criterion Covered Total %
statement 832 848 98.1
branch 283 324 87.3
condition 84 116 72.4
subroutine 31 31 100.0
pod 8 9 88.8
total 1238 1328 93.2


line stmt bran cond sub pod time code
1             package Chemistry::Harmonia;
2              
3 8     8   311519 use 5.008008;
  8         30  
  8         523  
4 8     8   73 use strict;
  8         17  
  8         1493  
5 8     8   45 use warnings;
  8         50  
  8         322  
6              
7 8     8   125 use Carp;
  8         15  
  8         975  
8 8     8   18284 use Chemistry::File::Formula;
  8         7651042  
  8         115  
9 8     8   21519 use Algorithm::Combinatorics qw( variations_with_repetition subsets );
  8         1186969  
  8         819  
10 8     8   9328 use String::CRC::Cksum qw( cksum );
  8         925482  
  8         717  
11 8     8   19574 use Math::BigInt qw( blcm bgcd );
  8         2510123  
  8         50  
12 8     8   194738 use Math::BigRat;
  8         417543  
  8         68  
13 8     8   22811 use Math::Assistant qw(:algebra);
  8         25990  
  8         1307  
14              
15 8     8   19406 use Data::Dumper;
  8         72302  
  8         1557  
16              
17              
18             require Exporter;
19              
20             our @ISA = qw(Exporter);
21              
22             our %EXPORT_TAGS = ( 'all' => [ qw(
23             parse_chem_mix
24             prepare_mix
25             oxidation_state
26             redox_test
27             class_cir_brutto
28             good_formula
29             brutto_formula
30             stoichiometry
31             ttc_reaction
32             ) ],
33             'redox' => [ qw(
34             parse_chem_mix
35             prepare_mix
36             oxidation_state
37             redox_test
38             ) ],
39             'equation' => [ qw(
40             parse_chem_mix
41             prepare_mix
42             class_cir_brutto
43             good_formula
44             brutto_formula
45             stoichiometry
46             ttc_reaction
47             ) ],
48             );
49              
50             our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
51              
52             our @EXPORT = qw( );
53              
54             our $VERSION = '0.118';
55              
56 8     8   159 use base qw(Exporter);
  8         13  
  8         118165  
57              
58              
59             # Returns array of "good" chemical formulas
60             sub good_formula{
61 46   50 46 1 46952 my $substance = shift || return;
62 46         80 my $opts = shift;
63              
64 46         87 for( $substance ){
65 46         71 my $m = '(?:1|!|\|)'; # mask
66              
67             # Replacement by iodine
68 46         441 s/^(\W*)$m(\d*)$/$1I$2/;
69 46         106 s/j/I/ig;
70              
71 46         86 s/\$/s/g; # for 'S' or 's'
72              
73 46         344 s/((?:^|[^BCGLNPRT])A)$m/$1l/ig; # for 'Al'
74 46         280 s/((?:^|[^ACT])L)$m/$1i/ig; # for 'Li'
75              
76             # Is it 'o' or oxigen?
77 46         198 1 while s/(?<=[CHNP])0(?![,.]\d)/\$/g; # temporarilly to replace --> '$'
78 46         165 1 while s/(? 'o'
79              
80 46         298 s/(?<=[ACT])$m(?=\D|$)/l/ig; # for Al, Cl, Tl
81 46         258 s/(?<=[BLNS])$m(?=\D|$)/i/ig; # for Bi, Li, Ni, Si (without Ti)
82              
83 46         78 s/Q/O/g; # for oxigen
84 46         208 s/(?<=[AHMRS])q/g/ig; # for Aq, Hq, Mq, Rq, Sq
85             }
86              
87 46         68 my(@maU, @maUl);
88 0         0 my %sa; # Letters of possible atoms
89              
90 46         107 my $adb = &_atoms_db;
91              
92             # Possible atoms of substance
93 46         347 for( my $i = 0; $i < @$adb; $i+=5 ){
94 5382         6836 $_ = $adb->[$i];
95              
96 5382 100       47255 next if $substance !~ /$_/i;
97              
98 181 100       338 if( length > 1 ){
99 78         131 push @maUl, $_; # Double chem.symbol: 1th - CAPITAL, 2th - small
100              
101             }else{
102 103         195 push @maU, $_; # Unary chem.symbol
103             }
104 181         1185 $sa{ uc $_ } = '' for split //; # String of CAPITAL letters
105             }
106              
107             # All atom letters (CAPITAL) || return 0
108 46   50     232 my $sas = join('', keys %sa) || return;
109              
110 46         83 my $mz = '(?
111              
112 46         83 for( $substance ){
113 46         3567 s/,/./g;
114 46         2070 s/[^0-9\$\.\*\(\)\[\]\{\}$sas]+//ig; # to clean bad symbols
115              
116 46         267 s/\.+$//; # to clean points
117 46         268 s/(?
118              
119             # to clean sequences not unary symbols
120 46   100     1423 while( @maU && $sas =~ /([^@maU])/gx ){
121 42         273 my $ul = $1;
122 42         1374 s/($ul)$ul+($ul)/$1$2/ig;
123             }
124              
125             # remove bordering brackets
126 46         85 our( $maskr, $maskf, $maskq );
127 46         315 $maskr = qr/\((?:(?>[^\(\)]+)|(??{$maskr}))*\)/;
128 46         202 $maskf = qr/{(?:(?>[^{}]+)|(??{$maskf}))*}/;
129 46         213 $maskq = qr/\[(?:(?>[^\[\]]+)|(??{$maskq}))*\]/;
130              
131 46   100     3678 s/^.|.$//g while /($maskr|$maskf|$maskq)/g && $_ eq $1;
132              
133             # to do small the last symbol of double
134 46         188 s/([AEGLMR])(?=[^a-z]|$)/\l$1/g;
135              
136             # Double symbol similar unary with coefficient in the end
137 46 100 100     886 if( /^@maUl\d+$/i && @maU && /^(?i:[@maU])\L[@maU]\d+$/ ){
      100        
138 3         17 my @cf = ( "\u$_", uc );
139              
140 3 100 100     32 if( /$mz/ && exists( $opts->{'zero2oxi'} ) ){
141 1         4 my @a = @cf;
142 1         15 s/$mz/O/g for @a;
143 1         50 return [ @cf, @a ];
144             }
145 2         92 return \@cf;
146             }
147              
148             # Normalization of oxide-coated formulas (written through '*')
149 43 100       130 if(/\*/){
150 4         7 my %k;
151 4         9 my $l = 1;
152 4         18 for my $i (split /\*/){
153 8 100       138 if($i =~ /^(\d+)[a-zA-Z]/){ # Integer coefficient
    100          
154 1         6 $k{ $1 } = $1;
155              
156             }elsif($i =~ /^(\d*\.(\d+))/){ # Fractional coefficient
157 2         56 $k{ $1 } = $1;
158 2 100       12 $l *= 10 if length $2 >= length $l;
159              
160             }else{ # without coefficient (=1)
161 5         17 $k{ '' } = 1;
162             }
163             }
164              
165 4 100       15 if( $l > 1 ){
166 1         8 $k{ $_ } *= $l for keys %k;
167             }
168             # by GCD reduce order of coefficients
169 4         32 my $gcd = bgcd(values %k);
170              
171 4 100       743 if( $gcd > 1 ){
172 1         118 $k{ $_ } /= $gcd for keys %k;
173             }
174              
175 4         559 my $f;
176 4         15 for my $i (split /\*/){
177 8 100       47 $i =~ s/^(\d*\.?\d*)([\w\(\)\[\]\{\}]+)$/$k{$1}>1?"{$2}".$k{$1}:"{$2}"/e;
  8         39  
178 8         211 $f .= $i;
179             }
180 4         25 $_ = $f;
181             }
182              
183 43         218 1 while s/[\(\{\[][\)\}\]]//g; # clean empty brackets
184             }
185              
186 43         100 my $cf = [''];
187 43         352 for my $s (split /((?:\$|\d|\(|\)|\[|\]|\{|\})+)/,$substance){
188              
189 127 100 100     1431 if( $s =~ /[$sas]/i ){ # Atom symbols
    100          
    100          
190 67         206 &_in_gf($s, \@maU, \@maUl, $cf);
191              
192             }elsif( $s =~ /\$/ ){ # Zero symbols: 'o' && 'O'
193 4         15 $s =~ s/^\$//;
194 4         22 &_add_zero( $cf, "O$s", "o$s" ); # add oxigen to { C H N P } -> { CO HO NO PO }
195             # add 'o' to { C H N P } -> { Co Ho No Po }
196              
197             }elsif($s =~ /$mz/ && exists( $opts->{'zero2oxi'} ) ){ # Zero symbols
198 4         9 my $z = $s; # zero
199 4         25 $s =~ s/$mz/O/g; # oxigen
200              
201 4         15 &_add_zero( $cf, $s, $z );
202              
203             }else{ # Others
204 52         212 $_ .= $s for @$cf;
205             }
206             }
207              
208 43 50       2475 $cf->[0] =~ /\w/ ? return $cf : return;
209             }
210              
211             sub _add_zero{
212 8     8   15 my( $cf, $oxi, $z ) = @_;
213 8         12 my $n = $#{$cf};
  8         19  
214              
215 8         33 for( my $i = 0; $i <= $n; $i++ ){
216 8         30 $cf->[$i + $n + 1] = $cf->[$i].$oxi; # add oxigen to { C H N P } -> { CO HO NO PO }
217 8         65 $cf->[$i] .= $z; # add 'o' to { C H N P } -> { Co Ho No Po }
218             }
219             }
220              
221             sub _in_gf{
222 25770     25770   43778 my($s, $maU, $maUl, $cf) = @_;
223              
224             # Unary symbols of fragment
225 25770         497909 my @i_maU = grep $s =~ /$_/i, @$maU;
226              
227 25770 100       75510 if( length($s) == 1 ){ # Unary atom symbol
228 9167 100       31288 @i_maU || return;
229              
230 9155         31516 $_ .= $i_maU[0] for @$cf;
231 9155         35250 return 1;
232             }
233              
234             # Double symbols of fragment
235 16603 100 100     297934 ( my @i_maUl = grep $s =~ /$_/i, @$maUl ) || @i_maU || return; # not present
236              
237             # Strings of unary and double atom letters
238 16565         31510 my $saU = join '', @i_maU; # String of unary atom letters
239 16565         24796 my $saUl = join '|', @i_maUl; # 1th - CAPITAL, 2th - small
240              
241 16565         17886 my @w; # Anchor symbols
242              
243             # Unique unary symbols (is not present in double)
244 16565         27228 for( @i_maU ){
245 34405 100       276930 next if $saUl =~ /$_/i;
246 2834         23097 $s =~ s/\L$_/$_/g; # to CAPITAL
247 2834         7135 push @w, $_;
248             }
249              
250             # Unique double (is not present in others)
251 16565         28309 for my $ul (@i_maUl){
252 24919         211114 (my $m = $saUl) =~ s/$ul//; # To copy and clean the tested
253              
254 24919         62814 for(split //,$ul){
255 46713 100 100     644296 next if $m =~ /$_/i || $saU =~ /$_/i;
256              
257 5910         49725 $s =~ s/$ul/$ul/ig;
258 5910         9826 push @w, $ul;
259 5910         16083 last;
260             }
261             }
262              
263 16565         27378 $_ = $s;
264              
265 16565 100       35160 if( length == 2){ # Two symbols
266              
267 9367         47981 (my $salU = $saUl) =~ s/(\w+)/\l\U$1/g; # 1th - small, 2th - CAPITAL
268              
269             # The order is important!!!
270 9367         12622 my @in_cf;
271 9367 100 100     305716 if( /$saUl/ || /[$saU]{2}/ ){ # Usual writing of atom: 1th - CAPITAL, 2th - small
    50 66        
    100 33        
    50          
    100          
272             # or both - CAPITAL unary letters
273 4561         7094 push @in_cf, $_;
274              
275             }elsif( /\U$saUl/ ){ # 1th and 2th - CAPITAL letters of double symbol
276 0         0 push @in_cf, ucfirst lc;
277              
278             }elsif( @i_maU && ( /[$saU][\L$saU\E]/ || /[\L$saU\E][$saU]/ ) ){ # For unary symbols:
279             # 1th - CAPITAL, 2th - small or 1th - small, 2th - CAPITAL letter
280 1         3 push @in_cf, uc;
281              
282             }elsif( /$salU/ ){ # 1th - small, 2th - CAPITAL letter of double symbol
283 0         0 push @in_cf, ucfirst lc;
284              
285             }elsif( length($saUl) == 0 ){ # Without double symbol
286 1         22 s/.?([$saU]+).?/$1/i; # unary symbol(s)
287 1         5 push @in_cf, uc;
288              
289             }else{ # 1th and 2th small letters - two alternative variants
290 4804 50       50253 if( /[\L$saU]{2}/ ){ # unary symbols
291 4804         12854 push @in_cf, uc;
292             }
293 4804 50       38930 if( /\L$saUl/ ){ # double symbol
294 4804         10398 push @in_cf, ucfirst;
295             }
296              
297 4804 50       11274 @in_cf || return;
298              
299 4804 50       9900 if( @in_cf == 2 ){
300              
301 4804         5099 my $n = $#{$cf};
  4804         7779  
302 4804         10891 for( my $i = 0; $i <= $n; $i++ ){
303 13473         31473 $cf->[$i + $n + 1] = $cf->[$i].$in_cf[1];
304 13473         33419 $cf->[$i] .= $in_cf[0];
305             }
306 4804         43532 return 1;
307             }
308             }
309              
310 4563         14056 $_ .= $in_cf[0] for @$cf;
311 4563         22509 return 1;
312             }
313              
314             # More 2 symbols
315              
316 7198         10068 for( $s ){
317 7198         15143 my $m = join '|', $saUl, @i_maU;
318              
319 7198         237117 while( ! /^(?:$m)/i ){ s/^.// } # Clear head
  5         45  
320 7198         253538 while( ! /(?:$m)$/i ){ s/.$// } # Clear tail
  31         304  
321              
322             # Search double and unary symbols
323 7198         18456 my $z = '';
324 7198         202829 while( /^((?:$m)+)/g ){ $z .= $1 }
  2222         15321  
325              
326 7198 100       29031 if( length $z ){
327              
328 2218         7054 $_ .= $z for @$cf;
329 2218 100       8049 return 1 if length($z) == length; # The end
330              
331 1738         14163 s/^$z//;
332 1738 100       7421 return &_in_gf($_, \@i_maU, \@i_maUl, $cf) if 3 > length;
333             }
334             }
335              
336 5749 100       14411 if( @w ){ # Search by fragments of anchor symbols
337 1802         3159 my $m = join '|', @w;
338              
339 1802         19498 for my $f (split /($m)/, $s){
340              
341 2926         7558 &_in_gf($f, \@i_maU, \@i_maUl, $cf);
342              
343 2926 100       4514 if( $#{$cf} ){ # For >1 formula
  2926         8004  
344             # To leave the longest fragments
345 2058         7371 for( my $i = $#$cf; $i > 0; $i-- ){
346 8072 50       28581 splice @$cf, $i, 1 if length( $cf->[$i] ) < length( $cf->[0] );
347             }
348             }
349             }
350 1802         14753 return 1;
351             }
352              
353             # Difficult fragment
354 3947         9290 my $cf_copy = [ @$cf ];
355 3947         4315 $#{$cf} = -1; # Reset
  3947         11640  
356              
357             _M_gf_1: # Letter by letter search
358 3947         10534 for( my $i = 1; $i < length($s); $i++ ){
359              
360 10907         23295 my $cf_new = [ @$cf_copy ];
361              
362 10907         474258 for( $s =~ /^(\w{$i})(\w+)/ ){
363 21808 100       49475 &_in_gf($_, \@i_maU, \@i_maUl, $cf_new) || next _M_gf_1;
364             }
365             # To accumulate only unique, big fragments
366 10895         29057 for my $k ( @$cf_new ){
367 47611 100 100     1368674 scalar( grep length($_) > length($k) || $_ eq $k, @$cf ) || push @$cf, $k;
368             }
369             }
370 3947         19526 return 1;
371             }
372              
373              
374             # Tactico-technical characteristics of chemical reaction.
375             # Return quantity hash (SAR):
376             # 's'ubstance
377             # 'a'toms
378             # 'r'ank
379             sub ttc_reaction{
380 2   50 2 1 15 my $ce = shift || return;
381            
382             # Hash atom matrix and quantity of atoms
383 2         9 my( $atoms_substance, $atoms ) = &_search_atoms_subs( $ce );
384              
385 2   50     20 my $rank = Rank( [ values %$atoms_substance ] ) || return;
386              
387 2         19894 return { 's' => scalar( keys %$atoms_substance ),
388             'a' => $atoms,
389             'r' => $rank,
390             };
391             }
392              
393             # Calculation common identifier for reaction:
394             # Alphabetic CLASS
395             # and
396             # Chemical Interger Reaction (CIR) identifier
397             # also
398             # brutto (gross) formulas of substances
399             sub class_cir_brutto{
400 66     66 1 120 my( $ce, $coef ) = @_;
401 66 50       142 $ce || return;
402              
403 66         81 my( %elm, %bf, @cir );
404              
405 66         141 for my $c ( @$ce ){
406 132         205 for my $s ( @$c ){
407              
408             # stoichiometry coefficient
409 358 50       884 my $k = exists $coef->{$s} ? $coef->{$s} : 1;
410              
411 358         1143 my %e = Chemistry::File::Formula->parse_formula( $s );
412              
413             # for brutto
414 358         58248 $bf{ $s } = join '', map( "$_$e{$_}", sort{ $a cmp $b } keys %e );
  687         2476  
415              
416             # for cir
417 358         991 push @cir, $k.$bf{ $s };
418              
419             # For class
420 358         1520 @elm{ keys %e } = ''; # reaction atoms
421             }
422             }
423              
424             # CLASS, CIR of reaction and hash: formula => brutto of substances
425 520         691 return [ join( '', sort { $a cmp $b } keys %elm ),
  622         956  
426 66         195 ( cksum( join '+', sort { $a cmp $b } @cir ) )[0],
427             \%bf ];
428             }
429              
430              
431             # Transformation substance arrays to chemical mix (equation)
432             # $ce -- ref to array of substance arrays
433             # $opts ->{ } (facultative parameters)
434             # ->'substances' -- ref to array of real (required) substances
435             # ->'coefficients' -- ref to hash stoichiometry coefficients for substances
436             # ->'norma' -- chemical mix to brutto-normalize
437             sub prepare_mix{
438 829     829 1 1198 my( $ce, $opts ) = @_;
439              
440             # Check empty reactants & products
441 1658         4695 croak('Bad equation!') if @$ce != 2 ||
442             scalar( grep ! defined, @$ce ) ||
443 829 50 50     4733 scalar( grep { grep ! defined, @{$_} } @$ce );
  1658   50     1613  
444              
445 829         1110 my $sign = 0;
446             # Any coefficient isn't set --> to bypass
447 829 100 66     2273 if( exists( $opts->{'norma'} ) &&
  98         639  
448             scalar( grep {
449 98         109 grep ! exists( $opts->{'coefficients'}{$_} ), @{$_}
450             } @$ce ) == 0 ){ # All coefficients are set
451              
452             # Test stoichiometry coefficient sign on initial (first) substances
453 49         71 $sign = 1;
454 49         60 for( @{ $ce->[0] } ){
  49         123  
455 55 100       339 $opts->{'coefficients'}{$_} || next; # Zero stoichiometry coefficient
456 49 100       1115 $sign = -1 if $opts->{'coefficients'}{$_} < 0;
457 49         5932 last;
458             }
459             }
460              
461 829 100       1952 if( $sign != 0 ){ # chemical mix to brutto-normalize
462              
463             # if assume all coefficients positive '+'
464 49 50       127 if( $opts->{'norma'} != 1 ){
465 0         0 $opts->{'coefficients'}{$_} *= -1 for @{ $ce->[1] };
  0         0  
466             }
467              
468 49         97 my @cir; # for brutto-formula
469 49         78 while( my($s, $c) = each %{ $opts->{'coefficients'} } ){
  306         2902  
470 257 100       755 $c || next;
471 234 100       6529 my $ip = $sign * $c > 0 ? 0 : 1;
472              
473 234         55555 my $ff = abs($c)." $s";
474              
475 234         11761 my %e = Chemistry::File::Formula->parse_formula( $ff );
476 234         45258 $cir[$ip]{$ff} = join '', map( "$_$e{$_}", sort{ $a cmp $b} keys %e );
  431         2338  
477             }
478             # Result
479 49         104 $_ = join(' == ', map{ join ' + ',sort {$_->{$a} cmp $_->{$b}} keys %{$_} } @cir);
  98         134  
  194         682  
  98         313  
480              
481             }else{
482 780 100 66     5577 if( exists( $opts->{'substances'} ) && @{ $opts->{'substances'} } ){
  779         3272  
483             # List of real substances is specified
484 779         869 my %bs;
485 779         1024 @bs{ @{ $opts->{'substances'} } } = ();
  779         2682  
486              
487 779         1233 $_ = join(' == ', map{ join ' + ', map{
488 5642 100       9290 if( exists $bs{$_} ){
  1558         2162  
489 2928 100       10779 exists( $opts->{'coefficients'}{$_} ) ? abs( $opts->{'coefficients'}{$_} )." $_" : $_
490             }else{
491             ( )
492 2714         4886 }
493 1558         1662 } @{$_} } @$ce );
494              
495             }else{ # List isn't specified
496 1 100       3 $_ = join(' == ', map{ join ' + ', map{
  6         32  
497 2         3 exists( $opts->{'coefficients'}{$_} ) ? abs( $opts->{'coefficients'}{$_} )." $_" : $_ } @{$_} } @$ce );
  2         4  
498             }
499             }
500              
501 829         6462 s/^\s*==|==\s*$//g;
502 829 50       6912 /\S\s*[=+]+\s*\S/ ? $_ : return;
503             }
504              
505              
506             # Расчёт (поиск) стехиометрических коэффициентов
507             # Возвращает :
508             # решение в $cheq
509             # undef --- нет решения
510             sub stoichiometry{
511 16     16 1 16996 my($mix, $opts) = @_;
512 16 100       58 $opts->{'redox_pairs'} = 1 unless exists $opts->{'redox_pairs'}; # вкл. redox-уравниватель
513              
514 16         23 my @cheq; # возвращаемые уравнения
515              
516 16         51 &_in_search_stoich($mix, \@cheq, $opts ); # Рекурсивный поиск
517              
518 16 50       113 @cheq ? \@cheq : return undef;
519             }
520              
521              
522             # Рекурсивный поиск стехиометрических коэффициентов
523             sub _in_search_stoich{
524 1389     1389   2530 my($mix, $cheq, $opts) = @_;
525              
526 1389         2164 my %coef; # на случай заданных коэф. в $mix
527 1389         3224 my $chem_eq = parse_chem_mix($mix, \%coef);
528              
529 1389 50       3698 croak('Bad equation!') if @$chem_eq != 2;
530              
531             # Хеш Атомной матрицы и кол-во атомов
532 1389         2936 my ( $atoms_substance, $num_atoms ) = &_search_atoms_subs( $chem_eq );
533              
534             # Список Всех веществ и копия атомной матрицы (для проверки баланса в конце)
535 672         2684 my $All_substances = [ keys %$atoms_substance ];
536 672         1420 my $ini_am = { map{ $_, [ @{ $atoms_substance->{$_} } ] } @$All_substances };
  4558         4421  
  4558         4271059  
537              
538 672         2709 my $R_free = [ (0) x $num_atoms ]; # R-вектор свободных членов (правая часть)
539 672         1429 my $R_zero = 1; # Признак нулевого R-вектора
540              
541 672         2449 while( my($s, $c) = each %coef ){ # В-во и стех.коэф.
542 2 50       12 $opts->{'coefficients'}{$s} = $c unless exists( $opts->{'coefficients'}{$s} );
543             }
544              
545             # Если заданы стех.коэффициенты
546 672 100 100     2299 if( exists $opts->{'coefficients'} && keys %{ $opts->{'coefficients'} } ){
  662         3422  
547              
548 597         1471 my $sign = 1; # знак переноса вправо по исходным в-вам
549              
550 597         1126 for my $ip ( @$chem_eq ){
551 1194         1610 for my $s ( @$ip ){
552              
553 4176 100       10309 if( exists $opts->{'coefficients'}{$s} ){
554              
555             # устанавливаем знак-признак исх./продукт
556 2875         6685 $opts->{'coefficients'}{$s} = $sign * abs( $opts->{'coefficients'}{$s} );
557              
558 2875         2859 my $i;
559             # формируем Вектор правой части СЛАУ
560 2875         19099 $R_free->[$i++] -= $_ * $opts->{'coefficients'}{$s}
561 2875         2762 for @{ $atoms_substance->{$s} };
562             # удаляем вещество после переноса вправо
563 2875         7844 delete $atoms_substance->{$s};
564             }
565             }
566 1194         2081 $sign = -1; # знак переноса вправо по продуктам
567             }
568              
569             # Поиск полностью нулевых атомных строк
570 597         1582 my @sum_atom = (0) x $num_atoms;
571 597         703 for my $s ( keys %{ $atoms_substance} ) {
  597         1342  
572 1301         1383 my $i;
573 1301         1239 $sum_atom[$i++] += $_ for @{ $atoms_substance->{$s} };
  1301         6862  
574             }
575              
576 597         920 my @atom_del; # удаляемые 0-строки
577 597         1755 for( my $i = $#sum_atom; $i >= 0; $i--){ # Обратный отсчёт
578              
579 1864 100       3757 unless( $sum_atom[$i] ){ # Все атомы нули
580 590 100       8899 return if $R_free->[$i]; # Нет баланса, нет решения
581 52         118 push @atom_del, $i;
582             }
583              
584 1326 100       5699 $R_zero = 0 if $R_free->[$i]; # R-вектор не 0
585             }
586              
587             # Удаление 0-х атомных строк
588 59         106 for my $i ( @atom_del ){
589 52         167 splice @$R_free, $i, 1;
590              
591 52         92 for( values %{ $atoms_substance} ) {
  52         126  
592 113         141 splice @{ $_ }, $i, 1;
  113         288  
593             }
594             }
595              
596             # Уменьшаем число атомов
597 59 50       217 unless( $num_atoms -= @atom_del ){
598              
599             # Заданы все правильные стех.коэф.
600 0         0 push @$cheq, prepare_mix( $chem_eq,
601             { 'norma' => 1, 'coefficients' => $opts->{'coefficients'} } );
602 0         0 return;
603             }
604              
605             # Одно в-во без стех.коэф.
606 59 50       372 if( (my @v = values %$atoms_substance) == 1 ){
607              
608             # Должна быть пропорция между атомами в в-ве и правом векторе
609 0         0 for( my $i = 0; $i < $#{ $v[0] }; $i++){
  0         0  
610 0 0       0 return if $v[0]->[$i] * $R_free->[$i+1] != $v[0]->[$i+1] * $R_free->[$i]
611             }
612             }
613             }
614              
615             # Список искомых веществ
616 134         688 my $substances_X = [ keys %$atoms_substance ];
617              
618 134 50 33     554 return if @$substances_X < 2 && $R_zero; # 0|1 вещество и не заданы коэф.
619              
620             # Ранг матрицы --- кол-ва независимых строк, столбцов
621 134         1113 my $order = Rank( [ values %$atoms_substance ] );
622              
623             # print "$mix\n";
624             # print "Atoms = $num_atoms
625             #Substances = ",scalar(keys %$atoms_substance),
626             #"\nRank = $order\n--------\n";
627              
628             # не задан R-вектор (коэффициенты) и ранг == веществам
629 134 100 100     370568 return if $R_zero && $order == @$substances_X; # нет решения
630              
631 117 100       468 if( @$substances_X - $order > 1 ){ # Веществ больше Ранга на 2 и более
632              
633             # Ограничить рекурсию самым Верхним!!! уровнем перебора
634 12 100       135 return if @$cheq; # || @$substances_X < 3;
635              
636 6 100 100     35 if( $opts->{'redox_pairs'} && ( my $redox = &redox_test($chem_eq) ) ){
637             # Проверить реакцию на ОВР
638             # if( my $redox = &redox_test($chem_eq) ){ # Это ОВР
639              
640 2         15 my @nm_rx = sort keys %$redox; # 0='oxidant' и 1='reducer'
641 2         4 my %pairs;
642              
643 2         3 for my $nm ( @nm_rx ){
644 4         5 for my $e ( keys %{ $redox->{$nm} } ){ # Элементы
  4         11  
645             # Исходное->Конечное состояние элемента
646 6         8 for my $i_k ( keys %{ $redox->{$nm}{$e} } ){
  6         12  
647 8         7 push @{ $pairs{$nm} }, $i_k;
  8         29  
648             }
649             }
650             }
651              
652             # print Dumper \%pairs;
653              
654 2         14 my $iter_oxi = subsets( $pairs{ $nm_rx[0] }); # можно задать ,1 - один окислитель
655              
656 2         87 while(my $pair_oxi = $iter_oxi->next){ # Набор пар
657 12 100       297 next unless @$pair_oxi; # убрать пустые (фича subsets)
658              
659 10         18 my $sum_oxi_e = 0; # сумма e- нов принимаемых окислителями
660              
661 10         16 for my $e ( keys %{ $redox->{ $nm_rx[0] } } ){ # Элементы окислителя
  10         32  
662 20         306 for my $i_k ( @{ $pair_oxi } ){
  20         37  
663 32 100       938 $sum_oxi_e += $redox->{ $nm_rx[0] }{$e}{$i_k}[0] if exists( $redox->{ $nm_rx[0] }{$e}{$i_k} );
664             }
665             }
666              
667             # 1 - один восстановитель (надо настраивать!!!)
668 10         763 my $iter_red = subsets( $pairs{ $nm_rx[1] } );
669              
670 10         371 while(my $pair_red = $iter_red->next){ # Набор пар
671 34 50       2087 next if $sum_oxi_e < @{ $pair_red }; # принимаемых e- нов меньше восст-лей
  34         129  
672              
673 34         3301 my $sum_red_e = 0; # сумма e- нов отдаваемых восстановителями
674              
675 34         53 for my $e ( keys %{ $redox->{ $nm_rx[1] } } ){ # Элементы восстановителя
  34         132  
676 34         47 for my $i_k ( @{ $pair_red } ){
  34         82  
677 31 50       1392 $sum_red_e += $redox->{ $nm_rx[1] }{$e}{$i_k}[0] if exists( $redox->{ $nm_rx[1] }{$e}{$i_k} );
678             }
679             }
680              
681 34 100       3447 next if $sum_red_e < @{ $pair_oxi }; # отдаваемых e- нов меньше окисл-ей
  34         139  
682              
683             # Формируем вектор правых частей
684              
685             # из окислителей
686 23         80 my $oxi_in = variations_with_repetition( [ (1..($sum_red_e - @{ $pair_oxi } + 1) ) ],
  23         6989  
687 23         1878 scalar(@{ $pair_oxi }) );
688 23         990 while (my $e_oxi = $oxi_in->next) {
689              
690             # Все e-ны должны быть распределены по окислителям
691 1198         117285 my $sum_e = 0;
692 1198         3760 $sum_e += $_ for @$e_oxi;
693 1198 100       3480 next if $sum_e != $sum_red_e;
694              
695             # из восстановителей
696 122         459 my $red_in = variations_with_repetition( [ (1..($sum_oxi_e - @{ $pair_red } + 1) ) ],
  122         47939  
697 122         9327 scalar(@{ $pair_red }) );
698 122         5573 while (my $e_red = $red_in->next) {
699              
700             # Все e-ны должны быть распределены по восстановителям
701 5204         448594 my $sum_e = 0;
702 5204         15883 $sum_e += $_ for @$e_red;
703 5204 100       14572 next if $sum_e != $sum_oxi_e;
704              
705             # Копируем во временный хеш опций
706 595         44067 my %in_opts;
707 595         1662 $in_opts{'redox_pairs'} = $opts->{'redox_pairs'};
708 595         742 $in_opts{'coefficients'}{ $_ } = $opts->{'coefficients'}{ $_ } for keys %{ $opts->{'coefficients'} };
  595         2099  
709              
710             # Доформировываем внутренний хеш опций для R-вектора
711 595         1026 my $j = 0;
712 595         643 for my $i_k ( @{ $pair_oxi } ){ # пары для окислителей
  595         1160  
713 1555         4176 for( split "->",$i_k ){ # Получаем Исходное и конечное
714 3110 50       6883 unless( exists( $opts->{'coefficients'}{$_} ) ){
715 3110         7925 $in_opts{'coefficients'}{$_} += $e_oxi->[$j];
716             }
717             # last;
718             }
719 1555         3408 $j++;
720             }
721 595         910 $j = 0;
722 595         1100 for my $i_k ( @$pair_red ){ # пары для восстановителей
723 1134         2596 for( split "->",$i_k ){
724 2268 50       5394 unless( exists( $opts->{'coefficients'}{$_} ) ){
725 2268         4766 $in_opts{'coefficients'}{$_} += $e_red->[$j];
726             }
727             }
728 1134         2046 $j++;
729             }
730              
731 595         1037 eval{ &_in_search_stoich($mix, $cheq, \%in_opts) };
  595         1491  
732             }
733             }
734             }
735             }
736             }
737              
738             # Ordinary no-ОВР reactions
739              
740 6         49 my $iter = subsets($All_substances);
741 6         260 $iter->next; # to discard 1st full substances list
742              
743 6         98 while(my $ys = $iter->next){ # Substances list
744 826 100       36184 next if @$ys < 2; # it isn't enough substances for a variation (not last!)
745              
746 778   50     2593 my $new_mix = prepare_mix( $chem_eq, { 'substances' => $ys } ) || next;
747              
748 778         1859 eval{ &_in_search_stoich($new_mix, $cheq, $opts) }; # recursion
  778         1521  
749             }
750 6         215 return;
751             }
752              
753 105         365 my $base_subst = [ @$substances_X ]; # Base substances list
754 105         202 my $var_subst; # Varied substance
755              
756             # R-vector не задан (0) или задан (1) and matrix недоопределённая
757 105 100 33     722 if( $R_zero || ( $R_zero == 0 && @$substances_X == $order + 1 ) ){
      66        
758              
759             # Search linear-dependent substance (column)
760 46         75 for( 1..$#{ $substances_X } ){
  46         130  
761 50         98 $var_subst = pop @$base_subst; # взять последнее
762 50 100       463 last if Rank( [ ( map $atoms_substance->{$_}, @$base_subst ) ] ) == $order;
763 4         9984 unshift @$base_subst, $var_subst; # add в начало
764             }
765              
766             # return if @$base_subst == @$substances_X; # no found (?)
767              
768 46         162668 my $i;
769             # доформировываем R-vector
770 46         77 $R_free->[$i++] -= $_ for @{ $atoms_substance->{$var_subst} };
  46         298  
771             }
772              
773             # Переопределённая матрица (atoms >= substances > rank)
774 105         222 my $ao = $num_atoms - $order;
775 105 100       337 if( $ao > 0 ){
776              
777             # Copy vector & atom matrix
778 82         211 my $R_copy = [ @$R_free ];
779 82         277 my $cam = { map{ $_, [ @{ $atoms_substance->{$_} } ] } keys %$atoms_substance };
  234         278  
  234         835  
780              
781 82         570 my $it = subsets( [ ( 0..$num_atoms - 1 ) ], $ao );
782 82         5116 while( my $p = $it->next ){
783              
784             # Удаление атомных строк (от старших)
785 88         986 for my $i ( reverse @$p ){
786 149         302 splice @$R_free, $i, 1; # из вектора
787 149         340 splice @{ $_ }, $i, 1 for values %$atoms_substance; # из атомной матрицы
  410         896  
788             }
789              
790             # All linear-dependent atom rows removed
791 88 100       518 last if Rank( [ map $atoms_substance->{$_}, @$base_subst ] ) == $order;
792              
793             # Recovery and search repetition
794 6         7214 $R_free = [ @$R_copy ]; # vector
795 6         21 $atoms_substance = { map{ $_, [ @{ $cam->{$_} } ] } keys %$cam }; # matrix
  23         25  
  23         97  
796              
797             }
798             # $num_atoms -= $ao; # уменьшаем число атомов
799             }
800              
801             # Находим решение
802             # Формируем атомную матрицу,
803 105 50       5580326 my $steX = Solve_Det(
804             [ map $atoms_substance->{$_}, @$base_subst ],
805             $R_free,
806             { 'eqs' => 'column', 'int' => 1 } )
807             or
808             croak("No solver!"); # Нет решения
809              
810             # Решение НАЙДЕНО!
811 105         331023 my @den; # Знаменатели
812 105         388 for( @$steX ){
813 330         8633 my($n, $d) = Math::BigRat->new("$_")->parts();
814 330         83139 $_ = $n; # числитель
815              
816 330   50     1298 push @den, $d || 1;
817             }
818              
819             # lowest common multiplicator
820 105         3194 my $lcm = blcm(@den);
821              
822 105         47186 my %cfs; # Стех. коэффициенты
823 105 100       427 $cfs{$var_subst} = $lcm if defined $var_subst; # варьируемого в-ва
824              
825 105 100       300 unless( $R_zero ){ # Некоторые в-ва заданы
826 59         92 while( my($s, $c) = each %{ $opts->{'coefficients'} } ){
  332         31913  
827 273         673 $cfs{$s} = $lcm * $c;
828             }
829             }
830              
831 105         228 my $i = 0;
832 105         253 for( @$base_subst ){ # Искомые
833 330         1139 $cfs{$_} = $steX->[$i] * $lcm / $den[$i];
834 330         6914603 $i++;
835             }
836              
837 105 100       347 if( $ao > 0 ){ # Для переопределённых - Проверка мат.баланса
838              
839 82         146 my @balance;
840 82         489 for my $s (keys %cfs){ # в-во
841 505         24700 my $i = 0;
842              
843 505         565 $balance[ $i++ ] += $_ * $cfs{$s}->numify() for @{ $ini_am->{$s} };
  505         2047  
844             }
845              
846 82 100       6353 return if scalar(grep $_, @balance); # no balance
847             }
848              
849             # С помощью НОД (GCD) уменьшаем порядок стех.коэф.
850 49 50       188 if( ( my $gcd = bgcd(values %cfs) ) > 1 ){
851 0         0 $cfs{$_} /= $gcd for keys %cfs;
852             }
853              
854 49         8004 my $keq = prepare_mix( $chem_eq, { 'norma' => 1, 'coefficients' => \%cfs } );
855              
856             # Save only unique chemical equation
857 49 100       2000 scalar(grep $_ eq $keq, @$cheq) || push @$cheq, $keq;
858             }
859              
860              
861             # Возвращает массив хешей:
862             # $redox{oxidant/reducer}{Элементы}{исх.->прод.}
863             # [0] - число e- нов принимаемых/отдаваемых
864             # [1] - OS элемента в исходном
865             # [2] - ... в продукте
866             # [3] - число атомов элемента в исходном
867             # [4] - ... в продукте
868             # [5] - множитель на MAX число атомов исх.
869             # [6] - ... продукта
870              
871             # $el_half_eqs{oxidant/reducer}[..] - 2 массива полуреакций (в проекте для другой п/п)
872              
873             sub redox_test{
874 4     4 0 6 my $chem_eq = shift;
875              
876 4         8 my @rx; # $rx[исх/прод] {элементы} {OS элемента} {вещества} = Кол-во атомов элемента
877             my %ve; # $ve{в-во} = количество элементов
878              
879 4         7 my $z = 0; # Признак исх.в-в
880 4         6 for my $eq (@{ $chem_eq }){
  4         8  
881 8         12 for my $s (@{ $eq }){
  8         12  
882              
883 29         84 my $os = oxidation_state($s);
884             # Рез-ты: $os->{элемент}
885             # {num}[0 .. n-1] - кол-во по каждому 1..n атому элемента
886             # {OS}[0 .. n-1][ ] - степень(и) окисления (OS) 1..n атому элемента
887              
888 29         92 $ve{$s} = keys %$os; # кол-во Элементов
889              
890 29         40 for my $e ( keys %{$os} ){ # Элемент
  29         71  
891              
892             # Берём OS атомов элемента в в-ве
893 67         101 for ( my $i=0; $i<=$#{ $os->{$e}{'OS'} }; $i++ ){
  134         509  
894            
895             # !!! Продумать дробные степени окисления (сделано)
896             # Объединяем множественные OS
897 67         127 my($a, $sum, $n);
898 67         95 for( @{ $os->{$e}{'OS'}[$i] } ){
  67         189  
899 67         75 $sum += $_;
900 67         126 $n++; # кол-во
901             }
902            
903 67 50       145 if($n > 1){
904 0         0 my $gcd = bgcd(abs($sum), $n); # НОД
905 0 0       0 if($gcd > 1){
906 0         0 $sum /= $gcd;
907 0         0 $n /= $gcd;
908             }
909 0         0 $a = "$sum/$n";
910              
911             }else{
912 67         95 $a = $sum;
913             }
914             # Кол-во атомов элемента
915 67         295 $rx[$z]{$e}{$a}{$s} = $os->{$e}{'num'}[$i];
916             }
917             }
918             }
919 8         25 $z = 1; # продукты
920             }
921              
922             _M_redox_1:
923 4         11 for my $e ( keys %{ $rx[0] } ){ # Элементы (берём из исходных)
  4         19  
924 22         25 for( keys %{ $rx[0]{$e} } ) { # OS элемента
  22         55  
925 23 100       86 next _M_redox_1 unless exists( $rx[1]{$e}{$_} ); # Другая в продуктах
926             }
927              
928 10         17 for( keys %{ $rx[1]{$e} } ) { # OS элемента
  10         25  
929 10 50       32 next _M_redox_1 unless exists( $rx[0]{$e}{$_} ); # Другая в исходных
930             }
931             # Удалить не ОВР элементы
932 10         57 delete $rx[$_]{$e} for 0,1;
933             }
934              
935             # не ОВР
936 4 50 33     9 return if ! keys( %{ $rx[0] } ) || ! keys( %{ $rx[1] } );
  4         18  
  4         13  
937             # return undef unless keys( %{ $rx[1] } );
938              
939 4         7 my %del_rx; # хеш исключённых в-в
940             my %redox; # результаты
941              
942             _M_redox_2:
943 4         7 while( 1 ){
944 6         12 my %vv; # $vv{в-во} {'yes'} = встечаемость в-ва в redox-парах
945             # {'os'}{элемент} = 1 - Элементы меняющие свою OS
946              
947 6         15 for my $e ( keys %{ $rx[0] } ){ # Элементы (берём из исходных)
  6         20  
948 16         22 for my $os_i ( keys %{ $rx[0]{$e} } ) { # OS элемента в исх.
  16         44  
949 19         23 for my $os_p ( keys %{ $rx[1]{$e} } ) { # OS элемента в продуктах
  19         63  
950              
951              
952             # !!! Продумать дробные степени окисления (сделано)
953 18         95 my $n_os_i = Math::BigRat -> new("$os_i") -> numify();
954 18         2470 my $n_os_p = Math::BigRat -> new("$os_p") -> numify();
955              
956 18         2000 my $nm; # название: oxidant / reducer
957 18 100       75 if($n_os_i > $n_os_p){ # Окислитель?
    100          
958 8         15 $nm = 'oxidant';
959              
960             }elsif($n_os_i < $n_os_p){ # Восстановитель?
961 7         17 $nm = 'reducer';
962              
963             }else{
964 3         12 next;
965             }
966              
967             # Составляем пару
968 15         20 while( my($v_i, $n_i) = each %{ $rx[0]{$e}{$os_i} } ){ # исх. в-ва и кол-во атомов
  33         187  
969 18 100       53 next if exists $del_rx{$v_i}; # исключённое в-во
970              
971 17         41 $vv{$v_i}{'yes'}++;
972 17         43 $vv{$v_i}{'os'}{$e} = 1; # Элементы меняющие свою OS
973              
974 17         21 while( my($v_p, $n_p) = each %{ $rx[1]{$e}{$os_p} } ){ # продукты и кол-во атомов
  40         202  
975 23 100       57 next if exists $del_rx{$v_p}; # исключённое в-во
976              
977 21         48 $vv{$v_p}{'yes'}++;
978 21         48 $vv{$v_p}{'os'}{$e} = 1; # Элементы меняющие свою OS
979              
980 21         66 my $lcm = blcm($n_i, $n_p); # наименьший общий множитель кол-ва атомов
981              
982 21         6064 my($c, $z) = Math::BigRat->new("$os_i")->parts();
983 21         4184 my $ee = $lcm * $c / $z;
984              
985 21         3102 ($c, $z) = Math::BigRat->new("$os_p")->parts();
986 21         4261 $ee -= $lcm * $c / $z;
987            
988             # Принятые элек-ны, OS, число атомов и
989             # множители на MAX число атомов исх. и прод.
990 21         4416 @{ $redox{$nm}{$e}{"$v_i->$v_p"} } = ( abs($ee),
  21         6654  
991             $os_i, $os_p,
992             $n_i, $n_p,
993             $lcm / $n_i, $lcm / $n_p );
994             }
995             }
996             }
997             }
998             }
999              
1000             # !!! Интуитивно исключаем из redox-списков не ОВР в-ва
1001 6         22 for my $nm ( 'oxidant','reducer' ){
1002 11         20 for my $e ( keys %{ $redox{$nm} } ){ # Элементы
  11         44  
1003              
1004             # Redox-пары элемента
1005 11 100       15 next if keys %{ $redox{$nm}{$e} } < 2; # одна пара
  11         48  
1006              
1007 5         10 for my $i_k ( keys %{ $redox{$nm}{$e} } ){
  5         21  
1008 10         38 for( split "->",$i_k ){
1009 19 100       68 next if $ve{$_} < 3; # кол-во Элементов в в-ве
1010 6 100       26 next if $vv{$_}{'yes'} > 1; # в-во встречается часто
1011 2 50       3 next if keys %{ $vv{$_}{'os'} } > 1; # OS меняет более 1 Элемента
  2         13  
1012              
1013 2         7 $del_rx{$_} = ''; # внести в список исключённых в-в
1014 2         43 %redox = (); # очистить
1015 2         53 next _M_redox_2;
1016             }
1017             }
1018             }
1019              
1020              
1021             }
1022 4 100 66     19 return keys( %{ $redox{'oxidant'} } ) && keys( %{ $redox{'reducer'} } ) ? \%redox : undef; # не ОВР
1023             }
1024             }
1025              
1026              
1027             sub _search_atoms_subs{
1028 1391     1391   1569 my $chem_eq = shift;
1029              
1030 1391         1378 my %tmp_subs; # Atoms of substance
1031             my %atoms; # Atom hash
1032              
1033 1391         2183 for my $i ( @$chem_eq ){
1034 2782         4068 for my $s ( @$i ){
1035             # Atoms of substance
1036 7213 50       9137 my %f = eval{ Chemistry::File::Formula->parse_formula( $s ) } or
  7213         22999  
1037             croak("'$s' is not substance!");
1038              
1039 7213         1226380 for( keys %f ){
1040 17235         38960 $tmp_subs{$s}{$_} = $f{$_};
1041 17235         44603 $atoms{$_}++; # Atom balance
1042             }
1043             }
1044             }
1045              
1046 1391         4862 while( my($k,$v) = each %atoms){
1047 4792 100       148463 croak("No balance of '$k' atom!") if $v == 1;
1048             }
1049              
1050             # Atom (stoichiometry) matrix vectors (quantity of atoms for each substance)
1051 674         1218 my %atoms_substance;
1052 674         2125 for my $subs (keys %tmp_subs){
1053 4574 100       8773 $atoms_substance{$subs} = [ map { $tmp_subs{$subs}{$_} || 0 } keys %atoms ];
  23109         89361  
1054             }
1055              
1056 674         17041 return( \%atoms_substance, scalar(keys %atoms) );
1057             }
1058              
1059             # Transform classic into brutto (gross) formula
1060             sub brutto_formula{
1061 3     3 1 1518 my $s = shift;
1062              
1063 3 50       6 my %e = eval{ Chemistry::File::Formula->parse_formula( $s ) } or
  3         18  
1064             croak("'$s' is not substance!");
1065              
1066 3         2568 return( join( '', map{ "$_$e{$_}" } sort { $a cmp $b } keys %e ) );
  14         55  
  19         25  
1067             }
1068              
1069             # Decomposes the chemical equation to
1070             # arrays of initial substances and products
1071             sub parse_chem_mix{
1072 1498     1498 1 65472 $_ = shift; # List of substances with delimiters (spaces, comma, + )
1073 1498         1977 my $coef = shift; # hash of coefficients
1074              
1075 1498         1542 my %cf; # temp hash of coefficients
1076              
1077 1498         3850 s/^\s+//;
1078 1498         5363 s/\s+$//;
1079              
1080 1498         5505 my $m = qr/\s*[ ,;+]+\s*/; # mask of delimeters
1081              
1082             # to unite the separeted numerals ( coefficient ? )
1083 1498         36049 1 while s/((?:$m|^)[1-9]+)$m([1-9]+(?:$m|$))/$1$2/g;
1084              
1085             # to unite the separeted substance & index
1086 1498         10985 s/([)}\]a-zA-Z1-9])\s+([1-9]+\s*[)}\]\-=+])/$1$2/g;
1087              
1088             # remove spaces around brackets
1089 1498         2703 s/([({\[])\s+/$1/g;
1090 1498         7747 s/\s+([)}\]])/$1/g;
1091              
1092             # The chemical equation is set:
1093             # A + B --> C + D ||
1094             # A, B, C, D
1095 1498 100       45897 my $chem_eq = /(.+?)\s*[=-]+>*\s*(.+)/ ?
1096             [ map [ split "$m" ], ($1,$2) ] :
1097             [ map [ split "$m" ], ( /(.+)$m(\S+)/ ) ];
1098              
1099 1498 50       3912 return unless @$chem_eq;
1100              
1101 1498         4404 for my $ip ( @$chem_eq ){
1102              
1103 2996         3886 my @ip_del; # For removal from an array of coefficients
1104              
1105 2996         5433 for(my $i=0; $i<=$#{ $ip }; $i++ ){
  11112         25181  
1106 8116         11868 $_ = $ip->[$i];
1107              
1108 8116 100       41546 if( /^(\d+)([({\[a-zA-Z].*)/ ){ # together coefficient and substance
    100          
    100          
    100          
1109              
1110 39         83 my ($c, $s) = ($1, $2); # coefficient, substance
1111              
1112             # zero coefficient
1113 39 100 66     160 if( $c =~ /^0$/ && ( $i==0 || ( $i>0 && $ip->[$i-1] =~ /\D/ ) ) ){
    100 66        
1114 2         6 push @ip_del, $i; # remove substance
1115              
1116             }elsif( s/^0/O/ ){ # oxigen?
1117 2         5 &_p_c_m( $_, $i, \%cf, $ip );
1118              
1119             }else{
1120 35         118 $cf{$s} = $c; # coefficient
1121 35         80 $ip->[$i] = $s; # substance
1122             }
1123              
1124             }elsif( s/^0(.)/O$1/ ){ # oxigen
1125 6         19 &_p_c_m( $_, $i, \%cf, $ip );
1126              
1127             }elsif( /^0$/ ){ # zero coefficient
1128 12 100 100     35 if( $i < $#{ $ip } && ! exists( $coef->{'zero2oxi'} ) ){ # remove
  12         58  
1129 5         6 push @ip_del, $i; # coefficient
1130 5         11 push @ip_del, ++$i; # substance
1131              
1132             }else{
1133 7         20 s/^0/O/; # oxigen
1134 7         17 &_p_c_m( $_, $i, \%cf, $ip );
1135             }
1136              
1137             }elsif( /^\d+$/ ){ # only numerals
1138             # coefficient
1139 411 100 66     832 if( defined $ip->[$i+1] ){
  5 100 66     41  
1140 402         992 $cf{ $ip->[$i+1] } = $_;
1141            
1142             }elsif( $ip eq $chem_eq->[0] &&
1143             @{ $chem_eq->[1] } == 1 &&
1144             $chem_eq->[1][0] =~ /[a-zA-Z]/ ){
1145              
1146             # coefficient of product
1147 5         14 $chem_eq->[1][0] = $_.$chem_eq->[1][0];
1148             }
1149 411         724 push @ip_del, $i;
1150             }
1151             }
1152 2996         11077 splice @$ip, $_, 1 for( reverse @ip_del );
1153             }
1154              
1155             # To removal identical reactants and products
1156 1498         2480 for my $ip ( @$chem_eq ){
1157              
1158 2996         3183 my @ip_del;
1159 2996         3847 for(my $i = 0; $i < $#{ $ip }; $i++ ){
  7706         15799  
1160              
1161 4710         5842 for(my $j = $i + 1; $j <= $#{ $ip }; $j++ ){
  13143         27528  
1162              
1163 8443 100       18416 if( $ip->[$i] eq $ip->[$j] ){
1164 10         11 push @ip_del, $j;
1165 10         19 last;
1166             }
1167             }
1168             }
1169 2996         9748 splice @$ip, $_, 1 for( sort{ $b <=> $a } @ip_del );
  4         15  
1170             }
1171              
1172             # Check empty reactants
1173 1498 100       2263 if( ! @{ $chem_eq->[0] } ){
  1498         4170  
1174 3 100       4 return if @{ $chem_eq->[1] } < 2; # bad mix (one or no substance)
  3         13  
1175 2         3 push @{ $chem_eq->[0] }, shift @{ $chem_eq->[1] };
  2         4  
  2         26  
1176             }
1177              
1178             # To removal identical products with reactants
1179 1497         1651 for my $s ( @{ $chem_eq->[0] } ){
  1497         2660  
1180 3882         3732 my @ip_del;
1181 3882         5041 for(my $i=0; $i<=$#{ $chem_eq->[1] }; $i++ ){
  14261         29024  
1182 10379 100       21644 push @ip_del, $i if $s eq $chem_eq->[1][$i];
1183             }
1184 3882         11420 splice @{ $chem_eq->[1] }, $_, 1 for( reverse @ip_del );
  2         9  
1185             }
1186              
1187             # Check empty products
1188 1497 100       1779 if( ! @{ $chem_eq->[1] } ){
  1497         3616  
1189 6 100       7 return if @{ $chem_eq->[0] } < 2; # bad mix (one or no substance)
  6         38  
1190 4         6 push @{ $chem_eq->[1] }, pop @{ $chem_eq->[0] };
  4         6  
  4         8  
1191             }
1192              
1193 1495         10948 @$coef{ keys %cf} = values %cf; # join (copy)
1194              
1195 1495         2374 delete $coef->{'zero2oxi'};
1196              
1197             # Lists of substances:
1198             # fist --- reactants or '=','-','>'
1199             # last --- products or '=','-','>'
1200 1495         6004 return $chem_eq;
1201             }
1202              
1203             sub _p_c_m{
1204 15     15   26 my( $s, $i, $cf, $ip ) = @_;
1205              
1206 15 100       39 if( exists $cf->{ $ip->[$i] } ){
1207 8         17 $cf->{$s} = $cf->{ $ip->[$i] };
1208 8         18 delete $cf->{ $ip->[$i] };
1209             }
1210 15         44 $ip->[$i] = $s; # replacement
1211             }
1212              
1213              
1214             # Calculation of oxidation state
1215             sub oxidation_state{
1216 199   50 199 1 562612 my $s = shift || return;
1217              
1218 199         359 our $mask;
1219 199         1917 $mask = qr/{(?:(?>[^{}]+)|(??{$mask}))*}\d*/;
1220 199         1847 my @species = $s =~ /$mask/g;
1221              
1222             # One substance
1223 199 100 66     1296 return &_in_os($s) if @species < 2 || $s ne join('',@species);
1224              
1225 1         2 my $r; # Result:
1226             # HASH{element}{num}[0 .. n-1] - Element amount on everyone 1..n atom
1227             # HASH{element}{OS}[0 .. n-1][ .. ] - Oxidation States of Element (OSE) arrays 1..n atom
1228              
1229             # Mix of substances
1230 1         3 for( @species ){
1231              
1232             # remove bordering brackets
1233 2         9 s/^\{//;
1234 2         9 s/\}(\d*)$//;
1235             # save possible coefficient
1236 2         6 my $d = $1;
1237            
1238 2   50     6 my $p = &_in_os($_) || return;
1239              
1240 2         7 for my $e ( keys %$p ){
1241 4         7 push @{ $r->{$e}{'OS'} }, @{ $p->{$e}{'OS'} };
  4         11  
  4         11  
1242              
1243 4 100       12 if( $d ){
1244 2         4 $_ *= $d for @{ $p->{$e}{'num'} }
  2         8  
1245             }
1246 4         5 push @{ $r->{$e}{'num'} }, @{ $p->{$e}{'num'} };
  4         11  
  4         19  
1247             }
1248             }
1249              
1250 1         9 $r;
1251             }
1252              
1253             sub _in_os{
1254 200     200   436 my $chem_sub = shift;
1255              
1256             # prepare atomic composition of substance
1257 200         454 my %nf = eval{ Chemistry::File::Formula->parse_formula( $chem_sub ) };
  200         1687  
1258 200 50       58676 return unless keys %nf;
1259              
1260             # Count of "pure" atoms each element of substance
1261 200         515 $_ = $chem_sub;
1262 200         1061 s/\d+//g; # remove digits
1263 200         843 my %num = Chemistry::File::Formula->parse_formula( $_ );
1264              
1265             # Ions: { element }{ length }{ ion-pattern }[ [ array OSE ] ]
1266 200         42202 my $ions = &_read_ions( $chem_sub );
1267              
1268             # Read Pauling electronegativity and OSE:
1269             # atom electronegativity, oxidation state, intermetallic compound
1270 200         811 my( $atom_el_neg, $atom_OS, $intermet ) = &_read_atoms( \%nf );
1271 200 50       944 return if keys( %$atom_el_neg ) != keys( %nf );
1272              
1273 200         246 my $prop; # Result:
1274              
1275             # Substance is intermetallic compound or Simple substance (one element)
1276 200 100 66     1467 if( $intermet || keys %nf == 1 ){
1277              
1278 2         11 while( my( $e, $n ) = each %nf ){
1279             # Total quantity of atoms for element
1280 2         10 $prop->{ $e }{ 'num' }[ 0 ] = $n;
1281              
1282             # By default in the list of OSE only 0th charge
1283 2         13 $prop->{ $e }{ 'OS' }[ 0 ] = [ 0 ];
1284             }
1285              
1286 2         17 return $prop ;
1287             }
1288              
1289             # Sort atoms in decreasing order of electronegativity:
1290             # 1th --- the most electronegative
1291 198         275 my @neg = sort { $atom_el_neg->{$b} <=> $atom_el_neg->{$a} } keys %{ $atom_el_neg };
  471         4516  
  198         1589  
1292              
1293 198         513 my %bOS; # by default OSE basic list
1294              
1295             # Basic OSE list of atoms on decrease of electronegativities
1296 198         1378 for(my $i = 0; $i <= $#neg; $i++){
1297              
1298 556         974 my $e = $neg[$i]; # element
1299              
1300             # Electronegative 1th is identical with next elements
1301 556 100 66     4828 if( ( $i < $#neg &&
    100 66        
1302             $atom_el_neg->{ $neg[0] } == $atom_el_neg->{ $neg[$i+1] } ) ||
1303             $i == 1 ){
1304              
1305             # '-' and '+' OSE, without 0
1306 198         742 $bOS{ $e } = [ grep $_, @{ $atom_OS->{$e} } ];
  198         878  
1307              
1308             }elsif( $i == 0 ) { # 1th (most electronegative) -> only '-' OSE
1309 198         321 $bOS{ $e } = [ grep $_ < 0, @{ $atom_OS->{$e} } ];
  198         2571  
1310              
1311             }else{ # Others -> only '+' OSE
1312 160         246 $bOS{ $e } = [ grep $_ > 0, @{ $atom_OS->{$e} } ];
  160         696  
1313             }
1314              
1315             # Inert elements
1316 556 100       1775 $bOS{ $e } = [ @{ $atom_OS->{$e} } ] if $e=~/He|Ne|Ar|Kr|Xe|Rn/;
  5         17  
1317              
1318             # mask for search ions
1319 556 100       2463 my $m = length($e)==1 ? "$e\\d+|$e(?![a-gik-pr-u])" : "$e\\d*";
1320              
1321 556         1516 for(my $j = 0; $j < $num{$e}; $j++){
1322              
1323             # Number of various atoms for element in substance
1324 586 100       1317 if( $num{$e} == 1 ){ # One atom of element in substance
1325 526         564 push @{ $prop->{$e}{'num'} }, $nf{$e};
  526         4131  
1326              
1327             }else{
1328 60         92 my $count = 0;
1329 60         102 $_ = $chem_sub;
1330              
1331             # Search ion-group. Remove all atoms of element, except current
1332 60 100       1314 s{ ($m) }{ $1 if $count++ == $j }gex;
  120         747  
1333              
1334 60         600 my %f = Chemistry::File::Formula->parse_formula( $_ );
1335 60         28077 push @{ $prop->{ $e }{'num'} }, $f{ $e };
  60         465  
1336             }
1337             }
1338             }
1339              
1340             # Two pass: 1st -- ion recognition, 0th -- without ions (possible)
1341 198         632 for my $yni (1, 0){
1342 201         299 my $no_ion = 1; # no ions
1343              
1344 201         296 my $balance_A = 0; # for Electronic balance
1345 201         204 my $max_n_OS = 0; # Number of OSE in maximum list
1346 201         321 my @atoms; # Varied atoms of elements
1347             my %osin; # lists of OSE ions
1348              
1349 201         907 for(my $i = 0; $i <= $#neg; $i++){
1350              
1351 566         878 my $e = $neg[$i]; # element
1352              
1353             # mask for search ions
1354 566 100       1661 my $m = length($e)==1 ? "$e\\d+|$e(?![a-gik-pr-u])" : "$e\\d*";
1355              
1356             _SO_M1: # Number of various atoms for element in substance
1357 566         1565 for(my $j = 0; $j < $num{$e}; $j++){
1358              
1359 598         933 $_ = $chem_sub;
1360              
1361 598         689 my $count = 0;
1362             # Search ion-group. Remove all atoms of element, except current
1363 598 100       16242 s{ ($m) }{ $1 if $count++ == $j }gex;
  662         9865  
1364              
1365 598 100       1677 if( $yni ){
1366             # Sort by decrease length of ion-group
1367 586         719 for my $l (sort {$b <=> $a} keys %{ $ions->{$e} } ) {
  57         227  
  586         2534  
1368              
1369 339         436 for my $mg ( keys %{ $ions->{$e}{$l} } ){
  339         1003  
1370 343 100       3800 next unless /$mg/; # Ion-group isn't found
1371              
1372 321         547 $no_ion = 0; # yes ions
1373              
1374             # Ion-group is found. Save list of OSE
1375             # for j-th atom of element
1376 321 100       369 if( @{ $ions->{$e}{$l}{$mg} } == 1 ){ # One list OSE ion
  321         942  
1377              
1378 298         373 push @{ $prop->{$e}{'OS'} }, $ions->{$e}{$l}{$mg}[0];
  298         1136  
1379              
1380             # Calculation total and mean OSE
1381 298         570 my $os;
1382 298         337 $os += $_ for @{ $prop->{$e}{'OS'}[$j] };
  298         1542  
1383            
1384             # sum OSE * number of atoms / number of OSE
1385 298         785 $balance_A += $os * $prop->{$e}{'num'}[$j] / @{ $prop->{$e}{'OS'}[$j] };
  298         919  
1386              
1387             }else{ # Many OSE ion lists
1388             # Add in array varied OSE for j-th atom of element
1389 23         73 push @atoms, "$e:$j";
1390 23         104 $osin{"$e:$j"} = $ions->{$e}{$l}{$mg};
1391              
1392             # Define from a basic list of OSE
1393 23         33 push @{ $prop->{$e}{'OS'} }, [ -999 ];
  23         92  
1394              
1395             # Quantity of OSE from basic list
1396 23         31 my $n_OS = $#{ $ions->{$e}{$l}{$mg} };
  23         60  
1397 23 100       65 $max_n_OS = $n_OS if $n_OS > $max_n_OS; # max list of OSE
1398             }
1399 321         2095 next _SO_M1; # Ion-group is found
1400             }
1401             }
1402             }
1403              
1404             # Quantity of OSE from basic list
1405 277         546 my $n_OS = $#{ $bOS{ $e } };
  277         554  
1406 277 100       667 $max_n_OS = $n_OS if $n_OS > $max_n_OS; # max list of OSE
1407              
1408 277 100       644 if( $n_OS ) { # number of OSE > 1
1409             # Add in array varied OSE for j-th atom of element
1410 144         394 push @atoms, "$e:$j";
1411              
1412             # Define from a basic list of OSE
1413 144         186 push @{ $prop->{$e}{'OS'} }, [ -999 ];
  144         1075  
1414              
1415             }else{ # One OSE in list
1416 133         182 push @{ $prop->{$e}{'OS'} }, [ $bOS{ $e }[0] ];
  133         533  
1417              
1418             # charge * number of atoms
1419 133         873 $balance_A += $bOS{ $e }[0] * $prop->{$e}{'num'}[$j];
1420             }
1421             }
1422             }
1423              
1424 201         344 my $balance_B = 0; # for Electronic balance
1425              
1426             # Select oxidation states
1427 201 100       478 if( $max_n_OS ){
1428 106         918 my $iter = variations_with_repetition( [ (0..$max_n_OS) ], scalar( @atoms ) );
1429             _SO_M2:
1430 106         4339 while (my $p = $iter->next) {
1431              
1432 467         4537 $balance_B = 0;
1433 467         649 for(my $i = 0; $i<=$#{ $p }; $i++){
  1430         7738  
1434              
1435 1041         1733 my $x = $atoms[$i];
1436 1041         2592 my($e, $j) = split /:/,$x;
1437              
1438 1041         1296 my($sum, @os);
1439              
1440 1041 100       2033 if( exists $osin{ $x } ){ # Some OSE ion lists
1441              
1442 135         172 $sum += $_ for @{ $osin{ $x }[ $p->[$i] ] };
  135         493  
1443 135         168 @os = @{ $osin{ $x }[ $p->[$i] ] };
  135         340  
1444              
1445             }else{
1446 906         1948 $os[0] = $sum = $bOS{ $e }[ $p->[$i] ];
1447             }
1448              
1449 1041 100       2290 next _SO_M2 unless defined $sum; # OSE have ended
1450              
1451             # sum OSE * number of atoms / number of OSE
1452 963         2909 $balance_B += $sum * $prop->{ $e }{'num'}[$j] / @os;
1453 963         3732 $prop->{ $e }{'OS'}[$j] = [ @os ];
1454              
1455             }
1456 389 100       2219 last unless $balance_A + $balance_B; # balance is found
1457             }
1458             }
1459              
1460 201 50 66     802 return if $no_ion && ($balance_A + $balance_B); # balance is not found
1461              
1462 201         360 $balance_A = 0; # for electronic balance
1463              
1464             # Check electronic balance
1465 201         759 for my $e (keys %$prop ){
1466 566         845 my $i=0;
1467 566         641 for my $os ( @{ $prop->{$e}{'OS'} } ){
  566         1189  
1468              
1469 598         620 my $sum;
1470 598         1549 $sum += $_ for @$os;
1471              
1472             # sum OSE * number of atoms / number of OSE
1473 598         2501 $balance_A += $sum * $prop->{$e}{'num'}[ $i++ ] / @$os;
1474             }
1475             }
1476              
1477 201 100       466 if( $balance_A ){
1478 3 50       11 return if $no_ion;
1479             }else{
1480 198         3351 return $prop;
1481             }
1482 3         26 delete $prop->{$_}{'OS'} for keys %$prop;
1483             }
1484             }
1485              
1486             # Read Pauling electronegativity and OSE
1487             # only for given elements of substance
1488             # input:
1489             # %atoms -- elements
1490             # return:
1491             # %atom_el_neg
1492             # %atom_OS
1493             # $intermet
1494              
1495             sub _read_atoms{
1496 200     200   361 my $atoms = shift;
1497              
1498 200         343 my %atom_el_neg; # atom electronegativity
1499             my %atom_OS; # oxidation state
1500 200         377 my $intermet = 1; # for intermetallic compound
1501              
1502 200         963 my $adb = &_atoms_db;
1503              
1504 200         1690 for( my $i = 0; $i < @$adb; $i+=5 ){
1505 23400         27479 $_ = $adb->[$i];
1506 23400 100       61052 next if !exists $atoms->{ $_ };
1507              
1508 558         1390 $atom_el_neg{ $_ } = $adb->[$i+2];
1509 558 100       1376 $intermet = 0 unless $adb->[$i+3]; # Not intermetallic compound
1510 558         1668 $atom_OS{ $_ } = $adb->[$i+4];
1511             }
1512              
1513 200         8039 \%atom_el_neg, \%atom_OS, $intermet;
1514             }
1515              
1516             # Read necessary ion-group
1517             # input:
1518             # $Chemistry_substance
1519             # return:
1520             # $ions
1521             sub _read_ions {
1522 200     200   454 my $chem_sub = shift;
1523 200         277 my %ions;
1524              
1525 200         511 my $idb = &_ions_db;
1526              
1527             # Construct pattern
1528 200         798 for( my $j = 0; $j < @$idb; $j+=2 ){
1529 21400         113241 my $frm = $idb->[$j];
1530 21400         53122 my $os = $idb->[$j+1];
1531              
1532 21400         167404 my %a = split /_|=/,$os; # Parse to element end OSE
1533              
1534 21400 100       69806 if($os =~ /~/){ # Macro-substitutions
1535              
1536 5800         6934 my %ek;
1537 5800         6666 my $max_n_ek = 0; # max number of element-pattern in macro-substitutions
1538              
1539 5800         30554 while( my($e, $v) = each %a ){
1540              
1541 12400 100       53315 if($e =~ /(\w+~)(.*)/){
1542 5800         42951 $ek{$1}[0] = [ split ',',$2 ]; # elements
1543 5800         22815 $ek{$1}[1] = $v; # OSE for group
1544              
1545 5800         6435 my $n_ek = $#{ $ek{$1}[0] }; # number of element-substitutions
  5800         14361  
1546 5800 100       27279 $max_n_ek = $n_ek if $n_ek > $max_n_ek; # max list
1547             }
1548             }
1549              
1550 5800         39127 my $iter = variations_with_repetition( [ (0..$max_n_ek) ], scalar( keys %ek ) );
1551             ELEMENT_MACRO_1:
1552 5800         215284 while (my $p = $iter->next) {
1553 27800         263361 my $m = $frm; # macro-formula (mask)
1554 27800         31531 my $i = 0;
1555 27800         68279 for my $em (sort keys %ek){
1556 27800         55758 my $e = $ek{ $em }[0][ $p->[$i++] ]; # element
1557 27800 50       52346 next ELEMENT_MACRO_1 unless defined $e; # pattern have ended
1558              
1559 27800         165216 $m =~ s/$em/$e/g; # Construct ion mask
1560             }
1561              
1562 27800 100       485171 next unless $chem_sub =~ /($m)/; # Ions in substance aren't present
1563              
1564 57         338 my $l = length($1); # Length of ion mask
1565              
1566 57         95 $i = 0;
1567 57         392 for my $em (sort keys %ek){
1568 57         158 my $e = $ek{ $em }[0][ $p->[$i++] ]; # element
1569              
1570 57         219 for my $z ( split /!/, $ek{ $em }[1] ){
1571             # list OSE
1572 61         266 push @{ $ions{ $e }{ $l }{ $m } }, [ split /;/,$z ];
  61         889  
1573             }
1574             }
1575              
1576 57         431 while(my ($e, $v) = each %a ){
1577 124 100       782 next if $e =~ /~/;
1578              
1579             # list OSE
1580 67         194 for my $z ( split /!/, $v ){
1581 67         93 push @{ $ions{ $e }{ $l }{ $m } }, [ split /;/,$z ];
  67         3371  
1582             }
1583             }
1584             }
1585              
1586             }else{
1587 15600 100       245442 next unless $chem_sub =~ /($frm)/; # no ions in substance
1588              
1589 116         376 my $l = length($1); # Length of the found group
1590              
1591 116         480 while(my ($e, $v) = each %a ){
1592             # list OSE
1593 234         569 for my $z ( split /!/, $v ){
1594 265         291 push @{ $ions{ $e }{ $l }{ $frm } }, [ split /;/,$z ];
  265         2630  
1595             }
1596             }
1597             }
1598             }
1599              
1600 200         4444 \%ions;
1601             }
1602              
1603              
1604             # Pauling scale (adapted).
1605             # Atomic weights from NIST.
1606             # Attention!
1607             # order of OSE is important (last are exotic OSE)
1608             sub _atoms_db{
1609             return [
1610 246     246   39783 'Ac', 227, 110, 1, [0, 3],
1611             'Ag', 107.8682, 193, 1, [0, 1, 2, 3, 5],
1612             'Al', 26.9815386, 161, 1, [-3, 0, 3, 1, 2],
1613             'Am', 243, 113, 1, [0, 2, 3, 4, 5, 6],
1614             'Ar', 39.948, 0, 0, [0],
1615             'As', 74.9216, 221, 0, [-3, 0, 3, 5], # 2
1616             'At', 210, 225, 0, [-1, 0, 1, 5, 7], # 3
1617             'Au', 196.966569, 254, 1, [0, 1, 2, 3, 5, 7], # -1
1618             'B', 10.811, 204, 0, [-3, 0, 1, 2, 3],
1619             'Ba', 137.327, 89, 1, [0, 2],
1620             'Be', 9.012182, 157, 1, [0, 2],
1621             'Bh', 272.13803, 0, 1, [0, 7],
1622             'Bi', 208.9804, 221, 0, [-3, 0, 2, 3, 5],
1623             'Bk', 247, 130, 1, [0, 3, 4],
1624             'Br', 79.904, 296, 0, [-1, 0, 1, 3, 4, 5, 6, 7],
1625             'C', 12.0107, 255, 0, [-4, -3, -1, 0, 2, 3, 4], # -2, 1
1626             'Ca', 40.078, 100, 1, [0, 2],
1627             'Cd', 112.411, 169, 1, [0, 2],
1628             'Ce', 140.116, 112, 1, [0, 3, 4], # 2
1629             'Cf', 251, 130, 1, [0, 2, 3, 4],
1630             'Cl', 35.453, 316, 0, [-1, 0, 1, 3, 4, 5, 6, 7], # 2
1631             'Cm', 247, 128, 1, [0, 3, 4],
1632             'Cn', 285.17411, 205, 1, [0, 2, 4],
1633             'Co', 58.933195, 188, 1, [0, 1, 2, 3, 4], # -1, 5
1634             'Cr', 51.9961, 166, 1, [0, 1, 2, 3, 4, 5, 6], # -2, -1
1635             'Cs', 132.9054519, 79, 1, [0, 1],
1636             'Cu', 63.546, 190, 1, [0, 2, 1, 3], # 4
1637             'D', 2.0141017778, 220, 0, [-1, 0, 1],
1638             'Db', 268.12545, 0, 1, [0, 5], # old Ns
1639             'Ds', 281.16206, 0, 1, [0, 6, 4, 2, 5], # as Pt
1640             'Dy', 162.5, 122, 1, [0, 3, 4], # 2
1641             'Er', 167.259, 124, 1, [0, 3],
1642             'Es', 252.08298, 130, 1, [0, 2, 3],
1643             'Eu', 151.964, 120, 1, [0, 2, 3],
1644             'F', 18.9984032, 400, 0, [-1, 0],
1645             'Fe', 55.845, 183, 1, [0, 2, 3, 6, 8, 4, 5], # -2, -1, 1
1646             'Fm', 257.095105, 130, 1, [0, 2, 3],
1647             'Fr', 223.0197359, 70, 1, [0, 1],
1648             'Ga', 69.723, 181, 1, [0, 1, 2, 3],
1649             'Gd', 157.25, 120, 1, [0, 3], # 1, 2
1650             'Ge', 72.64, 201, 0, [-4, -2, 0, 2, 4], # 1, 3
1651             'H', 1.00794, 220, 0, [-1, 0, 1],
1652             'He', 4.002602, 0, 0, [0],
1653             'Hf', 178.49, 130, 1, [0, 2, 3, 4],
1654             'Hg', 200.59, 200, 1, [0, 1, 2], # 4
1655             'Ho', 164.93032, 123, 1, [0, 3],
1656             'Hs', 270.13465, 0, 1, [0, 7],
1657             'I', 126.90447, 266, 0, [-1, 0, 1, 3, 5, 7],
1658             'In', 114.818, 178, 1, [0, 1, 2, 3],
1659             'Ir', 192.217, 220, 1, [0, 1, 2, 3, 4, 5, 6, 8], # -3, -1
1660             'K', 39.0983, 82, 1, [0, 1],
1661             'Kr', 83.798, 0, 0, [0, 2, 4, 6],
1662             'Ku', 265.1167, 0, 1, [0, 4], # now Rf
1663             'La', 138.90547, 110, 1, [0, 3], # 2
1664             'Li', 6.941, 98, 1, [0, 1],
1665             'Lr', 262.10963, 129, 1, [0, 3],
1666             'Lu', 174.9668, 127, 1, [0, 3],
1667             'Md', 258.098431, 130, 1, [0, 2, 3],
1668             'Mg', 24.305, 131, 1, [0, 2],
1669             'Mn', 54.938045, 155, 1, [0, 1, 2, 3, 4, 5, 6, 7], # -3, -2, -1
1670             'Mo', 95.96, 216, 1, [0, 2, 3, 4, 5, 6], # -2, -1, 1
1671             'Mt', 276.15116, 0, 1, [0, 4],
1672             'N', 14.0067, 304, 0, [-3, -2, -1, 0, 1, 2, 3, 4, 5],
1673             'Na', 22.98976928, 93, 1, [0, 1],
1674             'Nb', 92.90638, 160, 1, [0, 1, 2, 3, 4, 5], # -1
1675             'Nd', 144.242, 114, 1, [0, 3], # 2
1676             'Ne', 20.1797, 0, 0, [0],
1677             'Ni', 58.6934, 191, 1, [0, 2, 3, 4, 1], # -1
1678             'No', 259.10103, 130, 1, [0, 2, 3],
1679             'Np', 237, 136, 1, [0, 3, 4, 5, 6], # 7
1680             'Ns', 268.12545, 0, 1, [0, 5], # now Db
1681             'O', 15.9994, 344, 0, [-2, -1, 0, 1, 2],
1682             'Os', 190.23, 220, 1, [0, 2, 3, 4, 5, 6, 7, 8], # -2, -1, 1
1683             'P', 30.973762, 221, 0, [-3, -2, 0, 1, 3, 4, 5], # -1, 2
1684             'Pa', 231.03588, 150, 1, [0, 3, 4, 5],
1685             'Pb', 207.2, 233, 1, [-4, 0, 2, 4],
1686             'Pd', 106.42, 220, 1, [0, 1, 2, 3, 4],
1687             'Pm', 145, 113, 1, [0, 3],
1688             'Po', 209, 200, 1, [-2, 0, 2, 4, 6],
1689             'Pr', 140.90765, 113, 1, [0, 3, 4], # 2
1690             'Pt', 195.084, 228, 1, [0, 2, 3, 4, 5, 6, 1],
1691             'Pu', 244, 128, 1, [0, 2, 3, 4, 5, 6], # 7
1692             'Ra', 226, 90, 1, [0, 2, 4],
1693             'Rb', 85.4678, 82, 1, [0, 1],
1694             'Re', 186.207, 190, 1, [0, 1, 2, 3, 4, 5, 6, 7], # -3, -1
1695             'Rf', 265.1167, 0, 1, [0, 4], # old Ku
1696             'Rg', 280.16447, 0, 1, [0, 3, 1, 2], # as Au
1697             'Rh', 102.9055, 228, 1, [0, 1, 2, 3, 4, 6], # -1, 5
1698             'Rn', 222, 0, 0, [0, 2, 4, 6, 8],
1699             'Ru', 101.07, 220, 1, [0, 2, 3, 4, 5, 6, 7, 8], # -2, 1
1700             'S', 32.065, 258, 0, [-2, 0, 1, 2, 3, 4, 6], #-1, 5
1701             'Sb', 121.76, 221, 1, [-3, 0, 3, 4, 5],
1702             'Sc', 44.955912, 136, 1, [0, 3], # 1, 2
1703             'Se', 78.96, 255, 0, [-2, 0, 2, 4, 6],
1704             'Sg', 271.13347, 0, 1, [0, 6],
1705             'Si', 28.0855, 190, 0, [-4, 0, 2, 4], # -3, -2, -1, 1, 3
1706             'Sm', 150.36, 117, 1, [0, 2, 3],
1707             'Sn', 118.71, 196, 1, [-4, -2, 0, 2, 4],
1708             'Sr', 87.62, 95, 1, [0, 2],
1709             'T', 3.0160492777, 220, 0, [-1, 0, 1],
1710             'Ta', 180.94788, 150, 1, [0, 1, 2, 3, 4, 5], # -1
1711             'Tb', 158.92535, 110, 1, [0, 3, 4], # 1
1712             'Tc', 97.9072, 190, 1, [0, 1, 2, 3, 4, 5, 6, 7], # -3, -1
1713             'Te', 127.6, 221, 0, [-2, 0, 2, 4, 6], # 5
1714             'Th', 232.03806, 130, 1, [0, 2, 3, 4],
1715             'Ti', 47.867, 154, 1, [-2, 0, 2, 3, 4], # -1
1716             'Tl', 204.3833, 162, 1, [0, 1, 3],
1717             'Tm', 168.93421, 125, 1, [0, 2, 3],
1718             'Tn', 220, 0, 0, [0, 2, 4, 6, 8], # as Rn^220
1719             'U', 238.02891, 138, 1, [0, 3, 4, 5, 6],
1720             'V', 50.9415, 163, 1, [0, 2, 3, 4, 5], # -1, 1
1721             'W', 183.84, 220, 1, [0, 2, 3, 4, 5, 6], # -2, -1, 1
1722             'Xe', 131.293, 0, 0, [0, 1, 2, 4, 6, 8],
1723             'Y', 88.90585, 122, 1, [0, 3], # 1, 2
1724             'Yb', 173.054, 110, 1, [0, 2, 3],
1725             'Zn', 65.38, 165, 1, [0, 2],
1726             'Zr', 91.224, 133, 1, [0, 2, 3, 4], # 1
1727             ]
1728             }
1729              
1730              
1731             # Attention!
1732             # ion-group mask consist individual atoms only
1733             sub _ions_db{
1734             return [
1735             # hydroxide
1736 200     200   8798 '[^O]OH(?![efgos])', 'H=1_O=-2',
1737             '.a~O', 'a~Cl,Br=1_O=-2',
1738             '.IO', 'I=1!3_O=-2',
1739             # oxychloride: phosgene (carbonyl dichloride), thionyl...
1740             '.OCl2', 'Cl=-1_O=-2',
1741             # '.OCl2', 'Cl=-1;1_O=-2',
1742             # meta- antimonites, arsenites and others
1743             '.a~O2', 'a~Sb,Al,Ni,As,Au,Co,Ga,Cl,Br,B=3_O=-2',
1744             # nitrites, dioxynitrates
1745             '.NO2', 'N=3!2_O=-2',
1746             '.BO3', 'B=3_O=-2',
1747             # carbonates, selenates, tellurates
1748             '.a~O3', 'a~C,Se,Si,Ni,Te,Pt,Mo,Po,Mn,Fe,Ti,Zr,Hf,Re=4_O=-2',
1749             # bismuthates, nitrates and others
1750             '.a~O3', 'a~Bi,N,V,Cl,Br,I,Nb,Ta=5_O=-2',
1751             # plumbates, silicates
1752             '.a~O4', 'a~Pb,Si,Ge,Ti=4_O=-2',
1753             # ortho- antimonates, arsenates, phosphates and others
1754             '.a~O4', 'a~Sb,As,P,V,Ta,Nb=5_O=-2',
1755             # molybdates, tungstates, chromates and others (excepting peroxides)
1756             '.a~O4(?=[^O]|Os|$)', 'a~Kr,U,S,Se,Te,Mo,W,Cr,Pu,Os=6_O=-2',
1757             # per- chlorates, bromates
1758             '.a~O4', 'a~Cl,Br=7_O=-2',
1759             # rhodanides (thiocyanates) and for selenium
1760             '.(?:a~CN|a~NC|CNa~|Ca~N|Na~C|NCa~)(?![a-gik-pr-u])', 'a~S,Se=-2_C=4_N=-3',
1761             # ortho-/meta- : /phosphi(-a)tes, antimoni(-a)tes, arseni(-a)tes
1762             '.a~O3', 'a~P,Sb,As=5!3_O=-2',
1763              
1764             '.a~O4', 'a~I,Re=6!7_O=-2',
1765             '.a~O4', 'a~Tc,Ru=5!6!7_O=-2',
1766             '.MnO4', 'Mn=3!4!5!6!7_O=-2',
1767             # ferrate
1768             '.FeO4', 'Fe=3!4!6_O=-2',
1769              
1770             '.a~O5', 'a~Fe,Pu=6_O=-2',
1771             '.ReO5', 'Re=7_O=-2',
1772              
1773             '.I(?:O[56]|2O9)', 'I=7_O=-2',
1774             '^I2O4$', 'I=3;5_O=-2',
1775             '^I4O9$', 'I=3;5;5;5_O=-2',
1776              
1777             '.SnO6', 'Sn=4_O=-2',
1778             '.SbO6', 'Sb=5_O=-2',
1779             '.a~O6', 'a~Te,Am=6_O=-2',
1780             # perxenic acid
1781             '.XeO6', 'Xe=6!8_O=-2',
1782              
1783             '.MoO3F3', 'Mo=6_F=-1_O=-2',
1784             # sulfamic acid salts
1785             '.NSO3', 'S=6_O=-2_N=-3',
1786             # thiazates, thionitrites
1787             '.(?:NSO|SNO|NOS)(?=[\]\)\}]|$)', 'N=3_S=-2_O=-2',
1788             # sulphites
1789             '.SO3(?=[^OS]|Os|S[bcegimnr]|$)', 'S=4!6_O=-2',
1790             # thiosulphates
1791             '.S2O3', 'S=6;-2_O=-2',
1792             # peroxydisulfuric (marshal's) acid
1793             '.S2O8', 'S=6_O=-2;-2;-2;-2;-2;-2;-1;-1',
1794              
1795             '.a~2O7', 'a~P,Re=5_O=-2',
1796             # pirosulphates, bichromates
1797             '.a~2O7', 'a~S,Cr=6_O=-2',
1798             # rhodane
1799             '^\((?:SCN|SNC|CNS|CSN|NSC|NCS)\)2$', 'S=1_C=2_N=-3',
1800             # cyan (dicyan)
1801             '^\((?:CN|NC)\)2$', 'C=4;2_N=-3',
1802             # cyanamides
1803             '.CN2', 'C=4_N=-3',
1804             # cyanides
1805             '.CN', 'C=2_N=-3',
1806             # cyanates (salts of cyanic, isocyanic acid)
1807             '.CNO', 'C=4_N=-3_O=-2',
1808             # fulminates (salts of fulminic acid)
1809             '.ONC', 'C=-2_N=3_O=-2',
1810             # flaveanic hydrogen
1811             '^C2(?:H2N2S|N2SH2|SH2N2|SN2H2)$', 'C=2;4_H=1_S=-2_N=-3',
1812             # rubeanic hydrogen
1813             '^C2S2N2H4$', 'C=2;4_H=1_S=-2_N=-3',
1814             # salts of peroxonitric acid or orthonitrates
1815             '.NO4', 'N=5_O=-1;-1;-2;-2!-2',
1816             # salts of hyponitrous acid
1817             '.N2O2', 'N=1_O=-2',
1818              
1819             # salts of hyponitrates (триоксодиазотной) acid
1820             '.N2O3', 'N=2_O=-2',
1821              
1822             # salts of nitroxylic acid
1823             '.N2O4', 'N=2_O=-2',
1824             # salts of hydrazonic acid and azides (pernitrides)
1825             '(?:.[\[\(]?N3[\]\)]?|^N3H$)', 'N=-3;4;-2',
1826             '^(?:a~3\(N2\)2|a~3N4)$', 'a~Ca=2_N=-2;-2;-2;0', # OSE ?
1827             # diimide
1828             '^N2H2$', 'H=1_N=-2;0', # OSE ?
1829             # nitrosyl- group | ion
1830             '(?:[\[\(]NO[\]\)])', 'N=2!3_O=-2',
1831             '^a~(?:[\[\(]NO[\]\)])\d*$', 'a~Fe,Ru,Cr=0_N=2_O=-2',
1832             # hydroxylamine & its salts
1833             'NH[23]O.', 'H=1_N=-1_O=-2',
1834             # ammonia, amide
1835             'NH[234]', 'H=1_N=-3',
1836              
1837             '.BF4', 'B=3_F=-1',
1838             # hypophosphorous
1839             '.PO2', 'P=1_O=-2',
1840             # polyphosphates
1841             '.P2O4', 'P=2_O=-2',
1842             '.P2O5', 'P=3;3!2;4_O=-2',
1843             '.P2O6', 'P=4;4!3;5_O=-2',
1844             '.P3O8', 'P=3;4;4_O=-2',
1845             '.P6O12', 'P=3_O=-2',
1846              
1847             # Neutral ligands
1848             'CO\(NH2\)2', 'C=4_H=1_N=-3_O=-2',
1849             'H2O(?=[^2O]|Os|$)', 'H=1_O=-2',
1850             'C2H4', 'H=1_C=-2',
1851             # metal ammiakaty
1852             '^a~\(NH3\)\d*$', 'a~Ca=0_H=1_N=-3',
1853              
1854             '^(?:HOF|HFO|OFH|FOH)$', 'H=1_F=1_O=-2',
1855              
1856             '^Bi2O4$', 'Bi=3;5_O=-2',
1857             '^B4H10$', 'B=2;2;3;3_H=-1',
1858             '^Fe3C$', 'Fe=2;2;0_C=-4',
1859             '^Fe3P$', 'Fe=3;0;0_P=-3',
1860             '^P4S3$', 'P=1_S=-2;-2;0',
1861             '^P4S7$', 'P=1_S=-2;-2;0;0;0;0;0',
1862             '^P4S10$', 'P=1_S=-2;-2;0;0;0;0;0;0;0;0',
1863             '^P12H6$', 'H=1_P=-3;-3;0;0;0;0;0;0;0;0;0;0',
1864             # compound oxide
1865             '^U3O8$', 'U=5;5;6_O=-2', # triuranium octoxide
1866             '^Pb2O3$', 'Pb=2;4_O=-2',
1867             '^Sb2O4$', 'Sb=3;5_O=-2',
1868             '^Ag2O2$', 'Ag=1;3_O=-2', # silver peroxide
1869             '^a~3O4$', 'a~Pb,Pt=2;2;4_O=-2',
1870             '^a~3O4$', 'a~Fe,Co,Mn=2;3;3_O=-2',
1871             # carbonyls
1872             '^a~\d*\(CO\)\d*$', 'a~V,W,Cr,Ir,Mn,Fe,Co,Ni,Mo,Tc,Re,Ru,Rh,Os=0_C=2_O=-2',
1873             # alkaline metals and others
1874             '^a~2O2', 'a~H,Li,Na,K,Rb,Cs,Fr,Hg=1_O=-1', # peroxides
1875             '^(?:a~O2|a~2O4)', 'a~Li,Na,K,Rb,Cs,Fr=1_O=-1;0', # superoxides
1876             '^a~O3', 'a~Li,Na,K,Rb,Cs,Fr=1_O=-1;0;0', # ozonide
1877             # alkaline-earth metals and others
1878             '^a~O2', 'a~Mg,Ca,Sr,Ba,Ra,Zn,Cd,Hg,Cu=2_O=-1', # peroxides
1879             '^(?:a~\(O2\)2|a~O4)', 'a~Mg,Ca,Sr,Ba,Ra=2_O=-1;0', # superoxides
1880             '^(?:a~\(O3\)2|a~O6)', 'a~Mg,Ca,Sr,Ba,Ra=2_O=-1;0;0', # ozonide
1881             # all peroxides
1882             '.\(O2\)', 'O=-1',
1883             # dioxygenils, O2PtF6 ... (except O2F2)
1884             '^(?:\(O2\)|O2)(?![F])', 'O=1;0',
1885             # chromium peroxide
1886             '^CrO5$', 'Cr=6_O=-2;-1;-1;-1;-1',
1887             '^Cr2O8$', 'Cr=6_O=-2;-2;-2;-2;-1;-1;-1;-1',
1888             # rhenium, iodine, chlorine peroxide
1889             '^a~2O8$', 'a~Re,I,Cl=7_O=-2;-2;-2;-2;-2;-2;-1;-1',
1890             # sulfur peroxide
1891             '^SO4$', 'S=6_O=-2;-2;-1;-1',
1892             '^S2O7$', 'S=6_O=-2;-2;-2;-2;-2;-1;-1',
1893             # peroxymonosulfuric | persulfuric | Caro's acid
1894             '.SO5', 'S=6_O=-2;-2;-2;-1;-1',
1895             # per-carbonates (percarbonic acid)
1896             '.C2O6', 'C=4_O=-2;-2;-2;-2;-1;-1',
1897             # acid chlorine peroxide ?
1898             '.ClO5', 'Cl=7_O=-2;-2;-2;-1;-1',
1899             # dioxodifluorochlorate
1900             '.ClO2F2', 'Cl=5_O=-2_F=-1',
1901             # oxotetrafluorochlorate
1902             '.ClOF4', 'Cl=5_O=-2_F=-1',
1903             # oxofluorides
1904             '.ClO3F2', 'Cl=7_O=-2_F=-1',
1905             '.ClO2F4', 'Cl=7_O=-2_F=-1',
1906             # platinum hexafluoride (strongest oxidizer)
1907             '.PtF[6-9][\]\)]?$', 'Pt=5_F=-1',
1908             # chlorine nitrides
1909             '^(?:Cl3N|NCl3)$', 'Cl=1_N=-3',
1910             '.(?:ClN|NCl)', 'Cl=1_N=-3',
1911              
1912             '^Fe2P', 'P=5_Fe=-2;-3',
1913             # exotic
1914             '^FNO3$', 'F=1_N=5_O=-2',
1915             ]
1916             }
1917              
1918              
1919             1;
1920             __END__