Skip to content

MvsH

MvsH

A single magnetization vs. field (hysteresis) experiment at a single temperature.

Parameters:

Name Type Description Default
dat_file str, Path, or DatFile

The .dat file containing the data for the experiment.

required
temperature int or float

The temperature of the experiment in Kelvin. Requied if the .dat file contains multiple uncommented experiments at different temperatures. If None and the .dat file contains a single experiment, the temperature will be automatically detected. Defaults to None.

None
parse_raw bool

If True and there is a corresponding .rw.dat file, the raw data will be parsed and added to the data attribute. Defaults to False.

False
**kwargs dict

Keyword arguments used for algorithmic separation of data at the requested temperature. See magnetopy.parsing_utils.label_clusters for details.

  • eps : float, optional

  • min_samples : int, optional

  • n_digits : int, optional

{}

Attributes:

Name Type Description
origin_file str

The name of the .dat file from which the data was parsed.

temperature float

The temperature of the experiment in Kelvin.

data pandas.DataFrame

The data from the experiment. Columns are taken directly from the .dat file.

field_correction_file str

The name of the .dat file containing the Pd standard sequence used to correct the magnetic field for flux trapping. If no field correction has been applied, this will be an empty string.

scaling list of str

The scaling applied to the data. If no scaling has been applied, this will be an empty list. Possible values are: "mass", "molar", "eicosane", and "diamagnetic_correction".

field_range tuple of float

The minimum and maximum field values in the data.

Raises:

Type Description
self.TemperatureNotInDataError

If the requested temperature is not in the data or the comments are not formatted correctly and the temperature cannot be automatically detected.

self.FieldCorrectionError

If a field correction is applied but the Pd standard sequence does not have the same number of data points as the MvsH sequence.

self.SegmentError

If the requested segment is not found in the data.

Source code in magnetopy\experiments\mvsh.py
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
class MvsH:
    """A single magnetization vs. field (hysteresis) experiment at a single temperature.

    Parameters
    ----------
    dat_file : str, Path, or DatFile
        The .dat file containing the data for the experiment.
    temperature : int or float, optional
        The temperature of the experiment in Kelvin. Requied if the .dat file contains
        multiple uncommented experiments at different temperatures. If `None` and the
        .dat file contains a single experiment, the temperature will be automatically
        detected. Defaults to `None`.
    parse_raw : bool, optional
        If `True` and there is a corresponding .rw.dat file, the raw data will be
        parsed and added to the `data` attribute. Defaults to `False`.
    **kwargs : dict, optional
        Keyword arguments used for algorithmic separation of data at the requested
        temperature. See `magnetopy.parsing_utils.label_clusters` for details.

        - eps : float, optional

        - min_samples : int, optional

        - n_digits : int, optional

    Attributes
    ----------
    origin_file : str
        The name of the .dat file from which the data was parsed.
    temperature : float
        The temperature of the experiment in Kelvin.
    data : pandas.DataFrame
        The data from the experiment. Columns are taken directly from the .dat file.
    field_correction_file : str
        The name of the .dat file containing the Pd standard sequence used to correct
        the magnetic field for flux trapping. If no field correction has been applied,
        this will be an empty string.
    scaling : list of str
        The scaling applied to the data. If no scaling has been applied, this will be
        an empty list. Possible values are: `"mass"`, `"molar"`, `"eicosane"`,
        and `"diamagnetic_correction"`.
    field_range : tuple of float
        The minimum and maximum field values in the data.

    Raises
    ------
    self.TemperatureNotInDataError
        If the requested temperature is not in the data or the comments are not
        formatted correctly and the temperature cannot be automatically detected.
    self.FieldCorrectionError
        If a field correction is applied but the Pd standard sequence does not have the
        same number of data points as the MvsH sequence.
    self.SegmentError
        If the requested segment is not found in the data.
    """

    class TemperatureNotInDataError(Exception):
        pass

    class SegmentError(Exception):
        pass

    class FieldCorrectionError(Exception):
        pass

    def __init__(
        self,
        dat_file: str | Path | DatFile,
        temperature: int | float | None = None,
        parse_raw: bool = False,
        **kwargs,
    ) -> None:
        if not isinstance(dat_file, DatFile):
            dat_file = DatFile(Path(dat_file), parse_raw)
        self.origin_file = dat_file.local_path.name

        # optional arguments used for algorithmic separation of
        # data at the requested temperature
        n_digits = num_digits_after_decimal(temperature) if temperature else 0
        options = {"eps": 0.001, "min_samples": 10, "n_digits": n_digits}
        options.update(kwargs)

        if temperature is None:
            temperature = _auto_detect_temperature(
                dat_file, options["eps"], options["min_samples"], options["n_digits"]
            )

        self.temperature = temperature
        if dat_file.comments:
            self.data = self._set_data_from_comments(dat_file)
        else:
            self.data = self._set_data_auto(
                dat_file, options["eps"], options["min_samples"], options["n_digits"]
            )
        add_uncorrected_moment_columns(self)
        self.field_correction_file = ""
        self.scaling: list[str] = []
        self.field_range = self._determine_field_range()
        self._field_fluctuation_tolerance = 1

    def __str__(self) -> str:
        return f"MvsH at {self.temperature} K"

    def __repr__(self) -> str:
        return f"MvsH at {self.temperature} K"

    def _set_data_from_comments(self, dat_file: DatFile) -> pd.DataFrame:
        start_idx: int | None = None
        end_idx: int | None = None
        for comment_idx, (data_idx, comment_list) in enumerate(
            dat_file.comments.items()
        ):
            # ignore other experiments
            if "mvsh" not in map(str.lower, comment_list):
                continue
            # one of the comments should be a number denoting the temperature
            # may also include a unit, e.g. "300 K"
            for comment in comment_list:
                if match := re.search(r"\d+", comment):
                    found_temp = float(match.group())
                    # check to see if the unit is C otherwise assume K
                    if "C" in comment:
                        found_temp += 273
                    if found_temp == self.temperature:
                        start_idx = (
                            data_idx + 1
                        )  # +1 to skip the line containing the comment
                        end_idx = (
                            list(dat_file.comments.keys())[comment_idx + 1]
                            if comment_idx + 1 < len(dat_file.comments)
                            else (len(dat_file.data))
                        )
                        break
            if start_idx is not None:
                break
        else:
            raise self.TemperatureNotInDataError(
                f"Temperature {self.temperature} not in data in {dat_file}. "
                "Or the comments are not formatted correctly."
            )
        df = dat_file.data.iloc[start_idx:end_idx].reset_index(drop=True)
        return df

    def _set_data_auto(
        self, dat_file: DatFile, eps: float, min_samples: int, ndigits: int
    ) -> pd.DataFrame:
        file_data = dat_file.data.copy()
        file_data["cluster"] = label_clusters(
            file_data["Temperature (K)"], eps, min_samples
        )
        temps = unique_values(file_data["Temperature (K)"], eps, min_samples, ndigits)
        if self.temperature not in temps:
            raise self.TemperatureNotInDataError(
                f"Temperature {self.temperature} not in data in {dat_file}."
            )
        temperature_index = temps.index(self.temperature)
        cluster = file_data["cluster"].unique()[temperature_index]
        df = (
            file_data[file_data["cluster"] == cluster]
            .drop(columns=["cluster"])
            .reset_index(drop=True)
        )
        file_data.drop(columns=["cluster"], inplace=True)
        return df

    def simplified_data(
        self, segment: Literal["", "virgin", "forward", "reverse", "loop"] = ""
    ) -> pd.DataFrame:
        """Returns a simplified version of the data, removing unnecessary columns
        and renaming the remaining columns to more convenient names.

        Parameters
        ----------
        segment : {"", "virgin", "forward", "reverse", "loop"}, optional
            Return the selected segment. By default "", which returns the full data.

        Returns
        -------
        pd.DataFrame
            The simplified data. Contains the columns:
            - `"time"` in seconds
            - `"temperature"` in Kelvin
            - `"field"` in Oe
            - `"moment"`
            - `"moment_err"`
            - `"chi"`
            - `"chi_err"`
            - `"chi_t"`
            - `"chi_t_err"`

            Where units are not specified, they are determined by the scaling applied to the
            data (see `scaling` attribute).
        """
        full_df = self.select_segment(segment) if segment else self.data.copy()
        df = pd.DataFrame()
        df["time"] = full_df["Time Stamp (sec)"]
        df["temperature"] = full_df["Temperature (K)"]
        if self.field_correction_file:
            df["field"] = full_df["true_field"]
        else:
            df["field"] = full_df["Magnetic Field (Oe)"]
        if self.scaling:
            df["moment"] = full_df["moment"]
            df["moment_err"] = full_df["moment_err"]
            df["chi"] = full_df["chi"]
            df["chi_err"] = full_df["chi_err"]
            df["chi_t"] = full_df["chi_t"]
            df["chi_t_err"] = full_df["chi_t_err"]
        else:
            df["moment"] = full_df["uncorrected_moment"]
            df["moment_err"] = full_df["uncorrected_moment_err"]
            df["chi"] = df["moment"] / df["field"]
            df["chi_err"] = df["moment_err"] / df["field"]
            df["chi_t"] = df["chi"] * df["temperature"]
            df["chi_t_err"] = df["chi_err"] * df["temperature"]
        return df

    def scale_moment(
        self,
        mass: float = 0,
        eicosane_mass: float = 0,
        molecular_weight: float = 0,
        diamagnetic_correction: float = 0,
    ) -> None:
        """Adds the following columns to the `DataFrame` in the `data` attribute:
        `"moment"`, `"moment_err"`, `"chi"`, `"chi_err"`, `"chi_t"`, and
        `"chi_t_err"`. A record of what scaling was applied is added to the
        `scaling` attribute.

        See `magnetopy.experiments.utils.scale_dc_data` for more information.

        Parameters
        ----------
        mass : float, optional
            mg of sample, by default 0.
        eicosane_mass : float, optional
            mg of eicosane, by default 0.
        molecular_weight : float, optional
            Molecular weight of the material in g/mol, by default 0.
        diamagnetic_correction : float, optional
            Diamagnetic correction of the material in cm^3/mol, by default 0.
        """
        scale_dc_data(
            self,
            mass,
            eicosane_mass,
            molecular_weight,
            diamagnetic_correction,
        )

    def correct_field(self, field_correction_file: str | Path) -> None:
        """Applies a field correction to the data given data collected on the palladium
        standard with the same sequence as the current `MvsH` object. Adds a column
        called `"true_field"` to the `DataFrame` in the `data` attribute.

        See `magnetopy.cli.calibration_insall` for information on how to create a
        calibration directory.

        Parameters
        ----------
        field_correction_file : str | Path
            The name of the .dat file containing the Pd standard sequence, or if a
            configuration file containing calibration data is present, the name of the
            sequence in the configuration file.

        Raises
        ------
        self.FieldCorrectionError
            The true field calibration requires that the sequences of both the
            M vs. H experiment and the calibration experiment be exactly the same. This
            function only checks that they are the same length, and if they are not,
            raises this error.

        Notes
        -----
        As described in the Quantum Design application note[1], 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.

        References
        ----------
        [1] [Correcting for the Absolute Field Error using the Pd Standard](https://qdusa.com/siteDocs/appNotes/1500-021.pdf)
        """
        pd_mvsh = TrueFieldCorrection(field_correction_file)
        if len(pd_mvsh.data) != len(self.data):
            raise self.FieldCorrectionError(
                "The given Pd standard sequence does not have the same number of data "
                "points as the MvsH sequence."
            )
        self.field_correction_file = pd_mvsh.origin_file
        self.data["true_field"] = pd_mvsh.data["true_field"]
        self.field_range = self._determine_field_range()

    def _determine_field_range(self) -> tuple[float, float]:
        simplified_data = self.simplified_data()
        return simplified_data["field"].min(), simplified_data["field"].max()

    @property
    def virgin(self) -> pd.DataFrame:
        return self.select_segment("virgin")

    @property
    def forward(self) -> pd.DataFrame:
        return self.select_segment("forward")

    @property
    def reverse(self) -> pd.DataFrame:
        return self.select_segment("reverse")

    @property
    def loop(self) -> pd.DataFrame:
        return self.select_segment("loop")

    def select_segment(
        self, segment: Literal["virgin", "forward", "reverse", "loop"]
    ) -> pd.DataFrame:
        """Returns the requested segment of the data, if it exists.

        Parameters
        ----------
        segment : {"virgin", "forward", "reverse", "loop"}
            The segment of the M vs. H data to return. "loop" refers to the combination
            of the forward and reverse scans.

        Returns
        -------
        pd.DataFrame
            The requested segment of the data.

        Raises
        ------
        self.SegmentError
            If the requested segment is not found in the data.
        """
        segment_starts = find_sequence_starts(
            self.data["Magnetic Field (Oe)"], self._field_fluctuation_tolerance
        )
        df = self.data.copy()
        requested_segment = None
        if len(segment_starts) == 3:
            # assume virgin -> reverse -> forward
            if segment == "virgin":
                requested_segment = df[
                    segment_starts[0] : segment_starts[1]
                ].reset_index(drop=True)
            elif segment == "reverse":
                requested_segment = df[
                    segment_starts[1] - 1 : segment_starts[2]
                ].reset_index(drop=True)
            elif segment == "forward":
                requested_segment = df[segment_starts[2] - 1 :].reset_index(drop=True)
            elif segment == "loop":
                requested_segment = df[segment_starts[1] - 1 :].reset_index(drop=True)
        elif len(segment_starts) == 2:
            if segment == "loop":
                requested_segment = df
            # check to see if it's forward -> reverse or reverse -> forward
            elif (
                df.at[segment_starts[0], "Magnetic Field (Oe)"]
                > df.at[segment_starts[1], "Magnetic Field (Oe)"]
            ):
                if segment == "reverse":
                    requested_segment = df[
                        segment_starts[0] : segment_starts[1]
                    ].reset_index(drop=True)
                elif segment == "forward":
                    requested_segment = df[segment_starts[1] - 1 :].reset_index(
                        drop=True
                    )
            else:
                if segment == "forward":
                    requested_segment = df[
                        segment_starts[0] : segment_starts[1]
                    ].reset_index(drop=True)
                elif segment == "reverse":
                    requested_segment = df[segment_starts[1] - 1 :].reset_index(
                        drop=True
                    )
        elif len(segment_starts) == 1:
            if segment == "loop":
                raise self.SegmentError(
                    "Full loop requested but only one segment found"
                )
            elif segment == "virgin":
                if abs(df.at[0, "Magnetic Field (Oe)"]) > 5:
                    raise self.SegmentError(
                        "Virgin scan requested but data does not start at zero field"
                    )
                requested_segment = df
            elif segment == "forward":
                if df.at[0, "Magnetic Field (Oe)"] > 0:
                    raise self.SegmentError(
                        "Forward scan requested but start field is greater than end field."
                    )
                requested_segment = df
            elif segment == "reverse":
                if df.at[0, "Magnetic Field (Oe)"] < 0:
                    raise self.SegmentError(
                        "Reverse scan requested but start field is less than end field."
                    )
                requested_segment = df
        else:
            raise self.SegmentError(
                f"Something went wrong. {len(segment_starts)} segments found"
            )
        if requested_segment is None:
            raise self.SegmentError(f"Sequence {segment} not found in data")
        return requested_segment

    def plot(
        self,
        normalized: bool = False,
        segment: str = "",
        color: str = "black",
        label: str | None = "auto",
        title: str = "",
        **kwargs,
    ) -> tuple[plt.Figure, plt.Axes]:
        """Plots the M vs. H data data.

        Parameters
        ----------
        normalized : bool, optional
            If `True`, the magnetization will be normalized to the maximum value, by
            default False.
        segment : {"", "virgin", "forward", "reverse", "loop"}, optional
            If a segment is given, only that segment will be plotted, by default "".
        color : str | list[str], optional
            The color of the plot, by default "auto". If "auto", the color will be black.
        label : str | list[str] | None, optional
            The labels to assign the `MvsH` object in the axes legend, by default "auto".
            If "auto", the label will be the `temperature` of the `MvsH` object.
        title : str, optional
            The title of the plot, by default "".
        **kwargs
            Keyword arguments mostly meant to affect the plot style. See
            `magnetopy.experiments.plot_utils.handle_options` for details.

        Returns
        -------
        tuple[plt.Figure, plt.Axes]
        """
        return plot_single_mvsh(
            self, normalized, segment, color, label, title, **kwargs
        )

    def plot_raw(
        self,
        segment: Literal["virgin", "forward", "reverse"] = "forward",
        scan: Literal[
            "up",
            "up_raw",
            "down",
            "down_raw",
            "processed",
        ] = "up",
        center: Literal[
            "free",
            "fixed",
        ] = "free",
        colors: tuple[str, str] = ("purple", "orange"),
        label: bool = True,
        title: str = "",
    ) -> tuple[plt.Figure, plt.Axes]:
        """Plots the raw voltage data for the requested segment.

        Parameters
        ----------
        segment : {"virgin", "forward", "reverse"}, optional
            The segment of the M vs. H data to plot, by default "forward"
        scan : Literal["up", "up_raw", "down", "down_raw", "procssed"], optional
            Which data to plot. `"up"` and `"down"` will plot the processed directional
            scans (which have been adjusted for drift and shifted to center the waveform
            around 0, but have not been fit), `"up_raw"` and `"down_raw"` will plot the raw
            voltages as the come straight off the SQUID, and `"processed"` will plot the
            processed data (which is the result of fitting the up and down scans). `"up"` by
            default.
        center : Literal["free", "fixed"], optional
            Only used if `scan` is `"processed"`; determines whether to plot the "Free C
            Fitted" or "Fixed C Fitted" data. `"free"` by default.
        colors : tuple[str, str], optional
            The (start, end) colors for the color gradient. `"purple"` and `"orange"` by
            default.
        label : bool, optional
            Default `True`. Whether to put labels on the plot for the initial and final
            scans.
        title : str, optional
            The title of the plot. `""` by default.

        Returns
        -------
        tuple[plt.Figure, plt.Axes]
        """
        return plot_raw(
            self.select_segment(segment), None, scan, center, colors, label, title
        )

    def plot_raw_residual(
        self,
        segment: Literal["virgin", "forward", "reverse"] = "forward",
        scan: Literal["up", "down"] = "up",
        center: Literal["free", "fixed"] = "free",
        colors: tuple[str, str] | None = None,
        label: bool = True,
        title: str = "",
    ) -> tuple[plt.Figure, plt.Axes]:
        """Plots the residual of the raw voltage data for the requested segment.

        Parameters
        ----------
        segment : {"virgin", "forward", "reverse"}, optional
            The segment of the M vs. H data to plot, by default "forward"
        scan : Literal["up", "down"], optional
            Which data to use in the residual calculation. `"up"` and `"down"` will use the
            processed directional scans (which have been adjusted for drift and shifted to
            center the waveform around 0, but have not been fit). `"up"` by default.
        center : Literal["free", "fixed"], optional
            Only used if `scan` is `"processed"`; determines whether to plot the "Free C
            Fitted" or "Fixed C Fitted" data. `"free"` by default.
        colors : tuple[str, str], optional
            The (start, end) colors for the color gradient. `"purple"` and `"orange"` by
            default.
        label : bool, optional
            Default `True`. Whether to put labels on the plot for the initial and final
            scans.
        title : str, optional
            The title of the plot. `""` by default.

        Returns
        -------
        tuple[plt.Figure, plt.Axes]
        """
        return plot_raw_residual(
            self.select_segment(segment), None, scan, center, colors, label, title
        )

    def as_dict(self) -> dict[str, Any]:
        """Returns a dictionary representation of the `MvsH` object.

        Returns
        -------
        dict[str, Any]
            Keys are: `"origin_file"`, `"temperature"`, `"field_range"`,
            `"field_correction_file"`, and `"scaling"`.
        """
        return {
            "_class_": self.__class__.__name__,
            "origin_file": self.origin_file,
            "temperature": self.temperature,
            "field_range": self.field_range,
            "field_correction_file": self.field_correction_file,
            "scaling": self.scaling,
        }

    @classmethod
    def get_all_in_file(
        cls,
        dat_file: str | Path | DatFile,
        eps: float = 0.001,
        min_samples: int = 10,
        ndigits: int = 0,
        parse_raw: bool = False,
    ) -> list[MvsH]:
        """Given a .dat file that contains one or more M vs. H experiments, returns a
        list of `MvsH` objects, one for each experiment.

        Parameters
        ----------
        dat_file : str | Path | DatFile
            The .dat file containing the data for the experiment.
        eps : float, optional
            See `magnetopy.parsing_utils.label_clusters` for details, by default 0.001
        min_samples : int, optional
            See `magnetopy.parsing_utils.label_clusters` for details, by default 10
        ndigits : int, optional
            See `magnetopy.parsing_utils.label_clusters` for details, by default 0
        parse_raw : bool, optional
            If `True` and there is a corresponding .rw.dat file, the raw data will be
            parsed and added to the `data` attribute. Defaults to `False`.

        Returns
        -------
        list[MvsH]
            A list of `MvsH` objects, one for each experiment in the .dat file, sorted
            by increasing temperature.
        """
        if not isinstance(dat_file, DatFile):
            dat_file = DatFile(Path(dat_file), parse_raw)
        if dat_file.comments:
            mvsh_objs = cls._get_all_mvsh_in_commented_file(dat_file)
        else:
            mvsh_objs = cls._get_all_mvsh_in_uncommented_file(
                dat_file,
                eps,
                min_samples,
                ndigits,
            )
        mvsh_objs.sort(key=lambda x: x.temperature)
        return mvsh_objs

    @classmethod
    def _get_all_mvsh_in_commented_file(cls, dat_file: DatFile) -> list[MvsH]:
        mvsh_objs = []
        for comment_list in dat_file.comments.values():
            # ignore other experiments
            if "mvsh" not in map(str.lower, comment_list):
                continue
            # one of the comments should be a number denoting the temperature
            # may also include a unit, e.g. "300 K"
            for comment in comment_list:
                if match := re.search(r"\d+", comment):
                    temp = float(match.group())
                    # check to see if the unit is C otherwise assume K
                    if "C" in comment:
                        temp += 273
                    mvsh_objs.append(cls(dat_file, temp))
        return mvsh_objs

    @classmethod
    def _get_all_mvsh_in_uncommented_file(
        cls,
        dat_file: DatFile,
        eps: float,
        min_samples: int,
        ndigits: int,
    ) -> list[MvsH]:
        file_data = dat_file.data
        file_data["cluster"] = label_clusters(
            file_data["Temperature (K)"], eps, min_samples
        )
        temps = unique_values(file_data["Temperature (K)"], eps, min_samples, ndigits)
        mvsh_objs = []
        for temp in temps:
            mvsh_objs.append(cls(dat_file, temp, eps=eps, min_samples=min_samples))
        return mvsh_objs

simplified_data(segment='')

Returns a simplified version of the data, removing unnecessary columns and renaming the remaining columns to more convenient names.

Parameters:

Name Type Description Default
segment Literal['', 'virgin', 'forward', 'reverse', 'loop']

Return the selected segment. By default "", which returns the full data.

""

Returns:

Type Description
pd.DataFrame

The simplified data. Contains the columns: - "time" in seconds - "temperature" in Kelvin - "field" in Oe - "moment" - "moment_err" - "chi" - "chi_err" - "chi_t" - "chi_t_err"

Where units are not specified, they are determined by the scaling applied to the data (see scaling attribute).

Source code in magnetopy\experiments\mvsh.py
def simplified_data(
    self, segment: Literal["", "virgin", "forward", "reverse", "loop"] = ""
) -> pd.DataFrame:
    """Returns a simplified version of the data, removing unnecessary columns
    and renaming the remaining columns to more convenient names.

    Parameters
    ----------
    segment : {"", "virgin", "forward", "reverse", "loop"}, optional
        Return the selected segment. By default "", which returns the full data.

    Returns
    -------
    pd.DataFrame
        The simplified data. Contains the columns:
        - `"time"` in seconds
        - `"temperature"` in Kelvin
        - `"field"` in Oe
        - `"moment"`
        - `"moment_err"`
        - `"chi"`
        - `"chi_err"`
        - `"chi_t"`
        - `"chi_t_err"`

        Where units are not specified, they are determined by the scaling applied to the
        data (see `scaling` attribute).
    """
    full_df = self.select_segment(segment) if segment else self.data.copy()
    df = pd.DataFrame()
    df["time"] = full_df["Time Stamp (sec)"]
    df["temperature"] = full_df["Temperature (K)"]
    if self.field_correction_file:
        df["field"] = full_df["true_field"]
    else:
        df["field"] = full_df["Magnetic Field (Oe)"]
    if self.scaling:
        df["moment"] = full_df["moment"]
        df["moment_err"] = full_df["moment_err"]
        df["chi"] = full_df["chi"]
        df["chi_err"] = full_df["chi_err"]
        df["chi_t"] = full_df["chi_t"]
        df["chi_t_err"] = full_df["chi_t_err"]
    else:
        df["moment"] = full_df["uncorrected_moment"]
        df["moment_err"] = full_df["uncorrected_moment_err"]
        df["chi"] = df["moment"] / df["field"]
        df["chi_err"] = df["moment_err"] / df["field"]
        df["chi_t"] = df["chi"] * df["temperature"]
        df["chi_t_err"] = df["chi_err"] * df["temperature"]
    return df

scale_moment(mass=0, eicosane_mass=0, molecular_weight=0, diamagnetic_correction=0)

Adds the following columns to the DataFrame in the data attribute: "moment", "moment_err", "chi", "chi_err", "chi_t", and "chi_t_err". A record of what scaling was applied is added to the scaling attribute.

See magnetopy.experiments.utils.scale_dc_data for more information.

Parameters:

Name Type Description Default
mass float

mg of sample, by default 0.

0
eicosane_mass float

mg of eicosane, by default 0.

0
molecular_weight float

Molecular weight of the material in g/mol, by default 0.

0
diamagnetic_correction float

Diamagnetic correction of the material in cm^3/mol, by default 0.

0
Source code in magnetopy\experiments\mvsh.py
def scale_moment(
    self,
    mass: float = 0,
    eicosane_mass: float = 0,
    molecular_weight: float = 0,
    diamagnetic_correction: float = 0,
) -> None:
    """Adds the following columns to the `DataFrame` in the `data` attribute:
    `"moment"`, `"moment_err"`, `"chi"`, `"chi_err"`, `"chi_t"`, and
    `"chi_t_err"`. A record of what scaling was applied is added to the
    `scaling` attribute.

    See `magnetopy.experiments.utils.scale_dc_data` for more information.

    Parameters
    ----------
    mass : float, optional
        mg of sample, by default 0.
    eicosane_mass : float, optional
        mg of eicosane, by default 0.
    molecular_weight : float, optional
        Molecular weight of the material in g/mol, by default 0.
    diamagnetic_correction : float, optional
        Diamagnetic correction of the material in cm^3/mol, by default 0.
    """
    scale_dc_data(
        self,
        mass,
        eicosane_mass,
        molecular_weight,
        diamagnetic_correction,
    )

correct_field(field_correction_file)

Applies a field correction to the data given data collected on the palladium standard with the same sequence as the current MvsH object. Adds a column called "true_field" to the DataFrame in the data attribute.

See magnetopy.cli.calibration_insall for information on how to create a calibration directory.

Parameters:

Name Type Description Default
field_correction_file str | Path

The name of the .dat file containing the Pd standard sequence, or if a configuration file containing calibration data is present, the name of the sequence in the configuration file.

required

Raises:

Type Description
self.FieldCorrectionError

The true field calibration requires that the sequences of both the M vs. H experiment and the calibration experiment be exactly the same. This function only checks that they are the same length, and if they are not, raises this error.

Notes

As described in the Quantum Design application note[1], 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.

References

[1] Correcting for the Absolute Field Error using the Pd Standard

Source code in magnetopy\experiments\mvsh.py
def correct_field(self, field_correction_file: str | Path) -> None:
    """Applies a field correction to the data given data collected on the palladium
    standard with the same sequence as the current `MvsH` object. Adds a column
    called `"true_field"` to the `DataFrame` in the `data` attribute.

    See `magnetopy.cli.calibration_insall` for information on how to create a
    calibration directory.

    Parameters
    ----------
    field_correction_file : str | Path
        The name of the .dat file containing the Pd standard sequence, or if a
        configuration file containing calibration data is present, the name of the
        sequence in the configuration file.

    Raises
    ------
    self.FieldCorrectionError
        The true field calibration requires that the sequences of both the
        M vs. H experiment and the calibration experiment be exactly the same. This
        function only checks that they are the same length, and if they are not,
        raises this error.

    Notes
    -----
    As described in the Quantum Design application note[1], 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.

    References
    ----------
    [1] [Correcting for the Absolute Field Error using the Pd Standard](https://qdusa.com/siteDocs/appNotes/1500-021.pdf)
    """
    pd_mvsh = TrueFieldCorrection(field_correction_file)
    if len(pd_mvsh.data) != len(self.data):
        raise self.FieldCorrectionError(
            "The given Pd standard sequence does not have the same number of data "
            "points as the MvsH sequence."
        )
    self.field_correction_file = pd_mvsh.origin_file
    self.data["true_field"] = pd_mvsh.data["true_field"]
    self.field_range = self._determine_field_range()

select_segment(segment)

Returns the requested segment of the data, if it exists.

Parameters:

Name Type Description Default
segment Literal['virgin', 'forward', 'reverse', 'loop']

The segment of the M vs. H data to return. "loop" refers to the combination of the forward and reverse scans.

"virgin"

Returns:

Type Description
pd.DataFrame

The requested segment of the data.

Raises:

Type Description
self.SegmentError

If the requested segment is not found in the data.

Source code in magnetopy\experiments\mvsh.py
def select_segment(
    self, segment: Literal["virgin", "forward", "reverse", "loop"]
) -> pd.DataFrame:
    """Returns the requested segment of the data, if it exists.

    Parameters
    ----------
    segment : {"virgin", "forward", "reverse", "loop"}
        The segment of the M vs. H data to return. "loop" refers to the combination
        of the forward and reverse scans.

    Returns
    -------
    pd.DataFrame
        The requested segment of the data.

    Raises
    ------
    self.SegmentError
        If the requested segment is not found in the data.
    """
    segment_starts = find_sequence_starts(
        self.data["Magnetic Field (Oe)"], self._field_fluctuation_tolerance
    )
    df = self.data.copy()
    requested_segment = None
    if len(segment_starts) == 3:
        # assume virgin -> reverse -> forward
        if segment == "virgin":
            requested_segment = df[
                segment_starts[0] : segment_starts[1]
            ].reset_index(drop=True)
        elif segment == "reverse":
            requested_segment = df[
                segment_starts[1] - 1 : segment_starts[2]
            ].reset_index(drop=True)
        elif segment == "forward":
            requested_segment = df[segment_starts[2] - 1 :].reset_index(drop=True)
        elif segment == "loop":
            requested_segment = df[segment_starts[1] - 1 :].reset_index(drop=True)
    elif len(segment_starts) == 2:
        if segment == "loop":
            requested_segment = df
        # check to see if it's forward -> reverse or reverse -> forward
        elif (
            df.at[segment_starts[0], "Magnetic Field (Oe)"]
            > df.at[segment_starts[1], "Magnetic Field (Oe)"]
        ):
            if segment == "reverse":
                requested_segment = df[
                    segment_starts[0] : segment_starts[1]
                ].reset_index(drop=True)
            elif segment == "forward":
                requested_segment = df[segment_starts[1] - 1 :].reset_index(
                    drop=True
                )
        else:
            if segment == "forward":
                requested_segment = df[
                    segment_starts[0] : segment_starts[1]
                ].reset_index(drop=True)
            elif segment == "reverse":
                requested_segment = df[segment_starts[1] - 1 :].reset_index(
                    drop=True
                )
    elif len(segment_starts) == 1:
        if segment == "loop":
            raise self.SegmentError(
                "Full loop requested but only one segment found"
            )
        elif segment == "virgin":
            if abs(df.at[0, "Magnetic Field (Oe)"]) > 5:
                raise self.SegmentError(
                    "Virgin scan requested but data does not start at zero field"
                )
            requested_segment = df
        elif segment == "forward":
            if df.at[0, "Magnetic Field (Oe)"] > 0:
                raise self.SegmentError(
                    "Forward scan requested but start field is greater than end field."
                )
            requested_segment = df
        elif segment == "reverse":
            if df.at[0, "Magnetic Field (Oe)"] < 0:
                raise self.SegmentError(
                    "Reverse scan requested but start field is less than end field."
                )
            requested_segment = df
    else:
        raise self.SegmentError(
            f"Something went wrong. {len(segment_starts)} segments found"
        )
    if requested_segment is None:
        raise self.SegmentError(f"Sequence {segment} not found in data")
    return requested_segment

plot(normalized=False, segment='', color='black', label='auto', title='', **kwargs)

Plots the M vs. H data data.

Parameters:

Name Type Description Default
normalized bool

If True, the magnetization will be normalized to the maximum value, by default False.

False
segment str

If a segment is given, only that segment will be plotted, by default "".

""
color str | list[str]

The color of the plot, by default "auto". If "auto", the color will be black.

'black'
label str | list[str] | None

The labels to assign the MvsH object in the axes legend, by default "auto". If "auto", the label will be the temperature of the MvsH object.

'auto'
title str

The title of the plot, by default "".

''
**kwargs

Keyword arguments mostly meant to affect the plot style. See magnetopy.experiments.plot_utils.handle_options for details.

{}

Returns:

Type Description
tuple[plt.Figure, plt.Axes]
Source code in magnetopy\experiments\mvsh.py
def plot(
    self,
    normalized: bool = False,
    segment: str = "",
    color: str = "black",
    label: str | None = "auto",
    title: str = "",
    **kwargs,
) -> tuple[plt.Figure, plt.Axes]:
    """Plots the M vs. H data data.

    Parameters
    ----------
    normalized : bool, optional
        If `True`, the magnetization will be normalized to the maximum value, by
        default False.
    segment : {"", "virgin", "forward", "reverse", "loop"}, optional
        If a segment is given, only that segment will be plotted, by default "".
    color : str | list[str], optional
        The color of the plot, by default "auto". If "auto", the color will be black.
    label : str | list[str] | None, optional
        The labels to assign the `MvsH` object in the axes legend, by default "auto".
        If "auto", the label will be the `temperature` of the `MvsH` object.
    title : str, optional
        The title of the plot, by default "".
    **kwargs
        Keyword arguments mostly meant to affect the plot style. See
        `magnetopy.experiments.plot_utils.handle_options` for details.

    Returns
    -------
    tuple[plt.Figure, plt.Axes]
    """
    return plot_single_mvsh(
        self, normalized, segment, color, label, title, **kwargs
    )

plot_raw(segment='forward', scan='up', center='free', colors=('purple', 'orange'), label=True, title='')

Plots the raw voltage data for the requested segment.

Parameters:

Name Type Description Default
segment Literal['virgin', 'forward', 'reverse']

The segment of the M vs. H data to plot, by default "forward"

"virgin"
scan Literal['up', 'up_raw', 'down', 'down_raw', 'procssed']

Which data to plot. "up" and "down" will plot the processed directional scans (which have been adjusted for drift and shifted to center the waveform around 0, but have not been fit), "up_raw" and "down_raw" will plot the raw voltages as the come straight off the SQUID, and "processed" will plot the processed data (which is the result of fitting the up and down scans). "up" by default.

'up'
center Literal['free', 'fixed']

Only used if scan is "processed"; determines whether to plot the "Free C Fitted" or "Fixed C Fitted" data. "free" by default.

'free'
colors tuple[str, str]

The (start, end) colors for the color gradient. "purple" and "orange" by default.

('purple', 'orange')
label bool

Default True. Whether to put labels on the plot for the initial and final scans.

True
title str

The title of the plot. "" by default.

''

Returns:

