diff --git a/core/lexicon.json b/core/lexicon.json index 14d2a1828..dbb268673 100644 --- a/core/lexicon.json +++ b/core/lexicon.json @@ -341,6 +341,9 @@ {"categories": [{"name": "sample_method", "description": null}], "term": "pump", "definition": "pump"}, {"categories": [{"name": "sample_method", "description": null}], "term": "thief sampler", "definition": "thief sampler"}, + {"categories": [{"name": "analysis_method_type", "description": null}], "term": "Laboratory", "definition": "A procedure performed on a physical sample in a controlled, off-site laboratory environment. These methods typically involve complex instrumentation, standardized reagents, and formal quality control protocols."}, + {"categories": [{"name": "analysis_method_type", "description": null}], "term": "Field Procedure", "definition": "A standardized procedure performed on-site at the time of sample collection. This can involve direct measurement of the environmental medium using a calibrated field instrument or a specific, documented technique for collecting a sample."}, + {"categories": [{"name": "analysis_method_type", "description": null}], "term": "Calculation", "definition": "A mathematical procedure used to derive a new data point from one or more directly measured values. This type is used to document the provenance of calculated data, providing an auditable trail."}, {"categories": [{"name": "organization", "description": null}], "term": "USGS", "definition": "US Geological Survey"}, {"categories": [{"name": "organization", "description": null}], "term": "TWDB", "definition": "Texas Water Development Board"}, diff --git a/db/__init__.py b/db/__init__.py index e448b2b9e..bf0db93be 100644 --- a/db/__init__.py +++ b/db/__init__.py @@ -19,6 +19,7 @@ from db.base import * from db.base import Base +from db.analysis_method import * from db.asset import * from db.collabnet import * from db.contact import * diff --git a/db/analysis_method.py b/db/analysis_method.py new file mode 100644 index 000000000..631668400 --- /dev/null +++ b/db/analysis_method.py @@ -0,0 +1,45 @@ +""" +This table is a citable library of abstract analytical methods and +standardized field procedures. +""" + +from typing import List, TYPE_CHECKING + +from sqlalchemy.orm import relationship, Mapped, mapped_column + +from db.base import Base, AutoBaseMixin, ReleaseMixin, lexicon_term + +if TYPE_CHECKING: + from db.observation import Observation + + +class AnalysisMethod(Base, AutoBaseMixin, ReleaseMixin): + """ + Represents a single, citable analytical method or standard procedure. + This is a lookup table. + """ + + # --- Columns --- + analysis_method_code: Mapped[str] = mapped_column( + nullable=True, + unique=True, + comment="The official code or identifier for the method (e.g., 'EPA 300.0').", + ) + analysis_method_name: Mapped[str] = mapped_column( + nullable=False, + comment="The common, human-readable name of the method (e.g., Ion Chromatography for Anions).", + ) + analysis_method_type: Mapped[str] = lexicon_term( + nullable=True, + comment="A controlled vocabulary field to categorize the method (e.g., 'Laboratory', 'Field Procedure', 'Calculation').", + ) + source_organization: Mapped[str] = lexicon_term( + nullable=True, + comment="The organization that published the method (e.g., 'US EPA', 'USGS').", + ) + + # --- Relationships --- + # One-To-Many: An AnalysisMethod can be used for many Observations. + observations: Mapped[List["Observation"]] = relationship( + "Observation", back_populates="analysis_method" + ) diff --git a/db/observation.py b/db/observation.py index 720fcda0f..af737a7cf 100644 --- a/db/observation.py +++ b/db/observation.py @@ -17,11 +17,19 @@ from sqlalchemy import ( ForeignKey, DateTime, + Integer, ) from sqlalchemy.orm import mapped_column, relationship, Mapped from db.base import Base, AutoBaseMixin, ReleaseMixin, lexicon_term +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from db.sample import Sample + from db.sensor import Sensor + from db.analysis_method import AnalysisMethod + class Observation(Base, AutoBaseMixin, ReleaseMixin): __versioned__ = {} @@ -29,6 +37,7 @@ class Observation(Base, AutoBaseMixin, ReleaseMixin): # NM_Aquifer fields for audits nma_pk_waterlevels: Mapped[str] = mapped_column(nullable=True) + # --- Foreign Keys --- sample_id: Mapped[int] = mapped_column( ForeignKey("sample.id", ondelete="CASCADE"), nullable=False, @@ -37,7 +46,11 @@ class Observation(Base, AutoBaseMixin, ReleaseMixin): ForeignKey("sensor.id", ondelete="CASCADE"), nullable=True, ) + analysis_method_id: Mapped[int] = mapped_column( + Integer, ForeignKey("analysis_method.id"), nullable=True + ) + # --- Columns --- observation_datetime: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, doc="Timestamp of the observation" ) @@ -63,12 +76,21 @@ class Observation(Base, AutoBaseMixin, ReleaseMixin): doc="Depth of the geothermal observation in feet", ) + # --- Relationships --- + # Many-To-One: An Observation can be generated by one piece of Equipment. sensor: Mapped["Sensor"] = relationship( # noqa: F821 "Sensor", back_populates="observations", passive_deletes=True ) # noqa: F821 + + # Many-To-One: An Observation is derived from one Sample. sample: Mapped["Sample"] = relationship( # noqa: F821 "Sample", back_populates="observations", passive_deletes=True ) # noqa: F821 + # Many-To-One: An Observation can be generated using one AnalysisMethod. + analysis_method: Mapped["AnalysisMethod"] = relationship( + "AnalysisMethod", back_populates="observations" + ) + # ============= EOF =============================================