• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

morganjwilliams / pyrolite / 11717258294

07 Nov 2024 05:36AM UTC coverage: 91.526% (+0.2%) from 91.367%
11717258294

push

github

morganjwilliams
Update templates tutorial

6232 of 6809 relevant lines covered (91.53%)

10.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.28
/pyrolite/util/classification.py
1
"""
2
Utilities for rock chemistry and mineral abundance classification.
3

4
Todo
5
-------
6

7
* Petrological classifiers: QAPF (aphanitic/phaneritic),
8
  gabbroic Pyroxene-Olivine-Plagioclase,
9
  ultramafic Olivine-Orthopyroxene-Clinopyroxene
10
"""
11

12
import json
12✔
13

14
import matplotlib.lines
12✔
15
import matplotlib.patches
12✔
16
import matplotlib.text
12✔
17
import numpy as np
12✔
18
import pandas as pd
12✔
19
from matplotlib.projections import get_projection_class
12✔
20

21
from .log import Handle
12✔
22
from .meta import (
12✔
23
    pyrolite_datafolder,
24
    sphinx_doi_link,
25
    subkwargs,
26
    update_docstring_references,
27
)
28
from .plot.axes import init_axes
12✔
29
from .plot.helpers import get_centroid, get_visual_center
12✔
30
from .plot.style import patchkwargs
12✔
31
from .plot.transform import tlr_to_xy
12✔
32

33
logger = Handle(__name__)
12✔
34

35

36
def _read_poly(poly):
12✔
37
    """
38
    Read points from a polygon, allowing ratio values to be specified.
39
    """
40

41
    def get_ratio(s):
12✔
42
        a, b = s.split("/")
12✔
43
        return float(a) / float(b)
12✔
44

45
    return [
12✔
46
        [
47
            get_ratio(c) if isinstance(c, str) and c.count("/") == 1 else float(c)
48
            for c in pt
49
        ]
50
        for pt in poly
51
    ]
52

53

54
class PolygonClassifier(object):
12✔
55
    """
56
    A classifier model built form a series of polygons defining specific classes.
57

58
    Parameters
59
    -----------
60
    name : :class:`str`
61
        A name for the classifier model.
62
    axes : :class:`dict`
63
        Mapping from plot axes to variables to be used for labels.
64
    fields : :class:`dict`
65
        Dictionary describing indiviudal polygons, with identifiers as keys and
66
        dictionaries containing 'name' and 'fields' items.
67
    scale : :class:`float`
68
        Default maximum scale for the axes. Typically 100 (wt%) or 1 (fractional).
69
    xlim : :class:`tuple`
70
        Default x-limits for this classifier for plotting.
71
    ylim : :class:`tuple`
72
        Default y-limits for this classifier for plotting.
73
    """
74

75
    def __init__(
12✔
76
        self,
77
        name=None,
78
        axes=None,
79
        fields=None,
80
        scale=1.0,
81
        transform=None,
82
        mode=None,
83
        **kwargs,
84
    ):
85
        self.default_scale = scale
12✔
86
        self._scale = self.default_scale
12✔
87
        self.lims = {
12✔
88
            k: v for (k, v) in kwargs.items() if ("lim" in k) and (len(k) == 4)
89
        }
90
        self.projection = None
12✔
91
        if transform is not None:
12✔
92
            if isinstance(transform, str):
12✔
93
                if transform.lower().startswith("tern"):
12✔
94
                    self.transform = tlr_to_xy
12✔
95
                    self.projection = "ternary"
12✔
96
                else:
97
                    raise NotImplementedError
×
98
            else:
99
                self.transform = transform
×
100
        else:
101
            self.transform = lambda x: x  # passthrough
12✔
102

103
        self.name = name
12✔
104
        self.axes = axes or {}
12✔
105

106
        # addition for multiple modes of one diagram
107
        # the diagram itself is assigned at instantiation time, so
108
        # to swap modes, another diagram would need to be created
109
        valid_modes = kwargs.pop("modes", None)  # should be a list of valid modes
12✔
110
        if mode is None:
12✔
111
            mode = "default"
12✔
112
        elif valid_modes is not None:
12✔
113
            if mode not in valid_modes:
12✔
114
                raise ValueError(
×
115
                    "{} is an invalid mode for {}. Valid modes: {}".format(
116
                        mode, self.__class__.__name__, ", ".join(valid_modes)
117
                    )
118
                )
