Site Utilities#

The site utility module contains the shared helper functions used by the station containers, selectors, editors, profile tools, exporters, and reports. Most users will interact with these helpers indirectly through higher-level APIs. They are still useful when you need a small, explicit operation in a script: identify EDI-like inputs, coerce paths into an EDICollection, read station metadata, select frequency indices, or match station names.

Use this page when you need to:

  • normalize mixed inputs before passing them to site tools;

  • iterate safely over EDI-like objects;

  • read or update station names and coordinates;

  • implement copy-aware helper functions with an inplace flag;

  • build frequency index selections for editing;

  • match station names with literals, wildcards, regular expressions, or callables;

  • convert azimuths and angle units.

Utility Map#

Group

Helpers

Main purpose

Input detection

is_pathlike(), is_edi_file(), is_edi_collection()

Decide whether an object is a path, one EDI-like file, or a collection of EDI-like files.

Iteration and coercion

iter_edifiles(), as_edicollection()

Walk safely over EDI-like objects or build an EDICollection.

Station metadata

station_name(), set_station_name(), get_coords(), set_coords()

Read and update station identifiers and HEAD coordinates.

Copy and mutation

maybe_copy(), apply_inplace()

Centralize safe copy-versus-in-place behavior.

Frequency helpers

get_freq(), freq_match(), freq_select()

Read frequency vectors and turn frequency rules into integer indices.

Name matching

match_name(), select_by_name()

Match station names with literals, wildcards, regex, or callables.

Angle helpers

wrap_azimuth(), deg_to_mrad(), mrad_to_deg()

Normalize azimuths and convert degrees/milliradians.

Input Detection#

The detection helpers are intentionally duck-typed. They are designed to work with pyCSAMT EDI classes and compatible EDI-like test or adapter objects.

 1from pathlib import Path
 2
 3from pycsamt.seg.edi import EDIFile
 4from pycsamt.site.utils import (
 5    is_edi_collection,
 6    is_edi_file,
 7    is_pathlike,
 8)
 9
10path = Path("data/edi/S01.edi")
11edi = EDIFile(path)
12
13print(is_pathlike(path))
14print(is_edi_file(edi))
15print(is_edi_collection([edi]))

Detection rules:

  • is_pathlike() returns True for str and pathlib.Path inputs;

  • is_edi_file() returns True for objects exposing both get_section and Z;

  • is_edi_collection() returns True for an EDICollection or an iterable whose first item looks like an EDI file;

  • path-like strings are not treated as EDI iterables, even though strings are technically iterable in Python.

These helpers are conservative. They are best used to route input handling, not as scientific validation.

Safe EDI Iteration#

iter_edifiles() yields only EDI-like objects. Non-EDI elements are skipped, and path-like inputs yield nothing.

1from pycsamt.site.utils import iter_edifiles, station_name
2
3mixed = [edi_a, object(), edi_b]
4
5for ed in iter_edifiles(mixed):
6    print(station_name(ed))

A single EDI-like object is yielded once:

1items = list(iter_edifiles(edi_a))
2print(len(items))

Use this helper when writing functions that should accept either one station or many stations.

Coercing To EDICollection#

as_edicollection() turns heterogeneous inputs into an EDICollection when possible.

 1from pycsamt.site.utils import as_edicollection
 2
 3from_directory = as_edicollection(
 4    "data/edi",
 5    recursive=True,
 6    strict=False,
 7    on_dup="replace",
 8)
 9
10from_list = as_edicollection([edi_a, edi_b])
11
12if from_directory is None:
13    raise RuntimeError("No EDI files were discovered")

The coercion order is:

  1. Path-like inputs, or sequences of path-like inputs, are passed to EDICollection.from_sources.

  2. Existing EDICollection objects are returned unchanged.

  3. Other inputs are scanned with iter_edifiles(); if at least one EDI-like object is found, a new EDICollection is built.

  4. If nothing EDI-like is found, None is returned.

Path discovery options are forwarded to EDICollection.from_sources: recursive, strict, on_dup, and verbose.

Station Name Resolution#

station_name() returns the best available station identifier.

The lookup order is:

  1. ed.station when it exists and is truthy;

  2. HEAD.dataid through the internal header accessor;

  3. object-level fallback attributes name, site, then dataid;

  4. an empty string.

1from pycsamt.seg.edi import EDIFile
2from pycsamt.site.utils import station_name
3
4edi = EDIFile("data/edi/S01.edi")
5
6name = station_name(edi)
7print(name)

The matching and export tools use this same resolution pattern, so using station_name in your own scripts keeps naming behavior consistent.

Updating Station Names#

set_station_name() updates the object-level station name and the header dataid so they stay synchronized.

1from pycsamt.site.utils import set_station_name, station_name
2
3set_station_name(edi, "L01_S001", inplace=True)
4print(station_name(edi))

Use inplace=False when you want a copy-like update.

1renamed = set_station_name(
2    edi,
3    "L01_S001_REVIEWED",
4    inplace=False,
5)
6
7print(station_name(renamed))
8print(station_name(edi))

If name is omitted, the helper delegates to the core station-name policy utility using station_id and policy.

1renamed = set_station_name(
2    edi,
3    station_id=12,
4    policy=lambda value: f"S{int(value):03d}",
5    inplace=False,
6)
7
8print(station_name(renamed))

Coordinate Access#

get_coords() reads latitude, longitude, and elevation from the EDI HEAD section and returns a tuple-like object with lat, lon, and elev fields.

1from pycsamt.site.utils import get_coords
2
3coords = get_coords(edi)
4
5print(coords.lat)
6print(coords.lon)
7print(coords.elev)

Missing or unreadable coordinate fields return NaN values. The helper supports the legacy header spelling long for longitude.

Writing Coordinates#

set_coords() writes one or more coordinate fields into the EDI header. Values left as None are not changed.

 1from pycsamt.site.utils import get_coords, set_coords
 2
 3corrected = set_coords(
 4    edi,
 5    lat=35.125,
 6    lon=12.750,
 7    elev=1234.0,
 8    inplace=False,
 9)
10
11print(get_coords(corrected))

Longitude is written to both lon and long when the header allows it. This keeps newer code and older EDI conventions aligned.

Copy And In-Place Semantics#

Several site tools expose an inplace flag. The two helpers below make that pattern consistent.

maybe_copy()

Attempts copy.deepcopy(). If deep-copying fails, it returns the original object.

apply_inplace()

Calls a function directly on the input when inplace=True. Otherwise it tries to copy first, then calls the function on the copy.

 1from pycsamt.site.utils import apply_inplace, set_station_name
 2
 3def rename_to_reviewed(ed):
 4    return set_station_name(ed, "REVIEWED", inplace=True)
 5
 6reviewed = apply_inplace(
 7    edi,
 8    rename_to_reviewed,
 9    inplace=False,
10)

This pattern is useful when writing your own small site-editing helper:

1from pycsamt.site.utils import apply_inplace, set_coords
2
3def force_zero_elevation(ed, *, inplace=False):
4    def update(obj):
5        return set_coords(obj, elev=0.0, inplace=True)
6
7    return apply_inplace(ed, update, inplace=inplace)

Frequency Access#

get_freq() reads the frequency vector from ed.Z.freq or ed.Z._freq and returns a one-dimensional float array sorted in ascending order.

1from pycsamt.site.utils import get_freq
2
3freq = get_freq(edi)
4
5print(freq[:5])
6print(freq.size)

If no frequency vector is available, an empty array is returned. The helper is read-only: it does not reorder the data arrays in the EDI object.

Frequency Matching#

freq_match() returns integer indices where frequency values match one or more target frequencies within an absolute tolerance.

\[|f_i - t| \leq tol\]
 1import numpy as np
 2
 3from pycsamt.site.utils import freq_match
 4
 5f = np.array([1.0, 10.0, 10.0000004, 100.0])
 6
 7idx = freq_match(f, 10.0, tol=5e-7)
 8print(idx)
 9
10idx = freq_match(f, [1.0, 100.0])
11print(idx)

Non-finite frequency values never match. Duplicate values are all returned when they fall within the tolerance window.

Frequency Selection#

freq_select() turns a scalar, list, tuple range, or slice into integer indices.

 1import numpy as np
 2
 3from pycsamt.site.utils import freq_select
 4
 5f = np.array([1.0, 10.0, 100.0, 1_000.0])
 6
 7low_band = freq_select(f, (1.0, 100.0))
 8mid_band = freq_select(f, slice(10.0, 1_000.0))
 9exact = freq_select(f, [10.0, 100.0])
