Quickstart: From a Channel Table
Most deployments begin with exactly two artifacts: the data files and a spreadsheet mapping channels to coordinates. This page walks that spreadsheet into a minimal, valid inventory.
The skeleton below is deliberately small, and none of it is invented: every required value is an instrument setting the operator already made — channel spacing, gauge length, sample rate, measurement range — and vendors write them into the file headers.
The Starting Point
A GPS walk of the line (or a quick tap-test campaign) produces:
channel,latitude,longitude,elevation
0,40.7440,-108.0313,1712.0
1,40.7441,-108.0312,1712.2
2,40.7442,-108.0312,1712.1
...
2047,40.7581,-108.0224,1748.9
Plus the settings from the instrument (or the file header): 2048 channels at 1.02 m spacing, 10 m gauge length, 500 Hz sample rate.
Build The Inventory
import pandas as pd
import dascore.core.inventory as inv
table = pd.read_csv("channel_locations.csv")
SPATIAL_INTERVAL = 1.02 # m, from the instrument settings
acquisition = inv.Acquisition(
code="RAW",
location_code="00",
start_time="2026-06-01",
data_category="DAS",
data_type="strain_rate",
data_units="1/s",
gauge_length=10.0,
sample_rate=500.0,
spatial_interval=SPATIAL_INTERVAL,
# The interrogator's own zero is the path origin, one meter for one.
distance_map=inv.DistanceMap(instrument_distance=(0.0,), distance=(0.0,)),
)
# The recorded range: 2048 channels x 1.02 m. One segment states it honestly.
sensing_fiber = inv.FiberSegment(
name="sensing fiber",
optical_length=len(table) * SPATIAL_INTERVAL,
)
# Geometry control points at each surveyed channel's optical distance.
geometry = inv.Geometry(
name="GPS walk 2026-06-01",
distance=tuple(table["channel"] * SPATIAL_INTERVAL),
coordinates=tuple(zip(table["latitude"], table["longitude"], table["elevation"])),
)
path = inv.OpticalPath(
name="Main line",
location_code="00",
start_time="2026-06-01",
optical_components=(sensing_fiber,),
geometry=(geometry,),
).check()
inventory = inv.Inventory(
networks=(
inv.Network(
code="DAS",
fiber_arrays=(
inv.FiberArray(
code="L001",
name="Quickstart line",
start_time="2026-06-01",
acquisitions=(acquisition,),
optical_paths=(path,),
),
),
),
),
)That is the whole skeleton; the containers give the data its address, DAS.L001.00.RAW.
Use It
import dascore as dc
patch = dc.spool("das_data/")[0]
patch = patch.enrich(
inventory,
acquisition_key="DAS.L001.00.RAW",
coords=("latitude", "longitude", "elevation"),
)Two behaviors worth knowing:
- Operators routinely set the measurement range past the physical fiber end. Channels recorded beyond geometry coverage enrich as NaN and cannot match coordinate selections — partial coverage is undefined, not invalid.
- The one-point map above trusts the instrument’s nominal numbers: it states where the interrogator’s zero lands on the path and runs one meter for one from there. When a tap test later shows the wellhead responding twelve channels late, add the measured control points to the same DistanceMap — an in-place correction. A map with more than one point interpolates instead of extrapolating, so give it a point at or past the last channel; otherwise every channel beyond the tap point resolves to nothing.
What To Add When You Know More
Everything else in the model is refinement of this skeleton, added without restructuring it: coupling intervals when burial conditions matter, annotations for named zones, cable and interrogator resources for hardware provenance, a new path@ epoch when the fiber breaks, and new acquisition epochs when the configuration changes. The Simple Inventory example shows the next tier of detail; the authoring format lets field crews maintain all of it as a directory of YAML and CSV files.