Site Selection#
The selection helpers choose which stations remain in a survey before
editing, diagnostics, export, modelling, or inversion preparation. They are
small, stable filters: each helper accepts a site-like input, walks through
the EDI-like items in their existing order, and returns a new
pycsamt.site.base.Sites container with the retained stations.
Use this page when you need to:
keep stations by name, index, chainage, frequency coverage, or location;
remove empty or unusable stations before expensive processing;
apply a quick quality gate based on finite impedance or phase error;
combine several simple selectors into a reproducible preparation workflow;
decide when to use functional selectors versus
pycsamt.site.base.Sites.select().
Selector Map#
Function |
Keeps stations when |
Typical use |
|---|---|---|
The station name matches one or more patterns. |
Keep a named line, block, or manually reviewed station list. |
|
The station position appears in an index list. |
Keep the first, last, or manually inspected positions. |
|
Stored chainage lies inside a closed interval. |
Trim a profile to a modelled distance window. |
|
At least one finite frequency lies inside a closed interval. |
Keep stations that cover a target period or frequency band. |
|
Latitude and longitude fall inside a geographic bounding box. |
Keep stations inside a field area or map tile. |
|
A custom Python function returns |
Express project-specific filters. |
|
Impedance or resistivity arrays contain finite values. |
Drop invalid placeholders before diagnostics or inversion. |
|
The maximum finite phase error is below a threshold. |
Remove stations with very uncertain phase products. |
|
Frequency and impedance-like data are structurally present. |
Drop stations with no usable data arrays. |
Selection Contract#
All functional selectors follow the same broad contract.
1from pycsamt.seg.collection import EDICollection
2from pycsamt.site.selection import by_names, keep_finite_z
3
4collection = EDICollection.from_sources("data/edi")
5
6selected = by_names(collection, "L01_*")
7selected = keep_finite_z(selected)
8
9print(type(selected).__name__)
10print([site.name for site in selected])
The important guarantees are:
input can be a
pycsamt.site.base.Sites, an EDI collection, a list of EDI files, or any iterable accepted by the site iterator;output is a new
pycsamt.site.base.Siteswrapper;relative order of retained stations follows the input order;
duplicate index requests are collapsed because selection is membership based;
the original container is not modified by the selection itself.
Selectors keep the raw EDI objects inside the returned Sites wrapper. This
means later edits can still act on the original EDI structures if an editing
function is called with inplace=True.
Name Selection#
by_names() keeps stations whose resolved station name matches one or
more patterns. Station names are resolved through
pycsamt.site.utils.station_name(), so names are taken from the same
header/object fields used by the rest of the site tools.
Supported pattern types are:
strLiteral names, glob-like patterns such as
"S*"and"A??", and regex-looking strings.re.PatternA compiled regular expression. Use this when you need strict regular expression control.
callableA function receiving the station name and returning
TrueorFalse.listortupleAny mix of the above. A station is kept if any pattern matches.
1import re
2
3from pycsamt.seg.collection import EDICollection
4from pycsamt.site.selection import by_names
5
6sites = EDICollection.from_sources("data/edi")
7
8line_a = by_names(sites, "A*")
9stations_01_to_05 = by_names(sites, re.compile(r"^S0[1-5]$"))
10reviewed = by_names(sites, ["S01", "S05", "S09"])
11ending_with_a = by_names(sites, lambda name: name.endswith("A"))
String matching uses the shared pyCSAMT name matcher, which is designed for case-insensitive station matching. For strict case-sensitive regex behavior, pass a compiled regular expression with the flags you want.
Index Selection#
by_index() keeps stations by zero-based position. Negative indices follow
normal Python sequence rules, so -1 means the last station and -2 means
the station before it.
1from pycsamt.seg.collection import EDICollection
2from pycsamt.site.selection import by_index
3
4sites = EDICollection.from_sources("data/edi")
5
6first_and_last = by_index(sites, [0, -1])
7second = by_index(sites, 1)
8
9print([site.name for site in first_and_last])
10print([site.name for site in second])
Invalid indices are ignored. If all requested indices are invalid, the result
is an empty Sites container.
The output order follows the original survey order, not the order of the
indices argument:
1subset = by_index(sites, [-1, 0])
2
3# The first station still appears before the last station in the result.
4print([site.name for site in subset])
Chainage Selection#
by_chainage() keeps stations whose chainage lies in a closed interval
\(s_{min} \le s \le s_{max}\). The selector first looks for
HEAD.chainage and then falls back to edi.chainage.
1from pycsamt.seg.collection import EDICollection
2from pycsamt.site.selection import by_chainage
3
4sites = EDICollection.from_sources("data/edi")
5
6model_window = by_chainage(
7 sites,
8 smin=1_000.0,
9 smax=8_000.0,
10)
11
12print([site.name for site in model_window])
Stations without numeric chainage are skipped. This is useful after a profile has already been computed and chainage values have been written onto the EDI headers or attached to the EDI objects.
If you have coordinates but no stored chainage yet, build a profile first.
1from pycsamt.site.profile import Profile
2from pycsamt.site.selection import by_chainage
3
4profile = Profile.from_sites(sites)
5
6for ed in sites:
7 # Store chainage on the raw EDI object for later selectors.
8 station = getattr(ed, "station", "")
9 ed.chainage = profile.chainages.get(station)
10
11middle = by_chainage(sites, 2_000.0, 5_000.0)
Frequency Coverage Selection#
by_freq() keeps a station if at least one finite frequency value lies
inside a closed interval.
This is a station-level filter. It does not remove frequency rows from each station.
1from pycsamt.seg.collection import EDICollection
2from pycsamt.site.selection import by_freq
3
4sites = EDICollection.from_sources("data/edi")
5
6broadband = by_freq(sites, fmin=0.1, fmax=1_000.0)
7low_frequency = by_freq(sites, fmin=0.1, fmax=10.0)
8
9print(len(broadband), len(low_frequency))
To actually subset the rows of frequency-indexed arrays, use
pycsamt.site.edit.select_freq() for one site or
pycsamt.site.edit.select_freq_all() for a collection.
1from pycsamt.site.edit import select_freq_all
2from pycsamt.site.selection import by_freq
3
4sites = by_freq(sites, fmin=0.1, fmax=100.0)
5sliced = select_freq_all(
6 sites,
7 fmin=0.1,
8 fmax=100.0,
9 inplace=False,
10)
Bounding Box Selection#
by_bbox() keeps stations inside an inclusive latitude/longitude box.
1from pycsamt.seg.collection import EDICollection
2from pycsamt.site.selection import by_bbox
3
4sites = EDICollection.from_sources("data/edi")
5
6field_area = by_bbox(
7 sites,
8 minlat=34.5,
9 minlon=12.0,
10 maxlat=35.5,
11 maxlon=13.0,
12)
Coordinates are read through pycsamt.site.utils.get_coords(). Stations
with missing or non-finite coordinates are skipped.
This selector is intentionally simple: it performs an axis-aligned test in latitude and longitude degrees. It does not project coordinates and it does not handle antimeridian wrapping. For projected distance or profile filters, use the profile and location tools described in Location And Profiles.
Custom Predicate Selection#
by_predicate() keeps stations for which a user-provided function returns
True. The predicate receives the raw EDI-like object, not a
pycsamt.site.base.Site wrapper.
1from pycsamt.site.base import Site
2from pycsamt.site.selection import by_predicate
3
4stations_with_tipper = by_predicate(
5 sites,
6 lambda ed: Site(ed).has_component("tipper"),
7)
8
9enough_frequencies = by_predicate(
10 sites,
11 lambda ed: len(Site(ed).freq) >= 8,
12)
Predicate exceptions are caught and treated as False. This keeps survey
filters robust when one station has an unusual structure.
Use by_predicate for project logic that is too specific for the standard
selectors:
1from pycsamt.site.utils import station_name
2
3def keep_production_station(ed):
4 name = station_name(ed)
5 return (
6 name.startswith("P")
7 and not name.endswith("_TEST")
8 )
9
10production = by_predicate(sites, keep_production_station)
Finite Impedance Selection#
keep_finite_z() keeps stations that contain at least one finite
impedance value. If impedance values are unavailable, it falls back to common
resistivity array names such as _resistivity, resistivity, or rho.
1from pycsamt.seg.collection import EDICollection
2from pycsamt.site.selection import keep_finite_z
3
4sites = EDICollection.from_sources("data/edi")
5valid = keep_finite_z(sites)
6
7print(f"{len(valid)} stations contain finite impedance data")
This is the selector to run before diagnostics that require real impedance content. It does not inspect phase errors, frequency coverage, or coordinate quality.
Phase Error Selection#
mask_large_phase_err() filters whole stations using their maximum finite
phase-error value. Despite the function name, the current implementation does
not mask individual data rows. A station is kept when:
no phase-error array is found;
the phase-error array is empty or entirely non-finite;
the maximum finite phase error is less than or equal to
thresh.
1from pycsamt.site.selection import mask_large_phase_err
2
3conservative = mask_large_phase_err(sites, thresh=10.0)
4
5print([site.name for site in conservative])
Common phase-error attributes are checked on the Z container, including
_phase_err and phase_err.
Because stations without stored phase-error arrays are kept, this selector is best used as a quality gate after a processing step that actually produced phase-error products. If absence of error estimates should be considered a failure in your workflow, combine this selector with a stricter predicate.
1import numpy as np
2
3from pycsamt.site.selection import by_predicate, mask_large_phase_err
4
5def has_phase_error(ed):
6 z = getattr(ed, "Z", None)
7 arr = getattr(z, "_phase_err", None) if z is not None else None
8 return arr is not None and np.asarray(arr).size > 0
9
10with_errors = by_predicate(sites, has_phase_error)
11clean = mask_large_phase_err(with_errors, thresh=10.0)
Empty Site Selection#
drop_empty() removes stations that are structurally empty. A station is
considered empty when:
the frequency vector is missing or has zero length;
the
Zcontainer is missing;the
Zcontainer has no impedance array and no resistivity surrogate;available arrays are empty, entirely non-finite, or only huge sentinel values.
1from pycsamt.site.selection import drop_empty, keep_finite_z
2
3non_empty = drop_empty(sites)
4finite = keep_finite_z(non_empty)
This is usually the first cleanup selector in a workflow, because it removes stations that cannot participate in most downstream computations.
Functional Selectors Versus Sites.select#
The pycsamt.site.base.Sites container also exposes a compact
pycsamt.site.base.Sites.select() method. Use it for simple name lists or
wrapper-based predicates.
1from pycsamt.site.base import Sites
2
3sites = Sites.from_any("data/edi")
4
5reviewed = sites.select(names=["S01", "S03"])
6with_zxy = sites.select(
7 predicate=lambda site: site.has_component("Zxy")
8)
Use the functions in pycsamt.site.selection when you need richer
matching rules, index handling, chainage filtering, frequency coverage,
coordinate boxes, or data-quality filters.
Combining Selectors#
Selectors compose naturally because each one returns a Sites object.
Order the pipeline from cheap structural filters to more specific project
filters.
1from pycsamt.seg.collection import EDICollection
2from pycsamt.site.edit import select_freq_all
3from pycsamt.site.selection import (
4 by_bbox,
5 by_freq,
6 by_names,
7 drop_empty,
8 keep_finite_z,
9 mask_large_phase_err,
10)
11
12sites = EDICollection.from_sources("data/edi")
13
14sites = drop_empty(sites)
15sites = keep_finite_z(sites)
16sites = by_names(sites, ["L01_*", "L02_*"])
17sites = by_bbox(
18 sites,
19 minlat=34.5,
20 minlon=12.0,
21 maxlat=35.5,
22 maxlon=13.0,
23)
24sites = by_freq(sites, fmin=0.1, fmax=100.0)
25sites = mask_large_phase_err(sites, thresh=10.0)
26
27# Now modify the frequency rows, after selecting stations that overlap
28# the requested band.
29prepared = select_freq_all(
30 sites,
31 fmin=0.1,
32 fmax=100.0,
33 inplace=False,
34)
This pattern separates station selection from data editing. The selectors decide which stations remain; editing functions decide how station contents change.
Selection Before Inversion#
Before preparing files for Occam2D, ModEM, or MARE2DEM, a selection workflow should normally answer four questions:
Does every retained station contain usable impedance data?
Does every retained station overlap the target frequency band?
Are the retained stations inside the intended profile or map area?
Are station errors acceptable for the planned inversion weighting?
For a 2-D profile workflow, combine selectors with the profile tools:
1from pycsamt.site.profile import Profile
2from pycsamt.site.selection import (
3 by_chainage,
4 by_freq,
5 drop_empty,
6 keep_finite_z,
7)
8
9sites = drop_empty(sites)
10sites = keep_finite_z(sites)
11sites = by_freq(sites, 0.1, 100.0)
12
13profile = Profile.from_sites(sites)
14
15for ed in sites:
16 name = getattr(ed, "station", "")
17 ed.chainage = profile.chainages.get(name)
18
19model_sites = by_chainage(sites, 500.0, 9_500.0)
20ordered = profile.sort_sites(model_sites)
For a 3-D or map-based workflow, prefer geographic selection and then export or modelling preparation:
1from pycsamt.site.selection import by_bbox, by_freq, keep_finite_z
2
3cube_sites = keep_finite_z(sites)
4cube_sites = by_bbox(cube_sites, 34.5, 12.0, 35.5, 13.0)
5cube_sites = by_freq(cube_sites, 0.01, 1_000.0)
Common Mistakes#
- Using
by_freq()as if it sliced rows by_freq()keeps or drops stations. Usepycsamt.site.edit.select_freq_all()to change frequency rows.- Expecting
mask_large_phase_err()to mask cells The current function filters whole stations. Use editing tools or a custom predicate when you need row-level masking.
- Running
by_chainage()before chainage exists Chainage must already be stored on
HEAD.chainageoredi.chainage. Build a profile first when you only have coordinates.- Assuming
by_bbox()projects coordinates It works directly in latitude/longitude degrees. Use projection helpers from Location And Profiles for projected distances.
- Using strict name case expectations with string patterns
String matching is intentionally tolerant. Use compiled regular expressions when strict case behavior matters.
Next Pages#
Site Containers for the
SiteandSiteswrappers;Site Editing for changing station names, coordinates, frequency rows, and data arrays after selection;
Location And Profiles for chainage, profile geometry, distance, and coordinate projection;
Computed Diagnostics for station-level diagnostics that often run after selection.