Type Description
tuple[plt.Figure, plt.Axes]
Source code in magnetopy\experiments\mvsh.py
def plot_raw(
    self,
    segment: Literal["virgin", "forward", "reverse"] = "forward",
    scan: Literal[
        "up",
        "up_raw",
        "down",
        "down_raw",
        "processed",
    ] = "up",
    center: Literal[
        "free",
        "fixed",
    ] = "free",
    colors: tuple[str, str] = ("purple", "orange"),
    label: bool = True,
    title: str = "",
) -> tuple[plt.Figure, plt.Axes]:
    """Plots the raw voltage data for the requested segment.

    Parameters
    ----------
    segment : {"virgin", "forward", "reverse"}, optional
        The segment of the M vs. H data to plot, by default "forward"
    scan : Literal["up", "up_raw", "down", "down_raw", "procssed"], optional
        Which data to plot. `"up"` and `"down"` will plot the processed directional
        scans (which have been adjusted for drift and shifted to center the waveform
        around 0, but have not been fit), `"up_raw"` and `"down_raw"` will plot the raw
        voltages as the come straight off the SQUID, and `"processed"` will plot the
        processed data (which is the result of fitting the up and down scans). `"up"` by
        default.
    center : Literal["free", "fixed"], optional
        Only used if `scan` is `"processed"`; determines whether to plot the "Free C
        Fitted" or "Fixed C Fitted" data. `"free"` by default.
    colors : tuple[str, str], optional
        The (start, end) colors for the color gradient. `"purple"` and `"orange"` by
        default.
    label : bool, optional
        Default `True`. Whether to put labels on the plot for the initial and final
        scans.
    title : str, optional
        The title of the plot. `""` by default.

    Returns
    -------
    tuple[plt.Figure, plt.Axes]
    """
    return plot_raw(
        self.select_segment(segment), None, scan, center, colors, label, title
    )

plot_raw_residual(segment='forward', scan='up', center='free', colors=None, label=True, title='')

Plots the residual of the raw voltage data for the requested segment.

Parameters:

Name Type Description Default
segment Literal['virgin', 'forward', 'reverse']

The segment of the M vs. H data to plot, by default "forward"

"virgin"
scan Literal['up', 'down']

Which data to use in the residual calculation. "up" and "down" will use the processed directional scans (which have been adjusted for drift and shifted to center the waveform around 0, but have not been fit). "up" by default.

'up'
center Literal['free', 'fixed']

Only used if scan is "processed"; determines whether to plot the "Free C Fitted" or "Fixed C Fitted" data. "free" by default.

'free'
colors tuple[str, str]

The (start, end) colors for the color gradient. "purple" and "orange" by default.

None
label bool

Default True. Whether to put labels on the plot for the initial and final scans.

True
title str

The title of the plot. "" by default.

''

Returns:

Type Description
tuple[plt.Figure, plt.Axes]
Source code in magnetopy\experiments\mvsh.py
def plot_raw_residual(
    self,
    segment: Literal["virgin", "forward", "reverse"] = "forward",
    scan: Literal["up", "down"] = "up",
    center: Literal["free", "fixed"] = "free",
    colors: tuple[str, str] | None = None,
    label: bool = True,
    title: str = "",
) -> tuple[plt.Figure, plt.Axes]:
    """Plots the residual of the raw voltage data for the requested segment.

    Parameters
    ----------
    segment : {"virgin", "forward", "reverse"}, optional
        The segment of the M vs. H data to plot, by default "forward"
    scan : Literal["up", "down"], optional
        Which data to use in the residual calculation. `"up"` and `"down"` will use the
        processed directional scans (which have been adjusted for drift and shifted to
        center the waveform around 0, but have not been fit). `"up"` by default.
    center : Literal["free", "fixed"], optional
        Only used if `scan` is `"processed"`; determines whether to plot the "Free C
        Fitted" or "Fixed C Fitted" data. `"free"` by default.
    colors : tuple[str, str], optional
        The (start, end) colors for the color gradient. `"purple"` and `"orange"` by
        default.
    label : bool, optional
        Default `True`. Whether to put labels on the plot for the initial and final
        scans.
    title : str, optional
        The title of the plot. `""` by default.

    Returns
    -------
    tuple[plt.Figure, plt.Axes]
    """
    return plot_raw_residual(
        self.select_segment(segment), None, scan, center, colors, label, title
    )

as_dict()

Returns a dictionary representation of the MvsH object.

Returns:

Type Description
dict[str, Any]

Keys are: "origin_file", "temperature", "field_range", "field_correction_file", and "scaling".

Source code in magnetopy\experiments\mvsh.py
def as_dict(self) -> dict[str, Any]:
    """Returns a dictionary representation of the `MvsH` object.

    Returns
    -------
    dict[str, Any]
        Keys are: `"origin_file"`, `"temperature"`, `"field_range"`,
        `"field_correction_file"`, and `"scaling"`.
    """
    return {
        "_class_": self.__class__.__name__,
        "origin_file": self.origin_file,
        "temperature": self.temperature,
        "field_range": self.field_range,
        "field_correction_file": self.field_correction_file,
        "scaling": self.scaling,
    }

get_all_in_file(dat_file, eps=0.001, min_samples=10, ndigits=0, parse_raw=False) classmethod

Given a .dat file that contains one or more M vs. H experiments, returns a list of MvsH objects, one for each experiment.

Parameters:

Name Type Description Default
dat_file str | Path | DatFile

The .dat file containing the data for the experiment.

required
eps float

See magnetopy.parsing_utils.label_clusters for details, by default 0.001

0.001
min_samples int

See magnetopy.parsing_utils.label_clusters for details, by default 10

10
ndigits int

See magnetopy.parsing_utils.label_clusters for details, by default 0

0
parse_raw bool

If True and there is a corresponding .rw.dat file, the raw data will be parsed and added to the data attribute. Defaults to False.

False

Returns:

Type Description
list[MvsH]

A list of MvsH objects, one for each experiment in the .dat file, sorted by increasing temperature.

Source code in magnetopy\experiments\mvsh.py
@classmethod
def get_all_in_file(
    cls,
    dat_file: str | Path | DatFile,
    eps: float = 0.001,
    min_samples: int = 10,
    ndigits: int = 0,
    parse_raw: bool = False,
) -> list[MvsH]:
    """Given a .dat file that contains one or more M vs. H experiments, returns a
    list of `MvsH` objects, one for each experiment.

    Parameters
    ----------
    dat_file : str | Path | DatFile
        The .dat file containing the data for the experiment.
    eps : float, optional
        See `magnetopy.parsing_utils.label_clusters` for details, by default 0.001
    min_samples : int, optional
        See `magnetopy.parsing_utils.label_clusters` for details, by default 10
    ndigits : int, optional
        See `magnetopy.parsing_utils.label_clusters` for details, by default 0
    parse_raw : bool, optional
        If `True` and there is a corresponding .rw.dat file, the raw data will be
        parsed and added to the `data` attribute. Defaults to `False`.

    Returns
    -------
    list[MvsH]
        A list of `MvsH` objects, one for each experiment in the .dat file, sorted
        by increasing temperature.
    """
    if not isinstance(dat_file, DatFile):
        dat_file = DatFile(Path(dat_file), parse_raw)
    if dat_file.comments:
        mvsh_objs = cls._get_all_mvsh_in_commented_file(dat_file)
    else:
        mvsh_objs = cls._get_all_mvsh_in_uncommented_file(
            dat_file,
            eps,
            min_samples,
            ndigits,
        )
    mvsh_objs.sort(key=lambda x: x.temperature)
    return mvsh_objs

