Skip to content

Commit c4dd1c9

Browse files
authored
Merge pull request #314 from DataIntegrationGroup/observation-field-updates
BDMS 350: add groundwater_level_accuracy to observation model
2 parents b5adf60 + 6aeb49a commit c4dd1c9

9 files changed

Lines changed: 96 additions & 15 deletions

File tree

core/enums.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@
3030
ContactType: type[Enum] = build_enum_from_lexicon_category("contact_type")
3131
CoordinateMethod: type[Enum] = build_enum_from_lexicon_category("coordinate_method")
3232
WellPurpose: type[Enum] = build_enum_from_lexicon_category("well_purpose")
33-
DataQuality: type[Enum] = build_enum_from_lexicon_category("data_quality")
33+
GroundwaterLevelAccuracy: type[Enum] = build_enum_from_lexicon_category(
34+
"groundwater_level_accuracy"
35+
)
3436
DataSource: type[Enum] = build_enum_from_lexicon_category("data_source")
3537
DepthCompletionSource: type[Enum] = build_enum_from_lexicon_category(
3638
"depth_completion_source"

core/lexicon.json

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
{"name": "coordinate_method", "description": null},
1111
{"name": "country", "description": null},
1212
{"name": "county", "description": null},
13-
{"name": "data_quality", "description": null},
13+
{"name": "groundwater_level_accuracy", "description": null},
1414
{"name": "data_source", "description": null},
1515
{"name": "depth_completion_source", "description": null},
1616
{"name": "discharge_source", "description": null},
@@ -117,15 +117,15 @@
117117
{"categories": ["well_purpose"], "term": "Monitoring", "definition": "Monitoring"},
118118
{"categories": ["well_purpose"], "term": "Production", "definition": "Production"},
119119
{"categories": ["well_purpose"], "term": "Injection", "definition": "Injection"},
120-
{"categories": ["data_quality"], "term": "Water level accurate to within two hundreths of a foot", "definition": "Good"},
121-
{"categories": ["data_quality"], "term": "Water level accurate to within one foot", "definition": "Fair"},
122-
{"categories": ["data_quality"], "term": "Water level accuracy not to nearest foot or water level not repeatable", "definition": "Poor"},
123-
{"categories": ["data_quality"], "term": "Water level accurate to nearest foot (USGS accuracy level)", "definition": "Water level accurate to nearest foot (USGS accuracy level)"},
124-
{"categories": ["data_quality"], "term": "Water level accurate to nearest tenth of a foot (USGS accuracy level)", "definition": "Water level accurate to nearest tenth of a foot (USGS accuracy level)"},
125-
{"categories": ["data_quality"], "term": "Water level accurate to nearest one-hundredth of a foot (USGS accuracy level)", "definition": "Water level accurate to nearest one-hundredth of a foot (USGS accuracy level)"},
126-
{"categories": ["data_quality"], "term": "Water level accuracy not to nearest foot (USGS accuracy level)", "definition": "Water level accuracy not to nearest foot (USGS accuracy level)"},
127-
{"categories": ["data_quality"], "term": "Water level accuracy unknown (USGS accuracy level)", "definition": "Water level accuracy unknown (USGS accuracy level)"},
128-
{"categories": ["data_quality"], "term": "None", "definition": "NA"},
120+
{"categories": ["groundwater_level_accuracy"], "term": "Water level accurate to within two hundreths of a foot", "definition": "Good"},
121+
{"categories": ["groundwater_level_accuracy"], "term": "Water level accurate to within one foot", "definition": "Fair"},
122+
{"categories": ["groundwater_level_accuracy"], "term": "Water level accuracy not to nearest foot or water level not repeatable", "definition": "Poor"},
123+
{"categories": ["groundwater_level_accuracy"], "term": "Water level accurate to nearest foot (USGS accuracy level)", "definition": "Water level accurate to nearest foot (USGS accuracy level)"},
124+
{"categories": ["groundwater_level_accuracy"], "term": "Water level accurate to nearest tenth of a foot (USGS accuracy level)", "definition": "Water level accurate to nearest tenth of a foot (USGS accuracy level)"},
125+
{"categories": ["groundwater_level_accuracy"], "term": "Water level accurate to nearest one-hundredth of a foot (USGS accuracy level)", "definition": "Water level accurate to nearest one-hundredth of a foot (USGS accuracy level)"},
126+
{"categories": ["groundwater_level_accuracy"], "term": "Water level accuracy not to nearest foot (USGS accuracy level)", "definition": "Water level accuracy not to nearest foot (USGS accuracy level)"},
127+
{"categories": ["groundwater_level_accuracy"], "term": "Water level accuracy unknown (USGS accuracy level)", "definition": "Water level accuracy unknown (USGS accuracy level)"},
128+
{"categories": ["groundwater_level_accuracy"], "term": "None", "definition": "NA"},
129129
{"categories": ["data_source", "depth_completion_source", "discharge_source"], "term": "Reported by another agency", "definition": "Reported by another agency"},
130130
{"categories": ["data_source", "depth_completion_source"], "term": "From driller's log or well report", "definition": "From driller's log or well report"},
131131
{"categories": ["data_source", "depth_completion_source", "discharge_source"], "term": "Private geologist, consultant or univ associate", "definition": "Private geologist, consultant or univ associate"},

db/observation.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ class Observation(Base, AutoBaseMixin, ReleaseMixin):
7575
nullable=True,
7676
comment="The reason describes everything that can effect the observation the moment a sample/observation is attempted (e.g. obstruction, dry well, equipment failure); a null value must have an associated reason in the same record. Factors preventing the obtainment of the observation from the beginning of the field event to attempted sampling/observation (e.g. flat tire, locked gate, destroyed well) are not recorded here but in the notes field of the FieldEvent table; in this situation no sample/observation should be recorded.",
7777
)
78+
groundwater_level_accuracy: Mapped[str] = lexicon_term(
79+
nullable=True,
80+
comment="The accuracy describes the confidence in the observation value itself after the observation is obtained",
81+
)
7882

