from pathlib import Path
import matplotlib.pyplot as plt
import magnetopy as mp
DATA_PATH = Path("../../tests/data")
mvsh4 = mp.MvsH(DATA_PATH / "mvsh4.dat")
mvsh4
MvsH at 293.0 K
The as_dict()
method returns a dictionary of information about the experiment and any processing that has been performed on it. Note that these are all attributes of the MvsH
object, and can be accessed directly as well.
Since we just created this object and haven't done any processing, the field_correction_file
and scaling
attributes will be empty.
mvsh4.as_dict()
{'origin_file': 'mvsh4.dat', 'temperature': 293.0, 'field_range': (-70000.375, 70000.3359375), 'field_correction_file': '', 'scaling': []}
The data is stored in the data
attribute. The columns of this DataFrame
are created directly from the data file which, in this case, is a .dat file from a Quantum Design MPMS3. There are two additional columns at the end, "uncorrected_moment"
and "uncorrected_moment_err"
. The .dat file contains the magnetization data in one of two columns, depending on whether the measurements were done in DC or VSM mode. The MvsH
class automatically determines which column contains the data, and stores it in the "uncorrected_moment"
and "uncorrected_moment_err"
columns, which are used for subsequent processing.
mvsh4.data.head()
Comment | Time Stamp (sec) | Temperature (K) | Magnetic Field (Oe) | Moment (emu) | M. Std. Err. (emu) | Transport Action | Averaging Time (sec) | Frequency (Hz) | Peak Amplitude (mm) | ... | Map 09 | Map 10 | Map 11 | Map 12 | Map 13 | Map 14 | Map 15 | Map 16 | uncorrected_moment | uncorrected_moment_err | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | NaN | 3.860780e+09 | 293.223587 | -70000.000000 | NaN | NaN | 6.0 | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | -0.004099 | 0.000208 |
1 | NaN | 3.860780e+09 | 293.209259 | -65000.421875 | NaN | NaN | 6.0 | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | -0.004110 | 0.000209 |
2 | NaN | 3.860780e+09 | 293.174545 | -60000.355469 | NaN | NaN | 6.0 | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | -0.004117 | 0.000208 |
3 | NaN | 3.860780e+09 | 293.203033 | -55000.378906 | NaN | NaN | 6.0 | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | -0.004117 | 0.000208 |
4 | NaN | 3.860780e+09 | 293.191849 | -50000.183594 | NaN | NaN | 6.0 | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | -0.004120 | 0.000208 |
5 rows × 91 columns
Files with Multiple Experiments¶
Trying to create an MvsH
object from a file containing multiple experiments will result in a TemperatureDetectionError
:
try:
mvsh1 = mp.MvsH(DATA_PATH / "mvsh1.dat")
except mp.experiments.mvsh.TemperatureDetectionError as e:
print(e)
Auto-parsing of MvsH objects from DatFile objects requires that there be only one temperature in the data. Found 7 temperatures.
In this case we'll need to pass the temperature of our desired experiment to the constructor:
mvsh1_2k = mp.MvsH(DATA_PATH / "mvsh1.dat", temperature=2)
mvsh1_2k.as_dict()
{'origin_file': 'mvsh1.dat', 'temperature': 2, 'field_range': (-70000.35156, 70000.375), 'field_correction_file': '', 'scaling': []}
Creating Multiple MvsH
Objects From a Single File¶
In cases where a single file contains multiple experiments, we can create a list of MvsH
objects by passing the file path to the MvsH.get_all_in_file()
method:
mvsh1 = mp.MvsH.get_all_in_file(DATA_PATH / "mvsh1.dat")
for mvsh in mvsh1:
print(mvsh)
MvsH at 2 K MvsH at 4 K MvsH at 6 K MvsH at 8 K MvsH at 10 K MvsH at 12 K MvsH at 300 K
This also works for files containing a single experiment, you'll just get a list with a single MvsH
object in it.
mvsh4_list = mp.MvsH.get_all_in_file(DATA_PATH / "mvsh4.dat")
for mvsh in mvsh4_list:
print(mvsh)
MvsH at 293.0 K
Simplified Data and Segments¶
The columns given from the data file are likely more than we'll need for 98% of what we typically do with the data. Additionally, when MagnetoPy supports other file types from other instruments, we'll still want a standard interface upon which we can build anlayses and visualization methods. The simplified_data()
method returns a DataFrame
with only the relevant columns for an MvsH
experiment:
mvsh11 = mp.MvsH(DATA_PATH / "mvsh11.dat")
mvsh11.simplified_data().head()
time | temperature | field | moment | moment_err | chi | chi_err | chi_t | chi_t_err | |
---|---|---|---|---|---|---|---|---|---|
0 | 3.886899e+09 | 4.999953 | 0.085553 | -0.000045 | 6.638709e-07 | -5.310496e-04 | 7.759755e-06 | -2.655223e-03 | 3.879841e-05 |
1 | 3.886899e+09 | 4.999861 | 0.109997 | -0.000046 | 1.130964e-06 | -4.146267e-04 | 1.028179e-05 | -2.073076e-03 | 5.140751e-05 |
2 | 3.886899e+09 | 4.999944 | 108.469070 | -0.000148 | 2.639004e-05 | -1.364675e-06 | 2.432955e-07 | -6.823298e-06 | 1.216464e-06 |
3 | 3.886899e+09 | 4.999876 | 204.471832 | 0.000009 | 9.156850e-06 | 4.402288e-08 | 4.478294e-08 | 2.201089e-07 | 2.239092e-07 |
4 | 3.886899e+09 | 4.999945 | 300.352386 | 0.000246 | 1.018018e-05 | 8.198379e-07 | 3.389411e-08 | 4.099144e-06 | 1.694687e-07 |
In this case we haven't done any scaling or processing, so the values of the "moment"
and "moment_err"
columns are the same as the values in the previously mentioned "uncorrected_moment"
and "uncorrected_moment_err"
columns. We'll see how to scale the data in the next section.
Looking at the values of magnetic fields in the data, we can see that this experiment consists of a virgin, forward, and reverse scans:
fig, ax = plt.subplots()
x = mvsh11.simplified_data()["field"] / 10000
y = mvsh11.simplified_data()["moment"]
ax.plot(x, y)
ax.set_xlabel("Field (T)")
ax.set_ylabel("Moment (emu)")
mp.force_aspect(ax) # a plot utility function in Magnetopy
plt.show()
For some analyses and/or visualizations, we may only want to work with a single segment of the data. simplified_data()
has an optional segment
argument that can be used to return only a single segment of the data. Available segments are: "virgin"
, "forward"
, "reverse"
, and "loop"
(i.e., "forward"
+ "reverse"
).
mvsh11.simplified_data(segment="reverse").head()
time | temperature | field | moment | moment_err | chi | chi_err | chi_t | chi_t_err | |
---|---|---|---|---|---|---|---|---|---|
0 | 3.886900e+09 | 4.999997 | 70000.460938 | 0.408538 | 0.000393 | 0.000006 | 5.613738e-09 | 0.000029 | 2.806867e-08 |
1 | 3.886900e+09 | 5.000016 | 69999.851562 | 0.408824 | 0.000408 | 0.000006 | 5.821657e-09 | 0.000029 | 2.910838e-08 |
2 | 3.886900e+09 | 4.999962 | 69890.496094 | 0.408390 | 0.000389 | 0.000006 | 5.566526e-09 | 0.000029 | 2.783242e-08 |
3 | 3.886900e+09 | 5.000044 | 69798.500000 | 0.408776 | 0.000429 | 0.000006 | 6.151053e-09 | 0.000029 | 3.075553e-08 |
4 | 3.886900e+09 | 4.999870 | 69694.355469 | 0.408636 | 0.000383 | 0.000006 | 5.491631e-09 | 0.000029 | 2.745745e-08 |
fig, ax = plt.subplots()
x1 = mvsh11.simplified_data(segment="virgin")["field"] / 10000
y1 = mvsh11.simplified_data(segment="virgin")["moment"]
x2 = mvsh11.simplified_data(segment="reverse")["field"] / 10000
y2 = mvsh11.simplified_data(segment="reverse")["moment"]
x3 = mvsh11.simplified_data(segment="forward")["field"] / 10000
y3 = mvsh11.simplified_data(segment="forward")["moment"]
x4 = mvsh11.simplified_data(segment="loop")["field"] / 10000
y4 = mvsh11.simplified_data(segment="loop")["moment"]
ax.plot(x1, y1, label="virgin")
ax.plot(x2, y2, label="reverse")
ax.plot(x3, y3, label="forward")
ax.plot(x4, y4, label="loop", linewidth=4, zorder=-1, c="black")
ax.set_xlabel("Field (T)")
ax.set_ylabel("Moment (emu)")
ax.legend()
mp.force_aspect(ax)
plt.show()
Scaling the Moment¶
Scaling is something you'll likely do through the Magnetometry
class, discussed in a later example notebook. However, the Magnetometry
class uses methods within the MvsH
class, and these methods can be used directly as well.
Scaling can be done using the scale_moment()
method. As is described in the underlying utility function, this method adds columns to the data
attribute of the MvsH
object that contain the magnetic moment, magnetic susceptibility, and magnetic susceptibility times temperature (and their errors). The columns added are "moment"
, "moment_err"
, "chi"
, "chi_err"
, "chi_t"
, and "chi_t_err"
. The units of these values depend on the values of the mass
, eicosane_mass
, molecular_weight
, and diamagnetic_correction
which are passed as arguments. A record of what scaling was applied is added to the scaling
attribute of the MvsH
object.
Here are the currently supported scaling options:
- If
mass
is given but notmolecular_weight
, the only available scaling is a mass correction. - If
mass
andmolecular
weight are given, a molar correction is applied. The molar correction can be further modified by givingeicosane_mass
and/ordiamagnetic_correction
.
We'll use the SampleInfo
class to read the sample information from the header of the .dat file:
sample_info = mp.SampleInfo.from_dat_file(DATA_PATH / "mvsh11.dat")
print(
f"""{sample_info.mass = }
{sample_info.eicosane_mass = }
{sample_info.molecular_weight = }
{sample_info.diamagnetic_correction = }"""
)
sample_info.mass = 10.1 sample_info.eicosane_mass = 17.3 sample_info.molecular_weight = 704.95 sample_info.diamagnetic_correction = 0.0
We'll pass all of these to the scale_moment()
method, even though the diamagnetic_correction
is 0.
mvsh11.scale_moment(
sample_info.mass,
sample_info.eicosane_mass,
sample_info.molecular_weight,
sample_info.diamagnetic_correction
)
The scaling
attribute records the scaling that was applied as a list of manipulations. In this case, with a mass, molecular weight, and eicosane mass, the scaling is recorded as a "molar"
scaling with an "eicosane"
correction:
mvsh11.scaling
['molar', 'eicosane']
The mvsh.plot()
method will be discussed later, but for now notice that the default behavior, given molar scaling, is to plot the magnetization data in units of $N_A \mu_B$:
fig, ax = mvsh11.plot()
The plotting behavior works because the underlying plot_mvsh()
function accesses the simplified_data()
method. simplified_data()
always returns a DataFrame
with the same columns, but the values and units of those columns are dependent on the scaling that has been applied.
Compare the current simplified_data()
to the one we saw earlier -- the moment at 7 T was 0.4 emu and with the scaling that we just applied it's now 5.1 $N_A \mu_B$.
mvsh11.simplified_data(segment="reverse").head()
time | temperature | field | moment | moment_err | chi | chi_err | chi_t | chi_t_err | |
---|---|---|---|---|---|---|---|---|---|
0 | 3.886900e+09 | 4.999997 | 70000.460938 | 5.118614 | 0.017930 | 0.408390 | 0.001431 | 2.041947 | 0.007153 |
1 | 3.886900e+09 | 5.000016 | 69999.851562 | 5.122193 | 0.018112 | 0.408679 | 0.001445 | 2.043400 | 0.007225 |
2 | 3.886900e+09 | 4.999962 | 69890.496094 | 5.116739 | 0.017861 | 0.408882 | 0.001427 | 2.044396 | 0.007136 |
3 | 3.886900e+09 | 5.000044 | 69798.500000 | 5.121548 | 0.018347 | 0.409806 | 0.001468 | 2.049048 | 0.007340 |
4 | 3.886900e+09 | 4.999870 | 69694.355469 | 5.119781 | 0.017745 | 0.410277 | 0.001422 | 2.051331 | 0.007110 |
Correcting the Field for Flux Trapping¶
As described in this Quantum Design application note, the magnetic field reported by the magnetometer is determined by current from the magnet power supply and not by direct measurement. Flux trapping in the magnet can cause the reported field to be different from the actual field. While always present, it is most obvious in hysteresis curves of soft, non-hysteretic materials. In some cases the forward and reverse scans can have negative and postive coercivities, respectively, which is not physically possible.
The true field correction remedies this by using a Pd standard to determine the actual field applied to the sample. Assuming the calibration and sample sequences are the same, it is assumed that the flux trapping is the same for both sequences, and the calculated field from the measurement on the Pd standard is applied to the sample data.
Here's an M vs. H experiment that needs correction:
mvsh6 = mp.MvsH(DATA_PATH / "mvsh6.dat")
In the following plot, note that the forward segment has a negative coercivity and the reverse segment has a positive coercivity!
fig, ax = plt.subplots()
max_moment = mvsh6.simplified_data()["moment"].max()
x1 = mvsh6.simplified_data(segment="forward")["field"]
y1 = mvsh6.simplified_data(segment="forward")["moment"] / max_moment
x2 = mvsh6.simplified_data(segment="reverse")["field"]
y2 = mvsh6.simplified_data(segment="reverse")["moment"] / max_moment
ax.plot(x1, y1, label="forward")
ax.plot(x2, y2, label="reverse")
ax.set_xlim(-1000, 1000)
ax.set_ylim(-1, 1)
ax.set_xlabel("Field (Oe)")
ax.set_ylabel("Normalized Moment")
ax.legend()
mp.force_aspect(ax)
plt.show()
The field correction can be applied by passing the path to the .dat file containing the same experiment (i.e., same field sequence) collected on a Pd standard to the correct_field()
method:
mvsh6.correct_field(DATA_PATH / "Pd_std1.dat")
The corrected data removes the false coercivities:
fig, ax = plt.subplots()
max_moment = mvsh6.simplified_data()["moment"].max()
x1 = mvsh6.simplified_data(segment="forward")["field"]
y1 = mvsh6.simplified_data(segment="forward")["moment"] / max_moment
x2 = mvsh6.simplified_data(segment="reverse")["field"]
y2 = mvsh6.simplified_data(segment="reverse")["moment"] / max_moment
ax.plot(x1, y1, label="forward")
ax.plot(x2, y2, label="reverse")
ax.set_xlim(-1000, 1000)
ax.set_ylim(-1, 1)
ax.set_xlabel("Field (Oe)")
ax.set_ylabel("Normalized Moment")
ax.legend()
mp.force_aspect(ax)
plt.show()
Zooming in further we can see that the forward and reverse segments are mostly coincident and never cross:
fig, ax = plt.subplots()
max_moment = mvsh6.simplified_data()["moment"].max()
x1 = mvsh6.simplified_data(segment="forward")["field"]
y1 = mvsh6.simplified_data(segment="forward")["moment"] / max_moment
x2 = mvsh6.simplified_data(segment="reverse")["field"]
y2 = mvsh6.simplified_data(segment="reverse")["moment"] / max_moment
ax.plot(x1, y1, label="forward")
ax.plot(x2, y2, label="reverse")
ax.set_xlim(-100, 100)
ax.set_ylim(-0.1, 0.1)
ax.set_xlabel("Field (Oe)")
ax.set_ylabel("Normalized Moment")
ax.legend()
mp.force_aspect(ax)
plt.show()
The name of the file used for the correction is stored in the field_correction_file
attribute:
mvsh6.field_correction_file
'Pd_std1.dat'
The field correction can also be run by passing the name of a sequence within your standard calibration library. In our case, that's installed using the Rinehart group's MagnetoPyCalibration repository, and the desired sequence in this case is named "sequence_1"
.
mvsh6 = mp.MvsH(DATA_PATH / "mvsh6.dat")
mvsh6.correct_field("sequence_1")
fig, ax = plt.subplots()
max_moment = mvsh6.simplified_data()["moment"].max()
x1 = mvsh6.simplified_data(segment="forward")["field"]
y1 = mvsh6.simplified_data(segment="forward")["moment"] / max_moment
x2 = mvsh6.simplified_data(segment="reverse")["field"]
y2 = mvsh6.simplified_data(segment="reverse")["moment"] / max_moment
ax.plot(x1, y1, label="forward")
ax.plot(x2, y2, label="reverse")
ax.set_xlim(-1000, 1000)
ax.set_ylim(-1, 1)
ax.set_xlabel("Field (Oe)")
ax.set_ylabel("Normalized Moment")
ax.legend()
mp.force_aspect(ax)
plt.show()
The field correction file takes the name of the file in the standard calibration library:
mvsh6.field_correction_file
'mvsh_seq1.dat'
Plotting¶
The MagnetoPy function plot_mvsh()
provides basic plotting capabilities for MvsH
objects. It can be used to plot a single experiment or multiple experiments. In the former case it calls plot_single_mvsh()
and in the latter case it calls plot_multiple_mvsh()
. It shouldn't be necessary, but these functions can be called directly if desired.
There are also convenience functions in the MvsH
and Magnetometry
classes for plotting M vs. H experiments. In MvsH
, it is the plot()
method.
Plot a Single MvsH
Experiment¶
As we have already seen, the plot()
method of the MvsH
class plots the data contained within the class. The units are determined by the scaling that has been applied.
mvsh11 = mp.MvsH(DATA_PATH / "mvsh11.dat")
fig, ax = mvsh11.plot()
mvsh11 = mp.MvsH(DATA_PATH / "mvsh11.dat")
mvsh11_sample_info = mp.SampleInfo.from_dat_file(DATA_PATH / "mvsh11.dat")
mvsh11.scale_moment(
mass = mvsh11_sample_info.mass,
)
fig, ax = mvsh11.plot()
mvsh11 = mp.MvsH(DATA_PATH / "mvsh11.dat")
mvsh11_sample_info = mp.SampleInfo.from_dat_file(DATA_PATH / "mvsh11.dat")
mvsh11.scale_moment(
mass = mvsh11_sample_info.mass,
molecular_weight = mvsh11_sample_info.molecular_weight,
)
fig, ax = mvsh11.plot()
The "segment"
argument can be used to plot only a single segment of the data (options are "virgin"
, "forward"
, "reverse"
, and "loop"
). By default all data will be plotted.
mvsh11 = mp.MvsH(DATA_PATH / "mvsh11.dat")
mvsh11_sample_info = mp.SampleInfo.from_dat_file(DATA_PATH / "mvsh11.dat")
mvsh11.scale_moment(
mass = mvsh11_sample_info.mass,
molecular_weight = mvsh11_sample_info.molecular_weight,
)
fig, ax = mvsh11.plot(segment="virgin")
mvsh11 = mp.MvsH(DATA_PATH / "mvsh11.dat")
mvsh11_sample_info = mp.SampleInfo.from_dat_file(DATA_PATH / "mvsh11.dat")
mvsh11.scale_moment(
mass = mvsh11_sample_info.mass,
molecular_weight = mvsh11_sample_info.molecular_weight,
)
fig, ax = mvsh11.plot(segment="loop")
You can also plot single experiments using plot_mvsh()
by passing the experiment as an argument:
fig, ax = mp.plot_mvsh(mvsh11)
Plot Multiple MvsH
Experiments¶
The plot_mvsh()
function can be used to plot multiple experiments. If you want to plot multiple experiments from the same sample, the Magnetometry.plot_mvsh()
method will probably be more convenient.
mvsh10 = mp.MvsH(DATA_PATH / "mvsh10.dat")
mvsh10_sample_info = mp.SampleInfo.from_dat_file(DATA_PATH / "mvsh10.dat")
mvsh10.scale_moment(
mass = mvsh10_sample_info.mass,
molecular_weight = mvsh10_sample_info.molecular_weight,
)
mvsh11 = mp.MvsH(DATA_PATH / "mvsh11.dat")
mvsh11_sample_info = mp.SampleInfo.from_dat_file(DATA_PATH / "mvsh11.dat")
mvsh11.scale_moment(
mass = mvsh11_sample_info.mass,
molecular_weight = mvsh11_sample_info.molecular_weight,
)
fig, ax = mp.plot_mvsh([mvsh10, mvsh11], segment="loop", labels=["mvsh10", "mvsh11"])
If you want to plot multiple experiments which have different scaling, you'll need to take advantage of the normalize
argument.
mvsh2_5 = mp.MvsH(DATA_PATH / "mvsh2.dat", temperature=5)
fig, ax = mp.plot_mvsh(
[mvsh2_5, mvsh11],
normalized=True,
segment="loop",
colors=["green", "purple"],
labels=["mvsh2", "mvsh11"],
title="5 K"
)
Plotting Raw Data¶
The MvsH.plot_raw()
and MvsH.plot_raw_residual()
methods provide a more convenient way to plot the raw data from the .dat file than using the related DatFile
methods, as the MvsH
methods allow you to easily specify the segment to plot.
Note that when creating the MvsH
object you'll need to pass the parse_raw = True
argument to the constructor.
mvsh5 = mp.MvsH(DATA_PATH / "mvsh5.dat", parse_raw = True)
fig, ax = mvsh5.plot_raw(segment="forward")
fig, ax = mvsh5.plot_raw_residual(segment="forward")