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
inplaceflag;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 |
Decide whether an object is a path, one EDI-like file, or a collection of EDI-like files. |
|
Iteration and coercion |
Walk safely over EDI-like objects or build an |
|
Station metadata |
|
Read and update station identifiers and |
Copy and mutation |
Centralize safe copy-versus-in-place behavior. |
|
Frequency helpers |
Read frequency vectors and turn frequency rules into integer indices. |
|
Name matching |
Match station names with literals, wildcards, regex, or callables. |
|
Angle helpers |
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()returnsTrueforstrandpathlib.Pathinputs;is_edi_file()returnsTruefor objects exposing bothget_sectionandZ;is_edi_collection()returnsTruefor anEDICollectionor 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:
Path-like inputs, or sequences of path-like inputs, are passed to
EDICollection.from_sources.Existing
EDICollectionobjects are returned unchanged.Other inputs are scanned with
iter_edifiles(); if at least one EDI-like object is found, a newEDICollectionis built.If nothing EDI-like is found,
Noneis 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:
ed.stationwhen it exists and is truthy;HEAD.dataidthrough the internal header accessor;object-level fallback attributes
name,site, thendataid;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.
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
floatorintselects 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:
strLiteral names, wildcard-like strings such as
"E*"and"A??", or regex-looking strings.re.PatternA compiled regular expression. The pattern’s own flags are respected.
callableA 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.
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 aSitescontainer select_by_namereturns a plain list. Usepycsamt.site.selection.by_names()for aSitesresult.- 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#
Site Containers for the higher-level
SiteandSiteswrappers;Site Selection for container-returning station filters;
Site Editing for data-changing operations built on these utilities;
Location And Profiles for coordinate parsing, projection, chainage, and profiles;
Export And Reporting for delivery files and survey summaries.