pcsf 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pcsf/__init__.py +100 -0
- pcsf/_logging.py +73 -0
- pcsf/_version.py +19 -0
- pcsf/adapters/__init__.py +41 -0
- pcsf/adapters/_common.py +91 -0
- pcsf/adapters/emc.py +296 -0
- pcsf/adapters/mare2dem.py +207 -0
- pcsf/adapters/modem.py +344 -0
- pcsf/adapters/occam2d.py +246 -0
- pcsf/algorithms/__init__.py +65 -0
- pcsf/algorithms/comparison.py +145 -0
- pcsf/algorithms/locate.py +219 -0
- pcsf/algorithms/projection.py +90 -0
- pcsf/algorithms/registration.py +211 -0
- pcsf/algorithms/regrid.py +146 -0
- pcsf/algorithms/sampling.py +345 -0
- pcsf/algorithms/uncertainty.py +384 -0
- pcsf/api/__init__.py +375 -0
- pcsf/api/property.py +413 -0
- pcsf/api/registry.py +421 -0
- pcsf/api/types.py +45 -0
- pcsf/cli/__init__.py +5 -0
- pcsf/cli/__main__.py +8 -0
- pcsf/cli/main.py +183 -0
- pcsf/compat/__init__.py +20 -0
- pcsf/compat/h5py.py +71 -0
- pcsf/compat/numpy.py +32 -0
- pcsf/compat/optional.py +101 -0
- pcsf/compat/python.py +47 -0
- pcsf/core/__init__.py +89 -0
- pcsf/core/conformance.py +311 -0
- pcsf/core/encodings.py +216 -0
- pcsf/core/hdf5.py +639 -0
- pcsf/core/provenance.py +170 -0
- pcsf/core/record.py +301 -0
- pcsf/core/schema.py +939 -0
- pcsf/core/text.py +1320 -0
- pcsf/core/versioning.py +113 -0
- pcsf/datasets/__init__.py +58 -0
- pcsf/datasets/mare2dem_tiny/tiny.1.resistivity +8 -0
- pcsf/datasets/mare2dem_tiny/tiny.ele.txt +4 -0
- pcsf/datasets/mare2dem_tiny/tiny.logfile +10 -0
- pcsf/datasets/mare2dem_tiny/tiny.node.txt +6 -0
- pcsf/datasets/mare2dem_tiny/tiny.poly +19 -0
- pcsf/datasets/modem_tiny/tiny.dat +12 -0
- pcsf/datasets/modem_tiny/tiny_NLCG.log +5 -0
- pcsf/datasets/modem_tiny/tiny_NLCG_002.rho +19 -0
- pcsf/datasets/occam2d_tiny/ITER01.iter +11 -0
- pcsf/datasets/occam2d_tiny/ITER02.iter +11 -0
- pcsf/datasets/occam2d_tiny/LogFile.logfile +10 -0
- pcsf/datasets/occam2d_tiny/Occam2DMesh +21 -0
- pcsf/datasets/occam2d_tiny/Occam2DModel +11 -0
- pcsf/datasets/occam2d_tiny/OccamDataFile.dat +13 -0
- pcsf/decorators.py +263 -0
- pcsf/exceptions.py +181 -0
- pcsf/native/__init__.py +21 -0
- pcsf/native/_tokens.py +85 -0
- pcsf/native/mare2dem.py +411 -0
- pcsf/native/modem.py +419 -0
- pcsf/native/occam2d.py +643 -0
- pcsf/plugins/__init__.py +138 -0
- pcsf/py.typed +0 -0
- pcsf/schemas/__init__.py +99 -0
- pcsf/schemas/inversion-record.schema.json +83 -0
- pcsf/schemas/model-provenance.schema.json +28 -0
- pcsf/studio/__init__.py +201 -0
- pcsf/studio/__main__.py +8 -0
- pcsf/studio/convert.py +282 -0
- pcsf/studio/convert_page.py +664 -0
- pcsf/studio/figures.py +622 -0
- pcsf/studio/resources/pcsf-studio.png +0 -0
- pcsf/studio/resources/pcsf-studio.svg +44 -0
- pcsf/studio/sources.py +265 -0
- pcsf/studio/style.py +473 -0
- pcsf/studio/summary.py +444 -0
- pcsf/studio/window.py +2179 -0
- pcsf/utils/__init__.py +19 -0
- pcsf/utils/arrays.py +116 -0
- pcsf/utils/geo.py +101 -0
- pcsf/utils/hashing.py +91 -0
- pcsf/utils/io.py +154 -0
- pcsf/utils/text.py +42 -0
- pcsf-0.1.0.dist-info/METADATA +354 -0
- pcsf-0.1.0.dist-info/RECORD +88 -0
- pcsf-0.1.0.dist-info/WHEEL +5 -0
- pcsf-0.1.0.dist-info/entry_points.txt +5 -0
- pcsf-0.1.0.dist-info/licenses/LICENSE +28 -0
- pcsf-0.1.0.dist-info/top_level.txt +1 -0
pcsf/__init__.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""pcsf -- Portable Common Subsurface Format.
|
|
2
|
+
|
|
3
|
+
An exchange format for electromagnetic inversion models, with a binary
|
|
4
|
+
encoding (PCSF, HDF5) and a lossless text encoding (PCSM, optionally
|
|
5
|
+
gzip-compressed), and the algorithms to sample and compare models of
|
|
6
|
+
different geometry.
|
|
7
|
+
|
|
8
|
+
Quick start::
|
|
9
|
+
|
|
10
|
+
import pcsf
|
|
11
|
+
|
|
12
|
+
model = pcsf.read("model.pcsf")
|
|
13
|
+
print(pcsf.info(model))
|
|
14
|
+
report = pcsf.validate(model)
|
|
15
|
+
pcsf.write(model, "model.pcsm.gz")
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from . import datasets
|
|
19
|
+
from ._version import FORMAT_VERSION, __version__
|
|
20
|
+
from .api import convert, detect, info, read, validate, write
|
|
21
|
+
from .core import (
|
|
22
|
+
DerivedVolume,
|
|
23
|
+
Grid2DGeometry,
|
|
24
|
+
Grid3DGeometry,
|
|
25
|
+
InversionRecord,
|
|
26
|
+
LineEntry,
|
|
27
|
+
ModelProvenance,
|
|
28
|
+
MultilineGeometry,
|
|
29
|
+
PCSFModel,
|
|
30
|
+
StationTable,
|
|
31
|
+
TopographyPerStation,
|
|
32
|
+
TopographyRaster,
|
|
33
|
+
UnstructuredMeshGeometry,
|
|
34
|
+
attach_inversion_record,
|
|
35
|
+
check_conformance,
|
|
36
|
+
check_inversion_record,
|
|
37
|
+
get_inversion_record,
|
|
38
|
+
pcsf_to_pcsm,
|
|
39
|
+
pcsm_to_pcsf,
|
|
40
|
+
read_pcsf,
|
|
41
|
+
read_pcsm,
|
|
42
|
+
write_pcsf,
|
|
43
|
+
write_pcsm,
|
|
44
|
+
)
|
|
45
|
+
from .exceptions import (
|
|
46
|
+
ConformanceError,
|
|
47
|
+
EncodingError,
|
|
48
|
+
FormatError,
|
|
49
|
+
GeometryError,
|
|
50
|
+
MissingDependencyError,
|
|
51
|
+
PCSFError,
|
|
52
|
+
PCSMSyntaxError,
|
|
53
|
+
VersionError,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
__all__ = [
|
|
57
|
+
"FORMAT_VERSION",
|
|
58
|
+
# errors
|
|
59
|
+
"ConformanceError",
|
|
60
|
+
# data model
|
|
61
|
+
"DerivedVolume",
|
|
62
|
+
"EncodingError",
|
|
63
|
+
"FormatError",
|
|
64
|
+
"GeometryError",
|
|
65
|
+
"Grid2DGeometry",
|
|
66
|
+
"Grid3DGeometry",
|
|
67
|
+
"InversionRecord",
|
|
68
|
+
"LineEntry",
|
|
69
|
+
"MissingDependencyError",
|
|
70
|
+
"ModelProvenance",
|
|
71
|
+
"MultilineGeometry",
|
|
72
|
+
"PCSFError",
|
|
73
|
+
"PCSFModel",
|
|
74
|
+
"PCSMSyntaxError",
|
|
75
|
+
"StationTable",
|
|
76
|
+
"TopographyPerStation",
|
|
77
|
+
"TopographyRaster",
|
|
78
|
+
"UnstructuredMeshGeometry",
|
|
79
|
+
"VersionError",
|
|
80
|
+
"__version__",
|
|
81
|
+
# encodings and records
|
|
82
|
+
"attach_inversion_record",
|
|
83
|
+
"check_conformance",
|
|
84
|
+
"check_inversion_record",
|
|
85
|
+
# facade
|
|
86
|
+
"convert",
|
|
87
|
+
"datasets",
|
|
88
|
+
"detect",
|
|
89
|
+
"get_inversion_record",
|
|
90
|
+
"info",
|
|
91
|
+
"pcsf_to_pcsm",
|
|
92
|
+
"pcsm_to_pcsf",
|
|
93
|
+
"read",
|
|
94
|
+
"read_pcsf",
|
|
95
|
+
"read_pcsm",
|
|
96
|
+
"validate",
|
|
97
|
+
"write",
|
|
98
|
+
"write_pcsf",
|
|
99
|
+
"write_pcsm",
|
|
100
|
+
]
|
pcsf/_logging.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Package logger.
|
|
2
|
+
|
|
3
|
+
pcsf never configures the root logger. It logs to
|
|
4
|
+
``logging.getLogger("pcsf")``, which carries a ``NullHandler``, so
|
|
5
|
+
that applications decide where messages go. For interactive use,
|
|
6
|
+
:func:`enable_console_logging` prints them.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
|
|
13
|
+
__all__ = ["enable_console_logging", "get_logger"]
|
|
14
|
+
|
|
15
|
+
_ROOT = "pcsf"
|
|
16
|
+
logging.getLogger(_ROOT).addHandler(logging.NullHandler())
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_logger(name: str | None = None) -> logging.Logger:
|
|
20
|
+
"""Return the package logger or one of its children.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
name : str, optional
|
|
25
|
+
Module name, usually ``__name__``. Names outside the package
|
|
26
|
+
are attached under ``pcsf``.
|
|
27
|
+
|
|
28
|
+
Returns
|
|
29
|
+
-------
|
|
30
|
+
logging.Logger
|
|
31
|
+
``pcsf`` itself when *name* is empty, otherwise a child.
|
|
32
|
+
|
|
33
|
+
Examples
|
|
34
|
+
--------
|
|
35
|
+
>>> from pcsf._logging import get_logger
|
|
36
|
+
>>> get_logger("pcsf.core").name, get_logger("plugin").name
|
|
37
|
+
('pcsf.core', 'pcsf.plugin')
|
|
38
|
+
"""
|
|
39
|
+
if not name:
|
|
40
|
+
return logging.getLogger(_ROOT)
|
|
41
|
+
if name == _ROOT or name.startswith(_ROOT + "."):
|
|
42
|
+
return logging.getLogger(name)
|
|
43
|
+
return logging.getLogger(f"{_ROOT}.{name}")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def enable_console_logging(level: int | str = logging.INFO) -> None:
|
|
47
|
+
"""Send pcsf log messages to standard error.
|
|
48
|
+
|
|
49
|
+
Parameters
|
|
50
|
+
----------
|
|
51
|
+
level : int or str, default logging.INFO
|
|
52
|
+
Level set on the ``pcsf`` logger.
|
|
53
|
+
|
|
54
|
+
Notes
|
|
55
|
+
-----
|
|
56
|
+
A stream handler is added only once, however often the function
|
|
57
|
+
is called; later calls only change the level. This modifies
|
|
58
|
+
global logging state and is meant for scripts and notebooks, not
|
|
59
|
+
for libraries.
|
|
60
|
+
"""
|
|
61
|
+
logger = logging.getLogger(_ROOT)
|
|
62
|
+
has_stream = any(
|
|
63
|
+
isinstance(h, logging.StreamHandler)
|
|
64
|
+
and not isinstance(h, logging.NullHandler)
|
|
65
|
+
for h in logger.handlers
|
|
66
|
+
)
|
|
67
|
+
if not has_stream:
|
|
68
|
+
handler = logging.StreamHandler()
|
|
69
|
+
handler.setFormatter(
|
|
70
|
+
logging.Formatter("%(levelname)s %(name)s: %(message)s")
|
|
71
|
+
)
|
|
72
|
+
logger.addHandler(handler)
|
|
73
|
+
logger.setLevel(level)
|
pcsf/_version.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Version numbers.
|
|
2
|
+
|
|
3
|
+
Two numbers are kept apart on purpose:
|
|
4
|
+
|
|
5
|
+
``__version__``
|
|
6
|
+
The version of this Python package (the software).
|
|
7
|
+
``FORMAT_VERSION``
|
|
8
|
+
The version of the PCSF/PCSM specification the package writes. It
|
|
9
|
+
follows its own semantic-versioning policy (spec/, section
|
|
10
|
+
"Versioning") and changes only when the file layout changes.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
__version__ = "0.1.0"
|
|
14
|
+
|
|
15
|
+
#: Specification version written to every file (``pcsf_version`` /
|
|
16
|
+
#: ``PCSM_VERSION``).
|
|
17
|
+
FORMAT_VERSION = "0.1.0"
|
|
18
|
+
|
|
19
|
+
__all__ = ["FORMAT_VERSION", "__version__"]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Conversion of solver output to PCSF models.
|
|
2
|
+
|
|
3
|
+
Each adapter reads native files through :mod:`pcsf.native` and
|
|
4
|
+
registers itself in :data:`pcsf.api.registry.ADAPTERS`, so that
|
|
5
|
+
``ADAPTERS.get("occam2d")`` returns :func:`occam2d_to_pcsf`.
|
|
6
|
+
Third-party solvers plug in through the ``pcsf.adapters``
|
|
7
|
+
entry-point group (see :mod:`pcsf.plugins`).
|
|
8
|
+
|
|
9
|
+
========== ======================== =====================
|
|
10
|
+
Name Function Geometry kind
|
|
11
|
+
========== ======================== =====================
|
|
12
|
+
occam2d :func:`occam2d_to_pcsf` ``grid2d``
|
|
13
|
+
modem :func:`modem_to_pcsf` ``grid3d``
|
|
14
|
+
mare2dem :func:`mare2dem_to_pcsf` ``mesh_unstructured``
|
|
15
|
+
emc :func:`emc_to_pcsf` ``grid3d`` (geographic)
|
|
16
|
+
========== ======================== =====================
|
|
17
|
+
|
|
18
|
+
:func:`pcsf.adapters.emc.pcsf_to_emc` writes an EMC model back to
|
|
19
|
+
netCDF. MARE2DEM triangulation needs ``pcsf[mare2dem]`` and EMC files
|
|
20
|
+
need ``pcsf[emc]``; both are imported only when used.
|
|
21
|
+
|
|
22
|
+
Examples
|
|
23
|
+
--------
|
|
24
|
+
>>> from pcsf.api.registry import ADAPTERS
|
|
25
|
+
>>> import pcsf.adapters
|
|
26
|
+
>>> ADAPTERS.names()
|
|
27
|
+
('emc', 'mare2dem', 'modem', 'occam2d')
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from .emc import emc_to_pcsf, pcsf_to_emc
|
|
31
|
+
from .mare2dem import mare2dem_to_pcsf
|
|
32
|
+
from .modem import modem_to_pcsf
|
|
33
|
+
from .occam2d import occam2d_to_pcsf
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"emc_to_pcsf",
|
|
37
|
+
"mare2dem_to_pcsf",
|
|
38
|
+
"modem_to_pcsf",
|
|
39
|
+
"occam2d_to_pcsf",
|
|
40
|
+
"pcsf_to_emc",
|
|
41
|
+
]
|
pcsf/adapters/_common.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Helpers shared by the adapters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping, Sequence
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from ..core.schema import StationTable, TopographyPerStation
|
|
11
|
+
|
|
12
|
+
__all__ = ["station_table", "survey_dict"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def survey_dict(survey: Any) -> dict[str, Any]:
|
|
16
|
+
"""Plain dictionary of survey metadata.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
survey : mapping or object or None
|
|
21
|
+
Mapping, object with ``to_dict()``, or ``None``.
|
|
22
|
+
|
|
23
|
+
Returns
|
|
24
|
+
-------
|
|
25
|
+
dict
|
|
26
|
+
Empty for ``None``.
|
|
27
|
+
"""
|
|
28
|
+
if survey is None:
|
|
29
|
+
return {}
|
|
30
|
+
to_dict = getattr(survey, "to_dict", None)
|
|
31
|
+
if callable(to_dict):
|
|
32
|
+
return dict(to_dict())
|
|
33
|
+
return dict(survey)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def station_table(
|
|
37
|
+
names: Sequence[str],
|
|
38
|
+
x: np.ndarray,
|
|
39
|
+
y: np.ndarray,
|
|
40
|
+
z: np.ndarray,
|
|
41
|
+
*,
|
|
42
|
+
station_lonlat: Mapping[str, tuple[float, float]] | None = None,
|
|
43
|
+
station_elevations: Mapping[str, float] | None = None,
|
|
44
|
+
) -> tuple[StationTable, TopographyPerStation | None]:
|
|
45
|
+
"""Station table and optional topography from per-name values.
|
|
46
|
+
|
|
47
|
+
Parameters
|
|
48
|
+
----------
|
|
49
|
+
names : sequence of str
|
|
50
|
+
Station names in file order.
|
|
51
|
+
x, y, z : array_like of shape (n,)
|
|
52
|
+
Model-frame coordinates, metres.
|
|
53
|
+
station_lonlat : mapping of str to (lon, lat), optional
|
|
54
|
+
WGS84 positions; stations missing from it get NaN.
|
|
55
|
+
station_elevations : mapping of str to float, optional
|
|
56
|
+
Elevations in metres; they replace *z* for the stations they
|
|
57
|
+
name and define the topography.
|
|
58
|
+
|
|
59
|
+
Returns
|
|
60
|
+
-------
|
|
61
|
+
stations : StationTable
|
|
62
|
+
topography : TopographyPerStation or None
|
|
63
|
+
Set only when *station_elevations* is given.
|
|
64
|
+
"""
|
|
65
|
+
names = list(names)
|
|
66
|
+
z = np.asarray(z, dtype=float).copy()
|
|
67
|
+
topo = None
|
|
68
|
+
if station_elevations:
|
|
69
|
+
for i, name in enumerate(names):
|
|
70
|
+
if name in station_elevations:
|
|
71
|
+
z[i] = float(station_elevations[name])
|
|
72
|
+
topo = TopographyPerStation(
|
|
73
|
+
station_id=list(names), elevation=z
|
|
74
|
+
)
|
|
75
|
+
lon = lat = None
|
|
76
|
+
if station_lonlat:
|
|
77
|
+
pairs = np.array(
|
|
78
|
+
[station_lonlat.get(n, (np.nan, np.nan)) for n in names],
|
|
79
|
+
dtype=float,
|
|
80
|
+
).reshape(len(names), 2)
|
|
81
|
+
if not np.all(np.isnan(pairs)):
|
|
82
|
+
lon, lat = pairs[:, 0], pairs[:, 1]
|
|
83
|
+
table = StationTable(
|
|
84
|
+
name=names,
|
|
85
|
+
x=np.asarray(x, dtype=float),
|
|
86
|
+
y=np.asarray(y, dtype=float),
|
|
87
|
+
z=z,
|
|
88
|
+
lon=lon,
|
|
89
|
+
lat=lat,
|
|
90
|
+
)
|
|
91
|
+
return table, topo
|
pcsf/adapters/emc.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""EarthScope Earth Model Collaboration (EMC) netCDF files.
|
|
2
|
+
|
|
3
|
+
EMC [1]_ archives 3-D models as netCDF files on a geographic grid
|
|
4
|
+
(longitude, latitude, depth in km). :func:`emc_to_pcsf` converts
|
|
5
|
+
such a file to a ``grid3d`` model and :func:`pcsf_to_emc` writes it
|
|
6
|
+
back; the pair is lossless: values (fill values included), coordinate
|
|
7
|
+
and value types, every global and variable attribute with its type,
|
|
8
|
+
and the netCDF format are restored. Needs ``pcsf[emc]`` (netCDF4).
|
|
9
|
+
|
|
10
|
+
Notes
|
|
11
|
+
-----
|
|
12
|
+
Electrical models in EMC usually store
|
|
13
|
+
:math:`\\log_{10}\\sigma` with :math:`\\sigma` in S/m, which pcsf
|
|
14
|
+
records with encoding ``log10_conductivity``, so that
|
|
15
|
+
:math:`\\rho = 10^{-v}`. Depth is converted from km to m, and the
|
|
16
|
+
grid's horizontal coordinates stay in degrees with
|
|
17
|
+
``crs = "EPSG:4326"``.
|
|
18
|
+
|
|
19
|
+
References
|
|
20
|
+
----------
|
|
21
|
+
.. [1] Trabant, C., A. R. Hutko, M. Bahavar, R. Karstens, T. Ahern,
|
|
22
|
+
and R. Aster, 2012, Data products at the IRIS DMC: Stepping
|
|
23
|
+
stones for research and other applications: Seismological
|
|
24
|
+
Research Letters, 83, 846-854.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import os
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
import numpy as np
|
|
34
|
+
|
|
35
|
+
from ..api.registry import register_adapter
|
|
36
|
+
from ..compat.optional import import_optional
|
|
37
|
+
from ..core.schema import Grid3DGeometry, PCSFModel
|
|
38
|
+
from ..exceptions import AdapterError
|
|
39
|
+
|
|
40
|
+
__all__ = ["emc_to_pcsf", "pcsf_to_emc"]
|
|
41
|
+
|
|
42
|
+
_COORDS = ("depth", "latitude", "longitude")
|
|
43
|
+
_ENCODINGS = {
|
|
44
|
+
"log10_conductivity": "log10_conductivity",
|
|
45
|
+
"log10_resistivity": "log10",
|
|
46
|
+
"conductivity": "conductivity",
|
|
47
|
+
"resistivity": "linear",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _pack(value: Any) -> dict[str, Any]:
|
|
52
|
+
"""Attribute value with its type, as JSON-ready data."""
|
|
53
|
+
if isinstance(value, str):
|
|
54
|
+
return {"t": "str", "v": value}
|
|
55
|
+
arr = np.asarray(value)
|
|
56
|
+
return {"t": arr.dtype.str, "v": arr.tolist()}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _unpack(packed: dict[str, Any]) -> Any:
|
|
60
|
+
if packed["t"] == "str":
|
|
61
|
+
return packed["v"]
|
|
62
|
+
return np.array(packed["v"], dtype=np.dtype(packed["t"]))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _encoding(name: str, attrs: dict[str, Any]) -> str:
|
|
66
|
+
"""Resistivity encoding of an EMC variable from its name."""
|
|
67
|
+
text = " ".join(
|
|
68
|
+
[
|
|
69
|
+
name,
|
|
70
|
+
str(attrs.get("long_name", "")),
|
|
71
|
+
str(attrs.get("units", "")),
|
|
72
|
+
]
|
|
73
|
+
).lower()
|
|
74
|
+
for key in ("log10_conductivity", "log10_resistivity"):
|
|
75
|
+
if key in text.replace(" ", "_") or key in name.lower():
|
|
76
|
+
return _ENCODINGS[key]
|
|
77
|
+
if "log" in text and "conduct" in text:
|
|
78
|
+
return "log10_conductivity"
|
|
79
|
+
if "log" in text:
|
|
80
|
+
return "log10"
|
|
81
|
+
if "conduct" in text:
|
|
82
|
+
return "conductivity"
|
|
83
|
+
return "linear"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@register_adapter("emc")
|
|
87
|
+
def emc_to_pcsf(
|
|
88
|
+
path: str | os.PathLike[str],
|
|
89
|
+
*,
|
|
90
|
+
variable: str | None = None,
|
|
91
|
+
encoding: str | None = None,
|
|
92
|
+
created_by: str = "",
|
|
93
|
+
description: str = "",
|
|
94
|
+
) -> PCSFModel:
|
|
95
|
+
"""Convert an EMC netCDF model to a ``grid3d`` model.
|
|
96
|
+
|
|
97
|
+
Parameters
|
|
98
|
+
----------
|
|
99
|
+
path : str or os.PathLike
|
|
100
|
+
netCDF file.
|
|
101
|
+
variable : str, optional
|
|
102
|
+
3-D variable to convert; the first 3-D variable by default.
|
|
103
|
+
encoding : str, optional
|
|
104
|
+
Encoding of the values; guessed from the variable's name,
|
|
105
|
+
``long_name`` and ``units`` by default (usually
|
|
106
|
+
``log10_conductivity``).
|
|
107
|
+
created_by, description : str, optional
|
|
108
|
+
Stored in the model; the description defaults to the file's
|
|
109
|
+
``id`` and ``title``.
|
|
110
|
+
|
|
111
|
+
Returns
|
|
112
|
+
-------
|
|
113
|
+
PCSFModel
|
|
114
|
+
``grid3d`` model with axes ``(depth, latitude, longitude)``,
|
|
115
|
+
depth in metres, linear resistivity, the archived values as
|
|
116
|
+
source values (fill values as NaN), ``crs = "EPSG:4326"``,
|
|
117
|
+
and ``metadata["emc"]`` holding everything
|
|
118
|
+
:func:`pcsf_to_emc` needs.
|
|
119
|
+
|
|
120
|
+
Raises
|
|
121
|
+
------
|
|
122
|
+
MissingDependencyError
|
|
123
|
+
If netCDF4 is not installed.
|
|
124
|
+
AdapterError
|
|
125
|
+
If the file has no 3-D variable or lacks a coordinate.
|
|
126
|
+
|
|
127
|
+
See Also
|
|
128
|
+
--------
|
|
129
|
+
pcsf_to_emc : Inverse conversion.
|
|
130
|
+
|
|
131
|
+
Examples
|
|
132
|
+
--------
|
|
133
|
+
>>> from pcsf.adapters.emc import emc_to_pcsf
|
|
134
|
+
>>> model = emc_to_pcsf("WUS.MT.nc") # doctest: +SKIP
|
|
135
|
+
>>> model.resistivity_native_encoding # doctest: +SKIP
|
|
136
|
+
'log10_conductivity'
|
|
137
|
+
"""
|
|
138
|
+
nc = import_optional("netCDF4", feature="EMC netCDF files")
|
|
139
|
+
with nc.Dataset(path) as d:
|
|
140
|
+
d.set_auto_mask(False)
|
|
141
|
+
names = [v for v in d.variables if d[v].ndim == 3]
|
|
142
|
+
var = variable or (names[0] if names else None)
|
|
143
|
+
if var is None or var not in d.variables:
|
|
144
|
+
raise AdapterError(f"{path}: no 3-D variable to convert")
|
|
145
|
+
dims = list(d[var].dimensions)
|
|
146
|
+
if sorted(dims) != sorted(_COORDS):
|
|
147
|
+
raise AdapterError(
|
|
148
|
+
f"{path}: {var} has dimensions {dims}, expected "
|
|
149
|
+
f"{list(_COORDS)}"
|
|
150
|
+
)
|
|
151
|
+
v = d[var]
|
|
152
|
+
raw = v[:]
|
|
153
|
+
attrs = {a: v.getncattr(a) for a in v.ncattrs()}
|
|
154
|
+
fill = attrs.get("_FillValue")
|
|
155
|
+
native = raw.astype(np.float64)
|
|
156
|
+
if fill is not None:
|
|
157
|
+
native[raw == fill] = np.nan
|
|
158
|
+
order = [dims.index(c) for c in _COORDS]
|
|
159
|
+
native = np.transpose(native, order)
|
|
160
|
+
coords = {c: np.asarray(d[c][:]) for c in _COORDS}
|
|
161
|
+
meta = {
|
|
162
|
+
"file_format": d.data_model,
|
|
163
|
+
"variable": var,
|
|
164
|
+
"dimensions": dims,
|
|
165
|
+
"coordinate_dtypes": {
|
|
166
|
+
c: coords[c].dtype.str for c in _COORDS
|
|
167
|
+
},
|
|
168
|
+
"value_dtype": v.dtype.str,
|
|
169
|
+
"depth_units": getattr(d["depth"], "units", "km"),
|
|
170
|
+
"global_attributes": {
|
|
171
|
+
a: _pack(d.getncattr(a)) for a in d.ncattrs()
|
|
172
|
+
},
|
|
173
|
+
"variable_attributes": {
|
|
174
|
+
n: {
|
|
175
|
+
a: _pack(d[n].getncattr(a)) for a in d[n].ncattrs()
|
|
176
|
+
}
|
|
177
|
+
for n in d.variables
|
|
178
|
+
},
|
|
179
|
+
}
|
|
180
|
+
gattrs = {a: d.getncattr(a) for a in d.ncattrs()}
|
|
181
|
+
enc = encoding or _encoding(var, attrs)
|
|
182
|
+
from ..core.encodings import to_resistivity
|
|
183
|
+
|
|
184
|
+
scale = 1000.0 if str(meta["depth_units"]).lower() == "km" else 1.0
|
|
185
|
+
geometry = Grid3DGeometry(
|
|
186
|
+
x=coords["longitude"].astype(np.float64),
|
|
187
|
+
y=coords["latitude"].astype(np.float64),
|
|
188
|
+
z=coords["depth"].astype(np.float64) * scale,
|
|
189
|
+
)
|
|
190
|
+
if not description:
|
|
191
|
+
description = (
|
|
192
|
+
f"EMC model {gattrs.get('id', Path(path).stem)}: "
|
|
193
|
+
f"{gattrs.get('title', '')}"
|
|
194
|
+
)
|
|
195
|
+
return PCSFModel(
|
|
196
|
+
geometry=geometry,
|
|
197
|
+
resistivity=to_resistivity(native, enc),
|
|
198
|
+
resistivity_native=native,
|
|
199
|
+
resistivity_native_encoding=enc,
|
|
200
|
+
source_backend="emc",
|
|
201
|
+
created_by=created_by,
|
|
202
|
+
crs="EPSG:4326",
|
|
203
|
+
description=description,
|
|
204
|
+
metadata={"emc": meta},
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def pcsf_to_emc(
|
|
209
|
+
model: PCSFModel, path: str | os.PathLike[str]
|
|
210
|
+
) -> Path:
|
|
211
|
+
"""Write a model converted by :func:`emc_to_pcsf` back to EMC.
|
|
212
|
+
|
|
213
|
+
Parameters
|
|
214
|
+
----------
|
|
215
|
+
model : PCSFModel
|
|
216
|
+
Model carrying ``metadata["emc"]``, possibly after a round
|
|
217
|
+
trip through PCSF or PCSM files.
|
|
218
|
+
path : str or os.PathLike
|
|
219
|
+
Destination netCDF file.
|
|
220
|
+
|
|
221
|
+
Returns
|
|
222
|
+
-------
|
|
223
|
+
pathlib.Path
|
|
224
|
+
*path*.
|
|
225
|
+
|
|
226
|
+
Raises
|
|
227
|
+
------
|
|
228
|
+
MissingDependencyError
|
|
229
|
+
If netCDF4 is not installed.
|
|
230
|
+
AdapterError
|
|
231
|
+
If the model has no EMC metadata or source values.
|
|
232
|
+
|
|
233
|
+
See Also
|
|
234
|
+
--------
|
|
235
|
+
emc_to_pcsf : Forward conversion.
|
|
236
|
+
|
|
237
|
+
Notes
|
|
238
|
+
-----
|
|
239
|
+
Values are taken from the source values, not recomputed from the
|
|
240
|
+
canonical resistivity, so that the archived numbers are restored
|
|
241
|
+
bit for bit; NaN becomes the fill value again.
|
|
242
|
+
|
|
243
|
+
Examples
|
|
244
|
+
--------
|
|
245
|
+
>>> from pcsf.adapters.emc import emc_to_pcsf, pcsf_to_emc
|
|
246
|
+
>>> model = emc_to_pcsf("WUS.MT.nc") # doctest: +SKIP
|
|
247
|
+
>>> pcsf_to_emc(model, "copy.nc") # doctest: +SKIP
|
|
248
|
+
"""
|
|
249
|
+
nc = import_optional("netCDF4", feature="EMC netCDF files")
|
|
250
|
+
meta = (model.metadata or {}).get("emc")
|
|
251
|
+
if meta is None or model.resistivity_native is None:
|
|
252
|
+
raise AdapterError(
|
|
253
|
+
"model has no EMC metadata or source values; it was not "
|
|
254
|
+
"made by emc_to_pcsf"
|
|
255
|
+
)
|
|
256
|
+
path = Path(path)
|
|
257
|
+
var = meta["variable"]
|
|
258
|
+
vattrs = meta["variable_attributes"]
|
|
259
|
+
dims = list(meta["dimensions"])
|
|
260
|
+
geo = model.geometry
|
|
261
|
+
scale = 1000.0 if str(meta["depth_units"]).lower() == "km" else 1.0
|
|
262
|
+
coords = {
|
|
263
|
+
"longitude": geo.x,
|
|
264
|
+
"latitude": geo.y,
|
|
265
|
+
"depth": geo.z / scale,
|
|
266
|
+
}
|
|
267
|
+
data = np.asarray(model.resistivity_native, dtype=np.float64)
|
|
268
|
+
data = np.transpose(data, [_COORDS.index(c) for c in dims])
|
|
269
|
+
with nc.Dataset(path, "w", format=meta["file_format"]) as d:
|
|
270
|
+
for c in dims:
|
|
271
|
+
d.createDimension(c, coords[c].size)
|
|
272
|
+
for c in ("longitude", "latitude", "depth"):
|
|
273
|
+
dt = np.dtype(meta["coordinate_dtypes"][c])
|
|
274
|
+
v = d.createVariable(c, dt, (c,))
|
|
275
|
+
for a, p in vattrs.get(c, {}).items():
|
|
276
|
+
v.setncattr(a, _unpack(p))
|
|
277
|
+
v[:] = coords[c].astype(dt)
|
|
278
|
+
fill = None
|
|
279
|
+
if "_FillValue" in vattrs[var]:
|
|
280
|
+
fill = _unpack(vattrs[var]["_FillValue"])
|
|
281
|
+
v = d.createVariable(
|
|
282
|
+
var,
|
|
283
|
+
np.dtype(meta["value_dtype"]),
|
|
284
|
+
tuple(dims),
|
|
285
|
+
fill_value=fill,
|
|
286
|
+
)
|
|
287
|
+
v.set_auto_mask(False)
|
|
288
|
+
for a, p in vattrs[var].items():
|
|
289
|
+
if a != "_FillValue":
|
|
290
|
+
v.setncattr(a, _unpack(p))
|
|
291
|
+
if fill is not None:
|
|
292
|
+
data = np.where(np.isnan(data), float(fill), data)
|
|
293
|
+
v[:] = data.astype(np.dtype(meta["value_dtype"]))
|
|
294
|
+
for a, p in meta["global_attributes"].items():
|
|
295
|
+
d.setncattr(a, _unpack(p))
|
|
296
|
+
return path
|