pycsamt.site.utils#

Functions

apply_inplace(x, fn, *[, inplace])

Apply a function to an object with optional in-place semantics.

as_edicollection(edic, *[, recursive, ...])

Coerce edic into an EDICollection when possible.

deg_to_mrad(x)

Convert degrees to milliradians.

freq_match(f, target, *[, tol])

Indices where frequency values match targets within tolerance.

freq_select(f, sel, *[, tol])

Build an index selection from ranges, slices or scalars.

get_coords(ed)

Read latitude, longitude and elevation from an EDI header.

get_freq(ed)

Return the frequency vector from the impedance section.

is_edi_collection(x)

Detect whether x is an iterable collection of EDI objects.

is_edi_file(x)

Heuristically detect an EDI file object.

is_pathlike(x)

Return True if x looks like a filesystem path.

iter_edifiles(edic)

Iterate over EDI files contained in edic.

match_name(pat, name)

Case-insensitive station-name matcher with flexible patterns.

maybe_copy(x)

Attempt a deep copy of x, falling back to identity.

mrad_to_deg(x)

Convert milliradians to degrees.

select_by_name(edic, pat)

Collect EDI-like objects whose station names match a pattern.

set_coords(ed, *[, lat, lon, elev, inplace])

Write latitude, longitude and/or elevation into the header.

set_station_name(ed[, name, station_id, ...])

Set the station name on an EDI-like object and its header.

station_name(ed)

Return a best-effort station name for an EDI-like object.

wrap_azimuth(az)

Wrap an azimuth angle in degrees to the half-open range [0, 360).

pycsamt.site.utils.is_pathlike(x)[source]#

Return True if x looks like a filesystem path.

Parameters:

x (Any) – Object to test.

Returns:

True when x is a str or pathlib.Path, otherwise False.

Return type:

bool

Notes

This helper is intentionally conservative. Containers that happen to be iterable but are not paths will return False.

Examples

>>> from pathlib import Path
>>> from pycsamt.site.utils import is_pathlike
>>> is_pathlike("data/line01.edi")
True
>>> is_pathlike(Path("data/line01.edi"))
True
>>> is_pathlike(["a", "b"])
False

See also

pathlib.Path

pycsamt.site.utils.is_edi_file(x)[source]#

Heuristically detect an EDI file object.

Parameters:

x (Any) – Object to test.

Returns:

True if x exposes both get_section and an attribute named Z (the impedance section). Otherwise False.

Return type:

bool

Notes

This check is duck-typed and does not import the backend class. It is compatible with multiple EDI implementations.

Examples

>>> class Dummy:
...     def get_section(self, *_): ...
...     Z = object()
...
>>> from pycsamt.site.utils import is_edi_file
>>> is_edi_file(Dummy())
True
>>> is_edi_file(object())
False
pycsamt.site.utils.is_edi_collection(x)[source]#

Detect whether x is an iterable collection of EDI objects.

Parameters:

x (Any) – Candidate collection. May be an EDICollection instance, or any iterable containing EDI-like objects.

Returns:

True if x looks like a collection of EDI files.

Return type:

bool

Notes

The check is non-destructive. It peeks at the first element of the iterable to decide. Pathlike objects are not treated as iterables here, even though str is iterable.

Examples

>>> from pycsamt.site.utils import is_edi_collection, is_edi_file
>>> class E:
...     def get_section(self, *_): ...
...     Z = object()
...
>>> is_edi_collection([E(), E()])
True
>>> is_edi_collection("folder/*.edi")
False
>>> is_edi_collection(None)
False
pycsamt.site.utils.iter_edifiles(edic)[source]#

Iterate over EDI files contained in edic.

Parameters:

edic (Any) – Single EDI object or an iterable of EDI objects. Pathlike inputs are ignored (yield nothing).

Yields:

pycsamt.seg.edi.EDIFile – Each detected EDI-like object from edic.

Return type:

Iterator[EDIFile]

Notes

This is a safe iterator. Non EDI elements are skipped. A single EDI object will be yielded once.

Examples

>>> from pycsamt.site.utils import iter_edifiles
>>> class E:
...     def get_section(self, *_): ...
...     Z = object()
...
>>> list(iter_edifiles(E()))
[<E object at ...>]
>>> lst = [E(), object(), E()]
>>> len(list(iter_edifiles(lst)))
2
pycsamt.site.utils.as_edicollection(edic, *, recursive=True, strict=False, on_dup='replace', verbose=0)[source]#

Coerce edic into an EDICollection when possible.

