7.2. Rock resistivity database#
RockDatabase and
RockEntry are introduced in
Lithology classification, which also covers nearest-midpoint
versus overlap classification, the built-in table’s literature
provenance, loading a project-specific table from CSV, and the two
documented edge cases around non-finite or out-of-range resistivity
values – all with a worked example against a real resistivity
section, which this page does not repeat. What that page does not
cover is where a table can come from besides the bundled default or a
CSV file, and what a single RockEntry actually is as an object.
Both matter once a project wants to share one table across several
tools rather than keep a CSV in sync by hand.
7.2.1. RockEntry as a value object#
A RockEntry is a leaf value object in the
sense introduced in Package concepts: it never validates
rho_min/rho_max against each other, because nothing downstream
requires an order between them beyond what its own methods already
compute correctly regardless. rho_mid is the geometric-mean
midpoint that nearest-midpoint classification searches over, and
contains() is the containment check method="overlap" uses:
>>> from pycsamt.geology import RockEntry
>>> entry = RockEntry(
... name="Laterite", rho_min=80, rho_max=600,
... color="#B5651D", source="Site report 2024",
... )
>>> entry.rho_mid
219.08902300206645
>>> entry.log_rho_mid
2.340620618687794
>>> entry.contains(150.0), entry.contains(700.0)
(True, False)
A RockDatabase can be built directly from a
list of entries, without going through a CSV file at all – useful
when entries are generated programmatically rather than typed into a
spreadsheet:
>>> from pycsamt.geology import RockDatabase
>>> entries = [
... entry,
... RockEntry(name="Saprolite", rho_min=20, rho_max=300,
... color="#C9A66B", source="Site report 2024"),
... RockEntry(name="Fresh basement", rho_min=3000, rho_max=200000,
... color="#4A4A4A", source="Site report 2024"),
... ]
>>> db = RockDatabase(entries)
>>> db
RockDatabase(3 entries)
>>> db.metadata
{}
>>> db.classify(150.0).name
'Laterite'
Note that db.metadata is empty here. Unlike
default() (which stamps
{"origin": "default"}) or from_csv()
(which stamps the source path), the plain constructor does not invent
provenance on your behalf – pass metadata= yourself if the table’s
origin should travel with it.
Classifying the same three resistivity values against db and
against the built-in default makes the point of having more than one
table concrete: the query never changes, only the answer does.
View the source-comparison figure source codeClick to inspect and copy the complete code
1def make_rock_database_source_comparison() -> None:
2 """Write the "same resistivity, two tables" comparison figure.
3
4 Classifies three resistivity values against both the built-in
5 ``RockDatabase.default()`` and the small regional table built
6 directly on the page, showing that swapping the active table
7 changes the classified name even though the query value never
8 changes.
9 """
10 db_default = RockDatabase.default()
11 db_regional = RockDatabase(_REGIONAL_ENTRIES)
12
13 fig, ax = plt.subplots(figsize=(9, 4.2))
14
15 y_regional, y_default = 0.0, 1.0
16
17 for e in _REGIONAL_ENTRIES:
18 ax.fill_between(
19 [e.rho_min, e.rho_max], y_regional - 0.18, y_regional + 0.18,
20 color=e.color, edgecolor="0.25", linewidth=0.6, alpha=0.9,
21 )
22 ax.text(
23 e.rho_mid, y_regional, e.name, ha="center", va="center", fontsize=8,
24 color="white" if e.name != "Laterite" else "black", fontweight="bold",
25 )
26
27 for rho in _TEST_RHOS:
28 d = db_default.classify(rho)
29 ax.plot(
30 [rho, rho], [y_regional + 0.22, y_default - 0.06],
31 color="0.4", linestyle=":", linewidth=1.0, zorder=1,
32 )
33 ax.scatter([rho], [y_default], color=d.color, edgecolor="0.2", s=140, zorder=3)
34 ax.text(rho, y_default + 0.16, f"{rho:.0f} Ohm.m", ha="center", fontsize=8, color="0.25")
35 ax.text(rho, y_default - 0.20, d.name, ha="center", fontsize=8, color="0.15")
36
37 ax.set_xscale("log")
38 ax.set_xlim(10, 3e5)
39 ax.set_ylim(-0.4, 1.5)
40 ax.set_yticks([y_regional, y_default])
41 ax.set_yticklabels(
42 ["Regional\n(3-entry file:// table)", "Built-in default()\n(49 entries)"],
43 fontsize=9,
44 )
45 ax.set_xlabel(r"Resistivity ($\Omega\,\mathrm{m}$, log scale)")
46 ax.set_title("The same resistivity value, classified against two different tables")
47 ax.grid(axis="x", which="both", alpha=0.25)
48 for spine in ("top", "right", "left"):
49 ax.spines[spine].set_visible(False)
50
51 fig.tight_layout()
52 fig.savefig(
53 IMAGES / "rock_database_source_comparison.png", dpi=200, bbox_inches="tight"
54 )
55 plt.close(fig)
45, 150, and 5000 ohm metres, each classified against
the three-entry regional table (bottom) and
RockDatabase.default() (top, dot color and label only – its
full 49-entry range chart is already in
Lithology classification). Every one of the three values
gets a different name from the two tables; a downstream map or
report is only as meaningful as the table it was classified
against, which is exactly why that table’s provenance belongs in
metadata.#
7.2.2. Sourcing a table from elsewhere#
No public, machine-readable service currently maps a rock or
lithology name directly to a resistivity range – Data Series 595
covers 90 samples from one watershed, the Geochemical and Geophysical
Characteristics of the Conterminous US dataset’s rasters are
compressive strength and hydraulic conductivity rather than
resistivity, and Macrostrat’s REST API covers lithology names and
stratigraphic units rather than resistivity values. That is why
BUILTIN_ROCKS is a literature
compilation rather than a live fetch. It does not mean a table must
always be either that compilation or a hand-maintained CSV, though: a
project or organisation with its own controlled table – an internal
REST endpoint, a JSON file on a shared drive or object store – can
plug it in through RockPropertyProvider,
a two-method contract every rock-property source satisfies:
>>> from pycsamt.geology.rock_providers import (
... RockPropertyProvider, LocalRockPropertyProvider,
... )
>>> isinstance(LocalRockPropertyProvider(), RockPropertyProvider)
True
fetch() returns (entries, metadata) – a list of RockEntry
plus a small provenance dictionary – and that is the entire contract;
LocalRockPropertyProvider
above simply wraps default() or from_csv() behind it, so it can
stand in wherever a provider is expected but no remote source is
actually involved.
RemoteRockPropertyProvider
is the provider behind from_url():
it fetches a JSON array of objects shaped like RockEntry from any
URL urllib.request.urlopen() can open, including file:// for
a table on a shared drive, not only http(s)://. The example below
writes a small regional table to disk and loads it back purely as a
local file URL – no network access involved, and the same mechanics
apply to an internal https:// endpoint:
>>> import json
>>> from pathlib import Path
>>> table_path = Path("configuration/regional_rocks.json")
>>> table_path.parent.mkdir(parents=True, exist_ok=True)
>>> _ = table_path.write_text(json.dumps([
... {"name": "Laterite", "rho_min": 80, "rho_max": 600,
... "color": "#B5651D", "source": "Site report 2024"},
... {"name": "Saprolite", "rho_min": 20, "rho_max": 300,
... "color": "#C9A66B", "source": "Site report 2024"},
... {"name": "Fresh basement", "rho_min": 3000, "rho_max": 200000,
... "color": "#4A4A4A", "source": "Site report 2024"},
... ]))
>>> db_remote = RockDatabase.from_url(
... table_path.resolve().as_uri(),
... cache_dir="configuration/rock_db_cache",
... )
>>> db_remote.classify(150.0).name
'Laterite'
>>> db_remote.metadata["origin"], db_remote.metadata["cache_hit"]
('url', False)
A second call within ttl_seconds (one day by default) reuses the
cached response instead of fetching again:
>>> db_remote_again = RockDatabase.from_url(
... table_path.resolve().as_uri(),
... cache_dir="configuration/rock_db_cache",
... )
>>> db_remote_again.metadata["cache_hit"]
True
The cache lives under cache_dir (configuration/rock_db_cache
above; $PYCSAMT_ROCKDB_CACHE or ~/.pycsamt/rock_db by default),
keyed by a hash of the URL, matching the convention already used by
pycsamt.ai._zoo’s pretrained-model cache. Pass force=True to
bypass a fresh cache entry and re-fetch anyway.
If the fetch itself fails – network error, timeout, or a response
that is not a JSON array shaped like RockEntry – from_url
does not raise by default. It falls back first to a stale cache entry
from an earlier successful fetch, if one exists, and otherwise to
LocalRockPropertyProvider,
i.e. the same built-in table default()
returns:
>>> db_fallback = RockDatabase.from_url(
... "file:///no/such/table.json",
... cache_dir="configuration/rock_db_cache",
... )
>>> db_fallback.metadata["origin"]
'default-fallback'
>>> len(db_fallback)
49
Warning
Falling back silently is convenient for a demo but can mask a
genuinely broken endpoint in production – a database quietly
swapping from a regional table to the generic built-in one changes
every classification downstream without an obvious signal. Check
metadata["origin"] after every from_url()/from_provider()
call in an automated pipeline, or pass fallback=False to raise
RockProviderFetchError
instead and handle the failure explicitly.
from_provider() is the generic
entry point behind from_url – call it directly for anything that
is not a plain URL fetch, such as a provider pre-configured with
authentication, one composing several sources, or a lightweight
provider written for a single project. Nothing beyond the two-method
protocol is required:
>>> class InMemoryProvider:
... def __init__(self, entries):
... self._entries = entries
... def fetch(self):
... return list(self._entries), {"origin": "in-memory"}
>>> provider = InMemoryProvider([entry])
>>> isinstance(provider, RockPropertyProvider)
True
>>> db_custom = RockDatabase.from_provider(provider)
>>> db_custom, db_custom.metadata
(RockDatabase(1 entries), {'origin': 'in-memory'})
7.2.3. Where to go next#
Lithology classification covers classification itself in depth – nearest-midpoint versus overlap matching, extrapolation beyond the table’s coverage, and classifying a full resistivity section. Borehole logs and Structural measurements cover the other two data families in this package.