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.
@@ -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,8 @@
1
+ class VBCASError(Exception):
2
+ """Errors raised by vbcas"""
3
+
4
+ class VBCASTypeError(VBCASError,TypeError):
5
+ """Type errors raised by vbcas"""
6
+
7
+ class VBCASValueError(VBCASError,ValueError):
8
+ """Value errors raised by vbcas"""
@@ -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,2 @@
1
+ Incorrect data.
2
+ No CI expansion.
@@ -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']