Parameters:
  • edic (Any) – Single/sequence of EDI objects, an existing EDICollection, or path-like (file/dir/glob) input.

  • recursive (bool, default True) – When edic is path-like (or a sequence of), recurse into directories while discovering files.

  • strict (bool, default False) – If True and edic is path-like, propagate read errors; otherwise errors are captured in the parser.

  • on_dup ({‘replace’, ‘keep’}, default 'replace') – Duplicate policy during path-like discovery. See EDICollection.from_sources().

  • verbose (int, default 0) – Verbosity forwarded to underlying readers.

Returns:

A new collection containing the input items, or None if nothing EDI-like was found.

Return type:

EDICollection or None

Notes

  • If edic is path-like → use EDICollection.from_sources(...)().

  • If edic is already an EDICollection → return it.

  • Else → collect EDI-like items via iter_edifiles(edic) and build a collection.

Examples

>>> from pycsamt.site.utils import as_edicollection
>>> coll = as_edicollection("data/*.edi", recursive=True)
>>> coll is not None
>>>
>>> class E:
...     def get_section(self, *_): ...
...     Z = object()
...
>>> coll = as_edicollection([E(), E()])
>>> coll is not None
True
>>> as_edicollection([]) is None
True

References

True

pycsamt.site.utils.station_name(ed)[source]#

Return a best-effort station name for an EDI-like object.

The lookup order is:

  1. Attribute station on the object itself (if truthy).

  2. Header field dataid (via an internal head accessor).

  3. Fallback attributes on the object: name, site, or dataid (first non-empty one).

  4. Empty string when nothing suitable is found.

Parameters:

ed (Any) – EDI-like object exposing either a header section or plain attributes for station naming.

Returns:

The resolved station name, possibly an empty string.

Return type:

str

Notes

This helper is intentionally tolerant so it can work across different EDI backends. It prefers explicit station on the object, then the header dataid which is commonly used as the unique site identifier.

Examples

>>> from types import SimpleNamespace
>>> from pycsamt.site.utils import station_name
>>> ed = SimpleNamespace(station="E01")
>>> station_name(ed)
'E01'
>>> ed = SimpleNamespace(name="Alt01")
>>> station_name(ed)
'Alt01'

See also

set_station_name

Synchronously update object and header names.

get_coords

Read latitude, longitude and elevation from header.

pycsamt.site.utils.set_station_name(ed, name=None, *, station_id=None, policy=None, inplace=True)[source]#

Set the station name on an EDI-like object and its header.

This function updates multiple common identifiers so that the object-level name and the header dataid stay in sync.

Parameters:
  • ed (Any) – EDI-like object.

  • name (str, optional) – New station name. If not provided, it is derived from station_id or a policy callable.

  • station_id (str or int, optional) – Alternate identifier used to build the new name when name is not explicitly given.

  • policy (Any, optional) – A callable or policy object that can convert an input identifier to a station string, e.g. lambda s: s.upper().

  • inplace (bool, default True) – If True, mutate ed in place; otherwise return a modified copy when possible.

Returns:

The same object (when inplace=True) or a best-effort copy with the new identifiers applied.

Return type:

Any

Notes

Internally, the header object is created if missing. Both ed.station and head.dataid are written when available. Errors from non-writable fields are suppressed.

Examples

>>> from types import SimpleNamespace
>>> from pycsamt.site.utils import set_station_name, station_name
>>> ed = SimpleNamespace()
>>> set_station_name(ed, name="X01")
...
<...>
>>> station_name(ed)
'X01'

See also

station_name

Read the current station name.

maybe_copy

Lightweight deep-copy helper used when not in place.

pycsamt.site.utils.get_coords(ed)[source]#

Read latitude, longitude and elevation from an EDI header.

Parameters:

ed (Any) – EDI-like object holding a header section.

Returns:

A small tuple-like object with fields lat, lon and elev. Missing values are returned as nan.

Return type:

_Coord

Notes

Both lon and legacy long header attribute names are supported. If the header is missing or fields cannot be read, all values are nan.

Examples

>>> from types import SimpleNamespace
>>> from pycsamt.site.utils import get_coords
>>> head = SimpleNamespace(lat=10.0, long=20.0, elev=300.0)
>>> ed = SimpleNamespace(get_section=lambda *_: head)
>>> c = get_coords(ed)
>>> (c.lat, c.lon, c.elev)
(10.0, 20.0, 300.0)

See also

set_coords

Write latitude, longitude and elevation to header.

pycsamt.site.utils.set_coords(ed, *, lat=None, lon=None, elev=None, inplace=True)[source]#

Write latitude, longitude and/or elevation into the header.

Parameters:
  • ed (Any) – EDI-like object.

  • lat (float, optional) – Latitude in degrees. When None, the value is left unchanged.

  • lon (float, optional) – Longitude in degrees. When None, the value is left unchanged.

  • elev (float, optional) – Elevation in meters. When None, the value is left unchanged.

  • inplace (bool, default True) – If True, mutate ed in place; otherwise apply the update to a copy and return it.

Returns:

The updated object (in place) or a best-effort copy.

Return type:

Any

Notes

Both lon and long attribute names are supported on the header. If neither exists, a small adapter header may be created and attached back to the object when possible. Failures to write individual fields are ignored to maximize portability.

Examples

>>> from types import SimpleNamespace
>>> from pycsamt.site.utils import set_coords, get_coords
>>> head = SimpleNamespace(lat=0.0, long=0.0, elev=0.0)
>>> ed = SimpleNamespace(get_section=lambda *_: head)
>>> set_coords(ed, lat=12.5, lon=1.2, elev=350.0)
...
<...>
>>> tuple(map(float, get_coords(ed)))
(12.5, 1.2, 350.0)

See also

get_coords

Read header coordinates.

maybe_copy

Helper used when not writing in place.

pycsamt.site.utils.maybe_copy(x)[source]#

Attempt a deep copy of x, falling back to identity.

Parameters:

x (Any) – Object to copy.

Returns:

A deep-copied object when possible; otherwise x itself.

Return type:

Any

Notes

Some EDI backends contain objects that are not deepcopyable. This helper catches such cases and simply returns the original object to avoid raising.

Examples

>>> from pycsamt.site.utils import maybe_copy
>>> maybe_copy({"a": [1, 2]})
{'a': [1, 2]}
pycsamt.site.utils.apply_inplace(x, fn, *, inplace=False)[source]#

Apply a function to an object with optional in-place semantics.

Parameters:
  • x (Any) – Input object.

  • fn (callable) – Function fn(obj) -> obj that mutates and/or returns the object.

  • inplace (bool, default False) – If True, call fn directly on x. Otherwise a deep copy is attempted first.

Returns:

The result of applying fn to the chosen object.

Return type:

Any

Notes

This utility centralizes a common pattern used across the package: honor an inplace flag while being resilient to objects that cannot be deep-copied.

Examples

>>> from pycsamt.site.utils import apply_inplace
>>> def inc(d):
...     d["n"] = d.get("n", 0) + 1
...     return d
...
>>> data = {}
>>> r1 = apply_inplace(data, inc, inplace=False)
>>> r1 is data
False
>>> r2 = apply_inplace(data, inc, inplace=True)
>>> r2 is data
True
pycsamt.site.utils.get_freq(ed)[source]#

Return the frequency vector from the impedance section.

Parameters:

ed (Any) – EDI-like object exposing a Z section with either a public freq array or a private _freq array.

Returns:

A 1-D float array sorted in ascending order. Returns an empty array when no frequency information is available.

Return type:

numpy.ndarray

Notes

This is a read-only accessor. When both freq and _freq are absent or unreadable, an empty array is returned. Non-finite values are preserved; only ordering is enforced.

Examples

>>> import numpy as np
>>> from types import SimpleNamespace
>>> from pycsamt.site.utils import get_freq
>>> Z = SimpleNamespace(freq=np.array([2.0, 1.0]))
>>> ed = SimpleNamespace(Z=Z)
>>> get_freq(ed)
array([1., 2..])

See also

freq_match

Find indices of exact matches within a tolerance.

freq_select

Build index selections from ranges or scalars.

pycsamt.site.utils.freq_match(f, target, *, tol=1e-06)[source]#

Indices where frequency values match targets within tolerance.

Parameters:
  • f (numpy.ndarray) – Frequency array to search. It is cast to float.

  • target (float or Sequence[float]) – One or more target frequencies to match against f.

  • tol (float, default 1e-6) – Absolute tolerance for a match. A value v in f is considered a match to t when \(|v - t| \\leq \\mathrm{tol}\).

Returns:

Sorted integer indices into f where matches occur. May be empty when no value matches within tol.

Return type:

numpy.ndarray

Notes

Non-finite values in f never match. Duplicates in f are all returned if they fall within the tolerance window.

Examples

>>> import numpy as np
>>> from pycsamt.site.utils import freq_match
>>> f = np.array([10.0, 20.0, 20.0000004, 30.0])
>>> freq_match(f, 20.0, tol=5e-7)
array([1, 2])
>>> freq_match(f, [5.0, 30.0])
array([3])

See also

freq_select

More general selection by ranges or slices.

get_freq

Read the frequency vector from an EDI object.

pycsamt.site.utils.freq_select(f, sel, *, tol=1e-06)[source]#

Build an index selection from ranges, slices or scalars.

Parameters:
  • f (numpy.ndarray) – Frequency vector to index. Cast to float.

  • sel (slice or float or Sequence[float] or tuple(float, float)) –

    Selection specifier:
    • slice(lo, hi) inclusive on both ends (with tol).

    • (lo, hi) tuple uses the same semantics as above.

    • single float selects exact matches (with tol).

    • sequence of floats selects those exact targets.

  • tol (float, default 1e-6) – Absolute tolerance used for end inclusions and exact matching.

Returns:

Sorted integer indices into f that satisfy the selection. Returns an empty array when nothing matches.

Return type:

numpy.ndarray

Notes

Bounds are treated as inclusive with a tolerance, i.e. \(\\mathrm{lo} - \\mathrm{tol} \\le f \\le \\mathrm{hi} + \\mathrm{tol}\). For exact targets, matching is performed by freq_match().

Examples

>>> import numpy as np
>>> from pycsamt.site.utils import freq_select
>>> f = np.array([1.0, 10.0, 100.0])
>>> freq_select(f, slice(5.0, 100.0))
array([1, 2])
>>> freq_select(f, (0.9, 1.1))
array([0])
>>> freq_select(f, [10.0, 999.0])
array([1])

See also

freq_match

Matching by exact targets with tolerance.

get_freq

Frequency accessor for EDI objects.

pycsamt.site.utils.select_by_name(edic, pat)[source]#

Collect EDI-like objects whose station names match a pattern.

Parameters:
  • edic (Any) – Iterable of EDI-like objects. It may also be a single EDI object, in which case a list with zero or one element is returned depending on the match.

  • pat (str or Pattern or callable) – Pattern passed to match_name().

Returns:

Items whose names match pat in iteration order.

Return type:

list of pycsamt.seg.edi.EDIFile

Examples

>>> from types import SimpleNamespace
>>> from pycsamt.site.utils import select_by_name
>>> eds = [SimpleNamespace(station="E01"),
...        SimpleNamespace(station="N10")]
>>> keep = select_by_name(eds, "E*")
>>> [getattr(x, "station") for x in keep]
['E01']

See also

match_name

Flexible single-name predicate.

iter_edifiles

Safe iterator over EDI-like inputs.

pycsamt.site.utils.match_name(pat, name)[source]#

Case-insensitive station-name matcher with flexible patterns.

Parameters:
  • pat (str or Pattern or callable) – Matching strategy. If callable, it is invoked as bool(pat(name)). If a compiled regex, pat.search is used. If a string, the function tries in order: glob-like wildcards (*, ?, []), regex-looking strings, and finally a case-insensitive literal compare.

  • name (str) – The candidate station name.

Returns:

True if the pattern matches name, False on any error or no match.

Return type:

bool

Notes

Glob-like patterns are translated to regular expressions with re.IGNORECASE. Strings that contain typical regex meta- characters (e.g. . ^ $ + | ( ) \\) are treated as regexes.

Examples

>>> import re
>>> from pycsamt.site.utils import match_name
>>> match_name("E*", "E01")
True
>>> match_name(re.compile(r"^X\\d+$"), "X123")
True
>>> match_name(lambda s: s.endswith("99"), "A99")
True

See also

select_by_name

Collect items from an iterable using a pattern.

pycsamt.site.utils.wrap_azimuth(az)[source]#

Wrap an azimuth angle in degrees to the half-open range [0, 360).

Parameters:

az (float) – Angle in degrees, possibly outside the canonical range.

Returns:

Angle in degrees in the interval [0, 360).

Return type:

float

Notes

The operation is equivalent to az % 360 with care for negative inputs.

Examples

>>> from pycsamt.site.utils import wrap_azimuth
>>> wrap_azimuth(370.0)
10.0
>>> wrap_azimuth(-10.0)
350.0
pycsamt.site.utils.deg_to_mrad(x)[source]#

Convert degrees to milliradians.

Parameters:

x (float or numpy.ndarray) – Angle(s) in degrees.

Returns:

Converted values in milliradians.

Return type:

numpy.ndarray

Notes

The conversion uses: \(\\mathrm{mrad} = \\mathrm{deg} \\times \\pi / 180 \\times 1000\).

Examples

>>> import numpy as np
>>> from pycsamt.site.utils import deg_to_mrad
>>> float(deg_to_mrad(180.0))
3141.592653589793
>>> deg_to_mrad(np.array([0.0, 90.0]))
array([   0.        , 1570.79632679])
pycsamt.site.utils.mrad_to_deg(x)[source]#

Convert milliradians to degrees.

Parameters:

x (float or numpy.ndarray) – Angle(s) in milliradians.

Returns:

Converted values in degrees.

Return type:

numpy.ndarray

Notes

The conversion uses: \(\\mathrm{deg} = \\mathrm{mrad} \\times 180 / (\\pi \\times 1000)\).

Examples

>>> import numpy as np
>>> from pycsamt.site.utils import mrad_to_deg
>>> float(mrad_to_deg(3141.592653589793))
180.0
>>> mrad_to_deg(np.array([0.0, 1570.79632679]))
array([ 0., 90.])