119
        else:
120
            pass
121

122
        # check axes for ratios, addition/subtraction etc
123
        self.fields = fields or {}
12✔
124

125
        if mode in self.fields:
12✔
126
            self.fields = self.fields[mode]
12✔
127
        self.classes = list(self.fields.keys())
12✔
128

129
    def predict(self, X, data_scale=None):
12✔
130
        """
131
        Predict the classification of samples using the polygon-based classifier.
132

133
        Parameters
134
        -----------
135
        X : :class:`numpy.ndarray` | :class:`pandas.DataFrame`
136
            Data to classify.
137
        data_scale : :class:`float`
138
            Maximum scale for the data. Typically 100 (wt%) or 1 (fractional).
139

140
        Returns
141
        -------
142
        :class:`pandas.Series`
143
            Series containing classifer predictions. If a dataframe was input,
144
            it inherit the index.
145
        """
146
        classes = [k for (k, cfg) in self.fields.items() if cfg["poly"]]
12✔
147
        # transformed polys
148
        polys = [
12✔
149
            matplotlib.patches.Polygon(
150
                self.transform(_read_poly(self.fields[k]["poly"])), closed=True
151
            )
152
            for k in classes
153
        ]
154
        if isinstance(X, pd.DataFrame):
12✔
155
            # check whether the axes names are in the columns
156
            axes = self.axis_components
12✔
157
            idx = X.index
12✔
158
            X = X.loc[:, axes].values
12✔
159
        else:
160
            idx = np.arange(X.shape[0])
×
161

162
        out = pd.Series(index=idx, dtype="object")
12✔
163

164
        rescale_by = 1.0  # rescaling the data to fit the classifier scale
12✔
165
        if data_scale is not None:
12✔
166
            if not np.isclose(self.default_scale, data_scale):
12✔
167
                rescale_by = self.default_scale / data_scale
12✔
168

169
        X = self.transform(X) * rescale_by  # transformed X
12✔
170
        indexes = np.array([p.contains_points(X) for p in polys]).T
12✔
171
        notfound = np.logical_not(indexes.sum(axis=-1))
12✔
172
        outlist = list(map(lambda ix: classes[ix], np.argmax(indexes, axis=-1)))
12✔
173
        out.loc[:] = outlist
12✔
174
        out.loc[(notfound)] = "none"
12✔
175
        # for those which are none, we could check if they're on polygon boundaries
176
        # and assign to the closest centroid (e.g. for boundary points on axes)
177
        return out
12✔
178

179
    @property
12✔
180
    def axis_components(self):
12✔
181
        """
182
        Get the axis components used by the classifier.
183

184
        Returns
185
        -------
186
        :class:`tuple`
187
            Ordered names for axes used by the classifier.
188
        """
189
        return list(self.axes.values())
12✔
190

191
    def _add_polygons_to_axes(
12✔
192
        self,
193
        ax=None,
194
        fill=False,
195
        axes_scale=100.0,
196
        add_labels=False,
197
        which_labels="ID",
198
        which_ids=None,
199
        **kwargs,
200
    ):
201
        """
202
        Add the polygonal fields from the classifier to an axis.
203

204
        Parameters
205
        ----------
206
        ax : :class:`matplotlib.axes.Axes`
207
            Axis to add the polygons to.
208
        fill : :class:`bool`
209
            Whether to fill the polygons.
210
        axes_scale : :class:`float`
211
            Maximum scale for the axes. Typically 100 (for wt%) or 1 (fractional).
212
        add_labels : :class:`bool`
213
            Whether to add labels at polygon centroids.
214
        which_labels : :class:`str`
215
            Which data to use for field labels - field 'name' or 'ID'.
216
        which_ids : :class:`list`
217
            List of field IDs corresponding to the polygons to add to the axes object.
218
            (e.g. for TAS, ['F', 'T1'] to plot the Foidite and Trachyte fields).
219
            An empty list corresponds to plotting all the polygons.
220

221
        Returns
222
        --------
223
        ax : :class:`matplotlib.axes.Axes`
224

225
        Notes
226
        -----
227
        * Will rescale to the extent of the fields if limits not specified.
228
        * Will use IDs/keys for fields as labels if names not specified.
229
        """
230
        if ax is None:
12✔
231
            ax = init_axes(projection=self.projection, **kwargs)
12✔
232
        else:
