diff --git a/.gitignore b/.gitignore
index 0f95b42..e2d826e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,6 @@
# Directory that gets created when installing
/cube_conversion.egg-info/
+
+# IRIS StationXML validator which may be downloaded
+/stationxml-validator-*.jar
diff --git a/README.md b/README.md
index d64eff8..4b5cad6 100644
--- a/README.md
+++ b/README.md
@@ -1,23 +1,25 @@
cube_conversion
===============
-This command-line tool converts [DiGOS](https://digos.eu/) DATA-CUBE3
-files into miniSEED files of a desired length of time with specified metadata.
-Output miniSEED files have units of Pa, unless the user selects to export the files in
-a form suitable for submission to EarthScope (formerly IRIS). The tool
-can differentiate between channels for 3 channel DATA-CUBE3 files and
-optionally extract coordinates from the digitizer's GPS. The code only looks for
-files from digitizers defined in the `digitizer_sensor_pairs.json` file. Therefore,
-this file must be updated if pairings change or new pairings are added. The user
-can specify a custom "breakout box factor" for setups that modify the signal
-voltage via a voltage divider. This tool is currently only set up for conversion
-of infrasound data, but future updates will accommodate seismic as well.
+These command-line tools convert [DiGOS](https://digos.eu/) DATA-CUBE³ files into
+miniSEED files of a desired length of time with specified metadata, and produce
+validated StationXML files containing deployment, sensor, and digitizer information
+including sensor responses adjusted for individual sensor sensitivities. Output miniSEED
+files have units of Pa, unless the user selects to export the files in a form suitable
+for submission to EarthScope (formerly IRIS) — namely, integer counts. The miniSEED
+conversion tool can differentiate between channels for 3 channel DATA-CUBE³ files and
+optionally extract coordinates from the digitizer's GPS. The code only looks for files
+from digitizers defined in the `digitizer_sensor_pairs.json` file. Therefore, this file
+must be updated if pairings change or new pairings are added. The user can specify a
+custom "breakout box factor" for setups that modify the signal voltage via a voltage
+divider. This tool is currently only set up for conversion of infrasound data, but
+future updates could accommodate seismic as well.
Installation
------------
It's recommended that you run this script within a new or pre-existing
-[conda](https://docs.conda.io/projects/conda/en/latest/index.html) environment.
+[conda](https://docs.conda.io/projects/conda/en/stable/) environment.
(If you choose the latter option, ensure that your environment contains all of
the packages listed in the [Dependencies](#dependencies) section.)
@@ -72,45 +74,58 @@ Supplemental files
* `digitizer_offsets.json` — Digitizer offsets in V (We have found that each
digitizer has a slight voltage offset from zero)
-* `sensor_sensitivities.json` — Infrasound sensor sensitivities in V/Pa
+* `sensor_sensitivities.json` — Infrasound sensor model, sensitivity in V/Pa,
+ and frequency of sensitivity measurement in Hz
+
+For `cube_stationxml`, you may optionally use a local copy of the Nominal Response
+Library (NRL). It can be obtained by following the instructions
+[here](https://ds.iris.edu/ds/nrl/). Be sure to unzip the downloaded file to create an
+`NRL/` directory. **Note:** Obviously, since this is offline, you may need to update
+your local copy if a key response change is made, or if a new sensor is added! Hence,
+we recommend only using this local copy option if you're working offline.
Usage
-----
-To print the script's help menu, execute the following terminal commands:
+To run either command-line tool first activate your conda environment, e.g.,
```
conda activate uafinfra # Or your pre-existing env
+```
+
+## `cube_convert`
+
+To print the conversion script's help menu, run:
+```
cube_convert --help # Or: python /path/to/cube_conversion/cube_conversion/cube_convert.py --help
```
The help menu is shown below.
```
-usage: cube_convert [-h] [-v] [--grab-gps]
- [--bob-factor BREAKOUT_BOX_FACTOR] [--earthscope]
+usage: cube_convert [-h] [-v] [--grab-gps] [--bob-factor BREAKOUT_BOX_FACTOR]
+ [--earthscope]
input_dir [input_dir ...] output_dir network station
{01,02,03,04,AUTO} {AUTO,BDF,HDF,CDF}
-Convert DATA-CUBE files to miniSEED files while trimming, adding metadata, and
-renaming. Optionally extract coordinates from digitizer GPS.
+Convert DATA-CUBE files to miniSEED files while trimming, adding metadata, and renaming.
+Optionally extract coordinates from digitizer GPS.
positional arguments:
- input_dir one or more directories containing raw DATA-CUBE files
- (all files must originate from a single digitizer)
- [wildcards (*) supported]
+ input_dir one or more directories containing raw DATA-CUBE files (all files
+ must originate from a single digitizer) [wildcards (*) supported]
output_dir directory for output miniSEED and GPS-related files
network desired SEED network code (2 characters, A-Z)
station desired SEED station code (3-5 characters, A-Z & 0-9)
- {01,02,03,04,AUTO} desired SEED location code (if AUTO, choose
- automatically for 3 channel DATA-CUBE files)
- {AUTO,BDF,HDF,CDF} desired SEED channel code (if AUTO, determine
- automatically using SEED convention [preferred])
+ {01,02,03,04,AUTO} desired SEED location code (if AUTO, choose automatically for 3
+ channel DATA-CUBE files)
+ {AUTO,BDF,HDF,CDF} desired SEED channel code (if AUTO, determine automatically using
+ SEED convention [preferred])
options:
-h, --help show this help message and exit
-v, --verbose enable verbosity for GIPPtools commands
--grab-gps additionally extract coordinates from digitizer GPS
--bob-factor BREAKOUT_BOX_FACTOR
- factor by which to divide sensitivity values (for
- custom breakout boxes [4.5 for UAF DATA-CUBEs])
+ factor by which to divide sensitivity values (for custom breakout
+ boxes [4.5 for UAF DATA-CUBEs])
--earthscope format miniSEED files for EarthScope (formerly IRIS) data upload
```
For example, the command
@@ -122,6 +137,41 @@ means "convert all files in the subdirectories of `~/data/raw/` and place in
**01**, and an automatically determined channel code, dividing the sensitivity
by 4.5 and extracting coordinates from the digitizer's GPS."
+## `cube_stationxml`
+
+To print the StationXML generation script's help menu, run:
+```
+cube_stationxml --help # Or: python /path/to/cube_conversion/cube_conversion/cube_stationxml.py --help
+```
+The help menu is shown below.
+```
+usage: cube_stationxml [-h] [--nrl-path NRL_PATH] [--validate]
+ input_dir station_mapping [station_mapping ...] output_filename
+
+Generate StationXML files from DATA-CUBE³ miniSEED files and metadata.
+
+positional arguments:
+ input_dir directory containing miniSEED files and coordinate files produced
+ by cube_convert
+ station_mapping one or more mappings of the form
+ STATION_CODE:CUBE_NAME:SENSOR_SERIAL, for example UAF1:AVJ:903V2
+ output_filename filename for the output StationXML file (full path)
+
+options:
+ -h, --help show this help message and exit
+ --nrl-path NRL_PATH path to local copy of the NRL (Nominal Response Library)
+ directory, if not provided makes web services call
+ --validate run the IRIS StationXML validator on the output file
+```
+For example, the command
+```
+cube_stationxml ~/data/miniseed/ UAF1:B52:903V2 ~/data/station.xml --validate
+```
+means "process all miniSEED files in `~/data/miniseed/` assuming that station **UAF1**
+consisted of DATA-CUBE³ **B52** connected to sensor serial number **903V2**, and output
+a StationXML file to `~/data/station.xml`, validating the output file with the IRIS
+StationXML validator."
+
A note on SEED band codes
-------------------------
@@ -147,7 +197,7 @@ letter of the channel code; e.g., the "B" in "BDF".
| R | Extremely long-period | ≥ 0.0001 to < 0.001 | |
Note that the band code depends on both the sample rate of the digitizer and
-the corner period of the sensor. In `cube_convert.py` we allow for "B", "H", or
+the corner period of the sensor. In `cube_convert` we allow for "B", "H", or
"C", which covers a range of sample rates from 10 to 1000 Hz, all for corner
periods of 10 s or greater. While this covers most infrasound sensors, please
confirm that your digitizer sample rate and sensor corner period fit into the
diff --git a/cube_conversion/cube_convert.py b/cube_conversion/cube_convert.py
index dab04c5..1ee159a 100755
--- a/cube_conversion/cube_convert.py
+++ b/cube_conversion/cube_convert.py
@@ -161,7 +161,7 @@ def main():
else:
sensor = digitizer_sensor_pairs[digitizer]
try:
- sensitivity = sensitivities[sensor]
+ sensitivity = sensitivities[sensor]['sensitivity']
except KeyError:
warnings.warn('No matching sensitivities. Using default of '
f'{DEFAULT_SENSITIVITY} V/Pa.')
diff --git a/cube_conversion/cube_stationxml.py b/cube_conversion/cube_stationxml.py
new file mode 100755
index 0000000..c2e3bde
--- /dev/null
+++ b/cube_conversion/cube_stationxml.py
@@ -0,0 +1,382 @@
+#!/usr/bin/env python
+
+"""
+Make StationXML files from scratch, given an existing directory of miniSEED files and
+metadata generated by cube_convert. Adopted from the ObsPy tutorial here:
+ https://docs.obspy.org/tutorial/code_snippets/stationxml_file_from_scratch.html
+
+The code assumes that your sensors are all Chaparral Physics products, that your
+digitizers are all DiGOS DATA-CUBE³s, and — IMPORTANTLY — that you only have one sensor
+connected to each digitizer (i.e., one channel per station).
+"""
+
+import argparse
+import json
+import os
+import subprocess
+import warnings
+import xml.etree.ElementTree as ET
+from pathlib import Path
+from urllib.request import urlretrieve
+
+import requests
+from obspy import Stream, read, read_inventory
+from obspy.clients.nrl import NRL
+from obspy.core.inventory import (
+ Channel,
+ Equipment,
+ Inventory,
+ Network,
+ PolesZerosResponseStage,
+ Site,
+ Station,
+)
+
+# --------------------------------------------------------------------------------------
+# Advanced configuration options
+# --------------------------------------------------------------------------------------
+GAIN = 1 # DiGOS DATA-CUBE³ amplifier gain (this should usually be 1!)
+BOB_FACTOR = 10 # Breakout box factor for DATA-CUBE³s (this should usually be 10!)
+# --------------------------------------------------------------------------------------
+
+# These names must match EXACTLY what is in the NRL (v2). See here:
+# https://ds.iris.edu/ds/nrl/
+_SENSOR_MANUFACTURER = 'Chaparral'
+_DATALOGGER_MANUFACTURER = 'DiGOSOmnirecs'
+_DATALOGGER_MODEL = 'DataCube'
+
+# Base url for all NRL web service queries
+_BASE_URL = 'https://service.iris.edu/irisws/nrl/1/'
+
+# Ignore warning about Pa units
+warnings.filterwarnings(
+ 'ignore',
+ category=UserWarning,
+ message="ObsPy can not map unit 'PA' to displacement, velocity, or acceleration",
+)
+
+
+# Define callable main function to work with [project.scripts]
+def main():
+
+ # Set up command-line interface
+ parser = argparse.ArgumentParser(
+ description='Generate StationXML files from DATA-CUBE³ miniSEED files and metadata.',
+ allow_abbrev=False,
+ )
+ parser.add_argument(
+ 'input_dir',
+ help='directory containing miniSEED files and coordinate files produced by cube_convert',
+ )
+ parser.add_argument(
+ 'station_mapping',
+ nargs='+',
+ help='one or more mappings of the form STATION_CODE:CUBE_NAME:SENSOR_SERIAL, for example UAF1:AVJ:903V2',
+ )
+ parser.add_argument(
+ 'output_filename', help='filename for the output StationXML file (full path)'
+ )
+ parser.add_argument(
+ '--nrl-path',
+ default=None,
+ help='path to local copy of the NRL (Nominal Response Library) directory, if not provided makes web services call',
+ )
+ parser.add_argument(
+ '--validate',
+ action='store_true',
+ help='run the IRIS StationXML validator on the output file',
+ )
+ input_args = parser.parse_args()
+
+ # Check if input directory is valid
+ input_dir = Path(input_args.input_dir)
+ if not input_dir.is_dir():
+ raise NotADirectoryError(f'Input directory \'{input_dir}\' doesn\'t ' 'exist.')
+
+ # Parse mappings into dictionary with keys being the station codes (TODO: validate!)
+ station_mappings = {}
+ for mapping in input_args.station_mapping:
+ station_code, cube_name, sensor_serial = mapping.split(':')
+ station_mappings[station_code] = {
+ 'cube_name': cube_name,
+ 'sensor_serial': sensor_serial,
+ }
+
+ # Check if NRL path is a directory if provided
+ nrl_path = input_args.nrl_path
+ if nrl_path is not None:
+ nrl_path = Path(input_args.nrl_path).expanduser().absolute()
+ if not nrl_path.is_dir():
+ raise NotADirectoryError(
+ f'NRL path `{nrl_path}{os.sep}` is not a directory!'
+ )
+ print(f'Using local copy of NRL at `{nrl_path}{os.sep}`')
+
+ # Find root directory for cube_conversion repo
+ root_dir = Path(__file__).parents[1]
+
+ # Load sensor information including sensitivities, model numbers, etc.
+ with open(root_dir / 'sensor_sensitivities.json') as f:
+ sensor_info = json.load(f)
+
+ # Read in all data into Stream object for processing
+ st = Stream()
+ for mseed_file in sorted(input_dir.glob('??.*.*.???.????.???.??')):
+ st += read(mseed_file)
+
+ # Process SEED network code
+ network_codes = set(tr.stats.network for tr in st)
+ assert len(network_codes) == 1, 'Multiple network codes found in data!'
+ network_code = network_codes.pop()
+
+ # Process SEED station codes
+ station_codes = sorted(set(tr.stats.station for tr in st))
+
+ # Check SEED location codes, as these can indicate if multiple sensors are connected
+ # to the same digitizer (not supported by this script)
+ location_codes = sorted(set(tr.stats.location for tr in st))
+ assert len(location_codes) == 1, 'Multiple location codes found in data!'
+ location_code = location_codes.pop()
+
+ # Create Inventory with all required components
+ inv = Inventory()
+ net = Network(code=network_code)
+ for station in station_codes:
+
+ # Get info about this station from mappings and sensor info
+ serial_number = station_mappings[station]['sensor_serial']
+ cube_name = station_mappings[station]['cube_name']
+ sensor_model = sensor_info[serial_number]['model']
+ print('\n------------------------------')
+ print(f'{network_code}.{station}')
+ print('------------------------------')
+ print(f'Cube name: {cube_name}')
+ print(f'Sensor serial number: {serial_number}')
+ print(f'Sensor model: {sensor_model}')
+
+ # Get station start and end times, SEED channel code, sample rate...
+ st_station = st.select(station=station)
+ station_starttime = min(tr.stats.starttime for tr in st_station)
+ station_endtime = max(tr.stats.endtime for tr in st_station)
+ channel_codes = sorted(set(tr.stats.channel for tr in st_station))
+ assert (
+ len(channel_codes) == 1
+ ), f'Multiple channel codes found in for station {station}!'
+ channel_code = channel_codes.pop()
+ sample_rates = sorted(set(tr.stats.sampling_rate for tr in st_station))
+ assert len(sample_rates) == 1, 'Multiple sample rates found in data!'
+ sample_rate = sample_rates.pop()
+ # Read in coordinates from JSON file
+ coord_json_file = sorted(
+ input_dir.glob(
+ f'{network_code}.{station}.{location_code}.{channel_code}.json'
+ )
+ )
+ if len(coord_json_file) == 0:
+ raise FileNotFoundError(
+ f'No coordinate JSON file found for station {station}!'
+ )
+ elif len(coord_json_file) > 1:
+ raise ValueError # Almost impossible, but just in case
+ else:
+ coord_json_file = coord_json_file[0]
+ with open(coord_json_file) as f:
+ latitude, longitude, elevation = json.load(f)
+ # Make Station object
+ sta = Station(
+ code=station,
+ latitude=latitude,
+ longitude=longitude,
+ elevation=elevation,
+ site=Site(name=station), # Bare minumum... could make this more detailed
+ start_date=station_starttime,
+ end_date=station_endtime,
+ )
+ # Define sensor Equipment object
+ sensor = Equipment(
+ type='Infrasound sensor',
+ description=f'{_SENSOR_MANUFACTURER} {sensor_model}', # MDA shows this!
+ manufacturer=_SENSOR_MANUFACTURER,
+ model=sensor_model,
+ serial_number=serial_number,
+ )
+ # Define digitizer Equipment object
+ data_logger = Equipment(
+ type='Digitizer',
+ manufacturer=_DATALOGGER_MANUFACTURER,
+ model=_DATALOGGER_MODEL,
+ serial_number=cube_name,
+ )
+ # Make Channel object
+ cha = Channel(
+ code=channel_code,
+ location_code=location_code,
+ latitude=latitude, # Could be different if multi-channel setup!
+ longitude=longitude, # Could be different if multi-channel setup!
+ elevation=elevation,
+ depth=0, # Required, always 0?
+ sample_rate=sample_rate,
+ start_date=station_starttime, # Could be different if multi-channel setup!
+ end_date=station_endtime, # Could be different if multi-channel setup!
+ sensor=sensor,
+ data_logger=data_logger,
+ )
+ # Access the NRL to get response information. If the user provided a local path
+ # to the NRL, use that; otherwise, use the NRL web service (requires network
+ # connection).
+ if nrl_path is not None:
+ nrl = NRL(str(nrl_path))
+ lp_corner = list(nrl.sensors[_SENSOR_MANUFACTURER][sensor_model])
+ assert len(lp_corner) == 1, 'Multiple low-pass corner options found!'
+ lp_corner = lp_corner[0]
+ hf_corner = list(nrl.sensors[_SENSOR_MANUFACTURER][sensor_model][lp_corner])
+ assert len(hf_corner) == 1, 'Multiple high-pass corner options found!'
+ hf_corner = hf_corner[0]
+ sensor_keys = [_SENSOR_MANUFACTURER, sensor_model, lp_corner, hf_corner]
+ datalogger_keys = [
+ _DATALOGGER_MANUFACTURER,
+ _DATALOGGER_MODEL,
+ f'{GAIN:g}',
+ f'{sample_rate:g} Hz',
+ ]
+ # The contents of the NRL can be explored interactively in a Python prompt, see
+ # API documentation of NRL submodule:
+ # http://docs.obspy.org/packages/obspy.clients.nrl.html
+ # Here we assume that the end point of data logger and sensor are already known.
+
+ # Get the nominal response for this combination of sensor and digitizer
+ try:
+ response = nrl.get_response(
+ sensor_keys=sensor_keys, datalogger_keys=datalogger_keys
+ )
+ except KeyError as e:
+ msg = (
+ f'Could not find response in NRL for sensor keys {sensor_keys} and '
+ f'datalogger keys {datalogger_keys}.'
+ f'\n\n{nrl.sensors[sensor_keys[0]]}'
+ )
+ raise Exception(msg) from e
+ else: # Use NRL web service
+ # (1) Locate the `instconfig` for the Chaparral Physics sensor
+ params_catalog = dict(
+ element='sensor',
+ manufacturer=_SENSOR_MANUFACTURER,
+ model=sensor_model,
+ format='xml',
+ level='configuration',
+ )
+ response = requests.get(url=_BASE_URL + 'catalog', params=params_catalog)
+ root = ET.fromstring(response.text)
+ sensor_instconfigs = [element.text for element in root.iter('instconfig')]
+ assert len(sensor_instconfigs) == 1, 'Multiple instrument configs found!'
+ sensor_instconfig = sensor_instconfigs[0]
+ # (2) Define the `instconfig` for the DiGOS DATA-CUBE³ digitizer (easy!)
+ digitizer_instconfig = f'datalogger_{_DATALOGGER_MANUFACTURER}_{_DATALOGGER_MODEL}_PG{GAIN:g}_FR{sample_rate:g}'
+ # (3) Combine the two `instconfig` responses into a single StationXML response
+ params_combine = dict(
+ instconfig=':'.join([sensor_instconfig, digitizer_instconfig]),
+ format='stationxml',
+ )
+ request = requests.Request(
+ 'GET', url=_BASE_URL + 'combine', params=params_combine
+ )
+ stationxml_url = request.prepare().url
+ # (4) Read the combined StationXML response into ObsPy
+ response = read_inventory(stationxml_url)[0][0][0].response
+ # KEY: Add a response stage which applies the breakout box factor, after first
+ # stage (i.e., right after Pa --> V) — this should be a PZ stage, see:
+ # https://docs.fdsn.org/projects/stationxml/en/latest/reference.html#response-stage
+ bob_stage_sequence_number = 2
+ bob_stage = PolesZerosResponseStage(
+ description='DiGOS breakout box voltage step-down',
+ stage_sequence_number=bob_stage_sequence_number,
+ stage_gain=1 / BOB_FACTOR,
+ stage_gain_frequency=1, # [Hz] Anywhere where the response is flat?
+ input_units='V',
+ output_units='V',
+ pz_transfer_function_type='LAPLACE (RADIANS/SECOND)',
+ normalization_frequency=0,
+ normalization_factor=1,
+ poles=[],
+ zeros=[],
+ )
+ response.response_stages.insert(bob_stage_sequence_number - 1, bob_stage)
+ for response_stage in response.response_stages[bob_stage_sequence_number:]:
+ response_stage.stage_sequence_number += 1 # Increment following stage #s
+ # KEY: Update with our specific measured sensor sensitivity
+ nominal_sensitivity = response.response_stages[0].stage_gain
+ measured_sensitivity = sensor_info[serial_number]['sensitivity']
+ nominal_frequency = response.response_stages[0].stage_gain_frequency
+ measured_frequency = sensor_info[serial_number]['frequency']
+ response.response_stages[0].stage_gain = measured_sensitivity
+ response.response_stages[0].stage_gain_frequency = measured_frequency
+ print('Updated sensor sensitivity:')
+ print(f'\t{nominal_sensitivity} --> {measured_sensitivity} V/Pa')
+ print(f'\t{nominal_frequency:.2f} --> {measured_frequency:.2f} Hz')
+ # KEY: Recalculate overall sensitivity (at the calibration frequency)
+ response.recalculate_overall_sensitivity(frequency=measured_frequency)
+
+ cha.response = response
+ sta.channels.append(cha)
+ net.stations.append(sta)
+
+ # Set network start and end dates from station info
+ net.start_date = min([sta.start_date for sta in net])
+ net.end_date = max([sta.end_date for sta in net])
+
+ inv.networks.append(net)
+
+ # Write to StationXML file
+ output_filename = (
+ Path(input_args.output_filename.rstrip('.xml') + '.xml').expanduser().absolute()
+ )
+ inv.write(output_filename, format='stationxml', validate=True)
+ print(f'\nWrote StationXML file to `{output_filename}`\n')
+
+ # Download and run the StationXML validator, if user wants to
+ if input_args.validate:
+ # Functions for colored printing... helpful for highlighting errors in validator
+ # output!
+ # fmt: off
+ def print_red(string):
+ print('\u001b[31m' + string + '\u001b[0m')
+ def print_yellow(string):
+ print('\u001b[33m' + string + '\u001b[0m')
+ # fmt: on
+ # JAR file will downloaded here if it doesn't already exist
+ jar_file_path = root_dir / 'stationxml-validator-1.7.5.jar' # Selects version!
+ if jar_file_path.is_file():
+ print(f'Found `{jar_file_path.name}` — running command:')
+ else:
+ print(f'Downloading `{jar_file_path.name}`...')
+ url_base = 'https://github.com/iris-edu/stationxml-validator/releases/'
+ urlretrieve(
+ url_base + f'download/{jar_file_path.stem}/{jar_file_path.name}',
+ jar_file_path,
+ )
+ print('...done. Running command:')
+ args = ['java', '-jar', jar_file_path, output_filename]
+ print('```')
+ print(' '.join([str(arg) for arg in args]))
+ print('```')
+ print('------------------------------')
+ print('Output from validator')
+ print('------------------------------')
+ process = subprocess.run(args, capture_output=True, text=True)
+ if process.stderr: # Something went wrong!
+ print_red(process.stderr.strip())
+ elif process.stdout: # Normal operation
+ for line in process.stdout.strip().split('\n'):
+ if ',Error,' in line:
+ print_red(line)
+ elif ',Warning,' in line:
+ print_yellow(line)
+ else:
+ print(line)
+ else: # This will never happen?
+ raise OSError
+
+
+# Run the main function if this is called as a script
+if __name__ == '__main__':
+ main()
diff --git a/pyproject.toml b/pyproject.toml
index fa190c0..5ab6ca4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -8,3 +8,4 @@ build-backend = "setuptools.build_meta"
[project.scripts]
cube_convert = "cube_conversion.cube_convert:main"
+cube_stationxml = "cube_conversion.cube_stationxml:main"
diff --git a/sensor_sensitivities.json b/sensor_sensitivities.json
index d29aff7..a7ba515 100644
--- a/sensor_sensitivities.json
+++ b/sensor_sensitivities.json
@@ -1,18 +1,82 @@
{
-"SN46" : 0.00902,
-"SN47" : 0.00898,
-"SN48" : 0.00909,
-"SN49" : 0.00904,
-"SN50" : 0.00905,
-"SN51" : 0.00902,
-"SN73" : 0.00898,
-"SN74" : 0.00903,
-"SN80" : 0.00905,
-"SN81" : 0.00898,
-"SN82" : 0.00901,
-"SN83" : 0.00902,
-"SN84" : 0.00902,
-"SN85" : 0.00902,
-"SN86" : 0.00898,
-"SN87" : 0.00903
+ "SN46": {
+ "model": "?",
+ "sensitivity": 0.00902,
+ "frequency": -9999
+ },
+ "SN47": {
+ "model": "?",
+ "sensitivity": 0.00898,
+ "frequency": -9999
+ },
+ "SN48": {
+ "model": "60-UHP",
+ "sensitivity": 0.00909,
+ "frequency": -9999
+ },
+ "SN49": {
+ "model": "60-UHP",
+ "sensitivity": 0.00904,
+ "frequency": -9999
+ },
+ "SN50": {
+ "model": "60-UHP",
+ "sensitivity": 0.00905,
+ "frequency": -9999
+ },
+ "SN51": {
+ "model": "60-UHP",
+ "sensitivity": 0.00902,
+ "frequency": -9999
+ },
+ "SN73": {
+ "model": "?",
+ "sensitivity": 0.00898,
+ "frequency": -9999
+ },
+ "SN74": {
+ "model": "60-UHP",
+ "sensitivity": 0.00903,
+ "frequency": -9999
+ },
+ "SN80": {
+ "model": "?",
+ "sensitivity": 0.00905,
+ "frequency": -9999
+ },
+ "SN81": {
+ "model": "?",
+ "sensitivity": 0.00898,
+ "frequency": -9999
+ },
+ "SN82": {
+ "model": "?",
+ "sensitivity": 0.00901,
+ "frequency": -9999
+ },
+ "SN83": {
+ "model": "?",
+ "sensitivity": 0.00902,
+ "frequency": -9999
+ },
+ "SN84": {
+ "model": "?",
+ "sensitivity": 0.00902,
+ "frequency": -9999
+ },
+ "SN85": {
+ "model": "?",
+ "sensitivity": 0.00902,
+ "frequency": -9999
+ },
+ "SN86": {
+ "model": "?",
+ "sensitivity": 0.00898,
+ "frequency": -9999
+ },
+ "SN87": {
+ "model": "?",
+ "sensitivity": 0.00903,
+ "frequency": -9999
+ }
}