Skip to content

Commit 1e0ef99

Browse files
andyclaude
andcommitted
MOPAC: read orbitals from the GRAPHF file, and teach tests/e2e File= types
MOPAC's printed EIGENVECTORS block is the weaker source for orbitals. Its GRAPHF file (<job>.mgf) carries the same data better: precision Fortran doubles, against four printed decimals occupancy one field per orbital, so mopac.orbocc no longer has to reconstruct it from atomic orbital populations plus the open-shell count passed in argv shape free-form, not the fixed-width column groups that merged two coefficients into one for ORCA (#108) ai.mopac now asks for GRAPHF alongside VECTORS -- they serve different readers, VECTORS the person reading the output and GRAPHF ECCE -- and a new File= parse type reads it, the same mechanism Gaussian uses for fort.7 and GAMESS-UK for ftn058. Both entries emit the same keys on purpose. The file is delivered after the matched blocks, so it wins where it exists, and the printed block stays as the fallback for decks generated before GRAPHF was requested. That matters: a calculation run an hour ago would otherwise lose its orbitals. The .mgf also records the Slater exponents of the valence basis, which is what a 3-D orbital renderer for a semiempirical code would need. Nothing reads them yet, so they are deliberately not emitted rather than shipped unused. tests/e2e previously skipped File= parse types entirely, silently -- the codes' most important data for three of them. It now decodes the jmFILE announcements and runs the scripts as the client does. Two traps in that path, both now handled and documented: importDir MsgSendFile resolves "$idir$file", concatenated with no separator, and idir defaults to "-". Unset, every File= type looks for "-fort.7" and is skipped with no error. deck name MOPAC names its outputs after its input, so a fixture called ch4_mos.mop writes ch4_mos.mgf while mopac.desc names mopac.mgf -- correct, since ECCE always writes mopac.mop. The fixtures are renamed to match, and the case now asserts the GRAPHF file really was the source by the precision of the energies: eight decimals from the file, six from the printed block. That assertion was checked both ways -- pointing File= at a name that does not exist makes it fail with "6 decimals; 6 means the fallback ran", and restoring it passes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EFMUNbw2UpRPmgsqviAowb
1 parent 56569a7 commit 1e0ef99

10 files changed

Lines changed: 335 additions & 8 deletions

File tree

‎eccejobmonitor.propbuf‎

9.72 KB
Binary file not shown.

