PPMS_Toolkit 0.2.1__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.
- ppms_toolkit/__init__.py +1 -0
- ppms_toolkit/measurement/__init__.py +17 -0
- ppms_toolkit/measurement/base.py +74 -0
- ppms_toolkit/measurement/heat_capacity.py +166 -0
- ppms_toolkit/measurement/utils.py +101 -0
- ppms_toolkit/measurement/vsm.py +328 -0
- ppms_toolkit/sample.py +347 -0
- ppms_toolkit-0.2.1.dist-info/METADATA +296 -0
- ppms_toolkit-0.2.1.dist-info/RECORD +12 -0
- ppms_toolkit-0.2.1.dist-info/WHEEL +4 -0
- ppms_toolkit-0.2.1.dist-info/entry_points.txt +2 -0
- ppms_toolkit-0.2.1.dist-info/licenses/LICENSE +21 -0
ppms_toolkit/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# PPMS_Toolkit/__init__.py
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# __init__.py
|
|
2
|
+
|
|
3
|
+
# ruff: noqa
|
|
4
|
+
# pylint: disable=unused-import
|
|
5
|
+
|
|
6
|
+
from .base import Measurement
|
|
7
|
+
from .heat_capacity import HeatCapacityMeasurement
|
|
8
|
+
from .vsm import VSMMeasurement
|
|
9
|
+
from .utils import merge_by_temp_diff, debye_model, debye_model_extended,einstein_model
|
|
10
|
+
|
|
11
|
+
# When import with "from measurement import *",
|
|
12
|
+
# * includes the following thing.
|
|
13
|
+
# __all__ = ["Measurement",
|
|
14
|
+
# "HeatCapacityMeasurement",
|
|
15
|
+
# "merge_by_temp_diff",
|
|
16
|
+
# "debye_model",
|
|
17
|
+
# "debye_model_extended"]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'''
|
|
2
|
+
This Module defined the abstracted base class [Measurement]. It serves as
|
|
3
|
+
a backbone for its desecendent class, [HeatCapacityMeasurment],
|
|
4
|
+
[Magnetism Measurement], etc.
|
|
5
|
+
'''
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from typing import TYPE_CHECKING, Optional
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from ppms_toolkit.sample import Sample # Avoid Cylic-Import
|
|
10
|
+
|
|
11
|
+
import pandas as pd
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Measurement(ABC):
|
|
15
|
+
def __init__(self,
|
|
16
|
+
sample: "Sample",
|
|
17
|
+
comment: str = '',
|
|
18
|
+
metadata: Optional[dict] = None,
|
|
19
|
+
filepath: str | None = None,
|
|
20
|
+
raw_dataframe: pd.DataFrame | None = None, # 允许直接传入
|
|
21
|
+
processed_dataframe: pd.DataFrame | None = None):
|
|
22
|
+
self.filepath = filepath
|
|
23
|
+
self.sample = sample
|
|
24
|
+
self.comment = comment
|
|
25
|
+
self.metadata = metadata or {}
|
|
26
|
+
|
|
27
|
+
# 如果传入了 DataFrame,直接使用
|
|
28
|
+
if processed_dataframe is not None and raw_dataframe is not None:
|
|
29
|
+
self.raw_dataframe = raw_dataframe
|
|
30
|
+
self.dataframe = processed_dataframe
|
|
31
|
+
# 否则从文件加载
|
|
32
|
+
elif filepath is not None:
|
|
33
|
+
self.raw_dataframe, self.dataframe = self.load_data()
|
|
34
|
+
else:
|
|
35
|
+
raise ValueError("Must provide either 'filepath' or 'raw_dataframe'")
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def sample_name(self):
|
|
39
|
+
return self.sample.name if self.sample else "Unknown Sample"
|
|
40
|
+
|
|
41
|
+
def load_data(self):
|
|
42
|
+
assert self.filepath is not None
|
|
43
|
+
try:
|
|
44
|
+
with open(file=self.filepath, encoding='utf-8', errors="strict") as f:
|
|
45
|
+
content = f.readlines()
|
|
46
|
+
except UnicodeDecodeError:
|
|
47
|
+
with open(file=self.filepath, encoding='iso-8859-1') as f:
|
|
48
|
+
content = f.readlines()
|
|
49
|
+
|
|
50
|
+
# Data start after the Line [Data].
|
|
51
|
+
data_start_line = content.index('[Data]\n') + 1
|
|
52
|
+
data = content[data_start_line:]
|
|
53
|
+
splitted_data = [line.split(',') for line in data]
|
|
54
|
+
|
|
55
|
+
raw_df = pd.DataFrame(data=splitted_data[1:], columns=splitted_data[0])
|
|
56
|
+
|
|
57
|
+
df = self.process_data(raw_df, self.filepath)
|
|
58
|
+
|
|
59
|
+
return raw_df, df
|
|
60
|
+
|
|
61
|
+
'''
|
|
62
|
+
# Depracated function , Now Filepath is optional and unique check is depend on the sqlite
|
|
63
|
+
|
|
64
|
+
def __eq__(self, other):
|
|
65
|
+
from pathlib import Path
|
|
66
|
+
if not isinstance(other, Measurement):
|
|
67
|
+
return False
|
|
68
|
+
return Path(self.filepath).resolve() == Path(other.filepath).resolve()
|
|
69
|
+
'''
|
|
70
|
+
|
|
71
|
+
@abstractmethod
|
|
72
|
+
def process_data(self, raw_df, filepath) -> pd.DataFrame:
|
|
73
|
+
'''This Method have to be defined in the desendent class.'''
|
|
74
|
+
pass
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
'''
|
|
2
|
+
This module define the [HeatCapacityMeasurment] instance,
|
|
3
|
+
which is a descendant of [Measurement].
|
|
4
|
+
|
|
5
|
+
Heat Capcity's experiment condition:
|
|
6
|
+
[field_strength]
|
|
7
|
+
'''
|
|
8
|
+
|
|
9
|
+
from .base import Measurement
|
|
10
|
+
from typing import TYPE_CHECKING, Optional
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from ppms_toolkit.sample import Sample # Avoid Cylic-Import
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
import matplotlib.pyplot as plt
|
|
16
|
+
import numpy as np
|
|
17
|
+
import pandas as pd
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
from .utils import merge_by_temp_diff, debye_model_extended, debye_model
|
|
21
|
+
|
|
22
|
+
plt.rcParams['axes.grid'] = True # Would like every plot to have grid
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class HeatCapacityMeasurement(Measurement):
|
|
26
|
+
def __init__(self,
|
|
27
|
+
filepath: str,
|
|
28
|
+
sample: "Sample",
|
|
29
|
+
sample_mass: float = 1,
|
|
30
|
+
field_strength: float = 0,
|
|
31
|
+
comment: str = "",
|
|
32
|
+
metadata=None
|
|
33
|
+
):
|
|
34
|
+
self.field_strength = field_strength
|
|
35
|
+
self.sample_mass = sample_mass
|
|
36
|
+
super().__init__(
|
|
37
|
+
filepath=filepath,
|
|
38
|
+
sample=sample,
|
|
39
|
+
comment= comment,
|
|
40
|
+
metadata= metadata)
|
|
41
|
+
if sample: # If sample is inputted, add this mesurement in the sample.
|
|
42
|
+
sample.add_measurement(self)
|
|
43
|
+
|
|
44
|
+
def __repr__(self):
|
|
45
|
+
return (f'HC exp, {self.sample_name} '
|
|
46
|
+
f'with {self.field_strength} '
|
|
47
|
+
f'Oe {self.comment}'
|
|
48
|
+
f'T-range {self.t_range}'
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def to_dict(self):
|
|
52
|
+
'''Return a dict for representation purpose'''
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
"External field": getattr(self, "field_strength", None),
|
|
56
|
+
"Temp Range": getattr(self, "t_range", None),
|
|
57
|
+
"instance": self
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
def process_data(self, raw_df, filepath) -> pd.DataFrame:
|
|
61
|
+
import re
|
|
62
|
+
# Remove the comment lines.
|
|
63
|
+
df = raw_df[raw_df['Comment ()'] == '']
|
|
64
|
+
|
|
65
|
+
# Remove the error Code, fix the corrupted error code.
|
|
66
|
+
# Keep columns as an Index so we can access the `.str` accessor.
|
|
67
|
+
df.columns = df.columns.astype(str).str.replace('�', 'µ', regex=False)
|
|
68
|
+
# Convert all str into Floats
|
|
69
|
+
|
|
70
|
+
pattern = ( r'Time Stamp \(|'
|
|
71
|
+
r'Samp HC \(|'
|
|
72
|
+
r'Samp HC\/Temp \(|'
|
|
73
|
+
r'Sample Temp \(|'
|
|
74
|
+
r'Samp HC Err \(|'
|
|
75
|
+
r'Field \('
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# Use Index.str.contains directly on the columns Index.
|
|
80
|
+
keys_to_keep = df.columns[df.columns.str.contains(pattern)]
|
|
81
|
+
df = df[keys_to_keep]
|
|
82
|
+
df = df.apply(pd.to_numeric, errors='coerce')
|
|
83
|
+
|
|
84
|
+
df = merge_by_temp_diff(df=df,
|
|
85
|
+
temp_col='Sample Temp (Kelvin)',
|
|
86
|
+
tol=0.01)
|
|
87
|
+
|
|
88
|
+
# Divide the Moment by sample_mass, if there's none 1 sample mass.
|
|
89
|
+
pattern = re.compile(r'^(Samp HC \(|Samp HC/Temp \(|Samp HC Err \()')
|
|
90
|
+
for col in df.columns:
|
|
91
|
+
if pattern.match(col):
|
|
92
|
+
df[col] = df[col] / self.sample_mass
|
|
93
|
+
|
|
94
|
+
# Get the range of T
|
|
95
|
+
minimum = df['Sample Temp (Kelvin)'].min()
|
|
96
|
+
maximum = df['Sample Temp (Kelvin)'].max()
|
|
97
|
+
self.t_range = f'{minimum:.1f} ~ {maximum:.1f}'
|
|
98
|
+
|
|
99
|
+
return df
|
|
100
|
+
|
|
101
|
+
def plot(self, ax=None):
|
|
102
|
+
'''Create a standard plot of Heat Capacity Measurement'''
|
|
103
|
+
if ax is None:
|
|
104
|
+
fig, ax = plt.subplots(figsize=(6, 4))
|
|
105
|
+
|
|
106
|
+
cols = self.dataframe.columns
|
|
107
|
+
sample_T = cols[cols.str.contains(r'Sample Temp \(')][0]
|
|
108
|
+
sample_HC = cols[cols.str.contains(r'Samp HC \(')][0]
|
|
109
|
+
|
|
110
|
+
# The first graph is a Samp HC v.s. T
|
|
111
|
+
ax.plot(self.dataframe[sample_T],
|
|
112
|
+
self.dataframe[sample_HC],
|
|
113
|
+
label=f'{self.field_strength} Oe',)
|
|
114
|
+
ax.set_xlabel(sample_T)
|
|
115
|
+
ax.set_ylabel(sample_HC)
|
|
116
|
+
ax.legend()
|
|
117
|
+
|
|
118
|
+
return ax
|
|
119
|
+
|
|
120
|
+
def background_subtraction(self,
|
|
121
|
+
mask_func=lambda T: T > 0,
|
|
122
|
+
model=debye_model, bounds=None, p0=None):
|
|
123
|
+
from scipy.optimize import curve_fit
|
|
124
|
+
|
|
125
|
+
T = self.dataframe['Sample Temp (Kelvin)']
|
|
126
|
+
HC = self.dataframe['Samp HC (µJ/K)']
|
|
127
|
+
|
|
128
|
+
mask = mask_func(T)
|
|
129
|
+
T_fit = T[mask]
|
|
130
|
+
HC_fit = HC[mask]
|
|
131
|
+
|
|
132
|
+
kwargs = {}
|
|
133
|
+
if bounds is not None:
|
|
134
|
+
kwargs['bounds'] = bounds
|
|
135
|
+
if p0 is not None:
|
|
136
|
+
kwargs['p0'] = p0
|
|
137
|
+
|
|
138
|
+
params, _ = curve_fit(model, T_fit, HC_fit, **kwargs)
|
|
139
|
+
phonon_background = model(T, *params)
|
|
140
|
+
subtracted = HC - phonon_background
|
|
141
|
+
|
|
142
|
+
fig, ax = plt.subplots()
|
|
143
|
+
|
|
144
|
+
ax.plot(T, HC, label='Heat Capacity')
|
|
145
|
+
ax.plot(T, phonon_background, label='Phonon Background')
|
|
146
|
+
ax.plot(T, subtracted, label='Subtracted.')
|
|
147
|
+
plt.title(f'{self.sample_name} Phonon Background Subtraction')
|
|
148
|
+
plt.legend()
|
|
149
|
+
|
|
150
|
+
return fig, ax, params, T, subtracted
|
|
151
|
+
|
|
152
|
+
def background_subtraction_debye(
|
|
153
|
+
self, mask_func=lambda T: (T > 5) & (T < 300),
|
|
154
|
+
bounds=([1, 0.0001],
|
|
155
|
+
[500, 100])
|
|
156
|
+
):
|
|
157
|
+
return self.background_subtraction(
|
|
158
|
+
mask_func, debye_model, bounds)
|
|
159
|
+
|
|
160
|
+
def background_subtraction_debye_extended(
|
|
161
|
+
self, mask_func=lambda T: (T > 5) & (T < 300),
|
|
162
|
+
bounds=([1, 0.0001, -np.inf, -np.inf],
|
|
163
|
+
[500, 100, np.inf, np.inf])
|
|
164
|
+
):
|
|
165
|
+
return self.background_subtraction(
|
|
166
|
+
mask_func, debye_model_extended, bounds)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'''
|
|
2
|
+
This module provide some useful util functions
|
|
3
|
+
that might be used in general purpose
|
|
4
|
+
'''
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import numpy as np
|
|
7
|
+
from scipy.integrate import quad
|
|
8
|
+
|
|
9
|
+
def merge_by_temp_diff(df,
|
|
10
|
+
temp_col='Sample Temp (Kelvin)',
|
|
11
|
+
tol=0.01):
|
|
12
|
+
|
|
13
|
+
df_sorted = df.sort_values(by=temp_col).reset_index(drop=True)
|
|
14
|
+
|
|
15
|
+
temp_diffs = df_sorted[temp_col].diff().abs()
|
|
16
|
+
|
|
17
|
+
group_ids = (temp_diffs > tol).cumsum()
|
|
18
|
+
|
|
19
|
+
merged_df = df_sorted.groupby(group_ids).mean(numeric_only=True)
|
|
20
|
+
|
|
21
|
+
merged_df = merged_df.reset_index(drop=True)
|
|
22
|
+
|
|
23
|
+
return merged_df
|
|
24
|
+
|
|
25
|
+
'''
|
|
26
|
+
df_sorted = df.sort_values(by=temp_col).reset_index(drop=True)
|
|
27
|
+
|
|
28
|
+
groups = []
|
|
29
|
+
current_group = [0]
|
|
30
|
+
|
|
31
|
+
for i in range(1, len(df_sorted)):
|
|
32
|
+
t_curr = df_sorted.loc[i, temp_col]
|
|
33
|
+
t_prev = df_sorted.loc[current_group[-1], temp_col]
|
|
34
|
+
if abs(t_curr - t_prev) < tol:
|
|
35
|
+
current_group.append(i)
|
|
36
|
+
else:
|
|
37
|
+
groups.append(current_group)
|
|
38
|
+
current_group = [i]
|
|
39
|
+
groups.append(current_group)
|
|
40
|
+
|
|
41
|
+
merged_rows = []
|
|
42
|
+
for group in groups:
|
|
43
|
+
averaged = df_sorted.loc[group].mean(numeric_only=True)
|
|
44
|
+
merged_rows.append(averaged)
|
|
45
|
+
|
|
46
|
+
'''
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# Debye Model for Phonon
|
|
50
|
+
R = 8.314 # J/(mol·K)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def debye_integral(x):
|
|
54
|
+
x = np.clip(x, 1e-10, 500)
|
|
55
|
+
ex = np.exp(x)
|
|
56
|
+
denom = (ex - 1)
|
|
57
|
+
return np.where(denom == 0, 0, x**4 * ex / denom**2)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def debye_model(T, theta_D, scale=1.0):
|
|
61
|
+
T = np.array(T)
|
|
62
|
+
C = []
|
|
63
|
+
|
|
64
|
+
for t in T:
|
|
65
|
+
if t == 0:
|
|
66
|
+
C.append(0.0)
|
|
67
|
+
else:
|
|
68
|
+
x_max = min(500, theta_D / t)
|
|
69
|
+
integral, _ = quad(debye_integral, 0, x_max)
|
|
70
|
+
c = 9 * R * (t / theta_D)**3 * integral * scale
|
|
71
|
+
C.append(c)
|
|
72
|
+
|
|
73
|
+
return np.array(C)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def einstein_model(T, theta_E, scale=1.0):
|
|
77
|
+
x = theta_E / T
|
|
78
|
+
return 3 * R * (x**2) * np.exp(x) / (np.exp(x) - 1)**2 * scale
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def debye_model_extended(T, theta_D, B, D, scale=1.0):
|
|
82
|
+
T = np.array(T)
|
|
83
|
+
C = []
|
|
84
|
+
|
|
85
|
+
for t in T:
|
|
86
|
+
if t == 0:
|
|
87
|
+
C.append(0.0)
|
|
88
|
+
else:
|
|
89
|
+
integral, _ = quad(debye_integral, 0, theta_D / t)
|
|
90
|
+
c = 9 * R * (t / theta_D)**3 * integral * scale
|
|
91
|
+
C.append(c)
|
|
92
|
+
|
|
93
|
+
return np.array(C) + B * T + D
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def gauss_with_step(x, A, x0, sigma, cL, cR):
|
|
97
|
+
step = np.where(x < x0, cL, cR)
|
|
98
|
+
return A * np.exp(-((x - x0)**2) / (2 * sigma**2)) + step
|
|
99
|
+
|
|
100
|
+
def gauss(x, A, x0, sigma, c):
|
|
101
|
+
return A * np.exp(-(x-x0)**2/(2*sigma**2)) + c
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
'''
|
|
2
|
+
This module define the [VSM_Measurement] instance,
|
|
3
|
+
which is a descendant of [Measurement].
|
|
4
|
+
|
|
5
|
+
VSM experiment condition:
|
|
6
|
+
[Sample Orientation]
|
|
7
|
+
'''
|
|
8
|
+
|
|
9
|
+
from .base import Measurement
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import pandas as pd
|
|
14
|
+
import matplotlib.pyplot as plt
|
|
15
|
+
from scipy.signal import savgol_filter, detrend
|
|
16
|
+
from scipy.optimize import curve_fit
|
|
17
|
+
from scipy.interpolate import interp1d #,PchipInterpolator,UnivariateSpline
|
|
18
|
+
|
|
19
|
+
from .utils import gauss
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from ppms_toolkit.sample import Sample # Avoid Cylic-Import
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class VSMMeasurement(Measurement):
|
|
26
|
+
def __init__(self,
|
|
27
|
+
mode: str,
|
|
28
|
+
sample: 'Sample',
|
|
29
|
+
filepath: str | None = None,
|
|
30
|
+
raw_dataframe: pd.DataFrame | None = None, # 新增
|
|
31
|
+
processed_dataframe: pd.DataFrame | None = None, # 新增
|
|
32
|
+
comment: str = "",
|
|
33
|
+
metadata: dict | None =None
|
|
34
|
+
):
|
|
35
|
+
self.mode = mode
|
|
36
|
+
self.const_temp = None
|
|
37
|
+
self.const_field = None
|
|
38
|
+
self.condition = None
|
|
39
|
+
super().__init__(
|
|
40
|
+
filepath=filepath,
|
|
41
|
+
sample=sample,
|
|
42
|
+
comment=comment,
|
|
43
|
+
metadata=metadata,
|
|
44
|
+
raw_dataframe=raw_dataframe,
|
|
45
|
+
processed_dataframe=processed_dataframe)
|
|
46
|
+
|
|
47
|
+
if metadata:
|
|
48
|
+
self.const_field = metadata.get('const_field')
|
|
49
|
+
self.const_temp = metadata.get('const_temp') or metadata.get('const_temperature')
|
|
50
|
+
self.condition = metadata.get('condition')
|
|
51
|
+
|
|
52
|
+
sample.add_measurement(self)
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def mode(self):
|
|
56
|
+
return self._mode
|
|
57
|
+
|
|
58
|
+
@mode.setter
|
|
59
|
+
def mode(self, value):
|
|
60
|
+
if value not in ("MH", "MT"):
|
|
61
|
+
raise ValueError("Mode can only be 'MH' or 'MT'")
|
|
62
|
+
self._mode = value
|
|
63
|
+
|
|
64
|
+
def to_dict(self):
|
|
65
|
+
'''Return a dict for representation purpose'''
|
|
66
|
+
return {
|
|
67
|
+
"mode": getattr(self, "mode", None),
|
|
68
|
+
#"orientation":getattr(self, "sample_orientation", None),
|
|
69
|
+
"const temp": getattr(self, "const_temp", None),
|
|
70
|
+
"const field": getattr(self, "const_field", None),
|
|
71
|
+
"instance": self
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
def process_data(self, raw_df, filepath) -> pd.DataFrame:
|
|
75
|
+
import re
|
|
76
|
+
|
|
77
|
+
pattern = (
|
|
78
|
+
r'^Temperature \(|'
|
|
79
|
+
r'Magnetic Field \(|'
|
|
80
|
+
r'Moment \(|'
|
|
81
|
+
r'M. Std. Err. \('
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
cols = raw_df.columns
|
|
85
|
+
keys_to_keep = cols[cols.str.contains(pattern)]
|
|
86
|
+
df = raw_df[keys_to_keep].apply(pd.to_numeric, errors='coerce')
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# Divide the Moment by, if there's none 1 sample mass.
|
|
90
|
+
for col in df.columns:
|
|
91
|
+
if re.match(r'Moment',col):
|
|
92
|
+
df[col] = df[col] / self.sample.mass
|
|
93
|
+
|
|
94
|
+
if self.mode == 'MT':
|
|
95
|
+
self.const_field = np.average(df.filter(regex='Magnetic Field')).round().astype(int)
|
|
96
|
+
for col in df.columns:
|
|
97
|
+
if re.match(r'Moment',col):
|
|
98
|
+
df['chi'] = df[col] / self.const_field
|
|
99
|
+
if re.search(r'ZFC', filepath):
|
|
100
|
+
self.condition = 'ZFC'
|
|
101
|
+
elif re.search(r'FC', filepath):
|
|
102
|
+
self.condition = 'FC'
|
|
103
|
+
else:
|
|
104
|
+
self.condition = 'Unknown Condition'
|
|
105
|
+
|
|
106
|
+
else:
|
|
107
|
+
self.const_temp = np.average(df.filter(regex='^Temperature')).round(1)
|
|
108
|
+
|
|
109
|
+
return df
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def __repr__(self):
|
|
113
|
+
if self.mode == 'MT':
|
|
114
|
+
return (f'{self.mode} on {self.sample_name} '
|
|
115
|
+
f'with {self.sample.orientation or "Unknown"} orientation '
|
|
116
|
+
f'at {self.const_field}Oe')
|
|
117
|
+
else:
|
|
118
|
+
return (f'{self.mode} on {self.sample_name} '
|
|
119
|
+
f'with {self.sample.orientation or "Unknown"} orientation '
|
|
120
|
+
f'at {self.const_temp}K')
|
|
121
|
+
|
|
122
|
+
def plot(self, mid=None, ax=None, susceptibility=True, legend: str|list = 'Exp Setting' ):
|
|
123
|
+
|
|
124
|
+
if ax is None:
|
|
125
|
+
fig, ax = plt.subplots()
|
|
126
|
+
|
|
127
|
+
label: str|None = None
|
|
128
|
+
if legend == 'Exp Setting':
|
|
129
|
+
if self.mode == 'MH':
|
|
130
|
+
label = f'{mid if mid else ""} {self.const_temp:.0f}K'
|
|
131
|
+
elif self.mode == "MT":
|
|
132
|
+
label = f'{self.const_field}Oe {self.condition} {self.sample.orientation or "Unknown Ori"} {mid if mid else ""}'
|
|
133
|
+
elif legend == 'Sample Name':
|
|
134
|
+
label = f'{self.sample.name}'
|
|
135
|
+
elif type(legend) is list:
|
|
136
|
+
label = " ".join(str(getattr(self, l)) for l in legend)
|
|
137
|
+
else:
|
|
138
|
+
raise ValueError("Legend Mode is not set")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
df = self.dataframe
|
|
142
|
+
|
|
143
|
+
regex = '^Moment'
|
|
144
|
+
ax.set_ylabel('Moment (emu / gram)')
|
|
145
|
+
if self.mode == 'MT':
|
|
146
|
+
if susceptibility:
|
|
147
|
+
regex = 'chi'
|
|
148
|
+
ax.set_ylabel('Susceptibility (emu)')
|
|
149
|
+
ax.plot(df.filter(regex='^Temperature'), df.filter(regex=regex).squeeze(), label = label)
|
|
150
|
+
ax.set_xlabel('Temperature(K)')
|
|
151
|
+
elif self.mode == 'MH':
|
|
152
|
+
ax.plot(df.filter(regex='Magnetic Field'), df.filter(regex=regex).squeeze(), label = label)
|
|
153
|
+
ax.set_xlabel('Magnetic Field(Oe)')
|
|
154
|
+
else:
|
|
155
|
+
print('Ah oh, something went wrong. Check if measurement.mode is "MH" or "MT".')
|
|
156
|
+
|
|
157
|
+
#ax.set_ylabel('Susceptibility (emu)')
|
|
158
|
+
ax.set_title(f'{self.sample.name} {self.mode} {self.sample.orientation if self.sample.orientation else ""}')
|
|
159
|
+
ax.legend()
|
|
160
|
+
|
|
161
|
+
return ax
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def fit_MH(self, fit_window = [0,70000], window_length=101, p0=None):
|
|
165
|
+
"""
|
|
166
|
+
from M(H) curve, do dM/dH gussian fit, to extract:
|
|
167
|
+
- x0_fit: Peak Position (Oe)
|
|
168
|
+
- FWHM : Full Width Half Maximum (Oe)
|
|
169
|
+
also return fig, ax
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
fig, ax = plt.subplots(1,2, figsize=(16,5))
|
|
173
|
+
|
|
174
|
+
df = self.dataframe
|
|
175
|
+
|
|
176
|
+
field_col = "Magnetic Field (Oe)"
|
|
177
|
+
moment_col = "Moment (emu)"
|
|
178
|
+
|
|
179
|
+
mask_field = (df[field_col] > fit_window[0]) & (df[field_col] < fit_window[1])
|
|
180
|
+
this_df = df.loc[mask_field].copy()
|
|
181
|
+
|
|
182
|
+
if this_df.empty:
|
|
183
|
+
raise ValueError("fit_MH: No datapoint in desired fit_window")
|
|
184
|
+
|
|
185
|
+
# Transform into Numpy Array
|
|
186
|
+
ExtField = this_df[field_col].to_numpy()
|
|
187
|
+
Moment = this_df[moment_col].to_numpy()
|
|
188
|
+
|
|
189
|
+
dMdH = np.gradient(Moment, ExtField)
|
|
190
|
+
|
|
191
|
+
dMdH_detrended = detrend(dMdH)
|
|
192
|
+
base_y = dMdH - dMdH_detrended
|
|
193
|
+
|
|
194
|
+
if window_length >= len(dMdH_detrended):
|
|
195
|
+
# 选一个最大可能的奇数窗
|
|
196
|
+
window_length = max(3, len(dMdH_detrended) // 2 * 2 + 1)
|
|
197
|
+
dMdH_detrended_filtered = savgol_filter(dMdH_detrended, window_length, polyorder=3)
|
|
198
|
+
|
|
199
|
+
mask = (dMdH > 0)
|
|
200
|
+
x_fit = ExtField[mask]
|
|
201
|
+
y_fit = dMdH_detrended_filtered[mask]
|
|
202
|
+
|
|
203
|
+
# 3) 给出初始猜测 p0
|
|
204
|
+
if p0 is None:
|
|
205
|
+
A0 = y_fit.max() - y_fit.min() # 峰高
|
|
206
|
+
max_idx = int(np.argmax(y_fit))
|
|
207
|
+
x00 = float(x_fit[max_idx]) # 峰位
|
|
208
|
+
sigma0 = (x_fit.max() - x_fit.min())/6 # 大致半宽
|
|
209
|
+
c0 = y_fit.min() # 底线
|
|
210
|
+
p0 = [A0, x00, sigma0, c0]
|
|
211
|
+
|
|
212
|
+
# 4) 拟合
|
|
213
|
+
popt, pcov = curve_fit(gauss, x_fit, y_fit, p0=p0)
|
|
214
|
+
A_fit, x0_fit, sigma_fit, c_fit = popt
|
|
215
|
+
sigma_fit = abs(sigma_fit)
|
|
216
|
+
FWHM = 2 * np.sqrt(2 * np.log(2)) * sigma_fit
|
|
217
|
+
|
|
218
|
+
ax[0].scatter(ExtField, dMdH_detrended_filtered, s=1, label= 'Detrended Filtered Data')
|
|
219
|
+
ax[0].plot(ExtField, gauss(ExtField, *popt), '-', lw=2, label='Guassian fit',color='g')
|
|
220
|
+
ax[0].axvline(x0_fit, color='r', ls='--', label=f'Peak @ {x0_fit:.0f} Oe, FWHM = {FWHM}')
|
|
221
|
+
ax[0].legend()
|
|
222
|
+
|
|
223
|
+
ax[1].scatter(ExtField, dMdH, s=1, label = 'OG Data')
|
|
224
|
+
ax[1].plot(ExtField, gauss(ExtField, *popt)+base_y, '-', lw=2, label='Guassian fit',color='r')
|
|
225
|
+
ax[1].legend()
|
|
226
|
+
|
|
227
|
+
fig.suptitle(f'dMdH, T = {self.const_temp}')
|
|
228
|
+
|
|
229
|
+
return fig, ax, x0_fit, FWHM
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def fit_MT(self,
|
|
234
|
+
fit_func,
|
|
235
|
+
fit_window:list,
|
|
236
|
+
p0=None,
|
|
237
|
+
detrended:bool = True,
|
|
238
|
+
smooth_window:int = 101,
|
|
239
|
+
deriv_smooth_window:int = 31
|
|
240
|
+
):
|
|
241
|
+
|
|
242
|
+
field = self.const_field
|
|
243
|
+
this_df = self.dataframe
|
|
244
|
+
|
|
245
|
+
chi = this_df['chi']
|
|
246
|
+
Temp = this_df['Temperature (K)']
|
|
247
|
+
|
|
248
|
+
# --- 1. 等间距插值 & 平滑 χ(T) -----------------------------------
|
|
249
|
+
f_interp = interp1d(
|
|
250
|
+
Temp,
|
|
251
|
+
chi,
|
|
252
|
+
kind="linear",
|
|
253
|
+
bounds_error=False,
|
|
254
|
+
fill_value="extrapolate")# type: ignore[arg-type]
|
|
255
|
+
|
|
256
|
+
# 生成等间距 T_new
|
|
257
|
+
T_new = np.linspace(np.min(Temp), np.max(Temp), 2000)
|
|
258
|
+
chi_resampled = f_interp(T_new)
|
|
259
|
+
|
|
260
|
+
# 窗口必须是奇数且 < 数据长度
|
|
261
|
+
smooth_window = min(smooth_window, len(T_new) // 2 * 2 + 1)
|
|
262
|
+
chi_smooth = savgol_filter(chi_resampled, window_length=smooth_window, polyorder=3)
|
|
263
|
+
|
|
264
|
+
# Take gradient
|
|
265
|
+
dchidT = np.gradient(chi_smooth, T_new)
|
|
266
|
+
|
|
267
|
+
mask = np.isfinite(dchidT)
|
|
268
|
+
dchidT_clean = dchidT[mask]
|
|
269
|
+
T_new_clean = T_new[mask]
|
|
270
|
+
|
|
271
|
+
# --- 3. 去趋势 & 再平滑 -------------------------------------------
|
|
272
|
+
if detrended:
|
|
273
|
+
dchidT_trend_removed = detrend(dchidT_clean)
|
|
274
|
+
else:
|
|
275
|
+
dchidT_trend_removed = dchidT_clean.copy()
|
|
276
|
+
|
|
277
|
+
base_y = dchidT_clean - dchidT_trend_removed
|
|
278
|
+
deriv_smooth_window = min(deriv_smooth_window,
|
|
279
|
+
len(dchidT_trend_removed) // 2 * 2 + 1)
|
|
280
|
+
dchidT_detrended_filtered = savgol_filter(dchidT_trend_removed, deriv_smooth_window, 3)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
left, right = fit_window
|
|
284
|
+
mask = (T_new_clean > left) & (T_new_clean<right)
|
|
285
|
+
x_fit = T_new_clean[mask]
|
|
286
|
+
y_fit = dchidT_detrended_filtered[mask]
|
|
287
|
+
|
|
288
|
+
# --- 5. 初始猜测 p0(如果未给) -----------------------------------
|
|
289
|
+
if p0 is None:
|
|
290
|
+
A0 = float(y_fit.max() - y_fit.min())
|
|
291
|
+
# 粗略峰位:拟合区内最大值
|
|
292
|
+
x00 = float(x_fit[np.argmax(y_fit)])
|
|
293
|
+
sigma0 = float((x_fit.max() - x_fit.min()) / 6)
|
|
294
|
+
c0 = float(y_fit.min())
|
|
295
|
+
p0 = [A0, x00, sigma0, c0]
|
|
296
|
+
|
|
297
|
+
# 4) 拟合
|
|
298
|
+
popt, pcov = curve_fit(fit_func, x_fit, y_fit, p0=p0)
|
|
299
|
+
|
|
300
|
+
A_fit, x0_fit, sigma_fit, c_fit = popt
|
|
301
|
+
|
|
302
|
+
FWHM = 2 * np.sqrt(2 * np.log(2)) * sigma_fit
|
|
303
|
+
|
|
304
|
+
# 5) 作图验证
|
|
305
|
+
fig, ax = plt.subplots(1,2,figsize=(12,5))
|
|
306
|
+
|
|
307
|
+
ax[0].plot(T_new_clean, dchidT_detrended_filtered, '.', ms=3, alpha=0.5, label='data')
|
|
308
|
+
ax[0].plot(x_fit, y_fit, '.', ms=5, label='fit region')
|
|
309
|
+
ax[0].plot(T_new_clean, fit_func(T_new_clean, *popt), '-', lw=2, label='Guassian fit')
|
|
310
|
+
ax[0].axvline(x0_fit, color='r', ls='--', label=f'Peak @ {x0_fit:.2f} T, FWHM = {FWHM:.3f}')
|
|
311
|
+
ax[0].set_title(f'Field = {field} Oe')
|
|
312
|
+
ax[0].set_xlabel('Temperature(K)')
|
|
313
|
+
ax[0].set_ylabel('dChidT (emu/K)')
|
|
314
|
+
ax[0].legend()
|
|
315
|
+
|
|
316
|
+
ax[1].scatter(T_new_clean, dchidT_clean,s=1, label='original')
|
|
317
|
+
ax[1].plot(T_new_clean, base_y, label='linear background')
|
|
318
|
+
ax[1].scatter(T_new_clean,dchidT_trend_removed, label='Detrended',s=1)
|
|
319
|
+
ax[1].scatter(T_new_clean,dchidT_detrended_filtered, label='Detrended+Filtered',s=1)
|
|
320
|
+
ax[1].set_xlabel('Temperature (K)')
|
|
321
|
+
ax[1].set_ylabel('dchi/dT (emu/K)')
|
|
322
|
+
ax[1].legend()
|
|
323
|
+
|
|
324
|
+
plt.grid(True, linestyle='--', alpha=0.6, which='major')
|
|
325
|
+
|
|
326
|
+
return fig, ax, x0_fit, FWHM
|
|
327
|
+
|
|
328
|
+
|
ppms_toolkit/sample.py
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
'''
|
|
2
|
+
|
|
3
|
+
'''
|
|
4
|
+
|
|
5
|
+
import pickle
|
|
6
|
+
import os
|
|
7
|
+
from datetime import date
|
|
8
|
+
from multiprocessing import Pool, set_start_method
|
|
9
|
+
|
|
10
|
+
import pandas as pd
|
|
11
|
+
import matplotlib.pyplot as plt
|
|
12
|
+
|
|
13
|
+
from .measurement import Measurement,VSMMeasurement,HeatCapacityMeasurement
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Sample:
|
|
17
|
+
'''
|
|
18
|
+
This modules defined each sample, their properties
|
|
19
|
+
and their supported functions.
|
|
20
|
+
|
|
21
|
+
The Samples should contains serveal Measurement and could
|
|
22
|
+
be save into a .pickel file and reload some time again.
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
name : str
|
|
28
|
+
Name of the sample.
|
|
29
|
+
id : float, optional
|
|
30
|
+
Assign a unique id to the sample.
|
|
31
|
+
orientation : str
|
|
32
|
+
either "In Plane" or "Out of Plane"
|
|
33
|
+
mass : float, optional
|
|
34
|
+
Mass of the sample, unit is mg.
|
|
35
|
+
make_date: str
|
|
36
|
+
make date of the sample, in "%Y-%m-%d" format.
|
|
37
|
+
|
|
38
|
+
Attributes
|
|
39
|
+
----------
|
|
40
|
+
measurements: list
|
|
41
|
+
a list of measurements assigned to this sample.
|
|
42
|
+
All the parameters.
|
|
43
|
+
'''
|
|
44
|
+
def __init__(self, name: str,
|
|
45
|
+
id: float | None = None,
|
|
46
|
+
orientation: str | None = None,
|
|
47
|
+
mass: float | None = None,
|
|
48
|
+
make_date: str | None = None):
|
|
49
|
+
self.name = name
|
|
50
|
+
self.orientation = orientation
|
|
51
|
+
self.id = id
|
|
52
|
+
self.mass = mass # milligram
|
|
53
|
+
self.make_date = \
|
|
54
|
+
date.fromisoformat(make_date) if make_date else None
|
|
55
|
+
self._measurements = []
|
|
56
|
+
# in Sample.__init__
|
|
57
|
+
self.phase_points = pd.DataFrame(columns=["source", "temp", "field", "fwhm"])
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def measurements(self) -> tuple[pd.DataFrame, pd.DataFrame]:
|
|
62
|
+
'''Represent the measurement list as dataframes'''
|
|
63
|
+
df_vsm = self.measurements_vsm
|
|
64
|
+
df_hc = self.measurements_hc
|
|
65
|
+
return df_vsm, df_hc
|
|
66
|
+
|
|
67
|
+
# deprecated function
|
|
68
|
+
# @property
|
|
69
|
+
# def show_measurements(self):
|
|
70
|
+
# df_vsm = self.measurements_vsm
|
|
71
|
+
# df_hc = self.measurements_hc
|
|
72
|
+
# if df_hc.empty:
|
|
73
|
+
# print("There's no HC measurements bind to this sample.")
|
|
74
|
+
# else:
|
|
75
|
+
# print("#### HeatCapacity Measurements List")
|
|
76
|
+
# print(df_hc)
|
|
77
|
+
|
|
78
|
+
# if df_vsm.empty:
|
|
79
|
+
# print("There's no VSM measurements bind to this sample.")
|
|
80
|
+
# else:
|
|
81
|
+
# print("#### VSM Measurements List")
|
|
82
|
+
# print(df_vsm)
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def measurements_vsm(self):
|
|
86
|
+
return self.get_measurements_vsm()
|
|
87
|
+
|
|
88
|
+
def get_measurements_vsm(self, mode=None):
|
|
89
|
+
'''Represent the measurement list as pd.Dataframe'''
|
|
90
|
+
|
|
91
|
+
measurements = (
|
|
92
|
+
m for m in self._measurements
|
|
93
|
+
if isinstance(m, VSMMeasurement)
|
|
94
|
+
and (mode is None or m.mode == mode)
|
|
95
|
+
)
|
|
96
|
+
df = pd.DataFrame(m.to_dict() for m in measurements)
|
|
97
|
+
|
|
98
|
+
if df.empty:
|
|
99
|
+
return df
|
|
100
|
+
|
|
101
|
+
# 决定排序规则
|
|
102
|
+
sort_key = 'const temp' if mode == 'MH' else 'const field'
|
|
103
|
+
|
|
104
|
+
return df.sort_values(sort_key)
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def measurements_hc(self):
|
|
108
|
+
'''Represent the measurement list as pd.Dataframe'''
|
|
109
|
+
hc_rows = []
|
|
110
|
+
for m in self._measurements:
|
|
111
|
+
if isinstance(m, HeatCapacityMeasurement):
|
|
112
|
+
hc_rows.append(m.to_dict())
|
|
113
|
+
df_hc =pd.DataFrame(hc_rows)
|
|
114
|
+
return df_hc
|
|
115
|
+
|
|
116
|
+
def set_make_date(self, make_date: str):
|
|
117
|
+
self.make_date = date.fromisoformat(make_date)
|
|
118
|
+
|
|
119
|
+
def add_measurement(self, m: Measurement):
|
|
120
|
+
if m not in self._measurements:
|
|
121
|
+
print(f'{m} (added)')
|
|
122
|
+
self._measurements.append(m)
|
|
123
|
+
m.sample = self # double-linked with the measurement
|
|
124
|
+
else:
|
|
125
|
+
print(f'The Measurment [{m}] \n'
|
|
126
|
+
f'is already exist in sample [{self}] ')
|
|
127
|
+
|
|
128
|
+
def save(self):
|
|
129
|
+
with open(f'{self.name}'
|
|
130
|
+
f'({self.make_date if self.make_date else "Unknown MakeTime"}).pkl', "wb") as f:
|
|
131
|
+
pickle.dump(self, f)
|
|
132
|
+
|
|
133
|
+
@staticmethod
|
|
134
|
+
def load(filepath):
|
|
135
|
+
with open(filepath, "rb") as f:
|
|
136
|
+
return pickle.load(f)
|
|
137
|
+
|
|
138
|
+
def _vsm_measurement_reader(self, args):
|
|
139
|
+
|
|
140
|
+
path, orientation = args
|
|
141
|
+
if 'MT' in path:
|
|
142
|
+
mode = 'MT'
|
|
143
|
+
elif 'MH' in path:
|
|
144
|
+
mode = 'MH'
|
|
145
|
+
else:
|
|
146
|
+
raise ValueError(f'Mode is not contained in filename of {path}')
|
|
147
|
+
|
|
148
|
+
m=VSMMeasurement(filepath=path,
|
|
149
|
+
sample=self,
|
|
150
|
+
mode=mode)
|
|
151
|
+
|
|
152
|
+
return m
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def add_vsm_measurements_by_folder(self, folder_path, paralelle=False):
|
|
156
|
+
|
|
157
|
+
"""
|
|
158
|
+
bind multiple vsm measurement by folder name
|
|
159
|
+
"""
|
|
160
|
+
if os.name != "posix":
|
|
161
|
+
set_start_method("spawn", force=True)
|
|
162
|
+
|
|
163
|
+
if 'IP' in folder_path:
|
|
164
|
+
orientation = "In Plane"
|
|
165
|
+
elif 'OOP' in folder_path:
|
|
166
|
+
orientation = "Out of Plane"
|
|
167
|
+
else:
|
|
168
|
+
raise ValueError(f'Orientation is not contained in folderpath {folder_path}')
|
|
169
|
+
|
|
170
|
+
arg_list = [(folder_path + p, orientation) for p in os.listdir(folder_path)]
|
|
171
|
+
|
|
172
|
+
if paralelle:
|
|
173
|
+
with Pool() as pool:
|
|
174
|
+
measurements = pool.map(self._vsm_measurement_reader, arg_list)
|
|
175
|
+
|
|
176
|
+
for m in measurements:
|
|
177
|
+
self.add_measurement(m)
|
|
178
|
+
else:
|
|
179
|
+
for args in arg_list:
|
|
180
|
+
m = self._vsm_measurement_reader(args)
|
|
181
|
+
self.add_measurement(m)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def __repr__(self):
|
|
185
|
+
date = self.make_date if self.make_date else "Unknwon Date"
|
|
186
|
+
return (f'id: {self.id}, '
|
|
187
|
+
f'{self.name}, '
|
|
188
|
+
f'{self.mass}mg, '
|
|
189
|
+
f'made in {date}.')
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def add_phase_point(self, source, x, y, fwhm):
|
|
193
|
+
self.phase_points.loc[len(self.phase_points)] = [source, x, y, fwhm]
|
|
194
|
+
|
|
195
|
+
def plot_phase_diagram(self,
|
|
196
|
+
ax=None,
|
|
197
|
+
mh_fwhm_cut=5000,
|
|
198
|
+
mt_fwhm_cut=10,
|
|
199
|
+
xlim=None,
|
|
200
|
+
ylim=None,
|
|
201
|
+
title=None,
|
|
202
|
+
savepath=None,
|
|
203
|
+
dpi=300):
|
|
204
|
+
"""
|
|
205
|
+
Plot phase diagram using notebook-like style.
|
|
206
|
+
|
|
207
|
+
phase_points columns:
|
|
208
|
+
- source: 'MH' or 'MT'
|
|
209
|
+
- x, y
|
|
210
|
+
- fwhm
|
|
211
|
+
"""
|
|
212
|
+
if ax is None:
|
|
213
|
+
_, ax = plt.subplots()
|
|
214
|
+
|
|
215
|
+
df = self.phase_points.copy()
|
|
216
|
+
if df.empty:
|
|
217
|
+
raise ValueError("phase_points is empty")
|
|
218
|
+
|
|
219
|
+
df["source"] = df["source"].astype(str).str.upper()
|
|
220
|
+
df = df[df["source"].isin(["MH", "MT"])]
|
|
221
|
+
|
|
222
|
+
for col in ("temp", "field", "fwhm"):
|
|
223
|
+
df[col] = pd.to_numeric(df[col], errors="coerce")
|
|
224
|
+
df = df.dropna(subset=["temp", "field", "fwhm"])
|
|
225
|
+
|
|
226
|
+
def plot_with_err(sub_df, *, err_axis, color, marker, label):
|
|
227
|
+
if sub_df.empty:
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
err = sub_df["fwhm"].values / 2.0
|
|
231
|
+
kwargs = dict(
|
|
232
|
+
fmt=marker,
|
|
233
|
+
color=color,
|
|
234
|
+
ecolor="gray",
|
|
235
|
+
capsize=5,
|
|
236
|
+
elinewidth=1.5,
|
|
237
|
+
alpha=0.8,
|
|
238
|
+
linestyle="none",
|
|
239
|
+
label=label,
|
|
240
|
+
)
|
|
241
|
+
if err_axis == "y":
|
|
242
|
+
ax.errorbar(
|
|
243
|
+
x=sub_df["temp"].values,
|
|
244
|
+
y=sub_df["field"].values,
|
|
245
|
+
yerr=err,
|
|
246
|
+
**kwargs
|
|
247
|
+
)
|
|
248
|
+
else:
|
|
249
|
+
ax.errorbar(
|
|
250
|
+
x=sub_df["temp"].values,
|
|
251
|
+
y=sub_df["field"].values,
|
|
252
|
+
xerr=err,
|
|
253
|
+
**kwargs
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# MH: x=Temp, y=PeakField, yerr=FWHM/2
|
|
257
|
+
df_mh = df[df["source"] == "MH"]
|
|
258
|
+
wide_mh = df_mh["fwhm"] > mh_fwhm_cut
|
|
259
|
+
narrow_mh = ~wide_mh
|
|
260
|
+
plot_with_err(
|
|
261
|
+
df_mh[narrow_mh],
|
|
262
|
+
err_axis="y",
|
|
263
|
+
color="tab:green",
|
|
264
|
+
marker="s",
|
|
265
|
+
label=f"MH - reliable, width <= {mh_fwhm_cut / 1000:g} kOe",
|
|
266
|
+
)
|
|
267
|
+
plot_with_err(
|
|
268
|
+
df_mh[wide_mh],
|
|
269
|
+
err_axis="y",
|
|
270
|
+
color="tab:red",
|
|
271
|
+
marker="s",
|
|
272
|
+
label=f"MH - less reliable, width > {mh_fwhm_cut / 1000:g} kOe",
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
# MT: x=PeakTemp, y=Field, xerr=FWHM/2
|
|
276
|
+
df_mt = df[df["source"] == "MT"]
|
|
277
|
+
wide_mt = df_mt["fwhm"] > mt_fwhm_cut
|
|
278
|
+
narrow_mt = ~wide_mt
|
|
279
|
+
plot_with_err(
|
|
280
|
+
df_mt[narrow_mt],
|
|
281
|
+
err_axis="x",
|
|
282
|
+
color="tab:green",
|
|
283
|
+
marker="^",
|
|
284
|
+
label=f"MT - reliable, width <= {mt_fwhm_cut:g} K",
|
|
285
|
+
)
|
|
286
|
+
plot_with_err(
|
|
287
|
+
df_mt[wide_mt],
|
|
288
|
+
err_axis="x",
|
|
289
|
+
color="tab:red",
|
|
290
|
+
marker="^",
|
|
291
|
+
label=f"MT - less reliable, width > {mt_fwhm_cut:g} K",
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
ax.set_xlabel("Temperature (K)")
|
|
295
|
+
ax.set_ylabel("Magnetic Field (Oe)")
|
|
296
|
+
ax.grid(True, linestyle="--", alpha=0.6, which="major")
|
|
297
|
+
ax.legend()
|
|
298
|
+
|
|
299
|
+
if title:
|
|
300
|
+
ax.set_title(title)
|
|
301
|
+
else:
|
|
302
|
+
ax.set_title(f"{self.name} {self.orientation} Phase Diagram")
|
|
303
|
+
if xlim:
|
|
304
|
+
ax.set_xlim(xlim)
|
|
305
|
+
if ylim:
|
|
306
|
+
ax.set_ylim(ylim)
|
|
307
|
+
|
|
308
|
+
plt.tight_layout()
|
|
309
|
+
if savepath:
|
|
310
|
+
plt.savefig(savepath, dpi=dpi)
|
|
311
|
+
|
|
312
|
+
return ax
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
## This function is not quite useful.
|
|
316
|
+
def plot_vsm(self, mode, ax=None, field=None, temperature=None, condition=None, susceptibility=True, legend: str|list='Exp Setting'):
|
|
317
|
+
|
|
318
|
+
if not ax:
|
|
319
|
+
fig ,ax = plt.subplots()
|
|
320
|
+
|
|
321
|
+
df = self.measurements_vsm
|
|
322
|
+
if mode=='MH':
|
|
323
|
+
df = self.measurements_vsm.sort_values('const temp')
|
|
324
|
+
|
|
325
|
+
if mode:
|
|
326
|
+
mask_mode = df['mode'] == mode
|
|
327
|
+
else:
|
|
328
|
+
mask_mode = True
|
|
329
|
+
|
|
330
|
+
if field:
|
|
331
|
+
mask_field = df['const field'] == field
|
|
332
|
+
else:
|
|
333
|
+
mask_field = True
|
|
334
|
+
|
|
335
|
+
if temperature:
|
|
336
|
+
mask_temp = df['const temp'] == temperature
|
|
337
|
+
else:
|
|
338
|
+
mask_temp = True
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
df_filtered = df[mask_field & mask_mode & mask_temp]
|
|
342
|
+
for index, row in df_filtered.iterrows():
|
|
343
|
+
if condition:
|
|
344
|
+
if row['instance'].condition == condition:
|
|
345
|
+
row['instance'].plot(ax=ax, susceptibility=susceptibility, legend=legend)
|
|
346
|
+
else:
|
|
347
|
+
row['instance'].plot(ax=ax, susceptibility=susceptibility, legend=legend)
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: PPMS_Toolkit
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Toolkit for PPMS data processing
|
|
5
|
+
Author-email: Yunxiao LIU <yunxiao.liu@tum.de>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 Yunxiao Liu
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Classifier: Operating System :: OS Independent
|
|
29
|
+
Classifier: Programming Language :: Python :: 3
|
|
30
|
+
Requires-Python: >=3.9
|
|
31
|
+
Requires-Dist: matplotlib>=3.4.0
|
|
32
|
+
Requires-Dist: numpy>=1.21.0
|
|
33
|
+
Requires-Dist: pandas>=1.3.0
|
|
34
|
+
Requires-Dist: scipy>=1.7.0
|
|
35
|
+
Provides-Extra: gui
|
|
36
|
+
Requires-Dist: pyarrow>=10.0.0; extra == 'gui'
|
|
37
|
+
Requires-Dist: pyside6; extra == 'gui'
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
# PPMS Toolkit
|
|
41
|
+
|
|
42
|
+
> ⚠️ **Development Notice**
|
|
43
|
+
>
|
|
44
|
+
> This project is currently under active development and primarily developed and tested on **macOS**.
|
|
45
|
+
> It is a **pure Python** application built with **Qt (PySide6)**, so it should be *cross-platform* in principle.
|
|
46
|
+
> However, minor GUI display issues may occur on **Windows** or **Linux** systems.
|
|
47
|
+
|
|
48
|
+
<div align="center">
|
|
49
|
+
|
|
50
|
+
[](https://www.python.org/downloads/)
|
|
51
|
+
[](https://opensource.org/licenses/MIT)
|
|
52
|
+
[](https://doc.qt.io/qtforpython/)
|
|
53
|
+
|
|
54
|
+
*A Python toolkit for PPMS (Physical Property Measurement System) data analysis*
|
|
55
|
+
|
|
56
|
+
[Features](#-features) • [Installation](#-installation) • [Quick Start](#-quick-start) • [Documentation](#-documentation)
|
|
57
|
+
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 📖 Overview
|
|
63
|
+
|
|
64
|
+

|
|
65
|
+
|
|
66
|
+
**PPMS Toolkit** is a modern, user-friendly application designed for researchers working with Quantum Design's Physical Property Measurement System (PPMS). It provides both a powerful **GUI application** and a flexible **Python library** for:
|
|
67
|
+
|
|
68
|
+
- 📂 **Data Management**: Import, organize, and manage PPMS `.dat` files with SQLite database
|
|
69
|
+
- 📊 **Interactive Plotting**: Visualize VSM ~~and Heat Capacity measurements~~ with Matplotlib
|
|
70
|
+
- ~~🔬 **Advanced Analysis**: Curie temperature fitting, background subtraction, susceptibility analysis~~
|
|
71
|
+
- 💾 **Efficient Storage**: Parquet-based file format for fast data loading and minimal disk usage
|
|
72
|
+
- 🎨 **Cross-Platform**: Built with PySide6 (Qt6) for cross-platform compatibility
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## ✨ Features
|
|
77
|
+
|
|
78
|
+
### 🖥️ GUI Application
|
|
79
|
+
|
|
80
|
+
- **Sample Management**
|
|
81
|
+
- Create and edit sample metadata (name, mass, orientation, chemical formula)
|
|
82
|
+
- Track multiple measurements per sample
|
|
83
|
+
- Delete samples and associated data files
|
|
84
|
+
|
|
85
|
+
- **Measurement Management**
|
|
86
|
+
- Batch import of `.dat` files from PPMS
|
|
87
|
+
- Support for VSM (Vibrating Sample Magnetometer) measurements:
|
|
88
|
+
- **MH mode**: Magnetization vs. Field
|
|
89
|
+
- **MT mode**: Magnetization vs. Temperature
|
|
90
|
+
- ~~Support for Heat Capacity measurements~~
|
|
91
|
+
- Automatic deduplication based on file content hash
|
|
92
|
+
|
|
93
|
+
- **Interactive Plotting**
|
|
94
|
+
- Plot multiple measurements with customizable legends
|
|
95
|
+
- Click-to-hide curves for easy comparison
|
|
96
|
+
- Switch between susceptibility (χ) and moment views
|
|
97
|
+
- Zoom, pan, and export plots
|
|
98
|
+
|
|
99
|
+
- **Data Filtering**
|
|
100
|
+
- Multi-column filtering in measurement tables
|
|
101
|
+
- Filter by sample, mode, field, temperature, or condition
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## 🚀 Installation
|
|
106
|
+
|
|
107
|
+
### Option 1: Conda Environment (Recommended)
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
# Clone the repository
|
|
111
|
+
git clone https://github.com/AlbertRyu/PPMS_ToolKit.git
|
|
112
|
+
cd PPMS_ToolKit
|
|
113
|
+
|
|
114
|
+
# Create and activate conda environment
|
|
115
|
+
conda env create -f environment.yml
|
|
116
|
+
conda activate ppms_toolkit
|
|
117
|
+
|
|
118
|
+
# Install the package in development mode
|
|
119
|
+
pip install -e ".[gui]"
|
|
120
|
+
|
|
121
|
+
# Launch the GUI
|
|
122
|
+
ppms-toolkit
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Option 2: pip + venv
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
# Clone the repository
|
|
129
|
+
git clone https://github.com/AlbertRyu/PPMS_ToolKit.git
|
|
130
|
+
cd PPMS_ToolKit
|
|
131
|
+
|
|
132
|
+
# Create virtual environment
|
|
133
|
+
python -m venv venv
|
|
134
|
+
source venv/bin/activate # On Windows: venv\Scripts\activate
|
|
135
|
+
|
|
136
|
+
# Install with GUI support
|
|
137
|
+
pip install -e ".[gui]"
|
|
138
|
+
|
|
139
|
+
# Launch the GUI
|
|
140
|
+
ppms-toolkit
|
|
141
|
+
```
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## 🎯 Quick Start
|
|
145
|
+
|
|
146
|
+
### GUI Workflow
|
|
147
|
+
|
|
148
|
+
1. **Launch Application**
|
|
149
|
+
```bash
|
|
150
|
+
ppms-toolkit
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
2. **Select/Create Project**
|
|
154
|
+
- Choose an existing project folder or create a new one
|
|
155
|
+
- All data will be stored in this folder
|
|
156
|
+
|
|
157
|
+
3. **Add Samples**
|
|
158
|
+
- Navigate to "Samples" tab
|
|
159
|
+
- Click "Add Sample" and enter metadata (name, mass, orientation)
|
|
160
|
+
|
|
161
|
+
4. **Import Measurements**
|
|
162
|
+
- Go to "Plots" tab
|
|
163
|
+
- Click "Add Measurement"
|
|
164
|
+
- Select multiple `.dat` files (batch import supported)
|
|
165
|
+
- Choose the sample and measurement mode (MH/MT)
|
|
166
|
+
|
|
167
|
+
#### 📁 File Naming for MT Mode
|
|
168
|
+
|
|
169
|
+
The toolkit automatically detects measurement conditions from filenames:
|
|
170
|
+
|
|
171
|
+
| Filename Contains | Detected Condition |
|
|
172
|
+
|-------------------|-------------------|
|
|
173
|
+
| `ZFC` (case-insensitive) | Zero Field Cooling |
|
|
174
|
+
| `FC` (case-insensitive) | Field Cooling |
|
|
175
|
+
| Neither | Unknown Condition |
|
|
176
|
+
|
|
177
|
+
**Examples:**
|
|
178
|
+
```bash
|
|
179
|
+
✅ sample_ZFC_100Oe.dat → Condition: ZFC
|
|
180
|
+
✅ data_FC.dat → Condition: FC
|
|
181
|
+
⚠️ measurement.dat → Condition: Unknown
|
|
182
|
+
|
|
183
|
+
5. **Visualize Data**
|
|
184
|
+
- Select measurements from the table
|
|
185
|
+
- Click "Plot" to visualize
|
|
186
|
+
- Use legend to toggle curves
|
|
187
|
+
- Toggle χ/Moment view with checkbox
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## 📊 Supported Measurement Types
|
|
192
|
+
|
|
193
|
+
### VSM (Vibrating Sample Magnetometer)
|
|
194
|
+
|
|
195
|
+
| Mode | Description | Analysis Tools |
|
|
196
|
+
|------|-------------|----------------|
|
|
197
|
+
| **MH** | Magnetization vs. Field | ~~`fit_MH()` - Coercivity extraction~~~~ |
|
|
198
|
+
| **MT** | Magnetization vs. Temperature | ~~`fit_MT()` - Curie temperature fitting~~|
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## 🗂️ Project Structure
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
PPMS_ToolKit/
|
|
206
|
+
├── src/
|
|
207
|
+
│ ├── ppms_toolkit/ # Core library
|
|
208
|
+
│ │ ├── sample.py # Sample class
|
|
209
|
+
│ │ └── measurement/
|
|
210
|
+
│ │ ├── base.py # Base Measurement class
|
|
211
|
+
│ │ ├── vsm.py # VSM analysis
|
|
212
|
+
│ │ └── heat_capacity.py # Heat Capacity analysis
|
|
213
|
+
│ │
|
|
214
|
+
│ ├── ppms_toolkit_gui/ # GUI application
|
|
215
|
+
│ │ ├── app.py # Entry point
|
|
216
|
+
│ │ ├── main_window.py # Main window
|
|
217
|
+
│ │ ├── controller/ # MVC controllers
|
|
218
|
+
│ │ ├── widgets/ # Qt widgets
|
|
219
|
+
│ │ └── dialogs/ # Dialog windows
|
|
220
|
+
│ │
|
|
221
|
+
│ └── infrastructure/
|
|
222
|
+
│ └── db/
|
|
223
|
+
│ └── db.py # SQLite database wrapper
|
|
224
|
+
│
|
|
225
|
+
├── examples/
|
|
226
|
+
│ └── code_example.ipynb # Jupyter notebook examples
|
|
227
|
+
│
|
|
228
|
+
├── pyproject.toml # Project metadata & dependencies
|
|
229
|
+
├── environment.yml # Conda environment specification
|
|
230
|
+
└── README.md # This file
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## 🔧 Dependencies
|
|
236
|
+
|
|
237
|
+
### Core Dependencies
|
|
238
|
+
- **numpy** ≥ 1.21 - Numerical computing
|
|
239
|
+
- **pandas** ≥ 1.3 - Data manipulation
|
|
240
|
+
- **scipy** ≥ 1.7 - Scientific computing
|
|
241
|
+
- **matplotlib** ≥ 3.4 - Plotting
|
|
242
|
+
|
|
243
|
+
### GUI Dependencies (Optional)
|
|
244
|
+
- **PySide6** ≥ 6.4 - Qt6 GUI framework
|
|
245
|
+
- **pyarrow** ≥ 10.0 - Parquet file I/O
|
|
246
|
+
|
|
247
|
+
### Development Dependencies
|
|
248
|
+
- **ipykernel** - Jupyter notebook support
|
|
249
|
+
- **IPython** - Enhanced interactive shell
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## 📚 Documentation
|
|
254
|
+
|
|
255
|
+
### Database Structure
|
|
256
|
+
|
|
257
|
+
The toolkit uses SQLite for data management with two main tables:
|
|
258
|
+
|
|
259
|
+
**`samples`**
|
|
260
|
+
- `id`, `name`, `mass`, `chemical`, `orientation`, `created_at`, `notes`
|
|
261
|
+
|
|
262
|
+
**`measurements`**
|
|
263
|
+
- `id`, `sample_id` (FK), `measurement_type`, `mode`
|
|
264
|
+
- `const_field`, `const_temperature`
|
|
265
|
+
- `original_filepath`, `data_filepath`, `processed_data_filepath`
|
|
266
|
+
- `content_hash` (for deduplication)
|
|
267
|
+
- `extra_parameters` (JSON), `comment`, `created_at`
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## 📄 License
|
|
271
|
+
|
|
272
|
+
This project is licensed under the **MIT License**.
|
|
273
|
+
|
|
274
|
+
You are free to use, modify, and distribute this software, provided that:
|
|
275
|
+
- You include the original copyright notice and license text in any copies
|
|
276
|
+
- You do not hold the author liable for any damages
|
|
277
|
+
|
|
278
|
+
See [LICENSE](LICENSE) file for full details.
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## 🙏 Acknowledgments
|
|
283
|
+
|
|
284
|
+
- Built with [PySide6](https://doc.qt.io/qtforpython/) (Qt for Python)
|
|
285
|
+
- Data storage powered by [Apache Arrow](https://arrow.apache.org/) (Parquet format)
|
|
286
|
+
- Scientific computing with [NumPy](https://numpy.org/), [SciPy](https://scipy.org/), and [pandas](https://pandas.pydata.org/)
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
<div align="center">
|
|
291
|
+
|
|
292
|
+
**[⬆ Back to Top](#ppms-toolkit)**
|
|
293
|
+
|
|
294
|
+
Made with ❤️ for the research community
|
|
295
|
+
|
|
296
|
+
</div>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
ppms_toolkit/__init__.py,sha256=3flGSBZtv-_MCc6sgIaV6NeDZpKZwxEBLx827DFDUjw,27
|
|
2
|
+
ppms_toolkit/sample.py,sha256=1eWdPbTXqo020UBxNUG9lbPd7jzqPuiy9g0BxZCTuFg,10471
|
|
3
|
+
ppms_toolkit/measurement/__init__.py,sha256=Q-bhd43pE2QAhjdvo6SY6e6IUeYkykZA8EnXuk53hWM,512
|
|
4
|
+
ppms_toolkit/measurement/base.py,sha256=IxxRhWz12T79mdWbyHgGlaxtsit1WimNylSLEpxtm6Y,2624
|
|
5
|
+
ppms_toolkit/measurement/heat_capacity.py,sha256=HFE6-O-xZ3BH-mjA5TgWeSP0rxr76l72Vlpf33mHY7c,5481
|
|
6
|
+
ppms_toolkit/measurement/utils.py,sha256=xzrRJJMZe6KQiGI-q45Vq8s7hOwrJRJKQFmrYhxyFKQ,2477
|
|
7
|
+
ppms_toolkit/measurement/vsm.py,sha256=JrHCjbb3io-gFQ9g2G-fTTCzeOgVc5qGP3GOP-RuUbs,11712
|
|
8
|
+
ppms_toolkit-0.2.1.dist-info/METADATA,sha256=EXg3lV9iXmmmArDfC1wPJtKYWFzi0opR76HrxPCZ-_M,9636
|
|
9
|
+
ppms_toolkit-0.2.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
10
|
+
ppms_toolkit-0.2.1.dist-info/entry_points.txt,sha256=CN_9LJUB_mHCyNLeVjJdqq0-uxxf4TC5ffg5ujOhQsc,59
|
|
11
|
+
ppms_toolkit-0.2.1.dist-info/licenses/LICENSE,sha256=akBODBkAxof09O6zf-agbJKnBVlFvdfduN9GdHss0Qs,1068
|
|
12
|
+
ppms_toolkit-0.2.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Yunxiao Liu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|