Skip to content

Feature/auto fm inversion#176

Closed
comoglu wants to merge 4 commits intoSeisComP:mainfrom
comoglu:feature/auto-fm-inversion
Closed

Feature/auto fm inversion#176
comoglu wants to merge 4 commits intoSeisComP:mainfrom
comoglu:feature/auto-fm-inversion

Conversation

@comoglu
Copy link
Contributor

@comoglu comoglu commented Feb 12, 2026

Summary

  • Grid search focal mechanism inversion for P-wave first motion polarities in the scolv FM plot
  • Follows the HASH method (Hardebeck & Shearer, 2002, BSSA 92, 2264-2276) with direct takeoff/azimuth perturbation as validated by SKHASH (Skoumal, Hardebeck & Shearer, 2024, SRL 95, 2519-2526)
  • Preferred solution computed by averaging acceptable mechanisms with iterative outlier removal (HASH MECH_PROB algorithm)
  • Quality grades use the four HASH criteria: probability, RMS angular difference, weighted misfit, and STDR

What's included

  • New library: seiscomp/seismology/firstmotion.h/.cpp -- standalone inversion with no GUI dependencies
  • scolv integration: Auto button ("A") and context menu on the FM plot
  • Populates FocalMechanism fields (stationPolarityCount, misfit, stationDistributionRatio, azimuthalGap) on commit
  • Comprehensive SEISCOMP_DEBUG/INFO logging for --debug diagnostics

Implementation notes

  • Since scolv receives pre-computed takeoff angles from the locator (no access to velocity models), perturbation is applied directly to takeoff angles and azimuths rather than to source depth/velocity model. This is the approach used by SKHASH Driver 5 for pre-computed angles.
  • Minimum 6 polarity observations required
  • Configurable: grid spacing (5 deg), number of trials (50), takeoff uncertainty (10 deg), azimuth uncertainty (5 deg), bad fraction (0.1)

References

  • Hardebeck, J.L. and Shearer, P.M. (2002). A new method for determining first-motion focal mechanisms. Bull. Seismol. Soc. Am. 92, 2264-2276.
  • Skoumal, R.J., Hardebeck, J.L. and Shearer, P.M. (2024). SKHASH: A Python package for computing earthquake focal mechanisms. Seismol. Res. Lett. 95, 2519-2526.

HASH-style (Hardebeck & Shearer 2002) grid search inversion for P-wave
first motion polarities in the scolv focal mechanism plot.

Features:
- Grid search over strike/dip/rake with perturbation stability trials
- Auto button ("A") and context menu entry on FM plot
- Quality assessment: HASH letter grades (A/B/C/D), misfit, STDR,
  azimuthal gap, misfitting station identification
- Populates FocalMechanism fields (stationPolarityCount, misfit,
  stationDistributionRatio, azimuthalGap) on commit
- Comprehensive SEISCOMP_DEBUG/INFO logging for --debug diagnostics
- Azimuthal gap warning when > 180 degrees

Minimum 6 polarity observations with valid takeoff angles required.
@cla-bot cla-bot bot added the cla-signed The CLA has been signed by all contributors label Feb 12, 2026
@gempa-jabe
Copy link
Contributor

Thank you, we will be looking into it. What I can tell you from the first glance is that the code is looking pretty good and aligned with our coding style, well done! I will continue to comment in the code.

@comoglu comoglu force-pushed the feature/auto-fm-inversion branch 2 times, most recently from 0fa1cc6 to 55d74f1 Compare February 12, 2026 10:46
Copy link
Contributor

@gempa-stephan gempa-stephan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your efforts. SeisComP code may now be written for the C++17 coding standard. These are some Clang-Tidy remarks. I flagged each recommendation type only once.

@comoglu comoglu force-pushed the feature/auto-fm-inversion branch from 55d74f1 to e52c49c Compare February 12, 2026 12:15
@comoglu
Copy link
Contributor Author

comoglu commented Feb 12, 2026

Bugs are now fixed.

Fix three bugs in the first motion polarity inversion:

1. Fix double deg-to-rad conversion on np2nd input: np2nd() expects
   degrees and converts internally, but we were converting to radians
   before calling it. This caused all radiation pattern vectors to be
   computed from near-zero angles, making every mechanism appear
   equivalent (4.7M accepted mechanisms instead of thousands).

2. Fix double rad-to-deg conversion on nd2dc output: nd2dc() returns
   degrees (calls np2deg internally), but we applied rad2deg() again,
   producing garbage NP2 values (e.g. 5335.8/5156.6/-5204.1).

3. Fix MECH_AVG sign alignment reference: use a fixed reference (first
   mechanism) for sign alignment following HASH MECH_AVG (uncert_subs.f),
   instead of the running sum. The running sum becomes dominated by
   np2nd's hemisphere convention (n.z always <= 0), systematically
   collapsing the averaged solution to a degenerate near-horizontal dip.

Also improve MECH_PROB outlier removal performance: use batch removal
(discard all mechanisms beyond cutoff, recompute average, repeat until
stable) instead of one-at-a-time removal. This reduces complexity from
O(n^2) to O(n*k) where k is typically 2-6 iterations, fixing a hang
with large numbers of accepted mechanisms.
@comoglu comoglu force-pushed the feature/auto-fm-inversion branch from e52c49c to d135752 Compare February 12, 2026 14:40
@gempa-jabe
Copy link
Contributor

We have tested the code with an example event and the solution did not quite match our expectation. We can add an example if you would like to take a look, but can you provide some test results from your end?

There is currently no option to configure the inversion apart from modifying the source code. Do you have any ideas in that regard?

@comoglu
Copy link
Contributor Author

comoglu commented Feb 13, 2026

I have got a few good result with Alaskan event when I have really clear and quite number of polarities. I will work on it more. This is the best I have got.
image

@comoglu
Copy link
Contributor Author

comoglu commented Feb 13, 2026

Yeah, test results would be great.

@gempa-jabe
Copy link
Contributor

We haven't conducted any systematic tests, just a quick scolv test. Anyway, the algorithm should work correctly and we need unit tests to prove that. Do you have something available in that regard?

Here an example which does not meet our expectation:

image

@gempa-jabe
Copy link
Contributor

The FM seems to be inverted ...

@comoglu
Copy link
Contributor Author

comoglu commented Feb 13, 2026 via email

@gempa-jabe
Copy link
Contributor

Exactly, this is much better. So there seems to be a rotation somewhere? @gempa-lukas reported that the original SKHASH code produces similar issues.

@comoglu
Copy link
Contributor Author

comoglu commented Feb 13, 2026

Yes, thank you for the confirmation for SKHASH. Also correct there is an issue with the rotation. I am looking into it.

@gempa-lukas
Copy link
Contributor

gempa-lukas commented Feb 13, 2026

Last year I wrote a python script that can be used as external script in scolv. It simply dumps the origins (which include the takeoffangles, which is still a hack) and starts the SKHASH python script. The results seemed to be fitting quite good.
Therefore I can compare directly both versions (yours and my python-wrapper), although the input/config parameters might not be the same.

One thing I recently struggled with is the SKHASH parameter:

$flip_takeoff  # Handles flipped takeoffs such that [180: upgoing and 0: downgoing]
True

This must be true for SC.

- Implement full HASH MECH_ROT algorithm (Hardebeck & Shearer 2002)
  for mechanism rotation, replacing simplified average-of-angles approach
- Fix takeoff angle convention: rz=cos(ih) for NED z-Down coordinate
  system (was incorrectly using -cos from HASH z-UP convention)
- Add 21 unit tests covering mechanismRotation, predictPolarity,
  azimuthal gap, quality grading, and full inversion recovery
- Add FM inversion configuration parameters to OriginLocatorView:
  gridSpacing, numTrials, takeoffUncertainty, azimuthUncertainty,
  maxBadFraction
- Make mechanismRotation() part of the public API
@comoglu
Copy link
Contributor Author

comoglu commented Feb 13, 2026

I made some improvement, I believe. Would you mind testing the one you shared earlier?

@gempa-jabe
Copy link
Contributor

Yes, we will do. Thanks.

@gempa-lukas
Copy link
Contributor

gempa-lukas commented Feb 13, 2026

Although the mechanism of my previous event looks better now:
Screenshot From 2026-02-13 13-08-53

My SKHASH-wrapper script produces the following result:
Screenshot From 2026-02-13 13-10-11

I tested also other events which do not fit at all. It seems like there is always almost the same mechnism (only strike changes significantly) coming from your code:
Screenshot From 2026-02-13 13-06-20

Screenshot From 2026-02-13 13-03-16

@comoglu
Copy link
Contributor Author

comoglu commented Feb 13, 2026

Thanks for testing. As far as I know, the first motion method is not reliable for distant events at all. we would rather use Moment Tensor.

np2nd() returns normal and slip vectors in Aki & Richards convention
(z-UP), identical to HASH's TO_CAR subroutine. The ray direction must
use the same coordinate system: rz = -cos(ih), not +cos(ih).

The previous change to rz=cos(ih) mixed z-DOWN rays with z-UP mechanism
vectors, producing wrong polarities for ~50% of non-vertical mechanisms.
This caused the inversion to converge to the same mechanism regardless
of input data (only strike changed, dip/rake were always ~65/170).

Add polarity tests with oblique (strike=45/dip=60/rake=30) and normal
fault mechanisms that verify against hand-computed HASH TO_CAR radiation
patterns. These tests fail with the wrong rz sign.
@comoglu
Copy link
Contributor Author

comoglu commented Feb 13, 2026

I will add TX network as well. let me test. There is a new version already. would you mind testing with a local or regional event

@gempa-lukas
Copy link
Contributor

gempa-lukas commented Feb 13, 2026

Yes, first motions might not be that reliable for teleseismic events. Still if the polarities have been picked, the algorithm should be able to match the FM accordingly to the polarities/takeoffangle/azimuth etc.. That behavior should be independent of the type of EQ.

Nevertheless, I testet the newest version. Now it reverted back to the previous state, where the mechanism seemed to be flipped. And most of the times one nodal plane has a steep dip (>80°).

@comoglu
Copy link
Contributor Author

comoglu commented Feb 13, 2026

Closing this PR. After extensive testing and debugging, the C++ HASH reimplementation still has persistent issues with coordinate system conventions (takeoff angle / ray direction vs. np2nd's Aki & Richards z-UP system), leading to unreliable results on real data — specifically flipped mechanisms and a systematic bias toward steep dip (>80°), as @gempa-lukas pointed out.

Given that the SKHASH Python wrapper already exists and produces correct results, it makes more sense to use that for now. I may revisit this with a simpler, custom approach later. The original goal was to provide a native C++ solution accessible to the broader SeisComP community without external dependencies.

@comoglu comoglu closed this Feb 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed The CLA has been signed by all contributors

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants