Source code for qcelemental.molparse.chgmult

import itertools
from typing import Any, Dict, List, Union

import numpy as np

from ..exceptions import ValidationError
from ..util import unique_everseen


def _apply_default(llist, default):
    return [default if (c is None) else c for c in llist]


def _high_spin_sum(mult_list):
    mm = 1
    for m in mult_list:
        mm += m - 1
    return mm


def _mult_ok(m):
    return isinstance(m, (int, np.integer)) and m >= 1


def _sufficient_electrons_for_mult(z, c, m):
    """Require sufficient electrons in total: total mult ({}) - 1 > raw electrons ({}) - total chg ({})"""
    return m - 1 <= z - c


def _parity_ok(z, c, m):
    """Check total electrons (neutral protons `z` and charge `c`) is (un)paired-compatible with multiplicity `m`"""
    return (m % 2) != ((z - c) % 2)


# def _alpha_beta_allocator(z, c, m):
#    nbeta = (z - c - m + 1) // 2
#    nalpha = nbeta + m - 1
#    return nalpha, nbeta


[docs]def validate_and_fill_chgmult( zeff: np.ndarray, fragment_separators: np.ndarray, molecular_charge: Union[float, None], fragment_charges: Union[List[float], None], molecular_multiplicity: Union[int, None], fragment_multiplicities: Union[List[int], None], zero_ghost_fragments: bool = False, verbose: int = 1, ) -> Dict[str, Any]: r"""Forms molecular and fragment charge and multiplicity specification by completing and reconciling information from argument, supplemented by physical constraints and sensible defaults. Parameters ---------- zeff (nat,) electron counts (float) for neutral atoms, generally Z nuclear charge. 0 indicates ghosts such that a full fragment of 0s will be constained to `0 1` charge & multiplicity. fragment_separators (nfr - 1, ) indices splitting `zeff` into nfr fragments. molecular_charge Total charge for molecular system. fragment_charges (nfr,) known fragment charges with `None` as placeholder for unknown. Expected pre-defaulted so even if nothing known, if `fragment_separators` breaks `zeff` into `nfr=2` fragments, input value should be ``fragment_charges=[None, None]``. molecular_multiplicity Total multiplicity for molecular system. fragment_multiplicities (nfr,) known fragment charges with `None` as placeholder for unknown. Expected pre-defaulted so even if nothing known, if `fragment_separators` breaks `zeff` into `nfr=2` fragments, input value should be ``fragment_multiplicities=[None, None]``. zero_ghost_fragments Fragments composed entirely of ghost atoms (Zeff=0) are required to have chgmult `0 1`. When `False`, violations of this will cause a ValidationError. When `True`, treat ghost fragments indicated by `zeff` to contain superior information over chgmult arguments that might still correspond to full-real molecule. Clears information from `molecular_charge` and `molecular_multiplicity` and sets ghost fragments to `0 1`, leaving other positions free to readjust. Unused (prefer to set up such manipulations outside function call) but works. verbose Amount of printing. Returns ------- molecular_charge : float Total charge for molecular system. fragment_charges : list of float (nfr,) Charge on each fragment. molecular_multiplicity : int Total multiplicity for molecular system. fragment_multiplicities : list of int (nfr,) Multiplicity for each fragment. Raises ------ qcelemental.ValidationError When no solution to input arguments subject to the constraints below can be found. Notes ----- Returns combination of total & fragment charge & multiplicity among values of S1-7 that fulfill rules R1-9. A few derived implications in I1-3. * Constraints * R1 require all chg & mult exist * R2 require total charge to be the sum of frag chg * R3 require mult is positive int * R4 require sufficient tot electrons for mult: mult - 1 <= neut_el - chg * R5 require total parity consistent among tot electrons and mult: (mult % 2) != ((neut_el - chg) % 2) * R6 require chg match input argument values * R7 require mult match input argument values * R8 require that tot = sum(frag) mult follow high spin addition unless tot & frag mult fully specified * R9 require that ghost fragments (zeff all 0) be neutral singlet * Allowed values * S1 suggest input argument values for tot chg, frag chg, tot mult or frag mult * S2 suggest sum frag chg for tot chg, allowing for indiv frag chg defaulting to 0 * S3 suggest distributing unallocated chg onto frag chg * S4 suggest 0 default for frag chg * S5 suggest range of high-spin sum frag mult for tot mult, allowing for indiv frag mult defaulting to 1 or 2 * S6 suggest range of unallocated mult = tot - high_spin_sum(frag - 1), allowing for all indiv but self defaulting to 1 or 2. * S7 suggest 1 or 2 default for frag mult * Implications * I1 won't form an ion just to be closed shell (would require choosing +1 vs. -1) * I2 unallocated chg or mult lands on the first unspecified fragment able to bear it (enforced by returning first match encountered; subsequent matches distribute charge to later frags) * I3 missing chg or mult from tot - frags will always be allocated as a block, not distributed Examples -------- >>> validate_and_fill_chgmult(*sys('He'), 0, [0], 1, [1]) 0, [0], 1, [1] >>> validate_and_fill_chgmult(*sys('He'), None, [None], None, [None]) 0, [0], 1, [1] >>> validate_and_fill_chgmult(*sys('He/He'), None, [None, None], None, [None, None]) 0, [0, 0], 1, [1, 1]) >>> validate_and_fill_chgmult(*sys('He/He'), 2, [None, None], None, [None, None]) 2, [2, 0], 1, [1, 1]) >>> validate_and_fill_chgmult(*sys('He/He'), None, [2, None], None, [None, None]) 2, [2, 0], 1, [1, 1]) >>> validate_and_fill_chgmult(*sys('He/He'), 0, [2, None], None, [None, None]) 0, [2, -2], 1, [1, 1]) >>> validate_and_fill_chgmult(*sys('Ne/He/He'), -2, [None, 2, None], None, [None, None, None]) -2, [-4, 2, 0], 1, [1, 1, 1] >>> validate_and_fill_chgmult(*sys('Ne/He/He'), 2, [None, -2, None], None, [None, None, None]) 2, [4, -2, 0], 1, [1, 1, 1] # 9 - residual +4 distributes to first fragment able to wholly accept it (He+4 is no-go) >>> validate_and_fill_chgmult(*sys('He/He/Ne'), 2, [None, -2, None], None, [None, None, None]) 2, [0, -2, 4], 1, [1, 1, 1] # 10 - residual +4 unsuited for only open fragment, He, so irreconcilable >>> validate_and_fill_chgmult(*sys('He/He/Ne'), 2, [None, -2, 0], None, [None, None, None]) ValidationError # 11 - non-positive multiplicity >>> validate_and_fill_chgmult(*sys('He/He/Ne'), 2, [2, -2, None], None, [None, None, None]) 2, [2, -2, 2], 1, [1, 1, 1]) >>> validate_and_fill_chgmult(*sys('He/He'), None, [-2, 2], None, [None, None]) 0, [-2, 2], 1, [1, 1] >>> validate_and_fill_chgmult(*sys('He/He'), None, [None, -2], None, [None, None]) -2, [0, -2], 1, [1, 1] >>> validate_and_fill_chgmult(*sys('Ne/Ne'), 0, [None, 4], None, [None, None]) 0, [-4, 4], 1, [1, 1] >>> validate_and_fill_chgmult(*sys('He/He/He'), 4, [2, None, None], None, [None, None, None]) 4, [2, 2, 0], 1, [1, 1, 1] >>> validate_and_fill_chgmult(*sys('He/He'), 0, [-2, 2], None, [None, None]) 0, [-2, 2], 1, [1, 1] >>> validate_and_fill_chgmult(*sys('He/He'), 0, [-2, -2], None, [None, None]) ValidationError >>> validate_and_fill_chgmult(*sys('He'), None, [None], 0, [None]) ValidationError >>> validate_and_fill_chgmult(*sys('He'), None, [None], None, [1]) 0, [0], 1, [1] # 20 - doublet non consistent with closed-shell, neutral default charge >>> validate_and_fill_chgmult(*sys('He'), None, [None], None, [2]) ValidationError >>> validate_and_fill_chgmult(*sys('He'), None, [None], None, [3]) 0, [0], 3, [3] # 22 - insufficient electrons for pentuplet >>> validate_and_fill_chgmult(*sys('He'), None, [None], None, [5]) ValidationError >>> validate_and_fill_chgmult(*sys('He'), None, [-1], None, [2]) -1, [-1], 2, [2] # 24 - doublet not consistent with even charge >>> validate_and_fill_chgmult(*sys('He'), None, [-2], None, [2]) ValidationError >>> validate_and_fill_chgmult(*sys('He/He'), None, [None, None], None, [1, 1]) 0, [0, 0], 1, [1, 1] >>> validate_and_fill_chgmult(*sys('He/He'), None, [None, None], None, [3, 1]) 0, [0, 0], 3, [3, 1] >>> validate_and_fill_chgmult(*sys('He/He'), None, [None, None], None, [1, 3]) 0, [0, 0], 3, [1, 3] >>> validate_and_fill_chgmult(*sys('He/He'), None, [None, None], None, [3, 3]) 0, [0, 0], 5, [3, 3] >>> validate_and_fill_chgmult(*sys('He/He'), None, [None, None], 3, [3, 3]) 0, [0, 0], 3, [3, 3] # 30 - bad parity btwn mult and total # electrons >>> validate_and_fill_chgmult(*sys('He/He'), None, [None, None], 2, [3, 3]) ValidationError >>> validate_and_fill_chgmult(*sys('H'), None, [None], None, [None]) 0, [0], 2, [2] >>> validate_and_fill_chgmult(*sys('H'), 1, [None], None, [None]) 1, [1], 1, [1] >>> validate_and_fill_chgmult(*sys('H'), None, [-1], None, [None]) -1, [-1], 1, [1] >>> validate_and_fill_chgmult(*sys('funnyH'), None, [None], None, [None]) 0, [0], 1, [1] # 35 - insufficient electrons >>> validate_and_fill_chgmult(*sys('funnierH'), None, [None], None, [None]) ValidationError >>> validate_and_fill_chgmult(*sys('H/H'), None, [None, None], None, [None, None]) 0, [0, 0], 3, [2, 2] >>> validate_and_fill_chgmult(*sys('H/He'), None, [None, None], None, [None, None]) 0, [0, 0], 2, [2, 1] >>> validate_and_fill_chgmult(*sys('H/He'), None, [1, 1], None, [None, None]) 2, [1, 1], 2, [1, 2] >>> validate_and_fill_chgmult(*sys('H/He'), -2, [-1, None], None, [None, None]) -2, [-1, -1], 2, [1, 2] >>> validate_and_fill_chgmult(*sys('H/He/Na/Ne'), None, [1, None, 1, None], None, [None, None, None, None]) 2, [1, 0, 1, 0], 1, [1, 1, 1, 1] >>> validate_and_fill_chgmult(*sys('H/He/Na/Ne'), None, [-1, None, 1, None], None, [None, None, None, None]) 0, [-1, 0, 1, 0], 1, [1, 1, 1, 1] >>> validate_and_fill_chgmult(*sys('H/He/Na/Ne'), 2, [None, None, 1, None], None, [None, None, None, None]) 2, [1, 0, 1, 0], 1, [1, 1, 1, 1] >>> validate_and_fill_chgmult(*sys('H/He/Na/Ne'), 3, [None, None, 1, None], None, [None, None, None, None]) 3, [0, 2, 1, 0], 2, [2, 1, 1, 1] >>> validate_and_fill_chgmult(*sys('H/He'), None, [1, None], None, [2, None]) ValidationError >>> validate_and_fill_chgmult(*sys('H/He'), None, [None, 0], None, [None, 2]) ValidationError >>> validate_and_fill_chgmult(*sys('H/He'), None, [None, -1], None, [None, 3]) ValidationError >>> validate_and_fill_chgmult(*sys('H/He/Na/Ne'), None, [None, 1, 0, 1], None, [None, None, None, None]) 2, [0, 1, 0, 1], 5, [2, 2, 2, 2] >>> validate_and_fill_chgmult(*sys('H/He/Na/Ne'), None, [None, 1, 0, None], None, [None, None, None, None]) 1, [0, 1, 0, 0], 4, [2, 2, 2, 1] >>> validate_and_fill_chgmult(*sys('H/He/Na/Ne'), None, [None, 1, 0, None], None, [None, None, 4, None]) 1, [0, 1, 0, 0], 6, [2, 2, 4, 1] >>> validate_and_fill_chgmult(*sys('He/He/He'), 0, [None, None, 1], None, [1, None, 2]) 0, [0, -1, 1], 3, [1, 2, 2] >>> validate_and_fill_chgmult(*sys('N/N/N'), None, [1, 1, 1], 3, [None, 3, None]) 3, [1, 1, 1], 3, [1, 3, 1] >>> validate_and_fill_chgmult(*sys('N/N/N'), None, [1, 1, 1], 3, [None, None, None]) 3, [1, 1, 1], 3, [3, 1, 1] >>> validate_and_fill_chgmult(*sys('N/N/N'), None, [None, None, None], 3, [None, None, 2]) ValidationError >>> validate_and_fill_chgmult(*sys('N/N/N'), 1, [None, -1, None], 3, [None, None, 2]) 1, [2, -1, 0], 3, [2, 1, 2] # 55 - both (1, (1, 0.0, 0.0), 4, (1, 3, 2)) and (1, (0.0, 0.0, 1), 4, (2, 3, 1)) plausible >>> validate_and_fill_chgmult(*sys('N/Ne/N'), 1, [None, None, None], 4, [None, 3, None]) 1, [1, 0, 0], 4, [1, 3, 2] >>> validate_and_fill_chgmult(*sys('N/Ne/N'), None, [None, None, 1], 4, [None, 3, None]) 1, [0, 0, 1], 4, [2, 3, 1] >>> validate_and_fill_chgmult(*sys('He/He'), None, [-1, 1], None, [None, None]) 0, [-1, 1], 3, [2, 2] >>> validate_and_fill_chgmult(*sys('Gh'), 1, [None], None, [None]) ValidationError >>> validate_and_fill_chgmult(*sys('Gh'), -1, [None], None, [None]) ValidationError >>> validate_and_fill_chgmult(*sys('Gh'), None, [None], 3, [None]) ValidationError >>> validate_and_fill_chgmult(*sys('He/Gh'), None, [2, None], None, [None, None]) 2, [2, 0], 1, [1, 1] >>> validate_and_fill_chgmult(*sys('Gh/He'), None, [2, None], None, [None, None]) ValidationError >>> validate_and_fill_chgmult(*sys('Gh/He/Ne'), 2, [None, -2, None], None, [None, None, None]) 2, [0, -2, 4], 1, [1, 1, 1] >>> validate_and_fill_chgmult(*sys('Gh/He/Gh'), 1, [None, None, None], None, [None, None, None]) 1, [0, 1, 0], 2, [1, 2, 1] >>> sys = { 'He': (np.array([2]), np.array([])), 'He/He': (np.array([2, 2]), np.array([1])), 'Ne/He/He': (np.array([10, 2, 2]), np.array([1, 2])), 'He/He/Ne': (np.array([2, 2, 10]), np.array([1, 2])), 'Ne/Ne': (np.array([10, 10]), np.array([1])), 'He/He/He': (np.array([2, 2, 2]), np.array([1, 2])), 'H': (np.array([1]), np.array([])), 'funnyH': (np.array([0]), np.array([])), # has no electrons 'funnierH': (np.array([-1]), np.array([])), # has positron 'H/H': (np.array([1, 1]), np.array([1])), 'H/He': (np.array([1, 2]), np.array([1])), 'H/He/Na/Ne': (np.array([1, 2, 11, 10]), np.array([1, 2, 3])), 'N/N/N': (np.array([7, 7, 7]), np.array([1, 2])), 'N/Ne/N': (np.array([7, 10, 7]), np.array([1, 2])), 'He/Gh': (np.array([2, 0]), np.array([1])), 'Gh/He': (np.array([0, 2]), np.array([1])), 'Gh': (np.array([0, 0]), np.array([])), 'Gh/He/Ne': (np.array([0, 0, 2, 10]), np.array([2, 3])), 'Gh/He/Gh': (np.array([0, 2, 0]), np.array([1, 2]))} """ log_full = verbose >= 2 log_brief = verbose >= 2 # TODO: Move back to 1 text = [] def int_if_possible(val): if isinstance(val, float) and val.is_integer(): return int(val) else: return val molecular_multiplicity = int_if_possible(molecular_multiplicity) fragment_multiplicities = [int_if_possible(m) for m in fragment_multiplicities] if (molecular_multiplicity and molecular_multiplicity < 1.0) or any(m < 1.0 for m in fragment_multiplicities if m): raise ValidationError( f"validate_and_fill_chgmult(): Multiplicity must be positive. m: {molecular_multiplicity}, fm: {fragment_multiplicities}" ) felez = np.split(zeff, fragment_separators) nfr = len(felez) if log_full: text.append("felez: {}".format(felez)) cgmp_exact_c = [] # exact_* are candidates for the final value cgmp_exact_fc: List[List[float]] = [[] for f in range(nfr)] cgmp_exact_m = [] cgmp_exact_fm: List[List[int]] = [[] for f in range(nfr)] cgmp_range = [] # tests that the final value must pass to be valid cgmp_rules = [] # key to what rules in cgmp_range are T/F real_fragments = np.array([not all(f == 0 for f in felez[ifr]) for ifr in range(nfr)]) all_fc_known = all(f is not None for f in fragment_charges) all_fm_known = all(f is not None for f in fragment_multiplicities) if log_full: text.append("all_fc_known: {}".format(all_fc_known)) text.append("all_fm_known: {}".format(all_fm_known)) if zero_ghost_fragments and not all(real_fragments): if log_brief: print("possibly adjusting charges") molecular_charge = None fragment_charges = [(fr if real_fragments[ifr] else 0.0) for ifr, fr in enumerate(fragment_charges)] molecular_multiplicity = None fragment_multiplicities = [(fr if real_fragments[ifr] else 1) for ifr, fr in enumerate(fragment_multiplicities)] # <<< assert broad physical requirements # * (R1) require all chg & mult exist cgmp_range.append( lambda c, fc, m, fm: c is not None and all(f is not None for f in fc) and m is not None and all(f is not None for f in fm) ) cgmp_rules.append("1") # * (R2) require total charge to be the sum of fragment charges cgmp_range.append(lambda c, fc, m, fm: c == sum(fc)) cgmp_rules.append("2") # * (R3) require mult is positive int cgmp_range.append(lambda c, fc, m, fm: _mult_ok(m) and all(_mult_ok(f) for f in fm)) cgmp_rules.append("3") # <<< assert electron count requirements zel = np.sum(zeff) # note: number electrons in neutral species, not number total electrons fzel = [np.sum(f) for f in felez] if log_full: text.append("zel: {}".format(zel)) text.append("fzel: {}".format(fzel)) # * (R4) require sufficient electrons for mult: mult - 1 <= neutral_electrons - chg cgmp_range.append(lambda c, fc, m, fm: _sufficient_electrons_for_mult(zel, c, m)) cgmp_rules.append("4") for ifr in range(nfr): cgmp_range.append( lambda c, fc, m, fm, ifr=ifr: _sufficient_electrons_for_mult(fzel[ifr], fc[ifr], fm[ifr]) # type: ignore ) cgmp_rules.append("4-" + str(ifr)) # * (R5) require total parity consistent among neutral_electrons, chg, and mult cgmp_range.append(lambda c, fc, m, fm: _parity_ok(zel, c, m)) cgmp_rules.append("5") for ifr in range(nfr): cgmp_range.append(lambda c, fc, m, fm, ifr=ifr: _parity_ok(fzel[ifr], fc[ifr], fm[ifr])) # type: ignore cgmp_rules.append("5-" + str(ifr)) # <<< (R6, R7, S1) assert & suggest input values if molecular_charge is not None: cgmp_exact_c.append(molecular_charge) cgmp_range.append(lambda c, fc, m, fm: c == molecular_charge) cgmp_rules.append("6") for ifr, chg in enumerate(fragment_charges): if chg is not None: cgmp_exact_fc[ifr].append(chg) cgmp_range.append(lambda c, fc, m, fm, ifr=ifr, chg=chg: fc[ifr] == chg) # type: ignore cgmp_rules.append("6-" + str(ifr)) if molecular_multiplicity is not None: cgmp_exact_m.append(molecular_multiplicity) cgmp_range.append(lambda c, fc, m, fm: m == molecular_multiplicity) cgmp_rules.append("7") for ifr, mult in enumerate(fragment_multiplicities): if mult is not None: cgmp_exact_fm[ifr].append(mult) cgmp_range.append(lambda c, fc, m, fm, ifr=ifr, mult=mult: fm[ifr] == mult) # type: ignore cgmp_rules.append("7-" + str(ifr)) # <<< assert high-spin-rule and suggest "missing quantity" and default values # * (S2) suggest net frag charge for total charge, allowing for indiv frag defaulting to 0 cgmp_exact_c.append(sum(filter(None, fragment_charges))) missing_frag_chg = 0.0 if molecular_charge is None else molecular_charge missing_frag_chg -= sum(filter(None, fragment_charges)) # * (S3) suggest distributing unallocated charge onto fragment # * (S4) suggest 0 default charge for fragment for ifr in range(nfr): if fragment_charges[ifr] is None: # unneeded, but shortens the exact lists cgmp_exact_fc[ifr].append(missing_frag_chg) cgmp_exact_fc[ifr].append(0.0) # * (R8) require that frag mult follow high spin addition unless fully specified if molecular_multiplicity is None or any(f is None for f in fragment_multiplicities): cgmp_range.append(lambda c, fc, m, fm: m == _high_spin_sum(fm)) cgmp_rules.append("8") # * (S5) suggest range of net frag mult for total mult, allowing for indiv frag defaulting to 1 or 2. # many in range may be unphysical, but those will be caught by physical rules. if molecular_multiplicity is None: # unneeded, but shortens the exact lists frag_mult_hi = _high_spin_sum(_apply_default(fragment_multiplicities, 2)) frag_mult_lo = _high_spin_sum(_apply_default(fragment_multiplicities, 1)) for m in range(frag_mult_lo, frag_mult_hi + 1): cgmp_exact_m.append(m) # * (S6) suggest range of missing mult = tot - high_spin_sum(frag - 1), # allowing for all indiv but self defaulting to 1 or 2. Many in range # may be unphysical, but those will be caught by physical rules. # * (S7) suggest 1 or 2 default multiplicity for fragment if molecular_multiplicity is not None and any(f is None for f in fragment_multiplicities): frag_mult_less_one_none = fragment_multiplicities[:] frag_mult_less_one_none.remove(None) # "missing" slot to solve for frag_mult_hi = _high_spin_sum(_apply_default(frag_mult_less_one_none, 2)) frag_mult_lo = _high_spin_sum(_apply_default(frag_mult_less_one_none, 1)) missing_mult_hi = molecular_multiplicity - frag_mult_lo + 1 missing_mult_lo = molecular_multiplicity - frag_mult_hi + 1 else: missing_mult_hi = 0 missing_mult_lo = 0 for ifr in range(nfr): if fragment_multiplicities[ifr] is None: # unneeded, but shortens the exact lists for m in reversed(range(max(missing_mult_lo, 1), missing_mult_hi + 1)): cgmp_exact_fm[ifr].append(m) cgmp_exact_fm[ifr].append(1) cgmp_exact_fm[ifr].append(2) # * (R9) require that ghost fragments be neutral singlet for ifr in range(nfr): if all(f == 0 for f in felez[ifr]): cgmp_range.append(lambda c, fc, m, fm, ifr=ifr: fc[ifr] == 0 and fm[ifr] == 1) # type: ignore cgmp_rules.append("9-" + str(ifr)) # <<< reconcile and report def reconcile(exact_c, exact_fc, exact_m, exact_fm): """Returns a member from all combinations of `exact` that passes all tests in cgmp_range, else raises error.""" # remove duplicates uniq_c = unique_everseen(exact_c) uniq_fc = [unique_everseen(f) for f in exact_fc] uniq_m = unique_everseen(exact_m) uniq_fm = [unique_everseen(f) for f in exact_fm] text.append("c: {}".format(list(exact_c))) for f in exact_fc: text.append("fc: {}".format(list(f))) text.append("m: {}".format(list(exact_m))) for f in exact_fm: text.append("fm: {}".format(list(f))) header = True for candidate in itertools.product(*[uniq_c, itertools.product(*uniq_fc), uniq_m, itertools.product(*uniq_fm)]): cc, cfc, cm, cfm = candidate if header: if log_full: text.append( """Assess candidate {}: {}""".format( candidate, " ".join(("{:3}".format(r) for r in cgmp_rules)) ) ) header = False assessment = [fn(cc, cfc, cm, cfm) for fn in cgmp_range] sass = ["{:3}".format("T" if b else "") for b in assessment] if log_full: text.append("""Assess candidate {:}: {} --> {}""".format(candidate, " ".join(sass), all(assessment))) if all(assessment): return candidate err = """Inconsistent or unspecified chg/mult: sys chg: {}, frag chg: {}, sys mult: {}, frag mult: {}""".format( molecular_charge, fragment_charges, molecular_multiplicity, fragment_multiplicities ) if verbose > -1: print("\n\n" + "\n".join(text)) raise ValidationError(err) def stringify(start, final): fcgmp = "{:^4}" return fcgmp.format(final) if final == start else fcgmp.format("(" + str(int(final)) + ")") # TODO could winnow down the exact_* lists a bit by ruling out # independent values. do this if many-frag molecular systems take too # long in the itertools.product c_final, fc_final, m_final, fm_final = reconcile(cgmp_exact_c, cgmp_exact_fc, cgmp_exact_m, cgmp_exact_fm) c_text = stringify(molecular_charge, c_final) fc_text = ", ".join((stringify(fs, ff) for fs, ff in zip(fragment_charges, fc_final))) m_text = stringify(molecular_multiplicity, m_final) fm_text = ", ".join((stringify(fs, ff) for fs, ff in zip(fragment_multiplicities, fm_final))) brief = [] if log_brief: brief.append(" {:26} {}".format(" charge = " + c_text, "fragments = " + fc_text)) brief.append(" {:26} {}".format("multiplicity = " + m_text, "fragments = " + fm_text)) been_defaulted = [] if c_text.count("(") + fc_text.count("(") > 1: been_defaulted.append("charge") if "(" in m_text or "(" in fm_text: been_defaulted.append("multiplicity") if been_defaulted and log_brief: brief.append( " Note: Default values have been applied for {}. Specify intentions in molecule input block".format( " and ".join(been_defaulted) ) ) if (m_final != _high_spin_sum(fm_final)) and log_brief: brief.append( " Warning: Total multiplicity is not high-spin sum of fragments; may be clobbered by psi4.core.Molecule.update_geometry()." ) if log_full: print("\n".join(text)) if log_brief: # TODO add back when printing worked out # print('\n'.join(brief)) pass return { "molecular_charge": float(c_final), "fragment_charges": [float(f) for f in fc_final], "molecular_multiplicity": m_final, "fragment_multiplicities": list(fm_final), }