TrueFieldCorrection

Bases: MvsH

A special MvsH class for handling the palladium standard calibration data used to correct the magnetic field for flux trapping. Unlikely to be used directly by the user, and instead will be called from the correct_field method of the MvsH class.

Parameters:

Name Type Description Default
sequence str | Path

This could be a path to a .dat file containing the Pd standard sequence, or if a configuration file containing calibration data is present, the name of the sequence in the configuration file.

required

See Also

magnetopy.cli.calibration_install

Notes

As described in the Quantum Design application note[1], 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. Provided 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.

References

[1] Correcting for the Absolute Field Error using the Pd Standard

Source code in magnetopy\experiments\mvsh.py
class TrueFieldCorrection(MvsH):
    """A special `MvsH` class for handling the palladium standard calibration data
    used to correct the magnetic field for flux trapping. Unlikely to be used directly
    by the user, and instead will be called from the `correct_field` method of the
    `MvsH` class.

    Parameters
    ----------
    sequence : str | Path
        This could be a path to a .dat file containing the Pd standard sequence, or if
        a configuration file containing calibration data is present, the name of the
        sequence in the configuration file.

    See Also
    --------
    magnetopy.cli.calibration_install

    Notes
    -----
    As described in the Quantum Design application note[1], 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. Provided 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.

    References
    ----------
    [1] [Correcting for the Absolute Field Error using the Pd Standard](https://qdusa.com/siteDocs/appNotes/1500-021.pdf)
    """

    def __init__(self, sequence: str | Path):
        dat_file = self._get_dat_file(sequence)
        super().__init__(dat_file)
        self.pd_mass = self._get_mass(dat_file)  # mass of the Pd standard in mg
        self._add_true_field()

    def _get_dat_file(self, sequence: str) -> DatFile:
        if Path(sequence).is_file():
            return DatFile(sequence)
        mp_cal = Path().home() / ".magnetopy/calibration"
        if (Path(sequence).suffix == ".dat") and (
            mp_cal / "calibration_files" / sequence
        ).is_file():
            return DatFile(mp_cal / "calibration_files" / sequence)
        with open(mp_cal / "calibration.json", "r", encoding="utf-8") as f:
            cal_json = json.load(f)
        if sequence in cal_json["mvsh"]:
            seq_dat = cal_json["mvsh"][sequence]
            return DatFile(mp_cal / "calibration_files" / seq_dat)
        raise FileNotFoundError(
            f"Could not find the requested sequence: {sequence}. "
            "TrueFieldCorrection requires either the name of a sequence listed in "
            f"{mp_cal / 'calibration.json'}, the name of a .dat file in "
            f"{mp_cal / 'calibration_files'}, or the path to a .dat file."
        )

    @staticmethod
    def _get_mass(dat_file: DatFile) -> float:
        for line in dat_file.header:
            category = line[0]
            if category != "INFO":
                continue
            info = line[2]
            if info == "SAMPLE_MASS":
                return float(line[1])
        raise ValueError("Could not find the sample mass in the .dat file header.")

    def _add_true_field(self):
        chi_g = 5.25e-6  # emu Oe / g
        self.data["true_field"] = self.data["uncorrected_moment"] / (
            chi_g * self.pd_mass * 1e-3
        )

plot_mvsh(mvsh, normalized=False, segment='', colors='auto', labels='auto', title='', **kwargs)

Plots either a single M vs. H experiment or several on the same axes.

Parameters:

Name Type Description Default
mvsh MvsH | list[MvsH]

The data to plot given as a single or list of MvsH objects.

required
normalized bool

If True, the magnetization will be normalized to the maximum value, by default False.

False
segment Literal['', 'virgin', 'forward', 'reverse', 'loop']

If a segment is given, only that segment will be plotted, by default "".

""
colors str | list[str]

A list of colors corresponding to the MvsH objects in mvsh, by default "auto". If "auto" and mvsh is a single MvsH object, the color will be black. If "auto" and mvsh is a list of MvsH objects with different temperatures, the colors will be a linear gradient from blue to red. If "auto" and mvsh is a list of MvsH objects with the same temperature, the colors will be the default matplotlib colors.

'auto'
labels str | list[str] | None

The labels to assign the MvsH objects in the axes legend, by default "auto". If "auto", the labels will be the temperature of the MvsH objects.

'auto'
title str

The title of the plot, by default "".

''
**kwargs

Keyword arguments mostly meant to affect the plot style. See magnetopy.experiments.plot_utils.handle_options for details.

{}

Returns:

Type Description
tuple[plt.Figure, plt.Axes]
Source code in magnetopy\experiments\mvsh.py
def plot_mvsh(
    mvsh: MvsH | list[MvsH],
    normalized: bool = False,
    segment: Literal["", "virgin", "forward", "reverse", "loop"] = "",
    colors: str | list[str] = "auto",
    labels: str | list[str] | None = "auto",
    title: str = "",
    **kwargs,
) -> tuple[plt.Figure, plt.Axes]:
    """Plots either a single M vs. H experiment or several on the same axes.

    Parameters
    ----------
    mvsh : MvsH | list[MvsH]
        The data to plot given as a single or list of `MvsH` objects.
    normalized : bool, optional
        If `True`, the magnetization will be normalized to the maximum value, by
        default False.
    segment : {"", "virgin", "forward", "reverse", "loop"}, optional
        If a segment is given, only that segment will be plotted, by default "".
    colors : str | list[str], optional
        A list of colors corresponding to the `MvsH` objects in `mvsh`, by default
        "auto". If "auto" and `mvsh` is a single `MvsH` object, the color will be
        black. If "auto" and `mvsh` is a list of `MvsH` objects with different
        temperatures, the colors will be a linear gradient from blue to red. If
        "auto" and `mvsh` is a list of `MvsH` objects with the same temperature, the
        colors will be the default `matplotlib` colors.
    labels : str | list[str] | None, optional
        The labels to assign the `MvsH` objects in the axes legend, by default "auto".
        If "auto", the labels will be the `temperature` of the `MvsH` objects.
    title : str, optional
        The title of the plot, by default "".
    **kwargs
        Keyword arguments mostly meant to affect the plot style. See
        `magnetopy.experiments.plot_utils.handle_options` for details.

    Returns
    -------
    tuple[plt.Figure, plt.Axes]
    """
    if isinstance(mvsh, list) and len(mvsh) == 1:
        mvsh = mvsh[0]
    if isinstance(mvsh, MvsH):
        if isinstance(colors, list) or isinstance(labels, list):
            raise ValueError(
                "If plotting a single MvsH, `colors` and `labels` must be a single value"
            )
        return plot_single_mvsh(
            mvsh=mvsh,
            normalized=normalized,
            segment=segment,
            color=colors,
            label=labels,
            title=title,
            **kwargs,
        )
    if colors != "auto" and not isinstance(colors, list):
        raise ValueError(
            "If plotting multiple MvsH, `colors` must be a list or 'auto'."
        )
    if labels is not None and labels != "auto" and not isinstance(labels, list):
        raise ValueError(
            "If plotting multiple MvsH, `labels` must be a list or 'auto' or `None`."
        )
    return plot_multiple_mvsh(
        mvsh,
        normalized=normalized,
        segment=segment,
        colors=colors,
        labels=labels,
        title=title,
        **kwargs,
    )

plot_single_mvsh(mvsh, normalized=False, segment='', color='black', label='auto', title='', **kwargs)

Plots a single M vs. H experiment.

Parameters:

Name Type Description Default
mvsh MvsH

The data to plot given as a single MvsH object.

required
normalized bool

If True, the magnetization will be normalized to the maximum value, by default False.

False
segment str

If a segment is given, only that segment will be plotted, by default "".

""
color str | list[str]

The color of the plot, by default "auto". If "auto", the color will be black.

'black'
label str | list[str] | None

The labels to assign the MvsH object in the axes legend, by default "auto". If "auto", the label will be the temperature of the MvsH object.

'auto'
title str

The title of the plot, by default "".

''
**kwargs

Keyword arguments mostly meant to affect the plot style. See magnetopy.experiments.plot_utils.handle_options for details.

{}

Returns:

Type Description
tuple[plt.Figure, plt.Axes]
Source code in magnetopy\experiments\mvsh.py
def plot_single_mvsh(
    mvsh: MvsH,
    normalized: bool = False,
    segment: str = "",
    color: str = "black",
    label: str | None = "auto",
    title: str = "",
    **kwargs,
) -> tuple[plt.Figure, plt.Axes]:
    """Plots a single M vs. H experiment.

    Parameters
    ----------
    mvsh : MvsH
        The data to plot given as a single `MvsH` object.
    normalized : bool, optional
        If `True`, the magnetization will be normalized to the maximum value, by
        default False.
    segment : {"", "virgin", "forward", "reverse", "loop"}, optional
        If a segment is given, only that segment will be plotted, by default "".
    color : str | list[str], optional
        The color of the plot, by default "auto". If "auto", the color will be black.
    label : str | list[str] | None, optional
        The labels to assign the `MvsH` object in the axes legend, by default "auto".
        If "auto", the label will be the `temperature` of the `MvsH` object.
    title : str, optional
        The title of the plot, by default "".
    **kwargs
        Keyword arguments mostly meant to affect the plot style. See
        `magnetopy.experiments.plot_utils.handle_options` for details.

    Returns
    -------
    tuple[plt.Figure, plt.Axes]
    """
    options = handle_kwargs(**kwargs)

    color = "black" if color == "auto" else color

    fig, ax = plt.subplots()
    x = mvsh.simplified_data(segment)["field"] / 10000
    y = mvsh.simplified_data(segment)["moment"]
    y = y / y.max() if normalized else y
    if label is None:
        ax.plot(x, y, c=color)
    else:
        if label == "auto":
            label = f"{mvsh.temperature} K"
        ax.plot(x, y, c=color, label=label)

    ax.set_xlabel("Field (T)")
    if normalized:
        ax.set_ylabel("Normalized Magnetization")
    else:
        ylabel = get_ylabel("moment", mvsh.scaling)
        ax.set_ylabel(ylabel)

    handle_options(ax, options, label, title)

    force_aspect(ax)
    if options["save"]:
        plt.savefig(
            options["save"], dpi=300, bbox_inches="tight", facecolor="w", edgecolor="w"
        )
    return fig, ax

plot_multiple_mvsh(mvsh, normalized=False, segment='', colors='auto', labels=None, title='', **kwargs)

Plots several M vs. H experiment on the same axes.

Parameters:

Name Type Description Default
mvsh MvsH | list[MvsH]

The data to plot given as a list of MvsH objects.

required
normalized bool

If True, the magnetization will be normalized to the maximum value, by default False.

False
segment str

If a segment is given, only that segment will be plotted, by default "".

""
colors str | list[str]

A list of colors corresponding to the MvsH objects in mvsh, by default "auto". If "auto" and mvsh is a list of MvsH objects with different temperatures, the colors will be a linear gradient from blue to red. If "auto" and mvsh is a list of MvsH objects with the same temperature, the colors will be the default matplotlib colors.

'auto'
labels str | list[str] | None

The labels to assign the MvsH objects in the axes legend, by default "auto". If "auto", the labels will be the temperature of the MvsH objects.

None
title str

The title of the plot, by default "".

''
**kwargs

Keyword arguments mostly meant to affect the plot style. See magnetopy.experiments.plot_utils.handle_options for details.

{}

Returns:

Type Description
tuple[plt.Figure, plt.Axes]
Source code in magnetopy\experiments\mvsh.py
def plot_multiple_mvsh(
    mvsh: list[MvsH],
    normalized: bool = False,
    segment: str = "",
    colors: list[str] | Literal["auto"] = "auto",
    labels: list[str] | None = None,
    title: str = "",
    **kwargs,
) -> tuple[plt.Figure, plt.Axes]:
    """Plots several M vs. H experiment on the same axes.

    Parameters
    ----------
    mvsh : MvsH | list[MvsH]
        The data to plot given as a list of `MvsH` objects.
    normalized : bool, optional
        If `True`, the magnetization will be normalized to the maximum value, by
        default False.
    segment : {"", "virgin", "forward", "reverse", "loop"}, optional
        If a segment is given, only that segment will be plotted, by default "".
    colors : str | list[str], optional
        A list of colors corresponding to the `MvsH` objects in `mvsh`, by default
        "auto". If "auto" and `mvsh` is a list of `MvsH` objects with different
        temperatures, the colors will be a linear gradient from blue to red. If
        "auto" and `mvsh` is a list of `MvsH` objects with the same temperature, the
        colors will be the default `matplotlib` colors.
    labels : str | list[str] | None, optional
        The labels to assign the `MvsH` objects in the axes legend, by default "auto".
        If "auto", the labels will be the `temperature` of the `MvsH` objects.
    title : str, optional
        The title of the plot, by default "".
    **kwargs
        Keyword arguments mostly meant to affect the plot style. See
        `magnetopy.experiments.plot_utils.handle_options` for details.

    Returns
    -------
    tuple[plt.Figure, plt.Axes]
    """
    options = handle_kwargs(**kwargs)

    if colors == "auto":
        colors = default_colors(len(mvsh))
    if _check_if_variable_temperature(mvsh):
        mvsh.sort(key=lambda x: x.temperature)
        colors = linear_color_gradient("blue", "red", len(mvsh))
        if labels == "auto":
            labels = [f"{x.temperature} K" for x in mvsh]
    if labels is None:
        labels: list[None] = [None] * len(mvsh)

    fig, ax = plt.subplots()
    for m, color, label in zip(mvsh, colors, labels):
        x = m.simplified_data(segment)["field"] / 10000
        y = m.simplified_data(segment)["moment"]
        y = y / y.max() if normalized else y
        if label:
            ax.plot(x, y, c=color, label=label)
        else:
            ax.plot(x, y, c=color)

    ax.set_xlabel("Field (T)")
    if normalized:
        ax.set_ylabel("Normalized Magnetization")
    else:
        ylabel = get_ylabel("moment", mvsh[0].scaling)
        ax.set_ylabel(ylabel)

    handle_options(ax, options, labels[0], title)
    force_aspect(ax)
    if options["save"]:
        plt.savefig(
            options["save"], dpi=300, bbox_inches="tight", facecolor="w", edgecolor="w"
        )
    return fig, ax