7.1. Package concepts#

Every class in pycsamt.geology – rock entries, boreholes, fault traces – follows the same small set of conventions. They are worth learning once, here, rather than re-explaining on every page that follows.

7.1.1. Leaf values and containers#

Classes in this package come in two shapes. A leaf value object describes one fact – one rock/resistivity range, one borehole depth interval, one field measurement – as a @dataclass that validates itself the moment it is constructed:

>>> from pycsamt.geology import RockEntry
>>> entry = RockEntry(
...     name="Granite (weathered)",
...     rho_min=50,
...     rho_max=2000,
...     source="Palacky (1988)",
... )
>>> entry
RockEntry(name='Granite (weathered)', rho_min=50, rho_max=2000, color='#AAAAAA', description='', code=0, source='Palacky (1988)')

A container object collects many leaf values that belong together – every rock entry in a table, every depth interval in one borehole, every structural observation on one profile. Containers are plain classes with an explicit __init__, a from_csv classmethod, and a query API, rather than dataclasses:

>>> from pycsamt.geology import RockDatabase
>>> db = RockDatabase.default()
>>> db
RockDatabase(49 entries)
>>> db.entries[0]
RockEntry(name='Sulfide ore body', rho_min=0.001, rho_max=0.1, color='#2C3E50', description='Massive sulfides, pyrite, chalcopyrite', code=1, source='Telford, Geldart & Sheriff (1990); Slichter & Telkes (1942)')

The same split recurs throughout the package: Interval (leaf) inside Borehole (container); StructuralMeasurement, LinearMeasurement, and FaultTrace (leaves) inside StructuralModel (container). Once you know which shape a class is, you already know most of its API.

7.1.2. Repr, dictionaries, and metadata#

Every class inherits PyCSAMTObject, which is what produced the compact, readable repr() output above without either class writing its own __repr__. It also provides to_dict(), a plain-dict view that is easy to serialize or hand to pandas:

>>> entry.to_dict()
{'name': 'Granite (weathered)', 'rho_min': 50, 'rho_max': 2000, 'color': '#AAAAAA', 'description': '', 'code': 0, 'source': 'Palacky (1988)'}

Container objects that carry provenance – RockDatabase and StructuralModel – additionally mix in MetadataMixin for a free-form metadata dictionary. RockDatabase.default() already stamps its own origin into it, and update_metadata() adds to it without disturbing what is already there:

>>> db.update_metadata(project="demo-survey", reviewer="D.K.")
RockDatabase(49 entries)
>>> db.metadata_dict()
{'origin': 'default', 'project': 'demo-survey', 'reviewer': 'D.K.'}

Leaf value objects do not carry metadata – there is one fact, and provenance for the whole collection belongs on the container, not repeated on every entry.

7.1.3. Editing an object: what clone() and update() actually check#

clone() returns a modified copy and update() modifies an object in place; both accept the same keyword overrides and both call validate() afterwards. The important detail is what validate() actually does: the base implementation is a no-op, so whether an edit is checked depends on whether the specific class overrides validate() – and not every class does, because not every class has the same amount to check.

StructuralMeasurement has real cross-field structure to protect (dip_direction_deg must stay consistent with strike_deg), so it overrides validate() to re-run the same check its constructor uses. Editing it with an inconsistent value is caught either way:

>>> from pycsamt.geology import StructuralMeasurement
>>> bedding = StructuralMeasurement(
...     x=100.0, strike_deg=45.0, dip_deg=30.0,
...     dip_direction_deg=135.0, kind="bedding",
... )
>>> bedding.clone(dip_direction_deg=0.0)
Traceback (most recent call last):
    ...
ValueError: dip_direction_deg (0.0) is not within 20.0 deg of strike_deg (45.0) +/- 90 -- check for a transposed strike/dip-direction reading.

Interval validates a single constraint (bottom > top) in __post_init__, but does not override validate() – so construction is checked, and an edit via clone()/update() is not:

>>> from pycsamt.geology import Interval
>>> Interval(top=20.0, bottom=10.0, lithology="Sand")
Traceback (most recent call last):
    ...
ValueError: Interval bottom (10.0) must be > top (20.0).
>>> good = Interval(top=10.0, bottom=20.0, lithology="Sand")
>>> edited = good.clone(bottom=5.0)
>>> edited.thickness
-5.0

RockEntry goes further still and never validates at all, at construction or otherwise – rho_min greater than rho_max is accepted silently, since nothing downstream assumes an order between them beyond what contains() computes correctly regardless.

Warning

Do not rely on clone()/update() to catch every mistake. Whether an edited object gets re-validated is a per-class property of whether that class overrides validate(), not a guarantee of the base PyCSAMTObject API. When editing a value in place – particularly Interval bounds or any leaf field without an obvious cross-check – inspect the result, or reconstruct the object directly so __post_init__ runs again.

7.1.4. Position is always profile-relative#

Every class that has a location in this package places it with a single x in metres along a 2-D profile – never a latitude/ longitude pair:

>>> from pycsamt.geology import Borehole, FaultTrace
>>> bh = Borehole(
...     name="BH01", x=1050.0,
...     intervals=[
...         Interval(top=0, bottom=5, lithology="Topsoil"),
...         Interval(top=5, bottom=40, lithology="Sand"),
...     ],
... )
>>> bh
Borehole('BH01', x=1050.0 m, 2 intervals, depth=40.0 m)
>>> FaultTrace(x=500.0, dip_deg=70.0, downthrown_side="right")
FaultTrace(x=500.0 m, dip=70 deg, down=right, throw=?)

This matches how a ResistivityModel’s x_centers are already expressed, so a borehole, fault trace, or structural measurement lines up directly against a section plot without a coordinate lookup. Real-world placement – the latitude/ longitude or projected easting/northing behind that x – is pycsamt.site/pycsamt.gis’s responsibility, not this package’s; pycsamt.geology only ever needs to know where something sits along the line.

7.1.5. Where to go next#

Rock resistivity database covers RockDatabase in full, including where BUILTIN_ROCKS comes from and how to source a table from elsewhere. Borehole logs and Structural measurements cover the two other leaf/ container families introduced above. For applying any of this to a real resistivity section, see Interpretation.