CFcal 0.1.6__py3-none-any.whl

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.
cfcal/__init__.py ADDED
@@ -0,0 +1 @@
1
+ from ._version import version as __version__
cfcal/_version.py ADDED
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.1.6'
22
+ __version_tuple__ = version_tuple = (0, 1, 6)
23
+
24
+ __commit_id__ = commit_id = None
cfcal/cfcal.py ADDED
@@ -0,0 +1,695 @@
1
+ # CFcal - Crystal Field Module
2
+ #
3
+ # Copyright (C) 2008-2026 R. Osborn, E. A. Goremychkin
4
+ #
5
+ # This program is free software; you can redistribute it and/or modify
6
+ # it under the terms of the GNU Lesser General Public License as published by
7
+ # the Free Software Foundation; either version 2 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU Lesser General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU Lesser General Public License
16
+ # along with this program; if not, write to the Free Software
17
+ # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
18
+ #
19
+ """
20
+ CFcal: Python calculator for crystal fields
21
+
22
+ Crystal Field calculations using the Stevens Operator formalism.
23
+ """
24
+
25
+ import os
26
+ from configparser import ConfigParser
27
+
28
+ import numpy as np
29
+ from scipy.linalg import eigh
30
+
31
+ REs = ['Ce', 'Pr', 'Nd', 'Pm', 'Sm', 'Eu', 'Gd', 'Tb', 'Dy', 'Ho', 'Er', 'Tm',
32
+ 'Yb']
33
+ Jf = [2.5, 4.0, 4.5, 4.0, 2.5, 0.0, 3.5, 6.0, 7.5, 8.0, 7.5, 6.0, 3.5]
34
+ gJ = [6.0/7.0, 4.0/5.0, 8.0/11.0, 3.0/5.0, 2.0/7.0, 0.0, 2.0, 3.0/2.0, 4.0/3.0,
35
+ 5.0/4.0, 6.0/5.0, 7.0/6.0, 8.0/7.0]
36
+ #<r^2> radial integral for 3+ f-electrons
37
+ r2 = [0.3666, 0.3380, 0.3120, 0.2917, 0.2728, 0.2569, 0.2428, 0.2302, 0.2188,
38
+ 0.2085, 0.1991, 0.1905, 0.1826]
39
+ #<r^4> radial integral for 3+ f-electrons
40
+ r4 = [0.3108, 0.2670, 0.2015, 0.1488, 0.1772, 0.1584, 0.1427, 0.1295, 0.1180,
41
+ 0.1081, 0.0996, 0.0921, 0.0854]
42
+ #<r^6> radial integral for 3+ f-electrons
43
+ r6 = [0.5119, 0.4150, 0.3300, 0.2787, 0.2317, 0.1985, 0.1720, 0.1505, 0.1328,
44
+ 0.1810, 0.1058, 0.0953, 0.0863]
45
+ #Second-degree Stevens factors for 3+ f-electrons
46
+ alphaJ = [-5.7143e-02, -2.1010e-02, -6.4279e-03, 7.7135e-03, 4.1270e-02, 0.0,
47
+ 0.0, -1.0101e-02, -6.3492e-03, -2.2222e-03, 2.5397e-03, 1.0101e-02,
48
+ 3.1746e-02]
49
+ #Fourth-degree Stevens factors for 3+ f-electrons
50
+ betaJ = [6.3492e-03, -7.3462e-04, -2.9111e-04, 4.0755e-04, 2.5012e-03, 0.0, 0.0,
51
+ 1.2244e-04, -5.9200e-05, -3.3300e-05, 4.4400e-05, 1.6325e-04,
52
+ -1.7316e-03]
53
+ #Sixth-degree Stevens factors for 3+ f-electrons
54
+ gammaJ = [0.0, 6.0994e-05, -3.7988e-05, 6.6859e-04, 0.0, 0.0, 0.0, -1.1212e-06,
55
+ 1.0350e-06, -1.2937e-06, 2.0699e-06, -5.6061e-06, 1.4800e-04]
56
+
57
+
58
+ class CF():
59
+ """
60
+ Class defining the trivalent rare earth compound and its crystal field
61
+ parameters
62
+
63
+ There is only one basic object in CFlib, defining the trivalent rare
64
+ earth, its crystal field parameters, and, if already diagonalized, the
65
+ eigenvalues and eigenfunctions of the CF Hamiltonian.
66
+ """
67
+
68
+ def __init__(self, RE=None, name=None, parfile=None):
69
+ """Initializes the CF object or read it from a file."""
70
+
71
+ if parfile:
72
+ self.load(parfile)
73
+ else:
74
+ if RE:
75
+ self.RE = RE
76
+ else:
77
+ self.RE = 'Ce'
78
+ self.B20 = 0.0
79
+ self.B40 = 0.0
80
+ self.B60 = 0.0
81
+ self.B22 = 0.0
82
+ self.B42 = 0.0
83
+ self.B62 = 0.0
84
+ self.B43 = 0.0
85
+ self.B63 = 0.0
86
+ self.B44 = 0.0
87
+ self.B64 = 0.0
88
+ self.B66 = 0.0
89
+ self.Hz = 0.0
90
+ self.Hx = 0.0
91
+ self.name = name
92
+ self.H = np.matrix(np.zeros((self.size, self.size), dtype='float64'))
93
+ self.Jz = np.zeros((self.size, self.size), dtype='float64')
94
+ self.Jp = np.zeros((self.size, self.size), dtype='float64')
95
+ self.Jm = np.zeros((self.size, self.size), dtype='float64')
96
+ self.TP = np.zeros((self.size, self.size), dtype='float64')
97
+ self.EF = np.zeros((self.size, self.size), dtype='float64')
98
+ self.EV = np.zeros(self.size, dtype='float64')
99
+ self.peaks = []
100
+ self.T = 0.0
101
+ self.Tused = self.T
102
+ self.resolution = 0.01
103
+ self.threshold = 0.0001
104
+ self.Jz_exp = 0.0
105
+ self.Jx_exp = 0.0
106
+ self.muz = 0.0
107
+ self.mux = 0.0
108
+
109
+ def __str__(self):
110
+ """
111
+ Return a summary of the model parameters
112
+
113
+ This includes the rare earth, the CF parameters, and, if
114
+ diagonalized, the eigenvalues and eigenvectors.
115
+ """
116
+
117
+ if self.name:
118
+ output = [self.name]
119
+ else:
120
+ output = []
121
+ output.append("%s: Nf = %s, J = %s" % (self.RE, self.Nf, self.J))
122
+ line = []
123
+ if self.B20 != 0.0:
124
+ line.append("B20 = %g" % self.B20)
125
+ if self.B22 != 0.0:
126
+ line.append("B22 = %g" % self.B22)
127
+ if self.B40 != 0.0:
128
+ line.append("B40 = %g" % self.B40)
129
+ if self.B42 != 0.0:
130
+ line.append("B42 = %g" % self.B42)
131
+ if self.B43 != 0.0:
132
+ line.append("B43 = %g" % self.B43)
133
+ if self.B44 != 0.0:
134
+ line.append("B44 = %g" % self.B44)
135
+ if self.B60 != 0.0:
136
+ line.append("B60 = %g" % self.B60)
137
+ if self.B62 != 0.0:
138
+ line.append("B62 = %g" % self.B62)
139
+ if self.B63 != 0.0:
140
+ line.append("B63 = %g" % self.B63)
141
+ if self.B64 != 0.0:
142
+ line.append("B64 = %g" % self.B64)
143
+ if self.B66 != 0.0:
144
+ line.append("B66 = %g" % self.B66)
145
+ if self.Hz != 0.0:
146
+ line.append("Hz = %g" % self.Hz)
147
+ if self.Hx != 0.0:
148
+ line.append("Hx = %g" % self.Hx)
149
+
150
+ if line:
151
+ output.append(" ".join(line))
152
+
153
+ self.get_peaks()
154
+ Jx_exp, Jz_exp, mux, muz = self.get_moments()
155
+
156
+ if muz != 0.0 or mux != 0.0:
157
+ output.append("<Jz> = %7.3f <muz> = %7.3f muB" % (Jz_exp, muz))
158
+ output.append("<Jx> = %7.3f <mux> = %7.3f muB" % (Jx_exp, mux))
159
+
160
+ if self.EV.any():
161
+ output.append("Crystal Field Eigenvalues and Eigenfunctions")
162
+ for i in range(self.EV.size):
163
+ line = ["%8.3f:" % self.EV[i]]
164
+ for j in range(self.EV.size):
165
+ if abs(self.EF[j, i]) > 0.0001:
166
+ Jz = j - self.J
167
+ if self.EF[j, i] < 0.0:
168
+ operator = "-"
169
+ else:
170
+ operator = "+"
171
+ line.append("%s%7.4f|%g>" %
172
+ (operator, abs(self.EF[j, i]), Jz))
173
+ output.append(" ".join(line))
174
+
175
+ if self.peaks:
176
+ output.append("Crystal Field Transitions")
177
+ if self.T != self.Tused:
178
+ self.get_peaks()
179
+ output.append("Temperature: %g K" % self.T)
180
+ for peak in self.peaks:
181
+ output.append("Energy: %8.3f meV Intensity: %8.4f" % peak)
182
+
183
+ return "\n".join(output)
184
+
185
+ def save(self, parfile=None):
186
+ """Store the current object for later use."""
187
+
188
+ parser = ConfigParser()
189
+ parser.optionxform = str
190
+
191
+ parser.add_section('material')
192
+ parser.set('material', 'name', str(self.name))
193
+ parser.set('material', 'RE', str(self.RE))
194
+
195
+ parser.add_section('parameters')
196
+ parser.set('parameters', 'B20', str(self.B20))
197
+ parser.set('parameters', 'B22', str(self.B22))
198
+ parser.set('parameters', 'B40', str(self.B40))
199
+ parser.set('parameters', 'B42', str(self.B42))
200
+ parser.set('parameters', 'B43', str(self.B43))
201
+ parser.set('parameters', 'B44', str(self.B44))
202
+ parser.set('parameters', 'B60', str(self.B60))
203
+ parser.set('parameters', 'B62', str(self.B62))
204
+ parser.set('parameters', 'B63', str(self.B63))
205
+ parser.set('parameters', 'B64', str(self.B64))
206
+ parser.set('parameters', 'B66', str(self.B66))
207
+ parser.set('parameters', 'Hz', str(self.Hz))
208
+ parser.set('parameters', 'Hx', str(self.Hx))
209
+
210
+ if parfile is None:
211
+ parfile = '%s.cfg' % self.name
212
+ with open(parfile, 'wb') as cfg:
213
+ parser.write(cfg)
214
+
215
+ def load(self, parfile):
216
+ """Load a saved version of the current object."""
217
+
218
+ if not os.path.exists(parfile):
219
+ raise OSError("'%s' does not exist" %
220
+ os.path.realpath(parfile))
221
+
222
+ parser = ConfigParser()
223
+ parser.read(parfile)
224
+
225
+ self.name = parser.get('material', 'name')
226
+ self.RE = parser.get('material', 'RE')
227
+
228
+ self.B20 = parser.getfloat('parameters', 'B20')
229
+ self.B22 = parser.getfloat('parameters', 'B22')
230
+ self.B40 = parser.getfloat('parameters', 'B40')
231
+ self.B42 = parser.getfloat('parameters', 'B42')
232
+ self.B43 = parser.getfloat('parameters', 'B43')
233
+ self.B44 = parser.getfloat('parameters', 'B44')
234
+ self.B60 = parser.getfloat('parameters', 'B60')
235
+ self.B62 = parser.getfloat('parameters', 'B62')
236
+ self.B63 = parser.getfloat('parameters', 'B63')
237
+ self.B64 = parser.getfloat('parameters', 'B64')
238
+ self.B66 = parser.getfloat('parameters', 'B66')
239
+ self.Hz = parser.getfloat('parameters', 'Hz')
240
+ self.Hx = parser.getfloat('parameters', 'Hx')
241
+
242
+ self.initialize()
243
+
244
+ def initialize(self):
245
+ """Reinitializes the arrays used for the CF Hamiltonian."""
246
+ self.H = np.matrix(np.zeros((self.size, self.size), dtype='float64'))
247
+ self.Jz = np.zeros((self.size, self.size), dtype='float64')
248
+ self.Jp = np.zeros((self.size, self.size), dtype='float64')
249
+ self.Jm = np.zeros((self.size, self.size), dtype='float64')
250
+ self.TP = np.zeros((self.size, self.size), dtype='float64')
251
+ self.EF = np.zeros((self.size, self.size), dtype='float64')
252
+ self.EV = np.zeros(self.size, dtype='float64')
253
+
254
+ @property
255
+ def index(self):
256
+ return REs.index(self.RE)
257
+
258
+ @property
259
+ def Nf(self):
260
+ return self.index + 1
261
+
262
+ @property
263
+ def J(self):
264
+ return Jf[self.index]
265
+
266
+ @property
267
+ def size(self):
268
+ return int(2 * self.J + 1)
269
+
270
+ @property
271
+ def gJ(self):
272
+ return gJ[self.index]
273
+
274
+ @property
275
+ def r2(self):
276
+ return r2[self.index]
277
+
278
+ @property
279
+ def r4(self):
280
+ return r4[self.index]
281
+
282
+ @property
283
+ def r6(self):
284
+ return r6[self.index]
285
+
286
+ @property
287
+ def alphaJ(self):
288
+ return alphaJ[self.index]
289
+
290
+ @property
291
+ def betaJ(self):
292
+ return betaJ[self.index]
293
+
294
+ @property
295
+ def gammaJ(self):
296
+ return gammaJ[self.index]
297
+
298
+ def CFham(self):
299
+ """Determine the CF Hamiltonian."""
300
+
301
+ J = self.J
302
+ H = np.matrix(np.zeros((self.size, self.size), dtype='float64'))
303
+
304
+ for m in range(self.size):
305
+ mJ = m - J
306
+ O20 = 3*mJ**2 - J*(J+1)
307
+ O40 = (35*mJ**4 - 30*J*(J+1)*mJ**2 +
308
+ 25*mJ**2 - 6*J*(J+1) + 3*(J*(J+1))**2)
309
+ O60 = (231*mJ**6 - 315*J*(J+1)*mJ**4 + 735*mJ**4 +
310
+ 105*(J*(J+1)*mJ)**2 - 525*J*(J+1)*mJ**2 + 294*mJ**2 -
311
+ 5*(J*(J+1))**3 + 40*(J*(J+1))**2 - 60*J*(J+1))
312
+ H[m, m] += self.B20*O20 + self.B40*O40 + self.B60*O60
313
+
314
+ for m in range(self.size-2):
315
+ mJ = m - J
316
+ n = m + 2
317
+ nJ = mJ + 2
318
+ O22 = 0.5*np.sqrt((J*(J+1) - nJ*(nJ-1))*(J*(J+1) - (nJ-1)*(nJ-2)))
319
+ O42 = (3.5*(mJ**2+nJ**2) - J*(J+1) - 5) * O22
320
+ O62 = (16.5*(mJ**4+nJ**4) - 9*(mJ**2+nJ**2)*J*(J+1) -
321
+ 61.5*(mJ**2+nJ**2) + (J*(J+1))**2 + 10*J*(J+1) + 102) * O22
322
+ H[m,n] += self.B22*O22 + self.B42*O42 + self.B62*O62
323
+ H[n,m] = H[m,n]
324
+
325
+ for m in range(self.size - 3):
326
+ mJ = m - J
327
+ n = m + 3
328
+ nJ = mJ + 3
329
+ A = 1.0
330
+ for k in range(3):
331
+ A *= J*(J+1) - (nJ-k)*(nJ-k-1)
332
+ O43 = 0.25*np.sqrt(A)*(mJ + nJ)
333
+ O63 = (0.25*(11*(mJ**3+nJ**3) - 3*(mJ+nJ)*J*(J+1) - 59*(mJ+nJ)) *
334
+ np.sqrt(A))
335
+ H[m,n] += self.B43*O43 + self.B63*O63
336
+ H[n,m] = H[m,n]
337
+
338
+ for m in range(self.size - 4):
339
+ mJ = m - J
340
+ n = m + 4
341
+ nJ = mJ + 4
342
+ O44 = 1.0
343
+ for k in range(4):
344
+ O44 *= J*(J+1) - (nJ - k)*(nJ - k - 1)
345
+ O44 = 0.5*np.sqrt(O44)
346
+ O64 = (5.5*(mJ**2 + nJ**2) - J*(J+1) - 38)*O44
347
+ H[m,n] += self.B44*O44 + self.B64*O64
348
+ H[n,m] = H[m,n]
349
+
350
+ for m in range(self.size - 6):
351
+ mJ = m - J
352
+ n = m + 6
353
+ nJ = mJ + 6
354
+ O66 = 1.0
355
+ for k in range(6):
356
+ O66 *= J*(J+1) - (nJ - k)*(nJ - k - 1)
357
+ O66 = 0.5*np.sqrt(O66)
358
+ H[m,n] += self.B66*O66
359
+ H[n,m] = H[m,n]
360
+
361
+ return H
362
+
363
+ def MFham(self):
364
+ """Determine the Zeeman terms to the Hamiltonian."""
365
+
366
+ J = self.J
367
+ H = np.matrix(np.zeros((self.size, self.size), dtype='float64'))
368
+
369
+ for m in range(self.size):
370
+ mJ = m - J
371
+ H[m, m] -= self.Hz*mJ
372
+
373
+ for m in range(self.size - 1):
374
+ mJ = m - J
375
+ n = m + 1
376
+ nJ = mJ + 1
377
+ H[m,n] -= 0.5*self.Hx*np.sqrt((J*(J+1)-mJ*nJ))
378
+ H[n,m] = H[m,n]
379
+
380
+ return H
381
+
382
+ def Ham(self):
383
+ """Returns the total Hamiltonian including CF and Zeeman terms."""
384
+
385
+ return (self.CFham() + self.MFham())
386
+
387
+ def EFS(self):
388
+ """Calculate eigenvalues/functions of the total Hamiltonian."""
389
+
390
+ H = self.Ham()
391
+ self.EV, self.EF = eigh(H)
392
+ self.EV = self.EV - self.EV.min()
393
+
394
+ def TPS(self):
395
+ """Determine dipole matrix elements of the total Hamiltonian."""
396
+
397
+ J = self.J
398
+
399
+ self.Jz = np.zeros((self.size, self.size), dtype='float64')
400
+ self.Jp = np.zeros((self.size, self.size), dtype='float64')
401
+ self.Jm = np.zeros((self.size, self.size), dtype='float64')
402
+ for m in range(self.size):
403
+ for n in range(self.size):
404
+ for k in range(self.size):
405
+ kJ = k - J
406
+ self.Jz[m,n] += self.EF[k, m]*self.EF[k,n]*kJ
407
+ for k in range(self.size - 1):
408
+ kJ = k - J
409
+ l = k + 1
410
+ lJ = l - J
411
+ self.Jp[m,n] = (self.Jp[m,n] +
412
+ self.EF[l,m]*self.EF[k,n] *
413
+ np.sqrt(J*(J+1)-kJ*lJ))
414
+ for k in range(1, self.size):
415
+ kJ = k - J
416
+ l = k - 1
417
+ lJ = l - J
418
+ self.Jm[m,n] = (self.Jm[m,n] +
419
+ self.EF[l,m]*self.EF[k,n] *
420
+ np.sqrt(J*(J+1)-kJ*lJ))
421
+ self.TP[m,n] = (
422
+ (2*self.Jz[m,n]**2+self.Jp[m,n]**2+self.Jm[m,n]**2) / 3)
423
+
424
+ def get_peaks(self, T=None, Hx=None, Hz=None):
425
+ """Determine the peak intensities from the total Hamiltonian."""
426
+
427
+ if Hx:
428
+ old_Hx = self.Hx
429
+ self.Hx = Hx
430
+ if Hz:
431
+ old_Hz = self.Hz
432
+ self.Hz = Hz
433
+ self.EFS()
434
+ self.TPS()
435
+
436
+ BF = np.zeros(self.size, dtype='float64')
437
+
438
+ if T is None:
439
+ T = self.T
440
+ kT = T / 11.6045
441
+ if kT <= 0.0:
442
+ BF[0] = 1.0
443
+ else:
444
+ Z = sum(np.exp(-self.EV/kT))
445
+ BF = np.exp(-self.EV/kT) / Z
446
+
447
+ peaks = []
448
+ for n in range(self.size):
449
+ for m in range(self.size):
450
+ ET = self.EV[m] - self.EV[n]
451
+ IT = self.TP[m,n]*BF[n]
452
+ if IT > 0.0:
453
+ peaks.append((ET, IT))
454
+
455
+ self.peaks = []
456
+ for k in range(len(peaks)):
457
+ ETk, ITk = peaks[k]
458
+ if ITk > 0.0:
459
+ sum_peaks = ETk*ITk
460
+ for l in range(len(peaks)):
461
+ if k != l:
462
+ ETl, ITl = peaks[l]
463
+ if ITl > 0.0 and abs(ETk - ETl) < self.resolution:
464
+ ITk = ITk + ITl
465
+ sum_peaks = sum_peaks + ETl*ITl
466
+ peaks[l] = (ETl, 0.0)
467
+ if ITk > self.threshold:
468
+ ETk = sum_peaks / ITk
469
+ self.peaks.append((ETk, ITk))
470
+ self.peaks.sort()
471
+ self.Tused = T
472
+ if Hx:
473
+ self.Hx = old_Hx
474
+ if Hz:
475
+ self.Hz = old_Hz
476
+
477
+ return self.peaks
478
+
479
+ def spectrum(self, eps=None, sigma=None, gamma=None, T=None,
480
+ Hx=None, Hz=None):
481
+ """Calculates the neutron scattering cross section."""
482
+
483
+ if T is None:
484
+ T = self.T
485
+
486
+ peaks = self.get_peaks(T, Hx, Hz)
487
+
488
+ if eps is None:
489
+ eps = np.linspace(-1.1*self.EV[-1], 1.1*self.EV[-1], 501)
490
+ if sigma is None and gamma is None:
491
+ sigma = 0.01*(max(eps) - min(eps))
492
+
493
+ S = np.zeros(eps.size, dtype='float64')
494
+
495
+ for EV, IT in peaks:
496
+ if sigma is not None and gamma is None:
497
+ S += IT * gauss(eps, EV, sigma)
498
+ elif sigma is None and gamma is not None:
499
+ S += IT * lorentz(eps, EV, gamma)
500
+ else:
501
+ S += IT * pseudovoigt(eps, EV, sigma, gamma)
502
+
503
+ S *= 72.65*self.gJ**2
504
+
505
+ return S
506
+
507
+ def NXspectrum(self, eps=None, sigma=None, gamma=None, T=None,
508
+ Hx=None, Hz=None):
509
+ """Returns the neutron scattering cross section."""
510
+
511
+ from nexusformat.nexus import NXdata, NXentry, NXfield, NXsample
512
+
513
+ if T is None:
514
+ T = self.T
515
+
516
+ S = self.spectrum(eps, sigma, gamma, T, Hx, Hz)
517
+ entry = NXentry()
518
+ entry.title = "Crystal Field Spectra for %s at %s K" % (self.name, T)
519
+ entry.sample = NXsample()
520
+ entry.sample.temperature = T
521
+ entry.sample.temperature.units = "K"
522
+ entry.data = NXdata(NXfield(S, name="intensity", units="mb/sr/meV"),
523
+ NXfield(eps, name="energy_transfer", units="meV"))
524
+ return entry
525
+
526
+ nxspectrum = NXspectrum
527
+
528
+ def get_moments(self, T=None):
529
+ """Calculate the magnetic moments of the CF model."""
530
+
531
+ if T is None:
532
+ T = self.T
533
+ if T != self.Tused:
534
+ self.get_peaks(T)
535
+ kT = T / 11.6045
536
+ if kT > 0.0:
537
+ Jz_exp = 0.0
538
+ Jx_exp = 0.0
539
+ Z = sum(np.exp(-self.EV/kT))
540
+ for i in range(self.EV.size):
541
+ Jz_exp = Jz_exp + self.Jz[i,i]*np.exp(-self.EV[i]/kT)
542
+ Jx_exp += (0.5*(self.Jp[i,i] + self.Jm[i,i]) *
543
+ np.exp(-self.EV[i]/kT))
544
+ Jz_exp = Jz_exp / Z
545
+ Jx_exp = Jx_exp / Z
546
+ else:
547
+ Jz_exp = (sum(self.Jz[self.EV==0.0, self.EV==0.0]) /
548
+ self.EV[self.EV==0.0].size)
549
+ Jx_exp = (sum(0.5*(self.Jp[self.EV==0.0, self.EV==0.0] +
550
+ self.Jm[self.EV==0.0, self.EV==0.0])) /
551
+ self.EV[self.EV==0.0].size)
552
+ self.Jz_exp = Jz_exp
553
+ self.muz = self.gJ*self.Jz_exp
554
+ self.Jx_exp = Jx_exp
555
+ self.mux = self.gJ*self.Jx_exp
556
+
557
+ return self.Jx_exp, self.Jz_exp, self.mux, self.muz
558
+
559
+ def chi(self, T=None):
560
+ """Calculate the susceptibility at a specified temperature."""
561
+
562
+ if T is None:
563
+ T = self.T
564
+
565
+ kT = T / 11.6045
566
+ Z = sum(np.exp(-self.EV/kT))
567
+
568
+ ChiC_zz = 0.0 * T
569
+ ChiC_xx = 0.0 * T
570
+ ChiV_zz = 0.0 * T
571
+ ChiV_xx = 0.0 * T
572
+ for m in range(self.EV.size):
573
+ for n in range(self.EV.size):
574
+ if abs(self.EV[n] - self.EV[m]) < 0.00001 * kT:
575
+ ChiC_zz += self.Jz[m,n]**2 * np.exp(-self.EV[m] / kT)
576
+ ChiC_xx += 0.25 * (self.Jp[m,n]**2 + self.Jm[m,n]**2) \
577
+ * np.exp(-self.EV[m]/kT)
578
+ else:
579
+ ChiV_zz += 2 * self.Jz[m,n]**2 * np.exp(-self.EV[m] / kT) \
580
+ / (self.EV[n] - self.EV[m])
581
+ ChiV_xx += 0.5 * (self.Jp[m,n]**2 + self.Jm[m,n]**2) \
582
+ * np.exp(-self.EV[m]/kT)/(self.EV[n]-self.EV[m])
583
+ ChiC_zz = self.gJ**2 * ChiC_zz / (kT * Z)
584
+ ChiC_xx = self.gJ**2 * ChiC_xx / (kT * Z)
585
+ ChiV_zz = self.gJ**2 * ChiV_zz / Z
586
+ ChiV_xx = self.gJ**2 * ChiV_xx / Z
587
+
588
+ return ChiC_zz, ChiC_xx, ChiV_zz, ChiV_xx
589
+
590
+ def chis(self, Ts=None):
591
+ """
592
+ Calculate the susceptibility as a function of temperature.
593
+
594
+ Parameters
595
+ ----------
596
+ Ts : array
597
+ The temperatures at which to calculate the susceptibility.
598
+ If Ts is None, then the susceptibility is calculated at 300
599
+ temperatures spaced evenly between 1 and 300 K.
600
+
601
+ Returns
602
+ -------
603
+ ChiC_zz, ChiC_xx, ChiV_zz, ChiV_xx : array
604
+ The calculated susceptibility at each temperature, split
605
+ into the three components.
606
+ """
607
+ if Ts is None:
608
+ Ts = np.linspace(1.0, 300.0, 300, dtype=np.float32)
609
+
610
+ self.EFS()
611
+ self.TPS()
612
+
613
+ if Ts is None:
614
+ Ts = np.linspace(1.0, 300.0, 300, dtype=np.float32)
615
+
616
+ ChiC_zz = np.zeros(shape=Ts.shape, dtype=np.float32)
617
+ ChiC_xx = np.zeros(shape=Ts.shape, dtype=np.float32)
618
+ ChiV_zz = np.zeros(shape=Ts.shape, dtype=np.float32)
619
+ ChiV_xx = np.zeros(shape=Ts.shape, dtype=np.float32)
620
+
621
+ for i, T in enumerate(Ts):
622
+ ChiC_zz[i], ChiC_xx[i], ChiV_zz[i], ChiV_xx[i] = self.chi(T)
623
+
624
+ return ChiC_zz, ChiC_xx, ChiV_zz, ChiV_xx
625
+
626
+
627
+ def NXchi(self, Ts=None):
628
+ """
629
+ Return the susceptibility of the crystal field model.
630
+
631
+ Parameters
632
+ ----------
633
+ Ts : array
634
+ The temperatures at which to calculate the susceptibility.
635
+ If Ts is None, then the susceptibility is calculated at 300
636
+ temperatures spaced evenly between 1 and 300 K.
637
+
638
+ Returns
639
+ -------
640
+ entry : NXentry
641
+ A NeXus data structure containing the susceptibility of the
642
+ crystal field model.
643
+ """
644
+ from nexusformat.nexus import NXdata, NXentry, NXfield
645
+
646
+ if Ts is None:
647
+ Ts = np.linspace(1.0, 300.0, 300, dtype=np.float32)
648
+
649
+ entry = NXentry()
650
+ entry.title = "Susceptibility of %s" % self.name
651
+ temperature = NXfield(Ts, name="temperature")
652
+ temperature.units = "K"
653
+
654
+ ChiC_zz, ChiC_xx, ChiV_zz, ChiV_xx = self.chis(Ts)
655
+
656
+ chi = NXfield(ChiC_zz + ChiV_zz + 2*(ChiC_xx + ChiV_xx), name="chi")
657
+ invchi = NXfield(1/chi, name='invchi')
658
+ chiz = NXfield(ChiC_zz+ChiV_zz, name='chiz')
659
+ chix = NXfield(ChiC_xx+ChiV_xx, name='chix')
660
+
661
+ entry.chi = NXdata(chi, temperature)
662
+ entry.chi.title = "Susceptibility of %s" % self.name
663
+ entry.invchi = NXdata(invchi, temperature)
664
+ entry.invchi.title = "Inverse Susceptibility of %s" % self.name
665
+ entry.chiz = NXdata(chiz, temperature)
666
+ entry.chiz.title = "Susceptibility of %s (z-axis)" % self.name
667
+ entry.chix = NXdata(chix, temperature)
668
+ entry.chix.title = "Susceptibility of %s (x-axis)" % self.name
669
+
670
+ return entry
671
+
672
+ nxchi = NXchi
673
+
674
+ integral_factor = np.sqrt(2*np.pi)
675
+ sigma_factor = 2 * np.sqrt(2*np.log(2))
676
+
677
+ def gauss(x, center, sigma):
678
+ return np.exp(-(x-center)**2/(2*sigma**2)) / (sigma * integral_factor)
679
+
680
+ def lorentz(x, center, gamma):
681
+ return (gamma / np.pi) / ((x - center) ** 2 + gamma ** 2)
682
+
683
+ def pseudovoigt(x, center, sigma, gamma):
684
+ GammaG = sigma_factor * sigma
685
+ GammaL = 2 * gamma
686
+ FWHM = (GammaG**5 +
687
+ 2.69269 * GammaG**4 * GammaL +
688
+ 2.42843 * GammaG**3 * GammaL**2 +
689
+ 4.47163 * GammaG**2 + GammaL**3 +
690
+ 0.07842 * GammaG**4 * GammaL +
691
+ GammaL**5)**(0.2)
692
+ ratio = GammaL / FWHM
693
+ fraction = 1.36603 * ratio - 0.47719 * ratio**2 + 0.11116 * ratio**3
694
+ return ((1-fraction) * gauss(x, center, sigma) +
695
+ fraction * lorentz(x, center, gamma))
@@ -0,0 +1,6 @@
1
+ from . import define_model
2
+
3
+ def plugin_menu():
4
+ menu = 'CF'
5
+ actions = [('Set CF Parameters', define_model.show_dialog)]
6
+ return menu, actions
@@ -0,0 +1,123 @@
1
+ from nexpy.gui.dialogs import GridParameters, NXDialog
2
+ from nexpy.gui.pyqt import QtCore, QtGui
3
+ from nexpy.gui.utils import report_error
4
+ from nexusformat.nexus import NeXusError
5
+
6
+
7
+ def show_dialog(parent=None):
8
+ try:
9
+ dialog = DefineModelDialog()
10
+ dialog.show()
11
+ except NeXusError as error:
12
+ report_error("Defining CF Model", error)
13
+
14
+
15
+ class DefineModelDialog(NXDialog):
16
+
17
+ def __init__(self, parent=None):
18
+ super(DefineModelDialog, self).__init__(parent)
19
+
20
+ node = self.get_node()
21
+ self.root = node.nxroot
22
+
23
+ symmetries = ['cubic', 'tetragonal', 'orthorhombic', 'hexagonal',
24
+ 'monoclinic', 'triclinic']
25
+
26
+ self.rare_earths = ['Ce', 'Pr', 'Nd', 'Pm', 'Sm', 'Eu', 'Gd', 'Tb',
27
+ 'Dy', 'Ho', 'Er', 'Tm', 'Yb']
28
+
29
+ self.parameters = GridParameters()
30
+ self.parameters.add('symmetry', symmetries, 'Symmetry')
31
+
32
+ action_buttons = self.action_buttons(('Plot', self.plot_lattice),
33
+ ('Save', self.write_parameters))
34
+ self.set_layout(self.entry_layout, self.parameters.grid(),
35
+ action_buttons, self.close_buttons())
36
+ self.set_title('Defining CF Model')
37
+
38
+
39
+ def cf_grid(self):
40
+ parameters = []
41
+ if self.symmetry == 'cubic':
42
+ parameters
43
+
44
+ self.B20_box = QtGui.QLineEdit()
45
+ self.B22_box = QtGui.QLineEdit()
46
+ self.B40_box = QtGui.QLineEdit()
47
+ self.B42_box = QtGui.QLineEdit()
48
+ self.B43_box = QtGui.QLineEdit()
49
+ self.B44_box = QtGui.QLineEdit()
50
+ self.B60_box = QtGui.QLineEdit()
51
+ self.B62_box = QtGui.QLineEdit()
52
+ self.B63_box = QtGui.QLineEdit()
53
+ self.B64_box = QtGui.QLineEdit()
54
+ self.B66_box = QtGui.QLineEdit()
55
+ self.Hz_box = QtGui.QLineEdit()
56
+ self.Hx_box = QtGui.QLineEdit()
57
+ grid = self.parameters.grid()
58
+ grid.addWidget(QtGui.QLabel('B20:'), 0, 0)
59
+ grid.addWidget(QtGui.QLabel('B22:'), 0, 0)
60
+ grid.addWidget(QtGui.QLabel('Unit Cell - a (Ang):'), 1, 0)
61
+ grid.addWidget(QtGui.QLabel('Unit Cell - b (Ang):'), 2, 0)
62
+ grid.addWidget(QtGui.QLabel('Unit Cell - c (Ang):'), 3, 0)
63
+ grid.addWidget(QtGui.QLabel('Unit Cell - alpha (deg):'), 4, 0)
64
+ grid.addWidget(QtGui.QLabel('Unit Cell - beta (deg):'), 5, 0)
65
+ grid.addWidget(QtGui.QLabel('Unit Cell - gamma (deg):'), 6, 0)
66
+ grid.addWidget(QtGui.QLabel('Wavelength (Ang):'), 7, 0)
67
+ grid.addWidget(QtGui.QLabel('Distance (mm):'), 8, 0)
68
+ grid.addWidget(QtGui.QLabel('Yaw (deg):'), 9, 0)
69
+ grid.addWidget(QtGui.QLabel('Pitch (deg):'), 10, 0)
70
+ grid.addWidget(QtGui.QLabel('Roll (deg):'), 11, 0)
71
+
72
+
73
+ def update_parameter(self, box, value):
74
+ if value is not None:
75
+ box.setText(str(value))
76
+
77
+ def update_parameters(self):
78
+ self.update_parameter(self.unitcell_a_box, self.refine.a)
79
+ self.update_parameter(self.unitcell_b_box, self.refine.b)
80
+ self.update_parameter(self.unitcell_c_box, self.refine.c)
81
+ self.update_parameter(self.unitcell_alpha_box, self.refine.alpha)
82
+ self.update_parameter(self.unitcell_beta_box, self.refine.beta)
83
+ self.update_parameter(self.unitcell_gamma_box, self.refine.gamma)
84
+ self.update_parameter(self.wavelength_box, self.refine.wavelength)
85
+ self.update_parameter(self.distance_box, self.refine.distance)
86
+ self.update_parameter(self.yaw_box, self.refine.yaw)
87
+ self.update_parameter(self.pitch_box, self.refine.pitch)
88
+ self.update_parameter(self.roll_box, self.refine.roll)
89
+ self.update_parameter(self.xc_box, self.refine.xc)
90
+ self.update_parameter(self.yc_box, self.refine.yc)
91
+
92
+ @property
93
+ def symmetry(self):
94
+ return self.symmetry_box.currentText()
95
+
96
+ def set_symmetry(self):
97
+ self.refine.symmetry = self.get_symmetry()
98
+ self.refine.set_symmetry()
99
+ self.update_parameters()
100
+ if self.refine.symmetry == 'cubic':
101
+ self.unitcell_b_checkbox.setCheckState(QtCore.Qt.Unchecked)
102
+ self.unitcell_c_checkbox.setCheckState(QtCore.Qt.Unchecked)
103
+ self.unitcell_alpha_checkbox.setCheckState(QtCore.Qt.Unchecked)
104
+ self.unitcell_beta_checkbox.setCheckState(QtCore.Qt.Unchecked)
105
+ self.unitcell_gamma_checkbox.setCheckState(QtCore.Qt.Unchecked)
106
+ elif self.refine.symmetry == 'tetragonal':
107
+ self.unitcell_b_checkbox.setCheckState(QtCore.Qt.Unchecked)
108
+ self.unitcell_alpha_checkbox.setCheckState(QtCore.Qt.Unchecked)
109
+ self.unitcell_beta_checkbox.setCheckState(QtCore.Qt.Unchecked)
110
+ self.unitcell_gamma_checkbox.setCheckState(QtCore.Qt.Unchecked)
111
+ elif self.refine.symmetry == 'orthorhombic':
112
+ self.unitcell_alpha_checkbox.setCheckState(QtCore.Qt.Unchecked)
113
+ self.unitcell_beta_checkbox.setCheckState(QtCore.Qt.Unchecked)
114
+ self.unitcell_gamma_checkbox.setCheckState(QtCore.Qt.Unchecked)
115
+ elif self.refine.symmetry == 'hexagonal':
116
+ self.unitcell_b_checkbox.setCheckState(QtCore.Qt.Unchecked)
117
+ self.unitcell_alpha_checkbox.setCheckState(QtCore.Qt.Unchecked)
118
+ self.unitcell_beta_checkbox.setCheckState(QtCore.Qt.Unchecked)
119
+ self.unitcell_gamma_checkbox.setCheckState(QtCore.Qt.Unchecked)
120
+ elif self.refine.symmetry == 'monoclinic':
121
+ self.unitcell_alpha_checkbox.setCheckState(QtCore.Qt.Unchecked)
122
+ self.unitcell_gamma_checkbox.setCheckState(QtCore.Qt.Unchecked)
123
+
@@ -0,0 +1,105 @@
1
+ Metadata-Version: 2.4
2
+ Name: CFcal
3
+ Version: 0.1.6
4
+ Summary: Python calculator for crystal fields
5
+ Author-email: Raymond Osborn <rayosborn@mac.com>
6
+ License: Licensing Terms for NeXpy
7
+ -------------------------
8
+
9
+ NeXpy is licensed under the terms of the Modified BSD License (also known as
10
+ New or Revised BSD), as follows:
11
+
12
+ Copyright (c) 2017, Ray Osborn
13
+
14
+ All rights reserved.
15
+
16
+ Redistribution and use in source and binary forms, with or without
17
+ modification, are permitted provided that the following conditions are met:
18
+
19
+ Redistributions of source code must retain the above copyright notice, this
20
+ list of conditions and the following disclaimer.
21
+
22
+ Redistributions in binary form must reproduce the above copyright notice, this
23
+ list of conditions and the following disclaimer in the documentation and/or
24
+ other materials provided with the distribution.
25
+
26
+ Neither the name of the NeXpy Development Team nor the names of its
27
+ contributors may be used to endorse or promote products derived from this
28
+ software without specific prior written permission.
29
+
30
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
31
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
32
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
33
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
34
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
35
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
36
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
37
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
38
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
39
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
40
+
41
+ Copyright
42
+ ---------
43
+
44
+ The following banner should be used in any source code file to indicate the
45
+ copyright and license terms:
46
+
47
+ #-----------------------------------------------------------------------------
48
+ # Copyright (c) 2017, Ray Osborn.
49
+ #
50
+ # Distributed under the terms of the Modified BSD License.
51
+ #
52
+ # The full license is in the file COPYING, distributed with this software.
53
+ #-----------------------------------------------------------------------------
54
+
55
+ Project-URL: Homepage, https://github.com/rayosborn/cfcal
56
+ Project-URL: Repository, https://github.com/rayosborn/cfcal.git
57
+ Project-URL: Issues, https://github.com/rayosborn/cfcal/issues
58
+ Project-URL: Documentation, https://github.com/rayosborn/cfcal
59
+ Project-URL: Changelog, https://github.com/rayosborn/cfcal/releases
60
+ Keywords: neutron scattering,crystal fields,data analysis
61
+ Classifier: Development Status :: 4 - Beta
62
+ Classifier: License :: OSI Approved :: BSD License
63
+ Classifier: Intended Audience :: Science/Research
64
+ Classifier: Topic :: Scientific/Engineering
65
+ Classifier: Programming Language :: Python :: 3
66
+ Classifier: Programming Language :: Python :: 3.10
67
+ Classifier: Programming Language :: Python :: 3.11
68
+ Classifier: Programming Language :: Python :: 3.12
69
+ Classifier: Programming Language :: Python :: 3.13
70
+ Classifier: Programming Language :: Python :: 3.14
71
+ Requires-Python: >=3.10
72
+ Description-Content-Type: text/markdown
73
+ License-File: COPYING
74
+ Requires-Dist: numpy
75
+ Requires-Dist: scipy
76
+ Dynamic: license-file
77
+
78
+ Introduction
79
+ ============
80
+ CFcal is a Python package for performing calculations of crystal field (CF)
81
+ properties of rare earth ions using the Stevens Operator formalism [K. W. H.
82
+ Stevens, Proc. Phys. Soc. A **65**, 209 (1952)]. Once the CF parameters have
83
+ been initialized, the CF Hamiltonian can be diagonalized to determine the
84
+ energies and wavefunctions of all the CF levels. These can be used to determine
85
+ the magnetic susceptibility and neutron scattering spectra as a function of
86
+ temperature.
87
+
88
+ Installing and Running
89
+ ======================
90
+ CFcal requires Python 3.10 or later.
91
+
92
+ The easiest way to install CFcal is from PyPI:
93
+
94
+ ```
95
+ $ pip install cfcal
96
+ ```
97
+
98
+ Alternatively, the latest development version can be installed from the CFcal
99
+ [Git repository](https://github.com/rayosborn/cfcal):
100
+
101
+ ```
102
+ $ git clone https://github.com/rayosborn/cfcal.git
103
+ $ cd cfcal
104
+ $ pip install .
105
+ ```
@@ -0,0 +1,11 @@
1
+ cfcal/__init__.py,sha256=X20GzQ1bJ735ICDyR_YgkId_HEoTJ-LBCxxfEQr6uPI,45
2
+ cfcal/_version.py,sha256=mSVY638Y8lbMDXqQg68klFY9uyBAV8qKjFmkqikYzOU,520
3
+ cfcal/cfcal.py,sha256=b3fiiPAeir5HKaM4gptlcCDgss6b5s82lMiXRLoo66g,24225
4
+ cfcal/plugins/cfcal/__init__.py,sha256=debg6EjpJh-FxK8QTRj2p1Zx32cQLm9J_o2FoA8i5_A,152
5
+ cfcal/plugins/cfcal/define_model.py,sha256=M81K8Wa3ypGBO8aio0CxNn364ILfYK6OS6PTzyFF68Y,5611
6
+ cfcal-0.1.6.dist-info/licenses/COPYING,sha256=tLIBbKjIFCqBXVWr4KfC5ZW2LgpoIYc-8CPhZ982j2I,2102
7
+ cfcal-0.1.6.dist-info/METADATA,sha256=bswpUeAfzjtqLyDhteE4VYwmGMpTN4Q8fIONmx2dub8,4557
8
+ cfcal-0.1.6.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
9
+ cfcal-0.1.6.dist-info/entry_points.txt,sha256=R2VtGrLXlUtVANoLClaoCkonw5EFJAXlcCvqaueKr9E,53
10
+ cfcal-0.1.6.dist-info/top_level.txt,sha256=DfZXDVdEmcIWp6aYS7LW8Q89PZl8Llx47Z6NvV2NIGw,6
11
+ cfcal-0.1.6.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [nexpy.plugins]
2
+ CF = cfcal.plugins.cfcal:plugin_menu
@@ -0,0 +1,48 @@
1
+ Licensing Terms for NeXpy
2
+ -------------------------
3
+
4
+ NeXpy is licensed under the terms of the Modified BSD License (also known as
5
+ New or Revised BSD), as follows:
6
+
7
+ Copyright (c) 2017, Ray Osborn
8
+
9
+ All rights reserved.
10
+
11
+ Redistribution and use in source and binary forms, with or without
12
+ modification, are permitted provided that the following conditions are met:
13
+
14
+ Redistributions of source code must retain the above copyright notice, this
15
+ list of conditions and the following disclaimer.
16
+
17
+ Redistributions in binary form must reproduce the above copyright notice, this
18
+ list of conditions and the following disclaimer in the documentation and/or
19
+ other materials provided with the distribution.
20
+
21
+ Neither the name of the NeXpy Development Team nor the names of its
22
+ contributors may be used to endorse or promote products derived from this
23
+ software without specific prior written permission.
24
+
25
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
26
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
27
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
28
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
29
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
30
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
31
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
32
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
33
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
34
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
35
+
36
+ Copyright
37
+ ---------
38
+
39
+ The following banner should be used in any source code file to indicate the
40
+ copyright and license terms:
41
+
42
+ #-----------------------------------------------------------------------------
43
+ # Copyright (c) 2017, Ray Osborn.
44
+ #
45
+ # Distributed under the terms of the Modified BSD License.
46
+ #
47
+ # The full license is in the file COPYING, distributed with this software.
48
+ #-----------------------------------------------------------------------------
@@ -0,0 +1 @@
1
+ cfcal