7983
# --- Relationships ---
8084
# Many-To-One: An Observation can be generated by one piece of Equipment.

docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ services:
2828
- POSTGRES_DB=${POSTGRES_DB}
2929
- POSTGRES_HOST=db
3030
- MODE=${MODE}
31+
- POSTGRES_PORT=5432
3132
- AUTHENTIK_DISABLE_AUTHENTICATION=${AUTHENTIK_DISABLE_AUTHENTICATION}
3233
ports:
3334
- 8000:8000

schemas/observation.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
UTCAwareDatetime,
3333
)
3434
from schemas.parameter import ParameterResponse
35-
from core.enums import Unit
35+
from core.enums import Unit, GroundwaterLevelAccuracy
3636

3737
# class GeothermalMixin:
3838
# depth: float
@@ -75,6 +75,7 @@ class CreateBaseObservation(BaseCreateModel, ValidateObservation):
7575
class CreateGroundwaterLevelObservation(CreateBaseObservation):
7676
measuring_point_height: float
7777
groundwater_level_reason: str
78+
groundwater_level_accuracy: GroundwaterLevelAccuracy
7879

7980

8081
class CreateWaterChemistryObservation(CreateBaseObservation):
@@ -117,6 +118,7 @@ class GroundwaterLevelObservationResponse(BaseObservationResponse):
117118
depth_to_water_bgs: float | None
118119
measuring_point_height: float | None
119120
groundwater_level_reason: str | None # NULL from legacy data
121+
groundwater_level_accuracy: GroundwaterLevelAccuracy | None
120122

121123
@model_validator(mode="before")
122124
def calculate_depth_to_water_bgs(self: Self) -> Self:

tests/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -760,6 +760,7 @@ def groundwater_level_observation(sensor, groundwater_level_sample):
760760
unit="ft",
761761
measuring_point_height=5.0,
762762
groundwater_level_reason="Water level not affected",
763+
groundwater_level_accuracy="Water level accurate to within two hundreths of a foot",
763764
)
764765
session.add(observation)
765766
session.commit()

tests/test_observation.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ def test_add_groundwater_level_observation(groundwater_level_sample, sensor):
9797
"sensor_id": sensor.id,
9898
"groundwater_level_reason": "Water level not affected",
9999
"unit": "ft",
100+
"groundwater_level_accuracy": "Water level accurate to within two hundreths of a foot",
100101
}
101102
response = client.post("/observation/groundwater-level", json=payload)
102103
data = response.json()
@@ -115,6 +116,7 @@ def test_add_groundwater_level_observation(groundwater_level_sample, sensor):
115116
data["depth_to_water_bgs"]
116117
== payload["value"] - payload["measuring_point_height"]
117118
)
119+
assert data["groundwater_level_accuracy"] == payload["groundwater_level_accuracy"]
118120

119121
cleanup_post_test(Observation, data["id"])
120122

@@ -324,6 +326,8 @@ def test_get_observation_by_id(
324326
assert data["release_status"] == obs.release_status
325327
if obs.parameter.id == groundwater_level_parameter_id:
326328
assert data["depth_to_water_bgs"] == obs.value - obs.measuring_point_height
329+
assert data["groundwater_level_reason"] == obs.groundwater_level_reason
330+
assert data["groundwater_level_accuracy"] == obs.groundwater_level_accuracy
327331
else:
328332
assert data["depth_to_water_bgs"] is None
329333

@@ -412,6 +416,10 @@ def test_get_groundwater_level_observation_by_id(groundwater_level_observation):
412416
data["measuring_point_height"]
413417
== groundwater_level_observation.measuring_point_height
414418
)
419+
assert (
420+
data["groundwater_level_accuracy"]
421+
== groundwater_level_observation.groundwater_level_accuracy
422+
)
415423

416424

417425
def test_get_groundwater_level_observation_by_id_404_not_found(
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from db import Observation, FieldEvent, FieldActivity, Sample
2+
from db.engine import session_ctx
3+
from transfers.well_transfer import WellTransferer
4+
from transfers.waterlevels_transfer import WaterLevelTransferer
5+
6+
7+
def test_water_level_with_unknown_data_quality():
8+
pointids = ["MG-020"]
9+
wt = WellTransferer(pointids=pointids)
10+
wt.transfer()
11+
12+
wlt = WaterLevelTransferer()
13+
input_df, cleaned_df = wlt._get_dfs()
14+
wlt.input_df = input_df
15+
wlt.cleaned_df = cleaned_df
16+
wlt.cleaned_df.at[wlt.cleaned_df.index[0], "DataQuality"] = "faux"
17+
18+
with session_ctx() as session:
19+
wlt._transfer_hook(session)
20+
21+
assert len(wlt.errors) == 1
22+
error = wlt.errors[0]
23+
assert error["pointid"] == "MG-020"
24+
assert error["table"] == "WaterLevels"
25+
assert error["field"] == "DataQuality"
26+
assert error["error"] == "Unknown DataQuality value: faux"
27+
28+
assert session.query(FieldEvent).count() == 2
29+
assert session.query(FieldActivity).count() == 2
30+
assert session.query(Sample).count() == 2
31+
assert session.query(Observation).count() == 2
32+
33+
session.query(Observation).delete()
34+
session.query(Sample).delete()
35+
session.query(FieldActivity).delete()
36+
session.query(FieldEvent).delete()

transfers/waterlevels_transfer.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,25 @@ def _transfer_hook(self, session: Session) -> None:
113113
except KeyError as e:
114114
continue
115115

116+
if not pd.isna(row.DataQuality):
117+
try:
118+
groundwater_level_accuracy = lexicon_mapper.map_value(
119+
f"LU_DataQuality:{row.DataQuality}"
120+
)
121+
if groundwater_level_accuracy == "None":
122+
# 0 maps to the string "None" in LU_DataQuality
123+
groundwater_level_accuracy = None
124+
except KeyError as e:
125+
self._capture_error(
126+
pointid=row.PointID,
127+
error=f"Unknown DataQuality value: {row.DataQuality}",
128+
table="WaterLevels",
129+
field="DataQuality",
130+
)
131+
continue
132+
else:
133+
groundwater_level_accuracy = None
134+
116135
release_status = "public" if row.PublicRelease else "private"
117136

118137
# field event
@@ -162,13 +181,20 @@ def _transfer_hook(self, session: Session) -> None:
162181
session.add(sample)
163182

164183
# Observation
165-
observation = self._make_observation(row, sample, dt_utc, glv)
184+
observation = self._make_observation(
185+
row, sample, dt_utc, glv, groundwater_level_accuracy
186+
)
166187
session.add(observation)
167188

168189
session.commit()
169190

170191
def _make_observation(
171-
self, row: pd.Series, sample: Sample, dt_utc: datetime, glv: str
192+
self,
193+
row: pd.Series,
194+
sample: Sample,
195+
dt_utc: datetime,
196+
glv: str,
197+
groundwater_level_accuracy: str | None,
172198
) -> Observation:
173199
if pd.isna(row.MPHeight):
174200
if pd.notna(row.DepthToWater) and pd.notna(row.DepthToWaterBGS):
@@ -197,7 +223,7 @@ def _make_observation(
197223
else:
198224
value = row.DepthToWater
199225

200-
# TODO: after sensors have been added to the database update sensor_id (or sensor) for waterlevels that come from db sensors (like e probes?)
226+
# TODO: after sensors have been added to the database update sensor_id (or sensor) for waterlevels that come from db sensors (like e probes?)
201227
observation = Observation(
202228
nma_pk_waterlevels=row.GlobalID,
203229
sample=sample,
@@ -209,6 +235,7 @@ def _make_observation(
209235
unit="ft",
210236
measuring_point_height=measuring_point_height,
211237
groundwater_level_reason=glv,
238+
groundwater_level_accuracy=groundwater_level_accuracy,
212239
)
213240
return observation
214241

0 commit comments

Comments
 (0)