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