import pyorbb
import numpy as np
import itertools as it # noqa: F401
import matplotlib.pyplot as plt
import os
import networkx as nx # noqa: F401
import warnings
warnings.filterwarnings('ignore')
ensure_list = lambda x: [x] if not isinstance(x, (list, tuple, set)) else list(x) # noqa: E731
INTERACTION_COLORS = {'OI': '#00FF00', 'PR': '#FF0000', 'Sanitization': '#FF00FF', 'Multiple': '#000000'}
[docs]
class Mixer:
def __init__(self, orbs: pyorbb.Orbitals or str, pr_min_thresh=1e-3, oi_min_thresh=1e-7, oi_max_N=50, pr_max_N=50):
self.orbs = orbs
if isinstance(orbs, str):
self.orbs = pyorbb.Orbitals(str)
self.fmos = self.orbs.fmos.orbitals
self.allowed_mos = self.orbs.mos.orbitals
self.allowed_fmos = self.orbs.fmos.orbitals
self.data = {}
self.main_mix = Mixing(self.orbs)
self.mixes = {'OI': {}, 'PR': {}}
self.energy_type = 'energy'
self.set_enable_oi(True)
self.set_enable_pr(True)
self.set_allowed_mos(self.orbs.mos.orbitals)
self.set_allowed_fmos(self.orbs.fmos.orbitals)
self._prepare()
self.oi_min_thresh = oi_min_thresh
self.pr_min_thresh = pr_min_thresh
self.oi_max_N = oi_max_N
self.pr_max_N = pr_max_N
self._get_orbital_interactions()
self._get_pauli_repulsions()
self.set_energy_type('energy')
[docs]
def find_two_mixing(self, orb1, orb2):
# self.reset_mixes()
for mix in self.main_mix.two_mixings:
if len(mix.fmos) > 2:
continue
if len(mix.mos) > 2:
continue
if not (orb1 in mix.fmos or orb1 in mix.mos):
continue
if not (orb2 in mix.fmos or orb2 in mix.mos):
continue
if mix.fraction is None:
continue
return mix
def _prepare(self):
'''
Prepare the data used to construct the mixing situations.
We construct an array ``Ei`` for each FMO energy-type we have in our system.
The values of the array indicate the strength and type of interaction.
Positive values indicate destabilizing Pauli repulsions, while
negative values indicate stabilizing orbital interactions.
'''
# prepare the data we will use
S = np.array([[fmo1.overlap(fmo2) for fmo1 in self.fmos] for fmo2 in self.fmos])
o = np.array([fmo.occupation for fmo in self.fmos])
p = np.array([fmo.gross_population for fmo in self.fmos])
# get the maximum occupation of an FMO
max_pop = 1 if self.orbs.data['calc_info']['unrestricted_fmos'] else 2
# make sure all populations are between 0 and max_pop
p = np.clip(p, 0, max_pop)
# mangle some data into various matrices
S2 = S*S # overlap squared
dp = (abs(p - o) * abs(p - o).reshape(-1, 1)) # electron gains and losses
P = p + p.reshape(-1, 1) # sum of FMO populations
O = o + o.reshape(-1, 1) # sum of occupations
# first max_pop electrons go to the bonding MO
Pbond = np.clip(P, 0, max_pop)
# any remaining electrons go to the anti-bonding MO
Panti = np.clip(P - Pbond, 0, max_pop)
# the number of electrons involved in pauli repulsion
Epr = np.clip(O - max_pop, 0, max_pop) * S2
# remove the upper echelon and diagonal
# this prevents FMO pair double counting
# and self-interactions
Epr = np.tril(Epr, k=-1)
for energy_type in self.orbs.fmo_energy_types:
# we calculate the Eoi for each energy type we have available
e = np.array([getattr(fmo, energy_type) for fmo in self.fmos])
de = abs(e - e.reshape(-1, 1)) # energy gaps
# calculate the non-degenerate orbital interaction terms
Eoi = - (Pbond - Panti) * dp * (S2 / de)
# for degenerate elements we replace S^2/de with S
degenerate_mask = np.isclose(de, 0, atol=0.002)
Eoi[degenerate_mask] = (-Pbond * dp * abs(S))[degenerate_mask]
# remove upper echelon plus diagonal
# since the matrix should be symmetric and the diagonal
# terms are the self-interactions
Eoi = np.tril(Eoi, k=-1)
self.data[energy_type] = (
Eoi, np.argsort(Eoi, axis=None),
Epr, np.argsort(-Epr, axis=None),
)
self.data['mo_occ'] = np.array([mo.occupied for mo in self.orbs.mos])
[docs]
def set_enable_oi(self, val):
self.enable_oi = val
[docs]
def set_enable_pr(self, val):
self.enable_pr = val
[docs]
def set_allowed_mos(self, allowed_mos):
self.allowed_mos = allowed_mos
[docs]
def set_allowed_fmos(self, allowed_fmos):
self.allowed_fmos = allowed_fmos
[docs]
def set_oi_threshold(self, thresh):
self.oi_threshold = thresh
self.oi_N = None
[docs]
def get_next_oi_threshold(self):
vals = [-val for val in self.mixes['OI'][self.energy_type].values() if -val <= self.oi_threshold]
if len(vals) == 0:
return self.oi_threshold
return vals[0]
[docs]
def get_previous_oi_threshold(self):
vals = [-val for val in self.mixes['OI'][self.energy_type].values() if -val >= self.oi_threshold]
if len(vals) == 0:
return self.oi_threshold
return vals[-1]
[docs]
def get_next_pr_threshold(self):
vals = [val for val in self.mixes['PR'][self.energy_type].values() if val <= self.pr_threshold]
if len(vals) == 0:
return self.pr_threshold
return vals[0]
[docs]
def get_previous_pr_threshold(self):
vals = [val for val in self.mixes['PR'][self.energy_type].values() if val >= self.pr_threshold]
if len(vals) == 0:
return self.pr_threshold
return vals[-1]
[docs]
def get_oi_default_threshold(self, fraction=0.7):
'''
Get a threshold that makes sure that at least 70% of the OI interactions are included.
'''
vals = list(sorted([abs(val) for val in self.mixes['OI'][self.energy_type].values()]))[::-1]
total = sum(vals)
fracs = [val/total for val in vals]
cumsum = np.cumsum(fracs)
idx = np.where(cumsum >= fraction)[0][0]
return vals[idx]
[docs]
def get_pr_default_threshold(self, fraction=0.1):
'''
Get a threshold that makes sure that at least 10% of the PR interactions are included.
'''
vals = list(sorted([abs(val) for val in self.mixes['PR'][self.energy_type].values()]))[::-1]
total = sum(vals)
fracs = [val/total for val in vals]
cumsum = np.cumsum(fracs)
idx = np.where(cumsum >= fraction)[0][0]
if len(vals) > 2:
return max(vals[idx], vals[2])
return vals[idx]
[docs]
def get_oi_fraction(self, threshold):
'''
Get the fraction of the total OI captured by a specific threshold.
'''
vals = list(sorted([abs(val) for val in self.mixes['OI'][self.energy_type].values()]))
total = sum(vals)
fracs = [val/total for val in vals if val >= threshold]
return sum(fracs)
[docs]
def get_pr_fraction(self, threshold):
'''
Get the fraction of the total OI captured by a specific threshold.
'''
vals = list(sorted([abs(val) for val in self.mixes['PR'][self.energy_type].values()]))
total = sum(vals)
fracs = [val/total for val in vals if val >= threshold]
return sum(fracs)
[docs]
def set_pr_threshold(self, thresh):
self.pr_threshold = thresh
self.pr_N = None
[docs]
def set_oi_N(self, N):
self.oi_threshold = None
self.oi_N = N
[docs]
def set_pr_N(self, N):
self.pr_threshold = None
self.pr_N = N
[docs]
def set_energy_type(self, typ):
self.energy_type = typ
if typ not in self.mixes['OI']:
self._get_orbital_interactions()
if typ not in self.mixes['PR']:
self._get_pauli_repulsions()
def _get_orbital_interactions(self):
Eoi, Eoi_order = self.data[self.energy_type][0], self.data[self.energy_type][1]
self._get_mixes(Eoi, Eoi_order, self.oi_min_thresh, 'OI')
def _get_pauli_repulsions(self):
Epr, Epr_order = self.data[self.energy_type][2], self.data[self.energy_type][3]
self._get_mixes(Epr, Epr_order, self.pr_min_thresh, 'PR')
def _get_mixes(self,
M,
order,
min_thresh,
interaction_type):
self.mixes[interaction_type][self.energy_type] = {}
n = 0
while 1:
i, j = np.unravel_index(order[n], M.shape)
v = M[i, j]
if abs(v) < min_thresh:
break
fmo1, fmo2 = self.fmos[i], self.fmos[j]
# if fmo1 not in self.allowed_fmos or fmo2 not in self.allowed_fmos:
# continue
mo1, mo2 = self._get_mos(fmo1, fmo2, interaction_type=interaction_type)
# if mo1 not in self.allowed_mos or mo2 not in self.allowed_mos:
# continue
mix = Mixing(self.orbs, [mo1, mo2], [fmo1, fmo2], fraction=v/np.sum(M), connection_type=interaction_type)
self.mixes[interaction_type][self.energy_type][mix] = v
n += 1
if interaction_type == 'OI':
if n == self.oi_max_N:
break
else:
if n == self.pr_max_N:
break
def _get_mos(self, fmo1, fmo2, interaction_type=None):
fmo1_contr = np.array([fmo1.mulliken_contribution(mo, normalized=True) for mo in self.orbs.mos.orbitals])
fmo2_contr = np.array([fmo2.mulliken_contribution(mo, normalized=True) for mo in self.orbs.mos.orbitals])
occ_contrs = abs(fmo1_contr * fmo2_contr) * self.data['mo_occ']
virt_contrs = abs(fmo2_contr * fmo1_contr) * (1-self.data['mo_occ'])
# handle orbital interactions
if interaction_type == 'OI':
occ_mo = self.orbs.mos.orbitals[argNmax(occ_contrs, 0)]
virt_mo = self.orbs.mos.orbitals[argNmax(virt_contrs, 0)]
return occ_mo, virt_mo
elif interaction_type == 'PR':
occ_mo1 = self.orbs.mos.orbitals[argNmax(occ_contrs, 0)]
occ_mo2 = self.orbs.mos.orbitals[argNmax(occ_contrs, 1)]
return occ_mo1, occ_mo2
[docs]
def reset_mixes(self):
self.main_mix = Mixing(self.orbs, energy_type=self.energy_type)
if self.enable_oi:
for mix, strength in self.mixes['OI'][self.energy_type].items():
if not any(mo in self.allowed_mos for mo in mix.mos):
continue
if any(fmo not in self.allowed_fmos for fmo in mix.fmos):
continue
if self.oi_threshold is not None and abs(strength) >= self.oi_threshold:
self.main_mix += mix
if self.enable_pr:
for mix, strength in self.mixes['PR'][self.energy_type].items():
if not any(mo in self.allowed_mos for mo in mix.mos):
continue
if any(fmo not in self.allowed_fmos for fmo in mix.fmos):
continue
if self.pr_threshold is not None and abs(strength) >= self.pr_threshold:
self.main_mix += mix
self.sanitize()
[docs]
def sanitize(self):
max_pop = 1 if self.orbs.data['calc_info']['unrestricted_fmos'] else 2
for spin in ['A', 'B']:
for symm in self.orbs.mos.symmetry:
relevant_mos = [mo for mo in self.orbs.mos if mo.spin in (spin, 'AB') and mo.symmetry == symm]
relevant_mix_mos = [mo for mo in relevant_mos if mo in self.main_mix.mos]
N_virt_MO = len([mo for mo in relevant_mix_mos if mo.occupation == 0])
N_occ_MO = len([mo for mo in relevant_mix_mos if mo.occupation > 0])
# allowed_fmos = [fmo for fmo in self.orbs.fmos]
relevant_fmos = [fmo for fmo in self.orbs.fmos if fmo.spin in (spin, 'AB') and fmo.symmetry == symm]
relevant_mix_fmos = [fmo for fmo in relevant_fmos if fmo in self.main_mix.fmos]
N_elec_FMO = round(sum([fmo.gross_population for fmo in relevant_mix_fmos]))
N_virt_FMO = len(relevant_mix_fmos) - N_elec_FMO/max_pop
N_occ_FMO = len(relevant_mix_fmos) - N_virt_FMO
# checking some requirements
missing_occ_MOs = N_occ_MO < N_occ_FMO
missing_occ_FMOs = N_occ_FMO < N_occ_MO
missing_virt_MOs = N_virt_MO < N_virt_FMO
missing_virt_FMOs = N_virt_FMO < N_virt_MO
if not any([missing_occ_MOs, missing_occ_FMOs, missing_virt_MOs, missing_virt_FMOs]):
continue
## GENERATE CANDIDATE MOs AND FMOs
candidate_occ_mos = {}
candidate_virt_mos = {}
for mo in relevant_mos:
if mo in self.main_mix.mos:
continue
# skip if we don't need the occupied MOs
if mo.occupied and not missing_occ_MOs:
continue
# same for virtual
if not mo.occupied and not missing_virt_MOs:
continue
highest = 0
for i in range(len(relevant_mix_fmos)):
C1 = relevant_mix_fmos[i].mulliken_contribution(mo, normalized=True)
for j in range(i+1, len(relevant_mix_fmos)):
C2 = relevant_mix_fmos[j].mulliken_contribution(mo, normalized=True)
if abs(C1 * C2) > highest:
highest = abs(C1*C2)
if mo.occupied:
candidate_occ_mos[mo] = highest
else:
candidate_virt_mos[mo] = highest
candidate_occ_mos = sorted(candidate_occ_mos.items(), key=lambda r: -r[1])
candidate_virt_mos = sorted(candidate_virt_mos.items(), key=lambda r: -r[1])
candidate_occ_fmos = {}
candidate_virt_fmos = {}
for fmo in relevant_fmos:
if fmo in relevant_mix_fmos:
continue
# skip if we don't need the occupied FMOs
if fmo.occupied and not missing_occ_FMOs:
continue
# same for virtual
if not fmo.occupied and not missing_virt_FMOs:
continue
highest = 0
for i in range(len(relevant_mix_mos)):
C1 = fmo.mulliken_contribution(relevant_mix_mos[i], normalized=True)
for j in range(i+1, len(relevant_mix_mos)):
C2 = fmo.mulliken_contribution(relevant_mix_mos[j], normalized=True)
if abs(C1 * C2) > highest:
highest = abs(C1*C2)
if fmo.occupied:
candidate_occ_fmos[fmo] = highest
else:
candidate_virt_fmos[fmo] = highest
candidate_occ_fmos = sorted(candidate_occ_fmos.items(), key=lambda r: -r[1])
candidate_virt_fmos = sorted(candidate_virt_fmos.items(), key=lambda r: -r[1])
# ADD MOs and FMOs BASED ON UNMET REQUIREMENTS
if missing_occ_MOs:
N_occ_MO_missing = N_occ_FMO - N_occ_MO
for i in range(int(N_occ_MO_missing)):
self.main_mix.add_mo(candidate_occ_mos[i][0])
if missing_occ_FMOs:
N_occ_FMO_missing = N_occ_MO - N_occ_FMO
for i in range(int(N_occ_FMO_missing)):
self.main_mix.add_fmo(candidate_occ_fmos[i][0])
if missing_virt_MOs:
N_virt_MO_missing = N_virt_FMO - N_virt_MO
for i in range(int(N_virt_MO_missing)):
self.main_mix.add_mo(candidate_virt_mos[i][0])
if missing_virt_FMOs:
N_virt_FMO_missing = N_virt_MO - N_virt_FMO
for i in range(int(N_virt_FMO_missing)):
self.main_mix.add_fmo(candidate_virt_fmos[i][0])
[delattr(fmo, '_display_occupation') for fmo in self.main_mix.fmos if hasattr(fmo, '_display_occupation')]
for mix in self.split():
# check if there are too many electrons in the mos compared to the fmos
total_mo_occ = sum(mo.occupation for mo in mix.mos)
total_fmo_occ = sum(fmo.occupation for fmo in mix.fmos)
diff = total_mo_occ - total_fmo_occ
# if there are more electrons in the mos we add an electron to the FMOs
if diff > 0:
while diff > 0:
max_fmo = max([fmo for fmo in mix.fmos if not hasattr(fmo, '_display_occupation')], key=lambda fmo: fmo.gross_population)
max_fmo._display_occupation = min(diff, max_pop)
diff -= max_fmo._display_occupation
else:
while diff < 0:
min_fmo = min([fmo for fmo in mix.fmos if not hasattr(fmo, '_display_occupation')], key=lambda fmo: fmo.gross_population)
min_fmo._display_occupation = min_fmo.occupation - min(min_fmo.occupation, abs(diff))
# print(min_fmo, min_fmo._display_occupation, to_remove, diff)
diff += min(min_fmo.occupation, abs(diff))
[docs]
def split(self):
return self.main_mix.split()
[docs]
def draw_diagram(self, *args, **kwargs):
return self.main_mix.draw_diagram(*args, **kwargs)
@property
def connections(self):
return self.main_mix.connections
class _Mixer:
'''
The main class responsible for generating 2-mixing situations.
Args:
orbs:
'''
def __init__(self, orbs: pyorbb.Orbitals, energy_type: str = 'energy'):
self.orbs = orbs
self.energy_type = energy_type
self._prepare()
def _prepare(self):
self.fmos = {}
self.fmos_occ = {}
self.fmos_vir = {}
self.fmos_energy = {}
for i, frag in enumerate(self.orbs.fmos.fragments):
self.fmos[frag] = [fmo for fmo in self.orbs.fmos if fmo.fragment == frag]
self.fmos_occ[frag] = np.array([fmo.occupation > 0 for fmo in self.fmos[frag]]).reshape(-1, 1)
if not self.orbs.fmos.unrestricted:
self.fmos_vir[frag] = np.array([fmo.occupation < 2 for fmo in self.fmos[frag]]).reshape(-1, 1)
else:
self.fmos_vir[frag] = np.array([fmo.occupation < 1 for fmo in self.fmos[frag]]).reshape(-1, 1)
self.fmos_energy[frag] = np.array([getattr(fmo, self.energy_type) for fmo in self.fmos[frag]]).reshape(-1, 1)
self.mos = list(self.orbs.mos)
self.mo_occ = np.array([mo.occupied for mo in self.mos])
self.S_oi = {}
self.dE_oi = {}
self.oi = {}
self.oi_approx_total = 0
self.S_pauli = {}
self.pauli = {}
self.pauli_approx_total = 0
for i, frag in enumerate(self.orbs.fmos.fragments):
for frag2 in self.orbs.fmos.fragments[i+1:]:
# get data for oi
occ_virt_mask = np.logical_or(np.logical_and(self.fmos_occ[frag], self.fmos_vir[frag2].T), np.logical_and(self.fmos_vir[frag], self.fmos_occ[frag2].T))
self.S_oi[(frag, frag2)] = overlap_mat(self.fmos[frag], self.fmos[frag2])
self.dE_oi[(frag, frag2)] = abs(self.fmos_energy[frag] - self.fmos_energy[frag2].T)
nogap_mask = self.dE_oi[(frag, frag2)] != 0
self.dE_oi[(frag, frag2)] += (1 - nogap_mask)
self.oi[(frag, frag2)] = -self.S_oi[(frag, frag2)]**2 / self.dE_oi[(frag, frag2)] * occ_virt_mask
self.oi_approx_total += self.oi[(frag, frag2)].sum()
# get data for pauli
occ_occ_mask = np.logical_and(self.fmos_occ[frag], self.fmos_occ[frag2].T)
self.S_pauli[(frag, frag2)] = overlap_mat(self.fmos[frag], self.fmos[frag2])
self.pauli[(frag, frag2)] = self.S_pauli[(frag, frag2)]**2 * occ_occ_mask
self.pauli_approx_total += self.pauli[(frag, frag2)].sum()
self.oi_ref = self.orbs.reader.read('Energy', 'Orb.Int. Total') * 627.503
self.pauli_ref = self.orbs.reader.read('Energy', 'Pauli Total') * 627.503
def orbital_interactions(self, fraction_thresh=None, N=None):
"""
Yield the first ``N`` strongest orbital interactions.
"""
if fraction_thresh is None and N is None:
fraction_thresh = .03
elif N is not None:
fraction_thresh = None
ret = []
for i, frag in enumerate(self.orbs.fmos.fragments):
for frag2 in self.orbs.fmos.fragments[i+1:]:
j = 0
while 1:
best = np.unravel_index(argNmax(-self.oi[(frag, frag2)], j), self.oi[(frag, frag2)].shape)
best_oi = self.oi[(frag, frag2)][best]
fmo1, fmo2 = self.fmos[frag][best[0]], self.fmos[frag2][best[1]]
fmo1_contr = np.array([fmo1.mulliken_contribution(mo) for mo in self.mos])
fmo2_contr = np.array([fmo2.mulliken_contribution(mo) for mo in self.mos])
occ_contrs = abs(fmo1_contr * fmo2_contr) * self.mo_occ
virt_contrs = abs(fmo2_contr * fmo1_contr) * (1-self.mo_occ)
occ_mo = self.mos[np.argmax(occ_contrs)]
virt_mo = self.mos[np.argmax(virt_contrs)]
frac = best_oi / self.oi_approx_total
if fraction_thresh is None and j == N:
break
elif fraction_thresh is not None and frac < fraction_thresh:
break
stab = frac * self.oi_ref
typ = {conn: 'OI' for conn in list(it.product([fmo1, fmo2], [occ_mo, virt_mo]))}
mix = Mixing(self.orbs, [occ_mo, virt_mo], [fmo1, fmo2], stab, frac, connection_type=typ)
ret.append(mix)
j += 1
ret = sorted(ret, key=lambda row: row.strength)
if N is not None:
ret = ret[:N]
return ret
def pauli_repulsions(self, fraction_thresh=None, N=None):
"""
Yield the first ``N`` strongest Pauli repulsion interactions.
"""
if fraction_thresh is None and N is None:
fraction_thresh = .03
elif N is not None:
fraction_thresh = None
ret = []
for i, frag in enumerate(self.orbs.fmos.fragments):
for frag2 in self.orbs.fmos.fragments[i+1:]:
j = 0
while 1:
best = np.unravel_index(argNmax(self.pauli[(frag, frag2)], j), self.pauli[(frag, frag2)].shape)
best_pauli = self.pauli[(frag, frag2)][best]
fmo1, fmo2 = self.fmos[frag][best[0]], self.fmos[frag2][best[1]]
fmo1_contr = np.array([fmo1.mulliken_contribution(mo) * mo.occupation for mo in self.mos])
fmo2_contr = np.array([fmo2.mulliken_contribution(mo) * mo.occupation for mo in self.mos])
occ_contrs = abs(fmo1_contr * fmo2_contr)
occ_mo1_idx = argNmax(occ_contrs, 0)
occ_mo2_idx = argNmax(occ_contrs, 1)
occ_mo1 = self.mos[occ_mo1_idx]
occ_mo2 = self.mos[occ_mo2_idx]
frac = best_pauli / self.pauli_approx_total
if fraction_thresh is None and j == N:
break
elif fraction_thresh is not None and frac < fraction_thresh:
break
stab = frac * self.pauli_ref
typ = {conn: 'PR' for conn in list(it.product([fmo1, fmo2], [occ_mo1, occ_mo2]))}
mix = Mixing(self.orbs, [occ_mo1, occ_mo2], [fmo1, fmo2], stab, frac, connection_type=typ)
ret.append(mix)
j += 1
ret = sorted(ret, key=lambda row: -row.strength)
if N is not None:
ret = ret[:N]
return ret
[docs]
class Mixing:
def __init__(self, orbs, mos=None, fmos=None, strength=None, fraction=None, connections=None, connection_type=None, energy_type='energy'):
self.orbs = orbs
self.mos = mos or []
self.fmos = fmos or []
self.strength = strength or 0
self.fraction = fraction or 0
self.energy_type = energy_type
self.connections = connections
self.connection_type = connection_type
if connections is None:
self.connections = list(it.product(self.fmos, self.mos))
if connection_type is None:
self.connection_type = {conn: 'Multiple' for conn in self.connections}
if isinstance(connection_type, str):
self.connection_type = {conn: connection_type for conn in self.connections}
self.two_mixings = [self]
def __str__(self):
s = f'{self.__class__.__name__}('
s += f'[{", ".join([fmo.make_name(frag_name=True, relative_name=True, spin=True) for fmo in self.fmos])}]'
s += ' -> '
s += f'[{", ".join([mo.relative_name for mo in self.mos])}]'
if self.strength:
s += f', strength={self.strength:+.1f} kcal/mol'
if self.fraction:
s += f', fraction={self.fraction:.1%}'
s += f', nelectrons={self.nelectrons()})'
return s
@property
def PR_is_empty(self):
max_pop = 1 if self.orbs.data['calc_info']['unrestricted_fmos'] else 2
for mix in self.two_mixings:
if mix is self:
continue
if mix.nelectrons() == 2 * max_pop:
return False
return True
@property
def OI_is_empty(self):
max_pop = 1 if self.orbs.data['calc_info']['unrestricted_fmos'] else 2
for mix in self.two_mixings:
if mix is self:
continue
if mix.nelectrons() == max_pop:
return False
return True
[docs]
def add_mo(self, mo, typ='Sanitization', connections=None):
'''
Add an MO to this mixing diagram.
'''
self.mos.append(mo)
if connections is None:
for fmo in self.fmos:
if abs(fmo.mulliken_contribution(mo)) > 0.03:
self.connections.append((fmo, mo))
self.connection_type[(fmo, mo)] = typ
else:
self.connections.append(connections)
for conn in self.connections:
self.connection_type[conn] = typ
[docs]
def add_fmo(self, fmo, typ='Sanitization', connections=None):
'''
Add an FMO to this mixing diagram.
'''
self.fmos.append(fmo)
if connections is None:
for mo in self.mos:
if abs(fmo.mulliken_contribution(mo)) > 0.03:
self.connections.append((fmo, mo))
self.connection_type[(fmo, mo)] = typ
else:
self.connections.append(connections)
for conn in self.connections:
self.connection_type[conn] = typ
[docs]
def draw_diagram(self, ax=None, ylim=None, simple=False, **kwargs):
if simple:
pyorbb.plotting.simple_orbital_diagram.draw_interaction(self.fmos, self.mos, self.connections, None, energy_type=self.energy_type, connection_types=self.connection_type, ax=ax, ylim=ylim)
else:
pyorbb.plotting.orbital_diagram.draw_interaction(self.fmos, self.mos, self.connections, None, energy_type=self.energy_type, connection_types=self.connection_type, ax=ax, ylim=ylim, **kwargs)
[docs]
def draw_fmos(self, overlap=False, screen=None):
import tcviewer # noqa: F811
if screen is None:
scr = tcviewer.Screen()
scr.__enter__()
else:
scr = screen
for fmo in self.fmos:
with scr.add_molscene() as scene:
cub = fmo.cube_file()
scene.draw_text(str(fmo))
if fmo.occupied:
colors = ([1, 0, 0], [0, 0, 1])
else:
colors = ([0, 1, 1], [1, .5, 0])
scene.draw_isosurface(cub, -.03, opacity=.25, color=colors[0])
scene.draw_isosurface(cub, .03, opacity=.25, color=colors[1])
scene.draw_molecule(fmo.molecule)
if screen is None:
scr.__exit__()
[docs]
def screenshot_fmos(self, outdir='FMO_pictures'):
import tcviewer # noqa: F811
os.makedirs(outdir, exist_ok=True)
with tcviewer.Screen(headless=True) as scr:
for fmo in self.fmos:
with scr.add_molscene() as scene:
cub = fmo.cube_file()
scene.draw_molecule(fmo.molecule)
if fmo.occupied:
colors = ([1, 0, 0], [0, 0, 1])
else:
colors = ([0, 1, 1], [1, .5, 0])
scene.draw_isosurface(cub, -.03, opacity=.5, color=colors[0])
scene.draw_isosurface(cub, .03, opacity=.5, color=colors[1])
scene.screenshot(os.path.join(outdir, f'{str(fmo)}.png'))
[docs]
def nelectrons(self):
return sum(mo.occupation for mo in self.mos)
def __add__(self, other: 'Mixing'):
self.fmos.extend([ofmo for ofmo in other.fmos if ofmo not in self.fmos])
self.mos.extend([omo for omo in other.mos if omo not in self.mos])
self.connections.extend([oconn for oconn in other.connections if oconn not in self.connections])
for conn, typ in other.connection_type.items():
if conn in self.connection_type:
if typ == self.connection_type[conn]:
continue
else:
self.connection_type[conn] = 'Multiple'
else:
self.connection_type[conn] = typ
self.strength = None
self.fraction = None
self.two_mixings.extend(other.two_mixings)
return self
[docs]
def fits(self, other: 'Mixing'):
if len(self.fmos) == 0 and len(self.mos) == 0:
return True
if not any(fmo in other.fmos for fmo in self.fmos):
return False
if not any(mo in other.mos for mo in self.mos):
return False
return True
[docs]
def xiaobo_value(self):
if all(fmo.occupied for fmo in self.fmos):
maxval = 0
for fmo1 in self.fmos:
for fmo2 in self.fmos:
if fmo1 == fmo2:
continue
maxval = max(maxval, abs(fmo1.overlap(fmo2)))
return maxval
val = (self.fmos[0] @ self.fmos[1])**2 / abs(self.fmos[0].energy - self.fmos[1].energy)
for fmo in self.fmos:
gp = fmo.gross_population
gp = np.clip(gp, 0, 2)
excess = abs(fmo.occupation - gp)
val *= excess
return val
[docs]
def xiaobo_check(self, threshold=0.01):
if all(fmo.occupied for fmo in self.fmos):
for fmo1 in self.fmos:
for fmo2 in self.fmos:
if fmo1 == fmo2:
continue
if abs(fmo1.overlap(fmo2)) < threshold:
return False
return True
val = (self.fmos[0] @ self.fmos[1])**2 / abs(self.fmos[0].energy - self.fmos[1].energy)
for fmo in self.fmos:
gp = fmo.gross_population
gp = np.clip(gp, 0, 2)
excess = abs(fmo.occupation - gp)
val *= excess
return val > threshold
@property
def fragments(self):
return set(fmo.fragment for fmo in self.fmos)
@property
def lowest_contribution(self):
return min([abs(fmo.mulliken_contribution(mo)) for fmo in self.fmos for mo in self.mos])
@property
def irrep(self):
return self.mos[0].symmetry
@property
def spin(self):
return self.mos[0].spin
[docs]
def split(self):
G = nx.Graph()
G.add_edges_from(self.connections)
subGs = [G.subgraph(c) for c in nx.connected_components(G)]
mixes = []
for subG in subGs:
orbs_ = subG.nodes()
mos = [orb for orb in orbs_ if isinstance(orb, pyorbb.orbitals.objects.MO)]
fmos = [orb for orb in orbs_ if isinstance(orb, pyorbb.orbitals.objects.FMO)]
connections = subG.edges()
connections = [conn[::-1] if isinstance(conn[0], pyorbb.orbitals.objects.MO) else conn for conn in connections]
mixes.append(Mixing(
self.orbs,
mos=mos,
fmos=fmos,
connections=connections,
connection_type={conn: self.connection_type[conn] for conn in connections},
energy_type=self.energy_type))
return mixes
[docs]
def find_closed_interactions(self, orb):
G = nx.Graph()
G.add_edges_from(self.connections)
cycles = [cycle for cycle in nx.algorithms.cycles.simple_cycles(G, length_bound=4) if orb in cycle]
return cycles
ret = []
for two_mixing in self.two_mixings:
if orb in two_mixing.fmos or orb in two_mixing.mos:
ret.append([*two_mixing.fmos, *two_mixing.mos])
return ret
[docs]
def overlap_mat(fmos1, fmos2):
ret = []
for fmo1 in ensure_list(fmos1):
ret.append([])
for fmo2 in ensure_list(fmos2):
ret[-1].append(abs(fmo1 @ fmo2))
return np.array(ret).squeeze()
[docs]
def argNmax(arr, N):
"""
Get the Nth maximum element of an array.
"""
return np.argsort(-arr, axis=None)[N]
[docs]
def track_mixing(orbss, mixing):
out_dir = 'pyfrag_imgs'
os.makedirs(out_dir, exist_ok=True)
for i, orbs in enumerate(orbss):
fmos = [orbs.fmos[str(mix_fmo)] for mix_fmo in mixing.fmos]
mos = [orbs.mos[str(mix_mo)] for mix_mo in mixing.mos]
pyorbb.plotting.orbital_diagram.draw_interaction(fmos, mos, it.product(fmos, mos))
plt.savefig(os.path.join(out_dir, f'{i}.jpg'))
plt.close()
def _is_bonding(fmo1, fmo2, mo):
return round(fmo1.coefficient(mo) * fmo2.coefficient(mo) * (fmo1 @ fmo2), 4) >= 0