10
11print(low_band)
12print(mid_band)
13print(exact)

Selection rules:

  • slice(lo, hi) selects inclusive bounds with tolerance;

  • (lo, hi) selects the same inclusive range;

  • one float or int selects exact matches with tolerance;

  • a sequence of floats selects exact target frequencies with tolerance.

Use this helper when building masks for pycsamt.site.edit.select_freq() or custom frequency-indexed transformations.

Name Matching#

match_name() tests one candidate station name against a pattern.

Supported pattern types are:

str

Literal names, wildcard-like strings such as "E*" and "A??", or regex-looking strings.

re.Pattern

A compiled regular expression. The pattern’s own flags are respected.

callable

A function fn(name) -> bool.

1import re
2
3from pycsamt.site.utils import match_name
4
5print(match_name("E*", "E01"))
6print(match_name(re.compile(r"^X\d+$"), "X123"))
7print(match_name(lambda name: name.endswith("99"), "A99"))

String matching is case-insensitive. Glob-like strings are translated to regular expressions internally. Regex-looking strings are compiled with case-insensitive matching.

Selecting By Name#

select_by_name() applies match_name() to every EDI-like object in an input and returns a list of matching EDI objects.

1from pycsamt.site.utils import select_by_name, station_name
2
3selected = select_by_name(sites, "L01_*")
4
5print([station_name(ed) for ed in selected])

For a higher-level container result, use pycsamt.site.selection.by_names() instead. select_by_name is the lightweight list-returning utility used by lower-level scripts and helpers.

Angle Helpers#

wrap_azimuth() wraps an angle in degrees into the half-open interval \([0, 360)\).

1from pycsamt.site.utils import wrap_azimuth
2
3print(wrap_azimuth(-10.0))
4print(wrap_azimuth(730.0))

deg_to_mrad() and mrad_to_deg() convert between degrees and milliradians.

\[mrad = deg \times \frac{\pi}{180} \times 1000\]
\[deg = mrad \times \frac{180}{\pi \times 1000}\]
 1import numpy as np
 2
 3from pycsamt.site.utils import deg_to_mrad, mrad_to_deg
 4
 5angles_deg = np.array([0.0, 90.0, 180.0])
 6angles_mrad = deg_to_mrad(angles_deg)
 7restored = mrad_to_deg(angles_mrad)
 8
 9print(angles_mrad)
10print(restored)

These helpers accept scalars or NumPy arrays and return NumPy arrays.

Putting Utilities Together#

The following example is intentionally small: it normalizes input, updates metadata, selects a frequency band, and collects a named subset.

 1from pycsamt.site.edit import select_freq
 2from pycsamt.site.utils import (
 3    as_edicollection,
 4    freq_select,
 5    get_freq,
 6    iter_edifiles,
 7    select_by_name,
 8    set_station_name,
 9)
10
11collection = as_edicollection("data/edi")
12if collection is None:
13    raise RuntimeError("No EDI files found")
14
15prepared = []
16
17for i, ed in enumerate(iter_edifiles(collection)):
18    ed = set_station_name(
19        ed,
20        station_id=i + 1,
21        policy=lambda n: f"L01_S{int(n):03d}",
22        inplace=False,
23    )
24
25    freq = get_freq(ed)
26    keep = freq_select(freq, (0.1, 100.0))
27    ed = select_freq(ed, keep=keep, inplace=False)
28    prepared.append(ed)
29
30line_start = select_by_name(prepared, "L01_S00?")

Common Mistakes#

Using is_edi_collection() on a one-shot generator

The function peeks at the first item. Prefer iter_edifiles() when you need to consume an iterable safely.

Assuming maybe_copy() always returns a distinct object

If deep-copying fails, the original object is returned. Functions that need strict copy isolation should test identity or implement a backend-specific copy path.

Forgetting that get_freq() sorts the returned vector

The helper returns a sorted array for matching convenience, but it does not reorder the EDI object’s data arrays. Use editing tools for real row selection.

Using select_by_name() when you need a Sites container

select_by_name returns a plain list. Use pycsamt.site.selection.by_names() for a Sites result.

Expecting get_coords() to validate coordinates

It is an accessor. Use the location tools when you need parsing, normalization, distance, projection, or topography handling.

Next Pages#