geolysis 0.3.0__py3-none-any.whl → 0.4.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,781 @@
1
+ """ Soil classification module.
2
+
3
+ Exceptions
4
+ ==========
5
+
6
+ .. autosummary::
7
+ :toctree: _autosummary
8
+
9
+ SizeDistError
10
+
11
+ Enums
12
+ =====
13
+
14
+ .. autosummary::
15
+ :toctree: _autosummary
16
+ :nosignatures:
17
+
18
+ CLF_TYPE
19
+ USCSSymbol
20
+ AASHTOSymbol
21
+
22
+ Classes
23
+ =======
24
+
25
+ .. autosummary::
26
+ :toctree: _autosummary
27
+
28
+ AtterbergLimits
29
+ PSD
30
+ AASHTO
31
+ USCS
32
+
33
+ Functions
34
+ =========
35
+
36
+ .. autosummary::
37
+ :toctree: _autosummary
38
+
39
+ create_soil_classifier
40
+ """
41
+ import enum
42
+ from abc import abstractmethod
43
+ from typing import Protocol
44
+ from typing import NamedTuple, Optional, Sequence
45
+
46
+ from geolysis.utils import enum_repr, isclose, round_, validators
47
+
48
+ __all__ = ["CLF_TYPE",
49
+ "AtterbergLimits",
50
+ "PSD",
51
+ "AASHTO",
52
+ "USCS",
53
+ "create_soil_classifier"]
54
+
55
+
56
+ class SizeDistError(ZeroDivisionError):
57
+ """Exception raised when size distribution is not provided."""
58
+
59
+
60
+ @enum_repr
61
+ class _Clf(tuple, enum.Enum):
62
+
63
+ def __str__(self) -> str:
64
+ return self.name
65
+
66
+ @property
67
+ def symbol(self) -> str:
68
+ return self.value[0]
69
+
70
+ @property
71
+ def description(self) -> str:
72
+ return self.value[1]
73
+
74
+
75
+ class USCSSymbol(_Clf):
76
+ """Unified Soil Classification System (USCS) symbols and descriptions."""
77
+ G = GRAVEL = ("G", "Gravel")
78
+ S = SAND = ("S", "Sand")
79
+ M = SILT = ("M", "Silt")
80
+ C = CLAY = ("C", "Clay")
81
+ O = ORGANIC = ("O", "Organic")
82
+ W = WELL_GRADED = ("W", "Well graded")
83
+ P = POORLY_GRADED = ("P", "Poorly graded")
84
+ L = LOW_PLASTICITY = ("L", "Low plasticity")
85
+ H = HIGH_PLASTICITY = ("H", "High plasticity")
86
+ GW = ("GW", "Well graded gravels")
87
+ GP = ("GP", "Poorly graded gravels")
88
+ GM = ("GM", "Silty gravels")
89
+ GC = ("GC", "Clayey gravels")
90
+ GM_GC = ("GM-GC", "Gravelly clayey silt")
91
+ GW_GM = ("GW-GM", "Well graded gravel with silt")
92
+ GP_GM = ("GP-GM", "Poorly graded gravel with silt")
93
+ GW_GC = ("GW-GC", "Well graded gravel with clay")
94
+ GP_GC = ("GP-GC", "Poorly graded gravel with clay")
95
+ SW = ("SW", "Well graded sands")
96
+ SP = ("SP", "Poorly graded sands")
97
+ SM = ("SM", "Silty sands")
98
+ SC = ("SC", "Clayey sands")
99
+ SM_SC = ("SM-SC", "Sandy clayey silt")
100
+ SW_SM = ("SW-SM", "Well graded sand with silt")
101
+ SP_SM = ("SP-SM", "Poorly graded sand with silt")
102
+ SW_SC = ("SW-SC", "Well graded sand with clay")
103
+ SP_SC = ("SP-SC", "Poorly graded sand with clay")
104
+ ML = ("ML", "Inorganic silts with low plasticity")
105
+ CL = ("CL", "Inorganic clays with low plasticity")
106
+ ML_CL = ("ML-CL", "Clayey silt with low plasticity")
107
+ OL = ("OL", "Organic clays with low plasticity")
108
+ MH = ("MH", "Inorganic silts with high plasticity")
109
+ CH = ("CH", "Inorganic clays with high plasticity")
110
+ OH = ("OH", "Organic silts with high plasticity")
111
+ Pt = ("Pt", "Highly organic soils")
112
+
113
+
114
+ class AASHTOSymbol(_Clf):
115
+ """AASHTO soil classification symbols and descriptions."""
116
+ A_1_a = ("A-1-a", "Stone fragments, gravel, and sand")
117
+ A_1_b = ("A-1-b", "Stone fragments, gravel, and sand")
118
+ A_3 = ("A-3", "Fine sand")
119
+ A_2_4 = ("A-2-4", "Silty or clayey gravel and sand")
120
+ A_2_5 = ("A-2-5", "Silty or clayey gravel and sand")
121
+ A_2_6 = ("A-2-6", "Silty or clayey gravel and sand")
122
+ A_2_7 = ("A-2-7", "Silty or clayey gravel and sand")
123
+ A_4 = ("A-4", "Silty soils")
124
+ A_5 = ("A-5", "Silty soils")
125
+ A_6 = ("A-6", "Clayey soils")
126
+ A_7_5 = ("A-7-5", "Clayey soils")
127
+ A_7_6 = ("A-7-6", "Clayey soils")
128
+
129
+
130
+ class AtterbergLimits:
131
+ """Represents the water contents at which soil changes from one state to
132
+ the other.
133
+ """
134
+
135
+ class __A_LINE:
136
+
137
+ def __get__(self, obj, objtype=None) -> float:
138
+ return 0.73 * (obj.liquid_limit - 20.0)
139
+
140
+ #: The ``A-line`` determines if a soil is clayey or silty.
141
+ #: :math:`A = 0.73(LL - 20.0)`
142
+ A_LINE = __A_LINE()
143
+
144
+ def __init__(self, liquid_limit: float, plastic_limit: float):
145
+ """
146
+ :param liquid_limit: Water content beyond which soils flows under their
147
+ own weight (%). It can also be defined as the
148
+ minimum moisture content at which a soil flows upon
149
+ application of a very small shear force.
150
+ :type liquid_limit: float
151
+
152
+ :param plastic_limit: Water content at which plastic deformation can be
153
+ initiated (%). It is also the minimum water
154
+ content at which soil can be rolled into a thread
155
+ 3mm thick. (molded without breaking)
156
+ :type plastic_limit: float
157
+ """
158
+ if liquid_limit < plastic_limit:
159
+ raise ValueError("liquid_limit cannot be less than plastic_limit")
160
+
161
+ self.liquid_limit = liquid_limit
162
+ self.plastic_limit = plastic_limit
163
+
164
+ @property
165
+ def liquid_limit(self) -> float:
166
+ return self._liquid_limit
167
+
168
+ @liquid_limit.setter
169
+ @validators.ge(0.0)
170
+ def liquid_limit(self, val: float) -> None:
171
+ self._liquid_limit = val
172
+
173
+ @property
174
+ def plastic_limit(self) -> float:
175
+ return self._plastic_limit
176
+
177
+ @plastic_limit.setter
178
+ @validators.ge(0.0)
179
+ def plastic_limit(self, val: float) -> None:
180
+ self._plastic_limit = val
181
+
182
+ @property
183
+ @round_
184
+ def plasticity_index(self) -> float:
185
+ """Plasticity index (PI) is the range of water content over which the
186
+ soil remains in the plastic state.
187
+
188
+ It is also the numerical difference between the liquid limit and
189
+ plastic limit of the soil.
190
+
191
+ :Equation:
192
+
193
+ .. math:: PI = LL - PL
194
+ """
195
+ return self.liquid_limit - self.plastic_limit
196
+
197
+ @property
198
+ def fine_material_type(self) -> USCSSymbol:
199
+ """
200
+ Checks whether the soil is either clay or silt.
201
+ """
202
+ return USCSSymbol.CLAY if self.above_A_LINE() else USCSSymbol.SILT
203
+
204
+ def above_A_LINE(self) -> bool:
205
+ """Checks if the soil sample is above A-Line."""
206
+ return self.plasticity_index > self.A_LINE
207
+
208
+ def limit_plot_in_hatched_zone(self) -> bool:
209
+ """Checks if soil sample plot in the hatched zone on the atterberg
210
+ chart.
211
+ """
212
+ return 4 <= self.plasticity_index <= 7 and 10 < self.liquid_limit < 30
213
+
214
+ @round_
215
+ def liquidity_index(self, nmc: float) -> float:
216
+ r"""Return the liquidity index of the soil.
217
+
218
+ Liquidity index of a soil indicates the nearness of its ``natural water
219
+ content`` to its ``liquid limit``. When the soil is at the plastic
220
+ limit its liquidity index is zero. Negative values of the liquidity
221
+ index indicate that the soil is in a hard (desiccated) state. It is
222
+ also known as Water-Plasticity ratio.
223
+
224
+ :param float nmc: Moisture contents of the soil in natural condition.
225
+
226
+ :Equation:
227
+
228
+ .. math:: I_l = \dfrac{w - PL}{PI} \cdot 100
229
+ """
230
+ return ((nmc - self.plastic_limit) / self.plasticity_index) * 100.0
231
+
232
+ @round_
233
+ def consistency_index(self, nmc: float) -> float:
234
+ r"""Return the consistency index of the soil.
235
+
236
+ Consistency index indicates the consistency (firmness) of soil. It
237
+ shows the nearness of the ``natural water content`` of the soil to its
238
+ ``plastic limit``. When the soil is at the liquid limit, the
239
+ consistency index is zero. The soil at consistency index of zero will
240
+ be extremely soft and has negligible shear strength. A soil at a water
241
+ content equal to the plastic limit has consistency index of 100%
242
+ indicating that the soil is relatively firm. A consistency index of
243
+ greater than 100% shows the soil is relatively strong
244
+ (semi-solid state). A negative value indicate the soil is in the liquid
245
+ state. It is also known as Relative Consistency.
246
+
247
+ :param float nmc: Moisture contents of the soil in natural condition.
248
+
249
+ :Equation:
250
+
251
+ .. math:: I_c = \dfrac{LL - w}{PI} \cdot 100
252
+ """
253
+ return ((self.liquid_limit - nmc) / self.plasticity_index) * 100.0
254
+
255
+
256
+ class _SizeDistribution:
257
+ """Particle size distribution of soil sample.
258
+
259
+ Features obtained from the Particle Size Distribution graph.
260
+ """
261
+
262
+ def __init__(self, d_10: float = 0, d_30: float = 0, d_60: float = 0):
263
+ self.d_10 = d_10
264
+ self.d_30 = d_30
265
+ self.d_60 = d_60
266
+
267
+ def __iter__(self):
268
+ return iter([self.d_10, self.d_30, self.d_60])
269
+
270
+ @property
271
+ def coeff_of_curvature(self) -> float:
272
+ return (self.d_30 ** 2.0) / (self.d_60 * self.d_10)
273
+
274
+ @property
275
+ def coeff_of_uniformity(self) -> float:
276
+ return self.d_60 / self.d_10
277
+
278
+ def grade(self, coarse_soil: USCSSymbol) -> USCSSymbol:
279
+ """Grade of soil sample. Soil grade can either be well graded or poorly
280
+ graded.
281
+
282
+ :param coarse_soil: Coarse fraction of the soil sample. Valid arguments
283
+ are ``USCSSymbol.GRAVEL`` and ``USCSSymbol.SAND``.
284
+ """
285
+ if coarse_soil is USCSSymbol.GRAVEL:
286
+ if 1 < self.coeff_of_curvature < 3 and self.coeff_of_uniformity >= 4:
287
+ grade = USCSSymbol.WELL_GRADED
288
+ else:
289
+ grade = USCSSymbol.POORLY_GRADED
290
+ return grade
291
+
292
+ # coarse soil is sand
293
+ if 1 < self.coeff_of_curvature < 3 and self.coeff_of_uniformity >= 6:
294
+ grade = USCSSymbol.WELL_GRADED
295
+ else:
296
+ grade = USCSSymbol.POORLY_GRADED
297
+ return grade
298
+
299
+
300
+ class PSD:
301
+ """Quantitative proportions by mass of various sizes of particles present
302
+ in a soil.
303
+ """
304
+
305
+ def __init__(self, fines: float, sand: float,
306
+ d_10: float = 0, d_30: float = 0, d_60: float = 0):
307
+ """
308
+ :param fines: Percentage of fines in soil sample (%) i.e. The
309
+ percentage of soil sample passing through No. 200 sieve
310
+ (0.075mm).
311
+ :type fines: float
312
+
313
+ :param sand: Percentage of sand in soil sample (%).
314
+ :type sand: float
315
+
316
+ :param d_10: Diameter at which 10% of the soil by weight is finer.
317
+ :type d_10: float
318
+ :param d_30: Diameter at which 30% of the soil by weight is finer.
319
+ :type d_30: float
320
+ :param d_60: Diameter at which 60% of the soil by weight is finer.
321
+ :type d_60: float
322
+ """
323
+ self.fines = fines
324
+ self.sand = sand
325
+ self.size_dist = _SizeDistribution(d_10=d_10, d_30=d_30, d_60=d_60)
326
+
327
+ @property
328
+ def gravel(self):
329
+ """Percentage of gravel in soil sample (%)."""
330
+ return 100.0 - (self.fines + self.sand)
331
+
332
+ @property
333
+ def coarse_material_type(self) -> USCSSymbol:
334
+ """Determines whether the soil is either gravel or sand."""
335
+ if self.gravel > self.sand:
336
+ return USCSSymbol.GRAVEL
337
+ return USCSSymbol.SAND
338
+
339
+ @property
340
+ @round_
341
+ def coeff_of_curvature(self) -> float:
342
+ r"""Coefficient of curvature of soil sample.
343
+
344
+ :Equation:
345
+
346
+ .. math:: C_c = \dfrac{D^2_{30}}{D_{60} \cdot D_{10}}
347
+
348
+ For the soil to be well graded, the value of :math:`C_c` must be
349
+ between 1 and 3.
350
+ """
351
+ return self.size_dist.coeff_of_curvature
352
+
353
+ @property
354
+ @round_
355
+ def coeff_of_uniformity(self) -> float:
356
+ r"""Coefficient of uniformity of soil sample.
357
+
358
+ :Equation:
359
+
360
+ .. math:: C_u = \dfrac{D_{60}}{D_{10}}
361
+
362
+ :math:`C_u` value greater than 4 to 6 classifies the soil as well
363
+ graded for gravels and sands respectively. When :math:`C_u` is less
364
+ than 4, it is classified as poorly graded or uniformly graded soil.
365
+
366
+ Higher values of :math:`C_u` indicates that the soil mass consists of
367
+ soil particles with different size ranges.
368
+ """
369
+ return self.size_dist.coeff_of_uniformity
370
+
371
+ def has_particle_sizes(self) -> bool:
372
+ """Checks if soil sample has particle sizes."""
373
+ return any(self.size_dist)
374
+
375
+ def grade(self) -> USCSSymbol:
376
+ r"""Return the grade of the soil sample, either well graded or poorly
377
+ graded.
378
+
379
+ Conditions for a well-graded soil:
380
+
381
+ :Equation:
382
+
383
+ - :math:`1 \lt C_c \lt 3` and :math:`C_u \ge 4` (for gravels)
384
+ - :math:`1 \lt C_c \lt 3` and :math:`C_u \ge 6` (for sands)
385
+ """
386
+ return self.size_dist.grade(coarse_soil=self.coarse_material_type)
387
+
388
+
389
+ class SoilClf(NamedTuple):
390
+ symbol: str
391
+ description: str
392
+
393
+
394
+ class AASHTO:
395
+ r"""American Association of State Highway and Transportation Officials
396
+ (AASHTO) classification system.
397
+
398
+ The AASHTO classification system is useful for classifying soils for
399
+ highways. It categorizes soils for highways based on particle size analysis
400
+ and plasticity characteristics. It classifies both coarse-grained and
401
+ fine-grained soils into eight main groups (A1-A7) with subgroups, along with
402
+ a separate category (A8) for organic soils.
403
+
404
+ - ``A1 ~ A3`` (Granular Materials) :math:`\le` 35% pass No. 200 sieve
405
+ - ``A4 ~ A7`` (Silt-clay Materials) :math:`\ge` 36% pass No. 200 sieve
406
+ - ``A8`` (Organic Materials)
407
+
408
+ The Group Index ``(GI)`` is used to further evaluate soils within a group.
409
+
410
+ .. note::
411
+
412
+ The ``GI`` must be mentioned even when it is zero, to indicate that the
413
+ soil has been classified as per AASHTO system.
414
+
415
+ :Equation:
416
+
417
+ .. math::
418
+
419
+ GI = (F_{200} - 35)[0.2 + 0.005(LL - 40)] + 0.01(F_{200} - 15)(PI - 10)
420
+ """
421
+
422
+ def __init__(self, atterberg_limits: AtterbergLimits,
423
+ fines: float, add_group_idx=True):
424
+ """
425
+ :param atterberg_limits: Atterberg limits of soil sample.
426
+ :type atterberg_limits: AtterbergLimits
427
+
428
+ :param fines: Percentage of fines in soil sample (%) i.e. The percentage
429
+ of soil sample passing through No. 200 sieve (0.075mm).
430
+ :type fines: float
431
+
432
+ :param add_group_idx: Used to indicate whether the group index should
433
+ be added to the classification or not, defaults
434
+ to True.
435
+ :type add_group_idx: bool, optional
436
+ """
437
+ self.atterberg_limits = atterberg_limits
438
+ self.fines = fines
439
+ self.add_group_idx = add_group_idx
440
+
441
+ @property
442
+ def fines(self) -> float:
443
+ return self._fines
444
+
445
+ @fines.setter
446
+ @validators.ge(0.0)
447
+ def fines(self, val: float) -> None:
448
+ self._fines = val
449
+
450
+ @round_(ndigits=0)
451
+ def group_index(self) -> float:
452
+ """Return the Group Index (GI) of the soil sample."""
453
+
454
+ liquid_lmt = self.atterberg_limits.liquid_limit
455
+ plasticity_idx = self.atterberg_limits.plasticity_index
456
+ fines = self.fines
457
+
458
+ var_a = 1.0 if (x_0 := fines - 35.0) < 0.0 else min(x_0, 40.0)
459
+ var_b = 1.0 if (x_0 := liquid_lmt - 40.0) < 0.0 else min(x_0, 20.0)
460
+ var_c = 1.0 if (x_0 := fines - 15.0) < 0.0 else min(x_0, 40.0)
461
+ var_d = 1.0 if (x_0 := plasticity_idx - 10.0) < 0.0 else min(x_0, 20.0)
462
+
463
+ return var_a * (0.2 + 0.005 * var_b) + 0.01 * var_c * var_d
464
+
465
+ def classify(self) -> SoilClf:
466
+ """Return the AASHTO classification of the soil."""
467
+ soil_clf = self._classify()
468
+
469
+ symbol, description = soil_clf.symbol, soil_clf.description
470
+
471
+ if self.add_group_idx:
472
+ symbol = f"{symbol}({self.group_index():.0f})"
473
+
474
+ return SoilClf(symbol, description)
475
+
476
+ def _classify(self) -> AASHTOSymbol:
477
+ # Silts A4-A7
478
+ if self.fines > 35:
479
+ soil_clf = self._fine_soil_classifier()
480
+ # Coarse A1-A3
481
+ else:
482
+ soil_clf = self._coarse_soil_classifier()
483
+
484
+ return soil_clf
485
+
486
+ def _fine_soil_classifier(self) -> AASHTOSymbol:
487
+ # A-4 -> A-5, Silty Soils
488
+ # A-6 -> A-7, Clayey Soils
489
+ liquid_lmt = self.atterberg_limits.liquid_limit
490
+ plasticity_idx = self.atterberg_limits.plasticity_index
491
+
492
+ if liquid_lmt <= 40:
493
+ if plasticity_idx <= 10.0:
494
+ soil_clf = AASHTOSymbol.A_4
495
+ else:
496
+ soil_clf = AASHTOSymbol.A_6
497
+ else:
498
+ if plasticity_idx <= 10.0:
499
+ soil_clf = AASHTOSymbol.A_5
500
+ else:
501
+ if plasticity_idx <= (liquid_lmt - 30.0):
502
+ soil_clf = AASHTOSymbol.A_7_5
503
+ else:
504
+ soil_clf = AASHTOSymbol.A_7_6
505
+
506
+ return soil_clf
507
+
508
+ def _coarse_soil_classifier(self) -> AASHTOSymbol:
509
+ # A-3, Fine sand
510
+ liquid_lmt = self.atterberg_limits.liquid_limit
511
+ plasticity_idx = self.atterberg_limits.plasticity_index
512
+
513
+ if self.fines <= 10.0 and isclose(plasticity_idx, 0.0, rel_tol=0.01):
514
+ soil_clf = AASHTOSymbol.A_3
515
+ # A-1-a -> A-1-b, Stone fragments, gravel, and sand
516
+ elif self.fines <= 15 and plasticity_idx <= 6:
517
+ soil_clf = AASHTOSymbol.A_1_a
518
+ elif self.fines <= 25 and plasticity_idx <= 6:
519
+ soil_clf = AASHTOSymbol.A_1_b
520
+ # A-2-4 -> A-2-7, Silty or clayey gravel and sand
521
+ elif liquid_lmt <= 40:
522
+ if plasticity_idx <= 10:
523
+ soil_clf = AASHTOSymbol.A_2_4
524
+ else:
525
+ soil_clf = AASHTOSymbol.A_2_6
526
+ else:
527
+ if plasticity_idx <= 10:
528
+ soil_clf = AASHTOSymbol.A_2_5
529
+ else:
530
+ soil_clf = AASHTOSymbol.A_2_7
531
+
532
+ return soil_clf
533
+
534
+
535
+ class USCS:
536
+ """Unified Soil Classification System (USCS).
537
+
538
+ The Unified Soil Classification System, initially developed by Casagrande
539
+ in 1948 and later modified in 1952, is widely utilized in engineering
540
+ projects involving soils. It is the most popular system for soil
541
+ classification and is similar to Casagrande's Classification System. The
542
+ system relies on particle size analysis and atterberg limits for
543
+ classification.
544
+
545
+ In this system, soils are first classified into two categories:
546
+
547
+ - Coarse grained soils: If more than 50% of the soils is retained on
548
+ No. 200 (0.075 mm) sieve, it is designated as coarse-grained soil.
549
+
550
+ - Fine grained soils: If more than 50% of the soil passes through No. 200
551
+ sieve, it is designated as fine-grained soil.
552
+
553
+ Highly Organic soils are identified by visual inspection. These soils are
554
+ termed as Peat. (:math:`P_t`)
555
+ """
556
+
557
+ def __init__(self, atterberg_limits: AtterbergLimits,
558
+ psd: PSD, organic=False):
559
+ """
560
+ :param atterberg_limits: Atterberg limits of the soil.
561
+ :type atterberg_limits: AtterbergLimits
562
+
563
+ :param psd: Particle size distribution of the soil.
564
+ :type psd: PSD
565
+
566
+ :param organic: Indicates whether soil is organic or not, defaults to
567
+ False.
568
+ :type organic: bool, optional
569
+ """
570
+ self.atterberg_limits = atterberg_limits
571
+ self.psd = psd
572
+ self.organic = organic
573
+
574
+ def classify(self) -> SoilClf:
575
+ """Return the USCS classification of the soil."""
576
+ soil_clf = self._classify()
577
+
578
+ # Ensure soil_clf is of type USCSSymbol
579
+ if isinstance(soil_clf, str):
580
+ soil_clf = USCSSymbol[soil_clf]
581
+
582
+ if isinstance(soil_clf, USCSSymbol):
583
+ return SoilClf(soil_clf.symbol, soil_clf.description)
584
+
585
+ # Handling tuple or list case for dual classification
586
+ first_clf, second_clf = map(lambda clf: USCSSymbol[clf], soil_clf)
587
+
588
+ comb_symbol = f"{first_clf.symbol},{second_clf.symbol}"
589
+ comb_desc = f"{first_clf.description},{second_clf.description}"
590
+
591
+ return SoilClf(comb_symbol, comb_desc)
592
+
593
+ def _classify(self) -> USCSSymbol | str | Sequence[str]:
594
+ # Fine-grained, Run Atterberg
595
+ if self.psd.fines > 50.0:
596
+ return self._fine_soil_classifier()
597
+ # Coarse grained, Run Sieve Analysis
598
+ # Gravel or Sand
599
+ return self._coarse_soil_classifier()
600
+
601
+ def _fine_soil_classifier(self) -> USCSSymbol:
602
+ liquid_lmt = self.atterberg_limits.liquid_limit
603
+ plasticity_idx = self.atterberg_limits.plasticity_index
604
+
605
+ if liquid_lmt < 50.0:
606
+ # Low LL
607
+ # Above A-line and PI > 7
608
+ if self.atterberg_limits.above_A_LINE() and plasticity_idx > 7.0:
609
+ soil_clf = USCSSymbol.CL
610
+
611
+ # Limit plot in hatched area on plasticity chart
612
+ elif self.atterberg_limits.limit_plot_in_hatched_zone():
613
+ soil_clf = USCSSymbol.ML_CL
614
+
615
+ # Below A-line or PI < 4
616
+ else:
617
+ soil_clf = USCSSymbol.OL if self.organic else USCSSymbol.ML
618
+
619
+ # High LL
620
+ else:
621
+ # Above A-Line
622
+ if self.atterberg_limits.above_A_LINE():
623
+ soil_clf = USCSSymbol.CH
624
+
625
+ # Below A-Line
626
+ else:
627
+ soil_clf = USCSSymbol.OH if self.organic else USCSSymbol.MH
628
+
629
+ return soil_clf
630
+
631
+ def _coarse_soil_classifier(self) -> USCSSymbol | str | Sequence[str]:
632
+ coarse_material_type = self.psd.coarse_material_type
633
+
634
+ # More than 12% pass No. 200 sieve
635
+ if self.psd.fines > 12.0:
636
+ # Above A-line
637
+ if self.atterberg_limits.above_A_LINE():
638
+ soil_clf = f"{coarse_material_type}{USCSSymbol.CLAY}"
639
+
640
+ # Limit plot in hatched zone on plasticity chart
641
+ elif self.atterberg_limits.limit_plot_in_hatched_zone():
642
+ if coarse_material_type == USCSSymbol.GRAVEL:
643
+ soil_clf = USCSSymbol.GM_GC
644
+ else:
645
+ soil_clf = USCSSymbol.SM_SC
646
+
647
+ # Below A-line
648
+ else:
649
+ if coarse_material_type == USCSSymbol.GRAVEL:
650
+ soil_clf = USCSSymbol.GM
651
+ else:
652
+ soil_clf = USCSSymbol.SM
653
+
654
+ elif 5.0 <= self.psd.fines <= 12.0:
655
+ # Requires dual symbol based on graduation and plasticity chart
656
+ if self.psd.has_particle_sizes():
657
+ soil_clf = self._dual_soil_classifier()
658
+ else:
659
+ fine_material_type = self.atterberg_limits.fine_material_type
660
+
661
+ soil_clf = (f"{coarse_material_type}{USCSSymbol.WELL_GRADED}_"
662
+ f"{coarse_material_type}{fine_material_type}",
663
+ f"{coarse_material_type}{USCSSymbol.POORLY_GRADED}_"
664
+ f"{coarse_material_type}{fine_material_type}")
665
+
666
+ # Less than 5% pass No. 200 sieve
667
+ # Obtain Cc and Cu from grain size graph
668
+ else:
669
+ if self.psd.has_particle_sizes():
670
+ soil_clf = f"{coarse_material_type}{self.psd.grade()}"
671
+
672
+ else:
673
+ soil_clf = (f"{coarse_material_type}{USCSSymbol.WELL_GRADED}",
674
+ f"{coarse_material_type}{USCSSymbol.POORLY_GRADED}")
675
+
676
+ return soil_clf
677
+
678
+ def _dual_soil_classifier(self) -> str:
679
+ fine_material_type = self.atterberg_limits.fine_material_type
680
+ coarse_material_type = self.psd.coarse_material_type
681
+
682
+ return (f"{coarse_material_type}{self.psd.grade()}_"
683
+ f"{coarse_material_type}{fine_material_type}")
684
+
685
+
686
+ @enum_repr
687
+ class CLF_TYPE(enum.StrEnum):
688
+ """Enumeration of soil classification types."""
689
+ AASHTO = enum.auto()
690
+ USCS = enum.auto()
691
+
692
+
693
+ class SoilClassifier(Protocol):
694
+ @abstractmethod
695
+ def classify(self) -> SoilClf: ...
696
+
697
+
698
+ def create_soil_classifier(liquid_limit: float,
699
+ plastic_limit: float,
700
+ fines: float,
701
+ sand: Optional[float] = None,
702
+ d_10: float = 0, d_30: float = 0, d_60: float = 0,
703
+ add_group_idx: bool = True,
704
+ organic: bool = False,
705
+ clf_type: Optional[CLF_TYPE | str] = None
706
+ ) -> SoilClassifier:
707
+ """ A factory function that encapsulates the creation of a soil classifier.
708
+
709
+ :param liquid_limit: Water content beyond which soils flows under their own
710
+ weight (%). It can also be defined as the minimum
711
+ moisture content at which a soil flows upon application
712
+ of a very small shear force.
713
+ :type liquid_limit: float
714
+
715
+ :param plastic_limit: Water content at which plastic deformation can be
716
+ initiated (%). It is also the minimum water content at
717
+ which soil can be rolled into a thread 3mm thick.
718
+ (molded without breaking)
719
+ :type plastic_limit: float
720
+
721
+ :param fines: Percentage of fines in soil sample (%) i.e. The percentage of
722
+ soil sample passing through No. 200 sieve (0.075mm).
723
+ :type fines: float
724
+
725
+ :param sand: Percentage of sand in soil sample (%).
726
+ :type sand: float
727
+
728
+ :param d_10: Diameter at which 10% of the soil by weight is finer.
729
+ :type d_10: float
730
+
731
+ :param d_30: Diameter at which 30% of the soil by weight is finer.
732
+ :type d_30: float
733
+
734
+ :param d_60: Diameter at which 60% of the soil by weight is finer.
735
+ :type d_60: float
736
+
737
+ :param add_group_idx: Used to indicate whether the group index should
738
+ be added to the classification or not, defaults to
739
+ True.
740
+ :type add_group_idx: bool, optional
741
+
742
+ :param organic: Indicates whether soil is organic or not, defaults to False.
743
+ :type organic: bool, optional
744
+
745
+ :param clf_type: Used to indicate which type of soil classifier should be
746
+ used, defaults to None.
747
+ :type clf_type: CLF_TYPE | str
748
+
749
+ :raises ValueError: Raises ValueError if ``clf_type`` is not supported or
750
+ None
751
+ :raises ValueError: Raises ValueError if ``sand`` is not provided for
752
+ :class:`USCS` classification.
753
+ """
754
+ msg = (f"{clf_type = } is not supported, Supported "
755
+ f"types are: {list(CLF_TYPE)}")
756
+
757
+ if clf_type is None:
758
+ raise ValueError(msg)
759
+
760
+ try:
761
+ clf_type = CLF_TYPE(str(clf_type).casefold())
762
+ except ValueError as e:
763
+ raise ValueError(msg) from e
764
+
765
+ atterberg_lmts = AtterbergLimits(liquid_limit=liquid_limit,
766
+ plastic_limit=plastic_limit)
767
+
768
+ if clf_type == CLF_TYPE.AASHTO:
769
+ clf = AASHTO(atterberg_limits=atterberg_lmts,
770
+ fines=fines,
771
+ add_group_idx=add_group_idx)
772
+ return clf
773
+
774
+ # USCS classification
775
+ if sand is None:
776
+ raise ValueError("sand must be specified for USCS classification")
777
+
778
+ psd = PSD(fines=fines, sand=sand, d_10=d_10, d_30=d_30, d_60=d_60)
779
+ clf = USCS(atterberg_limits=atterberg_lmts, psd=psd, organic=organic)
780
+
781
+ return clf