"""
A module to parse and store the results of a MOPAC7.1 calculation.
It interfaces directly with the Fortran code to populate the output variables
from memory.
"""
# Contributors: Mike Beachy, Mark A. Watson
import re
from itertools import zip_longest
import numpy
import schrodinger.infra.mopac as _mop
from schrodinger.application.jaguar import file_logger
MOPAC71 = 'MOPAC71'
[docs]class MopacResults71():
"""
A class to parse and store the results of a MOPAC7.1 calculation.
"""
MULLIK = "MULLIK"
ESP = "ESP"
SUPER = "SUPER"
UHF = "UHF"
PLOTESP = "PLOTESP"
PLOTALIE = "PLOTALIE"
PLOTALEA = "PLOTALEA"
scf_OK = int(_mop.molkst_c.scfstatus_ok)
scf_FAIL = int(_mop.molkst_c.scfstatus_fail)
exit_status_OK = int(_mop.molkst_c.moperrno_ok)
exit_status_FATAL = int(_mop.molkst_c.moperrno_fatal)
exit_status_BAD_INPUT = int(_mop.molkst_c.moperrno_bad_input)
# Molecular properties
energy_prop = "r_NDDO_NDDO_Heat_of_Formation"
method_prop = "s_NDDO_SemiEmpirical_Method"
homo_prop = "r_NDDO_HOMO_Energy"
lumo_prop = "r_NDDO_LUMO_Energy"
ahomo_prop = "r_NDDO_Alpha_HOMO_Energy"
alumo_prop = "r_NDDO_Alpha_LUMO_Energy"
bhomo_prop = "r_NDDO_Beta_HOMO_Energy"
blumo_prop = "r_NDDO_Beta_LUMO_Energy"
hardness_prop = "r_NDDO_Molecular_Hardness"
eneg_prop = "r_NDDO_Molecular_Electronegativity"
se_tot_prop = "r_NDDO_Total_Electrophilic_Superdelocalizability"
sn_tot_prop = "r_NDDO_Total_Nucleophilic_Superdelocalizability"
sr_tot_prop = "r_NDDO_Total_Radical_Superdelocalizability"
asp_tot_prop = "r_NDDO_Total_Atom_Self_Polarizability"
dipole_prop = "r_NDDO_Dipole"
dipole_x_prop = "r_NDDO_Dipole_X"
dipole_y_prop = "r_NDDO_Dipole_Y"
dipole_z_prop = "r_NDDO_Dipole_Z"
dipole_esp_prop = "r_NDDO_Dipole_ESP"
dipole_esp_x_prop = "r_NDDO_Dipole_ESP_X"
dipole_esp_y_prop = "r_NDDO_Dipole_ESP_Y"
dipole_esp_z_prop = "r_NDDO_Dipole_ESP_Z"
min_esp_prop = "r_NDDO_Min_ESP_On_Mol_Surface"
max_esp_prop = "r_NDDO_Max_ESP_On_Mol_Surface"
mean_esp_prop = "r_NDDO_Mean_ESP_On_Mol_Surface"
mean_pos_esp_prop = "r_NDDO_Mean_Pos_ESP_On_Mol_Surface"
mean_neg_esp_prop = "r_NDDO_Mean_Neg_ESP_On_Mol_Surface"
sig_pos_esp_prop = "r_NDDO_Pos_ESP_Variance_On_Mol_Surface"
sig_neg_esp_prop = "r_NDDO_Neg_ESP_Variance_On_Mol_Surface"
sig_tot_esp_prop = "r_NDDO_Tot_ESP_Variance_On_Mol_Surface"
balance_esp_prop = "r_NDDO_ESP_Balance_On_Mol_Surface"
local_pol_esp_prop = "r_NDDO_Avg_Abs_Dev_from_Mean_ESP_On_Mol_Surface"
min_alie_prop = "r_NDDO_Min_ALIE_On_Mol_Surface"
max_alie_prop = "r_NDDO_Max_ALIE_On_Mol_Surface"
mean_alie_prop = "r_NDDO_Mean_ALIE_On_Mol_Surface"
mean_pos_alie_prop = "r_NDDO_Mean_Pos_ALIE_On_Mol_Surface"
mean_neg_alie_prop = "r_NDDO_Mean_Neg_ALIE_On_Mol_Surface"
sig_pos_alie_prop = "r_NDDO_Pos_ALIE_Variance_On_Mol_Surface"
sig_neg_alie_prop = "r_NDDO_Neg_ALIE_Variance_On_Mol_Surface"
sig_tot_alie_prop = "r_NDDO_Tot_ALIE_Variance_On_Mol_Surface"
balance_alie_prop = "r_NDDO_ALIE_Balance_On_Mol_Surface"
local_pol_alie_prop = "r_NDDO_Avg_Abs_Dev_from_Mean_ALIE_On_Mol_Surface"
min_alea_prop = "r_NDDO_Min_ALEA_On_Mol_Surface"
max_alea_prop = "r_NDDO_Max_ALEA_On_Mol_Surface"
mean_alea_prop = "r_NDDO_Mean_ALEA_On_Mol_Surface"
mean_pos_alea_prop = "r_NDDO_Mean_Pos_ALEA_On_Mol_Surface"
mean_neg_alea_prop = "r_NDDO_Mean_Neg_ALEA_On_Mol_Surface"
sig_pos_alea_prop = "r_NDDO_Pos_ALEA_Variance_On_Mol_Surface"
sig_neg_alea_prop = "r_NDDO_Neg_ALEA_Variance_On_Mol_Surface"
sig_tot_alea_prop = "r_NDDO_Tot_ALEA_Variance_On_Mol_Surface"
balance_alea_prop = "r_NDDO_ALEA_Balance_On_Mol_Surface"
local_pol_alea_prop = "r_NDDO_Avg_Abs_Dev_from_Mean_ALEA_On_Mol_Surface"
# Atomic properties
mulliken_charge_prop = "r_NDDO_Mulliken_Charge"
atomic_charge_prop = "r_NDDO_NDDO_Charge"
esp_charge_prop = "r_NDDO_ESP_Charge"
fe_prop = "r_NDDO_Electrophilic_Frontier_Electron_Density"
fn_prop = "r_NDDO_Nucleophilic_Frontier_Electron_Density"
se_prop = "r_NDDO_Electrophilic_Superdelocalizability"
sn_prop = "r_NDDO_Nucleophilic_Superdelocalizability"
sr_prop = "r_NDDO_Radical_Superdelocalizability"
asp_prop = "r_NDDO_Atom_Self_Polarizability"
maxat_esp_prop = "r_NDDO_Max_surface_ESP"
minat_esp_prop = "r_NDDO_Min_surface_ESP"
maxat_alie_prop = "r_NDDO_Max_surface_ALIE"
minat_alie_prop = "r_NDDO_Min_surface_ALIE"
maxat_alea_prop = "r_NDDO_Max_surface_ALEA"
minat_alea_prop = "r_NDDO_Min_surface_ALEA"
[docs] def __init__(self, mopfile):
#
# General attributes
#
self.mopfile = mopfile
self.error_text = self.get_error_text()
self.scf_status = int(_mop.molkst_c.iscf)
self.geopt_status = int(_mop.molkst_c.iflepo)
self.molchg = int(_mop.molkst_c.molchg)
self._keywords = str(_mop.molkst_c.prevkeywrd, 'utf-8').rstrip()
self._return_code = None
self._structure = None
self._method = None
self._output_file = None
@property
def structure(self):
return self._structure
@property
def method(self):
return self._method
@property
def output_file(self):
return self._output_file
@property
def statusOk(self):
"""
Check the summary status of the job, looking at SCF status and
return code of the main routine.
Return True if all is ok, False if not.
"""
if (self.scf_status != self.scf_OK or
self._return_code != self.exit_status_OK):
return False
else:
return True
[docs] def set_method(self, value):
self._method = value
[docs] def set_output_file(self, value):
self._output_file = value
[docs] def set_return_code(self, value):
self._return_code = value
[docs] def set_count(self, value):
# Determine the calculation count based on the current value of
# numcal and the last stored value of numcal.
self._calc_count = _mop.molkst_c.numcal - value - 1
[docs] def populate_from_memory(self):
"""
Populate data derived from the f2py wrapped code.
"""
# Local aliases
numat = _mop.molkst_c.numat
molkst_c = _mop.molkst_c
perm_arrays = _mop.permanent_arrays
charges_c = _mop.charges_c
esp_c = _mop.esp_c
properties = _mop.properties_c
if self.scf_status == self.scf_OK:
self.escf = float(molkst_c.escf)
self.ehomo = float(properties.ehomo)
self.elumo = float(properties.elumo)
if self.check_key(self.UHF):
self.ehomo_b = float(properties.ehomo_b)
self.elumo_b = float(properties.elumo_b)
else:
self.ehomo_b = None
self.elumo_b = None
self.dipole = float(molkst_c.utotal)
self.dipole_x = float(molkst_c.ux)
self.dipole_y = float(molkst_c.uy)
self.dipole_z = float(molkst_c.uz)
else:
self.escf = None
self.ehomo = None
self.elumo = None
self.dipole = None
self.dipole_x = None
self.dipole_y = None
self.dipole_z = None
self.geopt_status = int(molkst_c.iflepo)
# TODO: add checks on geopt_status. The iflepo meanings are recorded
# in the flepo array defined in src_subroutines/writmo.F90.
self.geometry = numpy.array(perm_arrays.coord[:, :numat]).transpose()
# We do charge-fitting if either ESP is set or PLOTESP is set, but
# we don't calculate the dipole from the charges unless the
# molecular charge is 0.
if (self.check_key(self.ESP) or
self.check_key(self.PLOTESP)) and self.molchg == 0:
self.dipole_esp = float(molkst_c.dipole_esp)
self.dipole_esp_x = float(molkst_c.dipole_esp_x)
self.dipole_esp_y = float(molkst_c.dipole_esp_y)
self.dipole_esp_z = float(molkst_c.dipole_esp_z)
else:
self.dipole_esp = None
self.dipole_esp_x = None
self.dipole_esp_y = None
self.dipole_esp_z = None
if self.check_key(self.SUPER):
self.eneg = float(properties.eneg)
self.hardness = float(properties.hardness)
self.se_tot = float(properties.se_tot)
self.sn_tot = float(properties.sn_tot)
self.sr_tot = float(properties.sr_tot)
self.asp_tot = float(properties.asp_tot)
else:
self.eneg = None
self.hardness = None
self.se_tot = None
self.sn_tot = None
self.sr_tot = None
self.asp_tot = None
if self.check_key(self.PLOTESP):
self.min_esp = float(properties.min_esp)
self.max_esp = float(properties.max_esp)
self.mean_esp = float(properties.mean_esp)
self.mean_pos_esp = float(properties.mean_pos_esp)
self.mean_neg_esp = float(properties.mean_neg_esp)
self.sig_pos_esp = float(properties.sig_pos_esp)
self.sig_neg_esp = float(properties.sig_neg_esp)
self.sig_tot_esp = float(properties.sig_tot_esp)
self.balance_esp = float(properties.balance_esp)
self.local_pol_esp = float(properties.local_pol_esp)
else:
self.min_esp = None
self.max_esp = None
self.mean_esp = None
self.mean_pos_esp = None
self.mean_neg_esp = None
self.sig_pos_esp = None
self.sig_neg_esp = None
self.sig_tot_esp = None
self.balance_esp = None
self.local_pol_esp = None
if self.check_key(self.PLOTALIE):
self.min_alie = float(properties.min_alie)
self.max_alie = float(properties.max_alie)
self.mean_alie = float(properties.mean_alie)
self.mean_pos_alie = float(properties.mean_pos_alie)
self.mean_neg_alie = float(properties.mean_neg_alie)
self.sig_pos_alie = float(properties.sig_pos_alie)
self.sig_neg_alie = float(properties.sig_neg_alie)
self.sig_tot_alie = float(properties.sig_tot_alie)
self.balance_alie = float(properties.balance_alie)
self.local_pol_alie = float(properties.local_pol_alie)
else:
self.min_alie = None
self.max_alie = None
self.mean_alie = None
self.mean_pos_alie = None
self.mean_neg_alie = None
self.sig_pos_alie = None
self.sig_neg_alie = None
self.sig_tot_alie = None
self.balance_alie = None
self.local_pol_alie = None
if self.check_key(self.PLOTALEA):
self.min_alea = float(properties.min_alea)
self.max_alea = float(properties.max_alea)
self.mean_alea = float(properties.mean_alea)
self.mean_pos_alea = float(properties.mean_pos_alea)
self.mean_neg_alea = float(properties.mean_neg_alea)
self.sig_pos_alea = float(properties.sig_pos_alea)
self.sig_neg_alea = float(properties.sig_neg_alea)
self.sig_tot_alea = float(properties.sig_tot_alea)
self.balance_alea = float(properties.balance_alea)
self.local_pol_alea = float(properties.local_pol_alea)
else:
self.min_alea = None
self.max_alea = None
self.mean_alea = None
self.mean_pos_alea = None
self.mean_neg_alea = None
self.sig_pos_alea = None
self.sig_neg_alea = None
self.sig_tot_alea = None
self.balance_alea = None
self.local_pol_alea = None
## Atomic properties
self.atomic_charge = numpy.array(charges_c.q2)
if self.check_key(self.MULLIK):
self.mulliken_charge = numpy.array(charges_c.mulliken)
else:
self.mulliken_charge = None
## We do charge-fitting if either ESP is set or PLOTESP is set
if self.check_key(self.ESP) or \
self.check_key(self.PLOTESP):
self.esp_charge = numpy.array(esp_c.qesp[:numat])
else:
self.esp_charge = None
if self.check_key(self.PLOTESP):
self.maxat_esp = numpy.array(properties.maxat_esp[:numat])
self.minat_esp = numpy.array(properties.minat_esp[:numat])
else:
self.maxat_esp = None
self.minat_esp = None
if self.check_key(self.PLOTALIE):
self.maxat_alie = numpy.array(properties.maxat_alie[:numat])
self.minat_alie = numpy.array(properties.minat_alie[:numat])
else:
self.maxat_alie = None
self.minat_alie = None
if self.check_key(self.PLOTALEA):
self.maxat_alea = numpy.array(properties.maxat_alea[:numat])
self.minat_alea = numpy.array(properties.minat_alea[:numat])
else:
self.maxat_alea = None
self.minat_alea = None
if self.check_key(self.SUPER):
self.fe = numpy.array(properties.fe)
self.fn = numpy.array(properties.fn)
self.se = numpy.array(properties.se)
self.sn = numpy.array(properties.sn)
self.sr = numpy.array(properties.sr)
self.asp = numpy.array(properties.asp)
else:
self.fe = None
self.fn = None
self.se = None
self.sn = None
self.sr = None
self.asp = None
[docs] def check_key(self, keyword):
"""
If the given keyword was set in the job specification, return True,
otherwise return False.
"""
if self._keywords.find(" " + keyword.upper()) != -1:
return True
else:
return False
[docs] def set_final_structure(self, structure):
"""
Set the final structure and update the coordinates to match the
final geometry.
"""
self._structure = structure
self._structure.setXYZ(self.geometry)
# Used to create .smap files
self._structure.property[file_logger.JNAME] = self.mopfile
# Molecular properties
if self.escf is not None:
self._structure.property[self.energy_prop] = self.escf
if self._method:
self._structure.property[self.method_prop] = self._method
if self.dipole is not None:
self._structure.property[self.dipole_prop] = self.dipole
self._structure.property[self.dipole_x_prop] = self.dipole_x
self._structure.property[self.dipole_y_prop] = self.dipole_y
self._structure.property[self.dipole_z_prop] = self.dipole_z
if self.dipole_esp is not None:
self._structure.property[self.dipole_esp_prop] = self.dipole_esp
self._structure.property[self.dipole_esp_x_prop] = self.dipole_esp_x
self._structure.property[self.dipole_esp_y_prop] = self.dipole_esp_y
self._structure.property[self.dipole_esp_z_prop] = self.dipole_esp_z
if self.check_key(self.UHF):
if self.ehomo is not None:
self._structure.property[self.ahomo_prop] = self.ehomo
if self.elumo is not None:
self._structure.property[self.alumo_prop] = self.elumo
if self.ehomo_b is not None:
self._structure.property[self.bhomo_prop] = self.ehomo_b
if self.elumo_b is not None:
self._structure.property[self.blumo_prop] = self.elumo_b
else:
if self.ehomo is not None:
self._structure.property[self.homo_prop] = self.ehomo
if self.elumo is not None:
self._structure.property[self.lumo_prop] = self.elumo
if self.eneg is not None:
self._structure.property[self.eneg_prop] = self.eneg
if self.hardness is not None:
self._structure.property[self.hardness_prop] = self.hardness
if self.se_tot is not None:
self._structure.property[self.se_tot_prop] = self.se_tot
if self.sn_tot is not None:
self._structure.property[self.sn_tot_prop] = self.sn_tot
if self.sr_tot is not None:
self._structure.property[self.sr_tot_prop] = self.sr_tot
if self.asp_tot is not None:
self._structure.property[self.asp_tot_prop] = self.asp_tot
if self.min_esp is not None:
self._structure.property[self.min_esp_prop] = self.min_esp
if self.max_esp is not None:
self._structure.property[self.max_esp_prop] = self.max_esp
if self.mean_esp is not None:
self._structure.property[self.mean_esp_prop] = self.mean_esp
if self.mean_pos_esp is not None:
self._structure.property[self.mean_pos_esp_prop] = self.mean_pos_esp
if self.mean_neg_esp is not None:
self._structure.property[self.mean_neg_esp_prop] = self.mean_neg_esp
if self.sig_pos_esp is not None:
self._structure.property[self.sig_pos_esp_prop] = self.sig_pos_esp
if self.sig_neg_esp is not None:
self._structure.property[self.sig_neg_esp_prop] = self.sig_neg_esp
if self.sig_tot_esp is not None:
self._structure.property[self.sig_tot_esp_prop] = self.sig_tot_esp
if self.balance_esp is not None:
self._structure.property[self.balance_esp_prop] = self.balance_esp
if self.local_pol_esp is not None:
self._structure.property[
self.local_pol_esp_prop] = self.local_pol_esp
if self.min_alie is not None:
self._structure.property[self.min_alie_prop] = self.min_alie
if self.max_alie is not None:
self._structure.property[self.max_alie_prop] = self.max_alie
if self.mean_alie is not None:
self._structure.property[self.mean_alie_prop] = self.mean_alie
if self.mean_pos_alie is not None:
self._structure.property[
self.mean_pos_alie_prop] = self.mean_pos_alie
if self.mean_neg_alie is not None:
self._structure.property[
self.mean_neg_alie_prop] = self.mean_neg_alie
if self.sig_pos_alie is not None:
self._structure.property[self.sig_pos_alie_prop] = self.sig_pos_alie
if self.sig_neg_alie is not None:
self._structure.property[self.sig_neg_alie_prop] = self.sig_neg_alie
if self.sig_tot_alie is not None:
self._structure.property[self.sig_tot_alie_prop] = self.sig_tot_alie
if self.balance_alie is not None:
self._structure.property[self.balance_alie_prop] = self.balance_alie
if self.local_pol_alie is not None:
self._structure.property[
self.local_pol_alie_prop] = self.local_pol_alie
if self.min_alea is not None:
self._structure.property[self.min_alea_prop] = self.min_alea
if self.max_alea is not None:
self._structure.property[self.max_alea_prop] = self.max_alea
if self.mean_alea is not None:
self._structure.property[self.mean_alea_prop] = self.mean_alea
if self.mean_pos_alea is not None:
self._structure.property[
self.mean_pos_alea_prop] = self.mean_pos_alea
if self.mean_neg_alea is not None:
self._structure.property[
self.mean_neg_alea_prop] = self.mean_neg_alea
if self.sig_pos_alea is not None:
self._structure.property[self.sig_pos_alea_prop] = self.sig_pos_alea
if self.sig_neg_alea is not None:
self._structure.property[self.sig_neg_alea_prop] = self.sig_neg_alea
if self.sig_tot_alea is not None:
self._structure.property[self.sig_tot_alea_prop] = self.sig_tot_alea
if self.balance_alea is not None:
self._structure.property[self.balance_alea_prop] = self.balance_alea
if self.local_pol_alea is not None:
self._structure.property[
self.local_pol_alea_prop] = self.local_pol_alea
# Atomic properties
for at, c in zip(self._structure.atom, self.atomic_charge):
at.property[self.atomic_charge_prop] = c
# Also set charge1 and charge2.
at.partial_charge = c
at.solvation_charge = c
if self.mulliken_charge is not None:
for at, c in zip(self._structure.atom, self.mulliken_charge):
at.property[self.mulliken_charge_prop] = c
if self.esp_charge is not None:
for at, c in zip(self._structure.atom, self.esp_charge):
at.property[self.esp_charge_prop] = c
if self.maxat_esp is not None:
for at, c in zip(self._structure.atom, self.maxat_esp):
at.property[self.maxat_esp_prop] = c
if self.minat_esp is not None:
for at, c in zip(self._structure.atom, self.minat_esp):
at.property[self.minat_esp_prop] = c
if self.maxat_alie is not None:
for at, c in zip(self._structure.atom, self.maxat_alie):
at.property[self.maxat_alie_prop] = c
if self.minat_alie is not None:
for at, c in zip(self._structure.atom, self.minat_alie):
at.property[self.minat_alie_prop] = c
if self.maxat_alea is not None:
for at, c in zip(self._structure.atom, self.maxat_alea):
at.property[self.maxat_alea_prop] = c
if self.minat_alea is not None:
for at, c in zip(self._structure.atom, self.minat_alea):
at.property[self.minat_alea_prop] = c
# The following 6 properties are all initialized to 0.0 in the
# fortran code, but they are not actually defined for hydrogen
# atoms, so don't set them here for hydrogens (or dummies).
if self.fe is not None:
for at, c in zip(self._structure.atom, self.fe):
if at.atomic_number > 1:
at.property[self.fe_prop] = c
if self.fn is not None:
for at, c in zip(self._structure.atom, self.fn):
if at.atomic_number > 1:
at.property[self.fn_prop] = c
if self.se is not None:
for at, c in zip(self._structure.atom, self.se):
if at.atomic_number > 1:
at.property[self.se_prop] = c
if self.sn is not None:
for at, c in zip(self._structure.atom, self.sn):
if at.atomic_number > 1:
at.property[self.sn_prop] = c
if self.sr is not None:
for at, c in zip(self._structure.atom, self.sr):
if at.atomic_number > 1:
at.property[self.sr_prop] = c
if self.asp is not None:
for at, c in zip(self._structure.atom, self.asp):
if at.atomic_number > 1:
at.property[self.asp_prop] = c
[docs] def get_error_text(self):
"""
Get the error text set during the main MOPAC calculation.
If there is no error text, returns the empty string.
"""
return str(_mop.molkst_c.errtxt, 'utf-8').rstrip()
[docs] def write_vis_files(self):
"""
This function is not needed in this API
"""
return True
[docs] def populate_from_outfile(self, structures):
"""
Parse a MOPAC .out file for a limited set of properties and populate
an associated Structure instance with the parsed values.
Currently only "semi-empirical method" (e.g. AM1, PM3) and
"Final Heat of Formation" are supported.
We consider a subjob to have failed for a given structure if any of these
properties fail to be populated from the output file. i.e. we expect any
type of MOPAC job will generate these properties.
This method should be used when running a multi-structure MOPAC input
file when the f2py interface cannot be used to get results.
:type structures: list
:param structures: list of Structure objects
:return: list of bools denoting job status of each subjob
"""
# Regex for regular decimal float
DNUM_REGEX = r'(\s*\+?\-?\d+\.\d+)'
PROPERTIES = {
'energy_prop':
(re.compile(r'FINAL HEAT OF FORMATION\s+=' + DNUM_REGEX), float
),
'method_prop': (re.compile(r' (\S*)\s*CALCULATION RESULTS'), str)
}
results = []
# Parse .mop output file for selected properties and store them
with open(self._output_file, 'r') as fh:
for line in fh:
if line.strip() == 'Schrodinger semiempirical NDDO module':
# Create a new result container for this subjob
result = dict((k, None) for k in PROPERTIES.keys())
elif line.strip() == '== DONE ==':
# Save the result and reset
results.append(result)
result = dict((k, None) for k in PROPERTIES.keys())
# Search line for any relevant data to store
for prop, (pattern, cast) in PROPERTIES.items():
match = pattern.search(line)
if match:
result[prop] = cast(match.group(1))
# Populate structure properties with the parsed data.
# If lists have different lengths, we assume results has been truncated
# due to e.g. job crash, so we only update CT's as far as we can.
all_ok = []
null = dict((k, None) for k in PROPERTIES.keys())
i = 0
for ct, result in zip_longest(structures, results, fillvalue=null):
i += 1
ok = True
for k, v in result.items():
if v is not None:
ct.property[getattr(self, k)] = v
else:
# Consider job to have failed if any of the expected
# properties fail to be populated
print(f'Failed to parse {k} for job {ct.title}')
ok = False
all_ok.append(ok)
if len(all_ok) != len(structures):
msg = 'Error text-parsing MOPAC7.1 output file.\n'
msg += 'len(ok) = %d\n' % len(ok)
msg += 'len(structures) = %d' % len(structures)
raise RuntimeError(msg)
return all_ok