233
            if self.projection:
12✔
234
                if not isinstance(ax, get_projection_class(self.projection)):
12✔
235
                    logger.warning(
×
236
                        "Projection of axis for {} should be {}.".format(
237
                            self.name or self.__class.__name__, self.projection
238
                        )
239
                    )
240
        rescale_by = 1.0
12✔
241
        if axes_scale is not None:  # rescale polygons to fit ax
12✔
242
            if not np.isclose(self.default_scale, axes_scale):
12✔
243
                rescale_by = axes_scale / self.default_scale
12✔
244

245
        pgns = []
12✔
246
        poly_config = patchkwargs(kwargs)
12✔
247
        poly_config["edgecolor"] = kwargs.get("edgecolor", kwargs.get("color", "k"))
12✔
248
        poly_config["zorder"] = poly_config.get("zorder", -1)
12✔
249
        if not fill:
12✔
250
            poly_config["facecolor"] = "none"
12✔
251
            poly_config.pop("color", None)
12✔
252

253
        use_keys = not which_labels.lower().startswith("name")
12✔
254

255
        if which_ids is None:
12✔
256
            which_ids = list(self.fields.keys())
12✔
257

258
        for k, cfg in self.fields.items():
12✔
259
            if cfg["poly"] and (k in which_ids):
12✔
260
                verts = self.transform(np.array(_read_poly(cfg["poly"]))) * rescale_by
12✔
261
                pg = matplotlib.patches.Polygon(
12✔
262
                    verts,
263
                    closed=True,
264
                    transform=ax.transAxes
265
                    if self.projection is not None
266
                    else ax.transData,
267
                    **poly_config,
268
                )
269
                pgns.append(pg)
12✔
270
                ax.add_patch(pg)
12✔
271
                if add_labels:
12✔
272
                    # try and get name, otherwise use ID
273
                    label = k if use_keys else cfg.get("name", k)
12✔
274
                    x, y = get_centroid(pg)
12✔
275
                    ax.annotate(
12✔
276
                        "\n".join(label.split()),
277
                        xy=(x, y),
278
                        ha="center",
279
                        va="center",
280
                        fontsize=kwargs.get("fontsize", 8),
281
                        xycoords=ax.transAxes
282
                        if self.projection is not None
283
                        else ax.transData,
284
                        **subkwargs(kwargs, ax.annotate),
285
                    )
286

287
        # if the axis has the default scaling, there's a good chance that it hasn't
288
        # been rescaled/rendered. We need to rescale to show the polygons.
289
        # for the moment we're only doing this for standard projections
290
        # todo: automatically find the relevant lim function,
291
        # such that e.g. ternary limits might be able to be specified?
292
        if self.projection is None:
12✔
293
            if np.allclose(ax.get_xlim(), [0, 1]) & np.allclose(ax.get_ylim(), [0, 1]):
12✔
294
                # collect verts from polygons
295
                _verts = np.vstack([p.get_path().vertices for p in pgns])
12✔
296
                ax.set(
12✔
297
                    xlim=np.array(self.lims["xlim"]) * rescale_by
298
                    if "xlim" in self.lims
299
                    else (np.nanmin(_verts[:, 0]), np.nanmax(_verts[:, 0])),
300
                    ylim=np.array(self.lims["ylim"]) * rescale_by
301
                    if "ylim" in self.lims
302
                    else (np.nanmin(_verts[:, 1]), np.nanmax(_verts[:, 1])),
303
                )
304

305
        return ax
12✔
306

307
    def add_to_axes(
12✔
308
        self,
309
        ax=None,
310
        fill=False,
311
        axes_scale=1.0,
312
        add_labels=False,
313
        which_labels="ID",
314
        which_ids=None,
315
        **kwargs,
316
    ):
317
        """
318
        Add the fields from the classifier to an axis.
319

320
        Parameters
321
        ----------
322
        ax : :class:`matplotlib.axes.Axes`
323
            Axis to add the polygons to.
324
        fill : :class:`bool`
325
            Whether to fill the polygons.
326
        axes_scale : :class:`float`
327
            Maximum scale for the axes. Typically 100 (for wt%) or 1 (fractional).
328
        add_labels : :class:`bool`
329
            Whether to add labels for the polygons.
330
        which_labels : :class:`str`
331
            Which data to use for field labels - field 'name' or 'ID'.
332
        which_ids : :class:`list`
333
            List of field IDs corresponding to the polygons to add to the axes object.
334
            (e.g. for TAS, ['F', 'T1'] to plot the Foidite and Trachyte fields).
335
            An empty list corresponds to plotting all the polygons.
336

337
        Returns
338
        --------
339
        ax : :class:`matplotlib.axes.Axes`
340
        """
341
        ax = init_axes(ax=ax, projection=self.projection)
12✔
342

343
        ax = self._add_polygons_to_axes(
12✔
344
            ax=ax,
345
            fill=fill,
346
            axes_scale=axes_scale,
347
            add_labels=add_labels,
348
            which_labels=which_labels,
349
            which_ids=which_ids,
350
            **kwargs,
351
        )
352
        if self.axes is not None:
12✔
353
            ax.set(**{"{}label".format(a): var for a, var in self.axes.items()})
12✔
354
        return ax
12✔
355

356

357
@update_docstring_references
12✔
358
class TAS(PolygonClassifier):
12✔
359
    """
360
    Total-alkali Silica Diagram classifier from Middlemost (1994) [#ref_1]_,
361
    a closed-polygon variant after Le Bas et al. (1992) [#ref_2]_.
362

363
    Parameters
364
    -----------
365
    name : :class:`str`
366
        A name for the classifier model.
367
    axes : :class:`list` | :class:`tuple`
368
        Names of the axes corresponding to the polygon coordinates.
369
    fields : :class:`dict`
370
        Dictionary describing indiviudal polygons, with identifiers as keys and
371
        dictionaries containing 'name' and 'fields' items.
372
    scale : :class:`float`
373
        Default maximum scale for the axes. Typically 100 (wt%) or 1 (fractional).
374
    xlim : :class:`tuple`
375
        Default x-limits for this classifier for plotting.
376
    ylim : :class:`tuple`
377
        Default y-limits for this classifier for plotting.
378
    which_model : :class:`str`
379
        The name of the model variant to use, if not Middlemost.
380

381
    References
382
    -----------
383
    .. [#ref_1] Middlemost, E. A. K. (1994).
384
                Naming materials in the magma/igneous rock system.
385
                Earth-Science Reviews, 37(3), 215–224.
386
                doi: {Middlemost1994}
387
    .. [#ref_2] Le Bas, M.J., Le Maitre, R.W., Woolley, A.R. (1992).
388
                The construction of the Total Alkali-Silica chemical
389
                classification of volcanic rocks.
390
                Mineralogy and Petrology 46, 1–22.
391
                doi: {LeBas1992}
392
    .. [#ref_3] Le Maitre, R.W. (2002). Igneous Rocks: A Classification and Glossary
393
                of Terms : Recommendations of International Union of Geological
394
                Sciences Subcommission on the Systematics of Igneous Rocks.
395
                Cambridge University Press, 236pp.
396
                doi: {LeMaitre2002}
397
    """
398

399
    def __init__(self, which_model=None, **kwargs):
12✔
400
        if which_model == "LeMaitre":
12✔
401
            src = (
12✔
402
                pyrolite_datafolder(subfolder="models") / "TAS" / "config_lemaitre.json"
403
            )
404
        elif which_model == "LeMaitreCombined":
12✔
405
            src = (
12✔
406
                pyrolite_datafolder(subfolder="models")
407
                / "TAS"
408
                / "config_lemaitre_combined.json"
409
            )
410
        else:
411
            # fallback to Middlemost
412
            src = pyrolite_datafolder(subfolder="models") / "TAS" / "config.json"
12✔
413

414
        with open(src, "r") as f:
12✔
415
            config = json.load(f)
12✔
416
        kw = dict(scale=100.0, xlim=[30, 90], ylim=[0, 20])
12✔
417
        kw.update(kwargs)
12✔
418
        poly_config = {**config, **kw}
12✔
419
        super().__init__(**poly_config)
12✔
420

421
    def add_to_axes(
12✔
422
        self,
423
        ax=None,
424
        fill=False,
425
        axes_scale=100.0,
426
        add_labels=False,
427
        which_labels="ID",
428
        which_ids=None,
429
        label_at_centroid=True,
430
        **kwargs,
431
    ):
432
        """
433
        Add the TAS fields from the classifier to an axis.
434

435
        Parameters
436
        ----------
437
        ax : :class:`matplotlib.axes.Axes`
438
            Axis to add the polygons to.
439
        fill : :class:`bool`
440
            Whether to fill the polygons.
441
        axes_scale : :class:`float`
442
            Maximum scale for the axes. Typically 100 (for wt%) or 1 (fractional).
443
        add_labels : :class:`bool`
444
            Whether to add labels for the polygons.
445
        which_labels : :class:`str`
446
            Which labels to add to the polygons (e.g. for TAS, 'volcanic', 'intrusive'
447
            or the field 'ID').
448
        which_ids : :class:`list`
449
            List of field IDs corresponding to the polygons to add to the axes object.
450
            (e.g. for TAS, ['F', 'T1'] to plot the Foidite and Trachyte fields).
451
            An empty list corresponds to plotting all the polygons.
452
        label_at_centroid : :class:`bool`
453
            Whether to label the fields at the centroid (True) or at the visual
454
            center of the field (False).
455

456
        Returns
457
        --------
458
        ax : :class:`matplotlib.axes.Axes`
459
        """
460
        # use and override the default add_to_axes
461
        # here we don't want to add the labels in the normal way, because there
462
        # are two sets - one for volcanic rocks and one for plutonic rocks
463
        ax = self._add_polygons_to_axes(
12✔
464
            ax=ax,
465
            fill=fill,
466
            axes_scale=axes_scale,
467
            add_labels=False,
468
            which_ids=which_ids,
469
            **kwargs,
470
        )
471

472
        if not label_at_centroid:
12✔
473
            # Calculate the effective vertical exaggeration that
474
            # produces nice positioning of labels. The true vertical
475
            # exaggeration is increased by a scale_factor because
476
            # the text labels are typically wider than they are long,
477
            # so we want to promote the labels
478
            # being placed at the widest part of the field.
479
            scale_factor = 1.5
12✔
480
            p = ax.transData.transform([[0.0, 0.0], [1.0, 1.0]])
12✔
481
            yx_scaling = (p[1][1] - p[0][1]) / (p[1][0] - p[0][0]) * scale_factor
12✔
482

483
        rescale_by = 1.0
12✔
484
        if axes_scale is not None:  # rescale polygons to fit ax
12✔
485
            if not np.isclose(self.default_scale, axes_scale):
12✔
486
                rescale_by = axes_scale / self.default_scale
×
487

488
        if which_ids is None:
12✔
489
            which_ids = list(self.fields.keys())
12✔
490

491
        if add_labels:
12✔
492
            for k, cfg in self.fields.items():
12✔
493
                if cfg["poly"] and (k in which_ids):
12✔
494
                    if which_labels.lower().startswith("id"):
12✔
495
                        label = k
12✔
496
                    elif which_labels.lower().startswith(
×
497
                        "volc"
498
                    ):  # use the volcanic name
499
                        label = cfg["name"][0]
×
500
                    elif which_labels.lower().startswith(
×
501
                        "intr"
502
                    ):  # use the intrusive name
503
                        label = cfg["name"][-1]
×
504
                    else:  # use the field identifier
505
                        raise NotImplementedError(
×
506
                            "Invalid specification for labels: {}; chose from {}".format(
507
                                which_labels, ", ".join(["volcanic", "intrusive", "ID"])
508
                            )
509
                        )
510
                    verts = np.array(_read_poly(cfg["poly"])) * rescale_by
12✔
511
                    _poly = matplotlib.patches.Polygon(verts)
12✔
512
                    if label_at_centroid:
12✔
513
                        x, y = get_centroid(_poly)
12✔
514
                    else:
515
                        x, y = get_visual_center(_poly, yx_scaling)
12✔
516
                    ax.annotate(
12✔
517
                        "\n".join(label.split()),
518
                        xy=(x, y),
519
                        ha="center",
520
                        va="center",
521
                        fontsize=kwargs.get("fontsize", 8),
522
                        **subkwargs(kwargs, ax.annotate),
523
                    )
524

525
        ax.set(xlabel="SiO$_2$", ylabel="Na$_2$O + K$_2$O")
12✔
526
        return ax
12✔
527

528

529
@update_docstring_references
12✔
530
class USDASoilTexture(PolygonClassifier):
12✔
531
    """
532
    United States Department of Agriculture Soil Texture classification model
533
    [#ref_1]_ [#ref_2]_.
534

535
    Parameters
536
    -----------
537
    name : :class:`str`
538
        A name for the classifier model.
539
    axes : :class:`list` | :class:`tuple`
540
        Names of the axes corresponding to the polygon coordinates.
541
    fields : :class:`dict`
542
        Dictionary describing indiviudal polygons, with identifiers as keys and
543
        dictionaries containing 'name' and 'fields' items.
544

545
    References
546
    -----------
547
    .. [#ref_1] Soil Science Division Staff (2017). Soil Survey Manual.
548
                C. Ditzler, K. Scheffe, and H.C. Monger (eds.).
549
                USDA Handbook 18. Government Printing Office, Washington, D.C.
550
    .. [#ref_2] Thien, Steve J. (1979). A Flow Diagram for Teaching
551
                Texture-by-Feel Analysis. Journal of Agronomic Education 8:54–55.
552
                doi: {Thien1979}
553
    """
554

555
    def __init__(self, **kwargs):
12✔
556
        src = (
12✔
557
            pyrolite_datafolder(subfolder="models") / "USDASoilTexture" / "config.json"
558
        )
559

560
        with open(src, "r") as f:
12✔
561
            config = json.load(f)
12✔
562

563
        poly_config = {**config, **kwargs, "transform": "ternary"}
12✔
564
        super().__init__(**poly_config)
12✔
565

566

567
@update_docstring_references
12✔
568
class QAP(PolygonClassifier):
12✔
569
    """
570
    IUGS QAP ternary classification
571
    [#ref_1]_ [#ref_2]_.
572

573
    Parameters
574
    -----------
575
    name : :class:`str`
576
        A name for the classifier model.
577
    axes : :class:`list` | :class:`tuple`
578
        Names of the axes corresponding to the polygon coordinates.
579
    fields : :class:`dict`
580
        Dictionary describing indiviudal polygons, with identifiers as keys and
581
        dictionaries containing 'name' and 'fields' items.
582

583
    References
584
    -----------
585
    .. [#ref_1] Streckeisen, A. (1974). Classification and nomenclature of plutonic
586
                rocks: recommendations of the IUGS subcommission on the systematics
587
                of Igneous Rocks. Geol Rundsch 63, 773–786.
588
                doi: {Streckeisen1974}
589
    .. [#ref_2] Le Maitre,R.W. (2002). Igneous Rocks: A Classification and Glossary
590
                of Terms : Recommendations of International Union of Geological
591
                Sciences Subcommission on the Systematics of Igneous Rocks.
592
                Cambridge University Press, 236pp
593
                doi: {LeMaitre2002}
594
    """
595

596
    def __init__(self, **kwargs):
12✔
597
        src = pyrolite_datafolder(subfolder="models") / "QAP" / "config.json"
12✔
598

599
        with open(src, "r") as f:
12✔
600
            config = json.load(f)
12✔
601

602
        poly_config = {**config, **kwargs, "transform": "ternary"}
12✔
603
        super().__init__(**poly_config)
12✔
604

605

606
@update_docstring_references
12✔
607
class FeldsparTernary(PolygonClassifier):
12✔
608
    """
609
    Simplified feldspar diagram classifier, based on a version printed in the
610
    second edition of 'An Introduction to the Rock Forming Minerals' (Deer,
611
    Howie and Zussman).
612

613
    Parameters
614
    -----------
615
    name : :class:`str`
616
        A name for the classifier model.
617
    axes : :class:`list` | :class:`tuple`
618
        Names of the axes corresponding to the polygon coordinates.
619
    fields : :class:`dict`
620
        Dictionary describing indiviudal polygons, with identifiers as keys and
621
        dictionaries containing 'name' and 'fields' items.
622
    mode : :class:`str`
623
        Mode of the diagram to use; two are currently available - 'default',
624
        which fills the entire ternary space, and 'miscibility-gap' which gives
625
        a simplified approximation of the miscibility gap.
626

627
    References
628
    -----------
629
    .. [#ref_1] Deer, W. A., Howie, R. A., & Zussman, J. (2013).
630
        An introduction to the rock-forming minerals (3rd ed.).
631
        Mineralogical Society of Great Britain and Ireland.
632
    """
633

634
    def __init__(self, **kwargs):
12✔
635
        src = (
12✔
636
            pyrolite_datafolder(subfolder="models") / "FeldsparTernary" / "config.json"
637
        )
638

639
        with open(src, "r") as f:
12✔
640
            config = json.load(f)
12✔
641

642
        poly_config = {**config, **kwargs, "transform": "ternary"}
12✔
643
        super().__init__(**poly_config)
12✔
644

645

646
class PeralkalinityClassifier(object):
12✔
647
    def __init__(self):
12✔
648
        self.fields = None
12✔
649

650
    def predict(self, df: pd.DataFrame):
12✔
651
        TotalAlkali = df.Na2O + df.K2O
12✔
652
        perkalkaline_where = (df.Al2O3 < (TotalAlkali + df.CaO)) & (
12✔
653
            TotalAlkali > df.Al2O3
654
        )
655
        metaluminous_where = (df.Al2O3 > (TotalAlkali + df.CaO)) & (
12✔
656
            TotalAlkali < df.Al2O3
657
        )
658
        peraluminous_where = (df.Al2O3 < (TotalAlkali + df.CaO)) & (
12✔
659
            TotalAlkali < df.Al2O3
660
        )
661
        out = pd.Series(index=df.index, dtype="object")
12✔
662
        out.loc[peraluminous_where] = "Peraluminous"
12✔
663
        out.loc[metaluminous_where] = "Metaluminous"
12✔
664
        out.loc[perkalkaline_where] = "Peralkaline"
12✔
665
        return out
12✔
666

667

668
@update_docstring_references
12✔
669
class JensenPlot(PolygonClassifier):
12✔
670
    """
671
    Jensen Plot for classification of subalkaline volcanic rocks  [#ref_1]_.
672

673
    Parameters
674
    -----------
675
    name : :class:`str`
676
        A name for the classifier model.
677
    axes : :class:`list` | :class:`tuple`
678
        Names of the axes corresponding to the polygon coordinates.
679
    fields : :class:`dict`
680
        Dictionary describing indiviudal polygons, with identifiers as keys and
681
        dictionaries containing 'name' and 'fields' items.
682

683
    References
684
    -----------
685
    .. [#ref_1] Jensen, L. S. (1976). A new cation plot for classifying
686
                sub-alkaline volcanic rocks.
687
                Ontario Division of Mines. Miscellaneous Paper No. 66.
688

689
    Notes
690
    -----
691
    Diagram used for the classification classification of subalkalic volcanic rocks.
692
    The diagram is constructed for molar cation percentages of Al, Fe+Ti and Mg,
693
    on account of these elements' stability upon metamorphism.
694
    This particular version uses updated labels relative to Jensen (1976),
695
    in which the fields have been extended to the full range of the ternary plot.
696
    """
697

698
    def __init__(self, **kwargs):
12✔
699
        src = pyrolite_datafolder(subfolder="models") / "JensenPlot" / "config.json"
12✔
700

701
        with open(src, "r") as f:
12✔
702
            config = json.load(f)
12✔
703

704
        poly_config = {**config, **kwargs, "transform": "ternary"}
12✔
705
        super().__init__(**poly_config)
12✔
706

707

708
class SpinelTrivalentTernary(PolygonClassifier):
12✔
709
    """
710
    Spinel Trivalent Ternary classification  - designed for data in atoms per formula unit
711

712
    Parameters
713
    -----------
714
    name : :class:`str`
715
        A name for the classifier model.
716
    axes : :class:`list` | :class:`tuple`
717
        Names of the axes corresponding to the polygon coordinates.
718
    fields : :class:`dict`
719
        Dictionary describing indiviudal polygons, with identifiers as keys and
720
        dictionaries containing 'name' and 'fields' items.
721
    """
722

723
    def __init__(self, **kwargs):
12✔
724
        src = (
12✔
725
            pyrolite_datafolder(subfolder="models")
726
            / "SpinelTrivalentTernary"
727
            / "config.json"
728
        )
729

730
        with open(src, "r") as f:
12✔
731
            config = json.load(f)
12✔
732

733
        poly_config = {**config, **kwargs, "transform": "ternary"}
12✔
734
        super().__init__(**poly_config)
12✔
735

736

737
class SpinelFeBivariate(PolygonClassifier):
12✔
738
    """
739
    Fe-Spinel classification, designed for data in atoms per formula unit.
740

741
    Parameters
742
    -----------
743
    name : :class:`str`
744
        A name for the classifier model.
745
    axes : :class:`list` | :class:`tuple`
746
        Names of the axes corresponding to the polygon coordinates.
747
    fields : :class:`dict`
748
        Dictionary describing indiviudal polygons, with identifiers as keys and
749
        dictionaries containing 'name' and 'fields' items.
750
    """
751

752
    def __init__(self, **kwargs):
12✔
753
        src = (
12✔
754
            pyrolite_datafolder(subfolder="models")
755
            / "SpinelFeBivariate"
756
            / "config.json"
757
        )
758

759
        with open(src, "r") as f:
12✔
760
            config = json.load(f)
12✔
761

762
        poly_config = {**config, **kwargs}
12✔
763
        super().__init__(**poly_config)
12✔
764

765

766
@update_docstring_references
12✔
767
class Pettijohn(PolygonClassifier):
12✔
768
    """
769
    Pettijohn (1973) sandstones classification
770
    [#ref_1]_.
771

772
    Parameters
773
    -----------
774
    name : :class:`str`
775
        A name for the classifier model.
776
    axes : :class:`list` | :class:`tuple`
777
        Names of the axes corresponding to the polygon coordinates.
778
    fields : :class:`dict`
779
        Dictionary describing indiviudal polygons, with identifiers as keys and
780
        dictionaries containing 'name' and 'fields' items.
781

782
    References
783
    -----------
784
    .. [#ref_1] Pettijohn, F. J., Potter, P. E. and Siever, R. (1973).
785
                Sand  and Sandstone. New York, Springer-Verlag. 618p.
786
                doi: {Pettijohn1973}
787
    """
788

789
    def __init__(self, **kwargs):
12✔
790
        src = (
12✔
791
            pyrolite_datafolder(subfolder="models")
792
            / "sandstones"
793
            / "config_pettijohn.json"
794
        )
795

796
        with open(src, "r") as f:
12✔
797
            config = json.load(f)
12✔
798

799
        poly_config = {**config, **kwargs}
12✔
800
        super().__init__(**poly_config)
12✔
801

802

803
@update_docstring_references
12✔
804
class Herron(PolygonClassifier):
12✔
805
    """
806
    Herron (1988) sandstones classification
807
    [#ref_1]_.
808

809
    Parameters
810
    -----------
811
    name : :class:`str`
812
        A name for the classifier model.
813
    axes : :class:`list` | :class:`tuple`
814
        Names of the axes corresponding to the polygon coordinates.
815
    fields : :class:`dict`
816
        Dictionary describing indiviudal polygons, with identifiers as keys and
817
        dictionaries containing 'name' and 'fields' items.
818

819
    References
820
    -----------
821
    .. [#ref_1] Herron, M.M. (1988).
822
                Geochemical classification of terrigenous sands and shales
823
                from core or log data.
824
                Journal of Sedimentary Research, 58(5), pp.820-829.
825
                doi: {Herron1988}
826
    """
827

828
    def __init__(self, **kwargs):
12✔
829
        src = (
12✔
830
            pyrolite_datafolder(subfolder="models")
831
            / "sandstones"
832
            / "config_herron.json"
833
        )
834

835
        with open(src, "r") as f:
12✔
836
            config = json.load(f)
12✔
837

838
        poly_config = {**config, **kwargs}
12✔
839
        super().__init__(**poly_config)
12✔
840

841

842
TAS.__doc__ = TAS.__doc__.format(
12✔
843
    LeBas1992=sphinx_doi_link("10.1007/BF01160698"),
844
    Middlemost1994=sphinx_doi_link("10.1016/0012-8252(94)90029-9"),
845
    LeMaitre2002=sphinx_doi_link("10.1017/CBO9780511535581"),
846
)
847
USDASoilTexture.__doc__ = USDASoilTexture.__doc__.format(
12✔
848
    Thien1979=sphinx_doi_link("10.2134/jae.1979.0054")
849
)
850
QAP.__doc__ = QAP.__doc__.format(
12✔
851
    Streckeisen1974=sphinx_doi_link("10.1007/BF01820841"),
852
    LeMaitre2002=sphinx_doi_link("10.1017/CBO9780511535581"),
853
)
854
Pettijohn.__doc__ = Pettijohn.__doc__.format(
12✔
855
    Pettijohn1973=sphinx_doi_link("10.1007/978-1-4615-9974-6"),
856
)
857
Herron.__doc__ = Herron.__doc__.format(
12✔
858
    Herron1988=sphinx_doi_link("10.1306/212F8E77-2B24-11D7-8648000102C1865D"),
859
)
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc