NORFOX

Warning

This example is still under development; don’t read too closely yet.

This example sketches a DASDAE inventory for the NORFOX fibre-optic array, a five-arm fiber-optic deployment at NORSAR’s Stendammen test site. It shows how a complex public deployment can be represented; some details take liberties for demonstration.

Figure 1: NORFOX cable path and NORES stations, from the NORSAR NORFOX data page

This page is an illustrative inventory example for the proposal. It uses public NORFOX descriptions and an assumed local copy of the official KMZ. The exact Python API names may shift as the DASDAE inventory implementation evolves.

Source Material

The example assumes the official NORFOX KMZ has already been downloaded from the NORSAR page and placed locally:

data/NORFOX_interpolated_Arms.kmz

Each of NORFOX’s five arms is approximately 1700 m long, radiating from the Stendammen facility. Each arm contains an OCC 4x G652 standard fiber cable, an OFS AcoustiSense enhanced backscatter cable, and two empty 16 mm pipes. Arms C and E also include an empty 40 mm pipe. The cables are buried about 50 cm below the surface in forest soil. The OFS enhanced fiber is spliced to standard fiber at each arm end to close the loop. The deployment is co-located with the NORES seismic array.

Useful public references:

Field Structure

The public arm layout can be summarized as follows.

Table 1: Public NORFOX arm structure represented in this example.
Arm Approx. Length (m) Fiber cables Empty pipe enclosures Notes
A 1700 OCC 4x G652, OFS AcoustiSense 2 x 16 mm loop closure at arm end
B 1700 OCC 4x G652, OFS AcoustiSense 2 x 16 mm loop closure at arm end
C 1700 OCC 4x G652, OFS AcoustiSense 2 x 16 mm, 1 x 40 mm loop closure at arm end
D 1700 OCC 4x G652, OFS AcoustiSense 2 x 16 mm loop closure at arm end
E 1700 OCC 4x G652, OFS AcoustiSense 2 x 16 mm, 1 x 40 mm loop closure at arm end

Build The Inventory

The example uses a few constants and a local KMZ path. The coordinates in the KMZ are geographic, so the inventory CRS uses longitude, latitude, and elevation labels.

from pathlib import Path

import dascore.core.inventory as inv

KMZ_PATH = Path("data/NORFOX_interpolated_Arms.kmz")
ARMS = ("A", "B", "C", "D", "E")
ARM_LENGTH = 1700.0

crs = inv.CoordinateReferenceSystem(
    authority="EPSG",
    code="4979",
    name="WGS 84 3D",
    coordinate_labels=("longitude", "latitude", "elevation"),
    units=("degree", "degree", "meter"),
)

Interrogators

The NORFOX material identifies multiple interrogator configurations used for comparison. Interrogators are physical instruments, so they are modeled separately from acquisition streams.

Table 2: Interrogator configurations represented in the NORFOX example.
Interrogator Role
Febus-A1R DAS / phase-sensitive OTDR comparison
OptDAS DAS / phase-sensitive OTDR comparison
OptDAS DAS / phase-sensitive OTDR comparison
febus_a1r = inv.Interrogator(
    name="NORFOX Febus-A1R interrogator",
    manufacturer="Febus",
    model="A1R",
    instrument_type="DAS interrogator",
)
optdas_1 = inv.Interrogator(
    name="NORFOX OptDAS interrogator 1",
    model="OptDAS",
    instrument_type="DAS interrogator",
)
optdas_2 = inv.Interrogator(
    name="NORFOX OptDAS interrogator 2",
    model="OptDAS",
    instrument_type="DAS interrogator",
)

Acquisitions

Each arm is its own fiber array in this example, so each arm carries its own acquisition streams. Acquisition codes only need to be unique within a fiber array, so the same three codes recur on every arm — NO.NFXA..FEBUS and NO.NFXB..FEBUS are distinct acquisition_keys. Wavelength-specific settings can be added later if they are needed for an analysis, but they are not required for the structural inventory.

common_acquisition_fields = dict(
    location_code="",
    start_time="2022-01-01",
    data_category="DAS",
    data_type="strain_rate",
    data_units="1/s",
    spatial_interval=1.0,
    distance_map=inv.DistanceMap(instrument_distance=(0.0,), distance=(0.0,)),
)

def arm_acquisitions(arm):
    return (
        inv.Acquisition(
            code="FEBUS",
            interrogator=febus_a1r,
            **common_acquisition_fields,
        ),
        inv.Acquisition(
            code="OPT1",
            interrogator=optdas_1,
            **common_acquisition_fields,
        ),
        inv.Acquisition(
            code="OPT2",
            interrogator=optdas_2,
            **common_acquisition_fields,
        ),
    )

Cables And Pipe Enclosures

Each arm has two fiber cables and multiple empty pipes. The cables and pipe enclosures are physical resources; the ordered optical path only contains components the light passes through.

def arm_resources(arm):
    occ_cable = inv.Cable(
        name=f"NORFOX Arm {arm} OCC 4x G652 cable",
        manufacturer="OCC",
        model="4x G652",
        fiber_count=4,
    )
    ofs_cable = inv.Cable(
        name=f"NORFOX Arm {arm} OFS AcoustiSense cable",
        manufacturer="OFS",
        model="AcoustiSense",
        fiber_count=1,
        diameter=0.0095,
        description="Aramid yarn strength member.",
    )
    pipe_enclosures = [
        inv.Enclosure(
            name=f"NORFOX Arm {arm} empty 16 mm pipe {index}",
            enclosure_type="pipe",
            inner_diameter=0.016,
        )
        for index in (1, 2)
    ]
    if arm in {"C", "E"}:
        pipe_enclosures.append(
            inv.Enclosure(
                name=f"NORFOX Arm {arm} empty 40 mm pipe",
                enclosure_type="pipe",
                inner_diameter=0.040,
            )
        )
    return occ_cable, ofs_cable, pipe_enclosures

Geometry From KML

The official KMZ contains one line for each arm. The proposed Geometry.from_kml helper lets the inventory import a named placemark without embedding thousands of coordinate rows in the example.

def arm_geometries(arm):
    outbound = inv.Geometry.from_kml(
        KMZ_PATH,
        label=f"Arm {arm}",
        name=f"NORFOX Arm {arm} outbound geometry",
        distance=(0.0, ARM_LENGTH),
    )
    inbound = inv.Geometry.from_kml(
        KMZ_PATH,
        label=f"Arm {arm}",
        name=f"NORFOX Arm {arm} return geometry",
        distance=(ARM_LENGTH, 2 * ARM_LENGTH),
        reverse=True,
    )
    return outbound, inbound

The intended behavior is:

  • select the KML or KMZ placemark whose name matches label;
  • import that placemark’s coordinate sequence using the inventory CRS;
  • spread a distance coordinate across the given range along the imported sequence;
  • reverse the coordinate sequence when reverse=True, so the return geometry retraces the outbound route over its own optical-distance range.

Calibrated optical distance stays inventory metadata, not something inferred from map distance.

Optical Components

Each arm is represented with an enhanced DAS sensing cable, a splice at the arm end, and the standard fiber return path used to close the loop.

OFS_DATASHEET = inv.OpticalMeasurement(
    resource_id="ofs-datasheet-1550",
    name="OFS AcoustiSense datasheet",
    method="datasheet",
    wavelength=1550,
)
OCC_DATASHEET = inv.OpticalMeasurement(
    resource_id="occ-datasheet-1550",
    name="OCC G652 datasheet",
    method="datasheet",
    wavelength=1550,
)


def arm_optical_components(arm, occ_cable, ofs_cable):
    enhanced_fiber = inv.FiberSegment(
        name=f"NORFOX Arm {arm} OFS AcoustiSense sensing fiber",
        optical_length=ARM_LENGTH,
        fiber_type="enhanced_backscatter",
        fiber_standard="OFS AcoustiSense",
        fiber_color="blue",
        loss_db=0.7 * ARM_LENGTH / 1000,
        loss_measurement=OFS_DATASHEET,
        container=ofs_cable,
    )
    loop_splice = inv.Splice(
        name=f"NORFOX Arm {arm} loop closure splice",
        splice_type="fusion",
    )
    return_fiber = inv.FiberSegment(
        name=f"NORFOX Arm {arm} OCC G652 return fiber",
        optical_length=ARM_LENGTH,
        fiber_type="single_mode",
        fiber_standard="G652",
        fiber_color="white",
        loss_db=0.3 * ARM_LENGTH / 1000,
        loss_measurement=OCC_DATASHEET,
        container=occ_cable,
    )
    return enhanced_fiber, loop_splice, return_fiber

Coupling And Labels

The public installation description says the cables are buried about 50 cm below the surface in forest soil. That is an interval property, so it belongs on the coupling track.

def arm_couplings(arm):
    kwargs = dict(
        coupling_type="trench",
        medium="forest_soil",
        attachment="direct_burial",
        depth=0.5,
    )
    outbound = inv.CouplingCondition(
        start_distance=0.0, end_distance=ARM_LENGTH, **kwargs
    )
    inbound = inv.CouplingCondition(
        start_distance=ARM_LENGTH, end_distance=2 * ARM_LENGTH, **kwargs
    )
    return outbound, inbound

def arm_annotation(arm):
    return inv.OpticalPathAnnotation(
        start_distance=0.0,
        end_distance=2 * ARM_LENGTH,
        group="arm",
        value=arm,
    )

Arm Fiber Arrays

Each arm is a separate FiberArray carrying a single OpticalPath. The arms are dissimilar routes, so under the granularity rule they are five fiber arrays. A break on Arm C becomes a path epoch under NFXC; the other arms are untouched. A different implementation could use one compound path with arm annotations if the acquisition stream is stored as one continuous channel axis.

fiber_arrays = []
physical_resources = []
for arm in ARMS:
    occ_cable, ofs_cable, pipe_enclosures = arm_resources(arm)
    physical_resources.extend((occ_cable, ofs_cable, *pipe_enclosures))
    path = inv.OpticalPath(
        name=f"NORFOX Arm {arm}",
        start_time="2022-01-01",
        optical_components=arm_optical_components(arm, occ_cable, ofs_cable),
        geometry=arm_geometries(arm),
        coupling=arm_couplings(arm),
        annotations=(arm_annotation(arm),),
    ).check()
    fiber_arrays.append(
        inv.FiberArray(
            code=f"NFX{arm}",
            name=f"NORFOX fibre-optic array, Arm {arm}",
            start_time="2022-01-01",
            acquisitions=arm_acquisitions(arm),
            optical_paths=(path,),
        )
    )

NORES Stations

The NORFOX deployment is co-located with NORES. The seismic array can live under the same network as conventional station/channel metadata while the fiber arrays carry the distributed optical paths.

Rather than manually transcribing station codes, coordinates, channels, and responses, the DASDAE inventory can ingest standard StationXML. ObsPy already knows how to read StationXML from the FDSN services, and the DASDAE layer can translate the standard station/channel objects into inventory Station and Channel objects. Response attachment is left as an exercise for the reader.

from obspy.clients.fdsn import Client

station_network_reference = inv.ExternalResource(
    uri="https://doi.org/10.21348/d.no.0001",
    name="NORSAR Station Network",
    description="Authoritative station metadata and citation for the NORSAR NO network.",
)
eida_access = inv.ExternalResource(
    uri="https://www.orfeus-eu.org/data/eida/webservices",
    name="UIB-NORSAR EIDA FDSN services",
    description="Waveform and StationXML access route for NORSAR NO network data.",
)

eida = Client("UIB-NORSAR")
nores_stationxml = eida.get_stations(
    network="NO",
    station="NR*",
    level="channel",
)

nores_stations = inv.Station.from_stationxml(
    nores_stationxml,
    include_response=False,
    description="Co-located NORES seismometer imported from standard StationXML.",
)

physical_resources.extend((station_network_reference, eida_access))

Conventional seismic metadata stays conventional: StationXML remains the exchange format, and the inventory attaches the resulting stations beside the fiber arrays.

Assemble The Inventory

The network gathers the five arm fiber arrays beside the NORES stations.

The resources argument accepts any iterable; entries are keyed by their resource_id, generated when unset.

network = inv.Network(
    code="NO",
    name="NORSAR test site network",
    fiber_arrays=tuple(fiber_arrays),
    stations=nores_stations,
)

inventory = inv.Inventory(
    resources=tuple(physical_resources),
    coordinate_reference_system=crs,
    networks=(network,),
)

What This Example Exercises

NORFOX touches most of the model boundaries:

  • Five dissimilar arms, five fiber arrays (the granularity rule).
  • KML-derived arm geometries are separate from calibrated optical distance.
  • Two fiber cable types are represented as physical resources.
  • Empty pipes are Enclosure resources with enclosure_type="pipe", not optical components.
  • Loop closure is represented with ordered optical components.
  • Burial in forest soil is represented as coupling context.
  • NORES seismometers remain conventional station/channel metadata under the same network.