13.4. 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 objects in their existing order, and returns a new pycsamt.site.base.Sites container with the retained stations.

The examples below select from the bundled WILLY AMT line, data/AMT/WILLY_DATA/L18PLT (28 stations), so every count and station list is real output from this survey rather than a sketch.

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().

13.4.1. Selector Map#

Function

Keeps stations when

Typical use

by_names()

The station name matches one or more patterns.

Keep a named line, block, or manually reviewed station list.

by_index()

The station position appears in an index list.

Keep the first, last, or manually inspected positions.

by_chainage()

Stored chainage lies inside a closed interval.

Trim a profile to a modelled distance window.

by_freq()

At least one finite frequency lies inside a closed interval.

Keep stations that cover a target period or frequency band.

by_bbox()

Latitude and longitude fall inside a geographic bounding box.

Keep stations inside a field area or map tile.

by_predicate()

A custom Python function returns True.

Express project-specific filters.

keep_finite_z()

Impedance or resistivity arrays contain finite values.

Drop invalid placeholders before diagnostics or inversion.

mask_large_phase_err()

The maximum finite phase error is below a threshold.

Remove stations with very uncertain phase products.

drop_empty()

Frequency and impedance-like data are structurally present.

Drop stations with no usable data arrays.

13.4.2. Selection Contract#

All functional selectors follow the same broad contract.

>>> from pycsamt.seg.collection import EDICollection
>>> from pycsamt.site.selection import by_names, keep_finite_z

>>> collection = EDICollection.from_sources("data/AMT/WILLY_DATA/L18PLT")

>>> selected = by_names(collection, "18-01*")
>>> selected = keep_finite_z(selected)

>>> print(type(selected).__name__)
Sites
>>> print([site.name for site in selected])
['18-010U', '18-011A', '18-012A', '18-013U', '18-014A', '18-015U',
 '18-016A', '18-017U', '18-018A', '18-019U']

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.Sites wrapper;

  • 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.

Note the shift in what iteration hands you. collection above is an EDICollection, so iterating it yields raw EDI objects; selected is a Sites, so iterating it yields Site wrappers with a normalized .name already available. Wrapping also runs the same station identity stabilization described in Site Containers, so an EDI whose header dataid disagreed with its filename comes out of any selector with that mismatch already resolved.

13.4.3. 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:

str

Literal names, glob-like patterns such as "S*" and "A??", and regex-looking strings.

re.Pattern

A compiled regular expression. Use this when you need strict regular expression control.

callable

A function receiving the station name and returning True or False.

list or tuple

Any mix of the above. A station is kept if any pattern matches.

>>> import re

>>> from pycsamt.seg.collection import EDICollection
>>> from pycsamt.site.selection import by_names

>>> sites = EDICollection.from_sources("data/AMT/WILLY_DATA/L18PLT")

>>> first_nine = by_names(sites, "18-00*")
>>> stations_10_to_19 = by_names(sites, re.compile(r"^18-01[0-9][A-Z]+$"))
>>> reviewed = by_names(sites, ["18-001A", "18-005U", "18-009A"])
>>> ending_with_a = by_names(sites, lambda name: name.endswith("A"))

>>> print(len(first_nine), [s.name for s in first_nine])
9 ['18-001A', '18-002U', '18-003A', '18-004A', '18-005U', '18-006A',
   '18-007U', '18-008U', '18-009A']
>>> print(len(stations_10_to_19))
10
>>> print([s.name for s in reviewed])
['18-001A', '18-005U', '18-009A']
>>> print(len(ending_with_a), [s.name for s in ending_with_a])
13 ['18-001A', '18-003A', '18-004A', '18-006A', '18-009A', '18-011A',
    '18-012A', '18-014A', '18-016A', '18-018A', '18-020A', '18-023A',
    '18-025A']

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.

13.4.4. 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.

>>> from pycsamt.seg.collection import EDICollection
>>> from pycsamt.site.selection import by_index

>>> sites = EDICollection.from_sources("data/AMT/WILLY_DATA/L18PLT")

>>> first_and_last = by_index(sites, [0, -1])
>>> second = by_index(sites, 1)

>>> print([site.name for site in first_and_last])
['18-001A', '18-025A']
>>> print([site.name for site in second])
['18-002U']

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:

>>> subset = by_index(sites, [-1, 0])

>>> # The first station still appears before the last station in the result.
>>> print([site.name for site in subset])
['18-001A', '18-025A']

13.4.5. 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. Neither is present on a freshly loaded EDI file, so calling it straight away skips every station:

>>> from pycsamt.seg.collection import EDICollection
>>> from pycsamt.site.selection import by_chainage

>>> sites = EDICollection.from_sources("data/AMT/WILLY_DATA/L18PLT")

>>> model_window = by_chainage(sites, smin=1_000.0, smax=8_000.0)
>>> print(len(model_window))
0

Stations without numeric chainage are skipped – here, all of them. This selector is only useful after a profile has already been computed and chainage values have been written onto the EDI headers or attached to the EDI objects, so build a profile first:

>>> from pycsamt.site.profile import Profile
>>> from pycsamt.site.selection import by_chainage

>>> profile = Profile.from_sites(sites)

>>> for ed in sites:
...     # Store chainage on the raw EDI object for later selectors.
...     station = getattr(ed, "station", "")
...     ed.chainage = profile.chainages.get(station)
...
>>> middle = by_chainage(sites, -2_000.0, -1_000.0)
>>> print(len(middle), sorted(site.name for site in middle))
11 ['18-012A', '18-013U', '18-014A', '18-015U', '18-016A', '18-017U',
    '18-018A', '18-019U', '18-020A', '18-021B', '18-021U']

On this line, profile.chainages runs from 0.0 at 18-001A down to about -2403 metres at 18-025AProfile.from_sites picked its own origin and azimuth automatically, in the opposite sense from the origin/azimuth combination used in Site Containers, so chainages here are negative rather than positive. Check the sign and range of profile.chainages on your own data before picking smin/smax; there is no universal convention for which end of a line is “positive.”

13.4.6. Frequency Coverage Selection#

by_freq() keeps a station if at least one finite frequency value lies inside a closed interval.

(1)#\[\operatorname{keep}(S)= \mathbb{1}\!\left[\exists i:\ f_i\text{ is finite and } f_{\min}\leq f_i\leq f_{\max}\right].\]

This is a station-level filter. It does not remove frequency rows from each station. Equation (1) returns one Boolean decision for the whole station; it is not a frequency mask.

>>> from pycsamt.seg.collection import EDICollection
>>> from pycsamt.site.selection import by_freq

>>> sites = EDICollection.from_sources("data/AMT/WILLY_DATA/L18PLT")

>>> broadband = by_freq(sites, fmin=0.1, fmax=1_000.0)
>>> no_overlap = by_freq(sites, fmin=20_000.0, fmax=30_000.0)

>>> print(len(broadband), len(no_overlap))
28 0

Every station on this line shares the same frequency grid, so any window that overlaps it at all keeps all 28 stations – by_freq only starts dropping stations on a survey where coverage genuinely differs from station to station, or when the window misses the data entirely, as [20000, 30000] Hz does here. 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.

>>> from pycsamt.site.edit import select_freq_all
>>> from pycsamt.site.selection import by_freq

>>> sites = by_freq(sites, fmin=0.1, fmax=100.0)
>>> sliced = select_freq_all(
...     sites,
...     fmin=0.1,
...     fmax=100.0,
...     inplace=False,
... )

>>> print(len(sites))
28
>>> print([len(s.freq) for s in sliced][:5])
[26, 26, 26, 26, 26]

by_freq above only decided which 28 stations qualify; select_freq_all is the step that actually trims each station down from 53 rows to the 26 that fall inside [0.1, 100.0] Hz.

13.4.7. Bounding Box Selection#

by_bbox() keeps stations inside an inclusive latitude/longitude box.

(2)#\[\operatorname{keep}(S)= \mathbb{1}\!\left[ \phi_{\min}\leq\phi_S\leq\phi_{\max} \ \land\ \lambda_{\min}\leq\lambda_S\leq\lambda_{\max} \right],\]

where \(\phi\) denotes latitude and \(\lambda\) longitude. Both boundaries in equation (2) are inclusive.

>>> from pycsamt.seg.collection import EDICollection
>>> from pycsamt.site.selection import by_bbox

>>> sites = EDICollection.from_sources("data/AMT/WILLY_DATA/L18PLT")

>>> field_area = by_bbox(
...     sites,
...     minlat=32.12,
...     minlon=119.128,
...     maxlat=32.13,
...     maxlon=119.130,
... )

>>> print(len(field_area), sorted(site.name for site in field_area))
11 ['18-001A', '18-002U', '18-003A', '18-004A', '18-005U', '18-006A',
    '18-007U', '18-008U', '18-009A', '18-010U', '18-011A']

This box covers roughly the southern third of the line – stations further north, such as 18-025A at lat=32.14195, fall outside maxlat and are dropped. 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.

13.4.8. 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.

>>> from pycsamt.site.base import Site
>>> from pycsamt.site.selection import by_predicate

>>> stations_with_tipper = by_predicate(
...     sites,
...     lambda ed: Site(ed).has_component("tipper"),
... )

>>> enough_frequencies = by_predicate(
...     sites,
...     lambda ed: len(Site(ed).freq) >= 8,
... )

>>> print(len(stations_with_tipper), len(enough_frequencies))
0 28

None of these stations carry tipper data, so the first predicate empties the collection; every station keeps well over 8 rows even after the earlier frequency trim, so the second keeps all 28. 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. This line has three station numbers that were occupied twice, under different letter suffixes:

>>> import re

>>> from pycsamt.site.utils import station_name

>>> seen = set()

>>> def keep_first_occupation(ed):
...     # Match the trailing "-<number><letters>" so this works whether
...     # ``ed``'s station identity has already been normalized to
...     # "18-001A" or still carries a raw EDI dataid like "23-18-001A".
...     name = station_name(ed)
...     number = re.search(r"-(\d{3})[A-Za-z]+$", name).group(1)
...     if number in seen:
...         return False
...     seen.add(number)
...     return True
...
>>> first_occupations = by_predicate(sites, keep_first_occupation)
>>> # ``sites`` holds raw EDI objects (no ``.name``); ``first_occupations``
>>> # holds Site wrappers. Compare through the shared station_name() helper.
>>> dropped = {station_name(ed) for ed in sites} - {
...     station_name(s.edi) for s in first_occupations
... }

>>> print(len(first_occupations))
25
>>> print(sorted(dropped))
['18-021U', '18-022V', '18-023V']

Stations 21, 22, and 23 were each recorded twice – (18-021B/18-021U), (18-022U/18-022V), and (18-023A/18-023V) – and keep_first_occupation keeps only the first-encountered reading of each, using a closure over a mutable seen set. This works because by_predicate() visits stations in their original survey order, so “first” here means “first in the input,” not an arbitrary pick.

13.4.9. 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.

>>> from pycsamt.seg.collection import EDICollection
>>> from pycsamt.site.selection import keep_finite_z

>>> sites = EDICollection.from_sources("data/AMT/WILLY_DATA/L18PLT")
>>> valid = keep_finite_z(sites)

>>> print(f"{len(valid)} stations contain finite impedance data")
28 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.

13.4.10. 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.

When a finite phase-error array exists, the implemented station-level rule is

(3)#\[e_{\max}(S)=\max_{i,j,k\,:\,e_{ijk}\text{ finite}}e_{ijk}, \qquad \operatorname{keep}(S)=\mathbb{1}[e_{\max}(S)\leq\tau],\]

with threshold \(\tau=\texttt{thresh}\). If no usable error value exists, the implementation bypasses equation (3) and keeps the station by default.

>>> from pycsamt.site.selection import mask_large_phase_err

>>> conservative = mask_large_phase_err(sites, thresh=10.0)

>>> print([site.name for site in conservative])
[]

Every station on this line has a stored phase-error array whose maximum finite value is a 90-degree sentinel – except two, 18-001A and 18-019U, whose real maxima are around 20-25 degrees. A thresh=10.0 gate is stricter than any of them, so it empties the collection; loosen it past the two genuine values and only they survive:

>>> conservative = mask_large_phase_err(sites, thresh=30.0)
>>> print([site.name for site in conservative])
['18-001A', '18-019U']

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.

>>> import numpy as np

>>> from pycsamt.site.selection import by_predicate, mask_large_phase_err

>>> def has_phase_error(ed):
...     z = getattr(ed, "Z", None)
...     arr = getattr(z, "_phase_err", None) if z is not None else None
...     return arr is not None and np.asarray(arr).size > 0
...
>>> with_errors = by_predicate(sites, has_phase_error)
>>> clean = mask_large_phase_err(with_errors, thresh=30.0)

>>> print(len(with_errors), len(clean))
28 2

All 28 stations here do carry a phase-error array – so with_errors changes nothing by itself on this survey – but chaining it in front of mask_large_phase_err is what makes the pipeline strict on data that doesn’t store phase error, turning “kept by default” into “kept only with evidence.”

The geographic and phase-error decisions can be audited together. Orange points in the map satisfy the inclusive box used earlier; green bars satisfy the 30-degree whole-station error gate:

>>> import matplotlib.pyplot as plt
>>> import numpy as np
>>> from matplotlib.patches import Rectangle
>>> from pycsamt.site.base import Site
>>> from pycsamt.site.utils import get_coords

>>> edis = list(EDICollection.from_sources("data/AMT/WILLY_DATA/L18PLT"))
>>> names = [Site(ed).name for ed in edis]
>>> coords = [get_coords(ed) for ed in edis]
>>> lats = np.asarray([coord.lat for coord in coords])
>>> lons = np.asarray([coord.lon for coord in coords])
>>> bbox = (32.12, 119.128, 32.13, 119.130)
>>> inside = (
...     (lats >= bbox[0]) & (lats <= bbox[2])
...     & (lons >= bbox[1]) & (lons <= bbox[3])
... )

>>> maxima = []
>>> for ed in edis:
...     errors = np.asarray(getattr(ed.Z, "_phase_err", []), dtype=float)
...     finite = errors[np.isfinite(errors)]
...     maxima.append(float(np.max(finite)) if finite.size else np.nan)
...
>>> maxima = np.asarray(maxima)
>>> passes = maxima <= 30.0

>>> fig, ax = plt.subplots(1, 2, figsize=(11, 4), constrained_layout=True)
>>> _ = ax[0].scatter(lons[~inside], lats[~inside], color="0.72", label="outside")
>>> _ = ax[0].scatter(lons[inside], lats[inside], color="#d95f02", label="inside")
>>> _ = ax[0].add_patch(Rectangle(
...     (bbox[1], bbox[0]), bbox[3] - bbox[1], bbox[2] - bbox[0],
...     fill=False, edgecolor="#d95f02", linewidth=1.5,
... ))
>>> _ = ax[0].set(
...     xlabel="longitude", ylabel="latitude", title="Inclusive bounding box"
... )
>>> ax[0].ticklabel_format(axis="x", style="plain", useOffset=False)
>>> _ = ax[0].legend(frameon=False)

>>> colors = np.where(passes, "#1b9e77", "0.68")
>>> _ = ax[1].bar(np.arange(len(names)), maxima, color=colors)
>>> _ = ax[1].axhline(
...     30.0, color="#d95f02", linestyle="--", label="threshold = 30°"
... )
>>> _ = ax[1].set(
...     xticks=np.arange(len(names)), xticklabels=names,
...     ylabel="maximum finite phase error (degrees)",
...     title="Whole-station phase-error gate",
... )
>>> ax[1].tick_params(axis="x", labelrotation=90, labelsize=7)
>>> _ = ax[1].legend(frameon=False)
>>> for axis in ax:
...     axis.grid(True, axis="y", alpha=0.22)
...
>>> fig.savefig("selection_bbox_phase_error.png", dpi=180)
L18PLT station map showing an inclusive bounding box beside maximum phase-error bars and a 30-degree threshold.

Two independent station-level decisions on L18PLT: geographic inclusion on the left and maximum phase-error acceptance on the right.#

The box retains the southern 11 stations because its northern edge is lat=32.13. The phase gate tells a different story: only 18-001A and 18-019U lie below 30 degrees. The remaining grey bars reach the stored 90-degree sentinel, so their rejection reflects that value—not geographic position or missing impedance. Combining the selectors computes the intersection of these independent conditions; it should not be interpreted as one selector confirming the other.

13.4.11. 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 Z container is missing;

  • the Z container has no impedance array and no resistivity surrogate;

  • available arrays are empty, entirely non-finite, or only huge sentinel values.

>>> from pycsamt.site.selection import drop_empty, keep_finite_z

>>> non_empty = drop_empty(sites)
>>> finite = keep_finite_z(non_empty)

>>> print(len(non_empty), len(finite))
28 28

None of this line’s stations are structurally empty, so both steps pass all 28 through unchanged here. This is usually the first cleanup selector in a workflow, because it removes stations that cannot participate in most downstream computations – its effect shows up on surveys with genuinely missing or placeholder stations, not on a clean bundled line like this one.

13.4.12. 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.

>>> from pycsamt.site.base import Sites

>>> sites = Sites.from_any("data/AMT/WILLY_DATA/L18PLT")

>>> reviewed = sites.select(names=["18-001A", "18-003A"])
>>> with_zxy = sites.select(
...     predicate=lambda site: site.has_component("Zxy")
... )

>>> print([s.name for s in reviewed])
['18-001A', '18-003A']
>>> print(len(with_zxy))
28

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.

13.4.13. Combining Selectors#

Selectors compose naturally because each one returns a Sites object. Order the pipeline from cheap structural filters to more specific project filters.

>>> from pycsamt.seg.collection import EDICollection
>>> from pycsamt.site.edit import select_freq_all
>>> from pycsamt.site.selection import (
...     by_bbox,
...     by_freq,
...     by_names,
...     drop_empty,
...     keep_finite_z,
...     mask_large_phase_err,
... )

>>> sites = EDICollection.from_sources("data/AMT/WILLY_DATA/L18PLT")

>>> sites = drop_empty(sites)
>>> sites = keep_finite_z(sites)
>>> sites = by_names(sites, ["18-00*", "18-01*"])
>>> sites = by_bbox(
...     sites,
...     minlat=32.12,
...     minlon=119.128,
...     maxlat=32.20,
...     maxlon=119.130,
... )
>>> sites = by_freq(sites, fmin=0.1, fmax=10_000.0)
>>> sites = mask_large_phase_err(sites, thresh=30.0)

>>> print(len(sites), [s.name for s in sites])
2 ['18-001A', '18-019U']

>>> # Now modify the frequency rows, after selecting stations that overlap
>>> # the requested band.
>>> prepared = select_freq_all(
...     sites,
...     fmin=0.1,
...     fmax=100.0,
...     inplace=False,
... )

>>> print([len(s.freq) for s in prepared])
[26, 26]

Structural and name filters narrow 28 stations to 19; the bounding box and frequency check pass all 19 through unchanged; the phase-error gate is what actually does the work here, narrowing to the same two well-behaved stations seen in the previous section. select_freq_all then trims each of those two from 53 rows down to the 26 that lie in [0.1, 100.0] Hz. This pattern separates station selection from data editing. The selectors decide which stations remain; editing functions decide how station contents change.

13.4.14. Selection Before Inversion#

Before preparing files for Occam2D, ModEM, or MARE2DEM, a selection workflow should normally answer four questions:

  1. Does every retained station contain usable impedance data?

  2. Does every retained station overlap the target frequency band?

  3. Are the retained stations inside the intended profile or map area?

  4. Are station errors acceptable for the planned inversion weighting?

For a 2-D profile workflow, combine selectors with the profile tools:

>>> from pycsamt.seg.collection import EDICollection
>>> from pycsamt.site.profile import Profile
>>> from pycsamt.site.selection import (
...     by_chainage,
...     by_freq,
...     drop_empty,
...     keep_finite_z,
... )

>>> sites = EDICollection.from_sources("data/AMT/WILLY_DATA/L18PLT")
>>> sites = drop_empty(sites)
>>> sites = keep_finite_z(sites)
>>> sites = by_freq(sites, 0.1, 10_000.0)

>>> profile = Profile.from_sites(sites)

>>> for ed in sites:
...     # ``sites`` is already a Sites wrapper here, so iterating it
...     # yields Site objects: use the normalized ``.name`` and reach
...     # through ``.edi`` to store chainage on the underlying EDI, which
...     # is what by_chainage actually inspects.
...     ed.edi.chainage = profile.chainages.get(ed.name)
...
>>> model_sites = by_chainage(sites, -2_000.0, -200.0)
>>> ordered = profile.sort_sites(model_sites)

>>> print(len(model_sites))
19

Once sites has passed through any selector, it is a Sites, not a raw EDICollection – iterating it hands you Site wrappers, whose .name is already normalized but which do not forward arbitrary EDI attributes like .station. Writing ed.chainage directly on the wrapper would silently do nothing; ed.edi.chainage reaches the object by_chainage() actually reads.

For a 3-D or map-based workflow, prefer geographic selection and then export or modelling preparation:

>>> from pycsamt.site.selection import by_bbox, by_freq, keep_finite_z

>>> cube_sites = keep_finite_z(sites)
>>> cube_sites = by_bbox(cube_sites, 32.12, 119.128, 32.20, 119.130)
>>> cube_sites = by_freq(cube_sites, 0.01, 10_000.0)

>>> print(len(cube_sites))
28

13.4.15. Common Mistakes#

Using by_freq() as if it sliced rows

by_freq() keeps or drops stations. Use pycsamt.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.chainage or edi.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.

Setting attributes on a selector’s output as if it were still raw EDI

Once any selector returns, iterating it yields Site wrappers. Reach through .edi to set attributes – such as chainage – that downstream selectors read from the underlying EDI object.

Every selector on this page works identically for EDI- and XML-native stations without any special handling: they read site.name, site.coords, site.freq, site.z, and similar SiteMixin accessors, which are backend-neutral by design (see Site Containers). The one exception is the .edi reach-through pattern noted just above – setting an attribute directly on site.edi (rather than through a typed accessor) materializes an EDI view for an XML-native station the first time it happens, same as any other .edi access.

13.4.16. Next Pages#