geolysis 0.2.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.
@@ -1,658 +1,719 @@
1
- from types import MappingProxyType
2
- from typing import NamedTuple
3
-
4
- from .constants import ERROR_TOL
5
- from .utils import ceil, isclose, round_
6
-
7
- __all__ = [
8
- "AASHTO_SOIL_DESC",
9
- "USCS_SOIL_DESC",
10
- "AtterbergLimits",
11
- "PSD",
12
- "AASHTO",
13
- "USCS",
14
- ]
15
-
16
-
17
- def _chk_psd(fines: float, sand: float, gravel: float):
18
- total_agg = fines + sand + gravel
19
- if not isclose(total_agg, 100.0, rel_tol=ERROR_TOL):
20
- errmsg = f"fines + sand + gravels = 100% not {total_agg}"
21
- raise PSDError(errmsg)
22
-
23
-
24
- # Soil Symbols
25
- GRAVEL = "G"
26
- SAND = "S"
27
- SILT = "M"
28
- CLAY = "C"
29
- ORGANIC = "O"
30
-
31
- # Gradation Symbols
32
- WELL_GRADED = "W"
33
- POORLY_GRADED = "P"
34
-
35
- # Liquid Limit Symbols
36
- LOW_PLASTICITY = "L"
37
- HIGH_PLASTICITY = "H"
38
-
39
-
40
- #: Descriptions for various AASHTO soil classes.
41
- AASHTO_SOIL_DESC = {
42
- "A-1-a": "Stone fragments, gravel, and sand",
43
- "A-1-b": "Stone fragments, gravel, and sand",
44
- "A-3": "Fine sand",
45
- "A-2-4": "Silty or clayey gravel and sand",
46
- "A-2-5": "Silty or clayey gravel and sand",
47
- "A-2-6": "Silty or clayey gravel and sand",
48
- "A-2-7": "Silty or clayey gravel and sand",
49
- "A-4": "Silty soils",
50
- "A-5": "Silty soils",
51
- "A-6": "Clayey soils",
52
- "A-7-5": "Clayey soils",
53
- "A-7-6": "Clayey soils",
54
- }
55
-
56
- #: Descriptions for various USCS soil classes.
57
- USCS_SOIL_DESC = {
58
- "GW": "Well graded gravels",
59
- "GP": "Poorly graded gravels",
60
- "GM": "Silty gravels",
61
- "GC": "Clayey gravels",
62
- "GM-GC": "Gravelly clayey silt",
63
- "GW-GM": "Well graded gravel with silt",
64
- "GP-GM": "Poorly graded gravel with silt",
65
- "GW-GC": "Well graded gravel with clay",
66
- "GP-GC": "Poorly graded gravel with clay",
67
- "SW": "Well graded sands",
68
- "SP": "Poorly graded sands",
69
- "SM": "Silty sands",
70
- "SC": "Clayey sands",
71
- "SM-SC": "Sandy clayey silt",
72
- "SW-SM": "Well graded sand with silt",
73
- "SP-SM": "Poorly graded sand with silt",
74
- "SW-SC": "Well graded sand with clay",
75
- "SP-SC": "Poorly graded sand with clay",
76
- "ML": "Inorganic silts with low plasticity",
77
- "CL": "Inorganic clays with low plasticity",
78
- "ML-CL": "Clayey silt with low plasticity",
79
- "OL": "Organic clays with low plasticity",
80
- "MH": "Inorganic silts with high plasticity",
81
- "CH": "Inorganic clays with high plasticity",
82
- "OH": "Organic silts with high plasticity",
83
- "Pt": "Highly organic soils",
84
- }
85
-
86
-
87
- class PSDError(ValueError):
88
- """
89
- Exception raised when soil aggregates does not approximately sum up to
90
- 100%.
91
- """
92
-
93
-
94
- class AtterbergLimits:
95
- """
96
- Water contents at which soil changes from one state to the other.
97
-
98
- In 1911, a Swedish agriculture engineer ``Atterberg`` mentioned that a fined-grained
99
- soil can exist in four states, namely, liquid, plastic, semi-solid or solid state.
100
-
101
- The main use of Atterberg Limits is in the classification of soils.
102
-
103
- :param float liquid_limit: Water content beyond which soils flows under their own weight.
104
- It can also be defined as the minimum moisture content at which a soil flows upon
105
- application of a very small shear force.
106
- :param float plastic_limit: Water content at which plastic deformation can be initiated.
107
- It is also the minimum water content at which soil can be rolled into a thread 3mm
108
- thick (molded without breaking)
109
- """
110
-
111
- def __init__(self, liquid_limit: float, plastic_limit: float):
112
- self.liquid_limit = liquid_limit
113
- self.plastic_limit = plastic_limit
114
-
115
- @property
116
- def plasticity_index(self) -> float:
117
- """
118
- Return the plasticity index of the soil.
119
-
120
- Plasticity index is the range of water content over which the soil remains in the
121
- plastic state. It is also the numerical difference between the liquid limit and
122
- plastic limit of the soil.
123
-
124
- .. math::
125
-
126
- PI = LL - PL
127
- """
128
- return self.liquid_limit - self.plastic_limit
129
-
130
- @property
131
- @round_(ndigits=2)
132
- def A_line(self) -> float:
133
- """
134
- Return the ``A-line`` which is used to determine if a soil is clayey or
135
- silty.
136
- """
137
- return 0.73 * (self.liquid_limit - 20)
138
-
139
- @property
140
- def type_of_fines(self) -> str:
141
- """
142
- Return the type of fine soil, either ``CLAY`` or ``SILT``.
143
- """
144
- return CLAY if self.above_A_LINE() else SILT
145
-
146
- def above_A_LINE(self) -> bool:
147
- """
148
- Checks if the soil sample is above A-Line.
149
- """
150
- return self.plasticity_index > self.A_line
151
-
152
- def limit_plot_in_hatched_zone(self) -> bool:
153
- """
154
- Checks if soil sample plot in the hatched zone on the atterberg chart.
155
- """
156
- return 4 <= self.plasticity_index <= 7 and 10 < self.liquid_limit < 30
157
-
158
- @round_(ndigits=2)
159
- def liquidity_index(self, nmc: float) -> float:
160
- r"""
161
- Return the liquidity index of the soil.
162
-
163
- Liquidity index of a soil indicates the nearness of its water content
164
- to its liquid limit. When the soil is at the plastic limit its liquidity
165
- index is zero. Negative values of the liquidity index indicate that the
166
- soil is in a hard (desiccated) state. It is also known as Water-Plasticity
167
- ratio.
168
-
169
- :param float nmc: Moisture contents of the soil in natural condition.
170
- (Natural Moisture Content)
171
-
172
- .. math::
173
-
174
- I_l = \dfrac{w - PL}{PI} \cdot 100
175
- """
176
- return ((nmc - self.plastic_limit) / self.plasticity_index) * 100
177
-
178
- @round_(ndigits=2)
179
- def consistency_index(self, nmc: float) -> float:
180
- r"""
181
- Return the consistency index of the soil.
182
-
183
- Consistency index indicates the consistency (firmness) of soil. It shows the
184
- nearness of the water content of the soil to its plastic limit. When the soil
185
- is at the liquid limit, the consistency index is zero. The soil at consistency
186
- index of zero will be extremely soft and has negligible shear strength. A soil
187
- at a water content equal to the plastic limit has consistency index of 100%
188
- indicating that the soil is relatively firm. A consistency index of greater than
189
- 100% shows the soil is relatively strong (semi-solid state). A negative value
190
- indicate the soil is in the liquid state. It is also known as Relative Consistency.
191
-
192
- :param float nmc: Moisture contents of the soil in natural condition.
193
- (Natural Moisture Content)
194
-
195
- .. math::
196
-
197
- I_c = \dfrac{LL - w}{PI} \cdot 100
198
- """
199
- return ((self.liquid_limit - nmc) / self.plasticity_index) * 100.0
200
-
201
-
202
- class _SizeDistribution(NamedTuple):
203
- """
204
- Features obtained from the Particle Size Distribution graph.
205
- """
206
-
207
- d_10: float
208
- d_30: float
209
- d_60: float
210
-
211
- @property
212
- @round_(ndigits=2)
213
- def coeff_of_curvature(self) -> float:
214
- """
215
- Coefficient of curvature of soil sample.
216
- """
217
- return (self.d_30**2) / (self.d_60 * self.d_10)
218
-
219
- @property
220
- @round_(ndigits=2)
221
- def coeff_of_uniformity(self) -> float:
222
- """
223
- Coefficient of uniformity of soil sample.
224
- """
225
- return self.d_60 / self.d_10
226
-
227
- def grade(self, coarse_soil: str) -> str:
228
- """
229
- Grade of soil sample. Soil grade can either be ``WELL_GRADED`` or
230
- ``POORLY_GRADED``.
231
-
232
- :param str coarse_soil: Coarse fraction of the soil sample. Valid arguments
233
- are ``GRAVEL`` or ``SAND``.
234
- """
235
- if coarse_soil == GRAVEL and (
236
- 1 < self.coeff_of_curvature < 3 and self.coeff_of_uniformity >= 4
237
- ):
238
- grade = WELL_GRADED
239
-
240
- elif coarse_soil == SAND and (
241
- 1 < self.coeff_of_curvature < 3 and self.coeff_of_uniformity >= 6
242
- ):
243
- grade = WELL_GRADED
244
-
245
- else:
246
- grade = POORLY_GRADED
247
-
248
- return grade
249
-
250
-
251
- class PSD:
252
- """
253
- Quantitative proportions by mass of various sizes of particles present in a
254
- soil.
255
-
256
- Particle Size Distribution is a method of separation of soils into
257
- different fractions using a stack of sieves to measure the size of the
258
- particles in a sample and graphing the results to illustrate the
259
- distribution of the particle sizes.
260
-
261
- :param float fines: Percentage of fines in soil sample i.e. the percentage
262
- of soil sample passing through No. 200 sieve (0.075mm)
263
- :param float sand: Percentage of sand in soil sample (%)
264
- :param float gravel: Percentage of gravel in soil sample (%)
265
- :param float d_10: Diameter at which 10% of the soil by weight is finer.
266
- :param float d_30: Diameter at which 30% of the soil by weight is finer.
267
- :param float d_60: Diameter at which 60% of the soil by weight is finer.
268
-
269
- :raises PSDError: Raised when soil aggregates does not approximately sum up
270
- to 100%.
271
- """
272
-
273
- def __init__(
274
- self,
275
- fines: float,
276
- sand: float,
277
- gravel: float,
278
- d_10: float = 0,
279
- d_30: float = 0,
280
- d_60: float = 0,
281
- ):
282
- self.fines = fines
283
- self.sand = sand
284
- self.gravel = gravel
285
- self.size_dist = _SizeDistribution(d_10, d_30, d_60)
286
-
287
- _chk_psd(self.fines, self.sand, self.gravel)
288
-
289
- @property
290
- def type_of_coarse(self) -> str:
291
- """
292
- Return the type of coarse material i.e. either ``GRAVEL`` or ``SAND``.
293
- """
294
- return GRAVEL if self.gravel > self.sand else SAND
295
-
296
- @property
297
- def coeff_of_curvature(self) -> float:
298
- r"""
299
- Return the coefficient of curvature of the soil.
300
-
301
- .. math::
302
-
303
- C_c = \dfrac{D^2_{30}}{D_{60} \times D_{10}}
304
- """
305
- return self.size_dist.coeff_of_curvature
306
-
307
- @property
308
- def coeff_of_uniformity(self) -> float:
309
- r"""
310
- Return the coefficient of uniformity of the soil.
311
-
312
- .. math::
313
-
314
- C_u = \dfrac{D_{60}}{D_{10}}
315
- """
316
- return self.size_dist.coeff_of_uniformity
317
-
318
- def _has_particle_sizes(self) -> bool:
319
- """
320
- Checks if soil sample has particle sizes.
321
- """
322
- return all(self.size_dist)
323
-
324
- def grade(self) -> str:
325
- r"""
326
- Return the grade of the soil sample either ``WELL_GRADED`` or
327
- ``POORLY_GRADED``.
328
-
329
- Conditions for a well-graded soil:
330
-
331
- - :math:`1 \lt C_c \lt 3` and :math:`C_u \ge 4` (for gravels)
332
- - :math:`1 \lt C_c \lt 3` and :math:`C_u \ge 6` (for sands)
333
- """
334
- return self.size_dist.grade(coarse_soil=self.type_of_coarse)
335
-
336
-
337
- class AASHTO:
338
- r"""
339
- American Association of State Highway and Transportation Officials (AASHTO)
340
- classification system.
341
-
342
- The AASHTO classification system is useful for classifying soils for highways. It
343
- categorizes soils for highways based on particle size analysis and plasticity
344
- characteristics. It classifies both coarse-grained and fine-grained soils into eight
345
- main groups (A1-A7) with subgroups, along with a separate category (A8) for organic
346
- soils.
347
-
348
- - ``A1 ~ A3`` (Granular Materials) :math:`\le` 35% pass No. 200 sieve
349
- - ``A4 ~ A7`` (Silt-clay Materials) :math:`\ge` 36% pass No. 200 sieve
350
-
351
- The Group Index ``(GI)`` is used to further evaluate soils with a group (subgroups).
352
- When calculating ``GI`` from the equation below, if any term in the parenthesis
353
- becomes negative, it is drop and not given a negative value. The maximum values of
354
- :math:`(F_{200} - 35)` and :math:`(F_{200} - 15)` are taken as 40 and :math:`(LL - 40)`
355
- and :math:`(PI - 10)` as 20.
356
-
357
- If the computed value for ``GI`` is negative, it is reported as zero.
358
-
359
- In general, the rating for the pavement subgrade is inversely proportional to the ``GI``
360
- (lower the ``GI``, better the material). For e.g., a ``GI`` of zero indicates a good
361
- subgrade, whereas a group index of 20 or greater shows a very poor subgrade.
362
-
363
- .. note::
364
-
365
- The ``GI`` must be mentioned even when it is zero, to indicate that the soil has been
366
- classified as per AASHTO system.
367
-
368
- :param float liquid_limit: Water content beyond which soils flows under their own weight.
369
- :param float plasticity_index: Range of water content over which soil remains in plastic
370
- condition.
371
- :param float fines: Percentage of fines in soil sample i.e. the percentage of soil
372
- sample passing through No. 200 sieve (0.075mm).
373
- :kwparam bool add_group_idx: Used to indicate whether the group index should be added to
374
- the classification or not. Defaults to True.
375
- """
376
-
377
- def __init__(
378
- self,
379
- liquid_limit: float,
380
- plasticity_index: float,
381
- fines: float,
382
- add_group_idx=True,
383
- ):
384
- self.liquid_limit = liquid_limit
385
- self.plasticity_index = plasticity_index
386
- self.fines = fines
387
- self.add_group_idx = add_group_idx
388
-
389
- def group_index(self) -> float:
390
- """
391
- Return the Group Index (GI) of the soil sample.
392
-
393
- .. math::
394
-
395
- GI = (F_{200} - 35)[0.2 + 0.005(LL - 40)] + 0.01(F_{200} - 15)(PI - 10)
396
-
397
- - :math:`F_{200}`: Percentage by mass passing American Sieve No. 200.
398
- - LL: Liquid Limit (%), expressed as a whole number.
399
- - PI: Plasticity Index (%), expressed as a whole number.
400
- """
401
- x_1 = 1 if (x_0 := self.fines - 35) < 0 else min(x_0, 40)
402
- x_2 = 1 if (x_0 := self.liquid_limit - 40) < 0 else min(x_0, 20)
403
- x_3 = 1 if (x_0 := self.fines - 15) < 0 else min(x_0, 40)
404
- x_4 = 1 if (x_0 := self.plasticity_index - 10) < 0 else min(x_0, 20)
405
-
406
- grp_idx = round(x_1 * (0.2 + 0.005 * x_2) + 0.01 * x_3 * x_4, 0)
407
-
408
- return 0 if grp_idx <= 0 else ceil(grp_idx)
409
-
410
- @property
411
- def soil_class(self) -> str:
412
- """
413
- Return the AASHTO classification of the soil.
414
- """
415
- return self._classify()
416
-
417
- @property
418
- def soil_desc(self) -> str | None:
419
- """
420
- Return the AASHTO description of the soil.
421
- """
422
- tmp_state = self.add_group_idx
423
- try:
424
- self.add_group_idx = False
425
- return AASHTO_SOIL_DESC[self._classify()]
426
- finally:
427
- self.add_group_idx = tmp_state
428
-
429
- def _classify(self) -> str:
430
- # Silts A4-A7
431
- if self.fines > 35:
432
- return self._fine_soil_classifier()
433
-
434
- # Coarse A1-A3
435
- return self._coarse_soil_classifier()
436
-
437
- def _coarse_soil_classifier(self) -> str:
438
- # A-3, Fine sand
439
- if self.fines <= 10 and isclose(
440
- self.plasticity_index, 0, rel_tol=ERROR_TOL
441
- ):
442
- clf = "A-3"
443
-
444
- # A-1-a -> A-1-b, Stone fragments, gravel, and sand
445
- elif self.fines <= 15 and self.plasticity_index <= 6:
446
- clf = "A-1-a"
447
-
448
- elif self.fines <= 25 and self.plasticity_index <= 6:
449
- clf = "A-1-b"
450
-
451
- # A-2-4 -> A-2-7, Silty or clayey gravel and sand
452
- elif self.liquid_limit <= 40:
453
- if self.plasticity_index <= 10:
454
- clf = "A-2-4"
455
- else:
456
- clf = "A-2-6"
457
-
458
- else:
459
- if self.plasticity_index <= 10:
460
- clf = "A-2-5"
461
- else:
462
- clf = "A-2-7"
463
-
464
- return f"{clf}({self.group_index()})" if self.add_group_idx else clf
465
-
466
- def _fine_soil_classifier(self) -> str:
467
- # A-4 -> A-5, Silty Soils
468
- # A-6 -> A-7, Clayey Soils
469
- if self.liquid_limit <= 40:
470
- clf = "A-4" if self.plasticity_index <= 10 else "A-6"
471
-
472
- else:
473
- if self.plasticity_index <= 10:
474
- clf = "A-5"
475
- else:
476
- _x = self.liquid_limit - 30
477
- clf = "A-7-5" if self.plasticity_index <= _x else "A-7-6"
478
-
479
- return f"{clf}({self.group_index()})" if self.add_group_idx else clf
480
-
481
-
482
- class USCS:
483
- """
484
- Unified Soil Classification System (USCS).
485
-
486
- The Unified Soil Classification System, initially developed by Casagrande in 1948 and
487
- later modified in 1952, is widely utilized in engineering projects involving soils. It
488
- is the most popular system for soil classification and is similar to Casagrande's
489
- Classification System. The system relies on particle size analysis and atterberg limits
490
- for classification.
491
-
492
- In this system, soils are first classified into two categories:
493
-
494
- - Coarse grained soils: If more than 50% of the soils is retained on No. 200 (0.075 mm)
495
- sieve, it is designated as coarse-grained soil.
496
-
497
- - Fine grained soils: If more than 50% of the soil passes through No. 200 sieve, it is
498
- designated as fine grained soil.
499
-
500
- Highly Organic soils are identified by visual inspection. These soils are termed as Peat.
501
- (:math:`P_t`)
502
-
503
- Soil symbols:
504
-
505
- - G: Gravel
506
- - S: Sand
507
- - M: Silt
508
- - C: Clay
509
- - O: Organic Clay
510
- - Pt: Peat
511
-
512
- Liquid limit symbols:
513
-
514
- - H: High Plasticity :math:`(LL > 50)`
515
- - L: Low Plasticity :math:`(LL < 50)`
516
-
517
- Gradation symbols:
518
-
519
- - W: Well-graded
520
- - P: Poorly-graded
521
-
522
- :param float liquid_limit: Water content beyond which soils flows under their own weight.
523
- It can also be defined as the minimum moisture content at which a soil flows upon
524
- application of a very small shear force.
525
- :param float plastic_limit: Water content at which plastic deformation can be initiated.
526
- It is also the minimum water content at which soil can be rolled into a thread 3mm
527
- thick (molded without breaking)
528
- :param float fines: Percentage of fines in soil sample i.e. the percentage of soil sample
529
- passing through No. 200 sieve (0.075mm)
530
- :param float sand: Percentage of sand in soil sample (%)
531
- :param float gravel: Percentage of gravel in soil sample (%)
532
- :param float d_10: Diameter at which 10% of the soil by weight is finer.
533
- :param float d_30: Diameter at which 30% of the soil by weight is finer.
534
- :param float d_60: Diameter at which 60% of the soil by weight is finer.
535
- :kwparam bool organic: Indicates whether soil is organic or not.
536
- """
537
-
538
- def __init__(
539
- self,
540
- liquid_limit: float,
541
- plastic_limit: float,
542
- fines: float,
543
- sand: float,
544
- gravel: float,
545
- *,
546
- d_10=0,
547
- d_30=0,
548
- d_60=0,
549
- organic=False,
550
- ):
551
- self.atterberg_limits = AtterbergLimits(liquid_limit, plastic_limit)
552
- self.psd = PSD(fines, sand, gravel, d_10, d_30, d_60)
553
- self.organic = organic
554
-
555
- @property
556
- def soil_class(self) -> str:
557
- """
558
- Return the USCS Classification of the soil.
559
- """
560
- return self._classify()
561
-
562
- @property
563
- def soil_desc(self) -> str | None:
564
- """
565
- Return the USCS description of the soil.
566
- """
567
- return USCS_SOIL_DESC[self._classify()]
568
-
569
- def _classify(self) -> str:
570
- # Fine grained, Run Atterberg
571
- if self.psd.fines > 50:
572
- return self._fine_soil_classifier()
573
-
574
- # Coarse grained, Run Sieve Analysis
575
- # Gravel or Sand
576
- return self._coarse_soil_classifier()
577
-
578
- def _dual_soil_classifier(self) -> str:
579
- fine_soil = self.atterberg_limits.type_of_fines
580
- coarse_soil = self.psd.type_of_coarse
581
-
582
- return f"{coarse_soil}{self.psd.grade()}-{coarse_soil}{fine_soil}"
583
-
584
- def _coarse_soil_classifier(self) -> str:
585
- coarse_soil = self.psd.type_of_coarse
586
-
587
- # More than 12% pass No. 200 sieve
588
- if self.psd.fines > 12:
589
- # Above A-line
590
- if self.atterberg_limits.above_A_LINE():
591
- soil_class = f"{coarse_soil}{CLAY}"
592
-
593
- # Limit plot in hatched zone on plasticity chart
594
- elif self.atterberg_limits.limit_plot_in_hatched_zone():
595
- soil_class = f"{coarse_soil}{SILT}-{coarse_soil}{CLAY}"
596
-
597
- # Below A-line
598
- else:
599
- soil_class = f"{coarse_soil}{SILT}"
600
-
601
- elif 5 <= self.psd.fines <= 12:
602
- # Requires dual symbol based on graduation and plasticity chart
603
- if self.psd._has_particle_sizes():
604
- soil_class = self._dual_soil_classifier()
605
-
606
- else:
607
- fine_soil = self.atterberg_limits.type_of_fines
608
- soil_class = (
609
- f"{coarse_soil}{WELL_GRADED}-{coarse_soil}{fine_soil},"
610
- f"{coarse_soil}{POORLY_GRADED}-{coarse_soil}{fine_soil}"
611
- )
612
-
613
- # Less than 5% pass No. 200 sieve
614
- # Obtain Cc and Cu from grain size graph
615
- else:
616
- if self.psd._has_particle_sizes():
617
- soil_class = f"{coarse_soil}{self.psd.grade()}"
618
-
619
- else:
620
- soil_class = f"{coarse_soil}{WELL_GRADED} or {coarse_soil}{POORLY_GRADED}"
621
-
622
- return soil_class
623
-
624
- def _fine_soil_classifier(self) -> str:
625
- if self.atterberg_limits.liquid_limit < 50:
626
- # Low LL
627
- # Above A-line and PI > 7
628
- if (self.atterberg_limits.above_A_LINE()) and (
629
- self.atterberg_limits.plasticity_index > 7
630
- ):
631
- soil_class = f"{CLAY}{LOW_PLASTICITY}"
632
-
633
- # Limit plot in hatched area on plasticity chart
634
- elif self.atterberg_limits.limit_plot_in_hatched_zone():
635
- soil_class = f"{SILT}{LOW_PLASTICITY}-{CLAY}{LOW_PLASTICITY}"
636
-
637
- # Below A-line or PI < 4
638
- else:
639
- if self.organic:
640
- soil_class = f"{ORGANIC}{LOW_PLASTICITY}"
641
-
642
- else:
643
- soil_class = f"{SILT}{LOW_PLASTICITY}"
644
-
645
- # High LL
646
- else:
647
- # Above A-Line
648
- if self.atterberg_limits.above_A_LINE():
649
- soil_class = f"{CLAY}{HIGH_PLASTICITY}"
650
-
651
- # Below A-Line
652
- else:
653
- if self.organic:
654
- soil_class = f"{ORGANIC}{HIGH_PLASTICITY}"
655
- else:
656
- soil_class = f"{SILT}{HIGH_PLASTICITY}"
657
-
658
- return soil_class
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