Skip to content

Fix Simpeg3DData tipper receivers, tipper sign, and receiver orientations - #109

Open
jcapriot wants to merge 3 commits into
MTgeophysics:mainfrom
jcapriot:fix/simpeg-3d-tipper
Open

jcapriot wants to merge 3 commits into
MTgeophysics:mainfrom
jcapriot:fix/simpeg-3d-tipper

Conversation

@jcapriot

@jcapriot jcapriot commented Sep 30, 2026 •

Copy link
Copy Markdown

Description

Three fixes to mtpy/modeling/simpeg/data_3d.py (Simpeg3DData), which converts mtpy components (x = north, y = east, z = down) to SimPEG's (x = east, y = north, z = up), and one fix to pin the SimPEG minimum version:

  1. Tipper receivers use nsem.receivers.Tipper. Point3DTipper was removed in current SimPEG (0.25): constructing one raises NotImplementedError: Point3DTipper has been removed, please use Tipper. As a result, Simpeg3DData.survey failed for any data containing tipper. Tipper is available in every SimPEG version that has the Impedance receiver this module already uses.

  2. The tipper changes sign when converted to SimPEG axes. Swapping x and y only reorders the impedance indices, because impedance is a ratio of two horizontal fields. The tipper is H_z / H_horizontal, and H_z changes sign when z flips from down to up. The correct mapping is therefore:

    • SimPEG tzx = −mtpy t_zy
    • SimPEG tzy = −mtpy t_zx

    component_map already swapped the indices but kept the sign. to_rec_array now negates t_zx and t_zy on its own copy of the dataframe, so the caller's dataframe is unchanged.

  3. Survey receivers use SimPEG's orientation, not mtpy's. _sources_list iterates over mtpy component names, but each source_* property built a receiver with mtpy's orientation. For example, source_z_xy built a SimPEG "xy" receiver, which corresponds to mtpy z_yx, while to_rec_array labels z_xy's data "zyx". With all four impedance elements selected the two sets happen to coincide. With a subset (e.g. invert_z_xy=True and the others False), the survey measured a different element from the one the data described. Orientations now come from component_map.

  4. standard_deviations() returns its dataframe. It built the <component>_model_error frame (z_xx … t_zy) and returned None. The columns keep mtpy's names: each is the standard deviation of a complex element, which the index swap and tipper sign change don't affect.

  5. Pins simpeg>=0.23.0. 0.23.0 is the first release with the Impedance and Tipper receivers that data_3d.py, data_2d.py and the 1D inversion recipe use. simpeg was unpinned in pyproject.toml and environment.yml, so on older installs these modules failed with an AttributeError.

Motivation and Context

Running a SimPEG 3D forward from MTData.to_simpeg_3d() with tipper failed on SimPEG 0.25 because of (1). Working around that exposed (2): simulated tipper came out with the opposite sign to the observed EDI tipper.

The sign was checked against SimPEG directly. With a conductor to the east of a station, SimPEG's Re(tzx) is positive. That means mtpy's Re(t_zy) is negative, which is the usual convention: the real induction arrow points away from the conductor.

How Has This Been Tested?

New tests/modeling/simpeg/test_simpeg_3d_conventions.py, 11 tests that need no mtpy_data files:

  • Tipper receivers are Tipper instances; this fails with Point3DTipper on SimPEG 0.25.
  • For each single component, the survey's receiver type and orientation match the element to_rec_array labels its data with.
  • to_rec_array swaps the impedance indices and negates the tipper.
  • to_rec_array leaves the caller's dataframe unchanged.
  • get_simpeg_data_object() carries the negated tipper.
  • A small SimPEG Simulation3DPrimarySecondary with a conductor to the east confirms Re(t_zy) < 0 in mtpy's convention (about 7 s).

Reverting each fix makes at least one of these tests fail.

pytest tests/modeling/simpeg: 18 passed, 3 skipped (the skips need mtpy_data, which I didn't have installed). Environment: Python 3.12, SimPEG 0.25.2 (minimum now pinned at 0.23.0), macOS arm64.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Note: the tipper values to_rec_array / get_simpeg_data_object return change sign. That's the fix, but any downstream code that compensated for the old sign will need to drop its workaround.

Checklist:

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have added tests to cover my changes.
  • All new and existing tests passed.

Not addressed here: from_simpeg_data_object is still NotImplementedError.

jcapriot and others added 3 commits September 30, 2026 10:11
…G axes

Simpeg3DData converts mtpy (EDI) components, x = north, y = east, z = down,
to SimPEG's x = east, y = north, z = up. Three things were wrong:

- Tipper receivers used nsem.receivers.Point3DTipper, which current SimPEG
  (0.25) removes: constructing one raises NotImplementedError, so `survey`
  failed for any data carrying tipper. Use nsem.receivers.Tipper.
- The tipper kept its sign. Swapping x and y only reorders the indices of
  the impedance, a ratio of two horizontal fields, but the tipper is
  H_z / H_h and H_z changes sign with z: SimPEG tzx = -mtpy t_zy and
  tzy = -mtpy t_zx. to_rec_array now negates t_zx and t_zy (on its copy of
  the dataframe). Checked against a SimPEG simulation: with a conductor to
  the east, SimPEG's Re(tzx) is positive, so mtpy's Re(t_zy) is negative,
  the real induction arrow pointing away from the conductor.
- The survey's receivers used mtpy's orientation as SimPEG's: `source_z_xy`
  built a SimPEG "xy" receiver, which is mtpy z_yx, while to_rec_array
  labels z_xy's data "zyx". With all four elements the sets coincided;
  inverting a subset gave the wrong receivers. Orientations now come from
  component_map.

Adds tests that need no data files: receiver types and orientations per
component, the rec array's swap and sign, the untouched dataframe, and a
small SimPEG simulation pinning the tipper sign.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
0.23.0 is the first release with nsem.receivers.Impedance and Tipper, which
data_3d.py, data_2d.py and the 1D inversion recipe use. simpeg was unpinned,
so an older install failed with an AttributeError at the first receiver.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tions

It built the `<component>_model_error` frame and then returned None. The
columns stay in mtpy's names: each is the standard deviation of a complex
element, which the index swap and the tipper's sign change leave alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant