qcmr.vbcas 0.5.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qcmr_vbcas-0.5.1/PKG-INFO +31 -0
- qcmr_vbcas-0.5.1/README.md +17 -0
- qcmr_vbcas-0.5.1/pyproject.toml +46 -0
- qcmr_vbcas-0.5.1/src/qcmr/vbcas/__init__.py +11 -0
- qcmr_vbcas-0.5.1/src/qcmr/vbcas/cli.py +114 -0
- qcmr_vbcas-0.5.1/src/qcmr/vbcas/error.py +8 -0
- qcmr_vbcas-0.5.1/src/qcmr/vbcas/vbcas.py +356 -0
- qcmr_vbcas-0.5.1/tests/__init__.py +0 -0
- qcmr_vbcas-0.5.1/tests/data/ex1.out.gz +0 -0
- qcmr_vbcas-0.5.1/tests/data/incorrect.out +2 -0
- qcmr_vbcas-0.5.1/tests/test_vbcas.py +127 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: qcmr.vbcas
|
|
3
|
+
Version: 0.5.1
|
|
4
|
+
Summary: VB-analysis of CAS-CI expansion
|
|
5
|
+
Author-Email: Mariusz Radon <mariusz.radon@uj.edu.pl>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Requires-Dist: roman>=4.2
|
|
9
|
+
Requires-Dist: wrapfile>=0.1
|
|
10
|
+
Requires-Dist: pandas>=1.5.2
|
|
11
|
+
Requires-Dist: numpy>=1.23.5
|
|
12
|
+
Requires-Dist: matplotlib>=3.7.4
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# qcmr.vbcas
|
|
16
|
+
|
|
17
|
+
VB-CAS = Valence Bond (VB) analysis of CASSCF wave function
|
|
18
|
+
in the basis of (partially) localized orbitals.
|
|
19
|
+
|
|
20
|
+
The tool supports VB-CAS analysis to calculate cumulative
|
|
21
|
+
percentage contributions of various resonance structures
|
|
22
|
+
based on [Open]Molcas RASSCF output files. It can also
|
|
23
|
+
produce simple piechart out of the results.
|
|
24
|
+
|
|
25
|
+
For description and examples of VB-CAS analyses,
|
|
26
|
+
see the following papers (DOIs):
|
|
27
|
+
10.1016/j.jcat.2020.07.018,
|
|
28
|
+
10.3390/molecules26061511,
|
|
29
|
+
10.1039/c5cp07452e,
|
|
30
|
+
10.1021/jp910220r
|
|
31
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# qcmr.vbcas
|
|
2
|
+
|
|
3
|
+
VB-CAS = Valence Bond (VB) analysis of CASSCF wave function
|
|
4
|
+
in the basis of (partially) localized orbitals.
|
|
5
|
+
|
|
6
|
+
The tool supports VB-CAS analysis to calculate cumulative
|
|
7
|
+
percentage contributions of various resonance structures
|
|
8
|
+
based on [Open]Molcas RASSCF output files. It can also
|
|
9
|
+
produce simple piechart out of the results.
|
|
10
|
+
|
|
11
|
+
For description and examples of VB-CAS analyses,
|
|
12
|
+
see the following papers (DOIs):
|
|
13
|
+
10.1016/j.jcat.2020.07.018,
|
|
14
|
+
10.3390/molecules26061511,
|
|
15
|
+
10.1039/c5cp07452e,
|
|
16
|
+
10.1021/jp910220r
|
|
17
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "qcmr.vbcas"
|
|
3
|
+
description = "VB-analysis of CAS-CI expansion"
|
|
4
|
+
authors = [
|
|
5
|
+
{ name = "Mariusz Radon", email = "mariusz.radon@uj.edu.pl" },
|
|
6
|
+
]
|
|
7
|
+
dynamic = []
|
|
8
|
+
dependencies = [
|
|
9
|
+
"roman>=4.2",
|
|
10
|
+
"wrapfile>=0.1",
|
|
11
|
+
"pandas>=1.5.2",
|
|
12
|
+
"numpy>=1.23.5",
|
|
13
|
+
"matplotlib>=3.7.4",
|
|
14
|
+
]
|
|
15
|
+
requires-python = ">=3.8"
|
|
16
|
+
readme = "README.md"
|
|
17
|
+
version = "0.5.1"
|
|
18
|
+
|
|
19
|
+
[project.license]
|
|
20
|
+
text = "MIT"
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
vbcas = "qcmr.vbcas.cli:main"
|
|
24
|
+
|
|
25
|
+
[build-system]
|
|
26
|
+
requires = [
|
|
27
|
+
"pdm-backend",
|
|
28
|
+
]
|
|
29
|
+
build-backend = "pdm.backend"
|
|
30
|
+
|
|
31
|
+
[tool.pdm]
|
|
32
|
+
distribution = true
|
|
33
|
+
|
|
34
|
+
[tool.pdm.version]
|
|
35
|
+
source = "scm"
|
|
36
|
+
|
|
37
|
+
[tool.pdm.build]
|
|
38
|
+
includes = [
|
|
39
|
+
"src/qcmr/vbcas",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[dependency-groups]
|
|
43
|
+
dev = [
|
|
44
|
+
"ipython>=8.12.3",
|
|
45
|
+
"pytest>=8.3.5",
|
|
46
|
+
]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .vbcas import readci, analyze, piechart
|
|
2
|
+
__all__=["readci", "analyze", "piechart"]
|
|
3
|
+
|
|
4
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version("qcmr.vbcas")
|
|
7
|
+
except PackageNotFoundError:
|
|
8
|
+
try:
|
|
9
|
+
__version__ = version("qcmr-vbcas")
|
|
10
|
+
except PackageNotFoundError:
|
|
11
|
+
__version__ = "unknown"
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
4
|
+
|
|
5
|
+
from qcmr.vbcas import readci, analyze, piechart, __version__
|
|
6
|
+
|
|
7
|
+
def main():
|
|
8
|
+
ap = argparse.ArgumentParser('vbcas')
|
|
9
|
+
ap.add_argument(
|
|
10
|
+
'molcas_output', type=str,
|
|
11
|
+
help='Molcas (RASSCF) output with CI expansion to analyse'
|
|
12
|
+
)
|
|
13
|
+
ap.add_argument(
|
|
14
|
+
'k', metavar='K', type=int,
|
|
15
|
+
help="""The number of orbitals to count electrons in the CSF
|
|
16
|
+
(reading a CSF string from left to right).
|
|
17
|
+
The number of electrons/charge and spin (if applicable)
|
|
18
|
+
will be calculated based on orbitals 1 to K (default)
|
|
19
|
+
or (with --after) orbitals starting from K+1."""
|
|
20
|
+
)
|
|
21
|
+
ap.add_argument(
|
|
22
|
+
'--after', '-a', action='store_true',
|
|
23
|
+
help="""Changes how argument K is interpreted. See above"""
|
|
24
|
+
)
|
|
25
|
+
ap.add_argument(
|
|
26
|
+
'--nelec', '-n', metavar='N', type=int,
|
|
27
|
+
help="""The number of electrons which corresponds to the
|
|
28
|
+
oxidation state zero. If this is provided, the CSFs will
|
|
29
|
+
be classified using charge rather than electron count
|
|
30
|
+
(optional, the default is to classify
|
|
31
|
+
CSF by the electron count)."""
|
|
32
|
+
)
|
|
33
|
+
ap.add_argument(
|
|
34
|
+
'--spin', '-s', action='store_true',
|
|
35
|
+
help="""Classify CSFs not only by the number of electrons/charge,
|
|
36
|
+
but also by the spin stemming from electrons in orbitals 1 to K
|
|
37
|
+
(optional, the default is to ignore the spin)."""
|
|
38
|
+
)
|
|
39
|
+
ap.add_argument(
|
|
40
|
+
'--thresh', '-t', type=float,
|
|
41
|
+
help="threshold for printing individual CSFs"
|
|
42
|
+
)
|
|
43
|
+
ap.add_argument(
|
|
44
|
+
'--roman', action='store_true',
|
|
45
|
+
help='''Format oxidation state using Roman numbers.
|
|
46
|
+
Only applicable if --nelec was used.'''
|
|
47
|
+
)
|
|
48
|
+
ap.add_argument(
|
|
49
|
+
'--add-bar', action='store_true',
|
|
50
|
+
help='''Add vertical bar ("|") to CSF strings after orbitals 1 to K.
|
|
51
|
+
This could make these labels more readable'''
|
|
52
|
+
)
|
|
53
|
+
ap.add_argument(
|
|
54
|
+
'--csv', metavar='FILE',
|
|
55
|
+
help='''Save results to FILE in csv format (with semicolon as separator)'''
|
|
56
|
+
)
|
|
57
|
+
ap.add_argument(
|
|
58
|
+
'--piechart', metavar='FILE',
|
|
59
|
+
help='''Create a pie chart and save it in FILE'''
|
|
60
|
+
)
|
|
61
|
+
ap.add_argument(
|
|
62
|
+
'--root', type=int, default=1,
|
|
63
|
+
help='''Number of root to create a pie chart.
|
|
64
|
+
Only meaningfull with --piechart.'''
|
|
65
|
+
)
|
|
66
|
+
ap.add_argument(
|
|
67
|
+
'--figsize', type=float, nargs=2,
|
|
68
|
+
default=[3.33,3.33],
|
|
69
|
+
help='''Size (in inch) of the piechart.
|
|
70
|
+
Only meaningfull with --piechart.'''
|
|
71
|
+
)
|
|
72
|
+
ap.add_argument(
|
|
73
|
+
'--colordict', choices=['CSR2026', 'IC2023'],
|
|
74
|
+
help='''Choice of colordict for the piechart'''
|
|
75
|
+
)
|
|
76
|
+
ap.add_argument(
|
|
77
|
+
'--version', action='version',
|
|
78
|
+
version='%(prog)s {version}'.format(version=__version__)
|
|
79
|
+
)
|
|
80
|
+
args = ap.parse_args()
|
|
81
|
+
cidata = readci(args.molcas_output)
|
|
82
|
+
kwargs = dict(
|
|
83
|
+
korb=args.k,
|
|
84
|
+
after=args.after,
|
|
85
|
+
nelec=args.nelec,
|
|
86
|
+
spin=args.spin,
|
|
87
|
+
thresh=args.thresh,
|
|
88
|
+
roman=args.roman,
|
|
89
|
+
add_bar=args.add_bar,
|
|
90
|
+
)
|
|
91
|
+
if args.csv:
|
|
92
|
+
df = analyze(
|
|
93
|
+
cidata,
|
|
94
|
+
**kwargs
|
|
95
|
+
)
|
|
96
|
+
df.to_csv(args.csv,sep=';')
|
|
97
|
+
else:
|
|
98
|
+
analyze(
|
|
99
|
+
cidata,
|
|
100
|
+
outfile=sys.stdout,
|
|
101
|
+
**kwargs
|
|
102
|
+
)
|
|
103
|
+
if args.piechart:
|
|
104
|
+
df = analyze(
|
|
105
|
+
cidata,
|
|
106
|
+
**kwargs
|
|
107
|
+
)
|
|
108
|
+
fig, ax = plt.subplots(figsize=args.figsize)
|
|
109
|
+
piechart(
|
|
110
|
+
ax,
|
|
111
|
+
data=df[df['root']==args.root],
|
|
112
|
+
colordict=args.colordict,
|
|
113
|
+
)
|
|
114
|
+
fig.savefig(args.piechart, bbox_inches='tight')
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
from wrapfile import TextFileReader, TextFileWriter
|
|
2
|
+
from collections import namedtuple
|
|
3
|
+
import re
|
|
4
|
+
import roman
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import numpy as np
|
|
7
|
+
from .error import VBCASTypeError, VBCASValueError
|
|
8
|
+
|
|
9
|
+
rstart = re.compile('^ +printout of CI-coefficients larger than .+ for root *(?P<n>[0123456789]+)')
|
|
10
|
+
|
|
11
|
+
ConfigItem = namedtuple(
|
|
12
|
+
'ConfigItem', ['ii', 'conf', 'coeff', 'weight']
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
def readci(
|
|
16
|
+
filepath_or_buf,
|
|
17
|
+
recompute_weights=True,
|
|
18
|
+
):
|
|
19
|
+
"""Read CI expansion from Molcas RASSCF output
|
|
20
|
+
|
|
21
|
+
Parameters:
|
|
22
|
+
-----------
|
|
23
|
+
filepath_or_buf : str, path-like or file-like
|
|
24
|
+
a file or a path to file containing output
|
|
25
|
+
from Molcas RASSCF calculations
|
|
26
|
+
|
|
27
|
+
recompute_weights : bool (optional, default: True)
|
|
28
|
+
recompute small weights from squared coeffcients.
|
|
29
|
+
This is beneficial for the accuracy for weigths
|
|
30
|
+
below 0.25.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
--------
|
|
34
|
+
results : pandas.DataFrame
|
|
35
|
+
CI results as dataframe with the following columns:
|
|
36
|
+
'root', 'ii', 'conf', 'coeff', 'weight'.
|
|
37
|
+
"""
|
|
38
|
+
results = {
|
|
39
|
+
'root': [],
|
|
40
|
+
'ii': [],
|
|
41
|
+
'conf': [],
|
|
42
|
+
'coeff': [],
|
|
43
|
+
'weight': []
|
|
44
|
+
}
|
|
45
|
+
ready = False
|
|
46
|
+
with TextFileReader(filepath_or_buf) as f:
|
|
47
|
+
for l in f.readlines():
|
|
48
|
+
if ready:
|
|
49
|
+
if skip > 0:
|
|
50
|
+
skip -= 1
|
|
51
|
+
elif skip == 0:
|
|
52
|
+
ll = l.split()
|
|
53
|
+
if not ll:
|
|
54
|
+
ready = False
|
|
55
|
+
continue
|
|
56
|
+
weight = float(ll[-1])
|
|
57
|
+
coeff = float(ll[-2])
|
|
58
|
+
if recompute_weights:
|
|
59
|
+
# coeff < 1/2, recompute weight
|
|
60
|
+
# from coeff to reduce error
|
|
61
|
+
# (this comes from error propagation:
|
|
62
|
+
# w = c^2, so err(w) = 2*c*err(c);
|
|
63
|
+
# and both are printed to the same accuracy
|
|
64
|
+
if weight < 0.25:
|
|
65
|
+
weight = coeff**2
|
|
66
|
+
ii = int(ll[0])
|
|
67
|
+
conf = ''.join(ll[1:-2])
|
|
68
|
+
results['root'].append(n)
|
|
69
|
+
results['ii'].append(ii)
|
|
70
|
+
results['conf'].append(conf)
|
|
71
|
+
results['coeff'].append(coeff)
|
|
72
|
+
results['weight'].append(weight)
|
|
73
|
+
elif m := rstart.match(l):
|
|
74
|
+
n = int(m.group('n'))
|
|
75
|
+
ready = True
|
|
76
|
+
skip = 2
|
|
77
|
+
results = pd.DataFrame(results)
|
|
78
|
+
if results.empty:
|
|
79
|
+
raise VBCASValueError(
|
|
80
|
+
f"Cannot read a CI expansion from {filepath_or_buf}"
|
|
81
|
+
)
|
|
82
|
+
return results
|
|
83
|
+
|
|
84
|
+
class Counter:
|
|
85
|
+
def __call__(self, conf):
|
|
86
|
+
raise NotImplementedError
|
|
87
|
+
|
|
88
|
+
class ElectronCounter(Counter):
|
|
89
|
+
def __init__(self, k, after=False):
|
|
90
|
+
self.k = k
|
|
91
|
+
self.after = after
|
|
92
|
+
def __call__(self, conf):
|
|
93
|
+
n = 0
|
|
94
|
+
if self.after:
|
|
95
|
+
c = conf[self.k:]
|
|
96
|
+
else:
|
|
97
|
+
c = conf[:self.k]
|
|
98
|
+
for i in c:
|
|
99
|
+
if i == '2':
|
|
100
|
+
n += 2
|
|
101
|
+
elif i == 'u' or i == 'd':
|
|
102
|
+
n += 1
|
|
103
|
+
return n
|
|
104
|
+
|
|
105
|
+
class ChargeCounter(ElectronCounter):
|
|
106
|
+
def __init__(self, k, n, after=False):
|
|
107
|
+
super().__init__(k, after)
|
|
108
|
+
self.n = n
|
|
109
|
+
def __call__(self, conf):
|
|
110
|
+
n = super().__call__(conf)
|
|
111
|
+
return self.n - n
|
|
112
|
+
|
|
113
|
+
class SpinCounter(Counter):
|
|
114
|
+
def __init__(self, k):
|
|
115
|
+
self.k = k
|
|
116
|
+
def __call__(self, conf):
|
|
117
|
+
s = 0
|
|
118
|
+
for i in conf[:self.k]:
|
|
119
|
+
if i == 'u':
|
|
120
|
+
s += 1
|
|
121
|
+
elif i == 'd':
|
|
122
|
+
s -= 1
|
|
123
|
+
return s/2
|
|
124
|
+
|
|
125
|
+
def oxstate(n):
|
|
126
|
+
if n < 0:
|
|
127
|
+
return '-' + roman.toRoman(-n)
|
|
128
|
+
elif n == 0:
|
|
129
|
+
return '0'
|
|
130
|
+
else: # n > 0
|
|
131
|
+
return roman.toRoman(n)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def analyze(
|
|
135
|
+
cidata, korb, after=False,
|
|
136
|
+
nelec=None, spin=False, thresh=None,
|
|
137
|
+
outfile=None, fmt="%-20s %5.2f%%",
|
|
138
|
+
roman=False,
|
|
139
|
+
add_bar=False):
|
|
140
|
+
"""VB-CAS analysis
|
|
141
|
+
|
|
142
|
+
Parameters:
|
|
143
|
+
-----------
|
|
144
|
+
cidata : pd.DataFrame
|
|
145
|
+
CI data in the dataframe obtained from readci()
|
|
146
|
+
|
|
147
|
+
korb : int
|
|
148
|
+
number of orbitals to count electrons (and spins) in a CSF
|
|
149
|
+
|
|
150
|
+
after : bool (optional)
|
|
151
|
+
If True, the number of electrons or charge will be calculated
|
|
152
|
+
for orbitals after korb ([korb:]).
|
|
153
|
+
Otherwise this is calculated for orbitals 1 to korb-th
|
|
154
|
+
([:korb]).
|
|
155
|
+
|
|
156
|
+
nelec : int (optional)
|
|
157
|
+
number of electrons to assign a zeroth charge
|
|
158
|
+
|
|
159
|
+
spin : bool (optional, default False)
|
|
160
|
+
if True, also count electrons' spin
|
|
161
|
+
|
|
162
|
+
thresh : float (optional)
|
|
163
|
+
if set, will print individual contributions
|
|
164
|
+
with weights greater than this value.
|
|
165
|
+
Only applicable if outfile is set.
|
|
166
|
+
|
|
167
|
+
outfile : path-like or file-like (optional)
|
|
168
|
+
the file to write results to.
|
|
169
|
+
If not defined, the results are returned as dict.
|
|
170
|
+
|
|
171
|
+
roman : bool (optional)
|
|
172
|
+
Format oxidation state using Roman numbers.
|
|
173
|
+
Only applicable if nelec was used.
|
|
174
|
+
|
|
175
|
+
add_bar : bool (optional)
|
|
176
|
+
Add bar to CSF after k-th orbital to improve
|
|
177
|
+
readability.
|
|
178
|
+
|
|
179
|
+
fmt : str, format-spec (optional)
|
|
180
|
+
the format for analysis output.
|
|
181
|
+
Only applicable if outfile is set.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
-------
|
|
185
|
+
None or pd.DataFrame
|
|
186
|
+
If `outfile` is not defined, return pandas.DataFrame
|
|
187
|
+
with the analysis results. Otherwise return None.
|
|
188
|
+
|
|
189
|
+
"""
|
|
190
|
+
if not isinstance(cidata, pd.DataFrame):
|
|
191
|
+
raise VBCASTypeError(
|
|
192
|
+
f"Argument `cidata` must be DataFrame, not {type(cidata)}"
|
|
193
|
+
)
|
|
194
|
+
results = cidata.copy()
|
|
195
|
+
# determine how CSFs will be classified
|
|
196
|
+
if nelec:
|
|
197
|
+
cc = ChargeCounter(korb, nelec, after)
|
|
198
|
+
else:
|
|
199
|
+
cc = ElectronCounter(korb, after)
|
|
200
|
+
if spin:
|
|
201
|
+
sc = SpinCounter(korb)
|
|
202
|
+
else:
|
|
203
|
+
sc = None
|
|
204
|
+
if sc:
|
|
205
|
+
f = lambda x: (cc(x),sc(x))
|
|
206
|
+
else:
|
|
207
|
+
f = cc
|
|
208
|
+
results['class'] = results['conf'].map(f)
|
|
209
|
+
del f
|
|
210
|
+
# aggregate CSF belonging to the same class
|
|
211
|
+
z = results[
|
|
212
|
+
['root','class','weight']
|
|
213
|
+
].groupby(
|
|
214
|
+
['root','class']
|
|
215
|
+
).agg('sum')
|
|
216
|
+
# possible formatting of individual CSFs
|
|
217
|
+
if add_bar:
|
|
218
|
+
fmt_conf = lambda conf: conf[:korb] + '|' + conf[korb:]
|
|
219
|
+
else:
|
|
220
|
+
fmt_conf = lambda conf: conf
|
|
221
|
+
def fmt_conf_list(conf_list):
|
|
222
|
+
return [fmt_conf(x) for x in conf_list]
|
|
223
|
+
# selected CSF belonging to the same class
|
|
224
|
+
t = results[results['weight'] >= thresh][
|
|
225
|
+
['root','class','conf','coeff']
|
|
226
|
+
].groupby(
|
|
227
|
+
['root','class']
|
|
228
|
+
).agg({'conf': fmt_conf_list, 'coeff': list})
|
|
229
|
+
df = pd.concat([z,t], axis=1).reset_index()
|
|
230
|
+
del z, t
|
|
231
|
+
# final formatting of charges
|
|
232
|
+
if nelec and roman:
|
|
233
|
+
chrg = oxstate
|
|
234
|
+
else:
|
|
235
|
+
chrg = lambda x: x
|
|
236
|
+
if sc:
|
|
237
|
+
f = lambda x: (chrg(x[0]),x[1])
|
|
238
|
+
else:
|
|
239
|
+
f = lambda x: chrg(x)
|
|
240
|
+
df['class'] = df['class'].map(f)
|
|
241
|
+
# textual report ...
|
|
242
|
+
if outfile:
|
|
243
|
+
df = df.set_index(['root','class'])
|
|
244
|
+
with TextFileWriter(outfile) as f:
|
|
245
|
+
# loop over roots
|
|
246
|
+
for n in df.index.levels[0]:
|
|
247
|
+
print(f'* root {n:2d}', file=f)
|
|
248
|
+
df_tmp = df.loc[n].reset_index()
|
|
249
|
+
w = 0
|
|
250
|
+
for i,r in df_tmp.iterrows():
|
|
251
|
+
print(
|
|
252
|
+
fmt %(
|
|
253
|
+
r['class'],
|
|
254
|
+
r['weight']*100
|
|
255
|
+
),
|
|
256
|
+
file=f
|
|
257
|
+
)
|
|
258
|
+
if pd.api.types.is_list_like(r['conf']):
|
|
259
|
+
for conf,coeff in zip(
|
|
260
|
+
r['conf'],
|
|
261
|
+
r['coeff']
|
|
262
|
+
):
|
|
263
|
+
print(
|
|
264
|
+
'.. ' + fmt %(conf,100*coeff**2),
|
|
265
|
+
file=f
|
|
266
|
+
)
|
|
267
|
+
w += r['weight']
|
|
268
|
+
residue = 1 - w
|
|
269
|
+
print(fmt %('residue', (1-w)*100), file=f)
|
|
270
|
+
print(file=f)
|
|
271
|
+
else: # ... or return data for analysis
|
|
272
|
+
# class being a tuple is problematic for .loc
|
|
273
|
+
# so lets split it into two columns
|
|
274
|
+
if sc:
|
|
275
|
+
df.insert(2, 'spin', df['class'].map(lambda x: x[1]))
|
|
276
|
+
df['class'] = df['class'].map(lambda x: x[0])
|
|
277
|
+
return df
|
|
278
|
+
|
|
279
|
+
COLORDICT = {
|
|
280
|
+
'CSR2026': {
|
|
281
|
+
1: '#149FE0',
|
|
282
|
+
0: '#AF977A',
|
|
283
|
+
-1: '#FD191F',
|
|
284
|
+
'residue': 'white',
|
|
285
|
+
}, # doi: 10.1039/D5CS00346F, Fig. 36
|
|
286
|
+
'IC2023': {
|
|
287
|
+
1: '#0F64FB',
|
|
288
|
+
0: '#82CF22',
|
|
289
|
+
-1: '#FE9E27',
|
|
290
|
+
-2: '#B8FEFE',
|
|
291
|
+
'residue': 'white',
|
|
292
|
+
}, # doi: 10.1021/acs.inorgchem.3c03689, Fig. 3
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
def piechart(
|
|
296
|
+
ax, data,
|
|
297
|
+
labeldict=None,
|
|
298
|
+
colordict=None,
|
|
299
|
+
thresh_show=0.01,
|
|
300
|
+
thresh_residue=0.01,
|
|
301
|
+
residue_label='residue',
|
|
302
|
+
autopct='%1.0f%%',
|
|
303
|
+
text_color='black',
|
|
304
|
+
text_fontsize=12,
|
|
305
|
+
text_weight='bold',
|
|
306
|
+
autopct_color='white',
|
|
307
|
+
autopct_fontsize=12,
|
|
308
|
+
autopct_weight='bold',
|
|
309
|
+
pie_kwargs={},
|
|
310
|
+
):
|
|
311
|
+
if type(colordict) == str:
|
|
312
|
+
colordict = COLORDICT[colordict]
|
|
313
|
+
sizes = []
|
|
314
|
+
labels = []
|
|
315
|
+
colors = []
|
|
316
|
+
if 'spin' in data.columns:
|
|
317
|
+
cs = zip(data['class'],data['spin'])
|
|
318
|
+
else:
|
|
319
|
+
cs = data['class']
|
|
320
|
+
for c,w in zip(cs,data['weight']):
|
|
321
|
+
if w >= thresh_show:
|
|
322
|
+
sizes.append(w)
|
|
323
|
+
if labeldict:
|
|
324
|
+
labels.append(labeldict[c])
|
|
325
|
+
else:
|
|
326
|
+
labels.append(str(c))
|
|
327
|
+
if colordict:
|
|
328
|
+
colors.append(colordict[c])
|
|
329
|
+
w = list(data['weight'])
|
|
330
|
+
r = 1 - sum(w)
|
|
331
|
+
if r > thresh_residue:
|
|
332
|
+
sizes.append(r)
|
|
333
|
+
labels.append(residue_label)
|
|
334
|
+
if colordict:
|
|
335
|
+
colors.append(colordict[residue_label])
|
|
336
|
+
if not colors:
|
|
337
|
+
colors = None
|
|
338
|
+
wedge_properties = {
|
|
339
|
+
'edgecolor': 'black',
|
|
340
|
+
'linewidth': 0.7
|
|
341
|
+
}
|
|
342
|
+
wedges, texts, autopct_texts = ax.pie(
|
|
343
|
+
sizes, labels=labels, colors=colors,
|
|
344
|
+
autopct=autopct,
|
|
345
|
+
wedgeprops=wedge_properties,
|
|
346
|
+
**pie_kwargs,
|
|
347
|
+
)
|
|
348
|
+
for text in texts:
|
|
349
|
+
text.set_color(text_color)
|
|
350
|
+
text.set_fontsize(text_fontsize)
|
|
351
|
+
text.set_weight(text_weight)
|
|
352
|
+
for text in autopct_texts:
|
|
353
|
+
text.set_color(autopct_color)
|
|
354
|
+
text.set_fontsize(autopct_fontsize)
|
|
355
|
+
text.set_weight(autopct_weight)
|
|
356
|
+
return wedges, texts, autopct_texts
|
|
File without changes
|
|
Binary file
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
import gzip
|
|
3
|
+
import pytest
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
from qcmr.vbcas import readci, analyze
|
|
8
|
+
from qcmr.vbcas.error import VBCASValueError
|
|
9
|
+
|
|
10
|
+
p = pathlib.Path(__file__).parent
|
|
11
|
+
|
|
12
|
+
def test_readci():
|
|
13
|
+
with gzip.open(p/'data'/'ex1.out.gz', 'rt') as f:
|
|
14
|
+
r = readci(f)
|
|
15
|
+
# test if there are roots 1 to 6
|
|
16
|
+
assert set(r['root']) == set(range(1,7))
|
|
17
|
+
# tests lengths of CI expansions
|
|
18
|
+
assert len(r[r['root']==1]) == 7
|
|
19
|
+
assert len(r[r['root']==2]) == 7
|
|
20
|
+
assert len(r[r['root']==3]) == 7
|
|
21
|
+
assert len(r[r['root']==4]) == 8
|
|
22
|
+
assert len(r[r['root']==5]) == 10
|
|
23
|
+
assert len(r[r['root']==6]) == 8
|
|
24
|
+
# test selected items
|
|
25
|
+
x = r[r['root']==1].reset_index().iloc[0]
|
|
26
|
+
assert x.conf == '22200'
|
|
27
|
+
assert x.coeff == pytest.approx(0.29609, abs=1e-5)
|
|
28
|
+
assert x.weight == pytest.approx(0.08767, abs=1e-5)
|
|
29
|
+
y = r[r['root']==5].reset_index().iloc[9]
|
|
30
|
+
assert y.conf == '022ud'
|
|
31
|
+
assert y.coeff == pytest.approx(0.06341, abs=1e-5)
|
|
32
|
+
assert y.weight == pytest.approx(0.00402, abs=1e-5)
|
|
33
|
+
|
|
34
|
+
def test_readci__filepath_or_buf():
|
|
35
|
+
filepath = p/'data'/'ex1.out.gz'
|
|
36
|
+
with gzip.open(filepath, 'rt') as f:
|
|
37
|
+
r1 = readci(f)
|
|
38
|
+
r2 = readci(filepath)
|
|
39
|
+
assert r1.equals(r2)
|
|
40
|
+
|
|
41
|
+
def test_readci__incorrect_data():
|
|
42
|
+
try:
|
|
43
|
+
readci(p/'data'/'incorrect.out')
|
|
44
|
+
assert False
|
|
45
|
+
except VBCASValueError:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
def test_analyze():
|
|
49
|
+
with gzip.open(p/'data'/'ex1.out.gz', 'rt') as f:
|
|
50
|
+
r = readci(f)
|
|
51
|
+
a = analyze(r, 3)
|
|
52
|
+
|
|
53
|
+
# tests number of roots
|
|
54
|
+
assert set(a['root'].unique()) == set(range(1,7))
|
|
55
|
+
|
|
56
|
+
# test classes for some roots
|
|
57
|
+
assert not a[(a['root']==1) & (a['class']==6)].empty
|
|
58
|
+
assert not a[(a['root']==1) & (a['class']==5)].empty
|
|
59
|
+
assert not a[(a['root']==1) & (a['class']==4)].empty
|
|
60
|
+
assert a[(a['root']==2) & (a['class']==6)].empty
|
|
61
|
+
assert not a[(a['root']==2) & (a['class']==5)].empty
|
|
62
|
+
assert not a[(a['root']==2) & (a['class']==4)].empty
|
|
63
|
+
|
|
64
|
+
# tests weights for selected items
|
|
65
|
+
a = a.set_index(['root','class'])
|
|
66
|
+
assert a.loc[1,6].weight == pytest.approx(0.08767, abs=1e-5)
|
|
67
|
+
assert a.loc[1,5].weight == pytest.approx(0.82442, abs=1e-5)
|
|
68
|
+
assert a.loc[1,4].weight == pytest.approx(0.08461, abs=1e-5)
|
|
69
|
+
assert a.loc[2,4].weight == pytest.approx(0.06398, abs=1e-5)
|
|
70
|
+
|
|
71
|
+
# all items in coeff should be N/A
|
|
72
|
+
assert a['coeff'].isna().all()
|
|
73
|
+
|
|
74
|
+
def test_analyze_charge():
|
|
75
|
+
with gzip.open(p/'data'/'ex1.out.gz', 'rt') as f:
|
|
76
|
+
r = readci(f)
|
|
77
|
+
a = analyze(r, 3, nelec=8).set_index(['root','class'])
|
|
78
|
+
assert a.loc[1,3].weight == pytest.approx(0.82442, abs=1e-5)
|
|
79
|
+
|
|
80
|
+
def test_analyze_charge_spin_thresh():
|
|
81
|
+
with gzip.open(p/'data'/'ex1.out.gz', 'rt') as f:
|
|
82
|
+
r = readci(f)
|
|
83
|
+
#
|
|
84
|
+
a = analyze(r, 3, nelec=8, spin=True, thresh=0.1).set_index(
|
|
85
|
+
['root','class','spin']
|
|
86
|
+
)
|
|
87
|
+
assert a.loc[1,3,0.5].weight == pytest.approx(0.82442, abs=1e-5)
|
|
88
|
+
assert a.loc[1,3,0.5].conf == ['2u20d', 'u22d0']
|
|
89
|
+
assert a.loc[1,4,1.0].weight == pytest.approx(0.05516, abs=1e-5)
|
|
90
|
+
assert np.isnan(a.loc[1,4,1.0].conf)
|
|
91
|
+
#
|
|
92
|
+
a = analyze(r, 3, nelec=8, spin=True, thresh=0.00001)
|
|
93
|
+
# tests number of roots
|
|
94
|
+
assert set(a['root']) == set(range(1,7))
|
|
95
|
+
|
|
96
|
+
a = a.set_index(['root','class','spin'])
|
|
97
|
+
assert a.loc[1,2,0].weight == pytest.approx(0.08767, abs=1e-5)
|
|
98
|
+
assert a.loc[1,2,0].conf == ['22200']
|
|
99
|
+
assert a.loc[1,4,1].weight == pytest.approx(0.05516, abs=1e-5)
|
|
100
|
+
assert a.loc[1,4,1].conf == ['uu2dd']
|
|
101
|
+
assert a.loc[1,4,1].coeff[0] == pytest.approx(-0.23486, abs=1e-5)
|
|
102
|
+
assert a.loc[6,3,0.5].weight == pytest.approx(0.96759, abs=1e-5)
|
|
103
|
+
assert a.loc[6,3,0.5].conf[0] == '2u2d0'
|
|
104
|
+
assert a.loc[6,3,0.5].conf[3] == 'u220d'
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_analyze_dataframe():
|
|
108
|
+
r = readci(p/'data'/'ex1.out.gz')
|
|
109
|
+
df = analyze(r, 3, nelec=8)
|
|
110
|
+
t = df[df['root']==1]
|
|
111
|
+
assert len(t['class']) == 3
|
|
112
|
+
for c,w in zip(t['class'],t['weight']):
|
|
113
|
+
if c == 2:
|
|
114
|
+
assert w == pytest.approx(0.08767, abs=1e-5)
|
|
115
|
+
elif c == 3:
|
|
116
|
+
assert w == pytest.approx(0.82442, abs=1e-5)
|
|
117
|
+
elif c == 4:
|
|
118
|
+
assert w == pytest.approx(0.08461, abs=1e-5)
|
|
119
|
+
df = analyze(r, 3, nelec=8, roman=True)
|
|
120
|
+
t = df[df['root']==2]
|
|
121
|
+
assert len(t['class']) == 2
|
|
122
|
+
for c,w in zip(t['class'],t['weight']):
|
|
123
|
+
if c == 'III':
|
|
124
|
+
assert w == pytest.approx(0.93471, abs=1e-5)
|
|
125
|
+
elif c == 'IV':
|
|
126
|
+
assert w == pytest.approx(0.06398, abs=1e-5)
|
|
127
|
+
assert list(t['class']) == ['III', 'IV']
|