‎scripts/parsers/ai.mopac‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,8 +325,16 @@ sub KeywordLine {
325325
# all, and treating absent as "0" silently gave those calculations no
326326
# orbitals -- which is every calculation anyone already had. Only an
327327
# explicit "0" turns it off.
328+
# GRAPHF as well as VECTORS. They serve different readers: VECTORS
329+
# puts the eigenvectors in the output file where a person can read
330+
# them, GRAPHF writes <job>.mgf, which ECCE parses instead because it
331+
# carries full-precision coefficients, the occupancies and the
332+
# symmetry labels in one machine-readable place -- see mopac.mgf.
333+
# GRAPHF also records the Slater exponents, which is what a 3-D
334+
# orbital renderer for a semiempirical code would need.
328335
my $printMOs = $AbiDict{"ES.Theory.SCF.PrintMOs"};
329-
push(@kw, "VECTORS", "ALLVEC") if (!defined($printMOs) || $printMOs ne "0");
336+
push(@kw, "VECTORS", "ALLVEC", "GRAPHF")
337+
if (!defined($printMOs) || $printMOs ne "0");
330338

331339
# PRECISE: "TIGHTER CRITERIA TO BE USED" (binary's own banner text).
332340
push(@kw, "PRECISE") if ($AbiDict{"ES.Theory.SCF.Precise"} eq "1");

‎scripts/parsers/mopac.desc‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,27 @@ Frequency=last
187187
End=HEATS OF FORMATION ARE RELATIVE
188188
[END]
189189

190+
# MO data from MOPAC's GRAPHF file rather than its printed output.
191+
#
192+
# A File= parse type, the same mechanism Gaussian uses for fort.7 and
193+
# GAMESS-UK for ftn058: eccejobmonitor ships the named file whole and
194+
# the script parses it, instead of matching Begin/End in the job's
195+
# stdout.
196+
#
197+
# This entry and the printed-eigenvector one above emit the SAME keys,
198+
# deliberately. The .mgf is the better source -- full-precision
199+
# coefficients, occupancies and symmetry labels all in one place -- but
200+
# it only exists when the deck asked for GRAPHF, which decks generated
201+
# before that did not. Both are kept so an older calculation still
202+
# gets its orbitals from the printed block, and a new one gets the
203+
# better data. Whichever is delivered later wins, which is what the
204+
# test fixture pins.
205+
[MOFILE]
206+
Script=mopac.mgf
207+
File=mopac.mgf
208+
Begin=mopac.mgf
209+
[END]
210+
190211
[ORBOCC][ORBOCCBETA]
191212
Script=mopac.orbocc
192213
Begin=ATOMIC ORBITAL ELECTRON POPULATIONS

‎scripts/parsers/mopac.mgf‎

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
#!/usr/bin/env perl
2+
################################################################################
3+
#
4+
# Purpose:
5+
# Parses MOPAC's GRAPHF file (<job>.mgf) into MO/ORBENG/ORBOCC/ORBSYM and
6+
# the UHF beta counterparts.
7+
#
8+
# WHY THIS RATHER THAN THE PRINTED EIGENVECTORS. mopac.mo reads the
9+
# human-readable "EIGENVECTORS" block out of the main output, which works
10+
# but is the weaker source in three ways:
11+
#
12+
# precision the printed block carries four decimals; this file carries
13+
# Fortran D-notation doubles.
14+
# occupancy the printed block has none, so mopac.orbocc has to
15+
# reconstruct it from the atomic orbital populations plus
16+
# the open-shell count ECCE passes in argv. Here it is
17+
# simply the second field of each ORBITAL line.
18+
# shape the printed block is fixed-width and column-grouped, the
19+
# exact shape that merged two coefficients into one for
20+
# ORCA (issue #108). This file is free-form.
21+
#
22+
# Real MOPAC 23.1.2 shape:
23+
#
24+
# 3 MOPAC-Graphical data Version 23.1.2
25+
# 8 0.0000000 -0.0000030 0.1149079 -0.6491 <- Z x y z charge
26+
# 1 0.0000000 0.7594689 -0.4644394 0.3246
27+
# 1 -0.0000000 -0.7594570 -0.4644300 0.3246
28+
# 5.9723090 2.3490170 0.0000000 <- zeta s p d
29+
# 1.2602370 0.0000000 0.0000000
30+
# 1.2602370 0.0000000 0.0000000
31+
# ORBITAL 2 1a1 -29.6190 <- occ sym E(eV)
32+
# 0.85155170D+00-0.28614321D-16 ... <- coefficients
33+
#
34+
# The atom count on line 1 gives the size of BOTH leading blocks: one
35+
# coordinate line and one exponent line per atom. The exponent lines are
36+
# the Slater exponents of the valence basis, which is what a future
37+
# 3-D orbital renderer would need; nothing reads them yet, so they are
38+
# deliberately not emitted as a property rather than shipped unused.
39+
#
40+
# Energies are eV here and Hartree in ECCE (MoPanel hard-codes the column
41+
# label), so they are converted.
42+
#
43+
# The symmetry label is a single token such as "1a1", "2b2" or "1a''" --
44+
# an index within the irrep followed by the irrep itself. Only the irrep
45+
# is kept: MoPanel::fillGraph() groups the orbital plot by this string,
46+
# so keeping the index would put every orbital in a group of its own.
47+
#
48+
################################################################################
49+
50+
$| = 1;
51+
52+
use constant EV_PER_HARTREE => 27.211386245988;
53+
54+
my @lines;
55+
while (<STDIN>) { chomp; push(@lines, $_); }
56+
57+
exit(0) if (!@lines);
58+
59+
# Line 1 begins with the atom count.
60+
my $natoms = 0;
61+
$natoms = $1 if ($lines[0] =~ /^\s*(\d+)\s/);
62+
exit(0) if ($natoms <= 0);
63+
64+
# Coordinates then exponents, one line each per atom. The number of
65+
# basis functions is not stated anywhere, so it is taken from the
66+
# coefficient count of the first orbital instead.
67+
my $idx = 1 + 2 * $natoms;
68+
69+
my (@occ, @eng, @sym, @coeff);
70+
while ($idx <= $#lines) {
71+
my $line = $lines[$idx];
72+
if ($line =~ /^\s*ORBITAL\s+(\S+)\s+(\S+)\s+(-?[\d.]+)\s*$/) {
73+
my ($o, $s, $e) = ($1, $2, $3);
74+
# "1a1" -> "a1"; "2a''" -> "a''". A label that is nothing but the
75+
# irrep is left alone.
76+
$s =~ s/^\d+//;
77+
push(@occ, $o);
78+
push(@eng, $e / EV_PER_HARTREE);
79+
push(@sym, $s);
80+
push(@coeff, []);
81+
$idx++;
82+
# Coefficients run until the next line that is not purely numbers.
83+
#
84+
# Terminating on a whitelist of what a coefficient line looks like,
85+
# rather than a blacklist of what follows, because more follows than
86+
# is obvious: after the last orbital MOPAC writes an
87+
# "INVERSE_MATRIX[6x6]=" block of the same D-notation numbers, and a
88+
# loop that stopped only at "ORBITAL" or "Keywords:" swallowed all
89+
# of it into the final orbital -- 27 coefficients where the other
90+
# five had 6. That then failed the rectangular check below and the
91+
# script emitted nothing at all, silently.
92+
while ($idx <= $#lines &&
93+
$lines[$idx] =~ /^[-+\s\d.DdEe]+$/ &&
94+
$lines[$idx] =~ /\d[DdEe][-+]?\d/) {
95+
my $text = $lines[$idx];
96+
# Fortran D exponents, and values packed without separators:
97+
# "-0.63614535D-01-0.45803202D-14" is two numbers, not one.
98+
while ($text =~ /(-?\d*\.\d+[DdEe][-+]?\d+)/g) {
99+
my $v = $1;
100+
$v =~ s/[Dd]/e/;
101+
push(@{$coeff[-1]}, $v + 0);
102+
}
103+
$idx++;
104+
}
105+
next;
106+
}
107+
$idx++;
108+
}
109+
110+
my $nmo = scalar(@eng);
111+
exit(0) if ($nmo == 0);
112+
113+
my $nbas = scalar(@{$coeff[0]});
114+
exit(0) if ($nbas == 0);
115+
# Every orbital must have the same number of coefficients, or the table
116+
# is not rectangular and PropTable rejects it on load with "input vector
117+
# length does not match rows*columns" -- which presents as the Orbitals
118+
# panel refusing to compute, not as a parse error.
119+
foreach my $c (@coeff) {
120+
exit(0) if (scalar(@$c) != $nbas);
121+
}
122+
123+
# UHF writes the alpha set then the beta set into the same file, so twice
124+
# the basis size means both are present.
125+
my $half = ($nmo == 2 * $nbas) ? $nbas : $nmo;
126+
127+
sub emitSet {
128+
my ($from, $count, $moKey, $engKey, $occKey, $symKey) = @_;
129+
return if ($count <= 0);
130+
131+
print "key: $engKey\n";
132+
print "size:\n$count\n";
133+
print "values:\n";
134+
printf("%.8f ", $eng[$from + $_]) foreach (0 .. $count - 1);
135+
print "\nunits:\nHartree\n";
136+
print "END\n";
137+
138+
print "key: $occKey\n";
139+
print "size:\n$count\n";
140+
print "values:\n";
141+
printf("%.4f ", $occ[$from + $_]) foreach (0 .. $count - 1);
142+
print "\nunits:\nelectrons\n";
143+
print "END\n";
144+
145+
print "key: $symKey\n";
146+
print "size:\n$count\n";
147+
print "rowlabels:\n";
148+
print " " . ($_ + 1) . " " foreach (0 .. $count - 1);
149+
print "\nvalues:\n";
150+
print join(" ", @sym[$from .. $from + $count - 1]), "\n";
151+
print "END\n";
152+
153+
print "key: $moKey\n";
154+
print "size:\n$count $nbas\n";
155+
print "rowlabels:\n";
156+
for my $m (1 .. $count) {
157+
print "MO-$m ";
158+
print "\n" if (($m % 8) == 0);
159+
}
160+
print "\ncolumnlabels:\n";
161+
for my $b (1 .. $nbas) {
162+
print "BasFun-$b ";
163+
print "\n" if (($b % 6) == 0);
164+
}
165+
print "\nvalues:\n";
166+
for my $m (0 .. $count - 1) {
167+
my $row = $coeff[$from + $m];
168+
printf("%.8f ", $_) foreach (@$row);
169+
print "\n";
170+
}
171+
print "END\n";
172+
}
173+
174+
&emitSet(0, $half, "MO", "ORBENG", "ORBOCC", "ORBSYM");
175+
&emitSet($half, $nmo - $half,
176+
"MOBETA", "ORBENGBETA", "ORBOCCBETA", "ORBSYMBETA")
177+
if ($nmo > $half);
178+
exit(0);

‎tests/e2e/README.md‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,39 @@ properties first — a coefficient matrix exceeds `msgLength` and arrives
7575
split, so treating each packet as a message truncates exactly the
7676
properties most worth testing.
7777

78+
## File= parse types
79+
80+
Some codes do not put their best data in the job output at all. Gaussian
81+
writes MO coefficients to `fort.7`, GAMESS-UK to `ftn058`, MOPAC to the
82+
GRAPHF file. Those are `File=` parse types, and they work differently:
83+
eccejobmonitor never matches them against the output. It sends a
84+
**jmFILE** message naming the file and the parse type, and the client
85+
fetches that file and runs the script on it. The message carries no
86+
content.
87+
88+
`pipeline.unpack_files()` decodes those announcements and `run_parsers()`
89+
acts on them, after the matched blocks -- which is the monitor's own
90+
order, `JobFilesGet()` running after `JobOutputGet()`. That ordering is
91+
load-bearing where both kinds of entry emit the same keys: MOPAC's
92+
GRAPHF file is the better source for orbitals, but only newer decks
93+
request it, so the printed-eigenvector entry remains as a fallback and
94+
the file wins when present.
95+
96+
Two traps found wiring this up:
97+
98+
- **`importDir`**. In anything but live mode `MsgSendFile` resolves the
99+
name against `$idir$file` -- concatenated, no separator -- and `idir`
100+
defaults to `"-"`. Left unset, every `File=` parse type silently looks
101+
for `-fort.7`, finds nothing, and is skipped with no error. The
102+
harness passes the work directory with a trailing separator.
103+
- **The deck's name**. MOPAC derives every output name from the input
104+
name, so a fixture called `ch4_mos.mop` writes `ch4_mos.mgf` while
105+
`mopac.desc` names `mopac.mgf` -- correct, because ECCE always writes
106+
`mopac.mop`. The fixtures are therefore named the way ECCE names them,
107+
and `mopac-ch4-mos` asserts the GRAPHF file really was the source by
108+
checking the precision of the energies: eight decimals from the file,
109+
six from the printed block.
110+
78111
## Assertion style
79112

80113
Loose about exact numbers, strict about structure. Pinning an energy to

‎tests/e2e/cases.py‎

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,22 @@ def expect_mopac_ch4(props, report):
6464
report.check(occ[:4] == [2.0] * 4 and occ[4:] == [0.0] * 4,
6565
'four filled levels then virtuals')
6666

67+
# THE GRAPHF FILE MUST BE THE SOURCE, not the printed eigenvector
68+
# block. Both parse types emit these keys and the File= one is
69+
# delivered last, so it wins -- but only if MOPAC actually wrote it
70+
# and mopac.desc's File= name matched, neither of which is visible
71+
# from the values alone.
72+
#
73+
# The tell is precision: mopac.mo prints six decimals because that
74+
# is all the printed block has, mopac.mgf prints eight because the
75+
# GRAPHF file carries Fortran doubles. A value that stops at six
76+
# means the fallback ran and the better source was missed.
77+
decimals = max(len(t.split('.')[1]) for t in
78+
sect(orbeng[-1], 'values').split() if '.' in t)
79+
report.check(decimals >= 8,
80+
'ORBENG came from the GRAPHF file, not the printed block '
81+
'(%d decimals; 6 means the fallback ran)' % decimals)
82+
6783
# Energies must be in Hartree, not the eV MOPAC prints. MoPanel
6884
# hard-codes its column label to "Energy Hartree", so a missing
6985
# conversion shows numbers 27x too large with no error at all.
@@ -178,20 +194,20 @@ def expect_nwchem_h2o(props, report):
178194
name='mopac-ch4-mos',
179195
code='mopac',
180196
desc='mopac.desc',
181-
deck='mopac/ch4_mos.mop',
197+
deck='mopac/mos/mopac.mop',
182198
# ". <runtype> <theory category> <theory name> <open shells>",
183199
# as DavCalculation::getParseScriptArgs builds it.
184200
parse_args=('.', 'Energy', 'SE', 'PM7', '0'),
185-
output='ch4_mos.out',
201+
output='mopac.out',
186202
expect=expect_mopac_ch4,
187203
),
188204
dict(
189205
name='mopac-ch4-thermo',
190206
code='mopac',
191207
desc='mopac.desc',
192-
deck='mopac/ch4_thermo.mop',
208+
deck='mopac/thermo/mopac.mop',
193209
parse_args=('.', 'Vibration', 'SE', 'PM7', '0'),
194-
output='ch4_thermo.out',
210+
output='mopac.out',
195211
expect=expect_mopac_ch4_thermo,
196212
),
197213
dict(
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
PM7 VECTORS ALLVEC CHARGE=0
1+
PM7 VECTORS ALLVEC GRAPHF CHARGE=0
22
ECCE MOPAC calculation
33

44
C 0.000000 1 0.000000 1 0.000000 1

0 commit comments

Comments
 (0)