From a083c2a57e4696fb332f069524ce1ca54c36129b Mon Sep 17 00:00:00 2001 From: miili Date: Tue, 31 Oct 2023 00:34:00 +0100 Subject: [PATCH] docs --- docs/components/image_function.md | 7 ++ docs/components/octree.md | 7 ++ docs/components/ray_tracer.md | 25 ++++++- docs/components/seismic_data.md | 16 ++++- docs/components/station_corrections.md | 9 +++ docs/getting_started.md | 10 ++- lassie/apps/lassie.py | 17 +++-- lassie/images/phase_net.py | 68 +++++++++++++++---- lassie/models/station.py | 17 +++-- lassie/octree.py | 36 ++++++++-- lassie/station_corrections.py | 30 ++++++-- lassie/tracers/cake.py | 22 +++--- lassie/tracers/constant_velocity.py | 12 +++- lassie/tracers/fast_marching/fast_marching.py | 15 ++-- lassie/utils.py | 41 ++++++++++- lassie/waveforms/squirrel.py | 43 +++++++++--- mkdocs.yml | 19 ++---- 17 files changed, 314 insertions(+), 80 deletions(-) diff --git a/docs/components/image_function.md b/docs/components/image_function.md index 5711ab70..10582bf3 100644 --- a/docs/components/image_function.md +++ b/docs/components/image_function.md @@ -6,3 +6,10 @@ For image functions this version of Lassie relies heavily on machine learning pi !!! abstract "Citation PhaseNet" *Zhu, Weiqiang, and Gregory C. Beroza. "PhaseNet: A Deep-Neural-Network-Based Seismic Arrival Time Picking Method." arXiv preprint arXiv:1803.03211 (2018).* + +```python exec='on' +from lassie.utils import generate_docs +from lassie.images.phase_net import PhaseNet + +print(generate_docs(PhaseNet())) +``` diff --git a/docs/components/octree.md b/docs/components/octree.md index e82a48cb..b0ab2334 100644 --- a/docs/components/octree.md +++ b/docs/components/octree.md @@ -1,3 +1,10 @@ # Octree A 3D space is searched for sources of seismic energy. Lassie created an octree structure which is iteratively refined when energy is detected, to focus on the source' location. This speeds up the search and improves the resolution of the localisations. + +```python exec='on' +from lassie.utils import generate_docs +from lassie.octree import Octree + +print(generate_docs(Octree())) +``` diff --git a/docs/components/ray_tracer.md b/docs/components/ray_tracer.md index ef93e48e..3fc3ab09 100644 --- a/docs/components/ray_tracer.md +++ b/docs/components/ray_tracer.md @@ -12,6 +12,13 @@ $$ This module is used for simple use cases and cross-referencing testing. +```python exec='on' +from lassie.utils import generate_docs +from lassie.tracers.constant_velocity import ConstantVelocityTracer + +print(generate_docs(ConstantVelocityTracer())) +``` + ## 1D Layered Model Calculation of travel times in 1D layered media is based on the [Pyrocko Cake](https://pyrocko.org/docs/current/apps/cake/manual.html#command-line-examples) ray tracer. @@ -19,14 +26,28 @@ Calculation of travel times in 1D layered media is based on the [Pyrocko Cake](h ![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 +```python exec='on' +from lassie.utils import generate_docs +from lassie.tracers.cake import CakeTracer + +print(generate_docs(CakeTracer(), exclude={'earthmodel': {'raw_file_data'}})) +``` -We implement the fast marching method for calculating first arrivals of waves in 3D volumes. +## 3D Fast Marching + +We implement the fast marching method for calculating first arrivals of waves in 3D volumes. Currently three different 3D velocity models are supported: * [x] Import [NonLinLoc](http://alomax.free.fr/nlloc/) 3D velocity model * [x] 1D layered model 🥞 * [x] Constant velocity, mainly for testing purposes 🥼 +```python exec='on' +from lassie.utils import generate_docs +from lassie.tracers.fast_marching import FastMarchingTracer + +print(generate_docs(FastMarchingTracer())) +``` + ### Visualizing 3D Models 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. diff --git a/docs/components/seismic_data.md b/docs/components/seismic_data.md index 755a8175..8ecb3c11 100644 --- a/docs/components/seismic_data.md +++ b/docs/components/seismic_data.md @@ -4,7 +4,14 @@ The seismic can be delivered in MiniSeed or any other format compatible with Pyrocko. -Organize your data in an [SDS structure](https://www.seiscomp.de/doc/base/concepts/waveformarchives.html) or just a blob if MiniSeed. +Organize your data in an [SDS structure](https://www.seiscomp.de/doc/base/concepts/waveformarchives.html) or just a single MiniSeed file. + +```python exec='on' +from lassie.utils import generate_docs +from lassie.waveforms import PyrockoSquirrel + +print(generate_docs(PyrockoSquirrel())) +``` ## Meta Data @@ -16,3 +23,10 @@ Supported data formats are: * [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. + +```python exec='on' +from lassie.utils import generate_docs +from lassie.models.station import Stations + +print(generate_docs(Stations())) +``` diff --git a/docs/components/station_corrections.md b/docs/components/station_corrections.md index e1901688..a35818c6 100644 --- a/docs/components/station_corrections.md +++ b/docs/components/station_corrections.md @@ -1 +1,10 @@ # Station Corrections + +Station corrections can be extract from previous runs to refine the localisation accuracy. The corrections can also help to improve the semblance find more events in a dataset. + +```python exec='on' +from lassie.utils import generate_docs +from lassie.models.station import Stations + +print(generate_docs(Stations())) +``` diff --git a/docs/getting_started.md b/docs/getting_started.md index 6929f83c..a87dbcea 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -8,6 +8,14 @@ The installation is straight-forward: pip install git+https://github.com/pyrocko/lassie-v2 ``` +## Running Lassie + +The main entry point in the executeable is the `lassie` command. The provided command line interface (CLI) and a JSON config file is all what is needed to run the program. + +```bash exec='on' result='ansi' source='above' +lassie -h +``` + ## Initializing a New Project Once installed you can run the lassie executeable to initialize a new project. @@ -96,8 +104,6 @@ Check out the `search.json` config file and add your waveform data and velocity "window_length": "PT300S", "n_threads_parstack": 0, "n_threads_argmax": 4, - "plot_octree_surface": false, - "created": "2023-10-29T19:17:17.676279Z" } ``` diff --git a/lassie/apps/lassie.py b/lassie/apps/lassie.py index 9cf30fb3..49e25dd9 100644 --- a/lassie/apps/lassie.py +++ b/lassie/apps/lassie.py @@ -11,10 +11,6 @@ from pkg_resources import get_distribution from lassie.console import console -from lassie.models import Stations -from lassie.search import Search -from lassie.server import WebServer -from lassie.station_corrections import StationCorrections from lassie.utils import CACHE_DIR, setup_rich_logging nest_asyncio.apply() @@ -22,7 +18,7 @@ logger = logging.getLogger(__name__) -def main() -> None: +def get_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( prog="lassie", description="Lassie - The friendly earthquake detector 🐕", @@ -122,7 +118,18 @@ def main() -> None: ) dump_schemas.add_argument("folder", type=Path, help="folder to dump schemas to") + return parser + + +def main() -> None: + parser = get_parser() args = parser.parse_args() + + from lassie.models import Stations + from lassie.search import Search + from lassie.server import WebServer + from lassie.station_corrections import StationCorrections + setup_rich_logging(level=logging.INFO - args.verbose * 10) if args.command == "init": diff --git a/lassie/images/phase_net.py b/lassie/images/phase_net.py index 3ab6343f..602f4b11 100644 --- a/lassie/images/phase_net.py +++ b/lassie/images/phase_net.py @@ -82,22 +82,60 @@ def search_phase_arrival( class PhaseNet(ImageFunction): + """PhaseNet image function. For more details see SeisBench documentation.""" + image: Literal["PhaseNet"] = "PhaseNet" - model: ModelName = "ethz" - window_overlap_samples: int = Field(default=2000, ge=1000, le=3000) - torch_use_cuda: bool = False - torch_cpu_threads: PositiveInt = 4 - batch_size: int = Field(default=64, ge=64) - stack_method: StackMethod = "avg" - upscale_input: PositiveInt = 1 - phase_map: dict[PhaseName, str] = { - "P": "constant:P", - "S": "constant:S", - } - weights: dict[PhaseName, PositiveFloat] = { - "P": 1.0, - "S": 1.0, - } + model: ModelName = Field( + default="ethz", + description="SeisBench pre-trained PhaseNet model to use. " + "Choose from `ethz`, `geofon`, `instance`, `iquique`, `lendb`, `neic`, `obs`," + " `original`, `scedc`, `stead`." + " For more details see SeisBench documentation", + ) + window_overlap_samples: int = Field( + default=2000, + ge=1000, + le=3000, + description="Window overlap in samples.", + ) + torch_use_cuda: bool = Field( + default=False, + description="Use CUDA for inference.", + ) + torch_cpu_threads: PositiveInt = Field( + default=4, + description="Number of CPU threads to use if only CPU is used.", + ) + batch_size: int = Field( + default=64, + ge=64, + description="Batch size for inference, larger values can improve performance.", + ) + stack_method: StackMethod = Field( + default="avg", + description="Method to stack the overlaping blocks internally. " + "Choose from `avg` and `max`.", + ) + upscale_input: PositiveInt = Field( + default=1, + description="Upscale input by factor. " + "This augments the input data from e.g. 100 Hz to 50 Hz (factor: `2`). Can be" + " useful for high-frequency earthquake signals.", + ) + phase_map: dict[PhaseName, str] = Field( + default={ + "P": "constant:P", + "S": "constant:S", + }, + description="Phase mapping from SeisBench PhaseNet to Lassie phases.", + ) + weights: dict[PhaseName, PositiveFloat] = Field( + default={ + "P": 1.0, + "S": 1.0, + }, + description="Weights for each phase.", + ) _phase_net: PhaseNetSeisBench = PrivateAttr(None) diff --git a/lassie/models/station.py b/lassie/models/station.py index 9f730f9e..91eab159 100644 --- a/lassie/models/station.py +++ b/lassie/models/station.py @@ -56,11 +56,20 @@ def __hash__(self) -> int: class Stations(BaseModel): - station_xmls: list[Path] = [] - pyrocko_station_yamls: list[Path] = [] - + pyrocko_station_yamls: list[Path] = Field( + default=[], + description="List of Pyrocko station yaml files.", + ) + station_xmls: list[Path] = Field( + default=[], + description="List of StationXML files.", + ) + + blacklist: set[constr(pattern=NSL_RE)] = Field( + default=set(), + description="Blacklist as `['NET.STA.LOC', ...]`", + ) stations: list[Station] = [] - blacklist: set[constr(pattern=NSL_RE)] = set() def model_post_init(self, __context: Any) -> None: loaded_stations = [] diff --git a/lassie/octree.py b/lassie/octree.py index 58a92386..ac8fa1de 100644 --- a/lassie/octree.py +++ b/lassie/octree.py @@ -180,13 +180,35 @@ def __hash__(self) -> int: class Octree(BaseModel): - 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) - north_bounds: tuple[float, float] = (-10 * KM, 10 * KM) - depth_bounds: tuple[float, float] = (0 * KM, 20 * KM) - absorbing_boundary: float = Field(default=1 * KM, ge=0.0) + location: Location = Field( + default=Location(lat=0.0, lon=0.0), + description="The reference location of the octree.", + ) + size_initial: PositiveFloat = Field( + default=2 * KM, + description="Initial size of a cubic octree node in meters.", + ) + size_limit: PositiveFloat = Field( + default=500.0, + description="Smallest possible size of an octree node in meters.", + ) + east_bounds: tuple[float, float] = Field( + default=(-10 * KM, 10 * KM), + description="East bounds of the octree in meters.", + ) + north_bounds: tuple[float, float] = Field( + default=(-10 * KM, 10 * KM), + description="North bounds of the octree in meters.", + ) + depth_bounds: tuple[float, float] = Field( + default=(0 * KM, 20 * KM), + description="Depth bounds of the octree in meters.", + ) + absorbing_boundary: float = Field( + default=1 * KM, + ge=0.0, + description="Absorbing boundary in meters. Detections inside the boundary will be tagged.", + ) _root_nodes: list[Node] = PrivateAttr([]) _cached_coordinates: dict[CoordSystem, np.ndarray] = PrivateAttr({}) diff --git a/lassie/station_corrections.py b/lassie/station_corrections.py index 621cac94..0616ffd6 100644 --- a/lassie/station_corrections.py +++ b/lassie/station_corrections.py @@ -315,12 +315,32 @@ class StationCorrections(BaseModel): default=None, description="Lassie rundir to calculate the corrections from.", ) - measure: Literal["median", "average"] = "median" - weighting: ArrivalWeighting = "mul-PhaseNet-semblance" + measure: Literal["median", "average"] = Field( + default="median", + description="Arithmetic measure for the traveltime delays. " + "Choose from `median` and `average`.", + ) + weighting: ArrivalWeighting = Field( + default="mul-PhaseNet-semblance", + description="Weighting of the traveltime delays. Choose from `none`, " + "`PhaseNet`, `semblance`, `add-PhaseNet-semblance`" + " and `mul-PhaseNet-semblance`.", + ) - minimum_num_picks: PositiveInt = 5 - minimum_distance_border: PositiveFloat = 2000.0 - minimum_depth: PositiveFloat = 3000.0 + minimum_num_picks: PositiveInt = Field( + default=5, + description="Minimum number of picks at a station required" + " to calculate station corrections.", + ) + minimum_distance_border: PositiveFloat = Field( + default=2000.0, + description="Minimum distance to the octree border " + "to be considered for correction.", + ) + minimum_depth: PositiveFloat = Field( + default=3000.0, + description="Minimum depth of the detection to be considered for correction.", + ) _station_corrections: dict[str, StationCorrection] = PrivateAttr({}) _traveltime_delay_cache: dict[tuple[NSL, PhaseDescription], float] = PrivateAttr({}) diff --git a/lassie/tracers/cake.py b/lassie/tracers/cake.py index c53b1304..278208fa 100644 --- a/lassie/tracers/cake.py +++ b/lassie/tracers/cake.py @@ -98,11 +98,11 @@ class EarthModel(BaseModel): ) format: Literal["nd", "hyposat"] = Field( default="nd", - description="Format of the velocity model. nd or hyposat is supported.", + description="Format of the velocity model. `nd` or `hyposat` is supported.", ) crust2_profile: constr(to_upper=True) | tuple[float, float] = Field( default="", - description="Crust2 profile name or a tuple of (lat, lon) coordinates.", + description="Crust2 profile name or a tuple of `(lat, lon)` coordinates.", ) raw_file_data: str | None = Field( @@ -495,18 +495,24 @@ def get_travel_time(self, source: Location, receiver: Location) -> float: class CakeTracer(RayTracer): tracer: Literal["CakeTracer"] = "CakeTracer" - phases: dict[PhaseDescription, Timing] = { - "cake:P": Timing(definition="P,p"), - "cake:S": Timing(definition="S,s"), - } - earthmodel: EarthModel = Field(default_factory=EarthModel) + phases: dict[PhaseDescription, Timing] = Field( + default={ + "cake:P": Timing(definition="P,p"), + "cake:S": Timing(definition="S,s"), + }, + description="Dictionary of phases and timings to calculate.", + ) + earthmodel: EarthModel = Field( + default_factory=EarthModel, + description="Earth model to calculate travel times for.", + ) trim_earth_model_depth: bool = Field( default=True, description="Trim earth model to max depth of the octree.", ) lut_cache_size: ByteSize = Field( default=2 * GiB, - description="Size of the LUT cache.", + description="Size of the LUT cache. Default is `2G`.", ) _traveltime_trees: dict[PhaseDescription, TravelTimeTree] = PrivateAttr({}) diff --git a/lassie/tracers/constant_velocity.py b/lassie/tracers/constant_velocity.py index e84b8392..1dc88175 100644 --- a/lassie/tracers/constant_velocity.py +++ b/lassie/tracers/constant_velocity.py @@ -3,7 +3,7 @@ from datetime import datetime, timedelta from typing import TYPE_CHECKING, Literal, Sequence -from pydantic import PositiveFloat +from pydantic import Field, PositiveFloat from lassie.tracers.base import ModelledArrival, RayTracer from lassie.utils import PhaseDescription, log_call @@ -23,8 +23,14 @@ class ConstantVelocityArrival(ModelledArrival): class ConstantVelocityTracer(RayTracer): tracer: Literal["ConstantVelocityTracer"] = "ConstantVelocityTracer" - phase: PhaseDescription = "constant:P" - velocity: PositiveFloat = 5000.0 + phase: PhaseDescription = Field( + default="constant:P", + description="Name of the phase.", + ) + velocity: PositiveFloat = Field( + default=5000.0, + description="Constant velocity of the phase in m/s.", + ) def get_available_phases(self) -> tuple[str, ...]: return (self.phase,) diff --git a/lassie/tracers/fast_marching/fast_marching.py b/lassie/tracers/fast_marching/fast_marching.py index 23c406fc..91434f35 100644 --- a/lassie/tracers/fast_marching/fast_marching.py +++ b/lassie/tracers/fast_marching/fast_marching.py @@ -274,19 +274,26 @@ class FastMarchingTracer(RayTracer): tracer: Literal["FastMarchingRayTracer"] = "FastMarchingRayTracer" phase: PhaseDescription = "fm:P" - interpolation_method: Literal["nearest", "linear", "cubic"] = "linear" + interpolation_method: Literal["nearest", "linear", "cubic"] = Field( + default="linear", + description="Interpolation method for travel times." + "Choose from `nearest`, `linear` or `cubic`.", + ) nthreads: int = Field( default=0, description="Number of threads to use for travel time." - "If set to 0, cpu_count*2 will be used.", + " If set to `0`, `cpu_count*2` will be used.", ) lut_cache_size: ByteSize = Field( default=2 * GiB, - description="Size of the LUT cache.", + description="Size of the LUT cache. Default is `2G`.", ) - velocity_model: VelocityModels = Constant3DVelocityModel() + velocity_model: VelocityModels = Field( + default=Constant3DVelocityModel(), + description="Velocity model for the ray tracer.", + ) _travel_time_volumes: dict[str, StationTravelTimeVolume] = PrivateAttr({}) _velocity_model: VelocityModel3D | None = PrivateAttr(None) diff --git a/lassie/utils.py b/lassie/utils.py index 88ca53b1..b3d9254e 100644 --- a/lassie/utils.py +++ b/lassie/utils.py @@ -7,7 +7,7 @@ from pathlib import Path from typing import TYPE_CHECKING, Annotated, Awaitable, Callable, ParamSpec, TypeVar -from pydantic import constr +from pydantic import BaseModel, constr from pyrocko.util import UnavailableDecimation from rich.logging import RichHandler @@ -105,3 +105,42 @@ def human_readable_bytes(size: int | float) -> str: def datetime_now() -> datetime: return datetime.now(tz=timezone.utc) + + +def generate_docs(model: BaseModel, exclude: dict | set | None = None) -> str: + """Takes model and dumps markdown for documentation""" + + def generate_submodel(model: BaseModel) -> list[str]: + lines = [] + for name, field in model.model_fields.items(): + if field.description is None: + continue + lines += [ + f" - **`{name}`** *`{field.annotation}`*\n", + f" {field.description}", + ] + return lines + + model_name = model.__class__.__name__ + lines = [f"### {model_name} Module"] + if model.__class__.__doc__ is not None: + lines += [f"{model.__class__.__doc__}\n"] + lines += [f'=== "Config {model_name}"'] + for name, field in model.model_fields.items(): + if field.description is None: + continue + lines += [ + f" - **`{name}`**\n", + f" {field.description}", + ] + + def dump_json() -> list[str]: + dump = model.model_dump_json(by_alias=False, indent=2, exclude=exclude) + lines = dump.split("\n") + return [f" {line}" for line in lines] + + lines += ['=== "JSON Block"'] + lines += [f" ```json title='JSON block for {model_name}'"] + lines.extend(dump_json()) + lines += [" ```"] + return "\n".join(lines) diff --git a/lassie/waveforms/squirrel.py b/lassie/waveforms/squirrel.py index c40c622e..265caf53 100644 --- a/lassie/waveforms/squirrel.py +++ b/lassie/waveforms/squirrel.py @@ -81,17 +81,42 @@ def filter_freqs(batch: Batch) -> Batch: class PyrockoSquirrel(WaveformProvider): - provider: Literal["PyrockoSquirrel"] = "PyrockoSquirrel" - - environment: Path = Path(".") - waveform_dirs: list[Path] = [] - start_time: AwareDatetime | None = None - end_time: AwareDatetime | None = None + """Waveform provider using Pyrocko's Squirrel.""" - highpass: PositiveFloat | None = None - lowpass: PositiveFloat | None = None + provider: Literal["PyrockoSquirrel"] = "PyrockoSquirrel" - channel_selector: str = Field(default="*", max_length=3) + environment: Path = Field( + default=Path("."), + description="Path to Squirrel environment.", + ) + waveform_dirs: list[Path] = Field( + default=[], + description="List of directories holding the waveform files.", + ) + start_time: AwareDatetime | None = Field( + default=None, + description="Start time for the search.", + ) + end_time: AwareDatetime | None = Field( + default=None, + description="End time for the search.", + ) + + highpass: PositiveFloat | None = Field( + default=None, + description="Highpass filter, corner frequency in Hz.", + ) + lowpass: PositiveFloat | None = Field( + default=None, + description="Lowpass filter, corner frequency in Hz.", + ) + + channel_selector: str = Field( + default="*", + max_length=3, + description="Channel selector for Pyrocko's Squirrel, " + "use e.g. `EN?` for selection.", + ) async_prefetch_batches: PositiveInt = 4 _squirrel: Squirrel | None = PrivateAttr(None) diff --git a/mkdocs.yml b/mkdocs.yml index 97942552..558cb69b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -35,14 +35,14 @@ markdown_extensions: anchor_linenums: true line_spans: __span pygments_lang_class: true - - pymdownx.inlinehilite - pymdownx.snippets - pymdownx.superfences - pymdownx.details + - pymdownx.tasklist - pymdownx.arithmatex: generic: true - - pymdownx.tasklist - - md_in_html + - pymdownx.tabbed: + alternate_style: true - admonition - attr_list @@ -58,15 +58,7 @@ 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 + - markdown-exec nav: - Earthquake Detector: @@ -74,10 +66,9 @@ nav: - Getting Started: getting_started.md - Visualising Detections: visualizing_results.md - Usage: + - Configuration: components/configuration.md - Seismic Data: components/seismic_data.md - Ray Tracer: components/ray_tracer.md - Image Function: components/image_function.md - Octree: components/octree.md - Station Corrections: components/station_corrections.md - - CLI: - - Interface: cli.md