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.
@@ -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
+ [![Python](https://img.shields.io/badge/Python-3.9+-blue.svg)](https://www.python.org/downloads/)
51
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
52
+ [![PySide6](https://img.shields.io/badge/GUI-PySide6-green.svg)](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
+ ![Easy plot with Measurement List](screenshots/plot_window.png)
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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ppms-toolkit = ppms_toolkit_gui.app:main
@@ -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.