Source code for rotsim2d.symbolic.functions

# pylint: disable=line-too-long
"""This module contains functions to derive and manipulate SymPy expressions
related to polarization dependence and angular momentum dependence of four-fold
dipole interaction operator. The derived expressions are in
:mod:`rotsim2d.symbolic.results`."""
# * Imports
import attrs
import inspect
import itertools as it
import re
from collections.abc import Mapping
from functools import reduce
from operator import mul

import numpy as np
import rotsim2d.dressedleaf as dl
import rotsim2d.pathways as pw
import sympy.physics.quantum.cg as cg
from molspecutils.molecule import RotState
from rotsim2d.couple import T00, G
from rotsim2d.symbolic.common import *
from rotsim2d.symbolic.results import (T00_exprs, gfactors, gfactors_highj,
                                       gfactors_highj_numeric)
from scipy.optimize import minimize
from sympy import *
from typing import (Any, Callable, Dict, Iterable, List, NewType, Optional,
                    Sequence, Tuple, Union)

#: Dummy type for any SymPy expressions, since SymPy is not annotated
# Expr = NewType('Expr', Any)

# * Utilities
def inf_syms():
    i = 0
    while True:
        yield Symbol("x"+str(i))
        i += 1

# * Analytical Wigner-6j functions
[docs]def gfactor_expr(ji, jj, jk, jl, k): """Call :func:`rotsim2d.couple.G` with symbolic `sqrt`.""" return G(ji, jj, jk, jl, k, sqrt=sqrt)
# * Polarization # ** Linear polarization
[docs]def T1q(q, ph): """Linear polarization spherical tensor component `q`.""" if q == 1: return -1/sqrt(2)*exp(1j*ph) elif q == -1: return 1/sqrt(2)*exp(-1j*ph) else: return 0
[docs]def T00_phis(k, phis): """k-th component of isotropic four-fold polarization tensor.""" pre = 2*k+1 ret = 0 for q in range(-k, k+1): for qp in (-1, 0, 1): for qpp in (-1, 0 ,1): ret += cg.wigner_3j(k, k, 0, q, -q, 0)*\ cg.wigner_3j(1,1,k,qp,q-qp,-q)*\ cg.wigner_3j(1,1,k,qpp,-q-qpp,q)*\ T1q(qp,phis[0])*T1q(q-qp,phis[1])*T1q(qpp,phis[2])*T1q(-q-qpp,phis[3]) return pre*ret
#: Cosines terms present in `T00_exprs` (dummy angles) T00_trigs = [cos(phi + phj - phk - phl), cos(phi - phj + phk - phl), cos(phi - phj - phk + phl)] #: Cosines terms present in `T00_exprs` (experimental angles) T00_theta_trigs = [cos(theta_i + theta_j - theta_k - theta_l), cos(theta_i - theta_j + theta_k - theta_l), cos(theta_i - theta_j - theta_k + theta_l)] # * R-factors # Combined polarization-angular momentum factors for four-fold dipole # interaction operator. # ** Simplify R-factor
[docs]class RFactor: def __init__(self, coeffs: Union[Mapping, Sequence], angles: str='dummy'): """Expression describing polarization and angular momentum dependence of a pathway. Parameters ---------- coeffs Mapping or a sequence of `c0`, `c12`, `c13` and `c14` coefficients. angles, optional R-factor as a function of 'dummy' (phis) or 'experimental` (thetas) angles. """ if isinstance(coeffs, Sequence): if len(coeffs) == 3: coeffs = (1,) + tuple(coeffs) if len(coeffs) == 4: self.dict = dict(zip(('c0', 'c12', 'c13', 'c14'), coeffs)) else: raise ValueError("coeffs has to be a Mapping or a Sequence with len=3 or len=4") else: self.dict = coeffs """Dictionary of R-factor coefficients: 'c0', 'c12', 'c13', 'c14'.""" if angles == 'dummy': self.trigs = T00_trigs """Sympy expressions for cosines of 4 angles present in expression for the R-factor.""" self.angles = (phi, phj, phk, phl) """SymPy symbols for 4 angles.""" self.angles_type = angles """Kind of angles, either 'dummy' (phis) or 'experimental` (thetas).""" elif angles == 'experimental': self.trigs = T00_theta_trigs self.angles = thetas self.angles_type = angles else: raise ValueError("angles has to be either 'dummy' or 'experimental'") self._numeric = None self._numeric_rel = None self.expr = None "SymPy expressions for R-factor." self.dict_to_expr() def __repr__(self): return f"RFactor(coeffs={self.tuple!r}, angles={self.angles!r})"
[docs] @classmethod def from_gterms(cls, gterms: Sequence[Basic], pterms: Sequence[Basic]=T00_exprs, angles: str='dummy'): """Make :class:`RFactor` from G-factor and polarization terms. Parameters ---------- gterms Spherical tensor components of G-factor. pterms Spherical tensor components of four-fold polarization tensor. angles, optional R-factor as a function of 'dummy' (phis) or 'experimental` (thetas) angles, has to match `pterms`. """ rexpr = sum(gterms[i]*FU['TR8'](pterms[i]) for i in range(3)) return cls.from_expr(rexpr, angles)
[docs] @classmethod def from_expr(cls, rexpr: Basic, angles: str='dummy'): """Make :class:`RFactor` from R-factor expression. Parameters ---------- rexpr SymPy expression for R-factor. angles, optional R-factor as a function of 'dummy' (phis) or 'experimental` (thetas) angles, has to match `rexpr`. """ if angles == 'dummy': trigs = T00_trigs elif angles == 'experimental': trigs = T00_theta_trigs else: raise ValueError("`trigs` has to be either 'experimental' or 'dummy'") _, d = cls._simplify(rexpr, trigs) # 2nd run gets rid of last two dups _, d = cls._simplify(_, trigs) return cls(d, angles=angles)
[docs] @classmethod def from_pathway(cls, pw: dl.Pathway, highj: bool=False, normalize: bool=False, only_angles: bool=False, rotated: Optional[str]=None): """Return R-factor corresponding to :class:`rotsim2d.dressedleaf.Pathway`. Parameters ---------- pw Pathway or DressedPathway. highj Use high-J limit versions of G-factors. normalize Make the sign of the common factor positive and flip the signs of the coefficients if the `c12` coefficient is negative. only_angles Set 'c00' to 1. """ if highj: gterms = gfactors_highj[pw.geo_label] else: gterms = gfactors[pw.geo_label] if rotated is not None: if rotated == 'forward': deltaj = pw.js[0]-pw.js[-1] # Jj-Ji, Jj = Ji + (Jj-Ji) gterms = tuple([(S(gterm).subs(J_i, J_j)*sqrt((2*J_j+1)/(2*J_i+1)))\ .subs(J_j, J_i+deltaj) for gterm in gterms]) elif rotated == 'backward': deltaj = pw.js[0]-pw.js[1] # Jl-Ji, Jl = Ji + (Jl-Ji) gterms = tuple([(S(gterm).subs(J_i, J_l)*sqrt((2*J_l+1)/(2*J_i+1)))\ .subs(J_l, J_i+deltaj) for gterm in gterms]) else: raise ValueError("`rotated` needs to be either 'forward' or 'backward'") subs_dict = dict(zip([phi, phj, phk, phl], pw._phi_angles(thetas))) pterms = [e.subs(subs_dict) for e in T00_exprs[:]] rfac = cls.from_gterms(gterms, pterms, 'experimental') if only_angles: rfac.dict['c0'] = 1 rfac.dict_to_expr() if normalize: rfac.dict['c0'] = abs(rfac.dict['c0']) if rfac.dict['c12'] < 0: rfac.dict['c12'] = -rfac.dict['c12'] rfac.dict['c13'] = -rfac.dict['c13'] rfac.dict['c14'] = -rfac.dict['c14'] rfac.dict_to_expr() return rfac
[docs] def dict_to_expr(self): """Regenerate expression from dict.""" self.expr = self.dict['c0']*(self.dict['c12']*self.trigs[0] + self.dict['c13']*self.trigs[1] + self.dict['c14']*self.trigs[2])
@property def tuple(self): """Tuple of coefficients (c0, c12, c13, c14).""" return (self.dict['c0'], self.dict['c12'], self.dict['c13'], self.dict['c14']) @staticmethod def _simplify(expr: Basic, subterms: Sequence[Basic]) -> Tuple[Basic, Dict]: """Simplify RFactor SymPy expression. Parameters ---------- expr Expression to simplify, subterms Collect coefficients for expressions in `subterms` Returns ------- Tuple factorized expression for R-factor and dict of coefficients. """ subs1 = dict(zip(subterms, [x1, x2, x3])) subs2 = dict(zip([x1, x2, x3], subterms)) expr = expr.subs(subs1) expr_dict = {k: powdenest(factor(powdenest(v, force=True), deep=True), force=True) for k, v in collect(expand(expr), [x1, x2, x3], evaluate=False).items()} expr = collect(factor(sum(k*v for k, v in expr_dict.items())), [x1, x2, x3]) ret_expr = expr.subs(subs2) common, uncommon = S(1), [] for term in expr.args: if {x1, x2, x3} & term.free_symbols: uncommon.append(term) else: common = common*term # special case when common == 1 # could also be dealt with by checking if top-level operator is Mul or Add if len(uncommon) > 1 and common == S(1): uncommon = [Add(*uncommon)] # `uncommon` is now a sum of >=3 cosines with individual non-factorable # coeffs # collect terms corresponding to each of three cosines uncommon_dict = collect(uncommon[0], [x1, x2, x3], evaluate=False) ret_dict = {'c0': common, 'c12': uncommon_dict[x1], 'c13': uncommon_dict[x2], 'c14': uncommon_dict[x3]} return ret_expr, ret_dict
[docs] def expr_xxxx(self): """Return R-factor expression for XXXX polarization.""" expr = self.expr.subs(dict(zip(self.angles, [0]*4))) return simplify(factor(powdenest(expr, force=True), deep=True))
[docs] def expr_relative(self): """Return R-factor expressions relative to XXXX polarization.""" return self._simplify(self.expr/self.expr_xxxx(), self.trigs)[0]
[docs] def numeric(self, thetai, thetaj, thetak, thetal, Ji=None): """Try to efficiently evaluate R-factor with NumPy. It is always safe to pass `Ji` argument. It will be ignored if it's not needed. This is because even fully general R-factors sometimes do not depend on `J_i`. """ if self._numeric is None: args = list(self.angles) if J_i in self.expr.free_symbols: args = args + [J_i] self._numeric = lambdify(args, self.expr) self._numeric_nargs = len(inspect.signature( self._numeric).parameters) args = (thetai, thetaj, thetak, thetal, Ji) return self._numeric(*args[:self._numeric_nargs])
[docs] def numeric_rel(self, thetai, thetaj, thetak, thetal, Ji=None): """Try to efficiently evaluate relative R-factor with NumPy. It is always safe to pass `Ji` argument. It will be ignored if it's not needed. This is because even fully general R-factors sometimes do not depend on `J_i`. """ if self._numeric_rel is None: args = list(self.angles) expr = self.expr_relative() if J_i in expr.free_symbols: args = args + [J_i] self._numeric_rel = lambdify(args, expr) self._numeric_rel_nargs = len(inspect.signature( self._numeric_rel).parameters) args = (thetai, thetaj, thetak, thetal, Ji) return self._numeric_rel(*args[:self._numeric_rel_nargs])
[docs] def det_angle(self, angles: Optional[Sequence]=None) -> Basic: """Return expr. for `tan(theta_l)` which zeroes this R-factor. `angles` contains linear polarization angles of up to three pulses in sequence. If `angles` is None, then the first angle is set to 0. """ return solve_det_angle(self, angles)
def __eq__(self, o): if isinstance(o, RFactor): return self == o.tuple elif isinstance(o, Mapping): return all(factor(self.dict[k]-o[k], deep=True) == S(0) for k in self.dict) else: return all(factor(st-ot, deep=True) == S(0) for st, ot in zip(self.tuple, o)) def __hash__(self): # could also convert expressions to str but I'm not sure if the strings # will be unambiguous return hash(tuple([float(x.subs(J_i, 100) if isinstance(x, Basic) else x) for x in self.tuple]))
# * Classify pathways with regards to R-factor # ** Functions
[docs]def dl_gfactors(dl: dl.Pathway, evalf: bool=True, abstract: bool=False): """Return G-factors for a :class:`rotsim2d.dressedleaf.Pathway`.""" js = list(dl.js) if abstract: evalf = False js = [J_i+(j-js[0]) for j in js] gfactors = (gfactor_expr(*(js + [0])), gfactor_expr(*(js + [1])), gfactor_expr(*(js + [2]))) if evalf: gfactors = tuple(gfactor.evalf() for gfactor in gfactors) return gfactors
[docs]def dl_T00s(dl: dl.Pathway, angles, evalf: bool=True): """Return polarization components a :class:`rotsim2d.dressedleaf.Pathway`.""" phis = [phi, phj, phk, phl] phi_angles = dl._phi_angles(angles) T00_exprs = [ cos(phi - phj)*cos(phk - phl)/3, sqrt(3)*sin(phi - phj)*sin(phk - phl)/6, sqrt(5)*(cos(phi - phj - phk + phl) + cos(phi - phj + phk - phl) + 6*cos(phi + phj - phk - phl))/60] T00_exprs = tuple(T00_expr.subs(dict(zip(phis, phi_angles))) for T00_expr in T00_exprs) if evalf: T00_exprs = tuple(T00_expr.evalf() for T00_expr in T00_exprs) return T00_exprs
[docs]def classify_dls(dressed_pws: Sequence[dl.Pathway], rfactors: Sequence[RFactor], states: bool=False) -> Dict[RFactor, List[dl.Pathway]]: """Return a mapping between R-factors and pathways in `dressed_pws`. If `states` is True, convert Pathways to lists of states. `rfactors` are R-factors as functions of experimental angles (thetas) not dummy angles (phis). """ classified = {} for dressed_leaf, cur_rfac in zip(dressed_pws, rfactors): found = False rfacs = list(classified.keys()) for rfac in rfacs: if rfac == cur_rfac: found = rfac break if found: classified[found].append(dressed_leaf) else: classified[cur_rfac] = [dressed_leaf] if states: return {k: [pw.leaf.to_statelist(diatom=True, normalize=True) for pw in v] for k, v in classified.items()} return classified
[docs]def solve_det_angle(rexpr: RFactor, angles: Optional[Sequence]=None) -> Basic: """Return expr. for `tan(theta_l)` which zeroes `rexpr`. `angles` contains linear polarization angles of up to three pulses in sequence. If `angles` is None, then the first angle is set to 0. """ if angles is None: angles = [0] expr = expand_trig(rexpr.expr).subs(dict(zip(rexpr.angles, angles))).subs( {sin(rexpr.angles[3]): x1, cos(rexpr.angles[3]): x2}).subs({x1: x1x2*x2, x2: x1/x1x2}) sol = solve(expr, x1x2) if sol: return atan(factor(expand_trig(sol[0]), deep=True)) sol = solve(expr, x1) if sol: return asin(factor(expand_trig(sol[0]), deep=True)) sol = solve(expr, x2) if sol: return acos(factor(expand_trig(sol[0]), deep=True))
[docs]def suppression_angles(exprs: Sequence[RFactor], angles: Optional[Sequence[Basic]]=None) -> Dict[Basic, Basic]: """Return roots of expressions in `exprs` with respect to detection angle. `angles` contains linear polarization angles of up to three pulses in sequence. If `angles` is None, then the first angle is set to 0. """ return {k: solve_det_angle(k, angles=angles) for k in exprs}
def dummify_angle_expr(angle_expr: Basic, angles_vars: Optional[Sequence[Basic]]=thetas) -> Basic: """Substitute dummy variables for tangents of angles. SymPy struggles with solving equations involving several trigonometric functions. Simplify the task by solving for tangents of angles. """ angle_expr = factor( angle_expr.\ subs({sin(angles_vars[1]): x1, cos(angles_vars[1]): x2}).\ subs({x1: x1x2*x2, x2: x1/x1x2})) angle_expr = factor( angle_expr.\ subs({sin(angles_vars[2]): x3, cos(angles_vars[2]): x4}).\ subs({x3: x3x4*x4, x4: x3/x3x4})) return angle_expr
[docs]def common_angles(exprs: Sequence[Basic], angles_vars: Optional[Sequence[Basic]]=thetas) -> Dict[Basic, Basic]: r"""Find angles simultaneously zeroing all in `exprs`. The expressions in `exprs` should be formulas for the tan of detection angle, :math:`\theta_l`, zeroing some pathways, as obtained by calling :func:`solve_det_angle`. Uses :func:`sympy.solve`. """ back_subs = {x0: tan(angles_vars[3]), x1x2: tan(angles_vars[1]), x3x4: tan(angles_vars[2])} system = [dummify_angle_expr(v, angles_vars)-x0 for v in exprs] sols = solve(system, [x1x2, x3x4, x0], dict=True) sols = [{k.subs(back_subs): v.subs(back_subs) for k, v in s.items()} for s in sols] return sols
[docs]def classify_suppression(classified: Dict[RFactor, Sequence[dl.Pathway]], angles: Dict[RFactor, Basic]) -> Dict[Basic, List[dl.Pathway]]: """Return a map between suppression angles and pathways. `classified` is the map between expressions and pathways returned by :func:`classify_dls`. """ angles_to_pws = {} for k, v in angles.items(): angles_to_pws.setdefault(v, []).extend(classified[k]) return angles_to_pws
[docs]def pathway_angles(pws: Sequence[dl.Pathway], angles: Sequence, angles_vars: Optional[Sequence[Basic]]=thetas) -> Dict[Basic, List[dl.Pathway]]: """Return a map between detection angles and elements of `pws` they suppress. Parameters ---------- pws Sequence of class:`dressedleaf.Pathway`. angles Linear polarization angles of three interacting pulses in sequence. """ pws_rfactors = [RFactor.from_pathway(pw, True, True) for pw in pws] # pws_rfactors = [dl_to_rfactor(pw, rfactors_highj) for pw in pws] classified = classify_dls(pws, pws_rfactors) zeroing_angles = suppression_angles(classified.keys(), angles, angles_vars) ret = classify_suppression(classified, zeroing_angles) return ret
[docs]def detection_angles(angles: Sequence, meths: Optional[Sequence[Callable]]=None, angles_vars: Optional[Sequence[Basic]]=thetas): """Zeroing det. angles for a minimal complete set of pathways. Generate a minimal complete set of non-R-factor-equivalent pathways including Q-branch transitions. The resulting dict can be used with :func:`rotsim2d.dressedleaf.equiv_peaks` or :func:`rotsim2d.dressedleaf.split_by_equiv_peaks` to identify other pathways that are also zeroed by any of the detection angles. Parameters ---------- angles Light beam polarizations. meths Filters for KetBra excitation tree, see :func:`rotsim2d.pathways.gen_pathways`. Returns ------- det_angles Dict from detection angles to symmetric top pathways. """ kbs = pw.gen_pathways([5], rotor='symmetric', kiter_func=lambda x: [1], meths=meths) pws = dl.Pathway.from_kb_list(kbs) det_angles = pathway_angles(pws, angles, angles_vars) return det_angles
# ** RFactorPathways
[docs]class RFactorPathways: """Container for :class:`RFactor` and associated :class:`rotsim2d.dressedleaf.Pathway`.""" def __init__(self, rfactor: RFactor, pws: List[dl.Pathway]): self.rfactor = rfactor "R-factor associated with these pathways." self.pws = pws "List of pathways." self.props = {} """Dictionary of pathway properties. Each dict value is a list with length equal to ``len(self.pws)``. Each element of the list is a value of some quantity corresponding to a pathway in :attr:`pws`.""" self.det_angle = None "Detection angle zeroing this R-factor." def __repr__(self): return str(self) def __str__(self): return "RFactorPathways(rfactor={rfac!r}, peak_labels={pl!r}, trans_labels_deg={tld!r})".format( rfac=self.rfactor.tuple, tld=self.trans_labels_deg, pl=self.peak_labels )
[docs] @classmethod def from_pwlist(cls, pwlist: Sequence[dl.Pathway], highj: bool=False, normalize: bool=False) -> List['RFactorPathways']: """Classify pathways in ``pwlist`` with respect to R-factors. Return a list of :class:`RFactorPathways`.""" rfactors = [RFactor.from_pathway(pw, highj, normalize) for pw in pwlist] classified_pws = classify_dls(pwlist, rfactors) return [cls(rfactor, pws) for rfactor, pws in classified_pws.items()]
[docs] def calc_det_angle(self, angles: Optional[Sequence]=None): """Calculate zeroing detection angle for this R-factor.""" self.det_angle = solve_det_angle(self.rfactor, angles)
@property def trans_labels(self) -> List[str]: "Combined transition labels for all included pathways." labels = [pw.trans_label for pw in self.pws] labels.sort(key=lambda x: re.sub(r'[)(0-9)]', '', x)) return labels @property def trans_labels_deg(self) -> List[str]: "Combined degenerate transition labels for all included pathways." labels = list(set([pw.trans_label_deg for pw in self.pws])) labels.sort() return labels @property def peak_labels(self) -> List[str]: "Combined 2D peak labels for all included pathways." labels = list(set([pw.peak_label for pw in self.pws])) labels.sort() return labels
[docs] def add_property(self, name: str, d: List): """Add some property to pathways.""" assert len(d) == len(self.pws) self.props[name] = d
[docs]def calc_rfactors(rf_pw: RFactorPathways, *args, **kwargs): """Calculate J-dependent R-factors and uniquify them. Pass ``*args`` and ``**kwargs`` to :meth:`RFactor.from_pathway`. """ rf_pw.add_property( 'rfactors', [RFactor.from_pathway(pw, *args, **kwargs) for pw in rf_pw.pws])
[docs]def calc_angle_funcs(rf_pw: RFactorPathways, angles: Sequence): """Generate J-dependent zeroing angle functions.""" angle_funcs, angle_exprs = [], [] for rf in rf_pw.props['rfactors']: rf_det_angle = tan(solve_det_angle(rf, angles=angles[:3])) angle_exprs.append(rf_det_angle) if J_i in rf_det_angle.free_symbols: angle_funcs.append( lambdify(J_i, simplify(rf_det_angle))) else: angle_funcs.append(None) rf_pw.add_property('angle_funcs', angle_funcs) rf_pw.add_property('angle_exprs', angle_exprs)
[docs]def calc_angle_amps(rf_pw: RFactorPathways, angles_num: Sequence[float], js: Sequence[int]): """Calculate geometric amplitudes for specific angles and J_i values.""" amps = [] for rf in rf_pw.props['rfactors']: d = np.atleast_1d(rf.numeric_rel(*(angles_num+[js]))) if len(d) == 1: d = np.full(len(js), d[0]) amps.append(d) rf_pw.add_property('angle_amps', amps)
[docs]def calc_relative_exprs(rf_pw: RFactorPathways, angles: Sequence): """Calculate relative amplitudes for specified angles.""" exprs = [] for rf in rf_pw.props['rfactors']: expr = rf.expr_relative() expr = expr.subs(dict(zip(rf.angles, angles))) expr = fu(expr) # sym.FU['TR11'] might be enough exprs.append(expr) rf_pw.add_property('exprs_relative', exprs)
[docs]def calc_amplitude_exprs(rf_pw: RFactorPathways, angles: Sequence, norm: bool=True): """Calculate pathway amplitude expressions.""" exprs = [] for pw1 in rf_pw.pws: rfactor = RFactor.from_pathway(pw1) exprs.append( (amplitude_expr_pw(pw1, norm=norm)*\ rfactor.expr.subs( dict(zip(rfactor.angles, angles)))).\ replace(Abs, Id)) rf_pw.add_property('amplitude_exprs', exprs)
[docs]def optimize_contrast(rfpws_min: Sequence[RFactorPathways], rfpws_max: Sequence[RFactorPathways], initial_guess: Sequence[float], **min_kwargs): """Optimize contrast between two sets of R-factors. This is useful in case there are no analytical angles that simultaneously zero all R-factors in `rfpws_min`. Keyword arguments are passed to scipy.optimize.minimize. Returns OptimizeResult object. """ min_pws = sum((rfpw.pws for rfpw in rfpws_min), []) min_pws = list(dl.split_by_peaks(min_pws).values()) max_pws = sum((rfpw.pws for rfpw in rfpws_max), []) max_pws = list(dl.split_by_peaks(max_pws).values()) def sum_rfactors(peaks: Iterable[dl.Pathway], angles: Sequence[float]): angles = list(angles) sum_geo = 0.0 for peak_pws in peaks: peak_sum = 0.0 for pw in peak_pws: thetas = pw._phi_angles([0.0] + angles) gfacs = gfactors_highj_numeric[pw.geo_label] for k in (0, 1, 2): peak_sum += gfacs[k]*T00(*(thetas + [k])) sum_geo += abs(peak_sum) return sum_geo def opt_func(thetas): min_geo = sum_rfactors(min_pws, thetas) max_geo = sum_rfactors(max_pws, thetas) print("max_geo={:f}, min_geo={:f}".format(max_geo, min_geo)) return -(max_geo-min_geo) return minimize(opt_func, initial_guess, bounds=[(-np.pi/2, np.pi/2)]*3, **min_kwargs)
# * Rotational coherence # Retrieve rotational coherence expressions from Pathways. F = Function('F') def rot_expression(state: RotState, jref: int, higher_order: Optional[str]=None) -> Basic: """Rotational energy expression relative to ``jref``.""" dj = state.j-jref globs = globals() nu = globs['nu'+str(state.nu)] is_symtop = getattr(state, "k", None) if higher_order: if is_symtop: expr = F(nu, J_i+dj, K) else: expr = F(nu, J_i+dj) else: expr = nu + globs['B'+str(state.nu)]*(J_i+dj)*(J_i+dj+1) if is_symtop: expr += globs['BK'+str(state.nu)]*K**2 if higher_order == 'DJ': expr += globs['DJ'+str(state.nu)]*((J_i+dj)*(J_i+dj+1))**2 return expr def rcs_expression(pair: Tuple[RotState, RotState], jref: int, higher_order: str=None) -> Basic: """Return rotational beat expression.""" return factor(rot_expression(pair[1], jref, higher_order)- rot_expression(pair[0], jref, higher_order), deep=True) honl_london_factors = {0: {-1: (Jpp+Kpp)*(Jpp-Kpp)/Jpp, 0: (2*Jpp+1)*Kpp**2/Jpp/(Jpp+1), 1: (Jpp+1+Kpp)*(Jpp+1-Kpp)/(Jpp+1)}} def honl_london(pair: Tuple[RotState, RotState], jref: int) -> Basic: """Return Honl-London factor in terms of J_i = jref.""" if 'k' in attrs.fields_dict(type(pair[0])): K = Kpp else: K = 0 dj = pair[0].j-jref return honl_london_factors[0][pair[1].j-pair[0].j].\ subs(Kpp, K).subs(Jpp, J_i+dj) def vib_expr(pair: Tuple[RotState, RotState]) -> Basic: """Return mu_ij symbol for `pair`.""" nus = sorted((pair[0].nu, pair[1].nu)) globs = globals() return globs['mu'+str(nus[0])+str(nus[1])] def honl_london_pw(transitions: Sequence[Tuple[RotState, RotState]], jref: int) -> Basic: """Four-fold Honl-London factor for a pathway.""" return factor(reduce(mul, [honl_london(t, jref) for t in transitions], 1), deep=True) def amplitude_expr_pw(pw1, norm: bool=False) -> Basic: """Pathway amplitude expression. Without equilibrium population (Boltzmann) factor and R-factor. Includes isotropy factor. For a quantity roughly equal for each J_i state, multiply by (2*J_i+1)**(1/2). Parameters ---------- pw1 : Pathway norm : bool Flip the sign for negative probe pathway. Returns ------- Basic Sympy pathway amplitude expression. """ # Need to include the sign of rmu. const = -(I/hbar)**(len(pw1.transitions)-1)*pw1.leaf.total_side()/sqrt(2*J_i+1) vibs = factor(reduce(mul, [vib_expr(t) for t in pw1.transitions], 1), deep=True) rots = sqrt(honl_london_pw(pw1.transitions, pw1.leaf.root.ket.j)) rot_sign = 1 sides = [li.side for li in pw1.leaf.interactions()] for pair, side in zip(pw1.transitions, sides): if side == pw.Side.BRA: pair = pair[::-1] rot_sign *= -1 if pair[0].j < pair[1].j else 1 if norm: probe = pw1.coherences[2] if (probe[0].nu < probe[1].nu) or\ (probe[0].nu == probe[1].nu and probe[0].j < probe[1].j): rot_sign *= -1 return const*vibs*rots*rot_sign