Site Editing#

The pycsamt.site.edit module contains practical editing helpers for EDI-like station objects and site collections. These helpers are designed for survey preparation: rotate tensors, reduce frequency ranges, normalize station names, assign coordinates, fill missing arrays, and recompute derived resistivity/phase quantities after edits.

The editing functions are deliberately tolerant. When an optional section is missing or has an incompatible shape, most functions skip that part and keep processing the rest of the object. This is helpful for real field data, where some stations may lack tipper, errors, or derived arrays.

For a complete survey-level cleanup that reads EDI files, applies edits, recomputes derived quantities, preserves line folders, and writes new pyCSAMT-authored EDI files, use EDI Recompute Workflow. The functions on this page are the lower-level building blocks used by that workflow.

Editing Map#

Function

Scope

Main purpose

rotate()

One site

Rotate impedance tensors and tipper values by an azimuthal angle.

select_freq()

One site

Subset frequency-indexed arrays by range, indices, or boolean mask.

rename()

One site

Set an explicit station name or apply a naming policy.

set_coords()

One site

Update latitude, longitude, and elevation in the EDI header.

fill_missing()

One site

Fill or allocate missing Z and tipper arrays.

recompute_res_phase()

One site

Recompute apparent resistivity and phase from the impedance tensor.

rotate_all()

Collection

Rotate every site in a collection.

select_freq_all()

Collection

Apply frequency subsetting to every site.

rename_all()

Collection

Rename stations across a collection.

set_coords_all()

Collection

Assign coordinates from a callable, mapping, or table holder.

set_coords_from_table()

Collection

Assign coordinates from CSV, DataFrame, structured array, or row list.

set_coords_from_en()

One site

Project easting/northing coordinates and store lon/lat on a site.

Copy Versus In-Place Editing#

Most editing helpers accept inplace. The default is False:

1from pycsamt.seg.edi import EDIFile
2from pycsamt.site.edit import rename
3
4edi = EDIFile("data/edi/S01.edi")
5
6edited = rename(edi, name="LINE01_S01", inplace=False)
7
8print(edited is edi)  # False

Use inplace=True when you intentionally want to mutate the provided object:

1rename(edi, name="LINE01_S01", inplace=True)

For collection helpers, inplace=False returns a new pycsamt.site.base.Sites wrapper. The original EDI objects are left untouched as far as the helper can manage. Use inplace=True only when the calling workflow owns the input objects.

Tensor Rotation#

rotate() rotates the impedance tensor and, when present, the tipper. For the impedance tensor, pyCSAMT applies:

\[Z' = R Z R^{-1},\]

where \(R\) is the horizontal rotation matrix built from angle_deg.

1from pycsamt.seg.edi import EDIFile
2from pycsamt.site.edit import rotate
3
4edi = EDIFile("data/edi/S01.edi")
5rotated = rotate(edi, angle_deg=30.0)

The helper checks common tensor attribute names such as z, impedance, and _z. Tipper arrays may live under T, TIP, Tip, or sometimes as a tipper-like attribute under Z.

Error arrays are rotated with a magnitude-only approximation using absolute values of the rotation matrices. Treat this as a practical uncertainty propagation, not a full covariance rotation.

Rotate a whole collection with rotate_all():

1from pycsamt.seg.collection import EDICollection
2from pycsamt.site.edit import rotate_all
3
4collection = EDICollection.from_sources("data/edi")
5rotated_sites = rotate_all(collection, angle_deg=30.0)

Frequency Subsetting#

select_freq() subsets a station along its frequency axis and slices aligned arrays together. It handles Z, Z errors, apparent resistivity, phase, tipper, tipper errors, and several common aliases.

Keep a frequency range:

1from pycsamt.site.edit import select_freq
2
3band = select_freq(
4    edi,
5    fmin=1.0,
6    fmax=1000.0,
7    inplace=False,
8)

Keep explicit row indices:

1edges = select_freq(edi, keep=[0, -1])

Use a boolean mask:

1import numpy as np
2
3freq = np.asarray(edi.Z.freq)
4mask = freq >= 10.0
5high = select_freq(edi, keep=mask)

When keep is provided, fmin and fmax are ignored. Use select_freq_all() for collections:

1from pycsamt.site.edit import select_freq_all
2
3trimmed = select_freq_all(
4    collection,
5    fmin=1.0,
6    fmax=1000.0,
7)

Station Renaming#

rename() updates common station identifiers in the EDI header and mirrors the result to edi.name when possible. This makes later lookup through pycsamt.site.base.Site and pycsamt.site.base.Sites more consistent.

Set an explicit name:

1from pycsamt.site.edit import rename
2
3renamed = rename(edi, name="LINE01_S01")

Apply a policy to the current station name:

1renamed = rename(
2    edi,
3    policy=lambda old: f"LINE01_{old}",
4)

If both name and policy are provided, the explicit name wins. Renaming does not change the on-disk filename. Use export templates later if you want output filenames to follow the new station names.

For collections, use rename_all():

1from pathlib import Path
2from pycsamt.site.edit import rename_all
3
4renamed_sites = rename_all(
5    collection,
6    name_fn=lambda edi: f"L01_{Path(getattr(edi, 'path', '')).stem}",
7)

Prefer name_fn when the original EDI files have duplicate station names. It receives the EDI object and can use file paths, metadata, or external state to generate unique identifiers.

Coordinate Editing#

set_coords() updates latitude, longitude, and elevation on one site. Only the values provided are changed.

1from pycsamt.site.edit import set_coords
2
3moved = set_coords(
4    edi,
5    lat=35.125,
6    lon=12.750,
7    elev=1234.0,
8)

To apply coordinates to many sites, use set_coords_all().

From a mapping keyed by station name:

1from pycsamt.site.edit import set_coords_all
2
3coords = {
4    "S01": (35.125, 12.750, 1234.0),
5    "S02": (35.200, 12.900, 1180.0),
6}
7
8updated = set_coords_all(collection, coords)

From a callable:

1def lookup(edi):
2    name = getattr(edi, "name", "")
3    if name == "S01":
4        return 35.125, 12.750, 1234.0
5    return None
6
7updated = set_coords_all(collection, lookup)

From an object exposing a .frame attribute:

1class CoordinateTable:
2    def __init__(self, frame):
3        self.frame = frame
4
5updated = set_coords_all(collection, CoordinateTable(frame))

Coordinate Tables#

set_coords_from_table() is the high-level table loader. It accepts:

  • path to a CSV file;

  • path to a whitespace-delimited text file;

  • pandas.DataFrame;

  • NumPy structured array;

  • list of dictionaries;

  • list of row-like tuples that pandas can convert to a frame.

With standard geographic columns:

 1import pandas as pd
 2from pycsamt.site.edit import set_coords_from_table
 3
 4table = pd.DataFrame(
 5    {
 6        "station": ["S01", "S02"],
 7        "lat": [35.125, 35.200],
 8        "lon": [12.750, 12.900],
 9        "elev": [1234.0, 1180.0],
10    }
11)
12
13updated = set_coords_from_table(collection, table)

The resolver understands common aliases:

Canonical field

Accepted names

station

station, name, site, id.

lat

lat, latitude.

lon

lon, long, longitude.

elev

elev, elevation, z.

easting

easting, x.

northing

northing, y.

Use an explicit column map when a field table uses non-standard names:

 1updated = set_coords_from_table(
 2    collection,
 3    table,
 4    columns={
 5        "station": "name",
 6        "lat": "latitude",
 7        "lon": "long",
 8        "elev": "elevation",
 9    },
10)

When both geographic coordinates and easting/northing are present, latitude and longitude are preferred.

Easting/Northing Conversion#

If a table provides projected coordinates, pass crs_from:

 1table = pd.DataFrame(
 2    {
 3        "station": ["S10", "S11"],
 4        "easting": [400000.0, 401250.0],
 5        "northing": [5750000.0, 5750400.0],
 6        "elev": [250.0, 252.0],
 7    }
 8)
 9
10updated = set_coords_from_table(
11    collection,
12    table,
13    crs_from="EPSG:32631",
14)

Projection uses pyproj. If pyproj is not installed and projected coordinates must be converted, the helper raises ImportError.

For one site, use set_coords_from_en():

1from pycsamt.site.edit import set_coords_from_en
2
3projected = set_coords_from_en(
4    edi,
5    easting=400000.0,
6    northing=5750000.0,
7    crs_from="EPSG:32631",
8    elev=250.0,
9)

Missing Data Filling#

fill_missing() fills or allocates missing Z and tipper arrays. It is often used before diagnostics or before recomputing derived quantities.

1from pycsamt.site.edit import fill_missing
2
3filled = fill_missing(
4    edi,
5    how="zero",
6    components=("Z",),
7)

Available fill policies are:

how="zero"

Replace non-finite values with numeric zeros.

how="nan"

Replace non-finite values with NaN.

The components argument is case-insensitive and accepts "Z" and "Tip". Z arrays are expected to have shape (n_freq, 2, 2). Tipper arrays are expected to have shape (n_freq, 2). The number of rows is inferred from the frequency vector.

Use this function carefully. Zero-filled tensors are convenient for tests and some robust workflows, but zeros are not measured values. Keep a note in the processing log when missing data have been filled.

Recomputing Resistivity And Phase#

recompute_res_phase() calls the available Z-section compute_resistivity_phase() method. It is best used after changing frequency rows, tensor values, or masks.

1from pycsamt.site.edit import recompute_res_phase
2
3edited = select_freq(edi, fmin=1.0, fmax=1000.0)
4edited = recompute_res_phase(edited)

The function is best-effort. If the Z section is missing, incompatible, or does not expose a recomputation method, the object is returned unchanged.

Practical Preparation Workflow#

The following pattern is common before diagnostics or inversion preparation:

 1from pycsamt.seg.collection import EDICollection
 2from pycsamt.site.edit import (
 3    fill_missing,
 4    recompute_res_phase,
 5    rename_all,
 6    rotate_all,
 7    select_freq_all,
 8    set_coords_from_table,
 9)
10
11collection = EDICollection.from_sources("data/raw_edi")
12
13sites = rename_all(
14    collection,
15    name_fn=lambda edi: f"L01_{getattr(edi, 'name', 'site')}",
16)
17sites = set_coords_from_table(sites, "data/station_coordinates.csv")
18sites = rotate_all(sites, angle_deg=30.0)
19sites = select_freq_all(sites, fmin=1.0, fmax=1000.0)
20
21prepared = []
22for edi in sites.as_list():
23    edi = fill_missing(edi, how="nan", components=("Z",))
24    edi = recompute_res_phase(edi)
25    prepared.append(edi)

Each stage should be recorded in a processing log, especially rotation angles, frequency bands, coordinate sources, and missing-data fill policies.

Common Mistakes#

Using zero fill as if it were measured data

fill_missing(..., how="zero") is useful for deterministic tests and some defensive algorithms. It should not silently replace failed field measurements in scientific interpretation.

Renaming without checking duplicates

A policy such as lambda name: "X_" + name preserves duplicates if the original station names were duplicated. Use name_fn with file stems or a station table when uniqueness matters.

Mixing frequency masks from different stations

keep=[...] and boolean masks are applied row-wise. A mask built from one station may not represent the same frequencies on another station if their frequency axes differ.

Forgetting to recompute derived arrays

After changing Z or frequency rows, apparent resistivity and phase may be stale. Run recompute_res_phase() when downstream steps use derived arrays.

Ignoring CRS metadata

Easting/northing values are meaningless without their source CRS. Always provide the correct crs_from when using projected coordinates.

Next Pages#

Continue with: