Skip to content

Commit d5f2204

Browse files
authored
F/dynamic probing (#14)
* Add support for Dynamic Probing (DP) * Add alternative codes for penetration rate (B, C), torque (V, AB) and ramming (S, SA).
1 parent 10901cf commit d5f2204

16 files changed

Lines changed: 1814 additions & 179 deletions

.gitattributes

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# This .gitattributes file primarily made for the test files. Some test files are deliberatly constructed with wrong
2+
# line endings for test purposes and should be specified here.
3+
4+
# Don't let git modify these:
5+
*.[Cc][Pp][Tt] binary
6+
*.[Dd][Tt][Rr] binary
7+
*.[Hh][Ff][Aa] binary
8+
*.[Jj][Bb][123Tt] binary
9+
*.[Vv][Ii][Mm] binary
10+
*.[Ss][Tt][Dd] binary
11+
*.[Tt][Oo][Tt] binary
12+
13+
# std, cpt, tot and dtr files are specified as eol=crlf
14+
*.[Cc][Pp][Tt] text eol=crlf
15+
*.[Dd][Tt][Rr] text eol=crlf
16+
*.[Ss][Tt][Dd] text eol=crlf
17+
*.[Tt][Oo][Tt] text eol=crlf
18+
19+
# Shell scripts may not contain crlf
20+
*.sh -crlf
21+
22+
# Yaml file should not contain crlf
23+
*.yaml -crlf
24+
*.yml -crlf

CHANGES.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# NGI Python SGF Parser Package
22

3+
_2024-08-22_
4+
5+
Version 0.0.1b5
6+
7+
Add support for Dynamic Probing (DP).
8+
39
_2024-08-08_
410

511
Version 0.0.1b4

poetry.lock

Lines changed: 198 additions & 150 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ exclude = '''
1515

1616
[tool.poetry]
1717
name = "sgf-parser"
18-
version = "0.0.1b4"
18+
version = "0.0.1b5"
1919
description = "Parser for the Swedish Geotechnical Society / Svenska Geotekniska Föreningen (SGF) data format"
2020
license = "MIT"
2121
authors = ["Jostein Leira <jostein@leira.net>"]

src/sgf_parser/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from sgf_parser.models.types import StopCode, ParseState
44
from sgf_parser.models.method import Method, MethodData
55
from sgf_parser.models.method_cpt import MethodCPT, MethodCPTData
6+
from sgf_parser.models.method_dp import MethodDP, MethodDPData
67
from sgf_parser.models.method_dt import MethodDT, MethodDTData
78
from sgf_parser.models.method_rp import MethodRP, MethodRPData
89
from sgf_parser.models.method_srs import MethodSRS, MethodSRSData

src/sgf_parser/models/method.py

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
class MethodData(BaseModel, abc.ABC):
1515
@classmethod
1616
def _fix_malformed_data(cls, code: str) -> str | None:
17+
"""Wrong K codes as "4,0" will be converted to "40"."""
1718
return re.sub("[^0-9]", "", code)
1819

1920
@classmethod
@@ -53,20 +54,19 @@ def comment_code_format(cls, data: Any) -> Any:
5354
"""
5455
if isinstance(data, dict):
5556
if "K" in data and data["K"] is not None:
56-
5757
# We want to interpret K as a stop code (with integer value)
5858
# sometimes K is a string with e.g. "SAND", in that case we move it to T
5959
K_is_digit = any(char.isdigit() for char in data["K"])
60-
60+
6161
if not K_is_digit:
6262
# move it to T (remarks column)
6363
if "T" not in data:
6464
data["T"] = data["K"]
6565
else:
6666
data["T"] = f"{data['K']}, {data['T']}"
67-
67+
6868
del data["K"]
69-
69+
7070
else:
7171
# we identify the most important stop code, and move the rest to T
7272
_code, _rest = cls._extract_comment_code(data)
@@ -83,6 +83,52 @@ def comment_code_format(cls, data: Any) -> Any:
8383
comment_code: int | None = Field(None, alias="K")
8484
remarks: str | None = Field(None, alias="T")
8585

86+
@model_validator(mode="before")
87+
@classmethod
88+
def penetration_rate_validator(cls, data: Any) -> Any:
89+
"""
90+
If the penetration rate (B mm/s) is not set, but C is (s/0.2m) then convert C to B and set B.
91+
"""
92+
if isinstance(data, dict):
93+
if ("B" not in data or data["B"] is None) and "C" in data and data["C"] is not None:
94+
# B is not set, but C is, convert C to B
95+
try:
96+
data["B"] = 200 / float(data["C"])
97+
except (ZeroDivisionError, ValueError):
98+
data["B"] = None
99+
100+
return data
101+
102+
@model_validator(mode="before")
103+
@classmethod
104+
def torque_validator(cls, data: Any) -> Any:
105+
"""
106+
If the torque V (kNm) is not set, but AB is (Nm) then convert AB to V and set V.
107+
"""
108+
if isinstance(data, dict):
109+
if ("V" not in data or data["V"] is None) and "AB" in data and data["AB"] is not None:
110+
try:
111+
data["V"] = float(data["AB"]) / 1000
112+
except ValueError:
113+
data["V"] = None
114+
115+
return data
116+
117+
@model_validator(mode="before")
118+
@classmethod
119+
def ramming_validator(cls, data: Any) -> Any:
120+
"""
121+
If the ramming S (blows/0.2m) is not set, but SA is (blows/0.1m) then convert SA to S and set S.
122+
"""
123+
if isinstance(data, dict):
124+
if ("S" not in data or data["S"] is None) and "SA" in data and data["SA"] is not None:
125+
try:
126+
data["S"] = float(data["SA"]) * 2
127+
except ValueError:
128+
data["S"] = None
129+
130+
return data
131+
86132
def __repr__(self):
87133
return f"<{self.__class__.__name__} {self.depth}>"
88134

src/sgf_parser/models/method_dp.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
from decimal import Decimal
2+
from typing import Literal, Any
3+
4+
from pydantic import Field, model_validator, computed_field, AliasChoices
5+
6+
from sgf_parser.models import MethodData, Method, MethodType
7+
from sgf_parser.models.types import DPType
8+
9+
10+
class MethodDPData(MethodData):
11+
"""
12+
Dynamic Probing Data
13+
"""
14+
15+
def __init__(self, **kwargs):
16+
super().__init__(**kwargs)
17+
18+
depth: Decimal = Field(..., alias="D", description="Depth (m)")
19+
20+
penetration_force: Decimal | None = Field(None, alias="A", description="Penetration force (kN)")
21+
penetration_rate: Decimal | None = Field(
22+
None,
23+
validation_alias=AliasChoices(
24+
"B", # mm/s
25+
"C", # s/0.2m
26+
),
27+
description="Penetration rate (mm/s)",
28+
)
29+
torque: Decimal | None = Field(
30+
None,
31+
validation_alias=AliasChoices(
32+
"V", # kNm
33+
"AB", # Nm
34+
),
35+
description="Torque (kNm)",
36+
)
37+
ramming: Decimal = Field(None, validation_alias=AliasChoices("S", "SA"), description="Ramming (Blow/0.2 m)")
38+
rotation_rate: Decimal | None = Field(None, alias="R", description="Rotation rate (rpm)")
39+
increased_rotation_rate: bool | None = Field(None, alias="AQ")
40+
41+
42+
class MethodDP(Method):
43+
"""
44+
Dynamic Probing (Swedish Hejarsondering)
45+
"""
46+
47+
def __init__(self, **kwargs):
48+
super().__init__(**kwargs)
49+
50+
name: str = "DP"
51+
method_type: Literal[MethodType.DP] = MethodType.DP
52+
method_data_type: type[MethodDPData] = MethodDPData
53+
54+
type: DPType
55+
56+
predrilling_depth: Decimal = Field(Decimal("0"), alias="HO")
57+
58+
cone_type: str | None = Field(
59+
None, validation_alias=AliasChoices("KonTyp", "HN", "HC"), description="Type of cone used."
60+
)
61+
cushion_type: str | None = Field(None, alias="DynTyp", description="Type of impact cushion used.")
62+
use_damper: bool | None = Field(None, alias="Gummi", description="Use of damper.")
63+
64+
method_data: list[MethodDPData] = []
65+
66+
@computed_field
67+
def depth_top(self) -> Decimal | None:
68+
if not self.method_data:
69+
return None
70+
71+
return min(method_data.depth for method_data in self.method_data)
72+
73+
@computed_field
74+
def depth_base(self) -> Decimal | None:
75+
if not self.method_data:
76+
return None
77+
78+
return max(method_data.depth for method_data in self.method_data)
79+
80+
@model_validator(mode="before")
81+
@classmethod
82+
def set_operation(cls, data: Any) -> Any:
83+
if isinstance(data, dict):
84+
if "HM" in data and data["HM"] is not None:
85+
data["type"] = {
86+
"8": DPType.DPSH_A,
87+
"108A": DPType.DPSH_A,
88+
"108B": DPType.DPL,
89+
"108C": DPType.DPM,
90+
"108D": DPType.DPH,
91+
"108E": DPType.DPSH_B,
92+
}[data["HM"]]
93+
94+
return data
95+
96+
@computed_field
97+
def stopcode(self) -> int | None:
98+
if not self.method_data:
99+
return None
100+
101+
return self.method_data[-1].comment_code

src/sgf_parser/models/method_type.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ class MethodType(enum.StrEnum):
77
"""
88

99
CPT = "CPT"
10+
DP = "DP"
11+
DT = "DT"
1012
RP = "RP"
1113
SRS = "SRS"
1214
SVT = "SVT"
1315
TOT = "TOT"
1416
WST = "WST"
15-
DT = "DT"

src/sgf_parser/models/types.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,16 @@ class Operation(enum.StrEnum):
134134

135135
MANUAL = "MANUAL"
136136
MECHANICAL = "MECHANICAL"
137+
138+
139+
class DPType(enum.StrEnum):
140+
"""
141+
DP Type (Dynamic Probing)
142+
143+
"""
144+
145+
DPSH_A = "DPSH-A"
146+
DPL = "DPL"
147+
DPM = "DPM"
148+
DPH = "DPH"
149+
DPSH_B = "DPSH-B"

src/sgf_parser/parser.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
# to contain only a-z and A-Z. In addition, the Geotech AB extension,
1212
# have date fields (key "%") with no "=" separating the key from the
1313
# value...
14-
_RE_FIELD_SEP = re.compile(r",(?:(?=[a-zA-Z])|(?=%))")
14+
_RE_FIELD_SEP = re.compile(r",(?=[a-zA-Z%])")
1515

1616

1717
class Parser:
@@ -26,6 +26,12 @@ class Parser:
2626
"102": models.MethodWST,
2727
"107A": models.MethodCPT,
2828
"107B": models.MethodCPT,
29+
"8": models.MethodDP,
30+
"108A": models.MethodDP,
31+
"108B": models.MethodDP,
32+
"108C": models.MethodDP,
33+
"108D": models.MethodDP,
34+
"108E": models.MethodDP,
2935
"23": models.MethodRP,
3036
"24": models.MethodTOT,
3137
"12": models.MethodSRS,

0 commit comments

Comments
 (0)