pyspectra: A CLI for Engineers to Request Seismic Data

Posted on Sep 21, 2024
tl;dr: I wrote a CLI in Python to retrieve data used by structural engineers for seismic design and analysis of buildings and other structures.

Motivation

Constructing another command-line interface (CLI), but this time it’s in Python. I wanted to pick up more CLI development experience with languages other than Go. The argparse module is a CLI framework that ships with Python’s standard library and is the framework I selected to build this CLI.

Repo: Check it out.

Similar to the last CLI I wrote, my goals were simple:

  1. Formatting the JSON server response into a readable table to be output directly to the terminal.
  2. Take advantage of piping (|) and redirection (>) functionality where available for further processing or writing to a file. In other words, it should be able to preserve the response data in its JSON format to be inspected with CLI tools like jq or be able to emit portions of data in another format such as CSV.

An Aside: A Short (the Shortest…) Segue about Seismic Design

Earthquake engineering is a subset within structural engineering. Structures, especially ones along the west coast of the United States, experience earthquakes and are a design consideration for new construction as well as existing construction (e.g. when seismic retrofits are required by building code). A common task structural engineers regularly need to know is an estimate of a structure’s lateral load demand known as the base shear. Using a procedure such as the Equivalent Lateral Force method, the base shear is calculated as a proportion of the building’s weight and without going into too much theory, is an estimate of the lateral load that a building may experience during a design event. For example, the simplest representation of base shear for a short-period building can be reduced to the following equation:

$$ V = \frac{S_{DS}}{\frac{R}{I_e}}W $$

where:

$$ \begin{align*} V &= \text{Base shear} \newline S_{DS} &= \text{Design spectral acceleration} \newline R &= \text{Response modification factor} \newline I_e &= \text{Importance factor} \newline W &= \text{Building weight} \end{align*} $$

While $R$, $I_e$, and $W$ are fixed quantities defined in the ASCE 7 documents, the value of $S_{DS}$ (along with other site specific data) is the value that I want programmatic access from my terminal. There’s more theory behind how these values are developed in a field called Probabilistic Seismic Hazard Analysis with its origins rooted in a paper written by C. Allin Cornell.

API Observations

The USGS provides an API to access seismic design data (e.g. $S_{DS}$) programmatically and is the basis for browser-based tools such as the ASCE Hazard Tool. With each new release of a design standard, comes an expansion of USGS’s JSON server response and an increase in the number of seismic data parameters reported from a client’s request. For fun, I wanted to understand how many more parameters are returned from all similar standards so I wrote a Node.js script that recursively searches through the JSON responses and builds a composition JavaScript object of the properties that are common to all design standards (a set intersection) as well as the set union of all properties for each of the standards.

First Iteration

The first iteration of the CLI was a barebones use of argparse and some of its basic features. How the positional and optional arguments work, help and usage messages, and etc. With argparse, a help/usage message comes free with the instantiation of an ArgumentParser object:

$ python3 webspectra.py -h
usage: webspectra.py [-h] [-o {json,table}] std latitude longitude risk_category site_class

A command utility to obtain USGS seismic accelerations.

positional arguments:
  std                   Design Standard
  latitude              Site Latitude
  longitude             Site Longitude
  risk_category         Risk Category
  site_class            Site Class

options:
  -h, --help            show this help message and exit
  -o {json,table}, --output {json,table}
                        output format

Here’s an example run:

$ python3 webspectra.py asce7-22 47.61 -122.33 II D
------------------------------------------------
| Input             | Values                   |
------------------------------------------------
| date              | 2024-09-20T05:39:23.850Z |
| referenceDocument | ASCE7-22                 |
| latitude          | 47.61                    |
| longitude         | -122.33                  |
| riskCategory      | II                       |
| siteClass         | D                        |
| title             | Example                  |
------------------------------------------------
-----------------------------------------------------------------
| Parameters | Values | Description                             |
-----------------------------------------------------------------
| pgam       | 0.68   | Site Mod. PGA                           |
| sms        | 1.72   | Site-Modified SS (Sms)                  |
| sm1        | 1.37   | Site-Modified S1 (Sm1)                  |
| sds        | 1.14   | Design Spectra (Sds)                    | # <- here's S_ds  
| sd1        | 0.92   | Design Spectra (Sd1)                    |
| sdc        | D      | Seismic Design Category via Sds and Sd1 |
| ss         | 1.56   | Short Spectra (SS) MCEr                 |
| s1         | 0.65   | S1 MCEr                                 |
| tl         | 6      | Long-period Transition, Tl (seconds)    |
| cv         | None   | Vertical Coefficient (Cv)               |
-----------------------------------------------------------------
-----------------------------------------------------------------------------------------------
| Metadata                   | Values          | Description                                  |
-----------------------------------------------------------------------------------------------
| vs30                       | 260             | Shear Wave Velocity (m/s)                    |
| modelVersion               | v5.0.x          | Version of USGS Hazard Model                 |
| pgadFloor                  | 0.53            | Det. Lower Limit Peak Ground Accel. (g)      |
| pgadPercentileFactor       | 1               | Factor To Achieve Target Ground Motion (PGA) |
| spatialInterpolationMethod | linearloglinear | Interpolation Method Used                    |
-----------------------------------------------------------------------------------------------

This first pass is still limited. At the time, I didn’t spend much time on building functionality for returning series data to users with the first iteration. Also, this is probably more than enough output information to display for someone that wants a snapshot of seismic hazard data at a specific site and should probably be reduced for viewing on narrower terminal widths and for brevity. The functionality of the CLI works, though I haven’t done extensive testing though to see where things might break.

Take Two: Adding Functionality - Creating subcommands with argparse

Turning the design standards into subcommands using subparsers was intended to address the following issues:

  • Some design standards have differing ranges of choices for Site Classes such as the differences between the ASCE 7-05 and ASCE 7-22 design standards. ASCE 7-05 ranges from A through E, while ASCE 7-22 also ranged from classes A through E with more granularity of Site Class with hybrid values such as AB, BC, DE.
  • Each design standard has different available choices for series spectrum data (i.e. design spectrum, MCEr spectrum, multi-period spectrum, etc.). For example, the ASCE 7-05 has only two kinds of spectra available while the ASCE 7-22 can have as many as eight for a site.

Fundamentally, each subcommand for a standard is going to look pretty similar between each other. The general structure of the subcommands can be reduced to the following form:

# ...having previously instantiated ArgumentParser and a subparser object...

asce722_parser = subparsers.add_parser(
    'asce7-22', 
    aliases=['asce'],
    description="Seismic design data from the ASCE 7-22 design standard.",
    help='ASCE 7-22 help'
    )
asce722_parser.add_argument(**latitude_arg_template)
asce722_parser.add_argument(**longitude_arg_template)
asce722_parser.add_argument(**risk_category_arg_template)
asce722_parser.add_argument(**site_class_arg_template_three)
asce722_parser.add_argument(
    '-s', '--spectrum',
    help='request spectrum only',
    choices=['two-design', 'two-mcer',
            'vert-design', 'vert-mcer', 
            'multi-design', 'multi-mcer',
            'risk-targeted', '84th',
            ],
    )
asce722_parser.set_defaults(std='asce7-22', fn=make_request)

# some more design standard subparsers here...

args = parser.parse_args()
args.fn(args) # calls the make_request function

The output format still functions the same as the first iteration for querying data at a particular site.

When addressing how best to retrieve spectrum data, I defaulted the output to emit CSV that way if an engineer was using the CLI, they could redirect it to a file and then use that file to import it into Excel or literally copy and paste the output from the terminal into Excel (I mean… let’s face it, knowing the engineering profession, it’s going end up in Excel one way or another…) and perform some ’text-to-column’ manipulation to make the series data usable. It also outputs the series data into JSON in case I’m interested in further manipulating the data with jq or another JSON utility.

Below, is an example run for retrieving a design spectrum formatted into JSON to the terminal:

$ python3 webspectra.py -o json asce7-22 -s two-design 47.61 -122.33 II D
{"periods": [0, 0.025, 0.05, 0.1, 0.15, # ... 134 points omitted for brevity ...
             6.8, 6.85, 6.9, 6.95, 7],
"ordinates": [0.46, 0.56, 0.67, 0.89, 1.1, # ... 134 points omitted for brevity ...
              0.12, 0.12, 0.12, 0.11, 0.11]}

The same call, but emitting CSV:

$ python3 webspectra.py asce7-22 -s two-design 47.61 -122.33 II D
0,0.46
0.025,0.56
0.05,0.67
0.1,0.89
0.15,1.1
# ... 134 points omitted for brevity ...
6.8,0.12
6.85,0.12
6.9,0.12
6.95,0.11
7,0.11

Future Directions

There’s more room for improvement with this CLI. The CLI currently handles seismic design data only. Adding more hazard load cases such as snow load and wind speeds are certainly a possibility. Also, creating a more compact way to display queried data is another option. For now, I think this is a fine stopping point.