diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml new file mode 100644 index 00000000..2d540ba1 --- /dev/null +++ b/.github/workflows/docs.yaml @@ -0,0 +1,25 @@ +name: docs + +on: + push: + branches: + - main +permissions: + contents: write +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: 3.10 + - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV + - uses: actions/cache@v3 + with: + key: mkdocs-material-${{ env.cache_id }} + path: .cache + restore-keys: | + mkdocs-material- + - run: pip install .[dev] + - run: mkdocs gh-deploy --force diff --git a/docs/components/image_function.md b/docs/components/image_function.md new file mode 100644 index 00000000..6b7e2a35 --- /dev/null +++ b/docs/components/image_function.md @@ -0,0 +1,8 @@ +# Image Function + +For image functions this version of Lassie relies heavily on machine learning pickers delivered by [SeisBench](https://github.com/seisbench/seisbench). + +## PhaseNet + +!!! abstract "PhaseNet Citation" + Zhu, Weiqiang, and Gregory C. Beroza. "PhaseNet: A Deep-Neural-Network-Based Seismic Arrival Time Picking Method." arXiv preprint arXiv:1803.03211 (2018). diff --git a/docs/components/octree.md b/docs/components/octree.md new file mode 100644 index 00000000..7f6e11a6 --- /dev/null +++ b/docs/components/octree.md @@ -0,0 +1,3 @@ +# Octree + +We search the for energy in a 3D octree structure to simultaneously speed up the search and improve the resolution of the localisations. diff --git a/docs/components/ray_tracer.md b/docs/components/ray_tracer.md new file mode 100644 index 00000000..d093eebf --- /dev/null +++ b/docs/components/ray_tracer.md @@ -0,0 +1,31 @@ +# Ray Tracers + +The calculation of seismic travel times is a cornerstone for the migration and stacking approach. Lassie supports different ray tracers for travel time calculation, which can be adapted for different geological settings. + +## Constant Velocity + +The constant velocity models is trivial and follows + +$$ +t_{P} = \frac{d}{v_P} +$$ + +## 1D Layered Model + +The 1D ray tracer is based on [Pyrocko Cake](https://pyrocko.org/docs/current/apps/cake/manual.html#command-line-examples). + +![Pyrocko Cake Ray Tracer](https://pyrocko.org/docs/current/_images/cake_plot_example_2.png) +*Pyrocko Cake 1D ray tracer for travel time calculation in 1D layered media* + +## 3D Velocity Model + +We implement the fast marching method for calculating first arrivals of waves in 3D volumes. + +* [x] Import [NonLinLoc](http://alomax.free.fr/nlloc/) 3D velocity model +* [x] 1D Layered 🥼 +* [x] Constant velocity 🥼 + +For quality check, all 3D velocity models are exported to `vtk/` folder as `.vti` files. Use [ParaView](https://www.paraview.org/) to inspect and explore the velocity models. + +![Velocity model FORGE](../images/FORGE-velocity-model.webp) +*Seismic velocity model of the Utah FORGE testbed site, visualized in ParaView.* diff --git a/docs/components/seismic_data.md b/docs/components/seismic_data.md new file mode 100644 index 00000000..5244b7d7 --- /dev/null +++ b/docs/components/seismic_data.md @@ -0,0 +1,14 @@ +# Seismic Data + +## Waveform Data + +The seismic can be delivered in MiniSeed or any other format compatible with Pyrocko. + +## Meta Data + +Supported data formats are: + +* [x] [StationXML](https://www.fdsn.org/xml/station/) +* [x] [Pyrocko Station YAML](https://pyrocko.org/docs/current/formats/yaml.html) + +Metadata does not need to include response information for pure detection and localisation. If local magnitudes $M_L$ are extracted response information is required. diff --git a/docs/getting_started.md b/docs/getting_started.md new file mode 100644 index 00000000..47e95e2b --- /dev/null +++ b/docs/getting_started.md @@ -0,0 +1,116 @@ +# Getting Started + +## Installation + +The installation is straight-forward: + +```sh +pip install lassie-v2 +``` + +or install from GitHub + +```sh title="From GitHub" +pip install git+https://github.com/pyrocko/lassie-v2 +``` + +## Initializing a new Project + +Once installed you can run the lassie executeable + +```sh title="Initialize new Project" +lassie init my-project +``` + +Check out the `search.json` config file and add your waveform data and velocity models. + +??? abstract "Minimal Configuration Example" + Here is a minimal JSON configuration for Lassie + ```json + { + "project_dir": ".", + "stations": { + "station_xmls": [], + "pyrocko_station_yamls": ["search/pyrocko-stations.yaml"], + }, + "data_provider": { + "provider": "PyrockoSquirrel", + "environment": ".", + "waveform_dirs": ["data/"], + }, + "octree": { + "location": { + "lat": 0.0, + "lon": 0.0, + "east_shift": 0.0, + "north_shift": 0.0, + "elevation": 0.0, + "depth": 0.0 + }, + "size_initial": 2000.0, + "size_limit": 500.0, + "east_bounds": [ + -10000.0, + 10000.0 + ], + "north_bounds": [ + -10000.0, + 10000.0 + ], + "depth_bounds": [ + 0.0, + 20000.0 + ], + "absorbing_boundary": 1000.0 + }, + "image_functions": [ + { + "image": "PhaseNet", + "model": "ethz", + "torch_use_cuda": false, + "phase_map": { + "P": "constant:P", + "S": "constant:S" + }, + "weights": { + "P": 1.0, + "S": 1.0 + } + } + ], + "ray_tracers": [ + { + "tracer": "ConstantVelocityTracer", + "phase": "constant:P", + "velocity": 5000.0 + } + ], + "station_corrections": { + "rundir": null, + "measure": "median", + "weighting": "mul-PhaseNet-semblance", + "minimum_num_picks": 5, + "minimum_distance_border": 2000.0, + "minimum_depth": 3000.0 + }, + "event_features": [], + "sampling_rate": 100, + "detection_threshold": 0.05, + "detection_blinding": "PT2S", + "image_mean_p": 1.0, + "node_split_threshold": 0.9, + "window_length": "PT300S", + "n_threads_parstack": 0, + "n_threads_argmax": 4, + "plot_octree_surface": false, + "created": "2023-10-29T19:17:17.676279Z" + } + ``` + + +## Starting the Search +Once happy, start the search with + +```sh title="Start earthquake detection" +lassie search search.json +``` diff --git a/docs/images/FORGE-velocity-model.webp b/docs/images/FORGE-velocity-model.webp new file mode 100644 index 00000000..399ef062 Binary files /dev/null and b/docs/images/FORGE-velocity-model.webp differ diff --git a/docs/images/logo.webp b/docs/images/logo.webp new file mode 100644 index 00000000..cffad92e Binary files /dev/null and b/docs/images/logo.webp differ diff --git a/docs/images/qgis-loaded.webp b/docs/images/qgis-loaded.webp new file mode 100644 index 00000000..23206cba Binary files /dev/null and b/docs/images/qgis-loaded.webp differ diff --git a/docs/images/reykjanes-demo.webp b/docs/images/reykjanes-demo.webp new file mode 100644 index 00000000..8bdb0e7a Binary files /dev/null and b/docs/images/reykjanes-demo.webp differ diff --git a/docs/images/squirrel-reykjanes.webp b/docs/images/squirrel-reykjanes.webp new file mode 100644 index 00000000..e59c6b53 Binary files /dev/null and b/docs/images/squirrel-reykjanes.webp differ diff --git a/docs/index.md b/docs/index.md index 000ea345..3b690e3c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,17 +1,37 @@ -# Welcome to MkDocs +# Welcome to Lassie 🐕‍🦺 -For full documentation visit [mkdocs.org](https://www.mkdocs.org). +Lassie is an earthquake detection and localisation framework. It combines modern **machine learning phase detection and robust migration and stacking techniques**. -## Commands +The detector is leveraging [Pyrocko](https://pyrocko.org) and [SeisBench](https://github.com/seisbench/seisbench), it is highly-performant and can scan massive data sets efficiently. -* `mkdocs new [dir-name]` - Create a new project. -* `mkdocs serve` - Start the live-reloading docs server. -* `mkdocs build` - Build the documentation site. -* `mkdocs -h` - Print help message and exit. +![Reykjanes detections](images/reykjanes-demo.webp) -## Project layout +*Seismic swarm activity at Iceland, Reykjanes Peninsula during a 2020 unrest. 15,000+ earthquakes detected, outlining a dike intrusion, preceeding the 2021 Fagradasfjall eruption. Visualized in [Pyrocko Sparrow](https://pyrocko.org).* - mkdocs.yml # The configuration file. - docs/ - index.md # The documentation homepage. - ... # Other markdown pages, images and other files. +## Features + +!!! note "Citation" + TDB + +* [x] Earthquake phase detection using machine-learning pickers from [SeisBench](https://github.com/seisbench/seisbench) +* [x] Octree localisation approach for efficient and accurate search +* [x] Different velocity models: + * [x] Constant velocity + * [x] 1D Layered velocity model + * [x] 3D fast-marching velocity model (NonLinLoc compatible) +* [x] Extraction of earthquake event features: + * [x] Local magnitudes + * [x] Ground motion attributes +* [x] Automatic extraction of modelled and picked travel times +* [x] Calculation and application of station corrections / station delay times + + + + +[Get Started!](getting_started.md){ .md-button } + +## Build with + +![Pyrocko](https://pyrocko.org/docs/current/_images/pyrocko_shadow.png){height=150 padding=50} +![GFZ](https://www.gfz-potsdam.de/fileadmin/gfz/GFZ.svg){width=150, padding=50} +![SeisBench](https://seisbench.readthedocs.io/en/stable/_images/seisbench_logo_subtitle_outlined.svg){height=100, padding=50} diff --git a/docs/visualizing_results.md b/docs/visualizing_results.md new file mode 100644 index 00000000..b16f755b --- /dev/null +++ b/docs/visualizing_results.md @@ -0,0 +1,15 @@ +# Visualizing Detections + +The event detections are exported in Lassie-native JSON, Pyrocko YAML format and as CSV files. + +## Pyrocko Sparrow + +For large data sets use the [Pyrocko Sparrow](https://pyrocko.org) to visualise seismic event detections in 3D. Also seismic stations and many other features from the Pyrocko ecosystem can be integrated. + +![Pyrocko Squirrel EQ Detections](images/squirrel-reykjanes.webp) + +## QGIS + +Import the `.csv` file into QGIS and render by e.g. the detection semblance or the calculated magnitude. + +![QGIS EQ Detections](images/qgis-loaded.webp) diff --git a/lassie/octree.py b/lassie/octree.py index c7e4c0c7..58a92386 100644 --- a/lassie/octree.py +++ b/lassie/octree.py @@ -140,7 +140,7 @@ def as_location(self) -> Location: if not self.tree: raise AttributeError("parent tree not set") if not self._location: - reference = self.tree.reference + reference = self.tree.location self._location = Location.model_construct( lat=reference.lat, lon=reference.lon, @@ -159,12 +159,14 @@ def __iter__(self) -> Iterator[Node]: yield self def hash(self) -> bytes: + if not self.tree: + raise AttributeError("parent tree not set") if self._hash is None: self._hash = sha1( struct.pack( "dddddd", - self.tree.reference.lat, - self.tree.reference.lon, + self.tree.location.lat, + self.tree.location.lon, self.east, self.north, self.depth, @@ -178,7 +180,7 @@ def __hash__(self) -> int: class Octree(BaseModel): - reference: Location = Location(lat=0.0, lon=0) + location: Location = Location(lat=0.0, lon=0.0) size_initial: PositiveFloat = 2 * KM size_limit: PositiveFloat = 500 east_bounds: tuple[float, float] = (-10 * KM, 10 * KM) @@ -191,6 +193,12 @@ class Octree(BaseModel): model_config = ConfigDict(ignored_types=(cached_property,)) + @field_validator("location") + def check_reference(cls, location: Location) -> Location: # noqa: N805 + if location.lat == 0.0 and location.lon == 0.0: + raise ValueError("invalid location, expected non-zero lat/lon") + return location + @field_validator("east_bounds", "north_bounds", "depth_bounds") def check_bounds( cls, # noqa: N805 diff --git a/lassie/search.py b/lassie/search.py index 55049fc5..7399d9cc 100644 --- a/lassie/search.py +++ b/lassie/search.py @@ -303,7 +303,7 @@ async def start(self, force_rundir: bool = False) -> None: ) remaining_time *= batch.n_batches - batch.i_batch - 1 logger.info( - "processing rate %s/s - %s remaining - estimated finish at %s", + "processing rate %s/s - %s remaining - finish at %s", processing_rate_bytes, remaining_time, datetime.now() + remaining_time, # noqa: DTZ005 diff --git a/lassie/tracers/cake.py b/lassie/tracers/cake.py index 549e8e11..c53b1304 100644 --- a/lassie/tracers/cake.py +++ b/lassie/tracers/cake.py @@ -556,7 +556,7 @@ async def prepare( cached_trees = self._load_cached_trees() distances = surface_distances(octree, stations) - source_depths = np.asarray(octree.depth_bounds) - octree.reference.elevation + source_depths = np.asarray(octree.depth_bounds) - octree.location.elevation receiver_depths = np.fromiter((sta.effective_depth for sta in stations), float) distance_bounds = (distances.min(), distances.max()) diff --git a/lassie/tracers/fast_marching/fast_marching.py b/lassie/tracers/fast_marching/fast_marching.py index 84a5b804..23c406fc 100644 --- a/lassie/tracers/fast_marching/fast_marching.py +++ b/lassie/tracers/fast_marching/fast_marching.py @@ -404,12 +404,12 @@ async def prepare( logger.info("exporting vtk files") velocity_model.export_vtk( vtk_dir / f"velocity-model-{self.phase}", - reference=octree.reference, + reference=octree.location, ) for volume in self._travel_time_volumes.values(): volume.export_vtk( vtk_dir / f"travel-times-{volume.station.pretty_nsl}", - reference=octree.reference, + reference=octree.location, ) def _load_cached_tavel_times(self, cache_dir: Path) -> None: diff --git a/lassie/tracers/fast_marching/velocity_models.py b/lassie/tracers/fast_marching/velocity_models.py index f4f0e733..502399ac 100644 --- a/lassie/tracers/fast_marching/velocity_models.py +++ b/lassie/tracers/fast_marching/velocity_models.py @@ -280,7 +280,7 @@ def get_model(self, octree: Octree) -> VelocityModel3D: grid_spacing = self.grid_spacing model = VelocityModel3D( - center=octree.reference, + center=octree.location, grid_spacing=grid_spacing, east_bounds=octree.east_bounds, north_bounds=octree.north_bounds, @@ -546,7 +546,7 @@ def get_model(self, octree: Octree) -> VelocityModel3D: grid_spacing = self.grid_spacing model = VelocityModel3D( - center=octree.reference, + center=octree.location, grid_spacing=grid_spacing, east_bounds=octree.east_bounds, north_bounds=octree.north_bounds, diff --git a/mkdocs.yml b/mkdocs.yml index 9198fde5..68600857 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,14 +1,74 @@ site_name: Lassie site_description: The friendly earthquake detector -repo_url: https://github.com/miili/lassie-v2 -repo_name: miili/lassie-v2 - - +repo_url: https://github.com/pyrocko/lassie-v2 +repo_name: pyrocko/lassie-v2 theme: name: material palette: - scheme: slate + - scheme: default + primary: blue grey + toggle: + icon: material/toggle-switch + name: Switch to dark mode + # Palette toggle for dark mode + - scheme: slate + primary: blue grey + toggle: + icon: material/toggle-switch-off-outline + name: Switch to light mode icon: repo: fontawesome/brands/git-alt - logo: logos/lassie.webp + logo: images/logo.webp + features: + - navigation.tabs + - search.suggest + - content.code.copy + +markdown_extensions: + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences + - admonition + - pymdownx.details + - pymdownx.arithmatex: + generic: true + - pymdownx.tasklist + - attr_list + +extra_javascript: + - javascripts/mathjax.js + - https://polyfill.io/v3/polyfill.min.js?features=es6 + - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js + +watch: + - lassie + +plugins: + - search + - mkdocstrings: + default_handler: python + handlers: + python: + paths: [.] + options: + members_order: source + separate_signature: true + docstring_options: + ignore_init_summary: true + merge_init_into_class: true + +nav: + - 🐕‍🦺 Earthquake Detector: + - Welcome: index.md + - Getting Started: getting_started.md + - Visualising Detections: visualizing_results.md + - Components: + - Seismic Data: components/seismic_data.md + - Ray Tracer: components/ray_tracer.md + - Image Function: components/image_function.md + - Octree: components/octree.md diff --git a/pyproject.toml b/pyproject.toml index 40ebcfd4..5cb608f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,4 @@ + [build-system] requires = [ "wheel", @@ -56,13 +57,13 @@ classifiers = [ [project.optional-dependencies] dev = [ - "pre-commit", - "black", - "ruff", - "pytest", - "pytest-asyncio", - "mkdocs-material", - "mkdocstrings-python", + "pre-commit>=3.4", + "black>=23.7", + "ruff>=0.1", + "pytest>=7.4", + "pytest-asyncio>=0.21", + "mkdocs-material>=9.4", + "mkdocstrings[python]>=0.23", ] [project.scripts] diff --git a/test/conftest.py b/test/conftest.py index 11f9189c..009f7945 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -92,7 +92,7 @@ def data_dir() -> Path: @pytest.fixture(scope="session") def octree() -> Octree: return Octree( - reference=Location( + location=Location( lat=10.0, lon=10.0, elevation=1.0 * KM, diff --git a/test/test_cake.py b/test/test_cake.py index 314af37c..8fb192a9 100644 --- a/test/test_cake.py +++ b/test/test_cake.py @@ -20,7 +20,7 @@ @pytest.fixture(scope="session") def small_octree() -> Octree: return Octree( - reference=Location( + location=Location( lat=10.0, lon=10.0, elevation=0.2 * KM, diff --git a/test/test_fast_marching.py b/test/test_fast_marching.py index fb0282cb..79322ab6 100644 --- a/test/test_fast_marching.py +++ b/test/test_fast_marching.py @@ -82,7 +82,7 @@ def stations_inside( def octree_cover(model: VelocityModel3D) -> Octree: return Octree( - reference=model.center, + location=model.center, size_initial=2 * KM, size_limit=500, east_bounds=model.east_bounds, @@ -96,7 +96,7 @@ def octree_cover(model: VelocityModel3D) -> Octree: async def station_travel_times( octree: Octree, stations: Stations ) -> StationTravelTimeVolume: - octree.reference.elevation = 1 * KM + octree.location.elevation = 1 * KM model = Constant3DVelocityModel(velocity=CONSTANT_VELOCITY, grid_spacing=100.0) model_3d = model.get_model(octree) return await StationTravelTimeVolume.calculate_from_eikonal(