From c4b2750f5590aeb11f27d7f15cee904ab1cde03c Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 10 May 2024 11:33:30 -0700 Subject: [PATCH 001/194] Replace SimPEG for simpeg in API reference (#1446) Rename rst files in `docs/content/api` and replace `SimPEG` for `simpeg` in `docs/content/api/index.rst`. Fixes #1437 --- docs/content/api/SimPEG.directives.rst | 1 - .../api/SimPEG.electromagnetics.base.rst | 1 - ...mPEG.electromagnetics.frequency_domain.rst | 1 - ...SimPEG.electromagnetics.natural_source.rst | 1 - docs/content/api/SimPEG.electromagnetics.rst | 27 ------------------- ...omagnetics.static.induced_polarization.rst | 1 - ...EG.electromagnetics.static.resistivity.rst | 1 - ...electromagnetics.static.self_potential.rst | 1 - ...s.static.spectral_induced_polarization.rst | 1 - ...magnetics.static.spontaneous_potential.rst | 1 - .../SimPEG.electromagnetics.static.utils.rst | 1 - .../SimPEG.electromagnetics.time_domain.rst | 1 - .../api/SimPEG.electromagnetics.utils.rst | 1 - ...gnetics.viscous_remanent_magnetization.rst | 1 - docs/content/api/SimPEG.flow.richards.rst | 1 - docs/content/api/SimPEG.meta.rst | 1 - .../api/SimPEG.potential_fields.base.rst | 1 - .../api/SimPEG.potential_fields.gravity.rst | 1 - .../api/SimPEG.potential_fields.magnetics.rst | 1 - docs/content/api/SimPEG.regularization.rst | 1 - ...SimPEG.seismic.straight_ray_tomography.rst | 1 - docs/content/api/SimPEG.utils.rst | 1 - docs/content/api/index.rst | 18 ++++++------- docs/content/api/simpeg.directives.rst | 1 + .../api/simpeg.electromagnetics.base.rst | 1 + ...mpeg.electromagnetics.frequency_domain.rst | 1 + ...simpeg.electromagnetics.natural_source.rst | 1 + docs/content/api/simpeg.electromagnetics.rst | 27 +++++++++++++++++++ ...omagnetics.static.induced_polarization.rst | 1 + ...eg.electromagnetics.static.resistivity.rst | 1 + ...electromagnetics.static.self_potential.rst | 1 + ...s.static.spectral_induced_polarization.rst | 1 + ...magnetics.static.spontaneous_potential.rst | 1 + .../simpeg.electromagnetics.static.utils.rst | 1 + .../simpeg.electromagnetics.time_domain.rst | 1 + .../api/simpeg.electromagnetics.utils.rst | 1 + ...gnetics.viscous_remanent_magnetization.rst | 1 + docs/content/api/simpeg.flow.richards.rst | 1 + .../api/{SimPEG.flow.rst => simpeg.flow.rst} | 2 +- docs/content/api/simpeg.meta.rst | 1 + .../api/simpeg.potential_fields.base.rst | 1 + .../api/simpeg.potential_fields.gravity.rst | 1 + .../api/simpeg.potential_fields.magnetics.rst | 1 + ...fields.rst => simpeg.potential_fields.rst} | 6 ++--- docs/content/api/simpeg.regularization.rst | 1 + docs/content/api/{SimPEG.rst => simpeg.rst} | 0 ...{SimPEG.seismic.rst => simpeg.seismic.rst} | 2 +- ...simpeg.seismic.straight_ray_tomography.rst | 1 + docs/content/api/simpeg.utils.rst | 1 + 49 files changed, 62 insertions(+), 62 deletions(-) delete mode 100644 docs/content/api/SimPEG.directives.rst delete mode 100644 docs/content/api/SimPEG.electromagnetics.base.rst delete mode 100644 docs/content/api/SimPEG.electromagnetics.frequency_domain.rst delete mode 100644 docs/content/api/SimPEG.electromagnetics.natural_source.rst delete mode 100644 docs/content/api/SimPEG.electromagnetics.rst delete mode 100644 docs/content/api/SimPEG.electromagnetics.static.induced_polarization.rst delete mode 100644 docs/content/api/SimPEG.electromagnetics.static.resistivity.rst delete mode 100644 docs/content/api/SimPEG.electromagnetics.static.self_potential.rst delete mode 100644 docs/content/api/SimPEG.electromagnetics.static.spectral_induced_polarization.rst delete mode 100644 docs/content/api/SimPEG.electromagnetics.static.spontaneous_potential.rst delete mode 100644 docs/content/api/SimPEG.electromagnetics.static.utils.rst delete mode 100644 docs/content/api/SimPEG.electromagnetics.time_domain.rst delete mode 100644 docs/content/api/SimPEG.electromagnetics.utils.rst delete mode 100644 docs/content/api/SimPEG.electromagnetics.viscous_remanent_magnetization.rst delete mode 100644 docs/content/api/SimPEG.flow.richards.rst delete mode 100644 docs/content/api/SimPEG.meta.rst delete mode 100644 docs/content/api/SimPEG.potential_fields.base.rst delete mode 100644 docs/content/api/SimPEG.potential_fields.gravity.rst delete mode 100644 docs/content/api/SimPEG.potential_fields.magnetics.rst delete mode 100644 docs/content/api/SimPEG.regularization.rst delete mode 100644 docs/content/api/SimPEG.seismic.straight_ray_tomography.rst delete mode 100644 docs/content/api/SimPEG.utils.rst create mode 100644 docs/content/api/simpeg.directives.rst create mode 100644 docs/content/api/simpeg.electromagnetics.base.rst create mode 100644 docs/content/api/simpeg.electromagnetics.frequency_domain.rst create mode 100644 docs/content/api/simpeg.electromagnetics.natural_source.rst create mode 100644 docs/content/api/simpeg.electromagnetics.rst create mode 100644 docs/content/api/simpeg.electromagnetics.static.induced_polarization.rst create mode 100644 docs/content/api/simpeg.electromagnetics.static.resistivity.rst create mode 100644 docs/content/api/simpeg.electromagnetics.static.self_potential.rst create mode 100644 docs/content/api/simpeg.electromagnetics.static.spectral_induced_polarization.rst create mode 100644 docs/content/api/simpeg.electromagnetics.static.spontaneous_potential.rst create mode 100644 docs/content/api/simpeg.electromagnetics.static.utils.rst create mode 100644 docs/content/api/simpeg.electromagnetics.time_domain.rst create mode 100644 docs/content/api/simpeg.electromagnetics.utils.rst create mode 100644 docs/content/api/simpeg.electromagnetics.viscous_remanent_magnetization.rst create mode 100644 docs/content/api/simpeg.flow.richards.rst rename docs/content/api/{SimPEG.flow.rst => simpeg.flow.rst} (84%) create mode 100644 docs/content/api/simpeg.meta.rst create mode 100644 docs/content/api/simpeg.potential_fields.base.rst create mode 100644 docs/content/api/simpeg.potential_fields.gravity.rst create mode 100644 docs/content/api/simpeg.potential_fields.magnetics.rst rename docs/content/api/{SimPEG.potential_fields.rst => simpeg.potential_fields.rst} (63%) create mode 100644 docs/content/api/simpeg.regularization.rst rename docs/content/api/{SimPEG.rst => simpeg.rst} (100%) rename docs/content/api/{SimPEG.seismic.rst => simpeg.seismic.rst} (75%) create mode 100644 docs/content/api/simpeg.seismic.straight_ray_tomography.rst create mode 100644 docs/content/api/simpeg.utils.rst diff --git a/docs/content/api/SimPEG.directives.rst b/docs/content/api/SimPEG.directives.rst deleted file mode 100644 index 35999d49d0..0000000000 --- a/docs/content/api/SimPEG.directives.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.directives diff --git a/docs/content/api/SimPEG.electromagnetics.base.rst b/docs/content/api/SimPEG.electromagnetics.base.rst deleted file mode 100644 index c32e9227a1..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.base.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.electromagnetics \ No newline at end of file diff --git a/docs/content/api/SimPEG.electromagnetics.frequency_domain.rst b/docs/content/api/SimPEG.electromagnetics.frequency_domain.rst deleted file mode 100644 index dc5de2199b..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.frequency_domain.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.electromagnetics.frequency_domain \ No newline at end of file diff --git a/docs/content/api/SimPEG.electromagnetics.natural_source.rst b/docs/content/api/SimPEG.electromagnetics.natural_source.rst deleted file mode 100644 index 5e276c525b..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.natural_source.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.electromagnetics.natural_source \ No newline at end of file diff --git a/docs/content/api/SimPEG.electromagnetics.rst b/docs/content/api/SimPEG.electromagnetics.rst deleted file mode 100644 index b465abd441..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.rst +++ /dev/null @@ -1,27 +0,0 @@ -========================= -Electromagnetics -========================= - -Things about electromagnetics - -.. toctree:: - :maxdepth: 2 - - SimPEG.electromagnetics.static.induced_polarization - SimPEG.electromagnetics.static.resistivity - SimPEG.electromagnetics.static.spectral_induced_polarization - SimPEG.electromagnetics.static.self_potential - SimPEG.electromagnetics.frequency_domain - SimPEG.electromagnetics.natural_source - SimPEG.electromagnetics.time_domain - SimPEG.electromagnetics.viscous_remanent_magnetization - -Electromagnetics Utilities --------------------------- - -.. toctree:: - :maxdepth: 2 - - SimPEG.electromagnetics.static.utils - SimPEG.electromagnetics.utils - SimPEG.electromagnetics.base diff --git a/docs/content/api/SimPEG.electromagnetics.static.induced_polarization.rst b/docs/content/api/SimPEG.electromagnetics.static.induced_polarization.rst deleted file mode 100644 index 94b7fdedd8..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.static.induced_polarization.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.electromagnetics.static.induced_polarization \ No newline at end of file diff --git a/docs/content/api/SimPEG.electromagnetics.static.resistivity.rst b/docs/content/api/SimPEG.electromagnetics.static.resistivity.rst deleted file mode 100644 index f93b976667..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.static.resistivity.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.electromagnetics.static.resistivity diff --git a/docs/content/api/SimPEG.electromagnetics.static.self_potential.rst b/docs/content/api/SimPEG.electromagnetics.static.self_potential.rst deleted file mode 100644 index ae8d5f2859..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.static.self_potential.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.electromagnetics.static.self_potential diff --git a/docs/content/api/SimPEG.electromagnetics.static.spectral_induced_polarization.rst b/docs/content/api/SimPEG.electromagnetics.static.spectral_induced_polarization.rst deleted file mode 100644 index c02a3ec010..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.static.spectral_induced_polarization.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.electromagnetics.static.spectral_induced_polarization \ No newline at end of file diff --git a/docs/content/api/SimPEG.electromagnetics.static.spontaneous_potential.rst b/docs/content/api/SimPEG.electromagnetics.static.spontaneous_potential.rst deleted file mode 100644 index d5d02e8ff2..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.static.spontaneous_potential.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.electromagnetics.static.spontaneous_potential diff --git a/docs/content/api/SimPEG.electromagnetics.static.utils.rst b/docs/content/api/SimPEG.electromagnetics.static.utils.rst deleted file mode 100644 index 0cb346c648..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.static.utils.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.electromagnetics.static.utils \ No newline at end of file diff --git a/docs/content/api/SimPEG.electromagnetics.time_domain.rst b/docs/content/api/SimPEG.electromagnetics.time_domain.rst deleted file mode 100644 index d93f52fce3..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.time_domain.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.electromagnetics.time_domain \ No newline at end of file diff --git a/docs/content/api/SimPEG.electromagnetics.utils.rst b/docs/content/api/SimPEG.electromagnetics.utils.rst deleted file mode 100644 index eef7ebc5c5..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.utils.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.electromagnetics.utils \ No newline at end of file diff --git a/docs/content/api/SimPEG.electromagnetics.viscous_remanent_magnetization.rst b/docs/content/api/SimPEG.electromagnetics.viscous_remanent_magnetization.rst deleted file mode 100644 index d9cf10e232..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.viscous_remanent_magnetization.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.electromagnetics.viscous_remanent_magnetization \ No newline at end of file diff --git a/docs/content/api/SimPEG.flow.richards.rst b/docs/content/api/SimPEG.flow.richards.rst deleted file mode 100644 index 9367f1d62a..0000000000 --- a/docs/content/api/SimPEG.flow.richards.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.flow.richards diff --git a/docs/content/api/SimPEG.meta.rst b/docs/content/api/SimPEG.meta.rst deleted file mode 100644 index 469456456c..0000000000 --- a/docs/content/api/SimPEG.meta.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.meta \ No newline at end of file diff --git a/docs/content/api/SimPEG.potential_fields.base.rst b/docs/content/api/SimPEG.potential_fields.base.rst deleted file mode 100644 index aba910d082..0000000000 --- a/docs/content/api/SimPEG.potential_fields.base.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.potential_fields diff --git a/docs/content/api/SimPEG.potential_fields.gravity.rst b/docs/content/api/SimPEG.potential_fields.gravity.rst deleted file mode 100644 index 4fd6dec3f3..0000000000 --- a/docs/content/api/SimPEG.potential_fields.gravity.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.potential_fields.gravity diff --git a/docs/content/api/SimPEG.potential_fields.magnetics.rst b/docs/content/api/SimPEG.potential_fields.magnetics.rst deleted file mode 100644 index c7dfb47af0..0000000000 --- a/docs/content/api/SimPEG.potential_fields.magnetics.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.potential_fields.magnetics diff --git a/docs/content/api/SimPEG.regularization.rst b/docs/content/api/SimPEG.regularization.rst deleted file mode 100644 index 35fb57ad5a..0000000000 --- a/docs/content/api/SimPEG.regularization.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.regularization diff --git a/docs/content/api/SimPEG.seismic.straight_ray_tomography.rst b/docs/content/api/SimPEG.seismic.straight_ray_tomography.rst deleted file mode 100644 index 9590370726..0000000000 --- a/docs/content/api/SimPEG.seismic.straight_ray_tomography.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.seismic.straight_ray_tomography diff --git a/docs/content/api/SimPEG.utils.rst b/docs/content/api/SimPEG.utils.rst deleted file mode 100644 index 7791aa3277..0000000000 --- a/docs/content/api/SimPEG.utils.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.utils diff --git a/docs/content/api/index.rst b/docs/content/api/index.rst index 8ffe60ffa9..a6f8a0d0b0 100644 --- a/docs/content/api/index.rst +++ b/docs/content/api/index.rst @@ -10,10 +10,10 @@ Geophysical Simulation Modules .. toctree:: :maxdepth: 2 - SimPEG.potential_fields - SimPEG.electromagnetics - SimPEG.flow - SimPEG.seismic + simpeg.potential_fields + simpeg.electromagnetics + simpeg.flow + simpeg.seismic SimPEG Building Blocks ====================== @@ -23,21 +23,21 @@ Base SimPEG .. toctree:: :maxdepth: 3 - SimPEG + simpeg Regularizations --------------- .. toctree:: :maxdepth: 2 - SimPEG.regularization + simpeg.regularization Directives ---------- .. toctree:: :maxdepth: 2 - SimPEG.directives + simpeg.directives Utilities --------- @@ -47,7 +47,7 @@ Classes and functions for performing useful operations. .. toctree:: :maxdepth: 2 - SimPEG.utils + simpeg.utils Meta ---- @@ -56,4 +56,4 @@ Classes for encapsulating many simulations. .. toctree:: :maxdepth: 2 - SimPEG.meta + simpeg.meta diff --git a/docs/content/api/simpeg.directives.rst b/docs/content/api/simpeg.directives.rst new file mode 100644 index 0000000000..b6c05c89d2 --- /dev/null +++ b/docs/content/api/simpeg.directives.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.directives diff --git a/docs/content/api/simpeg.electromagnetics.base.rst b/docs/content/api/simpeg.electromagnetics.base.rst new file mode 100644 index 0000000000..fb103a8f43 --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.base.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.electromagnetics \ No newline at end of file diff --git a/docs/content/api/simpeg.electromagnetics.frequency_domain.rst b/docs/content/api/simpeg.electromagnetics.frequency_domain.rst new file mode 100644 index 0000000000..c3a9a071af --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.frequency_domain.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.electromagnetics.frequency_domain \ No newline at end of file diff --git a/docs/content/api/simpeg.electromagnetics.natural_source.rst b/docs/content/api/simpeg.electromagnetics.natural_source.rst new file mode 100644 index 0000000000..cf9c7c669a --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.natural_source.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.electromagnetics.natural_source \ No newline at end of file diff --git a/docs/content/api/simpeg.electromagnetics.rst b/docs/content/api/simpeg.electromagnetics.rst new file mode 100644 index 0000000000..eb04516321 --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.rst @@ -0,0 +1,27 @@ +========================= +Electromagnetics +========================= + +Things about electromagnetics + +.. toctree:: + :maxdepth: 2 + + simpeg.electromagnetics.static.induced_polarization + simpeg.electromagnetics.static.resistivity + simpeg.electromagnetics.static.spectral_induced_polarization + simpeg.electromagnetics.static.self_potential + simpeg.electromagnetics.frequency_domain + simpeg.electromagnetics.natural_source + simpeg.electromagnetics.time_domain + simpeg.electromagnetics.viscous_remanent_magnetization + +Electromagnetics Utilities +-------------------------- + +.. toctree:: + :maxdepth: 2 + + simpeg.electromagnetics.static.utils + simpeg.electromagnetics.utils + simpeg.electromagnetics.base diff --git a/docs/content/api/simpeg.electromagnetics.static.induced_polarization.rst b/docs/content/api/simpeg.electromagnetics.static.induced_polarization.rst new file mode 100644 index 0000000000..8c29897f92 --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.static.induced_polarization.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.electromagnetics.static.induced_polarization \ No newline at end of file diff --git a/docs/content/api/simpeg.electromagnetics.static.resistivity.rst b/docs/content/api/simpeg.electromagnetics.static.resistivity.rst new file mode 100644 index 0000000000..1ad60928fe --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.static.resistivity.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.electromagnetics.static.resistivity diff --git a/docs/content/api/simpeg.electromagnetics.static.self_potential.rst b/docs/content/api/simpeg.electromagnetics.static.self_potential.rst new file mode 100644 index 0000000000..968ab4855b --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.static.self_potential.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.electromagnetics.static.self_potential diff --git a/docs/content/api/simpeg.electromagnetics.static.spectral_induced_polarization.rst b/docs/content/api/simpeg.electromagnetics.static.spectral_induced_polarization.rst new file mode 100644 index 0000000000..ea0594a742 --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.static.spectral_induced_polarization.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.electromagnetics.static.spectral_induced_polarization \ No newline at end of file diff --git a/docs/content/api/simpeg.electromagnetics.static.spontaneous_potential.rst b/docs/content/api/simpeg.electromagnetics.static.spontaneous_potential.rst new file mode 100644 index 0000000000..2e7ee86039 --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.static.spontaneous_potential.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.electromagnetics.static.spontaneous_potential diff --git a/docs/content/api/simpeg.electromagnetics.static.utils.rst b/docs/content/api/simpeg.electromagnetics.static.utils.rst new file mode 100644 index 0000000000..7d70b243c7 --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.static.utils.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.electromagnetics.static.utils \ No newline at end of file diff --git a/docs/content/api/simpeg.electromagnetics.time_domain.rst b/docs/content/api/simpeg.electromagnetics.time_domain.rst new file mode 100644 index 0000000000..4160a46799 --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.time_domain.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.electromagnetics.time_domain \ No newline at end of file diff --git a/docs/content/api/simpeg.electromagnetics.utils.rst b/docs/content/api/simpeg.electromagnetics.utils.rst new file mode 100644 index 0000000000..e040bf84e2 --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.utils.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.electromagnetics.utils \ No newline at end of file diff --git a/docs/content/api/simpeg.electromagnetics.viscous_remanent_magnetization.rst b/docs/content/api/simpeg.electromagnetics.viscous_remanent_magnetization.rst new file mode 100644 index 0000000000..9eb72e4e07 --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.viscous_remanent_magnetization.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.electromagnetics.viscous_remanent_magnetization \ No newline at end of file diff --git a/docs/content/api/simpeg.flow.richards.rst b/docs/content/api/simpeg.flow.richards.rst new file mode 100644 index 0000000000..f357129635 --- /dev/null +++ b/docs/content/api/simpeg.flow.richards.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.flow.richards diff --git a/docs/content/api/SimPEG.flow.rst b/docs/content/api/simpeg.flow.rst similarity index 84% rename from docs/content/api/SimPEG.flow.rst rename to docs/content/api/simpeg.flow.rst index 576e049f1b..a9c972db27 100644 --- a/docs/content/api/SimPEG.flow.rst +++ b/docs/content/api/simpeg.flow.rst @@ -7,4 +7,4 @@ Things about the fluid flow module .. toctree:: :maxdepth: 2 - SimPEG.flow.richards + simpeg.flow.richards diff --git a/docs/content/api/simpeg.meta.rst b/docs/content/api/simpeg.meta.rst new file mode 100644 index 0000000000..4fd168df84 --- /dev/null +++ b/docs/content/api/simpeg.meta.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.meta \ No newline at end of file diff --git a/docs/content/api/simpeg.potential_fields.base.rst b/docs/content/api/simpeg.potential_fields.base.rst new file mode 100644 index 0000000000..e62e05fcf3 --- /dev/null +++ b/docs/content/api/simpeg.potential_fields.base.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.potential_fields diff --git a/docs/content/api/simpeg.potential_fields.gravity.rst b/docs/content/api/simpeg.potential_fields.gravity.rst new file mode 100644 index 0000000000..aa28a01ba9 --- /dev/null +++ b/docs/content/api/simpeg.potential_fields.gravity.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.potential_fields.gravity diff --git a/docs/content/api/simpeg.potential_fields.magnetics.rst b/docs/content/api/simpeg.potential_fields.magnetics.rst new file mode 100644 index 0000000000..88bb6bfb84 --- /dev/null +++ b/docs/content/api/simpeg.potential_fields.magnetics.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.potential_fields.magnetics diff --git a/docs/content/api/SimPEG.potential_fields.rst b/docs/content/api/simpeg.potential_fields.rst similarity index 63% rename from docs/content/api/SimPEG.potential_fields.rst rename to docs/content/api/simpeg.potential_fields.rst index 26d7c34c0b..f5097a9176 100644 --- a/docs/content/api/SimPEG.potential_fields.rst +++ b/docs/content/api/simpeg.potential_fields.rst @@ -5,12 +5,12 @@ Potential Fields .. toctree:: :maxdepth: 2 - SimPEG.potential_fields.gravity - SimPEG.potential_fields.magnetics + simpeg.potential_fields.gravity + simpeg.potential_fields.magnetics Base Potential Fields --------------------- .. toctree:: :maxdepth: 2 - SimPEG.potential_fields.base + simpeg.potential_fields.base diff --git a/docs/content/api/simpeg.regularization.rst b/docs/content/api/simpeg.regularization.rst new file mode 100644 index 0000000000..fd8099173b --- /dev/null +++ b/docs/content/api/simpeg.regularization.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.regularization diff --git a/docs/content/api/SimPEG.rst b/docs/content/api/simpeg.rst similarity index 100% rename from docs/content/api/SimPEG.rst rename to docs/content/api/simpeg.rst diff --git a/docs/content/api/SimPEG.seismic.rst b/docs/content/api/simpeg.seismic.rst similarity index 75% rename from docs/content/api/SimPEG.seismic.rst rename to docs/content/api/simpeg.seismic.rst index 57f467182e..a92572ef1b 100644 --- a/docs/content/api/SimPEG.seismic.rst +++ b/docs/content/api/simpeg.seismic.rst @@ -7,4 +7,4 @@ Things about the Seismic module .. toctree:: :maxdepth: 2 - SimPEG.seismic.straight_ray_tomography + simpeg.seismic.straight_ray_tomography diff --git a/docs/content/api/simpeg.seismic.straight_ray_tomography.rst b/docs/content/api/simpeg.seismic.straight_ray_tomography.rst new file mode 100644 index 0000000000..e8bfe945d6 --- /dev/null +++ b/docs/content/api/simpeg.seismic.straight_ray_tomography.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.seismic.straight_ray_tomography diff --git a/docs/content/api/simpeg.utils.rst b/docs/content/api/simpeg.utils.rst new file mode 100644 index 0000000000..2ff4babe74 --- /dev/null +++ b/docs/content/api/simpeg.utils.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.utils From c12f51efe1afb52ad9cf6ffe1e357b85c7a51c0e Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 10 May 2024 12:28:38 -0700 Subject: [PATCH 002/194] Replace SimPEG for simpeg in getting started pages (#1447) Replace usage of `SimPEG` for `simpeg` in the Getting Started documentation pages. --- docs/content/getting_started/big_picture.rst | 22 +++++++++---------- .../contributing/code-style.rst | 2 +- .../contributing/documentation.rst | 6 ++--- .../getting_started/contributing/testing.rst | 4 ++-- .../contributing/working-with-github.rst | 2 +- docs/content/getting_started/index.rst | 2 +- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/docs/content/getting_started/big_picture.rst b/docs/content/getting_started/big_picture.rst index ee3be2f3a3..f8060f0bb6 100644 --- a/docs/content/getting_started/big_picture.rst +++ b/docs/content/getting_started/big_picture.rst @@ -99,29 +99,29 @@ empirical by nature and our software package is designed to facilitate this iterative process. To accomplish this, we have divided the inversion methodology into eight major components (See figure above). The :class:`discretize.base.BaseMesh` class handles the discretization of the -earth and also provides numerical operators. The :class:`SimPEG.survey.BaseSurvey` +earth and also provides numerical operators. The :class:`simpeg.survey.BaseSurvey` class handles the geometry of a geophysical problem as well as sources and -receivers. The :class:`SimPEG.simulation.BaseSimulation` class handles the +receivers. The :class:`simpeg.simulation.BaseSimulation` class handles the simulation of the physics for the geophysical problem of interest. The -:class:`SimPEG.simulation.BaseSimulation` creates geophysical fields given a -source from the :class:`SimPEG.survey.BaseSurvey`, interpolates these fields to +:class:`simpeg.simulation.BaseSimulation` creates geophysical fields given a +source from the :class:`simpeg.survey.BaseSurvey`, interpolates these fields to the receiver locations, and converts them to the appropriate data type, for example, by selecting only the measured components of the field. Each of these operations may have associated derivatives with respect to the model and the computed field; these are included in the calculation of the sensitivity. For -the inversion, a :class:`SimPEG.data_misfit.BaseDataMisfit` is chosen to capture +the inversion, a :class:`simpeg.data_misfit.BaseDataMisfit` is chosen to capture the goodness of fit of the predicted data and a -:class:`SimPEG.regularization.BaseRegularization` is chosen to handle the non- +:class:`simpeg.regularization.BaseRegularization` is chosen to handle the non- uniqueness. These inversion elements and an Optimization routine are combined -into an inverse problem class :class:`SimPEG.inverse_problem.BaseInvProblem`. -:class:`SimPEG.inverse_problem.BaseInvProblem` is the mathematical statement that +into an inverse problem class :class:`simpeg.inverse_problem.BaseInvProblem`. +:class:`simpeg.inverse_problem.BaseInvProblem` is the mathematical statement that will be numerically solved by running an Inversion. The -:class:`SimPEG.inversion.BaseInversion` class handles organization and +:class:`simpeg.inversion.BaseInversion` class handles organization and dispatch of directives between all of the various pieces of the framework. The arrows in the figure above indicate what each class takes as a primary -argument. For example, both the :class:`SimPEG.simulation.BaseSimulation` and -:class:`SimPEG.regularization.BaseRegularization` classes take a +argument. For example, both the :class:`simpeg.simulation.BaseSimulation` and +:class:`simpeg.regularization.BaseRegularization` classes take a :class:`discretize.base.BaseMesh` class as an argument. The diagram does not show class inheritance, as each of the base classes outlined have many subtypes that can be interchanged. The :class:`discretize.base.BaseMesh` diff --git a/docs/content/getting_started/contributing/code-style.rst b/docs/content/getting_started/contributing/code-style.rst index 8a021f333a..75f235d107 100644 --- a/docs/content/getting_started/contributing/code-style.rst +++ b/docs/content/getting_started/contributing/code-style.rst @@ -20,7 +20,7 @@ Run ``black`` on SimPEG directories that contain Python source files: .. code:: - black SimPEG examples tutorials tests + black simpeg examples tutorials tests Run ``flake8`` on the whole project with: diff --git a/docs/content/getting_started/contributing/documentation.rst b/docs/content/getting_started/contributing/documentation.rst index d4bda313e0..8ef695961a 100644 --- a/docs/content/getting_started/contributing/documentation.rst +++ b/docs/content/getting_started/contributing/documentation.rst @@ -39,7 +39,7 @@ For example: Second order smoothness weights for the respective dimensions. length_scale_x, length_scale_y, length_scale_z : float, optional First order smoothness length scales for the respective dimensions. - mapping : SimPEG.maps.IdentityMap, optional + mapping : simpeg.maps.IdentityMap, optional A mapping to apply to the model before regularization. reference_model : array_like, optional reference_model_in_smooth : bool, optional @@ -70,14 +70,14 @@ For example: Building the documentation ~~~~~~~~~~~~~~~~~~~~~~~~~~ -If you would like to see the documentation changes. +If you would like to see the documentation changes. In the repo's root directory, enter the following in your terminal. .. code:: make all -Serving the documentation locally +Serving the documentation locally ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Once the documentation is built. You can view it directly using the following command. This will automatically serve the docs and you can see them in your browser. diff --git a/docs/content/getting_started/contributing/testing.rst b/docs/content/getting_started/contributing/testing.rst index ba4e207e86..282bc90997 100644 --- a/docs/content/getting_started/contributing/testing.rst +++ b/docs/content/getting_started/contributing/testing.rst @@ -58,7 +58,7 @@ the ``numpy.testing`` module to check for approximate equals. For instance, import numpy as np import discretize - from SimPEG import maps + from simpeg import maps def test_map_multiplication(self): mesh = discretize.TensorMesh([2,3]) @@ -131,7 +131,7 @@ have first order convergence (e.g. the improvement in the approximation is directly related to how small :math:`\Delta x` is, while if we include the first derivative in our approximation, we expect that :math:`\|f(x) + J(x)\Delta x - f(x + \Delta x)\|` to converge at a second-order rate. For -example, all `maps have an associated derivative test `_ . An example from `test_FDEM_derivs.py `_ . An example from `test_FDEM_derivs.py `_ diff --git a/docs/content/getting_started/contributing/working-with-github.rst b/docs/content/getting_started/contributing/working-with-github.rst index 2876c1e0c2..cb944eadd0 100644 --- a/docs/content/getting_started/contributing/working-with-github.rst +++ b/docs/content/getting_started/contributing/working-with-github.rst @@ -21,7 +21,7 @@ There are two ways you can clone a repository: 1. From a terminal (checkout: https://docs.github.com/en/get-started/quickstart/set-up-git for an tutorial) :: - git clone https://github.com/YOUR-USERNAME/SimPEG + git clone https://github.com/YOUR-USERNAME/simpeg 2. Using a desktop client such as SourceTree_ or GitKraken_. diff --git a/docs/content/getting_started/index.rst b/docs/content/getting_started/index.rst index 6721be40a5..dfef8b8d96 100644 --- a/docs/content/getting_started/index.rst +++ b/docs/content/getting_started/index.rst @@ -4,7 +4,7 @@ Getting Started =============== -Here you'll find instructions on getting up and running with ``SimPEG``. +Here you'll find instructions on getting up and running with SimPEG. .. toctree:: :maxdepth: 2 From 7dec0becdc4cd3321b35472e783300d5c1eb426f Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 13 May 2024 14:00:25 -0700 Subject: [PATCH 003/194] Check inputs for converting 3d surveys to 2d lines (#1392) Add checks to validate the `survey` and `lineID` inputs in the `convert_survey_3d_to_2d_lines` function. Add tests to check if errors are raised after passing invalid arguments. Improve the docstring of the function. --- .../static/utils/static_utils.py | 22 ++++++- tests/em/static/test_DC_Utils.py | 66 +++++++++++++++++++ 2 files changed, 85 insertions(+), 3 deletions(-) diff --git a/simpeg/electromagnetics/static/utils/static_utils.py b/simpeg/electromagnetics/static/utils/static_utils.py index 30edce9209..a5fcaa7e52 100644 --- a/simpeg/electromagnetics/static/utils/static_utils.py +++ b/simpeg/electromagnetics/static/utils/static_utils.py @@ -358,7 +358,7 @@ def convert_survey_3d_to_2d_lines( Defines the corresponding line ID for each datum data_type : {'volt', 'apparent_resistivity', 'apparent_conductivity', 'apparent_chargeability'} Data type for the survey. - output_indexing : bool, default=``False`` + output_indexing : bool, default=False, optional If ``True`` output a list of indexing arrays that map from the original 3D data to each 2D survey line. @@ -366,10 +366,26 @@ def convert_survey_3d_to_2d_lines( ------- survey_list : list of simpeg.electromagnetics.static.resistivity.Survey A list of 2D survey objects - out_indices_list : list of numpy.ndarray + out_indices_list : list of numpy.ndarray, optional A list of indexing arrays that map from the original 3D data to each 2D - survey line. + survey line. Will be returned only if ``output_indexing`` is set to + True. """ + # Check if the survey is 3D + if (ndims := survey.locations_a.shape[1]) != 3: + raise ValueError(f"Invalid {ndims}D 'survey'. It should be a 3D survey.") + # Checks on the passed lineID array + if (ndims := lineID.ndim) != 1: + raise ValueError( + f"Invalid 'lineID' array with '{ndims}' dimensions. " + "It should be a 1D array." + ) + if (size := lineID.size) != survey.nD: + raise ValueError( + f"Invalid 'lineID' array with '{size}' elements. " + "It should have the same number of elements as data " + f"in the survey ('{survey.nD}')." + ) # Find all unique line id unique_lineID = np.unique(lineID) diff --git a/tests/em/static/test_DC_Utils.py b/tests/em/static/test_DC_Utils.py index bc872fcc85..5e98513a81 100644 --- a/tests/em/static/test_DC_Utils.py +++ b/tests/em/static/test_DC_Utils.py @@ -1,5 +1,6 @@ # import matplotlib # matplotlib.use('Agg') +import pytest import unittest import numpy as np import discretize @@ -345,5 +346,70 @@ def test_convert_to_2d(self): self.assertEqual(survey.locations_a[0, 0], 0) +class TestConvertTo2DInvalidInputs: + """ + Test convert_survey_3d_to_2d_lines after passing invalid inputs. + """ + + @pytest.fixture + def survey_3d(self): + """Sample 3D DC survey.""" + receiver = dc.receivers.Dipole( + locations_m=np.array([[-100, 0, 0]]), + locations_n=np.array([[100, 0, 0]]), + data_type="volt", + ) + source = dc.sources.Dipole( + receiver_list=[receiver], + location_a=np.array([-50, 0, 0]), + location_b=np.array([50, 0, 0]), + ) + survey = dc.Survey(source_list=[source]) + return survey + + @pytest.fixture + def survey_2d(self): + """Sample 2D DC survey.""" + receiver = dc.receivers.Dipole( + locations_m=np.array([[-100, 0]]), + locations_n=np.array([[100, 0]]), + data_type="volt", + ) + source = dc.sources.Dipole( + receiver_list=[receiver], + location_a=np.array([-50, 0]), + location_b=np.array([50, 0]), + ) + survey = dc.Survey(source_list=[source]) + return survey + + def test_invalid_survey(self, survey_2d): + """ + Test if error is raised when passing an invalid survey (2D survey) + """ + line_ids = np.ones(survey_2d.nD) + with pytest.raises(ValueError, match="Invalid 2D 'survey'"): + utils.convert_survey_3d_to_2d_lines(survey_2d, line_ids) + + def test_invalid_line_ids_wrong_dims(self, survey_3d): + """ + Test if error is raised after invalid line_ids with wrong dimensions. + """ + line_ids = np.atleast_2d(np.ones(survey_3d.nD)) + msg = "Invalid 'lineID' array with '2' dimensions. " + with pytest.raises(ValueError, match=msg): + utils.convert_survey_3d_to_2d_lines(survey_3d, line_ids) + + def test_invalid_line_ids_wrong_size(self, survey_3d): + """ + Test if error is raised after an invalid line_ids with wrong size. + """ + size = survey_3d.nD - 1 + line_ids = np.ones(size) + msg = f"Invalid 'lineID' array with '{size}' elements. " + with pytest.raises(ValueError, match=msg): + utils.convert_survey_3d_to_2d_lines(survey_3d, line_ids) + + if __name__ == "__main__": unittest.main() From 6f35ed29dc8c61785f759de3842646fcbcbebfc7 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 15 May 2024 09:25:36 -0700 Subject: [PATCH 004/194] Always use Pydata Sphinx theme for building docs (#1445) Always use the Pydata Sphinx theme for building the docs. Remove the `try` statement that used to check if the theme was installed and defaulted to the default theme if it wasn't. --- docs/conf.py | 106 +++++++++++++++++++++++++-------------------------- 1 file changed, 51 insertions(+), 55 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index dde265d6d5..30d36e1446 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -237,62 +237,58 @@ def linkcode_resolve(domain, info): dict(name="Contact", url="https://mattermost.softwareunderground.org/simpeg"), ] -try: - import pydata_sphinx_theme - - html_theme = "pydata_sphinx_theme" - - # If false, no module index is generated. - html_use_modindex = True - - html_theme_options = { - "external_links": external_links, - "icon_links": [ - { - "name": "GitHub", - "url": "https://github.com/simpeg/simpeg", - "icon": "fab fa-github", - }, - { - "name": "Mattermost", - "url": "https://mattermost.softwareunderground.org/simpeg", - "icon": "fas fa-comment", - }, - { - "name": "Discourse", - "url": "https://simpeg.discourse.group/", - "icon": "fab fa-discourse", - }, - { - "name": "Youtube", - "url": "https://www.youtube.com/c/geoscixyz", - "icon": "fab fa-youtube", - }, - ], - "use_edit_page_button": False, - "collapse_navigation": True, - "analytics": { - "plausible_analytics_domain": "docs.simpeg.xyz", - "plausible_analytics_url": "https://plausible.io/js/script.js", +# Use Pydata Sphinx theme +html_theme = "pydata_sphinx_theme" + +# If false, no module index is generated. +html_use_modindex = True + +html_theme_options = { + "external_links": external_links, + "icon_links": [ + { + "name": "GitHub", + "url": "https://github.com/simpeg/simpeg", + "icon": "fab fa-github", + }, + { + "name": "Mattermost", + "url": "https://mattermost.softwareunderground.org/simpeg", + "icon": "fas fa-comment", }, - "navbar_align": "left", # make elements closer to logo on the left - } - html_logo = "images/simpeg-logo.png" - - html_static_path = ["_static"] - - html_css_files = [ - "css/custom.css", - ] - - html_context = { - "github_user": "simpeg", - "github_repo": "simpeg", - "github_version": "main", - "doc_path": "docs", - } -except Exception: - html_theme = "default" + { + "name": "Discourse", + "url": "https://simpeg.discourse.group/", + "icon": "fab fa-discourse", + }, + { + "name": "Youtube", + "url": "https://www.youtube.com/c/geoscixyz", + "icon": "fab fa-youtube", + }, + ], + "use_edit_page_button": False, + "collapse_navigation": True, + "analytics": { + "plausible_analytics_domain": "docs.simpeg.xyz", + "plausible_analytics_url": "https://plausible.io/js/script.js", + }, + "navbar_align": "left", # make elements closer to logo on the left +} +html_logo = "images/simpeg-logo.png" + +html_static_path = ["_static"] + +html_css_files = [ + "css/custom.css", +] + +html_context = { + "github_user": "simpeg", + "github_repo": "simpeg", + "github_version": "main", + "doc_path": "docs", +} # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the From e13a25f096587a5bd17a81893a3d7164b4c909e4 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 15 May 2024 11:30:38 -0700 Subject: [PATCH 005/194] Simplify interface of UniformBackgroundField (#1421) Remove the `**kwargs` from the constructor of `UniformBackgroundField`. Make `receiver_list`, `amplitude`, `inclination` and `declination` as required arguments without any default value. Include type hints in the signature of the constructor. Add check for type of receivers to the constructor of `UniformBackgroundField`. Add test that checks the value of `UniformBackgroundField.b0`, passing `receiver_list` as an actual list of point receivers and as `None`. Add test that checks if the error is raised after passing invalid receiver type. Minor improvements to the docstring of the class. --- simpeg/potential_fields/magnetics/sources.py | 60 +++++++++++-------- tests/pf/test_forward_Mag_Linear.py | 4 +- tests/pf/test_mag_uniform_background_field.py | 52 ++++++++++++++-- tests/pf/test_survey_counting.py | 4 +- tests/utils/test_io_utils.py | 4 +- 5 files changed, 90 insertions(+), 34 deletions(-) diff --git a/simpeg/potential_fields/magnetics/sources.py b/simpeg/potential_fields/magnetics/sources.py index 0e4000e9c2..df9027f38a 100644 --- a/simpeg/potential_fields/magnetics/sources.py +++ b/simpeg/potential_fields/magnetics/sources.py @@ -1,6 +1,9 @@ +from __future__ import annotations from ...survey import BaseSrc -from simpeg.utils.mat_utils import dip_azimuth2cartesian -from simpeg.utils.code_utils import deprecate_class, validate_float +from ...utils.mat_utils import dip_azimuth2cartesian +from ...utils.code_utils import deprecate_class, validate_float, validate_list_of_types + +from .receivers import Point class UniformBackgroundField(BaseSrc): @@ -11,39 +14,46 @@ class UniformBackgroundField(BaseSrc): Parameters ---------- - receiver_list : list of simpeg.potential_fields.magnetics.Point - amplitude : float, optional - amplitude of the inducing backgound field, usually this is in units of nT. - inclination : float, optional - Dip angle in degrees from the horizon, positive points into the earth. - declination : float, optional + receiver_list : simpeg.potential_fields.magnetics.Point, list of simpeg.potential_fields.magnetics.Point or None + Point magnetic receivers. + amplitude : float + Amplitude of the inducing background field, usually this is in + units of nT. + inclination : float + Dip angle in degrees from the horizon, positive value into the earth. + declination : float Azimuthal angle in degrees from north, positive clockwise. """ def __init__( self, - receiver_list=None, - amplitude=50000.0, - inclination=90.0, - declination=0.0, - **kwargs, + receiver_list: Point | list[Point] | None, + amplitude: float, + inclination: float, + declination: float, ): - # Raise errors on 'parameters' argument - # The parameters argument was supported in the deprecated SourceField - # class. We would like to raise an error in case the user passes it - # so the class doesn't behave differently than expected. - if (key := "parameters") in kwargs: - raise TypeError( - f"'{key}' property has been removed." - "Please pass the amplitude, inclination and declination" - " through their own arguments." - ) - self.amplitude = amplitude self.inclination = inclination self.declination = declination + super().__init__(receiver_list=receiver_list) + + @property + def receiver_list(self): + """ + List of receivers associated with the survey. - super().__init__(receiver_list=receiver_list, **kwargs) + Returns + ------- + list of SimPEG.potential_fields.magnetics.Point + List of magnetic receivers associated with the survey + """ + return self._receiver_list + + @receiver_list.setter + def receiver_list(self, value): + self._receiver_list = validate_list_of_types( + "receiver_list", value, Point, ensure_unique=True + ) @property def amplitude(self): diff --git a/tests/pf/test_forward_Mag_Linear.py b/tests/pf/test_forward_Mag_Linear.py index 21f0570189..45ff17ba8c 100644 --- a/tests/pf/test_forward_Mag_Linear.py +++ b/tests/pf/test_forward_Mag_Linear.py @@ -504,7 +504,9 @@ def test_removed_modeltype(): mesh = discretize.TensorMesh(h) receiver_location = np.array([[0, 0, 100]]) receiver = mag.Point(receiver_location, components="tmi") - background_field = mag.UniformBackgroundField(receiver_list=[receiver]) + background_field = mag.UniformBackgroundField( + receiver_list=[receiver], amplitude=50_000, inclination=90, declination=0 + ) survey = mag.Survey(background_field) mapping = maps.IdentityMap(mesh, nP=mesh.n_cells) sim = mag.Simulation3DIntegral(mesh, survey=survey, chiMap=mapping) diff --git a/tests/pf/test_mag_uniform_background_field.py b/tests/pf/test_mag_uniform_background_field.py index 785be3355e..feeb65e909 100644 --- a/tests/pf/test_mag_uniform_background_field.py +++ b/tests/pf/test_mag_uniform_background_field.py @@ -3,7 +3,8 @@ """ import pytest -from simpeg.potential_fields.magnetics import UniformBackgroundField, SourceField +import numpy as np +from simpeg.potential_fields.magnetics import UniformBackgroundField, SourceField, Point def test_invalid_parameters_argument(): @@ -11,11 +12,7 @@ def test_invalid_parameters_argument(): Test if error is raised after passing 'parameters' as argument """ parameters = (1, 35, 60) - msg = ( - "'parameters' property has been removed." - "Please pass the amplitude, inclination and declination" - " through their own arguments." - ) + msg = r"__init__\(\) got an unexpected keyword argument 'parameters'" with pytest.raises(TypeError, match=msg): UniformBackgroundField(parameters=parameters) @@ -27,3 +24,46 @@ def test_deprecated_source_field(): msg = "SourceField has been removed, please use UniformBackgroundField." with pytest.raises(NotImplementedError, match=msg): SourceField() + + +@pytest.mark.parametrize("receiver_as_list", (True, False)) +def test_invalid_receiver_type(receiver_as_list): + """ + Test if error is raised after passing invalid type of receivers + """ + receiver_invalid = np.array([[1.0, 1.0, 1.0]]) + if receiver_as_list: + receiver_valid = Point(locations=np.array([[0.0, 0.0, 0.0]]), components="tmi") + receiver_list = [receiver_valid, receiver_invalid] + else: + receiver_list = receiver_invalid + msg = f"'receiver_list' must be a list of {Point}" + with pytest.raises(TypeError, match=msg): + UniformBackgroundField( + receiver_list=receiver_list, + amplitude=55_000, + inclination=45, + declination=30, + ) + + +@pytest.mark.parametrize( + "receiver_list", + (None, [Point(locations=np.array([[0.0, 0.0, 0.0]]), components="tmi")]), + ids=("None", "Point"), +) +def test_value_b0(receiver_list): + """ + Test UniformBackgroundField.b0 value + """ + amplitude = 55_000 + inclination = 45 + declination = 10 + expected_b0 = (6753.3292182935065, 38300.03321760104, -38890.87296526011) + uniform_background_field = UniformBackgroundField( + receiver_list=receiver_list, + amplitude=amplitude, + inclination=inclination, + declination=declination, + ) + np.testing.assert_allclose(uniform_background_field.b0, expected_b0) diff --git a/tests/pf/test_survey_counting.py b/tests/pf/test_survey_counting.py index 84aa334561..d0e0d71002 100644 --- a/tests/pf/test_survey_counting.py +++ b/tests/pf/test_survey_counting.py @@ -27,7 +27,9 @@ def test_magnetics_survey(): rx1 = mag.Point(rx_locs, components=rx_components) rx2 = mag.Point(rx_locs, components="tmi") - src = mag.UniformBackgroundField([rx1, rx2]) + src = mag.UniformBackgroundField( + receiver_list=[rx1, rx2], amplitude=50_000, inclination=90, declination=0 + ) survey = mag.Survey(src) assert rx1.nD == 60 diff --git a/tests/utils/test_io_utils.py b/tests/utils/test_io_utils.py index 9e9a07f206..105ade7451 100644 --- a/tests/utils/test_io_utils.py +++ b/tests/utils/test_io_utils.py @@ -256,7 +256,9 @@ def setUp(self): self.std = std rx2 = magnetics.receivers.Point(xyz, components="tmi") - src_bad = magnetics.sources.UniformBackgroundField([rx, rx2]) + src_bad = magnetics.sources.UniformBackgroundField( + receiver_list=[rx, rx2], amplitude=50_000, inclination=90, declination=0 + ) survey_bad = magnetics.survey.Survey(src_bad) self.survey_bad = survey_bad From 80e3b1807d8dedac04cb8aa1731957462ffa187c Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Wed, 15 May 2024 16:35:17 -0600 Subject: [PATCH 006/194] Ensure the queue's are joined when the meta simulation is joined. (#1464) Join the multiprocessing queues in the `MultiprocessingMetaSimulation` when the simulation is joined. --- simpeg/meta/multiprocessing.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/simpeg/meta/multiprocessing.py b/simpeg/meta/multiprocessing.py index 23e025688b..f5aceceda6 100644 --- a/simpeg/meta/multiprocessing.py +++ b/simpeg/meta/multiprocessing.py @@ -26,7 +26,12 @@ def __init__(self, item_id, t_queue, r_queue): def __del__(self): # Tell the child process that this object is no longer needed in its cache. - self.t_queue.put(("del_item", (self.item_id,))) + try: + self.t_queue.put(("del_item", (self.item_id,))) + except ValueError: + # if the queue was already closed it will throw a value error + # so catch it here gracefully and continue on. + pass class _SimulationProcess(Process): @@ -169,6 +174,15 @@ def result(self): self._check_closed() return self.result_queue.get() + def join(self, timeout=None): + self._check_closed() + self.task_queue.put(None) + self.task_queue.close() + self.result_queue.close() + self.task_queue.join_thread() + self.result_queue.join_thread() + super().join(timeout=timeout) + class MultiprocessingMetaSimulation(MetaSimulation): """Multiprocessing version of simulation of simulations. @@ -193,11 +207,11 @@ class MultiprocessingMetaSimulation(MetaSimulation): ... sim = MultiprocessingMetaSimulation(...) ... sim.dpred(model) - You must also be sure to call sim.close() before discarding + You must also be sure to call `sim.join()` before discarding this worker to kill the subprocesses that are created, as you would with - any other multiprocessing queue. + any other multiprocessing process. - >>> sim.close() + >>> sim.join() Parameters ---------- @@ -344,7 +358,6 @@ def getJtJdiag(self, m, W=None, f=None): def join(self, timeout=None): for p in self._sim_processes: if p.is_alive(): - p.task_queue.put(None) p.join(timeout=timeout) From 50306eebab58dc1255548c791eab91df76b652cb Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 16 May 2024 11:34:59 -0700 Subject: [PATCH 007/194] Add maintenance issue template (#1468) Add a new issue template for opening maintenance issues. --- .github/ISSUE_TEMPLATE/maintenance.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/maintenance.md diff --git a/.github/ISSUE_TEMPLATE/maintenance.md b/.github/ISSUE_TEMPLATE/maintenance.md new file mode 100644 index 0000000000..ffbb7cd4a2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/maintenance.md @@ -0,0 +1,18 @@ +--- +name: Maintenance +about: "Maintainers only: Issues for maintenance tasks" +title: "MNT: " +labels: "maintenance" +assignees: "" +--- + +**Description of the maintenance task** + + From dcec9d5fbbfbb8691769e41da16a790368959958 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 17 May 2024 09:21:50 -0700 Subject: [PATCH 008/194] Add instructions to update the environment (#1462) Add instructions to the Contributing to SimPEG pages on how to update the environment once it's already created. --- .../contributing/setting-up-environment.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/content/getting_started/contributing/setting-up-environment.rst b/docs/content/getting_started/contributing/setting-up-environment.rst index 0ec601f540..5775920d31 100644 --- a/docs/content/getting_started/contributing/setting-up-environment.rst +++ b/docs/content/getting_started/contributing/setting-up-environment.rst @@ -138,3 +138,19 @@ you want to commit them nonetheless. .. _pre-commit: https://pre-commit.com/ .. _Black: https://black.readthedocs.io .. _flake8: https://flake8.pycqa.org + + +Update your environment +----------------------- + +Every once in a while, the minimum versions of the packages in the +``environment.yml`` file get updated. After this happens, it's better to update +the ``simpeg-test`` environment we have created. This way we ensure that we are +checking the style and testing our code using those updated versions. + +To update our environment we need to navigate to the directory where you +:ref:`cloned SimPEG's repository ` and run: + +.. code:: + + conda env update -f environment_test.yml From 67c041699c19185a52c01960fed9fd29cc59ce57 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 17 May 2024 09:22:56 -0700 Subject: [PATCH 009/194] Stop recommending mamba for installing simpeg (#1463) Since latest versions of `conda` make use of `libmamba` that achieves the same performance as `mamba` while installing packages and creating environments, we can stop recommending `mamba` to our users as a better alternative for `conda`. Update the admonitions in the instructions to install simpeg and to set up the environment for contributors. Mention the minimum version of `conda` that uses `libmamba` by default. Replace recommendations for Mambaforge to Miniforge. --- .../contributing/setting-up-environment.rst | 24 ++++++++++------ docs/content/getting_started/installing.rst | 28 ++++++++++++------- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/docs/content/getting_started/contributing/setting-up-environment.rst b/docs/content/getting_started/contributing/setting-up-environment.rst index 5775920d31..d775983dff 100644 --- a/docs/content/getting_started/contributing/setting-up-environment.rst +++ b/docs/content/getting_started/contributing/setting-up-environment.rst @@ -8,13 +8,13 @@ Install Python First you will need to install Python. You can find instructions in :ref:`installing_python`. We highly encourage to install Anaconda_ or -Mambaforge_. +Miniforge_. Create environment ------------------ To get started developing SimPEG we recommend setting up an environment using -the ``conda`` (or ``mamba``) package manager that mimics the testing +the ``conda`` package manager that mimics the testing environment used for continuous integration testing. Most of the packages that we use are available through the ``conda-forge`` project. This will ensure you have all of the necessary packages to both develop SimPEG and run tests @@ -30,11 +30,19 @@ repository ` and run: .. note:: - If you find yourself wanting a faster package manager than ``conda`` - check out the ``mamba`` project at https://mamba.readthedocs.io/. It - usually is able to set up environments much quicker than ``conda`` and - can be used as a drop-in replacement (i.e. replace ``conda`` commands with - ``mamba``). + Since `version 23.10.0 + `_, + ``conda`` makes use of the ``libmamba`` solver to resolve dependencies. It + makes creation of environments and installation of new packages much faster + than when using older versions of ``conda``. + + Since this version, ``conda`` can achieve the same performance as + ``mamba``, so there's no need to install ``mamba`` if you have an updated + version of ``conda``. + If not, either `update conda + `_, or + keep using ``mamba`` instead. + Once the environment is successfully created, you can *activate* it with @@ -72,7 +80,7 @@ This practice also allows you to uninstall SimPEG if so desired: a way to install SimPEG for developers. .. _Anaconda: https://www.anaconda.com/products/individual -.. _Mambaforge: https://www.anaconda.com/products/individual +.. _Miniforge: https://github.com/conda-forge/miniforge Check your installation ----------------------- diff --git a/docs/content/getting_started/installing.rst b/docs/content/getting_started/installing.rst index 14adff1570..06724787e7 100644 --- a/docs/content/getting_started/installing.rst +++ b/docs/content/getting_started/installing.rst @@ -10,7 +10,7 @@ Prerequisite: Installing Python =============================== SimPEG is written in Python_! -We highly recommend installing it using Anaconda_ (or the alternative Mambaforge_). +We highly recommend installing it using Anaconda_ (or the alternative Miniforge_). It installs `Python `_, `Jupyter `_ and other core Python libraries for scientific computing. @@ -30,7 +30,7 @@ recommend checking out `Software Carpentry `_. .. _Python: https://www.python.org/ .. _Anaconda: https://www.anaconda.com/products/individual -.. _Mambaforge: https://www.anaconda.com/products/individual +.. _Miniforge: https://github.com/conda-forge/miniforge .. _installing_simpeg: @@ -42,21 +42,29 @@ Conda Forge ----------- SimPEG is available through `conda-forge` and you can install is using the -`conda package manager `_ that comes with the Anaconda -distribution: +`conda package manager `_ that comes with the Anaconda_ +or Miniforge_ distributions: .. code:: conda install SimPEG --channel conda-forge -Installing through `conda`/`mamba` is our recommended method of installation. +Installing through `conda` is our recommended method of installation. .. note:: - If you find yourself wanting a faster package manager than ``conda`` - check out the ``mamba`` project at https://mamba.readthedocs.io/. It - usually is able to set up environments much quicker than ``conda`` and - can be used as a drop-in replacement (i.e. replace ``conda`` commands with - ``mamba``). + + Since `version 23.10.0 + `_, + ``conda`` makes use of the ``libmamba`` solver to resolve dependencies. It + makes creation of environments and installation of new packages much faster + than when using older versions of ``conda``. + + Since this version, ``conda`` can achieve the same performance as + ``mamba``, so there's no need to install ``mamba`` if you have an updated + version of ``conda``. + If not, either `update conda + `_, or + keep using ``mamba`` instead. PyPi ---- From 712cfda4457651142dec6011b38547a1300534df Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 22 May 2024 13:10:32 -0700 Subject: [PATCH 010/194] Fix bug on arguments of beta estimator directives (#1460) Fix bug in the constructor of beta estimator directive classes: the `seed` argument was being passed as a positional argument to the parent's constructor, which would assign it to the `n_pw_iter` attribute instead to the `seed`, leading to unwanted behaviours. Pass the arguments to the constructor through keywords to make it more explicit and less prone to errors. Add tests that would fail without this bugfix. Remove the `n_pw_iter` and the `method` arguments from the constructor of `BaseBetaEstimator`. --- simpeg/directives/directives.py | 6 ++---- tests/base/test_directives.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/simpeg/directives/directives.py b/simpeg/directives/directives.py index 156f1acb33..1fd28d2467 100644 --- a/simpeg/directives/directives.py +++ b/simpeg/directives/directives.py @@ -355,9 +355,7 @@ class BaseBetaEstimator(InversionDirective): def __init__( self, beta0_ratio=1.0, - n_pw_iter=4, seed=None, - method="power_iteration", **kwargs, ): super().__init__(**kwargs) @@ -452,7 +450,7 @@ class BetaEstimateMaxDerivative(BaseBetaEstimator): """ def __init__(self, beta0_ratio=1.0, seed=None, **kwargs): - super().__init__(beta0_ratio, seed, **kwargs) + super().__init__(beta0_ratio=beta0_ratio, seed=seed, **kwargs) def initialize(self): if self.seed is not None: @@ -522,7 +520,7 @@ class BetaEstimate_ByEig(BaseBetaEstimator): """ def __init__(self, beta0_ratio=1.0, n_pw_iter=4, seed=None, **kwargs): - super().__init__(beta0_ratio, seed, **kwargs) + super().__init__(beta0_ratio=beta0_ratio, seed=seed, **kwargs) self.n_pw_iter = n_pw_iter @property diff --git a/tests/base/test_directives.py b/tests/base/test_directives.py index 474fba332a..aa83489187 100644 --- a/tests/base/test_directives.py +++ b/tests/base/test_directives.py @@ -397,5 +397,34 @@ def test_normalization_method_setter_invalid(self, normalization_method): d_temp.normalization_method = normalization_method +class TestBetaEstimatorArguments: + """ + Test if arguments are assigned in beta estimator directives. + These tests catch the bug described and fixed in #1460. + """ + + def test_beta_estimate_by_eig(self): + """Test on directives.BetaEstimate_ByEig.""" + beta0_ratio = 3.0 + n_pw_iter = 3 + seed = 42 + directive = directives.BetaEstimate_ByEig( + beta0_ratio=beta0_ratio, n_pw_iter=n_pw_iter, seed=seed + ) + assert directive.beta0_ratio == beta0_ratio + assert directive.n_pw_iter == n_pw_iter + assert directive.seed == seed + + def test_beta_estimate_max_derivative(self): + """Test on directives.BetaEstimateMaxDerivative.""" + beta0_ratio = 3.0 + seed = 42 + directive = directives.BetaEstimateMaxDerivative( + beta0_ratio=beta0_ratio, seed=seed + ) + assert directive.beta0_ratio == beta0_ratio + assert directive.seed == seed + + if __name__ == "__main__": unittest.main() From bc9a424c531dc76f05dc4e799ce4c7981dda0044 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 22 May 2024 14:41:09 -0700 Subject: [PATCH 011/194] Improve interface for DC Dipole source (#1393) Remove `**kwargs` from the constructor of the class, add `current` as one of the arguments. Improve the logic and error messages for the `location`, `location_a` and `location_b` arguments. Add tests for these new checks. Update docstring of the constructor to reflect latest changes. Fix the expected shape of the `location_a` and `location_b` arguments. --- .../static/resistivity/sources.py | 80 +++++---- tests/em/static/test_dc_sources_interface.py | 152 ++++++++++++++++++ 2 files changed, 197 insertions(+), 35 deletions(-) create mode 100644 tests/em/static/test_dc_sources_interface.py diff --git a/simpeg/electromagnetics/static/resistivity/sources.py b/simpeg/electromagnetics/static/resistivity/sources.py index dd55089689..ee46681d3b 100644 --- a/simpeg/electromagnetics/static/resistivity/sources.py +++ b/simpeg/electromagnetics/static/resistivity/sources.py @@ -139,13 +139,18 @@ class Dipole(BaseSrc): ---------- receiver_list : list of simpeg.electromagnetics.static.resistivity.receivers.BaseRx A list of DC/IP receivers - location_a : (n_source, dim) numpy.array_like - A electrode locations; remember to set 'location_b' keyword argument to define N electrode locations. - location_b : (n_source, dim) numpy.array_like - B electrode locations; remember to set 'location_a' keyword argument to define M electrode locations. - location : list or tuple of length 2 of numpy.array_like - A and B electrode locations. In this case, do not set the 'location_a' and 'location_b' - keyword arguments. And we supply a list or tuple of the form [location_a, location_b]. + location_a : (dim) array_like + A electrode locations; remember to set ``location_b`` keyword argument + to define B electrode location. + location_b : (dim) array_like + B electrode locations; remember to set ``location_a`` keyword argument + to define A electrode location. + location : tuple of array_like, optional + A and B electrode locations. If ``location_a`` and ``location_b`` are + provided, don't pass values to this argument. Otherwise, provide + a tuple of the form ``(location_a, location_b)``. + current : float, optional + Current amplitude in :math:`A` that goes through each electrode. """ def __init__( @@ -154,41 +159,46 @@ def __init__( location_a=None, location_b=None, location=None, - **kwargs, + current=1.0, ): - if "current" in kwargs.keys(): - value = kwargs.pop("current") - current = [value, -value] - else: - current = [1.0, -1.0] - - # if location_a set, then use location_a, location_b - if location_a is not None: - if location_b is None: - raise ValueError( - "For a dipole source both location_a and location_b " "must be set" + if location is None and location_a is None and location_b is None: + raise TypeError( + "Found 'location', 'location_a' and 'location_b' as None. " + "Please specify 'location', or 'location_a' and 'location_b' " + "when defining a dipole source." + ) + if location is not None and (location_a is not None or location_b is not None): + raise TypeError( + "Found 'location_a' and/or 'location_b' as not None values. " + "When passing a not None value for 'location', 'location_a' and " + "'location_b' should be set to None." + ) + if location is None: + if location_a is None: + raise TypeError( + "Invalid 'location_a' set to None. When 'location' is None, " + "both 'location_a' and 'location_b' should be set to " + "a value different than None." ) - - if location is not None: - raise ValueError( - "Cannot set both location and location_a, location_b. " - "Please provide either location=(location_a, location_b) " - "or both location_a=location_a, location_b=location_b" + if location_b is None: + raise TypeError( + "Invalid 'location_b' set to None. When 'location' is None, " + "both 'location_a' and 'location_b' should be set to " + "a value different than None." ) - location = [location_a, location_b] - elif location is not None: - if len(location) != 2: - raise ValueError( - "location must be a list or tuple of length 2: " - "[location_a, location_b]. The input location has " - f"length {len(location)}" - ) + if len(location) != 2: + raise ValueError( + "location must be a list or tuple of length 2: " + "[location_a, location_b]. The input location has " + f"length {len(location)}" + ) - # instantiate super().__init__( - receiver_list=receiver_list, location=location, current=current, **kwargs + receiver_list=receiver_list, + location=location, + current=[current, -current], ) def __repr__(self): diff --git a/tests/em/static/test_dc_sources_interface.py b/tests/em/static/test_dc_sources_interface.py new file mode 100644 index 0000000000..74a85ea5e1 --- /dev/null +++ b/tests/em/static/test_dc_sources_interface.py @@ -0,0 +1,152 @@ +""" +Test interface for some DC sources. +""" + +import pytest +import numpy as np +from simpeg.electromagnetics.static import resistivity as dc + + +class TestDipoleLocations: + r""" + Test the location, location_a and location_b arguments for the Dipole + + Considering that `location`, `location_a`, `location_b` can be None or not + None, then we have 8 different possible combinations. + + + .. code:: + + | location | location_a | location_b | Result | + |----------|------------|------------|--------| + | None | None | None | Error | + | None | None | not None | Error | + | None | not None | None | Error | + | None | not None | not None | Run | + | not None | None | None | Run | + | not None | None | not None | Error | + | not None | not None | None | Error | + | not None | not None | not None | Error | + """ + + @pytest.fixture + def receiver(self): + """Sample DC dipole receiver.""" + receiver = dc.receivers.Dipole( + locations_m=np.array([[-100, 0]]), + locations_n=np.array([[100, 0]]), + data_type="volt", + ) + return receiver + + def test_all_nones(self, receiver): + """ + Test error being raised when passing all location as None + """ + msg = "Found 'location', 'location_a' and 'location_b' as None. " + with pytest.raises(TypeError, match=msg): + dc.sources.Dipole( + receiver_list=[receiver], + location_a=None, + location_b=None, + location=None, + ) + + @pytest.mark.parametrize("electrode", ("a", "b", "both")) + def test_not_nones(self, receiver, electrode): + """ + Test error after location as not None, and location_a and/or location_b + as not None + """ + msg = ( + "Found 'location_a' and/or 'location_b' as not None values. " + "When passing a not None value for 'location', 'location_a' and " + "'location_b' should be set to None." + ) + electrode_a = np.array([-1.0, 0.0]) + electrode_b = np.array([1.0, 0.0]) + if electrode == "a": + kwargs = dict(location_a=electrode_a, location_b=None) + elif electrode == "b": + kwargs = dict(location_a=None, location_b=electrode_b) + else: + kwargs = dict(location_a=electrode_a, location_b=electrode_b) + with pytest.raises(TypeError, match=msg): + dc.sources.Dipole( + receiver_list=[receiver], + location=[electrode_a, electrode_b], + **kwargs, + ) + + @pytest.mark.parametrize("none_electrode", ("a", "b")) + def test_single_location_as_none(self, receiver, none_electrode): + """ + Test error after location is None and one of location_a or location_b + is also None. + """ + msg = ( + f"Invalid 'location_{none_electrode}' set to None. " + "When 'location' is None, both 'location_a' and 'location_b' " + "should be set to a value different than None." + ) + electrode_a = np.array([-1.0, 0.0]) + electrode_b = np.array([1.0, 0.0]) + if none_electrode == "a": + kwargs = dict(location_a=None, location_b=electrode_b) + else: + kwargs = dict(location_a=electrode_a, location_b=None) + with pytest.raises(TypeError, match=msg): + dc.sources.Dipole( + receiver_list=[receiver], + location=None, + **kwargs, + ) + + def test_location_none(self, receiver): + """ + Test if object is correctly initialized with location set to None + """ + electrode_a = np.array([-1.0, 0.0]) + electrode_b = np.array([1.0, 0.0]) + source = dc.sources.Dipole( + receiver_list=[receiver], + location_a=electrode_a, + location_b=electrode_b, + location=None, + ) + assert isinstance(source.location, np.ndarray) + assert len(source.location) == 2 + np.testing.assert_allclose(source.location, [electrode_a, electrode_b]) + + def test_location_not_none(self, receiver): + """ + Test if object is correctly initialized with location is set + """ + electrode_a = np.array([-1.0, 0.0]) + electrode_b = np.array([1.0, 0.0]) + source = dc.sources.Dipole( + receiver_list=[receiver], + location=[electrode_a, electrode_b], + ) + assert isinstance(source.location, np.ndarray) + assert len(source.location) == 2 + np.testing.assert_allclose(source.location, [electrode_a, electrode_b]) + + @pytest.mark.parametrize("length", (0, 1, 3)) + def test_location_invalid_num_elements(self, length, receiver): + """ + Test error after passing location with invalid number of elements + """ + if length == 0: + location = () + elif length == 1: + location = (np.array([1.0, 0.0]),) + else: + location = ( + np.array([1.0, 0.0]), + np.array([1.0, 0.0]), + np.array([1.0, 0.0]), + ) + msg = "location must be a list or tuple of length 2" + with pytest.raises(ValueError, match=msg): + dc.sources.Dipole(receiver_list=[receiver], location=location) From 079d870afda7cc50175e124dd7ef1279268f1fdc Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 22 May 2024 16:45:19 -0700 Subject: [PATCH 012/194] Use Numpy random number generator in codebase (#1394) Replace the usage of legacy `numpy.random.seed()` in SimPEG codebase for the Numpy random number generator object that can be created through the `numpy.random.default_rng()` function. Add a new `typing` module that contain new type aliases, and create a new `RandomSeed` type alias for variables accepted by `numpy.random.default_rng()`. Extend the accepted values for `seed` arguments in several methods and functions to be `None` or `RandomSeed`. Apply these replacements to directives, `eigenvalue_by_power_iteration()`, `create_random_model()`, utility functions on `test_utils.py` and usage of `eigenvalue_by_power_iteration()` in tests. Update documentation and type hint of `make_synthetic_model()` allowing it to get a Numpy `Generator` as `random_seed`. Add a new documentation page for the new `typing` module. --- docs/content/api/index.rst | 11 ++ docs/content/api/simpeg.typing.rst | 1 + simpeg/__init__.py | 1 + simpeg/directives/directives.py | 104 +++++++++++++----- simpeg/directives/sim_directives.py | 5 +- .../natural_source/utils/test_utils.py | 8 +- simpeg/simulation.py | 12 +- simpeg/typing/__init__.py | 61 ++++++++++ simpeg/utils/mat_utils.py | 20 ++-- simpeg/utils/model_builder.py | 29 +++-- tests/base/test_directives.py | 32 ++++++ tests/utils/test_mat_utils.py | 8 +- 12 files changed, 231 insertions(+), 61 deletions(-) create mode 100644 docs/content/api/simpeg.typing.rst create mode 100644 simpeg/typing/__init__.py diff --git a/docs/content/api/index.rst b/docs/content/api/index.rst index a6f8a0d0b0..e401b4422d 100644 --- a/docs/content/api/index.rst +++ b/docs/content/api/index.rst @@ -57,3 +57,14 @@ Classes for encapsulating many simulations. :maxdepth: 2 simpeg.meta + + +Typing +------ + +PEP 484 type aliases used in ``simpeg``. + +.. toctree:: + :maxdepth: 1 + + simpeg.typing \ No newline at end of file diff --git a/docs/content/api/simpeg.typing.rst b/docs/content/api/simpeg.typing.rst new file mode 100644 index 0000000000..44c57f983f --- /dev/null +++ b/docs/content/api/simpeg.typing.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.typing diff --git a/simpeg/__init__.py b/simpeg/__init__.py index f46c2b0e0b..0dc76dab2d 100644 --- a/simpeg/__init__.py +++ b/simpeg/__init__.py @@ -149,6 +149,7 @@ from . import regularization from . import survey from . import simulation +from . import typing from . import utils from .utils import mkvc diff --git a/simpeg/directives/directives.py b/simpeg/directives/directives.py index 1fd28d2467..86d8728f4b 100644 --- a/simpeg/directives/directives.py +++ b/simpeg/directives/directives.py @@ -1,8 +1,10 @@ +from __future__ import annotations # needed to use type operands in Python 3.8 import numpy as np import matplotlib.pyplot as plt import warnings import os import scipy.sparse as sp +from ..typing import RandomSeed from ..data_misfit import BaseDataMisfit from ..objective_function import ComboObjectiveFunction from ..maps import IdentityMap, Wires @@ -347,15 +349,17 @@ class BaseBetaEstimator(InversionDirective): ---------- beta0_ratio : float Desired ratio between data misfit and model objective function at initial beta iteration. - seed : int, None - Seed used for random sampling. + seed : None or :class:`~simpeg.typing.RandomSeed`, optional + Random seed used for random sampling. It can either be an int, + a predefined Numpy random number generator, or any valid input to + ``numpy.random.default_rng``. """ def __init__( self, beta0_ratio=1.0, - seed=None, + seed: RandomSeed | None = None, **kwargs, ): super().__init__(**kwargs) @@ -384,14 +388,20 @@ def seed(self): Returns ------- - int + int, numpy.random.Generator or None """ return self._seed @seed.setter def seed(self, value): - if value is not None: - value = validate_integer("seed", value, min_val=1) + try: + np.random.default_rng(value) + except TypeError as err: + msg = ( + "Unable to initialize the random number generator with " + f"a {type(value).__name__}" + ) + raise TypeError(msg) from err self._seed = value def validate(self, directive_list): @@ -418,8 +428,10 @@ class BetaEstimateMaxDerivative(BaseBetaEstimator): ---------- beta0_ratio: float Desired ratio between data misfit and model objective function at initial beta iteration. - seed : int, None - Seed used for random sampling. + seed : None or :class:`~simpeg.typing.RandomSeed`, optional + Random seed used for random sampling. It can either be an int, + a predefined Numpy random number generator, or any valid input to + ``numpy.random.default_rng``. Notes ----- @@ -449,19 +461,18 @@ class BetaEstimateMaxDerivative(BaseBetaEstimator): """ - def __init__(self, beta0_ratio=1.0, seed=None, **kwargs): + def __init__(self, beta0_ratio=1.0, seed: RandomSeed | None = None, **kwargs): super().__init__(beta0_ratio=beta0_ratio, seed=seed, **kwargs) def initialize(self): - if self.seed is not None: - np.random.seed(self.seed) + rng = np.random.default_rng(seed=self.seed) if self.verbose: print("Calculating the beta0 parameter.") m = self.invProb.model - x0 = np.random.rand(*m.shape) + x0 = rng.random(size=m.shape) phi_d_deriv = np.abs(self.dmisfit.deriv(m)).max() dm = x0 / x0.max() * m.max() phi_m_deriv = np.abs(self.reg.deriv(m + dm)).max() @@ -489,8 +500,10 @@ class BetaEstimate_ByEig(BaseBetaEstimator): Desired ratio between data misfit and model objective function at initial beta iteration. n_pw_iter : int Number of power iterations used to estimate largest eigenvalues. - seed : int, None - Seed used for random sampling. + seed : None or :class:`~simpeg.typing.RandomSeed`, optional + Random seed used for random sampling. It can either be an int, + a predefined Numpy random number generator, or any valid input to + ``numpy.random.default_rng``. Notes ----- @@ -519,7 +532,13 @@ class BetaEstimate_ByEig(BaseBetaEstimator): """ - def __init__(self, beta0_ratio=1.0, n_pw_iter=4, seed=None, **kwargs): + def __init__( + self, + beta0_ratio=1.0, + n_pw_iter=4, + seed: RandomSeed | None = None, + **kwargs, + ): super().__init__(beta0_ratio=beta0_ratio, seed=seed, **kwargs) self.n_pw_iter = n_pw_iter @@ -539,8 +558,7 @@ def n_pw_iter(self, value): self._n_pw_iter = validate_integer("n_pw_iter", value, min_val=1) def initialize(self): - if self.seed is not None: - np.random.seed(self.seed) + rng = np.random.default_rng(seed=self.seed) if self.verbose: print("Calculating the beta0 parameter.") @@ -551,11 +569,13 @@ def initialize(self): self.dmisfit, m, n_pw_iter=self.n_pw_iter, + seed=rng, ) reg_eigenvalue = eigenvalue_by_power_iteration( self.reg, m, n_pw_iter=self.n_pw_iter, + seed=rng, ) self.ratio = np.asarray(dm_eigenvalue / reg_eigenvalue) @@ -639,7 +659,13 @@ class AlphasSmoothEstimate_ByEig(InversionDirective): The highest eigenvalue are estimated through power iterations and Rayleigh quotient. """ - def __init__(self, alpha0_ratio=1.0, n_pw_iter=4, seed=None, **kwargs): + def __init__( + self, + alpha0_ratio=1.0, + n_pw_iter=4, + seed: RandomSeed | None = None, + **kwargs, + ): super().__init__(**kwargs) self.alpha0_ratio = alpha0_ratio self.n_pw_iter = n_pw_iter @@ -681,20 +707,25 @@ def seed(self): Returns ------- - int + int, numpy.random.Generator or None """ return self._seed @seed.setter def seed(self, value): - if value is not None: - value = validate_integer("seed", value, min_val=1) + try: + np.random.default_rng(value) + except TypeError as err: + msg = ( + "Unable to initialize the random number generator with " + f"a {type(value).__name__}" + ) + raise TypeError(msg) from err self._seed = value def initialize(self): """""" - if self.seed is not None: - np.random.seed(self.seed) + rng = np.random.default_rng(seed=self.seed) smoothness = [] smallness = [] @@ -729,6 +760,7 @@ def initialize(self): smallness[0], self.invProb.model, n_pw_iter=self.n_pw_iter, + seed=rng, ) self.alpha0_ratio = self.alpha0_ratio * np.ones(len(smoothness)) @@ -744,6 +776,7 @@ def initialize(self): obj, self.invProb.model, n_pw_iter=self.n_pw_iter, + seed=rng, ) ratio = smallness_eigenvalue / smooth_i_eigenvalue @@ -765,7 +798,13 @@ class ScalingMultipleDataMisfits_ByEig(InversionDirective): The highest eigenvalue are estimated through power iterations and Rayleigh quotient. """ - def __init__(self, chi0_ratio=None, n_pw_iter=4, seed=None, **kwargs): + def __init__( + self, + chi0_ratio=None, + n_pw_iter=4, + seed: RandomSeed | None = None, + **kwargs, + ): super().__init__(**kwargs) self.chi0_ratio = chi0_ratio self.n_pw_iter = n_pw_iter @@ -807,20 +846,25 @@ def seed(self): Returns ------- - int + int, numpy.random.Generator or None """ return self._seed @seed.setter def seed(self, value): - if value is not None: - value = validate_integer("seed", value, min_val=1) + try: + np.random.default_rng(value) + except TypeError as err: + msg = ( + "Unable to initialize the random number generator with " + f"a {type(value).__name__}" + ) + raise TypeError(msg) from err self._seed = value def initialize(self): """""" - if self.seed is not None: - np.random.seed(self.seed) + rng = np.random.default_rng(seed=self.seed) if self.verbose: print("Calculating the scaling parameter.") @@ -843,7 +887,7 @@ def initialize(self): dm_eigenvalue_list = [] for dm in self.dmisfit.objfcts: - dm_eigenvalue_list += [eigenvalue_by_power_iteration(dm, m)] + dm_eigenvalue_list += [eigenvalue_by_power_iteration(dm, m, seed=rng)] self.chi0 = self.chi0_ratio / np.r_[dm_eigenvalue_list] self.chi0 = self.chi0 / np.sum(self.chi0) diff --git a/simpeg/directives/sim_directives.py b/simpeg/directives/sim_directives.py index 0a3464717d..480cda76ee 100644 --- a/simpeg/directives/sim_directives.py +++ b/simpeg/directives/sim_directives.py @@ -245,8 +245,7 @@ def initialize(self): :rtype: float :return: beta0 """ - if self.seed is not None: - np.random.seed(self.seed) + rng = np.random.default_rng(seed=self.seed) if self.verbose: print("Calculating the beta0 parameter.") @@ -271,6 +270,7 @@ def initialize(self): dmis, m, n_pw_iter=self.n_pw_iter, + seed=rng, ) ) @@ -279,6 +279,7 @@ def initialize(self): reg, m, n_pw_iter=self.n_pw_iter, + seed=rng, ) ) diff --git a/simpeg/electromagnetics/natural_source/utils/test_utils.py b/simpeg/electromagnetics/natural_source/utils/test_utils.py index 9cde461e5a..878ddaea82 100644 --- a/simpeg/electromagnetics/natural_source/utils/test_utils.py +++ b/simpeg/electromagnetics/natural_source/utils/test_utils.py @@ -12,7 +12,6 @@ from ..simulation import Simulation3DPrimarySecondary from .data_utils import appResPhs -np.random.seed(1100) # Define the tolerances TOLr = 5e-2 TOLp = 5e-2 @@ -510,14 +509,15 @@ def getInputs(): return M, freqs, rx_loc, elev -def random(conds): +def random(conds, seed=42): """Returns a random model based on the inputs""" + rng = np.random.default_rng(seed=seed) M, freqs, rx_loc, elev = getInputs() - # Backround + # Background sigBG = np.ones(M.nC) * conds # Add randomness to the model (10% of the value). - sig = np.exp(np.log(sigBG) + np.random.randn(M.nC) * (conds) * 1e-1) + sig = np.exp(np.log(sigBG) + rng.random(size=M.nC) * (conds) * 1e-1) return (M, freqs, sig, sigBG, rx_loc) diff --git a/simpeg/simulation.py b/simpeg/simulation.py index cc751647cd..e1091b1997 100644 --- a/simpeg/simulation.py +++ b/simpeg/simulation.py @@ -2,6 +2,7 @@ Define simulation classes. """ +from __future__ import annotations # needed to use type operands in Python 3.8 import os import inspect import numpy as np @@ -12,6 +13,7 @@ from discretize.utils import unpack_widths, sdiag from . import props +from .typing import RandomSeed from .data import SyntheticData, Data from .survey import BaseSurvey from .utils import ( @@ -465,7 +467,7 @@ def make_synthetic_data( noise_floor=0.0, f=None, add_noise=False, - random_seed=None, + random_seed: RandomSeed | None = None, **kwargs, ): r"""Make synthetic data for the model and Gaussian noise provided. @@ -490,8 +492,10 @@ def make_synthetic_data( forward problem to obtain noiseless data. add_noise : bool Whether to add gaussian noise to the synthetic data or not. - random_seed : int, optional - Random seed to pass to :py:class:`numpy.random.default_rng`. + random_seed : None or :class:`~simpeg.typing.RandomSeed`, optional + Random seed used for random sampling. It can either be an int or + a predefined Numpy random number generator (see + ``numpy.random.default_rng``). Returns ------- @@ -511,8 +515,8 @@ def make_synthetic_data( dclean = self.dpred(m, f=f) if add_noise is True: - std = np.sqrt((relative_error * np.abs(dclean)) ** 2 + noise_floor**2) random_num_generator = np.random.default_rng(seed=random_seed) + std = np.sqrt((relative_error * np.abs(dclean)) ** 2 + noise_floor**2) noise = random_num_generator.normal(loc=0, scale=std, size=dclean.shape) dobs = dclean + noise else: diff --git a/simpeg/typing/__init__.py b/simpeg/typing/__init__.py new file mode 100644 index 0000000000..07975782bd --- /dev/null +++ b/simpeg/typing/__init__.py @@ -0,0 +1,61 @@ +""" +============================= +Typing (:mod:`simpeg.typing`) +============================= + +This module provides additional `PEP 484 `_ +type aliases used in ``simpeg``'s codebase. + +API +--- + +.. autosummary:: + :toctree: generated/ + + RandomSeed + +""" + +from __future__ import annotations +import numpy as np +import numpy.typing as npt +from typing import Union + +# Use try and except to support Python<3.10 +try: + from typing import TypeAlias + + RandomSeed: TypeAlias = Union[ + int, + npt.NDArray[np.int_], + np.random.SeedSequence, + np.random.BitGenerator, + np.random.Generator, + ] +except ImportError: + RandomSeed = Union[ + int, + npt.NDArray[np.int_], + np.random.SeedSequence, + np.random.BitGenerator, + np.random.Generator, + ] + +RandomSeed.__doc__ = """ +A ``typing.Union`` for random seeds and Numpy's random number generators. + +These type of variables can be used throughout ``simpeg`` to control random +states of functions and classes. These variables can either be an integer that +will be used as a ``seed`` to define a Numpy's :class:`numpy.random.Generator`, or +a predefined random number generator. + +Examples +-------- + +>>> import numpy as np +>>> from simpeg.typing import RandomSeed +>>> +>>> def my_function(seed: RandomSeed = None): +... rng = np.random.default_rng(seed=seed) +... ... +""" diff --git a/simpeg/utils/mat_utils.py b/simpeg/utils/mat_utils.py index 3614a15c2f..e57b4a34f0 100644 --- a/simpeg/utils/mat_utils.py +++ b/simpeg/utils/mat_utils.py @@ -1,5 +1,7 @@ +from __future__ import annotations # needed to use type operands in Python 3.8 import numpy as np from .code_utils import deprecate_function +from ..typing import RandomSeed from discretize.utils import ( # noqa: F401 Zero, Identity, @@ -129,7 +131,11 @@ def unique_rows(M): def eigenvalue_by_power_iteration( - combo_objfct, model, n_pw_iter=4, fields_list=None, seed=None + combo_objfct, + model, + n_pw_iter=4, + fields_list=None, + seed: RandomSeed | None = None, ): r"""Estimate largest eigenvalue in absolute value using power iteration. @@ -150,8 +156,10 @@ def eigenvalue_by_power_iteration( they will be evaluated within the function. If combo_objfct mixs data misfit and regularization terms, the list should contains simpeg.fields for the data misfit terms and None for the regularization term. - seed : int - Random seed for the initial random guess of eigenvector. + seed : None or :class:`~simpeg.typing.RandomSeed`, optional + Random seed for the initial random guess of eigenvector. It can either + be an int, a predefined Numpy random number generator, or any valid + input to ``numpy.random.default_rng``. Returns ------- @@ -176,12 +184,10 @@ def eigenvalue_by_power_iteration( selected from a uniform distribution. """ - - if seed is not None: - np.random.seed(seed) + rng = np.random.default_rng(seed=seed) # Initial guess for eigen-vector - x0 = np.random.rand(*model.shape) + x0 = rng.random(size=model.shape) x0 = x0 / np.linalg.norm(x0) # transform to ComboObjectiveFunction if required diff --git a/simpeg/utils/model_builder.py b/simpeg/utils/model_builder.py index 285fa976c1..c3a68abcec 100644 --- a/simpeg/utils/model_builder.py +++ b/simpeg/utils/model_builder.py @@ -1,3 +1,4 @@ +from __future__ import annotations # needed to use type operands in Python 3.8 import numpy as np import scipy.ndimage as ndi import scipy.sparse as sp @@ -5,6 +6,8 @@ from scipy.spatial import Delaunay from discretize.base import BaseMesh +from ..typing import RandomSeed + def add_block(cell_centers, model, p0, p1, prop_value): """Add a homogeneous block to an existing cell centered model @@ -414,15 +417,24 @@ def create_layers_model(cell_centers, layer_tops, layer_values): return model -def create_random_model(shape, seed=1000, anisotropy=None, its=100, bounds=None): - """Create random model by convolving a kernel with a uniformly distributed random model. +def create_random_model( + shape, + seed: RandomSeed | None = 1000, + anisotropy=None, + its=100, + bounds=None, +): + """ + Create random model by convolving a kernel with a uniformly distributed random model. Parameters ---------- shape : int or tuple of int Shape of the model. Can define a vector of size (n_cells) or define the dimensions of a tensor - seed : int, optional - If not None, sets the seed for the random uniform model that is convolved with the kernel. + seed : None or :class:`~simpeg.typing.RandomSeed`, optional + Random seed for random uniform model that is convolved with the kernel. + It can either be an int, a predefined Numpy random number generator, or + any valid input to ``numpy.random.default_rng``. anisotropy : numpy.ndarray this is the (*3*, *n*) blurring kernel that is used. its : int @@ -450,14 +462,11 @@ def create_random_model(shape, seed=1000, anisotropy=None, its=100, bounds=None) if bounds is None: bounds = [0, 1] - if seed is not None: - np.random.seed(seed) - print("Using a seed of: ", seed) - - if isinstance(shape, (int, float)): + if isinstance(shape, int): shape = (shape,) # make it a tuple for consistency - mr = np.random.rand(*shape) + rng = np.random.default_rng(seed=seed) + mr = rng.random(size=shape) if anisotropy is None: if len(shape) == 1: smth = np.array([1, 10.0, 1], dtype=float) diff --git a/tests/base/test_directives.py b/tests/base/test_directives.py index aa83489187..f6450eb586 100644 --- a/tests/base/test_directives.py +++ b/tests/base/test_directives.py @@ -397,6 +397,38 @@ def test_normalization_method_setter_invalid(self, normalization_method): d_temp.normalization_method = normalization_method +class TestSeedProperty: + """ + Test ``seed`` setter methods of directives. + """ + + directive_classes = ( + directives.AlphasSmoothEstimate_ByEig, + directives.BetaEstimate_ByEig, + directives.BetaEstimateMaxDerivative, + directives.ScalingMultipleDataMisfits_ByEig, + ) + + @pytest.mark.parametrize("directive_class", directive_classes) + @pytest.mark.parametrize( + "seed", + (42, np.random.default_rng(seed=1), np.array([1, 2])), + ids=("int", "rng", "array"), + ) + def test_valid_seed(self, directive_class, seed): + "Test if seed setter works as expected on valid seed arguments." + directive = directive_class(seed=seed) + assert directive.seed is seed + + @pytest.mark.parametrize("directive_class", directive_classes) + @pytest.mark.parametrize("seed", (42.1, np.array([1.0, 2.0]))) + def test_invalid_seed(self, directive_class, seed): + "Test if seed setter works as expected on valid seed arguments." + msg = "Unable to initialize the random number generator with " + with pytest.raises(TypeError, match=msg): + directive_class(seed=seed) + + class TestBetaEstimatorArguments: """ Test if arguments are assigned in beta estimator directives. diff --git a/tests/utils/test_mat_utils.py b/tests/utils/test_mat_utils.py index 3c8e1db1fd..5d85f51804 100644 --- a/tests/utils/test_mat_utils.py +++ b/tests/utils/test_mat_utils.py @@ -79,7 +79,7 @@ def test_dm_eigenvalue_by_power_iteration(self): field = self.dmis.simulation.fields(self.true_model) max_eigenvalue_numpy, _ = eigsh(dmis_matrix, k=1) max_eigenvalue_directive = eigenvalue_by_power_iteration( - self.dmis, self.true_model, fields_list=field, n_pw_iter=30 + self.dmis, self.true_model, fields_list=field, n_pw_iter=30, seed=42 ) passed = np.isclose(max_eigenvalue_numpy, max_eigenvalue_directive, rtol=1e-2) self.assertTrue(passed, True) @@ -92,7 +92,7 @@ def test_dm_eigenvalue_by_power_iteration(self): dmiscombo_matrix = 2 * self.G.T.dot(WtW.dot(self.G)) max_eigenvalue_numpy, _ = eigsh(dmiscombo_matrix, k=1) max_eigenvalue_directive = eigenvalue_by_power_iteration( - self.dmiscombo, self.true_model, n_pw_iter=30 + self.dmiscombo, self.true_model, n_pw_iter=30, seed=42 ) passed = np.isclose(max_eigenvalue_numpy, max_eigenvalue_directive, rtol=1e-2) self.assertTrue(passed, True) @@ -102,7 +102,7 @@ def test_reg_eigenvalue_by_power_iteration(self): reg_maxtrix = self.reg.deriv2(self.true_model) max_eigenvalue_numpy, _ = eigsh(reg_maxtrix, k=1) max_eigenvalue_directive = eigenvalue_by_power_iteration( - self.reg, self.true_model, n_pw_iter=100 + self.reg, self.true_model, n_pw_iter=100, seed=42 ) passed = np.isclose(max_eigenvalue_numpy, max_eigenvalue_directive, rtol=1e-2) self.assertTrue(passed, True) @@ -114,7 +114,7 @@ def test_combo_eigenvalue_by_power_iteration(self): combo_matrix = dmis_matrix + self.beta * reg_maxtrix max_eigenvalue_numpy, _ = eigsh(combo_matrix, k=1) max_eigenvalue_directive = eigenvalue_by_power_iteration( - self.mixcombo, self.true_model, n_pw_iter=100 + self.mixcombo, self.true_model, n_pw_iter=100, seed=42 ) passed = np.isclose(max_eigenvalue_numpy, max_eigenvalue_directive, rtol=1e-2) self.assertTrue(passed, True) From 5ed93bf5d296c286506c2d73b3ca277d5a9a76b8 Mon Sep 17 00:00:00 2001 From: "Devin C. Cowan" Date: Thu, 30 May 2024 08:53:25 -0700 Subject: [PATCH 013/194] Extend docstrings for FDEM and TDEM fields and 3D simulations (#1414) Extend docstrings for the 3D field and simulation classes for FDEM and TDEM modules. Provide mathematical background for the implementations. Update format of docstrings following numpydoc style. Add a few constructor methods for some classes to fix some missing properties in child classes. --- .gitignore | 2 + simpeg/directives/directives.py | 3 +- .../frequency_domain/__init__.py | 22 +- .../frequency_domain/fields.py | 343 ++- .../frequency_domain/simulation.py | 1765 ++++++++++++--- .../electromagnetics/time_domain/__init__.py | 22 +- simpeg/electromagnetics/time_domain/fields.py | 361 +++- .../time_domain/simulation.py | 1883 +++++++++++++++-- simpeg/fields.py | 205 +- simpeg/regularization/base.py | 2 +- simpeg/utils/pgi_utils.py | 22 +- 11 files changed, 3902 insertions(+), 728 deletions(-) diff --git a/.gitignore b/.gitignore index c274ffb66f..5609580098 100644 --- a/.gitignore +++ b/.gitignore @@ -53,10 +53,12 @@ docs/sg_execution_times.rst .vscode/* # paths to where data are downloaded +examples/04-dcip/test_url/* examples/20-published/bookpurnong_inversion/* examples/20-published/._bookpurnong_inversion examples/20-published/*.tar.gz examples/20-published/*.png +examples/20-published/Chile_GRAV_4_Miller/* tutorials/03-gravity/gravity/* tutorials/03-gravity/outputs/* tutorials/04-magnetics/magnetics/* diff --git a/simpeg/directives/directives.py b/simpeg/directives/directives.py index 86d8728f4b..75ad4834ab 100644 --- a/simpeg/directives/directives.py +++ b/simpeg/directives/directives.py @@ -2527,8 +2527,7 @@ class UpdateSensitivityWeights(InversionDirective): The dynamic range of RMS sensitivities can span many orders of magnitude. When computing sensitivity weights, thresholding is generally applied to set a minimum value. - Thresholding - ^^^^^^^^^^^^ + **Thresholding:** If **global** thresholding is applied, we add a constant :math:`\tau` to the RMS sensitivities: diff --git a/simpeg/electromagnetics/frequency_domain/__init__.py b/simpeg/electromagnetics/frequency_domain/__init__.py index 5ec6ac0a13..e2f9422515 100644 --- a/simpeg/electromagnetics/frequency_domain/__init__.py +++ b/simpeg/electromagnetics/frequency_domain/__init__.py @@ -1,10 +1,28 @@ -""" +r""" ============================================================================== Frequency-Domain EM (:mod:`simpeg.electromagnetics.frequency_domain`) ============================================================================== .. currentmodule:: simpeg.electromagnetics.frequency_domain -About ``frequency_domain`` +The ``frequency_domain`` module contains functionality for solving Maxwell's equations +in the frequency-domain for controlled sources. Where a :math:`+i\omega t` +Fourier convention is used, this module is used to solve problems of the form: + +.. math:: + \begin{align} + \nabla \times \vec{E} + i\omega \vec{B} &= - i \omega \vec{S}_m \\ + \nabla \times \vec{H} - \vec{J} &= \vec{S}_e + \end{align} + +where the constitutive relations between fields and fluxes are given by: + +* :math:`\vec{J} = (\sigma + i \omega \varepsilon) \vec{E}` +* :math:`\vec{B} = \mu \vec{H}` + +and: + +* :math:`\vec{S}_m` represents a magnetic source term +* :math:`\vec{S}_e` represents a current source term Simulations =========== diff --git a/simpeg/electromagnetics/frequency_domain/fields.py b/simpeg/electromagnetics/frequency_domain/fields.py index ecf0023c55..429829b58e 100644 --- a/simpeg/electromagnetics/frequency_domain/fields.py +++ b/simpeg/electromagnetics/frequency_domain/fields.py @@ -6,34 +6,60 @@ class FieldsFDEM(Fields): - r""" - Fancy Field Storage for a FDEM survey. Only one field type is stored for - each problem, the rest are computed. The fields object acts like an array - and is indexed by + r"""Base class for storing FDEM fields. - .. code-block:: python + FDEM fields classes are used to store the discrete solution of the fields for a + corresponding FDEM simulation; see :class:`.BaseFDEMSimulation`. + Only one field type (e.g. ``'e'``, ``'j'``, ``'h'``, or ``'b'``) is stored, but certain field types + can be rapidly computed and returned on the fly. The field type that is stored and the + field types that can be returned depend on the formulation used by the associated simulation class. + Once a field object has been created, the individual fields can be accessed; see the example below. - f = problem.fields(m) - e = f[source_list,'e'] - b = f[source_list,'b'] + Parameters + ---------- + simulation : .BaseFDEMSimulation + The FDEM simulation object used to compute the discrete field solution. - If accessing all sources for a given field, use the :code:`:` + Example + ------- + We want to access the fields for a discrete solution with :math:`\mathbf{e}` discretized + to edges and :math:`\mathbf{b}` discretized to faces. To extract the fields for all sources: .. code-block:: python - f = problem.fields(m) + f = simulation.fields(m) e = f[:,'e'] b = f[:,'b'] - The array returned will be size (``nE`` or ``nF``, ``nSrcs`` :math:`\times` - ``nFrequencies``) + The array ``e`` returned will have shape (`n_edges`, `n_sources`). And the array ``b`` + returned will have shape (`n_faces`, `n_sources`). We can also extract the fields for + a subset of the source list used for the simulation as follows: + + .. code-block:: python + + f = simulation.fields(m) + e = f[source_list,'e'] + b = f[source_list,'b'] + """ - knownFields = {} - _dtype = complex + def __init__(self, simulation): + dtype = complex + super().__init__(simulation=simulation, dtype=dtype) def _GLoc(self, fieldType): - """Grid location of the fieldType""" + """Return grid locations of the fieldType. + + Parameters + ---------- + fieldType : str + The field type. + + Returns + ------- + str + The grid locations. One of {``'CC'``, ``'N'``, ``'E'``, ``'F'``}. + """ return self.aliasFields[fieldType][1] def _e(self, solution, source_list): @@ -295,28 +321,66 @@ def _jDeriv(self, src, du_dm_v, v, adjoint=False): class Fields3DElectricField(FieldsFDEM): - """ - Fields object for Simulation3DElectricField. + r"""Fields class for storing 3D total electric field solutions. + + This class stores the total electric field solution computed using a + :class:`.frequency_domain.Simulation3DElectricField` + simulation object. This class can be used to extract the following quantities: + + * ``'e'``, ``'ePrimary'``, ``'eSecondary'`` and ``'j'`` on mesh edges. + * ``'h'``, ``'b'``, ``'bPrimary'`` and ``'bSecondary'`` on mesh faces. + * ``'charge'`` on mesh nodes. + * ``'charge_density'`` at cell centers. + + See the example below to learn how fields can be extracted from a + ``Fields3DElectricField`` object. + + Parameters + ---------- + simulation : .frequency_domain.Simulation3DElectricField + The FDEM simulation object associated with the fields. - :param discretize.base.BaseMesh mesh: mesh - :param simpeg.electromagnetics.frequency_domain.SurveyFDEM.Survey survey: survey + Example + ------- + The ``Fields3DElectricField`` object stores the total electric field solution + on mesh edges. To extract the discrete electric fields and magnetic flux + densities for all sources: + + .. code-block:: python + + f = simulation.fields(m) + e = f[:, 'e'] + b = f[:, 'b'] + + The array ``e`` returned will have shape (`n_edges`, `n_sources`). And the array ``b`` + returned will have shape (`n_faces`, `n_sources`). We can also extract the fields for + a subset of the source list used for the simulation as follows: + + .. code-block:: python + + f = simulation.fields(m) + e = f[source_list,'e'] + b = f[source_list,'b'] """ - knownFields = {"eSolution": "E"} - aliasFields = { - "e": ["eSolution", "E", "_e"], - "ePrimary": ["eSolution", "E", "_ePrimary"], - "eSecondary": ["eSolution", "E", "_eSecondary"], - "b": ["eSolution", "F", "_b"], - "bPrimary": ["eSolution", "F", "_bPrimary"], - "bSecondary": ["eSolution", "F", "_bSecondary"], - "j": ["eSolution", "E", "_j"], - "h": ["eSolution", "F", "_h"], - "charge": ["eSolution", "N", "_charge"], - "charge_density": ["eSolution", "CC", "_charge_density"], - } + def __init__(self, simulation): + super().__init__(simulation=simulation) + self._knownFields = {"eSolution": "E"} + self._aliasFields = { + "e": ["eSolution", "E", "_e"], + "ePrimary": ["eSolution", "E", "_ePrimary"], + "eSecondary": ["eSolution", "E", "_eSecondary"], + "b": ["eSolution", "F", "_b"], + "bPrimary": ["eSolution", "F", "_bPrimary"], + "bSecondary": ["eSolution", "F", "_bSecondary"], + "j": ["eSolution", "E", "_j"], + "h": ["eSolution", "F", "_h"], + "charge": ["eSolution", "N", "_charge"], + "charge_density": ["eSolution", "CC", "_charge_density"], + } def startup(self): + # Docstring inherited from parent. self._edgeCurl = self.simulation.mesh.edge_curl self._aveE2CCV = self.simulation.mesh.aveE2CCV self._aveF2CCV = self.simulation.mesh.aveF2CCV @@ -623,28 +687,67 @@ def _charge_density(self, eSolution, source_list): class Fields3DMagneticFluxDensity(FieldsFDEM): - """ - Fields object for Simulation3DMagneticFluxDensity. + r"""Fields class for storing 3D total magnetic flux density solutions. + + This class stores the total magnetic flux density solution computed using a + :class:`.frequency_domain.Simulation3DMagneticFluxDensity` + simulation object. This class can be used to extract the following quantities: + + * ``'b'``, ``'bPrimary'``, ``'bSecondary'`` and ``'h'`` on mesh faces. + * ``'e'``, ``'ePrimary'``, ``'eSecondary'`` and ``'j'`` on mesh edges. + * ``'charge'`` on mesh nodes. + * ``'charge_density'`` at cell centers. + + See the example below to learn how fields can be extracted from a + ``Fields3DMagneticFluxDensity`` object. + + Parameters + ---------- + simulation : .frequency_domain.Simulation3DMagneticFluxDensity + The FDEM simulation object associated with the fields. + + Example + ------- + The ``Fields3DMagneticFluxDensity`` object stores the total magnetic flux density solution + on mesh faces. To extract the discrete electric fields and magnetic flux + densities for all sources: + + .. code-block:: python + + f = simulation.fields(m) + e = f[:, 'e'] + b = f[:, 'b'] + + The array ``e`` returned will have shape (`n_edges`, `n_sources`). And the array ``b`` + returned will have shape (`n_faces`, `n_sources`). We can also extract the fields for + a subset of the source list used for the simulation as follows: + + .. code-block:: python + + f = simulation.fields(m) + e = f[source_list, 'e'] + b = f[source_list, 'b'] - :param discretize.base.BaseMesh mesh: mesh - :param simpeg.electromagnetics.frequency_domain.SurveyFDEM.Survey survey: survey """ - knownFields = {"bSolution": "F"} - aliasFields = { - "b": ["bSolution", "F", "_b"], - "bPrimary": ["bSolution", "F", "_bPrimary"], - "bSecondary": ["bSolution", "F", "_bSecondary"], - "e": ["bSolution", "E", "_e"], - "ePrimary": ["bSolution", "E", "_ePrimary"], - "eSecondary": ["bSolution", "E", "_eSecondary"], - "j": ["bSolution", "E", "_j"], - "h": ["bSolution", "F", "_h"], - "charge": ["bSolution", "N", "_charge"], - "charge_density": ["bSolution", "CC", "_charge_density"], - } + def __init__(self, simulation): + super().__init__(simulation=simulation) + self._knownFields = {"bSolution": "F"} + self._aliasFields = { + "b": ["bSolution", "F", "_b"], + "bPrimary": ["bSolution", "F", "_bPrimary"], + "bSecondary": ["bSolution", "F", "_bSecondary"], + "e": ["bSolution", "E", "_e"], + "ePrimary": ["bSolution", "E", "_ePrimary"], + "eSecondary": ["bSolution", "E", "_eSecondary"], + "j": ["bSolution", "E", "_j"], + "h": ["bSolution", "F", "_h"], + "charge": ["bSolution", "N", "_charge"], + "charge_density": ["bSolution", "CC", "_charge_density"], + } def startup(self): + # Docstring inherited from parent. self._edgeCurl = self.simulation.mesh.edge_curl self._MeSigma = self.simulation.MeSigma self._MeSigmaI = self.simulation.MeSigmaI @@ -953,28 +1056,65 @@ def _charge_density(self, bSolution, source_list): class Fields3DCurrentDensity(FieldsFDEM): - """ - Fields object for Simulation3DCurrentDensity. + r"""Fields class for storing 3D current density solutions. + + This class stores the total current density solution computed using a + :class:`.frequency_domain.Simulation3DCurrentDensity` + simulation object. This class can be used to extract the following quantities: + + * ``'j'``, ``'jPrimary'``, ``'jSecondary'`` and ``'e'`` on mesh faces. + * ``'h'``, ``'hPrimary'``, ``'hSecondary'`` and ``'b'`` on mesh edges. + * ``'charge'`` and ``'charge_density'`` at cell centers. + + See the example below to learn how fields can be extracted from a + ``Fields3DCurrentDensity`` object. + + Parameters + ---------- + simulation : .frequency_domain.Simulation3DCurrentDensity + The FDEM simulation object associated with the fields. + + Example + ------- + The ``Fields3DCurrentDensity`` object stores the total current density solution + on mesh faces. To extract the discrete current density and magnetic field: + + .. code-block:: python + + f = simulation.fields(m) + j = f[:, 'j'] + h = f[:, 'h'] + + The array ``j`` returned will have shape (`n_faces`, `n_sources`). And the array ``h`` + returned will have shape (`n_edges`, `n_sources`). We can also extract the fields for + a subset of the source list used for the simulation as follows: + + .. code-block:: python + + f = simulation.fields(m) + j = f[source_list, 'j'] + h = f[source_list, 'h'] - :param discretize.base.BaseMesh mesh: mesh - :param simpeg.electromagnetics.frequency_domain.SurveyFDEM.Survey survey: survey """ - knownFields = {"jSolution": "F"} - aliasFields = { - "j": ["jSolution", "F", "_j"], - "jPrimary": ["jSolution", "F", "_jPrimary"], - "jSecondary": ["jSolution", "F", "_jSecondary"], - "h": ["jSolution", "E", "_h"], - "hPrimary": ["jSolution", "E", "_hPrimary"], - "hSecondary": ["jSolution", "E", "_hSecondary"], - "e": ["jSolution", "F", "_e"], - "b": ["jSolution", "E", "_b"], - "charge": ["bSolution", "CC", "_charge"], - "charge_density": ["bSolution", "CC", "_charge_density"], - } + def __init__(self, simulation): + super().__init__(simulation=simulation) + self._knownFields = {"jSolution": "F"} + self._aliasFields = { + "j": ["jSolution", "F", "_j"], + "jPrimary": ["jSolution", "F", "_jPrimary"], + "jSecondary": ["jSolution", "F", "_jSecondary"], + "h": ["jSolution", "E", "_h"], + "hPrimary": ["jSolution", "E", "_hPrimary"], + "hSecondary": ["jSolution", "E", "_hSecondary"], + "e": ["jSolution", "F", "_e"], + "b": ["jSolution", "E", "_b"], + "charge": ["jSolution", "CC", "_charge"], + "charge_density": ["jSolution", "CC", "_charge_density"], + } def startup(self): + # Docstring inherited from parent. self._edgeCurl = self.simulation.mesh.edge_curl self._MeMu = self.simulation.MeMu self._MeMuI = self.simulation.MeMuI @@ -1344,28 +1484,65 @@ def _charge_density(self, jSolution, source_list): class Fields3DMagneticField(FieldsFDEM): - """ - Fields object for Simulation3DMagneticField. + r"""Fields class for storing 3D magnetic field solutions. + + This class stores the total magnetic field solution computed using a + :class:`.frequency_domain.Simulation3DMagneticField` + simulation object. This class can be used to extract the following quantities: + + * ``'h'``, ``'hPrimary'``, ``'hSecondary'`` and ``'b'`` on mesh edges. + * ``'j'``, ``'jPrimary'``, ``'jSecondary'`` and ``'e'`` on mesh faces. + * ``'charge'`` and ``'charge_density'`` at cell centers. + + See the example below to learn how fields can be extracted from a + ``Fields3DMagneticField`` object. + + Parameters + ---------- + simulation : .frequency_domain.Simulation3DMagneticField + The FDEM simulation object associated with the fields. + + Example + ------- + The ``Fields3DMagneticField`` object stores the total magnetic field solution + on mesh edges. To extract the discrete current density and magnetic field: + + .. code-block:: python + + f = simulation.fields(m) + j = f[:, 'j'] + h = f[:, 'h'] + + The array ``j`` returned will have shape (`n_faces`, `n_sources`). And the array ``h`` + returned will have shape (`n_edges`, `n_sources`). We can also extract the fields for + a subset of the source list used for the simulation as follows: + + .. code-block:: python + + f = simulation.fields(m) + j = f[source_list, 'j'] + h = f[source_list, 'h'] - :param discretize.base.BaseMesh mesh: mesh - :param simpeg.electromagnetics.frequency_domain.SurveyFDEM.Survey survey: survey """ - knownFields = {"hSolution": "E"} - aliasFields = { - "h": ["hSolution", "E", "_h"], - "hPrimary": ["hSolution", "E", "_hPrimary"], - "hSecondary": ["hSolution", "E", "_hSecondary"], - "j": ["hSolution", "F", "_j"], - "jPrimary": ["hSolution", "F", "_jPrimary"], - "jSecondary": ["hSolution", "F", "_jSecondary"], - "e": ["hSolution", "CCV", "_e"], - "b": ["hSolution", "CCV", "_b"], - "charge": ["hSolution", "CC", "_charge"], - "charge_density": ["hSolution", "CC", "_charge_density"], - } + def __init__(self, simulation): + super().__init__(simulation=simulation) + self._knownFields = {"hSolution": "E"} + self._aliasFields = { + "h": ["hSolution", "E", "_h"], + "hPrimary": ["hSolution", "E", "_hPrimary"], + "hSecondary": ["hSolution", "E", "_hSecondary"], + "j": ["hSolution", "F", "_j"], + "jPrimary": ["hSolution", "F", "_jPrimary"], + "jSecondary": ["hSolution", "F", "_jSecondary"], + "e": ["hSolution", "CCV", "_e"], + "b": ["hSolution", "CCV", "_b"], + "charge": ["hSolution", "CC", "_charge"], + "charge_density": ["hSolution", "CC", "_charge_density"], + } def startup(self): + # Docstring inherited from parent. self._edgeCurl = self.simulation.mesh.edge_curl self._MeMu = self.simulation.MeMu self._MeMuDeriv = self.simulation.MeMuDeriv diff --git a/simpeg/electromagnetics/frequency_domain/simulation.py b/simpeg/electromagnetics/frequency_domain/simulation.py index 19619a8f89..de1afecb68 100644 --- a/simpeg/electromagnetics/frequency_domain/simulation.py +++ b/simpeg/electromagnetics/frequency_domain/simulation.py @@ -20,43 +20,51 @@ class BaseFDEMSimulation(BaseEMSimulation): - r""" - We start by looking at Maxwell's equations in the electric - field (:math:`\mathbf{e}`) and the magnetic flux - density (:math:`\mathbf{b}`) - - .. math :: - - \mathbf{C} \mathbf{e} + i \omega \mathbf{b} = \mathbf{s_m} - {\mathbf{C}^{\top} \mathbf{M_{\mu^{-1}}^f} \mathbf{b} - - \mathbf{M_{\sigma}^e} \mathbf{e} = \mathbf{s_e}} - - if using the E-B formulation (:code:`Simulation3DElectricField` - or :code:`Simulation3DMagneticFluxDensity`). Note that in this case, - :math:`\mathbf{s_e}` is an integrated quantity. - - If we write Maxwell's equations in terms of - :math:`\mathbf{h}` and current density :math:`\mathbf{j}`. - - .. math :: - - \mathbf{C}^{\top} \mathbf{M_{\rho}^f} \mathbf{j} + - i \omega \mathbf{M_{\mu}^e} \mathbf{h} = \mathbf{s_m} - \mathbf{C} \mathbf{h} - \mathbf{j} = \mathbf{s_e} - - if using the H-J formulation (:code:`Simulation3DCurrentDensity` or - :code:`Simulation3DMagneticField`). Note that here, :math:`\mathbf{s_m}` is an - integrated quantity. - - The problem performs the elimination so that we are solving the system - for :math:`mathbf{e}`, :math:`mathbf{b}`, :math:`mathbf{j}` or - :math:`mathbf{h}`. - + r"""Base finite volume FDEM simulation class. + + This class is used to define properties and methods necessary for solving + 3D frequency-domain EM problems. For a :math:`+i\omega t` Fourier convention, + Maxwell's equations are expressed as: + + .. math:: + \begin{align} + \nabla \times \vec{E} + i\omega \vec{B} &= - i \omega \vec{S}_m \\ + \nabla \times \vec{H} - \vec{J} &= \vec{S}_e + \end{align} + + where the constitutive relations between fields and fluxes are given by: + + * :math:`\vec{J} = \sigma \vec{E}` + * :math:`\vec{B} = \mu \vec{H}` + + and: + + * :math:`\vec{S}_m` represents a magnetic source term + * :math:`\vec{S}_e` represents a current source term + + Child classes of ``BaseFDEMSimulation`` solve the above expression numerically + for various cases using mimetic finite volume. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + The mesh. + survey : .frequency_domain.survey.Survey + The frequency-domain EM survey. + forward_only : bool, optional + If ``True``, the factorization for the inverse of the system matrix at each + frequency is discarded after the fields are computed at that frequency. + If ``False``, the factorizations of the system matrices for all frequencies are stored. + permittivity : (n_cells,) numpy.ndarray, optional + Dielectric permittivity (F/m) defined on the entire mesh. If ``None``, electric displacement + is ignored. Please note that `permittivity` is not an invertible property, and that future + development will result in the deprecation of this propery. + storeJ : bool, optional + Whether to compute and store the sensitivity matrix. """ fieldsPair = FieldsFDEM permittivity = props.PhysicalProperty("Dielectric permittivity (F/m)") - # permittivity, permittivityMap, permittivityDeriv = props.Invertible("Dielectric permittivity (F/m)") def __init__( self, @@ -79,11 +87,12 @@ def __init__( @property def survey(self): - """The simulations survey. + """The FDEM survey object. Returns ------- - simpeg.electromagnetics.frequency_domain.survey.Survey + .frequency_domain.survey.Survey + The FDEM survey object. """ if self._survey is None: raise AttributeError("Simulation must have a survey set") @@ -98,11 +107,12 @@ def survey(self, value): @property def storeJ(self): - """Whether to store the sensitivity matrix + """Whether to compute and store the sensitivity matrix. Returns ------- bool + Whether to compute and store the sensitivity matrix. """ return self._storeJ @@ -112,11 +122,16 @@ def storeJ(self, value): @property def forward_only(self): - """If True, A-inverse not stored at each frequency in forward simulation. + """Whether to store the factorizations of the inverses of the system matrices. + + If ``True``, the factorization for the inverse of the system matrix at each + frequency is discarded after the fields are computed at that frequency. + If ``False``, the factorizations of the system matrices for all frequencies are stored. Returns ------- bool + Whether to store the factorizations of the inverses of the system matrices. """ return self._forward_only @@ -154,12 +169,17 @@ def _get_edge_admittivity_property_matrix( # @profile def fields(self, m=None): - """ - Solve the forward problem for the fields. + """Compute and return the fields for the model provided. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model. - :param numpy.ndarray m: inversion model (nP,) - :rtype: numpy.ndarray - :return f: forward solution + Returns + ------- + .frequency_domain.fields.FieldsFDEM + The FDEM fields object. """ if m is not None: @@ -186,15 +206,34 @@ def fields(self, m=None): # @profile def Jvec(self, m, v, f=None): - """ - Sensitivity times a vector. - - :param numpy.ndarray m: inversion model (nP,) - :param numpy.ndarray v: vector which we take sensitivity product with - (nP,) - :param simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM u: fields object - :rtype: numpy.ndarray - :return: Jv (ndata,) + r"""Compute the sensitivity matrix times a vector. + + Where :math:`\mathbf{d}` are the data, :math:`\mathbf{m}` are the model parameters, + and the sensitivity matrix is defined as: + + .. math:: + \mathbf{J} = \dfrac{\partial \mathbf{d}}{\partial \mathbf{m}} + + this method computes and returns the matrix-vector product: + + .. math:: + \mathbf{J v} + + for a given vector :math:`v`. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. + v : (n_param,) numpy.ndarray + The vector. + f : .frequency_domain.fields.FieldsFDEM, optional + Fields solved for all sources. + + Returns + ------- + (n_data,) numpy.ndarray + The sensitivity matrix times a vector. """ if f is None: @@ -216,14 +255,34 @@ def Jvec(self, m, v, f=None): return Jv.dobs def Jtvec(self, m, v, f=None): - """ - Sensitivity transpose times a vector + r"""Compute the adjoint sensitivity matrix times a vector. + + Where :math:`\mathbf{d}` are the data, :math:`\mathbf{m}` are the model parameters, + and the sensitivity matrix is defined as: + + .. math:: + \mathbf{J} = \dfrac{\partial \mathbf{d}}{\partial \mathbf{m}} + + this method computes and returns the matrix-vector product: - :param numpy.ndarray m: inversion model (nP,) - :param numpy.ndarray v: vector which we take adjoint product with (nP,) - :param simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM u: fields object - :rtype: numpy.ndarray - :return: Jv (ndata,) + .. math:: + \mathbf{J^T v} + + for a given vector :math:`v`. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. + v : (n_data,) numpy.ndarray + The vector. + f : .frequency_domain.fields.FieldsFDEM, optional + Fields solved for all sources. + + Returns + ------- + (n_param,) numpy.ndarray + The adjoint sensitivity matrix times a vector. """ if f is None: @@ -263,13 +322,27 @@ def Jtvec(self, m, v, f=None): return mkvc(Jtv) def getJ(self, m, f=None): - """ - Method to form full J given a model m + r"""Generate the full sensitivity matrix. + + This method generates and stores the full sensitivity matrix for the + model provided. I.e.: + + .. math:: + \mathbf{J} = \dfrac{\partial \mathbf{d}}{\partial \mathbf{m}} + + where :math:`\mathbf{d}` are the data and :math:`\mathbf{m}` are the model parameters. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. + f : .static.resistivity.fields.FieldsDC, optional + Fields solved for all sources. - :param numpy.ndarray m: inversion model (nP,) - :param simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM u: fields object - :rtype: numpy.ndarray - :return: J (ndata, nP) + Returns + ------- + (n_data, n_param) numpy.ndarray + The full sensitivity matrix. """ self.model = m @@ -317,14 +390,32 @@ def getJ(self, m, f=None): return self._Jmatrix def getJtJdiag(self, m, W=None, f=None): - """ - Return the diagonal of JtJ + r"""Return the diagonal of :math:`\mathbf{J^T J}`. + + Where :math:`\mathbf{d}` are the data and :math:`\mathbf{m}` are the model parameters, + the sensitivity matrix :math:`\mathbf{J}` is defined as: + + .. math:: + \mathbf{J} = \dfrac{\partial \mathbf{d}}{\partial \mathbf{m}} + + This method returns the diagonals of :math:`\mathbf{J^T J}`. When the + *W* input argument is used to include a diagonal weighting matrix + :math:`\mathbf{W}`, this method returns the diagonal of + :math:`\mathbf{W^T J^T J W}`. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. + W : (n_param, n_param) scipy.sparse.csr_matrix + A diagonal weighting matrix. + f : .frequency_domain.fields.FieldsFDEM, optional + Fields solved for all sources. - :param numpy.ndarray m: inversion model (nP,) - :param numpy.ndarray W: vector of weights (ndata,) - :param simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM u: fields object - :rtype: numpy.ndarray - :return: JtJ (nP,) + Returns + ------- + (n_param,) numpy.ndarray + The diagonals. """ self.model = m @@ -344,13 +435,33 @@ def getJtJdiag(self, m, W=None, f=None): # @profile def getSourceTerm(self, freq): - """ - Evaluates the sources for a given frequency and puts them in matrix - form + r"""Returns the discrete source terms for the frequency provided. + + This method computes and returns the discrete magnetic and electric source + terms for all soundings at the frequency provided. The exact shape and + implementation of the source terms when solving for the fields at each frequency + is formulation dependent. + + For definitions of the discrete magnetic (:math:`\mathbf{s_m}`) and electric + (:math:`\mathbf{s_e}`) source terms for each simulation, see the *Notes* sections + of the docstrings for: + + * :class:`.frequency_domain.Simulation3DElectricField` + * :class:`.frequency_domain.Simulation3DMagneticField` + * :class:`.frequency_domain.Simulation3DCurrentDensity` + * :class:`.frequency_domain.Simulation3DMagneticFluxDensity` - :param float freq: Frequency - :rtype: tuple - :return: (s_m, s_e) (nE or nF, nSrc) + Parameters + ---------- + freq : float + The frequency in Hz. + + Returns + ------- + s_m : numpy.ndarray + The magnetic sources terms. (n_faces, n_sources) for EB-formulations. (n_edges, n_sources) for HJ-formulations. + s_e : numpy.ndarray + The electric sources terms. (n_edges, n_sources) for EB-formulations. (n_faces, n_sources) for HJ-formulations. """ Srcs = self.survey.get_sources_by_frequency(freq) n_fields = sum(src._fields_per_source for src in Srcs) @@ -376,6 +487,16 @@ def getSourceTerm(self, freq): @property def deleteTheseOnModelUpdate(self): + """List of model-dependent attributes to clean upon model update. + + Some of the FDEM simulation's attributes are model-dependent. This property specifies + the model-dependent attributes that much be cleared when the model is updated. + + Returns + ------- + list of str + List of the model-dependent attributes to clean upon model update. + """ toDelete = super().deleteTheseOnModelUpdate return toDelete + ["_Jmatrix", "_gtgdiag"] @@ -386,28 +507,99 @@ def deleteTheseOnModelUpdate(self): class Simulation3DElectricField(BaseFDEMSimulation): - r""" - By eliminating the magnetic flux density using - - .. math :: - - \mathbf{b} = \frac{1}{i \omega}\left(-\mathbf{C} \mathbf{e} + - \mathbf{s_m}\right) - - - we can write Maxwell's equations as a second order system in - :math:`mathbf{e}` only: + r"""3D FDEM simulation in terms of the electric field. + + This simulation solves for the electric field at each frequency. + In this formulation, the electric fields are defined on mesh edges and the + magnetic flux density is defined on mesh faces; i.e. it is an EB formulation. + See the *Notes* section for a comprehensive description of the formulation. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + The mesh. + survey : .frequency_domain.survey.Survey + The frequency-domain EM survey. + forward_only : bool, optional + If ``True``, the factorization for the inverse of the system matrix at each + frequency is discarded after the fields are computed at that frequency. + If ``False``, the factorizations of the system matrices for all frequencies are stored. + permittivity : (n_cells,) numpy.ndarray, optional + Dielectric permittivity (F/m) defined on the entire mesh. If ``None``, electric displacement + is ignored. Please note that `permittivity` is not an invertible property, and that future + development will result in the deprecation of this propery. + storeJ : bool, optional + Whether to compute and store the sensitivity matrix. + + Notes + ----- + Here, we start with the Maxwell's equations in the frequency-domain where a + :math:`+i\omega t` Fourier convention is used: + + .. math:: + \begin{align} + &\nabla \times \vec{E} + i\omega \vec{B} = - i \omega \vec{S}_m \\ + &\nabla \times \vec{H} - \vec{J} = \vec{S}_e + \end{align} + + where :math:`\vec{S}_e` is an electric source term that defines a source current density, + and :math:`\vec{S}_m` magnetic source term that defines a source magnetic flux density. + We define the constitutive relations for the electrical conductivity :math:`\sigma` + and magnetic permeability :math:`\mu` as: + + .. math:: + \vec{J} &= \sigma \vec{E} \\ + \vec{H} &= \mu^{-1} \vec{B} + + We then take the inner products of all previous expressions with a vector test function :math:`\vec{u}`. + Through vector calculus identities and the divergence theorem, we obtain: + + .. math:: + & \int_\Omega \vec{u} \cdot (\nabla \times \vec{E}) \, dv + + i \omega \int_\Omega \vec{u} \cdot \vec{B} \, dv + = - i \omega \int_\Omega \vec{u} \cdot \vec{S}_m \, dv \\ + & \int_\Omega (\nabla \times \vec{u}) \cdot \vec{H} \, dv + - \oint_{\partial \Omega} \vec{u} \cdot (\vec{H} \times \hat{n}) \, da + - \int_\Omega \vec{u} \cdot \vec{J} \, dv + = \int_\Omega \vec{u} \cdot \vec{S}_j \, dv \\ + & \int_\Omega \vec{u} \cdot \vec{J} \, dv = \int_\Omega \vec{u} \cdot \sigma \vec{E} \, dv \\ + & \int_\Omega \vec{u} \cdot \vec{H} \, dv = \int_\Omega \vec{u} \cdot \mu^{-1} \vec{B} \, dv + + Assuming natural boundary conditions, the surface integral is zero. + + The above expressions are discretized in space according to the finite volume method. + The discrete electric fields :math:`\mathbf{e}` are defined on mesh edges, + and the discrete magnetic flux densities :math:`\mathbf{b}` are defined on mesh faces. + This implies :math:`\mathbf{j}` must be defined on mesh edges and :math:`\mathbf{h}` must + be defined on mesh faces. Where :math:`\mathbf{u_e}` and :math:`\mathbf{u_f}` represent + test functions discretized to edges and faces, respectively, we obtain the following + set of discrete inner-products: + + .. math:: + &\mathbf{u_f^T M_f C e} + i \omega \mathbf{u_f^T M_f b} = - i \omega \mathbf{u_f^T M_f s_m} \\ + &\mathbf{u_e^T C^T M_f h} - \mathbf{u_e^T M_e j} = \mathbf{u_e^T s_e} \\ + &\mathbf{u_e^T M_e j} = \mathbf{u_e^T M_{e \sigma} e} \\ + &\mathbf{u_f^T M_f h} = \mathbf{u_f^T M_{f \mu} b} + + where + + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_e}` is the edge inner-product matrix + * :math:`\mathbf{M_f}` is the face inner-product matrix + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces + + By cancelling like-terms and combining the discrete expressions to solve for the electric field, we obtain: + + .. math:: + \mathbf{A \, e} = \mathbf{q} + + where + + * :math:`\mathbf{A} = \mathbf{C^T M_{f\frac{1}{\mu}} C} + i\omega \mathbf{M_{e\sigma}}` + * :math:`\mathbf{q} = - i \omega \mathbf{s_e} - i \omega \mathbf{C^T M_{f\frac{1}{\mu}} s_m }` - .. math :: - - \left(\mathbf{C}^{\top} \mathbf{M_{\mu^{-1}}^f} \mathbf{C} + - i \omega \mathbf{M^e_{\sigma}} \right)\mathbf{e} = - \mathbf{C}^{\top} \mathbf{M_{\mu^{-1}}^f}\mathbf{s_m} - - i\omega\mathbf{M^e}\mathbf{s_e} - - which we solve for :math:`\mathbf{e}`. - - :param discretize.base.BaseMesh mesh: mesh """ _solutionType = "eSolution" @@ -415,17 +607,30 @@ class Simulation3DElectricField(BaseFDEMSimulation): fieldsPair = Fields3DElectricField def getA(self, freq): - r""" - System matrix + r"""System matrix for the frequency provided. + + This method returns the system matrix for the frequency provided: + + .. math:: + \mathbf{A} = \mathbf{C^T M_{f\frac{1}{\mu}} C} + i\omega \mathbf{M_{e\sigma}} - .. math :: + where - \mathbf{A} = \mathbf{C}^{\top} \mathbf{M_{\mu^{-1}}^f} \mathbf{C} - + i \omega \mathbf{M^e_{\sigma}} + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces - :param float freq: Frequency - :rtype: scipy.sparse.csr_matrix - :return: A + See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` + for a full description of the formulation. + + Parameters + ---------- + freq : float + The frequency in Hz. + + Returns + ------- + (n_edges, n_edges) sp.sparse.csr_matrix + The system matrix. """ MfMui = self.MfMui @@ -441,38 +646,98 @@ def getA(self, freq): return A def getADeriv_sigma(self, freq, u, v, adjoint=False): - r""" - Product of the derivative of our system matrix with respect to the - conductivity model and a vector - - .. math :: - - \frac{\mathbf{A}(\mathbf{m}) \mathbf{v}}{d \mathbf{m}_{\sigma}} = - i \omega \frac{d \mathbf{M^e_{\sigma}}(\mathbf{u})\mathbf{v} }{d\mathbf{m}} - - :param float freq: frequency - :param numpy.ndarray u: solution vector (nE,) - :param numpy.ndarray v: vector to take prodct with (nP,) or (nD,) for - adjoint - :param bool adjoint: adjoint? - :rtype: numpy.ndarray - :return: derivative of the system matrix times a vector (nP,) or - adjoint (nD,) + r"""Conductivity derivative operation for the system matrix times a vector. + + The system matrix at each frequency is given by: + + .. math:: + \mathbf{A} = \mathbf{C^T M_{f\frac{1}{\mu}} C} + i\omega \mathbf{M_{e\sigma}} + + where + + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces + + See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` + for a full description of the formulation. + + Where :math:`\mathbf{m}_\boldsymbol{\sigma}` are the set of model parameters defining the conductivity, + :math:`\mathbf{v}` is a vector and :math:`\mathbf{e}` is the discrete electric field solution, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A \, e})}{\partial \mathbf{m}_\boldsymbol{\sigma}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A \, e})}{\partial \mathbf{m}_\boldsymbol{\sigma}}^T \, \mathbf{v} + + Parameters + ---------- + freq : float + The frequency in Hz. + u : (n_edges,) numpy.ndarray + The solution for the fields for the current model at the specified frequency. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. """ dMe_dsig_v = self.MeSigmaDeriv(u, v, adjoint) return 1j * omega(freq) * dMe_dsig_v def getADeriv_mui(self, freq, u, v, adjoint=False): - r""" - Product of the derivative of the system matrix with respect to the - permeability model and a vector. + r"""Inverse permeability derivative operation for the system matrix times a vector. + + The system matrix at each frequency is given by: + + .. math:: + \mathbf{A} = \mathbf{C^T M_{f\frac{1}{\mu}} C} + i\omega \mathbf{M_{e\sigma}} + + where + + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces - .. math :: + See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` + for a full description of the formulation. - \frac{\mathbf{A}(\mathbf{m}) \mathbf{v}}{d \mathbf{m}_{\mu^{-1}} = - \mathbf{C}^{\top} \frac{d \mathbf{M^f_{\mu^{-1}}}\mathbf{v}}{d\mathbf{m}} + Where :math:`\mathbf{m}_\boldsymbol{\mu}` are the set of model parameters defining the permeability, + :math:`\mathbf{v}` is a vector and :math:`\mathbf{e}` is the discrete electric field solution, this method assumes + the discrete solution is fixed and returns + .. math:: + \frac{\partial (\mathbf{A \, e})}{\partial \mathbf{m}_\boldsymbol{\mu}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A \, e})}{\partial \mathbf{m}_\boldsymbol{\mu}}^T \, \mathbf{v} + + Parameters + ---------- + freq : float + The frequency in Hz. + u : (n_edges,) numpy.ndarray + The solution for the fields for the current model at the specified frequency. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. """ C = self.mesh.edge_curl @@ -483,6 +748,50 @@ def getADeriv_mui(self, freq, u, v, adjoint=False): return C.T * (self.MfMuiDeriv(C * u) * v) def getADeriv(self, freq, u, v, adjoint=False): + r"""Derivative operation for the system matrix times a vector. + + The system matrix at each frequency is given by: + + .. math:: + \mathbf{A} = \mathbf{C^T M_{f\frac{1}{\mu}} C} + i\omega \mathbf{M_{e\sigma}} + + where + + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces + + See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters defining the electromagnetic properties + :math:`\mathbf{v}` is a vector and :math:`\mathbf{e}` is the discrete electric field solution, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A \, e})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A \, e})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + freq : float + The frequency in Hz. + u : (n_edges,) numpy.ndarray + The solution for the fields for the current model at the specified frequency. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. + """ return ( self.getADeriv_sigma(freq, u, v, adjoint) + self.getADeriv_mui(freq, u, v, adjoint) @@ -490,18 +799,32 @@ def getADeriv(self, freq, u, v, adjoint=False): ) def getRHS(self, freq): - r""" - Right hand side for the system + r"""Right-hand sides for the given frequency. + + This method returns the right-hand sides for the frequency provided. + The right-hand side for each source is constructed according to: - .. math :: + .. math:: + \mathbf{q} = - i \omega \mathbf{s_e} - i \omega \mathbf{C^T M_{f\frac{1}{\mu}} s_m } + + where + + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrices for inverse permeabilities projected to faces - \mathbf{RHS} = \mathbf{C}^{\top} - \mathbf{M_{\mu^{-1}}^f}\mathbf{s_m} - - i\omega\mathbf{M_e}\mathbf{s_e} + See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` + for a full description of the formulation. - :param float freq: Frequency - :rtype: numpy.ndarray - :return: RHS (nE, nSrc) + Parameters + ---------- + freq : float + The frequency in Hz. + + Returns + ------- + (n_edges, n_sources) numpy.ndarray + The right-hand sides. """ s_m, s_e = self.getSourceTerm(freq) @@ -511,9 +834,49 @@ def getRHS(self, freq): return C.T * (MfMui * s_m) - 1j * omega(freq) * s_e def getRHSDeriv(self, freq, src, v, adjoint=False): - """ - Derivative of the Right-hand side with respect to the model. This - includes calls to derivatives in the sources + r"""Derivative of the right-hand side times a vector for a given source and frequency. + + The right-hand side for each source is constructed according to: + + .. math:: + \mathbf{q} = -i \omega \mathbf{s_e} - i \omega \mathbf{C^T M_{f\frac{1}{\mu}} s_m } + + where + + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrices for inverse permeabilities projected to faces + + See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters and :math:`\mathbf{v}` is a vector, + this method returns + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + freq : int + The frequency in Hz. + src : .frequency_domain.sources.BaseFDEMSrc + The FDEM source object. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of the right-hand sides times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. """ C = self.mesh.edge_curl @@ -534,26 +897,99 @@ def getRHSDeriv(self, freq, src, v, adjoint=False): class Simulation3DMagneticFluxDensity(BaseFDEMSimulation): - r""" - We eliminate :math:`\mathbf{e}` using - - .. math :: - - \mathbf{e} = \mathbf{M^e_{\sigma}}^{-1} \left(\mathbf{C}^{\top} - \mathbf{M_{\mu^{-1}}^f} \mathbf{b} - \mathbf{s_e}\right) + r"""3D FDEM simulation in terms of the magnetic flux field. + + This simulation solves for the magnetic flux density at each frequency. + In this formulation, the electric fields are defined on mesh edges and the + magnetic flux density is defined on mesh faces; i.e. it is an EB formulation. + See the *Notes* section for a comprehensive description of the formulation. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + The mesh. + survey : .frequency_domain.survey.Survey + The frequency-domain EM survey. + forward_only : bool, optional + If ``True``, the factorization for the inverse of the system matrix at each + frequency is discarded after the fields are computed at that frequency. + If ``False``, the factorizations of the system matrices for all frequencies are stored. + permittivity : (n_cells,) numpy.ndarray, optional + Dielectric permittivity (F/m) defined on the entire mesh. If ``None``, electric displacement + is ignored. Please note that `permittivity` is not an invertible property, and that future + development will result in the deprecation of this propery. + storeJ : bool, optional + Whether to compute and store the sensitivity matrix. + + Notes + ----- + Here, we start with the Maxwell's equations in the frequency-domain where a + :math:`+i\omega t` Fourier convention is used: + + .. math:: + \begin{align} + &\nabla \times \vec{E} + i\omega \vec{B} = - i \omega \vec{S}_m \\ + &\nabla \times \vec{H} - \vec{J} = \vec{S}_e + \end{align} + + where :math:`\vec{S}_e` is an electric source term that defines a source current density, + and :math:`\vec{S}_m` magnetic source term that defines a source magnetic flux density. + We define the constitutive relations for the electrical conductivity :math:`\sigma` + and magnetic permeability :math:`\mu` as: + + .. math:: + \vec{J} &= \sigma \vec{E} \\ + \vec{H} &= \mu^{-1} \vec{B} + + We then take the inner products of all previous expressions with a vector test function :math:`\vec{u}`. + Through vector calculus identities and the divergence theorem, we obtain: + + .. math:: + & \int_\Omega \vec{u} \cdot (\nabla \times \vec{E}) \, dv + + i \omega \int_\Omega \vec{u} \cdot \vec{B} \, dv + = - i \omega \int_\Omega \vec{u} \cdot \vec{S}_m \, dv \\ + & \int_\Omega (\nabla \times \vec{u}) \cdot \vec{H} \, dv + - \oint_{\partial \Omega} \vec{u} \cdot (\vec{H} \times \hat{n}) \, da + - \int_\Omega \vec{u} \cdot \vec{J} \, dv + = \int_\Omega \vec{u} \cdot \vec{S}_j \, dv \\ + & \int_\Omega \vec{u} \cdot \vec{J} \, dv = \int_\Omega \vec{u} \cdot \sigma \vec{E} \, dv \\ + & \int_\Omega \vec{u} \cdot \vec{H} \, dv = \int_\Omega \vec{u} \cdot \mu^{-1} \vec{B} \, dv + + Assuming natural boundary conditions, the surface integral is zero. + + The above expressions are discretized in space according to the finite volume method. + The discrete electric fields :math:`\mathbf{e}` are defined on mesh edges, + and the discrete magnetic flux densities :math:`\mathbf{b}` are defined on mesh faces. + This implies :math:`\mathbf{j}` must be defined on mesh edges and :math:`\mathbf{h}` must + be defined on mesh faces. Where :math:`\mathbf{u_e}` and :math:`\mathbf{u_f}` represent + test functions discretized to edges and faces, respectively, we obtain the following + set of discrete inner-products: + + .. math:: + &\mathbf{u_f^T M_f C e} + i \omega \mathbf{u_f^T M_f b} = - i \omega \mathbf{u_f^T M_f s_m} \\ + &\mathbf{u_e^T C^T M_f h} - \mathbf{u_e^T M_e j} = \mathbf{u_e^T s_e} \\ + &\mathbf{u_e^T M_e j} = \mathbf{u_e^T M_{e\sigma} e} \\ + &\mathbf{u_f^T M_f h} = \mathbf{u_f^T M_{f \mu} b} + + where + + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_e}` is the edge inner-product matrix + * :math:`\mathbf{M_f}` is the face inner-product matrix + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces + + By cancelling like-terms and combining the discrete expressions to solve for the magnetic flux density, we obtain: + + .. math:: + \mathbf{A \, b} = \mathbf{q} + + where + + * :math:`\mathbf{A} = \mathbf{C M_{e\sigma}^{-1} C^T M_{f\frac{1}{\mu}}} + i\omega \mathbf{I}` + * :math:`\mathbf{q} = \mathbf{C M_{e\sigma}^{-1} s_e} - i \omega \mathbf{s_m}` - and solve for :math:`\mathbf{b}` using: - - .. math :: - - \left(\mathbf{C} \mathbf{M^e_{\sigma}}^{-1} \mathbf{C}^{\top} - \mathbf{M_{\mu^{-1}}^f} + i \omega \right)\mathbf{b} = \mathbf{s_m} + - \mathbf{M^e_{\sigma}}^{-1}\mathbf{M^e}\mathbf{s_e} - - .. note :: - The inverse problem will not work with full anisotropy - - :param discretize.base.BaseMesh mesh: mesh """ _solutionType = "bSolution" @@ -561,17 +997,32 @@ class Simulation3DMagneticFluxDensity(BaseFDEMSimulation): fieldsPair = Fields3DMagneticFluxDensity def getA(self, freq): - r""" - System matrix + r"""System matrix for the frequency provided. + + This method returns the system matrix for the frequency provided: + + .. math:: + \mathbf{A} = \mathbf{C M_{e\sigma}^{-1} C^T M_{f\frac{1}{\mu}}} + i\omega \mathbf{I} + + where + + * :math:`\mathbf{I}` is the identity matrix + * :math:`\mathbf{C}` is the curl operator + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces - .. math :: + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` + for a full description of the formulation. - \mathbf{A} = \mathbf{C} \mathbf{M^e_{\sigma}}^{-1} - \mathbf{C}^{\top} \mathbf{M_{\mu^{-1}}^f} + i \omega + Parameters + ---------- + freq : float + The frequency in Hz. - :param float freq: Frequency - :rtype: scipy.sparse.csr_matrix - :return: A + Returns + ------- + (n_faces, n_faces) sp.sparse.csr_matrix + The system matrix. """ MfMui = self.MfMui @@ -592,23 +1043,51 @@ def getA(self, freq): return A def getADeriv_sigma(self, freq, u, v, adjoint=False): - r""" - Product of the derivative of our system matrix with respect to the - model and a vector - - .. math :: - - \frac{\mathbf{A}(\mathbf{m}) \mathbf{v}}{d \mathbf{m}} = - \mathbf{C} \frac{\mathbf{M^e_{\sigma}} \mathbf{v}}{d\mathbf{m}} - - :param float freq: frequency - :param numpy.ndarray u: solution vector (nF,) - :param numpy.ndarray v: vector to take prodct with (nP,) or (nD,) for - adjoint - :param bool adjoint: adjoint? - :rtype: numpy.ndarray - :return: derivative of the system matrix times a vector (nP,) or - adjoint (nD,) + r"""Conductivity derivative operation for the system matrix times a vector. + + The system matrix at each frequency is given by: + + .. math:: + \mathbf{A} = \mathbf{C M_{e\sigma}^{-1} C^T M_{f\frac{1}{\mu}}} + i\omega \mathbf{I} + + where + + * :math:`\mathbf{I}` is the identity matrix + * :math:`\mathbf{C}` is the curl operator + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}_\boldsymbol{\sigma}` are the set of model parameters defining the conductivity, + :math:`\mathbf{v}` is a vector and :math:`\mathbf{b}` is the discrete magnetic flux density solution, + this method assumes the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A \, b})}{\partial \mathbf{m}_\boldsymbol{\sigma}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A \, b})}{\partial \mathbf{m}_\boldsymbol{\sigma}}^T \, \mathbf{v} + + Parameters + ---------- + freq : float + The frequency in Hz. + u : (n_faces,) numpy.ndarray + The solution for the fields for the current model at the specified frequency. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. """ MfMui = self.MfMui @@ -625,6 +1104,52 @@ def getADeriv_sigma(self, freq, u, v, adjoint=False): # return C * (MeSigmaIDeriv * v) def getADeriv_mui(self, freq, u, v, adjoint=False): + r"""Inverse permeability derivative operation for the system matrix times a vector. + + The system matrix at each frequency is given by: + + .. math:: + \mathbf{A} = \mathbf{C M_{e\sigma}^{-1} C^T M_{f\frac{1}{\mu}}} + i\omega \mathbf{I} + + where + + * :math:`\mathbf{I}` is the identity matrix + * :math:`\mathbf{C}` is the curl operator + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}_\boldsymbol{\mu}` are the set of model parameters defining the permeability, + :math:`\mathbf{v}` is a vector and :math:`\mathbf{b}` is the discrete magnetic flux density solution, + this method assumes the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A \, b})}{\partial \mathbf{m}_\boldsymbol{\mu}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A \, b})}{\partial \mathbf{m}_\boldsymbol{\mu}}^T \, \mathbf{v} + + Parameters + ---------- + freq : float + The frequency in Hz. + u : (n_faces,) numpy.ndarray + The solution for the fields for the current model at the specified frequency. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. + """ MfMuiDeriv = self.MfMuiDeriv(u) MeSigmaI = self.MeSigmaI C = self.mesh.edge_curl @@ -634,6 +1159,52 @@ def getADeriv_mui(self, freq, u, v, adjoint=False): return C * (MeSigmaI * (C.T * (MfMuiDeriv * v))) def getADeriv(self, freq, u, v, adjoint=False): + r"""Derivative operation for the system matrix times a vector. + + The system matrix at each frequency is given by: + + .. math:: + \mathbf{A} = \mathbf{C M_{e\sigma}^{-1} C^T M_{f\frac{1}{\mu}}} + i\omega \mathbf{I} + + where + + * :math:`\mathbf{I}` is the identity matrix + * :math:`\mathbf{C}` is the curl operator + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters defining the electromagnetic properties, + :math:`\mathbf{v}` is a vector and :math:`\mathbf{b}` is the discrete magnetic flux density solution, + this method assumes the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A \, b})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A \, b})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + freq : float + The frequency in Hz. + u : (n_faces,) numpy.ndarray + The solution for the fields for the current model at the specified frequency. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. + """ if adjoint is True and self._makeASymmetric: v = self.MfMui * v @@ -647,17 +1218,32 @@ def getADeriv(self, freq, u, v, adjoint=False): return ADeriv def getRHS(self, freq): - r""" - Right hand side for the system + r"""Right-hand sides for the given frequency. + + This method returns the right-hand sides for the frequency provided. + The right-hand side for each source is constructed according to: + + .. math:: + \mathbf{q} = \mathbf{C M_{e\sigma}^{-1} s_e} - i \omega \mathbf{s_m } - .. math :: + where - \mathbf{RHS} = \mathbf{s_m} + - \mathbf{M^e_{\sigma}}^{-1}\mathbf{s_e} + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges - :param float freq: Frequency - :rtype: numpy.ndarray - :return: RHS (nE, nSrc) + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` + for a full description of the formulation. + + Parameters + ---------- + freq : float + The frequency in Hz. + + Returns + ------- + (n_faces, n_sources) numpy.ndarray + The right-hand sides. """ s_m, s_e = self.getSourceTerm(freq) @@ -679,15 +1265,49 @@ def getRHS(self, freq): return RHS def getRHSDeriv(self, freq, src, v, adjoint=False): - """ - Derivative of the right hand side with respect to the model - - :param float freq: frequency - :param simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM src: FDEM source - :param numpy.ndarray v: vector to take product with - :param bool adjoint: adjoint? - :rtype: numpy.ndarray - :return: product of rhs deriv with a vector + r"""Derivative of the right-hand side times a vector for a given source and frequency. + + The right-hand side for each source is constructed according to: + + .. math:: + \mathbf{q} = \mathbf{C M_{e\sigma}^{-1} s_e} - i \omega \mathbf{s_m } + + where + + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters and :math:`\mathbf{v}` is a vector, + this method returns + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + freq : int + The frequency in Hz. + src : .frequency_domain.sources.BaseFDEMSrc + The FDEM source object. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of the right-hand sides times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. """ C = self.mesh.edge_curl @@ -721,30 +1341,99 @@ def getRHSDeriv(self, freq, src, v, adjoint=False): class Simulation3DCurrentDensity(BaseFDEMSimulation): - r""" - We eliminate :math:`mathbf{h}` using - - .. math :: - - \mathbf{h} = \frac{1}{i \omega} \mathbf{M_{\mu}^e}^{-1} - \left(-\mathbf{C}^{\top} \mathbf{M_{\rho}^f} \mathbf{j} + - \mathbf{M^e} \mathbf{s_m} \right) - - - and solve for :math:`mathbf{j}` using + r"""3D FDEM simulation in terms of the current density. + + This simulation solves for the current density at each frequency. + In this formulation, the magnetic fields are defined on mesh edges and the + current densities are defined on mesh faces; i.e. it is an HJ formulation. + See the *Notes* section for a comprehensive description of the formulation. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + The mesh. + survey : .frequency_domain.survey.Survey + The frequency-domain EM survey. + forward_only : bool, optional + If ``True``, the factorization for the inverse of the system matrix at each + frequency is discarded after the fields are computed at that frequency. + If ``False``, the factorizations of the system matrices for all frequencies are stored. + permittivity : (n_cells,) numpy.ndarray, optional + Dielectric permittivity (F/m) defined on the entire mesh. If ``None``, electric displacement + is ignored. Please note that `permittivity` is not an invertible property, and that future + development will result in the deprecation of this propery. + storeJ : bool, optional + Whether to compute and store the sensitivity matrix. + + Notes + ----- + Here, we start with the Maxwell's equations in the frequency-domain where a + :math:`+i\omega t` Fourier convention is used: + + .. math:: + \begin{align} + &\nabla \times \vec{E} + i\omega \vec{B} = - i \omega \vec{S}_m \\ + &\nabla \times \vec{H} - \vec{J} = \vec{S}_e + \end{align} + + where :math:`\vec{S}_e` is an electric source term that defines a source current density, + and :math:`\vec{S}_m` magnetic source term that defines a source magnetic flux density. + For now, we neglect displacement current (the `permittivity` attribute is ``None``). + We define the constitutive relations for the electrical resistivity :math:`\rho` + and magnetic permeability :math:`\mu` as: + + .. math:: + \vec{E} &= \rho \vec{J} \\ + \vec{B} &= \mu \vec{H} + + We then take the inner products of all previous expressions with a vector test function :math:`\vec{u}`. + Through vector calculus identities and the divergence theorem, we obtain: + + .. math:: + & \int_\Omega (\nabla \times \vec{u}) \cdot \vec{E} \; dv + - \oint_{\partial \Omega} \vec{u} \cdot (\vec{E} \times \hat{n} ) \, da + + i \omega \int_\Omega \vec{u} \cdot \vec{B} \, dv + = - i \omega \int_\Omega \vec{u} \cdot \vec{S}_m dv \\ + & \int_\Omega \vec{u} \cdot (\nabla \times \vec{H} ) \, dv + - \int_\Omega \vec{u} \cdot \vec{J} \, dv = \int_\Omega \vec{u} \cdot \vec{S}_j \, dv\\ + & \int_\Omega \vec{u} \cdot \vec{E} \, dv = \int_\Omega \vec{u} \cdot \rho \vec{J} \, dv \\ + & \int_\Omega \vec{u} \cdot \vec{B} \, dv = \int_\Omega \vec{u} \cdot \mu \vec{H} \, dv + + Assuming natural boundary conditions, the surface integral is zero. + + The above expressions are discretized in space according to the finite volume method. + The discrete magnetic fields :math:`\mathbf{h}` are defined on mesh edges, + and the discrete current densities :math:`\mathbf{j}` are defined on mesh faces. + This implies :math:`\mathbf{b}` must be defined on mesh edges and :math:`\mathbf{e}` must + be defined on mesh faces. Where :math:`\mathbf{u_e}` and :math:`\mathbf{u_f}` represent + test functions discretized to edges and faces, respectively, we obtain the following + set of discrete inner-products: + + .. math:: + &\mathbf{u_e^T C^T M_f \, e } + i \omega \mathbf{u_e^T M_e b} = - i\omega \mathbf{u_e^T s_m} \\ + &\mathbf{u_f^T C \, h} - \mathbf{u_f^T j} = \mathbf{u_f^T s_e} \\ + &\mathbf{u_f^T M_f e} = \mathbf{u_f^T M_{f\rho} j} \\ + &\mathbf{u_e^T M_e b} = \mathbf{u_e^T M_{e \mu} h} + + where + + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_e}` is the edge inner-product matrix + * :math:`\mathbf{M_f}` is the face inner-product matrix + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges + + By cancelling like-terms and combining the discrete expressions to solve for the current density, we obtain: + + .. math:: + \mathbf{A \, j} = \mathbf{q} + + where + + * :math:`\mathbf{A} = \mathbf{C M_{e\mu}^{-1} C^T M_{f\rho} + i\omega \mathbf{I}` + * :math:`\mathbf{q} = - i \omega \mathbf{s_e} - i \omega \mathbf{C M_{e\mu}^{-1} s_m}` - .. math :: - - \left(\mathbf{C} \mathbf{M_{\mu}^e}^{-1} \mathbf{C}^{\top} - \mathbf{M_{\rho}^f} + i \omega\right)\mathbf{j} = - \mathbf{C} \mathbf{M_{\mu}^e}^{-1} \mathbf{M^e} \mathbf{s_m} - - i\omega\mathbf{s_e} - - .. note:: - - This implementation does not yet work with full anisotropy!! - - :param discretize.base.BaseMesh mesh: mesh """ _solutionType = "jSolution" @@ -760,17 +1449,31 @@ def __init__( self.permittivity = permittivity def getA(self, freq): - r""" - System matrix + r"""System matrix for the frequency provided. + + This method returns the system matrix for the frequency provided. + The system matrix at each frequency is given by: + + .. math:: + \mathbf{A} = \mathbf{C M_{e\mu}^{-1} C^T M_{f\rho}} + i\omega \mathbf{I} + + where + + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges - .. math :: + See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` + for a full description of the formulation. - \mathbf{A} = \mathbf{C} \mathbf{M^e_{\mu^{-1}}} - \mathbf{C}^{\top} \mathbf{M^f_{\sigma^{-1}}} + i\omega + Parameters + ---------- + freq : float + The frequency in Hz. - :param float freq: Frequency - :rtype: scipy.sparse.csr_matrix - :return: A + Returns + ------- + (n_faces, n_faces) sp.sparse.csr_matrix + The system matrix. """ MeMuI = self.MeMuI @@ -791,28 +1494,49 @@ def getA(self, freq): return A def getADeriv_rho(self, freq, u, v, adjoint=False): - r""" - Product of the derivative of our system matrix with respect to the - model and a vector - - In this case, we assume that electrical conductivity, :math:`\sigma` - is the physical property of interest (i.e. :math:`\sigma` = - model.transform). Then we want - - .. math :: - - \frac{\mathbf{A(\sigma)} \mathbf{v}}{d \mathbf{m}} = - \mathbf{C} \mathbf{M^e_{mu^{-1}}} \mathbf{C^{\top}} - \frac{d \mathbf{M^f_{\sigma^{-1}}}\mathbf{v} }{d \mathbf{m}} - - :param float freq: frequency - :param numpy.ndarray u: solution vector (nF,) - :param numpy.ndarray v: vector to take prodct with (nP,) or (nD,) for - adjoint - :param bool adjoint: adjoint? - :rtype: numpy.ndarray - :return: derivative of the system matrix times a vector (nP,) or - adjoint (nD,) + r"""Resistivity derivative operation for the system matrix times a vector. + + The system matrix at each frequency is given by: + + .. math:: + \mathbf{A} = \mathbf{C M_{e\mu}^{-1} C^T M_{f\rho}} + i\omega \mathbf{I} + + where + + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}_\boldsymbol{\rho}` are the set of model parameters defining the resistivity, + :math:`\mathbf{v}` is a vector and :math:`\mathbf{j}` is the discrete current density solution, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A \, j})}{\partial \mathbf{m}_\boldsymbol{\sigma}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A \, j})}{\partial \mathbf{m}_\boldsymbol{\sigma}}^T \, \mathbf{v} + + Parameters + ---------- + freq : float + The frequency in Hz. + u : (n_faces,) numpy.ndarray + The solution for the fields for the current model at the specified frequency. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. """ MeMuI = self.MeMuI @@ -824,6 +1548,50 @@ def getADeriv_rho(self, freq, u, v, adjoint=False): return C * (MeMuI * (C.T * (self.MfRhoDeriv(u, v, adjoint)))) def getADeriv_mu(self, freq, u, v, adjoint=False): + r"""Permeability derivative operation for the system matrix times a vector. + + The system matrix at each frequency is given by: + + .. math:: + \mathbf{A} = \mathbf{C M_{e\mu}^{-1} C^T M_{f\rho}} + i\omega \mathbf{I} + + where + + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}_\boldsymbol{\mu}` are the set of model parameters defining the permeability, + :math:`\mathbf{v}` is a vector and :math:`\mathbf{j}` is the discrete current density solution, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A \, j})}{\partial \mathbf{m}_\boldsymbol{\mu}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A \, j})}{\partial \mathbf{m}_\boldsymbol{\mu}}^T \, \mathbf{v} + + Parameters + ---------- + freq : float + The frequency in Hz. + u : (n_faces,) numpy.ndarray + The solution for the fields for the current model at the specified frequency. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. + """ C = self.mesh.edge_curl MfRho = self.MfRho @@ -840,6 +1608,50 @@ def getADeriv_mu(self, freq, u, v, adjoint=False): return Aderiv def getADeriv(self, freq, u, v, adjoint=False): + r"""Derivative operation for the system matrix times a vector. + + The system matrix at each frequency is given by: + + .. math:: + \mathbf{A} = \mathbf{C M_{e\mu}^{-1} C^T M_{f\rho}} + i\omega \mathbf{I} + + where + + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters defining the electromagnetic properties, + :math:`\mathbf{v}` is a vector and :math:`\mathbf{j}` is the discrete current density solution, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A \, j})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A \, j})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + freq : float + The frequency in Hz. + u : (n_faces,) numpy.ndarray + The solution for the fields for the current model at the specified frequency. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. + """ if adjoint and self._makeASymmetric: v = self.MfRho * v @@ -853,17 +1665,32 @@ def getADeriv(self, freq, u, v, adjoint=False): return ADeriv def getRHS(self, freq): - r""" - Right hand side for the system + r"""Right-hand sides for the given frequency. + + This method returns the right-hand sides for the frequency provided. + The right-hand side for each source is constructed according to: - .. math :: + .. math:: + \mathbf{q} = - i \omega \mathbf{s_e} - i \omega \mathbf{C M_{e\mu}^{-1} s_m} - \mathbf{RHS} = \mathbf{C} \mathbf{M_{\mu}^e}^{-1}\mathbf{s_m} - - i\omega \mathbf{s_e} + where - :param float freq: Frequency - :rtype: numpy.ndarray - :return: RHS (nE, nSrc) + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrices for permeabilities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` + for a full description of the formulation. + + Parameters + ---------- + freq : float + The frequency in Hz. + + Returns + ------- + (n_faces, n_sources) numpy.ndarray + The right-hand sides. """ s_m, s_e = self.getSourceTerm(freq) @@ -878,15 +1705,49 @@ def getRHS(self, freq): return RHS def getRHSDeriv(self, freq, src, v, adjoint=False): - """ - Derivative of the right hand side with respect to the model - - :param float freq: frequency - :param simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM src: FDEM source - :param numpy.ndarray v: vector to take product with - :param bool adjoint: adjoint? - :rtype: numpy.ndarray - :return: product of rhs deriv with a vector + r"""Derivative of the right-hand side times a vector for a given source and frequency. + + The right-hand side for each source is constructed according to: + + .. math:: + \mathbf{q} = - i \omega \mathbf{s_e} - i \omega \mathbf{C M_{e\mu}^{-1} s_m} + + where + + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrices for permeabilities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters and :math:`\mathbf{v}` is a vector, + this method returns + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + freq : int + The frequency in Hz. + src : .frequency_domain.sources.BaseFDEMSrc + The FDEM source object. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of the right-hand sides times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. """ # RHS = C * (MeMuI * s_m) - 1j * omega(freq) * s_e @@ -923,22 +1784,99 @@ def getRHSDeriv(self, freq, src, v, adjoint=False): class Simulation3DMagneticField(BaseFDEMSimulation): - r""" - We eliminate :math:`mathbf{j}` using - - .. math :: - - \mathbf{j} = \mathbf{C} \mathbf{h} - \mathbf{s_e} - - and solve for :math:`\mathbf{h}` using + r"""3D FDEM simulation in terms of the magnetic field. + + This simulation solves for the magnetic field at each frequency. + In this formulation, the magnetic fields are defined on mesh edges and the + current densities are defined on mesh faces; i.e. it is an HJ formulation. + See the *Notes* section for a comprehensive description of the formulation. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + The mesh. + survey : .frequency_domain.survey.Survey + The frequency-domain EM survey. + forward_only : bool, optional + If ``True``, the factorization for the inverse of the system matrix at each + frequency is discarded after the fields are computed at that frequency. + If ``False``, the factorizations of the system matrices for all frequencies are stored. + permittivity : (n_cells,) numpy.ndarray, optional + Dielectric permittivity (F/m) defined on the entire mesh. If ``None``, electric displacement + is ignored. Please note that `permittivity` is not an invertible property, and that future + development will result in the deprecation of this propery. + storeJ : bool, optional + Whether to compute and store the sensitivity matrix. + + Notes + ----- + Here, we start with the Maxwell's equations in the frequency-domain where a + :math:`+i\omega t` Fourier convention is used: + + .. math:: + \begin{align} + &\nabla \times \vec{E} + i\omega \vec{B} = - i \omega \vec{S}_m \\ + &\nabla \times \vec{H} - \vec{J} = \vec{S}_e + \end{align} + + where :math:`\vec{S}_e` is an electric source term that defines a source current density, + and :math:`\vec{S}_m` magnetic source term that defines a source magnetic flux density. + For now, we neglect displacement current (the `permittivity` attribute is ``None``). + We define the constitutive relations for the electrical resistivity :math:`\rho` + and magnetic permeability :math:`\mu` as: + + .. math:: + \vec{E} &= \rho \vec{J} \\ + \vec{B} &= \mu \vec{H} + + We then take the inner products of all previous expressions with a vector test function :math:`\vec{u}`. + Through vector calculus identities and the divergence theorem, we obtain: + + .. math:: + & \int_\Omega (\nabla \times \vec{u}) \cdot \vec{E} \; dv + - \oint_{\partial \Omega} \vec{u} \cdot (\vec{E} \times \hat{n} ) \, da + + i \omega \int_\Omega \vec{u} \cdot \vec{B} \, dv + = - i \omega \int_\Omega \vec{u} \cdot \vec{S}_m dv \\ + & \int_\Omega \vec{u} \cdot (\nabla \times \vec{H} ) \, dv + - \int_\Omega \vec{u} \cdot \vec{J} \, dv = \int_\Omega \vec{u} \cdot \vec{S}_j \, dv\\ + & \int_\Omega \vec{u} \cdot \vec{E} \, dv = \int_\Omega \vec{u} \cdot \rho \vec{J} \, dv \\ + & \int_\Omega \vec{u} \cdot \vec{B} \, dv = \int_\Omega \vec{u} \cdot \mu \vec{H} \, dv + + Assuming natural boundary conditions, the surface integral is zero. + + The above expressions are discretized in space according to the finite volume method. + The discrete magnetic fields :math:`\mathbf{h}` are defined on mesh edges, + and the discrete current densities :math:`\mathbf{j}` are defined on mesh faces. + This implies :math:`\mathbf{b}` must be defined on mesh edges and :math:`\mathbf{e}` must + be defined on mesh faces. Where :math:`\mathbf{u_e}` and :math:`\mathbf{u_f}` represent + test functions discretized to edges and faces, respectively, we obtain the following + set of discrete inner-products: + + .. math:: + &\mathbf{u_e^T C^T M_f \, e } + i \omega \mathbf{u_e^T M_e b} = - i\omega \mathbf{u_e^T s_m} \\ + &\mathbf{u_f^T C \, h} - \mathbf{u_f^T j} = \mathbf{u_f^T s_e} \\ + &\mathbf{u_f^T M_f e} = \mathbf{u_f^T M_{f\rho} j} \\ + &\mathbf{u_e^T M_e b} = \mathbf{u_e^T M_{e \mu} h} + + where + + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_e}` is the edge inner-product matrix + * :math:`\mathbf{M_f}` is the face inner-product matrix + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges + + By cancelling like-terms and combining the discrete expressions to solve for the magnetic field, we obtain: + + .. math:: + \mathbf{A \, h} = \mathbf{q} + + where + + * :math:`\mathbf{A} = \mathbf{C^T M_{f\rho} C} + i\omega \mathbf{M_{e\mu}}` + * :math:`\mathbf{q} = \mathbf{C^T M_{f\rho} s_e} - i\omega \mathbf{s_m}` - .. math :: - - \left(\mathbf{C}^{\top} \mathbf{M_{\rho}^f} \mathbf{C} + - i \omega \mathbf{M_{\mu}^e}\right) \mathbf{h} = \mathbf{M^e} - \mathbf{s_m} + \mathbf{C}^{\top} \mathbf{M_{\rho}^f} \mathbf{s_e} - - :param discretize.base.BaseMesh mesh: mesh """ _solutionType = "hSolution" @@ -946,19 +1884,31 @@ class Simulation3DMagneticField(BaseFDEMSimulation): fieldsPair = Fields3DMagneticField def getA(self, freq): - r""" - System matrix + r"""System matrix for the frequency provided. + + This method returns the system matrix for the frequency provided. + The system matrix at each frequency is given by: .. math:: + \mathbf{A} = \mathbf{C^T M_{f\rho} C} + i\omega \mathbf{M_{e\mu}} - \mathbf{A} = \mathbf{C}^{\top} \mathbf{M_{\rho}^f} \mathbf{C} + - i \omega \mathbf{M_{\mu}^e} + where + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges - :param float freq: Frequency - :rtype: scipy.sparse.csr_matrix - :return: A + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` + for a full description of the formulation. + Parameters + ---------- + freq : float + The frequency in Hz. + + Returns + ------- + (n_edges, n_edges) sp.sparse.csr_matrix + The system matrix. """ MeMu = self.MeMu @@ -974,24 +1924,49 @@ def getA(self, freq): return C.T.tocsr() * (Mfyhati * C) + 1j * omega(freq) * MeMu def getADeriv_rho(self, freq, u, v, adjoint=False): - r""" - Product of the derivative of our system matrix with respect to the - model and a vector + r"""Resistivity derivative operation for the system matrix times a vector. + + The system matrix at each frequency is given by: + + .. math:: + \mathbf{A} = \mathbf{C^T M_{f\rho} C} + i\omega \mathbf{M_{e\mu}} + + where + + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` + for a full description of the formulation. + + Where :math:`\mathbf{m}_\boldsymbol{\sigma}` are the set of model parameters defining the conductivity, + :math:`\mathbf{v}` is a vector and :math:`\mathbf{h}` is the discrete magnetic field solution, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A \, h})}{\partial \mathbf{m}_\boldsymbol{\sigma}} \, \mathbf{v} + + Or the adjoint operation .. math:: + \frac{\partial (\mathbf{A \, h})}{\partial \mathbf{m}_\boldsymbol{\sigma}}^T \, \mathbf{v} + + Parameters + ---------- + freq : float + The frequency in Hz. + u : (n_edges,) numpy.ndarray + The solution for the fields for the current model at the specified frequency. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. - \frac{\mathbf{A}(\mathbf{m}) \mathbf{v}}{d \mathbf{m}} = - \mathbf{C}^{\top}\frac{d \mathbf{M^f_{\rho}}\mathbf{v}} - {d\mathbf{m}} - - :param float freq: frequency - :param numpy.ndarray u: solution vector (nE,) - :param numpy.ndarray v: vector to take prodct with (nP,) or (nD,) for - adjoint - :param bool adjoint: adjoint? - :rtype: numpy.ndarray - :return: derivative of the system matrix times a vector (nP,) or - adjoint (nD,) + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. """ C = self.mesh.edge_curl if adjoint: @@ -999,6 +1974,50 @@ def getADeriv_rho(self, freq, u, v, adjoint=False): return C.T * self.MfRhoDeriv(C * u, v, adjoint) def getADeriv_mu(self, freq, u, v, adjoint=False): + r"""Permeability derivative operation for the system matrix times a vector. + + The system matrix at each frequency is given by: + + .. math:: + \mathbf{A} = \mathbf{C^T M_{f\rho} C} + i\omega \mathbf{M_{e\mu}} + + where + + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` + for a full description of the formulation. + + Where :math:`\mathbf{m}_\boldsymbol{\mu}` are the set of model parameters defining the permeability, + :math:`\mathbf{v}` is a vector and :math:`\mathbf{h}` is the discrete magnetic field solution, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A \, h})}{\partial \mathbf{m}_\boldsymbol{\mu}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A \, h})}{\partial \mathbf{m}_\boldsymbol{\mu}}^T \, \mathbf{v} + + Parameters + ---------- + freq : float + The frequency in Hz. + u : (n_edges,) numpy.ndarray + The solution for the fields for the current model at the specified frequency. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. + """ MeMuDeriv = self.MeMuDeriv(u) if adjoint is True: @@ -1007,23 +2026,82 @@ def getADeriv_mu(self, freq, u, v, adjoint=False): return 1j * omega(freq) * (MeMuDeriv * v) def getADeriv(self, freq, u, v, adjoint=False): + r"""Derivative operation for the system matrix times a vector. + + The system matrix at each frequency is given by: + + .. math:: + \mathbf{A} = \mathbf{C^T M_{f\rho} C} + i\omega \mathbf{M_{e\mu}} + + where + + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters defining the electromagnetic properties, + :math:`\mathbf{v}` is a vector and :math:`\mathbf{h}` is the discrete electric field solution, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A \, h})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A \, h})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + freq : float + The frequency in Hz. + u : (n_edges,) numpy.ndarray + The solution for the fields for the current model at the specified frequency. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. + """ return self.getADeriv_rho(freq, u, v, adjoint) + self.getADeriv_mu( freq, u, v, adjoint ) def getRHS(self, freq): - r""" - Right hand side for the system + r"""Right-hand sides for the given frequency. + + This method returns the right-hand sides for the frequency provided. + The right-hand side for each source is constructed according to: + + .. math:: + \mathbf{q} = \mathbf{C^T M_{f\rho} s_e} - i\omega \mathbf{s_m} + + where - .. math :: + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrices for permeabilities projected to edges + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrices for resistivities projected to faces - \mathbf{RHS} = \mathbf{M^e} \mathbf{s_m} + \mathbf{C}^{\top} - \mathbf{M_{\rho}^f} \mathbf{s_e} + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` + for a full description of the formulation. - :param float freq: Frequency - :rtype: numpy.ndarray - :return: RHS (nE, nSrc) + Parameters + ---------- + freq : float + The frequency in Hz. + Returns + ------- + (n_edges, n_sources) numpy.ndarray + The right-hand sides. """ s_m, s_e = self.getSourceTerm(freq) @@ -1039,15 +2117,50 @@ def getRHS(self, freq): return s_m + C.T * (Mfyhati * s_e) def getRHSDeriv(self, freq, src, v, adjoint=False): - """ - Derivative of the right hand side with respect to the model - - :param float freq: frequency - :param simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM src: FDEM source - :param numpy.ndarray v: vector to take product with - :param bool adjoint: adjoint? - :rtype: numpy.ndarray - :return: product of rhs deriv with a vector + r"""Derivative of the right-hand side times a vector for a given source and frequency. + + The right-hand side for each source is constructed according to: + + .. math:: + \mathbf{q} = \mathbf{C^T M_{f\rho} s_e} - i\omega \mathbf{s_m} + + where + + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrices for permeabilities projected to edges + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrices for resistivities projected to faces + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters and :math:`\mathbf{v}` is a vector, + this method returns + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + freq : int + The frequency in Hz. + src : .frequency_domain.sources.BaseFDEMSrc + The FDEM source object. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of the right-hand sides times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. """ _, s_e = src.eval(self) diff --git a/simpeg/electromagnetics/time_domain/__init__.py b/simpeg/electromagnetics/time_domain/__init__.py index 3fb8db7522..5891b847ce 100644 --- a/simpeg/electromagnetics/time_domain/__init__.py +++ b/simpeg/electromagnetics/time_domain/__init__.py @@ -1,10 +1,28 @@ -""" +r""" ============================================================================== Time-Domain EM (:mod:`simpeg.electromagnetics.time_domain`) ============================================================================== .. currentmodule:: simpeg.electromagnetics.time_domain -About ``time_domain`` +The ``time_domain`` module contains functionality for solving Maxwell's equations +in the time-domain for controlled sources. Here, electric displacement is ignored, +and functionality is used to solve: + +.. math:: + \begin{align} + \nabla \times \vec{e} + \frac{\partial \vec{b}}{\partial t} &= -\frac{\partial \vec{s}_m}{\partial t} \\ + \nabla \times \vec{h} - \vec{j} &= \vec{s}_e + \end{align} + +where the constitutive relations between fields and fluxes are given by: + +* :math:`\vec{j} = \sigma \vec{e}` +* :math:`\vec{b} = \mu \vec{h}` + +and: + +* :math:`\vec{s}_m` represents a magnetic source term +* :math:`\vec{s}_e` represents a current source term Simulations =========== diff --git a/simpeg/electromagnetics/time_domain/fields.py b/simpeg/electromagnetics/time_domain/fields.py index 384432c736..927a35e3b1 100644 --- a/simpeg/electromagnetics/time_domain/fields.py +++ b/simpeg/electromagnetics/time_domain/fields.py @@ -6,31 +6,48 @@ class FieldsTDEM(TimeFields): - r""" - Fancy Field Storage for a TDEM simulation. Only one field type is stored for - each problem, the rest are computed. The fields obejct acts like an array - and is indexed by + r"""Base class for storing TDEM fields. + + TDEM fields classes are used to store the discrete solution of the fields for a + corresponding TDEM simulation; see :class:`.time_domain.BaseTDEMSimulation`. + Only one field type (e.g. ``'e'``, ``'j'``, ``'h'``, ``'b'``) is stored, but certain field types + can be rapidly computed and returned on the fly. The field type that is stored and the + field types that can be returned depend on the formulation used by the associated simulation class. + Once a field object has been created, the individual fields can be accessed; see the example below. + + Parameters + ---------- + simulation : .time_domain.BaseTDEMSimulation + The TDEM simulation object used to compute the discrete field solution. + + Example + ------- + We want to access the fields for a discrete solution with :math:`\mathbf{e}` discretized + to edges and :math:`\mathbf{b}` discretized to faces. To extract the fields for all sources + and all time steps: .. code-block:: python - f = problem.fields(m) - e = f[source_list,'e'] - b = f[source_list,'b'] + f = simulation.fields(m) + e = f[:, 'e', :] + b = f[:, 'b', :] - If accessing all sources for a given field, use the :code:`:` + The array ``e`` returned will have shape (`n_edges`, `n_sources`, `n_steps`). + And the array ``b`` returned will have shape (`n_faces`, `n_sources`, `n_steps`). + We can also extract the fields for + a subset of the source list used for the simulation and/or a subset of the time steps as follows: .. code-block:: python - f = problem.fields(m) - e = f[:,'e'] - b = f[:,'b'] + f = simulation.fields(m) + e = f[source_list, 'e', t_inds] + b = f[source_list, 'b', t_inds] - The array returned will be size (nE or nF, nSrcs :math:`\times` - nFrequencies) """ - knownFields = {} - dtype = float + def __init__(self, simulation): + dtype = float + super().__init__(simulation=simulation, dtype=dtype) def _GLoc(self, fieldType): """Grid location of the fieldType""" @@ -86,49 +103,105 @@ def _jDeriv(self, tInd, src, dun_dm_v, v, adjoint=False): class FieldsDerivativesEB(FieldsTDEM): - """ - A fields object for satshing derivs in the EB formulation + r"""Field class for stashing derivatives for EB formulations. + + Parameters + ---------- + simulation : .time_domain.BaseTDEMSimulation + The TDEM simulation object associated with the fields. + """ - knownFields = { - "bDeriv": "F", - "eDeriv": "E", - "hDeriv": "F", - "jDeriv": "E", - "dbdtDeriv": "F", - "dhdtDeriv": "F", - } + def __init__(self, simulation): + super().__init__(simulation=simulation) + self._knownFields = { + "bDeriv": "F", + "eDeriv": "E", + "hDeriv": "F", + "jDeriv": "E", + "dbdtDeriv": "F", + "dhdtDeriv": "F", + } class FieldsDerivativesHJ(FieldsTDEM): - """ - A fields object for satshing derivs in the HJ formulation + r"""Field class for stashing derivatives for HJ formulations. + + Parameters + ---------- + simulation : .time_domain.BaseTDEMSimulation + The TDEM simulation object associated with the fields. + """ - knownFields = { - "bDeriv": "E", - "eDeriv": "F", - "hDeriv": "E", - "jDeriv": "F", - "dbdtDeriv": "E", - "dhdtDeriv": "E", - } + def __init__(self, simulation): + super().__init__(simulation=simulation) + self._knownFields = { + "bDeriv": "E", + "eDeriv": "F", + "hDeriv": "E", + "jDeriv": "F", + "dbdtDeriv": "E", + "dhdtDeriv": "E", + } class Fields3DMagneticFluxDensity(FieldsTDEM): - """Field Storage for a TDEM simulation.""" - - knownFields = {"bSolution": "F"} - aliasFields = { - "b": ["bSolution", "F", "_b"], - "h": ["bSolution", "F", "_h"], - "e": ["bSolution", "E", "_e"], - "j": ["bSolution", "E", "_j"], - "dbdt": ["bSolution", "F", "_dbdt"], - "dhdt": ["bSolution", "F", "_dhdt"], - } + r"""Fields class for storing 3D total magnetic flux density solutions. + + This class stores the total magnetic flux density solution computed using a + :class:`.time_domain.Simulation3DMagneticFluxDensity` + simulation object. This class can be used to extract the following quantities: + + * ``'b'``, ``'h'``, ``'dbdt'`` and ``'dhdt'`` on mesh faces. + * ``'e'`` and ``'j'`` on mesh edges. + + See the example below to learn how fields can be extracted from a + ``Fields3DMagneticFluxDensity`` object. + + Parameters + ---------- + simulation : .time_domain.Simulation3DMagneticFluxDensity + The TDEM simulation object associated with the fields. + + Example + ------- + The ``Fields3DMagneticFluxDensity`` object stores the total magnetic flux density solution + on mesh faces. To extract the discrete electric fields and magnetic flux + densities for all sources and time-steps: + + .. code-block:: python + + f = simulation.fields(m) + e = f[:, 'e', :] + b = f[:, 'b', :] + + The array ``e`` returned will have shape (`n_edges`, `n_sources`, `n_steps`). + And the array ``b`` returned will have shape (`n_faces`, `n_sources`, `n_steps`). + We can also extract the fields for a subset of the sources and time-steps as follows: + + .. code-block:: python + + f = simulation.fields(m) + e = f[source_list, 'e', t_inds] + b = f[source_list, 'b', t_inds] + + """ + + def __init__(self, simulation): + super().__init__(simulation=simulation) + self._knownFields = {"bSolution": "F"} + self._aliasFields = { + "b": ["bSolution", "F", "_b"], + "h": ["bSolution", "F", "_h"], + "e": ["bSolution", "E", "_e"], + "j": ["bSolution", "E", "_j"], + "dbdt": ["bSolution", "F", "_dbdt"], + "dhdt": ["bSolution", "F", "_dhdt"], + } def startup(self): + # Docstring inherited from parent. self._times = self.simulation.times self._MeSigma = self.simulation.MeSigma self._MeSigmaI = self.simulation.MeSigmaI @@ -273,19 +346,61 @@ def _dhdtDeriv_m(self, tInd, src, v, adjoint=False): class Fields3DElectricField(FieldsTDEM): - """Fancy Field Storage for a TDEM simulation.""" - - knownFields = {"eSolution": "E"} - aliasFields = { - "e": ["eSolution", "E", "_e"], - "j": ["eSolution", "E", "_j"], - "b": ["eSolution", "F", "_b"], - # 'h': ['eSolution', 'F', '_h'], - "dbdt": ["eSolution", "F", "_dbdt"], - "dhdt": ["eSolution", "F", "_dhdt"], - } + r"""Fields class for storing 3D total electric field solutions. + + This class stores the total electric field solution computed using a + :class:`.time_domain.Simulation3DElectricField` + simulation object. This class can be used to extract the following quantities: + + * ``'e'`` and ``'j'`` on mesh edges. + * ``'b'``, ``'dbdt'`` and ``'dhdt'`` on mesh faces. + + See the example below to learn how fields can be extracted from a + ``Fields3DElectricField`` object. + + Parameters + ---------- + simulation : .time_domain.Simulation3DElectricField + The TDEM simulation object associated with the fields. + + Example + ------- + The ``Fields3DElectricField`` object stores the total electric field solution + on mesh edges. To extract the discrete electric fields and db/dt + for all sources and time-steps: + + .. code-block:: python + + f = simulation.fields(m) + e = f[:, 'e', :] + dbdt = f[:, 'dbdt', :] + + The array ``e`` returned will have shape (`n_edges`, `n_sources`, `n_steps`). + And the array ``dbdt`` returned will have shape (`n_faces`, `n_sources`, `n_steps`). + We can also extract the fields for a subset of the sources and time-steps as follows: + + .. code-block:: python + + f = simulation.fields(m) + e = f[source_list, 'e', t_inds] + dbdt = f[source_list, 'dbdt', t_inds] + + """ + + def __init__(self, simulation): + super().__init__(simulation=simulation) + self._knownFields = {"eSolution": "E"} + self._aliasFields = { + "e": ["eSolution", "E", "_e"], + "j": ["eSolution", "E", "_j"], + "b": ["eSolution", "F", "_b"], + # 'h': ['eSolution', 'F', '_h'], + "dbdt": ["eSolution", "F", "_dbdt"], + "dhdt": ["eSolution", "F", "_dhdt"], + } def startup(self): + # Docstring inherited from parent. self._times = self.simulation.times self._MeSigma = self.simulation.MeSigma self._MeSigmaI = self.simulation.MeSigmaI @@ -392,20 +507,63 @@ def _dhdtDeriv_m(self, tInd, src, v, adjoint=False): class Fields3DMagneticField(FieldsTDEM): - """Fancy Field Storage for a TDEM simulation.""" - - knownFields = {"hSolution": "E"} - aliasFields = { - "h": ["hSolution", "E", "_h"], - "b": ["hSolution", "E", "_b"], - "dhdt": ["hSolution", "E", "_dhdt"], - "dbdt": ["hSolution", "E", "_dbdt"], - "j": ["hSolution", "F", "_j"], - "e": ["hSolution", "F", "_e"], - "charge": ["hSolution", "CC", "_charge"], - } + r"""Fields class for storing 3D total magnetic field solutions. + + This class stores the total magnetic field solution computed using a + :class:`.time_domain.Simulation3DElectricField` + simulation object. This class can be used to extract the following quantities: + + * ``'h'``, ``'b'``, ``'dbdt'`` and ``'dbdt'`` on mesh edges. + * ``'j'`` and ``'e'`` on mesh faces. + * ``'charge'`` at cell centers. + + See the example below to learn how fields can be extracted from a + ``Fields3DMagneticField`` object. + + Parameters + ---------- + simulation : .time_domain.Simulation3DMagneticField + The TDEM simulation object associated with the fields. + + Example + ------- + The ``Fields3DMagneticField`` object stores the total magnetic field solution + on mesh edges. To extract the discrete magnetic fields and current density + for all sources and time-steps: + + .. code-block:: python + + f = simulation.fields(m) + h = f[:, 'h', :] + j = f[:, 'j', :] + + The array ``h`` returned will have shape (`n_edges`, `n_sources`, `n_steps`). + And the array ``j`` returned will have shape (`n_faces`, `n_sources`, `n_steps`). + We can also extract the fields for a subset of the sources and time-steps as follows: + + .. code-block:: python + + f = simulation.fields(m) + h = f[source_list, 'e', t_inds] + j = f[source_list, 'j', t_inds] + + """ + + def __init__(self, simulation): + super().__init__(simulation=simulation) + self._knownFields = {"hSolution": "E"} + self._aliasFields = { + "h": ["hSolution", "E", "_h"], + "b": ["hSolution", "E", "_b"], + "dhdt": ["hSolution", "E", "_dhdt"], + "dbdt": ["hSolution", "E", "_dbdt"], + "j": ["hSolution", "F", "_j"], + "e": ["hSolution", "F", "_e"], + "charge": ["hSolution", "CC", "_charge"], + } def startup(self): + # Docstring inherited from parent. self._times = self.simulation.times self._edgeCurl = self.simulation.mesh.edge_curl self._MeMuI = self.simulation.MeMuI @@ -559,19 +717,62 @@ def _charge(self, hSolution, source_list, tInd): class Fields3DCurrentDensity(FieldsTDEM): - """Fancy Field Storage for a TDEM simulation.""" - - knownFields = {"jSolution": "F"} - aliasFields = { - "dhdt": ["jSolution", "E", "_dhdt"], - "dbdt": ["jSolution", "E", "_dbdt"], - "j": ["jSolution", "F", "_j"], - "e": ["jSolution", "F", "_e"], - "charge": ["jSolution", "CC", "_charge"], - "charge_density": ["jSolution", "CC", "_charge_density"], - } + r"""Fields class for storing 3D current density solutions. + + This class stores the total current density solution computed using a + :class:`.time_domain.Simulation3DCurrentDensity` + simulation object. This class can be used to extract the following quantities: + + * ``'j'`` and ``'e'`` on mesh faces. + * ``'dbdt'`` and ``'dhdt'`` on mesh edges. + * ``'charge'`` and ``'charge_density'`` at cell centers. + + See the example below to learn how fields can be extracted from a + ``Fields3DCurrentDensity`` object. + + Parameters + ---------- + simulation : .time_domain.Simulation3DCurrentDensity + The TDEM simulation object associated with the fields. + + Example + ------- + The ``Fields3DCurrentDensity`` object stores the total current density solution + on mesh faces. To extract the discrete current densities and magnetic fields + for all sources and time-steps: + + .. code-block:: python + + f = simulation.fields(m) + j = f[:, 'j', :] + h = f[:, 'h', :] + + The array ``j`` returned will have shape (`n_faces`, `n_sources`, `n_steps`). + And the array ``h`` returned will have shape (`n_edges`, `n_sources`, `n_steps`). + We can also extract the fields for a subset of the sources and time-steps as follows: + + .. code-block:: python + + f = simulation.fields(m) + j = f[source_list, 'j', t_inds] + h = f[source_list, 'h', t_inds] + + """ + + def __init__(self, simulation): + super().__init__(simulation=simulation) + self._knownFields = {"jSolution": "F"} + self._aliasFields = { + "dhdt": ["jSolution", "E", "_dhdt"], + "dbdt": ["jSolution", "E", "_dbdt"], + "j": ["jSolution", "F", "_j"], + "e": ["jSolution", "F", "_e"], + "charge": ["jSolution", "CC", "_charge"], + "charge_density": ["jSolution", "CC", "_charge_density"], + } def startup(self): + # Docstring inherited from parent. self._times = self.simulation.times self._edgeCurl = self.simulation.mesh.edge_curl self._MeMuI = self.simulation.MeMuI diff --git a/simpeg/electromagnetics/time_domain/simulation.py b/simpeg/electromagnetics/time_domain/simulation.py index edb374cf06..8c5f23d006 100644 --- a/simpeg/electromagnetics/time_domain/simulation.py +++ b/simpeg/electromagnetics/time_domain/simulation.py @@ -17,10 +17,39 @@ class BaseTDEMSimulation(BaseTimeSimulation, BaseEMSimulation): - """ - We start with the first order form of Maxwell's equations, eliminate and - solve the second order form. For the time discretization, we use backward - Euler. + r"""Base class for quasi-static TDEM simulation with finite volume. + + This class is used to define properties and methods necessary for solving + 3D time-domain EM problems. In the quasi-static regime, we ignore electric + displacement, and Maxwell's equations are expressed as: + + .. math:: + \begin{align} + \nabla \times \vec{e} + \frac{\partial \vec{b}}{\partial t} &= -\frac{\partial \vec{s}_m}{\partial t} \\ + \nabla \times \vec{h} - \vec{j} &= \vec{s}_e + \end{align} + + where the constitutive relations between fields and fluxes are given by: + + * :math:`\vec{j} = \sigma \vec{e}` + * :math:`\vec{b} = \mu \vec{h}` + + and: + + * :math:`\vec{s}_m` represents a magnetic source term + * :math:`\vec{s}_e` represents a current source term + + Child classes of ``BaseTDEMSimulation`` solve the above expression numerically + for various cases using mimetic finite volume and backward Euler time discretization. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + The mesh. + survey : .time_domain.survey.Survey + The time-domain EM survey. + dt_threshold : float + Threshold used when determining the unique time-step lengths. """ def __init__(self, mesh, survey=None, dt_threshold=1e-8, **kwargs): @@ -34,10 +63,12 @@ def __init__(self, mesh, survey=None, dt_threshold=1e-8, **kwargs): @property def survey(self): - """The survey for the simulation + """The TDEM survey object. + Returns ------- - simpeg.electromagnetics.time_domain.survey.Survey + .time_domain.survey.Survey + The TDEM survey object. """ if self._survey is None: raise AttributeError("Simulation must have a survey set") @@ -51,14 +82,17 @@ def survey(self, value): @property def dt_threshold(self): - """The threshold used to determine if a previous matrix factor can be reused. + """Threshold used when determining the unique time-step lengths. - If the difference in time steps falls below this threshold, the factored matrix - is re-used. + The number of linear systems that must be factored to solve the forward + problem is equal to the number of unique time-step lengths. *dt_threshold* + effectively sets the round-off error when determining the unique time-step + lengths used by the simulation. Returns ------- float + Threshold used when determining the unique time-step lengths. """ return self._dt_threshold @@ -66,23 +100,18 @@ def dt_threshold(self): def dt_threshold(self, value): self._dt_threshold = validate_float("dt_threshold", value, min_val=0.0) - # def fields_nostore(self, m): - # """ - # Solve the forward problem without storing fields - - # :param numpy.ndarray m: inversion model (nP,) - # :rtype: numpy.ndarray - # :return numpy.ndarray: numpy.ndarray (nD,) - - # """ - def fields(self, m): - """ - Solve the forward problem for the fields. + """Compute and return the fields for the model provided. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model. - :param numpy.ndarray m: inversion model (nP,) - :rtype: simpeg.electromagnetics.time_domain.fields.FieldsTDEM - :return f: fields object + Returns + ------- + .time_domain.fields.FieldsTDEM + The TDEM fields object. """ self.model = m @@ -138,26 +167,34 @@ def fields(self, m): return f def Jvec(self, m, v, f=None): - r""" - Jvec computes the sensitivity times a vector + r"""Compute the sensitivity matrix times a vector. + + Where :math:`\mathbf{d}` are the data, :math:`\mathbf{m}` are the model parameters, + and the sensitivity matrix is defined as: .. math:: - \mathbf{J} \mathbf{v} = - \frac{d\mathbf{P}}{d\mathbf{F}} - \left( - \frac{d\mathbf{F}}{d\mathbf{u}} \frac{d\mathbf{u}}{d\mathbf{m}} - + \frac{\partial\mathbf{F}}{\partial\mathbf{m}} - \right) - \mathbf{v} + \mathbf{J} = \dfrac{\partial \mathbf{d}}{\partial \mathbf{m}} - where + this method computes and returns the matrix-vector product: .. math:: - \mathbf{A} \frac{d\mathbf{u}}{d\mathbf{m}} - + \frac{\partial \mathbf{A} (\mathbf{u}, \mathbf{m})} - {\partial\mathbf{m}} = - \frac{d \mathbf{RHS}}{d \mathbf{m}} + \mathbf{J v} + for a given vector :math:`v`. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. + v : (n_param,) numpy.ndarray + The vector. + f : .time_domain.fields.FieldsTDEM, optional + Fields solved for all sources. + + Returns + ------- + (n_data,) numpy.ndarray + The sensitivity matrix times a vector. """ if f is None: @@ -168,7 +205,7 @@ def Jvec(self, m, v, f=None): # mat to store previous time-step's solution deriv times a vector for # each source - # size: nu x nSrc + # size: nu x n_sources # this is a bit silly @@ -248,27 +285,34 @@ def Jvec(self, m, v, f=None): return np.hstack(Jv) def Jtvec(self, m, v, f=None): - r""" - Jvec computes the adjoint of the sensitivity times a vector + r"""Compute the adjoint sensitivity matrix times a vector. - .. math:: + Where :math:`\mathbf{d}` are the data, :math:`\mathbf{m}` are the model parameters, + and the sensitivity matrix is defined as: - \mathbf{J}^\top \mathbf{v} = - \left( - \frac{d\mathbf{u}}{d\mathbf{m}} ^ \top - \frac{d\mathbf{F}}{d\mathbf{u}} ^ \top - + \frac{\partial\mathbf{F}}{\partial\mathbf{m}} ^ \top - \right) - \frac{d\mathbf{P}}{d\mathbf{F}} ^ \top - \mathbf{v} + .. math:: + \mathbf{J} = \dfrac{\partial \mathbf{d}}{\partial \mathbf{m}} - where + this method computes and returns the matrix-vector product: .. math:: + \mathbf{J^T v} + + for a given vector :math:`v`. - \frac{d\mathbf{u}}{d\mathbf{m}} ^\top \mathbf{A}^\top + - \frac{d\mathbf{A}(\mathbf{u})}{d\mathbf{m}} ^ \top = - \frac{d \mathbf{RHS}}{d \mathbf{m}} ^ \top + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. + v : (n_data,) numpy.ndarray + The vector. + f : .time_domain.fields.FieldsTDEM, optional + Fields solved for all sources. + + Returns + ------- + (n_param,) numpy.ndarray + The adjoint sensitivity matrix times a vector. """ if f is None: @@ -392,9 +436,32 @@ def Jtvec(self, m, v, f=None): return mkvc(JTv).astype(float) def getSourceTerm(self, tInd): - """ - Assemble the source term. This ensures that the RHS is a vector / array - of the correct size + r"""Return the discrete source terms for the time index provided. + + This method computes and returns the discrete magnetic and electric source terms for + all soundings at the time index provided. The exact shape and implementation of source + terms when solving for the fields at each time-step is formulation dependent. + + For definitions of the discrete magnetic (:math:`\mathbf{s_m}`) and electric + (:math:`\mathbf{s_e}`) source terms for each simulation, see the *Notes* sections + of the docstrings for: + + * :class:`.time_domain.Simulation3DElectricField` + * :class:`.time_domain.Simulation3DMagneticField` + * :class:`.time_domain.Simulation3DCurrentDensity` + * :class:`.time_domain.Simulation3DMagneticFluxDensity` + + Parameters + ---------- + tInd : int + The time index. Value between ``[0, n_steps]``. + + Returns + ------- + s_m : numpy.ndarray + The magnetic sources terms. (n_faces, n_sources) for EB-formulations. (n_edges, n_sources) for HJ-formulations. + s_e : numpy.ndarray + The electric sources terms. (n_edges, n_sources) for EB-formulations. (n_faces, n_sources) for HJ-formulations. """ Srcs = self.survey.source_list @@ -414,8 +481,12 @@ def getSourceTerm(self, tInd): return s_m, s_e def getInitialFields(self): - """ - Ask the sources for initial fields + """Returns the fields for all sources at the initial time. + + Returns + ------- + (n_edges or n_faces, n_sources) numpy.ndarray + The fields for all sources at the initial time. """ Srcs = self.survey.source_list @@ -436,6 +507,40 @@ def getInitialFields(self): return ifields def getInitialFieldsDeriv(self, src, v, adjoint=False, f=None): + r"""Derivative of the initial fields with respect to the model for a given source. + + For a given source object `src`, let :math:`\mathbf{u_0}` represent the initial + fields discretized to the mesh. Where :math:`\mathbf{m}` are the model parameters + and :math:`\mathbf{v}` is a vector, this method computes and returns: + + .. math:: + \dfrac{\partial \mathbf{u_0}}{\partial \mathbf{m}} \, \mathbf{v} + + or the adjoint operation: + + .. math:: + \dfrac{\partial \mathbf{u_0}}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + src : .time_domain.sources.BaseTDEMSrc + A TDEM source. + v : numpy.ndarray + A vector of appropriate dimension. When `adjoint` is ``False``, `v` is a + (n_param,) numpy.ndarray. When `adjoint` is ``True``, `v` is a (n_edges or n_faces,) + numpy.ndarray. + adjoint : bool + Whether to perform the adjoint operation. + f : .time_domain.fields.BaseTDEMFields, optional + The TDEM fields object. + + Returns + ------- + numpy.ndarray + Derivatives of the intial fields with respect to the model for a given source. + (n_edges or n_faces,) numpy.ndarray when `adjoint` is ``False``. (n_param,) numpy.ndarray + when `ajoint` is ``True``. + """ ifieldsDeriv = mkvc( getattr(src, "{}InitialDeriv".format(self._fieldType), None)( self, v, adjoint, f @@ -462,6 +567,33 @@ def getInitialFieldsDeriv(self, src, v, adjoint=False, f=None): # initial condition @property def Adcinv(self): + r"""Inverse of the factored system matrix for the DC resistivity problem. + + The solution to the DC resistivity problem is necessary at the initial time for + galvanic sources whose currents are non-zero at the initial time. + This property is used to compute and store the inverse of the factored linear system + matrix for the DC resistivity problem given by: + + .. math:: + \mathbf{A_{dc}} \, \boldsymbol{\phi_0} = \mathbf{q_{dc}} + + where :math:`\mathbf{A_{dc}}` is the system matrix, :math:`\boldsymbol{\phi_0}` represents the + discrete solution for the electric potential and :math:`\mathbf{q_{dc}}` is the discrete + right-hand side. Electric fields are computed by applying a discrete gradient operator + to the discrete electric potential solution. + + Returns + ------- + pymatsolver.solvers.Base + Inver of the factored systems matrix for the DC resistivity problem. + + Notes + ----- + See the docstrings for :class:`.resistivity.BaseDCSimulation`, + :class:`.resistivity.Simulation3DCellCentered` and + :class:`.resistivity.Simulation3DNodal` to learn + more about how the DC resistivity problem is solved. + """ if not hasattr(self, "getAdc"): raise NotImplementedError( "Support for galvanic sources has not been implemented for " @@ -476,6 +608,16 @@ def Adcinv(self): @property def clean_on_model_update(self): + """List of model-dependent attributes to clean upon model update. + + Some of the TDEM simulation's attributes are model-dependent. This property specifies + the model-dependent attributes that much be cleared when the model is updated. + + Returns + ------- + list of str + List of the model-dependent attributes to clean upon model update. + """ items = super().clean_on_model_update return items + ["_Adcinv"] #: clear DC matrix factors on any model updates @@ -490,83 +632,160 @@ def clean_on_model_update(self): class Simulation3DMagneticFluxDensity(BaseTDEMSimulation): - r""" - Starting from the quasi-static E-B formulation of Maxwell's equations - (semi-discretized) + r"""3D TDEM simulation in terms of the magnetic flux density. + + This simulation solves for the magnetic flux density at each time-step. + In this formulation, the electric fields are defined on mesh edges and the + magnetic flux density is defined on mesh faces; i.e. it is an EB formulation. + See the *Notes* section for a comprehensive description of the formulation. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + The mesh. + survey : .time_domain.survey.Survey + The time-domain EM survey. + dt_threshold : float + Threshold used when determining the unique time-step lengths. + + Notes + ----- + Here, we start with the quasi-static approximation for Maxwell's equations by neglecting + electric displacement: .. math:: + &\nabla \times \vec{e} + \frac{\partial \vec{b}}{\partial t} = - \frac{\partial \vec{s}_m}{\partial t} \\ + &\nabla \times \vec{h} - \vec{j} = \vec{s}_e - \mathbf{C} \mathbf{e} + \frac{\partial \mathbf{b}}{\partial t} = - \mathbf{s_m} \\ - \mathbf{C}^{\top} \mathbf{M_{\mu^{-1}}^f} \mathbf{b} - - \mathbf{M_{\sigma}^e} \mathbf{e} = \mathbf{s_e} - - - where :math:`\mathbf{s_e}` is an integrated quantity, we eliminate - :math:`\mathbf{e}` using + where :math:`\vec{s}_e` is an electric source term that defines a source current density, + and :math:`\vec{s}_m` magnetic source term that defines a source magnetic flux density. + We define the constitutive relations for the electrical conductivity :math:`\sigma` + and magnetic permeability :math:`\mu` as: .. math:: + \vec{j} &= \sigma \vec{e} \\ + \vec{h} &= \mu^{-1} \vec{b} - \mathbf{e} = \mathbf{M_{\sigma}^e}^{-1} \mathbf{C}^{\top} - \mathbf{M_{\mu^{-1}}^f} \mathbf{b} - - \mathbf{M_{\sigma}^e}^{-1} \mathbf{s_e} + We then take the inner products of all previous expressions with a vector test function :math:`\vec{u}`. + Through vector calculus identities and the divergence theorem, we obtain: - - to obtain a second order semi-discretized system in :math:`\mathbf{b}` + .. math:: + & \int_\Omega \vec{u} \cdot (\nabla \times \vec{e}) \, dv + + \int_\Omega \vec{u} \cdot \frac{\partial \vec{b}}{\partial t} \, dv + = - \int_\Omega \vec{u} \cdot \frac{\partial \vec{s}_m}{\partial t} \, dv \\ + & \int_\Omega (\nabla \times \vec{u}) \cdot \vec{h} \, dv + - \oint_{\partial \Omega} \vec{u} \cdot (\vec{h} \times \hat{n}) \, da + - \int_\Omega \vec{u} \cdot \vec{j} \, dv = \int_\Omega \vec{u} \cdot \vec{s}_e \, dv \\ + & \int_\Omega \vec{u} \cdot \vec{j} \, dv = \int_\Omega \vec{u} \cdot \sigma \vec{e} \, dv \\ + & \int_\Omega \vec{u} \cdot \vec{h} \, dv = \int_\Omega \vec{u} \cdot \mu^{-1} \vec{b} \, dv + + Assuming natural boundary conditions, the surface integral is zero. + + The above expressions are discretized in space according to the finite volume method. + The discrete electric fields :math:`\mathbf{e}` are defined on mesh edges, + and the discrete magnetic flux densities :math:`\mathbf{b}` are defined on mesh faces. + This implies :math:`\mathbf{j}` must be defined on mesh edges and :math:`\mathbf{h}` must + be defined on mesh faces. Where :math:`\mathbf{u_e}` and :math:`\mathbf{u_f}` represent + test functions discretized to edges and faces, respectively, we obtain the following + set of discrete inner-products: .. math:: + &\mathbf{u_f^T C e} + \mathbf{u_f^T } \, \frac{\partial \mathbf{b}}{\partial t} + = - \mathbf{u_f^T } \, \frac{\partial \mathbf{s_m}}{\partial t} \\ + &\mathbf{u_e^T C^T M_f h} - \mathbf{u_e^T M_e j} = \mathbf{u_e^T s_e} \\ + &\mathbf{u_e^T M_e j} = \mathbf{u_e^T M_{e\sigma} e} \\ + &\mathbf{u_f^T M_f h} = \mathbf{u_f^T M_{f \frac{1}{\mu}} b} - \mathbf{C} \mathbf{M_{\sigma}^e}^{-1} \mathbf{C}^{\top} - \mathbf{M_{\mu^{-1}}^f} \mathbf{b} + - \frac{\partial \mathbf{b}}{\partial t} = - \mathbf{C} \mathbf{M_{\sigma}^e}^{-1} \mathbf{s_e} + \mathbf{s_m} + where + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_e}` is the edge inner-product matrix + * :math:`\mathbf{M_f}` is the face inner-product matrix + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces - and moving everything except the time derivative to the rhs gives + By cancelling like-terms and combining the discrete expressions in terms of the magnetic flux density, we obtain: .. math:: - \frac{\partial \mathbf{b}}{\partial t} = - -\mathbf{C} \mathbf{M_{\sigma}^e}^{-1} \mathbf{C}^{\top} - \mathbf{M_{\mu^{-1}}^f} \mathbf{b} + - \mathbf{C} \mathbf{M_{\sigma}^e}^{-1} \mathbf{s_e} + \mathbf{s_m} + \mathbf{C M_{e\sigma}^{-1} C^T M_{f\frac{1}{\mu}} b} + + \frac{\partial \mathbf{b}}{\partial t} + = \mathbf{C M_{e\sigma}^{-1}} \mathbf{s_e} + - \frac{\partial \mathbf{s_m}}{\partial t} - For the time discretization, we use backward euler. To solve for the - :math:`n+1` th time step, we have + Finally, we discretize in time according to backward Euler. The discrete magnetic flux density + on mesh faces at time :math:`t_k > t_0` is obtained by solving the following at each time-step: .. math:: + \mathbf{A}_k \mathbf{b}_k = \mathbf{q_k} - \mathbf{B}_k \mathbf{b}_{k-1} - \frac{\mathbf{b}^{n+1} - \mathbf{b}^{n}}{\mathbf{dt}} = - -\mathbf{C} \mathbf{M_{\sigma}^e}^{-1} \mathbf{C}^{\top} - \mathbf{M_{\mu^{-1}}^f} \mathbf{b}^{n+1} + - \mathbf{C} \mathbf{M_{\sigma}^e}^{-1} \mathbf{s_e}^{n+1} + - \mathbf{s_m}^{n+1} + where :math:`\Delta t_k = t_k - t_{k-1}` and + .. math:: + &\mathbf{A}_k = \mathbf{C M_{e\sigma}^{-1} C^T M_{f\frac{1}{\mu}}} + \frac{1}{\Delta t_k} \mathbf{I} \\ + &\mathbf{B}_k = -\frac{1}{\Delta t_k} \mathbf{I}\\ + &\mathbf{q}_k = \mathbf{C M_{e\sigma}^{-1}} \mathbf{s}_{\mathbf{e}, k} \; + - \; \frac{1}{\Delta t_k} \big [ \mathbf{s}_{\mathbf{m}, k} - \mathbf{s}_{\mathbf{m}, k-1} \big ] - re-arranging to put :math:`\mathbf{b}^{n+1}` on the left hand side gives + Although the following system is never explicitly formed, we can represent + the solution at all time-steps as: .. math:: - - (\mathbf{I} + \mathbf{dt} \mathbf{C} \mathbf{M_{\sigma}^e}^{-1} - \mathbf{C}^{\top} \mathbf{M_{\mu^{-1}}^f}) \mathbf{b}^{n+1} = - \mathbf{b}^{n} + \mathbf{dt}(\mathbf{C} \mathbf{M_{\sigma}^e}^{-1} - \mathbf{s_e}^{n+1} + \mathbf{s_m}^{n+1}) + \begin{bmatrix} + \mathbf{A_1} & & & & \\ + \mathbf{B_2} & \mathbf{A_2} & & & \\ + & & \ddots & & \\ + & & & \mathbf{B_n} & \mathbf{A_n} + \end{bmatrix} + \begin{bmatrix} + \mathbf{b_1} \\ \mathbf{b_2} \\ \vdots \\ \mathbf{b_n} + \end{bmatrix} = + \begin{bmatrix} + \mathbf{q_1} \\ \mathbf{q_2} \\ \vdots \\ \mathbf{q_n} + \end{bmatrix} - + \begin{bmatrix} + \mathbf{B_1 b_0} \\ \mathbf{0} \\ \vdots \\ \mathbf{0} + \end{bmatrix} + + where the magnetic flux densities at the initial time :math:`\mathbf{b_0}` + are computed analytically or numerically depending on whether the source + carries non-zero current at the initial time. """ _fieldType = "b" _formulation = "EB" - fieldsPair = Fields3DMagneticFluxDensity #: A simpeg.EM.TDEM.Fields3DMagneticFluxDensity object + fieldsPair = Fields3DMagneticFluxDensity Fields_Derivs = FieldsDerivativesEB def getAdiag(self, tInd): - r""" - System matrix at a given time index + r"""Diagonal system matrix for the given time-step index. + + This method returns the diagonal system matrix for the time-step index provided: .. math:: + \mathbf{A}_k = \mathbf{C M_{e\sigma}^{-1} C^T M_{f\frac{1}{\mu}}} + \frac{1}{\Delta t_k} \mathbf{I} + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{I}` is the identity matrix + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{M_{e \sigma}}` is the conductivity inner-product matrix on edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inverse permeability inner-product matrix on faces - (\mathbf{I} + \mathbf{dt} \mathbf{C} \mathbf{M_{\sigma}^e}^{-1} - \mathbf{C}^{\top} \mathbf{M_{\mu^{-1}}^f}) + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` + for a full description of the formulation. + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + + Returns + ------- + (n_faces, n_faces) sp.sparse.csr_matrix + The diagonal system matrix. """ assert tInd >= 0 and tInd < self.nT @@ -583,8 +802,52 @@ def getAdiag(self, tInd): return A def getAdiagDeriv(self, tInd, u, v, adjoint=False): - """ - Derivative of ADiag + r"""Derivative operation for the diagonal system matrix times a vector. + + The diagonal system matrix for time-step index *k* is given by: + + .. math:: + \mathbf{A}_k = \mathbf{C M_{e\sigma}^{-1} C^T M_{f\frac{1}{\mu}}} + \frac{1}{\Delta t_k} \mathbf{I} + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{I}` is the identity matrix + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{M_{e \sigma}}` is the conductivity inner-product matrix on edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inverse permeability inner-product matrix on faces + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters, :math:`\mathbf{v}` is a vector + and :math:`\mathbf{b_k}` is the discrete solution for time-step *k*, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A_k \, b_k})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A_k \, b_k})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + u : (n_faces,) numpy.ndarray + The solution for the fields for the current model; i.e. :math:`\mathbf{b_k}`. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. """ C = self.mesh.edge_curl @@ -605,8 +868,27 @@ def getAdiagDeriv(self, tInd, u, v, adjoint=False): return ADeriv def getAsubdiag(self, tInd): - """ - Matrix below the diagonal + r"""Sub-diagonal system matrix for the time-step index provided. + + This method returns the sub-diagonal system matrix for the time-step index provided: + + .. math:: + \mathbf{B}_k = -\frac{1}{\Delta t_k} \mathbf{I} + + where :math:`\Delta t_k` is the step length and :math:`\mathbf{I}` is the identity matrix. + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` + for a full description of the formulation. + + Parameters + ---------- + tInd : int + The time index; between ``[0, n_steps-1]``. + + Returns + ------- + (n_faces, n_faces) sp.sparse.csr_matrix + The sub-diagonal system matrix. """ dt = self.time_steps[tInd] @@ -619,11 +901,82 @@ def getAsubdiag(self, tInd): return Asubdiag def getAsubdiagDeriv(self, tInd, u, v, adjoint=False): + r"""Derivative operation for the sub-diagonal system matrix times a vector. + + The sub-diagonal system matrix for time-step index *k* is given by: + + .. math:: + \mathbf{B}_k = -\frac{1}{\Delta t_k} \mathbf{I} + + where :math:`\Delta t_k` is the step length and :math:`\mathbf{I}` is the identity matrix. + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters, :math:`\mathbf{v}` is a vector + and :math:`\mathbf{b_{k-1}}` is the discrete solution for the previous time-step, + this method assumes the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{B_k \, b_{k-1}})}{\partial \mathbf{m}} \, \mathbf{v} = \mathbf{0} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{B_k \, b_{k-1}})}{\partial \mathbf{m}}^T \, \mathbf{v} = \mathbf{0} + + The derivative operation returns a vector of zeros because the sub-diagonal system matrix + does not depend on the model!!! + + Parameters + ---------- + tInd : int + The time index; between ``[0, n_steps-1]``. + u : (n_faces,) numpy.ndarray + The solution for the fields for the current model for the previous time-step; + i.e. :math:`\mathbf{b_{k-1}}`. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. + """ return Zero() * v def getRHS(self, tInd): - """ - Assemble the RHS + r"""Right-hand sides for the given time index. + + This method returns the right-hand sides for the time index provided. + The right-hand side for each source is constructed according to: + + .. math:: + \mathbf{q}_k = \mathbf{C M_{e\sigma}^{-1}} \mathbf{s}_{\mathbf{e}, k} \; + - \; \frac{1}{\Delta t_k} \big [ \mathbf{s}_{\mathbf{m}, k} - \mathbf{s}_{\mathbf{m}, k-1} \big ] + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` + for a full description of the formulation. + + Parameters + ---------- + tInd : int + The time index; between ``[0, n_steps]``. + + Returns + ------- + (n_faces, n_sources) numpy.ndarray + The right-hand sides. """ C = self.mesh.edge_curl MeSigmaI = self.MeSigmaI @@ -637,8 +990,51 @@ def getRHS(self, tInd): return rhs def getRHSDeriv(self, tInd, src, v, adjoint=False): - """ - Derivative of the RHS + r"""Derivative of the right-hand side times a vector for a given source and time index. + + The right-hand side for a given source at time index *k* is constructed according to: + + .. math:: + \mathbf{q}_k = \mathbf{C M_{e\sigma}^{-1}} \mathbf{s}_{\mathbf{e}, k} \; + - \; \frac{1}{\Delta t_k} \big [ \mathbf{s}_{\mathbf{m}, k} - \mathbf{s}_{\mathbf{m}, k-1} \big ] + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters and :math:`\mathbf{v}` is a vector, + this method returns + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + tInd : int + The time index; between ``[0, n_steps]``. + src : .time_domain.sources.BaseTDEMSrc + The TDEM source object. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of the right-hand sides times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. """ C = self.mesh.edge_curl @@ -673,38 +1069,124 @@ def getRHSDeriv(self, tInd, src, v, adjoint=False): # ------------------------------- Simulation3DElectricField ------------------------------- # class Simulation3DElectricField(BaseTDEMSimulation): - r""" - Solve the EB-formulation of Maxwell's equations for the electric field, e. - - Starting with + r"""3D TDEM simulation in terms of the electric field. + + This simulation solves for the electric field at each time-step. + In this formulation, the electric fields are defined on mesh edges and the + magnetic flux density is defined on mesh faces; i.e. it is an EB formulation. + See the *Notes* section for a comprehensive description of the formulation. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + The mesh. + survey : .time_domain.survey.Survey + The time-domain EM survey. + dt_threshold : float + Threshold used when determining the unique time-step lengths. + + Notes + ----- + Here, we start with the quasi-static approximation for Maxwell's equations by neglecting + electric displacement: .. math:: + &\nabla \times \vec{e} + \frac{\partial \vec{b}}{\partial t} = - \frac{\partial \vec{s}_m}{\partial t} \\ + &\nabla \times \vec{h} - \vec{j} = \vec{s}_e + + where :math:`\vec{s}_e` is an electric source term that defines a source current density, + and :math:`\vec{s}_m` magnetic source term that defines a source magnetic flux density. + We define the constitutive relations for the electrical conductivity :math:`\sigma` + and magnetic permeability :math:`\mu` as: - \nabla \times \mathbf{e} + \frac{\partial \mathbf{b}}{\partial t} = \mathbf{s_m} \ - \nabla \times \mu^{-1} \mathbf{b} - \sigma \mathbf{e} = \mathbf{s_e} + .. math:: + \vec{j} &= \sigma \vec{e} \\ + \vec{h} &= \mu^{-1} \vec{b} + We then take the inner products of all previous expressions with a vector test function :math:`\vec{u}`. + Through vector calculus identities and the divergence theorem, we obtain: - we eliminate :math:`\frac{\partial b}{\partial t}` using + .. math:: + & \int_\Omega \vec{u} \cdot (\nabla \times \vec{e}) \, dv + + \int_\Omega \vec{u} \cdot \frac{\partial \vec{b}}{\partial t} \, dv + = - \int_\Omega \vec{u} \cdot \frac{\partial \vec{s}_m}{\partial t} \, dv \\ + & \int_\Omega (\nabla \times \vec{u}) \cdot \vec{h} \, dv + - \oint_{\partial \Omega} \vec{u} \cdot (\vec{h} \times \hat{n}) \, da + - \int_\Omega \vec{u} \cdot \vec{j} \, dv = \int_\Omega \vec{u} \cdot \vec{s}_e \, dv \\ + & \int_\Omega \vec{u} \cdot \vec{j} \, dv = \int_\Omega \vec{u} \cdot \sigma \vec{e} \, dv \\ + & \int_\Omega \vec{u} \cdot \vec{h} \, dv = \int_\Omega \vec{u} \cdot \mu^{-1} \vec{b} \, dv + + Assuming natural boundary conditions, the surface integral is zero. + + The above expressions are discretized in space according to the finite volume method. + The discrete electric fields :math:`\mathbf{e}` are defined on mesh edges, + and the discrete magnetic flux densities :math:`\mathbf{b}` are defined on mesh faces. + This implies :math:`\mathbf{j}` must be defined on mesh edges and :math:`\mathbf{h}` must + be defined on mesh faces. Where :math:`\mathbf{u_e}` and :math:`\mathbf{u_f}` represent + test functions discretized to edges and faces, respectively, we obtain the following + set of discrete inner-products: .. math:: + &\mathbf{u_f^T C e} + \mathbf{u_f^T } \, \frac{\partial \mathbf{b}}{\partial t} + = - \mathbf{u_f^T } \, \frac{\partial \mathbf{s_m}}{\partial t} \\ + &\mathbf{u_e^T C^T M_f h} - \mathbf{u_e^T M_e j} = \mathbf{u_e^T s_e} \\ + &\mathbf{u_e^T M_e j} = \mathbf{u_e^T M_{e\sigma} e} \\ + &\mathbf{u_f^T M_f h} = \mathbf{u_f^T M_{f \frac{1}{\mu}} b} - \frac{\partial \mathbf{b}}{\partial t} = - \nabla \times \mathbf{e} + \mathbf{s_m} + where + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_e}` is the edge inner-product matrix + * :math:`\mathbf{M_f}` is the face inner-product matrix + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces - taking the time-derivative of Ampere's law, we see + By cancelling like-terms and combining the discrete expressions in terms of the electric field, we obtain: .. math:: + \mathbf{C^T M_{f\frac{1}{\mu}} C e} + \mathbf{M_{e\sigma}}\frac{\partial \mathbf{e}}{\partial t} + = \mathbf{C^T M_{f\frac{1}{\mu}}} \frac{\partial \mathbf{s_m}}{\partial t} + - \frac{\partial \mathbf{s_e}}{\partial t} - \frac{\partial}{\partial t}\left( \nabla \times \mu^{-1} \mathbf{b} - \sigma \mathbf{e} \right) = \frac{\partial \mathbf{s_e}}{\partial t} \ - \nabla \times \mu^{-1} \frac{\partial \mathbf{b}}{\partial t} - \sigma \frac{\partial\mathbf{e}}{\partial t} = \frac{\partial \mathbf{s_e}}{\partial t} + Finally, we discretize in time according to backward Euler. The discrete electric fields + on mesh edges at time :math:`t_k > t_0` is obtained by solving the following at each time-step: + .. math:: + \mathbf{A}_k \mathbf{b}_k = \mathbf{q}_k - \mathbf{B}_k \mathbf{b}_{k-1} - which gives us + where :math:`\Delta t_k = t_k - t_{k-1}` and .. math:: + &\mathbf{A}_k = \mathbf{C^T M_{f\frac{1}{\mu}} C} + \frac{1}{\Delta t_k} \mathbf{M_{e\sigma}} \\ + &\mathbf{B}_k = -\frac{1}{\Delta t_k} \mathbf{M_{e\sigma}} \\ + &\mathbf{q}_k = \frac{1}{\Delta t_k} \mathbf{C^T M_{f\frac{1}{\mu}}} + \big [ \mathbf{s}_{\mathbf{m}, k} - \mathbf{s}_{\mathbf{m}, k-1} \big ] + -\frac{1}{\Delta t_k} \big [ \mathbf{s}_{\mathbf{e}, k} - \mathbf{s}_{\mathbf{e}, k-1} \big ] - \nabla \times \mu^{-1} \nabla \times \mathbf{e} + \sigma \frac{\partial\mathbf{e}}{\partial t} = \nabla \times \mu^{-1} \mathbf{s_m} + \frac{\partial \mathbf{s_e}}{\partial t} + Although the following system is never explicitly formed, we can represent + the solution at all time-steps as: + .. math:: + \begin{bmatrix} + \mathbf{A_1} & & & & \\ + \mathbf{B_2} & \mathbf{A_2} & & & \\ + & & \ddots & & \\ + & & & \mathbf{B_n} & \mathbf{A_n} + \end{bmatrix} + \begin{bmatrix} + \mathbf{e_1} \\ \mathbf{e_2} \\ \vdots \\ \mathbf{e_n} + \end{bmatrix} = + \begin{bmatrix} + \mathbf{q_1} \\ \mathbf{q_2} \\ \vdots \\ \mathbf{q_n} + \end{bmatrix} - + \begin{bmatrix} + \mathbf{B_1 e_0} \\ \mathbf{0} \\ \vdots \\ \mathbf{0} + \end{bmatrix} + + where the electric fields at the initial time :math:`\mathbf{e_0}` + are computed analytically or numerically depending on whether the + source is galvanic and carries non-zero current at the initial time. """ @@ -715,10 +1197,7 @@ class Simulation3DElectricField(BaseTDEMSimulation): # @profile def Jtvec(self, m, v, f=None): - """ - Jvec computes the adjoint of the sensitivity times a vector - """ - + # Doctring inherited from parent class. if f is None: f = self.fields(m) @@ -873,8 +1352,32 @@ def Jtvec(self, m, v, f=None): return mkvc(JTv).astype(float) def getAdiag(self, tInd): - """ - Diagonal of the system matrix at a given time index + r"""Diagonal system matrix for the time-step index provided. + + This method returns the diagonal system matrix for the time-step index provided: + + .. math:: + \mathbf{A}_k = \mathbf{C^T M_{f\frac{1}{\mu}} C} + \frac{1}{\Delta t_k} \mathbf{M_{e\sigma}} + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{M_{e \sigma}}` is the conductivity inner-product matrix on edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inverse permeability inner-product matrix on faces + + See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` + for a full description of the formulation. + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + + Returns + ------- + (n_edges, n_edges) sp.sparse.csr_matrix + The diagonal system matrix. """ assert tInd >= 0 and tInd < self.nT @@ -886,8 +1389,52 @@ def getAdiag(self, tInd): return C.T.tocsr() * (MfMui * C) + 1.0 / dt * MeSigma def getAdiagDeriv(self, tInd, u, v, adjoint=False): - """ - Deriv of ADiag with respect to electrical conductivity + r"""Derivative operation for the diagonal system matrix times a vector. + + The diagonal system matrix for time-step index *k* is given by: + + .. math:: + \mathbf{A}_k = \mathbf{C^T M_{f\frac{1}{\mu}} C} + \frac{1}{\Delta t_k} \mathbf{M_{e\sigma}} + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{M_{e \sigma}}` is the conductivity inner-product matrix on edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inverse permeability inner-product matrix on faces + + See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters, :math:`\mathbf{v}` is a vector + and :math:`\mathbf{e_k}` is the discrete solution for time-step *k*, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A_k \, e_k})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A_k \, e_k})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + u : (n_edges,) numpy.ndarray + The solution for the fields for the current model for the specified time-step; + i.e. :math:`\mathbf{e_k}`. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. """ assert tInd >= 0 and tInd < self.nT @@ -900,8 +1447,28 @@ def getAdiagDeriv(self, tInd, u, v, adjoint=False): return 1.0 / dt * self.MeSigmaDeriv(u, v, adjoint) def getAsubdiag(self, tInd): - """ - Matrix below the diagonal + r"""Sub-diagonal system matrix for the time-step index provided. + + This method returns the sub-diagonal system matrix for the time-step index provided: + + .. math:: + \mathbf{B}_k = -\frac{1}{\Delta t_k} \mathbf{M_{e\sigma}} + + where :math:`\Delta t_k` is the step-length and :math:`\mathbf{M_{e \sigma}}` is the + conductivity inner-product matrix on edges. + + See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` + for a full description of the formulation. + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + + Returns + ------- + (n_edges, n_edges) sp.sparse.csr_matrix + The sub-diagonal system matrix. """ assert tInd >= 0 and tInd < self.nT @@ -910,9 +1477,48 @@ def getAsubdiag(self, tInd): return -1.0 / dt * self.MeSigma def getAsubdiagDeriv(self, tInd, u, v, adjoint=False): - """ - Derivative of the matrix below the diagonal with respect to electrical - conductivity + r"""Derivative operation for the sub-diagonal system matrix times a vector. + + The sub-diagonal system matrix for time-step index *k* is given by: + + .. math:: + \mathbf{B}_k = -\frac{1}{\Delta t_k} \mathbf{M_{e\sigma}} + + where :math:`\Delta t_k` is the step-length and :math:`\mathbf{M_{e \sigma}}` is the + conductivity inner-product matrix on edges. + + See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters, :math:`\mathbf{v}` is a vector + and :math:`\mathbf{e_{k-1}}` is the discrete solution for the previous time-step, + this method assumes the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{B_k \, e_{k-1}})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{B_k \, e_{k-1}})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + u : (n_edges,) numpy.ndarray + The solution for the fields for the current model for the previous time-step; + i.e. :math:`\mathbf{e_{k-1}}`. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. """ dt = self.time_steps[tInd] @@ -922,8 +1528,36 @@ def getAsubdiagDeriv(self, tInd, u, v, adjoint=False): return -1.0 / dt * self.MeSigmaDeriv(u, v, adjoint) def getRHS(self, tInd): - """ - right hand side + r"""Right-hand sides for the given time index. + + This method returns the right-hand sides for the time index provided. + The right-hand side for each source is constructed according to: + + .. math:: + \mathbf{q}_k = + -\frac{1}{\Delta t_k} \big [ \mathbf{s}_{\mathbf{e}, k} - \mathbf{s}_{\mathbf{e}, k-1} \big ] + - \frac{1}{\Delta t_k} \mathbf{C^T M_{f\frac{1}{\mu}}} + \big [ \mathbf{s}_{\mathbf{m}, k} - \mathbf{s}_{\mathbf{m}, k-1} \big ] + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrices for inverse permeabilities projected to faces + + See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` + for a full description of the formulation. + + Parameters + ---------- + tInd : int + The time index; between ``[0, n_steps]``. + + Returns + ------- + (n_edges, n_sources) numpy.ndarray + The right-hand sides. """ # Omit this: Note input was tInd+1 # if tInd == len(self.time_steps): @@ -936,68 +1570,268 @@ def getRHS(self, tInd): return -1.0 / dt * (s_e - s_en1) + self.mesh.edge_curl.T * self.MfMui * s_m def getRHSDeriv(self, tInd, src, v, adjoint=False): - # right now, we are assuming that s_e, s_m do not depend on the model. - return Zero() - - def getAdc(self): - MeSigma = self.MeSigma - Grad = self.mesh.nodal_gradient - Adc = Grad.T.tocsr() * MeSigma * Grad - # Handling Null space of A - Adc[0, 0] = Adc[0, 0] + 1.0 - return Adc - - def getAdcDeriv(self, u, v, adjoint=False): - Grad = self.mesh.nodal_gradient - if not adjoint: - return Grad.T * self.MeSigmaDeriv(-u, v, adjoint) - else: - return self.MeSigmaDeriv(-u, Grad * v, adjoint) - - # def clean(self): - # """ - # Clean factors - # """ - # if self.Adcinv is not None: - # self.Adcinv.clean() - + r"""Derivative of the right-hand side times a vector for a given source and time index. -############################################################################### -# # -# H-J Formulation # -# # -############################################################################### + The right-hand side for a given source at time index *k* is constructed according to: -# ------------------------------- Simulation3DMagneticField ------------------------------- # + .. math:: + \mathbf{q}_k = + -\frac{1}{\Delta t_k} \big [ \mathbf{s}_{\mathbf{e}, k} - \mathbf{s}_{\mathbf{e}, k-1} \big ] + - \frac{1}{\Delta t_k} \mathbf{C^T M_{f\frac{1}{\mu}} } + \big [ \mathbf{s}_{\mathbf{m}, k} - \mathbf{s}_{\mathbf{m}, k-1} \big ] + where -class Simulation3DMagneticField(BaseTDEMSimulation): - r""" - Solve the H-J formulation of Maxwell's equations for the magnetic field h. + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrices for inverse permeabilities projected to faces - We start with Maxwell's equations in terms of the magnetic field and - current density + See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` + for a full description of the formulation. - .. math:: + Where :math:`\mathbf{m}` are the set of model parameters and :math:`\mathbf{v}` is a vector, + this method returns - \nabla \times \rho \mathbf{j} + \mu \frac{\partial h}{\partial t} = \mathbf{s_m} \ - \nabla \times \mathbf{h} - \mathbf{j} = \mathbf{s_e} + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}} \, \mathbf{v} + Or the adjoint operation - and eliminate :math:`\mathbf{j}` using + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + tInd : int + The time index; between ``[0, n_steps]``. + src : .time_domain.sources.BaseTDEMSrc + The TDEM source object. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of the right-hand sides times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. + """ + # right now, we are assuming that s_e, s_m do not depend on the model. + return Zero() + + def getAdc(self): + r"""The system matrix for the DC resistivity problem. + + The solution to the DC resistivity problem is necessary at the initial time for + galvanic sources whose currents are non-zero at the initial time. + The discrete solution to the 3D DC resistivity problem is expressed as: + + .. math:: + \mathbf{A_{dc}}\,\boldsymbol{\phi_0} = \mathbf{q_{dc}} + + where :math:`\mathbf{A_{dc}}` is the DC resistivity system matrix, :math:`\boldsymbol{\phi_0}` + is the discrete solution for the electric potentials at the initial time, and :math:`\mathbf{q_{dc}}` + is the galvanic source term. This method returns the system matrix + for the nodal formulation, i.e.: + + .. math:: + \mathbf{A_{dc}} = \mathbf{G^T \, M_{e\sigma} \, G} + + where :math:`\mathbf{G}` is the nodal gradient operator with imposed boundary conditions, + and :math:`\mathbf{M_{e\sigma}}` is the inner product matrix for conductivities projected to edges. + + The electric fields at the initial time :math:`\mathbf{e_0}` are obtained by applying the + nodal gradient operator. I.e.: + + .. math:: + \mathbf{e_0} = \mathbf{G} \, \boldsymbol{\phi_0} + + See the *Notes* section of the doc strings for :class:`.resistivity.Simulation3DNodal` + for a full description of the nodal DC resistivity formulation. + + Returns + ------- + (n_nodes, n_nodes) sp.sparse.csr_matrix + The system matrix for the DC resistivity problem. + """ + MeSigma = self.MeSigma + Grad = self.mesh.nodal_gradient + Adc = Grad.T.tocsr() * MeSigma * Grad + # Handling Null space of A + Adc[0, 0] = Adc[0, 0] + 1.0 + return Adc + + def getAdcDeriv(self, u, v, adjoint=False): + r"""Derivative operation for the DC resistivity system matrix times a vector. + + The discrete solution to the 3D DC resistivity problem is expressed as: + + .. math:: + \mathbf{A_{dc}}\boldsymbol{\phi_0} = \mathbf{q_{dc}} + + where :math:`\mathbf{A_{dc}}` is the DC resistivity system matrix, :math:`\boldsymbol{\phi_0}` + is the discrete solution for the electric potentials at the initial time, and :math:`\mathbf{q_{dc}}` + is the galvanic source term. For a vector :math:`\mathbf{v}`, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A_{dc}}\boldsymbol{\phi_0})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A_{dc}}\boldsymbol{\phi_0})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + u : (n_nodes,) numpy.ndarray + The solution for the fields for the current model; i.e. electric potentials at nodes. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_nodes,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of the DC resistivity system matrix times a vector. (n_nodes,) for the standard operation. + (n_param,) for the adjoint operation. + """ + Grad = self.mesh.nodal_gradient + if not adjoint: + return Grad.T * self.MeSigmaDeriv(-u, v, adjoint) + else: + return self.MeSigmaDeriv(-u, Grad * v, adjoint) + + +############################################################################### +# # +# H-J Formulation # +# # +############################################################################### + +# ------------------------------- Simulation3DMagneticField ------------------------------- # + + +class Simulation3DMagneticField(BaseTDEMSimulation): + r"""3D TDEM simulation in terms of the magnetic field. + + This simulation solves for the magnetic field at each time-step. + In this formulation, the magnetic fields are defined on mesh edges and the + current density is defined on mesh faces; i.e. it is an HJ formulation. + See the *Notes* section for a comprehensive description of the formulation. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + The mesh. + survey : .time_domain.survey.Survey + The time-domain EM survey. + dt_threshold : float + Threshold used when determining the unique time-step lengths. + + Notes + ----- + Here, we start with the quasi-static approximation for Maxwell's equations by neglecting + electric displacement: + + .. math:: + &\nabla \times \vec{e} + \frac{\partial \vec{b}}{\partial t} = - \frac{\partial \vec{s}_m}{\partial t} \\ + &\nabla \times \vec{h} - \vec{j} = \vec{s}_e + + where :math:`\vec{s}_e` is an electric source term that defines a source current density, + and :math:`\vec{s}_m` magnetic source term that defines a source magnetic flux density. + We define the constitutive relations for the electrical resistivity :math:`\rho` + and magnetic permeability :math:`\mu` as: + + .. math:: + \vec{e} &= \rho \vec{e} \\ + \vec{b} &= \mu \vec{h} + + We then take the inner products of all previous expressions with a vector test function :math:`\vec{u}`. + Through vector calculus identities and the divergence theorem, we obtain: + + .. math:: + & \int_\Omega (\nabla \times \vec{u}) \cdot \vec{e} \; dv + - \oint_{\partial \Omega} \vec{u} \cdot (\vec{e} \times \hat{n} ) \, da + + \int_\Omega \vec{u} \cdot \frac{\partial \vec{b}}{\partial t} \, dv + = - \int_\Omega \vec{u} \cdot \frac{\partial \vec{s}_m}{\partial t} \, dv \\ + & \int_\Omega \vec{u} \cdot (\nabla \times \vec{h} ) \, dv - \int_\Omega \vec{u} \cdot \vec{j} \, dv + = \int_\Omega \vec{u} \cdot \vec{s}_e \, dv\\ + & \int_\Omega \vec{u} \cdot \vec{e} \, dv = \int_\Omega \vec{u} \cdot \rho \vec{j} \, dv \\ + & \int_\Omega \vec{u} \cdot \vec{b} \, dv = \int_\Omega \vec{u} \cdot \mu \vec{h} \, dv + + Assuming natural boundary conditions, the surface integral is zero. + + The above expressions are discretized in space according to the finite volume method. + The discrete magnetic fields :math:`\mathbf{h}` are defined on mesh edges, + and the discrete current densities :math:`\mathbf{j}` are defined on mesh faces. + This implies :math:`\mathbf{b}` must be defined on mesh edges and :math:`\mathbf{e}` must + be defined on mesh faces. Where :math:`\mathbf{u_e}` and :math:`\mathbf{u_f}` represent + test functions discretized to edges and faces, respectively, we obtain the following + set of discrete inner-products: .. math:: + &\mathbf{u_e^T C^T M_f \, e } + \mathbf{u_e^T M_e} \frac{\partial \mathbf{b}}{\partial t} + = - \mathbf{u_e^T} \, \frac{\partial \mathbf{s_m}}{\partial t} \\ + &\mathbf{u_f^T C \, h} - \mathbf{u_f^T j} = \mathbf{u_f^T s_e} \\ + &\mathbf{u_f^T M_f e} = \mathbf{u_f^T M_{f\rho} \, j} \\ + &\mathbf{u_e^T M_e b} = \mathbf{u_e^T M_{e \mu} h} - \mathbf{j} = \nabla \times \mathbf{h} - \mathbf{s_e} + where + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_e}` is the edge inner-product matrix + * :math:`\mathbf{M_f}` is the face inner-product matrix + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces - giving + Cancelling like-terms and combining the discrete expressions in terms of the magnetic field, we obtain: .. math:: + \mathbf{C^T M_{f\rho} C \, h} + \mathbf{M_{e\mu}} \frac{\partial \mathbf{h}}{\partial t} + = \mathbf{C^T M_{f\rho} s_e} - \frac{\partial \mathbf{s_m}}{\partial t} - \nabla \times \rho \nabla \times \mathbf{h} + \mu \frac{\partial h}{\partial t} - = \nabla \times \rho \mathbf{s_e} + \mathbf{s_m} + Finally, we discretize in time according to backward Euler. The discrete magnetic field + on mesh edges at time :math:`t_k > t_0` is obtained by solving the following at each time-step: + .. math:: + \mathbf{A}_k \mathbf{h}_k = \mathbf{q}_k - \mathbf{B}_k \mathbf{h}_{k-1} + + where :math:`\Delta t_k = t_k - t_{k-1}` and + + .. math:: + &\mathbf{A}_k = \mathbf{C^T M_{f\rho} C} + \frac{1}{\Delta t_k} \mathbf{M_{e\mu}} \\ + &\mathbf{B}_k = -\frac{1}{\Delta t_k} \mathbf{M_{e\mu}}\\ + &\mathbf{q}_k = \mathbf{C^T M_{f\rho} s}_{\mathbf{e},k} \; + - \frac{1}{\Delta t_k} \big [ \mathbf{s}_{\mathbf{m},k} + \mathbf{s}_{\mathbf{m},k-1} \big ] + + Although the following system is never explicitly formed, we can represent + the solution at all time-steps as: + + .. math:: + \begin{bmatrix} + \mathbf{A_1} & & & & \\ + \mathbf{B_2} & \mathbf{A_2} & & & \\ + & & \ddots & & \\ + & & & \mathbf{B_n} & \mathbf{A_n} + \end{bmatrix} + \begin{bmatrix} + \mathbf{h_1} \\ \mathbf{h_2} \\ \vdots \\ \mathbf{h_n} + \end{bmatrix} = + \begin{bmatrix} + \mathbf{q_1} \\ \mathbf{q_2} \\ \vdots \\ \mathbf{q_n} + \end{bmatrix} - + \begin{bmatrix} + \mathbf{B_1 h_0} \\ \mathbf{0} \\ \vdots \\ \mathbf{0} + \end{bmatrix} + + where the magnetic fields at the initial time :math:`\mathbf{h_0}` + are computed analytically or numerically depending on whether the source + carries non-zero current at the initial time. """ @@ -1007,9 +1841,32 @@ class Simulation3DMagneticField(BaseTDEMSimulation): Fields_Derivs = FieldsDerivativesHJ def getAdiag(self, tInd): - """ - System matrix at a given time index + r"""Diagonal system matrix for the given time-step index. + This method returns the diagonal system matrix for the time-step index provided: + + .. math:: + \mathbf{A}_k = \mathbf{C^T M_{f\rho} C} + \frac{1}{\Delta t_k} \mathbf{M_{e\mu}} + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{M_{f \rho}}` is the resistivity inner-product matrix on faces + * :math:`\mathbf{M_{e\mu}}` is the permeability inner-product matrix on edges + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` + for a full description of the formulation. + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + + Returns + ------- + (n_edges, n_edges) sp.sparse.csr_matrix + The diagonal system matrix. """ assert tInd >= 0 and tInd < self.nT @@ -1021,6 +1878,52 @@ def getAdiag(self, tInd): return C.T * (MfRho * C) + 1.0 / dt * MeMu def getAdiagDeriv(self, tInd, u, v, adjoint=False): + r"""Derivative operation for the diagonal system matrix times a vector. + + The diagonal system matrix for time-step index *k* is given by: + + .. math:: + \mathbf{A}_k = \mathbf{C^T M_{f\rho} C} + \frac{1}{\Delta t_k} \mathbf{M_{e\mu}} + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{M_{f \rho}}` is the resistivity inner-product matrix on faces + * :math:`\mathbf{M_{e\mu}}` is the permeability inner-product matrix on edges + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters, :math:`\mathbf{v}` is a vector + and :math:`\mathbf{h_k}` is the discrete solution for time-step *k*, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A_k \, h_k})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A_k \, h_k})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + u : (n_edges,) numpy.ndarray + The solution for the fields for the current model; i.e. :math:`\mathbf{h_k}`. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. + """ assert tInd >= 0 and tInd < self.nT C = self.mesh.edge_curl @@ -1031,6 +1934,29 @@ def getAdiagDeriv(self, tInd, u, v, adjoint=False): return C.T * self.MfRhoDeriv(C * u, v, adjoint) def getAsubdiag(self, tInd): + r"""Sub-diagonal system matrix for the time-step index provided. + + This method returns the sub-diagonal system matrix for the time-step index provided: + + .. math:: + \mathbf{B}_k = -\frac{1}{\Delta t_k} \mathbf{M_{e\mu}} + + where :math:`\Delta t_k` is the step-length and :math:`\mathbf{M_{e \mu}}` is the + permeability inner-product matrix on edges. + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` + for a full description of the formulation. + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + + Returns + ------- + (n_edges, n_edges) sp.sparse.csr_matrix + The sub-diagonal system matrix. + """ assert tInd >= 0 and tInd < self.nT dt = self.time_steps[tInd] @@ -1038,9 +1964,81 @@ def getAsubdiag(self, tInd): return -1.0 / dt * self.MeMu def getAsubdiagDeriv(self, tInd, u, v, adjoint=False): + r"""Derivative operation for the sub-diagonal system matrix times a vector. + + The sub-diagonal system matrix for time-step index *k* is given by: + + .. math:: + \mathbf{B}_k = -\frac{1}{\Delta t_k} \mathbf{M_{e\mu}} + + where :math:`\Delta t_k` is the step-length and :math:`\mathbf{M_{e \mu}}` is the + permeability inner-product matrix on edges. + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters, :math:`\mathbf{v}` is a vector + and :math:`\mathbf{h_{k-1}}` is the discrete solution for the previous time-step, + this method assumes the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{B_k \, h_{k-1}})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{B_k \, h_{k-1}})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + u : (n_edges,) numpy.ndarray + The solution for the fields for the current model for the previous time-step; + i.e. :math:`\mathbf{h_{k-1}}`. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. + """ return Zero() def getRHS(self, tInd): + r"""Right-hand sides for the given time index. + + This method returns the right-hand sides for the time index provided. + The right-hand side for each source is constructed according to: + + .. math:: + \mathbf{q}_k = \mathbf{C^T M_{f\rho} s}_{\mathbf{e},k} \; + - \frac{1}{\Delta t_k} \big [ \mathbf{s}_{\mathbf{m},k} + \mathbf{s}_{\mathbf{m},k-1} \big ] + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resisitivites projected to faces + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` + for a full description of the formulation. + + Parameters + ---------- + tInd : int + The time index; between ``[0, n_steps]``. + + Returns + ------- + (n_edges, n_sources) numpy.ndarray + The right-hand sides. + """ C = self.mesh.edge_curl MfRho = self.MfRho s_m, s_e = self.getSourceTerm(tInd) @@ -1048,6 +2046,52 @@ def getRHS(self, tInd): return C.T * (MfRho * s_e) + s_m def getRHSDeriv(self, tInd, src, v, adjoint=False): + r"""Derivative of the right-hand side times a vector for a given source and time index. + + The right-hand side for a given source at time index *k* is constructed according to: + + .. math:: + \mathbf{q}_k = \mathbf{C^T M_{f\rho} s}_{\mathbf{e},k} \; + - \frac{1}{\Delta t_k} \big [ \mathbf{s}_{\mathbf{m},k} + \mathbf{s}_{\mathbf{m},k-1} \big ] + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resisitivites projected to faces + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters and :math:`\mathbf{v}` is a vector, + this method returns + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + tInd : int + The time index; between ``[0, n_steps]``. + src : .time_domain.sources.BaseTDEMSrc + The TDEM source object. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of the right-hand sides times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. + """ C = self.mesh.edge_curl s_m, s_e = src.eval(self, self.times[tInd]) @@ -1056,13 +2100,79 @@ def getRHSDeriv(self, tInd, src, v, adjoint=False): # assumes no source derivs return C.T * self.MfRhoDeriv(s_e, v, adjoint) + # I DON'T THINK THIS IS CURRENTLY USED BY THE H-FORMULATION. def getAdc(self): + r"""The system matrix for the DC resistivity problem. + + The solution to the DC resistivity problem is necessary at the initial time for + galvanic sources whose currents are non-zero at the initial time. + The discrete solution to the 3D DC resistivity problem is expressed as: + + .. math:: + \mathbf{A_{dc}}\,\boldsymbol{\phi_0} = \mathbf{q_{dc}} + + where :math:`\mathbf{A_{dc}}` is the DC resistivity system matrix, :math:`\boldsymbol{\phi_0}` + is the discrete solution for the electric potentials at the initial time, and :math:`\mathbf{q_{dc}}` + is the galvanic source term. This method returns the system matrix + for the cell-centered formulation, i.e.: + + .. math:: + \mathbf{D \, M_{f\rho}^{-1} \, G} + + where :math:`\mathbf{D}` is the face divergence operator, :math:`\mathbf{G}` is the cell gradient + operator with imposed boundary conditions, and :math:`\mathbf{M_{f\rho}}` is the inner product + matrix for resistivities projected to faces. + + See the *Notes* section of the doc strings for + :class:`.resistivity.Simulation3DCellCentered` + for a full description of the cell centered DC resistivity formulation. + + Returns + ------- + (n_cells, n_cells) sp.sparse.csr_matrix + The system matrix for the DC resistivity problem. + """ D = sdiag(self.mesh.cell_volumes) * self.mesh.face_divergence G = D.T MfRhoI = self.MfRhoI return D * MfRhoI * G def getAdcDeriv(self, u, v, adjoint=False): + r"""Derivative operation for the DC resistivity system matrix times a vector. + + The discrete solution to the 3D DC resistivity problem is expressed as: + + .. math:: + \mathbf{A_{dc}}\boldsymbol{\phi_0} = \mathbf{q_{dc}} + + where :math:`\mathbf{A_{dc}}` is the DC resistivity system matrix, :math:`\boldsymbol{\phi_0}` + is the discrete solution for the electric potentials at the initial time, and :math:`\mathbf{q_{dc}}` + is the galvanic source term. For a vector :math:`\mathbf{v}`, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A_{dc}}\boldsymbol{\phi_0})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A_{dc}}\boldsymbol{\phi_0})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + u : (n_cells,) numpy.ndarray + The solution for the fields for the current model; i.e. electric potentials at cell centers. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_cells,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of the DC resistivity system matrix times a vector. (n_cells,) for the standard operation. + (n_param,) for the adjoint operation. + """ D = sdiag(self.mesh.cell_volumes) * self.mesh.face_divergence G = D.T @@ -1077,12 +2187,124 @@ def getAdcDeriv(self, u, v, adjoint=False): class Simulation3DCurrentDensity(BaseTDEMSimulation): - r""" - Solve the H-J formulation for current density + r"""3D TDEM simulation in terms of the current density. + + This simulation solves for the current density at each time-step. + In this formulation, the magnetic fields are defined on mesh edges and the + current densities are defined on mesh faces; i.e. it is an HJ formulation. + See the *Notes* section for a comprehensive description of the formulation. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + The mesh. + survey : .time_domain.survey.Survey + The time-domain EM survey. + dt_threshold : float + Threshold used when determining the unique time-step lengths. + + Notes + ----- + Here, we start with the quasi-static approximation for Maxwell's equations by neglecting + electric displacement: + + .. math:: + &\nabla \times \vec{e} + \frac{\partial \vec{b}}{\partial t} = - \frac{\partial \vec{s}_m}{\partial t} \\ + &\nabla \times \vec{h} - \vec{j} = \vec{s}_e + + where :math:`\vec{s}_e` is an electric source term that defines a source current density, + and :math:`\vec{s}_m` magnetic source term that defines a source magnetic flux density. + We define the constitutive relations for the electrical resistivity :math:`\rho` + and magnetic permeability :math:`\mu` as: + + .. math:: + \vec{e} &= \rho \vec{e} \\ + \vec{b} &= \mu \vec{h} + + We then take the inner products of all previous expressions with a vector test function :math:`\vec{u}`. + Through vector calculus identities and the divergence theorem, we obtain: + + .. math:: + & \int_\Omega (\nabla \times \vec{u}) \cdot \vec{e} \; dv + - \oint_{\partial \Omega} \vec{u} \cdot (\vec{e} \times \hat{n} ) \, da + + \int_\Omega \vec{u} \cdot \frac{\partial \vec{b}}{\partial t} \, dv + = - \int_\Omega \vec{u} \cdot \frac{\partial \vec{s}_m}{\partial t} \, dv \\ + & \int_\Omega \vec{u} \cdot (\nabla \times \vec{h} ) \, dv - \int_\Omega \vec{u} \cdot \vec{j} \, dv + = \int_\Omega \vec{u} \cdot \vec{s}_e \, dv\\ + & \int_\Omega \vec{u} \cdot \vec{e} \, dv = \int_\Omega \vec{u} \cdot \rho \vec{j} \, dv \\ + & \int_\Omega \vec{u} \cdot \vec{b} \, dv = \int_\Omega \vec{u} \cdot \mu \vec{h} \, dv + + Assuming natural boundary conditions, the surface integral is zero. + + The above expressions are discretized in space according to the finite volume method. + The discrete magnetic fields :math:`\mathbf{h}` are defined on mesh edges, + and the discrete current densities :math:`\mathbf{j}` are defined on mesh faces. + This implies :math:`\mathbf{b}` must be defined on mesh edges and :math:`\mathbf{e}` must + be defined on mesh faces. Where :math:`\mathbf{u_e}` and :math:`\mathbf{u_f}` represent + test functions discretized to edges and faces, respectively, we obtain the following + set of discrete inner-products: + + .. math:: + &\mathbf{u_e^T C^T M_f \, e } + \mathbf{u_e^T M_e} \frac{\partial \mathbf{b}}{\partial t} + = - \mathbf{u_e^T} \, \frac{\partial \mathbf{s_m}}{\partial t} \\ + &\mathbf{u_f^T C \, h} - \mathbf{u_f^T j} = \mathbf{u_f^T s_e} \\ + &\mathbf{u_f^T M_f e} = \mathbf{u_f^T M_{f\rho} \, j} \\ + &\mathbf{u_e^T M_e b} = \mathbf{u_e^T M_{e \mu} h} + + where + + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_e}` is the edge inner-product matrix + * :math:`\mathbf{M_f}` is the face inner-product matrix + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces + + Cancelling like-terms and combining the discrete expressions in terms of the current density, we obtain: + + .. math:: + \mathbf{C M_{e\mu}^{-1} C^T M_{f\rho} \, j} + + \frac{\partial \mathbf{j}}{\partial t} = + - \frac{\partial \mathbf{s_e}}{\partial t} + - \mathbf{C M_{e\mu}^{-1}} \frac{\partial \mathbf{s_m}}{\partial t} + + Finally, we discretize in time according to backward Euler. The discrete current density + on mesh edges at time :math:`t_k > t_0` is obtained by solving the following at each time-step: + + .. math:: + \mathbf{A}_k \mathbf{j}_k = \mathbf{q}_k - \mathbf{B}_k \mathbf{j}_{k-1} + + where :math:`\Delta t_k = t_k - t_{k-1}` and - In this case, we eliminate :math:`\partial \mathbf{h} / \partial t` and - solve for :math:`\mathbf{j}` + .. math:: + &\mathbf{A}_k = \mathbf{C M_{e\mu}^{-1} C^T M_{f\rho}} + \frac{1}{\Delta t_k} \mathbf{I} \\ + &\mathbf{B}_k = - \frac{1}{\Delta t_k} \mathbf{I}\\ + &\mathbf{q}_k = - \frac{1}{\Delta t_k} \big [ \mathbf{s}_{\mathbf{e},k} + \mathbf{s}_{\mathbf{e},k-1} \big ] \; + - \; \frac{1}{\Delta t_k} \mathbf{C M_{e\mu}^{-1}} \big [ \mathbf{s}_{\mathbf{m},k} + \mathbf{s}_{\mathbf{m},k-1} \big ] + + Although the following system is never explicitly formed, we can represent + the solution at all time-steps as: + .. math:: + \begin{bmatrix} + \mathbf{A_1} & & & & \\ + \mathbf{B_2} & \mathbf{A_2} & & & \\ + & & \ddots & & \\ + & & & \mathbf{B_n} & \mathbf{A_n} + \end{bmatrix} + \begin{bmatrix} + \mathbf{j_1} \\ \mathbf{j_2} \\ \vdots \\ \mathbf{j_n} + \end{bmatrix} = + \begin{bmatrix} + \mathbf{q_1} \\ \mathbf{q_2} \\ \vdots \\ \mathbf{q_n} + \end{bmatrix} - + \begin{bmatrix} + \mathbf{B_1 j_0} \\ \mathbf{0} \\ \vdots \\ \mathbf{0} + \end{bmatrix} + + where the current densities at the initial time :math:`\mathbf{j_0}` + are computed analytically or numerically depending on whether the source + carries non-zero current at the initial time. """ _fieldType = "j" @@ -1091,9 +2313,33 @@ class Simulation3DCurrentDensity(BaseTDEMSimulation): Fields_Derivs = FieldsDerivativesHJ def getAdiag(self, tInd): - """ - System matrix at a given time index + r"""Diagonal system matrix for the given time-step index. + + This method returns the diagonal system matrix for the time-step index provided: + .. math:: + \mathbf{A}_k = \mathbf{C M_{e\mu}^{-1} C^T M_{f\rho}} + \frac{1}{\Delta t_k} \mathbf{I} + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{I}` is the identity matrix + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{M_{e \mu}}` is the permeability inner-product matrix on faces + * :math:`\mathbf{M_{f \rho}}` is the permeability inner-product matrix on edges + + See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` + for a full description of the formulation. + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + + Returns + ------- + (n_faces, n_faces) sp.sparse.csr_matrix + The diagonal system matrix. """ assert tInd >= 0 and tInd < self.nT @@ -1111,6 +2357,53 @@ def getAdiag(self, tInd): return A def getAdiagDeriv(self, tInd, u, v, adjoint=False): + r"""Derivative operation for the diagonal system matrix times a vector. + + The diagonal system matrix for time-step index *k* is given by: + + .. math:: + \mathbf{A}_k = \mathbf{C M_{e\mu}^{-1} C^T M_{f\rho}} + \frac{1}{\Delta t_k} \mathbf{I} + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{I}` is the identity matrix + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{M_{e \mu}}` is the permeability inner-product matrix on faces + * :math:`\mathbf{M_{f \rho}}` is the permeability inner-product matrix on edges + + See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters, :math:`\mathbf{v}` is a vector + and :math:`\mathbf{j_k}` is the discrete solution for time-step *k*, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A_k \, j_k})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A_k \, j_k})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + u : (n_faces,) numpy.ndarray + The solution for the fields for the current model; i.e. :math:`\mathbf{j_k}`. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. + """ assert tInd >= 0 and tInd < self.nT C = self.mesh.edge_curl @@ -1128,6 +2421,29 @@ def getAdiagDeriv(self, tInd, u, v, adjoint=False): return ADeriv def getAsubdiag(self, tInd): + r"""Sub-diagonal system matrix for the time-step index provided. + + This method returns the sub-diagonal system matrix for the time-step index provided: + + .. math:: + \mathbf{B}_k = -\frac{1}{\Delta t_k} \mathbf{I} + + where :math:`\Delta t_k` is the step-length and :math:`\mathbf{I}` is the + identity matrix. + + See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` + for a full description of the formulation. + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + + Returns + ------- + (n_faces, n_faces) sp.sparse.csr_matrix + The sub-diagonal system matrix. + """ assert tInd >= 0 and tInd < self.nT eye = sp.eye(self.mesh.n_faces) @@ -1138,9 +2454,81 @@ def getAsubdiag(self, tInd): return -1.0 / dt * eye def getAsubdiagDeriv(self, tInd, u, v, adjoint=False): + r"""Derivative operation for the sub-diagonal system matrix times a vector. + + The sub-diagonal system matrix for time-step index *k* is given by: + + .. math:: + \mathbf{B}_k = -\frac{1}{\Delta t_k} \mathbf{I} + + where :math:`\Delta t_k` is the step-length and :math:`\mathbf{I}` is the + identity matrix. + + See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters, :math:`\mathbf{v}` is a vector + and :math:`\mathbf{j_{k-1}}` is the discrete solution for the previous time-step, + this method assumes the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{B_k \, j_{k-1}})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{B_k \, j_{k-1}})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + u : (n_faces,) numpy.ndarray + The solution for the fields for the current model for the previous time-step; + i.e. :math:`\mathbf{j_{k-1}}`. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. + """ return Zero() def getRHS(self, tInd): + r"""Right-hand sides for the given time index. + + This method returns the right-hand sides for the time index provided. + The right-hand side for each source is constructed according to: + + .. math:: + \mathbf{q}_k = - \frac{1}{\Delta t_k} \big [ \mathbf{s}_{\mathbf{e},k} + \mathbf{s}_{\mathbf{e},k-1} \big ] \; + - \; \frac{1}{\Delta t_k} \mathbf{C M_{e\mu}^{-1}} \big [ \mathbf{s}_{\mathbf{m},k} + \mathbf{s}_{\mathbf{m},k-1} \big ] + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` + for a full description of the formulation. + + Parameters + ---------- + tInd : int + The time index; between ``[0, n_steps]``. + + Returns + ------- + (n_faces, n_sources) numpy.ndarray + The right-hand sides. + """ if tInd == len(self.time_steps): tInd = tInd - 1 @@ -1156,15 +2544,130 @@ def getRHS(self, tInd): return rhs def getRHSDeriv(self, tInd, src, v, adjoint=False): + r"""Derivative of the right-hand side times a vector for a given source and time index. + + The right-hand side for a given source at time index *k* is constructed according to: + + .. math:: + \mathbf{q}_k = - \frac{1}{\Delta t_k} \big [ \mathbf{s}_{\mathbf{e},k} + \mathbf{s}_{\mathbf{e},k-1} \big ] \; + - \; \frac{1}{\Delta t_k} \mathbf{C M_{e\mu}^{-1}} \big [ \mathbf{s}_{\mathbf{m},k} + \mathbf{s}_{\mathbf{m},k-1} \big ] + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters and :math:`\mathbf{v}` is a vector, + this method returns + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + tInd : int + The time index; between ``[0, n_steps]``. + src : .time_domain.sources.BaseTDEMSrc + The TDEM source object. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of the right-hand sides times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. + """ return Zero() # assumes no derivs on sources def getAdc(self): + r"""The system matrix for the DC resistivity problem. + + The solution to the DC resistivity problem is necessary at the initial time for + galvanic sources whose currents are non-zero at the initial time. + The discrete solution to the 3D DC resistivity problem is expressed as: + + .. math:: + \mathbf{A_{dc}}\,\boldsymbol{\phi_0} = \mathbf{q_{dc}} + + where :math:`\mathbf{A_{dc}}` is the DC resistivity system matrix, :math:`\boldsymbol{\phi_0}` + is the discrete solution for the electric potentials at the initial time, and :math:`\mathbf{q_{dc}}` + is the galvanic source term. This method returns the system matrix + for the cell-centered formulation, i.e.: + + .. math:: + \mathbf{D \, M_{f\rho}^{-1} \, G} + + where :math:`\mathbf{D}` is the face divergence operator, :math:`\mathbf{G}` is the cell gradient + operator with imposed boundary conditions, and :math:`\mathbf{M_{f\rho}}` is the inner product + matrix for resistivities projected to faces. + + The current density at the initial time :math:`\mathbf{j_0}` are obtained by applying: + + .. math:: + \mathbf{j_0} = \mathbf{M_{f\rho}^{-1} \, G} \, \boldsymbol{\phi_0} + + See the *Notes* section of the doc strings for :class:`.resistivity.Simulation3DCellCentered` + for a full description of the cell centered DC resistivity formulation. + + Returns + ------- + (n_cells, n_cells) sp.sparse.csr_matrix + The system matrix for the DC resistivity problem. + """ D = sdiag(self.mesh.cell_volumes) * self.mesh.face_divergence G = D.T MfRhoI = self.MfRhoI return D * MfRhoI * G def getAdcDeriv(self, u, v, adjoint=False): + r"""Derivative operation for the DC resistivity system matrix times a vector. + + The discrete solution to the 3D DC resistivity problem is expressed as: + + .. math:: + \mathbf{A_{dc}}\boldsymbol{\phi_0} = \mathbf{q_{dc}} + + where :math:`\mathbf{A_{dc}}` is the DC resistivity system matrix, :math:`\boldsymbol{\phi_0}` + is the discrete solution for the electric potentials at the initial time, and :math:`\mathbf{q_{dc}}` + is the galvanic source term. For a vector :math:`\mathbf{v}`, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A_{dc}}\boldsymbol{\phi_0})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A_{dc}}\boldsymbol{\phi_0})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + u : (n_cells,) numpy.ndarray + The solution for the fields for the current model; i.e. electric potentials at cell centers. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_cells,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of the DC resistivity system matrix times a vector. (n_cells,) for the standard operation. + (n_param,) for the adjoint operation. + """ D = sdiag(self.mesh.cell_volumes) * self.mesh.face_divergence G = D.T diff --git a/simpeg/fields.py b/simpeg/fields.py index d3eb461c27..fd20be1716 100644 --- a/simpeg/fields.py +++ b/simpeg/fields.py @@ -5,24 +5,71 @@ class Fields: - """Fancy Field Storage + r"""Base class for storing fields. + + Fields classes are used to store the discrete field solution for a + corresponding simulation object; see :py:class:`SimPEG.simulation.BaseSimulation`. + Generally only one field solution (e.g. ``'eSolution'``, ``'phiSolution'``, ``'bSolution'``) is stored. + However, it may be possible to extract multiple field types (e.g. ``'e'``, ``'b'``, ``'j'``, ``'h'``) + on the fly from the fields object. The field solution that is stored and the + field types that can be extracted depend on the formulation used by the associated simulation. + See the example below to learn how fields are extracted from fields objects. + + Parameters + ---------- + simulation : SimPEG.simulation.BaseSimulation + The simulation object used to compute the discrete field solution. + knownFields : dict of {key: str}, optional + Dictionary defining the field solutions that are stored and where + on the mesh they are discretized. E.g. ``{'eSolution': 'E', 'bSolution': 'F'}`` + would store the `eSolution` on edges and `bSolution` on faces. + The ``str`` must be one of {``'CC'``, ``'N'``, ``'E'``, ``'F'``}. + aliasFields : dict of {key: list}, optional + Set aliases to extract different field types from the field solutions that are + stored by the fields object. The ``key`` defines the name you would like to use + when extracting a given field type from the fields object. In order, the list + contains: + + * the key for the known field solution that is used to compute the field type + * where the output field type lives {``'CC'``, ``'N'``, ``'E'``, ``'F'``} + * the name of the method used to compute the output field. + + E.g. ``{'b': ['eSolution', 'F', '_b']}`` is an alias that + would allow you to extract a field type (``'b'``) that lives on mesh faces (``'F'``) + from the E-field solution (``'eSolution'``) by calling a method (``'_b'``). + dtype : dtype or dict of {str : dtype}, optional + Set the Python data type for each numerical field solution that is stored in + the fields object. E.g. ``float``, ``complex``, + ``{'eSolution': complex, 'bSolution': complex}``. Examples -------- - >>> fields = Fields( - ... simulation=simulation, knownFields={"phi": "CC"} - ... ) - >>> fields[:,'phi'] = phi + We want to access the fields for a discrete solution with :math:`\mathbf{e}` discretized + to edges and :math:`\mathbf{b}` discretized to faces. To extract the fields for all sources: + + .. code-block:: python + + f = simulation.fields(m) + e = f[:,'e'] + b = f[:,'b'] + + The array ``e`` returned will have shape (`n_edges`, `n_sources`). And the array ``b`` + returned will have shape (`n_faces`, `n_sources`). We can also extract the fields for + a subset of the source list used for the simulation as follows: + + .. code-block:: python + + f = simulation.fields(m) + e = f[source_list,'e'] + b = f[source_list,'b'] + """ _dtype = float _knownFields = {} _aliasFields = {} - def __init__( - self, simulation, knownFields=None, aliasFields=None, dtype=None, **kwargs - ): - super().__init__(**kwargs) + def __init__(self, simulation, knownFields=None, aliasFields=None, dtype=None): self.simulation = simulation if knownFields is not None: @@ -45,11 +92,12 @@ def __init__( @property def simulation(self): - """The simulation object that created these fields + """The simulation object used to compute the field solution. Returns ------- - simpeg.simulation.BaseSimulation + SimPEG.simulation.BaseSimulation + The simulation object used to compute the field solution. """ return self._simulation @@ -61,39 +109,41 @@ def simulation(self, value): @property def knownFields(self): - """The known fields of this object. - - The dictionary representing the known fields and their locations on the simulation - mesh. The keys are the names of the fields, and the values are the location on - the mesh. + """The field solutions and where they are discretized on the mesh. - >>> fields.knownFields - {'e': 'E', 'phi': 'CC'} + Dictionary defining the field solutions that are stored and where + on the mesh they are discretized. The ``key`` defines the name + of the field solution that is stored, and a ``str`` defines where + on the mesh the stored field solution is discretized. The + ``str`` must be one of {``'CC'``, ``'N'``, ``'E'``, ``'F'``}. - Would represent that the `e` field and `phi` fields are known, and they are - located on the mesh edges and cell centers, respectively. + E.g. ``{'eSolution': 'E', 'bSolution': 'F'}`` + would define the `eSolution` on edges and `bSolution` on faces. Returns ------- dict - They keys are the field names and the values are the field locations. + The keys are the field solution names and the values {'N', 'CC', 'E'. 'F'} + define where the field solution is discretized. """ return self._knownFields @property def aliasFields(self): - """The aliased fields of this object. + """The aliased fields of the object. - The dictionary representing the aliased fields that can be accessed on this - object. The keys are the names of the fields, and the values are a list of the - known field, the aliased field's location on the mesh, and a function that goes - from the known field to the aliased field. + Aliases are defined to extract different field types from the field solutions that are + stored by the fields object. The ``key`` defines the name you would like to use + when extracting a given field type from the fields object. In order, the list + contains: - >>> fields.aliasFields - {'b': ['e', 'F', '_e']} + * the key for the known field solution that is used to compute the field type + * where the output field type lives {``'CC'``, ``'N'``, ``'E'``, ``'F'``} + * the name of the method used to compute the output field. - Would represent that the `e` field and `phi` fields are known, and they are - located on the mesh edges and cell centers, respectively. + E.g. ``{'b': ['eSolution', 'F', '_b']}`` is an alias that + would allow you to extract a field type ('b') that lives on mesh faces ('F') + from the E-field solution ('eSolution') by calling a method ('_b'). Returns ------- @@ -106,28 +156,53 @@ def aliasFields(self): @property def dtype(self): - """The data type of the storage matrix + """Python data type(s) used to store the fields. + + the Python data type for each numerical field solution that is stored in + the fields object. E.g. ``float``, ``complex``, ``{'eSolution': complex, 'bSolution': complex}``. Returns ------- dtype or dict of {str : dtype} + Python data type(s) used to store the fields. """ return self._dtype @property def mesh(self): + """Mesh used by the simulation. + + Returns + ------- + discretize.BaseMesh + Mesh used by the simulation. + """ return self.simulation.mesh @property def survey(self): + """Survey used by the simulation. + + Returns + ------- + SimPEG.survey.BaseSurvey + Survey used by the simulation. + """ return self.simulation.survey def startup(self): + """Run startup to connect the simulation's discrete attributes to the fields object.""" pass @property def approxSize(self): - """The approximate cost to storing all of the known fields.""" + """Approximate cost of storing all of the known fields in MB. + + Returns + ------- + int + Approximate cost of storing all of the known fields in MB. + """ sz = 0.0 for f in self.knownFields: loc = self.knownFields[f] @@ -273,21 +348,73 @@ def __contains__(self, other): class TimeFields(Fields): - """Fancy Field Storage for time domain problems - .. code:: python + r"""Base class for storing TDEM fields. + + ``TimeFields`` is a base class for storing discrete field solutions for simulations + that use discrete time-stepping; see :py:class:`SimPEG.simulation.BaseTimeSimulation`. + Generally only one field solution (e.g. ``'eSolution'``, ``'phiSolution'``, ``'bSolution'``) is stored. + However, it may be possible to extract multiple field types (e.g. ``'e'``, ``'b'``, ``'j'``, ``'h'``) + on the fly from the fields object. The field solution that is stored and the + field types that can be extracted depend on the formulation used by the associated simulation. + See the example below to learn how fields are extracted from fields objects. + + Parameters + ---------- + simulation : SimPEG.simulation.BaseTimeSimulation + The simulation object used to compute the discrete field solution. + knownFields : dict of {key: str}, optional + Dictionary defining the field solutions that are stored and where + on the mesh they are discretized. E.g. ``{'eSolution': 'E', 'bSolution': 'F'}`` + would store the `eSolution` on edges and `bSolution` on faces. + The ``str`` must be one of {``'CC'``, ``'N'``, ``'E'``, ``'F'``}. + aliasFields : dict of {key: list}, optional + Set aliases to extract different field types from the field solutions that are + stored by the fields object. The ``key`` defines the name you would like to use + when extracting a given field type from the fields object. In order, the list + contains: + + * the key for the known field solution that is used to compute the field type + * where the output field type lives {``'CC'``, ``'N'``, ``'E'``, ``'F'``} + * the name of the method used to compute the output field. + + E.g. ``{'b': ['eSolution', 'F', '_b']}`` is an alias that + would allow you to extract a field type ('b') that lives on mesh faces ('F') + from the E-field solution ('eSolution') by calling a method ('_b'). + dtype : dtype or dict of {str : dtype}, optional + Set the Python data type for each numerical field solution that is stored in + the fields object. E.g. ``float``, ``complex``, ``{'eSolution': complex, 'bSolution': complex}``. + + Examples + -------- + We want to access the fields for a discrete solution with :math:`\mathbf{e}` discretized + to edges and :math:`\mathbf{b}` discretized to faces. To extract the fields for all sources: + + .. code-block:: python + + f = simulation.fields(m) + e = f[:, 'e', :] + b = f[:, 'b', :] + + The array ``e`` returned will have shape (`n_edges`, `n_sources`, `n_steps`). And the array ``b`` + returned will have shape (`n_faces`, `n_sources`, `n_steps`). We can also extract the fields for + a subset of the source list used for the simulation and/or a subset of the time steps as follows: + + .. code-block:: python + + f = simulation.fields(m) + e = f[source_list, 'e', t_inds] + b = f[source_list, 'b', t_inds] - fields = TimeFields(simulation=simulation, knownFields={'phi':'CC'}) - fields[:,'phi', timeInd] = phi - print(fields[src0,'phi']) """ @property def simulation(self): - """The simulation object that created these fields + """The simulation object used to compute the field solution. Returns ------- - simpeg.simulation.BaseTimeSimulation + SimPEG.simulation.BaseTimeSimulation + The simulation object used to compute the field solution. """ return self._simulation diff --git a/simpeg/regularization/base.py b/simpeg/regularization/base.py index 823c7fd0de..3165244228 100644 --- a/simpeg/regularization/base.py +++ b/simpeg/regularization/base.py @@ -311,7 +311,7 @@ def get_weights(self, key) -> np.ndarray: """Cell weights for a given key. Parameters - ------------ + ---------- key: str Name of the weights requested. diff --git a/simpeg/utils/pgi_utils.py b/simpeg/utils/pgi_utils.py index a11fe9fb56..7141fccc98 100644 --- a/simpeg/utils/pgi_utils.py +++ b/simpeg/utils/pgi_utils.py @@ -158,13 +158,13 @@ def compute_clusters_covariances(self): def order_clusters_GM_weight(self, outputindex=False): """Order clusters by decreasing weights - PARAMETERS + Parameters ---------- outputindex : bool, default: ``True`` If ``True``, return the sorting index - RETURN - ------ + Returns + ------- np.ndarray Sorting index """ @@ -194,6 +194,7 @@ def _check_weights(self, weights, n_components, n_samples): """ [modified from Scikit-Learn.mixture.gaussian_mixture] Check the user provided 'weights'. + Parameters ---------- weights : array-like, shape (n_components,) or (n_samples, n_components) @@ -271,6 +272,7 @@ def _initialize_parameters(self, X, random_state): """ [modified from Scikit-Learn.mixture._base] Initialize the model parameters. + Parameters ---------- X : array-like, shape (n_samples, n_features) @@ -303,6 +305,7 @@ def _m_step(self, X, log_resp): """ [modified from Scikit-Learn.mixture.gaussian_mixture] M step. + Parameters ---------- X : array-like, shape (n_samples, n_features) @@ -327,6 +330,7 @@ def _estimate_gaussian_covariances_tied(self, resp, X, nk, means, reg_covar): """ [modified from Scikit-Learn.mixture.gaussian_mixture] Estimate the tied covariance matrix. + Parameters ---------- resp : array-like, shape (n_samples, n_components) @@ -334,6 +338,7 @@ def _estimate_gaussian_covariances_tied(self, resp, X, nk, means, reg_covar): nk : array-like, shape (n_components,) means : array-like, shape (n_components, n_features) reg_covar : float + Returns ------- covariance : array, shape (n_features, n_features) @@ -350,6 +355,7 @@ def _estimate_gaussian_parameters(self, X, mesh, resp, reg_covar, covariance_typ """ [modified from Scikit-Learn.mixture.gaussian_mixture] Estimate the Gaussian distribution parameters. + Parameters ---------- X : array-like, shape (n_samples, n_features) @@ -360,6 +366,7 @@ def _estimate_gaussian_parameters(self, X, mesh, resp, reg_covar, covariance_typ The regularization added to the diagonal of the covariance matrices. covariance_type : {'full', 'tied', 'diag', 'spherical'} The type of precision matrices. + Returns ------- nk : array-like, shape (n_components,) @@ -385,9 +392,11 @@ def _e_step(self, X): """ [modified from Scikit-Learn.mixture.gaussian_mixture] E step. + Parameters ---------- X : array-like, shape (n_samples, n_features) + Returns ------- log_prob_norm : float @@ -426,6 +435,7 @@ def _estimate_log_gaussian_prob_with_sensW( """ [New function, modified from Scikit-Learn.mixture.gaussian_mixture._estimate_log_gaussian_prob] Estimate the log Gaussian probability with depth or sensitivity weighting. + Parameters ---------- X : array-like, shape (n_samples, n_features) @@ -438,6 +448,7 @@ def _estimate_log_gaussian_prob_with_sensW( 'diag' : shape of (n_components, n_features) 'spherical' : shape of (n_components,) covariance_type : {'full', 'tied', 'diag', 'spherical'} + Returns ------- log_prob : array, shape (n_samples, n_components) @@ -484,9 +495,11 @@ def _estimate_weighted_log_prob_with_sensW(self, X, sensW): """ [New function, modified from Scikit-Learn.mixture.gaussian_mixture._estimate_weighted_log_prob] Estimate the weighted log-probabilities, log P(X | Z) + log weights. + Parameters ---------- X : array-like, shape (n_samples, n_features) + Returns ------- weighted_log_prob : array, shape (n_samples, n_component) @@ -1254,6 +1267,7 @@ def _initialize(self, X, resp): """ [modified from Scikit-Learn.mixture.gaussian_mixture] Initialization of the Gaussian mixture parameters. + Parameters ---------- X : array-like, shape (n_samples, n_features) @@ -1295,6 +1309,7 @@ def _estimate_log_gaussian_prob( """ [modified from Scikit-Learn.mixture.gaussian_mixture] Estimate the log Gaussian probability. + Parameters ---------- X : array-like, shape (n_samples, n_features) @@ -1306,6 +1321,7 @@ def _estimate_log_gaussian_prob( 'diag' : shape of (n_components, n_features) 'spherical' : shape of (n_components,) covariance_type : {'full', 'tied', 'diag', 'spherical'} + Returns ------- log_prob : array, shape (n_samples, n_components) From 5d489f3d854ac27a0fa3528090160539fb895038 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 31 May 2024 10:41:36 -0700 Subject: [PATCH 014/194] Magnetic simulation with Choclo as engine (#1321) Add new implementation of the magnetic simulation using the integral formulation based on [Choclo](https://www.fatiando.org/choclo) and Numba. Allow users to run the simulation with Choclo as engine through a new `engine` argument of the constructor of the simulation class. Add `numba_parallel` argument to control if the simulation should be run in parallel or in a single thread. Move common methods and properties of the gravity and magnetic simulation to the base class for potential field simulations. Add new `potential_fields/_numba_utils.py` file with functions that are needed both in the magnetic and gravity Numba implementations. Refactor and extend tests for the magnetic simulation, including tests that assert the accuracy of the new implementation against analytic solutions. Add new tests for the base class for potential field simulations. Update examples using Choclo as engine, include admonitions instructing how to use it. --------- Co-authored-by: Jacob Edman Co-authored-by: Joseph Capriotti --- simpeg/potential_fields/_numba_utils.py | 43 + simpeg/potential_fields/base.py | 137 ++- .../gravity/_numba_functions.py | 68 +- simpeg/potential_fields/gravity/simulation.py | 110 +- .../magnetics/_numba_functions.py | 659 +++++++++++ .../potential_fields/magnetics/simulation.py | 243 +++- tests/pf/test_base_pf_simulation.py | 306 +++++ tests/pf/test_forward_Grav_Linear.py | 5 +- tests/pf/test_forward_Mag_Linear.py | 1049 +++++++++++------ .../04-magnetics/plot_2a_magnetics_induced.py | 14 + .../04-magnetics/plot_2b_magnetics_mvi.py | 5 + .../plot_inv_2a_magnetics_induced.py | 12 + 12 files changed, 2118 insertions(+), 533 deletions(-) create mode 100644 simpeg/potential_fields/_numba_utils.py create mode 100644 simpeg/potential_fields/magnetics/_numba_functions.py create mode 100644 tests/pf/test_base_pf_simulation.py diff --git a/simpeg/potential_fields/_numba_utils.py b/simpeg/potential_fields/_numba_utils.py new file mode 100644 index 0000000000..2bdea6da2a --- /dev/null +++ b/simpeg/potential_fields/_numba_utils.py @@ -0,0 +1,43 @@ +""" +Utility functions for Numba implementations + +These functions are meant to be used both in the Numba-based gravity and +magnetic simulations. +""" + +try: + from numba import jit +except ImportError: + # Define dummy jit decorator + def jit(*args, **kwargs): + return lambda f: f + + +@jit(nopython=True) +def kernels_in_nodes_to_cell(kernels, nodes_indices): + """ + Evaluate integral on a given cell from evaluation of kernels on nodes + + Parameters + ---------- + kernels : (n_active_nodes,) array + Array with kernel values on each one of the nodes in the mesh. + nodes_indices : (8,) array of int + Indices of the nodes for the current cell in "F" order (x changes + faster than y, and y faster than z). + + Returns + ------- + float + """ + result = ( + -kernels[nodes_indices[0]] + + kernels[nodes_indices[1]] + + kernels[nodes_indices[2]] + - kernels[nodes_indices[3]] + + kernels[nodes_indices[4]] + - kernels[nodes_indices[5]] + - kernels[nodes_indices[6]] + + kernels[nodes_indices[7]] + ) + return result diff --git a/simpeg/potential_fields/base.py b/simpeg/potential_fields/base.py index d457483e3e..db63463a1a 100644 --- a/simpeg/potential_fields/base.py +++ b/simpeg/potential_fields/base.py @@ -10,6 +10,11 @@ from ..simulation import LinearSimulation from ..utils import validate_active_indices, validate_integer, validate_string +try: + import choclo +except ImportError: + choclo = None + ############################################################################### # # # Base Potential Fields Simulation # @@ -47,7 +52,14 @@ class BasePFSimulation(LinearSimulation): n_processes : None or int, optional The number of processes to use in the internal multiprocessing pool for forward modeling. The default value of 1 will not use multiprocessing. Any other setting - will. `None` implies setting by the number of cpus. + will. `None` implies setting by the number of cpus. If engine is + ``"choclo"``, then this argument will be ignored. + engine : {"geoana", "choclo"}, optional + Choose which engine should be used to run the forward model. + numba_parallel : bool, optional + If True, the simulation will run in parallel. If False, it will + run in serial. If ``engine`` is not ``"choclo"`` this argument will be + ignored. Notes ----- @@ -73,6 +85,8 @@ def __init__( store_sensitivities="ram", n_processes=1, sensitivity_dtype=np.float32, + engine="geoana", + numba_parallel=True, **kwargs, ): # If deprecated property set with kwargs @@ -88,10 +102,18 @@ def __init__( self.store_sensitivities = store_sensitivities self.sensitivity_dtype = sensitivity_dtype + self.engine = engine + self.numba_parallel = numba_parallel super().__init__(mesh, **kwargs) self.solver = None self.n_processes = n_processes + # Check sensitivity_path when engine is "choclo" + self._check_engine_and_sensitivity_path() + + # Check dimensions of the mesh when engine is "choclo" + self._check_engine_and_mesh_dimensions() + # Find non-zero cells indices if ind_active is None: ind_active = np.ones(mesh.n_cells, dtype=bool) @@ -188,6 +210,54 @@ def n_processes(self, value): value = validate_integer("n_processes", value, min_val=1) self._n_processes = value + @property + def engine(self) -> str: + """ + Engine that will be used to run the simulation. + + It can be either ``"geoana"`` or "``choclo``". + """ + return self._engine + + @engine.setter + def engine(self, value: str): + validate_string( + "engine", value, string_list=("geoana", "choclo"), case_sensitive=True + ) + if value == "choclo" and choclo is None: + raise ImportError( + "The choclo package couldn't be found." + "Running a gravity simulation with 'engine=\"choclo\"' needs " + "choclo to be installed." + "\nTry installing choclo with:" + "\n pip install choclo" + "\nor:" + "\n conda install choclo" + ) + self._engine = value + + @property + def numba_parallel(self) -> bool: + """ + Run simulation in parallel or single-threaded when using Numba. + + If True, the simulation will run in parallel. If False, it will + run in serial. + + .. important:: + + If ``engine`` is not ``"choclo"`` this property will be ignored. + """ + return self._numba_parallel + + @numba_parallel.setter + def numba_parallel(self, value: bool): + if not isinstance(value, bool): + raise TypeError( + f"Invalid 'numba_parallel' value of type {type(value)}. Must be a bool." + ) + self._numba_parallel = value + @property def ind_active(self): """Active topography cells. @@ -255,6 +325,71 @@ def linear_operator(self): np.save(sens_name, kernel) return kernel + def _check_engine_and_sensitivity_path(self): + """ + Check if sensitivity_path is a file if engine is set to "choclo" + """ + if ( + self.engine == "choclo" + and self.store_sensitivities == "disk" + and os.path.isdir(self.sensitivity_path) + ): + raise ValueError( + f"The passed sensitivity_path '{self.sensitivity_path}' is " + "a directory. " + "When using 'choclo' as the engine, 'senstivity_path' " + "should be the path to a new or existing file." + ) + + def _check_engine_and_mesh_dimensions(self): + """ + Check dimensions of the mesh when using choclo as engine + """ + if self.engine == "choclo" and self.mesh.dim != 3: + raise ValueError( + f"Invalid mesh with {self.mesh.dim} dimensions. " + "Only 3D meshes are supported when using 'choclo' as engine." + ) + + def _get_active_nodes(self): + """ + Return locations of nodes only for active cells + + Also return an array containing the indices of the "active nodes" for + each active cell in the mesh + """ + # Get all nodes in the mesh + if isinstance(self.mesh, discretize.TreeMesh): + nodes = self.mesh.total_nodes + elif isinstance(self.mesh, discretize.TensorMesh): + nodes = self.mesh.nodes + else: + raise TypeError(f"Invalid mesh of type {self.mesh.__class__.__name__}.") + # Get original cell_nodes but only for active cells + cell_nodes = self.mesh.cell_nodes + # If all cells in the mesh are active, return nodes and cell_nodes + if self.nC == self.mesh.n_cells: + return nodes, cell_nodes + # Keep only the cell_nodes for active cells + cell_nodes = cell_nodes[self.ind_active] + # Get the unique indices of the nodes that belong to every active cell + # (these indices correspond to the original `nodes` array) + unique_nodes, active_cell_nodes = np.unique(cell_nodes, return_inverse=True) + # Select only the nodes that belong to the active cells (active nodes) + active_nodes = nodes[unique_nodes] + # Reshape indices of active cell nodes for each active cell in the mesh + active_cell_nodes = active_cell_nodes.reshape(cell_nodes.shape) + return active_nodes, active_cell_nodes + + def _get_components_and_receivers(self): + """Generator for receiver locations and their field components.""" + if not hasattr(self.survey, "source_field"): + raise AttributeError( + f"The survey '{self.survey}' has no 'source_field' attribute." + ) + for receiver_object in self.survey.source_field.receiver_list: + yield receiver_object.components, receiver_object.locations + class BaseEquivalentSourceLayerSimulation(BasePFSimulation): """Base equivalent source layer simulation class. diff --git a/simpeg/potential_fields/gravity/_numba_functions.py b/simpeg/potential_fields/gravity/_numba_functions.py index 1d6b363b27..3eb6ae9bf1 100644 --- a/simpeg/potential_fields/gravity/_numba_functions.py +++ b/simpeg/potential_fields/gravity/_numba_functions.py @@ -15,6 +15,8 @@ def jit(*args, **kwargs): else: from numba import jit, prange +from .._numba_utils import kernels_in_nodes_to_cell + def _forward_gravity( receivers, @@ -30,7 +32,7 @@ def _forward_gravity( This function should be used with a `numba.jit` decorator, for example: - ..code:: + .. code:: from numba import jit @@ -85,16 +87,9 @@ def _forward_gravity( fields[i] += ( constant_factor * densities[k] - * _kernels_in_nodes_to_cell( + * kernels_in_nodes_to_cell( kernels, - cell_nodes[k, 0], - cell_nodes[k, 1], - cell_nodes[k, 2], - cell_nodes[k, 3], - cell_nodes[k, 4], - cell_nodes[k, 5], - cell_nodes[k, 6], - cell_nodes[k, 7], + cell_nodes[k, :], ) ) @@ -112,7 +107,7 @@ def _sensitivity_gravity( This function should be used with a `numba.jit` decorator, for example: - ..code:: + .. code:: from numba import jit @@ -162,16 +157,9 @@ def _sensitivity_gravity( ) # Compute sensitivity matrix elements from the kernel values for k in range(n_cells): - sensitivity_matrix[i, k] = constant_factor * _kernels_in_nodes_to_cell( + sensitivity_matrix[i, k] = constant_factor * kernels_in_nodes_to_cell( kernels, - cell_nodes[k, 0], - cell_nodes[k, 1], - cell_nodes[k, 2], - cell_nodes[k, 3], - cell_nodes[k, 4], - cell_nodes[k, 5], - cell_nodes[k, 6], - cell_nodes[k, 7], + cell_nodes[k, :], ) @@ -204,46 +192,6 @@ def _evaluate_kernel( return kernel_func(dx, dy, dz, distance) -@jit(nopython=True) -def _kernels_in_nodes_to_cell( - kernels, - nodes_indices_0, - nodes_indices_1, - nodes_indices_2, - nodes_indices_3, - nodes_indices_4, - nodes_indices_5, - nodes_indices_6, - nodes_indices_7, -): - """ - Evaluate integral on a given cell from evaluation of kernels on nodes - - Parameters - ---------- - kernels : (n_active_nodes,) numpy.ndarray - Array with kernel values on each one of the nodes in the mesh. - nodes_indices : ints - Indices of the nodes for the current cell in "F" order (x changes - faster than y, and y faster than z). - - Returns - ------- - float - """ - result = ( - -kernels[nodes_indices_0] - + kernels[nodes_indices_1] - + kernels[nodes_indices_2] - - kernels[nodes_indices_3] - + kernels[nodes_indices_4] - - kernels[nodes_indices_5] - - kernels[nodes_indices_6] - + kernels[nodes_indices_7] - ) - return result - - # Define decorated versions of these functions _sensitivity_gravity_parallel = jit(nopython=True, parallel=True)(_sensitivity_gravity) _sensitivity_gravity_serial = jit(nopython=True, parallel=False)(_sensitivity_gravity) diff --git a/simpeg/potential_fields/gravity/simulation.py b/simpeg/potential_fields/gravity/simulation.py index 311b19dfc5..1596b15ec2 100644 --- a/simpeg/potential_fields/gravity/simulation.py +++ b/simpeg/potential_fields/gravity/simulation.py @@ -1,7 +1,5 @@ -import os import warnings import numpy as np -import discretize import scipy.constants as constants from geoana.kernels import prism_fz, prism_fzx, prism_fzy, prism_fzz from scipy.constants import G as NewtG @@ -85,13 +83,13 @@ class Simulation3DIntegral(BasePFSimulation): Gravity survey with information of the receivers. ind_active : (n_cells) numpy.ndarray, optional Array that indicates which cells in ``mesh`` are active cells. - rho : numpy.ndarray (optional) + rho : numpy.ndarray, optional Density array for the active cells in the mesh. - rhoMap : Mapping (optional) + rhoMap : Mapping, optional Model mapping. sensitivity_dtype : numpy.dtype, optional Data type that will be used to build the sensitivity matrix. - store_sensitivities : str + store_sensitivities : {"ram", "disk", "forward_only"} Options for storing sensitivity matrix. There are 3 options - 'ram': sensitivities are stored in the computer's RAM @@ -102,9 +100,8 @@ class Simulation3DIntegral(BasePFSimulation): sensitivity_path : str, optional Path to store the sensitivity matrix if ``store_sensitivities`` is set to ``"disk"``. Default to "./sensitivities". - engine : str, optional - Choose which engine should be used to run the forward model: - ``"geoana"`` or "``choclo``". + engine : {"geoana", "choclo"}, optional + Choose which engine should be used to run the forward model. numba_parallel : bool, optional If True, the simulation will run in parallel. If False, it will run in serial. If ``engine`` is not ``"choclo"`` this argument will be @@ -122,51 +119,13 @@ def __init__( numba_parallel=True, **kwargs, ): - super().__init__(mesh, **kwargs) + super().__init__(mesh, engine=engine, numba_parallel=numba_parallel, **kwargs) self.rho = rho self.rhoMap = rhoMap self._G = None self._gtg_diagonal = None self.modelMap = self.rhoMap - self.numba_parallel = numba_parallel - self.engine = engine - self._sanity_checks_engine(kwargs) - if self.engine == "choclo": - # Check dimensions of the mesh - if self.mesh.dim != 3: - raise ValueError( - f"Invalid mesh with {self.mesh.dim} dimensions. " - "Only 3D meshes are supported when using 'choclo' as engine." - ) - # Define jit functions - if numba_parallel: - self._sensitivity_gravity = _sensitivity_gravity_parallel - self._forward_gravity = _forward_gravity_parallel - else: - self._sensitivity_gravity = _sensitivity_gravity_serial - self._forward_gravity = _forward_gravity_serial - - def _sanity_checks_engine(self, kwargs): - """ - Sanity checks for the engine parameter. - Needs the kwargs passed to the __init__ method to raise some warnings. - Will set n_processes to None if it's present in kwargs. - """ - if self.engine not in ("choclo", "geoana"): - raise ValueError( - f"Invalid engine '{self.engine}'. Choose from 'geoana' or 'choclo'." - ) - if self.engine == "choclo" and choclo is None: - raise ImportError( - "The choclo package couldn't be found." - "Running a gravity simulation with 'engine=\"choclo\"' needs " - "choclo to be installed." - "\nTry installing choclo with:" - "\n pip install choclo" - "\nor:" - "\n conda install choclo" - ) # Warn if n_processes has been passed if self.engine == "choclo" and "n_processes" in kwargs: warnings.warn( @@ -176,15 +135,15 @@ def _sanity_checks_engine(self, kwargs): stacklevel=1, ) self.n_processes = None - # Sanity checks for sensitivity_path when using choclo and storing in disk - if self.engine == "choclo" and self.store_sensitivities == "disk": - if os.path.isdir(self.sensitivity_path): - raise ValueError( - f"The passed sensitivity_path '{self.sensitivity_path}' is " - "a directory. " - "When using 'choclo' as the engine, 'senstivity_path' " - "should be the path to a new or existing file." - ) + + # Define jit functions + if self.engine == "choclo": + if self.numba_parallel: + self._sensitivity_gravity = _sensitivity_gravity_parallel + self._forward_gravity = _forward_gravity_parallel + else: + self._sensitivity_gravity = _sensitivity_gravity_serial + self._forward_gravity = _forward_gravity_serial def fields(self, m): """ @@ -455,45 +414,6 @@ def _sensitivity_matrix(self): index_offset += n_rows return sensitivity_matrix - def _get_active_nodes(self): - """ - Return locations of nodes only for active cells - - Also return an array containing the indices of the "active nodes" for - each active cell in the mesh - """ - # Get all nodes in the mesh - if isinstance(self.mesh, discretize.TreeMesh): - nodes = self.mesh.total_nodes - elif isinstance(self.mesh, discretize.TensorMesh): - nodes = self.mesh.nodes - else: - raise TypeError(f"Invalid mesh of type {self.mesh.__class__.__name__}.") - # Get original cell_nodes but only for active cells - cell_nodes = self.mesh.cell_nodes - # If all cells in the mesh are active, return nodes and cell_nodes - if self.nC == self.mesh.n_cells: - return nodes, cell_nodes - # Keep only the cell_nodes for active cells - cell_nodes = cell_nodes[self.ind_active] - # Get the unique indices of the nodes that belong to every active cell - # (these indices correspond to the original `nodes` array) - unique_nodes, active_cell_nodes = np.unique(cell_nodes, return_inverse=True) - # Select only the nodes that belong to the active cells (active nodes) - active_nodes = nodes[unique_nodes] - # Reshape indices of active cell nodes for each active cell in the mesh - active_cell_nodes = active_cell_nodes.reshape(cell_nodes.shape) - return active_nodes, active_cell_nodes - - def _get_components_and_receivers(self): - """Generator for receiver locations and their field components.""" - if not hasattr(self.survey, "source_field"): - raise AttributeError( - f"The survey '{self.survey}' has no 'source_field' attribute." - ) - for receiver_object in self.survey.source_field.receiver_list: - yield receiver_object.components, receiver_object.locations - class SimulationEquivalentSourceLayer( BaseEquivalentSourceLayerSimulation, Simulation3DIntegral diff --git a/simpeg/potential_fields/magnetics/_numba_functions.py b/simpeg/potential_fields/magnetics/_numba_functions.py new file mode 100644 index 0000000000..92a5d2eacd --- /dev/null +++ b/simpeg/potential_fields/magnetics/_numba_functions.py @@ -0,0 +1,659 @@ +""" +Numba functions for magnetic simulation of rectangular prisms +""" + +import numpy as np + +try: + import choclo +except ImportError: + # Define dummy jit decorator + def jit(*args, **kwargs): + return lambda f: f + + choclo = None +else: + from numba import jit, prange + +from .._numba_utils import kernels_in_nodes_to_cell + + +def _sensitivity_mag( + receivers, + nodes, + sensitivity_matrix, + cell_nodes, + regional_field, + kernel_x, + kernel_y, + kernel_z, + constant_factor, + scalar_model, +): + r""" + Fill the sensitivity matrix for single mag component + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_sensitivity_mag = jit(nopython=True, parallel=True)(_sensitivity_mag) + + Parameters + ---------- + receivers : (n_receivers, 3) array + Array with the locations of the receivers + nodes : (n_active_nodes, 3) array + Array with the location of the mesh nodes. + sensitivity_matrix : (n_receivers, n_active_nodes) array + Empty 2d array where the sensitivity matrix elements will be filled. + This could be a preallocated empty array or a slice of it. + cell_nodes : (n_active_cells, 8) array + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_x, kernel_y, kernel_z : callable + Kernels used to compute the desired magnetic component. For example, + for computing bx we need to use ``kernel_x=kernel_ee``, + ``kernel_y=kernel_en``, ``kernel_z=kernel_eu``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the sensitivity matrix is built to work with scalar models + (susceptibilities). + If False, the sensitivity matrix is built to work with vector models + (effective susceptibilities). + + Notes + ----- + + About the kernel functions + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + + For computing the ``bx`` component of the magnetic field we need to use the + following kernels: + + .. code:: + + kernel_x=kernel_ee, kernel_y=kernel_en, kernel_z=kernel_eu + + + For computing the ``by`` component of the magnetic field we need to use the + following kernels: + + .. code:: + + kernel_x=kernel_en, kernel_y=kernel_nn, kernel_z=kernel_nu + + For computing the ``bz`` component of the magnetic field we need to use the + following kernels: + + .. code:: + + kernel_x=kernel_eu, kernel_y=kernel_nu, kernel_z=kernel_uu + + About the model array + ^^^^^^^^^^^^^^^^^^^^^ + + The ``model`` must always be a 1d array: + + * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with + the same number of elements as active cells in the mesh. It should store + the magnetic susceptibilities of each active cell in SI units. + * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array + with a number of elements equal to three times the active cells in the + mesh. It should store the components of the magnetization vector of each + active cell in :math:`Am^{-1}`. The order in which the components should + be passed are: + * every _easting_ component of each active cell, + * then every _northing_ component of each active cell, + * and finally every _upward_ component of each active cell. + + About the sensitivity matrix + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + Each row of the sensitivity matrix corresponds to a single receiver + location. + + If ``scalar_model`` is True, then each element of the row will + correspond to the partial derivative of the selected magnetic component + with respect to the susceptibility of each cell in the mesh. + + If ``scalar_model`` is False, then each row can be split in three sections + containing: + + * the partial derivatives of the selected magnetic component with respect + to the _x_ component of the effective susceptibility of each cell; then + * the partial derivatives of the selected magnetic component with respect + to the _y_ component of the effective susceptibility of each cell; and then + * the partial derivatives of the selected magnetic component with respect + to the _z_ component of the effective susceptibility of each cell. + + So, if we call :math:`B_j` the magnetic field component on the receiver + :math:`j`, and :math:`\bar{\chi}^{(i)} = (\chi_x^{(i)}, \chi_y^{(i)}, + \chi_z^{(i)})` the effective susceptibility of the active cell :math:`i`, + then each row of the sensitivity matrix will be: + + .. math:: + + \left[ + \frac{\partial B_j}{\partial \chi_x^{(1)}}, + \dots, + \frac{\partial B_j}{\partial \chi_x^{(N)}}, + \frac{\partial B_j}{\partial \chi_y^{(1)}}, + \dots, + \frac{\partial B_j}{\partial \chi_y^{(N)}}, + \frac{\partial B_j}{\partial \chi_z^{(1)}}, + \dots, + \frac{\partial B_j}{\partial \chi_z^{(N)}} + \right] + + where :math:`N` is the total number of active cells. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kx, ky, kz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kx[j] = kernel_x(dx, dy, dz, distance) + ky[j] = kernel_y(dx, dy, dz, distance) + kz[j] = kernel_z(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + ux = kernels_in_nodes_to_cell(kx, nodes_indices) + uy = kernels_in_nodes_to_cell(ky, nodes_indices) + uz = kernels_in_nodes_to_cell(kz, nodes_indices) + if scalar_model: + sensitivity_matrix[i, k] = ( + constant_factor + * regional_field_amplitude + * (ux * fx + uy * fy + uz * fz) + ) + else: + sensitivity_matrix[i, k] = ( + constant_factor * regional_field_amplitude * ux + ) + sensitivity_matrix[i, k + n_cells] = ( + constant_factor * regional_field_amplitude * uy + ) + sensitivity_matrix[i, k + 2 * n_cells] = ( + constant_factor * regional_field_amplitude * uz + ) + + +def _sensitivity_tmi( + receivers, + nodes, + sensitivity_matrix, + cell_nodes, + regional_field, + constant_factor, + scalar_model, +): + r""" + Fill the sensitivity matrix for TMI + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_sensitivity_tmi = jit(nopython=True, parallel=True)(_sensitivity_tmi) + + Parameters + ---------- + receivers : (n_receivers, 3) array + Array with the locations of the receivers + nodes : (n_active_nodes, 3) array + Array with the location of the mesh nodes. + sensitivity_matrix : array + Empty 2d array where the sensitivity matrix elements will be filled. + This could be a preallocated empty array or a slice of it. + The array should have a shape of ``(n_receivers, n_active_nodes)`` + if ``scalar_model`` is True. + The array should have a shape of ``(n_receivers, 3 * n_active_nodes)`` + if ``scalar_model`` is False. + cell_nodes : (n_active_cells, 8) array + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the sensitivity matrix is build to work with scalar models + (susceptibilities). + If False, the sensitivity matrix is build to work with vector models + (effective susceptibilities). + + Notes + ----- + + About the model array + ^^^^^^^^^^^^^^^^^^^^^ + + The ``model`` must always be a 1d array: + + * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with + the same number of elements as active cells in the mesh. It should store + the magnetic susceptibilities of each active cell in SI units. + * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array + with a number of elements equal to three times the active cells in the + mesh. It should store the components of the magnetization vector of each + active cell in :math:`Am^{-1}`. The order in which the components should + be passed are: + * every _easting_ component of each active cell, + * then every _northing_ component of each active cell, + * and finally every _upward_ component of each active cell. + + About the sensitivity matrix + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + Each row of the sensitivity matrix corresponds to a single receiver + location. + + If ``scalar_model`` is True, then each element of the row will + correspond to the partial derivative of the tmi + with respect to the susceptibility of each cell in the mesh. + + If ``scalar_model`` is False, then each row can be split in three sections + containing: + + * the partial derivatives of the tmi with respect + to the _x_ component of the effective susceptibility of each cell; then + * the partial derivatives of the tmi with respect + to the _y_ component of the effective susceptibility of each cell; and then + * the partial derivatives of the tmi with respect + to the _z_ component of the effective susceptibility of each cell. + + So, if we call :math:`T_j` the tmi on the receiver + :math:`j`, and :math:`\bar{\chi}^{(i)} = (\chi_x^{(i)}, \chi_y^{(i)}, + \chi_z^{(i)})` the effective susceptibility of the active cell :math:`i`, + then each row of the sensitivity matrix will be: + + .. math:: + + \left[ + \frac{\partial T_j}{\partial \chi_x^{(1)}}, + \dots, + \frac{\partial T_j}{\partial \chi_x^{(N)}}, + \frac{\partial T_j}{\partial \chi_y^{(1)}}, + \dots, + \frac{\partial T_j}{\partial \chi_y^{(N)}}, + \frac{\partial T_j}{\partial \chi_z^{(1)}}, + \dots, + \frac{\partial T_j}{\partial \chi_z^{(N)}} + \right] + + where :math:`N` is the total number of active cells. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kxx, kyy, kzz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + kxy, kxz, kyz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kxx[j] = choclo.prism.kernel_ee(dx, dy, dz, distance) + kyy[j] = choclo.prism.kernel_nn(dx, dy, dz, distance) + kzz[j] = choclo.prism.kernel_uu(dx, dy, dz, distance) + kxy[j] = choclo.prism.kernel_en(dx, dy, dz, distance) + kxz[j] = choclo.prism.kernel_eu(dx, dy, dz, distance) + kyz[j] = choclo.prism.kernel_nu(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + uxx = kernels_in_nodes_to_cell(kxx, nodes_indices) + uyy = kernels_in_nodes_to_cell(kyy, nodes_indices) + uzz = kernels_in_nodes_to_cell(kzz, nodes_indices) + uxy = kernels_in_nodes_to_cell(kxy, nodes_indices) + uxz = kernels_in_nodes_to_cell(kxz, nodes_indices) + uyz = kernels_in_nodes_to_cell(kyz, nodes_indices) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + # Fill the sensitivity matrix element(s) that correspond to the + # current active cell + if scalar_model: + sensitivity_matrix[i, k] = ( + constant_factor + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + else: + sensitivity_matrix[i, k] = ( + constant_factor * regional_field_amplitude * bx + ) + sensitivity_matrix[i, k + n_cells] = ( + constant_factor * regional_field_amplitude * by + ) + sensitivity_matrix[i, k + 2 * n_cells] = ( + constant_factor * regional_field_amplitude * bz + ) + + +def _forward_mag( + receivers, + nodes, + model, + fields, + cell_nodes, + regional_field, + kernel_x, + kernel_y, + kernel_z, + constant_factor, + scalar_model, +): + """ + Forward model single magnetic component + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_forward = jit(nopython=True, parallel=True)(_forward_mag) + + Parameters + ---------- + receivers : (n_receivers, 3) array + Array with the locations of the receivers + nodes : (n_active_nodes, 3) array + Array with the location of the mesh nodes. + model : (n_active_cells) or (3 * n_active_cells) array + Array containing the susceptibilities (scalar) or effective + susceptibilities (vector) of the active cells in the mesh, in SI + units. + Susceptibilities are expected if ``scalar_model`` is True, + and the array should have ``n_active_cells`` elements. + Effective susceptibilities are expected if ``scalar_model`` is False, + and the array should have ``3 * n_active_cells`` elements. + fields : (n_receivers) array + Array full of zeros where the magnetic component on each receiver will + be stored. This could be a preallocated array or a slice of it. + cell_nodes : (n_active_cells, 8) array + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_x, kernel_y, kernel_z : callable + Kernels used to compute the desired magnetic component. For example, + for computing bx we need to use ``kernel_x=kernel_ee``, + ``kernel_y=kernel_en``, ``kernel_z=kernel_eu``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the forward will be computing assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the forward will be computing assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + + Notes + ----- + + About the kernel functions + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + + For computing the ``bx`` component of the magnetic field we need to use the + following kernels: + + .. code:: + + kernel_x=kernel_ee, kernel_y=kernel_en, kernel_z=kernel_eu + + + For computing the ``by`` component of the magnetic field we need to use the + following kernels: + + .. code:: + + kernel_x=kernel_en, kernel_y=kernel_nn, kernel_z=kernel_nu + + For computing the ``bz`` component of the magnetic field we need to use the + following kernels: + + .. code:: + + kernel_x=kernel_eu, kernel_y=kernel_nu, kernel_z=kernel_uu + + + About the model array + ^^^^^^^^^^^^^^^^^^^^^ + + The ``model`` must always be a 1d array: + + * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with + the same number of elements as active cells in the mesh. It should store + the magnetic susceptibilities of each active cell in SI units. + * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array + with a number of elements equal to three times the active cells in the + mesh. It should store the components of the magnetization vector of each + active cell in :math:`Am^{-1}`. The order in which the components should + be passed are: + * every _easting_ component of each active cell, + * then every _northing_ component of each active cell, + * and finally every _upward_ component of each active cell. + + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kx, ky, kz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kx[j] = kernel_x(dx, dy, dz, distance) + ky[j] = kernel_y(dx, dy, dz, distance) + kz[j] = kernel_z(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + ux = kernels_in_nodes_to_cell(kx, nodes_indices) + uy = kernels_in_nodes_to_cell(ky, nodes_indices) + uz = kernels_in_nodes_to_cell(kz, nodes_indices) + if scalar_model: + fields[i] += ( + constant_factor + * model[k] + * regional_field_amplitude + * (ux * fx + uy * fy + uz * fz) + ) + else: + fields[i] += ( + constant_factor + * regional_field_amplitude + * ( + ux * model[k] + + uy * model[k + n_cells] + + uz * model[k + 2 * n_cells] + ) + ) + + +def _forward_tmi( + receivers, + nodes, + model, + fields, + cell_nodes, + regional_field, + constant_factor, + scalar_model, +): + """ + Forward model the TMI + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_forward = jit(nopython=True, parallel=True)(_forward_tmi) + + Parameters + ---------- + receivers : (n_receivers, 3) array + Array with the locations of the receivers + nodes : (n_active_nodes, 3) array + Array with the location of the mesh nodes. + model : (n_active_cells) or (3 * n_active_cells) + Array with the susceptibility (scalar model) or the effective + susceptibility (vector model) of each active cell in the mesh. + If the model is scalar, the ``model`` array should have + ``n_active_cells`` elements and ``scalar_model`` should be True. + If the model is vector, the ``model`` array should have + ``3 * n_active_cells`` elements and ``scalar_model`` should be False. + fields : (n_receivers) array + Array full of zeros where the TMI on each receiver will be stored. This + could be a preallocated array or a slice of it. + cell_nodes : (n_active_cells, 8) array + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the sensitivity matrix is build to work with scalar models + (susceptibilities). + If False, the sensitivity matrix is build to work with vector models + (effective susceptibilities). + + Notes + ----- + + The ``model`` must always be a 1d array: + + * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with + the same number of elements as active cells in the mesh. It should store + the magnetic susceptibilities of each active cell in SI units. + * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array + with a number of elements equal to three times the active cells in the + mesh. It should store the components of the magnetization vector of each + active cell in :math:`Am^{-1}`. The order in which the components should + be passed are: + * every _easting_ component of each active cell, + * then every _northing_ component of each active cell, + * and finally every _upward_ component of each active cell. + + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kxx, kyy, kzz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + kxy, kxz, kyz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kxx[j] = choclo.prism.kernel_ee(dx, dy, dz, distance) + kyy[j] = choclo.prism.kernel_nn(dx, dy, dz, distance) + kzz[j] = choclo.prism.kernel_uu(dx, dy, dz, distance) + kxy[j] = choclo.prism.kernel_en(dx, dy, dz, distance) + kxz[j] = choclo.prism.kernel_eu(dx, dy, dz, distance) + kyz[j] = choclo.prism.kernel_nu(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + uxx = kernels_in_nodes_to_cell(kxx, nodes_indices) + uyy = kernels_in_nodes_to_cell(kyy, nodes_indices) + uzz = kernels_in_nodes_to_cell(kzz, nodes_indices) + uxy = kernels_in_nodes_to_cell(kxy, nodes_indices) + uxz = kernels_in_nodes_to_cell(kxz, nodes_indices) + uyz = kernels_in_nodes_to_cell(kyz, nodes_indices) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + if scalar_model: + fields[i] += ( + constant_factor + * model[k] + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + else: + fields[i] += ( + constant_factor + * regional_field_amplitude + * ( + bx * model[k] + + by * model[k + n_cells] + + bz * model[k + 2 * n_cells] + ) + ) + + +_sensitivity_tmi_serial = jit(nopython=True, parallel=False)(_sensitivity_tmi) +_sensitivity_tmi_parallel = jit(nopython=True, parallel=True)(_sensitivity_tmi) +_forward_tmi_serial = jit(nopython=True, parallel=False)(_forward_tmi) +_forward_tmi_parallel = jit(nopython=True, parallel=True)(_forward_tmi) +_forward_mag_serial = jit(nopython=True, parallel=False)(_forward_mag) +_forward_mag_parallel = jit(nopython=True, parallel=True)(_forward_mag) +_sensitivity_mag_serial = jit(nopython=True, parallel=False)(_sensitivity_mag) +_sensitivity_mag_parallel = jit(nopython=True, parallel=True)(_sensitivity_mag) diff --git a/simpeg/potential_fields/magnetics/simulation.py b/simpeg/potential_fields/magnetics/simulation.py index 5d3d27171c..85d6e53109 100644 --- a/simpeg/potential_fields/magnetics/simulation.py +++ b/simpeg/potential_fields/magnetics/simulation.py @@ -1,3 +1,4 @@ +import warnings import numpy as np import scipy.sparse as sp from geoana.kernels import ( @@ -20,11 +21,68 @@ from .analytics import CongruousMagBC from .survey import Survey +from ._numba_functions import ( + choclo, + _sensitivity_tmi_parallel, + _sensitivity_tmi_serial, + _sensitivity_mag_parallel, + _sensitivity_mag_serial, + _forward_tmi_parallel, + _forward_tmi_serial, + _forward_mag_parallel, + _forward_mag_serial, +) + +if choclo is not None: + CHOCLO_SUPPORTED_COMPONENTS = {"tmi", "bx", "by", "bz"} + CHOCLO_KERNELS = { + "bx": (choclo.prism.kernel_ee, choclo.prism.kernel_en, choclo.prism.kernel_eu), + "by": (choclo.prism.kernel_en, choclo.prism.kernel_nn, choclo.prism.kernel_nu), + "bz": (choclo.prism.kernel_eu, choclo.prism.kernel_nu, choclo.prism.kernel_uu), + } + class Simulation3DIntegral(BasePFSimulation): """ - magnetic simulation in integral form. + Magnetic simulation in integral form. + Parameters + ---------- + mesh : discretize.TreeMesh or discretize.TensorMesh + Mesh use to run the magnetic simulation. + survey : simpeg.potential_fields.magnetics.Survey + Magnetic survey with information of the receivers. + ind_active : (n_cells) numpy.ndarray, optional + Array that indicates which cells in ``mesh`` are active cells. + chi : numpy.ndarray, optional + Susceptibility array for the active cells in the mesh. + chiMap : Mapping, optional + Model mapping. + model_type : str, optional + Whether the model are susceptibilities of the cells (``"scalar"``), + or effective susceptibilities (``"vector"``). + is_amplitude_data : bool, optional + If True, the returned fields will be the amplitude of the magnetic + field. If False, the fields will be returned unmodified. + sensitivity_dtype : numpy.dtype, optional + Data type that will be used to build the sensitivity matrix. + store_sensitivities : {"ram", "disk", "forward_only"} + Options for storing sensitivity matrix. There are 3 options + + - 'ram': sensitivities are stored in the computer's RAM + - 'disk': sensitivities are written to a directory + - 'forward_only': you intend only do perform a forward simulation and + sensitivities do not need to be stored + + sensitivity_path : str, optional + Path to store the sensitivity matrix if ``store_sensitivities`` is set + to ``"disk"``. Default to "./sensitivities". + engine : {"geoana", "choclo"}, optional + Choose which engine should be used to run the forward model. + numba_parallel : bool, optional + If True, the simulation will run in parallel. If False, it will + run in serial. If ``engine`` is not ``"choclo"`` this argument will be + ignored. """ chi, chiMap, chiDeriv = props.Invertible("Magnetic Susceptibility (SI)") @@ -36,10 +94,12 @@ def __init__( chiMap=None, model_type="scalar", is_amplitude_data=False, - **kwargs + engine="geoana", + numba_parallel=True, + **kwargs, ): self.model_type = model_type - super().__init__(mesh, **kwargs) + super().__init__(mesh, engine=engine, numba_parallel=numba_parallel, **kwargs) self.chi = chi self.chiMap = chiMap @@ -49,6 +109,28 @@ def __init__( self.is_amplitude_data = is_amplitude_data self.modelMap = self.chiMap + # Warn if n_processes has been passed + if self.engine == "choclo" and "n_processes" in kwargs: + warnings.warn( + "The 'n_processes' will be ignored when selecting 'choclo' as the " + "engine in the magnetic simulation.", + UserWarning, + stacklevel=1, + ) + self.n_processes = None + + if self.engine == "choclo": + if self.numba_parallel: + self._sensitivity_tmi = _sensitivity_tmi_parallel + self._sensitivity_mag = _sensitivity_mag_parallel + self._forward_tmi = _forward_tmi_parallel + self._forward_mag = _forward_mag_parallel + else: + self._sensitivity_tmi = _sensitivity_tmi_serial + self._sensitivity_mag = _sensitivity_mag_serial + self._forward_tmi = _forward_tmi_serial + self._forward_mag = _forward_mag_serial + @property def model_type(self): """Type of magnetization model @@ -103,7 +185,10 @@ def fields(self, model): self.model = model # model = self.chiMap * model if self.store_sensitivities == "forward_only": - fields = mkvc(self.linear_operator()) + if self.engine == "choclo": + fields = self._forward(self.chi) + else: + fields = mkvc(self.linear_operator()) else: fields = np.asarray( self.G @ self.chi.astype(self.sensitivity_dtype, copy=False) @@ -117,7 +202,10 @@ def fields(self, model): @property def G(self): if getattr(self, "_G", None) is None: - self._G = self.linear_operator() + if self.engine == "choclo": + self._G = self._sensitivity_matrix() + else: + self._G = self.linear_operator() return self._G @@ -494,6 +582,151 @@ def deleteTheseOnModelUpdate(self): deletes = deletes + ["_gtg_diagonal", "_ampDeriv"] return deletes + def _forward(self, model): + """ + Forward model the fields of active cells in the mesh on receivers. + + Parameters + ---------- + model : (n_active_cells) or (3 * n_active_cells) array + Array containing the susceptibilities (scalar) or effective + susceptibilities (vector) of the active cells in the mesh, in SI + units. + Susceptibilities are expected if ``model_type`` is ``"scalar"``, + and the array should have ``n_active_cells`` elements. + Effective susceptibilities are expected if ``model_type`` is + ``"vector"``, and the array should have ``3 * n_active_cells`` + elements. + + Returns + ------- + (nD, ) array + Always return a ``np.float64`` array. + """ + # Gather active nodes and the indices of the nodes for each active cell + active_nodes, active_cell_nodes = self._get_active_nodes() + # Get regional field + regional_field = self.survey.source_field.b0 + # Allocate fields array + fields = np.zeros(self.survey.nD, dtype=self.sensitivity_dtype) + # Define the constant factor + constant_factor = 1 / 4 / np.pi + # Start computing the fields + index_offset = 0 + scalar_model = self.model_type == "scalar" + for components, receivers in self._get_components_and_receivers(): + if not CHOCLO_SUPPORTED_COMPONENTS.issuperset(components): + raise NotImplementedError( + f"Other components besides {CHOCLO_SUPPORTED_COMPONENTS} " + "aren't implemented yet." + ) + n_components = len(components) + n_rows = n_components * receivers.shape[0] + for i, component in enumerate(components): + vector_slice = slice( + index_offset + i, index_offset + n_rows, n_components + ) + if component == "tmi": + self._forward_tmi( + receivers, + active_nodes, + model, + fields[vector_slice], + active_cell_nodes, + regional_field, + constant_factor, + scalar_model, + ) + else: + kernel_x, kernel_y, kernel_z = CHOCLO_KERNELS[component] + self._forward_mag( + receivers, + active_nodes, + model, + fields[vector_slice], + active_cell_nodes, + regional_field, + kernel_x, + kernel_y, + kernel_z, + constant_factor, + scalar_model, + ) + index_offset += n_rows + return fields + + def _sensitivity_matrix(self): + """ + Compute the sensitivity matrix G + + Returns + ------- + (nD, n_active_cells) array + """ + # Gather active nodes and the indices of the nodes for each active cell + active_nodes, active_cell_nodes = self._get_active_nodes() + # Get regional field + regional_field = self.survey.source_field.b0 + # Allocate sensitivity matrix + if self.model_type == "scalar": + n_columns = self.nC + else: + n_columns = 3 * self.nC + shape = (self.survey.nD, n_columns) + if self.store_sensitivities == "disk": + sensitivity_matrix = np.memmap( + self.sensitivity_path, + shape=shape, + dtype=self.sensitivity_dtype, + order="C", # it's more efficient to write in row major + mode="w+", + ) + else: + sensitivity_matrix = np.empty(shape, dtype=self.sensitivity_dtype) + # Define the constant factor + constant_factor = 1 / 4 / np.pi + # Start filling the sensitivity matrix + index_offset = 0 + scalar_model = self.model_type == "scalar" + for components, receivers in self._get_components_and_receivers(): + if not CHOCLO_SUPPORTED_COMPONENTS.issuperset(components): + raise NotImplementedError( + f"Other components besides {CHOCLO_SUPPORTED_COMPONENTS} " + "aren't implemented yet." + ) + n_components = len(components) + n_rows = n_components * receivers.shape[0] + for i, component in enumerate(components): + matrix_slice = slice( + index_offset + i, index_offset + n_rows, n_components + ) + if component == "tmi": + self._sensitivity_tmi( + receivers, + active_nodes, + sensitivity_matrix[matrix_slice, :], + active_cell_nodes, + regional_field, + constant_factor, + scalar_model, + ) + else: + kernel_x, kernel_y, kernel_z = CHOCLO_KERNELS[component] + self._sensitivity_mag( + receivers, + active_nodes, + sensitivity_matrix[matrix_slice, :], + active_cell_nodes, + regional_field, + kernel_x, + kernel_y, + kernel_z, + constant_factor, + scalar_model, + ) + index_offset += n_rows + return sensitivity_matrix + class SimulationEquivalentSourceLayer( BaseEquivalentSourceLayerSimulation, Simulation3DIntegral diff --git a/tests/pf/test_base_pf_simulation.py b/tests/pf/test_base_pf_simulation.py new file mode 100644 index 0000000000..9cf7964ee4 --- /dev/null +++ b/tests/pf/test_base_pf_simulation.py @@ -0,0 +1,306 @@ +""" +Test BasePFSimulation class +""" + +import pytest +import numpy as np +from discretize import CylindricalMesh, TensorMesh, TreeMesh + +import simpeg +from simpeg.potential_fields.base import BasePFSimulation +from simpeg.survey import BaseSurvey +from simpeg.potential_fields import gravity, magnetics + + +@pytest.fixture +def mock_simulation_class(): + """ + Mock simulation class as child of BasePFSimulation + """ + + class MockSimulation(BasePFSimulation): + @property + def G(self): + """Define a dummy G property to avoid warnings on tests.""" + pass + + return MockSimulation + + +@pytest.fixture +def tensor_mesh(): + """ + Return sample TensorMesh + """ + h = (3, 3, 3) + return TensorMesh(h) + + +@pytest.fixture +def tree_mesh(): + """ + Return sample TensorMesh + """ + h = (4, 4, 4) + mesh = TreeMesh(h) + mesh.refine_points(points=(0, 0, 0), level=2) + return mesh + + +@pytest.fixture +def mock_survey_class(): + """ + Mock survey class as child of BaseSurvey + """ + + class MockSurvey(BaseSurvey): + pass + + return MockSurvey + + +class TestEngine: + """ + Test the engine property and some of its relations with other attributes + """ + + def test_invalid_engine(self, tensor_mesh, mock_simulation_class): + """ + Test if error is raised after invalid engine + """ + engine = "invalid engine" + msg = rf"'engine' must be in \('geoana', 'choclo'\). Got '{engine}'" + with pytest.raises(ValueError, match=msg): + mock_simulation_class(tensor_mesh, engine=engine) + + def test_invalid_engine_without_choclo( + self, tensor_mesh, mock_simulation_class, monkeypatch + ): + """ + Test error after choosing "choclo" as engine but not being installed + """ + monkeypatch.setattr(simpeg.potential_fields.base, "choclo", None) + engine = "choclo" + msg = "The choclo package couldn't be found." + with pytest.raises(ImportError, match=msg): + mock_simulation_class(tensor_mesh, engine=engine) + + def test_sensitivity_path_as_dir(self, tensor_mesh, mock_simulation_class, tmpdir): + """ + Test error if the sensitivity_path is a dir + + Error should be raised if using ``engine=="choclo"`` and setting + ``store_sensitivities="disk"``. + """ + sensitivity_path = str(tmpdir.mkdir("sensitivities")) + msg = f"The passed sensitivity_path '{sensitivity_path}' is a directory." + with pytest.raises(ValueError, match=msg): + mock_simulation_class( + tensor_mesh, + engine="choclo", + store_sensitivities="disk", + sensitivity_path=sensitivity_path, + ) + + +class TestGetActiveNodes: + """ + Tests _get_active_nodes private method + """ + + def test_invalid_mesh(self, tensor_mesh, mock_simulation_class): + """ + Test error on invalid mesh class + """ + # Initialize base simulation with valid mesh (so we don't trigger + # errors in the constructor) + simulation = mock_simulation_class(tensor_mesh) + # Assign an invalid mesh to the simulation + simulation.mesh = CylindricalMesh(tensor_mesh.h) + msg = "Invalid mesh of type CylindricalMesh." + with pytest.raises(TypeError, match=msg): + simulation._get_active_nodes() + + def test_no_inactive_cells_tensor(self, tensor_mesh, mock_simulation_class): + """ + Test _get_active_nodes when all cells are active on a tensor mesh + """ + simulation = mock_simulation_class(tensor_mesh) + active_nodes, active_cell_nodes = simulation._get_active_nodes() + np.testing.assert_equal(active_nodes, tensor_mesh.nodes) + np.testing.assert_equal(active_cell_nodes, tensor_mesh.cell_nodes) + + def test_no_inactive_cells_tree(self, tree_mesh, mock_simulation_class): + """ + Test _get_active_nodes when all cells are active on a tree mesh + """ + simulation = mock_simulation_class(tree_mesh) + active_nodes, active_cell_nodes = simulation._get_active_nodes() + np.testing.assert_equal(active_nodes, tree_mesh.total_nodes) + np.testing.assert_equal(active_cell_nodes, tree_mesh.cell_nodes) + + def test_inactive_cells_tensor(self, tensor_mesh, mock_simulation_class): + """ + Test _get_active_nodes with some inactive cells on a tensor mesh + """ + # Define active cells: only the first cell is active + active_cells = np.zeros(tensor_mesh.n_cells, dtype=bool) + active_cells[0] = True + # Initialize simulation + simulation = mock_simulation_class(tensor_mesh, ind_active=active_cells) + # Build expected active_nodes and active_cell_nodes + expected_active_nodes = tensor_mesh.nodes[tensor_mesh[0].nodes] + expected_active_cell_nodes = np.atleast_2d(np.arange(8, dtype=int)) + # Test method + active_nodes, active_cell_nodes = simulation._get_active_nodes() + np.testing.assert_equal(active_nodes, expected_active_nodes) + np.testing.assert_equal(active_cell_nodes, expected_active_cell_nodes) + + def test_inactive_cells_tree(self, tree_mesh, mock_simulation_class): + """ + Test _get_active_nodes with some inactive cells on a tensor mesh + """ + # Define active cells: only the first cell is active + active_cells = np.zeros(tree_mesh.n_cells, dtype=bool) + active_cells[0] = True + + # Initialize simulation + simulation = mock_simulation_class(tree_mesh, ind_active=active_cells) + + # Build expected active_nodes (in the right order for a single cell) + expected_active_nodes = [ + [0, 0, 0], + [0.25, 0, 0], + [0, 0.25, 0], + [0.25, 0.25, 0], + [0, 0, 0.25], + [0.25, 0, 0.25], + [0, 0.25, 0.25], + [0.25, 0.25, 0.25], + ] + + # Run method + active_nodes, active_cell_nodes = simulation._get_active_nodes() + + # Check shape of active nodes and check if all of them are there + assert active_nodes.shape == (8, 3) + for node in expected_active_nodes: + assert node in active_nodes + + # Check shape of active_cell_nodes and check if they are in the right + # order + assert active_cell_nodes.shape == (1, 8) + for node, node_index in zip(expected_active_nodes, active_cell_nodes[0]): + np.testing.assert_equal(node, active_nodes[node_index]) + + +class TestGetComponentsAndReceivers: + """ + Test _get_components_and_receivers private method + """ + + @pytest.fixture + def receiver_locations(self): + receiver_locations = np.array([[0, 0, 0], [1, 1, 1]], dtype=np.float64) + return receiver_locations + + @pytest.fixture + def gravity_survey(self, receiver_locations): + receiver_locations = np.array([[0, 0, 0], [1, 1, 1]], dtype=np.float64) + components = ["gxy", "guv"] + receivers = gravity.receivers.Point( + receiver_locations, + components=components, + ) + # Define the SourceField and the Survey + source_field = gravity.sources.SourceField(receiver_list=[receivers]) + return gravity.Survey(source_field) + + @pytest.fixture + def magnetic_survey(self, receiver_locations): + receiver_locations = np.array([[0, 0, 0], [1, 1, 1]], dtype=np.float64) + components = ["tmi", "bx"] + receivers = magnetics.receivers.Point( + receiver_locations, + components=components, + ) + # Define the SourceField and the Survey + source_field = magnetics.sources.UniformBackgroundField( + receiver_list=[receivers], + amplitude=55_000, + inclination=45.0, + declination=12.0, + ) + return magnetics.Survey(source_field) + + def test_missing_source_field( + self, tensor_mesh, mock_survey_class, mock_simulation_class + ): + """ + Test error after missing survey in simulation + """ + survey = mock_survey_class(source_list=None) + simulation = mock_simulation_class(tensor_mesh, survey=survey) + msg = "The survey '(.*)' has no 'source_field' attribute." + with pytest.raises(AttributeError, match=msg): + # need to iterate over the generator to actually test its code + [item for item in simulation._get_components_and_receivers()] + + def test_components_and_receivers_gravity( + self, tensor_mesh, gravity_survey, mock_simulation_class, receiver_locations + ): + """ + Test method on a gravity survey + """ + simulation = mock_simulation_class(tensor_mesh, survey=gravity_survey) + components_and_receivers = tuple( + items for items in simulation._get_components_and_receivers() + ) + # Check we have a single element in the iterator + assert len(components_and_receivers) == 1 + # Check if components and receiver locations are correct + components, receivers = components_and_receivers[0] + assert components == ["gxy", "guv"] + np.testing.assert_equal(receivers, receiver_locations) + + def test_components_and_receivers_magnetics( + self, tensor_mesh, magnetic_survey, mock_simulation_class, receiver_locations + ): + """ + Test method on a magnetic survey + """ + simulation = mock_simulation_class(tensor_mesh, survey=magnetic_survey) + components_and_receivers = tuple( + items for items in simulation._get_components_and_receivers() + ) + # Check we have a single element in the iterator + assert len(components_and_receivers) == 1 + # Check if components and receiver locations are correct + components, receivers = components_and_receivers[0] + assert components == ["tmi", "bx"] + np.testing.assert_equal(receivers, receiver_locations) + + +class TestInvalidMeshChoclo: + @pytest.fixture(params=("tensormesh", "treemesh")) + def mesh(self, request): + """Sample 2D mesh.""" + hx, hy = [(0.1, 8)], [(0.1, 8)] + h = (hx, hy) + if request.param == "tensormesh": + mesh = TensorMesh(h, "CC") + else: + mesh = TreeMesh(h, origin="CC") + mesh.finalize() + return mesh + + def test_invalid_mesh_with_choclo(self, mesh, mock_simulation_class): + """ + Test if simulation raises error when passing an invalid mesh and using choclo + """ + msg = ( + "Invalid mesh with 2 dimensions. " + "Only 3D meshes are supported when using 'choclo' as engine." + ) + with pytest.raises(ValueError, match=msg): + mock_simulation_class(mesh, engine="choclo") diff --git a/tests/pf/test_forward_Grav_Linear.py b/tests/pf/test_forward_Grav_Linear.py index 40aff12b53..32a964710a 100644 --- a/tests/pf/test_forward_Grav_Linear.py +++ b/tests/pf/test_forward_Grav_Linear.py @@ -329,7 +329,8 @@ def test_invalid_sensitivity_dtype_assignment(self, simple_mesh, invalid_dtype): def test_invalid_engine(self, simple_mesh): """Test if error is raised after invalid engine.""" engine = "invalid engine" - with pytest.raises(ValueError, match=f"Invalid engine '{engine}'"): + msg = rf"'engine' must be in \('geoana', 'choclo'\). Got '{engine}'" + with pytest.raises(ValueError, match=msg): gravity.Simulation3DIntegral(simple_mesh, engine=engine) def test_choclo_and_n_proceesses(self, simple_mesh): @@ -405,7 +406,7 @@ def test_choclo_missing(self, simple_mesh, monkeypatch): Check if error is raised when choclo is missing and chosen as engine. """ # Monkeypatch choclo in simpeg.potential_fields.base - monkeypatch.setattr(simpeg.potential_fields.gravity.simulation, "choclo", None) + monkeypatch.setattr(simpeg.potential_fields.base, "choclo", None) # Check if error is raised msg = "The choclo package couldn't be found." with pytest.raises(ImportError, match=msg): diff --git a/tests/pf/test_forward_Mag_Linear.py b/tests/pf/test_forward_Mag_Linear.py index 45ff17ba8c..9cf3498238 100644 --- a/tests/pf/test_forward_Mag_Linear.py +++ b/tests/pf/test_forward_Mag_Linear.py @@ -1,120 +1,667 @@ -import pytest -import unittest +from __future__ import annotations import discretize import numpy as np +import pytest from geoana.em.static import MagneticPrism from scipy.constants import mu_0 +import simpeg from simpeg import maps, utils from simpeg.potential_fields import magnetics as mag -def test_ana_mag_forward(): - nx = 5 - ny = 5 - - h0_amplitude, h0_inclination, h0_declination = (50000.0, 60.0, 250.0) - b0 = mag.analytics.IDTtoxyz(-h0_inclination, h0_declination, h0_amplitude) - chi1 = 0.01 - chi2 = 0.02 - - # Define a mesh - cs = 0.2 - hxind = [(cs, 41)] - hyind = [(cs, 41)] - hzind = [(cs, 41)] - mesh = discretize.TensorMesh([hxind, hyind, hzind], "CCC") +def get_block_inds(grid: np.ndarray, block: np.ndarray) -> np.ndarray: + """ + Get the indices for a block + + Parameters + ---------- + grid : np.ndarray + (n, 3) array of xyz locations + block : np.ndarray + (3, 2) array of (xmin, xmax), (ymin, ymax), (zmin, zmax) dimensions of + the block. + + Returns + ------- + np.ndarray + boolean array of indices corresponding to the block + """ + + return np.where( + (grid[:, 0] > block[0, 0]) + & (grid[:, 0] < block[0, 1]) + & (grid[:, 1] > block[1, 0]) + & (grid[:, 1] < block[1, 1]) + & (grid[:, 2] > block[2, 0]) + & (grid[:, 2] < block[2, 1]) + ) - # create a model of two blocks, 1 inside the other - block1 = np.array([[-1.5, 1.5], [-1.5, 1.5], [-1.5, 1.5]]) - block2 = np.array([[-0.7, 0.7], [-0.7, 0.7], [-0.7, 0.7]]) - def get_block_inds(grid, block): - return np.where( - (grid[:, 0] > block[0, 0]) - & (grid[:, 0] < block[0, 1]) - & (grid[:, 1] > block[1, 0]) - & (grid[:, 1] < block[1, 1]) - & (grid[:, 2] > block[2, 0]) - & (grid[:, 2] < block[2, 1]) +def create_block_model( + mesh: discretize.TensorMesh, + blocks: tuple[np.ndarray, ...], + block_params: tuple[float, ...] | tuple[np.ndarray, ...], +) -> tuple[np.ndarray, np.ndarray]: + """ + Create a magnetic model from a sequence of blocks + + Parameters + ---------- + mesh : discretize.TensorMesh + TensorMesh object to put the model on + blocks : Tuple[np.ndarray, ...] + Tuple of block definitions (each element is (3, 2) array of + (xmin, xmax), (ymin, ymax), (zmin, zmax) dimensions of the block) + block_params : Tuple[float, ...] + Tuple of parameters to assign for each block. Must be the same length + as ``blocks``. + + Returns + ------- + Tuple[np.ndarray, np.ndarray] + Tuple of the magnetic model and active_cells (a boolean array) + + Raises + ------ + ValueError + if ``blocks`` and ``block_params`` have incompatible dimensions + """ + if len(blocks) != len(block_params): + raise ValueError( + "'blocks' and 'block_params' must have the same number of elements" + ) + model = np.zeros((mesh.n_cells, np.atleast_1d(block_params[0]).shape[0])) + for block, params in zip(blocks, block_params): + block_ind = get_block_inds(mesh.cell_centers, block) + model[block_ind] = params + active_cells = np.any(np.abs(model) > 0, axis=1) + return model.squeeze(), active_cells + + +def create_mag_survey( + components: list[str], + receiver_locations: np.ndarray, + inducing_field_params: tuple[float, float, float], +) -> mag.Survey: + """ + create a magnetic Survey + + Parameters + ---------- + components : List[str] + List of components to model + receiver_locations : np.ndarray + (n, 3) array of xyz receiver locations + inducing_field_params : Tuple[float, float, float] + amplitude, inclination, and declination of the inducing field + + Returns + ------- + mag.Survey + a magnetic Survey instance + """ + + receivers = mag.Point(receiver_locations, components=components) + strenght, inclination, declination = inducing_field_params + source_field = mag.UniformBackgroundField( + receiver_list=[receivers], + amplitude=strenght, + inclination=inclination, + declination=declination, + ) + return mag.Survey(source_field) + + +class TestsMagSimulation: + """ + Test mag simulation against the analytic solutions single prisms + """ + + @pytest.fixture + def mag_mesh(self) -> discretize.TensorMesh: + """ + a small tensor mesh for testing magnetic simulations + + Returns + ------- + discretize.TensorMesh + the tensor mesh for testing + """ + # Define a mesh + cs = 0.2 + hxind = [(cs, 41)] + hyind = [(cs, 41)] + hzind = [(cs, 41)] + mesh = discretize.TensorMesh([hxind, hyind, hzind], "CCC") + return mesh + + @pytest.fixture + def two_blocks(self) -> tuple[np.ndarray, np.ndarray]: + """ + The parameters defining two blocks + + Returns + ------- + Tuple[np.ndarray, np.ndarray] + Tuple of (3, 2) arrays of (xmin, xmax), (ymin, ymax), (zmin, zmax) + dimensions of each block. + """ + block1 = np.array([[-1.5, 1.5], [-1.5, 1.5], [-1.5, 1.5]]) + block2 = np.array([[-0.7, 0.7], [-0.7, 0.7], [-0.7, 0.7]]) + return block1, block2 + + @pytest.fixture + def receiver_locations(self) -> np.ndarray: + """ + a grid of receivers for testing + + Returns + ------- + np.ndarray + (n, 3) array of receiver locations + """ + # Create plane of observations + nx, ny = 5, 5 + xr = np.linspace(-20, 20, nx) + yr = np.linspace(-20, 20, ny) + X, Y = np.meshgrid(xr, yr) + Z = np.ones_like(X) * 3.0 + return np.c_[X.reshape(-1), Y.reshape(-1), Z.reshape(-1)] + + @pytest.fixture + def inducing_field( + self, + ) -> tuple[tuple[float, float, float], tuple[float, float, float]]: + """ + inducing field + + Return inducing field as amplitude and angles and as vector components. + + Returns + ------- + tuple[tuple[float, float, float], tuple[float, float, float]] + (amplitude, inclination, declination), (b_x, b_y, b_z) + """ + h0_amplitude, h0_inclination, h0_declination = (50000.0, 60.0, 250.0) + b0 = mag.analytics.IDTtoxyz(-h0_inclination, h0_declination, h0_amplitude) + return (h0_amplitude, h0_inclination, h0_declination), b0 + + @pytest.mark.parametrize( + "engine,parallel_kwargs", + [ + ("geoana", {"n_processes": None}), + ("geoana", {"n_processes": 1}), + ("choclo", {"numba_parallel": False}), + ("choclo", {"numba_parallel": True}), + ], + ids=["geoana_serial", "geoana_parallel", "choclo_serial", "choclo_parallel"], + ) + @pytest.mark.parametrize("store_sensitivities", ("ram", "disk", "forward_only")) + def test_magnetic_field_and_tmi_w_susceptibility( + self, + engine, + parallel_kwargs, + store_sensitivities, + tmp_path, + mag_mesh, + two_blocks, + receiver_locations, + inducing_field, + ): + """ + Test forwarding the magnetic field and tmi (with susceptibility as model) + """ + inducing_field_params, b0 = inducing_field + + chi1 = 0.01 + chi2 = 0.02 + model, active_cells = create_block_model(mag_mesh, two_blocks, (chi1, chi2)) + model_reduced = model[active_cells] + # Create reduced identity map for Linear Problem + identity_map = maps.IdentityMap(nP=int(sum(active_cells))) + + survey = create_mag_survey( + components=["bx", "by", "bz", "tmi"], + receiver_locations=receiver_locations, + inducing_field_params=inducing_field_params, ) - block1_inds = get_block_inds(mesh.cell_centers, block1) - block2_inds = get_block_inds(mesh.cell_centers, block2) - - model = np.zeros(mesh.n_cells) - model[block1_inds] = chi1 - model[block2_inds] = chi2 + sim = mag.Simulation3DIntegral( + mag_mesh, + survey=survey, + chiMap=identity_map, + ind_active=active_cells, + sensitivity_path=str(tmp_path / f"{engine}"), + store_sensitivities=store_sensitivities, + engine=engine, + **parallel_kwargs, + ) - active_cells = model != 0.0 - model_reduced = model[active_cells] + data = sim.dpred(model_reduced) + d_x = data[0::4] + d_y = data[1::4] + d_z = data[2::4] + d_t = data[3::4] + + # Compute analytical response from magnetic prism + block1, block2 = two_blocks + prism_1 = MagneticPrism(block1[:, 0], block1[:, 1], chi1 * b0 / mu_0) + prism_2 = MagneticPrism(block2[:, 0], block2[:, 1], -chi1 * b0 / mu_0) + prism_3 = MagneticPrism(block2[:, 0], block2[:, 1], chi2 * b0 / mu_0) + + d = ( + prism_1.magnetic_flux_density(receiver_locations) + + prism_2.magnetic_flux_density(receiver_locations) + + prism_3.magnetic_flux_density(receiver_locations) + ) - # Create reduced identity map for Linear Problem - idenMap = maps.IdentityMap(nP=int(sum(active_cells))) + # TMI projection + tmi = sim.tmi_projection + d_t2 = d_x * tmi[0] + d_y * tmi[1] + d_z * tmi[2] + + # Check results + rtol, atol = 1e-7, 1e-6 + np.testing.assert_allclose( + d_t, d_t2, rtol=rtol, atol=atol + ) # double check internal projection + np.testing.assert_allclose(d_x, d[:, 0], rtol=rtol, atol=atol) + np.testing.assert_allclose(d_y, d[:, 1], rtol=rtol, atol=atol) + np.testing.assert_allclose(d_z, d[:, 2], rtol=rtol, atol=atol) + np.testing.assert_allclose(d_t, d @ tmi, rtol=rtol, atol=atol) + + @pytest.mark.parametrize( + "engine, parallel_kwargs", + [ + ("geoana", {"n_processes": None}), + ("geoana", {"n_processes": 1}), + ("choclo", {"numba_parallel": False}), + ("choclo", {"numba_parallel": True}), + ], + ids=["geoana_serial", "geoana_parallel", "choclo_serial", "choclo_parallel"], + ) + @pytest.mark.parametrize("store_sensitivities", ("ram", "disk", "forward_only")) + def test_magnetic_gradiometry_w_susceptibility( + self, + engine, + parallel_kwargs, + store_sensitivities, + tmp_path, + mag_mesh, + two_blocks, + receiver_locations, + inducing_field, + ): + """ + Test magnetic gradiometry components (with susceptibility as model) + """ + inducing_field_params, b0 = inducing_field + chi1 = 0.01 + chi2 = 0.02 + model, active_cells = create_block_model(mag_mesh, two_blocks, (chi1, chi2)) + model_reduced = model[active_cells] + # Create reduced identity map for Linear Problem + identity_map = maps.IdentityMap(nP=int(sum(active_cells))) + + survey = create_mag_survey( + components=["bxx", "bxy", "bxz", "byy", "byz", "bzz"], + receiver_locations=receiver_locations, + inducing_field_params=inducing_field_params, + ) + sim = mag.Simulation3DIntegral( + mag_mesh, + survey=survey, + chiMap=identity_map, + ind_active=active_cells, + sensitivity_path=str(tmp_path / f"{engine}"), + store_sensitivities=store_sensitivities, + engine=engine, + **parallel_kwargs, + ) + if engine == "choclo": + # gradient simulation not implemented for choclo yet + with pytest.raises(NotImplementedError): + data = sim.dpred(model_reduced) + else: + data = sim.dpred(model_reduced) + d_xx = data[0::6] + d_xy = data[1::6] + d_xz = data[2::6] + d_yy = data[3::6] + d_yz = data[4::6] + d_zz = data[5::6] + + # Compute analytical response from magnetic prism + block1, block2 = two_blocks + prism_1 = MagneticPrism(block1[:, 0], block1[:, 1], chi1 * b0 / mu_0) + prism_2 = MagneticPrism(block2[:, 0], block2[:, 1], -chi1 * b0 / mu_0) + prism_3 = MagneticPrism(block2[:, 0], block2[:, 1], chi2 * b0 / mu_0) + + d = ( + prism_1.magnetic_field_gradient(receiver_locations) + + prism_2.magnetic_field_gradient(receiver_locations) + + prism_3.magnetic_field_gradient(receiver_locations) + ) * mu_0 + + # Check results + rtol, atol = 5e-7, 1e-6 + np.testing.assert_allclose(d_xx, d[..., 0, 0], rtol=rtol, atol=atol) + np.testing.assert_allclose(d_xy, d[..., 0, 1], rtol=rtol, atol=atol) + np.testing.assert_allclose(d_xz, d[..., 0, 2], rtol=rtol, atol=atol) + np.testing.assert_allclose(d_yy, d[..., 1, 1], rtol=rtol, atol=atol) + np.testing.assert_allclose(d_yz, d[..., 1, 2], rtol=rtol, atol=atol) + np.testing.assert_allclose(d_zz, d[..., 2, 2], rtol=rtol, atol=atol) + + @pytest.mark.parametrize( + "engine, parallel_kwargs", + [ + ("geoana", {"n_processes": None}), + ("geoana", {"n_processes": 1}), + ("choclo", {"numba_parallel": False}), + ("choclo", {"numba_parallel": True}), + ], + ids=["geoana_serial", "geoana_parallel", "choclo_serial", "choclo_parallel"], + ) + @pytest.mark.parametrize("store_sensitivities", ("ram", "disk", "forward_only")) + def test_magnetic_vector_and_tmi_w_magnetization( + self, + engine, + parallel_kwargs, + store_sensitivities, + tmp_path, + mag_mesh, + two_blocks, + receiver_locations, + inducing_field, + ): + """ + Test magnetic vector and TMI (using magnetization vectors as model) + """ + inducing_field_params, b0 = inducing_field + M1 = (utils.mat_utils.dip_azimuth2cartesian(45, -40) * 0.05).squeeze() + M2 = (utils.mat_utils.dip_azimuth2cartesian(120, 32) * 0.1).squeeze() + + model, active_cells = create_block_model(mag_mesh, two_blocks, (M1, M2)) + model_reduced = model[active_cells].reshape(-1, order="F") + # Create reduced identity map for Linear Problem + identity_map = maps.IdentityMap(nP=int(sum(active_cells)) * 3) + + survey = create_mag_survey( + components=["bx", "by", "bz", "tmi"], + receiver_locations=receiver_locations, + inducing_field_params=inducing_field_params, + ) - # Create plane of observations - xr = np.linspace(-20, 20, nx) - yr = np.linspace(-20, 20, ny) - X, Y = np.meshgrid(xr, yr) - Z = np.ones_like(X) * 3.0 - locXyz = np.c_[X.reshape(-1), Y.reshape(-1), Z.reshape(-1)] - components = ["bx", "by", "bz", "tmi"] + sim = mag.Simulation3DIntegral( + mag_mesh, + survey=survey, + chiMap=identity_map, + ind_active=active_cells, + sensitivity_path=str(tmp_path / f"{engine}"), + store_sensitivities=store_sensitivities, + model_type="vector", + engine=engine, + **parallel_kwargs, + ) - rxLoc = mag.Point(locXyz, components=components) - srcField = mag.UniformBackgroundField( - receiver_list=[rxLoc], - amplitude=h0_amplitude, - inclination=h0_inclination, - declination=h0_declination, - ) - survey = mag.Survey(srcField) + data = sim.dpred(model_reduced).reshape(-1, 4) - # Creat reduced identity map for Linear Problem - idenMap = maps.IdentityMap(nP=int(sum(active_cells))) + # Compute analytical response from magnetic prism + block1, block2 = two_blocks + prism_1 = MagneticPrism( + block1[:, 0], block1[:, 1], M1 * np.linalg.norm(b0) / mu_0 + ) + prism_2 = MagneticPrism( + block2[:, 0], block2[:, 1], -M1 * np.linalg.norm(b0) / mu_0 + ) + prism_3 = MagneticPrism( + block2[:, 0], block2[:, 1], M2 * np.linalg.norm(b0) / mu_0 + ) - sim = mag.Simulation3DIntegral( - mesh, - survey=survey, - chiMap=idenMap, - ind_active=active_cells, - store_sensitivities="forward_only", - n_processes=None, + d = ( + prism_1.magnetic_flux_density(receiver_locations) + + prism_2.magnetic_flux_density(receiver_locations) + + prism_3.magnetic_flux_density(receiver_locations) + ) + tmi = sim.tmi_projection + + # Check results + rtol, atol = 5e-7, 1e-6 + np.testing.assert_allclose(data[:, 0], d[:, 0], rtol=rtol, atol=atol) + np.testing.assert_allclose(data[:, 1], d[:, 1], rtol=rtol, atol=atol) + np.testing.assert_allclose(data[:, 2], d[:, 2], rtol=rtol, atol=atol) + np.testing.assert_allclose(data[:, 3], d @ tmi, rtol=rtol, atol=atol) + + @pytest.mark.parametrize( + "engine, parallel_kwargs", + [ + ("geoana", {"n_processes": None}), + ("geoana", {"n_processes": 1}), + ("choclo", {"numba_parallel": False}), + ("choclo", {"numba_parallel": True}), + ], + ids=["geoana_serial", "geoana_parallel", "choclo_serial", "choclo_parallel"], ) + @pytest.mark.parametrize("store_sensitivities", ("ram", "disk", "forward_only")) + def test_magnetic_field_amplitude_w_magnetization( + self, + engine, + parallel_kwargs, + store_sensitivities, + tmp_path, + mag_mesh, + two_blocks, + receiver_locations, + inducing_field, + ): + """ + Test magnetic field amplitude (using magnetization vectors as model) + """ + inducing_field_params, b0 = inducing_field + M1 = (utils.mat_utils.dip_azimuth2cartesian(45, -40) * 0.05).squeeze() + M2 = (utils.mat_utils.dip_azimuth2cartesian(120, 32) * 0.1).squeeze() + + model, active_cells = create_block_model(mag_mesh, two_blocks, (M1, M2)) + model_reduced = model[active_cells].reshape(-1, order="F") + # Create reduced identity map for Linear Problem + identity_map = maps.IdentityMap(nP=int(sum(active_cells)) * 3) + + survey = create_mag_survey( + components=["bx", "by", "bz"], + receiver_locations=receiver_locations, + inducing_field_params=inducing_field_params, + ) - data = sim.dpred(model_reduced) - d_x = data[0::4] - d_y = data[1::4] - d_z = data[2::4] - d_t = data[3::4] - - tmi = sim.tmi_projection - d_t2 = d_x * tmi[0] + d_y * tmi[1] + d_z * tmi[2] - np.testing.assert_allclose(d_t, d_t2) # double check internal projection + sim = mag.Simulation3DIntegral( + mag_mesh, + survey=survey, + chiMap=identity_map, + ind_active=active_cells, + sensitivity_path=str(tmp_path / f"{engine}"), + store_sensitivities=store_sensitivities, + model_type="vector", + is_amplitude_data=True, + engine=engine, + **parallel_kwargs, + ) - # Compute analytical response from magnetic prism - prism_1 = MagneticPrism(block1[:, 0], block1[:, 1], chi1 * b0 / mu_0) - prism_2 = MagneticPrism(block2[:, 0], block2[:, 1], -chi1 * b0 / mu_0) - prism_3 = MagneticPrism(block2[:, 0], block2[:, 1], chi2 * b0 / mu_0) + data = sim.dpred(model_reduced) - d = ( - prism_1.magnetic_flux_density(locXyz) - + prism_2.magnetic_flux_density(locXyz) - + prism_3.magnetic_flux_density(locXyz) - ) + # Compute analytical response from magnetic prism + block1, block2 = two_blocks + prism_1 = MagneticPrism( + block1[:, 0], block1[:, 1], M1 * np.linalg.norm(b0) / mu_0 + ) + prism_2 = MagneticPrism( + block2[:, 0], block2[:, 1], -M1 * np.linalg.norm(b0) / mu_0 + ) + prism_3 = MagneticPrism( + block2[:, 0], block2[:, 1], M2 * np.linalg.norm(b0) / mu_0 + ) - np.testing.assert_allclose(d_x, d[:, 0]) - np.testing.assert_allclose(d_y, d[:, 1]) - np.testing.assert_allclose(d_z, d[:, 2]) - np.testing.assert_allclose(d_t, d @ tmi) + d = ( + prism_1.magnetic_flux_density(receiver_locations) + + prism_2.magnetic_flux_density(receiver_locations) + + prism_3.magnetic_flux_density(receiver_locations) + ) + d_amp = np.linalg.norm(d, axis=1) + + # Check results + rtol, atol = 5e-7, 1e-6 + np.testing.assert_allclose(data, d_amp, rtol=rtol, atol=atol) + + @pytest.mark.parametrize("engine", ("choclo", "geoana")) + @pytest.mark.parametrize("store_sensitivities", ("ram", "disk", "forward_only")) + def test_sensitivity_dtype( + self, + engine, + store_sensitivities, + mag_mesh, + receiver_locations, + tmp_path, + ): + """Test sensitivity_dtype.""" + # Create survey + receivers = mag.Point(receiver_locations, components="tmi") + sources = mag.UniformBackgroundField( + [receivers], amplitude=50_000, inclination=45, declination=10 + ) + survey = mag.Survey(sources) + # Create reduced identity map for Linear Problem + active_cells = np.ones(mag_mesh.n_cells, dtype=bool) + idenMap = maps.IdentityMap(nP=mag_mesh.n_cells) + # Create simulation + sensitivity_path = tmp_path + if engine == "choclo": + sensitivity_path /= "dummy" + simulation = mag.Simulation3DIntegral( + mag_mesh, + survey=survey, + chiMap=idenMap, + ind_active=active_cells, + engine=engine, + store_sensitivities=store_sensitivities, + sensitivity_path=str(sensitivity_path), + ) + # sensitivity_dtype should be float64 when running forward only, + # but float32 in other cases + if store_sensitivities == "forward_only": + assert simulation.sensitivity_dtype is np.float64 + else: + assert simulation.sensitivity_dtype is np.float32 + + @pytest.mark.parametrize("invalid_dtype", (float, np.float16)) + def test_invalid_sensitivity_dtype_assignment(self, mag_mesh, invalid_dtype): + """ + Test invalid sensitivity_dtype assignment + """ + simulation = mag.Simulation3DIntegral(mag_mesh) + # Check if error is raised + msg = "sensitivity_dtype must be either np.float32 or np.float64." + with pytest.raises(TypeError, match=msg): + simulation.sensitivity_dtype = invalid_dtype + + def test_invalid_engine(self, mag_mesh): + """Test if error is raised after invalid engine.""" + engine = "invalid engine" + msg = rf"'engine' must be in \('geoana', 'choclo'\). Got '{engine}'" + with pytest.raises(ValueError, match=msg): + mag.Simulation3DIntegral(mag_mesh, engine=engine) + + def test_choclo_and_n_proceesses(self, mag_mesh): + """Check if warning is raised after passing n_processes with choclo engine.""" + msg = "The 'n_processes' will be ignored when selecting 'choclo'" + with pytest.warns(UserWarning, match=msg): + simulation = mag.Simulation3DIntegral( + mag_mesh, engine="choclo", n_processes=2 + ) + # Check if n_processes was overwritten and set to None + assert simulation.n_processes is None + + def test_choclo_and_sensitivity_path_as_dir(self, mag_mesh, tmp_path): + """ + Check if error is raised when sensitivity_path is a dir with choclo engine. + """ + # Create a sensitivity_path directory + sensitivity_path = tmp_path / "sensitivity_dummy" + sensitivity_path.mkdir() + # Check if error is raised + msg = f"The passed sensitivity_path '{str(sensitivity_path)}' is a directory" + with pytest.raises(ValueError, match=msg): + mag.Simulation3DIntegral( + mag_mesh, + store_sensitivities="disk", + sensitivity_path=str(sensitivity_path), + engine="choclo", + ) + + def test_sensitivities_on_disk(self, mag_mesh, receiver_locations, tmp_path): + """ + Test if sensitivity matrix is correctly being stored in disk when asked + """ + # Build survey + survey = create_mag_survey( + components=["tmi"], + receiver_locations=receiver_locations, + inducing_field_params=(50000.0, 20.0, 45.0), + ) + # Build simulation + sensitivities_path = tmp_path / "sensitivities" + simulation = mag.Simulation3DIntegral( + mesh=mag_mesh, + survey=survey, + store_sensitivities="disk", + sensitivity_path=str(sensitivities_path), + engine="choclo", + ) + simulation.G + # Check if sensitivity matrix was stored in disk and is a memmap + assert sensitivities_path.is_file() + assert type(simulation.G) is np.memmap + + def test_sensitivities_on_ram(self, mag_mesh, receiver_locations, tmp_path): + """ + Test if sensitivity matrix is correctly being allocated in memory when asked + """ + # Build survey + survey = create_mag_survey( + components=["tmi"], + receiver_locations=receiver_locations, + inducing_field_params=(50000.0, 20.0, 45.0), + ) + # Build simulation + simulation = mag.Simulation3DIntegral( + mesh=mag_mesh, + survey=survey, + store_sensitivities="ram", + engine="choclo", + ) + simulation.G + # Check if sensitivity matrix is a Numpy array (stored in memory) + assert type(simulation.G) is np.ndarray + + def test_choclo_missing(self, mag_mesh, monkeypatch): + """ + Check if error is raised when choclo is missing and chosen as engine. + """ + # Monkeypatch choclo in simpeg.potential_fields.base + monkeypatch.setattr(simpeg.potential_fields.base, "choclo", None) + # Check if error is raised + msg = "The choclo package couldn't be found." + with pytest.raises(ImportError, match=msg): + mag.Simulation3DIntegral(mag_mesh, engine="choclo") def test_ana_mag_tmi_grad_forward(): + """ + Test TMI gradiometry using susceptibilities as model + """ nx = 61 ny = 61 - H0 = (50000.0, 60.0, 250.0) - b0 = mag.analytics.IDTtoxyz(-H0[1], H0[2], H0[0]) + h0_amplitude, h0_inclination, h0_declination = (50000.0, 60.0, 250.0) + b0 = mag.analytics.IDTtoxyz(-h0_inclination, h0_declination, h0_amplitude) chi1 = 0.01 chi2 = 0.02 @@ -164,11 +711,14 @@ def get_block_inds(grid, block): rxLoc = mag.Point(locXyz, components=components) srcField = mag.UniformBackgroundField( - [rxLoc], amplitude=H0[0], inclination=H0[1], declination=H0[2] + receiver_list=[rxLoc], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, ) survey = mag.Survey(srcField) - # Creat reduced identity map for Linear Problem + # Create reduced identity map for Linear Problem idenMap = maps.IdentityMap(nP=int(sum(active_cells))) sim = mag.Simulation3DIntegral( @@ -196,9 +746,15 @@ def get_block_inds(grid, block): + prism_2.magnetic_field_gradient(locXyz) + prism_3.magnetic_field_gradient(locXyz) ) * mu_0 - tmi_x = (d[:, 0, 0] * b0[0] + d[:, 0, 1] * b0[1] + d[:, 0, 2] * b0[2]) / H0[0] - tmi_y = (d[:, 1, 0] * b0[0] + d[:, 1, 1] * b0[1] + d[:, 1, 2] * b0[2]) / H0[0] - tmi_z = (d[:, 2, 0] * b0[0] + d[:, 2, 1] * b0[1] + d[:, 2, 2] * b0[2]) / H0[0] + tmi_x = ( + d[:, 0, 0] * b0[0] + d[:, 0, 1] * b0[1] + d[:, 0, 2] * b0[2] + ) / h0_amplitude + tmi_y = ( + d[:, 1, 0] * b0[0] + d[:, 1, 1] * b0[1] + d[:, 1, 2] * b0[2] + ) / h0_amplitude + tmi_z = ( + d[:, 2, 0] * b0[0] + d[:, 2, 1] * b0[1] + d[:, 2, 2] * b0[2] + ) / h0_amplitude np.testing.assert_allclose(d_x, tmi_x, rtol=1e-10, atol=1e-12) np.testing.assert_allclose(d_y, tmi_y, rtol=1e-10, atol=1e-12) np.testing.assert_allclose(d_z, tmi_z, rtol=1e-10, atol=1e-12) @@ -219,283 +775,40 @@ def get_block_inds(grid, block): ) -def test_ana_mag_grad_forward(): - nx = 5 - ny = 5 - - h0_amplitude, h0_inclination, h0_declination = (50000.0, 60.0, 250.0) - b0 = mag.analytics.IDTtoxyz(-h0_inclination, h0_declination, h0_amplitude) - chi1 = 0.01 - chi2 = 0.02 - - # Define a mesh - cs = 0.2 - hxind = [(cs, 41)] - hyind = [(cs, 41)] - hzind = [(cs, 41)] - mesh = discretize.TensorMesh([hxind, hyind, hzind], "CCC") - - # create a model of two blocks, 1 inside the other - block1 = np.array([[-1.5, 1.5], [-1.5, 1.5], [-1.5, 1.5]]) - block2 = np.array([[-0.7, 0.7], [-0.7, 0.7], [-0.7, 0.7]]) - - def get_block_inds(grid, block): - return np.where( - (grid[:, 0] > block[0, 0]) - & (grid[:, 0] < block[0, 1]) - & (grid[:, 1] > block[1, 0]) - & (grid[:, 1] < block[1, 1]) - & (grid[:, 2] > block[2, 0]) - & (grid[:, 2] < block[2, 1]) - ) - - block1_inds = get_block_inds(mesh.cell_centers, block1) - block2_inds = get_block_inds(mesh.cell_centers, block2) - - model = np.zeros(mesh.n_cells) - model[block1_inds] = chi1 - model[block2_inds] = chi2 - - active_cells = model != 0.0 - model_reduced = model[active_cells] - - # Create reduced identity map for Linear Problem - idenMap = maps.IdentityMap(nP=int(sum(active_cells))) - - # Create plane of observations - xr = np.linspace(-20, 20, nx) - yr = np.linspace(-20, 20, ny) - X, Y = np.meshgrid(xr, yr) - Z = np.ones_like(X) * 3.0 - locXyz = np.c_[X.reshape(-1), Y.reshape(-1), Z.reshape(-1)] - components = ["bxx", "bxy", "bxz", "byy", "byz", "bzz"] - - rxLoc = mag.Point(locXyz, components=components) - srcField = mag.UniformBackgroundField( - [rxLoc], - amplitude=h0_amplitude, - inclination=h0_inclination, - declination=h0_declination, - ) - survey = mag.Survey(srcField) - - # Creat reduced identity map for Linear Problem - idenMap = maps.IdentityMap(nP=int(sum(active_cells))) - - sim = mag.Simulation3DIntegral( - mesh, - survey=survey, - chiMap=idenMap, - ind_active=active_cells, - store_sensitivities="forward_only", - n_processes=None, - ) - - data = sim.dpred(model_reduced) - d_xx = data[0::6] - d_xy = data[1::6] - d_xz = data[2::6] - d_yy = data[3::6] - d_yz = data[4::6] - d_zz = data[5::6] - - # Compute analytical response from magnetic prism - prism_1 = MagneticPrism(block1[:, 0], block1[:, 1], chi1 * b0 / mu_0) - prism_2 = MagneticPrism(block2[:, 0], block2[:, 1], -chi1 * b0 / mu_0) - prism_3 = MagneticPrism(block2[:, 0], block2[:, 1], chi2 * b0 / mu_0) - - d = ( - prism_1.magnetic_field_gradient(locXyz) - + prism_2.magnetic_field_gradient(locXyz) - + prism_3.magnetic_field_gradient(locXyz) - ) * mu_0 - - np.testing.assert_allclose(d_xx, d[..., 0, 0], rtol=1e-10, atol=1e-12) - np.testing.assert_allclose(d_xy, d[..., 0, 1], rtol=1e-10, atol=1e-12) - np.testing.assert_allclose(d_xz, d[..., 0, 2], rtol=1e-10, atol=1e-12) - np.testing.assert_allclose(d_yy, d[..., 1, 1], rtol=1e-10, atol=1e-12) - np.testing.assert_allclose(d_yz, d[..., 1, 2], rtol=1e-10, atol=1e-12) - np.testing.assert_allclose(d_zz, d[..., 2, 2], rtol=1e-10, atol=1e-12) - - -def test_ana_mag_vec_forward(): - nx = 5 - ny = 5 - - h0_amplitude, h0_inclination, h0_declination = (50000.0, 60.0, 250.0) - b0 = mag.analytics.IDTtoxyz(-h0_inclination, h0_declination, h0_amplitude) - - M1 = utils.mat_utils.dip_azimuth2cartesian(45, -40) * 0.05 - M2 = utils.mat_utils.dip_azimuth2cartesian(120, 32) * 0.1 - - # Define a mesh - cs = 0.2 - hxind = [(cs, 41)] - hyind = [(cs, 41)] - hzind = [(cs, 41)] - mesh = discretize.TensorMesh([hxind, hyind, hzind], "CCC") - - # create a model of two blocks, 1 inside the other - block1 = np.array([[-1.5, 1.5], [-1.5, 1.5], [-1.5, 1.5]]) - block2 = np.array([[-0.7, 0.7], [-0.7, 0.7], [-0.7, 0.7]]) - - def get_block_inds(grid, block): - return np.where( - (grid[:, 0] > block[0, 0]) - & (grid[:, 0] < block[0, 1]) - & (grid[:, 1] > block[1, 0]) - & (grid[:, 1] < block[1, 1]) - & (grid[:, 2] > block[2, 0]) - & (grid[:, 2] < block[2, 1]) +class TestInvalidMeshChoclo: + @pytest.fixture(params=("tensormesh", "treemesh")) + def mesh(self, request): + """Sample 2D mesh.""" + hx, hy = [(0.1, 8)], [(0.1, 8)] + h = (hx, hy) + if request.param == "tensormesh": + mesh = discretize.TensorMesh(h, "CC") + else: + mesh = discretize.TreeMesh(h, origin="CC") + mesh.finalize() + return mesh + + def test_invalid_mesh_with_choclo(self, mesh): + """ + Test if simulation raises error when passing an invalid mesh and using choclo + """ + # Build survey + receivers_locations = np.array([[0, 0, 0]]) + receivers = mag.Point(receivers_locations) + sources = mag.UniformBackgroundField( + receiver_list=[receivers], + amplitude=50_000, + inclination=45.0, + declination=12.0, ) - - block1_inds = get_block_inds(mesh.cell_centers, block1) - block2_inds = get_block_inds(mesh.cell_centers, block2) - - model = np.zeros((mesh.n_cells, 3)) - model[block1_inds] = M1 - model[block2_inds] = M2 - - active_cells = np.any(model != 0.0, axis=1) - model_reduced = model[active_cells].reshape(-1, order="F") - - # Create plane of observations - xr = np.linspace(-20, 20, nx) - yr = np.linspace(-20, 20, ny) - X, Y = np.meshgrid(xr, yr) - Z = np.ones_like(X) * 3.0 - locXyz = np.c_[X.reshape(-1), Y.reshape(-1), Z.reshape(-1)] - components = ["bx", "by", "bz", "tmi"] - - rxLoc = mag.Point(locXyz, components=components) - srcField = mag.UniformBackgroundField( - receiver_list=[rxLoc], - amplitude=h0_amplitude, - inclination=h0_inclination, - declination=h0_declination, - ) - survey = mag.Survey(srcField) - - # Create reduced identity map for Linear Problem - idenMap = maps.IdentityMap(nP=int(sum(active_cells)) * 3) - - sim = mag.Simulation3DIntegral( - mesh, - survey=survey, - chiMap=idenMap, - ind_active=active_cells, - store_sensitivities="forward_only", - model_type="vector", - n_processes=None, - ) - - data = sim.dpred(model_reduced).reshape(-1, 4) - - # Compute analytical response from magnetic prism - prism_1 = MagneticPrism(block1[:, 0], block1[:, 1], M1 * np.linalg.norm(b0) / mu_0) - prism_2 = MagneticPrism(block2[:, 0], block2[:, 1], -M1 * np.linalg.norm(b0) / mu_0) - prism_3 = MagneticPrism(block2[:, 0], block2[:, 1], M2 * np.linalg.norm(b0) / mu_0) - - d = ( - prism_1.magnetic_flux_density(locXyz) - + prism_2.magnetic_flux_density(locXyz) - + prism_3.magnetic_flux_density(locXyz) - ) - tmi = sim.tmi_projection - - np.testing.assert_allclose(data[:, 0], d[:, 0]) - np.testing.assert_allclose(data[:, 1], d[:, 1]) - np.testing.assert_allclose(data[:, 2], d[:, 2]) - np.testing.assert_allclose(data[:, 3], d @ tmi) - - -def test_ana_mag_amp_forward(): - nx = 5 - ny = 5 - - h0_amplitude, h0_inclination, h0_declination = (50000.0, 60.0, 250.0) - b0 = mag.analytics.IDTtoxyz(-h0_inclination, h0_declination, h0_amplitude) - - M1 = utils.mat_utils.dip_azimuth2cartesian(45, -40) * 0.05 - M2 = utils.mat_utils.dip_azimuth2cartesian(120, 32) * 0.1 - - # Define a mesh - cs = 0.2 - hxind = [(cs, 41)] - hyind = [(cs, 41)] - hzind = [(cs, 41)] - mesh = discretize.TensorMesh([hxind, hyind, hzind], "CCC") - - # create a model of two blocks, 1 inside the other - block1 = np.array([[-1.5, 1.5], [-1.5, 1.5], [-1.5, 1.5]]) - block2 = np.array([[-0.7, 0.7], [-0.7, 0.7], [-0.7, 0.7]]) - - def get_block_inds(grid, block): - return np.where( - (grid[:, 0] > block[0, 0]) - & (grid[:, 0] < block[0, 1]) - & (grid[:, 1] > block[1, 0]) - & (grid[:, 1] < block[1, 1]) - & (grid[:, 2] > block[2, 0]) - & (grid[:, 2] < block[2, 1]) + survey = mag.Survey(sources) + # Check if error is raised + msg = ( + "Invalid mesh with 2 dimensions. " + "Only 3D meshes are supported when using 'choclo' as engine." ) - - block1_inds = get_block_inds(mesh.cell_centers, block1) - block2_inds = get_block_inds(mesh.cell_centers, block2) - - model = np.zeros((mesh.n_cells, 3)) - model[block1_inds] = M1 - model[block2_inds] = M2 - - active_cells = np.any(model != 0.0, axis=1) - model_reduced = model[active_cells].reshape(-1, order="F") - - # Create plane of observations - xr = np.linspace(-20, 20, nx) - yr = np.linspace(-20, 20, ny) - X, Y = np.meshgrid(xr, yr) - Z = np.ones_like(X) * 3.0 - locXyz = np.c_[X.reshape(-1), Y.reshape(-1), Z.reshape(-1)] - components = ["bx", "by", "bz"] - - rxLoc = mag.Point(locXyz, components=components) - srcField = mag.UniformBackgroundField( - receiver_list=[rxLoc], - amplitude=h0_amplitude, - inclination=h0_inclination, - declination=h0_declination, - ) - survey = mag.Survey(srcField) - - # Create reduced identity map for Linear Problem - idenMap = maps.IdentityMap(nP=int(sum(active_cells)) * 3) - - sim = mag.Simulation3DIntegral( - mesh, - survey=survey, - chiMap=idenMap, - ind_active=active_cells, - store_sensitivities="forward_only", - model_type="vector", - is_amplitude_data=True, - n_processes=None, - ) - - data = sim.dpred(model_reduced) - - # Compute analytical response from magnetic prism - prism_1 = MagneticPrism(block1[:, 0], block1[:, 1], M1 * np.linalg.norm(b0) / mu_0) - prism_2 = MagneticPrism(block2[:, 0], block2[:, 1], -M1 * np.linalg.norm(b0) / mu_0) - prism_3 = MagneticPrism(block2[:, 0], block2[:, 1], M2 * np.linalg.norm(b0) / mu_0) - - d = ( - prism_1.magnetic_flux_density(locXyz) - + prism_2.magnetic_flux_density(locXyz) - + prism_3.magnetic_flux_density(locXyz) - ) - d_amp = np.linalg.norm(d, axis=1) - - np.testing.assert_allclose(data, d_amp) + with pytest.raises(ValueError, match=msg): + mag.Simulation3DIntegral(mesh, survey, engine="choclo") def test_removed_modeltype(): @@ -513,7 +826,3 @@ def test_removed_modeltype(): message = "modelType has been removed, please use model_type." with pytest.raises(NotImplementedError, match=message): sim.modelType - - -if __name__ == "__main__": - unittest.main() diff --git a/tutorials/04-magnetics/plot_2a_magnetics_induced.py b/tutorials/04-magnetics/plot_2a_magnetics_induced.py index d5b90274fd..67889ba62a 100644 --- a/tutorials/04-magnetics/plot_2a_magnetics_induced.py +++ b/tutorials/04-magnetics/plot_2a_magnetics_induced.py @@ -169,8 +169,10 @@ # susceptibility model using the integral formulation. # +############################################################################### # Define the forward simulation. By setting the 'store_sensitivities' keyword # argument to "forward_only", we simulate the data without storing the sensitivities + simulation = magnetics.simulation.Simulation3DIntegral( survey=survey, mesh=mesh, @@ -178,9 +180,21 @@ chiMap=model_map, ind_active=ind_active, store_sensitivities="forward_only", + engine="choclo", ) +############################################################################### +# .. tip:: +# +# Since SimPEG v0.22.0 we can use `Choclo +# `_ as the engine for running the magnetic +# simulations, which results in faster and more memory efficient runs. Just +# pass ``engine="choclo"`` when constructing the simulation. +# + +############################################################################### # Compute predicted data for a susceptibility model + dpred = simulation.dpred(model) # Plot diff --git a/tutorials/04-magnetics/plot_2b_magnetics_mvi.py b/tutorials/04-magnetics/plot_2b_magnetics_mvi.py index 0007708f83..70c9621221 100644 --- a/tutorials/04-magnetics/plot_2b_magnetics_mvi.py +++ b/tutorials/04-magnetics/plot_2b_magnetics_mvi.py @@ -225,8 +225,10 @@ # in the case of remanent magnetization. # +############################################################################### # Define the forward simulation. By setting the 'store_sensitivities' keyword # argument to "forward_only", we simulate the data without storing the sensitivities + simulation = magnetics.simulation.Simulation3DIntegral( survey=survey, mesh=mesh, @@ -236,7 +238,10 @@ store_sensitivities="forward_only", ) + +############################################################################### # Compute predicted data for some model + dpred = simulation.dpred(model) n_data = len(dpred) diff --git a/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py b/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py index 24430e337a..92ab0691e0 100644 --- a/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py +++ b/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py @@ -229,15 +229,27 @@ # class. # +############################################################################### # Define the problem. Define the cells below topography and the mapping + simulation = magnetics.simulation.Simulation3DIntegral( survey=survey, mesh=mesh, model_type="scalar", chiMap=model_map, ind_active=active_cells, + engine="choclo", ) +############################################################################### +# .. tip:: +# +# Since SimPEG v0.22.0 we can use `Choclo +# `_ as the engine for running the magnetic +# simulations, which results in faster and more memory efficient runs. Just +# pass ``engine="choclo"`` when constructing the simulation. +# + ####################################################################### # Define Inverse Problem From cbd200b6774b201f2e6f60cda8b35b4118d512cd Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 31 May 2024 13:03:24 -0700 Subject: [PATCH 015/194] Fix typos in EM docstrings (#1473) Fix minor typos in EM docstrings. --- simpeg/electromagnetics/static/resistivity/receivers.py | 2 +- simpeg/electromagnetics/time_domain/simulation.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/simpeg/electromagnetics/static/resistivity/receivers.py b/simpeg/electromagnetics/static/resistivity/receivers.py index 3e54d85d96..53c2614bb7 100644 --- a/simpeg/electromagnetics/static/resistivity/receivers.py +++ b/simpeg/electromagnetics/static/resistivity/receivers.py @@ -210,7 +210,7 @@ def evalDeriv(self, src, mesh, f, v=None, adjoint=False): v : numpy.ndarray The vector which being multiplied adjoint : bool, default = ``False`` - If ``True``, return the ajoint + If ``True``, return the adjoint Returns ------- diff --git a/simpeg/electromagnetics/time_domain/simulation.py b/simpeg/electromagnetics/time_domain/simulation.py index 8c5f23d006..31720a94c5 100644 --- a/simpeg/electromagnetics/time_domain/simulation.py +++ b/simpeg/electromagnetics/time_domain/simulation.py @@ -537,9 +537,10 @@ def getInitialFieldsDeriv(self, src, v, adjoint=False, f=None): Returns ------- numpy.ndarray - Derivatives of the intial fields with respect to the model for a given source. - (n_edges or n_faces,) numpy.ndarray when `adjoint` is ``False``. (n_param,) numpy.ndarray - when `ajoint` is ``True``. + Derivatives of the initial fields with respect to the model for + a given source. + (n_edges or n_faces,) numpy.ndarray when ``adjoint`` is ``False``. + (n_param,) numpy.ndarray when ``adjoint`` is ``True``. """ ifieldsDeriv = mkvc( getattr(src, "{}InitialDeriv".format(self._fieldType), None)( From dd7cc94d39c0afd46c9eb108c33ba9a67782496d Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 5 Jun 2024 11:36:45 -0700 Subject: [PATCH 016/194] Fix call to private method in GaussianMixtureWithPrior (#1476) Update call of `_print_verbose_msg_init_end` in `GaussianMixtureWithPrior`: since `scikit-learn>=1.5.0` it requires two positional arguments. Use a `try-except` block to maintain support for older versions of `scikit-learn`. Create a new private method for the `GaussianMixtureWithPrior` class to wrap the `_print_verbose_msg_init_end` method including the `try-except` block. Add tests. Fix #1475 --- simpeg/utils/pgi_utils.py | 18 +++++++++- tests/utils/test_gmm_utils.py | 67 +++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/simpeg/utils/pgi_utils.py b/simpeg/utils/pgi_utils.py index 7141fccc98..638b94cfb3 100644 --- a/simpeg/utils/pgi_utils.py +++ b/simpeg/utils/pgi_utils.py @@ -1170,7 +1170,7 @@ def fit_predict(self, X, y=None, debug=False): self.converged_ = True break - self._print_verbose_msg_init_end(lower_bound) + self._custom_print_verbose_msg_init_end(lower_bound) if lower_bound > max_lower_bound or max_lower_bound == -np.inf: max_lower_bound = lower_bound @@ -1193,6 +1193,22 @@ def fit_predict(self, X, y=None, debug=False): return self + def _custom_print_verbose_msg_init_end(self, ll): + """ + Wrapper for the upstream _print_verbose_msg_init_end + + This method was created to provide support of older versions + (scikit-learn<1.5.0) of this private method. + """ + try: + self._print_verbose_msg_init_end(ll, init_has_converged=True) + except TypeError as exception: + # In scikit-learn<1.5.0, the method has a single argument + match = "got an unexpected keyword argument 'init_has_converged'" + if match not in str(exception): + raise + self._print_verbose_msg_init_end(ll) + class GaussianMixtureWithNonlinearRelationships(WeightedGaussianMixture): """Gaussian mixture class for non-linear relationships. diff --git a/tests/utils/test_gmm_utils.py b/tests/utils/test_gmm_utils.py index dbef02a7f5..f6e6c93d18 100644 --- a/tests/utils/test_gmm_utils.py +++ b/tests/utils/test_gmm_utils.py @@ -1,3 +1,4 @@ +import pytest import numpy as np import unittest import discretize @@ -277,5 +278,71 @@ def test_MAP_estimate_multi_component_multidimensions(self): ) +class MockGMMLatest(GaussianMixtureWithPrior): + """ + Mock of ``GaussianMixtureWithPrior`` with a ``_print_verbose_msg_init_end`` + method with two positional arguments (scikit-learn==1.5.0). + """ + + def _print_verbose_msg_init_end(self, ll, init_has_converged): + """Override upstream method just for test purposes.""" + return None + + +class MockGMMOlder(GaussianMixtureWithPrior): + """ + Mock of ``GaussianMixtureWithPrior`` with a ``_print_verbose_msg_init_end`` + method with a single positional argument (scikit-learn<1.5.0). + """ + + def _print_verbose_msg_init_end(self, ll): + """Override upstream method just for test purposes.""" + return None + + +class TestCustomPrintMethod: + """ + Test the ``GaussianMixtureWithPrior._print_verbose_msg_init_end`` method + with different signatures of the upstream ``_print_verbose_msg_init_end`` + private method. + """ + + @pytest.fixture + def mesh(self): + """Sample mesh""" + mesh = discretize.TensorMesh([8, 7, 6]) + return mesh + + @pytest.fixture + def model(self, mesh): + """Sample model.""" + model = np.ones(mesh.n_cells, dtype=np.float64) + return model + + @pytest.fixture + def gmmref(self, mesh, model): + """Sample GMM""" + active_cells = np.ones(mesh.n_cells, dtype=bool) + gmmref = WeightedGaussianMixture( + mesh=mesh, + actv=active_cells, + n_components=1, + covariance_type="full", + max_iter=1000, + n_init=10, + tol=1e-8, + warm_start=True, + ) + gmmref.fit(model.reshape(-1, 1)) + return gmmref + + @pytest.mark.parametrize("gmm_class", (MockGMMLatest, MockGMMOlder)) + def test_custom_print_verbose_method(self, gmmref, gmm_class): + """Test custom method against older and latest signature of the upstream one.""" + gmm = gmm_class(gmmref=gmmref) + # Run the custom private method: it should not raise any error + gmm._custom_print_verbose_msg_init_end(3) + + if __name__ == "__main__": unittest.main() From ae63c363f4cc8118c6fcd0e36f8e43419e9f3739 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 5 Jun 2024 16:31:19 -0700 Subject: [PATCH 017/194] Add version switcher to Sphinx docs (#1428) Add a version switcher to the navbar of the docs, using the built in one in PyData Sphinx theme. Point to the experimental `versions.json` file located in `doctest.simpeg.xyz` that should serve the files in [simpeg/simpeg-doctest](https://github.com/simpeg/simpeg-doctest). Add experimental staged to Azure Pipelines that pushes docs to the `simpeg-doctest` repository. One is triggered nightly and deploys the latest docs in `main` to the `dev` branch in `simpeg-doctest` and updates the `dev` submodule in `gh-pages`. The other one is triggered after a release and pushes the new version of the docs to `gh-pages` branch in `simpeg-doctest` onto a new folder, while also updating the `latest` link. --- .github/ISSUE_TEMPLATE/release-checklist.md | 21 +- azure-pipelines.yml | 245 +++++++++++++++++++- docs/_static/versions.json | 31 +++ docs/conf.py | 15 ++ 4 files changed, 309 insertions(+), 3 deletions(-) create mode 100644 docs/_static/versions.json diff --git a/.github/ISSUE_TEMPLATE/release-checklist.md b/.github/ISSUE_TEMPLATE/release-checklist.md index d173424945..543bdb1fdd 100644 --- a/.github/ISSUE_TEMPLATE/release-checklist.md +++ b/.github/ISSUE_TEMPLATE/release-checklist.md @@ -46,10 +46,9 @@ assignees: "" - [ ] Edit the release notes file, following the template below and the previous release notes. - [ ] Add the new release notes to the list in `docs/content/release/index.rst`. -- [ ] Open a PR with the new release notes. +- [ ] **Open a PR** with the new release notes. - [ ] Manually view the built documentation by downloading the Azure `html_doc` artifact and check for formatting and errors. -- [ ] Merge that PR
@@ -111,6 +110,24 @@ Pull Requests
+### Add new version to version switcher + +Edit the `docs/_static/versions.json` file and: + +- [ ] Add an entry for the new version. +- [ ] Move the line with `"name":` to the new entry (so the new version is set + as the _latest_ one). +- [ ] Update the version number in the `"name":` line. +- [ ] Run `cat docs/_static/versions.json | python -m json.tool > /dev/null` to + check if the syntax of the JSON file is correct. If no errors are prompted, + then your file is OK. +- [ ] Double-check the changes. +- [ ] Commit the changes to the same branch. + +### Merge the PR + +- [ ] **Merge that PR.** + ## Make the new release - [ ] Draft a new GitHub Release diff --git a/azure-pipelines.yml b/azure-pipelines.yml index f7f7ed21a9..fdad0a95f4 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -8,12 +8,22 @@ trigger: include: - '*' +schedules: +- cron: "0 8 * * *" # trigger cron job every day at 08:00 AM GMT + displayName: "Scheduled nightly job" + branches: + include: [ "main" ] + always: false # don't run if no changes have been applied since last sucessful run + batch: false # dont' run if last pipeline is still in-progress + pr: branches: include: - '*' exclude: - '*no-ci*' + + stages: - stage: StyleChecks @@ -75,6 +85,15 @@ stages: python.version: '3.8' timeoutInMinutes: 240 steps: + + # Checkout simpeg repo, including tags. + # We need to sync tags and disable shallow depth in order to get the + # SimPEG version while building the docs. + - checkout: self + fetchDepth: 0 + fetchTags: true + displayName: Checkout repository (including tags) + - script: | git config --global user.name ${GH_NAME} git config --global user.email ${GH_EMAIL} @@ -106,6 +125,12 @@ stages: pip install -e . displayName: Build package + - script: | + source "${HOME}/conda/etc/profile.d/conda.sh" + conda activate simpeg-test + python -c "import simpeg; print(simpeg.__version__)" + displayName: Check SimPEG version + - script: | source "${HOME}/conda/etc/profile.d/conda.sh" conda activate simpeg-test @@ -141,4 +166,222 @@ stages: git push displayName: Push documentation to simpeg-docs env: - GH_TOKEN: $(gh.token) \ No newline at end of file + GH_TOKEN: $(gh.token) + +- stage: Deploy_dev_docs_experimental + dependsOn: Testing + condition: eq(variables['Build.Reason'], 'Schedule') # run only scheduled triggers + jobs: + - job: + displayName: Deploy dev docs to simpeg-doctest (experimental) + pool: + vmImage: ubuntu-latest + variables: + python.version: '3.8' + timeoutInMinutes: 240 + steps: + + # Checkout simpeg repo, including tags. + # We need to sync tags and disable shallow depth in order to get the + # SimPEG version while building the docs. + - checkout: self + fetchDepth: 0 + fetchTags: true + displayName: Checkout repository (including tags) + + - bash: | + git config --global user.name ${GH_NAME} + git config --global user.email ${GH_EMAIL} + git config --list | grep user. + displayName: 'Configure git' + env: + GH_NAME: $(gh.name) + GH_EMAIL: $(gh.email) + + - bash: | + wget -O Mambaforge.sh "https://github.com/conda-forge/miniforge/releases/latest/download/Mambaforge-$(uname)-$(uname -m).sh" + bash Mambaforge.sh -b -p "${HOME}/conda" + displayName: Install mamba + + - bash: | + source "${HOME}/conda/etc/profile.d/conda.sh" + source "${HOME}/conda/etc/profile.d/mamba.sh" + cp environment_test.yml environment_test_with_pyversion.yml + echo " - python="$(python.version) >> environment_test_with_pyversion.yml + mamba env create -f environment_test_with_pyversion.yml + rm environment_test_with_pyversion.yml + conda activate simpeg-test + pip install pytest-azurepipelines + displayName: Create Anaconda testing environment + + - bash: | + source "${HOME}/conda/etc/profile.d/conda.sh" + conda activate simpeg-test + pip install -e . + displayName: Build package + + - script: | + source "${HOME}/conda/etc/profile.d/conda.sh" + conda activate simpeg-test + python -c "import simpeg; print(simpeg.__version__)" + displayName: Check SimPEG version + + - bash: | + source "${HOME}/conda/etc/profile.d/conda.sh" + conda activate simpeg-test + export KMP_WARNINGS=0 + make -C docs html + displayName: Building documentation + + # Upload dev build of the docs to a dev branch in simpeg/simpeg-doctest + # and update submodule in the gh-pages branch + - bash: | + # Push new docs + # ------------- + # Capture hash of last commit in simpeg + commit=$(git rev-parse --short HEAD) + # Clone the repo where we store the documentation (dev branch) + git clone -b dev --depth 1 https://${GH_TOKEN}@github.com/simpeg/simpeg-doctest.git + cd simpeg-doctest + # Remove all files + shopt -s dotglob # configure bash to include dotfiles in * globs + export GLOBIGNORE=".git" # ignore .git directory in glob + git rm -rf * # remove all files + # Copy the built docs to the root of the repo + cp -r $BUILD_SOURCESDIRECTORY/docs/_build/html/* -t . + # Commit the new docs. Amend to avoid having a very large history. + git add . + message="Azure CI deploy dev from ${commit}" + echo -e "\nAmending last commit:" + git commit --amend --reset-author -m "$message" + # Make the push quiet just in case there is anything that could + # leak sensitive information. + echo -e "\nPushing changes to simpeg/simpeg-doctest (dev branch)." + git push -fq origin dev 2>&1 >/dev/null + echo -e "\nFinished uploading doc files." + + # Update submodule + # ---------------- + # Switch to the gh-pages branch + git switch gh-pages + # Update the dev submodule + git submodule update --init --recursive --remote dev + # Commit changes + git add dev + message="Azure CI update dev submodule from ${commit}" + echo -e "\nMaking a new commit:" + git commit -m "$message" + # Make the push quiet just in case there is anything that could + # leak sensitive information. + echo -e "\nPushing changes to simpeg/simpeg-doctest (gh-pages branch)." + git push -fq origin gh-pages 2>&1 >/dev/null + echo -e "\nFinished updating submodule dev." + + # Unset dotglob + shopt -u dotglob + export GLOBIGNORE="" + displayName: Push documentation to simpeg-doctest (dev branch) + env: + GH_TOKEN: $(gh.token) + +- stage: Deploy_release_docs_experimental + dependsOn: Testing + condition: startsWith(variables['build.sourceBranch'], 'refs/tags/') + jobs: + - job: + displayName: Deploy release docs to simpeg-doctest (experimental) + pool: + vmImage: ubuntu-latest + variables: + python.version: '3.8' + timeoutInMinutes: 240 + steps: + + # Checkout simpeg repo, including tags. + # We need to sync tags and disable shallow depth in order to get the + # SimPEG version while building the docs. + - checkout: self + fetchDepth: 0 + fetchTags: true + displayName: Checkout repository (including tags) + + - bash: | + git config --global user.name ${GH_NAME} + git config --global user.email ${GH_EMAIL} + git config --list | grep user. + displayName: 'Configure git' + env: + GH_NAME: $(gh.name) + GH_EMAIL: $(gh.email) + + - bash: | + wget -O Mambaforge.sh "https://github.com/conda-forge/miniforge/releases/latest/download/Mambaforge-$(uname)-$(uname -m).sh" + bash Mambaforge.sh -b -p "${HOME}/conda" + displayName: Install mamba + + - bash: | + source "${HOME}/conda/etc/profile.d/conda.sh" + source "${HOME}/conda/etc/profile.d/mamba.sh" + cp environment_test.yml environment_test_with_pyversion.yml + echo " - python="$(python.version) >> environment_test_with_pyversion.yml + mamba env create -f environment_test_with_pyversion.yml + rm environment_test_with_pyversion.yml + conda activate simpeg-test + pip install pytest-azurepipelines + displayName: Create Anaconda testing environment + + - bash: | + source "${HOME}/conda/etc/profile.d/conda.sh" + conda activate simpeg-test + pip install -e . + displayName: Build package + + - script: | + source "${HOME}/conda/etc/profile.d/conda.sh" + conda activate simpeg-test + python -c "import simpeg; print(simpeg.__version__)" + displayName: Check SimPEG version + + - bash: | + source "${HOME}/conda/etc/profile.d/conda.sh" + conda activate simpeg-test + export KMP_WARNINGS=0 + make -C docs html + displayName: Building documentation + + # Upload release build of the docs to gh-pages branch in simpeg/simpeg-doctest + - bash: | + # Capture version + # TODO: we should be able to get the version from the + # build.sourceBranch variable + version=$(git tag --points-at HEAD) + if [ -n "$version" ]; then + echo "Version could not be obtained from tag. Exiting." + exit 1 + fi + # Capture hash of last commit in simpeg + commit=$(git rev-parse --short HEAD) + # Clone the repo where we store the documentation + git clone --depth 1 https://${GH_TOKEN}@github.com/simpeg/simpeg-doctest.git + cd simpeg-doctest + # Move the built docs to a new dev folder + cp -r $BUILD_SOURCESDIRECTORY/docs/_build/html "$version" + cp $BUILD_SOURCESDIRECTORY/docs/README.md . + # Add .nojekyll if missing + touch .nojekyll + # Update latest symlink + rm -f latest + ln -s "$version" latest + # Commit the new docs. + git add "$version" README.md .nojekyll latest + message="Azure CI deploy ${version} from ${commit}" + echo -e "\nMaking a new commit:" + git commit -m "$message" + # Make the push quiet just in case there is anything that could + # leak sensitive information. + echo -e "\nPushing changes to simpeg/simpeg-doctest." + git push -fq origin gh-pages 2>&1 >/dev/null + echo -e "\nFinished uploading generated files." + displayName: Push documentation to simpeg-doctest + env: + GH_TOKEN: $(gh.token) diff --git a/docs/_static/versions.json b/docs/_static/versions.json new file mode 100644 index 0000000000..265c9718fa --- /dev/null +++ b/docs/_static/versions.json @@ -0,0 +1,31 @@ +[ + { + "version": "dev", + "url": "https://doctest.simpeg.xyz/dev/" + }, + { + "name": "v0.21.1 (latest)", + "version": "v0.21.1", + "url": "https://doctest.simpeg.xyz/v0.21.1/" + }, + { + "version": "v0.21.0", + "url": "https://doctest.simpeg.xyz/v0.21.0/" + }, + { + "version": "v0.20.0", + "url": "https://doctest.simpeg.xyz/v0.20.0/" + }, + { + "version": "v0.19.0", + "url": "https://doctest.simpeg.xyz/v0.19.0/" + }, + { + "version": "v0.18.1", + "url": "https://doctest.simpeg.xyz/v0.18.1/" + }, + { + "version": "v0.18.0", + "url": "https://doctest.simpeg.xyz/v0.18.0/" + } +] diff --git a/docs/conf.py b/docs/conf.py index 30d36e1446..b71abf3649 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,6 +16,7 @@ from sphinx_gallery.sorting import FileNameSortKey import glob import simpeg +from packaging.version import parse import plotly.io as pio from importlib.metadata import version @@ -237,6 +238,13 @@ def linkcode_resolve(domain, info): dict(name="Contact", url="https://mattermost.softwareunderground.org/simpeg"), ] +# Define SimPEG version for the version switcher +simpeg_version = parse(simpeg.__version__) +if simpeg_version.is_devrelease: + switcher_version = "dev" +else: + switcher_version = f"v{simpeg_version.public}" + # Use Pydata Sphinx theme html_theme = "pydata_sphinx_theme" @@ -274,7 +282,14 @@ def linkcode_resolve(domain, info): "plausible_analytics_url": "https://plausible.io/js/script.js", }, "navbar_align": "left", # make elements closer to logo on the left + "navbar_end": ["version-switcher", "theme-switcher", "navbar-icon-links"], + # Configure version switcher + "switcher": { + "version_match": switcher_version, + "json_url": "https://doctest.simpeg.xyz/latest/_static/versions.json", + }, } + html_logo = "images/simpeg-logo.png" html_static_path = ["_static"] From 23d8cd3de6de0b798892beec48ecc57a138fc5ca Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 5 Jun 2024 18:21:28 -0700 Subject: [PATCH 018/194] Use random seed on synthetic data in mag tests (#1457) Make use of the `random_seed` argument of the `make_synthetic_data` method in magnetic tests. Remove the lines that set a global `np.random.seed` in those tests. Part of the solution to #1289 --- tests/pf/test_mag_MVI_Octree.py | 7 +++++-- tests/pf/test_mag_inversion_linear.py | 2 -- tests/pf/test_mag_inversion_linear_Octree.py | 8 +++++--- tests/pf/test_mag_vector_amplitude.py | 7 +++++-- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/tests/pf/test_mag_MVI_Octree.py b/tests/pf/test_mag_MVI_Octree.py index 5fc3fc68c2..48aa8e26bb 100644 --- a/tests/pf/test_mag_MVI_Octree.py +++ b/tests/pf/test_mag_MVI_Octree.py @@ -19,7 +19,6 @@ class MVIProblemTest(unittest.TestCase): def setUp(self): - np.random.seed(0) h0_amplitude, h0_inclination, h0_declination = (50000.0, 90.0, 0.0) # The magnetization is set along a different @@ -106,7 +105,11 @@ def setUp(self): # Compute some data and add some random noise data = sim.make_synthetic_data( - utils.mkvc(self.model), relative_error=0.0, noise_floor=5.0, add_noise=True + utils.mkvc(self.model), + relative_error=0.0, + noise_floor=5.0, + add_noise=True, + random_seed=0, ) # This Mapping connects the regularizations for the three-component diff --git a/tests/pf/test_mag_inversion_linear.py b/tests/pf/test_mag_inversion_linear.py index 2e766c7478..47d8df2321 100644 --- a/tests/pf/test_mag_inversion_linear.py +++ b/tests/pf/test_mag_inversion_linear.py @@ -20,8 +20,6 @@ class MagInvLinProblemTest(unittest.TestCase): def setUp(self): - np.random.seed(0) - # Define the inducing field parameter h0_amplitude, h0_inclination, h0_declination = (50000, 90, 0) diff --git a/tests/pf/test_mag_inversion_linear_Octree.py b/tests/pf/test_mag_inversion_linear_Octree.py index a78e714e2e..d754d64e5c 100644 --- a/tests/pf/test_mag_inversion_linear_Octree.py +++ b/tests/pf/test_mag_inversion_linear_Octree.py @@ -18,8 +18,6 @@ class MagInvLinProblemTest(unittest.TestCase): def setUp(self): - np.random.seed(0) - # First we need to define the direction of the inducing field # As a simple case, we pick a vertical inducing field of magnitude # 50,000nT. @@ -111,7 +109,11 @@ def setUp(self): ) self.sim = sim data = sim.make_synthetic_data( - self.model, relative_error=0.0, noise_floor=1.0, add_noise=True + self.model, + relative_error=0.0, + noise_floor=1.0, + add_noise=True, + random_seed=0, ) # Create a regularization diff --git a/tests/pf/test_mag_vector_amplitude.py b/tests/pf/test_mag_vector_amplitude.py index 65cdde4ddc..5115e4a22a 100644 --- a/tests/pf/test_mag_vector_amplitude.py +++ b/tests/pf/test_mag_vector_amplitude.py @@ -19,7 +19,6 @@ class MVIProblemTest(unittest.TestCase): def setUp(self): - np.random.seed(0) h0_amplitude, h0_inclination, h0_declination = (50000.0, 90.0, 0.0) # The magnetization is set along a different @@ -106,7 +105,11 @@ def setUp(self): # Compute some data and add some random noise data = sim.make_synthetic_data( - utils.mkvc(self.model), relative_error=0.0, noise_floor=5.0, add_noise=True + utils.mkvc(self.model), + relative_error=0.0, + noise_floor=5.0, + add_noise=True, + random_seed=0, ) reg = regularization.VectorAmplitude( From f6b8035c2f8324368160facf7b9b323a3b570402 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 6 Jun 2024 11:23:38 -0700 Subject: [PATCH 019/194] Fix links to source code in documentation pages (#1444) Update the `linkcode_resolve` function in Sphinx configuration file so the links to the source code of each function, class or method points to the actual code corresponding to that version, and not always to the code in `main`. Update some Azure configurations: checkout simpeg repo without shallow depth and fetching tags. Clean up the working directory before installing SimPEG so the version doesn't have an extra hash. --------- Co-authored-by: Joseph Capriotti --- .azure-pipelines/matrix.yml | 23 ++++++++++++++++++-- azure-pipelines.yml | 8 ++++++- docs/conf.py | 43 +++++++++++++++++++++---------------- 3 files changed, 53 insertions(+), 21 deletions(-) diff --git a/.azure-pipelines/matrix.yml b/.azure-pipelines/matrix.yml index cc3fa7c170..4269e7bebe 100644 --- a/.azure-pipelines/matrix.yml +++ b/.azure-pipelines/matrix.yml @@ -23,6 +23,15 @@ jobs: vmImage: ${{ os }} timeoutInMinutes: 120 steps: + + # Checkout simpeg repo, including tags. + # We need to sync tags and disable shallow depth in order to get the + # SimPEG version while building the docs. + - checkout: self + fetchDepth: 0 + fetchTags: true + displayName: Checkout repository (including tags) + - script: | wget -O Mambaforge.sh "https://github.com/conda-forge/miniforge/releases/latest/download/Mambaforge-$(uname)-$(uname -m).sh" bash Mambaforge.sh -b -p "${HOME}/conda" @@ -31,10 +40,14 @@ jobs: - script: | source "${HOME}/conda/etc/profile.d/conda.sh" source "${HOME}/conda/etc/profile.d/mamba.sh" - echo " - python="${{ py_vers }} >> environment_test.yml - mamba env create -f environment_test.yml + cp environment_test.yml environment_test_with_pyversion.yml + echo " - python="${{ py_vers }} >> environment_test_with_pyversion.yml + mamba env create -f environment_test_with_pyversion.yml + rm environment_test_with_pyversion.yml conda activate simpeg-test pip install pytest-azurepipelines + echo "\nList installed packages" + conda list displayName: Create Anaconda testing environment - script: | @@ -43,6 +56,12 @@ jobs: pip install -e . displayName: Build package + - script: | + source "${HOME}/conda/etc/profile.d/conda.sh" + conda activate simpeg-test + python -c "import simpeg; print(simpeg.__version__)" + displayName: Check SimPEG version + - script: | source "${HOME}/conda/etc/profile.d/conda.sh" conda activate simpeg-test diff --git a/azure-pipelines.yml b/azure-pipelines.yml index fdad0a95f4..db36785acc 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -117,6 +117,8 @@ stages: rm environment_test_with_pyversion.yml conda activate simpeg-test pip install pytest-azurepipelines + echo "\nList installed packages" + conda list displayName: Create Anaconda testing environment - script: | @@ -212,6 +214,8 @@ stages: rm environment_test_with_pyversion.yml conda activate simpeg-test pip install pytest-azurepipelines + echo "\nList installed packages" + conda list displayName: Create Anaconda testing environment - bash: | @@ -328,6 +332,8 @@ stages: rm environment_test_with_pyversion.yml conda activate simpeg-test pip install pytest-azurepipelines + echo "\nList installed packages" + conda list displayName: Create Anaconda testing environment - bash: | @@ -384,4 +390,4 @@ stages: echo -e "\nFinished uploading generated files." displayName: Push documentation to simpeg-doctest env: - GH_TOKEN: $(gh.token) + GH_TOKEN: $(gh.token) \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index b71abf3649..38bdeef6b2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,7 +48,6 @@ "sphinx.ext.mathjax", "sphinx_gallery.gen_gallery", "sphinx.ext.todo", - "sphinx.ext.linkcode", "matplotlib.sphinxext.plot_directive", ] @@ -136,17 +135,24 @@ # edit_on_github_branch = "main/docs" # check_meta = False -# source code links + +# ----------------- +# Source code links +# ----------------- +# Function inspired in matplotlib's configuration + link_github = True -# You can build old with link_github = False if link_github: import inspect - from os.path import relpath, dirname + from packaging.version import parse extensions.append("sphinx.ext.linkcode") def linkcode_resolve(domain, info): + """ + Determine the URL corresponding to Python object + """ if domain != "py": return None @@ -161,44 +167,45 @@ def linkcode_resolve(domain, info): for part in fullname.split("."): try: obj = getattr(obj, part) - except Exception: + except AttributeError: return None - try: - unwrap = inspect.unwrap - except AttributeError: - pass - else: - obj = unwrap(obj) - + if inspect.isfunction(obj): + obj = inspect.unwrap(obj) try: fn = inspect.getsourcefile(obj) - except Exception: + except TypeError: fn = None + if not fn or fn.endswith("__init__.py"): + try: + fn = inspect.getsourcefile(sys.modules[obj.__module__]) + except (TypeError, AttributeError, KeyError): + fn = None if not fn: return None try: source, lineno = inspect.getsourcelines(obj) - except Exception: + except (OSError, TypeError): lineno = None if lineno: - linespec = "#L%d-L%d" % (lineno, lineno + len(source) - 1) + linespec = f"#L{lineno:d}-L{lineno + len(source) - 1:d}" else: linespec = "" try: - fn = relpath(fn, start=dirname(simpeg.__file__)) + fn = os.path.relpath(fn, start=os.path.dirname(simpeg.__file__)) except ValueError: return None - return f"https://github.com/simpeg/simpeg/blob/main/simpeg/{fn}{linespec}" + simpeg_version = parse(simpeg.__version__) + tag = "main" if simpeg_version.is_devrelease else f"v{simpeg_version.public}" + return f"https://github.com/simpeg/simpeg/blob/{tag}/simpeg/{fn}{linespec}" else: extensions.append("sphinx.ext.viewcode") - # Make numpydoc to generate plots for example sections numpydoc_use_plots = True plot_pre_code = """ From 30fadd308a796e292c220b94e968c9502ab26e3f Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 6 Jun 2024 12:59:45 -0700 Subject: [PATCH 020/194] Fix script for new deployment of docs (#1478) Fix issues in the new (experimental) scripts for deployment of docs to `simpeg-doctest`. Fetch the `gh-pages` before trying to checkout it. This is needed because we clone the repo with `--depth 1`. When passing this option, git appends the `--single-branch` option meaning the `dev` branch is the only branch that is being cloned. Improve how we clone the `simpeg-doctest` repo: clone quietly to reduce output lines, and specify the branch while deploying release docs (in case the default branch in the repo gets changed). --- azure-pipelines.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index db36785acc..7866c36069 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -245,7 +245,7 @@ stages: # Capture hash of last commit in simpeg commit=$(git rev-parse --short HEAD) # Clone the repo where we store the documentation (dev branch) - git clone -b dev --depth 1 https://${GH_TOKEN}@github.com/simpeg/simpeg-doctest.git + git clone -q --branch dev --depth 1 https://${GH_TOKEN}@github.com/simpeg/simpeg-doctest.git cd simpeg-doctest # Remove all files shopt -s dotglob # configure bash to include dotfiles in * globs @@ -266,6 +266,9 @@ stages: # Update submodule # ---------------- + # Need to fetch the gh-pages branch first (because we clone with + # shallow depth) + git fetch --depth 1 origin gh-pages:gh-pages # Switch to the gh-pages branch git switch gh-pages # Update the dev submodule @@ -278,7 +281,7 @@ stages: # Make the push quiet just in case there is anything that could # leak sensitive information. echo -e "\nPushing changes to simpeg/simpeg-doctest (gh-pages branch)." - git push -fq origin gh-pages 2>&1 >/dev/null + git push -q origin gh-pages 2>&1 >/dev/null echo -e "\nFinished updating submodule dev." # Unset dotglob @@ -368,7 +371,7 @@ stages: # Capture hash of last commit in simpeg commit=$(git rev-parse --short HEAD) # Clone the repo where we store the documentation - git clone --depth 1 https://${GH_TOKEN}@github.com/simpeg/simpeg-doctest.git + git clone -q --branch gh-pages --depth 1 https://${GH_TOKEN}@github.com/simpeg/simpeg-doctest.git cd simpeg-doctest # Move the built docs to a new dev folder cp -r $BUILD_SOURCESDIRECTORY/docs/_build/html "$version" From 772429a0a2e8aa5a59d14d9e42262dcf667bae9a Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 6 Jun 2024 15:13:30 -0700 Subject: [PATCH 021/194] Print SimPEG version in the inversion log (#1477) Include the version of SimPEG being used to run a simulation in the inversion log. Fixes #1474 --- simpeg/inverse_problem.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/simpeg/inverse_problem.py b/simpeg/inverse_problem.py index b4f5630bd9..e554c95cee 100644 --- a/simpeg/inverse_problem.py +++ b/simpeg/inverse_problem.py @@ -14,13 +14,22 @@ validate_ndarray_with_shape, ) from .simulation import DefaultSolver +from .version import __version__ as simpeg_version class BaseInvProblem: """BaseInvProblem(dmisfit, reg, opt)""" def __init__( - self, dmisfit, reg, opt, beta=1.0, debug=False, counter=None, **kwargs + self, + dmisfit, + reg, + opt, + beta=1.0, + debug=False, + counter=None, + print_version=True, + **kwargs, ): super().__init__(**kwargs) assert isinstance(reg, BaseRegularization) or isinstance( @@ -35,6 +44,7 @@ def __init__( self.debug = debug self.counter = counter self.model = None + self.print_version = print_version # TODO: Remove: (and make iteration printers better!) self.opt.parent = self self.reg.parent = self @@ -174,6 +184,9 @@ def startup(self, m0): if self.debug: print("Calling InvProblem.startup") + if self.print_version: + print(f"\nRunning inversion with SimPEG v{simpeg_version}") + for fct in self.reg.objfcts: if ( hasattr(fct, "reference_model") From caa60b71a25d87319111ffcd948b03cf6a01a43f Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 7 Jun 2024 12:58:57 -0700 Subject: [PATCH 022/194] Use Numpy rng in FDEM tests (#1449) Replace the usage of the deprecated functions in `numpy.random` module for the Numpy's random number generator class and its methods, in most of the FDEM tests. Increase number of iterations for checking derivatives where needed. Part of the solution to #1289 --------- Co-authored-by: Lindsey Heagy --- tests/em/fdem/forward/test_FDEM_casing.py | 22 ++++++++++++++----- tests/em/fdem/forward/test_FDEM_primsec.py | 8 +++---- tests/em/fdem/forward/test_properties.py | 9 ++++---- .../inverse/adjoint/test_FDEM_adjointEB.py | 9 ++++---- .../inverse/adjoint/test_FDEM_adjointHJ.py | 9 ++++---- .../fdem/inverse/derivs/test_FDEM_derivs.py | 7 +++--- tests/em/fdem/muinverse/test_muinverse.py | 21 ++++++++++-------- 7 files changed, 50 insertions(+), 35 deletions(-) diff --git a/tests/em/fdem/forward/test_FDEM_casing.py b/tests/em/fdem/forward/test_FDEM_casing.py index f1aa75a736..a0ceffc3c8 100644 --- a/tests/em/fdem/forward/test_FDEM_casing.py +++ b/tests/em/fdem/forward/test_FDEM_casing.py @@ -12,9 +12,10 @@ sigma = np.r_[10.0, 5.5e6, 1e-1] mu = mu_0 * np.r_[1.0, 100.0, 1.0] srcloc = np.r_[0.0, 0.0, 0.0] -xobs = np.random.rand(n) + 10.0 +rng = np.random.default_rng(seed=42) +xobs = rng.uniform(size=n) + 10.0 yobs = np.zeros(n) -zobs = np.random.randn(n) +zobs = rng.normal(size=n) def CasingMagDipoleDeriv_r(x): @@ -63,15 +64,24 @@ def CasingMagDipole2Deriv_z_z(z): class Casing_DerivTest(unittest.TestCase): def test_derivs(self): + rng = np.random.default_rng(seed=42) + + np.random.seed(1983) # set a random seed for check_derivative tests.check_derivative( - CasingMagDipoleDeriv_r, np.ones(n) * 10 + np.random.randn(n), plotIt=False + CasingMagDipoleDeriv_r, np.ones(n) * 10 + rng.normal(size=n), plotIt=False ) - tests.check_derivative(CasingMagDipoleDeriv_z, np.random.randn(n), plotIt=False) + + np.random.seed(1983) # set a random seed for check_derivative + tests.check_derivative(CasingMagDipoleDeriv_z, rng.normal(size=n), plotIt=False) + + np.random.seed(1983) # set a random seed for check_derivative tests.check_derivative( CasingMagDipole2Deriv_z_r, - np.ones(n) * 10 + np.random.randn(n), + np.ones(n) * 10 + rng.normal(size=n), plotIt=False, ) + + np.random.seed(1983) # set a random seed for check_derivative tests.check_derivative( - CasingMagDipole2Deriv_z_z, np.random.randn(n), plotIt=False + CasingMagDipole2Deriv_z_z, rng.normal(size=n), plotIt=False ) diff --git a/tests/em/fdem/forward/test_FDEM_primsec.py b/tests/em/fdem/forward/test_FDEM_primsec.py index c66b688bd0..a4e4494871 100644 --- a/tests/em/fdem/forward/test_FDEM_primsec.py +++ b/tests/em/fdem/forward/test_FDEM_primsec.py @@ -16,8 +16,6 @@ TOL_JT = 1e-10 FLR = 1e-20 # "zero", so if residual below this --> pass regardless of order -np.random.seed(2016) - # To test the primary secondary-source, we look at make sure doing primary # secondary for a simple model gives comprable results to just solving a 3D # problem @@ -128,6 +126,7 @@ def fun(x): lambda x: self.secondarySimulation.Jvec(x0, x, f=self.fields_primsec), ] + np.random.seed(1983) # set a random seed for check_derivative return tests.check_derivative(fun, x0, num=2, plotIt=False) def AdjointTest(self): @@ -135,8 +134,9 @@ def AdjointTest(self): m = model f = self.fields_primsec - v = np.random.rand(self.secondarySurvey.nD) - w = np.random.rand(self.secondarySimulation.sigmaMap.nP) + rng = np.random.default_rng(seed=2016) + v = rng.uniform(size=self.secondarySurvey.nD) + w = rng.uniform(size=self.secondarySimulation.sigmaMap.nP) vJw = v.dot(self.secondarySimulation.Jvec(m, w, f)) wJtv = w.dot(self.secondarySimulation.Jtvec(m, v, f)) diff --git a/tests/em/fdem/forward/test_properties.py b/tests/em/fdem/forward/test_properties.py index 2b20b8c900..c583907f7e 100644 --- a/tests/em/fdem/forward/test_properties.py +++ b/tests/em/fdem/forward/test_properties.py @@ -44,12 +44,13 @@ def test_source_properties_validation(): # LineCurrent with pytest.raises(TypeError): fdem.sources.LineCurrent([], frequency, location=["a", "b", "c"]) + rng = np.random.default_rng(seed=42) + random_locations = rng.normal(size=(5, 3, 2)) with pytest.raises(ValueError): - fdem.sources.LineCurrent([], frequency, location=np.random.rand(5, 3, 2)) + fdem.sources.LineCurrent([], frequency, location=random_locations) + random_locations = rng.normal(size=(5, 3)) with pytest.raises(ValueError): - fdem.sources.LineCurrent( - [], frequency, location=np.random.rand(5, 3), current=0.0 - ) + fdem.sources.LineCurrent([], frequency, location=random_locations, current=0.0) def test_bad_source_type(): diff --git a/tests/em/fdem/inverse/adjoint/test_FDEM_adjointEB.py b/tests/em/fdem/inverse/adjoint/test_FDEM_adjointEB.py index 618d133b5b..260227a860 100644 --- a/tests/em/fdem/inverse/adjoint/test_FDEM_adjointEB.py +++ b/tests/em/fdem/inverse/adjoint/test_FDEM_adjointEB.py @@ -26,17 +26,18 @@ def adjointTest(fdemType, comp): m = np.log(np.ones(prb.sigmaMap.nP) * CONDUCTIVITY) mu = np.ones(prb.mesh.nC) * MU + rng = np.random.default_rng(seed=42) if addrandoms is True: - m = m + np.random.randn(prb.sigmaMap.nP) * np.log(CONDUCTIVITY) * 1e-1 - mu = mu + np.random.randn(prb.mesh.nC) * MU * 1e-1 + m = m + rng.normal(size=prb.sigmaMap.nP) * np.log(CONDUCTIVITY) * 1e-1 + mu = mu + rng.normal(size=prb.mesh.nC) * MU * 1e-1 survey = prb.survey # prb.PropMap.PropModel.mu = mu # prb.PropMap.PropModel.mui = 1./mu u = prb.fields(m) - v = np.random.rand(survey.nD) - w = np.random.rand(prb.mesh.nC) + v = rng.uniform(size=survey.nD) + w = rng.uniform(size=prb.mesh.nC) vJw = v.dot(prb.Jvec(m, w, u)) wJtv = w.dot(prb.Jtvec(m, v, u)) diff --git a/tests/em/fdem/inverse/adjoint/test_FDEM_adjointHJ.py b/tests/em/fdem/inverse/adjoint/test_FDEM_adjointHJ.py index 4713ff4611..bb0dcac83b 100644 --- a/tests/em/fdem/inverse/adjoint/test_FDEM_adjointHJ.py +++ b/tests/em/fdem/inverse/adjoint/test_FDEM_adjointHJ.py @@ -26,15 +26,16 @@ def adjointTest(fdemType, comp, src): m = np.log(np.ones(prb.sigmaMap.nP) * CONDUCTIVITY) mu = np.ones(prb.mesh.nC) * MU + rng = np.random.default_rng(seed=42) if addrandoms is True: - m = m + np.random.randn(prb.sigmaMap.nP) * np.log(CONDUCTIVITY) * 1e-1 - mu = mu + np.random.randn(prb.mesh.nC) * MU * 1e-1 + m = m + rng.normal(size=prb.sigmaMap.nP) * np.log(CONDUCTIVITY) * 1e-1 + mu = mu + rng.normal(size=prb.mesh.nC) * MU * 1e-1 survey = prb.survey u = prb.fields(m) - v = np.random.rand(survey.nD) - w = np.random.rand(prb.mesh.nC) + v = rng.uniform(size=survey.nD) + w = rng.uniform(size=prb.mesh.nC) vJw = v.dot(prb.Jvec(m, w, u)) wJtv = w.dot(prb.Jtvec(m, v, u)) diff --git a/tests/em/fdem/inverse/derivs/test_FDEM_derivs.py b/tests/em/fdem/inverse/derivs/test_FDEM_derivs.py index 7434ade3e1..c30005ba36 100644 --- a/tests/em/fdem/inverse/derivs/test_FDEM_derivs.py +++ b/tests/em/fdem/inverse/derivs/test_FDEM_derivs.py @@ -29,19 +29,18 @@ def derivTest(fdemType, comp, src): prb = getFDEMProblem(fdemType, comp, SrcType, freq) - # prb.solverOpts = dict(check_accuracy=True) print(f"{fdemType} formulation {src} - {comp}") x0 = np.log(np.ones(prb.sigmaMap.nP) * CONDUCTIVITY) - # mu = np.log(np.ones(prb.mesh.nC)*MU) if addrandoms is True: - x0 = x0 + np.random.randn(prb.sigmaMap.nP) * np.log(CONDUCTIVITY) * 1e-1 - # mu = mu + np.random.randn(prb.sigmaMap.nP)*MU*1e-1 + rng = np.random.default_rng(seed=42) + x0 = x0 + rng.normal(size=prb.sigmaMap.nP) * np.log(CONDUCTIVITY) * 1e-1 def fun(x): return prb.dpred(x), lambda x: prb.Jvec(x0, x) + np.random.seed(1983) # set a random seed for check_derivative return tests.check_derivative(fun, x0, num=2, plotIt=False, eps=FLR) diff --git a/tests/em/fdem/muinverse/test_muinverse.py b/tests/em/fdem/muinverse/test_muinverse.py index 50864fa9e3..7e3841d396 100644 --- a/tests/em/fdem/muinverse/test_muinverse.py +++ b/tests/em/fdem/muinverse/test_muinverse.py @@ -18,8 +18,9 @@ def setupMeshModel(): hz = [(cs, npad, -1.3), (cs, nc), (cs, npad, 1.3)] mesh = discretize.CylindricalMesh([hx, 1.0, hz], "0CC") - muMod = 1 + MuMax * np.random.randn(mesh.nC) - sigmaMod = np.random.randn(mesh.nC) + rng = np.random.default_rng(seed=2016) + muMod = 1 + MuMax * rng.normal(size=mesh.nC) + sigmaMod = rng.normal(size=mesh.nC) return mesh, muMod, sigmaMod @@ -149,7 +150,8 @@ def test_mats_cleared(self): MfMuiIDeriv_zero = self.simulation.MfMuiIDeriv(utils.Zero()) MeMuDeriv_zero = self.simulation.MeMuDeriv(utils.Zero()) - m1 = np.random.rand(self.mesh.nC) + rng = np.random.default_rng(seed=2016) + m1 = rng.uniform(size=self.mesh.nC) self.simulation.model = m1 self.assertTrue(getattr(self, "_MeMu", None) is None) @@ -168,7 +170,6 @@ def JvecTest( self.setUpProb(prbtype, sigmaInInversion, invertMui) print("Testing Jvec {}".format(prbtype)) - np.random.seed(3321) mod = self.m0 def fun(x): @@ -177,9 +178,11 @@ def fun(x): lambda x: self.simulation.Jvec(mod, x), ) - dx = np.random.rand(*mod.shape) * (mod.max() - mod.min()) * 0.01 + rng = np.random.default_rng(seed=3321) + dx = rng.uniform(size=mod.shape) * (mod.max() - mod.min()) * 0.01 - return tests.check_derivative(fun, mod, dx=dx, num=3, plotIt=False) + np.random.seed(1983) # set a random seed for check_derivative + return tests.check_derivative(fun, mod, dx=dx, num=4, plotIt=False) def JtvecTest( self, prbtype="ElectricField", sigmaInInversion=False, invertMui=False @@ -187,9 +190,9 @@ def JtvecTest( self.setUpProb(prbtype, sigmaInInversion, invertMui) print("Testing Jvec {}".format(prbtype)) - np.random.seed(31345) - u = np.random.rand(self.simulation.muMap.nP) - v = np.random.rand(self.survey.nD) + rng = np.random.default_rng(seed=3321) + u = rng.uniform(size=self.simulation.muMap.nP) + v = rng.uniform(size=self.survey.nD) self.simulation.model = self.m0 From efad8b0e174caac997c4eafd8f489e42a5bb8e5f Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 7 Jun 2024 15:26:55 -0700 Subject: [PATCH 023/194] Add `random_seed` argument to objective fun's derivative tests (#1448) Add a new `random_seed` argument to the `test()` method of objective functions to control their random state. Use Numpy's random number generator for managing the random state and the generation of random numbers. Minor improvements to the implementation of the test methods. Update tests that make use of these methods, and make them to use a seed in every case. Part of the solution to #1289 --- simpeg/objective_function.py | 49 +++++++++++++------ .../regularizations/test_cross_gradient.py | 32 +++++------- tests/base/regularizations/test_jtv.py | 8 +-- tests/base/test_correspondance.py | 4 +- tests/base/test_joint.py | 4 +- tests/base/test_objective_function.py | 13 ++--- 6 files changed, 61 insertions(+), 49 deletions(-) diff --git a/simpeg/objective_function.py b/simpeg/objective_function.py index 1e864d1b6e..e28bec0c90 100644 --- a/simpeg/objective_function.py +++ b/simpeg/objective_function.py @@ -9,6 +9,7 @@ from .maps import IdentityMap from .props import BaseSimPEG from .utils import timeIt, Zero, Identity +from .typing import RandomSeed __all__ = ["BaseObjectiveFunction", "ComboObjectiveFunction", "L2ObjectiveFunction"] @@ -194,27 +195,38 @@ def deriv2(self, m, v=None, **kwargs): ) ) - def _test_deriv(self, x=None, num=4, plotIt=False, **kwargs): + def _test_deriv( + self, + x=None, + num=4, + plotIt=False, + random_seed: RandomSeed | None = None, + **kwargs, + ): print("Testing {0!s} Deriv".format(self.__class__.__name__)) if x is None: - if self.nP == "*": - x = np.random.randn(np.random.randint(1e2, high=1e3)) - else: - x = np.random.randn(self.nP) - + rng = np.random.default_rng(seed=random_seed) + n_params = rng.integers(low=100, high=1_000) if self.nP == "*" else self.nP + x = rng.standard_normal(size=n_params) return check_derivative( lambda m: [self(m), self.deriv(m)], x, num=num, plotIt=plotIt, **kwargs ) - def _test_deriv2(self, x=None, num=4, plotIt=False, **kwargs): + def _test_deriv2( + self, + x=None, + num=4, + plotIt=False, + random_seed: RandomSeed | None = None, + **kwargs, + ): print("Testing {0!s} Deriv2".format(self.__class__.__name__)) + rng = np.random.default_rng(seed=random_seed) if x is None: - if self.nP == "*": - x = np.random.randn(np.random.randint(1e2, high=1e3)) - else: - x = np.random.randn(self.nP) + n_params = rng.integers(low=100, high=1_000) if self.nP == "*" else self.nP + x = rng.standard_normal(size=n_params) - v = x + 0.1 * np.random.rand(len(x)) + v = x + 0.1 * rng.uniform(size=len(x)) expectedOrder = kwargs.pop("expectedOrder", 1) return check_derivative( lambda m: [self.deriv(m).dot(v), self.deriv2(m, v=v)], @@ -225,7 +237,7 @@ def _test_deriv2(self, x=None, num=4, plotIt=False, **kwargs): **kwargs, ) - def test(self, x=None, num=4, **kwargs): + def test(self, x=None, num=4, random_seed: RandomSeed | None = None, **kwargs): """Run a convergence test on both the first and second derivatives. They should be second order! @@ -236,6 +248,11 @@ def test(self, x=None, num=4, **kwargs): The evaluation point for the Taylor expansion. num : int The number of iterations in the convergence test. + random_seed : :class:`~simpeg.typing.RandomSeed` or None, optional + Random seed used for generating a random array for ``x`` if it's + None, and the ``v`` array for testing the second derivatives. It + can either be an int, a predefined Numpy random number generator, + or any valid input to ``numpy.random.default_rng``. Returns ------- @@ -243,8 +260,10 @@ def test(self, x=None, num=4, **kwargs): ``True`` if both tests pass. ``False`` if either test fails. """ - deriv = self._test_deriv(x=x, num=num, **kwargs) - deriv2 = self._test_deriv2(x=x, num=num, plotIt=False, **kwargs) + deriv = self._test_deriv(x=x, num=num, random_seed=random_seed, **kwargs) + deriv2 = self._test_deriv2( + x=x, num=num, plotIt=False, random_seed=random_seed, **kwargs + ) return deriv & deriv2 __numpy_ufunc__ = True diff --git a/tests/base/regularizations/test_cross_gradient.py b/tests/base/regularizations/test_cross_gradient.py index 4b5741a7cb..65f21ea4b7 100644 --- a/tests/base/regularizations/test_cross_gradient.py +++ b/tests/base/regularizations/test_cross_gradient.py @@ -40,10 +40,9 @@ def test_order_approximate_hessian(self): Test deriv and deriv2 matrix of cross-gradient with approx_hessian=True """ - np.random.seed(10) cross_grad = self.cross_grad cross_grad.approx_hessian = True - self.assertTrue(cross_grad.test()) + self.assertTrue(cross_grad.test(random_seed=10)) def test_order_full_hessian(self): """ @@ -51,11 +50,10 @@ def test_order_full_hessian(self): Test deriv and deriv2 matrix of cross-gradient with approx_hessian=True """ - np.random.seed(10) cross_grad = self.cross_grad cross_grad.approx_hessian = False - self.assertTrue(cross_grad._test_deriv()) - self.assertTrue(cross_grad._test_deriv2(expectedOrder=2)) + self.assertTrue(cross_grad._test_deriv(random_seed=10)) + self.assertTrue(cross_grad._test_deriv2(random_seed=10, expectedOrder=2)) def test_deriv2_no_arg(self): np.random.seed(10) @@ -135,10 +133,9 @@ def test_order_approximate_hessian(self): Test deriv and deriv2 matrix of cross-gradient with approx_hessian=True """ - np.random.seed(10) cross_grad = self.cross_grad cross_grad.approx_hessian = True - self.assertTrue(cross_grad.test()) + self.assertTrue(cross_grad.test(random_seed=10)) def test_order_full_hessian(self): """ @@ -146,11 +143,10 @@ def test_order_full_hessian(self): Test deriv and deriv2 matrix of cross-gradient with approx_hessian=True """ - np.random.seed(10) cross_grad = self.cross_grad cross_grad.approx_hessian = False - self.assertTrue(cross_grad._test_deriv()) - self.assertTrue(cross_grad._test_deriv2(expectedOrder=2)) + self.assertTrue(cross_grad._test_deriv(random_seed=10)) + self.assertTrue(cross_grad._test_deriv2(random_seed=10, expectedOrder=2)) def test_deriv2_no_arg(self): np.random.seed(10) @@ -214,10 +210,9 @@ def test_order_approximate_hessian(self): Test deriv and deriv2 matrix of cross-gradient with approx_hessian=True """ - np.random.seed(10) cross_grad = self.cross_grad cross_grad.approx_hessian = True - self.assertTrue(cross_grad.test()) + self.assertTrue(cross_grad.test(random_seed=10)) def test_order_full_hessian(self): """ @@ -225,11 +220,10 @@ def test_order_full_hessian(self): Test deriv and deriv2 matrix of cross-gradient with approx_hessian=True """ - np.random.seed(10) cross_grad = self.cross_grad cross_grad.approx_hessian = False - self.assertTrue(cross_grad._test_deriv()) - self.assertTrue(cross_grad._test_deriv2(expectedOrder=2)) + self.assertTrue(cross_grad._test_deriv(random_seed=10)) + self.assertTrue(cross_grad._test_deriv2(random_seed=10, expectedOrder=2)) def test_deriv2_no_arg(self): np.random.seed(10) @@ -282,10 +276,9 @@ def test_order_approximate_hessian(self): Test deriv and deriv2 matrix of cross-gradient with approx_hessian=True """ - np.random.seed(10) cross_grad = self.cross_grad cross_grad.approx_hessian = True - self.assertTrue(cross_grad.test()) + self.assertTrue(cross_grad.test(random_seed=10)) def test_order_full_hessian(self): """ @@ -293,11 +286,10 @@ def test_order_full_hessian(self): Test deriv and deriv2 matrix of cross-gradient with approx_hessian=True """ - np.random.seed(10) cross_grad = self.cross_grad cross_grad.approx_hessian = False - self.assertTrue(cross_grad._test_deriv()) - self.assertTrue(cross_grad._test_deriv2(expectedOrder=2)) + self.assertTrue(cross_grad._test_deriv(random_seed=10)) + self.assertTrue(cross_grad._test_deriv2(random_seed=10, expectedOrder=2)) def test_deriv2_no_arg(self): np.random.seed(10) diff --git a/tests/base/regularizations/test_jtv.py b/tests/base/regularizations/test_jtv.py index 29239f36d7..e30d570c5e 100644 --- a/tests/base/regularizations/test_jtv.py +++ b/tests/base/regularizations/test_jtv.py @@ -46,7 +46,7 @@ def test_order_full_hessian(self): """ jtv = self.jtv self.assertTrue(jtv._test_deriv(self.x0)) - self.assertTrue(jtv._test_deriv2(self.x0, expectedOrder=2)) + self.assertTrue(jtv._test_deriv2(self.x0, random_seed=42, expectedOrder=2)) def test_deriv2_no_arg(self): m = self.x0 @@ -96,7 +96,7 @@ def test_order_full_hessian(self): """ jtv = self.jtv self.assertTrue(jtv._test_deriv(self.x0)) - self.assertTrue(jtv._test_deriv2(self.x0, expectedOrder=2)) + self.assertTrue(jtv._test_deriv2(self.x0, random_seed=42, expectedOrder=2)) def test_deriv2_no_arg(self): m = self.x0 @@ -143,7 +143,7 @@ def test_order_full_hessian(self): """ jtv = self.jtv self.assertTrue(jtv._test_deriv(self.x0)) - self.assertTrue(jtv._test_deriv2(self.x0, expectedOrder=2)) + self.assertTrue(jtv._test_deriv2(self.x0, random_seed=42, expectedOrder=2)) def test_deriv2_no_arg(self): m = self.x0 @@ -192,7 +192,7 @@ def test_order_full_hessian(self): """ jtv = self.jtv self.assertTrue(jtv._test_deriv(self.x0)) - self.assertTrue(jtv._test_deriv2(self.x0, expectedOrder=2)) + self.assertTrue(jtv._test_deriv2(self.x0, random_seed=42, expectedOrder=2)) def test_deriv2_no_arg(self): m = self.x0 diff --git a/tests/base/test_correspondance.py b/tests/base/test_correspondance.py index e4cd6cac3f..8e92fdb848 100644 --- a/tests/base/test_correspondance.py +++ b/tests/base/test_correspondance.py @@ -43,8 +43,8 @@ def test_order_full_hessian(self): """ corr = self.corr - self.assertTrue(corr._test_deriv()) - self.assertTrue(corr._test_deriv2(expectedOrder=2)) + self.assertTrue(corr._test_deriv(random_seed=10)) + self.assertTrue(corr._test_deriv2(random_seed=10, expectedOrder=2)) def test_deriv2_no_arg(self): m = np.random.randn(2 * len(self.mesh)) diff --git a/tests/base/test_joint.py b/tests/base/test_joint.py index f856239edc..3d269cb141 100644 --- a/tests/base/test_joint.py +++ b/tests/base/test_joint.py @@ -72,8 +72,8 @@ def setUp(self): self.dmiscombo = self.dmis0 + self.dmis1 def test_multiDataMisfit(self): - self.dmis0.test() - self.dmis1.test() + self.dmis0.test(random_seed=42) + self.dmis1.test(random_seed=42) self.dmiscombo.test(x=self.model) def test_inv(self): diff --git a/tests/base/test_objective_function.py b/tests/base/test_objective_function.py index 060dcf907c..ad4a3d22ac 100644 --- a/tests/base/test_objective_function.py +++ b/tests/base/test_objective_function.py @@ -57,7 +57,7 @@ def test_scalarmul(self): objfct_c = objfct_a + objfct_b self.assertTrue(scalar * objfct_a(m) == objfct_b(m)) - self.assertTrue(objfct_b.test()) + self.assertTrue(objfct_b.test(random_seed=42)) self.assertTrue(objfct_c(m) == objfct_a(m) + objfct_b(m)) self.assertTrue(len(objfct_c.objfcts) == 2) @@ -126,7 +126,7 @@ def test_3sum(self): self.assertTrue(len(phi.objfcts) == 3) - self.assertTrue(phi.test()) + self.assertTrue(phi.test(random_seed=42)) def test_sum_fail(self): nP1 = 10 @@ -166,7 +166,7 @@ def test_ZeroObjFct(self): + utils.Zero() * objective_function.L2ObjectiveFunction() ) self.assertTrue(len(phi.objfcts) == 1) - self.assertTrue(phi.test()) + self.assertTrue(phi.test(random_seed=42)) def test_updateMultipliers(self): nP = 10 @@ -257,9 +257,10 @@ def test_Maps(self): self.assertTrue(objfct3(m) == objfct1(m) + objfct2(m)) - objfct1.test() - objfct2.test() - objfct3.test() + seed = 42 + objfct1.test(random_seed=seed) + objfct2.test(random_seed=seed) + objfct3.test(random_seed=seed) def test_ComboW(self): nP = 15 From c31da2552ae5110cd6a7871f4d6bd2f9ac0bf2c4 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 10 Jun 2024 09:25:27 -0700 Subject: [PATCH 024/194] Add `random_seed` to the `test` method of maps (#1465) Add `random_seed` argument to `IdentityMap.test`. Add a new `random_seed` argument to the `test` method of `IdentityMap` to control the random state for creating the `m` array if it's not provided. Update tests to use the `random_seed`. If the `m` array is passed while running the `test()` method, specify the keyword for the argument to avoid relying only on its position. --- simpeg/maps.py | 12 ++++++++++-- tests/base/test_maps.py | 42 ++++++++++++++++++++--------------------- 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/simpeg/maps.py b/simpeg/maps.py index 8e9cfc7bc8..628cf15c89 100644 --- a/simpeg/maps.py +++ b/simpeg/maps.py @@ -1,3 +1,5 @@ +from __future__ import annotations # needed to use type operands in Python 3.8 + from collections import namedtuple import warnings import discretize @@ -32,6 +34,7 @@ validate_active_indices, validate_list_of_types, ) +from .typing import RandomSeed class IdentityMap: @@ -189,7 +192,7 @@ def deriv(self, m, v=None): return sp.identity(self.nP) return Identity() - def test(self, m=None, num=4, **kwargs): + def test(self, m=None, num=4, random_seed: RandomSeed | None = None, **kwargs): """Derivative test for the mapping. This test validates the mapping by performing a convergence test. @@ -200,6 +203,10 @@ def test(self, m=None, num=4, **kwargs): Starting vector of model parameters for the derivative test num : int Number of iterations for the derivative test + random_seed : None or :class:`~simpeg.typing.RandomSeed`, optional + Random seed used for generating a random array for ``m`` if it's + None. It can either be an int, a predefined Numpy random number + generator, or any valid input to ``numpy.random.default_rng``. kwargs: dict Keyword arguments and associated values in the dictionary must match those used in :meth:`discretize.tests.check_derivative` @@ -211,7 +218,8 @@ def test(self, m=None, num=4, **kwargs): """ print("Testing {0!s}".format(str(self))) if m is None: - m = abs(np.random.rand(self.nP)) + rng = np.random.default_rng(seed=random_seed) + m = rng.uniform(size=self.nP) if "plotIt" not in kwargs: kwargs["plotIt"] = False diff --git a/tests/base/test_maps.py b/tests/base/test_maps.py index 4957f8db28..f5a815a819 100644 --- a/tests/base/test_maps.py +++ b/tests/base/test_maps.py @@ -129,19 +129,19 @@ def setUp(self): def test_transforms2D(self): for M in self.maps2test2D: - self.assertTrue(M(self.mesh2).test()) + self.assertTrue(M(self.mesh2).test(random_seed=42)) def test_transforms2Dvec(self): for M in self.maps2test2D: - self.assertTrue(M(self.mesh2).test()) + self.assertTrue(M(self.mesh2).test(random_seed=42)) def test_transforms3D(self): for M in self.maps2test3D: - self.assertTrue(M(self.mesh3).test()) + self.assertTrue(M(self.mesh3).test(random_seed=42)) def test_transforms3Dvec(self): for M in self.maps2test3D: - self.assertTrue(M(self.mesh3).test()) + self.assertTrue(M(self.mesh3).test(random_seed=42)) def test_invtransforms2D(self): for M in self.maps2test2D: @@ -184,14 +184,14 @@ def test_invtransforms3D(self): def test_ParametricCasingAndLayer(self): mapping = maps.ParametricCasingAndLayer(self.meshCyl) m = np.r_[-2.0, 1.0, 6.0, 2.0, -0.1, 0.2, 0.5, 0.2, -0.2, 0.2] - self.assertTrue(mapping.test(m)) + self.assertTrue(mapping.test(m=m)) def test_ParametricBlock2D(self): mesh = discretize.TensorMesh([np.ones(30), np.ones(20)], x0=np.array([-15, -5])) mapping = maps.ParametricBlock(mesh) # val_background,val_block, block_x0, block_dx, block_y0, block_dy m = np.r_[-2.0, 1.0, -5, 10, 5, 4] - self.assertTrue(mapping.test(m)) + self.assertTrue(mapping.test(m=m)) def test_transforms_logMap_reciprocalMap(self): # Note that log/reciprocal maps can be kinda finicky, so we are being @@ -233,22 +233,22 @@ def test_transforms_logMap_reciprocalMap(self): ] mapping = maps.LogMap(self.mesh2) - self.assertTrue(mapping.test(v2, dx=dv2)) + self.assertTrue(mapping.test(m=v2, dx=dv2)) mapping = maps.LogMap(self.mesh3) - self.assertTrue(mapping.test(v3, dx=dv3)) + self.assertTrue(mapping.test(m=v3, dx=dv3)) mapping = maps.ReciprocalMap(self.mesh2) - self.assertTrue(mapping.test(v2, dx=dv2)) + self.assertTrue(mapping.test(m=v2, dx=dv2)) mapping = maps.ReciprocalMap(self.mesh3) - self.assertTrue(mapping.test(v3, dx=dv3)) + self.assertTrue(mapping.test(m=v3, dx=dv3)) def test_Mesh2MeshMap(self): mapping = maps.Mesh2Mesh([self.mesh22, self.mesh2]) - self.assertTrue(mapping.test()) + self.assertTrue(mapping.test(random_seed=42)) def test_Mesh2MeshMapVec(self): mapping = maps.Mesh2Mesh([self.mesh22, self.mesh2]) - self.assertTrue(mapping.test()) + self.assertTrue(mapping.test(random_seed=42)) def test_mapMultiplication(self): M = discretize.TensorMesh([2, 3]) @@ -335,7 +335,7 @@ def test_map2Dto3D_x(self): ]: # m2to3 = maps.Surject2Dto3D(M3, normal='X') m = np.arange(m2to3.nP) - self.assertTrue(m2to3.test()) + self.assertTrue(m2to3.test(random_seed=42)) self.assertTrue( np.all(utils.mkvc((m2to3 * m).reshape(M3.vnC, order="F")[0, :, :]) == m) ) @@ -350,7 +350,7 @@ def test_map2Dto3D_y(self): ]: # m2to3 = maps.Surject2Dto3D(M3, normal='Y') m = np.arange(m2to3.nP) - self.assertTrue(m2to3.test()) + self.assertTrue(m2to3.test(random_seed=42)) self.assertTrue( np.all(utils.mkvc((m2to3 * m).reshape(M3.vnC, order="F")[:, 0, :]) == m) ) @@ -365,7 +365,7 @@ def test_map2Dto3D_z(self): ]: # m2to3 = maps.Surject2Dto3D(M3, normal='Z') m = np.arange(m2to3.nP) - self.assertTrue(m2to3.test()) + self.assertTrue(m2to3.test(random_seed=42)) self.assertTrue( np.all(utils.mkvc((m2to3 * m).reshape(M3.vnC, order="F")[:, :, 0]) == m) ) @@ -379,7 +379,7 @@ def test_ParametricSplineMap(self): M2 = discretize.TensorMesh([np.ones(10), np.ones(10)], "CN") x = M2.cell_centers_x mParamSpline = maps.ParametricSplineMap(M2, x, normal="Y", order=1) - self.assertTrue(mParamSpline.test()) + self.assertTrue(mParamSpline.test(random_seed=42)) def test_parametric_block(self): M1 = discretize.TensorMesh([np.ones(10)], "C") @@ -471,8 +471,8 @@ def test_sum(self): self.assertTrue(np.all(summap0 * m0 == summap1 * m0)) - self.assertTrue(summap0.test(m0)) - self.assertTrue(summap1.test(m0)) + self.assertTrue(summap0.test(m=m0)) + self.assertTrue(summap1.test(m=m0)) def test_surject_units(self): M2 = discretize.TensorMesh([np.ones(10), np.ones(20)], "CC") @@ -486,7 +486,7 @@ def test_surject_units(self): self.assertTrue(np.all(m1[unit1] == 0)) self.assertTrue(np.all(m1[unit2] == 1)) - self.assertTrue(surject_units.test(m0)) + self.assertTrue(surject_units.test(m=m0)) def test_Projection(self): nP = 10 @@ -508,7 +508,7 @@ def test_Projection(self): maps.Projection(nP, np.r_[10]) * m mapping = maps.Projection(nP, np.r_[1, 2, 6, 1, 3, 5, 4, 9, 9, 8, 0]) - mapping.test() + mapping.test(random_seed=42) def test_Tile(self): """ @@ -690,7 +690,7 @@ def test_LinearMapDerivs(A, b): y1 = mapping.deriv(m) @ v y2 = mapping.deriv(m, v=v) np.testing.assert_equal(y1, y2) - mapping.test() + mapping.test(random_seed=42) def test_LinearMap_errors(): From 91d1f570653e449439c0653cf885989bb62cc3f6 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 10 Jun 2024 12:52:12 -0700 Subject: [PATCH 025/194] Use Numpy rng in TDEM tests (#1452) Replace the usage of the deprecated functions in `numpy.random` module for the Numpy's random number generator class and its methods, in most of the TDEM tests. Part of the solution to #1289 --- tests/em/tdem/test_TDEM_DerivAdjoint.py | 23 +++++++++++-------- .../test_TDEM_DerivAdjoint_RawWaveform.py | 12 +++++----- .../tdem/test_TDEM_DerivAdjoint_galvanic.py | 11 +++++---- tests/em/tdem/test_TDEM_crosscheck.py | 6 +++-- tests/em/tdem/test_TDEM_grounded.py | 19 +++++++++------ .../em/tdem/test_TDEM_inductive_permeable.py | 10 ++++---- tests/em/tdem/test_TDEM_sources.py | 11 +++++++++ 7 files changed, 59 insertions(+), 33 deletions(-) diff --git a/tests/em/tdem/test_TDEM_DerivAdjoint.py b/tests/em/tdem/test_TDEM_DerivAdjoint.py index 49d0b4476d..9dc8f53cf2 100644 --- a/tests/em/tdem/test_TDEM_DerivAdjoint.py +++ b/tests/em/tdem/test_TDEM_DerivAdjoint.py @@ -67,8 +67,9 @@ def setUpClass(self): mapping = get_mapping(mesh) self.survey = get_survey() self.prob = get_prob(mesh, mapping, self.formulation, survey=self.survey) - self.m = np.log(1e-1) * np.ones(self.prob.sigmaMap.nP) + 1e-3 * np.random.randn( - self.prob.sigmaMap.nP + rng = np.random.default_rng(seed=42) + self.m = np.log(1e-1) * np.ones(self.prob.sigmaMap.nP) + 1e-3 * rng.normal( + size=self.prob.sigmaMap.nP ) print("Solving Fields for problem {}".format(self.formulation)) t = time.time() @@ -103,7 +104,6 @@ def set_receiver_list(self, rxcomp): src.receiver_list = rxlist def JvecTest(self, rxcomp): - np.random.seed(10) self.set_receiver_list(rxcomp) def derChk(m): @@ -117,17 +117,18 @@ def derChk(m): prbtype=self.formulation, rxcomp=rxcomp ) ) + np.random.seed(10) # use seed for check_derivative tests.check_derivative(derChk, self.m, plotIt=False, num=2, eps=1e-20) def JvecVsJtvecTest(self, rxcomp): - np.random.seed(10) self.set_receiver_list(rxcomp) print( "\nAdjoint Testing Jvec, Jtvec prob {}, {}".format(self.formulation, rxcomp) ) - m = np.random.rand(self.prob.sigmaMap.nP) - d = np.random.randn(self.prob.survey.nD) + rng = np.random.default_rng(seed=42) + m = rng.uniform(size=self.prob.sigmaMap.nP) + d = rng.normal(size=self.prob.survey.nD) V1 = d.dot(self.prob.Jvec(self.m, m, f=self.fields)) V2 = m.dot(self.prob.Jtvec(self.m, d, f=self.fields)) tol = TOL * (np.abs(V1) + np.abs(V2)) / 2.0 @@ -146,8 +147,9 @@ def test_eDeriv_m_adjoint(self): print("\n Testing eDeriv_m Adjoint") - m = np.random.rand(len(self.m)) - e = np.random.randn(prb.mesh.nE) + rng = np.random.default_rng(seed=42) + m = rng.uniform(size=len(self.m)) + e = rng.normal(size=prb.mesh.nE) V1 = e.dot(f._eDeriv_m(1, prb.survey.source_list[0], m)) V2 = m.dot(f._eDeriv_m(1, prb.survey.source_list[0], e, adjoint=True)) tol = TOL * (np.abs(V1) + np.abs(V2)) / 2.0 @@ -162,8 +164,9 @@ def test_eDeriv_u_adjoint(self): prb = self.prob f = self.fields - b = np.random.rand(prb.mesh.nF) - e = np.random.randn(prb.mesh.nE) + rng = np.random.default_rng(seed=42) + b = rng.uniform(size=prb.mesh.nF) + e = rng.normal(size=prb.mesh.nE) V1 = e.dot(f._eDeriv_u(1, prb.survey.source_list[0], b)) V2 = b.dot(f._eDeriv_u(1, prb.survey.source_list[0], e, adjoint=True)) tol = TOL * (np.abs(V1) + np.abs(V2)) / 2.0 diff --git a/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py b/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py index e650dde269..3fe03cf1be 100644 --- a/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py +++ b/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py @@ -72,14 +72,14 @@ def setUpClass(self): time_steps = [(1e-3, 5), (1e-4, 5), (5e-5, 10), (5e-5, 10), (1e-4, 10)] t_mesh = discretize.TensorMesh([time_steps]) times = t_mesh.nodes_x - np.random.rand(412) + rng = np.random.default_rng(seed=42) self.survey = get_survey(times, self.t0) self.prob = get_prob( mesh, mapping, self.formulation, survey=self.survey, time_steps=time_steps ) self.m = np.log(1e-1) * np.ones(self.prob.sigmaMap.nP) - self.m *= 0.25 * np.random.rand(*self.m.shape) + 1 + self.m *= 0.25 * rng.uniform(size=self.m.shape) + 1 print("Solving Fields for problem {}".format(self.formulation)) t = time.time() @@ -120,7 +120,6 @@ def set_receiver_list(self, rxcomp): src.receiver_list = rxlist def JvecTest(self, rxcomp): - np.random.seed(4) self.set_receiver_list(rxcomp) def derChk(m): @@ -134,17 +133,18 @@ def derChk(m): prbtype=self.formulation, rxcomp=rxcomp ) ) + np.random.seed(4) # set seed for check_derivative tests.check_derivative(derChk, self.m, plotIt=False, num=3, eps=1e-20) def JvecVsJtvecTest(self, rxcomp): - np.random.seed(4) self.set_receiver_list(rxcomp) print( "\nAdjoint Testing Jvec, Jtvec prob {}, {}".format(self.formulation, rxcomp) ) - m = np.random.rand(self.prob.sigmaMap.nP) - d = np.random.randn(self.prob.survey.nD) + rng = np.random.default_rng(seed=4) + m = rng.uniform(size=self.prob.sigmaMap.nP) + d = rng.normal(size=self.prob.survey.nD) V1 = d.dot(self.prob.Jvec(self.m, m, f=self.fields)) V2 = m.dot(self.prob.Jtvec(self.m, d, f=self.fields)) tol = TOL * (np.abs(V1) + np.abs(V2)) / 2.0 diff --git a/tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py b/tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py index fb764d924b..7255e54d37 100644 --- a/tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py +++ b/tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py @@ -14,7 +14,6 @@ def setUp_TDEM(prbtype="ElectricField", rxcomp="ElectricFieldx", src_z=0.0): - np.random.seed(10) cs = 5.0 ncx = 8 ncy = 8 @@ -55,7 +54,8 @@ def setUp_TDEM(prbtype="ElectricField", rxcomp="ElectricFieldx", src_z=0.0): time_steps = [(1e-05, 10), (5e-05, 10), (2.5e-4, 10)] - m = np.log(5e-1) * np.ones(mapping.nP) + 1e-3 * np.random.randn(mapping.nP) + rng = np.random.default_rng(seed=42) + m = np.log(5e-1) * np.ones(mapping.nP) + 1e-3 * rng.normal(size=mapping.nP) prb = getattr(tdem, "Simulation3D{}".format(prbtype))( mesh, survey=survey, time_steps=time_steps, sigmaMap=mapping @@ -77,6 +77,8 @@ def derChk(m): return [prb.dpred(m), lambda mx: prb.Jvec(m, mx)] print("test_Jvec_{prbtype}_{rxcomp}".format(prbtype=prbtype, rxcomp=rxcomp)) + + np.random.seed(10) # use seed for check_derivative tests.check_derivative(derChk, m, plotIt=False, num=2, eps=1e-20) def test_Jvec_e_dbzdt(self): @@ -107,8 +109,9 @@ def JvecVsJtvecTest( print("\nAdjoint Testing Jvec, Jtvec prob {}, {}".format(prbtype, rxcomp)) prb, m0, mesh = setUp_TDEM(prbtype, rxcomp, src_z) - m = np.random.rand(prb.sigmaMap.nP) - d = np.random.randn(prb.survey.nD) + rng = np.random.default_rng(seed=42) + m = rng.uniform(size=prb.sigmaMap.nP) + d = rng.normal(size=prb.survey.nD) print(m.shape, d.shape, m0.shape) diff --git a/tests/em/tdem/test_TDEM_crosscheck.py b/tests/em/tdem/test_TDEM_crosscheck.py index ec0742b066..bb029d2085 100644 --- a/tests/em/tdem/test_TDEM_crosscheck.py +++ b/tests/em/tdem/test_TDEM_crosscheck.py @@ -16,7 +16,6 @@ def setUp_TDEM( prbtype="MagneticFluxDensity", rxcomp="bz", waveform="stepoff", src_type=None ): # set a seed so that the same conductivity model is used for all runs - np.random.seed(25) cs = 10.0 ncx = 4 ncy = 4 @@ -76,7 +75,10 @@ def setUp_TDEM( ) prb.solver = Solver - m = np.log(1e-1) * np.ones(prb.sigmaMap.nP) + 1e-2 * np.random.rand(prb.sigmaMap.nP) + rng = np.random.default_rng(seed=42) + m = np.log(1e-1) * np.ones(prb.sigmaMap.nP) + 1e-2 * rng.uniform( + size=prb.sigmaMap.nP + ) return prb, m, mesh diff --git a/tests/em/tdem/test_TDEM_grounded.py b/tests/em/tdem/test_TDEM_grounded.py index c85a8807cc..4f9587234b 100644 --- a/tests/em/tdem/test_TDEM_grounded.py +++ b/tests/em/tdem/test_TDEM_grounded.py @@ -93,9 +93,11 @@ def setUpClass(self): print("Testing problem {} \n\n".format(self.prob_type)) def derivtest(self, deriv_fct): - m0 = np.log(self.sigma) + np.random.rand(self.mesh.nC) + rng = np.random.default_rng(seed=42) + m0 = np.log(self.sigma) + rng.uniform(size=self.mesh.nC) self.prob.model = m0 + np.random.seed(10) # use seed for check_derivative return tests.check_derivative( deriv_fct, np.log(self.sigma), num=3, plotIt=False ) @@ -131,22 +133,25 @@ def deriv_check(m): self.derivtest(deriv_check) def test_adjoint_phi(self): - v = np.random.rand(self.mesh.nC) - w = np.random.rand(self.mesh.nC) + rng = np.random.default_rng(seed=42) + v = rng.uniform(size=self.mesh.nC) + w = rng.uniform(size=self.mesh.nC) a = w.T.dot(self.src._phiInitialDeriv(self.prob, v=v)) b = v.T.dot(self.src._phiInitialDeriv(self.prob, v=w, adjoint=True)) self.assertTrue(np.allclose(a, b)) def test_adjoint_j(self): - v = np.random.rand(self.mesh.nC) - w = np.random.rand(self.mesh.nF) + rng = np.random.default_rng(seed=42) + v = rng.uniform(size=self.mesh.nC) + w = rng.uniform(size=self.mesh.nF) a = w.T.dot(self.src.jInitialDeriv(self.prob, v=v)) b = v.T.dot(self.src.jInitialDeriv(self.prob, v=w, adjoint=True)) self.assertTrue(np.allclose(a, b)) def test_adjoint_h(self): - v = np.random.rand(self.mesh.nC) - w = np.random.rand(self.mesh.nE) + rng = np.random.default_rng(seed=42) + v = rng.uniform(size=self.mesh.nC) + w = rng.uniform(size=self.mesh.nE) a = w.T.dot(self.src.hInitialDeriv(self.prob, v=v)) b = v.T.dot(self.src.hInitialDeriv(self.prob, v=w, adjoint=True)) self.assertTrue(np.allclose(a, b)) diff --git a/tests/em/tdem/test_TDEM_inductive_permeable.py b/tests/em/tdem/test_TDEM_inductive_permeable.py index 8bf243554d..5e0a2cd5f9 100644 --- a/tests/em/tdem/test_TDEM_inductive_permeable.py +++ b/tests/em/tdem/test_TDEM_inductive_permeable.py @@ -232,8 +232,9 @@ def populate_target(mur): assert all(passed) prob.sigma = 1e-4 * np.ones(mesh.nC) - v = utils.mkvc(np.random.rand(mesh.nE)) - w = utils.mkvc(np.random.rand(mesh.nF)) + rng = np.random.default_rng(seed=42) + v = utils.mkvc(rng.uniform(size=mesh.nE)) + w = utils.mkvc(rng.uniform(size=mesh.nF)) assert np.all( mesh.get_edge_inner_product(1e-4 * np.ones(mesh.nC)) * v == prob.MeSigma * v ) @@ -256,8 +257,9 @@ def populate_target(mur): ) prob.rho = 1.0 / 1e-3 * np.ones(mesh.nC) - v = utils.mkvc(np.random.rand(mesh.nE)) - w = utils.mkvc(np.random.rand(mesh.nF)) + rng = np.random.default_rng(seed=42) + v = utils.mkvc(rng.uniform(size=mesh.nE)) + w = utils.mkvc(rng.uniform(size=mesh.nF)) np.testing.assert_allclose( mesh.get_edge_inner_product(1e-3 * np.ones(mesh.nC)) * v, prob.MeSigma * v diff --git a/tests/em/tdem/test_TDEM_sources.py b/tests/em/tdem/test_TDEM_sources.py index 3d4fff3896..d038766716 100644 --- a/tests/em/tdem/test_TDEM_sources.py +++ b/tests/em/tdem/test_TDEM_sources.py @@ -73,6 +73,7 @@ def f(t): ) dt = np.min(np.diff(t0)) * 0.5 * np.ones_like(t0) + np.random.seed(10) # use seed for check_derivative assert check_derivative(f, t0, dx=dt, plotIt=False) @@ -115,6 +116,7 @@ def f(t): ) dt = np.min(np.diff(t0)) * 0.5 * np.ones_like(t0) + np.random.seed(10) # use seed for check_derivative assert check_derivative(f, t0, dx=dt, plotIt=False) @@ -157,6 +159,7 @@ def f(t): ) dt = np.min(np.diff(t0)) * 0.5 * np.ones_like(t0) + np.random.seed(10) # use seed for check_derivative assert check_derivative(f, t0, dx=dt, plotIt=False) @@ -195,6 +198,7 @@ def f(t): ) dt = np.min(np.diff(t0)) * 0.5 * np.ones_like(t0) + np.random.seed(10) # use seed for check_derivative assert check_derivative(f, t0, dx=dt, plotIt=False) @@ -268,6 +272,7 @@ def f(t): ) dt = np.min(np.diff(t0)) * 0.5 * np.ones_like(t0) + np.random.seed(10) # use seed for check_derivative assert check_derivative(f, t0, dx=dt, plotIt=False) def test_waveform_without_plateau_derivative(self): @@ -290,6 +295,7 @@ def f(t): ) dt = np.min(np.diff(t0)) * 0.5 * np.ones_like(t0) + np.random.seed(10) # use seed for check_derivative assert check_derivative(f, t0, dx=dt, plotIt=False) def test_waveform_negative_plateau_derivative(self): @@ -312,6 +318,7 @@ def f(t): ) dt = np.min(np.diff(t0)) * 0.5 * np.ones_like(t0) + np.random.seed(10) # use seed for check_derivative assert check_derivative(f, t0, dx=dt, plotIt=False) @@ -372,6 +379,7 @@ def f(t): ) dt = np.min(np.diff(t0)) * 0.5 * np.ones_like(t0) + np.random.seed(10) # use seed for check_derivative assert check_derivative(f, t0, dx=dt, plotIt=False) def test_waveform_without_plateau_derivative(self): @@ -392,6 +400,7 @@ def f(t): ) dt = np.min(np.diff(t0)) * 0.5 * np.ones_like(t0) + np.random.seed(10) # use seed for check_derivative assert check_derivative(f, t0, dx=dt, plotIt=False) @@ -430,6 +439,7 @@ def f(t): ) dt = np.min(np.diff(t0)) * 0.5 * np.ones_like(t0) + np.random.seed(10) # use seed for check_derivative assert check_derivative(f, t0, dx=dt, plotIt=False) @@ -520,6 +530,7 @@ def f(t): ) dt = np.min(np.diff(t0)) * 0.5 * np.ones_like(t0) + np.random.seed(10) # use seed for check_derivative assert check_derivative(f, t0, dx=dt, plotIt=False) From 5575c5e7504cf652b38754d3589f13743f650802 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 10 Jun 2024 15:47:29 -0700 Subject: [PATCH 026/194] Use random seed in missed objective function tests (#1483) Make use of the `random_seed` arguments in a few objective function tests that were missed in previous PR. Related to #1448. --- tests/base/test_objective_function.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/base/test_objective_function.py b/tests/base/test_objective_function.py index ad4a3d22ac..24650f64b4 100644 --- a/tests/base/test_objective_function.py +++ b/tests/base/test_objective_function.py @@ -35,7 +35,7 @@ def deriv2(self, m, v=None): class TestBaseObjFct(unittest.TestCase): def test_derivs(self): objfct = objective_function.L2ObjectiveFunction() - self.assertTrue(objfct.test(eps=1e-9)) + self.assertTrue(objfct.test(eps=1e-9, random_seed=42)) def test_deriv2(self): nP = 100 @@ -70,7 +70,7 @@ def test_sum(self): objfct = objective_function.L2ObjectiveFunction( W=sp.eye(nP) ) + scalar * objective_function.L2ObjectiveFunction(W=sp.eye(nP)) - self.assertTrue(objfct.test(eps=1e-9)) + self.assertTrue(objfct.test(eps=1e-9, random_seed=42)) self.assertTrue(np.all(objfct.multipliers == np.r_[1.0, scalar])) @@ -84,7 +84,7 @@ def test_2sum(self): + alpha1 * objective_function.L2ObjectiveFunction() ) phi2 = objective_function.L2ObjectiveFunction() + alpha2 * phi1 - self.assertTrue(phi2.test(eps=EPS)) + self.assertTrue(phi2.test(eps=EPS, random_seed=42)) self.assertTrue(len(phi1.multipliers) == 2) self.assertTrue(len(phi2.multipliers) == 2) From 00d3371f872fe43239da39484fd03bde8a5b32e2 Mon Sep 17 00:00:00 2001 From: William Davis <38541020+williamjsdavis@users.noreply.github.com> Date: Fri, 14 Jun 2024 11:03:42 -0700 Subject: [PATCH 027/194] Fix contour colors in gravity plot in User Guide (#1486) Correcting contour colors in a gravity plot in the User Guide for a gravity plot: correctly use the color limits both in the contours and while building the custom color bar. --- tutorials/03-gravity/plot_1a_gravity_anomaly.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tutorials/03-gravity/plot_1a_gravity_anomaly.py b/tutorials/03-gravity/plot_1a_gravity_anomaly.py index d2d8aacfd6..15fb56c0c3 100644 --- a/tutorials/03-gravity/plot_1a_gravity_anomaly.py +++ b/tutorials/03-gravity/plot_1a_gravity_anomaly.py @@ -214,14 +214,22 @@ # Plot fig = plt.figure(figsize=(7, 5)) +v_max = np.max(np.abs(dpred)) + ax1 = fig.add_axes([0.1, 0.1, 0.75, 0.85]) -plot2Ddata(receiver_list[0].locations, dpred, ax=ax1, contourOpts={"cmap": "bwr"}) +plot2Ddata( + receiver_list[0].locations, + dpred, + clim=(-v_max, v_max), + ax=ax1, + contourOpts={"cmap": "bwr"}, +) ax1.set_title("Gravity Anomaly (Z-component)") ax1.set_xlabel("x (m)") ax1.set_ylabel("y (m)") ax2 = fig.add_axes([0.82, 0.1, 0.03, 0.85]) -norm = mpl.colors.Normalize(vmin=-np.max(np.abs(dpred)), vmax=np.max(np.abs(dpred))) +norm = mpl.colors.Normalize(vmin=-v_max, vmax=v_max) cbar = mpl.colorbar.ColorbarBase( ax2, norm=norm, orientation="vertical", cmap=mpl.cm.bwr, format="%.1e" ) From 078256750bfac6cd9a325599854a2838dad466e6 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 14 Jun 2024 15:05:40 -0700 Subject: [PATCH 028/194] Hide type hints from signatures in documentation pages (#1471) Configure Sphinx autodoc to hide type hints from signatures of functions, methods and classes to improve readability. Expected types for each parameter should be listed in the docstring. --- docs/conf.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 38bdeef6b2..a5456fd45b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -505,6 +505,8 @@ def linkcode_resolve(domain, info): autodoc_member_order = "bysource" +# Don't show type hints in signatures +autodoc_typehints = "none" # def supress_nonlocal_image_warn(): # import sphinx.environment From 77edcc826895761c93111fea814935f68f053819 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 14 Jun 2024 17:09:20 -0700 Subject: [PATCH 029/194] Reorganize the maps submodule (#1480) Remove the `simpeg/maps.py` file and create a new `simpeg/maps` directory. Add private files for each category of map classes inside the new submodule. Make these classes public through the `simpeg/maps/__init__.py`. --- simpeg/maps.py | 6418 --------------------------------- simpeg/maps/__init__.py | 35 + simpeg/maps/_base.py | 1342 +++++++ simpeg/maps/_clustering.py | 152 + simpeg/maps/_injection.py | 284 ++ simpeg/maps/_parametric.py | 2634 ++++++++++++++ simpeg/maps/_property_maps.py | 1474 ++++++++ simpeg/maps/_surjection.py | 568 +++ 8 files changed, 6489 insertions(+), 6418 deletions(-) delete mode 100644 simpeg/maps.py create mode 100644 simpeg/maps/__init__.py create mode 100644 simpeg/maps/_base.py create mode 100644 simpeg/maps/_clustering.py create mode 100644 simpeg/maps/_injection.py create mode 100644 simpeg/maps/_parametric.py create mode 100644 simpeg/maps/_property_maps.py create mode 100644 simpeg/maps/_surjection.py diff --git a/simpeg/maps.py b/simpeg/maps.py deleted file mode 100644 index 628cf15c89..0000000000 --- a/simpeg/maps.py +++ /dev/null @@ -1,6418 +0,0 @@ -from __future__ import annotations # needed to use type operands in Python 3.8 - -from collections import namedtuple -import warnings -import discretize -import numpy as np -from numpy.polynomial import polynomial -import scipy.sparse as sp -from scipy.sparse.linalg import LinearOperator -from scipy.interpolate import UnivariateSpline -from scipy.constants import mu_0 -from scipy.sparse import csr_matrix as csr -from scipy.special import expit, logit - -from discretize.tests import check_derivative -from discretize import TensorMesh, CylindricalMesh -from discretize.utils import ( - mkvc, - rotation_matrix_from_normals, - Zero, - Identity, - sdiag, - speye, -) - -from .utils import ( - mat_utils, - validate_type, - validate_ndarray_with_shape, - validate_float, - validate_direction, - validate_integer, - validate_string, - validate_active_indices, - validate_list_of_types, -) -from .typing import RandomSeed - - -class IdentityMap: - r"""Identity mapping and the base mapping class for all other SimPEG mappings. - - The ``IdentityMap`` class is used to define the mapping when - the model parameters are the same as the parameters used in the forward - simulation. For a discrete set of model parameters :math:`\mathbf{m}`, - the mapping :math:`\mathbf{u}(\mathbf{m})` is equivalent to applying - the identity matrix; i.e.: - - .. math:: - - \mathbf{u}(\mathbf{m}) = \mathbf{Im} - - The ``IdentityMap`` also acts as the base class for all other SimPEG mapping classes. - - Using the *mesh* or *nP* input arguments, the dimensions of the corresponding - mapping operator can be permanently set; i.e. (*mesh.nC*, *mesh.nC*) or (*nP*, *nP*). - However if both input arguments *mesh* and *nP* are ``None``, the shape of - mapping operator is arbitrary and can act on any vector; i.e. has shape (``*``, ``*``). - - Parameters - ---------- - mesh : discretize.BaseMesh - The number of parameters accepted by the mapping is set to equal the number - of mesh cells. - nP : int, or '*' - Set the number of parameters accepted by the mapping directly. Used if the - number of parameters is known. Used generally when the number of parameters - is not equal to the number of cells in a mesh. - """ - - def __init__(self, mesh=None, nP=None, **kwargs): - if (isinstance(nP, str) and nP == "*") or nP is None: - if mesh is not None: - nP = mesh.n_cells - else: - nP = "*" - else: - try: - nP = int(nP) - except (TypeError, ValueError) as err: - raise TypeError( - f"Unrecognized input of {repr(nP)} for number of parameters, must be an integer or '*'." - ) from err - self.mesh = mesh - self._nP = nP - - super().__init__(**kwargs) - - @property - def nP(self): - r"""Number of parameters the mapping acts on. - - Returns - ------- - int or ``*`` - Number of parameters that the mapping acts on. Returns an - ``int`` if the dimensions of the mapping are set. If the - mapping can act on a vector of any length, ``*`` is returned. - """ - if self._nP != "*": - return int(self._nP) - if self.mesh is None: - return "*" - return int(self.mesh.nC) - - @property - def shape(self): - r"""Dimensions of the mapping operator - - The dimensions of the mesh depend on the input arguments used - during instantiation. If *mesh* is used to define the - identity map, the shape of mapping operator is (*mesh.nC*, *mesh.nC*). - If *nP* is used to define the identity map, the mapping operator - has dimensions (*nP*, *nP*). However if both *mesh* and *nP* are - used to define the identity map, the mapping will have shape - (*mesh.nC*, *nP*)! And if *mesh* and *nP* were ``None`` when - instantiating, the mapping has dimensions (``*``, ``*``) and may - act on a vector of any length. - - Returns - ------- - tuple - Dimensions of the mapping operator. If the dimensions of - the mapping are set, the return is a tuple (``int``,``int``). - If the mapping can act on a vector of arbitrary length, the - return is a tuple (``*``, ``*``). - """ - if self.mesh is None: - return (self.nP, self.nP) - return (self.mesh.nC, self.nP) - - def _transform(self, m): - """ - Changes the model into the physical property. - - .. note:: - - This can be called by the __mul__ property against a - :meth:numpy.ndarray. - - :param numpy.ndarray m: model - :rtype: numpy.ndarray - :return: transformed model - - """ - return m - - def inverse(self, D): - """ - The transform inverse is not implemented. - """ - raise NotImplementedError("The transform inverse is not implemented.") - - def deriv(self, m, v=None): - r"""Derivative of the mapping with respect to the input parameters. - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix or numpy.ndarray - Derivative of the mapping with respect to the model parameters. For an - identity mapping, this is just a sparse identity matrix. If the input - argument *v* is not ``None``, the method returns the derivative times - the vector *v*; which in this case is just *v*. - - Notes - ----- - Let :math:`\mathbf{m}` be a set of model parameters and let :math:`\mathbf{I}` - denote the identity map. Where the identity mapping acting on the model parameters - can be expressed as: - - .. math:: - \mathbf{u} = \mathbf{I m}, - - the **deriv** method returns the derivative of :math:`\mathbf{u}` with respect - to the model parameters; i.e.: - - .. math:: - \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = \mathbf{I} - - For the Identity map **deriv** simply returns a sparse identity matrix. - """ - if v is not None: - return v - if isinstance(self.nP, (int, np.integer)): - return sp.identity(self.nP) - return Identity() - - def test(self, m=None, num=4, random_seed: RandomSeed | None = None, **kwargs): - """Derivative test for the mapping. - - This test validates the mapping by performing a convergence test. - - Parameters - ---------- - m : (nP) numpy.ndarray - Starting vector of model parameters for the derivative test - num : int - Number of iterations for the derivative test - random_seed : None or :class:`~simpeg.typing.RandomSeed`, optional - Random seed used for generating a random array for ``m`` if it's - None. It can either be an int, a predefined Numpy random number - generator, or any valid input to ``numpy.random.default_rng``. - kwargs: dict - Keyword arguments and associated values in the dictionary must - match those used in :meth:`discretize.tests.check_derivative` - - Returns - ------- - bool - Returns ``True`` if the test passes - """ - print("Testing {0!s}".format(str(self))) - if m is None: - rng = np.random.default_rng(seed=random_seed) - m = rng.uniform(size=self.nP) - if "plotIt" not in kwargs: - kwargs["plotIt"] = False - - assert isinstance( - self.nP, (int, np.integer) - ), "nP must be an integer for {}".format(self.__class__.__name__) - return check_derivative( - lambda m: [self * m, self.deriv(m)], m, num=num, **kwargs - ) - - def _assertMatchesPair(self, pair): - assert ( - isinstance(self, pair) - or isinstance(self, ComboMap) - and isinstance(self.maps[0], pair) - ), "Mapping object must be an instance of a {0!s} class.".format(pair.__name__) - - def __mul__(self, val): - if isinstance(val, IdentityMap): - if ( - not (self.shape[1] == "*" or val.shape[0] == "*") - and not self.shape[1] == val.shape[0] - ): - raise ValueError( - "Dimension mismatch in {0!s} and {1!s}.".format(str(self), str(val)) - ) - return ComboMap([self, val]) - - elif isinstance(val, np.ndarray): - if not self.shape[1] == "*" and not self.shape[1] == val.shape[0]: - raise ValueError( - "Dimension mismatch in {0!s} and np.ndarray{1!s}.".format( - str(self), str(val.shape) - ) - ) - return self._transform(val) - - elif isinstance(val, Zero): - return Zero() - - raise Exception( - "Unrecognized data type to multiply. Try a map or a numpy.ndarray!" - "You used a {} of type {}".format(val, type(val)) - ) - - def dot(self, map1): - r"""Multiply two mappings to create a :class:`simpeg.maps.ComboMap`. - - Let :math:`\mathbf{f}_1` and :math:`\mathbf{f}_2` represent two mapping functions. - Where :math:`\mathbf{m}` represents a set of input model parameters, - the ``dot`` method is used to create a combination mapping: - - .. math:: - u(\mathbf{m}) = f_2(f_1(\mathbf{m})) - - Where :math:`\mathbf{f_1} : M \rightarrow K_1` and acts on the - model first, and :math:`\mathbf{f_2} : K_1 \rightarrow K_2`, the combination - mapping :math:`\mathbf{u} : M \rightarrow K_2`. - - When using the **dot** method, the input argument *map1* represents the first - mapping that is be applied and *self* represents the second mapping - that is be applied. Therefore, the correct syntax for using this method is:: - - self.dot(map1) - - - Parameters - ---------- - map1 : - A SimPEG mapping object. - - Examples - -------- - Here we create a combination mapping that 1) projects a single scalar to - a vector space of length 5, then takes the natural exponent. - - >>> import numpy as np - >>> from simpeg.maps import ExpMap, Projection - - >>> nP1 = 1 - >>> nP2 = 5 - >>> ind = np.zeros(nP1, dtype=int) - - >>> projection_map = Projection(nP1, ind) - >>> projection_map.shape - (5, 1) - - >>> exp_map = ExpMap(nP=5) - >>> exp_map.shape - (5, 5) - - >>> combo_map = exp_map.dot(projection_map) - >>> combo_map.shape - (5, 1) - - >>> m = np.array([2]) - >>> combo_map * m - array([7.3890561, 7.3890561, 7.3890561, 7.3890561, 7.3890561]) - - """ - return self.__mul__(map1) - - def __matmul__(self, map1): - return self.__mul__(map1) - - __numpy_ufunc__ = True - - def __add__(self, map1): - return SumMap([self, map1]) # error-checking done inside of the SumMap - - def __str__(self): - return "{0!s}({1!s},{2!s})".format( - self.__class__.__name__, self.shape[0], self.shape[1] - ) - - def __len__(self): - return 1 - - @property - def mesh(self): - """ - The mesh used for the mapping - - Returns - ------- - discretize.base.BaseMesh or None - """ - return self._mesh - - @mesh.setter - def mesh(self, value): - if value is not None: - value = validate_type("mesh", value, discretize.base.BaseMesh, cast=False) - self._mesh = value - - @property - def is_linear(self): - """Determine whether or not this mapping is a linear operation. - - Returns - ------- - bool - """ - return True - - -class ComboMap(IdentityMap): - r"""Combination mapping constructed by joining a set of other mappings. - - A ``ComboMap`` is a single mapping object made by joining a set - of basic mapping operations by chaining them together, in order. - When creating a ``ComboMap``, the user provides a list of SimPEG mapping objects they wish to join. - The order of the mappings in this list is from last to first; i.e. - :math:`[\mathbf{f}_n , ... , \mathbf{f}_2 , \mathbf{f}_1]`. - - The combination mapping :math:`\mathbf{u}(\mathbf{m})` that acts on a - set of input model parameters :math:`\mathbf{m}` is defined as: - - .. math:: - \mathbf{u}(\mathbf{m}) = f_n(f_{n-1}(\cdots f_1(f_0(\mathbf{m})))) - - Note that any time that you create your own combination mapping, - be sure to test that the derivative is correct. - - Parameters - ---------- - maps : list of simpeg.maps.IdentityMap - A ``list`` of SimPEG mapping objects. The ordering of the mapping - objects in the ``list`` is from last applied to first applied! - - Examples - -------- - Here we create a combination mapping that 1) projects a single scalar to - a vector space of length 5, then takes the natural exponent. - - >>> import numpy as np - >>> from simpeg.maps import ExpMap, Projection, ComboMap - - >>> nP1 = 1 - >>> nP2 = 5 - >>> ind = np.zeros(nP1, dtype=int) - - >>> projection_map = Projection(nP1, ind) - >>> projection_map.shape - (5, 1) - - >>> exp_map = ExpMap(nP=5) - >>> exp_map.shape - (5, 5) - - Recall that the order of the mapping objects is from last applied - to first applied. - - >>> map_list = [exp_map, projection_map] - >>> combo_map = ComboMap(map_list) - >>> combo_map.shape - (5, 1) - - >>> m = np.array([2.]) - >>> combo_map * m - array([7.3890561, 7.3890561, 7.3890561, 7.3890561, 7.3890561]) - - """ - - def __init__(self, maps, **kwargs): - super().__init__(mesh=None, **kwargs) - - self.maps = [] - for ii, m in enumerate(maps): - assert isinstance(m, IdentityMap), "Unrecognized data type, " - "inherit from an IdentityMap or ComboMap!" - - if ( - ii > 0 - and not (self.shape[1] == "*" or m.shape[0] == "*") - and not self.shape[1] == m.shape[0] - ): - prev = self.maps[-1] - - raise ValueError( - "Dimension mismatch in map[{0!s}] ({1!s}, {2!s}) " - "and map[{3!s}] ({4!s}, {5!s}).".format( - prev.__class__.__name__, - prev.shape[0], - prev.shape[1], - m.__class__.__name__, - m.shape[0], - m.shape[1], - ) - ) - - if np.any([isinstance(m, SumMap), isinstance(m, IdentityMap)]): - self.maps += [m] - elif isinstance(m, ComboMap): - self.maps += m.maps - else: - raise ValueError("Map[{0!s}] not supported", m.__class__.__name__) - - @property - def shape(self): - r"""Dimensions of the mapping. - - For a list of SimPEG mappings [:math:`\mathbf{f}_n,...,\mathbf{f}_1`] - that have been joined to create a ``ComboMap``, this method returns - the dimensions of the combination mapping. Recall that the ordering - of the list of mappings is from last to first. - - Returns - ------- - (2) tuple of int - Dimensions of the mapping operator. - """ - return (self.maps[0].shape[0], self.maps[-1].shape[1]) - - @property - def nP(self): - r"""Number of parameters the mapping acts on. - - Returns - ------- - int - Number of parameters that the mapping acts on. - """ - return self.maps[-1].nP - - def _transform(self, m): - for map_i in reversed(self.maps): - m = map_i * m - return m - - def deriv(self, m, v=None): - r"""Derivative of the mapping with respect to the input parameters. - - Any time that you create your own combination mapping, - be sure to test that the derivative is correct. - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. - If the input argument *v* is not ``None``, the method returns - the derivative times the vector *v*. - - Notes - ----- - Let :math:`\mathbf{m}` be a set of model parameters and let - [:math:`\mathbf{f}_n,...,\mathbf{f}_1`] be the list of SimPEG mappings joined - to create a combination mapping. Recall that the list of mappings is ordered - from last applied to first applied. - - Where the combination mapping acting on the model parameters - can be expressed as: - - .. math:: - \mathbf{u}(\mathbf{m}) = f_n(f_{n-1}(\cdots f_1(f_0(\mathbf{m})))) - - The **deriv** method returns the derivative of :math:`\mathbf{u}` with respect - to the model parameters. To do this, we use the chain rule, i.e.: - - .. math:: - \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = - \frac{\partial \mathbf{f_n}}{\partial \mathbf{f_{n-1}}} - \cdots - \frac{\partial \mathbf{f_2}}{\partial \mathbf{f_{1}}} - \frac{\partial \mathbf{f_1}}{\partial \mathbf{m}} - """ - - if v is not None: - deriv = v - else: - deriv = 1 - - mi = m - for map_i in reversed(self.maps): - deriv = map_i.deriv(mi) * deriv - mi = map_i * mi - return deriv - - def __str__(self): - return "ComboMap[{0!s}]({1!s},{2!s})".format( - " * ".join([m.__str__() for m in self.maps]), self.shape[0], self.shape[1] - ) - - def __len__(self): - return len(self.maps) - - @property - def is_linear(self): - return all(m.is_linear for m in self.maps) - - -class LinearMap(IdentityMap): - """A generalized linear mapping. - - A simple map that implements the linear mapping, - - >>> y = A @ x + b - - Parameters - ---------- - A : (M, N) array_like, optional - The matrix operator, can be any object that implements `__matmul__` - and has a `shape` attribute. - b : (M) array_like, optional - Additive part of the linear operation. - """ - - def __init__(self, A, b=None, **kwargs): - kwargs.pop("mesh", None) - kwargs.pop("nP", None) - super().__init__(**kwargs) - self.A = A - self.b = b - - @property - def A(self): - """The linear operator matrix. - - Returns - ------- - LinearOperator - Must support matrix multiplication and have a shape attribute. - """ - return self._A - - @A.setter - def A(self, value): - if not hasattr(value, "__matmul__"): - raise TypeError( - f"{repr(value)} does not implement the matrix multiplication operator." - ) - if not hasattr(value, "shape"): - raise TypeError(f"{repr(value)} does not have a shape attribute.") - self._A = value - self._nP = value.shape[1] - self._shape = value.shape - - @property - def shape(self): - return self._shape - - @property - def b(self): - """Added part of the linear operation. - - Returns - ------- - numpy.ndarray - """ - return self._b - - @b.setter - def b(self, value): - if value is not None: - value = validate_ndarray_with_shape("b", value, shape=(self.shape[0],)) - self._b = value - - def _transform(self, m): - if self.b is None: - return self.A @ m - return self.A @ m + self.b - - def deriv(self, m, v=None): - if v is None: - return self.A - return self.A @ v - - -class Projection(IdentityMap): - r"""Projection mapping. - - ``Projection`` mapping can be used to project and/or rearange model - parameters. For a set of model parameter :math:`\mathbf{m}`, - the mapping :math:`\mathbf{u}(\mathbf{m})` can be defined by a linear - projection matrix :math:`\mathbf{P}` acting on the model, i.e.: - - .. math:: - \mathbf{u}(\mathbf{m}) = \mathbf{Pm} - - The number of model parameters the mapping acts on is - defined by *nP*. Projection and/or rearrangement of the parameters - is defined by *index*. Thus the dimensions of the mapping is - (*nInd*, *nP*). - - Parameters - ---------- - nP : int - Number of model parameters the mapping acts on - index : numpy.ndarray of int - Indexes defining the projection from the model space - - Examples - -------- - Here we define a mapping that rearranges and projects 2 model - parameters to a vector space spanning 4 parameters. - - >>> from simpeg.maps import Projection - >>> import numpy as np - - >>> nP = 2 - >>> index = np.array([1, 0, 1, 0], dtype=int) - >>> mapping = Projection(nP, index) - - >>> m = np.array([6, 8]) - >>> mapping * m - array([8, 6, 8, 6]) - - """ - - def __init__(self, nP, index, **kwargs): - assert isinstance( - index, (np.ndarray, slice, list) - ), "index must be a np.ndarray or slice, not {}".format(type(index)) - super(Projection, self).__init__(nP=nP, **kwargs) - - if isinstance(index, slice): - index = list(range(*index.indices(self.nP))) - - if isinstance(index, np.ndarray): - if index.dtype is np.dtype("bool"): - index = np.where(index)[0] - - self.index = index - self._shape = nI, nP = len(self.index), self.nP - - assert max(index) < nP, "maximum index must be less than {}".format(nP) - - # sparse projection matrix - self.P = sp.csr_matrix((np.ones(nI), (range(nI), self.index)), shape=(nI, nP)) - - def _transform(self, m): - return m[self.index] - - @property - def shape(self): - r"""Dimensions of the mapping. - - Returns - ------- - tuple - Where *nP* is the number of parameters the mapping acts on and - *nInd* is the length of the vector defining the mapping, the - dimensions of the mapping operator is a tuple of the - form (*nInd*, *nP*). - """ - return self._shape - - def deriv(self, m, v=None): - r"""Derivative of the mapping with respect to the input parameters. - - Let :math:`\mathbf{m}` be a set of model parameters and let :math:`\mathbf{P}` - be a matrix denoting the projection mapping. Where the projection mapping acting - on the model parameters can be expressed as: - - .. math:: - \mathbf{u} = \mathbf{P m}, - - the **deriv** method returns the derivative of :math:`\mathbf{u}` with respect - to the model parameters; i.e.: - - .. math:: - \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = \mathbf{P} - - Note that in this case, **deriv** simply returns a sparse projection matrix. - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. If the - input argument *v* is not ``None``, the method returns the derivative times - the vector *v*. - """ - - if v is not None: - return self.P * v - return self.P - - -class SumMap(ComboMap): - """Combination map constructed by summing multiple mappings - to the same vector space. - - A map to add model parameters contributing to the - forward operation e.g. F(m) = F(g(x) + h(y)) - - Assumes that the model vectors defined by g(x) and h(y) - are equal in length. - Allows to assume different things about the model m: - i.e. parametric + voxel models - - Parameters - ---------- - maps : list - A list of SimPEG mapping objects that are being summed. - Each mapping object in the list must act on the same number - of model parameters and must map to the same vector space! - """ - - def __init__(self, maps, **kwargs): - maps = validate_list_of_types("maps", maps, IdentityMap) - - # skip ComboMap's init - super(ComboMap, self).__init__(mesh=None, **kwargs) - - self.maps = [] - for ii, m in enumerate(maps): - if not isinstance(m, IdentityMap): - raise TypeError( - "Unrecognized data type {}, inherit from an " - "IdentityMap!".format(type(m)) - ) - - if ( - ii > 0 - and not (self.shape == "*" or m.shape == "*") - and not self.shape == m.shape - ): - raise ValueError( - "Dimension mismatch in map[{0!s}] ({1!s}, {2!s}) " - "and map[{3!s}] ({4!s}, {5!s}).".format( - self.maps[0].__class__.__name__, - self.maps[0].shape[0], - self.maps[0].shape[1], - m.__class__.__name__, - m.shape[0], - m.shape[1], - ) - ) - - self.maps += [m] - - @property - def shape(self): - """Dimensions of the mapping. - - Returns - ------- - tuple - The dimensions of the mapping. A tuple of the form (``int``,``int``) - """ - return (self.maps[0].shape[0], self.maps[0].shape[1]) - - @property - def nP(self): - r"""Number of parameters the combined mapping acts on. - - Returns - ------- - int - Number of parameters that the mapping acts on. - """ - return self.maps[-1].shape[1] - - def _transform(self, m): - for ii, map_i in enumerate(self.maps): - m0 = m.copy() - m0 = map_i * m0 - - if ii == 0: - mout = m0 - else: - mout += m0 - return mout - - def deriv(self, m, v=None): - """Derivative of mapping with respect to the input parameters - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. If the - input argument *v* is not ``None``, the method returns the derivative times - the vector *v*. - """ - - for ii, map_i in enumerate(self.maps): - m0 = m.copy() - - if v is not None: - deriv = v - else: - deriv = sp.eye(self.nP) - - deriv = map_i.deriv(m0, v=deriv) - if ii == 0: - sumDeriv = deriv - else: - sumDeriv += deriv - - return sumDeriv - - -class SurjectUnits(IdentityMap): - r"""Surjective mapping to all mesh cells. - - Let :math:`\mathbf{m}` be a model that contains a physical property value - for *nP* geological units. ``SurjectUnits`` is used to construct a surjective - mapping that projects :math:`\mathbf{m}` to the set of voxel cells defining a mesh. - As a result, the mapping :math:`\mathbf{u(\mathbf{m})}` is defined as - a projection matrix :math:`\mathbf{P}` acting on the model. Thus: - - .. math:: - \mathbf{u}(\mathbf{m}) = \mathbf{Pm} - - - The mapping therefore has dimensions (*mesh.nC*, *nP*). - - Parameters - ---------- - indices : (nP) list of (mesh.nC) numpy.ndarray - Each entry in the :class:`list` is a boolean :class:`numpy.ndarray` of length - *mesh.nC* that assigns the corresponding physical property value to the - appropriate mesh cells. - - Examples - -------- - For this example, we have a model that defines the property values - for two units. Using ``SurjectUnit``, we construct the mapping from - the model to a 1D mesh where the 1st unit's value is assigned to - all cells whose centers are located at *x < 0* and the 2nd unit's value - is assigned to all cells whose centers are located at *x > 0*. - - >>> from simpeg.maps import SurjectUnits - >>> from discretize import TensorMesh - >>> import numpy as np - - >>> nP = 8 - >>> mesh = TensorMesh([np.ones(nP)], 'C') - >>> unit_1_ind = mesh.cell_centers < 0 - - >>> indices_list = [unit_1_ind, ~unit_1_ind] - >>> mapping = SurjectUnits(indices_list, nP=nP) - - >>> m = np.r_[0.01, 0.05] - >>> mapping * m - array([0.01, 0.01, 0.01, 0.01, 0.05, 0.05, 0.05, 0.05]) - - """ - - def __init__(self, indices, **kwargs): - super().__init__(**kwargs) - self.indices = indices - - @property - def indices(self): - """List assigning a given physical property to specific model cells. - - Each entry in the :class:`list` is a boolean :class:`numpy.ndarray` of length - *mesh.nC* that assigns the corresponding physical property value to the - appropriate mesh cells. - - Returns - ------- - (nP) list of (mesh.n_cells) numpy.ndarray - """ - return self._indices - - @indices.setter - def indices(self, values): - values = validate_type("indices", values, list) - mesh = self.mesh - last_shape = None - for i in range(len(values)): - if mesh is not None: - values[i] = validate_active_indices( - "indices", values[i], self.mesh.n_cells - ) - else: - values[i] = validate_ndarray_with_shape( - "indices", values[i], shape=("*",), dtype=int - ) - if last_shape is not None and last_shape != values[i].shape: - raise ValueError("all indicies must have the same shape.") - last_shape = values[i].shape - self._indices = values - - @property - def P(self): - """ - Projection matrix from model parameters to mesh cells. - """ - if getattr(self, "_P", None) is None: - # sparse projection matrix - row = [] - col = [] - val = [] - for ii, ind in enumerate(self.indices): - col += [ii] * ind.sum() - row += np.where(ind)[0].tolist() - val += [1] * ind.sum() - - self._P = sp.csr_matrix( - (val, (row, col)), shape=(len(self.indices[0]), self.nP) - ) - - # self._P = sp.block_diag([P for ii in range(self.nBlock)]) - - return self._P - - def _transform(self, m): - return self.P * m - - @property - def nP(self): - r"""Number of parameters the mapping acts on. - - Returns - ------- - int - Number of parameters that the mapping acts on. - """ - return len(self.indices) - - @property - def shape(self): - """Dimensions of the mapping - - Returns - ------- - tuple - Dimensions of the mapping. Where *nP* is the number of parameters the - mapping acts on and *mesh.nC* is the number of cells the corresponding - mesh, the return is a tuple of the form (*mesh.nC*, *nP*). - """ - # return self.n_block*len(self.indices[0]), self.n_block*len(self.indices) - return (len(self.indices[0]), self.nP) - - def deriv(self, m, v=None): - r"""Derivative of the mapping with respect to the input parameters. - - Let :math:`\mathbf{m}` be a set of model parameters. The surjective mapping - can be defined as a sparse projection matrix :math:`\mathbf{P}`. Therefore - we can define the surjective mapping acting on the model parameters as: - - .. math:: - \mathbf{u} = \mathbf{P m}, - - the **deriv** method returns the derivative of :math:`\mathbf{u}` with respect - to the model parameters; i.e.: - - .. math:: - \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = \mathbf{P} - - Note that in this case, **deriv** simply returns a sparse projection matrix. - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. - If the input argument *v* is not ``None``, the method returns - the derivative times the vector *v*. - """ - - if v is not None: - return self.P * v - return self.P - - -class SphericalSystem(IdentityMap): - r"""Mapping vectors from spherical to Cartesian coordinates. - - Let :math:`\mathbf{m}` be a model containing the amplitudes - (:math:`\mathbf{a}`), azimuthal angles (:math:`\mathbf{t}`) - and radial angles (:math:`\mathbf{p}`) for a set of vectors - in spherical space such that: - - .. math:: - \mathbf{m} = \begin{bmatrix} \mathbf{a} \\ \mathbf{t} \\ \mathbf{p} \end{bmatrix} - - ``SphericalSystem`` constructs a mapping :math:`\mathbf{u}(\mathbf{m}) - that converts the set of vectors in spherical coordinates to - their representation in Cartesian coordinates, i.e.: - - .. math:: - \mathbf{u}(\mathbf{m}) = \begin{bmatrix} \mathbf{v_x} \\ \mathbf{v_y} \\ \mathbf{v_z} \end{bmatrix} - - where :math:`\mathbf{v_x}`, :math:`\mathbf{v_y}` and :math:`\mathbf{v_z}` - store the x, y and z components of the vectors, respectively. - - Using the *mesh* or *nP* input arguments, the dimensions of the corresponding - mapping operator can be permanently set; i.e. (*3\*mesh.nC*, *3\*mesh.nC*) or (*nP*, *nP*). - However if both input arguments *mesh* and *nP* are ``None``, the shape of - mapping operator is arbitrary and can act on any vector whose length - is a multiple of 3; i.e. has shape (``*``, ``*``). - - Notes - ----- - - In Cartesian space, the components of each vector are defined as - - .. math:: - \mathbf{v} = (v_x, v_y, v_z) - - In spherical coordinates, vectors are is defined as: - - .. math:: - \mathbf{v^\prime} = (a, t, p) - - where - - - :math:`a` is the amplitude of the vector - - :math:`t` is the azimuthal angle defined positive from vertical - - :math:`p` is the radial angle defined positive CCW from Easting - - Parameters - ---------- - mesh : discretize.BaseMesh - The number of parameters accepted by the mapping is set to equal - *3\*mesh.nC* . - nP : int - Set the number of parameters accepted by the mapping directly. Used if the - number of parameters is known. Used generally when the number of parameters - is not equal to the number of cells in a mesh. - """ - - def __init__(self, mesh=None, nP=None, **kwargs): - if nP is not None: - assert nP % 3 == 0, "Number of parameters must be a multiple of 3" - super().__init__(mesh, nP, **kwargs) - self.model = None - - def sphericalDeriv(self, model): - if getattr(self, "model", None) is None: - self.model = model - - if getattr(self, "_sphericalDeriv", None) is None or not all( - self.model == model - ): - self.model = model - - # Do a double projection to make sure the parameters are bounded - m_xyz = mat_utils.spherical2cartesian(model.reshape((-1, 3), order="F")) - m_atp = mat_utils.cartesian2spherical( - m_xyz.reshape((-1, 3), order="F") - ).reshape((-1, 3), order="F") - - nC = m_atp[:, 0].shape[0] - - dm_dx = sp.hstack( - [ - sp.diags(np.cos(m_atp[:, 1]) * np.cos(m_atp[:, 2]), 0), - sp.diags( - -m_atp[:, 0] * np.sin(m_atp[:, 1]) * np.cos(m_atp[:, 2]), 0 - ), - sp.diags( - -m_atp[:, 0] * np.cos(m_atp[:, 1]) * np.sin(m_atp[:, 2]), 0 - ), - ] - ) - - dm_dy = sp.hstack( - [ - sp.diags(np.cos(m_atp[:, 1]) * np.sin(m_atp[:, 2]), 0), - sp.diags( - -m_atp[:, 0] * np.sin(m_atp[:, 1]) * np.sin(m_atp[:, 2]), 0 - ), - sp.diags( - m_atp[:, 0] * np.cos(m_atp[:, 1]) * np.cos(m_atp[:, 2]), 0 - ), - ] - ) - - dm_dz = sp.hstack( - [ - sp.diags(np.sin(m_atp[:, 1]), 0), - sp.diags(m_atp[:, 0] * np.cos(m_atp[:, 1]), 0), - csr((nC, nC)), - ] - ) - - self._sphericalDeriv = sp.vstack([dm_dx, dm_dy, dm_dz]) - - return self._sphericalDeriv - - def _transform(self, model): - return mat_utils.spherical2cartesian(model.reshape((-1, 3), order="F")) - - def inverse(self, u): - r"""Maps vectors in Cartesian coordinates to spherical coordinates. - - Let :math:`\mathbf{v_x}`, :math:`\mathbf{v_y}` and :math:`\mathbf{v_z}` - store the x, y and z components of a set of vectors in Cartesian - coordinates such that: - - .. math:: - \mathbf{u} = \begin{bmatrix} \mathbf{x} \\ \mathbf{y} \\ \mathbf{z} \end{bmatrix} - - The inverse mapping recovers the vectors in spherical coordinates, i.e.: - - .. math:: - \mathbf{m}(\mathbf{u}) = \begin{bmatrix} \mathbf{a} \\ \mathbf{t} \\ \mathbf{p} \end{bmatrix} - - where :math:`\mathbf{a}` are the amplitudes, :math:`\mathbf{t}` are the - azimuthal angles and :math:`\mathbf{p}` are the radial angles. - - Parameters - ---------- - u : numpy.ndarray - The x, y and z components of a set of vectors in Cartesian coordinates. - If the mapping is defined for a mesh, the numpy.ndarray has length - *3\*mesh.nC* . - - Returns - ------- - numpy.ndarray - The amplitudes (:math:`\mathbf{a}`), azimuthal angles (:math:`\mathbf{t}`) - and radial angles (:math:`\mathbf{p}`) for the set of vectors in spherical - coordinates. If the mapping is defined for a mesh, the numpy.ndarray has length - *3\*mesh.nC* . - """ - return mat_utils.cartesian2spherical(u.reshape((-1, 3), order="F")) - - @property - def shape(self): - r"""Dimensions of the mapping - - The dimensions of the mesh depend on the input arguments used - during instantiation. If *mesh* is used to define the - mapping, the shape of mapping operator is (*3\*mesh.nC*, *3\*mesh.nC*). - If *nP* is used to define the identity map, the mapping operator - has dimensions (*nP*, *nP*). If *mesh* and *nP* were ``None`` when - instantiating, the mapping has dimensions (``*``, ``*``) and may - act on a vector whose length is a multiple of 3. - - Returns - ------- - tuple - Dimensions of the mapping operator. If the dimensions of - the mapping are set, the return is a tuple (``int``,``int``). - If the mapping can act on a vector of arbitrary length, the - return is a tuple (``*``, ``*``). - """ - # return self.n_block*len(self.indices[0]), self.n_block*len(self.indices) - return (self.nP, self.nP) - - def deriv(self, m, v=None): - """Derivative of mapping with respect to the input parameters - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. If the - input argument *v* is not ``None``, the method returns the derivative times - the vector *v*. - """ - - if v is not None: - return self.sphericalDeriv(m) * v - return self.sphericalDeriv(m) - - @property - def is_linear(self): - return False - - -class Wires(object): - r"""Mapping class for organizing multiple parameter types into a single model. - - Let :math:`\mathbf{p_1}` and :math:`\mathbf{p_2}` be vectors that - contain the parameter values for two different parameter types; for example, - electrical conductivity and magnetic permeability. Here, all parameters - are organized into a single model :math:`\mathbf{m}` of the form: - - .. math:: - \mathbf{m} = \begin{bmatrix} \mathbf{p_1} \\ \mathbf{p_2} \end{bmatrix} - - The ``Wires`` class constructs and applies the basic projection mappings - for extracting the values of a particular parameter type from the model. - For example: - - .. math:: - \mathbf{p_1} = \mathbf{P_{\! 1} m} - - where :math:`\mathbf{P_1}` is the projection matrix that extracts parameters - :math:`\mathbf{p_1}` from the complete set of model parameters :math:`\mathbf{m}`. - Likewise, there is a projection matrix for extracting :math:`\mathbf{p_2}`. - This can be extended to a model that containing more than 2 parameter types. - - Parameters - ---------- - args : tuple - Each input argument is a tuple (``str``, ``int``) that provides the name - and number of parameters for a given parameters type. - - Examples - -------- - Here we construct a wire mapping for a model where there - are two parameters types. Note that the number of parameters - of each type does not need to be the same. - - >>> from simpeg.maps import Wires, ReciprocalMap - >>> import numpy as np - - >>> p1 = np.r_[4.5, 2.7, 6.9, 7.1, 1.2] - >>> p2 = np.r_[10., 2., 5.]**-1 - >>> nP1 = len(p1) - >>> nP2 = len(p2) - >>> m = np.r_[p1, p2] - >>> m - array([4.5, 2.7, 6.9, 7.1, 1.2, 0.1, 0.5, 0.2]) - - Here we construct the wire map. The user provides a name - and the number of parameters for each type. The name - provided becomes the name of the method for constructing - the projection mapping. - - >>> wire_map = Wires(('name_1', nP1), ('name_2', nP2)) - - Here, we extract the values for the first parameter type. - - >>> wire_map.name_1 * m - array([4.5, 2.7, 6.9, 7.1, 1.2]) - - And here, we extract the values for the second parameter - type then apply a reciprocal mapping. - - >>> reciprocal_map = ReciprocalMap() - >>> reciprocal_map * wire_map.name_2 * m - array([10., 2., 5.]) - - """ - - def __init__(self, *args): - for arg in args: - assert ( - isinstance(arg, tuple) - and len(arg) == 2 - and isinstance(arg[0], str) - and - # TODO: this should be extended to a slice. - isinstance(arg[1], (int, np.integer)) - ), ( - "Each wire needs to be a tuple: (name, length). " - "You provided: {}".format(arg) - ) - - self._nP = int(np.sum([w[1] for w in args])) - start = 0 - maps = [] - for arg in args: - wire = Projection(self.nP, slice(start, start + arg[1])) - setattr(self, arg[0], wire) - maps += [(arg[0], wire)] - start += arg[1] - self.maps = maps - - self._tuple = namedtuple("Model", [w[0] for w in args]) - - def __mul__(self, val): - assert isinstance(val, np.ndarray) - split = [] - for _, w in self.maps: - split += [w * val] - return self._tuple(*split) - - @property - def nP(self): - r"""Number of parameters the mapping acts on. - - Returns - ------- - int - Number of parameters that the mapping acts on. - """ - return self._nP - - -class SelfConsistentEffectiveMedium(IdentityMap): - r""" - Two phase self-consistent effective medium theory mapping for - ellipsoidal inclusions. The inversion model is the concentration - (volume fraction) of the phase 2 material. - - The inversion model is :math:`\varphi`. We solve for :math:`\sigma` - given :math:`\sigma_0`, :math:`\sigma_1` and :math:`\varphi` . Each of - the following are implicit expressions of the effective conductivity. - They are solved using a fixed point iteration. - - **Spherical Inclusions** - - If the shape of the inclusions are spheres, we use - - .. math:: - - \sum_{j=1}^N (\sigma^* - \sigma_j)R^{j} = 0 - - where :math:`j=[1,N]` is the each material phase, and N is the number - of phases. Currently, the implementation is only set up for 2 phase - materials, so we solve - - .. math:: - - (1-\\varphi)(\sigma - \sigma_0)R^{(0)} + \varphi(\sigma - \sigma_1)R^{(1)} = 0. - - Where :math:`R^{(j)}` is given by - - .. math:: - - R^{(j)} = \left[1 + \frac{1}{3}\frac{\sigma_j - \sigma}{\sigma} \right]^{-1}. - - **Ellipsoids** - - .. todo:: - - Aligned Ellipsoids have not yet been implemented, only randomly - oriented ellipsoids - - If the inclusions are aligned ellipsoids, we solve - - .. math:: - - \sum_{j=1}^N \varphi_j (\Sigma^* - \sigma_j\mathbf{I}) \mathbf{R}^{j, *} = 0 - - where - - .. math:: - - \mathbf{R}^{(j, *)} = \left[ \mathbf{I} + \mathbf{A}_j {\Sigma^{*}}^{-1}(\sigma_j \mathbf{I} - \Sigma^*) \\right]^{-1} - - and the depolarization tensor :math:`\mathbf{A}_j` is given by - - .. math:: - - \mathbf{A}^* = \left[\begin{array}{ccc} - Q & 0 & 0 \\ - 0 & Q & 0 \\ - 0 & 0 & 1-2Q - \end{array}\right] - - for a spheroid aligned along the z-axis. For an oblate spheroid - (:math:`\alpha < 1`, pancake-like) - - .. math:: - - Q = \frac{1}{2}\left( - 1 + \frac{1}{\alpha^2 - 1} \left[ - 1 - \frac{1}{\chi}\tan^{-1}(\chi) - \right] - \right) - - where - - .. math:: - - \chi = \sqrt{\frac{1}{\alpha^2} - 1} - - - For reference, see - `Torquato (2002), Random Heterogeneous Materials `_ - - - """ - - def __init__( - self, - mesh=None, - nP=None, - sigma0=None, - sigma1=None, - alpha0=1.0, - alpha1=1.0, - orientation0="z", - orientation1="z", - random=True, - rel_tol=1e-3, - maxIter=50, - **kwargs, - ): - self._sigstart = None - self.sigma0 = sigma0 - self.sigma1 = sigma1 - self.alpha0 = alpha0 - self.alpha1 = alpha1 - self.orientation0 = orientation0 - self.orientation1 = orientation1 - self.random = random - self.rel_tol = rel_tol - self.maxIter = maxIter - super(SelfConsistentEffectiveMedium, self).__init__(mesh, nP, **kwargs) - - @property - def sigma0(self): - """Physical property value for phase-0 material. - - Returns - ------- - float - """ - return self._sigma0 - - @sigma0.setter - def sigma0(self, value): - self._sigma0 = validate_float("sigma0", value, min_val=0.0) - - @property - def sigma1(self): - """Physical property value for phase-1 material. - - Returns - ------- - float - """ - return self._sigma1 - - @sigma1.setter - def sigma1(self, value): - self._sigma1 = validate_float("sigma1", value, min_val=0.0) - - @property - def alpha0(self): - """Aspect ratio of the phase-0 ellipsoids. - - Returns - ------- - float - """ - return self._alpha0 - - @alpha0.setter - def alpha0(self, value): - self._alpha0 = validate_float("alpha0", value, min_val=0.0) - - @property - def alpha1(self): - """Aspect ratio of the phase-1 ellipsoids. - - Returns - ------- - float - """ - return self._alpha1 - - @alpha1.setter - def alpha1(self, value): - self._alpha1 = validate_float("alpha1", value, min_val=0.0) - - @property - def orientation0(self): - """Orientation of the phase-0 inclusions. - - Returns - ------- - numpy.ndarray - """ - return self._orientation0 - - @orientation0.setter - def orientation0(self, value): - self._orientation0 = validate_direction("orientation0", value, dim=3) - - @property - def orientation1(self): - """Orientation of the phase-0 inclusions. - - Returns - ------- - numpy.ndarray - """ - return self._orientation1 - - @orientation1.setter - def orientation1(self, value): - self._orientation1 = validate_direction("orientation1", value, dim=3) - - @property - def random(self): - """Are the inclusions randomly oriented (True) or preferentially aligned (False)? - - Returns - ------- - bool - """ - return self._random - - @random.setter - def random(self, value): - self._random = validate_type("random", value, bool) - - @property - def rel_tol(self): - """relative tolerance for convergence for the fixed-point iteration. - - Returns - ------- - float - """ - return self._rel_tol - - @rel_tol.setter - def rel_tol(self, value): - self._rel_tol = validate_float( - "rel_tol", value, min_val=0.0, inclusive_min=False - ) - - @property - def maxIter(self): - """Maximum number of iterations for the fixed point iteration calculation. - - Returns - ------- - int - """ - return self._maxIter - - @maxIter.setter - def maxIter(self, value): - self._maxIter = validate_integer("maxIter", value, min_val=0) - - @property - def tol(self): - """ - absolute tolerance for the convergence of the fixed point iteration - calc - """ - if getattr(self, "_tol", None) is None: - self._tol = self.rel_tol * min(self.sigma0, self.sigma1) - return self._tol - - @property - def sigstart(self): - """ - first guess for sigma - """ - return self._sigstart - - @sigstart.setter - def sigstart(self, value): - if value is not None: - value = validate_float("sigstart", value) - self._sigstart = value - - def wiener_bounds(self, phi1): - """Define Wenner Conductivity Bounds - - See Torquato, 2002 - """ - phi0 = 1.0 - phi1 - sigWup = phi0 * self.sigma0 + phi1 * self.sigma1 - sigWlo = 1.0 / (phi0 / self.sigma0 + phi1 / self.sigma1) - W = np.array([sigWlo, sigWup]) - - return W - - def hashin_shtrikman_bounds(self, phi1): - """Hashin Shtrikman bounds - - See Torquato, 2002 - """ - # TODO: this should probably exsist on its own as a util - - phi0 = 1.0 - phi1 - sigWu = self.wiener_bounds(phi1)[1] - sig_tilde = phi0 * self.sigma1 + phi1 * self.sigma0 - - sigma_min = np.min([self.sigma0, self.sigma1]) - sigma_max = np.max([self.sigma0, self.sigma1]) - - sigHSlo = sigWu - ( - (phi0 * phi1 * (self.sigma0 - self.sigma1) ** 2) - / (sig_tilde + 2 * sigma_max) - ) - sigHSup = sigWu - ( - (phi0 * phi1 * (self.sigma0 - self.sigma1) ** 2) - / (sig_tilde + 2 * sigma_min) - ) - - return np.array([sigHSlo, sigHSup]) - - def hashin_shtrikman_bounds_anisotropic(self, phi1): - """Hashin Shtrikman bounds for anisotropic media - - See Torquato, 2002 - """ - phi0 = 1.0 - phi1 - sigWu = self.wiener_bounds(phi1)[1] - - sigma_min = np.min([self.sigma0, self.sigma1]) - sigma_max = np.max([self.sigma0, self.sigma1]) - - phi_min = phi0 if self.sigma1 > self.sigma0 else phi1 - phi_max = phi1 if self.sigma1 > self.sigma0 else phi0 - - amax = ( - -phi0 - * phi1 - * self.getA( - self.alpha1 if self.sigma1 > self.sigma0 else self.alpha0, - self.orientation1 if self.sigma1 > self.sigma0 else self.orientation0, - ) - ) - I = np.eye(3) - - sigHSlo = sigWu * I + ( - (sigma_min - sigma_max) ** 2 - * amax - * np.linalg.inv(sigma_min * I + (sigma_min - sigma_max) / phi_max * amax) - ) - sigHSup = sigWu * I + ( - (sigma_max - sigma_min) ** 2 - * amax - * np.linalg.inv(sigma_max * I + (sigma_max - sigma_min) / phi_min * amax) - ) - - return [sigHSlo, sigHSup] - - def getQ(self, alpha): - """Geometric factor in the depolarization tensor""" - if alpha < 1.0: # oblate spheroid - chi = np.sqrt((1.0 / alpha**2.0) - 1) - return ( - 1.0 / 2.0 * (1 + 1.0 / (alpha**2.0 - 1) * (1.0 - np.arctan(chi) / chi)) - ) - elif alpha > 1.0: # prolate spheroid - chi = np.sqrt(1 - (1.0 / alpha**2.0)) - return ( - 1.0 - / 2.0 - * ( - 1 - + 1.0 - / (alpha**2.0 - 1) - * (1.0 - 1.0 / (2.0 * chi) * np.log((1 + chi) / (1 - chi))) - ) - ) - elif alpha == 1: # sphere - return 1.0 / 3.0 - - def getA(self, alpha, orientation): - """Depolarization tensor""" - Q = self.getQ(alpha) - A = np.diag([Q, Q, 1 - 2 * Q]) - R = rotation_matrix_from_normals(np.r_[0.0, 0.0, 1.0], orientation) - return (R.T).dot(A).dot(R) - - def getR(self, sj, se, alpha, orientation=None): - """Electric field concentration tensor""" - if self.random is True: # isotropic - if alpha == 1.0: - return 3.0 * se / (2.0 * se + sj) - Q = self.getQ(alpha) - return ( - se - / 3.0 - * (2.0 / (se + Q * (sj - se)) + 1.0 / (sj - 2.0 * Q * (sj - se))) - ) - else: # anisotropic - if orientation is None: - raise Exception("orientation must be provided if random=False") - I = np.eye(3) - seinv = np.linalg.inv(se) - Rinv = I + self.getA(alpha, orientation) * seinv * (sj * I - se) - return np.linalg.inv(Rinv) - - def getdR(self, sj, se, alpha, orientation=None): - """ - Derivative of the electric field concentration tensor with respect - to the concentration of the second phase material. - """ - if self.random is True: - if alpha == 1.0: - return 3.0 / (2.0 * se + sj) - 6.0 * se / (2.0 * se + sj) ** 2 - Q = self.getQ(alpha) - return ( - 1 - / 3 - * ( - 2.0 / (se + Q * (sj - se)) - + 1.0 / (sj - 2.0 * Q * (sj - se)) - + se - * ( - -2 * (1 - Q) / (se + Q * (sj - se)) ** 2 - - 2 * Q / (sj - 2.0 * Q * (sj - se)) ** 2 - ) - ) - ) - else: - if orientation is None: - raise Exception("orientation must be provided if random=False") - raise NotImplementedError - - def _sc2phaseEMTSpheroidstransform(self, phi1): - """ - Self Consistent Effective Medium Theory Model Transform, - alpha = aspect ratio (c/a <= 1) - """ - - if not (np.all(0 <= phi1) and np.all(phi1 <= 1)): - warnings.warn("there are phis outside bounds of 0 and 1", stacklevel=2) - phi1 = np.median(np.c_[phi1 * 0, phi1, phi1 * 0 + 1.0]) - - phi0 = 1.0 - phi1 - - # starting guess - if self.sigstart is None: - sige1 = np.mean(self.wiener_bounds(phi1)) - else: - sige1 = self.sigstart - - if self.random is False: - sige1 = sige1 * np.eye(3) - - for _ in range(self.maxIter): - R0 = self.getR(self.sigma0, sige1, self.alpha0, self.orientation0) - R1 = self.getR(self.sigma1, sige1, self.alpha1, self.orientation1) - - den = phi0 * R0 + phi1 * R1 - num = phi0 * self.sigma0 * R0 + phi1 * self.sigma1 * R1 - - if self.random is True: - sige2 = num / den - relerr = np.abs(sige2 - sige1) - else: - sige2 = num * np.linalg.inv(den) - relerr = np.linalg.norm(np.abs(sige2 - sige1).flatten(), np.inf) - - if np.all(relerr <= self.tol): - if self.sigstart is None: - self._sigstart = ( - sige2 # store as a starting point for the next time around - ) - return sige2 - - sige1 = sige2 - # TODO: make this a proper warning, and output relevant info (sigma0, sigma1, phi, sigstart, and relerr) - warnings.warn("Maximum number of iterations reached", stacklevel=2) - - return sige2 - - def _sc2phaseEMTSpheroidsinversetransform(self, sige): - R0 = self.getR(self.sigma0, sige, self.alpha0, self.orientation0) - R1 = self.getR(self.sigma1, sige, self.alpha1, self.orientation1) - - num = -(self.sigma0 - sige) * R0 - den = (self.sigma1 - sige) * R1 - (self.sigma0 - sige) * R0 - - return num / den - - def _sc2phaseEMTSpheroidstransformDeriv(self, sige, phi1): - phi0 = 1.0 - phi1 - - R0 = self.getR(self.sigma0, sige, self.alpha0, self.orientation0) - R1 = self.getR(self.sigma1, sige, self.alpha1, self.orientation1) - - dR0 = self.getdR(self.sigma0, sige, self.alpha0, self.orientation0) - dR1 = self.getdR(self.sigma1, sige, self.alpha1, self.orientation1) - - num = (sige - self.sigma0) * R0 - (sige - self.sigma1) * R1 - den = phi0 * (R0 + (sige - self.sigma0) * dR0) + phi1 * ( - R1 + (sige - self.sigma1) * dR1 - ) - - return sdiag(num / den) - - def _transform(self, m): - return self._sc2phaseEMTSpheroidstransform(m) - - def deriv(self, m): - """ - Derivative of the effective conductivity with respect to the - volume fraction of phase 2 material - """ - sige = self._transform(m) - return self._sc2phaseEMTSpheroidstransformDeriv(sige, m) - - def inverse(self, sige): - """ - Compute the concentration given the effective conductivity - """ - return self._sc2phaseEMTSpheroidsinversetransform(sige) - - @property - def is_linear(self): - return False - - -############################################################################### -# # -# Mesh Independent Maps # -# # -############################################################################### - - -class ExpMap(IdentityMap): - r"""Mapping that computes the natural exponentials of the model parameters. - - Where :math:`\mathbf{m}` is a set of model parameters, ``ExpMap`` creates - a mapping :math:`\mathbf{u}(\mathbf{m})` that computes the natural exponential - of every element in :math:`\mathbf{m}`; i.e.: - - .. math:: - \mathbf{u}(\mathbf{m}) = exp(\mathbf{m}) - - ``ExpMap`` is commonly used when working with physical properties whose values - span many orders of magnitude (e.g. the electrical conductivity :math:`\sigma`). - By using ``ExpMap``, we can invert for a model that represents the natural log - of a set of physical property values, i.e. when :math:`m = log(\sigma)` - - Parameters - ---------- - mesh : discretize.BaseMesh - The number of parameters accepted by the mapping is set to equal the number - of mesh cells. - nP : int - Set the number of parameters accepted by the mapping directly. Used if the - number of parameters is known. Used generally when the number of parameters - is not equal to the number of cells in a mesh. - """ - - def __init__(self, mesh=None, nP=None, **kwargs): - super().__init__(mesh=mesh, nP=nP, **kwargs) - - def _transform(self, m): - return np.exp(mkvc(m)) - - def inverse(self, D): - r"""Apply the inverse of the exponential mapping to an array. - - For the exponential mapping :math:`\mathbf{u}(\mathbf{m})`, the - inverse mapping on a variable :math:`\mathbf{x}` is performed by taking - the natural logarithms of elements, i.e.: - - .. math:: - \mathbf{m} = \mathbf{u}^{-1}(\mathbf{x}) = log(\mathbf{x}) - - Parameters - ---------- - D : numpy.ndarray - A set of input values - - Returns - ------- - numpy.ndarray - A :class:`numpy.ndarray` containing result of applying the - inverse mapping to the elements in *D*; which in this case - is the natural logarithm. - """ - return np.log(mkvc(D)) - - def deriv(self, m, v=None): - r"""Derivative of mapping with respect to the input parameters. - - For a mapping :math:`\mathbf{u}(\mathbf{m})` that computes the natural - exponential function for each parameter in the model :math:`\mathbf{m}`, - i.e.: - - .. math:: - \mathbf{u}(\mathbf{m}) = exp(\mathbf{m}), - - the derivative of the mapping with respect to the model is a diagonal - matrix of the form: - - .. math:: - \frac{\partial \mathbf{u}}{\partial \mathbf{m}} - = \textrm{diag} \big ( exp(\mathbf{m}) \big ) - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. If the - input argument *v* is not ``None``, the method returns the derivative times - the vector *v*. - """ - deriv = sdiag(np.exp(mkvc(m))) - if v is not None: - return deriv * v - return deriv - - @property - def is_linear(self): - return False - - -class ReciprocalMap(IdentityMap): - r"""Mapping that computes the reciprocals of the model parameters. - - Where :math:`\mathbf{m}` is a set of model parameters, ``ReciprocalMap`` - creates a mapping :math:`\mathbf{u}(\mathbf{m})` that computes the - reciprocal of every element in :math:`\mathbf{m}`; - i.e.: - - .. math:: - \mathbf{u}(\mathbf{m}) = \mathbf{m}^{-1} - - Parameters - ---------- - mesh : discretize.BaseMesh - The number of parameters accepted by the mapping is set to equal the number - of mesh cells. - nP : int - Set the number of parameters accepted by the mapping directly. Used if the - number of parameters is known. Used generally when the number of parameters - is not equal to the number of cells in a mesh. - """ - - def __init__(self, mesh=None, nP=None, **kwargs): - super().__init__(mesh=mesh, nP=nP, **kwargs) - - def _transform(self, m): - return 1.0 / mkvc(m) - - def inverse(self, D): - r"""Apply the inverse of the reciprocal mapping to an array. - - For the reciprocal mapping :math:`\mathbf{u}(\mathbf{m})`, - the inverse mapping on a variable :math:`\mathbf{x}` is itself a - reciprocal mapping, i.e.: - - .. math:: - \mathbf{m} = \mathbf{u}^{-1}(\mathbf{x}) = \mathbf{x}^{-1} - - Parameters - ---------- - D : numpy.ndarray - A set of input values - - Returns - ------- - numpy.ndarray - A :class:`numpy.ndarray` containing result of applying the - inverse mapping to the elements in *D*; which in this case - is just a reciprocal mapping. - """ - return 1.0 / mkvc(D) - - def deriv(self, m, v=None): - r"""Derivative of mapping with respect to the input parameters. - - For a mapping that computes the reciprocal for each - parameter in the model :math:`\mathbf{m}`, i.e.: - - .. math:: - \mathbf{u}(\mathbf{m}) = \mathbf{m}^{-1} - - the derivative of the mapping with respect to the model is a diagonal - matrix of the form: - - .. math:: - \frac{\partial \mathbf{u}}{\partial \mathbf{m}} - = \textrm{diag} \big ( -\mathbf{m}^{-2} \big ) - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. If the - input argument *v* is not ``None``, the method returns the derivative times - the vector *v*. - """ - deriv = sdiag(-mkvc(m) ** (-2)) - if v is not None: - return deriv * v - return deriv - - @property - def is_linear(self): - return False - - -class LogMap(IdentityMap): - r"""Mapping that computes the natural logarithm of the model parameters. - - Where :math:`\mathbf{m}` is a set of model parameters, ``LogMap`` - creates a mapping :math:`\mathbf{u}(\mathbf{m})` that computes the - natural logarithm of every element in - :math:`\mathbf{m}`; i.e.: - - .. math:: - \mathbf{u}(\mathbf{m}) = \textrm{log}(\mathbf{m}) - - Parameters - ---------- - mesh : discretize.BaseMesh - The number of parameters accepted by the mapping is set to equal the number - of mesh cells. - nP : int - Set the number of parameters accepted by the mapping directly. Used if the - number of parameters is known. Used generally when the number of parameters - is not equal to the number of cells in a mesh. - """ - - def __init__(self, mesh=None, nP=None, **kwargs): - super().__init__(mesh=mesh, nP=nP, **kwargs) - - def _transform(self, m): - return np.log(mkvc(m)) - - def deriv(self, m, v=None): - r"""Derivative of mapping with respect to the input parameters. - - For a mapping :math:`\mathbf{u}(\mathbf{m})` that computes the - natural logarithm for each parameter in the model :math:`\mathbf{m}`, - i.e.: - - .. math:: - \mathbf{u}(\mathbf{m}) = log(\mathbf{m}) - - the derivative of the mapping with respect to the model is a diagonal - matrix of the form: - - .. math:: - \frac{\partial \mathbf{u}}{\partial \mathbf{m}} - = \textrm{diag} \big ( \mathbf{m}^{-1} \big ) - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. If the - input argument *v* is not ``None``, the method returns the derivative times - the vector *v*. - """ - mod = mkvc(m) - deriv = np.zeros(mod.shape) - tol = 1e-16 # zero - ind = np.greater_equal(np.abs(mod), tol) - deriv[ind] = 1.0 / mod[ind] - if v is not None: - return sdiag(deriv) * v - return sdiag(deriv) - - def inverse(self, m): - r"""Apply the inverse of the natural log mapping to an array. - - For the natural log mapping :math:`\mathbf{u}(\mathbf{m})`, - the inverse mapping on a variable :math:`\mathbf{x}` is performed by - taking the natural exponent of the elements, i.e.: - - .. math:: - \mathbf{m} = \mathbf{u}^{-1}(\mathbf{x}) = exp(\mathbf{x}) - - Parameters - ---------- - D : numpy.ndarray - A set of input values - - Returns - ------- - numpy.ndarray - A :class:`numpy.ndarray` containing result of applying the - inverse mapping to the elements in *D*; which in this case - is the natural exponent. - """ - return np.exp(mkvc(m)) - - @property - def is_linear(self): - return False - - -class LogisticSigmoidMap(IdentityMap): - r"""Mapping that computes the logistic sigmoid of the model parameters. - - Where :math:`\mathbf{m}` is a set of model parameters, ``LogisticSigmoidMap`` creates - a mapping :math:`\mathbf{u}(\mathbf{m})` that computes the logistic sigmoid - of every element in :math:`\mathbf{m}`; i.e.: - - .. math:: - \mathbf{u}(\mathbf{m}) = sigmoid(\mathbf{m}) = \frac{1}{1+\exp{-\mathbf{m}}} - - ``LogisticSigmoidMap`` transforms values onto the interval (0,1), but can optionally - be scaled and shifted to the interval (a,b). This can be useful for inversion - of data that varies over a log scale and bounded on some interval: - - .. math:: - \mathbf{u}(\mathbf{m}) = a + (b - a) \cdot sigmoid(\mathbf{m}) - - Parameters - ---------- - mesh : discretize.BaseMesh - The number of parameters accepted by the mapping is set to equal the number - of mesh cells. - nP : int - Set the number of parameters accepted by the mapping directly. Used if the - number of parameters is known. Used generally when the number of parameters - is not equal to the number of cells in a mesh. - lower_bound: float or (nP) numpy.ndarray - lower bound (a) for the transform. Default 0. Defined \in \mathbf{u} space. - upper_bound: float or (nP) numpy.ndarray - upper bound (b) for the transform. Default 1. Defined \in \mathbf{u} space. - - """ - - def __init__(self, mesh=None, nP=None, lower_bound=0, upper_bound=1, **kwargs): - super().__init__(mesh=mesh, nP=nP, **kwargs) - lower_bound = np.atleast_1d(lower_bound) - upper_bound = np.atleast_1d(upper_bound) - if self.nP != "*": - # check if lower bound and upper bound broadcast to nP - try: - np.broadcast_shapes(lower_bound.shape, (self.nP,)) - except ValueError as err: - raise ValueError( - f"Lower bound does not broadcast to the number of parameters. " - f"Lower bound shape is {lower_bound.shape} and tried against " - f"{self.nP} parameters." - ) from err - try: - np.broadcast_shapes(upper_bound.shape, (self.nP,)) - except ValueError as err: - raise ValueError( - f"Upper bound does not broadcast to the number of parameters. " - f"Upper bound shape is {upper_bound.shape} and tried against " - f"{self.nP} parameters." - ) from err - # make sure lower and upper bound broadcast to each other... - try: - np.broadcast_shapes(lower_bound.shape, upper_bound.shape) - except ValueError as err: - raise ValueError( - f"Upper bound does not broadcast to the lower bound. " - f"Shapes {upper_bound.shape} and {lower_bound.shape} " - f"are incompatible with each other." - ) from err - - if np.any(lower_bound >= upper_bound): - raise ValueError( - "A lower bound is greater than or equal to the upper bound." - ) - - self._lower_bound = lower_bound - self._upper_bound = upper_bound - - @property - def lower_bound(self): - """The lower bound - - Returns - ------- - numpy.ndarray - """ - return self._lower_bound - - @property - def upper_bound(self): - """The upper bound - - Returns - ------- - numpy.ndarray - """ - return self._upper_bound - - def _transform(self, m): - return self.lower_bound + (self.upper_bound - self.lower_bound) * expit(mkvc(m)) - - def inverse(self, m): - r"""Apply the inverse of the mapping to an array. - - For the logistic sigmoid mapping :math:`\mathbf{u}(\mathbf{m})`, the - inverse mapping on a variable :math:`\mathbf{x}` is performed by taking - the log-odds of elements, i.e.: - - .. math:: - \mathbf{m} = \mathbf{u}^{-1}(\mathbf{x}) = logit(\mathbf{x}) = \log \frac{\mathbf{x}}{1 - \mathbf{x}} - - or scaled and translated to interval (a,b): - .. math:: - \mathbf{m} = logit(\frac{(\mathbf{x} - a)}{b-a}) - - Parameters - ---------- - m : numpy.ndarray - A set of input values - - Returns - ------- - numpy.ndarray - the inverse mapping to the elements in *m*; which in this case - is the log-odds function with scaled and shifted input. - """ - return logit( - (mkvc(m) - self.lower_bound) / (self.upper_bound - self.lower_bound) - ) - - def deriv(self, m, v=None): - r"""Derivative of mapping with respect to the input parameters. - - For a mapping :math:`\mathbf{u}(\mathbf{m})` the derivative of the mapping with - respect to the model is a diagonal matrix of the form: - - .. math:: - \frac{\partial \mathbf{u}}{\partial \mathbf{m}} - = \textrm{diag} \big ( (b-a)\cdot sigmoid(\mathbf{m})\cdot(1-sigmoid(\mathbf{m})) \big ) - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - numpy.ndarray or scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. If the - input argument *v* is not ``None``, the method returns the derivative times - the vector *v*. - """ - sigmoid = expit(mkvc(m)) - deriv = (self.upper_bound - self.lower_bound) * sigmoid * (1.0 - sigmoid) - if v is not None: - return deriv * v - return sdiag(deriv) - - @property - def is_linear(self): - return False - - -class ChiMap(IdentityMap): - r"""Mapping that computes the magnetic permeability given a set of magnetic susceptibilities. - - Where :math:`\boldsymbol{\chi}` is the input model parameters defining a set of magnetic - susceptibilities, ``ChiMap`` creates a mapping :math:`\boldsymbol{\mu}(\boldsymbol{\chi})` - that computes the corresponding magnetic permeabilities of every - element in :math:`\boldsymbol{\chi}`; i.e.: - - .. math:: - \boldsymbol{\mu}(\boldsymbol{\chi}) = \mu_0 \big (1 + \boldsymbol{\chi} \big ) - - where :math:`\mu_0` is the permeability of free space. - - Parameters - ---------- - mesh : discretize.BaseMesh - The number of parameters accepted by the mapping is set to equal the number - of mesh cells. - nP : int - Set the number of parameters accepted by the mapping directly. Used if the - number of parameters is known. Used generally when the number of parameters - is not equal to the number of cells in a mesh. - """ - - def __init__(self, mesh=None, nP=None, **kwargs): - super().__init__(mesh=mesh, nP=nP, **kwargs) - - def _transform(self, m): - return mu_0 * (1 + m) - - def deriv(self, m, v=None): - r"""Derivative of mapping with respect to the input parameters. - - For a mapping :math:`\boldsymbol{\mu}(\boldsymbol{\chi})` that transforms a - set of magnetic susceptibilities :math:`\boldsymbol{\chi}` to their corresponding - magnetic permeabilities, i.e.: - - .. math:: - \boldsymbol{\mu}(\boldsymbol{\chi}) = \mu_0 \big (1 + \boldsymbol{\chi} \big ), - - the derivative of the mapping with respect to the model is the identity - matrix scaled by the permeability of free-space. Thus: - - .. math:: - \frac{\partial \boldsymbol{\mu}}{\partial \boldsymbol{\chi}} = \mu_0 \mathbf{I} - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. If the - input argument *v* is not ``None``, the method returns the derivative times - the vector *v*. - """ - if v is not None: - return mu_0 * v - return mu_0 * sp.eye(self.nP) - - def inverse(self, m): - r"""Apply the inverse mapping to an array. - - For the ``ChiMap`` class, the inverse mapping recoveres the set of - magnetic susceptibilities :math:`\boldsymbol{\chi}` from a set of - magnetic permeabilities :math:`\boldsymbol{\mu}`. Thus the inverse - mapping is defined as: - - .. math:: - \boldsymbol{\chi}(\boldsymbol{\mu}) = \frac{\boldsymbol{\mu}}{\mu_0} - 1 - - where :math:`\mu_0` is the permeability of free space. - - Parameters - ---------- - D : numpy.ndarray - A set of input values - - Returns - ------- - numpy.ndarray - A :class:`numpy.ndarray` containing result of applying the - inverse mapping to the elements in *D*; which in this case - represents the conversion of magnetic permeabilities - to their corresponding magnetic susceptibility values. - """ - return m / mu_0 - 1 - - -class MuRelative(IdentityMap): - r"""Mapping that computes the magnetic permeability given a set of relative permeabilities. - - Where :math:`\boldsymbol{\mu_r}` defines a set of relative permeabilities, ``MuRelative`` - creates a mapping :math:`\boldsymbol{\mu}(\boldsymbol{\mu_r})` that computes the - corresponding magnetic permeabilities of every element in :math:`\boldsymbol{\mu_r}`; - i.e.: - - .. math:: - \boldsymbol{\mu}(\boldsymbol{\mu_r}) = \mu_0 \boldsymbol{\mu_r} - - where :math:`\mu_0` is the permeability of free space. - - Parameters - ---------- - mesh : discretize.BaseMesh - The number of parameters accepted by the mapping is set to equal the number - of mesh cells. - nP : int - Set the number of parameters accepted by the mapping directly. Used if the - number of parameters is known. Used generally when the number of parameters - is not equal to the number of cells in a mesh. - """ - - def __init__(self, mesh=None, nP=None, **kwargs): - super().__init__(mesh=mesh, nP=nP, **kwargs) - - def _transform(self, m): - return mu_0 * m - - def deriv(self, m, v=None): - r"""Derivative of mapping with respect to the input parameters. - - For a mapping that transforms a set of relative permeabilities - :math:`\boldsymbol{\mu_r}` to their corresponding magnetic permeabilities, i.e.: - - .. math:: - \boldsymbol{\mu}(\boldsymbol{\mu_r}) = \mu_0 \boldsymbol{\mu_r}, - - the derivative of the mapping with respect to the model is the identity - matrix scaled by the permeability of free-space. Thus: - - .. math:: - \frac{\partial \boldsymbol{\mu}}{\partial \boldsymbol{\mu_r}} = \mu_0 \mathbf{I} - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. If the - input argument *v* is not ``None``, the method returns the derivative times - the vector *v*. - """ - if v is not None: - return mu_0 * v - return mu_0 * sp.eye(self.nP) - - def inverse(self, m): - r"""Apply the inverse mapping to an array. - - For the ``MuRelative`` class, the inverse mapping recoveres the set of - relative permeabilities :math:`\boldsymbol{\mu_r}` from a set of - magnetic permeabilities :math:`\boldsymbol{\mu}`. Thus the inverse - mapping is defined as: - - .. math:: - \boldsymbol{\mu_r}(\boldsymbol{\mu}) = \frac{\boldsymbol{\mu}}{\mu_0} - - where :math:`\mu_0` is the permeability of free space. - - Parameters - ---------- - D : numpy.ndarray - A set of input values - - Returns - ------- - numpy.ndarray - A :class:`numpy.ndarray` containing result of applying the - inverse mapping to the elements in *D*; which in this case - represents the conversion of magnetic permeabilities - to their corresponding relative permeability values. - """ - return 1.0 / mu_0 * m - - -class Weighting(IdentityMap): - r"""Mapping that scales the elements of the model by a corresponding set of weights. - - Where :math:`\mathbf{m}` defines the set of input model parameters and - :math:`\mathbf{w}` represents a corresponding set of model weight, - ``Weighting`` constructs a mapping :math:`\mathbf{u}(\mathbf{m})` of the form: - - .. math:: - \mathbf{u}(\mathbf{m}) = \mathbf{w} \odot \mathbf{m} - - where :math:`\odot` is the Hadamard product. The mapping may also be - defined using a linear operator as follows: - - .. math:: - \mathbf{u}(\mathbf{m}) = \mathbf{Pm} \;\;\;\;\; \textrm{where} \;\;\;\;\; \mathbf{P} = diag(\mathbf{w}) - - Parameters - ---------- - mesh : discretize.BaseMesh - The number of parameters accepted by the mapping is set to equal the number - of mesh cells. - nP : int - Set the number of parameters accepted by the mapping directly. Used if the - number of parameters is known. Used generally when the number of parameters - is not equal to the number of cells in a mesh. - weights : (nP) numpy.ndarray - A set of independent model weights. If ``None``, all model weights are set - to *1*. - """ - - def __init__(self, mesh=None, nP=None, weights=None, **kwargs): - if "nC" in kwargs: - raise TypeError( - "`nC` has been removed. Use `nP` to set the number of model " - "parameters." - ) - - super(Weighting, self).__init__(mesh=mesh, nP=nP, **kwargs) - - if weights is None: - weights = np.ones(self.nP) - - self.weights = np.array(weights, dtype=float) - - @property - def shape(self): - """Dimensions of the mapping. - - Returns - ------- - tuple - Dimensions of the mapping. Where *nP* is the number of parameters - the mapping acts on, this method returns a tuple of the form - (*nP*, *nP*). - """ - return (self.nP, self.nP) - - @property - def P(self): - r"""The linear mapping operator - - This property returns the sparse matrix :math:`\mathbf{P}` that carries - out the weighting mapping via matrix-vector product, i.e.: - - .. math:: - \mathbf{u}(\mathbf{m}) = \mathbf{Pm} \;\;\;\;\; \textrm{where} \;\;\;\;\; \mathbf{P} = diag(\mathbf{w}) - - Returns - ------- - scipy.sparse.csr_matrix - Sparse linear mapping operator - """ - return sdiag(self.weights) - - def _transform(self, m): - return self.weights * m - - def inverse(self, D): - r"""Apply the inverse of the weighting mapping to an array. - - For the weighting mapping :math:`\mathbf{u}(\mathbf{m})`, the inverse - mapping on a variable :math:`\mathbf{x}` is performed by multplying each element by - the reciprocal of its corresponding weighting value, i.e.: - - .. math:: - \mathbf{m} = \mathbf{u}^{-1}(\mathbf{x}) = \mathbf{w}^{-1} \odot \mathbf{x} - - where :math:`\odot` is the Hadamard product. The inverse mapping may also be defined - using a linear operator as follows: - - .. math:: - \mathbf{m} = \mathbf{u}^{-1}(\mathbf{x}) = \mathbf{P^{-1} m} - \;\;\;\;\; \textrm{where} \;\;\;\;\; \mathbf{P} = diag(\mathbf{w}) - - Parameters - ---------- - D : numpy.ndarray - A set of input values - - Returns - ------- - numpy.ndarray - A :class:`numpy.ndarray` containing result of applying the - inverse mapping to the elements in *D*; which in this case - is simply dividing each element by its corresponding - weight. - """ - return self.weights ** (-1.0) * D - - def deriv(self, m, v=None): - r"""Derivative of mapping with respect to the input parameters. - - For a weighting mapping :math:`\mathbf{u}(\mathbf{m})` that scales the - input parameters in the model :math:`\mathbf{m}` by their corresponding - weights :math:`\mathbf{w}`; i.e.: - - .. math:: - \mathbf{u}(\mathbf{m}) = \mathbf{w} \dot \mathbf{m}, - - the derivative of the mapping with respect to the model is a diagonal - matrix of the form: - - .. math:: - \frac{\partial \mathbf{u}}{\partial \mathbf{m}} - = diag (\mathbf{w}) - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. If the - input argument *v* is not ``None``, the method returns the derivative times - the vector *v*. - """ - if v is not None: - return self.weights * v - return self.P - - -class ComplexMap(IdentityMap): - r"""Maps the real and imaginary component values stored in a model to complex values. - - Let :math:`\mathbf{m}` be a model which stores the real and imaginary components of - a set of complex values :math:`\mathbf{z}`. Where the model parameters are organized - into a vector of the form - :math:`\mathbf{m} = [\mathbf{z}^\prime , \mathbf{z}^{\prime\prime}]`, ``ComplexMap`` - constructs the following mapping: - - .. math:: - \mathbf{z}(\mathbf{m}) = \mathbf{z}^\prime + j \mathbf{z}^{\prime\prime} - - Note that the mapping is :math:`\mathbb{R}^{2n} \rightarrow \mathbb{C}^n`. - - Parameters - ---------- - mesh : discretize.BaseMesh - If a mesh is used to construct the mapping, the number of input model - parameters is *2\*mesh.nC* and the number of complex values output from - the mapping is equal to *mesh.nC*. If *mesh* is ``None``, the dimensions - of the mapping are set using the *nP* input argument. - nP : int - Defines the number of input model parameters directly. Must be an even number!!! - In this case, the number of complex values output from the mapping is *nP/2*. - If *nP* = ``None``, the dimensions of the mapping are set using the *mesh* - input argument. - - Examples - -------- - Here we construct a complex mapping on a 1D mesh comprised - of 4 cells. The input model is real-valued array of length 8 - (4 real and 4 imaginary values). The output of the mapping - is a complex array with 4 values. - - >>> from simpeg.maps import ComplexMap - >>> from discretize import TensorMesh - >>> import numpy as np - - >>> nC = 4 - >>> mesh = TensorMesh([np.ones(nC)]) - - >>> z_real = np.ones(nC) - >>> z_imag = 2*np.ones(nC) - >>> m = np.r_[z_real, z_imag] - >>> m - array([1., 1., 1., 1., 2., 2., 2., 2.]) - - >>> mapping = ComplexMap(mesh=mesh) - >>> z = mapping * m - >>> z - array([1.+2.j, 1.+2.j, 1.+2.j, 1.+2.j]) - - """ - - def __init__(self, mesh=None, nP=None, **kwargs): - super().__init__(mesh=mesh, nP=nP, **kwargs) - if nP is not None and mesh is not None: - assert ( - 2 * mesh.nC == nP - ), "Number parameters must be 2 X number of mesh cells." - if nP is not None: - assert nP % 2 == 0, "nP must be even." - self._nP = nP or int(self.mesh.nC * 2) - - @property - def nP(self): - r"""Number of parameters the mapping acts on. - - Returns - ------- - int or '*' - Number of parameters that the mapping acts on. - """ - return self._nP - - @property - def shape(self): - """Dimensions of the mapping - - Returns - ------- - tuple - The dimensions of the mapping. Where *nP* is the number - of input parameters, this property returns a tuple - (*nP/2*, *nP*). - """ - return (int(self.nP / 2), self.nP) - - def _transform(self, m): - nC = int(self.nP / 2) - return m[:nC] + m[nC:] * 1j - - def deriv(self, m, v=None): - r"""Derivative of the complex mapping with respect to the input parameters. - - The complex mapping maps the real and imaginary components stored in a model - of the form :math:`\mathbf{m} = [\mathbf{z}^\prime , \mathbf{z}^{\prime\prime}]` - to their corresponding complex values :math:`\mathbf{z}`, i.e. - - .. math:: - \mathbf{z}(\mathbf{m}) = \mathbf{z}^\prime + j \mathbf{z}^{\prime\prime} - - The derivative of the mapping with respect to the model is block - matrix of the form: - - .. math:: - \frac{\partial \mathbf{z}}{\partial \mathbf{m}} = \big ( \mathbf{I} \;\;\; j\mathbf{I} \big ) - - where :math:`\mathbf{I}` is the identity matrix of shape (*nP/2*, *nP/2*) and - :math:`j = \sqrt{-1}`. - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. If the - input argument *v* is not ``None``, the method returns the derivative times - the vector *v*. - - Examples - -------- - Here we construct the derivative operator for the complex mapping on a 1D - mesh comprised of 4 cells. We then demonstrate how the derivative of the - mapping and its adjoint can be applied to a vector. - - >>> from simpeg.maps import ComplexMap - >>> from discretize import TensorMesh - >>> import numpy as np - - >>> nC = 4 - >>> mesh = TensorMesh([np.ones(nC)]) - - >>> m = np.random.rand(2*nC) - >>> mapping = ComplexMap(mesh=mesh) - >>> M = mapping.deriv(m) - - When applying the derivative operator to a vector, it will convert - the real and imaginary values stored in the vector to - complex values; essentially applying the mapping. - - >>> v1 = np.arange(0, 2*nC, 1) - >>> u1 = M * v1 - >>> u1 - array([0.+4.j, 1.+5.j, 2.+6.j, 3.+7.j]) - - When applying the adjoint of the derivative operator to a set of - complex values, the operator will decompose these values into - their real and imaginary components. - - >>> v2 = np.arange(0, nC, 1) + 1j*np.arange(nC, 2*nC, 1) - >>> u2 = M.adjoint() * v2 - >>> u2 - array([0., 1., 2., 3., 4., 5., 6., 7.]) - - """ - nC = self.shape[0] - shp = (nC, nC * 2) - - def fwd(v): - return v[:nC] + v[nC:] * 1j - - def adj(v): - return np.r_[v.real, v.imag] - - if v is not None: - return LinearOperator(shp, matvec=fwd, rmatvec=adj) * v - return LinearOperator(shp, matvec=fwd, rmatvec=adj) - - # inverse = deriv - - -############################################################################### -# # -# Surjection, Injection and Interpolation Maps # -# # -############################################################################### - - -class SurjectFull(IdentityMap): - r"""Mapping a single property value to all mesh cells. - - Let :math:`m` be a model defined by a single physical property value - ``SurjectFull`` construct a surjective mapping that projects :math:`m` - to the set of voxel cells defining a mesh. The mapping - :math:`\mathbf{u(m)}` is a matrix of 1s of shape (*mesh.nC* , 1) that - projects the model to all mesh cells, i.e.: - - .. math:: - \mathbf{u}(\mathbf{m}) = \mathbf{Pm} - - Parameters - ---------- - mesh : discretize.BaseMesh - A discretize mesh - - """ - - def __init__(self, mesh, **kwargs): - super().__init__(mesh=mesh, **kwargs) - - @property - def nP(self): - r"""Number of parameters the mapping acts on; i.e. 1. - - Returns - ------- - int - Returns an integer value of 1 - """ - return 1 - - def _transform(self, m): - """ - :param m: model (scalar) - :rtype: numpy.ndarray - :return: transformed model - """ - return np.ones(self.mesh.nC) * m - - def deriv(self, m, v=None): - r"""Derivative of the mapping with respect to the input parameters. - - Let :math:`m` be the single parameter that the mapping acts on. The - ``SurjectFull`` class constructs a mapping that can be defined as - a projection matrix :math:`\mathbf{P}`; i.e.: - - .. math:: - \mathbf{u} = \mathbf{P m}, - - the **deriv** method returns the derivative of :math:`\mathbf{u}` with respect - to the model parameters; i.e.: - - .. math:: - \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = \mathbf{P} - - Note that in this case, **deriv** simply returns the original operator - :math:`\mathbf{P}`; a (*mesh.nC* , 1) numpy.ndarray of 1s. - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - """ - deriv = sp.csr_matrix(np.ones([self.mesh.nC, 1])) - if v is not None: - return deriv * v - return deriv - - -class SurjectVertical1D(IdentityMap): - r"""Map 1D layered Earth model to 2D or 3D tensor mesh. - - Let :math:`m` be a 1D model that defines the property values along - the last dimension of a tensor mesh; i.e. the y-direction for 2D - meshes and the z-direction for 3D meshes. ``SurjectVertical1D`` - construct a surjective mapping from the 1D model to all voxel cells - in the 2D or 3D tensor mesh provided. - - Mathematically, the mapping :math:`\mathbf{u}(\mathbf{m})` can be - represented by a projection matrix: - - .. math:: - \mathbf{u}(\mathbf{m}) = \mathbf{Pm} - - Parameters - ---------- - mesh : discretize.TensorMesh - A 2D or 3D tensor mesh - - Examples - -------- - Here we define a 1D layered Earth model comprised of 3 layers - on a 1D tensor mesh. We then use ``SurjectVertical1D`` to - construct a mapping which projects the 1D model onto a 2D - tensor mesh. - - >>> from simpeg.maps import SurjectVertical1D - >>> from simpeg.utils import plot_1d_layer_model - >>> from discretize import TensorMesh - >>> import numpy as np - >>> import matplotlib as mpl - >>> import matplotlib.pyplot as plt - - >>> dh = np.ones(20) - >>> mesh1D = TensorMesh([dh], 'C') - >>> mesh2D = TensorMesh([dh, dh], 'CC') - - >>> m = np.zeros(mesh1D.nC) - >>> m[mesh1D.cell_centers < 0] = 10. - >>> m[mesh1D.cell_centers < -5] = 5. - - >>> fig1 = plt.figure(figsize=(5,5)) - >>> ax1 = fig1.add_subplot(111) - >>> plot_1d_layer_model( - >>> mesh1D.h[0], np.flip(m), ax=ax1, z0=0, - >>> scale='linear', show_layers=True, plot_elevation=True - >>> ) - >>> ax1.set_xlim([-0.1, 11]) - >>> ax1.set_title('1D Model') - - >>> mapping = SurjectVertical1D(mesh2D) - >>> u = mapping * m - - >>> fig2 = plt.figure(figsize=(6, 5)) - >>> ax2a = fig2.add_axes([0.1, 0.15, 0.7, 0.8]) - >>> mesh2D.plot_image(u, ax=ax2a, grid=True) - >>> ax2a.set_title('Projected to 2D Mesh') - >>> ax2b = fig2.add_axes([0.83, 0.15, 0.05, 0.8]) - >>> norm = mpl.colors.Normalize(vmin=np.min(m), vmax=np.max(m)) - >>> cbar = mpl.colorbar.ColorbarBase(ax2b, norm=norm, orientation="vertical") - - """ - - def __init__(self, mesh, **kwargs): - assert isinstance( - mesh, (TensorMesh, CylindricalMesh) - ), "Only implemented for tensor meshes" - super().__init__(mesh=mesh, **kwargs) - - @property - def nP(self): - r"""Number of parameters the mapping acts on. - - Returns - ------- - int - Number of parameters the mapping acts on. Should equal the - number of cells along the last dimension of the tensor mesh - supplied when defining the mapping. - """ - return int(self.mesh.vnC[self.mesh.dim - 1]) - - def _transform(self, m): - repNum = np.prod(self.mesh.vnC[: self.mesh.dim - 1]) - return mkvc(m).repeat(repNum) - - def deriv(self, m, v=None): - r"""Derivative of the mapping with respect to the model paramters. - - Let :math:`\mathbf{m}` be a set of parameter values for the 1D model - and let :math:`\mathbf{P}` be a projection matrix that maps the 1D - model the 2D/3D tensor mesh. The forward mapping :math:`\mathbf{u}(\mathbf{m})` - is given by: - - .. math:: - \mathbf{u} = \mathbf{P m}, - - the **deriv** method returns the derivative of :math:`\mathbf{u}` with respect - to the model parameters; i.e.: - - .. math:: - \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = \mathbf{P} - - Note that in this case, **deriv** simply returns the projection matrix. - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. If the - input argument *v* is not ``None``, the method returns the derivative times - the vector *v*. - """ - repNum = np.prod(self.mesh.vnC[: self.mesh.dim - 1]) - repVec = sp.csr_matrix( - (np.ones(repNum), (range(repNum), np.zeros(repNum))), shape=(repNum, 1) - ) - deriv = sp.kron(sp.identity(self.nP), repVec) - if v is not None: - return deriv * v - return deriv - - -class Surject2Dto3D(IdentityMap): - r"""Map 2D tensor model to 3D tensor mesh. - - Let :math:`m` define the parameters for a 2D tensor model. - ``Surject2Dto3D`` constructs a surjective mapping that projects - the 2D tensor model to a 3D tensor mesh. - - Mathematically, the mapping :math:`\mathbf{u}(\mathbf{m})` can be - represented by a projection matrix: - - .. math:: - \mathbf{u}(\mathbf{m}) = \mathbf{Pm} - - Parameters - ---------- - mesh : discretize.TensorMesh - A 3D tensor mesh - normal : {'y', 'x', 'z'} - Define the projection axis. - - Examples - -------- - Here we project a 3 layered Earth model defined on a 2D tensor mesh - to a 3D tensor mesh. We assume that at for some y-location, we - have a 2D tensor model which defines the physical property distribution - as a function of the *x* and *z* location. Using ``Surject2Dto3D``, - we project the model along the y-axis to obtain a 3D distribution - for the physical property (i.e. a 3D tensor model). - - >>> from simpeg.maps import Surject2Dto3D - >>> from discretize import TensorMesh - >>> import numpy as np - >>> import matplotlib as mpl - >>> import matplotlib.pyplot as plt - - >>> dh = np.ones(20) - >>> mesh2D = TensorMesh([dh, dh], 'CC') - >>> mesh3D = TensorMesh([dh, dh, dh], 'CCC') - - Here, we define the 2D tensor model. - - >>> m = np.zeros(mesh2D.nC) - >>> m[mesh2D.cell_centers[:, 1] < 0] = 10. - >>> m[mesh2D.cell_centers[:, 1] < -5] = 5. - - We then plot the 2D tensor model; which is defined along the - x and z axes. - - >>> fig1 = plt.figure(figsize=(6, 5)) - >>> ax11 = fig1.add_axes([0.1, 0.15, 0.7, 0.8]) - >>> mesh2D.plot_image(m, ax=ax11, grid=True) - >>> ax11.set_ylabel('z') - >>> ax11.set_title('2D Tensor Model') - >>> ax12 = fig1.add_axes([0.83, 0.15, 0.05, 0.8]) - >>> norm1 = mpl.colors.Normalize(vmin=np.min(m), vmax=np.max(m)) - >>> cbar1 = mpl.colorbar.ColorbarBase(ax12, norm=norm1, orientation="vertical") - - By setting *normal = 'Y'* we are projecting along the y-axis. - - >>> mapping = Surject2Dto3D(mesh3D, normal='Y') - >>> u = mapping * m - - Finally we plot a slice of the resulting 3D tensor model. - - >>> fig2 = plt.figure(figsize=(6, 5)) - >>> ax21 = fig2.add_axes([0.1, 0.15, 0.7, 0.8]) - >>> mesh3D.plot_slice(u, ax=ax21, ind=10, normal='Y', grid=True) - >>> ax21.set_ylabel('z') - >>> ax21.set_title('Projected to 3D Mesh (y=0)') - >>> ax22 = fig2.add_axes([0.83, 0.15, 0.05, 0.8]) - >>> norm2 = mpl.colors.Normalize(vmin=np.min(m), vmax=np.max(m)) - >>> cbar2 = mpl.colorbar.ColorbarBase(ax22, norm=norm2, orientation="vertical") - - """ - - def __init__(self, mesh, normal="y", **kwargs): - self.normal = normal - super().__init__(mesh=mesh, **kwargs) - - @IdentityMap.mesh.setter - def mesh(self, value): - value = validate_type("mesh", value, discretize.TensorMesh, cast=False) - if value.dim != 3: - raise ValueError("Surject2Dto3D Only works for a 3D Mesh") - self._mesh = value - - @property - def normal(self): - """The projection axis. - - Returns - ------- - str - """ - return self._normal - - @normal.setter - def normal(self, value): - self._normal = validate_string("normal", value, ("x", "y", "z")) - - @property - def nP(self): - """Number of model properties. - - The number of cells in the - last dimension of the mesh.""" - if self.normal == "z": - return self.mesh.shape_cells[0] * self.mesh.shape_cells[1] - elif self.normal == "y": - return self.mesh.shape_cells[0] * self.mesh.shape_cells[2] - elif self.normal == "x": - return self.mesh.shape_cells[1] * self.mesh.shape_cells[2] - - def _transform(self, m): - m = mkvc(m) - if self.normal == "z": - return mkvc( - m.reshape(self.mesh.vnC[:2], order="F")[:, :, np.newaxis].repeat( - self.mesh.shape_cells[2], axis=2 - ) - ) - elif self.normal == "y": - return mkvc( - m.reshape(self.mesh.vnC[::2], order="F")[:, np.newaxis, :].repeat( - self.mesh.shape_cells[1], axis=1 - ) - ) - elif self.normal == "x": - return mkvc( - m.reshape(self.mesh.vnC[1:], order="F")[np.newaxis, :, :].repeat( - self.mesh.shape_cells[0], axis=0 - ) - ) - - def deriv(self, m, v=None): - r"""Derivative of the mapping with respect to the model paramters. - - Let :math:`\mathbf{m}` be a set of parameter values for the 2D tensor model - and let :math:`\mathbf{P}` be a projection matrix that maps the 2D tensor model - to the 3D tensor mesh. The forward mapping :math:`\mathbf{u}(\mathbf{m})` - is given by: - - .. math:: - \mathbf{u} = \mathbf{P m}, - - the **deriv** method returns the derivative of :math:`\mathbf{u}` with respect - to the model parameters; i.e.: - - .. math:: - \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = \mathbf{P} - - Note that in this case, **deriv** simply returns the projection matrix. - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. If the - input argument *v* is not ``None``, the method returns the derivative times - the vector *v*. - """ - inds = self * np.arange(self.nP) - nC, nP = self.mesh.nC, self.nP - P = sp.csr_matrix((np.ones(nC), (range(nC), inds)), shape=(nC, nP)) - if v is not None: - return P * v - return P - - -class Mesh2Mesh(IdentityMap): - """ - Takes a model on one mesh are translates it to another mesh. - """ - - def __init__(self, meshes, indActive=None, **kwargs): - # Sanity checks for the meshes parameter - try: - mesh, mesh2 = meshes - except TypeError: - raise TypeError("Couldn't unpack 'meshes' into two meshes.") - - super().__init__(mesh=mesh, **kwargs) - - self.mesh2 = mesh2 - # Check dimensions of both meshes - if mesh.dim != mesh2.dim: - raise ValueError( - f"Found meshes with dimensions '{mesh.dim}' and '{mesh2.dim}'. " - + "Both meshes must have the same dimension." - ) - self.indActive = indActive - - # reset to not accepted None for mesh - @IdentityMap.mesh.setter - def mesh(self, value): - self._mesh = validate_type("mesh", value, discretize.base.BaseMesh, cast=False) - - @property - def mesh2(self): - """The source mesh used for the mapping. - - Returns - ------- - discretize.base.BaseMesh - """ - return self._mesh2 - - @mesh2.setter - def mesh2(self, value): - self._mesh2 = validate_type( - "mesh2", value, discretize.base.BaseMesh, cast=False - ) - - @property - def indActive(self): - """Active indices on target mesh. - - Returns - ------- - (mesh.n_cells) numpy.ndarray of bool or none - """ - return self._indActive - - @indActive.setter - def indActive(self, value): - if value is not None: - value = validate_active_indices("indActive", value, self.mesh.n_cells) - self._indActive = value - - @property - def P(self): - if getattr(self, "_P", None) is None: - self._P = self.mesh2.get_interpolation_matrix( - ( - self.mesh.cell_centers[self.indActive, :] - if self.indActive is not None - else self.mesh.cell_centers - ), - "CC", - zeros_outside=True, - ) - return self._P - - @property - def shape(self): - """Number of parameters in the model.""" - if self.indActive is not None: - return (self.indActive.sum(), self.mesh2.nC) - return (self.mesh.nC, self.mesh2.nC) - - @property - def nP(self): - """Number of parameters in the model.""" - return self.mesh2.nC - - def _transform(self, m): - return self.P * m - - def deriv(self, m, v=None): - if v is not None: - return self.P * v - return self.P - - -class InjectActiveCells(IdentityMap): - r"""Map active cells model to all cell of a mesh. - - The ``InjectActiveCells`` class is used to define the mapping when - the model consists of physical property values for a set of active - mesh cells; e.g. cells below topography. For a discrete set of - model parameters :math:`\mathbf{m}` defined on a set of active - cells, the mapping :math:`\mathbf{u}(\mathbf{m})` is defined as: - - .. math:: - \mathbf{u}(\mathbf{m}) = \mathbf{Pm} + \mathbf{d}\, m_\perp - - where :math:`\mathbf{P}` is a (*nC* , *nP*) projection matrix from - active cells to all mesh cells, and :math:`\mathbf{d}` is a - (*nC* , 1) matrix that projects the inactive cell value - :math:`m_\perp` to all inactive mesh cells. - - Parameters - ---------- - mesh : discretize.BaseMesh - A discretize mesh - indActive : numpy.ndarray - Active cells array. Can be a boolean ``numpy.ndarray`` of length *mesh.nC* - or a ``numpy.ndarray`` of ``int`` containing the indices of the active cells. - valInactive : float or numpy.ndarray - The physical property value assigned to all inactive cells in the mesh - - """ - - def __init__(self, mesh, indActive=None, valInactive=0.0, nC=None): - self.mesh = mesh - self.nC = nC or mesh.nC - - self._indActive = validate_active_indices("indActive", indActive, self.nC) - self._nP = np.sum(self.indActive) - - self.P = sp.eye(self.nC, format="csr")[:, self.indActive] - - self.valInactive = valInactive - - @property - def valInactive(self): - """The physical property value assigned to all inactive cells in the mesh. - - Returns - ------- - numpy.ndarray - """ - return self._valInactive - - @valInactive.setter - def valInactive(self, value): - n_inactive = self.nC - self.nP - try: - value = validate_float("valInactive", value) - value = np.full(n_inactive, value) - except Exception: - pass - value = validate_ndarray_with_shape("valInactive", value, shape=(n_inactive,)) - - self._valInactive = np.zeros(self.nC, dtype=float) - self._valInactive[~self.indActive] = value - - @property - def indActive(self): - """ - - Returns - ------- - numpy.ndarray of bool - - """ - return self._indActive - - @property - def shape(self): - """Dimensions of the mapping - - Returns - ------- - tuple of int - Where *nP* is the number of active cells and *nC* is - number of cell in the mesh, **shape** returns a - tuple (*nC* , *nP*). - """ - return (self.nC, self.nP) - - @property - def nP(self): - """Number of parameters the model acts on. - - Returns - ------- - int - Number of parameters the model acts on; i.e. the number of active cells - """ - return int(self.indActive.sum()) - - def _transform(self, m): - if m.ndim > 1: - return self.P * m + self.valInactive[:, None] - return self.P * m + self.valInactive - - def inverse(self, u): - r"""Recover the model parameters (active cells) from a set of physical - property values defined on the entire mesh. - - For a discrete set of model parameters :math:`\mathbf{m}` defined - on a set of active cells, the mapping :math:`\mathbf{u}(\mathbf{m})` - is defined as: - - .. math:: - \mathbf{u}(\mathbf{m}) = \mathbf{Pm} + \mathbf{d} \,m_\perp - - where :math:`\mathbf{P}` is a (*nC* , *nP*) projection matrix from - active cells to all mesh cells, and :math:`\mathbf{d}` is a - (*nC* , 1) matrix that projects the inactive cell value - :math:`m_\perp` to all inactive mesh cells. - - The inverse mapping is given by: - - .. math:: - \mathbf{m}(\mathbf{u}) = \mathbf{P^T u} - - Parameters - ---------- - u : (mesh.nC) numpy.ndarray - A vector which contains physical property values for all - mesh cells. - """ - return self.P.T * u - - def deriv(self, m, v=None): - r"""Derivative of the mapping with respect to the input parameters. - - For a discrete set of model parameters :math:`\mathbf{m}` defined - on a set of active cells, the mapping :math:`\mathbf{u}(\mathbf{m})` - is defined as: - - .. math:: - \mathbf{u}(\mathbf{m}) = \mathbf{Pm} + \mathbf{d} \, m_\perp - - where :math:`\mathbf{P}` is a (*nC* , *nP*) projection matrix from - active cells to all mesh cells, and :math:`\mathbf{d}` is a - (*nC* , 1) matrix that projects the inactive cell value - :math:`m_\perp` to all inactive mesh cells. - - the **deriv** method returns the derivative of :math:`\mathbf{u}` with respect - to the model parameters; i.e.: - - .. math:: - \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = \mathbf{P} - - Note that in this case, **deriv** simply returns a sparse projection matrix. - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. If the - input argument *v* is not ``None``, the method returns the derivative times - the vector *v*. - """ - if v is not None: - return self.P * v - return self.P - - -############################################################################### -# # -# Parametric Maps # -# # -############################################################################### - - -class ParametricCircleMap(IdentityMap): - r"""Mapping for a parameterized circle. - - Define the mapping from a parameterized model for a circle in a wholespace - to all cells within a 2D mesh. For a circle within a wholespace, the - model is defined by 5 parameters: the background physical property value - (:math:`\sigma_0`), the physical property value for the circle - (:math:`\sigma_c`), the x location :math:`x_0` and y location :math:`y_0` - for center of the circle, and the circle's radius (:math:`R`). - - Let :math:`\mathbf{m} = [\sigma_0, \sigma_1, x_0, y_0, R]` be the set of - model parameters the defines a circle within a wholespace. The mapping - :math:`\mathbf{u}(\mathbf{m})` from the parameterized model to all cells - within a 2D mesh is given by: - - .. math:: - - \mathbf{u}(\mathbf{m}) = \sigma_0 + (\sigma_1 - \sigma_0) - \bigg [ \frac{1}{2} + \pi^{-1} \arctan \bigg ( a \big [ \sqrt{(\mathbf{x_c}-x_0)^2 + - (\mathbf{y_c}-y_0)^2} - R \big ] \bigg ) \bigg ] - - where :math:`\mathbf{x_c}` and :math:`\mathbf{y_c}` are vectors storing - the x and y positions of all cell centers for the 2D mesh and :math:`a` - is a user-defined constant which defines the sharpness of boundary of the - circular structure. - - Parameters - ---------- - mesh : discretize.BaseMesh - A 2D discretize mesh - logSigma : bool - If ``True``, parameters :math:`\sigma_0` and :math:`\sigma_1` represent the - natural log of the physical property values for the background and circle, - respectively. - slope : float - A constant for defining the sharpness of the boundary between the circle - and the wholespace. The sharpness increases as *slope* is increased. - - Examples - -------- - Here we define the parameterized model for a circle in a wholespace. We then - create and use a ``ParametricCircleMap`` to map the model to a 2D mesh. - - >>> from simpeg.maps import ParametricCircleMap - >>> from discretize import TensorMesh - >>> import numpy as np - >>> import matplotlib.pyplot as plt - - >>> h = 0.5*np.ones(20) - >>> mesh = TensorMesh([h, h]) - - >>> sigma0, sigma1, x0, y0, R = 0., 10., 4., 6., 2. - >>> model = np.r_[sigma0, sigma1, x0, y0, R] - >>> mapping = ParametricCircleMap(mesh, logSigma=False, slope=2) - - >>> fig = plt.figure(figsize=(5, 5)) - >>> ax = fig.add_subplot(111) - >>> mesh.plot_image(mapping * model, ax=ax) - - """ - - def __init__(self, mesh, logSigma=True, slope=0.1): - super().__init__(mesh=mesh) - if mesh.dim != 2: - raise NotImplementedError( - "Mesh must be 2D, not implemented yet for other dimensions." - ) - # TODO: this should be done through a composition with and ExpMap - self.logSigma = logSigma - self.slope = slope - - @property - def slope(self): - """Sharpness of the boundary. - - Larger number are sharper. - - Returns - ------- - float - """ - return self._slope - - @slope.setter - def slope(self, value): - self._slope = validate_float("slope", value, min_val=0.0, inclusive_min=False) - - @property - def logSigma(self): - """Whether the input needs to be transformed by an exponential - - Returns - ------- - float - """ - return self._logSigma - - @logSigma.setter - def logSigma(self, value): - self._logSigma = validate_type("logSigma", value, bool) - - @property - def nP(self): - r"""Number of parameters the mapping acts on; i.e. 5. - - Returns - ------- - int - The ``ParametricCircleMap`` acts on 5 parameters. - """ - return 5 - - def _transform(self, m): - a = self.slope - sig1, sig2, x, y, r = m[0], m[1], m[2], m[3], m[4] - if self.logSigma: - sig1, sig2 = np.exp(sig1), np.exp(sig2) - X = self.mesh.cell_centers[:, 0] - Y = self.mesh.cell_centers[:, 1] - return sig1 + (sig2 - sig1) * ( - np.arctan(a * (np.sqrt((X - x) ** 2 + (Y - y) ** 2) - r)) / np.pi + 0.5 - ) - - def deriv(self, m, v=None): - r"""Derivative of the mapping with respect to the input parameters. - - Let :math:`\mathbf{m} = [\sigma_0, \sigma_1, x_0, y_0, R]` be the set of - model parameters the defines a circle within a wholespace. The mapping - :math:`\mathbf{u}(\mathbf{m})`from the parameterized model to all cells - within a 2D mesh is given by: - - .. math:: - \mathbf{u}(\mathbf{m}) = \sigma_0 + (\sigma_1 - \sigma_0) - \bigg [ \frac{1}{2} + \pi^{-1} \arctan \bigg ( a \big [ \sqrt{(\mathbf{x_c}-x_0)^2 + - (\mathbf{y_c}-y_0)^2} - R \big ] \bigg ) \bigg ] - - The derivative of the mapping with respect to the model parameters is a - ``numpy.ndarray`` of shape (*mesh.nC*, 5) given by: - - .. math:: - \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = - \Bigg [ \frac{\partial \mathbf{u}}{\partial \sigma_0} \;\; - \Bigg [ \frac{\partial \mathbf{u}}{\partial \sigma_1} \;\; - \Bigg [ \frac{\partial \mathbf{u}}{\partial x_0} \;\; - \Bigg [ \frac{\partial \mathbf{u}}{\partial y_0} \;\; - \Bigg [ \frac{\partial \mathbf{u}}{\partial R} - \Bigg ] - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. If the - input argument *v* is not ``None``, the method returns the derivative times - the vector *v*. - """ - a = self.slope - sig1, sig2, x, y, r = m[0], m[1], m[2], m[3], m[4] - if self.logSigma: - sig1, sig2 = np.exp(sig1), np.exp(sig2) - X = self.mesh.cell_centers[:, 0] - Y = self.mesh.cell_centers[:, 1] - if self.logSigma: - g1 = ( - -( - np.arctan(a * (-r + np.sqrt((X - x) ** 2 + (Y - y) ** 2))) / np.pi - + 0.5 - ) - * sig1 - + sig1 - ) - g2 = ( - np.arctan(a * (-r + np.sqrt((X - x) ** 2 + (Y - y) ** 2))) / np.pi + 0.5 - ) * sig2 - else: - g1 = ( - -( - np.arctan(a * (-r + np.sqrt((X - x) ** 2 + (Y - y) ** 2))) / np.pi - + 0.5 - ) - + 1.0 - ) - g2 = ( - np.arctan(a * (-r + np.sqrt((X - x) ** 2 + (Y - y) ** 2))) / np.pi + 0.5 - ) - - g3 = ( - a - * (-X + x) - * (-sig1 + sig2) - / ( - np.pi - * (a**2 * (-r + np.sqrt((X - x) ** 2 + (Y - y) ** 2)) ** 2 + 1) - * np.sqrt((X - x) ** 2 + (Y - y) ** 2) - ) - ) - - g4 = ( - a - * (-Y + y) - * (-sig1 + sig2) - / ( - np.pi - * (a**2 * (-r + np.sqrt((X - x) ** 2 + (Y - y) ** 2)) ** 2 + 1) - * np.sqrt((X - x) ** 2 + (Y - y) ** 2) - ) - ) - - g5 = ( - -a - * (-sig1 + sig2) - / (np.pi * (a**2 * (-r + np.sqrt((X - x) ** 2 + (Y - y) ** 2)) ** 2 + 1)) - ) - - if v is not None: - return sp.csr_matrix(np.c_[g1, g2, g3, g4, g5]) * v - return sp.csr_matrix(np.c_[g1, g2, g3, g4, g5]) - - @property - def is_linear(self): - return False - - -class ParametricPolyMap(IdentityMap): - r"""Mapping for 2 layer model whose interface is defined by a polynomial. - - This mapping is used when the cells lying below the Earth's surface can - be parameterized by a 2 layer model whose interface is defined by a - polynomial function. The model is defined by the physical property - values for each unit (:math:`\sigma_1` and :math:`\sigma_2`) and the - coefficients for the polynomial function (:math:`\mathbf{c}`). - - **For a 2D mesh** , the interface is defined by a polynomial function - of the form: - - .. math:: - p(x) = \sum_{i=0}^N c_i x^i - - where :math:`c_i` are the polynomial coefficients and :math:`N` is - the order of the polynomial. In this case, the model is defined as - - .. math:: - \mathbf{m} = [\sigma_1, \;\sigma_2,\; c_0 ,\;\ldots\; ,\; c_N] - - The mapping :math:`\mathbf{u}(\mathbf{m})` from the model to the mesh - is given by: - - .. math:: - - \mathbf{u}(\mathbf{m}) = \sigma_1 + (\sigma_2 - \sigma_1) - \bigg [ \frac{1}{2} + \pi^{-1} \arctan \bigg ( - a \Big ( \mathbf{p}(\mathbf{x_c}) - \mathbf{y_c} \Big ) - \bigg ) \bigg ] - - where :math:`\mathbf{x_c}` and :math:`\mathbf{y_c}` are vectors containing the - x and y cell center locations for all active cells in the mesh, and :math:`a` is a - parameter which defines the sharpness of the boundary between the two layers. - :math:`\mathbf{p}(\mathbf{x_c})` evaluates the polynomial function for - every element in :math:`\mathbf{x_c}`. - - **For a 3D mesh** , the interface is defined by a 2D polynomial function - of the form: - - .. math:: - p(x,y) = - \sum_{j=0}^{N_y} \sum_{i=0}^{N_x} c_{ij} \, x^i y^j - - where :math:`c_{ij}` are the polynomial coefficients. :math:`N_x` - and :math:`N_y` define the order of the polynomial in :math:`x` and - :math:`y`, respectively. In this case, the model is defined as: - - .. math:: - \mathbf{m} = [\sigma_1, \; \sigma_2, \; c_{0,0} , \; c_{1,0} , \;\ldots , \; c_{N_x, N_y}] - - The mapping :math:`\mathbf{u}(\mathbf{m})` from the model to the mesh - is given by: - - .. math:: - - \mathbf{u}(\mathbf{m}) = \sigma_1 + (\sigma_2 - \sigma_1) - \bigg [ \frac{1}{2} + \pi^{-1} \arctan \bigg ( - a \Big ( \mathbf{p}(\mathbf{x_c,y_c}) - \mathbf{z_c} \Big ) - \bigg ) \bigg ] - - where :math:`\mathbf{x_c}, \mathbf{y_c}` and :math:`\mathbf{y_z}` are vectors - containing the x, y and z cell center locations for all active cells in the mesh. - :math:`\mathbf{p}(\mathbf{x_c, y_c})` evaluates the polynomial function for - every corresponding pair of :math:`\mathbf{x_c}` and :math:`\mathbf{y_c}` - elements. - - Parameters - ---------- - mesh : discretize.BaseMesh - A discretize mesh - order : int or list of int - Order of the polynomial. For a 2D mesh, this is an ``int``. For a 3D - mesh, the order for both variables is entered separately; i.e. - [*order1* , *order2*]. - logSigma : bool - If ``True``, parameters :math:`\sigma_1` and :math:`\sigma_2` represent - the natural log of a physical property. - normal : {'x', 'y', 'z'} - actInd : numpy.ndarray - Active cells array. Can be a boolean ``numpy.ndarray`` of length *mesh.nC* - or a ``numpy.ndarray`` of ``int`` containing the indices of the active cells. - - Examples - -------- - In this example, we define a 2 layer model whose interface is sharp and lies - along a polynomial function :math:`y(x)=c_0 + c_1 x`. In this case, the model is - defined as :math:`\mathbf{m} = [\sigma_1 , \sigma_2 , c_0 , c_1]`. We construct - a polynomial mapping from the model to the set of active cells (i.e. below the surface), - We then use an active cells mapping to map from the set of active cells to all - cells in the 2D mesh. - - >>> from simpeg.maps import ParametricPolyMap, InjectActiveCells - >>> from discretize import TensorMesh - >>> import numpy as np - >>> import matplotlib.pyplot as plt - - >>> h = 0.5*np.ones(20) - >>> mesh = TensorMesh([h, h]) - >>> ind_active = mesh.cell_centers[:, 1] < 8 - >>> - >>> sig1, sig2, c0, c1 = 10., 5., 2., 0.5 - >>> model = np.r_[sig1, sig2, c0, c1] - - >>> poly_map = ParametricPolyMap( - >>> mesh, order=1, logSigma=False, normal='Y', actInd=ind_active, slope=1e4 - >>> ) - >>> act_map = InjectActiveCells(mesh, ind_active, 0.) - - >>> fig = plt.figure(figsize=(5, 5)) - >>> ax = fig.add_subplot(111) - >>> mesh.plot_image(act_map * poly_map * model, ax=ax) - >>> ax.set_title('Mapping on a 2D mesh') - - Here, we recreate the previous example on a 3D mesh but with a smoother interface. - For a 3D mesh, the 2D polynomial defining the sloping interface is given by - :math:`z(x,y) = c_0 + c_x x + c_y y + c_{xy} xy`. In this case, the model is - defined as :math:`\mathbf{m} = [\sigma_1 , \sigma_2 , c_0 , c_x, c_y, c_{xy}]`. - - >>> mesh = TensorMesh([h, h, h]) - >>> ind_active = mesh.cell_centers[:, 2] < 8 - >>> - >>> sig1, sig2, c0, cx, cy, cxy = 10., 5., 2., 0.5, 0., 0. - >>> model = np.r_[sig1, sig2, c0, cx, cy, cxy] - >>> - >>> poly_map = ParametricPolyMap( - >>> mesh, order=[1, 1], logSigma=False, normal='Z', actInd=ind_active, slope=2 - >>> ) - >>> act_map = InjectActiveCells(mesh, ind_active, 0.) - >>> - >>> fig = plt.figure(figsize=(5, 5)) - >>> ax = fig.add_subplot(111) - >>> mesh.plot_slice(act_map * poly_map * model, ax=ax, normal='Y', ind=10) - >>> ax.set_title('Mapping on a 3D mesh') - - """ - - def __init__(self, mesh, order, logSigma=True, normal="X", actInd=None, slope=1e4): - super().__init__(mesh=mesh) - self.logSigma = logSigma - self.order = order - self.normal = normal - self.slope = slope - - if actInd is None: - actInd = np.ones(mesh.n_cells, dtype=bool) - self.actInd = actInd - - @property - def slope(self): - """Sharpness of the boundary. - - Larger number are sharper. - - Returns - ------- - float - """ - return self._slope - - @slope.setter - def slope(self, value): - self._slope = validate_float("slope", value, min_val=0.0, inclusive_min=False) - - @property - def logSigma(self): - """Whether the input needs to be transformed by an exponential - - Returns - ------- - float - """ - return self._logSigma - - @logSigma.setter - def logSigma(self, value): - self._logSigma = validate_type("logSigma", value, bool) - - @property - def normal(self): - """The projection axis. - - Returns - ------- - str - """ - return self._normal - - @normal.setter - def normal(self, value): - self._normal = validate_string("normal", value, ("x", "y", "z")) - - @property - def actInd(self): - """Active indices of the mesh. - - Returns - ------- - (mesh.n_cells) numpy.ndarray of bool - """ - return self._actInd - - @actInd.setter - def actInd(self, value): - self._actInd = validate_active_indices("actInd", value, self.mesh.n_cells) - self._nC = sum(self._actInd) - - @property - def shape(self): - """Dimensions of the mapping. - - Returns - ------- - tuple of int - The dimensions of the mapping as a tuple of the form - (*nC* , *nP*), where *nP* is the number of model parameters - the mapping acts on and *nC* is the number of active cells - being mapping to. If *actInd* is ``None``, then - *nC = mesh.nC*. - """ - return (self.nC, self.nP) - - @property - def nC(self): - """Number of active cells being mapped too. - - Returns - ------- - int - """ - return self._nC - - @property - def nP(self): - """Number of parameters the mapping acts on. - - Returns - ------- - int - The number of parameters the mapping acts on. - """ - if np.isscalar(self.order): - nP = self.order + 3 - else: - nP = (self.order[0] + 1) * (self.order[1] + 1) + 2 - return nP - - def _transform(self, m): - # Set model parameters - alpha = self.slope - sig1, sig2 = m[0], m[1] - c = m[2:] - if self.logSigma: - sig1, sig2 = np.exp(sig1), np.exp(sig2) - - # 2D - if self.mesh.dim == 2: - X = self.mesh.cell_centers[self.actInd, 0] - Y = self.mesh.cell_centers[self.actInd, 1] - if self.normal == "x": - f = polynomial.polyval(Y, c) - X - elif self.normal == "y": - f = polynomial.polyval(X, c) - Y - else: - raise (Exception("Input for normal = X or Y or Z")) - - # 3D - elif self.mesh.dim == 3: - X = self.mesh.cell_centers[self.actInd, 0] - Y = self.mesh.cell_centers[self.actInd, 1] - Z = self.mesh.cell_centers[self.actInd, 2] - - if self.normal == "x": - f = ( - polynomial.polyval2d( - Y, - Z, - c.reshape((self.order[0] + 1, self.order[1] + 1), order="F"), - ) - - X - ) - elif self.normal == "y": - f = ( - polynomial.polyval2d( - X, - Z, - c.reshape((self.order[0] + 1, self.order[1] + 1), order="F"), - ) - - Y - ) - elif self.normal == "z": - f = ( - polynomial.polyval2d( - X, - Y, - c.reshape((self.order[0] + 1, self.order[1] + 1), order="F"), - ) - - Z - ) - else: - raise (Exception("Input for normal = X or Y or Z")) - - else: - raise (Exception("Only supports 2D or 3D")) - - return sig1 + (sig2 - sig1) * (np.arctan(alpha * f) / np.pi + 0.5) - - def deriv(self, m, v=None): - r"""Derivative of the mapping with respect to the model. - - For a model :math:`\mathbf{m} = [\sigma_1, \sigma_2, \mathbf{c}]`, - the derivative of the mapping with respect to the model parameters is a - ``numpy.ndarray`` of shape (*mesh.nC*, *nP*) of the form: - - .. math:: - \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = - \Bigg [ \frac{\partial \mathbf{u}}{\partial \sigma_0} \;\; - \Bigg [ \frac{\partial \mathbf{u}}{\partial \sigma_1} \;\; - \Bigg [ \frac{\partial \mathbf{u}}{\partial c_0} \;\; - \Bigg [ \frac{\partial \mathbf{u}}{\partial c_1} \;\; - \cdots \;\; - \Bigg [ \frac{\partial \mathbf{u}}{\partial c_N} - \Bigg ] - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. If the - input argument *v* is not ``None``, the method returns the derivative times - the vector *v*. - - """ - alpha = self.slope - sig1, sig2, c = m[0], m[1], m[2:] - if self.logSigma: - sig1, sig2 = np.exp(sig1), np.exp(sig2) - - # 2D - if self.mesh.dim == 2: - X = self.mesh.cell_centers[self.actInd, 0] - Y = self.mesh.cell_centers[self.actInd, 1] - - if self.normal == "x": - f = polynomial.polyval(Y, c) - X - V = polynomial.polyvander(Y, len(c) - 1) - elif self.normal == "y": - f = polynomial.polyval(X, c) - Y - V = polynomial.polyvander(X, len(c) - 1) - else: - raise (Exception("Input for normal = X or Y")) - - # 3D - elif self.mesh.dim == 3: - X = self.mesh.cell_centers[self.actInd, 0] - Y = self.mesh.cell_centers[self.actInd, 1] - Z = self.mesh.cell_centers[self.actInd, 2] - - if self.normal == "x": - f = ( - polynomial.polyval2d( - Y, Z, c.reshape((self.order[0] + 1, self.order[1] + 1)) - ) - - X - ) - V = polynomial.polyvander2d(Y, Z, self.order) - elif self.normal == "y": - f = ( - polynomial.polyval2d( - X, Z, c.reshape((self.order[0] + 1, self.order[1] + 1)) - ) - - Y - ) - V = polynomial.polyvander2d(X, Z, self.order) - elif self.normal == "z": - f = ( - polynomial.polyval2d( - X, Y, c.reshape((self.order[0] + 1, self.order[1] + 1)) - ) - - Z - ) - V = polynomial.polyvander2d(X, Y, self.order) - else: - raise (Exception("Input for normal = X or Y or Z")) - - if self.logSigma: - g1 = -(np.arctan(alpha * f) / np.pi + 0.5) * sig1 + sig1 - g2 = (np.arctan(alpha * f) / np.pi + 0.5) * sig2 - else: - g1 = -(np.arctan(alpha * f) / np.pi + 0.5) + 1.0 - g2 = np.arctan(alpha * f) / np.pi + 0.5 - - g3 = sdiag(alpha * (sig2 - sig1) / (1.0 + (alpha * f) ** 2) / np.pi) * V - - if v is not None: - return sp.csr_matrix(np.c_[g1, g2, g3]) * v - return sp.csr_matrix(np.c_[g1, g2, g3]) - - @property - def is_linear(self): - return False - - -class ParametricSplineMap(IdentityMap): - r"""Mapping to parameterize the boundary between two geological units using - spline interpolation. - - .. math:: - - g = f(x)-y - - Define the model as: - - .. math:: - - m = [\sigma_1, \sigma_2, y] - - Parameters - ---------- - mesh : discretize.BaseMesh - A discretize mesh - pts : (n) numpy.ndarray - Points for the 1D spline tie points. - ptsv : (2) array_like - Points for linear interpolation between two splines in 3D. - order : int - Order of the spline mapping; e.g. 3 is cubic spline - logSigma : bool - If ``True``, :math:`\sigma_1` and :math:`\sigma_2` represent the natural - log of some physical property value for each unit. - normal : {'x', 'y', 'z'} - Defines the general direction of the normal vector for the interface. - slope : float - Parameter for defining the sharpness of the boundary. The sharpness is increased - if *slope* is large. - - Examples - -------- - In this example, we define a 2 layered model with a sloping - interface on a 2D mesh. The model consists of the physical - property values for the layers and the known elevations - for the interface at the horizontal positions supplied when - creating the mapping. - - >>> from simpeg.maps import ParametricSplineMap - >>> from discretize import TensorMesh - >>> import numpy as np - >>> import matplotlib.pyplot as plt - - >>> h = 0.5*np.ones(20) - >>> mesh = TensorMesh([h, h]) - - >>> x = np.linspace(0, 10, 6) - >>> y = 0.5*x + 2.5 - - >>> model = np.r_[10., 0., y] - >>> mapping = ParametricSplineMap(mesh, x, order=2, normal='Y', slope=2) - - >>> fig = plt.figure(figsize=(5, 5)) - >>> ax = fig.add_subplot(111) - >>> mesh.plot_image(mapping * model, ax=ax) - - """ - - def __init__( - self, mesh, pts, ptsv=None, order=3, logSigma=True, normal="x", slope=1e4 - ): - super().__init__(mesh=mesh) - self.slope = slope - self.logSigma = logSigma - self.normal = normal - self.order = order - self.pts = pts - self.ptsv = ptsv - self.spl = None - - @IdentityMap.mesh.setter - def mesh(self, value): - self._mesh = validate_type( - "mesh", value, discretize.base.BaseTensorMesh, cast=False - ) - - @property - def slope(self): - """Sharpness of the boundary. - - Larger number are sharper. - - Returns - ------- - float - """ - return self._slope - - @slope.setter - def slope(self, value): - self._slope = validate_float("slope", value, min_val=0.0, inclusive_min=False) - - @property - def logSigma(self): - """Whether the input needs to be transformed by an exponential - - Returns - ------- - float - """ - return self._logSigma - - @logSigma.setter - def logSigma(self, value): - self._logSigma = validate_type("logSigma", value, bool) - - @property - def normal(self): - """The projection axis. - - Returns - ------- - str - """ - return self._normal - - @normal.setter - def normal(self, value): - self._normal = validate_string("normal", value, ("x", "y", "z")) - - @property - def order(self): - """Order of the spline mapping. - - Returns - ------- - int - """ - return self._order - - @order.setter - def order(self, value): - self._order = validate_integer("order", value, min_val=1) - - @property - def pts(self): - """Points for the spline. - - Returns - ------- - numpy.ndarray - """ - return self._pts - - @pts.setter - def pts(self, value): - self._pts = validate_ndarray_with_shape("pts", value, shape=("*"), dtype=float) - - @property - def npts(self): - """The number of points. - - Returns - ------- - int - """ - return self._pts.shape[0] - - @property - def ptsv(self): - """Bottom and top values for the 3D spline surface. - - In 3D, two splines are created and linearly interpolated between these two - points. - - Returns - ------- - (2) numpy.ndarray - """ - return self._ptsv - - @ptsv.setter - def ptsv(self, value): - if value is not None: - value = validate_ndarray_with_shape("ptsv", value, shape=(2,)) - self._ptsv = value - - @property - def nP(self): - r"""Number of parameters the mapping acts on - - Returns - ------- - int - Number of parameters the mapping acts on. - - **2D mesh:** the mapping acts on *mesh.nC + 2* parameters - - **3D mesh:** the mapping acts on *2\*mesh.nC + 2* parameters - """ - if self.mesh.dim == 2: - return np.size(self.pts) + 2 - elif self.mesh.dim == 3: - return np.size(self.pts) * 2 + 2 - else: - raise (Exception("Only supports 2D and 3D")) - - def _transform(self, m): - # Set model parameters - alpha = self.slope - sig1, sig2 = m[0], m[1] - c = m[2:] - if self.logSigma: - sig1, sig2 = np.exp(sig1), np.exp(sig2) - # 2D - if self.mesh.dim == 2: - X = self.mesh.cell_centers[:, 0] - Y = self.mesh.cell_centers[:, 1] - self.spl = UnivariateSpline(self.pts, c, k=self.order, s=0) - if self.normal == "x": - f = self.spl(Y) - X - elif self.normal == "y": - f = self.spl(X) - Y - else: - raise (Exception("Input for normal = X or Y or Z")) - - # 3D: - # Comments: - # Make two spline functions and link them using linear interpolation. - # This is not quite direct extension of 2D to 3D case - # Using 2D interpolation is possible - - elif self.mesh.dim == 3: - X = self.mesh.cell_centers[:, 0] - Y = self.mesh.cell_centers[:, 1] - Z = self.mesh.cell_centers[:, 2] - - npts = np.size(self.pts) - if np.mod(c.size, 2): - raise (Exception("Put even points!")) - - self.spl = { - "splb": UnivariateSpline(self.pts, c[:npts], k=self.order, s=0), - "splt": UnivariateSpline(self.pts, c[npts:], k=self.order, s=0), - } - - if self.normal == "x": - zb = self.ptsv[0] - zt = self.ptsv[1] - flines = (self.spl["splt"](Y) - self.spl["splb"](Y)) * (Z - zb) / ( - zt - zb - ) + self.spl["splb"](Y) - f = flines - X - # elif self.normal =='Y': - # elif self.normal =='Z': - else: - raise (Exception("Input for normal = X or Y or Z")) - else: - raise (Exception("Only supports 2D and 3D")) - - return sig1 + (sig2 - sig1) * (np.arctan(alpha * f) / np.pi + 0.5) - - def deriv(self, m, v=None): - alpha = self.slope - sig1, sig2, c = m[0], m[1], m[2:] - if self.logSigma: - sig1, sig2 = np.exp(sig1), np.exp(sig2) - # 2D - if self.mesh.dim == 2: - X = self.mesh.cell_centers[:, 0] - Y = self.mesh.cell_centers[:, 1] - - if self.normal == "x": - f = self.spl(Y) - X - elif self.normal == "y": - f = self.spl(X) - Y - else: - raise (Exception("Input for normal = X or Y or Z")) - # 3D - elif self.mesh.dim == 3: - X = self.mesh.cell_centers[:, 0] - Y = self.mesh.cell_centers[:, 1] - Z = self.mesh.cell_centers[:, 2] - - if self.normal == "x": - zb = self.ptsv[0] - zt = self.ptsv[1] - flines = (self.spl["splt"](Y) - self.spl["splb"](Y)) * (Z - zb) / ( - zt - zb - ) + self.spl["splb"](Y) - f = flines - X - # elif self.normal =='Y': - # elif self.normal =='Z': - else: - raise (Exception("Not Implemented for Y and Z, your turn :)")) - - if self.logSigma: - g1 = -(np.arctan(alpha * f) / np.pi + 0.5) * sig1 + sig1 - g2 = (np.arctan(alpha * f) / np.pi + 0.5) * sig2 - else: - g1 = -(np.arctan(alpha * f) / np.pi + 0.5) + 1.0 - g2 = np.arctan(alpha * f) / np.pi + 0.5 - - if self.mesh.dim == 2: - g3 = np.zeros((self.mesh.nC, self.npts)) - if self.normal == "y": - # Here we use perturbation to compute sensitivity - # TODO: bit more generalization of this ... - # Modfications for X and Z directions ... - for i in range(np.size(self.pts)): - ctemp = c[i] - ind = np.argmin(abs(self.mesh.cell_centers_y - ctemp)) - ca = c.copy() - cb = c.copy() - dy = self.mesh.h[1][ind] * 1.5 - ca[i] = ctemp + dy - cb[i] = ctemp - dy - spla = UnivariateSpline(self.pts, ca, k=self.order, s=0) - splb = UnivariateSpline(self.pts, cb, k=self.order, s=0) - fderiv = (spla(X) - splb(X)) / (2 * dy) - g3[:, i] = ( - sdiag(alpha * (sig2 - sig1) / (1.0 + (alpha * f) ** 2) / np.pi) - * fderiv - ) - - elif self.mesh.dim == 3: - g3 = np.zeros((self.mesh.nC, self.npts * 2)) - if self.normal == "x": - # Here we use perturbation to compute sensitivity - for i in range(self.npts * 2): - ctemp = c[i] - ind = np.argmin(abs(self.mesh.cell_centers_y - ctemp)) - ca = c.copy() - cb = c.copy() - dy = self.mesh.h[1][ind] * 1.5 - ca[i] = ctemp + dy - cb[i] = ctemp - dy - - # treat bottom boundary - if i < self.npts: - splba = UnivariateSpline( - self.pts, ca[: self.npts], k=self.order, s=0 - ) - splbb = UnivariateSpline( - self.pts, cb[: self.npts], k=self.order, s=0 - ) - flinesa = ( - (self.spl["splt"](Y) - splba(Y)) * (Z - zb) / (zt - zb) - + splba(Y) - - X - ) - flinesb = ( - (self.spl["splt"](Y) - splbb(Y)) * (Z - zb) / (zt - zb) - + splbb(Y) - - X - ) - - # treat top boundary - else: - splta = UnivariateSpline( - self.pts, ca[self.npts :], k=self.order, s=0 - ) - spltb = UnivariateSpline( - self.pts, ca[self.npts :], k=self.order, s=0 - ) - flinesa = ( - (self.spl["splt"](Y) - splta(Y)) * (Z - zb) / (zt - zb) - + splta(Y) - - X - ) - flinesb = ( - (self.spl["splt"](Y) - spltb(Y)) * (Z - zb) / (zt - zb) - + spltb(Y) - - X - ) - fderiv = (flinesa - flinesb) / (2 * dy) - g3[:, i] = ( - sdiag(alpha * (sig2 - sig1) / (1.0 + (alpha * f) ** 2) / np.pi) - * fderiv - ) - else: - raise (Exception("Not Implemented for Y and Z, your turn :)")) - - if v is not None: - return sp.csr_matrix(np.c_[g1, g2, g3]) * v - return sp.csr_matrix(np.c_[g1, g2, g3]) - - @property - def is_linear(self): - return False - - -class BaseParametric(IdentityMap): - """Base class for parametric mappings from simple geological structures to meshes. - - Parameters - ---------- - mesh : discretize.BaseMesh - A discretize mesh - indActive : numpy.ndarray, optional - Active cells array. Can be a boolean ``numpy.ndarray`` of length *mesh.nC* - or a ``numpy.ndarray`` of ``int`` containing the indices of the active cells. - slope : float, optional - Directly set the scaling parameter *slope* which sets the sharpness of boundaries - between units. - slopeFact : float, optional - Set sharpness of boundaries between units based on minimum cell size. If set, - the scalaing parameter *slope = slopeFact / dh*. - - """ - - def __init__(self, mesh, slope=None, slopeFact=1.0, indActive=None, **kwargs): - super(BaseParametric, self).__init__(mesh, **kwargs) - self.indActive = indActive - self.slopeFact = slopeFact - if slope is not None: - self.slope = slope - - @property - def slope(self): - """Defines the sharpness of the boundaries. - - Returns - ------- - float - """ - return self._slope - - @slope.setter - def slope(self, value): - self._slope = validate_float("slope", value, min_val=0.0) - - @property - def slopeFact(self): - """Defines the slope scaled by the mesh. - - Returns - ------- - float - """ - return self._slopeFact - - @slopeFact.setter - def slopeFact(self, value): - self._slopeFact = validate_float("slopeFact", value, min_val=0.0) - self.slope = self._slopeFact / self.mesh.edge_lengths.min() - - @property - def indActive(self): - return self._indActive - - @indActive.setter - def indActive(self, value): - if value is not None: - value = validate_active_indices("indActive", value, self.mesh.n_cells) - self._indActive = value - - @property - def x(self): - """X cell center locations (active) for the output of the mapping. - - Returns - ------- - (n_active) numpy.ndarray - X cell center locations (active) for the output of the mapping. - """ - if getattr(self, "_x", None) is None: - if self.mesh.dim == 1: - self._x = [ - ( - self.mesh.cell_centers - if self.indActive is None - else self.mesh.cell_centers[self.indActive] - ) - ][0] - else: - self._x = [ - ( - self.mesh.cell_centers[:, 0] - if self.indActive is None - else self.mesh.cell_centers[self.indActive, 0] - ) - ][0] - return self._x - - @property - def y(self): - """Y cell center locations (active) for the output of the mapping. - - Returns - ------- - (n_active) numpy.ndarray - Y cell center locations (active) for the output of the mapping. - """ - if getattr(self, "_y", None) is None: - if self.mesh.dim > 1: - self._y = [ - ( - self.mesh.cell_centers[:, 1] - if self.indActive is None - else self.mesh.cell_centers[self.indActive, 1] - ) - ][0] - else: - self._y = None - return self._y - - @property - def z(self): - """Z cell center locations (active) for the output of the mapping. - - Returns - ------- - (n_active) numpy.ndarray - Z cell center locations (active) for the output of the mapping. - """ - if getattr(self, "_z", None) is None: - if self.mesh.dim > 2: - self._z = [ - ( - self.mesh.cell_centers[:, 2] - if self.indActive is None - else self.mesh.cell_centers[self.indActive, 2] - ) - ][0] - else: - self._z = None - return self._z - - def _atanfct(self, val, slope): - return np.arctan(slope * val) / np.pi + 0.5 - - def _atanfctDeriv(self, val, slope): - # d/dx(atan(x)) = 1/(1+x**2) - x = slope * val - dx = -slope - return (1.0 / (1 + x**2)) / np.pi * dx - - @property - def is_linear(self): - return False - - -class ParametricLayer(BaseParametric): - r"""Mapping for a horizontal layer within a wholespace. - - This mapping is used when the cells lying below the Earth's surface can - be parameterized by horizontal layer within a homogeneous medium. - The model is defined by the physical property value for the background - (:math:`\sigma_0`), the physical property value for the layer - (:math:`\sigma_1`), the elevation for the middle of the layer (:math:`z_L`) - and the thickness of the layer :math:`h`. - - For this mapping, the set of input model parameters are organized: - - .. math:: - \mathbf{m} = [\sigma_0, \;\sigma_1,\; z_L , \; h] - - The mapping :math:`\mathbf{u}(\mathbf{m})` from the model to the mesh - is given by: - - .. math:: - - \mathbf{u}(\mathbf{m}) = \sigma_0 + \frac{(\sigma_1 - \sigma_0)}{\pi} \Bigg [ - \arctan \Bigg ( a \bigg ( \mathbf{z_c} - z_L + \frac{h}{2} \bigg ) \Bigg ) - - \arctan \Bigg ( a \bigg ( \mathbf{z_c} - z_L - \frac{h}{2} \bigg ) \Bigg ) \Bigg ] - - where :math:`\mathbf{z_c}` is a vectors containing the vertical cell center - locations for all active cells in the mesh, and :math:`a` is a - parameter which defines the sharpness of the boundaries between the layer - and the background. - - Parameters - ---------- - mesh : discretize.BaseMesh - A discretize mesh - indActive : numpy.ndarray - Active cells array. Can be a boolean ``numpy.ndarray`` of length *mesh.nC* - or a ``numpy.ndarray`` of ``int`` containing the indices of the active cells. - slope : float - Directly define the constant *a* in the mapping function which defines the - sharpness of the boundaries. - slopeFact : float - Scaling factor for the sharpness of the boundaries based on cell size. - Using this option, we set *a = slopeFact / dh*. - - Examples - -------- - In this example, we define a layer in a wholespace whose interface is sharp. - We construct the mapping from the model to the set of active cells - (i.e. below the surface), We then use an active cells mapping to map from - the set of active cells to all cells in the mesh. - - >>> from simpeg.maps import ParametricLayer, InjectActiveCells - >>> from discretize import TensorMesh - >>> import numpy as np - >>> import matplotlib.pyplot as plt - - >>> dh = 0.25*np.ones(40) - >>> mesh = TensorMesh([dh, dh]) - >>> ind_active = mesh.cell_centers[:, 1] < 8 - - >>> sig0, sig1, zL, h = 5., 10., 4., 2 - >>> model = np.r_[sig0, sig1, zL, h] - - >>> layer_map = ParametricLayer( - >>> mesh, indActive=ind_active, slope=4 - >>> ) - >>> act_map = InjectActiveCells(mesh, ind_active, 0.) - - >>> fig = plt.figure(figsize=(5, 5)) - >>> ax = fig.add_subplot(111) - >>> mesh.plot_image(act_map * layer_map * model, ax=ax) - - """ - - def __init__(self, mesh, **kwargs): - super().__init__(mesh, **kwargs) - - @property - def nP(self): - """Number of model parameters the mapping acts on; i.e 4 - - Returns - ------- - int - Returns an integer value of *4*. - """ - return 4 - - @property - def shape(self): - """Dimensions of the mapping - - Returns - ------- - tuple of int - Where *nP=4* is the number of parameters the mapping acts on - and *nAct* is the number of active cells in the mesh, **shape** - returns a tuple (*nAct* , *4*). - """ - if self.indActive is not None: - return (sum(self.indActive), self.nP) - return (self.mesh.nC, self.nP) - - def mDict(self, m): - r"""Return model parameters as a dictionary. - - For a model :math:`\mathbf{m} = [\sigma_0, \;\sigma_1,\; z_L , \; h]`, - **mDict** returns a dictionary:: - - {"val_background": m[0], "val_layer": m[1], "layer_center": m[2], "layer_thickness": m[3]} - - Returns - ------- - dict - The model as a dictionary - """ - return { - "val_background": m[0], - "val_layer": m[1], - "layer_center": m[2], - "layer_thickness": m[3], - } - - def _atanLayer(self, mDict): - if self.mesh.dim == 2: - z = self.y - elif self.mesh.dim == 3: - z = self.z - - layer_bottom = mDict["layer_center"] - mDict["layer_thickness"] / 2.0 - layer_top = mDict["layer_center"] + mDict["layer_thickness"] / 2.0 - - return self._atanfct(z - layer_bottom, self.slope) * self._atanfct( - z - layer_top, -self.slope - ) - - def _atanLayerDeriv_layer_center(self, mDict): - if self.mesh.dim == 2: - z = self.y - elif self.mesh.dim == 3: - z = self.z - - layer_bottom = mDict["layer_center"] - mDict["layer_thickness"] / 2.0 - layer_top = mDict["layer_center"] + mDict["layer_thickness"] / 2.0 - - return self._atanfctDeriv(z - layer_bottom, self.slope) * self._atanfct( - z - layer_top, -self.slope - ) + self._atanfct(z - layer_bottom, self.slope) * self._atanfctDeriv( - z - layer_top, -self.slope - ) - - def _atanLayerDeriv_layer_thickness(self, mDict): - if self.mesh.dim == 2: - z = self.y - elif self.mesh.dim == 3: - z = self.z - - layer_bottom = mDict["layer_center"] - mDict["layer_thickness"] / 2.0 - layer_top = mDict["layer_center"] + mDict["layer_thickness"] / 2.0 - - return -0.5 * self._atanfctDeriv(z - layer_bottom, self.slope) * self._atanfct( - z - layer_top, -self.slope - ) + 0.5 * self._atanfct(z - layer_bottom, self.slope) * self._atanfctDeriv( - z - layer_top, -self.slope - ) - - def layer_cont(self, mDict): - return mDict["val_background"] + ( - mDict["val_layer"] - mDict["val_background"] - ) * self._atanLayer(mDict) - - def _transform(self, m): - mDict = self.mDict(m) - return self.layer_cont(mDict) - - def _deriv_val_background(self, mDict): - return np.ones_like(self.x) - self._atanLayer(mDict) - - def _deriv_val_layer(self, mDict): - return self._atanLayer(mDict) - - def _deriv_layer_center(self, mDict): - return ( - mDict["val_layer"] - mDict["val_background"] - ) * self._atanLayerDeriv_layer_center(mDict) - - def _deriv_layer_thickness(self, mDict): - return ( - mDict["val_layer"] - mDict["val_background"] - ) * self._atanLayerDeriv_layer_thickness(mDict) - - def deriv(self, m): - r"""Derivative of the mapping with respect to the input parameters. - - Let :math:`\mathbf{m} = [\sigma_0, \;\sigma_1,\; z_L , \; h]` be the set of - model parameters the defines a layer within a wholespace. The mapping - :math:`\mathbf{u}(\mathbf{m})`from the parameterized model to all - active cells is given by: - - .. math:: - \mathbf{u}(\mathbf{m}) = \sigma_0 + \frac{(\sigma_1 - \sigma_0)}{\pi} \Bigg [ - \arctan \Bigg ( a \bigg ( \mathbf{z_c} - z_L + \frac{h}{2} \bigg ) \Bigg ) - - \arctan \Bigg ( a \bigg ( \mathbf{z_c} - z_L - \frac{h}{2} \bigg ) \Bigg ) \Bigg ] - - where :math:`\mathbf{z_c}` is a vectors containing the vertical cell center - locations for all active cells in the mesh. The derivative of the mapping - with respect to the model parameters is a ``numpy.ndarray`` of - shape (*nAct*, *4*) given by: - - .. math:: - \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = - \Bigg [ \frac{\partial \mathbf{u}}{\partial \sigma_0} \;\; - \frac{\partial \mathbf{u}}{\partial \sigma_1} \;\; - \frac{\partial \mathbf{u}}{\partial z_L} \;\; - \frac{\partial \mathbf{u}}{\partial h} - \Bigg ] - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. If the - input argument *v* is not ``None``, the method returns the derivative times - the vector *v*. - """ - - mDict = self.mDict(m) - - return sp.csr_matrix( - np.vstack( - [ - self._deriv_val_background(mDict), - self._deriv_val_layer(mDict), - self._deriv_layer_center(mDict), - self._deriv_layer_thickness(mDict), - ] - ).T - ) - - -class ParametricBlock(BaseParametric): - r"""Mapping for a rectangular block within a wholespace. - - This mapping is used when the cells lying below the Earth's surface can - be parameterized by rectangular block within a homogeneous medium. - The model is defined by the physical property value for the background - (:math:`\sigma_0`), the physical property value for the block - (:math:`\sigma_b`), parameters for the center of the block - (:math:`x_b [,y_b, z_b]`) and parameters for the dimensions along - each Cartesian direction (:math:`dx [,dy, dz]`) - - For this mapping, the set of input model parameters are organized: - - .. math:: - \mathbf{m} = \begin{cases} - 1D: \;\; [\sigma_0, \;\sigma_b,\; x_b , \; dx] \\ - 2D: \;\; [\sigma_0, \;\sigma_b,\; x_b , \; dx,\; y_b , \; dy] \\ - 3D: \;\; [\sigma_0, \;\sigma_b,\; x_b , \; dx,\; y_b , \; dy,\; z_b , \; dz] - \end{cases} - - The mapping :math:`\mathbf{u}(\mathbf{m})` from the model to the mesh - is given by: - - .. math:: - - \mathbf{u}(\mathbf{m}) = \sigma_0 + (\sigma_b - \sigma_0) \bigg [ \frac{1}{2} + - \pi^{-1} \arctan \bigg ( a \, \boldsymbol{\eta} \big ( - x_b, y_b, z_b, dx, dy, dz \big ) \bigg ) \bigg ] - - where *a* is a parameter that impacts the sharpness of the arctan function, and - - .. math:: - \boldsymbol{\eta} \big ( x_b, y_b, z_b, dx, dy, dz \big ) = 1 - - \sum_{\xi \in (x,y,z)} \bigg [ \bigg ( \frac{2(\boldsymbol{\xi_c} - \xi_b)}{d\xi} \bigg )^2 + \varepsilon^2 - \bigg ]^{p/2} - - Parameters :math:`p` and :math:`\varepsilon` define the parameters of the Ekblom - function. :math:`\boldsymbol{\xi_c}` is a place holder for vectors containing - the x, [y and z] cell center locations of the mesh, :math:`\xi_b` is a placeholder - for the x[, y and z] location for the center of the block, and :math:`d\xi` is a - placeholder for the x[, y and z] dimensions of the block. - - Parameters - ---------- - mesh : discretize.BaseMesh - A discretize mesh - indActive : numpy.ndarray - Active cells array. Can be a boolean ``numpy.ndarray`` of length *mesh.nC* - or a ``numpy.ndarray`` of ``int`` containing the indices of the active cells. - slope : float - Directly define the constant *a* in the mapping function which defines the - sharpness of the boundaries. - slopeFact : float - Scaling factor for the sharpness of the boundaries based on cell size. - Using this option, we set *a = slopeFact / dh*. - epsilon : float - Epsilon value used in the ekblom representation of the block - p : float - p-value used in the ekblom representation of the block. - - Examples - -------- - In this example, we define a rectangular block in a wholespace whose - interface is sharp. We construct the mapping from the model to the - set of active cells (i.e. below the surface), We then use an active - cells mapping to map from the set of active cells to all cells in the mesh. - - >>> from simpeg.maps import ParametricBlock, InjectActiveCells - >>> from discretize import TensorMesh - >>> import numpy as np - >>> import matplotlib.pyplot as plt - - >>> dh = 0.5*np.ones(20) - >>> mesh = TensorMesh([dh, dh]) - >>> ind_active = mesh.cell_centers[:, 1] < 8 - - >>> sig0, sigb, xb, Lx, yb, Ly = 5., 10., 5., 4., 4., 2. - >>> model = np.r_[sig0, sigb, xb, Lx, yb, Ly] - - >>> block_map = ParametricBlock(mesh, indActive=ind_active) - >>> act_map = InjectActiveCells(mesh, ind_active, 0.) - - >>> fig = plt.figure(figsize=(5, 5)) - >>> ax = fig.add_subplot(111) - >>> mesh.plot_image(act_map * block_map * model, ax=ax) - - """ - - def __init__(self, mesh, epsilon=1e-6, p=10, **kwargs): - self.epsilon = epsilon - self.p = p - super(ParametricBlock, self).__init__(mesh, **kwargs) - - @property - def epsilon(self): - """epsilon value used in the ekblom representation of the block. - - Returns - ------- - float - """ - return self._epsilon - - @epsilon.setter - def epsilon(self, value): - self._epsilon = validate_float("epsilon", value, min_val=0.0) - - @property - def p(self): - """p-value used in the ekblom representation of the block. - - Returns - ------- - float - """ - return self._p - - @p.setter - def p(self, value): - self._p = validate_float("p", value, min_val=0.0) - - @property - def nP(self): - """Number of parameters the mapping acts on. - - Returns - ------- - int - The number of the parameters defining the model depends on the dimension - of the mesh. *nP* - - - =4 for a 1D mesh - - =6 for a 2D mesh - - =8 for a 3D mesh - """ - if self.mesh.dim == 1: - return 4 - if self.mesh.dim == 2: - return 6 - elif self.mesh.dim == 3: - return 8 - - @property - def shape(self): - """Dimensions of the mapping - - Returns - ------- - tuple of int - Where *nP* is the number of parameters the mapping acts on - and *nAct* is the number of active cells in the mesh, **shape** - returns a tuple (*nAct* , *nP*). - """ - if self.indActive is not None: - return (sum(self.indActive), self.nP) - return (self.mesh.nC, self.nP) - - def _mDict1d(self, m): - return { - "val_background": m[0], - "val_block": m[1], - "x0": m[2], - "dx": m[3], - } - - def _mDict2d(self, m): - mDict = self._mDict1d(m) - mDict.update( - { - # 'theta_x': m[4], - "y0": m[4], - "dy": m[5], - # 'theta_y': m[7] - } - ) - return mDict - - def _mDict3d(self, m): - mDict = self._mDict2d(m) - mDict.update( - { - "z0": m[6], - "dz": m[7], - # 'theta_z': m[10] - } - ) - return mDict - - def mDict(self, m): - r"""Return model parameters as a dictionary. - - Returns - ------- - dict - The model as a dictionary - """ - return getattr(self, "_mDict{}d".format(self.mesh.dim))(m) - - def _ekblom(self, val): - return (val**2 + self.epsilon**2) ** (self.p / 2.0) - - def _ekblomDeriv(self, val): - return (self.p / 2) * (val**2 + self.epsilon**2) ** ((self.p / 2) - 1) * 2 * val - - # def _rotation(self, mDict): - # if self.mesh.dim == 2: - - # elif self.mesh.dim == 3: - - def _block1D(self, mDict): - return 1 - (self._ekblom((self.x - mDict["x0"]) / (0.5 * mDict["dx"]))) - - def _block2D(self, mDict): - return 1 - ( - self._ekblom((self.x - mDict["x0"]) / (0.5 * mDict["dx"])) - + self._ekblom((self.y - mDict["y0"]) / (0.5 * mDict["dy"])) - ) - - def _block3D(self, mDict): - return 1 - ( - self._ekblom((self.x - mDict["x0"]) / (0.5 * mDict["dx"])) - + self._ekblom((self.y - mDict["y0"]) / (0.5 * mDict["dy"])) - + self._ekblom((self.z - mDict["z0"]) / (0.5 * mDict["dz"])) - ) - - def _transform(self, m): - mDict = self.mDict(m) - return mDict["val_background"] + ( - mDict["val_block"] - mDict["val_background"] - ) * self._atanfct( - getattr(self, "_block{}D".format(self.mesh.dim))(mDict), slope=self.slope - ) - - def _deriv_val_background(self, mDict): - return 1 - self._atanfct( - getattr(self, "_block{}D".format(self.mesh.dim))(mDict), slope=self.slope - ) - - def _deriv_val_block(self, mDict): - return self._atanfct( - getattr(self, "_block{}D".format(self.mesh.dim))(mDict), slope=self.slope - ) - - def _deriv_center_block(self, mDict, orientation): - x = getattr(self, orientation) - x0 = mDict["{}0".format(orientation)] - dx = mDict["d{}".format(orientation)] - return (mDict["val_block"] - mDict["val_background"]) * ( - self._atanfctDeriv( - getattr(self, "_block{}D".format(self.mesh.dim))(mDict), - slope=self.slope, - ) - * (self._ekblomDeriv((x - x0) / (0.5 * dx))) - / -(0.5 * dx) - ) - - def _deriv_width_block(self, mDict, orientation): - x = getattr(self, orientation) - x0 = mDict["{}0".format(orientation)] - dx = mDict["d{}".format(orientation)] - return (mDict["val_block"] - mDict["val_background"]) * ( - self._atanfctDeriv( - getattr(self, "_block{}D".format(self.mesh.dim))(mDict), - slope=self.slope, - ) - * (self._ekblomDeriv((x - x0) / (0.5 * dx)) * (-(x - x0) / (0.5 * dx**2))) - ) - - def _deriv1D(self, mDict): - return np.vstack( - [ - self._deriv_val_background(mDict), - self._deriv_val_block(mDict), - self._deriv_center_block(mDict, "x"), - self._deriv_width_block(mDict, "x"), - ] - ).T - - def _deriv2D(self, mDict): - return np.vstack( - [ - self._deriv_val_background(mDict), - self._deriv_val_block(mDict), - self._deriv_center_block(mDict, "x"), - self._deriv_width_block(mDict, "x"), - self._deriv_center_block(mDict, "y"), - self._deriv_width_block(mDict, "y"), - ] - ).T - - def _deriv3D(self, mDict): - return np.vstack( - [ - self._deriv_val_background(mDict), - self._deriv_val_block(mDict), - self._deriv_center_block(mDict, "x"), - self._deriv_width_block(mDict, "x"), - self._deriv_center_block(mDict, "y"), - self._deriv_width_block(mDict, "y"), - self._deriv_center_block(mDict, "z"), - self._deriv_width_block(mDict, "z"), - ] - ).T - - def deriv(self, m): - r"""Derivative of the mapping with respect to the input parameters. - - Let :math:`\mathbf{m} = [\sigma_0, \;\sigma_1,\; x_b, \; dx, (\; y_b, \; dy, \; z_b , dz)]` - be the set of model parameters the defines a block/ellipsoid within a wholespace. - The mapping :math:`\mathbf{u}(\mathbf{m})` from the parameterized model to all - active cells is given by: - - The derivative of the mapping :math:`\mathbf{u}(\mathbf{m})` with respect to - the model parameters is a ``numpy.ndarray`` of shape (*nAct*, *nP*) given by: - - .. math:: - \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = \Bigg [ - \frac{\partial \mathbf{u}}{\partial \sigma_0} \;\; - \frac{\partial \mathbf{u}}{\partial \sigma_1} \;\; - \frac{\partial \mathbf{u}}{\partial x_b} \;\; - \frac{\partial \mathbf{u}}{\partial dx} \;\; - \frac{\partial \mathbf{u}}{\partial y_b} \;\; - \frac{\partial \mathbf{u}}{\partial dy} \;\; - \frac{\partial \mathbf{u}}{\partial z_b} \;\; - \frac{\partial \mathbf{u}}{\partial dz} - \Bigg ) \Bigg ] - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. If the - input argument *v* is not ``None``, the method returns the derivative times - the vector *v*. - """ - return sp.csr_matrix( - getattr(self, "_deriv{}D".format(self.mesh.dim))(self.mDict(m)) - ) - - -class ParametricEllipsoid(ParametricBlock): - r"""Mapping for a rectangular block within a wholespace. - - This mapping is used when the cells lying below the Earth's surface can - be parameterized by an ellipsoid within a homogeneous medium. - The model is defined by the physical property value for the background - (:math:`\sigma_0`), the physical property value for the layer - (:math:`\sigma_b`), parameters for the center of the ellipsoid - (:math:`x_b [,y_b, z_b]`) and parameters for the dimensions along - each Cartesian direction (:math:`dx [,dy, dz]`) - - For this mapping, the set of input model parameters are organized: - - .. math:: - \mathbf{m} = \begin{cases} - 1D: \;\; [\sigma_0, \;\sigma_b,\; x_b , \; dx] \\ - 2D: \;\; [\sigma_0, \;\sigma_b,\; x_b , \; dx,\; y_b , \; dy] \\ - 3D: \;\; [\sigma_0, \;\sigma_b,\; x_b , \; dx,\; y_b , \; dy,\; z_b , \; dz] - \end{cases} - - The mapping :math:`\mathbf{u}(\mathbf{m})` from the model to the mesh - is given by: - - .. math:: - - \mathbf{u}(\mathbf{m}) = \sigma_0 + (\sigma_b - \sigma_0) \bigg [ \frac{1}{2} + - \pi^{-1} \arctan \bigg ( a \, \boldsymbol{\eta} \big ( - x_b, y_b, z_b, dx, dy, dz \big ) \bigg ) \bigg ] - - where *a* is a parameter that impacts the sharpness of the arctan function, and - - .. math:: - \boldsymbol{\eta} \big ( x_b, y_b, z_b, dx, dy, dz \big ) = 1 - - \sum_{\xi \in (x,y,z)} \bigg [ \bigg ( \frac{2(\boldsymbol{\xi_c} - \xi_b)}{d\xi} \bigg )^2 + \varepsilon^2 - \bigg ] - - :math:`\boldsymbol{\xi_c}` is a place holder for vectors containing - the x, [y and z] cell center locations of the mesh, :math:`\xi_b` is a placeholder - for the x[, y and z] location for the center of the block, and :math:`d\xi` is a - placeholder for the x[, y and z] dimensions of the block. - - Parameters - ---------- - mesh : discretize.BaseMesh - A discretize mesh - indActive : numpy.ndarray - Active cells array. Can be a boolean ``numpy.ndarray`` of length *mesh.nC* - or a ``numpy.ndarray`` of ``int`` containing the indices of the active cells. - slope : float - Directly define the constant *a* in the mapping function which defines the - sharpness of the boundaries. - slopeFact : float - Scaling factor for the sharpness of the boundaries based on cell size. - Using this option, we set *a = slopeFact / dh*. - epsilon : float - Epsilon value used in the ekblom representation of the block - - Examples - -------- - In this example, we define an ellipse in a wholespace whose - interface is sharp. We construct the mapping from the model to the - set of active cells (i.e. below the surface), We then use an active - cells mapping to map from the set of active cells to all cells in the mesh. - - >>> from simpeg.maps import ParametricEllipsoid, InjectActiveCells - >>> from discretize import TensorMesh - >>> import numpy as np - >>> import matplotlib.pyplot as plt - - >>> dh = 0.5*np.ones(20) - >>> mesh = TensorMesh([dh, dh]) - >>> ind_active = mesh.cell_centers[:, 1] < 8 - - >>> sig0, sigb, xb, Lx, yb, Ly = 5., 10., 5., 4., 4., 3. - >>> model = np.r_[sig0, sigb, xb, Lx, yb, Ly] - - >>> ellipsoid_map = ParametricEllipsoid(mesh, indActive=ind_active) - >>> act_map = InjectActiveCells(mesh, ind_active, 0.) - - >>> fig = plt.figure(figsize=(5, 5)) - >>> ax = fig.add_subplot(111) - >>> mesh.plot_image(act_map * ellipsoid_map * model, ax=ax) - - """ - - def __init__(self, mesh, **kwargs): - super(ParametricEllipsoid, self).__init__(mesh, p=2, **kwargs) - - -class ParametricCasingAndLayer(ParametricLayer): - """ - Parametric layered space with casing. - - .. code:: python - - m = [val_background, - val_layer, - val_casing, - val_insideCasing, - layer_center, - layer_thickness, - casing_radius, - casing_thickness, - casing_bottom, - casing_top - ] - - """ - - def __init__(self, mesh, **kwargs): - assert ( - mesh._meshType == "CYL" - ), "Parametric Casing in a layer map only works for a cyl mesh." - - super().__init__(mesh, **kwargs) - - @property - def nP(self): - return 10 - - @property - def shape(self): - if self.indActive is not None: - return (sum(self.indActive), self.nP) - return (self.mesh.nC, self.nP) - - def mDict(self, m): - # m = [val_background, val_layer, val_casing, val_insideCasing, - # layer_center, layer_thickness, casing_radius, casing_thickness, - # casing_bottom, casing_top] - - return { - "val_background": m[0], - "val_layer": m[1], - "val_casing": m[2], - "val_insideCasing": m[3], - "layer_center": m[4], - "layer_thickness": m[5], - "casing_radius": m[6], - "casing_thickness": m[7], - "casing_bottom": m[8], - "casing_top": m[9], - } - - def casing_a(self, mDict): - return mDict["casing_radius"] - 0.5 * mDict["casing_thickness"] - - def casing_b(self, mDict): - return mDict["casing_radius"] + 0.5 * mDict["casing_thickness"] - - def _atanCasingLength(self, mDict): - return self._atanfct(self.z - mDict["casing_top"], -self.slope) * self._atanfct( - self.z - mDict["casing_bottom"], self.slope - ) - - def _atanCasingLengthDeriv_casing_top(self, mDict): - return self._atanfctDeriv( - self.z - mDict["casing_top"], -self.slope - ) * self._atanfct(self.z - mDict["casing_bottom"], self.slope) - - def _atanCasingLengthDeriv_casing_bottom(self, mDict): - return self._atanfct( - self.z - mDict["casing_top"], -self.slope - ) * self._atanfctDeriv(self.z - mDict["casing_bottom"], self.slope) - - def _atanInsideCasing(self, mDict): - return self._atanCasingLength(mDict) * self._atanfct( - self.x - self.casing_a(mDict), -self.slope - ) - - def _atanInsideCasingDeriv_casing_radius(self, mDict): - return self._atanCasingLength(mDict) * self._atanfctDeriv( - self.x - self.casing_a(mDict), -self.slope - ) - - def _atanInsideCasingDeriv_casing_thickness(self, mDict): - return ( - self._atanCasingLength(mDict) - * -0.5 - * self._atanfctDeriv(self.x - self.casing_a(mDict), -self.slope) - ) - - def _atanInsideCasingDeriv_casing_top(self, mDict): - return self._atanCasingLengthDeriv_casing_top(mDict) * self._atanfct( - self.x - self.casing_a(mDict), -self.slope - ) - - def _atanInsideCasingDeriv_casing_bottom(self, mDict): - return self._atanCasingLengthDeriv_casing_bottom(mDict) * self._atanfct( - self.x - self.casing_a(mDict), -self.slope - ) - - def _atanCasing(self, mDict): - return ( - self._atanCasingLength(mDict) - * self._atanfct(self.x - self.casing_a(mDict), self.slope) - * self._atanfct(self.x - self.casing_b(mDict), -self.slope) - ) - - def _atanCasingDeriv_casing_radius(self, mDict): - return self._atanCasingLength(mDict) * ( - self._atanfctDeriv(self.x - self.casing_a(mDict), self.slope) - * self._atanfct(self.x - self.casing_b(mDict), -self.slope) - + self._atanfct(self.x - self.casing_a(mDict), self.slope) - * self._atanfctDeriv(self.x - self.casing_b(mDict), -self.slope) - ) - - def _atanCasingDeriv_casing_thickness(self, mDict): - return self._atanCasingLength(mDict) * ( - -0.5 - * self._atanfctDeriv(self.x - self.casing_a(mDict), self.slope) - * self._atanfct(self.x - self.casing_b(mDict), -self.slope) - + self._atanfct(self.x - self.casing_a(mDict), self.slope) - * 0.5 - * self._atanfctDeriv(self.x - self.casing_b(mDict), -self.slope) - ) - - def _atanCasingDeriv_casing_bottom(self, mDict): - return ( - self._atanCasingLengthDeriv_casing_bottom(mDict) - * self._atanfct(self.x - self.casing_a(mDict), self.slope) - * self._atanfct(self.x - self.casing_b(mDict), -self.slope) - ) - - def _atanCasingDeriv_casing_top(self, mDict): - return ( - self._atanCasingLengthDeriv_casing_top(mDict) - * self._atanfct(self.x - self.casing_a(mDict), self.slope) - * self._atanfct(self.x - self.casing_b(mDict), -self.slope) - ) - - def layer_cont(self, mDict): - # contribution from the layered background - return mDict["val_background"] + ( - mDict["val_layer"] - mDict["val_background"] - ) * self._atanLayer(mDict) - - def _transform(self, m): - mDict = self.mDict(m) - - # assemble the model - layer = self.layer_cont(mDict) - casing = (mDict["val_casing"] - layer) * self._atanCasing(mDict) - insideCasing = (mDict["val_insideCasing"] - layer) * self._atanInsideCasing( - mDict - ) - - return layer + casing + insideCasing - - def _deriv_val_background(self, mDict): - # contribution from the layered background - d_layer_cont_dval_background = 1.0 - self._atanLayer(mDict) - d_casing_cont_dval_background = ( - -1.0 * d_layer_cont_dval_background * self._atanCasing(mDict) - ) - d_insideCasing_cont_dval_background = ( - -1.0 * d_layer_cont_dval_background * self._atanInsideCasing(mDict) - ) - return ( - d_layer_cont_dval_background - + d_casing_cont_dval_background - + d_insideCasing_cont_dval_background - ) - - def _deriv_val_layer(self, mDict): - d_layer_cont_dval_layer = self._atanLayer(mDict) - d_casing_cont_dval_layer = ( - -1.0 * d_layer_cont_dval_layer * self._atanCasing(mDict) - ) - d_insideCasing_cont_dval_layer = ( - -1.0 * d_layer_cont_dval_layer * self._atanInsideCasing(mDict) - ) - return ( - d_layer_cont_dval_layer - + d_casing_cont_dval_layer - + d_insideCasing_cont_dval_layer - ) - - def _deriv_val_casing(self, mDict): - d_layer_cont_dval_casing = 0.0 - d_casing_cont_dval_casing = self._atanCasing(mDict) - d_insideCasing_cont_dval_casing = 0.0 - return ( - d_layer_cont_dval_casing - + d_casing_cont_dval_casing - + d_insideCasing_cont_dval_casing - ) - - def _deriv_val_insideCasing(self, mDict): - d_layer_cont_dval_insideCasing = 0.0 - d_casing_cont_dval_insideCasing = 0.0 - d_insideCasing_cont_dval_insideCasing = self._atanInsideCasing(mDict) - return ( - d_layer_cont_dval_insideCasing - + d_casing_cont_dval_insideCasing - + d_insideCasing_cont_dval_insideCasing - ) - - def _deriv_layer_center(self, mDict): - d_layer_cont_dlayer_center = ( - mDict["val_layer"] - mDict["val_background"] - ) * self._atanLayerDeriv_layer_center(mDict) - d_casing_cont_dlayer_center = -d_layer_cont_dlayer_center * self._atanCasing( - mDict - ) - d_insideCasing_cont_dlayer_center = ( - -d_layer_cont_dlayer_center * self._atanInsideCasing(mDict) - ) - return ( - d_layer_cont_dlayer_center - + d_casing_cont_dlayer_center - + d_insideCasing_cont_dlayer_center - ) - - def _deriv_layer_thickness(self, mDict): - d_layer_cont_dlayer_thickness = ( - mDict["val_layer"] - mDict["val_background"] - ) * self._atanLayerDeriv_layer_thickness(mDict) - d_casing_cont_dlayer_thickness = ( - -d_layer_cont_dlayer_thickness * self._atanCasing(mDict) - ) - d_insideCasing_cont_dlayer_thickness = ( - -d_layer_cont_dlayer_thickness * self._atanInsideCasing(mDict) - ) - return ( - d_layer_cont_dlayer_thickness - + d_casing_cont_dlayer_thickness - + d_insideCasing_cont_dlayer_thickness - ) - - def _deriv_casing_radius(self, mDict): - layer = self.layer_cont(mDict) - d_layer_cont_dcasing_radius = 0.0 - d_casing_cont_dcasing_radius = ( - mDict["val_casing"] - layer - ) * self._atanCasingDeriv_casing_radius(mDict) - d_insideCasing_cont_dcasing_radius = ( - mDict["val_insideCasing"] - layer - ) * self._atanInsideCasingDeriv_casing_radius(mDict) - return ( - d_layer_cont_dcasing_radius - + d_casing_cont_dcasing_radius - + d_insideCasing_cont_dcasing_radius - ) - - def _deriv_casing_thickness(self, mDict): - d_layer_cont_dcasing_thickness = 0.0 - d_casing_cont_dcasing_thickness = ( - mDict["val_casing"] - self.layer_cont(mDict) - ) * self._atanCasingDeriv_casing_thickness(mDict) - d_insideCasing_cont_dcasing_thickness = ( - mDict["val_insideCasing"] - self.layer_cont(mDict) - ) * self._atanInsideCasingDeriv_casing_thickness(mDict) - return ( - d_layer_cont_dcasing_thickness - + d_casing_cont_dcasing_thickness - + d_insideCasing_cont_dcasing_thickness - ) - - def _deriv_casing_bottom(self, mDict): - d_layer_cont_dcasing_bottom = 0.0 - d_casing_cont_dcasing_bottom = ( - mDict["val_casing"] - self.layer_cont(mDict) - ) * self._atanCasingDeriv_casing_bottom(mDict) - d_insideCasing_cont_dcasing_bottom = ( - mDict["val_insideCasing"] - self.layer_cont(mDict) - ) * self._atanInsideCasingDeriv_casing_bottom(mDict) - return ( - d_layer_cont_dcasing_bottom - + d_casing_cont_dcasing_bottom - + d_insideCasing_cont_dcasing_bottom - ) - - def _deriv_casing_top(self, mDict): - d_layer_cont_dcasing_top = 0.0 - d_casing_cont_dcasing_top = ( - mDict["val_casing"] - self.layer_cont(mDict) - ) * self._atanCasingDeriv_casing_top(mDict) - d_insideCasing_cont_dcasing_top = ( - mDict["val_insideCasing"] - self.layer_cont(mDict) - ) * self._atanInsideCasingDeriv_casing_top(mDict) - return ( - d_layer_cont_dcasing_top - + d_casing_cont_dcasing_top - + d_insideCasing_cont_dcasing_top - ) - - def deriv(self, m): - mDict = self.mDict(m) - - return sp.csr_matrix( - np.vstack( - [ - self._deriv_val_background(mDict), - self._deriv_val_layer(mDict), - self._deriv_val_casing(mDict), - self._deriv_val_insideCasing(mDict), - self._deriv_layer_center(mDict), - self._deriv_layer_thickness(mDict), - self._deriv_casing_radius(mDict), - self._deriv_casing_thickness(mDict), - self._deriv_casing_bottom(mDict), - self._deriv_casing_top(mDict), - ] - ).T - ) - - -class ParametricBlockInLayer(ParametricLayer): - """ - Parametric Block in a Layered Space - - For 2D: - - .. code:: python - - m = [val_background, - val_layer, - val_block, - layer_center, - layer_thickness, - block_x0, - block_dx - ] - - For 3D: - - .. code:: python - - m = [val_background, - val_layer, - val_block, - layer_center, - layer_thickness, - block_x0, - block_y0, - block_dx, - block_dy - ] - - **Required** - - :param discretize.base.BaseMesh mesh: SimPEG Mesh, 2D or 3D - - **Optional** - - :param float slopeFact: arctan slope factor - divided by the minimum h - spacing to give the slope of the arctan - functions - :param float slope: slope of the arctan function - :param numpy.ndarray indActive: bool vector with - - """ - - def __init__(self, mesh, **kwargs): - super().__init__(mesh, **kwargs) - - @property - def nP(self): - if self.mesh.dim == 2: - return 7 - elif self.mesh.dim == 3: - return 9 - - @property - def shape(self): - if self.indActive is not None: - return (sum(self.indActive), self.nP) - return (self.mesh.nC, self.nP) - - def _mDict2d(self, m): - return { - "val_background": m[0], - "val_layer": m[1], - "val_block": m[2], - "layer_center": m[3], - "layer_thickness": m[4], - "x0": m[5], - "dx": m[6], - } - - def _mDict3d(self, m): - return { - "val_background": m[0], - "val_layer": m[1], - "val_block": m[2], - "layer_center": m[3], - "layer_thickness": m[4], - "x0": m[5], - "y0": m[6], - "dx": m[7], - "dy": m[8], - } - - def mDict(self, m): - if self.mesh.dim == 2: - return self._mDict2d(m) - elif self.mesh.dim == 3: - return self._mDict3d(m) - - def xleft(self, mDict): - return mDict["x0"] - 0.5 * mDict["dx"] - - def xright(self, mDict): - return mDict["x0"] + 0.5 * mDict["dx"] - - def yleft(self, mDict): - return mDict["y0"] - 0.5 * mDict["dy"] - - def yright(self, mDict): - return mDict["y0"] + 0.5 * mDict["dy"] - - def _atanBlock2d(self, mDict): - return ( - self._atanLayer(mDict) - * self._atanfct(self.x - self.xleft(mDict), self.slope) - * self._atanfct(self.x - self.xright(mDict), -self.slope) - ) - - def _atanBlock2dDeriv_layer_center(self, mDict): - return ( - self._atanLayerDeriv_layer_center(mDict) - * self._atanfct(self.x - self.xleft(mDict), self.slope) - * self._atanfct(self.x - self.xright(mDict), -self.slope) - ) - - def _atanBlock2dDeriv_layer_thickness(self, mDict): - return ( - self._atanLayerDeriv_layer_thickness(mDict) - * self._atanfct(self.x - self.xleft(mDict), self.slope) - * self._atanfct(self.x - self.xright(mDict), -self.slope) - ) - - def _atanBlock2dDeriv_x0(self, mDict): - return self._atanLayer(mDict) * ( - ( - self._atanfctDeriv(self.x - self.xleft(mDict), self.slope) - * self._atanfct(self.x - self.xright(mDict), -self.slope) - ) - + ( - self._atanfct(self.x - self.xleft(mDict), self.slope) - * self._atanfctDeriv(self.x - self.xright(mDict), -self.slope) - ) - ) - - def _atanBlock2dDeriv_dx(self, mDict): - return self._atanLayer(mDict) * ( - ( - self._atanfctDeriv(self.x - self.xleft(mDict), self.slope) - * -0.5 - * self._atanfct(self.x - self.xright(mDict), -self.slope) - ) - + ( - self._atanfct(self.x - self.xleft(mDict), self.slope) - * 0.5 - * self._atanfctDeriv(self.x - self.xright(mDict), -self.slope) - ) - ) - - def _atanBlock3d(self, mDict): - return ( - self._atanLayer(mDict) - * self._atanfct(self.x - self.xleft(mDict), self.slope) - * self._atanfct(self.x - self.xright(mDict), -self.slope) - * self._atanfct(self.y - self.yleft(mDict), self.slope) - * self._atanfct(self.y - self.yright(mDict), -self.slope) - ) - - def _atanBlock3dDeriv_layer_center(self, mDict): - return ( - self._atanLayerDeriv_layer_center(mDict) - * self._atanfct(self.x - self.xleft(mDict), self.slope) - * self._atanfct(self.x - self.xright(mDict), -self.slope) - * self._atanfct(self.y - self.yleft(mDict), self.slope) - * self._atanfct(self.y - self.yright(mDict), -self.slope) - ) - - def _atanBlock3dDeriv_layer_thickness(self, mDict): - return ( - self._atanLayerDeriv_layer_thickness(mDict) - * self._atanfct(self.x - self.xleft(mDict), self.slope) - * self._atanfct(self.x - self.xright(mDict), -self.slope) - * self._atanfct(self.y - self.yleft(mDict), self.slope) - * self._atanfct(self.y - self.yright(mDict), -self.slope) - ) - - def _atanBlock3dDeriv_x0(self, mDict): - return self._atanLayer(mDict) * ( - ( - self._atanfctDeriv(self.x - self.xleft(mDict), self.slope) - * self._atanfct(self.x - self.xright(mDict), -self.slope) - * self._atanfct(self.y - self.yleft(mDict), self.slope) - * self._atanfct(self.y - self.yright(mDict), -self.slope) - ) - + ( - self._atanfct(self.x - self.xleft(mDict), self.slope) - * self._atanfctDeriv(self.x - self.xright(mDict), -self.slope) - * self._atanfct(self.y - self.yleft(mDict), self.slope) - * self._atanfct(self.y - self.yright(mDict), -self.slope) - ) - ) - - def _atanBlock3dDeriv_y0(self, mDict): - return self._atanLayer(mDict) * ( - ( - self._atanfct(self.x - self.xleft(mDict), self.slope) - * self._atanfct(self.x - self.xright(mDict), -self.slope) - * self._atanfctDeriv(self.y - self.yleft(mDict), self.slope) - * self._atanfct(self.y - self.yright(mDict), -self.slope) - ) - + ( - self._atanfct(self.x - self.xleft(mDict), self.slope) - * self._atanfct(self.x - self.xright(mDict), -self.slope) - * self._atanfct(self.y - self.yleft(mDict), self.slope) - * self._atanfctDeriv(self.y - self.yright(mDict), -self.slope) - ) - ) - - def _atanBlock3dDeriv_dx(self, mDict): - return self._atanLayer(mDict) * ( - ( - self._atanfctDeriv(self.x - self.xleft(mDict), self.slope) - * -0.5 - * self._atanfct(self.x - self.xright(mDict), -self.slope) - * self._atanfct(self.y - self.yleft(mDict), self.slope) - * self._atanfct(self.y - self.yright(mDict), -self.slope) - ) - + ( - self._atanfct(self.x - self.xleft(mDict), self.slope) - * self._atanfctDeriv(self.x - self.xright(mDict), -self.slope) - * 0.5 - * self._atanfct(self.y - self.yleft(mDict), self.slope) - * self._atanfct(self.y - self.yright(mDict), -self.slope) - ) - ) - - def _atanBlock3dDeriv_dy(self, mDict): - return self._atanLayer(mDict) * ( - ( - self._atanfct(self.x - self.xleft(mDict), self.slope) - * self._atanfct(self.x - self.xright(mDict), -self.slope) - * self._atanfctDeriv(self.y - self.yleft(mDict), self.slope) - * -0.5 - * self._atanfct(self.y - self.yright(mDict), -self.slope) - ) - + ( - self._atanfct(self.x - self.xleft(mDict), self.slope) - * self._atanfct(self.x - self.xright(mDict), -self.slope) - * self._atanfct(self.y - self.yleft(mDict), self.slope) - * self._atanfctDeriv(self.y - self.yright(mDict), -self.slope) - * 0.5 - ) - ) - - def _transform2d(self, m): - mDict = self.mDict(m) - # assemble the model - # contribution from the layered background - layer_cont = mDict["val_background"] + ( - mDict["val_layer"] - mDict["val_background"] - ) * self._atanLayer(mDict) - - # perturbation due to the blocks - block_cont = (mDict["val_block"] - layer_cont) * self._atanBlock2d(mDict) - - return layer_cont + block_cont - - def _deriv2d_val_background(self, mDict): - d_layer_dval_background = np.ones_like(self.x) - self._atanLayer(mDict) - d_block_dval_background = (-d_layer_dval_background) * self._atanBlock2d(mDict) - return d_layer_dval_background + d_block_dval_background - - def _deriv2d_val_layer(self, mDict): - d_layer_dval_layer = self._atanLayer(mDict) - d_block_dval_layer = (-d_layer_dval_layer) * self._atanBlock2d(mDict) - return d_layer_dval_layer + d_block_dval_layer - - def _deriv2d_val_block(self, mDict): - d_layer_dval_block = 0.0 - d_block_dval_block = (1.0 - d_layer_dval_block) * self._atanBlock2d(mDict) - return d_layer_dval_block + d_block_dval_block - - def _deriv2d_layer_center(self, mDict): - d_layer_dlayer_center = ( - mDict["val_layer"] - mDict["val_background"] - ) * self._atanLayerDeriv_layer_center(mDict) - d_block_dlayer_center = ( - mDict["val_block"] - self.layer_cont(mDict) - ) * self._atanBlock2dDeriv_layer_center( - mDict - ) - d_layer_dlayer_center * self._atanBlock2d( - mDict - ) - return d_layer_dlayer_center + d_block_dlayer_center - - def _deriv2d_layer_thickness(self, mDict): - d_layer_dlayer_thickness = ( - mDict["val_layer"] - mDict["val_background"] - ) * self._atanLayerDeriv_layer_thickness(mDict) - d_block_dlayer_thickness = ( - mDict["val_block"] - self.layer_cont(mDict) - ) * self._atanBlock2dDeriv_layer_thickness( - mDict - ) - d_layer_dlayer_thickness * self._atanBlock2d( - mDict - ) - return d_layer_dlayer_thickness + d_block_dlayer_thickness - - def _deriv2d_x0(self, mDict): - d_layer_dx0 = 0.0 - d_block_dx0 = ( - mDict["val_block"] - self.layer_cont(mDict) - ) * self._atanBlock2dDeriv_x0(mDict) - return d_layer_dx0 + d_block_dx0 - - def _deriv2d_dx(self, mDict): - d_layer_ddx = 0.0 - d_block_ddx = ( - mDict["val_block"] - self.layer_cont(mDict) - ) * self._atanBlock2dDeriv_dx(mDict) - return d_layer_ddx + d_block_ddx - - def _deriv2d(self, m): - mDict = self.mDict(m) - - return np.vstack( - [ - self._deriv2d_val_background(mDict), - self._deriv2d_val_layer(mDict), - self._deriv2d_val_block(mDict), - self._deriv2d_layer_center(mDict), - self._deriv2d_layer_thickness(mDict), - self._deriv2d_x0(mDict), - self._deriv2d_dx(mDict), - ] - ).T - - def _transform3d(self, m): - # parse model - mDict = self.mDict(m) - - # assemble the model - # contribution from the layered background - layer_cont = mDict["val_background"] + ( - mDict["val_layer"] - mDict["val_background"] - ) * self._atanLayer(mDict) - # perturbation due to the block - block_cont = (mDict["val_block"] - layer_cont) * self._atanBlock3d(mDict) - - return layer_cont + block_cont - - def _deriv3d_val_background(self, mDict): - d_layer_dval_background = np.ones_like(self.x) - self._atanLayer(mDict) - d_block_dval_background = (-d_layer_dval_background) * self._atanBlock3d(mDict) - return d_layer_dval_background + d_block_dval_background - - def _deriv3d_val_layer(self, mDict): - d_layer_dval_layer = self._atanLayer(mDict) - d_block_dval_layer = (-d_layer_dval_layer) * self._atanBlock3d(mDict) - return d_layer_dval_layer + d_block_dval_layer - - def _deriv3d_val_block(self, mDict): - d_layer_dval_block = 0.0 - d_block_dval_block = (1.0 - d_layer_dval_block) * self._atanBlock3d(mDict) - return d_layer_dval_block + d_block_dval_block - - def _deriv3d_layer_center(self, mDict): - d_layer_dlayer_center = ( - mDict["val_layer"] - mDict["val_background"] - ) * self._atanLayerDeriv_layer_center(mDict) - d_block_dlayer_center = ( - mDict["val_block"] - self.layer_cont(mDict) - ) * self._atanBlock3dDeriv_layer_center( - mDict - ) - d_layer_dlayer_center * self._atanBlock3d( - mDict - ) - return d_layer_dlayer_center + d_block_dlayer_center - - def _deriv3d_layer_thickness(self, mDict): - d_layer_dlayer_thickness = ( - mDict["val_layer"] - mDict["val_background"] - ) * self._atanLayerDeriv_layer_thickness(mDict) - d_block_dlayer_thickness = ( - mDict["val_block"] - self.layer_cont(mDict) - ) * self._atanBlock3dDeriv_layer_thickness( - mDict - ) - d_layer_dlayer_thickness * self._atanBlock3d( - mDict - ) - return d_layer_dlayer_thickness + d_block_dlayer_thickness - - def _deriv3d_x0(self, mDict): - d_layer_dx0 = 0.0 - d_block_dx0 = ( - mDict["val_block"] - self.layer_cont(mDict) - ) * self._atanBlock3dDeriv_x0(mDict) - return d_layer_dx0 + d_block_dx0 - - def _deriv3d_y0(self, mDict): - d_layer_dy0 = 0.0 - d_block_dy0 = ( - mDict["val_block"] - self.layer_cont(mDict) - ) * self._atanBlock3dDeriv_y0(mDict) - return d_layer_dy0 + d_block_dy0 - - def _deriv3d_dx(self, mDict): - d_layer_ddx = 0.0 - d_block_ddx = ( - mDict["val_block"] - self.layer_cont(mDict) - ) * self._atanBlock3dDeriv_dx(mDict) - return d_layer_ddx + d_block_ddx - - def _deriv3d_dy(self, mDict): - d_layer_ddy = 0.0 - d_block_ddy = ( - mDict["val_block"] - self.layer_cont(mDict) - ) * self._atanBlock3dDeriv_dy(mDict) - return d_layer_ddy + d_block_ddy - - def _deriv3d(self, m): - mDict = self.mDict(m) - - return np.vstack( - [ - self._deriv3d_val_background(mDict), - self._deriv3d_val_layer(mDict), - self._deriv3d_val_block(mDict), - self._deriv3d_layer_center(mDict), - self._deriv3d_layer_thickness(mDict), - self._deriv3d_x0(mDict), - self._deriv3d_y0(mDict), - self._deriv3d_dx(mDict), - self._deriv3d_dy(mDict), - ] - ).T - - def _transform(self, m): - if self.mesh.dim == 2: - return self._transform2d(m) - elif self.mesh.dim == 3: - return self._transform3d(m) - - def deriv(self, m): - if self.mesh.dim == 2: - return sp.csr_matrix(self._deriv2d(m)) - elif self.mesh.dim == 3: - return sp.csr_matrix(self._deriv3d(m)) - - -class TileMap(IdentityMap): - """ - Mapping for tiled inversion. - - Uses volume averaging to map a model defined on a global mesh to the - local mesh. Everycell in the local mesh must also be in the global mesh. - """ - - def __init__( - self, - global_mesh, - global_active, - local_mesh, - tol=1e-8, - components=1, - **kwargs, - ): - """ - Parameters - ---------- - global_mesh : discretize.TreeMesh - Global TreeMesh defining the entire domain. - global_active : numpy.ndarray of bool or int - Defines the active cells in the global mesh. - local_mesh : discretize.TreeMesh - Local TreeMesh for the simulation. - tol : float, optional - Tolerance to avoid zero division - components : int, optional - Number of components in the model. E.g. a vector model in 3D would have 3 - components. - """ - super().__init__(mesh=None, **kwargs) - self._global_mesh = validate_type( - "global_mesh", global_mesh, discretize.TreeMesh, cast=False - ) - self._local_mesh = validate_type( - "local_mesh", local_mesh, discretize.TreeMesh, cast=False - ) - - self._global_active = validate_active_indices( - "global_active", global_active, self.global_mesh.n_cells - ) - - self._tol = validate_float("tol", tol, min_val=0.0, inclusive_min=False) - self._components = validate_integer("components", components, min_val=1) - - # trigger creation of P - self.P - - @property - def global_mesh(self): - """Global TreeMesh defining the entire domain. - - Returns - ------- - discretize.TreeMesh - """ - return self._global_mesh - - @property - def local_mesh(self): - """Local TreeMesh defining the local domain. - - Returns - ------- - discretize.TreeMesh - """ - return self._local_mesh - - @property - def global_active(self): - """Defines the active cells in the global mesh. - - Returns - ------- - (global_mesh.n_cells) numpy.ndarray of bool - """ - return self._global_active - - @property - def local_active(self): - """ - This is the local_active of the global_active used in the global problem. - - Returns - ------- - (local_mesh.n_cells) numpy.ndarray of bool - """ - return self._local_active - - @property - def tol(self): - """Tolerance to avoid zero division. - - Returns - ------- - float - """ - return self._tol - - @property - def components(self): - """Number of components in the model. - - Returns - ------- - int - """ - return self._components - - @property - def P(self): - """ - Set the projection matrix with partial volumes - """ - if getattr(self, "_P", None) is None: - in_local = self.local_mesh._get_containing_cell_indexes( - self.global_mesh.cell_centers - ) - - P = ( - sp.csr_matrix( - ( - self.global_mesh.cell_volumes, - (in_local, np.arange(self.global_mesh.nC)), - ), - shape=(self.local_mesh.nC, self.global_mesh.nC), - ) - * speye(self.global_mesh.nC)[:, self.global_active] - ) - - self._local_active = mkvc(np.sum(P, axis=1) > 0) - - P = P[self.local_active, :] - - self._P = sp.block_diag( - [ - sdiag(1.0 / self.local_mesh.cell_volumes[self.local_active]) * P - for ii in range(self.components) - ] - ) - - return self._P - - def _transform(self, m): - return self.P * m - - @property - def shape(self): - """ - Shape of the matrix operation (number of indices x nP) - """ - return self.P.shape - - def deriv(self, m, v=None): - """ - :param numpy.ndarray m: model - :rtype: scipy.sparse.csr_matrix - :return: derivative of transformed model - """ - if v is not None: - return self.P * v - return self.P - - -############################################################################### -# # -# Maps for petrophsyics clusters # -# # -############################################################################### - - -class PolynomialPetroClusterMap(IdentityMap): - """ - Modeling polynomial relationships between physical properties - - Parameters - ---------- - coeffxx : array_like, optional - Coefficients for the xx component. Default is [0, 1] - coeffxy : array_like, optional - Coefficients for the xy component. Default is [0] - coeffyx : array_like, optional - Coefficients for the yx component. Default is [0] - coeffyy : array_like, optional - Coefficients for the yy component. Default is [0, 1] - """ - - def __init__( - self, - coeffxx=None, - coeffxy=None, - coeffyx=None, - coeffyy=None, - mesh=None, - nP=None, - **kwargs, - ): - if coeffxx is None: - coeffxx = np.r_[0.0, 1.0] - if coeffxy is None: - coeffxy = np.r_[0.0] - if coeffyx is None: - coeffyx = np.r_[0.0] - if coeffyy is None: - coeffyy = np.r_[0.0, 1.0] - - self._coeffxx = validate_ndarray_with_shape("coeffxx", coeffxx, shape=("*",)) - self._coeffxy = validate_ndarray_with_shape("coeffxy", coeffxy, shape=("*",)) - self._coeffyx = validate_ndarray_with_shape("coeffyx", coeffyx, shape=("*",)) - self._coeffyy = validate_ndarray_with_shape("coeffyy", coeffyy, shape=("*",)) - - self._polynomialxx = polynomial.Polynomial(self.coeffxx) - self._polynomialxy = polynomial.Polynomial(self.coeffxy) - self._polynomialyx = polynomial.Polynomial(self.coeffyx) - self._polynomialyy = polynomial.Polynomial(self.coeffyy) - self._polynomialxx_deriv = self._polynomialxx.deriv(m=1) - self._polynomialxy_deriv = self._polynomialxy.deriv(m=1) - self._polynomialyx_deriv = self._polynomialyx.deriv(m=1) - self._polynomialyy_deriv = self._polynomialyy.deriv(m=1) - - super().__init__(mesh=mesh, nP=nP, **kwargs) - - @property - def coeffxx(self): - """Coefficients for the xx component. - - Returns - ------- - numpy.ndarray - """ - return self._coeffxx - - @property - def coeffxy(self): - """Coefficients for the xy component. - - Returns - ------- - numpy.ndarray - """ - return self._coeffxy - - @property - def coeffyx(self): - """Coefficients for the yx component. - - Returns - ------- - numpy.ndarray - """ - return self._coeffyx - - @property - def coeffyy(self): - """Coefficients for the yy component. - - Returns - ------- - numpy.ndarray - """ - return self._coeffyy - - def _transform(self, m): - out = m.copy() - out[:, 0] = self._polynomialxx(m[:, 0]) + self._polynomialxy(m[:, 1]) - out[:, 1] = self._polynomialyx(m[:, 0]) + self._polynomialyy(m[:, 1]) - return out - - def inverse(self, D): - r""" - :param numpy.array D: physical property - :rtype: numpy.array - :return: model - - The *transformInverse* changes the physical property into the - model. - - .. math:: - - m = \log{\sigma} - - """ - raise NotImplementedError("Inverse is not implemented.") - - def _derivmatrix(self, m): - return np.r_[ - [ - [ - self._polynomialxx_deriv(m[:, 0])[0], - self._polynomialyx_deriv(m[:, 0])[0], - ], - [ - self._polynomialxy_deriv(m[:, 1])[0], - self._polynomialyy_deriv(m[:, 1])[0], - ], - ] - ] - - def deriv(self, m, v=None): - """""" - if v is None: - out = self._derivmatrix(m.reshape(-1, 2)) - return out - else: - out = np.dot(self._derivmatrix(m.reshape(-1, 2)), v.reshape(2, -1)) - return out - - @property - def is_linear(self): - return False diff --git a/simpeg/maps/__init__.py b/simpeg/maps/__init__.py new file mode 100644 index 0000000000..d24d07aa9f --- /dev/null +++ b/simpeg/maps/__init__.py @@ -0,0 +1,35 @@ +from ._base import ( + ComboMap, + IdentityMap, + LinearMap, + Projection, + SphericalSystem, + SumMap, + TileMap, + Wires, +) +from ._clustering import PolynomialPetroClusterMap +from ._injection import Mesh2Mesh, InjectActiveCells +from ._property_maps import ( + ChiMap, + ComplexMap, + ExpMap, + LogisticSigmoidMap, + LogMap, + MuRelative, + ReciprocalMap, + SelfConsistentEffectiveMedium, + Weighting, +) +from ._parametric import ( + BaseParametric, + ParametricBlock, + ParametricBlockInLayer, + ParametricCasingAndLayer, + ParametricCircleMap, + ParametricEllipsoid, + ParametricLayer, + ParametricPolyMap, + ParametricSplineMap, +) +from ._surjection import Surject2Dto3D, SurjectFull, SurjectUnits, SurjectVertical1D diff --git a/simpeg/maps/_base.py b/simpeg/maps/_base.py new file mode 100644 index 0000000000..79effec39a --- /dev/null +++ b/simpeg/maps/_base.py @@ -0,0 +1,1342 @@ +""" +Base and general map classes. +""" + +from __future__ import annotations # needed to use type operands in Python 3.8 + +from collections import namedtuple +import discretize +import numpy as np +import scipy.sparse as sp +from scipy.sparse import csr_matrix as csr +from discretize.tests import check_derivative +from discretize.utils import Zero, Identity, mkvc, speye, sdiag + +from ..utils import ( + mat_utils, + validate_type, + validate_ndarray_with_shape, + validate_list_of_types, + validate_active_indices, + validate_integer, + validate_float, +) +from ..typing import RandomSeed + + +class IdentityMap: + r"""Identity mapping and the base mapping class for all other SimPEG mappings. + + The ``IdentityMap`` class is used to define the mapping when + the model parameters are the same as the parameters used in the forward + simulation. For a discrete set of model parameters :math:`\mathbf{m}`, + the mapping :math:`\mathbf{u}(\mathbf{m})` is equivalent to applying + the identity matrix; i.e.: + + .. math:: + + \mathbf{u}(\mathbf{m}) = \mathbf{Im} + + The ``IdentityMap`` also acts as the base class for all other SimPEG mapping classes. + + Using the *mesh* or *nP* input arguments, the dimensions of the corresponding + mapping operator can be permanently set; i.e. (*mesh.nC*, *mesh.nC*) or (*nP*, *nP*). + However if both input arguments *mesh* and *nP* are ``None``, the shape of + mapping operator is arbitrary and can act on any vector; i.e. has shape (``*``, ``*``). + + Parameters + ---------- + mesh : discretize.BaseMesh + The number of parameters accepted by the mapping is set to equal the number + of mesh cells. + nP : int, or '*' + Set the number of parameters accepted by the mapping directly. Used if the + number of parameters is known. Used generally when the number of parameters + is not equal to the number of cells in a mesh. + """ + + def __init__(self, mesh=None, nP=None, **kwargs): + if (isinstance(nP, str) and nP == "*") or nP is None: + if mesh is not None: + nP = mesh.n_cells + else: + nP = "*" + else: + try: + nP = int(nP) + except (TypeError, ValueError) as err: + raise TypeError( + f"Unrecognized input of {repr(nP)} for number of parameters, must be an integer or '*'." + ) from err + self.mesh = mesh + self._nP = nP + + super().__init__(**kwargs) + + @property + def nP(self): + r"""Number of parameters the mapping acts on. + + Returns + ------- + int or ``*`` + Number of parameters that the mapping acts on. Returns an + ``int`` if the dimensions of the mapping are set. If the + mapping can act on a vector of any length, ``*`` is returned. + """ + if self._nP != "*": + return int(self._nP) + if self.mesh is None: + return "*" + return int(self.mesh.nC) + + @property + def shape(self): + r"""Dimensions of the mapping operator + + The dimensions of the mesh depend on the input arguments used + during instantiation. If *mesh* is used to define the + identity map, the shape of mapping operator is (*mesh.nC*, *mesh.nC*). + If *nP* is used to define the identity map, the mapping operator + has dimensions (*nP*, *nP*). However if both *mesh* and *nP* are + used to define the identity map, the mapping will have shape + (*mesh.nC*, *nP*)! And if *mesh* and *nP* were ``None`` when + instantiating, the mapping has dimensions (``*``, ``*``) and may + act on a vector of any length. + + Returns + ------- + tuple + Dimensions of the mapping operator. If the dimensions of + the mapping are set, the return is a tuple (``int``,``int``). + If the mapping can act on a vector of arbitrary length, the + return is a tuple (``*``, ``*``). + """ + if self.mesh is None: + return (self.nP, self.nP) + return (self.mesh.nC, self.nP) + + def _transform(self, m): + """ + Changes the model into the physical property. + + .. note:: + + This can be called by the __mul__ property against a + :meth:numpy.ndarray. + + :param numpy.ndarray m: model + :rtype: numpy.ndarray + :return: transformed model + + """ + return m + + def inverse(self, D): + """ + The transform inverse is not implemented. + """ + raise NotImplementedError("The transform inverse is not implemented.") + + def deriv(self, m, v=None): + r"""Derivative of the mapping with respect to the input parameters. + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix or numpy.ndarray + Derivative of the mapping with respect to the model parameters. For an + identity mapping, this is just a sparse identity matrix. If the input + argument *v* is not ``None``, the method returns the derivative times + the vector *v*; which in this case is just *v*. + + Notes + ----- + Let :math:`\mathbf{m}` be a set of model parameters and let :math:`\mathbf{I}` + denote the identity map. Where the identity mapping acting on the model parameters + can be expressed as: + + .. math:: + \mathbf{u} = \mathbf{I m}, + + the **deriv** method returns the derivative of :math:`\mathbf{u}` with respect + to the model parameters; i.e.: + + .. math:: + \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = \mathbf{I} + + For the Identity map **deriv** simply returns a sparse identity matrix. + """ + if v is not None: + return v + if isinstance(self.nP, (int, np.integer)): + return sp.identity(self.nP) + return Identity() + + def test(self, m=None, num=4, random_seed: RandomSeed | None = None, **kwargs): + """Derivative test for the mapping. + + This test validates the mapping by performing a convergence test. + + Parameters + ---------- + m : (nP) numpy.ndarray + Starting vector of model parameters for the derivative test + num : int + Number of iterations for the derivative test + random_seed : None or :class:`~simpeg.typing.RandomSeed`, optional + Random seed used for generating a random array for ``m`` if it's + None. It can either be an int, a predefined Numpy random number + generator, or any valid input to ``numpy.random.default_rng``. + kwargs: dict + Keyword arguments and associated values in the dictionary must + match those used in :meth:`discretize.tests.check_derivative` + + Returns + ------- + bool + Returns ``True`` if the test passes + """ + print("Testing {0!s}".format(str(self))) + if m is None: + rng = np.random.default_rng(seed=random_seed) + m = rng.uniform(size=self.nP) + if "plotIt" not in kwargs: + kwargs["plotIt"] = False + + assert isinstance( + self.nP, (int, np.integer) + ), "nP must be an integer for {}".format(self.__class__.__name__) + return check_derivative( + lambda m: [self * m, self.deriv(m)], m, num=num, **kwargs + ) + + def _assertMatchesPair(self, pair): + assert ( + isinstance(self, pair) + or isinstance(self, ComboMap) + and isinstance(self.maps[0], pair) + ), "Mapping object must be an instance of a {0!s} class.".format(pair.__name__) + + def __mul__(self, val): + if isinstance(val, IdentityMap): + if ( + not (self.shape[1] == "*" or val.shape[0] == "*") + and not self.shape[1] == val.shape[0] + ): + raise ValueError( + "Dimension mismatch in {0!s} and {1!s}.".format(str(self), str(val)) + ) + return ComboMap([self, val]) + + elif isinstance(val, np.ndarray): + if not self.shape[1] == "*" and not self.shape[1] == val.shape[0]: + raise ValueError( + "Dimension mismatch in {0!s} and np.ndarray{1!s}.".format( + str(self), str(val.shape) + ) + ) + return self._transform(val) + + elif isinstance(val, Zero): + return Zero() + + raise Exception( + "Unrecognized data type to multiply. Try a map or a numpy.ndarray!" + "You used a {} of type {}".format(val, type(val)) + ) + + def dot(self, map1): + r"""Multiply two mappings to create a :class:`simpeg.maps.ComboMap`. + + Let :math:`\mathbf{f}_1` and :math:`\mathbf{f}_2` represent two mapping functions. + Where :math:`\mathbf{m}` represents a set of input model parameters, + the ``dot`` method is used to create a combination mapping: + + .. math:: + u(\mathbf{m}) = f_2(f_1(\mathbf{m})) + + Where :math:`\mathbf{f_1} : M \rightarrow K_1` and acts on the + model first, and :math:`\mathbf{f_2} : K_1 \rightarrow K_2`, the combination + mapping :math:`\mathbf{u} : M \rightarrow K_2`. + + When using the **dot** method, the input argument *map1* represents the first + mapping that is be applied and *self* represents the second mapping + that is be applied. Therefore, the correct syntax for using this method is:: + + self.dot(map1) + + + Parameters + ---------- + map1 : + A SimPEG mapping object. + + Examples + -------- + Here we create a combination mapping that 1) projects a single scalar to + a vector space of length 5, then takes the natural exponent. + + >>> import numpy as np + >>> from simpeg.maps import ExpMap, Projection + + >>> nP1 = 1 + >>> nP2 = 5 + >>> ind = np.zeros(nP1, dtype=int) + + >>> projection_map = Projection(nP1, ind) + >>> projection_map.shape + (5, 1) + + >>> exp_map = ExpMap(nP=5) + >>> exp_map.shape + (5, 5) + + >>> combo_map = exp_map.dot(projection_map) + >>> combo_map.shape + (5, 1) + + >>> m = np.array([2]) + >>> combo_map * m + array([7.3890561, 7.3890561, 7.3890561, 7.3890561, 7.3890561]) + + """ + return self.__mul__(map1) + + def __matmul__(self, map1): + return self.__mul__(map1) + + __numpy_ufunc__ = True + + def __add__(self, map1): + return SumMap([self, map1]) # error-checking done inside of the SumMap + + def __str__(self): + return "{0!s}({1!s},{2!s})".format( + self.__class__.__name__, self.shape[0], self.shape[1] + ) + + def __len__(self): + return 1 + + @property + def mesh(self): + """ + The mesh used for the mapping + + Returns + ------- + discretize.base.BaseMesh or None + """ + return self._mesh + + @mesh.setter + def mesh(self, value): + if value is not None: + value = validate_type("mesh", value, discretize.base.BaseMesh, cast=False) + self._mesh = value + + @property + def is_linear(self): + """Determine whether or not this mapping is a linear operation. + + Returns + ------- + bool + """ + return True + + +class ComboMap(IdentityMap): + r"""Combination mapping constructed by joining a set of other mappings. + + A ``ComboMap`` is a single mapping object made by joining a set + of basic mapping operations by chaining them together, in order. + When creating a ``ComboMap``, the user provides a list of SimPEG mapping objects they wish to join. + The order of the mappings in this list is from last to first; i.e. + :math:`[\mathbf{f}_n , ... , \mathbf{f}_2 , \mathbf{f}_1]`. + + The combination mapping :math:`\mathbf{u}(\mathbf{m})` that acts on a + set of input model parameters :math:`\mathbf{m}` is defined as: + + .. math:: + \mathbf{u}(\mathbf{m}) = f_n(f_{n-1}(\cdots f_1(f_0(\mathbf{m})))) + + Note that any time that you create your own combination mapping, + be sure to test that the derivative is correct. + + Parameters + ---------- + maps : list of simpeg.maps.IdentityMap + A ``list`` of SimPEG mapping objects. The ordering of the mapping + objects in the ``list`` is from last applied to first applied! + + Examples + -------- + Here we create a combination mapping that 1) projects a single scalar to + a vector space of length 5, then takes the natural exponent. + + >>> import numpy as np + >>> from simpeg.maps import ExpMap, Projection, ComboMap + + >>> nP1 = 1 + >>> nP2 = 5 + >>> ind = np.zeros(nP1, dtype=int) + + >>> projection_map = Projection(nP1, ind) + >>> projection_map.shape + (5, 1) + + >>> exp_map = ExpMap(nP=5) + >>> exp_map.shape + (5, 5) + + Recall that the order of the mapping objects is from last applied + to first applied. + + >>> map_list = [exp_map, projection_map] + >>> combo_map = ComboMap(map_list) + >>> combo_map.shape + (5, 1) + + >>> m = np.array([2.]) + >>> combo_map * m + array([7.3890561, 7.3890561, 7.3890561, 7.3890561, 7.3890561]) + + """ + + def __init__(self, maps, **kwargs): + super().__init__(mesh=None, **kwargs) + + self.maps = [] + for ii, m in enumerate(maps): + assert isinstance(m, IdentityMap), "Unrecognized data type, " + "inherit from an IdentityMap or ComboMap!" + + if ( + ii > 0 + and not (self.shape[1] == "*" or m.shape[0] == "*") + and not self.shape[1] == m.shape[0] + ): + prev = self.maps[-1] + + raise ValueError( + "Dimension mismatch in map[{0!s}] ({1!s}, {2!s}) " + "and map[{3!s}] ({4!s}, {5!s}).".format( + prev.__class__.__name__, + prev.shape[0], + prev.shape[1], + m.__class__.__name__, + m.shape[0], + m.shape[1], + ) + ) + + if np.any([isinstance(m, SumMap), isinstance(m, IdentityMap)]): + self.maps += [m] + elif isinstance(m, ComboMap): + self.maps += m.maps + else: + raise ValueError("Map[{0!s}] not supported", m.__class__.__name__) + + @property + def shape(self): + r"""Dimensions of the mapping. + + For a list of SimPEG mappings [:math:`\mathbf{f}_n,...,\mathbf{f}_1`] + that have been joined to create a ``ComboMap``, this method returns + the dimensions of the combination mapping. Recall that the ordering + of the list of mappings is from last to first. + + Returns + ------- + (2) tuple of int + Dimensions of the mapping operator. + """ + return (self.maps[0].shape[0], self.maps[-1].shape[1]) + + @property + def nP(self): + r"""Number of parameters the mapping acts on. + + Returns + ------- + int + Number of parameters that the mapping acts on. + """ + return self.maps[-1].nP + + def _transform(self, m): + for map_i in reversed(self.maps): + m = map_i * m + return m + + def deriv(self, m, v=None): + r"""Derivative of the mapping with respect to the input parameters. + + Any time that you create your own combination mapping, + be sure to test that the derivative is correct. + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. + If the input argument *v* is not ``None``, the method returns + the derivative times the vector *v*. + + Notes + ----- + Let :math:`\mathbf{m}` be a set of model parameters and let + [:math:`\mathbf{f}_n,...,\mathbf{f}_1`] be the list of SimPEG mappings joined + to create a combination mapping. Recall that the list of mappings is ordered + from last applied to first applied. + + Where the combination mapping acting on the model parameters + can be expressed as: + + .. math:: + \mathbf{u}(\mathbf{m}) = f_n(f_{n-1}(\cdots f_1(f_0(\mathbf{m})))) + + The **deriv** method returns the derivative of :math:`\mathbf{u}` with respect + to the model parameters. To do this, we use the chain rule, i.e.: + + .. math:: + \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = + \frac{\partial \mathbf{f_n}}{\partial \mathbf{f_{n-1}}} + \cdots + \frac{\partial \mathbf{f_2}}{\partial \mathbf{f_{1}}} + \frac{\partial \mathbf{f_1}}{\partial \mathbf{m}} + """ + + if v is not None: + deriv = v + else: + deriv = 1 + + mi = m + for map_i in reversed(self.maps): + deriv = map_i.deriv(mi) * deriv + mi = map_i * mi + return deriv + + def __str__(self): + return "ComboMap[{0!s}]({1!s},{2!s})".format( + " * ".join([m.__str__() for m in self.maps]), self.shape[0], self.shape[1] + ) + + def __len__(self): + return len(self.maps) + + @property + def is_linear(self): + return all(m.is_linear for m in self.maps) + + +class LinearMap(IdentityMap): + """A generalized linear mapping. + + A simple map that implements the linear mapping, + + >>> y = A @ x + b + + Parameters + ---------- + A : (M, N) array_like, optional + The matrix operator, can be any object that implements `__matmul__` + and has a `shape` attribute. + b : (M) array_like, optional + Additive part of the linear operation. + """ + + def __init__(self, A, b=None, **kwargs): + kwargs.pop("mesh", None) + kwargs.pop("nP", None) + super().__init__(**kwargs) + self.A = A + self.b = b + + @property + def A(self): + """The linear operator matrix. + + Returns + ------- + LinearOperator + Must support matrix multiplication and have a shape attribute. + """ + return self._A + + @A.setter + def A(self, value): + if not hasattr(value, "__matmul__"): + raise TypeError( + f"{repr(value)} does not implement the matrix multiplication operator." + ) + if not hasattr(value, "shape"): + raise TypeError(f"{repr(value)} does not have a shape attribute.") + self._A = value + self._nP = value.shape[1] + self._shape = value.shape + + @property + def shape(self): + return self._shape + + @property + def b(self): + """Added part of the linear operation. + + Returns + ------- + numpy.ndarray + """ + return self._b + + @b.setter + def b(self, value): + if value is not None: + value = validate_ndarray_with_shape("b", value, shape=(self.shape[0],)) + self._b = value + + def _transform(self, m): + if self.b is None: + return self.A @ m + return self.A @ m + self.b + + def deriv(self, m, v=None): + if v is None: + return self.A + return self.A @ v + + +class Projection(IdentityMap): + r"""Projection mapping. + + ``Projection`` mapping can be used to project and/or rearange model + parameters. For a set of model parameter :math:`\mathbf{m}`, + the mapping :math:`\mathbf{u}(\mathbf{m})` can be defined by a linear + projection matrix :math:`\mathbf{P}` acting on the model, i.e.: + + .. math:: + \mathbf{u}(\mathbf{m}) = \mathbf{Pm} + + The number of model parameters the mapping acts on is + defined by *nP*. Projection and/or rearrangement of the parameters + is defined by *index*. Thus the dimensions of the mapping is + (*nInd*, *nP*). + + Parameters + ---------- + nP : int + Number of model parameters the mapping acts on + index : numpy.ndarray of int + Indexes defining the projection from the model space + + Examples + -------- + Here we define a mapping that rearranges and projects 2 model + parameters to a vector space spanning 4 parameters. + + >>> from simpeg.maps import Projection + >>> import numpy as np + + >>> nP = 2 + >>> index = np.array([1, 0, 1, 0], dtype=int) + >>> mapping = Projection(nP, index) + + >>> m = np.array([6, 8]) + >>> mapping * m + array([8, 6, 8, 6]) + + """ + + def __init__(self, nP, index, **kwargs): + assert isinstance( + index, (np.ndarray, slice, list) + ), "index must be a np.ndarray or slice, not {}".format(type(index)) + super(Projection, self).__init__(nP=nP, **kwargs) + + if isinstance(index, slice): + index = list(range(*index.indices(self.nP))) + + if isinstance(index, np.ndarray): + if index.dtype is np.dtype("bool"): + index = np.where(index)[0] + + self.index = index + self._shape = nI, nP = len(self.index), self.nP + + assert max(index) < nP, "maximum index must be less than {}".format(nP) + + # sparse projection matrix + self.P = sp.csr_matrix((np.ones(nI), (range(nI), self.index)), shape=(nI, nP)) + + def _transform(self, m): + return m[self.index] + + @property + def shape(self): + r"""Dimensions of the mapping. + + Returns + ------- + tuple + Where *nP* is the number of parameters the mapping acts on and + *nInd* is the length of the vector defining the mapping, the + dimensions of the mapping operator is a tuple of the + form (*nInd*, *nP*). + """ + return self._shape + + def deriv(self, m, v=None): + r"""Derivative of the mapping with respect to the input parameters. + + Let :math:`\mathbf{m}` be a set of model parameters and let :math:`\mathbf{P}` + be a matrix denoting the projection mapping. Where the projection mapping acting + on the model parameters can be expressed as: + + .. math:: + \mathbf{u} = \mathbf{P m}, + + the **deriv** method returns the derivative of :math:`\mathbf{u}` with respect + to the model parameters; i.e.: + + .. math:: + \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = \mathbf{P} + + Note that in this case, **deriv** simply returns a sparse projection matrix. + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. If the + input argument *v* is not ``None``, the method returns the derivative times + the vector *v*. + """ + + if v is not None: + return self.P * v + return self.P + + +class SumMap(ComboMap): + """Combination map constructed by summing multiple mappings + to the same vector space. + + A map to add model parameters contributing to the + forward operation e.g. F(m) = F(g(x) + h(y)) + + Assumes that the model vectors defined by g(x) and h(y) + are equal in length. + Allows to assume different things about the model m: + i.e. parametric + voxel models + + Parameters + ---------- + maps : list + A list of SimPEG mapping objects that are being summed. + Each mapping object in the list must act on the same number + of model parameters and must map to the same vector space! + """ + + def __init__(self, maps, **kwargs): + maps = validate_list_of_types("maps", maps, IdentityMap) + + # skip ComboMap's init + super(ComboMap, self).__init__(mesh=None, **kwargs) + + self.maps = [] + for ii, m in enumerate(maps): + if not isinstance(m, IdentityMap): + raise TypeError( + "Unrecognized data type {}, inherit from an " + "IdentityMap!".format(type(m)) + ) + + if ( + ii > 0 + and not (self.shape == "*" or m.shape == "*") + and not self.shape == m.shape + ): + raise ValueError( + "Dimension mismatch in map[{0!s}] ({1!s}, {2!s}) " + "and map[{3!s}] ({4!s}, {5!s}).".format( + self.maps[0].__class__.__name__, + self.maps[0].shape[0], + self.maps[0].shape[1], + m.__class__.__name__, + m.shape[0], + m.shape[1], + ) + ) + + self.maps += [m] + + @property + def shape(self): + """Dimensions of the mapping. + + Returns + ------- + tuple + The dimensions of the mapping. A tuple of the form (``int``,``int``) + """ + return (self.maps[0].shape[0], self.maps[0].shape[1]) + + @property + def nP(self): + r"""Number of parameters the combined mapping acts on. + + Returns + ------- + int + Number of parameters that the mapping acts on. + """ + return self.maps[-1].shape[1] + + def _transform(self, m): + for ii, map_i in enumerate(self.maps): + m0 = m.copy() + m0 = map_i * m0 + + if ii == 0: + mout = m0 + else: + mout += m0 + return mout + + def deriv(self, m, v=None): + """Derivative of mapping with respect to the input parameters + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. If the + input argument *v* is not ``None``, the method returns the derivative times + the vector *v*. + """ + + for ii, map_i in enumerate(self.maps): + m0 = m.copy() + + if v is not None: + deriv = v + else: + deriv = sp.eye(self.nP) + + deriv = map_i.deriv(m0, v=deriv) + if ii == 0: + sumDeriv = deriv + else: + sumDeriv += deriv + + return sumDeriv + + +class SphericalSystem(IdentityMap): + r"""Mapping vectors from spherical to Cartesian coordinates. + + Let :math:`\mathbf{m}` be a model containing the amplitudes + (:math:`\mathbf{a}`), azimuthal angles (:math:`\mathbf{t}`) + and radial angles (:math:`\mathbf{p}`) for a set of vectors + in spherical space such that: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{a} \\ \mathbf{t} \\ \mathbf{p} \end{bmatrix} + + ``SphericalSystem`` constructs a mapping :math:`\mathbf{u}(\mathbf{m}) + that converts the set of vectors in spherical coordinates to + their representation in Cartesian coordinates, i.e.: + + .. math:: + \mathbf{u}(\mathbf{m}) = \begin{bmatrix} \mathbf{v_x} \\ \mathbf{v_y} \\ \mathbf{v_z} \end{bmatrix} + + where :math:`\mathbf{v_x}`, :math:`\mathbf{v_y}` and :math:`\mathbf{v_z}` + store the x, y and z components of the vectors, respectively. + + Using the *mesh* or *nP* input arguments, the dimensions of the corresponding + mapping operator can be permanently set; i.e. (*3\*mesh.nC*, *3\*mesh.nC*) or (*nP*, *nP*). + However if both input arguments *mesh* and *nP* are ``None``, the shape of + mapping operator is arbitrary and can act on any vector whose length + is a multiple of 3; i.e. has shape (``*``, ``*``). + + Notes + ----- + + In Cartesian space, the components of each vector are defined as + + .. math:: + \mathbf{v} = (v_x, v_y, v_z) + + In spherical coordinates, vectors are is defined as: + + .. math:: + \mathbf{v^\prime} = (a, t, p) + + where + + - :math:`a` is the amplitude of the vector + - :math:`t` is the azimuthal angle defined positive from vertical + - :math:`p` is the radial angle defined positive CCW from Easting + + Parameters + ---------- + mesh : discretize.BaseMesh + The number of parameters accepted by the mapping is set to equal + *3\*mesh.nC* . + nP : int + Set the number of parameters accepted by the mapping directly. Used if the + number of parameters is known. Used generally when the number of parameters + is not equal to the number of cells in a mesh. + """ + + def __init__(self, mesh=None, nP=None, **kwargs): + if nP is not None: + assert nP % 3 == 0, "Number of parameters must be a multiple of 3" + super().__init__(mesh, nP, **kwargs) + self.model = None + + def sphericalDeriv(self, model): + if getattr(self, "model", None) is None: + self.model = model + + if getattr(self, "_sphericalDeriv", None) is None or not all( + self.model == model + ): + self.model = model + + # Do a double projection to make sure the parameters are bounded + m_xyz = mat_utils.spherical2cartesian(model.reshape((-1, 3), order="F")) + m_atp = mat_utils.cartesian2spherical( + m_xyz.reshape((-1, 3), order="F") + ).reshape((-1, 3), order="F") + + nC = m_atp[:, 0].shape[0] + + dm_dx = sp.hstack( + [ + sp.diags(np.cos(m_atp[:, 1]) * np.cos(m_atp[:, 2]), 0), + sp.diags( + -m_atp[:, 0] * np.sin(m_atp[:, 1]) * np.cos(m_atp[:, 2]), 0 + ), + sp.diags( + -m_atp[:, 0] * np.cos(m_atp[:, 1]) * np.sin(m_atp[:, 2]), 0 + ), + ] + ) + + dm_dy = sp.hstack( + [ + sp.diags(np.cos(m_atp[:, 1]) * np.sin(m_atp[:, 2]), 0), + sp.diags( + -m_atp[:, 0] * np.sin(m_atp[:, 1]) * np.sin(m_atp[:, 2]), 0 + ), + sp.diags( + m_atp[:, 0] * np.cos(m_atp[:, 1]) * np.cos(m_atp[:, 2]), 0 + ), + ] + ) + + dm_dz = sp.hstack( + [ + sp.diags(np.sin(m_atp[:, 1]), 0), + sp.diags(m_atp[:, 0] * np.cos(m_atp[:, 1]), 0), + csr((nC, nC)), + ] + ) + + self._sphericalDeriv = sp.vstack([dm_dx, dm_dy, dm_dz]) + + return self._sphericalDeriv + + def _transform(self, model): + return mat_utils.spherical2cartesian(model.reshape((-1, 3), order="F")) + + def inverse(self, u): + r"""Maps vectors in Cartesian coordinates to spherical coordinates. + + Let :math:`\mathbf{v_x}`, :math:`\mathbf{v_y}` and :math:`\mathbf{v_z}` + store the x, y and z components of a set of vectors in Cartesian + coordinates such that: + + .. math:: + \mathbf{u} = \begin{bmatrix} \mathbf{x} \\ \mathbf{y} \\ \mathbf{z} \end{bmatrix} + + The inverse mapping recovers the vectors in spherical coordinates, i.e.: + + .. math:: + \mathbf{m}(\mathbf{u}) = \begin{bmatrix} \mathbf{a} \\ \mathbf{t} \\ \mathbf{p} \end{bmatrix} + + where :math:`\mathbf{a}` are the amplitudes, :math:`\mathbf{t}` are the + azimuthal angles and :math:`\mathbf{p}` are the radial angles. + + Parameters + ---------- + u : numpy.ndarray + The x, y and z components of a set of vectors in Cartesian coordinates. + If the mapping is defined for a mesh, the numpy.ndarray has length + *3\*mesh.nC* . + + Returns + ------- + numpy.ndarray + The amplitudes (:math:`\mathbf{a}`), azimuthal angles (:math:`\mathbf{t}`) + and radial angles (:math:`\mathbf{p}`) for the set of vectors in spherical + coordinates. If the mapping is defined for a mesh, the numpy.ndarray has length + *3\*mesh.nC* . + """ + return mat_utils.cartesian2spherical(u.reshape((-1, 3), order="F")) + + @property + def shape(self): + r"""Dimensions of the mapping + + The dimensions of the mesh depend on the input arguments used + during instantiation. If *mesh* is used to define the + mapping, the shape of mapping operator is (*3\*mesh.nC*, *3\*mesh.nC*). + If *nP* is used to define the identity map, the mapping operator + has dimensions (*nP*, *nP*). If *mesh* and *nP* were ``None`` when + instantiating, the mapping has dimensions (``*``, ``*``) and may + act on a vector whose length is a multiple of 3. + + Returns + ------- + tuple + Dimensions of the mapping operator. If the dimensions of + the mapping are set, the return is a tuple (``int``,``int``). + If the mapping can act on a vector of arbitrary length, the + return is a tuple (``*``, ``*``). + """ + # return self.n_block*len(self.indices[0]), self.n_block*len(self.indices) + return (self.nP, self.nP) + + def deriv(self, m, v=None): + """Derivative of mapping with respect to the input parameters + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. If the + input argument *v* is not ``None``, the method returns the derivative times + the vector *v*. + """ + + if v is not None: + return self.sphericalDeriv(m) * v + return self.sphericalDeriv(m) + + @property + def is_linear(self): + return False + + +class Wires(object): + r"""Mapping class for organizing multiple parameter types into a single model. + + Let :math:`\mathbf{p_1}` and :math:`\mathbf{p_2}` be vectors that + contain the parameter values for two different parameter types; for example, + electrical conductivity and magnetic permeability. Here, all parameters + are organized into a single model :math:`\mathbf{m}` of the form: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{p_1} \\ \mathbf{p_2} \end{bmatrix} + + The ``Wires`` class constructs and applies the basic projection mappings + for extracting the values of a particular parameter type from the model. + For example: + + .. math:: + \mathbf{p_1} = \mathbf{P_{\! 1} m} + + where :math:`\mathbf{P_1}` is the projection matrix that extracts parameters + :math:`\mathbf{p_1}` from the complete set of model parameters :math:`\mathbf{m}`. + Likewise, there is a projection matrix for extracting :math:`\mathbf{p_2}`. + This can be extended to a model that containing more than 2 parameter types. + + Parameters + ---------- + args : tuple + Each input argument is a tuple (``str``, ``int``) that provides the name + and number of parameters for a given parameters type. + + Examples + -------- + Here we construct a wire mapping for a model where there + are two parameters types. Note that the number of parameters + of each type does not need to be the same. + + >>> from simpeg.maps import Wires, ReciprocalMap + >>> import numpy as np + + >>> p1 = np.r_[4.5, 2.7, 6.9, 7.1, 1.2] + >>> p2 = np.r_[10., 2., 5.]**-1 + >>> nP1 = len(p1) + >>> nP2 = len(p2) + >>> m = np.r_[p1, p2] + >>> m + array([4.5, 2.7, 6.9, 7.1, 1.2, 0.1, 0.5, 0.2]) + + Here we construct the wire map. The user provides a name + and the number of parameters for each type. The name + provided becomes the name of the method for constructing + the projection mapping. + + >>> wire_map = Wires(('name_1', nP1), ('name_2', nP2)) + + Here, we extract the values for the first parameter type. + + >>> wire_map.name_1 * m + array([4.5, 2.7, 6.9, 7.1, 1.2]) + + And here, we extract the values for the second parameter + type then apply a reciprocal mapping. + + >>> reciprocal_map = ReciprocalMap() + >>> reciprocal_map * wire_map.name_2 * m + array([10., 2., 5.]) + + """ + + def __init__(self, *args): + for arg in args: + assert ( + isinstance(arg, tuple) + and len(arg) == 2 + and isinstance(arg[0], str) + and + # TODO: this should be extended to a slice. + isinstance(arg[1], (int, np.integer)) + ), ( + "Each wire needs to be a tuple: (name, length). " + "You provided: {}".format(arg) + ) + + self._nP = int(np.sum([w[1] for w in args])) + start = 0 + maps = [] + for arg in args: + wire = Projection(self.nP, slice(start, start + arg[1])) + setattr(self, arg[0], wire) + maps += [(arg[0], wire)] + start += arg[1] + self.maps = maps + + self._tuple = namedtuple("Model", [w[0] for w in args]) + + def __mul__(self, val): + assert isinstance(val, np.ndarray) + split = [] + for _, w in self.maps: + split += [w * val] + return self._tuple(*split) + + @property + def nP(self): + r"""Number of parameters the mapping acts on. + + Returns + ------- + int + Number of parameters that the mapping acts on. + """ + return self._nP + + +class TileMap(IdentityMap): + """ + Mapping for tiled inversion. + + Uses volume averaging to map a model defined on a global mesh to the + local mesh. Everycell in the local mesh must also be in the global mesh. + """ + + def __init__( + self, + global_mesh, + global_active, + local_mesh, + tol=1e-8, + components=1, + **kwargs, + ): + """ + Parameters + ---------- + global_mesh : discretize.TreeMesh + Global TreeMesh defining the entire domain. + global_active : numpy.ndarray of bool or int + Defines the active cells in the global mesh. + local_mesh : discretize.TreeMesh + Local TreeMesh for the simulation. + tol : float, optional + Tolerance to avoid zero division + components : int, optional + Number of components in the model. E.g. a vector model in 3D would have 3 + components. + """ + super().__init__(mesh=None, **kwargs) + self._global_mesh = validate_type( + "global_mesh", global_mesh, discretize.TreeMesh, cast=False + ) + self._local_mesh = validate_type( + "local_mesh", local_mesh, discretize.TreeMesh, cast=False + ) + + self._global_active = validate_active_indices( + "global_active", global_active, self.global_mesh.n_cells + ) + + self._tol = validate_float("tol", tol, min_val=0.0, inclusive_min=False) + self._components = validate_integer("components", components, min_val=1) + + # trigger creation of P + self.P + + @property + def global_mesh(self): + """Global TreeMesh defining the entire domain. + + Returns + ------- + discretize.TreeMesh + """ + return self._global_mesh + + @property + def local_mesh(self): + """Local TreeMesh defining the local domain. + + Returns + ------- + discretize.TreeMesh + """ + return self._local_mesh + + @property + def global_active(self): + """Defines the active cells in the global mesh. + + Returns + ------- + (global_mesh.n_cells) numpy.ndarray of bool + """ + return self._global_active + + @property + def local_active(self): + """ + This is the local_active of the global_active used in the global problem. + + Returns + ------- + (local_mesh.n_cells) numpy.ndarray of bool + """ + return self._local_active + + @property + def tol(self): + """Tolerance to avoid zero division. + + Returns + ------- + float + """ + return self._tol + + @property + def components(self): + """Number of components in the model. + + Returns + ------- + int + """ + return self._components + + @property + def P(self): + """ + Set the projection matrix with partial volumes + """ + if getattr(self, "_P", None) is None: + in_local = self.local_mesh._get_containing_cell_indexes( + self.global_mesh.cell_centers + ) + + P = ( + sp.csr_matrix( + ( + self.global_mesh.cell_volumes, + (in_local, np.arange(self.global_mesh.nC)), + ), + shape=(self.local_mesh.nC, self.global_mesh.nC), + ) + * speye(self.global_mesh.nC)[:, self.global_active] + ) + + self._local_active = mkvc(np.sum(P, axis=1) > 0) + + P = P[self.local_active, :] + + self._P = sp.block_diag( + [ + sdiag(1.0 / self.local_mesh.cell_volumes[self.local_active]) * P + for ii in range(self.components) + ] + ) + + return self._P + + def _transform(self, m): + return self.P * m + + @property + def shape(self): + """ + Shape of the matrix operation (number of indices x nP) + """ + return self.P.shape + + def deriv(self, m, v=None): + """ + :param numpy.ndarray m: model + :rtype: scipy.sparse.csr_matrix + :return: derivative of transformed model + """ + if v is not None: + return self.P * v + return self.P diff --git a/simpeg/maps/_clustering.py b/simpeg/maps/_clustering.py new file mode 100644 index 0000000000..5761e92d72 --- /dev/null +++ b/simpeg/maps/_clustering.py @@ -0,0 +1,152 @@ +""" +Map classes for petrophysics clusters. +""" + +import numpy as np +from numpy.polynomial import polynomial + + +from ..utils import validate_ndarray_with_shape + +from ._base import IdentityMap + + +class PolynomialPetroClusterMap(IdentityMap): + """ + Modeling polynomial relationships between physical properties + + Parameters + ---------- + coeffxx : array_like, optional + Coefficients for the xx component. Default is [0, 1] + coeffxy : array_like, optional + Coefficients for the xy component. Default is [0] + coeffyx : array_like, optional + Coefficients for the yx component. Default is [0] + coeffyy : array_like, optional + Coefficients for the yy component. Default is [0, 1] + """ + + def __init__( + self, + coeffxx=None, + coeffxy=None, + coeffyx=None, + coeffyy=None, + mesh=None, + nP=None, + **kwargs, + ): + if coeffxx is None: + coeffxx = np.r_[0.0, 1.0] + if coeffxy is None: + coeffxy = np.r_[0.0] + if coeffyx is None: + coeffyx = np.r_[0.0] + if coeffyy is None: + coeffyy = np.r_[0.0, 1.0] + + self._coeffxx = validate_ndarray_with_shape("coeffxx", coeffxx, shape=("*",)) + self._coeffxy = validate_ndarray_with_shape("coeffxy", coeffxy, shape=("*",)) + self._coeffyx = validate_ndarray_with_shape("coeffyx", coeffyx, shape=("*",)) + self._coeffyy = validate_ndarray_with_shape("coeffyy", coeffyy, shape=("*",)) + + self._polynomialxx = polynomial.Polynomial(self.coeffxx) + self._polynomialxy = polynomial.Polynomial(self.coeffxy) + self._polynomialyx = polynomial.Polynomial(self.coeffyx) + self._polynomialyy = polynomial.Polynomial(self.coeffyy) + self._polynomialxx_deriv = self._polynomialxx.deriv(m=1) + self._polynomialxy_deriv = self._polynomialxy.deriv(m=1) + self._polynomialyx_deriv = self._polynomialyx.deriv(m=1) + self._polynomialyy_deriv = self._polynomialyy.deriv(m=1) + + super().__init__(mesh=mesh, nP=nP, **kwargs) + + @property + def coeffxx(self): + """Coefficients for the xx component. + + Returns + ------- + numpy.ndarray + """ + return self._coeffxx + + @property + def coeffxy(self): + """Coefficients for the xy component. + + Returns + ------- + numpy.ndarray + """ + return self._coeffxy + + @property + def coeffyx(self): + """Coefficients for the yx component. + + Returns + ------- + numpy.ndarray + """ + return self._coeffyx + + @property + def coeffyy(self): + """Coefficients for the yy component. + + Returns + ------- + numpy.ndarray + """ + return self._coeffyy + + def _transform(self, m): + out = m.copy() + out[:, 0] = self._polynomialxx(m[:, 0]) + self._polynomialxy(m[:, 1]) + out[:, 1] = self._polynomialyx(m[:, 0]) + self._polynomialyy(m[:, 1]) + return out + + def inverse(self, D): + r""" + :param numpy.array D: physical property + :rtype: numpy.array + :return: model + + The *transformInverse* changes the physical property into the + model. + + .. math:: + + m = \log{\sigma} + + """ + raise NotImplementedError("Inverse is not implemented.") + + def _derivmatrix(self, m): + return np.r_[ + [ + [ + self._polynomialxx_deriv(m[:, 0])[0], + self._polynomialyx_deriv(m[:, 0])[0], + ], + [ + self._polynomialxy_deriv(m[:, 1])[0], + self._polynomialyy_deriv(m[:, 1])[0], + ], + ] + ] + + def deriv(self, m, v=None): + """""" + if v is None: + out = self._derivmatrix(m.reshape(-1, 2)) + return out + else: + out = np.dot(self._derivmatrix(m.reshape(-1, 2)), v.reshape(2, -1)) + return out + + @property + def is_linear(self): + return False diff --git a/simpeg/maps/_injection.py b/simpeg/maps/_injection.py new file mode 100644 index 0000000000..4b80d6cb7f --- /dev/null +++ b/simpeg/maps/_injection.py @@ -0,0 +1,284 @@ +""" +Injection and interpolation map classes. +""" + +import discretize +import numpy as np +import scipy.sparse as sp + +from ..utils import ( + validate_type, + validate_ndarray_with_shape, + validate_float, + validate_active_indices, +) +from ._base import IdentityMap + + +class Mesh2Mesh(IdentityMap): + """ + Takes a model on one mesh are translates it to another mesh. + """ + + def __init__(self, meshes, indActive=None, **kwargs): + # Sanity checks for the meshes parameter + try: + mesh, mesh2 = meshes + except TypeError: + raise TypeError("Couldn't unpack 'meshes' into two meshes.") + + super().__init__(mesh=mesh, **kwargs) + + self.mesh2 = mesh2 + # Check dimensions of both meshes + if mesh.dim != mesh2.dim: + raise ValueError( + f"Found meshes with dimensions '{mesh.dim}' and '{mesh2.dim}'. " + + "Both meshes must have the same dimension." + ) + self.indActive = indActive + + # reset to not accepted None for mesh + @IdentityMap.mesh.setter + def mesh(self, value): + self._mesh = validate_type("mesh", value, discretize.base.BaseMesh, cast=False) + + @property + def mesh2(self): + """The source mesh used for the mapping. + + Returns + ------- + discretize.base.BaseMesh + """ + return self._mesh2 + + @mesh2.setter + def mesh2(self, value): + self._mesh2 = validate_type( + "mesh2", value, discretize.base.BaseMesh, cast=False + ) + + @property + def indActive(self): + """Active indices on target mesh. + + Returns + ------- + (mesh.n_cells) numpy.ndarray of bool or none + """ + return self._indActive + + @indActive.setter + def indActive(self, value): + if value is not None: + value = validate_active_indices("indActive", value, self.mesh.n_cells) + self._indActive = value + + @property + def P(self): + if getattr(self, "_P", None) is None: + self._P = self.mesh2.get_interpolation_matrix( + ( + self.mesh.cell_centers[self.indActive, :] + if self.indActive is not None + else self.mesh.cell_centers + ), + "CC", + zeros_outside=True, + ) + return self._P + + @property + def shape(self): + """Number of parameters in the model.""" + if self.indActive is not None: + return (self.indActive.sum(), self.mesh2.nC) + return (self.mesh.nC, self.mesh2.nC) + + @property + def nP(self): + """Number of parameters in the model.""" + return self.mesh2.nC + + def _transform(self, m): + return self.P * m + + def deriv(self, m, v=None): + if v is not None: + return self.P * v + return self.P + + +class InjectActiveCells(IdentityMap): + r"""Map active cells model to all cell of a mesh. + + The ``InjectActiveCells`` class is used to define the mapping when + the model consists of physical property values for a set of active + mesh cells; e.g. cells below topography. For a discrete set of + model parameters :math:`\mathbf{m}` defined on a set of active + cells, the mapping :math:`\mathbf{u}(\mathbf{m})` is defined as: + + .. math:: + \mathbf{u}(\mathbf{m}) = \mathbf{Pm} + \mathbf{d}\, m_\perp + + where :math:`\mathbf{P}` is a (*nC* , *nP*) projection matrix from + active cells to all mesh cells, and :math:`\mathbf{d}` is a + (*nC* , 1) matrix that projects the inactive cell value + :math:`m_\perp` to all inactive mesh cells. + + Parameters + ---------- + mesh : discretize.BaseMesh + A discretize mesh + indActive : numpy.ndarray + Active cells array. Can be a boolean ``numpy.ndarray`` of length *mesh.nC* + or a ``numpy.ndarray`` of ``int`` containing the indices of the active cells. + valInactive : float or numpy.ndarray + The physical property value assigned to all inactive cells in the mesh + + """ + + def __init__(self, mesh, indActive=None, valInactive=0.0, nC=None): + self.mesh = mesh + self.nC = nC or mesh.nC + + self._indActive = validate_active_indices("indActive", indActive, self.nC) + self._nP = np.sum(self.indActive) + + self.P = sp.eye(self.nC, format="csr")[:, self.indActive] + + self.valInactive = valInactive + + @property + def valInactive(self): + """The physical property value assigned to all inactive cells in the mesh. + + Returns + ------- + numpy.ndarray + """ + return self._valInactive + + @valInactive.setter + def valInactive(self, value): + n_inactive = self.nC - self.nP + try: + value = validate_float("valInactive", value) + value = np.full(n_inactive, value) + except Exception: + pass + value = validate_ndarray_with_shape("valInactive", value, shape=(n_inactive,)) + + self._valInactive = np.zeros(self.nC, dtype=float) + self._valInactive[~self.indActive] = value + + @property + def indActive(self): + """ + + Returns + ------- + numpy.ndarray of bool + + """ + return self._indActive + + @property + def shape(self): + """Dimensions of the mapping + + Returns + ------- + tuple of int + Where *nP* is the number of active cells and *nC* is + number of cell in the mesh, **shape** returns a + tuple (*nC* , *nP*). + """ + return (self.nC, self.nP) + + @property + def nP(self): + """Number of parameters the model acts on. + + Returns + ------- + int + Number of parameters the model acts on; i.e. the number of active cells + """ + return int(self.indActive.sum()) + + def _transform(self, m): + if m.ndim > 1: + return self.P * m + self.valInactive[:, None] + return self.P * m + self.valInactive + + def inverse(self, u): + r"""Recover the model parameters (active cells) from a set of physical + property values defined on the entire mesh. + + For a discrete set of model parameters :math:`\mathbf{m}` defined + on a set of active cells, the mapping :math:`\mathbf{u}(\mathbf{m})` + is defined as: + + .. math:: + \mathbf{u}(\mathbf{m}) = \mathbf{Pm} + \mathbf{d} \,m_\perp + + where :math:`\mathbf{P}` is a (*nC* , *nP*) projection matrix from + active cells to all mesh cells, and :math:`\mathbf{d}` is a + (*nC* , 1) matrix that projects the inactive cell value + :math:`m_\perp` to all inactive mesh cells. + + The inverse mapping is given by: + + .. math:: + \mathbf{m}(\mathbf{u}) = \mathbf{P^T u} + + Parameters + ---------- + u : (mesh.nC) numpy.ndarray + A vector which contains physical property values for all + mesh cells. + """ + return self.P.T * u + + def deriv(self, m, v=None): + r"""Derivative of the mapping with respect to the input parameters. + + For a discrete set of model parameters :math:`\mathbf{m}` defined + on a set of active cells, the mapping :math:`\mathbf{u}(\mathbf{m})` + is defined as: + + .. math:: + \mathbf{u}(\mathbf{m}) = \mathbf{Pm} + \mathbf{d} \, m_\perp + + where :math:`\mathbf{P}` is a (*nC* , *nP*) projection matrix from + active cells to all mesh cells, and :math:`\mathbf{d}` is a + (*nC* , 1) matrix that projects the inactive cell value + :math:`m_\perp` to all inactive mesh cells. + + the **deriv** method returns the derivative of :math:`\mathbf{u}` with respect + to the model parameters; i.e.: + + .. math:: + \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = \mathbf{P} + + Note that in this case, **deriv** simply returns a sparse projection matrix. + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. If the + input argument *v* is not ``None``, the method returns the derivative times + the vector *v*. + """ + if v is not None: + return self.P * v + return self.P diff --git a/simpeg/maps/_parametric.py b/simpeg/maps/_parametric.py new file mode 100644 index 0000000000..db52989620 --- /dev/null +++ b/simpeg/maps/_parametric.py @@ -0,0 +1,2634 @@ +""" +Parametric map classes. +""" + +import discretize +import numpy as np +from numpy.polynomial import polynomial +import scipy.sparse as sp +from scipy.interpolate import UnivariateSpline + +from discretize.utils import sdiag + +from ..utils import ( + validate_type, + validate_ndarray_with_shape, + validate_float, + validate_integer, + validate_string, + validate_active_indices, +) +from ._base import IdentityMap + + +class ParametricCircleMap(IdentityMap): + r"""Mapping for a parameterized circle. + + Define the mapping from a parameterized model for a circle in a wholespace + to all cells within a 2D mesh. For a circle within a wholespace, the + model is defined by 5 parameters: the background physical property value + (:math:`\sigma_0`), the physical property value for the circle + (:math:`\sigma_c`), the x location :math:`x_0` and y location :math:`y_0` + for center of the circle, and the circle's radius (:math:`R`). + + Let :math:`\mathbf{m} = [\sigma_0, \sigma_1, x_0, y_0, R]` be the set of + model parameters the defines a circle within a wholespace. The mapping + :math:`\mathbf{u}(\mathbf{m})` from the parameterized model to all cells + within a 2D mesh is given by: + + .. math:: + + \mathbf{u}(\mathbf{m}) = \sigma_0 + (\sigma_1 - \sigma_0) + \bigg [ \frac{1}{2} + \pi^{-1} \arctan \bigg ( a \big [ \sqrt{(\mathbf{x_c}-x_0)^2 + + (\mathbf{y_c}-y_0)^2} - R \big ] \bigg ) \bigg ] + + where :math:`\mathbf{x_c}` and :math:`\mathbf{y_c}` are vectors storing + the x and y positions of all cell centers for the 2D mesh and :math:`a` + is a user-defined constant which defines the sharpness of boundary of the + circular structure. + + Parameters + ---------- + mesh : discretize.BaseMesh + A 2D discretize mesh + logSigma : bool + If ``True``, parameters :math:`\sigma_0` and :math:`\sigma_1` represent the + natural log of the physical property values for the background and circle, + respectively. + slope : float + A constant for defining the sharpness of the boundary between the circle + and the wholespace. The sharpness increases as *slope* is increased. + + Examples + -------- + Here we define the parameterized model for a circle in a wholespace. We then + create and use a ``ParametricCircleMap`` to map the model to a 2D mesh. + + >>> from simpeg.maps import ParametricCircleMap + >>> from discretize import TensorMesh + >>> import numpy as np + >>> import matplotlib.pyplot as plt + + >>> h = 0.5*np.ones(20) + >>> mesh = TensorMesh([h, h]) + + >>> sigma0, sigma1, x0, y0, R = 0., 10., 4., 6., 2. + >>> model = np.r_[sigma0, sigma1, x0, y0, R] + >>> mapping = ParametricCircleMap(mesh, logSigma=False, slope=2) + + >>> fig = plt.figure(figsize=(5, 5)) + >>> ax = fig.add_subplot(111) + >>> mesh.plot_image(mapping * model, ax=ax) + + """ + + def __init__(self, mesh, logSigma=True, slope=0.1): + super().__init__(mesh=mesh) + if mesh.dim != 2: + raise NotImplementedError( + "Mesh must be 2D, not implemented yet for other dimensions." + ) + # TODO: this should be done through a composition with and ExpMap + self.logSigma = logSigma + self.slope = slope + + @property + def slope(self): + """Sharpness of the boundary. + + Larger number are sharper. + + Returns + ------- + float + """ + return self._slope + + @slope.setter + def slope(self, value): + self._slope = validate_float("slope", value, min_val=0.0, inclusive_min=False) + + @property + def logSigma(self): + """Whether the input needs to be transformed by an exponential + + Returns + ------- + float + """ + return self._logSigma + + @logSigma.setter + def logSigma(self, value): + self._logSigma = validate_type("logSigma", value, bool) + + @property + def nP(self): + r"""Number of parameters the mapping acts on; i.e. 5. + + Returns + ------- + int + The ``ParametricCircleMap`` acts on 5 parameters. + """ + return 5 + + def _transform(self, m): + a = self.slope + sig1, sig2, x, y, r = m[0], m[1], m[2], m[3], m[4] + if self.logSigma: + sig1, sig2 = np.exp(sig1), np.exp(sig2) + X = self.mesh.cell_centers[:, 0] + Y = self.mesh.cell_centers[:, 1] + return sig1 + (sig2 - sig1) * ( + np.arctan(a * (np.sqrt((X - x) ** 2 + (Y - y) ** 2) - r)) / np.pi + 0.5 + ) + + def deriv(self, m, v=None): + r"""Derivative of the mapping with respect to the input parameters. + + Let :math:`\mathbf{m} = [\sigma_0, \sigma_1, x_0, y_0, R]` be the set of + model parameters the defines a circle within a wholespace. The mapping + :math:`\mathbf{u}(\mathbf{m})`from the parameterized model to all cells + within a 2D mesh is given by: + + .. math:: + \mathbf{u}(\mathbf{m}) = \sigma_0 + (\sigma_1 - \sigma_0) + \bigg [ \frac{1}{2} + \pi^{-1} \arctan \bigg ( a \big [ \sqrt{(\mathbf{x_c}-x_0)^2 + + (\mathbf{y_c}-y_0)^2} - R \big ] \bigg ) \bigg ] + + The derivative of the mapping with respect to the model parameters is a + ``numpy.ndarray`` of shape (*mesh.nC*, 5) given by: + + .. math:: + \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = + \Bigg [ \frac{\partial \mathbf{u}}{\partial \sigma_0} \;\; + \Bigg [ \frac{\partial \mathbf{u}}{\partial \sigma_1} \;\; + \Bigg [ \frac{\partial \mathbf{u}}{\partial x_0} \;\; + \Bigg [ \frac{\partial \mathbf{u}}{\partial y_0} \;\; + \Bigg [ \frac{\partial \mathbf{u}}{\partial R} + \Bigg ] + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. If the + input argument *v* is not ``None``, the method returns the derivative times + the vector *v*. + """ + a = self.slope + sig1, sig2, x, y, r = m[0], m[1], m[2], m[3], m[4] + if self.logSigma: + sig1, sig2 = np.exp(sig1), np.exp(sig2) + X = self.mesh.cell_centers[:, 0] + Y = self.mesh.cell_centers[:, 1] + if self.logSigma: + g1 = ( + -( + np.arctan(a * (-r + np.sqrt((X - x) ** 2 + (Y - y) ** 2))) / np.pi + + 0.5 + ) + * sig1 + + sig1 + ) + g2 = ( + np.arctan(a * (-r + np.sqrt((X - x) ** 2 + (Y - y) ** 2))) / np.pi + 0.5 + ) * sig2 + else: + g1 = ( + -( + np.arctan(a * (-r + np.sqrt((X - x) ** 2 + (Y - y) ** 2))) / np.pi + + 0.5 + ) + + 1.0 + ) + g2 = ( + np.arctan(a * (-r + np.sqrt((X - x) ** 2 + (Y - y) ** 2))) / np.pi + 0.5 + ) + + g3 = ( + a + * (-X + x) + * (-sig1 + sig2) + / ( + np.pi + * (a**2 * (-r + np.sqrt((X - x) ** 2 + (Y - y) ** 2)) ** 2 + 1) + * np.sqrt((X - x) ** 2 + (Y - y) ** 2) + ) + ) + + g4 = ( + a + * (-Y + y) + * (-sig1 + sig2) + / ( + np.pi + * (a**2 * (-r + np.sqrt((X - x) ** 2 + (Y - y) ** 2)) ** 2 + 1) + * np.sqrt((X - x) ** 2 + (Y - y) ** 2) + ) + ) + + g5 = ( + -a + * (-sig1 + sig2) + / (np.pi * (a**2 * (-r + np.sqrt((X - x) ** 2 + (Y - y) ** 2)) ** 2 + 1)) + ) + + if v is not None: + return sp.csr_matrix(np.c_[g1, g2, g3, g4, g5]) * v + return sp.csr_matrix(np.c_[g1, g2, g3, g4, g5]) + + @property + def is_linear(self): + return False + + +class ParametricPolyMap(IdentityMap): + r"""Mapping for 2 layer model whose interface is defined by a polynomial. + + This mapping is used when the cells lying below the Earth's surface can + be parameterized by a 2 layer model whose interface is defined by a + polynomial function. The model is defined by the physical property + values for each unit (:math:`\sigma_1` and :math:`\sigma_2`) and the + coefficients for the polynomial function (:math:`\mathbf{c}`). + + **For a 2D mesh** , the interface is defined by a polynomial function + of the form: + + .. math:: + p(x) = \sum_{i=0}^N c_i x^i + + where :math:`c_i` are the polynomial coefficients and :math:`N` is + the order of the polynomial. In this case, the model is defined as + + .. math:: + \mathbf{m} = [\sigma_1, \;\sigma_2,\; c_0 ,\;\ldots\; ,\; c_N] + + The mapping :math:`\mathbf{u}(\mathbf{m})` from the model to the mesh + is given by: + + .. math:: + + \mathbf{u}(\mathbf{m}) = \sigma_1 + (\sigma_2 - \sigma_1) + \bigg [ \frac{1}{2} + \pi^{-1} \arctan \bigg ( + a \Big ( \mathbf{p}(\mathbf{x_c}) - \mathbf{y_c} \Big ) + \bigg ) \bigg ] + + where :math:`\mathbf{x_c}` and :math:`\mathbf{y_c}` are vectors containing the + x and y cell center locations for all active cells in the mesh, and :math:`a` is a + parameter which defines the sharpness of the boundary between the two layers. + :math:`\mathbf{p}(\mathbf{x_c})` evaluates the polynomial function for + every element in :math:`\mathbf{x_c}`. + + **For a 3D mesh** , the interface is defined by a 2D polynomial function + of the form: + + .. math:: + p(x,y) = + \sum_{j=0}^{N_y} \sum_{i=0}^{N_x} c_{ij} \, x^i y^j + + where :math:`c_{ij}` are the polynomial coefficients. :math:`N_x` + and :math:`N_y` define the order of the polynomial in :math:`x` and + :math:`y`, respectively. In this case, the model is defined as: + + .. math:: + \mathbf{m} = [\sigma_1, \; \sigma_2, \; c_{0,0} , \; c_{1,0} , \;\ldots , \; c_{N_x, N_y}] + + The mapping :math:`\mathbf{u}(\mathbf{m})` from the model to the mesh + is given by: + + .. math:: + + \mathbf{u}(\mathbf{m}) = \sigma_1 + (\sigma_2 - \sigma_1) + \bigg [ \frac{1}{2} + \pi^{-1} \arctan \bigg ( + a \Big ( \mathbf{p}(\mathbf{x_c,y_c}) - \mathbf{z_c} \Big ) + \bigg ) \bigg ] + + where :math:`\mathbf{x_c}, \mathbf{y_c}` and :math:`\mathbf{y_z}` are vectors + containing the x, y and z cell center locations for all active cells in the mesh. + :math:`\mathbf{p}(\mathbf{x_c, y_c})` evaluates the polynomial function for + every corresponding pair of :math:`\mathbf{x_c}` and :math:`\mathbf{y_c}` + elements. + + Parameters + ---------- + mesh : discretize.BaseMesh + A discretize mesh + order : int or list of int + Order of the polynomial. For a 2D mesh, this is an ``int``. For a 3D + mesh, the order for both variables is entered separately; i.e. + [*order1* , *order2*]. + logSigma : bool + If ``True``, parameters :math:`\sigma_1` and :math:`\sigma_2` represent + the natural log of a physical property. + normal : {'x', 'y', 'z'} + actInd : numpy.ndarray + Active cells array. Can be a boolean ``numpy.ndarray`` of length *mesh.nC* + or a ``numpy.ndarray`` of ``int`` containing the indices of the active cells. + + Examples + -------- + In this example, we define a 2 layer model whose interface is sharp and lies + along a polynomial function :math:`y(x)=c_0 + c_1 x`. In this case, the model is + defined as :math:`\mathbf{m} = [\sigma_1 , \sigma_2 , c_0 , c_1]`. We construct + a polynomial mapping from the model to the set of active cells (i.e. below the surface), + We then use an active cells mapping to map from the set of active cells to all + cells in the 2D mesh. + + >>> from simpeg.maps import ParametricPolyMap, InjectActiveCells + >>> from discretize import TensorMesh + >>> import numpy as np + >>> import matplotlib.pyplot as plt + + >>> h = 0.5*np.ones(20) + >>> mesh = TensorMesh([h, h]) + >>> ind_active = mesh.cell_centers[:, 1] < 8 + >>> + >>> sig1, sig2, c0, c1 = 10., 5., 2., 0.5 + >>> model = np.r_[sig1, sig2, c0, c1] + + >>> poly_map = ParametricPolyMap( + >>> mesh, order=1, logSigma=False, normal='Y', actInd=ind_active, slope=1e4 + >>> ) + >>> act_map = InjectActiveCells(mesh, ind_active, 0.) + + >>> fig = plt.figure(figsize=(5, 5)) + >>> ax = fig.add_subplot(111) + >>> mesh.plot_image(act_map * poly_map * model, ax=ax) + >>> ax.set_title('Mapping on a 2D mesh') + + Here, we recreate the previous example on a 3D mesh but with a smoother interface. + For a 3D mesh, the 2D polynomial defining the sloping interface is given by + :math:`z(x,y) = c_0 + c_x x + c_y y + c_{xy} xy`. In this case, the model is + defined as :math:`\mathbf{m} = [\sigma_1 , \sigma_2 , c_0 , c_x, c_y, c_{xy}]`. + + >>> mesh = TensorMesh([h, h, h]) + >>> ind_active = mesh.cell_centers[:, 2] < 8 + >>> + >>> sig1, sig2, c0, cx, cy, cxy = 10., 5., 2., 0.5, 0., 0. + >>> model = np.r_[sig1, sig2, c0, cx, cy, cxy] + >>> + >>> poly_map = ParametricPolyMap( + >>> mesh, order=[1, 1], logSigma=False, normal='Z', actInd=ind_active, slope=2 + >>> ) + >>> act_map = InjectActiveCells(mesh, ind_active, 0.) + >>> + >>> fig = plt.figure(figsize=(5, 5)) + >>> ax = fig.add_subplot(111) + >>> mesh.plot_slice(act_map * poly_map * model, ax=ax, normal='Y', ind=10) + >>> ax.set_title('Mapping on a 3D mesh') + + """ + + def __init__(self, mesh, order, logSigma=True, normal="X", actInd=None, slope=1e4): + super().__init__(mesh=mesh) + self.logSigma = logSigma + self.order = order + self.normal = normal + self.slope = slope + + if actInd is None: + actInd = np.ones(mesh.n_cells, dtype=bool) + self.actInd = actInd + + @property + def slope(self): + """Sharpness of the boundary. + + Larger number are sharper. + + Returns + ------- + float + """ + return self._slope + + @slope.setter + def slope(self, value): + self._slope = validate_float("slope", value, min_val=0.0, inclusive_min=False) + + @property + def logSigma(self): + """Whether the input needs to be transformed by an exponential + + Returns + ------- + float + """ + return self._logSigma + + @logSigma.setter + def logSigma(self, value): + self._logSigma = validate_type("logSigma", value, bool) + + @property + def normal(self): + """The projection axis. + + Returns + ------- + str + """ + return self._normal + + @normal.setter + def normal(self, value): + self._normal = validate_string("normal", value, ("x", "y", "z")) + + @property + def actInd(self): + """Active indices of the mesh. + + Returns + ------- + (mesh.n_cells) numpy.ndarray of bool + """ + return self._actInd + + @actInd.setter + def actInd(self, value): + self._actInd = validate_active_indices("actInd", value, self.mesh.n_cells) + self._nC = sum(self._actInd) + + @property + def shape(self): + """Dimensions of the mapping. + + Returns + ------- + tuple of int + The dimensions of the mapping as a tuple of the form + (*nC* , *nP*), where *nP* is the number of model parameters + the mapping acts on and *nC* is the number of active cells + being mapping to. If *actInd* is ``None``, then + *nC = mesh.nC*. + """ + return (self.nC, self.nP) + + @property + def nC(self): + """Number of active cells being mapped too. + + Returns + ------- + int + """ + return self._nC + + @property + def nP(self): + """Number of parameters the mapping acts on. + + Returns + ------- + int + The number of parameters the mapping acts on. + """ + if np.isscalar(self.order): + nP = self.order + 3 + else: + nP = (self.order[0] + 1) * (self.order[1] + 1) + 2 + return nP + + def _transform(self, m): + # Set model parameters + alpha = self.slope + sig1, sig2 = m[0], m[1] + c = m[2:] + if self.logSigma: + sig1, sig2 = np.exp(sig1), np.exp(sig2) + + # 2D + if self.mesh.dim == 2: + X = self.mesh.cell_centers[self.actInd, 0] + Y = self.mesh.cell_centers[self.actInd, 1] + if self.normal == "x": + f = polynomial.polyval(Y, c) - X + elif self.normal == "y": + f = polynomial.polyval(X, c) - Y + else: + raise (Exception("Input for normal = X or Y or Z")) + + # 3D + elif self.mesh.dim == 3: + X = self.mesh.cell_centers[self.actInd, 0] + Y = self.mesh.cell_centers[self.actInd, 1] + Z = self.mesh.cell_centers[self.actInd, 2] + + if self.normal == "x": + f = ( + polynomial.polyval2d( + Y, + Z, + c.reshape((self.order[0] + 1, self.order[1] + 1), order="F"), + ) + - X + ) + elif self.normal == "y": + f = ( + polynomial.polyval2d( + X, + Z, + c.reshape((self.order[0] + 1, self.order[1] + 1), order="F"), + ) + - Y + ) + elif self.normal == "z": + f = ( + polynomial.polyval2d( + X, + Y, + c.reshape((self.order[0] + 1, self.order[1] + 1), order="F"), + ) + - Z + ) + else: + raise (Exception("Input for normal = X or Y or Z")) + + else: + raise (Exception("Only supports 2D or 3D")) + + return sig1 + (sig2 - sig1) * (np.arctan(alpha * f) / np.pi + 0.5) + + def deriv(self, m, v=None): + r"""Derivative of the mapping with respect to the model. + + For a model :math:`\mathbf{m} = [\sigma_1, \sigma_2, \mathbf{c}]`, + the derivative of the mapping with respect to the model parameters is a + ``numpy.ndarray`` of shape (*mesh.nC*, *nP*) of the form: + + .. math:: + \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = + \Bigg [ \frac{\partial \mathbf{u}}{\partial \sigma_0} \;\; + \Bigg [ \frac{\partial \mathbf{u}}{\partial \sigma_1} \;\; + \Bigg [ \frac{\partial \mathbf{u}}{\partial c_0} \;\; + \Bigg [ \frac{\partial \mathbf{u}}{\partial c_1} \;\; + \cdots \;\; + \Bigg [ \frac{\partial \mathbf{u}}{\partial c_N} + \Bigg ] + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. If the + input argument *v* is not ``None``, the method returns the derivative times + the vector *v*. + + """ + alpha = self.slope + sig1, sig2, c = m[0], m[1], m[2:] + if self.logSigma: + sig1, sig2 = np.exp(sig1), np.exp(sig2) + + # 2D + if self.mesh.dim == 2: + X = self.mesh.cell_centers[self.actInd, 0] + Y = self.mesh.cell_centers[self.actInd, 1] + + if self.normal == "x": + f = polynomial.polyval(Y, c) - X + V = polynomial.polyvander(Y, len(c) - 1) + elif self.normal == "y": + f = polynomial.polyval(X, c) - Y + V = polynomial.polyvander(X, len(c) - 1) + else: + raise (Exception("Input for normal = X or Y")) + + # 3D + elif self.mesh.dim == 3: + X = self.mesh.cell_centers[self.actInd, 0] + Y = self.mesh.cell_centers[self.actInd, 1] + Z = self.mesh.cell_centers[self.actInd, 2] + + if self.normal == "x": + f = ( + polynomial.polyval2d( + Y, Z, c.reshape((self.order[0] + 1, self.order[1] + 1)) + ) + - X + ) + V = polynomial.polyvander2d(Y, Z, self.order) + elif self.normal == "y": + f = ( + polynomial.polyval2d( + X, Z, c.reshape((self.order[0] + 1, self.order[1] + 1)) + ) + - Y + ) + V = polynomial.polyvander2d(X, Z, self.order) + elif self.normal == "z": + f = ( + polynomial.polyval2d( + X, Y, c.reshape((self.order[0] + 1, self.order[1] + 1)) + ) + - Z + ) + V = polynomial.polyvander2d(X, Y, self.order) + else: + raise (Exception("Input for normal = X or Y or Z")) + + if self.logSigma: + g1 = -(np.arctan(alpha * f) / np.pi + 0.5) * sig1 + sig1 + g2 = (np.arctan(alpha * f) / np.pi + 0.5) * sig2 + else: + g1 = -(np.arctan(alpha * f) / np.pi + 0.5) + 1.0 + g2 = np.arctan(alpha * f) / np.pi + 0.5 + + g3 = sdiag(alpha * (sig2 - sig1) / (1.0 + (alpha * f) ** 2) / np.pi) * V + + if v is not None: + return sp.csr_matrix(np.c_[g1, g2, g3]) * v + return sp.csr_matrix(np.c_[g1, g2, g3]) + + @property + def is_linear(self): + return False + + +class ParametricSplineMap(IdentityMap): + r"""Mapping to parameterize the boundary between two geological units using + spline interpolation. + + .. math:: + + g = f(x)-y + + Define the model as: + + .. math:: + + m = [\sigma_1, \sigma_2, y] + + Parameters + ---------- + mesh : discretize.BaseMesh + A discretize mesh + pts : (n) numpy.ndarray + Points for the 1D spline tie points. + ptsv : (2) array_like + Points for linear interpolation between two splines in 3D. + order : int + Order of the spline mapping; e.g. 3 is cubic spline + logSigma : bool + If ``True``, :math:`\sigma_1` and :math:`\sigma_2` represent the natural + log of some physical property value for each unit. + normal : {'x', 'y', 'z'} + Defines the general direction of the normal vector for the interface. + slope : float + Parameter for defining the sharpness of the boundary. The sharpness is increased + if *slope* is large. + + Examples + -------- + In this example, we define a 2 layered model with a sloping + interface on a 2D mesh. The model consists of the physical + property values for the layers and the known elevations + for the interface at the horizontal positions supplied when + creating the mapping. + + >>> from simpeg.maps import ParametricSplineMap + >>> from discretize import TensorMesh + >>> import numpy as np + >>> import matplotlib.pyplot as plt + + >>> h = 0.5*np.ones(20) + >>> mesh = TensorMesh([h, h]) + + >>> x = np.linspace(0, 10, 6) + >>> y = 0.5*x + 2.5 + + >>> model = np.r_[10., 0., y] + >>> mapping = ParametricSplineMap(mesh, x, order=2, normal='Y', slope=2) + + >>> fig = plt.figure(figsize=(5, 5)) + >>> ax = fig.add_subplot(111) + >>> mesh.plot_image(mapping * model, ax=ax) + + """ + + def __init__( + self, mesh, pts, ptsv=None, order=3, logSigma=True, normal="x", slope=1e4 + ): + super().__init__(mesh=mesh) + self.slope = slope + self.logSigma = logSigma + self.normal = normal + self.order = order + self.pts = pts + self.ptsv = ptsv + self.spl = None + + @IdentityMap.mesh.setter + def mesh(self, value): + self._mesh = validate_type( + "mesh", value, discretize.base.BaseTensorMesh, cast=False + ) + + @property + def slope(self): + """Sharpness of the boundary. + + Larger number are sharper. + + Returns + ------- + float + """ + return self._slope + + @slope.setter + def slope(self, value): + self._slope = validate_float("slope", value, min_val=0.0, inclusive_min=False) + + @property + def logSigma(self): + """Whether the input needs to be transformed by an exponential + + Returns + ------- + float + """ + return self._logSigma + + @logSigma.setter + def logSigma(self, value): + self._logSigma = validate_type("logSigma", value, bool) + + @property + def normal(self): + """The projection axis. + + Returns + ------- + str + """ + return self._normal + + @normal.setter + def normal(self, value): + self._normal = validate_string("normal", value, ("x", "y", "z")) + + @property + def order(self): + """Order of the spline mapping. + + Returns + ------- + int + """ + return self._order + + @order.setter + def order(self, value): + self._order = validate_integer("order", value, min_val=1) + + @property + def pts(self): + """Points for the spline. + + Returns + ------- + numpy.ndarray + """ + return self._pts + + @pts.setter + def pts(self, value): + self._pts = validate_ndarray_with_shape("pts", value, shape=("*"), dtype=float) + + @property + def npts(self): + """The number of points. + + Returns + ------- + int + """ + return self._pts.shape[0] + + @property + def ptsv(self): + """Bottom and top values for the 3D spline surface. + + In 3D, two splines are created and linearly interpolated between these two + points. + + Returns + ------- + (2) numpy.ndarray + """ + return self._ptsv + + @ptsv.setter + def ptsv(self, value): + if value is not None: + value = validate_ndarray_with_shape("ptsv", value, shape=(2,)) + self._ptsv = value + + @property + def nP(self): + r"""Number of parameters the mapping acts on + + Returns + ------- + int + Number of parameters the mapping acts on. + - **2D mesh:** the mapping acts on *mesh.nC + 2* parameters + - **3D mesh:** the mapping acts on *2\*mesh.nC + 2* parameters + """ + if self.mesh.dim == 2: + return np.size(self.pts) + 2 + elif self.mesh.dim == 3: + return np.size(self.pts) * 2 + 2 + else: + raise (Exception("Only supports 2D and 3D")) + + def _transform(self, m): + # Set model parameters + alpha = self.slope + sig1, sig2 = m[0], m[1] + c = m[2:] + if self.logSigma: + sig1, sig2 = np.exp(sig1), np.exp(sig2) + # 2D + if self.mesh.dim == 2: + X = self.mesh.cell_centers[:, 0] + Y = self.mesh.cell_centers[:, 1] + self.spl = UnivariateSpline(self.pts, c, k=self.order, s=0) + if self.normal == "x": + f = self.spl(Y) - X + elif self.normal == "y": + f = self.spl(X) - Y + else: + raise (Exception("Input for normal = X or Y or Z")) + + # 3D: + # Comments: + # Make two spline functions and link them using linear interpolation. + # This is not quite direct extension of 2D to 3D case + # Using 2D interpolation is possible + + elif self.mesh.dim == 3: + X = self.mesh.cell_centers[:, 0] + Y = self.mesh.cell_centers[:, 1] + Z = self.mesh.cell_centers[:, 2] + + npts = np.size(self.pts) + if np.mod(c.size, 2): + raise (Exception("Put even points!")) + + self.spl = { + "splb": UnivariateSpline(self.pts, c[:npts], k=self.order, s=0), + "splt": UnivariateSpline(self.pts, c[npts:], k=self.order, s=0), + } + + if self.normal == "x": + zb = self.ptsv[0] + zt = self.ptsv[1] + flines = (self.spl["splt"](Y) - self.spl["splb"](Y)) * (Z - zb) / ( + zt - zb + ) + self.spl["splb"](Y) + f = flines - X + # elif self.normal =='Y': + # elif self.normal =='Z': + else: + raise (Exception("Input for normal = X or Y or Z")) + else: + raise (Exception("Only supports 2D and 3D")) + + return sig1 + (sig2 - sig1) * (np.arctan(alpha * f) / np.pi + 0.5) + + def deriv(self, m, v=None): + alpha = self.slope + sig1, sig2, c = m[0], m[1], m[2:] + if self.logSigma: + sig1, sig2 = np.exp(sig1), np.exp(sig2) + # 2D + if self.mesh.dim == 2: + X = self.mesh.cell_centers[:, 0] + Y = self.mesh.cell_centers[:, 1] + + if self.normal == "x": + f = self.spl(Y) - X + elif self.normal == "y": + f = self.spl(X) - Y + else: + raise (Exception("Input for normal = X or Y or Z")) + # 3D + elif self.mesh.dim == 3: + X = self.mesh.cell_centers[:, 0] + Y = self.mesh.cell_centers[:, 1] + Z = self.mesh.cell_centers[:, 2] + + if self.normal == "x": + zb = self.ptsv[0] + zt = self.ptsv[1] + flines = (self.spl["splt"](Y) - self.spl["splb"](Y)) * (Z - zb) / ( + zt - zb + ) + self.spl["splb"](Y) + f = flines - X + # elif self.normal =='Y': + # elif self.normal =='Z': + else: + raise (Exception("Not Implemented for Y and Z, your turn :)")) + + if self.logSigma: + g1 = -(np.arctan(alpha * f) / np.pi + 0.5) * sig1 + sig1 + g2 = (np.arctan(alpha * f) / np.pi + 0.5) * sig2 + else: + g1 = -(np.arctan(alpha * f) / np.pi + 0.5) + 1.0 + g2 = np.arctan(alpha * f) / np.pi + 0.5 + + if self.mesh.dim == 2: + g3 = np.zeros((self.mesh.nC, self.npts)) + if self.normal == "y": + # Here we use perturbation to compute sensitivity + # TODO: bit more generalization of this ... + # Modfications for X and Z directions ... + for i in range(np.size(self.pts)): + ctemp = c[i] + ind = np.argmin(abs(self.mesh.cell_centers_y - ctemp)) + ca = c.copy() + cb = c.copy() + dy = self.mesh.h[1][ind] * 1.5 + ca[i] = ctemp + dy + cb[i] = ctemp - dy + spla = UnivariateSpline(self.pts, ca, k=self.order, s=0) + splb = UnivariateSpline(self.pts, cb, k=self.order, s=0) + fderiv = (spla(X) - splb(X)) / (2 * dy) + g3[:, i] = ( + sdiag(alpha * (sig2 - sig1) / (1.0 + (alpha * f) ** 2) / np.pi) + * fderiv + ) + + elif self.mesh.dim == 3: + g3 = np.zeros((self.mesh.nC, self.npts * 2)) + if self.normal == "x": + # Here we use perturbation to compute sensitivity + for i in range(self.npts * 2): + ctemp = c[i] + ind = np.argmin(abs(self.mesh.cell_centers_y - ctemp)) + ca = c.copy() + cb = c.copy() + dy = self.mesh.h[1][ind] * 1.5 + ca[i] = ctemp + dy + cb[i] = ctemp - dy + + # treat bottom boundary + if i < self.npts: + splba = UnivariateSpline( + self.pts, ca[: self.npts], k=self.order, s=0 + ) + splbb = UnivariateSpline( + self.pts, cb[: self.npts], k=self.order, s=0 + ) + flinesa = ( + (self.spl["splt"](Y) - splba(Y)) * (Z - zb) / (zt - zb) + + splba(Y) + - X + ) + flinesb = ( + (self.spl["splt"](Y) - splbb(Y)) * (Z - zb) / (zt - zb) + + splbb(Y) + - X + ) + + # treat top boundary + else: + splta = UnivariateSpline( + self.pts, ca[self.npts :], k=self.order, s=0 + ) + spltb = UnivariateSpline( + self.pts, ca[self.npts :], k=self.order, s=0 + ) + flinesa = ( + (self.spl["splt"](Y) - splta(Y)) * (Z - zb) / (zt - zb) + + splta(Y) + - X + ) + flinesb = ( + (self.spl["splt"](Y) - spltb(Y)) * (Z - zb) / (zt - zb) + + spltb(Y) + - X + ) + fderiv = (flinesa - flinesb) / (2 * dy) + g3[:, i] = ( + sdiag(alpha * (sig2 - sig1) / (1.0 + (alpha * f) ** 2) / np.pi) + * fderiv + ) + else: + raise (Exception("Not Implemented for Y and Z, your turn :)")) + + if v is not None: + return sp.csr_matrix(np.c_[g1, g2, g3]) * v + return sp.csr_matrix(np.c_[g1, g2, g3]) + + @property + def is_linear(self): + return False + + +class BaseParametric(IdentityMap): + """Base class for parametric mappings from simple geological structures to meshes. + + Parameters + ---------- + mesh : discretize.BaseMesh + A discretize mesh + indActive : numpy.ndarray, optional + Active cells array. Can be a boolean ``numpy.ndarray`` of length *mesh.nC* + or a ``numpy.ndarray`` of ``int`` containing the indices of the active cells. + slope : float, optional + Directly set the scaling parameter *slope* which sets the sharpness of boundaries + between units. + slopeFact : float, optional + Set sharpness of boundaries between units based on minimum cell size. If set, + the scalaing parameter *slope = slopeFact / dh*. + + """ + + def __init__(self, mesh, slope=None, slopeFact=1.0, indActive=None, **kwargs): + super(BaseParametric, self).__init__(mesh, **kwargs) + self.indActive = indActive + self.slopeFact = slopeFact + if slope is not None: + self.slope = slope + + @property + def slope(self): + """Defines the sharpness of the boundaries. + + Returns + ------- + float + """ + return self._slope + + @slope.setter + def slope(self, value): + self._slope = validate_float("slope", value, min_val=0.0) + + @property + def slopeFact(self): + """Defines the slope scaled by the mesh. + + Returns + ------- + float + """ + return self._slopeFact + + @slopeFact.setter + def slopeFact(self, value): + self._slopeFact = validate_float("slopeFact", value, min_val=0.0) + self.slope = self._slopeFact / self.mesh.edge_lengths.min() + + @property + def indActive(self): + return self._indActive + + @indActive.setter + def indActive(self, value): + if value is not None: + value = validate_active_indices("indActive", value, self.mesh.n_cells) + self._indActive = value + + @property + def x(self): + """X cell center locations (active) for the output of the mapping. + + Returns + ------- + (n_active) numpy.ndarray + X cell center locations (active) for the output of the mapping. + """ + if getattr(self, "_x", None) is None: + if self.mesh.dim == 1: + self._x = [ + ( + self.mesh.cell_centers + if self.indActive is None + else self.mesh.cell_centers[self.indActive] + ) + ][0] + else: + self._x = [ + ( + self.mesh.cell_centers[:, 0] + if self.indActive is None + else self.mesh.cell_centers[self.indActive, 0] + ) + ][0] + return self._x + + @property + def y(self): + """Y cell center locations (active) for the output of the mapping. + + Returns + ------- + (n_active) numpy.ndarray + Y cell center locations (active) for the output of the mapping. + """ + if getattr(self, "_y", None) is None: + if self.mesh.dim > 1: + self._y = [ + ( + self.mesh.cell_centers[:, 1] + if self.indActive is None + else self.mesh.cell_centers[self.indActive, 1] + ) + ][0] + else: + self._y = None + return self._y + + @property + def z(self): + """Z cell center locations (active) for the output of the mapping. + + Returns + ------- + (n_active) numpy.ndarray + Z cell center locations (active) for the output of the mapping. + """ + if getattr(self, "_z", None) is None: + if self.mesh.dim > 2: + self._z = [ + ( + self.mesh.cell_centers[:, 2] + if self.indActive is None + else self.mesh.cell_centers[self.indActive, 2] + ) + ][0] + else: + self._z = None + return self._z + + def _atanfct(self, val, slope): + return np.arctan(slope * val) / np.pi + 0.5 + + def _atanfctDeriv(self, val, slope): + # d/dx(atan(x)) = 1/(1+x**2) + x = slope * val + dx = -slope + return (1.0 / (1 + x**2)) / np.pi * dx + + @property + def is_linear(self): + return False + + +class ParametricLayer(BaseParametric): + r"""Mapping for a horizontal layer within a wholespace. + + This mapping is used when the cells lying below the Earth's surface can + be parameterized by horizontal layer within a homogeneous medium. + The model is defined by the physical property value for the background + (:math:`\sigma_0`), the physical property value for the layer + (:math:`\sigma_1`), the elevation for the middle of the layer (:math:`z_L`) + and the thickness of the layer :math:`h`. + + For this mapping, the set of input model parameters are organized: + + .. math:: + \mathbf{m} = [\sigma_0, \;\sigma_1,\; z_L , \; h] + + The mapping :math:`\mathbf{u}(\mathbf{m})` from the model to the mesh + is given by: + + .. math:: + + \mathbf{u}(\mathbf{m}) = \sigma_0 + \frac{(\sigma_1 - \sigma_0)}{\pi} \Bigg [ + \arctan \Bigg ( a \bigg ( \mathbf{z_c} - z_L + \frac{h}{2} \bigg ) \Bigg ) + - \arctan \Bigg ( a \bigg ( \mathbf{z_c} - z_L - \frac{h}{2} \bigg ) \Bigg ) \Bigg ] + + where :math:`\mathbf{z_c}` is a vectors containing the vertical cell center + locations for all active cells in the mesh, and :math:`a` is a + parameter which defines the sharpness of the boundaries between the layer + and the background. + + Parameters + ---------- + mesh : discretize.BaseMesh + A discretize mesh + indActive : numpy.ndarray + Active cells array. Can be a boolean ``numpy.ndarray`` of length *mesh.nC* + or a ``numpy.ndarray`` of ``int`` containing the indices of the active cells. + slope : float + Directly define the constant *a* in the mapping function which defines the + sharpness of the boundaries. + slopeFact : float + Scaling factor for the sharpness of the boundaries based on cell size. + Using this option, we set *a = slopeFact / dh*. + + Examples + -------- + In this example, we define a layer in a wholespace whose interface is sharp. + We construct the mapping from the model to the set of active cells + (i.e. below the surface), We then use an active cells mapping to map from + the set of active cells to all cells in the mesh. + + >>> from simpeg.maps import ParametricLayer, InjectActiveCells + >>> from discretize import TensorMesh + >>> import numpy as np + >>> import matplotlib.pyplot as plt + + >>> dh = 0.25*np.ones(40) + >>> mesh = TensorMesh([dh, dh]) + >>> ind_active = mesh.cell_centers[:, 1] < 8 + + >>> sig0, sig1, zL, h = 5., 10., 4., 2 + >>> model = np.r_[sig0, sig1, zL, h] + + >>> layer_map = ParametricLayer( + >>> mesh, indActive=ind_active, slope=4 + >>> ) + >>> act_map = InjectActiveCells(mesh, ind_active, 0.) + + >>> fig = plt.figure(figsize=(5, 5)) + >>> ax = fig.add_subplot(111) + >>> mesh.plot_image(act_map * layer_map * model, ax=ax) + + """ + + def __init__(self, mesh, **kwargs): + super().__init__(mesh, **kwargs) + + @property + def nP(self): + """Number of model parameters the mapping acts on; i.e 4 + + Returns + ------- + int + Returns an integer value of *4*. + """ + return 4 + + @property + def shape(self): + """Dimensions of the mapping + + Returns + ------- + tuple of int + Where *nP=4* is the number of parameters the mapping acts on + and *nAct* is the number of active cells in the mesh, **shape** + returns a tuple (*nAct* , *4*). + """ + if self.indActive is not None: + return (sum(self.indActive), self.nP) + return (self.mesh.nC, self.nP) + + def mDict(self, m): + r"""Return model parameters as a dictionary. + + For a model :math:`\mathbf{m} = [\sigma_0, \;\sigma_1,\; z_L , \; h]`, + **mDict** returns a dictionary:: + + {"val_background": m[0], "val_layer": m[1], "layer_center": m[2], "layer_thickness": m[3]} + + Returns + ------- + dict + The model as a dictionary + """ + return { + "val_background": m[0], + "val_layer": m[1], + "layer_center": m[2], + "layer_thickness": m[3], + } + + def _atanLayer(self, mDict): + if self.mesh.dim == 2: + z = self.y + elif self.mesh.dim == 3: + z = self.z + + layer_bottom = mDict["layer_center"] - mDict["layer_thickness"] / 2.0 + layer_top = mDict["layer_center"] + mDict["layer_thickness"] / 2.0 + + return self._atanfct(z - layer_bottom, self.slope) * self._atanfct( + z - layer_top, -self.slope + ) + + def _atanLayerDeriv_layer_center(self, mDict): + if self.mesh.dim == 2: + z = self.y + elif self.mesh.dim == 3: + z = self.z + + layer_bottom = mDict["layer_center"] - mDict["layer_thickness"] / 2.0 + layer_top = mDict["layer_center"] + mDict["layer_thickness"] / 2.0 + + return self._atanfctDeriv(z - layer_bottom, self.slope) * self._atanfct( + z - layer_top, -self.slope + ) + self._atanfct(z - layer_bottom, self.slope) * self._atanfctDeriv( + z - layer_top, -self.slope + ) + + def _atanLayerDeriv_layer_thickness(self, mDict): + if self.mesh.dim == 2: + z = self.y + elif self.mesh.dim == 3: + z = self.z + + layer_bottom = mDict["layer_center"] - mDict["layer_thickness"] / 2.0 + layer_top = mDict["layer_center"] + mDict["layer_thickness"] / 2.0 + + return -0.5 * self._atanfctDeriv(z - layer_bottom, self.slope) * self._atanfct( + z - layer_top, -self.slope + ) + 0.5 * self._atanfct(z - layer_bottom, self.slope) * self._atanfctDeriv( + z - layer_top, -self.slope + ) + + def layer_cont(self, mDict): + return mDict["val_background"] + ( + mDict["val_layer"] - mDict["val_background"] + ) * self._atanLayer(mDict) + + def _transform(self, m): + mDict = self.mDict(m) + return self.layer_cont(mDict) + + def _deriv_val_background(self, mDict): + return np.ones_like(self.x) - self._atanLayer(mDict) + + def _deriv_val_layer(self, mDict): + return self._atanLayer(mDict) + + def _deriv_layer_center(self, mDict): + return ( + mDict["val_layer"] - mDict["val_background"] + ) * self._atanLayerDeriv_layer_center(mDict) + + def _deriv_layer_thickness(self, mDict): + return ( + mDict["val_layer"] - mDict["val_background"] + ) * self._atanLayerDeriv_layer_thickness(mDict) + + def deriv(self, m): + r"""Derivative of the mapping with respect to the input parameters. + + Let :math:`\mathbf{m} = [\sigma_0, \;\sigma_1,\; z_L , \; h]` be the set of + model parameters the defines a layer within a wholespace. The mapping + :math:`\mathbf{u}(\mathbf{m})`from the parameterized model to all + active cells is given by: + + .. math:: + \mathbf{u}(\mathbf{m}) = \sigma_0 + \frac{(\sigma_1 - \sigma_0)}{\pi} \Bigg [ + \arctan \Bigg ( a \bigg ( \mathbf{z_c} - z_L + \frac{h}{2} \bigg ) \Bigg ) + - \arctan \Bigg ( a \bigg ( \mathbf{z_c} - z_L - \frac{h}{2} \bigg ) \Bigg ) \Bigg ] + + where :math:`\mathbf{z_c}` is a vectors containing the vertical cell center + locations for all active cells in the mesh. The derivative of the mapping + with respect to the model parameters is a ``numpy.ndarray`` of + shape (*nAct*, *4*) given by: + + .. math:: + \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = + \Bigg [ \frac{\partial \mathbf{u}}{\partial \sigma_0} \;\; + \frac{\partial \mathbf{u}}{\partial \sigma_1} \;\; + \frac{\partial \mathbf{u}}{\partial z_L} \;\; + \frac{\partial \mathbf{u}}{\partial h} + \Bigg ] + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. If the + input argument *v* is not ``None``, the method returns the derivative times + the vector *v*. + """ + + mDict = self.mDict(m) + + return sp.csr_matrix( + np.vstack( + [ + self._deriv_val_background(mDict), + self._deriv_val_layer(mDict), + self._deriv_layer_center(mDict), + self._deriv_layer_thickness(mDict), + ] + ).T + ) + + +class ParametricBlock(BaseParametric): + r"""Mapping for a rectangular block within a wholespace. + + This mapping is used when the cells lying below the Earth's surface can + be parameterized by rectangular block within a homogeneous medium. + The model is defined by the physical property value for the background + (:math:`\sigma_0`), the physical property value for the block + (:math:`\sigma_b`), parameters for the center of the block + (:math:`x_b [,y_b, z_b]`) and parameters for the dimensions along + each Cartesian direction (:math:`dx [,dy, dz]`) + + For this mapping, the set of input model parameters are organized: + + .. math:: + \mathbf{m} = \begin{cases} + 1D: \;\; [\sigma_0, \;\sigma_b,\; x_b , \; dx] \\ + 2D: \;\; [\sigma_0, \;\sigma_b,\; x_b , \; dx,\; y_b , \; dy] \\ + 3D: \;\; [\sigma_0, \;\sigma_b,\; x_b , \; dx,\; y_b , \; dy,\; z_b , \; dz] + \end{cases} + + The mapping :math:`\mathbf{u}(\mathbf{m})` from the model to the mesh + is given by: + + .. math:: + + \mathbf{u}(\mathbf{m}) = \sigma_0 + (\sigma_b - \sigma_0) \bigg [ \frac{1}{2} + + \pi^{-1} \arctan \bigg ( a \, \boldsymbol{\eta} \big ( + x_b, y_b, z_b, dx, dy, dz \big ) \bigg ) \bigg ] + + where *a* is a parameter that impacts the sharpness of the arctan function, and + + .. math:: + \boldsymbol{\eta} \big ( x_b, y_b, z_b, dx, dy, dz \big ) = 1 - + \sum_{\xi \in (x,y,z)} \bigg [ \bigg ( \frac{2(\boldsymbol{\xi_c} - \xi_b)}{d\xi} \bigg )^2 + \varepsilon^2 + \bigg ]^{p/2} + + Parameters :math:`p` and :math:`\varepsilon` define the parameters of the Ekblom + function. :math:`\boldsymbol{\xi_c}` is a place holder for vectors containing + the x, [y and z] cell center locations of the mesh, :math:`\xi_b` is a placeholder + for the x[, y and z] location for the center of the block, and :math:`d\xi` is a + placeholder for the x[, y and z] dimensions of the block. + + Parameters + ---------- + mesh : discretize.BaseMesh + A discretize mesh + indActive : numpy.ndarray + Active cells array. Can be a boolean ``numpy.ndarray`` of length *mesh.nC* + or a ``numpy.ndarray`` of ``int`` containing the indices of the active cells. + slope : float + Directly define the constant *a* in the mapping function which defines the + sharpness of the boundaries. + slopeFact : float + Scaling factor for the sharpness of the boundaries based on cell size. + Using this option, we set *a = slopeFact / dh*. + epsilon : float + Epsilon value used in the ekblom representation of the block + p : float + p-value used in the ekblom representation of the block. + + Examples + -------- + In this example, we define a rectangular block in a wholespace whose + interface is sharp. We construct the mapping from the model to the + set of active cells (i.e. below the surface), We then use an active + cells mapping to map from the set of active cells to all cells in the mesh. + + >>> from simpeg.maps import ParametricBlock, InjectActiveCells + >>> from discretize import TensorMesh + >>> import numpy as np + >>> import matplotlib.pyplot as plt + + >>> dh = 0.5*np.ones(20) + >>> mesh = TensorMesh([dh, dh]) + >>> ind_active = mesh.cell_centers[:, 1] < 8 + + >>> sig0, sigb, xb, Lx, yb, Ly = 5., 10., 5., 4., 4., 2. + >>> model = np.r_[sig0, sigb, xb, Lx, yb, Ly] + + >>> block_map = ParametricBlock(mesh, indActive=ind_active) + >>> act_map = InjectActiveCells(mesh, ind_active, 0.) + + >>> fig = plt.figure(figsize=(5, 5)) + >>> ax = fig.add_subplot(111) + >>> mesh.plot_image(act_map * block_map * model, ax=ax) + + """ + + def __init__(self, mesh, epsilon=1e-6, p=10, **kwargs): + self.epsilon = epsilon + self.p = p + super(ParametricBlock, self).__init__(mesh, **kwargs) + + @property + def epsilon(self): + """epsilon value used in the ekblom representation of the block. + + Returns + ------- + float + """ + return self._epsilon + + @epsilon.setter + def epsilon(self, value): + self._epsilon = validate_float("epsilon", value, min_val=0.0) + + @property + def p(self): + """p-value used in the ekblom representation of the block. + + Returns + ------- + float + """ + return self._p + + @p.setter + def p(self, value): + self._p = validate_float("p", value, min_val=0.0) + + @property + def nP(self): + """Number of parameters the mapping acts on. + + Returns + ------- + int + The number of the parameters defining the model depends on the dimension + of the mesh. *nP* + + - =4 for a 1D mesh + - =6 for a 2D mesh + - =8 for a 3D mesh + """ + if self.mesh.dim == 1: + return 4 + if self.mesh.dim == 2: + return 6 + elif self.mesh.dim == 3: + return 8 + + @property + def shape(self): + """Dimensions of the mapping + + Returns + ------- + tuple of int + Where *nP* is the number of parameters the mapping acts on + and *nAct* is the number of active cells in the mesh, **shape** + returns a tuple (*nAct* , *nP*). + """ + if self.indActive is not None: + return (sum(self.indActive), self.nP) + return (self.mesh.nC, self.nP) + + def _mDict1d(self, m): + return { + "val_background": m[0], + "val_block": m[1], + "x0": m[2], + "dx": m[3], + } + + def _mDict2d(self, m): + mDict = self._mDict1d(m) + mDict.update( + { + # 'theta_x': m[4], + "y0": m[4], + "dy": m[5], + # 'theta_y': m[7] + } + ) + return mDict + + def _mDict3d(self, m): + mDict = self._mDict2d(m) + mDict.update( + { + "z0": m[6], + "dz": m[7], + # 'theta_z': m[10] + } + ) + return mDict + + def mDict(self, m): + r"""Return model parameters as a dictionary. + + Returns + ------- + dict + The model as a dictionary + """ + return getattr(self, "_mDict{}d".format(self.mesh.dim))(m) + + def _ekblom(self, val): + return (val**2 + self.epsilon**2) ** (self.p / 2.0) + + def _ekblomDeriv(self, val): + return (self.p / 2) * (val**2 + self.epsilon**2) ** ((self.p / 2) - 1) * 2 * val + + # def _rotation(self, mDict): + # if self.mesh.dim == 2: + + # elif self.mesh.dim == 3: + + def _block1D(self, mDict): + return 1 - (self._ekblom((self.x - mDict["x0"]) / (0.5 * mDict["dx"]))) + + def _block2D(self, mDict): + return 1 - ( + self._ekblom((self.x - mDict["x0"]) / (0.5 * mDict["dx"])) + + self._ekblom((self.y - mDict["y0"]) / (0.5 * mDict["dy"])) + ) + + def _block3D(self, mDict): + return 1 - ( + self._ekblom((self.x - mDict["x0"]) / (0.5 * mDict["dx"])) + + self._ekblom((self.y - mDict["y0"]) / (0.5 * mDict["dy"])) + + self._ekblom((self.z - mDict["z0"]) / (0.5 * mDict["dz"])) + ) + + def _transform(self, m): + mDict = self.mDict(m) + return mDict["val_background"] + ( + mDict["val_block"] - mDict["val_background"] + ) * self._atanfct( + getattr(self, "_block{}D".format(self.mesh.dim))(mDict), slope=self.slope + ) + + def _deriv_val_background(self, mDict): + return 1 - self._atanfct( + getattr(self, "_block{}D".format(self.mesh.dim))(mDict), slope=self.slope + ) + + def _deriv_val_block(self, mDict): + return self._atanfct( + getattr(self, "_block{}D".format(self.mesh.dim))(mDict), slope=self.slope + ) + + def _deriv_center_block(self, mDict, orientation): + x = getattr(self, orientation) + x0 = mDict["{}0".format(orientation)] + dx = mDict["d{}".format(orientation)] + return (mDict["val_block"] - mDict["val_background"]) * ( + self._atanfctDeriv( + getattr(self, "_block{}D".format(self.mesh.dim))(mDict), + slope=self.slope, + ) + * (self._ekblomDeriv((x - x0) / (0.5 * dx))) + / -(0.5 * dx) + ) + + def _deriv_width_block(self, mDict, orientation): + x = getattr(self, orientation) + x0 = mDict["{}0".format(orientation)] + dx = mDict["d{}".format(orientation)] + return (mDict["val_block"] - mDict["val_background"]) * ( + self._atanfctDeriv( + getattr(self, "_block{}D".format(self.mesh.dim))(mDict), + slope=self.slope, + ) + * (self._ekblomDeriv((x - x0) / (0.5 * dx)) * (-(x - x0) / (0.5 * dx**2))) + ) + + def _deriv1D(self, mDict): + return np.vstack( + [ + self._deriv_val_background(mDict), + self._deriv_val_block(mDict), + self._deriv_center_block(mDict, "x"), + self._deriv_width_block(mDict, "x"), + ] + ).T + + def _deriv2D(self, mDict): + return np.vstack( + [ + self._deriv_val_background(mDict), + self._deriv_val_block(mDict), + self._deriv_center_block(mDict, "x"), + self._deriv_width_block(mDict, "x"), + self._deriv_center_block(mDict, "y"), + self._deriv_width_block(mDict, "y"), + ] + ).T + + def _deriv3D(self, mDict): + return np.vstack( + [ + self._deriv_val_background(mDict), + self._deriv_val_block(mDict), + self._deriv_center_block(mDict, "x"), + self._deriv_width_block(mDict, "x"), + self._deriv_center_block(mDict, "y"), + self._deriv_width_block(mDict, "y"), + self._deriv_center_block(mDict, "z"), + self._deriv_width_block(mDict, "z"), + ] + ).T + + def deriv(self, m): + r"""Derivative of the mapping with respect to the input parameters. + + Let :math:`\mathbf{m} = [\sigma_0, \;\sigma_1,\; x_b, \; dx, (\; y_b, \; dy, \; z_b , dz)]` + be the set of model parameters the defines a block/ellipsoid within a wholespace. + The mapping :math:`\mathbf{u}(\mathbf{m})` from the parameterized model to all + active cells is given by: + + The derivative of the mapping :math:`\mathbf{u}(\mathbf{m})` with respect to + the model parameters is a ``numpy.ndarray`` of shape (*nAct*, *nP*) given by: + + .. math:: + \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = \Bigg [ + \frac{\partial \mathbf{u}}{\partial \sigma_0} \;\; + \frac{\partial \mathbf{u}}{\partial \sigma_1} \;\; + \frac{\partial \mathbf{u}}{\partial x_b} \;\; + \frac{\partial \mathbf{u}}{\partial dx} \;\; + \frac{\partial \mathbf{u}}{\partial y_b} \;\; + \frac{\partial \mathbf{u}}{\partial dy} \;\; + \frac{\partial \mathbf{u}}{\partial z_b} \;\; + \frac{\partial \mathbf{u}}{\partial dz} + \Bigg ) \Bigg ] + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. If the + input argument *v* is not ``None``, the method returns the derivative times + the vector *v*. + """ + return sp.csr_matrix( + getattr(self, "_deriv{}D".format(self.mesh.dim))(self.mDict(m)) + ) + + +class ParametricEllipsoid(ParametricBlock): + r"""Mapping for a rectangular block within a wholespace. + + This mapping is used when the cells lying below the Earth's surface can + be parameterized by an ellipsoid within a homogeneous medium. + The model is defined by the physical property value for the background + (:math:`\sigma_0`), the physical property value for the layer + (:math:`\sigma_b`), parameters for the center of the ellipsoid + (:math:`x_b [,y_b, z_b]`) and parameters for the dimensions along + each Cartesian direction (:math:`dx [,dy, dz]`) + + For this mapping, the set of input model parameters are organized: + + .. math:: + \mathbf{m} = \begin{cases} + 1D: \;\; [\sigma_0, \;\sigma_b,\; x_b , \; dx] \\ + 2D: \;\; [\sigma_0, \;\sigma_b,\; x_b , \; dx,\; y_b , \; dy] \\ + 3D: \;\; [\sigma_0, \;\sigma_b,\; x_b , \; dx,\; y_b , \; dy,\; z_b , \; dz] + \end{cases} + + The mapping :math:`\mathbf{u}(\mathbf{m})` from the model to the mesh + is given by: + + .. math:: + + \mathbf{u}(\mathbf{m}) = \sigma_0 + (\sigma_b - \sigma_0) \bigg [ \frac{1}{2} + + \pi^{-1} \arctan \bigg ( a \, \boldsymbol{\eta} \big ( + x_b, y_b, z_b, dx, dy, dz \big ) \bigg ) \bigg ] + + where *a* is a parameter that impacts the sharpness of the arctan function, and + + .. math:: + \boldsymbol{\eta} \big ( x_b, y_b, z_b, dx, dy, dz \big ) = 1 - + \sum_{\xi \in (x,y,z)} \bigg [ \bigg ( \frac{2(\boldsymbol{\xi_c} - \xi_b)}{d\xi} \bigg )^2 + \varepsilon^2 + \bigg ] + + :math:`\boldsymbol{\xi_c}` is a place holder for vectors containing + the x, [y and z] cell center locations of the mesh, :math:`\xi_b` is a placeholder + for the x[, y and z] location for the center of the block, and :math:`d\xi` is a + placeholder for the x[, y and z] dimensions of the block. + + Parameters + ---------- + mesh : discretize.BaseMesh + A discretize mesh + indActive : numpy.ndarray + Active cells array. Can be a boolean ``numpy.ndarray`` of length *mesh.nC* + or a ``numpy.ndarray`` of ``int`` containing the indices of the active cells. + slope : float + Directly define the constant *a* in the mapping function which defines the + sharpness of the boundaries. + slopeFact : float + Scaling factor for the sharpness of the boundaries based on cell size. + Using this option, we set *a = slopeFact / dh*. + epsilon : float + Epsilon value used in the ekblom representation of the block + + Examples + -------- + In this example, we define an ellipse in a wholespace whose + interface is sharp. We construct the mapping from the model to the + set of active cells (i.e. below the surface), We then use an active + cells mapping to map from the set of active cells to all cells in the mesh. + + >>> from simpeg.maps import ParametricEllipsoid, InjectActiveCells + >>> from discretize import TensorMesh + >>> import numpy as np + >>> import matplotlib.pyplot as plt + + >>> dh = 0.5*np.ones(20) + >>> mesh = TensorMesh([dh, dh]) + >>> ind_active = mesh.cell_centers[:, 1] < 8 + + >>> sig0, sigb, xb, Lx, yb, Ly = 5., 10., 5., 4., 4., 3. + >>> model = np.r_[sig0, sigb, xb, Lx, yb, Ly] + + >>> ellipsoid_map = ParametricEllipsoid(mesh, indActive=ind_active) + >>> act_map = InjectActiveCells(mesh, ind_active, 0.) + + >>> fig = plt.figure(figsize=(5, 5)) + >>> ax = fig.add_subplot(111) + >>> mesh.plot_image(act_map * ellipsoid_map * model, ax=ax) + + """ + + def __init__(self, mesh, **kwargs): + super(ParametricEllipsoid, self).__init__(mesh, p=2, **kwargs) + + +class ParametricCasingAndLayer(ParametricLayer): + """ + Parametric layered space with casing. + + .. code:: python + + m = [val_background, + val_layer, + val_casing, + val_insideCasing, + layer_center, + layer_thickness, + casing_radius, + casing_thickness, + casing_bottom, + casing_top + ] + + """ + + def __init__(self, mesh, **kwargs): + assert ( + mesh._meshType == "CYL" + ), "Parametric Casing in a layer map only works for a cyl mesh." + + super().__init__(mesh, **kwargs) + + @property + def nP(self): + return 10 + + @property + def shape(self): + if self.indActive is not None: + return (sum(self.indActive), self.nP) + return (self.mesh.nC, self.nP) + + def mDict(self, m): + # m = [val_background, val_layer, val_casing, val_insideCasing, + # layer_center, layer_thickness, casing_radius, casing_thickness, + # casing_bottom, casing_top] + + return { + "val_background": m[0], + "val_layer": m[1], + "val_casing": m[2], + "val_insideCasing": m[3], + "layer_center": m[4], + "layer_thickness": m[5], + "casing_radius": m[6], + "casing_thickness": m[7], + "casing_bottom": m[8], + "casing_top": m[9], + } + + def casing_a(self, mDict): + return mDict["casing_radius"] - 0.5 * mDict["casing_thickness"] + + def casing_b(self, mDict): + return mDict["casing_radius"] + 0.5 * mDict["casing_thickness"] + + def _atanCasingLength(self, mDict): + return self._atanfct(self.z - mDict["casing_top"], -self.slope) * self._atanfct( + self.z - mDict["casing_bottom"], self.slope + ) + + def _atanCasingLengthDeriv_casing_top(self, mDict): + return self._atanfctDeriv( + self.z - mDict["casing_top"], -self.slope + ) * self._atanfct(self.z - mDict["casing_bottom"], self.slope) + + def _atanCasingLengthDeriv_casing_bottom(self, mDict): + return self._atanfct( + self.z - mDict["casing_top"], -self.slope + ) * self._atanfctDeriv(self.z - mDict["casing_bottom"], self.slope) + + def _atanInsideCasing(self, mDict): + return self._atanCasingLength(mDict) * self._atanfct( + self.x - self.casing_a(mDict), -self.slope + ) + + def _atanInsideCasingDeriv_casing_radius(self, mDict): + return self._atanCasingLength(mDict) * self._atanfctDeriv( + self.x - self.casing_a(mDict), -self.slope + ) + + def _atanInsideCasingDeriv_casing_thickness(self, mDict): + return ( + self._atanCasingLength(mDict) + * -0.5 + * self._atanfctDeriv(self.x - self.casing_a(mDict), -self.slope) + ) + + def _atanInsideCasingDeriv_casing_top(self, mDict): + return self._atanCasingLengthDeriv_casing_top(mDict) * self._atanfct( + self.x - self.casing_a(mDict), -self.slope + ) + + def _atanInsideCasingDeriv_casing_bottom(self, mDict): + return self._atanCasingLengthDeriv_casing_bottom(mDict) * self._atanfct( + self.x - self.casing_a(mDict), -self.slope + ) + + def _atanCasing(self, mDict): + return ( + self._atanCasingLength(mDict) + * self._atanfct(self.x - self.casing_a(mDict), self.slope) + * self._atanfct(self.x - self.casing_b(mDict), -self.slope) + ) + + def _atanCasingDeriv_casing_radius(self, mDict): + return self._atanCasingLength(mDict) * ( + self._atanfctDeriv(self.x - self.casing_a(mDict), self.slope) + * self._atanfct(self.x - self.casing_b(mDict), -self.slope) + + self._atanfct(self.x - self.casing_a(mDict), self.slope) + * self._atanfctDeriv(self.x - self.casing_b(mDict), -self.slope) + ) + + def _atanCasingDeriv_casing_thickness(self, mDict): + return self._atanCasingLength(mDict) * ( + -0.5 + * self._atanfctDeriv(self.x - self.casing_a(mDict), self.slope) + * self._atanfct(self.x - self.casing_b(mDict), -self.slope) + + self._atanfct(self.x - self.casing_a(mDict), self.slope) + * 0.5 + * self._atanfctDeriv(self.x - self.casing_b(mDict), -self.slope) + ) + + def _atanCasingDeriv_casing_bottom(self, mDict): + return ( + self._atanCasingLengthDeriv_casing_bottom(mDict) + * self._atanfct(self.x - self.casing_a(mDict), self.slope) + * self._atanfct(self.x - self.casing_b(mDict), -self.slope) + ) + + def _atanCasingDeriv_casing_top(self, mDict): + return ( + self._atanCasingLengthDeriv_casing_top(mDict) + * self._atanfct(self.x - self.casing_a(mDict), self.slope) + * self._atanfct(self.x - self.casing_b(mDict), -self.slope) + ) + + def layer_cont(self, mDict): + # contribution from the layered background + return mDict["val_background"] + ( + mDict["val_layer"] - mDict["val_background"] + ) * self._atanLayer(mDict) + + def _transform(self, m): + mDict = self.mDict(m) + + # assemble the model + layer = self.layer_cont(mDict) + casing = (mDict["val_casing"] - layer) * self._atanCasing(mDict) + insideCasing = (mDict["val_insideCasing"] - layer) * self._atanInsideCasing( + mDict + ) + + return layer + casing + insideCasing + + def _deriv_val_background(self, mDict): + # contribution from the layered background + d_layer_cont_dval_background = 1.0 - self._atanLayer(mDict) + d_casing_cont_dval_background = ( + -1.0 * d_layer_cont_dval_background * self._atanCasing(mDict) + ) + d_insideCasing_cont_dval_background = ( + -1.0 * d_layer_cont_dval_background * self._atanInsideCasing(mDict) + ) + return ( + d_layer_cont_dval_background + + d_casing_cont_dval_background + + d_insideCasing_cont_dval_background + ) + + def _deriv_val_layer(self, mDict): + d_layer_cont_dval_layer = self._atanLayer(mDict) + d_casing_cont_dval_layer = ( + -1.0 * d_layer_cont_dval_layer * self._atanCasing(mDict) + ) + d_insideCasing_cont_dval_layer = ( + -1.0 * d_layer_cont_dval_layer * self._atanInsideCasing(mDict) + ) + return ( + d_layer_cont_dval_layer + + d_casing_cont_dval_layer + + d_insideCasing_cont_dval_layer + ) + + def _deriv_val_casing(self, mDict): + d_layer_cont_dval_casing = 0.0 + d_casing_cont_dval_casing = self._atanCasing(mDict) + d_insideCasing_cont_dval_casing = 0.0 + return ( + d_layer_cont_dval_casing + + d_casing_cont_dval_casing + + d_insideCasing_cont_dval_casing + ) + + def _deriv_val_insideCasing(self, mDict): + d_layer_cont_dval_insideCasing = 0.0 + d_casing_cont_dval_insideCasing = 0.0 + d_insideCasing_cont_dval_insideCasing = self._atanInsideCasing(mDict) + return ( + d_layer_cont_dval_insideCasing + + d_casing_cont_dval_insideCasing + + d_insideCasing_cont_dval_insideCasing + ) + + def _deriv_layer_center(self, mDict): + d_layer_cont_dlayer_center = ( + mDict["val_layer"] - mDict["val_background"] + ) * self._atanLayerDeriv_layer_center(mDict) + d_casing_cont_dlayer_center = -d_layer_cont_dlayer_center * self._atanCasing( + mDict + ) + d_insideCasing_cont_dlayer_center = ( + -d_layer_cont_dlayer_center * self._atanInsideCasing(mDict) + ) + return ( + d_layer_cont_dlayer_center + + d_casing_cont_dlayer_center + + d_insideCasing_cont_dlayer_center + ) + + def _deriv_layer_thickness(self, mDict): + d_layer_cont_dlayer_thickness = ( + mDict["val_layer"] - mDict["val_background"] + ) * self._atanLayerDeriv_layer_thickness(mDict) + d_casing_cont_dlayer_thickness = ( + -d_layer_cont_dlayer_thickness * self._atanCasing(mDict) + ) + d_insideCasing_cont_dlayer_thickness = ( + -d_layer_cont_dlayer_thickness * self._atanInsideCasing(mDict) + ) + return ( + d_layer_cont_dlayer_thickness + + d_casing_cont_dlayer_thickness + + d_insideCasing_cont_dlayer_thickness + ) + + def _deriv_casing_radius(self, mDict): + layer = self.layer_cont(mDict) + d_layer_cont_dcasing_radius = 0.0 + d_casing_cont_dcasing_radius = ( + mDict["val_casing"] - layer + ) * self._atanCasingDeriv_casing_radius(mDict) + d_insideCasing_cont_dcasing_radius = ( + mDict["val_insideCasing"] - layer + ) * self._atanInsideCasingDeriv_casing_radius(mDict) + return ( + d_layer_cont_dcasing_radius + + d_casing_cont_dcasing_radius + + d_insideCasing_cont_dcasing_radius + ) + + def _deriv_casing_thickness(self, mDict): + d_layer_cont_dcasing_thickness = 0.0 + d_casing_cont_dcasing_thickness = ( + mDict["val_casing"] - self.layer_cont(mDict) + ) * self._atanCasingDeriv_casing_thickness(mDict) + d_insideCasing_cont_dcasing_thickness = ( + mDict["val_insideCasing"] - self.layer_cont(mDict) + ) * self._atanInsideCasingDeriv_casing_thickness(mDict) + return ( + d_layer_cont_dcasing_thickness + + d_casing_cont_dcasing_thickness + + d_insideCasing_cont_dcasing_thickness + ) + + def _deriv_casing_bottom(self, mDict): + d_layer_cont_dcasing_bottom = 0.0 + d_casing_cont_dcasing_bottom = ( + mDict["val_casing"] - self.layer_cont(mDict) + ) * self._atanCasingDeriv_casing_bottom(mDict) + d_insideCasing_cont_dcasing_bottom = ( + mDict["val_insideCasing"] - self.layer_cont(mDict) + ) * self._atanInsideCasingDeriv_casing_bottom(mDict) + return ( + d_layer_cont_dcasing_bottom + + d_casing_cont_dcasing_bottom + + d_insideCasing_cont_dcasing_bottom + ) + + def _deriv_casing_top(self, mDict): + d_layer_cont_dcasing_top = 0.0 + d_casing_cont_dcasing_top = ( + mDict["val_casing"] - self.layer_cont(mDict) + ) * self._atanCasingDeriv_casing_top(mDict) + d_insideCasing_cont_dcasing_top = ( + mDict["val_insideCasing"] - self.layer_cont(mDict) + ) * self._atanInsideCasingDeriv_casing_top(mDict) + return ( + d_layer_cont_dcasing_top + + d_casing_cont_dcasing_top + + d_insideCasing_cont_dcasing_top + ) + + def deriv(self, m): + mDict = self.mDict(m) + + return sp.csr_matrix( + np.vstack( + [ + self._deriv_val_background(mDict), + self._deriv_val_layer(mDict), + self._deriv_val_casing(mDict), + self._deriv_val_insideCasing(mDict), + self._deriv_layer_center(mDict), + self._deriv_layer_thickness(mDict), + self._deriv_casing_radius(mDict), + self._deriv_casing_thickness(mDict), + self._deriv_casing_bottom(mDict), + self._deriv_casing_top(mDict), + ] + ).T + ) + + +class ParametricBlockInLayer(ParametricLayer): + """ + Parametric Block in a Layered Space + + For 2D: + + .. code:: python + + m = [val_background, + val_layer, + val_block, + layer_center, + layer_thickness, + block_x0, + block_dx + ] + + For 3D: + + .. code:: python + + m = [val_background, + val_layer, + val_block, + layer_center, + layer_thickness, + block_x0, + block_y0, + block_dx, + block_dy + ] + + **Required** + + :param discretize.base.BaseMesh mesh: SimPEG Mesh, 2D or 3D + + **Optional** + + :param float slopeFact: arctan slope factor - divided by the minimum h + spacing to give the slope of the arctan + functions + :param float slope: slope of the arctan function + :param numpy.ndarray indActive: bool vector with + + """ + + def __init__(self, mesh, **kwargs): + super().__init__(mesh, **kwargs) + + @property + def nP(self): + if self.mesh.dim == 2: + return 7 + elif self.mesh.dim == 3: + return 9 + + @property + def shape(self): + if self.indActive is not None: + return (sum(self.indActive), self.nP) + return (self.mesh.nC, self.nP) + + def _mDict2d(self, m): + return { + "val_background": m[0], + "val_layer": m[1], + "val_block": m[2], + "layer_center": m[3], + "layer_thickness": m[4], + "x0": m[5], + "dx": m[6], + } + + def _mDict3d(self, m): + return { + "val_background": m[0], + "val_layer": m[1], + "val_block": m[2], + "layer_center": m[3], + "layer_thickness": m[4], + "x0": m[5], + "y0": m[6], + "dx": m[7], + "dy": m[8], + } + + def mDict(self, m): + if self.mesh.dim == 2: + return self._mDict2d(m) + elif self.mesh.dim == 3: + return self._mDict3d(m) + + def xleft(self, mDict): + return mDict["x0"] - 0.5 * mDict["dx"] + + def xright(self, mDict): + return mDict["x0"] + 0.5 * mDict["dx"] + + def yleft(self, mDict): + return mDict["y0"] - 0.5 * mDict["dy"] + + def yright(self, mDict): + return mDict["y0"] + 0.5 * mDict["dy"] + + def _atanBlock2d(self, mDict): + return ( + self._atanLayer(mDict) + * self._atanfct(self.x - self.xleft(mDict), self.slope) + * self._atanfct(self.x - self.xright(mDict), -self.slope) + ) + + def _atanBlock2dDeriv_layer_center(self, mDict): + return ( + self._atanLayerDeriv_layer_center(mDict) + * self._atanfct(self.x - self.xleft(mDict), self.slope) + * self._atanfct(self.x - self.xright(mDict), -self.slope) + ) + + def _atanBlock2dDeriv_layer_thickness(self, mDict): + return ( + self._atanLayerDeriv_layer_thickness(mDict) + * self._atanfct(self.x - self.xleft(mDict), self.slope) + * self._atanfct(self.x - self.xright(mDict), -self.slope) + ) + + def _atanBlock2dDeriv_x0(self, mDict): + return self._atanLayer(mDict) * ( + ( + self._atanfctDeriv(self.x - self.xleft(mDict), self.slope) + * self._atanfct(self.x - self.xright(mDict), -self.slope) + ) + + ( + self._atanfct(self.x - self.xleft(mDict), self.slope) + * self._atanfctDeriv(self.x - self.xright(mDict), -self.slope) + ) + ) + + def _atanBlock2dDeriv_dx(self, mDict): + return self._atanLayer(mDict) * ( + ( + self._atanfctDeriv(self.x - self.xleft(mDict), self.slope) + * -0.5 + * self._atanfct(self.x - self.xright(mDict), -self.slope) + ) + + ( + self._atanfct(self.x - self.xleft(mDict), self.slope) + * 0.5 + * self._atanfctDeriv(self.x - self.xright(mDict), -self.slope) + ) + ) + + def _atanBlock3d(self, mDict): + return ( + self._atanLayer(mDict) + * self._atanfct(self.x - self.xleft(mDict), self.slope) + * self._atanfct(self.x - self.xright(mDict), -self.slope) + * self._atanfct(self.y - self.yleft(mDict), self.slope) + * self._atanfct(self.y - self.yright(mDict), -self.slope) + ) + + def _atanBlock3dDeriv_layer_center(self, mDict): + return ( + self._atanLayerDeriv_layer_center(mDict) + * self._atanfct(self.x - self.xleft(mDict), self.slope) + * self._atanfct(self.x - self.xright(mDict), -self.slope) + * self._atanfct(self.y - self.yleft(mDict), self.slope) + * self._atanfct(self.y - self.yright(mDict), -self.slope) + ) + + def _atanBlock3dDeriv_layer_thickness(self, mDict): + return ( + self._atanLayerDeriv_layer_thickness(mDict) + * self._atanfct(self.x - self.xleft(mDict), self.slope) + * self._atanfct(self.x - self.xright(mDict), -self.slope) + * self._atanfct(self.y - self.yleft(mDict), self.slope) + * self._atanfct(self.y - self.yright(mDict), -self.slope) + ) + + def _atanBlock3dDeriv_x0(self, mDict): + return self._atanLayer(mDict) * ( + ( + self._atanfctDeriv(self.x - self.xleft(mDict), self.slope) + * self._atanfct(self.x - self.xright(mDict), -self.slope) + * self._atanfct(self.y - self.yleft(mDict), self.slope) + * self._atanfct(self.y - self.yright(mDict), -self.slope) + ) + + ( + self._atanfct(self.x - self.xleft(mDict), self.slope) + * self._atanfctDeriv(self.x - self.xright(mDict), -self.slope) + * self._atanfct(self.y - self.yleft(mDict), self.slope) + * self._atanfct(self.y - self.yright(mDict), -self.slope) + ) + ) + + def _atanBlock3dDeriv_y0(self, mDict): + return self._atanLayer(mDict) * ( + ( + self._atanfct(self.x - self.xleft(mDict), self.slope) + * self._atanfct(self.x - self.xright(mDict), -self.slope) + * self._atanfctDeriv(self.y - self.yleft(mDict), self.slope) + * self._atanfct(self.y - self.yright(mDict), -self.slope) + ) + + ( + self._atanfct(self.x - self.xleft(mDict), self.slope) + * self._atanfct(self.x - self.xright(mDict), -self.slope) + * self._atanfct(self.y - self.yleft(mDict), self.slope) + * self._atanfctDeriv(self.y - self.yright(mDict), -self.slope) + ) + ) + + def _atanBlock3dDeriv_dx(self, mDict): + return self._atanLayer(mDict) * ( + ( + self._atanfctDeriv(self.x - self.xleft(mDict), self.slope) + * -0.5 + * self._atanfct(self.x - self.xright(mDict), -self.slope) + * self._atanfct(self.y - self.yleft(mDict), self.slope) + * self._atanfct(self.y - self.yright(mDict), -self.slope) + ) + + ( + self._atanfct(self.x - self.xleft(mDict), self.slope) + * self._atanfctDeriv(self.x - self.xright(mDict), -self.slope) + * 0.5 + * self._atanfct(self.y - self.yleft(mDict), self.slope) + * self._atanfct(self.y - self.yright(mDict), -self.slope) + ) + ) + + def _atanBlock3dDeriv_dy(self, mDict): + return self._atanLayer(mDict) * ( + ( + self._atanfct(self.x - self.xleft(mDict), self.slope) + * self._atanfct(self.x - self.xright(mDict), -self.slope) + * self._atanfctDeriv(self.y - self.yleft(mDict), self.slope) + * -0.5 + * self._atanfct(self.y - self.yright(mDict), -self.slope) + ) + + ( + self._atanfct(self.x - self.xleft(mDict), self.slope) + * self._atanfct(self.x - self.xright(mDict), -self.slope) + * self._atanfct(self.y - self.yleft(mDict), self.slope) + * self._atanfctDeriv(self.y - self.yright(mDict), -self.slope) + * 0.5 + ) + ) + + def _transform2d(self, m): + mDict = self.mDict(m) + # assemble the model + # contribution from the layered background + layer_cont = mDict["val_background"] + ( + mDict["val_layer"] - mDict["val_background"] + ) * self._atanLayer(mDict) + + # perturbation due to the blocks + block_cont = (mDict["val_block"] - layer_cont) * self._atanBlock2d(mDict) + + return layer_cont + block_cont + + def _deriv2d_val_background(self, mDict): + d_layer_dval_background = np.ones_like(self.x) - self._atanLayer(mDict) + d_block_dval_background = (-d_layer_dval_background) * self._atanBlock2d(mDict) + return d_layer_dval_background + d_block_dval_background + + def _deriv2d_val_layer(self, mDict): + d_layer_dval_layer = self._atanLayer(mDict) + d_block_dval_layer = (-d_layer_dval_layer) * self._atanBlock2d(mDict) + return d_layer_dval_layer + d_block_dval_layer + + def _deriv2d_val_block(self, mDict): + d_layer_dval_block = 0.0 + d_block_dval_block = (1.0 - d_layer_dval_block) * self._atanBlock2d(mDict) + return d_layer_dval_block + d_block_dval_block + + def _deriv2d_layer_center(self, mDict): + d_layer_dlayer_center = ( + mDict["val_layer"] - mDict["val_background"] + ) * self._atanLayerDeriv_layer_center(mDict) + d_block_dlayer_center = ( + mDict["val_block"] - self.layer_cont(mDict) + ) * self._atanBlock2dDeriv_layer_center( + mDict + ) - d_layer_dlayer_center * self._atanBlock2d( + mDict + ) + return d_layer_dlayer_center + d_block_dlayer_center + + def _deriv2d_layer_thickness(self, mDict): + d_layer_dlayer_thickness = ( + mDict["val_layer"] - mDict["val_background"] + ) * self._atanLayerDeriv_layer_thickness(mDict) + d_block_dlayer_thickness = ( + mDict["val_block"] - self.layer_cont(mDict) + ) * self._atanBlock2dDeriv_layer_thickness( + mDict + ) - d_layer_dlayer_thickness * self._atanBlock2d( + mDict + ) + return d_layer_dlayer_thickness + d_block_dlayer_thickness + + def _deriv2d_x0(self, mDict): + d_layer_dx0 = 0.0 + d_block_dx0 = ( + mDict["val_block"] - self.layer_cont(mDict) + ) * self._atanBlock2dDeriv_x0(mDict) + return d_layer_dx0 + d_block_dx0 + + def _deriv2d_dx(self, mDict): + d_layer_ddx = 0.0 + d_block_ddx = ( + mDict["val_block"] - self.layer_cont(mDict) + ) * self._atanBlock2dDeriv_dx(mDict) + return d_layer_ddx + d_block_ddx + + def _deriv2d(self, m): + mDict = self.mDict(m) + + return np.vstack( + [ + self._deriv2d_val_background(mDict), + self._deriv2d_val_layer(mDict), + self._deriv2d_val_block(mDict), + self._deriv2d_layer_center(mDict), + self._deriv2d_layer_thickness(mDict), + self._deriv2d_x0(mDict), + self._deriv2d_dx(mDict), + ] + ).T + + def _transform3d(self, m): + # parse model + mDict = self.mDict(m) + + # assemble the model + # contribution from the layered background + layer_cont = mDict["val_background"] + ( + mDict["val_layer"] - mDict["val_background"] + ) * self._atanLayer(mDict) + # perturbation due to the block + block_cont = (mDict["val_block"] - layer_cont) * self._atanBlock3d(mDict) + + return layer_cont + block_cont + + def _deriv3d_val_background(self, mDict): + d_layer_dval_background = np.ones_like(self.x) - self._atanLayer(mDict) + d_block_dval_background = (-d_layer_dval_background) * self._atanBlock3d(mDict) + return d_layer_dval_background + d_block_dval_background + + def _deriv3d_val_layer(self, mDict): + d_layer_dval_layer = self._atanLayer(mDict) + d_block_dval_layer = (-d_layer_dval_layer) * self._atanBlock3d(mDict) + return d_layer_dval_layer + d_block_dval_layer + + def _deriv3d_val_block(self, mDict): + d_layer_dval_block = 0.0 + d_block_dval_block = (1.0 - d_layer_dval_block) * self._atanBlock3d(mDict) + return d_layer_dval_block + d_block_dval_block + + def _deriv3d_layer_center(self, mDict): + d_layer_dlayer_center = ( + mDict["val_layer"] - mDict["val_background"] + ) * self._atanLayerDeriv_layer_center(mDict) + d_block_dlayer_center = ( + mDict["val_block"] - self.layer_cont(mDict) + ) * self._atanBlock3dDeriv_layer_center( + mDict + ) - d_layer_dlayer_center * self._atanBlock3d( + mDict + ) + return d_layer_dlayer_center + d_block_dlayer_center + + def _deriv3d_layer_thickness(self, mDict): + d_layer_dlayer_thickness = ( + mDict["val_layer"] - mDict["val_background"] + ) * self._atanLayerDeriv_layer_thickness(mDict) + d_block_dlayer_thickness = ( + mDict["val_block"] - self.layer_cont(mDict) + ) * self._atanBlock3dDeriv_layer_thickness( + mDict + ) - d_layer_dlayer_thickness * self._atanBlock3d( + mDict + ) + return d_layer_dlayer_thickness + d_block_dlayer_thickness + + def _deriv3d_x0(self, mDict): + d_layer_dx0 = 0.0 + d_block_dx0 = ( + mDict["val_block"] - self.layer_cont(mDict) + ) * self._atanBlock3dDeriv_x0(mDict) + return d_layer_dx0 + d_block_dx0 + + def _deriv3d_y0(self, mDict): + d_layer_dy0 = 0.0 + d_block_dy0 = ( + mDict["val_block"] - self.layer_cont(mDict) + ) * self._atanBlock3dDeriv_y0(mDict) + return d_layer_dy0 + d_block_dy0 + + def _deriv3d_dx(self, mDict): + d_layer_ddx = 0.0 + d_block_ddx = ( + mDict["val_block"] - self.layer_cont(mDict) + ) * self._atanBlock3dDeriv_dx(mDict) + return d_layer_ddx + d_block_ddx + + def _deriv3d_dy(self, mDict): + d_layer_ddy = 0.0 + d_block_ddy = ( + mDict["val_block"] - self.layer_cont(mDict) + ) * self._atanBlock3dDeriv_dy(mDict) + return d_layer_ddy + d_block_ddy + + def _deriv3d(self, m): + mDict = self.mDict(m) + + return np.vstack( + [ + self._deriv3d_val_background(mDict), + self._deriv3d_val_layer(mDict), + self._deriv3d_val_block(mDict), + self._deriv3d_layer_center(mDict), + self._deriv3d_layer_thickness(mDict), + self._deriv3d_x0(mDict), + self._deriv3d_y0(mDict), + self._deriv3d_dx(mDict), + self._deriv3d_dy(mDict), + ] + ).T + + def _transform(self, m): + if self.mesh.dim == 2: + return self._transform2d(m) + elif self.mesh.dim == 3: + return self._transform3d(m) + + def deriv(self, m): + if self.mesh.dim == 2: + return sp.csr_matrix(self._deriv2d(m)) + elif self.mesh.dim == 3: + return sp.csr_matrix(self._deriv3d(m)) diff --git a/simpeg/maps/_property_maps.py b/simpeg/maps/_property_maps.py new file mode 100644 index 0000000000..87bf56b48b --- /dev/null +++ b/simpeg/maps/_property_maps.py @@ -0,0 +1,1474 @@ +""" +Maps that transform physical properties from one space to another. +""" + +import warnings +import numpy as np +import scipy.sparse as sp +from scipy.sparse.linalg import LinearOperator +from scipy.constants import mu_0 +from scipy.special import expit, logit +from discretize.utils import mkvc, sdiag, rotation_matrix_from_normals + +from ._base import IdentityMap + +from ..utils import validate_integer, validate_direction, validate_float, validate_type + + +class ExpMap(IdentityMap): + r"""Mapping that computes the natural exponentials of the model parameters. + + Where :math:`\mathbf{m}` is a set of model parameters, ``ExpMap`` creates + a mapping :math:`\mathbf{u}(\mathbf{m})` that computes the natural exponential + of every element in :math:`\mathbf{m}`; i.e.: + + .. math:: + \mathbf{u}(\mathbf{m}) = exp(\mathbf{m}) + + ``ExpMap`` is commonly used when working with physical properties whose values + span many orders of magnitude (e.g. the electrical conductivity :math:`\sigma`). + By using ``ExpMap``, we can invert for a model that represents the natural log + of a set of physical property values, i.e. when :math:`m = log(\sigma)` + + Parameters + ---------- + mesh : discretize.BaseMesh + The number of parameters accepted by the mapping is set to equal the number + of mesh cells. + nP : int + Set the number of parameters accepted by the mapping directly. Used if the + number of parameters is known. Used generally when the number of parameters + is not equal to the number of cells in a mesh. + """ + + def __init__(self, mesh=None, nP=None, **kwargs): + super().__init__(mesh=mesh, nP=nP, **kwargs) + + def _transform(self, m): + return np.exp(mkvc(m)) + + def inverse(self, D): + r"""Apply the inverse of the exponential mapping to an array. + + For the exponential mapping :math:`\mathbf{u}(\mathbf{m})`, the + inverse mapping on a variable :math:`\mathbf{x}` is performed by taking + the natural logarithms of elements, i.e.: + + .. math:: + \mathbf{m} = \mathbf{u}^{-1}(\mathbf{x}) = log(\mathbf{x}) + + Parameters + ---------- + D : numpy.ndarray + A set of input values + + Returns + ------- + numpy.ndarray + A :class:`numpy.ndarray` containing result of applying the + inverse mapping to the elements in *D*; which in this case + is the natural logarithm. + """ + return np.log(mkvc(D)) + + def deriv(self, m, v=None): + r"""Derivative of mapping with respect to the input parameters. + + For a mapping :math:`\mathbf{u}(\mathbf{m})` that computes the natural + exponential function for each parameter in the model :math:`\mathbf{m}`, + i.e.: + + .. math:: + \mathbf{u}(\mathbf{m}) = exp(\mathbf{m}), + + the derivative of the mapping with respect to the model is a diagonal + matrix of the form: + + .. math:: + \frac{\partial \mathbf{u}}{\partial \mathbf{m}} + = \textrm{diag} \big ( exp(\mathbf{m}) \big ) + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. If the + input argument *v* is not ``None``, the method returns the derivative times + the vector *v*. + """ + deriv = sdiag(np.exp(mkvc(m))) + if v is not None: + return deriv * v + return deriv + + @property + def is_linear(self): + return False + + +class ReciprocalMap(IdentityMap): + r"""Mapping that computes the reciprocals of the model parameters. + + Where :math:`\mathbf{m}` is a set of model parameters, ``ReciprocalMap`` + creates a mapping :math:`\mathbf{u}(\mathbf{m})` that computes the + reciprocal of every element in :math:`\mathbf{m}`; + i.e.: + + .. math:: + \mathbf{u}(\mathbf{m}) = \mathbf{m}^{-1} + + Parameters + ---------- + mesh : discretize.BaseMesh + The number of parameters accepted by the mapping is set to equal the number + of mesh cells. + nP : int + Set the number of parameters accepted by the mapping directly. Used if the + number of parameters is known. Used generally when the number of parameters + is not equal to the number of cells in a mesh. + """ + + def __init__(self, mesh=None, nP=None, **kwargs): + super().__init__(mesh=mesh, nP=nP, **kwargs) + + def _transform(self, m): + return 1.0 / mkvc(m) + + def inverse(self, D): + r"""Apply the inverse of the reciprocal mapping to an array. + + For the reciprocal mapping :math:`\mathbf{u}(\mathbf{m})`, + the inverse mapping on a variable :math:`\mathbf{x}` is itself a + reciprocal mapping, i.e.: + + .. math:: + \mathbf{m} = \mathbf{u}^{-1}(\mathbf{x}) = \mathbf{x}^{-1} + + Parameters + ---------- + D : numpy.ndarray + A set of input values + + Returns + ------- + numpy.ndarray + A :class:`numpy.ndarray` containing result of applying the + inverse mapping to the elements in *D*; which in this case + is just a reciprocal mapping. + """ + return 1.0 / mkvc(D) + + def deriv(self, m, v=None): + r"""Derivative of mapping with respect to the input parameters. + + For a mapping that computes the reciprocal for each + parameter in the model :math:`\mathbf{m}`, i.e.: + + .. math:: + \mathbf{u}(\mathbf{m}) = \mathbf{m}^{-1} + + the derivative of the mapping with respect to the model is a diagonal + matrix of the form: + + .. math:: + \frac{\partial \mathbf{u}}{\partial \mathbf{m}} + = \textrm{diag} \big ( -\mathbf{m}^{-2} \big ) + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. If the + input argument *v* is not ``None``, the method returns the derivative times + the vector *v*. + """ + deriv = sdiag(-mkvc(m) ** (-2)) + if v is not None: + return deriv * v + return deriv + + @property + def is_linear(self): + return False + + +class LogMap(IdentityMap): + r"""Mapping that computes the natural logarithm of the model parameters. + + Where :math:`\mathbf{m}` is a set of model parameters, ``LogMap`` + creates a mapping :math:`\mathbf{u}(\mathbf{m})` that computes the + natural logarithm of every element in + :math:`\mathbf{m}`; i.e.: + + .. math:: + \mathbf{u}(\mathbf{m}) = \textrm{log}(\mathbf{m}) + + Parameters + ---------- + mesh : discretize.BaseMesh + The number of parameters accepted by the mapping is set to equal the number + of mesh cells. + nP : int + Set the number of parameters accepted by the mapping directly. Used if the + number of parameters is known. Used generally when the number of parameters + is not equal to the number of cells in a mesh. + """ + + def __init__(self, mesh=None, nP=None, **kwargs): + super().__init__(mesh=mesh, nP=nP, **kwargs) + + def _transform(self, m): + return np.log(mkvc(m)) + + def deriv(self, m, v=None): + r"""Derivative of mapping with respect to the input parameters. + + For a mapping :math:`\mathbf{u}(\mathbf{m})` that computes the + natural logarithm for each parameter in the model :math:`\mathbf{m}`, + i.e.: + + .. math:: + \mathbf{u}(\mathbf{m}) = log(\mathbf{m}) + + the derivative of the mapping with respect to the model is a diagonal + matrix of the form: + + .. math:: + \frac{\partial \mathbf{u}}{\partial \mathbf{m}} + = \textrm{diag} \big ( \mathbf{m}^{-1} \big ) + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. If the + input argument *v* is not ``None``, the method returns the derivative times + the vector *v*. + """ + mod = mkvc(m) + deriv = np.zeros(mod.shape) + tol = 1e-16 # zero + ind = np.greater_equal(np.abs(mod), tol) + deriv[ind] = 1.0 / mod[ind] + if v is not None: + return sdiag(deriv) * v + return sdiag(deriv) + + def inverse(self, m): + r"""Apply the inverse of the natural log mapping to an array. + + For the natural log mapping :math:`\mathbf{u}(\mathbf{m})`, + the inverse mapping on a variable :math:`\mathbf{x}` is performed by + taking the natural exponent of the elements, i.e.: + + .. math:: + \mathbf{m} = \mathbf{u}^{-1}(\mathbf{x}) = exp(\mathbf{x}) + + Parameters + ---------- + D : numpy.ndarray + A set of input values + + Returns + ------- + numpy.ndarray + A :class:`numpy.ndarray` containing result of applying the + inverse mapping to the elements in *D*; which in this case + is the natural exponent. + """ + return np.exp(mkvc(m)) + + @property + def is_linear(self): + return False + + +class LogisticSigmoidMap(IdentityMap): + r"""Mapping that computes the logistic sigmoid of the model parameters. + + Where :math:`\mathbf{m}` is a set of model parameters, ``LogisticSigmoidMap`` creates + a mapping :math:`\mathbf{u}(\mathbf{m})` that computes the logistic sigmoid + of every element in :math:`\mathbf{m}`; i.e.: + + .. math:: + \mathbf{u}(\mathbf{m}) = sigmoid(\mathbf{m}) = \frac{1}{1+\exp{-\mathbf{m}}} + + ``LogisticSigmoidMap`` transforms values onto the interval (0,1), but can optionally + be scaled and shifted to the interval (a,b). This can be useful for inversion + of data that varies over a log scale and bounded on some interval: + + .. math:: + \mathbf{u}(\mathbf{m}) = a + (b - a) \cdot sigmoid(\mathbf{m}) + + Parameters + ---------- + mesh : discretize.BaseMesh + The number of parameters accepted by the mapping is set to equal the number + of mesh cells. + nP : int + Set the number of parameters accepted by the mapping directly. Used if the + number of parameters is known. Used generally when the number of parameters + is not equal to the number of cells in a mesh. + lower_bound: float or (nP) numpy.ndarray + lower bound (a) for the transform. Default 0. Defined \in \mathbf{u} space. + upper_bound: float or (nP) numpy.ndarray + upper bound (b) for the transform. Default 1. Defined \in \mathbf{u} space. + + """ + + def __init__(self, mesh=None, nP=None, lower_bound=0, upper_bound=1, **kwargs): + super().__init__(mesh=mesh, nP=nP, **kwargs) + lower_bound = np.atleast_1d(lower_bound) + upper_bound = np.atleast_1d(upper_bound) + if self.nP != "*": + # check if lower bound and upper bound broadcast to nP + try: + np.broadcast_shapes(lower_bound.shape, (self.nP,)) + except ValueError as err: + raise ValueError( + f"Lower bound does not broadcast to the number of parameters. " + f"Lower bound shape is {lower_bound.shape} and tried against " + f"{self.nP} parameters." + ) from err + try: + np.broadcast_shapes(upper_bound.shape, (self.nP,)) + except ValueError as err: + raise ValueError( + f"Upper bound does not broadcast to the number of parameters. " + f"Upper bound shape is {upper_bound.shape} and tried against " + f"{self.nP} parameters." + ) from err + # make sure lower and upper bound broadcast to each other... + try: + np.broadcast_shapes(lower_bound.shape, upper_bound.shape) + except ValueError as err: + raise ValueError( + f"Upper bound does not broadcast to the lower bound. " + f"Shapes {upper_bound.shape} and {lower_bound.shape} " + f"are incompatible with each other." + ) from err + + if np.any(lower_bound >= upper_bound): + raise ValueError( + "A lower bound is greater than or equal to the upper bound." + ) + + self._lower_bound = lower_bound + self._upper_bound = upper_bound + + @property + def lower_bound(self): + """The lower bound + + Returns + ------- + numpy.ndarray + """ + return self._lower_bound + + @property + def upper_bound(self): + """The upper bound + + Returns + ------- + numpy.ndarray + """ + return self._upper_bound + + def _transform(self, m): + return self.lower_bound + (self.upper_bound - self.lower_bound) * expit(mkvc(m)) + + def inverse(self, m): + r"""Apply the inverse of the mapping to an array. + + For the logistic sigmoid mapping :math:`\mathbf{u}(\mathbf{m})`, the + inverse mapping on a variable :math:`\mathbf{x}` is performed by taking + the log-odds of elements, i.e.: + + .. math:: + \mathbf{m} = \mathbf{u}^{-1}(\mathbf{x}) = logit(\mathbf{x}) = \log \frac{\mathbf{x}}{1 - \mathbf{x}} + + or scaled and translated to interval (a,b): + .. math:: + \mathbf{m} = logit(\frac{(\mathbf{x} - a)}{b-a}) + + Parameters + ---------- + m : numpy.ndarray + A set of input values + + Returns + ------- + numpy.ndarray + the inverse mapping to the elements in *m*; which in this case + is the log-odds function with scaled and shifted input. + """ + return logit( + (mkvc(m) - self.lower_bound) / (self.upper_bound - self.lower_bound) + ) + + def deriv(self, m, v=None): + r"""Derivative of mapping with respect to the input parameters. + + For a mapping :math:`\mathbf{u}(\mathbf{m})` the derivative of the mapping with + respect to the model is a diagonal matrix of the form: + + .. math:: + \frac{\partial \mathbf{u}}{\partial \mathbf{m}} + = \textrm{diag} \big ( (b-a)\cdot sigmoid(\mathbf{m})\cdot(1-sigmoid(\mathbf{m})) \big ) + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + numpy.ndarray or scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. If the + input argument *v* is not ``None``, the method returns the derivative times + the vector *v*. + """ + sigmoid = expit(mkvc(m)) + deriv = (self.upper_bound - self.lower_bound) * sigmoid * (1.0 - sigmoid) + if v is not None: + return deriv * v + return sdiag(deriv) + + @property + def is_linear(self): + return False + + +class ChiMap(IdentityMap): + r"""Mapping that computes the magnetic permeability given a set of magnetic susceptibilities. + + Where :math:`\boldsymbol{\chi}` is the input model parameters defining a set of magnetic + susceptibilities, ``ChiMap`` creates a mapping :math:`\boldsymbol{\mu}(\boldsymbol{\chi})` + that computes the corresponding magnetic permeabilities of every + element in :math:`\boldsymbol{\chi}`; i.e.: + + .. math:: + \boldsymbol{\mu}(\boldsymbol{\chi}) = \mu_0 \big (1 + \boldsymbol{\chi} \big ) + + where :math:`\mu_0` is the permeability of free space. + + Parameters + ---------- + mesh : discretize.BaseMesh + The number of parameters accepted by the mapping is set to equal the number + of mesh cells. + nP : int + Set the number of parameters accepted by the mapping directly. Used if the + number of parameters is known. Used generally when the number of parameters + is not equal to the number of cells in a mesh. + """ + + def __init__(self, mesh=None, nP=None, **kwargs): + super().__init__(mesh=mesh, nP=nP, **kwargs) + + def _transform(self, m): + return mu_0 * (1 + m) + + def deriv(self, m, v=None): + r"""Derivative of mapping with respect to the input parameters. + + For a mapping :math:`\boldsymbol{\mu}(\boldsymbol{\chi})` that transforms a + set of magnetic susceptibilities :math:`\boldsymbol{\chi}` to their corresponding + magnetic permeabilities, i.e.: + + .. math:: + \boldsymbol{\mu}(\boldsymbol{\chi}) = \mu_0 \big (1 + \boldsymbol{\chi} \big ), + + the derivative of the mapping with respect to the model is the identity + matrix scaled by the permeability of free-space. Thus: + + .. math:: + \frac{\partial \boldsymbol{\mu}}{\partial \boldsymbol{\chi}} = \mu_0 \mathbf{I} + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. If the + input argument *v* is not ``None``, the method returns the derivative times + the vector *v*. + """ + if v is not None: + return mu_0 * v + return mu_0 * sp.eye(self.nP) + + def inverse(self, m): + r"""Apply the inverse mapping to an array. + + For the ``ChiMap`` class, the inverse mapping recoveres the set of + magnetic susceptibilities :math:`\boldsymbol{\chi}` from a set of + magnetic permeabilities :math:`\boldsymbol{\mu}`. Thus the inverse + mapping is defined as: + + .. math:: + \boldsymbol{\chi}(\boldsymbol{\mu}) = \frac{\boldsymbol{\mu}}{\mu_0} - 1 + + where :math:`\mu_0` is the permeability of free space. + + Parameters + ---------- + D : numpy.ndarray + A set of input values + + Returns + ------- + numpy.ndarray + A :class:`numpy.ndarray` containing result of applying the + inverse mapping to the elements in *D*; which in this case + represents the conversion of magnetic permeabilities + to their corresponding magnetic susceptibility values. + """ + return m / mu_0 - 1 + + +class MuRelative(IdentityMap): + r"""Mapping that computes the magnetic permeability given a set of relative permeabilities. + + Where :math:`\boldsymbol{\mu_r}` defines a set of relative permeabilities, ``MuRelative`` + creates a mapping :math:`\boldsymbol{\mu}(\boldsymbol{\mu_r})` that computes the + corresponding magnetic permeabilities of every element in :math:`\boldsymbol{\mu_r}`; + i.e.: + + .. math:: + \boldsymbol{\mu}(\boldsymbol{\mu_r}) = \mu_0 \boldsymbol{\mu_r} + + where :math:`\mu_0` is the permeability of free space. + + Parameters + ---------- + mesh : discretize.BaseMesh + The number of parameters accepted by the mapping is set to equal the number + of mesh cells. + nP : int + Set the number of parameters accepted by the mapping directly. Used if the + number of parameters is known. Used generally when the number of parameters + is not equal to the number of cells in a mesh. + """ + + def __init__(self, mesh=None, nP=None, **kwargs): + super().__init__(mesh=mesh, nP=nP, **kwargs) + + def _transform(self, m): + return mu_0 * m + + def deriv(self, m, v=None): + r"""Derivative of mapping with respect to the input parameters. + + For a mapping that transforms a set of relative permeabilities + :math:`\boldsymbol{\mu_r}` to their corresponding magnetic permeabilities, i.e.: + + .. math:: + \boldsymbol{\mu}(\boldsymbol{\mu_r}) = \mu_0 \boldsymbol{\mu_r}, + + the derivative of the mapping with respect to the model is the identity + matrix scaled by the permeability of free-space. Thus: + + .. math:: + \frac{\partial \boldsymbol{\mu}}{\partial \boldsymbol{\mu_r}} = \mu_0 \mathbf{I} + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. If the + input argument *v* is not ``None``, the method returns the derivative times + the vector *v*. + """ + if v is not None: + return mu_0 * v + return mu_0 * sp.eye(self.nP) + + def inverse(self, m): + r"""Apply the inverse mapping to an array. + + For the ``MuRelative`` class, the inverse mapping recoveres the set of + relative permeabilities :math:`\boldsymbol{\mu_r}` from a set of + magnetic permeabilities :math:`\boldsymbol{\mu}`. Thus the inverse + mapping is defined as: + + .. math:: + \boldsymbol{\mu_r}(\boldsymbol{\mu}) = \frac{\boldsymbol{\mu}}{\mu_0} + + where :math:`\mu_0` is the permeability of free space. + + Parameters + ---------- + D : numpy.ndarray + A set of input values + + Returns + ------- + numpy.ndarray + A :class:`numpy.ndarray` containing result of applying the + inverse mapping to the elements in *D*; which in this case + represents the conversion of magnetic permeabilities + to their corresponding relative permeability values. + """ + return 1.0 / mu_0 * m + + +class Weighting(IdentityMap): + r"""Mapping that scales the elements of the model by a corresponding set of weights. + + Where :math:`\mathbf{m}` defines the set of input model parameters and + :math:`\mathbf{w}` represents a corresponding set of model weight, + ``Weighting`` constructs a mapping :math:`\mathbf{u}(\mathbf{m})` of the form: + + .. math:: + \mathbf{u}(\mathbf{m}) = \mathbf{w} \odot \mathbf{m} + + where :math:`\odot` is the Hadamard product. The mapping may also be + defined using a linear operator as follows: + + .. math:: + \mathbf{u}(\mathbf{m}) = \mathbf{Pm} \;\;\;\;\; \textrm{where} \;\;\;\;\; \mathbf{P} = diag(\mathbf{w}) + + Parameters + ---------- + mesh : discretize.BaseMesh + The number of parameters accepted by the mapping is set to equal the number + of mesh cells. + nP : int + Set the number of parameters accepted by the mapping directly. Used if the + number of parameters is known. Used generally when the number of parameters + is not equal to the number of cells in a mesh. + weights : (nP) numpy.ndarray + A set of independent model weights. If ``None``, all model weights are set + to *1*. + """ + + def __init__(self, mesh=None, nP=None, weights=None, **kwargs): + if "nC" in kwargs: + raise TypeError( + "`nC` has been removed. Use `nP` to set the number of model " + "parameters." + ) + + super(Weighting, self).__init__(mesh=mesh, nP=nP, **kwargs) + + if weights is None: + weights = np.ones(self.nP) + + self.weights = np.array(weights, dtype=float) + + @property + def shape(self): + """Dimensions of the mapping. + + Returns + ------- + tuple + Dimensions of the mapping. Where *nP* is the number of parameters + the mapping acts on, this method returns a tuple of the form + (*nP*, *nP*). + """ + return (self.nP, self.nP) + + @property + def P(self): + r"""The linear mapping operator + + This property returns the sparse matrix :math:`\mathbf{P}` that carries + out the weighting mapping via matrix-vector product, i.e.: + + .. math:: + \mathbf{u}(\mathbf{m}) = \mathbf{Pm} \;\;\;\;\; \textrm{where} \;\;\;\;\; \mathbf{P} = diag(\mathbf{w}) + + Returns + ------- + scipy.sparse.csr_matrix + Sparse linear mapping operator + """ + return sdiag(self.weights) + + def _transform(self, m): + return self.weights * m + + def inverse(self, D): + r"""Apply the inverse of the weighting mapping to an array. + + For the weighting mapping :math:`\mathbf{u}(\mathbf{m})`, the inverse + mapping on a variable :math:`\mathbf{x}` is performed by multplying each element by + the reciprocal of its corresponding weighting value, i.e.: + + .. math:: + \mathbf{m} = \mathbf{u}^{-1}(\mathbf{x}) = \mathbf{w}^{-1} \odot \mathbf{x} + + where :math:`\odot` is the Hadamard product. The inverse mapping may also be defined + using a linear operator as follows: + + .. math:: + \mathbf{m} = \mathbf{u}^{-1}(\mathbf{x}) = \mathbf{P^{-1} m} + \;\;\;\;\; \textrm{where} \;\;\;\;\; \mathbf{P} = diag(\mathbf{w}) + + Parameters + ---------- + D : numpy.ndarray + A set of input values + + Returns + ------- + numpy.ndarray + A :class:`numpy.ndarray` containing result of applying the + inverse mapping to the elements in *D*; which in this case + is simply dividing each element by its corresponding + weight. + """ + return self.weights ** (-1.0) * D + + def deriv(self, m, v=None): + r"""Derivative of mapping with respect to the input parameters. + + For a weighting mapping :math:`\mathbf{u}(\mathbf{m})` that scales the + input parameters in the model :math:`\mathbf{m}` by their corresponding + weights :math:`\mathbf{w}`; i.e.: + + .. math:: + \mathbf{u}(\mathbf{m}) = \mathbf{w} \dot \mathbf{m}, + + the derivative of the mapping with respect to the model is a diagonal + matrix of the form: + + .. math:: + \frac{\partial \mathbf{u}}{\partial \mathbf{m}} + = diag (\mathbf{w}) + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. If the + input argument *v* is not ``None``, the method returns the derivative times + the vector *v*. + """ + if v is not None: + return self.weights * v + return self.P + + +class ComplexMap(IdentityMap): + r"""Maps the real and imaginary component values stored in a model to complex values. + + Let :math:`\mathbf{m}` be a model which stores the real and imaginary components of + a set of complex values :math:`\mathbf{z}`. Where the model parameters are organized + into a vector of the form + :math:`\mathbf{m} = [\mathbf{z}^\prime , \mathbf{z}^{\prime\prime}]`, ``ComplexMap`` + constructs the following mapping: + + .. math:: + \mathbf{z}(\mathbf{m}) = \mathbf{z}^\prime + j \mathbf{z}^{\prime\prime} + + Note that the mapping is :math:`\mathbb{R}^{2n} \rightarrow \mathbb{C}^n`. + + Parameters + ---------- + mesh : discretize.BaseMesh + If a mesh is used to construct the mapping, the number of input model + parameters is *2\*mesh.nC* and the number of complex values output from + the mapping is equal to *mesh.nC*. If *mesh* is ``None``, the dimensions + of the mapping are set using the *nP* input argument. + nP : int + Defines the number of input model parameters directly. Must be an even number!!! + In this case, the number of complex values output from the mapping is *nP/2*. + If *nP* = ``None``, the dimensions of the mapping are set using the *mesh* + input argument. + + Examples + -------- + Here we construct a complex mapping on a 1D mesh comprised + of 4 cells. The input model is real-valued array of length 8 + (4 real and 4 imaginary values). The output of the mapping + is a complex array with 4 values. + + >>> from simpeg.maps import ComplexMap + >>> from discretize import TensorMesh + >>> import numpy as np + + >>> nC = 4 + >>> mesh = TensorMesh([np.ones(nC)]) + + >>> z_real = np.ones(nC) + >>> z_imag = 2*np.ones(nC) + >>> m = np.r_[z_real, z_imag] + >>> m + array([1., 1., 1., 1., 2., 2., 2., 2.]) + + >>> mapping = ComplexMap(mesh=mesh) + >>> z = mapping * m + >>> z + array([1.+2.j, 1.+2.j, 1.+2.j, 1.+2.j]) + + """ + + def __init__(self, mesh=None, nP=None, **kwargs): + super().__init__(mesh=mesh, nP=nP, **kwargs) + if nP is not None and mesh is not None: + assert ( + 2 * mesh.nC == nP + ), "Number parameters must be 2 X number of mesh cells." + if nP is not None: + assert nP % 2 == 0, "nP must be even." + self._nP = nP or int(self.mesh.nC * 2) + + @property + def nP(self): + r"""Number of parameters the mapping acts on. + + Returns + ------- + int or '*' + Number of parameters that the mapping acts on. + """ + return self._nP + + @property + def shape(self): + """Dimensions of the mapping + + Returns + ------- + tuple + The dimensions of the mapping. Where *nP* is the number + of input parameters, this property returns a tuple + (*nP/2*, *nP*). + """ + return (int(self.nP / 2), self.nP) + + def _transform(self, m): + nC = int(self.nP / 2) + return m[:nC] + m[nC:] * 1j + + def deriv(self, m, v=None): + r"""Derivative of the complex mapping with respect to the input parameters. + + The complex mapping maps the real and imaginary components stored in a model + of the form :math:`\mathbf{m} = [\mathbf{z}^\prime , \mathbf{z}^{\prime\prime}]` + to their corresponding complex values :math:`\mathbf{z}`, i.e. + + .. math:: + \mathbf{z}(\mathbf{m}) = \mathbf{z}^\prime + j \mathbf{z}^{\prime\prime} + + The derivative of the mapping with respect to the model is block + matrix of the form: + + .. math:: + \frac{\partial \mathbf{z}}{\partial \mathbf{m}} = \big ( \mathbf{I} \;\;\; j\mathbf{I} \big ) + + where :math:`\mathbf{I}` is the identity matrix of shape (*nP/2*, *nP/2*) and + :math:`j = \sqrt{-1}`. + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. If the + input argument *v* is not ``None``, the method returns the derivative times + the vector *v*. + + Examples + -------- + Here we construct the derivative operator for the complex mapping on a 1D + mesh comprised of 4 cells. We then demonstrate how the derivative of the + mapping and its adjoint can be applied to a vector. + + >>> from simpeg.maps import ComplexMap + >>> from discretize import TensorMesh + >>> import numpy as np + + >>> nC = 4 + >>> mesh = TensorMesh([np.ones(nC)]) + + >>> m = np.random.rand(2*nC) + >>> mapping = ComplexMap(mesh=mesh) + >>> M = mapping.deriv(m) + + When applying the derivative operator to a vector, it will convert + the real and imaginary values stored in the vector to + complex values; essentially applying the mapping. + + >>> v1 = np.arange(0, 2*nC, 1) + >>> u1 = M * v1 + >>> u1 + array([0.+4.j, 1.+5.j, 2.+6.j, 3.+7.j]) + + When applying the adjoint of the derivative operator to a set of + complex values, the operator will decompose these values into + their real and imaginary components. + + >>> v2 = np.arange(0, nC, 1) + 1j*np.arange(nC, 2*nC, 1) + >>> u2 = M.adjoint() * v2 + >>> u2 + array([0., 1., 2., 3., 4., 5., 6., 7.]) + + """ + nC = self.shape[0] + shp = (nC, nC * 2) + + def fwd(v): + return v[:nC] + v[nC:] * 1j + + def adj(v): + return np.r_[v.real, v.imag] + + if v is not None: + return LinearOperator(shp, matvec=fwd, rmatvec=adj) * v + return LinearOperator(shp, matvec=fwd, rmatvec=adj) + + +class SelfConsistentEffectiveMedium(IdentityMap): + r""" + Two phase self-consistent effective medium theory mapping for + ellipsoidal inclusions. The inversion model is the concentration + (volume fraction) of the phase 2 material. + + The inversion model is :math:`\varphi`. We solve for :math:`\sigma` + given :math:`\sigma_0`, :math:`\sigma_1` and :math:`\varphi` . Each of + the following are implicit expressions of the effective conductivity. + They are solved using a fixed point iteration. + + **Spherical Inclusions** + + If the shape of the inclusions are spheres, we use + + .. math:: + + \sum_{j=1}^N (\sigma^* - \sigma_j)R^{j} = 0 + + where :math:`j=[1,N]` is the each material phase, and N is the number + of phases. Currently, the implementation is only set up for 2 phase + materials, so we solve + + .. math:: + + (1-\\varphi)(\sigma - \sigma_0)R^{(0)} + \varphi(\sigma - \sigma_1)R^{(1)} = 0. + + Where :math:`R^{(j)}` is given by + + .. math:: + + R^{(j)} = \left[1 + \frac{1}{3}\frac{\sigma_j - \sigma}{\sigma} \right]^{-1}. + + **Ellipsoids** + + .. todo:: + + Aligned Ellipsoids have not yet been implemented, only randomly + oriented ellipsoids + + If the inclusions are aligned ellipsoids, we solve + + .. math:: + + \sum_{j=1}^N \varphi_j (\Sigma^* - \sigma_j\mathbf{I}) \mathbf{R}^{j, *} = 0 + + where + + .. math:: + + \mathbf{R}^{(j, *)} = \left[ \mathbf{I} + \mathbf{A}_j {\Sigma^{*}}^{-1}(\sigma_j \mathbf{I} - \Sigma^*) \\right]^{-1} + + and the depolarization tensor :math:`\mathbf{A}_j` is given by + + .. math:: + + \mathbf{A}^* = \left[\begin{array}{ccc} + Q & 0 & 0 \\ + 0 & Q & 0 \\ + 0 & 0 & 1-2Q + \end{array}\right] + + for a spheroid aligned along the z-axis. For an oblate spheroid + (:math:`\alpha < 1`, pancake-like) + + .. math:: + + Q = \frac{1}{2}\left( + 1 + \frac{1}{\alpha^2 - 1} \left[ + 1 - \frac{1}{\chi}\tan^{-1}(\chi) + \right] + \right) + + where + + .. math:: + + \chi = \sqrt{\frac{1}{\alpha^2} - 1} + + + For reference, see + `Torquato (2002), Random Heterogeneous Materials `_ + + + """ + + def __init__( + self, + mesh=None, + nP=None, + sigma0=None, + sigma1=None, + alpha0=1.0, + alpha1=1.0, + orientation0="z", + orientation1="z", + random=True, + rel_tol=1e-3, + maxIter=50, + **kwargs, + ): + self._sigstart = None + self.sigma0 = sigma0 + self.sigma1 = sigma1 + self.alpha0 = alpha0 + self.alpha1 = alpha1 + self.orientation0 = orientation0 + self.orientation1 = orientation1 + self.random = random + self.rel_tol = rel_tol + self.maxIter = maxIter + super(SelfConsistentEffectiveMedium, self).__init__(mesh, nP, **kwargs) + + @property + def sigma0(self): + """Physical property value for phase-0 material. + + Returns + ------- + float + """ + return self._sigma0 + + @sigma0.setter + def sigma0(self, value): + self._sigma0 = validate_float("sigma0", value, min_val=0.0) + + @property + def sigma1(self): + """Physical property value for phase-1 material. + + Returns + ------- + float + """ + return self._sigma1 + + @sigma1.setter + def sigma1(self, value): + self._sigma1 = validate_float("sigma1", value, min_val=0.0) + + @property + def alpha0(self): + """Aspect ratio of the phase-0 ellipsoids. + + Returns + ------- + float + """ + return self._alpha0 + + @alpha0.setter + def alpha0(self, value): + self._alpha0 = validate_float("alpha0", value, min_val=0.0) + + @property + def alpha1(self): + """Aspect ratio of the phase-1 ellipsoids. + + Returns + ------- + float + """ + return self._alpha1 + + @alpha1.setter + def alpha1(self, value): + self._alpha1 = validate_float("alpha1", value, min_val=0.0) + + @property + def orientation0(self): + """Orientation of the phase-0 inclusions. + + Returns + ------- + numpy.ndarray + """ + return self._orientation0 + + @orientation0.setter + def orientation0(self, value): + self._orientation0 = validate_direction("orientation0", value, dim=3) + + @property + def orientation1(self): + """Orientation of the phase-0 inclusions. + + Returns + ------- + numpy.ndarray + """ + return self._orientation1 + + @orientation1.setter + def orientation1(self, value): + self._orientation1 = validate_direction("orientation1", value, dim=3) + + @property + def random(self): + """Are the inclusions randomly oriented (True) or preferentially aligned (False)? + + Returns + ------- + bool + """ + return self._random + + @random.setter + def random(self, value): + self._random = validate_type("random", value, bool) + + @property + def rel_tol(self): + """relative tolerance for convergence for the fixed-point iteration. + + Returns + ------- + float + """ + return self._rel_tol + + @rel_tol.setter + def rel_tol(self, value): + self._rel_tol = validate_float( + "rel_tol", value, min_val=0.0, inclusive_min=False + ) + + @property + def maxIter(self): + """Maximum number of iterations for the fixed point iteration calculation. + + Returns + ------- + int + """ + return self._maxIter + + @maxIter.setter + def maxIter(self, value): + self._maxIter = validate_integer("maxIter", value, min_val=0) + + @property + def tol(self): + """ + absolute tolerance for the convergence of the fixed point iteration + calc + """ + if getattr(self, "_tol", None) is None: + self._tol = self.rel_tol * min(self.sigma0, self.sigma1) + return self._tol + + @property + def sigstart(self): + """ + first guess for sigma + """ + return self._sigstart + + @sigstart.setter + def sigstart(self, value): + if value is not None: + value = validate_float("sigstart", value) + self._sigstart = value + + def wiener_bounds(self, phi1): + """Define Wenner Conductivity Bounds + + See Torquato, 2002 + """ + phi0 = 1.0 - phi1 + sigWup = phi0 * self.sigma0 + phi1 * self.sigma1 + sigWlo = 1.0 / (phi0 / self.sigma0 + phi1 / self.sigma1) + W = np.array([sigWlo, sigWup]) + + return W + + def hashin_shtrikman_bounds(self, phi1): + """Hashin Shtrikman bounds + + See Torquato, 2002 + """ + # TODO: this should probably exsist on its own as a util + + phi0 = 1.0 - phi1 + sigWu = self.wiener_bounds(phi1)[1] + sig_tilde = phi0 * self.sigma1 + phi1 * self.sigma0 + + sigma_min = np.min([self.sigma0, self.sigma1]) + sigma_max = np.max([self.sigma0, self.sigma1]) + + sigHSlo = sigWu - ( + (phi0 * phi1 * (self.sigma0 - self.sigma1) ** 2) + / (sig_tilde + 2 * sigma_max) + ) + sigHSup = sigWu - ( + (phi0 * phi1 * (self.sigma0 - self.sigma1) ** 2) + / (sig_tilde + 2 * sigma_min) + ) + + return np.array([sigHSlo, sigHSup]) + + def hashin_shtrikman_bounds_anisotropic(self, phi1): + """Hashin Shtrikman bounds for anisotropic media + + See Torquato, 2002 + """ + phi0 = 1.0 - phi1 + sigWu = self.wiener_bounds(phi1)[1] + + sigma_min = np.min([self.sigma0, self.sigma1]) + sigma_max = np.max([self.sigma0, self.sigma1]) + + phi_min = phi0 if self.sigma1 > self.sigma0 else phi1 + phi_max = phi1 if self.sigma1 > self.sigma0 else phi0 + + amax = ( + -phi0 + * phi1 + * self.getA( + self.alpha1 if self.sigma1 > self.sigma0 else self.alpha0, + self.orientation1 if self.sigma1 > self.sigma0 else self.orientation0, + ) + ) + I = np.eye(3) + + sigHSlo = sigWu * I + ( + (sigma_min - sigma_max) ** 2 + * amax + * np.linalg.inv(sigma_min * I + (sigma_min - sigma_max) / phi_max * amax) + ) + sigHSup = sigWu * I + ( + (sigma_max - sigma_min) ** 2 + * amax + * np.linalg.inv(sigma_max * I + (sigma_max - sigma_min) / phi_min * amax) + ) + + return [sigHSlo, sigHSup] + + def getQ(self, alpha): + """Geometric factor in the depolarization tensor""" + if alpha < 1.0: # oblate spheroid + chi = np.sqrt((1.0 / alpha**2.0) - 1) + return ( + 1.0 / 2.0 * (1 + 1.0 / (alpha**2.0 - 1) * (1.0 - np.arctan(chi) / chi)) + ) + elif alpha > 1.0: # prolate spheroid + chi = np.sqrt(1 - (1.0 / alpha**2.0)) + return ( + 1.0 + / 2.0 + * ( + 1 + + 1.0 + / (alpha**2.0 - 1) + * (1.0 - 1.0 / (2.0 * chi) * np.log((1 + chi) / (1 - chi))) + ) + ) + elif alpha == 1: # sphere + return 1.0 / 3.0 + + def getA(self, alpha, orientation): + """Depolarization tensor""" + Q = self.getQ(alpha) + A = np.diag([Q, Q, 1 - 2 * Q]) + R = rotation_matrix_from_normals(np.r_[0.0, 0.0, 1.0], orientation) + return (R.T).dot(A).dot(R) + + def getR(self, sj, se, alpha, orientation=None): + """Electric field concentration tensor""" + if self.random is True: # isotropic + if alpha == 1.0: + return 3.0 * se / (2.0 * se + sj) + Q = self.getQ(alpha) + return ( + se + / 3.0 + * (2.0 / (se + Q * (sj - se)) + 1.0 / (sj - 2.0 * Q * (sj - se))) + ) + else: # anisotropic + if orientation is None: + raise Exception("orientation must be provided if random=False") + I = np.eye(3) + seinv = np.linalg.inv(se) + Rinv = I + self.getA(alpha, orientation) * seinv * (sj * I - se) + return np.linalg.inv(Rinv) + + def getdR(self, sj, se, alpha, orientation=None): + """ + Derivative of the electric field concentration tensor with respect + to the concentration of the second phase material. + """ + if self.random is True: + if alpha == 1.0: + return 3.0 / (2.0 * se + sj) - 6.0 * se / (2.0 * se + sj) ** 2 + Q = self.getQ(alpha) + return ( + 1 + / 3 + * ( + 2.0 / (se + Q * (sj - se)) + + 1.0 / (sj - 2.0 * Q * (sj - se)) + + se + * ( + -2 * (1 - Q) / (se + Q * (sj - se)) ** 2 + - 2 * Q / (sj - 2.0 * Q * (sj - se)) ** 2 + ) + ) + ) + else: + if orientation is None: + raise Exception("orientation must be provided if random=False") + raise NotImplementedError + + def _sc2phaseEMTSpheroidstransform(self, phi1): + """ + Self Consistent Effective Medium Theory Model Transform, + alpha = aspect ratio (c/a <= 1) + """ + + if not (np.all(0 <= phi1) and np.all(phi1 <= 1)): + warnings.warn("there are phis outside bounds of 0 and 1", stacklevel=2) + phi1 = np.median(np.c_[phi1 * 0, phi1, phi1 * 0 + 1.0]) + + phi0 = 1.0 - phi1 + + # starting guess + if self.sigstart is None: + sige1 = np.mean(self.wiener_bounds(phi1)) + else: + sige1 = self.sigstart + + if self.random is False: + sige1 = sige1 * np.eye(3) + + for _ in range(self.maxIter): + R0 = self.getR(self.sigma0, sige1, self.alpha0, self.orientation0) + R1 = self.getR(self.sigma1, sige1, self.alpha1, self.orientation1) + + den = phi0 * R0 + phi1 * R1 + num = phi0 * self.sigma0 * R0 + phi1 * self.sigma1 * R1 + + if self.random is True: + sige2 = num / den + relerr = np.abs(sige2 - sige1) + else: + sige2 = num * np.linalg.inv(den) + relerr = np.linalg.norm(np.abs(sige2 - sige1).flatten(), np.inf) + + if np.all(relerr <= self.tol): + if self.sigstart is None: + self._sigstart = ( + sige2 # store as a starting point for the next time around + ) + return sige2 + + sige1 = sige2 + # TODO: make this a proper warning, and output relevant info (sigma0, sigma1, phi, sigstart, and relerr) + warnings.warn("Maximum number of iterations reached", stacklevel=2) + + return sige2 + + def _sc2phaseEMTSpheroidsinversetransform(self, sige): + R0 = self.getR(self.sigma0, sige, self.alpha0, self.orientation0) + R1 = self.getR(self.sigma1, sige, self.alpha1, self.orientation1) + + num = -(self.sigma0 - sige) * R0 + den = (self.sigma1 - sige) * R1 - (self.sigma0 - sige) * R0 + + return num / den + + def _sc2phaseEMTSpheroidstransformDeriv(self, sige, phi1): + phi0 = 1.0 - phi1 + + R0 = self.getR(self.sigma0, sige, self.alpha0, self.orientation0) + R1 = self.getR(self.sigma1, sige, self.alpha1, self.orientation1) + + dR0 = self.getdR(self.sigma0, sige, self.alpha0, self.orientation0) + dR1 = self.getdR(self.sigma1, sige, self.alpha1, self.orientation1) + + num = (sige - self.sigma0) * R0 - (sige - self.sigma1) * R1 + den = phi0 * (R0 + (sige - self.sigma0) * dR0) + phi1 * ( + R1 + (sige - self.sigma1) * dR1 + ) + + return sdiag(num / den) + + def _transform(self, m): + return self._sc2phaseEMTSpheroidstransform(m) + + def deriv(self, m): + """ + Derivative of the effective conductivity with respect to the + volume fraction of phase 2 material + """ + sige = self._transform(m) + return self._sc2phaseEMTSpheroidstransformDeriv(sige, m) + + def inverse(self, sige): + """ + Compute the concentration given the effective conductivity + """ + return self._sc2phaseEMTSpheroidsinversetransform(sige) + + @property + def is_linear(self): + return False diff --git a/simpeg/maps/_surjection.py b/simpeg/maps/_surjection.py new file mode 100644 index 0000000000..edcfa1f420 --- /dev/null +++ b/simpeg/maps/_surjection.py @@ -0,0 +1,568 @@ +""" +Surjection map classes. +""" + +import discretize +import numpy as np +import scipy.sparse as sp +from discretize import TensorMesh, CylindricalMesh +from discretize.utils import mkvc + +from ..utils import ( + validate_type, + validate_ndarray_with_shape, + validate_string, + validate_active_indices, +) +from ._base import IdentityMap + + +class SurjectFull(IdentityMap): + r"""Mapping a single property value to all mesh cells. + + Let :math:`m` be a model defined by a single physical property value + ``SurjectFull`` construct a surjective mapping that projects :math:`m` + to the set of voxel cells defining a mesh. The mapping + :math:`\mathbf{u(m)}` is a matrix of 1s of shape (*mesh.nC* , 1) that + projects the model to all mesh cells, i.e.: + + .. math:: + \mathbf{u}(\mathbf{m}) = \mathbf{Pm} + + Parameters + ---------- + mesh : discretize.BaseMesh + A discretize mesh + + """ + + def __init__(self, mesh, **kwargs): + super().__init__(mesh=mesh, **kwargs) + + @property + def nP(self): + r"""Number of parameters the mapping acts on; i.e. 1. + + Returns + ------- + int + Returns an integer value of 1 + """ + return 1 + + def _transform(self, m): + """ + :param m: model (scalar) + :rtype: numpy.ndarray + :return: transformed model + """ + return np.ones(self.mesh.nC) * m + + def deriv(self, m, v=None): + r"""Derivative of the mapping with respect to the input parameters. + + Let :math:`m` be the single parameter that the mapping acts on. The + ``SurjectFull`` class constructs a mapping that can be defined as + a projection matrix :math:`\mathbf{P}`; i.e.: + + .. math:: + \mathbf{u} = \mathbf{P m}, + + the **deriv** method returns the derivative of :math:`\mathbf{u}` with respect + to the model parameters; i.e.: + + .. math:: + \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = \mathbf{P} + + Note that in this case, **deriv** simply returns the original operator + :math:`\mathbf{P}`; a (*mesh.nC* , 1) numpy.ndarray of 1s. + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + """ + deriv = sp.csr_matrix(np.ones([self.mesh.nC, 1])) + if v is not None: + return deriv * v + return deriv + + +class SurjectVertical1D(IdentityMap): + r"""Map 1D layered Earth model to 2D or 3D tensor mesh. + + Let :math:`m` be a 1D model that defines the property values along + the last dimension of a tensor mesh; i.e. the y-direction for 2D + meshes and the z-direction for 3D meshes. ``SurjectVertical1D`` + construct a surjective mapping from the 1D model to all voxel cells + in the 2D or 3D tensor mesh provided. + + Mathematically, the mapping :math:`\mathbf{u}(\mathbf{m})` can be + represented by a projection matrix: + + .. math:: + \mathbf{u}(\mathbf{m}) = \mathbf{Pm} + + Parameters + ---------- + mesh : discretize.TensorMesh + A 2D or 3D tensor mesh + + Examples + -------- + Here we define a 1D layered Earth model comprised of 3 layers + on a 1D tensor mesh. We then use ``SurjectVertical1D`` to + construct a mapping which projects the 1D model onto a 2D + tensor mesh. + + >>> from simpeg.maps import SurjectVertical1D + >>> from simpeg.utils import plot_1d_layer_model + >>> from discretize import TensorMesh + >>> import numpy as np + >>> import matplotlib as mpl + >>> import matplotlib.pyplot as plt + + >>> dh = np.ones(20) + >>> mesh1D = TensorMesh([dh], 'C') + >>> mesh2D = TensorMesh([dh, dh], 'CC') + + >>> m = np.zeros(mesh1D.nC) + >>> m[mesh1D.cell_centers < 0] = 10. + >>> m[mesh1D.cell_centers < -5] = 5. + + >>> fig1 = plt.figure(figsize=(5,5)) + >>> ax1 = fig1.add_subplot(111) + >>> plot_1d_layer_model( + >>> mesh1D.h[0], np.flip(m), ax=ax1, z0=0, + >>> scale='linear', show_layers=True, plot_elevation=True + >>> ) + >>> ax1.set_xlim([-0.1, 11]) + >>> ax1.set_title('1D Model') + + >>> mapping = SurjectVertical1D(mesh2D) + >>> u = mapping * m + + >>> fig2 = plt.figure(figsize=(6, 5)) + >>> ax2a = fig2.add_axes([0.1, 0.15, 0.7, 0.8]) + >>> mesh2D.plot_image(u, ax=ax2a, grid=True) + >>> ax2a.set_title('Projected to 2D Mesh') + >>> ax2b = fig2.add_axes([0.83, 0.15, 0.05, 0.8]) + >>> norm = mpl.colors.Normalize(vmin=np.min(m), vmax=np.max(m)) + >>> cbar = mpl.colorbar.ColorbarBase(ax2b, norm=norm, orientation="vertical") + + """ + + def __init__(self, mesh, **kwargs): + assert isinstance( + mesh, (TensorMesh, CylindricalMesh) + ), "Only implemented for tensor meshes" + super().__init__(mesh=mesh, **kwargs) + + @property + def nP(self): + r"""Number of parameters the mapping acts on. + + Returns + ------- + int + Number of parameters the mapping acts on. Should equal the + number of cells along the last dimension of the tensor mesh + supplied when defining the mapping. + """ + return int(self.mesh.vnC[self.mesh.dim - 1]) + + def _transform(self, m): + repNum = np.prod(self.mesh.vnC[: self.mesh.dim - 1]) + return mkvc(m).repeat(repNum) + + def deriv(self, m, v=None): + r"""Derivative of the mapping with respect to the model paramters. + + Let :math:`\mathbf{m}` be a set of parameter values for the 1D model + and let :math:`\mathbf{P}` be a projection matrix that maps the 1D + model the 2D/3D tensor mesh. The forward mapping :math:`\mathbf{u}(\mathbf{m})` + is given by: + + .. math:: + \mathbf{u} = \mathbf{P m}, + + the **deriv** method returns the derivative of :math:`\mathbf{u}` with respect + to the model parameters; i.e.: + + .. math:: + \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = \mathbf{P} + + Note that in this case, **deriv** simply returns the projection matrix. + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. If the + input argument *v* is not ``None``, the method returns the derivative times + the vector *v*. + """ + repNum = np.prod(self.mesh.vnC[: self.mesh.dim - 1]) + repVec = sp.csr_matrix( + (np.ones(repNum), (range(repNum), np.zeros(repNum))), shape=(repNum, 1) + ) + deriv = sp.kron(sp.identity(self.nP), repVec) + if v is not None: + return deriv * v + return deriv + + +class Surject2Dto3D(IdentityMap): + r"""Map 2D tensor model to 3D tensor mesh. + + Let :math:`m` define the parameters for a 2D tensor model. + ``Surject2Dto3D`` constructs a surjective mapping that projects + the 2D tensor model to a 3D tensor mesh. + + Mathematically, the mapping :math:`\mathbf{u}(\mathbf{m})` can be + represented by a projection matrix: + + .. math:: + \mathbf{u}(\mathbf{m}) = \mathbf{Pm} + + Parameters + ---------- + mesh : discretize.TensorMesh + A 3D tensor mesh + normal : {'y', 'x', 'z'} + Define the projection axis. + + Examples + -------- + Here we project a 3 layered Earth model defined on a 2D tensor mesh + to a 3D tensor mesh. We assume that at for some y-location, we + have a 2D tensor model which defines the physical property distribution + as a function of the *x* and *z* location. Using ``Surject2Dto3D``, + we project the model along the y-axis to obtain a 3D distribution + for the physical property (i.e. a 3D tensor model). + + >>> from simpeg.maps import Surject2Dto3D + >>> from discretize import TensorMesh + >>> import numpy as np + >>> import matplotlib as mpl + >>> import matplotlib.pyplot as plt + + >>> dh = np.ones(20) + >>> mesh2D = TensorMesh([dh, dh], 'CC') + >>> mesh3D = TensorMesh([dh, dh, dh], 'CCC') + + Here, we define the 2D tensor model. + + >>> m = np.zeros(mesh2D.nC) + >>> m[mesh2D.cell_centers[:, 1] < 0] = 10. + >>> m[mesh2D.cell_centers[:, 1] < -5] = 5. + + We then plot the 2D tensor model; which is defined along the + x and z axes. + + >>> fig1 = plt.figure(figsize=(6, 5)) + >>> ax11 = fig1.add_axes([0.1, 0.15, 0.7, 0.8]) + >>> mesh2D.plot_image(m, ax=ax11, grid=True) + >>> ax11.set_ylabel('z') + >>> ax11.set_title('2D Tensor Model') + >>> ax12 = fig1.add_axes([0.83, 0.15, 0.05, 0.8]) + >>> norm1 = mpl.colors.Normalize(vmin=np.min(m), vmax=np.max(m)) + >>> cbar1 = mpl.colorbar.ColorbarBase(ax12, norm=norm1, orientation="vertical") + + By setting *normal = 'Y'* we are projecting along the y-axis. + + >>> mapping = Surject2Dto3D(mesh3D, normal='Y') + >>> u = mapping * m + + Finally we plot a slice of the resulting 3D tensor model. + + >>> fig2 = plt.figure(figsize=(6, 5)) + >>> ax21 = fig2.add_axes([0.1, 0.15, 0.7, 0.8]) + >>> mesh3D.plot_slice(u, ax=ax21, ind=10, normal='Y', grid=True) + >>> ax21.set_ylabel('z') + >>> ax21.set_title('Projected to 3D Mesh (y=0)') + >>> ax22 = fig2.add_axes([0.83, 0.15, 0.05, 0.8]) + >>> norm2 = mpl.colors.Normalize(vmin=np.min(m), vmax=np.max(m)) + >>> cbar2 = mpl.colorbar.ColorbarBase(ax22, norm=norm2, orientation="vertical") + + """ + + def __init__(self, mesh, normal="y", **kwargs): + self.normal = normal + super().__init__(mesh=mesh, **kwargs) + + @IdentityMap.mesh.setter + def mesh(self, value): + value = validate_type("mesh", value, discretize.TensorMesh, cast=False) + if value.dim != 3: + raise ValueError("Surject2Dto3D Only works for a 3D Mesh") + self._mesh = value + + @property + def normal(self): + """The projection axis. + + Returns + ------- + str + """ + return self._normal + + @normal.setter + def normal(self, value): + self._normal = validate_string("normal", value, ("x", "y", "z")) + + @property + def nP(self): + """Number of model properties. + + The number of cells in the + last dimension of the mesh.""" + if self.normal == "z": + return self.mesh.shape_cells[0] * self.mesh.shape_cells[1] + elif self.normal == "y": + return self.mesh.shape_cells[0] * self.mesh.shape_cells[2] + elif self.normal == "x": + return self.mesh.shape_cells[1] * self.mesh.shape_cells[2] + + def _transform(self, m): + m = mkvc(m) + if self.normal == "z": + return mkvc( + m.reshape(self.mesh.vnC[:2], order="F")[:, :, np.newaxis].repeat( + self.mesh.shape_cells[2], axis=2 + ) + ) + elif self.normal == "y": + return mkvc( + m.reshape(self.mesh.vnC[::2], order="F")[:, np.newaxis, :].repeat( + self.mesh.shape_cells[1], axis=1 + ) + ) + elif self.normal == "x": + return mkvc( + m.reshape(self.mesh.vnC[1:], order="F")[np.newaxis, :, :].repeat( + self.mesh.shape_cells[0], axis=0 + ) + ) + + def deriv(self, m, v=None): + r"""Derivative of the mapping with respect to the model paramters. + + Let :math:`\mathbf{m}` be a set of parameter values for the 2D tensor model + and let :math:`\mathbf{P}` be a projection matrix that maps the 2D tensor model + to the 3D tensor mesh. The forward mapping :math:`\mathbf{u}(\mathbf{m})` + is given by: + + .. math:: + \mathbf{u} = \mathbf{P m}, + + the **deriv** method returns the derivative of :math:`\mathbf{u}` with respect + to the model parameters; i.e.: + + .. math:: + \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = \mathbf{P} + + Note that in this case, **deriv** simply returns the projection matrix. + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. If the + input argument *v* is not ``None``, the method returns the derivative times + the vector *v*. + """ + inds = self * np.arange(self.nP) + nC, nP = self.mesh.nC, self.nP + P = sp.csr_matrix((np.ones(nC), (range(nC), inds)), shape=(nC, nP)) + if v is not None: + return P * v + return P + + +class SurjectUnits(IdentityMap): + r"""Surjective mapping to all mesh cells. + + Let :math:`\mathbf{m}` be a model that contains a physical property value + for *nP* geological units. ``SurjectUnits`` is used to construct a surjective + mapping that projects :math:`\mathbf{m}` to the set of voxel cells defining a mesh. + As a result, the mapping :math:`\mathbf{u(\mathbf{m})}` is defined as + a projection matrix :math:`\mathbf{P}` acting on the model. Thus: + + .. math:: + \mathbf{u}(\mathbf{m}) = \mathbf{Pm} + + + The mapping therefore has dimensions (*mesh.nC*, *nP*). + + Parameters + ---------- + indices : (nP) list of (mesh.nC) numpy.ndarray + Each entry in the :class:`list` is a boolean :class:`numpy.ndarray` of length + *mesh.nC* that assigns the corresponding physical property value to the + appropriate mesh cells. + + Examples + -------- + For this example, we have a model that defines the property values + for two units. Using ``SurjectUnit``, we construct the mapping from + the model to a 1D mesh where the 1st unit's value is assigned to + all cells whose centers are located at *x < 0* and the 2nd unit's value + is assigned to all cells whose centers are located at *x > 0*. + + >>> from simpeg.maps import SurjectUnits + >>> from discretize import TensorMesh + >>> import numpy as np + + >>> nP = 8 + >>> mesh = TensorMesh([np.ones(nP)], 'C') + >>> unit_1_ind = mesh.cell_centers < 0 + + >>> indices_list = [unit_1_ind, ~unit_1_ind] + >>> mapping = SurjectUnits(indices_list, nP=nP) + + >>> m = np.r_[0.01, 0.05] + >>> mapping * m + array([0.01, 0.01, 0.01, 0.01, 0.05, 0.05, 0.05, 0.05]) + + """ + + def __init__(self, indices, **kwargs): + super().__init__(**kwargs) + self.indices = indices + + @property + def indices(self): + """List assigning a given physical property to specific model cells. + + Each entry in the :class:`list` is a boolean :class:`numpy.ndarray` of length + *mesh.nC* that assigns the corresponding physical property value to the + appropriate mesh cells. + + Returns + ------- + (nP) list of (mesh.n_cells) numpy.ndarray + """ + return self._indices + + @indices.setter + def indices(self, values): + values = validate_type("indices", values, list) + mesh = self.mesh + last_shape = None + for i in range(len(values)): + if mesh is not None: + values[i] = validate_active_indices( + "indices", values[i], self.mesh.n_cells + ) + else: + values[i] = validate_ndarray_with_shape( + "indices", values[i], shape=("*",), dtype=int + ) + if last_shape is not None and last_shape != values[i].shape: + raise ValueError("all indicies must have the same shape.") + last_shape = values[i].shape + self._indices = values + + @property + def P(self): + """ + Projection matrix from model parameters to mesh cells. + """ + if getattr(self, "_P", None) is None: + # sparse projection matrix + row = [] + col = [] + val = [] + for ii, ind in enumerate(self.indices): + col += [ii] * ind.sum() + row += np.where(ind)[0].tolist() + val += [1] * ind.sum() + + self._P = sp.csr_matrix( + (val, (row, col)), shape=(len(self.indices[0]), self.nP) + ) + + # self._P = sp.block_diag([P for ii in range(self.nBlock)]) + + return self._P + + def _transform(self, m): + return self.P * m + + @property + def nP(self): + r"""Number of parameters the mapping acts on. + + Returns + ------- + int + Number of parameters that the mapping acts on. + """ + return len(self.indices) + + @property + def shape(self): + """Dimensions of the mapping + + Returns + ------- + tuple + Dimensions of the mapping. Where *nP* is the number of parameters the + mapping acts on and *mesh.nC* is the number of cells the corresponding + mesh, the return is a tuple of the form (*mesh.nC*, *nP*). + """ + # return self.n_block*len(self.indices[0]), self.n_block*len(self.indices) + return (len(self.indices[0]), self.nP) + + def deriv(self, m, v=None): + r"""Derivative of the mapping with respect to the input parameters. + + Let :math:`\mathbf{m}` be a set of model parameters. The surjective mapping + can be defined as a sparse projection matrix :math:`\mathbf{P}`. Therefore + we can define the surjective mapping acting on the model parameters as: + + .. math:: + \mathbf{u} = \mathbf{P m}, + + the **deriv** method returns the derivative of :math:`\mathbf{u}` with respect + to the model parameters; i.e.: + + .. math:: + \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = \mathbf{P} + + Note that in this case, **deriv** simply returns a sparse projection matrix. + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. + If the input argument *v* is not ``None``, the method returns + the derivative times the vector *v*. + """ + + if v is not None: + return self.P * v + return self.P From c37536ce0757ea9dfe38ec470f6ea2d79ba2a297 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Mon, 17 Jun 2024 14:22:56 -0600 Subject: [PATCH 030/194] Pyproject.toml (#1482) Moves simpeg to a pyproject.toml based setup. * as much configuration as possible in `pyproject.toml` * general repository cleanup * ci shell scripts --- .azure-pipelines/matrix.yml | 91 ------- .ci/azure/matrix.yml | 58 ++++ .ci/azure/run_tests_with_coverage.sh | 6 + .ci/azure/setup_env.sh | 33 +++ .ci/environment_test.yml | 48 ++++ .ci/install_style.sh | 12 + .ci/parse_style_requirements.py | 12 + .coveragerc | 4 - .flake8 | 125 --------- .git_archival.txt | 4 + .gitattributes | 1 + .github/workflows/pull_request.yml | 4 +- .gitignore | 1 + .mailmap | 60 ++++- .pre-commit-config.yaml | 7 +- MANIFEST.in | 5 +- Makefile | 18 +- azure-pipelines.yml | 143 +++------- docs/conf.py | 8 +- .../contributing/code-style.rst | 2 +- .../contributing/setting-up-environment.rst | 19 +- environment_test.yml => environment.yml | 48 ++-- pyproject.toml | 251 ++++++++++++++++++ requirements.txt | 1 - requirements_dask.txt | 3 - requirements_dev.txt | 27 -- requirements_style.txt | 7 - setup.py | 63 ----- simpeg/data.py | 2 +- .../frequency_domain/simulation.py | 2 +- .../natural_source/simulation_1d.py | 2 +- .../static/induced_polarization/simulation.py | 2 +- .../static/self_potential/simulation.py | 6 +- .../simulation.py | 2 +- .../spectral_induced_polarization/survey.py | 2 +- simpeg/flow/richards/empirical.py | 12 +- simpeg/flow/richards/simulation.py | 2 +- simpeg/inversion.py | 2 +- simpeg/meta/dask_sim.py | 2 +- simpeg/regularization/__init__.py | 4 +- simpeg/utils/plot_utils.py | 4 +- tests/base/test_Props.py | 2 +- tests/pf/test_forward_PFproblem.py | 2 +- 43 files changed, 590 insertions(+), 519 deletions(-) delete mode 100644 .azure-pipelines/matrix.yml create mode 100644 .ci/azure/matrix.yml create mode 100755 .ci/azure/run_tests_with_coverage.sh create mode 100755 .ci/azure/setup_env.sh create mode 100644 .ci/environment_test.yml create mode 100755 .ci/install_style.sh create mode 100644 .ci/parse_style_requirements.py delete mode 100644 .coveragerc delete mode 100644 .flake8 create mode 100644 .git_archival.txt create mode 100644 .gitattributes rename environment_test.yml => environment.yml (63%) create mode 100644 pyproject.toml delete mode 100644 requirements.txt delete mode 100644 requirements_dask.txt delete mode 100644 requirements_dev.txt delete mode 100644 requirements_style.txt delete mode 100644 setup.py diff --git a/.azure-pipelines/matrix.yml b/.azure-pipelines/matrix.yml deleted file mode 100644 index 4269e7bebe..0000000000 --- a/.azure-pipelines/matrix.yml +++ /dev/null @@ -1,91 +0,0 @@ -parameters: - os : ['ubuntu-latest'] - py_vers: ['3.8'] - test: ['tests/em', - 'tests/base tests/flow tests/seis tests/utils tests/meta', - 'tests/docs -s -v', - 'tests/examples/test_examples_1.py', - 'tests/examples/test_examples_2.py', - 'tests/examples/test_examples_3.py', - 'tests/examples/test_tutorials_1.py tests/examples/test_tutorials_2.py', - 'tests/examples/test_tutorials_3.py', - 'tests/pf', - 'tests/dask', # This must be ran on it's own to avoid modifying the code from any other tests. - ] - -jobs: - - ${{ each os in parameters.os }}: - - ${{ each py_vers in parameters.py_vers }}: - - ${{ each test in parameters.test }}: - - job: - displayName: ${{ os }}_${{ py_vers }}_${{ test }} - pool: - vmImage: ${{ os }} - timeoutInMinutes: 120 - steps: - - # Checkout simpeg repo, including tags. - # We need to sync tags and disable shallow depth in order to get the - # SimPEG version while building the docs. - - checkout: self - fetchDepth: 0 - fetchTags: true - displayName: Checkout repository (including tags) - - - script: | - wget -O Mambaforge.sh "https://github.com/conda-forge/miniforge/releases/latest/download/Mambaforge-$(uname)-$(uname -m).sh" - bash Mambaforge.sh -b -p "${HOME}/conda" - displayName: Install mamba - - - script: | - source "${HOME}/conda/etc/profile.d/conda.sh" - source "${HOME}/conda/etc/profile.d/mamba.sh" - cp environment_test.yml environment_test_with_pyversion.yml - echo " - python="${{ py_vers }} >> environment_test_with_pyversion.yml - mamba env create -f environment_test_with_pyversion.yml - rm environment_test_with_pyversion.yml - conda activate simpeg-test - pip install pytest-azurepipelines - echo "\nList installed packages" - conda list - displayName: Create Anaconda testing environment - - - script: | - source "${HOME}/conda/etc/profile.d/conda.sh" - conda activate simpeg-test - pip install -e . - displayName: Build package - - - script: | - source "${HOME}/conda/etc/profile.d/conda.sh" - conda activate simpeg-test - python -c "import simpeg; print(simpeg.__version__)" - displayName: Check SimPEG version - - - script: | - source "${HOME}/conda/etc/profile.d/conda.sh" - conda activate simpeg-test - export KMP_WARNINGS=0 - pytest ${{ test }} -v --cov-config=.coveragerc --cov=simpeg --cov-report=xml --cov-report=html -W ignore::DeprecationWarning - displayName: 'Testing ${{ test }}' - - - task: PublishPipelineArtifact@1 - inputs: - targetPath: $(Build.SourcesDirectory)/docs/_build/html - artifactName: html_docs - displayName: 'Publish documentation artifact' - condition: eq('${{ test }}', 'tests/docs -s -v') - - - script: | - curl -Os https://uploader.codecov.io/latest/linux/codecov - chmod +x codecov - ./codecov --sha $(system.pullRequest.sourceCommitId) - displayName: 'Upload PR coverage to codecov.io' - condition: and(eq(${{ py_vers }}, '3.8'), startsWith(variables['build.sourceBranch'], 'refs/pull/')) - - - script: | - curl -Os https://uploader.codecov.io/latest/linux/codecov - chmod +x codecov - ./codecov - displayName: 'Upload coverage to codecov.io' - condition: and(eq(${{ py_vers }}, '3.8'), not(startsWith(variables['build.sourceBranch'], 'refs/pull/'))) diff --git a/.ci/azure/matrix.yml b/.ci/azure/matrix.yml new file mode 100644 index 0000000000..5f7a3901cc --- /dev/null +++ b/.ci/azure/matrix.yml @@ -0,0 +1,58 @@ +parameters: + os : ['ubuntu-latest'] + py_vers: ['3.8'] + test: ['tests/em', + 'tests/base tests/flow tests/seis tests/utils tests/meta', + 'tests/docs -s -v', + 'tests/examples/test_examples_1.py', + 'tests/examples/test_examples_2.py', + 'tests/examples/test_examples_3.py', + 'tests/examples/test_tutorials_1.py tests/examples/test_tutorials_2.py', + 'tests/examples/test_tutorials_3.py', + 'tests/pf', + 'tests/dask', # This must be ran on it's own to avoid modifying the code from any other tests. + ] + +jobs: + - ${{ each os in parameters.os }}: + - ${{ each py_vers in parameters.py_vers }}: + - ${{ each test in parameters.test }}: + - job: + displayName: ${{ os }}_${{ py_vers }}_${{ test }} + pool: + vmImage: ${{ os }} + timeoutInMinutes: 120 + variables: + python.version: ${{ py_vers }} + test.target: ${{ test }} + steps: + + # Checkout simpeg repo, including tags. + # We need to sync tags and disable shallow depth in order to get the + # SimPEG version while building the docs. + - checkout: self + fetchDepth: 0 + fetchTags: true + displayName: Checkout repository (including tags) + + - bash: echo "##vso[task.prependpath]$CONDA/bin" + displayName: Add conda to PATH + + - bash: .ci/azure/setup_env.sh + displayName: Setup SimPEG environment + + - bash: .ci/azure/run_tests_with_coverage.sh + displayName: 'Testing ${{ test }}' + + - task: PublishPipelineArtifact@1 + inputs: + targetPath: $(Build.SourcesDirectory)/docs/_build/html + artifactName: html_docs + displayName: 'Publish documentation artifact' + condition: eq('${{ test }}', 'tests/docs -s -v') + + - script: | + curl -Os https://uploader.codecov.io/latest/linux/codecov + chmod +x codecov + ./codecov + displayName: 'Upload coverage to codecov.io' diff --git a/.ci/azure/run_tests_with_coverage.sh b/.ci/azure/run_tests_with_coverage.sh new file mode 100755 index 0000000000..def90a983c --- /dev/null +++ b/.ci/azure/run_tests_with_coverage.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -ex #echo on and exit if any line fails + +source activate simpeg-test +pytest $TEST_TARGET --cov --cov-config=pyproject.toml -v -W ignore::DeprecationWarning +coverage xml \ No newline at end of file diff --git a/.ci/azure/setup_env.sh b/.ci/azure/setup_env.sh new file mode 100755 index 0000000000..8d873a601d --- /dev/null +++ b/.ci/azure/setup_env.sh @@ -0,0 +1,33 @@ +#!/bin/bash +set -ex #echo on and exit if any line fails + +# TF_BUILD is set to True on azure pipelines. +is_azure=$(${TF_BUILD:-false} | tr '[:upper:]' '[:lower:]') + +if ${is_azure} +then + conda update --yes -n base conda +fi + +cp .ci/environment_test.yml environment_test_with_pyversion.yml +echo " - python="$PYTHON_VERSION >> environment_test_with_pyversion.yml + +conda env create --file environment_test_with_pyversion.yml +rm environment_test_with_pyversion.yml + + +if ${is_azure} +then + source activate simpeg-test + pip install pytest-azurepipelines +else + conda activate simpeg-test +fi + +pip install --no-deps --editable . + +echo "Conda Environment:" +conda list + +echo "Installed SimPEG version:" +python -c "import simpeg; print(simpeg.__version__)" \ No newline at end of file diff --git a/.ci/environment_test.yml b/.ci/environment_test.yml new file mode 100644 index 0000000000..5003045093 --- /dev/null +++ b/.ci/environment_test.yml @@ -0,0 +1,48 @@ +name: simpeg-test +channels: + - conda-forge +dependencies: + - numpy>=1.20 + - scipy>=1.8 + - scikit-learn>=1.2 + - pymatsolver-base>=0.2 + - matplotlib-base + - discretize>=0.10 + - geoana>=0.5.0 + - empymod>=2.0.0 + - pandas + +# Solver + - pydiso + +# optional dependencies + - dask + - zarr + - fsspec>=0.3.3 + - choclo + - scooby + - plotly + +# documentation building + - sphinx + - sphinx-gallery>=0.1.13 + - sphinxcontrib-apidoc + - pydata-sphinx-theme + - nbsphinx + - numpydoc + - pillow + - sympy + - memory_profiler + - python-kaleido + - h5py + +# testing + - pytest + - pytest-cov + +# Testing environment doesn't need style checkers + +# PyPI uploading + - wheel + - twine + - build diff --git a/.ci/install_style.sh b/.ci/install_style.sh new file mode 100755 index 0000000000..b26211ad95 --- /dev/null +++ b/.ci/install_style.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -ex #echo on and exit if any line fails + +# get directory of this script +script_dir=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +style_script=$script_dir/parse_style_requirements.py + +# parse the style requirements +requirements=$(python $style_script) + +pip install $requirements + diff --git a/.ci/parse_style_requirements.py b/.ci/parse_style_requirements.py new file mode 100644 index 0000000000..8183280b78 --- /dev/null +++ b/.ci/parse_style_requirements.py @@ -0,0 +1,12 @@ +import tomllib +import pathlib + +root_dir = pathlib.Path(__file__).parent.parent.resolve() +pyproject_file = root_dir / "pyproject.toml" + +with open(pyproject_file, "rb") as f: + pyproject = tomllib.load(f) + +style_requirements = pyproject["project"]["optional-dependencies"]["style"] +for req in style_requirements: + print(req) diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index a79c8987f3..0000000000 --- a/.coveragerc +++ /dev/null @@ -1,4 +0,0 @@ -[run] -source = simpeg -omit = - */setup.py diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 14d1e5f416..0000000000 --- a/.flake8 +++ /dev/null @@ -1,125 +0,0 @@ -# ----------------------------- -# Configuration file for flake8 -# ----------------------------- - -# Configure flake8 -# ---------------- -[flake8] -extend-ignore = - # Default ignores by flake (added here for when ignore gets overwritten) - E121,E123,E126,E226,E24,E704,W503,W504, - # Too many leading '#' for block comment - E266, - # Line too long (82 > 79 characters) - E501, - # Do not use variables named 'I', 'O', or 'l' - E741, - # Line break before binary operator (conflicts with black) - W503, - # Ignore spaces before a colon (Black handles it) - E203, -exclude = - .git, - __pycache__, - .ipynb_checkpoints, - setup.py, - docs/conf.py, - docs/_build/, -per-file-ignores = - # disable unused-imports errors on __init__.py - __init__.py: F401 -exclude-from-doctest = - # Don't check style in docstring of test functions - tests -# Define flake rules that will be ignored for now. Every time a new warning is -# solved througout the entire project, it should be removed to this list. -ignore = - # assertRaises(Exception): should be considered evil - B017, - # Missing docstring in public module - D100, - # Missing docstring in public class - D101, - # Missing docstring in public method - D102, - # Missing docstring in public function - D103, - # Missing docstring in public package - D104, - # Missing docstring in magic method - D105, - # Missing docstring in __init__ - D107, - # One-line docstring should fit on one line with quotes - D200, - # No blank lines allowed before function docstring - D201, - # No blank lines allowed after function docstring - D202, - # 1 blank line required between summary line and description - D205, - # Docstring is over-indented - D208, - # Multi-line docstring closing quotes should be on a separate line - D209, - # No whitespaces allowed surrounding docstring text - D210, - # No blank lines allowed before class docstring - D211, - # Use """triple double quotes""" - D300, - # First line should end with a period - D400, - # First line should be in imperative mood; try rephrasing - D401, - # First line should not be the function's "signature" - D402, - # First word of the first line should be properly capitalized - D403, - # No blank lines allowed between a section header and its content - D412, - # Section has no content - D414, - # Docstring is empty - D419, - # module level import not at top of file - E402, - # undefined name %r - F821, - # Block quote ends without a blank line; unexpected unindent. - RST201, - # Definition list ends without a blank line; unexpected unindent. - RST203, - # Field list ends without a blank line; unexpected unindent. - RST206, - # Inline strong start-string without end-string. - RST210, - # Title underline too short. - RST212, - # Inline emphasis start-string without end-string. - RST213, - # Inline interpreted text or phrase reference start-string without end-string. - RST215, - # Inline substitution_reference start-string without end-string. - RST219, - # Unexpected indentation. - RST301, - # Unknown directive type "*". - RST303, - # Unknown interpreted text role "*". - RST304, - # Error in "*" directive: - RST307, - # Previously unseen severe error, not yet assigned a unique code. - RST499, - - -# Configure flake8-rst-docstrings -# ------------------------------- -# Add some roles used in our docstrings -rst-roles = - class, - func, - mod, - meth, - ref, diff --git a/.git_archival.txt b/.git_archival.txt new file mode 100644 index 0000000000..b1a286bbb6 --- /dev/null +++ b/.git_archival.txt @@ -0,0 +1,4 @@ +node: $Format:%H$ +node-date: $Format:%cI$ +describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$ +ref-names: $Format:%D$ \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..82bf71c1c5 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +.git_archival.txt export-subst \ No newline at end of file diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 0151372c8d..d6af2dcbf8 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -15,7 +15,7 @@ jobs: python-version: '3.11' - name: Install dependencies to run the flake8 checks - run: pip install -r requirements_style.txt + run: .ci/install_style.sh - name: checkout pull request source uses: actions/checkout@v4 @@ -43,7 +43,7 @@ jobs: python-version: '3.11' - name: Install dependencies to run the black checks - run: pip install -r requirements_style.txt + run: .ci/install_style.sh - name: checkout pull request source uses: actions/checkout@v4 diff --git a/.gitignore b/.gitignore index 5609580098..53545d898d 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ pip-log.txt # Unit test / coverage reports .coverage +coverage_html_report/* .tox nosetests.xml diff --git a/.mailmap b/.mailmap index 26757e1035..7bdb3acc4f 100644 --- a/.mailmap +++ b/.mailmap @@ -23,25 +23,81 @@ Seogi Kang Seogi Kang Dom Fournier +Dom Fournier domfournier Gudni Karl Rosenkjaer Gudni Karl Rosenkjaer Gudni Karl Rosenkjaer -Thibaut Astic -Thibaut Astic + +Thibaut Astic +Thibaut Astic +Thibaut Astic +Thibaut Astic +Thibaut Astic <97514898+thibaut-kobold@users.noreply.github.com> +Thibaut Astic Devin Cowan Dave Marchant Michael Mitchell +Michael Mitchell Lars Ruthotto Dieter Werthmüller Dieter Werthmüller +Dieter Werthmüller Eldad Haber Brendan Smithyman + +Xiaolong Wei <56940558+xiaolongw1223@users.noreply.github.com> + +Ying Hu <64567062+YingHuuu@users.noreply.github.com> + +Richard Scott <72196131+RichardScottOZ@users.noreply.github.com> + +Nick Williams <86257151+nwilliams-kobold@users.noreply.github.com> + +Fernando Perez + +Adam Kosík + +Andrea Balza Morales + +Craig Miller + +Colton Kohnke +Colton Kohnke + +Franklin Koch + +Jacob Edman + +Joseph Capriotti +Joseph Capriotti + +John Kuttai + +John Weiss +John Weiss <49649694+johnweis0480@users.noreply.github.com> + +Kalen Martens + +Luz Angelica Caudillo Mata + +Neil Godber +Neil Godber + +Santiago Soler + +Sarah Devriese + +Zheng-Kai Ye + +Zhuo Liu <20036557+Zhuoliulz@users.noreply.github.com> + +Mike Wathen \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ccb608b94b..52c3fc8c2d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,17 +1,18 @@ repos: - - repo: https://github.com/psf/black + - repo: https://github.com/psf/black-pre-commit-mirror rev: 24.3.0 hooks: - id: black - language_version: python3 + language_version: python3.11 - repo: https://github.com/pycqa/flake8 rev: 7.0.0 hooks: - id: flake8 - language_version: python3 + language_version: python3.11 additional_dependencies: - flake8-bugbear==23.12.2 - flake8-builtins==2.2.0 - flake8-mutable==1.2.0 - flake8-rst-docstrings==0.3.0 - flake8-docstrings==1.7.0 + - flake8-pyproject==1.2.3 diff --git a/MANIFEST.in b/MANIFEST.in index 7c04fa62b2..b61f50d054 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,10 +1,9 @@ prune .github -prune .azure-pipelines +prune .ci prune docs prune examples prune tutorials prune tests exclude .coveragerc .flake8 .gitignore MANIFEST.in .pre-commit-config.yaml exclude azure-pipelines.yml .mailmap -exclude environment_test.yml -exclude requirements.txt requirements_dev.txt requirements_dask.txt requirements_style.txt \ No newline at end of file +exclude .git_archival.txt .gitattributes Makefile environment.yml \ No newline at end of file diff --git a/Makefile b/Makefile index 20c18b456c..e367bad141 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ STYLE_CHECK_FILES = simpeg examples tutorials tests -.PHONY: help build coverage lint graphs tests docs check black flake +.PHONY: help docs check black flake help: @echo "Commands:" @@ -11,22 +11,6 @@ help: @echo " flake-all checks code style with flake8 (full set of rules)" @echo "" -build: - python setup.py build_ext --inplace - -coverage: - nosetests --logging-level=INFO --with-coverage --cover-package=simpeg --cover-html - open cover/index.html - -lint: - pylint --output-format=html simpeg> pylint.html - -graphs: - pyreverse -my -A -o pdf -p simpeg simpeg/**.py simpeg/**/**.py - -tests: - nosetests --logging-level=INFO - docs: cd docs;make html diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 7866c36069..233a15a0e5 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -33,11 +33,11 @@ stages: displayName: Run style checks with Black pool: vmImage: ubuntu-latest - variables: - python.version: '3.8' steps: - - script: | - pip install -r requirements_style.txt + - task: UsePythonVersion@0 + inputs: + versionSpec: "3.11" + - bash: .ci/install_style.sh displayName: "Install dependencies to run the checks" - script: make black displayName: "Run black" @@ -46,11 +46,11 @@ stages: displayName: Run (permissive) style checks with flake8 pool: vmImage: ubuntu-latest - variables: - python.version: '3.8' steps: - - script: | - pip install -r requirements_style.txt + - task: UsePythonVersion@0 + inputs: + versionSpec: "3.11" + - bash: .ci/install_style.sh displayName: "Install dependencies to run the checks" - script: make flake displayName: "Run flake8" @@ -59,19 +59,21 @@ stages: displayName: Run style checks with flake8 (allowed to fail) pool: vmImage: ubuntu-latest - variables: - python.version: '3.8' steps: - - script: | - pip install -r requirements_style.txt + - task: UsePythonVersion@0 + inputs: + versionSpec: "3.11" + - bash: .ci/install_style.sh displayName: "Install dependencies to run the checks" - - script: FLAKE8_OPTS="--exit-zero" make flake-all + - script: make flake-all displayName: "Run flake8" + env: + FLAKE8_OPTS: "--exit-zero" - stage: Testing dependsOn: StyleChecks jobs: - - template: ./.azure-pipelines/matrix.yml + - template: ./.ci/azure/matrix.yml - stage: Deploy dependsOn: Testing @@ -103,40 +105,15 @@ stages: GH_NAME: $(gh.name) GH_EMAIL: $(gh.email) - - script: | - wget -O Mambaforge.sh "https://github.com/conda-forge/miniforge/releases/latest/download/Mambaforge-$(uname)-$(uname -m).sh" - bash Mambaforge.sh -b -p "${HOME}/conda" - displayName: Install mamba - - - script: | - source "${HOME}/conda/etc/profile.d/conda.sh" - source "${HOME}/conda/etc/profile.d/mamba.sh" - cp environment_test.yml environment_test_with_pyversion.yml - echo " - python="$(python.version) >> environment_test_with_pyversion.yml - mamba env create -f environment_test_with_pyversion.yml - rm environment_test_with_pyversion.yml - conda activate simpeg-test - pip install pytest-azurepipelines - echo "\nList installed packages" - conda list - displayName: Create Anaconda testing environment - - - script: | - source "${HOME}/conda/etc/profile.d/conda.sh" - conda activate simpeg-test - pip install -e . - displayName: Build package + - bash: echo "##vso[task.prependpath]$CONDA/bin" + displayName: Add conda to PATH - - script: | - source "${HOME}/conda/etc/profile.d/conda.sh" - conda activate simpeg-test - python -c "import simpeg; print(simpeg.__version__)" - displayName: Check SimPEG version + - bash: .ci/azure/setup_env.sh + displayName: Setup SimPEG environment - script: | - source "${HOME}/conda/etc/profile.d/conda.sh" - conda activate simpeg-test - python setup.py sdist + source activate simpeg-test + python -m build --no-isolation --skip-dependency-check --sdist . twine upload --skip-existing dist/* displayName: Deploy source env: @@ -144,9 +121,7 @@ stages: TWINE_PASSWORD: $(twine.password) - script: | - source "${HOME}/conda/etc/profile.d/conda.sh" - conda activate simpeg-test - export KMP_WARNINGS=0 + source activate simpeg-test cd docs make html cd .. @@ -200,40 +175,14 @@ stages: GH_NAME: $(gh.name) GH_EMAIL: $(gh.email) - - bash: | - wget -O Mambaforge.sh "https://github.com/conda-forge/miniforge/releases/latest/download/Mambaforge-$(uname)-$(uname -m).sh" - bash Mambaforge.sh -b -p "${HOME}/conda" - displayName: Install mamba - - - bash: | - source "${HOME}/conda/etc/profile.d/conda.sh" - source "${HOME}/conda/etc/profile.d/mamba.sh" - cp environment_test.yml environment_test_with_pyversion.yml - echo " - python="$(python.version) >> environment_test_with_pyversion.yml - mamba env create -f environment_test_with_pyversion.yml - rm environment_test_with_pyversion.yml - conda activate simpeg-test - pip install pytest-azurepipelines - echo "\nList installed packages" - conda list - displayName: Create Anaconda testing environment - - - bash: | - source "${HOME}/conda/etc/profile.d/conda.sh" - conda activate simpeg-test - pip install -e . - displayName: Build package + - bash: echo "##vso[task.prependpath]$CONDA/bin" + displayName: Add conda to PATH - - script: | - source "${HOME}/conda/etc/profile.d/conda.sh" - conda activate simpeg-test - python -c "import simpeg; print(simpeg.__version__)" - displayName: Check SimPEG version + - bash: .ci/azure/setup_env.sh + displayName: Setup SimPEG environment - bash: | - source "${HOME}/conda/etc/profile.d/conda.sh" - conda activate simpeg-test - export KMP_WARNINGS=0 + source activate simpeg-test make -C docs html displayName: Building documentation @@ -321,40 +270,14 @@ stages: GH_NAME: $(gh.name) GH_EMAIL: $(gh.email) - - bash: | - wget -O Mambaforge.sh "https://github.com/conda-forge/miniforge/releases/latest/download/Mambaforge-$(uname)-$(uname -m).sh" - bash Mambaforge.sh -b -p "${HOME}/conda" - displayName: Install mamba - - - bash: | - source "${HOME}/conda/etc/profile.d/conda.sh" - source "${HOME}/conda/etc/profile.d/mamba.sh" - cp environment_test.yml environment_test_with_pyversion.yml - echo " - python="$(python.version) >> environment_test_with_pyversion.yml - mamba env create -f environment_test_with_pyversion.yml - rm environment_test_with_pyversion.yml - conda activate simpeg-test - pip install pytest-azurepipelines - echo "\nList installed packages" - conda list - displayName: Create Anaconda testing environment + - bash: echo "##vso[task.prependpath]$CONDA/bin" + displayName: Add conda to PATH - - bash: | - source "${HOME}/conda/etc/profile.d/conda.sh" - conda activate simpeg-test - pip install -e . - displayName: Build package - - - script: | - source "${HOME}/conda/etc/profile.d/conda.sh" - conda activate simpeg-test - python -c "import simpeg; print(simpeg.__version__)" - displayName: Check SimPEG version + - bash: .ci/azure/setup_env.sh + displayName: Setup SimPEG environment - bash: | - source "${HOME}/conda/etc/profile.d/conda.sh" - conda activate simpeg-test - export KMP_WARNINGS=0 + source activate simpeg-test make -C docs html displayName: Building documentation diff --git a/docs/conf.py b/docs/conf.py index a5456fd45b..8a50f66cd8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -467,14 +467,8 @@ def linkcode_resolve(domain, info): # Scaping images to generate on website from plotly.io._sg_scraper import plotly_sg_scraper -import pyvista -# Make sure off screen is set to true when building locally -pyvista.OFF_SCREEN = True -# necessary when building the sphinx gallery -pyvista.BUILDING_GALLERY = True - -image_scrapers = ("matplotlib", plotly_sg_scraper, pyvista.Scraper()) +image_scrapers = ("matplotlib", plotly_sg_scraper) # Sphinx Gallery sphinx_gallery_conf = { diff --git a/docs/content/getting_started/contributing/code-style.rst b/docs/content/getting_started/contributing/code-style.rst index 75f235d107..3202d48ac2 100644 --- a/docs/content/getting_started/contributing/code-style.rst +++ b/docs/content/getting_started/contributing/code-style.rst @@ -20,7 +20,7 @@ Run ``black`` on SimPEG directories that contain Python source files: .. code:: - black simpeg examples tutorials tests + black . Run ``flake8`` on the whole project with: diff --git a/docs/content/getting_started/contributing/setting-up-environment.rst b/docs/content/getting_started/contributing/setting-up-environment.rst index d775983dff..d3d670fdc9 100644 --- a/docs/content/getting_started/contributing/setting-up-environment.rst +++ b/docs/content/getting_started/contributing/setting-up-environment.rst @@ -14,11 +14,10 @@ Create environment ------------------ To get started developing SimPEG we recommend setting up an environment using -the ``conda`` package manager that mimics the testing -environment used for continuous integration testing. Most of the packages that -we use are available through the ``conda-forge`` project. This will ensure you -have all of the necessary packages to both develop SimPEG and run tests -locally. We provide an ``environment_test.yml`` in the base level directory. +the ``conda`` package manager that includes all odthe packages necessary to +both develop SimPEG and run tests locally. Most of the packages that +we use are available through the ``conda-forge`` project. +We provide an ``environment.yml`` in the base level directory. To create the environment and install all packages needed to run and write code for SimPEG, navigate to the directory where you :ref:`cloned SimPEG's @@ -26,7 +25,7 @@ repository ` and run: .. code:: - conda env create -f environment_test.yml + conda env create -f environment.yml .. note:: @@ -48,7 +47,7 @@ Once the environment is successfully created, you can *activate* it with .. code:: - conda activate simpeg-test + conda activate simpeg-dev Install SimPEG in developer mode @@ -56,7 +55,7 @@ Install SimPEG in developer mode There are many options to install SimPEG into this local environment, we recommend using `pip`. After ensuring that all necessary packages from -`environment_test.yml` are installed, the most robust command you can use, +`environment.yml` are installed, the most robust command you can use, executed from the base level directory would be: .. code:: @@ -153,7 +152,7 @@ Update your environment Every once in a while, the minimum versions of the packages in the ``environment.yml`` file get updated. After this happens, it's better to update -the ``simpeg-test`` environment we have created. This way we ensure that we are +the ``simpeg-dev`` environment we have created. This way we ensure that we are checking the style and testing our code using those updated versions. To update our environment we need to navigate to the directory where you @@ -161,4 +160,4 @@ To update our environment we need to navigate to the directory where you .. code:: - conda env update -f environment_test.yml + conda env update -f environment.yml diff --git a/environment_test.yml b/environment.yml similarity index 63% rename from environment_test.yml rename to environment.yml index 84940f92d6..709a40813c 100644 --- a/environment_test.yml +++ b/environment.yml @@ -1,22 +1,32 @@ -name: simpeg-test +name: simpeg-dev channels: - conda-forge dependencies: +# dependencies + - python=3.11 - numpy>=1.20 - scipy>=1.8 - scikit-learn>=1.2 - - pymatsolver>=0.2 - - matplotlib + - pymatsolver-base>=0.2 + - matplotlib-base - discretize>=0.10 - geoana>=0.5.0 - empymod>=2.0.0 - - setuptools_scm - pandas + +# solver +# uncomment the next line if you are on an intel platform +# - pydiso # if on intel pc + +# optional dependencies - dask - zarr - - fsspec - - jupyter - - h5py + - fsspec>=0.3.3 + - choclo + - scooby + - plotly + +# documentation building - sphinx - sphinx-gallery>=0.1.13 - sphinxcontrib-apidoc @@ -24,26 +34,26 @@ dependencies: - nbsphinx - numpydoc - pillow - - pylint - sympy - - wheel - - pytest - - pytest-cov - - toolz - - twine - memory_profiler - - plotly - - pyvista - - pip - python-kaleido - # Optional dependencies - - choclo - # Linters and code style + - h5py + +# testing + - pytest + - pytest-cov + +# style checking - pre-commit - black==24.3.0 - flake8==7.0.0 + - flake8-pyproject==1.2.3 - flake8-bugbear==23.12.2 - flake8-builtins==2.2.0 - flake8-mutable==1.2.0 - flake8-rst-docstrings==0.3.0 - flake8-docstrings==1.7.0 + +# recommended + - jupyter + - pyvista \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..e63ea662d9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,251 @@ +[build-system] +requires = ["setuptools>=64", "setuptools_scm>=8"] +build-backend = "setuptools.build_meta" + + +[project] +name = 'simpeg' +description = "SimPEG: Simulation and Parameter Estimation in Geophysics" +readme = 'README.rst' +requires-python = '>=3.8' +authors = [ + {name = 'SimPEG developers', email = 'rowanc1@gmail.com'}, +] +keywords = [ + 'geophysics', 'inverse problem' +] +dependencies = [ + "numpy>=1.20", + "scipy>=1.8", + "scikit-learn>=1.2", + "pymatsolver>=0.2", + "matplotlib", + "discretize>=0.10", + "geoana>=0.5.0", + "empymod>=2.0.0", + "pandas", +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Topic :: Scientific/Engineering", + "Topic :: Scientific/Engineering :: Mathematics", + "Topic :: Scientific/Engineering :: Physics", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX", + "Operating System :: Unix", + "Operating System :: MacOS", + "Natural Language :: English", +] +dynamic = ["version"] + +[project.license] +file = 'LICENSE' + +[project.urls] +Homepage = 'https://simpeg.xyz' +Documentation = 'https://docs.simpeg.xyz' +Repository = 'http://github.com/simpeg/simpeg.git' + +[project.optional-dependencies] +dask = ["dask", "zarr", "fsspec>=0.3.3"] +choclo = ["choclo"] +reporting = ["scooby"] +plotting = ["plotly"] +all = [ + "simpeg[dask,choclo,plotting,reporting]" +] # all optional *runtime* dependencies (not related to development) +style = [ + "black==24.3.0", + "flake8==7.0.0", + "flake8-bugbear==23.12.2", + "flake8-builtins==2.2.0", + "flake8-mutable==1.2.0", + "flake8-rst-docstrings==0.3.0", + "flake8-docstrings==1.7.0", + "flake8-pyproject==1.2.3", +] +docs = [ + "sphinx", + "sphinx-gallery>=0.1.13", + "sphinxcontrib-apidoc", + "pydata-sphinx-theme", + "nbsphinx", + "numpydoc", + "pillow", + "sympy", + "memory_profiler", + "python-kaleido", +] +tests = [ + "simpeg[all,docs]", + "pytest", + "pytest-cov", +] +dev = [ + "simpeg[all,style,docs,tests]", +] # the whole kit and caboodle + +[tool.setuptools] +py-modules = ['SimPEG'] + +[tool.setuptools.packages.find] +include = ["simpeg*"] + +[tool.setuptools_scm] +version_file = "simpeg/version.py" + +[tool.coverage.run] +branch = true +source = ["simpeg", "tests", "examples", "tutorials"] + +[tool.coverage.report] +ignore_errors = false +show_missing = true +# Regexes for lines to exclude from consideration +exclude_also = [ + # Don't complain about missing debug-only code: + "def __repr__", + "if self\\.debug", + + # Don't complain if tests don't hit defensive assertion code: + "raise AssertionError", + "raise NotImplementedError", + "AbstractMethodError", + + # Don't complain if non-runnable code isn't run: + "if 0:", + "if __name__ == .__main__.:", + + # Don't complain about abstract methods, they aren't run: + "@(abc\\.)?abstractmethod", +] + +[tool.coverage.html] +directory = "coverage_html_report" + +[tool.black] +required-version = '24.3.0' +target-version = ['py38', 'py39', 'py310', 'py311'] + +[tool.flake8] +extend-ignore = [ + # Default ignores by flake (added here for when ignore gets overwritten) + 'E121','E123','E126','E226','E24','E704','W503','W504', + # Too many leading '#' for block comment + 'E266', + # Line too long (82 > 79 characters) + 'E501', + # Do not use variables named 'I', 'O', or 'l' + 'E741', + # Line break before binary operator (conflicts with black) + 'W503', + # Ignore spaces before a colon (Black handles it) + 'E203', +] +exclude = [ + '.git', + '__pycache__', + '.ipynb_checkpoints', + 'docs/conf.py', + 'docs/_build/', +] +per-file-ignores = [ + # disable unused-imports errors on __init__.py + '__init__.py:F401', +] +exclude-from-doctest = [ + # Don't check style in docstring of test functions + 'tests', +] +ignore = [ + # assertRaises(Exception): should be considered evil + 'B017', + # Missing docstring in public module + 'D100', + # Missing docstring in public class + 'D101', + # Missing docstring in public method + 'D102', + # Missing docstring in public function + 'D103', + # Missing docstring in public package + 'D104', + # Missing docstring in magic method + 'D105', + # Missing docstring in __init__ + 'D107', + # One-line docstring should fit on one line with quotes + 'D200', + # No blank lines allowed before function docstring + 'D201', + # No blank lines allowed after function docstring + 'D202', + # 1 blank line required between summary line and description + 'D205', + # Docstring is over-indented + 'D208', + # Multi-line docstring closing quotes should be on a separate line + 'D209', + # No whitespaces allowed surrounding docstring text + 'D210', + # No blank lines allowed before class docstring + 'D211', + # Use """triple double quotes""" + 'D300', + # First line should end with a period + 'D400', + # First line should be in imperative mood; try rephrasing + 'D401', + # First line should not be the function's "signature" + 'D402', + # First word of the first line should be properly capitalized + 'D403', + # No blank lines allowed between a section header and its content + 'D412', + # Section has no content + 'D414', + # Docstring is empty + 'D419', + # module level import not at top of file + 'E402', + # undefined name %r + 'F821', + # Block quote ends without a blank line; unexpected unindent. + 'RST201', + # Definition list ends without a blank line; unexpected unindent. + 'RST203', + # Field list ends without a blank line; unexpected unindent. + 'RST206', + # Inline strong start-string without end-string. + 'RST210', + # Title underline too short. + 'RST212', + # Inline emphasis start-string without end-string. + 'RST213', + # Inline interpreted text or phrase reference start-string without end-string. + 'RST215', + # Inline substitution_reference start-string without end-string. + 'RST219', + # Unexpected indentation. + 'RST301', + # Unknown directive type "*". + 'RST303', + # Unknown interpreted text role "*". + 'RST304', + # Error in "*" directive: + 'RST307', + # Previously unseen severe error, not yet assigned a unique code. + 'RST499', +] + +rst-roles = [ + 'class', + 'func', + 'mod', + 'meth', + 'ref', +] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index d6e1198b1a..0000000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ --e . diff --git a/requirements_dask.txt b/requirements_dask.txt deleted file mode 100644 index 557aece885..0000000000 --- a/requirements_dask.txt +++ /dev/null @@ -1,3 +0,0 @@ -dask -zarr -fsspec>=0.3.3 diff --git a/requirements_dev.txt b/requirements_dev.txt deleted file mode 100644 index 5bab4a0c4b..0000000000 --- a/requirements_dev.txt +++ /dev/null @@ -1,27 +0,0 @@ -sphinx -sphinx_rtd_theme -sphinx-gallery>=0.1.13 -sphinx-toolbox -sphinxcontrib-apidoc -pydata-sphinx-theme -nbsphinx -numpydoc -pillow -pylint -numpy>=1.20 -scipy>=1.8 -scikit-learn>=1.2 -sympy -wheel -pytest -pytest-cov -jupyter -toolz -empymod>=2.0.0 -scooby -black==24.3.0 -pre-commit -twine -memory_profiler -plotly -setuptools_scm diff --git a/requirements_style.txt b/requirements_style.txt deleted file mode 100644 index a4fd699571..0000000000 --- a/requirements_style.txt +++ /dev/null @@ -1,7 +0,0 @@ -black==24.3.0 -flake8==7.0.0 -flake8-bugbear==23.12.2 -flake8-builtins==2.2.0 -flake8-mutable==1.2.0 -flake8-rst-docstrings==0.3.0 -flake8-docstrings==1.7.0 diff --git a/setup.py b/setup.py deleted file mode 100644 index 10411d654e..0000000000 --- a/setup.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python -"""SimPEG: Simulation and Parameter Estimation in Geophysics - -SimPEG is a python package for simulation and gradient based -parameter estimation in the context of geophysical applications. -""" - -from setuptools import setup, find_packages -import os - -CLASSIFIERS = [ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "Intended Audience :: Science/Research", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python", - "Topic :: Scientific/Engineering", - "Topic :: Scientific/Engineering :: Mathematics", - "Topic :: Scientific/Engineering :: Physics", - "Operating System :: Microsoft :: Windows", - "Operating System :: POSIX", - "Operating System :: Unix", - "Operating System :: MacOS", - "Natural Language :: English", -] - -with open("README.rst") as f: - LONG_DESCRIPTION = "".join(f.readlines()) - -setup( - name="simpeg", - packages=find_packages(exclude=["tests*", "examples*", "tutorials*"]), - py_modules=["SimPEG"], - python_requires=">=3.8", - setup_requires=[ - "setuptools_scm", - ], - install_requires=[ - "numpy>=1.20", - "scipy>=1.8", - "scikit-learn>=1.2", - "pymatsolver>=0.2", - "matplotlib", - "discretize>=0.10", - "geoana>=0.5.0", - "empymod>=2.0.0", - "pandas", - ], - author="Rowan Cockett", - author_email="rowanc1@gmail.com", - description="SimPEG: Simulation and Parameter Estimation in Geophysics", - long_description=LONG_DESCRIPTION, - license="MIT", - keywords="geophysics inverse problem", - url="https://simpeg.xyz/", - download_url="https://github.com/simpeg/simpeg", - classifiers=CLASSIFIERS, - platforms=["Windows", "Linux", "Solaris", "Mac OS-X", "Unix"], - use_2to3=False, - use_scm_version={ - "write_to": os.path.join("simpeg", "version.py"), - }, -) diff --git a/simpeg/data.py b/simpeg/data.py index 1d53972caa..00be1504a7 100644 --- a/simpeg/data.py +++ b/simpeg/data.py @@ -58,7 +58,7 @@ def __init__( relative_error=None, noise_floor=None, standard_deviation=None, - **kwargs + **kwargs, ): super().__init__(**kwargs) self.survey = survey diff --git a/simpeg/electromagnetics/frequency_domain/simulation.py b/simpeg/electromagnetics/frequency_domain/simulation.py index de1afecb68..55ad162e71 100644 --- a/simpeg/electromagnetics/frequency_domain/simulation.py +++ b/simpeg/electromagnetics/frequency_domain/simulation.py @@ -73,7 +73,7 @@ def __init__( forward_only=False, permittivity=None, storeJ=False, - **kwargs + **kwargs, ): super().__init__(mesh=mesh, survey=survey, **kwargs) self.forward_only = forward_only diff --git a/simpeg/electromagnetics/natural_source/simulation_1d.py b/simpeg/electromagnetics/natural_source/simulation_1d.py index 8ab8992ec5..bc05c91759 100644 --- a/simpeg/electromagnetics/natural_source/simulation_1d.py +++ b/simpeg/electromagnetics/natural_source/simulation_1d.py @@ -54,7 +54,7 @@ def __init__( thicknesses=None, thicknessesMap=None, fix_Jmatrix=False, - **kwargs + **kwargs, ): super().__init__(mesh=None, survey=survey, **kwargs) self.fix_Jmatrix = fix_Jmatrix diff --git a/simpeg/electromagnetics/static/induced_polarization/simulation.py b/simpeg/electromagnetics/static/induced_polarization/simulation.py index 027d6933bf..64b07682bc 100644 --- a/simpeg/electromagnetics/static/induced_polarization/simulation.py +++ b/simpeg/electromagnetics/static/induced_polarization/simulation.py @@ -70,7 +70,7 @@ def __init__( etaMap=None, Ainv=None, # A DC's Ainv _f=None, # A pre-computed DC field - **kwargs + **kwargs, ): super().__init__(mesh=mesh, survey=survey, **kwargs) self.sigma = sigma diff --git a/simpeg/electromagnetics/static/self_potential/simulation.py b/simpeg/electromagnetics/static/self_potential/simulation.py index b97d6c21ad..2ee621464f 100644 --- a/simpeg/electromagnetics/static/self_potential/simulation.py +++ b/simpeg/electromagnetics/static/self_potential/simulation.py @@ -64,7 +64,7 @@ def __init__( rho=rho, sigmaMap=None, rhoMap=None, - **kwargs + **kwargs, ) self.q = q self.qMap = qMap @@ -166,7 +166,7 @@ class Survey(dc.Survey): Parameters ---------- - source_list : list of sources.StreamingCurrents + source_list : list of .sources.StreamingCurrents """ @property @@ -175,7 +175,7 @@ def source_list(self): Returns ------- - list of sources.StreamingCurrents + list of .sources.StreamingCurrents """ return self._source_list diff --git a/simpeg/electromagnetics/static/spectral_induced_polarization/simulation.py b/simpeg/electromagnetics/static/spectral_induced_polarization/simulation.py index 87e5638875..04263d3197 100644 --- a/simpeg/electromagnetics/static/spectral_induced_polarization/simulation.py +++ b/simpeg/electromagnetics/static/spectral_induced_polarization/simulation.py @@ -45,7 +45,7 @@ def __init__( storeJ=False, actinds=None, storeInnerProduct=True, - **kwargs + **kwargs, ): super().__init__(mesh=mesh, survey=survey, **kwargs) self.tau = tau diff --git a/simpeg/electromagnetics/static/spectral_induced_polarization/survey.py b/simpeg/electromagnetics/static/spectral_induced_polarization/survey.py index 05f5a0d460..7039d4fb05 100644 --- a/simpeg/electromagnetics/static/spectral_induced_polarization/survey.py +++ b/simpeg/electromagnetics/static/spectral_induced_polarization/survey.py @@ -26,7 +26,7 @@ def __init__( source_list=None, survey_geometry="surface", survey_type="dipole-dipole", - **kwargs + **kwargs, ): if source_list is None: raise AttributeError("Survey cannot be instantiated without sources") diff --git a/simpeg/flow/richards/empirical.py b/simpeg/flow/richards/empirical.py index d726aaad91..ae74f05b96 100644 --- a/simpeg/flow/richards/empirical.py +++ b/simpeg/flow/richards/empirical.py @@ -100,7 +100,7 @@ def __init__( alphaMap=None, beta=3.96, betaMap=None, - **kwargs + **kwargs, ): super().__init__(mesh=mesh, **kwargs) self.theta_r = theta_r @@ -224,7 +224,7 @@ def __init__( AMap=None, gamma=4.74, gammaMap=None, - **kwargs + **kwargs, ): super().__init__(mesh=mesh, **kwargs) self.Ks = Ks @@ -292,7 +292,7 @@ def haverkamp(mesh, **kwargs): Haverkamp_theta, ["Ks", "A", "gamma"], ["alpha", "beta", "theta_r", "theta_s"], - **kwargs + **kwargs, ) @@ -344,7 +344,7 @@ def __init__( nMap=None, alpha=0.036, alphaMap=None, - **kwargs + **kwargs, ): super().__init__(mesh=mesh, **kwargs) self.theta_r = theta_r @@ -499,7 +499,7 @@ def __init__( nMap=None, alpha=0.036, alphaMap=None, - **kwargs + **kwargs, ): super().__init__(mesh=mesh, **kwargs) self.Ks = Ks @@ -807,7 +807,7 @@ def van_genuchten(mesh, **kwargs): Vangenuchten_theta, ["alpha", "n", "Ks", "I"], ["alpha", "n", "theta_r", "theta_s"], - **kwargs + **kwargs, ) diff --git a/simpeg/flow/richards/simulation.py b/simpeg/flow/richards/simulation.py index b897230230..c6dae5c408 100644 --- a/simpeg/flow/richards/simulation.py +++ b/simpeg/flow/richards/simulation.py @@ -33,7 +33,7 @@ def __init__( do_newton=False, root_finder_max_iter=30, root_finder_tol=1e-4, - **kwargs + **kwargs, ): debug = kwargs.pop("debug", None) if debug is not None: diff --git a/simpeg/inversion.py b/simpeg/inversion.py index 9826f8cca9..a3e51cf541 100644 --- a/simpeg/inversion.py +++ b/simpeg/inversion.py @@ -15,7 +15,7 @@ def __init__( counter=None, debug=False, name="BaseInversion", - **kwargs + **kwargs, ): if directiveList is None: directiveList = [] diff --git a/simpeg/meta/dask_sim.py b/simpeg/meta/dask_sim.py index bddf091920..5f6f09e064 100644 --- a/simpeg/meta/dask_sim.py +++ b/simpeg/meta/dask_sim.py @@ -247,7 +247,7 @@ def check_mapping(mapping, sim, model_len): raise ValueError("All mappings must have the same input length") if np.any(error_checks == 2): raise ValueError( - f"Simulations and mappings at indices {np.where(error_checks==2)}" + f"Simulations and mappings at indices {np.where(error_checks == 2)}" f" are inconsistent." ) diff --git a/simpeg/regularization/__init__.py b/simpeg/regularization/__init__.py index dbc30a5984..e28f24ca0a 100644 --- a/simpeg/regularization/__init__.py +++ b/simpeg/regularization/__init__.py @@ -200,7 +200,7 @@ def __init__(self, mesh=None, alpha_x=1.0, alpha_y=1.0, alpha_z=1.0, **kwargs): length_scale_x=alpha_x, length_scale_y=alpha_y, length_scale_z=alpha_z, - **kwargs + **kwargs, ) @@ -217,7 +217,7 @@ def __init__( alpha_x=alpha_x, alpha_y=alpha_y, alpha_z=alpha_z, - **kwargs + **kwargs, ) diff --git a/simpeg/utils/plot_utils.py b/simpeg/utils/plot_utils.py index fc50246fd8..1c96afd170 100644 --- a/simpeg/utils/plot_utils.py +++ b/simpeg/utils/plot_utils.py @@ -255,7 +255,7 @@ def hillshade(array, azimuth, angle_altitude): Y, hillshade(DATA, shade_azimuth, shade_angle_altitude), shade_ncontour, - **shadeOpts + **shadeOpts, ) if dataloc: @@ -279,7 +279,7 @@ def plot_1d_layer_model( plot_elevation=False, show_layers=False, vlim=None, - **kwargs + **kwargs, ): """ Plot the vertical profile for a 1D layered Earth model. diff --git a/tests/base/test_Props.py b/tests/base/test_Props.py index bb7de0602d..53c2948788 100644 --- a/tests/base/test_Props.py +++ b/tests/base/test_Props.py @@ -106,7 +106,7 @@ def __init__( AMap=None, gamma=4.74, gammaMap=None, - **kwargs + **kwargs, ): super().__init__(**kwargs) self.Ks = Ks diff --git a/tests/pf/test_forward_PFproblem.py b/tests/pf/test_forward_PFproblem.py index 660af07d3f..0777e45c56 100644 --- a/tests/pf/test_forward_PFproblem.py +++ b/tests/pf/test_forward_PFproblem.py @@ -69,7 +69,7 @@ def test_ana_forward(self): *self.sphere_center, self.chiblk, self.b0, - "secondary" + "secondary", ) n_obs, n_comp = self.rxLoc.shape[0], len(self.survey.components) From eee9ce4b12feb8a76a0f7a57ca18cc1303e85c9d Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 17 Jun 2024 15:17:02 -0700 Subject: [PATCH 031/194] Use Numpy rng in static EM tests (#1451) Replace the usage of the deprecated functions in `numpy.random` module for the Numpy's random number generator class and its methods, in most of the static EM tests. Part of the solution to #1289 --- tests/em/static/test_DCIP_io_utils.py | 9 ++-- tests/em/static/test_DC_1D_jvecjtvecadj.py | 19 ++++---- tests/em/static/test_DC_2D_jvecjtvecadj.py | 2 + tests/em/static/test_DC_jvecjtvecadj.py | 53 ++++++++++++++------- tests/em/static/test_DC_miniaturize.py | 12 +++-- tests/em/static/test_IP_2D_jvecjtvecadj.py | 16 ++++--- tests/em/static/test_IP_jvecjtvecadj.py | 30 ++++++++---- tests/em/static/test_SIP_2D_jvecjtvecadj.py | 23 +++++---- tests/em/static/test_SIP_jvecjtvecadj.py | 23 +++++---- tests/em/static/test_SPjvecjtvecadj.py | 7 ++- 10 files changed, 128 insertions(+), 66 deletions(-) diff --git a/tests/em/static/test_DCIP_io_utils.py b/tests/em/static/test_DCIP_io_utils.py index 12bd6c6beb..eebf780d06 100644 --- a/tests/em/static/test_DCIP_io_utils.py +++ b/tests/em/static/test_DCIP_io_utils.py @@ -39,7 +39,8 @@ def test_dc2d(self): self.station_spacing, ) survey2D = dc.survey.Survey(source_list) - dobs = np.random.rand(survey2D.nD) + rng = np.random.default_rng(seed=42) + dobs = rng.uniform(size=survey2D.nD) dunc = 1e-3 * np.ones(survey2D.nD) data2D = data.Data(survey2D, dobs=dobs, standard_deviation=dunc) @@ -94,7 +95,8 @@ def test_ip2d(self): self.station_spacing, ) survey2D = dc.survey.Survey(source_list) - dobs = np.random.rand(survey2D.nD) + rng = np.random.default_rng(seed=42) + dobs = rng.uniform(size=survey2D.nD) dunc = 1e-3 * np.ones(survey2D.nD) data2D = data.Data(survey2D, dobs=dobs, standard_deviation=dunc) @@ -151,7 +153,8 @@ def test_dcip3d(self): self.station_spacing, ) survey3D = dc.survey.Survey(source_list) - dobs = np.random.rand(survey3D.nD) + rng = np.random.default_rng(seed=42) + dobs = rng.uniform(size=survey3D.nD) dunc = 1e-3 * np.ones(survey3D.nD) data3D = data.Data(survey3D, dobs=dobs, standard_deviation=dunc) diff --git a/tests/em/static/test_DC_1D_jvecjtvecadj.py b/tests/em/static/test_DC_1D_jvecjtvecadj.py index 87ff631371..679e0d824f 100644 --- a/tests/em/static/test_DC_1D_jvecjtvecadj.py +++ b/tests/em/static/test_DC_1D_jvecjtvecadj.py @@ -87,9 +87,9 @@ def test_forward_accuracy(data_type, rx_type, tx_type): @pytest.mark.parametrize("deriv_type", ("sigma", "h", "both")) def test_derivative(deriv_type): n_layer = 4 - np.random.seed(40) - log_cond = np.random.rand(n_layer) - log_thick = np.random.rand(n_layer - 1) + rng = np.random.default_rng(seed=40) + log_cond = rng.uniform(size=n_layer) + log_thick = rng.uniform(size=n_layer - 1) if deriv_type != "h": sigma_map = maps.ExpMap() @@ -130,15 +130,16 @@ def J(v): return d, J + np.random.seed(40) # set a random seed for check_derivative assert check_derivative(sim_1d_func, model, plotIt=False, num=4) @pytest.mark.parametrize("deriv_type", ("sigma", "h", "both")) def test_adjoint(deriv_type): n_layer = 4 - np.random.seed(40) - log_cond = np.random.rand(n_layer) - log_thick = np.random.rand(n_layer - 1) + rng = np.random.default_rng(seed=40) + log_cond = rng.uniform(size=n_layer) + log_thick = rng.uniform(size=n_layer - 1) if deriv_type != "h": sigma_map = maps.ExpMap() @@ -219,9 +220,9 @@ def test_errors(): def test_functionality(): n_layer = 4 - np.random.seed(40) - log_cond = np.random.rand(n_layer) - thick = np.random.rand(n_layer - 1) + rng = np.random.default_rng(seed=40) + log_cond = rng.uniform(size=n_layer) + thick = rng.uniform(size=n_layer - 1) sigma_map = maps.ExpMap() model = log_cond diff --git a/tests/em/static/test_DC_2D_jvecjtvecadj.py b/tests/em/static/test_DC_2D_jvecjtvecadj.py index c25e06711f..c974a5b5e7 100644 --- a/tests/em/static/test_DC_2D_jvecjtvecadj.py +++ b/tests/em/static/test_DC_2D_jvecjtvecadj.py @@ -73,6 +73,7 @@ def setUp(self): self.data = data def test_misfit(self): + np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: (self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)), self.m0, @@ -93,6 +94,7 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): + np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 ) diff --git a/tests/em/static/test_DC_jvecjtvecadj.py b/tests/em/static/test_DC_jvecjtvecadj.py index 25c08fc8e2..703bc8e7ce 100644 --- a/tests/em/static/test_DC_jvecjtvecadj.py +++ b/tests/em/static/test_DC_jvecjtvecadj.py @@ -67,6 +67,7 @@ def setUp(self): self.dobs = dobs def test_misfit(self): + np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)], self.m0, @@ -78,8 +79,9 @@ def test_misfit(self): def test_adjoint(self): # Adjoint Test # u = np.random.rand(self.mesh.nC*self.survey.nSrc) - v = np.random.rand(self.mesh.nC) - w = np.random.rand(mkvc(self.dobs).shape[0]) + rng = np.random.default_rng(seed=42) + v = rng.uniform(size=self.mesh.nC) + w = rng.uniform(size=mkvc(self.dobs).shape[0]) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < 1e-10 @@ -87,6 +89,7 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): + np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=6 ) @@ -134,22 +137,25 @@ def setUp(self): ) def test_e_deriv(self): - x0 = -1 + 1e-1 * np.random.rand(self.sigma_map.nP) + rng = np.random.default_rng(seed=42) + x0 = -1 + 1e-1 * rng.uniform(size=self.sigma_map.nP) def fun(x): return self.prob.dpred(x), lambda x: self.prob.Jvec(x0, x) + np.random.seed(40) # set a random seed for check_derivative return tests.check_derivative(fun, x0, num=3, plotIt=False) def test_e_adjoint(self): print("Adjoint Test for e") - m = -1 + 1e-1 * np.random.rand(self.sigma_map.nP) + rng = np.random.default_rng(seed=42) + m = -1 + 1e-1 * rng.uniform(size=self.sigma_map.nP) u = self.prob.fields(m) # u = u[self.survey.source_list,'e'] - v = np.random.rand(self.survey.nD) - w = np.random.rand(self.sigma_map.nP) + v = rng.uniform(size=self.survey.nD) + w = rng.uniform(size=self.sigma_map.nP) vJw = v.dot(self.prob.Jvec(m, w, u)) wJtv = w.dot(self.prob.Jtvec(m, v, u)) @@ -206,6 +212,7 @@ def setUp(self): self.dobs = dobs def test_misfit(self): + np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)], self.m0, @@ -217,8 +224,9 @@ def test_misfit(self): def test_adjoint(self): # Adjoint Test # u = np.random.rand(self.mesh.nC*self.survey.nSrc) - v = np.random.rand(self.mesh.nC) - w = np.random.rand(mkvc(self.dobs).shape[0]) + rng = np.random.default_rng(seed=42) + v = rng.uniform(size=self.mesh.nC) + w = rng.uniform(size=mkvc(self.dobs).shape[0]) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < 1e-8 @@ -226,6 +234,7 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): + np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 ) @@ -277,6 +286,7 @@ def setUp(self): self.dobs = dobs def test_misfit(self): + np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)], self.m0, @@ -288,8 +298,9 @@ def test_misfit(self): def test_adjoint(self): # Adjoint Test # u = np.random.rand(self.mesh.nC*self.survey.nSrc) - v = np.random.rand(self.mesh.nC) - w = np.random.rand(mkvc(self.dobs).shape[0]) + rng = np.random.default_rng(seed=42) + v = rng.uniform(size=self.mesh.nC) + w = rng.uniform(size=mkvc(self.dobs).shape[0]) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < 1e-8 @@ -297,6 +308,7 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): + np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 ) @@ -348,6 +360,7 @@ def setUp(self): self.dobs = dobs def test_misfit(self): + np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)], self.m0, @@ -359,8 +372,9 @@ def test_misfit(self): def test_adjoint(self): # Adjoint Test # u = np.random.rand(self.mesh.nC*self.survey.nSrc) - v = np.random.rand(self.mesh.nC) - w = np.random.rand(mkvc(self.dobs).shape[0]) + rng = np.random.default_rng(seed=42) + v = rng.uniform(size=self.mesh.nC) + w = rng.uniform(size=mkvc(self.dobs).shape[0]) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < 1e-10 @@ -368,6 +382,7 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): + np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=4 ) @@ -426,6 +441,7 @@ def setUp(self): self.dobs = dobs def test_misfit(self): + np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)], self.m0, @@ -437,8 +453,9 @@ def test_misfit(self): def test_adjoint(self): # Adjoint Test # u = np.random.rand(self.mesh.nC*self.survey.nSrc) - v = np.random.rand(self.mesh.nC) - w = np.random.rand(mkvc(self.dobs).shape[0]) + rng = np.random.default_rng(seed=42) + v = rng.uniform(size=self.mesh.nC) + w = rng.uniform(size=mkvc(self.dobs).shape[0]) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < 1e-8 @@ -446,6 +463,7 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): + np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 ) @@ -508,6 +526,7 @@ def setUp(self): self.dobs = dobs def test_misfit(self): + np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)], self.m0, @@ -519,8 +538,9 @@ def test_misfit(self): def test_adjoint(self): # Adjoint Test # u = np.random.rand(self.mesh.nC*self.survey.nSrc) - v = np.random.rand(self.mesh.nC) - w = np.random.rand(mkvc(self.dobs).shape[0]) + rng = np.random.default_rng(seed=42) + v = rng.uniform(size=self.mesh.nC) + w = rng.uniform(size=mkvc(self.dobs).shape[0]) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < 1e-8 @@ -528,6 +548,7 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): + np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 ) diff --git a/tests/em/static/test_DC_miniaturize.py b/tests/em/static/test_DC_miniaturize.py index 95a2fb67a7..c2b7bc1c78 100644 --- a/tests/em/static/test_DC_miniaturize.py +++ b/tests/em/static/test_DC_miniaturize.py @@ -198,13 +198,15 @@ def test_dpred(self): self.assertTrue(np.allclose(d1, d2)) def test_Jvec(self): - u = np.random.rand(*self.model.shape) + rng = np.random.default_rng(seed=42) + u = rng.uniform(size=self.model.shape) J1u = self.sim1.Jvec(self.model, u, f=self.f1) J2u = self.sim2.Jvec(self.model, u, f=self.f2) self.assertTrue(np.allclose(J1u, J2u)) def test_Jtvec(self): - v = np.random.rand(self.survey.nD) + rng = np.random.default_rng(seed=42) + v = rng.uniform(size=self.survey.nD) J1tv = self.sim1.Jtvec(self.model, v, f=self.f1) J2tv = self.sim2.Jtvec(self.model, v, f=self.f2) self.assertTrue(np.allclose(J1tv, J2tv)) @@ -295,13 +297,15 @@ def test_dpred(self): self.assertTrue(np.allclose(d1, d2)) def test_Jvec(self): - u = np.random.rand(*self.model.shape) + rng = np.random.default_rng(seed=42) + u = rng.uniform(size=self.model.shape) J1u = self.sim1.Jvec(self.model, u, f=self.f1) J2u = self.sim2.Jvec(self.model, u, f=self.f2) self.assertTrue(np.allclose(J1u, J2u)) def test_Jtvec(self): - v = np.random.rand(self.survey.nD) + rng = np.random.default_rng(seed=42) + v = rng.uniform(size=self.survey.nD) J1tv = self.sim1.Jtvec(self.model, v, f=self.f1) J2tv = self.sim2.Jtvec(self.model, v, f=self.f2) self.assertTrue(np.allclose(J1tv, J2tv)) diff --git a/tests/em/static/test_IP_2D_jvecjtvecadj.py b/tests/em/static/test_IP_2D_jvecjtvecadj.py index c95da77982..99e5cfb06b 100644 --- a/tests/em/static/test_IP_2D_jvecjtvecadj.py +++ b/tests/em/static/test_IP_2D_jvecjtvecadj.py @@ -14,8 +14,6 @@ from simpeg.electromagnetics import resistivity as dc from simpeg.electromagnetics import induced_polarization as ip -np.random.seed(30) - class IPProblemTestsCC(unittest.TestCase): def setUp(self): @@ -68,6 +66,7 @@ def setUp(self): self.dmis = dmis def test_misfit(self): + np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)], self.m0, @@ -79,8 +78,9 @@ def test_misfit(self): def test_adjoint(self): # Adjoint Test # u = np.random.rand(self.mesh.nC*self.survey.nSrc) - v = np.random.rand(self.mesh.nC) - w = np.random.rand(self.survey.nD) + rng = np.random.default_rng(seed=30) + v = rng.uniform(size=self.mesh.nC) + w = rng.uniform(size=self.survey.nD) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < 1e-10 @@ -88,6 +88,7 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): + np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 ) @@ -145,6 +146,7 @@ def setUp(self): self.dmis = dmis def test_misfit(self): + np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)], self.m0, @@ -156,8 +158,9 @@ def test_misfit(self): def test_adjoint(self): # Adjoint Test # u = np.random.rand(self.mesh.nC*self.survey.nSrc) - v = np.random.rand(self.mesh.nC) - w = np.random.rand(self.survey.nD) + rng = np.random.default_rng(seed=30) + v = rng.uniform(size=self.mesh.nC) + w = rng.uniform(size=self.survey.nD) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < 1e-8 @@ -165,6 +168,7 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): + np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 ) diff --git a/tests/em/static/test_IP_jvecjtvecadj.py b/tests/em/static/test_IP_jvecjtvecadj.py index 8884b3b385..30189d133d 100644 --- a/tests/em/static/test_IP_jvecjtvecadj.py +++ b/tests/em/static/test_IP_jvecjtvecadj.py @@ -14,8 +14,6 @@ from simpeg.electromagnetics import induced_polarization as ip import shutil -np.random.seed(30) - class IPProblemTestsCC(unittest.TestCase): def setUp(self): @@ -61,6 +59,7 @@ def setUp(self): # self.dobe = dobs def test_misfit(self): + np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)], self.m0, @@ -72,8 +71,9 @@ def test_misfit(self): def test_adjoint(self): # Adjoint Test # u = np.random.rand(self.mesh.nC*self.survey.Survey.nSrc) - v = np.random.rand(self.mesh.nC) - w = np.random.rand(self.survey.nD) + rng = np.random.default_rng(seed=30) + v = rng.uniform(size=self.mesh.nC) + w = rng.uniform(size=self.survey.nD) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < 1e-10 @@ -81,6 +81,7 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): + np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 ) @@ -130,6 +131,7 @@ def setUp(self): self.dmis = dmis def test_misfit(self): + np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)], self.m0, @@ -141,8 +143,9 @@ def test_misfit(self): def test_adjoint(self): # Adjoint Test # u = np.random.rand(self.mesh.nC*self.survey.Survey.nSrc) - v = np.random.rand(self.mesh.nC) - w = np.random.rand(self.survey.nD) + rng = np.random.default_rng(seed=30) + v = rng.uniform(size=self.mesh.nC) + w = rng.uniform(size=self.survey.nD) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < 1e-8 @@ -150,6 +153,7 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): + np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 ) @@ -203,6 +207,7 @@ def setUp(self): self.dmis = dmis def test_misfit(self): + np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)], self.m0, @@ -214,8 +219,9 @@ def test_misfit(self): def test_adjoint(self): # Adjoint Test # u = np.random.rand(self.mesh.nC*self.survey.Survey.nSrc) - v = np.random.rand(self.mesh.nC) - w = np.random.rand(self.survey.nD) + rng = np.random.default_rng(seed=30) + v = rng.uniform(size=self.mesh.nC) + w = rng.uniform(size=self.survey.nD) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < 1e-10 @@ -223,6 +229,7 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): + np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 ) @@ -283,6 +290,7 @@ def setUp(self): self.dmis = dmis def test_misfit(self): + np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)], self.m0, @@ -294,8 +302,9 @@ def test_misfit(self): def test_adjoint(self): # Adjoint Test # u = np.random.rand(self.mesh.nC*self.survey.Survey.nSrc) - v = np.random.rand(self.mesh.nC) - w = np.random.rand(self.survey.nD) + rng = np.random.default_rng(seed=30) + v = rng.uniform(size=self.mesh.nC) + w = rng.uniform(size=self.survey.nD) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < 1e-8 @@ -303,6 +312,7 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): + np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 ) diff --git a/tests/em/static/test_SIP_2D_jvecjtvecadj.py b/tests/em/static/test_SIP_2D_jvecjtvecadj.py index 751aa7ea98..1df7d48000 100644 --- a/tests/em/static/test_SIP_2D_jvecjtvecadj.py +++ b/tests/em/static/test_SIP_2D_jvecjtvecadj.py @@ -19,8 +19,6 @@ except ImportError: from simpeg import SolverLU as Solver -np.random.seed(38) - class SIPProblemTestsCC(unittest.TestCase): def setUp(self): @@ -88,6 +86,7 @@ def setUp(self): self.dobs = dobs def test_misfit(self): + np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)], self.m0, @@ -99,8 +98,9 @@ def test_misfit(self): def test_adjoint(self): # Adjoint Test # u = np.random.rand(self.mesh.nC*self.survey.nSrc) - v = np.random.rand(self.mesh.nC * 2) - w = np.random.rand(self.survey.nD) + rng = np.random.default_rng(seed=38) + v = rng.uniform(size=self.mesh.nC * 2) + w = rng.uniform(size=self.survey.nD) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < 1e-10 @@ -108,6 +108,7 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): + np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 ) @@ -180,6 +181,7 @@ def setUp(self): self.dobs = dobs def test_misfit(self): + np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)], self.m0, @@ -190,8 +192,9 @@ def test_misfit(self): def test_adjoint(self): # Adjoint Test - v = np.random.rand(self.mesh.nC * 2) - w = np.random.rand(self.survey.nD) + rng = np.random.default_rng(seed=38) + v = rng.uniform(size=self.mesh.nC * 2) + w = rng.uniform(size=self.survey.nD) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < 1e-8 @@ -199,6 +202,7 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): + np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=2 ) @@ -290,6 +294,7 @@ def setUp(self): self.dobs = dobs def test_misfit(self): + np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)], self.m0, @@ -300,8 +305,9 @@ def test_misfit(self): def test_adjoint(self): # Adjoint Test - v = np.random.rand(self.reg.mapping.nP) - w = np.random.rand(self.survey.nD) + rng = np.random.default_rng(seed=38) + v = rng.uniform(size=self.reg.mapping.nP) + w = rng.uniform(size=self.survey.nD) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < 1e-8 @@ -309,6 +315,7 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): + np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 ) diff --git a/tests/em/static/test_SIP_jvecjtvecadj.py b/tests/em/static/test_SIP_jvecjtvecadj.py index d9400f6f1e..a1cc1aea2f 100644 --- a/tests/em/static/test_SIP_jvecjtvecadj.py +++ b/tests/em/static/test_SIP_jvecjtvecadj.py @@ -18,8 +18,6 @@ except ImportError: from simpeg import SolverLU as Solver -np.random.seed(38) - class SIPProblemTestsCC(unittest.TestCase): def setUp(self): @@ -94,6 +92,7 @@ def setUp(self): self.dobs = dobs def test_misfit(self): + np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)], self.m0, @@ -105,8 +104,9 @@ def test_misfit(self): def test_adjoint(self): # Adjoint Test # u = np.random.rand(self.mesh.nC*self.survey.nSrc) - v = np.random.rand(self.mesh.nC * 2) - w = np.random.rand(self.dobs.shape[0]) + rng = np.random.default_rng(seed=38) + v = rng.uniform(size=self.mesh.nC * 2) + w = rng.uniform(size=self.dobs.shape[0]) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < 1e-10 @@ -114,6 +114,7 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): + np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 ) @@ -192,6 +193,7 @@ def setUp(self): self.dobs = dobs def test_misfit(self): + np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)], self.m0, @@ -202,8 +204,9 @@ def test_misfit(self): def test_adjoint(self): # Adjoint Test - v = np.random.rand(self.mesh.nC * 2) - w = np.random.rand(self.dobs.shape[0]) + rng = np.random.default_rng(seed=38) + v = rng.uniform(size=self.mesh.nC * 2) + w = rng.uniform(size=self.dobs.shape[0]) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < 1e-8 @@ -211,6 +214,7 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): + np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 ) @@ -302,6 +306,7 @@ def setUp(self): self.dobs = dobs def test_misfit(self): + np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)], self.m0, @@ -312,8 +317,9 @@ def test_misfit(self): def test_adjoint(self): # Adjoint Test - v = np.random.rand(self.reg.mapping.nP) - w = np.random.rand(self.dobs.shape[0]) + rng = np.random.default_rng(seed=38) + v = rng.uniform(size=self.reg.mapping.nP) + w = rng.uniform(size=self.dobs.shape[0]) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < 1e-8 @@ -321,6 +327,7 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): + np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 ) diff --git a/tests/em/static/test_SPjvecjtvecadj.py b/tests/em/static/test_SPjvecjtvecadj.py index 8d806a3de3..df26ee3706 100644 --- a/tests/em/static/test_SPjvecjtvecadj.py +++ b/tests/em/static/test_SPjvecjtvecadj.py @@ -71,7 +71,9 @@ def Jvec(v): return d, Jvec - m0 = np.random.randn(q_map.shape[1]) + rng = np.random.default_rng(seed=42) + m0 = rng.normal(size=q_map.shape[1]) + np.random.seed(40) # set a random seed for check_derivative check_derivative(func, m0, plotIt=False) @@ -87,7 +89,8 @@ def test_adjoint(q_map): sim.model = None sim.qMap = q_map - model = np.random.rand(q_map.shape[1]) + rng = np.random.default_rng(seed=42) + model = rng.uniform(size=q_map.shape[1]) f = sim.fields(model) def Jvec(v): From 3eb16c05b4875c4384111787bf78410266da4b7c Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 18 Jun 2024 14:22:29 -0700 Subject: [PATCH 032/194] Split Azure Pipelines configuration into multiple files (#1481) Split the stages from `azure-pipelines.yml` into multiple files under the `.ci/azure` directory. Strip deployment to PyPI from the deployment of the docs into different stages. Add new job to upload each version merged to `main` to TestPyPI. Configure the `setuptools_scm` local version scheme so we can modify it before building the wheels for TestPyPI. Refactor the new docs pipeline: have a single build job and two jobs for deploying the docs to `simpeg-doctest`, one after a release and one for nightly builds. While building the sdist (before pushing to PyPI), rely on `build` to install build dependencies listed in `pyproject.toml` in isolation. --------- Co-authored-by: Joseph Capriotti --- .ci/azure/docs.yml | 190 +++++++++++++++ .ci/azure/old-docs.yml | 56 +++++ .ci/azure/pypi.yml | 83 +++++++ .ci/azure/style.yml | 41 ++++ .ci/azure/{matrix.yml => test.yml} | 0 azure-pipelines.yml | 361 +++++------------------------ pyproject.toml | 1 + 7 files changed, 426 insertions(+), 306 deletions(-) create mode 100644 .ci/azure/docs.yml create mode 100644 .ci/azure/old-docs.yml create mode 100644 .ci/azure/pypi.yml create mode 100644 .ci/azure/style.yml rename .ci/azure/{matrix.yml => test.yml} (100%) diff --git a/.ci/azure/docs.yml b/.ci/azure/docs.yml new file mode 100644 index 0000000000..bb3a64a45f --- /dev/null +++ b/.ci/azure/docs.yml @@ -0,0 +1,190 @@ +jobs: + # Build docs only on scheduled jobs or on a relase + - job: BuildDocs + condition: or(eq(variables['Build.Reason'], 'Schedule'), startsWith(variables['build.sourceBranch'], 'refs/tags/')) + pool: + vmImage: ubuntu-latest + variables: + python.version: "3.8" + timeoutInMinutes: 240 + steps: + # Checkout simpeg repo. + # Sync tags and disable shallow depth to get the SimPEG version. + - checkout: self + fetchDepth: 0 + fetchTags: true + displayName: Checkout repository (including tags) + + - bash: echo "##vso[task.prependpath]$CONDA/bin" + displayName: Add conda to PATH + + - bash: .ci/azure/setup_env.sh + displayName: Setup SimPEG environment + + - bash: | + source activate simpeg-test + make -C docs html + displayName: Building documentation + + - task: PublishPipelineArtifact@1 + inputs: + targetPath: $(Build.SourcesDirectory)/docs/_build/html + artifactName: built-docs + displayName: "Upload docs as artifact" + + - job: DeployRelease + dependsOn: BuildDocs + condition: startsWith(variables['build.sourceBranch'], 'refs/tags/') + pool: + vmImage: ubuntu-latest + timeoutInMinutes: 240 + steps: + # Checkout simpeg repo. + # Sync tags and disable shallow depth to get the SimPEG version. + - checkout: self + fetchDepth: 0 + fetchTags: true + displayName: Checkout repository (including tags) + + - bash: | + git config --global user.name ${GH_NAME} + git config --global user.email ${GH_EMAIL} + git config --list | grep user. + displayName: "Configure git" + env: + GH_NAME: $(gh.name) + GH_EMAIL: $(gh.email) + + - bash: | + mkdir -p docs/_build/html + displayName: "Create directory for built docs" + + - task: DownloadPipelineArtifact@2 + inputs: + artifact: built-docs + targetPath: docs/_build/html + displayName: "Download docs artifact" + + # Upload release build of the docs to gh-pages branch in simpeg/simpeg-doctest + - bash: | + # Capture version + # TODO: we should be able to get the version from the + # build.sourceBranch variable + version=$(git tag --points-at HEAD) + if [ -n "$version" ]; then + echo "Version could not be obtained from tag. Exiting." + exit 1 + fi + # Capture hash of last commit in simpeg + commit=$(git rev-parse --short HEAD) + # Clone the repo where we store the documentation + git clone -q --branch gh-pages --depth 1 https://${GH_TOKEN}@github.com/simpeg/simpeg-doctest.git + cd simpeg-doctest + # Move the built docs to a new dev folder + cp -r $BUILD_SOURCESDIRECTORY/docs/_build/html "$version" + cp $BUILD_SOURCESDIRECTORY/docs/README.md . + # Add .nojekyll if missing + touch .nojekyll + # Update latest symlink + rm -f latest + ln -s "$version" latest + # Commit the new docs. + git add "$version" README.md .nojekyll latest + message="Azure CI deploy ${version} from ${commit}" + echo -e "\nMaking a new commit:" + git commit -m "$message" + # Make the push quiet just in case there is anything that could + # leak sensitive information. + echo -e "\nPushing changes to simpeg/simpeg-doctest." + git push -fq origin gh-pages 2>&1 >/dev/null + echo -e "\nFinished uploading generated files." + displayName: Push documentation to simpeg-doctest + env: + GH_TOKEN: $(gh.token) + + - job: DeployDev + dependsOn: BuildDocs + condition: eq(variables['Build.Reason'], 'Schedule') # run only scheduled triggers + pool: + vmImage: ubuntu-latest + timeoutInMinutes: 240 + steps: + # Checkout simpeg repo. + # Sync tags and disable shallow depth to get the SimPEG version. + - checkout: self + fetchDepth: 0 + fetchTags: true + displayName: Checkout repository (including tags) + + - bash: | + git config --global user.name ${GH_NAME} + git config --global user.email ${GH_EMAIL} + git config --list | grep user. + displayName: "Configure git" + env: + GH_NAME: $(gh.name) + GH_EMAIL: $(gh.email) + + - bash: | + mkdir -p docs/_build/html + displayName: "Create directory for built docs" + + - task: DownloadPipelineArtifact@2 + inputs: + artifact: built-docs + targetPath: docs/_build/html + displayName: "Download docs artifact" + + # Upload dev build of the docs to a dev branch in simpeg/simpeg-doctest + # and update submodule in the gh-pages branch + - bash: | + # Push new docs + # ------------- + # Capture hash of last commit in simpeg + commit=$(git rev-parse --short HEAD) + # Clone the repo where we store the documentation (dev branch) + git clone -q --branch dev --depth 1 https://${GH_TOKEN}@github.com/simpeg/simpeg-doctest.git + cd simpeg-doctest + # Remove all files + shopt -s dotglob # configure bash to include dotfiles in * globs + export GLOBIGNORE=".git" # ignore .git directory in glob + git rm -rf * # remove all files + # Copy the built docs to the root of the repo + cp -r $BUILD_SOURCESDIRECTORY/docs/_build/html/* -t . + # Commit the new docs. Amend to avoid having a very large history. + git add . + message="Azure CI deploy dev from ${commit}" + echo -e "\nAmending last commit:" + git commit --amend --reset-author -m "$message" + # Make the push quiet just in case there is anything that could + # leak sensitive information. + echo -e "\nPushing changes to simpeg/simpeg-doctest (dev branch)." + git push -fq origin dev 2>&1 >/dev/null + echo -e "\nFinished uploading doc files." + + # Update submodule + # ---------------- + # Need to fetch the gh-pages branch first (because we clone with + # shallow depth) + git fetch --depth 1 origin gh-pages:gh-pages + # Switch to the gh-pages branch + git switch gh-pages + # Update the dev submodule + git submodule update --init --recursive --remote dev + # Commit changes + git add dev + message="Azure CI update dev submodule from ${commit}" + echo -e "\nMaking a new commit:" + git commit -m "$message" + # Make the push quiet just in case there is anything that could + # leak sensitive information. + echo -e "\nPushing changes to simpeg/simpeg-doctest (gh-pages branch)." + git push -q origin gh-pages 2>&1 >/dev/null + echo -e "\nFinished updating submodule dev." + + # Unset dotglob + shopt -u dotglob + export GLOBIGNORE="" + displayName: Push documentation to simpeg-doctest (dev branch) + env: + GH_TOKEN: $(gh.token) diff --git a/.ci/azure/old-docs.yml b/.ci/azure/old-docs.yml new file mode 100644 index 0000000000..cd3e35d77f --- /dev/null +++ b/.ci/azure/old-docs.yml @@ -0,0 +1,56 @@ +jobs: + - job: + displayName: Deploy Docs + pool: + vmImage: ubuntu-latest + variables: + python.version: "3.8" + timeoutInMinutes: 240 + steps: + # Checkout simpeg repo, including tags. + # We need to sync tags and disable shallow depth in order to get the + # SimPEG version while building the docs. + - checkout: self + fetchDepth: 0 + fetchTags: true + displayName: Checkout repository (including tags) + + - script: | + git config --global user.name ${GH_NAME} + git config --global user.email ${GH_EMAIL} + git config --list | grep user. + displayName: "Configure git" + env: + GH_NAME: $(gh.name) + GH_EMAIL: $(gh.email) + + - bash: echo "##vso[task.prependpath]$CONDA/bin" + displayName: Add conda to PATH + + - bash: .ci/azure/setup_env.sh + displayName: Setup SimPEG environment + + - script: | + source activate simpeg-test + cd docs + make html + cd .. + displayName: Building documentation + + # upload documentation to simpeg-docs gh-pages on tags + - script: | + git clone --depth 1 https://${GH_TOKEN}@github.com/simpeg/simpeg-docs.git + cd simpeg-docs + git gc --prune=now + git remote prune origin + rm -rf * + cp -r $BUILD_SOURCESDIRECTORY/docs/_build/html/* . + cp $BUILD_SOURCESDIRECTORY/docs/README.md . + touch .nojekyll + echo "docs.simpeg.xyz" >> CNAME + git add . + git commit -am "Azure CI commit ref $(Build.SourceVersion)" + git push + displayName: Push documentation to simpeg-docs + env: + GH_TOKEN: $(gh.token) diff --git a/.ci/azure/pypi.yml b/.ci/azure/pypi.yml new file mode 100644 index 0000000000..5abd0ad681 --- /dev/null +++ b/.ci/azure/pypi.yml @@ -0,0 +1,83 @@ +jobs: + - job: Build + pool: + vmImage: ubuntu-latest + steps: + # Checkout simpeg repo. + # Sync tags and disable shallow depth to get the SimPEG version. + - checkout: self + fetchDepth: 0 + fetchTags: true + displayName: "Checkout repository (including tags)" + + - task: UsePythonVersion@0 + inputs: + versionSpec: "3.9" + displayName: "Setup Python" + + - bash: | + pip install build twine + displayName: "Install build dependencies" + + - bash: | + # Change setuptools-scm local_scheme to "no-local-version" so the + # local part of the version isn't included, making the version string + # compatible with Test PyPI. Only do this when building for TestPyPI. + sed --in-place 's/node-and-date/no-local-version/g' pyproject.toml + condition: not(startsWith(variables['build.sourceBranch'], 'refs/tags/')) + displayName: "Configure local_scheme (except on release)" + + - bash: | + python -m build --sdist . + displayName: "Create source distribution for simpeg" + + - bash: | + twine check dist/* + displayName: "Check the source distribution" + + - task: PublishPipelineArtifact@1 + inputs: + targetPath: $(Build.SourcesDirectory)/dist + artifactName: pypi-dist + displayName: "Upload dist as artifact" + + - job: Deploy + dependsOn: Build + condition: or(startsWith(variables['build.sourceBranch'], 'refs/tags/'), eq(variables['build.sourceBranch'], 'refs/heads/main')) + pool: + vmImage: ubuntu-latest + steps: + - checkout: none + + - task: DownloadPipelineArtifact@2 + inputs: + artifact: pypi-dist + targetPath: dist + displayName: "Download dist artifact" + + - task: UsePythonVersion@0 + inputs: + versionSpec: "3.9" + displayName: "Setup Python" + + - bash: | + pip install twine + displayName: "Install twine" + + # Push to TestPyPI (only on push to main) + - bash: | + twine upload --repository testpypi dist/* + displayName: "Upload to TestPyPI" + condition: eq(variables['build.sourceBranch'], 'refs/heads/main') + env: + TWINE_USERNAME: $(twine.username) + TWINE_PASSWORD: $(test.twine.password) + + # Push to PyPI (only on release) + - bash: | + twine upload --skip-existing dist/* + displayName: "Upload to PyPI" + condition: startsWith(variables['build.sourceBranch'], 'refs/tags/') + env: + TWINE_USERNAME: $(twine.username) + TWINE_PASSWORD: $(twine.password) diff --git a/.ci/azure/style.yml b/.ci/azure/style.yml new file mode 100644 index 0000000000..59e6334475 --- /dev/null +++ b/.ci/azure/style.yml @@ -0,0 +1,41 @@ +jobs: + - job: + displayName: Run style checks with Black + pool: + vmImage: ubuntu-latest + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: "3.11" + - bash: .ci/install_style.sh + displayName: "Install dependencies to run the checks" + - script: make black + displayName: "Run black" + + - job: + displayName: Run (permissive) style checks with flake8 + pool: + vmImage: ubuntu-latest + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: "3.11" + - bash: .ci/install_style.sh + displayName: "Install dependencies to run the checks" + - script: make flake + displayName: "Run flake8" + + - job: + displayName: Run style checks with flake8 (allowed to fail) + pool: + vmImage: ubuntu-latest + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: "3.11" + - bash: .ci/install_style.sh + displayName: "Install dependencies to run the checks" + - script: make flake-all + displayName: "Run flake8" + env: + FLAKE8_OPTS: "--exit-zero" diff --git a/.ci/azure/matrix.yml b/.ci/azure/test.yml similarity index 100% rename from .ci/azure/matrix.yml rename to .ci/azure/test.yml diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 233a15a0e5..4cd7a06a20 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,319 +1,68 @@ +# ============================================= +# Configure Azure Pipelines for automated tasks +# ============================================= + +# Define triggers for runs in CI +# ------------------------------ trigger: branches: include: - - 'main' + - "main" exclude: - - '*no-ci*' + - "*no-ci*" tags: include: - - '*' - -schedules: -- cron: "0 8 * * *" # trigger cron job every day at 08:00 AM GMT - displayName: "Scheduled nightly job" - branches: - include: [ "main" ] - always: false # don't run if no changes have been applied since last sucessful run - batch: false # dont' run if last pipeline is still in-progress + - "*" pr: branches: include: - - '*' + - "*" exclude: - - '*no-ci*' - + - "*no-ci*" +schedules: + - cron: "0 8 * * *" # trigger cron job every day at 08:00 AM GMT + displayName: "Scheduled nightly job" + branches: + include: ["main"] + always: false # don't run if no changes have been applied since last sucessful run + batch: false # dont' run if last pipeline is still in-progress + +# Run stages +# ---------- stages: - -- stage: StyleChecks - displayName: "Style Checks" - jobs: - - job: - displayName: Run style checks with Black - pool: - vmImage: ubuntu-latest - steps: - - task: UsePythonVersion@0 - inputs: - versionSpec: "3.11" - - bash: .ci/install_style.sh - displayName: "Install dependencies to run the checks" - - script: make black - displayName: "Run black" - - - job: - displayName: Run (permissive) style checks with flake8 - pool: - vmImage: ubuntu-latest - steps: - - task: UsePythonVersion@0 - inputs: - versionSpec: "3.11" - - bash: .ci/install_style.sh - displayName: "Install dependencies to run the checks" - - script: make flake - displayName: "Run flake8" - - - job: - displayName: Run style checks with flake8 (allowed to fail) - pool: - vmImage: ubuntu-latest - steps: - - task: UsePythonVersion@0 - inputs: - versionSpec: "3.11" - - bash: .ci/install_style.sh - displayName: "Install dependencies to run the checks" - - script: make flake-all - displayName: "Run flake8" - env: - FLAKE8_OPTS: "--exit-zero" - -- stage: Testing - dependsOn: StyleChecks - jobs: - - template: ./.ci/azure/matrix.yml - -- stage: Deploy - dependsOn: Testing - condition: startsWith(variables['build.sourceBranch'], 'refs/tags/') - jobs: - - job: - displayName: Deploy Docs and source - pool: - vmImage: ubuntu-latest - variables: - python.version: '3.8' - timeoutInMinutes: 240 - steps: - - # Checkout simpeg repo, including tags. - # We need to sync tags and disable shallow depth in order to get the - # SimPEG version while building the docs. - - checkout: self - fetchDepth: 0 - fetchTags: true - displayName: Checkout repository (including tags) - - - script: | - git config --global user.name ${GH_NAME} - git config --global user.email ${GH_EMAIL} - git config --list | grep user. - displayName: 'Configure git' - env: - GH_NAME: $(gh.name) - GH_EMAIL: $(gh.email) - - - bash: echo "##vso[task.prependpath]$CONDA/bin" - displayName: Add conda to PATH - - - bash: .ci/azure/setup_env.sh - displayName: Setup SimPEG environment - - - script: | - source activate simpeg-test - python -m build --no-isolation --skip-dependency-check --sdist . - twine upload --skip-existing dist/* - displayName: Deploy source - env: - TWINE_USERNAME: $(twine.username) - TWINE_PASSWORD: $(twine.password) - - - script: | - source activate simpeg-test - cd docs - make html - cd .. - displayName: Building documentation - - # upload documentation to simpeg-docs gh-pages on tags - - script: | - git clone --depth 1 https://${GH_TOKEN}@github.com/simpeg/simpeg-docs.git - cd simpeg-docs - git gc --prune=now - git remote prune origin - rm -rf * - cp -r $BUILD_SOURCESDIRECTORY/docs/_build/html/* . - cp $BUILD_SOURCESDIRECTORY/docs/README.md . - touch .nojekyll - echo "docs.simpeg.xyz" >> CNAME - git add . - git commit -am "Azure CI commit ref $(Build.SourceVersion)" - git push - displayName: Push documentation to simpeg-docs - env: - GH_TOKEN: $(gh.token) - -- stage: Deploy_dev_docs_experimental - dependsOn: Testing - condition: eq(variables['Build.Reason'], 'Schedule') # run only scheduled triggers - jobs: - - job: - displayName: Deploy dev docs to simpeg-doctest (experimental) - pool: - vmImage: ubuntu-latest - variables: - python.version: '3.8' - timeoutInMinutes: 240 - steps: - - # Checkout simpeg repo, including tags. - # We need to sync tags and disable shallow depth in order to get the - # SimPEG version while building the docs. - - checkout: self - fetchDepth: 0 - fetchTags: true - displayName: Checkout repository (including tags) - - - bash: | - git config --global user.name ${GH_NAME} - git config --global user.email ${GH_EMAIL} - git config --list | grep user. - displayName: 'Configure git' - env: - GH_NAME: $(gh.name) - GH_EMAIL: $(gh.email) - - - bash: echo "##vso[task.prependpath]$CONDA/bin" - displayName: Add conda to PATH - - - bash: .ci/azure/setup_env.sh - displayName: Setup SimPEG environment - - - bash: | - source activate simpeg-test - make -C docs html - displayName: Building documentation - - # Upload dev build of the docs to a dev branch in simpeg/simpeg-doctest - # and update submodule in the gh-pages branch - - bash: | - # Push new docs - # ------------- - # Capture hash of last commit in simpeg - commit=$(git rev-parse --short HEAD) - # Clone the repo where we store the documentation (dev branch) - git clone -q --branch dev --depth 1 https://${GH_TOKEN}@github.com/simpeg/simpeg-doctest.git - cd simpeg-doctest - # Remove all files - shopt -s dotglob # configure bash to include dotfiles in * globs - export GLOBIGNORE=".git" # ignore .git directory in glob - git rm -rf * # remove all files - # Copy the built docs to the root of the repo - cp -r $BUILD_SOURCESDIRECTORY/docs/_build/html/* -t . - # Commit the new docs. Amend to avoid having a very large history. - git add . - message="Azure CI deploy dev from ${commit}" - echo -e "\nAmending last commit:" - git commit --amend --reset-author -m "$message" - # Make the push quiet just in case there is anything that could - # leak sensitive information. - echo -e "\nPushing changes to simpeg/simpeg-doctest (dev branch)." - git push -fq origin dev 2>&1 >/dev/null - echo -e "\nFinished uploading doc files." - - # Update submodule - # ---------------- - # Need to fetch the gh-pages branch first (because we clone with - # shallow depth) - git fetch --depth 1 origin gh-pages:gh-pages - # Switch to the gh-pages branch - git switch gh-pages - # Update the dev submodule - git submodule update --init --recursive --remote dev - # Commit changes - git add dev - message="Azure CI update dev submodule from ${commit}" - echo -e "\nMaking a new commit:" - git commit -m "$message" - # Make the push quiet just in case there is anything that could - # leak sensitive information. - echo -e "\nPushing changes to simpeg/simpeg-doctest (gh-pages branch)." - git push -q origin gh-pages 2>&1 >/dev/null - echo -e "\nFinished updating submodule dev." - - # Unset dotglob - shopt -u dotglob - export GLOBIGNORE="" - displayName: Push documentation to simpeg-doctest (dev branch) - env: - GH_TOKEN: $(gh.token) - -- stage: Deploy_release_docs_experimental - dependsOn: Testing - condition: startsWith(variables['build.sourceBranch'], 'refs/tags/') - jobs: - - job: - displayName: Deploy release docs to simpeg-doctest (experimental) - pool: - vmImage: ubuntu-latest - variables: - python.version: '3.8' - timeoutInMinutes: 240 - steps: - - # Checkout simpeg repo, including tags. - # We need to sync tags and disable shallow depth in order to get the - # SimPEG version while building the docs. - - checkout: self - fetchDepth: 0 - fetchTags: true - displayName: Checkout repository (including tags) - - - bash: | - git config --global user.name ${GH_NAME} - git config --global user.email ${GH_EMAIL} - git config --list | grep user. - displayName: 'Configure git' - env: - GH_NAME: $(gh.name) - GH_EMAIL: $(gh.email) - - - bash: echo "##vso[task.prependpath]$CONDA/bin" - displayName: Add conda to PATH - - - bash: .ci/azure/setup_env.sh - displayName: Setup SimPEG environment - - - bash: | - source activate simpeg-test - make -C docs html - displayName: Building documentation - - # Upload release build of the docs to gh-pages branch in simpeg/simpeg-doctest - - bash: | - # Capture version - # TODO: we should be able to get the version from the - # build.sourceBranch variable - version=$(git tag --points-at HEAD) - if [ -n "$version" ]; then - echo "Version could not be obtained from tag. Exiting." - exit 1 - fi - # Capture hash of last commit in simpeg - commit=$(git rev-parse --short HEAD) - # Clone the repo where we store the documentation - git clone -q --branch gh-pages --depth 1 https://${GH_TOKEN}@github.com/simpeg/simpeg-doctest.git - cd simpeg-doctest - # Move the built docs to a new dev folder - cp -r $BUILD_SOURCESDIRECTORY/docs/_build/html "$version" - cp $BUILD_SOURCESDIRECTORY/docs/README.md . - # Add .nojekyll if missing - touch .nojekyll - # Update latest symlink - rm -f latest - ln -s "$version" latest - # Commit the new docs. - git add "$version" README.md .nojekyll latest - message="Azure CI deploy ${version} from ${commit}" - echo -e "\nMaking a new commit:" - git commit -m "$message" - # Make the push quiet just in case there is anything that could - # leak sensitive information. - echo -e "\nPushing changes to simpeg/simpeg-doctest." - git push -fq origin gh-pages 2>&1 >/dev/null - echo -e "\nFinished uploading generated files." - displayName: Push documentation to simpeg-doctest - env: - GH_TOKEN: $(gh.token) \ No newline at end of file + - stage: StyleChecks + displayName: "Style Checks" + jobs: + - template: .ci/azure/style.yml + + - stage: Testing + dependsOn: StyleChecks + jobs: + - template: .ci/azure/test.yml + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # Keep old docs for now (we can remove them later in favor of Docs) + - stage: OldDocs + condition: startsWith(variables['build.sourceBranch'], 'refs/tags/') + dependsOn: + - StyleChecks + - Testing + jobs: + - template: .ci/azure/old-docs.yml + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + - stage: Docs + dependsOn: + - StyleChecks + - Testing + jobs: + - template: .ci/azure/docs.yml + + - stage: PyPI + dependsOn: + - StyleChecks + - Testing + jobs: + - template: .ci/azure/pypi.yml diff --git a/pyproject.toml b/pyproject.toml index e63ea662d9..671ff9416c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -97,6 +97,7 @@ include = ["simpeg*"] [tool.setuptools_scm] version_file = "simpeg/version.py" +local_scheme = "node-and-date" [tool.coverage.run] branch = true From 83183b4817f541f648c44d8c806d592961ff8f10 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 20 Jun 2024 13:47:22 -0700 Subject: [PATCH 033/194] Ignore `survey_type` argument in DC and SIP surveys (#1458) Ignore the `survey_type` argument in the dc and sip survey constructors and raise warning. Raise warning when trying to access or set the `survey_type` properties. The `survey_type` argument is not being used anymore and was a residue from an older implementation of the survey classes. Keeping it as part of the class creates confusion among users on how to properly use it. Update tests and examples that were still using that argument. Add tests to check if warnings are being raised when trying to access the `survey_type` property. Fixes #1432 --- .../static/resistivity/survey.py | 50 ++++++++++++----- .../spectral_induced_polarization/survey.py | 52 ++++++++++++------ .../static/utils/static_utils.py | 2 +- tests/em/static/test_DC_Utils.py | 2 - tests/em/static/test_dc_survey.py | 55 +++++++++++++++++++ tests/em/static/test_sip_survey.py | 34 ++++++++++++ tests/utils/test_io_utils.py | 4 +- tutorials/05-dcr/plot_fwd_2_dcr2d.py | 2 +- tutorials/06-ip/plot_fwd_2_dcip2d.py | 2 +- tutorials/06-ip/plot_fwd_3_dcip3d.py | 2 +- 10 files changed, 166 insertions(+), 39 deletions(-) create mode 100644 tests/em/static/test_dc_survey.py create mode 100644 tests/em/static/test_sip_survey.py diff --git a/simpeg/electromagnetics/static/resistivity/survey.py b/simpeg/electromagnetics/static/resistivity/survey.py index 8f2e438664..411f9ff7a6 100644 --- a/simpeg/electromagnetics/static/resistivity/survey.py +++ b/simpeg/electromagnetics/static/resistivity/survey.py @@ -1,3 +1,4 @@ +import warnings import numpy as np from ....utils.code_utils import validate_string @@ -19,20 +20,26 @@ class Survey(BaseSurvey): List of SimPEG DC/IP sources survey_geometry : {"surface", "borehole", "general"} Survey geometry. - survey_type : {"dipole-dipole", "pole-dipole", "dipole-pole", "pole-pole"} - Survey type. """ def __init__( self, source_list, survey_geometry="surface", - survey_type="dipole-dipole", **kwargs, ): + if (key := "survey_type") in kwargs: + warnings.warn( + f"Argument '{key}' is ignored and will be removed in future " + "versions of SimPEG. Types of sources and their corresponding " + "receivers are obtained from their respective classes, without " + "the need to specify the survey type.", + FutureWarning, + stacklevel=0, + ) + kwargs.pop(key) super(Survey, self).__init__(source_list, **kwargs) self.survey_geometry = survey_geometry - self.survey_type = survey_type @property def survey_geometry(self): @@ -56,29 +63,42 @@ def survey_geometry(self, var): @property def survey_type(self): - """Survey type; one of {"dipole-dipole", "pole-dipole", "dipole-pole", "pole-pole"} + """ + ``survey_type`` has been removed. + + Survey type; one of {"dipole-dipole", "pole-dipole", "dipole-pole", "pole-pole"} + + .. important: + + The `survey_type` property has been removed. Types of sources and + their corresponding receivers are obtained from their respective + classes, without the need to specify the survey type. Returns ------- str Survey type; one of {"dipole-dipole", "pole-dipole", "dipole-pole", "pole-pole"} """ - return self._survey_type + warnings.warn( + "Property 'survey_type' has been removed." + "Types of sources and their corresponding receivers are obtained from " + "their respective classes, without the need to specify the survey type.", + FutureWarning, + stacklevel=0, + ) @survey_type.setter def survey_type(self, var): - var = validate_string( - "survey_type", - var, - ("dipole-dipole", "pole-dipole", "dipole-pole", "pole-pole"), + warnings.warn( + "Property 'survey_type' has been removed." + "Types of sources and their corresponding receivers are obtained from " + "their respective classes, without the need to specify the survey type.", + FutureWarning, + stacklevel=0, ) - self._survey_type = var def __repr__(self): - return ( - f"{self.__class__.__name__}({self.survey_type}; " - f"#sources: {self.nSrc}; #data: {self.nD})" - ) + return f"{self.__class__.__name__}(#sources: {self.nSrc}; #data: {self.nD})" @property def locations_a(self): diff --git a/simpeg/electromagnetics/static/spectral_induced_polarization/survey.py b/simpeg/electromagnetics/static/spectral_induced_polarization/survey.py index 7039d4fb05..0826d84f4b 100644 --- a/simpeg/electromagnetics/static/spectral_induced_polarization/survey.py +++ b/simpeg/electromagnetics/static/spectral_induced_polarization/survey.py @@ -1,3 +1,4 @@ +import warnings from ....survey import BaseTimeSurvey from . import sources from . import receivers @@ -14,25 +15,27 @@ class Survey(BaseTimeSurvey): List of SimPEG spectral IP sources survey_geometry : {"surface", "borehole", "general"} Survey geometry. - survey_type : {"dipole-dipole", "pole-dipole", "dipole-pole", "pole-pole"} - Survey type. """ _n_pulse = 2 _T = 8.0 - def __init__( - self, - source_list=None, - survey_geometry="surface", - survey_type="dipole-dipole", - **kwargs, - ): + def __init__(self, source_list=None, survey_geometry="surface", **kwargs): + if (key := "survey_type") in kwargs: + warnings.warn( + f"Argument '{key}' is ignored and will be removed in future " + "versions of SimPEG. Types of sources and their corresponding " + "receivers are obtained from their respective classes, without " + "the need to specify the survey type.", + FutureWarning, + stacklevel=1, + ) + kwargs.pop(key) + if source_list is None: raise AttributeError("Survey cannot be instantiated without sources") super(Survey, self).__init__(source_list, **kwargs) self.survey_geometry = survey_geometry - self.survey_type = survey_type @property def n_pulse(self): @@ -75,21 +78,38 @@ def survey_geometry(self, var): @property def survey_type(self): - """Survey type; one of {"dipole-dipole", "pole-dipole", "dipole-pole", "pole-pole"} + """ + ``survey_type`` has been removed. + + Survey type; one of {"dipole-dipole", "pole-dipole", "dipole-pole", "pole-pole"} + + .. important: + + The `survey_type` property has been removed. Types of sources and + their corresponding receivers are obtained from their respective + classes, without the need to specify the survey type. Returns ------- str Survey type; one of {"dipole-dipole", "pole-dipole", "dipole-pole", "pole-pole"} """ - return self._survey_type + warnings.warn( + "Property 'survey_type' has been removed." + "Types of sources and their corresponding receivers are obtained from " + "their respective classes, without the need to specify the survey type.", + FutureWarning, + stacklevel=1, + ) @survey_type.setter def survey_type(self, var): - self._survey_type = validate_string( - "survey_type", - var, - ("dipole-dipole", "pole-dipole", "dipole-pole", "pole-pole"), + warnings.warn( + "Property 'survey_type' has been removed." + "Types of sources and their corresponding receivers are obtained from " + "their respective classes, without the need to specify the survey type.", + FutureWarning, + stacklevel=1, ) @property diff --git a/simpeg/electromagnetics/static/utils/static_utils.py b/simpeg/electromagnetics/static/utils/static_utils.py index a5fcaa7e52..7d9d43146c 100644 --- a/simpeg/electromagnetics/static/utils/static_utils.py +++ b/simpeg/electromagnetics/static/utils/static_utils.py @@ -1261,7 +1261,7 @@ def xy_2_r(x1, x2, y1, y2): srcClass = dc.Src.Dipole([rxClass], (endl[0, :]), (endl[1, :])) SrcList.append(srcClass) - survey = dc.Survey(SrcList, survey_type=survey_type) + survey = dc.Survey(SrcList) return survey diff --git a/tests/em/static/test_DC_Utils.py b/tests/em/static/test_DC_Utils.py index 5e98513a81..4c33717cd1 100644 --- a/tests/em/static/test_DC_Utils.py +++ b/tests/em/static/test_DC_Utils.py @@ -83,8 +83,6 @@ def test_io_rhoa(self): dim=self.mesh.dim, ) - self.assertEqual(survey_type, survey.survey_type) - # Setup Problem with exponential mapping expmap = maps.ExpMap(self.mesh) problem = dc.Simulation3DCellCentered( diff --git a/tests/em/static/test_dc_survey.py b/tests/em/static/test_dc_survey.py new file mode 100644 index 0000000000..20d7f8619a --- /dev/null +++ b/tests/em/static/test_dc_survey.py @@ -0,0 +1,55 @@ +""" +Tests for resistivity (DC) survey objects. +""" + +import pytest + +from simpeg.electromagnetics.static.resistivity import Survey +from simpeg.electromagnetics.static.resistivity import sources +from simpeg.electromagnetics.static.resistivity import receivers + + +class TestRemovedSourceType: + """ + Tests after removing the source_type argument and property. + """ + + def test_warning_after_argument(self): + """ + Test warning after passing source_type as argument to the constructor. + """ + msg = "Argument 'survey_type' is ignored and will be removed in future" + with pytest.warns(FutureWarning, match=msg): + survey = Survey(source_list=[], survey_type="dipole-dipole") + # Check if the object doesn't have a `_survey_type` attribute + assert not hasattr(survey, "_survey_type") + + def test_warning_removed_property(self): + """ + Test if warning is raised when accessing the survey_type property. + """ + survey = Survey(source_list=[]) + msg = "Property 'survey_type' has been removed." + with pytest.warns(FutureWarning, match=msg): + survey.survey_type + with pytest.warns(FutureWarning, match=msg): + survey.survey_type = "dipole-dipole" + + +def test_repr(): + """ + Test the __repr__ method of the survey. + """ + receivers_list = [ + receivers.Dipole( + locations_m=[[1, 2, 3], [4, 5, 6]], locations_n=[[7, 8, 9], [10, 11, 12]] + ) + ] + sources_list = [ + sources.Dipole( + receivers_list, location_a=[0.5, 1.5, 2.5], location_b=[4.5, 5.5, 6.5] + ) + ] + survey = Survey(source_list=sources_list) + expected_repr = "Survey(#sources: 1; #data: 2)" + assert repr(survey) == expected_repr diff --git a/tests/em/static/test_sip_survey.py b/tests/em/static/test_sip_survey.py new file mode 100644 index 0000000000..ff3ecc3b51 --- /dev/null +++ b/tests/em/static/test_sip_survey.py @@ -0,0 +1,34 @@ +""" +Tests for Spectral IP (SIP) survey objects. +""" + +import pytest + +from simpeg.electromagnetics.static.spectral_induced_polarization import Survey + + +class TestRemovedSourceType: + """ + Tests after removing the source_type argument and property. + """ + + def test_warning_after_argument(self): + """ + Test warning after passing source_type as argument to the constructor. + """ + msg = "Argument 'survey_type' is ignored and will be removed in future" + with pytest.warns(FutureWarning, match=msg): + survey = Survey(source_list=[], survey_type="dipole-dipole") + # Check if the object doesn't have a `_survey_type` attribute + assert not hasattr(survey, "_survey_type") + + def test_warning_removed_property(self): + """ + Test if warning is raised when accessing the survey_type property. + """ + survey = Survey(source_list=[]) + msg = "Property 'survey_type' has been removed." + with pytest.warns(FutureWarning, match=msg): + survey.survey_type + with pytest.warns(FutureWarning, match=msg): + survey.survey_type = "dipole-dipole" diff --git a/tests/utils/test_io_utils.py b/tests/utils/test_io_utils.py index 105ade7451..ef6fc2d473 100644 --- a/tests/utils/test_io_utils.py +++ b/tests/utils/test_io_utils.py @@ -400,8 +400,8 @@ def setUp(self): pp_sources.append(dc.sources.Pole(pp_receivers, a_loc)) dpdp_sources.append(dc.sources.Dipole(dpdp_receivers, a_loc, b_loc)) - self.pp_survey = dc.survey.Survey(pp_sources, survey_type="pole-pole") - self.dpdp_survey = dc.survey.Survey(dpdp_sources, survey_type="dipole-dipole") + self.pp_survey = dc.survey.Survey(pp_sources) + self.dpdp_survey = dc.survey.Survey(dpdp_sources) # Define data and uncertainties. In this case nD = 6 n_data = len(xa) * len(xm) diff --git a/tutorials/05-dcr/plot_fwd_2_dcr2d.py b/tutorials/05-dcr/plot_fwd_2_dcr2d.py index 8095504f90..8b88ec4ba7 100644 --- a/tutorials/05-dcr/plot_fwd_2_dcr2d.py +++ b/tutorials/05-dcr/plot_fwd_2_dcr2d.py @@ -102,7 +102,7 @@ ) # Define survey -survey = dc.survey.Survey(source_list, survey_type=survey_type) +survey = dc.survey.Survey(source_list) ############################################################### # Create Tree Mesh diff --git a/tutorials/06-ip/plot_fwd_2_dcip2d.py b/tutorials/06-ip/plot_fwd_2_dcip2d.py index 4e1add360e..a24187b473 100644 --- a/tutorials/06-ip/plot_fwd_2_dcip2d.py +++ b/tutorials/06-ip/plot_fwd_2_dcip2d.py @@ -320,7 +320,7 @@ ) # Define survey -ip_survey = ip.survey.Survey(source_list, survey_type=survey_type) +ip_survey = ip.survey.Survey(source_list) # Drape over discrete topography ip_survey.drape_electrodes_on_topography(mesh, ind_active, option="top") diff --git a/tutorials/06-ip/plot_fwd_3_dcip3d.py b/tutorials/06-ip/plot_fwd_3_dcip3d.py index dc5a07db96..96d57ba8e4 100644 --- a/tutorials/06-ip/plot_fwd_3_dcip3d.py +++ b/tutorials/06-ip/plot_fwd_3_dcip3d.py @@ -360,7 +360,7 @@ ) # Define survey -ip_survey = ip.survey.Survey(source_list, survey_type=survey_type) +ip_survey = ip.survey.Survey(source_list) # Drape to discretized topography as before ip_survey.drape_electrodes_on_topography(mesh, ind_active, option="top") From 637815ac8fc4ada17bf87e163c512f6a17309489 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 21 Jun 2024 09:11:47 -0700 Subject: [PATCH 034/194] Update deployment of docs to simpeg-docs (#1490) Remove the `old-docs.yml` Azure pipeline. Update the new pipelines to deploy docs so they push the changes to the `gh-pages` and `dev` branches in the `simpeg-docs` repo. Update the url for the `versions.json`. Move scripts that deploy the docs to actual bash scripts. Apply some improvements to the scripts and solve a few issues in them that weren't working as expected. --- .ci/azure/deploy-dev-docs.sh | 69 ++++++++++++++++++++++++ .ci/azure/deploy-release-docs.sh | 48 +++++++++++++++++ .ci/azure/docs.yml | 90 +++----------------------------- .ci/azure/old-docs.yml | 56 -------------------- azure-pipelines.yml | 11 ---- docs/_static/versions.json | 14 ++--- docs/conf.py | 2 +- 7 files changed, 131 insertions(+), 159 deletions(-) create mode 100755 .ci/azure/deploy-dev-docs.sh create mode 100755 .ci/azure/deploy-release-docs.sh delete mode 100644 .ci/azure/old-docs.yml diff --git a/.ci/azure/deploy-dev-docs.sh b/.ci/azure/deploy-dev-docs.sh new file mode 100755 index 0000000000..8aa097618c --- /dev/null +++ b/.ci/azure/deploy-dev-docs.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash + +# Push built docs to dev branch in simpeg-docs repository + +set -ex #echo on and exit if any line fails + +# --------------------------- +# Push new docs to dev branch +# --------------------------- +# Capture hash of last commit in simpeg +commit=$(git rev-parse --short HEAD) + +# Clone the repo where we store the documentation (dev branch) +git clone -q --branch dev --depth 1 "https://${GH_TOKEN}@github.com/simpeg/simpeg-docs.git" +cd simpeg-docs + +# Remove all files (but .git folder) +find . -not -path "./.git/*" -not -path "./.git" -delete + +# Copy the built docs to the root of the repo +cp -r "$BUILD_SOURCESDIRECTORY"/docs/_build/html/. -t . + +# Add new files +git add . + +# List files in working directory and show git status +ls -la +git status + +# Commit the new docs. Amend to avoid having a very large history. +message="Azure CI deploy dev from ${commit}" +echo -e "\nAmending last commit:" +git commit --amend --reset-author -m "$message" + +# Make the push quiet just in case there is anything that could +# leak sensitive information. +echo -e "\nPushing changes to simpeg/simpeg-docs (dev branch)." +git push -fq origin dev 2>&1 >/dev/null +echo -e "\nFinished uploading doc files." + +# ---------------- +# Update submodule +# ---------------- +# Need to fetch the gh-pages branch first (because we clone with shallow depth) +git fetch --depth 1 origin gh-pages:gh-pages + +# Switch to the gh-pages branch +git switch gh-pages + +# Update the dev submodule +git submodule update --init --recursive --remote dev + +# Add updated submodule +git add dev + +# List files in working directory and show git status +ls -la +git status + +# Commit changes +message="Azure CI update dev submodule from ${commit}" +echo -e "\nMaking a new commit:" +git commit -m "$message" + +# Make the push quiet just in case there is anything that could +# leak sensitive information. +echo -e "\nPushing changes to simpeg/simpeg-docs (gh-pages branch)." +git push -q origin gh-pages 2>&1 >/dev/null +echo -e "\nFinished updating submodule dev." diff --git a/.ci/azure/deploy-release-docs.sh b/.ci/azure/deploy-release-docs.sh new file mode 100755 index 0000000000..2f1650af1c --- /dev/null +++ b/.ci/azure/deploy-release-docs.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash + +# Push built docs to gh-pages branch in simpeg-docs repository + +set -ex #echo on and exit if any line fails + +# Capture simpeg version +version=$(git tag --points-at HEAD) +if [[ -z $version ]]; then +echo "Version could not be obtained from tag. Exiting." +exit 1 +fi + +# Capture hash of last commit in simpeg +commit=$(git rev-parse --short HEAD) + +# Clone the repo where we store the documentation +git clone -q --branch gh-pages --depth 1 "https://${GH_TOKEN}@github.com/simpeg/simpeg-docs.git" +cd simpeg-docs + +# Move the built docs to a new dev folder +cp -r "$BUILD_SOURCESDIRECTORY/docs/_build/html" "$version" +cp "$BUILD_SOURCESDIRECTORY/docs/README.md" . + +# Add .nojekyll if missing +touch .nojekyll + +# Update latest symlink +rm -f latest +ln -s "$version" latest + +# Add new docs and relevant files +git add "$version" README.md .nojekyll latest + +# List files in working directory and show git status +ls -la +git status + +# Commit the new docs. +message="Azure CI deploy ${version} from ${commit}" +echo -e "\nMaking a new commit:" +git commit -m "$message" + +# Make the push quiet just in case there is anything that could +# leak sensitive information. +echo -e "\nPushing changes to simpeg/simpeg-docs." +git push -fq origin gh-pages 2>&1 >/dev/null +echo -e "\nFinished uploading generated files." diff --git a/.ci/azure/docs.yml b/.ci/azure/docs.yml index bb3a64a45f..7f1918386d 100644 --- a/.ci/azure/docs.yml +++ b/.ci/azure/docs.yml @@ -65,40 +65,9 @@ jobs: targetPath: docs/_build/html displayName: "Download docs artifact" - # Upload release build of the docs to gh-pages branch in simpeg/simpeg-doctest - - bash: | - # Capture version - # TODO: we should be able to get the version from the - # build.sourceBranch variable - version=$(git tag --points-at HEAD) - if [ -n "$version" ]; then - echo "Version could not be obtained from tag. Exiting." - exit 1 - fi - # Capture hash of last commit in simpeg - commit=$(git rev-parse --short HEAD) - # Clone the repo where we store the documentation - git clone -q --branch gh-pages --depth 1 https://${GH_TOKEN}@github.com/simpeg/simpeg-doctest.git - cd simpeg-doctest - # Move the built docs to a new dev folder - cp -r $BUILD_SOURCESDIRECTORY/docs/_build/html "$version" - cp $BUILD_SOURCESDIRECTORY/docs/README.md . - # Add .nojekyll if missing - touch .nojekyll - # Update latest symlink - rm -f latest - ln -s "$version" latest - # Commit the new docs. - git add "$version" README.md .nojekyll latest - message="Azure CI deploy ${version} from ${commit}" - echo -e "\nMaking a new commit:" - git commit -m "$message" - # Make the push quiet just in case there is anything that could - # leak sensitive information. - echo -e "\nPushing changes to simpeg/simpeg-doctest." - git push -fq origin gh-pages 2>&1 >/dev/null - echo -e "\nFinished uploading generated files." - displayName: Push documentation to simpeg-doctest + # Upload release build of the docs to gh-pages branch in simpeg/simpeg-docs + - bash: .ci/azure/deploy-release-docs.sh + displayName: Push documentation to simpeg-docs env: GH_TOKEN: $(gh.token) @@ -135,56 +104,9 @@ jobs: targetPath: docs/_build/html displayName: "Download docs artifact" - # Upload dev build of the docs to a dev branch in simpeg/simpeg-doctest + # Upload dev build of the docs to a dev branch in simpeg/simpeg-docs # and update submodule in the gh-pages branch - - bash: | - # Push new docs - # ------------- - # Capture hash of last commit in simpeg - commit=$(git rev-parse --short HEAD) - # Clone the repo where we store the documentation (dev branch) - git clone -q --branch dev --depth 1 https://${GH_TOKEN}@github.com/simpeg/simpeg-doctest.git - cd simpeg-doctest - # Remove all files - shopt -s dotglob # configure bash to include dotfiles in * globs - export GLOBIGNORE=".git" # ignore .git directory in glob - git rm -rf * # remove all files - # Copy the built docs to the root of the repo - cp -r $BUILD_SOURCESDIRECTORY/docs/_build/html/* -t . - # Commit the new docs. Amend to avoid having a very large history. - git add . - message="Azure CI deploy dev from ${commit}" - echo -e "\nAmending last commit:" - git commit --amend --reset-author -m "$message" - # Make the push quiet just in case there is anything that could - # leak sensitive information. - echo -e "\nPushing changes to simpeg/simpeg-doctest (dev branch)." - git push -fq origin dev 2>&1 >/dev/null - echo -e "\nFinished uploading doc files." - - # Update submodule - # ---------------- - # Need to fetch the gh-pages branch first (because we clone with - # shallow depth) - git fetch --depth 1 origin gh-pages:gh-pages - # Switch to the gh-pages branch - git switch gh-pages - # Update the dev submodule - git submodule update --init --recursive --remote dev - # Commit changes - git add dev - message="Azure CI update dev submodule from ${commit}" - echo -e "\nMaking a new commit:" - git commit -m "$message" - # Make the push quiet just in case there is anything that could - # leak sensitive information. - echo -e "\nPushing changes to simpeg/simpeg-doctest (gh-pages branch)." - git push -q origin gh-pages 2>&1 >/dev/null - echo -e "\nFinished updating submodule dev." - - # Unset dotglob - shopt -u dotglob - export GLOBIGNORE="" - displayName: Push documentation to simpeg-doctest (dev branch) + - bash: .ci/azure/deploy-dev-docs.sh + displayName: Push documentation to simpeg-docs (dev branch) env: GH_TOKEN: $(gh.token) diff --git a/.ci/azure/old-docs.yml b/.ci/azure/old-docs.yml deleted file mode 100644 index cd3e35d77f..0000000000 --- a/.ci/azure/old-docs.yml +++ /dev/null @@ -1,56 +0,0 @@ -jobs: - - job: - displayName: Deploy Docs - pool: - vmImage: ubuntu-latest - variables: - python.version: "3.8" - timeoutInMinutes: 240 - steps: - # Checkout simpeg repo, including tags. - # We need to sync tags and disable shallow depth in order to get the - # SimPEG version while building the docs. - - checkout: self - fetchDepth: 0 - fetchTags: true - displayName: Checkout repository (including tags) - - - script: | - git config --global user.name ${GH_NAME} - git config --global user.email ${GH_EMAIL} - git config --list | grep user. - displayName: "Configure git" - env: - GH_NAME: $(gh.name) - GH_EMAIL: $(gh.email) - - - bash: echo "##vso[task.prependpath]$CONDA/bin" - displayName: Add conda to PATH - - - bash: .ci/azure/setup_env.sh - displayName: Setup SimPEG environment - - - script: | - source activate simpeg-test - cd docs - make html - cd .. - displayName: Building documentation - - # upload documentation to simpeg-docs gh-pages on tags - - script: | - git clone --depth 1 https://${GH_TOKEN}@github.com/simpeg/simpeg-docs.git - cd simpeg-docs - git gc --prune=now - git remote prune origin - rm -rf * - cp -r $BUILD_SOURCESDIRECTORY/docs/_build/html/* . - cp $BUILD_SOURCESDIRECTORY/docs/README.md . - touch .nojekyll - echo "docs.simpeg.xyz" >> CNAME - git add . - git commit -am "Azure CI commit ref $(Build.SourceVersion)" - git push - displayName: Push documentation to simpeg-docs - env: - GH_TOKEN: $(gh.token) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 4cd7a06a20..f755b7f443 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -42,17 +42,6 @@ stages: jobs: - template: .ci/azure/test.yml - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # Keep old docs for now (we can remove them later in favor of Docs) - - stage: OldDocs - condition: startsWith(variables['build.sourceBranch'], 'refs/tags/') - dependsOn: - - StyleChecks - - Testing - jobs: - - template: .ci/azure/old-docs.yml - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - stage: Docs dependsOn: - StyleChecks diff --git a/docs/_static/versions.json b/docs/_static/versions.json index 265c9718fa..4fe5c5dfea 100644 --- a/docs/_static/versions.json +++ b/docs/_static/versions.json @@ -1,31 +1,31 @@ [ { "version": "dev", - "url": "https://doctest.simpeg.xyz/dev/" + "url": "https://docs.simpeg.xyz/dev/" }, { "name": "v0.21.1 (latest)", "version": "v0.21.1", - "url": "https://doctest.simpeg.xyz/v0.21.1/" + "url": "https://docs.simpeg.xyz/v0.21.1/" }, { "version": "v0.21.0", - "url": "https://doctest.simpeg.xyz/v0.21.0/" + "url": "https://docs.simpeg.xyz/v0.21.0/" }, { "version": "v0.20.0", - "url": "https://doctest.simpeg.xyz/v0.20.0/" + "url": "https://docs.simpeg.xyz/v0.20.0/" }, { "version": "v0.19.0", - "url": "https://doctest.simpeg.xyz/v0.19.0/" + "url": "https://docs.simpeg.xyz/v0.19.0/" }, { "version": "v0.18.1", - "url": "https://doctest.simpeg.xyz/v0.18.1/" + "url": "https://docs.simpeg.xyz/v0.18.1/" }, { "version": "v0.18.0", - "url": "https://doctest.simpeg.xyz/v0.18.0/" + "url": "https://docs.simpeg.xyz/v0.18.0/" } ] diff --git a/docs/conf.py b/docs/conf.py index 8a50f66cd8..fd2220f41e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -293,7 +293,7 @@ def linkcode_resolve(domain, info): # Configure version switcher "switcher": { "version_match": switcher_version, - "json_url": "https://doctest.simpeg.xyz/latest/_static/versions.json", + "json_url": "https://docs.simpeg.xyz/latest/_static/versions.json", }, } From e322585e74549a0865e8da76ccd568ccbeb1551f Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 21 Jun 2024 09:16:22 -0700 Subject: [PATCH 035/194] Change cron time to test new deployment of docs (#1492) This commit should be reverted after checking that everything works fine. --- azure-pipelines.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index f755b7f443..03542bfb61 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -22,7 +22,8 @@ pr: - "*no-ci*" schedules: - - cron: "0 8 * * *" # trigger cron job every day at 08:00 AM GMT + # - cron: "0 8 * * *" # trigger cron job every day at 08:00 AM GMT + - cron: "30 16 * * *" displayName: "Scheduled nightly job" branches: include: ["main"] From 31fb6feee65e34ec9bf4da0615d6d603e8d99329 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 21 Jun 2024 12:50:16 -0700 Subject: [PATCH 036/194] Revert "Change cron time to test new deployment of docs (#1492)" (#1494) This reverts commit e322585e74549a0865e8da76ccd568ccbeb1551f. --- azure-pipelines.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 03542bfb61..f755b7f443 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -22,8 +22,7 @@ pr: - "*no-ci*" schedules: - # - cron: "0 8 * * *" # trigger cron job every day at 08:00 AM GMT - - cron: "30 16 * * *" + - cron: "0 8 * * *" # trigger cron job every day at 08:00 AM GMT displayName: "Scheduled nightly job" branches: include: ["main"] From f23c88446ad64fcbcba674a44d395e05ce888357 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 24 Jun 2024 12:23:14 -0700 Subject: [PATCH 037/194] Fix F821 flake error: undefined variable (#1487) Remove the `F821` warning from the ignored flake8 warning lists. Solve a few undefined variables across the repository. Remove the unused `DerivProjfieldsTest` test function. --------- Co-authored-by: Lindsey Heagy --- .../20-published/plot_heagyetal2017_casing.py | 6 ++-- pyproject.toml | 4 +-- .../analytics/FDEMDipolarfields.py | 5 +-- .../electromagnetics/analytics/FDEMcasing.py | 3 +- .../electromagnetics/natural_source/fields.py | 2 +- .../electromagnetics/natural_source/survey.py | 17 ++++++---- .../natural_source/utils/plot_utils.py | 2 +- .../natural_source/utils/source_utils.py | 4 +-- .../spectral_induced_polarization/data.py | 1 + tests/base/test_Fields.py | 7 +---- .../nsem/inversion/test_Problem3D_Derivs.py | 31 +------------------ tests/em/tdem/test_TDEM_crosscheck.py | 5 +-- 12 files changed, 28 insertions(+), 59 deletions(-) diff --git a/examples/20-published/plot_heagyetal2017_casing.py b/examples/20-published/plot_heagyetal2017_casing.py index e9f817c04d..fda876b955 100644 --- a/examples/20-published/plot_heagyetal2017_casing.py +++ b/examples/20-published/plot_heagyetal2017_casing.py @@ -935,8 +935,8 @@ def plotJ( from matplotlib.colors import LogNorm f = ax.contourf( - rx_x, - rx_y, + self.rx_x, + self.rx_y, np.absolute(Jv), num, cmap=plt.get_cmap("viridis"), @@ -950,7 +950,7 @@ def plotJ( if plotGrid: self.meshs.plot_slice( - np.nan * np.ones(mesh.nC), normal="Z", grid=True, ax=ax + np.nan * np.ones(self.meshs.nC), normal="Z", grid=True, ax=ax ) if xlim is not None: diff --git a/pyproject.toml b/pyproject.toml index 671ff9416c..6a8926907a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -213,8 +213,6 @@ ignore = [ 'D419', # module level import not at top of file 'E402', - # undefined name %r - 'F821', # Block quote ends without a blank line; unexpected unindent. 'RST201', # Definition list ends without a blank line; unexpected unindent. @@ -249,4 +247,4 @@ rst-roles = [ 'mod', 'meth', 'ref', -] \ No newline at end of file +] diff --git a/simpeg/electromagnetics/analytics/FDEMDipolarfields.py b/simpeg/electromagnetics/analytics/FDEMDipolarfields.py index bfdfee7748..62f6e8d1eb 100644 --- a/simpeg/electromagnetics/analytics/FDEMDipolarfields.py +++ b/simpeg/electromagnetics/analytics/FDEMDipolarfields.py @@ -199,7 +199,7 @@ def J_galvanic_from_ElectricDipoleWholeSpace( Add description of parameters """ - Ex_galvanic, Ey_galvanic, Ez_galvanic = E_galvanic_from_ElectricDipoleWholeSpaced( + Ex_galvanic, Ey_galvanic, Ez_galvanic = E_galvanic_from_ElectricDipoleWholeSpace( XYZ, srcLoc, sig, @@ -229,7 +229,7 @@ def J_inductive_from_ElectricDipoleWholeSpace( Ex_inductive, Ey_inductive, Ez_inductive, - ) = E_inductive_from_ElectricDipoleWholeSpaced( + ) = E_inductive_from_ElectricDipoleWholeSpace( XYZ, srcLoc, sig, @@ -318,6 +318,7 @@ def B_from_ElectricDipoleWholeSpace( kappa=kappa, epsr=epsr, ) + mu = mu_0 * (1 + kappa) Bx = mu * Hx By = mu * Hy Bz = mu * Hz diff --git a/simpeg/electromagnetics/analytics/FDEMcasing.py b/simpeg/electromagnetics/analytics/FDEMcasing.py index 0196d1277b..1510a6a2e9 100644 --- a/simpeg/electromagnetics/analytics/FDEMcasing.py +++ b/simpeg/electromagnetics/analytics/FDEMcasing.py @@ -120,9 +120,10 @@ def getCasingEphiMagDipole( srcloc, obsloc, freq, sigma, a, b, mu=(mu_0, mu_0, mu_0), eps=epsilon_0, moment=1.0 ): mu = np.asarray(mu) + omega = 2 * np.pi * freq return ( 1j - * omega(freq) + * omega * mu * _getCasingHertzMagDipoleDeriv_r( srcloc, obsloc, freq, sigma, a, b, mu, eps, moment diff --git a/simpeg/electromagnetics/natural_source/fields.py b/simpeg/electromagnetics/natural_source/fields.py index 2493c7f0bd..3ba8507807 100644 --- a/simpeg/electromagnetics/natural_source/fields.py +++ b/simpeg/electromagnetics/natural_source/fields.py @@ -681,7 +681,7 @@ def _b_pyDeriv(self, src, du_dm_v, adjoint=False): # Primary does not depend on u return np.array( self._b_pyDeriv_u(src, du_dm_v, adjoint) - + self._b_pyDeriv_m(src, v, adjoint), + + self._b_pyDeriv_m(src, du_dm_v, adjoint), complex, ) diff --git a/simpeg/electromagnetics/natural_source/survey.py b/simpeg/electromagnetics/natural_source/survey.py index 935316a955..0c001fa704 100644 --- a/simpeg/electromagnetics/natural_source/survey.py +++ b/simpeg/electromagnetics/natural_source/survey.py @@ -141,9 +141,11 @@ def fromRecArray(cls, recArray, srcType="primary"): Parameters ---------- recArray : numpy.ndarray - Record array with the data. Has to have ('freq','x','y','z') columns and some ('zxx','zxy','zyx','zyy','tzx','tzy') - srcType : str, default: "primary" - The type of simpeg.EM.NSEM.SrcNSEM to be used. Either "primary" or "total" + Record array with the data. Has to have ('freq','x','y','z') + columns and some ('zxx','zxy','zyx','zyy','tzx','tzy'). + srcType : {"primary"} + The type of simpeg.EM.NSEM.SrcNSEM to be used. It currently accepts + only ``"primary"``. Returns ------- @@ -152,10 +154,13 @@ def fromRecArray(cls, recArray, srcType="primary"): """ if srcType == "primary": src = PlanewaveXYPrimary - elif srcType == "total": - src = Planewave_xy_1DhomotD + # TODO: implement total field formulation + # elif srcType == "total": + # src = Planewave_xy_1DhomotD else: - raise NotImplementedError("{:s} is not a valid source type for NSEMdata") + raise NotImplementedError( + f"Invalid srcType '{srcType}'. " "Only 'primary' is supported." + ) # Find all the frequencies in recArray uniFreq = np.unique(recArray["freq"].copy()) diff --git a/simpeg/electromagnetics/natural_source/utils/plot_utils.py b/simpeg/electromagnetics/natural_source/utils/plot_utils.py index 81c6c6b140..261d80591a 100644 --- a/simpeg/electromagnetics/natural_source/utils/plot_utils.py +++ b/simpeg/electromagnetics/natural_source/utils/plot_utils.py @@ -691,7 +691,7 @@ def _get_map_data(data, frequency, orientation, component, plot_error=False): else: if plot_error: freqs, plot_data, std_data, floor_data = _extract_frequency_data( - data, frequency, orientation, component, return_uncert=error + data, frequency, orientation, component, return_uncert=True ) attr_uncert = std_data * np.abs(plot_data) + floor_data errorbars = [attr_uncert, attr_uncert] diff --git a/simpeg/electromagnetics/natural_source/utils/source_utils.py b/simpeg/electromagnetics/natural_source/utils/source_utils.py index 7687ff5894..822c04d2c5 100644 --- a/simpeg/electromagnetics/natural_source/utils/source_utils.py +++ b/simpeg/electromagnetics/natural_source/utils/source_utils.py @@ -43,7 +43,7 @@ def homo1DModelSource(mesh, freq, sigma_1d): for i in np.arange(mesh.vnEy[0]): ey_py[i, :] = e0_1d # ey_py[1:-1, 1:-1, 1:-1] = 0 - eBG_py = np.vstack((ex_py, mkvc(ey_py, 2), ez_py)) + eBG_py = np.vstack((ex_py, mkvc(ey_py, 2))) elif mesh.dim == 3: # us the z component of ex_grid as lookup for solution edges_u, inv_edges = np.unique(mesh.gridEx[:, -1], return_inverse=True) @@ -113,7 +113,7 @@ def analytic1DModelSource(mesh, freq, sigma_1d): for i in np.arange(mesh.vnEy[0]): ey_py[i, :] = e0_1d # ey_py[1:-1, 1:-1, 1:-1] = 0 - eBG_py = np.vstack((ex_py, mkvc(ey_py, 2), ez_py)) + eBG_py = np.vstack((ex_py, mkvc(ey_py, 2))) elif mesh.dim == 3: # Setup x (east) polarization (_x) ex_px = -np.array([E1dFieldDict[i] for i in mesh.gridEx[:, 2]]).reshape(-1, 1) diff --git a/simpeg/electromagnetics/static/spectral_induced_polarization/data.py b/simpeg/electromagnetics/static/spectral_induced_polarization/data.py index 8535915679..f84e0185ee 100644 --- a/simpeg/electromagnetics/static/spectral_induced_polarization/data.py +++ b/simpeg/electromagnetics/static/spectral_induced_polarization/data.py @@ -1,4 +1,5 @@ import numpy as np +from discretize.utils import mkvc from ....data import Data as BaseData diff --git a/tests/base/test_Fields.py b/tests/base/test_Fields.py index b9450bfc5f..99fbf6c01c 100644 --- a/tests/base/test_Fields.py +++ b/tests/base/test_Fields.py @@ -4,14 +4,9 @@ from simpeg import survey, simulation, utils, fields, data import numpy as np -import sys np.random.seed(32) - -if sys.version_info < (3,): - zero_types = [0, 0.0, np.r_[0], long(0)] -else: - zero_types = [0, 0.0, np.r_[0]] +zero_types = [0, 0.0, np.r_[0]] class FieldsTest(unittest.TestCase): diff --git a/tests/em/nsem/inversion/test_Problem3D_Derivs.py b/tests/em/nsem/inversion/test_Problem3D_Derivs.py index 9d4b332a06..05f8acfa95 100644 --- a/tests/em/nsem/inversion/test_Problem3D_Derivs.py +++ b/tests/em/nsem/inversion/test_Problem3D_Derivs.py @@ -2,7 +2,7 @@ import pytest import unittest import numpy as np -from simpeg import tests, mkvc +from simpeg import tests from simpeg.electromagnetics import natural_source as nsem from scipy.constants import mu_0 @@ -91,35 +91,6 @@ def fun(x): return tests.check_derivative(fun, m, num=3, plotIt=False, eps=FLR) -def DerivProjfieldsTest(inputSetup, comp="All", freq=False): - survey, simulation = nsem.utils.test_utils.setupSimpegNSEM_ePrimSec( - inputSetup, comp, freq - ) - print("Derivative test of data projection for eFormulation primary/secondary\n") - # simulation.mapping = Maps.ExpMap(simulation.mesh) - # Initate things for the derivs Test - src = survey.source_list[0] - np.random.seed(1983) - u0x = np.random.randn(survey.mesh.nE) + np.random.randn(survey.mesh.nE) * 1j - u0y = np.random.randn(survey.mesh.nE) + np.random.randn(survey.mesh.nE) * 1j - u0 = np.vstack((mkvc(u0x, 2), mkvc(u0y, 2))) - f0 = simulation.fieldsPair(survey.mesh, survey) - # u0 = np.hstack((mkvc(u0_px,2),mkvc(u0_py,2))) - f0[src, "e_pxSolution"] = u0[: len(u0) / 2] # u0x - f0[src, "e_pySolution"] = u0[len(u0) / 2 : :] # u0y - - def fun(u): - f = simulation.fieldsPair(survey.mesh, survey) - f[src, "e_pxSolution"] = u[: len(u) / 2] - f[src, "e_pySolution"] = u[len(u) / 2 : :] - return ( - rx.eval(src, survey.mesh, f), - lambda t: rx.evalDeriv(src, survey.mesh, f0, mkvc(t, 2)), - ) - - return tests.check_derivative(fun, u0, num=3, plotIt=False, eps=FLR) - - class NSEM_DerivTests(unittest.TestCase): def setUp(self): pass diff --git a/tests/em/tdem/test_TDEM_crosscheck.py b/tests/em/tdem/test_TDEM_crosscheck.py index bb029d2085..6792b39bbc 100644 --- a/tests/em/tdem/test_TDEM_crosscheck.py +++ b/tests/em/tdem/test_TDEM_crosscheck.py @@ -3,7 +3,6 @@ from simpeg import maps from simpeg.electromagnetics import time_domain as tdem -from simpeg.electromagnetics import utils import numpy as np from pymatsolver import Pardiso as Solver @@ -41,10 +40,8 @@ def setUp_TDEM( rxtimes = np.logspace(-4, -3, 20) if waveform.upper() == "RAW": - out = utils.VTEMFun(prb.times, 0.00595, 0.006, 100) - wavefun = interp1d(prb.times, out) t0 = 0.006 - waveform = tdem.Src.RawWaveform(off_time=t0, waveform_function=wavefun) + waveform = tdem.sources.VTEMWaveform(off_time=t0) time_steps = [(1e-3, 5), (1e-4, 5), (5e-5, 10), (5e-5, 10), (1e-4, 10)] rxtimes = t0 + rxtimes From fb10cda8c87accce46320de4789fe21c6db81eca Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 24 Jun 2024 14:21:21 -0700 Subject: [PATCH 038/194] Use Numpy rng in viscous remanent mag tests (#1453) Replace the usage of the deprecated functions in `numpy.random` module for the Numpy's random number generator class and its methods, in most of the Viscous Remanent Magnetization tests. Part of the solution to #1289 --- tests/em/vrm/test_vrmfwd.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/tests/em/vrm/test_vrmfwd.py b/tests/em/vrm/test_vrmfwd.py index 5e45cdcb6f..ea751f0afe 100644 --- a/tests/em/vrm/test_vrmfwd.py +++ b/tests/em/vrm/test_vrmfwd.py @@ -13,8 +13,6 @@ class VRM_fwd_tests(unittest.TestCase): seed = 518936 def test_predict_dipolar(self): - np.random.seed(self.seed) - h = [0.05, 0.05] meshObj = discretize.TensorMesh((h, h, h), x0="CCC") @@ -26,8 +24,9 @@ def test_predict_dipolar(self): times = np.logspace(-4, -2, 3) waveObj = vrm.waveforms.SquarePulse(delt=0.02) - phi = np.random.uniform(-np.pi, np.pi) - psi = np.random.uniform(-np.pi, np.pi) + rng = np.random.default_rng(seed=self.seed) + phi = rng.uniform(low=-np.pi, high=np.pi) + psi = rng.uniform(low=-np.pi, high=np.pi) R = 2.0 loc_rx = ( R * np.c_[np.sin(phi) * np.cos(psi), np.sin(phi) * np.sin(psi), np.cos(phi)] @@ -43,8 +42,9 @@ def test_predict_dipolar(self): vrm.receivers.Point(loc_rx, times=times, field_type="dhdt", orientation="z") ) - alpha = np.random.uniform(0, np.pi) - beta = np.random.uniform(-np.pi, np.pi) + rng = np.random.default_rng(seed=self.seed) + alpha = rng.uniform(low=0, high=np.pi) + beta = rng.uniform(low=-np.pi, high=np.pi) loc_tx = [0.0, 0.0, 0.0] Src = vrm.sources.CircLoop( receiver_list, loc_tx, 25.0, np.r_[alpha, beta], 1.0, waveObj @@ -94,8 +94,6 @@ def test_sources(self): computed. """ - np.random.seed(self.seed) - h = [0.5, 0.5] meshObj = discretize.TensorMesh((h, h, h), x0="CCC") @@ -107,8 +105,9 @@ def test_sources(self): times = np.logspace(-4, -2, 3) waveObj = vrm.waveforms.SquarePulse(delt=0.02) - phi = np.random.uniform(-np.pi, np.pi) - psi = np.random.uniform(-np.pi, np.pi) + rng = np.random.default_rng(seed=self.seed) + phi = rng.uniform(low=-np.pi, high=np.pi) + psi = rng.uniform(low=-np.pi, high=np.pi) Rrx = 3.0 loc_rx = ( Rrx @@ -125,8 +124,9 @@ def test_sources(self): vrm.receivers.Point(loc_rx, times=times, field_type="dhdt", orientation="z") ) - alpha = np.random.uniform(0, np.pi) - beta = np.random.uniform(-np.pi, np.pi) + rng = np.random.default_rng(seed=self.seed) + alpha = rng.uniform(low=0, high=np.pi) + beta = rng.uniform(low=-np.pi, high=np.pi) Rtx = 4.0 loc_tx = ( Rtx @@ -424,8 +424,6 @@ def test_receiver_types(self): are correct. """ - np.random.seed(self.seed) - h1 = [0.25, 0.25] meshObj_Tensor = discretize.TensorMesh((h1, h1, h1), x0="CCN") @@ -439,8 +437,9 @@ def test_receiver_types(self): times = np.array([1e-3]) waveObj = vrm.waveforms.SquarePulse(delt=0.02) - phi = np.random.uniform(-np.pi, np.pi) - psi = np.random.uniform(-np.pi, np.pi) + rng = np.random.default_rng(seed=self.seed) + phi = rng.uniform(low=-np.pi, high=np.pi) + psi = rng.uniform(low=-np.pi, high=np.pi) R = 5.0 loc_rx = ( R * np.c_[np.sin(phi) * np.cos(psi), np.sin(phi) * np.sin(psi), np.cos(phi)] From 7f86584c8c7e404e1393b289d8824cafc2c0b73a Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 24 Jun 2024 15:50:46 -0700 Subject: [PATCH 039/194] Use Numpy rng in NSEM tests (#1450) Replace the usage of the deprecated functions in `numpy.random` module for the Numpy's random number generator class and its methods, in most of the NSEM tests. Part of the solution to #1289 --- .../forward/test_Problem3D_VsAnalyticSolution.py | 2 -- tests/em/nsem/inversion/test_BC_Sims.py | 15 ++++++++++----- .../em/nsem/inversion/test_Problem1D_Adjoint.py | 16 ++++++---------- tests/em/nsem/inversion/test_Problem1D_Derivs.py | 4 ++-- .../em/nsem/inversion/test_Problem3D_Adjoint.py | 11 ++++------- tests/em/nsem/inversion/test_Problem3D_Derivs.py | 1 + .../nsem/inversion/test_complex_resistivity.py | 6 ++++-- 7 files changed, 27 insertions(+), 28 deletions(-) diff --git a/tests/em/nsem/forward/test_Problem3D_VsAnalyticSolution.py b/tests/em/nsem/forward/test_Problem3D_VsAnalyticSolution.py index 5086c62d43..5300d49273 100644 --- a/tests/em/nsem/forward/test_Problem3D_VsAnalyticSolution.py +++ b/tests/em/nsem/forward/test_Problem3D_VsAnalyticSolution.py @@ -5,8 +5,6 @@ from simpeg.electromagnetics import natural_source as nsem -np.random.seed(1100) - TOLr = 1 TOLp = 2 FLR = 1e-20 # "zero", so if residual below this --> pass regardless of order diff --git a/tests/em/nsem/inversion/test_BC_Sims.py b/tests/em/nsem/inversion/test_BC_Sims.py index 10edb4ce3d..b029b2e177 100644 --- a/tests/em/nsem/inversion/test_BC_Sims.py +++ b/tests/em/nsem/inversion/test_BC_Sims.py @@ -20,13 +20,15 @@ def J_func(v): return d, J_func + np.random.seed(1983) # use seed for check_derivative passed = check_derivative(func, x0, plotIt=False, **kwargs) return passed def check_adjoint(sim, test_mod): - u = np.random.rand(len(test_mod)) - v = np.random.rand(sim.survey.nD) + rng = np.random.default_rng(seed=42) + u = rng.uniform(size=len(test_mod)) + v = rng.uniform(size=sim.survey.nD) f = sim.fields(test_mod) Ju = sim.Jvec(test_mod, u, f=f) @@ -332,13 +334,15 @@ def test_errors(self): nsem.simulation.Simulation2DMagneticField( mesh_2d, survey=survey_yx, e_bc=100 ) + + random_array = np.random.default_rng(seed=42).uniform(size=20) with self.assertRaises(TypeError): nsem.simulation.Simulation2DElectricField( - mesh_2d, survey=survey_xy, h_bc=np.random.rand(20) + mesh_2d, survey=survey_xy, h_bc=random_array ) with self.assertRaises(TypeError): nsem.simulation.Simulation2DMagneticField( - mesh_2d, survey=survey_yx, e_bc=np.random.rand(20) + mesh_2d, survey=survey_yx, e_bc=random_array ) # Check fixed boundary condition Key Error @@ -353,8 +357,9 @@ def test_errors(self): # Check fixed boundary condition length error bc = {} + rng = np.random.default_rng(seed=42) for freq in survey_xy.frequencies: - bc[freq] = np.random.rand(mesh_2d.boundary_edges.shape[0] + 3) + bc[freq] = rng.uniform(size=mesh_2d.boundary_edges.shape[0] + 3) with self.assertRaises(ValueError): nsem.simulation.Simulation2DElectricField( mesh_2d, survey=survey_xy, h_bc=bc diff --git a/tests/em/nsem/inversion/test_Problem1D_Adjoint.py b/tests/em/nsem/inversion/test_Problem1D_Adjoint.py index d3a9aaaafc..b81cda78b6 100644 --- a/tests/em/nsem/inversion/test_Problem1D_Adjoint.py +++ b/tests/em/nsem/inversion/test_Problem1D_Adjoint.py @@ -50,9 +50,9 @@ def JvecAdjointTest_1D(sigmaHalf, formulation="PrimSec"): m = np.r_[sigma_model, layer_thicknesses] u = simulation.fields(m) - np.random.seed(1983) - v = np.random.rand(survey.nD) - w = np.random.rand(len(m)) + rng = np.random.default_rng(seed=1983) + v = rng.uniform(size=survey.nD) + w = rng.uniform(size=len(m)) vJw = v.dot(simulation.Jvec(m, w, u)) wJtv = w.dot(simulation.Jtvec(m, v, u)) @@ -80,14 +80,10 @@ def JvecAdjointTest(sigmaHalf, formulation="PrimSec"): m = sigma u = problem.fields(m) - np.random.seed(1983) - v = np.random.rand( - survey.nD, - ) + rng = np.random.default_rng(seed=1983) + v = rng.uniform(size=survey.nD) # print problem.PropMap.PropModel.nP - w = np.random.rand( - problem.mesh.nC, - ) + w = rng.uniform(size=problem.mesh.nC) vJw = v.ravel().dot(problem.Jvec(m, w, u)) wJtv = w.ravel().dot(problem.Jtvec(m, v, u)) diff --git a/tests/em/nsem/inversion/test_Problem1D_Derivs.py b/tests/em/nsem/inversion/test_Problem1D_Derivs.py index 733e5bda1f..52c9f80db5 100644 --- a/tests/em/nsem/inversion/test_Problem1D_Derivs.py +++ b/tests/em/nsem/inversion/test_Problem1D_Derivs.py @@ -46,11 +46,11 @@ def DerivJvecTest_1D(halfspace_value, freq=False, expMap=True): ) x0 = np.r_[sigma_model, layer_thicknesses] - np.random.seed(1983) def fun(x): return simulation.dpred(x), lambda x: simulation.Jvec(x0, x) + np.random.seed(1983) # use seed for check_derivative return tests.check_derivative(fun, x0, num=6, plotIt=False, eps=FLR) @@ -69,12 +69,12 @@ def DerivJvecTest(halfspace_value, freq=False, expMap=True): ) x0 = sigBG - np.random.seed(1983) survey = simulation.survey def fun(x): return simulation.dpred(x), lambda x: simulation.Jvec(x0, x) + np.random.seed(1983) # set a random seed for check_derivative return tests.check_derivative(fun, x0, num=4, plotIt=False, eps=FLR) diff --git a/tests/em/nsem/inversion/test_Problem3D_Adjoint.py b/tests/em/nsem/inversion/test_Problem3D_Adjoint.py index ecafadd8d0..eb3ec464a4 100644 --- a/tests/em/nsem/inversion/test_Problem3D_Adjoint.py +++ b/tests/em/nsem/inversion/test_Problem3D_Adjoint.py @@ -44,14 +44,11 @@ def JvecAdjointTest( ) u = simulation.fields(m) - np.random.seed(1983) - v = np.random.rand( - simulation.survey.nD, - ) + rng = np.random.default_rng(seed=1983) + v = rng.uniform(size=simulation.survey.nD) # print problem.PropMap.PropModel.nP - w = np.random.rand( - len(m), - ) + w = rng.uniform(size=len(m)) + # print(problem.Jvec(m, w, u)) vJw = v.ravel().dot(simulation.Jvec(m, w, u)) wJtv = w.ravel().dot(simulation.Jtvec(m, v, u)) diff --git a/tests/em/nsem/inversion/test_Problem3D_Derivs.py b/tests/em/nsem/inversion/test_Problem3D_Derivs.py index 05f8acfa95..c08eff446c 100644 --- a/tests/em/nsem/inversion/test_Problem3D_Derivs.py +++ b/tests/em/nsem/inversion/test_Problem3D_Derivs.py @@ -88,6 +88,7 @@ def DerivJvecTest(inputSetup, comp="All", freq=False, expMap=True): def fun(x): return simulation.dpred(x), lambda x: simulation.Jvec(m, x) + np.random.seed(1983) # use seed for check_derivative return tests.check_derivative(fun, m, num=3, plotIt=False, eps=FLR) diff --git a/tests/em/nsem/inversion/test_complex_resistivity.py b/tests/em/nsem/inversion/test_complex_resistivity.py index 45cc6ef6cc..67cdbbba2d 100644 --- a/tests/em/nsem/inversion/test_complex_resistivity.py +++ b/tests/em/nsem/inversion/test_complex_resistivity.py @@ -229,12 +229,14 @@ def check_deriv(self, sim): def fun(x): return sim.dpred(x), lambda x: sim.Jvec(self.model, x) + np.random.seed(1983) # set a random seed for check_derivative passed = tests.check_derivative(fun, self.model, num=3, plotIt=False) self.assertTrue(passed) def check_adjoint(self, sim): - w = np.random.rand(len(self.model)) - v = np.random.rand(sim.survey.nD) + rng = np.random.default_rng(seed=42) + w = rng.uniform(size=len(self.model)) + v = rng.uniform(size=sim.survey.nD) f = sim.fields(self.model) vJw = v.ravel().dot(sim.Jvec(self.model, w, f)) From 74ce4d7f970854cc91848ffbbd6e41f0f4368080 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 25 Jun 2024 15:12:16 -0700 Subject: [PATCH 040/194] Unwrap lines in release checklist (#1498) Remove new line characters in release checklist so the paragraphs and list items are unwrapped. This improves how GitHub renders the issue template. Minor improvements to Markdown style. --- .github/ISSUE_TEMPLATE/release-checklist.md | 49 +++++++-------------- 1 file changed, 15 insertions(+), 34 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/release-checklist.md b/.github/ISSUE_TEMPLATE/release-checklist.md index 543bdb1fdd..1f04699e51 100644 --- a/.github/ISSUE_TEMPLATE/release-checklist.md +++ b/.github/ISSUE_TEMPLATE/release-checklist.md @@ -6,8 +6,6 @@ labels: "maintenance" assignees: "" --- - - **Target date:** YYYY/MM/DD ## Generate release notes @@ -28,9 +26,7 @@ assignees: "" grep -Eo "@[[:alnum:]-]+" notes.rst | sort -u | sed -E 's/^/* /' ``` Paste the list into the file under a new `Contributors` category. -- [ ] Check if every contributor that participated in the release is in the - list. Generate a list of authors and co-authors from the git log with (update - the `last_release`): +- [ ] Check if every contributor that participated in the release is in the list. Generate a list of authors and co-authors from the git log with (update the `last_release`): ```bash export last_release="v0.20.0" git shortlog HEAD...$last_release -sne > contributors @@ -41,14 +37,11 @@ assignees: "" ```bash sed -Ei 's/@([[:alnum:]-]+)/`@\1 `__/' notes.rst ``` -- [ ] Copy the content of `notes.rst` to a new file - `docs/content/release/-notes.rst`. -- [ ] Edit the release notes file, following the template below and the - previous release notes. +- [ ] Copy the content of `notes.rst` to a new file `docs/content/release/-notes.rst`. +- [ ] Edit the release notes file, following the template below and the previous release notes. - [ ] Add the new release notes to the list in `docs/content/release/index.rst`. - [ ] **Open a PR** with the new release notes. -- [ ] Manually view the built documentation by downloading the Azure `html_doc` - artifact and check for formatting and errors. +- [ ] Manually view the built documentation by downloading the Azure `html_doc` artifact and check for formatting and errors.
@@ -115,12 +108,9 @@ Pull Requests Edit the `docs/_static/versions.json` file and: - [ ] Add an entry for the new version. -- [ ] Move the line with `"name":` to the new entry (so the new version is set - as the _latest_ one). +- [ ] Move the line with `"name":` to the new entry (so the new version is set as the _latest_ one). - [ ] Update the version number in the `"name":` line. -- [ ] Run `cat docs/_static/versions.json | python -m json.tool > /dev/null` to - check if the syntax of the JSON file is correct. If no errors are prompted, - then your file is OK. +- [ ] Run `cat docs/_static/versions.json | python -m json.tool > /dev/null` to check if the syntax of the JSON file is correct. If no errors are prompted, then your file is OK. - [ ] Double-check the changes. - [ ] Commit the changes to the same branch. @@ -130,39 +120,30 @@ Edit the `docs/_static/versions.json` file and: ## Make the new release -- [ ] Draft a new GitHub Release +- [ ] Draft a new GitHub Release. - [ ] Create a new tag for it (the version number with a leading `v`). -- [ ] Target the release on `main` or on a particular commit from `main` +- [ ] Target the release on `main` or on a particular commit from `main`. - [ ] Generate release notes automatically. -- [ ] Publish the release +- [ ] Publish the release. ## Extra tasks -After publishing the release, Azure will automatically push the new version to -PyPI, and build and deploy the docs. You can check the progress of these tasks -in: https://dev.azure.com/simpeg/simpeg/_build +After publishing the release, Azure will automatically push the new version to PyPI, and build and deploy the docs. You can check the progress of these tasks in: https://dev.azure.com/simpeg/simpeg/_build . After they finish: -- [ ] Check the new version is available in PyPI: https://pypi.org/project/SimPEG/ +- [ ] Check the new version is available in PyPI: https://pypi.org/project/SimPEG/ . - [ ] Check the new documentation is online: https://docs.simpeg.xyz -For the new version to be available in conda-forge, we need to update the -[conda-forge/simpeg-feedstock](https://github.com/conda-forge/simpeg-feedstock) -repository. Within the same day of the release a new PR will be automatically -open in that repository. So: +For the new version to be available in conda-forge, we need to update the [conda-forge/simpeg-feedstock](https://github.com/conda-forge/simpeg-feedstock) repository. Within the same day of the release a new PR will be automatically open in that repository. So: - [ ] Follow the steps provided in the checklist in that PR and merge it. - [ ] Make sure the new version is available through conda-forge: https://anaconda.org/conda-forge/simpeg -Lastly, we would need to update the SimPEG version used in -[`simpeg/user-tutorials`](https://github.com/simpeg/user-tutorials) and rerun -its notebooks: +Lastly, we would need to update the SimPEG version used in [`simpeg/user-tutorials`](https://github.com/simpeg/user-tutorials) and rerun its notebooks: -- [ ] Open issue in - [`simpeg/user-tutorials`](https://github.com/simpeg/user-tutorials) for - rerunning the notebooks using the new released version of SimPEG +- [ ] Open issue in [`simpeg/user-tutorials`](https://github.com/simpeg/user-tutorials) for rerunning the notebooks using the new released version of SimPEG. Finally: -- [ ] Close this issue +- [ ] Close this issue. From 95d620452568ca6425bd53e1050d89cb90290833 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 26 Jun 2024 09:24:59 -0700 Subject: [PATCH 041/194] Improve instructions to update versions.json (#1500) Improve the instructions to update the `versions.json` file in the release checklist. --- .github/ISSUE_TEMPLATE/release-checklist.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/release-checklist.md b/.github/ISSUE_TEMPLATE/release-checklist.md index 1f04699e51..9d0a441b3b 100644 --- a/.github/ISSUE_TEMPLATE/release-checklist.md +++ b/.github/ISSUE_TEMPLATE/release-checklist.md @@ -107,9 +107,9 @@ Pull Requests Edit the `docs/_static/versions.json` file and: -- [ ] Add an entry for the new version. -- [ ] Move the line with `"name":` to the new entry (so the new version is set as the _latest_ one). -- [ ] Update the version number in the `"name":` line. +- [ ] Add an entry for the new version (below dev, above the current _latest_). +- [ ] Make sure to set the new the version number in every field in the new entry. +- [ ] Mark the new version as the `latest`, and remove `latest` from the previous one. - [ ] Run `cat docs/_static/versions.json | python -m json.tool > /dev/null` to check if the syntax of the JSON file is correct. If no errors are prompted, then your file is OK. - [ ] Double-check the changes. - [ ] Commit the changes to the same branch. From cf6034c35c05bf21fd350b2f5c20c58e06882bb8 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 26 Jun 2024 11:41:13 -0700 Subject: [PATCH 042/194] Add release notes for v0.22.0 (#1499) Add release notes for v0.22.0. Add new version to `versions.json` file and mark it as the latest. --- docs/_static/versions.json | 7 +- docs/content/release/0.22.0-notes.rst | 213 ++++++++++++++++++++++++++ docs/content/release/index.rst | 1 + 3 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 docs/content/release/0.22.0-notes.rst diff --git a/docs/_static/versions.json b/docs/_static/versions.json index 4fe5c5dfea..80a6ce36b0 100644 --- a/docs/_static/versions.json +++ b/docs/_static/versions.json @@ -4,7 +4,12 @@ "url": "https://docs.simpeg.xyz/dev/" }, { - "name": "v0.21.1 (latest)", + "name": "v0.22.0 (latest)", + "version": "v0.22.0", + "url": "https://docs.simpeg.xyz/v0.22.0/" + }, + { + "name": "v0.21.1", "version": "v0.21.1", "url": "https://docs.simpeg.xyz/v0.21.1/" }, diff --git a/docs/content/release/0.22.0-notes.rst b/docs/content/release/0.22.0-notes.rst new file mode 100644 index 0000000000..ac843655ac --- /dev/null +++ b/docs/content/release/0.22.0-notes.rst @@ -0,0 +1,213 @@ +.. _0.22.0_notes: + +============================ +SimPEG 0.22.0 Release Notes +============================ + +June 26th, 2024 + +.. contents:: Highlights + :depth: 3 + +Updates +======= + +``SimPEG`` has been renamed to ``simpeg`` +----------------------------------------- + +SimPEG v0.22.0 comes with a major change that everyone will experience! We are +excited to announce that we renamed the package from ``SimPEG`` to ``simpeg``, +making it compliant to `PEP8 `__. + +Since this version and moving forward we'll import ``simpeg`` as: + +.. code:: python + + import simpeg + +or any of its submodules as: + +.. code:: python + + from simpeg.potential_fields import magnetics + +Although we encourage users to update their code after they update their +installed SimPEG version, we still support the old ``SimPEG``. +We'll just receive a warning about the deprecation of ``SimPEG`` with +upper-cases if we try to import it: + +.. code:: python + + import SimPEG + +.. code:: + + FutureWarning: Importing `SimPEG` is deprecated. please import from `simpeg`. + +New features +------------ + +We have a faster and more memory efficient implementation of the magnetic +simulation +:class:`~simpeg.potential_fields.magnetics.Simulation3DIntegral`, built using +`Choclo `__ and `Numba `__. +In order to use it, you will need to `install Choclo +`__ in addition to +``simpeg``. This new implementation is able to forward model TMI and all +magnetic field components with susceptibility (scalar) or effective +susceptibility (vector) models. + +Documentation +------------- + +Since v0.22.0, we serve the documentation pages for `latest +`__ and older versions of SimPEG, along with +the pages for the `development version `__ (the +latest version in the ``main`` branch of our GitHub repository). +The docs now include a version switcher in the top bar that will allow users to +navigate between the pages for each different version. + +Several improvements have been made to the documentation pages. + +In the `Getting Started +`__ guide we +stopped recommending ``mamba`` as the best package manager for installing +SimPEG after ``conda`` started using ``libmamba`` under the hood, achieving the +same performance as ``mamba``. We also included instructions so our +contributors can easily update their local environment, which is specially +useful after we update the minimum version of the development dependencies +(such as autoformatters and linters). + +The documentation for Frequency-Domain (FDEM) and Time-Domain (TDEM) EM fields +and 3D simulations got greatly extended, with more details on the parameters +for each method, along with more mathematical background for their +implementations. + +Lastly, we fixed typos, the contour colors for one of the gravity examples, and +the broken links to the source code for each class, function and method. +These links now point to their corresponding version in the GitHub repository. + +Bugfixes +-------- + +This release comes with a few bug fixes. We solved an issue with the distance +calculation in the +:func:`~simpeg.electromagnetics.static.utils.convert_survey_3d_to_2d_lines` +utility function that converts 3D DC-IP survey locations to 2D lines. We fixed +a bug on how the arguments passed to the directives that estimate the beta +parameter where being passed to the parent classes. We updated the code in +:class:`~simpeg.utils.pgi_utils.GaussianMixtureWithPrior` to make it compatible +with the latest versions of `scikit-learn `__. And +we now make sure that the queues are joined when the +:class:`~simpeg.meta.MetaSimulation` is joined. + +Breaking changes +---------------- + +The :mod:`~simpeg.electromagnetics.static.spontaneous_potential` has been +renamed to :mod:`~simpeg.electromagnetics.static.self_potential` to accurately +reflect the nature of the process, as the term being used in the literature. + +General improvements +-------------------- + +We improved the interface of the +:class:`~simpeg.potential_fields.magnetics.UniformBackgroundField` class, and +for the DC :class:`~simpeg.electromagnetics.static.resistivity.sources.Dipole` +source class. + +We moved away from the deprecated Numpy's global random seeds and replace them +for the new `random number generator object +`__ +in the entire SimPEG's code base and in most of its tests. This greatly helps +the experience of ensuring reproducible runs of our inversions and tests. + +Lastly, the inversion logs now also include the SimPEG version that is being +used. + +Maintenance +----------- + +We updated the configuration files to build and install SimPEG, moving away +from the old ``setup.py`` into the new ``pyproject.toml``. +We fixed another important flake8 warning across the code base: F821, which +highlights undefined varibles in the code. +And cleaned up the scripts for running automated tasks in Azure Pipelines (like +checking style, testing, deploying docs and code). + +Contributors +============ + +This is a combination of contributors and reviewers who've made contributions +towards this release (in no particular order). + +- `@dccowan `__ +- `@jcapriot `__ +- `@jedman `__ +- `@kehrl-kobold `__ +- `@lheagy `__ +- `@santisoler `__ +- `@williamjsdavis `__ + +We would like to highlight the contributions made by new contributors: + +- `@kehrl-kobold `__ made their first contribution in + https://github.com/simpeg/simpeg/pull/1390 +- `@williamjsdavis `__ made their first contribution in + https://github.com/simpeg/simpeg/pull/1486 + + +Pull Requests +============= + +- Remove the parameters argument from docstring by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1417 +- Use reviewdog to annotate PR’s with black and flake8 errors. by `@jcapriot `__ in https://github.com/simpeg/simpeg/pull/1424 +- Safely run reviewdog on ``pull_request_target`` events by `@jcapriot `__ in https://github.com/simpeg/simpeg/pull/1427 +- Add new Issue template for making a release by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1410 +- Replace use of ``refine_tree_xyz`` in DCIP tutorials by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1381 +- Fix rst syntax in release notes for v0.21.0 by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1434 +- Move to a PEP8 compliant package name. by `@jcapriot `__ in https://github.com/simpeg/simpeg/pull/1430 +- Update copyright year in **init**.py by `@lheagy `__ in https://github.com/simpeg/simpeg/pull/1436 +- Replace SimPEG for simpeg across docstrings by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1438 +- Lowercase simpeg for generating coverage on Azure by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1443 +- Rename spontaneous potential to self potential by `@lheagy `__ in https://github.com/simpeg/simpeg/pull/1422 +- Fix distance calculation in ``convert_survey_3d_to_2d_lines`` by `@kehrl-kobold `__ in https://github.com/simpeg/simpeg/pull/1390 +- Replace SimPEG for simpeg in API reference by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1446 +- Replace SimPEG for simpeg in getting started pages by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1447 +- Check inputs for converting 3d surveys to 2d lines by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1392 +- Always use Pydata Sphinx theme for building docs by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1445 +- Simplify interface of UniformBackgroundField by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1421 +- Ensure the queue’s are joined when the meta simulation is joined. by `@jcapriot `__ in https://github.com/simpeg/simpeg/pull/1464 +- Add maintenance issue template by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1468 +- Add instructions to update the environment by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1462 +- Stop recommending mamba for installing simpeg by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1463 +- Fix bug on arguments of beta estimator directives by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1460 +- Improve interface for DC Dipole source by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1393 +- Use Numpy random number generator in codebase by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1394 +- Extend docstrings for FDEM and TDEM fields and 3D simulations by `@dccowan `__ in https://github.com/simpeg/simpeg/pull/1414 +- Magnetic simulation with Choclo as engine by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1321 +- Fix typos in EM docstrings by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1473 +- Fix call to private method in GaussianMixtureWithPrior by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1476 +- Add version switcher to Sphinx docs by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1428 +- Use random seed on synthetic data in mag tests by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1457 +- Fix links to source code in documentation pages by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1444 +- Fix script for new deployment of docs by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1478 +- Print SimPEG version in the inversion log by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1477 +- Use Numpy rng in FDEM tests by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1449 +- Add ``random_seed`` argument to objective fun’s derivative tests by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1448 +- Add ``random_seed`` to the ``test`` method of maps by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1465 +- Use Numpy rng in TDEM tests by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1452 +- Use random seed in missed objective function tests by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1483 +- Fix contour colors in gravity plot in User Guide by `@williamjsdavis `__ in https://github.com/simpeg/simpeg/pull/1486 +- Hide type hints from signatures in documentation pages by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1471 +- Reorganize the maps submodule by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1480 +- Pyproject.toml by `@jcapriot `__ in https://github.com/simpeg/simpeg/pull/1482 +- Use Numpy rng in static EM tests by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1451 +- Split Azure Pipelines configuration into multiple files by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1481 +- Ignore ``survey_type`` argument in DC and SIP surveys by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1458 +- Update deployment of docs to simpeg-docs by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1490 +- Fix F821 flake error: undefined variable by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1487 +- Use Numpy rng in viscous remanent mag tests by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1453 +- Use Numpy rng in NSEM tests by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1450 +- Unwrap lines in release checklist by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1498 +- Improve instructions to update versions.json by `@santisoler ` in https://github.com/simpeg/simpeg/pull/1500 diff --git a/docs/content/release/index.rst b/docs/content/release/index.rst index 49daf1cfc9..b9027968c7 100644 --- a/docs/content/release/index.rst +++ b/docs/content/release/index.rst @@ -5,6 +5,7 @@ Release Notes .. toctree:: :maxdepth: 2 + 0.22.0 <0.22.0-notes> 0.21.1 <0.21.1-notes> 0.21.0 <0.21.0-notes> 0.20.0 <0.20.0-notes> From 6269fe6f6563d7c9a9a2d23661fe55e6387bb73c Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 28 Jun 2024 08:07:06 -0700 Subject: [PATCH 043/194] Configure version warning banner in Sphinx (#1501) Configure the version warning banner in Sphinx PyData theme. Set v0.22.0 as the preferred version in `versions.json`. Remove leading `v` in version numbers in `versions.json`. Remove leading `v` in the `switcher_version` variable defined in `docs/conf.py`. --- .github/ISSUE_TEMPLATE/release-checklist.md | 2 ++ docs/_static/versions.json | 17 +++++++++-------- docs/conf.py | 3 ++- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/release-checklist.md b/.github/ISSUE_TEMPLATE/release-checklist.md index 9d0a441b3b..33a3c64690 100644 --- a/.github/ISSUE_TEMPLATE/release-checklist.md +++ b/.github/ISSUE_TEMPLATE/release-checklist.md @@ -110,6 +110,8 @@ Edit the `docs/_static/versions.json` file and: - [ ] Add an entry for the new version (below dev, above the current _latest_). - [ ] Make sure to set the new the version number in every field in the new entry. - [ ] Mark the new version as the `latest`, and remove `latest` from the previous one. +- [ ] Set the new version as the `preferred` one. +- [ ] Remove the `preferred` line from the older version. - [ ] Run `cat docs/_static/versions.json | python -m json.tool > /dev/null` to check if the syntax of the JSON file is correct. If no errors are prompted, then your file is OK. - [ ] Double-check the changes. - [ ] Commit the changes to the same branch. diff --git a/docs/_static/versions.json b/docs/_static/versions.json index 80a6ce36b0..a9e2d957d3 100644 --- a/docs/_static/versions.json +++ b/docs/_static/versions.json @@ -5,32 +5,33 @@ }, { "name": "v0.22.0 (latest)", - "version": "v0.22.0", - "url": "https://docs.simpeg.xyz/v0.22.0/" + "version": "0.22.0", + "url": "https://docs.simpeg.xyz/v0.22.0/", + "preferred": true }, { "name": "v0.21.1", - "version": "v0.21.1", + "version": "0.21.1", "url": "https://docs.simpeg.xyz/v0.21.1/" }, { - "version": "v0.21.0", + "version": "0.21.0", "url": "https://docs.simpeg.xyz/v0.21.0/" }, { - "version": "v0.20.0", + "version": "0.20.0", "url": "https://docs.simpeg.xyz/v0.20.0/" }, { - "version": "v0.19.0", + "version": "0.19.0", "url": "https://docs.simpeg.xyz/v0.19.0/" }, { - "version": "v0.18.1", + "version": "0.18.1", "url": "https://docs.simpeg.xyz/v0.18.1/" }, { - "version": "v0.18.0", + "version": "0.18.0", "url": "https://docs.simpeg.xyz/v0.18.0/" } ] diff --git a/docs/conf.py b/docs/conf.py index fd2220f41e..5b7c5c9175 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -250,7 +250,7 @@ def linkcode_resolve(domain, info): if simpeg_version.is_devrelease: switcher_version = "dev" else: - switcher_version = f"v{simpeg_version.public}" + switcher_version = simpeg_version.public # Use Pydata Sphinx theme html_theme = "pydata_sphinx_theme" @@ -295,6 +295,7 @@ def linkcode_resolve(domain, info): "version_match": switcher_version, "json_url": "https://docs.simpeg.xyz/latest/_static/versions.json", }, + "show_version_warning_banner": True, } html_logo = "images/simpeg-logo.png" From 0f5b437b116fdd2a59769ac825cf9c263ba5d125 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 2 Jul 2024 09:44:40 -0700 Subject: [PATCH 044/194] Improve imports in natural source utils (#1503) Make them less prone to failing by splitting the imports, so we don't rely on matplotib importing numpy in its base `__init__.py`. Fixes #1502 --- .../electromagnetics/natural_source/utils/plot_data_types.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/simpeg/electromagnetics/natural_source/utils/plot_data_types.py b/simpeg/electromagnetics/natural_source/utils/plot_data_types.py index aad9088c34..aea4821aec 100644 --- a/simpeg/electromagnetics/natural_source/utils/plot_data_types.py +++ b/simpeg/electromagnetics/natural_source/utils/plot_data_types.py @@ -1,4 +1,6 @@ -from matplotlib import pyplot as plt, colors, numpy as np +import numpy as np +from matplotlib import colors +from matplotlib import pyplot as plt def plotIsoFreqNSimpedance( From fc83aa771269f8540c0fa6443f20d87060b1536f Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 5 Jul 2024 09:47:24 -0700 Subject: [PATCH 045/194] Add release notes for v0.22.1 (#1508) Add release noted for v0.22.1, and include them in the releases index. Update `versions.json` file: add v0.22.1 as the latest and preferred version. --- docs/_static/versions.json | 11 ++++++--- docs/content/release/0.22.1-notes.rst | 33 +++++++++++++++++++++++++++ docs/content/release/index.rst | 1 + 3 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 docs/content/release/0.22.1-notes.rst diff --git a/docs/_static/versions.json b/docs/_static/versions.json index a9e2d957d3..f1b0281d32 100644 --- a/docs/_static/versions.json +++ b/docs/_static/versions.json @@ -4,11 +4,16 @@ "url": "https://docs.simpeg.xyz/dev/" }, { - "name": "v0.22.0 (latest)", - "version": "0.22.0", - "url": "https://docs.simpeg.xyz/v0.22.0/", + "name": "v0.22.1 (latest)", + "version": "0.22.1", + "url": "https://docs.simpeg.xyz/v0.22.1/", "preferred": true }, + { + "name": "v0.22.0", + "version": "0.22.0", + "url": "https://docs.simpeg.xyz/v0.22.0/" + }, { "name": "v0.21.1", "version": "0.21.1", diff --git a/docs/content/release/0.22.1-notes.rst b/docs/content/release/0.22.1-notes.rst new file mode 100644 index 0000000000..68c3e64d2b --- /dev/null +++ b/docs/content/release/0.22.1-notes.rst @@ -0,0 +1,33 @@ +.. _0.22.1_notes: + +=========================== +SimPEG 0.22.1 Release Notes +=========================== + +July 5th, 2024 + +.. contents:: Highlights + :depth: 2 + +Updates +======= + +This patch release includes and improvement in one of the import lines, fixing +an bug that was making importing frequency domain modules in :mod:`simpeg` to +fail when working with :mod:`matplotlib` ``>=3.9.0``. + +It also enables the version warning banner in PyData Sphinx theme, allowing +users to quickly notice if the docs they are reading are not the latest ones. + +Contributors +============ + +- `@santisoler `__ + +Pull Requests +============= + +- Configure version warning banner in Sphinx by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1501 +- Improve imports in natural source utils by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1503 diff --git a/docs/content/release/index.rst b/docs/content/release/index.rst index b9027968c7..7439a185c3 100644 --- a/docs/content/release/index.rst +++ b/docs/content/release/index.rst @@ -5,6 +5,7 @@ Release Notes .. toctree:: :maxdepth: 2 + 0.22.1 <0.22.1-notes> 0.22.0 <0.22.0-notes> 0.21.1 <0.21.1-notes> 0.21.0 <0.21.0-notes> From 83bbbf8fa47b334d7afca44f34ecd245f0af684f Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 5 Jul 2024 11:38:48 -0700 Subject: [PATCH 046/194] Deploy to TestPyPI only on nightly builds (#1509) Configure the deploy job in the PyPI stage on Azure to only trigger after a release or a nightly run. This prevents trying to push the same release to TestPyPI multiple times. --- .ci/azure/pypi.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.ci/azure/pypi.yml b/.ci/azure/pypi.yml index 5abd0ad681..89bed4e43e 100644 --- a/.ci/azure/pypi.yml +++ b/.ci/azure/pypi.yml @@ -43,7 +43,7 @@ jobs: - job: Deploy dependsOn: Build - condition: or(startsWith(variables['build.sourceBranch'], 'refs/tags/'), eq(variables['build.sourceBranch'], 'refs/heads/main')) + condition: or(startsWith(variables['build.sourceBranch'], 'refs/tags/'), eq(variables['Build.Reason'], 'Schedule')) pool: vmImage: ubuntu-latest steps: @@ -68,7 +68,7 @@ jobs: - bash: | twine upload --repository testpypi dist/* displayName: "Upload to TestPyPI" - condition: eq(variables['build.sourceBranch'], 'refs/heads/main') + condition: eq(variables['Build.Reason'], 'Schedule') env: TWINE_USERNAME: $(twine.username) TWINE_PASSWORD: $(test.twine.password) From c699a84f699699f94ef2066288268e13c6090913 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 5 Jul 2024 13:04:01 -0700 Subject: [PATCH 047/194] Add latest merged PR to release notes for v0.22.1 (#1510) Add #1509 to the release notes for v0.22.1. --- docs/content/release/0.22.1-notes.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/content/release/0.22.1-notes.rst b/docs/content/release/0.22.1-notes.rst index 68c3e64d2b..c06d4ee577 100644 --- a/docs/content/release/0.22.1-notes.rst +++ b/docs/content/release/0.22.1-notes.rst @@ -31,3 +31,5 @@ Pull Requests https://github.com/simpeg/simpeg/pull/1501 - Improve imports in natural source utils by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1503 +- Deploy to TestPyPI only on nightly builds by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1509 From 95a123280056211be7c89e0cd8dc0b907b4ddf1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dieter=20Werthm=C3=BCller?= Date: Tue, 20 Aug 2024 18:37:25 +0200 Subject: [PATCH 048/194] Minor fixes to disclaimer in `pgi_utils.py` (#1512) Fix typo and update ASCII style of a comment block in `pgi_utils.py`. Small non-controversial take-out from #1075. --- simpeg/utils/pgi_utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/simpeg/utils/pgi_utils.py b/simpeg/utils/pgi_utils.py index 638b94cfb3..196957de21 100644 --- a/simpeg/utils/pgi_utils.py +++ b/simpeg/utils/pgi_utils.py @@ -24,10 +24,10 @@ ############################################################################### # Disclaimer: the following classes built upon the GaussianMixture class # -# from Scikit-Learn. New functionalitie are added, as well as modifications to# -# existing functions, to serve the purposes pursued within SimPEG. # +# from Scikit-Learn. New functionalities are added, as well as modifications # +# to existing functions, to serve the purposes pursued within SimPEG. # # This use is allowed by the Scikit-Learn licensing (BSD-3-Clause License) # -# and we are grateful for their contributions to the open-source community. # # +# and we are grateful for their contributions to the open-source community. # ############################################################################### From 43325c4b437da03af2f574134a274ee6e90e2903 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dieter=20Werthm=C3=BCller?= Date: Tue, 20 Aug 2024 19:58:17 +0200 Subject: [PATCH 049/194] Fix misuse of the `requires` decorator in `code_utils.py` (#1513) Use the `discretize`'s `requires` to decorate `mem_profile_class`, instead of `simpeg.utils.code_utils.requires`, since the former is intended to be used for modules, while the latter should be used for attributes. --- simpeg/utils/code_utils.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/simpeg/utils/code_utils.py b/simpeg/utils/code_utils.py index 8c2014b216..8d7fd20469 100644 --- a/simpeg/utils/code_utils.py +++ b/simpeg/utils/code_utils.py @@ -5,6 +5,7 @@ import warnings from discretize.utils import as_array_n_by_dim # noqa: F401 +from discretize.utils import requires as module_requires # scooby is a soft dependency for simpeg try: @@ -20,6 +21,12 @@ def __init__(self, additional, core, optional, ncol, text_width, sort): ) +try: + import memory_profiler +except ImportError: + memory_profiler = False + + def requires(var): """Wrap a function to require a specfic attribute. @@ -80,7 +87,7 @@ def requiresVarWrapper(self, *args, **kwargs): return requiresVar -@requires("memory_profiler") +@module_requires({"memory_profiler": memory_profiler}) def mem_profile_class(input_class, *args): """Creates a new class from the target class with memory profiled methods. From dc281c25a023986b85ce0fb3d32b915319c54286 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 21 Aug 2024 11:53:56 -0700 Subject: [PATCH 050/194] Use lowercase simpeg in a few missing docstrings (#1519) Replace `SimPEG` for `simpeg` in a few missing docstrings, so Sphinx can link them to the respective classes and functions. --- simpeg/fields.py | 14 +++++++------- simpeg/potential_fields/magnetics/sources.py | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/simpeg/fields.py b/simpeg/fields.py index fd20be1716..f78c994219 100644 --- a/simpeg/fields.py +++ b/simpeg/fields.py @@ -8,7 +8,7 @@ class Fields: r"""Base class for storing fields. Fields classes are used to store the discrete field solution for a - corresponding simulation object; see :py:class:`SimPEG.simulation.BaseSimulation`. + corresponding simulation object; see :py:class:`simpeg.simulation.BaseSimulation`. Generally only one field solution (e.g. ``'eSolution'``, ``'phiSolution'``, ``'bSolution'``) is stored. However, it may be possible to extract multiple field types (e.g. ``'e'``, ``'b'``, ``'j'``, ``'h'``) on the fly from the fields object. The field solution that is stored and the @@ -17,7 +17,7 @@ class Fields: Parameters ---------- - simulation : SimPEG.simulation.BaseSimulation + simulation : simpeg.simulation.BaseSimulation The simulation object used to compute the discrete field solution. knownFields : dict of {key: str}, optional Dictionary defining the field solutions that are stored and where @@ -96,7 +96,7 @@ def simulation(self): Returns ------- - SimPEG.simulation.BaseSimulation + simpeg.simulation.BaseSimulation The simulation object used to compute the field solution. """ return self._simulation @@ -185,7 +185,7 @@ def survey(self): Returns ------- - SimPEG.survey.BaseSurvey + simpeg.survey.BaseSurvey Survey used by the simulation. """ return self.simulation.survey @@ -351,7 +351,7 @@ class TimeFields(Fields): r"""Base class for storing TDEM fields. ``TimeFields`` is a base class for storing discrete field solutions for simulations - that use discrete time-stepping; see :py:class:`SimPEG.simulation.BaseTimeSimulation`. + that use discrete time-stepping; see :py:class:`simpeg.simulation.BaseTimeSimulation`. Generally only one field solution (e.g. ``'eSolution'``, ``'phiSolution'``, ``'bSolution'``) is stored. However, it may be possible to extract multiple field types (e.g. ``'e'``, ``'b'``, ``'j'``, ``'h'``) on the fly from the fields object. The field solution that is stored and the @@ -360,7 +360,7 @@ class TimeFields(Fields): Parameters ---------- - simulation : SimPEG.simulation.BaseTimeSimulation + simulation : simpeg.simulation.BaseTimeSimulation The simulation object used to compute the discrete field solution. knownFields : dict of {key: str}, optional Dictionary defining the field solutions that are stored and where @@ -413,7 +413,7 @@ def simulation(self): Returns ------- - SimPEG.simulation.BaseTimeSimulation + simpeg.simulation.BaseTimeSimulation The simulation object used to compute the field solution. """ return self._simulation diff --git a/simpeg/potential_fields/magnetics/sources.py b/simpeg/potential_fields/magnetics/sources.py index df9027f38a..f74bada543 100644 --- a/simpeg/potential_fields/magnetics/sources.py +++ b/simpeg/potential_fields/magnetics/sources.py @@ -44,7 +44,7 @@ def receiver_list(self): Returns ------- - list of SimPEG.potential_fields.magnetics.Point + list of simpeg.potential_fields.magnetics.Point List of magnetic receivers associated with the survey """ return self._receiver_list From 37d69cff3ef74f4c0c6fedb69721879de0651def Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dieter=20Werthm=C3=BCller?= Date: Thu, 22 Aug 2024 18:41:32 +0200 Subject: [PATCH 051/194] Pass `rtol` to SciPy solvers for SciPy>=1.12 (#1517) Pass `rtol` or `tol` to SciPy solvers depending on the available version of SciPy: for `scipy>=1.12` use `rtol`, otherwise use `tol`. SciPy 1.12 changed `tol` to `rtol` for `scipy.sparse.linalg...`, and removed `tol` in SciPy 1.14. Closes #1516 --------- Co-authored-by: Santiago Soler --- environment.yml | 2 +- simpeg/optimization.py | 35 +++++++++++++++++++++++++++++------ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/environment.yml b/environment.yml index 709a40813c..5fdf11c818 100644 --- a/environment.yml +++ b/environment.yml @@ -56,4 +56,4 @@ dependencies: # recommended - jupyter - - pyvista \ No newline at end of file + - pyvista diff --git a/simpeg/optimization.py b/simpeg/optimization.py index edc68b1bd5..d4fa750ccc 100644 --- a/simpeg/optimization.py +++ b/simpeg/optimization.py @@ -1,4 +1,6 @@ +from __future__ import annotations import numpy as np +import scipy import scipy.sparse as sp from .utils.solver_utils import SolverWrapI, Solver, SolverDiag @@ -17,6 +19,23 @@ norm = np.linalg.norm +# Create a flag if the installed version of SciPy is newer or equal to 1.12.0 +# (Used to choose whether to pass `tol` or `rtol` to the solvers. See #1516). +class Version: + def __init__(self, version): + self.version = version + + def as_tuple(self) -> tuple[int, int]: + major, minor = tuple(int(p) for p in self.version.split(".")[:2]) + return (major, minor) + + def __ge__(self, other): + return self.as_tuple() >= other.as_tuple() + + +SCIPY_1_12 = Version(scipy.__version__) >= Version("1.12.0") + + __all__ = [ "Minimize", "Remember", @@ -892,9 +911,12 @@ def reduceHess(v): operator = sp.linalg.LinearOperator( (shape[1], shape[1]), reduceHess, dtype=self.xc.dtype ) - p, info = sp.linalg.cg( - operator, -Z.T * self.g, tol=self.tolCG, maxiter=self.maxIterCG - ) + + # Choose `rtol` or `tol` argument based on installed scipy version + tol_key = "rtol" if SCIPY_1_12 else "tol" + + inp = {tol_key: self.tolCG, "maxiter": self.maxIterCG} + p, info = sp.linalg.cg(operator, -Z.T * self.g, **inp) p = Z * p # bring up to full size # aSet_after = self.activeSet(self.xc+p) return p @@ -1069,9 +1091,10 @@ def approxHinv(self, value): @timeIt def findSearchDirection(self): - Hinv = SolverICG( - self.H, M=self.approxHinv, tol=self.tolCG, maxiter=self.maxIterCG - ) + # Choose `rtol` or `tol` argument based on installed scipy version + tol_key = "rtol" if SCIPY_1_12 else "tol" + inp = {tol_key: self.tolCG, "maxiter": self.maxIterCG} + Hinv = SolverICG(self.H, M=self.approxHinv, **inp) p = Hinv * (-self.g) return p From 5e4b583be3e5dafa3622dcff59894a2a02e5a304 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dieter=20Werthm=C3=BCller?= Date: Fri, 23 Aug 2024 18:38:15 +0200 Subject: [PATCH 052/194] Make `pandas` & `scikit-learn` optional dependencies (#1514) Set `sklearn` and `pandas` as optional dependencies in `pyproject.toml`. Make use of `discretize`'s `requires` utils function to decorate the functions and methods that require either `pandas` or `scikit-learn` to be installed, specifically in `simpeg.electromagnetics.static.resisitivity.IO` and PGI-related classes. --- .ci/environment_test.yml | 4 +- environment.yml | 4 +- pyproject.toml | 6 +-- .../static/resistivity/IODC.py | 16 +++++-- simpeg/utils/code_utils.py | 4 +- simpeg/utils/pgi_utils.py | 46 ++++++++++++------- tests/utils/test_report.py | 4 +- 7 files changed, 53 insertions(+), 31 deletions(-) diff --git a/.ci/environment_test.yml b/.ci/environment_test.yml index 5003045093..1b947eb468 100644 --- a/.ci/environment_test.yml +++ b/.ci/environment_test.yml @@ -4,13 +4,11 @@ channels: dependencies: - numpy>=1.20 - scipy>=1.8 - - scikit-learn>=1.2 - pymatsolver-base>=0.2 - matplotlib-base - discretize>=0.10 - geoana>=0.5.0 - empymod>=2.0.0 - - pandas # Solver - pydiso @@ -22,6 +20,8 @@ dependencies: - choclo - scooby - plotly + - scikit-learn>=1.2 + - pandas # documentation building - sphinx diff --git a/environment.yml b/environment.yml index 5fdf11c818..6d07ad0fc8 100644 --- a/environment.yml +++ b/environment.yml @@ -6,13 +6,11 @@ dependencies: - python=3.11 - numpy>=1.20 - scipy>=1.8 - - scikit-learn>=1.2 - pymatsolver-base>=0.2 - matplotlib-base - discretize>=0.10 - geoana>=0.5.0 - empymod>=2.0.0 - - pandas # solver # uncomment the next line if you are on an intel platform @@ -25,6 +23,8 @@ dependencies: - choclo - scooby - plotly + - scikit-learn>=1.2 + - pandas # documentation building - sphinx diff --git a/pyproject.toml b/pyproject.toml index 6a8926907a..476e59b18d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,13 +17,11 @@ keywords = [ dependencies = [ "numpy>=1.20", "scipy>=1.8", - "scikit-learn>=1.2", "pymatsolver>=0.2", "matplotlib", "discretize>=0.10", "geoana>=0.5.0", "empymod>=2.0.0", - "pandas", ] classifiers = [ "Development Status :: 4 - Beta", @@ -55,8 +53,10 @@ dask = ["dask", "zarr", "fsspec>=0.3.3"] choclo = ["choclo"] reporting = ["scooby"] plotting = ["plotly"] +sklearn = ["scikit-learn>=1.2"] +pandas = ["pandas"] all = [ - "simpeg[dask,choclo,plotting,reporting]" + "simpeg[dask,choclo,plotting,reporting,sklearn,pandas]" ] # all optional *runtime* dependencies (not related to development) style = [ "black==24.3.0", diff --git a/simpeg/electromagnetics/static/resistivity/IODC.py b/simpeg/electromagnetics/static/resistivity/IODC.py index cc47403d7e..912bccc7a0 100644 --- a/simpeg/electromagnetics/static/resistivity/IODC.py +++ b/simpeg/electromagnetics/static/resistivity/IODC.py @@ -1,5 +1,4 @@ import numpy as np -import pandas as pd import matplotlib.pyplot as plt import matplotlib import warnings @@ -7,6 +6,12 @@ from discretize import TensorMesh, TreeMesh from discretize.base import BaseMesh from discretize.utils import refine_tree_xyz, unpack_widths, active_from_xyz +from discretize.utils import requires as module_requires + +try: + import pandas +except ImportError: + pandas = False from ....utils import ( sdiag, @@ -1283,6 +1288,7 @@ def read_ubc_dc2d_obs_file(self, filename, input_type="simple", toponame=None): survey.topo = topo return survey + @module_requires({"pandas": pandas}) def write_to_csv(self, fname, dobs, standard_deviation=None, **kwargs): uncert = kwargs.pop("uncertainty", None) if uncert is not None: @@ -1300,7 +1306,7 @@ def write_to_csv(self, fname, dobs, standard_deviation=None, **kwargs): dobs, standard_deviation, ] - df = pd.DataFrame( + df = pandas.DataFrame( data=data, columns=[ "Ax", @@ -1317,8 +1323,9 @@ def write_to_csv(self, fname, dobs, standard_deviation=None, **kwargs): ) df.to_csv(fname) + @module_requires({"pandas": pandas}) def read_dc_data_csv(self, fname, dim=2): - df = pd.read_csv(fname) + df = pandas.read_csv(fname) if dim == 2: a_locations = df[["Ax", "Az"]].values b_locations = df[["Bx", "Bz"]].values @@ -1352,8 +1359,9 @@ def read_dc_data_csv(self, fname, dim=2): raise NotImplementedError() return survey + @module_requires({"pandas": pandas}) def read_topo_csv(self, fname, dim=2): if dim == 2: - df = pd.read_csv(fname) + df = pandas.read_csv(fname) topo = df[["X", "Z"]].values return topo diff --git a/simpeg/utils/code_utils.py b/simpeg/utils/code_utils.py index 8d7fd20469..691865d13a 100644 --- a/simpeg/utils/code_utils.py +++ b/simpeg/utils/code_utils.py @@ -478,11 +478,9 @@ def __init__(self, add_pckg=None, ncol=3, text_width=80, sort=False): "pymatsolver", "numpy", "scipy", - "sklearn", "matplotlib", "empymod", "geoana", - "pandas", ] # Optional packages. @@ -491,6 +489,8 @@ def __init__(self, add_pckg=None, ncol=3, text_width=80, sort=False): "pydiso", "numba", "dask", + "sklearn", + "pandas", "sympy", "IPython", "ipywidgets", diff --git a/simpeg/utils/pgi_utils.py b/simpeg/utils/pgi_utils.py index 196957de21..145850f03d 100644 --- a/simpeg/utils/pgi_utils.py +++ b/simpeg/utils/pgi_utils.py @@ -3,24 +3,34 @@ from mpl_toolkits.axes_grid1.inset_locator import inset_axes from scipy import linalg from scipy.special import logsumexp -from sklearn.mixture import GaussianMixture -from sklearn.cluster import KMeans -from sklearn.utils import check_array -from sklearn.utils.validation import check_is_fitted -from sklearn.mixture._gaussian_mixture import ( - _compute_precision_cholesky, - _compute_log_det_cholesky, - _estimate_gaussian_covariances_full, - _estimate_gaussian_covariances_diag, - _estimate_gaussian_covariances_spherical, - _check_means, - _check_precisions, - _check_shape, -) -from sklearn.mixture._base import check_random_state, ConvergenceWarning import warnings from simpeg.maps import IdentityMap +from discretize.utils.code_utils import requires + +# sklearn is a soft dependency +try: + import sklearn + from sklearn.mixture import GaussianMixture + from sklearn.cluster import KMeans + from sklearn.utils import check_array + from sklearn.utils.validation import check_is_fitted + from sklearn.mixture._gaussian_mixture import ( + _compute_precision_cholesky, + _compute_log_det_cholesky, + _estimate_gaussian_covariances_full, + _estimate_gaussian_covariances_diag, + _estimate_gaussian_covariances_spherical, + _check_means, + _check_precisions, + _check_shape, + ) + from sklearn.mixture._base import check_random_state, ConvergenceWarning + +except ImportError: + GaussianMixture = None + sklearn = False + ############################################################################### # Disclaimer: the following classes built upon the GaussianMixture class # @@ -31,7 +41,7 @@ ############################################################################### -class WeightedGaussianMixture(GaussianMixture): +class WeightedGaussianMixture(GaussianMixture if sklearn else object): """ Weighted Gaussian mixture class @@ -65,6 +75,7 @@ class WeightedGaussianMixture(GaussianMixture): Active indexes """ + @requires({"sklearn": sklearn}) def __init__( self, n_components, @@ -841,6 +852,7 @@ class GaussianMixtureWithPrior(WeightedGaussianMixture): Shape is (index of the fixed cell, lithology index) fixed_membership: """ + @requires({"sklearn": sklearn}) def __init__( self, gmmref, @@ -1236,6 +1248,7 @@ class GaussianMixtureWithNonlinearRelationships(WeightedGaussianMixture): List of mapping describing a nonlinear relationships between physical properties; one per cluster/unit. """ + @requires({"sklearn": sklearn}) def __init__( self, mesh, @@ -1546,6 +1559,7 @@ class GaussianMixtureWithNonlinearRelationshipsWithPrior(GaussianMixtureWithPrio """ + @requires({"sklearn": sklearn}) def __init__( self, gmmref, diff --git a/tests/utils/test_report.py b/tests/utils/test_report.py index 828d06ec3f..bd6238712e 100644 --- a/tests/utils/test_report.py +++ b/tests/utils/test_report.py @@ -15,11 +15,9 @@ def test_version_defaults(self): "pymatsolver", "numpy", "scipy", - "sklearn", "matplotlib", "empymod", "geoana", - "pandas", ], # Optional packages. optional=[ @@ -27,6 +25,8 @@ def test_version_defaults(self): "pydiso", "numba", "dask", + "sklearn", + "pandas", "sympy", "IPython", "ipywidgets", From 7bb6088629f79e9ba8260be0a0af392c0dc6ebec Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Sat, 31 Aug 2024 14:23:49 -0600 Subject: [PATCH 053/194] Dask races (#1469) #### Summary Updates for `dask>2024.2.1` #### PR Checklist * [ ] If this is a work in progress PR, set as a Draft PR * [ ] Linted my code according to the [style guides](https://docs.simpeg.xyz/content/getting_started/contributing/code-style.html). * [ ] Added [tests](https://docs.simpeg.xyz/content/getting_started/practices.html#testing) to verify changes to the code. * [ ] Added necessary documentation to any new functions/classes following the expect [style](https://docs.simpeg.xyz/content/getting_started/practices.html#documentation). * [ ] Marked as ready for review (if this is was a draft PR), and converted to a Pull Request * [ ] Tagged ``@simpeg/simpeg-developers`` when ready for review. #### What does this implement/fix? A few things: 1) dask recently updated how things items are hashed to create a more deterministic hash for items. For objects it will change depending on the state of the objects. `Simulation` objects are mutable things, thus their hash would change overtime for the same object. For the `DaskMetaSimulation`, we want a single simulation's hash (also a single map's hash) to be constant in time, so this PR adds uuid properties to each of those base classes that are then registered to be used by dask to hash the objects. 2) This deterministic hashing lead to a race condition in the error testing. When objects are scattered, dereferenced, then scattered again. The garbage collector would destroy the scattered objects dereferenced from the first call `after` the second scatter, thus creating a case where we are trying to access a Canceled Future. 3) This is also now a bit more rigorous about only using a single worker for each simulation operation when the simulations inadvertently live on multiple workers, by selecting the worker that had fewer simulations assigned to it. #### Additional information See https://github.com/dask/distributed/issues/8576 for more information about the race condition. This also implements one of the updates in #1444 regarding the testing environment script so that we are sure we are creating the correct environment. --- .ci/azure/test.yml | 5 +- simpeg/maps/_base.py | 3 + simpeg/meta/dask_sim.py | 141 +++++++++++++++++++++++++-------- simpeg/meta/multiprocessing.py | 18 ++--- simpeg/simulation.py | 3 + tests/meta/test_dask_meta.py | 9 +++ 6 files changed, 132 insertions(+), 47 deletions(-) diff --git a/.ci/azure/test.yml b/.ci/azure/test.yml index 5f7a3901cc..d8e5974859 100644 --- a/.ci/azure/test.yml +++ b/.ci/azure/test.yml @@ -2,7 +2,8 @@ parameters: os : ['ubuntu-latest'] py_vers: ['3.8'] test: ['tests/em', - 'tests/base tests/flow tests/seis tests/utils tests/meta', + 'tests/base tests/flow tests/seis tests/utils', + 'tests/meta', 'tests/docs -s -v', 'tests/examples/test_examples_1.py', 'tests/examples/test_examples_2.py', @@ -10,7 +11,7 @@ parameters: 'tests/examples/test_tutorials_1.py tests/examples/test_tutorials_2.py', 'tests/examples/test_tutorials_3.py', 'tests/pf', - 'tests/dask', # This must be ran on it's own to avoid modifying the code from any other tests. + 'tests/dask', # This code must be tested on its own to avoid modifying the implementation for any other tests. ] jobs: diff --git a/simpeg/maps/_base.py b/simpeg/maps/_base.py index 79effec39a..199887f3ca 100644 --- a/simpeg/maps/_base.py +++ b/simpeg/maps/_base.py @@ -11,6 +11,7 @@ from scipy.sparse import csr_matrix as csr from discretize.tests import check_derivative from discretize.utils import Zero, Identity, mkvc, speye, sdiag +import uuid from ..utils import ( mat_utils, @@ -71,6 +72,8 @@ def __init__(self, mesh=None, nP=None, **kwargs): self.mesh = mesh self._nP = nP + self._uuid = uuid.uuid4() + super().__init__(**kwargs) @property diff --git a/simpeg/meta/dask_sim.py b/simpeg/meta/dask_sim.py index 5f6f09e064..701dffe323 100644 --- a/simpeg/meta/dask_sim.py +++ b/simpeg/meta/dask_sim.py @@ -13,6 +13,18 @@ from operator import add import warnings +from dask.base import normalize_token + + +@normalize_token.register(IdentityMap) +def _normalize_map(mapping): + return mapping._uuid.hex + + +@normalize_token.register(BaseSimulation) +def _normalize_simulation(sim): + return sim._uuid.hex + def _store_model(mapping, sim, model): sim.model = mapping * model @@ -59,13 +71,17 @@ def _get_jtj_diag(mapping, sim, model, field, w, apply_map=False): return np.asarray((sim_jtj @ m_deriv).power(2).sum(axis=0)).flatten() -def _reduce(client, operation, items): +def _reduce(client, operation, items, workers): + # first sort by workers so items on the same workers are mapped together. + items = [val for (_, val) in sorted(zip(workers, items), key=lambda x: x[0])] while len(items) > 1: - new_reduce = client.map(operation, items[::2], items[1::2]) + new_reduce = client.map(operation, items[::2], items[1::2], pure=False) if len(items) % 2 == 1: - new_reduce[-1] = client.submit(operation, new_reduce[-1], items[-1]) + new_reduce[-1] = client.submit( + operation, new_reduce[-1], items[-1], pure=False + ) items = new_reduce - return client.gather(items[0]) + return items[0].result() def _validate_type_or_future_of_type( @@ -84,9 +100,11 @@ def _validate_type_or_future_of_type( if workers is None: objects = client.scatter(objects) else: + # If workers are already set, move the object to the respective worker. tmp = [] for obj, worker in zip(objects, workers): - tmp.append(client.scatter([obj], workers=worker)[0]) + future = client.scatter(obj, workers=worker) + tmp.append(future) objects = tmp except TypeError: pass @@ -99,14 +117,43 @@ def _validate_type_or_future_of_type( # Figure out where everything lives who = client.who_has(objects) if workers is None: - workers = [] + # Because we only ever want to allow execution on a single consistent + # worker for each simulation-mapping pair, we need to do a bit of sanity + # checking to choose which worker if the object exists on multiple + # workers. + + # find out how objects have been assigned to each worker. + workers_assign_count = {} for obj in objects: - workers.append(who[obj.key]) + workers = who[obj.key] + for worker in workers: + workers_assign_count[worker] = workers_assign_count.get(worker, 0) + 1 + + # then loop through and if they exist on multiple workers, + # choose the worker with the fewest assignments. + # then decrement any other workers + worker_assignments = [] + for obj in objects: + workers = who[obj.key] + n_assigned = len(objects) + assigned = None + for worker in workers: + n_test = workers_assign_count[worker] + # choose the worker with the least assigned tasks: + if n_test < n_assigned: + assigned = worker + n_assigned = n_test + # discount workers who had this object but were not chosen: + for worker in workers: + if worker != assigned: + workers_assign_count[worker] -= 1 + worker_assignments.append(assigned) + workers = worker_assignments else: # Issue a warning if the future is not on the expected worker for i, (obj, worker) in enumerate(zip(objects, workers)): - obj_owner = client.who_has(obj)[obj.key] - if obj_owner != worker: + obj_owners = client.who_has(obj)[obj.key] + if worker not in obj_owners: warnings.warn( f"{property_name} {i} is not on the expected worker.", stacklevel=2 ) @@ -115,7 +162,9 @@ def _validate_type_or_future_of_type( futures = [] for obj, worker in zip(objects, workers): futures.append( - client.submit(lambda v: not isinstance(v, obj_type), obj, workers=worker) + client.submit( + lambda v: not isinstance(v, obj_type), obj, workers=worker, pure=False + ) ) is_not_obj = np.array(client.gather(futures)) if np.any(is_not_obj): @@ -159,7 +208,9 @@ def _make_survey(self): vnD = [] client = self.client for sim, worker in zip(self.simulations, self._workers): - vnD.append(client.submit(lambda s: s.survey.nD, sim, workers=worker)) + vnD.append( + client.submit(lambda s: s.survey.nD, sim, workers=worker, pure=False) + ) vnD = client.gather(vnD) survey._vnD = vnD return survey @@ -214,7 +265,9 @@ def mappings(self, value): ) # validate mapping shapes and simulation shapes - model_len = client.submit(lambda v: v.shape[1], mappings[0]).result() + model_len = client.submit( + lambda v: v.shape[1], mappings[0], pure=False + ).result() def check_mapping(mapping, sim, model_len): if mapping.shape[1] != model_len: @@ -236,10 +289,12 @@ def check_mapping(mapping, sim, model_len): error_checks = [] for mapping, sim, worker in zip(mappings, self.simulations, workers): - # if it was a repeat sim, this should cause the simulation to be transfered - # to each worker. + # if it was a repeat sim, this should cause the simulation to be transferred + # to each worker if it was originally passed as a future. error_checks.append( - client.submit(check_mapping, mapping, sim, model_len, workers=worker) + client.submit( + check_mapping, mapping, sim, model_len, workers=worker, pure=False + ) ) error_checks = np.asarray(client.gather(error_checks)) @@ -266,6 +321,7 @@ def _model_map(self): lambda v: v.shape[1], self.mappings[0], workers=self._workers[0], + pure=False, ) n_m = client.gather(n_m) self.__model_map = IdentityMap(nP=n_m) @@ -291,7 +347,7 @@ def model(self, value): # Only send the model to the internal simulations if it was updated. if updated: client = self.client - [self._m_as_future] = client.scatter([self._model], broadcast=True) + self._m_as_future = client.scatter(self._model, broadcast=True, hash=False) if not self._repeat_sim: futures = [] for mapping, sim, worker in zip( @@ -304,6 +360,7 @@ def model(self, value): sim, self._m_as_future, workers=worker, + pure=False, ) ) self.client.gather( @@ -325,6 +382,7 @@ def fields(self, m): m_future, self._repeat_sim, workers=worker, + pure=False, ) ) return f @@ -349,6 +407,7 @@ def dpred(self, m=None, f=None): field, self._repeat_sim, workers=worker, + pure=False, ) ) return np.concatenate(client.gather(dpred)) @@ -359,7 +418,7 @@ def Jvec(self, m, v, f=None): if f is None: f = self.fields(m) client = self.client - [v_future] = client.scatter([v], broadcast=True) + v_future = client.scatter(v, broadcast=True, hash=False) j_vec = [] for mapping, sim, worker, field in zip( self.mappings, self.simulations, self._workers, f @@ -374,6 +433,7 @@ def Jvec(self, m, v, f=None): v_future, self._repeat_sim, workers=worker, + pure=False, ) ) return np.concatenate(self.client.gather(j_vec)) @@ -398,11 +458,12 @@ def Jtvec(self, m, v, f=None): v[self._data_offsets[i] : self._data_offsets[i + 1]], self._repeat_sim, workers=worker, + pure=False, ) ) # Do the sum by a reduction operation to avoid gathering a vector # of size n_simulations by n_model parameters on the head. - return _reduce(client, add, jt_vec) + return _reduce(client, add, jt_vec, workers=self._workers) def getJtJdiag(self, m, W=None, f=None): self.model = m @@ -430,9 +491,10 @@ def getJtJdiag(self, m, W=None, f=None): sim_w, self._repeat_sim, workers=worker, + pure=False, ) ) - self._jtjdiag = _reduce(client, add, jtj_diag) + self._jtjdiag = _reduce(client, add, jtj_diag, workers=self._workers) return self._jtjdiag @@ -461,7 +523,9 @@ def __init__(self, simulations, mappings, client): def _make_survey(self): survey = BaseSurvey([]) client = self.client - n_d = client.submit(lambda s: s.survey.nD, self.simulations[0]).result() + n_d = client.submit( + lambda s: s.survey.nD, self.simulations[0], pure=False + ).result() survey._vnD = [ n_d, ] @@ -473,11 +537,15 @@ def simulations(self, value): simulations, workers = _validate_type_or_future_of_type( "simulations", value, BaseSimulation, client, return_workers=True ) - n_d = client.submit(lambda s: s.survey.nD, simulations[0], workers=workers[0]) + n_d = client.submit( + lambda s: s.survey.nD, simulations[0], workers=workers[0], pure=False + ) sim_check = [] for sim, worker in zip(simulations, workers): sim_check.append( - client.submit(lambda s, n: s.survey.nD != n, sim, n_d, workers=worker) + client.submit( + lambda s, n: s.survey.nD != n, sim, n_d, workers=worker, pure=False + ) ) if np.any(client.gather(sim_check)): raise ValueError("All simulations must have the same number of data.") @@ -493,16 +561,18 @@ def dpred(self, m=None, f=None): dpred = [] for sim, worker, field in zip(self.simulations, self._workers, f): dpred.append( - client.submit(_calc_dpred, None, sim, None, field, workers=worker) + client.submit( + _calc_dpred, None, sim, None, field, workers=worker, pure=False + ) ) - return _reduce(client, add, dpred) + return _reduce(client, add, dpred, workers=self._workers) def Jvec(self, m, v, f=None): self.model = m if f is None: f = self.fields(m) client = self.client - [v_future] = client.scatter([v], broadcast=True) + v_future = client.scatter(v, broadcast=True, hash=False) j_vec = [] for mapping, sim, worker, field in zip( self.mappings, self._simulations, self._workers, f @@ -516,9 +586,10 @@ def Jvec(self, m, v, f=None): field, v_future, workers=worker, + pure=False, ) ) - return _reduce(client, add, j_vec) + return _reduce(client, add, j_vec, workers=self._workers) def Jtvec(self, m, v, f=None): self.model = m @@ -538,11 +609,12 @@ def Jtvec(self, m, v, f=None): field, v, workers=worker, + pure=False, ) ) # Do the sum by a reduction operation to avoid gathering a vector # of size n_simulations by n_model parameters on the head. - return _reduce(client, add, jt_vec) + return _reduce(client, add, jt_vec, workers=self._workers) def getJtJdiag(self, m, W=None, f=None): self.model = m @@ -567,9 +639,10 @@ def getJtJdiag(self, m, W=None, f=None): field, W, workers=worker, + pure=False, ) ) - self._jtjdiag = _reduce(client, add, jtj_diag) + self._jtjdiag = _reduce(client, add, jtj_diag, workers=self._workers) return self._jtjdiag @@ -607,7 +680,9 @@ def __init__(self, simulation, mappings, client): def _make_survey(self): survey = BaseSurvey([]) - nD = self.client.submit(lambda s: s.survey.nD, self.simulation).result() + nD = self.client.submit( + lambda s: s.survey.nD, self.simulation, pure=False + ).result() survey._vnD = len(self.mappings) * [nD] return survey @@ -630,12 +705,12 @@ def simulation(self, value): client = self.client if isinstance(value, BaseSimulation): # Scatter sim to every client - [ - value, - ] = client.scatter([value], broadcast=True) + value = client.scatter(value, broadcast=True) if not ( isinstance(value, Future) - and client.submit(lambda s: isinstance(s, BaseSimulation), value).result() + and client.submit( + lambda s: isinstance(s, BaseSimulation), value, pure=False + ).result() ): raise TypeError( "simulation must be an instance of BaseSimulation or a Future that returns" diff --git a/simpeg/meta/multiprocessing.py b/simpeg/meta/multiprocessing.py index f5aceceda6..b637ac0072 100644 --- a/simpeg/meta/multiprocessing.py +++ b/simpeg/meta/multiprocessing.py @@ -8,10 +8,9 @@ class SimpleFuture: """Represents an object stored on a seperate simulation process.""" - def __init__(self, item_id, t_queue, r_queue): + def __init__(self, item_id, sim_process): self.item_id = item_id - self.t_queue = t_queue - self.r_queue = r_queue + self.sim_process = sim_process # This doesn't quite work well yet, # Due to the fact that some fields objects from the PDE @@ -25,13 +24,8 @@ def __init__(self, item_id, t_queue, r_queue): # return item def __del__(self): - # Tell the child process that this object is no longer needed in its cache. - try: - self.t_queue.put(("del_item", (self.item_id,))) - except ValueError: - # if the queue was already closed it will throw a value error - # so catch it here gracefully and continue on. - pass + if self.sim_process.is_alive(): + self.sim_process.task_queue.put(("del_item", (self.item_id,))) class _SimulationProcess(Process): @@ -124,7 +118,7 @@ def set_sim(self, sim): self._check_closed() self.task_queue.put(("set_sim", (sim,))) key = self.result_queue.get() - future = SimpleFuture(key, self.task_queue, self.result_queue) + future = SimpleFuture(key, self) self._my_sim = future return future @@ -138,7 +132,7 @@ def get_fields(self): sim = self._my_sim self.task_queue.put((1, (sim.item_id,))) key = self.result_queue.get() - future = SimpleFuture(key, self.task_queue, self.result_queue) + future = SimpleFuture(key, self) return future def start_dpred(self, f_future): diff --git a/simpeg/simulation.py b/simpeg/simulation.py index e1091b1997..db48739f2c 100644 --- a/simpeg/simulation.py +++ b/simpeg/simulation.py @@ -27,6 +27,7 @@ validate_string, validate_integer, ) +import uuid try: from pymatsolver import Pardiso as DefaultSolver @@ -103,6 +104,8 @@ def __init__( self.counter = counter self.verbose = verbose + self._uuid = uuid.uuid4() + super().__init__(**kwargs) @property diff --git a/tests/meta/test_dask_meta.py b/tests/meta/test_dask_meta.py index 5feb5f6c75..bca049224a 100644 --- a/tests/meta/test_dask_meta.py +++ b/tests/meta/test_dask_meta.py @@ -5,6 +5,7 @@ from discretize import TensorMesh import scipy.sparse as sp import pytest +import time from simpeg.meta import ( MetaSimulation, @@ -289,6 +290,7 @@ def test_dask_meta_errors(cluster): # incompatible length of mappings and simulations lists with pytest.raises(ValueError): DaskMetaSimulation(sims[:-1], mappings, client) + time.sleep(0.1) # sleep for a bit to let the communicator catch up # Bad Simulation type? with pytest.raises(TypeError): @@ -300,16 +302,19 @@ def test_dask_meta_errors(cluster): mappings, client, ) + time.sleep(0.1) # sleep for a bit to let the communicator catch up # mappings have incompatible input lengths: mappings[0] = maps.Projection(mesh.n_cells + 10, np.arange(mesh.n_cells) + 1) with pytest.raises(ValueError): DaskMetaSimulation(sims, mappings, client) + time.sleep(0.1) # sleep for a bit to let the communicator catch up # incompatible mapping and simulation mappings[0] = maps.Projection(mesh.n_cells, [0, 1, 3, 5, 10]) with pytest.raises(ValueError): DaskMetaSimulation(sims, mappings, client) + time.sleep(0.1) # sleep for a bit to let the communicator catch up def test_sum_errors(cluster): @@ -351,6 +356,7 @@ def test_sum_errors(cluster): # Test simulations with different numbers of data. with pytest.raises(ValueError): DaskSumMetaSimulation(sims, mappings, client) + time.sleep(0.1) # sleep for a half second to let the communicator catch up def test_repeat_errors(cluster): @@ -382,12 +388,15 @@ def test_repeat_errors(cluster): mappings[0] = maps.Projection(mesh.n_cells + 1, np.arange(mesh.n_cells) + 1) with pytest.raises(ValueError): DaskRepeatedSimulation(sim, mappings, client) + time.sleep(0.1) # sleep for a half second to let the communicator catch up # incompatible mappings and simulations mappings[0] = maps.Projection(mesh.n_cells, [0, 1, 3, 5, 10]) with pytest.raises(ValueError): DaskRepeatedSimulation(sim, mappings, client) + time.sleep(0.1) # sleep for a half second to let the communicator catch up # Bad Simulation type? with pytest.raises(TypeError): DaskRepeatedSimulation(lambda x: x * 2, mappings, client) + time.sleep(0.1) # sleep for a half second to let the communicator catch up From a95c421d708257b79a8907141937b8f6e246d4a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dieter=20Werthm=C3=BCller?= Date: Sat, 31 Aug 2024 22:28:18 +0200 Subject: [PATCH 054/194] Fix validate_ndarray_with_shape (#1523) #### Summary The current implementation of `simpeg.utils.code_utils.validate_ndarray_with_shape` works for numbers or lists, but fails for `np.ndarray`'s. Example ```python import numpy as np from simpeg.utils.code_utils import validate_ndarray_with_shape ``` First a list, all as expected, it returns a complex numpy array. ```python validate_ndarray_with_shape('Test 1', [1+1j, 2+2j], '*', (float, complex)) ``` ``` array([1.+1.j, 2.+2.j]) ``` Now directly for a complex numpy array: It casts the array to the first provided dtypes, here floats, and raises a warning if that casting has some side-effects, here that it casts complex values to real values. Not what was wanted. ```python validate_ndarray_with_shape('Test 1', np.array([1+1j, 2+2j]), '*', (float, complex)) ``` ``` /simpeg/utils/code_utils.py:1044: ComplexWarning: Casting complex values to real discards the imaginary part var = np.asarray(var, dtype=dtype) array([1., 2.]) ``` #### PR Checklist * [x] If this is a work in progress PR, set as a Draft PR * [x] Linted my code according to the [style guides](https://docs.simpeg.xyz/content/getting_started/contributing/code-style.html). * [x] Added [tests](https://docs.simpeg.xyz/content/getting_started/practices.html#testing) to verify changes to the code. * [x] Added necessary documentation to any new functions/classes following the expect [style](https://docs.simpeg.xyz/content/getting_started/practices.html#documentation). * [x] Marked as ready for review (if this is was a draft PR), and converted to a Pull Request * [x] Tagged ``@simpeg/simpeg-developers`` when ready for review. @simpeg/simpeg-developers - small fix, should be ready. --- simpeg/utils/code_utils.py | 5 ++++- tests/base/test_validators.py | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/simpeg/utils/code_utils.py b/simpeg/utils/code_utils.py index 691865d13a..30debf8cd9 100644 --- a/simpeg/utils/code_utils.py +++ b/simpeg/utils/code_utils.py @@ -1035,7 +1035,10 @@ def validate_ndarray_with_shape(property_name, var, shape=None, dtype=float): dtypes = dtype for dtype in dtypes: try: - var = np.asarray(var, dtype=dtype) + if isinstance(var, np.ndarray): + var = var.astype(dtype, casting="safe", copy=False) + else: + var = np.asarray(var, dtype=dtype) bad_type = False break except (TypeError, ValueError) as err: diff --git a/tests/base/test_validators.py b/tests/base/test_validators.py index 2d2df0eb87..c80f8d59a4 100644 --- a/tests/base/test_validators.py +++ b/tests/base/test_validators.py @@ -223,6 +223,12 @@ def test_ndarray_validation(): assert np.issubdtype(out.dtype, complex) np.testing.assert_equal(out, np.array([3.0j, 4.0j, 5.0j])) + out = validate_ndarray_with_shape( + "array_prop", np.array([3j, 4j, 5j]), dtype=(float, complex) + ) + assert np.issubdtype(out.dtype, complex) + np.testing.assert_equal(out, np.array([3.0j, 4.0j, 5.0j])) + # Valid any shaped arrays assert validate_ndarray_with_shape( "NDarrayProperty", np.random.rand(3, 3, 3), ("*", "*", "*"), float From c6ca1375a899f27a065d540d8696b34ca57b11c5 Mon Sep 17 00:00:00 2001 From: domfournier Date: Mon, 2 Sep 2024 08:43:46 -0700 Subject: [PATCH 055/194] Irls refactor (#1349) #### Summary Refactoring of the UpdateIRLS directive used to control the `Sparse` regularization. - Attribute name changes to snake_case format - Remove unused attributes and change to instance attributes. - Move responsibility for the scaling of spherical parameters into a new `SphericalDomain` directive. - Move responsibility for the cooling schedule to a `BetaSchedule` directive. #### PR Checklist * [x ] If this is a work in progress PR, set as a Draft PR * [x] Linted my code according to the [style guides](https://docs.simpeg.xyz/content/getting_started/contributing/code-style.html). * [x] Added [tests](https://docs.simpeg.xyz/content/getting_started/practices.html#testing) to verify changes to the code. * [x] Added necessary documentation to any new functions/classes following the expect [style](https://docs.simpeg.xyz/content/getting_started/practices.html#documentation). * [x] Marked as ready for review (if this is was a draft PR), and converted to a Pull Request * [x ] Tagged ``@simpeg/simpeg-developers`` when ready for review. #### Reference issue Related to restructuring proposed on #1327 #### What does this implement/fix? Reduces complexity of current `Update_IRLS` class + PEP8 standard. #### Additional information Clean up deprecation warnings on #1472 --------- Co-authored-by: Santiago Soler Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- simpeg/directives/__init__.py | 6 +- simpeg/directives/_regularization.py | 492 ++++++++++++++++++ simpeg/directives/directives.py | 19 +- simpeg/inverse_problem.py | 4 +- tests/base/test_directives.py | 117 ++++- .../plot_inv_1b_gravity_anomaly_irls.py | 7 +- 6 files changed, 630 insertions(+), 15 deletions(-) create mode 100644 simpeg/directives/_regularization.py diff --git a/simpeg/directives/__init__.py b/simpeg/directives/__init__.py index 69081767d1..bbffa1ea9c 100644 --- a/simpeg/directives/__init__.py +++ b/simpeg/directives/__init__.py @@ -55,6 +55,8 @@ .. autosummary:: :toctree: generated/ + UpdateIRLS + SphericalUnitsWeights Update_IRLS @@ -108,7 +110,6 @@ SaveModelEveryIteration, SaveOutputEveryIteration, SaveOutputDictEveryIteration, - Update_IRLS, UpdatePreconditioner, Update_Wj, AlphasSmoothEstimate_ByEig, @@ -116,6 +117,7 @@ ScalingMultipleDataMisfits_ByEig, JointScalingSchedule, UpdateSensitivityWeights, + Update_IRLS, ProjectSphericalBounds, ) @@ -125,6 +127,8 @@ PGI_AddMrefInSmooth, ) +from ._regularization import UpdateIRLS, SphericalUnitsWeights + from .sim_directives import ( SimilarityMeasureInversionDirective, SimilarityMeasureSaveOutputEveryIteration, diff --git a/simpeg/directives/_regularization.py b/simpeg/directives/_regularization.py new file mode 100644 index 0000000000..686813ed38 --- /dev/null +++ b/simpeg/directives/_regularization.py @@ -0,0 +1,492 @@ +from __future__ import annotations + +import warnings + +import numpy as np +from dataclasses import dataclass + +from ..maps import Projection +from .directives import InversionDirective, UpdatePreconditioner, BetaSchedule +from ..regularization import ( + Sparse, + BaseSparse, + SmoothnessFirstOrder, + WeightedLeastSquares, +) +from ..utils import validate_integer, validate_float + + +@dataclass +class IRLSMetrics: + """ + Data class to store metrics used by the IRLS algorithm. + + Parameters + ---------- + input_norms : list of floats or None + List of norms temporarily stored during the initialization. + irls_iteration_count : int + Number of IRLS iterations. + start_irls_iter : int or None + Iteration number when the IRLS process started. + f_old : float + Previous value of the regularization function. + """ + + input_norms: list[float] | None = None + irls_iteration_count: int = 0 + start_irls_iter: int | None = None + f_old: float = 0.0 + + +class UpdateIRLS(InversionDirective): + """ + Directive to control the IRLS iterations for :class:`~simpeg.regularization.Sparse`. + + Parameters + ---------- + cooling_rate: int + Number of iterations to cool beta. + cooling_factor: float + Factor to cool beta. + chifact_start: float + Starting chi factor for the IRLS iterations. + chifact_target: float + Target chi factor for the IRLS iterations. + irls_cooling_factor: float + Factor to cool the IRLS threshold epsilon. + f_min_change: float + Minimum change in the regularization function to continue the IRLS iterations. + max_irls_iterations: int + Maximum number of IRLS iterations. + misfit_tolerance: float + Tolerance for the target misfit. + percentile: float + Percentile of the function values used to determine the initial IRLS threshold. + verbose: bool + Print information to the screen. + """ + + def __init__( + self, + cooling_rate: int = 1, + cooling_factor: float = 2.0, + chifact_start: float = 1.0, + chifact_target: float = 1.0, + irls_cooling_factor: float = 1.2, + f_min_change: float = 1e-2, + max_irls_iterations: int = 20, + misfit_tolerance: float = 1e-1, + percentile: float = 100.0, + verbose: bool = True, + **kwargs, + ): + self._metrics: IRLSMetrics | None = None + self.cooling_rate = cooling_rate + self.cooling_factor = cooling_factor + self.chifact_start: float = chifact_start + self.chifact_target: float = chifact_target + self.irls_cooling_factor: float = irls_cooling_factor + self.f_min_change: float = f_min_change + self.max_irls_iterations: int = max_irls_iterations + self.misfit_tolerance: float = misfit_tolerance + self.percentile: float = percentile + + super().__init__( + verbose=verbose, + **kwargs, + ) + + @property + def metrics(self) -> IRLSMetrics: + """Various metrics used by the IRLS algorithm.""" + if self._metrics is None: + self._metrics = IRLSMetrics() + return self._metrics + + @property + def max_irls_iterations(self) -> int: + """Maximum irls iterations.""" + return self._max_irls_iterations + + @max_irls_iterations.setter + def max_irls_iterations(self, value): + self._max_irls_iterations = validate_integer( + "max_irls_iterations", value, min_val=0 + ) + + @property + def misfit_tolerance(self) -> float: + """Tolerance on deviation from the target chi factor, as a fractional percent.""" + return self._misfit_tolerance + + @misfit_tolerance.setter + def misfit_tolerance(self, value): + self._misfit_tolerance = validate_float("misfit_tolerance", value, min_val=0) + + @property + def percentile(self) -> float: + """Tolerance on deviation from the target chi factor, as a fractional percent.""" + return self._percentile + + @percentile.setter + def percentile(self, value): + self._percentile = validate_float( + "percentile", value, min_val=0.0, max_val=100.0 + ) + + @property + def chifact_start(self) -> float: + """Target chi factor to start the IRLS process.""" + return self._chifact_start + + @chifact_start.setter + def chifact_start(self, value): + self._chifact_start = validate_float( + "chifact_start", value, min_val=0, inclusive_min=False + ) + + @property + def chifact_target(self) -> float: + """Targer chi factor to maintain during the IRLS process.""" + return self._chifact_target + + @chifact_target.setter + def chifact_target(self, value): + self._chifact_target = validate_float( + "chifact_target", value, min_val=0, inclusive_min=False + ) + + @property + def cooling_factor(self): + """Beta is divided by this value every :attr:`cooling_rate` iterations. + + Returns + ------- + float + """ + return self._cooling_factor + + @cooling_factor.setter + def cooling_factor(self, value): + self._cooling_factor = validate_float( + "cooling_factor", value, min_val=0.0, inclusive_min=False + ) + + @property + def cooling_rate(self): + """Cool beta after this number of iterations. + + Returns + ------- + int + """ + return self._cooling_rate + + @cooling_rate.setter + def cooling_rate(self, value): + self._cooling_rate = validate_integer("cooling_rate", value, min_val=1) + + @property + def irls_cooling_factor(self) -> float: + """IRLS threshold parameter (epsilon) is divided by this value every iteration.""" + return self._irls_cooling_factor + + @irls_cooling_factor.setter + def irls_cooling_factor(self, value): + self._irls_cooling_factor = validate_float( + "irls_cooling_factor", value, min_val=0.0, inclusive_min=False + ) + + @property + def f_min_change(self) -> float: + """Target chi factor to start the IRLS process.""" + return self._f_min_change + + @f_min_change.setter + def f_min_change(self, value): + self._f_min_change = validate_float( + "f_min_change", value, min_val=0, inclusive_min=False + ) + + def misfit_from_chi_factor(self, chi_factor: float) -> float: + """ + Compute the target misfit from the chi factor. + + Parameters + ---------- + chi_factor : float + Chi factor to compute the target misfit from. + """ + value = 0 + + for survey in self.survey: + value += survey.nD * chi_factor + + return value + + def adjust_cooling_schedule(self): + """ + Adjust the cooling schedule based on the misfit. + """ + ratio = self.invProb.phi_d / self.misfit_from_chi_factor(self.chifact_target) + + if ( + np.abs(1.0 - ratio) > self.misfit_tolerance + and self.metrics.start_irls_iter is not None + ): + + if ratio > 1: + ratio = np.mean([2.0, ratio]) + else: + ratio = np.mean([0.75, ratio]) + + self.cooling_factor = ratio + + def initialize(self): + """ + Initialize the IRLS iterations with l2-norm regularization (mode:1). + """ + + input_norms = [] + for reg in self.reg.objfcts: + if not isinstance(reg, Sparse): + input_norms += [None] + else: + input_norms += [reg.norms] + reg.norms = [2.0 for _ in reg.objfcts] + + self._metrics = IRLSMetrics(input_norms=input_norms) + + def endIter(self): + """ + Check on progress of the inversion and start/update the IRLS process. + """ + # After reaching target misfit with l2-norm, switch to IRLS (mode:2) + if ( + self.metrics.start_irls_iter is None + and self.invProb.phi_d < self.misfit_from_chi_factor(self.chifact_start) + ): + self.start_irls() + + # Check if misfit is within the tolerance, otherwise scale beta + self.adjust_cooling_schedule() + + # Only update after GN iterations + if ( + self.metrics.start_irls_iter is not None + and (self.opt.iter - self.metrics.start_irls_iter) % self.cooling_rate == 0 + ): + if self.stopping_criteria(): + self.opt.stopNextIteration = True + return + else: + self.opt.stopNextIteration = False + + # Print to screen + for reg in self.reg.objfcts: + if not isinstance(reg, Sparse): + continue + + for obj in reg.objfcts: + if isinstance(reg, (Sparse, BaseSparse)): + obj.irls_threshold /= self.irls_cooling_factor + + self.metrics.irls_iteration_count += 1 + + # Reset the regularization matrices so that it is + # recalculated for current model. Do it to all levels of comboObj + for reg in self.reg.objfcts: + if not isinstance(reg, Sparse): + continue + + reg.update_weights(reg.model) + + self.invProb.phi_m_last = self.reg(self.invProb.model) + + # Apply beta cooling schedule mechanism + if self.opt.iter > 0 and self.opt.iter % self.cooling_rate == 0: + self.invProb.beta /= self.cooling_factor + + def start_irls(self): + """ + Start the IRLS iterations by computing the initial threshold values. + """ + if self.verbose: + print( + "Reached starting chifact with l2-norm regularization:" + + " Start IRLS steps..." + ) + + self.metrics.start_irls_iter = getattr(self.opt, "iter", 0) + self.invProb.phi_m_last = self.reg(self.invProb.model) + + # Either use the supplied irls_threshold, or fix base on distribution of + # model values + for reg, norms in zip(self.reg.objfcts, self.metrics.input_norms): + if not isinstance(reg, Sparse): + continue + + for obj in reg.objfcts: + threshold = np.percentile( + np.abs(obj.mapping * obj._delta_m(self.invProb.model)), + self.percentile, + ) + if isinstance(obj, SmoothnessFirstOrder): + threshold /= reg.regularization_mesh.base_length + + obj.irls_threshold = threshold + + reg.norms = norms + + if self.verbose: + print("irls_threshold " + str(reg.objfcts[0].irls_threshold)) + + # Save l2-model + self.invProb.l2model = self.invProb.model.copy() + + def validate(self, directiveList=None): + directive_list = directiveList.dList + self_ind = directive_list.index(self) + lin_precond_ind = [isinstance(d, UpdatePreconditioner) for d in directive_list] + + if any(lin_precond_ind) and lin_precond_ind.index(True) < self_ind: + raise AssertionError( + "The directive 'UpdatePreconditioner' must be after Update_IRLS " + "in the directiveList" + ) + else: + warnings.warn( + "Without a Linear preconditioner, convergence may be slow. " + "Consider adding `directives.UpdatePreconditioner` to your " + "directives list", + stacklevel=2, + ) + + beta_schedule = [ + d for d in directive_list if isinstance(d, BetaSchedule) and d is not self + ] + + if beta_schedule: + raise AssertionError( + "Beta scheduling is handled by the `UpdateIRLS` directive." + "Remove the redundant `BetaSchedule` from your list of directives.", + ) + + spherical_scale = [isinstance(d, SphericalUnitsWeights) for d in directive_list] + if any(spherical_scale): + assert spherical_scale.index(True) < self_ind, ( + "The directive 'SphericalUnitsWeights' must be before UpdateIRLS " + "in the directiveList" + ) + + return True + + def stopping_criteria(self): + """ + Check for stopping criteria of max_irls_iteration or minimum change. + """ + phim_new = 0 + for reg in self.reg.objfcts: + if isinstance(reg, (Sparse, BaseSparse)): + reg.model = self.invProb.model + phim_new += reg(reg.model) + + # Check for maximum number of IRLS cycles + if self.metrics.irls_iteration_count == self.max_irls_iterations: + if self.verbose: + print( + "Reach maximum number of IRLS cycles:" + + f" {self.max_irls_iterations:d}" + ) + return True + + # Check if the function has changed enough + f_change = np.abs(self.metrics.f_old - phim_new) / (self.metrics.f_old + 1e-12) + + if ( + f_change < self.f_min_change + and self.metrics.irls_iteration_count > 1 + and np.abs( + 1.0 + - self.invProb.phi_d / self.misfit_from_chi_factor(self.chifact_target) + ) + < self.misfit_tolerance + ): + if self.verbose: + print("Minimum decrease in regularization. End of IRLS") + return True + + self.metrics.f_old = phim_new + + return False + + +class SphericalUnitsWeights(InversionDirective): + """ + Directive to update the regularization weights to account for spherical + parameters in radian and SI. + + The scaling applied to the regularization weights is based on the ratio + between the maximum value of the model and the maximum value of angles (pi). + + Parameters + ---------- + amplitude: Projection + Map to the model parameters for the amplitude of the vector + angles: list[WeightedLeastSquares] + List of WeightedLeastSquares for the angles. + verbose: bool + Print information to the screen. + """ + + def __init__( + self, + amplitude: Projection, + angles: list[WeightedLeastSquares], + verbose: bool = True, + **kwargs, + ): + + if not isinstance(amplitude, Projection): + raise TypeError( + "Attribute 'amplitude' must be of type " "'wires.Projection'" + ) + + self._amplitude = amplitude + + if not isinstance(angles, (list, tuple)) or not all( + [isinstance(fun, WeightedLeastSquares) for fun in angles] + ): + raise TypeError( + "Attribute 'angles' must be a list of " + "'regularization.WeightedLeastSquares'." + ) + + self._angles = angles + + super().__init__( + verbose=verbose, + **kwargs, + ) + + def initialize(self): + self.update_scaling() + + def endIter(self): + self.update_scaling() + + def update_scaling(self): + """ + Add an 'angle_scale' to the list of weights on the angle regularization for the + different block of models to account for units of radian and SI. + """ + amplitude = self._amplitude * self.invProb.model + max_p = max(amplitude) + + for reg in self._angles: + for obj in reg.objfcts: + if obj.units != "radian": + continue + + obj.set_weights(angle_scale=np.ones_like(amplitude) * max_p / np.pi) diff --git a/simpeg/directives/directives.py b/simpeg/directives/directives.py index 75ad4834ab..22fd36dc40 100644 --- a/simpeg/directives/directives.py +++ b/simpeg/directives/directives.py @@ -1,4 +1,7 @@ from __future__ import annotations # needed to use type operands in Python 3.8 + +from typing import TYPE_CHECKING + import numpy as np import matplotlib.pyplot as plt import warnings @@ -6,7 +9,7 @@ import scipy.sparse as sp from ..typing import RandomSeed from ..data_misfit import BaseDataMisfit -from ..objective_function import ComboObjectiveFunction +from ..objective_function import BaseObjectiveFunction, ComboObjectiveFunction from ..maps import IdentityMap, Wires from ..regularization import ( WeightedLeastSquares, @@ -32,6 +35,7 @@ validate_string, ) from ..utils.code_utils import ( + deprecate_class, deprecate_property, validate_type, validate_integer, @@ -39,6 +43,10 @@ validate_ndarray_with_shape, ) +if TYPE_CHECKING: + from ..simulation import BaseSimulation + from ..survey import BaseSurvey + class InversionDirective: """Base inversion directive class. @@ -140,7 +148,7 @@ def opt(self): return self.invProb.opt @property - def reg(self): + def reg(self) -> BaseObjectiveFunction: """Regularization associated with the directive. Returns @@ -164,7 +172,7 @@ def reg(self, value): self._reg = value @property - def dmisfit(self): + def dmisfit(self) -> BaseObjectiveFunction: """Data misfit associated with the directive. Returns @@ -188,7 +196,7 @@ def dmisfit(self, value): self._dmisfit = value @property - def survey(self): + def survey(self) -> list[BaseSurvey]: """Return survey for all data misfits Assuming that ``dmisfit`` is always a ``ComboObjectiveFunction``, @@ -203,7 +211,7 @@ def survey(self): return [objfcts.simulation.survey for objfcts in self.dmisfit.objfcts] @property - def simulation(self): + def simulation(self) -> list[BaseSimulation]: """Return simulation for all data misfits. Assuming that ``dmisfit`` is always a ``ComboObjectiveFunction``, @@ -1963,6 +1971,7 @@ def endIter(self): self.outDict[self.opt.iter] = iterDict +@deprecate_class(removal_version="0.24.0", error=False) class Update_IRLS(InversionDirective): f_old = 0 f_min_change = 1e-2 diff --git a/simpeg/inverse_problem.py b/simpeg/inverse_problem.py index e554c95cee..8bc52a24c0 100644 --- a/simpeg/inverse_problem.py +++ b/simpeg/inverse_problem.py @@ -96,7 +96,7 @@ def counter(self, value): self._counter = value @property - def dmisfit(self): + def dmisfit(self) -> ComboObjectiveFunction: """The data misfit. Returns @@ -113,7 +113,7 @@ def dmisfit(self, value): self._dmisfit = value @property - def reg(self): + def reg(self) -> ComboObjectiveFunction: """The regularization object for the inversion Returns diff --git a/tests/base/test_directives.py b/tests/base/test_directives.py index f6450eb586..a47ae57205 100644 --- a/tests/base/test_directives.py +++ b/tests/base/test_directives.py @@ -116,14 +116,14 @@ def test_validation_in_inversion(self): with self.assertRaises(AssertionError): # validation should happen and this will fail # (IRLS needs to be before update_Jacobi) - inv = inversion.BaseInversion( + inversion.BaseInversion( invProb, directiveList=[betaest, update_Jacobi, IRLS] ) with self.assertRaises(AssertionError): # validation should happen and this will fail # (sensitivity_weights needs to be before betaest) - inv = inversion.BaseInversion( + inversion.BaseInversion( invProb, directiveList=[betaest, sensitivity_weights] ) @@ -258,7 +258,118 @@ def test_sensitivity_weighting_amplitude_minimum(self): # self.test_sensitivity_weighting_subroutine(test_weights, test_directive) - print("SENSITIVITY WEIGHTING BY AMPLIUTDE AND MAX ALUE TEST PASSED") + print("SENSITIVITY WEIGHTING BY AMPLITUDE AND MAX VALUE TEST PASSED") + + def test_irls_directive(self): + input_norms = [0.0, 1.0, 1.0, 1.0] + reg = regularization.Sparse(self.mesh) + reg.norms = input_norms + projection = maps.Projection(self.mesh.n_cells, np.arange(self.mesh.n_cells)) + + other_reg = regularization.WeightedLeastSquares(self.mesh) + + invProb = inverse_problem.BaseInvProblem(self.dmis, reg + other_reg, self.opt) + + beta_schedule = directives.BetaSchedule(coolingFactor=3) + + # Here is where the norms are applied + irls_directive = directives.UpdateIRLS( + cooling_factor=3, + chifact_start=100.0, + chifact_target=1.0, + irls_cooling_factor=1.2, + f_min_change=np.inf, + max_irls_iterations=20, + misfit_tolerance=1e-0, + percentile=100, + verbose=True, + ) + + assert irls_directive.cooling_factor == 3 + assert irls_directive.metrics is not None + + # TODO Move these assertion test to the 'test_validation_in_inversion' after update + with self.assertRaises(AssertionError): + inversion.BaseInversion( + invProb, directiveList=[beta_schedule, irls_directive] + ) + + with self.assertRaises(AssertionError): + inversion.BaseInversion( + invProb, directiveList=[beta_schedule, irls_directive] + ) + + spherical_weights = directives.SphericalUnitsWeights(projection, [reg]) + with self.assertRaises(AssertionError): + inversion.BaseInversion( + invProb, directiveList=[irls_directive, spherical_weights] + ) + + update_Jacobi = directives.UpdatePreconditioner() + with self.assertRaises(AssertionError): + inversion.BaseInversion( + invProb, directiveList=[update_Jacobi, irls_directive] + ) + + invProb.phi_d = 1.0 + self.opt.iter = 3 + invProb.model = np.random.randn(reg.regularization_mesh.nC) + inv = inversion.BaseInversion(invProb, directiveList=[irls_directive]) + + irls_directive.initialize() + assert irls_directive.metrics.input_norms == [input_norms, None] + assert reg.norms == [2.0, 2.0, 2.0, 2.0] + + irls_directive.inversion = inv + irls_directive.endIter() + + assert irls_directive.metrics.start_irls_iter == self.opt.iter + assert len(reg.objfcts[0]._weights) == 2 # With irls weights + assert len(other_reg.objfcts[0]._weights) == 1 # No irls + irls_directive.metrics.irls_iteration_count += 1 + irls_directive.endIter() + + assert self.opt.stopNextIteration + + # Test stopping criteria based on max_irls_iter + irls_directive.max_irls_iterations = 2 + assert irls_directive.stopping_criteria() + + # Test beta re-adjustment down + invProb.phi_d = 4.0 + irls_directive.misfit_tolerance = 0.1 + irls_directive.adjust_cooling_schedule() + assert irls_directive.cooling_factor == 2.0 + + # Test beta re-adjustment up + invProb.phi_d = 0.5 + irls_directive.adjust_cooling_schedule() + assert irls_directive.cooling_factor == 0.5 + + def test_spherical_weights(self): + reg = regularization.Sparse(self.mesh) + projection = maps.Projection(self.mesh.n_cells, np.arange(self.mesh.n_cells)) + for obj in reg.objfcts[1:]: + obj.units = "radian" + + with pytest.raises(TypeError, match="Attribute 'amplitude' must be of type"): + directives.SphericalUnitsWeights(reg, [reg]) + + with pytest.raises(TypeError, match="Attribute 'angles' must be a list of"): + directives.SphericalUnitsWeights(projection, ["abc"]) + + spherical_weights = directives.SphericalUnitsWeights(projection, [reg]) + + inv_prob = inverse_problem.BaseInvProblem(self.dmis, reg, self.opt) + model = np.abs(np.random.randn(reg.regularization_mesh.nC)) + inv_prob.model = model + inv = inversion.BaseInversion(inv_prob, directiveList=[spherical_weights]) + spherical_weights.inversion = inv + + spherical_weights.initialize() + + assert "angle_scale" not in reg.objfcts[0]._weights + assert reg.objfcts[1]._weights["angle_scale"].max() == model.max() / np.pi def tearDown(self): # Clean up the working directory diff --git a/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py b/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py index 7bfd6da31f..5bb75bfa9f 100644 --- a/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py +++ b/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py @@ -271,13 +271,12 @@ # Defines the directives for the IRLS regularization. This includes setting # the cooling schedule for the trade-off parameter. -update_IRLS = directives.Update_IRLS( +update_IRLS = directives.UpdateIRLS( f_min_change=1e-4, max_irls_iterations=30, - coolEpsFact=1.5, - beta_tol=1e-2, + irls_cooling_factor=1.5, + misfit_tolerance=1e-2, ) - # Options for outputting recovered models and predicted data for each beta. save_iteration = directives.SaveOutputEveryIteration(save_txt=False) From 291ca6c7417c310999a22e4b78ed3f2809935ed3 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 5 Sep 2024 15:31:54 -0700 Subject: [PATCH 056/194] Fix bug in GMM's `_check_weights` (#1526) Fix bug while checking range of 2d weights. Use Numpy's `a.any()` method instead of Python built-in `any()` function to check if the `weights` is within the `[0, 1]` range. The `any()` function doesn't work with 2d arrays of bools. Convert weights to array before checking shape to allow array-like `weights`. Update method's docstring. Add tests that catch the bug. Include more tests for the `_check_weights` method to extend coverage and cover different scenarios. --- simpeg/utils/pgi_utils.py | 8 +- .../test_pgi_regularization.py | 80 +++++++++++++++++++ 2 files changed, 85 insertions(+), 3 deletions(-) diff --git a/simpeg/utils/pgi_utils.py b/simpeg/utils/pgi_utils.py index 145850f03d..1c5c9c8f8a 100644 --- a/simpeg/utils/pgi_utils.py +++ b/simpeg/utils/pgi_utils.py @@ -212,12 +212,14 @@ def _check_weights(self, weights, n_components, n_samples): The proportions of components of each mixture. n_components : int Number of components. + n_samples : int or None + Number of samples. Returns ------- - weights : array, shape (n_components,) + weights : (n_components,) or (n_samples, n_components) numpy.ndarray """ - + weights = np.asarray(weights) if len(weights.shape) == 2: weights = check_array( weights, dtype=[np.float64, np.float32], ensure_2d=True @@ -230,7 +232,7 @@ def _check_weights(self, weights, n_components, n_samples): _check_shape(weights, (n_components,), "weights") # check range - if any(np.less(weights, 0.0)) or any(np.greater(weights, 1.0)): + if (weights < 0.0).any() or (weights > 1.0).any(): raise ValueError( "The parameter 'weights' should be in the range " "[0, 1], but got max value %.5f, min value %.5f" diff --git a/tests/base/regularizations/test_pgi_regularization.py b/tests/base/regularizations/test_pgi_regularization.py index b1f08e905f..d39a733e77 100644 --- a/tests/base/regularizations/test_pgi_regularization.py +++ b/tests/base/regularizations/test_pgi_regularization.py @@ -484,5 +484,85 @@ def test_removed_mref(): pgi.mref +class TestCheckWeights: + """Test the ``WeightedGaussianMixture._check_weights`` method.""" + + VALID_ARGS = { + "1d-array": (np.array([0.5, 0.2, 0.3]), 3, None), + "2d-array": (np.array([[0.5, 0.2, 0.3], [0.25, 0.70, 0.05]]), 3, 2), + "1d-list": ([0.5, 0.2, 0.3], 3, None), + "2d-list": ([[0.5, 0.2, 0.3], [0.25, 0.70, 0.05]], 3, 2), + } + INVALID_SHAPE = { + "1d-array": (np.array([0.5, 0.2, 0.3]), 5, None), + "2d-array": (np.array([[0.5, 0.2, 0.3], [0.25, 0.70, 0.05]]), 5, 13), + "1d-list": ([0.5, 0.2, 0.3], 5, None), + "2d-list": ([[0.5, 0.2, 0.3], [0.25, 0.70, 0.05]], 5, 13), + } + INVALID_RANGE = { + "1d-greater": (np.array([10.5, 0.2, 0.3]), 3, None), + "1d-lower": (np.array([-1.0, 0.2, 0.3]), 3, None), + "2d-greater": (np.array([[0.5, 0.2, 0.3], [10.25, 0.70, 0.05]]), 3, 2), + "2d-lower": (np.array([[0.5, 0.2, 0.3], [0.25, -0.70, 0.05]]), 3, 2), + } + INVALID_NORM = { + "1d-lower": (np.array([0.001, 0.2, 0.3]), 3, None), + "1d-greater": (np.array([0.99, 0.2, 0.3]), 3, None), + "2d-lower": (np.array([[0.001, 0.2, 0.3], [0.25, 0.70, 0.05]]), 3, 2), + "2d-greater": (np.array([[0.99, 0.2, 0.3], [0.25, 0.70, 0.05]]), 3, 2), + } + + @pytest.fixture + def mesh(self): + mesh = discretize.TensorMesh([2, 2, 2]) + return mesh + + @pytest.mark.parametrize("args", VALID_ARGS.values(), ids=VALID_ARGS.keys()) + def test_valid_arguments(self, mesh, args): + """ + Check if method doesn't fail if arguments are valid. + """ + weights, n_components, n_samples = args + WeightedGaussianMixture(n_components=1, mesh=mesh)._check_weights( + weights, n_components, n_samples + ) + + @pytest.mark.parametrize("args", INVALID_SHAPE.values(), ids=INVALID_SHAPE.keys()) + def test_invalid_shape(self, mesh, args): + """ + Check if method raise error upon weights with invalid shape. + """ + weights, n_components, n_samples = args + msg = "The parameter 'weights' should have the shape of" + with pytest.raises(ValueError, match=msg): + WeightedGaussianMixture(n_components=1, mesh=mesh)._check_weights( + weights, n_components, n_samples + ) + + @pytest.mark.parametrize("args", INVALID_RANGE.values(), ids=INVALID_RANGE.keys()) + def test_invalid_range(self, mesh, args): + """ + Check if method raise error upon weights with invalid range. + """ + weights, n_components, n_samples = args + msg = r"The parameter 'weights' should be in the range \[0, 1\]" + with pytest.raises(ValueError, match=msg): + WeightedGaussianMixture(n_components=1, mesh=mesh)._check_weights( + weights, n_components, n_samples + ) + + @pytest.mark.parametrize("args", INVALID_NORM.values(), ids=INVALID_NORM.keys()) + def test_non_normalized(self, mesh, args): + """ + Check if method raise error upon non-normalized weights. + """ + weights, n_components, n_samples = args + msg = r"The parameter 'weights' should be normalized" + with pytest.raises(ValueError, match=msg): + WeightedGaussianMixture(n_components=1, mesh=mesh)._check_weights( + weights, n_components, n_samples + ) + + if __name__ == "__main__": unittest.main() From 1245a24c450d762840a41d805c911612efd4a4e1 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 11 Sep 2024 14:43:13 -0700 Subject: [PATCH 057/194] Add release notes after v0.22.2 (#1533) Bring in the release notes of `v0.22.2` from the `maintenance/v0.22.x` branch. Add `v0.22.2` to `versions.json`. --- docs/_static/versions.json | 11 +++++-- docs/content/release/0.22.2-notes.rst | 41 +++++++++++++++++++++++++++ docs/content/release/index.rst | 1 + 3 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 docs/content/release/0.22.2-notes.rst diff --git a/docs/_static/versions.json b/docs/_static/versions.json index f1b0281d32..d1c70abbca 100644 --- a/docs/_static/versions.json +++ b/docs/_static/versions.json @@ -4,11 +4,16 @@ "url": "https://docs.simpeg.xyz/dev/" }, { - "name": "v0.22.1 (latest)", - "version": "0.22.1", - "url": "https://docs.simpeg.xyz/v0.22.1/", + "name": "v0.22.2 (latest)", + "version": "0.22.2", + "url": "https://docs.simpeg.xyz/v0.22.2/", "preferred": true }, + { + "name": "v0.22.1", + "version": "0.22.1", + "url": "https://docs.simpeg.xyz/v0.22.1/" + }, { "name": "v0.22.0", "version": "0.22.0", diff --git a/docs/content/release/0.22.2-notes.rst b/docs/content/release/0.22.2-notes.rst new file mode 100644 index 0000000000..9f635ae8b4 --- /dev/null +++ b/docs/content/release/0.22.2-notes.rst @@ -0,0 +1,41 @@ +.. _0.22.2_notes: + +=========================== +SimPEG 0.22.2 Release Notes +=========================== + +September 11th, 2024 + +.. contents:: Highlights + :depth: 2 + +Updates +======= + +This patch release includes a few fixes: SciPy solvers can now be used +with newer versions of SciPy (``>=1.14``), we fixed a bug in one of the methods +of the Gaussian Mixture Models used in PGI, we included a fix on one of the +internal validation functions, and we also applied some minor improvements to +the documentation pages. + +Contributors +============ + +- `@prisae `__ +- `@santisoler `__ + +Pull Requests +============= + +- Minor fixes to disclaimer in ``pgi_utils.py`` by `@prisae `__ in + https://github.com/simpeg/simpeg/pull/1512 +- Fix misuse of the ``requires`` decorator in ``code_utils.py`` by + `@prisae `__ in https://github.com/simpeg/simpeg/pull/1513 +- Use lowercase simpeg in a few missing docstrings by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1519 +- Pass ``rtol`` to SciPy solvers for SciPy>=1.12 by `@prisae `__ in + https://github.com/simpeg/simpeg/pull/1517 +- Fix ``validate_ndarray_with_shape`` by `@prisae `__ in + https://github.com/simpeg/simpeg/pull/1523 +- Fix bug in GMM’s ``_check_weights`` by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1526 diff --git a/docs/content/release/index.rst b/docs/content/release/index.rst index 7439a185c3..5e6f5c6c84 100644 --- a/docs/content/release/index.rst +++ b/docs/content/release/index.rst @@ -5,6 +5,7 @@ Release Notes .. toctree:: :maxdepth: 2 + 0.22.2 <0.22.2-notes> 0.22.1 <0.22.1-notes> 0.22.0 <0.22.0-notes> 0.21.1 <0.21.1-notes> From 4eef48c26296e74f4027f8ce96c66b7bc2f37992 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Wed, 11 Sep 2024 23:03:01 -0600 Subject: [PATCH 058/194] Minimum python version update (#1522) #### Summary Updates the minimum python version to 3.10. #### Reference issue closes #1521 #### What does this implement/fix? Updates the minimum python version to 3.10 and removes references to python 3.8. Also it removes code that was required to work around supporting 3.8 as the minimum version. #### Additional information Most recent `numpy` (2.1.0) has a minimum version of 3.10 and it makes sense that we should match that moving forward. --------- Co-authored-by: Santiago Soler --- .ci/azure/docs.yml | 2 +- .ci/azure/pypi.yml | 4 +- .ci/azure/test.yml | 2 +- .ci/environment_test.yml | 2 +- .../contributing/setting-up-environment.rst | 4 +- environment.yml | 2 +- examples/04-dcip/dcr3d/topo_xyz.txt | 19881 ---------------- pyproject.toml | 4 +- simpeg/directives/directives.py | 7 +- simpeg/maps/_base.py | 2 - simpeg/meta/multiprocessing.py | 6 - simpeg/objective_function.py | 2 - simpeg/optimization.py | 1 - simpeg/potential_fields/magnetics/sources.py | 1 - simpeg/regularization/base.py | 6 +- simpeg/regularization/pgi.py | 2 - simpeg/regularization/sparse.py | 2 - simpeg/regularization/vector.py | 7 +- simpeg/simulation.py | 1 - simpeg/typing/__init__.py | 31 +- simpeg/utils/code_utils.py | 1 - simpeg/utils/mat_utils.py | 1 - simpeg/utils/model_builder.py | 1 - tests/meta/test_multiprocessing_sim.py | 5 - tests/pf/test_forward_Mag_Linear.py | 2 - 25 files changed, 23 insertions(+), 19956 deletions(-) delete mode 100644 examples/04-dcip/dcr3d/topo_xyz.txt diff --git a/.ci/azure/docs.yml b/.ci/azure/docs.yml index 7f1918386d..d2b976d8b5 100644 --- a/.ci/azure/docs.yml +++ b/.ci/azure/docs.yml @@ -5,7 +5,7 @@ jobs: pool: vmImage: ubuntu-latest variables: - python.version: "3.8" + python.version: "3.10" timeoutInMinutes: 240 steps: # Checkout simpeg repo. diff --git a/.ci/azure/pypi.yml b/.ci/azure/pypi.yml index 89bed4e43e..c6eaa93dad 100644 --- a/.ci/azure/pypi.yml +++ b/.ci/azure/pypi.yml @@ -12,7 +12,7 @@ jobs: - task: UsePythonVersion@0 inputs: - versionSpec: "3.9" + versionSpec: "3.10" displayName: "Setup Python" - bash: | @@ -57,7 +57,7 @@ jobs: - task: UsePythonVersion@0 inputs: - versionSpec: "3.9" + versionSpec: "3.10" displayName: "Setup Python" - bash: | diff --git a/.ci/azure/test.yml b/.ci/azure/test.yml index d8e5974859..b9f27d1989 100644 --- a/.ci/azure/test.yml +++ b/.ci/azure/test.yml @@ -1,6 +1,6 @@ parameters: os : ['ubuntu-latest'] - py_vers: ['3.8'] + py_vers: ['3.10'] test: ['tests/em', 'tests/base tests/flow tests/seis tests/utils', 'tests/meta', diff --git a/.ci/environment_test.yml b/.ci/environment_test.yml index 1b947eb468..d26d956b01 100644 --- a/.ci/environment_test.yml +++ b/.ci/environment_test.yml @@ -2,7 +2,7 @@ name: simpeg-test channels: - conda-forge dependencies: - - numpy>=1.20 + - numpy>=1.21 - scipy>=1.8 - pymatsolver-base>=0.2 - matplotlib-base diff --git a/docs/content/getting_started/contributing/setting-up-environment.rst b/docs/content/getting_started/contributing/setting-up-environment.rst index d3d670fdc9..f1b3d5c992 100644 --- a/docs/content/getting_started/contributing/setting-up-environment.rst +++ b/docs/content/getting_started/contributing/setting-up-environment.rst @@ -71,7 +71,7 @@ This practice also allows you to uninstall SimPEG if so desired: .. code:: - pip uninstall SimPEG + pip uninstall simpeg .. note:: @@ -84,7 +84,7 @@ This practice also allows you to uninstall SimPEG if so desired: Check your installation ----------------------- -You should be able to open a terminal within SimPEG/tutorials and run an +You should be able to open a terminal within simpeg/tutorials and run an example, i.e. .. code:: diff --git a/environment.yml b/environment.yml index 6d07ad0fc8..982337f40d 100644 --- a/environment.yml +++ b/environment.yml @@ -4,7 +4,7 @@ channels: dependencies: # dependencies - python=3.11 - - numpy>=1.20 + - numpy>=1.21 - scipy>=1.8 - pymatsolver-base>=0.2 - matplotlib-base diff --git a/examples/04-dcip/dcr3d/topo_xyz.txt b/examples/04-dcip/dcr3d/topo_xyz.txt deleted file mode 100644 index 7c149205d2..0000000000 --- a/examples/04-dcip/dcr3d/topo_xyz.txt +++ /dev/null @@ -1,19881 +0,0 @@ --2.1000e+03 -2.0000e+03 6.9049e+00 --2.1000e+03 -1.9714e+03 6.8784e+00 --2.1000e+03 -1.9429e+03 6.8516e+00 --2.1000e+03 -1.9143e+03 6.8245e+00 --2.1000e+03 -1.8857e+03 6.7972e+00 --2.1000e+03 -1.8571e+03 6.7697e+00 --2.1000e+03 -1.8286e+03 6.7419e+00 --2.1000e+03 -1.8000e+03 6.7139e+00 --2.1000e+03 -1.7714e+03 6.6857e+00 --2.1000e+03 -1.7429e+03 6.6573e+00 --2.1000e+03 -1.7143e+03 6.6286e+00 --2.1000e+03 -1.6857e+03 6.5998e+00 --2.1000e+03 -1.6571e+03 6.5707e+00 --2.1000e+03 -1.6286e+03 6.5415e+00 --2.1000e+03 -1.6000e+03 6.5121e+00 --2.1000e+03 -1.5714e+03 6.4825e+00 --2.1000e+03 -1.5429e+03 6.4528e+00 --2.1000e+03 -1.5143e+03 6.4230e+00 --2.1000e+03 -1.4857e+03 6.3930e+00 --2.1000e+03 -1.4571e+03 6.3629e+00 --2.1000e+03 -1.4286e+03 6.3327e+00 --2.1000e+03 -1.4000e+03 6.3024e+00 --2.1000e+03 -1.3714e+03 6.2720e+00 --2.1000e+03 -1.3429e+03 6.2416e+00 --2.1000e+03 -1.3143e+03 6.2112e+00 --2.1000e+03 -1.2857e+03 6.1808e+00 --2.1000e+03 -1.2571e+03 6.1503e+00 --2.1000e+03 -1.2286e+03 6.1199e+00 --2.1000e+03 -1.2000e+03 6.0896e+00 --2.1000e+03 -1.1714e+03 6.0593e+00 --2.1000e+03 -1.1429e+03 6.0291e+00 --2.1000e+03 -1.1143e+03 5.9990e+00 --2.1000e+03 -1.0857e+03 5.9691e+00 --2.1000e+03 -1.0571e+03 5.9394e+00 --2.1000e+03 -1.0286e+03 5.9099e+00 --2.1000e+03 -1.0000e+03 5.8806e+00 --2.1000e+03 -9.7143e+02 5.8516e+00 --2.1000e+03 -9.4286e+02 5.8229e+00 --2.1000e+03 -9.1429e+02 5.7945e+00 --2.1000e+03 -8.8571e+02 5.7665e+00 --2.1000e+03 -8.5714e+02 5.7389e+00 --2.1000e+03 -8.2857e+02 5.7117e+00 --2.1000e+03 -8.0000e+02 5.6849e+00 --2.1000e+03 -7.7143e+02 5.6587e+00 --2.1000e+03 -7.4286e+02 5.6330e+00 --2.1000e+03 -7.1429e+02 5.6079e+00 --2.1000e+03 -6.8571e+02 5.5834e+00 --2.1000e+03 -6.5714e+02 5.5596e+00 --2.1000e+03 -6.2857e+02 5.5364e+00 --2.1000e+03 -6.0000e+02 5.5140e+00 --2.1000e+03 -5.7143e+02 5.4923e+00 --2.1000e+03 -5.4286e+02 5.4714e+00 --2.1000e+03 -5.1429e+02 5.4513e+00 --2.1000e+03 -4.8571e+02 5.4321e+00 --2.1000e+03 -4.5714e+02 5.4137e+00 --2.1000e+03 -4.2857e+02 5.3963e+00 --2.1000e+03 -4.0000e+02 5.3799e+00 --2.1000e+03 -3.7143e+02 5.3645e+00 --2.1000e+03 -3.4286e+02 5.3500e+00 --2.1000e+03 -3.1429e+02 5.3366e+00 --2.1000e+03 -2.8571e+02 5.3243e+00 --2.1000e+03 -2.5714e+02 5.3131e+00 --2.1000e+03 -2.2857e+02 5.3030e+00 --2.1000e+03 -2.0000e+02 5.2941e+00 --2.1000e+03 -1.7143e+02 5.2863e+00 --2.1000e+03 -1.4286e+02 5.2796e+00 --2.1000e+03 -1.1429e+02 5.2742e+00 --2.1000e+03 -8.5714e+01 5.2700e+00 --2.1000e+03 -5.7143e+01 5.2669e+00 --2.1000e+03 -2.8571e+01 5.2651e+00 --2.1000e+03 0.0000e+00 5.2645e+00 --2.1000e+03 2.8571e+01 5.2651e+00 --2.1000e+03 5.7143e+01 5.2669e+00 --2.1000e+03 8.5714e+01 5.2700e+00 --2.1000e+03 1.1429e+02 5.2742e+00 --2.1000e+03 1.4286e+02 5.2796e+00 --2.1000e+03 1.7143e+02 5.2863e+00 --2.1000e+03 2.0000e+02 5.2941e+00 --2.1000e+03 2.2857e+02 5.3030e+00 --2.1000e+03 2.5714e+02 5.3131e+00 --2.1000e+03 2.8571e+02 5.3243e+00 --2.1000e+03 3.1429e+02 5.3366e+00 --2.1000e+03 3.4286e+02 5.3500e+00 --2.1000e+03 3.7143e+02 5.3645e+00 --2.1000e+03 4.0000e+02 5.3799e+00 --2.1000e+03 4.2857e+02 5.3963e+00 --2.1000e+03 4.5714e+02 5.4137e+00 --2.1000e+03 4.8571e+02 5.4321e+00 --2.1000e+03 5.1429e+02 5.4513e+00 --2.1000e+03 5.4286e+02 5.4714e+00 --2.1000e+03 5.7143e+02 5.4923e+00 --2.1000e+03 6.0000e+02 5.5140e+00 --2.1000e+03 6.2857e+02 5.5364e+00 --2.1000e+03 6.5714e+02 5.5596e+00 --2.1000e+03 6.8571e+02 5.5834e+00 --2.1000e+03 7.1429e+02 5.6079e+00 --2.1000e+03 7.4286e+02 5.6330e+00 --2.1000e+03 7.7143e+02 5.6587e+00 --2.1000e+03 8.0000e+02 5.6849e+00 --2.1000e+03 8.2857e+02 5.7117e+00 --2.1000e+03 8.5714e+02 5.7389e+00 --2.1000e+03 8.8571e+02 5.7665e+00 --2.1000e+03 9.1429e+02 5.7945e+00 --2.1000e+03 9.4286e+02 5.8229e+00 --2.1000e+03 9.7143e+02 5.8516e+00 --2.1000e+03 1.0000e+03 5.8806e+00 --2.1000e+03 1.0286e+03 5.9099e+00 --2.1000e+03 1.0571e+03 5.9394e+00 --2.1000e+03 1.0857e+03 5.9691e+00 --2.1000e+03 1.1143e+03 5.9990e+00 --2.1000e+03 1.1429e+03 6.0291e+00 --2.1000e+03 1.1714e+03 6.0593e+00 --2.1000e+03 1.2000e+03 6.0896e+00 --2.1000e+03 1.2286e+03 6.1199e+00 --2.1000e+03 1.2571e+03 6.1503e+00 --2.1000e+03 1.2857e+03 6.1808e+00 --2.1000e+03 1.3143e+03 6.2112e+00 --2.1000e+03 1.3429e+03 6.2416e+00 --2.1000e+03 1.3714e+03 6.2720e+00 --2.1000e+03 1.4000e+03 6.3024e+00 --2.1000e+03 1.4286e+03 6.3327e+00 --2.1000e+03 1.4571e+03 6.3629e+00 --2.1000e+03 1.4857e+03 6.3930e+00 --2.1000e+03 1.5143e+03 6.4230e+00 --2.1000e+03 1.5429e+03 6.4528e+00 --2.1000e+03 1.5714e+03 6.4825e+00 --2.1000e+03 1.6000e+03 6.5121e+00 --2.1000e+03 1.6286e+03 6.5415e+00 --2.1000e+03 1.6571e+03 6.5707e+00 --2.1000e+03 1.6857e+03 6.5998e+00 --2.1000e+03 1.7143e+03 6.6286e+00 --2.1000e+03 1.7429e+03 6.6573e+00 --2.1000e+03 1.7714e+03 6.6857e+00 --2.1000e+03 1.8000e+03 6.7139e+00 --2.1000e+03 1.8286e+03 6.7419e+00 --2.1000e+03 1.8571e+03 6.7697e+00 --2.1000e+03 1.8857e+03 6.7972e+00 --2.1000e+03 1.9143e+03 6.8245e+00 --2.1000e+03 1.9429e+03 6.8516e+00 --2.1000e+03 1.9714e+03 6.8784e+00 --2.1000e+03 2.0000e+03 6.9049e+00 --2.0700e+03 -2.0000e+03 6.8756e+00 --2.0700e+03 -1.9714e+03 6.8483e+00 --2.0700e+03 -1.9429e+03 6.8208e+00 --2.0700e+03 -1.9143e+03 6.7930e+00 --2.0700e+03 -1.8857e+03 6.7649e+00 --2.0700e+03 -1.8571e+03 6.7366e+00 --2.0700e+03 -1.8286e+03 6.7080e+00 --2.0700e+03 -1.8000e+03 6.6792e+00 --2.0700e+03 -1.7714e+03 6.6502e+00 --2.0700e+03 -1.7429e+03 6.6209e+00 --2.0700e+03 -1.7143e+03 6.5913e+00 --2.0700e+03 -1.6857e+03 6.5616e+00 --2.0700e+03 -1.6571e+03 6.5316e+00 --2.0700e+03 -1.6286e+03 6.5015e+00 --2.0700e+03 -1.6000e+03 6.4711e+00 --2.0700e+03 -1.5714e+03 6.4406e+00 --2.0700e+03 -1.5429e+03 6.4098e+00 --2.0700e+03 -1.5143e+03 6.3790e+00 --2.0700e+03 -1.4857e+03 6.3479e+00 --2.0700e+03 -1.4571e+03 6.3168e+00 --2.0700e+03 -1.4286e+03 6.2855e+00 --2.0700e+03 -1.4000e+03 6.2541e+00 --2.0700e+03 -1.3714e+03 6.2227e+00 --2.0700e+03 -1.3429e+03 6.1911e+00 --2.0700e+03 -1.3143e+03 6.1595e+00 --2.0700e+03 -1.2857e+03 6.1279e+00 --2.0700e+03 -1.2571e+03 6.0963e+00 --2.0700e+03 -1.2286e+03 6.0647e+00 --2.0700e+03 -1.2000e+03 6.0331e+00 --2.0700e+03 -1.1714e+03 6.0016e+00 --2.0700e+03 -1.1429e+03 5.9702e+00 --2.0700e+03 -1.1143e+03 5.9389e+00 --2.0700e+03 -1.0857e+03 5.9077e+00 --2.0700e+03 -1.0571e+03 5.8768e+00 --2.0700e+03 -1.0286e+03 5.8460e+00 --2.0700e+03 -1.0000e+03 5.8154e+00 --2.0700e+03 -9.7143e+02 5.7851e+00 --2.0700e+03 -9.4286e+02 5.7551e+00 --2.0700e+03 -9.1429e+02 5.7254e+00 --2.0700e+03 -8.8571e+02 5.6961e+00 --2.0700e+03 -8.5714e+02 5.6672e+00 --2.0700e+03 -8.2857e+02 5.6388e+00 --2.0700e+03 -8.0000e+02 5.6108e+00 --2.0700e+03 -7.7143e+02 5.5833e+00 --2.0700e+03 -7.4286e+02 5.5564e+00 --2.0700e+03 -7.1429e+02 5.5300e+00 --2.0700e+03 -6.8571e+02 5.5043e+00 --2.0700e+03 -6.5714e+02 5.4793e+00 --2.0700e+03 -6.2857e+02 5.4550e+00 --2.0700e+03 -6.0000e+02 5.4314e+00 --2.0700e+03 -5.7143e+02 5.4086e+00 --2.0700e+03 -5.4286e+02 5.3866e+00 --2.0700e+03 -5.1429e+02 5.3655e+00 --2.0700e+03 -4.8571e+02 5.3452e+00 --2.0700e+03 -4.5714e+02 5.3260e+00 --2.0700e+03 -4.2857e+02 5.3076e+00 --2.0700e+03 -4.0000e+02 5.2903e+00 --2.0700e+03 -3.7143e+02 5.2740e+00 --2.0700e+03 -3.4286e+02 5.2588e+00 --2.0700e+03 -3.1429e+02 5.2447e+00 --2.0700e+03 -2.8571e+02 5.2317e+00 --2.0700e+03 -2.5714e+02 5.2199e+00 --2.0700e+03 -2.2857e+02 5.2093e+00 --2.0700e+03 -2.0000e+02 5.1998e+00 --2.0700e+03 -1.7143e+02 5.1916e+00 --2.0700e+03 -1.4286e+02 5.1846e+00 --2.0700e+03 -1.1429e+02 5.1788e+00 --2.0700e+03 -8.5714e+01 5.1744e+00 --2.0700e+03 -5.7143e+01 5.1712e+00 --2.0700e+03 -2.8571e+01 5.1692e+00 --2.0700e+03 0.0000e+00 5.1686e+00 --2.0700e+03 2.8571e+01 5.1692e+00 --2.0700e+03 5.7143e+01 5.1712e+00 --2.0700e+03 8.5714e+01 5.1744e+00 --2.0700e+03 1.1429e+02 5.1788e+00 --2.0700e+03 1.4286e+02 5.1846e+00 --2.0700e+03 1.7143e+02 5.1916e+00 --2.0700e+03 2.0000e+02 5.1998e+00 --2.0700e+03 2.2857e+02 5.2093e+00 --2.0700e+03 2.5714e+02 5.2199e+00 --2.0700e+03 2.8571e+02 5.2317e+00 --2.0700e+03 3.1429e+02 5.2447e+00 --2.0700e+03 3.4286e+02 5.2588e+00 --2.0700e+03 3.7143e+02 5.2740e+00 --2.0700e+03 4.0000e+02 5.2903e+00 --2.0700e+03 4.2857e+02 5.3076e+00 --2.0700e+03 4.5714e+02 5.3260e+00 --2.0700e+03 4.8571e+02 5.3452e+00 --2.0700e+03 5.1429e+02 5.3655e+00 --2.0700e+03 5.4286e+02 5.3866e+00 --2.0700e+03 5.7143e+02 5.4086e+00 --2.0700e+03 6.0000e+02 5.4314e+00 --2.0700e+03 6.2857e+02 5.4550e+00 --2.0700e+03 6.5714e+02 5.4793e+00 --2.0700e+03 6.8571e+02 5.5043e+00 --2.0700e+03 7.1429e+02 5.5300e+00 --2.0700e+03 7.4286e+02 5.5564e+00 --2.0700e+03 7.7143e+02 5.5833e+00 --2.0700e+03 8.0000e+02 5.6108e+00 --2.0700e+03 8.2857e+02 5.6388e+00 --2.0700e+03 8.5714e+02 5.6672e+00 --2.0700e+03 8.8571e+02 5.6961e+00 --2.0700e+03 9.1429e+02 5.7254e+00 --2.0700e+03 9.4286e+02 5.7551e+00 --2.0700e+03 9.7143e+02 5.7851e+00 --2.0700e+03 1.0000e+03 5.8154e+00 --2.0700e+03 1.0286e+03 5.8460e+00 --2.0700e+03 1.0571e+03 5.8768e+00 --2.0700e+03 1.0857e+03 5.9077e+00 --2.0700e+03 1.1143e+03 5.9389e+00 --2.0700e+03 1.1429e+03 5.9702e+00 --2.0700e+03 1.1714e+03 6.0016e+00 --2.0700e+03 1.2000e+03 6.0331e+00 --2.0700e+03 1.2286e+03 6.0647e+00 --2.0700e+03 1.2571e+03 6.0963e+00 --2.0700e+03 1.2857e+03 6.1279e+00 --2.0700e+03 1.3143e+03 6.1595e+00 --2.0700e+03 1.3429e+03 6.1911e+00 --2.0700e+03 1.3714e+03 6.2227e+00 --2.0700e+03 1.4000e+03 6.2541e+00 --2.0700e+03 1.4286e+03 6.2855e+00 --2.0700e+03 1.4571e+03 6.3168e+00 --2.0700e+03 1.4857e+03 6.3479e+00 --2.0700e+03 1.5143e+03 6.3790e+00 --2.0700e+03 1.5429e+03 6.4098e+00 --2.0700e+03 1.5714e+03 6.4406e+00 --2.0700e+03 1.6000e+03 6.4711e+00 --2.0700e+03 1.6286e+03 6.5015e+00 --2.0700e+03 1.6571e+03 6.5316e+00 --2.0700e+03 1.6857e+03 6.5616e+00 --2.0700e+03 1.7143e+03 6.5913e+00 --2.0700e+03 1.7429e+03 6.6209e+00 --2.0700e+03 1.7714e+03 6.6502e+00 --2.0700e+03 1.8000e+03 6.6792e+00 --2.0700e+03 1.8286e+03 6.7080e+00 --2.0700e+03 1.8571e+03 6.7366e+00 --2.0700e+03 1.8857e+03 6.7649e+00 --2.0700e+03 1.9143e+03 6.7930e+00 --2.0700e+03 1.9429e+03 6.8208e+00 --2.0700e+03 1.9714e+03 6.8483e+00 --2.0700e+03 2.0000e+03 6.8756e+00 --2.0400e+03 -2.0000e+03 6.8459e+00 --2.0400e+03 -1.9714e+03 6.8179e+00 --2.0400e+03 -1.9429e+03 6.7897e+00 --2.0400e+03 -1.9143e+03 6.7611e+00 --2.0400e+03 -1.8857e+03 6.7322e+00 --2.0400e+03 -1.8571e+03 6.7031e+00 --2.0400e+03 -1.8286e+03 6.6737e+00 --2.0400e+03 -1.8000e+03 6.6440e+00 --2.0400e+03 -1.7714e+03 6.6141e+00 --2.0400e+03 -1.7429e+03 6.5839e+00 --2.0400e+03 -1.7143e+03 6.5535e+00 --2.0400e+03 -1.6857e+03 6.5228e+00 --2.0400e+03 -1.6571e+03 6.4919e+00 --2.0400e+03 -1.6286e+03 6.4607e+00 --2.0400e+03 -1.6000e+03 6.4294e+00 --2.0400e+03 -1.5714e+03 6.3978e+00 --2.0400e+03 -1.5429e+03 6.3660e+00 --2.0400e+03 -1.5143e+03 6.3341e+00 --2.0400e+03 -1.4857e+03 6.3020e+00 --2.0400e+03 -1.4571e+03 6.2697e+00 --2.0400e+03 -1.4286e+03 6.2373e+00 --2.0400e+03 -1.4000e+03 6.2048e+00 --2.0400e+03 -1.3714e+03 6.1722e+00 --2.0400e+03 -1.3429e+03 6.1395e+00 --2.0400e+03 -1.3143e+03 6.1067e+00 --2.0400e+03 -1.2857e+03 6.0738e+00 --2.0400e+03 -1.2571e+03 6.0410e+00 --2.0400e+03 -1.2286e+03 6.0081e+00 --2.0400e+03 -1.2000e+03 5.9753e+00 --2.0400e+03 -1.1714e+03 5.9425e+00 --2.0400e+03 -1.1429e+03 5.9098e+00 --2.0400e+03 -1.1143e+03 5.8772e+00 --2.0400e+03 -1.0857e+03 5.8447e+00 --2.0400e+03 -1.0571e+03 5.8123e+00 --2.0400e+03 -1.0286e+03 5.7802e+00 --2.0400e+03 -1.0000e+03 5.7483e+00 --2.0400e+03 -9.7143e+02 5.7166e+00 --2.0400e+03 -9.4286e+02 5.6853e+00 --2.0400e+03 -9.1429e+02 5.6543e+00 --2.0400e+03 -8.8571e+02 5.6236e+00 --2.0400e+03 -8.5714e+02 5.5933e+00 --2.0400e+03 -8.2857e+02 5.5635e+00 --2.0400e+03 -8.0000e+02 5.5342e+00 --2.0400e+03 -7.7143e+02 5.5054e+00 --2.0400e+03 -7.4286e+02 5.4771e+00 --2.0400e+03 -7.1429e+02 5.4495e+00 --2.0400e+03 -6.8571e+02 5.4225e+00 --2.0400e+03 -6.5714e+02 5.3962e+00 --2.0400e+03 -6.2857e+02 5.3706e+00 --2.0400e+03 -6.0000e+02 5.3458e+00 --2.0400e+03 -5.7143e+02 5.3218e+00 --2.0400e+03 -5.4286e+02 5.2987e+00 --2.0400e+03 -5.1429e+02 5.2764e+00 --2.0400e+03 -4.8571e+02 5.2551e+00 --2.0400e+03 -4.5714e+02 5.2348e+00 --2.0400e+03 -4.2857e+02 5.2155e+00 --2.0400e+03 -4.0000e+02 5.1973e+00 --2.0400e+03 -3.7143e+02 5.1801e+00 --2.0400e+03 -3.4286e+02 5.1640e+00 --2.0400e+03 -3.1429e+02 5.1492e+00 --2.0400e+03 -2.8571e+02 5.1355e+00 --2.0400e+03 -2.5714e+02 5.1230e+00 --2.0400e+03 -2.2857e+02 5.1117e+00 --2.0400e+03 -2.0000e+02 5.1017e+00 --2.0400e+03 -1.7143e+02 5.0930e+00 --2.0400e+03 -1.4286e+02 5.0857e+00 --2.0400e+03 -1.1429e+02 5.0796e+00 --2.0400e+03 -8.5714e+01 5.0748e+00 --2.0400e+03 -5.7143e+01 5.0715e+00 --2.0400e+03 -2.8571e+01 5.0694e+00 --2.0400e+03 0.0000e+00 5.0687e+00 --2.0400e+03 2.8571e+01 5.0694e+00 --2.0400e+03 5.7143e+01 5.0715e+00 --2.0400e+03 8.5714e+01 5.0748e+00 --2.0400e+03 1.1429e+02 5.0796e+00 --2.0400e+03 1.4286e+02 5.0857e+00 --2.0400e+03 1.7143e+02 5.0930e+00 --2.0400e+03 2.0000e+02 5.1017e+00 --2.0400e+03 2.2857e+02 5.1117e+00 --2.0400e+03 2.5714e+02 5.1230e+00 --2.0400e+03 2.8571e+02 5.1355e+00 --2.0400e+03 3.1429e+02 5.1492e+00 --2.0400e+03 3.4286e+02 5.1640e+00 --2.0400e+03 3.7143e+02 5.1801e+00 --2.0400e+03 4.0000e+02 5.1973e+00 --2.0400e+03 4.2857e+02 5.2155e+00 --2.0400e+03 4.5714e+02 5.2348e+00 --2.0400e+03 4.8571e+02 5.2551e+00 --2.0400e+03 5.1429e+02 5.2764e+00 --2.0400e+03 5.4286e+02 5.2987e+00 --2.0400e+03 5.7143e+02 5.3218e+00 --2.0400e+03 6.0000e+02 5.3458e+00 --2.0400e+03 6.2857e+02 5.3706e+00 --2.0400e+03 6.5714e+02 5.3962e+00 --2.0400e+03 6.8571e+02 5.4225e+00 --2.0400e+03 7.1429e+02 5.4495e+00 --2.0400e+03 7.4286e+02 5.4771e+00 --2.0400e+03 7.7143e+02 5.5054e+00 --2.0400e+03 8.0000e+02 5.5342e+00 --2.0400e+03 8.2857e+02 5.5635e+00 --2.0400e+03 8.5714e+02 5.5933e+00 --2.0400e+03 8.8571e+02 5.6236e+00 --2.0400e+03 9.1429e+02 5.6543e+00 --2.0400e+03 9.4286e+02 5.6853e+00 --2.0400e+03 9.7143e+02 5.7166e+00 --2.0400e+03 1.0000e+03 5.7483e+00 --2.0400e+03 1.0286e+03 5.7802e+00 --2.0400e+03 1.0571e+03 5.8123e+00 --2.0400e+03 1.0857e+03 5.8447e+00 --2.0400e+03 1.1143e+03 5.8772e+00 --2.0400e+03 1.1429e+03 5.9098e+00 --2.0400e+03 1.1714e+03 5.9425e+00 --2.0400e+03 1.2000e+03 5.9753e+00 --2.0400e+03 1.2286e+03 6.0081e+00 --2.0400e+03 1.2571e+03 6.0410e+00 --2.0400e+03 1.2857e+03 6.0738e+00 --2.0400e+03 1.3143e+03 6.1067e+00 --2.0400e+03 1.3429e+03 6.1395e+00 --2.0400e+03 1.3714e+03 6.1722e+00 --2.0400e+03 1.4000e+03 6.2048e+00 --2.0400e+03 1.4286e+03 6.2373e+00 --2.0400e+03 1.4571e+03 6.2697e+00 --2.0400e+03 1.4857e+03 6.3020e+00 --2.0400e+03 1.5143e+03 6.3341e+00 --2.0400e+03 1.5429e+03 6.3660e+00 --2.0400e+03 1.5714e+03 6.3978e+00 --2.0400e+03 1.6000e+03 6.4294e+00 --2.0400e+03 1.6286e+03 6.4607e+00 --2.0400e+03 1.6571e+03 6.4919e+00 --2.0400e+03 1.6857e+03 6.5228e+00 --2.0400e+03 1.7143e+03 6.5535e+00 --2.0400e+03 1.7429e+03 6.5839e+00 --2.0400e+03 1.7714e+03 6.6141e+00 --2.0400e+03 1.8000e+03 6.6440e+00 --2.0400e+03 1.8286e+03 6.6737e+00 --2.0400e+03 1.8571e+03 6.7031e+00 --2.0400e+03 1.8857e+03 6.7322e+00 --2.0400e+03 1.9143e+03 6.7611e+00 --2.0400e+03 1.9429e+03 6.7897e+00 --2.0400e+03 1.9714e+03 6.8179e+00 --2.0400e+03 2.0000e+03 6.8459e+00 --2.0100e+03 -2.0000e+03 6.8159e+00 --2.0100e+03 -1.9714e+03 6.7872e+00 --2.0100e+03 -1.9429e+03 6.7581e+00 --2.0100e+03 -1.9143e+03 6.7287e+00 --2.0100e+03 -1.8857e+03 6.6991e+00 --2.0100e+03 -1.8571e+03 6.6691e+00 --2.0100e+03 -1.8286e+03 6.6388e+00 --2.0100e+03 -1.8000e+03 6.6083e+00 --2.0100e+03 -1.7714e+03 6.5775e+00 --2.0100e+03 -1.7429e+03 6.5463e+00 --2.0100e+03 -1.7143e+03 6.5150e+00 --2.0100e+03 -1.6857e+03 6.4833e+00 --2.0100e+03 -1.6571e+03 6.4514e+00 --2.0100e+03 -1.6286e+03 6.4193e+00 --2.0100e+03 -1.6000e+03 6.3869e+00 --2.0100e+03 -1.5714e+03 6.3542e+00 --2.0100e+03 -1.5429e+03 6.3214e+00 --2.0100e+03 -1.5143e+03 6.2884e+00 --2.0100e+03 -1.4857e+03 6.2551e+00 --2.0100e+03 -1.4571e+03 6.2217e+00 --2.0100e+03 -1.4286e+03 6.1881e+00 --2.0100e+03 -1.4000e+03 6.1544e+00 --2.0100e+03 -1.3714e+03 6.1206e+00 --2.0100e+03 -1.3429e+03 6.0866e+00 --2.0100e+03 -1.3143e+03 6.0526e+00 --2.0100e+03 -1.2857e+03 6.0185e+00 --2.0100e+03 -1.2571e+03 5.9843e+00 --2.0100e+03 -1.2286e+03 5.9501e+00 --2.0100e+03 -1.2000e+03 5.9160e+00 --2.0100e+03 -1.1714e+03 5.8818e+00 --2.0100e+03 -1.1429e+03 5.8477e+00 --2.0100e+03 -1.1143e+03 5.8137e+00 --2.0100e+03 -1.0857e+03 5.7798e+00 --2.0100e+03 -1.0571e+03 5.7461e+00 --2.0100e+03 -1.0286e+03 5.7125e+00 --2.0100e+03 -1.0000e+03 5.6792e+00 --2.0100e+03 -9.7143e+02 5.6461e+00 --2.0100e+03 -9.4286e+02 5.6133e+00 --2.0100e+03 -9.1429e+02 5.5808e+00 --2.0100e+03 -8.8571e+02 5.5487e+00 --2.0100e+03 -8.5714e+02 5.5170e+00 --2.0100e+03 -8.2857e+02 5.4858e+00 --2.0100e+03 -8.0000e+02 5.4550e+00 --2.0100e+03 -7.7143e+02 5.4248e+00 --2.0100e+03 -7.4286e+02 5.3951e+00 --2.0100e+03 -7.1429e+02 5.3661e+00 --2.0100e+03 -6.8571e+02 5.3377e+00 --2.0100e+03 -6.5714e+02 5.3101e+00 --2.0100e+03 -6.2857e+02 5.2832e+00 --2.0100e+03 -6.0000e+02 5.2571e+00 --2.0100e+03 -5.7143e+02 5.2318e+00 --2.0100e+03 -5.4286e+02 5.2075e+00 --2.0100e+03 -5.1429e+02 5.1840e+00 --2.0100e+03 -4.8571e+02 5.1616e+00 --2.0100e+03 -4.5714e+02 5.1401e+00 --2.0100e+03 -4.2857e+02 5.1198e+00 --2.0100e+03 -4.0000e+02 5.1005e+00 --2.0100e+03 -3.7143e+02 5.0824e+00 --2.0100e+03 -3.4286e+02 5.0654e+00 --2.0100e+03 -3.1429e+02 5.0497e+00 --2.0100e+03 -2.8571e+02 5.0352e+00 --2.0100e+03 -2.5714e+02 5.0220e+00 --2.0100e+03 -2.2857e+02 5.0102e+00 --2.0100e+03 -2.0000e+02 4.9996e+00 --2.0100e+03 -1.7143e+02 4.9904e+00 --2.0100e+03 -1.4286e+02 4.9826e+00 --2.0100e+03 -1.1429e+02 4.9762e+00 --2.0100e+03 -8.5714e+01 4.9712e+00 --2.0100e+03 -5.7143e+01 4.9676e+00 --2.0100e+03 -2.8571e+01 4.9654e+00 --2.0100e+03 0.0000e+00 4.9647e+00 --2.0100e+03 2.8571e+01 4.9654e+00 --2.0100e+03 5.7143e+01 4.9676e+00 --2.0100e+03 8.5714e+01 4.9712e+00 --2.0100e+03 1.1429e+02 4.9762e+00 --2.0100e+03 1.4286e+02 4.9826e+00 --2.0100e+03 1.7143e+02 4.9904e+00 --2.0100e+03 2.0000e+02 4.9996e+00 --2.0100e+03 2.2857e+02 5.0102e+00 --2.0100e+03 2.5714e+02 5.0220e+00 --2.0100e+03 2.8571e+02 5.0352e+00 --2.0100e+03 3.1429e+02 5.0497e+00 --2.0100e+03 3.4286e+02 5.0654e+00 --2.0100e+03 3.7143e+02 5.0824e+00 --2.0100e+03 4.0000e+02 5.1005e+00 --2.0100e+03 4.2857e+02 5.1198e+00 --2.0100e+03 4.5714e+02 5.1401e+00 --2.0100e+03 4.8571e+02 5.1616e+00 --2.0100e+03 5.1429e+02 5.1840e+00 --2.0100e+03 5.4286e+02 5.2075e+00 --2.0100e+03 5.7143e+02 5.2318e+00 --2.0100e+03 6.0000e+02 5.2571e+00 --2.0100e+03 6.2857e+02 5.2832e+00 --2.0100e+03 6.5714e+02 5.3101e+00 --2.0100e+03 6.8571e+02 5.3377e+00 --2.0100e+03 7.1429e+02 5.3661e+00 --2.0100e+03 7.4286e+02 5.3951e+00 --2.0100e+03 7.7143e+02 5.4248e+00 --2.0100e+03 8.0000e+02 5.4550e+00 --2.0100e+03 8.2857e+02 5.4858e+00 --2.0100e+03 8.5714e+02 5.5170e+00 --2.0100e+03 8.8571e+02 5.5487e+00 --2.0100e+03 9.1429e+02 5.5808e+00 --2.0100e+03 9.4286e+02 5.6133e+00 --2.0100e+03 9.7143e+02 5.6461e+00 --2.0100e+03 1.0000e+03 5.6792e+00 --2.0100e+03 1.0286e+03 5.7125e+00 --2.0100e+03 1.0571e+03 5.7461e+00 --2.0100e+03 1.0857e+03 5.7798e+00 --2.0100e+03 1.1143e+03 5.8137e+00 --2.0100e+03 1.1429e+03 5.8477e+00 --2.0100e+03 1.1714e+03 5.8818e+00 --2.0100e+03 1.2000e+03 5.9160e+00 --2.0100e+03 1.2286e+03 5.9501e+00 --2.0100e+03 1.2571e+03 5.9843e+00 --2.0100e+03 1.2857e+03 6.0185e+00 --2.0100e+03 1.3143e+03 6.0526e+00 --2.0100e+03 1.3429e+03 6.0866e+00 --2.0100e+03 1.3714e+03 6.1206e+00 --2.0100e+03 1.4000e+03 6.1544e+00 --2.0100e+03 1.4286e+03 6.1881e+00 --2.0100e+03 1.4571e+03 6.2217e+00 --2.0100e+03 1.4857e+03 6.2551e+00 --2.0100e+03 1.5143e+03 6.2884e+00 --2.0100e+03 1.5429e+03 6.3214e+00 --2.0100e+03 1.5714e+03 6.3542e+00 --2.0100e+03 1.6000e+03 6.3869e+00 --2.0100e+03 1.6286e+03 6.4193e+00 --2.0100e+03 1.6571e+03 6.4514e+00 --2.0100e+03 1.6857e+03 6.4833e+00 --2.0100e+03 1.7143e+03 6.5150e+00 --2.0100e+03 1.7429e+03 6.5463e+00 --2.0100e+03 1.7714e+03 6.5775e+00 --2.0100e+03 1.8000e+03 6.6083e+00 --2.0100e+03 1.8286e+03 6.6388e+00 --2.0100e+03 1.8571e+03 6.6691e+00 --2.0100e+03 1.8857e+03 6.6991e+00 --2.0100e+03 1.9143e+03 6.7287e+00 --2.0100e+03 1.9429e+03 6.7581e+00 --2.0100e+03 1.9714e+03 6.7872e+00 --2.0100e+03 2.0000e+03 6.8159e+00 --1.9800e+03 -2.0000e+03 6.7856e+00 --1.9800e+03 -1.9714e+03 6.7560e+00 --1.9800e+03 -1.9429e+03 6.7262e+00 --1.9800e+03 -1.9143e+03 6.6960e+00 --1.9800e+03 -1.8857e+03 6.6655e+00 --1.9800e+03 -1.8571e+03 6.6346e+00 --1.9800e+03 -1.8286e+03 6.6035e+00 --1.9800e+03 -1.8000e+03 6.5720e+00 --1.9800e+03 -1.7714e+03 6.5403e+00 --1.9800e+03 -1.7429e+03 6.5082e+00 --1.9800e+03 -1.7143e+03 6.4758e+00 --1.9800e+03 -1.6857e+03 6.4432e+00 --1.9800e+03 -1.6571e+03 6.4103e+00 --1.9800e+03 -1.6286e+03 6.3771e+00 --1.9800e+03 -1.6000e+03 6.3436e+00 --1.9800e+03 -1.5714e+03 6.3099e+00 --1.9800e+03 -1.5429e+03 6.2759e+00 --1.9800e+03 -1.5143e+03 6.2417e+00 --1.9800e+03 -1.4857e+03 6.2073e+00 --1.9800e+03 -1.4571e+03 6.1727e+00 --1.9800e+03 -1.4286e+03 6.1379e+00 --1.9800e+03 -1.4000e+03 6.1029e+00 --1.9800e+03 -1.3714e+03 6.0678e+00 --1.9800e+03 -1.3429e+03 6.0326e+00 --1.9800e+03 -1.3143e+03 5.9972e+00 --1.9800e+03 -1.2857e+03 5.9618e+00 --1.9800e+03 -1.2571e+03 5.9262e+00 --1.9800e+03 -1.2286e+03 5.8907e+00 --1.9800e+03 -1.2000e+03 5.8551e+00 --1.9800e+03 -1.1714e+03 5.8195e+00 --1.9800e+03 -1.1429e+03 5.7840e+00 --1.9800e+03 -1.1143e+03 5.7485e+00 --1.9800e+03 -1.0857e+03 5.7132e+00 --1.9800e+03 -1.0571e+03 5.6780e+00 --1.9800e+03 -1.0286e+03 5.6429e+00 --1.9800e+03 -1.0000e+03 5.6081e+00 --1.9800e+03 -9.7143e+02 5.5734e+00 --1.9800e+03 -9.4286e+02 5.5391e+00 --1.9800e+03 -9.1429e+02 5.5051e+00 --1.9800e+03 -8.8571e+02 5.4715e+00 --1.9800e+03 -8.5714e+02 5.4383e+00 --1.9800e+03 -8.2857e+02 5.4055e+00 --1.9800e+03 -8.0000e+02 5.3732e+00 --1.9800e+03 -7.7143e+02 5.3415e+00 --1.9800e+03 -7.4286e+02 5.3103e+00 --1.9800e+03 -7.1429e+02 5.2798e+00 --1.9800e+03 -6.8571e+02 5.2499e+00 --1.9800e+03 -6.5714e+02 5.2208e+00 --1.9800e+03 -6.2857e+02 5.1925e+00 --1.9800e+03 -6.0000e+02 5.1650e+00 --1.9800e+03 -5.7143e+02 5.1384e+00 --1.9800e+03 -5.4286e+02 5.1127e+00 --1.9800e+03 -5.1429e+02 5.0880e+00 --1.9800e+03 -4.8571e+02 5.0643e+00 --1.9800e+03 -4.5714e+02 5.0417e+00 --1.9800e+03 -4.2857e+02 5.0202e+00 --1.9800e+03 -4.0000e+02 4.9999e+00 --1.9800e+03 -3.7143e+02 4.9807e+00 --1.9800e+03 -3.4286e+02 4.9628e+00 --1.9800e+03 -3.1429e+02 4.9462e+00 --1.9800e+03 -2.8571e+02 4.9309e+00 --1.9800e+03 -2.5714e+02 4.9169e+00 --1.9800e+03 -2.2857e+02 4.9043e+00 --1.9800e+03 -2.0000e+02 4.8932e+00 --1.9800e+03 -1.7143e+02 4.8834e+00 --1.9800e+03 -1.4286e+02 4.8751e+00 --1.9800e+03 -1.1429e+02 4.8683e+00 --1.9800e+03 -8.5714e+01 4.8630e+00 --1.9800e+03 -5.7143e+01 4.8592e+00 --1.9800e+03 -2.8571e+01 4.8570e+00 --1.9800e+03 0.0000e+00 4.8562e+00 --1.9800e+03 2.8571e+01 4.8570e+00 --1.9800e+03 5.7143e+01 4.8592e+00 --1.9800e+03 8.5714e+01 4.8630e+00 --1.9800e+03 1.1429e+02 4.8683e+00 --1.9800e+03 1.4286e+02 4.8751e+00 --1.9800e+03 1.7143e+02 4.8834e+00 --1.9800e+03 2.0000e+02 4.8932e+00 --1.9800e+03 2.2857e+02 4.9043e+00 --1.9800e+03 2.5714e+02 4.9169e+00 --1.9800e+03 2.8571e+02 4.9309e+00 --1.9800e+03 3.1429e+02 4.9462e+00 --1.9800e+03 3.4286e+02 4.9628e+00 --1.9800e+03 3.7143e+02 4.9807e+00 --1.9800e+03 4.0000e+02 4.9999e+00 --1.9800e+03 4.2857e+02 5.0202e+00 --1.9800e+03 4.5714e+02 5.0417e+00 --1.9800e+03 4.8571e+02 5.0643e+00 --1.9800e+03 5.1429e+02 5.0880e+00 --1.9800e+03 5.4286e+02 5.1127e+00 --1.9800e+03 5.7143e+02 5.1384e+00 --1.9800e+03 6.0000e+02 5.1650e+00 --1.9800e+03 6.2857e+02 5.1925e+00 --1.9800e+03 6.5714e+02 5.2208e+00 --1.9800e+03 6.8571e+02 5.2499e+00 --1.9800e+03 7.1429e+02 5.2798e+00 --1.9800e+03 7.4286e+02 5.3103e+00 --1.9800e+03 7.7143e+02 5.3415e+00 --1.9800e+03 8.0000e+02 5.3732e+00 --1.9800e+03 8.2857e+02 5.4055e+00 --1.9800e+03 8.5714e+02 5.4383e+00 --1.9800e+03 8.8571e+02 5.4715e+00 --1.9800e+03 9.1429e+02 5.5051e+00 --1.9800e+03 9.4286e+02 5.5391e+00 --1.9800e+03 9.7143e+02 5.5734e+00 --1.9800e+03 1.0000e+03 5.6081e+00 --1.9800e+03 1.0286e+03 5.6429e+00 --1.9800e+03 1.0571e+03 5.6780e+00 --1.9800e+03 1.0857e+03 5.7132e+00 --1.9800e+03 1.1143e+03 5.7485e+00 --1.9800e+03 1.1429e+03 5.7840e+00 --1.9800e+03 1.1714e+03 5.8195e+00 --1.9800e+03 1.2000e+03 5.8551e+00 --1.9800e+03 1.2286e+03 5.8907e+00 --1.9800e+03 1.2571e+03 5.9262e+00 --1.9800e+03 1.2857e+03 5.9618e+00 --1.9800e+03 1.3143e+03 5.9972e+00 --1.9800e+03 1.3429e+03 6.0326e+00 --1.9800e+03 1.3714e+03 6.0678e+00 --1.9800e+03 1.4000e+03 6.1029e+00 --1.9800e+03 1.4286e+03 6.1379e+00 --1.9800e+03 1.4571e+03 6.1727e+00 --1.9800e+03 1.4857e+03 6.2073e+00 --1.9800e+03 1.5143e+03 6.2417e+00 --1.9800e+03 1.5429e+03 6.2759e+00 --1.9800e+03 1.5714e+03 6.3099e+00 --1.9800e+03 1.6000e+03 6.3436e+00 --1.9800e+03 1.6286e+03 6.3771e+00 --1.9800e+03 1.6571e+03 6.4103e+00 --1.9800e+03 1.6857e+03 6.4432e+00 --1.9800e+03 1.7143e+03 6.4758e+00 --1.9800e+03 1.7429e+03 6.5082e+00 --1.9800e+03 1.7714e+03 6.5403e+00 --1.9800e+03 1.8000e+03 6.5720e+00 --1.9800e+03 1.8286e+03 6.6035e+00 --1.9800e+03 1.8571e+03 6.6346e+00 --1.9800e+03 1.8857e+03 6.6655e+00 --1.9800e+03 1.9143e+03 6.6960e+00 --1.9800e+03 1.9429e+03 6.7262e+00 --1.9800e+03 1.9714e+03 6.7560e+00 --1.9800e+03 2.0000e+03 6.7856e+00 --1.9500e+03 -2.0000e+03 6.7549e+00 --1.9500e+03 -1.9714e+03 6.7245e+00 --1.9500e+03 -1.9429e+03 6.6938e+00 --1.9500e+03 -1.9143e+03 6.6628e+00 --1.9500e+03 -1.8857e+03 6.6314e+00 --1.9500e+03 -1.8571e+03 6.5997e+00 --1.9500e+03 -1.8286e+03 6.5677e+00 --1.9500e+03 -1.8000e+03 6.5353e+00 --1.9500e+03 -1.7714e+03 6.5025e+00 --1.9500e+03 -1.7429e+03 6.4695e+00 --1.9500e+03 -1.7143e+03 6.4361e+00 --1.9500e+03 -1.6857e+03 6.4024e+00 --1.9500e+03 -1.6571e+03 6.3684e+00 --1.9500e+03 -1.6286e+03 6.3341e+00 --1.9500e+03 -1.6000e+03 6.2995e+00 --1.9500e+03 -1.5714e+03 6.2647e+00 --1.9500e+03 -1.5429e+03 6.2296e+00 --1.9500e+03 -1.5143e+03 6.1942e+00 --1.9500e+03 -1.4857e+03 6.1585e+00 --1.9500e+03 -1.4571e+03 6.1227e+00 --1.9500e+03 -1.4286e+03 6.0866e+00 --1.9500e+03 -1.4000e+03 6.0503e+00 --1.9500e+03 -1.3714e+03 6.0139e+00 --1.9500e+03 -1.3429e+03 5.9773e+00 --1.9500e+03 -1.3143e+03 5.9405e+00 --1.9500e+03 -1.2857e+03 5.9037e+00 --1.9500e+03 -1.2571e+03 5.8667e+00 --1.9500e+03 -1.2286e+03 5.8297e+00 --1.9500e+03 -1.2000e+03 5.7927e+00 --1.9500e+03 -1.1714e+03 5.7556e+00 --1.9500e+03 -1.1429e+03 5.7185e+00 --1.9500e+03 -1.1143e+03 5.6815e+00 --1.9500e+03 -1.0857e+03 5.6446e+00 --1.9500e+03 -1.0571e+03 5.6078e+00 --1.9500e+03 -1.0286e+03 5.5712e+00 --1.9500e+03 -1.0000e+03 5.5348e+00 --1.9500e+03 -9.7143e+02 5.4985e+00 --1.9500e+03 -9.4286e+02 5.4626e+00 --1.9500e+03 -9.1429e+02 5.4270e+00 --1.9500e+03 -8.8571e+02 5.3917e+00 --1.9500e+03 -8.5714e+02 5.3569e+00 --1.9500e+03 -8.2857e+02 5.3225e+00 --1.9500e+03 -8.0000e+02 5.2886e+00 --1.9500e+03 -7.7143e+02 5.2552e+00 --1.9500e+03 -7.4286e+02 5.2225e+00 --1.9500e+03 -7.1429e+02 5.1903e+00 --1.9500e+03 -6.8571e+02 5.1590e+00 --1.9500e+03 -6.5714e+02 5.1283e+00 --1.9500e+03 -6.2857e+02 5.0985e+00 --1.9500e+03 -6.0000e+02 5.0695e+00 --1.9500e+03 -5.7143e+02 5.0414e+00 --1.9500e+03 -5.4286e+02 5.0143e+00 --1.9500e+03 -5.1429e+02 4.9882e+00 --1.9500e+03 -4.8571e+02 4.9632e+00 --1.9500e+03 -4.5714e+02 4.9393e+00 --1.9500e+03 -4.2857e+02 4.9166e+00 --1.9500e+03 -4.0000e+02 4.8951e+00 --1.9500e+03 -3.7143e+02 4.8748e+00 --1.9500e+03 -3.4286e+02 4.8559e+00 --1.9500e+03 -3.1429e+02 4.8383e+00 --1.9500e+03 -2.8571e+02 4.8221e+00 --1.9500e+03 -2.5714e+02 4.8073e+00 --1.9500e+03 -2.2857e+02 4.7939e+00 --1.9500e+03 -2.0000e+02 4.7821e+00 --1.9500e+03 -1.7143e+02 4.7718e+00 --1.9500e+03 -1.4286e+02 4.7630e+00 --1.9500e+03 -1.1429e+02 4.7558e+00 --1.9500e+03 -8.5714e+01 4.7502e+00 --1.9500e+03 -5.7143e+01 4.7462e+00 --1.9500e+03 -2.8571e+01 4.7437e+00 --1.9500e+03 0.0000e+00 4.7429e+00 --1.9500e+03 2.8571e+01 4.7437e+00 --1.9500e+03 5.7143e+01 4.7462e+00 --1.9500e+03 8.5714e+01 4.7502e+00 --1.9500e+03 1.1429e+02 4.7558e+00 --1.9500e+03 1.4286e+02 4.7630e+00 --1.9500e+03 1.7143e+02 4.7718e+00 --1.9500e+03 2.0000e+02 4.7821e+00 --1.9500e+03 2.2857e+02 4.7939e+00 --1.9500e+03 2.5714e+02 4.8073e+00 --1.9500e+03 2.8571e+02 4.8221e+00 --1.9500e+03 3.1429e+02 4.8383e+00 --1.9500e+03 3.4286e+02 4.8559e+00 --1.9500e+03 3.7143e+02 4.8748e+00 --1.9500e+03 4.0000e+02 4.8951e+00 --1.9500e+03 4.2857e+02 4.9166e+00 --1.9500e+03 4.5714e+02 4.9393e+00 --1.9500e+03 4.8571e+02 4.9632e+00 --1.9500e+03 5.1429e+02 4.9882e+00 --1.9500e+03 5.4286e+02 5.0143e+00 --1.9500e+03 5.7143e+02 5.0414e+00 --1.9500e+03 6.0000e+02 5.0695e+00 --1.9500e+03 6.2857e+02 5.0985e+00 --1.9500e+03 6.5714e+02 5.1283e+00 --1.9500e+03 6.8571e+02 5.1590e+00 --1.9500e+03 7.1429e+02 5.1903e+00 --1.9500e+03 7.4286e+02 5.2225e+00 --1.9500e+03 7.7143e+02 5.2552e+00 --1.9500e+03 8.0000e+02 5.2886e+00 --1.9500e+03 8.2857e+02 5.3225e+00 --1.9500e+03 8.5714e+02 5.3569e+00 --1.9500e+03 8.8571e+02 5.3917e+00 --1.9500e+03 9.1429e+02 5.4270e+00 --1.9500e+03 9.4286e+02 5.4626e+00 --1.9500e+03 9.7143e+02 5.4985e+00 --1.9500e+03 1.0000e+03 5.5348e+00 --1.9500e+03 1.0286e+03 5.5712e+00 --1.9500e+03 1.0571e+03 5.6078e+00 --1.9500e+03 1.0857e+03 5.6446e+00 --1.9500e+03 1.1143e+03 5.6815e+00 --1.9500e+03 1.1429e+03 5.7185e+00 --1.9500e+03 1.1714e+03 5.7556e+00 --1.9500e+03 1.2000e+03 5.7927e+00 --1.9500e+03 1.2286e+03 5.8297e+00 --1.9500e+03 1.2571e+03 5.8667e+00 --1.9500e+03 1.2857e+03 5.9037e+00 --1.9500e+03 1.3143e+03 5.9405e+00 --1.9500e+03 1.3429e+03 5.9773e+00 --1.9500e+03 1.3714e+03 6.0139e+00 --1.9500e+03 1.4000e+03 6.0503e+00 --1.9500e+03 1.4286e+03 6.0866e+00 --1.9500e+03 1.4571e+03 6.1227e+00 --1.9500e+03 1.4857e+03 6.1585e+00 --1.9500e+03 1.5143e+03 6.1942e+00 --1.9500e+03 1.5429e+03 6.2296e+00 --1.9500e+03 1.5714e+03 6.2647e+00 --1.9500e+03 1.6000e+03 6.2995e+00 --1.9500e+03 1.6286e+03 6.3341e+00 --1.9500e+03 1.6571e+03 6.3684e+00 --1.9500e+03 1.6857e+03 6.4024e+00 --1.9500e+03 1.7143e+03 6.4361e+00 --1.9500e+03 1.7429e+03 6.4695e+00 --1.9500e+03 1.7714e+03 6.5025e+00 --1.9500e+03 1.8000e+03 6.5353e+00 --1.9500e+03 1.8286e+03 6.5677e+00 --1.9500e+03 1.8571e+03 6.5997e+00 --1.9500e+03 1.8857e+03 6.6314e+00 --1.9500e+03 1.9143e+03 6.6628e+00 --1.9500e+03 1.9429e+03 6.6938e+00 --1.9500e+03 1.9714e+03 6.7245e+00 --1.9500e+03 2.0000e+03 6.7549e+00 --1.9200e+03 -2.0000e+03 6.7238e+00 --1.9200e+03 -1.9714e+03 6.6927e+00 --1.9200e+03 -1.9429e+03 6.6611e+00 --1.9200e+03 -1.9143e+03 6.6292e+00 --1.9200e+03 -1.8857e+03 6.5970e+00 --1.9200e+03 -1.8571e+03 6.5643e+00 --1.9200e+03 -1.8286e+03 6.5313e+00 --1.9200e+03 -1.8000e+03 6.4980e+00 --1.9200e+03 -1.7714e+03 6.4642e+00 --1.9200e+03 -1.7429e+03 6.4302e+00 --1.9200e+03 -1.7143e+03 6.3957e+00 --1.9200e+03 -1.6857e+03 6.3610e+00 --1.9200e+03 -1.6571e+03 6.3259e+00 --1.9200e+03 -1.6286e+03 6.2905e+00 --1.9200e+03 -1.6000e+03 6.2547e+00 --1.9200e+03 -1.5714e+03 6.2187e+00 --1.9200e+03 -1.5429e+03 6.1823e+00 --1.9200e+03 -1.5143e+03 6.1457e+00 --1.9200e+03 -1.4857e+03 6.1088e+00 --1.9200e+03 -1.4571e+03 6.0716e+00 --1.9200e+03 -1.4286e+03 6.0342e+00 --1.9200e+03 -1.4000e+03 5.9966e+00 --1.9200e+03 -1.3714e+03 5.9587e+00 --1.9200e+03 -1.3429e+03 5.9207e+00 --1.9200e+03 -1.3143e+03 5.8825e+00 --1.9200e+03 -1.2857e+03 5.8442e+00 --1.9200e+03 -1.2571e+03 5.8057e+00 --1.9200e+03 -1.2286e+03 5.7672e+00 --1.9200e+03 -1.2000e+03 5.7286e+00 --1.9200e+03 -1.1714e+03 5.6899e+00 --1.9200e+03 -1.1429e+03 5.6513e+00 --1.9200e+03 -1.1143e+03 5.6127e+00 --1.9200e+03 -1.0857e+03 5.5741e+00 --1.9200e+03 -1.0571e+03 5.5357e+00 --1.9200e+03 -1.0286e+03 5.4974e+00 --1.9200e+03 -1.0000e+03 5.4592e+00 --1.9200e+03 -9.7143e+02 5.4213e+00 --1.9200e+03 -9.4286e+02 5.3837e+00 --1.9200e+03 -9.1429e+02 5.3463e+00 --1.9200e+03 -8.8571e+02 5.3093e+00 --1.9200e+03 -8.5714e+02 5.2727e+00 --1.9200e+03 -8.2857e+02 5.2366e+00 --1.9200e+03 -8.0000e+02 5.2010e+00 --1.9200e+03 -7.7143e+02 5.1659e+00 --1.9200e+03 -7.4286e+02 5.1314e+00 --1.9200e+03 -7.1429e+02 5.0976e+00 --1.9200e+03 -6.8571e+02 5.0646e+00 --1.9200e+03 -6.5714e+02 5.0323e+00 --1.9200e+03 -6.2857e+02 5.0008e+00 --1.9200e+03 -6.0000e+02 4.9702e+00 --1.9200e+03 -5.7143e+02 4.9406e+00 --1.9200e+03 -5.4286e+02 4.9120e+00 --1.9200e+03 -5.1429e+02 4.8844e+00 --1.9200e+03 -4.8571e+02 4.8580e+00 --1.9200e+03 -4.5714e+02 4.8327e+00 --1.9200e+03 -4.2857e+02 4.8087e+00 --1.9200e+03 -4.0000e+02 4.7859e+00 --1.9200e+03 -3.7143e+02 4.7644e+00 --1.9200e+03 -3.4286e+02 4.7444e+00 --1.9200e+03 -3.1429e+02 4.7257e+00 --1.9200e+03 -2.8571e+02 4.7085e+00 --1.9200e+03 -2.5714e+02 4.6929e+00 --1.9200e+03 -2.2857e+02 4.6787e+00 --1.9200e+03 -2.0000e+02 4.6662e+00 --1.9200e+03 -1.7143e+02 4.6552e+00 --1.9200e+03 -1.4286e+02 4.6459e+00 --1.9200e+03 -1.1429e+02 4.6383e+00 --1.9200e+03 -8.5714e+01 4.6323e+00 --1.9200e+03 -5.7143e+01 4.6280e+00 --1.9200e+03 -2.8571e+01 4.6255e+00 --1.9200e+03 0.0000e+00 4.6246e+00 --1.9200e+03 2.8571e+01 4.6255e+00 --1.9200e+03 5.7143e+01 4.6280e+00 --1.9200e+03 8.5714e+01 4.6323e+00 --1.9200e+03 1.1429e+02 4.6383e+00 --1.9200e+03 1.4286e+02 4.6459e+00 --1.9200e+03 1.7143e+02 4.6552e+00 --1.9200e+03 2.0000e+02 4.6662e+00 --1.9200e+03 2.2857e+02 4.6787e+00 --1.9200e+03 2.5714e+02 4.6929e+00 --1.9200e+03 2.8571e+02 4.7085e+00 --1.9200e+03 3.1429e+02 4.7257e+00 --1.9200e+03 3.4286e+02 4.7444e+00 --1.9200e+03 3.7143e+02 4.7644e+00 --1.9200e+03 4.0000e+02 4.7859e+00 --1.9200e+03 4.2857e+02 4.8087e+00 --1.9200e+03 4.5714e+02 4.8327e+00 --1.9200e+03 4.8571e+02 4.8580e+00 --1.9200e+03 5.1429e+02 4.8844e+00 --1.9200e+03 5.4286e+02 4.9120e+00 --1.9200e+03 5.7143e+02 4.9406e+00 --1.9200e+03 6.0000e+02 4.9702e+00 --1.9200e+03 6.2857e+02 5.0008e+00 --1.9200e+03 6.5714e+02 5.0323e+00 --1.9200e+03 6.8571e+02 5.0646e+00 --1.9200e+03 7.1429e+02 5.0976e+00 --1.9200e+03 7.4286e+02 5.1314e+00 --1.9200e+03 7.7143e+02 5.1659e+00 --1.9200e+03 8.0000e+02 5.2010e+00 --1.9200e+03 8.2857e+02 5.2366e+00 --1.9200e+03 8.5714e+02 5.2727e+00 --1.9200e+03 8.8571e+02 5.3093e+00 --1.9200e+03 9.1429e+02 5.3463e+00 --1.9200e+03 9.4286e+02 5.3837e+00 --1.9200e+03 9.7143e+02 5.4213e+00 --1.9200e+03 1.0000e+03 5.4592e+00 --1.9200e+03 1.0286e+03 5.4974e+00 --1.9200e+03 1.0571e+03 5.5357e+00 --1.9200e+03 1.0857e+03 5.5741e+00 --1.9200e+03 1.1143e+03 5.6127e+00 --1.9200e+03 1.1429e+03 5.6513e+00 --1.9200e+03 1.1714e+03 5.6899e+00 --1.9200e+03 1.2000e+03 5.7286e+00 --1.9200e+03 1.2286e+03 5.7672e+00 --1.9200e+03 1.2571e+03 5.8057e+00 --1.9200e+03 1.2857e+03 5.8442e+00 --1.9200e+03 1.3143e+03 5.8825e+00 --1.9200e+03 1.3429e+03 5.9207e+00 --1.9200e+03 1.3714e+03 5.9587e+00 --1.9200e+03 1.4000e+03 5.9966e+00 --1.9200e+03 1.4286e+03 6.0342e+00 --1.9200e+03 1.4571e+03 6.0716e+00 --1.9200e+03 1.4857e+03 6.1088e+00 --1.9200e+03 1.5143e+03 6.1457e+00 --1.9200e+03 1.5429e+03 6.1823e+00 --1.9200e+03 1.5714e+03 6.2187e+00 --1.9200e+03 1.6000e+03 6.2547e+00 --1.9200e+03 1.6286e+03 6.2905e+00 --1.9200e+03 1.6571e+03 6.3259e+00 --1.9200e+03 1.6857e+03 6.3610e+00 --1.9200e+03 1.7143e+03 6.3957e+00 --1.9200e+03 1.7429e+03 6.4302e+00 --1.9200e+03 1.7714e+03 6.4642e+00 --1.9200e+03 1.8000e+03 6.4980e+00 --1.9200e+03 1.8286e+03 6.5313e+00 --1.9200e+03 1.8571e+03 6.5643e+00 --1.9200e+03 1.8857e+03 6.5970e+00 --1.9200e+03 1.9143e+03 6.6292e+00 --1.9200e+03 1.9429e+03 6.6611e+00 --1.9200e+03 1.9714e+03 6.6927e+00 --1.9200e+03 2.0000e+03 6.7238e+00 --1.8900e+03 -2.0000e+03 6.6924e+00 --1.8900e+03 -1.9714e+03 6.6604e+00 --1.8900e+03 -1.9429e+03 6.6280e+00 --1.8900e+03 -1.9143e+03 6.5952e+00 --1.8900e+03 -1.8857e+03 6.5620e+00 --1.8900e+03 -1.8571e+03 6.5285e+00 --1.8900e+03 -1.8286e+03 6.4945e+00 --1.8900e+03 -1.8000e+03 6.4601e+00 --1.8900e+03 -1.7714e+03 6.4254e+00 --1.8900e+03 -1.7429e+03 6.3903e+00 --1.8900e+03 -1.7143e+03 6.3547e+00 --1.8900e+03 -1.6857e+03 6.3189e+00 --1.8900e+03 -1.6571e+03 6.2826e+00 --1.8900e+03 -1.6286e+03 6.2460e+00 --1.8900e+03 -1.6000e+03 6.2091e+00 --1.8900e+03 -1.5714e+03 6.1718e+00 --1.8900e+03 -1.5429e+03 6.1342e+00 --1.8900e+03 -1.5143e+03 6.0962e+00 --1.8900e+03 -1.4857e+03 6.0580e+00 --1.8900e+03 -1.4571e+03 6.0195e+00 --1.8900e+03 -1.4286e+03 5.9807e+00 --1.8900e+03 -1.4000e+03 5.9416e+00 --1.8900e+03 -1.3714e+03 5.9023e+00 --1.8900e+03 -1.3429e+03 5.8628e+00 --1.8900e+03 -1.3143e+03 5.8231e+00 --1.8900e+03 -1.2857e+03 5.7833e+00 --1.8900e+03 -1.2571e+03 5.7432e+00 --1.8900e+03 -1.2286e+03 5.7031e+00 --1.8900e+03 -1.2000e+03 5.6628e+00 --1.8900e+03 -1.1714e+03 5.6225e+00 --1.8900e+03 -1.1429e+03 5.5822e+00 --1.8900e+03 -1.1143e+03 5.5419e+00 --1.8900e+03 -1.0857e+03 5.5016e+00 --1.8900e+03 -1.0571e+03 5.4614e+00 --1.8900e+03 -1.0286e+03 5.4213e+00 --1.8900e+03 -1.0000e+03 5.3814e+00 --1.8900e+03 -9.7143e+02 5.3416e+00 --1.8900e+03 -9.4286e+02 5.3022e+00 --1.8900e+03 -9.1429e+02 5.2630e+00 --1.8900e+03 -8.8571e+02 5.2242e+00 --1.8900e+03 -8.5714e+02 5.1857e+00 --1.8900e+03 -8.2857e+02 5.1478e+00 --1.8900e+03 -8.0000e+02 5.1103e+00 --1.8900e+03 -7.7143e+02 5.0734e+00 --1.8900e+03 -7.4286e+02 5.0371e+00 --1.8900e+03 -7.1429e+02 5.0015e+00 --1.8900e+03 -6.8571e+02 4.9666e+00 --1.8900e+03 -6.5714e+02 4.9326e+00 --1.8900e+03 -6.2857e+02 4.8994e+00 --1.8900e+03 -6.0000e+02 4.8671e+00 --1.8900e+03 -5.7143e+02 4.8358e+00 --1.8900e+03 -5.4286e+02 4.8055e+00 --1.8900e+03 -5.1429e+02 4.7764e+00 --1.8900e+03 -4.8571e+02 4.7484e+00 --1.8900e+03 -4.5714e+02 4.7216e+00 --1.8900e+03 -4.2857e+02 4.6962e+00 --1.8900e+03 -4.0000e+02 4.6720e+00 --1.8900e+03 -3.7143e+02 4.6493e+00 --1.8900e+03 -3.4286e+02 4.6280e+00 --1.8900e+03 -3.1429e+02 4.6082e+00 --1.8900e+03 -2.8571e+02 4.5900e+00 --1.8900e+03 -2.5714e+02 4.5734e+00 --1.8900e+03 -2.2857e+02 4.5583e+00 --1.8900e+03 -2.0000e+02 4.5450e+00 --1.8900e+03 -1.7143e+02 4.5334e+00 --1.8900e+03 -1.4286e+02 4.5235e+00 --1.8900e+03 -1.1429e+02 4.5154e+00 --1.8900e+03 -8.5714e+01 4.5090e+00 --1.8900e+03 -5.7143e+01 4.5045e+00 --1.8900e+03 -2.8571e+01 4.5018e+00 --1.8900e+03 0.0000e+00 4.5008e+00 --1.8900e+03 2.8571e+01 4.5018e+00 --1.8900e+03 5.7143e+01 4.5045e+00 --1.8900e+03 8.5714e+01 4.5090e+00 --1.8900e+03 1.1429e+02 4.5154e+00 --1.8900e+03 1.4286e+02 4.5235e+00 --1.8900e+03 1.7143e+02 4.5334e+00 --1.8900e+03 2.0000e+02 4.5450e+00 --1.8900e+03 2.2857e+02 4.5583e+00 --1.8900e+03 2.5714e+02 4.5734e+00 --1.8900e+03 2.8571e+02 4.5900e+00 --1.8900e+03 3.1429e+02 4.6082e+00 --1.8900e+03 3.4286e+02 4.6280e+00 --1.8900e+03 3.7143e+02 4.6493e+00 --1.8900e+03 4.0000e+02 4.6720e+00 --1.8900e+03 4.2857e+02 4.6962e+00 --1.8900e+03 4.5714e+02 4.7216e+00 --1.8900e+03 4.8571e+02 4.7484e+00 --1.8900e+03 5.1429e+02 4.7764e+00 --1.8900e+03 5.4286e+02 4.8055e+00 --1.8900e+03 5.7143e+02 4.8358e+00 --1.8900e+03 6.0000e+02 4.8671e+00 --1.8900e+03 6.2857e+02 4.8994e+00 --1.8900e+03 6.5714e+02 4.9326e+00 --1.8900e+03 6.8571e+02 4.9666e+00 --1.8900e+03 7.1429e+02 5.0015e+00 --1.8900e+03 7.4286e+02 5.0371e+00 --1.8900e+03 7.7143e+02 5.0734e+00 --1.8900e+03 8.0000e+02 5.1103e+00 --1.8900e+03 8.2857e+02 5.1478e+00 --1.8900e+03 8.5714e+02 5.1857e+00 --1.8900e+03 8.8571e+02 5.2242e+00 --1.8900e+03 9.1429e+02 5.2630e+00 --1.8900e+03 9.4286e+02 5.3022e+00 --1.8900e+03 9.7143e+02 5.3416e+00 --1.8900e+03 1.0000e+03 5.3814e+00 --1.8900e+03 1.0286e+03 5.4213e+00 --1.8900e+03 1.0571e+03 5.4614e+00 --1.8900e+03 1.0857e+03 5.5016e+00 --1.8900e+03 1.1143e+03 5.5419e+00 --1.8900e+03 1.1429e+03 5.5822e+00 --1.8900e+03 1.1714e+03 5.6225e+00 --1.8900e+03 1.2000e+03 5.6628e+00 --1.8900e+03 1.2286e+03 5.7031e+00 --1.8900e+03 1.2571e+03 5.7432e+00 --1.8900e+03 1.2857e+03 5.7833e+00 --1.8900e+03 1.3143e+03 5.8231e+00 --1.8900e+03 1.3429e+03 5.8628e+00 --1.8900e+03 1.3714e+03 5.9023e+00 --1.8900e+03 1.4000e+03 5.9416e+00 --1.8900e+03 1.4286e+03 5.9807e+00 --1.8900e+03 1.4571e+03 6.0195e+00 --1.8900e+03 1.4857e+03 6.0580e+00 --1.8900e+03 1.5143e+03 6.0962e+00 --1.8900e+03 1.5429e+03 6.1342e+00 --1.8900e+03 1.5714e+03 6.1718e+00 --1.8900e+03 1.6000e+03 6.2091e+00 --1.8900e+03 1.6286e+03 6.2460e+00 --1.8900e+03 1.6571e+03 6.2826e+00 --1.8900e+03 1.6857e+03 6.3189e+00 --1.8900e+03 1.7143e+03 6.3547e+00 --1.8900e+03 1.7429e+03 6.3903e+00 --1.8900e+03 1.7714e+03 6.4254e+00 --1.8900e+03 1.8000e+03 6.4601e+00 --1.8900e+03 1.8286e+03 6.4945e+00 --1.8900e+03 1.8571e+03 6.5285e+00 --1.8900e+03 1.8857e+03 6.5620e+00 --1.8900e+03 1.9143e+03 6.5952e+00 --1.8900e+03 1.9429e+03 6.6280e+00 --1.8900e+03 1.9714e+03 6.6604e+00 --1.8900e+03 2.0000e+03 6.6924e+00 --1.8600e+03 -2.0000e+03 6.6607e+00 --1.8600e+03 -1.9714e+03 6.6278e+00 --1.8600e+03 -1.9429e+03 6.5945e+00 --1.8600e+03 -1.9143e+03 6.5608e+00 --1.8600e+03 -1.8857e+03 6.5267e+00 --1.8600e+03 -1.8571e+03 6.4921e+00 --1.8600e+03 -1.8286e+03 6.4572e+00 --1.8600e+03 -1.8000e+03 6.4218e+00 --1.8600e+03 -1.7714e+03 6.3860e+00 --1.8600e+03 -1.7429e+03 6.3497e+00 --1.8600e+03 -1.7143e+03 6.3131e+00 --1.8600e+03 -1.6857e+03 6.2761e+00 --1.8600e+03 -1.6571e+03 6.2386e+00 --1.8600e+03 -1.6286e+03 6.2008e+00 --1.8600e+03 -1.6000e+03 6.1626e+00 --1.8600e+03 -1.5714e+03 6.1241e+00 --1.8600e+03 -1.5429e+03 6.0851e+00 --1.8600e+03 -1.5143e+03 6.0459e+00 --1.8600e+03 -1.4857e+03 6.0062e+00 --1.8600e+03 -1.4571e+03 5.9663e+00 --1.8600e+03 -1.4286e+03 5.9260e+00 --1.8600e+03 -1.4000e+03 5.8855e+00 --1.8600e+03 -1.3714e+03 5.8447e+00 --1.8600e+03 -1.3429e+03 5.8036e+00 --1.8600e+03 -1.3143e+03 5.7623e+00 --1.8600e+03 -1.2857e+03 5.7208e+00 --1.8600e+03 -1.2571e+03 5.6791e+00 --1.8600e+03 -1.2286e+03 5.6373e+00 --1.8600e+03 -1.2000e+03 5.5954e+00 --1.8600e+03 -1.1714e+03 5.5533e+00 --1.8600e+03 -1.1429e+03 5.5112e+00 --1.8600e+03 -1.1143e+03 5.4691e+00 --1.8600e+03 -1.0857e+03 5.4270e+00 --1.8600e+03 -1.0571e+03 5.3849e+00 --1.8600e+03 -1.0286e+03 5.3429e+00 --1.8600e+03 -1.0000e+03 5.3011e+00 --1.8600e+03 -9.7143e+02 5.2595e+00 --1.8600e+03 -9.4286e+02 5.2180e+00 --1.8600e+03 -9.1429e+02 5.1769e+00 --1.8600e+03 -8.8571e+02 5.1361e+00 --1.8600e+03 -8.5714e+02 5.0958e+00 --1.8600e+03 -8.2857e+02 5.0558e+00 --1.8600e+03 -8.0000e+02 5.0164e+00 --1.8600e+03 -7.7143e+02 4.9775e+00 --1.8600e+03 -7.4286e+02 4.9393e+00 --1.8600e+03 -7.1429e+02 4.9017e+00 --1.8600e+03 -6.8571e+02 4.8650e+00 --1.8600e+03 -6.5714e+02 4.8290e+00 --1.8600e+03 -6.2857e+02 4.7939e+00 --1.8600e+03 -6.0000e+02 4.7598e+00 --1.8600e+03 -5.7143e+02 4.7267e+00 --1.8600e+03 -5.4286e+02 4.6947e+00 --1.8600e+03 -5.1429e+02 4.6638e+00 --1.8600e+03 -4.8571e+02 4.6342e+00 --1.8600e+03 -4.5714e+02 4.6058e+00 --1.8600e+03 -4.2857e+02 4.5788e+00 --1.8600e+03 -4.0000e+02 4.5532e+00 --1.8600e+03 -3.7143e+02 4.5291e+00 --1.8600e+03 -3.4286e+02 4.5065e+00 --1.8600e+03 -3.1429e+02 4.4855e+00 --1.8600e+03 -2.8571e+02 4.4661e+00 --1.8600e+03 -2.5714e+02 4.4484e+00 --1.8600e+03 -2.2857e+02 4.4325e+00 --1.8600e+03 -2.0000e+02 4.4183e+00 --1.8600e+03 -1.7143e+02 4.4059e+00 --1.8600e+03 -1.4286e+02 4.3954e+00 --1.8600e+03 -1.1429e+02 4.3867e+00 --1.8600e+03 -8.5714e+01 4.3800e+00 --1.8600e+03 -5.7143e+01 4.3752e+00 --1.8600e+03 -2.8571e+01 4.3723e+00 --1.8600e+03 0.0000e+00 4.3713e+00 --1.8600e+03 2.8571e+01 4.3723e+00 --1.8600e+03 5.7143e+01 4.3752e+00 --1.8600e+03 8.5714e+01 4.3800e+00 --1.8600e+03 1.1429e+02 4.3867e+00 --1.8600e+03 1.4286e+02 4.3954e+00 --1.8600e+03 1.7143e+02 4.4059e+00 --1.8600e+03 2.0000e+02 4.4183e+00 --1.8600e+03 2.2857e+02 4.4325e+00 --1.8600e+03 2.5714e+02 4.4484e+00 --1.8600e+03 2.8571e+02 4.4661e+00 --1.8600e+03 3.1429e+02 4.4855e+00 --1.8600e+03 3.4286e+02 4.5065e+00 --1.8600e+03 3.7143e+02 4.5291e+00 --1.8600e+03 4.0000e+02 4.5532e+00 --1.8600e+03 4.2857e+02 4.5788e+00 --1.8600e+03 4.5714e+02 4.6058e+00 --1.8600e+03 4.8571e+02 4.6342e+00 --1.8600e+03 5.1429e+02 4.6638e+00 --1.8600e+03 5.4286e+02 4.6947e+00 --1.8600e+03 5.7143e+02 4.7267e+00 --1.8600e+03 6.0000e+02 4.7598e+00 --1.8600e+03 6.2857e+02 4.7939e+00 --1.8600e+03 6.5714e+02 4.8290e+00 --1.8600e+03 6.8571e+02 4.8650e+00 --1.8600e+03 7.1429e+02 4.9017e+00 --1.8600e+03 7.4286e+02 4.9393e+00 --1.8600e+03 7.7143e+02 4.9775e+00 --1.8600e+03 8.0000e+02 5.0164e+00 --1.8600e+03 8.2857e+02 5.0558e+00 --1.8600e+03 8.5714e+02 5.0958e+00 --1.8600e+03 8.8571e+02 5.1361e+00 --1.8600e+03 9.1429e+02 5.1769e+00 --1.8600e+03 9.4286e+02 5.2180e+00 --1.8600e+03 9.7143e+02 5.2595e+00 --1.8600e+03 1.0000e+03 5.3011e+00 --1.8600e+03 1.0286e+03 5.3429e+00 --1.8600e+03 1.0571e+03 5.3849e+00 --1.8600e+03 1.0857e+03 5.4270e+00 --1.8600e+03 1.1143e+03 5.4691e+00 --1.8600e+03 1.1429e+03 5.5112e+00 --1.8600e+03 1.1714e+03 5.5533e+00 --1.8600e+03 1.2000e+03 5.5954e+00 --1.8600e+03 1.2286e+03 5.6373e+00 --1.8600e+03 1.2571e+03 5.6791e+00 --1.8600e+03 1.2857e+03 5.7208e+00 --1.8600e+03 1.3143e+03 5.7623e+00 --1.8600e+03 1.3429e+03 5.8036e+00 --1.8600e+03 1.3714e+03 5.8447e+00 --1.8600e+03 1.4000e+03 5.8855e+00 --1.8600e+03 1.4286e+03 5.9260e+00 --1.8600e+03 1.4571e+03 5.9663e+00 --1.8600e+03 1.4857e+03 6.0062e+00 --1.8600e+03 1.5143e+03 6.0459e+00 --1.8600e+03 1.5429e+03 6.0851e+00 --1.8600e+03 1.5714e+03 6.1241e+00 --1.8600e+03 1.6000e+03 6.1626e+00 --1.8600e+03 1.6286e+03 6.2008e+00 --1.8600e+03 1.6571e+03 6.2386e+00 --1.8600e+03 1.6857e+03 6.2761e+00 --1.8600e+03 1.7143e+03 6.3131e+00 --1.8600e+03 1.7429e+03 6.3497e+00 --1.8600e+03 1.7714e+03 6.3860e+00 --1.8600e+03 1.8000e+03 6.4218e+00 --1.8600e+03 1.8286e+03 6.4572e+00 --1.8600e+03 1.8571e+03 6.4921e+00 --1.8600e+03 1.8857e+03 6.5267e+00 --1.8600e+03 1.9143e+03 6.5608e+00 --1.8600e+03 1.9429e+03 6.5945e+00 --1.8600e+03 1.9714e+03 6.6278e+00 --1.8600e+03 2.0000e+03 6.6607e+00 --1.8300e+03 -2.0000e+03 6.6287e+00 --1.8300e+03 -1.9714e+03 6.5949e+00 --1.8300e+03 -1.9429e+03 6.5607e+00 --1.8300e+03 -1.9143e+03 6.5260e+00 --1.8300e+03 -1.8857e+03 6.4909e+00 --1.8300e+03 -1.8571e+03 6.4553e+00 --1.8300e+03 -1.8286e+03 6.4193e+00 --1.8300e+03 -1.8000e+03 6.3829e+00 --1.8300e+03 -1.7714e+03 6.3460e+00 --1.8300e+03 -1.7429e+03 6.3086e+00 --1.8300e+03 -1.7143e+03 6.2708e+00 --1.8300e+03 -1.6857e+03 6.2326e+00 --1.8300e+03 -1.6571e+03 6.1939e+00 --1.8300e+03 -1.6286e+03 6.1549e+00 --1.8300e+03 -1.6000e+03 6.1154e+00 --1.8300e+03 -1.5714e+03 6.0755e+00 --1.8300e+03 -1.5429e+03 6.0352e+00 --1.8300e+03 -1.5143e+03 5.9945e+00 --1.8300e+03 -1.4857e+03 5.9534e+00 --1.8300e+03 -1.4571e+03 5.9120e+00 --1.8300e+03 -1.4286e+03 5.8702e+00 --1.8300e+03 -1.4000e+03 5.8281e+00 --1.8300e+03 -1.3714e+03 5.7857e+00 --1.8300e+03 -1.3429e+03 5.7430e+00 --1.8300e+03 -1.3143e+03 5.7001e+00 --1.8300e+03 -1.2857e+03 5.6569e+00 --1.8300e+03 -1.2571e+03 5.6135e+00 --1.8300e+03 -1.2286e+03 5.5698e+00 --1.8300e+03 -1.2000e+03 5.5261e+00 --1.8300e+03 -1.1714e+03 5.4822e+00 --1.8300e+03 -1.1429e+03 5.4382e+00 --1.8300e+03 -1.1143e+03 5.3942e+00 --1.8300e+03 -1.0857e+03 5.3501e+00 --1.8300e+03 -1.0571e+03 5.3061e+00 --1.8300e+03 -1.0286e+03 5.2621e+00 --1.8300e+03 -1.0000e+03 5.2183e+00 --1.8300e+03 -9.7143e+02 5.1746e+00 --1.8300e+03 -9.4286e+02 5.1312e+00 --1.8300e+03 -9.1429e+02 5.0880e+00 --1.8300e+03 -8.8571e+02 5.0451e+00 --1.8300e+03 -8.5714e+02 5.0026e+00 --1.8300e+03 -8.2857e+02 4.9606e+00 --1.8300e+03 -8.0000e+02 4.9191e+00 --1.8300e+03 -7.7143e+02 4.8781e+00 --1.8300e+03 -7.4286e+02 4.8378e+00 --1.8300e+03 -7.1429e+02 4.7981e+00 --1.8300e+03 -6.8571e+02 4.7593e+00 --1.8300e+03 -6.5714e+02 4.7213e+00 --1.8300e+03 -6.2857e+02 4.6842e+00 --1.8300e+03 -6.0000e+02 4.6481e+00 --1.8300e+03 -5.7143e+02 4.6130e+00 --1.8300e+03 -5.4286e+02 4.5791e+00 --1.8300e+03 -5.1429e+02 4.5464e+00 --1.8300e+03 -4.8571e+02 4.5150e+00 --1.8300e+03 -4.5714e+02 4.4849e+00 --1.8300e+03 -4.2857e+02 4.4562e+00 --1.8300e+03 -4.0000e+02 4.4291e+00 --1.8300e+03 -3.7143e+02 4.4034e+00 --1.8300e+03 -3.4286e+02 4.3794e+00 --1.8300e+03 -3.1429e+02 4.3571e+00 --1.8300e+03 -2.8571e+02 4.3365e+00 --1.8300e+03 -2.5714e+02 4.3176e+00 --1.8300e+03 -2.2857e+02 4.3007e+00 --1.8300e+03 -2.0000e+02 4.2856e+00 --1.8300e+03 -1.7143e+02 4.2724e+00 --1.8300e+03 -1.4286e+02 4.2612e+00 --1.8300e+03 -1.1429e+02 4.2520e+00 --1.8300e+03 -8.5714e+01 4.2448e+00 --1.8300e+03 -5.7143e+01 4.2396e+00 --1.8300e+03 -2.8571e+01 4.2365e+00 --1.8300e+03 0.0000e+00 4.2355e+00 --1.8300e+03 2.8571e+01 4.2365e+00 --1.8300e+03 5.7143e+01 4.2396e+00 --1.8300e+03 8.5714e+01 4.2448e+00 --1.8300e+03 1.1429e+02 4.2520e+00 --1.8300e+03 1.4286e+02 4.2612e+00 --1.8300e+03 1.7143e+02 4.2724e+00 --1.8300e+03 2.0000e+02 4.2856e+00 --1.8300e+03 2.2857e+02 4.3007e+00 --1.8300e+03 2.5714e+02 4.3176e+00 --1.8300e+03 2.8571e+02 4.3365e+00 --1.8300e+03 3.1429e+02 4.3571e+00 --1.8300e+03 3.4286e+02 4.3794e+00 --1.8300e+03 3.7143e+02 4.4034e+00 --1.8300e+03 4.0000e+02 4.4291e+00 --1.8300e+03 4.2857e+02 4.4562e+00 --1.8300e+03 4.5714e+02 4.4849e+00 --1.8300e+03 4.8571e+02 4.5150e+00 --1.8300e+03 5.1429e+02 4.5464e+00 --1.8300e+03 5.4286e+02 4.5791e+00 --1.8300e+03 5.7143e+02 4.6130e+00 --1.8300e+03 6.0000e+02 4.6481e+00 --1.8300e+03 6.2857e+02 4.6842e+00 --1.8300e+03 6.5714e+02 4.7213e+00 --1.8300e+03 6.8571e+02 4.7593e+00 --1.8300e+03 7.1429e+02 4.7981e+00 --1.8300e+03 7.4286e+02 4.8378e+00 --1.8300e+03 7.7143e+02 4.8781e+00 --1.8300e+03 8.0000e+02 4.9191e+00 --1.8300e+03 8.2857e+02 4.9606e+00 --1.8300e+03 8.5714e+02 5.0026e+00 --1.8300e+03 8.8571e+02 5.0451e+00 --1.8300e+03 9.1429e+02 5.0880e+00 --1.8300e+03 9.4286e+02 5.1312e+00 --1.8300e+03 9.7143e+02 5.1746e+00 --1.8300e+03 1.0000e+03 5.2183e+00 --1.8300e+03 1.0286e+03 5.2621e+00 --1.8300e+03 1.0571e+03 5.3061e+00 --1.8300e+03 1.0857e+03 5.3501e+00 --1.8300e+03 1.1143e+03 5.3942e+00 --1.8300e+03 1.1429e+03 5.4382e+00 --1.8300e+03 1.1714e+03 5.4822e+00 --1.8300e+03 1.2000e+03 5.5261e+00 --1.8300e+03 1.2286e+03 5.5698e+00 --1.8300e+03 1.2571e+03 5.6135e+00 --1.8300e+03 1.2857e+03 5.6569e+00 --1.8300e+03 1.3143e+03 5.7001e+00 --1.8300e+03 1.3429e+03 5.7430e+00 --1.8300e+03 1.3714e+03 5.7857e+00 --1.8300e+03 1.4000e+03 5.8281e+00 --1.8300e+03 1.4286e+03 5.8702e+00 --1.8300e+03 1.4571e+03 5.9120e+00 --1.8300e+03 1.4857e+03 5.9534e+00 --1.8300e+03 1.5143e+03 5.9945e+00 --1.8300e+03 1.5429e+03 6.0352e+00 --1.8300e+03 1.5714e+03 6.0755e+00 --1.8300e+03 1.6000e+03 6.1154e+00 --1.8300e+03 1.6286e+03 6.1549e+00 --1.8300e+03 1.6571e+03 6.1939e+00 --1.8300e+03 1.6857e+03 6.2326e+00 --1.8300e+03 1.7143e+03 6.2708e+00 --1.8300e+03 1.7429e+03 6.3086e+00 --1.8300e+03 1.7714e+03 6.3460e+00 --1.8300e+03 1.8000e+03 6.3829e+00 --1.8300e+03 1.8286e+03 6.4193e+00 --1.8300e+03 1.8571e+03 6.4553e+00 --1.8300e+03 1.8857e+03 6.4909e+00 --1.8300e+03 1.9143e+03 6.5260e+00 --1.8300e+03 1.9429e+03 6.5607e+00 --1.8300e+03 1.9714e+03 6.5949e+00 --1.8300e+03 2.0000e+03 6.6287e+00 --1.8000e+03 -2.0000e+03 6.5963e+00 --1.8000e+03 -1.9714e+03 6.5616e+00 --1.8000e+03 -1.9429e+03 6.5264e+00 --1.8000e+03 -1.9143e+03 6.4908e+00 --1.8000e+03 -1.8857e+03 6.4547e+00 --1.8000e+03 -1.8571e+03 6.4181e+00 --1.8000e+03 -1.8286e+03 6.3810e+00 --1.8000e+03 -1.8000e+03 6.3434e+00 --1.8000e+03 -1.7714e+03 6.3054e+00 --1.8000e+03 -1.7429e+03 6.2669e+00 --1.8000e+03 -1.7143e+03 6.2279e+00 --1.8000e+03 -1.6857e+03 6.1884e+00 --1.8000e+03 -1.6571e+03 6.1485e+00 --1.8000e+03 -1.6286e+03 6.1081e+00 --1.8000e+03 -1.6000e+03 6.0673e+00 --1.8000e+03 -1.5714e+03 6.0260e+00 --1.8000e+03 -1.5429e+03 5.9843e+00 --1.8000e+03 -1.5143e+03 5.9421e+00 --1.8000e+03 -1.4857e+03 5.8995e+00 --1.8000e+03 -1.4571e+03 5.8566e+00 --1.8000e+03 -1.4286e+03 5.8132e+00 --1.8000e+03 -1.4000e+03 5.7695e+00 --1.8000e+03 -1.3714e+03 5.7254e+00 --1.8000e+03 -1.3429e+03 5.6810e+00 --1.8000e+03 -1.3143e+03 5.6363e+00 --1.8000e+03 -1.2857e+03 5.5913e+00 --1.8000e+03 -1.2571e+03 5.5461e+00 --1.8000e+03 -1.2286e+03 5.5006e+00 --1.8000e+03 -1.2000e+03 5.4550e+00 --1.8000e+03 -1.1714e+03 5.4091e+00 --1.8000e+03 -1.1429e+03 5.3632e+00 --1.8000e+03 -1.1143e+03 5.3171e+00 --1.8000e+03 -1.0857e+03 5.2710e+00 --1.8000e+03 -1.0571e+03 5.2249e+00 --1.8000e+03 -1.0286e+03 5.1788e+00 --1.8000e+03 -1.0000e+03 5.1329e+00 --1.8000e+03 -9.7143e+02 5.0870e+00 --1.8000e+03 -9.4286e+02 5.0414e+00 --1.8000e+03 -9.1429e+02 4.9960e+00 --1.8000e+03 -8.8571e+02 4.9509e+00 --1.8000e+03 -8.5714e+02 4.9062e+00 --1.8000e+03 -8.2857e+02 4.8619e+00 --1.8000e+03 -8.0000e+02 4.8181e+00 --1.8000e+03 -7.7143e+02 4.7749e+00 --1.8000e+03 -7.4286e+02 4.7324e+00 --1.8000e+03 -7.1429e+02 4.6905e+00 --1.8000e+03 -6.8571e+02 4.6494e+00 --1.8000e+03 -6.5714e+02 4.6092e+00 --1.8000e+03 -6.2857e+02 4.5700e+00 --1.8000e+03 -6.0000e+02 4.5318e+00 --1.8000e+03 -5.7143e+02 4.4946e+00 --1.8000e+03 -5.4286e+02 4.4587e+00 --1.8000e+03 -5.1429e+02 4.4240e+00 --1.8000e+03 -4.8571e+02 4.3906e+00 --1.8000e+03 -4.5714e+02 4.3586e+00 --1.8000e+03 -4.2857e+02 4.3282e+00 --1.8000e+03 -4.0000e+02 4.2993e+00 --1.8000e+03 -3.7143e+02 4.2720e+00 --1.8000e+03 -3.4286e+02 4.2464e+00 --1.8000e+03 -3.1429e+02 4.2226e+00 --1.8000e+03 -2.8571e+02 4.2007e+00 --1.8000e+03 -2.5714e+02 4.1807e+00 --1.8000e+03 -2.2857e+02 4.1626e+00 --1.8000e+03 -2.0000e+02 4.1465e+00 --1.8000e+03 -1.7143e+02 4.1324e+00 --1.8000e+03 -1.4286e+02 4.1205e+00 --1.8000e+03 -1.1429e+02 4.1106e+00 --1.8000e+03 -8.5714e+01 4.1030e+00 --1.8000e+03 -5.7143e+01 4.0975e+00 --1.8000e+03 -2.8571e+01 4.0942e+00 --1.8000e+03 0.0000e+00 4.0931e+00 --1.8000e+03 2.8571e+01 4.0942e+00 --1.8000e+03 5.7143e+01 4.0975e+00 --1.8000e+03 8.5714e+01 4.1030e+00 --1.8000e+03 1.1429e+02 4.1106e+00 --1.8000e+03 1.4286e+02 4.1205e+00 --1.8000e+03 1.7143e+02 4.1324e+00 --1.8000e+03 2.0000e+02 4.1465e+00 --1.8000e+03 2.2857e+02 4.1626e+00 --1.8000e+03 2.5714e+02 4.1807e+00 --1.8000e+03 2.8571e+02 4.2007e+00 --1.8000e+03 3.1429e+02 4.2226e+00 --1.8000e+03 3.4286e+02 4.2464e+00 --1.8000e+03 3.7143e+02 4.2720e+00 --1.8000e+03 4.0000e+02 4.2993e+00 --1.8000e+03 4.2857e+02 4.3282e+00 --1.8000e+03 4.5714e+02 4.3586e+00 --1.8000e+03 4.8571e+02 4.3906e+00 --1.8000e+03 5.1429e+02 4.4240e+00 --1.8000e+03 5.4286e+02 4.4587e+00 --1.8000e+03 5.7143e+02 4.4946e+00 --1.8000e+03 6.0000e+02 4.5318e+00 --1.8000e+03 6.2857e+02 4.5700e+00 --1.8000e+03 6.5714e+02 4.6092e+00 --1.8000e+03 6.8571e+02 4.6494e+00 --1.8000e+03 7.1429e+02 4.6905e+00 --1.8000e+03 7.4286e+02 4.7324e+00 --1.8000e+03 7.7143e+02 4.7749e+00 --1.8000e+03 8.0000e+02 4.8181e+00 --1.8000e+03 8.2857e+02 4.8619e+00 --1.8000e+03 8.5714e+02 4.9062e+00 --1.8000e+03 8.8571e+02 4.9509e+00 --1.8000e+03 9.1429e+02 4.9960e+00 --1.8000e+03 9.4286e+02 5.0414e+00 --1.8000e+03 9.7143e+02 5.0870e+00 --1.8000e+03 1.0000e+03 5.1329e+00 --1.8000e+03 1.0286e+03 5.1788e+00 --1.8000e+03 1.0571e+03 5.2249e+00 --1.8000e+03 1.0857e+03 5.2710e+00 --1.8000e+03 1.1143e+03 5.3171e+00 --1.8000e+03 1.1429e+03 5.3632e+00 --1.8000e+03 1.1714e+03 5.4091e+00 --1.8000e+03 1.2000e+03 5.4550e+00 --1.8000e+03 1.2286e+03 5.5006e+00 --1.8000e+03 1.2571e+03 5.5461e+00 --1.8000e+03 1.2857e+03 5.5913e+00 --1.8000e+03 1.3143e+03 5.6363e+00 --1.8000e+03 1.3429e+03 5.6810e+00 --1.8000e+03 1.3714e+03 5.7254e+00 --1.8000e+03 1.4000e+03 5.7695e+00 --1.8000e+03 1.4286e+03 5.8132e+00 --1.8000e+03 1.4571e+03 5.8566e+00 --1.8000e+03 1.4857e+03 5.8995e+00 --1.8000e+03 1.5143e+03 5.9421e+00 --1.8000e+03 1.5429e+03 5.9843e+00 --1.8000e+03 1.5714e+03 6.0260e+00 --1.8000e+03 1.6000e+03 6.0673e+00 --1.8000e+03 1.6286e+03 6.1081e+00 --1.8000e+03 1.6571e+03 6.1485e+00 --1.8000e+03 1.6857e+03 6.1884e+00 --1.8000e+03 1.7143e+03 6.2279e+00 --1.8000e+03 1.7429e+03 6.2669e+00 --1.8000e+03 1.7714e+03 6.3054e+00 --1.8000e+03 1.8000e+03 6.3434e+00 --1.8000e+03 1.8286e+03 6.3810e+00 --1.8000e+03 1.8571e+03 6.4181e+00 --1.8000e+03 1.8857e+03 6.4547e+00 --1.8000e+03 1.9143e+03 6.4908e+00 --1.8000e+03 1.9429e+03 6.5264e+00 --1.8000e+03 1.9714e+03 6.5616e+00 --1.8000e+03 2.0000e+03 6.5963e+00 --1.7700e+03 -2.0000e+03 6.5636e+00 --1.7700e+03 -1.9714e+03 6.5279e+00 --1.7700e+03 -1.9429e+03 6.4918e+00 --1.7700e+03 -1.9143e+03 6.4552e+00 --1.7700e+03 -1.8857e+03 6.4180e+00 --1.7700e+03 -1.8571e+03 6.3804e+00 --1.7700e+03 -1.8286e+03 6.3422e+00 --1.7700e+03 -1.8000e+03 6.3035e+00 --1.7700e+03 -1.7714e+03 6.2643e+00 --1.7700e+03 -1.7429e+03 6.2246e+00 --1.7700e+03 -1.7143e+03 6.1843e+00 --1.7700e+03 -1.6857e+03 6.1436e+00 --1.7700e+03 -1.6571e+03 6.1023e+00 --1.7700e+03 -1.6286e+03 6.0606e+00 --1.7700e+03 -1.6000e+03 6.0183e+00 --1.7700e+03 -1.5714e+03 5.9756e+00 --1.7700e+03 -1.5429e+03 5.9324e+00 --1.7700e+03 -1.5143e+03 5.8887e+00 --1.7700e+03 -1.4857e+03 5.8446e+00 --1.7700e+03 -1.4571e+03 5.8000e+00 --1.7700e+03 -1.4286e+03 5.7550e+00 --1.7700e+03 -1.4000e+03 5.7096e+00 --1.7700e+03 -1.3714e+03 5.6638e+00 --1.7700e+03 -1.3429e+03 5.6176e+00 --1.7700e+03 -1.3143e+03 5.5711e+00 --1.7700e+03 -1.2857e+03 5.5242e+00 --1.7700e+03 -1.2571e+03 5.4770e+00 --1.7700e+03 -1.2286e+03 5.4296e+00 --1.7700e+03 -1.2000e+03 5.3819e+00 --1.7700e+03 -1.1714e+03 5.3340e+00 --1.7700e+03 -1.1429e+03 5.2860e+00 --1.7700e+03 -1.1143e+03 5.2378e+00 --1.7700e+03 -1.0857e+03 5.1895e+00 --1.7700e+03 -1.0571e+03 5.1412e+00 --1.7700e+03 -1.0286e+03 5.0929e+00 --1.7700e+03 -1.0000e+03 5.0447e+00 --1.7700e+03 -9.7143e+02 4.9966e+00 --1.7700e+03 -9.4286e+02 4.9486e+00 --1.7700e+03 -9.1429e+02 4.9008e+00 --1.7700e+03 -8.8571e+02 4.8534e+00 --1.7700e+03 -8.5714e+02 4.8063e+00 --1.7700e+03 -8.2857e+02 4.7596e+00 --1.7700e+03 -8.0000e+02 4.7134e+00 --1.7700e+03 -7.7143e+02 4.6678e+00 --1.7700e+03 -7.4286e+02 4.6229e+00 --1.7700e+03 -7.1429e+02 4.5786e+00 --1.7700e+03 -6.8571e+02 4.5352e+00 --1.7700e+03 -6.5714e+02 4.4926e+00 --1.7700e+03 -6.2857e+02 4.4510e+00 --1.7700e+03 -6.0000e+02 4.4105e+00 --1.7700e+03 -5.7143e+02 4.3711e+00 --1.7700e+03 -5.4286e+02 4.3329e+00 --1.7700e+03 -5.1429e+02 4.2960e+00 --1.7700e+03 -4.8571e+02 4.2606e+00 --1.7700e+03 -4.5714e+02 4.2266e+00 --1.7700e+03 -4.2857e+02 4.1942e+00 --1.7700e+03 -4.0000e+02 4.1634e+00 --1.7700e+03 -3.7143e+02 4.1344e+00 --1.7700e+03 -3.4286e+02 4.1071e+00 --1.7700e+03 -3.1429e+02 4.0818e+00 --1.7700e+03 -2.8571e+02 4.0584e+00 --1.7700e+03 -2.5714e+02 4.0370e+00 --1.7700e+03 -2.2857e+02 4.0177e+00 --1.7700e+03 -2.0000e+02 4.0005e+00 --1.7700e+03 -1.7143e+02 3.9855e+00 --1.7700e+03 -1.4286e+02 3.9727e+00 --1.7700e+03 -1.1429e+02 3.9622e+00 --1.7700e+03 -8.5714e+01 3.9540e+00 --1.7700e+03 -5.7143e+01 3.9481e+00 --1.7700e+03 -2.8571e+01 3.9446e+00 --1.7700e+03 0.0000e+00 3.9434e+00 --1.7700e+03 2.8571e+01 3.9446e+00 --1.7700e+03 5.7143e+01 3.9481e+00 --1.7700e+03 8.5714e+01 3.9540e+00 --1.7700e+03 1.1429e+02 3.9622e+00 --1.7700e+03 1.4286e+02 3.9727e+00 --1.7700e+03 1.7143e+02 3.9855e+00 --1.7700e+03 2.0000e+02 4.0005e+00 --1.7700e+03 2.2857e+02 4.0177e+00 --1.7700e+03 2.5714e+02 4.0370e+00 --1.7700e+03 2.8571e+02 4.0584e+00 --1.7700e+03 3.1429e+02 4.0818e+00 --1.7700e+03 3.4286e+02 4.1071e+00 --1.7700e+03 3.7143e+02 4.1344e+00 --1.7700e+03 4.0000e+02 4.1634e+00 --1.7700e+03 4.2857e+02 4.1942e+00 --1.7700e+03 4.5714e+02 4.2266e+00 --1.7700e+03 4.8571e+02 4.2606e+00 --1.7700e+03 5.1429e+02 4.2960e+00 --1.7700e+03 5.4286e+02 4.3329e+00 --1.7700e+03 5.7143e+02 4.3711e+00 --1.7700e+03 6.0000e+02 4.4105e+00 --1.7700e+03 6.2857e+02 4.4510e+00 --1.7700e+03 6.5714e+02 4.4926e+00 --1.7700e+03 6.8571e+02 4.5352e+00 --1.7700e+03 7.1429e+02 4.5786e+00 --1.7700e+03 7.4286e+02 4.6229e+00 --1.7700e+03 7.7143e+02 4.6678e+00 --1.7700e+03 8.0000e+02 4.7134e+00 --1.7700e+03 8.2857e+02 4.7596e+00 --1.7700e+03 8.5714e+02 4.8063e+00 --1.7700e+03 8.8571e+02 4.8534e+00 --1.7700e+03 9.1429e+02 4.9008e+00 --1.7700e+03 9.4286e+02 4.9486e+00 --1.7700e+03 9.7143e+02 4.9966e+00 --1.7700e+03 1.0000e+03 5.0447e+00 --1.7700e+03 1.0286e+03 5.0929e+00 --1.7700e+03 1.0571e+03 5.1412e+00 --1.7700e+03 1.0857e+03 5.1895e+00 --1.7700e+03 1.1143e+03 5.2378e+00 --1.7700e+03 1.1429e+03 5.2860e+00 --1.7700e+03 1.1714e+03 5.3340e+00 --1.7700e+03 1.2000e+03 5.3819e+00 --1.7700e+03 1.2286e+03 5.4296e+00 --1.7700e+03 1.2571e+03 5.4770e+00 --1.7700e+03 1.2857e+03 5.5242e+00 --1.7700e+03 1.3143e+03 5.5711e+00 --1.7700e+03 1.3429e+03 5.6176e+00 --1.7700e+03 1.3714e+03 5.6638e+00 --1.7700e+03 1.4000e+03 5.7096e+00 --1.7700e+03 1.4286e+03 5.7550e+00 --1.7700e+03 1.4571e+03 5.8000e+00 --1.7700e+03 1.4857e+03 5.8446e+00 --1.7700e+03 1.5143e+03 5.8887e+00 --1.7700e+03 1.5429e+03 5.9324e+00 --1.7700e+03 1.5714e+03 5.9756e+00 --1.7700e+03 1.6000e+03 6.0183e+00 --1.7700e+03 1.6286e+03 6.0606e+00 --1.7700e+03 1.6571e+03 6.1023e+00 --1.7700e+03 1.6857e+03 6.1436e+00 --1.7700e+03 1.7143e+03 6.1843e+00 --1.7700e+03 1.7429e+03 6.2246e+00 --1.7700e+03 1.7714e+03 6.2643e+00 --1.7700e+03 1.8000e+03 6.3035e+00 --1.7700e+03 1.8286e+03 6.3422e+00 --1.7700e+03 1.8571e+03 6.3804e+00 --1.7700e+03 1.8857e+03 6.4180e+00 --1.7700e+03 1.9143e+03 6.4552e+00 --1.7700e+03 1.9429e+03 6.4918e+00 --1.7700e+03 1.9714e+03 6.5279e+00 --1.7700e+03 2.0000e+03 6.5636e+00 --1.7400e+03 -2.0000e+03 6.5305e+00 --1.7400e+03 -1.9714e+03 6.4940e+00 --1.7400e+03 -1.9429e+03 6.4568e+00 --1.7400e+03 -1.9143e+03 6.4192e+00 --1.7400e+03 -1.8857e+03 6.3810e+00 --1.7400e+03 -1.8571e+03 6.3422e+00 --1.7400e+03 -1.8286e+03 6.3029e+00 --1.7400e+03 -1.8000e+03 6.2630e+00 --1.7400e+03 -1.7714e+03 6.2226e+00 --1.7400e+03 -1.7429e+03 6.1816e+00 --1.7400e+03 -1.7143e+03 6.1401e+00 --1.7400e+03 -1.6857e+03 6.0980e+00 --1.7400e+03 -1.6571e+03 6.0554e+00 --1.7400e+03 -1.6286e+03 6.0123e+00 --1.7400e+03 -1.6000e+03 5.9686e+00 --1.7400e+03 -1.5714e+03 5.9243e+00 --1.7400e+03 -1.5429e+03 5.8796e+00 --1.7400e+03 -1.5143e+03 5.8343e+00 --1.7400e+03 -1.4857e+03 5.7886e+00 --1.7400e+03 -1.4571e+03 5.7423e+00 --1.7400e+03 -1.4286e+03 5.6956e+00 --1.7400e+03 -1.4000e+03 5.6484e+00 --1.7400e+03 -1.3714e+03 5.6008e+00 --1.7400e+03 -1.3429e+03 5.5527e+00 --1.7400e+03 -1.3143e+03 5.5042e+00 --1.7400e+03 -1.2857e+03 5.4554e+00 --1.7400e+03 -1.2571e+03 5.4062e+00 --1.7400e+03 -1.2286e+03 5.3567e+00 --1.7400e+03 -1.2000e+03 5.3069e+00 --1.7400e+03 -1.1714e+03 5.2569e+00 --1.7400e+03 -1.1429e+03 5.2066e+00 --1.7400e+03 -1.1143e+03 5.1562e+00 --1.7400e+03 -1.0857e+03 5.1056e+00 --1.7400e+03 -1.0571e+03 5.0550e+00 --1.7400e+03 -1.0286e+03 5.0043e+00 --1.7400e+03 -1.0000e+03 4.9537e+00 --1.7400e+03 -9.7143e+02 4.9031e+00 --1.7400e+03 -9.4286e+02 4.8526e+00 --1.7400e+03 -9.1429e+02 4.8024e+00 --1.7400e+03 -8.8571e+02 4.7524e+00 --1.7400e+03 -8.5714e+02 4.7028e+00 --1.7400e+03 -8.2857e+02 4.6535e+00 --1.7400e+03 -8.0000e+02 4.6048e+00 --1.7400e+03 -7.7143e+02 4.5566e+00 --1.7400e+03 -7.4286e+02 4.5090e+00 --1.7400e+03 -7.1429e+02 4.4622e+00 --1.7400e+03 -6.8571e+02 4.4162e+00 --1.7400e+03 -6.5714e+02 4.3711e+00 --1.7400e+03 -6.2857e+02 4.3270e+00 --1.7400e+03 -6.0000e+02 4.2840e+00 --1.7400e+03 -5.7143e+02 4.2421e+00 --1.7400e+03 -5.4286e+02 4.2016e+00 --1.7400e+03 -5.1429e+02 4.1624e+00 --1.7400e+03 -4.8571e+02 4.1246e+00 --1.7400e+03 -4.5714e+02 4.0884e+00 --1.7400e+03 -4.2857e+02 4.0539e+00 --1.7400e+03 -4.0000e+02 4.0211e+00 --1.7400e+03 -3.7143e+02 3.9901e+00 --1.7400e+03 -3.4286e+02 3.9611e+00 --1.7400e+03 -3.1429e+02 3.9340e+00 --1.7400e+03 -2.8571e+02 3.9090e+00 --1.7400e+03 -2.5714e+02 3.8861e+00 --1.7400e+03 -2.2857e+02 3.8655e+00 --1.7400e+03 -2.0000e+02 3.8471e+00 --1.7400e+03 -1.7143e+02 3.8311e+00 --1.7400e+03 -1.4286e+02 3.8174e+00 --1.7400e+03 -1.1429e+02 3.8062e+00 --1.7400e+03 -8.5714e+01 3.7974e+00 --1.7400e+03 -5.7143e+01 3.7911e+00 --1.7400e+03 -2.8571e+01 3.7873e+00 --1.7400e+03 0.0000e+00 3.7861e+00 --1.7400e+03 2.8571e+01 3.7873e+00 --1.7400e+03 5.7143e+01 3.7911e+00 --1.7400e+03 8.5714e+01 3.7974e+00 --1.7400e+03 1.1429e+02 3.8062e+00 --1.7400e+03 1.4286e+02 3.8174e+00 --1.7400e+03 1.7143e+02 3.8311e+00 --1.7400e+03 2.0000e+02 3.8471e+00 --1.7400e+03 2.2857e+02 3.8655e+00 --1.7400e+03 2.5714e+02 3.8861e+00 --1.7400e+03 2.8571e+02 3.9090e+00 --1.7400e+03 3.1429e+02 3.9340e+00 --1.7400e+03 3.4286e+02 3.9611e+00 --1.7400e+03 3.7143e+02 3.9901e+00 --1.7400e+03 4.0000e+02 4.0211e+00 --1.7400e+03 4.2857e+02 4.0539e+00 --1.7400e+03 4.5714e+02 4.0884e+00 --1.7400e+03 4.8571e+02 4.1246e+00 --1.7400e+03 5.1429e+02 4.1624e+00 --1.7400e+03 5.4286e+02 4.2016e+00 --1.7400e+03 5.7143e+02 4.2421e+00 --1.7400e+03 6.0000e+02 4.2840e+00 --1.7400e+03 6.2857e+02 4.3270e+00 --1.7400e+03 6.5714e+02 4.3711e+00 --1.7400e+03 6.8571e+02 4.4162e+00 --1.7400e+03 7.1429e+02 4.4622e+00 --1.7400e+03 7.4286e+02 4.5090e+00 --1.7400e+03 7.7143e+02 4.5566e+00 --1.7400e+03 8.0000e+02 4.6048e+00 --1.7400e+03 8.2857e+02 4.6535e+00 --1.7400e+03 8.5714e+02 4.7028e+00 --1.7400e+03 8.8571e+02 4.7524e+00 --1.7400e+03 9.1429e+02 4.8024e+00 --1.7400e+03 9.4286e+02 4.8526e+00 --1.7400e+03 9.7143e+02 4.9031e+00 --1.7400e+03 1.0000e+03 4.9537e+00 --1.7400e+03 1.0286e+03 5.0043e+00 --1.7400e+03 1.0571e+03 5.0550e+00 --1.7400e+03 1.0857e+03 5.1056e+00 --1.7400e+03 1.1143e+03 5.1562e+00 --1.7400e+03 1.1429e+03 5.2066e+00 --1.7400e+03 1.1714e+03 5.2569e+00 --1.7400e+03 1.2000e+03 5.3069e+00 --1.7400e+03 1.2286e+03 5.3567e+00 --1.7400e+03 1.2571e+03 5.4062e+00 --1.7400e+03 1.2857e+03 5.4554e+00 --1.7400e+03 1.3143e+03 5.5042e+00 --1.7400e+03 1.3429e+03 5.5527e+00 --1.7400e+03 1.3714e+03 5.6008e+00 --1.7400e+03 1.4000e+03 5.6484e+00 --1.7400e+03 1.4286e+03 5.6956e+00 --1.7400e+03 1.4571e+03 5.7423e+00 --1.7400e+03 1.4857e+03 5.7886e+00 --1.7400e+03 1.5143e+03 5.8343e+00 --1.7400e+03 1.5429e+03 5.8796e+00 --1.7400e+03 1.5714e+03 5.9243e+00 --1.7400e+03 1.6000e+03 5.9686e+00 --1.7400e+03 1.6286e+03 6.0123e+00 --1.7400e+03 1.6571e+03 6.0554e+00 --1.7400e+03 1.6857e+03 6.0980e+00 --1.7400e+03 1.7143e+03 6.1401e+00 --1.7400e+03 1.7429e+03 6.1816e+00 --1.7400e+03 1.7714e+03 6.2226e+00 --1.7400e+03 1.8000e+03 6.2630e+00 --1.7400e+03 1.8286e+03 6.3029e+00 --1.7400e+03 1.8571e+03 6.3422e+00 --1.7400e+03 1.8857e+03 6.3810e+00 --1.7400e+03 1.9143e+03 6.4192e+00 --1.7400e+03 1.9429e+03 6.4568e+00 --1.7400e+03 1.9714e+03 6.4940e+00 --1.7400e+03 2.0000e+03 6.5305e+00 --1.7100e+03 -2.0000e+03 6.4972e+00 --1.7100e+03 -1.9714e+03 6.4596e+00 --1.7100e+03 -1.9429e+03 6.4215e+00 --1.7100e+03 -1.9143e+03 6.3828e+00 --1.7100e+03 -1.8857e+03 6.3435e+00 --1.7100e+03 -1.8571e+03 6.3036e+00 --1.7100e+03 -1.8286e+03 6.2631e+00 --1.7100e+03 -1.8000e+03 6.2220e+00 --1.7100e+03 -1.7714e+03 6.1803e+00 --1.7100e+03 -1.7429e+03 6.1381e+00 --1.7100e+03 -1.7143e+03 6.0952e+00 --1.7100e+03 -1.6857e+03 6.0518e+00 --1.7100e+03 -1.6571e+03 6.0078e+00 --1.7100e+03 -1.6286e+03 5.9631e+00 --1.7100e+03 -1.6000e+03 5.9179e+00 --1.7100e+03 -1.5714e+03 5.8722e+00 --1.7100e+03 -1.5429e+03 5.8258e+00 --1.7100e+03 -1.5143e+03 5.7789e+00 --1.7100e+03 -1.4857e+03 5.7314e+00 --1.7100e+03 -1.4571e+03 5.6834e+00 --1.7100e+03 -1.4286e+03 5.6349e+00 --1.7100e+03 -1.4000e+03 5.5858e+00 --1.7100e+03 -1.3714e+03 5.5363e+00 --1.7100e+03 -1.3429e+03 5.4863e+00 --1.7100e+03 -1.3143e+03 5.4358e+00 --1.7100e+03 -1.2857e+03 5.3849e+00 --1.7100e+03 -1.2571e+03 5.3336e+00 --1.7100e+03 -1.2286e+03 5.2819e+00 --1.7100e+03 -1.2000e+03 5.2299e+00 --1.7100e+03 -1.1714e+03 5.1776e+00 --1.7100e+03 -1.1429e+03 5.1250e+00 --1.7100e+03 -1.1143e+03 5.0722e+00 --1.7100e+03 -1.0857e+03 5.0192e+00 --1.7100e+03 -1.0571e+03 4.9661e+00 --1.7100e+03 -1.0286e+03 4.9129e+00 --1.7100e+03 -1.0000e+03 4.8596e+00 --1.7100e+03 -9.7143e+02 4.8065e+00 --1.7100e+03 -9.4286e+02 4.7534e+00 --1.7100e+03 -9.1429e+02 4.7005e+00 --1.7100e+03 -8.8571e+02 4.6478e+00 --1.7100e+03 -8.5714e+02 4.5954e+00 --1.7100e+03 -8.2857e+02 4.5434e+00 --1.7100e+03 -8.0000e+02 4.4919e+00 --1.7100e+03 -7.7143e+02 4.4409e+00 --1.7100e+03 -7.4286e+02 4.3906e+00 --1.7100e+03 -7.1429e+02 4.3410e+00 --1.7100e+03 -6.8571e+02 4.2922e+00 --1.7100e+03 -6.5714e+02 4.2444e+00 --1.7100e+03 -6.2857e+02 4.1976e+00 --1.7100e+03 -6.0000e+02 4.1519e+00 --1.7100e+03 -5.7143e+02 4.1074e+00 --1.7100e+03 -5.4286e+02 4.0642e+00 --1.7100e+03 -5.1429e+02 4.0225e+00 --1.7100e+03 -4.8571e+02 3.9823e+00 --1.7100e+03 -4.5714e+02 3.9437e+00 --1.7100e+03 -4.2857e+02 3.9068e+00 --1.7100e+03 -4.0000e+02 3.8718e+00 --1.7100e+03 -3.7143e+02 3.8388e+00 --1.7100e+03 -3.4286e+02 3.8077e+00 --1.7100e+03 -3.1429e+02 3.7788e+00 --1.7100e+03 -2.8571e+02 3.7520e+00 --1.7100e+03 -2.5714e+02 3.7276e+00 --1.7100e+03 -2.2857e+02 3.7055e+00 --1.7100e+03 -2.0000e+02 3.6858e+00 --1.7100e+03 -1.7143e+02 3.6686e+00 --1.7100e+03 -1.4286e+02 3.6540e+00 --1.7100e+03 -1.1429e+02 3.6419e+00 --1.7100e+03 -8.5714e+01 3.6325e+00 --1.7100e+03 -5.7143e+01 3.6258e+00 --1.7100e+03 -2.8571e+01 3.6217e+00 --1.7100e+03 0.0000e+00 3.6204e+00 --1.7100e+03 2.8571e+01 3.6217e+00 --1.7100e+03 5.7143e+01 3.6258e+00 --1.7100e+03 8.5714e+01 3.6325e+00 --1.7100e+03 1.1429e+02 3.6419e+00 --1.7100e+03 1.4286e+02 3.6540e+00 --1.7100e+03 1.7143e+02 3.6686e+00 --1.7100e+03 2.0000e+02 3.6858e+00 --1.7100e+03 2.2857e+02 3.7055e+00 --1.7100e+03 2.5714e+02 3.7276e+00 --1.7100e+03 2.8571e+02 3.7520e+00 --1.7100e+03 3.1429e+02 3.7788e+00 --1.7100e+03 3.4286e+02 3.8077e+00 --1.7100e+03 3.7143e+02 3.8388e+00 --1.7100e+03 4.0000e+02 3.8718e+00 --1.7100e+03 4.2857e+02 3.9068e+00 --1.7100e+03 4.5714e+02 3.9437e+00 --1.7100e+03 4.8571e+02 3.9823e+00 --1.7100e+03 5.1429e+02 4.0225e+00 --1.7100e+03 5.4286e+02 4.0642e+00 --1.7100e+03 5.7143e+02 4.1074e+00 --1.7100e+03 6.0000e+02 4.1519e+00 --1.7100e+03 6.2857e+02 4.1976e+00 --1.7100e+03 6.5714e+02 4.2444e+00 --1.7100e+03 6.8571e+02 4.2922e+00 --1.7100e+03 7.1429e+02 4.3410e+00 --1.7100e+03 7.4286e+02 4.3906e+00 --1.7100e+03 7.7143e+02 4.4409e+00 --1.7100e+03 8.0000e+02 4.4919e+00 --1.7100e+03 8.2857e+02 4.5434e+00 --1.7100e+03 8.5714e+02 4.5954e+00 --1.7100e+03 8.8571e+02 4.6478e+00 --1.7100e+03 9.1429e+02 4.7005e+00 --1.7100e+03 9.4286e+02 4.7534e+00 --1.7100e+03 9.7143e+02 4.8065e+00 --1.7100e+03 1.0000e+03 4.8596e+00 --1.7100e+03 1.0286e+03 4.9129e+00 --1.7100e+03 1.0571e+03 4.9661e+00 --1.7100e+03 1.0857e+03 5.0192e+00 --1.7100e+03 1.1143e+03 5.0722e+00 --1.7100e+03 1.1429e+03 5.1250e+00 --1.7100e+03 1.1714e+03 5.1776e+00 --1.7100e+03 1.2000e+03 5.2299e+00 --1.7100e+03 1.2286e+03 5.2819e+00 --1.7100e+03 1.2571e+03 5.3336e+00 --1.7100e+03 1.2857e+03 5.3849e+00 --1.7100e+03 1.3143e+03 5.4358e+00 --1.7100e+03 1.3429e+03 5.4863e+00 --1.7100e+03 1.3714e+03 5.5363e+00 --1.7100e+03 1.4000e+03 5.5858e+00 --1.7100e+03 1.4286e+03 5.6349e+00 --1.7100e+03 1.4571e+03 5.6834e+00 --1.7100e+03 1.4857e+03 5.7314e+00 --1.7100e+03 1.5143e+03 5.7789e+00 --1.7100e+03 1.5429e+03 5.8258e+00 --1.7100e+03 1.5714e+03 5.8722e+00 --1.7100e+03 1.6000e+03 5.9179e+00 --1.7100e+03 1.6286e+03 5.9631e+00 --1.7100e+03 1.6571e+03 6.0078e+00 --1.7100e+03 1.6857e+03 6.0518e+00 --1.7100e+03 1.7143e+03 6.0952e+00 --1.7100e+03 1.7429e+03 6.1381e+00 --1.7100e+03 1.7714e+03 6.1803e+00 --1.7100e+03 1.8000e+03 6.2220e+00 --1.7100e+03 1.8286e+03 6.2631e+00 --1.7100e+03 1.8571e+03 6.3036e+00 --1.7100e+03 1.8857e+03 6.3435e+00 --1.7100e+03 1.9143e+03 6.3828e+00 --1.7100e+03 1.9429e+03 6.4215e+00 --1.7100e+03 1.9714e+03 6.4596e+00 --1.7100e+03 2.0000e+03 6.4972e+00 --1.6800e+03 -2.0000e+03 6.4636e+00 --1.6800e+03 -1.9714e+03 6.4250e+00 --1.6800e+03 -1.9429e+03 6.3858e+00 --1.6800e+03 -1.9143e+03 6.3460e+00 --1.6800e+03 -1.8857e+03 6.3055e+00 --1.6800e+03 -1.8571e+03 6.2645e+00 --1.6800e+03 -1.8286e+03 6.2228e+00 --1.6800e+03 -1.8000e+03 6.1805e+00 --1.6800e+03 -1.7714e+03 6.1375e+00 --1.6800e+03 -1.7429e+03 6.0940e+00 --1.6800e+03 -1.7143e+03 6.0497e+00 --1.6800e+03 -1.6857e+03 6.0049e+00 --1.6800e+03 -1.6571e+03 5.9594e+00 --1.6800e+03 -1.6286e+03 5.9132e+00 --1.6800e+03 -1.6000e+03 5.8665e+00 --1.6800e+03 -1.5714e+03 5.8191e+00 --1.6800e+03 -1.5429e+03 5.7710e+00 --1.6800e+03 -1.5143e+03 5.7224e+00 --1.6800e+03 -1.4857e+03 5.6732e+00 --1.6800e+03 -1.4571e+03 5.6233e+00 --1.6800e+03 -1.4286e+03 5.5729e+00 --1.6800e+03 -1.4000e+03 5.5219e+00 --1.6800e+03 -1.3714e+03 5.4704e+00 --1.6800e+03 -1.3429e+03 5.4183e+00 --1.6800e+03 -1.3143e+03 5.3657e+00 --1.6800e+03 -1.2857e+03 5.3126e+00 --1.6800e+03 -1.2571e+03 5.2591e+00 --1.6800e+03 -1.2286e+03 5.2051e+00 --1.6800e+03 -1.2000e+03 5.1508e+00 --1.6800e+03 -1.1714e+03 5.0960e+00 --1.6800e+03 -1.1429e+03 5.0410e+00 --1.6800e+03 -1.1143e+03 4.9857e+00 --1.6800e+03 -1.0857e+03 4.9301e+00 --1.6800e+03 -1.0571e+03 4.8744e+00 --1.6800e+03 -1.0286e+03 4.8185e+00 --1.6800e+03 -1.0000e+03 4.7625e+00 --1.6800e+03 -9.7143e+02 4.7066e+00 --1.6800e+03 -9.4286e+02 4.6507e+00 --1.6800e+03 -9.1429e+02 4.5949e+00 --1.6800e+03 -8.8571e+02 4.5393e+00 --1.6800e+03 -8.5714e+02 4.4840e+00 --1.6800e+03 -8.2857e+02 4.4291e+00 --1.6800e+03 -8.0000e+02 4.3746e+00 --1.6800e+03 -7.7143e+02 4.3207e+00 --1.6800e+03 -7.4286e+02 4.2674e+00 --1.6800e+03 -7.1429e+02 4.2148e+00 --1.6800e+03 -6.8571e+02 4.1630e+00 --1.6800e+03 -6.5714e+02 4.1122e+00 --1.6800e+03 -6.2857e+02 4.0624e+00 --1.6800e+03 -6.0000e+02 4.0138e+00 --1.6800e+03 -5.7143e+02 3.9665e+00 --1.6800e+03 -5.4286e+02 3.9205e+00 --1.6800e+03 -5.1429e+02 3.8760e+00 --1.6800e+03 -4.8571e+02 3.8331e+00 --1.6800e+03 -4.5714e+02 3.7919e+00 --1.6800e+03 -4.2857e+02 3.7526e+00 --1.6800e+03 -4.0000e+02 3.7151e+00 --1.6800e+03 -3.7143e+02 3.6798e+00 --1.6800e+03 -3.4286e+02 3.6465e+00 --1.6800e+03 -3.1429e+02 3.6155e+00 --1.6800e+03 -2.8571e+02 3.5869e+00 --1.6800e+03 -2.5714e+02 3.5607e+00 --1.6800e+03 -2.2857e+02 3.5370e+00 --1.6800e+03 -2.0000e+02 3.5159e+00 --1.6800e+03 -1.7143e+02 3.4975e+00 --1.6800e+03 -1.4286e+02 3.4818e+00 --1.6800e+03 -1.1429e+02 3.4688e+00 --1.6800e+03 -8.5714e+01 3.4587e+00 --1.6800e+03 -5.7143e+01 3.4515e+00 --1.6800e+03 -2.8571e+01 3.4471e+00 --1.6800e+03 0.0000e+00 3.4457e+00 --1.6800e+03 2.8571e+01 3.4471e+00 --1.6800e+03 5.7143e+01 3.4515e+00 --1.6800e+03 8.5714e+01 3.4587e+00 --1.6800e+03 1.1429e+02 3.4688e+00 --1.6800e+03 1.4286e+02 3.4818e+00 --1.6800e+03 1.7143e+02 3.4975e+00 --1.6800e+03 2.0000e+02 3.5159e+00 --1.6800e+03 2.2857e+02 3.5370e+00 --1.6800e+03 2.5714e+02 3.5607e+00 --1.6800e+03 2.8571e+02 3.5869e+00 --1.6800e+03 3.1429e+02 3.6155e+00 --1.6800e+03 3.4286e+02 3.6465e+00 --1.6800e+03 3.7143e+02 3.6798e+00 --1.6800e+03 4.0000e+02 3.7151e+00 --1.6800e+03 4.2857e+02 3.7526e+00 --1.6800e+03 4.5714e+02 3.7919e+00 --1.6800e+03 4.8571e+02 3.8331e+00 --1.6800e+03 5.1429e+02 3.8760e+00 --1.6800e+03 5.4286e+02 3.9205e+00 --1.6800e+03 5.7143e+02 3.9665e+00 --1.6800e+03 6.0000e+02 4.0138e+00 --1.6800e+03 6.2857e+02 4.0624e+00 --1.6800e+03 6.5714e+02 4.1122e+00 --1.6800e+03 6.8571e+02 4.1630e+00 --1.6800e+03 7.1429e+02 4.2148e+00 --1.6800e+03 7.4286e+02 4.2674e+00 --1.6800e+03 7.7143e+02 4.3207e+00 --1.6800e+03 8.0000e+02 4.3746e+00 --1.6800e+03 8.2857e+02 4.4291e+00 --1.6800e+03 8.5714e+02 4.4840e+00 --1.6800e+03 8.8571e+02 4.5393e+00 --1.6800e+03 9.1429e+02 4.5949e+00 --1.6800e+03 9.4286e+02 4.6507e+00 --1.6800e+03 9.7143e+02 4.7066e+00 --1.6800e+03 1.0000e+03 4.7625e+00 --1.6800e+03 1.0286e+03 4.8185e+00 --1.6800e+03 1.0571e+03 4.8744e+00 --1.6800e+03 1.0857e+03 4.9301e+00 --1.6800e+03 1.1143e+03 4.9857e+00 --1.6800e+03 1.1429e+03 5.0410e+00 --1.6800e+03 1.1714e+03 5.0960e+00 --1.6800e+03 1.2000e+03 5.1508e+00 --1.6800e+03 1.2286e+03 5.2051e+00 --1.6800e+03 1.2571e+03 5.2591e+00 --1.6800e+03 1.2857e+03 5.3126e+00 --1.6800e+03 1.3143e+03 5.3657e+00 --1.6800e+03 1.3429e+03 5.4183e+00 --1.6800e+03 1.3714e+03 5.4704e+00 --1.6800e+03 1.4000e+03 5.5219e+00 --1.6800e+03 1.4286e+03 5.5729e+00 --1.6800e+03 1.4571e+03 5.6233e+00 --1.6800e+03 1.4857e+03 5.6732e+00 --1.6800e+03 1.5143e+03 5.7224e+00 --1.6800e+03 1.5429e+03 5.7710e+00 --1.6800e+03 1.5714e+03 5.8191e+00 --1.6800e+03 1.6000e+03 5.8665e+00 --1.6800e+03 1.6286e+03 5.9132e+00 --1.6800e+03 1.6571e+03 5.9594e+00 --1.6800e+03 1.6857e+03 6.0049e+00 --1.6800e+03 1.7143e+03 6.0497e+00 --1.6800e+03 1.7429e+03 6.0940e+00 --1.6800e+03 1.7714e+03 6.1375e+00 --1.6800e+03 1.8000e+03 6.1805e+00 --1.6800e+03 1.8286e+03 6.2228e+00 --1.6800e+03 1.8571e+03 6.2645e+00 --1.6800e+03 1.8857e+03 6.3055e+00 --1.6800e+03 1.9143e+03 6.3460e+00 --1.6800e+03 1.9429e+03 6.3858e+00 --1.6800e+03 1.9714e+03 6.4250e+00 --1.6800e+03 2.0000e+03 6.4636e+00 --1.6500e+03 -2.0000e+03 6.4297e+00 --1.6500e+03 -1.9714e+03 6.3900e+00 --1.6500e+03 -1.9429e+03 6.3498e+00 --1.6500e+03 -1.9143e+03 6.3088e+00 --1.6500e+03 -1.8857e+03 6.2672e+00 --1.6500e+03 -1.8571e+03 6.2250e+00 --1.6500e+03 -1.8286e+03 6.1821e+00 --1.6500e+03 -1.8000e+03 6.1385e+00 --1.6500e+03 -1.7714e+03 6.0942e+00 --1.6500e+03 -1.7429e+03 6.0492e+00 --1.6500e+03 -1.7143e+03 6.0036e+00 --1.6500e+03 -1.6857e+03 5.9572e+00 --1.6500e+03 -1.6571e+03 5.9102e+00 --1.6500e+03 -1.6286e+03 5.8625e+00 --1.6500e+03 -1.6000e+03 5.8141e+00 --1.6500e+03 -1.5714e+03 5.7651e+00 --1.6500e+03 -1.5429e+03 5.7153e+00 --1.6500e+03 -1.5143e+03 5.6649e+00 --1.6500e+03 -1.4857e+03 5.6138e+00 --1.6500e+03 -1.4571e+03 5.5620e+00 --1.6500e+03 -1.4286e+03 5.5096e+00 --1.6500e+03 -1.4000e+03 5.4566e+00 --1.6500e+03 -1.3714e+03 5.4030e+00 --1.6500e+03 -1.3429e+03 5.3488e+00 --1.6500e+03 -1.3143e+03 5.2940e+00 --1.6500e+03 -1.2857e+03 5.2386e+00 --1.6500e+03 -1.2571e+03 5.1827e+00 --1.6500e+03 -1.2286e+03 5.1263e+00 --1.6500e+03 -1.2000e+03 5.0695e+00 --1.6500e+03 -1.1714e+03 5.0122e+00 --1.6500e+03 -1.1429e+03 4.9546e+00 --1.6500e+03 -1.1143e+03 4.8966e+00 --1.6500e+03 -1.0857e+03 4.8383e+00 --1.6500e+03 -1.0571e+03 4.7797e+00 --1.6500e+03 -1.0286e+03 4.7210e+00 --1.6500e+03 -1.0000e+03 4.6622e+00 --1.6500e+03 -9.7143e+02 4.6032e+00 --1.6500e+03 -9.4286e+02 4.5443e+00 --1.6500e+03 -9.1429e+02 4.4855e+00 --1.6500e+03 -8.8571e+02 4.4268e+00 --1.6500e+03 -8.5714e+02 4.3684e+00 --1.6500e+03 -8.2857e+02 4.3103e+00 --1.6500e+03 -8.0000e+02 4.2527e+00 --1.6500e+03 -7.7143e+02 4.1955e+00 --1.6500e+03 -7.4286e+02 4.1390e+00 --1.6500e+03 -7.1429e+02 4.0832e+00 --1.6500e+03 -6.8571e+02 4.0282e+00 --1.6500e+03 -6.5714e+02 3.9742e+00 --1.6500e+03 -6.2857e+02 3.9212e+00 --1.6500e+03 -6.0000e+02 3.8695e+00 --1.6500e+03 -5.7143e+02 3.8190e+00 --1.6500e+03 -5.4286e+02 3.7699e+00 --1.6500e+03 -5.1429e+02 3.7224e+00 --1.6500e+03 -4.8571e+02 3.6766e+00 --1.6500e+03 -4.5714e+02 3.6326e+00 --1.6500e+03 -4.2857e+02 3.5905e+00 --1.6500e+03 -4.0000e+02 3.5504e+00 --1.6500e+03 -3.7143e+02 3.5125e+00 --1.6500e+03 -3.4286e+02 3.4769e+00 --1.6500e+03 -3.1429e+02 3.4437e+00 --1.6500e+03 -2.8571e+02 3.4130e+00 --1.6500e+03 -2.5714e+02 3.3848e+00 --1.6500e+03 -2.2857e+02 3.3594e+00 --1.6500e+03 -2.0000e+02 3.3367e+00 --1.6500e+03 -1.7143e+02 3.3169e+00 --1.6500e+03 -1.4286e+02 3.3000e+00 --1.6500e+03 -1.1429e+02 3.2861e+00 --1.6500e+03 -8.5714e+01 3.2753e+00 --1.6500e+03 -5.7143e+01 3.2675e+00 --1.6500e+03 -2.8571e+01 3.2628e+00 --1.6500e+03 0.0000e+00 3.2612e+00 --1.6500e+03 2.8571e+01 3.2628e+00 --1.6500e+03 5.7143e+01 3.2675e+00 --1.6500e+03 8.5714e+01 3.2753e+00 --1.6500e+03 1.1429e+02 3.2861e+00 --1.6500e+03 1.4286e+02 3.3000e+00 --1.6500e+03 1.7143e+02 3.3169e+00 --1.6500e+03 2.0000e+02 3.3367e+00 --1.6500e+03 2.2857e+02 3.3594e+00 --1.6500e+03 2.5714e+02 3.3848e+00 --1.6500e+03 2.8571e+02 3.4130e+00 --1.6500e+03 3.1429e+02 3.4437e+00 --1.6500e+03 3.4286e+02 3.4769e+00 --1.6500e+03 3.7143e+02 3.5125e+00 --1.6500e+03 4.0000e+02 3.5504e+00 --1.6500e+03 4.2857e+02 3.5905e+00 --1.6500e+03 4.5714e+02 3.6326e+00 --1.6500e+03 4.8571e+02 3.6766e+00 --1.6500e+03 5.1429e+02 3.7224e+00 --1.6500e+03 5.4286e+02 3.7699e+00 --1.6500e+03 5.7143e+02 3.8190e+00 --1.6500e+03 6.0000e+02 3.8695e+00 --1.6500e+03 6.2857e+02 3.9212e+00 --1.6500e+03 6.5714e+02 3.9742e+00 --1.6500e+03 6.8571e+02 4.0282e+00 --1.6500e+03 7.1429e+02 4.0832e+00 --1.6500e+03 7.4286e+02 4.1390e+00 --1.6500e+03 7.7143e+02 4.1955e+00 --1.6500e+03 8.0000e+02 4.2527e+00 --1.6500e+03 8.2857e+02 4.3103e+00 --1.6500e+03 8.5714e+02 4.3684e+00 --1.6500e+03 8.8571e+02 4.4268e+00 --1.6500e+03 9.1429e+02 4.4855e+00 --1.6500e+03 9.4286e+02 4.5443e+00 --1.6500e+03 9.7143e+02 4.6032e+00 --1.6500e+03 1.0000e+03 4.6622e+00 --1.6500e+03 1.0286e+03 4.7210e+00 --1.6500e+03 1.0571e+03 4.7797e+00 --1.6500e+03 1.0857e+03 4.8383e+00 --1.6500e+03 1.1143e+03 4.8966e+00 --1.6500e+03 1.1429e+03 4.9546e+00 --1.6500e+03 1.1714e+03 5.0122e+00 --1.6500e+03 1.2000e+03 5.0695e+00 --1.6500e+03 1.2286e+03 5.1263e+00 --1.6500e+03 1.2571e+03 5.1827e+00 --1.6500e+03 1.2857e+03 5.2386e+00 --1.6500e+03 1.3143e+03 5.2940e+00 --1.6500e+03 1.3429e+03 5.3488e+00 --1.6500e+03 1.3714e+03 5.4030e+00 --1.6500e+03 1.4000e+03 5.4566e+00 --1.6500e+03 1.4286e+03 5.5096e+00 --1.6500e+03 1.4571e+03 5.5620e+00 --1.6500e+03 1.4857e+03 5.6138e+00 --1.6500e+03 1.5143e+03 5.6649e+00 --1.6500e+03 1.5429e+03 5.7153e+00 --1.6500e+03 1.5714e+03 5.7651e+00 --1.6500e+03 1.6000e+03 5.8141e+00 --1.6500e+03 1.6286e+03 5.8625e+00 --1.6500e+03 1.6571e+03 5.9102e+00 --1.6500e+03 1.6857e+03 5.9572e+00 --1.6500e+03 1.7143e+03 6.0036e+00 --1.6500e+03 1.7429e+03 6.0492e+00 --1.6500e+03 1.7714e+03 6.0942e+00 --1.6500e+03 1.8000e+03 6.1385e+00 --1.6500e+03 1.8286e+03 6.1821e+00 --1.6500e+03 1.8571e+03 6.2250e+00 --1.6500e+03 1.8857e+03 6.2672e+00 --1.6500e+03 1.9143e+03 6.3088e+00 --1.6500e+03 1.9429e+03 6.3498e+00 --1.6500e+03 1.9714e+03 6.3900e+00 --1.6500e+03 2.0000e+03 6.4297e+00 --1.6200e+03 -2.0000e+03 6.3955e+00 --1.6200e+03 -1.9714e+03 6.3548e+00 --1.6200e+03 -1.9429e+03 6.3134e+00 --1.6200e+03 -1.9143e+03 6.2713e+00 --1.6200e+03 -1.8857e+03 6.2285e+00 --1.6200e+03 -1.8571e+03 6.1850e+00 --1.6200e+03 -1.8286e+03 6.1408e+00 --1.6200e+03 -1.8000e+03 6.0959e+00 --1.6200e+03 -1.7714e+03 6.0503e+00 --1.6200e+03 -1.7429e+03 6.0039e+00 --1.6200e+03 -1.7143e+03 5.9568e+00 --1.6200e+03 -1.6857e+03 5.9089e+00 --1.6200e+03 -1.6571e+03 5.8603e+00 --1.6200e+03 -1.6286e+03 5.8110e+00 --1.6200e+03 -1.6000e+03 5.7609e+00 --1.6200e+03 -1.5714e+03 5.7101e+00 --1.6200e+03 -1.5429e+03 5.6586e+00 --1.6200e+03 -1.5143e+03 5.6063e+00 --1.6200e+03 -1.4857e+03 5.5532e+00 --1.6200e+03 -1.4571e+03 5.4995e+00 --1.6200e+03 -1.4286e+03 5.4451e+00 --1.6200e+03 -1.4000e+03 5.3899e+00 --1.6200e+03 -1.3714e+03 5.3341e+00 --1.6200e+03 -1.3429e+03 5.2776e+00 --1.6200e+03 -1.3143e+03 5.2205e+00 --1.6200e+03 -1.2857e+03 5.1627e+00 --1.6200e+03 -1.2571e+03 5.1044e+00 --1.6200e+03 -1.2286e+03 5.0454e+00 --1.6200e+03 -1.2000e+03 4.9860e+00 --1.6200e+03 -1.1714e+03 4.9260e+00 --1.6200e+03 -1.1429e+03 4.8656e+00 --1.6200e+03 -1.1143e+03 4.8048e+00 --1.6200e+03 -1.0857e+03 4.7436e+00 --1.6200e+03 -1.0571e+03 4.6821e+00 --1.6200e+03 -1.0286e+03 4.6204e+00 --1.6200e+03 -1.0000e+03 4.5584e+00 --1.6200e+03 -9.7143e+02 4.4963e+00 --1.6200e+03 -9.4286e+02 4.4342e+00 --1.6200e+03 -9.1429e+02 4.3721e+00 --1.6200e+03 -8.8571e+02 4.3102e+00 --1.6200e+03 -8.5714e+02 4.2484e+00 --1.6200e+03 -8.2857e+02 4.1869e+00 --1.6200e+03 -8.0000e+02 4.1258e+00 --1.6200e+03 -7.7143e+02 4.0652e+00 --1.6200e+03 -7.4286e+02 4.0052e+00 --1.6200e+03 -7.1429e+02 3.9459e+00 --1.6200e+03 -6.8571e+02 3.8874e+00 --1.6200e+03 -6.5714e+02 3.8299e+00 --1.6200e+03 -6.2857e+02 3.7735e+00 --1.6200e+03 -6.0000e+02 3.7183e+00 --1.6200e+03 -5.7143e+02 3.6645e+00 --1.6200e+03 -5.4286e+02 3.6121e+00 --1.6200e+03 -5.1429e+02 3.5613e+00 --1.6200e+03 -4.8571e+02 3.5123e+00 --1.6200e+03 -4.5714e+02 3.4652e+00 --1.6200e+03 -4.2857e+02 3.4201e+00 --1.6200e+03 -4.0000e+02 3.3771e+00 --1.6200e+03 -3.7143e+02 3.3365e+00 --1.6200e+03 -3.4286e+02 3.2982e+00 --1.6200e+03 -3.1429e+02 3.2625e+00 --1.6200e+03 -2.8571e+02 3.2295e+00 --1.6200e+03 -2.5714e+02 3.1992e+00 --1.6200e+03 -2.2857e+02 3.1719e+00 --1.6200e+03 -2.0000e+02 3.1475e+00 --1.6200e+03 -1.7143e+02 3.1261e+00 --1.6200e+03 -1.4286e+02 3.1080e+00 --1.6200e+03 -1.1429e+02 3.0930e+00 --1.6200e+03 -8.5714e+01 3.0813e+00 --1.6200e+03 -5.7143e+01 3.0729e+00 --1.6200e+03 -2.8571e+01 3.0679e+00 --1.6200e+03 0.0000e+00 3.0662e+00 --1.6200e+03 2.8571e+01 3.0679e+00 --1.6200e+03 5.7143e+01 3.0729e+00 --1.6200e+03 8.5714e+01 3.0813e+00 --1.6200e+03 1.1429e+02 3.0930e+00 --1.6200e+03 1.4286e+02 3.1080e+00 --1.6200e+03 1.7143e+02 3.1261e+00 --1.6200e+03 2.0000e+02 3.1475e+00 --1.6200e+03 2.2857e+02 3.1719e+00 --1.6200e+03 2.5714e+02 3.1992e+00 --1.6200e+03 2.8571e+02 3.2295e+00 --1.6200e+03 3.1429e+02 3.2625e+00 --1.6200e+03 3.4286e+02 3.2982e+00 --1.6200e+03 3.7143e+02 3.3365e+00 --1.6200e+03 4.0000e+02 3.3771e+00 --1.6200e+03 4.2857e+02 3.4201e+00 --1.6200e+03 4.5714e+02 3.4652e+00 --1.6200e+03 4.8571e+02 3.5123e+00 --1.6200e+03 5.1429e+02 3.5613e+00 --1.6200e+03 5.4286e+02 3.6121e+00 --1.6200e+03 5.7143e+02 3.6645e+00 --1.6200e+03 6.0000e+02 3.7183e+00 --1.6200e+03 6.2857e+02 3.7735e+00 --1.6200e+03 6.5714e+02 3.8299e+00 --1.6200e+03 6.8571e+02 3.8874e+00 --1.6200e+03 7.1429e+02 3.9459e+00 --1.6200e+03 7.4286e+02 4.0052e+00 --1.6200e+03 7.7143e+02 4.0652e+00 --1.6200e+03 8.0000e+02 4.1258e+00 --1.6200e+03 8.2857e+02 4.1869e+00 --1.6200e+03 8.5714e+02 4.2484e+00 --1.6200e+03 8.8571e+02 4.3102e+00 --1.6200e+03 9.1429e+02 4.3721e+00 --1.6200e+03 9.4286e+02 4.4342e+00 --1.6200e+03 9.7143e+02 4.4963e+00 --1.6200e+03 1.0000e+03 4.5584e+00 --1.6200e+03 1.0286e+03 4.6204e+00 --1.6200e+03 1.0571e+03 4.6821e+00 --1.6200e+03 1.0857e+03 4.7436e+00 --1.6200e+03 1.1143e+03 4.8048e+00 --1.6200e+03 1.1429e+03 4.8656e+00 --1.6200e+03 1.1714e+03 4.9260e+00 --1.6200e+03 1.2000e+03 4.9860e+00 --1.6200e+03 1.2286e+03 5.0454e+00 --1.6200e+03 1.2571e+03 5.1044e+00 --1.6200e+03 1.2857e+03 5.1627e+00 --1.6200e+03 1.3143e+03 5.2205e+00 --1.6200e+03 1.3429e+03 5.2776e+00 --1.6200e+03 1.3714e+03 5.3341e+00 --1.6200e+03 1.4000e+03 5.3899e+00 --1.6200e+03 1.4286e+03 5.4451e+00 --1.6200e+03 1.4571e+03 5.4995e+00 --1.6200e+03 1.4857e+03 5.5532e+00 --1.6200e+03 1.5143e+03 5.6063e+00 --1.6200e+03 1.5429e+03 5.6586e+00 --1.6200e+03 1.5714e+03 5.7101e+00 --1.6200e+03 1.6000e+03 5.7609e+00 --1.6200e+03 1.6286e+03 5.8110e+00 --1.6200e+03 1.6571e+03 5.8603e+00 --1.6200e+03 1.6857e+03 5.9089e+00 --1.6200e+03 1.7143e+03 5.9568e+00 --1.6200e+03 1.7429e+03 6.0039e+00 --1.6200e+03 1.7714e+03 6.0503e+00 --1.6200e+03 1.8000e+03 6.0959e+00 --1.6200e+03 1.8286e+03 6.1408e+00 --1.6200e+03 1.8571e+03 6.1850e+00 --1.6200e+03 1.8857e+03 6.2285e+00 --1.6200e+03 1.9143e+03 6.2713e+00 --1.6200e+03 1.9429e+03 6.3134e+00 --1.6200e+03 1.9714e+03 6.3548e+00 --1.6200e+03 2.0000e+03 6.3955e+00 --1.5900e+03 -2.0000e+03 6.3610e+00 --1.5900e+03 -1.9714e+03 6.3192e+00 --1.5900e+03 -1.9429e+03 6.2767e+00 --1.5900e+03 -1.9143e+03 6.2334e+00 --1.5900e+03 -1.8857e+03 6.1894e+00 --1.5900e+03 -1.8571e+03 6.1447e+00 --1.5900e+03 -1.8286e+03 6.0992e+00 --1.5900e+03 -1.8000e+03 6.0529e+00 --1.5900e+03 -1.7714e+03 6.0058e+00 --1.5900e+03 -1.7429e+03 5.9580e+00 --1.5900e+03 -1.7143e+03 5.9093e+00 --1.5900e+03 -1.6857e+03 5.8599e+00 --1.5900e+03 -1.6571e+03 5.8097e+00 --1.5900e+03 -1.6286e+03 5.7587e+00 --1.5900e+03 -1.6000e+03 5.7069e+00 --1.5900e+03 -1.5714e+03 5.6542e+00 --1.5900e+03 -1.5429e+03 5.6008e+00 --1.5900e+03 -1.5143e+03 5.5466e+00 --1.5900e+03 -1.4857e+03 5.4916e+00 --1.5900e+03 -1.4571e+03 5.4357e+00 --1.5900e+03 -1.4286e+03 5.3791e+00 --1.5900e+03 -1.4000e+03 5.3218e+00 --1.5900e+03 -1.3714e+03 5.2637e+00 --1.5900e+03 -1.3429e+03 5.2048e+00 --1.5900e+03 -1.3143e+03 5.1452e+00 --1.5900e+03 -1.2857e+03 5.0850e+00 --1.5900e+03 -1.2571e+03 5.0240e+00 --1.5900e+03 -1.2286e+03 4.9624e+00 --1.5900e+03 -1.2000e+03 4.9002e+00 --1.5900e+03 -1.1714e+03 4.8374e+00 --1.5900e+03 -1.1429e+03 4.7741e+00 --1.5900e+03 -1.1143e+03 4.7103e+00 --1.5900e+03 -1.0857e+03 4.6460e+00 --1.5900e+03 -1.0571e+03 4.5813e+00 --1.5900e+03 -1.0286e+03 4.5164e+00 --1.5900e+03 -1.0000e+03 4.4511e+00 --1.5900e+03 -9.7143e+02 4.3857e+00 --1.5900e+03 -9.4286e+02 4.3201e+00 --1.5900e+03 -9.1429e+02 4.2546e+00 --1.5900e+03 -8.8571e+02 4.1890e+00 --1.5900e+03 -8.5714e+02 4.1236e+00 --1.5900e+03 -8.2857e+02 4.0585e+00 --1.5900e+03 -8.0000e+02 3.9937e+00 --1.5900e+03 -7.7143e+02 3.9294e+00 --1.5900e+03 -7.4286e+02 3.8656e+00 --1.5900e+03 -7.1429e+02 3.8026e+00 --1.5900e+03 -6.8571e+02 3.7403e+00 --1.5900e+03 -6.5714e+02 3.6791e+00 --1.5900e+03 -6.2857e+02 3.6189e+00 --1.5900e+03 -6.0000e+02 3.5600e+00 --1.5900e+03 -5.7143e+02 3.5024e+00 --1.5900e+03 -5.4286e+02 3.4464e+00 --1.5900e+03 -5.1429e+02 3.3920e+00 --1.5900e+03 -4.8571e+02 3.3395e+00 --1.5900e+03 -4.5714e+02 3.2890e+00 --1.5900e+03 -4.2857e+02 3.2406e+00 --1.5900e+03 -4.0000e+02 3.1945e+00 --1.5900e+03 -3.7143e+02 3.1508e+00 --1.5900e+03 -3.4286e+02 3.1097e+00 --1.5900e+03 -3.1429e+02 3.0713e+00 --1.5900e+03 -2.8571e+02 3.0357e+00 --1.5900e+03 -2.5714e+02 3.0031e+00 --1.5900e+03 -2.2857e+02 2.9736e+00 --1.5900e+03 -2.0000e+02 2.9473e+00 --1.5900e+03 -1.7143e+02 2.9243e+00 --1.5900e+03 -1.4286e+02 2.9047e+00 --1.5900e+03 -1.1429e+02 2.8885e+00 --1.5900e+03 -8.5714e+01 2.8759e+00 --1.5900e+03 -5.7143e+01 2.8668e+00 --1.5900e+03 -2.8571e+01 2.8614e+00 --1.5900e+03 0.0000e+00 2.8596e+00 --1.5900e+03 2.8571e+01 2.8614e+00 --1.5900e+03 5.7143e+01 2.8668e+00 --1.5900e+03 8.5714e+01 2.8759e+00 --1.5900e+03 1.1429e+02 2.8885e+00 --1.5900e+03 1.4286e+02 2.9047e+00 --1.5900e+03 1.7143e+02 2.9243e+00 --1.5900e+03 2.0000e+02 2.9473e+00 --1.5900e+03 2.2857e+02 2.9736e+00 --1.5900e+03 2.5714e+02 3.0031e+00 --1.5900e+03 2.8571e+02 3.0357e+00 --1.5900e+03 3.1429e+02 3.0713e+00 --1.5900e+03 3.4286e+02 3.1097e+00 --1.5900e+03 3.7143e+02 3.1508e+00 --1.5900e+03 4.0000e+02 3.1945e+00 --1.5900e+03 4.2857e+02 3.2406e+00 --1.5900e+03 4.5714e+02 3.2890e+00 --1.5900e+03 4.8571e+02 3.3395e+00 --1.5900e+03 5.1429e+02 3.3920e+00 --1.5900e+03 5.4286e+02 3.4464e+00 --1.5900e+03 5.7143e+02 3.5024e+00 --1.5900e+03 6.0000e+02 3.5600e+00 --1.5900e+03 6.2857e+02 3.6189e+00 --1.5900e+03 6.5714e+02 3.6791e+00 --1.5900e+03 6.8571e+02 3.7403e+00 --1.5900e+03 7.1429e+02 3.8026e+00 --1.5900e+03 7.4286e+02 3.8656e+00 --1.5900e+03 7.7143e+02 3.9294e+00 --1.5900e+03 8.0000e+02 3.9937e+00 --1.5900e+03 8.2857e+02 4.0585e+00 --1.5900e+03 8.5714e+02 4.1236e+00 --1.5900e+03 8.8571e+02 4.1890e+00 --1.5900e+03 9.1429e+02 4.2546e+00 --1.5900e+03 9.4286e+02 4.3201e+00 --1.5900e+03 9.7143e+02 4.3857e+00 --1.5900e+03 1.0000e+03 4.4511e+00 --1.5900e+03 1.0286e+03 4.5164e+00 --1.5900e+03 1.0571e+03 4.5813e+00 --1.5900e+03 1.0857e+03 4.6460e+00 --1.5900e+03 1.1143e+03 4.7103e+00 --1.5900e+03 1.1429e+03 4.7741e+00 --1.5900e+03 1.1714e+03 4.8374e+00 --1.5900e+03 1.2000e+03 4.9002e+00 --1.5900e+03 1.2286e+03 4.9624e+00 --1.5900e+03 1.2571e+03 5.0240e+00 --1.5900e+03 1.2857e+03 5.0850e+00 --1.5900e+03 1.3143e+03 5.1452e+00 --1.5900e+03 1.3429e+03 5.2048e+00 --1.5900e+03 1.3714e+03 5.2637e+00 --1.5900e+03 1.4000e+03 5.3218e+00 --1.5900e+03 1.4286e+03 5.3791e+00 --1.5900e+03 1.4571e+03 5.4357e+00 --1.5900e+03 1.4857e+03 5.4916e+00 --1.5900e+03 1.5143e+03 5.5466e+00 --1.5900e+03 1.5429e+03 5.6008e+00 --1.5900e+03 1.5714e+03 5.6542e+00 --1.5900e+03 1.6000e+03 5.7069e+00 --1.5900e+03 1.6286e+03 5.7587e+00 --1.5900e+03 1.6571e+03 5.8097e+00 --1.5900e+03 1.6857e+03 5.8599e+00 --1.5900e+03 1.7143e+03 5.9093e+00 --1.5900e+03 1.7429e+03 5.9580e+00 --1.5900e+03 1.7714e+03 6.0058e+00 --1.5900e+03 1.8000e+03 6.0529e+00 --1.5900e+03 1.8286e+03 6.0992e+00 --1.5900e+03 1.8571e+03 6.1447e+00 --1.5900e+03 1.8857e+03 6.1894e+00 --1.5900e+03 1.9143e+03 6.2334e+00 --1.5900e+03 1.9429e+03 6.2767e+00 --1.5900e+03 1.9714e+03 6.3192e+00 --1.5900e+03 2.0000e+03 6.3610e+00 --1.5600e+03 -2.0000e+03 6.3263e+00 --1.5600e+03 -1.9714e+03 6.2834e+00 --1.5600e+03 -1.9429e+03 6.2397e+00 --1.5600e+03 -1.9143e+03 6.1952e+00 --1.5600e+03 -1.8857e+03 6.1499e+00 --1.5600e+03 -1.8571e+03 6.1039e+00 --1.5600e+03 -1.8286e+03 6.0570e+00 --1.5600e+03 -1.8000e+03 6.0093e+00 --1.5600e+03 -1.7714e+03 5.9608e+00 --1.5600e+03 -1.7429e+03 5.9115e+00 --1.5600e+03 -1.7143e+03 5.8613e+00 --1.5600e+03 -1.6857e+03 5.8102e+00 --1.5600e+03 -1.6571e+03 5.7583e+00 --1.5600e+03 -1.6286e+03 5.7056e+00 --1.5600e+03 -1.6000e+03 5.6519e+00 --1.5600e+03 -1.5714e+03 5.5974e+00 --1.5600e+03 -1.5429e+03 5.5421e+00 --1.5600e+03 -1.5143e+03 5.4858e+00 --1.5600e+03 -1.4857e+03 5.4287e+00 --1.5600e+03 -1.4571e+03 5.3707e+00 --1.5600e+03 -1.4286e+03 5.3119e+00 --1.5600e+03 -1.4000e+03 5.2522e+00 --1.5600e+03 -1.3714e+03 5.1917e+00 --1.5600e+03 -1.3429e+03 5.1303e+00 --1.5600e+03 -1.3143e+03 5.0682e+00 --1.5600e+03 -1.2857e+03 5.0053e+00 --1.5600e+03 -1.2571e+03 4.9416e+00 --1.5600e+03 -1.2286e+03 4.8772e+00 --1.5600e+03 -1.2000e+03 4.8120e+00 --1.5600e+03 -1.1714e+03 4.7462e+00 --1.5600e+03 -1.1429e+03 4.6798e+00 --1.5600e+03 -1.1143e+03 4.6128e+00 --1.5600e+03 -1.0857e+03 4.5453e+00 --1.5600e+03 -1.0571e+03 4.4773e+00 --1.5600e+03 -1.0286e+03 4.4089e+00 --1.5600e+03 -1.0000e+03 4.3402e+00 --1.5600e+03 -9.7143e+02 4.2711e+00 --1.5600e+03 -9.4286e+02 4.2019e+00 --1.5600e+03 -9.1429e+02 4.1326e+00 --1.5600e+03 -8.8571e+02 4.0632e+00 --1.5600e+03 -8.5714e+02 3.9940e+00 --1.5600e+03 -8.2857e+02 3.9249e+00 --1.5600e+03 -8.0000e+02 3.8561e+00 --1.5600e+03 -7.7143e+02 3.7878e+00 --1.5600e+03 -7.4286e+02 3.7200e+00 --1.5600e+03 -7.1429e+02 3.6528e+00 --1.5600e+03 -6.8571e+02 3.5865e+00 --1.5600e+03 -6.5714e+02 3.5211e+00 --1.5600e+03 -6.2857e+02 3.4569e+00 --1.5600e+03 -6.0000e+02 3.3939e+00 --1.5600e+03 -5.7143e+02 3.3323e+00 --1.5600e+03 -5.4286e+02 3.2723e+00 --1.5600e+03 -5.1429e+02 3.2140e+00 --1.5600e+03 -4.8571e+02 3.1577e+00 --1.5600e+03 -4.5714e+02 3.1034e+00 --1.5600e+03 -4.2857e+02 3.0514e+00 --1.5600e+03 -4.0000e+02 3.0018e+00 --1.5600e+03 -3.7143e+02 2.9548e+00 --1.5600e+03 -3.4286e+02 2.9105e+00 --1.5600e+03 -3.1429e+02 2.8691e+00 --1.5600e+03 -2.8571e+02 2.8307e+00 --1.5600e+03 -2.5714e+02 2.7956e+00 --1.5600e+03 -2.2857e+02 2.7637e+00 --1.5600e+03 -2.0000e+02 2.7353e+00 --1.5600e+03 -1.7143e+02 2.7104e+00 --1.5600e+03 -1.4286e+02 2.6892e+00 --1.5600e+03 -1.1429e+02 2.6718e+00 --1.5600e+03 -8.5714e+01 2.6581e+00 --1.5600e+03 -5.7143e+01 2.6483e+00 --1.5600e+03 -2.8571e+01 2.6424e+00 --1.5600e+03 0.0000e+00 2.6404e+00 --1.5600e+03 2.8571e+01 2.6424e+00 --1.5600e+03 5.7143e+01 2.6483e+00 --1.5600e+03 8.5714e+01 2.6581e+00 --1.5600e+03 1.1429e+02 2.6718e+00 --1.5600e+03 1.4286e+02 2.6892e+00 --1.5600e+03 1.7143e+02 2.7104e+00 --1.5600e+03 2.0000e+02 2.7353e+00 --1.5600e+03 2.2857e+02 2.7637e+00 --1.5600e+03 2.5714e+02 2.7956e+00 --1.5600e+03 2.8571e+02 2.8307e+00 --1.5600e+03 3.1429e+02 2.8691e+00 --1.5600e+03 3.4286e+02 2.9105e+00 --1.5600e+03 3.7143e+02 2.9548e+00 --1.5600e+03 4.0000e+02 3.0018e+00 --1.5600e+03 4.2857e+02 3.0514e+00 --1.5600e+03 4.5714e+02 3.1034e+00 --1.5600e+03 4.8571e+02 3.1577e+00 --1.5600e+03 5.1429e+02 3.2140e+00 --1.5600e+03 5.4286e+02 3.2723e+00 --1.5600e+03 5.7143e+02 3.3323e+00 --1.5600e+03 6.0000e+02 3.3939e+00 --1.5600e+03 6.2857e+02 3.4569e+00 --1.5600e+03 6.5714e+02 3.5211e+00 --1.5600e+03 6.8571e+02 3.5865e+00 --1.5600e+03 7.1429e+02 3.6528e+00 --1.5600e+03 7.4286e+02 3.7200e+00 --1.5600e+03 7.7143e+02 3.7878e+00 --1.5600e+03 8.0000e+02 3.8561e+00 --1.5600e+03 8.2857e+02 3.9249e+00 --1.5600e+03 8.5714e+02 3.9940e+00 --1.5600e+03 8.8571e+02 4.0632e+00 --1.5600e+03 9.1429e+02 4.1326e+00 --1.5600e+03 9.4286e+02 4.2019e+00 --1.5600e+03 9.7143e+02 4.2711e+00 --1.5600e+03 1.0000e+03 4.3402e+00 --1.5600e+03 1.0286e+03 4.4089e+00 --1.5600e+03 1.0571e+03 4.4773e+00 --1.5600e+03 1.0857e+03 4.5453e+00 --1.5600e+03 1.1143e+03 4.6128e+00 --1.5600e+03 1.1429e+03 4.6798e+00 --1.5600e+03 1.1714e+03 4.7462e+00 --1.5600e+03 1.2000e+03 4.8120e+00 --1.5600e+03 1.2286e+03 4.8772e+00 --1.5600e+03 1.2571e+03 4.9416e+00 --1.5600e+03 1.2857e+03 5.0053e+00 --1.5600e+03 1.3143e+03 5.0682e+00 --1.5600e+03 1.3429e+03 5.1303e+00 --1.5600e+03 1.3714e+03 5.1917e+00 --1.5600e+03 1.4000e+03 5.2522e+00 --1.5600e+03 1.4286e+03 5.3119e+00 --1.5600e+03 1.4571e+03 5.3707e+00 --1.5600e+03 1.4857e+03 5.4287e+00 --1.5600e+03 1.5143e+03 5.4858e+00 --1.5600e+03 1.5429e+03 5.5421e+00 --1.5600e+03 1.5714e+03 5.5974e+00 --1.5600e+03 1.6000e+03 5.6519e+00 --1.5600e+03 1.6286e+03 5.7056e+00 --1.5600e+03 1.6571e+03 5.7583e+00 --1.5600e+03 1.6857e+03 5.8102e+00 --1.5600e+03 1.7143e+03 5.8613e+00 --1.5600e+03 1.7429e+03 5.9115e+00 --1.5600e+03 1.7714e+03 5.9608e+00 --1.5600e+03 1.8000e+03 6.0093e+00 --1.5600e+03 1.8286e+03 6.0570e+00 --1.5600e+03 1.8571e+03 6.1039e+00 --1.5600e+03 1.8857e+03 6.1499e+00 --1.5600e+03 1.9143e+03 6.1952e+00 --1.5600e+03 1.9429e+03 6.2397e+00 --1.5600e+03 1.9714e+03 6.2834e+00 --1.5600e+03 2.0000e+03 6.3263e+00 --1.5300e+03 -2.0000e+03 6.2913e+00 --1.5300e+03 -1.9714e+03 6.2472e+00 --1.5300e+03 -1.9429e+03 6.2024e+00 --1.5300e+03 -1.9143e+03 6.1567e+00 --1.5300e+03 -1.8857e+03 6.1101e+00 --1.5300e+03 -1.8571e+03 6.0627e+00 --1.5300e+03 -1.8286e+03 6.0145e+00 --1.5300e+03 -1.8000e+03 5.9653e+00 --1.5300e+03 -1.7714e+03 5.9153e+00 --1.5300e+03 -1.7429e+03 5.8644e+00 --1.5300e+03 -1.7143e+03 5.8126e+00 --1.5300e+03 -1.6857e+03 5.7599e+00 --1.5300e+03 -1.6571e+03 5.7062e+00 --1.5300e+03 -1.6286e+03 5.6517e+00 --1.5300e+03 -1.6000e+03 5.5962e+00 --1.5300e+03 -1.5714e+03 5.5397e+00 --1.5300e+03 -1.5429e+03 5.4823e+00 --1.5300e+03 -1.5143e+03 5.4240e+00 --1.5300e+03 -1.4857e+03 5.3647e+00 --1.5300e+03 -1.4571e+03 5.3044e+00 --1.5300e+03 -1.4286e+03 5.2432e+00 --1.5300e+03 -1.4000e+03 5.1811e+00 --1.5300e+03 -1.3714e+03 5.1181e+00 --1.5300e+03 -1.3429e+03 5.0542e+00 --1.5300e+03 -1.3143e+03 4.9893e+00 --1.5300e+03 -1.2857e+03 4.9236e+00 --1.5300e+03 -1.2571e+03 4.8570e+00 --1.5300e+03 -1.2286e+03 4.7896e+00 --1.5300e+03 -1.2000e+03 4.7215e+00 --1.5300e+03 -1.1714e+03 4.6525e+00 --1.5300e+03 -1.1429e+03 4.5828e+00 --1.5300e+03 -1.1143e+03 4.5125e+00 --1.5300e+03 -1.0857e+03 4.4415e+00 --1.5300e+03 -1.0571e+03 4.3699e+00 --1.5300e+03 -1.0286e+03 4.2979e+00 --1.5300e+03 -1.0000e+03 4.2254e+00 --1.5300e+03 -9.7143e+02 4.1525e+00 --1.5300e+03 -9.4286e+02 4.0793e+00 --1.5300e+03 -9.1429e+02 4.0060e+00 --1.5300e+03 -8.8571e+02 3.9326e+00 --1.5300e+03 -8.5714e+02 3.8591e+00 --1.5300e+03 -8.2857e+02 3.7858e+00 --1.5300e+03 -8.0000e+02 3.7127e+00 --1.5300e+03 -7.7143e+02 3.6400e+00 --1.5300e+03 -7.4286e+02 3.5678e+00 --1.5300e+03 -7.1429e+02 3.4963e+00 --1.5300e+03 -6.8571e+02 3.4255e+00 --1.5300e+03 -6.5714e+02 3.3557e+00 --1.5300e+03 -6.2857e+02 3.2870e+00 --1.5300e+03 -6.0000e+02 3.2195e+00 --1.5300e+03 -5.7143e+02 3.1535e+00 --1.5300e+03 -5.4286e+02 3.0892e+00 --1.5300e+03 -5.1429e+02 3.0266e+00 --1.5300e+03 -4.8571e+02 2.9661e+00 --1.5300e+03 -4.5714e+02 2.9077e+00 --1.5300e+03 -4.2857e+02 2.8517e+00 --1.5300e+03 -4.0000e+02 2.7983e+00 --1.5300e+03 -3.7143e+02 2.7475e+00 --1.5300e+03 -3.4286e+02 2.6997e+00 --1.5300e+03 -3.1429e+02 2.6550e+00 --1.5300e+03 -2.8571e+02 2.6136e+00 --1.5300e+03 -2.5714e+02 2.5756e+00 --1.5300e+03 -2.2857e+02 2.5411e+00 --1.5300e+03 -2.0000e+02 2.5103e+00 --1.5300e+03 -1.7143e+02 2.4834e+00 --1.5300e+03 -1.4286e+02 2.4604e+00 --1.5300e+03 -1.1429e+02 2.4415e+00 --1.5300e+03 -8.5714e+01 2.4267e+00 --1.5300e+03 -5.7143e+01 2.4160e+00 --1.5300e+03 -2.8571e+01 2.4096e+00 --1.5300e+03 0.0000e+00 2.4075e+00 --1.5300e+03 2.8571e+01 2.4096e+00 --1.5300e+03 5.7143e+01 2.4160e+00 --1.5300e+03 8.5714e+01 2.4267e+00 --1.5300e+03 1.1429e+02 2.4415e+00 --1.5300e+03 1.4286e+02 2.4604e+00 --1.5300e+03 1.7143e+02 2.4834e+00 --1.5300e+03 2.0000e+02 2.5103e+00 --1.5300e+03 2.2857e+02 2.5411e+00 --1.5300e+03 2.5714e+02 2.5756e+00 --1.5300e+03 2.8571e+02 2.6136e+00 --1.5300e+03 3.1429e+02 2.6550e+00 --1.5300e+03 3.4286e+02 2.6997e+00 --1.5300e+03 3.7143e+02 2.7475e+00 --1.5300e+03 4.0000e+02 2.7983e+00 --1.5300e+03 4.2857e+02 2.8517e+00 --1.5300e+03 4.5714e+02 2.9077e+00 --1.5300e+03 4.8571e+02 2.9661e+00 --1.5300e+03 5.1429e+02 3.0266e+00 --1.5300e+03 5.4286e+02 3.0892e+00 --1.5300e+03 5.7143e+02 3.1535e+00 --1.5300e+03 6.0000e+02 3.2195e+00 --1.5300e+03 6.2857e+02 3.2870e+00 --1.5300e+03 6.5714e+02 3.3557e+00 --1.5300e+03 6.8571e+02 3.4255e+00 --1.5300e+03 7.1429e+02 3.4963e+00 --1.5300e+03 7.4286e+02 3.5678e+00 --1.5300e+03 7.7143e+02 3.6400e+00 --1.5300e+03 8.0000e+02 3.7127e+00 --1.5300e+03 8.2857e+02 3.7858e+00 --1.5300e+03 8.5714e+02 3.8591e+00 --1.5300e+03 8.8571e+02 3.9326e+00 --1.5300e+03 9.1429e+02 4.0060e+00 --1.5300e+03 9.4286e+02 4.0793e+00 --1.5300e+03 9.7143e+02 4.1525e+00 --1.5300e+03 1.0000e+03 4.2254e+00 --1.5300e+03 1.0286e+03 4.2979e+00 --1.5300e+03 1.0571e+03 4.3699e+00 --1.5300e+03 1.0857e+03 4.4415e+00 --1.5300e+03 1.1143e+03 4.5125e+00 --1.5300e+03 1.1429e+03 4.5828e+00 --1.5300e+03 1.1714e+03 4.6525e+00 --1.5300e+03 1.2000e+03 4.7215e+00 --1.5300e+03 1.2286e+03 4.7896e+00 --1.5300e+03 1.2571e+03 4.8570e+00 --1.5300e+03 1.2857e+03 4.9236e+00 --1.5300e+03 1.3143e+03 4.9893e+00 --1.5300e+03 1.3429e+03 5.0542e+00 --1.5300e+03 1.3714e+03 5.1181e+00 --1.5300e+03 1.4000e+03 5.1811e+00 --1.5300e+03 1.4286e+03 5.2432e+00 --1.5300e+03 1.4571e+03 5.3044e+00 --1.5300e+03 1.4857e+03 5.3647e+00 --1.5300e+03 1.5143e+03 5.4240e+00 --1.5300e+03 1.5429e+03 5.4823e+00 --1.5300e+03 1.5714e+03 5.5397e+00 --1.5300e+03 1.6000e+03 5.5962e+00 --1.5300e+03 1.6286e+03 5.6517e+00 --1.5300e+03 1.6571e+03 5.7062e+00 --1.5300e+03 1.6857e+03 5.7599e+00 --1.5300e+03 1.7143e+03 5.8126e+00 --1.5300e+03 1.7429e+03 5.8644e+00 --1.5300e+03 1.7714e+03 5.9153e+00 --1.5300e+03 1.8000e+03 5.9653e+00 --1.5300e+03 1.8286e+03 6.0145e+00 --1.5300e+03 1.8571e+03 6.0627e+00 --1.5300e+03 1.8857e+03 6.1101e+00 --1.5300e+03 1.9143e+03 6.1567e+00 --1.5300e+03 1.9429e+03 6.2024e+00 --1.5300e+03 1.9714e+03 6.2472e+00 --1.5300e+03 2.0000e+03 6.2913e+00 --1.5000e+03 -2.0000e+03 6.2561e+00 --1.5000e+03 -1.9714e+03 6.2109e+00 --1.5000e+03 -1.9429e+03 6.1648e+00 --1.5000e+03 -1.9143e+03 6.1178e+00 --1.5000e+03 -1.8857e+03 6.0699e+00 --1.5000e+03 -1.8571e+03 6.0212e+00 --1.5000e+03 -1.8286e+03 5.9715e+00 --1.5000e+03 -1.8000e+03 5.9209e+00 --1.5000e+03 -1.7714e+03 5.8693e+00 --1.5000e+03 -1.7429e+03 5.8168e+00 --1.5000e+03 -1.7143e+03 5.7633e+00 --1.5000e+03 -1.6857e+03 5.7089e+00 --1.5000e+03 -1.6571e+03 5.6534e+00 --1.5000e+03 -1.6286e+03 5.5970e+00 --1.5000e+03 -1.6000e+03 5.5395e+00 --1.5000e+03 -1.5714e+03 5.4810e+00 --1.5000e+03 -1.5429e+03 5.4215e+00 --1.5000e+03 -1.5143e+03 5.3610e+00 --1.5000e+03 -1.4857e+03 5.2994e+00 --1.5000e+03 -1.4571e+03 5.2369e+00 --1.5000e+03 -1.4286e+03 5.1732e+00 --1.5000e+03 -1.4000e+03 5.1086e+00 --1.5000e+03 -1.3714e+03 5.0429e+00 --1.5000e+03 -1.3429e+03 4.9763e+00 --1.5000e+03 -1.3143e+03 4.9086e+00 --1.5000e+03 -1.2857e+03 4.8400e+00 --1.5000e+03 -1.2571e+03 4.7704e+00 --1.5000e+03 -1.2286e+03 4.6998e+00 --1.5000e+03 -1.2000e+03 4.6284e+00 --1.5000e+03 -1.1714e+03 4.5561e+00 --1.5000e+03 -1.1429e+03 4.4829e+00 --1.5000e+03 -1.1143e+03 4.4090e+00 --1.5000e+03 -1.0857e+03 4.3343e+00 --1.5000e+03 -1.0571e+03 4.2590e+00 --1.5000e+03 -1.0286e+03 4.1830e+00 --1.5000e+03 -1.0000e+03 4.1065e+00 --1.5000e+03 -9.7143e+02 4.0296e+00 --1.5000e+03 -9.4286e+02 3.9522e+00 --1.5000e+03 -9.1429e+02 3.8745e+00 --1.5000e+03 -8.8571e+02 3.7967e+00 --1.5000e+03 -8.5714e+02 3.7188e+00 --1.5000e+03 -8.2857e+02 3.6409e+00 --1.5000e+03 -8.0000e+02 3.5632e+00 --1.5000e+03 -7.7143e+02 3.4858e+00 --1.5000e+03 -7.4286e+02 3.4088e+00 --1.5000e+03 -7.1429e+02 3.3324e+00 --1.5000e+03 -6.8571e+02 3.2568e+00 --1.5000e+03 -6.5714e+02 3.1821e+00 --1.5000e+03 -6.2857e+02 3.1086e+00 --1.5000e+03 -6.0000e+02 3.0363e+00 --1.5000e+03 -5.7143e+02 2.9655e+00 --1.5000e+03 -5.4286e+02 2.8963e+00 --1.5000e+03 -5.1429e+02 2.8291e+00 --1.5000e+03 -4.8571e+02 2.7639e+00 --1.5000e+03 -4.5714e+02 2.7010e+00 --1.5000e+03 -4.2857e+02 2.6406e+00 --1.5000e+03 -4.0000e+02 2.5829e+00 --1.5000e+03 -3.7143e+02 2.5281e+00 --1.5000e+03 -3.4286e+02 2.4764e+00 --1.5000e+03 -3.1429e+02 2.4280e+00 --1.5000e+03 -2.8571e+02 2.3832e+00 --1.5000e+03 -2.5714e+02 2.3419e+00 --1.5000e+03 -2.2857e+02 2.3046e+00 --1.5000e+03 -2.0000e+02 2.2712e+00 --1.5000e+03 -1.7143e+02 2.2420e+00 --1.5000e+03 -1.4286e+02 2.2170e+00 --1.5000e+03 -1.1429e+02 2.1965e+00 --1.5000e+03 -8.5714e+01 2.1804e+00 --1.5000e+03 -5.7143e+01 2.1688e+00 --1.5000e+03 -2.8571e+01 2.1619e+00 --1.5000e+03 0.0000e+00 2.1595e+00 --1.5000e+03 2.8571e+01 2.1619e+00 --1.5000e+03 5.7143e+01 2.1688e+00 --1.5000e+03 8.5714e+01 2.1804e+00 --1.5000e+03 1.1429e+02 2.1965e+00 --1.5000e+03 1.4286e+02 2.2170e+00 --1.5000e+03 1.7143e+02 2.2420e+00 --1.5000e+03 2.0000e+02 2.2712e+00 --1.5000e+03 2.2857e+02 2.3046e+00 --1.5000e+03 2.5714e+02 2.3419e+00 --1.5000e+03 2.8571e+02 2.3832e+00 --1.5000e+03 3.1429e+02 2.4280e+00 --1.5000e+03 3.4286e+02 2.4764e+00 --1.5000e+03 3.7143e+02 2.5281e+00 --1.5000e+03 4.0000e+02 2.5829e+00 --1.5000e+03 4.2857e+02 2.6406e+00 --1.5000e+03 4.5714e+02 2.7010e+00 --1.5000e+03 4.8571e+02 2.7639e+00 --1.5000e+03 5.1429e+02 2.8291e+00 --1.5000e+03 5.4286e+02 2.8963e+00 --1.5000e+03 5.7143e+02 2.9655e+00 --1.5000e+03 6.0000e+02 3.0363e+00 --1.5000e+03 6.2857e+02 3.1086e+00 --1.5000e+03 6.5714e+02 3.1821e+00 --1.5000e+03 6.8571e+02 3.2568e+00 --1.5000e+03 7.1429e+02 3.3324e+00 --1.5000e+03 7.4286e+02 3.4088e+00 --1.5000e+03 7.7143e+02 3.4858e+00 --1.5000e+03 8.0000e+02 3.5632e+00 --1.5000e+03 8.2857e+02 3.6409e+00 --1.5000e+03 8.5714e+02 3.7188e+00 --1.5000e+03 8.8571e+02 3.7967e+00 --1.5000e+03 9.1429e+02 3.8745e+00 --1.5000e+03 9.4286e+02 3.9522e+00 --1.5000e+03 9.7143e+02 4.0296e+00 --1.5000e+03 1.0000e+03 4.1065e+00 --1.5000e+03 1.0286e+03 4.1830e+00 --1.5000e+03 1.0571e+03 4.2590e+00 --1.5000e+03 1.0857e+03 4.3343e+00 --1.5000e+03 1.1143e+03 4.4090e+00 --1.5000e+03 1.1429e+03 4.4829e+00 --1.5000e+03 1.1714e+03 4.5561e+00 --1.5000e+03 1.2000e+03 4.6284e+00 --1.5000e+03 1.2286e+03 4.6998e+00 --1.5000e+03 1.2571e+03 4.7704e+00 --1.5000e+03 1.2857e+03 4.8400e+00 --1.5000e+03 1.3143e+03 4.9086e+00 --1.5000e+03 1.3429e+03 4.9763e+00 --1.5000e+03 1.3714e+03 5.0429e+00 --1.5000e+03 1.4000e+03 5.1086e+00 --1.5000e+03 1.4286e+03 5.1732e+00 --1.5000e+03 1.4571e+03 5.2369e+00 --1.5000e+03 1.4857e+03 5.2994e+00 --1.5000e+03 1.5143e+03 5.3610e+00 --1.5000e+03 1.5429e+03 5.4215e+00 --1.5000e+03 1.5714e+03 5.4810e+00 --1.5000e+03 1.6000e+03 5.5395e+00 --1.5000e+03 1.6286e+03 5.5970e+00 --1.5000e+03 1.6571e+03 5.6534e+00 --1.5000e+03 1.6857e+03 5.7089e+00 --1.5000e+03 1.7143e+03 5.7633e+00 --1.5000e+03 1.7429e+03 5.8168e+00 --1.5000e+03 1.7714e+03 5.8693e+00 --1.5000e+03 1.8000e+03 5.9209e+00 --1.5000e+03 1.8286e+03 5.9715e+00 --1.5000e+03 1.8571e+03 6.0212e+00 --1.5000e+03 1.8857e+03 6.0699e+00 --1.5000e+03 1.9143e+03 6.1178e+00 --1.5000e+03 1.9429e+03 6.1648e+00 --1.5000e+03 1.9714e+03 6.2109e+00 --1.5000e+03 2.0000e+03 6.2561e+00 --1.4700e+03 -2.0000e+03 6.2207e+00 --1.4700e+03 -1.9714e+03 6.1743e+00 --1.4700e+03 -1.9429e+03 6.1269e+00 --1.4700e+03 -1.9143e+03 6.0786e+00 --1.4700e+03 -1.8857e+03 6.0294e+00 --1.4700e+03 -1.8571e+03 5.9792e+00 --1.4700e+03 -1.8286e+03 5.9281e+00 --1.4700e+03 -1.8000e+03 5.8760e+00 --1.4700e+03 -1.7714e+03 5.8228e+00 --1.4700e+03 -1.7429e+03 5.7687e+00 --1.4700e+03 -1.7143e+03 5.7135e+00 --1.4700e+03 -1.6857e+03 5.6572e+00 --1.4700e+03 -1.6571e+03 5.5999e+00 --1.4700e+03 -1.6286e+03 5.5415e+00 --1.4700e+03 -1.6000e+03 5.4820e+00 --1.4700e+03 -1.5714e+03 5.4215e+00 --1.4700e+03 -1.5429e+03 5.3598e+00 --1.4700e+03 -1.5143e+03 5.2970e+00 --1.4700e+03 -1.4857e+03 5.2330e+00 --1.4700e+03 -1.4571e+03 5.1680e+00 --1.4700e+03 -1.4286e+03 5.1018e+00 --1.4700e+03 -1.4000e+03 5.0345e+00 --1.4700e+03 -1.3714e+03 4.9661e+00 --1.4700e+03 -1.3429e+03 4.8966e+00 --1.4700e+03 -1.3143e+03 4.8260e+00 --1.4700e+03 -1.2857e+03 4.7542e+00 --1.4700e+03 -1.2571e+03 4.6814e+00 --1.4700e+03 -1.2286e+03 4.6076e+00 --1.4700e+03 -1.2000e+03 4.5327e+00 --1.4700e+03 -1.1714e+03 4.4569e+00 --1.4700e+03 -1.1429e+03 4.3801e+00 --1.4700e+03 -1.1143e+03 4.3024e+00 --1.4700e+03 -1.0857e+03 4.2238e+00 --1.4700e+03 -1.0571e+03 4.1444e+00 --1.4700e+03 -1.0286e+03 4.0643e+00 --1.4700e+03 -1.0000e+03 3.9835e+00 --1.4700e+03 -9.7143e+02 3.9021e+00 --1.4700e+03 -9.4286e+02 3.8203e+00 --1.4700e+03 -9.1429e+02 3.7380e+00 --1.4700e+03 -8.8571e+02 3.6554e+00 --1.4700e+03 -8.5714e+02 3.5727e+00 --1.4700e+03 -8.2857e+02 3.4899e+00 --1.4700e+03 -8.0000e+02 3.4071e+00 --1.4700e+03 -7.7143e+02 3.3246e+00 --1.4700e+03 -7.4286e+02 3.2425e+00 --1.4700e+03 -7.1429e+02 3.1609e+00 --1.4700e+03 -6.8571e+02 3.0800e+00 --1.4700e+03 -6.5714e+02 3.0000e+00 --1.4700e+03 -6.2857e+02 2.9211e+00 --1.4700e+03 -6.0000e+02 2.8435e+00 --1.4700e+03 -5.7143e+02 2.7674e+00 --1.4700e+03 -5.4286e+02 2.6930e+00 --1.4700e+03 -5.1429e+02 2.6205e+00 --1.4700e+03 -4.8571e+02 2.5503e+00 --1.4700e+03 -4.5714e+02 2.4824e+00 --1.4700e+03 -4.2857e+02 2.4171e+00 --1.4700e+03 -4.0000e+02 2.3547e+00 --1.4700e+03 -3.7143e+02 2.2954e+00 --1.4700e+03 -3.4286e+02 2.2394e+00 --1.4700e+03 -3.1429e+02 2.1869e+00 --1.4700e+03 -2.8571e+02 2.1382e+00 --1.4700e+03 -2.5714e+02 2.0935e+00 --1.4700e+03 -2.2857e+02 2.0529e+00 --1.4700e+03 -2.0000e+02 2.0166e+00 --1.4700e+03 -1.7143e+02 1.9848e+00 --1.4700e+03 -1.4286e+02 1.9576e+00 --1.4700e+03 -1.1429e+02 1.9352e+00 --1.4700e+03 -8.5714e+01 1.9177e+00 --1.4700e+03 -5.7143e+01 1.9051e+00 --1.4700e+03 -2.8571e+01 1.8975e+00 --1.4700e+03 0.0000e+00 1.8950e+00 --1.4700e+03 2.8571e+01 1.8975e+00 --1.4700e+03 5.7143e+01 1.9051e+00 --1.4700e+03 8.5714e+01 1.9177e+00 --1.4700e+03 1.1429e+02 1.9352e+00 --1.4700e+03 1.4286e+02 1.9576e+00 --1.4700e+03 1.7143e+02 1.9848e+00 --1.4700e+03 2.0000e+02 2.0166e+00 --1.4700e+03 2.2857e+02 2.0529e+00 --1.4700e+03 2.5714e+02 2.0935e+00 --1.4700e+03 2.8571e+02 2.1382e+00 --1.4700e+03 3.1429e+02 2.1869e+00 --1.4700e+03 3.4286e+02 2.2394e+00 --1.4700e+03 3.7143e+02 2.2954e+00 --1.4700e+03 4.0000e+02 2.3547e+00 --1.4700e+03 4.2857e+02 2.4171e+00 --1.4700e+03 4.5714e+02 2.4824e+00 --1.4700e+03 4.8571e+02 2.5503e+00 --1.4700e+03 5.1429e+02 2.6205e+00 --1.4700e+03 5.4286e+02 2.6930e+00 --1.4700e+03 5.7143e+02 2.7674e+00 --1.4700e+03 6.0000e+02 2.8435e+00 --1.4700e+03 6.2857e+02 2.9211e+00 --1.4700e+03 6.5714e+02 3.0000e+00 --1.4700e+03 6.8571e+02 3.0800e+00 --1.4700e+03 7.1429e+02 3.1609e+00 --1.4700e+03 7.4286e+02 3.2425e+00 --1.4700e+03 7.7143e+02 3.3246e+00 --1.4700e+03 8.0000e+02 3.4071e+00 --1.4700e+03 8.2857e+02 3.4899e+00 --1.4700e+03 8.5714e+02 3.5727e+00 --1.4700e+03 8.8571e+02 3.6554e+00 --1.4700e+03 9.1429e+02 3.7380e+00 --1.4700e+03 9.4286e+02 3.8203e+00 --1.4700e+03 9.7143e+02 3.9021e+00 --1.4700e+03 1.0000e+03 3.9835e+00 --1.4700e+03 1.0286e+03 4.0643e+00 --1.4700e+03 1.0571e+03 4.1444e+00 --1.4700e+03 1.0857e+03 4.2238e+00 --1.4700e+03 1.1143e+03 4.3024e+00 --1.4700e+03 1.1429e+03 4.3801e+00 --1.4700e+03 1.1714e+03 4.4569e+00 --1.4700e+03 1.2000e+03 4.5327e+00 --1.4700e+03 1.2286e+03 4.6076e+00 --1.4700e+03 1.2571e+03 4.6814e+00 --1.4700e+03 1.2857e+03 4.7542e+00 --1.4700e+03 1.3143e+03 4.8260e+00 --1.4700e+03 1.3429e+03 4.8966e+00 --1.4700e+03 1.3714e+03 4.9661e+00 --1.4700e+03 1.4000e+03 5.0345e+00 --1.4700e+03 1.4286e+03 5.1018e+00 --1.4700e+03 1.4571e+03 5.1680e+00 --1.4700e+03 1.4857e+03 5.2330e+00 --1.4700e+03 1.5143e+03 5.2970e+00 --1.4700e+03 1.5429e+03 5.3598e+00 --1.4700e+03 1.5714e+03 5.4215e+00 --1.4700e+03 1.6000e+03 5.4820e+00 --1.4700e+03 1.6286e+03 5.5415e+00 --1.4700e+03 1.6571e+03 5.5999e+00 --1.4700e+03 1.6857e+03 5.6572e+00 --1.4700e+03 1.7143e+03 5.7135e+00 --1.4700e+03 1.7429e+03 5.7687e+00 --1.4700e+03 1.7714e+03 5.8228e+00 --1.4700e+03 1.8000e+03 5.8760e+00 --1.4700e+03 1.8286e+03 5.9281e+00 --1.4700e+03 1.8571e+03 5.9792e+00 --1.4700e+03 1.8857e+03 6.0294e+00 --1.4700e+03 1.9143e+03 6.0786e+00 --1.4700e+03 1.9429e+03 6.1269e+00 --1.4700e+03 1.9714e+03 6.1743e+00 --1.4700e+03 2.0000e+03 6.2207e+00 --1.4400e+03 -2.0000e+03 6.1851e+00 --1.4400e+03 -1.9714e+03 6.1374e+00 --1.4400e+03 -1.9429e+03 6.0888e+00 --1.4400e+03 -1.9143e+03 6.0392e+00 --1.4400e+03 -1.8857e+03 5.9886e+00 --1.4400e+03 -1.8571e+03 5.9370e+00 --1.4400e+03 -1.8286e+03 5.8843e+00 --1.4400e+03 -1.8000e+03 5.8306e+00 --1.4400e+03 -1.7714e+03 5.7758e+00 --1.4400e+03 -1.7429e+03 5.7200e+00 --1.4400e+03 -1.7143e+03 5.6630e+00 --1.4400e+03 -1.6857e+03 5.6049e+00 --1.4400e+03 -1.6571e+03 5.5457e+00 --1.4400e+03 -1.6286e+03 5.4853e+00 --1.4400e+03 -1.6000e+03 5.4237e+00 --1.4400e+03 -1.5714e+03 5.3610e+00 --1.4400e+03 -1.5429e+03 5.2970e+00 --1.4400e+03 -1.5143e+03 5.2318e+00 --1.4400e+03 -1.4857e+03 5.1655e+00 --1.4400e+03 -1.4571e+03 5.0979e+00 --1.4400e+03 -1.4286e+03 5.0290e+00 --1.4400e+03 -1.4000e+03 4.9590e+00 --1.4400e+03 -1.3714e+03 4.8877e+00 --1.4400e+03 -1.3429e+03 4.8151e+00 --1.4400e+03 -1.3143e+03 4.7414e+00 --1.4400e+03 -1.2857e+03 4.6664e+00 --1.4400e+03 -1.2571e+03 4.5903e+00 --1.4400e+03 -1.2286e+03 4.5130e+00 --1.4400e+03 -1.2000e+03 4.4345e+00 --1.4400e+03 -1.1714e+03 4.3549e+00 --1.4400e+03 -1.1429e+03 4.2742e+00 --1.4400e+03 -1.1143e+03 4.1924e+00 --1.4400e+03 -1.0857e+03 4.1097e+00 --1.4400e+03 -1.0571e+03 4.0260e+00 --1.4400e+03 -1.0286e+03 3.9415e+00 --1.4400e+03 -1.0000e+03 3.8561e+00 --1.4400e+03 -9.7143e+02 3.7701e+00 --1.4400e+03 -9.4286e+02 3.6833e+00 --1.4400e+03 -9.1429e+02 3.5961e+00 --1.4400e+03 -8.8571e+02 3.5084e+00 --1.4400e+03 -8.5714e+02 3.4205e+00 --1.4400e+03 -8.2857e+02 3.3323e+00 --1.4400e+03 -8.0000e+02 3.2441e+00 --1.4400e+03 -7.7143e+02 3.1561e+00 --1.4400e+03 -7.4286e+02 3.0683e+00 --1.4400e+03 -7.1429e+02 2.9810e+00 --1.4400e+03 -6.8571e+02 2.8944e+00 --1.4400e+03 -6.5714e+02 2.8086e+00 --1.4400e+03 -6.2857e+02 2.7239e+00 --1.4400e+03 -6.0000e+02 2.6404e+00 --1.4400e+03 -5.7143e+02 2.5585e+00 --1.4400e+03 -5.4286e+02 2.4783e+00 --1.4400e+03 -5.1429e+02 2.4001e+00 --1.4400e+03 -4.8571e+02 2.3242e+00 --1.4400e+03 -4.5714e+02 2.2508e+00 --1.4400e+03 -4.2857e+02 2.1802e+00 --1.4400e+03 -4.0000e+02 2.1125e+00 --1.4400e+03 -3.7143e+02 2.0482e+00 --1.4400e+03 -3.4286e+02 1.9874e+00 --1.4400e+03 -3.1429e+02 1.9304e+00 --1.4400e+03 -2.8571e+02 1.8774e+00 --1.4400e+03 -2.5714e+02 1.8287e+00 --1.4400e+03 -2.2857e+02 1.7844e+00 --1.4400e+03 -2.0000e+02 1.7449e+00 --1.4400e+03 -1.7143e+02 1.7102e+00 --1.4400e+03 -1.4286e+02 1.6806e+00 --1.4400e+03 -1.1429e+02 1.6561e+00 --1.4400e+03 -8.5714e+01 1.6370e+00 --1.4400e+03 -5.7143e+01 1.6232e+00 --1.4400e+03 -2.8571e+01 1.6150e+00 --1.4400e+03 0.0000e+00 1.6122e+00 --1.4400e+03 2.8571e+01 1.6150e+00 --1.4400e+03 5.7143e+01 1.6232e+00 --1.4400e+03 8.5714e+01 1.6370e+00 --1.4400e+03 1.1429e+02 1.6561e+00 --1.4400e+03 1.4286e+02 1.6806e+00 --1.4400e+03 1.7143e+02 1.7102e+00 --1.4400e+03 2.0000e+02 1.7449e+00 --1.4400e+03 2.2857e+02 1.7844e+00 --1.4400e+03 2.5714e+02 1.8287e+00 --1.4400e+03 2.8571e+02 1.8774e+00 --1.4400e+03 3.1429e+02 1.9304e+00 --1.4400e+03 3.4286e+02 1.9874e+00 --1.4400e+03 3.7143e+02 2.0482e+00 --1.4400e+03 4.0000e+02 2.1125e+00 --1.4400e+03 4.2857e+02 2.1802e+00 --1.4400e+03 4.5714e+02 2.2508e+00 --1.4400e+03 4.8571e+02 2.3242e+00 --1.4400e+03 5.1429e+02 2.4001e+00 --1.4400e+03 5.4286e+02 2.4783e+00 --1.4400e+03 5.7143e+02 2.5585e+00 --1.4400e+03 6.0000e+02 2.6404e+00 --1.4400e+03 6.2857e+02 2.7239e+00 --1.4400e+03 6.5714e+02 2.8086e+00 --1.4400e+03 6.8571e+02 2.8944e+00 --1.4400e+03 7.1429e+02 2.9810e+00 --1.4400e+03 7.4286e+02 3.0683e+00 --1.4400e+03 7.7143e+02 3.1561e+00 --1.4400e+03 8.0000e+02 3.2441e+00 --1.4400e+03 8.2857e+02 3.3323e+00 --1.4400e+03 8.5714e+02 3.4205e+00 --1.4400e+03 8.8571e+02 3.5084e+00 --1.4400e+03 9.1429e+02 3.5961e+00 --1.4400e+03 9.4286e+02 3.6833e+00 --1.4400e+03 9.7143e+02 3.7701e+00 --1.4400e+03 1.0000e+03 3.8561e+00 --1.4400e+03 1.0286e+03 3.9415e+00 --1.4400e+03 1.0571e+03 4.0260e+00 --1.4400e+03 1.0857e+03 4.1097e+00 --1.4400e+03 1.1143e+03 4.1924e+00 --1.4400e+03 1.1429e+03 4.2742e+00 --1.4400e+03 1.1714e+03 4.3549e+00 --1.4400e+03 1.2000e+03 4.4345e+00 --1.4400e+03 1.2286e+03 4.5130e+00 --1.4400e+03 1.2571e+03 4.5903e+00 --1.4400e+03 1.2857e+03 4.6664e+00 --1.4400e+03 1.3143e+03 4.7414e+00 --1.4400e+03 1.3429e+03 4.8151e+00 --1.4400e+03 1.3714e+03 4.8877e+00 --1.4400e+03 1.4000e+03 4.9590e+00 --1.4400e+03 1.4286e+03 5.0290e+00 --1.4400e+03 1.4571e+03 5.0979e+00 --1.4400e+03 1.4857e+03 5.1655e+00 --1.4400e+03 1.5143e+03 5.2318e+00 --1.4400e+03 1.5429e+03 5.2970e+00 --1.4400e+03 1.5714e+03 5.3610e+00 --1.4400e+03 1.6000e+03 5.4237e+00 --1.4400e+03 1.6286e+03 5.4853e+00 --1.4400e+03 1.6571e+03 5.5457e+00 --1.4400e+03 1.6857e+03 5.6049e+00 --1.4400e+03 1.7143e+03 5.6630e+00 --1.4400e+03 1.7429e+03 5.7200e+00 --1.4400e+03 1.7714e+03 5.7758e+00 --1.4400e+03 1.8000e+03 5.8306e+00 --1.4400e+03 1.8286e+03 5.8843e+00 --1.4400e+03 1.8571e+03 5.9370e+00 --1.4400e+03 1.8857e+03 5.9886e+00 --1.4400e+03 1.9143e+03 6.0392e+00 --1.4400e+03 1.9429e+03 6.0888e+00 --1.4400e+03 1.9714e+03 6.1374e+00 --1.4400e+03 2.0000e+03 6.1851e+00 --1.4100e+03 -2.0000e+03 6.1493e+00 --1.4100e+03 -1.9714e+03 6.1004e+00 --1.4100e+03 -1.9429e+03 6.0505e+00 --1.4100e+03 -1.9143e+03 5.9995e+00 --1.4100e+03 -1.8857e+03 5.9475e+00 --1.4100e+03 -1.8571e+03 5.8944e+00 --1.4100e+03 -1.8286e+03 5.8402e+00 --1.4100e+03 -1.8000e+03 5.7849e+00 --1.4100e+03 -1.7714e+03 5.7284e+00 --1.4100e+03 -1.7429e+03 5.6708e+00 --1.4100e+03 -1.7143e+03 5.6120e+00 --1.4100e+03 -1.6857e+03 5.5520e+00 --1.4100e+03 -1.6571e+03 5.4908e+00 --1.4100e+03 -1.6286e+03 5.4283e+00 --1.4100e+03 -1.6000e+03 5.3646e+00 --1.4100e+03 -1.5714e+03 5.2995e+00 --1.4100e+03 -1.5429e+03 5.2332e+00 --1.4100e+03 -1.5143e+03 5.1656e+00 --1.4100e+03 -1.4857e+03 5.0967e+00 --1.4100e+03 -1.4571e+03 5.0264e+00 --1.4100e+03 -1.4286e+03 4.9548e+00 --1.4100e+03 -1.4000e+03 4.8819e+00 --1.4100e+03 -1.3714e+03 4.8076e+00 --1.4100e+03 -1.3429e+03 4.7319e+00 --1.4100e+03 -1.3143e+03 4.6549e+00 --1.4100e+03 -1.2857e+03 4.5765e+00 --1.4100e+03 -1.2571e+03 4.4968e+00 --1.4100e+03 -1.2286e+03 4.4158e+00 --1.4100e+03 -1.2000e+03 4.3335e+00 --1.4100e+03 -1.1714e+03 4.2499e+00 --1.4100e+03 -1.1429e+03 4.1651e+00 --1.4100e+03 -1.1143e+03 4.0791e+00 --1.4100e+03 -1.0857e+03 3.9920e+00 --1.4100e+03 -1.0571e+03 3.9037e+00 --1.4100e+03 -1.0286e+03 3.8144e+00 --1.4100e+03 -1.0000e+03 3.7242e+00 --1.4100e+03 -9.7143e+02 3.6331e+00 --1.4100e+03 -9.4286e+02 3.5412e+00 --1.4100e+03 -9.1429e+02 3.4486e+00 --1.4100e+03 -8.8571e+02 3.3554e+00 --1.4100e+03 -8.5714e+02 3.2618e+00 --1.4100e+03 -8.2857e+02 3.1679e+00 --1.4100e+03 -8.0000e+02 3.0738e+00 --1.4100e+03 -7.7143e+02 2.9797e+00 --1.4100e+03 -7.4286e+02 2.8858e+00 --1.4100e+03 -7.1429e+02 2.7923e+00 --1.4100e+03 -6.8571e+02 2.6994e+00 --1.4100e+03 -6.5714e+02 2.6073e+00 --1.4100e+03 -6.2857e+02 2.5161e+00 --1.4100e+03 -6.0000e+02 2.4263e+00 --1.4100e+03 -5.7143e+02 2.3380e+00 --1.4100e+03 -5.4286e+02 2.2514e+00 --1.4100e+03 -5.1429e+02 2.1669e+00 --1.4100e+03 -4.8571e+02 2.0847e+00 --1.4100e+03 -4.5714e+02 2.0052e+00 --1.4100e+03 -4.2857e+02 1.9285e+00 --1.4100e+03 -4.0000e+02 1.8551e+00 --1.4100e+03 -3.7143e+02 1.7851e+00 --1.4100e+03 -3.4286e+02 1.7190e+00 --1.4100e+03 -3.1429e+02 1.6569e+00 --1.4100e+03 -2.8571e+02 1.5991e+00 --1.4100e+03 -2.5714e+02 1.5459e+00 --1.4100e+03 -2.2857e+02 1.4976e+00 --1.4100e+03 -2.0000e+02 1.4544e+00 --1.4100e+03 -1.7143e+02 1.4165e+00 --1.4100e+03 -1.4286e+02 1.3841e+00 --1.4100e+03 -1.1429e+02 1.3573e+00 --1.4100e+03 -8.5714e+01 1.3364e+00 --1.4100e+03 -5.7143e+01 1.3213e+00 --1.4100e+03 -2.8571e+01 1.3123e+00 --1.4100e+03 0.0000e+00 1.3092e+00 --1.4100e+03 2.8571e+01 1.3123e+00 --1.4100e+03 5.7143e+01 1.3213e+00 --1.4100e+03 8.5714e+01 1.3364e+00 --1.4100e+03 1.1429e+02 1.3573e+00 --1.4100e+03 1.4286e+02 1.3841e+00 --1.4100e+03 1.7143e+02 1.4165e+00 --1.4100e+03 2.0000e+02 1.4544e+00 --1.4100e+03 2.2857e+02 1.4976e+00 --1.4100e+03 2.5714e+02 1.5459e+00 --1.4100e+03 2.8571e+02 1.5991e+00 --1.4100e+03 3.1429e+02 1.6569e+00 --1.4100e+03 3.4286e+02 1.7190e+00 --1.4100e+03 3.7143e+02 1.7851e+00 --1.4100e+03 4.0000e+02 1.8551e+00 --1.4100e+03 4.2857e+02 1.9285e+00 --1.4100e+03 4.5714e+02 2.0052e+00 --1.4100e+03 4.8571e+02 2.0847e+00 --1.4100e+03 5.1429e+02 2.1669e+00 --1.4100e+03 5.4286e+02 2.2514e+00 --1.4100e+03 5.7143e+02 2.3380e+00 --1.4100e+03 6.0000e+02 2.4263e+00 --1.4100e+03 6.2857e+02 2.5161e+00 --1.4100e+03 6.5714e+02 2.6073e+00 --1.4100e+03 6.8571e+02 2.6994e+00 --1.4100e+03 7.1429e+02 2.7923e+00 --1.4100e+03 7.4286e+02 2.8858e+00 --1.4100e+03 7.7143e+02 2.9797e+00 --1.4100e+03 8.0000e+02 3.0738e+00 --1.4100e+03 8.2857e+02 3.1679e+00 --1.4100e+03 8.5714e+02 3.2618e+00 --1.4100e+03 8.8571e+02 3.3554e+00 --1.4100e+03 9.1429e+02 3.4486e+00 --1.4100e+03 9.4286e+02 3.5412e+00 --1.4100e+03 9.7143e+02 3.6331e+00 --1.4100e+03 1.0000e+03 3.7242e+00 --1.4100e+03 1.0286e+03 3.8144e+00 --1.4100e+03 1.0571e+03 3.9037e+00 --1.4100e+03 1.0857e+03 3.9920e+00 --1.4100e+03 1.1143e+03 4.0791e+00 --1.4100e+03 1.1429e+03 4.1651e+00 --1.4100e+03 1.1714e+03 4.2499e+00 --1.4100e+03 1.2000e+03 4.3335e+00 --1.4100e+03 1.2286e+03 4.4158e+00 --1.4100e+03 1.2571e+03 4.4968e+00 --1.4100e+03 1.2857e+03 4.5765e+00 --1.4100e+03 1.3143e+03 4.6549e+00 --1.4100e+03 1.3429e+03 4.7319e+00 --1.4100e+03 1.3714e+03 4.8076e+00 --1.4100e+03 1.4000e+03 4.8819e+00 --1.4100e+03 1.4286e+03 4.9548e+00 --1.4100e+03 1.4571e+03 5.0264e+00 --1.4100e+03 1.4857e+03 5.0967e+00 --1.4100e+03 1.5143e+03 5.1656e+00 --1.4100e+03 1.5429e+03 5.2332e+00 --1.4100e+03 1.5714e+03 5.2995e+00 --1.4100e+03 1.6000e+03 5.3646e+00 --1.4100e+03 1.6286e+03 5.4283e+00 --1.4100e+03 1.6571e+03 5.4908e+00 --1.4100e+03 1.6857e+03 5.5520e+00 --1.4100e+03 1.7143e+03 5.6120e+00 --1.4100e+03 1.7429e+03 5.6708e+00 --1.4100e+03 1.7714e+03 5.7284e+00 --1.4100e+03 1.8000e+03 5.7849e+00 --1.4100e+03 1.8286e+03 5.8402e+00 --1.4100e+03 1.8571e+03 5.8944e+00 --1.4100e+03 1.8857e+03 5.9475e+00 --1.4100e+03 1.9143e+03 5.9995e+00 --1.4100e+03 1.9429e+03 6.0505e+00 --1.4100e+03 1.9714e+03 6.1004e+00 --1.4100e+03 2.0000e+03 6.1493e+00 --1.3800e+03 -2.0000e+03 6.1134e+00 --1.3800e+03 -1.9714e+03 6.0632e+00 --1.3800e+03 -1.9429e+03 6.0119e+00 --1.3800e+03 -1.9143e+03 5.9595e+00 --1.3800e+03 -1.8857e+03 5.9061e+00 --1.3800e+03 -1.8571e+03 5.8514e+00 --1.3800e+03 -1.8286e+03 5.7957e+00 --1.3800e+03 -1.8000e+03 5.7387e+00 --1.3800e+03 -1.7714e+03 5.6805e+00 --1.3800e+03 -1.7429e+03 5.6211e+00 --1.3800e+03 -1.7143e+03 5.5604e+00 --1.3800e+03 -1.6857e+03 5.4984e+00 --1.3800e+03 -1.6571e+03 5.4352e+00 --1.3800e+03 -1.6286e+03 5.3706e+00 --1.3800e+03 -1.6000e+03 5.3046e+00 --1.3800e+03 -1.5714e+03 5.2373e+00 --1.3800e+03 -1.5429e+03 5.1685e+00 --1.3800e+03 -1.5143e+03 5.0984e+00 --1.3800e+03 -1.4857e+03 5.0268e+00 --1.3800e+03 -1.4571e+03 4.9537e+00 --1.3800e+03 -1.4286e+03 4.8792e+00 --1.3800e+03 -1.4000e+03 4.8032e+00 --1.3800e+03 -1.3714e+03 4.7258e+00 --1.3800e+03 -1.3429e+03 4.6468e+00 --1.3800e+03 -1.3143e+03 4.5664e+00 --1.3800e+03 -1.2857e+03 4.4844e+00 --1.3800e+03 -1.2571e+03 4.4010e+00 --1.3800e+03 -1.2286e+03 4.3161e+00 --1.3800e+03 -1.2000e+03 4.2298e+00 --1.3800e+03 -1.1714e+03 4.1420e+00 --1.3800e+03 -1.1429e+03 4.0529e+00 --1.3800e+03 -1.1143e+03 3.9623e+00 --1.3800e+03 -1.0857e+03 3.8705e+00 --1.3800e+03 -1.0571e+03 3.7773e+00 --1.3800e+03 -1.0286e+03 3.6830e+00 --1.3800e+03 -1.0000e+03 3.5875e+00 --1.3800e+03 -9.7143e+02 3.4910e+00 --1.3800e+03 -9.4286e+02 3.3935e+00 --1.3800e+03 -9.1429e+02 3.2951e+00 --1.3800e+03 -8.8571e+02 3.1960e+00 --1.3800e+03 -8.5714e+02 3.0963e+00 --1.3800e+03 -8.2857e+02 2.9962e+00 --1.3800e+03 -8.0000e+02 2.8957e+00 --1.3800e+03 -7.7143e+02 2.7951e+00 --1.3800e+03 -7.4286e+02 2.6945e+00 --1.3800e+03 -7.1429e+02 2.5942e+00 --1.3800e+03 -6.8571e+02 2.4944e+00 --1.3800e+03 -6.5714e+02 2.3953e+00 --1.3800e+03 -6.2857e+02 2.2971e+00 --1.3800e+03 -6.0000e+02 2.2002e+00 --1.3800e+03 -5.7143e+02 2.1048e+00 --1.3800e+03 -5.4286e+02 2.0112e+00 --1.3800e+03 -5.1429e+02 1.9197e+00 --1.3800e+03 -4.8571e+02 1.8306e+00 --1.3800e+03 -4.5714e+02 1.7442e+00 --1.3800e+03 -4.2857e+02 1.6609e+00 --1.3800e+03 -4.0000e+02 1.5809e+00 --1.3800e+03 -3.7143e+02 1.5047e+00 --1.3800e+03 -3.4286e+02 1.4325e+00 --1.3800e+03 -3.1429e+02 1.3647e+00 --1.3800e+03 -2.8571e+02 1.3016e+00 --1.3800e+03 -2.5714e+02 1.2434e+00 --1.3800e+03 -2.2857e+02 1.1905e+00 --1.3800e+03 -2.0000e+02 1.1432e+00 --1.3800e+03 -1.7143e+02 1.1016e+00 --1.3800e+03 -1.4286e+02 1.0661e+00 --1.3800e+03 -1.1429e+02 1.0367e+00 --1.3800e+03 -8.5714e+01 1.0137e+00 --1.3800e+03 -5.7143e+01 9.9714e-01 --1.3800e+03 -2.8571e+01 9.8719e-01 --1.3800e+03 0.0000e+00 9.8386e-01 --1.3800e+03 2.8571e+01 9.8719e-01 --1.3800e+03 5.7143e+01 9.9714e-01 --1.3800e+03 8.5714e+01 1.0137e+00 --1.3800e+03 1.1429e+02 1.0367e+00 --1.3800e+03 1.4286e+02 1.0661e+00 --1.3800e+03 1.7143e+02 1.1016e+00 --1.3800e+03 2.0000e+02 1.1432e+00 --1.3800e+03 2.2857e+02 1.1905e+00 --1.3800e+03 2.5714e+02 1.2434e+00 --1.3800e+03 2.8571e+02 1.3016e+00 --1.3800e+03 3.1429e+02 1.3647e+00 --1.3800e+03 3.4286e+02 1.4325e+00 --1.3800e+03 3.7143e+02 1.5047e+00 --1.3800e+03 4.0000e+02 1.5809e+00 --1.3800e+03 4.2857e+02 1.6609e+00 --1.3800e+03 4.5714e+02 1.7442e+00 --1.3800e+03 4.8571e+02 1.8306e+00 --1.3800e+03 5.1429e+02 1.9197e+00 --1.3800e+03 5.4286e+02 2.0112e+00 --1.3800e+03 5.7143e+02 2.1048e+00 --1.3800e+03 6.0000e+02 2.2002e+00 --1.3800e+03 6.2857e+02 2.2971e+00 --1.3800e+03 6.5714e+02 2.3953e+00 --1.3800e+03 6.8571e+02 2.4944e+00 --1.3800e+03 7.1429e+02 2.5942e+00 --1.3800e+03 7.4286e+02 2.6945e+00 --1.3800e+03 7.7143e+02 2.7951e+00 --1.3800e+03 8.0000e+02 2.8957e+00 --1.3800e+03 8.2857e+02 2.9962e+00 --1.3800e+03 8.5714e+02 3.0963e+00 --1.3800e+03 8.8571e+02 3.1960e+00 --1.3800e+03 9.1429e+02 3.2951e+00 --1.3800e+03 9.4286e+02 3.3935e+00 --1.3800e+03 9.7143e+02 3.4910e+00 --1.3800e+03 1.0000e+03 3.5875e+00 --1.3800e+03 1.0286e+03 3.6830e+00 --1.3800e+03 1.0571e+03 3.7773e+00 --1.3800e+03 1.0857e+03 3.8705e+00 --1.3800e+03 1.1143e+03 3.9623e+00 --1.3800e+03 1.1429e+03 4.0529e+00 --1.3800e+03 1.1714e+03 4.1420e+00 --1.3800e+03 1.2000e+03 4.2298e+00 --1.3800e+03 1.2286e+03 4.3161e+00 --1.3800e+03 1.2571e+03 4.4010e+00 --1.3800e+03 1.2857e+03 4.4844e+00 --1.3800e+03 1.3143e+03 4.5664e+00 --1.3800e+03 1.3429e+03 4.6468e+00 --1.3800e+03 1.3714e+03 4.7258e+00 --1.3800e+03 1.4000e+03 4.8032e+00 --1.3800e+03 1.4286e+03 4.8792e+00 --1.3800e+03 1.4571e+03 4.9537e+00 --1.3800e+03 1.4857e+03 5.0268e+00 --1.3800e+03 1.5143e+03 5.0984e+00 --1.3800e+03 1.5429e+03 5.1685e+00 --1.3800e+03 1.5714e+03 5.2373e+00 --1.3800e+03 1.6000e+03 5.3046e+00 --1.3800e+03 1.6286e+03 5.3706e+00 --1.3800e+03 1.6571e+03 5.4352e+00 --1.3800e+03 1.6857e+03 5.4984e+00 --1.3800e+03 1.7143e+03 5.5604e+00 --1.3800e+03 1.7429e+03 5.6211e+00 --1.3800e+03 1.7714e+03 5.6805e+00 --1.3800e+03 1.8000e+03 5.7387e+00 --1.3800e+03 1.8286e+03 5.7957e+00 --1.3800e+03 1.8571e+03 5.8514e+00 --1.3800e+03 1.8857e+03 5.9061e+00 --1.3800e+03 1.9143e+03 5.9595e+00 --1.3800e+03 1.9429e+03 6.0119e+00 --1.3800e+03 1.9714e+03 6.0632e+00 --1.3800e+03 2.0000e+03 6.1134e+00 --1.3500e+03 -2.0000e+03 6.0773e+00 --1.3500e+03 -1.9714e+03 6.0258e+00 --1.3500e+03 -1.9429e+03 5.9732e+00 --1.3500e+03 -1.9143e+03 5.9194e+00 --1.3500e+03 -1.8857e+03 5.8644e+00 --1.3500e+03 -1.8571e+03 5.8082e+00 --1.3500e+03 -1.8286e+03 5.7508e+00 --1.3500e+03 -1.8000e+03 5.6922e+00 --1.3500e+03 -1.7714e+03 5.6322e+00 --1.3500e+03 -1.7429e+03 5.5709e+00 --1.3500e+03 -1.7143e+03 5.5083e+00 --1.3500e+03 -1.6857e+03 5.4443e+00 --1.3500e+03 -1.6571e+03 5.3789e+00 --1.3500e+03 -1.6286e+03 5.3121e+00 --1.3500e+03 -1.6000e+03 5.2438e+00 --1.3500e+03 -1.5714e+03 5.1741e+00 --1.3500e+03 -1.5429e+03 5.1028e+00 --1.3500e+03 -1.5143e+03 5.0300e+00 --1.3500e+03 -1.4857e+03 4.9556e+00 --1.3500e+03 -1.4571e+03 4.8797e+00 --1.3500e+03 -1.4286e+03 4.8022e+00 --1.3500e+03 -1.4000e+03 4.7231e+00 --1.3500e+03 -1.3714e+03 4.6423e+00 --1.3500e+03 -1.3429e+03 4.5599e+00 --1.3500e+03 -1.3143e+03 4.4758e+00 --1.3500e+03 -1.2857e+03 4.3902e+00 --1.3500e+03 -1.2571e+03 4.3028e+00 --1.3500e+03 -1.2286e+03 4.2139e+00 --1.3500e+03 -1.2000e+03 4.1233e+00 --1.3500e+03 -1.1714e+03 4.0310e+00 --1.3500e+03 -1.1429e+03 3.9372e+00 --1.3500e+03 -1.1143e+03 3.8419e+00 --1.3500e+03 -1.0857e+03 3.7450e+00 --1.3500e+03 -1.0571e+03 3.6467e+00 --1.3500e+03 -1.0286e+03 3.5469e+00 --1.3500e+03 -1.0000e+03 3.4459e+00 --1.3500e+03 -9.7143e+02 3.3435e+00 --1.3500e+03 -9.4286e+02 3.2400e+00 --1.3500e+03 -9.1429e+02 3.1355e+00 --1.3500e+03 -8.8571e+02 3.0300e+00 --1.3500e+03 -8.5714e+02 2.9237e+00 --1.3500e+03 -8.2857e+02 2.8167e+00 --1.3500e+03 -8.0000e+02 2.7093e+00 --1.3500e+03 -7.7143e+02 2.6016e+00 --1.3500e+03 -7.4286e+02 2.4937e+00 --1.3500e+03 -7.1429e+02 2.3860e+00 --1.3500e+03 -6.8571e+02 2.2786e+00 --1.3500e+03 -6.5714e+02 2.1718e+00 --1.3500e+03 -6.2857e+02 2.0660e+00 --1.3500e+03 -6.0000e+02 1.9612e+00 --1.3500e+03 -5.7143e+02 1.8580e+00 --1.3500e+03 -5.4286e+02 1.7566e+00 --1.3500e+03 -5.1429e+02 1.6572e+00 --1.3500e+03 -4.8571e+02 1.5604e+00 --1.3500e+03 -4.5714e+02 1.4664e+00 --1.3500e+03 -4.2857e+02 1.3756e+00 --1.3500e+03 -4.0000e+02 1.2884e+00 --1.3500e+03 -3.7143e+02 1.2051e+00 --1.3500e+03 -3.4286e+02 1.1262e+00 --1.3500e+03 -3.1429e+02 1.0520e+00 --1.3500e+03 -2.8571e+02 9.8277e-01 --1.3500e+03 -2.5714e+02 9.1898e-01 --1.3500e+03 -2.2857e+02 8.6092e-01 --1.3500e+03 -2.0000e+02 8.0890e-01 --1.3500e+03 -1.7143e+02 7.6319e-01 --1.3500e+03 -1.4286e+02 7.2407e-01 --1.3500e+03 -1.1429e+02 6.9174e-01 --1.3500e+03 -8.5714e+01 6.6640e-01 --1.3500e+03 -5.7143e+01 6.4819e-01 --1.3500e+03 -2.8571e+01 6.3722e-01 --1.3500e+03 0.0000e+00 6.3355e-01 --1.3500e+03 2.8571e+01 6.3722e-01 --1.3500e+03 5.7143e+01 6.4819e-01 --1.3500e+03 8.5714e+01 6.6640e-01 --1.3500e+03 1.1429e+02 6.9174e-01 --1.3500e+03 1.4286e+02 7.2407e-01 --1.3500e+03 1.7143e+02 7.6319e-01 --1.3500e+03 2.0000e+02 8.0890e-01 --1.3500e+03 2.2857e+02 8.6092e-01 --1.3500e+03 2.5714e+02 9.1898e-01 --1.3500e+03 2.8571e+02 9.8277e-01 --1.3500e+03 3.1429e+02 1.0520e+00 --1.3500e+03 3.4286e+02 1.1262e+00 --1.3500e+03 3.7143e+02 1.2051e+00 --1.3500e+03 4.0000e+02 1.2884e+00 --1.3500e+03 4.2857e+02 1.3756e+00 --1.3500e+03 4.5714e+02 1.4664e+00 --1.3500e+03 4.8571e+02 1.5604e+00 --1.3500e+03 5.1429e+02 1.6572e+00 --1.3500e+03 5.4286e+02 1.7566e+00 --1.3500e+03 5.7143e+02 1.8580e+00 --1.3500e+03 6.0000e+02 1.9612e+00 --1.3500e+03 6.2857e+02 2.0660e+00 --1.3500e+03 6.5714e+02 2.1718e+00 --1.3500e+03 6.8571e+02 2.2786e+00 --1.3500e+03 7.1429e+02 2.3860e+00 --1.3500e+03 7.4286e+02 2.4937e+00 --1.3500e+03 7.7143e+02 2.6016e+00 --1.3500e+03 8.0000e+02 2.7093e+00 --1.3500e+03 8.2857e+02 2.8167e+00 --1.3500e+03 8.5714e+02 2.9237e+00 --1.3500e+03 8.8571e+02 3.0300e+00 --1.3500e+03 9.1429e+02 3.1355e+00 --1.3500e+03 9.4286e+02 3.2400e+00 --1.3500e+03 9.7143e+02 3.3435e+00 --1.3500e+03 1.0000e+03 3.4459e+00 --1.3500e+03 1.0286e+03 3.5469e+00 --1.3500e+03 1.0571e+03 3.6467e+00 --1.3500e+03 1.0857e+03 3.7450e+00 --1.3500e+03 1.1143e+03 3.8419e+00 --1.3500e+03 1.1429e+03 3.9372e+00 --1.3500e+03 1.1714e+03 4.0310e+00 --1.3500e+03 1.2000e+03 4.1233e+00 --1.3500e+03 1.2286e+03 4.2139e+00 --1.3500e+03 1.2571e+03 4.3028e+00 --1.3500e+03 1.2857e+03 4.3902e+00 --1.3500e+03 1.3143e+03 4.4758e+00 --1.3500e+03 1.3429e+03 4.5599e+00 --1.3500e+03 1.3714e+03 4.6423e+00 --1.3500e+03 1.4000e+03 4.7231e+00 --1.3500e+03 1.4286e+03 4.8022e+00 --1.3500e+03 1.4571e+03 4.8797e+00 --1.3500e+03 1.4857e+03 4.9556e+00 --1.3500e+03 1.5143e+03 5.0300e+00 --1.3500e+03 1.5429e+03 5.1028e+00 --1.3500e+03 1.5714e+03 5.1741e+00 --1.3500e+03 1.6000e+03 5.2438e+00 --1.3500e+03 1.6286e+03 5.3121e+00 --1.3500e+03 1.6571e+03 5.3789e+00 --1.3500e+03 1.6857e+03 5.4443e+00 --1.3500e+03 1.7143e+03 5.5083e+00 --1.3500e+03 1.7429e+03 5.5709e+00 --1.3500e+03 1.7714e+03 5.6322e+00 --1.3500e+03 1.8000e+03 5.6922e+00 --1.3500e+03 1.8286e+03 5.7508e+00 --1.3500e+03 1.8571e+03 5.8082e+00 --1.3500e+03 1.8857e+03 5.8644e+00 --1.3500e+03 1.9143e+03 5.9194e+00 --1.3500e+03 1.9429e+03 5.9732e+00 --1.3500e+03 1.9714e+03 6.0258e+00 --1.3500e+03 2.0000e+03 6.0773e+00 --1.3200e+03 -2.0000e+03 6.0412e+00 --1.3200e+03 -1.9714e+03 5.9883e+00 --1.3200e+03 -1.9429e+03 5.9343e+00 --1.3200e+03 -1.9143e+03 5.8790e+00 --1.3200e+03 -1.8857e+03 5.8225e+00 --1.3200e+03 -1.8571e+03 5.7648e+00 --1.3200e+03 -1.8286e+03 5.7057e+00 --1.3200e+03 -1.8000e+03 5.6453e+00 --1.3200e+03 -1.7714e+03 5.5835e+00 --1.3200e+03 -1.7429e+03 5.5204e+00 --1.3200e+03 -1.7143e+03 5.4557e+00 --1.3200e+03 -1.6857e+03 5.3897e+00 --1.3200e+03 -1.6571e+03 5.3221e+00 --1.3200e+03 -1.6286e+03 5.2530e+00 --1.3200e+03 -1.6000e+03 5.1823e+00 --1.3200e+03 -1.5714e+03 5.1100e+00 --1.3200e+03 -1.5429e+03 5.0362e+00 --1.3200e+03 -1.5143e+03 4.9606e+00 --1.3200e+03 -1.4857e+03 4.8834e+00 --1.3200e+03 -1.4571e+03 4.8044e+00 --1.3200e+03 -1.4286e+03 4.7238e+00 --1.3200e+03 -1.4000e+03 4.6413e+00 --1.3200e+03 -1.3714e+03 4.5571e+00 --1.3200e+03 -1.3429e+03 4.4711e+00 --1.3200e+03 -1.3143e+03 4.3833e+00 --1.3200e+03 -1.2857e+03 4.2937e+00 --1.3200e+03 -1.2571e+03 4.2022e+00 --1.3200e+03 -1.2286e+03 4.1089e+00 --1.3200e+03 -1.2000e+03 4.0138e+00 --1.3200e+03 -1.1714e+03 3.9169e+00 --1.3200e+03 -1.1429e+03 3.8182e+00 --1.3200e+03 -1.1143e+03 3.7177e+00 --1.3200e+03 -1.0857e+03 3.6155e+00 --1.3200e+03 -1.0571e+03 3.5117e+00 --1.3200e+03 -1.0286e+03 3.4061e+00 --1.3200e+03 -1.0000e+03 3.2991e+00 --1.3200e+03 -9.7143e+02 3.1905e+00 --1.3200e+03 -9.4286e+02 3.0805e+00 --1.3200e+03 -9.1429e+02 2.9693e+00 --1.3200e+03 -8.8571e+02 2.8569e+00 --1.3200e+03 -8.5714e+02 2.7434e+00 --1.3200e+03 -8.2857e+02 2.6291e+00 --1.3200e+03 -8.0000e+02 2.5141e+00 --1.3200e+03 -7.7143e+02 2.3986e+00 --1.3200e+03 -7.4286e+02 2.2828e+00 --1.3200e+03 -7.1429e+02 2.1669e+00 --1.3200e+03 -6.8571e+02 2.0513e+00 --1.3200e+03 -6.5714e+02 1.9361e+00 --1.3200e+03 -6.2857e+02 1.8216e+00 --1.3200e+03 -6.0000e+02 1.7083e+00 --1.3200e+03 -5.7143e+02 1.5964e+00 --1.3200e+03 -5.4286e+02 1.4862e+00 --1.3200e+03 -5.1429e+02 1.3782e+00 --1.3200e+03 -4.8571e+02 1.2728e+00 --1.3200e+03 -4.5714e+02 1.1702e+00 --1.3200e+03 -4.2857e+02 1.0711e+00 --1.3200e+03 -4.0000e+02 9.7570e-01 --1.3200e+03 -3.7143e+02 8.8452e-01 --1.3200e+03 -3.4286e+02 7.9797e-01 --1.3200e+03 -3.1429e+02 7.1647e-01 --1.3200e+03 -2.8571e+02 6.4043e-01 --1.3200e+03 -2.5714e+02 5.7025e-01 --1.3200e+03 -2.2857e+02 5.0631e-01 --1.3200e+03 -2.0000e+02 4.4897e-01 --1.3200e+03 -1.7143e+02 3.9856e-01 --1.3200e+03 -1.4286e+02 3.5538e-01 --1.3200e+03 -1.1429e+02 3.1968e-01 --1.3200e+03 -8.5714e+01 2.9168e-01 --1.3200e+03 -5.7143e+01 2.7155e-01 --1.3200e+03 -2.8571e+01 2.5942e-01 --1.3200e+03 0.0000e+00 2.5537e-01 --1.3200e+03 2.8571e+01 2.5942e-01 --1.3200e+03 5.7143e+01 2.7155e-01 --1.3200e+03 8.5714e+01 2.9168e-01 --1.3200e+03 1.1429e+02 3.1968e-01 --1.3200e+03 1.4286e+02 3.5538e-01 --1.3200e+03 1.7143e+02 3.9856e-01 --1.3200e+03 2.0000e+02 4.4897e-01 --1.3200e+03 2.2857e+02 5.0631e-01 --1.3200e+03 2.5714e+02 5.7025e-01 --1.3200e+03 2.8571e+02 6.4043e-01 --1.3200e+03 3.1429e+02 7.1647e-01 --1.3200e+03 3.4286e+02 7.9797e-01 --1.3200e+03 3.7143e+02 8.8452e-01 --1.3200e+03 4.0000e+02 9.7570e-01 --1.3200e+03 4.2857e+02 1.0711e+00 --1.3200e+03 4.5714e+02 1.1702e+00 --1.3200e+03 4.8571e+02 1.2728e+00 --1.3200e+03 5.1429e+02 1.3782e+00 --1.3200e+03 5.4286e+02 1.4862e+00 --1.3200e+03 5.7143e+02 1.5964e+00 --1.3200e+03 6.0000e+02 1.7083e+00 --1.3200e+03 6.2857e+02 1.8216e+00 --1.3200e+03 6.5714e+02 1.9361e+00 --1.3200e+03 6.8571e+02 2.0513e+00 --1.3200e+03 7.1429e+02 2.1669e+00 --1.3200e+03 7.4286e+02 2.2828e+00 --1.3200e+03 7.7143e+02 2.3986e+00 --1.3200e+03 8.0000e+02 2.5141e+00 --1.3200e+03 8.2857e+02 2.6291e+00 --1.3200e+03 8.5714e+02 2.7434e+00 --1.3200e+03 8.8571e+02 2.8569e+00 --1.3200e+03 9.1429e+02 2.9693e+00 --1.3200e+03 9.4286e+02 3.0805e+00 --1.3200e+03 9.7143e+02 3.1905e+00 --1.3200e+03 1.0000e+03 3.2991e+00 --1.3200e+03 1.0286e+03 3.4061e+00 --1.3200e+03 1.0571e+03 3.5117e+00 --1.3200e+03 1.0857e+03 3.6155e+00 --1.3200e+03 1.1143e+03 3.7177e+00 --1.3200e+03 1.1429e+03 3.8182e+00 --1.3200e+03 1.1714e+03 3.9169e+00 --1.3200e+03 1.2000e+03 4.0138e+00 --1.3200e+03 1.2286e+03 4.1089e+00 --1.3200e+03 1.2571e+03 4.2022e+00 --1.3200e+03 1.2857e+03 4.2937e+00 --1.3200e+03 1.3143e+03 4.3833e+00 --1.3200e+03 1.3429e+03 4.4711e+00 --1.3200e+03 1.3714e+03 4.5571e+00 --1.3200e+03 1.4000e+03 4.6413e+00 --1.3200e+03 1.4286e+03 4.7238e+00 --1.3200e+03 1.4571e+03 4.8044e+00 --1.3200e+03 1.4857e+03 4.8834e+00 --1.3200e+03 1.5143e+03 4.9606e+00 --1.3200e+03 1.5429e+03 5.0362e+00 --1.3200e+03 1.5714e+03 5.1100e+00 --1.3200e+03 1.6000e+03 5.1823e+00 --1.3200e+03 1.6286e+03 5.2530e+00 --1.3200e+03 1.6571e+03 5.3221e+00 --1.3200e+03 1.6857e+03 5.3897e+00 --1.3200e+03 1.7143e+03 5.4557e+00 --1.3200e+03 1.7429e+03 5.5204e+00 --1.3200e+03 1.7714e+03 5.5835e+00 --1.3200e+03 1.8000e+03 5.6453e+00 --1.3200e+03 1.8286e+03 5.7057e+00 --1.3200e+03 1.8571e+03 5.7648e+00 --1.3200e+03 1.8857e+03 5.8225e+00 --1.3200e+03 1.9143e+03 5.8790e+00 --1.3200e+03 1.9429e+03 5.9343e+00 --1.3200e+03 1.9714e+03 5.9883e+00 --1.3200e+03 2.0000e+03 6.0412e+00 --1.2900e+03 -2.0000e+03 6.0049e+00 --1.2900e+03 -1.9714e+03 5.9507e+00 --1.2900e+03 -1.9429e+03 5.8952e+00 --1.2900e+03 -1.9143e+03 5.8385e+00 --1.2900e+03 -1.8857e+03 5.7805e+00 --1.2900e+03 -1.8571e+03 5.7211e+00 --1.2900e+03 -1.8286e+03 5.6603e+00 --1.2900e+03 -1.8000e+03 5.5981e+00 --1.2900e+03 -1.7714e+03 5.5345e+00 --1.2900e+03 -1.7429e+03 5.4693e+00 --1.2900e+03 -1.7143e+03 5.4027e+00 --1.2900e+03 -1.6857e+03 5.3345e+00 --1.2900e+03 -1.6571e+03 5.2646e+00 --1.2900e+03 -1.6286e+03 5.1932e+00 --1.2900e+03 -1.6000e+03 5.1200e+00 --1.2900e+03 -1.5714e+03 5.0452e+00 --1.2900e+03 -1.5429e+03 4.9686e+00 --1.2900e+03 -1.5143e+03 4.8902e+00 --1.2900e+03 -1.4857e+03 4.8100e+00 --1.2900e+03 -1.4571e+03 4.7279e+00 --1.2900e+03 -1.4286e+03 4.6440e+00 --1.2900e+03 -1.4000e+03 4.5581e+00 --1.2900e+03 -1.3714e+03 4.4703e+00 --1.2900e+03 -1.3429e+03 4.3805e+00 --1.2900e+03 -1.3143e+03 4.2887e+00 --1.2900e+03 -1.2857e+03 4.1949e+00 --1.2900e+03 -1.2571e+03 4.0991e+00 --1.2900e+03 -1.2286e+03 4.0013e+00 --1.2900e+03 -1.2000e+03 3.9014e+00 --1.2900e+03 -1.1714e+03 3.7996e+00 --1.2900e+03 -1.1429e+03 3.6957e+00 --1.2900e+03 -1.1143e+03 3.5898e+00 --1.2900e+03 -1.0857e+03 3.4819e+00 --1.2900e+03 -1.0571e+03 3.3721e+00 --1.2900e+03 -1.0286e+03 3.2604e+00 --1.2900e+03 -1.0000e+03 3.1469e+00 --1.2900e+03 -9.7143e+02 3.0316e+00 --1.2900e+03 -9.4286e+02 2.9147e+00 --1.2900e+03 -9.1429e+02 2.7962e+00 --1.2900e+03 -8.8571e+02 2.6764e+00 --1.2900e+03 -8.5714e+02 2.5552e+00 --1.2900e+03 -8.2857e+02 2.4329e+00 --1.2900e+03 -8.0000e+02 2.3096e+00 --1.2900e+03 -7.7143e+02 2.1856e+00 --1.2900e+03 -7.4286e+02 2.0611e+00 --1.2900e+03 -7.1429e+02 1.9363e+00 --1.2900e+03 -6.8571e+02 1.8115e+00 --1.2900e+03 -6.5714e+02 1.6870e+00 --1.2900e+03 -6.2857e+02 1.5631e+00 --1.2900e+03 -6.0000e+02 1.4402e+00 --1.2900e+03 -5.7143e+02 1.3186e+00 --1.2900e+03 -5.4286e+02 1.1988e+00 --1.2900e+03 -5.1429e+02 1.0811e+00 --1.2900e+03 -4.8571e+02 9.6596e-01 --1.2900e+03 -4.5714e+02 8.5389e-01 --1.2900e+03 -4.2857e+02 7.4532e-01 --1.2900e+03 -4.0000e+02 6.4073e-01 --1.2900e+03 -3.7143e+02 5.4061e-01 --1.2900e+03 -3.4286e+02 4.4544e-01 --1.2900e+03 -3.1429e+02 3.5570e-01 --1.2900e+03 -2.8571e+02 2.7188e-01 --1.2900e+03 -2.5714e+02 1.9443e-01 --1.2900e+03 -2.2857e+02 1.2380e-01 --1.2900e+03 -2.0000e+02 6.0403e-02 --1.2900e+03 -1.7143e+02 4.6172e-03 --1.2900e+03 -1.4286e+02 -4.3208e-02 --1.2900e+03 -1.1429e+02 -8.2769e-02 --1.2900e+03 -8.5714e+01 -1.1381e-01 --1.2900e+03 -5.7143e+01 -1.3613e-01 --1.2900e+03 -2.8571e+01 -1.4959e-01 --1.2900e+03 0.0000e+00 -1.5408e-01 --1.2900e+03 2.8571e+01 -1.4959e-01 --1.2900e+03 5.7143e+01 -1.3613e-01 --1.2900e+03 8.5714e+01 -1.1381e-01 --1.2900e+03 1.1429e+02 -8.2769e-02 --1.2900e+03 1.4286e+02 -4.3208e-02 --1.2900e+03 1.7143e+02 4.6172e-03 --1.2900e+03 2.0000e+02 6.0403e-02 --1.2900e+03 2.2857e+02 1.2380e-01 --1.2900e+03 2.5714e+02 1.9443e-01 --1.2900e+03 2.8571e+02 2.7188e-01 --1.2900e+03 3.1429e+02 3.5570e-01 --1.2900e+03 3.4286e+02 4.4544e-01 --1.2900e+03 3.7143e+02 5.4061e-01 --1.2900e+03 4.0000e+02 6.4073e-01 --1.2900e+03 4.2857e+02 7.4532e-01 --1.2900e+03 4.5714e+02 8.5389e-01 --1.2900e+03 4.8571e+02 9.6596e-01 --1.2900e+03 5.1429e+02 1.0811e+00 --1.2900e+03 5.4286e+02 1.1988e+00 --1.2900e+03 5.7143e+02 1.3186e+00 --1.2900e+03 6.0000e+02 1.4402e+00 --1.2900e+03 6.2857e+02 1.5631e+00 --1.2900e+03 6.5714e+02 1.6870e+00 --1.2900e+03 6.8571e+02 1.8115e+00 --1.2900e+03 7.1429e+02 1.9363e+00 --1.2900e+03 7.4286e+02 2.0611e+00 --1.2900e+03 7.7143e+02 2.1856e+00 --1.2900e+03 8.0000e+02 2.3096e+00 --1.2900e+03 8.2857e+02 2.4329e+00 --1.2900e+03 8.5714e+02 2.5552e+00 --1.2900e+03 8.8571e+02 2.6764e+00 --1.2900e+03 9.1429e+02 2.7962e+00 --1.2900e+03 9.4286e+02 2.9147e+00 --1.2900e+03 9.7143e+02 3.0316e+00 --1.2900e+03 1.0000e+03 3.1469e+00 --1.2900e+03 1.0286e+03 3.2604e+00 --1.2900e+03 1.0571e+03 3.3721e+00 --1.2900e+03 1.0857e+03 3.4819e+00 --1.2900e+03 1.1143e+03 3.5898e+00 --1.2900e+03 1.1429e+03 3.6957e+00 --1.2900e+03 1.1714e+03 3.7996e+00 --1.2900e+03 1.2000e+03 3.9014e+00 --1.2900e+03 1.2286e+03 4.0013e+00 --1.2900e+03 1.2571e+03 4.0991e+00 --1.2900e+03 1.2857e+03 4.1949e+00 --1.2900e+03 1.3143e+03 4.2887e+00 --1.2900e+03 1.3429e+03 4.3805e+00 --1.2900e+03 1.3714e+03 4.4703e+00 --1.2900e+03 1.4000e+03 4.5581e+00 --1.2900e+03 1.4286e+03 4.6440e+00 --1.2900e+03 1.4571e+03 4.7279e+00 --1.2900e+03 1.4857e+03 4.8100e+00 --1.2900e+03 1.5143e+03 4.8902e+00 --1.2900e+03 1.5429e+03 4.9686e+00 --1.2900e+03 1.5714e+03 5.0452e+00 --1.2900e+03 1.6000e+03 5.1200e+00 --1.2900e+03 1.6286e+03 5.1932e+00 --1.2900e+03 1.6571e+03 5.2646e+00 --1.2900e+03 1.6857e+03 5.3345e+00 --1.2900e+03 1.7143e+03 5.4027e+00 --1.2900e+03 1.7429e+03 5.4693e+00 --1.2900e+03 1.7714e+03 5.5345e+00 --1.2900e+03 1.8000e+03 5.5981e+00 --1.2900e+03 1.8286e+03 5.6603e+00 --1.2900e+03 1.8571e+03 5.7211e+00 --1.2900e+03 1.8857e+03 5.7805e+00 --1.2900e+03 1.9143e+03 5.8385e+00 --1.2900e+03 1.9429e+03 5.8952e+00 --1.2900e+03 1.9714e+03 5.9507e+00 --1.2900e+03 2.0000e+03 6.0049e+00 --1.2600e+03 -2.0000e+03 5.9686e+00 --1.2600e+03 -1.9714e+03 5.9130e+00 --1.2600e+03 -1.9429e+03 5.8561e+00 --1.2600e+03 -1.9143e+03 5.7978e+00 --1.2600e+03 -1.8857e+03 5.7382e+00 --1.2600e+03 -1.8571e+03 5.6772e+00 --1.2600e+03 -1.8286e+03 5.6146e+00 --1.2600e+03 -1.8000e+03 5.5506e+00 --1.2600e+03 -1.7714e+03 5.4851e+00 --1.2600e+03 -1.7429e+03 5.4180e+00 --1.2600e+03 -1.7143e+03 5.3492e+00 --1.2600e+03 -1.6857e+03 5.2788e+00 --1.2600e+03 -1.6571e+03 5.2066e+00 --1.2600e+03 -1.6286e+03 5.1327e+00 --1.2600e+03 -1.6000e+03 5.0570e+00 --1.2600e+03 -1.5714e+03 4.9795e+00 --1.2600e+03 -1.5429e+03 4.9001e+00 --1.2600e+03 -1.5143e+03 4.8188e+00 --1.2600e+03 -1.4857e+03 4.7355e+00 --1.2600e+03 -1.4571e+03 4.6501e+00 --1.2600e+03 -1.4286e+03 4.5628e+00 --1.2600e+03 -1.4000e+03 4.4733e+00 --1.2600e+03 -1.3714e+03 4.3817e+00 --1.2600e+03 -1.3429e+03 4.2880e+00 --1.2600e+03 -1.3143e+03 4.1921e+00 --1.2600e+03 -1.2857e+03 4.0939e+00 --1.2600e+03 -1.2571e+03 3.9936e+00 --1.2600e+03 -1.2286e+03 3.8910e+00 --1.2600e+03 -1.2000e+03 3.7861e+00 --1.2600e+03 -1.1714e+03 3.6789e+00 --1.2600e+03 -1.1429e+03 3.5695e+00 --1.2600e+03 -1.1143e+03 3.4578e+00 --1.2600e+03 -1.0857e+03 3.3439e+00 --1.2600e+03 -1.0571e+03 3.2278e+00 --1.2600e+03 -1.0286e+03 3.1095e+00 --1.2600e+03 -1.0000e+03 2.9891e+00 --1.2600e+03 -9.7143e+02 2.8666e+00 --1.2600e+03 -9.4286e+02 2.7422e+00 --1.2600e+03 -9.1429e+02 2.6160e+00 --1.2600e+03 -8.8571e+02 2.4880e+00 --1.2600e+03 -8.5714e+02 2.3584e+00 --1.2600e+03 -8.2857e+02 2.2274e+00 --1.2600e+03 -8.0000e+02 2.0951e+00 --1.2600e+03 -7.7143e+02 1.9619e+00 --1.2600e+03 -7.4286e+02 1.8278e+00 --1.2600e+03 -7.1429e+02 1.6931e+00 --1.2600e+03 -6.8571e+02 1.5583e+00 --1.2600e+03 -6.5714e+02 1.4235e+00 --1.2600e+03 -6.2857e+02 1.2892e+00 --1.2600e+03 -6.0000e+02 1.1556e+00 --1.2600e+03 -5.7143e+02 1.0233e+00 --1.2600e+03 -5.4286e+02 8.9262e-01 --1.2600e+03 -5.1429e+02 7.6407e-01 --1.2600e+03 -4.8571e+02 6.3812e-01 --1.2600e+03 -4.5714e+02 5.1529e-01 --1.2600e+03 -4.2857e+02 3.9610e-01 --1.2600e+03 -4.0000e+02 2.8110e-01 --1.2600e+03 -3.7143e+02 1.7084e-01 --1.2600e+03 -3.4286e+02 6.5885e-02 --1.2600e+03 -3.1429e+02 -3.3215e-02 --1.2600e+03 -2.8571e+02 -1.2591e-01 --1.2600e+03 -2.5714e+02 -2.1165e-01 --1.2600e+03 -2.2857e+02 -2.8994e-01 --1.2600e+03 -2.0000e+02 -3.6029e-01 --1.2600e+03 -1.7143e+02 -4.2224e-01 --1.2600e+03 -1.4286e+02 -4.7540e-01 --1.2600e+03 -1.1429e+02 -5.1940e-01 --1.2600e+03 -8.5714e+01 -5.5394e-01 --1.2600e+03 -5.7143e+01 -5.7879e-01 --1.2600e+03 -2.8571e+01 -5.9377e-01 --1.2600e+03 0.0000e+00 -5.9877e-01 --1.2600e+03 2.8571e+01 -5.9377e-01 --1.2600e+03 5.7143e+01 -5.7879e-01 --1.2600e+03 8.5714e+01 -5.5394e-01 --1.2600e+03 1.1429e+02 -5.1940e-01 --1.2600e+03 1.4286e+02 -4.7540e-01 --1.2600e+03 1.7143e+02 -4.2224e-01 --1.2600e+03 2.0000e+02 -3.6029e-01 --1.2600e+03 2.2857e+02 -2.8994e-01 --1.2600e+03 2.5714e+02 -2.1165e-01 --1.2600e+03 2.8571e+02 -1.2591e-01 --1.2600e+03 3.1429e+02 -3.3215e-02 --1.2600e+03 3.4286e+02 6.5885e-02 --1.2600e+03 3.7143e+02 1.7084e-01 --1.2600e+03 4.0000e+02 2.8110e-01 --1.2600e+03 4.2857e+02 3.9610e-01 --1.2600e+03 4.5714e+02 5.1529e-01 --1.2600e+03 4.8571e+02 6.3812e-01 --1.2600e+03 5.1429e+02 7.6407e-01 --1.2600e+03 5.4286e+02 8.9262e-01 --1.2600e+03 5.7143e+02 1.0233e+00 --1.2600e+03 6.0000e+02 1.1556e+00 --1.2600e+03 6.2857e+02 1.2892e+00 --1.2600e+03 6.5714e+02 1.4235e+00 --1.2600e+03 6.8571e+02 1.5583e+00 --1.2600e+03 7.1429e+02 1.6931e+00 --1.2600e+03 7.4286e+02 1.8278e+00 --1.2600e+03 7.7143e+02 1.9619e+00 --1.2600e+03 8.0000e+02 2.0951e+00 --1.2600e+03 8.2857e+02 2.2274e+00 --1.2600e+03 8.5714e+02 2.3584e+00 --1.2600e+03 8.8571e+02 2.4880e+00 --1.2600e+03 9.1429e+02 2.6160e+00 --1.2600e+03 9.4286e+02 2.7422e+00 --1.2600e+03 9.7143e+02 2.8666e+00 --1.2600e+03 1.0000e+03 2.9891e+00 --1.2600e+03 1.0286e+03 3.1095e+00 --1.2600e+03 1.0571e+03 3.2278e+00 --1.2600e+03 1.0857e+03 3.3439e+00 --1.2600e+03 1.1143e+03 3.4578e+00 --1.2600e+03 1.1429e+03 3.5695e+00 --1.2600e+03 1.1714e+03 3.6789e+00 --1.2600e+03 1.2000e+03 3.7861e+00 --1.2600e+03 1.2286e+03 3.8910e+00 --1.2600e+03 1.2571e+03 3.9936e+00 --1.2600e+03 1.2857e+03 4.0939e+00 --1.2600e+03 1.3143e+03 4.1921e+00 --1.2600e+03 1.3429e+03 4.2880e+00 --1.2600e+03 1.3714e+03 4.3817e+00 --1.2600e+03 1.4000e+03 4.4733e+00 --1.2600e+03 1.4286e+03 4.5628e+00 --1.2600e+03 1.4571e+03 4.6501e+00 --1.2600e+03 1.4857e+03 4.7355e+00 --1.2600e+03 1.5143e+03 4.8188e+00 --1.2600e+03 1.5429e+03 4.9001e+00 --1.2600e+03 1.5714e+03 4.9795e+00 --1.2600e+03 1.6000e+03 5.0570e+00 --1.2600e+03 1.6286e+03 5.1327e+00 --1.2600e+03 1.6571e+03 5.2066e+00 --1.2600e+03 1.6857e+03 5.2788e+00 --1.2600e+03 1.7143e+03 5.3492e+00 --1.2600e+03 1.7429e+03 5.4180e+00 --1.2600e+03 1.7714e+03 5.4851e+00 --1.2600e+03 1.8000e+03 5.5506e+00 --1.2600e+03 1.8286e+03 5.6146e+00 --1.2600e+03 1.8571e+03 5.6772e+00 --1.2600e+03 1.8857e+03 5.7382e+00 --1.2600e+03 1.9143e+03 5.7978e+00 --1.2600e+03 1.9429e+03 5.8561e+00 --1.2600e+03 1.9714e+03 5.9130e+00 --1.2600e+03 2.0000e+03 5.9686e+00 --1.2300e+03 -2.0000e+03 5.9322e+00 --1.2300e+03 -1.9714e+03 5.8752e+00 --1.2300e+03 -1.9429e+03 5.8168e+00 --1.2300e+03 -1.9143e+03 5.7571e+00 --1.2300e+03 -1.8857e+03 5.6958e+00 --1.2300e+03 -1.8571e+03 5.6331e+00 --1.2300e+03 -1.8286e+03 5.5688e+00 --1.2300e+03 -1.8000e+03 5.5029e+00 --1.2300e+03 -1.7714e+03 5.4354e+00 --1.2300e+03 -1.7429e+03 5.3662e+00 --1.2300e+03 -1.7143e+03 5.2953e+00 --1.2300e+03 -1.6857e+03 5.2226e+00 --1.2300e+03 -1.6571e+03 5.1481e+00 --1.2300e+03 -1.6286e+03 5.0717e+00 --1.2300e+03 -1.6000e+03 4.9934e+00 --1.2300e+03 -1.5714e+03 4.9131e+00 --1.2300e+03 -1.5429e+03 4.8308e+00 --1.2300e+03 -1.5143e+03 4.7464e+00 --1.2300e+03 -1.4857e+03 4.6598e+00 --1.2300e+03 -1.4571e+03 4.5711e+00 --1.2300e+03 -1.4286e+03 4.4802e+00 --1.2300e+03 -1.4000e+03 4.3870e+00 --1.2300e+03 -1.3714e+03 4.2915e+00 --1.2300e+03 -1.3429e+03 4.1937e+00 --1.2300e+03 -1.3143e+03 4.0934e+00 --1.2300e+03 -1.2857e+03 3.9907e+00 --1.2300e+03 -1.2571e+03 3.8855e+00 --1.2300e+03 -1.2286e+03 3.7779e+00 --1.2300e+03 -1.2000e+03 3.6677e+00 --1.2300e+03 -1.1714e+03 3.5549e+00 --1.2300e+03 -1.1429e+03 3.4397e+00 --1.2300e+03 -1.1143e+03 3.3219e+00 --1.2300e+03 -1.0857e+03 3.2015e+00 --1.2300e+03 -1.0571e+03 3.0786e+00 --1.2300e+03 -1.0286e+03 2.9533e+00 --1.2300e+03 -1.0000e+03 2.8255e+00 --1.2300e+03 -9.7143e+02 2.6953e+00 --1.2300e+03 -9.4286e+02 2.5628e+00 --1.2300e+03 -9.1429e+02 2.4282e+00 --1.2300e+03 -8.8571e+02 2.2914e+00 --1.2300e+03 -8.5714e+02 2.1527e+00 --1.2300e+03 -8.2857e+02 2.0122e+00 --1.2300e+03 -8.0000e+02 1.8701e+00 --1.2300e+03 -7.7143e+02 1.7267e+00 --1.2300e+03 -7.4286e+02 1.5821e+00 --1.2300e+03 -7.1429e+02 1.4366e+00 --1.2300e+03 -6.8571e+02 1.2907e+00 --1.2300e+03 -6.5714e+02 1.1445e+00 --1.2300e+03 -6.2857e+02 9.9851e-01 --1.2300e+03 -6.0000e+02 8.5313e-01 --1.2300e+03 -5.7143e+02 7.0880e-01 --1.2300e+03 -5.4286e+02 5.6600e-01 --1.2300e+03 -5.1429e+02 4.2526e-01 --1.2300e+03 -4.8571e+02 2.8711e-01 --1.2300e+03 -4.5714e+02 1.5213e-01 --1.2300e+03 -4.2857e+02 2.0919e-02 --1.2300e+03 -4.0000e+02 -1.0590e-01 --1.2300e+03 -3.7143e+02 -2.2768e-01 --1.2300e+03 -3.4286e+02 -3.4380e-01 --1.2300e+03 -3.1429e+02 -4.5361e-01 --1.2300e+03 -2.8571e+02 -5.5646e-01 --1.2300e+03 -2.5714e+02 -6.5173e-01 --1.2300e+03 -2.2857e+02 -7.3882e-01 --1.2300e+03 -2.0000e+02 -8.1716e-01 --1.2300e+03 -1.7143e+02 -8.8622e-01 --1.2300e+03 -1.4286e+02 -9.4552e-01 --1.2300e+03 -1.1429e+02 -9.9465e-01 --1.2300e+03 -8.5714e+01 -1.0332e+00 --1.2300e+03 -5.7143e+01 -1.0610e+00 --1.2300e+03 -2.8571e+01 -1.0778e+00 --1.2300e+03 0.0000e+00 -1.0834e+00 --1.2300e+03 2.8571e+01 -1.0778e+00 --1.2300e+03 5.7143e+01 -1.0610e+00 --1.2300e+03 8.5714e+01 -1.0332e+00 --1.2300e+03 1.1429e+02 -9.9465e-01 --1.2300e+03 1.4286e+02 -9.4552e-01 --1.2300e+03 1.7143e+02 -8.8622e-01 --1.2300e+03 2.0000e+02 -8.1716e-01 --1.2300e+03 2.2857e+02 -7.3882e-01 --1.2300e+03 2.5714e+02 -6.5173e-01 --1.2300e+03 2.8571e+02 -5.5646e-01 --1.2300e+03 3.1429e+02 -4.5361e-01 --1.2300e+03 3.4286e+02 -3.4380e-01 --1.2300e+03 3.7143e+02 -2.2768e-01 --1.2300e+03 4.0000e+02 -1.0590e-01 --1.2300e+03 4.2857e+02 2.0919e-02 --1.2300e+03 4.5714e+02 1.5213e-01 --1.2300e+03 4.8571e+02 2.8711e-01 --1.2300e+03 5.1429e+02 4.2526e-01 --1.2300e+03 5.4286e+02 5.6600e-01 --1.2300e+03 5.7143e+02 7.0880e-01 --1.2300e+03 6.0000e+02 8.5313e-01 --1.2300e+03 6.2857e+02 9.9851e-01 --1.2300e+03 6.5714e+02 1.1445e+00 --1.2300e+03 6.8571e+02 1.2907e+00 --1.2300e+03 7.1429e+02 1.4366e+00 --1.2300e+03 7.4286e+02 1.5821e+00 --1.2300e+03 7.7143e+02 1.7267e+00 --1.2300e+03 8.0000e+02 1.8701e+00 --1.2300e+03 8.2857e+02 2.0122e+00 --1.2300e+03 8.5714e+02 2.1527e+00 --1.2300e+03 8.8571e+02 2.2914e+00 --1.2300e+03 9.1429e+02 2.4282e+00 --1.2300e+03 9.4286e+02 2.5628e+00 --1.2300e+03 9.7143e+02 2.6953e+00 --1.2300e+03 1.0000e+03 2.8255e+00 --1.2300e+03 1.0286e+03 2.9533e+00 --1.2300e+03 1.0571e+03 3.0786e+00 --1.2300e+03 1.0857e+03 3.2015e+00 --1.2300e+03 1.1143e+03 3.3219e+00 --1.2300e+03 1.1429e+03 3.4397e+00 --1.2300e+03 1.1714e+03 3.5549e+00 --1.2300e+03 1.2000e+03 3.6677e+00 --1.2300e+03 1.2286e+03 3.7779e+00 --1.2300e+03 1.2571e+03 3.8855e+00 --1.2300e+03 1.2857e+03 3.9907e+00 --1.2300e+03 1.3143e+03 4.0934e+00 --1.2300e+03 1.3429e+03 4.1937e+00 --1.2300e+03 1.3714e+03 4.2915e+00 --1.2300e+03 1.4000e+03 4.3870e+00 --1.2300e+03 1.4286e+03 4.4802e+00 --1.2300e+03 1.4571e+03 4.5711e+00 --1.2300e+03 1.4857e+03 4.6598e+00 --1.2300e+03 1.5143e+03 4.7464e+00 --1.2300e+03 1.5429e+03 4.8308e+00 --1.2300e+03 1.5714e+03 4.9131e+00 --1.2300e+03 1.6000e+03 4.9934e+00 --1.2300e+03 1.6286e+03 5.0717e+00 --1.2300e+03 1.6571e+03 5.1481e+00 --1.2300e+03 1.6857e+03 5.2226e+00 --1.2300e+03 1.7143e+03 5.2953e+00 --1.2300e+03 1.7429e+03 5.3662e+00 --1.2300e+03 1.7714e+03 5.4354e+00 --1.2300e+03 1.8000e+03 5.5029e+00 --1.2300e+03 1.8286e+03 5.5688e+00 --1.2300e+03 1.8571e+03 5.6331e+00 --1.2300e+03 1.8857e+03 5.6958e+00 --1.2300e+03 1.9143e+03 5.7571e+00 --1.2300e+03 1.9429e+03 5.8168e+00 --1.2300e+03 1.9714e+03 5.8752e+00 --1.2300e+03 2.0000e+03 5.9322e+00 --1.2000e+03 -2.0000e+03 5.8958e+00 --1.2000e+03 -1.9714e+03 5.8374e+00 --1.2000e+03 -1.9429e+03 5.7776e+00 --1.2000e+03 -1.9143e+03 5.7162e+00 --1.2000e+03 -1.8857e+03 5.6533e+00 --1.2000e+03 -1.8571e+03 5.5888e+00 --1.2000e+03 -1.8286e+03 5.5227e+00 --1.2000e+03 -1.8000e+03 5.4550e+00 --1.2000e+03 -1.7714e+03 5.3854e+00 --1.2000e+03 -1.7429e+03 5.3142e+00 --1.2000e+03 -1.7143e+03 5.2410e+00 --1.2000e+03 -1.6857e+03 5.1660e+00 --1.2000e+03 -1.6571e+03 5.0890e+00 --1.2000e+03 -1.6286e+03 5.0101e+00 --1.2000e+03 -1.6000e+03 4.9290e+00 --1.2000e+03 -1.5714e+03 4.8459e+00 --1.2000e+03 -1.5429e+03 4.7606e+00 --1.2000e+03 -1.5143e+03 4.6730e+00 --1.2000e+03 -1.4857e+03 4.5832e+00 --1.2000e+03 -1.4571e+03 4.4910e+00 --1.2000e+03 -1.4286e+03 4.3963e+00 --1.2000e+03 -1.4000e+03 4.2993e+00 --1.2000e+03 -1.3714e+03 4.1996e+00 --1.2000e+03 -1.3429e+03 4.0975e+00 --1.2000e+03 -1.3143e+03 3.9926e+00 --1.2000e+03 -1.2857e+03 3.8852e+00 --1.2000e+03 -1.2571e+03 3.7749e+00 --1.2000e+03 -1.2286e+03 3.6619e+00 --1.2000e+03 -1.2000e+03 3.5462e+00 --1.2000e+03 -1.1714e+03 3.4275e+00 --1.2000e+03 -1.1429e+03 3.3061e+00 --1.2000e+03 -1.1143e+03 3.1817e+00 --1.2000e+03 -1.0857e+03 3.0545e+00 --1.2000e+03 -1.0571e+03 2.9245e+00 --1.2000e+03 -1.0286e+03 2.7915e+00 --1.2000e+03 -1.0000e+03 2.6558e+00 --1.2000e+03 -9.7143e+02 2.5173e+00 --1.2000e+03 -9.4286e+02 2.3761e+00 --1.2000e+03 -9.1429e+02 2.2324e+00 --1.2000e+03 -8.8571e+02 2.0861e+00 --1.2000e+03 -8.5714e+02 1.9375e+00 --1.2000e+03 -8.2857e+02 1.7866e+00 --1.2000e+03 -8.0000e+02 1.6338e+00 --1.2000e+03 -7.7143e+02 1.4792e+00 --1.2000e+03 -7.4286e+02 1.3231e+00 --1.2000e+03 -7.1429e+02 1.1657e+00 --1.2000e+03 -6.8571e+02 1.0074e+00 --1.2000e+03 -6.5714e+02 8.4863e-01 --1.2000e+03 -6.2857e+02 6.8971e-01 --1.2000e+03 -6.0000e+02 5.3111e-01 --1.2000e+03 -5.7143e+02 3.7334e-01 --1.2000e+03 -5.4286e+02 2.1692e-01 --1.2000e+03 -5.1429e+02 6.2437e-02 --1.2000e+03 -4.8571e+02 -8.9501e-02 --1.2000e+03 -4.5714e+02 -2.3824e-01 --1.2000e+03 -4.2857e+02 -3.8310e-01 --1.2000e+03 -4.0000e+02 -5.2338e-01 --1.2000e+03 -3.7143e+02 -6.5834e-01 --1.2000e+03 -3.4286e+02 -7.8723e-01 --1.2000e+03 -3.1429e+02 -9.0932e-01 --1.2000e+03 -2.8571e+02 -1.0239e+00 --1.2000e+03 -2.5714e+02 -1.1301e+00 --1.2000e+03 -2.2857e+02 -1.2274e+00 --1.2000e+03 -2.0000e+02 -1.3149e+00 --1.2000e+03 -1.7143e+02 -1.3922e+00 --1.2000e+03 -1.4286e+02 -1.4587e+00 --1.2000e+03 -1.1429e+02 -1.5138e+00 --1.2000e+03 -8.5714e+01 -1.5571e+00 --1.2000e+03 -5.7143e+01 -1.5882e+00 --1.2000e+03 -2.8571e+01 -1.6070e+00 --1.2000e+03 0.0000e+00 -1.6133e+00 --1.2000e+03 2.8571e+01 -1.6070e+00 --1.2000e+03 5.7143e+01 -1.5882e+00 --1.2000e+03 8.5714e+01 -1.5571e+00 --1.2000e+03 1.1429e+02 -1.5138e+00 --1.2000e+03 1.4286e+02 -1.4587e+00 --1.2000e+03 1.7143e+02 -1.3922e+00 --1.2000e+03 2.0000e+02 -1.3149e+00 --1.2000e+03 2.2857e+02 -1.2274e+00 --1.2000e+03 2.5714e+02 -1.1301e+00 --1.2000e+03 2.8571e+02 -1.0239e+00 --1.2000e+03 3.1429e+02 -9.0932e-01 --1.2000e+03 3.4286e+02 -7.8723e-01 --1.2000e+03 3.7143e+02 -6.5834e-01 --1.2000e+03 4.0000e+02 -5.2338e-01 --1.2000e+03 4.2857e+02 -3.8310e-01 --1.2000e+03 4.5714e+02 -2.3824e-01 --1.2000e+03 4.8571e+02 -8.9501e-02 --1.2000e+03 5.1429e+02 6.2437e-02 --1.2000e+03 5.4286e+02 2.1692e-01 --1.2000e+03 5.7143e+02 3.7334e-01 --1.2000e+03 6.0000e+02 5.3111e-01 --1.2000e+03 6.2857e+02 6.8971e-01 --1.2000e+03 6.5714e+02 8.4863e-01 --1.2000e+03 6.8571e+02 1.0074e+00 --1.2000e+03 7.1429e+02 1.1657e+00 --1.2000e+03 7.4286e+02 1.3231e+00 --1.2000e+03 7.7143e+02 1.4792e+00 --1.2000e+03 8.0000e+02 1.6338e+00 --1.2000e+03 8.2857e+02 1.7866e+00 --1.2000e+03 8.5714e+02 1.9375e+00 --1.2000e+03 8.8571e+02 2.0861e+00 --1.2000e+03 9.1429e+02 2.2324e+00 --1.2000e+03 9.4286e+02 2.3761e+00 --1.2000e+03 9.7143e+02 2.5173e+00 --1.2000e+03 1.0000e+03 2.6558e+00 --1.2000e+03 1.0286e+03 2.7915e+00 --1.2000e+03 1.0571e+03 2.9245e+00 --1.2000e+03 1.0857e+03 3.0545e+00 --1.2000e+03 1.1143e+03 3.1817e+00 --1.2000e+03 1.1429e+03 3.3061e+00 --1.2000e+03 1.1714e+03 3.4275e+00 --1.2000e+03 1.2000e+03 3.5462e+00 --1.2000e+03 1.2286e+03 3.6619e+00 --1.2000e+03 1.2571e+03 3.7749e+00 --1.2000e+03 1.2857e+03 3.8852e+00 --1.2000e+03 1.3143e+03 3.9926e+00 --1.2000e+03 1.3429e+03 4.0975e+00 --1.2000e+03 1.3714e+03 4.1996e+00 --1.2000e+03 1.4000e+03 4.2993e+00 --1.2000e+03 1.4286e+03 4.3963e+00 --1.2000e+03 1.4571e+03 4.4910e+00 --1.2000e+03 1.4857e+03 4.5832e+00 --1.2000e+03 1.5143e+03 4.6730e+00 --1.2000e+03 1.5429e+03 4.7606e+00 --1.2000e+03 1.5714e+03 4.8459e+00 --1.2000e+03 1.6000e+03 4.9290e+00 --1.2000e+03 1.6286e+03 5.0101e+00 --1.2000e+03 1.6571e+03 5.0890e+00 --1.2000e+03 1.6857e+03 5.1660e+00 --1.2000e+03 1.7143e+03 5.2410e+00 --1.2000e+03 1.7429e+03 5.3142e+00 --1.2000e+03 1.7714e+03 5.3854e+00 --1.2000e+03 1.8000e+03 5.4550e+00 --1.2000e+03 1.8286e+03 5.5227e+00 --1.2000e+03 1.8571e+03 5.5888e+00 --1.2000e+03 1.8857e+03 5.6533e+00 --1.2000e+03 1.9143e+03 5.7162e+00 --1.2000e+03 1.9429e+03 5.7776e+00 --1.2000e+03 1.9714e+03 5.8374e+00 --1.2000e+03 2.0000e+03 5.8958e+00 --1.1700e+03 -2.0000e+03 5.8595e+00 --1.1700e+03 -1.9714e+03 5.7996e+00 --1.1700e+03 -1.9429e+03 5.7382e+00 --1.1700e+03 -1.9143e+03 5.6753e+00 --1.1700e+03 -1.8857e+03 5.6107e+00 --1.1700e+03 -1.8571e+03 5.5445e+00 --1.1700e+03 -1.8286e+03 5.4766e+00 --1.1700e+03 -1.8000e+03 5.4068e+00 --1.1700e+03 -1.7714e+03 5.3353e+00 --1.1700e+03 -1.7429e+03 5.2618e+00 --1.1700e+03 -1.7143e+03 5.1864e+00 --1.1700e+03 -1.6857e+03 5.1090e+00 --1.1700e+03 -1.6571e+03 5.0295e+00 --1.1700e+03 -1.6286e+03 4.9479e+00 --1.1700e+03 -1.6000e+03 4.8641e+00 --1.1700e+03 -1.5714e+03 4.7780e+00 --1.1700e+03 -1.5429e+03 4.6896e+00 --1.1700e+03 -1.5143e+03 4.5988e+00 --1.1700e+03 -1.4857e+03 4.5055e+00 --1.1700e+03 -1.4571e+03 4.4096e+00 --1.1700e+03 -1.4286e+03 4.3112e+00 --1.1700e+03 -1.4000e+03 4.2100e+00 --1.1700e+03 -1.3714e+03 4.1061e+00 --1.1700e+03 -1.3429e+03 3.9994e+00 --1.1700e+03 -1.3143e+03 3.8899e+00 --1.1700e+03 -1.2857e+03 3.7773e+00 --1.1700e+03 -1.2571e+03 3.6618e+00 --1.1700e+03 -1.2286e+03 3.5432e+00 --1.1700e+03 -1.2000e+03 3.4215e+00 --1.1700e+03 -1.1714e+03 3.2967e+00 --1.1700e+03 -1.1429e+03 3.1686e+00 --1.1700e+03 -1.1143e+03 3.0374e+00 --1.1700e+03 -1.0857e+03 2.9029e+00 --1.1700e+03 -1.0571e+03 2.7651e+00 --1.1700e+03 -1.0286e+03 2.6241e+00 --1.1700e+03 -1.0000e+03 2.4799e+00 --1.1700e+03 -9.7143e+02 2.3324e+00 --1.1700e+03 -9.4286e+02 2.1819e+00 --1.1700e+03 -9.1429e+02 2.0282e+00 --1.1700e+03 -8.8571e+02 1.8716e+00 --1.1700e+03 -8.5714e+02 1.7122e+00 --1.1700e+03 -8.2857e+02 1.5501e+00 --1.1700e+03 -8.0000e+02 1.3855e+00 --1.1700e+03 -7.7143e+02 1.2187e+00 --1.1700e+03 -7.4286e+02 1.0498e+00 --1.1700e+03 -7.1429e+02 8.7926e-01 --1.1700e+03 -6.8571e+02 7.0736e-01 --1.1700e+03 -6.5714e+02 5.3451e-01 --1.1700e+03 -6.2857e+02 3.6115e-01 --1.1700e+03 -6.0000e+02 1.8776e-01 --1.1700e+03 -5.7143e+02 1.4889e-02 --1.1700e+03 -5.4286e+02 -1.5687e-01 --1.1700e+03 -5.1429e+02 -3.2688e-01 --1.1700e+03 -4.8571e+02 -4.9444e-01 --1.1700e+03 -4.5714e+02 -6.5883e-01 --1.1700e+03 -4.2857e+02 -8.1927e-01 --1.1700e+03 -4.0000e+02 -9.7493e-01 --1.1700e+03 -3.7143e+02 -1.1250e+00 --1.1700e+03 -3.4286e+02 -1.2686e+00 --1.1700e+03 -3.1429e+02 -1.4048e+00 --1.1700e+03 -2.8571e+02 -1.5329e+00 --1.1700e+03 -2.5714e+02 -1.6518e+00 --1.1700e+03 -2.2857e+02 -1.7609e+00 --1.1700e+03 -2.0000e+02 -1.8592e+00 --1.1700e+03 -1.7143e+02 -1.9461e+00 --1.1700e+03 -1.4286e+02 -2.0209e+00 --1.1700e+03 -1.1429e+02 -2.0829e+00 --1.1700e+03 -8.5714e+01 -2.1317e+00 --1.1700e+03 -5.7143e+01 -2.1669e+00 --1.1700e+03 -2.8571e+01 -2.1881e+00 --1.1700e+03 0.0000e+00 -2.1952e+00 --1.1700e+03 2.8571e+01 -2.1881e+00 --1.1700e+03 5.7143e+01 -2.1669e+00 --1.1700e+03 8.5714e+01 -2.1317e+00 --1.1700e+03 1.1429e+02 -2.0829e+00 --1.1700e+03 1.4286e+02 -2.0209e+00 --1.1700e+03 1.7143e+02 -1.9461e+00 --1.1700e+03 2.0000e+02 -1.8592e+00 --1.1700e+03 2.2857e+02 -1.7609e+00 --1.1700e+03 2.5714e+02 -1.6518e+00 --1.1700e+03 2.8571e+02 -1.5329e+00 --1.1700e+03 3.1429e+02 -1.4048e+00 --1.1700e+03 3.4286e+02 -1.2686e+00 --1.1700e+03 3.7143e+02 -1.1250e+00 --1.1700e+03 4.0000e+02 -9.7493e-01 --1.1700e+03 4.2857e+02 -8.1927e-01 --1.1700e+03 4.5714e+02 -6.5883e-01 --1.1700e+03 4.8571e+02 -4.9444e-01 --1.1700e+03 5.1429e+02 -3.2688e-01 --1.1700e+03 5.4286e+02 -1.5687e-01 --1.1700e+03 5.7143e+02 1.4889e-02 --1.1700e+03 6.0000e+02 1.8776e-01 --1.1700e+03 6.2857e+02 3.6115e-01 --1.1700e+03 6.5714e+02 5.3451e-01 --1.1700e+03 6.8571e+02 7.0736e-01 --1.1700e+03 7.1429e+02 8.7926e-01 --1.1700e+03 7.4286e+02 1.0498e+00 --1.1700e+03 7.7143e+02 1.2187e+00 --1.1700e+03 8.0000e+02 1.3855e+00 --1.1700e+03 8.2857e+02 1.5501e+00 --1.1700e+03 8.5714e+02 1.7122e+00 --1.1700e+03 8.8571e+02 1.8716e+00 --1.1700e+03 9.1429e+02 2.0282e+00 --1.1700e+03 9.4286e+02 2.1819e+00 --1.1700e+03 9.7143e+02 2.3324e+00 --1.1700e+03 1.0000e+03 2.4799e+00 --1.1700e+03 1.0286e+03 2.6241e+00 --1.1700e+03 1.0571e+03 2.7651e+00 --1.1700e+03 1.0857e+03 2.9029e+00 --1.1700e+03 1.1143e+03 3.0374e+00 --1.1700e+03 1.1429e+03 3.1686e+00 --1.1700e+03 1.1714e+03 3.2967e+00 --1.1700e+03 1.2000e+03 3.4215e+00 --1.1700e+03 1.2286e+03 3.5432e+00 --1.1700e+03 1.2571e+03 3.6618e+00 --1.1700e+03 1.2857e+03 3.7773e+00 --1.1700e+03 1.3143e+03 3.8899e+00 --1.1700e+03 1.3429e+03 3.9994e+00 --1.1700e+03 1.3714e+03 4.1061e+00 --1.1700e+03 1.4000e+03 4.2100e+00 --1.1700e+03 1.4286e+03 4.3112e+00 --1.1700e+03 1.4571e+03 4.4096e+00 --1.1700e+03 1.4857e+03 4.5055e+00 --1.1700e+03 1.5143e+03 4.5988e+00 --1.1700e+03 1.5429e+03 4.6896e+00 --1.1700e+03 1.5714e+03 4.7780e+00 --1.1700e+03 1.6000e+03 4.8641e+00 --1.1700e+03 1.6286e+03 4.9479e+00 --1.1700e+03 1.6571e+03 5.0295e+00 --1.1700e+03 1.6857e+03 5.1090e+00 --1.1700e+03 1.7143e+03 5.1864e+00 --1.1700e+03 1.7429e+03 5.2618e+00 --1.1700e+03 1.7714e+03 5.3353e+00 --1.1700e+03 1.8000e+03 5.4068e+00 --1.1700e+03 1.8286e+03 5.4766e+00 --1.1700e+03 1.8571e+03 5.5445e+00 --1.1700e+03 1.8857e+03 5.6107e+00 --1.1700e+03 1.9143e+03 5.6753e+00 --1.1700e+03 1.9429e+03 5.7382e+00 --1.1700e+03 1.9714e+03 5.7996e+00 --1.1700e+03 2.0000e+03 5.8595e+00 --1.1400e+03 -2.0000e+03 5.8232e+00 --1.1400e+03 -1.9714e+03 5.7619e+00 --1.1400e+03 -1.9429e+03 5.6990e+00 --1.1400e+03 -1.9143e+03 5.6344e+00 --1.1400e+03 -1.8857e+03 5.5681e+00 --1.1400e+03 -1.8571e+03 5.5001e+00 --1.1400e+03 -1.8286e+03 5.4303e+00 --1.1400e+03 -1.8000e+03 5.3586e+00 --1.1400e+03 -1.7714e+03 5.2849e+00 --1.1400e+03 -1.7429e+03 5.2093e+00 --1.1400e+03 -1.7143e+03 5.1316e+00 --1.1400e+03 -1.6857e+03 5.0517e+00 --1.1400e+03 -1.6571e+03 4.9696e+00 --1.1400e+03 -1.6286e+03 4.8853e+00 --1.1400e+03 -1.6000e+03 4.7986e+00 --1.1400e+03 -1.5714e+03 4.7095e+00 --1.1400e+03 -1.5429e+03 4.6179e+00 --1.1400e+03 -1.5143e+03 4.5237e+00 --1.1400e+03 -1.4857e+03 4.4268e+00 --1.1400e+03 -1.4571e+03 4.3272e+00 --1.1400e+03 -1.4286e+03 4.2247e+00 --1.1400e+03 -1.4000e+03 4.1194e+00 --1.1400e+03 -1.3714e+03 4.0110e+00 --1.1400e+03 -1.3429e+03 3.8996e+00 --1.1400e+03 -1.3143e+03 3.7851e+00 --1.1400e+03 -1.2857e+03 3.6673e+00 --1.1400e+03 -1.2571e+03 3.5462e+00 --1.1400e+03 -1.2286e+03 3.4217e+00 --1.1400e+03 -1.2000e+03 3.2938e+00 --1.1400e+03 -1.1714e+03 3.1623e+00 --1.1400e+03 -1.1429e+03 3.0273e+00 --1.1400e+03 -1.1143e+03 2.8887e+00 --1.1400e+03 -1.0857e+03 2.7464e+00 --1.1400e+03 -1.0571e+03 2.6004e+00 --1.1400e+03 -1.0286e+03 2.4508e+00 --1.1400e+03 -1.0000e+03 2.2974e+00 --1.1400e+03 -9.7143e+02 2.1403e+00 --1.1400e+03 -9.4286e+02 1.9796e+00 --1.1400e+03 -9.1429e+02 1.8153e+00 --1.1400e+03 -8.8571e+02 1.6475e+00 --1.1400e+03 -8.5714e+02 1.4764e+00 --1.1400e+03 -8.2857e+02 1.3019e+00 --1.1400e+03 -8.0000e+02 1.1244e+00 --1.1400e+03 -7.7143e+02 9.4412e-01 --1.1400e+03 -7.4286e+02 7.6124e-01 --1.1400e+03 -7.1429e+02 5.7610e-01 --1.1400e+03 -6.8571e+02 3.8906e-01 --1.1400e+03 -6.5714e+02 2.0055e-01 --1.1400e+03 -6.2857e+02 1.1040e-02 --1.1400e+03 -6.0000e+02 -1.7895e-01 --1.1400e+03 -5.7143e+02 -3.6881e-01 --1.1400e+03 -5.4286e+02 -5.5791e-01 --1.1400e+03 -5.1429e+02 -7.4551e-01 --1.1400e+03 -4.8571e+02 -9.3085e-01 --1.1400e+03 -4.5714e+02 -1.1131e+00 --1.1400e+03 -4.2857e+02 -1.2914e+00 --1.1400e+03 -4.0000e+02 -1.4647e+00 --1.1400e+03 -3.7143e+02 -1.6322e+00 --1.1400e+03 -3.4286e+02 -1.7927e+00 --1.1400e+03 -3.1429e+02 -1.9454e+00 --1.1400e+03 -2.8571e+02 -2.0891e+00 --1.1400e+03 -2.5714e+02 -2.2229e+00 --1.1400e+03 -2.2857e+02 -2.3457e+00 --1.1400e+03 -2.0000e+02 -2.4566e+00 --1.1400e+03 -1.7143e+02 -2.5548e+00 --1.1400e+03 -1.4286e+02 -2.6393e+00 --1.1400e+03 -1.1429e+02 -2.7095e+00 --1.1400e+03 -8.5714e+01 -2.7648e+00 --1.1400e+03 -5.7143e+01 -2.8046e+00 --1.1400e+03 -2.8571e+01 -2.8287e+00 --1.1400e+03 0.0000e+00 -2.8367e+00 --1.1400e+03 2.8571e+01 -2.8287e+00 --1.1400e+03 5.7143e+01 -2.8046e+00 --1.1400e+03 8.5714e+01 -2.7648e+00 --1.1400e+03 1.1429e+02 -2.7095e+00 --1.1400e+03 1.4286e+02 -2.6393e+00 --1.1400e+03 1.7143e+02 -2.5548e+00 --1.1400e+03 2.0000e+02 -2.4566e+00 --1.1400e+03 2.2857e+02 -2.3457e+00 --1.1400e+03 2.5714e+02 -2.2229e+00 --1.1400e+03 2.8571e+02 -2.0891e+00 --1.1400e+03 3.1429e+02 -1.9454e+00 --1.1400e+03 3.4286e+02 -1.7927e+00 --1.1400e+03 3.7143e+02 -1.6322e+00 --1.1400e+03 4.0000e+02 -1.4647e+00 --1.1400e+03 4.2857e+02 -1.2914e+00 --1.1400e+03 4.5714e+02 -1.1131e+00 --1.1400e+03 4.8571e+02 -9.3085e-01 --1.1400e+03 5.1429e+02 -7.4551e-01 --1.1400e+03 5.4286e+02 -5.5791e-01 --1.1400e+03 5.7143e+02 -3.6881e-01 --1.1400e+03 6.0000e+02 -1.7895e-01 --1.1400e+03 6.2857e+02 1.1040e-02 --1.1400e+03 6.5714e+02 2.0055e-01 --1.1400e+03 6.8571e+02 3.8906e-01 --1.1400e+03 7.1429e+02 5.7610e-01 --1.1400e+03 7.4286e+02 7.6124e-01 --1.1400e+03 7.7143e+02 9.4412e-01 --1.1400e+03 8.0000e+02 1.1244e+00 --1.1400e+03 8.2857e+02 1.3019e+00 --1.1400e+03 8.5714e+02 1.4764e+00 --1.1400e+03 8.8571e+02 1.6475e+00 --1.1400e+03 9.1429e+02 1.8153e+00 --1.1400e+03 9.4286e+02 1.9796e+00 --1.1400e+03 9.7143e+02 2.1403e+00 --1.1400e+03 1.0000e+03 2.2974e+00 --1.1400e+03 1.0286e+03 2.4508e+00 --1.1400e+03 1.0571e+03 2.6004e+00 --1.1400e+03 1.0857e+03 2.7464e+00 --1.1400e+03 1.1143e+03 2.8887e+00 --1.1400e+03 1.1429e+03 3.0273e+00 --1.1400e+03 1.1714e+03 3.1623e+00 --1.1400e+03 1.2000e+03 3.2938e+00 --1.1400e+03 1.2286e+03 3.4217e+00 --1.1400e+03 1.2571e+03 3.5462e+00 --1.1400e+03 1.2857e+03 3.6673e+00 --1.1400e+03 1.3143e+03 3.7851e+00 --1.1400e+03 1.3429e+03 3.8996e+00 --1.1400e+03 1.3714e+03 4.0110e+00 --1.1400e+03 1.4000e+03 4.1194e+00 --1.1400e+03 1.4286e+03 4.2247e+00 --1.1400e+03 1.4571e+03 4.3272e+00 --1.1400e+03 1.4857e+03 4.4268e+00 --1.1400e+03 1.5143e+03 4.5237e+00 --1.1400e+03 1.5429e+03 4.6179e+00 --1.1400e+03 1.5714e+03 4.7095e+00 --1.1400e+03 1.6000e+03 4.7986e+00 --1.1400e+03 1.6286e+03 4.8853e+00 --1.1400e+03 1.6571e+03 4.9696e+00 --1.1400e+03 1.6857e+03 5.0517e+00 --1.1400e+03 1.7143e+03 5.1316e+00 --1.1400e+03 1.7429e+03 5.2093e+00 --1.1400e+03 1.7714e+03 5.2849e+00 --1.1400e+03 1.8000e+03 5.3586e+00 --1.1400e+03 1.8286e+03 5.4303e+00 --1.1400e+03 1.8571e+03 5.5001e+00 --1.1400e+03 1.8857e+03 5.5681e+00 --1.1400e+03 1.9143e+03 5.6344e+00 --1.1400e+03 1.9429e+03 5.6990e+00 --1.1400e+03 1.9714e+03 5.7619e+00 --1.1400e+03 2.0000e+03 5.8232e+00 --1.1100e+03 -2.0000e+03 5.7870e+00 --1.1100e+03 -1.9714e+03 5.7242e+00 --1.1100e+03 -1.9429e+03 5.6597e+00 --1.1100e+03 -1.9143e+03 5.5935e+00 --1.1100e+03 -1.8857e+03 5.5255e+00 --1.1100e+03 -1.8571e+03 5.4557e+00 --1.1100e+03 -1.8286e+03 5.3839e+00 --1.1100e+03 -1.8000e+03 5.3102e+00 --1.1100e+03 -1.7714e+03 5.2344e+00 --1.1100e+03 -1.7429e+03 5.1565e+00 --1.1100e+03 -1.7143e+03 5.0764e+00 --1.1100e+03 -1.6857e+03 4.9941e+00 --1.1100e+03 -1.6571e+03 4.9094e+00 --1.1100e+03 -1.6286e+03 4.8223e+00 --1.1100e+03 -1.6000e+03 4.7326e+00 --1.1100e+03 -1.5714e+03 4.6404e+00 --1.1100e+03 -1.5429e+03 4.5455e+00 --1.1100e+03 -1.5143e+03 4.4478e+00 --1.1100e+03 -1.4857e+03 4.3472e+00 --1.1100e+03 -1.4571e+03 4.2437e+00 --1.1100e+03 -1.4286e+03 4.1371e+00 --1.1100e+03 -1.4000e+03 4.0274e+00 --1.1100e+03 -1.3714e+03 3.9144e+00 --1.1100e+03 -1.3429e+03 3.7981e+00 --1.1100e+03 -1.3143e+03 3.6783e+00 --1.1100e+03 -1.2857e+03 3.5550e+00 --1.1100e+03 -1.2571e+03 3.4280e+00 --1.1100e+03 -1.2286e+03 3.2973e+00 --1.1100e+03 -1.2000e+03 3.1628e+00 --1.1100e+03 -1.1714e+03 3.0244e+00 --1.1100e+03 -1.1429e+03 2.8821e+00 --1.1100e+03 -1.1143e+03 2.7356e+00 --1.1100e+03 -1.0857e+03 2.5851e+00 --1.1100e+03 -1.0571e+03 2.4303e+00 --1.1100e+03 -1.0286e+03 2.2714e+00 --1.1100e+03 -1.0000e+03 2.1082e+00 --1.1100e+03 -9.7143e+02 1.9408e+00 --1.1100e+03 -9.4286e+02 1.7691e+00 --1.1100e+03 -9.1429e+02 1.5933e+00 --1.1100e+03 -8.8571e+02 1.4133e+00 --1.1100e+03 -8.5714e+02 1.2293e+00 --1.1100e+03 -8.2857e+02 1.0414e+00 --1.1100e+03 -8.0000e+02 8.4975e-01 --1.1100e+03 -7.7143e+02 6.5461e-01 --1.1100e+03 -7.4286e+02 4.5622e-01 --1.1100e+03 -7.1429e+02 2.5490e-01 --1.1100e+03 -6.8571e+02 5.1018e-02 --1.1100e+03 -6.5714e+02 -1.5498e-01 --1.1100e+03 -6.2857e+02 -3.6260e-01 --1.1100e+03 -6.0000e+02 -5.7126e-01 --1.1100e+03 -5.7143e+02 -7.8032e-01 --1.1100e+03 -5.4286e+02 -9.8905e-01 --1.1100e+03 -5.1429e+02 -1.1967e+00 --1.1100e+03 -4.8571e+02 -1.4023e+00 --1.1100e+03 -4.5714e+02 -1.6050e+00 --1.1100e+03 -4.2857e+02 -1.8038e+00 --1.1100e+03 -4.0000e+02 -1.9975e+00 --1.1100e+03 -3.7143e+02 -2.1851e+00 --1.1100e+03 -3.4286e+02 -2.3654e+00 --1.1100e+03 -3.1429e+02 -2.5372e+00 --1.1100e+03 -2.8571e+02 -2.6992e+00 --1.1100e+03 -2.5714e+02 -2.8503e+00 --1.1100e+03 -2.2857e+02 -2.9893e+00 --1.1100e+03 -2.0000e+02 -3.1150e+00 --1.1100e+03 -1.7143e+02 -3.2264e+00 --1.1100e+03 -1.4286e+02 -3.3225e+00 --1.1100e+03 -1.1429e+02 -3.4024e+00 --1.1100e+03 -8.5714e+01 -3.4653e+00 --1.1100e+03 -5.7143e+01 -3.5107e+00 --1.1100e+03 -2.8571e+01 -3.5381e+00 --1.1100e+03 0.0000e+00 -3.5473e+00 --1.1100e+03 2.8571e+01 -3.5381e+00 --1.1100e+03 5.7143e+01 -3.5107e+00 --1.1100e+03 8.5714e+01 -3.4653e+00 --1.1100e+03 1.1429e+02 -3.4024e+00 --1.1100e+03 1.4286e+02 -3.3225e+00 --1.1100e+03 1.7143e+02 -3.2264e+00 --1.1100e+03 2.0000e+02 -3.1150e+00 --1.1100e+03 2.2857e+02 -2.9893e+00 --1.1100e+03 2.5714e+02 -2.8503e+00 --1.1100e+03 2.8571e+02 -2.6992e+00 --1.1100e+03 3.1429e+02 -2.5372e+00 --1.1100e+03 3.4286e+02 -2.3654e+00 --1.1100e+03 3.7143e+02 -2.1851e+00 --1.1100e+03 4.0000e+02 -1.9975e+00 --1.1100e+03 4.2857e+02 -1.8038e+00 --1.1100e+03 4.5714e+02 -1.6050e+00 --1.1100e+03 4.8571e+02 -1.4023e+00 --1.1100e+03 5.1429e+02 -1.1967e+00 --1.1100e+03 5.4286e+02 -9.8905e-01 --1.1100e+03 5.7143e+02 -7.8032e-01 --1.1100e+03 6.0000e+02 -5.7126e-01 --1.1100e+03 6.2857e+02 -3.6260e-01 --1.1100e+03 6.5714e+02 -1.5498e-01 --1.1100e+03 6.8571e+02 5.1018e-02 --1.1100e+03 7.1429e+02 2.5490e-01 --1.1100e+03 7.4286e+02 4.5622e-01 --1.1100e+03 7.7143e+02 6.5461e-01 --1.1100e+03 8.0000e+02 8.4975e-01 --1.1100e+03 8.2857e+02 1.0414e+00 --1.1100e+03 8.5714e+02 1.2293e+00 --1.1100e+03 8.8571e+02 1.4133e+00 --1.1100e+03 9.1429e+02 1.5933e+00 --1.1100e+03 9.4286e+02 1.7691e+00 --1.1100e+03 9.7143e+02 1.9408e+00 --1.1100e+03 1.0000e+03 2.1082e+00 --1.1100e+03 1.0286e+03 2.2714e+00 --1.1100e+03 1.0571e+03 2.4303e+00 --1.1100e+03 1.0857e+03 2.5851e+00 --1.1100e+03 1.1143e+03 2.7356e+00 --1.1100e+03 1.1429e+03 2.8821e+00 --1.1100e+03 1.1714e+03 3.0244e+00 --1.1100e+03 1.2000e+03 3.1628e+00 --1.1100e+03 1.2286e+03 3.2973e+00 --1.1100e+03 1.2571e+03 3.4280e+00 --1.1100e+03 1.2857e+03 3.5550e+00 --1.1100e+03 1.3143e+03 3.6783e+00 --1.1100e+03 1.3429e+03 3.7981e+00 --1.1100e+03 1.3714e+03 3.9144e+00 --1.1100e+03 1.4000e+03 4.0274e+00 --1.1100e+03 1.4286e+03 4.1371e+00 --1.1100e+03 1.4571e+03 4.2437e+00 --1.1100e+03 1.4857e+03 4.3472e+00 --1.1100e+03 1.5143e+03 4.4478e+00 --1.1100e+03 1.5429e+03 4.5455e+00 --1.1100e+03 1.5714e+03 4.6404e+00 --1.1100e+03 1.6000e+03 4.7326e+00 --1.1100e+03 1.6286e+03 4.8223e+00 --1.1100e+03 1.6571e+03 4.9094e+00 --1.1100e+03 1.6857e+03 4.9941e+00 --1.1100e+03 1.7143e+03 5.0764e+00 --1.1100e+03 1.7429e+03 5.1565e+00 --1.1100e+03 1.7714e+03 5.2344e+00 --1.1100e+03 1.8000e+03 5.3102e+00 --1.1100e+03 1.8286e+03 5.3839e+00 --1.1100e+03 1.8571e+03 5.4557e+00 --1.1100e+03 1.8857e+03 5.5255e+00 --1.1100e+03 1.9143e+03 5.5935e+00 --1.1100e+03 1.9429e+03 5.6597e+00 --1.1100e+03 1.9714e+03 5.7242e+00 --1.1100e+03 2.0000e+03 5.7870e+00 --1.0800e+03 -2.0000e+03 5.7510e+00 --1.0800e+03 -1.9714e+03 5.6866e+00 --1.0800e+03 -1.9429e+03 5.6206e+00 --1.0800e+03 -1.9143e+03 5.5527e+00 --1.0800e+03 -1.8857e+03 5.4830e+00 --1.0800e+03 -1.8571e+03 5.4113e+00 --1.0800e+03 -1.8286e+03 5.3376e+00 --1.0800e+03 -1.8000e+03 5.2618e+00 --1.0800e+03 -1.7714e+03 5.1838e+00 --1.0800e+03 -1.7429e+03 5.1037e+00 --1.0800e+03 -1.7143e+03 5.0212e+00 --1.0800e+03 -1.6857e+03 4.9362e+00 --1.0800e+03 -1.6571e+03 4.8488e+00 --1.0800e+03 -1.6286e+03 4.7588e+00 --1.0800e+03 -1.6000e+03 4.6662e+00 --1.0800e+03 -1.5714e+03 4.5707e+00 --1.0800e+03 -1.5429e+03 4.4724e+00 --1.0800e+03 -1.5143e+03 4.3711e+00 --1.0800e+03 -1.4857e+03 4.2667e+00 --1.0800e+03 -1.4571e+03 4.1592e+00 --1.0800e+03 -1.4286e+03 4.0483e+00 --1.0800e+03 -1.4000e+03 3.9340e+00 --1.0800e+03 -1.3714e+03 3.8162e+00 --1.0800e+03 -1.3429e+03 3.6948e+00 --1.0800e+03 -1.3143e+03 3.5695e+00 --1.0800e+03 -1.2857e+03 3.4405e+00 --1.0800e+03 -1.2571e+03 3.3074e+00 --1.0800e+03 -1.2286e+03 3.1702e+00 --1.0800e+03 -1.2000e+03 3.0287e+00 --1.0800e+03 -1.1714e+03 2.8830e+00 --1.0800e+03 -1.1429e+03 2.7328e+00 --1.0800e+03 -1.1143e+03 2.5780e+00 --1.0800e+03 -1.0857e+03 2.4187e+00 --1.0800e+03 -1.0571e+03 2.2546e+00 --1.0800e+03 -1.0286e+03 2.0857e+00 --1.0800e+03 -1.0000e+03 1.9120e+00 --1.0800e+03 -9.7143e+02 1.7334e+00 --1.0800e+03 -9.4286e+02 1.5499e+00 --1.0800e+03 -9.1429e+02 1.3616e+00 --1.0800e+03 -8.8571e+02 1.1684e+00 --1.0800e+03 -8.5714e+02 9.7037e-01 --1.0800e+03 -8.2857e+02 7.6771e-01 --1.0800e+03 -8.0000e+02 5.6054e-01 --1.0800e+03 -7.7143e+02 3.4905e-01 --1.0800e+03 -7.4286e+02 1.3351e-01 --1.0800e+03 -7.1429e+02 -8.5786e-02 --1.0800e+03 -6.8571e+02 -3.0845e-01 --1.0800e+03 -6.5714e+02 -5.3402e-01 --1.0800e+03 -6.2857e+02 -7.6197e-01 --1.0800e+03 -6.0000e+02 -9.9170e-01 --1.0800e+03 -5.7143e+02 -1.2225e+00 --1.0800e+03 -5.4286e+02 -1.4536e+00 --1.0800e+03 -5.1429e+02 -1.6840e+00 --1.0800e+03 -4.8571e+02 -1.9129e+00 --1.0800e+03 -4.5714e+02 -2.1392e+00 --1.0800e+03 -4.2857e+02 -2.3616e+00 --1.0800e+03 -4.0000e+02 -2.5790e+00 --1.0800e+03 -3.7143e+02 -2.7900e+00 --1.0800e+03 -3.4286e+02 -2.9933e+00 --1.0800e+03 -3.1429e+02 -3.1875e+00 --1.0800e+03 -2.8571e+02 -3.3710e+00 --1.0800e+03 -2.5714e+02 -3.5426e+00 --1.0800e+03 -2.2857e+02 -3.7006e+00 --1.0800e+03 -2.0000e+02 -3.8439e+00 --1.0800e+03 -1.7143e+02 -3.9710e+00 --1.0800e+03 -1.4286e+02 -4.0808e+00 --1.0800e+03 -1.1429e+02 -4.1722e+00 --1.0800e+03 -8.5714e+01 -4.2443e+00 --1.0800e+03 -5.7143e+01 -4.2963e+00 --1.0800e+03 -2.8571e+01 -4.3278e+00 --1.0800e+03 0.0000e+00 -4.3383e+00 --1.0800e+03 2.8571e+01 -4.3278e+00 --1.0800e+03 5.7143e+01 -4.2963e+00 --1.0800e+03 8.5714e+01 -4.2443e+00 --1.0800e+03 1.1429e+02 -4.1722e+00 --1.0800e+03 1.4286e+02 -4.0808e+00 --1.0800e+03 1.7143e+02 -3.9710e+00 --1.0800e+03 2.0000e+02 -3.8439e+00 --1.0800e+03 2.2857e+02 -3.7006e+00 --1.0800e+03 2.5714e+02 -3.5426e+00 --1.0800e+03 2.8571e+02 -3.3710e+00 --1.0800e+03 3.1429e+02 -3.1875e+00 --1.0800e+03 3.4286e+02 -2.9933e+00 --1.0800e+03 3.7143e+02 -2.7900e+00 --1.0800e+03 4.0000e+02 -2.5790e+00 --1.0800e+03 4.2857e+02 -2.3616e+00 --1.0800e+03 4.5714e+02 -2.1392e+00 --1.0800e+03 4.8571e+02 -1.9129e+00 --1.0800e+03 5.1429e+02 -1.6840e+00 --1.0800e+03 5.4286e+02 -1.4536e+00 --1.0800e+03 5.7143e+02 -1.2225e+00 --1.0800e+03 6.0000e+02 -9.9170e-01 --1.0800e+03 6.2857e+02 -7.6197e-01 --1.0800e+03 6.5714e+02 -5.3402e-01 --1.0800e+03 6.8571e+02 -3.0845e-01 --1.0800e+03 7.1429e+02 -8.5786e-02 --1.0800e+03 7.4286e+02 1.3351e-01 --1.0800e+03 7.7143e+02 3.4905e-01 --1.0800e+03 8.0000e+02 5.6054e-01 --1.0800e+03 8.2857e+02 7.6771e-01 --1.0800e+03 8.5714e+02 9.7037e-01 --1.0800e+03 8.8571e+02 1.1684e+00 --1.0800e+03 9.1429e+02 1.3616e+00 --1.0800e+03 9.4286e+02 1.5499e+00 --1.0800e+03 9.7143e+02 1.7334e+00 --1.0800e+03 1.0000e+03 1.9120e+00 --1.0800e+03 1.0286e+03 2.0857e+00 --1.0800e+03 1.0571e+03 2.2546e+00 --1.0800e+03 1.0857e+03 2.4187e+00 --1.0800e+03 1.1143e+03 2.5780e+00 --1.0800e+03 1.1429e+03 2.7328e+00 --1.0800e+03 1.1714e+03 2.8830e+00 --1.0800e+03 1.2000e+03 3.0287e+00 --1.0800e+03 1.2286e+03 3.1702e+00 --1.0800e+03 1.2571e+03 3.3074e+00 --1.0800e+03 1.2857e+03 3.4405e+00 --1.0800e+03 1.3143e+03 3.5695e+00 --1.0800e+03 1.3429e+03 3.6948e+00 --1.0800e+03 1.3714e+03 3.8162e+00 --1.0800e+03 1.4000e+03 3.9340e+00 --1.0800e+03 1.4286e+03 4.0483e+00 --1.0800e+03 1.4571e+03 4.1592e+00 --1.0800e+03 1.4857e+03 4.2667e+00 --1.0800e+03 1.5143e+03 4.3711e+00 --1.0800e+03 1.5429e+03 4.4724e+00 --1.0800e+03 1.5714e+03 4.5707e+00 --1.0800e+03 1.6000e+03 4.6662e+00 --1.0800e+03 1.6286e+03 4.7588e+00 --1.0800e+03 1.6571e+03 4.8488e+00 --1.0800e+03 1.6857e+03 4.9362e+00 --1.0800e+03 1.7143e+03 5.0212e+00 --1.0800e+03 1.7429e+03 5.1037e+00 --1.0800e+03 1.7714e+03 5.1838e+00 --1.0800e+03 1.8000e+03 5.2618e+00 --1.0800e+03 1.8286e+03 5.3376e+00 --1.0800e+03 1.8571e+03 5.4113e+00 --1.0800e+03 1.8857e+03 5.4830e+00 --1.0800e+03 1.9143e+03 5.5527e+00 --1.0800e+03 1.9429e+03 5.6206e+00 --1.0800e+03 1.9714e+03 5.6866e+00 --1.0800e+03 2.0000e+03 5.7510e+00 --1.0500e+03 -2.0000e+03 5.7151e+00 --1.0500e+03 -1.9714e+03 5.6492e+00 --1.0500e+03 -1.9429e+03 5.5816e+00 --1.0500e+03 -1.9143e+03 5.5120e+00 --1.0500e+03 -1.8857e+03 5.4405e+00 --1.0500e+03 -1.8571e+03 5.3669e+00 --1.0500e+03 -1.8286e+03 5.2913e+00 --1.0500e+03 -1.8000e+03 5.2134e+00 --1.0500e+03 -1.7714e+03 5.1332e+00 --1.0500e+03 -1.7429e+03 5.0507e+00 --1.0500e+03 -1.7143e+03 4.9657e+00 --1.0500e+03 -1.6857e+03 4.8782e+00 --1.0500e+03 -1.6571e+03 4.7880e+00 --1.0500e+03 -1.6286e+03 4.6951e+00 --1.0500e+03 -1.6000e+03 4.5993e+00 --1.0500e+03 -1.5714e+03 4.5006e+00 --1.0500e+03 -1.5429e+03 4.3988e+00 --1.0500e+03 -1.5143e+03 4.2938e+00 --1.0500e+03 -1.4857e+03 4.1855e+00 --1.0500e+03 -1.4571e+03 4.0737e+00 --1.0500e+03 -1.4286e+03 3.9584e+00 --1.0500e+03 -1.4000e+03 3.8394e+00 --1.0500e+03 -1.3714e+03 3.7166e+00 --1.0500e+03 -1.3429e+03 3.5898e+00 --1.0500e+03 -1.3143e+03 3.4589e+00 --1.0500e+03 -1.2857e+03 3.3238e+00 --1.0500e+03 -1.2571e+03 3.1843e+00 --1.0500e+03 -1.2286e+03 3.0402e+00 --1.0500e+03 -1.2000e+03 2.8915e+00 --1.0500e+03 -1.1714e+03 2.7380e+00 --1.0500e+03 -1.1429e+03 2.5795e+00 --1.0500e+03 -1.1143e+03 2.4160e+00 --1.0500e+03 -1.0857e+03 2.2472e+00 --1.0500e+03 -1.0571e+03 2.0731e+00 --1.0500e+03 -1.0286e+03 1.8936e+00 --1.0500e+03 -1.0000e+03 1.7086e+00 --1.0500e+03 -9.7143e+02 1.5180e+00 --1.0500e+03 -9.4286e+02 1.3217e+00 --1.0500e+03 -9.1429e+02 1.1198e+00 --1.0500e+03 -8.8571e+02 9.1220e-01 --1.0500e+03 -8.5714e+02 6.9895e-01 --1.0500e+03 -8.2857e+02 4.8013e-01 --1.0500e+03 -8.0000e+02 2.5587e-01 --1.0500e+03 -7.7143e+02 2.6339e-02 --1.0500e+03 -7.4286e+02 -2.0822e-01 --1.0500e+03 -7.1429e+02 -4.4752e-01 --1.0500e+03 -6.8571e+02 -6.9116e-01 --1.0500e+03 -6.5714e+02 -9.3869e-01 --1.0500e+03 -6.2857e+02 -1.1896e+00 --1.0500e+03 -6.0000e+02 -1.4431e+00 --1.0500e+03 -5.7143e+02 -1.6986e+00 --1.0500e+03 -5.4286e+02 -1.9552e+00 --1.0500e+03 -5.1429e+02 -2.2118e+00 --1.0500e+03 -4.8571e+02 -2.4675e+00 --1.0500e+03 -4.5714e+02 -2.7209e+00 --1.0500e+03 -4.2857e+02 -2.9708e+00 --1.0500e+03 -4.0000e+02 -3.2157e+00 --1.0500e+03 -3.7143e+02 -3.4541e+00 --1.0500e+03 -3.4286e+02 -3.6844e+00 --1.0500e+03 -3.1429e+02 -3.9049e+00 --1.0500e+03 -2.8571e+02 -4.1139e+00 --1.0500e+03 -2.5714e+02 -4.3097e+00 --1.0500e+03 -2.2857e+02 -4.4905e+00 --1.0500e+03 -2.0000e+02 -4.6547e+00 --1.0500e+03 -1.7143e+02 -4.8006e+00 --1.0500e+03 -1.4286e+02 -4.9268e+00 --1.0500e+03 -1.1429e+02 -5.0320e+00 --1.0500e+03 -8.5714e+01 -5.1151e+00 --1.0500e+03 -5.7143e+01 -5.1751e+00 --1.0500e+03 -2.8571e+01 -5.2114e+00 --1.0500e+03 0.0000e+00 -5.2235e+00 --1.0500e+03 2.8571e+01 -5.2114e+00 --1.0500e+03 5.7143e+01 -5.1751e+00 --1.0500e+03 8.5714e+01 -5.1151e+00 --1.0500e+03 1.1429e+02 -5.0320e+00 --1.0500e+03 1.4286e+02 -4.9268e+00 --1.0500e+03 1.7143e+02 -4.8006e+00 --1.0500e+03 2.0000e+02 -4.6547e+00 --1.0500e+03 2.2857e+02 -4.4905e+00 --1.0500e+03 2.5714e+02 -4.3097e+00 --1.0500e+03 2.8571e+02 -4.1139e+00 --1.0500e+03 3.1429e+02 -3.9049e+00 --1.0500e+03 3.4286e+02 -3.6844e+00 --1.0500e+03 3.7143e+02 -3.4541e+00 --1.0500e+03 4.0000e+02 -3.2157e+00 --1.0500e+03 4.2857e+02 -2.9708e+00 --1.0500e+03 4.5714e+02 -2.7209e+00 --1.0500e+03 4.8571e+02 -2.4675e+00 --1.0500e+03 5.1429e+02 -2.2118e+00 --1.0500e+03 5.4286e+02 -1.9552e+00 --1.0500e+03 5.7143e+02 -1.6986e+00 --1.0500e+03 6.0000e+02 -1.4431e+00 --1.0500e+03 6.2857e+02 -1.1896e+00 --1.0500e+03 6.5714e+02 -9.3869e-01 --1.0500e+03 6.8571e+02 -6.9116e-01 --1.0500e+03 7.1429e+02 -4.4752e-01 --1.0500e+03 7.4286e+02 -2.0822e-01 --1.0500e+03 7.7143e+02 2.6339e-02 --1.0500e+03 8.0000e+02 2.5587e-01 --1.0500e+03 8.2857e+02 4.8013e-01 --1.0500e+03 8.5714e+02 6.9895e-01 --1.0500e+03 8.8571e+02 9.1220e-01 --1.0500e+03 9.1429e+02 1.1198e+00 --1.0500e+03 9.4286e+02 1.3217e+00 --1.0500e+03 9.7143e+02 1.5180e+00 --1.0500e+03 1.0000e+03 1.7086e+00 --1.0500e+03 1.0286e+03 1.8936e+00 --1.0500e+03 1.0571e+03 2.0731e+00 --1.0500e+03 1.0857e+03 2.2472e+00 --1.0500e+03 1.1143e+03 2.4160e+00 --1.0500e+03 1.1429e+03 2.5795e+00 --1.0500e+03 1.1714e+03 2.7380e+00 --1.0500e+03 1.2000e+03 2.8915e+00 --1.0500e+03 1.2286e+03 3.0402e+00 --1.0500e+03 1.2571e+03 3.1843e+00 --1.0500e+03 1.2857e+03 3.3238e+00 --1.0500e+03 1.3143e+03 3.4589e+00 --1.0500e+03 1.3429e+03 3.5898e+00 --1.0500e+03 1.3714e+03 3.7166e+00 --1.0500e+03 1.4000e+03 3.8394e+00 --1.0500e+03 1.4286e+03 3.9584e+00 --1.0500e+03 1.4571e+03 4.0737e+00 --1.0500e+03 1.4857e+03 4.1855e+00 --1.0500e+03 1.5143e+03 4.2938e+00 --1.0500e+03 1.5429e+03 4.3988e+00 --1.0500e+03 1.5714e+03 4.5006e+00 --1.0500e+03 1.6000e+03 4.5993e+00 --1.0500e+03 1.6286e+03 4.6951e+00 --1.0500e+03 1.6571e+03 4.7880e+00 --1.0500e+03 1.6857e+03 4.8782e+00 --1.0500e+03 1.7143e+03 4.9657e+00 --1.0500e+03 1.7429e+03 5.0507e+00 --1.0500e+03 1.7714e+03 5.1332e+00 --1.0500e+03 1.8000e+03 5.2134e+00 --1.0500e+03 1.8286e+03 5.2913e+00 --1.0500e+03 1.8571e+03 5.3669e+00 --1.0500e+03 1.8857e+03 5.4405e+00 --1.0500e+03 1.9143e+03 5.5120e+00 --1.0500e+03 1.9429e+03 5.5816e+00 --1.0500e+03 1.9714e+03 5.6492e+00 --1.0500e+03 2.0000e+03 5.7151e+00 --1.0200e+03 -2.0000e+03 5.6794e+00 --1.0200e+03 -1.9714e+03 5.6120e+00 --1.0200e+03 -1.9429e+03 5.5427e+00 --1.0200e+03 -1.9143e+03 5.4715e+00 --1.0200e+03 -1.8857e+03 5.3982e+00 --1.0200e+03 -1.8571e+03 5.3227e+00 --1.0200e+03 -1.8286e+03 5.2450e+00 --1.0200e+03 -1.8000e+03 5.1650e+00 --1.0200e+03 -1.7714e+03 5.0826e+00 --1.0200e+03 -1.7429e+03 4.9977e+00 --1.0200e+03 -1.7143e+03 4.9103e+00 --1.0200e+03 -1.6857e+03 4.8201e+00 --1.0200e+03 -1.6571e+03 4.7271e+00 --1.0200e+03 -1.6286e+03 4.6311e+00 --1.0200e+03 -1.6000e+03 4.5322e+00 --1.0200e+03 -1.5714e+03 4.4301e+00 --1.0200e+03 -1.5429e+03 4.3247e+00 --1.0200e+03 -1.5143e+03 4.2159e+00 --1.0200e+03 -1.4857e+03 4.1035e+00 --1.0200e+03 -1.4571e+03 3.9874e+00 --1.0200e+03 -1.4286e+03 3.8675e+00 --1.0200e+03 -1.4000e+03 3.7436e+00 --1.0200e+03 -1.3714e+03 3.6156e+00 --1.0200e+03 -1.3429e+03 3.4833e+00 --1.0200e+03 -1.3143e+03 3.3464e+00 --1.0200e+03 -1.2857e+03 3.2050e+00 --1.0200e+03 -1.2571e+03 3.0587e+00 --1.0200e+03 -1.2286e+03 2.9075e+00 --1.0200e+03 -1.2000e+03 2.7511e+00 --1.0200e+03 -1.1714e+03 2.5894e+00 --1.0200e+03 -1.1429e+03 2.4222e+00 --1.0200e+03 -1.1143e+03 2.2493e+00 --1.0200e+03 -1.0857e+03 2.0706e+00 --1.0200e+03 -1.0571e+03 1.8858e+00 --1.0200e+03 -1.0286e+03 1.6950e+00 --1.0200e+03 -1.0000e+03 1.4978e+00 --1.0200e+03 -9.7143e+02 1.2943e+00 --1.0200e+03 -9.4286e+02 1.0842e+00 --1.0200e+03 -9.1429e+02 8.6755e-01 --1.0200e+03 -8.8571e+02 6.4427e-01 --1.0200e+03 -8.5714e+02 4.1434e-01 --1.0200e+03 -8.2857e+02 1.7780e-01 --1.0200e+03 -8.0000e+02 -6.5281e-02 --1.0200e+03 -7.7143e+02 -3.1476e-01 --1.0200e+03 -7.4286e+02 -5.7042e-01 --1.0200e+03 -7.1429e+02 -8.3201e-01 --1.0200e+03 -6.8571e+02 -1.0991e+00 --1.0200e+03 -6.5714e+02 -1.3714e+00 --1.0200e+03 -6.2857e+02 -1.6481e+00 --1.0200e+03 -6.0000e+02 -1.9287e+00 --1.0200e+03 -5.7143e+02 -2.2123e+00 --1.0200e+03 -5.4286e+02 -2.4981e+00 --1.0200e+03 -5.1429e+02 -2.7848e+00 --1.0200e+03 -4.8571e+02 -3.0714e+00 --1.0200e+03 -4.5714e+02 -3.3564e+00 --1.0200e+03 -4.2857e+02 -3.6383e+00 --1.0200e+03 -4.0000e+02 -3.9154e+00 --1.0200e+03 -3.7143e+02 -4.1861e+00 --1.0200e+03 -3.4286e+02 -4.4482e+00 --1.0200e+03 -3.1429e+02 -4.7000e+00 --1.0200e+03 -2.8571e+02 -4.9393e+00 --1.0200e+03 -2.5714e+02 -5.1639e+00 --1.0200e+03 -2.2857e+02 -5.3719e+00 --1.0200e+03 -2.0000e+02 -5.5612e+00 --1.0200e+03 -1.7143e+02 -5.7298e+00 --1.0200e+03 -1.4286e+02 -5.8759e+00 --1.0200e+03 -1.1429e+02 -5.9978e+00 --1.0200e+03 -8.5714e+01 -6.0942e+00 --1.0200e+03 -5.7143e+01 -6.1639e+00 --1.0200e+03 -2.8571e+01 -6.2060e+00 --1.0200e+03 0.0000e+00 -6.2201e+00 --1.0200e+03 2.8571e+01 -6.2060e+00 --1.0200e+03 5.7143e+01 -6.1639e+00 --1.0200e+03 8.5714e+01 -6.0942e+00 --1.0200e+03 1.1429e+02 -5.9978e+00 --1.0200e+03 1.4286e+02 -5.8759e+00 --1.0200e+03 1.7143e+02 -5.7298e+00 --1.0200e+03 2.0000e+02 -5.5612e+00 --1.0200e+03 2.2857e+02 -5.3719e+00 --1.0200e+03 2.5714e+02 -5.1639e+00 --1.0200e+03 2.8571e+02 -4.9393e+00 --1.0200e+03 3.1429e+02 -4.7000e+00 --1.0200e+03 3.4286e+02 -4.4482e+00 --1.0200e+03 3.7143e+02 -4.1861e+00 --1.0200e+03 4.0000e+02 -3.9154e+00 --1.0200e+03 4.2857e+02 -3.6383e+00 --1.0200e+03 4.5714e+02 -3.3564e+00 --1.0200e+03 4.8571e+02 -3.0714e+00 --1.0200e+03 5.1429e+02 -2.7848e+00 --1.0200e+03 5.4286e+02 -2.4981e+00 --1.0200e+03 5.7143e+02 -2.2123e+00 --1.0200e+03 6.0000e+02 -1.9287e+00 --1.0200e+03 6.2857e+02 -1.6481e+00 --1.0200e+03 6.5714e+02 -1.3714e+00 --1.0200e+03 6.8571e+02 -1.0991e+00 --1.0200e+03 7.1429e+02 -8.3201e-01 --1.0200e+03 7.4286e+02 -5.7042e-01 --1.0200e+03 7.7143e+02 -3.1476e-01 --1.0200e+03 8.0000e+02 -6.5281e-02 --1.0200e+03 8.2857e+02 1.7780e-01 --1.0200e+03 8.5714e+02 4.1434e-01 --1.0200e+03 8.8571e+02 6.4427e-01 --1.0200e+03 9.1429e+02 8.6755e-01 --1.0200e+03 9.4286e+02 1.0842e+00 --1.0200e+03 9.7143e+02 1.2943e+00 --1.0200e+03 1.0000e+03 1.4978e+00 --1.0200e+03 1.0286e+03 1.6950e+00 --1.0200e+03 1.0571e+03 1.8858e+00 --1.0200e+03 1.0857e+03 2.0706e+00 --1.0200e+03 1.1143e+03 2.2493e+00 --1.0200e+03 1.1429e+03 2.4222e+00 --1.0200e+03 1.1714e+03 2.5894e+00 --1.0200e+03 1.2000e+03 2.7511e+00 --1.0200e+03 1.2286e+03 2.9075e+00 --1.0200e+03 1.2571e+03 3.0587e+00 --1.0200e+03 1.2857e+03 3.2050e+00 --1.0200e+03 1.3143e+03 3.3464e+00 --1.0200e+03 1.3429e+03 3.4833e+00 --1.0200e+03 1.3714e+03 3.6156e+00 --1.0200e+03 1.4000e+03 3.7436e+00 --1.0200e+03 1.4286e+03 3.8675e+00 --1.0200e+03 1.4571e+03 3.9874e+00 --1.0200e+03 1.4857e+03 4.1035e+00 --1.0200e+03 1.5143e+03 4.2159e+00 --1.0200e+03 1.5429e+03 4.3247e+00 --1.0200e+03 1.5714e+03 4.4301e+00 --1.0200e+03 1.6000e+03 4.5322e+00 --1.0200e+03 1.6286e+03 4.6311e+00 --1.0200e+03 1.6571e+03 4.7271e+00 --1.0200e+03 1.6857e+03 4.8201e+00 --1.0200e+03 1.7143e+03 4.9103e+00 --1.0200e+03 1.7429e+03 4.9977e+00 --1.0200e+03 1.7714e+03 5.0826e+00 --1.0200e+03 1.8000e+03 5.1650e+00 --1.0200e+03 1.8286e+03 5.2450e+00 --1.0200e+03 1.8571e+03 5.3227e+00 --1.0200e+03 1.8857e+03 5.3982e+00 --1.0200e+03 1.9143e+03 5.4715e+00 --1.0200e+03 1.9429e+03 5.5427e+00 --1.0200e+03 1.9714e+03 5.6120e+00 --1.0200e+03 2.0000e+03 5.6794e+00 --9.9000e+02 -2.0000e+03 5.6439e+00 --9.9000e+02 -1.9714e+03 5.5750e+00 --9.9000e+02 -1.9429e+03 5.5041e+00 --9.9000e+02 -1.9143e+03 5.4312e+00 --9.9000e+02 -1.8857e+03 5.3560e+00 --9.9000e+02 -1.8571e+03 5.2787e+00 --9.9000e+02 -1.8286e+03 5.1989e+00 --9.9000e+02 -1.8000e+03 5.1168e+00 --9.9000e+02 -1.7714e+03 5.0321e+00 --9.9000e+02 -1.7429e+03 4.9448e+00 --9.9000e+02 -1.7143e+03 4.8548e+00 --9.9000e+02 -1.6857e+03 4.7619e+00 --9.9000e+02 -1.6571e+03 4.6660e+00 --9.9000e+02 -1.6286e+03 4.5670e+00 --9.9000e+02 -1.6000e+03 4.4648e+00 --9.9000e+02 -1.5714e+03 4.3592e+00 --9.9000e+02 -1.5429e+03 4.2502e+00 --9.9000e+02 -1.5143e+03 4.1374e+00 --9.9000e+02 -1.4857e+03 4.0209e+00 --9.9000e+02 -1.4571e+03 3.9004e+00 --9.9000e+02 -1.4286e+03 3.7757e+00 --9.9000e+02 -1.4000e+03 3.6468e+00 --9.9000e+02 -1.3714e+03 3.5133e+00 --9.9000e+02 -1.3429e+03 3.3752e+00 --9.9000e+02 -1.3143e+03 3.2322e+00 --9.9000e+02 -1.2857e+03 3.0842e+00 --9.9000e+02 -1.2571e+03 2.9309e+00 --9.9000e+02 -1.2286e+03 2.7721e+00 --9.9000e+02 -1.2000e+03 2.6077e+00 --9.9000e+02 -1.1714e+03 2.4373e+00 --9.9000e+02 -1.1429e+03 2.2608e+00 --9.9000e+02 -1.1143e+03 2.0780e+00 --9.9000e+02 -1.0857e+03 1.8887e+00 --9.9000e+02 -1.0571e+03 1.6926e+00 --9.9000e+02 -1.0286e+03 1.4896e+00 --9.9000e+02 -1.0000e+03 1.2794e+00 --9.9000e+02 -9.7143e+02 1.0619e+00 --9.9000e+02 -9.4286e+02 8.3695e-01 --9.9000e+02 -9.1429e+02 6.0433e-01 --9.9000e+02 -8.8571e+02 3.6398e-01 --9.9000e+02 -8.5714e+02 1.1583e-01 --9.9000e+02 -8.2857e+02 -1.4017e-01 --9.9000e+02 -8.0000e+02 -4.0398e-01 --9.9000e+02 -7.7143e+02 -6.7552e-01 --9.9000e+02 -7.4286e+02 -9.5465e-01 --9.9000e+02 -7.1429e+02 -1.2411e+00 --9.9000e+02 -6.8571e+02 -1.5346e+00 --9.9000e+02 -6.5714e+02 -1.8346e+00 --9.9000e+02 -6.2857e+02 -2.1407e+00 --9.9000e+02 -6.0000e+02 -2.4521e+00 --9.9000e+02 -5.7143e+02 -2.7679e+00 --9.9000e+02 -5.4286e+02 -3.0871e+00 --9.9000e+02 -5.1429e+02 -3.4086e+00 --9.9000e+02 -4.8571e+02 -3.7311e+00 --9.9000e+02 -4.5714e+02 -4.0529e+00 --9.9000e+02 -4.2857e+02 -4.3723e+00 --9.9000e+02 -4.0000e+02 -4.6874e+00 --9.9000e+02 -3.7143e+02 -4.9961e+00 --9.9000e+02 -3.4286e+02 -5.2961e+00 --9.9000e+02 -3.1429e+02 -5.5852e+00 --9.9000e+02 -2.8571e+02 -5.8607e+00 --9.9000e+02 -2.5714e+02 -6.1201e+00 --9.9000e+02 -2.2857e+02 -6.3609e+00 --9.9000e+02 -2.0000e+02 -6.5806e+00 --9.9000e+02 -1.7143e+02 -6.7767e+00 --9.9000e+02 -1.4286e+02 -6.9469e+00 --9.9000e+02 -1.1429e+02 -7.0892e+00 --9.9000e+02 -8.5714e+01 -7.2019e+00 --9.9000e+02 -5.7143e+01 -7.2834e+00 --9.9000e+02 -2.8571e+01 -7.3327e+00 --9.9000e+02 0.0000e+00 -7.3493e+00 --9.9000e+02 2.8571e+01 -7.3327e+00 --9.9000e+02 5.7143e+01 -7.2834e+00 --9.9000e+02 8.5714e+01 -7.2019e+00 --9.9000e+02 1.1429e+02 -7.0892e+00 --9.9000e+02 1.4286e+02 -6.9469e+00 --9.9000e+02 1.7143e+02 -6.7767e+00 --9.9000e+02 2.0000e+02 -6.5806e+00 --9.9000e+02 2.2857e+02 -6.3609e+00 --9.9000e+02 2.5714e+02 -6.1201e+00 --9.9000e+02 2.8571e+02 -5.8607e+00 --9.9000e+02 3.1429e+02 -5.5852e+00 --9.9000e+02 3.4286e+02 -5.2961e+00 --9.9000e+02 3.7143e+02 -4.9961e+00 --9.9000e+02 4.0000e+02 -4.6874e+00 --9.9000e+02 4.2857e+02 -4.3723e+00 --9.9000e+02 4.5714e+02 -4.0529e+00 --9.9000e+02 4.8571e+02 -3.7311e+00 --9.9000e+02 5.1429e+02 -3.4086e+00 --9.9000e+02 5.4286e+02 -3.0871e+00 --9.9000e+02 5.7143e+02 -2.7679e+00 --9.9000e+02 6.0000e+02 -2.4521e+00 --9.9000e+02 6.2857e+02 -2.1407e+00 --9.9000e+02 6.5714e+02 -1.8346e+00 --9.9000e+02 6.8571e+02 -1.5346e+00 --9.9000e+02 7.1429e+02 -1.2411e+00 --9.9000e+02 7.4286e+02 -9.5465e-01 --9.9000e+02 7.7143e+02 -6.7552e-01 --9.9000e+02 8.0000e+02 -4.0398e-01 --9.9000e+02 8.2857e+02 -1.4017e-01 --9.9000e+02 8.5714e+02 1.1583e-01 --9.9000e+02 8.8571e+02 3.6398e-01 --9.9000e+02 9.1429e+02 6.0433e-01 --9.9000e+02 9.4286e+02 8.3695e-01 --9.9000e+02 9.7143e+02 1.0619e+00 --9.9000e+02 1.0000e+03 1.2794e+00 --9.9000e+02 1.0286e+03 1.4896e+00 --9.9000e+02 1.0571e+03 1.6926e+00 --9.9000e+02 1.0857e+03 1.8887e+00 --9.9000e+02 1.1143e+03 2.0780e+00 --9.9000e+02 1.1429e+03 2.2608e+00 --9.9000e+02 1.1714e+03 2.4373e+00 --9.9000e+02 1.2000e+03 2.6077e+00 --9.9000e+02 1.2286e+03 2.7721e+00 --9.9000e+02 1.2571e+03 2.9309e+00 --9.9000e+02 1.2857e+03 3.0842e+00 --9.9000e+02 1.3143e+03 3.2322e+00 --9.9000e+02 1.3429e+03 3.3752e+00 --9.9000e+02 1.3714e+03 3.5133e+00 --9.9000e+02 1.4000e+03 3.6468e+00 --9.9000e+02 1.4286e+03 3.7757e+00 --9.9000e+02 1.4571e+03 3.9004e+00 --9.9000e+02 1.4857e+03 4.0209e+00 --9.9000e+02 1.5143e+03 4.1374e+00 --9.9000e+02 1.5429e+03 4.2502e+00 --9.9000e+02 1.5714e+03 4.3592e+00 --9.9000e+02 1.6000e+03 4.4648e+00 --9.9000e+02 1.6286e+03 4.5670e+00 --9.9000e+02 1.6571e+03 4.6660e+00 --9.9000e+02 1.6857e+03 4.7619e+00 --9.9000e+02 1.7143e+03 4.8548e+00 --9.9000e+02 1.7429e+03 4.9448e+00 --9.9000e+02 1.7714e+03 5.0321e+00 --9.9000e+02 1.8000e+03 5.1168e+00 --9.9000e+02 1.8286e+03 5.1989e+00 --9.9000e+02 1.8571e+03 5.2787e+00 --9.9000e+02 1.8857e+03 5.3560e+00 --9.9000e+02 1.9143e+03 5.4312e+00 --9.9000e+02 1.9429e+03 5.5041e+00 --9.9000e+02 1.9714e+03 5.5750e+00 --9.9000e+02 2.0000e+03 5.6439e+00 --9.6000e+02 -2.0000e+03 5.6088e+00 --9.6000e+02 -1.9714e+03 5.5383e+00 --9.6000e+02 -1.9429e+03 5.4658e+00 --9.6000e+02 -1.9143e+03 5.3911e+00 --9.6000e+02 -1.8857e+03 5.3141e+00 --9.6000e+02 -1.8571e+03 5.2348e+00 --9.6000e+02 -1.8286e+03 5.1531e+00 --9.6000e+02 -1.8000e+03 5.0687e+00 --9.6000e+02 -1.7714e+03 4.9818e+00 --9.6000e+02 -1.7429e+03 4.8920e+00 --9.6000e+02 -1.7143e+03 4.7994e+00 --9.6000e+02 -1.6857e+03 4.7037e+00 --9.6000e+02 -1.6571e+03 4.6049e+00 --9.6000e+02 -1.6286e+03 4.5028e+00 --9.6000e+02 -1.6000e+03 4.3973e+00 --9.6000e+02 -1.5714e+03 4.2882e+00 --9.6000e+02 -1.5429e+03 4.1753e+00 --9.6000e+02 -1.5143e+03 4.0585e+00 --9.6000e+02 -1.4857e+03 3.9377e+00 --9.6000e+02 -1.4571e+03 3.8126e+00 --9.6000e+02 -1.4286e+03 3.6831e+00 --9.6000e+02 -1.4000e+03 3.5489e+00 --9.6000e+02 -1.3714e+03 3.4099e+00 --9.6000e+02 -1.3429e+03 3.2658e+00 --9.6000e+02 -1.3143e+03 3.1164e+00 --9.6000e+02 -1.2857e+03 2.9615e+00 --9.6000e+02 -1.2571e+03 2.8008e+00 --9.6000e+02 -1.2286e+03 2.6341e+00 --9.6000e+02 -1.2000e+03 2.4612e+00 --9.6000e+02 -1.1714e+03 2.2817e+00 --9.6000e+02 -1.1429e+03 2.0955e+00 --9.6000e+02 -1.1143e+03 1.9022e+00 --9.6000e+02 -1.0857e+03 1.7016e+00 --9.6000e+02 -1.0571e+03 1.4934e+00 --9.6000e+02 -1.0286e+03 1.2774e+00 --9.6000e+02 -1.0000e+03 1.0533e+00 --9.6000e+02 -9.7143e+02 8.2075e-01 --9.6000e+02 -9.4286e+02 5.7963e-01 --9.6000e+02 -9.1429e+02 3.2971e-01 --9.6000e+02 -8.8571e+02 7.0775e-02 --9.6000e+02 -8.5714e+02 -1.9731e-01 --9.6000e+02 -8.2857e+02 -4.7467e-01 --9.6000e+02 -8.0000e+02 -7.6135e-01 --9.6000e+02 -7.7143e+02 -1.0574e+00 --9.6000e+02 -7.4286e+02 -1.3626e+00 --9.6000e+02 -7.1429e+02 -1.6769e+00 --9.6000e+02 -6.8571e+02 -2.0000e+00 --9.6000e+02 -6.5714e+02 -2.3315e+00 --9.6000e+02 -6.2857e+02 -2.6708e+00 --9.6000e+02 -6.0000e+02 -3.0172e+00 --9.6000e+02 -5.7143e+02 -3.3700e+00 --9.6000e+02 -5.4286e+02 -3.7279e+00 --9.6000e+02 -5.1429e+02 -4.0897e+00 --9.6000e+02 -4.8571e+02 -4.4539e+00 --9.6000e+02 -4.5714e+02 -4.8188e+00 --9.6000e+02 -4.2857e+02 -5.1824e+00 --9.6000e+02 -4.0000e+02 -5.5424e+00 --9.6000e+02 -3.7143e+02 -5.8964e+00 --9.6000e+02 -3.4286e+02 -6.2418e+00 --9.6000e+02 -3.1429e+02 -6.5756e+00 --9.6000e+02 -2.8571e+02 -6.8948e+00 --9.6000e+02 -2.5714e+02 -7.1964e+00 --9.6000e+02 -2.2857e+02 -7.4771e+00 --9.6000e+02 -2.0000e+02 -7.7339e+00 --9.6000e+02 -1.7143e+02 -7.9636e+00 --9.6000e+02 -1.4286e+02 -8.1635e+00 --9.6000e+02 -1.1429e+02 -8.3309e+00 --9.6000e+02 -8.5714e+01 -8.4636e+00 --9.6000e+02 -5.7143e+01 -8.5597e+00 --9.6000e+02 -2.8571e+01 -8.6180e+00 --9.6000e+02 0.0000e+00 -8.6375e+00 --9.6000e+02 2.8571e+01 -8.6180e+00 --9.6000e+02 5.7143e+01 -8.5597e+00 --9.6000e+02 8.5714e+01 -8.4636e+00 --9.6000e+02 1.1429e+02 -8.3309e+00 --9.6000e+02 1.4286e+02 -8.1635e+00 --9.6000e+02 1.7143e+02 -7.9636e+00 --9.6000e+02 2.0000e+02 -7.7339e+00 --9.6000e+02 2.2857e+02 -7.4771e+00 --9.6000e+02 2.5714e+02 -7.1964e+00 --9.6000e+02 2.8571e+02 -6.8948e+00 --9.6000e+02 3.1429e+02 -6.5756e+00 --9.6000e+02 3.4286e+02 -6.2418e+00 --9.6000e+02 3.7143e+02 -5.8964e+00 --9.6000e+02 4.0000e+02 -5.5424e+00 --9.6000e+02 4.2857e+02 -5.1824e+00 --9.6000e+02 4.5714e+02 -4.8188e+00 --9.6000e+02 4.8571e+02 -4.4539e+00 --9.6000e+02 5.1429e+02 -4.0897e+00 --9.6000e+02 5.4286e+02 -3.7279e+00 --9.6000e+02 5.7143e+02 -3.3700e+00 --9.6000e+02 6.0000e+02 -3.0172e+00 --9.6000e+02 6.2857e+02 -2.6708e+00 --9.6000e+02 6.5714e+02 -2.3315e+00 --9.6000e+02 6.8571e+02 -2.0000e+00 --9.6000e+02 7.1429e+02 -1.6769e+00 --9.6000e+02 7.4286e+02 -1.3626e+00 --9.6000e+02 7.7143e+02 -1.0574e+00 --9.6000e+02 8.0000e+02 -7.6135e-01 --9.6000e+02 8.2857e+02 -4.7467e-01 --9.6000e+02 8.5714e+02 -1.9731e-01 --9.6000e+02 8.8571e+02 7.0775e-02 --9.6000e+02 9.1429e+02 3.2971e-01 --9.6000e+02 9.4286e+02 5.7963e-01 --9.6000e+02 9.7143e+02 8.2075e-01 --9.6000e+02 1.0000e+03 1.0533e+00 --9.6000e+02 1.0286e+03 1.2774e+00 --9.6000e+02 1.0571e+03 1.4934e+00 --9.6000e+02 1.0857e+03 1.7016e+00 --9.6000e+02 1.1143e+03 1.9022e+00 --9.6000e+02 1.1429e+03 2.0955e+00 --9.6000e+02 1.1714e+03 2.2817e+00 --9.6000e+02 1.2000e+03 2.4612e+00 --9.6000e+02 1.2286e+03 2.6341e+00 --9.6000e+02 1.2571e+03 2.8008e+00 --9.6000e+02 1.2857e+03 2.9615e+00 --9.6000e+02 1.3143e+03 3.1164e+00 --9.6000e+02 1.3429e+03 3.2658e+00 --9.6000e+02 1.3714e+03 3.4099e+00 --9.6000e+02 1.4000e+03 3.5489e+00 --9.6000e+02 1.4286e+03 3.6831e+00 --9.6000e+02 1.4571e+03 3.8126e+00 --9.6000e+02 1.4857e+03 3.9377e+00 --9.6000e+02 1.5143e+03 4.0585e+00 --9.6000e+02 1.5429e+03 4.1753e+00 --9.6000e+02 1.5714e+03 4.2882e+00 --9.6000e+02 1.6000e+03 4.3973e+00 --9.6000e+02 1.6286e+03 4.5028e+00 --9.6000e+02 1.6571e+03 4.6049e+00 --9.6000e+02 1.6857e+03 4.7037e+00 --9.6000e+02 1.7143e+03 4.7994e+00 --9.6000e+02 1.7429e+03 4.8920e+00 --9.6000e+02 1.7714e+03 4.9818e+00 --9.6000e+02 1.8000e+03 5.0687e+00 --9.6000e+02 1.8286e+03 5.1531e+00 --9.6000e+02 1.8571e+03 5.2348e+00 --9.6000e+02 1.8857e+03 5.3141e+00 --9.6000e+02 1.9143e+03 5.3911e+00 --9.6000e+02 1.9429e+03 5.4658e+00 --9.6000e+02 1.9714e+03 5.5383e+00 --9.6000e+02 2.0000e+03 5.6088e+00 --9.3000e+02 -2.0000e+03 5.5740e+00 --9.3000e+02 -1.9714e+03 5.5020e+00 --9.3000e+02 -1.9429e+03 5.4278e+00 --9.3000e+02 -1.9143e+03 5.3513e+00 --9.3000e+02 -1.8857e+03 5.2725e+00 --9.3000e+02 -1.8571e+03 5.1913e+00 --9.3000e+02 -1.8286e+03 5.1074e+00 --9.3000e+02 -1.8000e+03 5.0209e+00 --9.3000e+02 -1.7714e+03 4.9316e+00 --9.3000e+02 -1.7429e+03 4.8394e+00 --9.3000e+02 -1.7143e+03 4.7441e+00 --9.3000e+02 -1.6857e+03 4.6456e+00 --9.3000e+02 -1.6571e+03 4.5438e+00 --9.3000e+02 -1.6286e+03 4.4386e+00 --9.3000e+02 -1.6000e+03 4.3296e+00 --9.3000e+02 -1.5714e+03 4.2169e+00 --9.3000e+02 -1.5429e+03 4.1002e+00 --9.3000e+02 -1.5143e+03 3.9793e+00 --9.3000e+02 -1.4857e+03 3.8541e+00 --9.3000e+02 -1.4571e+03 3.7243e+00 --9.3000e+02 -1.4286e+03 3.5897e+00 --9.3000e+02 -1.4000e+03 3.4501e+00 --9.3000e+02 -1.3714e+03 3.3053e+00 --9.3000e+02 -1.3429e+03 3.1550e+00 --9.3000e+02 -1.3143e+03 2.9989e+00 --9.3000e+02 -1.2857e+03 2.8369e+00 --9.3000e+02 -1.2571e+03 2.6685e+00 --9.3000e+02 -1.2286e+03 2.4936e+00 --9.3000e+02 -1.2000e+03 2.3118e+00 --9.3000e+02 -1.1714e+03 2.1227e+00 --9.3000e+02 -1.1429e+03 1.9262e+00 --9.3000e+02 -1.1143e+03 1.7218e+00 --9.3000e+02 -1.0857e+03 1.5093e+00 --9.3000e+02 -1.0571e+03 1.2882e+00 --9.3000e+02 -1.0286e+03 1.0583e+00 --9.3000e+02 -1.0000e+03 8.1915e-01 --9.3000e+02 -9.7143e+02 5.7048e-01 --9.3000e+02 -9.4286e+02 3.1195e-01 --9.3000e+02 -9.1429e+02 4.3236e-02 --9.3000e+02 -8.8571e+02 -2.3594e-01 --9.3000e+02 -8.5714e+02 -5.2583e-01 --9.3000e+02 -8.2857e+02 -8.2665e-01 --9.3000e+02 -8.0000e+02 -1.1386e+00 --9.3000e+02 -7.7143e+02 -1.4617e+00 --9.3000e+02 -7.4286e+02 -1.7961e+00 --9.3000e+02 -7.1429e+02 -2.1415e+00 --9.3000e+02 -6.8571e+02 -2.4980e+00 --9.3000e+02 -6.5714e+02 -2.8650e+00 --9.3000e+02 -6.2857e+02 -3.2422e+00 --9.3000e+02 -6.0000e+02 -3.6288e+00 --9.3000e+02 -5.7143e+02 -4.0240e+00 --9.3000e+02 -5.4286e+02 -4.4267e+00 --9.3000e+02 -5.1429e+02 -4.8354e+00 --9.3000e+02 -4.8571e+02 -5.2486e+00 --9.3000e+02 -4.5714e+02 -5.6643e+00 --9.3000e+02 -4.2857e+02 -6.0802e+00 --9.3000e+02 -4.0000e+02 -6.4937e+00 --9.3000e+02 -3.7143e+02 -6.9019e+00 --9.3000e+02 -3.4286e+02 -7.3018e+00 --9.3000e+02 -3.1429e+02 -7.6897e+00 --9.3000e+02 -2.8571e+02 -8.0621e+00 --9.3000e+02 -2.5714e+02 -8.4151e+00 --9.3000e+02 -2.2857e+02 -8.7448e+00 --9.3000e+02 -2.0000e+02 -9.0472e+00 --9.3000e+02 -1.7143e+02 -9.3185e+00 --9.3000e+02 -1.4286e+02 -9.5551e+00 --9.3000e+02 -1.1429e+02 -9.7537e+00 --9.3000e+02 -8.5714e+01 -9.9113e+00 --9.3000e+02 -5.7143e+01 -1.0026e+01 --9.3000e+02 -2.8571e+01 -1.0095e+01 --9.3000e+02 0.0000e+00 -1.0118e+01 --9.3000e+02 2.8571e+01 -1.0095e+01 --9.3000e+02 5.7143e+01 -1.0026e+01 --9.3000e+02 8.5714e+01 -9.9113e+00 --9.3000e+02 1.1429e+02 -9.7537e+00 --9.3000e+02 1.4286e+02 -9.5551e+00 --9.3000e+02 1.7143e+02 -9.3185e+00 --9.3000e+02 2.0000e+02 -9.0472e+00 --9.3000e+02 2.2857e+02 -8.7448e+00 --9.3000e+02 2.5714e+02 -8.4151e+00 --9.3000e+02 2.8571e+02 -8.0621e+00 --9.3000e+02 3.1429e+02 -7.6897e+00 --9.3000e+02 3.4286e+02 -7.3018e+00 --9.3000e+02 3.7143e+02 -6.9019e+00 --9.3000e+02 4.0000e+02 -6.4937e+00 --9.3000e+02 4.2857e+02 -6.0802e+00 --9.3000e+02 4.5714e+02 -5.6643e+00 --9.3000e+02 4.8571e+02 -5.2486e+00 --9.3000e+02 5.1429e+02 -4.8354e+00 --9.3000e+02 5.4286e+02 -4.4267e+00 --9.3000e+02 5.7143e+02 -4.0240e+00 --9.3000e+02 6.0000e+02 -3.6288e+00 --9.3000e+02 6.2857e+02 -3.2422e+00 --9.3000e+02 6.5714e+02 -2.8650e+00 --9.3000e+02 6.8571e+02 -2.4980e+00 --9.3000e+02 7.1429e+02 -2.1415e+00 --9.3000e+02 7.4286e+02 -1.7961e+00 --9.3000e+02 7.7143e+02 -1.4617e+00 --9.3000e+02 8.0000e+02 -1.1386e+00 --9.3000e+02 8.2857e+02 -8.2665e-01 --9.3000e+02 8.5714e+02 -5.2583e-01 --9.3000e+02 8.8571e+02 -2.3594e-01 --9.3000e+02 9.1429e+02 4.3236e-02 --9.3000e+02 9.4286e+02 3.1195e-01 --9.3000e+02 9.7143e+02 5.7048e-01 --9.3000e+02 1.0000e+03 8.1915e-01 --9.3000e+02 1.0286e+03 1.0583e+00 --9.3000e+02 1.0571e+03 1.2882e+00 --9.3000e+02 1.0857e+03 1.5093e+00 --9.3000e+02 1.1143e+03 1.7218e+00 --9.3000e+02 1.1429e+03 1.9262e+00 --9.3000e+02 1.1714e+03 2.1227e+00 --9.3000e+02 1.2000e+03 2.3118e+00 --9.3000e+02 1.2286e+03 2.4936e+00 --9.3000e+02 1.2571e+03 2.6685e+00 --9.3000e+02 1.2857e+03 2.8369e+00 --9.3000e+02 1.3143e+03 2.9989e+00 --9.3000e+02 1.3429e+03 3.1550e+00 --9.3000e+02 1.3714e+03 3.3053e+00 --9.3000e+02 1.4000e+03 3.4501e+00 --9.3000e+02 1.4286e+03 3.5897e+00 --9.3000e+02 1.4571e+03 3.7243e+00 --9.3000e+02 1.4857e+03 3.8541e+00 --9.3000e+02 1.5143e+03 3.9793e+00 --9.3000e+02 1.5429e+03 4.1002e+00 --9.3000e+02 1.5714e+03 4.2169e+00 --9.3000e+02 1.6000e+03 4.3296e+00 --9.3000e+02 1.6286e+03 4.4386e+00 --9.3000e+02 1.6571e+03 4.5438e+00 --9.3000e+02 1.6857e+03 4.6456e+00 --9.3000e+02 1.7143e+03 4.7441e+00 --9.3000e+02 1.7429e+03 4.8394e+00 --9.3000e+02 1.7714e+03 4.9316e+00 --9.3000e+02 1.8000e+03 5.0209e+00 --9.3000e+02 1.8286e+03 5.1074e+00 --9.3000e+02 1.8571e+03 5.1913e+00 --9.3000e+02 1.8857e+03 5.2725e+00 --9.3000e+02 1.9143e+03 5.3513e+00 --9.3000e+02 1.9429e+03 5.4278e+00 --9.3000e+02 1.9714e+03 5.5020e+00 --9.3000e+02 2.0000e+03 5.5740e+00 --9.0000e+02 -2.0000e+03 5.5395e+00 --9.0000e+02 -1.9714e+03 5.4660e+00 --9.0000e+02 -1.9429e+03 5.3901e+00 --9.0000e+02 -1.9143e+03 5.3119e+00 --9.0000e+02 -1.8857e+03 5.2313e+00 --9.0000e+02 -1.8571e+03 5.1480e+00 --9.0000e+02 -1.8286e+03 5.0621e+00 --9.0000e+02 -1.8000e+03 4.9734e+00 --9.0000e+02 -1.7714e+03 4.8817e+00 --9.0000e+02 -1.7429e+03 4.7870e+00 --9.0000e+02 -1.7143e+03 4.6891e+00 --9.0000e+02 -1.6857e+03 4.5878e+00 --9.0000e+02 -1.6571e+03 4.4829e+00 --9.0000e+02 -1.6286e+03 4.3744e+00 --9.0000e+02 -1.6000e+03 4.2621e+00 --9.0000e+02 -1.5714e+03 4.1457e+00 --9.0000e+02 -1.5429e+03 4.0250e+00 --9.0000e+02 -1.5143e+03 3.8999e+00 --9.0000e+02 -1.4857e+03 3.7702e+00 --9.0000e+02 -1.4571e+03 3.6355e+00 --9.0000e+02 -1.4286e+03 3.4958e+00 --9.0000e+02 -1.4000e+03 3.3506e+00 --9.0000e+02 -1.3714e+03 3.1998e+00 --9.0000e+02 -1.3429e+03 3.0431e+00 --9.0000e+02 -1.3143e+03 2.8801e+00 --9.0000e+02 -1.2857e+03 2.7106e+00 --9.0000e+02 -1.2571e+03 2.5342e+00 --9.0000e+02 -1.2286e+03 2.3507e+00 --9.0000e+02 -1.2000e+03 2.1595e+00 --9.0000e+02 -1.1714e+03 1.9605e+00 --9.0000e+02 -1.1429e+03 1.7531e+00 --9.0000e+02 -1.1143e+03 1.5370e+00 --9.0000e+02 -1.0857e+03 1.3117e+00 --9.0000e+02 -1.0571e+03 1.0770e+00 --9.0000e+02 -1.0286e+03 8.3218e-01 --9.0000e+02 -1.0000e+03 5.7699e-01 --9.0000e+02 -9.7143e+02 3.1094e-01 --9.0000e+02 -9.4286e+02 3.3585e-02 --9.0000e+02 -9.1429e+02 -2.5550e-01 --9.0000e+02 -8.8571e+02 -5.5672e-01 --9.0000e+02 -8.5714e+02 -8.7046e-01 --9.0000e+02 -8.2857e+02 -1.1971e+00 --9.0000e+02 -8.0000e+02 -1.5369e+00 --9.0000e+02 -7.7143e+02 -1.8901e+00 --9.0000e+02 -7.4286e+02 -2.2570e+00 --9.0000e+02 -7.1429e+02 -2.6374e+00 --9.0000e+02 -6.8571e+02 -3.0315e+00 --9.0000e+02 -6.5714e+02 -3.4389e+00 --9.0000e+02 -6.2857e+02 -3.8593e+00 --9.0000e+02 -6.0000e+02 -4.2921e+00 --9.0000e+02 -5.7143e+02 -4.7363e+00 --9.0000e+02 -5.4286e+02 -5.1910e+00 --9.0000e+02 -5.1429e+02 -5.6546e+00 --9.0000e+02 -4.8571e+02 -6.1253e+00 --9.0000e+02 -4.5714e+02 -6.6010e+00 --9.0000e+02 -4.2857e+02 -7.0792e+00 --9.0000e+02 -4.0000e+02 -7.5568e+00 --9.0000e+02 -3.7143e+02 -8.0305e+00 --9.0000e+02 -3.4286e+02 -8.4963e+00 --9.0000e+02 -3.1429e+02 -8.9503e+00 --9.0000e+02 -2.8571e+02 -9.3878e+00 --9.0000e+02 -2.5714e+02 -9.8041e+00 --9.0000e+02 -2.2857e+02 -1.0194e+01 --9.0000e+02 -2.0000e+02 -1.0553e+01 --9.0000e+02 -1.7143e+02 -1.0876e+01 --9.0000e+02 -1.4286e+02 -1.1159e+01 --9.0000e+02 -1.1429e+02 -1.1397e+01 --9.0000e+02 -8.5714e+01 -1.1586e+01 --9.0000e+02 -5.7143e+01 -1.1723e+01 --9.0000e+02 -2.8571e+01 -1.1806e+01 --9.0000e+02 0.0000e+00 -1.1834e+01 --9.0000e+02 2.8571e+01 -1.1806e+01 --9.0000e+02 5.7143e+01 -1.1723e+01 --9.0000e+02 8.5714e+01 -1.1586e+01 --9.0000e+02 1.1429e+02 -1.1397e+01 --9.0000e+02 1.4286e+02 -1.1159e+01 --9.0000e+02 1.7143e+02 -1.0876e+01 --9.0000e+02 2.0000e+02 -1.0553e+01 --9.0000e+02 2.2857e+02 -1.0194e+01 --9.0000e+02 2.5714e+02 -9.8041e+00 --9.0000e+02 2.8571e+02 -9.3878e+00 --9.0000e+02 3.1429e+02 -8.9503e+00 --9.0000e+02 3.4286e+02 -8.4963e+00 --9.0000e+02 3.7143e+02 -8.0305e+00 --9.0000e+02 4.0000e+02 -7.5568e+00 --9.0000e+02 4.2857e+02 -7.0792e+00 --9.0000e+02 4.5714e+02 -6.6010e+00 --9.0000e+02 4.8571e+02 -6.1253e+00 --9.0000e+02 5.1429e+02 -5.6546e+00 --9.0000e+02 5.4286e+02 -5.1910e+00 --9.0000e+02 5.7143e+02 -4.7363e+00 --9.0000e+02 6.0000e+02 -4.2921e+00 --9.0000e+02 6.2857e+02 -3.8593e+00 --9.0000e+02 6.5714e+02 -3.4389e+00 --9.0000e+02 6.8571e+02 -3.0315e+00 --9.0000e+02 7.1429e+02 -2.6374e+00 --9.0000e+02 7.4286e+02 -2.2570e+00 --9.0000e+02 7.7143e+02 -1.8901e+00 --9.0000e+02 8.0000e+02 -1.5369e+00 --9.0000e+02 8.2857e+02 -1.1971e+00 --9.0000e+02 8.5714e+02 -8.7046e-01 --9.0000e+02 8.8571e+02 -5.5672e-01 --9.0000e+02 9.1429e+02 -2.5550e-01 --9.0000e+02 9.4286e+02 3.3585e-02 --9.0000e+02 9.7143e+02 3.1094e-01 --9.0000e+02 1.0000e+03 5.7699e-01 --9.0000e+02 1.0286e+03 8.3218e-01 --9.0000e+02 1.0571e+03 1.0770e+00 --9.0000e+02 1.0857e+03 1.3117e+00 --9.0000e+02 1.1143e+03 1.5370e+00 --9.0000e+02 1.1429e+03 1.7531e+00 --9.0000e+02 1.1714e+03 1.9605e+00 --9.0000e+02 1.2000e+03 2.1595e+00 --9.0000e+02 1.2286e+03 2.3507e+00 --9.0000e+02 1.2571e+03 2.5342e+00 --9.0000e+02 1.2857e+03 2.7106e+00 --9.0000e+02 1.3143e+03 2.8801e+00 --9.0000e+02 1.3429e+03 3.0431e+00 --9.0000e+02 1.3714e+03 3.1998e+00 --9.0000e+02 1.4000e+03 3.3506e+00 --9.0000e+02 1.4286e+03 3.4958e+00 --9.0000e+02 1.4571e+03 3.6355e+00 --9.0000e+02 1.4857e+03 3.7702e+00 --9.0000e+02 1.5143e+03 3.8999e+00 --9.0000e+02 1.5429e+03 4.0250e+00 --9.0000e+02 1.5714e+03 4.1457e+00 --9.0000e+02 1.6000e+03 4.2621e+00 --9.0000e+02 1.6286e+03 4.3744e+00 --9.0000e+02 1.6571e+03 4.4829e+00 --9.0000e+02 1.6857e+03 4.5878e+00 --9.0000e+02 1.7143e+03 4.6891e+00 --9.0000e+02 1.7429e+03 4.7870e+00 --9.0000e+02 1.7714e+03 4.8817e+00 --9.0000e+02 1.8000e+03 4.9734e+00 --9.0000e+02 1.8286e+03 5.0621e+00 --9.0000e+02 1.8571e+03 5.1480e+00 --9.0000e+02 1.8857e+03 5.2313e+00 --9.0000e+02 1.9143e+03 5.3119e+00 --9.0000e+02 1.9429e+03 5.3901e+00 --9.0000e+02 1.9714e+03 5.4660e+00 --9.0000e+02 2.0000e+03 5.5395e+00 --8.7000e+02 -2.0000e+03 5.5055e+00 --8.7000e+02 -1.9714e+03 5.4304e+00 --8.7000e+02 -1.9429e+03 5.3529e+00 --8.7000e+02 -1.9143e+03 5.2730e+00 --8.7000e+02 -1.8857e+03 5.1904e+00 --8.7000e+02 -1.8571e+03 5.1052e+00 --8.7000e+02 -1.8286e+03 5.0172e+00 --8.7000e+02 -1.8000e+03 4.9263e+00 --8.7000e+02 -1.7714e+03 4.8322e+00 --8.7000e+02 -1.7429e+03 4.7350e+00 --8.7000e+02 -1.7143e+03 4.6343e+00 --8.7000e+02 -1.6857e+03 4.5301e+00 --8.7000e+02 -1.6571e+03 4.4223e+00 --8.7000e+02 -1.6286e+03 4.3105e+00 --8.7000e+02 -1.6000e+03 4.1946e+00 --8.7000e+02 -1.5714e+03 4.0744e+00 --8.7000e+02 -1.5429e+03 3.9498e+00 --8.7000e+02 -1.5143e+03 3.8204e+00 --8.7000e+02 -1.4857e+03 3.6860e+00 --8.7000e+02 -1.4571e+03 3.5464e+00 --8.7000e+02 -1.4286e+03 3.4014e+00 --8.7000e+02 -1.4000e+03 3.2505e+00 --8.7000e+02 -1.3714e+03 3.0935e+00 --8.7000e+02 -1.3429e+03 2.9301e+00 --8.7000e+02 -1.3143e+03 2.7600e+00 --8.7000e+02 -1.2857e+03 2.5828e+00 --8.7000e+02 -1.2571e+03 2.3981e+00 --8.7000e+02 -1.2286e+03 2.2055e+00 --8.7000e+02 -1.2000e+03 2.0046e+00 --8.7000e+02 -1.1714e+03 1.7950e+00 --8.7000e+02 -1.1429e+03 1.5762e+00 --8.7000e+02 -1.1143e+03 1.3477e+00 --8.7000e+02 -1.0857e+03 1.1090e+00 --8.7000e+02 -1.0571e+03 8.5968e-01 --8.7000e+02 -1.0286e+03 5.9908e-01 --8.7000e+02 -1.0000e+03 3.2669e-01 --8.7000e+02 -9.7143e+02 4.1952e-02 --8.7000e+02 -9.4286e+02 -2.5571e-01 --8.7000e+02 -9.1429e+02 -5.6688e-01 --8.7000e+02 -8.8571e+02 -8.9211e-01 --8.7000e+02 -8.5714e+02 -1.2320e+00 --8.7000e+02 -8.2857e+02 -1.5870e+00 --8.7000e+02 -8.0000e+02 -1.9576e+00 --8.7000e+02 -7.7143e+02 -2.3443e+00 --8.7000e+02 -7.4286e+02 -2.7474e+00 --8.7000e+02 -7.1429e+02 -3.1672e+00 --8.7000e+02 -6.8571e+02 -3.6038e+00 --8.7000e+02 -6.5714e+02 -4.0571e+00 --8.7000e+02 -6.2857e+02 -4.5269e+00 --8.7000e+02 -6.0000e+02 -5.0127e+00 --8.7000e+02 -5.7143e+02 -5.5139e+00 --8.7000e+02 -5.4286e+02 -6.0291e+00 --8.7000e+02 -5.1429e+02 -6.5571e+00 --8.7000e+02 -4.8571e+02 -7.0959e+00 --8.7000e+02 -4.5714e+02 -7.6431e+00 --8.7000e+02 -4.2857e+02 -8.1959e+00 --8.7000e+02 -4.0000e+02 -8.7507e+00 --8.7000e+02 -3.7143e+02 -9.3037e+00 --8.7000e+02 -3.4286e+02 -9.8502e+00 --8.7000e+02 -3.1429e+02 -1.0385e+01 --8.7000e+02 -2.8571e+02 -1.0903e+01 --8.7000e+02 -2.5714e+02 -1.1398e+01 --8.7000e+02 -2.2857e+02 -1.1864e+01 --8.7000e+02 -2.0000e+02 -1.2294e+01 --8.7000e+02 -1.7143e+02 -1.2682e+01 --8.7000e+02 -1.4286e+02 -1.3022e+01 --8.7000e+02 -1.1429e+02 -1.3309e+01 --8.7000e+02 -8.5714e+01 -1.3538e+01 --8.7000e+02 -5.7143e+01 -1.3704e+01 --8.7000e+02 -2.8571e+01 -1.3806e+01 --8.7000e+02 0.0000e+00 -1.3839e+01 --8.7000e+02 2.8571e+01 -1.3806e+01 --8.7000e+02 5.7143e+01 -1.3704e+01 --8.7000e+02 8.5714e+01 -1.3538e+01 --8.7000e+02 1.1429e+02 -1.3309e+01 --8.7000e+02 1.4286e+02 -1.3022e+01 --8.7000e+02 1.7143e+02 -1.2682e+01 --8.7000e+02 2.0000e+02 -1.2294e+01 --8.7000e+02 2.2857e+02 -1.1864e+01 --8.7000e+02 2.5714e+02 -1.1398e+01 --8.7000e+02 2.8571e+02 -1.0903e+01 --8.7000e+02 3.1429e+02 -1.0385e+01 --8.7000e+02 3.4286e+02 -9.8502e+00 --8.7000e+02 3.7143e+02 -9.3037e+00 --8.7000e+02 4.0000e+02 -8.7507e+00 --8.7000e+02 4.2857e+02 -8.1959e+00 --8.7000e+02 4.5714e+02 -7.6431e+00 --8.7000e+02 4.8571e+02 -7.0959e+00 --8.7000e+02 5.1429e+02 -6.5571e+00 --8.7000e+02 5.4286e+02 -6.0291e+00 --8.7000e+02 5.7143e+02 -5.5139e+00 --8.7000e+02 6.0000e+02 -5.0127e+00 --8.7000e+02 6.2857e+02 -4.5269e+00 --8.7000e+02 6.5714e+02 -4.0571e+00 --8.7000e+02 6.8571e+02 -3.6038e+00 --8.7000e+02 7.1429e+02 -3.1672e+00 --8.7000e+02 7.4286e+02 -2.7474e+00 --8.7000e+02 7.7143e+02 -2.3443e+00 --8.7000e+02 8.0000e+02 -1.9576e+00 --8.7000e+02 8.2857e+02 -1.5870e+00 --8.7000e+02 8.5714e+02 -1.2320e+00 --8.7000e+02 8.8571e+02 -8.9211e-01 --8.7000e+02 9.1429e+02 -5.6688e-01 --8.7000e+02 9.4286e+02 -2.5571e-01 --8.7000e+02 9.7143e+02 4.1952e-02 --8.7000e+02 1.0000e+03 3.2669e-01 --8.7000e+02 1.0286e+03 5.9908e-01 --8.7000e+02 1.0571e+03 8.5968e-01 --8.7000e+02 1.0857e+03 1.1090e+00 --8.7000e+02 1.1143e+03 1.3477e+00 --8.7000e+02 1.1429e+03 1.5762e+00 --8.7000e+02 1.1714e+03 1.7950e+00 --8.7000e+02 1.2000e+03 2.0046e+00 --8.7000e+02 1.2286e+03 2.2055e+00 --8.7000e+02 1.2571e+03 2.3981e+00 --8.7000e+02 1.2857e+03 2.5828e+00 --8.7000e+02 1.3143e+03 2.7600e+00 --8.7000e+02 1.3429e+03 2.9301e+00 --8.7000e+02 1.3714e+03 3.0935e+00 --8.7000e+02 1.4000e+03 3.2505e+00 --8.7000e+02 1.4286e+03 3.4014e+00 --8.7000e+02 1.4571e+03 3.5464e+00 --8.7000e+02 1.4857e+03 3.6860e+00 --8.7000e+02 1.5143e+03 3.8204e+00 --8.7000e+02 1.5429e+03 3.9498e+00 --8.7000e+02 1.5714e+03 4.0744e+00 --8.7000e+02 1.6000e+03 4.1946e+00 --8.7000e+02 1.6286e+03 4.3105e+00 --8.7000e+02 1.6571e+03 4.4223e+00 --8.7000e+02 1.6857e+03 4.5301e+00 --8.7000e+02 1.7143e+03 4.6343e+00 --8.7000e+02 1.7429e+03 4.7350e+00 --8.7000e+02 1.7714e+03 4.8322e+00 --8.7000e+02 1.8000e+03 4.9263e+00 --8.7000e+02 1.8286e+03 5.0172e+00 --8.7000e+02 1.8571e+03 5.1052e+00 --8.7000e+02 1.8857e+03 5.1904e+00 --8.7000e+02 1.9143e+03 5.2730e+00 --8.7000e+02 1.9429e+03 5.3529e+00 --8.7000e+02 1.9714e+03 5.4304e+00 --8.7000e+02 2.0000e+03 5.5055e+00 --8.4000e+02 -2.0000e+03 5.4720e+00 --8.4000e+02 -1.9714e+03 5.3953e+00 --8.4000e+02 -1.9429e+03 5.3161e+00 --8.4000e+02 -1.9143e+03 5.2344e+00 --8.4000e+02 -1.8857e+03 5.1501e+00 --8.4000e+02 -1.8571e+03 5.0629e+00 --8.4000e+02 -1.8286e+03 4.9728e+00 --8.4000e+02 -1.8000e+03 4.8796e+00 --8.4000e+02 -1.7714e+03 4.7831e+00 --8.4000e+02 -1.7429e+03 4.6833e+00 --8.4000e+02 -1.7143e+03 4.5800e+00 --8.4000e+02 -1.6857e+03 4.4729e+00 --8.4000e+02 -1.6571e+03 4.3619e+00 --8.4000e+02 -1.6286e+03 4.2468e+00 --8.4000e+02 -1.6000e+03 4.1274e+00 --8.4000e+02 -1.5714e+03 4.0034e+00 --8.4000e+02 -1.5429e+03 3.8747e+00 --8.4000e+02 -1.5143e+03 3.7409e+00 --8.4000e+02 -1.4857e+03 3.6018e+00 --8.4000e+02 -1.4571e+03 3.4572e+00 --8.4000e+02 -1.4286e+03 3.3066e+00 --8.4000e+02 -1.4000e+03 3.1499e+00 --8.4000e+02 -1.3714e+03 2.9866e+00 --8.4000e+02 -1.3429e+03 2.8164e+00 --8.4000e+02 -1.3143e+03 2.6388e+00 --8.4000e+02 -1.2857e+03 2.4536e+00 --8.4000e+02 -1.2571e+03 2.2603e+00 --8.4000e+02 -1.2286e+03 2.0583e+00 --8.4000e+02 -1.2000e+03 1.8472e+00 --8.4000e+02 -1.1714e+03 1.6266e+00 --8.4000e+02 -1.1429e+03 1.3957e+00 --8.4000e+02 -1.1143e+03 1.1542e+00 --8.4000e+02 -1.0857e+03 9.0131e-01 --8.4000e+02 -1.0571e+03 6.3647e-01 --8.4000e+02 -1.0286e+03 3.5901e-01 --8.4000e+02 -1.0000e+03 6.8225e-02 --8.4000e+02 -9.7143e+02 -2.3659e-01 --8.4000e+02 -9.4286e+02 -5.5617e-01 --8.4000e+02 -9.1429e+02 -8.9127e-01 --8.4000e+02 -8.8571e+02 -1.2426e+00 --8.4000e+02 -8.5714e+02 -1.6111e+00 --8.4000e+02 -8.2857e+02 -1.9973e+00 --8.4000e+02 -8.0000e+02 -2.4020e+00 --8.4000e+02 -7.7143e+02 -2.8259e+00 --8.4000e+02 -7.4286e+02 -3.2696e+00 --8.4000e+02 -7.1429e+02 -3.7336e+00 --8.4000e+02 -6.8571e+02 -4.2183e+00 --8.4000e+02 -6.5714e+02 -4.7239e+00 --8.4000e+02 -6.2857e+02 -5.2504e+00 --8.4000e+02 -6.0000e+02 -5.7975e+00 --8.4000e+02 -5.7143e+02 -6.3647e+00 --8.4000e+02 -5.4286e+02 -6.9510e+00 --8.4000e+02 -5.1429e+02 -7.5549e+00 --8.4000e+02 -4.8571e+02 -8.1745e+00 --8.4000e+02 -4.5714e+02 -8.8072e+00 --8.4000e+02 -4.2857e+02 -9.4498e+00 --8.4000e+02 -4.0000e+02 -1.0098e+01 --8.4000e+02 -3.7143e+02 -1.0748e+01 --8.4000e+02 -3.4286e+02 -1.1394e+01 --8.4000e+02 -3.1429e+02 -1.2029e+01 --8.4000e+02 -2.8571e+02 -1.2647e+01 --8.4000e+02 -2.5714e+02 -1.3240e+01 --8.4000e+02 -2.2857e+02 -1.3800e+01 --8.4000e+02 -2.0000e+02 -1.4320e+01 --8.4000e+02 -1.7143e+02 -1.4791e+01 --8.4000e+02 -1.4286e+02 -1.5205e+01 --8.4000e+02 -1.1429e+02 -1.5555e+01 --8.4000e+02 -8.5714e+01 -1.5834e+01 --8.4000e+02 -5.7143e+01 -1.6038e+01 --8.4000e+02 -2.8571e+01 -1.6162e+01 --8.4000e+02 0.0000e+00 -1.6203e+01 --8.4000e+02 2.8571e+01 -1.6162e+01 --8.4000e+02 5.7143e+01 -1.6038e+01 --8.4000e+02 8.5714e+01 -1.5834e+01 --8.4000e+02 1.1429e+02 -1.5555e+01 --8.4000e+02 1.4286e+02 -1.5205e+01 --8.4000e+02 1.7143e+02 -1.4791e+01 --8.4000e+02 2.0000e+02 -1.4320e+01 --8.4000e+02 2.2857e+02 -1.3800e+01 --8.4000e+02 2.5714e+02 -1.3240e+01 --8.4000e+02 2.8571e+02 -1.2647e+01 --8.4000e+02 3.1429e+02 -1.2029e+01 --8.4000e+02 3.4286e+02 -1.1394e+01 --8.4000e+02 3.7143e+02 -1.0748e+01 --8.4000e+02 4.0000e+02 -1.0098e+01 --8.4000e+02 4.2857e+02 -9.4498e+00 --8.4000e+02 4.5714e+02 -8.8072e+00 --8.4000e+02 4.8571e+02 -8.1745e+00 --8.4000e+02 5.1429e+02 -7.5549e+00 --8.4000e+02 5.4286e+02 -6.9510e+00 --8.4000e+02 5.7143e+02 -6.3647e+00 --8.4000e+02 6.0000e+02 -5.7975e+00 --8.4000e+02 6.2857e+02 -5.2504e+00 --8.4000e+02 6.5714e+02 -4.7239e+00 --8.4000e+02 6.8571e+02 -4.2183e+00 --8.4000e+02 7.1429e+02 -3.7336e+00 --8.4000e+02 7.4286e+02 -3.2696e+00 --8.4000e+02 7.7143e+02 -2.8259e+00 --8.4000e+02 8.0000e+02 -2.4020e+00 --8.4000e+02 8.2857e+02 -1.9973e+00 --8.4000e+02 8.5714e+02 -1.6111e+00 --8.4000e+02 8.8571e+02 -1.2426e+00 --8.4000e+02 9.1429e+02 -8.9127e-01 --8.4000e+02 9.4286e+02 -5.5617e-01 --8.4000e+02 9.7143e+02 -2.3659e-01 --8.4000e+02 1.0000e+03 6.8225e-02 --8.4000e+02 1.0286e+03 3.5901e-01 --8.4000e+02 1.0571e+03 6.3647e-01 --8.4000e+02 1.0857e+03 9.0131e-01 --8.4000e+02 1.1143e+03 1.1542e+00 --8.4000e+02 1.1429e+03 1.3957e+00 --8.4000e+02 1.1714e+03 1.6266e+00 --8.4000e+02 1.2000e+03 1.8472e+00 --8.4000e+02 1.2286e+03 2.0583e+00 --8.4000e+02 1.2571e+03 2.2603e+00 --8.4000e+02 1.2857e+03 2.4536e+00 --8.4000e+02 1.3143e+03 2.6388e+00 --8.4000e+02 1.3429e+03 2.8164e+00 --8.4000e+02 1.3714e+03 2.9866e+00 --8.4000e+02 1.4000e+03 3.1499e+00 --8.4000e+02 1.4286e+03 3.3066e+00 --8.4000e+02 1.4571e+03 3.4572e+00 --8.4000e+02 1.4857e+03 3.6018e+00 --8.4000e+02 1.5143e+03 3.7409e+00 --8.4000e+02 1.5429e+03 3.8747e+00 --8.4000e+02 1.5714e+03 4.0034e+00 --8.4000e+02 1.6000e+03 4.1274e+00 --8.4000e+02 1.6286e+03 4.2468e+00 --8.4000e+02 1.6571e+03 4.3619e+00 --8.4000e+02 1.6857e+03 4.4729e+00 --8.4000e+02 1.7143e+03 4.5800e+00 --8.4000e+02 1.7429e+03 4.6833e+00 --8.4000e+02 1.7714e+03 4.7831e+00 --8.4000e+02 1.8000e+03 4.8796e+00 --8.4000e+02 1.8286e+03 4.9728e+00 --8.4000e+02 1.8571e+03 5.0629e+00 --8.4000e+02 1.8857e+03 5.1501e+00 --8.4000e+02 1.9143e+03 5.2344e+00 --8.4000e+02 1.9429e+03 5.3161e+00 --8.4000e+02 1.9714e+03 5.3953e+00 --8.4000e+02 2.0000e+03 5.4720e+00 --8.1000e+02 -2.0000e+03 5.4389e+00 --8.1000e+02 -1.9714e+03 5.3607e+00 --8.1000e+02 -1.9429e+03 5.2799e+00 --8.1000e+02 -1.9143e+03 5.1965e+00 --8.1000e+02 -1.8857e+03 5.1102e+00 --8.1000e+02 -1.8571e+03 5.0211e+00 --8.1000e+02 -1.8286e+03 4.9288e+00 --8.1000e+02 -1.8000e+03 4.8334e+00 --8.1000e+02 -1.7714e+03 4.7346e+00 --8.1000e+02 -1.7429e+03 4.6322e+00 --8.1000e+02 -1.7143e+03 4.5261e+00 --8.1000e+02 -1.6857e+03 4.4161e+00 --8.1000e+02 -1.6571e+03 4.3020e+00 --8.1000e+02 -1.6286e+03 4.1835e+00 --8.1000e+02 -1.6000e+03 4.0605e+00 --8.1000e+02 -1.5714e+03 3.9327e+00 --8.1000e+02 -1.5429e+03 3.7998e+00 --8.1000e+02 -1.5143e+03 3.6616e+00 --8.1000e+02 -1.4857e+03 3.5177e+00 --8.1000e+02 -1.4571e+03 3.3679e+00 --8.1000e+02 -1.4286e+03 3.2118e+00 --8.1000e+02 -1.4000e+03 3.0490e+00 --8.1000e+02 -1.3714e+03 2.8792e+00 --8.1000e+02 -1.3429e+03 2.7019e+00 --8.1000e+02 -1.3143e+03 2.5168e+00 --8.1000e+02 -1.2857e+03 2.3233e+00 --8.1000e+02 -1.2571e+03 2.1209e+00 --8.1000e+02 -1.2286e+03 1.9092e+00 --8.1000e+02 -1.2000e+03 1.6875e+00 --8.1000e+02 -1.1714e+03 1.4553e+00 --8.1000e+02 -1.1429e+03 1.2119e+00 --8.1000e+02 -1.1143e+03 9.5659e-01 --8.1000e+02 -1.0857e+03 6.8871e-01 --8.1000e+02 -1.0571e+03 4.0746e-01 --8.1000e+02 -1.0286e+03 1.1205e-01 --8.1000e+02 -1.0000e+03 -1.9839e-01 --8.1000e+02 -9.7143e+02 -5.2474e-01 --8.1000e+02 -9.4286e+02 -8.6794e-01 --8.1000e+02 -9.1429e+02 -1.2290e+00 --8.1000e+02 -8.8571e+02 -1.6088e+00 --8.1000e+02 -8.5714e+02 -2.0084e+00 --8.1000e+02 -8.2857e+02 -2.4290e+00 --8.1000e+02 -8.0000e+02 -2.8713e+00 --8.1000e+02 -7.7143e+02 -3.3366e+00 --8.1000e+02 -7.4286e+02 -3.8257e+00 --8.1000e+02 -7.1429e+02 -4.3395e+00 --8.1000e+02 -6.8571e+02 -4.8788e+00 --8.1000e+02 -6.5714e+02 -5.4441e+00 --8.1000e+02 -6.2857e+02 -6.0357e+00 --8.1000e+02 -6.0000e+02 -6.6539e+00 --8.1000e+02 -5.7143e+02 -7.2982e+00 --8.1000e+02 -5.4286e+02 -7.9679e+00 --8.1000e+02 -5.1429e+02 -8.6617e+00 --8.1000e+02 -4.8571e+02 -9.3777e+00 --8.1000e+02 -4.5714e+02 -1.0113e+01 --8.1000e+02 -4.2857e+02 -1.0865e+01 --8.1000e+02 -4.0000e+02 -1.1627e+01 --8.1000e+02 -3.7143e+02 -1.2396e+01 --8.1000e+02 -3.4286e+02 -1.3164e+01 --8.1000e+02 -3.1429e+02 -1.3924e+01 --8.1000e+02 -2.8571e+02 -1.4667e+01 --8.1000e+02 -2.5714e+02 -1.5384e+01 --8.1000e+02 -2.2857e+02 -1.6064e+01 --8.1000e+02 -2.0000e+02 -1.6698e+01 --8.1000e+02 -1.7143e+02 -1.7274e+01 --8.1000e+02 -1.4286e+02 -1.7782e+01 --8.1000e+02 -1.1429e+02 -1.8213e+01 --8.1000e+02 -8.5714e+01 -1.8558e+01 --8.1000e+02 -5.7143e+01 -1.8809e+01 --8.1000e+02 -2.8571e+01 -1.8963e+01 --8.1000e+02 0.0000e+00 -1.9014e+01 --8.1000e+02 2.8571e+01 -1.8963e+01 --8.1000e+02 5.7143e+01 -1.8809e+01 --8.1000e+02 8.5714e+01 -1.8558e+01 --8.1000e+02 1.1429e+02 -1.8213e+01 --8.1000e+02 1.4286e+02 -1.7782e+01 --8.1000e+02 1.7143e+02 -1.7274e+01 --8.1000e+02 2.0000e+02 -1.6698e+01 --8.1000e+02 2.2857e+02 -1.6064e+01 --8.1000e+02 2.5714e+02 -1.5384e+01 --8.1000e+02 2.8571e+02 -1.4667e+01 --8.1000e+02 3.1429e+02 -1.3924e+01 --8.1000e+02 3.4286e+02 -1.3164e+01 --8.1000e+02 3.7143e+02 -1.2396e+01 --8.1000e+02 4.0000e+02 -1.1627e+01 --8.1000e+02 4.2857e+02 -1.0865e+01 --8.1000e+02 4.5714e+02 -1.0113e+01 --8.1000e+02 4.8571e+02 -9.3777e+00 --8.1000e+02 5.1429e+02 -8.6617e+00 --8.1000e+02 5.4286e+02 -7.9679e+00 --8.1000e+02 5.7143e+02 -7.2982e+00 --8.1000e+02 6.0000e+02 -6.6539e+00 --8.1000e+02 6.2857e+02 -6.0357e+00 --8.1000e+02 6.5714e+02 -5.4441e+00 --8.1000e+02 6.8571e+02 -4.8788e+00 --8.1000e+02 7.1429e+02 -4.3395e+00 --8.1000e+02 7.4286e+02 -3.8257e+00 --8.1000e+02 7.7143e+02 -3.3366e+00 --8.1000e+02 8.0000e+02 -2.8713e+00 --8.1000e+02 8.2857e+02 -2.4290e+00 --8.1000e+02 8.5714e+02 -2.0084e+00 --8.1000e+02 8.8571e+02 -1.6088e+00 --8.1000e+02 9.1429e+02 -1.2290e+00 --8.1000e+02 9.4286e+02 -8.6794e-01 --8.1000e+02 9.7143e+02 -5.2474e-01 --8.1000e+02 1.0000e+03 -1.9839e-01 --8.1000e+02 1.0286e+03 1.1205e-01 --8.1000e+02 1.0571e+03 4.0746e-01 --8.1000e+02 1.0857e+03 6.8871e-01 --8.1000e+02 1.1143e+03 9.5659e-01 --8.1000e+02 1.1429e+03 1.2119e+00 --8.1000e+02 1.1714e+03 1.4553e+00 --8.1000e+02 1.2000e+03 1.6875e+00 --8.1000e+02 1.2286e+03 1.9092e+00 --8.1000e+02 1.2571e+03 2.1209e+00 --8.1000e+02 1.2857e+03 2.3233e+00 --8.1000e+02 1.3143e+03 2.5168e+00 --8.1000e+02 1.3429e+03 2.7019e+00 --8.1000e+02 1.3714e+03 2.8792e+00 --8.1000e+02 1.4000e+03 3.0490e+00 --8.1000e+02 1.4286e+03 3.2118e+00 --8.1000e+02 1.4571e+03 3.3679e+00 --8.1000e+02 1.4857e+03 3.5177e+00 --8.1000e+02 1.5143e+03 3.6616e+00 --8.1000e+02 1.5429e+03 3.7998e+00 --8.1000e+02 1.5714e+03 3.9327e+00 --8.1000e+02 1.6000e+03 4.0605e+00 --8.1000e+02 1.6286e+03 4.1835e+00 --8.1000e+02 1.6571e+03 4.3020e+00 --8.1000e+02 1.6857e+03 4.4161e+00 --8.1000e+02 1.7143e+03 4.5261e+00 --8.1000e+02 1.7429e+03 4.6322e+00 --8.1000e+02 1.7714e+03 4.7346e+00 --8.1000e+02 1.8000e+03 4.8334e+00 --8.1000e+02 1.8286e+03 4.9288e+00 --8.1000e+02 1.8571e+03 5.0211e+00 --8.1000e+02 1.8857e+03 5.1102e+00 --8.1000e+02 1.9143e+03 5.1965e+00 --8.1000e+02 1.9429e+03 5.2799e+00 --8.1000e+02 1.9714e+03 5.3607e+00 --8.1000e+02 2.0000e+03 5.4389e+00 --7.8000e+02 -2.0000e+03 5.4065e+00 --7.8000e+02 -1.9714e+03 5.3267e+00 --7.8000e+02 -1.9429e+03 5.2443e+00 --7.8000e+02 -1.9143e+03 5.1591e+00 --7.8000e+02 -1.8857e+03 5.0710e+00 --7.8000e+02 -1.8571e+03 4.9799e+00 --7.8000e+02 -1.8286e+03 4.8855e+00 --7.8000e+02 -1.8000e+03 4.7878e+00 --7.8000e+02 -1.7714e+03 4.6866e+00 --7.8000e+02 -1.7429e+03 4.5817e+00 --7.8000e+02 -1.7143e+03 4.4728e+00 --7.8000e+02 -1.6857e+03 4.3599e+00 --7.8000e+02 -1.6571e+03 4.2426e+00 --7.8000e+02 -1.6286e+03 4.1208e+00 --7.8000e+02 -1.6000e+03 3.9941e+00 --7.8000e+02 -1.5714e+03 3.8624e+00 --7.8000e+02 -1.5429e+03 3.7253e+00 --7.8000e+02 -1.5143e+03 3.5826e+00 --7.8000e+02 -1.4857e+03 3.4338e+00 --7.8000e+02 -1.4571e+03 3.2787e+00 --7.8000e+02 -1.4286e+03 3.1169e+00 --7.8000e+02 -1.4000e+03 2.9480e+00 --7.8000e+02 -1.3714e+03 2.7715e+00 --7.8000e+02 -1.3429e+03 2.5870e+00 --7.8000e+02 -1.3143e+03 2.3940e+00 --7.8000e+02 -1.2857e+03 2.1920e+00 --7.8000e+02 -1.2571e+03 1.9803e+00 --7.8000e+02 -1.2286e+03 1.7585e+00 --7.8000e+02 -1.2000e+03 1.5258e+00 --7.8000e+02 -1.1714e+03 1.2815e+00 --7.8000e+02 -1.1429e+03 1.0249e+00 --7.8000e+02 -1.1143e+03 7.5515e-01 --7.8000e+02 -1.0857e+03 4.7144e-01 --7.8000e+02 -1.0571e+03 1.7283e-01 --7.8000e+02 -1.0286e+03 -1.4166e-01 --7.8000e+02 -1.0000e+03 -4.7306e-01 --7.8000e+02 -9.7143e+02 -8.2250e-01 --7.8000e+02 -9.4286e+02 -1.1911e+00 --7.8000e+02 -9.1429e+02 -1.5802e+00 --7.8000e+02 -8.8571e+02 -1.9909e+00 --7.8000e+02 -8.5714e+02 -2.4247e+00 --7.8000e+02 -8.2857e+02 -2.8830e+00 --7.8000e+02 -8.0000e+02 -3.3670e+00 --7.8000e+02 -7.7143e+02 -3.8783e+00 --7.8000e+02 -7.4286e+02 -4.4183e+00 --7.8000e+02 -7.1429e+02 -4.9882e+00 --7.8000e+02 -6.8571e+02 -5.5894e+00 --7.8000e+02 -6.5714e+02 -6.2230e+00 --7.8000e+02 -6.2857e+02 -6.8897e+00 --7.8000e+02 -6.0000e+02 -7.5902e+00 --7.8000e+02 -5.7143e+02 -8.3247e+00 --7.8000e+02 -5.4286e+02 -9.0928e+00 --7.8000e+02 -5.1429e+02 -9.8934e+00 --7.8000e+02 -4.8571e+02 -1.0725e+01 --7.8000e+02 -4.5714e+02 -1.1585e+01 --7.8000e+02 -4.2857e+02 -1.2468e+01 --7.8000e+02 -4.0000e+02 -1.3371e+01 --7.8000e+02 -3.7143e+02 -1.4287e+01 --7.8000e+02 -3.4286e+02 -1.5207e+01 --7.8000e+02 -3.1429e+02 -1.6123e+01 --7.8000e+02 -2.8571e+02 -1.7024e+01 --7.8000e+02 -2.5714e+02 -1.7897e+01 --7.8000e+02 -2.2857e+02 -1.8730e+01 --7.8000e+02 -2.0000e+02 -1.9509e+01 --7.8000e+02 -1.7143e+02 -2.0220e+01 --7.8000e+02 -1.4286e+02 -2.0849e+01 --7.8000e+02 -1.1429e+02 -2.1384e+01 --7.8000e+02 -8.5714e+01 -2.1813e+01 --7.8000e+02 -5.7143e+01 -2.2126e+01 --7.8000e+02 -2.8571e+01 -2.2317e+01 --7.8000e+02 0.0000e+00 -2.2382e+01 --7.8000e+02 2.8571e+01 -2.2317e+01 --7.8000e+02 5.7143e+01 -2.2126e+01 --7.8000e+02 8.5714e+01 -2.1813e+01 --7.8000e+02 1.1429e+02 -2.1384e+01 --7.8000e+02 1.4286e+02 -2.0849e+01 --7.8000e+02 1.7143e+02 -2.0220e+01 --7.8000e+02 2.0000e+02 -1.9509e+01 --7.8000e+02 2.2857e+02 -1.8730e+01 --7.8000e+02 2.5714e+02 -1.7897e+01 --7.8000e+02 2.8571e+02 -1.7024e+01 --7.8000e+02 3.1429e+02 -1.6123e+01 --7.8000e+02 3.4286e+02 -1.5207e+01 --7.8000e+02 3.7143e+02 -1.4287e+01 --7.8000e+02 4.0000e+02 -1.3371e+01 --7.8000e+02 4.2857e+02 -1.2468e+01 --7.8000e+02 4.5714e+02 -1.1585e+01 --7.8000e+02 4.8571e+02 -1.0725e+01 --7.8000e+02 5.1429e+02 -9.8934e+00 --7.8000e+02 5.4286e+02 -9.0928e+00 --7.8000e+02 5.7143e+02 -8.3247e+00 --7.8000e+02 6.0000e+02 -7.5902e+00 --7.8000e+02 6.2857e+02 -6.8897e+00 --7.8000e+02 6.5714e+02 -6.2230e+00 --7.8000e+02 6.8571e+02 -5.5894e+00 --7.8000e+02 7.1429e+02 -4.9882e+00 --7.8000e+02 7.4286e+02 -4.4183e+00 --7.8000e+02 7.7143e+02 -3.8783e+00 --7.8000e+02 8.0000e+02 -3.3670e+00 --7.8000e+02 8.2857e+02 -2.8830e+00 --7.8000e+02 8.5714e+02 -2.4247e+00 --7.8000e+02 8.8571e+02 -1.9909e+00 --7.8000e+02 9.1429e+02 -1.5802e+00 --7.8000e+02 9.4286e+02 -1.1911e+00 --7.8000e+02 9.7143e+02 -8.2250e-01 --7.8000e+02 1.0000e+03 -4.7306e-01 --7.8000e+02 1.0286e+03 -1.4166e-01 --7.8000e+02 1.0571e+03 1.7283e-01 --7.8000e+02 1.0857e+03 4.7144e-01 --7.8000e+02 1.1143e+03 7.5515e-01 --7.8000e+02 1.1429e+03 1.0249e+00 --7.8000e+02 1.1714e+03 1.2815e+00 --7.8000e+02 1.2000e+03 1.5258e+00 --7.8000e+02 1.2286e+03 1.7585e+00 --7.8000e+02 1.2571e+03 1.9803e+00 --7.8000e+02 1.2857e+03 2.1920e+00 --7.8000e+02 1.3143e+03 2.3940e+00 --7.8000e+02 1.3429e+03 2.5870e+00 --7.8000e+02 1.3714e+03 2.7715e+00 --7.8000e+02 1.4000e+03 2.9480e+00 --7.8000e+02 1.4286e+03 3.1169e+00 --7.8000e+02 1.4571e+03 3.2787e+00 --7.8000e+02 1.4857e+03 3.4338e+00 --7.8000e+02 1.5143e+03 3.5826e+00 --7.8000e+02 1.5429e+03 3.7253e+00 --7.8000e+02 1.5714e+03 3.8624e+00 --7.8000e+02 1.6000e+03 3.9941e+00 --7.8000e+02 1.6286e+03 4.1208e+00 --7.8000e+02 1.6571e+03 4.2426e+00 --7.8000e+02 1.6857e+03 4.3599e+00 --7.8000e+02 1.7143e+03 4.4728e+00 --7.8000e+02 1.7429e+03 4.5817e+00 --7.8000e+02 1.7714e+03 4.6866e+00 --7.8000e+02 1.8000e+03 4.7878e+00 --7.8000e+02 1.8286e+03 4.8855e+00 --7.8000e+02 1.8571e+03 4.9799e+00 --7.8000e+02 1.8857e+03 5.0710e+00 --7.8000e+02 1.9143e+03 5.1591e+00 --7.8000e+02 1.9429e+03 5.2443e+00 --7.8000e+02 1.9714e+03 5.3267e+00 --7.8000e+02 2.0000e+03 5.4065e+00 --7.5000e+02 -2.0000e+03 5.3747e+00 --7.5000e+02 -1.9714e+03 5.2934e+00 --7.5000e+02 -1.9429e+03 5.2093e+00 --7.5000e+02 -1.9143e+03 5.1224e+00 --7.5000e+02 -1.8857e+03 5.0324e+00 --7.5000e+02 -1.8571e+03 4.9393e+00 --7.5000e+02 -1.8286e+03 4.8429e+00 --7.5000e+02 -1.8000e+03 4.7429e+00 --7.5000e+02 -1.7714e+03 4.6393e+00 --7.5000e+02 -1.7429e+03 4.5318e+00 --7.5000e+02 -1.7143e+03 4.4202e+00 --7.5000e+02 -1.6857e+03 4.3043e+00 --7.5000e+02 -1.6571e+03 4.1839e+00 --7.5000e+02 -1.6286e+03 4.0587e+00 --7.5000e+02 -1.6000e+03 3.9284e+00 --7.5000e+02 -1.5714e+03 3.7927e+00 --7.5000e+02 -1.5429e+03 3.6514e+00 --7.5000e+02 -1.5143e+03 3.5040e+00 --7.5000e+02 -1.4857e+03 3.3503e+00 --7.5000e+02 -1.4571e+03 3.1899e+00 --7.5000e+02 -1.4286e+03 3.0223e+00 --7.5000e+02 -1.4000e+03 2.8471e+00 --7.5000e+02 -1.3714e+03 2.6638e+00 --7.5000e+02 -1.3429e+03 2.4719e+00 --7.5000e+02 -1.3143e+03 2.2708e+00 --7.5000e+02 -1.2857e+03 2.0600e+00 --7.5000e+02 -1.2571e+03 1.8387e+00 --7.5000e+02 -1.2286e+03 1.6064e+00 --7.5000e+02 -1.2000e+03 1.3622e+00 --7.5000e+02 -1.1714e+03 1.1054e+00 --7.5000e+02 -1.1429e+03 8.3500e-01 --7.5000e+02 -1.1143e+03 5.5014e-01 --7.5000e+02 -1.0857e+03 2.4978e-01 --7.5000e+02 -1.0571e+03 -6.7162e-02 --7.5000e+02 -1.0286e+03 -4.0187e-01 --7.5000e+02 -1.0000e+03 -7.5562e-01 --7.5000e+02 -9.7143e+02 -1.1297e+00 --7.5000e+02 -9.4286e+02 -1.5257e+00 --7.5000e+02 -9.1429e+02 -1.9450e+00 --7.5000e+02 -8.8571e+02 -2.3894e+00 --7.5000e+02 -8.5714e+02 -2.8605e+00 --7.5000e+02 -8.2857e+02 -3.3602e+00 --7.5000e+02 -8.0000e+02 -3.8903e+00 --7.5000e+02 -7.7143e+02 -4.4528e+00 --7.5000e+02 -7.4286e+02 -5.0497e+00 --7.5000e+02 -7.1429e+02 -5.6830e+00 --7.5000e+02 -6.8571e+02 -6.3546e+00 --7.5000e+02 -6.5714e+02 -7.0663e+00 --7.5000e+02 -6.2857e+02 -7.8196e+00 --7.5000e+02 -6.0000e+02 -8.6160e+00 --7.5000e+02 -5.7143e+02 -9.4562e+00 --7.5000e+02 -5.4286e+02 -1.0341e+01 --7.5000e+02 -5.1429e+02 -1.1269e+01 --7.5000e+02 -4.8571e+02 -1.2239e+01 --7.5000e+02 -4.5714e+02 -1.3249e+01 --7.5000e+02 -4.2857e+02 -1.4295e+01 --7.5000e+02 -4.0000e+02 -1.5371e+01 --7.5000e+02 -3.7143e+02 -1.6468e+01 --7.5000e+02 -3.4286e+02 -1.7579e+01 --7.5000e+02 -3.1429e+02 -1.8691e+01 --7.5000e+02 -2.8571e+02 -1.9791e+01 --7.5000e+02 -2.5714e+02 -2.0862e+01 --7.5000e+02 -2.2857e+02 -2.1889e+01 --7.5000e+02 -2.0000e+02 -2.2853e+01 --7.5000e+02 -1.7143e+02 -2.3736e+01 --7.5000e+02 -1.4286e+02 -2.4520e+01 --7.5000e+02 -1.1429e+02 -2.5188e+01 --7.5000e+02 -8.5714e+01 -2.5724e+01 --7.5000e+02 -5.7143e+01 -2.6117e+01 --7.5000e+02 -2.8571e+01 -2.6357e+01 --7.5000e+02 0.0000e+00 -2.6437e+01 --7.5000e+02 2.8571e+01 -2.6357e+01 --7.5000e+02 5.7143e+01 -2.6117e+01 --7.5000e+02 8.5714e+01 -2.5724e+01 --7.5000e+02 1.1429e+02 -2.5188e+01 --7.5000e+02 1.4286e+02 -2.4520e+01 --7.5000e+02 1.7143e+02 -2.3736e+01 --7.5000e+02 2.0000e+02 -2.2853e+01 --7.5000e+02 2.2857e+02 -2.1889e+01 --7.5000e+02 2.5714e+02 -2.0862e+01 --7.5000e+02 2.8571e+02 -1.9791e+01 --7.5000e+02 3.1429e+02 -1.8691e+01 --7.5000e+02 3.4286e+02 -1.7579e+01 --7.5000e+02 3.7143e+02 -1.6468e+01 --7.5000e+02 4.0000e+02 -1.5371e+01 --7.5000e+02 4.2857e+02 -1.4295e+01 --7.5000e+02 4.5714e+02 -1.3249e+01 --7.5000e+02 4.8571e+02 -1.2239e+01 --7.5000e+02 5.1429e+02 -1.1269e+01 --7.5000e+02 5.4286e+02 -1.0341e+01 --7.5000e+02 5.7143e+02 -9.4562e+00 --7.5000e+02 6.0000e+02 -8.6160e+00 --7.5000e+02 6.2857e+02 -7.8196e+00 --7.5000e+02 6.5714e+02 -7.0663e+00 --7.5000e+02 6.8571e+02 -6.3546e+00 --7.5000e+02 7.1429e+02 -5.6830e+00 --7.5000e+02 7.4286e+02 -5.0497e+00 --7.5000e+02 7.7143e+02 -4.4528e+00 --7.5000e+02 8.0000e+02 -3.8903e+00 --7.5000e+02 8.2857e+02 -3.3602e+00 --7.5000e+02 8.5714e+02 -2.8605e+00 --7.5000e+02 8.8571e+02 -2.3894e+00 --7.5000e+02 9.1429e+02 -1.9450e+00 --7.5000e+02 9.4286e+02 -1.5257e+00 --7.5000e+02 9.7143e+02 -1.1297e+00 --7.5000e+02 1.0000e+03 -7.5562e-01 --7.5000e+02 1.0286e+03 -4.0187e-01 --7.5000e+02 1.0571e+03 -6.7162e-02 --7.5000e+02 1.0857e+03 2.4978e-01 --7.5000e+02 1.1143e+03 5.5014e-01 --7.5000e+02 1.1429e+03 8.3500e-01 --7.5000e+02 1.1714e+03 1.1054e+00 --7.5000e+02 1.2000e+03 1.3622e+00 --7.5000e+02 1.2286e+03 1.6064e+00 --7.5000e+02 1.2571e+03 1.8387e+00 --7.5000e+02 1.2857e+03 2.0600e+00 --7.5000e+02 1.3143e+03 2.2708e+00 --7.5000e+02 1.3429e+03 2.4719e+00 --7.5000e+02 1.3714e+03 2.6638e+00 --7.5000e+02 1.4000e+03 2.8471e+00 --7.5000e+02 1.4286e+03 3.0223e+00 --7.5000e+02 1.4571e+03 3.1899e+00 --7.5000e+02 1.4857e+03 3.3503e+00 --7.5000e+02 1.5143e+03 3.5040e+00 --7.5000e+02 1.5429e+03 3.6514e+00 --7.5000e+02 1.5714e+03 3.7927e+00 --7.5000e+02 1.6000e+03 3.9284e+00 --7.5000e+02 1.6286e+03 4.0587e+00 --7.5000e+02 1.6571e+03 4.1839e+00 --7.5000e+02 1.6857e+03 4.3043e+00 --7.5000e+02 1.7143e+03 4.4202e+00 --7.5000e+02 1.7429e+03 4.5318e+00 --7.5000e+02 1.7714e+03 4.6393e+00 --7.5000e+02 1.8000e+03 4.7429e+00 --7.5000e+02 1.8286e+03 4.8429e+00 --7.5000e+02 1.8571e+03 4.9393e+00 --7.5000e+02 1.8857e+03 5.0324e+00 --7.5000e+02 1.9143e+03 5.1224e+00 --7.5000e+02 1.9429e+03 5.2093e+00 --7.5000e+02 1.9714e+03 5.2934e+00 --7.5000e+02 2.0000e+03 5.3747e+00 --7.2000e+02 -2.0000e+03 5.3435e+00 --7.2000e+02 -1.9714e+03 5.2607e+00 --7.2000e+02 -1.9429e+03 5.1750e+00 --7.2000e+02 -1.9143e+03 5.0864e+00 --7.2000e+02 -1.8857e+03 4.9946e+00 --7.2000e+02 -1.8571e+03 4.8995e+00 --7.2000e+02 -1.8286e+03 4.8010e+00 --7.2000e+02 -1.8000e+03 4.6988e+00 --7.2000e+02 -1.7714e+03 4.5928e+00 --7.2000e+02 -1.7429e+03 4.4827e+00 --7.2000e+02 -1.7143e+03 4.3684e+00 --7.2000e+02 -1.6857e+03 4.2496e+00 --7.2000e+02 -1.6571e+03 4.1259e+00 --7.2000e+02 -1.6286e+03 3.9973e+00 --7.2000e+02 -1.6000e+03 3.8633e+00 --7.2000e+02 -1.5714e+03 3.7237e+00 --7.2000e+02 -1.5429e+03 3.5781e+00 --7.2000e+02 -1.5143e+03 3.4261e+00 --7.2000e+02 -1.4857e+03 3.2674e+00 --7.2000e+02 -1.4571e+03 3.1016e+00 --7.2000e+02 -1.4286e+03 2.9281e+00 --7.2000e+02 -1.4000e+03 2.7465e+00 --7.2000e+02 -1.3714e+03 2.5562e+00 --7.2000e+02 -1.3429e+03 2.3567e+00 --7.2000e+02 -1.3143e+03 2.1474e+00 --7.2000e+02 -1.2857e+03 1.9276e+00 --7.2000e+02 -1.2571e+03 1.6965e+00 --7.2000e+02 -1.2286e+03 1.4533e+00 --7.2000e+02 -1.2000e+03 1.1973e+00 --7.2000e+02 -1.1714e+03 9.2737e-01 --7.2000e+02 -1.1429e+03 6.4262e-01 --7.2000e+02 -1.1143e+03 3.4193e-01 --7.2000e+02 -1.0857e+03 2.4092e-02 --7.2000e+02 -1.0571e+03 -3.1218e-01 --7.2000e+02 -1.0286e+03 -6.6830e-01 --7.2000e+02 -1.0000e+03 -1.0458e+00 --7.2000e+02 -9.7143e+02 -1.4463e+00 --7.2000e+02 -9.4286e+02 -1.8716e+00 --7.2000e+02 -9.1429e+02 -2.3236e+00 --7.2000e+02 -8.8571e+02 -2.8044e+00 --7.2000e+02 -8.5714e+02 -3.3162e+00 --7.2000e+02 -8.2857e+02 -3.8613e+00 --7.2000e+02 -8.0000e+02 -4.4424e+00 --7.2000e+02 -7.7143e+02 -5.0618e+00 --7.2000e+02 -7.4286e+02 -5.7226e+00 --7.2000e+02 -7.1429e+02 -6.4273e+00 --7.2000e+02 -6.8571e+02 -7.1789e+00 --7.2000e+02 -6.5714e+02 -7.9801e+00 --7.2000e+02 -6.2857e+02 -8.8336e+00 --7.2000e+02 -6.0000e+02 -9.7417e+00 --7.2000e+02 -5.7143e+02 -1.0706e+01 --7.2000e+02 -5.4286e+02 -1.1729e+01 --7.2000e+02 -5.1429e+02 -1.2809e+01 --7.2000e+02 -4.8571e+02 -1.3947e+01 --7.2000e+02 -4.5714e+02 -1.5140e+01 --7.2000e+02 -4.2857e+02 -1.6385e+01 --7.2000e+02 -4.0000e+02 -1.7673e+01 --7.2000e+02 -3.7143e+02 -1.8998e+01 --7.2000e+02 -3.4286e+02 -2.0346e+01 --7.2000e+02 -3.1429e+02 -2.1704e+01 --7.2000e+02 -2.8571e+02 -2.3054e+01 --7.2000e+02 -2.5714e+02 -2.4377e+01 --7.2000e+02 -2.2857e+02 -2.5648e+01 --7.2000e+02 -2.0000e+02 -2.6846e+01 --7.2000e+02 -1.7143e+02 -2.7947e+01 --7.2000e+02 -1.4286e+02 -2.8925e+01 --7.2000e+02 -1.1429e+02 -2.9760e+01 --7.2000e+02 -8.5714e+01 -3.0431e+01 --7.2000e+02 -5.7143e+01 -3.0923e+01 --7.2000e+02 -2.8571e+01 -3.1223e+01 --7.2000e+02 0.0000e+00 -3.1323e+01 --7.2000e+02 2.8571e+01 -3.1223e+01 --7.2000e+02 5.7143e+01 -3.0923e+01 --7.2000e+02 8.5714e+01 -3.0431e+01 --7.2000e+02 1.1429e+02 -2.9760e+01 --7.2000e+02 1.4286e+02 -2.8925e+01 --7.2000e+02 1.7143e+02 -2.7947e+01 --7.2000e+02 2.0000e+02 -2.6846e+01 --7.2000e+02 2.2857e+02 -2.5648e+01 --7.2000e+02 2.5714e+02 -2.4377e+01 --7.2000e+02 2.8571e+02 -2.3054e+01 --7.2000e+02 3.1429e+02 -2.1704e+01 --7.2000e+02 3.4286e+02 -2.0346e+01 --7.2000e+02 3.7143e+02 -1.8998e+01 --7.2000e+02 4.0000e+02 -1.7673e+01 --7.2000e+02 4.2857e+02 -1.6385e+01 --7.2000e+02 4.5714e+02 -1.5140e+01 --7.2000e+02 4.8571e+02 -1.3947e+01 --7.2000e+02 5.1429e+02 -1.2809e+01 --7.2000e+02 5.4286e+02 -1.1729e+01 --7.2000e+02 5.7143e+02 -1.0706e+01 --7.2000e+02 6.0000e+02 -9.7417e+00 --7.2000e+02 6.2857e+02 -8.8336e+00 --7.2000e+02 6.5714e+02 -7.9801e+00 --7.2000e+02 6.8571e+02 -7.1789e+00 --7.2000e+02 7.1429e+02 -6.4273e+00 --7.2000e+02 7.4286e+02 -5.7226e+00 --7.2000e+02 7.7143e+02 -5.0618e+00 --7.2000e+02 8.0000e+02 -4.4424e+00 --7.2000e+02 8.2857e+02 -3.8613e+00 --7.2000e+02 8.5714e+02 -3.3162e+00 --7.2000e+02 8.8571e+02 -2.8044e+00 --7.2000e+02 9.1429e+02 -2.3236e+00 --7.2000e+02 9.4286e+02 -1.8716e+00 --7.2000e+02 9.7143e+02 -1.4463e+00 --7.2000e+02 1.0000e+03 -1.0458e+00 --7.2000e+02 1.0286e+03 -6.6830e-01 --7.2000e+02 1.0571e+03 -3.1218e-01 --7.2000e+02 1.0857e+03 2.4092e-02 --7.2000e+02 1.1143e+03 3.4193e-01 --7.2000e+02 1.1429e+03 6.4262e-01 --7.2000e+02 1.1714e+03 9.2737e-01 --7.2000e+02 1.2000e+03 1.1973e+00 --7.2000e+02 1.2286e+03 1.4533e+00 --7.2000e+02 1.2571e+03 1.6965e+00 --7.2000e+02 1.2857e+03 1.9276e+00 --7.2000e+02 1.3143e+03 2.1474e+00 --7.2000e+02 1.3429e+03 2.3567e+00 --7.2000e+02 1.3714e+03 2.5562e+00 --7.2000e+02 1.4000e+03 2.7465e+00 --7.2000e+02 1.4286e+03 2.9281e+00 --7.2000e+02 1.4571e+03 3.1016e+00 --7.2000e+02 1.4857e+03 3.2674e+00 --7.2000e+02 1.5143e+03 3.4261e+00 --7.2000e+02 1.5429e+03 3.5781e+00 --7.2000e+02 1.5714e+03 3.7237e+00 --7.2000e+02 1.6000e+03 3.8633e+00 --7.2000e+02 1.6286e+03 3.9973e+00 --7.2000e+02 1.6571e+03 4.1259e+00 --7.2000e+02 1.6857e+03 4.2496e+00 --7.2000e+02 1.7143e+03 4.3684e+00 --7.2000e+02 1.7429e+03 4.4827e+00 --7.2000e+02 1.7714e+03 4.5928e+00 --7.2000e+02 1.8000e+03 4.6988e+00 --7.2000e+02 1.8286e+03 4.8010e+00 --7.2000e+02 1.8571e+03 4.8995e+00 --7.2000e+02 1.8857e+03 4.9946e+00 --7.2000e+02 1.9143e+03 5.0864e+00 --7.2000e+02 1.9429e+03 5.1750e+00 --7.2000e+02 1.9714e+03 5.2607e+00 --7.2000e+02 2.0000e+03 5.3435e+00 --6.9000e+02 -2.0000e+03 5.3131e+00 --6.9000e+02 -1.9714e+03 5.2288e+00 --6.9000e+02 -1.9429e+03 5.1415e+00 --6.9000e+02 -1.9143e+03 5.0512e+00 --6.9000e+02 -1.8857e+03 4.9576e+00 --6.9000e+02 -1.8571e+03 4.8606e+00 --6.9000e+02 -1.8286e+03 4.7600e+00 --6.9000e+02 -1.8000e+03 4.6556e+00 --6.9000e+02 -1.7714e+03 4.5472e+00 --6.9000e+02 -1.7429e+03 4.4345e+00 --6.9000e+02 -1.7143e+03 4.3175e+00 --6.9000e+02 -1.6857e+03 4.1957e+00 --6.9000e+02 -1.6571e+03 4.0689e+00 --6.9000e+02 -1.6286e+03 3.9368e+00 --6.9000e+02 -1.6000e+03 3.7992e+00 --6.9000e+02 -1.5714e+03 3.6556e+00 --6.9000e+02 -1.5429e+03 3.5057e+00 --6.9000e+02 -1.5143e+03 3.3491e+00 --6.9000e+02 -1.4857e+03 3.1853e+00 --6.9000e+02 -1.4571e+03 3.0140e+00 --6.9000e+02 -1.4286e+03 2.8345e+00 --6.9000e+02 -1.4000e+03 2.6464e+00 --6.9000e+02 -1.3714e+03 2.4491e+00 --6.9000e+02 -1.3429e+03 2.2419e+00 --6.9000e+02 -1.3143e+03 2.0242e+00 --6.9000e+02 -1.2857e+03 1.7951e+00 --6.9000e+02 -1.2571e+03 1.5538e+00 --6.9000e+02 -1.2286e+03 1.2995e+00 --6.9000e+02 -1.2000e+03 1.0312e+00 --6.9000e+02 -1.1714e+03 7.4781e-01 --6.9000e+02 -1.1429e+03 4.4813e-01 --6.9000e+02 -1.1143e+03 1.3092e-01 --6.9000e+02 -1.0857e+03 -2.0522e-01 --6.9000e+02 -1.0571e+03 -5.6181e-01 --6.9000e+02 -1.0286e+03 -9.4053e-01 --6.9000e+02 -1.0000e+03 -1.3432e+00 --6.9000e+02 -9.7143e+02 -1.7718e+00 --6.9000e+02 -9.4286e+02 -2.2285e+00 --6.9000e+02 -9.1429e+02 -2.7157e+00 --6.9000e+02 -8.8571e+02 -3.2360e+00 --6.9000e+02 -8.5714e+02 -3.7921e+00 --6.9000e+02 -8.2857e+02 -4.3871e+00 --6.9000e+02 -8.0000e+02 -5.0243e+00 --6.9000e+02 -7.7143e+02 -5.7071e+00 --6.9000e+02 -7.4286e+02 -6.4392e+00 --6.9000e+02 -7.1429e+02 -7.2246e+00 --6.9000e+02 -6.8571e+02 -8.0672e+00 --6.9000e+02 -6.5714e+02 -8.9712e+00 --6.9000e+02 -6.2857e+02 -9.9405e+00 --6.9000e+02 -6.0000e+02 -1.0979e+01 --6.9000e+02 -5.7143e+02 -1.2090e+01 --6.9000e+02 -5.4286e+02 -1.3276e+01 --6.9000e+02 -5.1429e+02 -1.4540e+01 --6.9000e+02 -4.8571e+02 -1.5880e+01 --6.9000e+02 -4.5714e+02 -1.7296e+01 --6.9000e+02 -4.2857e+02 -1.8784e+01 --6.9000e+02 -4.0000e+02 -2.0336e+01 --6.9000e+02 -3.7143e+02 -2.1942e+01 --6.9000e+02 -3.4286e+02 -2.3586e+01 --6.9000e+02 -3.1429e+02 -2.5251e+01 --6.9000e+02 -2.8571e+02 -2.6914e+01 --6.9000e+02 -2.5714e+02 -2.8548e+01 --6.9000e+02 -2.2857e+02 -3.0123e+01 --6.9000e+02 -2.0000e+02 -3.1610e+01 --6.9000e+02 -1.7143e+02 -3.2976e+01 --6.9000e+02 -1.4286e+02 -3.4191e+01 --6.9000e+02 -1.1429e+02 -3.5227e+01 --6.9000e+02 -8.5714e+01 -3.6060e+01 --6.9000e+02 -5.7143e+01 -3.6670e+01 --6.9000e+02 -2.8571e+01 -3.7041e+01 --6.9000e+02 0.0000e+00 -3.7166e+01 --6.9000e+02 2.8571e+01 -3.7041e+01 --6.9000e+02 5.7143e+01 -3.6670e+01 --6.9000e+02 8.5714e+01 -3.6060e+01 --6.9000e+02 1.1429e+02 -3.5227e+01 --6.9000e+02 1.4286e+02 -3.4191e+01 --6.9000e+02 1.7143e+02 -3.2976e+01 --6.9000e+02 2.0000e+02 -3.1610e+01 --6.9000e+02 2.2857e+02 -3.0123e+01 --6.9000e+02 2.5714e+02 -2.8548e+01 --6.9000e+02 2.8571e+02 -2.6914e+01 --6.9000e+02 3.1429e+02 -2.5251e+01 --6.9000e+02 3.4286e+02 -2.3586e+01 --6.9000e+02 3.7143e+02 -2.1942e+01 --6.9000e+02 4.0000e+02 -2.0336e+01 --6.9000e+02 4.2857e+02 -1.8784e+01 --6.9000e+02 4.5714e+02 -1.7296e+01 --6.9000e+02 4.8571e+02 -1.5880e+01 --6.9000e+02 5.1429e+02 -1.4540e+01 --6.9000e+02 5.4286e+02 -1.3276e+01 --6.9000e+02 5.7143e+02 -1.2090e+01 --6.9000e+02 6.0000e+02 -1.0979e+01 --6.9000e+02 6.2857e+02 -9.9405e+00 --6.9000e+02 6.5714e+02 -8.9712e+00 --6.9000e+02 6.8571e+02 -8.0672e+00 --6.9000e+02 7.1429e+02 -7.2246e+00 --6.9000e+02 7.4286e+02 -6.4392e+00 --6.9000e+02 7.7143e+02 -5.7071e+00 --6.9000e+02 8.0000e+02 -5.0243e+00 --6.9000e+02 8.2857e+02 -4.3871e+00 --6.9000e+02 8.5714e+02 -3.7921e+00 --6.9000e+02 8.8571e+02 -3.2360e+00 --6.9000e+02 9.1429e+02 -2.7157e+00 --6.9000e+02 9.4286e+02 -2.2285e+00 --6.9000e+02 9.7143e+02 -1.7718e+00 --6.9000e+02 1.0000e+03 -1.3432e+00 --6.9000e+02 1.0286e+03 -9.4053e-01 --6.9000e+02 1.0571e+03 -5.6181e-01 --6.9000e+02 1.0857e+03 -2.0522e-01 --6.9000e+02 1.1143e+03 1.3092e-01 --6.9000e+02 1.1429e+03 4.4813e-01 --6.9000e+02 1.1714e+03 7.4781e-01 --6.9000e+02 1.2000e+03 1.0312e+00 --6.9000e+02 1.2286e+03 1.2995e+00 --6.9000e+02 1.2571e+03 1.5538e+00 --6.9000e+02 1.2857e+03 1.7951e+00 --6.9000e+02 1.3143e+03 2.0242e+00 --6.9000e+02 1.3429e+03 2.2419e+00 --6.9000e+02 1.3714e+03 2.4491e+00 --6.9000e+02 1.4000e+03 2.6464e+00 --6.9000e+02 1.4286e+03 2.8345e+00 --6.9000e+02 1.4571e+03 3.0140e+00 --6.9000e+02 1.4857e+03 3.1853e+00 --6.9000e+02 1.5143e+03 3.3491e+00 --6.9000e+02 1.5429e+03 3.5057e+00 --6.9000e+02 1.5714e+03 3.6556e+00 --6.9000e+02 1.6000e+03 3.7992e+00 --6.9000e+02 1.6286e+03 3.9368e+00 --6.9000e+02 1.6571e+03 4.0689e+00 --6.9000e+02 1.6857e+03 4.1957e+00 --6.9000e+02 1.7143e+03 4.3175e+00 --6.9000e+02 1.7429e+03 4.4345e+00 --6.9000e+02 1.7714e+03 4.5472e+00 --6.9000e+02 1.8000e+03 4.6556e+00 --6.9000e+02 1.8286e+03 4.7600e+00 --6.9000e+02 1.8571e+03 4.8606e+00 --6.9000e+02 1.8857e+03 4.9576e+00 --6.9000e+02 1.9143e+03 5.0512e+00 --6.9000e+02 1.9429e+03 5.1415e+00 --6.9000e+02 1.9714e+03 5.2288e+00 --6.9000e+02 2.0000e+03 5.3131e+00 --6.6000e+02 -2.0000e+03 5.2835e+00 --6.6000e+02 -1.9714e+03 5.1977e+00 --6.6000e+02 -1.9429e+03 5.1088e+00 --6.6000e+02 -1.9143e+03 5.0168e+00 --6.6000e+02 -1.8857e+03 4.9214e+00 --6.6000e+02 -1.8571e+03 4.8225e+00 --6.6000e+02 -1.8286e+03 4.7198e+00 --6.6000e+02 -1.8000e+03 4.6132e+00 --6.6000e+02 -1.7714e+03 4.5025e+00 --6.6000e+02 -1.7429e+03 4.3873e+00 --6.6000e+02 -1.7143e+03 4.2675e+00 --6.6000e+02 -1.6857e+03 4.1428e+00 --6.6000e+02 -1.6571e+03 4.0129e+00 --6.6000e+02 -1.6286e+03 3.8774e+00 --6.6000e+02 -1.6000e+03 3.7361e+00 --6.6000e+02 -1.5714e+03 3.5885e+00 --6.6000e+02 -1.5429e+03 3.4343e+00 --6.6000e+02 -1.5143e+03 3.2730e+00 --6.6000e+02 -1.4857e+03 3.1042e+00 --6.6000e+02 -1.4571e+03 2.9273e+00 --6.6000e+02 -1.4286e+03 2.7419e+00 --6.6000e+02 -1.4000e+03 2.5472e+00 --6.6000e+02 -1.3714e+03 2.3427e+00 --6.6000e+02 -1.3429e+03 2.1277e+00 --6.6000e+02 -1.3143e+03 1.9013e+00 --6.6000e+02 -1.2857e+03 1.6628e+00 --6.6000e+02 -1.2571e+03 1.4112e+00 --6.6000e+02 -1.2286e+03 1.1455e+00 --6.6000e+02 -1.2000e+03 8.6453e-01 --6.6000e+02 -1.1714e+03 5.6715e-01 --6.6000e+02 -1.1429e+03 2.5201e-01 --6.6000e+02 -1.1143e+03 -8.2383e-02 --6.6000e+02 -1.0857e+03 -4.3764e-01 --6.6000e+02 -1.0571e+03 -8.1554e-01 --6.6000e+02 -1.0286e+03 -1.2181e+00 --6.6000e+02 -1.0000e+03 -1.6473e+00 --6.6000e+02 -9.7143e+02 -2.1058e+00 --6.6000e+02 -9.4286e+02 -2.5961e+00 --6.6000e+02 -9.1429e+02 -3.1211e+00 --6.6000e+02 -8.8571e+02 -3.6840e+00 --6.6000e+02 -8.5714e+02 -4.2883e+00 --6.6000e+02 -8.2857e+02 -4.9379e+00 --6.6000e+02 -8.0000e+02 -5.6369e+00 --6.6000e+02 -7.7143e+02 -6.3900e+00 --6.6000e+02 -7.4286e+02 -7.2021e+00 --6.6000e+02 -7.1429e+02 -8.0784e+00 --6.6000e+02 -6.8571e+02 -9.0245e+00 --6.6000e+02 -6.5714e+02 -1.0046e+01 --6.6000e+02 -6.2857e+02 -1.1150e+01 --6.6000e+02 -6.0000e+02 -1.2340e+01 --6.6000e+02 -5.7143e+02 -1.3624e+01 --6.6000e+02 -5.4286e+02 -1.5005e+01 --6.6000e+02 -5.1429e+02 -1.6488e+01 --6.6000e+02 -4.8571e+02 -1.8073e+01 --6.6000e+02 -4.5714e+02 -1.9761e+01 --6.6000e+02 -4.2857e+02 -2.1547e+01 --6.6000e+02 -4.0000e+02 -2.3422e+01 --6.6000e+02 -3.7143e+02 -2.5374e+01 --6.6000e+02 -3.4286e+02 -2.7383e+01 --6.6000e+02 -3.1429e+02 -2.9425e+01 --6.6000e+02 -2.8571e+02 -3.1468e+01 --6.6000e+02 -2.5714e+02 -3.3479e+01 --6.6000e+02 -2.2857e+02 -3.5417e+01 --6.6000e+02 -2.0000e+02 -3.7243e+01 --6.6000e+02 -1.7143e+02 -3.8917e+01 --6.6000e+02 -1.4286e+02 -4.0401e+01 --6.6000e+02 -1.1429e+02 -4.1663e+01 --6.6000e+02 -8.5714e+01 -4.2674e+01 --6.6000e+02 -5.7143e+01 -4.3412e+01 --6.6000e+02 -2.8571e+01 -4.3861e+01 --6.6000e+02 0.0000e+00 -4.4012e+01 --6.6000e+02 2.8571e+01 -4.3861e+01 --6.6000e+02 5.7143e+01 -4.3412e+01 --6.6000e+02 8.5714e+01 -4.2674e+01 --6.6000e+02 1.1429e+02 -4.1663e+01 --6.6000e+02 1.4286e+02 -4.0401e+01 --6.6000e+02 1.7143e+02 -3.8917e+01 --6.6000e+02 2.0000e+02 -3.7243e+01 --6.6000e+02 2.2857e+02 -3.5417e+01 --6.6000e+02 2.5714e+02 -3.3479e+01 --6.6000e+02 2.8571e+02 -3.1468e+01 --6.6000e+02 3.1429e+02 -2.9425e+01 --6.6000e+02 3.4286e+02 -2.7383e+01 --6.6000e+02 3.7143e+02 -2.5374e+01 --6.6000e+02 4.0000e+02 -2.3422e+01 --6.6000e+02 4.2857e+02 -2.1547e+01 --6.6000e+02 4.5714e+02 -1.9761e+01 --6.6000e+02 4.8571e+02 -1.8073e+01 --6.6000e+02 5.1429e+02 -1.6488e+01 --6.6000e+02 5.4286e+02 -1.5005e+01 --6.6000e+02 5.7143e+02 -1.3624e+01 --6.6000e+02 6.0000e+02 -1.2340e+01 --6.6000e+02 6.2857e+02 -1.1150e+01 --6.6000e+02 6.5714e+02 -1.0046e+01 --6.6000e+02 6.8571e+02 -9.0245e+00 --6.6000e+02 7.1429e+02 -8.0784e+00 --6.6000e+02 7.4286e+02 -7.2021e+00 --6.6000e+02 7.7143e+02 -6.3900e+00 --6.6000e+02 8.0000e+02 -5.6369e+00 --6.6000e+02 8.2857e+02 -4.9379e+00 --6.6000e+02 8.5714e+02 -4.2883e+00 --6.6000e+02 8.8571e+02 -3.6840e+00 --6.6000e+02 9.1429e+02 -3.1211e+00 --6.6000e+02 9.4286e+02 -2.5961e+00 --6.6000e+02 9.7143e+02 -2.1058e+00 --6.6000e+02 1.0000e+03 -1.6473e+00 --6.6000e+02 1.0286e+03 -1.2181e+00 --6.6000e+02 1.0571e+03 -8.1554e-01 --6.6000e+02 1.0857e+03 -4.3764e-01 --6.6000e+02 1.1143e+03 -8.2383e-02 --6.6000e+02 1.1429e+03 2.5201e-01 --6.6000e+02 1.1714e+03 5.6715e-01 --6.6000e+02 1.2000e+03 8.6453e-01 --6.6000e+02 1.2286e+03 1.1455e+00 --6.6000e+02 1.2571e+03 1.4112e+00 --6.6000e+02 1.2857e+03 1.6628e+00 --6.6000e+02 1.3143e+03 1.9013e+00 --6.6000e+02 1.3429e+03 2.1277e+00 --6.6000e+02 1.3714e+03 2.3427e+00 --6.6000e+02 1.4000e+03 2.5472e+00 --6.6000e+02 1.4286e+03 2.7419e+00 --6.6000e+02 1.4571e+03 2.9273e+00 --6.6000e+02 1.4857e+03 3.1042e+00 --6.6000e+02 1.5143e+03 3.2730e+00 --6.6000e+02 1.5429e+03 3.4343e+00 --6.6000e+02 1.5714e+03 3.5885e+00 --6.6000e+02 1.6000e+03 3.7361e+00 --6.6000e+02 1.6286e+03 3.8774e+00 --6.6000e+02 1.6571e+03 4.0129e+00 --6.6000e+02 1.6857e+03 4.1428e+00 --6.6000e+02 1.7143e+03 4.2675e+00 --6.6000e+02 1.7429e+03 4.3873e+00 --6.6000e+02 1.7714e+03 4.5025e+00 --6.6000e+02 1.8000e+03 4.6132e+00 --6.6000e+02 1.8286e+03 4.7198e+00 --6.6000e+02 1.8571e+03 4.8225e+00 --6.6000e+02 1.8857e+03 4.9214e+00 --6.6000e+02 1.9143e+03 5.0168e+00 --6.6000e+02 1.9429e+03 5.1088e+00 --6.6000e+02 1.9714e+03 5.1977e+00 --6.6000e+02 2.0000e+03 5.2835e+00 --6.3000e+02 -2.0000e+03 5.2547e+00 --6.3000e+02 -1.9714e+03 5.1674e+00 --6.3000e+02 -1.9429e+03 5.0770e+00 --6.3000e+02 -1.9143e+03 4.9834e+00 --6.3000e+02 -1.8857e+03 4.8862e+00 --6.3000e+02 -1.8571e+03 4.7854e+00 --6.3000e+02 -1.8286e+03 4.6807e+00 --6.3000e+02 -1.8000e+03 4.5719e+00 --6.3000e+02 -1.7714e+03 4.4588e+00 --6.3000e+02 -1.7429e+03 4.3412e+00 --6.3000e+02 -1.7143e+03 4.2187e+00 --6.3000e+02 -1.6857e+03 4.0911e+00 --6.3000e+02 -1.6571e+03 3.9580e+00 --6.3000e+02 -1.6286e+03 3.8192e+00 --6.3000e+02 -1.6000e+03 3.6742e+00 --6.3000e+02 -1.5714e+03 3.5226e+00 --6.3000e+02 -1.5429e+03 3.3641e+00 --6.3000e+02 -1.5143e+03 3.1981e+00 --6.3000e+02 -1.4857e+03 3.0242e+00 --6.3000e+02 -1.4571e+03 2.8418e+00 --6.3000e+02 -1.4286e+03 2.6503e+00 --6.3000e+02 -1.4000e+03 2.4491e+00 --6.3000e+02 -1.3714e+03 2.2374e+00 --6.3000e+02 -1.3429e+03 2.0144e+00 --6.3000e+02 -1.3143e+03 1.7793e+00 --6.3000e+02 -1.2857e+03 1.5312e+00 --6.3000e+02 -1.2571e+03 1.2690e+00 --6.3000e+02 -1.2286e+03 9.9155e-01 --6.3000e+02 -1.2000e+03 6.9765e-01 --6.3000e+02 -1.1714e+03 3.8590e-01 --6.3000e+02 -1.1429e+03 5.4774e-02 --6.3000e+02 -1.1143e+03 -2.9743e-01 --6.3000e+02 -1.0857e+03 -6.7257e-01 --6.3000e+02 -1.0571e+03 -1.0727e+00 --6.3000e+02 -1.0286e+03 -1.5002e+00 --6.3000e+02 -1.0000e+03 -1.9576e+00 --6.3000e+02 -9.7143e+02 -2.4477e+00 --6.3000e+02 -9.4286e+02 -2.9738e+00 --6.3000e+02 -9.1429e+02 -3.5392e+00 --6.3000e+02 -8.8571e+02 -4.1481e+00 --6.3000e+02 -8.5714e+02 -4.8047e+00 --6.3000e+02 -8.2857e+02 -5.5139e+00 --6.3000e+02 -8.0000e+02 -6.2810e+00 --6.3000e+02 -7.7143e+02 -7.1119e+00 --6.3000e+02 -7.4286e+02 -8.0132e+00 --6.3000e+02 -7.1429e+02 -8.9919e+00 --6.3000e+02 -6.8571e+02 -1.0056e+01 --6.3000e+02 -6.5714e+02 -1.1213e+01 --6.3000e+02 -6.2857e+02 -1.2471e+01 --6.3000e+02 -6.0000e+02 -1.3839e+01 --6.3000e+02 -5.7143e+02 -1.5326e+01 --6.3000e+02 -5.4286e+02 -1.6939e+01 --6.3000e+02 -5.1429e+02 -1.8684e+01 --6.3000e+02 -4.8571e+02 -2.0565e+01 --6.3000e+02 -4.5714e+02 -2.2581e+01 --6.3000e+02 -4.2857e+02 -2.4730e+01 --6.3000e+02 -4.0000e+02 -2.6998e+01 --6.3000e+02 -3.7143e+02 -2.9370e+01 --6.3000e+02 -3.4286e+02 -3.1817e+01 --6.3000e+02 -3.1429e+02 -3.4306e+01 --6.3000e+02 -2.8571e+02 -3.6795e+01 --6.3000e+02 -2.5714e+02 -3.9236e+01 --6.3000e+02 -2.2857e+02 -4.1579e+01 --6.3000e+02 -2.0000e+02 -4.3772e+01 --6.3000e+02 -1.7143e+02 -4.5769e+01 --6.3000e+02 -1.4286e+02 -4.7526e+01 --6.3000e+02 -1.1429e+02 -4.9009e+01 --6.3000e+02 -8.5714e+01 -5.0189e+01 --6.3000e+02 -5.7143e+01 -5.1047e+01 --6.3000e+02 -2.8571e+01 -5.1566e+01 --6.3000e+02 0.0000e+00 -5.1740e+01 --6.3000e+02 2.8571e+01 -5.1566e+01 --6.3000e+02 5.7143e+01 -5.1047e+01 --6.3000e+02 8.5714e+01 -5.0189e+01 --6.3000e+02 1.1429e+02 -4.9009e+01 --6.3000e+02 1.4286e+02 -4.7526e+01 --6.3000e+02 1.7143e+02 -4.5769e+01 --6.3000e+02 2.0000e+02 -4.3772e+01 --6.3000e+02 2.2857e+02 -4.1579e+01 --6.3000e+02 2.5714e+02 -3.9236e+01 --6.3000e+02 2.8571e+02 -3.6795e+01 --6.3000e+02 3.1429e+02 -3.4306e+01 --6.3000e+02 3.4286e+02 -3.1817e+01 --6.3000e+02 3.7143e+02 -2.9370e+01 --6.3000e+02 4.0000e+02 -2.6998e+01 --6.3000e+02 4.2857e+02 -2.4730e+01 --6.3000e+02 4.5714e+02 -2.2581e+01 --6.3000e+02 4.8571e+02 -2.0565e+01 --6.3000e+02 5.1429e+02 -1.8684e+01 --6.3000e+02 5.4286e+02 -1.6939e+01 --6.3000e+02 5.7143e+02 -1.5326e+01 --6.3000e+02 6.0000e+02 -1.3839e+01 --6.3000e+02 6.2857e+02 -1.2471e+01 --6.3000e+02 6.5714e+02 -1.1213e+01 --6.3000e+02 6.8571e+02 -1.0056e+01 --6.3000e+02 7.1429e+02 -8.9919e+00 --6.3000e+02 7.4286e+02 -8.0132e+00 --6.3000e+02 7.7143e+02 -7.1119e+00 --6.3000e+02 8.0000e+02 -6.2810e+00 --6.3000e+02 8.2857e+02 -5.5139e+00 --6.3000e+02 8.5714e+02 -4.8047e+00 --6.3000e+02 8.8571e+02 -4.1481e+00 --6.3000e+02 9.1429e+02 -3.5392e+00 --6.3000e+02 9.4286e+02 -2.9738e+00 --6.3000e+02 9.7143e+02 -2.4477e+00 --6.3000e+02 1.0000e+03 -1.9576e+00 --6.3000e+02 1.0286e+03 -1.5002e+00 --6.3000e+02 1.0571e+03 -1.0727e+00 --6.3000e+02 1.0857e+03 -6.7257e-01 --6.3000e+02 1.1143e+03 -2.9743e-01 --6.3000e+02 1.1429e+03 5.4774e-02 --6.3000e+02 1.1714e+03 3.8590e-01 --6.3000e+02 1.2000e+03 6.9765e-01 --6.3000e+02 1.2286e+03 9.9155e-01 --6.3000e+02 1.2571e+03 1.2690e+00 --6.3000e+02 1.2857e+03 1.5312e+00 --6.3000e+02 1.3143e+03 1.7793e+00 --6.3000e+02 1.3429e+03 2.0144e+00 --6.3000e+02 1.3714e+03 2.2374e+00 --6.3000e+02 1.4000e+03 2.4491e+00 --6.3000e+02 1.4286e+03 2.6503e+00 --6.3000e+02 1.4571e+03 2.8418e+00 --6.3000e+02 1.4857e+03 3.0242e+00 --6.3000e+02 1.5143e+03 3.1981e+00 --6.3000e+02 1.5429e+03 3.3641e+00 --6.3000e+02 1.5714e+03 3.5226e+00 --6.3000e+02 1.6000e+03 3.6742e+00 --6.3000e+02 1.6286e+03 3.8192e+00 --6.3000e+02 1.6571e+03 3.9580e+00 --6.3000e+02 1.6857e+03 4.0911e+00 --6.3000e+02 1.7143e+03 4.2187e+00 --6.3000e+02 1.7429e+03 4.3412e+00 --6.3000e+02 1.7714e+03 4.4588e+00 --6.3000e+02 1.8000e+03 4.5719e+00 --6.3000e+02 1.8286e+03 4.6807e+00 --6.3000e+02 1.8571e+03 4.7854e+00 --6.3000e+02 1.8857e+03 4.8862e+00 --6.3000e+02 1.9143e+03 4.9834e+00 --6.3000e+02 1.9429e+03 5.0770e+00 --6.3000e+02 1.9714e+03 5.1674e+00 --6.3000e+02 2.0000e+03 5.2547e+00 --6.0000e+02 -2.0000e+03 5.2268e+00 --6.0000e+02 -1.9714e+03 5.1381e+00 --6.0000e+02 -1.9429e+03 5.0462e+00 --6.0000e+02 -1.9143e+03 4.9509e+00 --6.0000e+02 -1.8857e+03 4.8520e+00 --6.0000e+02 -1.8571e+03 4.7493e+00 --6.0000e+02 -1.8286e+03 4.6427e+00 --6.0000e+02 -1.8000e+03 4.5318e+00 --6.0000e+02 -1.7714e+03 4.4164e+00 --6.0000e+02 -1.7429e+03 4.2962e+00 --6.0000e+02 -1.7143e+03 4.1711e+00 --6.0000e+02 -1.6857e+03 4.0406e+00 --6.0000e+02 -1.6571e+03 3.9044e+00 --6.0000e+02 -1.6286e+03 3.7622e+00 --6.0000e+02 -1.6000e+03 3.6136e+00 --6.0000e+02 -1.5714e+03 3.4581e+00 --6.0000e+02 -1.5429e+03 3.2953e+00 --6.0000e+02 -1.5143e+03 3.1247e+00 --6.0000e+02 -1.4857e+03 2.9457e+00 --6.0000e+02 -1.4571e+03 2.7578e+00 --6.0000e+02 -1.4286e+03 2.5602e+00 --6.0000e+02 -1.4000e+03 2.3523e+00 --6.0000e+02 -1.3714e+03 2.1333e+00 --6.0000e+02 -1.3429e+03 1.9023e+00 --6.0000e+02 -1.3143e+03 1.6584e+00 --6.0000e+02 -1.2857e+03 1.4006e+00 --6.0000e+02 -1.2571e+03 1.1276e+00 --6.0000e+02 -1.2286e+03 8.3825e-01 --6.0000e+02 -1.2000e+03 5.3111e-01 --6.0000e+02 -1.1714e+03 2.0462e-01 --6.0000e+02 -1.1429e+03 -1.4296e-01 --6.0000e+02 -1.1143e+03 -5.1355e-01 --6.0000e+02 -1.0857e+03 -9.0932e-01 --6.0000e+02 -1.0571e+03 -1.3327e+00 --6.0000e+02 -1.0286e+03 -1.7863e+00 --6.0000e+02 -1.0000e+03 -2.2732e+00 --6.0000e+02 -9.7143e+02 -2.7967e+00 --6.0000e+02 -9.4286e+02 -3.3607e+00 --6.0000e+02 -9.1429e+02 -3.9694e+00 --6.0000e+02 -8.8571e+02 -4.6277e+00 --6.0000e+02 -8.5714e+02 -5.3408e+00 --6.0000e+02 -8.2857e+02 -6.1149e+00 --6.0000e+02 -8.0000e+02 -6.9567e+00 --6.0000e+02 -7.7143e+02 -7.8737e+00 --6.0000e+02 -7.4286e+02 -8.8745e+00 --6.0000e+02 -7.1429e+02 -9.9684e+00 --6.0000e+02 -6.8571e+02 -1.1166e+01 --6.0000e+02 -6.5714e+02 -1.2477e+01 --6.0000e+02 -6.2857e+02 -1.3914e+01 --6.0000e+02 -6.0000e+02 -1.5490e+01 --6.0000e+02 -5.7143e+02 -1.7216e+01 --6.0000e+02 -5.4286e+02 -1.9103e+01 --6.0000e+02 -5.1429e+02 -2.1161e+01 --6.0000e+02 -4.8571e+02 -2.3395e+01 --6.0000e+02 -4.5714e+02 -2.5807e+01 --6.0000e+02 -4.2857e+02 -2.8389e+01 --6.0000e+02 -4.0000e+02 -3.1126e+01 --6.0000e+02 -3.7143e+02 -3.3991e+01 --6.0000e+02 -3.4286e+02 -3.6945e+01 --6.0000e+02 -3.1429e+02 -3.9937e+01 --6.0000e+02 -2.8571e+02 -4.2910e+01 --6.0000e+02 -2.5714e+02 -4.5800e+01 --6.0000e+02 -2.2857e+02 -4.8544e+01 --6.0000e+02 -2.0000e+02 -5.1081e+01 --6.0000e+02 -1.7143e+02 -5.3362e+01 --6.0000e+02 -1.4286e+02 -5.5346e+01 --6.0000e+02 -1.1429e+02 -5.7000e+01 --6.0000e+02 -8.5714e+01 -5.8304e+01 --6.0000e+02 -5.7143e+01 -5.9244e+01 --6.0000e+02 -2.8571e+01 -5.9811e+01 --6.0000e+02 0.0000e+00 -6.0000e+01 --6.0000e+02 2.8571e+01 -5.9811e+01 --6.0000e+02 5.7143e+01 -5.9244e+01 --6.0000e+02 8.5714e+01 -5.8304e+01 --6.0000e+02 1.1429e+02 -5.7000e+01 --6.0000e+02 1.4286e+02 -5.5346e+01 --6.0000e+02 1.7143e+02 -5.3362e+01 --6.0000e+02 2.0000e+02 -5.1081e+01 --6.0000e+02 2.2857e+02 -4.8544e+01 --6.0000e+02 2.5714e+02 -4.5800e+01 --6.0000e+02 2.8571e+02 -4.2910e+01 --6.0000e+02 3.1429e+02 -3.9937e+01 --6.0000e+02 3.4286e+02 -3.6945e+01 --6.0000e+02 3.7143e+02 -3.3991e+01 --6.0000e+02 4.0000e+02 -3.1126e+01 --6.0000e+02 4.2857e+02 -2.8389e+01 --6.0000e+02 4.5714e+02 -2.5807e+01 --6.0000e+02 4.8571e+02 -2.3395e+01 --6.0000e+02 5.1429e+02 -2.1161e+01 --6.0000e+02 5.4286e+02 -1.9103e+01 --6.0000e+02 5.7143e+02 -1.7216e+01 --6.0000e+02 6.0000e+02 -1.5490e+01 --6.0000e+02 6.2857e+02 -1.3914e+01 --6.0000e+02 6.5714e+02 -1.2477e+01 --6.0000e+02 6.8571e+02 -1.1166e+01 --6.0000e+02 7.1429e+02 -9.9684e+00 --6.0000e+02 7.4286e+02 -8.8745e+00 --6.0000e+02 7.7143e+02 -7.8737e+00 --6.0000e+02 8.0000e+02 -6.9567e+00 --6.0000e+02 8.2857e+02 -6.1149e+00 --6.0000e+02 8.5714e+02 -5.3408e+00 --6.0000e+02 8.8571e+02 -4.6277e+00 --6.0000e+02 9.1429e+02 -3.9694e+00 --6.0000e+02 9.4286e+02 -3.3607e+00 --6.0000e+02 9.7143e+02 -2.7967e+00 --6.0000e+02 1.0000e+03 -2.2732e+00 --6.0000e+02 1.0286e+03 -1.7863e+00 --6.0000e+02 1.0571e+03 -1.3327e+00 --6.0000e+02 1.0857e+03 -9.0932e-01 --6.0000e+02 1.1143e+03 -5.1355e-01 --6.0000e+02 1.1429e+03 -1.4296e-01 --6.0000e+02 1.1714e+03 2.0462e-01 --6.0000e+02 1.2000e+03 5.3111e-01 --6.0000e+02 1.2286e+03 8.3825e-01 --6.0000e+02 1.2571e+03 1.1276e+00 --6.0000e+02 1.2857e+03 1.4006e+00 --6.0000e+02 1.3143e+03 1.6584e+00 --6.0000e+02 1.3429e+03 1.9023e+00 --6.0000e+02 1.3714e+03 2.1333e+00 --6.0000e+02 1.4000e+03 2.3523e+00 --6.0000e+02 1.4286e+03 2.5602e+00 --6.0000e+02 1.4571e+03 2.7578e+00 --6.0000e+02 1.4857e+03 2.9457e+00 --6.0000e+02 1.5143e+03 3.1247e+00 --6.0000e+02 1.5429e+03 3.2953e+00 --6.0000e+02 1.5714e+03 3.4581e+00 --6.0000e+02 1.6000e+03 3.6136e+00 --6.0000e+02 1.6286e+03 3.7622e+00 --6.0000e+02 1.6571e+03 3.9044e+00 --6.0000e+02 1.6857e+03 4.0406e+00 --6.0000e+02 1.7143e+03 4.1711e+00 --6.0000e+02 1.7429e+03 4.2962e+00 --6.0000e+02 1.7714e+03 4.4164e+00 --6.0000e+02 1.8000e+03 4.5318e+00 --6.0000e+02 1.8286e+03 4.6427e+00 --6.0000e+02 1.8571e+03 4.7493e+00 --6.0000e+02 1.8857e+03 4.8520e+00 --6.0000e+02 1.9143e+03 4.9509e+00 --6.0000e+02 1.9429e+03 5.0462e+00 --6.0000e+02 1.9714e+03 5.1381e+00 --6.0000e+02 2.0000e+03 5.2268e+00 --5.7000e+02 -2.0000e+03 5.1998e+00 --5.7000e+02 -1.9714e+03 5.1098e+00 --5.7000e+02 -1.9429e+03 5.0164e+00 --5.7000e+02 -1.9143e+03 4.9195e+00 --5.7000e+02 -1.8857e+03 4.8189e+00 --5.7000e+02 -1.8571e+03 4.7144e+00 --5.7000e+02 -1.8286e+03 4.6058e+00 --5.7000e+02 -1.8000e+03 4.4928e+00 --5.7000e+02 -1.7714e+03 4.3752e+00 --5.7000e+02 -1.7429e+03 4.2526e+00 --5.7000e+02 -1.7143e+03 4.1248e+00 --5.7000e+02 -1.6857e+03 3.9915e+00 --5.7000e+02 -1.6571e+03 3.8523e+00 --5.7000e+02 -1.6286e+03 3.7067e+00 --5.7000e+02 -1.6000e+03 3.5545e+00 --5.7000e+02 -1.5714e+03 3.3951e+00 --5.7000e+02 -1.5429e+03 3.2281e+00 --5.7000e+02 -1.5143e+03 3.0528e+00 --5.7000e+02 -1.4857e+03 2.8688e+00 --5.7000e+02 -1.4571e+03 2.6753e+00 --5.7000e+02 -1.4286e+03 2.4717e+00 --5.7000e+02 -1.4000e+03 2.2572e+00 --5.7000e+02 -1.3714e+03 2.0309e+00 --5.7000e+02 -1.3429e+03 1.7919e+00 --5.7000e+02 -1.3143e+03 1.5391e+00 --5.7000e+02 -1.2857e+03 1.2714e+00 --5.7000e+02 -1.2571e+03 9.8756e-01 --5.7000e+02 -1.2286e+03 6.8611e-01 --5.7000e+02 -1.2000e+03 3.6548e-01 --5.7000e+02 -1.1714e+03 2.3931e-02 --5.7000e+02 -1.1429e+03 -3.4051e-01 --5.7000e+02 -1.1143e+03 -7.3003e-01 --5.7000e+02 -1.0857e+03 -1.1471e+00 --5.7000e+02 -1.0571e+03 -1.5945e+00 --5.7000e+02 -1.0286e+03 -2.0753e+00 --5.7000e+02 -1.0000e+03 -2.5931e+00 --5.7000e+02 -9.7143e+02 -3.1518e+00 --5.7000e+02 -9.4286e+02 -3.7560e+00 --5.7000e+02 -9.1429e+02 -4.4107e+00 --5.7000e+02 -8.8571e+02 -5.1218e+00 --5.7000e+02 -8.5714e+02 -5.8959e+00 --5.7000e+02 -8.2857e+02 -6.7404e+00 --5.7000e+02 -8.0000e+02 -7.6638e+00 --5.7000e+02 -7.7143e+02 -8.6758e+00 --5.7000e+02 -7.4286e+02 -9.7872e+00 --5.7000e+02 -7.1429e+02 -1.1010e+01 --5.7000e+02 -6.8571e+02 -1.2358e+01 --5.7000e+02 -6.5714e+02 -1.3846e+01 --5.7000e+02 -6.2857e+02 -1.5490e+01 --5.7000e+02 -6.0000e+02 -1.7306e+01 --5.7000e+02 -5.7143e+02 -1.9312e+01 --5.7000e+02 -5.4286e+02 -2.1523e+01 --5.7000e+02 -5.1429e+02 -2.3951e+01 --5.7000e+02 -4.8571e+02 -2.6604e+01 --5.7000e+02 -4.5714e+02 -2.9481e+01 --5.7000e+02 -4.2857e+02 -3.2571e+01 --5.7000e+02 -4.0000e+02 -3.5846e+01 --5.7000e+02 -3.7143e+02 -3.9263e+01 --5.7000e+02 -3.4286e+02 -4.2763e+01 --5.7000e+02 -3.1429e+02 -4.6274e+01 --5.7000e+02 -2.8571e+02 -4.9714e+01 --5.7000e+02 -2.5714e+02 -5.3006e+01 --5.7000e+02 -2.2857e+02 -5.6077e+01 --5.7000e+02 -2.0000e+02 -5.8867e+01 --5.7000e+02 -1.7143e+02 -6.1331e+01 --5.7000e+02 -1.4286e+02 -6.3439e+01 --5.7000e+02 -1.1429e+02 -6.5173e+01 --5.7000e+02 -8.5714e+01 -6.6524e+01 --5.7000e+02 -5.7143e+01 -6.7489e+01 --5.7000e+02 -2.8571e+01 -6.8067e+01 --5.7000e+02 0.0000e+00 -6.8260e+01 --5.7000e+02 2.8571e+01 -6.8067e+01 --5.7000e+02 5.7143e+01 -6.7489e+01 --5.7000e+02 8.5714e+01 -6.6524e+01 --5.7000e+02 1.1429e+02 -6.5173e+01 --5.7000e+02 1.4286e+02 -6.3439e+01 --5.7000e+02 1.7143e+02 -6.1331e+01 --5.7000e+02 2.0000e+02 -5.8867e+01 --5.7000e+02 2.2857e+02 -5.6077e+01 --5.7000e+02 2.5714e+02 -5.3006e+01 --5.7000e+02 2.8571e+02 -4.9714e+01 --5.7000e+02 3.1429e+02 -4.6274e+01 --5.7000e+02 3.4286e+02 -4.2763e+01 --5.7000e+02 3.7143e+02 -3.9263e+01 --5.7000e+02 4.0000e+02 -3.5846e+01 --5.7000e+02 4.2857e+02 -3.2571e+01 --5.7000e+02 4.5714e+02 -2.9481e+01 --5.7000e+02 4.8571e+02 -2.6604e+01 --5.7000e+02 5.1429e+02 -2.3951e+01 --5.7000e+02 5.4286e+02 -2.1523e+01 --5.7000e+02 5.7143e+02 -1.9312e+01 --5.7000e+02 6.0000e+02 -1.7306e+01 --5.7000e+02 6.2857e+02 -1.5490e+01 --5.7000e+02 6.5714e+02 -1.3846e+01 --5.7000e+02 6.8571e+02 -1.2358e+01 --5.7000e+02 7.1429e+02 -1.1010e+01 --5.7000e+02 7.4286e+02 -9.7872e+00 --5.7000e+02 7.7143e+02 -8.6758e+00 --5.7000e+02 8.0000e+02 -7.6638e+00 --5.7000e+02 8.2857e+02 -6.7404e+00 --5.7000e+02 8.5714e+02 -5.8959e+00 --5.7000e+02 8.8571e+02 -5.1218e+00 --5.7000e+02 9.1429e+02 -4.4107e+00 --5.7000e+02 9.4286e+02 -3.7560e+00 --5.7000e+02 9.7143e+02 -3.1518e+00 --5.7000e+02 1.0000e+03 -2.5931e+00 --5.7000e+02 1.0286e+03 -2.0753e+00 --5.7000e+02 1.0571e+03 -1.5945e+00 --5.7000e+02 1.0857e+03 -1.1471e+00 --5.7000e+02 1.1143e+03 -7.3003e-01 --5.7000e+02 1.1429e+03 -3.4051e-01 --5.7000e+02 1.1714e+03 2.3931e-02 --5.7000e+02 1.2000e+03 3.6548e-01 --5.7000e+02 1.2286e+03 6.8611e-01 --5.7000e+02 1.2571e+03 9.8756e-01 --5.7000e+02 1.2857e+03 1.2714e+00 --5.7000e+02 1.3143e+03 1.5391e+00 --5.7000e+02 1.3429e+03 1.7919e+00 --5.7000e+02 1.3714e+03 2.0309e+00 --5.7000e+02 1.4000e+03 2.2572e+00 --5.7000e+02 1.4286e+03 2.4717e+00 --5.7000e+02 1.4571e+03 2.6753e+00 --5.7000e+02 1.4857e+03 2.8688e+00 --5.7000e+02 1.5143e+03 3.0528e+00 --5.7000e+02 1.5429e+03 3.2281e+00 --5.7000e+02 1.5714e+03 3.3951e+00 --5.7000e+02 1.6000e+03 3.5545e+00 --5.7000e+02 1.6286e+03 3.7067e+00 --5.7000e+02 1.6571e+03 3.8523e+00 --5.7000e+02 1.6857e+03 3.9915e+00 --5.7000e+02 1.7143e+03 4.1248e+00 --5.7000e+02 1.7429e+03 4.2526e+00 --5.7000e+02 1.7714e+03 4.3752e+00 --5.7000e+02 1.8000e+03 4.4928e+00 --5.7000e+02 1.8286e+03 4.6058e+00 --5.7000e+02 1.8571e+03 4.7144e+00 --5.7000e+02 1.8857e+03 4.8189e+00 --5.7000e+02 1.9143e+03 4.9195e+00 --5.7000e+02 1.9429e+03 5.0164e+00 --5.7000e+02 1.9714e+03 5.1098e+00 --5.7000e+02 2.0000e+03 5.1998e+00 --5.4000e+02 -2.0000e+03 5.1739e+00 --5.4000e+02 -1.9714e+03 5.0824e+00 --5.4000e+02 -1.9429e+03 4.9876e+00 --5.4000e+02 -1.9143e+03 4.8892e+00 --5.4000e+02 -1.8857e+03 4.7870e+00 --5.4000e+02 -1.8571e+03 4.6807e+00 --5.4000e+02 -1.8286e+03 4.5702e+00 --5.4000e+02 -1.8000e+03 4.4551e+00 --5.4000e+02 -1.7714e+03 4.3353e+00 --5.4000e+02 -1.7429e+03 4.2104e+00 --5.4000e+02 -1.7143e+03 4.0800e+00 --5.4000e+02 -1.6857e+03 3.9439e+00 --5.4000e+02 -1.6571e+03 3.8017e+00 --5.4000e+02 -1.6286e+03 3.6529e+00 --5.4000e+02 -1.6000e+03 3.4971e+00 --5.4000e+02 -1.5714e+03 3.3339e+00 --5.4000e+02 -1.5429e+03 3.1627e+00 --5.4000e+02 -1.5143e+03 2.9828e+00 --5.4000e+02 -1.4857e+03 2.7938e+00 --5.4000e+02 -1.4571e+03 2.5948e+00 --5.4000e+02 -1.4286e+03 2.3852e+00 --5.4000e+02 -1.4000e+03 2.1641e+00 --5.4000e+02 -1.3714e+03 1.9305e+00 --5.4000e+02 -1.3429e+03 1.6834e+00 --5.4000e+02 -1.3143e+03 1.4218e+00 --5.4000e+02 -1.2857e+03 1.1442e+00 --5.4000e+02 -1.2571e+03 8.4936e-01 --5.4000e+02 -1.2286e+03 5.3567e-01 --5.4000e+02 -1.2000e+03 2.0138e-01 --5.4000e+02 -1.1714e+03 -1.5549e-01 --5.4000e+02 -1.1429e+03 -5.3713e-01 --5.4000e+02 -1.1143e+03 -9.4603e-01 --5.4000e+02 -1.0857e+03 -1.3850e+00 --5.4000e+02 -1.0571e+03 -1.8572e+00 --5.4000e+02 -1.0286e+03 -2.3662e+00 --5.4000e+02 -1.0000e+03 -2.9162e+00 --5.4000e+02 -9.7143e+02 -3.5117e+00 --5.4000e+02 -9.4286e+02 -4.1582e+00 --5.4000e+02 -9.1429e+02 -4.8617e+00 --5.4000e+02 -8.8571e+02 -5.6292e+00 --5.4000e+02 -8.5714e+02 -6.4686e+00 --5.4000e+02 -8.2857e+02 -7.3893e+00 --5.4000e+02 -8.0000e+02 -8.4017e+00 --5.4000e+02 -7.7143e+02 -9.5180e+00 --5.4000e+02 -7.4286e+02 -1.0752e+01 --5.4000e+02 -7.1429e+02 -1.2119e+01 --5.4000e+02 -6.8571e+02 -1.3637e+01 --5.4000e+02 -6.5714e+02 -1.5326e+01 --5.4000e+02 -6.2857e+02 -1.7207e+01 --5.4000e+02 -6.0000e+02 -1.9301e+01 --5.4000e+02 -5.7143e+02 -2.1632e+01 --5.4000e+02 -5.4286e+02 -2.4220e+01 --5.4000e+02 -5.1429e+02 -2.7081e+01 --5.4000e+02 -4.8571e+02 -3.0221e+01 --5.4000e+02 -4.5714e+02 -3.3633e+01 --5.4000e+02 -4.2857e+02 -3.7293e+01 --5.4000e+02 -4.0000e+02 -4.1153e+01 --5.4000e+02 -3.7143e+02 -4.5144e+01 --5.4000e+02 -3.4286e+02 -4.9175e+01 --5.4000e+02 -3.1429e+02 -5.3147e+01 --5.4000e+02 -2.8571e+02 -5.6961e+01 --5.4000e+02 -2.5714e+02 -6.0529e+01 --5.4000e+02 -2.2857e+02 -6.3784e+01 --5.4000e+02 -2.0000e+02 -6.6677e+01 --5.4000e+02 -1.7143e+02 -6.9182e+01 --5.4000e+02 -1.4286e+02 -7.1289e+01 --5.4000e+02 -1.1429e+02 -7.2998e+01 --5.4000e+02 -8.5714e+01 -7.4315e+01 --5.4000e+02 -5.7143e+01 -7.5247e+01 --5.4000e+02 -2.8571e+01 -7.5803e+01 --5.4000e+02 0.0000e+00 -7.5988e+01 --5.4000e+02 2.8571e+01 -7.5803e+01 --5.4000e+02 5.7143e+01 -7.5247e+01 --5.4000e+02 8.5714e+01 -7.4315e+01 --5.4000e+02 1.1429e+02 -7.2998e+01 --5.4000e+02 1.4286e+02 -7.1289e+01 --5.4000e+02 1.7143e+02 -6.9182e+01 --5.4000e+02 2.0000e+02 -6.6677e+01 --5.4000e+02 2.2857e+02 -6.3784e+01 --5.4000e+02 2.5714e+02 -6.0529e+01 --5.4000e+02 2.8571e+02 -5.6961e+01 --5.4000e+02 3.1429e+02 -5.3147e+01 --5.4000e+02 3.4286e+02 -4.9175e+01 --5.4000e+02 3.7143e+02 -4.5144e+01 --5.4000e+02 4.0000e+02 -4.1153e+01 --5.4000e+02 4.2857e+02 -3.7293e+01 --5.4000e+02 4.5714e+02 -3.3633e+01 --5.4000e+02 4.8571e+02 -3.0221e+01 --5.4000e+02 5.1429e+02 -2.7081e+01 --5.4000e+02 5.4286e+02 -2.4220e+01 --5.4000e+02 5.7143e+02 -2.1632e+01 --5.4000e+02 6.0000e+02 -1.9301e+01 --5.4000e+02 6.2857e+02 -1.7207e+01 --5.4000e+02 6.5714e+02 -1.5326e+01 --5.4000e+02 6.8571e+02 -1.3637e+01 --5.4000e+02 7.1429e+02 -1.2119e+01 --5.4000e+02 7.4286e+02 -1.0752e+01 --5.4000e+02 7.7143e+02 -9.5180e+00 --5.4000e+02 8.0000e+02 -8.4017e+00 --5.4000e+02 8.2857e+02 -7.3893e+00 --5.4000e+02 8.5714e+02 -6.4686e+00 --5.4000e+02 8.8571e+02 -5.6292e+00 --5.4000e+02 9.1429e+02 -4.8617e+00 --5.4000e+02 9.4286e+02 -4.1582e+00 --5.4000e+02 9.7143e+02 -3.5117e+00 --5.4000e+02 1.0000e+03 -2.9162e+00 --5.4000e+02 1.0286e+03 -2.3662e+00 --5.4000e+02 1.0571e+03 -1.8572e+00 --5.4000e+02 1.0857e+03 -1.3850e+00 --5.4000e+02 1.1143e+03 -9.4603e-01 --5.4000e+02 1.1429e+03 -5.3713e-01 --5.4000e+02 1.1714e+03 -1.5549e-01 --5.4000e+02 1.2000e+03 2.0138e-01 --5.4000e+02 1.2286e+03 5.3567e-01 --5.4000e+02 1.2571e+03 8.4936e-01 --5.4000e+02 1.2857e+03 1.1442e+00 --5.4000e+02 1.3143e+03 1.4218e+00 --5.4000e+02 1.3429e+03 1.6834e+00 --5.4000e+02 1.3714e+03 1.9305e+00 --5.4000e+02 1.4000e+03 2.1641e+00 --5.4000e+02 1.4286e+03 2.3852e+00 --5.4000e+02 1.4571e+03 2.5948e+00 --5.4000e+02 1.4857e+03 2.7938e+00 --5.4000e+02 1.5143e+03 2.9828e+00 --5.4000e+02 1.5429e+03 3.1627e+00 --5.4000e+02 1.5714e+03 3.3339e+00 --5.4000e+02 1.6000e+03 3.4971e+00 --5.4000e+02 1.6286e+03 3.6529e+00 --5.4000e+02 1.6571e+03 3.8017e+00 --5.4000e+02 1.6857e+03 3.9439e+00 --5.4000e+02 1.7143e+03 4.0800e+00 --5.4000e+02 1.7429e+03 4.2104e+00 --5.4000e+02 1.7714e+03 4.3353e+00 --5.4000e+02 1.8000e+03 4.4551e+00 --5.4000e+02 1.8286e+03 4.5702e+00 --5.4000e+02 1.8571e+03 4.6807e+00 --5.4000e+02 1.8857e+03 4.7870e+00 --5.4000e+02 1.9143e+03 4.8892e+00 --5.4000e+02 1.9429e+03 4.9876e+00 --5.4000e+02 1.9714e+03 5.0824e+00 --5.4000e+02 2.0000e+03 5.1739e+00 --5.1000e+02 -2.0000e+03 5.1489e+00 --5.1000e+02 -1.9714e+03 5.0562e+00 --5.1000e+02 -1.9429e+03 4.9600e+00 --5.1000e+02 -1.9143e+03 4.8601e+00 --5.1000e+02 -1.8857e+03 4.7563e+00 --5.1000e+02 -1.8571e+03 4.6483e+00 --5.1000e+02 -1.8286e+03 4.5359e+00 --5.1000e+02 -1.8000e+03 4.4189e+00 --5.1000e+02 -1.7714e+03 4.2969e+00 --5.1000e+02 -1.7429e+03 4.1696e+00 --5.1000e+02 -1.7143e+03 4.0368e+00 --5.1000e+02 -1.6857e+03 3.8980e+00 --5.1000e+02 -1.6571e+03 3.7528e+00 --5.1000e+02 -1.6286e+03 3.6008e+00 --5.1000e+02 -1.6000e+03 3.4416e+00 --5.1000e+02 -1.5714e+03 3.2746e+00 --5.1000e+02 -1.5429e+03 3.0992e+00 --5.1000e+02 -1.5143e+03 2.9149e+00 --5.1000e+02 -1.4857e+03 2.7209e+00 --5.1000e+02 -1.4571e+03 2.5166e+00 --5.1000e+02 -1.4286e+03 2.3010e+00 --5.1000e+02 -1.4000e+03 2.0733e+00 --5.1000e+02 -1.3714e+03 1.8324e+00 --5.1000e+02 -1.3429e+03 1.5774e+00 --5.1000e+02 -1.3143e+03 1.3068e+00 --5.1000e+02 -1.2857e+03 1.0194e+00 --5.1000e+02 -1.2571e+03 7.1354e-01 --5.1000e+02 -1.2286e+03 3.8755e-01 --5.1000e+02 -1.2000e+03 3.9469e-02 --5.1000e+02 -1.1714e+03 -3.3289e-01 --5.1000e+02 -1.1429e+03 -7.3199e-01 --5.1000e+02 -1.1143e+03 -1.1606e+00 --5.1000e+02 -1.0857e+03 -1.6220e+00 --5.1000e+02 -1.0571e+03 -2.1197e+00 --5.1000e+02 -1.0286e+03 -2.6578e+00 --5.1000e+02 -1.0000e+03 -3.2412e+00 --5.1000e+02 -9.7143e+02 -3.8751e+00 --5.1000e+02 -9.4286e+02 -4.5659e+00 --5.1000e+02 -9.1429e+02 -5.3208e+00 --5.1000e+02 -8.8571e+02 -6.1480e+00 --5.1000e+02 -8.5714e+02 -7.0573e+00 --5.1000e+02 -8.2857e+02 -8.0600e+00 --5.1000e+02 -8.0000e+02 -9.1688e+00 --5.1000e+02 -7.7143e+02 -1.0399e+01 --5.1000e+02 -7.4286e+02 -1.1768e+01 --5.1000e+02 -7.1429e+02 -1.3296e+01 --5.1000e+02 -6.8571e+02 -1.5005e+01 --5.1000e+02 -6.5714e+02 -1.6921e+01 --5.1000e+02 -6.2857e+02 -1.9071e+01 --5.1000e+02 -6.0000e+02 -2.1485e+01 --5.1000e+02 -5.7143e+02 -2.4191e+01 --5.1000e+02 -5.4286e+02 -2.7214e+01 --5.1000e+02 -5.1429e+02 -3.0569e+01 --5.1000e+02 -4.8571e+02 -3.4259e+01 --5.1000e+02 -4.5714e+02 -3.8261e+01 --5.1000e+02 -4.2857e+02 -4.2526e+01 --5.1000e+02 -4.0000e+02 -4.6973e+01 --5.1000e+02 -3.7143e+02 -5.1493e+01 --5.1000e+02 -3.4286e+02 -5.5963e+01 --5.1000e+02 -3.1429e+02 -6.0261e+01 --5.1000e+02 -2.8571e+02 -6.4282e+01 --5.1000e+02 -2.5714e+02 -6.7948e+01 --5.1000e+02 -2.2857e+02 -7.1211e+01 --5.1000e+02 -2.0000e+02 -7.4050e+01 --5.1000e+02 -1.7143e+02 -7.6465e+01 --5.1000e+02 -1.4286e+02 -7.8465e+01 --5.1000e+02 -1.1429e+02 -8.0068e+01 --5.1000e+02 -8.5714e+01 -8.1292e+01 --5.1000e+02 -5.7143e+01 -8.2153e+01 --5.1000e+02 -2.8571e+01 -8.2664e+01 --5.1000e+02 0.0000e+00 -8.2834e+01 --5.1000e+02 2.8571e+01 -8.2664e+01 --5.1000e+02 5.7143e+01 -8.2153e+01 --5.1000e+02 8.5714e+01 -8.1292e+01 --5.1000e+02 1.1429e+02 -8.0068e+01 --5.1000e+02 1.4286e+02 -7.8465e+01 --5.1000e+02 1.7143e+02 -7.6465e+01 --5.1000e+02 2.0000e+02 -7.4050e+01 --5.1000e+02 2.2857e+02 -7.1211e+01 --5.1000e+02 2.5714e+02 -6.7948e+01 --5.1000e+02 2.8571e+02 -6.4282e+01 --5.1000e+02 3.1429e+02 -6.0261e+01 --5.1000e+02 3.4286e+02 -5.5963e+01 --5.1000e+02 3.7143e+02 -5.1493e+01 --5.1000e+02 4.0000e+02 -4.6973e+01 --5.1000e+02 4.2857e+02 -4.2526e+01 --5.1000e+02 4.5714e+02 -3.8261e+01 --5.1000e+02 4.8571e+02 -3.4259e+01 --5.1000e+02 5.1429e+02 -3.0569e+01 --5.1000e+02 5.4286e+02 -2.7214e+01 --5.1000e+02 5.7143e+02 -2.4191e+01 --5.1000e+02 6.0000e+02 -2.1485e+01 --5.1000e+02 6.2857e+02 -1.9071e+01 --5.1000e+02 6.5714e+02 -1.6921e+01 --5.1000e+02 6.8571e+02 -1.5005e+01 --5.1000e+02 7.1429e+02 -1.3296e+01 --5.1000e+02 7.4286e+02 -1.1768e+01 --5.1000e+02 7.7143e+02 -1.0399e+01 --5.1000e+02 8.0000e+02 -9.1688e+00 --5.1000e+02 8.2857e+02 -8.0600e+00 --5.1000e+02 8.5714e+02 -7.0573e+00 --5.1000e+02 8.8571e+02 -6.1480e+00 --5.1000e+02 9.1429e+02 -5.3208e+00 --5.1000e+02 9.4286e+02 -4.5659e+00 --5.1000e+02 9.7143e+02 -3.8751e+00 --5.1000e+02 1.0000e+03 -3.2412e+00 --5.1000e+02 1.0286e+03 -2.6578e+00 --5.1000e+02 1.0571e+03 -2.1197e+00 --5.1000e+02 1.0857e+03 -1.6220e+00 --5.1000e+02 1.1143e+03 -1.1606e+00 --5.1000e+02 1.1429e+03 -7.3199e-01 --5.1000e+02 1.1714e+03 -3.3289e-01 --5.1000e+02 1.2000e+03 3.9469e-02 --5.1000e+02 1.2286e+03 3.8755e-01 --5.1000e+02 1.2571e+03 7.1354e-01 --5.1000e+02 1.2857e+03 1.0194e+00 --5.1000e+02 1.3143e+03 1.3068e+00 --5.1000e+02 1.3429e+03 1.5774e+00 --5.1000e+02 1.3714e+03 1.8324e+00 --5.1000e+02 1.4000e+03 2.0733e+00 --5.1000e+02 1.4286e+03 2.3010e+00 --5.1000e+02 1.4571e+03 2.5166e+00 --5.1000e+02 1.4857e+03 2.7209e+00 --5.1000e+02 1.5143e+03 2.9149e+00 --5.1000e+02 1.5429e+03 3.0992e+00 --5.1000e+02 1.5714e+03 3.2746e+00 --5.1000e+02 1.6000e+03 3.4416e+00 --5.1000e+02 1.6286e+03 3.6008e+00 --5.1000e+02 1.6571e+03 3.7528e+00 --5.1000e+02 1.6857e+03 3.8980e+00 --5.1000e+02 1.7143e+03 4.0368e+00 --5.1000e+02 1.7429e+03 4.1696e+00 --5.1000e+02 1.7714e+03 4.2969e+00 --5.1000e+02 1.8000e+03 4.4189e+00 --5.1000e+02 1.8286e+03 4.5359e+00 --5.1000e+02 1.8571e+03 4.6483e+00 --5.1000e+02 1.8857e+03 4.7563e+00 --5.1000e+02 1.9143e+03 4.8601e+00 --5.1000e+02 1.9429e+03 4.9600e+00 --5.1000e+02 1.9714e+03 5.0562e+00 --5.1000e+02 2.0000e+03 5.1489e+00 --4.8000e+02 -2.0000e+03 5.1251e+00 --4.8000e+02 -1.9714e+03 5.0312e+00 --4.8000e+02 -1.9429e+03 4.9336e+00 --4.8000e+02 -1.9143e+03 4.8322e+00 --4.8000e+02 -1.8857e+03 4.7269e+00 --4.8000e+02 -1.8571e+03 4.6172e+00 --4.8000e+02 -1.8286e+03 4.5030e+00 --4.8000e+02 -1.8000e+03 4.3841e+00 --4.8000e+02 -1.7714e+03 4.2600e+00 --4.8000e+02 -1.7429e+03 4.1305e+00 --4.8000e+02 -1.7143e+03 3.9952e+00 --4.8000e+02 -1.6857e+03 3.8538e+00 --4.8000e+02 -1.6571e+03 3.7058e+00 --4.8000e+02 -1.6286e+03 3.5507e+00 --4.8000e+02 -1.6000e+03 3.3881e+00 --4.8000e+02 -1.5714e+03 3.2174e+00 --4.8000e+02 -1.5429e+03 3.0380e+00 --4.8000e+02 -1.5143e+03 2.8492e+00 --4.8000e+02 -1.4857e+03 2.6504e+00 --4.8000e+02 -1.4571e+03 2.4407e+00 --4.8000e+02 -1.4286e+03 2.2193e+00 --4.8000e+02 -1.4000e+03 1.9851e+00 --4.8000e+02 -1.3714e+03 1.7371e+00 --4.8000e+02 -1.3429e+03 1.4741e+00 --4.8000e+02 -1.3143e+03 1.1948e+00 --4.8000e+02 -1.2857e+03 8.9750e-01 --4.8000e+02 -1.2571e+03 5.8067e-01 --4.8000e+02 -1.2286e+03 2.4238e-01 --4.8000e+02 -1.2000e+03 -1.1953e-01 --4.8000e+02 -1.1714e+03 -5.0747e-01 --4.8000e+02 -1.1429e+03 -9.2420e-01 --4.8000e+02 -1.1143e+03 -1.3729e+00 --4.8000e+02 -1.0857e+03 -1.8570e+00 --4.8000e+02 -1.0571e+03 -2.3807e+00 --4.8000e+02 -1.0286e+03 -2.9487e+00 --4.8000e+02 -1.0000e+03 -3.5665e+00 --4.8000e+02 -9.7143e+02 -4.2402e+00 --4.8000e+02 -9.4286e+02 -4.9771e+00 --4.8000e+02 -9.1429e+02 -5.7858e+00 --4.8000e+02 -8.8571e+02 -6.6762e+00 --4.8000e+02 -8.5714e+02 -7.6597e+00 --4.8000e+02 -8.2857e+02 -8.7500e+00 --4.8000e+02 -8.0000e+02 -9.9629e+00 --4.8000e+02 -7.7143e+02 -1.1317e+01 --4.8000e+02 -7.4286e+02 -1.2834e+01 --4.8000e+02 -7.1429e+02 -1.4539e+01 --4.8000e+02 -6.8571e+02 -1.6461e+01 --4.8000e+02 -6.5714e+02 -1.8632e+01 --4.8000e+02 -6.2857e+02 -2.1087e+01 --4.8000e+02 -6.0000e+02 -2.3864e+01 --4.8000e+02 -5.7143e+02 -2.6995e+01 --4.8000e+02 -5.4286e+02 -3.0509e+01 --4.8000e+02 -5.1429e+02 -3.4416e+01 --4.8000e+02 -4.8571e+02 -3.8702e+01 --4.8000e+02 -4.5714e+02 -4.3318e+01 --4.8000e+02 -4.2857e+02 -4.8174e+01 --4.8000e+02 -4.0000e+02 -5.3142e+01 --4.8000e+02 -3.7143e+02 -5.8072e+01 --4.8000e+02 -3.4286e+02 -6.2817e+01 --4.8000e+02 -3.1429e+02 -6.7250e+01 --4.8000e+02 -2.8571e+02 -7.1284e+01 --4.8000e+02 -2.5714e+02 -7.4870e+01 --4.8000e+02 -2.2857e+02 -7.7993e+01 --4.8000e+02 -2.0000e+02 -8.0662e+01 --4.8000e+02 -1.7143e+02 -8.2899e+01 --4.8000e+02 -1.4286e+02 -8.4731e+01 --4.8000e+02 -1.1429e+02 -8.6187e+01 --4.8000e+02 -8.5714e+01 -8.7292e+01 --4.8000e+02 -5.7143e+01 -8.8066e+01 --4.8000e+02 -2.8571e+01 -8.8525e+01 --4.8000e+02 0.0000e+00 -8.8677e+01 --4.8000e+02 2.8571e+01 -8.8525e+01 --4.8000e+02 5.7143e+01 -8.8066e+01 --4.8000e+02 8.5714e+01 -8.7292e+01 --4.8000e+02 1.1429e+02 -8.6187e+01 --4.8000e+02 1.4286e+02 -8.4731e+01 --4.8000e+02 1.7143e+02 -8.2899e+01 --4.8000e+02 2.0000e+02 -8.0662e+01 --4.8000e+02 2.2857e+02 -7.7993e+01 --4.8000e+02 2.5714e+02 -7.4870e+01 --4.8000e+02 2.8571e+02 -7.1284e+01 --4.8000e+02 3.1429e+02 -6.7250e+01 --4.8000e+02 3.4286e+02 -6.2817e+01 --4.8000e+02 3.7143e+02 -5.8072e+01 --4.8000e+02 4.0000e+02 -5.3142e+01 --4.8000e+02 4.2857e+02 -4.8174e+01 --4.8000e+02 4.5714e+02 -4.3318e+01 --4.8000e+02 4.8571e+02 -3.8702e+01 --4.8000e+02 5.1429e+02 -3.4416e+01 --4.8000e+02 5.4286e+02 -3.0509e+01 --4.8000e+02 5.7143e+02 -2.6995e+01 --4.8000e+02 6.0000e+02 -2.3864e+01 --4.8000e+02 6.2857e+02 -2.1087e+01 --4.8000e+02 6.5714e+02 -1.8632e+01 --4.8000e+02 6.8571e+02 -1.6461e+01 --4.8000e+02 7.1429e+02 -1.4539e+01 --4.8000e+02 7.4286e+02 -1.2834e+01 --4.8000e+02 7.7143e+02 -1.1317e+01 --4.8000e+02 8.0000e+02 -9.9629e+00 --4.8000e+02 8.2857e+02 -8.7500e+00 --4.8000e+02 8.5714e+02 -7.6597e+00 --4.8000e+02 8.8571e+02 -6.6762e+00 --4.8000e+02 9.1429e+02 -5.7858e+00 --4.8000e+02 9.4286e+02 -4.9771e+00 --4.8000e+02 9.7143e+02 -4.2402e+00 --4.8000e+02 1.0000e+03 -3.5665e+00 --4.8000e+02 1.0286e+03 -2.9487e+00 --4.8000e+02 1.0571e+03 -2.3807e+00 --4.8000e+02 1.0857e+03 -1.8570e+00 --4.8000e+02 1.1143e+03 -1.3729e+00 --4.8000e+02 1.1429e+03 -9.2420e-01 --4.8000e+02 1.1714e+03 -5.0747e-01 --4.8000e+02 1.2000e+03 -1.1953e-01 --4.8000e+02 1.2286e+03 2.4238e-01 --4.8000e+02 1.2571e+03 5.8067e-01 --4.8000e+02 1.2857e+03 8.9750e-01 --4.8000e+02 1.3143e+03 1.1948e+00 --4.8000e+02 1.3429e+03 1.4741e+00 --4.8000e+02 1.3714e+03 1.7371e+00 --4.8000e+02 1.4000e+03 1.9851e+00 --4.8000e+02 1.4286e+03 2.2193e+00 --4.8000e+02 1.4571e+03 2.4407e+00 --4.8000e+02 1.4857e+03 2.6504e+00 --4.8000e+02 1.5143e+03 2.8492e+00 --4.8000e+02 1.5429e+03 3.0380e+00 --4.8000e+02 1.5714e+03 3.2174e+00 --4.8000e+02 1.6000e+03 3.3881e+00 --4.8000e+02 1.6286e+03 3.5507e+00 --4.8000e+02 1.6571e+03 3.7058e+00 --4.8000e+02 1.6857e+03 3.8538e+00 --4.8000e+02 1.7143e+03 3.9952e+00 --4.8000e+02 1.7429e+03 4.1305e+00 --4.8000e+02 1.7714e+03 4.2600e+00 --4.8000e+02 1.8000e+03 4.3841e+00 --4.8000e+02 1.8286e+03 4.5030e+00 --4.8000e+02 1.8571e+03 4.6172e+00 --4.8000e+02 1.8857e+03 4.7269e+00 --4.8000e+02 1.9143e+03 4.8322e+00 --4.8000e+02 1.9429e+03 4.9336e+00 --4.8000e+02 1.9714e+03 5.0312e+00 --4.8000e+02 2.0000e+03 5.1251e+00 --4.5000e+02 -2.0000e+03 5.1025e+00 --4.5000e+02 -1.9714e+03 5.0073e+00 --4.5000e+02 -1.9429e+03 4.9085e+00 --4.5000e+02 -1.9143e+03 4.8057e+00 --4.5000e+02 -1.8857e+03 4.6988e+00 --4.5000e+02 -1.8571e+03 4.5876e+00 --4.5000e+02 -1.8286e+03 4.4717e+00 --4.5000e+02 -1.8000e+03 4.3509e+00 --4.5000e+02 -1.7714e+03 4.2248e+00 --4.5000e+02 -1.7429e+03 4.0931e+00 --4.5000e+02 -1.7143e+03 3.9555e+00 --4.5000e+02 -1.6857e+03 3.8115e+00 --4.5000e+02 -1.6571e+03 3.6607e+00 --4.5000e+02 -1.6286e+03 3.5026e+00 --4.5000e+02 -1.6000e+03 3.3367e+00 --4.5000e+02 -1.5714e+03 3.1624e+00 --4.5000e+02 -1.5429e+03 2.9791e+00 --4.5000e+02 -1.5143e+03 2.7861e+00 --4.5000e+02 -1.4857e+03 2.5825e+00 --4.5000e+02 -1.4571e+03 2.3676e+00 --4.5000e+02 -1.4286e+03 2.1405e+00 --4.5000e+02 -1.4000e+03 1.9000e+00 --4.5000e+02 -1.3714e+03 1.6449e+00 --4.5000e+02 -1.3429e+03 1.3741e+00 --4.5000e+02 -1.3143e+03 1.0861e+00 --4.5000e+02 -1.2857e+03 7.7908e-01 --4.5000e+02 -1.2571e+03 4.5137e-01 --4.5000e+02 -1.2286e+03 1.0084e-01 --4.5000e+02 -1.2000e+03 -2.7485e-01 --4.5000e+02 -1.1714e+03 -6.7838e-01 --4.5000e+02 -1.1429e+03 -1.1128e+00 --4.5000e+02 -1.1143e+03 -1.5816e+00 --4.5000e+02 -1.0857e+03 -2.0888e+00 --4.5000e+02 -1.0571e+03 -2.6389e+00 --4.5000e+02 -1.0286e+03 -3.2373e+00 --4.5000e+02 -1.0000e+03 -3.8903e+00 --4.5000e+02 -9.7143e+02 -4.6049e+00 --4.5000e+02 -9.4286e+02 -5.3897e+00 --4.5000e+02 -9.1429e+02 -6.2545e+00 --4.5000e+02 -8.8571e+02 -7.2109e+00 --4.5000e+02 -8.5714e+02 -8.2728e+00 --4.5000e+02 -8.2857e+02 -9.4562e+00 --4.5000e+02 -8.0000e+02 -1.0781e+01 --4.5000e+02 -7.7143e+02 -1.2268e+01 --4.5000e+02 -7.4286e+02 -1.3947e+01 --4.5000e+02 -7.1429e+02 -1.5846e+01 --4.5000e+02 -6.8571e+02 -1.8003e+01 --4.5000e+02 -6.5714e+02 -2.0459e+01 --4.5000e+02 -6.2857e+02 -2.3255e+01 --4.5000e+02 -6.0000e+02 -2.6437e+01 --4.5000e+02 -5.7143e+02 -3.0043e+01 --4.5000e+02 -5.4286e+02 -3.4096e+01 --4.5000e+02 -5.1429e+02 -3.8595e+01 --4.5000e+02 -4.8571e+02 -4.3494e+01 --4.5000e+02 -4.5714e+02 -4.8699e+01 --4.5000e+02 -4.2857e+02 -5.4067e+01 --4.5000e+02 -4.0000e+02 -5.9421e+01 --4.5000e+02 -3.7143e+02 -6.4582e+01 --4.5000e+02 -3.4286e+02 -6.9403e+01 --4.5000e+02 -3.1429e+02 -7.3780e+01 --4.5000e+02 -2.8571e+02 -7.7663e+01 --4.5000e+02 -2.5714e+02 -8.1042e+01 --4.5000e+02 -2.2857e+02 -8.3935e+01 --4.5000e+02 -2.0000e+02 -8.6375e+01 --4.5000e+02 -1.7143e+02 -8.8400e+01 --4.5000e+02 -1.4286e+02 -9.0046e+01 --4.5000e+02 -1.1429e+02 -9.1348e+01 --4.5000e+02 -8.5714e+01 -9.2333e+01 --4.5000e+02 -5.7143e+01 -9.3021e+01 --4.5000e+02 -2.8571e+01 -9.3428e+01 --4.5000e+02 0.0000e+00 -9.3563e+01 --4.5000e+02 2.8571e+01 -9.3428e+01 --4.5000e+02 5.7143e+01 -9.3021e+01 --4.5000e+02 8.5714e+01 -9.2333e+01 --4.5000e+02 1.1429e+02 -9.1348e+01 --4.5000e+02 1.4286e+02 -9.0046e+01 --4.5000e+02 1.7143e+02 -8.8400e+01 --4.5000e+02 2.0000e+02 -8.6375e+01 --4.5000e+02 2.2857e+02 -8.3935e+01 --4.5000e+02 2.5714e+02 -8.1042e+01 --4.5000e+02 2.8571e+02 -7.7663e+01 --4.5000e+02 3.1429e+02 -7.3780e+01 --4.5000e+02 3.4286e+02 -6.9403e+01 --4.5000e+02 3.7143e+02 -6.4582e+01 --4.5000e+02 4.0000e+02 -5.9421e+01 --4.5000e+02 4.2857e+02 -5.4067e+01 --4.5000e+02 4.5714e+02 -4.8699e+01 --4.5000e+02 4.8571e+02 -4.3494e+01 --4.5000e+02 5.1429e+02 -3.8595e+01 --4.5000e+02 5.4286e+02 -3.4096e+01 --4.5000e+02 5.7143e+02 -3.0043e+01 --4.5000e+02 6.0000e+02 -2.6437e+01 --4.5000e+02 6.2857e+02 -2.3255e+01 --4.5000e+02 6.5714e+02 -2.0459e+01 --4.5000e+02 6.8571e+02 -1.8003e+01 --4.5000e+02 7.1429e+02 -1.5846e+01 --4.5000e+02 7.4286e+02 -1.3947e+01 --4.5000e+02 7.7143e+02 -1.2268e+01 --4.5000e+02 8.0000e+02 -1.0781e+01 --4.5000e+02 8.2857e+02 -9.4562e+00 --4.5000e+02 8.5714e+02 -8.2728e+00 --4.5000e+02 8.8571e+02 -7.2109e+00 --4.5000e+02 9.1429e+02 -6.2545e+00 --4.5000e+02 9.4286e+02 -5.3897e+00 --4.5000e+02 9.7143e+02 -4.6049e+00 --4.5000e+02 1.0000e+03 -3.8903e+00 --4.5000e+02 1.0286e+03 -3.2373e+00 --4.5000e+02 1.0571e+03 -2.6389e+00 --4.5000e+02 1.0857e+03 -2.0888e+00 --4.5000e+02 1.1143e+03 -1.5816e+00 --4.5000e+02 1.1429e+03 -1.1128e+00 --4.5000e+02 1.1714e+03 -6.7838e-01 --4.5000e+02 1.2000e+03 -2.7485e-01 --4.5000e+02 1.2286e+03 1.0084e-01 --4.5000e+02 1.2571e+03 4.5137e-01 --4.5000e+02 1.2857e+03 7.7908e-01 --4.5000e+02 1.3143e+03 1.0861e+00 --4.5000e+02 1.3429e+03 1.3741e+00 --4.5000e+02 1.3714e+03 1.6449e+00 --4.5000e+02 1.4000e+03 1.9000e+00 --4.5000e+02 1.4286e+03 2.1405e+00 --4.5000e+02 1.4571e+03 2.3676e+00 --4.5000e+02 1.4857e+03 2.5825e+00 --4.5000e+02 1.5143e+03 2.7861e+00 --4.5000e+02 1.5429e+03 2.9791e+00 --4.5000e+02 1.5714e+03 3.1624e+00 --4.5000e+02 1.6000e+03 3.3367e+00 --4.5000e+02 1.6286e+03 3.5026e+00 --4.5000e+02 1.6571e+03 3.6607e+00 --4.5000e+02 1.6857e+03 3.8115e+00 --4.5000e+02 1.7143e+03 3.9555e+00 --4.5000e+02 1.7429e+03 4.0931e+00 --4.5000e+02 1.7714e+03 4.2248e+00 --4.5000e+02 1.8000e+03 4.3509e+00 --4.5000e+02 1.8286e+03 4.4717e+00 --4.5000e+02 1.8571e+03 4.5876e+00 --4.5000e+02 1.8857e+03 4.6988e+00 --4.5000e+02 1.9143e+03 4.8057e+00 --4.5000e+02 1.9429e+03 4.9085e+00 --4.5000e+02 1.9714e+03 5.0073e+00 --4.5000e+02 2.0000e+03 5.1025e+00 --4.2000e+02 -2.0000e+03 5.0810e+00 --4.2000e+02 -1.9714e+03 4.9847e+00 --4.2000e+02 -1.9429e+03 4.8846e+00 --4.2000e+02 -1.9143e+03 4.7805e+00 --4.2000e+02 -1.8857e+03 4.6723e+00 --4.2000e+02 -1.8571e+03 4.5595e+00 --4.2000e+02 -1.8286e+03 4.4419e+00 --4.2000e+02 -1.8000e+03 4.3193e+00 --4.2000e+02 -1.7714e+03 4.1913e+00 --4.2000e+02 -1.7429e+03 4.0576e+00 --4.2000e+02 -1.7143e+03 3.9177e+00 --4.2000e+02 -1.6857e+03 3.7712e+00 --4.2000e+02 -1.6571e+03 3.6178e+00 --4.2000e+02 -1.6286e+03 3.4568e+00 --4.2000e+02 -1.6000e+03 3.2877e+00 --4.2000e+02 -1.5714e+03 3.1100e+00 --4.2000e+02 -1.5429e+03 2.9228e+00 --4.2000e+02 -1.5143e+03 2.7256e+00 --4.2000e+02 -1.4857e+03 2.5175e+00 --4.2000e+02 -1.4571e+03 2.2976e+00 --4.2000e+02 -1.4286e+03 2.0648e+00 --4.2000e+02 -1.4000e+03 1.8181e+00 --4.2000e+02 -1.3714e+03 1.5563e+00 --4.2000e+02 -1.3429e+03 1.2778e+00 --4.2000e+02 -1.3143e+03 9.8120e-01 --4.2000e+02 -1.2857e+03 6.6468e-01 --4.2000e+02 -1.2571e+03 3.2625e-01 --4.2000e+02 -1.2286e+03 -3.6358e-02 --4.2000e+02 -1.2000e+03 -4.2570e-01 --4.2000e+02 -1.1714e+03 -8.4472e-01 --4.2000e+02 -1.1429e+03 -1.2968e+00 --4.2000e+02 -1.1143e+03 -1.7857e+00 --4.2000e+02 -1.0857e+03 -2.3160e+00 --4.2000e+02 -1.0571e+03 -2.8927e+00 --4.2000e+02 -1.0286e+03 -3.5220e+00 --4.2000e+02 -1.0000e+03 -4.2107e+00 --4.2000e+02 -9.7143e+02 -4.9672e+00 --4.2000e+02 -9.4286e+02 -5.8011e+00 --4.2000e+02 -9.1429e+02 -6.7238e+00 --4.2000e+02 -8.8571e+02 -7.7490e+00 --4.2000e+02 -8.5714e+02 -8.8928e+00 --4.2000e+02 -8.2857e+02 -1.0175e+01 --4.2000e+02 -8.0000e+02 -1.1617e+01 --4.2000e+02 -7.7143e+02 -1.3248e+01 --4.2000e+02 -7.4286e+02 -1.5101e+01 --4.2000e+02 -7.1429e+02 -1.7212e+01 --4.2000e+02 -6.8571e+02 -1.9627e+01 --4.2000e+02 -6.5714e+02 -2.2395e+01 --4.2000e+02 -6.2857e+02 -2.5567e+01 --4.2000e+02 -6.0000e+02 -2.9195e+01 --4.2000e+02 -5.7143e+02 -3.3316e+01 --4.2000e+02 -5.4286e+02 -3.7943e+01 --4.2000e+02 -5.1429e+02 -4.3044e+01 --4.2000e+02 -4.8571e+02 -4.8529e+01 --4.2000e+02 -4.5714e+02 -5.4242e+01 --4.2000e+02 -4.2857e+02 -5.9983e+01 --4.2000e+02 -4.0000e+02 -6.5542e+01 --4.2000e+02 -3.7143e+02 -7.0739e+01 --4.2000e+02 -3.4286e+02 -7.5455e+01 --4.2000e+02 -3.1429e+02 -7.9631e+01 --4.2000e+02 -2.8571e+02 -8.3261e+01 --4.2000e+02 -2.5714e+02 -8.6370e+01 --4.2000e+02 -2.2857e+02 -8.9001e+01 --4.2000e+02 -2.0000e+02 -9.1202e+01 --4.2000e+02 -1.7143e+02 -9.3017e+01 --4.2000e+02 -1.4286e+02 -9.4489e+01 --4.2000e+02 -1.1429e+02 -9.5649e+01 --4.2000e+02 -8.5714e+01 -9.6525e+01 --4.2000e+02 -5.7143e+01 -9.7137e+01 --4.2000e+02 -2.8571e+01 -9.7499e+01 --4.2000e+02 0.0000e+00 -9.7618e+01 --4.2000e+02 2.8571e+01 -9.7499e+01 --4.2000e+02 5.7143e+01 -9.7137e+01 --4.2000e+02 8.5714e+01 -9.6525e+01 --4.2000e+02 1.1429e+02 -9.5649e+01 --4.2000e+02 1.4286e+02 -9.4489e+01 --4.2000e+02 1.7143e+02 -9.3017e+01 --4.2000e+02 2.0000e+02 -9.1202e+01 --4.2000e+02 2.2857e+02 -8.9001e+01 --4.2000e+02 2.5714e+02 -8.6370e+01 --4.2000e+02 2.8571e+02 -8.3261e+01 --4.2000e+02 3.1429e+02 -7.9631e+01 --4.2000e+02 3.4286e+02 -7.5455e+01 --4.2000e+02 3.7143e+02 -7.0739e+01 --4.2000e+02 4.0000e+02 -6.5542e+01 --4.2000e+02 4.2857e+02 -5.9983e+01 --4.2000e+02 4.5714e+02 -5.4242e+01 --4.2000e+02 4.8571e+02 -4.8529e+01 --4.2000e+02 5.1429e+02 -4.3044e+01 --4.2000e+02 5.4286e+02 -3.7943e+01 --4.2000e+02 5.7143e+02 -3.3316e+01 --4.2000e+02 6.0000e+02 -2.9195e+01 --4.2000e+02 6.2857e+02 -2.5567e+01 --4.2000e+02 6.5714e+02 -2.2395e+01 --4.2000e+02 6.8571e+02 -1.9627e+01 --4.2000e+02 7.1429e+02 -1.7212e+01 --4.2000e+02 7.4286e+02 -1.5101e+01 --4.2000e+02 7.7143e+02 -1.3248e+01 --4.2000e+02 8.0000e+02 -1.1617e+01 --4.2000e+02 8.2857e+02 -1.0175e+01 --4.2000e+02 8.5714e+02 -8.8928e+00 --4.2000e+02 8.8571e+02 -7.7490e+00 --4.2000e+02 9.1429e+02 -6.7238e+00 --4.2000e+02 9.4286e+02 -5.8011e+00 --4.2000e+02 9.7143e+02 -4.9672e+00 --4.2000e+02 1.0000e+03 -4.2107e+00 --4.2000e+02 1.0286e+03 -3.5220e+00 --4.2000e+02 1.0571e+03 -2.8927e+00 --4.2000e+02 1.0857e+03 -2.3160e+00 --4.2000e+02 1.1143e+03 -1.7857e+00 --4.2000e+02 1.1429e+03 -1.2968e+00 --4.2000e+02 1.1714e+03 -8.4472e-01 --4.2000e+02 1.2000e+03 -4.2570e-01 --4.2000e+02 1.2286e+03 -3.6358e-02 --4.2000e+02 1.2571e+03 3.2625e-01 --4.2000e+02 1.2857e+03 6.6468e-01 --4.2000e+02 1.3143e+03 9.8120e-01 --4.2000e+02 1.3429e+03 1.2778e+00 --4.2000e+02 1.3714e+03 1.5563e+00 --4.2000e+02 1.4000e+03 1.8181e+00 --4.2000e+02 1.4286e+03 2.0648e+00 --4.2000e+02 1.4571e+03 2.2976e+00 --4.2000e+02 1.4857e+03 2.5175e+00 --4.2000e+02 1.5143e+03 2.7256e+00 --4.2000e+02 1.5429e+03 2.9228e+00 --4.2000e+02 1.5714e+03 3.1100e+00 --4.2000e+02 1.6000e+03 3.2877e+00 --4.2000e+02 1.6286e+03 3.4568e+00 --4.2000e+02 1.6571e+03 3.6178e+00 --4.2000e+02 1.6857e+03 3.7712e+00 --4.2000e+02 1.7143e+03 3.9177e+00 --4.2000e+02 1.7429e+03 4.0576e+00 --4.2000e+02 1.7714e+03 4.1913e+00 --4.2000e+02 1.8000e+03 4.3193e+00 --4.2000e+02 1.8286e+03 4.4419e+00 --4.2000e+02 1.8571e+03 4.5595e+00 --4.2000e+02 1.8857e+03 4.6723e+00 --4.2000e+02 1.9143e+03 4.7805e+00 --4.2000e+02 1.9429e+03 4.8846e+00 --4.2000e+02 1.9714e+03 4.9847e+00 --4.2000e+02 2.0000e+03 5.0810e+00 --3.9000e+02 -2.0000e+03 5.0608e+00 --3.9000e+02 -1.9714e+03 4.9634e+00 --3.9000e+02 -1.9429e+03 4.8622e+00 --3.9000e+02 -1.9143e+03 4.7568e+00 --3.9000e+02 -1.8857e+03 4.6472e+00 --3.9000e+02 -1.8571e+03 4.5329e+00 --3.9000e+02 -1.8286e+03 4.4138e+00 --3.9000e+02 -1.8000e+03 4.2895e+00 --3.9000e+02 -1.7714e+03 4.1597e+00 --3.9000e+02 -1.7429e+03 4.0240e+00 --3.9000e+02 -1.7143e+03 3.8819e+00 --3.9000e+02 -1.6857e+03 3.7331e+00 --3.9000e+02 -1.6571e+03 3.5771e+00 --3.9000e+02 -1.6286e+03 3.4133e+00 --3.9000e+02 -1.6000e+03 3.2412e+00 --3.9000e+02 -1.5714e+03 3.0601e+00 --3.9000e+02 -1.5429e+03 2.8694e+00 --3.9000e+02 -1.5143e+03 2.6682e+00 --3.9000e+02 -1.4857e+03 2.4556e+00 --3.9000e+02 -1.4571e+03 2.2308e+00 --3.9000e+02 -1.4286e+03 1.9927e+00 --3.9000e+02 -1.4000e+03 1.7400e+00 --3.9000e+02 -1.3714e+03 1.4715e+00 --3.9000e+02 -1.3429e+03 1.1856e+00 --3.9000e+02 -1.3143e+03 8.8070e-01 --3.9000e+02 -1.2857e+03 5.5486e-01 --3.9000e+02 -1.2571e+03 2.0595e-01 --3.9000e+02 -1.2286e+03 -1.6850e-01 --3.9000e+02 -1.2000e+03 -5.7126e-01 --3.9000e+02 -1.1714e+03 -1.0055e+00 --3.9000e+02 -1.1429e+03 -1.4750e+00 --3.9000e+02 -1.1143e+03 -1.9839e+00 --3.9000e+02 -1.0857e+03 -2.5372e+00 --3.9000e+02 -1.0571e+03 -3.1405e+00 --3.9000e+02 -1.0286e+03 -3.8007e+00 --3.9000e+02 -1.0000e+03 -4.5256e+00 --3.9000e+02 -9.7143e+02 -5.3245e+00 --3.9000e+02 -9.4286e+02 -6.2084e+00 --3.9000e+02 -9.1429e+02 -7.1905e+00 --3.9000e+02 -8.8571e+02 -8.2866e+00 --3.9000e+02 -8.5714e+02 -9.5155e+00 --3.9000e+02 -8.2857e+02 -1.0900e+01 --3.9000e+02 -8.0000e+02 -1.2467e+01 --3.9000e+02 -7.7143e+02 -1.4250e+01 --3.9000e+02 -7.4286e+02 -1.6289e+01 --3.9000e+02 -7.1429e+02 -1.8628e+01 --3.9000e+02 -6.8571e+02 -2.1322e+01 --3.9000e+02 -6.5714e+02 -2.4428e+01 --3.9000e+02 -6.2857e+02 -2.8009e+01 --3.9000e+02 -6.0000e+02 -3.2116e+01 --3.9000e+02 -5.7143e+02 -3.6781e+01 --3.9000e+02 -5.4286e+02 -4.1991e+01 --3.9000e+02 -5.1429e+02 -4.7669e+01 --3.9000e+02 -4.8571e+02 -5.3662e+01 --3.9000e+02 -4.5714e+02 -5.9750e+01 --3.9000e+02 -4.2857e+02 -6.5690e+01 --3.9000e+02 -4.0000e+02 -7.1268e+01 --3.9000e+02 -3.7143e+02 -7.6336e+01 --3.9000e+02 -3.4286e+02 -8.0822e+01 --3.9000e+02 -3.1429e+02 -8.4717e+01 --3.9000e+02 -2.8571e+02 -8.8054e+01 --3.9000e+02 -2.5714e+02 -9.0882e+01 --3.9000e+02 -2.2857e+02 -9.3258e+01 --3.9000e+02 -2.0000e+02 -9.5237e+01 --3.9000e+02 -1.7143e+02 -9.6865e+01 --3.9000e+02 -1.4286e+02 -9.8183e+01 --3.9000e+02 -1.1429e+02 -9.9222e+01 --3.9000e+02 -8.5714e+01 -1.0001e+02 --3.9000e+02 -5.7143e+01 -1.0055e+02 --3.9000e+02 -2.8571e+01 -1.0088e+02 --3.9000e+02 0.0000e+00 -1.0099e+02 --3.9000e+02 2.8571e+01 -1.0088e+02 --3.9000e+02 5.7143e+01 -1.0055e+02 --3.9000e+02 8.5714e+01 -1.0001e+02 --3.9000e+02 1.1429e+02 -9.9222e+01 --3.9000e+02 1.4286e+02 -9.8183e+01 --3.9000e+02 1.7143e+02 -9.6865e+01 --3.9000e+02 2.0000e+02 -9.5237e+01 --3.9000e+02 2.2857e+02 -9.3258e+01 --3.9000e+02 2.5714e+02 -9.0882e+01 --3.9000e+02 2.8571e+02 -8.8054e+01 --3.9000e+02 3.1429e+02 -8.4717e+01 --3.9000e+02 3.4286e+02 -8.0822e+01 --3.9000e+02 3.7143e+02 -7.6336e+01 --3.9000e+02 4.0000e+02 -7.1268e+01 --3.9000e+02 4.2857e+02 -6.5690e+01 --3.9000e+02 4.5714e+02 -5.9750e+01 --3.9000e+02 4.8571e+02 -5.3662e+01 --3.9000e+02 5.1429e+02 -4.7669e+01 --3.9000e+02 5.4286e+02 -4.1991e+01 --3.9000e+02 5.7143e+02 -3.6781e+01 --3.9000e+02 6.0000e+02 -3.2116e+01 --3.9000e+02 6.2857e+02 -2.8009e+01 --3.9000e+02 6.5714e+02 -2.4428e+01 --3.9000e+02 6.8571e+02 -2.1322e+01 --3.9000e+02 7.1429e+02 -1.8628e+01 --3.9000e+02 7.4286e+02 -1.6289e+01 --3.9000e+02 7.7143e+02 -1.4250e+01 --3.9000e+02 8.0000e+02 -1.2467e+01 --3.9000e+02 8.2857e+02 -1.0900e+01 --3.9000e+02 8.5714e+02 -9.5155e+00 --3.9000e+02 8.8571e+02 -8.2866e+00 --3.9000e+02 9.1429e+02 -7.1905e+00 --3.9000e+02 9.4286e+02 -6.2084e+00 --3.9000e+02 9.7143e+02 -5.3245e+00 --3.9000e+02 1.0000e+03 -4.5256e+00 --3.9000e+02 1.0286e+03 -3.8007e+00 --3.9000e+02 1.0571e+03 -3.1405e+00 --3.9000e+02 1.0857e+03 -2.5372e+00 --3.9000e+02 1.1143e+03 -1.9839e+00 --3.9000e+02 1.1429e+03 -1.4750e+00 --3.9000e+02 1.1714e+03 -1.0055e+00 --3.9000e+02 1.2000e+03 -5.7126e-01 --3.9000e+02 1.2286e+03 -1.6850e-01 --3.9000e+02 1.2571e+03 2.0595e-01 --3.9000e+02 1.2857e+03 5.5486e-01 --3.9000e+02 1.3143e+03 8.8070e-01 --3.9000e+02 1.3429e+03 1.1856e+00 --3.9000e+02 1.3714e+03 1.4715e+00 --3.9000e+02 1.4000e+03 1.7400e+00 --3.9000e+02 1.4286e+03 1.9927e+00 --3.9000e+02 1.4571e+03 2.2308e+00 --3.9000e+02 1.4857e+03 2.4556e+00 --3.9000e+02 1.5143e+03 2.6682e+00 --3.9000e+02 1.5429e+03 2.8694e+00 --3.9000e+02 1.5714e+03 3.0601e+00 --3.9000e+02 1.6000e+03 3.2412e+00 --3.9000e+02 1.6286e+03 3.4133e+00 --3.9000e+02 1.6571e+03 3.5771e+00 --3.9000e+02 1.6857e+03 3.7331e+00 --3.9000e+02 1.7143e+03 3.8819e+00 --3.9000e+02 1.7429e+03 4.0240e+00 --3.9000e+02 1.7714e+03 4.1597e+00 --3.9000e+02 1.8000e+03 4.2895e+00 --3.9000e+02 1.8286e+03 4.4138e+00 --3.9000e+02 1.8571e+03 4.5329e+00 --3.9000e+02 1.8857e+03 4.6472e+00 --3.9000e+02 1.9143e+03 4.7568e+00 --3.9000e+02 1.9429e+03 4.8622e+00 --3.9000e+02 1.9714e+03 4.9634e+00 --3.9000e+02 2.0000e+03 5.0608e+00 --3.6000e+02 -2.0000e+03 5.0419e+00 --3.6000e+02 -1.9714e+03 4.9435e+00 --3.6000e+02 -1.9429e+03 4.8411e+00 --3.6000e+02 -1.9143e+03 4.7346e+00 --3.6000e+02 -1.8857e+03 4.6237e+00 --3.6000e+02 -1.8571e+03 4.5081e+00 --3.6000e+02 -1.8286e+03 4.3875e+00 --3.6000e+02 -1.8000e+03 4.2616e+00 --3.6000e+02 -1.7714e+03 4.1300e+00 --3.6000e+02 -1.7429e+03 3.9924e+00 --3.6000e+02 -1.7143e+03 3.8483e+00 --3.6000e+02 -1.6857e+03 3.6973e+00 --3.6000e+02 -1.6571e+03 3.5388e+00 --3.6000e+02 -1.6286e+03 3.3724e+00 --3.6000e+02 -1.6000e+03 3.1974e+00 --3.6000e+02 -1.5714e+03 3.0132e+00 --3.6000e+02 -1.5429e+03 2.8189e+00 --3.6000e+02 -1.5143e+03 2.6139e+00 --3.6000e+02 -1.4857e+03 2.3971e+00 --3.6000e+02 -1.4571e+03 2.1677e+00 --3.6000e+02 -1.4286e+03 1.9244e+00 --3.6000e+02 -1.4000e+03 1.6659e+00 --3.6000e+02 -1.3714e+03 1.3910e+00 --3.6000e+02 -1.3429e+03 1.0980e+00 --3.6000e+02 -1.3143e+03 7.8505e-01 --3.6000e+02 -1.2857e+03 4.5020e-01 --3.6000e+02 -1.2571e+03 9.1122e-02 --3.6000e+02 -1.2286e+03 -2.9483e-01 --3.6000e+02 -1.2000e+03 -7.1066e-01 --3.6000e+02 -1.1714e+03 -1.1599e+00 --3.6000e+02 -1.1429e+03 -1.6464e+00 --3.6000e+02 -1.1143e+03 -2.1749e+00 --3.6000e+02 -1.0857e+03 -2.7509e+00 --3.6000e+02 -1.0571e+03 -3.3806e+00 --3.6000e+02 -1.0286e+03 -4.0716e+00 --3.6000e+02 -1.0000e+03 -4.8325e+00 --3.6000e+02 -9.7143e+02 -5.6739e+00 --3.6000e+02 -9.4286e+02 -6.6084e+00 --3.6000e+02 -9.1429e+02 -7.6508e+00 --3.6000e+02 -8.8571e+02 -8.8192e+00 --3.6000e+02 -8.5714e+02 -1.0136e+01 --3.6000e+02 -8.2857e+02 -1.1626e+01 --3.6000e+02 -8.0000e+02 -1.3324e+01 --3.6000e+02 -7.7143e+02 -1.5266e+01 --3.6000e+02 -7.4286e+02 -1.7501e+01 --3.6000e+02 -7.1429e+02 -2.0082e+01 --3.6000e+02 -6.8571e+02 -2.3073e+01 --3.6000e+02 -6.5714e+02 -2.6542e+01 --3.6000e+02 -6.2857e+02 -3.0556e+01 --3.6000e+02 -6.0000e+02 -3.5164e+01 --3.6000e+02 -5.7143e+02 -4.0381e+01 --3.6000e+02 -5.4286e+02 -4.6154e+01 --3.6000e+02 -5.1429e+02 -5.2343e+01 --3.6000e+02 -4.8571e+02 -5.8724e+01 --3.6000e+02 -4.5714e+02 -6.5027e+01 --3.6000e+02 -4.2857e+02 -7.0993e+01 --3.6000e+02 -4.0000e+02 -7.6439e+01 --3.6000e+02 -3.7143e+02 -8.1268e+01 --3.6000e+02 -3.4286e+02 -8.5461e+01 --3.6000e+02 -3.1429e+02 -8.9051e+01 --3.6000e+02 -2.8571e+02 -9.2096e+01 --3.6000e+02 -2.5714e+02 -9.4663e+01 --3.6000e+02 -2.2857e+02 -9.6812e+01 --3.6000e+02 -2.0000e+02 -9.8598e+01 --3.6000e+02 -1.7143e+02 -1.0007e+02 --3.6000e+02 -1.4286e+02 -1.0126e+02 --3.6000e+02 -1.1429e+02 -1.0220e+02 --3.6000e+02 -8.5714e+01 -1.0291e+02 --3.6000e+02 -5.7143e+01 -1.0341e+02 --3.6000e+02 -2.8571e+01 -1.0370e+02 --3.6000e+02 0.0000e+00 -1.0380e+02 --3.6000e+02 2.8571e+01 -1.0370e+02 --3.6000e+02 5.7143e+01 -1.0341e+02 --3.6000e+02 8.5714e+01 -1.0291e+02 --3.6000e+02 1.1429e+02 -1.0220e+02 --3.6000e+02 1.4286e+02 -1.0126e+02 --3.6000e+02 1.7143e+02 -1.0007e+02 --3.6000e+02 2.0000e+02 -9.8598e+01 --3.6000e+02 2.2857e+02 -9.6812e+01 --3.6000e+02 2.5714e+02 -9.4663e+01 --3.6000e+02 2.8571e+02 -9.2096e+01 --3.6000e+02 3.1429e+02 -8.9051e+01 --3.6000e+02 3.4286e+02 -8.5461e+01 --3.6000e+02 3.7143e+02 -8.1268e+01 --3.6000e+02 4.0000e+02 -7.6439e+01 --3.6000e+02 4.2857e+02 -7.0993e+01 --3.6000e+02 4.5714e+02 -6.5027e+01 --3.6000e+02 4.8571e+02 -5.8724e+01 --3.6000e+02 5.1429e+02 -5.2343e+01 --3.6000e+02 5.4286e+02 -4.6154e+01 --3.6000e+02 5.7143e+02 -4.0381e+01 --3.6000e+02 6.0000e+02 -3.5164e+01 --3.6000e+02 6.2857e+02 -3.0556e+01 --3.6000e+02 6.5714e+02 -2.6542e+01 --3.6000e+02 6.8571e+02 -2.3073e+01 --3.6000e+02 7.1429e+02 -2.0082e+01 --3.6000e+02 7.4286e+02 -1.7501e+01 --3.6000e+02 7.7143e+02 -1.5266e+01 --3.6000e+02 8.0000e+02 -1.3324e+01 --3.6000e+02 8.2857e+02 -1.1626e+01 --3.6000e+02 8.5714e+02 -1.0136e+01 --3.6000e+02 8.8571e+02 -8.8192e+00 --3.6000e+02 9.1429e+02 -7.6508e+00 --3.6000e+02 9.4286e+02 -6.6084e+00 --3.6000e+02 9.7143e+02 -5.6739e+00 --3.6000e+02 1.0000e+03 -4.8325e+00 --3.6000e+02 1.0286e+03 -4.0716e+00 --3.6000e+02 1.0571e+03 -3.3806e+00 --3.6000e+02 1.0857e+03 -2.7509e+00 --3.6000e+02 1.1143e+03 -2.1749e+00 --3.6000e+02 1.1429e+03 -1.6464e+00 --3.6000e+02 1.1714e+03 -1.1599e+00 --3.6000e+02 1.2000e+03 -7.1066e-01 --3.6000e+02 1.2286e+03 -2.9483e-01 --3.6000e+02 1.2571e+03 9.1122e-02 --3.6000e+02 1.2857e+03 4.5020e-01 --3.6000e+02 1.3143e+03 7.8505e-01 --3.6000e+02 1.3429e+03 1.0980e+00 --3.6000e+02 1.3714e+03 1.3910e+00 --3.6000e+02 1.4000e+03 1.6659e+00 --3.6000e+02 1.4286e+03 1.9244e+00 --3.6000e+02 1.4571e+03 2.1677e+00 --3.6000e+02 1.4857e+03 2.3971e+00 --3.6000e+02 1.5143e+03 2.6139e+00 --3.6000e+02 1.5429e+03 2.8189e+00 --3.6000e+02 1.5714e+03 3.0132e+00 --3.6000e+02 1.6000e+03 3.1974e+00 --3.6000e+02 1.6286e+03 3.3724e+00 --3.6000e+02 1.6571e+03 3.5388e+00 --3.6000e+02 1.6857e+03 3.6973e+00 --3.6000e+02 1.7143e+03 3.8483e+00 --3.6000e+02 1.7429e+03 3.9924e+00 --3.6000e+02 1.7714e+03 4.1300e+00 --3.6000e+02 1.8000e+03 4.2616e+00 --3.6000e+02 1.8286e+03 4.3875e+00 --3.6000e+02 1.8571e+03 4.5081e+00 --3.6000e+02 1.8857e+03 4.6237e+00 --3.6000e+02 1.9143e+03 4.7346e+00 --3.6000e+02 1.9429e+03 4.8411e+00 --3.6000e+02 1.9714e+03 4.9435e+00 --3.6000e+02 2.0000e+03 5.0419e+00 --3.3000e+02 -2.0000e+03 5.0243e+00 --3.3000e+02 -1.9714e+03 4.9249e+00 --3.3000e+02 -1.9429e+03 4.8215e+00 --3.3000e+02 -1.9143e+03 4.7139e+00 --3.3000e+02 -1.8857e+03 4.6018e+00 --3.3000e+02 -1.8571e+03 4.4849e+00 --3.3000e+02 -1.8286e+03 4.3629e+00 --3.3000e+02 -1.8000e+03 4.2355e+00 --3.3000e+02 -1.7714e+03 4.1023e+00 --3.3000e+02 -1.7429e+03 3.9629e+00 --3.3000e+02 -1.7143e+03 3.8169e+00 --3.3000e+02 -1.6857e+03 3.6638e+00 --3.3000e+02 -1.6571e+03 3.5031e+00 --3.3000e+02 -1.6286e+03 3.3342e+00 --3.3000e+02 -1.6000e+03 3.1565e+00 --3.3000e+02 -1.5714e+03 2.9692e+00 --3.3000e+02 -1.5429e+03 2.7717e+00 --3.3000e+02 -1.5143e+03 2.5630e+00 --3.3000e+02 -1.4857e+03 2.3423e+00 --3.3000e+02 -1.4571e+03 2.1084e+00 --3.3000e+02 -1.4286e+03 1.8601e+00 --3.3000e+02 -1.4000e+03 1.5963e+00 --3.3000e+02 -1.3714e+03 1.3152e+00 --3.3000e+02 -1.3429e+03 1.0154e+00 --3.3000e+02 -1.3143e+03 6.9477e-01 --3.3000e+02 -1.2857e+03 3.5128e-01 --3.3000e+02 -1.2571e+03 -1.7572e-02 --3.3000e+02 -1.2286e+03 -4.1460e-01 --3.3000e+02 -1.2000e+03 -8.4306e-01 --3.3000e+02 -1.1714e+03 -1.3067e+00 --3.3000e+02 -1.1429e+03 -1.8098e+00 --3.3000e+02 -1.1143e+03 -2.3575e+00 --3.3000e+02 -1.0857e+03 -2.9556e+00 --3.3000e+02 -1.0571e+03 -3.6112e+00 --3.3000e+02 -1.0286e+03 -4.3324e+00 --3.3000e+02 -1.0000e+03 -5.1290e+00 --3.3000e+02 -9.7143e+02 -6.0127e+00 --3.3000e+02 -9.4286e+02 -6.9975e+00 --3.3000e+02 -9.1429e+02 -8.1004e+00 --3.3000e+02 -8.8571e+02 -9.3419e+00 --3.3000e+02 -8.5714e+02 -1.0747e+01 --3.3000e+02 -8.2857e+02 -1.2346e+01 --3.3000e+02 -8.0000e+02 -1.4177e+01 --3.3000e+02 -7.7143e+02 -1.6285e+01 --3.3000e+02 -7.4286e+02 -1.8724e+01 --3.3000e+02 -7.1429e+02 -2.1559e+01 --3.3000e+02 -6.8571e+02 -2.4863e+01 --3.3000e+02 -6.5714e+02 -2.8711e+01 --3.3000e+02 -6.2857e+02 -3.3173e+01 --3.3000e+02 -6.0000e+02 -3.8290e+01 --3.3000e+02 -5.7143e+02 -4.4043e+01 --3.3000e+02 -5.4286e+02 -5.0326e+01 --3.3000e+02 -5.1429e+02 -5.6925e+01 --3.3000e+02 -4.8571e+02 -6.3554e+01 --3.3000e+02 -4.5714e+02 -6.9913e+01 --3.3000e+02 -4.2857e+02 -7.5768e+01 --3.3000e+02 -4.0000e+02 -8.0982e+01 --3.3000e+02 -3.7143e+02 -8.5519e+01 --3.3000e+02 -3.4286e+02 -8.9404e+01 --3.3000e+02 -3.1429e+02 -9.2701e+01 --3.3000e+02 -2.8571e+02 -9.5482e+01 --3.3000e+02 -2.5714e+02 -9.7819e+01 --3.3000e+02 -2.2857e+02 -9.9775e+01 --3.3000e+02 -2.0000e+02 -1.0140e+02 --3.3000e+02 -1.7143e+02 -1.0274e+02 --3.3000e+02 -1.4286e+02 -1.0383e+02 --3.3000e+02 -1.1429e+02 -1.0469e+02 --3.3000e+02 -8.5714e+01 -1.0534e+02 --3.3000e+02 -5.7143e+01 -1.0580e+02 --3.3000e+02 -2.8571e+01 -1.0607e+02 --3.3000e+02 0.0000e+00 -1.0616e+02 --3.3000e+02 2.8571e+01 -1.0607e+02 --3.3000e+02 5.7143e+01 -1.0580e+02 --3.3000e+02 8.5714e+01 -1.0534e+02 --3.3000e+02 1.1429e+02 -1.0469e+02 --3.3000e+02 1.4286e+02 -1.0383e+02 --3.3000e+02 1.7143e+02 -1.0274e+02 --3.3000e+02 2.0000e+02 -1.0140e+02 --3.3000e+02 2.2857e+02 -9.9775e+01 --3.3000e+02 2.5714e+02 -9.7819e+01 --3.3000e+02 2.8571e+02 -9.5482e+01 --3.3000e+02 3.1429e+02 -9.2701e+01 --3.3000e+02 3.4286e+02 -8.9404e+01 --3.3000e+02 3.7143e+02 -8.5519e+01 --3.3000e+02 4.0000e+02 -8.0982e+01 --3.3000e+02 4.2857e+02 -7.5768e+01 --3.3000e+02 4.5714e+02 -6.9913e+01 --3.3000e+02 4.8571e+02 -6.3554e+01 --3.3000e+02 5.1429e+02 -5.6925e+01 --3.3000e+02 5.4286e+02 -5.0326e+01 --3.3000e+02 5.7143e+02 -4.4043e+01 --3.3000e+02 6.0000e+02 -3.8290e+01 --3.3000e+02 6.2857e+02 -3.3173e+01 --3.3000e+02 6.5714e+02 -2.8711e+01 --3.3000e+02 6.8571e+02 -2.4863e+01 --3.3000e+02 7.1429e+02 -2.1559e+01 --3.3000e+02 7.4286e+02 -1.8724e+01 --3.3000e+02 7.7143e+02 -1.6285e+01 --3.3000e+02 8.0000e+02 -1.4177e+01 --3.3000e+02 8.2857e+02 -1.2346e+01 --3.3000e+02 8.5714e+02 -1.0747e+01 --3.3000e+02 8.8571e+02 -9.3419e+00 --3.3000e+02 9.1429e+02 -8.1004e+00 --3.3000e+02 9.4286e+02 -6.9975e+00 --3.3000e+02 9.7143e+02 -6.0127e+00 --3.3000e+02 1.0000e+03 -5.1290e+00 --3.3000e+02 1.0286e+03 -4.3324e+00 --3.3000e+02 1.0571e+03 -3.6112e+00 --3.3000e+02 1.0857e+03 -2.9556e+00 --3.3000e+02 1.1143e+03 -2.3575e+00 --3.3000e+02 1.1429e+03 -1.8098e+00 --3.3000e+02 1.1714e+03 -1.3067e+00 --3.3000e+02 1.2000e+03 -8.4306e-01 --3.3000e+02 1.2286e+03 -4.1460e-01 --3.3000e+02 1.2571e+03 -1.7572e-02 --3.3000e+02 1.2857e+03 3.5128e-01 --3.3000e+02 1.3143e+03 6.9477e-01 --3.3000e+02 1.3429e+03 1.0154e+00 --3.3000e+02 1.3714e+03 1.3152e+00 --3.3000e+02 1.4000e+03 1.5963e+00 --3.3000e+02 1.4286e+03 1.8601e+00 --3.3000e+02 1.4571e+03 2.1084e+00 --3.3000e+02 1.4857e+03 2.3423e+00 --3.3000e+02 1.5143e+03 2.5630e+00 --3.3000e+02 1.5429e+03 2.7717e+00 --3.3000e+02 1.5714e+03 2.9692e+00 --3.3000e+02 1.6000e+03 3.1565e+00 --3.3000e+02 1.6286e+03 3.3342e+00 --3.3000e+02 1.6571e+03 3.5031e+00 --3.3000e+02 1.6857e+03 3.6638e+00 --3.3000e+02 1.7143e+03 3.8169e+00 --3.3000e+02 1.7429e+03 3.9629e+00 --3.3000e+02 1.7714e+03 4.1023e+00 --3.3000e+02 1.8000e+03 4.2355e+00 --3.3000e+02 1.8286e+03 4.3629e+00 --3.3000e+02 1.8571e+03 4.4849e+00 --3.3000e+02 1.8857e+03 4.6018e+00 --3.3000e+02 1.9143e+03 4.7139e+00 --3.3000e+02 1.9429e+03 4.8215e+00 --3.3000e+02 1.9714e+03 4.9249e+00 --3.3000e+02 2.0000e+03 5.0243e+00 --3.0000e+02 -2.0000e+03 5.0081e+00 --3.0000e+02 -1.9714e+03 4.9079e+00 --3.0000e+02 -1.9429e+03 4.8035e+00 --3.0000e+02 -1.9143e+03 4.6949e+00 --3.0000e+02 -1.8857e+03 4.5816e+00 --3.0000e+02 -1.8571e+03 4.4635e+00 --3.0000e+02 -1.8286e+03 4.3403e+00 --3.0000e+02 -1.8000e+03 4.2114e+00 --3.0000e+02 -1.7714e+03 4.0767e+00 --3.0000e+02 -1.7429e+03 3.9357e+00 --3.0000e+02 -1.7143e+03 3.7879e+00 --3.0000e+02 -1.6857e+03 3.6328e+00 --3.0000e+02 -1.6571e+03 3.4700e+00 --3.0000e+02 -1.6286e+03 3.2988e+00 --3.0000e+02 -1.6000e+03 3.1185e+00 --3.0000e+02 -1.5714e+03 2.9285e+00 --3.0000e+02 -1.5429e+03 2.7278e+00 --3.0000e+02 -1.5143e+03 2.5158e+00 --3.0000e+02 -1.4857e+03 2.2913e+00 --3.0000e+02 -1.4571e+03 2.0532e+00 --3.0000e+02 -1.4286e+03 1.8004e+00 --3.0000e+02 -1.4000e+03 1.5313e+00 --3.0000e+02 -1.3714e+03 1.2445e+00 --3.0000e+02 -1.3429e+03 9.3820e-01 --3.0000e+02 -1.3143e+03 6.1034e-01 --3.0000e+02 -1.2857e+03 2.5865e-01 --3.0000e+02 -1.2571e+03 -1.1948e-01 --3.0000e+02 -1.2286e+03 -5.2707e-01 --3.0000e+02 -1.2000e+03 -9.6758e-01 --3.0000e+02 -1.1714e+03 -1.4450e+00 --3.0000e+02 -1.1429e+03 -1.9640e+00 --3.0000e+02 -1.1143e+03 -2.5301e+00 --3.0000e+02 -1.0857e+03 -3.1497e+00 --3.0000e+02 -1.0571e+03 -3.8303e+00 --3.0000e+02 -1.0286e+03 -4.5809e+00 --3.0000e+02 -1.0000e+03 -5.4124e+00 --3.0000e+02 -9.7143e+02 -6.3375e+00 --3.0000e+02 -9.4286e+02 -7.3720e+00 --3.0000e+02 -9.1429e+02 -8.5348e+00 --3.0000e+02 -8.8571e+02 -9.8491e+00 --3.0000e+02 -8.5714e+02 -1.1343e+01 --3.0000e+02 -8.2857e+02 -1.3052e+01 --3.0000e+02 -8.0000e+02 -1.5019e+01 --3.0000e+02 -7.7143e+02 -1.7295e+01 --3.0000e+02 -7.4286e+02 -1.9944e+01 --3.0000e+02 -7.1429e+02 -2.3040e+01 --3.0000e+02 -6.8571e+02 -2.6665e+01 --3.0000e+02 -6.5714e+02 -3.0902e+01 --3.0000e+02 -6.2857e+02 -3.5816e+01 --3.0000e+02 -6.0000e+02 -4.1430e+01 --3.0000e+02 -5.7143e+02 -4.7681e+01 --3.0000e+02 -5.4286e+02 -5.4393e+01 --3.0000e+02 -5.1429e+02 -6.1284e+01 --3.0000e+02 -4.8571e+02 -6.8019e+01 --3.0000e+02 -4.5714e+02 -7.4307e+01 --3.0000e+02 -4.2857e+02 -7.9957e+01 --3.0000e+02 -4.0000e+02 -8.4893e+01 --3.0000e+02 -3.7143e+02 -8.9128e+01 --3.0000e+02 -3.4286e+02 -9.2722e+01 --3.0000e+02 -3.1429e+02 -9.5755e+01 --3.0000e+02 -2.8571e+02 -9.8309e+01 --3.0000e+02 -2.5714e+02 -1.0045e+02 --3.0000e+02 -2.2857e+02 -1.0225e+02 --3.0000e+02 -2.0000e+02 -1.0375e+02 --3.0000e+02 -1.7143e+02 -1.0499e+02 --3.0000e+02 -1.4286e+02 -1.0599e+02 --3.0000e+02 -1.1429e+02 -1.0679e+02 --3.0000e+02 -8.5714e+01 -1.0740e+02 --3.0000e+02 -5.7143e+01 -1.0783e+02 --3.0000e+02 -2.8571e+01 -1.0808e+02 --3.0000e+02 0.0000e+00 -1.0817e+02 --3.0000e+02 2.8571e+01 -1.0808e+02 --3.0000e+02 5.7143e+01 -1.0783e+02 --3.0000e+02 8.5714e+01 -1.0740e+02 --3.0000e+02 1.1429e+02 -1.0679e+02 --3.0000e+02 1.4286e+02 -1.0599e+02 --3.0000e+02 1.7143e+02 -1.0499e+02 --3.0000e+02 2.0000e+02 -1.0375e+02 --3.0000e+02 2.2857e+02 -1.0225e+02 --3.0000e+02 2.5714e+02 -1.0045e+02 --3.0000e+02 2.8571e+02 -9.8309e+01 --3.0000e+02 3.1429e+02 -9.5755e+01 --3.0000e+02 3.4286e+02 -9.2722e+01 --3.0000e+02 3.7143e+02 -8.9128e+01 --3.0000e+02 4.0000e+02 -8.4893e+01 --3.0000e+02 4.2857e+02 -7.9957e+01 --3.0000e+02 4.5714e+02 -7.4307e+01 --3.0000e+02 4.8571e+02 -6.8019e+01 --3.0000e+02 5.1429e+02 -6.1284e+01 --3.0000e+02 5.4286e+02 -5.4393e+01 --3.0000e+02 5.7143e+02 -4.7681e+01 --3.0000e+02 6.0000e+02 -4.1430e+01 --3.0000e+02 6.2857e+02 -3.5816e+01 --3.0000e+02 6.5714e+02 -3.0902e+01 --3.0000e+02 6.8571e+02 -2.6665e+01 --3.0000e+02 7.1429e+02 -2.3040e+01 --3.0000e+02 7.4286e+02 -1.9944e+01 --3.0000e+02 7.7143e+02 -1.7295e+01 --3.0000e+02 8.0000e+02 -1.5019e+01 --3.0000e+02 8.2857e+02 -1.3052e+01 --3.0000e+02 8.5714e+02 -1.1343e+01 --3.0000e+02 8.8571e+02 -9.8491e+00 --3.0000e+02 9.1429e+02 -8.5348e+00 --3.0000e+02 9.4286e+02 -7.3720e+00 --3.0000e+02 9.7143e+02 -6.3375e+00 --3.0000e+02 1.0000e+03 -5.4124e+00 --3.0000e+02 1.0286e+03 -4.5809e+00 --3.0000e+02 1.0571e+03 -3.8303e+00 --3.0000e+02 1.0857e+03 -3.1497e+00 --3.0000e+02 1.1143e+03 -2.5301e+00 --3.0000e+02 1.1429e+03 -1.9640e+00 --3.0000e+02 1.1714e+03 -1.4450e+00 --3.0000e+02 1.2000e+03 -9.6758e-01 --3.0000e+02 1.2286e+03 -5.2707e-01 --3.0000e+02 1.2571e+03 -1.1948e-01 --3.0000e+02 1.2857e+03 2.5865e-01 --3.0000e+02 1.3143e+03 6.1034e-01 --3.0000e+02 1.3429e+03 9.3820e-01 --3.0000e+02 1.3714e+03 1.2445e+00 --3.0000e+02 1.4000e+03 1.5313e+00 --3.0000e+02 1.4286e+03 1.8004e+00 --3.0000e+02 1.4571e+03 2.0532e+00 --3.0000e+02 1.4857e+03 2.2913e+00 --3.0000e+02 1.5143e+03 2.5158e+00 --3.0000e+02 1.5429e+03 2.7278e+00 --3.0000e+02 1.5714e+03 2.9285e+00 --3.0000e+02 1.6000e+03 3.1185e+00 --3.0000e+02 1.6286e+03 3.2988e+00 --3.0000e+02 1.6571e+03 3.4700e+00 --3.0000e+02 1.6857e+03 3.6328e+00 --3.0000e+02 1.7143e+03 3.7879e+00 --3.0000e+02 1.7429e+03 3.9357e+00 --3.0000e+02 1.7714e+03 4.0767e+00 --3.0000e+02 1.8000e+03 4.2114e+00 --3.0000e+02 1.8286e+03 4.3403e+00 --3.0000e+02 1.8571e+03 4.4635e+00 --3.0000e+02 1.8857e+03 4.5816e+00 --3.0000e+02 1.9143e+03 4.6949e+00 --3.0000e+02 1.9429e+03 4.8035e+00 --3.0000e+02 1.9714e+03 4.9079e+00 --3.0000e+02 2.0000e+03 5.0081e+00 --2.7000e+02 -2.0000e+03 4.9934e+00 --2.7000e+02 -1.9714e+03 4.8923e+00 --2.7000e+02 -1.9429e+03 4.7870e+00 --2.7000e+02 -1.9143e+03 4.6774e+00 --2.7000e+02 -1.8857e+03 4.5632e+00 --2.7000e+02 -1.8571e+03 4.4440e+00 --2.7000e+02 -1.8286e+03 4.3195e+00 --2.7000e+02 -1.8000e+03 4.1894e+00 --2.7000e+02 -1.7714e+03 4.0533e+00 --2.7000e+02 -1.7429e+03 3.9108e+00 --2.7000e+02 -1.7143e+03 3.7613e+00 --2.7000e+02 -1.6857e+03 3.6045e+00 --2.7000e+02 -1.6571e+03 3.4397e+00 --2.7000e+02 -1.6286e+03 3.2663e+00 --2.7000e+02 -1.6000e+03 3.0837e+00 --2.7000e+02 -1.5714e+03 2.8910e+00 --2.7000e+02 -1.5429e+03 2.6876e+00 --2.7000e+02 -1.5143e+03 2.4724e+00 --2.7000e+02 -1.4857e+03 2.2444e+00 --2.7000e+02 -1.4571e+03 2.0024e+00 --2.7000e+02 -1.4286e+03 1.7453e+00 --2.7000e+02 -1.4000e+03 1.4714e+00 --2.7000e+02 -1.3714e+03 1.1793e+00 --2.7000e+02 -1.3429e+03 8.6691e-01 --2.7000e+02 -1.3143e+03 5.3225e-01 --2.7000e+02 -1.2857e+03 1.7288e-01 --2.7000e+02 -1.2571e+03 -2.1397e-01 --2.7000e+02 -1.2286e+03 -6.3149e-01 --2.7000e+02 -1.2000e+03 -1.0834e+00 --2.7000e+02 -1.1714e+03 -1.5739e+00 --2.7000e+02 -1.1429e+03 -2.1080e+00 --2.7000e+02 -1.1143e+03 -2.6915e+00 --2.7000e+02 -1.0857e+03 -3.3315e+00 --2.7000e+02 -1.0571e+03 -4.0360e+00 --2.7000e+02 -1.0286e+03 -4.8149e+00 --2.7000e+02 -1.0000e+03 -5.6799e+00 --2.7000e+02 -9.7143e+02 -6.6452e+00 --2.7000e+02 -9.4286e+02 -7.7279e+00 --2.7000e+02 -9.1429e+02 -8.9492e+00 --2.7000e+02 -8.8571e+02 -1.0335e+01 --2.7000e+02 -8.5714e+02 -1.1917e+01 --2.7000e+02 -8.2857e+02 -1.3735e+01 --2.7000e+02 -8.0000e+02 -1.5837e+01 --2.7000e+02 -7.7143e+02 -1.8282e+01 --2.7000e+02 -7.4286e+02 -2.1143e+01 --2.7000e+02 -7.1429e+02 -2.4503e+01 --2.7000e+02 -6.8571e+02 -2.8451e+01 --2.7000e+02 -6.5714e+02 -3.3075e+01 --2.7000e+02 -6.2857e+02 -3.8433e+01 --2.7000e+02 -6.0000e+02 -4.4514e+01 --2.7000e+02 -5.7143e+02 -5.1202e+01 --2.7000e+02 -5.4286e+02 -5.8248e+01 --2.7000e+02 -5.1429e+02 -6.5308e+01 --2.7000e+02 -4.8571e+02 -7.2033e+01 --2.7000e+02 -4.5714e+02 -7.8162e+01 --2.7000e+02 -4.2857e+02 -8.3561e+01 --2.7000e+02 -4.0000e+02 -8.8210e+01 --2.7000e+02 -3.7143e+02 -9.2160e+01 --2.7000e+02 -3.4286e+02 -9.5495e+01 --2.7000e+02 -3.1429e+02 -9.8302e+01 --2.7000e+02 -2.8571e+02 -1.0067e+02 --2.7000e+02 -2.5714e+02 -1.0265e+02 --2.7000e+02 -2.2857e+02 -1.0432e+02 --2.7000e+02 -2.0000e+02 -1.0572e+02 --2.7000e+02 -1.7143e+02 -1.0688e+02 --2.7000e+02 -1.4286e+02 -1.0782e+02 --2.7000e+02 -1.1429e+02 -1.0858e+02 --2.7000e+02 -8.5714e+01 -1.0915e+02 --2.7000e+02 -5.7143e+01 -1.0956e+02 --2.7000e+02 -2.8571e+01 -1.0980e+02 --2.7000e+02 0.0000e+00 -1.0988e+02 --2.7000e+02 2.8571e+01 -1.0980e+02 --2.7000e+02 5.7143e+01 -1.0956e+02 --2.7000e+02 8.5714e+01 -1.0915e+02 --2.7000e+02 1.1429e+02 -1.0858e+02 --2.7000e+02 1.4286e+02 -1.0782e+02 --2.7000e+02 1.7143e+02 -1.0688e+02 --2.7000e+02 2.0000e+02 -1.0572e+02 --2.7000e+02 2.2857e+02 -1.0432e+02 --2.7000e+02 2.5714e+02 -1.0265e+02 --2.7000e+02 2.8571e+02 -1.0067e+02 --2.7000e+02 3.1429e+02 -9.8302e+01 --2.7000e+02 3.4286e+02 -9.5495e+01 --2.7000e+02 3.7143e+02 -9.2160e+01 --2.7000e+02 4.0000e+02 -8.8210e+01 --2.7000e+02 4.2857e+02 -8.3561e+01 --2.7000e+02 4.5714e+02 -7.8162e+01 --2.7000e+02 4.8571e+02 -7.2033e+01 --2.7000e+02 5.1429e+02 -6.5308e+01 --2.7000e+02 5.4286e+02 -5.8248e+01 --2.7000e+02 5.7143e+02 -5.1202e+01 --2.7000e+02 6.0000e+02 -4.4514e+01 --2.7000e+02 6.2857e+02 -3.8433e+01 --2.7000e+02 6.5714e+02 -3.3075e+01 --2.7000e+02 6.8571e+02 -2.8451e+01 --2.7000e+02 7.1429e+02 -2.4503e+01 --2.7000e+02 7.4286e+02 -2.1143e+01 --2.7000e+02 7.7143e+02 -1.8282e+01 --2.7000e+02 8.0000e+02 -1.5837e+01 --2.7000e+02 8.2857e+02 -1.3735e+01 --2.7000e+02 8.5714e+02 -1.1917e+01 --2.7000e+02 8.8571e+02 -1.0335e+01 --2.7000e+02 9.1429e+02 -8.9492e+00 --2.7000e+02 9.4286e+02 -7.7279e+00 --2.7000e+02 9.7143e+02 -6.6452e+00 --2.7000e+02 1.0000e+03 -5.6799e+00 --2.7000e+02 1.0286e+03 -4.8149e+00 --2.7000e+02 1.0571e+03 -4.0360e+00 --2.7000e+02 1.0857e+03 -3.3315e+00 --2.7000e+02 1.1143e+03 -2.6915e+00 --2.7000e+02 1.1429e+03 -2.1080e+00 --2.7000e+02 1.1714e+03 -1.5739e+00 --2.7000e+02 1.2000e+03 -1.0834e+00 --2.7000e+02 1.2286e+03 -6.3149e-01 --2.7000e+02 1.2571e+03 -2.1397e-01 --2.7000e+02 1.2857e+03 1.7288e-01 --2.7000e+02 1.3143e+03 5.3225e-01 --2.7000e+02 1.3429e+03 8.6691e-01 --2.7000e+02 1.3714e+03 1.1793e+00 --2.7000e+02 1.4000e+03 1.4714e+00 --2.7000e+02 1.4286e+03 1.7453e+00 --2.7000e+02 1.4571e+03 2.0024e+00 --2.7000e+02 1.4857e+03 2.2444e+00 --2.7000e+02 1.5143e+03 2.4724e+00 --2.7000e+02 1.5429e+03 2.6876e+00 --2.7000e+02 1.5714e+03 2.8910e+00 --2.7000e+02 1.6000e+03 3.0837e+00 --2.7000e+02 1.6286e+03 3.2663e+00 --2.7000e+02 1.6571e+03 3.4397e+00 --2.7000e+02 1.6857e+03 3.6045e+00 --2.7000e+02 1.7143e+03 3.7613e+00 --2.7000e+02 1.7429e+03 3.9108e+00 --2.7000e+02 1.7714e+03 4.0533e+00 --2.7000e+02 1.8000e+03 4.1894e+00 --2.7000e+02 1.8286e+03 4.3195e+00 --2.7000e+02 1.8571e+03 4.4440e+00 --2.7000e+02 1.8857e+03 4.5632e+00 --2.7000e+02 1.9143e+03 4.6774e+00 --2.7000e+02 1.9429e+03 4.7870e+00 --2.7000e+02 1.9714e+03 4.8923e+00 --2.7000e+02 2.0000e+03 4.9934e+00 --2.4000e+02 -2.0000e+03 4.9801e+00 --2.4000e+02 -1.9714e+03 4.8782e+00 --2.4000e+02 -1.9429e+03 4.7722e+00 --2.4000e+02 -1.9143e+03 4.6617e+00 --2.4000e+02 -1.8857e+03 4.5466e+00 --2.4000e+02 -1.8571e+03 4.4264e+00 --2.4000e+02 -1.8286e+03 4.3008e+00 --2.4000e+02 -1.8000e+03 4.1696e+00 --2.4000e+02 -1.7714e+03 4.0322e+00 --2.4000e+02 -1.7429e+03 3.8882e+00 --2.4000e+02 -1.7143e+03 3.7373e+00 --2.4000e+02 -1.6857e+03 3.5788e+00 --2.4000e+02 -1.6571e+03 3.4122e+00 --2.4000e+02 -1.6286e+03 3.2369e+00 --2.4000e+02 -1.6000e+03 3.0521e+00 --2.4000e+02 -1.5714e+03 2.8571e+00 --2.4000e+02 -1.5429e+03 2.6510e+00 --2.4000e+02 -1.5143e+03 2.4329e+00 --2.4000e+02 -1.4857e+03 2.2018e+00 --2.4000e+02 -1.4571e+03 1.9563e+00 --2.4000e+02 -1.4286e+03 1.6952e+00 --2.4000e+02 -1.4000e+03 1.4169e+00 --2.4000e+02 -1.3714e+03 1.1198e+00 --2.4000e+02 -1.3429e+03 8.0189e-01 --2.4000e+02 -1.3143e+03 4.6097e-01 --2.4000e+02 -1.2857e+03 9.4502e-02 --2.4000e+02 -1.2571e+03 -3.0042e-01 --2.4000e+02 -1.2286e+03 -7.2715e-01 --2.4000e+02 -1.2000e+03 -1.1896e+00 --2.4000e+02 -1.1714e+03 -1.6922e+00 --2.4000e+02 -1.1429e+03 -2.2404e+00 --2.4000e+02 -1.1143e+03 -2.8403e+00 --2.4000e+02 -1.0857e+03 -3.4995e+00 --2.4000e+02 -1.0571e+03 -4.2265e+00 --2.4000e+02 -1.0286e+03 -5.0321e+00 --2.4000e+02 -1.0000e+03 -5.9288e+00 --2.4000e+02 -9.7143e+02 -6.9322e+00 --2.4000e+02 -9.4286e+02 -8.0611e+00 --2.4000e+02 -9.1429e+02 -9.3385e+00 --2.4000e+02 -8.8571e+02 -1.0793e+01 --2.4000e+02 -8.5714e+02 -1.2460e+01 --2.4000e+02 -8.2857e+02 -1.4384e+01 --2.4000e+02 -8.0000e+02 -1.6618e+01 --2.4000e+02 -7.7143e+02 -1.9230e+01 --2.4000e+02 -7.4286e+02 -2.2300e+01 --2.4000e+02 -7.1429e+02 -2.5920e+01 --2.4000e+02 -6.8571e+02 -3.0187e+01 --2.4000e+02 -6.5714e+02 -3.5188e+01 --2.4000e+02 -6.2857e+02 -4.0964e+01 --2.4000e+02 -6.0000e+02 -4.7468e+01 --2.4000e+02 -5.7143e+02 -5.4518e+01 --2.4000e+02 -5.4286e+02 -6.1797e+01 --2.4000e+02 -5.1429e+02 -6.8923e+01 --2.4000e+02 -4.8571e+02 -7.5553e+01 --2.4000e+02 -4.5714e+02 -8.1476e+01 --2.4000e+02 -4.2857e+02 -8.6613e+01 --2.4000e+02 -4.0000e+02 -9.0991e+01 --2.4000e+02 -3.7143e+02 -9.4689e+01 --2.4000e+02 -3.4286e+02 -9.7801e+01 --2.4000e+02 -3.1429e+02 -1.0042e+02 --2.4000e+02 -2.8571e+02 -1.0263e+02 --2.4000e+02 -2.5714e+02 -1.0449e+02 --2.4000e+02 -2.2857e+02 -1.0606e+02 --2.4000e+02 -2.0000e+02 -1.0738e+02 --2.4000e+02 -1.7143e+02 -1.0847e+02 --2.4000e+02 -1.4286e+02 -1.0938e+02 --2.4000e+02 -1.1429e+02 -1.1010e+02 --2.4000e+02 -8.5714e+01 -1.1066e+02 --2.4000e+02 -5.7143e+01 -1.1105e+02 --2.4000e+02 -2.8571e+01 -1.1128e+02 --2.4000e+02 0.0000e+00 -1.1136e+02 --2.4000e+02 2.8571e+01 -1.1128e+02 --2.4000e+02 5.7143e+01 -1.1105e+02 --2.4000e+02 8.5714e+01 -1.1066e+02 --2.4000e+02 1.1429e+02 -1.1010e+02 --2.4000e+02 1.4286e+02 -1.0938e+02 --2.4000e+02 1.7143e+02 -1.0847e+02 --2.4000e+02 2.0000e+02 -1.0738e+02 --2.4000e+02 2.2857e+02 -1.0606e+02 --2.4000e+02 2.5714e+02 -1.0449e+02 --2.4000e+02 2.8571e+02 -1.0263e+02 --2.4000e+02 3.1429e+02 -1.0042e+02 --2.4000e+02 3.4286e+02 -9.7801e+01 --2.4000e+02 3.7143e+02 -9.4689e+01 --2.4000e+02 4.0000e+02 -9.0991e+01 --2.4000e+02 4.2857e+02 -8.6613e+01 --2.4000e+02 4.5714e+02 -8.1476e+01 --2.4000e+02 4.8571e+02 -7.5553e+01 --2.4000e+02 5.1429e+02 -6.8923e+01 --2.4000e+02 5.4286e+02 -6.1797e+01 --2.4000e+02 5.7143e+02 -5.4518e+01 --2.4000e+02 6.0000e+02 -4.7468e+01 --2.4000e+02 6.2857e+02 -4.0964e+01 --2.4000e+02 6.5714e+02 -3.5188e+01 --2.4000e+02 6.8571e+02 -3.0187e+01 --2.4000e+02 7.1429e+02 -2.5920e+01 --2.4000e+02 7.4286e+02 -2.2300e+01 --2.4000e+02 7.7143e+02 -1.9230e+01 --2.4000e+02 8.0000e+02 -1.6618e+01 --2.4000e+02 8.2857e+02 -1.4384e+01 --2.4000e+02 8.5714e+02 -1.2460e+01 --2.4000e+02 8.8571e+02 -1.0793e+01 --2.4000e+02 9.1429e+02 -9.3385e+00 --2.4000e+02 9.4286e+02 -8.0611e+00 --2.4000e+02 9.7143e+02 -6.9322e+00 --2.4000e+02 1.0000e+03 -5.9288e+00 --2.4000e+02 1.0286e+03 -5.0321e+00 --2.4000e+02 1.0571e+03 -4.2265e+00 --2.4000e+02 1.0857e+03 -3.4995e+00 --2.4000e+02 1.1143e+03 -2.8403e+00 --2.4000e+02 1.1429e+03 -2.2404e+00 --2.4000e+02 1.1714e+03 -1.6922e+00 --2.4000e+02 1.2000e+03 -1.1896e+00 --2.4000e+02 1.2286e+03 -7.2715e-01 --2.4000e+02 1.2571e+03 -3.0042e-01 --2.4000e+02 1.2857e+03 9.4502e-02 --2.4000e+02 1.3143e+03 4.6097e-01 --2.4000e+02 1.3429e+03 8.0189e-01 --2.4000e+02 1.3714e+03 1.1198e+00 --2.4000e+02 1.4000e+03 1.4169e+00 --2.4000e+02 1.4286e+03 1.6952e+00 --2.4000e+02 1.4571e+03 1.9563e+00 --2.4000e+02 1.4857e+03 2.2018e+00 --2.4000e+02 1.5143e+03 2.4329e+00 --2.4000e+02 1.5429e+03 2.6510e+00 --2.4000e+02 1.5714e+03 2.8571e+00 --2.4000e+02 1.6000e+03 3.0521e+00 --2.4000e+02 1.6286e+03 3.2369e+00 --2.4000e+02 1.6571e+03 3.4122e+00 --2.4000e+02 1.6857e+03 3.5788e+00 --2.4000e+02 1.7143e+03 3.7373e+00 --2.4000e+02 1.7429e+03 3.8882e+00 --2.4000e+02 1.7714e+03 4.0322e+00 --2.4000e+02 1.8000e+03 4.1696e+00 --2.4000e+02 1.8286e+03 4.3008e+00 --2.4000e+02 1.8571e+03 4.4264e+00 --2.4000e+02 1.8857e+03 4.5466e+00 --2.4000e+02 1.9143e+03 4.6617e+00 --2.4000e+02 1.9429e+03 4.7722e+00 --2.4000e+02 1.9714e+03 4.8782e+00 --2.4000e+02 2.0000e+03 4.9801e+00 --2.1000e+02 -2.0000e+03 4.9682e+00 --2.1000e+02 -1.9714e+03 4.8657e+00 --2.1000e+02 -1.9429e+03 4.7590e+00 --2.1000e+02 -1.9143e+03 4.6478e+00 --2.1000e+02 -1.8857e+03 4.5318e+00 --2.1000e+02 -1.8571e+03 4.4107e+00 --2.1000e+02 -1.8286e+03 4.2842e+00 --2.1000e+02 -1.8000e+03 4.1519e+00 --2.1000e+02 -1.7714e+03 4.0133e+00 --2.1000e+02 -1.7429e+03 3.8682e+00 --2.1000e+02 -1.7143e+03 3.7159e+00 --2.1000e+02 -1.6857e+03 3.5559e+00 --2.1000e+02 -1.6571e+03 3.3877e+00 --2.1000e+02 -1.6286e+03 3.2106e+00 --2.1000e+02 -1.6000e+03 3.0239e+00 --2.1000e+02 -1.5714e+03 2.8268e+00 --2.1000e+02 -1.5429e+03 2.6184e+00 --2.1000e+02 -1.5143e+03 2.3977e+00 --2.1000e+02 -1.4857e+03 2.1636e+00 --2.1000e+02 -1.4571e+03 1.9150e+00 --2.1000e+02 -1.4286e+03 1.6503e+00 --2.1000e+02 -1.4000e+03 1.3681e+00 --2.1000e+02 -1.3714e+03 1.0665e+00 --2.1000e+02 -1.3429e+03 7.4354e-01 --2.1000e+02 -1.3143e+03 3.9693e-01 --2.1000e+02 -1.2857e+03 2.4017e-02 --2.1000e+02 -1.2571e+03 -3.7824e-01 --2.1000e+02 -1.2286e+03 -8.1336e-01 --2.1000e+02 -1.2000e+03 -1.2854e+00 --2.1000e+02 -1.1714e+03 -1.7992e+00 --2.1000e+02 -1.1429e+03 -2.3603e+00 --2.1000e+02 -1.1143e+03 -2.9752e+00 --2.1000e+02 -1.0857e+03 -3.6520e+00 --2.1000e+02 -1.0571e+03 -4.3998e+00 --2.1000e+02 -1.0286e+03 -5.2301e+00 --2.1000e+02 -1.0000e+03 -6.1564e+00 --2.1000e+02 -9.7143e+02 -7.1954e+00 --2.1000e+02 -9.4286e+02 -8.3674e+00 --2.1000e+02 -9.1429e+02 -9.6976e+00 --2.1000e+02 -8.8571e+02 -1.1217e+01 --2.1000e+02 -8.5714e+02 -1.2965e+01 --2.1000e+02 -8.2857e+02 -1.4990e+01 --2.1000e+02 -8.0000e+02 -1.7351e+01 --2.1000e+02 -7.7143e+02 -2.0123e+01 --2.1000e+02 -7.4286e+02 -2.3394e+01 --2.1000e+02 -7.1429e+02 -2.7264e+01 --2.1000e+02 -6.8571e+02 -3.1836e+01 --2.1000e+02 -6.5714e+02 -3.7191e+01 --2.1000e+02 -6.2857e+02 -4.3351e+01 --2.1000e+02 -6.0000e+02 -5.0220e+01 --2.1000e+02 -5.7143e+02 -5.7553e+01 --2.1000e+02 -5.4286e+02 -6.4976e+01 --2.1000e+02 -5.1429e+02 -7.2086e+01 --2.1000e+02 -4.8571e+02 -7.8573e+01 --2.1000e+02 -4.5714e+02 -8.4273e+01 --2.1000e+02 -4.2857e+02 -8.9162e+01 --2.1000e+02 -4.0000e+02 -9.3298e+01 --2.1000e+02 -3.7143e+02 -9.6779e+01 --2.1000e+02 -3.4286e+02 -9.9706e+01 --2.1000e+02 -3.1429e+02 -1.0217e+02 --2.1000e+02 -2.8571e+02 -1.0425e+02 --2.1000e+02 -2.5714e+02 -1.0602e+02 --2.1000e+02 -2.2857e+02 -1.0751e+02 --2.1000e+02 -2.0000e+02 -1.0877e+02 --2.1000e+02 -1.7143e+02 -1.0982e+02 --2.1000e+02 -1.4286e+02 -1.1070e+02 --2.1000e+02 -1.1429e+02 -1.1140e+02 --2.1000e+02 -8.5714e+01 -1.1195e+02 --2.1000e+02 -5.7143e+01 -1.1234e+02 --2.1000e+02 -2.8571e+01 -1.1257e+02 --2.1000e+02 0.0000e+00 -1.1265e+02 --2.1000e+02 2.8571e+01 -1.1257e+02 --2.1000e+02 5.7143e+01 -1.1234e+02 --2.1000e+02 8.5714e+01 -1.1195e+02 --2.1000e+02 1.1429e+02 -1.1140e+02 --2.1000e+02 1.4286e+02 -1.1070e+02 --2.1000e+02 1.7143e+02 -1.0982e+02 --2.1000e+02 2.0000e+02 -1.0877e+02 --2.1000e+02 2.2857e+02 -1.0751e+02 --2.1000e+02 2.5714e+02 -1.0602e+02 --2.1000e+02 2.8571e+02 -1.0425e+02 --2.1000e+02 3.1429e+02 -1.0217e+02 --2.1000e+02 3.4286e+02 -9.9706e+01 --2.1000e+02 3.7143e+02 -9.6779e+01 --2.1000e+02 4.0000e+02 -9.3298e+01 --2.1000e+02 4.2857e+02 -8.9162e+01 --2.1000e+02 4.5714e+02 -8.4273e+01 --2.1000e+02 4.8571e+02 -7.8573e+01 --2.1000e+02 5.1429e+02 -7.2086e+01 --2.1000e+02 5.4286e+02 -6.4976e+01 --2.1000e+02 5.7143e+02 -5.7553e+01 --2.1000e+02 6.0000e+02 -5.0220e+01 --2.1000e+02 6.2857e+02 -4.3351e+01 --2.1000e+02 6.5714e+02 -3.7191e+01 --2.1000e+02 6.8571e+02 -3.1836e+01 --2.1000e+02 7.1429e+02 -2.7264e+01 --2.1000e+02 7.4286e+02 -2.3394e+01 --2.1000e+02 7.7143e+02 -2.0123e+01 --2.1000e+02 8.0000e+02 -1.7351e+01 --2.1000e+02 8.2857e+02 -1.4990e+01 --2.1000e+02 8.5714e+02 -1.2965e+01 --2.1000e+02 8.8571e+02 -1.1217e+01 --2.1000e+02 9.1429e+02 -9.6976e+00 --2.1000e+02 9.4286e+02 -8.3674e+00 --2.1000e+02 9.7143e+02 -7.1954e+00 --2.1000e+02 1.0000e+03 -6.1564e+00 --2.1000e+02 1.0286e+03 -5.2301e+00 --2.1000e+02 1.0571e+03 -4.3998e+00 --2.1000e+02 1.0857e+03 -3.6520e+00 --2.1000e+02 1.1143e+03 -2.9752e+00 --2.1000e+02 1.1429e+03 -2.3603e+00 --2.1000e+02 1.1714e+03 -1.7992e+00 --2.1000e+02 1.2000e+03 -1.2854e+00 --2.1000e+02 1.2286e+03 -8.1336e-01 --2.1000e+02 1.2571e+03 -3.7824e-01 --2.1000e+02 1.2857e+03 2.4017e-02 --2.1000e+02 1.3143e+03 3.9693e-01 --2.1000e+02 1.3429e+03 7.4354e-01 --2.1000e+02 1.3714e+03 1.0665e+00 --2.1000e+02 1.4000e+03 1.3681e+00 --2.1000e+02 1.4286e+03 1.6503e+00 --2.1000e+02 1.4571e+03 1.9150e+00 --2.1000e+02 1.4857e+03 2.1636e+00 --2.1000e+02 1.5143e+03 2.3977e+00 --2.1000e+02 1.5429e+03 2.6184e+00 --2.1000e+02 1.5714e+03 2.8268e+00 --2.1000e+02 1.6000e+03 3.0239e+00 --2.1000e+02 1.6286e+03 3.2106e+00 --2.1000e+02 1.6571e+03 3.3877e+00 --2.1000e+02 1.6857e+03 3.5559e+00 --2.1000e+02 1.7143e+03 3.7159e+00 --2.1000e+02 1.7429e+03 3.8682e+00 --2.1000e+02 1.7714e+03 4.0133e+00 --2.1000e+02 1.8000e+03 4.1519e+00 --2.1000e+02 1.8286e+03 4.2842e+00 --2.1000e+02 1.8571e+03 4.4107e+00 --2.1000e+02 1.8857e+03 4.5318e+00 --2.1000e+02 1.9143e+03 4.6478e+00 --2.1000e+02 1.9429e+03 4.7590e+00 --2.1000e+02 1.9714e+03 4.8657e+00 --2.1000e+02 2.0000e+03 4.9682e+00 --1.8000e+02 -2.0000e+03 4.9579e+00 --1.8000e+02 -1.9714e+03 4.8548e+00 --1.8000e+02 -1.9429e+03 4.7475e+00 --1.8000e+02 -1.9143e+03 4.6356e+00 --1.8000e+02 -1.8857e+03 4.5189e+00 --1.8000e+02 -1.8571e+03 4.3970e+00 --1.8000e+02 -1.8286e+03 4.2696e+00 --1.8000e+02 -1.8000e+03 4.1364e+00 --1.8000e+02 -1.7714e+03 3.9969e+00 --1.8000e+02 -1.7429e+03 3.8507e+00 --1.8000e+02 -1.7143e+03 3.6972e+00 --1.8000e+02 -1.6857e+03 3.5359e+00 --1.8000e+02 -1.6571e+03 3.3663e+00 --1.8000e+02 -1.6286e+03 3.1877e+00 --1.8000e+02 -1.6000e+03 2.9993e+00 --1.8000e+02 -1.5714e+03 2.8003e+00 --1.8000e+02 -1.5429e+03 2.5898e+00 --1.8000e+02 -1.5143e+03 2.3668e+00 --1.8000e+02 -1.4857e+03 2.1302e+00 --1.8000e+02 -1.4571e+03 1.8787e+00 --1.8000e+02 -1.4286e+03 1.6109e+00 --1.8000e+02 -1.4000e+03 1.3251e+00 --1.8000e+02 -1.3714e+03 1.0196e+00 --1.8000e+02 -1.3429e+03 6.9220e-01 --1.8000e+02 -1.3143e+03 3.4054e-01 --1.8000e+02 -1.2857e+03 -3.8104e-02 --1.8000e+02 -1.2571e+03 -4.4690e-01 --1.8000e+02 -1.2286e+03 -8.8950e-01 --1.8000e+02 -1.2000e+03 -1.3702e+00 --1.8000e+02 -1.1714e+03 -1.8939e+00 --1.8000e+02 -1.1429e+03 -2.4665e+00 --1.8000e+02 -1.1143e+03 -3.0950e+00 --1.8000e+02 -1.0857e+03 -3.7876e+00 --1.8000e+02 -1.0571e+03 -4.5542e+00 --1.8000e+02 -1.0286e+03 -5.4069e+00 --1.8000e+02 -1.0000e+03 -6.3600e+00 --1.8000e+02 -9.7143e+02 -7.4313e+00 --1.8000e+02 -9.4286e+02 -8.6428e+00 --1.8000e+02 -9.1429e+02 -1.0021e+01 --1.8000e+02 -8.8571e+02 -1.1601e+01 --1.8000e+02 -8.5714e+02 -1.3423e+01 --1.8000e+02 -8.2857e+02 -1.5542e+01 --1.8000e+02 -8.0000e+02 -1.8021e+01 --1.8000e+02 -7.7143e+02 -2.0943e+01 --1.8000e+02 -7.4286e+02 -2.4402e+01 --1.8000e+02 -7.1429e+02 -2.8506e+01 --1.8000e+02 -6.8571e+02 -3.3360e+01 --1.8000e+02 -6.5714e+02 -3.9039e+01 --1.8000e+02 -6.2857e+02 -4.5536e+01 --1.8000e+02 -6.0000e+02 -5.2708e+01 --1.8000e+02 -5.7143e+02 -6.0248e+01 --1.8000e+02 -5.4286e+02 -6.7742e+01 --1.8000e+02 -5.1429e+02 -7.4786e+01 --1.8000e+02 -4.8571e+02 -8.1106e+01 --1.8000e+02 -4.5714e+02 -8.6592e+01 --1.8000e+02 -4.2857e+02 -9.1259e+01 --1.8000e+02 -4.0000e+02 -9.5189e+01 --1.8000e+02 -3.7143e+02 -9.8490e+01 --1.8000e+02 -3.4286e+02 -1.0127e+02 --1.8000e+02 -3.1429e+02 -1.0361e+02 --1.8000e+02 -2.8571e+02 -1.0559e+02 --1.8000e+02 -2.5714e+02 -1.0728e+02 --1.8000e+02 -2.2857e+02 -1.0871e+02 --1.8000e+02 -2.0000e+02 -1.0993e+02 --1.8000e+02 -1.7143e+02 -1.1096e+02 --1.8000e+02 -1.4286e+02 -1.1182e+02 --1.8000e+02 -1.1429e+02 -1.1252e+02 --1.8000e+02 -8.5714e+01 -1.1307e+02 --1.8000e+02 -5.7143e+01 -1.1346e+02 --1.8000e+02 -2.8571e+01 -1.1370e+02 --1.8000e+02 0.0000e+00 -1.1378e+02 --1.8000e+02 2.8571e+01 -1.1370e+02 --1.8000e+02 5.7143e+01 -1.1346e+02 --1.8000e+02 8.5714e+01 -1.1307e+02 --1.8000e+02 1.1429e+02 -1.1252e+02 --1.8000e+02 1.4286e+02 -1.1182e+02 --1.8000e+02 1.7143e+02 -1.1096e+02 --1.8000e+02 2.0000e+02 -1.0993e+02 --1.8000e+02 2.2857e+02 -1.0871e+02 --1.8000e+02 2.5714e+02 -1.0728e+02 --1.8000e+02 2.8571e+02 -1.0559e+02 --1.8000e+02 3.1429e+02 -1.0361e+02 --1.8000e+02 3.4286e+02 -1.0127e+02 --1.8000e+02 3.7143e+02 -9.8490e+01 --1.8000e+02 4.0000e+02 -9.5189e+01 --1.8000e+02 4.2857e+02 -9.1259e+01 --1.8000e+02 4.5714e+02 -8.6592e+01 --1.8000e+02 4.8571e+02 -8.1106e+01 --1.8000e+02 5.1429e+02 -7.4786e+01 --1.8000e+02 5.4286e+02 -6.7742e+01 --1.8000e+02 5.7143e+02 -6.0248e+01 --1.8000e+02 6.0000e+02 -5.2708e+01 --1.8000e+02 6.2857e+02 -4.5536e+01 --1.8000e+02 6.5714e+02 -3.9039e+01 --1.8000e+02 6.8571e+02 -3.3360e+01 --1.8000e+02 7.1429e+02 -2.8506e+01 --1.8000e+02 7.4286e+02 -2.4402e+01 --1.8000e+02 7.7143e+02 -2.0943e+01 --1.8000e+02 8.0000e+02 -1.8021e+01 --1.8000e+02 8.2857e+02 -1.5542e+01 --1.8000e+02 8.5714e+02 -1.3423e+01 --1.8000e+02 8.8571e+02 -1.1601e+01 --1.8000e+02 9.1429e+02 -1.0021e+01 --1.8000e+02 9.4286e+02 -8.6428e+00 --1.8000e+02 9.7143e+02 -7.4313e+00 --1.8000e+02 1.0000e+03 -6.3600e+00 --1.8000e+02 1.0286e+03 -5.4069e+00 --1.8000e+02 1.0571e+03 -4.5542e+00 --1.8000e+02 1.0857e+03 -3.7876e+00 --1.8000e+02 1.1143e+03 -3.0950e+00 --1.8000e+02 1.1429e+03 -2.4665e+00 --1.8000e+02 1.1714e+03 -1.8939e+00 --1.8000e+02 1.2000e+03 -1.3702e+00 --1.8000e+02 1.2286e+03 -8.8950e-01 --1.8000e+02 1.2571e+03 -4.4690e-01 --1.8000e+02 1.2857e+03 -3.8104e-02 --1.8000e+02 1.3143e+03 3.4054e-01 --1.8000e+02 1.3429e+03 6.9220e-01 --1.8000e+02 1.3714e+03 1.0196e+00 --1.8000e+02 1.4000e+03 1.3251e+00 --1.8000e+02 1.4286e+03 1.6109e+00 --1.8000e+02 1.4571e+03 1.8787e+00 --1.8000e+02 1.4857e+03 2.1302e+00 --1.8000e+02 1.5143e+03 2.3668e+00 --1.8000e+02 1.5429e+03 2.5898e+00 --1.8000e+02 1.5714e+03 2.8003e+00 --1.8000e+02 1.6000e+03 2.9993e+00 --1.8000e+02 1.6286e+03 3.1877e+00 --1.8000e+02 1.6571e+03 3.3663e+00 --1.8000e+02 1.6857e+03 3.5359e+00 --1.8000e+02 1.7143e+03 3.6972e+00 --1.8000e+02 1.7429e+03 3.8507e+00 --1.8000e+02 1.7714e+03 3.9969e+00 --1.8000e+02 1.8000e+03 4.1364e+00 --1.8000e+02 1.8286e+03 4.2696e+00 --1.8000e+02 1.8571e+03 4.3970e+00 --1.8000e+02 1.8857e+03 4.5189e+00 --1.8000e+02 1.9143e+03 4.6356e+00 --1.8000e+02 1.9429e+03 4.7475e+00 --1.8000e+02 1.9714e+03 4.8548e+00 --1.8000e+02 2.0000e+03 4.9579e+00 --1.5000e+02 -2.0000e+03 4.9491e+00 --1.5000e+02 -1.9714e+03 4.8456e+00 --1.5000e+02 -1.9429e+03 4.7377e+00 --1.5000e+02 -1.9143e+03 4.6252e+00 --1.5000e+02 -1.8857e+03 4.5079e+00 --1.5000e+02 -1.8571e+03 4.3854e+00 --1.5000e+02 -1.8286e+03 4.2573e+00 --1.5000e+02 -1.8000e+03 4.1233e+00 --1.5000e+02 -1.7714e+03 3.9829e+00 --1.5000e+02 -1.7429e+03 3.8357e+00 --1.5000e+02 -1.7143e+03 3.6812e+00 --1.5000e+02 -1.6857e+03 3.5189e+00 --1.5000e+02 -1.6571e+03 3.3481e+00 --1.5000e+02 -1.6286e+03 3.1681e+00 --1.5000e+02 -1.6000e+03 2.9782e+00 --1.5000e+02 -1.5714e+03 2.7776e+00 --1.5000e+02 -1.5429e+03 2.5654e+00 --1.5000e+02 -1.5143e+03 2.3404e+00 --1.5000e+02 -1.4857e+03 2.1017e+00 --1.5000e+02 -1.4571e+03 1.8477e+00 --1.5000e+02 -1.4286e+03 1.5772e+00 --1.5000e+02 -1.4000e+03 1.2884e+00 --1.5000e+02 -1.3714e+03 9.7945e-01 --1.5000e+02 -1.3429e+03 6.4819e-01 --1.5000e+02 -1.3143e+03 2.9217e-01 --1.5000e+02 -1.2857e+03 -9.1437e-02 --1.5000e+02 -1.2571e+03 -5.0589e-01 --1.5000e+02 -1.2286e+03 -9.5498e-01 --1.5000e+02 -1.2000e+03 -1.4431e+00 --1.5000e+02 -1.1714e+03 -1.9755e+00 --1.5000e+02 -1.1429e+03 -2.5582e+00 --1.5000e+02 -1.1143e+03 -3.1985e+00 --1.5000e+02 -1.0857e+03 -3.9049e+00 --1.5000e+02 -1.0571e+03 -4.6880e+00 --1.5000e+02 -1.0286e+03 -5.5603e+00 --1.5000e+02 -1.0000e+03 -6.5370e+00 --1.5000e+02 -9.7143e+02 -7.6370e+00 --1.5000e+02 -9.4286e+02 -8.8833e+00 --1.5000e+02 -9.1429e+02 -1.0305e+01 --1.5000e+02 -8.8571e+02 -1.1938e+01 --1.5000e+02 -8.5714e+02 -1.3827e+01 --1.5000e+02 -8.2857e+02 -1.6030e+01 --1.5000e+02 -8.0000e+02 -1.8616e+01 --1.5000e+02 -7.7143e+02 -2.1672e+01 --1.5000e+02 -7.4286e+02 -2.5301e+01 --1.5000e+02 -7.1429e+02 -2.9616e+01 --1.5000e+02 -6.8571e+02 -3.4723e+01 --1.5000e+02 -6.5714e+02 -4.0685e+01 --1.5000e+02 -6.2857e+02 -4.7468e+01 --1.5000e+02 -6.0000e+02 -5.4880e+01 --1.5000e+02 -5.7143e+02 -6.2563e+01 --1.5000e+02 -5.4286e+02 -7.0074e+01 --1.5000e+02 -5.1429e+02 -7.7025e+01 --1.5000e+02 -4.8571e+02 -8.3182e+01 --1.5000e+02 -4.5714e+02 -8.8476e+01 --1.5000e+02 -4.2857e+02 -9.2953e+01 --1.5000e+02 -4.0000e+02 -9.6713e+01 --1.5000e+02 -3.7143e+02 -9.9869e+01 --1.5000e+02 -3.4286e+02 -1.0252e+02 --1.5000e+02 -3.1429e+02 -1.0477e+02 --1.5000e+02 -2.8571e+02 -1.0668e+02 --1.5000e+02 -2.5714e+02 -1.0831e+02 --1.5000e+02 -2.2857e+02 -1.0970e+02 --1.5000e+02 -2.0000e+02 -1.1089e+02 --1.5000e+02 -1.7143e+02 -1.1191e+02 --1.5000e+02 -1.4286e+02 -1.1276e+02 --1.5000e+02 -1.1429e+02 -1.1347e+02 --1.5000e+02 -8.5714e+01 -1.1403e+02 --1.5000e+02 -5.7143e+01 -1.1444e+02 --1.5000e+02 -2.8571e+01 -1.1469e+02 --1.5000e+02 0.0000e+00 -1.1478e+02 --1.5000e+02 2.8571e+01 -1.1469e+02 --1.5000e+02 5.7143e+01 -1.1444e+02 --1.5000e+02 8.5714e+01 -1.1403e+02 --1.5000e+02 1.1429e+02 -1.1347e+02 --1.5000e+02 1.4286e+02 -1.1276e+02 --1.5000e+02 1.7143e+02 -1.1191e+02 --1.5000e+02 2.0000e+02 -1.1089e+02 --1.5000e+02 2.2857e+02 -1.0970e+02 --1.5000e+02 2.5714e+02 -1.0831e+02 --1.5000e+02 2.8571e+02 -1.0668e+02 --1.5000e+02 3.1429e+02 -1.0477e+02 --1.5000e+02 3.4286e+02 -1.0252e+02 --1.5000e+02 3.7143e+02 -9.9869e+01 --1.5000e+02 4.0000e+02 -9.6713e+01 --1.5000e+02 4.2857e+02 -9.2953e+01 --1.5000e+02 4.5714e+02 -8.8476e+01 --1.5000e+02 4.8571e+02 -8.3182e+01 --1.5000e+02 5.1429e+02 -7.7025e+01 --1.5000e+02 5.4286e+02 -7.0074e+01 --1.5000e+02 5.7143e+02 -6.2563e+01 --1.5000e+02 6.0000e+02 -5.4880e+01 --1.5000e+02 6.2857e+02 -4.7468e+01 --1.5000e+02 6.5714e+02 -4.0685e+01 --1.5000e+02 6.8571e+02 -3.4723e+01 --1.5000e+02 7.1429e+02 -2.9616e+01 --1.5000e+02 7.4286e+02 -2.5301e+01 --1.5000e+02 7.7143e+02 -2.1672e+01 --1.5000e+02 8.0000e+02 -1.8616e+01 --1.5000e+02 8.2857e+02 -1.6030e+01 --1.5000e+02 8.5714e+02 -1.3827e+01 --1.5000e+02 8.8571e+02 -1.1938e+01 --1.5000e+02 9.1429e+02 -1.0305e+01 --1.5000e+02 9.4286e+02 -8.8833e+00 --1.5000e+02 9.7143e+02 -7.6370e+00 --1.5000e+02 1.0000e+03 -6.5370e+00 --1.5000e+02 1.0286e+03 -5.5603e+00 --1.5000e+02 1.0571e+03 -4.6880e+00 --1.5000e+02 1.0857e+03 -3.9049e+00 --1.5000e+02 1.1143e+03 -3.1985e+00 --1.5000e+02 1.1429e+03 -2.5582e+00 --1.5000e+02 1.1714e+03 -1.9755e+00 --1.5000e+02 1.2000e+03 -1.4431e+00 --1.5000e+02 1.2286e+03 -9.5498e-01 --1.5000e+02 1.2571e+03 -5.0589e-01 --1.5000e+02 1.2857e+03 -9.1437e-02 --1.5000e+02 1.3143e+03 2.9217e-01 --1.5000e+02 1.3429e+03 6.4819e-01 --1.5000e+02 1.3714e+03 9.7945e-01 --1.5000e+02 1.4000e+03 1.2884e+00 --1.5000e+02 1.4286e+03 1.5772e+00 --1.5000e+02 1.4571e+03 1.8477e+00 --1.5000e+02 1.4857e+03 2.1017e+00 --1.5000e+02 1.5143e+03 2.3404e+00 --1.5000e+02 1.5429e+03 2.5654e+00 --1.5000e+02 1.5714e+03 2.7776e+00 --1.5000e+02 1.6000e+03 2.9782e+00 --1.5000e+02 1.6286e+03 3.1681e+00 --1.5000e+02 1.6571e+03 3.3481e+00 --1.5000e+02 1.6857e+03 3.5189e+00 --1.5000e+02 1.7143e+03 3.6812e+00 --1.5000e+02 1.7429e+03 3.8357e+00 --1.5000e+02 1.7714e+03 3.9829e+00 --1.5000e+02 1.8000e+03 4.1233e+00 --1.5000e+02 1.8286e+03 4.2573e+00 --1.5000e+02 1.8571e+03 4.3854e+00 --1.5000e+02 1.8857e+03 4.5079e+00 --1.5000e+02 1.9143e+03 4.6252e+00 --1.5000e+02 1.9429e+03 4.7377e+00 --1.5000e+02 1.9714e+03 4.8456e+00 --1.5000e+02 2.0000e+03 4.9491e+00 --1.2000e+02 -2.0000e+03 4.9419e+00 --1.2000e+02 -1.9714e+03 4.8379e+00 --1.2000e+02 -1.9429e+03 4.7296e+00 --1.2000e+02 -1.9143e+03 4.6167e+00 --1.2000e+02 -1.8857e+03 4.4989e+00 --1.2000e+02 -1.8571e+03 4.3758e+00 --1.2000e+02 -1.8286e+03 4.2471e+00 --1.2000e+02 -1.8000e+03 4.1124e+00 --1.2000e+02 -1.7714e+03 3.9714e+00 --1.2000e+02 -1.7429e+03 3.8234e+00 --1.2000e+02 -1.7143e+03 3.6681e+00 --1.2000e+02 -1.6857e+03 3.5049e+00 --1.2000e+02 -1.6571e+03 3.3330e+00 --1.2000e+02 -1.6286e+03 3.1520e+00 --1.2000e+02 -1.6000e+03 2.9609e+00 --1.2000e+02 -1.5714e+03 2.7590e+00 --1.2000e+02 -1.5429e+03 2.5452e+00 --1.2000e+02 -1.5143e+03 2.3187e+00 --1.2000e+02 -1.4857e+03 2.0781e+00 --1.2000e+02 -1.4571e+03 1.8222e+00 --1.2000e+02 -1.4286e+03 1.5494e+00 --1.2000e+02 -1.4000e+03 1.2580e+00 --1.2000e+02 -1.3714e+03 9.4624e-01 --1.2000e+02 -1.3429e+03 6.1178e-01 --1.2000e+02 -1.3143e+03 2.5213e-01 --1.2000e+02 -1.2857e+03 -1.3561e-01 --1.2000e+02 -1.2571e+03 -5.5478e-01 --1.2000e+02 -1.2286e+03 -1.0093e+00 --1.2000e+02 -1.2000e+03 -1.5037e+00 --1.2000e+02 -1.1714e+03 -2.0433e+00 --1.2000e+02 -1.1429e+03 -2.6344e+00 --1.2000e+02 -1.1143e+03 -3.2846e+00 --1.2000e+02 -1.0857e+03 -4.0028e+00 --1.2000e+02 -1.0571e+03 -4.7997e+00 --1.2000e+02 -1.0286e+03 -5.6885e+00 --1.2000e+02 -1.0000e+03 -6.6852e+00 --1.2000e+02 -9.7143e+02 -7.8095e+00 --1.2000e+02 -9.4286e+02 -9.0855e+00 --1.2000e+02 -9.1429e+02 -1.0544e+01 --1.2000e+02 -8.8571e+02 -1.2222e+01 --1.2000e+02 -8.5714e+02 -1.4169e+01 --1.2000e+02 -8.2857e+02 -1.6444e+01 --1.2000e+02 -8.0000e+02 -1.9122e+01 --1.2000e+02 -7.7143e+02 -2.2295e+01 --1.2000e+02 -7.4286e+02 -2.6071e+01 --1.2000e+02 -7.1429e+02 -3.0568e+01 --1.2000e+02 -6.8571e+02 -3.5889e+01 --1.2000e+02 -6.5714e+02 -4.2089e+01 --1.2000e+02 -6.2857e+02 -4.9103e+01 --1.2000e+02 -6.0000e+02 -5.6697e+01 --1.2000e+02 -5.7143e+02 -6.4471e+01 --1.2000e+02 -5.4286e+02 -7.1969e+01 --1.2000e+02 -5.1429e+02 -7.8821e+01 --1.2000e+02 -4.8571e+02 -8.4829e+01 --1.2000e+02 -4.5714e+02 -8.9961e+01 --1.2000e+02 -4.2857e+02 -9.4285e+01 --1.2000e+02 -4.0000e+02 -9.7910e+01 --1.2000e+02 -3.7143e+02 -1.0095e+02 --1.2000e+02 -3.4286e+02 -1.0352e+02 --1.2000e+02 -3.1429e+02 -1.0569e+02 --1.2000e+02 -2.8571e+02 -1.0754e+02 --1.2000e+02 -2.5714e+02 -1.0913e+02 --1.2000e+02 -2.2857e+02 -1.1049e+02 --1.2000e+02 -2.0000e+02 -1.1167e+02 --1.2000e+02 -1.7143e+02 -1.1268e+02 --1.2000e+02 -1.4286e+02 -1.1354e+02 --1.2000e+02 -1.1429e+02 -1.1427e+02 --1.2000e+02 -8.5714e+01 -1.1486e+02 --1.2000e+02 -5.7143e+01 -1.1529e+02 --1.2000e+02 -2.8571e+01 -1.1557e+02 --1.2000e+02 0.0000e+00 -1.1566e+02 --1.2000e+02 2.8571e+01 -1.1557e+02 --1.2000e+02 5.7143e+01 -1.1529e+02 --1.2000e+02 8.5714e+01 -1.1486e+02 --1.2000e+02 1.1429e+02 -1.1427e+02 --1.2000e+02 1.4286e+02 -1.1354e+02 --1.2000e+02 1.7143e+02 -1.1268e+02 --1.2000e+02 2.0000e+02 -1.1167e+02 --1.2000e+02 2.2857e+02 -1.1049e+02 --1.2000e+02 2.5714e+02 -1.0913e+02 --1.2000e+02 2.8571e+02 -1.0754e+02 --1.2000e+02 3.1429e+02 -1.0569e+02 --1.2000e+02 3.4286e+02 -1.0352e+02 --1.2000e+02 3.7143e+02 -1.0095e+02 --1.2000e+02 4.0000e+02 -9.7910e+01 --1.2000e+02 4.2857e+02 -9.4285e+01 --1.2000e+02 4.5714e+02 -8.9961e+01 --1.2000e+02 4.8571e+02 -8.4829e+01 --1.2000e+02 5.1429e+02 -7.8821e+01 --1.2000e+02 5.4286e+02 -7.1969e+01 --1.2000e+02 5.7143e+02 -6.4471e+01 --1.2000e+02 6.0000e+02 -5.6697e+01 --1.2000e+02 6.2857e+02 -4.9103e+01 --1.2000e+02 6.5714e+02 -4.2089e+01 --1.2000e+02 6.8571e+02 -3.5889e+01 --1.2000e+02 7.1429e+02 -3.0568e+01 --1.2000e+02 7.4286e+02 -2.6071e+01 --1.2000e+02 7.7143e+02 -2.2295e+01 --1.2000e+02 8.0000e+02 -1.9122e+01 --1.2000e+02 8.2857e+02 -1.6444e+01 --1.2000e+02 8.5714e+02 -1.4169e+01 --1.2000e+02 8.8571e+02 -1.2222e+01 --1.2000e+02 9.1429e+02 -1.0544e+01 --1.2000e+02 9.4286e+02 -9.0855e+00 --1.2000e+02 9.7143e+02 -7.8095e+00 --1.2000e+02 1.0000e+03 -6.6852e+00 --1.2000e+02 1.0286e+03 -5.6885e+00 --1.2000e+02 1.0571e+03 -4.7997e+00 --1.2000e+02 1.0857e+03 -4.0028e+00 --1.2000e+02 1.1143e+03 -3.2846e+00 --1.2000e+02 1.1429e+03 -2.6344e+00 --1.2000e+02 1.1714e+03 -2.0433e+00 --1.2000e+02 1.2000e+03 -1.5037e+00 --1.2000e+02 1.2286e+03 -1.0093e+00 --1.2000e+02 1.2571e+03 -5.5478e-01 --1.2000e+02 1.2857e+03 -1.3561e-01 --1.2000e+02 1.3143e+03 2.5213e-01 --1.2000e+02 1.3429e+03 6.1178e-01 --1.2000e+02 1.3714e+03 9.4624e-01 --1.2000e+02 1.4000e+03 1.2580e+00 --1.2000e+02 1.4286e+03 1.5494e+00 --1.2000e+02 1.4571e+03 1.8222e+00 --1.2000e+02 1.4857e+03 2.0781e+00 --1.2000e+02 1.5143e+03 2.3187e+00 --1.2000e+02 1.5429e+03 2.5452e+00 --1.2000e+02 1.5714e+03 2.7590e+00 --1.2000e+02 1.6000e+03 2.9609e+00 --1.2000e+02 1.6286e+03 3.1520e+00 --1.2000e+02 1.6571e+03 3.3330e+00 --1.2000e+02 1.6857e+03 3.5049e+00 --1.2000e+02 1.7143e+03 3.6681e+00 --1.2000e+02 1.7429e+03 3.8234e+00 --1.2000e+02 1.7714e+03 3.9714e+00 --1.2000e+02 1.8000e+03 4.1124e+00 --1.2000e+02 1.8286e+03 4.2471e+00 --1.2000e+02 1.8571e+03 4.3758e+00 --1.2000e+02 1.8857e+03 4.4989e+00 --1.2000e+02 1.9143e+03 4.6167e+00 --1.2000e+02 1.9429e+03 4.7296e+00 --1.2000e+02 1.9714e+03 4.8379e+00 --1.2000e+02 2.0000e+03 4.9419e+00 --9.0000e+01 -2.0000e+03 4.9363e+00 --9.0000e+01 -1.9714e+03 4.8320e+00 --9.0000e+01 -1.9429e+03 4.7233e+00 --9.0000e+01 -1.9143e+03 4.6100e+00 --9.0000e+01 -1.8857e+03 4.4918e+00 --9.0000e+01 -1.8571e+03 4.3683e+00 --9.0000e+01 -1.8286e+03 4.2391e+00 --9.0000e+01 -1.8000e+03 4.1040e+00 --9.0000e+01 -1.7714e+03 3.9624e+00 --9.0000e+01 -1.7429e+03 3.8138e+00 --9.0000e+01 -1.7143e+03 3.6579e+00 --9.0000e+01 -1.6857e+03 3.4939e+00 --9.0000e+01 -1.6571e+03 3.3213e+00 --9.0000e+01 -1.6286e+03 3.1393e+00 --9.0000e+01 -1.6000e+03 2.9473e+00 --9.0000e+01 -1.5714e+03 2.7444e+00 --9.0000e+01 -1.5429e+03 2.5295e+00 --9.0000e+01 -1.5143e+03 2.3016e+00 --9.0000e+01 -1.4857e+03 2.0596e+00 --9.0000e+01 -1.4571e+03 1.8021e+00 --9.0000e+01 -1.4286e+03 1.5276e+00 --9.0000e+01 -1.4000e+03 1.2342e+00 --9.0000e+01 -1.3714e+03 9.2020e-01 --9.0000e+01 -1.3429e+03 5.8321e-01 --9.0000e+01 -1.3143e+03 2.2070e-01 --9.0000e+01 -1.2857e+03 -1.7030e-01 --9.0000e+01 -1.2571e+03 -5.9321e-01 --9.0000e+01 -1.2286e+03 -1.0520e+00 --9.0000e+01 -1.2000e+03 -1.5513e+00 --9.0000e+01 -1.1714e+03 -2.0967e+00 --9.0000e+01 -1.1429e+03 -2.6945e+00 --9.0000e+01 -1.1143e+03 -3.3525e+00 --9.0000e+01 -1.0857e+03 -4.0800e+00 --9.0000e+01 -1.0571e+03 -4.8880e+00 --9.0000e+01 -1.0286e+03 -5.7901e+00 --9.0000e+01 -1.0000e+03 -6.8027e+00 --9.0000e+01 -9.7143e+02 -7.9464e+00 --9.0000e+01 -9.4286e+02 -9.2463e+00 --9.0000e+01 -9.1429e+02 -1.0734e+01 --9.0000e+01 -8.8571e+02 -1.2449e+01 --9.0000e+01 -8.5714e+02 -1.4442e+01 --9.0000e+01 -8.2857e+02 -1.6776e+01 --9.0000e+01 -8.0000e+02 -1.9529e+01 --9.0000e+01 -7.7143e+02 -2.2796e+01 --9.0000e+01 -7.4286e+02 -2.6692e+01 --9.0000e+01 -7.1429e+02 -3.1335e+01 --9.0000e+01 -6.8571e+02 -3.6830e+01 --9.0000e+01 -6.5714e+02 -4.3217e+01 --9.0000e+01 -6.2857e+02 -5.0408e+01 --9.0000e+01 -6.0000e+02 -5.8132e+01 --9.0000e+01 -5.7143e+02 -6.5960e+01 --9.0000e+01 -5.4286e+02 -7.3430e+01 --9.0000e+01 -5.1429e+02 -8.0191e+01 --9.0000e+01 -4.8571e+02 -8.6078e+01 --9.0000e+01 -4.5714e+02 -9.1083e+01 --9.0000e+01 -4.2857e+02 -9.5288e+01 --9.0000e+01 -4.0000e+02 -9.8810e+01 --9.0000e+01 -3.7143e+02 -1.0177e+02 --9.0000e+01 -3.4286e+02 -1.0426e+02 --9.0000e+01 -3.1429e+02 -1.0638e+02 --9.0000e+01 -2.8571e+02 -1.0819e+02 --9.0000e+01 -2.5714e+02 -1.0975e+02 --9.0000e+01 -2.2857e+02 -1.1110e+02 --9.0000e+01 -2.0000e+02 -1.1227e+02 --9.0000e+01 -1.7143e+02 -1.1329e+02 --9.0000e+01 -1.4286e+02 -1.1416e+02 --9.0000e+01 -1.1429e+02 -1.1492e+02 --9.0000e+01 -8.5714e+01 -1.1554e+02 --9.0000e+01 -5.7143e+01 -1.1603e+02 --9.0000e+01 -2.8571e+01 -1.1634e+02 --9.0000e+01 0.0000e+00 -1.1645e+02 --9.0000e+01 2.8571e+01 -1.1634e+02 --9.0000e+01 5.7143e+01 -1.1603e+02 --9.0000e+01 8.5714e+01 -1.1554e+02 --9.0000e+01 1.1429e+02 -1.1492e+02 --9.0000e+01 1.4286e+02 -1.1416e+02 --9.0000e+01 1.7143e+02 -1.1329e+02 --9.0000e+01 2.0000e+02 -1.1227e+02 --9.0000e+01 2.2857e+02 -1.1110e+02 --9.0000e+01 2.5714e+02 -1.0975e+02 --9.0000e+01 2.8571e+02 -1.0819e+02 --9.0000e+01 3.1429e+02 -1.0638e+02 --9.0000e+01 3.4286e+02 -1.0426e+02 --9.0000e+01 3.7143e+02 -1.0177e+02 --9.0000e+01 4.0000e+02 -9.8810e+01 --9.0000e+01 4.2857e+02 -9.5288e+01 --9.0000e+01 4.5714e+02 -9.1083e+01 --9.0000e+01 4.8571e+02 -8.6078e+01 --9.0000e+01 5.1429e+02 -8.0191e+01 --9.0000e+01 5.4286e+02 -7.3430e+01 --9.0000e+01 5.7143e+02 -6.5960e+01 --9.0000e+01 6.0000e+02 -5.8132e+01 --9.0000e+01 6.2857e+02 -5.0408e+01 --9.0000e+01 6.5714e+02 -4.3217e+01 --9.0000e+01 6.8571e+02 -3.6830e+01 --9.0000e+01 7.1429e+02 -3.1335e+01 --9.0000e+01 7.4286e+02 -2.6692e+01 --9.0000e+01 7.7143e+02 -2.2796e+01 --9.0000e+01 8.0000e+02 -1.9529e+01 --9.0000e+01 8.2857e+02 -1.6776e+01 --9.0000e+01 8.5714e+02 -1.4442e+01 --9.0000e+01 8.8571e+02 -1.2449e+01 --9.0000e+01 9.1429e+02 -1.0734e+01 --9.0000e+01 9.4286e+02 -9.2463e+00 --9.0000e+01 9.7143e+02 -7.9464e+00 --9.0000e+01 1.0000e+03 -6.8027e+00 --9.0000e+01 1.0286e+03 -5.7901e+00 --9.0000e+01 1.0571e+03 -4.8880e+00 --9.0000e+01 1.0857e+03 -4.0800e+00 --9.0000e+01 1.1143e+03 -3.3525e+00 --9.0000e+01 1.1429e+03 -2.6945e+00 --9.0000e+01 1.1714e+03 -2.0967e+00 --9.0000e+01 1.2000e+03 -1.5513e+00 --9.0000e+01 1.2286e+03 -1.0520e+00 --9.0000e+01 1.2571e+03 -5.9321e-01 --9.0000e+01 1.2857e+03 -1.7030e-01 --9.0000e+01 1.3143e+03 2.2070e-01 --9.0000e+01 1.3429e+03 5.8321e-01 --9.0000e+01 1.3714e+03 9.2020e-01 --9.0000e+01 1.4000e+03 1.2342e+00 --9.0000e+01 1.4286e+03 1.5276e+00 --9.0000e+01 1.4571e+03 1.8021e+00 --9.0000e+01 1.4857e+03 2.0596e+00 --9.0000e+01 1.5143e+03 2.3016e+00 --9.0000e+01 1.5429e+03 2.5295e+00 --9.0000e+01 1.5714e+03 2.7444e+00 --9.0000e+01 1.6000e+03 2.9473e+00 --9.0000e+01 1.6286e+03 3.1393e+00 --9.0000e+01 1.6571e+03 3.3213e+00 --9.0000e+01 1.6857e+03 3.4939e+00 --9.0000e+01 1.7143e+03 3.6579e+00 --9.0000e+01 1.7429e+03 3.8138e+00 --9.0000e+01 1.7714e+03 3.9624e+00 --9.0000e+01 1.8000e+03 4.1040e+00 --9.0000e+01 1.8286e+03 4.2391e+00 --9.0000e+01 1.8571e+03 4.3683e+00 --9.0000e+01 1.8857e+03 4.4918e+00 --9.0000e+01 1.9143e+03 4.6100e+00 --9.0000e+01 1.9429e+03 4.7233e+00 --9.0000e+01 1.9714e+03 4.8320e+00 --9.0000e+01 2.0000e+03 4.9363e+00 --6.0000e+01 -2.0000e+03 4.9323e+00 --6.0000e+01 -1.9714e+03 4.8277e+00 --6.0000e+01 -1.9429e+03 4.7188e+00 --6.0000e+01 -1.9143e+03 4.6053e+00 --6.0000e+01 -1.8857e+03 4.4867e+00 --6.0000e+01 -1.8571e+03 4.3629e+00 --6.0000e+01 -1.8286e+03 4.2334e+00 --6.0000e+01 -1.8000e+03 4.0979e+00 --6.0000e+01 -1.7714e+03 3.9559e+00 --6.0000e+01 -1.7429e+03 3.8069e+00 --6.0000e+01 -1.7143e+03 3.6505e+00 --6.0000e+01 -1.6857e+03 3.4860e+00 --6.0000e+01 -1.6571e+03 3.3128e+00 --6.0000e+01 -1.6286e+03 3.1303e+00 --6.0000e+01 -1.6000e+03 2.9376e+00 --6.0000e+01 -1.5714e+03 2.7339e+00 --6.0000e+01 -1.5429e+03 2.5182e+00 --6.0000e+01 -1.5143e+03 2.2894e+00 --6.0000e+01 -1.4857e+03 2.0464e+00 --6.0000e+01 -1.4571e+03 1.7877e+00 --6.0000e+01 -1.4286e+03 1.5119e+00 --6.0000e+01 -1.4000e+03 1.2171e+00 --6.0000e+01 -1.3714e+03 9.0148e-01 --6.0000e+01 -1.3429e+03 5.6267e-01 --6.0000e+01 -1.3143e+03 1.9809e-01 --6.0000e+01 -1.2857e+03 -1.9527e-01 --6.0000e+01 -1.2571e+03 -6.2087e-01 --6.0000e+01 -1.2286e+03 -1.0828e+00 --6.0000e+01 -1.2000e+03 -1.5857e+00 --6.0000e+01 -1.1714e+03 -2.1352e+00 --6.0000e+01 -1.1429e+03 -2.7379e+00 --6.0000e+01 -1.1143e+03 -3.4016e+00 --6.0000e+01 -1.0857e+03 -4.1358e+00 --6.0000e+01 -1.0571e+03 -4.9518e+00 --6.0000e+01 -1.0286e+03 -5.8635e+00 --6.0000e+01 -1.0000e+03 -6.8878e+00 --6.0000e+01 -9.7143e+02 -8.0457e+00 --6.0000e+01 -9.4286e+02 -9.3630e+00 --6.0000e+01 -9.1429e+02 -1.0872e+01 --6.0000e+01 -8.8571e+02 -1.2615e+01 --6.0000e+01 -8.5714e+02 -1.4642e+01 --6.0000e+01 -8.2857e+02 -1.7019e+01 --6.0000e+01 -8.0000e+02 -1.9826e+01 --6.0000e+01 -7.7143e+02 -2.3164e+01 --6.0000e+01 -7.4286e+02 -2.7148e+01 --6.0000e+01 -7.1429e+02 -3.1899e+01 --6.0000e+01 -6.8571e+02 -3.7520e+01 --6.0000e+01 -6.5714e+02 -4.4042e+01 --6.0000e+01 -6.2857e+02 -5.1357e+01 --6.0000e+01 -6.0000e+02 -5.9167e+01 --6.0000e+01 -5.7143e+02 -6.7024e+01 --6.0000e+01 -5.4286e+02 -7.4464e+01 --6.0000e+01 -5.1429e+02 -8.1155e+01 --6.0000e+01 -4.8571e+02 -8.6952e+01 --6.0000e+01 -4.5714e+02 -9.1865e+01 --6.0000e+01 -4.2857e+02 -9.5987e+01 --6.0000e+01 -4.0000e+02 -9.9438e+01 --6.0000e+01 -3.7143e+02 -1.0234e+02 --6.0000e+01 -3.4286e+02 -1.0479e+02 --6.0000e+01 -3.1429e+02 -1.0687e+02 --6.0000e+01 -2.8571e+02 -1.0865e+02 --6.0000e+01 -2.5714e+02 -1.1019e+02 --6.0000e+01 -2.2857e+02 -1.1153e+02 --6.0000e+01 -2.0000e+02 -1.1270e+02 --6.0000e+01 -1.7143e+02 -1.1372e+02 --6.0000e+01 -1.4286e+02 -1.1462e+02 --6.0000e+01 -1.1429e+02 -1.1540e+02 --6.0000e+01 -8.5714e+01 -1.1608e+02 --6.0000e+01 -5.7143e+01 -1.1663e+02 --6.0000e+01 -2.8571e+01 -1.1702e+02 --6.0000e+01 0.0000e+00 -1.1716e+02 --6.0000e+01 2.8571e+01 -1.1702e+02 --6.0000e+01 5.7143e+01 -1.1663e+02 --6.0000e+01 8.5714e+01 -1.1608e+02 --6.0000e+01 1.1429e+02 -1.1540e+02 --6.0000e+01 1.4286e+02 -1.1462e+02 --6.0000e+01 1.7143e+02 -1.1372e+02 --6.0000e+01 2.0000e+02 -1.1270e+02 --6.0000e+01 2.2857e+02 -1.1153e+02 --6.0000e+01 2.5714e+02 -1.1019e+02 --6.0000e+01 2.8571e+02 -1.0865e+02 --6.0000e+01 3.1429e+02 -1.0687e+02 --6.0000e+01 3.4286e+02 -1.0479e+02 --6.0000e+01 3.7143e+02 -1.0234e+02 --6.0000e+01 4.0000e+02 -9.9438e+01 --6.0000e+01 4.2857e+02 -9.5987e+01 --6.0000e+01 4.5714e+02 -9.1865e+01 --6.0000e+01 4.8571e+02 -8.6952e+01 --6.0000e+01 5.1429e+02 -8.1155e+01 --6.0000e+01 5.4286e+02 -7.4464e+01 --6.0000e+01 5.7143e+02 -6.7024e+01 --6.0000e+01 6.0000e+02 -5.9167e+01 --6.0000e+01 6.2857e+02 -5.1357e+01 --6.0000e+01 6.5714e+02 -4.4042e+01 --6.0000e+01 6.8571e+02 -3.7520e+01 --6.0000e+01 7.1429e+02 -3.1899e+01 --6.0000e+01 7.4286e+02 -2.7148e+01 --6.0000e+01 7.7143e+02 -2.3164e+01 --6.0000e+01 8.0000e+02 -1.9826e+01 --6.0000e+01 8.2857e+02 -1.7019e+01 --6.0000e+01 8.5714e+02 -1.4642e+01 --6.0000e+01 8.8571e+02 -1.2615e+01 --6.0000e+01 9.1429e+02 -1.0872e+01 --6.0000e+01 9.4286e+02 -9.3630e+00 --6.0000e+01 9.7143e+02 -8.0457e+00 --6.0000e+01 1.0000e+03 -6.8878e+00 --6.0000e+01 1.0286e+03 -5.8635e+00 --6.0000e+01 1.0571e+03 -4.9518e+00 --6.0000e+01 1.0857e+03 -4.1358e+00 --6.0000e+01 1.1143e+03 -3.4016e+00 --6.0000e+01 1.1429e+03 -2.7379e+00 --6.0000e+01 1.1714e+03 -2.1352e+00 --6.0000e+01 1.2000e+03 -1.5857e+00 --6.0000e+01 1.2286e+03 -1.0828e+00 --6.0000e+01 1.2571e+03 -6.2087e-01 --6.0000e+01 1.2857e+03 -1.9527e-01 --6.0000e+01 1.3143e+03 1.9809e-01 --6.0000e+01 1.3429e+03 5.6267e-01 --6.0000e+01 1.3714e+03 9.0148e-01 --6.0000e+01 1.4000e+03 1.2171e+00 --6.0000e+01 1.4286e+03 1.5119e+00 --6.0000e+01 1.4571e+03 1.7877e+00 --6.0000e+01 1.4857e+03 2.0464e+00 --6.0000e+01 1.5143e+03 2.2894e+00 --6.0000e+01 1.5429e+03 2.5182e+00 --6.0000e+01 1.5714e+03 2.7339e+00 --6.0000e+01 1.6000e+03 2.9376e+00 --6.0000e+01 1.6286e+03 3.1303e+00 --6.0000e+01 1.6571e+03 3.3128e+00 --6.0000e+01 1.6857e+03 3.4860e+00 --6.0000e+01 1.7143e+03 3.6505e+00 --6.0000e+01 1.7429e+03 3.8069e+00 --6.0000e+01 1.7714e+03 3.9559e+00 --6.0000e+01 1.8000e+03 4.0979e+00 --6.0000e+01 1.8286e+03 4.2334e+00 --6.0000e+01 1.8571e+03 4.3629e+00 --6.0000e+01 1.8857e+03 4.4867e+00 --6.0000e+01 1.9143e+03 4.6053e+00 --6.0000e+01 1.9429e+03 4.7188e+00 --6.0000e+01 1.9714e+03 4.8277e+00 --6.0000e+01 2.0000e+03 4.9323e+00 --3.0000e+01 -2.0000e+03 4.9299e+00 --3.0000e+01 -1.9714e+03 4.8252e+00 --3.0000e+01 -1.9429e+03 4.7161e+00 --3.0000e+01 -1.9143e+03 4.6024e+00 --3.0000e+01 -1.8857e+03 4.4837e+00 --3.0000e+01 -1.8571e+03 4.3597e+00 --3.0000e+01 -1.8286e+03 4.2300e+00 --3.0000e+01 -1.8000e+03 4.0943e+00 --3.0000e+01 -1.7714e+03 3.9520e+00 --3.0000e+01 -1.7429e+03 3.8028e+00 --3.0000e+01 -1.7143e+03 3.6461e+00 --3.0000e+01 -1.6857e+03 3.4813e+00 --3.0000e+01 -1.6571e+03 3.3078e+00 --3.0000e+01 -1.6286e+03 3.1249e+00 --3.0000e+01 -1.6000e+03 2.9317e+00 --3.0000e+01 -1.5714e+03 2.7276e+00 --3.0000e+01 -1.5429e+03 2.5114e+00 --3.0000e+01 -1.5143e+03 2.2820e+00 --3.0000e+01 -1.4857e+03 2.0384e+00 --3.0000e+01 -1.4571e+03 1.7790e+00 --3.0000e+01 -1.4286e+03 1.5024e+00 --3.0000e+01 -1.4000e+03 1.2068e+00 --3.0000e+01 -1.3714e+03 8.9021e-01 --3.0000e+01 -1.3429e+03 5.5029e-01 --3.0000e+01 -1.3143e+03 1.8446e-01 --3.0000e+01 -1.2857e+03 -2.1032e-01 --3.0000e+01 -1.2571e+03 -6.3755e-01 --3.0000e+01 -1.2286e+03 -1.1013e+00 --3.0000e+01 -1.2000e+03 -1.6064e+00 --3.0000e+01 -1.1714e+03 -2.1584e+00 --3.0000e+01 -1.1429e+03 -2.7641e+00 --3.0000e+01 -1.1143e+03 -3.4312e+00 --3.0000e+01 -1.0857e+03 -4.1695e+00 --3.0000e+01 -1.0571e+03 -4.9904e+00 --3.0000e+01 -1.0286e+03 -5.9080e+00 --3.0000e+01 -1.0000e+03 -6.9394e+00 --3.0000e+01 -9.7143e+02 -8.1059e+00 --3.0000e+01 -9.4286e+02 -9.4338e+00 --3.0000e+01 -9.1429e+02 -1.0956e+01 --3.0000e+01 -8.8571e+02 -1.2715e+01 --3.0000e+01 -8.5714e+02 -1.4763e+01 --3.0000e+01 -8.2857e+02 -1.7166e+01 --3.0000e+01 -8.0000e+02 -2.0008e+01 --3.0000e+01 -7.7143e+02 -2.3388e+01 --3.0000e+01 -7.4286e+02 -2.7426e+01 --3.0000e+01 -7.1429e+02 -3.2244e+01 --3.0000e+01 -6.8571e+02 -3.7942e+01 --3.0000e+01 -6.5714e+02 -4.4545e+01 --3.0000e+01 -6.2857e+02 -5.1932e+01 --3.0000e+01 -6.0000e+02 -5.9791e+01 --3.0000e+01 -5.7143e+02 -6.7662e+01 --3.0000e+01 -5.4286e+02 -7.5081e+01 --3.0000e+01 -5.1429e+02 -8.1727e+01 --3.0000e+01 -4.8571e+02 -8.7469e+01 --3.0000e+01 -4.5714e+02 -9.2327e+01 --3.0000e+01 -4.2857e+02 -9.6400e+01 --3.0000e+01 -4.0000e+02 -9.9809e+01 --3.0000e+01 -3.7143e+02 -1.0267e+02 --3.0000e+01 -3.4286e+02 -1.0509e+02 --3.0000e+01 -3.1429e+02 -1.0716e+02 --3.0000e+01 -2.8571e+02 -1.0892e+02 --3.0000e+01 -2.5714e+02 -1.1046e+02 --3.0000e+01 -2.2857e+02 -1.1179e+02 --3.0000e+01 -2.0000e+02 -1.1296e+02 --3.0000e+01 -1.7143e+02 -1.1399e+02 --3.0000e+01 -1.4286e+02 -1.1490e+02 --3.0000e+01 -1.1429e+02 -1.1571e+02 --3.0000e+01 -8.5714e+01 -1.1643e+02 --3.0000e+01 -5.7143e+01 -1.1706e+02 --3.0000e+01 -2.8571e+01 -1.1757e+02 --3.0000e+01 0.0000e+00 -1.1780e+02 --3.0000e+01 2.8571e+01 -1.1757e+02 --3.0000e+01 5.7143e+01 -1.1706e+02 --3.0000e+01 8.5714e+01 -1.1643e+02 --3.0000e+01 1.1429e+02 -1.1571e+02 --3.0000e+01 1.4286e+02 -1.1490e+02 --3.0000e+01 1.7143e+02 -1.1399e+02 --3.0000e+01 2.0000e+02 -1.1296e+02 --3.0000e+01 2.2857e+02 -1.1179e+02 --3.0000e+01 2.5714e+02 -1.1046e+02 --3.0000e+01 2.8571e+02 -1.0892e+02 --3.0000e+01 3.1429e+02 -1.0716e+02 --3.0000e+01 3.4286e+02 -1.0509e+02 --3.0000e+01 3.7143e+02 -1.0267e+02 --3.0000e+01 4.0000e+02 -9.9809e+01 --3.0000e+01 4.2857e+02 -9.6400e+01 --3.0000e+01 4.5714e+02 -9.2327e+01 --3.0000e+01 4.8571e+02 -8.7469e+01 --3.0000e+01 5.1429e+02 -8.1727e+01 --3.0000e+01 5.4286e+02 -7.5081e+01 --3.0000e+01 5.7143e+02 -6.7662e+01 --3.0000e+01 6.0000e+02 -5.9791e+01 --3.0000e+01 6.2857e+02 -5.1932e+01 --3.0000e+01 6.5714e+02 -4.4545e+01 --3.0000e+01 6.8571e+02 -3.7942e+01 --3.0000e+01 7.1429e+02 -3.2244e+01 --3.0000e+01 7.4286e+02 -2.7426e+01 --3.0000e+01 7.7143e+02 -2.3388e+01 --3.0000e+01 8.0000e+02 -2.0008e+01 --3.0000e+01 8.2857e+02 -1.7166e+01 --3.0000e+01 8.5714e+02 -1.4763e+01 --3.0000e+01 8.8571e+02 -1.2715e+01 --3.0000e+01 9.1429e+02 -1.0956e+01 --3.0000e+01 9.4286e+02 -9.4338e+00 --3.0000e+01 9.7143e+02 -8.1059e+00 --3.0000e+01 1.0000e+03 -6.9394e+00 --3.0000e+01 1.0286e+03 -5.9080e+00 --3.0000e+01 1.0571e+03 -4.9904e+00 --3.0000e+01 1.0857e+03 -4.1695e+00 --3.0000e+01 1.1143e+03 -3.4312e+00 --3.0000e+01 1.1429e+03 -2.7641e+00 --3.0000e+01 1.1714e+03 -2.1584e+00 --3.0000e+01 1.2000e+03 -1.6064e+00 --3.0000e+01 1.2286e+03 -1.1013e+00 --3.0000e+01 1.2571e+03 -6.3755e-01 --3.0000e+01 1.2857e+03 -2.1032e-01 --3.0000e+01 1.3143e+03 1.8446e-01 --3.0000e+01 1.3429e+03 5.5029e-01 --3.0000e+01 1.3714e+03 8.9021e-01 --3.0000e+01 1.4000e+03 1.2068e+00 --3.0000e+01 1.4286e+03 1.5024e+00 --3.0000e+01 1.4571e+03 1.7790e+00 --3.0000e+01 1.4857e+03 2.0384e+00 --3.0000e+01 1.5143e+03 2.2820e+00 --3.0000e+01 1.5429e+03 2.5114e+00 --3.0000e+01 1.5714e+03 2.7276e+00 --3.0000e+01 1.6000e+03 2.9317e+00 --3.0000e+01 1.6286e+03 3.1249e+00 --3.0000e+01 1.6571e+03 3.3078e+00 --3.0000e+01 1.6857e+03 3.4813e+00 --3.0000e+01 1.7143e+03 3.6461e+00 --3.0000e+01 1.7429e+03 3.8028e+00 --3.0000e+01 1.7714e+03 3.9520e+00 --3.0000e+01 1.8000e+03 4.0943e+00 --3.0000e+01 1.8286e+03 4.2300e+00 --3.0000e+01 1.8571e+03 4.3597e+00 --3.0000e+01 1.8857e+03 4.4837e+00 --3.0000e+01 1.9143e+03 4.6024e+00 --3.0000e+01 1.9429e+03 4.7161e+00 --3.0000e+01 1.9714e+03 4.8252e+00 --3.0000e+01 2.0000e+03 4.9299e+00 -0.0000e+00 -2.0000e+03 4.9290e+00 -0.0000e+00 -1.9714e+03 4.8243e+00 -0.0000e+00 -1.9429e+03 4.7152e+00 -0.0000e+00 -1.9143e+03 4.6015e+00 -0.0000e+00 -1.8857e+03 4.4827e+00 -0.0000e+00 -1.8571e+03 4.3586e+00 -0.0000e+00 -1.8286e+03 4.2289e+00 -0.0000e+00 -1.8000e+03 4.0931e+00 -0.0000e+00 -1.7714e+03 3.9507e+00 -0.0000e+00 -1.7429e+03 3.8014e+00 -0.0000e+00 -1.7143e+03 3.6446e+00 -0.0000e+00 -1.6857e+03 3.4797e+00 -0.0000e+00 -1.6571e+03 3.3061e+00 -0.0000e+00 -1.6286e+03 3.1230e+00 -0.0000e+00 -1.6000e+03 2.9298e+00 -0.0000e+00 -1.5714e+03 2.7255e+00 -0.0000e+00 -1.5429e+03 2.5091e+00 -0.0000e+00 -1.5143e+03 2.2796e+00 -0.0000e+00 -1.4857e+03 2.0357e+00 -0.0000e+00 -1.4571e+03 1.7761e+00 -0.0000e+00 -1.4286e+03 1.4993e+00 -0.0000e+00 -1.4000e+03 1.2034e+00 -0.0000e+00 -1.3714e+03 8.8644e-01 -0.0000e+00 -1.3429e+03 5.4616e-01 -0.0000e+00 -1.3143e+03 1.7990e-01 -0.0000e+00 -1.2857e+03 -2.1535e-01 -0.0000e+00 -1.2571e+03 -6.4313e-01 -0.0000e+00 -1.2286e+03 -1.1075e+00 -0.0000e+00 -1.2000e+03 -1.6133e+00 -0.0000e+00 -1.1714e+03 -2.1662e+00 -0.0000e+00 -1.1429e+03 -2.7728e+00 -0.0000e+00 -1.1143e+03 -3.4412e+00 -0.0000e+00 -1.0857e+03 -4.1808e+00 -0.0000e+00 -1.0571e+03 -5.0034e+00 -0.0000e+00 -1.0286e+03 -5.9229e+00 -0.0000e+00 -1.0000e+03 -6.9567e+00 -0.0000e+00 -9.7143e+02 -8.1260e+00 -0.0000e+00 -9.4286e+02 -9.4576e+00 -0.0000e+00 -9.1429e+02 -1.0985e+01 -0.0000e+00 -8.8571e+02 -1.2749e+01 -0.0000e+00 -8.5714e+02 -1.4804e+01 -0.0000e+00 -8.2857e+02 -1.7216e+01 -0.0000e+00 -8.0000e+02 -2.0069e+01 -0.0000e+00 -7.7143e+02 -2.3464e+01 -0.0000e+00 -7.4286e+02 -2.7520e+01 -0.0000e+00 -7.1429e+02 -3.2360e+01 -0.0000e+00 -6.8571e+02 -3.8083e+01 -0.0000e+00 -6.5714e+02 -4.4714e+01 -0.0000e+00 -6.2857e+02 -5.2125e+01 -0.0000e+00 -6.0000e+02 -6.0000e+01 -0.0000e+00 -5.7143e+02 -6.7875e+01 -0.0000e+00 -5.4286e+02 -7.5286e+01 -0.0000e+00 -5.1429e+02 -8.1917e+01 -0.0000e+00 -4.8571e+02 -8.7640e+01 -0.0000e+00 -4.5714e+02 -9.2480e+01 -0.0000e+00 -4.2857e+02 -9.6536e+01 -0.0000e+00 -4.0000e+02 -9.9931e+01 -0.0000e+00 -3.7143e+02 -1.0278e+02 -0.0000e+00 -3.4286e+02 -1.0520e+02 -0.0000e+00 -3.1429e+02 -1.0725e+02 -0.0000e+00 -2.8571e+02 -1.0902e+02 -0.0000e+00 -2.5714e+02 -1.1054e+02 -0.0000e+00 -2.2857e+02 -1.1187e+02 -0.0000e+00 -2.0000e+02 -1.1304e+02 -0.0000e+00 -1.7143e+02 -1.1408e+02 -0.0000e+00 -1.4286e+02 -1.1500e+02 -0.0000e+00 -1.1429e+02 -1.1582e+02 -0.0000e+00 -8.5714e+01 -1.1656e+02 -0.0000e+00 -5.7143e+01 -1.1723e+02 -0.0000e+00 -2.8571e+01 -1.1783e+02 -0.0000e+00 0.0000e+00 -1.1839e+02 -0.0000e+00 2.8571e+01 -1.1783e+02 -0.0000e+00 5.7143e+01 -1.1723e+02 -0.0000e+00 8.5714e+01 -1.1656e+02 -0.0000e+00 1.1429e+02 -1.1582e+02 -0.0000e+00 1.4286e+02 -1.1500e+02 -0.0000e+00 1.7143e+02 -1.1408e+02 -0.0000e+00 2.0000e+02 -1.1304e+02 -0.0000e+00 2.2857e+02 -1.1187e+02 -0.0000e+00 2.5714e+02 -1.1054e+02 -0.0000e+00 2.8571e+02 -1.0902e+02 -0.0000e+00 3.1429e+02 -1.0725e+02 -0.0000e+00 3.4286e+02 -1.0520e+02 -0.0000e+00 3.7143e+02 -1.0278e+02 -0.0000e+00 4.0000e+02 -9.9931e+01 -0.0000e+00 4.2857e+02 -9.6536e+01 -0.0000e+00 4.5714e+02 -9.2480e+01 -0.0000e+00 4.8571e+02 -8.7640e+01 -0.0000e+00 5.1429e+02 -8.1917e+01 -0.0000e+00 5.4286e+02 -7.5286e+01 -0.0000e+00 5.7143e+02 -6.7875e+01 -0.0000e+00 6.0000e+02 -6.0000e+01 -0.0000e+00 6.2857e+02 -5.2125e+01 -0.0000e+00 6.5714e+02 -4.4714e+01 -0.0000e+00 6.8571e+02 -3.8083e+01 -0.0000e+00 7.1429e+02 -3.2360e+01 -0.0000e+00 7.4286e+02 -2.7520e+01 -0.0000e+00 7.7143e+02 -2.3464e+01 -0.0000e+00 8.0000e+02 -2.0069e+01 -0.0000e+00 8.2857e+02 -1.7216e+01 -0.0000e+00 8.5714e+02 -1.4804e+01 -0.0000e+00 8.8571e+02 -1.2749e+01 -0.0000e+00 9.1429e+02 -1.0985e+01 -0.0000e+00 9.4286e+02 -9.4576e+00 -0.0000e+00 9.7143e+02 -8.1260e+00 -0.0000e+00 1.0000e+03 -6.9567e+00 -0.0000e+00 1.0286e+03 -5.9229e+00 -0.0000e+00 1.0571e+03 -5.0034e+00 -0.0000e+00 1.0857e+03 -4.1808e+00 -0.0000e+00 1.1143e+03 -3.4412e+00 -0.0000e+00 1.1429e+03 -2.7728e+00 -0.0000e+00 1.1714e+03 -2.1662e+00 -0.0000e+00 1.2000e+03 -1.6133e+00 -0.0000e+00 1.2286e+03 -1.1075e+00 -0.0000e+00 1.2571e+03 -6.4313e-01 -0.0000e+00 1.2857e+03 -2.1535e-01 -0.0000e+00 1.3143e+03 1.7990e-01 -0.0000e+00 1.3429e+03 5.4616e-01 -0.0000e+00 1.3714e+03 8.8644e-01 -0.0000e+00 1.4000e+03 1.2034e+00 -0.0000e+00 1.4286e+03 1.4993e+00 -0.0000e+00 1.4571e+03 1.7761e+00 -0.0000e+00 1.4857e+03 2.0357e+00 -0.0000e+00 1.5143e+03 2.2796e+00 -0.0000e+00 1.5429e+03 2.5091e+00 -0.0000e+00 1.5714e+03 2.7255e+00 -0.0000e+00 1.6000e+03 2.9298e+00 -0.0000e+00 1.6286e+03 3.1230e+00 -0.0000e+00 1.6571e+03 3.3061e+00 -0.0000e+00 1.6857e+03 3.4797e+00 -0.0000e+00 1.7143e+03 3.6446e+00 -0.0000e+00 1.7429e+03 3.8014e+00 -0.0000e+00 1.7714e+03 3.9507e+00 -0.0000e+00 1.8000e+03 4.0931e+00 -0.0000e+00 1.8286e+03 4.2289e+00 -0.0000e+00 1.8571e+03 4.3586e+00 -0.0000e+00 1.8857e+03 4.4827e+00 -0.0000e+00 1.9143e+03 4.6015e+00 -0.0000e+00 1.9429e+03 4.7152e+00 -0.0000e+00 1.9714e+03 4.8243e+00 -0.0000e+00 2.0000e+03 4.9290e+00 -3.0000e+01 -2.0000e+03 4.9299e+00 -3.0000e+01 -1.9714e+03 4.8252e+00 -3.0000e+01 -1.9429e+03 4.7161e+00 -3.0000e+01 -1.9143e+03 4.6024e+00 -3.0000e+01 -1.8857e+03 4.4837e+00 -3.0000e+01 -1.8571e+03 4.3597e+00 -3.0000e+01 -1.8286e+03 4.2300e+00 -3.0000e+01 -1.8000e+03 4.0943e+00 -3.0000e+01 -1.7714e+03 3.9520e+00 -3.0000e+01 -1.7429e+03 3.8028e+00 -3.0000e+01 -1.7143e+03 3.6461e+00 -3.0000e+01 -1.6857e+03 3.4813e+00 -3.0000e+01 -1.6571e+03 3.3078e+00 -3.0000e+01 -1.6286e+03 3.1249e+00 -3.0000e+01 -1.6000e+03 2.9317e+00 -3.0000e+01 -1.5714e+03 2.7276e+00 -3.0000e+01 -1.5429e+03 2.5114e+00 -3.0000e+01 -1.5143e+03 2.2820e+00 -3.0000e+01 -1.4857e+03 2.0384e+00 -3.0000e+01 -1.4571e+03 1.7790e+00 -3.0000e+01 -1.4286e+03 1.5024e+00 -3.0000e+01 -1.4000e+03 1.2068e+00 -3.0000e+01 -1.3714e+03 8.9021e-01 -3.0000e+01 -1.3429e+03 5.5029e-01 -3.0000e+01 -1.3143e+03 1.8446e-01 -3.0000e+01 -1.2857e+03 -2.1032e-01 -3.0000e+01 -1.2571e+03 -6.3755e-01 -3.0000e+01 -1.2286e+03 -1.1013e+00 -3.0000e+01 -1.2000e+03 -1.6064e+00 -3.0000e+01 -1.1714e+03 -2.1584e+00 -3.0000e+01 -1.1429e+03 -2.7641e+00 -3.0000e+01 -1.1143e+03 -3.4312e+00 -3.0000e+01 -1.0857e+03 -4.1695e+00 -3.0000e+01 -1.0571e+03 -4.9904e+00 -3.0000e+01 -1.0286e+03 -5.9080e+00 -3.0000e+01 -1.0000e+03 -6.9394e+00 -3.0000e+01 -9.7143e+02 -8.1059e+00 -3.0000e+01 -9.4286e+02 -9.4338e+00 -3.0000e+01 -9.1429e+02 -1.0956e+01 -3.0000e+01 -8.8571e+02 -1.2715e+01 -3.0000e+01 -8.5714e+02 -1.4763e+01 -3.0000e+01 -8.2857e+02 -1.7166e+01 -3.0000e+01 -8.0000e+02 -2.0008e+01 -3.0000e+01 -7.7143e+02 -2.3388e+01 -3.0000e+01 -7.4286e+02 -2.7426e+01 -3.0000e+01 -7.1429e+02 -3.2244e+01 -3.0000e+01 -6.8571e+02 -3.7942e+01 -3.0000e+01 -6.5714e+02 -4.4545e+01 -3.0000e+01 -6.2857e+02 -5.1932e+01 -3.0000e+01 -6.0000e+02 -5.9791e+01 -3.0000e+01 -5.7143e+02 -6.7662e+01 -3.0000e+01 -5.4286e+02 -7.5081e+01 -3.0000e+01 -5.1429e+02 -8.1727e+01 -3.0000e+01 -4.8571e+02 -8.7469e+01 -3.0000e+01 -4.5714e+02 -9.2327e+01 -3.0000e+01 -4.2857e+02 -9.6400e+01 -3.0000e+01 -4.0000e+02 -9.9809e+01 -3.0000e+01 -3.7143e+02 -1.0267e+02 -3.0000e+01 -3.4286e+02 -1.0509e+02 -3.0000e+01 -3.1429e+02 -1.0716e+02 -3.0000e+01 -2.8571e+02 -1.0892e+02 -3.0000e+01 -2.5714e+02 -1.1046e+02 -3.0000e+01 -2.2857e+02 -1.1179e+02 -3.0000e+01 -2.0000e+02 -1.1296e+02 -3.0000e+01 -1.7143e+02 -1.1399e+02 -3.0000e+01 -1.4286e+02 -1.1490e+02 -3.0000e+01 -1.1429e+02 -1.1571e+02 -3.0000e+01 -8.5714e+01 -1.1643e+02 -3.0000e+01 -5.7143e+01 -1.1706e+02 -3.0000e+01 -2.8571e+01 -1.1757e+02 -3.0000e+01 0.0000e+00 -1.1780e+02 -3.0000e+01 2.8571e+01 -1.1757e+02 -3.0000e+01 5.7143e+01 -1.1706e+02 -3.0000e+01 8.5714e+01 -1.1643e+02 -3.0000e+01 1.1429e+02 -1.1571e+02 -3.0000e+01 1.4286e+02 -1.1490e+02 -3.0000e+01 1.7143e+02 -1.1399e+02 -3.0000e+01 2.0000e+02 -1.1296e+02 -3.0000e+01 2.2857e+02 -1.1179e+02 -3.0000e+01 2.5714e+02 -1.1046e+02 -3.0000e+01 2.8571e+02 -1.0892e+02 -3.0000e+01 3.1429e+02 -1.0716e+02 -3.0000e+01 3.4286e+02 -1.0509e+02 -3.0000e+01 3.7143e+02 -1.0267e+02 -3.0000e+01 4.0000e+02 -9.9809e+01 -3.0000e+01 4.2857e+02 -9.6400e+01 -3.0000e+01 4.5714e+02 -9.2327e+01 -3.0000e+01 4.8571e+02 -8.7469e+01 -3.0000e+01 5.1429e+02 -8.1727e+01 -3.0000e+01 5.4286e+02 -7.5081e+01 -3.0000e+01 5.7143e+02 -6.7662e+01 -3.0000e+01 6.0000e+02 -5.9791e+01 -3.0000e+01 6.2857e+02 -5.1932e+01 -3.0000e+01 6.5714e+02 -4.4545e+01 -3.0000e+01 6.8571e+02 -3.7942e+01 -3.0000e+01 7.1429e+02 -3.2244e+01 -3.0000e+01 7.4286e+02 -2.7426e+01 -3.0000e+01 7.7143e+02 -2.3388e+01 -3.0000e+01 8.0000e+02 -2.0008e+01 -3.0000e+01 8.2857e+02 -1.7166e+01 -3.0000e+01 8.5714e+02 -1.4763e+01 -3.0000e+01 8.8571e+02 -1.2715e+01 -3.0000e+01 9.1429e+02 -1.0956e+01 -3.0000e+01 9.4286e+02 -9.4338e+00 -3.0000e+01 9.7143e+02 -8.1059e+00 -3.0000e+01 1.0000e+03 -6.9394e+00 -3.0000e+01 1.0286e+03 -5.9080e+00 -3.0000e+01 1.0571e+03 -4.9904e+00 -3.0000e+01 1.0857e+03 -4.1695e+00 -3.0000e+01 1.1143e+03 -3.4312e+00 -3.0000e+01 1.1429e+03 -2.7641e+00 -3.0000e+01 1.1714e+03 -2.1584e+00 -3.0000e+01 1.2000e+03 -1.6064e+00 -3.0000e+01 1.2286e+03 -1.1013e+00 -3.0000e+01 1.2571e+03 -6.3755e-01 -3.0000e+01 1.2857e+03 -2.1032e-01 -3.0000e+01 1.3143e+03 1.8446e-01 -3.0000e+01 1.3429e+03 5.5029e-01 -3.0000e+01 1.3714e+03 8.9021e-01 -3.0000e+01 1.4000e+03 1.2068e+00 -3.0000e+01 1.4286e+03 1.5024e+00 -3.0000e+01 1.4571e+03 1.7790e+00 -3.0000e+01 1.4857e+03 2.0384e+00 -3.0000e+01 1.5143e+03 2.2820e+00 -3.0000e+01 1.5429e+03 2.5114e+00 -3.0000e+01 1.5714e+03 2.7276e+00 -3.0000e+01 1.6000e+03 2.9317e+00 -3.0000e+01 1.6286e+03 3.1249e+00 -3.0000e+01 1.6571e+03 3.3078e+00 -3.0000e+01 1.6857e+03 3.4813e+00 -3.0000e+01 1.7143e+03 3.6461e+00 -3.0000e+01 1.7429e+03 3.8028e+00 -3.0000e+01 1.7714e+03 3.9520e+00 -3.0000e+01 1.8000e+03 4.0943e+00 -3.0000e+01 1.8286e+03 4.2300e+00 -3.0000e+01 1.8571e+03 4.3597e+00 -3.0000e+01 1.8857e+03 4.4837e+00 -3.0000e+01 1.9143e+03 4.6024e+00 -3.0000e+01 1.9429e+03 4.7161e+00 -3.0000e+01 1.9714e+03 4.8252e+00 -3.0000e+01 2.0000e+03 4.9299e+00 -6.0000e+01 -2.0000e+03 4.9323e+00 -6.0000e+01 -1.9714e+03 4.8277e+00 -6.0000e+01 -1.9429e+03 4.7188e+00 -6.0000e+01 -1.9143e+03 4.6053e+00 -6.0000e+01 -1.8857e+03 4.4867e+00 -6.0000e+01 -1.8571e+03 4.3629e+00 -6.0000e+01 -1.8286e+03 4.2334e+00 -6.0000e+01 -1.8000e+03 4.0979e+00 -6.0000e+01 -1.7714e+03 3.9559e+00 -6.0000e+01 -1.7429e+03 3.8069e+00 -6.0000e+01 -1.7143e+03 3.6505e+00 -6.0000e+01 -1.6857e+03 3.4860e+00 -6.0000e+01 -1.6571e+03 3.3128e+00 -6.0000e+01 -1.6286e+03 3.1303e+00 -6.0000e+01 -1.6000e+03 2.9376e+00 -6.0000e+01 -1.5714e+03 2.7339e+00 -6.0000e+01 -1.5429e+03 2.5182e+00 -6.0000e+01 -1.5143e+03 2.2894e+00 -6.0000e+01 -1.4857e+03 2.0464e+00 -6.0000e+01 -1.4571e+03 1.7877e+00 -6.0000e+01 -1.4286e+03 1.5119e+00 -6.0000e+01 -1.4000e+03 1.2171e+00 -6.0000e+01 -1.3714e+03 9.0148e-01 -6.0000e+01 -1.3429e+03 5.6267e-01 -6.0000e+01 -1.3143e+03 1.9809e-01 -6.0000e+01 -1.2857e+03 -1.9527e-01 -6.0000e+01 -1.2571e+03 -6.2087e-01 -6.0000e+01 -1.2286e+03 -1.0828e+00 -6.0000e+01 -1.2000e+03 -1.5857e+00 -6.0000e+01 -1.1714e+03 -2.1352e+00 -6.0000e+01 -1.1429e+03 -2.7379e+00 -6.0000e+01 -1.1143e+03 -3.4016e+00 -6.0000e+01 -1.0857e+03 -4.1358e+00 -6.0000e+01 -1.0571e+03 -4.9518e+00 -6.0000e+01 -1.0286e+03 -5.8635e+00 -6.0000e+01 -1.0000e+03 -6.8878e+00 -6.0000e+01 -9.7143e+02 -8.0457e+00 -6.0000e+01 -9.4286e+02 -9.3630e+00 -6.0000e+01 -9.1429e+02 -1.0872e+01 -6.0000e+01 -8.8571e+02 -1.2615e+01 -6.0000e+01 -8.5714e+02 -1.4642e+01 -6.0000e+01 -8.2857e+02 -1.7019e+01 -6.0000e+01 -8.0000e+02 -1.9826e+01 -6.0000e+01 -7.7143e+02 -2.3164e+01 -6.0000e+01 -7.4286e+02 -2.7148e+01 -6.0000e+01 -7.1429e+02 -3.1899e+01 -6.0000e+01 -6.8571e+02 -3.7520e+01 -6.0000e+01 -6.5714e+02 -4.4042e+01 -6.0000e+01 -6.2857e+02 -5.1357e+01 -6.0000e+01 -6.0000e+02 -5.9167e+01 -6.0000e+01 -5.7143e+02 -6.7024e+01 -6.0000e+01 -5.4286e+02 -7.4464e+01 -6.0000e+01 -5.1429e+02 -8.1155e+01 -6.0000e+01 -4.8571e+02 -8.6952e+01 -6.0000e+01 -4.5714e+02 -9.1865e+01 -6.0000e+01 -4.2857e+02 -9.5987e+01 -6.0000e+01 -4.0000e+02 -9.9438e+01 -6.0000e+01 -3.7143e+02 -1.0234e+02 -6.0000e+01 -3.4286e+02 -1.0479e+02 -6.0000e+01 -3.1429e+02 -1.0687e+02 -6.0000e+01 -2.8571e+02 -1.0865e+02 -6.0000e+01 -2.5714e+02 -1.1019e+02 -6.0000e+01 -2.2857e+02 -1.1153e+02 -6.0000e+01 -2.0000e+02 -1.1270e+02 -6.0000e+01 -1.7143e+02 -1.1372e+02 -6.0000e+01 -1.4286e+02 -1.1462e+02 -6.0000e+01 -1.1429e+02 -1.1540e+02 -6.0000e+01 -8.5714e+01 -1.1608e+02 -6.0000e+01 -5.7143e+01 -1.1663e+02 -6.0000e+01 -2.8571e+01 -1.1702e+02 -6.0000e+01 0.0000e+00 -1.1716e+02 -6.0000e+01 2.8571e+01 -1.1702e+02 -6.0000e+01 5.7143e+01 -1.1663e+02 -6.0000e+01 8.5714e+01 -1.1608e+02 -6.0000e+01 1.1429e+02 -1.1540e+02 -6.0000e+01 1.4286e+02 -1.1462e+02 -6.0000e+01 1.7143e+02 -1.1372e+02 -6.0000e+01 2.0000e+02 -1.1270e+02 -6.0000e+01 2.2857e+02 -1.1153e+02 -6.0000e+01 2.5714e+02 -1.1019e+02 -6.0000e+01 2.8571e+02 -1.0865e+02 -6.0000e+01 3.1429e+02 -1.0687e+02 -6.0000e+01 3.4286e+02 -1.0479e+02 -6.0000e+01 3.7143e+02 -1.0234e+02 -6.0000e+01 4.0000e+02 -9.9438e+01 -6.0000e+01 4.2857e+02 -9.5987e+01 -6.0000e+01 4.5714e+02 -9.1865e+01 -6.0000e+01 4.8571e+02 -8.6952e+01 -6.0000e+01 5.1429e+02 -8.1155e+01 -6.0000e+01 5.4286e+02 -7.4464e+01 -6.0000e+01 5.7143e+02 -6.7024e+01 -6.0000e+01 6.0000e+02 -5.9167e+01 -6.0000e+01 6.2857e+02 -5.1357e+01 -6.0000e+01 6.5714e+02 -4.4042e+01 -6.0000e+01 6.8571e+02 -3.7520e+01 -6.0000e+01 7.1429e+02 -3.1899e+01 -6.0000e+01 7.4286e+02 -2.7148e+01 -6.0000e+01 7.7143e+02 -2.3164e+01 -6.0000e+01 8.0000e+02 -1.9826e+01 -6.0000e+01 8.2857e+02 -1.7019e+01 -6.0000e+01 8.5714e+02 -1.4642e+01 -6.0000e+01 8.8571e+02 -1.2615e+01 -6.0000e+01 9.1429e+02 -1.0872e+01 -6.0000e+01 9.4286e+02 -9.3630e+00 -6.0000e+01 9.7143e+02 -8.0457e+00 -6.0000e+01 1.0000e+03 -6.8878e+00 -6.0000e+01 1.0286e+03 -5.8635e+00 -6.0000e+01 1.0571e+03 -4.9518e+00 -6.0000e+01 1.0857e+03 -4.1358e+00 -6.0000e+01 1.1143e+03 -3.4016e+00 -6.0000e+01 1.1429e+03 -2.7379e+00 -6.0000e+01 1.1714e+03 -2.1352e+00 -6.0000e+01 1.2000e+03 -1.5857e+00 -6.0000e+01 1.2286e+03 -1.0828e+00 -6.0000e+01 1.2571e+03 -6.2087e-01 -6.0000e+01 1.2857e+03 -1.9527e-01 -6.0000e+01 1.3143e+03 1.9809e-01 -6.0000e+01 1.3429e+03 5.6267e-01 -6.0000e+01 1.3714e+03 9.0148e-01 -6.0000e+01 1.4000e+03 1.2171e+00 -6.0000e+01 1.4286e+03 1.5119e+00 -6.0000e+01 1.4571e+03 1.7877e+00 -6.0000e+01 1.4857e+03 2.0464e+00 -6.0000e+01 1.5143e+03 2.2894e+00 -6.0000e+01 1.5429e+03 2.5182e+00 -6.0000e+01 1.5714e+03 2.7339e+00 -6.0000e+01 1.6000e+03 2.9376e+00 -6.0000e+01 1.6286e+03 3.1303e+00 -6.0000e+01 1.6571e+03 3.3128e+00 -6.0000e+01 1.6857e+03 3.4860e+00 -6.0000e+01 1.7143e+03 3.6505e+00 -6.0000e+01 1.7429e+03 3.8069e+00 -6.0000e+01 1.7714e+03 3.9559e+00 -6.0000e+01 1.8000e+03 4.0979e+00 -6.0000e+01 1.8286e+03 4.2334e+00 -6.0000e+01 1.8571e+03 4.3629e+00 -6.0000e+01 1.8857e+03 4.4867e+00 -6.0000e+01 1.9143e+03 4.6053e+00 -6.0000e+01 1.9429e+03 4.7188e+00 -6.0000e+01 1.9714e+03 4.8277e+00 -6.0000e+01 2.0000e+03 4.9323e+00 -9.0000e+01 -2.0000e+03 4.9363e+00 -9.0000e+01 -1.9714e+03 4.8320e+00 -9.0000e+01 -1.9429e+03 4.7233e+00 -9.0000e+01 -1.9143e+03 4.6100e+00 -9.0000e+01 -1.8857e+03 4.4918e+00 -9.0000e+01 -1.8571e+03 4.3683e+00 -9.0000e+01 -1.8286e+03 4.2391e+00 -9.0000e+01 -1.8000e+03 4.1040e+00 -9.0000e+01 -1.7714e+03 3.9624e+00 -9.0000e+01 -1.7429e+03 3.8138e+00 -9.0000e+01 -1.7143e+03 3.6579e+00 -9.0000e+01 -1.6857e+03 3.4939e+00 -9.0000e+01 -1.6571e+03 3.3213e+00 -9.0000e+01 -1.6286e+03 3.1393e+00 -9.0000e+01 -1.6000e+03 2.9473e+00 -9.0000e+01 -1.5714e+03 2.7444e+00 -9.0000e+01 -1.5429e+03 2.5295e+00 -9.0000e+01 -1.5143e+03 2.3016e+00 -9.0000e+01 -1.4857e+03 2.0596e+00 -9.0000e+01 -1.4571e+03 1.8021e+00 -9.0000e+01 -1.4286e+03 1.5276e+00 -9.0000e+01 -1.4000e+03 1.2342e+00 -9.0000e+01 -1.3714e+03 9.2020e-01 -9.0000e+01 -1.3429e+03 5.8321e-01 -9.0000e+01 -1.3143e+03 2.2070e-01 -9.0000e+01 -1.2857e+03 -1.7030e-01 -9.0000e+01 -1.2571e+03 -5.9321e-01 -9.0000e+01 -1.2286e+03 -1.0520e+00 -9.0000e+01 -1.2000e+03 -1.5513e+00 -9.0000e+01 -1.1714e+03 -2.0967e+00 -9.0000e+01 -1.1429e+03 -2.6945e+00 -9.0000e+01 -1.1143e+03 -3.3525e+00 -9.0000e+01 -1.0857e+03 -4.0800e+00 -9.0000e+01 -1.0571e+03 -4.8880e+00 -9.0000e+01 -1.0286e+03 -5.7901e+00 -9.0000e+01 -1.0000e+03 -6.8027e+00 -9.0000e+01 -9.7143e+02 -7.9464e+00 -9.0000e+01 -9.4286e+02 -9.2463e+00 -9.0000e+01 -9.1429e+02 -1.0734e+01 -9.0000e+01 -8.8571e+02 -1.2449e+01 -9.0000e+01 -8.5714e+02 -1.4442e+01 -9.0000e+01 -8.2857e+02 -1.6776e+01 -9.0000e+01 -8.0000e+02 -1.9529e+01 -9.0000e+01 -7.7143e+02 -2.2796e+01 -9.0000e+01 -7.4286e+02 -2.6692e+01 -9.0000e+01 -7.1429e+02 -3.1335e+01 -9.0000e+01 -6.8571e+02 -3.6830e+01 -9.0000e+01 -6.5714e+02 -4.3217e+01 -9.0000e+01 -6.2857e+02 -5.0408e+01 -9.0000e+01 -6.0000e+02 -5.8132e+01 -9.0000e+01 -5.7143e+02 -6.5960e+01 -9.0000e+01 -5.4286e+02 -7.3430e+01 -9.0000e+01 -5.1429e+02 -8.0191e+01 -9.0000e+01 -4.8571e+02 -8.6078e+01 -9.0000e+01 -4.5714e+02 -9.1083e+01 -9.0000e+01 -4.2857e+02 -9.5288e+01 -9.0000e+01 -4.0000e+02 -9.8810e+01 -9.0000e+01 -3.7143e+02 -1.0177e+02 -9.0000e+01 -3.4286e+02 -1.0426e+02 -9.0000e+01 -3.1429e+02 -1.0638e+02 -9.0000e+01 -2.8571e+02 -1.0819e+02 -9.0000e+01 -2.5714e+02 -1.0975e+02 -9.0000e+01 -2.2857e+02 -1.1110e+02 -9.0000e+01 -2.0000e+02 -1.1227e+02 -9.0000e+01 -1.7143e+02 -1.1329e+02 -9.0000e+01 -1.4286e+02 -1.1416e+02 -9.0000e+01 -1.1429e+02 -1.1492e+02 -9.0000e+01 -8.5714e+01 -1.1554e+02 -9.0000e+01 -5.7143e+01 -1.1603e+02 -9.0000e+01 -2.8571e+01 -1.1634e+02 -9.0000e+01 0.0000e+00 -1.1645e+02 -9.0000e+01 2.8571e+01 -1.1634e+02 -9.0000e+01 5.7143e+01 -1.1603e+02 -9.0000e+01 8.5714e+01 -1.1554e+02 -9.0000e+01 1.1429e+02 -1.1492e+02 -9.0000e+01 1.4286e+02 -1.1416e+02 -9.0000e+01 1.7143e+02 -1.1329e+02 -9.0000e+01 2.0000e+02 -1.1227e+02 -9.0000e+01 2.2857e+02 -1.1110e+02 -9.0000e+01 2.5714e+02 -1.0975e+02 -9.0000e+01 2.8571e+02 -1.0819e+02 -9.0000e+01 3.1429e+02 -1.0638e+02 -9.0000e+01 3.4286e+02 -1.0426e+02 -9.0000e+01 3.7143e+02 -1.0177e+02 -9.0000e+01 4.0000e+02 -9.8810e+01 -9.0000e+01 4.2857e+02 -9.5288e+01 -9.0000e+01 4.5714e+02 -9.1083e+01 -9.0000e+01 4.8571e+02 -8.6078e+01 -9.0000e+01 5.1429e+02 -8.0191e+01 -9.0000e+01 5.4286e+02 -7.3430e+01 -9.0000e+01 5.7143e+02 -6.5960e+01 -9.0000e+01 6.0000e+02 -5.8132e+01 -9.0000e+01 6.2857e+02 -5.0408e+01 -9.0000e+01 6.5714e+02 -4.3217e+01 -9.0000e+01 6.8571e+02 -3.6830e+01 -9.0000e+01 7.1429e+02 -3.1335e+01 -9.0000e+01 7.4286e+02 -2.6692e+01 -9.0000e+01 7.7143e+02 -2.2796e+01 -9.0000e+01 8.0000e+02 -1.9529e+01 -9.0000e+01 8.2857e+02 -1.6776e+01 -9.0000e+01 8.5714e+02 -1.4442e+01 -9.0000e+01 8.8571e+02 -1.2449e+01 -9.0000e+01 9.1429e+02 -1.0734e+01 -9.0000e+01 9.4286e+02 -9.2463e+00 -9.0000e+01 9.7143e+02 -7.9464e+00 -9.0000e+01 1.0000e+03 -6.8027e+00 -9.0000e+01 1.0286e+03 -5.7901e+00 -9.0000e+01 1.0571e+03 -4.8880e+00 -9.0000e+01 1.0857e+03 -4.0800e+00 -9.0000e+01 1.1143e+03 -3.3525e+00 -9.0000e+01 1.1429e+03 -2.6945e+00 -9.0000e+01 1.1714e+03 -2.0967e+00 -9.0000e+01 1.2000e+03 -1.5513e+00 -9.0000e+01 1.2286e+03 -1.0520e+00 -9.0000e+01 1.2571e+03 -5.9321e-01 -9.0000e+01 1.2857e+03 -1.7030e-01 -9.0000e+01 1.3143e+03 2.2070e-01 -9.0000e+01 1.3429e+03 5.8321e-01 -9.0000e+01 1.3714e+03 9.2020e-01 -9.0000e+01 1.4000e+03 1.2342e+00 -9.0000e+01 1.4286e+03 1.5276e+00 -9.0000e+01 1.4571e+03 1.8021e+00 -9.0000e+01 1.4857e+03 2.0596e+00 -9.0000e+01 1.5143e+03 2.3016e+00 -9.0000e+01 1.5429e+03 2.5295e+00 -9.0000e+01 1.5714e+03 2.7444e+00 -9.0000e+01 1.6000e+03 2.9473e+00 -9.0000e+01 1.6286e+03 3.1393e+00 -9.0000e+01 1.6571e+03 3.3213e+00 -9.0000e+01 1.6857e+03 3.4939e+00 -9.0000e+01 1.7143e+03 3.6579e+00 -9.0000e+01 1.7429e+03 3.8138e+00 -9.0000e+01 1.7714e+03 3.9624e+00 -9.0000e+01 1.8000e+03 4.1040e+00 -9.0000e+01 1.8286e+03 4.2391e+00 -9.0000e+01 1.8571e+03 4.3683e+00 -9.0000e+01 1.8857e+03 4.4918e+00 -9.0000e+01 1.9143e+03 4.6100e+00 -9.0000e+01 1.9429e+03 4.7233e+00 -9.0000e+01 1.9714e+03 4.8320e+00 -9.0000e+01 2.0000e+03 4.9363e+00 -1.2000e+02 -2.0000e+03 4.9419e+00 -1.2000e+02 -1.9714e+03 4.8379e+00 -1.2000e+02 -1.9429e+03 4.7296e+00 -1.2000e+02 -1.9143e+03 4.6167e+00 -1.2000e+02 -1.8857e+03 4.4989e+00 -1.2000e+02 -1.8571e+03 4.3758e+00 -1.2000e+02 -1.8286e+03 4.2471e+00 -1.2000e+02 -1.8000e+03 4.1124e+00 -1.2000e+02 -1.7714e+03 3.9714e+00 -1.2000e+02 -1.7429e+03 3.8234e+00 -1.2000e+02 -1.7143e+03 3.6681e+00 -1.2000e+02 -1.6857e+03 3.5049e+00 -1.2000e+02 -1.6571e+03 3.3330e+00 -1.2000e+02 -1.6286e+03 3.1520e+00 -1.2000e+02 -1.6000e+03 2.9609e+00 -1.2000e+02 -1.5714e+03 2.7590e+00 -1.2000e+02 -1.5429e+03 2.5452e+00 -1.2000e+02 -1.5143e+03 2.3187e+00 -1.2000e+02 -1.4857e+03 2.0781e+00 -1.2000e+02 -1.4571e+03 1.8222e+00 -1.2000e+02 -1.4286e+03 1.5494e+00 -1.2000e+02 -1.4000e+03 1.2580e+00 -1.2000e+02 -1.3714e+03 9.4624e-01 -1.2000e+02 -1.3429e+03 6.1178e-01 -1.2000e+02 -1.3143e+03 2.5213e-01 -1.2000e+02 -1.2857e+03 -1.3561e-01 -1.2000e+02 -1.2571e+03 -5.5478e-01 -1.2000e+02 -1.2286e+03 -1.0093e+00 -1.2000e+02 -1.2000e+03 -1.5037e+00 -1.2000e+02 -1.1714e+03 -2.0433e+00 -1.2000e+02 -1.1429e+03 -2.6344e+00 -1.2000e+02 -1.1143e+03 -3.2846e+00 -1.2000e+02 -1.0857e+03 -4.0028e+00 -1.2000e+02 -1.0571e+03 -4.7997e+00 -1.2000e+02 -1.0286e+03 -5.6885e+00 -1.2000e+02 -1.0000e+03 -6.6852e+00 -1.2000e+02 -9.7143e+02 -7.8095e+00 -1.2000e+02 -9.4286e+02 -9.0855e+00 -1.2000e+02 -9.1429e+02 -1.0544e+01 -1.2000e+02 -8.8571e+02 -1.2222e+01 -1.2000e+02 -8.5714e+02 -1.4169e+01 -1.2000e+02 -8.2857e+02 -1.6444e+01 -1.2000e+02 -8.0000e+02 -1.9122e+01 -1.2000e+02 -7.7143e+02 -2.2295e+01 -1.2000e+02 -7.4286e+02 -2.6071e+01 -1.2000e+02 -7.1429e+02 -3.0568e+01 -1.2000e+02 -6.8571e+02 -3.5889e+01 -1.2000e+02 -6.5714e+02 -4.2089e+01 -1.2000e+02 -6.2857e+02 -4.9103e+01 -1.2000e+02 -6.0000e+02 -5.6697e+01 -1.2000e+02 -5.7143e+02 -6.4471e+01 -1.2000e+02 -5.4286e+02 -7.1969e+01 -1.2000e+02 -5.1429e+02 -7.8821e+01 -1.2000e+02 -4.8571e+02 -8.4829e+01 -1.2000e+02 -4.5714e+02 -8.9961e+01 -1.2000e+02 -4.2857e+02 -9.4285e+01 -1.2000e+02 -4.0000e+02 -9.7910e+01 -1.2000e+02 -3.7143e+02 -1.0095e+02 -1.2000e+02 -3.4286e+02 -1.0352e+02 -1.2000e+02 -3.1429e+02 -1.0569e+02 -1.2000e+02 -2.8571e+02 -1.0754e+02 -1.2000e+02 -2.5714e+02 -1.0913e+02 -1.2000e+02 -2.2857e+02 -1.1049e+02 -1.2000e+02 -2.0000e+02 -1.1167e+02 -1.2000e+02 -1.7143e+02 -1.1268e+02 -1.2000e+02 -1.4286e+02 -1.1354e+02 -1.2000e+02 -1.1429e+02 -1.1427e+02 -1.2000e+02 -8.5714e+01 -1.1486e+02 -1.2000e+02 -5.7143e+01 -1.1529e+02 -1.2000e+02 -2.8571e+01 -1.1557e+02 -1.2000e+02 0.0000e+00 -1.1566e+02 -1.2000e+02 2.8571e+01 -1.1557e+02 -1.2000e+02 5.7143e+01 -1.1529e+02 -1.2000e+02 8.5714e+01 -1.1486e+02 -1.2000e+02 1.1429e+02 -1.1427e+02 -1.2000e+02 1.4286e+02 -1.1354e+02 -1.2000e+02 1.7143e+02 -1.1268e+02 -1.2000e+02 2.0000e+02 -1.1167e+02 -1.2000e+02 2.2857e+02 -1.1049e+02 -1.2000e+02 2.5714e+02 -1.0913e+02 -1.2000e+02 2.8571e+02 -1.0754e+02 -1.2000e+02 3.1429e+02 -1.0569e+02 -1.2000e+02 3.4286e+02 -1.0352e+02 -1.2000e+02 3.7143e+02 -1.0095e+02 -1.2000e+02 4.0000e+02 -9.7910e+01 -1.2000e+02 4.2857e+02 -9.4285e+01 -1.2000e+02 4.5714e+02 -8.9961e+01 -1.2000e+02 4.8571e+02 -8.4829e+01 -1.2000e+02 5.1429e+02 -7.8821e+01 -1.2000e+02 5.4286e+02 -7.1969e+01 -1.2000e+02 5.7143e+02 -6.4471e+01 -1.2000e+02 6.0000e+02 -5.6697e+01 -1.2000e+02 6.2857e+02 -4.9103e+01 -1.2000e+02 6.5714e+02 -4.2089e+01 -1.2000e+02 6.8571e+02 -3.5889e+01 -1.2000e+02 7.1429e+02 -3.0568e+01 -1.2000e+02 7.4286e+02 -2.6071e+01 -1.2000e+02 7.7143e+02 -2.2295e+01 -1.2000e+02 8.0000e+02 -1.9122e+01 -1.2000e+02 8.2857e+02 -1.6444e+01 -1.2000e+02 8.5714e+02 -1.4169e+01 -1.2000e+02 8.8571e+02 -1.2222e+01 -1.2000e+02 9.1429e+02 -1.0544e+01 -1.2000e+02 9.4286e+02 -9.0855e+00 -1.2000e+02 9.7143e+02 -7.8095e+00 -1.2000e+02 1.0000e+03 -6.6852e+00 -1.2000e+02 1.0286e+03 -5.6885e+00 -1.2000e+02 1.0571e+03 -4.7997e+00 -1.2000e+02 1.0857e+03 -4.0028e+00 -1.2000e+02 1.1143e+03 -3.2846e+00 -1.2000e+02 1.1429e+03 -2.6344e+00 -1.2000e+02 1.1714e+03 -2.0433e+00 -1.2000e+02 1.2000e+03 -1.5037e+00 -1.2000e+02 1.2286e+03 -1.0093e+00 -1.2000e+02 1.2571e+03 -5.5478e-01 -1.2000e+02 1.2857e+03 -1.3561e-01 -1.2000e+02 1.3143e+03 2.5213e-01 -1.2000e+02 1.3429e+03 6.1178e-01 -1.2000e+02 1.3714e+03 9.4624e-01 -1.2000e+02 1.4000e+03 1.2580e+00 -1.2000e+02 1.4286e+03 1.5494e+00 -1.2000e+02 1.4571e+03 1.8222e+00 -1.2000e+02 1.4857e+03 2.0781e+00 -1.2000e+02 1.5143e+03 2.3187e+00 -1.2000e+02 1.5429e+03 2.5452e+00 -1.2000e+02 1.5714e+03 2.7590e+00 -1.2000e+02 1.6000e+03 2.9609e+00 -1.2000e+02 1.6286e+03 3.1520e+00 -1.2000e+02 1.6571e+03 3.3330e+00 -1.2000e+02 1.6857e+03 3.5049e+00 -1.2000e+02 1.7143e+03 3.6681e+00 -1.2000e+02 1.7429e+03 3.8234e+00 -1.2000e+02 1.7714e+03 3.9714e+00 -1.2000e+02 1.8000e+03 4.1124e+00 -1.2000e+02 1.8286e+03 4.2471e+00 -1.2000e+02 1.8571e+03 4.3758e+00 -1.2000e+02 1.8857e+03 4.4989e+00 -1.2000e+02 1.9143e+03 4.6167e+00 -1.2000e+02 1.9429e+03 4.7296e+00 -1.2000e+02 1.9714e+03 4.8379e+00 -1.2000e+02 2.0000e+03 4.9419e+00 -1.5000e+02 -2.0000e+03 4.9491e+00 -1.5000e+02 -1.9714e+03 4.8456e+00 -1.5000e+02 -1.9429e+03 4.7377e+00 -1.5000e+02 -1.9143e+03 4.6252e+00 -1.5000e+02 -1.8857e+03 4.5079e+00 -1.5000e+02 -1.8571e+03 4.3854e+00 -1.5000e+02 -1.8286e+03 4.2573e+00 -1.5000e+02 -1.8000e+03 4.1233e+00 -1.5000e+02 -1.7714e+03 3.9829e+00 -1.5000e+02 -1.7429e+03 3.8357e+00 -1.5000e+02 -1.7143e+03 3.6812e+00 -1.5000e+02 -1.6857e+03 3.5189e+00 -1.5000e+02 -1.6571e+03 3.3481e+00 -1.5000e+02 -1.6286e+03 3.1681e+00 -1.5000e+02 -1.6000e+03 2.9782e+00 -1.5000e+02 -1.5714e+03 2.7776e+00 -1.5000e+02 -1.5429e+03 2.5654e+00 -1.5000e+02 -1.5143e+03 2.3404e+00 -1.5000e+02 -1.4857e+03 2.1017e+00 -1.5000e+02 -1.4571e+03 1.8477e+00 -1.5000e+02 -1.4286e+03 1.5772e+00 -1.5000e+02 -1.4000e+03 1.2884e+00 -1.5000e+02 -1.3714e+03 9.7945e-01 -1.5000e+02 -1.3429e+03 6.4819e-01 -1.5000e+02 -1.3143e+03 2.9217e-01 -1.5000e+02 -1.2857e+03 -9.1437e-02 -1.5000e+02 -1.2571e+03 -5.0589e-01 -1.5000e+02 -1.2286e+03 -9.5498e-01 -1.5000e+02 -1.2000e+03 -1.4431e+00 -1.5000e+02 -1.1714e+03 -1.9755e+00 -1.5000e+02 -1.1429e+03 -2.5582e+00 -1.5000e+02 -1.1143e+03 -3.1985e+00 -1.5000e+02 -1.0857e+03 -3.9049e+00 -1.5000e+02 -1.0571e+03 -4.6880e+00 -1.5000e+02 -1.0286e+03 -5.5603e+00 -1.5000e+02 -1.0000e+03 -6.5370e+00 -1.5000e+02 -9.7143e+02 -7.6370e+00 -1.5000e+02 -9.4286e+02 -8.8833e+00 -1.5000e+02 -9.1429e+02 -1.0305e+01 -1.5000e+02 -8.8571e+02 -1.1938e+01 -1.5000e+02 -8.5714e+02 -1.3827e+01 -1.5000e+02 -8.2857e+02 -1.6030e+01 -1.5000e+02 -8.0000e+02 -1.8616e+01 -1.5000e+02 -7.7143e+02 -2.1672e+01 -1.5000e+02 -7.4286e+02 -2.5301e+01 -1.5000e+02 -7.1429e+02 -2.9616e+01 -1.5000e+02 -6.8571e+02 -3.4723e+01 -1.5000e+02 -6.5714e+02 -4.0685e+01 -1.5000e+02 -6.2857e+02 -4.7468e+01 -1.5000e+02 -6.0000e+02 -5.4880e+01 -1.5000e+02 -5.7143e+02 -6.2563e+01 -1.5000e+02 -5.4286e+02 -7.0074e+01 -1.5000e+02 -5.1429e+02 -7.7025e+01 -1.5000e+02 -4.8571e+02 -8.3182e+01 -1.5000e+02 -4.5714e+02 -8.8476e+01 -1.5000e+02 -4.2857e+02 -9.2953e+01 -1.5000e+02 -4.0000e+02 -9.6713e+01 -1.5000e+02 -3.7143e+02 -9.9869e+01 -1.5000e+02 -3.4286e+02 -1.0252e+02 -1.5000e+02 -3.1429e+02 -1.0477e+02 -1.5000e+02 -2.8571e+02 -1.0668e+02 -1.5000e+02 -2.5714e+02 -1.0831e+02 -1.5000e+02 -2.2857e+02 -1.0970e+02 -1.5000e+02 -2.0000e+02 -1.1089e+02 -1.5000e+02 -1.7143e+02 -1.1191e+02 -1.5000e+02 -1.4286e+02 -1.1276e+02 -1.5000e+02 -1.1429e+02 -1.1347e+02 -1.5000e+02 -8.5714e+01 -1.1403e+02 -1.5000e+02 -5.7143e+01 -1.1444e+02 -1.5000e+02 -2.8571e+01 -1.1469e+02 -1.5000e+02 0.0000e+00 -1.1478e+02 -1.5000e+02 2.8571e+01 -1.1469e+02 -1.5000e+02 5.7143e+01 -1.1444e+02 -1.5000e+02 8.5714e+01 -1.1403e+02 -1.5000e+02 1.1429e+02 -1.1347e+02 -1.5000e+02 1.4286e+02 -1.1276e+02 -1.5000e+02 1.7143e+02 -1.1191e+02 -1.5000e+02 2.0000e+02 -1.1089e+02 -1.5000e+02 2.2857e+02 -1.0970e+02 -1.5000e+02 2.5714e+02 -1.0831e+02 -1.5000e+02 2.8571e+02 -1.0668e+02 -1.5000e+02 3.1429e+02 -1.0477e+02 -1.5000e+02 3.4286e+02 -1.0252e+02 -1.5000e+02 3.7143e+02 -9.9869e+01 -1.5000e+02 4.0000e+02 -9.6713e+01 -1.5000e+02 4.2857e+02 -9.2953e+01 -1.5000e+02 4.5714e+02 -8.8476e+01 -1.5000e+02 4.8571e+02 -8.3182e+01 -1.5000e+02 5.1429e+02 -7.7025e+01 -1.5000e+02 5.4286e+02 -7.0074e+01 -1.5000e+02 5.7143e+02 -6.2563e+01 -1.5000e+02 6.0000e+02 -5.4880e+01 -1.5000e+02 6.2857e+02 -4.7468e+01 -1.5000e+02 6.5714e+02 -4.0685e+01 -1.5000e+02 6.8571e+02 -3.4723e+01 -1.5000e+02 7.1429e+02 -2.9616e+01 -1.5000e+02 7.4286e+02 -2.5301e+01 -1.5000e+02 7.7143e+02 -2.1672e+01 -1.5000e+02 8.0000e+02 -1.8616e+01 -1.5000e+02 8.2857e+02 -1.6030e+01 -1.5000e+02 8.5714e+02 -1.3827e+01 -1.5000e+02 8.8571e+02 -1.1938e+01 -1.5000e+02 9.1429e+02 -1.0305e+01 -1.5000e+02 9.4286e+02 -8.8833e+00 -1.5000e+02 9.7143e+02 -7.6370e+00 -1.5000e+02 1.0000e+03 -6.5370e+00 -1.5000e+02 1.0286e+03 -5.5603e+00 -1.5000e+02 1.0571e+03 -4.6880e+00 -1.5000e+02 1.0857e+03 -3.9049e+00 -1.5000e+02 1.1143e+03 -3.1985e+00 -1.5000e+02 1.1429e+03 -2.5582e+00 -1.5000e+02 1.1714e+03 -1.9755e+00 -1.5000e+02 1.2000e+03 -1.4431e+00 -1.5000e+02 1.2286e+03 -9.5498e-01 -1.5000e+02 1.2571e+03 -5.0589e-01 -1.5000e+02 1.2857e+03 -9.1437e-02 -1.5000e+02 1.3143e+03 2.9217e-01 -1.5000e+02 1.3429e+03 6.4819e-01 -1.5000e+02 1.3714e+03 9.7945e-01 -1.5000e+02 1.4000e+03 1.2884e+00 -1.5000e+02 1.4286e+03 1.5772e+00 -1.5000e+02 1.4571e+03 1.8477e+00 -1.5000e+02 1.4857e+03 2.1017e+00 -1.5000e+02 1.5143e+03 2.3404e+00 -1.5000e+02 1.5429e+03 2.5654e+00 -1.5000e+02 1.5714e+03 2.7776e+00 -1.5000e+02 1.6000e+03 2.9782e+00 -1.5000e+02 1.6286e+03 3.1681e+00 -1.5000e+02 1.6571e+03 3.3481e+00 -1.5000e+02 1.6857e+03 3.5189e+00 -1.5000e+02 1.7143e+03 3.6812e+00 -1.5000e+02 1.7429e+03 3.8357e+00 -1.5000e+02 1.7714e+03 3.9829e+00 -1.5000e+02 1.8000e+03 4.1233e+00 -1.5000e+02 1.8286e+03 4.2573e+00 -1.5000e+02 1.8571e+03 4.3854e+00 -1.5000e+02 1.8857e+03 4.5079e+00 -1.5000e+02 1.9143e+03 4.6252e+00 -1.5000e+02 1.9429e+03 4.7377e+00 -1.5000e+02 1.9714e+03 4.8456e+00 -1.5000e+02 2.0000e+03 4.9491e+00 -1.8000e+02 -2.0000e+03 4.9579e+00 -1.8000e+02 -1.9714e+03 4.8548e+00 -1.8000e+02 -1.9429e+03 4.7475e+00 -1.8000e+02 -1.9143e+03 4.6356e+00 -1.8000e+02 -1.8857e+03 4.5189e+00 -1.8000e+02 -1.8571e+03 4.3970e+00 -1.8000e+02 -1.8286e+03 4.2696e+00 -1.8000e+02 -1.8000e+03 4.1364e+00 -1.8000e+02 -1.7714e+03 3.9969e+00 -1.8000e+02 -1.7429e+03 3.8507e+00 -1.8000e+02 -1.7143e+03 3.6972e+00 -1.8000e+02 -1.6857e+03 3.5359e+00 -1.8000e+02 -1.6571e+03 3.3663e+00 -1.8000e+02 -1.6286e+03 3.1877e+00 -1.8000e+02 -1.6000e+03 2.9993e+00 -1.8000e+02 -1.5714e+03 2.8003e+00 -1.8000e+02 -1.5429e+03 2.5898e+00 -1.8000e+02 -1.5143e+03 2.3668e+00 -1.8000e+02 -1.4857e+03 2.1302e+00 -1.8000e+02 -1.4571e+03 1.8787e+00 -1.8000e+02 -1.4286e+03 1.6109e+00 -1.8000e+02 -1.4000e+03 1.3251e+00 -1.8000e+02 -1.3714e+03 1.0196e+00 -1.8000e+02 -1.3429e+03 6.9220e-01 -1.8000e+02 -1.3143e+03 3.4054e-01 -1.8000e+02 -1.2857e+03 -3.8104e-02 -1.8000e+02 -1.2571e+03 -4.4690e-01 -1.8000e+02 -1.2286e+03 -8.8950e-01 -1.8000e+02 -1.2000e+03 -1.3702e+00 -1.8000e+02 -1.1714e+03 -1.8939e+00 -1.8000e+02 -1.1429e+03 -2.4665e+00 -1.8000e+02 -1.1143e+03 -3.0950e+00 -1.8000e+02 -1.0857e+03 -3.7876e+00 -1.8000e+02 -1.0571e+03 -4.5542e+00 -1.8000e+02 -1.0286e+03 -5.4069e+00 -1.8000e+02 -1.0000e+03 -6.3600e+00 -1.8000e+02 -9.7143e+02 -7.4313e+00 -1.8000e+02 -9.4286e+02 -8.6428e+00 -1.8000e+02 -9.1429e+02 -1.0021e+01 -1.8000e+02 -8.8571e+02 -1.1601e+01 -1.8000e+02 -8.5714e+02 -1.3423e+01 -1.8000e+02 -8.2857e+02 -1.5542e+01 -1.8000e+02 -8.0000e+02 -1.8021e+01 -1.8000e+02 -7.7143e+02 -2.0943e+01 -1.8000e+02 -7.4286e+02 -2.4402e+01 -1.8000e+02 -7.1429e+02 -2.8506e+01 -1.8000e+02 -6.8571e+02 -3.3360e+01 -1.8000e+02 -6.5714e+02 -3.9039e+01 -1.8000e+02 -6.2857e+02 -4.5536e+01 -1.8000e+02 -6.0000e+02 -5.2708e+01 -1.8000e+02 -5.7143e+02 -6.0248e+01 -1.8000e+02 -5.4286e+02 -6.7742e+01 -1.8000e+02 -5.1429e+02 -7.4786e+01 -1.8000e+02 -4.8571e+02 -8.1106e+01 -1.8000e+02 -4.5714e+02 -8.6592e+01 -1.8000e+02 -4.2857e+02 -9.1259e+01 -1.8000e+02 -4.0000e+02 -9.5189e+01 -1.8000e+02 -3.7143e+02 -9.8490e+01 -1.8000e+02 -3.4286e+02 -1.0127e+02 -1.8000e+02 -3.1429e+02 -1.0361e+02 -1.8000e+02 -2.8571e+02 -1.0559e+02 -1.8000e+02 -2.5714e+02 -1.0728e+02 -1.8000e+02 -2.2857e+02 -1.0871e+02 -1.8000e+02 -2.0000e+02 -1.0993e+02 -1.8000e+02 -1.7143e+02 -1.1096e+02 -1.8000e+02 -1.4286e+02 -1.1182e+02 -1.8000e+02 -1.1429e+02 -1.1252e+02 -1.8000e+02 -8.5714e+01 -1.1307e+02 -1.8000e+02 -5.7143e+01 -1.1346e+02 -1.8000e+02 -2.8571e+01 -1.1370e+02 -1.8000e+02 0.0000e+00 -1.1378e+02 -1.8000e+02 2.8571e+01 -1.1370e+02 -1.8000e+02 5.7143e+01 -1.1346e+02 -1.8000e+02 8.5714e+01 -1.1307e+02 -1.8000e+02 1.1429e+02 -1.1252e+02 -1.8000e+02 1.4286e+02 -1.1182e+02 -1.8000e+02 1.7143e+02 -1.1096e+02 -1.8000e+02 2.0000e+02 -1.0993e+02 -1.8000e+02 2.2857e+02 -1.0871e+02 -1.8000e+02 2.5714e+02 -1.0728e+02 -1.8000e+02 2.8571e+02 -1.0559e+02 -1.8000e+02 3.1429e+02 -1.0361e+02 -1.8000e+02 3.4286e+02 -1.0127e+02 -1.8000e+02 3.7143e+02 -9.8490e+01 -1.8000e+02 4.0000e+02 -9.5189e+01 -1.8000e+02 4.2857e+02 -9.1259e+01 -1.8000e+02 4.5714e+02 -8.6592e+01 -1.8000e+02 4.8571e+02 -8.1106e+01 -1.8000e+02 5.1429e+02 -7.4786e+01 -1.8000e+02 5.4286e+02 -6.7742e+01 -1.8000e+02 5.7143e+02 -6.0248e+01 -1.8000e+02 6.0000e+02 -5.2708e+01 -1.8000e+02 6.2857e+02 -4.5536e+01 -1.8000e+02 6.5714e+02 -3.9039e+01 -1.8000e+02 6.8571e+02 -3.3360e+01 -1.8000e+02 7.1429e+02 -2.8506e+01 -1.8000e+02 7.4286e+02 -2.4402e+01 -1.8000e+02 7.7143e+02 -2.0943e+01 -1.8000e+02 8.0000e+02 -1.8021e+01 -1.8000e+02 8.2857e+02 -1.5542e+01 -1.8000e+02 8.5714e+02 -1.3423e+01 -1.8000e+02 8.8571e+02 -1.1601e+01 -1.8000e+02 9.1429e+02 -1.0021e+01 -1.8000e+02 9.4286e+02 -8.6428e+00 -1.8000e+02 9.7143e+02 -7.4313e+00 -1.8000e+02 1.0000e+03 -6.3600e+00 -1.8000e+02 1.0286e+03 -5.4069e+00 -1.8000e+02 1.0571e+03 -4.5542e+00 -1.8000e+02 1.0857e+03 -3.7876e+00 -1.8000e+02 1.1143e+03 -3.0950e+00 -1.8000e+02 1.1429e+03 -2.4665e+00 -1.8000e+02 1.1714e+03 -1.8939e+00 -1.8000e+02 1.2000e+03 -1.3702e+00 -1.8000e+02 1.2286e+03 -8.8950e-01 -1.8000e+02 1.2571e+03 -4.4690e-01 -1.8000e+02 1.2857e+03 -3.8104e-02 -1.8000e+02 1.3143e+03 3.4054e-01 -1.8000e+02 1.3429e+03 6.9220e-01 -1.8000e+02 1.3714e+03 1.0196e+00 -1.8000e+02 1.4000e+03 1.3251e+00 -1.8000e+02 1.4286e+03 1.6109e+00 -1.8000e+02 1.4571e+03 1.8787e+00 -1.8000e+02 1.4857e+03 2.1302e+00 -1.8000e+02 1.5143e+03 2.3668e+00 -1.8000e+02 1.5429e+03 2.5898e+00 -1.8000e+02 1.5714e+03 2.8003e+00 -1.8000e+02 1.6000e+03 2.9993e+00 -1.8000e+02 1.6286e+03 3.1877e+00 -1.8000e+02 1.6571e+03 3.3663e+00 -1.8000e+02 1.6857e+03 3.5359e+00 -1.8000e+02 1.7143e+03 3.6972e+00 -1.8000e+02 1.7429e+03 3.8507e+00 -1.8000e+02 1.7714e+03 3.9969e+00 -1.8000e+02 1.8000e+03 4.1364e+00 -1.8000e+02 1.8286e+03 4.2696e+00 -1.8000e+02 1.8571e+03 4.3970e+00 -1.8000e+02 1.8857e+03 4.5189e+00 -1.8000e+02 1.9143e+03 4.6356e+00 -1.8000e+02 1.9429e+03 4.7475e+00 -1.8000e+02 1.9714e+03 4.8548e+00 -1.8000e+02 2.0000e+03 4.9579e+00 -2.1000e+02 -2.0000e+03 4.9682e+00 -2.1000e+02 -1.9714e+03 4.8657e+00 -2.1000e+02 -1.9429e+03 4.7590e+00 -2.1000e+02 -1.9143e+03 4.6478e+00 -2.1000e+02 -1.8857e+03 4.5318e+00 -2.1000e+02 -1.8571e+03 4.4107e+00 -2.1000e+02 -1.8286e+03 4.2842e+00 -2.1000e+02 -1.8000e+03 4.1519e+00 -2.1000e+02 -1.7714e+03 4.0133e+00 -2.1000e+02 -1.7429e+03 3.8682e+00 -2.1000e+02 -1.7143e+03 3.7159e+00 -2.1000e+02 -1.6857e+03 3.5559e+00 -2.1000e+02 -1.6571e+03 3.3877e+00 -2.1000e+02 -1.6286e+03 3.2106e+00 -2.1000e+02 -1.6000e+03 3.0239e+00 -2.1000e+02 -1.5714e+03 2.8268e+00 -2.1000e+02 -1.5429e+03 2.6184e+00 -2.1000e+02 -1.5143e+03 2.3977e+00 -2.1000e+02 -1.4857e+03 2.1636e+00 -2.1000e+02 -1.4571e+03 1.9150e+00 -2.1000e+02 -1.4286e+03 1.6503e+00 -2.1000e+02 -1.4000e+03 1.3681e+00 -2.1000e+02 -1.3714e+03 1.0665e+00 -2.1000e+02 -1.3429e+03 7.4354e-01 -2.1000e+02 -1.3143e+03 3.9693e-01 -2.1000e+02 -1.2857e+03 2.4017e-02 -2.1000e+02 -1.2571e+03 -3.7824e-01 -2.1000e+02 -1.2286e+03 -8.1336e-01 -2.1000e+02 -1.2000e+03 -1.2854e+00 -2.1000e+02 -1.1714e+03 -1.7992e+00 -2.1000e+02 -1.1429e+03 -2.3603e+00 -2.1000e+02 -1.1143e+03 -2.9752e+00 -2.1000e+02 -1.0857e+03 -3.6520e+00 -2.1000e+02 -1.0571e+03 -4.3998e+00 -2.1000e+02 -1.0286e+03 -5.2301e+00 -2.1000e+02 -1.0000e+03 -6.1564e+00 -2.1000e+02 -9.7143e+02 -7.1954e+00 -2.1000e+02 -9.4286e+02 -8.3674e+00 -2.1000e+02 -9.1429e+02 -9.6976e+00 -2.1000e+02 -8.8571e+02 -1.1217e+01 -2.1000e+02 -8.5714e+02 -1.2965e+01 -2.1000e+02 -8.2857e+02 -1.4990e+01 -2.1000e+02 -8.0000e+02 -1.7351e+01 -2.1000e+02 -7.7143e+02 -2.0123e+01 -2.1000e+02 -7.4286e+02 -2.3394e+01 -2.1000e+02 -7.1429e+02 -2.7264e+01 -2.1000e+02 -6.8571e+02 -3.1836e+01 -2.1000e+02 -6.5714e+02 -3.7191e+01 -2.1000e+02 -6.2857e+02 -4.3351e+01 -2.1000e+02 -6.0000e+02 -5.0220e+01 -2.1000e+02 -5.7143e+02 -5.7553e+01 -2.1000e+02 -5.4286e+02 -6.4976e+01 -2.1000e+02 -5.1429e+02 -7.2086e+01 -2.1000e+02 -4.8571e+02 -7.8573e+01 -2.1000e+02 -4.5714e+02 -8.4273e+01 -2.1000e+02 -4.2857e+02 -8.9162e+01 -2.1000e+02 -4.0000e+02 -9.3298e+01 -2.1000e+02 -3.7143e+02 -9.6779e+01 -2.1000e+02 -3.4286e+02 -9.9706e+01 -2.1000e+02 -3.1429e+02 -1.0217e+02 -2.1000e+02 -2.8571e+02 -1.0425e+02 -2.1000e+02 -2.5714e+02 -1.0602e+02 -2.1000e+02 -2.2857e+02 -1.0751e+02 -2.1000e+02 -2.0000e+02 -1.0877e+02 -2.1000e+02 -1.7143e+02 -1.0982e+02 -2.1000e+02 -1.4286e+02 -1.1070e+02 -2.1000e+02 -1.1429e+02 -1.1140e+02 -2.1000e+02 -8.5714e+01 -1.1195e+02 -2.1000e+02 -5.7143e+01 -1.1234e+02 -2.1000e+02 -2.8571e+01 -1.1257e+02 -2.1000e+02 0.0000e+00 -1.1265e+02 -2.1000e+02 2.8571e+01 -1.1257e+02 -2.1000e+02 5.7143e+01 -1.1234e+02 -2.1000e+02 8.5714e+01 -1.1195e+02 -2.1000e+02 1.1429e+02 -1.1140e+02 -2.1000e+02 1.4286e+02 -1.1070e+02 -2.1000e+02 1.7143e+02 -1.0982e+02 -2.1000e+02 2.0000e+02 -1.0877e+02 -2.1000e+02 2.2857e+02 -1.0751e+02 -2.1000e+02 2.5714e+02 -1.0602e+02 -2.1000e+02 2.8571e+02 -1.0425e+02 -2.1000e+02 3.1429e+02 -1.0217e+02 -2.1000e+02 3.4286e+02 -9.9706e+01 -2.1000e+02 3.7143e+02 -9.6779e+01 -2.1000e+02 4.0000e+02 -9.3298e+01 -2.1000e+02 4.2857e+02 -8.9162e+01 -2.1000e+02 4.5714e+02 -8.4273e+01 -2.1000e+02 4.8571e+02 -7.8573e+01 -2.1000e+02 5.1429e+02 -7.2086e+01 -2.1000e+02 5.4286e+02 -6.4976e+01 -2.1000e+02 5.7143e+02 -5.7553e+01 -2.1000e+02 6.0000e+02 -5.0220e+01 -2.1000e+02 6.2857e+02 -4.3351e+01 -2.1000e+02 6.5714e+02 -3.7191e+01 -2.1000e+02 6.8571e+02 -3.1836e+01 -2.1000e+02 7.1429e+02 -2.7264e+01 -2.1000e+02 7.4286e+02 -2.3394e+01 -2.1000e+02 7.7143e+02 -2.0123e+01 -2.1000e+02 8.0000e+02 -1.7351e+01 -2.1000e+02 8.2857e+02 -1.4990e+01 -2.1000e+02 8.5714e+02 -1.2965e+01 -2.1000e+02 8.8571e+02 -1.1217e+01 -2.1000e+02 9.1429e+02 -9.6976e+00 -2.1000e+02 9.4286e+02 -8.3674e+00 -2.1000e+02 9.7143e+02 -7.1954e+00 -2.1000e+02 1.0000e+03 -6.1564e+00 -2.1000e+02 1.0286e+03 -5.2301e+00 -2.1000e+02 1.0571e+03 -4.3998e+00 -2.1000e+02 1.0857e+03 -3.6520e+00 -2.1000e+02 1.1143e+03 -2.9752e+00 -2.1000e+02 1.1429e+03 -2.3603e+00 -2.1000e+02 1.1714e+03 -1.7992e+00 -2.1000e+02 1.2000e+03 -1.2854e+00 -2.1000e+02 1.2286e+03 -8.1336e-01 -2.1000e+02 1.2571e+03 -3.7824e-01 -2.1000e+02 1.2857e+03 2.4017e-02 -2.1000e+02 1.3143e+03 3.9693e-01 -2.1000e+02 1.3429e+03 7.4354e-01 -2.1000e+02 1.3714e+03 1.0665e+00 -2.1000e+02 1.4000e+03 1.3681e+00 -2.1000e+02 1.4286e+03 1.6503e+00 -2.1000e+02 1.4571e+03 1.9150e+00 -2.1000e+02 1.4857e+03 2.1636e+00 -2.1000e+02 1.5143e+03 2.3977e+00 -2.1000e+02 1.5429e+03 2.6184e+00 -2.1000e+02 1.5714e+03 2.8268e+00 -2.1000e+02 1.6000e+03 3.0239e+00 -2.1000e+02 1.6286e+03 3.2106e+00 -2.1000e+02 1.6571e+03 3.3877e+00 -2.1000e+02 1.6857e+03 3.5559e+00 -2.1000e+02 1.7143e+03 3.7159e+00 -2.1000e+02 1.7429e+03 3.8682e+00 -2.1000e+02 1.7714e+03 4.0133e+00 -2.1000e+02 1.8000e+03 4.1519e+00 -2.1000e+02 1.8286e+03 4.2842e+00 -2.1000e+02 1.8571e+03 4.4107e+00 -2.1000e+02 1.8857e+03 4.5318e+00 -2.1000e+02 1.9143e+03 4.6478e+00 -2.1000e+02 1.9429e+03 4.7590e+00 -2.1000e+02 1.9714e+03 4.8657e+00 -2.1000e+02 2.0000e+03 4.9682e+00 -2.4000e+02 -2.0000e+03 4.9801e+00 -2.4000e+02 -1.9714e+03 4.8782e+00 -2.4000e+02 -1.9429e+03 4.7722e+00 -2.4000e+02 -1.9143e+03 4.6617e+00 -2.4000e+02 -1.8857e+03 4.5466e+00 -2.4000e+02 -1.8571e+03 4.4264e+00 -2.4000e+02 -1.8286e+03 4.3008e+00 -2.4000e+02 -1.8000e+03 4.1696e+00 -2.4000e+02 -1.7714e+03 4.0322e+00 -2.4000e+02 -1.7429e+03 3.8882e+00 -2.4000e+02 -1.7143e+03 3.7373e+00 -2.4000e+02 -1.6857e+03 3.5788e+00 -2.4000e+02 -1.6571e+03 3.4122e+00 -2.4000e+02 -1.6286e+03 3.2369e+00 -2.4000e+02 -1.6000e+03 3.0521e+00 -2.4000e+02 -1.5714e+03 2.8571e+00 -2.4000e+02 -1.5429e+03 2.6510e+00 -2.4000e+02 -1.5143e+03 2.4329e+00 -2.4000e+02 -1.4857e+03 2.2018e+00 -2.4000e+02 -1.4571e+03 1.9563e+00 -2.4000e+02 -1.4286e+03 1.6952e+00 -2.4000e+02 -1.4000e+03 1.4169e+00 -2.4000e+02 -1.3714e+03 1.1198e+00 -2.4000e+02 -1.3429e+03 8.0189e-01 -2.4000e+02 -1.3143e+03 4.6097e-01 -2.4000e+02 -1.2857e+03 9.4502e-02 -2.4000e+02 -1.2571e+03 -3.0042e-01 -2.4000e+02 -1.2286e+03 -7.2715e-01 -2.4000e+02 -1.2000e+03 -1.1896e+00 -2.4000e+02 -1.1714e+03 -1.6922e+00 -2.4000e+02 -1.1429e+03 -2.2404e+00 -2.4000e+02 -1.1143e+03 -2.8403e+00 -2.4000e+02 -1.0857e+03 -3.4995e+00 -2.4000e+02 -1.0571e+03 -4.2265e+00 -2.4000e+02 -1.0286e+03 -5.0321e+00 -2.4000e+02 -1.0000e+03 -5.9288e+00 -2.4000e+02 -9.7143e+02 -6.9322e+00 -2.4000e+02 -9.4286e+02 -8.0611e+00 -2.4000e+02 -9.1429e+02 -9.3385e+00 -2.4000e+02 -8.8571e+02 -1.0793e+01 -2.4000e+02 -8.5714e+02 -1.2460e+01 -2.4000e+02 -8.2857e+02 -1.4384e+01 -2.4000e+02 -8.0000e+02 -1.6618e+01 -2.4000e+02 -7.7143e+02 -1.9230e+01 -2.4000e+02 -7.4286e+02 -2.2300e+01 -2.4000e+02 -7.1429e+02 -2.5920e+01 -2.4000e+02 -6.8571e+02 -3.0187e+01 -2.4000e+02 -6.5714e+02 -3.5188e+01 -2.4000e+02 -6.2857e+02 -4.0964e+01 -2.4000e+02 -6.0000e+02 -4.7468e+01 -2.4000e+02 -5.7143e+02 -5.4518e+01 -2.4000e+02 -5.4286e+02 -6.1797e+01 -2.4000e+02 -5.1429e+02 -6.8923e+01 -2.4000e+02 -4.8571e+02 -7.5553e+01 -2.4000e+02 -4.5714e+02 -8.1476e+01 -2.4000e+02 -4.2857e+02 -8.6613e+01 -2.4000e+02 -4.0000e+02 -9.0991e+01 -2.4000e+02 -3.7143e+02 -9.4689e+01 -2.4000e+02 -3.4286e+02 -9.7801e+01 -2.4000e+02 -3.1429e+02 -1.0042e+02 -2.4000e+02 -2.8571e+02 -1.0263e+02 -2.4000e+02 -2.5714e+02 -1.0449e+02 -2.4000e+02 -2.2857e+02 -1.0606e+02 -2.4000e+02 -2.0000e+02 -1.0738e+02 -2.4000e+02 -1.7143e+02 -1.0847e+02 -2.4000e+02 -1.4286e+02 -1.0938e+02 -2.4000e+02 -1.1429e+02 -1.1010e+02 -2.4000e+02 -8.5714e+01 -1.1066e+02 -2.4000e+02 -5.7143e+01 -1.1105e+02 -2.4000e+02 -2.8571e+01 -1.1128e+02 -2.4000e+02 0.0000e+00 -1.1136e+02 -2.4000e+02 2.8571e+01 -1.1128e+02 -2.4000e+02 5.7143e+01 -1.1105e+02 -2.4000e+02 8.5714e+01 -1.1066e+02 -2.4000e+02 1.1429e+02 -1.1010e+02 -2.4000e+02 1.4286e+02 -1.0938e+02 -2.4000e+02 1.7143e+02 -1.0847e+02 -2.4000e+02 2.0000e+02 -1.0738e+02 -2.4000e+02 2.2857e+02 -1.0606e+02 -2.4000e+02 2.5714e+02 -1.0449e+02 -2.4000e+02 2.8571e+02 -1.0263e+02 -2.4000e+02 3.1429e+02 -1.0042e+02 -2.4000e+02 3.4286e+02 -9.7801e+01 -2.4000e+02 3.7143e+02 -9.4689e+01 -2.4000e+02 4.0000e+02 -9.0991e+01 -2.4000e+02 4.2857e+02 -8.6613e+01 -2.4000e+02 4.5714e+02 -8.1476e+01 -2.4000e+02 4.8571e+02 -7.5553e+01 -2.4000e+02 5.1429e+02 -6.8923e+01 -2.4000e+02 5.4286e+02 -6.1797e+01 -2.4000e+02 5.7143e+02 -5.4518e+01 -2.4000e+02 6.0000e+02 -4.7468e+01 -2.4000e+02 6.2857e+02 -4.0964e+01 -2.4000e+02 6.5714e+02 -3.5188e+01 -2.4000e+02 6.8571e+02 -3.0187e+01 -2.4000e+02 7.1429e+02 -2.5920e+01 -2.4000e+02 7.4286e+02 -2.2300e+01 -2.4000e+02 7.7143e+02 -1.9230e+01 -2.4000e+02 8.0000e+02 -1.6618e+01 -2.4000e+02 8.2857e+02 -1.4384e+01 -2.4000e+02 8.5714e+02 -1.2460e+01 -2.4000e+02 8.8571e+02 -1.0793e+01 -2.4000e+02 9.1429e+02 -9.3385e+00 -2.4000e+02 9.4286e+02 -8.0611e+00 -2.4000e+02 9.7143e+02 -6.9322e+00 -2.4000e+02 1.0000e+03 -5.9288e+00 -2.4000e+02 1.0286e+03 -5.0321e+00 -2.4000e+02 1.0571e+03 -4.2265e+00 -2.4000e+02 1.0857e+03 -3.4995e+00 -2.4000e+02 1.1143e+03 -2.8403e+00 -2.4000e+02 1.1429e+03 -2.2404e+00 -2.4000e+02 1.1714e+03 -1.6922e+00 -2.4000e+02 1.2000e+03 -1.1896e+00 -2.4000e+02 1.2286e+03 -7.2715e-01 -2.4000e+02 1.2571e+03 -3.0042e-01 -2.4000e+02 1.2857e+03 9.4502e-02 -2.4000e+02 1.3143e+03 4.6097e-01 -2.4000e+02 1.3429e+03 8.0189e-01 -2.4000e+02 1.3714e+03 1.1198e+00 -2.4000e+02 1.4000e+03 1.4169e+00 -2.4000e+02 1.4286e+03 1.6952e+00 -2.4000e+02 1.4571e+03 1.9563e+00 -2.4000e+02 1.4857e+03 2.2018e+00 -2.4000e+02 1.5143e+03 2.4329e+00 -2.4000e+02 1.5429e+03 2.6510e+00 -2.4000e+02 1.5714e+03 2.8571e+00 -2.4000e+02 1.6000e+03 3.0521e+00 -2.4000e+02 1.6286e+03 3.2369e+00 -2.4000e+02 1.6571e+03 3.4122e+00 -2.4000e+02 1.6857e+03 3.5788e+00 -2.4000e+02 1.7143e+03 3.7373e+00 -2.4000e+02 1.7429e+03 3.8882e+00 -2.4000e+02 1.7714e+03 4.0322e+00 -2.4000e+02 1.8000e+03 4.1696e+00 -2.4000e+02 1.8286e+03 4.3008e+00 -2.4000e+02 1.8571e+03 4.4264e+00 -2.4000e+02 1.8857e+03 4.5466e+00 -2.4000e+02 1.9143e+03 4.6617e+00 -2.4000e+02 1.9429e+03 4.7722e+00 -2.4000e+02 1.9714e+03 4.8782e+00 -2.4000e+02 2.0000e+03 4.9801e+00 -2.7000e+02 -2.0000e+03 4.9934e+00 -2.7000e+02 -1.9714e+03 4.8923e+00 -2.7000e+02 -1.9429e+03 4.7870e+00 -2.7000e+02 -1.9143e+03 4.6774e+00 -2.7000e+02 -1.8857e+03 4.5632e+00 -2.7000e+02 -1.8571e+03 4.4440e+00 -2.7000e+02 -1.8286e+03 4.3195e+00 -2.7000e+02 -1.8000e+03 4.1894e+00 -2.7000e+02 -1.7714e+03 4.0533e+00 -2.7000e+02 -1.7429e+03 3.9108e+00 -2.7000e+02 -1.7143e+03 3.7613e+00 -2.7000e+02 -1.6857e+03 3.6045e+00 -2.7000e+02 -1.6571e+03 3.4397e+00 -2.7000e+02 -1.6286e+03 3.2663e+00 -2.7000e+02 -1.6000e+03 3.0837e+00 -2.7000e+02 -1.5714e+03 2.8910e+00 -2.7000e+02 -1.5429e+03 2.6876e+00 -2.7000e+02 -1.5143e+03 2.4724e+00 -2.7000e+02 -1.4857e+03 2.2444e+00 -2.7000e+02 -1.4571e+03 2.0024e+00 -2.7000e+02 -1.4286e+03 1.7453e+00 -2.7000e+02 -1.4000e+03 1.4714e+00 -2.7000e+02 -1.3714e+03 1.1793e+00 -2.7000e+02 -1.3429e+03 8.6691e-01 -2.7000e+02 -1.3143e+03 5.3225e-01 -2.7000e+02 -1.2857e+03 1.7288e-01 -2.7000e+02 -1.2571e+03 -2.1397e-01 -2.7000e+02 -1.2286e+03 -6.3149e-01 -2.7000e+02 -1.2000e+03 -1.0834e+00 -2.7000e+02 -1.1714e+03 -1.5739e+00 -2.7000e+02 -1.1429e+03 -2.1080e+00 -2.7000e+02 -1.1143e+03 -2.6915e+00 -2.7000e+02 -1.0857e+03 -3.3315e+00 -2.7000e+02 -1.0571e+03 -4.0360e+00 -2.7000e+02 -1.0286e+03 -4.8149e+00 -2.7000e+02 -1.0000e+03 -5.6799e+00 -2.7000e+02 -9.7143e+02 -6.6452e+00 -2.7000e+02 -9.4286e+02 -7.7279e+00 -2.7000e+02 -9.1429e+02 -8.9492e+00 -2.7000e+02 -8.8571e+02 -1.0335e+01 -2.7000e+02 -8.5714e+02 -1.1917e+01 -2.7000e+02 -8.2857e+02 -1.3735e+01 -2.7000e+02 -8.0000e+02 -1.5837e+01 -2.7000e+02 -7.7143e+02 -1.8282e+01 -2.7000e+02 -7.4286e+02 -2.1143e+01 -2.7000e+02 -7.1429e+02 -2.4503e+01 -2.7000e+02 -6.8571e+02 -2.8451e+01 -2.7000e+02 -6.5714e+02 -3.3075e+01 -2.7000e+02 -6.2857e+02 -3.8433e+01 -2.7000e+02 -6.0000e+02 -4.4514e+01 -2.7000e+02 -5.7143e+02 -5.1202e+01 -2.7000e+02 -5.4286e+02 -5.8248e+01 -2.7000e+02 -5.1429e+02 -6.5308e+01 -2.7000e+02 -4.8571e+02 -7.2033e+01 -2.7000e+02 -4.5714e+02 -7.8162e+01 -2.7000e+02 -4.2857e+02 -8.3561e+01 -2.7000e+02 -4.0000e+02 -8.8210e+01 -2.7000e+02 -3.7143e+02 -9.2160e+01 -2.7000e+02 -3.4286e+02 -9.5495e+01 -2.7000e+02 -3.1429e+02 -9.8302e+01 -2.7000e+02 -2.8571e+02 -1.0067e+02 -2.7000e+02 -2.5714e+02 -1.0265e+02 -2.7000e+02 -2.2857e+02 -1.0432e+02 -2.7000e+02 -2.0000e+02 -1.0572e+02 -2.7000e+02 -1.7143e+02 -1.0688e+02 -2.7000e+02 -1.4286e+02 -1.0782e+02 -2.7000e+02 -1.1429e+02 -1.0858e+02 -2.7000e+02 -8.5714e+01 -1.0915e+02 -2.7000e+02 -5.7143e+01 -1.0956e+02 -2.7000e+02 -2.8571e+01 -1.0980e+02 -2.7000e+02 0.0000e+00 -1.0988e+02 -2.7000e+02 2.8571e+01 -1.0980e+02 -2.7000e+02 5.7143e+01 -1.0956e+02 -2.7000e+02 8.5714e+01 -1.0915e+02 -2.7000e+02 1.1429e+02 -1.0858e+02 -2.7000e+02 1.4286e+02 -1.0782e+02 -2.7000e+02 1.7143e+02 -1.0688e+02 -2.7000e+02 2.0000e+02 -1.0572e+02 -2.7000e+02 2.2857e+02 -1.0432e+02 -2.7000e+02 2.5714e+02 -1.0265e+02 -2.7000e+02 2.8571e+02 -1.0067e+02 -2.7000e+02 3.1429e+02 -9.8302e+01 -2.7000e+02 3.4286e+02 -9.5495e+01 -2.7000e+02 3.7143e+02 -9.2160e+01 -2.7000e+02 4.0000e+02 -8.8210e+01 -2.7000e+02 4.2857e+02 -8.3561e+01 -2.7000e+02 4.5714e+02 -7.8162e+01 -2.7000e+02 4.8571e+02 -7.2033e+01 -2.7000e+02 5.1429e+02 -6.5308e+01 -2.7000e+02 5.4286e+02 -5.8248e+01 -2.7000e+02 5.7143e+02 -5.1202e+01 -2.7000e+02 6.0000e+02 -4.4514e+01 -2.7000e+02 6.2857e+02 -3.8433e+01 -2.7000e+02 6.5714e+02 -3.3075e+01 -2.7000e+02 6.8571e+02 -2.8451e+01 -2.7000e+02 7.1429e+02 -2.4503e+01 -2.7000e+02 7.4286e+02 -2.1143e+01 -2.7000e+02 7.7143e+02 -1.8282e+01 -2.7000e+02 8.0000e+02 -1.5837e+01 -2.7000e+02 8.2857e+02 -1.3735e+01 -2.7000e+02 8.5714e+02 -1.1917e+01 -2.7000e+02 8.8571e+02 -1.0335e+01 -2.7000e+02 9.1429e+02 -8.9492e+00 -2.7000e+02 9.4286e+02 -7.7279e+00 -2.7000e+02 9.7143e+02 -6.6452e+00 -2.7000e+02 1.0000e+03 -5.6799e+00 -2.7000e+02 1.0286e+03 -4.8149e+00 -2.7000e+02 1.0571e+03 -4.0360e+00 -2.7000e+02 1.0857e+03 -3.3315e+00 -2.7000e+02 1.1143e+03 -2.6915e+00 -2.7000e+02 1.1429e+03 -2.1080e+00 -2.7000e+02 1.1714e+03 -1.5739e+00 -2.7000e+02 1.2000e+03 -1.0834e+00 -2.7000e+02 1.2286e+03 -6.3149e-01 -2.7000e+02 1.2571e+03 -2.1397e-01 -2.7000e+02 1.2857e+03 1.7288e-01 -2.7000e+02 1.3143e+03 5.3225e-01 -2.7000e+02 1.3429e+03 8.6691e-01 -2.7000e+02 1.3714e+03 1.1793e+00 -2.7000e+02 1.4000e+03 1.4714e+00 -2.7000e+02 1.4286e+03 1.7453e+00 -2.7000e+02 1.4571e+03 2.0024e+00 -2.7000e+02 1.4857e+03 2.2444e+00 -2.7000e+02 1.5143e+03 2.4724e+00 -2.7000e+02 1.5429e+03 2.6876e+00 -2.7000e+02 1.5714e+03 2.8910e+00 -2.7000e+02 1.6000e+03 3.0837e+00 -2.7000e+02 1.6286e+03 3.2663e+00 -2.7000e+02 1.6571e+03 3.4397e+00 -2.7000e+02 1.6857e+03 3.6045e+00 -2.7000e+02 1.7143e+03 3.7613e+00 -2.7000e+02 1.7429e+03 3.9108e+00 -2.7000e+02 1.7714e+03 4.0533e+00 -2.7000e+02 1.8000e+03 4.1894e+00 -2.7000e+02 1.8286e+03 4.3195e+00 -2.7000e+02 1.8571e+03 4.4440e+00 -2.7000e+02 1.8857e+03 4.5632e+00 -2.7000e+02 1.9143e+03 4.6774e+00 -2.7000e+02 1.9429e+03 4.7870e+00 -2.7000e+02 1.9714e+03 4.8923e+00 -2.7000e+02 2.0000e+03 4.9934e+00 -3.0000e+02 -2.0000e+03 5.0081e+00 -3.0000e+02 -1.9714e+03 4.9079e+00 -3.0000e+02 -1.9429e+03 4.8035e+00 -3.0000e+02 -1.9143e+03 4.6949e+00 -3.0000e+02 -1.8857e+03 4.5816e+00 -3.0000e+02 -1.8571e+03 4.4635e+00 -3.0000e+02 -1.8286e+03 4.3403e+00 -3.0000e+02 -1.8000e+03 4.2114e+00 -3.0000e+02 -1.7714e+03 4.0767e+00 -3.0000e+02 -1.7429e+03 3.9357e+00 -3.0000e+02 -1.7143e+03 3.7879e+00 -3.0000e+02 -1.6857e+03 3.6328e+00 -3.0000e+02 -1.6571e+03 3.4700e+00 -3.0000e+02 -1.6286e+03 3.2988e+00 -3.0000e+02 -1.6000e+03 3.1185e+00 -3.0000e+02 -1.5714e+03 2.9285e+00 -3.0000e+02 -1.5429e+03 2.7278e+00 -3.0000e+02 -1.5143e+03 2.5158e+00 -3.0000e+02 -1.4857e+03 2.2913e+00 -3.0000e+02 -1.4571e+03 2.0532e+00 -3.0000e+02 -1.4286e+03 1.8004e+00 -3.0000e+02 -1.4000e+03 1.5313e+00 -3.0000e+02 -1.3714e+03 1.2445e+00 -3.0000e+02 -1.3429e+03 9.3820e-01 -3.0000e+02 -1.3143e+03 6.1034e-01 -3.0000e+02 -1.2857e+03 2.5865e-01 -3.0000e+02 -1.2571e+03 -1.1948e-01 -3.0000e+02 -1.2286e+03 -5.2707e-01 -3.0000e+02 -1.2000e+03 -9.6758e-01 -3.0000e+02 -1.1714e+03 -1.4450e+00 -3.0000e+02 -1.1429e+03 -1.9640e+00 -3.0000e+02 -1.1143e+03 -2.5301e+00 -3.0000e+02 -1.0857e+03 -3.1497e+00 -3.0000e+02 -1.0571e+03 -3.8303e+00 -3.0000e+02 -1.0286e+03 -4.5809e+00 -3.0000e+02 -1.0000e+03 -5.4124e+00 -3.0000e+02 -9.7143e+02 -6.3375e+00 -3.0000e+02 -9.4286e+02 -7.3720e+00 -3.0000e+02 -9.1429e+02 -8.5348e+00 -3.0000e+02 -8.8571e+02 -9.8491e+00 -3.0000e+02 -8.5714e+02 -1.1343e+01 -3.0000e+02 -8.2857e+02 -1.3052e+01 -3.0000e+02 -8.0000e+02 -1.5019e+01 -3.0000e+02 -7.7143e+02 -1.7295e+01 -3.0000e+02 -7.4286e+02 -1.9944e+01 -3.0000e+02 -7.1429e+02 -2.3040e+01 -3.0000e+02 -6.8571e+02 -2.6665e+01 -3.0000e+02 -6.5714e+02 -3.0902e+01 -3.0000e+02 -6.2857e+02 -3.5816e+01 -3.0000e+02 -6.0000e+02 -4.1430e+01 -3.0000e+02 -5.7143e+02 -4.7681e+01 -3.0000e+02 -5.4286e+02 -5.4393e+01 -3.0000e+02 -5.1429e+02 -6.1284e+01 -3.0000e+02 -4.8571e+02 -6.8019e+01 -3.0000e+02 -4.5714e+02 -7.4307e+01 -3.0000e+02 -4.2857e+02 -7.9957e+01 -3.0000e+02 -4.0000e+02 -8.4893e+01 -3.0000e+02 -3.7143e+02 -8.9128e+01 -3.0000e+02 -3.4286e+02 -9.2722e+01 -3.0000e+02 -3.1429e+02 -9.5755e+01 -3.0000e+02 -2.8571e+02 -9.8309e+01 -3.0000e+02 -2.5714e+02 -1.0045e+02 -3.0000e+02 -2.2857e+02 -1.0225e+02 -3.0000e+02 -2.0000e+02 -1.0375e+02 -3.0000e+02 -1.7143e+02 -1.0499e+02 -3.0000e+02 -1.4286e+02 -1.0599e+02 -3.0000e+02 -1.1429e+02 -1.0679e+02 -3.0000e+02 -8.5714e+01 -1.0740e+02 -3.0000e+02 -5.7143e+01 -1.0783e+02 -3.0000e+02 -2.8571e+01 -1.0808e+02 -3.0000e+02 0.0000e+00 -1.0817e+02 -3.0000e+02 2.8571e+01 -1.0808e+02 -3.0000e+02 5.7143e+01 -1.0783e+02 -3.0000e+02 8.5714e+01 -1.0740e+02 -3.0000e+02 1.1429e+02 -1.0679e+02 -3.0000e+02 1.4286e+02 -1.0599e+02 -3.0000e+02 1.7143e+02 -1.0499e+02 -3.0000e+02 2.0000e+02 -1.0375e+02 -3.0000e+02 2.2857e+02 -1.0225e+02 -3.0000e+02 2.5714e+02 -1.0045e+02 -3.0000e+02 2.8571e+02 -9.8309e+01 -3.0000e+02 3.1429e+02 -9.5755e+01 -3.0000e+02 3.4286e+02 -9.2722e+01 -3.0000e+02 3.7143e+02 -8.9128e+01 -3.0000e+02 4.0000e+02 -8.4893e+01 -3.0000e+02 4.2857e+02 -7.9957e+01 -3.0000e+02 4.5714e+02 -7.4307e+01 -3.0000e+02 4.8571e+02 -6.8019e+01 -3.0000e+02 5.1429e+02 -6.1284e+01 -3.0000e+02 5.4286e+02 -5.4393e+01 -3.0000e+02 5.7143e+02 -4.7681e+01 -3.0000e+02 6.0000e+02 -4.1430e+01 -3.0000e+02 6.2857e+02 -3.5816e+01 -3.0000e+02 6.5714e+02 -3.0902e+01 -3.0000e+02 6.8571e+02 -2.6665e+01 -3.0000e+02 7.1429e+02 -2.3040e+01 -3.0000e+02 7.4286e+02 -1.9944e+01 -3.0000e+02 7.7143e+02 -1.7295e+01 -3.0000e+02 8.0000e+02 -1.5019e+01 -3.0000e+02 8.2857e+02 -1.3052e+01 -3.0000e+02 8.5714e+02 -1.1343e+01 -3.0000e+02 8.8571e+02 -9.8491e+00 -3.0000e+02 9.1429e+02 -8.5348e+00 -3.0000e+02 9.4286e+02 -7.3720e+00 -3.0000e+02 9.7143e+02 -6.3375e+00 -3.0000e+02 1.0000e+03 -5.4124e+00 -3.0000e+02 1.0286e+03 -4.5809e+00 -3.0000e+02 1.0571e+03 -3.8303e+00 -3.0000e+02 1.0857e+03 -3.1497e+00 -3.0000e+02 1.1143e+03 -2.5301e+00 -3.0000e+02 1.1429e+03 -1.9640e+00 -3.0000e+02 1.1714e+03 -1.4450e+00 -3.0000e+02 1.2000e+03 -9.6758e-01 -3.0000e+02 1.2286e+03 -5.2707e-01 -3.0000e+02 1.2571e+03 -1.1948e-01 -3.0000e+02 1.2857e+03 2.5865e-01 -3.0000e+02 1.3143e+03 6.1034e-01 -3.0000e+02 1.3429e+03 9.3820e-01 -3.0000e+02 1.3714e+03 1.2445e+00 -3.0000e+02 1.4000e+03 1.5313e+00 -3.0000e+02 1.4286e+03 1.8004e+00 -3.0000e+02 1.4571e+03 2.0532e+00 -3.0000e+02 1.4857e+03 2.2913e+00 -3.0000e+02 1.5143e+03 2.5158e+00 -3.0000e+02 1.5429e+03 2.7278e+00 -3.0000e+02 1.5714e+03 2.9285e+00 -3.0000e+02 1.6000e+03 3.1185e+00 -3.0000e+02 1.6286e+03 3.2988e+00 -3.0000e+02 1.6571e+03 3.4700e+00 -3.0000e+02 1.6857e+03 3.6328e+00 -3.0000e+02 1.7143e+03 3.7879e+00 -3.0000e+02 1.7429e+03 3.9357e+00 -3.0000e+02 1.7714e+03 4.0767e+00 -3.0000e+02 1.8000e+03 4.2114e+00 -3.0000e+02 1.8286e+03 4.3403e+00 -3.0000e+02 1.8571e+03 4.4635e+00 -3.0000e+02 1.8857e+03 4.5816e+00 -3.0000e+02 1.9143e+03 4.6949e+00 -3.0000e+02 1.9429e+03 4.8035e+00 -3.0000e+02 1.9714e+03 4.9079e+00 -3.0000e+02 2.0000e+03 5.0081e+00 -3.3000e+02 -2.0000e+03 5.0243e+00 -3.3000e+02 -1.9714e+03 4.9249e+00 -3.3000e+02 -1.9429e+03 4.8215e+00 -3.3000e+02 -1.9143e+03 4.7139e+00 -3.3000e+02 -1.8857e+03 4.6018e+00 -3.3000e+02 -1.8571e+03 4.4849e+00 -3.3000e+02 -1.8286e+03 4.3629e+00 -3.3000e+02 -1.8000e+03 4.2355e+00 -3.3000e+02 -1.7714e+03 4.1023e+00 -3.3000e+02 -1.7429e+03 3.9629e+00 -3.3000e+02 -1.7143e+03 3.8169e+00 -3.3000e+02 -1.6857e+03 3.6638e+00 -3.3000e+02 -1.6571e+03 3.5031e+00 -3.3000e+02 -1.6286e+03 3.3342e+00 -3.3000e+02 -1.6000e+03 3.1565e+00 -3.3000e+02 -1.5714e+03 2.9692e+00 -3.3000e+02 -1.5429e+03 2.7717e+00 -3.3000e+02 -1.5143e+03 2.5630e+00 -3.3000e+02 -1.4857e+03 2.3423e+00 -3.3000e+02 -1.4571e+03 2.1084e+00 -3.3000e+02 -1.4286e+03 1.8601e+00 -3.3000e+02 -1.4000e+03 1.5963e+00 -3.3000e+02 -1.3714e+03 1.3152e+00 -3.3000e+02 -1.3429e+03 1.0154e+00 -3.3000e+02 -1.3143e+03 6.9477e-01 -3.3000e+02 -1.2857e+03 3.5128e-01 -3.3000e+02 -1.2571e+03 -1.7572e-02 -3.3000e+02 -1.2286e+03 -4.1460e-01 -3.3000e+02 -1.2000e+03 -8.4306e-01 -3.3000e+02 -1.1714e+03 -1.3067e+00 -3.3000e+02 -1.1429e+03 -1.8098e+00 -3.3000e+02 -1.1143e+03 -2.3575e+00 -3.3000e+02 -1.0857e+03 -2.9556e+00 -3.3000e+02 -1.0571e+03 -3.6112e+00 -3.3000e+02 -1.0286e+03 -4.3324e+00 -3.3000e+02 -1.0000e+03 -5.1290e+00 -3.3000e+02 -9.7143e+02 -6.0127e+00 -3.3000e+02 -9.4286e+02 -6.9975e+00 -3.3000e+02 -9.1429e+02 -8.1004e+00 -3.3000e+02 -8.8571e+02 -9.3419e+00 -3.3000e+02 -8.5714e+02 -1.0747e+01 -3.3000e+02 -8.2857e+02 -1.2346e+01 -3.3000e+02 -8.0000e+02 -1.4177e+01 -3.3000e+02 -7.7143e+02 -1.6285e+01 -3.3000e+02 -7.4286e+02 -1.8724e+01 -3.3000e+02 -7.1429e+02 -2.1559e+01 -3.3000e+02 -6.8571e+02 -2.4863e+01 -3.3000e+02 -6.5714e+02 -2.8711e+01 -3.3000e+02 -6.2857e+02 -3.3173e+01 -3.3000e+02 -6.0000e+02 -3.8290e+01 -3.3000e+02 -5.7143e+02 -4.4043e+01 -3.3000e+02 -5.4286e+02 -5.0326e+01 -3.3000e+02 -5.1429e+02 -5.6925e+01 -3.3000e+02 -4.8571e+02 -6.3554e+01 -3.3000e+02 -4.5714e+02 -6.9913e+01 -3.3000e+02 -4.2857e+02 -7.5768e+01 -3.3000e+02 -4.0000e+02 -8.0982e+01 -3.3000e+02 -3.7143e+02 -8.5519e+01 -3.3000e+02 -3.4286e+02 -8.9404e+01 -3.3000e+02 -3.1429e+02 -9.2701e+01 -3.3000e+02 -2.8571e+02 -9.5482e+01 -3.3000e+02 -2.5714e+02 -9.7819e+01 -3.3000e+02 -2.2857e+02 -9.9775e+01 -3.3000e+02 -2.0000e+02 -1.0140e+02 -3.3000e+02 -1.7143e+02 -1.0274e+02 -3.3000e+02 -1.4286e+02 -1.0383e+02 -3.3000e+02 -1.1429e+02 -1.0469e+02 -3.3000e+02 -8.5714e+01 -1.0534e+02 -3.3000e+02 -5.7143e+01 -1.0580e+02 -3.3000e+02 -2.8571e+01 -1.0607e+02 -3.3000e+02 0.0000e+00 -1.0616e+02 -3.3000e+02 2.8571e+01 -1.0607e+02 -3.3000e+02 5.7143e+01 -1.0580e+02 -3.3000e+02 8.5714e+01 -1.0534e+02 -3.3000e+02 1.1429e+02 -1.0469e+02 -3.3000e+02 1.4286e+02 -1.0383e+02 -3.3000e+02 1.7143e+02 -1.0274e+02 -3.3000e+02 2.0000e+02 -1.0140e+02 -3.3000e+02 2.2857e+02 -9.9775e+01 -3.3000e+02 2.5714e+02 -9.7819e+01 -3.3000e+02 2.8571e+02 -9.5482e+01 -3.3000e+02 3.1429e+02 -9.2701e+01 -3.3000e+02 3.4286e+02 -8.9404e+01 -3.3000e+02 3.7143e+02 -8.5519e+01 -3.3000e+02 4.0000e+02 -8.0982e+01 -3.3000e+02 4.2857e+02 -7.5768e+01 -3.3000e+02 4.5714e+02 -6.9913e+01 -3.3000e+02 4.8571e+02 -6.3554e+01 -3.3000e+02 5.1429e+02 -5.6925e+01 -3.3000e+02 5.4286e+02 -5.0326e+01 -3.3000e+02 5.7143e+02 -4.4043e+01 -3.3000e+02 6.0000e+02 -3.8290e+01 -3.3000e+02 6.2857e+02 -3.3173e+01 -3.3000e+02 6.5714e+02 -2.8711e+01 -3.3000e+02 6.8571e+02 -2.4863e+01 -3.3000e+02 7.1429e+02 -2.1559e+01 -3.3000e+02 7.4286e+02 -1.8724e+01 -3.3000e+02 7.7143e+02 -1.6285e+01 -3.3000e+02 8.0000e+02 -1.4177e+01 -3.3000e+02 8.2857e+02 -1.2346e+01 -3.3000e+02 8.5714e+02 -1.0747e+01 -3.3000e+02 8.8571e+02 -9.3419e+00 -3.3000e+02 9.1429e+02 -8.1004e+00 -3.3000e+02 9.4286e+02 -6.9975e+00 -3.3000e+02 9.7143e+02 -6.0127e+00 -3.3000e+02 1.0000e+03 -5.1290e+00 -3.3000e+02 1.0286e+03 -4.3324e+00 -3.3000e+02 1.0571e+03 -3.6112e+00 -3.3000e+02 1.0857e+03 -2.9556e+00 -3.3000e+02 1.1143e+03 -2.3575e+00 -3.3000e+02 1.1429e+03 -1.8098e+00 -3.3000e+02 1.1714e+03 -1.3067e+00 -3.3000e+02 1.2000e+03 -8.4306e-01 -3.3000e+02 1.2286e+03 -4.1460e-01 -3.3000e+02 1.2571e+03 -1.7572e-02 -3.3000e+02 1.2857e+03 3.5128e-01 -3.3000e+02 1.3143e+03 6.9477e-01 -3.3000e+02 1.3429e+03 1.0154e+00 -3.3000e+02 1.3714e+03 1.3152e+00 -3.3000e+02 1.4000e+03 1.5963e+00 -3.3000e+02 1.4286e+03 1.8601e+00 -3.3000e+02 1.4571e+03 2.1084e+00 -3.3000e+02 1.4857e+03 2.3423e+00 -3.3000e+02 1.5143e+03 2.5630e+00 -3.3000e+02 1.5429e+03 2.7717e+00 -3.3000e+02 1.5714e+03 2.9692e+00 -3.3000e+02 1.6000e+03 3.1565e+00 -3.3000e+02 1.6286e+03 3.3342e+00 -3.3000e+02 1.6571e+03 3.5031e+00 -3.3000e+02 1.6857e+03 3.6638e+00 -3.3000e+02 1.7143e+03 3.8169e+00 -3.3000e+02 1.7429e+03 3.9629e+00 -3.3000e+02 1.7714e+03 4.1023e+00 -3.3000e+02 1.8000e+03 4.2355e+00 -3.3000e+02 1.8286e+03 4.3629e+00 -3.3000e+02 1.8571e+03 4.4849e+00 -3.3000e+02 1.8857e+03 4.6018e+00 -3.3000e+02 1.9143e+03 4.7139e+00 -3.3000e+02 1.9429e+03 4.8215e+00 -3.3000e+02 1.9714e+03 4.9249e+00 -3.3000e+02 2.0000e+03 5.0243e+00 -3.6000e+02 -2.0000e+03 5.0419e+00 -3.6000e+02 -1.9714e+03 4.9435e+00 -3.6000e+02 -1.9429e+03 4.8411e+00 -3.6000e+02 -1.9143e+03 4.7346e+00 -3.6000e+02 -1.8857e+03 4.6237e+00 -3.6000e+02 -1.8571e+03 4.5081e+00 -3.6000e+02 -1.8286e+03 4.3875e+00 -3.6000e+02 -1.8000e+03 4.2616e+00 -3.6000e+02 -1.7714e+03 4.1300e+00 -3.6000e+02 -1.7429e+03 3.9924e+00 -3.6000e+02 -1.7143e+03 3.8483e+00 -3.6000e+02 -1.6857e+03 3.6973e+00 -3.6000e+02 -1.6571e+03 3.5388e+00 -3.6000e+02 -1.6286e+03 3.3724e+00 -3.6000e+02 -1.6000e+03 3.1974e+00 -3.6000e+02 -1.5714e+03 3.0132e+00 -3.6000e+02 -1.5429e+03 2.8189e+00 -3.6000e+02 -1.5143e+03 2.6139e+00 -3.6000e+02 -1.4857e+03 2.3971e+00 -3.6000e+02 -1.4571e+03 2.1677e+00 -3.6000e+02 -1.4286e+03 1.9244e+00 -3.6000e+02 -1.4000e+03 1.6659e+00 -3.6000e+02 -1.3714e+03 1.3910e+00 -3.6000e+02 -1.3429e+03 1.0980e+00 -3.6000e+02 -1.3143e+03 7.8505e-01 -3.6000e+02 -1.2857e+03 4.5020e-01 -3.6000e+02 -1.2571e+03 9.1122e-02 -3.6000e+02 -1.2286e+03 -2.9483e-01 -3.6000e+02 -1.2000e+03 -7.1066e-01 -3.6000e+02 -1.1714e+03 -1.1599e+00 -3.6000e+02 -1.1429e+03 -1.6464e+00 -3.6000e+02 -1.1143e+03 -2.1749e+00 -3.6000e+02 -1.0857e+03 -2.7509e+00 -3.6000e+02 -1.0571e+03 -3.3806e+00 -3.6000e+02 -1.0286e+03 -4.0716e+00 -3.6000e+02 -1.0000e+03 -4.8325e+00 -3.6000e+02 -9.7143e+02 -5.6739e+00 -3.6000e+02 -9.4286e+02 -6.6084e+00 -3.6000e+02 -9.1429e+02 -7.6508e+00 -3.6000e+02 -8.8571e+02 -8.8192e+00 -3.6000e+02 -8.5714e+02 -1.0136e+01 -3.6000e+02 -8.2857e+02 -1.1626e+01 -3.6000e+02 -8.0000e+02 -1.3324e+01 -3.6000e+02 -7.7143e+02 -1.5266e+01 -3.6000e+02 -7.4286e+02 -1.7501e+01 -3.6000e+02 -7.1429e+02 -2.0082e+01 -3.6000e+02 -6.8571e+02 -2.3073e+01 -3.6000e+02 -6.5714e+02 -2.6542e+01 -3.6000e+02 -6.2857e+02 -3.0556e+01 -3.6000e+02 -6.0000e+02 -3.5164e+01 -3.6000e+02 -5.7143e+02 -4.0381e+01 -3.6000e+02 -5.4286e+02 -4.6154e+01 -3.6000e+02 -5.1429e+02 -5.2343e+01 -3.6000e+02 -4.8571e+02 -5.8724e+01 -3.6000e+02 -4.5714e+02 -6.5027e+01 -3.6000e+02 -4.2857e+02 -7.0993e+01 -3.6000e+02 -4.0000e+02 -7.6439e+01 -3.6000e+02 -3.7143e+02 -8.1268e+01 -3.6000e+02 -3.4286e+02 -8.5461e+01 -3.6000e+02 -3.1429e+02 -8.9051e+01 -3.6000e+02 -2.8571e+02 -9.2096e+01 -3.6000e+02 -2.5714e+02 -9.4663e+01 -3.6000e+02 -2.2857e+02 -9.6812e+01 -3.6000e+02 -2.0000e+02 -9.8598e+01 -3.6000e+02 -1.7143e+02 -1.0007e+02 -3.6000e+02 -1.4286e+02 -1.0126e+02 -3.6000e+02 -1.1429e+02 -1.0220e+02 -3.6000e+02 -8.5714e+01 -1.0291e+02 -3.6000e+02 -5.7143e+01 -1.0341e+02 -3.6000e+02 -2.8571e+01 -1.0370e+02 -3.6000e+02 0.0000e+00 -1.0380e+02 -3.6000e+02 2.8571e+01 -1.0370e+02 -3.6000e+02 5.7143e+01 -1.0341e+02 -3.6000e+02 8.5714e+01 -1.0291e+02 -3.6000e+02 1.1429e+02 -1.0220e+02 -3.6000e+02 1.4286e+02 -1.0126e+02 -3.6000e+02 1.7143e+02 -1.0007e+02 -3.6000e+02 2.0000e+02 -9.8598e+01 -3.6000e+02 2.2857e+02 -9.6812e+01 -3.6000e+02 2.5714e+02 -9.4663e+01 -3.6000e+02 2.8571e+02 -9.2096e+01 -3.6000e+02 3.1429e+02 -8.9051e+01 -3.6000e+02 3.4286e+02 -8.5461e+01 -3.6000e+02 3.7143e+02 -8.1268e+01 -3.6000e+02 4.0000e+02 -7.6439e+01 -3.6000e+02 4.2857e+02 -7.0993e+01 -3.6000e+02 4.5714e+02 -6.5027e+01 -3.6000e+02 4.8571e+02 -5.8724e+01 -3.6000e+02 5.1429e+02 -5.2343e+01 -3.6000e+02 5.4286e+02 -4.6154e+01 -3.6000e+02 5.7143e+02 -4.0381e+01 -3.6000e+02 6.0000e+02 -3.5164e+01 -3.6000e+02 6.2857e+02 -3.0556e+01 -3.6000e+02 6.5714e+02 -2.6542e+01 -3.6000e+02 6.8571e+02 -2.3073e+01 -3.6000e+02 7.1429e+02 -2.0082e+01 -3.6000e+02 7.4286e+02 -1.7501e+01 -3.6000e+02 7.7143e+02 -1.5266e+01 -3.6000e+02 8.0000e+02 -1.3324e+01 -3.6000e+02 8.2857e+02 -1.1626e+01 -3.6000e+02 8.5714e+02 -1.0136e+01 -3.6000e+02 8.8571e+02 -8.8192e+00 -3.6000e+02 9.1429e+02 -7.6508e+00 -3.6000e+02 9.4286e+02 -6.6084e+00 -3.6000e+02 9.7143e+02 -5.6739e+00 -3.6000e+02 1.0000e+03 -4.8325e+00 -3.6000e+02 1.0286e+03 -4.0716e+00 -3.6000e+02 1.0571e+03 -3.3806e+00 -3.6000e+02 1.0857e+03 -2.7509e+00 -3.6000e+02 1.1143e+03 -2.1749e+00 -3.6000e+02 1.1429e+03 -1.6464e+00 -3.6000e+02 1.1714e+03 -1.1599e+00 -3.6000e+02 1.2000e+03 -7.1066e-01 -3.6000e+02 1.2286e+03 -2.9483e-01 -3.6000e+02 1.2571e+03 9.1122e-02 -3.6000e+02 1.2857e+03 4.5020e-01 -3.6000e+02 1.3143e+03 7.8505e-01 -3.6000e+02 1.3429e+03 1.0980e+00 -3.6000e+02 1.3714e+03 1.3910e+00 -3.6000e+02 1.4000e+03 1.6659e+00 -3.6000e+02 1.4286e+03 1.9244e+00 -3.6000e+02 1.4571e+03 2.1677e+00 -3.6000e+02 1.4857e+03 2.3971e+00 -3.6000e+02 1.5143e+03 2.6139e+00 -3.6000e+02 1.5429e+03 2.8189e+00 -3.6000e+02 1.5714e+03 3.0132e+00 -3.6000e+02 1.6000e+03 3.1974e+00 -3.6000e+02 1.6286e+03 3.3724e+00 -3.6000e+02 1.6571e+03 3.5388e+00 -3.6000e+02 1.6857e+03 3.6973e+00 -3.6000e+02 1.7143e+03 3.8483e+00 -3.6000e+02 1.7429e+03 3.9924e+00 -3.6000e+02 1.7714e+03 4.1300e+00 -3.6000e+02 1.8000e+03 4.2616e+00 -3.6000e+02 1.8286e+03 4.3875e+00 -3.6000e+02 1.8571e+03 4.5081e+00 -3.6000e+02 1.8857e+03 4.6237e+00 -3.6000e+02 1.9143e+03 4.7346e+00 -3.6000e+02 1.9429e+03 4.8411e+00 -3.6000e+02 1.9714e+03 4.9435e+00 -3.6000e+02 2.0000e+03 5.0419e+00 -3.9000e+02 -2.0000e+03 5.0608e+00 -3.9000e+02 -1.9714e+03 4.9634e+00 -3.9000e+02 -1.9429e+03 4.8622e+00 -3.9000e+02 -1.9143e+03 4.7568e+00 -3.9000e+02 -1.8857e+03 4.6472e+00 -3.9000e+02 -1.8571e+03 4.5329e+00 -3.9000e+02 -1.8286e+03 4.4138e+00 -3.9000e+02 -1.8000e+03 4.2895e+00 -3.9000e+02 -1.7714e+03 4.1597e+00 -3.9000e+02 -1.7429e+03 4.0240e+00 -3.9000e+02 -1.7143e+03 3.8819e+00 -3.9000e+02 -1.6857e+03 3.7331e+00 -3.9000e+02 -1.6571e+03 3.5771e+00 -3.9000e+02 -1.6286e+03 3.4133e+00 -3.9000e+02 -1.6000e+03 3.2412e+00 -3.9000e+02 -1.5714e+03 3.0601e+00 -3.9000e+02 -1.5429e+03 2.8694e+00 -3.9000e+02 -1.5143e+03 2.6682e+00 -3.9000e+02 -1.4857e+03 2.4556e+00 -3.9000e+02 -1.4571e+03 2.2308e+00 -3.9000e+02 -1.4286e+03 1.9927e+00 -3.9000e+02 -1.4000e+03 1.7400e+00 -3.9000e+02 -1.3714e+03 1.4715e+00 -3.9000e+02 -1.3429e+03 1.1856e+00 -3.9000e+02 -1.3143e+03 8.8070e-01 -3.9000e+02 -1.2857e+03 5.5486e-01 -3.9000e+02 -1.2571e+03 2.0595e-01 -3.9000e+02 -1.2286e+03 -1.6850e-01 -3.9000e+02 -1.2000e+03 -5.7126e-01 -3.9000e+02 -1.1714e+03 -1.0055e+00 -3.9000e+02 -1.1429e+03 -1.4750e+00 -3.9000e+02 -1.1143e+03 -1.9839e+00 -3.9000e+02 -1.0857e+03 -2.5372e+00 -3.9000e+02 -1.0571e+03 -3.1405e+00 -3.9000e+02 -1.0286e+03 -3.8007e+00 -3.9000e+02 -1.0000e+03 -4.5256e+00 -3.9000e+02 -9.7143e+02 -5.3245e+00 -3.9000e+02 -9.4286e+02 -6.2084e+00 -3.9000e+02 -9.1429e+02 -7.1905e+00 -3.9000e+02 -8.8571e+02 -8.2866e+00 -3.9000e+02 -8.5714e+02 -9.5155e+00 -3.9000e+02 -8.2857e+02 -1.0900e+01 -3.9000e+02 -8.0000e+02 -1.2467e+01 -3.9000e+02 -7.7143e+02 -1.4250e+01 -3.9000e+02 -7.4286e+02 -1.6289e+01 -3.9000e+02 -7.1429e+02 -1.8628e+01 -3.9000e+02 -6.8571e+02 -2.1322e+01 -3.9000e+02 -6.5714e+02 -2.4428e+01 -3.9000e+02 -6.2857e+02 -2.8009e+01 -3.9000e+02 -6.0000e+02 -3.2116e+01 -3.9000e+02 -5.7143e+02 -3.6781e+01 -3.9000e+02 -5.4286e+02 -4.1991e+01 -3.9000e+02 -5.1429e+02 -4.7669e+01 -3.9000e+02 -4.8571e+02 -5.3662e+01 -3.9000e+02 -4.5714e+02 -5.9750e+01 -3.9000e+02 -4.2857e+02 -6.5690e+01 -3.9000e+02 -4.0000e+02 -7.1268e+01 -3.9000e+02 -3.7143e+02 -7.6336e+01 -3.9000e+02 -3.4286e+02 -8.0822e+01 -3.9000e+02 -3.1429e+02 -8.4717e+01 -3.9000e+02 -2.8571e+02 -8.8054e+01 -3.9000e+02 -2.5714e+02 -9.0882e+01 -3.9000e+02 -2.2857e+02 -9.3258e+01 -3.9000e+02 -2.0000e+02 -9.5237e+01 -3.9000e+02 -1.7143e+02 -9.6865e+01 -3.9000e+02 -1.4286e+02 -9.8183e+01 -3.9000e+02 -1.1429e+02 -9.9222e+01 -3.9000e+02 -8.5714e+01 -1.0001e+02 -3.9000e+02 -5.7143e+01 -1.0055e+02 -3.9000e+02 -2.8571e+01 -1.0088e+02 -3.9000e+02 0.0000e+00 -1.0099e+02 -3.9000e+02 2.8571e+01 -1.0088e+02 -3.9000e+02 5.7143e+01 -1.0055e+02 -3.9000e+02 8.5714e+01 -1.0001e+02 -3.9000e+02 1.1429e+02 -9.9222e+01 -3.9000e+02 1.4286e+02 -9.8183e+01 -3.9000e+02 1.7143e+02 -9.6865e+01 -3.9000e+02 2.0000e+02 -9.5237e+01 -3.9000e+02 2.2857e+02 -9.3258e+01 -3.9000e+02 2.5714e+02 -9.0882e+01 -3.9000e+02 2.8571e+02 -8.8054e+01 -3.9000e+02 3.1429e+02 -8.4717e+01 -3.9000e+02 3.4286e+02 -8.0822e+01 -3.9000e+02 3.7143e+02 -7.6336e+01 -3.9000e+02 4.0000e+02 -7.1268e+01 -3.9000e+02 4.2857e+02 -6.5690e+01 -3.9000e+02 4.5714e+02 -5.9750e+01 -3.9000e+02 4.8571e+02 -5.3662e+01 -3.9000e+02 5.1429e+02 -4.7669e+01 -3.9000e+02 5.4286e+02 -4.1991e+01 -3.9000e+02 5.7143e+02 -3.6781e+01 -3.9000e+02 6.0000e+02 -3.2116e+01 -3.9000e+02 6.2857e+02 -2.8009e+01 -3.9000e+02 6.5714e+02 -2.4428e+01 -3.9000e+02 6.8571e+02 -2.1322e+01 -3.9000e+02 7.1429e+02 -1.8628e+01 -3.9000e+02 7.4286e+02 -1.6289e+01 -3.9000e+02 7.7143e+02 -1.4250e+01 -3.9000e+02 8.0000e+02 -1.2467e+01 -3.9000e+02 8.2857e+02 -1.0900e+01 -3.9000e+02 8.5714e+02 -9.5155e+00 -3.9000e+02 8.8571e+02 -8.2866e+00 -3.9000e+02 9.1429e+02 -7.1905e+00 -3.9000e+02 9.4286e+02 -6.2084e+00 -3.9000e+02 9.7143e+02 -5.3245e+00 -3.9000e+02 1.0000e+03 -4.5256e+00 -3.9000e+02 1.0286e+03 -3.8007e+00 -3.9000e+02 1.0571e+03 -3.1405e+00 -3.9000e+02 1.0857e+03 -2.5372e+00 -3.9000e+02 1.1143e+03 -1.9839e+00 -3.9000e+02 1.1429e+03 -1.4750e+00 -3.9000e+02 1.1714e+03 -1.0055e+00 -3.9000e+02 1.2000e+03 -5.7126e-01 -3.9000e+02 1.2286e+03 -1.6850e-01 -3.9000e+02 1.2571e+03 2.0595e-01 -3.9000e+02 1.2857e+03 5.5486e-01 -3.9000e+02 1.3143e+03 8.8070e-01 -3.9000e+02 1.3429e+03 1.1856e+00 -3.9000e+02 1.3714e+03 1.4715e+00 -3.9000e+02 1.4000e+03 1.7400e+00 -3.9000e+02 1.4286e+03 1.9927e+00 -3.9000e+02 1.4571e+03 2.2308e+00 -3.9000e+02 1.4857e+03 2.4556e+00 -3.9000e+02 1.5143e+03 2.6682e+00 -3.9000e+02 1.5429e+03 2.8694e+00 -3.9000e+02 1.5714e+03 3.0601e+00 -3.9000e+02 1.6000e+03 3.2412e+00 -3.9000e+02 1.6286e+03 3.4133e+00 -3.9000e+02 1.6571e+03 3.5771e+00 -3.9000e+02 1.6857e+03 3.7331e+00 -3.9000e+02 1.7143e+03 3.8819e+00 -3.9000e+02 1.7429e+03 4.0240e+00 -3.9000e+02 1.7714e+03 4.1597e+00 -3.9000e+02 1.8000e+03 4.2895e+00 -3.9000e+02 1.8286e+03 4.4138e+00 -3.9000e+02 1.8571e+03 4.5329e+00 -3.9000e+02 1.8857e+03 4.6472e+00 -3.9000e+02 1.9143e+03 4.7568e+00 -3.9000e+02 1.9429e+03 4.8622e+00 -3.9000e+02 1.9714e+03 4.9634e+00 -3.9000e+02 2.0000e+03 5.0608e+00 -4.2000e+02 -2.0000e+03 5.0810e+00 -4.2000e+02 -1.9714e+03 4.9847e+00 -4.2000e+02 -1.9429e+03 4.8846e+00 -4.2000e+02 -1.9143e+03 4.7805e+00 -4.2000e+02 -1.8857e+03 4.6723e+00 -4.2000e+02 -1.8571e+03 4.5595e+00 -4.2000e+02 -1.8286e+03 4.4419e+00 -4.2000e+02 -1.8000e+03 4.3193e+00 -4.2000e+02 -1.7714e+03 4.1913e+00 -4.2000e+02 -1.7429e+03 4.0576e+00 -4.2000e+02 -1.7143e+03 3.9177e+00 -4.2000e+02 -1.6857e+03 3.7712e+00 -4.2000e+02 -1.6571e+03 3.6178e+00 -4.2000e+02 -1.6286e+03 3.4568e+00 -4.2000e+02 -1.6000e+03 3.2877e+00 -4.2000e+02 -1.5714e+03 3.1100e+00 -4.2000e+02 -1.5429e+03 2.9228e+00 -4.2000e+02 -1.5143e+03 2.7256e+00 -4.2000e+02 -1.4857e+03 2.5175e+00 -4.2000e+02 -1.4571e+03 2.2976e+00 -4.2000e+02 -1.4286e+03 2.0648e+00 -4.2000e+02 -1.4000e+03 1.8181e+00 -4.2000e+02 -1.3714e+03 1.5563e+00 -4.2000e+02 -1.3429e+03 1.2778e+00 -4.2000e+02 -1.3143e+03 9.8120e-01 -4.2000e+02 -1.2857e+03 6.6468e-01 -4.2000e+02 -1.2571e+03 3.2625e-01 -4.2000e+02 -1.2286e+03 -3.6358e-02 -4.2000e+02 -1.2000e+03 -4.2570e-01 -4.2000e+02 -1.1714e+03 -8.4472e-01 -4.2000e+02 -1.1429e+03 -1.2968e+00 -4.2000e+02 -1.1143e+03 -1.7857e+00 -4.2000e+02 -1.0857e+03 -2.3160e+00 -4.2000e+02 -1.0571e+03 -2.8927e+00 -4.2000e+02 -1.0286e+03 -3.5220e+00 -4.2000e+02 -1.0000e+03 -4.2107e+00 -4.2000e+02 -9.7143e+02 -4.9672e+00 -4.2000e+02 -9.4286e+02 -5.8011e+00 -4.2000e+02 -9.1429e+02 -6.7238e+00 -4.2000e+02 -8.8571e+02 -7.7490e+00 -4.2000e+02 -8.5714e+02 -8.8928e+00 -4.2000e+02 -8.2857e+02 -1.0175e+01 -4.2000e+02 -8.0000e+02 -1.1617e+01 -4.2000e+02 -7.7143e+02 -1.3248e+01 -4.2000e+02 -7.4286e+02 -1.5101e+01 -4.2000e+02 -7.1429e+02 -1.7212e+01 -4.2000e+02 -6.8571e+02 -1.9627e+01 -4.2000e+02 -6.5714e+02 -2.2395e+01 -4.2000e+02 -6.2857e+02 -2.5567e+01 -4.2000e+02 -6.0000e+02 -2.9195e+01 -4.2000e+02 -5.7143e+02 -3.3316e+01 -4.2000e+02 -5.4286e+02 -3.7943e+01 -4.2000e+02 -5.1429e+02 -4.3044e+01 -4.2000e+02 -4.8571e+02 -4.8529e+01 -4.2000e+02 -4.5714e+02 -5.4242e+01 -4.2000e+02 -4.2857e+02 -5.9983e+01 -4.2000e+02 -4.0000e+02 -6.5542e+01 -4.2000e+02 -3.7143e+02 -7.0739e+01 -4.2000e+02 -3.4286e+02 -7.5455e+01 -4.2000e+02 -3.1429e+02 -7.9631e+01 -4.2000e+02 -2.8571e+02 -8.3261e+01 -4.2000e+02 -2.5714e+02 -8.6370e+01 -4.2000e+02 -2.2857e+02 -8.9001e+01 -4.2000e+02 -2.0000e+02 -9.1202e+01 -4.2000e+02 -1.7143e+02 -9.3017e+01 -4.2000e+02 -1.4286e+02 -9.4489e+01 -4.2000e+02 -1.1429e+02 -9.5649e+01 -4.2000e+02 -8.5714e+01 -9.6525e+01 -4.2000e+02 -5.7143e+01 -9.7137e+01 -4.2000e+02 -2.8571e+01 -9.7499e+01 -4.2000e+02 0.0000e+00 -9.7618e+01 -4.2000e+02 2.8571e+01 -9.7499e+01 -4.2000e+02 5.7143e+01 -9.7137e+01 -4.2000e+02 8.5714e+01 -9.6525e+01 -4.2000e+02 1.1429e+02 -9.5649e+01 -4.2000e+02 1.4286e+02 -9.4489e+01 -4.2000e+02 1.7143e+02 -9.3017e+01 -4.2000e+02 2.0000e+02 -9.1202e+01 -4.2000e+02 2.2857e+02 -8.9001e+01 -4.2000e+02 2.5714e+02 -8.6370e+01 -4.2000e+02 2.8571e+02 -8.3261e+01 -4.2000e+02 3.1429e+02 -7.9631e+01 -4.2000e+02 3.4286e+02 -7.5455e+01 -4.2000e+02 3.7143e+02 -7.0739e+01 -4.2000e+02 4.0000e+02 -6.5542e+01 -4.2000e+02 4.2857e+02 -5.9983e+01 -4.2000e+02 4.5714e+02 -5.4242e+01 -4.2000e+02 4.8571e+02 -4.8529e+01 -4.2000e+02 5.1429e+02 -4.3044e+01 -4.2000e+02 5.4286e+02 -3.7943e+01 -4.2000e+02 5.7143e+02 -3.3316e+01 -4.2000e+02 6.0000e+02 -2.9195e+01 -4.2000e+02 6.2857e+02 -2.5567e+01 -4.2000e+02 6.5714e+02 -2.2395e+01 -4.2000e+02 6.8571e+02 -1.9627e+01 -4.2000e+02 7.1429e+02 -1.7212e+01 -4.2000e+02 7.4286e+02 -1.5101e+01 -4.2000e+02 7.7143e+02 -1.3248e+01 -4.2000e+02 8.0000e+02 -1.1617e+01 -4.2000e+02 8.2857e+02 -1.0175e+01 -4.2000e+02 8.5714e+02 -8.8928e+00 -4.2000e+02 8.8571e+02 -7.7490e+00 -4.2000e+02 9.1429e+02 -6.7238e+00 -4.2000e+02 9.4286e+02 -5.8011e+00 -4.2000e+02 9.7143e+02 -4.9672e+00 -4.2000e+02 1.0000e+03 -4.2107e+00 -4.2000e+02 1.0286e+03 -3.5220e+00 -4.2000e+02 1.0571e+03 -2.8927e+00 -4.2000e+02 1.0857e+03 -2.3160e+00 -4.2000e+02 1.1143e+03 -1.7857e+00 -4.2000e+02 1.1429e+03 -1.2968e+00 -4.2000e+02 1.1714e+03 -8.4472e-01 -4.2000e+02 1.2000e+03 -4.2570e-01 -4.2000e+02 1.2286e+03 -3.6358e-02 -4.2000e+02 1.2571e+03 3.2625e-01 -4.2000e+02 1.2857e+03 6.6468e-01 -4.2000e+02 1.3143e+03 9.8120e-01 -4.2000e+02 1.3429e+03 1.2778e+00 -4.2000e+02 1.3714e+03 1.5563e+00 -4.2000e+02 1.4000e+03 1.8181e+00 -4.2000e+02 1.4286e+03 2.0648e+00 -4.2000e+02 1.4571e+03 2.2976e+00 -4.2000e+02 1.4857e+03 2.5175e+00 -4.2000e+02 1.5143e+03 2.7256e+00 -4.2000e+02 1.5429e+03 2.9228e+00 -4.2000e+02 1.5714e+03 3.1100e+00 -4.2000e+02 1.6000e+03 3.2877e+00 -4.2000e+02 1.6286e+03 3.4568e+00 -4.2000e+02 1.6571e+03 3.6178e+00 -4.2000e+02 1.6857e+03 3.7712e+00 -4.2000e+02 1.7143e+03 3.9177e+00 -4.2000e+02 1.7429e+03 4.0576e+00 -4.2000e+02 1.7714e+03 4.1913e+00 -4.2000e+02 1.8000e+03 4.3193e+00 -4.2000e+02 1.8286e+03 4.4419e+00 -4.2000e+02 1.8571e+03 4.5595e+00 -4.2000e+02 1.8857e+03 4.6723e+00 -4.2000e+02 1.9143e+03 4.7805e+00 -4.2000e+02 1.9429e+03 4.8846e+00 -4.2000e+02 1.9714e+03 4.9847e+00 -4.2000e+02 2.0000e+03 5.0810e+00 -4.5000e+02 -2.0000e+03 5.1025e+00 -4.5000e+02 -1.9714e+03 5.0073e+00 -4.5000e+02 -1.9429e+03 4.9085e+00 -4.5000e+02 -1.9143e+03 4.8057e+00 -4.5000e+02 -1.8857e+03 4.6988e+00 -4.5000e+02 -1.8571e+03 4.5876e+00 -4.5000e+02 -1.8286e+03 4.4717e+00 -4.5000e+02 -1.8000e+03 4.3509e+00 -4.5000e+02 -1.7714e+03 4.2248e+00 -4.5000e+02 -1.7429e+03 4.0931e+00 -4.5000e+02 -1.7143e+03 3.9555e+00 -4.5000e+02 -1.6857e+03 3.8115e+00 -4.5000e+02 -1.6571e+03 3.6607e+00 -4.5000e+02 -1.6286e+03 3.5026e+00 -4.5000e+02 -1.6000e+03 3.3367e+00 -4.5000e+02 -1.5714e+03 3.1624e+00 -4.5000e+02 -1.5429e+03 2.9791e+00 -4.5000e+02 -1.5143e+03 2.7861e+00 -4.5000e+02 -1.4857e+03 2.5825e+00 -4.5000e+02 -1.4571e+03 2.3676e+00 -4.5000e+02 -1.4286e+03 2.1405e+00 -4.5000e+02 -1.4000e+03 1.9000e+00 -4.5000e+02 -1.3714e+03 1.6449e+00 -4.5000e+02 -1.3429e+03 1.3741e+00 -4.5000e+02 -1.3143e+03 1.0861e+00 -4.5000e+02 -1.2857e+03 7.7908e-01 -4.5000e+02 -1.2571e+03 4.5137e-01 -4.5000e+02 -1.2286e+03 1.0084e-01 -4.5000e+02 -1.2000e+03 -2.7485e-01 -4.5000e+02 -1.1714e+03 -6.7838e-01 -4.5000e+02 -1.1429e+03 -1.1128e+00 -4.5000e+02 -1.1143e+03 -1.5816e+00 -4.5000e+02 -1.0857e+03 -2.0888e+00 -4.5000e+02 -1.0571e+03 -2.6389e+00 -4.5000e+02 -1.0286e+03 -3.2373e+00 -4.5000e+02 -1.0000e+03 -3.8903e+00 -4.5000e+02 -9.7143e+02 -4.6049e+00 -4.5000e+02 -9.4286e+02 -5.3897e+00 -4.5000e+02 -9.1429e+02 -6.2545e+00 -4.5000e+02 -8.8571e+02 -7.2109e+00 -4.5000e+02 -8.5714e+02 -8.2728e+00 -4.5000e+02 -8.2857e+02 -9.4562e+00 -4.5000e+02 -8.0000e+02 -1.0781e+01 -4.5000e+02 -7.7143e+02 -1.2268e+01 -4.5000e+02 -7.4286e+02 -1.3947e+01 -4.5000e+02 -7.1429e+02 -1.5846e+01 -4.5000e+02 -6.8571e+02 -1.8003e+01 -4.5000e+02 -6.5714e+02 -2.0459e+01 -4.5000e+02 -6.2857e+02 -2.3255e+01 -4.5000e+02 -6.0000e+02 -2.6437e+01 -4.5000e+02 -5.7143e+02 -3.0043e+01 -4.5000e+02 -5.4286e+02 -3.4096e+01 -4.5000e+02 -5.1429e+02 -3.8595e+01 -4.5000e+02 -4.8571e+02 -4.3494e+01 -4.5000e+02 -4.5714e+02 -4.8699e+01 -4.5000e+02 -4.2857e+02 -5.4067e+01 -4.5000e+02 -4.0000e+02 -5.9421e+01 -4.5000e+02 -3.7143e+02 -6.4582e+01 -4.5000e+02 -3.4286e+02 -6.9403e+01 -4.5000e+02 -3.1429e+02 -7.3780e+01 -4.5000e+02 -2.8571e+02 -7.7663e+01 -4.5000e+02 -2.5714e+02 -8.1042e+01 -4.5000e+02 -2.2857e+02 -8.3935e+01 -4.5000e+02 -2.0000e+02 -8.6375e+01 -4.5000e+02 -1.7143e+02 -8.8400e+01 -4.5000e+02 -1.4286e+02 -9.0046e+01 -4.5000e+02 -1.1429e+02 -9.1348e+01 -4.5000e+02 -8.5714e+01 -9.2333e+01 -4.5000e+02 -5.7143e+01 -9.3021e+01 -4.5000e+02 -2.8571e+01 -9.3428e+01 -4.5000e+02 0.0000e+00 -9.3563e+01 -4.5000e+02 2.8571e+01 -9.3428e+01 -4.5000e+02 5.7143e+01 -9.3021e+01 -4.5000e+02 8.5714e+01 -9.2333e+01 -4.5000e+02 1.1429e+02 -9.1348e+01 -4.5000e+02 1.4286e+02 -9.0046e+01 -4.5000e+02 1.7143e+02 -8.8400e+01 -4.5000e+02 2.0000e+02 -8.6375e+01 -4.5000e+02 2.2857e+02 -8.3935e+01 -4.5000e+02 2.5714e+02 -8.1042e+01 -4.5000e+02 2.8571e+02 -7.7663e+01 -4.5000e+02 3.1429e+02 -7.3780e+01 -4.5000e+02 3.4286e+02 -6.9403e+01 -4.5000e+02 3.7143e+02 -6.4582e+01 -4.5000e+02 4.0000e+02 -5.9421e+01 -4.5000e+02 4.2857e+02 -5.4067e+01 -4.5000e+02 4.5714e+02 -4.8699e+01 -4.5000e+02 4.8571e+02 -4.3494e+01 -4.5000e+02 5.1429e+02 -3.8595e+01 -4.5000e+02 5.4286e+02 -3.4096e+01 -4.5000e+02 5.7143e+02 -3.0043e+01 -4.5000e+02 6.0000e+02 -2.6437e+01 -4.5000e+02 6.2857e+02 -2.3255e+01 -4.5000e+02 6.5714e+02 -2.0459e+01 -4.5000e+02 6.8571e+02 -1.8003e+01 -4.5000e+02 7.1429e+02 -1.5846e+01 -4.5000e+02 7.4286e+02 -1.3947e+01 -4.5000e+02 7.7143e+02 -1.2268e+01 -4.5000e+02 8.0000e+02 -1.0781e+01 -4.5000e+02 8.2857e+02 -9.4562e+00 -4.5000e+02 8.5714e+02 -8.2728e+00 -4.5000e+02 8.8571e+02 -7.2109e+00 -4.5000e+02 9.1429e+02 -6.2545e+00 -4.5000e+02 9.4286e+02 -5.3897e+00 -4.5000e+02 9.7143e+02 -4.6049e+00 -4.5000e+02 1.0000e+03 -3.8903e+00 -4.5000e+02 1.0286e+03 -3.2373e+00 -4.5000e+02 1.0571e+03 -2.6389e+00 -4.5000e+02 1.0857e+03 -2.0888e+00 -4.5000e+02 1.1143e+03 -1.5816e+00 -4.5000e+02 1.1429e+03 -1.1128e+00 -4.5000e+02 1.1714e+03 -6.7838e-01 -4.5000e+02 1.2000e+03 -2.7485e-01 -4.5000e+02 1.2286e+03 1.0084e-01 -4.5000e+02 1.2571e+03 4.5137e-01 -4.5000e+02 1.2857e+03 7.7908e-01 -4.5000e+02 1.3143e+03 1.0861e+00 -4.5000e+02 1.3429e+03 1.3741e+00 -4.5000e+02 1.3714e+03 1.6449e+00 -4.5000e+02 1.4000e+03 1.9000e+00 -4.5000e+02 1.4286e+03 2.1405e+00 -4.5000e+02 1.4571e+03 2.3676e+00 -4.5000e+02 1.4857e+03 2.5825e+00 -4.5000e+02 1.5143e+03 2.7861e+00 -4.5000e+02 1.5429e+03 2.9791e+00 -4.5000e+02 1.5714e+03 3.1624e+00 -4.5000e+02 1.6000e+03 3.3367e+00 -4.5000e+02 1.6286e+03 3.5026e+00 -4.5000e+02 1.6571e+03 3.6607e+00 -4.5000e+02 1.6857e+03 3.8115e+00 -4.5000e+02 1.7143e+03 3.9555e+00 -4.5000e+02 1.7429e+03 4.0931e+00 -4.5000e+02 1.7714e+03 4.2248e+00 -4.5000e+02 1.8000e+03 4.3509e+00 -4.5000e+02 1.8286e+03 4.4717e+00 -4.5000e+02 1.8571e+03 4.5876e+00 -4.5000e+02 1.8857e+03 4.6988e+00 -4.5000e+02 1.9143e+03 4.8057e+00 -4.5000e+02 1.9429e+03 4.9085e+00 -4.5000e+02 1.9714e+03 5.0073e+00 -4.5000e+02 2.0000e+03 5.1025e+00 -4.8000e+02 -2.0000e+03 5.1251e+00 -4.8000e+02 -1.9714e+03 5.0312e+00 -4.8000e+02 -1.9429e+03 4.9336e+00 -4.8000e+02 -1.9143e+03 4.8322e+00 -4.8000e+02 -1.8857e+03 4.7269e+00 -4.8000e+02 -1.8571e+03 4.6172e+00 -4.8000e+02 -1.8286e+03 4.5030e+00 -4.8000e+02 -1.8000e+03 4.3841e+00 -4.8000e+02 -1.7714e+03 4.2600e+00 -4.8000e+02 -1.7429e+03 4.1305e+00 -4.8000e+02 -1.7143e+03 3.9952e+00 -4.8000e+02 -1.6857e+03 3.8538e+00 -4.8000e+02 -1.6571e+03 3.7058e+00 -4.8000e+02 -1.6286e+03 3.5507e+00 -4.8000e+02 -1.6000e+03 3.3881e+00 -4.8000e+02 -1.5714e+03 3.2174e+00 -4.8000e+02 -1.5429e+03 3.0380e+00 -4.8000e+02 -1.5143e+03 2.8492e+00 -4.8000e+02 -1.4857e+03 2.6504e+00 -4.8000e+02 -1.4571e+03 2.4407e+00 -4.8000e+02 -1.4286e+03 2.2193e+00 -4.8000e+02 -1.4000e+03 1.9851e+00 -4.8000e+02 -1.3714e+03 1.7371e+00 -4.8000e+02 -1.3429e+03 1.4741e+00 -4.8000e+02 -1.3143e+03 1.1948e+00 -4.8000e+02 -1.2857e+03 8.9750e-01 -4.8000e+02 -1.2571e+03 5.8067e-01 -4.8000e+02 -1.2286e+03 2.4238e-01 -4.8000e+02 -1.2000e+03 -1.1953e-01 -4.8000e+02 -1.1714e+03 -5.0747e-01 -4.8000e+02 -1.1429e+03 -9.2420e-01 -4.8000e+02 -1.1143e+03 -1.3729e+00 -4.8000e+02 -1.0857e+03 -1.8570e+00 -4.8000e+02 -1.0571e+03 -2.3807e+00 -4.8000e+02 -1.0286e+03 -2.9487e+00 -4.8000e+02 -1.0000e+03 -3.5665e+00 -4.8000e+02 -9.7143e+02 -4.2402e+00 -4.8000e+02 -9.4286e+02 -4.9771e+00 -4.8000e+02 -9.1429e+02 -5.7858e+00 -4.8000e+02 -8.8571e+02 -6.6762e+00 -4.8000e+02 -8.5714e+02 -7.6597e+00 -4.8000e+02 -8.2857e+02 -8.7500e+00 -4.8000e+02 -8.0000e+02 -9.9629e+00 -4.8000e+02 -7.7143e+02 -1.1317e+01 -4.8000e+02 -7.4286e+02 -1.2834e+01 -4.8000e+02 -7.1429e+02 -1.4539e+01 -4.8000e+02 -6.8571e+02 -1.6461e+01 -4.8000e+02 -6.5714e+02 -1.8632e+01 -4.8000e+02 -6.2857e+02 -2.1087e+01 -4.8000e+02 -6.0000e+02 -2.3864e+01 -4.8000e+02 -5.7143e+02 -2.6995e+01 -4.8000e+02 -5.4286e+02 -3.0509e+01 -4.8000e+02 -5.1429e+02 -3.4416e+01 -4.8000e+02 -4.8571e+02 -3.8702e+01 -4.8000e+02 -4.5714e+02 -4.3318e+01 -4.8000e+02 -4.2857e+02 -4.8174e+01 -4.8000e+02 -4.0000e+02 -5.3142e+01 -4.8000e+02 -3.7143e+02 -5.8072e+01 -4.8000e+02 -3.4286e+02 -6.2817e+01 -4.8000e+02 -3.1429e+02 -6.7250e+01 -4.8000e+02 -2.8571e+02 -7.1284e+01 -4.8000e+02 -2.5714e+02 -7.4870e+01 -4.8000e+02 -2.2857e+02 -7.7993e+01 -4.8000e+02 -2.0000e+02 -8.0662e+01 -4.8000e+02 -1.7143e+02 -8.2899e+01 -4.8000e+02 -1.4286e+02 -8.4731e+01 -4.8000e+02 -1.1429e+02 -8.6187e+01 -4.8000e+02 -8.5714e+01 -8.7292e+01 -4.8000e+02 -5.7143e+01 -8.8066e+01 -4.8000e+02 -2.8571e+01 -8.8525e+01 -4.8000e+02 0.0000e+00 -8.8677e+01 -4.8000e+02 2.8571e+01 -8.8525e+01 -4.8000e+02 5.7143e+01 -8.8066e+01 -4.8000e+02 8.5714e+01 -8.7292e+01 -4.8000e+02 1.1429e+02 -8.6187e+01 -4.8000e+02 1.4286e+02 -8.4731e+01 -4.8000e+02 1.7143e+02 -8.2899e+01 -4.8000e+02 2.0000e+02 -8.0662e+01 -4.8000e+02 2.2857e+02 -7.7993e+01 -4.8000e+02 2.5714e+02 -7.4870e+01 -4.8000e+02 2.8571e+02 -7.1284e+01 -4.8000e+02 3.1429e+02 -6.7250e+01 -4.8000e+02 3.4286e+02 -6.2817e+01 -4.8000e+02 3.7143e+02 -5.8072e+01 -4.8000e+02 4.0000e+02 -5.3142e+01 -4.8000e+02 4.2857e+02 -4.8174e+01 -4.8000e+02 4.5714e+02 -4.3318e+01 -4.8000e+02 4.8571e+02 -3.8702e+01 -4.8000e+02 5.1429e+02 -3.4416e+01 -4.8000e+02 5.4286e+02 -3.0509e+01 -4.8000e+02 5.7143e+02 -2.6995e+01 -4.8000e+02 6.0000e+02 -2.3864e+01 -4.8000e+02 6.2857e+02 -2.1087e+01 -4.8000e+02 6.5714e+02 -1.8632e+01 -4.8000e+02 6.8571e+02 -1.6461e+01 -4.8000e+02 7.1429e+02 -1.4539e+01 -4.8000e+02 7.4286e+02 -1.2834e+01 -4.8000e+02 7.7143e+02 -1.1317e+01 -4.8000e+02 8.0000e+02 -9.9629e+00 -4.8000e+02 8.2857e+02 -8.7500e+00 -4.8000e+02 8.5714e+02 -7.6597e+00 -4.8000e+02 8.8571e+02 -6.6762e+00 -4.8000e+02 9.1429e+02 -5.7858e+00 -4.8000e+02 9.4286e+02 -4.9771e+00 -4.8000e+02 9.7143e+02 -4.2402e+00 -4.8000e+02 1.0000e+03 -3.5665e+00 -4.8000e+02 1.0286e+03 -2.9487e+00 -4.8000e+02 1.0571e+03 -2.3807e+00 -4.8000e+02 1.0857e+03 -1.8570e+00 -4.8000e+02 1.1143e+03 -1.3729e+00 -4.8000e+02 1.1429e+03 -9.2420e-01 -4.8000e+02 1.1714e+03 -5.0747e-01 -4.8000e+02 1.2000e+03 -1.1953e-01 -4.8000e+02 1.2286e+03 2.4238e-01 -4.8000e+02 1.2571e+03 5.8067e-01 -4.8000e+02 1.2857e+03 8.9750e-01 -4.8000e+02 1.3143e+03 1.1948e+00 -4.8000e+02 1.3429e+03 1.4741e+00 -4.8000e+02 1.3714e+03 1.7371e+00 -4.8000e+02 1.4000e+03 1.9851e+00 -4.8000e+02 1.4286e+03 2.2193e+00 -4.8000e+02 1.4571e+03 2.4407e+00 -4.8000e+02 1.4857e+03 2.6504e+00 -4.8000e+02 1.5143e+03 2.8492e+00 -4.8000e+02 1.5429e+03 3.0380e+00 -4.8000e+02 1.5714e+03 3.2174e+00 -4.8000e+02 1.6000e+03 3.3881e+00 -4.8000e+02 1.6286e+03 3.5507e+00 -4.8000e+02 1.6571e+03 3.7058e+00 -4.8000e+02 1.6857e+03 3.8538e+00 -4.8000e+02 1.7143e+03 3.9952e+00 -4.8000e+02 1.7429e+03 4.1305e+00 -4.8000e+02 1.7714e+03 4.2600e+00 -4.8000e+02 1.8000e+03 4.3841e+00 -4.8000e+02 1.8286e+03 4.5030e+00 -4.8000e+02 1.8571e+03 4.6172e+00 -4.8000e+02 1.8857e+03 4.7269e+00 -4.8000e+02 1.9143e+03 4.8322e+00 -4.8000e+02 1.9429e+03 4.9336e+00 -4.8000e+02 1.9714e+03 5.0312e+00 -4.8000e+02 2.0000e+03 5.1251e+00 -5.1000e+02 -2.0000e+03 5.1489e+00 -5.1000e+02 -1.9714e+03 5.0562e+00 -5.1000e+02 -1.9429e+03 4.9600e+00 -5.1000e+02 -1.9143e+03 4.8601e+00 -5.1000e+02 -1.8857e+03 4.7563e+00 -5.1000e+02 -1.8571e+03 4.6483e+00 -5.1000e+02 -1.8286e+03 4.5359e+00 -5.1000e+02 -1.8000e+03 4.4189e+00 -5.1000e+02 -1.7714e+03 4.2969e+00 -5.1000e+02 -1.7429e+03 4.1696e+00 -5.1000e+02 -1.7143e+03 4.0368e+00 -5.1000e+02 -1.6857e+03 3.8980e+00 -5.1000e+02 -1.6571e+03 3.7528e+00 -5.1000e+02 -1.6286e+03 3.6008e+00 -5.1000e+02 -1.6000e+03 3.4416e+00 -5.1000e+02 -1.5714e+03 3.2746e+00 -5.1000e+02 -1.5429e+03 3.0992e+00 -5.1000e+02 -1.5143e+03 2.9149e+00 -5.1000e+02 -1.4857e+03 2.7209e+00 -5.1000e+02 -1.4571e+03 2.5166e+00 -5.1000e+02 -1.4286e+03 2.3010e+00 -5.1000e+02 -1.4000e+03 2.0733e+00 -5.1000e+02 -1.3714e+03 1.8324e+00 -5.1000e+02 -1.3429e+03 1.5774e+00 -5.1000e+02 -1.3143e+03 1.3068e+00 -5.1000e+02 -1.2857e+03 1.0194e+00 -5.1000e+02 -1.2571e+03 7.1354e-01 -5.1000e+02 -1.2286e+03 3.8755e-01 -5.1000e+02 -1.2000e+03 3.9469e-02 -5.1000e+02 -1.1714e+03 -3.3289e-01 -5.1000e+02 -1.1429e+03 -7.3199e-01 -5.1000e+02 -1.1143e+03 -1.1606e+00 -5.1000e+02 -1.0857e+03 -1.6220e+00 -5.1000e+02 -1.0571e+03 -2.1197e+00 -5.1000e+02 -1.0286e+03 -2.6578e+00 -5.1000e+02 -1.0000e+03 -3.2412e+00 -5.1000e+02 -9.7143e+02 -3.8751e+00 -5.1000e+02 -9.4286e+02 -4.5659e+00 -5.1000e+02 -9.1429e+02 -5.3208e+00 -5.1000e+02 -8.8571e+02 -6.1480e+00 -5.1000e+02 -8.5714e+02 -7.0573e+00 -5.1000e+02 -8.2857e+02 -8.0600e+00 -5.1000e+02 -8.0000e+02 -9.1688e+00 -5.1000e+02 -7.7143e+02 -1.0399e+01 -5.1000e+02 -7.4286e+02 -1.1768e+01 -5.1000e+02 -7.1429e+02 -1.3296e+01 -5.1000e+02 -6.8571e+02 -1.5005e+01 -5.1000e+02 -6.5714e+02 -1.6921e+01 -5.1000e+02 -6.2857e+02 -1.9071e+01 -5.1000e+02 -6.0000e+02 -2.1485e+01 -5.1000e+02 -5.7143e+02 -2.4191e+01 -5.1000e+02 -5.4286e+02 -2.7214e+01 -5.1000e+02 -5.1429e+02 -3.0569e+01 -5.1000e+02 -4.8571e+02 -3.4259e+01 -5.1000e+02 -4.5714e+02 -3.8261e+01 -5.1000e+02 -4.2857e+02 -4.2526e+01 -5.1000e+02 -4.0000e+02 -4.6973e+01 -5.1000e+02 -3.7143e+02 -5.1493e+01 -5.1000e+02 -3.4286e+02 -5.5963e+01 -5.1000e+02 -3.1429e+02 -6.0261e+01 -5.1000e+02 -2.8571e+02 -6.4282e+01 -5.1000e+02 -2.5714e+02 -6.7948e+01 -5.1000e+02 -2.2857e+02 -7.1211e+01 -5.1000e+02 -2.0000e+02 -7.4050e+01 -5.1000e+02 -1.7143e+02 -7.6465e+01 -5.1000e+02 -1.4286e+02 -7.8465e+01 -5.1000e+02 -1.1429e+02 -8.0068e+01 -5.1000e+02 -8.5714e+01 -8.1292e+01 -5.1000e+02 -5.7143e+01 -8.2153e+01 -5.1000e+02 -2.8571e+01 -8.2664e+01 -5.1000e+02 0.0000e+00 -8.2834e+01 -5.1000e+02 2.8571e+01 -8.2664e+01 -5.1000e+02 5.7143e+01 -8.2153e+01 -5.1000e+02 8.5714e+01 -8.1292e+01 -5.1000e+02 1.1429e+02 -8.0068e+01 -5.1000e+02 1.4286e+02 -7.8465e+01 -5.1000e+02 1.7143e+02 -7.6465e+01 -5.1000e+02 2.0000e+02 -7.4050e+01 -5.1000e+02 2.2857e+02 -7.1211e+01 -5.1000e+02 2.5714e+02 -6.7948e+01 -5.1000e+02 2.8571e+02 -6.4282e+01 -5.1000e+02 3.1429e+02 -6.0261e+01 -5.1000e+02 3.4286e+02 -5.5963e+01 -5.1000e+02 3.7143e+02 -5.1493e+01 -5.1000e+02 4.0000e+02 -4.6973e+01 -5.1000e+02 4.2857e+02 -4.2526e+01 -5.1000e+02 4.5714e+02 -3.8261e+01 -5.1000e+02 4.8571e+02 -3.4259e+01 -5.1000e+02 5.1429e+02 -3.0569e+01 -5.1000e+02 5.4286e+02 -2.7214e+01 -5.1000e+02 5.7143e+02 -2.4191e+01 -5.1000e+02 6.0000e+02 -2.1485e+01 -5.1000e+02 6.2857e+02 -1.9071e+01 -5.1000e+02 6.5714e+02 -1.6921e+01 -5.1000e+02 6.8571e+02 -1.5005e+01 -5.1000e+02 7.1429e+02 -1.3296e+01 -5.1000e+02 7.4286e+02 -1.1768e+01 -5.1000e+02 7.7143e+02 -1.0399e+01 -5.1000e+02 8.0000e+02 -9.1688e+00 -5.1000e+02 8.2857e+02 -8.0600e+00 -5.1000e+02 8.5714e+02 -7.0573e+00 -5.1000e+02 8.8571e+02 -6.1480e+00 -5.1000e+02 9.1429e+02 -5.3208e+00 -5.1000e+02 9.4286e+02 -4.5659e+00 -5.1000e+02 9.7143e+02 -3.8751e+00 -5.1000e+02 1.0000e+03 -3.2412e+00 -5.1000e+02 1.0286e+03 -2.6578e+00 -5.1000e+02 1.0571e+03 -2.1197e+00 -5.1000e+02 1.0857e+03 -1.6220e+00 -5.1000e+02 1.1143e+03 -1.1606e+00 -5.1000e+02 1.1429e+03 -7.3199e-01 -5.1000e+02 1.1714e+03 -3.3289e-01 -5.1000e+02 1.2000e+03 3.9469e-02 -5.1000e+02 1.2286e+03 3.8755e-01 -5.1000e+02 1.2571e+03 7.1354e-01 -5.1000e+02 1.2857e+03 1.0194e+00 -5.1000e+02 1.3143e+03 1.3068e+00 -5.1000e+02 1.3429e+03 1.5774e+00 -5.1000e+02 1.3714e+03 1.8324e+00 -5.1000e+02 1.4000e+03 2.0733e+00 -5.1000e+02 1.4286e+03 2.3010e+00 -5.1000e+02 1.4571e+03 2.5166e+00 -5.1000e+02 1.4857e+03 2.7209e+00 -5.1000e+02 1.5143e+03 2.9149e+00 -5.1000e+02 1.5429e+03 3.0992e+00 -5.1000e+02 1.5714e+03 3.2746e+00 -5.1000e+02 1.6000e+03 3.4416e+00 -5.1000e+02 1.6286e+03 3.6008e+00 -5.1000e+02 1.6571e+03 3.7528e+00 -5.1000e+02 1.6857e+03 3.8980e+00 -5.1000e+02 1.7143e+03 4.0368e+00 -5.1000e+02 1.7429e+03 4.1696e+00 -5.1000e+02 1.7714e+03 4.2969e+00 -5.1000e+02 1.8000e+03 4.4189e+00 -5.1000e+02 1.8286e+03 4.5359e+00 -5.1000e+02 1.8571e+03 4.6483e+00 -5.1000e+02 1.8857e+03 4.7563e+00 -5.1000e+02 1.9143e+03 4.8601e+00 -5.1000e+02 1.9429e+03 4.9600e+00 -5.1000e+02 1.9714e+03 5.0562e+00 -5.1000e+02 2.0000e+03 5.1489e+00 -5.4000e+02 -2.0000e+03 5.1739e+00 -5.4000e+02 -1.9714e+03 5.0824e+00 -5.4000e+02 -1.9429e+03 4.9876e+00 -5.4000e+02 -1.9143e+03 4.8892e+00 -5.4000e+02 -1.8857e+03 4.7870e+00 -5.4000e+02 -1.8571e+03 4.6807e+00 -5.4000e+02 -1.8286e+03 4.5702e+00 -5.4000e+02 -1.8000e+03 4.4551e+00 -5.4000e+02 -1.7714e+03 4.3353e+00 -5.4000e+02 -1.7429e+03 4.2104e+00 -5.4000e+02 -1.7143e+03 4.0800e+00 -5.4000e+02 -1.6857e+03 3.9439e+00 -5.4000e+02 -1.6571e+03 3.8017e+00 -5.4000e+02 -1.6286e+03 3.6529e+00 -5.4000e+02 -1.6000e+03 3.4971e+00 -5.4000e+02 -1.5714e+03 3.3339e+00 -5.4000e+02 -1.5429e+03 3.1627e+00 -5.4000e+02 -1.5143e+03 2.9828e+00 -5.4000e+02 -1.4857e+03 2.7938e+00 -5.4000e+02 -1.4571e+03 2.5948e+00 -5.4000e+02 -1.4286e+03 2.3852e+00 -5.4000e+02 -1.4000e+03 2.1641e+00 -5.4000e+02 -1.3714e+03 1.9305e+00 -5.4000e+02 -1.3429e+03 1.6834e+00 -5.4000e+02 -1.3143e+03 1.4218e+00 -5.4000e+02 -1.2857e+03 1.1442e+00 -5.4000e+02 -1.2571e+03 8.4936e-01 -5.4000e+02 -1.2286e+03 5.3567e-01 -5.4000e+02 -1.2000e+03 2.0138e-01 -5.4000e+02 -1.1714e+03 -1.5549e-01 -5.4000e+02 -1.1429e+03 -5.3713e-01 -5.4000e+02 -1.1143e+03 -9.4603e-01 -5.4000e+02 -1.0857e+03 -1.3850e+00 -5.4000e+02 -1.0571e+03 -1.8572e+00 -5.4000e+02 -1.0286e+03 -2.3662e+00 -5.4000e+02 -1.0000e+03 -2.9162e+00 -5.4000e+02 -9.7143e+02 -3.5117e+00 -5.4000e+02 -9.4286e+02 -4.1582e+00 -5.4000e+02 -9.1429e+02 -4.8617e+00 -5.4000e+02 -8.8571e+02 -5.6292e+00 -5.4000e+02 -8.5714e+02 -6.4686e+00 -5.4000e+02 -8.2857e+02 -7.3893e+00 -5.4000e+02 -8.0000e+02 -8.4017e+00 -5.4000e+02 -7.7143e+02 -9.5180e+00 -5.4000e+02 -7.4286e+02 -1.0752e+01 -5.4000e+02 -7.1429e+02 -1.2119e+01 -5.4000e+02 -6.8571e+02 -1.3637e+01 -5.4000e+02 -6.5714e+02 -1.5326e+01 -5.4000e+02 -6.2857e+02 -1.7207e+01 -5.4000e+02 -6.0000e+02 -1.9301e+01 -5.4000e+02 -5.7143e+02 -2.1632e+01 -5.4000e+02 -5.4286e+02 -2.4220e+01 -5.4000e+02 -5.1429e+02 -2.7081e+01 -5.4000e+02 -4.8571e+02 -3.0221e+01 -5.4000e+02 -4.5714e+02 -3.3633e+01 -5.4000e+02 -4.2857e+02 -3.7293e+01 -5.4000e+02 -4.0000e+02 -4.1153e+01 -5.4000e+02 -3.7143e+02 -4.5144e+01 -5.4000e+02 -3.4286e+02 -4.9175e+01 -5.4000e+02 -3.1429e+02 -5.3147e+01 -5.4000e+02 -2.8571e+02 -5.6961e+01 -5.4000e+02 -2.5714e+02 -6.0529e+01 -5.4000e+02 -2.2857e+02 -6.3784e+01 -5.4000e+02 -2.0000e+02 -6.6677e+01 -5.4000e+02 -1.7143e+02 -6.9182e+01 -5.4000e+02 -1.4286e+02 -7.1289e+01 -5.4000e+02 -1.1429e+02 -7.2998e+01 -5.4000e+02 -8.5714e+01 -7.4315e+01 -5.4000e+02 -5.7143e+01 -7.5247e+01 -5.4000e+02 -2.8571e+01 -7.5803e+01 -5.4000e+02 0.0000e+00 -7.5988e+01 -5.4000e+02 2.8571e+01 -7.5803e+01 -5.4000e+02 5.7143e+01 -7.5247e+01 -5.4000e+02 8.5714e+01 -7.4315e+01 -5.4000e+02 1.1429e+02 -7.2998e+01 -5.4000e+02 1.4286e+02 -7.1289e+01 -5.4000e+02 1.7143e+02 -6.9182e+01 -5.4000e+02 2.0000e+02 -6.6677e+01 -5.4000e+02 2.2857e+02 -6.3784e+01 -5.4000e+02 2.5714e+02 -6.0529e+01 -5.4000e+02 2.8571e+02 -5.6961e+01 -5.4000e+02 3.1429e+02 -5.3147e+01 -5.4000e+02 3.4286e+02 -4.9175e+01 -5.4000e+02 3.7143e+02 -4.5144e+01 -5.4000e+02 4.0000e+02 -4.1153e+01 -5.4000e+02 4.2857e+02 -3.7293e+01 -5.4000e+02 4.5714e+02 -3.3633e+01 -5.4000e+02 4.8571e+02 -3.0221e+01 -5.4000e+02 5.1429e+02 -2.7081e+01 -5.4000e+02 5.4286e+02 -2.4220e+01 -5.4000e+02 5.7143e+02 -2.1632e+01 -5.4000e+02 6.0000e+02 -1.9301e+01 -5.4000e+02 6.2857e+02 -1.7207e+01 -5.4000e+02 6.5714e+02 -1.5326e+01 -5.4000e+02 6.8571e+02 -1.3637e+01 -5.4000e+02 7.1429e+02 -1.2119e+01 -5.4000e+02 7.4286e+02 -1.0752e+01 -5.4000e+02 7.7143e+02 -9.5180e+00 -5.4000e+02 8.0000e+02 -8.4017e+00 -5.4000e+02 8.2857e+02 -7.3893e+00 -5.4000e+02 8.5714e+02 -6.4686e+00 -5.4000e+02 8.8571e+02 -5.6292e+00 -5.4000e+02 9.1429e+02 -4.8617e+00 -5.4000e+02 9.4286e+02 -4.1582e+00 -5.4000e+02 9.7143e+02 -3.5117e+00 -5.4000e+02 1.0000e+03 -2.9162e+00 -5.4000e+02 1.0286e+03 -2.3662e+00 -5.4000e+02 1.0571e+03 -1.8572e+00 -5.4000e+02 1.0857e+03 -1.3850e+00 -5.4000e+02 1.1143e+03 -9.4603e-01 -5.4000e+02 1.1429e+03 -5.3713e-01 -5.4000e+02 1.1714e+03 -1.5549e-01 -5.4000e+02 1.2000e+03 2.0138e-01 -5.4000e+02 1.2286e+03 5.3567e-01 -5.4000e+02 1.2571e+03 8.4936e-01 -5.4000e+02 1.2857e+03 1.1442e+00 -5.4000e+02 1.3143e+03 1.4218e+00 -5.4000e+02 1.3429e+03 1.6834e+00 -5.4000e+02 1.3714e+03 1.9305e+00 -5.4000e+02 1.4000e+03 2.1641e+00 -5.4000e+02 1.4286e+03 2.3852e+00 -5.4000e+02 1.4571e+03 2.5948e+00 -5.4000e+02 1.4857e+03 2.7938e+00 -5.4000e+02 1.5143e+03 2.9828e+00 -5.4000e+02 1.5429e+03 3.1627e+00 -5.4000e+02 1.5714e+03 3.3339e+00 -5.4000e+02 1.6000e+03 3.4971e+00 -5.4000e+02 1.6286e+03 3.6529e+00 -5.4000e+02 1.6571e+03 3.8017e+00 -5.4000e+02 1.6857e+03 3.9439e+00 -5.4000e+02 1.7143e+03 4.0800e+00 -5.4000e+02 1.7429e+03 4.2104e+00 -5.4000e+02 1.7714e+03 4.3353e+00 -5.4000e+02 1.8000e+03 4.4551e+00 -5.4000e+02 1.8286e+03 4.5702e+00 -5.4000e+02 1.8571e+03 4.6807e+00 -5.4000e+02 1.8857e+03 4.7870e+00 -5.4000e+02 1.9143e+03 4.8892e+00 -5.4000e+02 1.9429e+03 4.9876e+00 -5.4000e+02 1.9714e+03 5.0824e+00 -5.4000e+02 2.0000e+03 5.1739e+00 -5.7000e+02 -2.0000e+03 5.1998e+00 -5.7000e+02 -1.9714e+03 5.1098e+00 -5.7000e+02 -1.9429e+03 5.0164e+00 -5.7000e+02 -1.9143e+03 4.9195e+00 -5.7000e+02 -1.8857e+03 4.8189e+00 -5.7000e+02 -1.8571e+03 4.7144e+00 -5.7000e+02 -1.8286e+03 4.6058e+00 -5.7000e+02 -1.8000e+03 4.4928e+00 -5.7000e+02 -1.7714e+03 4.3752e+00 -5.7000e+02 -1.7429e+03 4.2526e+00 -5.7000e+02 -1.7143e+03 4.1248e+00 -5.7000e+02 -1.6857e+03 3.9915e+00 -5.7000e+02 -1.6571e+03 3.8523e+00 -5.7000e+02 -1.6286e+03 3.7067e+00 -5.7000e+02 -1.6000e+03 3.5545e+00 -5.7000e+02 -1.5714e+03 3.3951e+00 -5.7000e+02 -1.5429e+03 3.2281e+00 -5.7000e+02 -1.5143e+03 3.0528e+00 -5.7000e+02 -1.4857e+03 2.8688e+00 -5.7000e+02 -1.4571e+03 2.6753e+00 -5.7000e+02 -1.4286e+03 2.4717e+00 -5.7000e+02 -1.4000e+03 2.2572e+00 -5.7000e+02 -1.3714e+03 2.0309e+00 -5.7000e+02 -1.3429e+03 1.7919e+00 -5.7000e+02 -1.3143e+03 1.5391e+00 -5.7000e+02 -1.2857e+03 1.2714e+00 -5.7000e+02 -1.2571e+03 9.8756e-01 -5.7000e+02 -1.2286e+03 6.8611e-01 -5.7000e+02 -1.2000e+03 3.6548e-01 -5.7000e+02 -1.1714e+03 2.3931e-02 -5.7000e+02 -1.1429e+03 -3.4051e-01 -5.7000e+02 -1.1143e+03 -7.3003e-01 -5.7000e+02 -1.0857e+03 -1.1471e+00 -5.7000e+02 -1.0571e+03 -1.5945e+00 -5.7000e+02 -1.0286e+03 -2.0753e+00 -5.7000e+02 -1.0000e+03 -2.5931e+00 -5.7000e+02 -9.7143e+02 -3.1518e+00 -5.7000e+02 -9.4286e+02 -3.7560e+00 -5.7000e+02 -9.1429e+02 -4.4107e+00 -5.7000e+02 -8.8571e+02 -5.1218e+00 -5.7000e+02 -8.5714e+02 -5.8959e+00 -5.7000e+02 -8.2857e+02 -6.7404e+00 -5.7000e+02 -8.0000e+02 -7.6638e+00 -5.7000e+02 -7.7143e+02 -8.6758e+00 -5.7000e+02 -7.4286e+02 -9.7872e+00 -5.7000e+02 -7.1429e+02 -1.1010e+01 -5.7000e+02 -6.8571e+02 -1.2358e+01 -5.7000e+02 -6.5714e+02 -1.3846e+01 -5.7000e+02 -6.2857e+02 -1.5490e+01 -5.7000e+02 -6.0000e+02 -1.7306e+01 -5.7000e+02 -5.7143e+02 -1.9312e+01 -5.7000e+02 -5.4286e+02 -2.1523e+01 -5.7000e+02 -5.1429e+02 -2.3951e+01 -5.7000e+02 -4.8571e+02 -2.6604e+01 -5.7000e+02 -4.5714e+02 -2.9481e+01 -5.7000e+02 -4.2857e+02 -3.2571e+01 -5.7000e+02 -4.0000e+02 -3.5846e+01 -5.7000e+02 -3.7143e+02 -3.9263e+01 -5.7000e+02 -3.4286e+02 -4.2763e+01 -5.7000e+02 -3.1429e+02 -4.6274e+01 -5.7000e+02 -2.8571e+02 -4.9714e+01 -5.7000e+02 -2.5714e+02 -5.3006e+01 -5.7000e+02 -2.2857e+02 -5.6077e+01 -5.7000e+02 -2.0000e+02 -5.8867e+01 -5.7000e+02 -1.7143e+02 -6.1331e+01 -5.7000e+02 -1.4286e+02 -6.3439e+01 -5.7000e+02 -1.1429e+02 -6.5173e+01 -5.7000e+02 -8.5714e+01 -6.6524e+01 -5.7000e+02 -5.7143e+01 -6.7489e+01 -5.7000e+02 -2.8571e+01 -6.8067e+01 -5.7000e+02 0.0000e+00 -6.8260e+01 -5.7000e+02 2.8571e+01 -6.8067e+01 -5.7000e+02 5.7143e+01 -6.7489e+01 -5.7000e+02 8.5714e+01 -6.6524e+01 -5.7000e+02 1.1429e+02 -6.5173e+01 -5.7000e+02 1.4286e+02 -6.3439e+01 -5.7000e+02 1.7143e+02 -6.1331e+01 -5.7000e+02 2.0000e+02 -5.8867e+01 -5.7000e+02 2.2857e+02 -5.6077e+01 -5.7000e+02 2.5714e+02 -5.3006e+01 -5.7000e+02 2.8571e+02 -4.9714e+01 -5.7000e+02 3.1429e+02 -4.6274e+01 -5.7000e+02 3.4286e+02 -4.2763e+01 -5.7000e+02 3.7143e+02 -3.9263e+01 -5.7000e+02 4.0000e+02 -3.5846e+01 -5.7000e+02 4.2857e+02 -3.2571e+01 -5.7000e+02 4.5714e+02 -2.9481e+01 -5.7000e+02 4.8571e+02 -2.6604e+01 -5.7000e+02 5.1429e+02 -2.3951e+01 -5.7000e+02 5.4286e+02 -2.1523e+01 -5.7000e+02 5.7143e+02 -1.9312e+01 -5.7000e+02 6.0000e+02 -1.7306e+01 -5.7000e+02 6.2857e+02 -1.5490e+01 -5.7000e+02 6.5714e+02 -1.3846e+01 -5.7000e+02 6.8571e+02 -1.2358e+01 -5.7000e+02 7.1429e+02 -1.1010e+01 -5.7000e+02 7.4286e+02 -9.7872e+00 -5.7000e+02 7.7143e+02 -8.6758e+00 -5.7000e+02 8.0000e+02 -7.6638e+00 -5.7000e+02 8.2857e+02 -6.7404e+00 -5.7000e+02 8.5714e+02 -5.8959e+00 -5.7000e+02 8.8571e+02 -5.1218e+00 -5.7000e+02 9.1429e+02 -4.4107e+00 -5.7000e+02 9.4286e+02 -3.7560e+00 -5.7000e+02 9.7143e+02 -3.1518e+00 -5.7000e+02 1.0000e+03 -2.5931e+00 -5.7000e+02 1.0286e+03 -2.0753e+00 -5.7000e+02 1.0571e+03 -1.5945e+00 -5.7000e+02 1.0857e+03 -1.1471e+00 -5.7000e+02 1.1143e+03 -7.3003e-01 -5.7000e+02 1.1429e+03 -3.4051e-01 -5.7000e+02 1.1714e+03 2.3931e-02 -5.7000e+02 1.2000e+03 3.6548e-01 -5.7000e+02 1.2286e+03 6.8611e-01 -5.7000e+02 1.2571e+03 9.8756e-01 -5.7000e+02 1.2857e+03 1.2714e+00 -5.7000e+02 1.3143e+03 1.5391e+00 -5.7000e+02 1.3429e+03 1.7919e+00 -5.7000e+02 1.3714e+03 2.0309e+00 -5.7000e+02 1.4000e+03 2.2572e+00 -5.7000e+02 1.4286e+03 2.4717e+00 -5.7000e+02 1.4571e+03 2.6753e+00 -5.7000e+02 1.4857e+03 2.8688e+00 -5.7000e+02 1.5143e+03 3.0528e+00 -5.7000e+02 1.5429e+03 3.2281e+00 -5.7000e+02 1.5714e+03 3.3951e+00 -5.7000e+02 1.6000e+03 3.5545e+00 -5.7000e+02 1.6286e+03 3.7067e+00 -5.7000e+02 1.6571e+03 3.8523e+00 -5.7000e+02 1.6857e+03 3.9915e+00 -5.7000e+02 1.7143e+03 4.1248e+00 -5.7000e+02 1.7429e+03 4.2526e+00 -5.7000e+02 1.7714e+03 4.3752e+00 -5.7000e+02 1.8000e+03 4.4928e+00 -5.7000e+02 1.8286e+03 4.6058e+00 -5.7000e+02 1.8571e+03 4.7144e+00 -5.7000e+02 1.8857e+03 4.8189e+00 -5.7000e+02 1.9143e+03 4.9195e+00 -5.7000e+02 1.9429e+03 5.0164e+00 -5.7000e+02 1.9714e+03 5.1098e+00 -5.7000e+02 2.0000e+03 5.1998e+00 -6.0000e+02 -2.0000e+03 5.2268e+00 -6.0000e+02 -1.9714e+03 5.1381e+00 -6.0000e+02 -1.9429e+03 5.0462e+00 -6.0000e+02 -1.9143e+03 4.9509e+00 -6.0000e+02 -1.8857e+03 4.8520e+00 -6.0000e+02 -1.8571e+03 4.7493e+00 -6.0000e+02 -1.8286e+03 4.6427e+00 -6.0000e+02 -1.8000e+03 4.5318e+00 -6.0000e+02 -1.7714e+03 4.4164e+00 -6.0000e+02 -1.7429e+03 4.2962e+00 -6.0000e+02 -1.7143e+03 4.1711e+00 -6.0000e+02 -1.6857e+03 4.0406e+00 -6.0000e+02 -1.6571e+03 3.9044e+00 -6.0000e+02 -1.6286e+03 3.7622e+00 -6.0000e+02 -1.6000e+03 3.6136e+00 -6.0000e+02 -1.5714e+03 3.4581e+00 -6.0000e+02 -1.5429e+03 3.2953e+00 -6.0000e+02 -1.5143e+03 3.1247e+00 -6.0000e+02 -1.4857e+03 2.9457e+00 -6.0000e+02 -1.4571e+03 2.7578e+00 -6.0000e+02 -1.4286e+03 2.5602e+00 -6.0000e+02 -1.4000e+03 2.3523e+00 -6.0000e+02 -1.3714e+03 2.1333e+00 -6.0000e+02 -1.3429e+03 1.9023e+00 -6.0000e+02 -1.3143e+03 1.6584e+00 -6.0000e+02 -1.2857e+03 1.4006e+00 -6.0000e+02 -1.2571e+03 1.1276e+00 -6.0000e+02 -1.2286e+03 8.3825e-01 -6.0000e+02 -1.2000e+03 5.3111e-01 -6.0000e+02 -1.1714e+03 2.0462e-01 -6.0000e+02 -1.1429e+03 -1.4296e-01 -6.0000e+02 -1.1143e+03 -5.1355e-01 -6.0000e+02 -1.0857e+03 -9.0932e-01 -6.0000e+02 -1.0571e+03 -1.3327e+00 -6.0000e+02 -1.0286e+03 -1.7863e+00 -6.0000e+02 -1.0000e+03 -2.2732e+00 -6.0000e+02 -9.7143e+02 -2.7967e+00 -6.0000e+02 -9.4286e+02 -3.3607e+00 -6.0000e+02 -9.1429e+02 -3.9694e+00 -6.0000e+02 -8.8571e+02 -4.6277e+00 -6.0000e+02 -8.5714e+02 -5.3408e+00 -6.0000e+02 -8.2857e+02 -6.1149e+00 -6.0000e+02 -8.0000e+02 -6.9567e+00 -6.0000e+02 -7.7143e+02 -7.8737e+00 -6.0000e+02 -7.4286e+02 -8.8745e+00 -6.0000e+02 -7.1429e+02 -9.9684e+00 -6.0000e+02 -6.8571e+02 -1.1166e+01 -6.0000e+02 -6.5714e+02 -1.2477e+01 -6.0000e+02 -6.2857e+02 -1.3914e+01 -6.0000e+02 -6.0000e+02 -1.5490e+01 -6.0000e+02 -5.7143e+02 -1.7216e+01 -6.0000e+02 -5.4286e+02 -1.9103e+01 -6.0000e+02 -5.1429e+02 -2.1161e+01 -6.0000e+02 -4.8571e+02 -2.3395e+01 -6.0000e+02 -4.5714e+02 -2.5807e+01 -6.0000e+02 -4.2857e+02 -2.8389e+01 -6.0000e+02 -4.0000e+02 -3.1126e+01 -6.0000e+02 -3.7143e+02 -3.3991e+01 -6.0000e+02 -3.4286e+02 -3.6945e+01 -6.0000e+02 -3.1429e+02 -3.9937e+01 -6.0000e+02 -2.8571e+02 -4.2910e+01 -6.0000e+02 -2.5714e+02 -4.5800e+01 -6.0000e+02 -2.2857e+02 -4.8544e+01 -6.0000e+02 -2.0000e+02 -5.1081e+01 -6.0000e+02 -1.7143e+02 -5.3362e+01 -6.0000e+02 -1.4286e+02 -5.5346e+01 -6.0000e+02 -1.1429e+02 -5.7000e+01 -6.0000e+02 -8.5714e+01 -5.8304e+01 -6.0000e+02 -5.7143e+01 -5.9244e+01 -6.0000e+02 -2.8571e+01 -5.9811e+01 -6.0000e+02 0.0000e+00 -6.0000e+01 -6.0000e+02 2.8571e+01 -5.9811e+01 -6.0000e+02 5.7143e+01 -5.9244e+01 -6.0000e+02 8.5714e+01 -5.8304e+01 -6.0000e+02 1.1429e+02 -5.7000e+01 -6.0000e+02 1.4286e+02 -5.5346e+01 -6.0000e+02 1.7143e+02 -5.3362e+01 -6.0000e+02 2.0000e+02 -5.1081e+01 -6.0000e+02 2.2857e+02 -4.8544e+01 -6.0000e+02 2.5714e+02 -4.5800e+01 -6.0000e+02 2.8571e+02 -4.2910e+01 -6.0000e+02 3.1429e+02 -3.9937e+01 -6.0000e+02 3.4286e+02 -3.6945e+01 -6.0000e+02 3.7143e+02 -3.3991e+01 -6.0000e+02 4.0000e+02 -3.1126e+01 -6.0000e+02 4.2857e+02 -2.8389e+01 -6.0000e+02 4.5714e+02 -2.5807e+01 -6.0000e+02 4.8571e+02 -2.3395e+01 -6.0000e+02 5.1429e+02 -2.1161e+01 -6.0000e+02 5.4286e+02 -1.9103e+01 -6.0000e+02 5.7143e+02 -1.7216e+01 -6.0000e+02 6.0000e+02 -1.5490e+01 -6.0000e+02 6.2857e+02 -1.3914e+01 -6.0000e+02 6.5714e+02 -1.2477e+01 -6.0000e+02 6.8571e+02 -1.1166e+01 -6.0000e+02 7.1429e+02 -9.9684e+00 -6.0000e+02 7.4286e+02 -8.8745e+00 -6.0000e+02 7.7143e+02 -7.8737e+00 -6.0000e+02 8.0000e+02 -6.9567e+00 -6.0000e+02 8.2857e+02 -6.1149e+00 -6.0000e+02 8.5714e+02 -5.3408e+00 -6.0000e+02 8.8571e+02 -4.6277e+00 -6.0000e+02 9.1429e+02 -3.9694e+00 -6.0000e+02 9.4286e+02 -3.3607e+00 -6.0000e+02 9.7143e+02 -2.7967e+00 -6.0000e+02 1.0000e+03 -2.2732e+00 -6.0000e+02 1.0286e+03 -1.7863e+00 -6.0000e+02 1.0571e+03 -1.3327e+00 -6.0000e+02 1.0857e+03 -9.0932e-01 -6.0000e+02 1.1143e+03 -5.1355e-01 -6.0000e+02 1.1429e+03 -1.4296e-01 -6.0000e+02 1.1714e+03 2.0462e-01 -6.0000e+02 1.2000e+03 5.3111e-01 -6.0000e+02 1.2286e+03 8.3825e-01 -6.0000e+02 1.2571e+03 1.1276e+00 -6.0000e+02 1.2857e+03 1.4006e+00 -6.0000e+02 1.3143e+03 1.6584e+00 -6.0000e+02 1.3429e+03 1.9023e+00 -6.0000e+02 1.3714e+03 2.1333e+00 -6.0000e+02 1.4000e+03 2.3523e+00 -6.0000e+02 1.4286e+03 2.5602e+00 -6.0000e+02 1.4571e+03 2.7578e+00 -6.0000e+02 1.4857e+03 2.9457e+00 -6.0000e+02 1.5143e+03 3.1247e+00 -6.0000e+02 1.5429e+03 3.2953e+00 -6.0000e+02 1.5714e+03 3.4581e+00 -6.0000e+02 1.6000e+03 3.6136e+00 -6.0000e+02 1.6286e+03 3.7622e+00 -6.0000e+02 1.6571e+03 3.9044e+00 -6.0000e+02 1.6857e+03 4.0406e+00 -6.0000e+02 1.7143e+03 4.1711e+00 -6.0000e+02 1.7429e+03 4.2962e+00 -6.0000e+02 1.7714e+03 4.4164e+00 -6.0000e+02 1.8000e+03 4.5318e+00 -6.0000e+02 1.8286e+03 4.6427e+00 -6.0000e+02 1.8571e+03 4.7493e+00 -6.0000e+02 1.8857e+03 4.8520e+00 -6.0000e+02 1.9143e+03 4.9509e+00 -6.0000e+02 1.9429e+03 5.0462e+00 -6.0000e+02 1.9714e+03 5.1381e+00 -6.0000e+02 2.0000e+03 5.2268e+00 -6.3000e+02 -2.0000e+03 5.2547e+00 -6.3000e+02 -1.9714e+03 5.1674e+00 -6.3000e+02 -1.9429e+03 5.0770e+00 -6.3000e+02 -1.9143e+03 4.9834e+00 -6.3000e+02 -1.8857e+03 4.8862e+00 -6.3000e+02 -1.8571e+03 4.7854e+00 -6.3000e+02 -1.8286e+03 4.6807e+00 -6.3000e+02 -1.8000e+03 4.5719e+00 -6.3000e+02 -1.7714e+03 4.4588e+00 -6.3000e+02 -1.7429e+03 4.3412e+00 -6.3000e+02 -1.7143e+03 4.2187e+00 -6.3000e+02 -1.6857e+03 4.0911e+00 -6.3000e+02 -1.6571e+03 3.9580e+00 -6.3000e+02 -1.6286e+03 3.8192e+00 -6.3000e+02 -1.6000e+03 3.6742e+00 -6.3000e+02 -1.5714e+03 3.5226e+00 -6.3000e+02 -1.5429e+03 3.3641e+00 -6.3000e+02 -1.5143e+03 3.1981e+00 -6.3000e+02 -1.4857e+03 3.0242e+00 -6.3000e+02 -1.4571e+03 2.8418e+00 -6.3000e+02 -1.4286e+03 2.6503e+00 -6.3000e+02 -1.4000e+03 2.4491e+00 -6.3000e+02 -1.3714e+03 2.2374e+00 -6.3000e+02 -1.3429e+03 2.0144e+00 -6.3000e+02 -1.3143e+03 1.7793e+00 -6.3000e+02 -1.2857e+03 1.5312e+00 -6.3000e+02 -1.2571e+03 1.2690e+00 -6.3000e+02 -1.2286e+03 9.9155e-01 -6.3000e+02 -1.2000e+03 6.9765e-01 -6.3000e+02 -1.1714e+03 3.8590e-01 -6.3000e+02 -1.1429e+03 5.4774e-02 -6.3000e+02 -1.1143e+03 -2.9743e-01 -6.3000e+02 -1.0857e+03 -6.7257e-01 -6.3000e+02 -1.0571e+03 -1.0727e+00 -6.3000e+02 -1.0286e+03 -1.5002e+00 -6.3000e+02 -1.0000e+03 -1.9576e+00 -6.3000e+02 -9.7143e+02 -2.4477e+00 -6.3000e+02 -9.4286e+02 -2.9738e+00 -6.3000e+02 -9.1429e+02 -3.5392e+00 -6.3000e+02 -8.8571e+02 -4.1481e+00 -6.3000e+02 -8.5714e+02 -4.8047e+00 -6.3000e+02 -8.2857e+02 -5.5139e+00 -6.3000e+02 -8.0000e+02 -6.2810e+00 -6.3000e+02 -7.7143e+02 -7.1119e+00 -6.3000e+02 -7.4286e+02 -8.0132e+00 -6.3000e+02 -7.1429e+02 -8.9919e+00 -6.3000e+02 -6.8571e+02 -1.0056e+01 -6.3000e+02 -6.5714e+02 -1.1213e+01 -6.3000e+02 -6.2857e+02 -1.2471e+01 -6.3000e+02 -6.0000e+02 -1.3839e+01 -6.3000e+02 -5.7143e+02 -1.5326e+01 -6.3000e+02 -5.4286e+02 -1.6939e+01 -6.3000e+02 -5.1429e+02 -1.8684e+01 -6.3000e+02 -4.8571e+02 -2.0565e+01 -6.3000e+02 -4.5714e+02 -2.2581e+01 -6.3000e+02 -4.2857e+02 -2.4730e+01 -6.3000e+02 -4.0000e+02 -2.6998e+01 -6.3000e+02 -3.7143e+02 -2.9370e+01 -6.3000e+02 -3.4286e+02 -3.1817e+01 -6.3000e+02 -3.1429e+02 -3.4306e+01 -6.3000e+02 -2.8571e+02 -3.6795e+01 -6.3000e+02 -2.5714e+02 -3.9236e+01 -6.3000e+02 -2.2857e+02 -4.1579e+01 -6.3000e+02 -2.0000e+02 -4.3772e+01 -6.3000e+02 -1.7143e+02 -4.5769e+01 -6.3000e+02 -1.4286e+02 -4.7526e+01 -6.3000e+02 -1.1429e+02 -4.9009e+01 -6.3000e+02 -8.5714e+01 -5.0189e+01 -6.3000e+02 -5.7143e+01 -5.1047e+01 -6.3000e+02 -2.8571e+01 -5.1566e+01 -6.3000e+02 0.0000e+00 -5.1740e+01 -6.3000e+02 2.8571e+01 -5.1566e+01 -6.3000e+02 5.7143e+01 -5.1047e+01 -6.3000e+02 8.5714e+01 -5.0189e+01 -6.3000e+02 1.1429e+02 -4.9009e+01 -6.3000e+02 1.4286e+02 -4.7526e+01 -6.3000e+02 1.7143e+02 -4.5769e+01 -6.3000e+02 2.0000e+02 -4.3772e+01 -6.3000e+02 2.2857e+02 -4.1579e+01 -6.3000e+02 2.5714e+02 -3.9236e+01 -6.3000e+02 2.8571e+02 -3.6795e+01 -6.3000e+02 3.1429e+02 -3.4306e+01 -6.3000e+02 3.4286e+02 -3.1817e+01 -6.3000e+02 3.7143e+02 -2.9370e+01 -6.3000e+02 4.0000e+02 -2.6998e+01 -6.3000e+02 4.2857e+02 -2.4730e+01 -6.3000e+02 4.5714e+02 -2.2581e+01 -6.3000e+02 4.8571e+02 -2.0565e+01 -6.3000e+02 5.1429e+02 -1.8684e+01 -6.3000e+02 5.4286e+02 -1.6939e+01 -6.3000e+02 5.7143e+02 -1.5326e+01 -6.3000e+02 6.0000e+02 -1.3839e+01 -6.3000e+02 6.2857e+02 -1.2471e+01 -6.3000e+02 6.5714e+02 -1.1213e+01 -6.3000e+02 6.8571e+02 -1.0056e+01 -6.3000e+02 7.1429e+02 -8.9919e+00 -6.3000e+02 7.4286e+02 -8.0132e+00 -6.3000e+02 7.7143e+02 -7.1119e+00 -6.3000e+02 8.0000e+02 -6.2810e+00 -6.3000e+02 8.2857e+02 -5.5139e+00 -6.3000e+02 8.5714e+02 -4.8047e+00 -6.3000e+02 8.8571e+02 -4.1481e+00 -6.3000e+02 9.1429e+02 -3.5392e+00 -6.3000e+02 9.4286e+02 -2.9738e+00 -6.3000e+02 9.7143e+02 -2.4477e+00 -6.3000e+02 1.0000e+03 -1.9576e+00 -6.3000e+02 1.0286e+03 -1.5002e+00 -6.3000e+02 1.0571e+03 -1.0727e+00 -6.3000e+02 1.0857e+03 -6.7257e-01 -6.3000e+02 1.1143e+03 -2.9743e-01 -6.3000e+02 1.1429e+03 5.4774e-02 -6.3000e+02 1.1714e+03 3.8590e-01 -6.3000e+02 1.2000e+03 6.9765e-01 -6.3000e+02 1.2286e+03 9.9155e-01 -6.3000e+02 1.2571e+03 1.2690e+00 -6.3000e+02 1.2857e+03 1.5312e+00 -6.3000e+02 1.3143e+03 1.7793e+00 -6.3000e+02 1.3429e+03 2.0144e+00 -6.3000e+02 1.3714e+03 2.2374e+00 -6.3000e+02 1.4000e+03 2.4491e+00 -6.3000e+02 1.4286e+03 2.6503e+00 -6.3000e+02 1.4571e+03 2.8418e+00 -6.3000e+02 1.4857e+03 3.0242e+00 -6.3000e+02 1.5143e+03 3.1981e+00 -6.3000e+02 1.5429e+03 3.3641e+00 -6.3000e+02 1.5714e+03 3.5226e+00 -6.3000e+02 1.6000e+03 3.6742e+00 -6.3000e+02 1.6286e+03 3.8192e+00 -6.3000e+02 1.6571e+03 3.9580e+00 -6.3000e+02 1.6857e+03 4.0911e+00 -6.3000e+02 1.7143e+03 4.2187e+00 -6.3000e+02 1.7429e+03 4.3412e+00 -6.3000e+02 1.7714e+03 4.4588e+00 -6.3000e+02 1.8000e+03 4.5719e+00 -6.3000e+02 1.8286e+03 4.6807e+00 -6.3000e+02 1.8571e+03 4.7854e+00 -6.3000e+02 1.8857e+03 4.8862e+00 -6.3000e+02 1.9143e+03 4.9834e+00 -6.3000e+02 1.9429e+03 5.0770e+00 -6.3000e+02 1.9714e+03 5.1674e+00 -6.3000e+02 2.0000e+03 5.2547e+00 -6.6000e+02 -2.0000e+03 5.2835e+00 -6.6000e+02 -1.9714e+03 5.1977e+00 -6.6000e+02 -1.9429e+03 5.1088e+00 -6.6000e+02 -1.9143e+03 5.0168e+00 -6.6000e+02 -1.8857e+03 4.9214e+00 -6.6000e+02 -1.8571e+03 4.8225e+00 -6.6000e+02 -1.8286e+03 4.7198e+00 -6.6000e+02 -1.8000e+03 4.6132e+00 -6.6000e+02 -1.7714e+03 4.5025e+00 -6.6000e+02 -1.7429e+03 4.3873e+00 -6.6000e+02 -1.7143e+03 4.2675e+00 -6.6000e+02 -1.6857e+03 4.1428e+00 -6.6000e+02 -1.6571e+03 4.0129e+00 -6.6000e+02 -1.6286e+03 3.8774e+00 -6.6000e+02 -1.6000e+03 3.7361e+00 -6.6000e+02 -1.5714e+03 3.5885e+00 -6.6000e+02 -1.5429e+03 3.4343e+00 -6.6000e+02 -1.5143e+03 3.2730e+00 -6.6000e+02 -1.4857e+03 3.1042e+00 -6.6000e+02 -1.4571e+03 2.9273e+00 -6.6000e+02 -1.4286e+03 2.7419e+00 -6.6000e+02 -1.4000e+03 2.5472e+00 -6.6000e+02 -1.3714e+03 2.3427e+00 -6.6000e+02 -1.3429e+03 2.1277e+00 -6.6000e+02 -1.3143e+03 1.9013e+00 -6.6000e+02 -1.2857e+03 1.6628e+00 -6.6000e+02 -1.2571e+03 1.4112e+00 -6.6000e+02 -1.2286e+03 1.1455e+00 -6.6000e+02 -1.2000e+03 8.6453e-01 -6.6000e+02 -1.1714e+03 5.6715e-01 -6.6000e+02 -1.1429e+03 2.5201e-01 -6.6000e+02 -1.1143e+03 -8.2383e-02 -6.6000e+02 -1.0857e+03 -4.3764e-01 -6.6000e+02 -1.0571e+03 -8.1554e-01 -6.6000e+02 -1.0286e+03 -1.2181e+00 -6.6000e+02 -1.0000e+03 -1.6473e+00 -6.6000e+02 -9.7143e+02 -2.1058e+00 -6.6000e+02 -9.4286e+02 -2.5961e+00 -6.6000e+02 -9.1429e+02 -3.1211e+00 -6.6000e+02 -8.8571e+02 -3.6840e+00 -6.6000e+02 -8.5714e+02 -4.2883e+00 -6.6000e+02 -8.2857e+02 -4.9379e+00 -6.6000e+02 -8.0000e+02 -5.6369e+00 -6.6000e+02 -7.7143e+02 -6.3900e+00 -6.6000e+02 -7.4286e+02 -7.2021e+00 -6.6000e+02 -7.1429e+02 -8.0784e+00 -6.6000e+02 -6.8571e+02 -9.0245e+00 -6.6000e+02 -6.5714e+02 -1.0046e+01 -6.6000e+02 -6.2857e+02 -1.1150e+01 -6.6000e+02 -6.0000e+02 -1.2340e+01 -6.6000e+02 -5.7143e+02 -1.3624e+01 -6.6000e+02 -5.4286e+02 -1.5005e+01 -6.6000e+02 -5.1429e+02 -1.6488e+01 -6.6000e+02 -4.8571e+02 -1.8073e+01 -6.6000e+02 -4.5714e+02 -1.9761e+01 -6.6000e+02 -4.2857e+02 -2.1547e+01 -6.6000e+02 -4.0000e+02 -2.3422e+01 -6.6000e+02 -3.7143e+02 -2.5374e+01 -6.6000e+02 -3.4286e+02 -2.7383e+01 -6.6000e+02 -3.1429e+02 -2.9425e+01 -6.6000e+02 -2.8571e+02 -3.1468e+01 -6.6000e+02 -2.5714e+02 -3.3479e+01 -6.6000e+02 -2.2857e+02 -3.5417e+01 -6.6000e+02 -2.0000e+02 -3.7243e+01 -6.6000e+02 -1.7143e+02 -3.8917e+01 -6.6000e+02 -1.4286e+02 -4.0401e+01 -6.6000e+02 -1.1429e+02 -4.1663e+01 -6.6000e+02 -8.5714e+01 -4.2674e+01 -6.6000e+02 -5.7143e+01 -4.3412e+01 -6.6000e+02 -2.8571e+01 -4.3861e+01 -6.6000e+02 0.0000e+00 -4.4012e+01 -6.6000e+02 2.8571e+01 -4.3861e+01 -6.6000e+02 5.7143e+01 -4.3412e+01 -6.6000e+02 8.5714e+01 -4.2674e+01 -6.6000e+02 1.1429e+02 -4.1663e+01 -6.6000e+02 1.4286e+02 -4.0401e+01 -6.6000e+02 1.7143e+02 -3.8917e+01 -6.6000e+02 2.0000e+02 -3.7243e+01 -6.6000e+02 2.2857e+02 -3.5417e+01 -6.6000e+02 2.5714e+02 -3.3479e+01 -6.6000e+02 2.8571e+02 -3.1468e+01 -6.6000e+02 3.1429e+02 -2.9425e+01 -6.6000e+02 3.4286e+02 -2.7383e+01 -6.6000e+02 3.7143e+02 -2.5374e+01 -6.6000e+02 4.0000e+02 -2.3422e+01 -6.6000e+02 4.2857e+02 -2.1547e+01 -6.6000e+02 4.5714e+02 -1.9761e+01 -6.6000e+02 4.8571e+02 -1.8073e+01 -6.6000e+02 5.1429e+02 -1.6488e+01 -6.6000e+02 5.4286e+02 -1.5005e+01 -6.6000e+02 5.7143e+02 -1.3624e+01 -6.6000e+02 6.0000e+02 -1.2340e+01 -6.6000e+02 6.2857e+02 -1.1150e+01 -6.6000e+02 6.5714e+02 -1.0046e+01 -6.6000e+02 6.8571e+02 -9.0245e+00 -6.6000e+02 7.1429e+02 -8.0784e+00 -6.6000e+02 7.4286e+02 -7.2021e+00 -6.6000e+02 7.7143e+02 -6.3900e+00 -6.6000e+02 8.0000e+02 -5.6369e+00 -6.6000e+02 8.2857e+02 -4.9379e+00 -6.6000e+02 8.5714e+02 -4.2883e+00 -6.6000e+02 8.8571e+02 -3.6840e+00 -6.6000e+02 9.1429e+02 -3.1211e+00 -6.6000e+02 9.4286e+02 -2.5961e+00 -6.6000e+02 9.7143e+02 -2.1058e+00 -6.6000e+02 1.0000e+03 -1.6473e+00 -6.6000e+02 1.0286e+03 -1.2181e+00 -6.6000e+02 1.0571e+03 -8.1554e-01 -6.6000e+02 1.0857e+03 -4.3764e-01 -6.6000e+02 1.1143e+03 -8.2383e-02 -6.6000e+02 1.1429e+03 2.5201e-01 -6.6000e+02 1.1714e+03 5.6715e-01 -6.6000e+02 1.2000e+03 8.6453e-01 -6.6000e+02 1.2286e+03 1.1455e+00 -6.6000e+02 1.2571e+03 1.4112e+00 -6.6000e+02 1.2857e+03 1.6628e+00 -6.6000e+02 1.3143e+03 1.9013e+00 -6.6000e+02 1.3429e+03 2.1277e+00 -6.6000e+02 1.3714e+03 2.3427e+00 -6.6000e+02 1.4000e+03 2.5472e+00 -6.6000e+02 1.4286e+03 2.7419e+00 -6.6000e+02 1.4571e+03 2.9273e+00 -6.6000e+02 1.4857e+03 3.1042e+00 -6.6000e+02 1.5143e+03 3.2730e+00 -6.6000e+02 1.5429e+03 3.4343e+00 -6.6000e+02 1.5714e+03 3.5885e+00 -6.6000e+02 1.6000e+03 3.7361e+00 -6.6000e+02 1.6286e+03 3.8774e+00 -6.6000e+02 1.6571e+03 4.0129e+00 -6.6000e+02 1.6857e+03 4.1428e+00 -6.6000e+02 1.7143e+03 4.2675e+00 -6.6000e+02 1.7429e+03 4.3873e+00 -6.6000e+02 1.7714e+03 4.5025e+00 -6.6000e+02 1.8000e+03 4.6132e+00 -6.6000e+02 1.8286e+03 4.7198e+00 -6.6000e+02 1.8571e+03 4.8225e+00 -6.6000e+02 1.8857e+03 4.9214e+00 -6.6000e+02 1.9143e+03 5.0168e+00 -6.6000e+02 1.9429e+03 5.1088e+00 -6.6000e+02 1.9714e+03 5.1977e+00 -6.6000e+02 2.0000e+03 5.2835e+00 -6.9000e+02 -2.0000e+03 5.3131e+00 -6.9000e+02 -1.9714e+03 5.2288e+00 -6.9000e+02 -1.9429e+03 5.1415e+00 -6.9000e+02 -1.9143e+03 5.0512e+00 -6.9000e+02 -1.8857e+03 4.9576e+00 -6.9000e+02 -1.8571e+03 4.8606e+00 -6.9000e+02 -1.8286e+03 4.7600e+00 -6.9000e+02 -1.8000e+03 4.6556e+00 -6.9000e+02 -1.7714e+03 4.5472e+00 -6.9000e+02 -1.7429e+03 4.4345e+00 -6.9000e+02 -1.7143e+03 4.3175e+00 -6.9000e+02 -1.6857e+03 4.1957e+00 -6.9000e+02 -1.6571e+03 4.0689e+00 -6.9000e+02 -1.6286e+03 3.9368e+00 -6.9000e+02 -1.6000e+03 3.7992e+00 -6.9000e+02 -1.5714e+03 3.6556e+00 -6.9000e+02 -1.5429e+03 3.5057e+00 -6.9000e+02 -1.5143e+03 3.3491e+00 -6.9000e+02 -1.4857e+03 3.1853e+00 -6.9000e+02 -1.4571e+03 3.0140e+00 -6.9000e+02 -1.4286e+03 2.8345e+00 -6.9000e+02 -1.4000e+03 2.6464e+00 -6.9000e+02 -1.3714e+03 2.4491e+00 -6.9000e+02 -1.3429e+03 2.2419e+00 -6.9000e+02 -1.3143e+03 2.0242e+00 -6.9000e+02 -1.2857e+03 1.7951e+00 -6.9000e+02 -1.2571e+03 1.5538e+00 -6.9000e+02 -1.2286e+03 1.2995e+00 -6.9000e+02 -1.2000e+03 1.0312e+00 -6.9000e+02 -1.1714e+03 7.4781e-01 -6.9000e+02 -1.1429e+03 4.4813e-01 -6.9000e+02 -1.1143e+03 1.3092e-01 -6.9000e+02 -1.0857e+03 -2.0522e-01 -6.9000e+02 -1.0571e+03 -5.6181e-01 -6.9000e+02 -1.0286e+03 -9.4053e-01 -6.9000e+02 -1.0000e+03 -1.3432e+00 -6.9000e+02 -9.7143e+02 -1.7718e+00 -6.9000e+02 -9.4286e+02 -2.2285e+00 -6.9000e+02 -9.1429e+02 -2.7157e+00 -6.9000e+02 -8.8571e+02 -3.2360e+00 -6.9000e+02 -8.5714e+02 -3.7921e+00 -6.9000e+02 -8.2857e+02 -4.3871e+00 -6.9000e+02 -8.0000e+02 -5.0243e+00 -6.9000e+02 -7.7143e+02 -5.7071e+00 -6.9000e+02 -7.4286e+02 -6.4392e+00 -6.9000e+02 -7.1429e+02 -7.2246e+00 -6.9000e+02 -6.8571e+02 -8.0672e+00 -6.9000e+02 -6.5714e+02 -8.9712e+00 -6.9000e+02 -6.2857e+02 -9.9405e+00 -6.9000e+02 -6.0000e+02 -1.0979e+01 -6.9000e+02 -5.7143e+02 -1.2090e+01 -6.9000e+02 -5.4286e+02 -1.3276e+01 -6.9000e+02 -5.1429e+02 -1.4540e+01 -6.9000e+02 -4.8571e+02 -1.5880e+01 -6.9000e+02 -4.5714e+02 -1.7296e+01 -6.9000e+02 -4.2857e+02 -1.8784e+01 -6.9000e+02 -4.0000e+02 -2.0336e+01 -6.9000e+02 -3.7143e+02 -2.1942e+01 -6.9000e+02 -3.4286e+02 -2.3586e+01 -6.9000e+02 -3.1429e+02 -2.5251e+01 -6.9000e+02 -2.8571e+02 -2.6914e+01 -6.9000e+02 -2.5714e+02 -2.8548e+01 -6.9000e+02 -2.2857e+02 -3.0123e+01 -6.9000e+02 -2.0000e+02 -3.1610e+01 -6.9000e+02 -1.7143e+02 -3.2976e+01 -6.9000e+02 -1.4286e+02 -3.4191e+01 -6.9000e+02 -1.1429e+02 -3.5227e+01 -6.9000e+02 -8.5714e+01 -3.6060e+01 -6.9000e+02 -5.7143e+01 -3.6670e+01 -6.9000e+02 -2.8571e+01 -3.7041e+01 -6.9000e+02 0.0000e+00 -3.7166e+01 -6.9000e+02 2.8571e+01 -3.7041e+01 -6.9000e+02 5.7143e+01 -3.6670e+01 -6.9000e+02 8.5714e+01 -3.6060e+01 -6.9000e+02 1.1429e+02 -3.5227e+01 -6.9000e+02 1.4286e+02 -3.4191e+01 -6.9000e+02 1.7143e+02 -3.2976e+01 -6.9000e+02 2.0000e+02 -3.1610e+01 -6.9000e+02 2.2857e+02 -3.0123e+01 -6.9000e+02 2.5714e+02 -2.8548e+01 -6.9000e+02 2.8571e+02 -2.6914e+01 -6.9000e+02 3.1429e+02 -2.5251e+01 -6.9000e+02 3.4286e+02 -2.3586e+01 -6.9000e+02 3.7143e+02 -2.1942e+01 -6.9000e+02 4.0000e+02 -2.0336e+01 -6.9000e+02 4.2857e+02 -1.8784e+01 -6.9000e+02 4.5714e+02 -1.7296e+01 -6.9000e+02 4.8571e+02 -1.5880e+01 -6.9000e+02 5.1429e+02 -1.4540e+01 -6.9000e+02 5.4286e+02 -1.3276e+01 -6.9000e+02 5.7143e+02 -1.2090e+01 -6.9000e+02 6.0000e+02 -1.0979e+01 -6.9000e+02 6.2857e+02 -9.9405e+00 -6.9000e+02 6.5714e+02 -8.9712e+00 -6.9000e+02 6.8571e+02 -8.0672e+00 -6.9000e+02 7.1429e+02 -7.2246e+00 -6.9000e+02 7.4286e+02 -6.4392e+00 -6.9000e+02 7.7143e+02 -5.7071e+00 -6.9000e+02 8.0000e+02 -5.0243e+00 -6.9000e+02 8.2857e+02 -4.3871e+00 -6.9000e+02 8.5714e+02 -3.7921e+00 -6.9000e+02 8.8571e+02 -3.2360e+00 -6.9000e+02 9.1429e+02 -2.7157e+00 -6.9000e+02 9.4286e+02 -2.2285e+00 -6.9000e+02 9.7143e+02 -1.7718e+00 -6.9000e+02 1.0000e+03 -1.3432e+00 -6.9000e+02 1.0286e+03 -9.4053e-01 -6.9000e+02 1.0571e+03 -5.6181e-01 -6.9000e+02 1.0857e+03 -2.0522e-01 -6.9000e+02 1.1143e+03 1.3092e-01 -6.9000e+02 1.1429e+03 4.4813e-01 -6.9000e+02 1.1714e+03 7.4781e-01 -6.9000e+02 1.2000e+03 1.0312e+00 -6.9000e+02 1.2286e+03 1.2995e+00 -6.9000e+02 1.2571e+03 1.5538e+00 -6.9000e+02 1.2857e+03 1.7951e+00 -6.9000e+02 1.3143e+03 2.0242e+00 -6.9000e+02 1.3429e+03 2.2419e+00 -6.9000e+02 1.3714e+03 2.4491e+00 -6.9000e+02 1.4000e+03 2.6464e+00 -6.9000e+02 1.4286e+03 2.8345e+00 -6.9000e+02 1.4571e+03 3.0140e+00 -6.9000e+02 1.4857e+03 3.1853e+00 -6.9000e+02 1.5143e+03 3.3491e+00 -6.9000e+02 1.5429e+03 3.5057e+00 -6.9000e+02 1.5714e+03 3.6556e+00 -6.9000e+02 1.6000e+03 3.7992e+00 -6.9000e+02 1.6286e+03 3.9368e+00 -6.9000e+02 1.6571e+03 4.0689e+00 -6.9000e+02 1.6857e+03 4.1957e+00 -6.9000e+02 1.7143e+03 4.3175e+00 -6.9000e+02 1.7429e+03 4.4345e+00 -6.9000e+02 1.7714e+03 4.5472e+00 -6.9000e+02 1.8000e+03 4.6556e+00 -6.9000e+02 1.8286e+03 4.7600e+00 -6.9000e+02 1.8571e+03 4.8606e+00 -6.9000e+02 1.8857e+03 4.9576e+00 -6.9000e+02 1.9143e+03 5.0512e+00 -6.9000e+02 1.9429e+03 5.1415e+00 -6.9000e+02 1.9714e+03 5.2288e+00 -6.9000e+02 2.0000e+03 5.3131e+00 -7.2000e+02 -2.0000e+03 5.3435e+00 -7.2000e+02 -1.9714e+03 5.2607e+00 -7.2000e+02 -1.9429e+03 5.1750e+00 -7.2000e+02 -1.9143e+03 5.0864e+00 -7.2000e+02 -1.8857e+03 4.9946e+00 -7.2000e+02 -1.8571e+03 4.8995e+00 -7.2000e+02 -1.8286e+03 4.8010e+00 -7.2000e+02 -1.8000e+03 4.6988e+00 -7.2000e+02 -1.7714e+03 4.5928e+00 -7.2000e+02 -1.7429e+03 4.4827e+00 -7.2000e+02 -1.7143e+03 4.3684e+00 -7.2000e+02 -1.6857e+03 4.2496e+00 -7.2000e+02 -1.6571e+03 4.1259e+00 -7.2000e+02 -1.6286e+03 3.9973e+00 -7.2000e+02 -1.6000e+03 3.8633e+00 -7.2000e+02 -1.5714e+03 3.7237e+00 -7.2000e+02 -1.5429e+03 3.5781e+00 -7.2000e+02 -1.5143e+03 3.4261e+00 -7.2000e+02 -1.4857e+03 3.2674e+00 -7.2000e+02 -1.4571e+03 3.1016e+00 -7.2000e+02 -1.4286e+03 2.9281e+00 -7.2000e+02 -1.4000e+03 2.7465e+00 -7.2000e+02 -1.3714e+03 2.5562e+00 -7.2000e+02 -1.3429e+03 2.3567e+00 -7.2000e+02 -1.3143e+03 2.1474e+00 -7.2000e+02 -1.2857e+03 1.9276e+00 -7.2000e+02 -1.2571e+03 1.6965e+00 -7.2000e+02 -1.2286e+03 1.4533e+00 -7.2000e+02 -1.2000e+03 1.1973e+00 -7.2000e+02 -1.1714e+03 9.2737e-01 -7.2000e+02 -1.1429e+03 6.4262e-01 -7.2000e+02 -1.1143e+03 3.4193e-01 -7.2000e+02 -1.0857e+03 2.4092e-02 -7.2000e+02 -1.0571e+03 -3.1218e-01 -7.2000e+02 -1.0286e+03 -6.6830e-01 -7.2000e+02 -1.0000e+03 -1.0458e+00 -7.2000e+02 -9.7143e+02 -1.4463e+00 -7.2000e+02 -9.4286e+02 -1.8716e+00 -7.2000e+02 -9.1429e+02 -2.3236e+00 -7.2000e+02 -8.8571e+02 -2.8044e+00 -7.2000e+02 -8.5714e+02 -3.3162e+00 -7.2000e+02 -8.2857e+02 -3.8613e+00 -7.2000e+02 -8.0000e+02 -4.4424e+00 -7.2000e+02 -7.7143e+02 -5.0618e+00 -7.2000e+02 -7.4286e+02 -5.7226e+00 -7.2000e+02 -7.1429e+02 -6.4273e+00 -7.2000e+02 -6.8571e+02 -7.1789e+00 -7.2000e+02 -6.5714e+02 -7.9801e+00 -7.2000e+02 -6.2857e+02 -8.8336e+00 -7.2000e+02 -6.0000e+02 -9.7417e+00 -7.2000e+02 -5.7143e+02 -1.0706e+01 -7.2000e+02 -5.4286e+02 -1.1729e+01 -7.2000e+02 -5.1429e+02 -1.2809e+01 -7.2000e+02 -4.8571e+02 -1.3947e+01 -7.2000e+02 -4.5714e+02 -1.5140e+01 -7.2000e+02 -4.2857e+02 -1.6385e+01 -7.2000e+02 -4.0000e+02 -1.7673e+01 -7.2000e+02 -3.7143e+02 -1.8998e+01 -7.2000e+02 -3.4286e+02 -2.0346e+01 -7.2000e+02 -3.1429e+02 -2.1704e+01 -7.2000e+02 -2.8571e+02 -2.3054e+01 -7.2000e+02 -2.5714e+02 -2.4377e+01 -7.2000e+02 -2.2857e+02 -2.5648e+01 -7.2000e+02 -2.0000e+02 -2.6846e+01 -7.2000e+02 -1.7143e+02 -2.7947e+01 -7.2000e+02 -1.4286e+02 -2.8925e+01 -7.2000e+02 -1.1429e+02 -2.9760e+01 -7.2000e+02 -8.5714e+01 -3.0431e+01 -7.2000e+02 -5.7143e+01 -3.0923e+01 -7.2000e+02 -2.8571e+01 -3.1223e+01 -7.2000e+02 0.0000e+00 -3.1323e+01 -7.2000e+02 2.8571e+01 -3.1223e+01 -7.2000e+02 5.7143e+01 -3.0923e+01 -7.2000e+02 8.5714e+01 -3.0431e+01 -7.2000e+02 1.1429e+02 -2.9760e+01 -7.2000e+02 1.4286e+02 -2.8925e+01 -7.2000e+02 1.7143e+02 -2.7947e+01 -7.2000e+02 2.0000e+02 -2.6846e+01 -7.2000e+02 2.2857e+02 -2.5648e+01 -7.2000e+02 2.5714e+02 -2.4377e+01 -7.2000e+02 2.8571e+02 -2.3054e+01 -7.2000e+02 3.1429e+02 -2.1704e+01 -7.2000e+02 3.4286e+02 -2.0346e+01 -7.2000e+02 3.7143e+02 -1.8998e+01 -7.2000e+02 4.0000e+02 -1.7673e+01 -7.2000e+02 4.2857e+02 -1.6385e+01 -7.2000e+02 4.5714e+02 -1.5140e+01 -7.2000e+02 4.8571e+02 -1.3947e+01 -7.2000e+02 5.1429e+02 -1.2809e+01 -7.2000e+02 5.4286e+02 -1.1729e+01 -7.2000e+02 5.7143e+02 -1.0706e+01 -7.2000e+02 6.0000e+02 -9.7417e+00 -7.2000e+02 6.2857e+02 -8.8336e+00 -7.2000e+02 6.5714e+02 -7.9801e+00 -7.2000e+02 6.8571e+02 -7.1789e+00 -7.2000e+02 7.1429e+02 -6.4273e+00 -7.2000e+02 7.4286e+02 -5.7226e+00 -7.2000e+02 7.7143e+02 -5.0618e+00 -7.2000e+02 8.0000e+02 -4.4424e+00 -7.2000e+02 8.2857e+02 -3.8613e+00 -7.2000e+02 8.5714e+02 -3.3162e+00 -7.2000e+02 8.8571e+02 -2.8044e+00 -7.2000e+02 9.1429e+02 -2.3236e+00 -7.2000e+02 9.4286e+02 -1.8716e+00 -7.2000e+02 9.7143e+02 -1.4463e+00 -7.2000e+02 1.0000e+03 -1.0458e+00 -7.2000e+02 1.0286e+03 -6.6830e-01 -7.2000e+02 1.0571e+03 -3.1218e-01 -7.2000e+02 1.0857e+03 2.4092e-02 -7.2000e+02 1.1143e+03 3.4193e-01 -7.2000e+02 1.1429e+03 6.4262e-01 -7.2000e+02 1.1714e+03 9.2737e-01 -7.2000e+02 1.2000e+03 1.1973e+00 -7.2000e+02 1.2286e+03 1.4533e+00 -7.2000e+02 1.2571e+03 1.6965e+00 -7.2000e+02 1.2857e+03 1.9276e+00 -7.2000e+02 1.3143e+03 2.1474e+00 -7.2000e+02 1.3429e+03 2.3567e+00 -7.2000e+02 1.3714e+03 2.5562e+00 -7.2000e+02 1.4000e+03 2.7465e+00 -7.2000e+02 1.4286e+03 2.9281e+00 -7.2000e+02 1.4571e+03 3.1016e+00 -7.2000e+02 1.4857e+03 3.2674e+00 -7.2000e+02 1.5143e+03 3.4261e+00 -7.2000e+02 1.5429e+03 3.5781e+00 -7.2000e+02 1.5714e+03 3.7237e+00 -7.2000e+02 1.6000e+03 3.8633e+00 -7.2000e+02 1.6286e+03 3.9973e+00 -7.2000e+02 1.6571e+03 4.1259e+00 -7.2000e+02 1.6857e+03 4.2496e+00 -7.2000e+02 1.7143e+03 4.3684e+00 -7.2000e+02 1.7429e+03 4.4827e+00 -7.2000e+02 1.7714e+03 4.5928e+00 -7.2000e+02 1.8000e+03 4.6988e+00 -7.2000e+02 1.8286e+03 4.8010e+00 -7.2000e+02 1.8571e+03 4.8995e+00 -7.2000e+02 1.8857e+03 4.9946e+00 -7.2000e+02 1.9143e+03 5.0864e+00 -7.2000e+02 1.9429e+03 5.1750e+00 -7.2000e+02 1.9714e+03 5.2607e+00 -7.2000e+02 2.0000e+03 5.3435e+00 -7.5000e+02 -2.0000e+03 5.3747e+00 -7.5000e+02 -1.9714e+03 5.2934e+00 -7.5000e+02 -1.9429e+03 5.2093e+00 -7.5000e+02 -1.9143e+03 5.1224e+00 -7.5000e+02 -1.8857e+03 5.0324e+00 -7.5000e+02 -1.8571e+03 4.9393e+00 -7.5000e+02 -1.8286e+03 4.8429e+00 -7.5000e+02 -1.8000e+03 4.7429e+00 -7.5000e+02 -1.7714e+03 4.6393e+00 -7.5000e+02 -1.7429e+03 4.5318e+00 -7.5000e+02 -1.7143e+03 4.4202e+00 -7.5000e+02 -1.6857e+03 4.3043e+00 -7.5000e+02 -1.6571e+03 4.1839e+00 -7.5000e+02 -1.6286e+03 4.0587e+00 -7.5000e+02 -1.6000e+03 3.9284e+00 -7.5000e+02 -1.5714e+03 3.7927e+00 -7.5000e+02 -1.5429e+03 3.6514e+00 -7.5000e+02 -1.5143e+03 3.5040e+00 -7.5000e+02 -1.4857e+03 3.3503e+00 -7.5000e+02 -1.4571e+03 3.1899e+00 -7.5000e+02 -1.4286e+03 3.0223e+00 -7.5000e+02 -1.4000e+03 2.8471e+00 -7.5000e+02 -1.3714e+03 2.6638e+00 -7.5000e+02 -1.3429e+03 2.4719e+00 -7.5000e+02 -1.3143e+03 2.2708e+00 -7.5000e+02 -1.2857e+03 2.0600e+00 -7.5000e+02 -1.2571e+03 1.8387e+00 -7.5000e+02 -1.2286e+03 1.6064e+00 -7.5000e+02 -1.2000e+03 1.3622e+00 -7.5000e+02 -1.1714e+03 1.1054e+00 -7.5000e+02 -1.1429e+03 8.3500e-01 -7.5000e+02 -1.1143e+03 5.5014e-01 -7.5000e+02 -1.0857e+03 2.4978e-01 -7.5000e+02 -1.0571e+03 -6.7162e-02 -7.5000e+02 -1.0286e+03 -4.0187e-01 -7.5000e+02 -1.0000e+03 -7.5562e-01 -7.5000e+02 -9.7143e+02 -1.1297e+00 -7.5000e+02 -9.4286e+02 -1.5257e+00 -7.5000e+02 -9.1429e+02 -1.9450e+00 -7.5000e+02 -8.8571e+02 -2.3894e+00 -7.5000e+02 -8.5714e+02 -2.8605e+00 -7.5000e+02 -8.2857e+02 -3.3602e+00 -7.5000e+02 -8.0000e+02 -3.8903e+00 -7.5000e+02 -7.7143e+02 -4.4528e+00 -7.5000e+02 -7.4286e+02 -5.0497e+00 -7.5000e+02 -7.1429e+02 -5.6830e+00 -7.5000e+02 -6.8571e+02 -6.3546e+00 -7.5000e+02 -6.5714e+02 -7.0663e+00 -7.5000e+02 -6.2857e+02 -7.8196e+00 -7.5000e+02 -6.0000e+02 -8.6160e+00 -7.5000e+02 -5.7143e+02 -9.4562e+00 -7.5000e+02 -5.4286e+02 -1.0341e+01 -7.5000e+02 -5.1429e+02 -1.1269e+01 -7.5000e+02 -4.8571e+02 -1.2239e+01 -7.5000e+02 -4.5714e+02 -1.3249e+01 -7.5000e+02 -4.2857e+02 -1.4295e+01 -7.5000e+02 -4.0000e+02 -1.5371e+01 -7.5000e+02 -3.7143e+02 -1.6468e+01 -7.5000e+02 -3.4286e+02 -1.7579e+01 -7.5000e+02 -3.1429e+02 -1.8691e+01 -7.5000e+02 -2.8571e+02 -1.9791e+01 -7.5000e+02 -2.5714e+02 -2.0862e+01 -7.5000e+02 -2.2857e+02 -2.1889e+01 -7.5000e+02 -2.0000e+02 -2.2853e+01 -7.5000e+02 -1.7143e+02 -2.3736e+01 -7.5000e+02 -1.4286e+02 -2.4520e+01 -7.5000e+02 -1.1429e+02 -2.5188e+01 -7.5000e+02 -8.5714e+01 -2.5724e+01 -7.5000e+02 -5.7143e+01 -2.6117e+01 -7.5000e+02 -2.8571e+01 -2.6357e+01 -7.5000e+02 0.0000e+00 -2.6437e+01 -7.5000e+02 2.8571e+01 -2.6357e+01 -7.5000e+02 5.7143e+01 -2.6117e+01 -7.5000e+02 8.5714e+01 -2.5724e+01 -7.5000e+02 1.1429e+02 -2.5188e+01 -7.5000e+02 1.4286e+02 -2.4520e+01 -7.5000e+02 1.7143e+02 -2.3736e+01 -7.5000e+02 2.0000e+02 -2.2853e+01 -7.5000e+02 2.2857e+02 -2.1889e+01 -7.5000e+02 2.5714e+02 -2.0862e+01 -7.5000e+02 2.8571e+02 -1.9791e+01 -7.5000e+02 3.1429e+02 -1.8691e+01 -7.5000e+02 3.4286e+02 -1.7579e+01 -7.5000e+02 3.7143e+02 -1.6468e+01 -7.5000e+02 4.0000e+02 -1.5371e+01 -7.5000e+02 4.2857e+02 -1.4295e+01 -7.5000e+02 4.5714e+02 -1.3249e+01 -7.5000e+02 4.8571e+02 -1.2239e+01 -7.5000e+02 5.1429e+02 -1.1269e+01 -7.5000e+02 5.4286e+02 -1.0341e+01 -7.5000e+02 5.7143e+02 -9.4562e+00 -7.5000e+02 6.0000e+02 -8.6160e+00 -7.5000e+02 6.2857e+02 -7.8196e+00 -7.5000e+02 6.5714e+02 -7.0663e+00 -7.5000e+02 6.8571e+02 -6.3546e+00 -7.5000e+02 7.1429e+02 -5.6830e+00 -7.5000e+02 7.4286e+02 -5.0497e+00 -7.5000e+02 7.7143e+02 -4.4528e+00 -7.5000e+02 8.0000e+02 -3.8903e+00 -7.5000e+02 8.2857e+02 -3.3602e+00 -7.5000e+02 8.5714e+02 -2.8605e+00 -7.5000e+02 8.8571e+02 -2.3894e+00 -7.5000e+02 9.1429e+02 -1.9450e+00 -7.5000e+02 9.4286e+02 -1.5257e+00 -7.5000e+02 9.7143e+02 -1.1297e+00 -7.5000e+02 1.0000e+03 -7.5562e-01 -7.5000e+02 1.0286e+03 -4.0187e-01 -7.5000e+02 1.0571e+03 -6.7162e-02 -7.5000e+02 1.0857e+03 2.4978e-01 -7.5000e+02 1.1143e+03 5.5014e-01 -7.5000e+02 1.1429e+03 8.3500e-01 -7.5000e+02 1.1714e+03 1.1054e+00 -7.5000e+02 1.2000e+03 1.3622e+00 -7.5000e+02 1.2286e+03 1.6064e+00 -7.5000e+02 1.2571e+03 1.8387e+00 -7.5000e+02 1.2857e+03 2.0600e+00 -7.5000e+02 1.3143e+03 2.2708e+00 -7.5000e+02 1.3429e+03 2.4719e+00 -7.5000e+02 1.3714e+03 2.6638e+00 -7.5000e+02 1.4000e+03 2.8471e+00 -7.5000e+02 1.4286e+03 3.0223e+00 -7.5000e+02 1.4571e+03 3.1899e+00 -7.5000e+02 1.4857e+03 3.3503e+00 -7.5000e+02 1.5143e+03 3.5040e+00 -7.5000e+02 1.5429e+03 3.6514e+00 -7.5000e+02 1.5714e+03 3.7927e+00 -7.5000e+02 1.6000e+03 3.9284e+00 -7.5000e+02 1.6286e+03 4.0587e+00 -7.5000e+02 1.6571e+03 4.1839e+00 -7.5000e+02 1.6857e+03 4.3043e+00 -7.5000e+02 1.7143e+03 4.4202e+00 -7.5000e+02 1.7429e+03 4.5318e+00 -7.5000e+02 1.7714e+03 4.6393e+00 -7.5000e+02 1.8000e+03 4.7429e+00 -7.5000e+02 1.8286e+03 4.8429e+00 -7.5000e+02 1.8571e+03 4.9393e+00 -7.5000e+02 1.8857e+03 5.0324e+00 -7.5000e+02 1.9143e+03 5.1224e+00 -7.5000e+02 1.9429e+03 5.2093e+00 -7.5000e+02 1.9714e+03 5.2934e+00 -7.5000e+02 2.0000e+03 5.3747e+00 -7.8000e+02 -2.0000e+03 5.4065e+00 -7.8000e+02 -1.9714e+03 5.3267e+00 -7.8000e+02 -1.9429e+03 5.2443e+00 -7.8000e+02 -1.9143e+03 5.1591e+00 -7.8000e+02 -1.8857e+03 5.0710e+00 -7.8000e+02 -1.8571e+03 4.9799e+00 -7.8000e+02 -1.8286e+03 4.8855e+00 -7.8000e+02 -1.8000e+03 4.7878e+00 -7.8000e+02 -1.7714e+03 4.6866e+00 -7.8000e+02 -1.7429e+03 4.5817e+00 -7.8000e+02 -1.7143e+03 4.4728e+00 -7.8000e+02 -1.6857e+03 4.3599e+00 -7.8000e+02 -1.6571e+03 4.2426e+00 -7.8000e+02 -1.6286e+03 4.1208e+00 -7.8000e+02 -1.6000e+03 3.9941e+00 -7.8000e+02 -1.5714e+03 3.8624e+00 -7.8000e+02 -1.5429e+03 3.7253e+00 -7.8000e+02 -1.5143e+03 3.5826e+00 -7.8000e+02 -1.4857e+03 3.4338e+00 -7.8000e+02 -1.4571e+03 3.2787e+00 -7.8000e+02 -1.4286e+03 3.1169e+00 -7.8000e+02 -1.4000e+03 2.9480e+00 -7.8000e+02 -1.3714e+03 2.7715e+00 -7.8000e+02 -1.3429e+03 2.5870e+00 -7.8000e+02 -1.3143e+03 2.3940e+00 -7.8000e+02 -1.2857e+03 2.1920e+00 -7.8000e+02 -1.2571e+03 1.9803e+00 -7.8000e+02 -1.2286e+03 1.7585e+00 -7.8000e+02 -1.2000e+03 1.5258e+00 -7.8000e+02 -1.1714e+03 1.2815e+00 -7.8000e+02 -1.1429e+03 1.0249e+00 -7.8000e+02 -1.1143e+03 7.5515e-01 -7.8000e+02 -1.0857e+03 4.7144e-01 -7.8000e+02 -1.0571e+03 1.7283e-01 -7.8000e+02 -1.0286e+03 -1.4166e-01 -7.8000e+02 -1.0000e+03 -4.7306e-01 -7.8000e+02 -9.7143e+02 -8.2250e-01 -7.8000e+02 -9.4286e+02 -1.1911e+00 -7.8000e+02 -9.1429e+02 -1.5802e+00 -7.8000e+02 -8.8571e+02 -1.9909e+00 -7.8000e+02 -8.5714e+02 -2.4247e+00 -7.8000e+02 -8.2857e+02 -2.8830e+00 -7.8000e+02 -8.0000e+02 -3.3670e+00 -7.8000e+02 -7.7143e+02 -3.8783e+00 -7.8000e+02 -7.4286e+02 -4.4183e+00 -7.8000e+02 -7.1429e+02 -4.9882e+00 -7.8000e+02 -6.8571e+02 -5.5894e+00 -7.8000e+02 -6.5714e+02 -6.2230e+00 -7.8000e+02 -6.2857e+02 -6.8897e+00 -7.8000e+02 -6.0000e+02 -7.5902e+00 -7.8000e+02 -5.7143e+02 -8.3247e+00 -7.8000e+02 -5.4286e+02 -9.0928e+00 -7.8000e+02 -5.1429e+02 -9.8934e+00 -7.8000e+02 -4.8571e+02 -1.0725e+01 -7.8000e+02 -4.5714e+02 -1.1585e+01 -7.8000e+02 -4.2857e+02 -1.2468e+01 -7.8000e+02 -4.0000e+02 -1.3371e+01 -7.8000e+02 -3.7143e+02 -1.4287e+01 -7.8000e+02 -3.4286e+02 -1.5207e+01 -7.8000e+02 -3.1429e+02 -1.6123e+01 -7.8000e+02 -2.8571e+02 -1.7024e+01 -7.8000e+02 -2.5714e+02 -1.7897e+01 -7.8000e+02 -2.2857e+02 -1.8730e+01 -7.8000e+02 -2.0000e+02 -1.9509e+01 -7.8000e+02 -1.7143e+02 -2.0220e+01 -7.8000e+02 -1.4286e+02 -2.0849e+01 -7.8000e+02 -1.1429e+02 -2.1384e+01 -7.8000e+02 -8.5714e+01 -2.1813e+01 -7.8000e+02 -5.7143e+01 -2.2126e+01 -7.8000e+02 -2.8571e+01 -2.2317e+01 -7.8000e+02 0.0000e+00 -2.2382e+01 -7.8000e+02 2.8571e+01 -2.2317e+01 -7.8000e+02 5.7143e+01 -2.2126e+01 -7.8000e+02 8.5714e+01 -2.1813e+01 -7.8000e+02 1.1429e+02 -2.1384e+01 -7.8000e+02 1.4286e+02 -2.0849e+01 -7.8000e+02 1.7143e+02 -2.0220e+01 -7.8000e+02 2.0000e+02 -1.9509e+01 -7.8000e+02 2.2857e+02 -1.8730e+01 -7.8000e+02 2.5714e+02 -1.7897e+01 -7.8000e+02 2.8571e+02 -1.7024e+01 -7.8000e+02 3.1429e+02 -1.6123e+01 -7.8000e+02 3.4286e+02 -1.5207e+01 -7.8000e+02 3.7143e+02 -1.4287e+01 -7.8000e+02 4.0000e+02 -1.3371e+01 -7.8000e+02 4.2857e+02 -1.2468e+01 -7.8000e+02 4.5714e+02 -1.1585e+01 -7.8000e+02 4.8571e+02 -1.0725e+01 -7.8000e+02 5.1429e+02 -9.8934e+00 -7.8000e+02 5.4286e+02 -9.0928e+00 -7.8000e+02 5.7143e+02 -8.3247e+00 -7.8000e+02 6.0000e+02 -7.5902e+00 -7.8000e+02 6.2857e+02 -6.8897e+00 -7.8000e+02 6.5714e+02 -6.2230e+00 -7.8000e+02 6.8571e+02 -5.5894e+00 -7.8000e+02 7.1429e+02 -4.9882e+00 -7.8000e+02 7.4286e+02 -4.4183e+00 -7.8000e+02 7.7143e+02 -3.8783e+00 -7.8000e+02 8.0000e+02 -3.3670e+00 -7.8000e+02 8.2857e+02 -2.8830e+00 -7.8000e+02 8.5714e+02 -2.4247e+00 -7.8000e+02 8.8571e+02 -1.9909e+00 -7.8000e+02 9.1429e+02 -1.5802e+00 -7.8000e+02 9.4286e+02 -1.1911e+00 -7.8000e+02 9.7143e+02 -8.2250e-01 -7.8000e+02 1.0000e+03 -4.7306e-01 -7.8000e+02 1.0286e+03 -1.4166e-01 -7.8000e+02 1.0571e+03 1.7283e-01 -7.8000e+02 1.0857e+03 4.7144e-01 -7.8000e+02 1.1143e+03 7.5515e-01 -7.8000e+02 1.1429e+03 1.0249e+00 -7.8000e+02 1.1714e+03 1.2815e+00 -7.8000e+02 1.2000e+03 1.5258e+00 -7.8000e+02 1.2286e+03 1.7585e+00 -7.8000e+02 1.2571e+03 1.9803e+00 -7.8000e+02 1.2857e+03 2.1920e+00 -7.8000e+02 1.3143e+03 2.3940e+00 -7.8000e+02 1.3429e+03 2.5870e+00 -7.8000e+02 1.3714e+03 2.7715e+00 -7.8000e+02 1.4000e+03 2.9480e+00 -7.8000e+02 1.4286e+03 3.1169e+00 -7.8000e+02 1.4571e+03 3.2787e+00 -7.8000e+02 1.4857e+03 3.4338e+00 -7.8000e+02 1.5143e+03 3.5826e+00 -7.8000e+02 1.5429e+03 3.7253e+00 -7.8000e+02 1.5714e+03 3.8624e+00 -7.8000e+02 1.6000e+03 3.9941e+00 -7.8000e+02 1.6286e+03 4.1208e+00 -7.8000e+02 1.6571e+03 4.2426e+00 -7.8000e+02 1.6857e+03 4.3599e+00 -7.8000e+02 1.7143e+03 4.4728e+00 -7.8000e+02 1.7429e+03 4.5817e+00 -7.8000e+02 1.7714e+03 4.6866e+00 -7.8000e+02 1.8000e+03 4.7878e+00 -7.8000e+02 1.8286e+03 4.8855e+00 -7.8000e+02 1.8571e+03 4.9799e+00 -7.8000e+02 1.8857e+03 5.0710e+00 -7.8000e+02 1.9143e+03 5.1591e+00 -7.8000e+02 1.9429e+03 5.2443e+00 -7.8000e+02 1.9714e+03 5.3267e+00 -7.8000e+02 2.0000e+03 5.4065e+00 -8.1000e+02 -2.0000e+03 5.4389e+00 -8.1000e+02 -1.9714e+03 5.3607e+00 -8.1000e+02 -1.9429e+03 5.2799e+00 -8.1000e+02 -1.9143e+03 5.1965e+00 -8.1000e+02 -1.8857e+03 5.1102e+00 -8.1000e+02 -1.8571e+03 5.0211e+00 -8.1000e+02 -1.8286e+03 4.9288e+00 -8.1000e+02 -1.8000e+03 4.8334e+00 -8.1000e+02 -1.7714e+03 4.7346e+00 -8.1000e+02 -1.7429e+03 4.6322e+00 -8.1000e+02 -1.7143e+03 4.5261e+00 -8.1000e+02 -1.6857e+03 4.4161e+00 -8.1000e+02 -1.6571e+03 4.3020e+00 -8.1000e+02 -1.6286e+03 4.1835e+00 -8.1000e+02 -1.6000e+03 4.0605e+00 -8.1000e+02 -1.5714e+03 3.9327e+00 -8.1000e+02 -1.5429e+03 3.7998e+00 -8.1000e+02 -1.5143e+03 3.6616e+00 -8.1000e+02 -1.4857e+03 3.5177e+00 -8.1000e+02 -1.4571e+03 3.3679e+00 -8.1000e+02 -1.4286e+03 3.2118e+00 -8.1000e+02 -1.4000e+03 3.0490e+00 -8.1000e+02 -1.3714e+03 2.8792e+00 -8.1000e+02 -1.3429e+03 2.7019e+00 -8.1000e+02 -1.3143e+03 2.5168e+00 -8.1000e+02 -1.2857e+03 2.3233e+00 -8.1000e+02 -1.2571e+03 2.1209e+00 -8.1000e+02 -1.2286e+03 1.9092e+00 -8.1000e+02 -1.2000e+03 1.6875e+00 -8.1000e+02 -1.1714e+03 1.4553e+00 -8.1000e+02 -1.1429e+03 1.2119e+00 -8.1000e+02 -1.1143e+03 9.5659e-01 -8.1000e+02 -1.0857e+03 6.8871e-01 -8.1000e+02 -1.0571e+03 4.0746e-01 -8.1000e+02 -1.0286e+03 1.1205e-01 -8.1000e+02 -1.0000e+03 -1.9839e-01 -8.1000e+02 -9.7143e+02 -5.2474e-01 -8.1000e+02 -9.4286e+02 -8.6794e-01 -8.1000e+02 -9.1429e+02 -1.2290e+00 -8.1000e+02 -8.8571e+02 -1.6088e+00 -8.1000e+02 -8.5714e+02 -2.0084e+00 -8.1000e+02 -8.2857e+02 -2.4290e+00 -8.1000e+02 -8.0000e+02 -2.8713e+00 -8.1000e+02 -7.7143e+02 -3.3366e+00 -8.1000e+02 -7.4286e+02 -3.8257e+00 -8.1000e+02 -7.1429e+02 -4.3395e+00 -8.1000e+02 -6.8571e+02 -4.8788e+00 -8.1000e+02 -6.5714e+02 -5.4441e+00 -8.1000e+02 -6.2857e+02 -6.0357e+00 -8.1000e+02 -6.0000e+02 -6.6539e+00 -8.1000e+02 -5.7143e+02 -7.2982e+00 -8.1000e+02 -5.4286e+02 -7.9679e+00 -8.1000e+02 -5.1429e+02 -8.6617e+00 -8.1000e+02 -4.8571e+02 -9.3777e+00 -8.1000e+02 -4.5714e+02 -1.0113e+01 -8.1000e+02 -4.2857e+02 -1.0865e+01 -8.1000e+02 -4.0000e+02 -1.1627e+01 -8.1000e+02 -3.7143e+02 -1.2396e+01 -8.1000e+02 -3.4286e+02 -1.3164e+01 -8.1000e+02 -3.1429e+02 -1.3924e+01 -8.1000e+02 -2.8571e+02 -1.4667e+01 -8.1000e+02 -2.5714e+02 -1.5384e+01 -8.1000e+02 -2.2857e+02 -1.6064e+01 -8.1000e+02 -2.0000e+02 -1.6698e+01 -8.1000e+02 -1.7143e+02 -1.7274e+01 -8.1000e+02 -1.4286e+02 -1.7782e+01 -8.1000e+02 -1.1429e+02 -1.8213e+01 -8.1000e+02 -8.5714e+01 -1.8558e+01 -8.1000e+02 -5.7143e+01 -1.8809e+01 -8.1000e+02 -2.8571e+01 -1.8963e+01 -8.1000e+02 0.0000e+00 -1.9014e+01 -8.1000e+02 2.8571e+01 -1.8963e+01 -8.1000e+02 5.7143e+01 -1.8809e+01 -8.1000e+02 8.5714e+01 -1.8558e+01 -8.1000e+02 1.1429e+02 -1.8213e+01 -8.1000e+02 1.4286e+02 -1.7782e+01 -8.1000e+02 1.7143e+02 -1.7274e+01 -8.1000e+02 2.0000e+02 -1.6698e+01 -8.1000e+02 2.2857e+02 -1.6064e+01 -8.1000e+02 2.5714e+02 -1.5384e+01 -8.1000e+02 2.8571e+02 -1.4667e+01 -8.1000e+02 3.1429e+02 -1.3924e+01 -8.1000e+02 3.4286e+02 -1.3164e+01 -8.1000e+02 3.7143e+02 -1.2396e+01 -8.1000e+02 4.0000e+02 -1.1627e+01 -8.1000e+02 4.2857e+02 -1.0865e+01 -8.1000e+02 4.5714e+02 -1.0113e+01 -8.1000e+02 4.8571e+02 -9.3777e+00 -8.1000e+02 5.1429e+02 -8.6617e+00 -8.1000e+02 5.4286e+02 -7.9679e+00 -8.1000e+02 5.7143e+02 -7.2982e+00 -8.1000e+02 6.0000e+02 -6.6539e+00 -8.1000e+02 6.2857e+02 -6.0357e+00 -8.1000e+02 6.5714e+02 -5.4441e+00 -8.1000e+02 6.8571e+02 -4.8788e+00 -8.1000e+02 7.1429e+02 -4.3395e+00 -8.1000e+02 7.4286e+02 -3.8257e+00 -8.1000e+02 7.7143e+02 -3.3366e+00 -8.1000e+02 8.0000e+02 -2.8713e+00 -8.1000e+02 8.2857e+02 -2.4290e+00 -8.1000e+02 8.5714e+02 -2.0084e+00 -8.1000e+02 8.8571e+02 -1.6088e+00 -8.1000e+02 9.1429e+02 -1.2290e+00 -8.1000e+02 9.4286e+02 -8.6794e-01 -8.1000e+02 9.7143e+02 -5.2474e-01 -8.1000e+02 1.0000e+03 -1.9839e-01 -8.1000e+02 1.0286e+03 1.1205e-01 -8.1000e+02 1.0571e+03 4.0746e-01 -8.1000e+02 1.0857e+03 6.8871e-01 -8.1000e+02 1.1143e+03 9.5659e-01 -8.1000e+02 1.1429e+03 1.2119e+00 -8.1000e+02 1.1714e+03 1.4553e+00 -8.1000e+02 1.2000e+03 1.6875e+00 -8.1000e+02 1.2286e+03 1.9092e+00 -8.1000e+02 1.2571e+03 2.1209e+00 -8.1000e+02 1.2857e+03 2.3233e+00 -8.1000e+02 1.3143e+03 2.5168e+00 -8.1000e+02 1.3429e+03 2.7019e+00 -8.1000e+02 1.3714e+03 2.8792e+00 -8.1000e+02 1.4000e+03 3.0490e+00 -8.1000e+02 1.4286e+03 3.2118e+00 -8.1000e+02 1.4571e+03 3.3679e+00 -8.1000e+02 1.4857e+03 3.5177e+00 -8.1000e+02 1.5143e+03 3.6616e+00 -8.1000e+02 1.5429e+03 3.7998e+00 -8.1000e+02 1.5714e+03 3.9327e+00 -8.1000e+02 1.6000e+03 4.0605e+00 -8.1000e+02 1.6286e+03 4.1835e+00 -8.1000e+02 1.6571e+03 4.3020e+00 -8.1000e+02 1.6857e+03 4.4161e+00 -8.1000e+02 1.7143e+03 4.5261e+00 -8.1000e+02 1.7429e+03 4.6322e+00 -8.1000e+02 1.7714e+03 4.7346e+00 -8.1000e+02 1.8000e+03 4.8334e+00 -8.1000e+02 1.8286e+03 4.9288e+00 -8.1000e+02 1.8571e+03 5.0211e+00 -8.1000e+02 1.8857e+03 5.1102e+00 -8.1000e+02 1.9143e+03 5.1965e+00 -8.1000e+02 1.9429e+03 5.2799e+00 -8.1000e+02 1.9714e+03 5.3607e+00 -8.1000e+02 2.0000e+03 5.4389e+00 -8.4000e+02 -2.0000e+03 5.4720e+00 -8.4000e+02 -1.9714e+03 5.3953e+00 -8.4000e+02 -1.9429e+03 5.3161e+00 -8.4000e+02 -1.9143e+03 5.2344e+00 -8.4000e+02 -1.8857e+03 5.1501e+00 -8.4000e+02 -1.8571e+03 5.0629e+00 -8.4000e+02 -1.8286e+03 4.9728e+00 -8.4000e+02 -1.8000e+03 4.8796e+00 -8.4000e+02 -1.7714e+03 4.7831e+00 -8.4000e+02 -1.7429e+03 4.6833e+00 -8.4000e+02 -1.7143e+03 4.5800e+00 -8.4000e+02 -1.6857e+03 4.4729e+00 -8.4000e+02 -1.6571e+03 4.3619e+00 -8.4000e+02 -1.6286e+03 4.2468e+00 -8.4000e+02 -1.6000e+03 4.1274e+00 -8.4000e+02 -1.5714e+03 4.0034e+00 -8.4000e+02 -1.5429e+03 3.8747e+00 -8.4000e+02 -1.5143e+03 3.7409e+00 -8.4000e+02 -1.4857e+03 3.6018e+00 -8.4000e+02 -1.4571e+03 3.4572e+00 -8.4000e+02 -1.4286e+03 3.3066e+00 -8.4000e+02 -1.4000e+03 3.1499e+00 -8.4000e+02 -1.3714e+03 2.9866e+00 -8.4000e+02 -1.3429e+03 2.8164e+00 -8.4000e+02 -1.3143e+03 2.6388e+00 -8.4000e+02 -1.2857e+03 2.4536e+00 -8.4000e+02 -1.2571e+03 2.2603e+00 -8.4000e+02 -1.2286e+03 2.0583e+00 -8.4000e+02 -1.2000e+03 1.8472e+00 -8.4000e+02 -1.1714e+03 1.6266e+00 -8.4000e+02 -1.1429e+03 1.3957e+00 -8.4000e+02 -1.1143e+03 1.1542e+00 -8.4000e+02 -1.0857e+03 9.0131e-01 -8.4000e+02 -1.0571e+03 6.3647e-01 -8.4000e+02 -1.0286e+03 3.5901e-01 -8.4000e+02 -1.0000e+03 6.8225e-02 -8.4000e+02 -9.7143e+02 -2.3659e-01 -8.4000e+02 -9.4286e+02 -5.5617e-01 -8.4000e+02 -9.1429e+02 -8.9127e-01 -8.4000e+02 -8.8571e+02 -1.2426e+00 -8.4000e+02 -8.5714e+02 -1.6111e+00 -8.4000e+02 -8.2857e+02 -1.9973e+00 -8.4000e+02 -8.0000e+02 -2.4020e+00 -8.4000e+02 -7.7143e+02 -2.8259e+00 -8.4000e+02 -7.4286e+02 -3.2696e+00 -8.4000e+02 -7.1429e+02 -3.7336e+00 -8.4000e+02 -6.8571e+02 -4.2183e+00 -8.4000e+02 -6.5714e+02 -4.7239e+00 -8.4000e+02 -6.2857e+02 -5.2504e+00 -8.4000e+02 -6.0000e+02 -5.7975e+00 -8.4000e+02 -5.7143e+02 -6.3647e+00 -8.4000e+02 -5.4286e+02 -6.9510e+00 -8.4000e+02 -5.1429e+02 -7.5549e+00 -8.4000e+02 -4.8571e+02 -8.1745e+00 -8.4000e+02 -4.5714e+02 -8.8072e+00 -8.4000e+02 -4.2857e+02 -9.4498e+00 -8.4000e+02 -4.0000e+02 -1.0098e+01 -8.4000e+02 -3.7143e+02 -1.0748e+01 -8.4000e+02 -3.4286e+02 -1.1394e+01 -8.4000e+02 -3.1429e+02 -1.2029e+01 -8.4000e+02 -2.8571e+02 -1.2647e+01 -8.4000e+02 -2.5714e+02 -1.3240e+01 -8.4000e+02 -2.2857e+02 -1.3800e+01 -8.4000e+02 -2.0000e+02 -1.4320e+01 -8.4000e+02 -1.7143e+02 -1.4791e+01 -8.4000e+02 -1.4286e+02 -1.5205e+01 -8.4000e+02 -1.1429e+02 -1.5555e+01 -8.4000e+02 -8.5714e+01 -1.5834e+01 -8.4000e+02 -5.7143e+01 -1.6038e+01 -8.4000e+02 -2.8571e+01 -1.6162e+01 -8.4000e+02 0.0000e+00 -1.6203e+01 -8.4000e+02 2.8571e+01 -1.6162e+01 -8.4000e+02 5.7143e+01 -1.6038e+01 -8.4000e+02 8.5714e+01 -1.5834e+01 -8.4000e+02 1.1429e+02 -1.5555e+01 -8.4000e+02 1.4286e+02 -1.5205e+01 -8.4000e+02 1.7143e+02 -1.4791e+01 -8.4000e+02 2.0000e+02 -1.4320e+01 -8.4000e+02 2.2857e+02 -1.3800e+01 -8.4000e+02 2.5714e+02 -1.3240e+01 -8.4000e+02 2.8571e+02 -1.2647e+01 -8.4000e+02 3.1429e+02 -1.2029e+01 -8.4000e+02 3.4286e+02 -1.1394e+01 -8.4000e+02 3.7143e+02 -1.0748e+01 -8.4000e+02 4.0000e+02 -1.0098e+01 -8.4000e+02 4.2857e+02 -9.4498e+00 -8.4000e+02 4.5714e+02 -8.8072e+00 -8.4000e+02 4.8571e+02 -8.1745e+00 -8.4000e+02 5.1429e+02 -7.5549e+00 -8.4000e+02 5.4286e+02 -6.9510e+00 -8.4000e+02 5.7143e+02 -6.3647e+00 -8.4000e+02 6.0000e+02 -5.7975e+00 -8.4000e+02 6.2857e+02 -5.2504e+00 -8.4000e+02 6.5714e+02 -4.7239e+00 -8.4000e+02 6.8571e+02 -4.2183e+00 -8.4000e+02 7.1429e+02 -3.7336e+00 -8.4000e+02 7.4286e+02 -3.2696e+00 -8.4000e+02 7.7143e+02 -2.8259e+00 -8.4000e+02 8.0000e+02 -2.4020e+00 -8.4000e+02 8.2857e+02 -1.9973e+00 -8.4000e+02 8.5714e+02 -1.6111e+00 -8.4000e+02 8.8571e+02 -1.2426e+00 -8.4000e+02 9.1429e+02 -8.9127e-01 -8.4000e+02 9.4286e+02 -5.5617e-01 -8.4000e+02 9.7143e+02 -2.3659e-01 -8.4000e+02 1.0000e+03 6.8225e-02 -8.4000e+02 1.0286e+03 3.5901e-01 -8.4000e+02 1.0571e+03 6.3647e-01 -8.4000e+02 1.0857e+03 9.0131e-01 -8.4000e+02 1.1143e+03 1.1542e+00 -8.4000e+02 1.1429e+03 1.3957e+00 -8.4000e+02 1.1714e+03 1.6266e+00 -8.4000e+02 1.2000e+03 1.8472e+00 -8.4000e+02 1.2286e+03 2.0583e+00 -8.4000e+02 1.2571e+03 2.2603e+00 -8.4000e+02 1.2857e+03 2.4536e+00 -8.4000e+02 1.3143e+03 2.6388e+00 -8.4000e+02 1.3429e+03 2.8164e+00 -8.4000e+02 1.3714e+03 2.9866e+00 -8.4000e+02 1.4000e+03 3.1499e+00 -8.4000e+02 1.4286e+03 3.3066e+00 -8.4000e+02 1.4571e+03 3.4572e+00 -8.4000e+02 1.4857e+03 3.6018e+00 -8.4000e+02 1.5143e+03 3.7409e+00 -8.4000e+02 1.5429e+03 3.8747e+00 -8.4000e+02 1.5714e+03 4.0034e+00 -8.4000e+02 1.6000e+03 4.1274e+00 -8.4000e+02 1.6286e+03 4.2468e+00 -8.4000e+02 1.6571e+03 4.3619e+00 -8.4000e+02 1.6857e+03 4.4729e+00 -8.4000e+02 1.7143e+03 4.5800e+00 -8.4000e+02 1.7429e+03 4.6833e+00 -8.4000e+02 1.7714e+03 4.7831e+00 -8.4000e+02 1.8000e+03 4.8796e+00 -8.4000e+02 1.8286e+03 4.9728e+00 -8.4000e+02 1.8571e+03 5.0629e+00 -8.4000e+02 1.8857e+03 5.1501e+00 -8.4000e+02 1.9143e+03 5.2344e+00 -8.4000e+02 1.9429e+03 5.3161e+00 -8.4000e+02 1.9714e+03 5.3953e+00 -8.4000e+02 2.0000e+03 5.4720e+00 -8.7000e+02 -2.0000e+03 5.5055e+00 -8.7000e+02 -1.9714e+03 5.4304e+00 -8.7000e+02 -1.9429e+03 5.3529e+00 -8.7000e+02 -1.9143e+03 5.2730e+00 -8.7000e+02 -1.8857e+03 5.1904e+00 -8.7000e+02 -1.8571e+03 5.1052e+00 -8.7000e+02 -1.8286e+03 5.0172e+00 -8.7000e+02 -1.8000e+03 4.9263e+00 -8.7000e+02 -1.7714e+03 4.8322e+00 -8.7000e+02 -1.7429e+03 4.7350e+00 -8.7000e+02 -1.7143e+03 4.6343e+00 -8.7000e+02 -1.6857e+03 4.5301e+00 -8.7000e+02 -1.6571e+03 4.4223e+00 -8.7000e+02 -1.6286e+03 4.3105e+00 -8.7000e+02 -1.6000e+03 4.1946e+00 -8.7000e+02 -1.5714e+03 4.0744e+00 -8.7000e+02 -1.5429e+03 3.9498e+00 -8.7000e+02 -1.5143e+03 3.8204e+00 -8.7000e+02 -1.4857e+03 3.6860e+00 -8.7000e+02 -1.4571e+03 3.5464e+00 -8.7000e+02 -1.4286e+03 3.4014e+00 -8.7000e+02 -1.4000e+03 3.2505e+00 -8.7000e+02 -1.3714e+03 3.0935e+00 -8.7000e+02 -1.3429e+03 2.9301e+00 -8.7000e+02 -1.3143e+03 2.7600e+00 -8.7000e+02 -1.2857e+03 2.5828e+00 -8.7000e+02 -1.2571e+03 2.3981e+00 -8.7000e+02 -1.2286e+03 2.2055e+00 -8.7000e+02 -1.2000e+03 2.0046e+00 -8.7000e+02 -1.1714e+03 1.7950e+00 -8.7000e+02 -1.1429e+03 1.5762e+00 -8.7000e+02 -1.1143e+03 1.3477e+00 -8.7000e+02 -1.0857e+03 1.1090e+00 -8.7000e+02 -1.0571e+03 8.5968e-01 -8.7000e+02 -1.0286e+03 5.9908e-01 -8.7000e+02 -1.0000e+03 3.2669e-01 -8.7000e+02 -9.7143e+02 4.1952e-02 -8.7000e+02 -9.4286e+02 -2.5571e-01 -8.7000e+02 -9.1429e+02 -5.6688e-01 -8.7000e+02 -8.8571e+02 -8.9211e-01 -8.7000e+02 -8.5714e+02 -1.2320e+00 -8.7000e+02 -8.2857e+02 -1.5870e+00 -8.7000e+02 -8.0000e+02 -1.9576e+00 -8.7000e+02 -7.7143e+02 -2.3443e+00 -8.7000e+02 -7.4286e+02 -2.7474e+00 -8.7000e+02 -7.1429e+02 -3.1672e+00 -8.7000e+02 -6.8571e+02 -3.6038e+00 -8.7000e+02 -6.5714e+02 -4.0571e+00 -8.7000e+02 -6.2857e+02 -4.5269e+00 -8.7000e+02 -6.0000e+02 -5.0127e+00 -8.7000e+02 -5.7143e+02 -5.5139e+00 -8.7000e+02 -5.4286e+02 -6.0291e+00 -8.7000e+02 -5.1429e+02 -6.5571e+00 -8.7000e+02 -4.8571e+02 -7.0959e+00 -8.7000e+02 -4.5714e+02 -7.6431e+00 -8.7000e+02 -4.2857e+02 -8.1959e+00 -8.7000e+02 -4.0000e+02 -8.7507e+00 -8.7000e+02 -3.7143e+02 -9.3037e+00 -8.7000e+02 -3.4286e+02 -9.8502e+00 -8.7000e+02 -3.1429e+02 -1.0385e+01 -8.7000e+02 -2.8571e+02 -1.0903e+01 -8.7000e+02 -2.5714e+02 -1.1398e+01 -8.7000e+02 -2.2857e+02 -1.1864e+01 -8.7000e+02 -2.0000e+02 -1.2294e+01 -8.7000e+02 -1.7143e+02 -1.2682e+01 -8.7000e+02 -1.4286e+02 -1.3022e+01 -8.7000e+02 -1.1429e+02 -1.3309e+01 -8.7000e+02 -8.5714e+01 -1.3538e+01 -8.7000e+02 -5.7143e+01 -1.3704e+01 -8.7000e+02 -2.8571e+01 -1.3806e+01 -8.7000e+02 0.0000e+00 -1.3839e+01 -8.7000e+02 2.8571e+01 -1.3806e+01 -8.7000e+02 5.7143e+01 -1.3704e+01 -8.7000e+02 8.5714e+01 -1.3538e+01 -8.7000e+02 1.1429e+02 -1.3309e+01 -8.7000e+02 1.4286e+02 -1.3022e+01 -8.7000e+02 1.7143e+02 -1.2682e+01 -8.7000e+02 2.0000e+02 -1.2294e+01 -8.7000e+02 2.2857e+02 -1.1864e+01 -8.7000e+02 2.5714e+02 -1.1398e+01 -8.7000e+02 2.8571e+02 -1.0903e+01 -8.7000e+02 3.1429e+02 -1.0385e+01 -8.7000e+02 3.4286e+02 -9.8502e+00 -8.7000e+02 3.7143e+02 -9.3037e+00 -8.7000e+02 4.0000e+02 -8.7507e+00 -8.7000e+02 4.2857e+02 -8.1959e+00 -8.7000e+02 4.5714e+02 -7.6431e+00 -8.7000e+02 4.8571e+02 -7.0959e+00 -8.7000e+02 5.1429e+02 -6.5571e+00 -8.7000e+02 5.4286e+02 -6.0291e+00 -8.7000e+02 5.7143e+02 -5.5139e+00 -8.7000e+02 6.0000e+02 -5.0127e+00 -8.7000e+02 6.2857e+02 -4.5269e+00 -8.7000e+02 6.5714e+02 -4.0571e+00 -8.7000e+02 6.8571e+02 -3.6038e+00 -8.7000e+02 7.1429e+02 -3.1672e+00 -8.7000e+02 7.4286e+02 -2.7474e+00 -8.7000e+02 7.7143e+02 -2.3443e+00 -8.7000e+02 8.0000e+02 -1.9576e+00 -8.7000e+02 8.2857e+02 -1.5870e+00 -8.7000e+02 8.5714e+02 -1.2320e+00 -8.7000e+02 8.8571e+02 -8.9211e-01 -8.7000e+02 9.1429e+02 -5.6688e-01 -8.7000e+02 9.4286e+02 -2.5571e-01 -8.7000e+02 9.7143e+02 4.1952e-02 -8.7000e+02 1.0000e+03 3.2669e-01 -8.7000e+02 1.0286e+03 5.9908e-01 -8.7000e+02 1.0571e+03 8.5968e-01 -8.7000e+02 1.0857e+03 1.1090e+00 -8.7000e+02 1.1143e+03 1.3477e+00 -8.7000e+02 1.1429e+03 1.5762e+00 -8.7000e+02 1.1714e+03 1.7950e+00 -8.7000e+02 1.2000e+03 2.0046e+00 -8.7000e+02 1.2286e+03 2.2055e+00 -8.7000e+02 1.2571e+03 2.3981e+00 -8.7000e+02 1.2857e+03 2.5828e+00 -8.7000e+02 1.3143e+03 2.7600e+00 -8.7000e+02 1.3429e+03 2.9301e+00 -8.7000e+02 1.3714e+03 3.0935e+00 -8.7000e+02 1.4000e+03 3.2505e+00 -8.7000e+02 1.4286e+03 3.4014e+00 -8.7000e+02 1.4571e+03 3.5464e+00 -8.7000e+02 1.4857e+03 3.6860e+00 -8.7000e+02 1.5143e+03 3.8204e+00 -8.7000e+02 1.5429e+03 3.9498e+00 -8.7000e+02 1.5714e+03 4.0744e+00 -8.7000e+02 1.6000e+03 4.1946e+00 -8.7000e+02 1.6286e+03 4.3105e+00 -8.7000e+02 1.6571e+03 4.4223e+00 -8.7000e+02 1.6857e+03 4.5301e+00 -8.7000e+02 1.7143e+03 4.6343e+00 -8.7000e+02 1.7429e+03 4.7350e+00 -8.7000e+02 1.7714e+03 4.8322e+00 -8.7000e+02 1.8000e+03 4.9263e+00 -8.7000e+02 1.8286e+03 5.0172e+00 -8.7000e+02 1.8571e+03 5.1052e+00 -8.7000e+02 1.8857e+03 5.1904e+00 -8.7000e+02 1.9143e+03 5.2730e+00 -8.7000e+02 1.9429e+03 5.3529e+00 -8.7000e+02 1.9714e+03 5.4304e+00 -8.7000e+02 2.0000e+03 5.5055e+00 -9.0000e+02 -2.0000e+03 5.5395e+00 -9.0000e+02 -1.9714e+03 5.4660e+00 -9.0000e+02 -1.9429e+03 5.3901e+00 -9.0000e+02 -1.9143e+03 5.3119e+00 -9.0000e+02 -1.8857e+03 5.2313e+00 -9.0000e+02 -1.8571e+03 5.1480e+00 -9.0000e+02 -1.8286e+03 5.0621e+00 -9.0000e+02 -1.8000e+03 4.9734e+00 -9.0000e+02 -1.7714e+03 4.8817e+00 -9.0000e+02 -1.7429e+03 4.7870e+00 -9.0000e+02 -1.7143e+03 4.6891e+00 -9.0000e+02 -1.6857e+03 4.5878e+00 -9.0000e+02 -1.6571e+03 4.4829e+00 -9.0000e+02 -1.6286e+03 4.3744e+00 -9.0000e+02 -1.6000e+03 4.2621e+00 -9.0000e+02 -1.5714e+03 4.1457e+00 -9.0000e+02 -1.5429e+03 4.0250e+00 -9.0000e+02 -1.5143e+03 3.8999e+00 -9.0000e+02 -1.4857e+03 3.7702e+00 -9.0000e+02 -1.4571e+03 3.6355e+00 -9.0000e+02 -1.4286e+03 3.4958e+00 -9.0000e+02 -1.4000e+03 3.3506e+00 -9.0000e+02 -1.3714e+03 3.1998e+00 -9.0000e+02 -1.3429e+03 3.0431e+00 -9.0000e+02 -1.3143e+03 2.8801e+00 -9.0000e+02 -1.2857e+03 2.7106e+00 -9.0000e+02 -1.2571e+03 2.5342e+00 -9.0000e+02 -1.2286e+03 2.3507e+00 -9.0000e+02 -1.2000e+03 2.1595e+00 -9.0000e+02 -1.1714e+03 1.9605e+00 -9.0000e+02 -1.1429e+03 1.7531e+00 -9.0000e+02 -1.1143e+03 1.5370e+00 -9.0000e+02 -1.0857e+03 1.3117e+00 -9.0000e+02 -1.0571e+03 1.0770e+00 -9.0000e+02 -1.0286e+03 8.3218e-01 -9.0000e+02 -1.0000e+03 5.7699e-01 -9.0000e+02 -9.7143e+02 3.1094e-01 -9.0000e+02 -9.4286e+02 3.3585e-02 -9.0000e+02 -9.1429e+02 -2.5550e-01 -9.0000e+02 -8.8571e+02 -5.5672e-01 -9.0000e+02 -8.5714e+02 -8.7046e-01 -9.0000e+02 -8.2857e+02 -1.1971e+00 -9.0000e+02 -8.0000e+02 -1.5369e+00 -9.0000e+02 -7.7143e+02 -1.8901e+00 -9.0000e+02 -7.4286e+02 -2.2570e+00 -9.0000e+02 -7.1429e+02 -2.6374e+00 -9.0000e+02 -6.8571e+02 -3.0315e+00 -9.0000e+02 -6.5714e+02 -3.4389e+00 -9.0000e+02 -6.2857e+02 -3.8593e+00 -9.0000e+02 -6.0000e+02 -4.2921e+00 -9.0000e+02 -5.7143e+02 -4.7363e+00 -9.0000e+02 -5.4286e+02 -5.1910e+00 -9.0000e+02 -5.1429e+02 -5.6546e+00 -9.0000e+02 -4.8571e+02 -6.1253e+00 -9.0000e+02 -4.5714e+02 -6.6010e+00 -9.0000e+02 -4.2857e+02 -7.0792e+00 -9.0000e+02 -4.0000e+02 -7.5568e+00 -9.0000e+02 -3.7143e+02 -8.0305e+00 -9.0000e+02 -3.4286e+02 -8.4963e+00 -9.0000e+02 -3.1429e+02 -8.9503e+00 -9.0000e+02 -2.8571e+02 -9.3878e+00 -9.0000e+02 -2.5714e+02 -9.8041e+00 -9.0000e+02 -2.2857e+02 -1.0194e+01 -9.0000e+02 -2.0000e+02 -1.0553e+01 -9.0000e+02 -1.7143e+02 -1.0876e+01 -9.0000e+02 -1.4286e+02 -1.1159e+01 -9.0000e+02 -1.1429e+02 -1.1397e+01 -9.0000e+02 -8.5714e+01 -1.1586e+01 -9.0000e+02 -5.7143e+01 -1.1723e+01 -9.0000e+02 -2.8571e+01 -1.1806e+01 -9.0000e+02 0.0000e+00 -1.1834e+01 -9.0000e+02 2.8571e+01 -1.1806e+01 -9.0000e+02 5.7143e+01 -1.1723e+01 -9.0000e+02 8.5714e+01 -1.1586e+01 -9.0000e+02 1.1429e+02 -1.1397e+01 -9.0000e+02 1.4286e+02 -1.1159e+01 -9.0000e+02 1.7143e+02 -1.0876e+01 -9.0000e+02 2.0000e+02 -1.0553e+01 -9.0000e+02 2.2857e+02 -1.0194e+01 -9.0000e+02 2.5714e+02 -9.8041e+00 -9.0000e+02 2.8571e+02 -9.3878e+00 -9.0000e+02 3.1429e+02 -8.9503e+00 -9.0000e+02 3.4286e+02 -8.4963e+00 -9.0000e+02 3.7143e+02 -8.0305e+00 -9.0000e+02 4.0000e+02 -7.5568e+00 -9.0000e+02 4.2857e+02 -7.0792e+00 -9.0000e+02 4.5714e+02 -6.6010e+00 -9.0000e+02 4.8571e+02 -6.1253e+00 -9.0000e+02 5.1429e+02 -5.6546e+00 -9.0000e+02 5.4286e+02 -5.1910e+00 -9.0000e+02 5.7143e+02 -4.7363e+00 -9.0000e+02 6.0000e+02 -4.2921e+00 -9.0000e+02 6.2857e+02 -3.8593e+00 -9.0000e+02 6.5714e+02 -3.4389e+00 -9.0000e+02 6.8571e+02 -3.0315e+00 -9.0000e+02 7.1429e+02 -2.6374e+00 -9.0000e+02 7.4286e+02 -2.2570e+00 -9.0000e+02 7.7143e+02 -1.8901e+00 -9.0000e+02 8.0000e+02 -1.5369e+00 -9.0000e+02 8.2857e+02 -1.1971e+00 -9.0000e+02 8.5714e+02 -8.7046e-01 -9.0000e+02 8.8571e+02 -5.5672e-01 -9.0000e+02 9.1429e+02 -2.5550e-01 -9.0000e+02 9.4286e+02 3.3585e-02 -9.0000e+02 9.7143e+02 3.1094e-01 -9.0000e+02 1.0000e+03 5.7699e-01 -9.0000e+02 1.0286e+03 8.3218e-01 -9.0000e+02 1.0571e+03 1.0770e+00 -9.0000e+02 1.0857e+03 1.3117e+00 -9.0000e+02 1.1143e+03 1.5370e+00 -9.0000e+02 1.1429e+03 1.7531e+00 -9.0000e+02 1.1714e+03 1.9605e+00 -9.0000e+02 1.2000e+03 2.1595e+00 -9.0000e+02 1.2286e+03 2.3507e+00 -9.0000e+02 1.2571e+03 2.5342e+00 -9.0000e+02 1.2857e+03 2.7106e+00 -9.0000e+02 1.3143e+03 2.8801e+00 -9.0000e+02 1.3429e+03 3.0431e+00 -9.0000e+02 1.3714e+03 3.1998e+00 -9.0000e+02 1.4000e+03 3.3506e+00 -9.0000e+02 1.4286e+03 3.4958e+00 -9.0000e+02 1.4571e+03 3.6355e+00 -9.0000e+02 1.4857e+03 3.7702e+00 -9.0000e+02 1.5143e+03 3.8999e+00 -9.0000e+02 1.5429e+03 4.0250e+00 -9.0000e+02 1.5714e+03 4.1457e+00 -9.0000e+02 1.6000e+03 4.2621e+00 -9.0000e+02 1.6286e+03 4.3744e+00 -9.0000e+02 1.6571e+03 4.4829e+00 -9.0000e+02 1.6857e+03 4.5878e+00 -9.0000e+02 1.7143e+03 4.6891e+00 -9.0000e+02 1.7429e+03 4.7870e+00 -9.0000e+02 1.7714e+03 4.8817e+00 -9.0000e+02 1.8000e+03 4.9734e+00 -9.0000e+02 1.8286e+03 5.0621e+00 -9.0000e+02 1.8571e+03 5.1480e+00 -9.0000e+02 1.8857e+03 5.2313e+00 -9.0000e+02 1.9143e+03 5.3119e+00 -9.0000e+02 1.9429e+03 5.3901e+00 -9.0000e+02 1.9714e+03 5.4660e+00 -9.0000e+02 2.0000e+03 5.5395e+00 -9.3000e+02 -2.0000e+03 5.5740e+00 -9.3000e+02 -1.9714e+03 5.5020e+00 -9.3000e+02 -1.9429e+03 5.4278e+00 -9.3000e+02 -1.9143e+03 5.3513e+00 -9.3000e+02 -1.8857e+03 5.2725e+00 -9.3000e+02 -1.8571e+03 5.1913e+00 -9.3000e+02 -1.8286e+03 5.1074e+00 -9.3000e+02 -1.8000e+03 5.0209e+00 -9.3000e+02 -1.7714e+03 4.9316e+00 -9.3000e+02 -1.7429e+03 4.8394e+00 -9.3000e+02 -1.7143e+03 4.7441e+00 -9.3000e+02 -1.6857e+03 4.6456e+00 -9.3000e+02 -1.6571e+03 4.5438e+00 -9.3000e+02 -1.6286e+03 4.4386e+00 -9.3000e+02 -1.6000e+03 4.3296e+00 -9.3000e+02 -1.5714e+03 4.2169e+00 -9.3000e+02 -1.5429e+03 4.1002e+00 -9.3000e+02 -1.5143e+03 3.9793e+00 -9.3000e+02 -1.4857e+03 3.8541e+00 -9.3000e+02 -1.4571e+03 3.7243e+00 -9.3000e+02 -1.4286e+03 3.5897e+00 -9.3000e+02 -1.4000e+03 3.4501e+00 -9.3000e+02 -1.3714e+03 3.3053e+00 -9.3000e+02 -1.3429e+03 3.1550e+00 -9.3000e+02 -1.3143e+03 2.9989e+00 -9.3000e+02 -1.2857e+03 2.8369e+00 -9.3000e+02 -1.2571e+03 2.6685e+00 -9.3000e+02 -1.2286e+03 2.4936e+00 -9.3000e+02 -1.2000e+03 2.3118e+00 -9.3000e+02 -1.1714e+03 2.1227e+00 -9.3000e+02 -1.1429e+03 1.9262e+00 -9.3000e+02 -1.1143e+03 1.7218e+00 -9.3000e+02 -1.0857e+03 1.5093e+00 -9.3000e+02 -1.0571e+03 1.2882e+00 -9.3000e+02 -1.0286e+03 1.0583e+00 -9.3000e+02 -1.0000e+03 8.1915e-01 -9.3000e+02 -9.7143e+02 5.7048e-01 -9.3000e+02 -9.4286e+02 3.1195e-01 -9.3000e+02 -9.1429e+02 4.3236e-02 -9.3000e+02 -8.8571e+02 -2.3594e-01 -9.3000e+02 -8.5714e+02 -5.2583e-01 -9.3000e+02 -8.2857e+02 -8.2665e-01 -9.3000e+02 -8.0000e+02 -1.1386e+00 -9.3000e+02 -7.7143e+02 -1.4617e+00 -9.3000e+02 -7.4286e+02 -1.7961e+00 -9.3000e+02 -7.1429e+02 -2.1415e+00 -9.3000e+02 -6.8571e+02 -2.4980e+00 -9.3000e+02 -6.5714e+02 -2.8650e+00 -9.3000e+02 -6.2857e+02 -3.2422e+00 -9.3000e+02 -6.0000e+02 -3.6288e+00 -9.3000e+02 -5.7143e+02 -4.0240e+00 -9.3000e+02 -5.4286e+02 -4.4267e+00 -9.3000e+02 -5.1429e+02 -4.8354e+00 -9.3000e+02 -4.8571e+02 -5.2486e+00 -9.3000e+02 -4.5714e+02 -5.6643e+00 -9.3000e+02 -4.2857e+02 -6.0802e+00 -9.3000e+02 -4.0000e+02 -6.4937e+00 -9.3000e+02 -3.7143e+02 -6.9019e+00 -9.3000e+02 -3.4286e+02 -7.3018e+00 -9.3000e+02 -3.1429e+02 -7.6897e+00 -9.3000e+02 -2.8571e+02 -8.0621e+00 -9.3000e+02 -2.5714e+02 -8.4151e+00 -9.3000e+02 -2.2857e+02 -8.7448e+00 -9.3000e+02 -2.0000e+02 -9.0472e+00 -9.3000e+02 -1.7143e+02 -9.3185e+00 -9.3000e+02 -1.4286e+02 -9.5551e+00 -9.3000e+02 -1.1429e+02 -9.7537e+00 -9.3000e+02 -8.5714e+01 -9.9113e+00 -9.3000e+02 -5.7143e+01 -1.0026e+01 -9.3000e+02 -2.8571e+01 -1.0095e+01 -9.3000e+02 0.0000e+00 -1.0118e+01 -9.3000e+02 2.8571e+01 -1.0095e+01 -9.3000e+02 5.7143e+01 -1.0026e+01 -9.3000e+02 8.5714e+01 -9.9113e+00 -9.3000e+02 1.1429e+02 -9.7537e+00 -9.3000e+02 1.4286e+02 -9.5551e+00 -9.3000e+02 1.7143e+02 -9.3185e+00 -9.3000e+02 2.0000e+02 -9.0472e+00 -9.3000e+02 2.2857e+02 -8.7448e+00 -9.3000e+02 2.5714e+02 -8.4151e+00 -9.3000e+02 2.8571e+02 -8.0621e+00 -9.3000e+02 3.1429e+02 -7.6897e+00 -9.3000e+02 3.4286e+02 -7.3018e+00 -9.3000e+02 3.7143e+02 -6.9019e+00 -9.3000e+02 4.0000e+02 -6.4937e+00 -9.3000e+02 4.2857e+02 -6.0802e+00 -9.3000e+02 4.5714e+02 -5.6643e+00 -9.3000e+02 4.8571e+02 -5.2486e+00 -9.3000e+02 5.1429e+02 -4.8354e+00 -9.3000e+02 5.4286e+02 -4.4267e+00 -9.3000e+02 5.7143e+02 -4.0240e+00 -9.3000e+02 6.0000e+02 -3.6288e+00 -9.3000e+02 6.2857e+02 -3.2422e+00 -9.3000e+02 6.5714e+02 -2.8650e+00 -9.3000e+02 6.8571e+02 -2.4980e+00 -9.3000e+02 7.1429e+02 -2.1415e+00 -9.3000e+02 7.4286e+02 -1.7961e+00 -9.3000e+02 7.7143e+02 -1.4617e+00 -9.3000e+02 8.0000e+02 -1.1386e+00 -9.3000e+02 8.2857e+02 -8.2665e-01 -9.3000e+02 8.5714e+02 -5.2583e-01 -9.3000e+02 8.8571e+02 -2.3594e-01 -9.3000e+02 9.1429e+02 4.3236e-02 -9.3000e+02 9.4286e+02 3.1195e-01 -9.3000e+02 9.7143e+02 5.7048e-01 -9.3000e+02 1.0000e+03 8.1915e-01 -9.3000e+02 1.0286e+03 1.0583e+00 -9.3000e+02 1.0571e+03 1.2882e+00 -9.3000e+02 1.0857e+03 1.5093e+00 -9.3000e+02 1.1143e+03 1.7218e+00 -9.3000e+02 1.1429e+03 1.9262e+00 -9.3000e+02 1.1714e+03 2.1227e+00 -9.3000e+02 1.2000e+03 2.3118e+00 -9.3000e+02 1.2286e+03 2.4936e+00 -9.3000e+02 1.2571e+03 2.6685e+00 -9.3000e+02 1.2857e+03 2.8369e+00 -9.3000e+02 1.3143e+03 2.9989e+00 -9.3000e+02 1.3429e+03 3.1550e+00 -9.3000e+02 1.3714e+03 3.3053e+00 -9.3000e+02 1.4000e+03 3.4501e+00 -9.3000e+02 1.4286e+03 3.5897e+00 -9.3000e+02 1.4571e+03 3.7243e+00 -9.3000e+02 1.4857e+03 3.8541e+00 -9.3000e+02 1.5143e+03 3.9793e+00 -9.3000e+02 1.5429e+03 4.1002e+00 -9.3000e+02 1.5714e+03 4.2169e+00 -9.3000e+02 1.6000e+03 4.3296e+00 -9.3000e+02 1.6286e+03 4.4386e+00 -9.3000e+02 1.6571e+03 4.5438e+00 -9.3000e+02 1.6857e+03 4.6456e+00 -9.3000e+02 1.7143e+03 4.7441e+00 -9.3000e+02 1.7429e+03 4.8394e+00 -9.3000e+02 1.7714e+03 4.9316e+00 -9.3000e+02 1.8000e+03 5.0209e+00 -9.3000e+02 1.8286e+03 5.1074e+00 -9.3000e+02 1.8571e+03 5.1913e+00 -9.3000e+02 1.8857e+03 5.2725e+00 -9.3000e+02 1.9143e+03 5.3513e+00 -9.3000e+02 1.9429e+03 5.4278e+00 -9.3000e+02 1.9714e+03 5.5020e+00 -9.3000e+02 2.0000e+03 5.5740e+00 -9.6000e+02 -2.0000e+03 5.6088e+00 -9.6000e+02 -1.9714e+03 5.5383e+00 -9.6000e+02 -1.9429e+03 5.4658e+00 -9.6000e+02 -1.9143e+03 5.3911e+00 -9.6000e+02 -1.8857e+03 5.3141e+00 -9.6000e+02 -1.8571e+03 5.2348e+00 -9.6000e+02 -1.8286e+03 5.1531e+00 -9.6000e+02 -1.8000e+03 5.0687e+00 -9.6000e+02 -1.7714e+03 4.9818e+00 -9.6000e+02 -1.7429e+03 4.8920e+00 -9.6000e+02 -1.7143e+03 4.7994e+00 -9.6000e+02 -1.6857e+03 4.7037e+00 -9.6000e+02 -1.6571e+03 4.6049e+00 -9.6000e+02 -1.6286e+03 4.5028e+00 -9.6000e+02 -1.6000e+03 4.3973e+00 -9.6000e+02 -1.5714e+03 4.2882e+00 -9.6000e+02 -1.5429e+03 4.1753e+00 -9.6000e+02 -1.5143e+03 4.0585e+00 -9.6000e+02 -1.4857e+03 3.9377e+00 -9.6000e+02 -1.4571e+03 3.8126e+00 -9.6000e+02 -1.4286e+03 3.6831e+00 -9.6000e+02 -1.4000e+03 3.5489e+00 -9.6000e+02 -1.3714e+03 3.4099e+00 -9.6000e+02 -1.3429e+03 3.2658e+00 -9.6000e+02 -1.3143e+03 3.1164e+00 -9.6000e+02 -1.2857e+03 2.9615e+00 -9.6000e+02 -1.2571e+03 2.8008e+00 -9.6000e+02 -1.2286e+03 2.6341e+00 -9.6000e+02 -1.2000e+03 2.4612e+00 -9.6000e+02 -1.1714e+03 2.2817e+00 -9.6000e+02 -1.1429e+03 2.0955e+00 -9.6000e+02 -1.1143e+03 1.9022e+00 -9.6000e+02 -1.0857e+03 1.7016e+00 -9.6000e+02 -1.0571e+03 1.4934e+00 -9.6000e+02 -1.0286e+03 1.2774e+00 -9.6000e+02 -1.0000e+03 1.0533e+00 -9.6000e+02 -9.7143e+02 8.2075e-01 -9.6000e+02 -9.4286e+02 5.7963e-01 -9.6000e+02 -9.1429e+02 3.2971e-01 -9.6000e+02 -8.8571e+02 7.0775e-02 -9.6000e+02 -8.5714e+02 -1.9731e-01 -9.6000e+02 -8.2857e+02 -4.7467e-01 -9.6000e+02 -8.0000e+02 -7.6135e-01 -9.6000e+02 -7.7143e+02 -1.0574e+00 -9.6000e+02 -7.4286e+02 -1.3626e+00 -9.6000e+02 -7.1429e+02 -1.6769e+00 -9.6000e+02 -6.8571e+02 -2.0000e+00 -9.6000e+02 -6.5714e+02 -2.3315e+00 -9.6000e+02 -6.2857e+02 -2.6708e+00 -9.6000e+02 -6.0000e+02 -3.0172e+00 -9.6000e+02 -5.7143e+02 -3.3700e+00 -9.6000e+02 -5.4286e+02 -3.7279e+00 -9.6000e+02 -5.1429e+02 -4.0897e+00 -9.6000e+02 -4.8571e+02 -4.4539e+00 -9.6000e+02 -4.5714e+02 -4.8188e+00 -9.6000e+02 -4.2857e+02 -5.1824e+00 -9.6000e+02 -4.0000e+02 -5.5424e+00 -9.6000e+02 -3.7143e+02 -5.8964e+00 -9.6000e+02 -3.4286e+02 -6.2418e+00 -9.6000e+02 -3.1429e+02 -6.5756e+00 -9.6000e+02 -2.8571e+02 -6.8948e+00 -9.6000e+02 -2.5714e+02 -7.1964e+00 -9.6000e+02 -2.2857e+02 -7.4771e+00 -9.6000e+02 -2.0000e+02 -7.7339e+00 -9.6000e+02 -1.7143e+02 -7.9636e+00 -9.6000e+02 -1.4286e+02 -8.1635e+00 -9.6000e+02 -1.1429e+02 -8.3309e+00 -9.6000e+02 -8.5714e+01 -8.4636e+00 -9.6000e+02 -5.7143e+01 -8.5597e+00 -9.6000e+02 -2.8571e+01 -8.6180e+00 -9.6000e+02 0.0000e+00 -8.6375e+00 -9.6000e+02 2.8571e+01 -8.6180e+00 -9.6000e+02 5.7143e+01 -8.5597e+00 -9.6000e+02 8.5714e+01 -8.4636e+00 -9.6000e+02 1.1429e+02 -8.3309e+00 -9.6000e+02 1.4286e+02 -8.1635e+00 -9.6000e+02 1.7143e+02 -7.9636e+00 -9.6000e+02 2.0000e+02 -7.7339e+00 -9.6000e+02 2.2857e+02 -7.4771e+00 -9.6000e+02 2.5714e+02 -7.1964e+00 -9.6000e+02 2.8571e+02 -6.8948e+00 -9.6000e+02 3.1429e+02 -6.5756e+00 -9.6000e+02 3.4286e+02 -6.2418e+00 -9.6000e+02 3.7143e+02 -5.8964e+00 -9.6000e+02 4.0000e+02 -5.5424e+00 -9.6000e+02 4.2857e+02 -5.1824e+00 -9.6000e+02 4.5714e+02 -4.8188e+00 -9.6000e+02 4.8571e+02 -4.4539e+00 -9.6000e+02 5.1429e+02 -4.0897e+00 -9.6000e+02 5.4286e+02 -3.7279e+00 -9.6000e+02 5.7143e+02 -3.3700e+00 -9.6000e+02 6.0000e+02 -3.0172e+00 -9.6000e+02 6.2857e+02 -2.6708e+00 -9.6000e+02 6.5714e+02 -2.3315e+00 -9.6000e+02 6.8571e+02 -2.0000e+00 -9.6000e+02 7.1429e+02 -1.6769e+00 -9.6000e+02 7.4286e+02 -1.3626e+00 -9.6000e+02 7.7143e+02 -1.0574e+00 -9.6000e+02 8.0000e+02 -7.6135e-01 -9.6000e+02 8.2857e+02 -4.7467e-01 -9.6000e+02 8.5714e+02 -1.9731e-01 -9.6000e+02 8.8571e+02 7.0775e-02 -9.6000e+02 9.1429e+02 3.2971e-01 -9.6000e+02 9.4286e+02 5.7963e-01 -9.6000e+02 9.7143e+02 8.2075e-01 -9.6000e+02 1.0000e+03 1.0533e+00 -9.6000e+02 1.0286e+03 1.2774e+00 -9.6000e+02 1.0571e+03 1.4934e+00 -9.6000e+02 1.0857e+03 1.7016e+00 -9.6000e+02 1.1143e+03 1.9022e+00 -9.6000e+02 1.1429e+03 2.0955e+00 -9.6000e+02 1.1714e+03 2.2817e+00 -9.6000e+02 1.2000e+03 2.4612e+00 -9.6000e+02 1.2286e+03 2.6341e+00 -9.6000e+02 1.2571e+03 2.8008e+00 -9.6000e+02 1.2857e+03 2.9615e+00 -9.6000e+02 1.3143e+03 3.1164e+00 -9.6000e+02 1.3429e+03 3.2658e+00 -9.6000e+02 1.3714e+03 3.4099e+00 -9.6000e+02 1.4000e+03 3.5489e+00 -9.6000e+02 1.4286e+03 3.6831e+00 -9.6000e+02 1.4571e+03 3.8126e+00 -9.6000e+02 1.4857e+03 3.9377e+00 -9.6000e+02 1.5143e+03 4.0585e+00 -9.6000e+02 1.5429e+03 4.1753e+00 -9.6000e+02 1.5714e+03 4.2882e+00 -9.6000e+02 1.6000e+03 4.3973e+00 -9.6000e+02 1.6286e+03 4.5028e+00 -9.6000e+02 1.6571e+03 4.6049e+00 -9.6000e+02 1.6857e+03 4.7037e+00 -9.6000e+02 1.7143e+03 4.7994e+00 -9.6000e+02 1.7429e+03 4.8920e+00 -9.6000e+02 1.7714e+03 4.9818e+00 -9.6000e+02 1.8000e+03 5.0687e+00 -9.6000e+02 1.8286e+03 5.1531e+00 -9.6000e+02 1.8571e+03 5.2348e+00 -9.6000e+02 1.8857e+03 5.3141e+00 -9.6000e+02 1.9143e+03 5.3911e+00 -9.6000e+02 1.9429e+03 5.4658e+00 -9.6000e+02 1.9714e+03 5.5383e+00 -9.6000e+02 2.0000e+03 5.6088e+00 -9.9000e+02 -2.0000e+03 5.6439e+00 -9.9000e+02 -1.9714e+03 5.5750e+00 -9.9000e+02 -1.9429e+03 5.5041e+00 -9.9000e+02 -1.9143e+03 5.4312e+00 -9.9000e+02 -1.8857e+03 5.3560e+00 -9.9000e+02 -1.8571e+03 5.2787e+00 -9.9000e+02 -1.8286e+03 5.1989e+00 -9.9000e+02 -1.8000e+03 5.1168e+00 -9.9000e+02 -1.7714e+03 5.0321e+00 -9.9000e+02 -1.7429e+03 4.9448e+00 -9.9000e+02 -1.7143e+03 4.8548e+00 -9.9000e+02 -1.6857e+03 4.7619e+00 -9.9000e+02 -1.6571e+03 4.6660e+00 -9.9000e+02 -1.6286e+03 4.5670e+00 -9.9000e+02 -1.6000e+03 4.4648e+00 -9.9000e+02 -1.5714e+03 4.3592e+00 -9.9000e+02 -1.5429e+03 4.2502e+00 -9.9000e+02 -1.5143e+03 4.1374e+00 -9.9000e+02 -1.4857e+03 4.0209e+00 -9.9000e+02 -1.4571e+03 3.9004e+00 -9.9000e+02 -1.4286e+03 3.7757e+00 -9.9000e+02 -1.4000e+03 3.6468e+00 -9.9000e+02 -1.3714e+03 3.5133e+00 -9.9000e+02 -1.3429e+03 3.3752e+00 -9.9000e+02 -1.3143e+03 3.2322e+00 -9.9000e+02 -1.2857e+03 3.0842e+00 -9.9000e+02 -1.2571e+03 2.9309e+00 -9.9000e+02 -1.2286e+03 2.7721e+00 -9.9000e+02 -1.2000e+03 2.6077e+00 -9.9000e+02 -1.1714e+03 2.4373e+00 -9.9000e+02 -1.1429e+03 2.2608e+00 -9.9000e+02 -1.1143e+03 2.0780e+00 -9.9000e+02 -1.0857e+03 1.8887e+00 -9.9000e+02 -1.0571e+03 1.6926e+00 -9.9000e+02 -1.0286e+03 1.4896e+00 -9.9000e+02 -1.0000e+03 1.2794e+00 -9.9000e+02 -9.7143e+02 1.0619e+00 -9.9000e+02 -9.4286e+02 8.3695e-01 -9.9000e+02 -9.1429e+02 6.0433e-01 -9.9000e+02 -8.8571e+02 3.6398e-01 -9.9000e+02 -8.5714e+02 1.1583e-01 -9.9000e+02 -8.2857e+02 -1.4017e-01 -9.9000e+02 -8.0000e+02 -4.0398e-01 -9.9000e+02 -7.7143e+02 -6.7552e-01 -9.9000e+02 -7.4286e+02 -9.5465e-01 -9.9000e+02 -7.1429e+02 -1.2411e+00 -9.9000e+02 -6.8571e+02 -1.5346e+00 -9.9000e+02 -6.5714e+02 -1.8346e+00 -9.9000e+02 -6.2857e+02 -2.1407e+00 -9.9000e+02 -6.0000e+02 -2.4521e+00 -9.9000e+02 -5.7143e+02 -2.7679e+00 -9.9000e+02 -5.4286e+02 -3.0871e+00 -9.9000e+02 -5.1429e+02 -3.4086e+00 -9.9000e+02 -4.8571e+02 -3.7311e+00 -9.9000e+02 -4.5714e+02 -4.0529e+00 -9.9000e+02 -4.2857e+02 -4.3723e+00 -9.9000e+02 -4.0000e+02 -4.6874e+00 -9.9000e+02 -3.7143e+02 -4.9961e+00 -9.9000e+02 -3.4286e+02 -5.2961e+00 -9.9000e+02 -3.1429e+02 -5.5852e+00 -9.9000e+02 -2.8571e+02 -5.8607e+00 -9.9000e+02 -2.5714e+02 -6.1201e+00 -9.9000e+02 -2.2857e+02 -6.3609e+00 -9.9000e+02 -2.0000e+02 -6.5806e+00 -9.9000e+02 -1.7143e+02 -6.7767e+00 -9.9000e+02 -1.4286e+02 -6.9469e+00 -9.9000e+02 -1.1429e+02 -7.0892e+00 -9.9000e+02 -8.5714e+01 -7.2019e+00 -9.9000e+02 -5.7143e+01 -7.2834e+00 -9.9000e+02 -2.8571e+01 -7.3327e+00 -9.9000e+02 0.0000e+00 -7.3493e+00 -9.9000e+02 2.8571e+01 -7.3327e+00 -9.9000e+02 5.7143e+01 -7.2834e+00 -9.9000e+02 8.5714e+01 -7.2019e+00 -9.9000e+02 1.1429e+02 -7.0892e+00 -9.9000e+02 1.4286e+02 -6.9469e+00 -9.9000e+02 1.7143e+02 -6.7767e+00 -9.9000e+02 2.0000e+02 -6.5806e+00 -9.9000e+02 2.2857e+02 -6.3609e+00 -9.9000e+02 2.5714e+02 -6.1201e+00 -9.9000e+02 2.8571e+02 -5.8607e+00 -9.9000e+02 3.1429e+02 -5.5852e+00 -9.9000e+02 3.4286e+02 -5.2961e+00 -9.9000e+02 3.7143e+02 -4.9961e+00 -9.9000e+02 4.0000e+02 -4.6874e+00 -9.9000e+02 4.2857e+02 -4.3723e+00 -9.9000e+02 4.5714e+02 -4.0529e+00 -9.9000e+02 4.8571e+02 -3.7311e+00 -9.9000e+02 5.1429e+02 -3.4086e+00 -9.9000e+02 5.4286e+02 -3.0871e+00 -9.9000e+02 5.7143e+02 -2.7679e+00 -9.9000e+02 6.0000e+02 -2.4521e+00 -9.9000e+02 6.2857e+02 -2.1407e+00 -9.9000e+02 6.5714e+02 -1.8346e+00 -9.9000e+02 6.8571e+02 -1.5346e+00 -9.9000e+02 7.1429e+02 -1.2411e+00 -9.9000e+02 7.4286e+02 -9.5465e-01 -9.9000e+02 7.7143e+02 -6.7552e-01 -9.9000e+02 8.0000e+02 -4.0398e-01 -9.9000e+02 8.2857e+02 -1.4017e-01 -9.9000e+02 8.5714e+02 1.1583e-01 -9.9000e+02 8.8571e+02 3.6398e-01 -9.9000e+02 9.1429e+02 6.0433e-01 -9.9000e+02 9.4286e+02 8.3695e-01 -9.9000e+02 9.7143e+02 1.0619e+00 -9.9000e+02 1.0000e+03 1.2794e+00 -9.9000e+02 1.0286e+03 1.4896e+00 -9.9000e+02 1.0571e+03 1.6926e+00 -9.9000e+02 1.0857e+03 1.8887e+00 -9.9000e+02 1.1143e+03 2.0780e+00 -9.9000e+02 1.1429e+03 2.2608e+00 -9.9000e+02 1.1714e+03 2.4373e+00 -9.9000e+02 1.2000e+03 2.6077e+00 -9.9000e+02 1.2286e+03 2.7721e+00 -9.9000e+02 1.2571e+03 2.9309e+00 -9.9000e+02 1.2857e+03 3.0842e+00 -9.9000e+02 1.3143e+03 3.2322e+00 -9.9000e+02 1.3429e+03 3.3752e+00 -9.9000e+02 1.3714e+03 3.5133e+00 -9.9000e+02 1.4000e+03 3.6468e+00 -9.9000e+02 1.4286e+03 3.7757e+00 -9.9000e+02 1.4571e+03 3.9004e+00 -9.9000e+02 1.4857e+03 4.0209e+00 -9.9000e+02 1.5143e+03 4.1374e+00 -9.9000e+02 1.5429e+03 4.2502e+00 -9.9000e+02 1.5714e+03 4.3592e+00 -9.9000e+02 1.6000e+03 4.4648e+00 -9.9000e+02 1.6286e+03 4.5670e+00 -9.9000e+02 1.6571e+03 4.6660e+00 -9.9000e+02 1.6857e+03 4.7619e+00 -9.9000e+02 1.7143e+03 4.8548e+00 -9.9000e+02 1.7429e+03 4.9448e+00 -9.9000e+02 1.7714e+03 5.0321e+00 -9.9000e+02 1.8000e+03 5.1168e+00 -9.9000e+02 1.8286e+03 5.1989e+00 -9.9000e+02 1.8571e+03 5.2787e+00 -9.9000e+02 1.8857e+03 5.3560e+00 -9.9000e+02 1.9143e+03 5.4312e+00 -9.9000e+02 1.9429e+03 5.5041e+00 -9.9000e+02 1.9714e+03 5.5750e+00 -9.9000e+02 2.0000e+03 5.6439e+00 -1.0200e+03 -2.0000e+03 5.6794e+00 -1.0200e+03 -1.9714e+03 5.6120e+00 -1.0200e+03 -1.9429e+03 5.5427e+00 -1.0200e+03 -1.9143e+03 5.4715e+00 -1.0200e+03 -1.8857e+03 5.3982e+00 -1.0200e+03 -1.8571e+03 5.3227e+00 -1.0200e+03 -1.8286e+03 5.2450e+00 -1.0200e+03 -1.8000e+03 5.1650e+00 -1.0200e+03 -1.7714e+03 5.0826e+00 -1.0200e+03 -1.7429e+03 4.9977e+00 -1.0200e+03 -1.7143e+03 4.9103e+00 -1.0200e+03 -1.6857e+03 4.8201e+00 -1.0200e+03 -1.6571e+03 4.7271e+00 -1.0200e+03 -1.6286e+03 4.6311e+00 -1.0200e+03 -1.6000e+03 4.5322e+00 -1.0200e+03 -1.5714e+03 4.4301e+00 -1.0200e+03 -1.5429e+03 4.3247e+00 -1.0200e+03 -1.5143e+03 4.2159e+00 -1.0200e+03 -1.4857e+03 4.1035e+00 -1.0200e+03 -1.4571e+03 3.9874e+00 -1.0200e+03 -1.4286e+03 3.8675e+00 -1.0200e+03 -1.4000e+03 3.7436e+00 -1.0200e+03 -1.3714e+03 3.6156e+00 -1.0200e+03 -1.3429e+03 3.4833e+00 -1.0200e+03 -1.3143e+03 3.3464e+00 -1.0200e+03 -1.2857e+03 3.2050e+00 -1.0200e+03 -1.2571e+03 3.0587e+00 -1.0200e+03 -1.2286e+03 2.9075e+00 -1.0200e+03 -1.2000e+03 2.7511e+00 -1.0200e+03 -1.1714e+03 2.5894e+00 -1.0200e+03 -1.1429e+03 2.4222e+00 -1.0200e+03 -1.1143e+03 2.2493e+00 -1.0200e+03 -1.0857e+03 2.0706e+00 -1.0200e+03 -1.0571e+03 1.8858e+00 -1.0200e+03 -1.0286e+03 1.6950e+00 -1.0200e+03 -1.0000e+03 1.4978e+00 -1.0200e+03 -9.7143e+02 1.2943e+00 -1.0200e+03 -9.4286e+02 1.0842e+00 -1.0200e+03 -9.1429e+02 8.6755e-01 -1.0200e+03 -8.8571e+02 6.4427e-01 -1.0200e+03 -8.5714e+02 4.1434e-01 -1.0200e+03 -8.2857e+02 1.7780e-01 -1.0200e+03 -8.0000e+02 -6.5281e-02 -1.0200e+03 -7.7143e+02 -3.1476e-01 -1.0200e+03 -7.4286e+02 -5.7042e-01 -1.0200e+03 -7.1429e+02 -8.3201e-01 -1.0200e+03 -6.8571e+02 -1.0991e+00 -1.0200e+03 -6.5714e+02 -1.3714e+00 -1.0200e+03 -6.2857e+02 -1.6481e+00 -1.0200e+03 -6.0000e+02 -1.9287e+00 -1.0200e+03 -5.7143e+02 -2.2123e+00 -1.0200e+03 -5.4286e+02 -2.4981e+00 -1.0200e+03 -5.1429e+02 -2.7848e+00 -1.0200e+03 -4.8571e+02 -3.0714e+00 -1.0200e+03 -4.5714e+02 -3.3564e+00 -1.0200e+03 -4.2857e+02 -3.6383e+00 -1.0200e+03 -4.0000e+02 -3.9154e+00 -1.0200e+03 -3.7143e+02 -4.1861e+00 -1.0200e+03 -3.4286e+02 -4.4482e+00 -1.0200e+03 -3.1429e+02 -4.7000e+00 -1.0200e+03 -2.8571e+02 -4.9393e+00 -1.0200e+03 -2.5714e+02 -5.1639e+00 -1.0200e+03 -2.2857e+02 -5.3719e+00 -1.0200e+03 -2.0000e+02 -5.5612e+00 -1.0200e+03 -1.7143e+02 -5.7298e+00 -1.0200e+03 -1.4286e+02 -5.8759e+00 -1.0200e+03 -1.1429e+02 -5.9978e+00 -1.0200e+03 -8.5714e+01 -6.0942e+00 -1.0200e+03 -5.7143e+01 -6.1639e+00 -1.0200e+03 -2.8571e+01 -6.2060e+00 -1.0200e+03 0.0000e+00 -6.2201e+00 -1.0200e+03 2.8571e+01 -6.2060e+00 -1.0200e+03 5.7143e+01 -6.1639e+00 -1.0200e+03 8.5714e+01 -6.0942e+00 -1.0200e+03 1.1429e+02 -5.9978e+00 -1.0200e+03 1.4286e+02 -5.8759e+00 -1.0200e+03 1.7143e+02 -5.7298e+00 -1.0200e+03 2.0000e+02 -5.5612e+00 -1.0200e+03 2.2857e+02 -5.3719e+00 -1.0200e+03 2.5714e+02 -5.1639e+00 -1.0200e+03 2.8571e+02 -4.9393e+00 -1.0200e+03 3.1429e+02 -4.7000e+00 -1.0200e+03 3.4286e+02 -4.4482e+00 -1.0200e+03 3.7143e+02 -4.1861e+00 -1.0200e+03 4.0000e+02 -3.9154e+00 -1.0200e+03 4.2857e+02 -3.6383e+00 -1.0200e+03 4.5714e+02 -3.3564e+00 -1.0200e+03 4.8571e+02 -3.0714e+00 -1.0200e+03 5.1429e+02 -2.7848e+00 -1.0200e+03 5.4286e+02 -2.4981e+00 -1.0200e+03 5.7143e+02 -2.2123e+00 -1.0200e+03 6.0000e+02 -1.9287e+00 -1.0200e+03 6.2857e+02 -1.6481e+00 -1.0200e+03 6.5714e+02 -1.3714e+00 -1.0200e+03 6.8571e+02 -1.0991e+00 -1.0200e+03 7.1429e+02 -8.3201e-01 -1.0200e+03 7.4286e+02 -5.7042e-01 -1.0200e+03 7.7143e+02 -3.1476e-01 -1.0200e+03 8.0000e+02 -6.5281e-02 -1.0200e+03 8.2857e+02 1.7780e-01 -1.0200e+03 8.5714e+02 4.1434e-01 -1.0200e+03 8.8571e+02 6.4427e-01 -1.0200e+03 9.1429e+02 8.6755e-01 -1.0200e+03 9.4286e+02 1.0842e+00 -1.0200e+03 9.7143e+02 1.2943e+00 -1.0200e+03 1.0000e+03 1.4978e+00 -1.0200e+03 1.0286e+03 1.6950e+00 -1.0200e+03 1.0571e+03 1.8858e+00 -1.0200e+03 1.0857e+03 2.0706e+00 -1.0200e+03 1.1143e+03 2.2493e+00 -1.0200e+03 1.1429e+03 2.4222e+00 -1.0200e+03 1.1714e+03 2.5894e+00 -1.0200e+03 1.2000e+03 2.7511e+00 -1.0200e+03 1.2286e+03 2.9075e+00 -1.0200e+03 1.2571e+03 3.0587e+00 -1.0200e+03 1.2857e+03 3.2050e+00 -1.0200e+03 1.3143e+03 3.3464e+00 -1.0200e+03 1.3429e+03 3.4833e+00 -1.0200e+03 1.3714e+03 3.6156e+00 -1.0200e+03 1.4000e+03 3.7436e+00 -1.0200e+03 1.4286e+03 3.8675e+00 -1.0200e+03 1.4571e+03 3.9874e+00 -1.0200e+03 1.4857e+03 4.1035e+00 -1.0200e+03 1.5143e+03 4.2159e+00 -1.0200e+03 1.5429e+03 4.3247e+00 -1.0200e+03 1.5714e+03 4.4301e+00 -1.0200e+03 1.6000e+03 4.5322e+00 -1.0200e+03 1.6286e+03 4.6311e+00 -1.0200e+03 1.6571e+03 4.7271e+00 -1.0200e+03 1.6857e+03 4.8201e+00 -1.0200e+03 1.7143e+03 4.9103e+00 -1.0200e+03 1.7429e+03 4.9977e+00 -1.0200e+03 1.7714e+03 5.0826e+00 -1.0200e+03 1.8000e+03 5.1650e+00 -1.0200e+03 1.8286e+03 5.2450e+00 -1.0200e+03 1.8571e+03 5.3227e+00 -1.0200e+03 1.8857e+03 5.3982e+00 -1.0200e+03 1.9143e+03 5.4715e+00 -1.0200e+03 1.9429e+03 5.5427e+00 -1.0200e+03 1.9714e+03 5.6120e+00 -1.0200e+03 2.0000e+03 5.6794e+00 -1.0500e+03 -2.0000e+03 5.7151e+00 -1.0500e+03 -1.9714e+03 5.6492e+00 -1.0500e+03 -1.9429e+03 5.5816e+00 -1.0500e+03 -1.9143e+03 5.5120e+00 -1.0500e+03 -1.8857e+03 5.4405e+00 -1.0500e+03 -1.8571e+03 5.3669e+00 -1.0500e+03 -1.8286e+03 5.2913e+00 -1.0500e+03 -1.8000e+03 5.2134e+00 -1.0500e+03 -1.7714e+03 5.1332e+00 -1.0500e+03 -1.7429e+03 5.0507e+00 -1.0500e+03 -1.7143e+03 4.9657e+00 -1.0500e+03 -1.6857e+03 4.8782e+00 -1.0500e+03 -1.6571e+03 4.7880e+00 -1.0500e+03 -1.6286e+03 4.6951e+00 -1.0500e+03 -1.6000e+03 4.5993e+00 -1.0500e+03 -1.5714e+03 4.5006e+00 -1.0500e+03 -1.5429e+03 4.3988e+00 -1.0500e+03 -1.5143e+03 4.2938e+00 -1.0500e+03 -1.4857e+03 4.1855e+00 -1.0500e+03 -1.4571e+03 4.0737e+00 -1.0500e+03 -1.4286e+03 3.9584e+00 -1.0500e+03 -1.4000e+03 3.8394e+00 -1.0500e+03 -1.3714e+03 3.7166e+00 -1.0500e+03 -1.3429e+03 3.5898e+00 -1.0500e+03 -1.3143e+03 3.4589e+00 -1.0500e+03 -1.2857e+03 3.3238e+00 -1.0500e+03 -1.2571e+03 3.1843e+00 -1.0500e+03 -1.2286e+03 3.0402e+00 -1.0500e+03 -1.2000e+03 2.8915e+00 -1.0500e+03 -1.1714e+03 2.7380e+00 -1.0500e+03 -1.1429e+03 2.5795e+00 -1.0500e+03 -1.1143e+03 2.4160e+00 -1.0500e+03 -1.0857e+03 2.2472e+00 -1.0500e+03 -1.0571e+03 2.0731e+00 -1.0500e+03 -1.0286e+03 1.8936e+00 -1.0500e+03 -1.0000e+03 1.7086e+00 -1.0500e+03 -9.7143e+02 1.5180e+00 -1.0500e+03 -9.4286e+02 1.3217e+00 -1.0500e+03 -9.1429e+02 1.1198e+00 -1.0500e+03 -8.8571e+02 9.1220e-01 -1.0500e+03 -8.5714e+02 6.9895e-01 -1.0500e+03 -8.2857e+02 4.8013e-01 -1.0500e+03 -8.0000e+02 2.5587e-01 -1.0500e+03 -7.7143e+02 2.6339e-02 -1.0500e+03 -7.4286e+02 -2.0822e-01 -1.0500e+03 -7.1429e+02 -4.4752e-01 -1.0500e+03 -6.8571e+02 -6.9116e-01 -1.0500e+03 -6.5714e+02 -9.3869e-01 -1.0500e+03 -6.2857e+02 -1.1896e+00 -1.0500e+03 -6.0000e+02 -1.4431e+00 -1.0500e+03 -5.7143e+02 -1.6986e+00 -1.0500e+03 -5.4286e+02 -1.9552e+00 -1.0500e+03 -5.1429e+02 -2.2118e+00 -1.0500e+03 -4.8571e+02 -2.4675e+00 -1.0500e+03 -4.5714e+02 -2.7209e+00 -1.0500e+03 -4.2857e+02 -2.9708e+00 -1.0500e+03 -4.0000e+02 -3.2157e+00 -1.0500e+03 -3.7143e+02 -3.4541e+00 -1.0500e+03 -3.4286e+02 -3.6844e+00 -1.0500e+03 -3.1429e+02 -3.9049e+00 -1.0500e+03 -2.8571e+02 -4.1139e+00 -1.0500e+03 -2.5714e+02 -4.3097e+00 -1.0500e+03 -2.2857e+02 -4.4905e+00 -1.0500e+03 -2.0000e+02 -4.6547e+00 -1.0500e+03 -1.7143e+02 -4.8006e+00 -1.0500e+03 -1.4286e+02 -4.9268e+00 -1.0500e+03 -1.1429e+02 -5.0320e+00 -1.0500e+03 -8.5714e+01 -5.1151e+00 -1.0500e+03 -5.7143e+01 -5.1751e+00 -1.0500e+03 -2.8571e+01 -5.2114e+00 -1.0500e+03 0.0000e+00 -5.2235e+00 -1.0500e+03 2.8571e+01 -5.2114e+00 -1.0500e+03 5.7143e+01 -5.1751e+00 -1.0500e+03 8.5714e+01 -5.1151e+00 -1.0500e+03 1.1429e+02 -5.0320e+00 -1.0500e+03 1.4286e+02 -4.9268e+00 -1.0500e+03 1.7143e+02 -4.8006e+00 -1.0500e+03 2.0000e+02 -4.6547e+00 -1.0500e+03 2.2857e+02 -4.4905e+00 -1.0500e+03 2.5714e+02 -4.3097e+00 -1.0500e+03 2.8571e+02 -4.1139e+00 -1.0500e+03 3.1429e+02 -3.9049e+00 -1.0500e+03 3.4286e+02 -3.6844e+00 -1.0500e+03 3.7143e+02 -3.4541e+00 -1.0500e+03 4.0000e+02 -3.2157e+00 -1.0500e+03 4.2857e+02 -2.9708e+00 -1.0500e+03 4.5714e+02 -2.7209e+00 -1.0500e+03 4.8571e+02 -2.4675e+00 -1.0500e+03 5.1429e+02 -2.2118e+00 -1.0500e+03 5.4286e+02 -1.9552e+00 -1.0500e+03 5.7143e+02 -1.6986e+00 -1.0500e+03 6.0000e+02 -1.4431e+00 -1.0500e+03 6.2857e+02 -1.1896e+00 -1.0500e+03 6.5714e+02 -9.3869e-01 -1.0500e+03 6.8571e+02 -6.9116e-01 -1.0500e+03 7.1429e+02 -4.4752e-01 -1.0500e+03 7.4286e+02 -2.0822e-01 -1.0500e+03 7.7143e+02 2.6339e-02 -1.0500e+03 8.0000e+02 2.5587e-01 -1.0500e+03 8.2857e+02 4.8013e-01 -1.0500e+03 8.5714e+02 6.9895e-01 -1.0500e+03 8.8571e+02 9.1220e-01 -1.0500e+03 9.1429e+02 1.1198e+00 -1.0500e+03 9.4286e+02 1.3217e+00 -1.0500e+03 9.7143e+02 1.5180e+00 -1.0500e+03 1.0000e+03 1.7086e+00 -1.0500e+03 1.0286e+03 1.8936e+00 -1.0500e+03 1.0571e+03 2.0731e+00 -1.0500e+03 1.0857e+03 2.2472e+00 -1.0500e+03 1.1143e+03 2.4160e+00 -1.0500e+03 1.1429e+03 2.5795e+00 -1.0500e+03 1.1714e+03 2.7380e+00 -1.0500e+03 1.2000e+03 2.8915e+00 -1.0500e+03 1.2286e+03 3.0402e+00 -1.0500e+03 1.2571e+03 3.1843e+00 -1.0500e+03 1.2857e+03 3.3238e+00 -1.0500e+03 1.3143e+03 3.4589e+00 -1.0500e+03 1.3429e+03 3.5898e+00 -1.0500e+03 1.3714e+03 3.7166e+00 -1.0500e+03 1.4000e+03 3.8394e+00 -1.0500e+03 1.4286e+03 3.9584e+00 -1.0500e+03 1.4571e+03 4.0737e+00 -1.0500e+03 1.4857e+03 4.1855e+00 -1.0500e+03 1.5143e+03 4.2938e+00 -1.0500e+03 1.5429e+03 4.3988e+00 -1.0500e+03 1.5714e+03 4.5006e+00 -1.0500e+03 1.6000e+03 4.5993e+00 -1.0500e+03 1.6286e+03 4.6951e+00 -1.0500e+03 1.6571e+03 4.7880e+00 -1.0500e+03 1.6857e+03 4.8782e+00 -1.0500e+03 1.7143e+03 4.9657e+00 -1.0500e+03 1.7429e+03 5.0507e+00 -1.0500e+03 1.7714e+03 5.1332e+00 -1.0500e+03 1.8000e+03 5.2134e+00 -1.0500e+03 1.8286e+03 5.2913e+00 -1.0500e+03 1.8571e+03 5.3669e+00 -1.0500e+03 1.8857e+03 5.4405e+00 -1.0500e+03 1.9143e+03 5.5120e+00 -1.0500e+03 1.9429e+03 5.5816e+00 -1.0500e+03 1.9714e+03 5.6492e+00 -1.0500e+03 2.0000e+03 5.7151e+00 -1.0800e+03 -2.0000e+03 5.7510e+00 -1.0800e+03 -1.9714e+03 5.6866e+00 -1.0800e+03 -1.9429e+03 5.6206e+00 -1.0800e+03 -1.9143e+03 5.5527e+00 -1.0800e+03 -1.8857e+03 5.4830e+00 -1.0800e+03 -1.8571e+03 5.4113e+00 -1.0800e+03 -1.8286e+03 5.3376e+00 -1.0800e+03 -1.8000e+03 5.2618e+00 -1.0800e+03 -1.7714e+03 5.1838e+00 -1.0800e+03 -1.7429e+03 5.1037e+00 -1.0800e+03 -1.7143e+03 5.0212e+00 -1.0800e+03 -1.6857e+03 4.9362e+00 -1.0800e+03 -1.6571e+03 4.8488e+00 -1.0800e+03 -1.6286e+03 4.7588e+00 -1.0800e+03 -1.6000e+03 4.6662e+00 -1.0800e+03 -1.5714e+03 4.5707e+00 -1.0800e+03 -1.5429e+03 4.4724e+00 -1.0800e+03 -1.5143e+03 4.3711e+00 -1.0800e+03 -1.4857e+03 4.2667e+00 -1.0800e+03 -1.4571e+03 4.1592e+00 -1.0800e+03 -1.4286e+03 4.0483e+00 -1.0800e+03 -1.4000e+03 3.9340e+00 -1.0800e+03 -1.3714e+03 3.8162e+00 -1.0800e+03 -1.3429e+03 3.6948e+00 -1.0800e+03 -1.3143e+03 3.5695e+00 -1.0800e+03 -1.2857e+03 3.4405e+00 -1.0800e+03 -1.2571e+03 3.3074e+00 -1.0800e+03 -1.2286e+03 3.1702e+00 -1.0800e+03 -1.2000e+03 3.0287e+00 -1.0800e+03 -1.1714e+03 2.8830e+00 -1.0800e+03 -1.1429e+03 2.7328e+00 -1.0800e+03 -1.1143e+03 2.5780e+00 -1.0800e+03 -1.0857e+03 2.4187e+00 -1.0800e+03 -1.0571e+03 2.2546e+00 -1.0800e+03 -1.0286e+03 2.0857e+00 -1.0800e+03 -1.0000e+03 1.9120e+00 -1.0800e+03 -9.7143e+02 1.7334e+00 -1.0800e+03 -9.4286e+02 1.5499e+00 -1.0800e+03 -9.1429e+02 1.3616e+00 -1.0800e+03 -8.8571e+02 1.1684e+00 -1.0800e+03 -8.5714e+02 9.7037e-01 -1.0800e+03 -8.2857e+02 7.6771e-01 -1.0800e+03 -8.0000e+02 5.6054e-01 -1.0800e+03 -7.7143e+02 3.4905e-01 -1.0800e+03 -7.4286e+02 1.3351e-01 -1.0800e+03 -7.1429e+02 -8.5786e-02 -1.0800e+03 -6.8571e+02 -3.0845e-01 -1.0800e+03 -6.5714e+02 -5.3402e-01 -1.0800e+03 -6.2857e+02 -7.6197e-01 -1.0800e+03 -6.0000e+02 -9.9170e-01 -1.0800e+03 -5.7143e+02 -1.2225e+00 -1.0800e+03 -5.4286e+02 -1.4536e+00 -1.0800e+03 -5.1429e+02 -1.6840e+00 -1.0800e+03 -4.8571e+02 -1.9129e+00 -1.0800e+03 -4.5714e+02 -2.1392e+00 -1.0800e+03 -4.2857e+02 -2.3616e+00 -1.0800e+03 -4.0000e+02 -2.5790e+00 -1.0800e+03 -3.7143e+02 -2.7900e+00 -1.0800e+03 -3.4286e+02 -2.9933e+00 -1.0800e+03 -3.1429e+02 -3.1875e+00 -1.0800e+03 -2.8571e+02 -3.3710e+00 -1.0800e+03 -2.5714e+02 -3.5426e+00 -1.0800e+03 -2.2857e+02 -3.7006e+00 -1.0800e+03 -2.0000e+02 -3.8439e+00 -1.0800e+03 -1.7143e+02 -3.9710e+00 -1.0800e+03 -1.4286e+02 -4.0808e+00 -1.0800e+03 -1.1429e+02 -4.1722e+00 -1.0800e+03 -8.5714e+01 -4.2443e+00 -1.0800e+03 -5.7143e+01 -4.2963e+00 -1.0800e+03 -2.8571e+01 -4.3278e+00 -1.0800e+03 0.0000e+00 -4.3383e+00 -1.0800e+03 2.8571e+01 -4.3278e+00 -1.0800e+03 5.7143e+01 -4.2963e+00 -1.0800e+03 8.5714e+01 -4.2443e+00 -1.0800e+03 1.1429e+02 -4.1722e+00 -1.0800e+03 1.4286e+02 -4.0808e+00 -1.0800e+03 1.7143e+02 -3.9710e+00 -1.0800e+03 2.0000e+02 -3.8439e+00 -1.0800e+03 2.2857e+02 -3.7006e+00 -1.0800e+03 2.5714e+02 -3.5426e+00 -1.0800e+03 2.8571e+02 -3.3710e+00 -1.0800e+03 3.1429e+02 -3.1875e+00 -1.0800e+03 3.4286e+02 -2.9933e+00 -1.0800e+03 3.7143e+02 -2.7900e+00 -1.0800e+03 4.0000e+02 -2.5790e+00 -1.0800e+03 4.2857e+02 -2.3616e+00 -1.0800e+03 4.5714e+02 -2.1392e+00 -1.0800e+03 4.8571e+02 -1.9129e+00 -1.0800e+03 5.1429e+02 -1.6840e+00 -1.0800e+03 5.4286e+02 -1.4536e+00 -1.0800e+03 5.7143e+02 -1.2225e+00 -1.0800e+03 6.0000e+02 -9.9170e-01 -1.0800e+03 6.2857e+02 -7.6197e-01 -1.0800e+03 6.5714e+02 -5.3402e-01 -1.0800e+03 6.8571e+02 -3.0845e-01 -1.0800e+03 7.1429e+02 -8.5786e-02 -1.0800e+03 7.4286e+02 1.3351e-01 -1.0800e+03 7.7143e+02 3.4905e-01 -1.0800e+03 8.0000e+02 5.6054e-01 -1.0800e+03 8.2857e+02 7.6771e-01 -1.0800e+03 8.5714e+02 9.7037e-01 -1.0800e+03 8.8571e+02 1.1684e+00 -1.0800e+03 9.1429e+02 1.3616e+00 -1.0800e+03 9.4286e+02 1.5499e+00 -1.0800e+03 9.7143e+02 1.7334e+00 -1.0800e+03 1.0000e+03 1.9120e+00 -1.0800e+03 1.0286e+03 2.0857e+00 -1.0800e+03 1.0571e+03 2.2546e+00 -1.0800e+03 1.0857e+03 2.4187e+00 -1.0800e+03 1.1143e+03 2.5780e+00 -1.0800e+03 1.1429e+03 2.7328e+00 -1.0800e+03 1.1714e+03 2.8830e+00 -1.0800e+03 1.2000e+03 3.0287e+00 -1.0800e+03 1.2286e+03 3.1702e+00 -1.0800e+03 1.2571e+03 3.3074e+00 -1.0800e+03 1.2857e+03 3.4405e+00 -1.0800e+03 1.3143e+03 3.5695e+00 -1.0800e+03 1.3429e+03 3.6948e+00 -1.0800e+03 1.3714e+03 3.8162e+00 -1.0800e+03 1.4000e+03 3.9340e+00 -1.0800e+03 1.4286e+03 4.0483e+00 -1.0800e+03 1.4571e+03 4.1592e+00 -1.0800e+03 1.4857e+03 4.2667e+00 -1.0800e+03 1.5143e+03 4.3711e+00 -1.0800e+03 1.5429e+03 4.4724e+00 -1.0800e+03 1.5714e+03 4.5707e+00 -1.0800e+03 1.6000e+03 4.6662e+00 -1.0800e+03 1.6286e+03 4.7588e+00 -1.0800e+03 1.6571e+03 4.8488e+00 -1.0800e+03 1.6857e+03 4.9362e+00 -1.0800e+03 1.7143e+03 5.0212e+00 -1.0800e+03 1.7429e+03 5.1037e+00 -1.0800e+03 1.7714e+03 5.1838e+00 -1.0800e+03 1.8000e+03 5.2618e+00 -1.0800e+03 1.8286e+03 5.3376e+00 -1.0800e+03 1.8571e+03 5.4113e+00 -1.0800e+03 1.8857e+03 5.4830e+00 -1.0800e+03 1.9143e+03 5.5527e+00 -1.0800e+03 1.9429e+03 5.6206e+00 -1.0800e+03 1.9714e+03 5.6866e+00 -1.0800e+03 2.0000e+03 5.7510e+00 -1.1100e+03 -2.0000e+03 5.7870e+00 -1.1100e+03 -1.9714e+03 5.7242e+00 -1.1100e+03 -1.9429e+03 5.6597e+00 -1.1100e+03 -1.9143e+03 5.5935e+00 -1.1100e+03 -1.8857e+03 5.5255e+00 -1.1100e+03 -1.8571e+03 5.4557e+00 -1.1100e+03 -1.8286e+03 5.3839e+00 -1.1100e+03 -1.8000e+03 5.3102e+00 -1.1100e+03 -1.7714e+03 5.2344e+00 -1.1100e+03 -1.7429e+03 5.1565e+00 -1.1100e+03 -1.7143e+03 5.0764e+00 -1.1100e+03 -1.6857e+03 4.9941e+00 -1.1100e+03 -1.6571e+03 4.9094e+00 -1.1100e+03 -1.6286e+03 4.8223e+00 -1.1100e+03 -1.6000e+03 4.7326e+00 -1.1100e+03 -1.5714e+03 4.6404e+00 -1.1100e+03 -1.5429e+03 4.5455e+00 -1.1100e+03 -1.5143e+03 4.4478e+00 -1.1100e+03 -1.4857e+03 4.3472e+00 -1.1100e+03 -1.4571e+03 4.2437e+00 -1.1100e+03 -1.4286e+03 4.1371e+00 -1.1100e+03 -1.4000e+03 4.0274e+00 -1.1100e+03 -1.3714e+03 3.9144e+00 -1.1100e+03 -1.3429e+03 3.7981e+00 -1.1100e+03 -1.3143e+03 3.6783e+00 -1.1100e+03 -1.2857e+03 3.5550e+00 -1.1100e+03 -1.2571e+03 3.4280e+00 -1.1100e+03 -1.2286e+03 3.2973e+00 -1.1100e+03 -1.2000e+03 3.1628e+00 -1.1100e+03 -1.1714e+03 3.0244e+00 -1.1100e+03 -1.1429e+03 2.8821e+00 -1.1100e+03 -1.1143e+03 2.7356e+00 -1.1100e+03 -1.0857e+03 2.5851e+00 -1.1100e+03 -1.0571e+03 2.4303e+00 -1.1100e+03 -1.0286e+03 2.2714e+00 -1.1100e+03 -1.0000e+03 2.1082e+00 -1.1100e+03 -9.7143e+02 1.9408e+00 -1.1100e+03 -9.4286e+02 1.7691e+00 -1.1100e+03 -9.1429e+02 1.5933e+00 -1.1100e+03 -8.8571e+02 1.4133e+00 -1.1100e+03 -8.5714e+02 1.2293e+00 -1.1100e+03 -8.2857e+02 1.0414e+00 -1.1100e+03 -8.0000e+02 8.4975e-01 -1.1100e+03 -7.7143e+02 6.5461e-01 -1.1100e+03 -7.4286e+02 4.5622e-01 -1.1100e+03 -7.1429e+02 2.5490e-01 -1.1100e+03 -6.8571e+02 5.1018e-02 -1.1100e+03 -6.5714e+02 -1.5498e-01 -1.1100e+03 -6.2857e+02 -3.6260e-01 -1.1100e+03 -6.0000e+02 -5.7126e-01 -1.1100e+03 -5.7143e+02 -7.8032e-01 -1.1100e+03 -5.4286e+02 -9.8905e-01 -1.1100e+03 -5.1429e+02 -1.1967e+00 -1.1100e+03 -4.8571e+02 -1.4023e+00 -1.1100e+03 -4.5714e+02 -1.6050e+00 -1.1100e+03 -4.2857e+02 -1.8038e+00 -1.1100e+03 -4.0000e+02 -1.9975e+00 -1.1100e+03 -3.7143e+02 -2.1851e+00 -1.1100e+03 -3.4286e+02 -2.3654e+00 -1.1100e+03 -3.1429e+02 -2.5372e+00 -1.1100e+03 -2.8571e+02 -2.6992e+00 -1.1100e+03 -2.5714e+02 -2.8503e+00 -1.1100e+03 -2.2857e+02 -2.9893e+00 -1.1100e+03 -2.0000e+02 -3.1150e+00 -1.1100e+03 -1.7143e+02 -3.2264e+00 -1.1100e+03 -1.4286e+02 -3.3225e+00 -1.1100e+03 -1.1429e+02 -3.4024e+00 -1.1100e+03 -8.5714e+01 -3.4653e+00 -1.1100e+03 -5.7143e+01 -3.5107e+00 -1.1100e+03 -2.8571e+01 -3.5381e+00 -1.1100e+03 0.0000e+00 -3.5473e+00 -1.1100e+03 2.8571e+01 -3.5381e+00 -1.1100e+03 5.7143e+01 -3.5107e+00 -1.1100e+03 8.5714e+01 -3.4653e+00 -1.1100e+03 1.1429e+02 -3.4024e+00 -1.1100e+03 1.4286e+02 -3.3225e+00 -1.1100e+03 1.7143e+02 -3.2264e+00 -1.1100e+03 2.0000e+02 -3.1150e+00 -1.1100e+03 2.2857e+02 -2.9893e+00 -1.1100e+03 2.5714e+02 -2.8503e+00 -1.1100e+03 2.8571e+02 -2.6992e+00 -1.1100e+03 3.1429e+02 -2.5372e+00 -1.1100e+03 3.4286e+02 -2.3654e+00 -1.1100e+03 3.7143e+02 -2.1851e+00 -1.1100e+03 4.0000e+02 -1.9975e+00 -1.1100e+03 4.2857e+02 -1.8038e+00 -1.1100e+03 4.5714e+02 -1.6050e+00 -1.1100e+03 4.8571e+02 -1.4023e+00 -1.1100e+03 5.1429e+02 -1.1967e+00 -1.1100e+03 5.4286e+02 -9.8905e-01 -1.1100e+03 5.7143e+02 -7.8032e-01 -1.1100e+03 6.0000e+02 -5.7126e-01 -1.1100e+03 6.2857e+02 -3.6260e-01 -1.1100e+03 6.5714e+02 -1.5498e-01 -1.1100e+03 6.8571e+02 5.1018e-02 -1.1100e+03 7.1429e+02 2.5490e-01 -1.1100e+03 7.4286e+02 4.5622e-01 -1.1100e+03 7.7143e+02 6.5461e-01 -1.1100e+03 8.0000e+02 8.4975e-01 -1.1100e+03 8.2857e+02 1.0414e+00 -1.1100e+03 8.5714e+02 1.2293e+00 -1.1100e+03 8.8571e+02 1.4133e+00 -1.1100e+03 9.1429e+02 1.5933e+00 -1.1100e+03 9.4286e+02 1.7691e+00 -1.1100e+03 9.7143e+02 1.9408e+00 -1.1100e+03 1.0000e+03 2.1082e+00 -1.1100e+03 1.0286e+03 2.2714e+00 -1.1100e+03 1.0571e+03 2.4303e+00 -1.1100e+03 1.0857e+03 2.5851e+00 -1.1100e+03 1.1143e+03 2.7356e+00 -1.1100e+03 1.1429e+03 2.8821e+00 -1.1100e+03 1.1714e+03 3.0244e+00 -1.1100e+03 1.2000e+03 3.1628e+00 -1.1100e+03 1.2286e+03 3.2973e+00 -1.1100e+03 1.2571e+03 3.4280e+00 -1.1100e+03 1.2857e+03 3.5550e+00 -1.1100e+03 1.3143e+03 3.6783e+00 -1.1100e+03 1.3429e+03 3.7981e+00 -1.1100e+03 1.3714e+03 3.9144e+00 -1.1100e+03 1.4000e+03 4.0274e+00 -1.1100e+03 1.4286e+03 4.1371e+00 -1.1100e+03 1.4571e+03 4.2437e+00 -1.1100e+03 1.4857e+03 4.3472e+00 -1.1100e+03 1.5143e+03 4.4478e+00 -1.1100e+03 1.5429e+03 4.5455e+00 -1.1100e+03 1.5714e+03 4.6404e+00 -1.1100e+03 1.6000e+03 4.7326e+00 -1.1100e+03 1.6286e+03 4.8223e+00 -1.1100e+03 1.6571e+03 4.9094e+00 -1.1100e+03 1.6857e+03 4.9941e+00 -1.1100e+03 1.7143e+03 5.0764e+00 -1.1100e+03 1.7429e+03 5.1565e+00 -1.1100e+03 1.7714e+03 5.2344e+00 -1.1100e+03 1.8000e+03 5.3102e+00 -1.1100e+03 1.8286e+03 5.3839e+00 -1.1100e+03 1.8571e+03 5.4557e+00 -1.1100e+03 1.8857e+03 5.5255e+00 -1.1100e+03 1.9143e+03 5.5935e+00 -1.1100e+03 1.9429e+03 5.6597e+00 -1.1100e+03 1.9714e+03 5.7242e+00 -1.1100e+03 2.0000e+03 5.7870e+00 -1.1400e+03 -2.0000e+03 5.8232e+00 -1.1400e+03 -1.9714e+03 5.7619e+00 -1.1400e+03 -1.9429e+03 5.6990e+00 -1.1400e+03 -1.9143e+03 5.6344e+00 -1.1400e+03 -1.8857e+03 5.5681e+00 -1.1400e+03 -1.8571e+03 5.5001e+00 -1.1400e+03 -1.8286e+03 5.4303e+00 -1.1400e+03 -1.8000e+03 5.3586e+00 -1.1400e+03 -1.7714e+03 5.2849e+00 -1.1400e+03 -1.7429e+03 5.2093e+00 -1.1400e+03 -1.7143e+03 5.1316e+00 -1.1400e+03 -1.6857e+03 5.0517e+00 -1.1400e+03 -1.6571e+03 4.9696e+00 -1.1400e+03 -1.6286e+03 4.8853e+00 -1.1400e+03 -1.6000e+03 4.7986e+00 -1.1400e+03 -1.5714e+03 4.7095e+00 -1.1400e+03 -1.5429e+03 4.6179e+00 -1.1400e+03 -1.5143e+03 4.5237e+00 -1.1400e+03 -1.4857e+03 4.4268e+00 -1.1400e+03 -1.4571e+03 4.3272e+00 -1.1400e+03 -1.4286e+03 4.2247e+00 -1.1400e+03 -1.4000e+03 4.1194e+00 -1.1400e+03 -1.3714e+03 4.0110e+00 -1.1400e+03 -1.3429e+03 3.8996e+00 -1.1400e+03 -1.3143e+03 3.7851e+00 -1.1400e+03 -1.2857e+03 3.6673e+00 -1.1400e+03 -1.2571e+03 3.5462e+00 -1.1400e+03 -1.2286e+03 3.4217e+00 -1.1400e+03 -1.2000e+03 3.2938e+00 -1.1400e+03 -1.1714e+03 3.1623e+00 -1.1400e+03 -1.1429e+03 3.0273e+00 -1.1400e+03 -1.1143e+03 2.8887e+00 -1.1400e+03 -1.0857e+03 2.7464e+00 -1.1400e+03 -1.0571e+03 2.6004e+00 -1.1400e+03 -1.0286e+03 2.4508e+00 -1.1400e+03 -1.0000e+03 2.2974e+00 -1.1400e+03 -9.7143e+02 2.1403e+00 -1.1400e+03 -9.4286e+02 1.9796e+00 -1.1400e+03 -9.1429e+02 1.8153e+00 -1.1400e+03 -8.8571e+02 1.6475e+00 -1.1400e+03 -8.5714e+02 1.4764e+00 -1.1400e+03 -8.2857e+02 1.3019e+00 -1.1400e+03 -8.0000e+02 1.1244e+00 -1.1400e+03 -7.7143e+02 9.4412e-01 -1.1400e+03 -7.4286e+02 7.6124e-01 -1.1400e+03 -7.1429e+02 5.7610e-01 -1.1400e+03 -6.8571e+02 3.8906e-01 -1.1400e+03 -6.5714e+02 2.0055e-01 -1.1400e+03 -6.2857e+02 1.1040e-02 -1.1400e+03 -6.0000e+02 -1.7895e-01 -1.1400e+03 -5.7143e+02 -3.6881e-01 -1.1400e+03 -5.4286e+02 -5.5791e-01 -1.1400e+03 -5.1429e+02 -7.4551e-01 -1.1400e+03 -4.8571e+02 -9.3085e-01 -1.1400e+03 -4.5714e+02 -1.1131e+00 -1.1400e+03 -4.2857e+02 -1.2914e+00 -1.1400e+03 -4.0000e+02 -1.4647e+00 -1.1400e+03 -3.7143e+02 -1.6322e+00 -1.1400e+03 -3.4286e+02 -1.7927e+00 -1.1400e+03 -3.1429e+02 -1.9454e+00 -1.1400e+03 -2.8571e+02 -2.0891e+00 -1.1400e+03 -2.5714e+02 -2.2229e+00 -1.1400e+03 -2.2857e+02 -2.3457e+00 -1.1400e+03 -2.0000e+02 -2.4566e+00 -1.1400e+03 -1.7143e+02 -2.5548e+00 -1.1400e+03 -1.4286e+02 -2.6393e+00 -1.1400e+03 -1.1429e+02 -2.7095e+00 -1.1400e+03 -8.5714e+01 -2.7648e+00 -1.1400e+03 -5.7143e+01 -2.8046e+00 -1.1400e+03 -2.8571e+01 -2.8287e+00 -1.1400e+03 0.0000e+00 -2.8367e+00 -1.1400e+03 2.8571e+01 -2.8287e+00 -1.1400e+03 5.7143e+01 -2.8046e+00 -1.1400e+03 8.5714e+01 -2.7648e+00 -1.1400e+03 1.1429e+02 -2.7095e+00 -1.1400e+03 1.4286e+02 -2.6393e+00 -1.1400e+03 1.7143e+02 -2.5548e+00 -1.1400e+03 2.0000e+02 -2.4566e+00 -1.1400e+03 2.2857e+02 -2.3457e+00 -1.1400e+03 2.5714e+02 -2.2229e+00 -1.1400e+03 2.8571e+02 -2.0891e+00 -1.1400e+03 3.1429e+02 -1.9454e+00 -1.1400e+03 3.4286e+02 -1.7927e+00 -1.1400e+03 3.7143e+02 -1.6322e+00 -1.1400e+03 4.0000e+02 -1.4647e+00 -1.1400e+03 4.2857e+02 -1.2914e+00 -1.1400e+03 4.5714e+02 -1.1131e+00 -1.1400e+03 4.8571e+02 -9.3085e-01 -1.1400e+03 5.1429e+02 -7.4551e-01 -1.1400e+03 5.4286e+02 -5.5791e-01 -1.1400e+03 5.7143e+02 -3.6881e-01 -1.1400e+03 6.0000e+02 -1.7895e-01 -1.1400e+03 6.2857e+02 1.1040e-02 -1.1400e+03 6.5714e+02 2.0055e-01 -1.1400e+03 6.8571e+02 3.8906e-01 -1.1400e+03 7.1429e+02 5.7610e-01 -1.1400e+03 7.4286e+02 7.6124e-01 -1.1400e+03 7.7143e+02 9.4412e-01 -1.1400e+03 8.0000e+02 1.1244e+00 -1.1400e+03 8.2857e+02 1.3019e+00 -1.1400e+03 8.5714e+02 1.4764e+00 -1.1400e+03 8.8571e+02 1.6475e+00 -1.1400e+03 9.1429e+02 1.8153e+00 -1.1400e+03 9.4286e+02 1.9796e+00 -1.1400e+03 9.7143e+02 2.1403e+00 -1.1400e+03 1.0000e+03 2.2974e+00 -1.1400e+03 1.0286e+03 2.4508e+00 -1.1400e+03 1.0571e+03 2.6004e+00 -1.1400e+03 1.0857e+03 2.7464e+00 -1.1400e+03 1.1143e+03 2.8887e+00 -1.1400e+03 1.1429e+03 3.0273e+00 -1.1400e+03 1.1714e+03 3.1623e+00 -1.1400e+03 1.2000e+03 3.2938e+00 -1.1400e+03 1.2286e+03 3.4217e+00 -1.1400e+03 1.2571e+03 3.5462e+00 -1.1400e+03 1.2857e+03 3.6673e+00 -1.1400e+03 1.3143e+03 3.7851e+00 -1.1400e+03 1.3429e+03 3.8996e+00 -1.1400e+03 1.3714e+03 4.0110e+00 -1.1400e+03 1.4000e+03 4.1194e+00 -1.1400e+03 1.4286e+03 4.2247e+00 -1.1400e+03 1.4571e+03 4.3272e+00 -1.1400e+03 1.4857e+03 4.4268e+00 -1.1400e+03 1.5143e+03 4.5237e+00 -1.1400e+03 1.5429e+03 4.6179e+00 -1.1400e+03 1.5714e+03 4.7095e+00 -1.1400e+03 1.6000e+03 4.7986e+00 -1.1400e+03 1.6286e+03 4.8853e+00 -1.1400e+03 1.6571e+03 4.9696e+00 -1.1400e+03 1.6857e+03 5.0517e+00 -1.1400e+03 1.7143e+03 5.1316e+00 -1.1400e+03 1.7429e+03 5.2093e+00 -1.1400e+03 1.7714e+03 5.2849e+00 -1.1400e+03 1.8000e+03 5.3586e+00 -1.1400e+03 1.8286e+03 5.4303e+00 -1.1400e+03 1.8571e+03 5.5001e+00 -1.1400e+03 1.8857e+03 5.5681e+00 -1.1400e+03 1.9143e+03 5.6344e+00 -1.1400e+03 1.9429e+03 5.6990e+00 -1.1400e+03 1.9714e+03 5.7619e+00 -1.1400e+03 2.0000e+03 5.8232e+00 -1.1700e+03 -2.0000e+03 5.8595e+00 -1.1700e+03 -1.9714e+03 5.7996e+00 -1.1700e+03 -1.9429e+03 5.7382e+00 -1.1700e+03 -1.9143e+03 5.6753e+00 -1.1700e+03 -1.8857e+03 5.6107e+00 -1.1700e+03 -1.8571e+03 5.5445e+00 -1.1700e+03 -1.8286e+03 5.4766e+00 -1.1700e+03 -1.8000e+03 5.4068e+00 -1.1700e+03 -1.7714e+03 5.3353e+00 -1.1700e+03 -1.7429e+03 5.2618e+00 -1.1700e+03 -1.7143e+03 5.1864e+00 -1.1700e+03 -1.6857e+03 5.1090e+00 -1.1700e+03 -1.6571e+03 5.0295e+00 -1.1700e+03 -1.6286e+03 4.9479e+00 -1.1700e+03 -1.6000e+03 4.8641e+00 -1.1700e+03 -1.5714e+03 4.7780e+00 -1.1700e+03 -1.5429e+03 4.6896e+00 -1.1700e+03 -1.5143e+03 4.5988e+00 -1.1700e+03 -1.4857e+03 4.5055e+00 -1.1700e+03 -1.4571e+03 4.4096e+00 -1.1700e+03 -1.4286e+03 4.3112e+00 -1.1700e+03 -1.4000e+03 4.2100e+00 -1.1700e+03 -1.3714e+03 4.1061e+00 -1.1700e+03 -1.3429e+03 3.9994e+00 -1.1700e+03 -1.3143e+03 3.8899e+00 -1.1700e+03 -1.2857e+03 3.7773e+00 -1.1700e+03 -1.2571e+03 3.6618e+00 -1.1700e+03 -1.2286e+03 3.5432e+00 -1.1700e+03 -1.2000e+03 3.4215e+00 -1.1700e+03 -1.1714e+03 3.2967e+00 -1.1700e+03 -1.1429e+03 3.1686e+00 -1.1700e+03 -1.1143e+03 3.0374e+00 -1.1700e+03 -1.0857e+03 2.9029e+00 -1.1700e+03 -1.0571e+03 2.7651e+00 -1.1700e+03 -1.0286e+03 2.6241e+00 -1.1700e+03 -1.0000e+03 2.4799e+00 -1.1700e+03 -9.7143e+02 2.3324e+00 -1.1700e+03 -9.4286e+02 2.1819e+00 -1.1700e+03 -9.1429e+02 2.0282e+00 -1.1700e+03 -8.8571e+02 1.8716e+00 -1.1700e+03 -8.5714e+02 1.7122e+00 -1.1700e+03 -8.2857e+02 1.5501e+00 -1.1700e+03 -8.0000e+02 1.3855e+00 -1.1700e+03 -7.7143e+02 1.2187e+00 -1.1700e+03 -7.4286e+02 1.0498e+00 -1.1700e+03 -7.1429e+02 8.7926e-01 -1.1700e+03 -6.8571e+02 7.0736e-01 -1.1700e+03 -6.5714e+02 5.3451e-01 -1.1700e+03 -6.2857e+02 3.6115e-01 -1.1700e+03 -6.0000e+02 1.8776e-01 -1.1700e+03 -5.7143e+02 1.4889e-02 -1.1700e+03 -5.4286e+02 -1.5687e-01 -1.1700e+03 -5.1429e+02 -3.2688e-01 -1.1700e+03 -4.8571e+02 -4.9444e-01 -1.1700e+03 -4.5714e+02 -6.5883e-01 -1.1700e+03 -4.2857e+02 -8.1927e-01 -1.1700e+03 -4.0000e+02 -9.7493e-01 -1.1700e+03 -3.7143e+02 -1.1250e+00 -1.1700e+03 -3.4286e+02 -1.2686e+00 -1.1700e+03 -3.1429e+02 -1.4048e+00 -1.1700e+03 -2.8571e+02 -1.5329e+00 -1.1700e+03 -2.5714e+02 -1.6518e+00 -1.1700e+03 -2.2857e+02 -1.7609e+00 -1.1700e+03 -2.0000e+02 -1.8592e+00 -1.1700e+03 -1.7143e+02 -1.9461e+00 -1.1700e+03 -1.4286e+02 -2.0209e+00 -1.1700e+03 -1.1429e+02 -2.0829e+00 -1.1700e+03 -8.5714e+01 -2.1317e+00 -1.1700e+03 -5.7143e+01 -2.1669e+00 -1.1700e+03 -2.8571e+01 -2.1881e+00 -1.1700e+03 0.0000e+00 -2.1952e+00 -1.1700e+03 2.8571e+01 -2.1881e+00 -1.1700e+03 5.7143e+01 -2.1669e+00 -1.1700e+03 8.5714e+01 -2.1317e+00 -1.1700e+03 1.1429e+02 -2.0829e+00 -1.1700e+03 1.4286e+02 -2.0209e+00 -1.1700e+03 1.7143e+02 -1.9461e+00 -1.1700e+03 2.0000e+02 -1.8592e+00 -1.1700e+03 2.2857e+02 -1.7609e+00 -1.1700e+03 2.5714e+02 -1.6518e+00 -1.1700e+03 2.8571e+02 -1.5329e+00 -1.1700e+03 3.1429e+02 -1.4048e+00 -1.1700e+03 3.4286e+02 -1.2686e+00 -1.1700e+03 3.7143e+02 -1.1250e+00 -1.1700e+03 4.0000e+02 -9.7493e-01 -1.1700e+03 4.2857e+02 -8.1927e-01 -1.1700e+03 4.5714e+02 -6.5883e-01 -1.1700e+03 4.8571e+02 -4.9444e-01 -1.1700e+03 5.1429e+02 -3.2688e-01 -1.1700e+03 5.4286e+02 -1.5687e-01 -1.1700e+03 5.7143e+02 1.4889e-02 -1.1700e+03 6.0000e+02 1.8776e-01 -1.1700e+03 6.2857e+02 3.6115e-01 -1.1700e+03 6.5714e+02 5.3451e-01 -1.1700e+03 6.8571e+02 7.0736e-01 -1.1700e+03 7.1429e+02 8.7926e-01 -1.1700e+03 7.4286e+02 1.0498e+00 -1.1700e+03 7.7143e+02 1.2187e+00 -1.1700e+03 8.0000e+02 1.3855e+00 -1.1700e+03 8.2857e+02 1.5501e+00 -1.1700e+03 8.5714e+02 1.7122e+00 -1.1700e+03 8.8571e+02 1.8716e+00 -1.1700e+03 9.1429e+02 2.0282e+00 -1.1700e+03 9.4286e+02 2.1819e+00 -1.1700e+03 9.7143e+02 2.3324e+00 -1.1700e+03 1.0000e+03 2.4799e+00 -1.1700e+03 1.0286e+03 2.6241e+00 -1.1700e+03 1.0571e+03 2.7651e+00 -1.1700e+03 1.0857e+03 2.9029e+00 -1.1700e+03 1.1143e+03 3.0374e+00 -1.1700e+03 1.1429e+03 3.1686e+00 -1.1700e+03 1.1714e+03 3.2967e+00 -1.1700e+03 1.2000e+03 3.4215e+00 -1.1700e+03 1.2286e+03 3.5432e+00 -1.1700e+03 1.2571e+03 3.6618e+00 -1.1700e+03 1.2857e+03 3.7773e+00 -1.1700e+03 1.3143e+03 3.8899e+00 -1.1700e+03 1.3429e+03 3.9994e+00 -1.1700e+03 1.3714e+03 4.1061e+00 -1.1700e+03 1.4000e+03 4.2100e+00 -1.1700e+03 1.4286e+03 4.3112e+00 -1.1700e+03 1.4571e+03 4.4096e+00 -1.1700e+03 1.4857e+03 4.5055e+00 -1.1700e+03 1.5143e+03 4.5988e+00 -1.1700e+03 1.5429e+03 4.6896e+00 -1.1700e+03 1.5714e+03 4.7780e+00 -1.1700e+03 1.6000e+03 4.8641e+00 -1.1700e+03 1.6286e+03 4.9479e+00 -1.1700e+03 1.6571e+03 5.0295e+00 -1.1700e+03 1.6857e+03 5.1090e+00 -1.1700e+03 1.7143e+03 5.1864e+00 -1.1700e+03 1.7429e+03 5.2618e+00 -1.1700e+03 1.7714e+03 5.3353e+00 -1.1700e+03 1.8000e+03 5.4068e+00 -1.1700e+03 1.8286e+03 5.4766e+00 -1.1700e+03 1.8571e+03 5.5445e+00 -1.1700e+03 1.8857e+03 5.6107e+00 -1.1700e+03 1.9143e+03 5.6753e+00 -1.1700e+03 1.9429e+03 5.7382e+00 -1.1700e+03 1.9714e+03 5.7996e+00 -1.1700e+03 2.0000e+03 5.8595e+00 -1.2000e+03 -2.0000e+03 5.8958e+00 -1.2000e+03 -1.9714e+03 5.8374e+00 -1.2000e+03 -1.9429e+03 5.7776e+00 -1.2000e+03 -1.9143e+03 5.7162e+00 -1.2000e+03 -1.8857e+03 5.6533e+00 -1.2000e+03 -1.8571e+03 5.5888e+00 -1.2000e+03 -1.8286e+03 5.5227e+00 -1.2000e+03 -1.8000e+03 5.4550e+00 -1.2000e+03 -1.7714e+03 5.3854e+00 -1.2000e+03 -1.7429e+03 5.3142e+00 -1.2000e+03 -1.7143e+03 5.2410e+00 -1.2000e+03 -1.6857e+03 5.1660e+00 -1.2000e+03 -1.6571e+03 5.0890e+00 -1.2000e+03 -1.6286e+03 5.0101e+00 -1.2000e+03 -1.6000e+03 4.9290e+00 -1.2000e+03 -1.5714e+03 4.8459e+00 -1.2000e+03 -1.5429e+03 4.7606e+00 -1.2000e+03 -1.5143e+03 4.6730e+00 -1.2000e+03 -1.4857e+03 4.5832e+00 -1.2000e+03 -1.4571e+03 4.4910e+00 -1.2000e+03 -1.4286e+03 4.3963e+00 -1.2000e+03 -1.4000e+03 4.2993e+00 -1.2000e+03 -1.3714e+03 4.1996e+00 -1.2000e+03 -1.3429e+03 4.0975e+00 -1.2000e+03 -1.3143e+03 3.9926e+00 -1.2000e+03 -1.2857e+03 3.8852e+00 -1.2000e+03 -1.2571e+03 3.7749e+00 -1.2000e+03 -1.2286e+03 3.6619e+00 -1.2000e+03 -1.2000e+03 3.5462e+00 -1.2000e+03 -1.1714e+03 3.4275e+00 -1.2000e+03 -1.1429e+03 3.3061e+00 -1.2000e+03 -1.1143e+03 3.1817e+00 -1.2000e+03 -1.0857e+03 3.0545e+00 -1.2000e+03 -1.0571e+03 2.9245e+00 -1.2000e+03 -1.0286e+03 2.7915e+00 -1.2000e+03 -1.0000e+03 2.6558e+00 -1.2000e+03 -9.7143e+02 2.5173e+00 -1.2000e+03 -9.4286e+02 2.3761e+00 -1.2000e+03 -9.1429e+02 2.2324e+00 -1.2000e+03 -8.8571e+02 2.0861e+00 -1.2000e+03 -8.5714e+02 1.9375e+00 -1.2000e+03 -8.2857e+02 1.7866e+00 -1.2000e+03 -8.0000e+02 1.6338e+00 -1.2000e+03 -7.7143e+02 1.4792e+00 -1.2000e+03 -7.4286e+02 1.3231e+00 -1.2000e+03 -7.1429e+02 1.1657e+00 -1.2000e+03 -6.8571e+02 1.0074e+00 -1.2000e+03 -6.5714e+02 8.4863e-01 -1.2000e+03 -6.2857e+02 6.8971e-01 -1.2000e+03 -6.0000e+02 5.3111e-01 -1.2000e+03 -5.7143e+02 3.7334e-01 -1.2000e+03 -5.4286e+02 2.1692e-01 -1.2000e+03 -5.1429e+02 6.2437e-02 -1.2000e+03 -4.8571e+02 -8.9501e-02 -1.2000e+03 -4.5714e+02 -2.3824e-01 -1.2000e+03 -4.2857e+02 -3.8310e-01 -1.2000e+03 -4.0000e+02 -5.2338e-01 -1.2000e+03 -3.7143e+02 -6.5834e-01 -1.2000e+03 -3.4286e+02 -7.8723e-01 -1.2000e+03 -3.1429e+02 -9.0932e-01 -1.2000e+03 -2.8571e+02 -1.0239e+00 -1.2000e+03 -2.5714e+02 -1.1301e+00 -1.2000e+03 -2.2857e+02 -1.2274e+00 -1.2000e+03 -2.0000e+02 -1.3149e+00 -1.2000e+03 -1.7143e+02 -1.3922e+00 -1.2000e+03 -1.4286e+02 -1.4587e+00 -1.2000e+03 -1.1429e+02 -1.5138e+00 -1.2000e+03 -8.5714e+01 -1.5571e+00 -1.2000e+03 -5.7143e+01 -1.5882e+00 -1.2000e+03 -2.8571e+01 -1.6070e+00 -1.2000e+03 0.0000e+00 -1.6133e+00 -1.2000e+03 2.8571e+01 -1.6070e+00 -1.2000e+03 5.7143e+01 -1.5882e+00 -1.2000e+03 8.5714e+01 -1.5571e+00 -1.2000e+03 1.1429e+02 -1.5138e+00 -1.2000e+03 1.4286e+02 -1.4587e+00 -1.2000e+03 1.7143e+02 -1.3922e+00 -1.2000e+03 2.0000e+02 -1.3149e+00 -1.2000e+03 2.2857e+02 -1.2274e+00 -1.2000e+03 2.5714e+02 -1.1301e+00 -1.2000e+03 2.8571e+02 -1.0239e+00 -1.2000e+03 3.1429e+02 -9.0932e-01 -1.2000e+03 3.4286e+02 -7.8723e-01 -1.2000e+03 3.7143e+02 -6.5834e-01 -1.2000e+03 4.0000e+02 -5.2338e-01 -1.2000e+03 4.2857e+02 -3.8310e-01 -1.2000e+03 4.5714e+02 -2.3824e-01 -1.2000e+03 4.8571e+02 -8.9501e-02 -1.2000e+03 5.1429e+02 6.2437e-02 -1.2000e+03 5.4286e+02 2.1692e-01 -1.2000e+03 5.7143e+02 3.7334e-01 -1.2000e+03 6.0000e+02 5.3111e-01 -1.2000e+03 6.2857e+02 6.8971e-01 -1.2000e+03 6.5714e+02 8.4863e-01 -1.2000e+03 6.8571e+02 1.0074e+00 -1.2000e+03 7.1429e+02 1.1657e+00 -1.2000e+03 7.4286e+02 1.3231e+00 -1.2000e+03 7.7143e+02 1.4792e+00 -1.2000e+03 8.0000e+02 1.6338e+00 -1.2000e+03 8.2857e+02 1.7866e+00 -1.2000e+03 8.5714e+02 1.9375e+00 -1.2000e+03 8.8571e+02 2.0861e+00 -1.2000e+03 9.1429e+02 2.2324e+00 -1.2000e+03 9.4286e+02 2.3761e+00 -1.2000e+03 9.7143e+02 2.5173e+00 -1.2000e+03 1.0000e+03 2.6558e+00 -1.2000e+03 1.0286e+03 2.7915e+00 -1.2000e+03 1.0571e+03 2.9245e+00 -1.2000e+03 1.0857e+03 3.0545e+00 -1.2000e+03 1.1143e+03 3.1817e+00 -1.2000e+03 1.1429e+03 3.3061e+00 -1.2000e+03 1.1714e+03 3.4275e+00 -1.2000e+03 1.2000e+03 3.5462e+00 -1.2000e+03 1.2286e+03 3.6619e+00 -1.2000e+03 1.2571e+03 3.7749e+00 -1.2000e+03 1.2857e+03 3.8852e+00 -1.2000e+03 1.3143e+03 3.9926e+00 -1.2000e+03 1.3429e+03 4.0975e+00 -1.2000e+03 1.3714e+03 4.1996e+00 -1.2000e+03 1.4000e+03 4.2993e+00 -1.2000e+03 1.4286e+03 4.3963e+00 -1.2000e+03 1.4571e+03 4.4910e+00 -1.2000e+03 1.4857e+03 4.5832e+00 -1.2000e+03 1.5143e+03 4.6730e+00 -1.2000e+03 1.5429e+03 4.7606e+00 -1.2000e+03 1.5714e+03 4.8459e+00 -1.2000e+03 1.6000e+03 4.9290e+00 -1.2000e+03 1.6286e+03 5.0101e+00 -1.2000e+03 1.6571e+03 5.0890e+00 -1.2000e+03 1.6857e+03 5.1660e+00 -1.2000e+03 1.7143e+03 5.2410e+00 -1.2000e+03 1.7429e+03 5.3142e+00 -1.2000e+03 1.7714e+03 5.3854e+00 -1.2000e+03 1.8000e+03 5.4550e+00 -1.2000e+03 1.8286e+03 5.5227e+00 -1.2000e+03 1.8571e+03 5.5888e+00 -1.2000e+03 1.8857e+03 5.6533e+00 -1.2000e+03 1.9143e+03 5.7162e+00 -1.2000e+03 1.9429e+03 5.7776e+00 -1.2000e+03 1.9714e+03 5.8374e+00 -1.2000e+03 2.0000e+03 5.8958e+00 -1.2300e+03 -2.0000e+03 5.9322e+00 -1.2300e+03 -1.9714e+03 5.8752e+00 -1.2300e+03 -1.9429e+03 5.8168e+00 -1.2300e+03 -1.9143e+03 5.7571e+00 -1.2300e+03 -1.8857e+03 5.6958e+00 -1.2300e+03 -1.8571e+03 5.6331e+00 -1.2300e+03 -1.8286e+03 5.5688e+00 -1.2300e+03 -1.8000e+03 5.5029e+00 -1.2300e+03 -1.7714e+03 5.4354e+00 -1.2300e+03 -1.7429e+03 5.3662e+00 -1.2300e+03 -1.7143e+03 5.2953e+00 -1.2300e+03 -1.6857e+03 5.2226e+00 -1.2300e+03 -1.6571e+03 5.1481e+00 -1.2300e+03 -1.6286e+03 5.0717e+00 -1.2300e+03 -1.6000e+03 4.9934e+00 -1.2300e+03 -1.5714e+03 4.9131e+00 -1.2300e+03 -1.5429e+03 4.8308e+00 -1.2300e+03 -1.5143e+03 4.7464e+00 -1.2300e+03 -1.4857e+03 4.6598e+00 -1.2300e+03 -1.4571e+03 4.5711e+00 -1.2300e+03 -1.4286e+03 4.4802e+00 -1.2300e+03 -1.4000e+03 4.3870e+00 -1.2300e+03 -1.3714e+03 4.2915e+00 -1.2300e+03 -1.3429e+03 4.1937e+00 -1.2300e+03 -1.3143e+03 4.0934e+00 -1.2300e+03 -1.2857e+03 3.9907e+00 -1.2300e+03 -1.2571e+03 3.8855e+00 -1.2300e+03 -1.2286e+03 3.7779e+00 -1.2300e+03 -1.2000e+03 3.6677e+00 -1.2300e+03 -1.1714e+03 3.5549e+00 -1.2300e+03 -1.1429e+03 3.4397e+00 -1.2300e+03 -1.1143e+03 3.3219e+00 -1.2300e+03 -1.0857e+03 3.2015e+00 -1.2300e+03 -1.0571e+03 3.0786e+00 -1.2300e+03 -1.0286e+03 2.9533e+00 -1.2300e+03 -1.0000e+03 2.8255e+00 -1.2300e+03 -9.7143e+02 2.6953e+00 -1.2300e+03 -9.4286e+02 2.5628e+00 -1.2300e+03 -9.1429e+02 2.4282e+00 -1.2300e+03 -8.8571e+02 2.2914e+00 -1.2300e+03 -8.5714e+02 2.1527e+00 -1.2300e+03 -8.2857e+02 2.0122e+00 -1.2300e+03 -8.0000e+02 1.8701e+00 -1.2300e+03 -7.7143e+02 1.7267e+00 -1.2300e+03 -7.4286e+02 1.5821e+00 -1.2300e+03 -7.1429e+02 1.4366e+00 -1.2300e+03 -6.8571e+02 1.2907e+00 -1.2300e+03 -6.5714e+02 1.1445e+00 -1.2300e+03 -6.2857e+02 9.9851e-01 -1.2300e+03 -6.0000e+02 8.5313e-01 -1.2300e+03 -5.7143e+02 7.0880e-01 -1.2300e+03 -5.4286e+02 5.6600e-01 -1.2300e+03 -5.1429e+02 4.2526e-01 -1.2300e+03 -4.8571e+02 2.8711e-01 -1.2300e+03 -4.5714e+02 1.5213e-01 -1.2300e+03 -4.2857e+02 2.0919e-02 -1.2300e+03 -4.0000e+02 -1.0590e-01 -1.2300e+03 -3.7143e+02 -2.2768e-01 -1.2300e+03 -3.4286e+02 -3.4380e-01 -1.2300e+03 -3.1429e+02 -4.5361e-01 -1.2300e+03 -2.8571e+02 -5.5646e-01 -1.2300e+03 -2.5714e+02 -6.5173e-01 -1.2300e+03 -2.2857e+02 -7.3882e-01 -1.2300e+03 -2.0000e+02 -8.1716e-01 -1.2300e+03 -1.7143e+02 -8.8622e-01 -1.2300e+03 -1.4286e+02 -9.4552e-01 -1.2300e+03 -1.1429e+02 -9.9465e-01 -1.2300e+03 -8.5714e+01 -1.0332e+00 -1.2300e+03 -5.7143e+01 -1.0610e+00 -1.2300e+03 -2.8571e+01 -1.0778e+00 -1.2300e+03 0.0000e+00 -1.0834e+00 -1.2300e+03 2.8571e+01 -1.0778e+00 -1.2300e+03 5.7143e+01 -1.0610e+00 -1.2300e+03 8.5714e+01 -1.0332e+00 -1.2300e+03 1.1429e+02 -9.9465e-01 -1.2300e+03 1.4286e+02 -9.4552e-01 -1.2300e+03 1.7143e+02 -8.8622e-01 -1.2300e+03 2.0000e+02 -8.1716e-01 -1.2300e+03 2.2857e+02 -7.3882e-01 -1.2300e+03 2.5714e+02 -6.5173e-01 -1.2300e+03 2.8571e+02 -5.5646e-01 -1.2300e+03 3.1429e+02 -4.5361e-01 -1.2300e+03 3.4286e+02 -3.4380e-01 -1.2300e+03 3.7143e+02 -2.2768e-01 -1.2300e+03 4.0000e+02 -1.0590e-01 -1.2300e+03 4.2857e+02 2.0919e-02 -1.2300e+03 4.5714e+02 1.5213e-01 -1.2300e+03 4.8571e+02 2.8711e-01 -1.2300e+03 5.1429e+02 4.2526e-01 -1.2300e+03 5.4286e+02 5.6600e-01 -1.2300e+03 5.7143e+02 7.0880e-01 -1.2300e+03 6.0000e+02 8.5313e-01 -1.2300e+03 6.2857e+02 9.9851e-01 -1.2300e+03 6.5714e+02 1.1445e+00 -1.2300e+03 6.8571e+02 1.2907e+00 -1.2300e+03 7.1429e+02 1.4366e+00 -1.2300e+03 7.4286e+02 1.5821e+00 -1.2300e+03 7.7143e+02 1.7267e+00 -1.2300e+03 8.0000e+02 1.8701e+00 -1.2300e+03 8.2857e+02 2.0122e+00 -1.2300e+03 8.5714e+02 2.1527e+00 -1.2300e+03 8.8571e+02 2.2914e+00 -1.2300e+03 9.1429e+02 2.4282e+00 -1.2300e+03 9.4286e+02 2.5628e+00 -1.2300e+03 9.7143e+02 2.6953e+00 -1.2300e+03 1.0000e+03 2.8255e+00 -1.2300e+03 1.0286e+03 2.9533e+00 -1.2300e+03 1.0571e+03 3.0786e+00 -1.2300e+03 1.0857e+03 3.2015e+00 -1.2300e+03 1.1143e+03 3.3219e+00 -1.2300e+03 1.1429e+03 3.4397e+00 -1.2300e+03 1.1714e+03 3.5549e+00 -1.2300e+03 1.2000e+03 3.6677e+00 -1.2300e+03 1.2286e+03 3.7779e+00 -1.2300e+03 1.2571e+03 3.8855e+00 -1.2300e+03 1.2857e+03 3.9907e+00 -1.2300e+03 1.3143e+03 4.0934e+00 -1.2300e+03 1.3429e+03 4.1937e+00 -1.2300e+03 1.3714e+03 4.2915e+00 -1.2300e+03 1.4000e+03 4.3870e+00 -1.2300e+03 1.4286e+03 4.4802e+00 -1.2300e+03 1.4571e+03 4.5711e+00 -1.2300e+03 1.4857e+03 4.6598e+00 -1.2300e+03 1.5143e+03 4.7464e+00 -1.2300e+03 1.5429e+03 4.8308e+00 -1.2300e+03 1.5714e+03 4.9131e+00 -1.2300e+03 1.6000e+03 4.9934e+00 -1.2300e+03 1.6286e+03 5.0717e+00 -1.2300e+03 1.6571e+03 5.1481e+00 -1.2300e+03 1.6857e+03 5.2226e+00 -1.2300e+03 1.7143e+03 5.2953e+00 -1.2300e+03 1.7429e+03 5.3662e+00 -1.2300e+03 1.7714e+03 5.4354e+00 -1.2300e+03 1.8000e+03 5.5029e+00 -1.2300e+03 1.8286e+03 5.5688e+00 -1.2300e+03 1.8571e+03 5.6331e+00 -1.2300e+03 1.8857e+03 5.6958e+00 -1.2300e+03 1.9143e+03 5.7571e+00 -1.2300e+03 1.9429e+03 5.8168e+00 -1.2300e+03 1.9714e+03 5.8752e+00 -1.2300e+03 2.0000e+03 5.9322e+00 -1.2600e+03 -2.0000e+03 5.9686e+00 -1.2600e+03 -1.9714e+03 5.9130e+00 -1.2600e+03 -1.9429e+03 5.8561e+00 -1.2600e+03 -1.9143e+03 5.7978e+00 -1.2600e+03 -1.8857e+03 5.7382e+00 -1.2600e+03 -1.8571e+03 5.6772e+00 -1.2600e+03 -1.8286e+03 5.6146e+00 -1.2600e+03 -1.8000e+03 5.5506e+00 -1.2600e+03 -1.7714e+03 5.4851e+00 -1.2600e+03 -1.7429e+03 5.4180e+00 -1.2600e+03 -1.7143e+03 5.3492e+00 -1.2600e+03 -1.6857e+03 5.2788e+00 -1.2600e+03 -1.6571e+03 5.2066e+00 -1.2600e+03 -1.6286e+03 5.1327e+00 -1.2600e+03 -1.6000e+03 5.0570e+00 -1.2600e+03 -1.5714e+03 4.9795e+00 -1.2600e+03 -1.5429e+03 4.9001e+00 -1.2600e+03 -1.5143e+03 4.8188e+00 -1.2600e+03 -1.4857e+03 4.7355e+00 -1.2600e+03 -1.4571e+03 4.6501e+00 -1.2600e+03 -1.4286e+03 4.5628e+00 -1.2600e+03 -1.4000e+03 4.4733e+00 -1.2600e+03 -1.3714e+03 4.3817e+00 -1.2600e+03 -1.3429e+03 4.2880e+00 -1.2600e+03 -1.3143e+03 4.1921e+00 -1.2600e+03 -1.2857e+03 4.0939e+00 -1.2600e+03 -1.2571e+03 3.9936e+00 -1.2600e+03 -1.2286e+03 3.8910e+00 -1.2600e+03 -1.2000e+03 3.7861e+00 -1.2600e+03 -1.1714e+03 3.6789e+00 -1.2600e+03 -1.1429e+03 3.5695e+00 -1.2600e+03 -1.1143e+03 3.4578e+00 -1.2600e+03 -1.0857e+03 3.3439e+00 -1.2600e+03 -1.0571e+03 3.2278e+00 -1.2600e+03 -1.0286e+03 3.1095e+00 -1.2600e+03 -1.0000e+03 2.9891e+00 -1.2600e+03 -9.7143e+02 2.8666e+00 -1.2600e+03 -9.4286e+02 2.7422e+00 -1.2600e+03 -9.1429e+02 2.6160e+00 -1.2600e+03 -8.8571e+02 2.4880e+00 -1.2600e+03 -8.5714e+02 2.3584e+00 -1.2600e+03 -8.2857e+02 2.2274e+00 -1.2600e+03 -8.0000e+02 2.0951e+00 -1.2600e+03 -7.7143e+02 1.9619e+00 -1.2600e+03 -7.4286e+02 1.8278e+00 -1.2600e+03 -7.1429e+02 1.6931e+00 -1.2600e+03 -6.8571e+02 1.5583e+00 -1.2600e+03 -6.5714e+02 1.4235e+00 -1.2600e+03 -6.2857e+02 1.2892e+00 -1.2600e+03 -6.0000e+02 1.1556e+00 -1.2600e+03 -5.7143e+02 1.0233e+00 -1.2600e+03 -5.4286e+02 8.9262e-01 -1.2600e+03 -5.1429e+02 7.6407e-01 -1.2600e+03 -4.8571e+02 6.3812e-01 -1.2600e+03 -4.5714e+02 5.1529e-01 -1.2600e+03 -4.2857e+02 3.9610e-01 -1.2600e+03 -4.0000e+02 2.8110e-01 -1.2600e+03 -3.7143e+02 1.7084e-01 -1.2600e+03 -3.4286e+02 6.5885e-02 -1.2600e+03 -3.1429e+02 -3.3215e-02 -1.2600e+03 -2.8571e+02 -1.2591e-01 -1.2600e+03 -2.5714e+02 -2.1165e-01 -1.2600e+03 -2.2857e+02 -2.8994e-01 -1.2600e+03 -2.0000e+02 -3.6029e-01 -1.2600e+03 -1.7143e+02 -4.2224e-01 -1.2600e+03 -1.4286e+02 -4.7540e-01 -1.2600e+03 -1.1429e+02 -5.1940e-01 -1.2600e+03 -8.5714e+01 -5.5394e-01 -1.2600e+03 -5.7143e+01 -5.7879e-01 -1.2600e+03 -2.8571e+01 -5.9377e-01 -1.2600e+03 0.0000e+00 -5.9877e-01 -1.2600e+03 2.8571e+01 -5.9377e-01 -1.2600e+03 5.7143e+01 -5.7879e-01 -1.2600e+03 8.5714e+01 -5.5394e-01 -1.2600e+03 1.1429e+02 -5.1940e-01 -1.2600e+03 1.4286e+02 -4.7540e-01 -1.2600e+03 1.7143e+02 -4.2224e-01 -1.2600e+03 2.0000e+02 -3.6029e-01 -1.2600e+03 2.2857e+02 -2.8994e-01 -1.2600e+03 2.5714e+02 -2.1165e-01 -1.2600e+03 2.8571e+02 -1.2591e-01 -1.2600e+03 3.1429e+02 -3.3215e-02 -1.2600e+03 3.4286e+02 6.5885e-02 -1.2600e+03 3.7143e+02 1.7084e-01 -1.2600e+03 4.0000e+02 2.8110e-01 -1.2600e+03 4.2857e+02 3.9610e-01 -1.2600e+03 4.5714e+02 5.1529e-01 -1.2600e+03 4.8571e+02 6.3812e-01 -1.2600e+03 5.1429e+02 7.6407e-01 -1.2600e+03 5.4286e+02 8.9262e-01 -1.2600e+03 5.7143e+02 1.0233e+00 -1.2600e+03 6.0000e+02 1.1556e+00 -1.2600e+03 6.2857e+02 1.2892e+00 -1.2600e+03 6.5714e+02 1.4235e+00 -1.2600e+03 6.8571e+02 1.5583e+00 -1.2600e+03 7.1429e+02 1.6931e+00 -1.2600e+03 7.4286e+02 1.8278e+00 -1.2600e+03 7.7143e+02 1.9619e+00 -1.2600e+03 8.0000e+02 2.0951e+00 -1.2600e+03 8.2857e+02 2.2274e+00 -1.2600e+03 8.5714e+02 2.3584e+00 -1.2600e+03 8.8571e+02 2.4880e+00 -1.2600e+03 9.1429e+02 2.6160e+00 -1.2600e+03 9.4286e+02 2.7422e+00 -1.2600e+03 9.7143e+02 2.8666e+00 -1.2600e+03 1.0000e+03 2.9891e+00 -1.2600e+03 1.0286e+03 3.1095e+00 -1.2600e+03 1.0571e+03 3.2278e+00 -1.2600e+03 1.0857e+03 3.3439e+00 -1.2600e+03 1.1143e+03 3.4578e+00 -1.2600e+03 1.1429e+03 3.5695e+00 -1.2600e+03 1.1714e+03 3.6789e+00 -1.2600e+03 1.2000e+03 3.7861e+00 -1.2600e+03 1.2286e+03 3.8910e+00 -1.2600e+03 1.2571e+03 3.9936e+00 -1.2600e+03 1.2857e+03 4.0939e+00 -1.2600e+03 1.3143e+03 4.1921e+00 -1.2600e+03 1.3429e+03 4.2880e+00 -1.2600e+03 1.3714e+03 4.3817e+00 -1.2600e+03 1.4000e+03 4.4733e+00 -1.2600e+03 1.4286e+03 4.5628e+00 -1.2600e+03 1.4571e+03 4.6501e+00 -1.2600e+03 1.4857e+03 4.7355e+00 -1.2600e+03 1.5143e+03 4.8188e+00 -1.2600e+03 1.5429e+03 4.9001e+00 -1.2600e+03 1.5714e+03 4.9795e+00 -1.2600e+03 1.6000e+03 5.0570e+00 -1.2600e+03 1.6286e+03 5.1327e+00 -1.2600e+03 1.6571e+03 5.2066e+00 -1.2600e+03 1.6857e+03 5.2788e+00 -1.2600e+03 1.7143e+03 5.3492e+00 -1.2600e+03 1.7429e+03 5.4180e+00 -1.2600e+03 1.7714e+03 5.4851e+00 -1.2600e+03 1.8000e+03 5.5506e+00 -1.2600e+03 1.8286e+03 5.6146e+00 -1.2600e+03 1.8571e+03 5.6772e+00 -1.2600e+03 1.8857e+03 5.7382e+00 -1.2600e+03 1.9143e+03 5.7978e+00 -1.2600e+03 1.9429e+03 5.8561e+00 -1.2600e+03 1.9714e+03 5.9130e+00 -1.2600e+03 2.0000e+03 5.9686e+00 -1.2900e+03 -2.0000e+03 6.0049e+00 -1.2900e+03 -1.9714e+03 5.9507e+00 -1.2900e+03 -1.9429e+03 5.8952e+00 -1.2900e+03 -1.9143e+03 5.8385e+00 -1.2900e+03 -1.8857e+03 5.7805e+00 -1.2900e+03 -1.8571e+03 5.7211e+00 -1.2900e+03 -1.8286e+03 5.6603e+00 -1.2900e+03 -1.8000e+03 5.5981e+00 -1.2900e+03 -1.7714e+03 5.5345e+00 -1.2900e+03 -1.7429e+03 5.4693e+00 -1.2900e+03 -1.7143e+03 5.4027e+00 -1.2900e+03 -1.6857e+03 5.3345e+00 -1.2900e+03 -1.6571e+03 5.2646e+00 -1.2900e+03 -1.6286e+03 5.1932e+00 -1.2900e+03 -1.6000e+03 5.1200e+00 -1.2900e+03 -1.5714e+03 5.0452e+00 -1.2900e+03 -1.5429e+03 4.9686e+00 -1.2900e+03 -1.5143e+03 4.8902e+00 -1.2900e+03 -1.4857e+03 4.8100e+00 -1.2900e+03 -1.4571e+03 4.7279e+00 -1.2900e+03 -1.4286e+03 4.6440e+00 -1.2900e+03 -1.4000e+03 4.5581e+00 -1.2900e+03 -1.3714e+03 4.4703e+00 -1.2900e+03 -1.3429e+03 4.3805e+00 -1.2900e+03 -1.3143e+03 4.2887e+00 -1.2900e+03 -1.2857e+03 4.1949e+00 -1.2900e+03 -1.2571e+03 4.0991e+00 -1.2900e+03 -1.2286e+03 4.0013e+00 -1.2900e+03 -1.2000e+03 3.9014e+00 -1.2900e+03 -1.1714e+03 3.7996e+00 -1.2900e+03 -1.1429e+03 3.6957e+00 -1.2900e+03 -1.1143e+03 3.5898e+00 -1.2900e+03 -1.0857e+03 3.4819e+00 -1.2900e+03 -1.0571e+03 3.3721e+00 -1.2900e+03 -1.0286e+03 3.2604e+00 -1.2900e+03 -1.0000e+03 3.1469e+00 -1.2900e+03 -9.7143e+02 3.0316e+00 -1.2900e+03 -9.4286e+02 2.9147e+00 -1.2900e+03 -9.1429e+02 2.7962e+00 -1.2900e+03 -8.8571e+02 2.6764e+00 -1.2900e+03 -8.5714e+02 2.5552e+00 -1.2900e+03 -8.2857e+02 2.4329e+00 -1.2900e+03 -8.0000e+02 2.3096e+00 -1.2900e+03 -7.7143e+02 2.1856e+00 -1.2900e+03 -7.4286e+02 2.0611e+00 -1.2900e+03 -7.1429e+02 1.9363e+00 -1.2900e+03 -6.8571e+02 1.8115e+00 -1.2900e+03 -6.5714e+02 1.6870e+00 -1.2900e+03 -6.2857e+02 1.5631e+00 -1.2900e+03 -6.0000e+02 1.4402e+00 -1.2900e+03 -5.7143e+02 1.3186e+00 -1.2900e+03 -5.4286e+02 1.1988e+00 -1.2900e+03 -5.1429e+02 1.0811e+00 -1.2900e+03 -4.8571e+02 9.6596e-01 -1.2900e+03 -4.5714e+02 8.5389e-01 -1.2900e+03 -4.2857e+02 7.4532e-01 -1.2900e+03 -4.0000e+02 6.4073e-01 -1.2900e+03 -3.7143e+02 5.4061e-01 -1.2900e+03 -3.4286e+02 4.4544e-01 -1.2900e+03 -3.1429e+02 3.5570e-01 -1.2900e+03 -2.8571e+02 2.7188e-01 -1.2900e+03 -2.5714e+02 1.9443e-01 -1.2900e+03 -2.2857e+02 1.2380e-01 -1.2900e+03 -2.0000e+02 6.0403e-02 -1.2900e+03 -1.7143e+02 4.6172e-03 -1.2900e+03 -1.4286e+02 -4.3208e-02 -1.2900e+03 -1.1429e+02 -8.2769e-02 -1.2900e+03 -8.5714e+01 -1.1381e-01 -1.2900e+03 -5.7143e+01 -1.3613e-01 -1.2900e+03 -2.8571e+01 -1.4959e-01 -1.2900e+03 0.0000e+00 -1.5408e-01 -1.2900e+03 2.8571e+01 -1.4959e-01 -1.2900e+03 5.7143e+01 -1.3613e-01 -1.2900e+03 8.5714e+01 -1.1381e-01 -1.2900e+03 1.1429e+02 -8.2769e-02 -1.2900e+03 1.4286e+02 -4.3208e-02 -1.2900e+03 1.7143e+02 4.6172e-03 -1.2900e+03 2.0000e+02 6.0403e-02 -1.2900e+03 2.2857e+02 1.2380e-01 -1.2900e+03 2.5714e+02 1.9443e-01 -1.2900e+03 2.8571e+02 2.7188e-01 -1.2900e+03 3.1429e+02 3.5570e-01 -1.2900e+03 3.4286e+02 4.4544e-01 -1.2900e+03 3.7143e+02 5.4061e-01 -1.2900e+03 4.0000e+02 6.4073e-01 -1.2900e+03 4.2857e+02 7.4532e-01 -1.2900e+03 4.5714e+02 8.5389e-01 -1.2900e+03 4.8571e+02 9.6596e-01 -1.2900e+03 5.1429e+02 1.0811e+00 -1.2900e+03 5.4286e+02 1.1988e+00 -1.2900e+03 5.7143e+02 1.3186e+00 -1.2900e+03 6.0000e+02 1.4402e+00 -1.2900e+03 6.2857e+02 1.5631e+00 -1.2900e+03 6.5714e+02 1.6870e+00 -1.2900e+03 6.8571e+02 1.8115e+00 -1.2900e+03 7.1429e+02 1.9363e+00 -1.2900e+03 7.4286e+02 2.0611e+00 -1.2900e+03 7.7143e+02 2.1856e+00 -1.2900e+03 8.0000e+02 2.3096e+00 -1.2900e+03 8.2857e+02 2.4329e+00 -1.2900e+03 8.5714e+02 2.5552e+00 -1.2900e+03 8.8571e+02 2.6764e+00 -1.2900e+03 9.1429e+02 2.7962e+00 -1.2900e+03 9.4286e+02 2.9147e+00 -1.2900e+03 9.7143e+02 3.0316e+00 -1.2900e+03 1.0000e+03 3.1469e+00 -1.2900e+03 1.0286e+03 3.2604e+00 -1.2900e+03 1.0571e+03 3.3721e+00 -1.2900e+03 1.0857e+03 3.4819e+00 -1.2900e+03 1.1143e+03 3.5898e+00 -1.2900e+03 1.1429e+03 3.6957e+00 -1.2900e+03 1.1714e+03 3.7996e+00 -1.2900e+03 1.2000e+03 3.9014e+00 -1.2900e+03 1.2286e+03 4.0013e+00 -1.2900e+03 1.2571e+03 4.0991e+00 -1.2900e+03 1.2857e+03 4.1949e+00 -1.2900e+03 1.3143e+03 4.2887e+00 -1.2900e+03 1.3429e+03 4.3805e+00 -1.2900e+03 1.3714e+03 4.4703e+00 -1.2900e+03 1.4000e+03 4.5581e+00 -1.2900e+03 1.4286e+03 4.6440e+00 -1.2900e+03 1.4571e+03 4.7279e+00 -1.2900e+03 1.4857e+03 4.8100e+00 -1.2900e+03 1.5143e+03 4.8902e+00 -1.2900e+03 1.5429e+03 4.9686e+00 -1.2900e+03 1.5714e+03 5.0452e+00 -1.2900e+03 1.6000e+03 5.1200e+00 -1.2900e+03 1.6286e+03 5.1932e+00 -1.2900e+03 1.6571e+03 5.2646e+00 -1.2900e+03 1.6857e+03 5.3345e+00 -1.2900e+03 1.7143e+03 5.4027e+00 -1.2900e+03 1.7429e+03 5.4693e+00 -1.2900e+03 1.7714e+03 5.5345e+00 -1.2900e+03 1.8000e+03 5.5981e+00 -1.2900e+03 1.8286e+03 5.6603e+00 -1.2900e+03 1.8571e+03 5.7211e+00 -1.2900e+03 1.8857e+03 5.7805e+00 -1.2900e+03 1.9143e+03 5.8385e+00 -1.2900e+03 1.9429e+03 5.8952e+00 -1.2900e+03 1.9714e+03 5.9507e+00 -1.2900e+03 2.0000e+03 6.0049e+00 -1.3200e+03 -2.0000e+03 6.0412e+00 -1.3200e+03 -1.9714e+03 5.9883e+00 -1.3200e+03 -1.9429e+03 5.9343e+00 -1.3200e+03 -1.9143e+03 5.8790e+00 -1.3200e+03 -1.8857e+03 5.8225e+00 -1.3200e+03 -1.8571e+03 5.7648e+00 -1.3200e+03 -1.8286e+03 5.7057e+00 -1.3200e+03 -1.8000e+03 5.6453e+00 -1.3200e+03 -1.7714e+03 5.5835e+00 -1.3200e+03 -1.7429e+03 5.5204e+00 -1.3200e+03 -1.7143e+03 5.4557e+00 -1.3200e+03 -1.6857e+03 5.3897e+00 -1.3200e+03 -1.6571e+03 5.3221e+00 -1.3200e+03 -1.6286e+03 5.2530e+00 -1.3200e+03 -1.6000e+03 5.1823e+00 -1.3200e+03 -1.5714e+03 5.1100e+00 -1.3200e+03 -1.5429e+03 5.0362e+00 -1.3200e+03 -1.5143e+03 4.9606e+00 -1.3200e+03 -1.4857e+03 4.8834e+00 -1.3200e+03 -1.4571e+03 4.8044e+00 -1.3200e+03 -1.4286e+03 4.7238e+00 -1.3200e+03 -1.4000e+03 4.6413e+00 -1.3200e+03 -1.3714e+03 4.5571e+00 -1.3200e+03 -1.3429e+03 4.4711e+00 -1.3200e+03 -1.3143e+03 4.3833e+00 -1.3200e+03 -1.2857e+03 4.2937e+00 -1.3200e+03 -1.2571e+03 4.2022e+00 -1.3200e+03 -1.2286e+03 4.1089e+00 -1.3200e+03 -1.2000e+03 4.0138e+00 -1.3200e+03 -1.1714e+03 3.9169e+00 -1.3200e+03 -1.1429e+03 3.8182e+00 -1.3200e+03 -1.1143e+03 3.7177e+00 -1.3200e+03 -1.0857e+03 3.6155e+00 -1.3200e+03 -1.0571e+03 3.5117e+00 -1.3200e+03 -1.0286e+03 3.4061e+00 -1.3200e+03 -1.0000e+03 3.2991e+00 -1.3200e+03 -9.7143e+02 3.1905e+00 -1.3200e+03 -9.4286e+02 3.0805e+00 -1.3200e+03 -9.1429e+02 2.9693e+00 -1.3200e+03 -8.8571e+02 2.8569e+00 -1.3200e+03 -8.5714e+02 2.7434e+00 -1.3200e+03 -8.2857e+02 2.6291e+00 -1.3200e+03 -8.0000e+02 2.5141e+00 -1.3200e+03 -7.7143e+02 2.3986e+00 -1.3200e+03 -7.4286e+02 2.2828e+00 -1.3200e+03 -7.1429e+02 2.1669e+00 -1.3200e+03 -6.8571e+02 2.0513e+00 -1.3200e+03 -6.5714e+02 1.9361e+00 -1.3200e+03 -6.2857e+02 1.8216e+00 -1.3200e+03 -6.0000e+02 1.7083e+00 -1.3200e+03 -5.7143e+02 1.5964e+00 -1.3200e+03 -5.4286e+02 1.4862e+00 -1.3200e+03 -5.1429e+02 1.3782e+00 -1.3200e+03 -4.8571e+02 1.2728e+00 -1.3200e+03 -4.5714e+02 1.1702e+00 -1.3200e+03 -4.2857e+02 1.0711e+00 -1.3200e+03 -4.0000e+02 9.7570e-01 -1.3200e+03 -3.7143e+02 8.8452e-01 -1.3200e+03 -3.4286e+02 7.9797e-01 -1.3200e+03 -3.1429e+02 7.1647e-01 -1.3200e+03 -2.8571e+02 6.4043e-01 -1.3200e+03 -2.5714e+02 5.7025e-01 -1.3200e+03 -2.2857e+02 5.0631e-01 -1.3200e+03 -2.0000e+02 4.4897e-01 -1.3200e+03 -1.7143e+02 3.9856e-01 -1.3200e+03 -1.4286e+02 3.5538e-01 -1.3200e+03 -1.1429e+02 3.1968e-01 -1.3200e+03 -8.5714e+01 2.9168e-01 -1.3200e+03 -5.7143e+01 2.7155e-01 -1.3200e+03 -2.8571e+01 2.5942e-01 -1.3200e+03 0.0000e+00 2.5537e-01 -1.3200e+03 2.8571e+01 2.5942e-01 -1.3200e+03 5.7143e+01 2.7155e-01 -1.3200e+03 8.5714e+01 2.9168e-01 -1.3200e+03 1.1429e+02 3.1968e-01 -1.3200e+03 1.4286e+02 3.5538e-01 -1.3200e+03 1.7143e+02 3.9856e-01 -1.3200e+03 2.0000e+02 4.4897e-01 -1.3200e+03 2.2857e+02 5.0631e-01 -1.3200e+03 2.5714e+02 5.7025e-01 -1.3200e+03 2.8571e+02 6.4043e-01 -1.3200e+03 3.1429e+02 7.1647e-01 -1.3200e+03 3.4286e+02 7.9797e-01 -1.3200e+03 3.7143e+02 8.8452e-01 -1.3200e+03 4.0000e+02 9.7570e-01 -1.3200e+03 4.2857e+02 1.0711e+00 -1.3200e+03 4.5714e+02 1.1702e+00 -1.3200e+03 4.8571e+02 1.2728e+00 -1.3200e+03 5.1429e+02 1.3782e+00 -1.3200e+03 5.4286e+02 1.4862e+00 -1.3200e+03 5.7143e+02 1.5964e+00 -1.3200e+03 6.0000e+02 1.7083e+00 -1.3200e+03 6.2857e+02 1.8216e+00 -1.3200e+03 6.5714e+02 1.9361e+00 -1.3200e+03 6.8571e+02 2.0513e+00 -1.3200e+03 7.1429e+02 2.1669e+00 -1.3200e+03 7.4286e+02 2.2828e+00 -1.3200e+03 7.7143e+02 2.3986e+00 -1.3200e+03 8.0000e+02 2.5141e+00 -1.3200e+03 8.2857e+02 2.6291e+00 -1.3200e+03 8.5714e+02 2.7434e+00 -1.3200e+03 8.8571e+02 2.8569e+00 -1.3200e+03 9.1429e+02 2.9693e+00 -1.3200e+03 9.4286e+02 3.0805e+00 -1.3200e+03 9.7143e+02 3.1905e+00 -1.3200e+03 1.0000e+03 3.2991e+00 -1.3200e+03 1.0286e+03 3.4061e+00 -1.3200e+03 1.0571e+03 3.5117e+00 -1.3200e+03 1.0857e+03 3.6155e+00 -1.3200e+03 1.1143e+03 3.7177e+00 -1.3200e+03 1.1429e+03 3.8182e+00 -1.3200e+03 1.1714e+03 3.9169e+00 -1.3200e+03 1.2000e+03 4.0138e+00 -1.3200e+03 1.2286e+03 4.1089e+00 -1.3200e+03 1.2571e+03 4.2022e+00 -1.3200e+03 1.2857e+03 4.2937e+00 -1.3200e+03 1.3143e+03 4.3833e+00 -1.3200e+03 1.3429e+03 4.4711e+00 -1.3200e+03 1.3714e+03 4.5571e+00 -1.3200e+03 1.4000e+03 4.6413e+00 -1.3200e+03 1.4286e+03 4.7238e+00 -1.3200e+03 1.4571e+03 4.8044e+00 -1.3200e+03 1.4857e+03 4.8834e+00 -1.3200e+03 1.5143e+03 4.9606e+00 -1.3200e+03 1.5429e+03 5.0362e+00 -1.3200e+03 1.5714e+03 5.1100e+00 -1.3200e+03 1.6000e+03 5.1823e+00 -1.3200e+03 1.6286e+03 5.2530e+00 -1.3200e+03 1.6571e+03 5.3221e+00 -1.3200e+03 1.6857e+03 5.3897e+00 -1.3200e+03 1.7143e+03 5.4557e+00 -1.3200e+03 1.7429e+03 5.5204e+00 -1.3200e+03 1.7714e+03 5.5835e+00 -1.3200e+03 1.8000e+03 5.6453e+00 -1.3200e+03 1.8286e+03 5.7057e+00 -1.3200e+03 1.8571e+03 5.7648e+00 -1.3200e+03 1.8857e+03 5.8225e+00 -1.3200e+03 1.9143e+03 5.8790e+00 -1.3200e+03 1.9429e+03 5.9343e+00 -1.3200e+03 1.9714e+03 5.9883e+00 -1.3200e+03 2.0000e+03 6.0412e+00 -1.3500e+03 -2.0000e+03 6.0773e+00 -1.3500e+03 -1.9714e+03 6.0258e+00 -1.3500e+03 -1.9429e+03 5.9732e+00 -1.3500e+03 -1.9143e+03 5.9194e+00 -1.3500e+03 -1.8857e+03 5.8644e+00 -1.3500e+03 -1.8571e+03 5.8082e+00 -1.3500e+03 -1.8286e+03 5.7508e+00 -1.3500e+03 -1.8000e+03 5.6922e+00 -1.3500e+03 -1.7714e+03 5.6322e+00 -1.3500e+03 -1.7429e+03 5.5709e+00 -1.3500e+03 -1.7143e+03 5.5083e+00 -1.3500e+03 -1.6857e+03 5.4443e+00 -1.3500e+03 -1.6571e+03 5.3789e+00 -1.3500e+03 -1.6286e+03 5.3121e+00 -1.3500e+03 -1.6000e+03 5.2438e+00 -1.3500e+03 -1.5714e+03 5.1741e+00 -1.3500e+03 -1.5429e+03 5.1028e+00 -1.3500e+03 -1.5143e+03 5.0300e+00 -1.3500e+03 -1.4857e+03 4.9556e+00 -1.3500e+03 -1.4571e+03 4.8797e+00 -1.3500e+03 -1.4286e+03 4.8022e+00 -1.3500e+03 -1.4000e+03 4.7231e+00 -1.3500e+03 -1.3714e+03 4.6423e+00 -1.3500e+03 -1.3429e+03 4.5599e+00 -1.3500e+03 -1.3143e+03 4.4758e+00 -1.3500e+03 -1.2857e+03 4.3902e+00 -1.3500e+03 -1.2571e+03 4.3028e+00 -1.3500e+03 -1.2286e+03 4.2139e+00 -1.3500e+03 -1.2000e+03 4.1233e+00 -1.3500e+03 -1.1714e+03 4.0310e+00 -1.3500e+03 -1.1429e+03 3.9372e+00 -1.3500e+03 -1.1143e+03 3.8419e+00 -1.3500e+03 -1.0857e+03 3.7450e+00 -1.3500e+03 -1.0571e+03 3.6467e+00 -1.3500e+03 -1.0286e+03 3.5469e+00 -1.3500e+03 -1.0000e+03 3.4459e+00 -1.3500e+03 -9.7143e+02 3.3435e+00 -1.3500e+03 -9.4286e+02 3.2400e+00 -1.3500e+03 -9.1429e+02 3.1355e+00 -1.3500e+03 -8.8571e+02 3.0300e+00 -1.3500e+03 -8.5714e+02 2.9237e+00 -1.3500e+03 -8.2857e+02 2.8167e+00 -1.3500e+03 -8.0000e+02 2.7093e+00 -1.3500e+03 -7.7143e+02 2.6016e+00 -1.3500e+03 -7.4286e+02 2.4937e+00 -1.3500e+03 -7.1429e+02 2.3860e+00 -1.3500e+03 -6.8571e+02 2.2786e+00 -1.3500e+03 -6.5714e+02 2.1718e+00 -1.3500e+03 -6.2857e+02 2.0660e+00 -1.3500e+03 -6.0000e+02 1.9612e+00 -1.3500e+03 -5.7143e+02 1.8580e+00 -1.3500e+03 -5.4286e+02 1.7566e+00 -1.3500e+03 -5.1429e+02 1.6572e+00 -1.3500e+03 -4.8571e+02 1.5604e+00 -1.3500e+03 -4.5714e+02 1.4664e+00 -1.3500e+03 -4.2857e+02 1.3756e+00 -1.3500e+03 -4.0000e+02 1.2884e+00 -1.3500e+03 -3.7143e+02 1.2051e+00 -1.3500e+03 -3.4286e+02 1.1262e+00 -1.3500e+03 -3.1429e+02 1.0520e+00 -1.3500e+03 -2.8571e+02 9.8277e-01 -1.3500e+03 -2.5714e+02 9.1898e-01 -1.3500e+03 -2.2857e+02 8.6092e-01 -1.3500e+03 -2.0000e+02 8.0890e-01 -1.3500e+03 -1.7143e+02 7.6319e-01 -1.3500e+03 -1.4286e+02 7.2407e-01 -1.3500e+03 -1.1429e+02 6.9174e-01 -1.3500e+03 -8.5714e+01 6.6640e-01 -1.3500e+03 -5.7143e+01 6.4819e-01 -1.3500e+03 -2.8571e+01 6.3722e-01 -1.3500e+03 0.0000e+00 6.3355e-01 -1.3500e+03 2.8571e+01 6.3722e-01 -1.3500e+03 5.7143e+01 6.4819e-01 -1.3500e+03 8.5714e+01 6.6640e-01 -1.3500e+03 1.1429e+02 6.9174e-01 -1.3500e+03 1.4286e+02 7.2407e-01 -1.3500e+03 1.7143e+02 7.6319e-01 -1.3500e+03 2.0000e+02 8.0890e-01 -1.3500e+03 2.2857e+02 8.6092e-01 -1.3500e+03 2.5714e+02 9.1898e-01 -1.3500e+03 2.8571e+02 9.8277e-01 -1.3500e+03 3.1429e+02 1.0520e+00 -1.3500e+03 3.4286e+02 1.1262e+00 -1.3500e+03 3.7143e+02 1.2051e+00 -1.3500e+03 4.0000e+02 1.2884e+00 -1.3500e+03 4.2857e+02 1.3756e+00 -1.3500e+03 4.5714e+02 1.4664e+00 -1.3500e+03 4.8571e+02 1.5604e+00 -1.3500e+03 5.1429e+02 1.6572e+00 -1.3500e+03 5.4286e+02 1.7566e+00 -1.3500e+03 5.7143e+02 1.8580e+00 -1.3500e+03 6.0000e+02 1.9612e+00 -1.3500e+03 6.2857e+02 2.0660e+00 -1.3500e+03 6.5714e+02 2.1718e+00 -1.3500e+03 6.8571e+02 2.2786e+00 -1.3500e+03 7.1429e+02 2.3860e+00 -1.3500e+03 7.4286e+02 2.4937e+00 -1.3500e+03 7.7143e+02 2.6016e+00 -1.3500e+03 8.0000e+02 2.7093e+00 -1.3500e+03 8.2857e+02 2.8167e+00 -1.3500e+03 8.5714e+02 2.9237e+00 -1.3500e+03 8.8571e+02 3.0300e+00 -1.3500e+03 9.1429e+02 3.1355e+00 -1.3500e+03 9.4286e+02 3.2400e+00 -1.3500e+03 9.7143e+02 3.3435e+00 -1.3500e+03 1.0000e+03 3.4459e+00 -1.3500e+03 1.0286e+03 3.5469e+00 -1.3500e+03 1.0571e+03 3.6467e+00 -1.3500e+03 1.0857e+03 3.7450e+00 -1.3500e+03 1.1143e+03 3.8419e+00 -1.3500e+03 1.1429e+03 3.9372e+00 -1.3500e+03 1.1714e+03 4.0310e+00 -1.3500e+03 1.2000e+03 4.1233e+00 -1.3500e+03 1.2286e+03 4.2139e+00 -1.3500e+03 1.2571e+03 4.3028e+00 -1.3500e+03 1.2857e+03 4.3902e+00 -1.3500e+03 1.3143e+03 4.4758e+00 -1.3500e+03 1.3429e+03 4.5599e+00 -1.3500e+03 1.3714e+03 4.6423e+00 -1.3500e+03 1.4000e+03 4.7231e+00 -1.3500e+03 1.4286e+03 4.8022e+00 -1.3500e+03 1.4571e+03 4.8797e+00 -1.3500e+03 1.4857e+03 4.9556e+00 -1.3500e+03 1.5143e+03 5.0300e+00 -1.3500e+03 1.5429e+03 5.1028e+00 -1.3500e+03 1.5714e+03 5.1741e+00 -1.3500e+03 1.6000e+03 5.2438e+00 -1.3500e+03 1.6286e+03 5.3121e+00 -1.3500e+03 1.6571e+03 5.3789e+00 -1.3500e+03 1.6857e+03 5.4443e+00 -1.3500e+03 1.7143e+03 5.5083e+00 -1.3500e+03 1.7429e+03 5.5709e+00 -1.3500e+03 1.7714e+03 5.6322e+00 -1.3500e+03 1.8000e+03 5.6922e+00 -1.3500e+03 1.8286e+03 5.7508e+00 -1.3500e+03 1.8571e+03 5.8082e+00 -1.3500e+03 1.8857e+03 5.8644e+00 -1.3500e+03 1.9143e+03 5.9194e+00 -1.3500e+03 1.9429e+03 5.9732e+00 -1.3500e+03 1.9714e+03 6.0258e+00 -1.3500e+03 2.0000e+03 6.0773e+00 -1.3800e+03 -2.0000e+03 6.1134e+00 -1.3800e+03 -1.9714e+03 6.0632e+00 -1.3800e+03 -1.9429e+03 6.0119e+00 -1.3800e+03 -1.9143e+03 5.9595e+00 -1.3800e+03 -1.8857e+03 5.9061e+00 -1.3800e+03 -1.8571e+03 5.8514e+00 -1.3800e+03 -1.8286e+03 5.7957e+00 -1.3800e+03 -1.8000e+03 5.7387e+00 -1.3800e+03 -1.7714e+03 5.6805e+00 -1.3800e+03 -1.7429e+03 5.6211e+00 -1.3800e+03 -1.7143e+03 5.5604e+00 -1.3800e+03 -1.6857e+03 5.4984e+00 -1.3800e+03 -1.6571e+03 5.4352e+00 -1.3800e+03 -1.6286e+03 5.3706e+00 -1.3800e+03 -1.6000e+03 5.3046e+00 -1.3800e+03 -1.5714e+03 5.2373e+00 -1.3800e+03 -1.5429e+03 5.1685e+00 -1.3800e+03 -1.5143e+03 5.0984e+00 -1.3800e+03 -1.4857e+03 5.0268e+00 -1.3800e+03 -1.4571e+03 4.9537e+00 -1.3800e+03 -1.4286e+03 4.8792e+00 -1.3800e+03 -1.4000e+03 4.8032e+00 -1.3800e+03 -1.3714e+03 4.7258e+00 -1.3800e+03 -1.3429e+03 4.6468e+00 -1.3800e+03 -1.3143e+03 4.5664e+00 -1.3800e+03 -1.2857e+03 4.4844e+00 -1.3800e+03 -1.2571e+03 4.4010e+00 -1.3800e+03 -1.2286e+03 4.3161e+00 -1.3800e+03 -1.2000e+03 4.2298e+00 -1.3800e+03 -1.1714e+03 4.1420e+00 -1.3800e+03 -1.1429e+03 4.0529e+00 -1.3800e+03 -1.1143e+03 3.9623e+00 -1.3800e+03 -1.0857e+03 3.8705e+00 -1.3800e+03 -1.0571e+03 3.7773e+00 -1.3800e+03 -1.0286e+03 3.6830e+00 -1.3800e+03 -1.0000e+03 3.5875e+00 -1.3800e+03 -9.7143e+02 3.4910e+00 -1.3800e+03 -9.4286e+02 3.3935e+00 -1.3800e+03 -9.1429e+02 3.2951e+00 -1.3800e+03 -8.8571e+02 3.1960e+00 -1.3800e+03 -8.5714e+02 3.0963e+00 -1.3800e+03 -8.2857e+02 2.9962e+00 -1.3800e+03 -8.0000e+02 2.8957e+00 -1.3800e+03 -7.7143e+02 2.7951e+00 -1.3800e+03 -7.4286e+02 2.6945e+00 -1.3800e+03 -7.1429e+02 2.5942e+00 -1.3800e+03 -6.8571e+02 2.4944e+00 -1.3800e+03 -6.5714e+02 2.3953e+00 -1.3800e+03 -6.2857e+02 2.2971e+00 -1.3800e+03 -6.0000e+02 2.2002e+00 -1.3800e+03 -5.7143e+02 2.1048e+00 -1.3800e+03 -5.4286e+02 2.0112e+00 -1.3800e+03 -5.1429e+02 1.9197e+00 -1.3800e+03 -4.8571e+02 1.8306e+00 -1.3800e+03 -4.5714e+02 1.7442e+00 -1.3800e+03 -4.2857e+02 1.6609e+00 -1.3800e+03 -4.0000e+02 1.5809e+00 -1.3800e+03 -3.7143e+02 1.5047e+00 -1.3800e+03 -3.4286e+02 1.4325e+00 -1.3800e+03 -3.1429e+02 1.3647e+00 -1.3800e+03 -2.8571e+02 1.3016e+00 -1.3800e+03 -2.5714e+02 1.2434e+00 -1.3800e+03 -2.2857e+02 1.1905e+00 -1.3800e+03 -2.0000e+02 1.1432e+00 -1.3800e+03 -1.7143e+02 1.1016e+00 -1.3800e+03 -1.4286e+02 1.0661e+00 -1.3800e+03 -1.1429e+02 1.0367e+00 -1.3800e+03 -8.5714e+01 1.0137e+00 -1.3800e+03 -5.7143e+01 9.9714e-01 -1.3800e+03 -2.8571e+01 9.8719e-01 -1.3800e+03 0.0000e+00 9.8386e-01 -1.3800e+03 2.8571e+01 9.8719e-01 -1.3800e+03 5.7143e+01 9.9714e-01 -1.3800e+03 8.5714e+01 1.0137e+00 -1.3800e+03 1.1429e+02 1.0367e+00 -1.3800e+03 1.4286e+02 1.0661e+00 -1.3800e+03 1.7143e+02 1.1016e+00 -1.3800e+03 2.0000e+02 1.1432e+00 -1.3800e+03 2.2857e+02 1.1905e+00 -1.3800e+03 2.5714e+02 1.2434e+00 -1.3800e+03 2.8571e+02 1.3016e+00 -1.3800e+03 3.1429e+02 1.3647e+00 -1.3800e+03 3.4286e+02 1.4325e+00 -1.3800e+03 3.7143e+02 1.5047e+00 -1.3800e+03 4.0000e+02 1.5809e+00 -1.3800e+03 4.2857e+02 1.6609e+00 -1.3800e+03 4.5714e+02 1.7442e+00 -1.3800e+03 4.8571e+02 1.8306e+00 -1.3800e+03 5.1429e+02 1.9197e+00 -1.3800e+03 5.4286e+02 2.0112e+00 -1.3800e+03 5.7143e+02 2.1048e+00 -1.3800e+03 6.0000e+02 2.2002e+00 -1.3800e+03 6.2857e+02 2.2971e+00 -1.3800e+03 6.5714e+02 2.3953e+00 -1.3800e+03 6.8571e+02 2.4944e+00 -1.3800e+03 7.1429e+02 2.5942e+00 -1.3800e+03 7.4286e+02 2.6945e+00 -1.3800e+03 7.7143e+02 2.7951e+00 -1.3800e+03 8.0000e+02 2.8957e+00 -1.3800e+03 8.2857e+02 2.9962e+00 -1.3800e+03 8.5714e+02 3.0963e+00 -1.3800e+03 8.8571e+02 3.1960e+00 -1.3800e+03 9.1429e+02 3.2951e+00 -1.3800e+03 9.4286e+02 3.3935e+00 -1.3800e+03 9.7143e+02 3.4910e+00 -1.3800e+03 1.0000e+03 3.5875e+00 -1.3800e+03 1.0286e+03 3.6830e+00 -1.3800e+03 1.0571e+03 3.7773e+00 -1.3800e+03 1.0857e+03 3.8705e+00 -1.3800e+03 1.1143e+03 3.9623e+00 -1.3800e+03 1.1429e+03 4.0529e+00 -1.3800e+03 1.1714e+03 4.1420e+00 -1.3800e+03 1.2000e+03 4.2298e+00 -1.3800e+03 1.2286e+03 4.3161e+00 -1.3800e+03 1.2571e+03 4.4010e+00 -1.3800e+03 1.2857e+03 4.4844e+00 -1.3800e+03 1.3143e+03 4.5664e+00 -1.3800e+03 1.3429e+03 4.6468e+00 -1.3800e+03 1.3714e+03 4.7258e+00 -1.3800e+03 1.4000e+03 4.8032e+00 -1.3800e+03 1.4286e+03 4.8792e+00 -1.3800e+03 1.4571e+03 4.9537e+00 -1.3800e+03 1.4857e+03 5.0268e+00 -1.3800e+03 1.5143e+03 5.0984e+00 -1.3800e+03 1.5429e+03 5.1685e+00 -1.3800e+03 1.5714e+03 5.2373e+00 -1.3800e+03 1.6000e+03 5.3046e+00 -1.3800e+03 1.6286e+03 5.3706e+00 -1.3800e+03 1.6571e+03 5.4352e+00 -1.3800e+03 1.6857e+03 5.4984e+00 -1.3800e+03 1.7143e+03 5.5604e+00 -1.3800e+03 1.7429e+03 5.6211e+00 -1.3800e+03 1.7714e+03 5.6805e+00 -1.3800e+03 1.8000e+03 5.7387e+00 -1.3800e+03 1.8286e+03 5.7957e+00 -1.3800e+03 1.8571e+03 5.8514e+00 -1.3800e+03 1.8857e+03 5.9061e+00 -1.3800e+03 1.9143e+03 5.9595e+00 -1.3800e+03 1.9429e+03 6.0119e+00 -1.3800e+03 1.9714e+03 6.0632e+00 -1.3800e+03 2.0000e+03 6.1134e+00 -1.4100e+03 -2.0000e+03 6.1493e+00 -1.4100e+03 -1.9714e+03 6.1004e+00 -1.4100e+03 -1.9429e+03 6.0505e+00 -1.4100e+03 -1.9143e+03 5.9995e+00 -1.4100e+03 -1.8857e+03 5.9475e+00 -1.4100e+03 -1.8571e+03 5.8944e+00 -1.4100e+03 -1.8286e+03 5.8402e+00 -1.4100e+03 -1.8000e+03 5.7849e+00 -1.4100e+03 -1.7714e+03 5.7284e+00 -1.4100e+03 -1.7429e+03 5.6708e+00 -1.4100e+03 -1.7143e+03 5.6120e+00 -1.4100e+03 -1.6857e+03 5.5520e+00 -1.4100e+03 -1.6571e+03 5.4908e+00 -1.4100e+03 -1.6286e+03 5.4283e+00 -1.4100e+03 -1.6000e+03 5.3646e+00 -1.4100e+03 -1.5714e+03 5.2995e+00 -1.4100e+03 -1.5429e+03 5.2332e+00 -1.4100e+03 -1.5143e+03 5.1656e+00 -1.4100e+03 -1.4857e+03 5.0967e+00 -1.4100e+03 -1.4571e+03 5.0264e+00 -1.4100e+03 -1.4286e+03 4.9548e+00 -1.4100e+03 -1.4000e+03 4.8819e+00 -1.4100e+03 -1.3714e+03 4.8076e+00 -1.4100e+03 -1.3429e+03 4.7319e+00 -1.4100e+03 -1.3143e+03 4.6549e+00 -1.4100e+03 -1.2857e+03 4.5765e+00 -1.4100e+03 -1.2571e+03 4.4968e+00 -1.4100e+03 -1.2286e+03 4.4158e+00 -1.4100e+03 -1.2000e+03 4.3335e+00 -1.4100e+03 -1.1714e+03 4.2499e+00 -1.4100e+03 -1.1429e+03 4.1651e+00 -1.4100e+03 -1.1143e+03 4.0791e+00 -1.4100e+03 -1.0857e+03 3.9920e+00 -1.4100e+03 -1.0571e+03 3.9037e+00 -1.4100e+03 -1.0286e+03 3.8144e+00 -1.4100e+03 -1.0000e+03 3.7242e+00 -1.4100e+03 -9.7143e+02 3.6331e+00 -1.4100e+03 -9.4286e+02 3.5412e+00 -1.4100e+03 -9.1429e+02 3.4486e+00 -1.4100e+03 -8.8571e+02 3.3554e+00 -1.4100e+03 -8.5714e+02 3.2618e+00 -1.4100e+03 -8.2857e+02 3.1679e+00 -1.4100e+03 -8.0000e+02 3.0738e+00 -1.4100e+03 -7.7143e+02 2.9797e+00 -1.4100e+03 -7.4286e+02 2.8858e+00 -1.4100e+03 -7.1429e+02 2.7923e+00 -1.4100e+03 -6.8571e+02 2.6994e+00 -1.4100e+03 -6.5714e+02 2.6073e+00 -1.4100e+03 -6.2857e+02 2.5161e+00 -1.4100e+03 -6.0000e+02 2.4263e+00 -1.4100e+03 -5.7143e+02 2.3380e+00 -1.4100e+03 -5.4286e+02 2.2514e+00 -1.4100e+03 -5.1429e+02 2.1669e+00 -1.4100e+03 -4.8571e+02 2.0847e+00 -1.4100e+03 -4.5714e+02 2.0052e+00 -1.4100e+03 -4.2857e+02 1.9285e+00 -1.4100e+03 -4.0000e+02 1.8551e+00 -1.4100e+03 -3.7143e+02 1.7851e+00 -1.4100e+03 -3.4286e+02 1.7190e+00 -1.4100e+03 -3.1429e+02 1.6569e+00 -1.4100e+03 -2.8571e+02 1.5991e+00 -1.4100e+03 -2.5714e+02 1.5459e+00 -1.4100e+03 -2.2857e+02 1.4976e+00 -1.4100e+03 -2.0000e+02 1.4544e+00 -1.4100e+03 -1.7143e+02 1.4165e+00 -1.4100e+03 -1.4286e+02 1.3841e+00 -1.4100e+03 -1.1429e+02 1.3573e+00 -1.4100e+03 -8.5714e+01 1.3364e+00 -1.4100e+03 -5.7143e+01 1.3213e+00 -1.4100e+03 -2.8571e+01 1.3123e+00 -1.4100e+03 0.0000e+00 1.3092e+00 -1.4100e+03 2.8571e+01 1.3123e+00 -1.4100e+03 5.7143e+01 1.3213e+00 -1.4100e+03 8.5714e+01 1.3364e+00 -1.4100e+03 1.1429e+02 1.3573e+00 -1.4100e+03 1.4286e+02 1.3841e+00 -1.4100e+03 1.7143e+02 1.4165e+00 -1.4100e+03 2.0000e+02 1.4544e+00 -1.4100e+03 2.2857e+02 1.4976e+00 -1.4100e+03 2.5714e+02 1.5459e+00 -1.4100e+03 2.8571e+02 1.5991e+00 -1.4100e+03 3.1429e+02 1.6569e+00 -1.4100e+03 3.4286e+02 1.7190e+00 -1.4100e+03 3.7143e+02 1.7851e+00 -1.4100e+03 4.0000e+02 1.8551e+00 -1.4100e+03 4.2857e+02 1.9285e+00 -1.4100e+03 4.5714e+02 2.0052e+00 -1.4100e+03 4.8571e+02 2.0847e+00 -1.4100e+03 5.1429e+02 2.1669e+00 -1.4100e+03 5.4286e+02 2.2514e+00 -1.4100e+03 5.7143e+02 2.3380e+00 -1.4100e+03 6.0000e+02 2.4263e+00 -1.4100e+03 6.2857e+02 2.5161e+00 -1.4100e+03 6.5714e+02 2.6073e+00 -1.4100e+03 6.8571e+02 2.6994e+00 -1.4100e+03 7.1429e+02 2.7923e+00 -1.4100e+03 7.4286e+02 2.8858e+00 -1.4100e+03 7.7143e+02 2.9797e+00 -1.4100e+03 8.0000e+02 3.0738e+00 -1.4100e+03 8.2857e+02 3.1679e+00 -1.4100e+03 8.5714e+02 3.2618e+00 -1.4100e+03 8.8571e+02 3.3554e+00 -1.4100e+03 9.1429e+02 3.4486e+00 -1.4100e+03 9.4286e+02 3.5412e+00 -1.4100e+03 9.7143e+02 3.6331e+00 -1.4100e+03 1.0000e+03 3.7242e+00 -1.4100e+03 1.0286e+03 3.8144e+00 -1.4100e+03 1.0571e+03 3.9037e+00 -1.4100e+03 1.0857e+03 3.9920e+00 -1.4100e+03 1.1143e+03 4.0791e+00 -1.4100e+03 1.1429e+03 4.1651e+00 -1.4100e+03 1.1714e+03 4.2499e+00 -1.4100e+03 1.2000e+03 4.3335e+00 -1.4100e+03 1.2286e+03 4.4158e+00 -1.4100e+03 1.2571e+03 4.4968e+00 -1.4100e+03 1.2857e+03 4.5765e+00 -1.4100e+03 1.3143e+03 4.6549e+00 -1.4100e+03 1.3429e+03 4.7319e+00 -1.4100e+03 1.3714e+03 4.8076e+00 -1.4100e+03 1.4000e+03 4.8819e+00 -1.4100e+03 1.4286e+03 4.9548e+00 -1.4100e+03 1.4571e+03 5.0264e+00 -1.4100e+03 1.4857e+03 5.0967e+00 -1.4100e+03 1.5143e+03 5.1656e+00 -1.4100e+03 1.5429e+03 5.2332e+00 -1.4100e+03 1.5714e+03 5.2995e+00 -1.4100e+03 1.6000e+03 5.3646e+00 -1.4100e+03 1.6286e+03 5.4283e+00 -1.4100e+03 1.6571e+03 5.4908e+00 -1.4100e+03 1.6857e+03 5.5520e+00 -1.4100e+03 1.7143e+03 5.6120e+00 -1.4100e+03 1.7429e+03 5.6708e+00 -1.4100e+03 1.7714e+03 5.7284e+00 -1.4100e+03 1.8000e+03 5.7849e+00 -1.4100e+03 1.8286e+03 5.8402e+00 -1.4100e+03 1.8571e+03 5.8944e+00 -1.4100e+03 1.8857e+03 5.9475e+00 -1.4100e+03 1.9143e+03 5.9995e+00 -1.4100e+03 1.9429e+03 6.0505e+00 -1.4100e+03 1.9714e+03 6.1004e+00 -1.4100e+03 2.0000e+03 6.1493e+00 -1.4400e+03 -2.0000e+03 6.1851e+00 -1.4400e+03 -1.9714e+03 6.1374e+00 -1.4400e+03 -1.9429e+03 6.0888e+00 -1.4400e+03 -1.9143e+03 6.0392e+00 -1.4400e+03 -1.8857e+03 5.9886e+00 -1.4400e+03 -1.8571e+03 5.9370e+00 -1.4400e+03 -1.8286e+03 5.8843e+00 -1.4400e+03 -1.8000e+03 5.8306e+00 -1.4400e+03 -1.7714e+03 5.7758e+00 -1.4400e+03 -1.7429e+03 5.7200e+00 -1.4400e+03 -1.7143e+03 5.6630e+00 -1.4400e+03 -1.6857e+03 5.6049e+00 -1.4400e+03 -1.6571e+03 5.5457e+00 -1.4400e+03 -1.6286e+03 5.4853e+00 -1.4400e+03 -1.6000e+03 5.4237e+00 -1.4400e+03 -1.5714e+03 5.3610e+00 -1.4400e+03 -1.5429e+03 5.2970e+00 -1.4400e+03 -1.5143e+03 5.2318e+00 -1.4400e+03 -1.4857e+03 5.1655e+00 -1.4400e+03 -1.4571e+03 5.0979e+00 -1.4400e+03 -1.4286e+03 5.0290e+00 -1.4400e+03 -1.4000e+03 4.9590e+00 -1.4400e+03 -1.3714e+03 4.8877e+00 -1.4400e+03 -1.3429e+03 4.8151e+00 -1.4400e+03 -1.3143e+03 4.7414e+00 -1.4400e+03 -1.2857e+03 4.6664e+00 -1.4400e+03 -1.2571e+03 4.5903e+00 -1.4400e+03 -1.2286e+03 4.5130e+00 -1.4400e+03 -1.2000e+03 4.4345e+00 -1.4400e+03 -1.1714e+03 4.3549e+00 -1.4400e+03 -1.1429e+03 4.2742e+00 -1.4400e+03 -1.1143e+03 4.1924e+00 -1.4400e+03 -1.0857e+03 4.1097e+00 -1.4400e+03 -1.0571e+03 4.0260e+00 -1.4400e+03 -1.0286e+03 3.9415e+00 -1.4400e+03 -1.0000e+03 3.8561e+00 -1.4400e+03 -9.7143e+02 3.7701e+00 -1.4400e+03 -9.4286e+02 3.6833e+00 -1.4400e+03 -9.1429e+02 3.5961e+00 -1.4400e+03 -8.8571e+02 3.5084e+00 -1.4400e+03 -8.5714e+02 3.4205e+00 -1.4400e+03 -8.2857e+02 3.3323e+00 -1.4400e+03 -8.0000e+02 3.2441e+00 -1.4400e+03 -7.7143e+02 3.1561e+00 -1.4400e+03 -7.4286e+02 3.0683e+00 -1.4400e+03 -7.1429e+02 2.9810e+00 -1.4400e+03 -6.8571e+02 2.8944e+00 -1.4400e+03 -6.5714e+02 2.8086e+00 -1.4400e+03 -6.2857e+02 2.7239e+00 -1.4400e+03 -6.0000e+02 2.6404e+00 -1.4400e+03 -5.7143e+02 2.5585e+00 -1.4400e+03 -5.4286e+02 2.4783e+00 -1.4400e+03 -5.1429e+02 2.4001e+00 -1.4400e+03 -4.8571e+02 2.3242e+00 -1.4400e+03 -4.5714e+02 2.2508e+00 -1.4400e+03 -4.2857e+02 2.1802e+00 -1.4400e+03 -4.0000e+02 2.1125e+00 -1.4400e+03 -3.7143e+02 2.0482e+00 -1.4400e+03 -3.4286e+02 1.9874e+00 -1.4400e+03 -3.1429e+02 1.9304e+00 -1.4400e+03 -2.8571e+02 1.8774e+00 -1.4400e+03 -2.5714e+02 1.8287e+00 -1.4400e+03 -2.2857e+02 1.7844e+00 -1.4400e+03 -2.0000e+02 1.7449e+00 -1.4400e+03 -1.7143e+02 1.7102e+00 -1.4400e+03 -1.4286e+02 1.6806e+00 -1.4400e+03 -1.1429e+02 1.6561e+00 -1.4400e+03 -8.5714e+01 1.6370e+00 -1.4400e+03 -5.7143e+01 1.6232e+00 -1.4400e+03 -2.8571e+01 1.6150e+00 -1.4400e+03 0.0000e+00 1.6122e+00 -1.4400e+03 2.8571e+01 1.6150e+00 -1.4400e+03 5.7143e+01 1.6232e+00 -1.4400e+03 8.5714e+01 1.6370e+00 -1.4400e+03 1.1429e+02 1.6561e+00 -1.4400e+03 1.4286e+02 1.6806e+00 -1.4400e+03 1.7143e+02 1.7102e+00 -1.4400e+03 2.0000e+02 1.7449e+00 -1.4400e+03 2.2857e+02 1.7844e+00 -1.4400e+03 2.5714e+02 1.8287e+00 -1.4400e+03 2.8571e+02 1.8774e+00 -1.4400e+03 3.1429e+02 1.9304e+00 -1.4400e+03 3.4286e+02 1.9874e+00 -1.4400e+03 3.7143e+02 2.0482e+00 -1.4400e+03 4.0000e+02 2.1125e+00 -1.4400e+03 4.2857e+02 2.1802e+00 -1.4400e+03 4.5714e+02 2.2508e+00 -1.4400e+03 4.8571e+02 2.3242e+00 -1.4400e+03 5.1429e+02 2.4001e+00 -1.4400e+03 5.4286e+02 2.4783e+00 -1.4400e+03 5.7143e+02 2.5585e+00 -1.4400e+03 6.0000e+02 2.6404e+00 -1.4400e+03 6.2857e+02 2.7239e+00 -1.4400e+03 6.5714e+02 2.8086e+00 -1.4400e+03 6.8571e+02 2.8944e+00 -1.4400e+03 7.1429e+02 2.9810e+00 -1.4400e+03 7.4286e+02 3.0683e+00 -1.4400e+03 7.7143e+02 3.1561e+00 -1.4400e+03 8.0000e+02 3.2441e+00 -1.4400e+03 8.2857e+02 3.3323e+00 -1.4400e+03 8.5714e+02 3.4205e+00 -1.4400e+03 8.8571e+02 3.5084e+00 -1.4400e+03 9.1429e+02 3.5961e+00 -1.4400e+03 9.4286e+02 3.6833e+00 -1.4400e+03 9.7143e+02 3.7701e+00 -1.4400e+03 1.0000e+03 3.8561e+00 -1.4400e+03 1.0286e+03 3.9415e+00 -1.4400e+03 1.0571e+03 4.0260e+00 -1.4400e+03 1.0857e+03 4.1097e+00 -1.4400e+03 1.1143e+03 4.1924e+00 -1.4400e+03 1.1429e+03 4.2742e+00 -1.4400e+03 1.1714e+03 4.3549e+00 -1.4400e+03 1.2000e+03 4.4345e+00 -1.4400e+03 1.2286e+03 4.5130e+00 -1.4400e+03 1.2571e+03 4.5903e+00 -1.4400e+03 1.2857e+03 4.6664e+00 -1.4400e+03 1.3143e+03 4.7414e+00 -1.4400e+03 1.3429e+03 4.8151e+00 -1.4400e+03 1.3714e+03 4.8877e+00 -1.4400e+03 1.4000e+03 4.9590e+00 -1.4400e+03 1.4286e+03 5.0290e+00 -1.4400e+03 1.4571e+03 5.0979e+00 -1.4400e+03 1.4857e+03 5.1655e+00 -1.4400e+03 1.5143e+03 5.2318e+00 -1.4400e+03 1.5429e+03 5.2970e+00 -1.4400e+03 1.5714e+03 5.3610e+00 -1.4400e+03 1.6000e+03 5.4237e+00 -1.4400e+03 1.6286e+03 5.4853e+00 -1.4400e+03 1.6571e+03 5.5457e+00 -1.4400e+03 1.6857e+03 5.6049e+00 -1.4400e+03 1.7143e+03 5.6630e+00 -1.4400e+03 1.7429e+03 5.7200e+00 -1.4400e+03 1.7714e+03 5.7758e+00 -1.4400e+03 1.8000e+03 5.8306e+00 -1.4400e+03 1.8286e+03 5.8843e+00 -1.4400e+03 1.8571e+03 5.9370e+00 -1.4400e+03 1.8857e+03 5.9886e+00 -1.4400e+03 1.9143e+03 6.0392e+00 -1.4400e+03 1.9429e+03 6.0888e+00 -1.4400e+03 1.9714e+03 6.1374e+00 -1.4400e+03 2.0000e+03 6.1851e+00 -1.4700e+03 -2.0000e+03 6.2207e+00 -1.4700e+03 -1.9714e+03 6.1743e+00 -1.4700e+03 -1.9429e+03 6.1269e+00 -1.4700e+03 -1.9143e+03 6.0786e+00 -1.4700e+03 -1.8857e+03 6.0294e+00 -1.4700e+03 -1.8571e+03 5.9792e+00 -1.4700e+03 -1.8286e+03 5.9281e+00 -1.4700e+03 -1.8000e+03 5.8760e+00 -1.4700e+03 -1.7714e+03 5.8228e+00 -1.4700e+03 -1.7429e+03 5.7687e+00 -1.4700e+03 -1.7143e+03 5.7135e+00 -1.4700e+03 -1.6857e+03 5.6572e+00 -1.4700e+03 -1.6571e+03 5.5999e+00 -1.4700e+03 -1.6286e+03 5.5415e+00 -1.4700e+03 -1.6000e+03 5.4820e+00 -1.4700e+03 -1.5714e+03 5.4215e+00 -1.4700e+03 -1.5429e+03 5.3598e+00 -1.4700e+03 -1.5143e+03 5.2970e+00 -1.4700e+03 -1.4857e+03 5.2330e+00 -1.4700e+03 -1.4571e+03 5.1680e+00 -1.4700e+03 -1.4286e+03 5.1018e+00 -1.4700e+03 -1.4000e+03 5.0345e+00 -1.4700e+03 -1.3714e+03 4.9661e+00 -1.4700e+03 -1.3429e+03 4.8966e+00 -1.4700e+03 -1.3143e+03 4.8260e+00 -1.4700e+03 -1.2857e+03 4.7542e+00 -1.4700e+03 -1.2571e+03 4.6814e+00 -1.4700e+03 -1.2286e+03 4.6076e+00 -1.4700e+03 -1.2000e+03 4.5327e+00 -1.4700e+03 -1.1714e+03 4.4569e+00 -1.4700e+03 -1.1429e+03 4.3801e+00 -1.4700e+03 -1.1143e+03 4.3024e+00 -1.4700e+03 -1.0857e+03 4.2238e+00 -1.4700e+03 -1.0571e+03 4.1444e+00 -1.4700e+03 -1.0286e+03 4.0643e+00 -1.4700e+03 -1.0000e+03 3.9835e+00 -1.4700e+03 -9.7143e+02 3.9021e+00 -1.4700e+03 -9.4286e+02 3.8203e+00 -1.4700e+03 -9.1429e+02 3.7380e+00 -1.4700e+03 -8.8571e+02 3.6554e+00 -1.4700e+03 -8.5714e+02 3.5727e+00 -1.4700e+03 -8.2857e+02 3.4899e+00 -1.4700e+03 -8.0000e+02 3.4071e+00 -1.4700e+03 -7.7143e+02 3.3246e+00 -1.4700e+03 -7.4286e+02 3.2425e+00 -1.4700e+03 -7.1429e+02 3.1609e+00 -1.4700e+03 -6.8571e+02 3.0800e+00 -1.4700e+03 -6.5714e+02 3.0000e+00 -1.4700e+03 -6.2857e+02 2.9211e+00 -1.4700e+03 -6.0000e+02 2.8435e+00 -1.4700e+03 -5.7143e+02 2.7674e+00 -1.4700e+03 -5.4286e+02 2.6930e+00 -1.4700e+03 -5.1429e+02 2.6205e+00 -1.4700e+03 -4.8571e+02 2.5503e+00 -1.4700e+03 -4.5714e+02 2.4824e+00 -1.4700e+03 -4.2857e+02 2.4171e+00 -1.4700e+03 -4.0000e+02 2.3547e+00 -1.4700e+03 -3.7143e+02 2.2954e+00 -1.4700e+03 -3.4286e+02 2.2394e+00 -1.4700e+03 -3.1429e+02 2.1869e+00 -1.4700e+03 -2.8571e+02 2.1382e+00 -1.4700e+03 -2.5714e+02 2.0935e+00 -1.4700e+03 -2.2857e+02 2.0529e+00 -1.4700e+03 -2.0000e+02 2.0166e+00 -1.4700e+03 -1.7143e+02 1.9848e+00 -1.4700e+03 -1.4286e+02 1.9576e+00 -1.4700e+03 -1.1429e+02 1.9352e+00 -1.4700e+03 -8.5714e+01 1.9177e+00 -1.4700e+03 -5.7143e+01 1.9051e+00 -1.4700e+03 -2.8571e+01 1.8975e+00 -1.4700e+03 0.0000e+00 1.8950e+00 -1.4700e+03 2.8571e+01 1.8975e+00 -1.4700e+03 5.7143e+01 1.9051e+00 -1.4700e+03 8.5714e+01 1.9177e+00 -1.4700e+03 1.1429e+02 1.9352e+00 -1.4700e+03 1.4286e+02 1.9576e+00 -1.4700e+03 1.7143e+02 1.9848e+00 -1.4700e+03 2.0000e+02 2.0166e+00 -1.4700e+03 2.2857e+02 2.0529e+00 -1.4700e+03 2.5714e+02 2.0935e+00 -1.4700e+03 2.8571e+02 2.1382e+00 -1.4700e+03 3.1429e+02 2.1869e+00 -1.4700e+03 3.4286e+02 2.2394e+00 -1.4700e+03 3.7143e+02 2.2954e+00 -1.4700e+03 4.0000e+02 2.3547e+00 -1.4700e+03 4.2857e+02 2.4171e+00 -1.4700e+03 4.5714e+02 2.4824e+00 -1.4700e+03 4.8571e+02 2.5503e+00 -1.4700e+03 5.1429e+02 2.6205e+00 -1.4700e+03 5.4286e+02 2.6930e+00 -1.4700e+03 5.7143e+02 2.7674e+00 -1.4700e+03 6.0000e+02 2.8435e+00 -1.4700e+03 6.2857e+02 2.9211e+00 -1.4700e+03 6.5714e+02 3.0000e+00 -1.4700e+03 6.8571e+02 3.0800e+00 -1.4700e+03 7.1429e+02 3.1609e+00 -1.4700e+03 7.4286e+02 3.2425e+00 -1.4700e+03 7.7143e+02 3.3246e+00 -1.4700e+03 8.0000e+02 3.4071e+00 -1.4700e+03 8.2857e+02 3.4899e+00 -1.4700e+03 8.5714e+02 3.5727e+00 -1.4700e+03 8.8571e+02 3.6554e+00 -1.4700e+03 9.1429e+02 3.7380e+00 -1.4700e+03 9.4286e+02 3.8203e+00 -1.4700e+03 9.7143e+02 3.9021e+00 -1.4700e+03 1.0000e+03 3.9835e+00 -1.4700e+03 1.0286e+03 4.0643e+00 -1.4700e+03 1.0571e+03 4.1444e+00 -1.4700e+03 1.0857e+03 4.2238e+00 -1.4700e+03 1.1143e+03 4.3024e+00 -1.4700e+03 1.1429e+03 4.3801e+00 -1.4700e+03 1.1714e+03 4.4569e+00 -1.4700e+03 1.2000e+03 4.5327e+00 -1.4700e+03 1.2286e+03 4.6076e+00 -1.4700e+03 1.2571e+03 4.6814e+00 -1.4700e+03 1.2857e+03 4.7542e+00 -1.4700e+03 1.3143e+03 4.8260e+00 -1.4700e+03 1.3429e+03 4.8966e+00 -1.4700e+03 1.3714e+03 4.9661e+00 -1.4700e+03 1.4000e+03 5.0345e+00 -1.4700e+03 1.4286e+03 5.1018e+00 -1.4700e+03 1.4571e+03 5.1680e+00 -1.4700e+03 1.4857e+03 5.2330e+00 -1.4700e+03 1.5143e+03 5.2970e+00 -1.4700e+03 1.5429e+03 5.3598e+00 -1.4700e+03 1.5714e+03 5.4215e+00 -1.4700e+03 1.6000e+03 5.4820e+00 -1.4700e+03 1.6286e+03 5.5415e+00 -1.4700e+03 1.6571e+03 5.5999e+00 -1.4700e+03 1.6857e+03 5.6572e+00 -1.4700e+03 1.7143e+03 5.7135e+00 -1.4700e+03 1.7429e+03 5.7687e+00 -1.4700e+03 1.7714e+03 5.8228e+00 -1.4700e+03 1.8000e+03 5.8760e+00 -1.4700e+03 1.8286e+03 5.9281e+00 -1.4700e+03 1.8571e+03 5.9792e+00 -1.4700e+03 1.8857e+03 6.0294e+00 -1.4700e+03 1.9143e+03 6.0786e+00 -1.4700e+03 1.9429e+03 6.1269e+00 -1.4700e+03 1.9714e+03 6.1743e+00 -1.4700e+03 2.0000e+03 6.2207e+00 -1.5000e+03 -2.0000e+03 6.2561e+00 -1.5000e+03 -1.9714e+03 6.2109e+00 -1.5000e+03 -1.9429e+03 6.1648e+00 -1.5000e+03 -1.9143e+03 6.1178e+00 -1.5000e+03 -1.8857e+03 6.0699e+00 -1.5000e+03 -1.8571e+03 6.0212e+00 -1.5000e+03 -1.8286e+03 5.9715e+00 -1.5000e+03 -1.8000e+03 5.9209e+00 -1.5000e+03 -1.7714e+03 5.8693e+00 -1.5000e+03 -1.7429e+03 5.8168e+00 -1.5000e+03 -1.7143e+03 5.7633e+00 -1.5000e+03 -1.6857e+03 5.7089e+00 -1.5000e+03 -1.6571e+03 5.6534e+00 -1.5000e+03 -1.6286e+03 5.5970e+00 -1.5000e+03 -1.6000e+03 5.5395e+00 -1.5000e+03 -1.5714e+03 5.4810e+00 -1.5000e+03 -1.5429e+03 5.4215e+00 -1.5000e+03 -1.5143e+03 5.3610e+00 -1.5000e+03 -1.4857e+03 5.2994e+00 -1.5000e+03 -1.4571e+03 5.2369e+00 -1.5000e+03 -1.4286e+03 5.1732e+00 -1.5000e+03 -1.4000e+03 5.1086e+00 -1.5000e+03 -1.3714e+03 5.0429e+00 -1.5000e+03 -1.3429e+03 4.9763e+00 -1.5000e+03 -1.3143e+03 4.9086e+00 -1.5000e+03 -1.2857e+03 4.8400e+00 -1.5000e+03 -1.2571e+03 4.7704e+00 -1.5000e+03 -1.2286e+03 4.6998e+00 -1.5000e+03 -1.2000e+03 4.6284e+00 -1.5000e+03 -1.1714e+03 4.5561e+00 -1.5000e+03 -1.1429e+03 4.4829e+00 -1.5000e+03 -1.1143e+03 4.4090e+00 -1.5000e+03 -1.0857e+03 4.3343e+00 -1.5000e+03 -1.0571e+03 4.2590e+00 -1.5000e+03 -1.0286e+03 4.1830e+00 -1.5000e+03 -1.0000e+03 4.1065e+00 -1.5000e+03 -9.7143e+02 4.0296e+00 -1.5000e+03 -9.4286e+02 3.9522e+00 -1.5000e+03 -9.1429e+02 3.8745e+00 -1.5000e+03 -8.8571e+02 3.7967e+00 -1.5000e+03 -8.5714e+02 3.7188e+00 -1.5000e+03 -8.2857e+02 3.6409e+00 -1.5000e+03 -8.0000e+02 3.5632e+00 -1.5000e+03 -7.7143e+02 3.4858e+00 -1.5000e+03 -7.4286e+02 3.4088e+00 -1.5000e+03 -7.1429e+02 3.3324e+00 -1.5000e+03 -6.8571e+02 3.2568e+00 -1.5000e+03 -6.5714e+02 3.1821e+00 -1.5000e+03 -6.2857e+02 3.1086e+00 -1.5000e+03 -6.0000e+02 3.0363e+00 -1.5000e+03 -5.7143e+02 2.9655e+00 -1.5000e+03 -5.4286e+02 2.8963e+00 -1.5000e+03 -5.1429e+02 2.8291e+00 -1.5000e+03 -4.8571e+02 2.7639e+00 -1.5000e+03 -4.5714e+02 2.7010e+00 -1.5000e+03 -4.2857e+02 2.6406e+00 -1.5000e+03 -4.0000e+02 2.5829e+00 -1.5000e+03 -3.7143e+02 2.5281e+00 -1.5000e+03 -3.4286e+02 2.4764e+00 -1.5000e+03 -3.1429e+02 2.4280e+00 -1.5000e+03 -2.8571e+02 2.3832e+00 -1.5000e+03 -2.5714e+02 2.3419e+00 -1.5000e+03 -2.2857e+02 2.3046e+00 -1.5000e+03 -2.0000e+02 2.2712e+00 -1.5000e+03 -1.7143e+02 2.2420e+00 -1.5000e+03 -1.4286e+02 2.2170e+00 -1.5000e+03 -1.1429e+02 2.1965e+00 -1.5000e+03 -8.5714e+01 2.1804e+00 -1.5000e+03 -5.7143e+01 2.1688e+00 -1.5000e+03 -2.8571e+01 2.1619e+00 -1.5000e+03 0.0000e+00 2.1595e+00 -1.5000e+03 2.8571e+01 2.1619e+00 -1.5000e+03 5.7143e+01 2.1688e+00 -1.5000e+03 8.5714e+01 2.1804e+00 -1.5000e+03 1.1429e+02 2.1965e+00 -1.5000e+03 1.4286e+02 2.2170e+00 -1.5000e+03 1.7143e+02 2.2420e+00 -1.5000e+03 2.0000e+02 2.2712e+00 -1.5000e+03 2.2857e+02 2.3046e+00 -1.5000e+03 2.5714e+02 2.3419e+00 -1.5000e+03 2.8571e+02 2.3832e+00 -1.5000e+03 3.1429e+02 2.4280e+00 -1.5000e+03 3.4286e+02 2.4764e+00 -1.5000e+03 3.7143e+02 2.5281e+00 -1.5000e+03 4.0000e+02 2.5829e+00 -1.5000e+03 4.2857e+02 2.6406e+00 -1.5000e+03 4.5714e+02 2.7010e+00 -1.5000e+03 4.8571e+02 2.7639e+00 -1.5000e+03 5.1429e+02 2.8291e+00 -1.5000e+03 5.4286e+02 2.8963e+00 -1.5000e+03 5.7143e+02 2.9655e+00 -1.5000e+03 6.0000e+02 3.0363e+00 -1.5000e+03 6.2857e+02 3.1086e+00 -1.5000e+03 6.5714e+02 3.1821e+00 -1.5000e+03 6.8571e+02 3.2568e+00 -1.5000e+03 7.1429e+02 3.3324e+00 -1.5000e+03 7.4286e+02 3.4088e+00 -1.5000e+03 7.7143e+02 3.4858e+00 -1.5000e+03 8.0000e+02 3.5632e+00 -1.5000e+03 8.2857e+02 3.6409e+00 -1.5000e+03 8.5714e+02 3.7188e+00 -1.5000e+03 8.8571e+02 3.7967e+00 -1.5000e+03 9.1429e+02 3.8745e+00 -1.5000e+03 9.4286e+02 3.9522e+00 -1.5000e+03 9.7143e+02 4.0296e+00 -1.5000e+03 1.0000e+03 4.1065e+00 -1.5000e+03 1.0286e+03 4.1830e+00 -1.5000e+03 1.0571e+03 4.2590e+00 -1.5000e+03 1.0857e+03 4.3343e+00 -1.5000e+03 1.1143e+03 4.4090e+00 -1.5000e+03 1.1429e+03 4.4829e+00 -1.5000e+03 1.1714e+03 4.5561e+00 -1.5000e+03 1.2000e+03 4.6284e+00 -1.5000e+03 1.2286e+03 4.6998e+00 -1.5000e+03 1.2571e+03 4.7704e+00 -1.5000e+03 1.2857e+03 4.8400e+00 -1.5000e+03 1.3143e+03 4.9086e+00 -1.5000e+03 1.3429e+03 4.9763e+00 -1.5000e+03 1.3714e+03 5.0429e+00 -1.5000e+03 1.4000e+03 5.1086e+00 -1.5000e+03 1.4286e+03 5.1732e+00 -1.5000e+03 1.4571e+03 5.2369e+00 -1.5000e+03 1.4857e+03 5.2994e+00 -1.5000e+03 1.5143e+03 5.3610e+00 -1.5000e+03 1.5429e+03 5.4215e+00 -1.5000e+03 1.5714e+03 5.4810e+00 -1.5000e+03 1.6000e+03 5.5395e+00 -1.5000e+03 1.6286e+03 5.5970e+00 -1.5000e+03 1.6571e+03 5.6534e+00 -1.5000e+03 1.6857e+03 5.7089e+00 -1.5000e+03 1.7143e+03 5.7633e+00 -1.5000e+03 1.7429e+03 5.8168e+00 -1.5000e+03 1.7714e+03 5.8693e+00 -1.5000e+03 1.8000e+03 5.9209e+00 -1.5000e+03 1.8286e+03 5.9715e+00 -1.5000e+03 1.8571e+03 6.0212e+00 -1.5000e+03 1.8857e+03 6.0699e+00 -1.5000e+03 1.9143e+03 6.1178e+00 -1.5000e+03 1.9429e+03 6.1648e+00 -1.5000e+03 1.9714e+03 6.2109e+00 -1.5000e+03 2.0000e+03 6.2561e+00 -1.5300e+03 -2.0000e+03 6.2913e+00 -1.5300e+03 -1.9714e+03 6.2472e+00 -1.5300e+03 -1.9429e+03 6.2024e+00 -1.5300e+03 -1.9143e+03 6.1567e+00 -1.5300e+03 -1.8857e+03 6.1101e+00 -1.5300e+03 -1.8571e+03 6.0627e+00 -1.5300e+03 -1.8286e+03 6.0145e+00 -1.5300e+03 -1.8000e+03 5.9653e+00 -1.5300e+03 -1.7714e+03 5.9153e+00 -1.5300e+03 -1.7429e+03 5.8644e+00 -1.5300e+03 -1.7143e+03 5.8126e+00 -1.5300e+03 -1.6857e+03 5.7599e+00 -1.5300e+03 -1.6571e+03 5.7062e+00 -1.5300e+03 -1.6286e+03 5.6517e+00 -1.5300e+03 -1.6000e+03 5.5962e+00 -1.5300e+03 -1.5714e+03 5.5397e+00 -1.5300e+03 -1.5429e+03 5.4823e+00 -1.5300e+03 -1.5143e+03 5.4240e+00 -1.5300e+03 -1.4857e+03 5.3647e+00 -1.5300e+03 -1.4571e+03 5.3044e+00 -1.5300e+03 -1.4286e+03 5.2432e+00 -1.5300e+03 -1.4000e+03 5.1811e+00 -1.5300e+03 -1.3714e+03 5.1181e+00 -1.5300e+03 -1.3429e+03 5.0542e+00 -1.5300e+03 -1.3143e+03 4.9893e+00 -1.5300e+03 -1.2857e+03 4.9236e+00 -1.5300e+03 -1.2571e+03 4.8570e+00 -1.5300e+03 -1.2286e+03 4.7896e+00 -1.5300e+03 -1.2000e+03 4.7215e+00 -1.5300e+03 -1.1714e+03 4.6525e+00 -1.5300e+03 -1.1429e+03 4.5828e+00 -1.5300e+03 -1.1143e+03 4.5125e+00 -1.5300e+03 -1.0857e+03 4.4415e+00 -1.5300e+03 -1.0571e+03 4.3699e+00 -1.5300e+03 -1.0286e+03 4.2979e+00 -1.5300e+03 -1.0000e+03 4.2254e+00 -1.5300e+03 -9.7143e+02 4.1525e+00 -1.5300e+03 -9.4286e+02 4.0793e+00 -1.5300e+03 -9.1429e+02 4.0060e+00 -1.5300e+03 -8.8571e+02 3.9326e+00 -1.5300e+03 -8.5714e+02 3.8591e+00 -1.5300e+03 -8.2857e+02 3.7858e+00 -1.5300e+03 -8.0000e+02 3.7127e+00 -1.5300e+03 -7.7143e+02 3.6400e+00 -1.5300e+03 -7.4286e+02 3.5678e+00 -1.5300e+03 -7.1429e+02 3.4963e+00 -1.5300e+03 -6.8571e+02 3.4255e+00 -1.5300e+03 -6.5714e+02 3.3557e+00 -1.5300e+03 -6.2857e+02 3.2870e+00 -1.5300e+03 -6.0000e+02 3.2195e+00 -1.5300e+03 -5.7143e+02 3.1535e+00 -1.5300e+03 -5.4286e+02 3.0892e+00 -1.5300e+03 -5.1429e+02 3.0266e+00 -1.5300e+03 -4.8571e+02 2.9661e+00 -1.5300e+03 -4.5714e+02 2.9077e+00 -1.5300e+03 -4.2857e+02 2.8517e+00 -1.5300e+03 -4.0000e+02 2.7983e+00 -1.5300e+03 -3.7143e+02 2.7475e+00 -1.5300e+03 -3.4286e+02 2.6997e+00 -1.5300e+03 -3.1429e+02 2.6550e+00 -1.5300e+03 -2.8571e+02 2.6136e+00 -1.5300e+03 -2.5714e+02 2.5756e+00 -1.5300e+03 -2.2857e+02 2.5411e+00 -1.5300e+03 -2.0000e+02 2.5103e+00 -1.5300e+03 -1.7143e+02 2.4834e+00 -1.5300e+03 -1.4286e+02 2.4604e+00 -1.5300e+03 -1.1429e+02 2.4415e+00 -1.5300e+03 -8.5714e+01 2.4267e+00 -1.5300e+03 -5.7143e+01 2.4160e+00 -1.5300e+03 -2.8571e+01 2.4096e+00 -1.5300e+03 0.0000e+00 2.4075e+00 -1.5300e+03 2.8571e+01 2.4096e+00 -1.5300e+03 5.7143e+01 2.4160e+00 -1.5300e+03 8.5714e+01 2.4267e+00 -1.5300e+03 1.1429e+02 2.4415e+00 -1.5300e+03 1.4286e+02 2.4604e+00 -1.5300e+03 1.7143e+02 2.4834e+00 -1.5300e+03 2.0000e+02 2.5103e+00 -1.5300e+03 2.2857e+02 2.5411e+00 -1.5300e+03 2.5714e+02 2.5756e+00 -1.5300e+03 2.8571e+02 2.6136e+00 -1.5300e+03 3.1429e+02 2.6550e+00 -1.5300e+03 3.4286e+02 2.6997e+00 -1.5300e+03 3.7143e+02 2.7475e+00 -1.5300e+03 4.0000e+02 2.7983e+00 -1.5300e+03 4.2857e+02 2.8517e+00 -1.5300e+03 4.5714e+02 2.9077e+00 -1.5300e+03 4.8571e+02 2.9661e+00 -1.5300e+03 5.1429e+02 3.0266e+00 -1.5300e+03 5.4286e+02 3.0892e+00 -1.5300e+03 5.7143e+02 3.1535e+00 -1.5300e+03 6.0000e+02 3.2195e+00 -1.5300e+03 6.2857e+02 3.2870e+00 -1.5300e+03 6.5714e+02 3.3557e+00 -1.5300e+03 6.8571e+02 3.4255e+00 -1.5300e+03 7.1429e+02 3.4963e+00 -1.5300e+03 7.4286e+02 3.5678e+00 -1.5300e+03 7.7143e+02 3.6400e+00 -1.5300e+03 8.0000e+02 3.7127e+00 -1.5300e+03 8.2857e+02 3.7858e+00 -1.5300e+03 8.5714e+02 3.8591e+00 -1.5300e+03 8.8571e+02 3.9326e+00 -1.5300e+03 9.1429e+02 4.0060e+00 -1.5300e+03 9.4286e+02 4.0793e+00 -1.5300e+03 9.7143e+02 4.1525e+00 -1.5300e+03 1.0000e+03 4.2254e+00 -1.5300e+03 1.0286e+03 4.2979e+00 -1.5300e+03 1.0571e+03 4.3699e+00 -1.5300e+03 1.0857e+03 4.4415e+00 -1.5300e+03 1.1143e+03 4.5125e+00 -1.5300e+03 1.1429e+03 4.5828e+00 -1.5300e+03 1.1714e+03 4.6525e+00 -1.5300e+03 1.2000e+03 4.7215e+00 -1.5300e+03 1.2286e+03 4.7896e+00 -1.5300e+03 1.2571e+03 4.8570e+00 -1.5300e+03 1.2857e+03 4.9236e+00 -1.5300e+03 1.3143e+03 4.9893e+00 -1.5300e+03 1.3429e+03 5.0542e+00 -1.5300e+03 1.3714e+03 5.1181e+00 -1.5300e+03 1.4000e+03 5.1811e+00 -1.5300e+03 1.4286e+03 5.2432e+00 -1.5300e+03 1.4571e+03 5.3044e+00 -1.5300e+03 1.4857e+03 5.3647e+00 -1.5300e+03 1.5143e+03 5.4240e+00 -1.5300e+03 1.5429e+03 5.4823e+00 -1.5300e+03 1.5714e+03 5.5397e+00 -1.5300e+03 1.6000e+03 5.5962e+00 -1.5300e+03 1.6286e+03 5.6517e+00 -1.5300e+03 1.6571e+03 5.7062e+00 -1.5300e+03 1.6857e+03 5.7599e+00 -1.5300e+03 1.7143e+03 5.8126e+00 -1.5300e+03 1.7429e+03 5.8644e+00 -1.5300e+03 1.7714e+03 5.9153e+00 -1.5300e+03 1.8000e+03 5.9653e+00 -1.5300e+03 1.8286e+03 6.0145e+00 -1.5300e+03 1.8571e+03 6.0627e+00 -1.5300e+03 1.8857e+03 6.1101e+00 -1.5300e+03 1.9143e+03 6.1567e+00 -1.5300e+03 1.9429e+03 6.2024e+00 -1.5300e+03 1.9714e+03 6.2472e+00 -1.5300e+03 2.0000e+03 6.2913e+00 -1.5600e+03 -2.0000e+03 6.3263e+00 -1.5600e+03 -1.9714e+03 6.2834e+00 -1.5600e+03 -1.9429e+03 6.2397e+00 -1.5600e+03 -1.9143e+03 6.1952e+00 -1.5600e+03 -1.8857e+03 6.1499e+00 -1.5600e+03 -1.8571e+03 6.1039e+00 -1.5600e+03 -1.8286e+03 6.0570e+00 -1.5600e+03 -1.8000e+03 6.0093e+00 -1.5600e+03 -1.7714e+03 5.9608e+00 -1.5600e+03 -1.7429e+03 5.9115e+00 -1.5600e+03 -1.7143e+03 5.8613e+00 -1.5600e+03 -1.6857e+03 5.8102e+00 -1.5600e+03 -1.6571e+03 5.7583e+00 -1.5600e+03 -1.6286e+03 5.7056e+00 -1.5600e+03 -1.6000e+03 5.6519e+00 -1.5600e+03 -1.5714e+03 5.5974e+00 -1.5600e+03 -1.5429e+03 5.5421e+00 -1.5600e+03 -1.5143e+03 5.4858e+00 -1.5600e+03 -1.4857e+03 5.4287e+00 -1.5600e+03 -1.4571e+03 5.3707e+00 -1.5600e+03 -1.4286e+03 5.3119e+00 -1.5600e+03 -1.4000e+03 5.2522e+00 -1.5600e+03 -1.3714e+03 5.1917e+00 -1.5600e+03 -1.3429e+03 5.1303e+00 -1.5600e+03 -1.3143e+03 5.0682e+00 -1.5600e+03 -1.2857e+03 5.0053e+00 -1.5600e+03 -1.2571e+03 4.9416e+00 -1.5600e+03 -1.2286e+03 4.8772e+00 -1.5600e+03 -1.2000e+03 4.8120e+00 -1.5600e+03 -1.1714e+03 4.7462e+00 -1.5600e+03 -1.1429e+03 4.6798e+00 -1.5600e+03 -1.1143e+03 4.6128e+00 -1.5600e+03 -1.0857e+03 4.5453e+00 -1.5600e+03 -1.0571e+03 4.4773e+00 -1.5600e+03 -1.0286e+03 4.4089e+00 -1.5600e+03 -1.0000e+03 4.3402e+00 -1.5600e+03 -9.7143e+02 4.2711e+00 -1.5600e+03 -9.4286e+02 4.2019e+00 -1.5600e+03 -9.1429e+02 4.1326e+00 -1.5600e+03 -8.8571e+02 4.0632e+00 -1.5600e+03 -8.5714e+02 3.9940e+00 -1.5600e+03 -8.2857e+02 3.9249e+00 -1.5600e+03 -8.0000e+02 3.8561e+00 -1.5600e+03 -7.7143e+02 3.7878e+00 -1.5600e+03 -7.4286e+02 3.7200e+00 -1.5600e+03 -7.1429e+02 3.6528e+00 -1.5600e+03 -6.8571e+02 3.5865e+00 -1.5600e+03 -6.5714e+02 3.5211e+00 -1.5600e+03 -6.2857e+02 3.4569e+00 -1.5600e+03 -6.0000e+02 3.3939e+00 -1.5600e+03 -5.7143e+02 3.3323e+00 -1.5600e+03 -5.4286e+02 3.2723e+00 -1.5600e+03 -5.1429e+02 3.2140e+00 -1.5600e+03 -4.8571e+02 3.1577e+00 -1.5600e+03 -4.5714e+02 3.1034e+00 -1.5600e+03 -4.2857e+02 3.0514e+00 -1.5600e+03 -4.0000e+02 3.0018e+00 -1.5600e+03 -3.7143e+02 2.9548e+00 -1.5600e+03 -3.4286e+02 2.9105e+00 -1.5600e+03 -3.1429e+02 2.8691e+00 -1.5600e+03 -2.8571e+02 2.8307e+00 -1.5600e+03 -2.5714e+02 2.7956e+00 -1.5600e+03 -2.2857e+02 2.7637e+00 -1.5600e+03 -2.0000e+02 2.7353e+00 -1.5600e+03 -1.7143e+02 2.7104e+00 -1.5600e+03 -1.4286e+02 2.6892e+00 -1.5600e+03 -1.1429e+02 2.6718e+00 -1.5600e+03 -8.5714e+01 2.6581e+00 -1.5600e+03 -5.7143e+01 2.6483e+00 -1.5600e+03 -2.8571e+01 2.6424e+00 -1.5600e+03 0.0000e+00 2.6404e+00 -1.5600e+03 2.8571e+01 2.6424e+00 -1.5600e+03 5.7143e+01 2.6483e+00 -1.5600e+03 8.5714e+01 2.6581e+00 -1.5600e+03 1.1429e+02 2.6718e+00 -1.5600e+03 1.4286e+02 2.6892e+00 -1.5600e+03 1.7143e+02 2.7104e+00 -1.5600e+03 2.0000e+02 2.7353e+00 -1.5600e+03 2.2857e+02 2.7637e+00 -1.5600e+03 2.5714e+02 2.7956e+00 -1.5600e+03 2.8571e+02 2.8307e+00 -1.5600e+03 3.1429e+02 2.8691e+00 -1.5600e+03 3.4286e+02 2.9105e+00 -1.5600e+03 3.7143e+02 2.9548e+00 -1.5600e+03 4.0000e+02 3.0018e+00 -1.5600e+03 4.2857e+02 3.0514e+00 -1.5600e+03 4.5714e+02 3.1034e+00 -1.5600e+03 4.8571e+02 3.1577e+00 -1.5600e+03 5.1429e+02 3.2140e+00 -1.5600e+03 5.4286e+02 3.2723e+00 -1.5600e+03 5.7143e+02 3.3323e+00 -1.5600e+03 6.0000e+02 3.3939e+00 -1.5600e+03 6.2857e+02 3.4569e+00 -1.5600e+03 6.5714e+02 3.5211e+00 -1.5600e+03 6.8571e+02 3.5865e+00 -1.5600e+03 7.1429e+02 3.6528e+00 -1.5600e+03 7.4286e+02 3.7200e+00 -1.5600e+03 7.7143e+02 3.7878e+00 -1.5600e+03 8.0000e+02 3.8561e+00 -1.5600e+03 8.2857e+02 3.9249e+00 -1.5600e+03 8.5714e+02 3.9940e+00 -1.5600e+03 8.8571e+02 4.0632e+00 -1.5600e+03 9.1429e+02 4.1326e+00 -1.5600e+03 9.4286e+02 4.2019e+00 -1.5600e+03 9.7143e+02 4.2711e+00 -1.5600e+03 1.0000e+03 4.3402e+00 -1.5600e+03 1.0286e+03 4.4089e+00 -1.5600e+03 1.0571e+03 4.4773e+00 -1.5600e+03 1.0857e+03 4.5453e+00 -1.5600e+03 1.1143e+03 4.6128e+00 -1.5600e+03 1.1429e+03 4.6798e+00 -1.5600e+03 1.1714e+03 4.7462e+00 -1.5600e+03 1.2000e+03 4.8120e+00 -1.5600e+03 1.2286e+03 4.8772e+00 -1.5600e+03 1.2571e+03 4.9416e+00 -1.5600e+03 1.2857e+03 5.0053e+00 -1.5600e+03 1.3143e+03 5.0682e+00 -1.5600e+03 1.3429e+03 5.1303e+00 -1.5600e+03 1.3714e+03 5.1917e+00 -1.5600e+03 1.4000e+03 5.2522e+00 -1.5600e+03 1.4286e+03 5.3119e+00 -1.5600e+03 1.4571e+03 5.3707e+00 -1.5600e+03 1.4857e+03 5.4287e+00 -1.5600e+03 1.5143e+03 5.4858e+00 -1.5600e+03 1.5429e+03 5.5421e+00 -1.5600e+03 1.5714e+03 5.5974e+00 -1.5600e+03 1.6000e+03 5.6519e+00 -1.5600e+03 1.6286e+03 5.7056e+00 -1.5600e+03 1.6571e+03 5.7583e+00 -1.5600e+03 1.6857e+03 5.8102e+00 -1.5600e+03 1.7143e+03 5.8613e+00 -1.5600e+03 1.7429e+03 5.9115e+00 -1.5600e+03 1.7714e+03 5.9608e+00 -1.5600e+03 1.8000e+03 6.0093e+00 -1.5600e+03 1.8286e+03 6.0570e+00 -1.5600e+03 1.8571e+03 6.1039e+00 -1.5600e+03 1.8857e+03 6.1499e+00 -1.5600e+03 1.9143e+03 6.1952e+00 -1.5600e+03 1.9429e+03 6.2397e+00 -1.5600e+03 1.9714e+03 6.2834e+00 -1.5600e+03 2.0000e+03 6.3263e+00 -1.5900e+03 -2.0000e+03 6.3610e+00 -1.5900e+03 -1.9714e+03 6.3192e+00 -1.5900e+03 -1.9429e+03 6.2767e+00 -1.5900e+03 -1.9143e+03 6.2334e+00 -1.5900e+03 -1.8857e+03 6.1894e+00 -1.5900e+03 -1.8571e+03 6.1447e+00 -1.5900e+03 -1.8286e+03 6.0992e+00 -1.5900e+03 -1.8000e+03 6.0529e+00 -1.5900e+03 -1.7714e+03 6.0058e+00 -1.5900e+03 -1.7429e+03 5.9580e+00 -1.5900e+03 -1.7143e+03 5.9093e+00 -1.5900e+03 -1.6857e+03 5.8599e+00 -1.5900e+03 -1.6571e+03 5.8097e+00 -1.5900e+03 -1.6286e+03 5.7587e+00 -1.5900e+03 -1.6000e+03 5.7069e+00 -1.5900e+03 -1.5714e+03 5.6542e+00 -1.5900e+03 -1.5429e+03 5.6008e+00 -1.5900e+03 -1.5143e+03 5.5466e+00 -1.5900e+03 -1.4857e+03 5.4916e+00 -1.5900e+03 -1.4571e+03 5.4357e+00 -1.5900e+03 -1.4286e+03 5.3791e+00 -1.5900e+03 -1.4000e+03 5.3218e+00 -1.5900e+03 -1.3714e+03 5.2637e+00 -1.5900e+03 -1.3429e+03 5.2048e+00 -1.5900e+03 -1.3143e+03 5.1452e+00 -1.5900e+03 -1.2857e+03 5.0850e+00 -1.5900e+03 -1.2571e+03 5.0240e+00 -1.5900e+03 -1.2286e+03 4.9624e+00 -1.5900e+03 -1.2000e+03 4.9002e+00 -1.5900e+03 -1.1714e+03 4.8374e+00 -1.5900e+03 -1.1429e+03 4.7741e+00 -1.5900e+03 -1.1143e+03 4.7103e+00 -1.5900e+03 -1.0857e+03 4.6460e+00 -1.5900e+03 -1.0571e+03 4.5813e+00 -1.5900e+03 -1.0286e+03 4.5164e+00 -1.5900e+03 -1.0000e+03 4.4511e+00 -1.5900e+03 -9.7143e+02 4.3857e+00 -1.5900e+03 -9.4286e+02 4.3201e+00 -1.5900e+03 -9.1429e+02 4.2546e+00 -1.5900e+03 -8.8571e+02 4.1890e+00 -1.5900e+03 -8.5714e+02 4.1236e+00 -1.5900e+03 -8.2857e+02 4.0585e+00 -1.5900e+03 -8.0000e+02 3.9937e+00 -1.5900e+03 -7.7143e+02 3.9294e+00 -1.5900e+03 -7.4286e+02 3.8656e+00 -1.5900e+03 -7.1429e+02 3.8026e+00 -1.5900e+03 -6.8571e+02 3.7403e+00 -1.5900e+03 -6.5714e+02 3.6791e+00 -1.5900e+03 -6.2857e+02 3.6189e+00 -1.5900e+03 -6.0000e+02 3.5600e+00 -1.5900e+03 -5.7143e+02 3.5024e+00 -1.5900e+03 -5.4286e+02 3.4464e+00 -1.5900e+03 -5.1429e+02 3.3920e+00 -1.5900e+03 -4.8571e+02 3.3395e+00 -1.5900e+03 -4.5714e+02 3.2890e+00 -1.5900e+03 -4.2857e+02 3.2406e+00 -1.5900e+03 -4.0000e+02 3.1945e+00 -1.5900e+03 -3.7143e+02 3.1508e+00 -1.5900e+03 -3.4286e+02 3.1097e+00 -1.5900e+03 -3.1429e+02 3.0713e+00 -1.5900e+03 -2.8571e+02 3.0357e+00 -1.5900e+03 -2.5714e+02 3.0031e+00 -1.5900e+03 -2.2857e+02 2.9736e+00 -1.5900e+03 -2.0000e+02 2.9473e+00 -1.5900e+03 -1.7143e+02 2.9243e+00 -1.5900e+03 -1.4286e+02 2.9047e+00 -1.5900e+03 -1.1429e+02 2.8885e+00 -1.5900e+03 -8.5714e+01 2.8759e+00 -1.5900e+03 -5.7143e+01 2.8668e+00 -1.5900e+03 -2.8571e+01 2.8614e+00 -1.5900e+03 0.0000e+00 2.8596e+00 -1.5900e+03 2.8571e+01 2.8614e+00 -1.5900e+03 5.7143e+01 2.8668e+00 -1.5900e+03 8.5714e+01 2.8759e+00 -1.5900e+03 1.1429e+02 2.8885e+00 -1.5900e+03 1.4286e+02 2.9047e+00 -1.5900e+03 1.7143e+02 2.9243e+00 -1.5900e+03 2.0000e+02 2.9473e+00 -1.5900e+03 2.2857e+02 2.9736e+00 -1.5900e+03 2.5714e+02 3.0031e+00 -1.5900e+03 2.8571e+02 3.0357e+00 -1.5900e+03 3.1429e+02 3.0713e+00 -1.5900e+03 3.4286e+02 3.1097e+00 -1.5900e+03 3.7143e+02 3.1508e+00 -1.5900e+03 4.0000e+02 3.1945e+00 -1.5900e+03 4.2857e+02 3.2406e+00 -1.5900e+03 4.5714e+02 3.2890e+00 -1.5900e+03 4.8571e+02 3.3395e+00 -1.5900e+03 5.1429e+02 3.3920e+00 -1.5900e+03 5.4286e+02 3.4464e+00 -1.5900e+03 5.7143e+02 3.5024e+00 -1.5900e+03 6.0000e+02 3.5600e+00 -1.5900e+03 6.2857e+02 3.6189e+00 -1.5900e+03 6.5714e+02 3.6791e+00 -1.5900e+03 6.8571e+02 3.7403e+00 -1.5900e+03 7.1429e+02 3.8026e+00 -1.5900e+03 7.4286e+02 3.8656e+00 -1.5900e+03 7.7143e+02 3.9294e+00 -1.5900e+03 8.0000e+02 3.9937e+00 -1.5900e+03 8.2857e+02 4.0585e+00 -1.5900e+03 8.5714e+02 4.1236e+00 -1.5900e+03 8.8571e+02 4.1890e+00 -1.5900e+03 9.1429e+02 4.2546e+00 -1.5900e+03 9.4286e+02 4.3201e+00 -1.5900e+03 9.7143e+02 4.3857e+00 -1.5900e+03 1.0000e+03 4.4511e+00 -1.5900e+03 1.0286e+03 4.5164e+00 -1.5900e+03 1.0571e+03 4.5813e+00 -1.5900e+03 1.0857e+03 4.6460e+00 -1.5900e+03 1.1143e+03 4.7103e+00 -1.5900e+03 1.1429e+03 4.7741e+00 -1.5900e+03 1.1714e+03 4.8374e+00 -1.5900e+03 1.2000e+03 4.9002e+00 -1.5900e+03 1.2286e+03 4.9624e+00 -1.5900e+03 1.2571e+03 5.0240e+00 -1.5900e+03 1.2857e+03 5.0850e+00 -1.5900e+03 1.3143e+03 5.1452e+00 -1.5900e+03 1.3429e+03 5.2048e+00 -1.5900e+03 1.3714e+03 5.2637e+00 -1.5900e+03 1.4000e+03 5.3218e+00 -1.5900e+03 1.4286e+03 5.3791e+00 -1.5900e+03 1.4571e+03 5.4357e+00 -1.5900e+03 1.4857e+03 5.4916e+00 -1.5900e+03 1.5143e+03 5.5466e+00 -1.5900e+03 1.5429e+03 5.6008e+00 -1.5900e+03 1.5714e+03 5.6542e+00 -1.5900e+03 1.6000e+03 5.7069e+00 -1.5900e+03 1.6286e+03 5.7587e+00 -1.5900e+03 1.6571e+03 5.8097e+00 -1.5900e+03 1.6857e+03 5.8599e+00 -1.5900e+03 1.7143e+03 5.9093e+00 -1.5900e+03 1.7429e+03 5.9580e+00 -1.5900e+03 1.7714e+03 6.0058e+00 -1.5900e+03 1.8000e+03 6.0529e+00 -1.5900e+03 1.8286e+03 6.0992e+00 -1.5900e+03 1.8571e+03 6.1447e+00 -1.5900e+03 1.8857e+03 6.1894e+00 -1.5900e+03 1.9143e+03 6.2334e+00 -1.5900e+03 1.9429e+03 6.2767e+00 -1.5900e+03 1.9714e+03 6.3192e+00 -1.5900e+03 2.0000e+03 6.3610e+00 -1.6200e+03 -2.0000e+03 6.3955e+00 -1.6200e+03 -1.9714e+03 6.3548e+00 -1.6200e+03 -1.9429e+03 6.3134e+00 -1.6200e+03 -1.9143e+03 6.2713e+00 -1.6200e+03 -1.8857e+03 6.2285e+00 -1.6200e+03 -1.8571e+03 6.1850e+00 -1.6200e+03 -1.8286e+03 6.1408e+00 -1.6200e+03 -1.8000e+03 6.0959e+00 -1.6200e+03 -1.7714e+03 6.0503e+00 -1.6200e+03 -1.7429e+03 6.0039e+00 -1.6200e+03 -1.7143e+03 5.9568e+00 -1.6200e+03 -1.6857e+03 5.9089e+00 -1.6200e+03 -1.6571e+03 5.8603e+00 -1.6200e+03 -1.6286e+03 5.8110e+00 -1.6200e+03 -1.6000e+03 5.7609e+00 -1.6200e+03 -1.5714e+03 5.7101e+00 -1.6200e+03 -1.5429e+03 5.6586e+00 -1.6200e+03 -1.5143e+03 5.6063e+00 -1.6200e+03 -1.4857e+03 5.5532e+00 -1.6200e+03 -1.4571e+03 5.4995e+00 -1.6200e+03 -1.4286e+03 5.4451e+00 -1.6200e+03 -1.4000e+03 5.3899e+00 -1.6200e+03 -1.3714e+03 5.3341e+00 -1.6200e+03 -1.3429e+03 5.2776e+00 -1.6200e+03 -1.3143e+03 5.2205e+00 -1.6200e+03 -1.2857e+03 5.1627e+00 -1.6200e+03 -1.2571e+03 5.1044e+00 -1.6200e+03 -1.2286e+03 5.0454e+00 -1.6200e+03 -1.2000e+03 4.9860e+00 -1.6200e+03 -1.1714e+03 4.9260e+00 -1.6200e+03 -1.1429e+03 4.8656e+00 -1.6200e+03 -1.1143e+03 4.8048e+00 -1.6200e+03 -1.0857e+03 4.7436e+00 -1.6200e+03 -1.0571e+03 4.6821e+00 -1.6200e+03 -1.0286e+03 4.6204e+00 -1.6200e+03 -1.0000e+03 4.5584e+00 -1.6200e+03 -9.7143e+02 4.4963e+00 -1.6200e+03 -9.4286e+02 4.4342e+00 -1.6200e+03 -9.1429e+02 4.3721e+00 -1.6200e+03 -8.8571e+02 4.3102e+00 -1.6200e+03 -8.5714e+02 4.2484e+00 -1.6200e+03 -8.2857e+02 4.1869e+00 -1.6200e+03 -8.0000e+02 4.1258e+00 -1.6200e+03 -7.7143e+02 4.0652e+00 -1.6200e+03 -7.4286e+02 4.0052e+00 -1.6200e+03 -7.1429e+02 3.9459e+00 -1.6200e+03 -6.8571e+02 3.8874e+00 -1.6200e+03 -6.5714e+02 3.8299e+00 -1.6200e+03 -6.2857e+02 3.7735e+00 -1.6200e+03 -6.0000e+02 3.7183e+00 -1.6200e+03 -5.7143e+02 3.6645e+00 -1.6200e+03 -5.4286e+02 3.6121e+00 -1.6200e+03 -5.1429e+02 3.5613e+00 -1.6200e+03 -4.8571e+02 3.5123e+00 -1.6200e+03 -4.5714e+02 3.4652e+00 -1.6200e+03 -4.2857e+02 3.4201e+00 -1.6200e+03 -4.0000e+02 3.3771e+00 -1.6200e+03 -3.7143e+02 3.3365e+00 -1.6200e+03 -3.4286e+02 3.2982e+00 -1.6200e+03 -3.1429e+02 3.2625e+00 -1.6200e+03 -2.8571e+02 3.2295e+00 -1.6200e+03 -2.5714e+02 3.1992e+00 -1.6200e+03 -2.2857e+02 3.1719e+00 -1.6200e+03 -2.0000e+02 3.1475e+00 -1.6200e+03 -1.7143e+02 3.1261e+00 -1.6200e+03 -1.4286e+02 3.1080e+00 -1.6200e+03 -1.1429e+02 3.0930e+00 -1.6200e+03 -8.5714e+01 3.0813e+00 -1.6200e+03 -5.7143e+01 3.0729e+00 -1.6200e+03 -2.8571e+01 3.0679e+00 -1.6200e+03 0.0000e+00 3.0662e+00 -1.6200e+03 2.8571e+01 3.0679e+00 -1.6200e+03 5.7143e+01 3.0729e+00 -1.6200e+03 8.5714e+01 3.0813e+00 -1.6200e+03 1.1429e+02 3.0930e+00 -1.6200e+03 1.4286e+02 3.1080e+00 -1.6200e+03 1.7143e+02 3.1261e+00 -1.6200e+03 2.0000e+02 3.1475e+00 -1.6200e+03 2.2857e+02 3.1719e+00 -1.6200e+03 2.5714e+02 3.1992e+00 -1.6200e+03 2.8571e+02 3.2295e+00 -1.6200e+03 3.1429e+02 3.2625e+00 -1.6200e+03 3.4286e+02 3.2982e+00 -1.6200e+03 3.7143e+02 3.3365e+00 -1.6200e+03 4.0000e+02 3.3771e+00 -1.6200e+03 4.2857e+02 3.4201e+00 -1.6200e+03 4.5714e+02 3.4652e+00 -1.6200e+03 4.8571e+02 3.5123e+00 -1.6200e+03 5.1429e+02 3.5613e+00 -1.6200e+03 5.4286e+02 3.6121e+00 -1.6200e+03 5.7143e+02 3.6645e+00 -1.6200e+03 6.0000e+02 3.7183e+00 -1.6200e+03 6.2857e+02 3.7735e+00 -1.6200e+03 6.5714e+02 3.8299e+00 -1.6200e+03 6.8571e+02 3.8874e+00 -1.6200e+03 7.1429e+02 3.9459e+00 -1.6200e+03 7.4286e+02 4.0052e+00 -1.6200e+03 7.7143e+02 4.0652e+00 -1.6200e+03 8.0000e+02 4.1258e+00 -1.6200e+03 8.2857e+02 4.1869e+00 -1.6200e+03 8.5714e+02 4.2484e+00 -1.6200e+03 8.8571e+02 4.3102e+00 -1.6200e+03 9.1429e+02 4.3721e+00 -1.6200e+03 9.4286e+02 4.4342e+00 -1.6200e+03 9.7143e+02 4.4963e+00 -1.6200e+03 1.0000e+03 4.5584e+00 -1.6200e+03 1.0286e+03 4.6204e+00 -1.6200e+03 1.0571e+03 4.6821e+00 -1.6200e+03 1.0857e+03 4.7436e+00 -1.6200e+03 1.1143e+03 4.8048e+00 -1.6200e+03 1.1429e+03 4.8656e+00 -1.6200e+03 1.1714e+03 4.9260e+00 -1.6200e+03 1.2000e+03 4.9860e+00 -1.6200e+03 1.2286e+03 5.0454e+00 -1.6200e+03 1.2571e+03 5.1044e+00 -1.6200e+03 1.2857e+03 5.1627e+00 -1.6200e+03 1.3143e+03 5.2205e+00 -1.6200e+03 1.3429e+03 5.2776e+00 -1.6200e+03 1.3714e+03 5.3341e+00 -1.6200e+03 1.4000e+03 5.3899e+00 -1.6200e+03 1.4286e+03 5.4451e+00 -1.6200e+03 1.4571e+03 5.4995e+00 -1.6200e+03 1.4857e+03 5.5532e+00 -1.6200e+03 1.5143e+03 5.6063e+00 -1.6200e+03 1.5429e+03 5.6586e+00 -1.6200e+03 1.5714e+03 5.7101e+00 -1.6200e+03 1.6000e+03 5.7609e+00 -1.6200e+03 1.6286e+03 5.8110e+00 -1.6200e+03 1.6571e+03 5.8603e+00 -1.6200e+03 1.6857e+03 5.9089e+00 -1.6200e+03 1.7143e+03 5.9568e+00 -1.6200e+03 1.7429e+03 6.0039e+00 -1.6200e+03 1.7714e+03 6.0503e+00 -1.6200e+03 1.8000e+03 6.0959e+00 -1.6200e+03 1.8286e+03 6.1408e+00 -1.6200e+03 1.8571e+03 6.1850e+00 -1.6200e+03 1.8857e+03 6.2285e+00 -1.6200e+03 1.9143e+03 6.2713e+00 -1.6200e+03 1.9429e+03 6.3134e+00 -1.6200e+03 1.9714e+03 6.3548e+00 -1.6200e+03 2.0000e+03 6.3955e+00 -1.6500e+03 -2.0000e+03 6.4297e+00 -1.6500e+03 -1.9714e+03 6.3900e+00 -1.6500e+03 -1.9429e+03 6.3498e+00 -1.6500e+03 -1.9143e+03 6.3088e+00 -1.6500e+03 -1.8857e+03 6.2672e+00 -1.6500e+03 -1.8571e+03 6.2250e+00 -1.6500e+03 -1.8286e+03 6.1821e+00 -1.6500e+03 -1.8000e+03 6.1385e+00 -1.6500e+03 -1.7714e+03 6.0942e+00 -1.6500e+03 -1.7429e+03 6.0492e+00 -1.6500e+03 -1.7143e+03 6.0036e+00 -1.6500e+03 -1.6857e+03 5.9572e+00 -1.6500e+03 -1.6571e+03 5.9102e+00 -1.6500e+03 -1.6286e+03 5.8625e+00 -1.6500e+03 -1.6000e+03 5.8141e+00 -1.6500e+03 -1.5714e+03 5.7651e+00 -1.6500e+03 -1.5429e+03 5.7153e+00 -1.6500e+03 -1.5143e+03 5.6649e+00 -1.6500e+03 -1.4857e+03 5.6138e+00 -1.6500e+03 -1.4571e+03 5.5620e+00 -1.6500e+03 -1.4286e+03 5.5096e+00 -1.6500e+03 -1.4000e+03 5.4566e+00 -1.6500e+03 -1.3714e+03 5.4030e+00 -1.6500e+03 -1.3429e+03 5.3488e+00 -1.6500e+03 -1.3143e+03 5.2940e+00 -1.6500e+03 -1.2857e+03 5.2386e+00 -1.6500e+03 -1.2571e+03 5.1827e+00 -1.6500e+03 -1.2286e+03 5.1263e+00 -1.6500e+03 -1.2000e+03 5.0695e+00 -1.6500e+03 -1.1714e+03 5.0122e+00 -1.6500e+03 -1.1429e+03 4.9546e+00 -1.6500e+03 -1.1143e+03 4.8966e+00 -1.6500e+03 -1.0857e+03 4.8383e+00 -1.6500e+03 -1.0571e+03 4.7797e+00 -1.6500e+03 -1.0286e+03 4.7210e+00 -1.6500e+03 -1.0000e+03 4.6622e+00 -1.6500e+03 -9.7143e+02 4.6032e+00 -1.6500e+03 -9.4286e+02 4.5443e+00 -1.6500e+03 -9.1429e+02 4.4855e+00 -1.6500e+03 -8.8571e+02 4.4268e+00 -1.6500e+03 -8.5714e+02 4.3684e+00 -1.6500e+03 -8.2857e+02 4.3103e+00 -1.6500e+03 -8.0000e+02 4.2527e+00 -1.6500e+03 -7.7143e+02 4.1955e+00 -1.6500e+03 -7.4286e+02 4.1390e+00 -1.6500e+03 -7.1429e+02 4.0832e+00 -1.6500e+03 -6.8571e+02 4.0282e+00 -1.6500e+03 -6.5714e+02 3.9742e+00 -1.6500e+03 -6.2857e+02 3.9212e+00 -1.6500e+03 -6.0000e+02 3.8695e+00 -1.6500e+03 -5.7143e+02 3.8190e+00 -1.6500e+03 -5.4286e+02 3.7699e+00 -1.6500e+03 -5.1429e+02 3.7224e+00 -1.6500e+03 -4.8571e+02 3.6766e+00 -1.6500e+03 -4.5714e+02 3.6326e+00 -1.6500e+03 -4.2857e+02 3.5905e+00 -1.6500e+03 -4.0000e+02 3.5504e+00 -1.6500e+03 -3.7143e+02 3.5125e+00 -1.6500e+03 -3.4286e+02 3.4769e+00 -1.6500e+03 -3.1429e+02 3.4437e+00 -1.6500e+03 -2.8571e+02 3.4130e+00 -1.6500e+03 -2.5714e+02 3.3848e+00 -1.6500e+03 -2.2857e+02 3.3594e+00 -1.6500e+03 -2.0000e+02 3.3367e+00 -1.6500e+03 -1.7143e+02 3.3169e+00 -1.6500e+03 -1.4286e+02 3.3000e+00 -1.6500e+03 -1.1429e+02 3.2861e+00 -1.6500e+03 -8.5714e+01 3.2753e+00 -1.6500e+03 -5.7143e+01 3.2675e+00 -1.6500e+03 -2.8571e+01 3.2628e+00 -1.6500e+03 0.0000e+00 3.2612e+00 -1.6500e+03 2.8571e+01 3.2628e+00 -1.6500e+03 5.7143e+01 3.2675e+00 -1.6500e+03 8.5714e+01 3.2753e+00 -1.6500e+03 1.1429e+02 3.2861e+00 -1.6500e+03 1.4286e+02 3.3000e+00 -1.6500e+03 1.7143e+02 3.3169e+00 -1.6500e+03 2.0000e+02 3.3367e+00 -1.6500e+03 2.2857e+02 3.3594e+00 -1.6500e+03 2.5714e+02 3.3848e+00 -1.6500e+03 2.8571e+02 3.4130e+00 -1.6500e+03 3.1429e+02 3.4437e+00 -1.6500e+03 3.4286e+02 3.4769e+00 -1.6500e+03 3.7143e+02 3.5125e+00 -1.6500e+03 4.0000e+02 3.5504e+00 -1.6500e+03 4.2857e+02 3.5905e+00 -1.6500e+03 4.5714e+02 3.6326e+00 -1.6500e+03 4.8571e+02 3.6766e+00 -1.6500e+03 5.1429e+02 3.7224e+00 -1.6500e+03 5.4286e+02 3.7699e+00 -1.6500e+03 5.7143e+02 3.8190e+00 -1.6500e+03 6.0000e+02 3.8695e+00 -1.6500e+03 6.2857e+02 3.9212e+00 -1.6500e+03 6.5714e+02 3.9742e+00 -1.6500e+03 6.8571e+02 4.0282e+00 -1.6500e+03 7.1429e+02 4.0832e+00 -1.6500e+03 7.4286e+02 4.1390e+00 -1.6500e+03 7.7143e+02 4.1955e+00 -1.6500e+03 8.0000e+02 4.2527e+00 -1.6500e+03 8.2857e+02 4.3103e+00 -1.6500e+03 8.5714e+02 4.3684e+00 -1.6500e+03 8.8571e+02 4.4268e+00 -1.6500e+03 9.1429e+02 4.4855e+00 -1.6500e+03 9.4286e+02 4.5443e+00 -1.6500e+03 9.7143e+02 4.6032e+00 -1.6500e+03 1.0000e+03 4.6622e+00 -1.6500e+03 1.0286e+03 4.7210e+00 -1.6500e+03 1.0571e+03 4.7797e+00 -1.6500e+03 1.0857e+03 4.8383e+00 -1.6500e+03 1.1143e+03 4.8966e+00 -1.6500e+03 1.1429e+03 4.9546e+00 -1.6500e+03 1.1714e+03 5.0122e+00 -1.6500e+03 1.2000e+03 5.0695e+00 -1.6500e+03 1.2286e+03 5.1263e+00 -1.6500e+03 1.2571e+03 5.1827e+00 -1.6500e+03 1.2857e+03 5.2386e+00 -1.6500e+03 1.3143e+03 5.2940e+00 -1.6500e+03 1.3429e+03 5.3488e+00 -1.6500e+03 1.3714e+03 5.4030e+00 -1.6500e+03 1.4000e+03 5.4566e+00 -1.6500e+03 1.4286e+03 5.5096e+00 -1.6500e+03 1.4571e+03 5.5620e+00 -1.6500e+03 1.4857e+03 5.6138e+00 -1.6500e+03 1.5143e+03 5.6649e+00 -1.6500e+03 1.5429e+03 5.7153e+00 -1.6500e+03 1.5714e+03 5.7651e+00 -1.6500e+03 1.6000e+03 5.8141e+00 -1.6500e+03 1.6286e+03 5.8625e+00 -1.6500e+03 1.6571e+03 5.9102e+00 -1.6500e+03 1.6857e+03 5.9572e+00 -1.6500e+03 1.7143e+03 6.0036e+00 -1.6500e+03 1.7429e+03 6.0492e+00 -1.6500e+03 1.7714e+03 6.0942e+00 -1.6500e+03 1.8000e+03 6.1385e+00 -1.6500e+03 1.8286e+03 6.1821e+00 -1.6500e+03 1.8571e+03 6.2250e+00 -1.6500e+03 1.8857e+03 6.2672e+00 -1.6500e+03 1.9143e+03 6.3088e+00 -1.6500e+03 1.9429e+03 6.3498e+00 -1.6500e+03 1.9714e+03 6.3900e+00 -1.6500e+03 2.0000e+03 6.4297e+00 -1.6800e+03 -2.0000e+03 6.4636e+00 -1.6800e+03 -1.9714e+03 6.4250e+00 -1.6800e+03 -1.9429e+03 6.3858e+00 -1.6800e+03 -1.9143e+03 6.3460e+00 -1.6800e+03 -1.8857e+03 6.3055e+00 -1.6800e+03 -1.8571e+03 6.2645e+00 -1.6800e+03 -1.8286e+03 6.2228e+00 -1.6800e+03 -1.8000e+03 6.1805e+00 -1.6800e+03 -1.7714e+03 6.1375e+00 -1.6800e+03 -1.7429e+03 6.0940e+00 -1.6800e+03 -1.7143e+03 6.0497e+00 -1.6800e+03 -1.6857e+03 6.0049e+00 -1.6800e+03 -1.6571e+03 5.9594e+00 -1.6800e+03 -1.6286e+03 5.9132e+00 -1.6800e+03 -1.6000e+03 5.8665e+00 -1.6800e+03 -1.5714e+03 5.8191e+00 -1.6800e+03 -1.5429e+03 5.7710e+00 -1.6800e+03 -1.5143e+03 5.7224e+00 -1.6800e+03 -1.4857e+03 5.6732e+00 -1.6800e+03 -1.4571e+03 5.6233e+00 -1.6800e+03 -1.4286e+03 5.5729e+00 -1.6800e+03 -1.4000e+03 5.5219e+00 -1.6800e+03 -1.3714e+03 5.4704e+00 -1.6800e+03 -1.3429e+03 5.4183e+00 -1.6800e+03 -1.3143e+03 5.3657e+00 -1.6800e+03 -1.2857e+03 5.3126e+00 -1.6800e+03 -1.2571e+03 5.2591e+00 -1.6800e+03 -1.2286e+03 5.2051e+00 -1.6800e+03 -1.2000e+03 5.1508e+00 -1.6800e+03 -1.1714e+03 5.0960e+00 -1.6800e+03 -1.1429e+03 5.0410e+00 -1.6800e+03 -1.1143e+03 4.9857e+00 -1.6800e+03 -1.0857e+03 4.9301e+00 -1.6800e+03 -1.0571e+03 4.8744e+00 -1.6800e+03 -1.0286e+03 4.8185e+00 -1.6800e+03 -1.0000e+03 4.7625e+00 -1.6800e+03 -9.7143e+02 4.7066e+00 -1.6800e+03 -9.4286e+02 4.6507e+00 -1.6800e+03 -9.1429e+02 4.5949e+00 -1.6800e+03 -8.8571e+02 4.5393e+00 -1.6800e+03 -8.5714e+02 4.4840e+00 -1.6800e+03 -8.2857e+02 4.4291e+00 -1.6800e+03 -8.0000e+02 4.3746e+00 -1.6800e+03 -7.7143e+02 4.3207e+00 -1.6800e+03 -7.4286e+02 4.2674e+00 -1.6800e+03 -7.1429e+02 4.2148e+00 -1.6800e+03 -6.8571e+02 4.1630e+00 -1.6800e+03 -6.5714e+02 4.1122e+00 -1.6800e+03 -6.2857e+02 4.0624e+00 -1.6800e+03 -6.0000e+02 4.0138e+00 -1.6800e+03 -5.7143e+02 3.9665e+00 -1.6800e+03 -5.4286e+02 3.9205e+00 -1.6800e+03 -5.1429e+02 3.8760e+00 -1.6800e+03 -4.8571e+02 3.8331e+00 -1.6800e+03 -4.5714e+02 3.7919e+00 -1.6800e+03 -4.2857e+02 3.7526e+00 -1.6800e+03 -4.0000e+02 3.7151e+00 -1.6800e+03 -3.7143e+02 3.6798e+00 -1.6800e+03 -3.4286e+02 3.6465e+00 -1.6800e+03 -3.1429e+02 3.6155e+00 -1.6800e+03 -2.8571e+02 3.5869e+00 -1.6800e+03 -2.5714e+02 3.5607e+00 -1.6800e+03 -2.2857e+02 3.5370e+00 -1.6800e+03 -2.0000e+02 3.5159e+00 -1.6800e+03 -1.7143e+02 3.4975e+00 -1.6800e+03 -1.4286e+02 3.4818e+00 -1.6800e+03 -1.1429e+02 3.4688e+00 -1.6800e+03 -8.5714e+01 3.4587e+00 -1.6800e+03 -5.7143e+01 3.4515e+00 -1.6800e+03 -2.8571e+01 3.4471e+00 -1.6800e+03 0.0000e+00 3.4457e+00 -1.6800e+03 2.8571e+01 3.4471e+00 -1.6800e+03 5.7143e+01 3.4515e+00 -1.6800e+03 8.5714e+01 3.4587e+00 -1.6800e+03 1.1429e+02 3.4688e+00 -1.6800e+03 1.4286e+02 3.4818e+00 -1.6800e+03 1.7143e+02 3.4975e+00 -1.6800e+03 2.0000e+02 3.5159e+00 -1.6800e+03 2.2857e+02 3.5370e+00 -1.6800e+03 2.5714e+02 3.5607e+00 -1.6800e+03 2.8571e+02 3.5869e+00 -1.6800e+03 3.1429e+02 3.6155e+00 -1.6800e+03 3.4286e+02 3.6465e+00 -1.6800e+03 3.7143e+02 3.6798e+00 -1.6800e+03 4.0000e+02 3.7151e+00 -1.6800e+03 4.2857e+02 3.7526e+00 -1.6800e+03 4.5714e+02 3.7919e+00 -1.6800e+03 4.8571e+02 3.8331e+00 -1.6800e+03 5.1429e+02 3.8760e+00 -1.6800e+03 5.4286e+02 3.9205e+00 -1.6800e+03 5.7143e+02 3.9665e+00 -1.6800e+03 6.0000e+02 4.0138e+00 -1.6800e+03 6.2857e+02 4.0624e+00 -1.6800e+03 6.5714e+02 4.1122e+00 -1.6800e+03 6.8571e+02 4.1630e+00 -1.6800e+03 7.1429e+02 4.2148e+00 -1.6800e+03 7.4286e+02 4.2674e+00 -1.6800e+03 7.7143e+02 4.3207e+00 -1.6800e+03 8.0000e+02 4.3746e+00 -1.6800e+03 8.2857e+02 4.4291e+00 -1.6800e+03 8.5714e+02 4.4840e+00 -1.6800e+03 8.8571e+02 4.5393e+00 -1.6800e+03 9.1429e+02 4.5949e+00 -1.6800e+03 9.4286e+02 4.6507e+00 -1.6800e+03 9.7143e+02 4.7066e+00 -1.6800e+03 1.0000e+03 4.7625e+00 -1.6800e+03 1.0286e+03 4.8185e+00 -1.6800e+03 1.0571e+03 4.8744e+00 -1.6800e+03 1.0857e+03 4.9301e+00 -1.6800e+03 1.1143e+03 4.9857e+00 -1.6800e+03 1.1429e+03 5.0410e+00 -1.6800e+03 1.1714e+03 5.0960e+00 -1.6800e+03 1.2000e+03 5.1508e+00 -1.6800e+03 1.2286e+03 5.2051e+00 -1.6800e+03 1.2571e+03 5.2591e+00 -1.6800e+03 1.2857e+03 5.3126e+00 -1.6800e+03 1.3143e+03 5.3657e+00 -1.6800e+03 1.3429e+03 5.4183e+00 -1.6800e+03 1.3714e+03 5.4704e+00 -1.6800e+03 1.4000e+03 5.5219e+00 -1.6800e+03 1.4286e+03 5.5729e+00 -1.6800e+03 1.4571e+03 5.6233e+00 -1.6800e+03 1.4857e+03 5.6732e+00 -1.6800e+03 1.5143e+03 5.7224e+00 -1.6800e+03 1.5429e+03 5.7710e+00 -1.6800e+03 1.5714e+03 5.8191e+00 -1.6800e+03 1.6000e+03 5.8665e+00 -1.6800e+03 1.6286e+03 5.9132e+00 -1.6800e+03 1.6571e+03 5.9594e+00 -1.6800e+03 1.6857e+03 6.0049e+00 -1.6800e+03 1.7143e+03 6.0497e+00 -1.6800e+03 1.7429e+03 6.0940e+00 -1.6800e+03 1.7714e+03 6.1375e+00 -1.6800e+03 1.8000e+03 6.1805e+00 -1.6800e+03 1.8286e+03 6.2228e+00 -1.6800e+03 1.8571e+03 6.2645e+00 -1.6800e+03 1.8857e+03 6.3055e+00 -1.6800e+03 1.9143e+03 6.3460e+00 -1.6800e+03 1.9429e+03 6.3858e+00 -1.6800e+03 1.9714e+03 6.4250e+00 -1.6800e+03 2.0000e+03 6.4636e+00 -1.7100e+03 -2.0000e+03 6.4972e+00 -1.7100e+03 -1.9714e+03 6.4596e+00 -1.7100e+03 -1.9429e+03 6.4215e+00 -1.7100e+03 -1.9143e+03 6.3828e+00 -1.7100e+03 -1.8857e+03 6.3435e+00 -1.7100e+03 -1.8571e+03 6.3036e+00 -1.7100e+03 -1.8286e+03 6.2631e+00 -1.7100e+03 -1.8000e+03 6.2220e+00 -1.7100e+03 -1.7714e+03 6.1803e+00 -1.7100e+03 -1.7429e+03 6.1381e+00 -1.7100e+03 -1.7143e+03 6.0952e+00 -1.7100e+03 -1.6857e+03 6.0518e+00 -1.7100e+03 -1.6571e+03 6.0078e+00 -1.7100e+03 -1.6286e+03 5.9631e+00 -1.7100e+03 -1.6000e+03 5.9179e+00 -1.7100e+03 -1.5714e+03 5.8722e+00 -1.7100e+03 -1.5429e+03 5.8258e+00 -1.7100e+03 -1.5143e+03 5.7789e+00 -1.7100e+03 -1.4857e+03 5.7314e+00 -1.7100e+03 -1.4571e+03 5.6834e+00 -1.7100e+03 -1.4286e+03 5.6349e+00 -1.7100e+03 -1.4000e+03 5.5858e+00 -1.7100e+03 -1.3714e+03 5.5363e+00 -1.7100e+03 -1.3429e+03 5.4863e+00 -1.7100e+03 -1.3143e+03 5.4358e+00 -1.7100e+03 -1.2857e+03 5.3849e+00 -1.7100e+03 -1.2571e+03 5.3336e+00 -1.7100e+03 -1.2286e+03 5.2819e+00 -1.7100e+03 -1.2000e+03 5.2299e+00 -1.7100e+03 -1.1714e+03 5.1776e+00 -1.7100e+03 -1.1429e+03 5.1250e+00 -1.7100e+03 -1.1143e+03 5.0722e+00 -1.7100e+03 -1.0857e+03 5.0192e+00 -1.7100e+03 -1.0571e+03 4.9661e+00 -1.7100e+03 -1.0286e+03 4.9129e+00 -1.7100e+03 -1.0000e+03 4.8596e+00 -1.7100e+03 -9.7143e+02 4.8065e+00 -1.7100e+03 -9.4286e+02 4.7534e+00 -1.7100e+03 -9.1429e+02 4.7005e+00 -1.7100e+03 -8.8571e+02 4.6478e+00 -1.7100e+03 -8.5714e+02 4.5954e+00 -1.7100e+03 -8.2857e+02 4.5434e+00 -1.7100e+03 -8.0000e+02 4.4919e+00 -1.7100e+03 -7.7143e+02 4.4409e+00 -1.7100e+03 -7.4286e+02 4.3906e+00 -1.7100e+03 -7.1429e+02 4.3410e+00 -1.7100e+03 -6.8571e+02 4.2922e+00 -1.7100e+03 -6.5714e+02 4.2444e+00 -1.7100e+03 -6.2857e+02 4.1976e+00 -1.7100e+03 -6.0000e+02 4.1519e+00 -1.7100e+03 -5.7143e+02 4.1074e+00 -1.7100e+03 -5.4286e+02 4.0642e+00 -1.7100e+03 -5.1429e+02 4.0225e+00 -1.7100e+03 -4.8571e+02 3.9823e+00 -1.7100e+03 -4.5714e+02 3.9437e+00 -1.7100e+03 -4.2857e+02 3.9068e+00 -1.7100e+03 -4.0000e+02 3.8718e+00 -1.7100e+03 -3.7143e+02 3.8388e+00 -1.7100e+03 -3.4286e+02 3.8077e+00 -1.7100e+03 -3.1429e+02 3.7788e+00 -1.7100e+03 -2.8571e+02 3.7520e+00 -1.7100e+03 -2.5714e+02 3.7276e+00 -1.7100e+03 -2.2857e+02 3.7055e+00 -1.7100e+03 -2.0000e+02 3.6858e+00 -1.7100e+03 -1.7143e+02 3.6686e+00 -1.7100e+03 -1.4286e+02 3.6540e+00 -1.7100e+03 -1.1429e+02 3.6419e+00 -1.7100e+03 -8.5714e+01 3.6325e+00 -1.7100e+03 -5.7143e+01 3.6258e+00 -1.7100e+03 -2.8571e+01 3.6217e+00 -1.7100e+03 0.0000e+00 3.6204e+00 -1.7100e+03 2.8571e+01 3.6217e+00 -1.7100e+03 5.7143e+01 3.6258e+00 -1.7100e+03 8.5714e+01 3.6325e+00 -1.7100e+03 1.1429e+02 3.6419e+00 -1.7100e+03 1.4286e+02 3.6540e+00 -1.7100e+03 1.7143e+02 3.6686e+00 -1.7100e+03 2.0000e+02 3.6858e+00 -1.7100e+03 2.2857e+02 3.7055e+00 -1.7100e+03 2.5714e+02 3.7276e+00 -1.7100e+03 2.8571e+02 3.7520e+00 -1.7100e+03 3.1429e+02 3.7788e+00 -1.7100e+03 3.4286e+02 3.8077e+00 -1.7100e+03 3.7143e+02 3.8388e+00 -1.7100e+03 4.0000e+02 3.8718e+00 -1.7100e+03 4.2857e+02 3.9068e+00 -1.7100e+03 4.5714e+02 3.9437e+00 -1.7100e+03 4.8571e+02 3.9823e+00 -1.7100e+03 5.1429e+02 4.0225e+00 -1.7100e+03 5.4286e+02 4.0642e+00 -1.7100e+03 5.7143e+02 4.1074e+00 -1.7100e+03 6.0000e+02 4.1519e+00 -1.7100e+03 6.2857e+02 4.1976e+00 -1.7100e+03 6.5714e+02 4.2444e+00 -1.7100e+03 6.8571e+02 4.2922e+00 -1.7100e+03 7.1429e+02 4.3410e+00 -1.7100e+03 7.4286e+02 4.3906e+00 -1.7100e+03 7.7143e+02 4.4409e+00 -1.7100e+03 8.0000e+02 4.4919e+00 -1.7100e+03 8.2857e+02 4.5434e+00 -1.7100e+03 8.5714e+02 4.5954e+00 -1.7100e+03 8.8571e+02 4.6478e+00 -1.7100e+03 9.1429e+02 4.7005e+00 -1.7100e+03 9.4286e+02 4.7534e+00 -1.7100e+03 9.7143e+02 4.8065e+00 -1.7100e+03 1.0000e+03 4.8596e+00 -1.7100e+03 1.0286e+03 4.9129e+00 -1.7100e+03 1.0571e+03 4.9661e+00 -1.7100e+03 1.0857e+03 5.0192e+00 -1.7100e+03 1.1143e+03 5.0722e+00 -1.7100e+03 1.1429e+03 5.1250e+00 -1.7100e+03 1.1714e+03 5.1776e+00 -1.7100e+03 1.2000e+03 5.2299e+00 -1.7100e+03 1.2286e+03 5.2819e+00 -1.7100e+03 1.2571e+03 5.3336e+00 -1.7100e+03 1.2857e+03 5.3849e+00 -1.7100e+03 1.3143e+03 5.4358e+00 -1.7100e+03 1.3429e+03 5.4863e+00 -1.7100e+03 1.3714e+03 5.5363e+00 -1.7100e+03 1.4000e+03 5.5858e+00 -1.7100e+03 1.4286e+03 5.6349e+00 -1.7100e+03 1.4571e+03 5.6834e+00 -1.7100e+03 1.4857e+03 5.7314e+00 -1.7100e+03 1.5143e+03 5.7789e+00 -1.7100e+03 1.5429e+03 5.8258e+00 -1.7100e+03 1.5714e+03 5.8722e+00 -1.7100e+03 1.6000e+03 5.9179e+00 -1.7100e+03 1.6286e+03 5.9631e+00 -1.7100e+03 1.6571e+03 6.0078e+00 -1.7100e+03 1.6857e+03 6.0518e+00 -1.7100e+03 1.7143e+03 6.0952e+00 -1.7100e+03 1.7429e+03 6.1381e+00 -1.7100e+03 1.7714e+03 6.1803e+00 -1.7100e+03 1.8000e+03 6.2220e+00 -1.7100e+03 1.8286e+03 6.2631e+00 -1.7100e+03 1.8571e+03 6.3036e+00 -1.7100e+03 1.8857e+03 6.3435e+00 -1.7100e+03 1.9143e+03 6.3828e+00 -1.7100e+03 1.9429e+03 6.4215e+00 -1.7100e+03 1.9714e+03 6.4596e+00 -1.7100e+03 2.0000e+03 6.4972e+00 -1.7400e+03 -2.0000e+03 6.5305e+00 -1.7400e+03 -1.9714e+03 6.4940e+00 -1.7400e+03 -1.9429e+03 6.4568e+00 -1.7400e+03 -1.9143e+03 6.4192e+00 -1.7400e+03 -1.8857e+03 6.3810e+00 -1.7400e+03 -1.8571e+03 6.3422e+00 -1.7400e+03 -1.8286e+03 6.3029e+00 -1.7400e+03 -1.8000e+03 6.2630e+00 -1.7400e+03 -1.7714e+03 6.2226e+00 -1.7400e+03 -1.7429e+03 6.1816e+00 -1.7400e+03 -1.7143e+03 6.1401e+00 -1.7400e+03 -1.6857e+03 6.0980e+00 -1.7400e+03 -1.6571e+03 6.0554e+00 -1.7400e+03 -1.6286e+03 6.0123e+00 -1.7400e+03 -1.6000e+03 5.9686e+00 -1.7400e+03 -1.5714e+03 5.9243e+00 -1.7400e+03 -1.5429e+03 5.8796e+00 -1.7400e+03 -1.5143e+03 5.8343e+00 -1.7400e+03 -1.4857e+03 5.7886e+00 -1.7400e+03 -1.4571e+03 5.7423e+00 -1.7400e+03 -1.4286e+03 5.6956e+00 -1.7400e+03 -1.4000e+03 5.6484e+00 -1.7400e+03 -1.3714e+03 5.6008e+00 -1.7400e+03 -1.3429e+03 5.5527e+00 -1.7400e+03 -1.3143e+03 5.5042e+00 -1.7400e+03 -1.2857e+03 5.4554e+00 -1.7400e+03 -1.2571e+03 5.4062e+00 -1.7400e+03 -1.2286e+03 5.3567e+00 -1.7400e+03 -1.2000e+03 5.3069e+00 -1.7400e+03 -1.1714e+03 5.2569e+00 -1.7400e+03 -1.1429e+03 5.2066e+00 -1.7400e+03 -1.1143e+03 5.1562e+00 -1.7400e+03 -1.0857e+03 5.1056e+00 -1.7400e+03 -1.0571e+03 5.0550e+00 -1.7400e+03 -1.0286e+03 5.0043e+00 -1.7400e+03 -1.0000e+03 4.9537e+00 -1.7400e+03 -9.7143e+02 4.9031e+00 -1.7400e+03 -9.4286e+02 4.8526e+00 -1.7400e+03 -9.1429e+02 4.8024e+00 -1.7400e+03 -8.8571e+02 4.7524e+00 -1.7400e+03 -8.5714e+02 4.7028e+00 -1.7400e+03 -8.2857e+02 4.6535e+00 -1.7400e+03 -8.0000e+02 4.6048e+00 -1.7400e+03 -7.7143e+02 4.5566e+00 -1.7400e+03 -7.4286e+02 4.5090e+00 -1.7400e+03 -7.1429e+02 4.4622e+00 -1.7400e+03 -6.8571e+02 4.4162e+00 -1.7400e+03 -6.5714e+02 4.3711e+00 -1.7400e+03 -6.2857e+02 4.3270e+00 -1.7400e+03 -6.0000e+02 4.2840e+00 -1.7400e+03 -5.7143e+02 4.2421e+00 -1.7400e+03 -5.4286e+02 4.2016e+00 -1.7400e+03 -5.1429e+02 4.1624e+00 -1.7400e+03 -4.8571e+02 4.1246e+00 -1.7400e+03 -4.5714e+02 4.0884e+00 -1.7400e+03 -4.2857e+02 4.0539e+00 -1.7400e+03 -4.0000e+02 4.0211e+00 -1.7400e+03 -3.7143e+02 3.9901e+00 -1.7400e+03 -3.4286e+02 3.9611e+00 -1.7400e+03 -3.1429e+02 3.9340e+00 -1.7400e+03 -2.8571e+02 3.9090e+00 -1.7400e+03 -2.5714e+02 3.8861e+00 -1.7400e+03 -2.2857e+02 3.8655e+00 -1.7400e+03 -2.0000e+02 3.8471e+00 -1.7400e+03 -1.7143e+02 3.8311e+00 -1.7400e+03 -1.4286e+02 3.8174e+00 -1.7400e+03 -1.1429e+02 3.8062e+00 -1.7400e+03 -8.5714e+01 3.7974e+00 -1.7400e+03 -5.7143e+01 3.7911e+00 -1.7400e+03 -2.8571e+01 3.7873e+00 -1.7400e+03 0.0000e+00 3.7861e+00 -1.7400e+03 2.8571e+01 3.7873e+00 -1.7400e+03 5.7143e+01 3.7911e+00 -1.7400e+03 8.5714e+01 3.7974e+00 -1.7400e+03 1.1429e+02 3.8062e+00 -1.7400e+03 1.4286e+02 3.8174e+00 -1.7400e+03 1.7143e+02 3.8311e+00 -1.7400e+03 2.0000e+02 3.8471e+00 -1.7400e+03 2.2857e+02 3.8655e+00 -1.7400e+03 2.5714e+02 3.8861e+00 -1.7400e+03 2.8571e+02 3.9090e+00 -1.7400e+03 3.1429e+02 3.9340e+00 -1.7400e+03 3.4286e+02 3.9611e+00 -1.7400e+03 3.7143e+02 3.9901e+00 -1.7400e+03 4.0000e+02 4.0211e+00 -1.7400e+03 4.2857e+02 4.0539e+00 -1.7400e+03 4.5714e+02 4.0884e+00 -1.7400e+03 4.8571e+02 4.1246e+00 -1.7400e+03 5.1429e+02 4.1624e+00 -1.7400e+03 5.4286e+02 4.2016e+00 -1.7400e+03 5.7143e+02 4.2421e+00 -1.7400e+03 6.0000e+02 4.2840e+00 -1.7400e+03 6.2857e+02 4.3270e+00 -1.7400e+03 6.5714e+02 4.3711e+00 -1.7400e+03 6.8571e+02 4.4162e+00 -1.7400e+03 7.1429e+02 4.4622e+00 -1.7400e+03 7.4286e+02 4.5090e+00 -1.7400e+03 7.7143e+02 4.5566e+00 -1.7400e+03 8.0000e+02 4.6048e+00 -1.7400e+03 8.2857e+02 4.6535e+00 -1.7400e+03 8.5714e+02 4.7028e+00 -1.7400e+03 8.8571e+02 4.7524e+00 -1.7400e+03 9.1429e+02 4.8024e+00 -1.7400e+03 9.4286e+02 4.8526e+00 -1.7400e+03 9.7143e+02 4.9031e+00 -1.7400e+03 1.0000e+03 4.9537e+00 -1.7400e+03 1.0286e+03 5.0043e+00 -1.7400e+03 1.0571e+03 5.0550e+00 -1.7400e+03 1.0857e+03 5.1056e+00 -1.7400e+03 1.1143e+03 5.1562e+00 -1.7400e+03 1.1429e+03 5.2066e+00 -1.7400e+03 1.1714e+03 5.2569e+00 -1.7400e+03 1.2000e+03 5.3069e+00 -1.7400e+03 1.2286e+03 5.3567e+00 -1.7400e+03 1.2571e+03 5.4062e+00 -1.7400e+03 1.2857e+03 5.4554e+00 -1.7400e+03 1.3143e+03 5.5042e+00 -1.7400e+03 1.3429e+03 5.5527e+00 -1.7400e+03 1.3714e+03 5.6008e+00 -1.7400e+03 1.4000e+03 5.6484e+00 -1.7400e+03 1.4286e+03 5.6956e+00 -1.7400e+03 1.4571e+03 5.7423e+00 -1.7400e+03 1.4857e+03 5.7886e+00 -1.7400e+03 1.5143e+03 5.8343e+00 -1.7400e+03 1.5429e+03 5.8796e+00 -1.7400e+03 1.5714e+03 5.9243e+00 -1.7400e+03 1.6000e+03 5.9686e+00 -1.7400e+03 1.6286e+03 6.0123e+00 -1.7400e+03 1.6571e+03 6.0554e+00 -1.7400e+03 1.6857e+03 6.0980e+00 -1.7400e+03 1.7143e+03 6.1401e+00 -1.7400e+03 1.7429e+03 6.1816e+00 -1.7400e+03 1.7714e+03 6.2226e+00 -1.7400e+03 1.8000e+03 6.2630e+00 -1.7400e+03 1.8286e+03 6.3029e+00 -1.7400e+03 1.8571e+03 6.3422e+00 -1.7400e+03 1.8857e+03 6.3810e+00 -1.7400e+03 1.9143e+03 6.4192e+00 -1.7400e+03 1.9429e+03 6.4568e+00 -1.7400e+03 1.9714e+03 6.4940e+00 -1.7400e+03 2.0000e+03 6.5305e+00 -1.7700e+03 -2.0000e+03 6.5636e+00 -1.7700e+03 -1.9714e+03 6.5279e+00 -1.7700e+03 -1.9429e+03 6.4918e+00 -1.7700e+03 -1.9143e+03 6.4552e+00 -1.7700e+03 -1.8857e+03 6.4180e+00 -1.7700e+03 -1.8571e+03 6.3804e+00 -1.7700e+03 -1.8286e+03 6.3422e+00 -1.7700e+03 -1.8000e+03 6.3035e+00 -1.7700e+03 -1.7714e+03 6.2643e+00 -1.7700e+03 -1.7429e+03 6.2246e+00 -1.7700e+03 -1.7143e+03 6.1843e+00 -1.7700e+03 -1.6857e+03 6.1436e+00 -1.7700e+03 -1.6571e+03 6.1023e+00 -1.7700e+03 -1.6286e+03 6.0606e+00 -1.7700e+03 -1.6000e+03 6.0183e+00 -1.7700e+03 -1.5714e+03 5.9756e+00 -1.7700e+03 -1.5429e+03 5.9324e+00 -1.7700e+03 -1.5143e+03 5.8887e+00 -1.7700e+03 -1.4857e+03 5.8446e+00 -1.7700e+03 -1.4571e+03 5.8000e+00 -1.7700e+03 -1.4286e+03 5.7550e+00 -1.7700e+03 -1.4000e+03 5.7096e+00 -1.7700e+03 -1.3714e+03 5.6638e+00 -1.7700e+03 -1.3429e+03 5.6176e+00 -1.7700e+03 -1.3143e+03 5.5711e+00 -1.7700e+03 -1.2857e+03 5.5242e+00 -1.7700e+03 -1.2571e+03 5.4770e+00 -1.7700e+03 -1.2286e+03 5.4296e+00 -1.7700e+03 -1.2000e+03 5.3819e+00 -1.7700e+03 -1.1714e+03 5.3340e+00 -1.7700e+03 -1.1429e+03 5.2860e+00 -1.7700e+03 -1.1143e+03 5.2378e+00 -1.7700e+03 -1.0857e+03 5.1895e+00 -1.7700e+03 -1.0571e+03 5.1412e+00 -1.7700e+03 -1.0286e+03 5.0929e+00 -1.7700e+03 -1.0000e+03 5.0447e+00 -1.7700e+03 -9.7143e+02 4.9966e+00 -1.7700e+03 -9.4286e+02 4.9486e+00 -1.7700e+03 -9.1429e+02 4.9008e+00 -1.7700e+03 -8.8571e+02 4.8534e+00 -1.7700e+03 -8.5714e+02 4.8063e+00 -1.7700e+03 -8.2857e+02 4.7596e+00 -1.7700e+03 -8.0000e+02 4.7134e+00 -1.7700e+03 -7.7143e+02 4.6678e+00 -1.7700e+03 -7.4286e+02 4.6229e+00 -1.7700e+03 -7.1429e+02 4.5786e+00 -1.7700e+03 -6.8571e+02 4.5352e+00 -1.7700e+03 -6.5714e+02 4.4926e+00 -1.7700e+03 -6.2857e+02 4.4510e+00 -1.7700e+03 -6.0000e+02 4.4105e+00 -1.7700e+03 -5.7143e+02 4.3711e+00 -1.7700e+03 -5.4286e+02 4.3329e+00 -1.7700e+03 -5.1429e+02 4.2960e+00 -1.7700e+03 -4.8571e+02 4.2606e+00 -1.7700e+03 -4.5714e+02 4.2266e+00 -1.7700e+03 -4.2857e+02 4.1942e+00 -1.7700e+03 -4.0000e+02 4.1634e+00 -1.7700e+03 -3.7143e+02 4.1344e+00 -1.7700e+03 -3.4286e+02 4.1071e+00 -1.7700e+03 -3.1429e+02 4.0818e+00 -1.7700e+03 -2.8571e+02 4.0584e+00 -1.7700e+03 -2.5714e+02 4.0370e+00 -1.7700e+03 -2.2857e+02 4.0177e+00 -1.7700e+03 -2.0000e+02 4.0005e+00 -1.7700e+03 -1.7143e+02 3.9855e+00 -1.7700e+03 -1.4286e+02 3.9727e+00 -1.7700e+03 -1.1429e+02 3.9622e+00 -1.7700e+03 -8.5714e+01 3.9540e+00 -1.7700e+03 -5.7143e+01 3.9481e+00 -1.7700e+03 -2.8571e+01 3.9446e+00 -1.7700e+03 0.0000e+00 3.9434e+00 -1.7700e+03 2.8571e+01 3.9446e+00 -1.7700e+03 5.7143e+01 3.9481e+00 -1.7700e+03 8.5714e+01 3.9540e+00 -1.7700e+03 1.1429e+02 3.9622e+00 -1.7700e+03 1.4286e+02 3.9727e+00 -1.7700e+03 1.7143e+02 3.9855e+00 -1.7700e+03 2.0000e+02 4.0005e+00 -1.7700e+03 2.2857e+02 4.0177e+00 -1.7700e+03 2.5714e+02 4.0370e+00 -1.7700e+03 2.8571e+02 4.0584e+00 -1.7700e+03 3.1429e+02 4.0818e+00 -1.7700e+03 3.4286e+02 4.1071e+00 -1.7700e+03 3.7143e+02 4.1344e+00 -1.7700e+03 4.0000e+02 4.1634e+00 -1.7700e+03 4.2857e+02 4.1942e+00 -1.7700e+03 4.5714e+02 4.2266e+00 -1.7700e+03 4.8571e+02 4.2606e+00 -1.7700e+03 5.1429e+02 4.2960e+00 -1.7700e+03 5.4286e+02 4.3329e+00 -1.7700e+03 5.7143e+02 4.3711e+00 -1.7700e+03 6.0000e+02 4.4105e+00 -1.7700e+03 6.2857e+02 4.4510e+00 -1.7700e+03 6.5714e+02 4.4926e+00 -1.7700e+03 6.8571e+02 4.5352e+00 -1.7700e+03 7.1429e+02 4.5786e+00 -1.7700e+03 7.4286e+02 4.6229e+00 -1.7700e+03 7.7143e+02 4.6678e+00 -1.7700e+03 8.0000e+02 4.7134e+00 -1.7700e+03 8.2857e+02 4.7596e+00 -1.7700e+03 8.5714e+02 4.8063e+00 -1.7700e+03 8.8571e+02 4.8534e+00 -1.7700e+03 9.1429e+02 4.9008e+00 -1.7700e+03 9.4286e+02 4.9486e+00 -1.7700e+03 9.7143e+02 4.9966e+00 -1.7700e+03 1.0000e+03 5.0447e+00 -1.7700e+03 1.0286e+03 5.0929e+00 -1.7700e+03 1.0571e+03 5.1412e+00 -1.7700e+03 1.0857e+03 5.1895e+00 -1.7700e+03 1.1143e+03 5.2378e+00 -1.7700e+03 1.1429e+03 5.2860e+00 -1.7700e+03 1.1714e+03 5.3340e+00 -1.7700e+03 1.2000e+03 5.3819e+00 -1.7700e+03 1.2286e+03 5.4296e+00 -1.7700e+03 1.2571e+03 5.4770e+00 -1.7700e+03 1.2857e+03 5.5242e+00 -1.7700e+03 1.3143e+03 5.5711e+00 -1.7700e+03 1.3429e+03 5.6176e+00 -1.7700e+03 1.3714e+03 5.6638e+00 -1.7700e+03 1.4000e+03 5.7096e+00 -1.7700e+03 1.4286e+03 5.7550e+00 -1.7700e+03 1.4571e+03 5.8000e+00 -1.7700e+03 1.4857e+03 5.8446e+00 -1.7700e+03 1.5143e+03 5.8887e+00 -1.7700e+03 1.5429e+03 5.9324e+00 -1.7700e+03 1.5714e+03 5.9756e+00 -1.7700e+03 1.6000e+03 6.0183e+00 -1.7700e+03 1.6286e+03 6.0606e+00 -1.7700e+03 1.6571e+03 6.1023e+00 -1.7700e+03 1.6857e+03 6.1436e+00 -1.7700e+03 1.7143e+03 6.1843e+00 -1.7700e+03 1.7429e+03 6.2246e+00 -1.7700e+03 1.7714e+03 6.2643e+00 -1.7700e+03 1.8000e+03 6.3035e+00 -1.7700e+03 1.8286e+03 6.3422e+00 -1.7700e+03 1.8571e+03 6.3804e+00 -1.7700e+03 1.8857e+03 6.4180e+00 -1.7700e+03 1.9143e+03 6.4552e+00 -1.7700e+03 1.9429e+03 6.4918e+00 -1.7700e+03 1.9714e+03 6.5279e+00 -1.7700e+03 2.0000e+03 6.5636e+00 -1.8000e+03 -2.0000e+03 6.5963e+00 -1.8000e+03 -1.9714e+03 6.5616e+00 -1.8000e+03 -1.9429e+03 6.5264e+00 -1.8000e+03 -1.9143e+03 6.4908e+00 -1.8000e+03 -1.8857e+03 6.4547e+00 -1.8000e+03 -1.8571e+03 6.4181e+00 -1.8000e+03 -1.8286e+03 6.3810e+00 -1.8000e+03 -1.8000e+03 6.3434e+00 -1.8000e+03 -1.7714e+03 6.3054e+00 -1.8000e+03 -1.7429e+03 6.2669e+00 -1.8000e+03 -1.7143e+03 6.2279e+00 -1.8000e+03 -1.6857e+03 6.1884e+00 -1.8000e+03 -1.6571e+03 6.1485e+00 -1.8000e+03 -1.6286e+03 6.1081e+00 -1.8000e+03 -1.6000e+03 6.0673e+00 -1.8000e+03 -1.5714e+03 6.0260e+00 -1.8000e+03 -1.5429e+03 5.9843e+00 -1.8000e+03 -1.5143e+03 5.9421e+00 -1.8000e+03 -1.4857e+03 5.8995e+00 -1.8000e+03 -1.4571e+03 5.8566e+00 -1.8000e+03 -1.4286e+03 5.8132e+00 -1.8000e+03 -1.4000e+03 5.7695e+00 -1.8000e+03 -1.3714e+03 5.7254e+00 -1.8000e+03 -1.3429e+03 5.6810e+00 -1.8000e+03 -1.3143e+03 5.6363e+00 -1.8000e+03 -1.2857e+03 5.5913e+00 -1.8000e+03 -1.2571e+03 5.5461e+00 -1.8000e+03 -1.2286e+03 5.5006e+00 -1.8000e+03 -1.2000e+03 5.4550e+00 -1.8000e+03 -1.1714e+03 5.4091e+00 -1.8000e+03 -1.1429e+03 5.3632e+00 -1.8000e+03 -1.1143e+03 5.3171e+00 -1.8000e+03 -1.0857e+03 5.2710e+00 -1.8000e+03 -1.0571e+03 5.2249e+00 -1.8000e+03 -1.0286e+03 5.1788e+00 -1.8000e+03 -1.0000e+03 5.1329e+00 -1.8000e+03 -9.7143e+02 5.0870e+00 -1.8000e+03 -9.4286e+02 5.0414e+00 -1.8000e+03 -9.1429e+02 4.9960e+00 -1.8000e+03 -8.8571e+02 4.9509e+00 -1.8000e+03 -8.5714e+02 4.9062e+00 -1.8000e+03 -8.2857e+02 4.8619e+00 -1.8000e+03 -8.0000e+02 4.8181e+00 -1.8000e+03 -7.7143e+02 4.7749e+00 -1.8000e+03 -7.4286e+02 4.7324e+00 -1.8000e+03 -7.1429e+02 4.6905e+00 -1.8000e+03 -6.8571e+02 4.6494e+00 -1.8000e+03 -6.5714e+02 4.6092e+00 -1.8000e+03 -6.2857e+02 4.5700e+00 -1.8000e+03 -6.0000e+02 4.5318e+00 -1.8000e+03 -5.7143e+02 4.4946e+00 -1.8000e+03 -5.4286e+02 4.4587e+00 -1.8000e+03 -5.1429e+02 4.4240e+00 -1.8000e+03 -4.8571e+02 4.3906e+00 -1.8000e+03 -4.5714e+02 4.3586e+00 -1.8000e+03 -4.2857e+02 4.3282e+00 -1.8000e+03 -4.0000e+02 4.2993e+00 -1.8000e+03 -3.7143e+02 4.2720e+00 -1.8000e+03 -3.4286e+02 4.2464e+00 -1.8000e+03 -3.1429e+02 4.2226e+00 -1.8000e+03 -2.8571e+02 4.2007e+00 -1.8000e+03 -2.5714e+02 4.1807e+00 -1.8000e+03 -2.2857e+02 4.1626e+00 -1.8000e+03 -2.0000e+02 4.1465e+00 -1.8000e+03 -1.7143e+02 4.1324e+00 -1.8000e+03 -1.4286e+02 4.1205e+00 -1.8000e+03 -1.1429e+02 4.1106e+00 -1.8000e+03 -8.5714e+01 4.1030e+00 -1.8000e+03 -5.7143e+01 4.0975e+00 -1.8000e+03 -2.8571e+01 4.0942e+00 -1.8000e+03 0.0000e+00 4.0931e+00 -1.8000e+03 2.8571e+01 4.0942e+00 -1.8000e+03 5.7143e+01 4.0975e+00 -1.8000e+03 8.5714e+01 4.1030e+00 -1.8000e+03 1.1429e+02 4.1106e+00 -1.8000e+03 1.4286e+02 4.1205e+00 -1.8000e+03 1.7143e+02 4.1324e+00 -1.8000e+03 2.0000e+02 4.1465e+00 -1.8000e+03 2.2857e+02 4.1626e+00 -1.8000e+03 2.5714e+02 4.1807e+00 -1.8000e+03 2.8571e+02 4.2007e+00 -1.8000e+03 3.1429e+02 4.2226e+00 -1.8000e+03 3.4286e+02 4.2464e+00 -1.8000e+03 3.7143e+02 4.2720e+00 -1.8000e+03 4.0000e+02 4.2993e+00 -1.8000e+03 4.2857e+02 4.3282e+00 -1.8000e+03 4.5714e+02 4.3586e+00 -1.8000e+03 4.8571e+02 4.3906e+00 -1.8000e+03 5.1429e+02 4.4240e+00 -1.8000e+03 5.4286e+02 4.4587e+00 -1.8000e+03 5.7143e+02 4.4946e+00 -1.8000e+03 6.0000e+02 4.5318e+00 -1.8000e+03 6.2857e+02 4.5700e+00 -1.8000e+03 6.5714e+02 4.6092e+00 -1.8000e+03 6.8571e+02 4.6494e+00 -1.8000e+03 7.1429e+02 4.6905e+00 -1.8000e+03 7.4286e+02 4.7324e+00 -1.8000e+03 7.7143e+02 4.7749e+00 -1.8000e+03 8.0000e+02 4.8181e+00 -1.8000e+03 8.2857e+02 4.8619e+00 -1.8000e+03 8.5714e+02 4.9062e+00 -1.8000e+03 8.8571e+02 4.9509e+00 -1.8000e+03 9.1429e+02 4.9960e+00 -1.8000e+03 9.4286e+02 5.0414e+00 -1.8000e+03 9.7143e+02 5.0870e+00 -1.8000e+03 1.0000e+03 5.1329e+00 -1.8000e+03 1.0286e+03 5.1788e+00 -1.8000e+03 1.0571e+03 5.2249e+00 -1.8000e+03 1.0857e+03 5.2710e+00 -1.8000e+03 1.1143e+03 5.3171e+00 -1.8000e+03 1.1429e+03 5.3632e+00 -1.8000e+03 1.1714e+03 5.4091e+00 -1.8000e+03 1.2000e+03 5.4550e+00 -1.8000e+03 1.2286e+03 5.5006e+00 -1.8000e+03 1.2571e+03 5.5461e+00 -1.8000e+03 1.2857e+03 5.5913e+00 -1.8000e+03 1.3143e+03 5.6363e+00 -1.8000e+03 1.3429e+03 5.6810e+00 -1.8000e+03 1.3714e+03 5.7254e+00 -1.8000e+03 1.4000e+03 5.7695e+00 -1.8000e+03 1.4286e+03 5.8132e+00 -1.8000e+03 1.4571e+03 5.8566e+00 -1.8000e+03 1.4857e+03 5.8995e+00 -1.8000e+03 1.5143e+03 5.9421e+00 -1.8000e+03 1.5429e+03 5.9843e+00 -1.8000e+03 1.5714e+03 6.0260e+00 -1.8000e+03 1.6000e+03 6.0673e+00 -1.8000e+03 1.6286e+03 6.1081e+00 -1.8000e+03 1.6571e+03 6.1485e+00 -1.8000e+03 1.6857e+03 6.1884e+00 -1.8000e+03 1.7143e+03 6.2279e+00 -1.8000e+03 1.7429e+03 6.2669e+00 -1.8000e+03 1.7714e+03 6.3054e+00 -1.8000e+03 1.8000e+03 6.3434e+00 -1.8000e+03 1.8286e+03 6.3810e+00 -1.8000e+03 1.8571e+03 6.4181e+00 -1.8000e+03 1.8857e+03 6.4547e+00 -1.8000e+03 1.9143e+03 6.4908e+00 -1.8000e+03 1.9429e+03 6.5264e+00 -1.8000e+03 1.9714e+03 6.5616e+00 -1.8000e+03 2.0000e+03 6.5963e+00 -1.8300e+03 -2.0000e+03 6.6287e+00 -1.8300e+03 -1.9714e+03 6.5949e+00 -1.8300e+03 -1.9429e+03 6.5607e+00 -1.8300e+03 -1.9143e+03 6.5260e+00 -1.8300e+03 -1.8857e+03 6.4909e+00 -1.8300e+03 -1.8571e+03 6.4553e+00 -1.8300e+03 -1.8286e+03 6.4193e+00 -1.8300e+03 -1.8000e+03 6.3829e+00 -1.8300e+03 -1.7714e+03 6.3460e+00 -1.8300e+03 -1.7429e+03 6.3086e+00 -1.8300e+03 -1.7143e+03 6.2708e+00 -1.8300e+03 -1.6857e+03 6.2326e+00 -1.8300e+03 -1.6571e+03 6.1939e+00 -1.8300e+03 -1.6286e+03 6.1549e+00 -1.8300e+03 -1.6000e+03 6.1154e+00 -1.8300e+03 -1.5714e+03 6.0755e+00 -1.8300e+03 -1.5429e+03 6.0352e+00 -1.8300e+03 -1.5143e+03 5.9945e+00 -1.8300e+03 -1.4857e+03 5.9534e+00 -1.8300e+03 -1.4571e+03 5.9120e+00 -1.8300e+03 -1.4286e+03 5.8702e+00 -1.8300e+03 -1.4000e+03 5.8281e+00 -1.8300e+03 -1.3714e+03 5.7857e+00 -1.8300e+03 -1.3429e+03 5.7430e+00 -1.8300e+03 -1.3143e+03 5.7001e+00 -1.8300e+03 -1.2857e+03 5.6569e+00 -1.8300e+03 -1.2571e+03 5.6135e+00 -1.8300e+03 -1.2286e+03 5.5698e+00 -1.8300e+03 -1.2000e+03 5.5261e+00 -1.8300e+03 -1.1714e+03 5.4822e+00 -1.8300e+03 -1.1429e+03 5.4382e+00 -1.8300e+03 -1.1143e+03 5.3942e+00 -1.8300e+03 -1.0857e+03 5.3501e+00 -1.8300e+03 -1.0571e+03 5.3061e+00 -1.8300e+03 -1.0286e+03 5.2621e+00 -1.8300e+03 -1.0000e+03 5.2183e+00 -1.8300e+03 -9.7143e+02 5.1746e+00 -1.8300e+03 -9.4286e+02 5.1312e+00 -1.8300e+03 -9.1429e+02 5.0880e+00 -1.8300e+03 -8.8571e+02 5.0451e+00 -1.8300e+03 -8.5714e+02 5.0026e+00 -1.8300e+03 -8.2857e+02 4.9606e+00 -1.8300e+03 -8.0000e+02 4.9191e+00 -1.8300e+03 -7.7143e+02 4.8781e+00 -1.8300e+03 -7.4286e+02 4.8378e+00 -1.8300e+03 -7.1429e+02 4.7981e+00 -1.8300e+03 -6.8571e+02 4.7593e+00 -1.8300e+03 -6.5714e+02 4.7213e+00 -1.8300e+03 -6.2857e+02 4.6842e+00 -1.8300e+03 -6.0000e+02 4.6481e+00 -1.8300e+03 -5.7143e+02 4.6130e+00 -1.8300e+03 -5.4286e+02 4.5791e+00 -1.8300e+03 -5.1429e+02 4.5464e+00 -1.8300e+03 -4.8571e+02 4.5150e+00 -1.8300e+03 -4.5714e+02 4.4849e+00 -1.8300e+03 -4.2857e+02 4.4562e+00 -1.8300e+03 -4.0000e+02 4.4291e+00 -1.8300e+03 -3.7143e+02 4.4034e+00 -1.8300e+03 -3.4286e+02 4.3794e+00 -1.8300e+03 -3.1429e+02 4.3571e+00 -1.8300e+03 -2.8571e+02 4.3365e+00 -1.8300e+03 -2.5714e+02 4.3176e+00 -1.8300e+03 -2.2857e+02 4.3007e+00 -1.8300e+03 -2.0000e+02 4.2856e+00 -1.8300e+03 -1.7143e+02 4.2724e+00 -1.8300e+03 -1.4286e+02 4.2612e+00 -1.8300e+03 -1.1429e+02 4.2520e+00 -1.8300e+03 -8.5714e+01 4.2448e+00 -1.8300e+03 -5.7143e+01 4.2396e+00 -1.8300e+03 -2.8571e+01 4.2365e+00 -1.8300e+03 0.0000e+00 4.2355e+00 -1.8300e+03 2.8571e+01 4.2365e+00 -1.8300e+03 5.7143e+01 4.2396e+00 -1.8300e+03 8.5714e+01 4.2448e+00 -1.8300e+03 1.1429e+02 4.2520e+00 -1.8300e+03 1.4286e+02 4.2612e+00 -1.8300e+03 1.7143e+02 4.2724e+00 -1.8300e+03 2.0000e+02 4.2856e+00 -1.8300e+03 2.2857e+02 4.3007e+00 -1.8300e+03 2.5714e+02 4.3176e+00 -1.8300e+03 2.8571e+02 4.3365e+00 -1.8300e+03 3.1429e+02 4.3571e+00 -1.8300e+03 3.4286e+02 4.3794e+00 -1.8300e+03 3.7143e+02 4.4034e+00 -1.8300e+03 4.0000e+02 4.4291e+00 -1.8300e+03 4.2857e+02 4.4562e+00 -1.8300e+03 4.5714e+02 4.4849e+00 -1.8300e+03 4.8571e+02 4.5150e+00 -1.8300e+03 5.1429e+02 4.5464e+00 -1.8300e+03 5.4286e+02 4.5791e+00 -1.8300e+03 5.7143e+02 4.6130e+00 -1.8300e+03 6.0000e+02 4.6481e+00 -1.8300e+03 6.2857e+02 4.6842e+00 -1.8300e+03 6.5714e+02 4.7213e+00 -1.8300e+03 6.8571e+02 4.7593e+00 -1.8300e+03 7.1429e+02 4.7981e+00 -1.8300e+03 7.4286e+02 4.8378e+00 -1.8300e+03 7.7143e+02 4.8781e+00 -1.8300e+03 8.0000e+02 4.9191e+00 -1.8300e+03 8.2857e+02 4.9606e+00 -1.8300e+03 8.5714e+02 5.0026e+00 -1.8300e+03 8.8571e+02 5.0451e+00 -1.8300e+03 9.1429e+02 5.0880e+00 -1.8300e+03 9.4286e+02 5.1312e+00 -1.8300e+03 9.7143e+02 5.1746e+00 -1.8300e+03 1.0000e+03 5.2183e+00 -1.8300e+03 1.0286e+03 5.2621e+00 -1.8300e+03 1.0571e+03 5.3061e+00 -1.8300e+03 1.0857e+03 5.3501e+00 -1.8300e+03 1.1143e+03 5.3942e+00 -1.8300e+03 1.1429e+03 5.4382e+00 -1.8300e+03 1.1714e+03 5.4822e+00 -1.8300e+03 1.2000e+03 5.5261e+00 -1.8300e+03 1.2286e+03 5.5698e+00 -1.8300e+03 1.2571e+03 5.6135e+00 -1.8300e+03 1.2857e+03 5.6569e+00 -1.8300e+03 1.3143e+03 5.7001e+00 -1.8300e+03 1.3429e+03 5.7430e+00 -1.8300e+03 1.3714e+03 5.7857e+00 -1.8300e+03 1.4000e+03 5.8281e+00 -1.8300e+03 1.4286e+03 5.8702e+00 -1.8300e+03 1.4571e+03 5.9120e+00 -1.8300e+03 1.4857e+03 5.9534e+00 -1.8300e+03 1.5143e+03 5.9945e+00 -1.8300e+03 1.5429e+03 6.0352e+00 -1.8300e+03 1.5714e+03 6.0755e+00 -1.8300e+03 1.6000e+03 6.1154e+00 -1.8300e+03 1.6286e+03 6.1549e+00 -1.8300e+03 1.6571e+03 6.1939e+00 -1.8300e+03 1.6857e+03 6.2326e+00 -1.8300e+03 1.7143e+03 6.2708e+00 -1.8300e+03 1.7429e+03 6.3086e+00 -1.8300e+03 1.7714e+03 6.3460e+00 -1.8300e+03 1.8000e+03 6.3829e+00 -1.8300e+03 1.8286e+03 6.4193e+00 -1.8300e+03 1.8571e+03 6.4553e+00 -1.8300e+03 1.8857e+03 6.4909e+00 -1.8300e+03 1.9143e+03 6.5260e+00 -1.8300e+03 1.9429e+03 6.5607e+00 -1.8300e+03 1.9714e+03 6.5949e+00 -1.8300e+03 2.0000e+03 6.6287e+00 -1.8600e+03 -2.0000e+03 6.6607e+00 -1.8600e+03 -1.9714e+03 6.6278e+00 -1.8600e+03 -1.9429e+03 6.5945e+00 -1.8600e+03 -1.9143e+03 6.5608e+00 -1.8600e+03 -1.8857e+03 6.5267e+00 -1.8600e+03 -1.8571e+03 6.4921e+00 -1.8600e+03 -1.8286e+03 6.4572e+00 -1.8600e+03 -1.8000e+03 6.4218e+00 -1.8600e+03 -1.7714e+03 6.3860e+00 -1.8600e+03 -1.7429e+03 6.3497e+00 -1.8600e+03 -1.7143e+03 6.3131e+00 -1.8600e+03 -1.6857e+03 6.2761e+00 -1.8600e+03 -1.6571e+03 6.2386e+00 -1.8600e+03 -1.6286e+03 6.2008e+00 -1.8600e+03 -1.6000e+03 6.1626e+00 -1.8600e+03 -1.5714e+03 6.1241e+00 -1.8600e+03 -1.5429e+03 6.0851e+00 -1.8600e+03 -1.5143e+03 6.0459e+00 -1.8600e+03 -1.4857e+03 6.0062e+00 -1.8600e+03 -1.4571e+03 5.9663e+00 -1.8600e+03 -1.4286e+03 5.9260e+00 -1.8600e+03 -1.4000e+03 5.8855e+00 -1.8600e+03 -1.3714e+03 5.8447e+00 -1.8600e+03 -1.3429e+03 5.8036e+00 -1.8600e+03 -1.3143e+03 5.7623e+00 -1.8600e+03 -1.2857e+03 5.7208e+00 -1.8600e+03 -1.2571e+03 5.6791e+00 -1.8600e+03 -1.2286e+03 5.6373e+00 -1.8600e+03 -1.2000e+03 5.5954e+00 -1.8600e+03 -1.1714e+03 5.5533e+00 -1.8600e+03 -1.1429e+03 5.5112e+00 -1.8600e+03 -1.1143e+03 5.4691e+00 -1.8600e+03 -1.0857e+03 5.4270e+00 -1.8600e+03 -1.0571e+03 5.3849e+00 -1.8600e+03 -1.0286e+03 5.3429e+00 -1.8600e+03 -1.0000e+03 5.3011e+00 -1.8600e+03 -9.7143e+02 5.2595e+00 -1.8600e+03 -9.4286e+02 5.2180e+00 -1.8600e+03 -9.1429e+02 5.1769e+00 -1.8600e+03 -8.8571e+02 5.1361e+00 -1.8600e+03 -8.5714e+02 5.0958e+00 -1.8600e+03 -8.2857e+02 5.0558e+00 -1.8600e+03 -8.0000e+02 5.0164e+00 -1.8600e+03 -7.7143e+02 4.9775e+00 -1.8600e+03 -7.4286e+02 4.9393e+00 -1.8600e+03 -7.1429e+02 4.9017e+00 -1.8600e+03 -6.8571e+02 4.8650e+00 -1.8600e+03 -6.5714e+02 4.8290e+00 -1.8600e+03 -6.2857e+02 4.7939e+00 -1.8600e+03 -6.0000e+02 4.7598e+00 -1.8600e+03 -5.7143e+02 4.7267e+00 -1.8600e+03 -5.4286e+02 4.6947e+00 -1.8600e+03 -5.1429e+02 4.6638e+00 -1.8600e+03 -4.8571e+02 4.6342e+00 -1.8600e+03 -4.5714e+02 4.6058e+00 -1.8600e+03 -4.2857e+02 4.5788e+00 -1.8600e+03 -4.0000e+02 4.5532e+00 -1.8600e+03 -3.7143e+02 4.5291e+00 -1.8600e+03 -3.4286e+02 4.5065e+00 -1.8600e+03 -3.1429e+02 4.4855e+00 -1.8600e+03 -2.8571e+02 4.4661e+00 -1.8600e+03 -2.5714e+02 4.4484e+00 -1.8600e+03 -2.2857e+02 4.4325e+00 -1.8600e+03 -2.0000e+02 4.4183e+00 -1.8600e+03 -1.7143e+02 4.4059e+00 -1.8600e+03 -1.4286e+02 4.3954e+00 -1.8600e+03 -1.1429e+02 4.3867e+00 -1.8600e+03 -8.5714e+01 4.3800e+00 -1.8600e+03 -5.7143e+01 4.3752e+00 -1.8600e+03 -2.8571e+01 4.3723e+00 -1.8600e+03 0.0000e+00 4.3713e+00 -1.8600e+03 2.8571e+01 4.3723e+00 -1.8600e+03 5.7143e+01 4.3752e+00 -1.8600e+03 8.5714e+01 4.3800e+00 -1.8600e+03 1.1429e+02 4.3867e+00 -1.8600e+03 1.4286e+02 4.3954e+00 -1.8600e+03 1.7143e+02 4.4059e+00 -1.8600e+03 2.0000e+02 4.4183e+00 -1.8600e+03 2.2857e+02 4.4325e+00 -1.8600e+03 2.5714e+02 4.4484e+00 -1.8600e+03 2.8571e+02 4.4661e+00 -1.8600e+03 3.1429e+02 4.4855e+00 -1.8600e+03 3.4286e+02 4.5065e+00 -1.8600e+03 3.7143e+02 4.5291e+00 -1.8600e+03 4.0000e+02 4.5532e+00 -1.8600e+03 4.2857e+02 4.5788e+00 -1.8600e+03 4.5714e+02 4.6058e+00 -1.8600e+03 4.8571e+02 4.6342e+00 -1.8600e+03 5.1429e+02 4.6638e+00 -1.8600e+03 5.4286e+02 4.6947e+00 -1.8600e+03 5.7143e+02 4.7267e+00 -1.8600e+03 6.0000e+02 4.7598e+00 -1.8600e+03 6.2857e+02 4.7939e+00 -1.8600e+03 6.5714e+02 4.8290e+00 -1.8600e+03 6.8571e+02 4.8650e+00 -1.8600e+03 7.1429e+02 4.9017e+00 -1.8600e+03 7.4286e+02 4.9393e+00 -1.8600e+03 7.7143e+02 4.9775e+00 -1.8600e+03 8.0000e+02 5.0164e+00 -1.8600e+03 8.2857e+02 5.0558e+00 -1.8600e+03 8.5714e+02 5.0958e+00 -1.8600e+03 8.8571e+02 5.1361e+00 -1.8600e+03 9.1429e+02 5.1769e+00 -1.8600e+03 9.4286e+02 5.2180e+00 -1.8600e+03 9.7143e+02 5.2595e+00 -1.8600e+03 1.0000e+03 5.3011e+00 -1.8600e+03 1.0286e+03 5.3429e+00 -1.8600e+03 1.0571e+03 5.3849e+00 -1.8600e+03 1.0857e+03 5.4270e+00 -1.8600e+03 1.1143e+03 5.4691e+00 -1.8600e+03 1.1429e+03 5.5112e+00 -1.8600e+03 1.1714e+03 5.5533e+00 -1.8600e+03 1.2000e+03 5.5954e+00 -1.8600e+03 1.2286e+03 5.6373e+00 -1.8600e+03 1.2571e+03 5.6791e+00 -1.8600e+03 1.2857e+03 5.7208e+00 -1.8600e+03 1.3143e+03 5.7623e+00 -1.8600e+03 1.3429e+03 5.8036e+00 -1.8600e+03 1.3714e+03 5.8447e+00 -1.8600e+03 1.4000e+03 5.8855e+00 -1.8600e+03 1.4286e+03 5.9260e+00 -1.8600e+03 1.4571e+03 5.9663e+00 -1.8600e+03 1.4857e+03 6.0062e+00 -1.8600e+03 1.5143e+03 6.0459e+00 -1.8600e+03 1.5429e+03 6.0851e+00 -1.8600e+03 1.5714e+03 6.1241e+00 -1.8600e+03 1.6000e+03 6.1626e+00 -1.8600e+03 1.6286e+03 6.2008e+00 -1.8600e+03 1.6571e+03 6.2386e+00 -1.8600e+03 1.6857e+03 6.2761e+00 -1.8600e+03 1.7143e+03 6.3131e+00 -1.8600e+03 1.7429e+03 6.3497e+00 -1.8600e+03 1.7714e+03 6.3860e+00 -1.8600e+03 1.8000e+03 6.4218e+00 -1.8600e+03 1.8286e+03 6.4572e+00 -1.8600e+03 1.8571e+03 6.4921e+00 -1.8600e+03 1.8857e+03 6.5267e+00 -1.8600e+03 1.9143e+03 6.5608e+00 -1.8600e+03 1.9429e+03 6.5945e+00 -1.8600e+03 1.9714e+03 6.6278e+00 -1.8600e+03 2.0000e+03 6.6607e+00 -1.8900e+03 -2.0000e+03 6.6924e+00 -1.8900e+03 -1.9714e+03 6.6604e+00 -1.8900e+03 -1.9429e+03 6.6280e+00 -1.8900e+03 -1.9143e+03 6.5952e+00 -1.8900e+03 -1.8857e+03 6.5620e+00 -1.8900e+03 -1.8571e+03 6.5285e+00 -1.8900e+03 -1.8286e+03 6.4945e+00 -1.8900e+03 -1.8000e+03 6.4601e+00 -1.8900e+03 -1.7714e+03 6.4254e+00 -1.8900e+03 -1.7429e+03 6.3903e+00 -1.8900e+03 -1.7143e+03 6.3547e+00 -1.8900e+03 -1.6857e+03 6.3189e+00 -1.8900e+03 -1.6571e+03 6.2826e+00 -1.8900e+03 -1.6286e+03 6.2460e+00 -1.8900e+03 -1.6000e+03 6.2091e+00 -1.8900e+03 -1.5714e+03 6.1718e+00 -1.8900e+03 -1.5429e+03 6.1342e+00 -1.8900e+03 -1.5143e+03 6.0962e+00 -1.8900e+03 -1.4857e+03 6.0580e+00 -1.8900e+03 -1.4571e+03 6.0195e+00 -1.8900e+03 -1.4286e+03 5.9807e+00 -1.8900e+03 -1.4000e+03 5.9416e+00 -1.8900e+03 -1.3714e+03 5.9023e+00 -1.8900e+03 -1.3429e+03 5.8628e+00 -1.8900e+03 -1.3143e+03 5.8231e+00 -1.8900e+03 -1.2857e+03 5.7833e+00 -1.8900e+03 -1.2571e+03 5.7432e+00 -1.8900e+03 -1.2286e+03 5.7031e+00 -1.8900e+03 -1.2000e+03 5.6628e+00 -1.8900e+03 -1.1714e+03 5.6225e+00 -1.8900e+03 -1.1429e+03 5.5822e+00 -1.8900e+03 -1.1143e+03 5.5419e+00 -1.8900e+03 -1.0857e+03 5.5016e+00 -1.8900e+03 -1.0571e+03 5.4614e+00 -1.8900e+03 -1.0286e+03 5.4213e+00 -1.8900e+03 -1.0000e+03 5.3814e+00 -1.8900e+03 -9.7143e+02 5.3416e+00 -1.8900e+03 -9.4286e+02 5.3022e+00 -1.8900e+03 -9.1429e+02 5.2630e+00 -1.8900e+03 -8.8571e+02 5.2242e+00 -1.8900e+03 -8.5714e+02 5.1857e+00 -1.8900e+03 -8.2857e+02 5.1478e+00 -1.8900e+03 -8.0000e+02 5.1103e+00 -1.8900e+03 -7.7143e+02 5.0734e+00 -1.8900e+03 -7.4286e+02 5.0371e+00 -1.8900e+03 -7.1429e+02 5.0015e+00 -1.8900e+03 -6.8571e+02 4.9666e+00 -1.8900e+03 -6.5714e+02 4.9326e+00 -1.8900e+03 -6.2857e+02 4.8994e+00 -1.8900e+03 -6.0000e+02 4.8671e+00 -1.8900e+03 -5.7143e+02 4.8358e+00 -1.8900e+03 -5.4286e+02 4.8055e+00 -1.8900e+03 -5.1429e+02 4.7764e+00 -1.8900e+03 -4.8571e+02 4.7484e+00 -1.8900e+03 -4.5714e+02 4.7216e+00 -1.8900e+03 -4.2857e+02 4.6962e+00 -1.8900e+03 -4.0000e+02 4.6720e+00 -1.8900e+03 -3.7143e+02 4.6493e+00 -1.8900e+03 -3.4286e+02 4.6280e+00 -1.8900e+03 -3.1429e+02 4.6082e+00 -1.8900e+03 -2.8571e+02 4.5900e+00 -1.8900e+03 -2.5714e+02 4.5734e+00 -1.8900e+03 -2.2857e+02 4.5583e+00 -1.8900e+03 -2.0000e+02 4.5450e+00 -1.8900e+03 -1.7143e+02 4.5334e+00 -1.8900e+03 -1.4286e+02 4.5235e+00 -1.8900e+03 -1.1429e+02 4.5154e+00 -1.8900e+03 -8.5714e+01 4.5090e+00 -1.8900e+03 -5.7143e+01 4.5045e+00 -1.8900e+03 -2.8571e+01 4.5018e+00 -1.8900e+03 0.0000e+00 4.5008e+00 -1.8900e+03 2.8571e+01 4.5018e+00 -1.8900e+03 5.7143e+01 4.5045e+00 -1.8900e+03 8.5714e+01 4.5090e+00 -1.8900e+03 1.1429e+02 4.5154e+00 -1.8900e+03 1.4286e+02 4.5235e+00 -1.8900e+03 1.7143e+02 4.5334e+00 -1.8900e+03 2.0000e+02 4.5450e+00 -1.8900e+03 2.2857e+02 4.5583e+00 -1.8900e+03 2.5714e+02 4.5734e+00 -1.8900e+03 2.8571e+02 4.5900e+00 -1.8900e+03 3.1429e+02 4.6082e+00 -1.8900e+03 3.4286e+02 4.6280e+00 -1.8900e+03 3.7143e+02 4.6493e+00 -1.8900e+03 4.0000e+02 4.6720e+00 -1.8900e+03 4.2857e+02 4.6962e+00 -1.8900e+03 4.5714e+02 4.7216e+00 -1.8900e+03 4.8571e+02 4.7484e+00 -1.8900e+03 5.1429e+02 4.7764e+00 -1.8900e+03 5.4286e+02 4.8055e+00 -1.8900e+03 5.7143e+02 4.8358e+00 -1.8900e+03 6.0000e+02 4.8671e+00 -1.8900e+03 6.2857e+02 4.8994e+00 -1.8900e+03 6.5714e+02 4.9326e+00 -1.8900e+03 6.8571e+02 4.9666e+00 -1.8900e+03 7.1429e+02 5.0015e+00 -1.8900e+03 7.4286e+02 5.0371e+00 -1.8900e+03 7.7143e+02 5.0734e+00 -1.8900e+03 8.0000e+02 5.1103e+00 -1.8900e+03 8.2857e+02 5.1478e+00 -1.8900e+03 8.5714e+02 5.1857e+00 -1.8900e+03 8.8571e+02 5.2242e+00 -1.8900e+03 9.1429e+02 5.2630e+00 -1.8900e+03 9.4286e+02 5.3022e+00 -1.8900e+03 9.7143e+02 5.3416e+00 -1.8900e+03 1.0000e+03 5.3814e+00 -1.8900e+03 1.0286e+03 5.4213e+00 -1.8900e+03 1.0571e+03 5.4614e+00 -1.8900e+03 1.0857e+03 5.5016e+00 -1.8900e+03 1.1143e+03 5.5419e+00 -1.8900e+03 1.1429e+03 5.5822e+00 -1.8900e+03 1.1714e+03 5.6225e+00 -1.8900e+03 1.2000e+03 5.6628e+00 -1.8900e+03 1.2286e+03 5.7031e+00 -1.8900e+03 1.2571e+03 5.7432e+00 -1.8900e+03 1.2857e+03 5.7833e+00 -1.8900e+03 1.3143e+03 5.8231e+00 -1.8900e+03 1.3429e+03 5.8628e+00 -1.8900e+03 1.3714e+03 5.9023e+00 -1.8900e+03 1.4000e+03 5.9416e+00 -1.8900e+03 1.4286e+03 5.9807e+00 -1.8900e+03 1.4571e+03 6.0195e+00 -1.8900e+03 1.4857e+03 6.0580e+00 -1.8900e+03 1.5143e+03 6.0962e+00 -1.8900e+03 1.5429e+03 6.1342e+00 -1.8900e+03 1.5714e+03 6.1718e+00 -1.8900e+03 1.6000e+03 6.2091e+00 -1.8900e+03 1.6286e+03 6.2460e+00 -1.8900e+03 1.6571e+03 6.2826e+00 -1.8900e+03 1.6857e+03 6.3189e+00 -1.8900e+03 1.7143e+03 6.3547e+00 -1.8900e+03 1.7429e+03 6.3903e+00 -1.8900e+03 1.7714e+03 6.4254e+00 -1.8900e+03 1.8000e+03 6.4601e+00 -1.8900e+03 1.8286e+03 6.4945e+00 -1.8900e+03 1.8571e+03 6.5285e+00 -1.8900e+03 1.8857e+03 6.5620e+00 -1.8900e+03 1.9143e+03 6.5952e+00 -1.8900e+03 1.9429e+03 6.6280e+00 -1.8900e+03 1.9714e+03 6.6604e+00 -1.8900e+03 2.0000e+03 6.6924e+00 -1.9200e+03 -2.0000e+03 6.7238e+00 -1.9200e+03 -1.9714e+03 6.6927e+00 -1.9200e+03 -1.9429e+03 6.6611e+00 -1.9200e+03 -1.9143e+03 6.6292e+00 -1.9200e+03 -1.8857e+03 6.5970e+00 -1.9200e+03 -1.8571e+03 6.5643e+00 -1.9200e+03 -1.8286e+03 6.5313e+00 -1.9200e+03 -1.8000e+03 6.4980e+00 -1.9200e+03 -1.7714e+03 6.4642e+00 -1.9200e+03 -1.7429e+03 6.4302e+00 -1.9200e+03 -1.7143e+03 6.3957e+00 -1.9200e+03 -1.6857e+03 6.3610e+00 -1.9200e+03 -1.6571e+03 6.3259e+00 -1.9200e+03 -1.6286e+03 6.2905e+00 -1.9200e+03 -1.6000e+03 6.2547e+00 -1.9200e+03 -1.5714e+03 6.2187e+00 -1.9200e+03 -1.5429e+03 6.1823e+00 -1.9200e+03 -1.5143e+03 6.1457e+00 -1.9200e+03 -1.4857e+03 6.1088e+00 -1.9200e+03 -1.4571e+03 6.0716e+00 -1.9200e+03 -1.4286e+03 6.0342e+00 -1.9200e+03 -1.4000e+03 5.9966e+00 -1.9200e+03 -1.3714e+03 5.9587e+00 -1.9200e+03 -1.3429e+03 5.9207e+00 -1.9200e+03 -1.3143e+03 5.8825e+00 -1.9200e+03 -1.2857e+03 5.8442e+00 -1.9200e+03 -1.2571e+03 5.8057e+00 -1.9200e+03 -1.2286e+03 5.7672e+00 -1.9200e+03 -1.2000e+03 5.7286e+00 -1.9200e+03 -1.1714e+03 5.6899e+00 -1.9200e+03 -1.1429e+03 5.6513e+00 -1.9200e+03 -1.1143e+03 5.6127e+00 -1.9200e+03 -1.0857e+03 5.5741e+00 -1.9200e+03 -1.0571e+03 5.5357e+00 -1.9200e+03 -1.0286e+03 5.4974e+00 -1.9200e+03 -1.0000e+03 5.4592e+00 -1.9200e+03 -9.7143e+02 5.4213e+00 -1.9200e+03 -9.4286e+02 5.3837e+00 -1.9200e+03 -9.1429e+02 5.3463e+00 -1.9200e+03 -8.8571e+02 5.3093e+00 -1.9200e+03 -8.5714e+02 5.2727e+00 -1.9200e+03 -8.2857e+02 5.2366e+00 -1.9200e+03 -8.0000e+02 5.2010e+00 -1.9200e+03 -7.7143e+02 5.1659e+00 -1.9200e+03 -7.4286e+02 5.1314e+00 -1.9200e+03 -7.1429e+02 5.0976e+00 -1.9200e+03 -6.8571e+02 5.0646e+00 -1.9200e+03 -6.5714e+02 5.0323e+00 -1.9200e+03 -6.2857e+02 5.0008e+00 -1.9200e+03 -6.0000e+02 4.9702e+00 -1.9200e+03 -5.7143e+02 4.9406e+00 -1.9200e+03 -5.4286e+02 4.9120e+00 -1.9200e+03 -5.1429e+02 4.8844e+00 -1.9200e+03 -4.8571e+02 4.8580e+00 -1.9200e+03 -4.5714e+02 4.8327e+00 -1.9200e+03 -4.2857e+02 4.8087e+00 -1.9200e+03 -4.0000e+02 4.7859e+00 -1.9200e+03 -3.7143e+02 4.7644e+00 -1.9200e+03 -3.4286e+02 4.7444e+00 -1.9200e+03 -3.1429e+02 4.7257e+00 -1.9200e+03 -2.8571e+02 4.7085e+00 -1.9200e+03 -2.5714e+02 4.6929e+00 -1.9200e+03 -2.2857e+02 4.6787e+00 -1.9200e+03 -2.0000e+02 4.6662e+00 -1.9200e+03 -1.7143e+02 4.6552e+00 -1.9200e+03 -1.4286e+02 4.6459e+00 -1.9200e+03 -1.1429e+02 4.6383e+00 -1.9200e+03 -8.5714e+01 4.6323e+00 -1.9200e+03 -5.7143e+01 4.6280e+00 -1.9200e+03 -2.8571e+01 4.6255e+00 -1.9200e+03 0.0000e+00 4.6246e+00 -1.9200e+03 2.8571e+01 4.6255e+00 -1.9200e+03 5.7143e+01 4.6280e+00 -1.9200e+03 8.5714e+01 4.6323e+00 -1.9200e+03 1.1429e+02 4.6383e+00 -1.9200e+03 1.4286e+02 4.6459e+00 -1.9200e+03 1.7143e+02 4.6552e+00 -1.9200e+03 2.0000e+02 4.6662e+00 -1.9200e+03 2.2857e+02 4.6787e+00 -1.9200e+03 2.5714e+02 4.6929e+00 -1.9200e+03 2.8571e+02 4.7085e+00 -1.9200e+03 3.1429e+02 4.7257e+00 -1.9200e+03 3.4286e+02 4.7444e+00 -1.9200e+03 3.7143e+02 4.7644e+00 -1.9200e+03 4.0000e+02 4.7859e+00 -1.9200e+03 4.2857e+02 4.8087e+00 -1.9200e+03 4.5714e+02 4.8327e+00 -1.9200e+03 4.8571e+02 4.8580e+00 -1.9200e+03 5.1429e+02 4.8844e+00 -1.9200e+03 5.4286e+02 4.9120e+00 -1.9200e+03 5.7143e+02 4.9406e+00 -1.9200e+03 6.0000e+02 4.9702e+00 -1.9200e+03 6.2857e+02 5.0008e+00 -1.9200e+03 6.5714e+02 5.0323e+00 -1.9200e+03 6.8571e+02 5.0646e+00 -1.9200e+03 7.1429e+02 5.0976e+00 -1.9200e+03 7.4286e+02 5.1314e+00 -1.9200e+03 7.7143e+02 5.1659e+00 -1.9200e+03 8.0000e+02 5.2010e+00 -1.9200e+03 8.2857e+02 5.2366e+00 -1.9200e+03 8.5714e+02 5.2727e+00 -1.9200e+03 8.8571e+02 5.3093e+00 -1.9200e+03 9.1429e+02 5.3463e+00 -1.9200e+03 9.4286e+02 5.3837e+00 -1.9200e+03 9.7143e+02 5.4213e+00 -1.9200e+03 1.0000e+03 5.4592e+00 -1.9200e+03 1.0286e+03 5.4974e+00 -1.9200e+03 1.0571e+03 5.5357e+00 -1.9200e+03 1.0857e+03 5.5741e+00 -1.9200e+03 1.1143e+03 5.6127e+00 -1.9200e+03 1.1429e+03 5.6513e+00 -1.9200e+03 1.1714e+03 5.6899e+00 -1.9200e+03 1.2000e+03 5.7286e+00 -1.9200e+03 1.2286e+03 5.7672e+00 -1.9200e+03 1.2571e+03 5.8057e+00 -1.9200e+03 1.2857e+03 5.8442e+00 -1.9200e+03 1.3143e+03 5.8825e+00 -1.9200e+03 1.3429e+03 5.9207e+00 -1.9200e+03 1.3714e+03 5.9587e+00 -1.9200e+03 1.4000e+03 5.9966e+00 -1.9200e+03 1.4286e+03 6.0342e+00 -1.9200e+03 1.4571e+03 6.0716e+00 -1.9200e+03 1.4857e+03 6.1088e+00 -1.9200e+03 1.5143e+03 6.1457e+00 -1.9200e+03 1.5429e+03 6.1823e+00 -1.9200e+03 1.5714e+03 6.2187e+00 -1.9200e+03 1.6000e+03 6.2547e+00 -1.9200e+03 1.6286e+03 6.2905e+00 -1.9200e+03 1.6571e+03 6.3259e+00 -1.9200e+03 1.6857e+03 6.3610e+00 -1.9200e+03 1.7143e+03 6.3957e+00 -1.9200e+03 1.7429e+03 6.4302e+00 -1.9200e+03 1.7714e+03 6.4642e+00 -1.9200e+03 1.8000e+03 6.4980e+00 -1.9200e+03 1.8286e+03 6.5313e+00 -1.9200e+03 1.8571e+03 6.5643e+00 -1.9200e+03 1.8857e+03 6.5970e+00 -1.9200e+03 1.9143e+03 6.6292e+00 -1.9200e+03 1.9429e+03 6.6611e+00 -1.9200e+03 1.9714e+03 6.6927e+00 -1.9200e+03 2.0000e+03 6.7238e+00 -1.9500e+03 -2.0000e+03 6.7549e+00 -1.9500e+03 -1.9714e+03 6.7245e+00 -1.9500e+03 -1.9429e+03 6.6938e+00 -1.9500e+03 -1.9143e+03 6.6628e+00 -1.9500e+03 -1.8857e+03 6.6314e+00 -1.9500e+03 -1.8571e+03 6.5997e+00 -1.9500e+03 -1.8286e+03 6.5677e+00 -1.9500e+03 -1.8000e+03 6.5353e+00 -1.9500e+03 -1.7714e+03 6.5025e+00 -1.9500e+03 -1.7429e+03 6.4695e+00 -1.9500e+03 -1.7143e+03 6.4361e+00 -1.9500e+03 -1.6857e+03 6.4024e+00 -1.9500e+03 -1.6571e+03 6.3684e+00 -1.9500e+03 -1.6286e+03 6.3341e+00 -1.9500e+03 -1.6000e+03 6.2995e+00 -1.9500e+03 -1.5714e+03 6.2647e+00 -1.9500e+03 -1.5429e+03 6.2296e+00 -1.9500e+03 -1.5143e+03 6.1942e+00 -1.9500e+03 -1.4857e+03 6.1585e+00 -1.9500e+03 -1.4571e+03 6.1227e+00 -1.9500e+03 -1.4286e+03 6.0866e+00 -1.9500e+03 -1.4000e+03 6.0503e+00 -1.9500e+03 -1.3714e+03 6.0139e+00 -1.9500e+03 -1.3429e+03 5.9773e+00 -1.9500e+03 -1.3143e+03 5.9405e+00 -1.9500e+03 -1.2857e+03 5.9037e+00 -1.9500e+03 -1.2571e+03 5.8667e+00 -1.9500e+03 -1.2286e+03 5.8297e+00 -1.9500e+03 -1.2000e+03 5.7927e+00 -1.9500e+03 -1.1714e+03 5.7556e+00 -1.9500e+03 -1.1429e+03 5.7185e+00 -1.9500e+03 -1.1143e+03 5.6815e+00 -1.9500e+03 -1.0857e+03 5.6446e+00 -1.9500e+03 -1.0571e+03 5.6078e+00 -1.9500e+03 -1.0286e+03 5.5712e+00 -1.9500e+03 -1.0000e+03 5.5348e+00 -1.9500e+03 -9.7143e+02 5.4985e+00 -1.9500e+03 -9.4286e+02 5.4626e+00 -1.9500e+03 -9.1429e+02 5.4270e+00 -1.9500e+03 -8.8571e+02 5.3917e+00 -1.9500e+03 -8.5714e+02 5.3569e+00 -1.9500e+03 -8.2857e+02 5.3225e+00 -1.9500e+03 -8.0000e+02 5.2886e+00 -1.9500e+03 -7.7143e+02 5.2552e+00 -1.9500e+03 -7.4286e+02 5.2225e+00 -1.9500e+03 -7.1429e+02 5.1903e+00 -1.9500e+03 -6.8571e+02 5.1590e+00 -1.9500e+03 -6.5714e+02 5.1283e+00 -1.9500e+03 -6.2857e+02 5.0985e+00 -1.9500e+03 -6.0000e+02 5.0695e+00 -1.9500e+03 -5.7143e+02 5.0414e+00 -1.9500e+03 -5.4286e+02 5.0143e+00 -1.9500e+03 -5.1429e+02 4.9882e+00 -1.9500e+03 -4.8571e+02 4.9632e+00 -1.9500e+03 -4.5714e+02 4.9393e+00 -1.9500e+03 -4.2857e+02 4.9166e+00 -1.9500e+03 -4.0000e+02 4.8951e+00 -1.9500e+03 -3.7143e+02 4.8748e+00 -1.9500e+03 -3.4286e+02 4.8559e+00 -1.9500e+03 -3.1429e+02 4.8383e+00 -1.9500e+03 -2.8571e+02 4.8221e+00 -1.9500e+03 -2.5714e+02 4.8073e+00 -1.9500e+03 -2.2857e+02 4.7939e+00 -1.9500e+03 -2.0000e+02 4.7821e+00 -1.9500e+03 -1.7143e+02 4.7718e+00 -1.9500e+03 -1.4286e+02 4.7630e+00 -1.9500e+03 -1.1429e+02 4.7558e+00 -1.9500e+03 -8.5714e+01 4.7502e+00 -1.9500e+03 -5.7143e+01 4.7462e+00 -1.9500e+03 -2.8571e+01 4.7437e+00 -1.9500e+03 0.0000e+00 4.7429e+00 -1.9500e+03 2.8571e+01 4.7437e+00 -1.9500e+03 5.7143e+01 4.7462e+00 -1.9500e+03 8.5714e+01 4.7502e+00 -1.9500e+03 1.1429e+02 4.7558e+00 -1.9500e+03 1.4286e+02 4.7630e+00 -1.9500e+03 1.7143e+02 4.7718e+00 -1.9500e+03 2.0000e+02 4.7821e+00 -1.9500e+03 2.2857e+02 4.7939e+00 -1.9500e+03 2.5714e+02 4.8073e+00 -1.9500e+03 2.8571e+02 4.8221e+00 -1.9500e+03 3.1429e+02 4.8383e+00 -1.9500e+03 3.4286e+02 4.8559e+00 -1.9500e+03 3.7143e+02 4.8748e+00 -1.9500e+03 4.0000e+02 4.8951e+00 -1.9500e+03 4.2857e+02 4.9166e+00 -1.9500e+03 4.5714e+02 4.9393e+00 -1.9500e+03 4.8571e+02 4.9632e+00 -1.9500e+03 5.1429e+02 4.9882e+00 -1.9500e+03 5.4286e+02 5.0143e+00 -1.9500e+03 5.7143e+02 5.0414e+00 -1.9500e+03 6.0000e+02 5.0695e+00 -1.9500e+03 6.2857e+02 5.0985e+00 -1.9500e+03 6.5714e+02 5.1283e+00 -1.9500e+03 6.8571e+02 5.1590e+00 -1.9500e+03 7.1429e+02 5.1903e+00 -1.9500e+03 7.4286e+02 5.2225e+00 -1.9500e+03 7.7143e+02 5.2552e+00 -1.9500e+03 8.0000e+02 5.2886e+00 -1.9500e+03 8.2857e+02 5.3225e+00 -1.9500e+03 8.5714e+02 5.3569e+00 -1.9500e+03 8.8571e+02 5.3917e+00 -1.9500e+03 9.1429e+02 5.4270e+00 -1.9500e+03 9.4286e+02 5.4626e+00 -1.9500e+03 9.7143e+02 5.4985e+00 -1.9500e+03 1.0000e+03 5.5348e+00 -1.9500e+03 1.0286e+03 5.5712e+00 -1.9500e+03 1.0571e+03 5.6078e+00 -1.9500e+03 1.0857e+03 5.6446e+00 -1.9500e+03 1.1143e+03 5.6815e+00 -1.9500e+03 1.1429e+03 5.7185e+00 -1.9500e+03 1.1714e+03 5.7556e+00 -1.9500e+03 1.2000e+03 5.7927e+00 -1.9500e+03 1.2286e+03 5.8297e+00 -1.9500e+03 1.2571e+03 5.8667e+00 -1.9500e+03 1.2857e+03 5.9037e+00 -1.9500e+03 1.3143e+03 5.9405e+00 -1.9500e+03 1.3429e+03 5.9773e+00 -1.9500e+03 1.3714e+03 6.0139e+00 -1.9500e+03 1.4000e+03 6.0503e+00 -1.9500e+03 1.4286e+03 6.0866e+00 -1.9500e+03 1.4571e+03 6.1227e+00 -1.9500e+03 1.4857e+03 6.1585e+00 -1.9500e+03 1.5143e+03 6.1942e+00 -1.9500e+03 1.5429e+03 6.2296e+00 -1.9500e+03 1.5714e+03 6.2647e+00 -1.9500e+03 1.6000e+03 6.2995e+00 -1.9500e+03 1.6286e+03 6.3341e+00 -1.9500e+03 1.6571e+03 6.3684e+00 -1.9500e+03 1.6857e+03 6.4024e+00 -1.9500e+03 1.7143e+03 6.4361e+00 -1.9500e+03 1.7429e+03 6.4695e+00 -1.9500e+03 1.7714e+03 6.5025e+00 -1.9500e+03 1.8000e+03 6.5353e+00 -1.9500e+03 1.8286e+03 6.5677e+00 -1.9500e+03 1.8571e+03 6.5997e+00 -1.9500e+03 1.8857e+03 6.6314e+00 -1.9500e+03 1.9143e+03 6.6628e+00 -1.9500e+03 1.9429e+03 6.6938e+00 -1.9500e+03 1.9714e+03 6.7245e+00 -1.9500e+03 2.0000e+03 6.7549e+00 -1.9800e+03 -2.0000e+03 6.7856e+00 -1.9800e+03 -1.9714e+03 6.7560e+00 -1.9800e+03 -1.9429e+03 6.7262e+00 -1.9800e+03 -1.9143e+03 6.6960e+00 -1.9800e+03 -1.8857e+03 6.6655e+00 -1.9800e+03 -1.8571e+03 6.6346e+00 -1.9800e+03 -1.8286e+03 6.6035e+00 -1.9800e+03 -1.8000e+03 6.5720e+00 -1.9800e+03 -1.7714e+03 6.5403e+00 -1.9800e+03 -1.7429e+03 6.5082e+00 -1.9800e+03 -1.7143e+03 6.4758e+00 -1.9800e+03 -1.6857e+03 6.4432e+00 -1.9800e+03 -1.6571e+03 6.4103e+00 -1.9800e+03 -1.6286e+03 6.3771e+00 -1.9800e+03 -1.6000e+03 6.3436e+00 -1.9800e+03 -1.5714e+03 6.3099e+00 -1.9800e+03 -1.5429e+03 6.2759e+00 -1.9800e+03 -1.5143e+03 6.2417e+00 -1.9800e+03 -1.4857e+03 6.2073e+00 -1.9800e+03 -1.4571e+03 6.1727e+00 -1.9800e+03 -1.4286e+03 6.1379e+00 -1.9800e+03 -1.4000e+03 6.1029e+00 -1.9800e+03 -1.3714e+03 6.0678e+00 -1.9800e+03 -1.3429e+03 6.0326e+00 -1.9800e+03 -1.3143e+03 5.9972e+00 -1.9800e+03 -1.2857e+03 5.9618e+00 -1.9800e+03 -1.2571e+03 5.9262e+00 -1.9800e+03 -1.2286e+03 5.8907e+00 -1.9800e+03 -1.2000e+03 5.8551e+00 -1.9800e+03 -1.1714e+03 5.8195e+00 -1.9800e+03 -1.1429e+03 5.7840e+00 -1.9800e+03 -1.1143e+03 5.7485e+00 -1.9800e+03 -1.0857e+03 5.7132e+00 -1.9800e+03 -1.0571e+03 5.6780e+00 -1.9800e+03 -1.0286e+03 5.6429e+00 -1.9800e+03 -1.0000e+03 5.6081e+00 -1.9800e+03 -9.7143e+02 5.5734e+00 -1.9800e+03 -9.4286e+02 5.5391e+00 -1.9800e+03 -9.1429e+02 5.5051e+00 -1.9800e+03 -8.8571e+02 5.4715e+00 -1.9800e+03 -8.5714e+02 5.4383e+00 -1.9800e+03 -8.2857e+02 5.4055e+00 -1.9800e+03 -8.0000e+02 5.3732e+00 -1.9800e+03 -7.7143e+02 5.3415e+00 -1.9800e+03 -7.4286e+02 5.3103e+00 -1.9800e+03 -7.1429e+02 5.2798e+00 -1.9800e+03 -6.8571e+02 5.2499e+00 -1.9800e+03 -6.5714e+02 5.2208e+00 -1.9800e+03 -6.2857e+02 5.1925e+00 -1.9800e+03 -6.0000e+02 5.1650e+00 -1.9800e+03 -5.7143e+02 5.1384e+00 -1.9800e+03 -5.4286e+02 5.1127e+00 -1.9800e+03 -5.1429e+02 5.0880e+00 -1.9800e+03 -4.8571e+02 5.0643e+00 -1.9800e+03 -4.5714e+02 5.0417e+00 -1.9800e+03 -4.2857e+02 5.0202e+00 -1.9800e+03 -4.0000e+02 4.9999e+00 -1.9800e+03 -3.7143e+02 4.9807e+00 -1.9800e+03 -3.4286e+02 4.9628e+00 -1.9800e+03 -3.1429e+02 4.9462e+00 -1.9800e+03 -2.8571e+02 4.9309e+00 -1.9800e+03 -2.5714e+02 4.9169e+00 -1.9800e+03 -2.2857e+02 4.9043e+00 -1.9800e+03 -2.0000e+02 4.8932e+00 -1.9800e+03 -1.7143e+02 4.8834e+00 -1.9800e+03 -1.4286e+02 4.8751e+00 -1.9800e+03 -1.1429e+02 4.8683e+00 -1.9800e+03 -8.5714e+01 4.8630e+00 -1.9800e+03 -5.7143e+01 4.8592e+00 -1.9800e+03 -2.8571e+01 4.8570e+00 -1.9800e+03 0.0000e+00 4.8562e+00 -1.9800e+03 2.8571e+01 4.8570e+00 -1.9800e+03 5.7143e+01 4.8592e+00 -1.9800e+03 8.5714e+01 4.8630e+00 -1.9800e+03 1.1429e+02 4.8683e+00 -1.9800e+03 1.4286e+02 4.8751e+00 -1.9800e+03 1.7143e+02 4.8834e+00 -1.9800e+03 2.0000e+02 4.8932e+00 -1.9800e+03 2.2857e+02 4.9043e+00 -1.9800e+03 2.5714e+02 4.9169e+00 -1.9800e+03 2.8571e+02 4.9309e+00 -1.9800e+03 3.1429e+02 4.9462e+00 -1.9800e+03 3.4286e+02 4.9628e+00 -1.9800e+03 3.7143e+02 4.9807e+00 -1.9800e+03 4.0000e+02 4.9999e+00 -1.9800e+03 4.2857e+02 5.0202e+00 -1.9800e+03 4.5714e+02 5.0417e+00 -1.9800e+03 4.8571e+02 5.0643e+00 -1.9800e+03 5.1429e+02 5.0880e+00 -1.9800e+03 5.4286e+02 5.1127e+00 -1.9800e+03 5.7143e+02 5.1384e+00 -1.9800e+03 6.0000e+02 5.1650e+00 -1.9800e+03 6.2857e+02 5.1925e+00 -1.9800e+03 6.5714e+02 5.2208e+00 -1.9800e+03 6.8571e+02 5.2499e+00 -1.9800e+03 7.1429e+02 5.2798e+00 -1.9800e+03 7.4286e+02 5.3103e+00 -1.9800e+03 7.7143e+02 5.3415e+00 -1.9800e+03 8.0000e+02 5.3732e+00 -1.9800e+03 8.2857e+02 5.4055e+00 -1.9800e+03 8.5714e+02 5.4383e+00 -1.9800e+03 8.8571e+02 5.4715e+00 -1.9800e+03 9.1429e+02 5.5051e+00 -1.9800e+03 9.4286e+02 5.5391e+00 -1.9800e+03 9.7143e+02 5.5734e+00 -1.9800e+03 1.0000e+03 5.6081e+00 -1.9800e+03 1.0286e+03 5.6429e+00 -1.9800e+03 1.0571e+03 5.6780e+00 -1.9800e+03 1.0857e+03 5.7132e+00 -1.9800e+03 1.1143e+03 5.7485e+00 -1.9800e+03 1.1429e+03 5.7840e+00 -1.9800e+03 1.1714e+03 5.8195e+00 -1.9800e+03 1.2000e+03 5.8551e+00 -1.9800e+03 1.2286e+03 5.8907e+00 -1.9800e+03 1.2571e+03 5.9262e+00 -1.9800e+03 1.2857e+03 5.9618e+00 -1.9800e+03 1.3143e+03 5.9972e+00 -1.9800e+03 1.3429e+03 6.0326e+00 -1.9800e+03 1.3714e+03 6.0678e+00 -1.9800e+03 1.4000e+03 6.1029e+00 -1.9800e+03 1.4286e+03 6.1379e+00 -1.9800e+03 1.4571e+03 6.1727e+00 -1.9800e+03 1.4857e+03 6.2073e+00 -1.9800e+03 1.5143e+03 6.2417e+00 -1.9800e+03 1.5429e+03 6.2759e+00 -1.9800e+03 1.5714e+03 6.3099e+00 -1.9800e+03 1.6000e+03 6.3436e+00 -1.9800e+03 1.6286e+03 6.3771e+00 -1.9800e+03 1.6571e+03 6.4103e+00 -1.9800e+03 1.6857e+03 6.4432e+00 -1.9800e+03 1.7143e+03 6.4758e+00 -1.9800e+03 1.7429e+03 6.5082e+00 -1.9800e+03 1.7714e+03 6.5403e+00 -1.9800e+03 1.8000e+03 6.5720e+00 -1.9800e+03 1.8286e+03 6.6035e+00 -1.9800e+03 1.8571e+03 6.6346e+00 -1.9800e+03 1.8857e+03 6.6655e+00 -1.9800e+03 1.9143e+03 6.6960e+00 -1.9800e+03 1.9429e+03 6.7262e+00 -1.9800e+03 1.9714e+03 6.7560e+00 -1.9800e+03 2.0000e+03 6.7856e+00 -2.0100e+03 -2.0000e+03 6.8159e+00 -2.0100e+03 -1.9714e+03 6.7872e+00 -2.0100e+03 -1.9429e+03 6.7581e+00 -2.0100e+03 -1.9143e+03 6.7287e+00 -2.0100e+03 -1.8857e+03 6.6991e+00 -2.0100e+03 -1.8571e+03 6.6691e+00 -2.0100e+03 -1.8286e+03 6.6388e+00 -2.0100e+03 -1.8000e+03 6.6083e+00 -2.0100e+03 -1.7714e+03 6.5775e+00 -2.0100e+03 -1.7429e+03 6.5463e+00 -2.0100e+03 -1.7143e+03 6.5150e+00 -2.0100e+03 -1.6857e+03 6.4833e+00 -2.0100e+03 -1.6571e+03 6.4514e+00 -2.0100e+03 -1.6286e+03 6.4193e+00 -2.0100e+03 -1.6000e+03 6.3869e+00 -2.0100e+03 -1.5714e+03 6.3542e+00 -2.0100e+03 -1.5429e+03 6.3214e+00 -2.0100e+03 -1.5143e+03 6.2884e+00 -2.0100e+03 -1.4857e+03 6.2551e+00 -2.0100e+03 -1.4571e+03 6.2217e+00 -2.0100e+03 -1.4286e+03 6.1881e+00 -2.0100e+03 -1.4000e+03 6.1544e+00 -2.0100e+03 -1.3714e+03 6.1206e+00 -2.0100e+03 -1.3429e+03 6.0866e+00 -2.0100e+03 -1.3143e+03 6.0526e+00 -2.0100e+03 -1.2857e+03 6.0185e+00 -2.0100e+03 -1.2571e+03 5.9843e+00 -2.0100e+03 -1.2286e+03 5.9501e+00 -2.0100e+03 -1.2000e+03 5.9160e+00 -2.0100e+03 -1.1714e+03 5.8818e+00 -2.0100e+03 -1.1429e+03 5.8477e+00 -2.0100e+03 -1.1143e+03 5.8137e+00 -2.0100e+03 -1.0857e+03 5.7798e+00 -2.0100e+03 -1.0571e+03 5.7461e+00 -2.0100e+03 -1.0286e+03 5.7125e+00 -2.0100e+03 -1.0000e+03 5.6792e+00 -2.0100e+03 -9.7143e+02 5.6461e+00 -2.0100e+03 -9.4286e+02 5.6133e+00 -2.0100e+03 -9.1429e+02 5.5808e+00 -2.0100e+03 -8.8571e+02 5.5487e+00 -2.0100e+03 -8.5714e+02 5.5170e+00 -2.0100e+03 -8.2857e+02 5.4858e+00 -2.0100e+03 -8.0000e+02 5.4550e+00 -2.0100e+03 -7.7143e+02 5.4248e+00 -2.0100e+03 -7.4286e+02 5.3951e+00 -2.0100e+03 -7.1429e+02 5.3661e+00 -2.0100e+03 -6.8571e+02 5.3377e+00 -2.0100e+03 -6.5714e+02 5.3101e+00 -2.0100e+03 -6.2857e+02 5.2832e+00 -2.0100e+03 -6.0000e+02 5.2571e+00 -2.0100e+03 -5.7143e+02 5.2318e+00 -2.0100e+03 -5.4286e+02 5.2075e+00 -2.0100e+03 -5.1429e+02 5.1840e+00 -2.0100e+03 -4.8571e+02 5.1616e+00 -2.0100e+03 -4.5714e+02 5.1401e+00 -2.0100e+03 -4.2857e+02 5.1198e+00 -2.0100e+03 -4.0000e+02 5.1005e+00 -2.0100e+03 -3.7143e+02 5.0824e+00 -2.0100e+03 -3.4286e+02 5.0654e+00 -2.0100e+03 -3.1429e+02 5.0497e+00 -2.0100e+03 -2.8571e+02 5.0352e+00 -2.0100e+03 -2.5714e+02 5.0220e+00 -2.0100e+03 -2.2857e+02 5.0102e+00 -2.0100e+03 -2.0000e+02 4.9996e+00 -2.0100e+03 -1.7143e+02 4.9904e+00 -2.0100e+03 -1.4286e+02 4.9826e+00 -2.0100e+03 -1.1429e+02 4.9762e+00 -2.0100e+03 -8.5714e+01 4.9712e+00 -2.0100e+03 -5.7143e+01 4.9676e+00 -2.0100e+03 -2.8571e+01 4.9654e+00 -2.0100e+03 0.0000e+00 4.9647e+00 -2.0100e+03 2.8571e+01 4.9654e+00 -2.0100e+03 5.7143e+01 4.9676e+00 -2.0100e+03 8.5714e+01 4.9712e+00 -2.0100e+03 1.1429e+02 4.9762e+00 -2.0100e+03 1.4286e+02 4.9826e+00 -2.0100e+03 1.7143e+02 4.9904e+00 -2.0100e+03 2.0000e+02 4.9996e+00 -2.0100e+03 2.2857e+02 5.0102e+00 -2.0100e+03 2.5714e+02 5.0220e+00 -2.0100e+03 2.8571e+02 5.0352e+00 -2.0100e+03 3.1429e+02 5.0497e+00 -2.0100e+03 3.4286e+02 5.0654e+00 -2.0100e+03 3.7143e+02 5.0824e+00 -2.0100e+03 4.0000e+02 5.1005e+00 -2.0100e+03 4.2857e+02 5.1198e+00 -2.0100e+03 4.5714e+02 5.1401e+00 -2.0100e+03 4.8571e+02 5.1616e+00 -2.0100e+03 5.1429e+02 5.1840e+00 -2.0100e+03 5.4286e+02 5.2075e+00 -2.0100e+03 5.7143e+02 5.2318e+00 -2.0100e+03 6.0000e+02 5.2571e+00 -2.0100e+03 6.2857e+02 5.2832e+00 -2.0100e+03 6.5714e+02 5.3101e+00 -2.0100e+03 6.8571e+02 5.3377e+00 -2.0100e+03 7.1429e+02 5.3661e+00 -2.0100e+03 7.4286e+02 5.3951e+00 -2.0100e+03 7.7143e+02 5.4248e+00 -2.0100e+03 8.0000e+02 5.4550e+00 -2.0100e+03 8.2857e+02 5.4858e+00 -2.0100e+03 8.5714e+02 5.5170e+00 -2.0100e+03 8.8571e+02 5.5487e+00 -2.0100e+03 9.1429e+02 5.5808e+00 -2.0100e+03 9.4286e+02 5.6133e+00 -2.0100e+03 9.7143e+02 5.6461e+00 -2.0100e+03 1.0000e+03 5.6792e+00 -2.0100e+03 1.0286e+03 5.7125e+00 -2.0100e+03 1.0571e+03 5.7461e+00 -2.0100e+03 1.0857e+03 5.7798e+00 -2.0100e+03 1.1143e+03 5.8137e+00 -2.0100e+03 1.1429e+03 5.8477e+00 -2.0100e+03 1.1714e+03 5.8818e+00 -2.0100e+03 1.2000e+03 5.9160e+00 -2.0100e+03 1.2286e+03 5.9501e+00 -2.0100e+03 1.2571e+03 5.9843e+00 -2.0100e+03 1.2857e+03 6.0185e+00 -2.0100e+03 1.3143e+03 6.0526e+00 -2.0100e+03 1.3429e+03 6.0866e+00 -2.0100e+03 1.3714e+03 6.1206e+00 -2.0100e+03 1.4000e+03 6.1544e+00 -2.0100e+03 1.4286e+03 6.1881e+00 -2.0100e+03 1.4571e+03 6.2217e+00 -2.0100e+03 1.4857e+03 6.2551e+00 -2.0100e+03 1.5143e+03 6.2884e+00 -2.0100e+03 1.5429e+03 6.3214e+00 -2.0100e+03 1.5714e+03 6.3542e+00 -2.0100e+03 1.6000e+03 6.3869e+00 -2.0100e+03 1.6286e+03 6.4193e+00 -2.0100e+03 1.6571e+03 6.4514e+00 -2.0100e+03 1.6857e+03 6.4833e+00 -2.0100e+03 1.7143e+03 6.5150e+00 -2.0100e+03 1.7429e+03 6.5463e+00 -2.0100e+03 1.7714e+03 6.5775e+00 -2.0100e+03 1.8000e+03 6.6083e+00 -2.0100e+03 1.8286e+03 6.6388e+00 -2.0100e+03 1.8571e+03 6.6691e+00 -2.0100e+03 1.8857e+03 6.6991e+00 -2.0100e+03 1.9143e+03 6.7287e+00 -2.0100e+03 1.9429e+03 6.7581e+00 -2.0100e+03 1.9714e+03 6.7872e+00 -2.0100e+03 2.0000e+03 6.8159e+00 -2.0400e+03 -2.0000e+03 6.8459e+00 -2.0400e+03 -1.9714e+03 6.8179e+00 -2.0400e+03 -1.9429e+03 6.7897e+00 -2.0400e+03 -1.9143e+03 6.7611e+00 -2.0400e+03 -1.8857e+03 6.7322e+00 -2.0400e+03 -1.8571e+03 6.7031e+00 -2.0400e+03 -1.8286e+03 6.6737e+00 -2.0400e+03 -1.8000e+03 6.6440e+00 -2.0400e+03 -1.7714e+03 6.6141e+00 -2.0400e+03 -1.7429e+03 6.5839e+00 -2.0400e+03 -1.7143e+03 6.5535e+00 -2.0400e+03 -1.6857e+03 6.5228e+00 -2.0400e+03 -1.6571e+03 6.4919e+00 -2.0400e+03 -1.6286e+03 6.4607e+00 -2.0400e+03 -1.6000e+03 6.4294e+00 -2.0400e+03 -1.5714e+03 6.3978e+00 -2.0400e+03 -1.5429e+03 6.3660e+00 -2.0400e+03 -1.5143e+03 6.3341e+00 -2.0400e+03 -1.4857e+03 6.3020e+00 -2.0400e+03 -1.4571e+03 6.2697e+00 -2.0400e+03 -1.4286e+03 6.2373e+00 -2.0400e+03 -1.4000e+03 6.2048e+00 -2.0400e+03 -1.3714e+03 6.1722e+00 -2.0400e+03 -1.3429e+03 6.1395e+00 -2.0400e+03 -1.3143e+03 6.1067e+00 -2.0400e+03 -1.2857e+03 6.0738e+00 -2.0400e+03 -1.2571e+03 6.0410e+00 -2.0400e+03 -1.2286e+03 6.0081e+00 -2.0400e+03 -1.2000e+03 5.9753e+00 -2.0400e+03 -1.1714e+03 5.9425e+00 -2.0400e+03 -1.1429e+03 5.9098e+00 -2.0400e+03 -1.1143e+03 5.8772e+00 -2.0400e+03 -1.0857e+03 5.8447e+00 -2.0400e+03 -1.0571e+03 5.8123e+00 -2.0400e+03 -1.0286e+03 5.7802e+00 -2.0400e+03 -1.0000e+03 5.7483e+00 -2.0400e+03 -9.7143e+02 5.7166e+00 -2.0400e+03 -9.4286e+02 5.6853e+00 -2.0400e+03 -9.1429e+02 5.6543e+00 -2.0400e+03 -8.8571e+02 5.6236e+00 -2.0400e+03 -8.5714e+02 5.5933e+00 -2.0400e+03 -8.2857e+02 5.5635e+00 -2.0400e+03 -8.0000e+02 5.5342e+00 -2.0400e+03 -7.7143e+02 5.5054e+00 -2.0400e+03 -7.4286e+02 5.4771e+00 -2.0400e+03 -7.1429e+02 5.4495e+00 -2.0400e+03 -6.8571e+02 5.4225e+00 -2.0400e+03 -6.5714e+02 5.3962e+00 -2.0400e+03 -6.2857e+02 5.3706e+00 -2.0400e+03 -6.0000e+02 5.3458e+00 -2.0400e+03 -5.7143e+02 5.3218e+00 -2.0400e+03 -5.4286e+02 5.2987e+00 -2.0400e+03 -5.1429e+02 5.2764e+00 -2.0400e+03 -4.8571e+02 5.2551e+00 -2.0400e+03 -4.5714e+02 5.2348e+00 -2.0400e+03 -4.2857e+02 5.2155e+00 -2.0400e+03 -4.0000e+02 5.1973e+00 -2.0400e+03 -3.7143e+02 5.1801e+00 -2.0400e+03 -3.4286e+02 5.1640e+00 -2.0400e+03 -3.1429e+02 5.1492e+00 -2.0400e+03 -2.8571e+02 5.1355e+00 -2.0400e+03 -2.5714e+02 5.1230e+00 -2.0400e+03 -2.2857e+02 5.1117e+00 -2.0400e+03 -2.0000e+02 5.1017e+00 -2.0400e+03 -1.7143e+02 5.0930e+00 -2.0400e+03 -1.4286e+02 5.0857e+00 -2.0400e+03 -1.1429e+02 5.0796e+00 -2.0400e+03 -8.5714e+01 5.0748e+00 -2.0400e+03 -5.7143e+01 5.0715e+00 -2.0400e+03 -2.8571e+01 5.0694e+00 -2.0400e+03 0.0000e+00 5.0687e+00 -2.0400e+03 2.8571e+01 5.0694e+00 -2.0400e+03 5.7143e+01 5.0715e+00 -2.0400e+03 8.5714e+01 5.0748e+00 -2.0400e+03 1.1429e+02 5.0796e+00 -2.0400e+03 1.4286e+02 5.0857e+00 -2.0400e+03 1.7143e+02 5.0930e+00 -2.0400e+03 2.0000e+02 5.1017e+00 -2.0400e+03 2.2857e+02 5.1117e+00 -2.0400e+03 2.5714e+02 5.1230e+00 -2.0400e+03 2.8571e+02 5.1355e+00 -2.0400e+03 3.1429e+02 5.1492e+00 -2.0400e+03 3.4286e+02 5.1640e+00 -2.0400e+03 3.7143e+02 5.1801e+00 -2.0400e+03 4.0000e+02 5.1973e+00 -2.0400e+03 4.2857e+02 5.2155e+00 -2.0400e+03 4.5714e+02 5.2348e+00 -2.0400e+03 4.8571e+02 5.2551e+00 -2.0400e+03 5.1429e+02 5.2764e+00 -2.0400e+03 5.4286e+02 5.2987e+00 -2.0400e+03 5.7143e+02 5.3218e+00 -2.0400e+03 6.0000e+02 5.3458e+00 -2.0400e+03 6.2857e+02 5.3706e+00 -2.0400e+03 6.5714e+02 5.3962e+00 -2.0400e+03 6.8571e+02 5.4225e+00 -2.0400e+03 7.1429e+02 5.4495e+00 -2.0400e+03 7.4286e+02 5.4771e+00 -2.0400e+03 7.7143e+02 5.5054e+00 -2.0400e+03 8.0000e+02 5.5342e+00 -2.0400e+03 8.2857e+02 5.5635e+00 -2.0400e+03 8.5714e+02 5.5933e+00 -2.0400e+03 8.8571e+02 5.6236e+00 -2.0400e+03 9.1429e+02 5.6543e+00 -2.0400e+03 9.4286e+02 5.6853e+00 -2.0400e+03 9.7143e+02 5.7166e+00 -2.0400e+03 1.0000e+03 5.7483e+00 -2.0400e+03 1.0286e+03 5.7802e+00 -2.0400e+03 1.0571e+03 5.8123e+00 -2.0400e+03 1.0857e+03 5.8447e+00 -2.0400e+03 1.1143e+03 5.8772e+00 -2.0400e+03 1.1429e+03 5.9098e+00 -2.0400e+03 1.1714e+03 5.9425e+00 -2.0400e+03 1.2000e+03 5.9753e+00 -2.0400e+03 1.2286e+03 6.0081e+00 -2.0400e+03 1.2571e+03 6.0410e+00 -2.0400e+03 1.2857e+03 6.0738e+00 -2.0400e+03 1.3143e+03 6.1067e+00 -2.0400e+03 1.3429e+03 6.1395e+00 -2.0400e+03 1.3714e+03 6.1722e+00 -2.0400e+03 1.4000e+03 6.2048e+00 -2.0400e+03 1.4286e+03 6.2373e+00 -2.0400e+03 1.4571e+03 6.2697e+00 -2.0400e+03 1.4857e+03 6.3020e+00 -2.0400e+03 1.5143e+03 6.3341e+00 -2.0400e+03 1.5429e+03 6.3660e+00 -2.0400e+03 1.5714e+03 6.3978e+00 -2.0400e+03 1.6000e+03 6.4294e+00 -2.0400e+03 1.6286e+03 6.4607e+00 -2.0400e+03 1.6571e+03 6.4919e+00 -2.0400e+03 1.6857e+03 6.5228e+00 -2.0400e+03 1.7143e+03 6.5535e+00 -2.0400e+03 1.7429e+03 6.5839e+00 -2.0400e+03 1.7714e+03 6.6141e+00 -2.0400e+03 1.8000e+03 6.6440e+00 -2.0400e+03 1.8286e+03 6.6737e+00 -2.0400e+03 1.8571e+03 6.7031e+00 -2.0400e+03 1.8857e+03 6.7322e+00 -2.0400e+03 1.9143e+03 6.7611e+00 -2.0400e+03 1.9429e+03 6.7897e+00 -2.0400e+03 1.9714e+03 6.8179e+00 -2.0400e+03 2.0000e+03 6.8459e+00 -2.0700e+03 -2.0000e+03 6.8756e+00 -2.0700e+03 -1.9714e+03 6.8483e+00 -2.0700e+03 -1.9429e+03 6.8208e+00 -2.0700e+03 -1.9143e+03 6.7930e+00 -2.0700e+03 -1.8857e+03 6.7649e+00 -2.0700e+03 -1.8571e+03 6.7366e+00 -2.0700e+03 -1.8286e+03 6.7080e+00 -2.0700e+03 -1.8000e+03 6.6792e+00 -2.0700e+03 -1.7714e+03 6.6502e+00 -2.0700e+03 -1.7429e+03 6.6209e+00 -2.0700e+03 -1.7143e+03 6.5913e+00 -2.0700e+03 -1.6857e+03 6.5616e+00 -2.0700e+03 -1.6571e+03 6.5316e+00 -2.0700e+03 -1.6286e+03 6.5015e+00 -2.0700e+03 -1.6000e+03 6.4711e+00 -2.0700e+03 -1.5714e+03 6.4406e+00 -2.0700e+03 -1.5429e+03 6.4098e+00 -2.0700e+03 -1.5143e+03 6.3790e+00 -2.0700e+03 -1.4857e+03 6.3479e+00 -2.0700e+03 -1.4571e+03 6.3168e+00 -2.0700e+03 -1.4286e+03 6.2855e+00 -2.0700e+03 -1.4000e+03 6.2541e+00 -2.0700e+03 -1.3714e+03 6.2227e+00 -2.0700e+03 -1.3429e+03 6.1911e+00 -2.0700e+03 -1.3143e+03 6.1595e+00 -2.0700e+03 -1.2857e+03 6.1279e+00 -2.0700e+03 -1.2571e+03 6.0963e+00 -2.0700e+03 -1.2286e+03 6.0647e+00 -2.0700e+03 -1.2000e+03 6.0331e+00 -2.0700e+03 -1.1714e+03 6.0016e+00 -2.0700e+03 -1.1429e+03 5.9702e+00 -2.0700e+03 -1.1143e+03 5.9389e+00 -2.0700e+03 -1.0857e+03 5.9077e+00 -2.0700e+03 -1.0571e+03 5.8768e+00 -2.0700e+03 -1.0286e+03 5.8460e+00 -2.0700e+03 -1.0000e+03 5.8154e+00 -2.0700e+03 -9.7143e+02 5.7851e+00 -2.0700e+03 -9.4286e+02 5.7551e+00 -2.0700e+03 -9.1429e+02 5.7254e+00 -2.0700e+03 -8.8571e+02 5.6961e+00 -2.0700e+03 -8.5714e+02 5.6672e+00 -2.0700e+03 -8.2857e+02 5.6388e+00 -2.0700e+03 -8.0000e+02 5.6108e+00 -2.0700e+03 -7.7143e+02 5.5833e+00 -2.0700e+03 -7.4286e+02 5.5564e+00 -2.0700e+03 -7.1429e+02 5.5300e+00 -2.0700e+03 -6.8571e+02 5.5043e+00 -2.0700e+03 -6.5714e+02 5.4793e+00 -2.0700e+03 -6.2857e+02 5.4550e+00 -2.0700e+03 -6.0000e+02 5.4314e+00 -2.0700e+03 -5.7143e+02 5.4086e+00 -2.0700e+03 -5.4286e+02 5.3866e+00 -2.0700e+03 -5.1429e+02 5.3655e+00 -2.0700e+03 -4.8571e+02 5.3452e+00 -2.0700e+03 -4.5714e+02 5.3260e+00 -2.0700e+03 -4.2857e+02 5.3076e+00 -2.0700e+03 -4.0000e+02 5.2903e+00 -2.0700e+03 -3.7143e+02 5.2740e+00 -2.0700e+03 -3.4286e+02 5.2588e+00 -2.0700e+03 -3.1429e+02 5.2447e+00 -2.0700e+03 -2.8571e+02 5.2317e+00 -2.0700e+03 -2.5714e+02 5.2199e+00 -2.0700e+03 -2.2857e+02 5.2093e+00 -2.0700e+03 -2.0000e+02 5.1998e+00 -2.0700e+03 -1.7143e+02 5.1916e+00 -2.0700e+03 -1.4286e+02 5.1846e+00 -2.0700e+03 -1.1429e+02 5.1788e+00 -2.0700e+03 -8.5714e+01 5.1744e+00 -2.0700e+03 -5.7143e+01 5.1712e+00 -2.0700e+03 -2.8571e+01 5.1692e+00 -2.0700e+03 0.0000e+00 5.1686e+00 -2.0700e+03 2.8571e+01 5.1692e+00 -2.0700e+03 5.7143e+01 5.1712e+00 -2.0700e+03 8.5714e+01 5.1744e+00 -2.0700e+03 1.1429e+02 5.1788e+00 -2.0700e+03 1.4286e+02 5.1846e+00 -2.0700e+03 1.7143e+02 5.1916e+00 -2.0700e+03 2.0000e+02 5.1998e+00 -2.0700e+03 2.2857e+02 5.2093e+00 -2.0700e+03 2.5714e+02 5.2199e+00 -2.0700e+03 2.8571e+02 5.2317e+00 -2.0700e+03 3.1429e+02 5.2447e+00 -2.0700e+03 3.4286e+02 5.2588e+00 -2.0700e+03 3.7143e+02 5.2740e+00 -2.0700e+03 4.0000e+02 5.2903e+00 -2.0700e+03 4.2857e+02 5.3076e+00 -2.0700e+03 4.5714e+02 5.3260e+00 -2.0700e+03 4.8571e+02 5.3452e+00 -2.0700e+03 5.1429e+02 5.3655e+00 -2.0700e+03 5.4286e+02 5.3866e+00 -2.0700e+03 5.7143e+02 5.4086e+00 -2.0700e+03 6.0000e+02 5.4314e+00 -2.0700e+03 6.2857e+02 5.4550e+00 -2.0700e+03 6.5714e+02 5.4793e+00 -2.0700e+03 6.8571e+02 5.5043e+00 -2.0700e+03 7.1429e+02 5.5300e+00 -2.0700e+03 7.4286e+02 5.5564e+00 -2.0700e+03 7.7143e+02 5.5833e+00 -2.0700e+03 8.0000e+02 5.6108e+00 -2.0700e+03 8.2857e+02 5.6388e+00 -2.0700e+03 8.5714e+02 5.6672e+00 -2.0700e+03 8.8571e+02 5.6961e+00 -2.0700e+03 9.1429e+02 5.7254e+00 -2.0700e+03 9.4286e+02 5.7551e+00 -2.0700e+03 9.7143e+02 5.7851e+00 -2.0700e+03 1.0000e+03 5.8154e+00 -2.0700e+03 1.0286e+03 5.8460e+00 -2.0700e+03 1.0571e+03 5.8768e+00 -2.0700e+03 1.0857e+03 5.9077e+00 -2.0700e+03 1.1143e+03 5.9389e+00 -2.0700e+03 1.1429e+03 5.9702e+00 -2.0700e+03 1.1714e+03 6.0016e+00 -2.0700e+03 1.2000e+03 6.0331e+00 -2.0700e+03 1.2286e+03 6.0647e+00 -2.0700e+03 1.2571e+03 6.0963e+00 -2.0700e+03 1.2857e+03 6.1279e+00 -2.0700e+03 1.3143e+03 6.1595e+00 -2.0700e+03 1.3429e+03 6.1911e+00 -2.0700e+03 1.3714e+03 6.2227e+00 -2.0700e+03 1.4000e+03 6.2541e+00 -2.0700e+03 1.4286e+03 6.2855e+00 -2.0700e+03 1.4571e+03 6.3168e+00 -2.0700e+03 1.4857e+03 6.3479e+00 -2.0700e+03 1.5143e+03 6.3790e+00 -2.0700e+03 1.5429e+03 6.4098e+00 -2.0700e+03 1.5714e+03 6.4406e+00 -2.0700e+03 1.6000e+03 6.4711e+00 -2.0700e+03 1.6286e+03 6.5015e+00 -2.0700e+03 1.6571e+03 6.5316e+00 -2.0700e+03 1.6857e+03 6.5616e+00 -2.0700e+03 1.7143e+03 6.5913e+00 -2.0700e+03 1.7429e+03 6.6209e+00 -2.0700e+03 1.7714e+03 6.6502e+00 -2.0700e+03 1.8000e+03 6.6792e+00 -2.0700e+03 1.8286e+03 6.7080e+00 -2.0700e+03 1.8571e+03 6.7366e+00 -2.0700e+03 1.8857e+03 6.7649e+00 -2.0700e+03 1.9143e+03 6.7930e+00 -2.0700e+03 1.9429e+03 6.8208e+00 -2.0700e+03 1.9714e+03 6.8483e+00 -2.0700e+03 2.0000e+03 6.8756e+00 -2.1000e+03 -2.0000e+03 6.9049e+00 -2.1000e+03 -1.9714e+03 6.8784e+00 -2.1000e+03 -1.9429e+03 6.8516e+00 -2.1000e+03 -1.9143e+03 6.8245e+00 -2.1000e+03 -1.8857e+03 6.7972e+00 -2.1000e+03 -1.8571e+03 6.7697e+00 -2.1000e+03 -1.8286e+03 6.7419e+00 -2.1000e+03 -1.8000e+03 6.7139e+00 -2.1000e+03 -1.7714e+03 6.6857e+00 -2.1000e+03 -1.7429e+03 6.6573e+00 -2.1000e+03 -1.7143e+03 6.6286e+00 -2.1000e+03 -1.6857e+03 6.5998e+00 -2.1000e+03 -1.6571e+03 6.5707e+00 -2.1000e+03 -1.6286e+03 6.5415e+00 -2.1000e+03 -1.6000e+03 6.5121e+00 -2.1000e+03 -1.5714e+03 6.4825e+00 -2.1000e+03 -1.5429e+03 6.4528e+00 -2.1000e+03 -1.5143e+03 6.4230e+00 -2.1000e+03 -1.4857e+03 6.3930e+00 -2.1000e+03 -1.4571e+03 6.3629e+00 -2.1000e+03 -1.4286e+03 6.3327e+00 -2.1000e+03 -1.4000e+03 6.3024e+00 -2.1000e+03 -1.3714e+03 6.2720e+00 -2.1000e+03 -1.3429e+03 6.2416e+00 -2.1000e+03 -1.3143e+03 6.2112e+00 -2.1000e+03 -1.2857e+03 6.1808e+00 -2.1000e+03 -1.2571e+03 6.1503e+00 -2.1000e+03 -1.2286e+03 6.1199e+00 -2.1000e+03 -1.2000e+03 6.0896e+00 -2.1000e+03 -1.1714e+03 6.0593e+00 -2.1000e+03 -1.1429e+03 6.0291e+00 -2.1000e+03 -1.1143e+03 5.9990e+00 -2.1000e+03 -1.0857e+03 5.9691e+00 -2.1000e+03 -1.0571e+03 5.9394e+00 -2.1000e+03 -1.0286e+03 5.9099e+00 -2.1000e+03 -1.0000e+03 5.8806e+00 -2.1000e+03 -9.7143e+02 5.8516e+00 -2.1000e+03 -9.4286e+02 5.8229e+00 -2.1000e+03 -9.1429e+02 5.7945e+00 -2.1000e+03 -8.8571e+02 5.7665e+00 -2.1000e+03 -8.5714e+02 5.7389e+00 -2.1000e+03 -8.2857e+02 5.7117e+00 -2.1000e+03 -8.0000e+02 5.6849e+00 -2.1000e+03 -7.7143e+02 5.6587e+00 -2.1000e+03 -7.4286e+02 5.6330e+00 -2.1000e+03 -7.1429e+02 5.6079e+00 -2.1000e+03 -6.8571e+02 5.5834e+00 -2.1000e+03 -6.5714e+02 5.5596e+00 -2.1000e+03 -6.2857e+02 5.5364e+00 -2.1000e+03 -6.0000e+02 5.5140e+00 -2.1000e+03 -5.7143e+02 5.4923e+00 -2.1000e+03 -5.4286e+02 5.4714e+00 -2.1000e+03 -5.1429e+02 5.4513e+00 -2.1000e+03 -4.8571e+02 5.4321e+00 -2.1000e+03 -4.5714e+02 5.4137e+00 -2.1000e+03 -4.2857e+02 5.3963e+00 -2.1000e+03 -4.0000e+02 5.3799e+00 -2.1000e+03 -3.7143e+02 5.3645e+00 -2.1000e+03 -3.4286e+02 5.3500e+00 -2.1000e+03 -3.1429e+02 5.3366e+00 -2.1000e+03 -2.8571e+02 5.3243e+00 -2.1000e+03 -2.5714e+02 5.3131e+00 -2.1000e+03 -2.2857e+02 5.3030e+00 -2.1000e+03 -2.0000e+02 5.2941e+00 -2.1000e+03 -1.7143e+02 5.2863e+00 -2.1000e+03 -1.4286e+02 5.2796e+00 -2.1000e+03 -1.1429e+02 5.2742e+00 -2.1000e+03 -8.5714e+01 5.2700e+00 -2.1000e+03 -5.7143e+01 5.2669e+00 -2.1000e+03 -2.8571e+01 5.2651e+00 -2.1000e+03 0.0000e+00 5.2645e+00 -2.1000e+03 2.8571e+01 5.2651e+00 -2.1000e+03 5.7143e+01 5.2669e+00 -2.1000e+03 8.5714e+01 5.2700e+00 -2.1000e+03 1.1429e+02 5.2742e+00 -2.1000e+03 1.4286e+02 5.2796e+00 -2.1000e+03 1.7143e+02 5.2863e+00 -2.1000e+03 2.0000e+02 5.2941e+00 -2.1000e+03 2.2857e+02 5.3030e+00 -2.1000e+03 2.5714e+02 5.3131e+00 -2.1000e+03 2.8571e+02 5.3243e+00 -2.1000e+03 3.1429e+02 5.3366e+00 -2.1000e+03 3.4286e+02 5.3500e+00 -2.1000e+03 3.7143e+02 5.3645e+00 -2.1000e+03 4.0000e+02 5.3799e+00 -2.1000e+03 4.2857e+02 5.3963e+00 -2.1000e+03 4.5714e+02 5.4137e+00 -2.1000e+03 4.8571e+02 5.4321e+00 -2.1000e+03 5.1429e+02 5.4513e+00 -2.1000e+03 5.4286e+02 5.4714e+00 -2.1000e+03 5.7143e+02 5.4923e+00 -2.1000e+03 6.0000e+02 5.5140e+00 -2.1000e+03 6.2857e+02 5.5364e+00 -2.1000e+03 6.5714e+02 5.5596e+00 -2.1000e+03 6.8571e+02 5.5834e+00 -2.1000e+03 7.1429e+02 5.6079e+00 -2.1000e+03 7.4286e+02 5.6330e+00 -2.1000e+03 7.7143e+02 5.6587e+00 -2.1000e+03 8.0000e+02 5.6849e+00 -2.1000e+03 8.2857e+02 5.7117e+00 -2.1000e+03 8.5714e+02 5.7389e+00 -2.1000e+03 8.8571e+02 5.7665e+00 -2.1000e+03 9.1429e+02 5.7945e+00 -2.1000e+03 9.4286e+02 5.8229e+00 -2.1000e+03 9.7143e+02 5.8516e+00 -2.1000e+03 1.0000e+03 5.8806e+00 -2.1000e+03 1.0286e+03 5.9099e+00 -2.1000e+03 1.0571e+03 5.9394e+00 -2.1000e+03 1.0857e+03 5.9691e+00 -2.1000e+03 1.1143e+03 5.9990e+00 -2.1000e+03 1.1429e+03 6.0291e+00 -2.1000e+03 1.1714e+03 6.0593e+00 -2.1000e+03 1.2000e+03 6.0896e+00 -2.1000e+03 1.2286e+03 6.1199e+00 -2.1000e+03 1.2571e+03 6.1503e+00 -2.1000e+03 1.2857e+03 6.1808e+00 -2.1000e+03 1.3143e+03 6.2112e+00 -2.1000e+03 1.3429e+03 6.2416e+00 -2.1000e+03 1.3714e+03 6.2720e+00 -2.1000e+03 1.4000e+03 6.3024e+00 -2.1000e+03 1.4286e+03 6.3327e+00 -2.1000e+03 1.4571e+03 6.3629e+00 -2.1000e+03 1.4857e+03 6.3930e+00 -2.1000e+03 1.5143e+03 6.4230e+00 -2.1000e+03 1.5429e+03 6.4528e+00 -2.1000e+03 1.5714e+03 6.4825e+00 -2.1000e+03 1.6000e+03 6.5121e+00 -2.1000e+03 1.6286e+03 6.5415e+00 -2.1000e+03 1.6571e+03 6.5707e+00 -2.1000e+03 1.6857e+03 6.5998e+00 -2.1000e+03 1.7143e+03 6.6286e+00 -2.1000e+03 1.7429e+03 6.6573e+00 -2.1000e+03 1.7714e+03 6.6857e+00 -2.1000e+03 1.8000e+03 6.7139e+00 -2.1000e+03 1.8286e+03 6.7419e+00 -2.1000e+03 1.8571e+03 6.7697e+00 -2.1000e+03 1.8857e+03 6.7972e+00 -2.1000e+03 1.9143e+03 6.8245e+00 -2.1000e+03 1.9429e+03 6.8516e+00 -2.1000e+03 1.9714e+03 6.8784e+00 -2.1000e+03 2.0000e+03 6.9049e+00 diff --git a/pyproject.toml b/pyproject.toml index 476e59b18d..577d2bad77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta" name = 'simpeg' description = "SimPEG: Simulation and Parameter Estimation in Geophysics" readme = 'README.rst' -requires-python = '>=3.8' +requires-python = '>=3.10' authors = [ {name = 'SimPEG developers', email = 'rowanc1@gmail.com'}, ] @@ -15,7 +15,7 @@ keywords = [ 'geophysics', 'inverse problem' ] dependencies = [ - "numpy>=1.20", + "numpy>=1.21", "scipy>=1.8", "pymatsolver>=0.2", "matplotlib", diff --git a/simpeg/directives/directives.py b/simpeg/directives/directives.py index 22fd36dc40..2932de7af1 100644 --- a/simpeg/directives/directives.py +++ b/simpeg/directives/directives.py @@ -1,7 +1,4 @@ -from __future__ import annotations # needed to use type operands in Python 3.8 - from typing import TYPE_CHECKING - import numpy as np import matplotlib.pyplot as plt import warnings @@ -196,7 +193,7 @@ def dmisfit(self, value): self._dmisfit = value @property - def survey(self) -> list[BaseSurvey]: + def survey(self) -> list["BaseSurvey"]: """Return survey for all data misfits Assuming that ``dmisfit`` is always a ``ComboObjectiveFunction``, @@ -211,7 +208,7 @@ def survey(self) -> list[BaseSurvey]: return [objfcts.simulation.survey for objfcts in self.dmisfit.objfcts] @property - def simulation(self) -> list[BaseSimulation]: + def simulation(self) -> list["BaseSimulation"]: """Return simulation for all data misfits. Assuming that ``dmisfit`` is always a ``ComboObjectiveFunction``, diff --git a/simpeg/maps/_base.py b/simpeg/maps/_base.py index 199887f3ca..b3bf414af8 100644 --- a/simpeg/maps/_base.py +++ b/simpeg/maps/_base.py @@ -2,8 +2,6 @@ Base and general map classes. """ -from __future__ import annotations # needed to use type operands in Python 3.8 - from collections import namedtuple import discretize import numpy as np diff --git a/simpeg/meta/multiprocessing.py b/simpeg/meta/multiprocessing.py index b637ac0072..05edf03469 100644 --- a/simpeg/meta/multiprocessing.py +++ b/simpeg/meta/multiprocessing.py @@ -221,12 +221,6 @@ class MultiprocessingMetaSimulation(MetaSimulation): to `multiprocessing.cpu_count()`. The number of processes spawned will be the minimum of this number and the number of simulations. - Notes - ----- - On Unix systems with python version 3.8 the default `fork` method of starting the - processes has lead to program stalls in certain cases. If you encounter this - try setting the start method to `spawn'. - >>> import multiprocessing as mp >>> mp.set_start_method("spawn") """ diff --git a/simpeg/objective_function.py b/simpeg/objective_function.py index e28bec0c90..92f53fa1e4 100644 --- a/simpeg/objective_function.py +++ b/simpeg/objective_function.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import numbers import numpy as np import scipy.sparse as sp diff --git a/simpeg/optimization.py b/simpeg/optimization.py index d4fa750ccc..90837b2a84 100644 --- a/simpeg/optimization.py +++ b/simpeg/optimization.py @@ -1,4 +1,3 @@ -from __future__ import annotations import numpy as np import scipy import scipy.sparse as sp diff --git a/simpeg/potential_fields/magnetics/sources.py b/simpeg/potential_fields/magnetics/sources.py index f74bada543..d63797a99a 100644 --- a/simpeg/potential_fields/magnetics/sources.py +++ b/simpeg/potential_fields/magnetics/sources.py @@ -1,4 +1,3 @@ -from __future__ import annotations from ...survey import BaseSrc from ...utils.mat_utils import dip_azimuth2cartesian from ...utils.code_utils import deprecate_class, validate_float, validate_list_of_types diff --git a/simpeg/regularization/base.py b/simpeg/regularization/base.py index 3165244228..a5c59aee3f 100644 --- a/simpeg/regularization/base.py +++ b/simpeg/regularization/base.py @@ -1,8 +1,5 @@ -from __future__ import annotations - import numpy as np from discretize.base import BaseMesh -from typing import TYPE_CHECKING from .. import maps from ..objective_function import BaseObjectiveFunction, ComboObjectiveFunction from .. import utils @@ -10,8 +7,7 @@ from simpeg.utils.code_utils import deprecate_property, validate_ndarray_with_shape -if TYPE_CHECKING: - from scipy.sparse import csr_matrix +from scipy.sparse import csr_matrix class BaseRegularization(BaseObjectiveFunction): diff --git a/simpeg/regularization/pgi.py b/simpeg/regularization/pgi.py index 08a8f4ddef..762f670cad 100644 --- a/simpeg/regularization/pgi.py +++ b/simpeg/regularization/pgi.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import copy import warnings diff --git a/simpeg/regularization/sparse.py b/simpeg/regularization/sparse.py index a917e7ecbd..b5ba938866 100644 --- a/simpeg/regularization/sparse.py +++ b/simpeg/regularization/sparse.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import numpy as np from discretize.base import BaseMesh diff --git a/simpeg/regularization/vector.py b/simpeg/regularization/vector.py index cc2f628c44..579e57e9bb 100644 --- a/simpeg/regularization/vector.py +++ b/simpeg/regularization/vector.py @@ -1,15 +1,10 @@ -from __future__ import annotations -from typing import TYPE_CHECKING - import scipy.sparse as sp import numpy as np from .base import Smallness from discretize.base import BaseMesh from .base import RegularizationMesh, BaseRegularization from .sparse import Sparse, SparseSmallness, SparseSmoothness - -if TYPE_CHECKING: - from scipy.sparse import csr_matrix +from scipy.sparse import csr_matrix class BaseVectorRegularization(BaseRegularization): diff --git a/simpeg/simulation.py b/simpeg/simulation.py index db48739f2c..40b885f41c 100644 --- a/simpeg/simulation.py +++ b/simpeg/simulation.py @@ -2,7 +2,6 @@ Define simulation classes. """ -from __future__ import annotations # needed to use type operands in Python 3.8 import os import inspect import numpy as np diff --git a/simpeg/typing/__init__.py b/simpeg/typing/__init__.py index 07975782bd..f0d4d57b78 100644 --- a/simpeg/typing/__init__.py +++ b/simpeg/typing/__init__.py @@ -16,30 +16,17 @@ """ -from __future__ import annotations import numpy as np import numpy.typing as npt -from typing import Union - -# Use try and except to support Python<3.10 -try: - from typing import TypeAlias - - RandomSeed: TypeAlias = Union[ - int, - npt.NDArray[np.int_], - np.random.SeedSequence, - np.random.BitGenerator, - np.random.Generator, - ] -except ImportError: - RandomSeed = Union[ - int, - npt.NDArray[np.int_], - np.random.SeedSequence, - np.random.BitGenerator, - np.random.Generator, - ] +from typing import Union, TypeAlias + +RandomSeed: TypeAlias = Union[ + int, + npt.NDArray[np.int_], + np.random.SeedSequence, + np.random.BitGenerator, + np.random.Generator, +] RandomSeed.__doc__ = """ A ``typing.Union`` for random seeds and Numpy's random number generators. diff --git a/simpeg/utils/code_utils.py b/simpeg/utils/code_utils.py index 30debf8cd9..af517056f7 100644 --- a/simpeg/utils/code_utils.py +++ b/simpeg/utils/code_utils.py @@ -1,4 +1,3 @@ -from __future__ import annotations import types import numpy as np from functools import wraps diff --git a/simpeg/utils/mat_utils.py b/simpeg/utils/mat_utils.py index e57b4a34f0..ee99c4c73d 100644 --- a/simpeg/utils/mat_utils.py +++ b/simpeg/utils/mat_utils.py @@ -1,4 +1,3 @@ -from __future__ import annotations # needed to use type operands in Python 3.8 import numpy as np from .code_utils import deprecate_function from ..typing import RandomSeed diff --git a/simpeg/utils/model_builder.py b/simpeg/utils/model_builder.py index c3a68abcec..4298748973 100644 --- a/simpeg/utils/model_builder.py +++ b/simpeg/utils/model_builder.py @@ -1,4 +1,3 @@ -from __future__ import annotations # needed to use type operands in Python 3.8 import numpy as np import scipy.ndimage as ndi import scipy.sparse as sp diff --git a/tests/meta/test_multiprocessing_sim.py b/tests/meta/test_multiprocessing_sim.py index eaabf64f6f..be2431fb10 100644 --- a/tests/meta/test_multiprocessing_sim.py +++ b/tests/meta/test_multiprocessing_sim.py @@ -1,6 +1,4 @@ import numpy as np -import multiprocessing as mp -import sys from simpeg.potential_fields import gravity from simpeg.electromagnetics.static import resistivity as dc @@ -17,9 +15,6 @@ MultiprocessingRepeatedSimulation, ) -if sys.version_info[0] == 3 and sys.version_info[1] <= 8: - mp.set_start_method("spawn") - def test_meta_correctness(): mesh = TensorMesh([16, 16, 16], origin="CCN") diff --git a/tests/pf/test_forward_Mag_Linear.py b/tests/pf/test_forward_Mag_Linear.py index 9cf3498238..9798507aa4 100644 --- a/tests/pf/test_forward_Mag_Linear.py +++ b/tests/pf/test_forward_Mag_Linear.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import discretize import numpy as np import pytest From 0164fac5330cd18c788124d3eae6e5e03cc3c5b6 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 18 Sep 2024 11:25:34 -0700 Subject: [PATCH 059/194] Replace `ind_active` for `active_cells` in pf simulations (#1520) Replace the `ind_active` argument in potential field simulation classes for `active_cells`. Add tests that check the corresponding warnings and errors are being raised. Update example, tutorials and tests so they use `active_cells` instead of `ind_active`. Part of #1121 --- examples/01-maps/plot_sumMap.py | 4 +- examples/02-gravity/plot_inv_grav_tiled.py | 4 +- .../plot_inv_mag_MVI_Sparse_TreeMesh.py | 2 +- .../plot_inv_mag_MVI_VectorAmplitude.py | 2 +- .../plot_inv_mag_nonLinear_Amplitude.py | 20 ++++-- .../plot_laguna_del_maule_inversion.py | 2 +- examples/_archived/plot_inv_grav_linear.py | 2 +- examples/_archived/plot_inv_mag_linear.py | 2 +- simpeg/potential_fields/base.py | 66 ++++++++++++++----- simpeg/potential_fields/gravity/simulation.py | 8 ++- .../potential_fields/magnetics/simulation.py | 8 ++- tests/dask/test_grav_inversion_linear.py | 2 +- tests/dask/test_mag_MVI_Octree.py | 2 +- .../dask/test_mag_inversion_linear_Octree.py | 2 +- tests/dask/test_mag_nonLinear_Amplitude.py | 8 +-- tests/pf/test_base_pf_simulation.py | 42 +++++++++++- tests/pf/test_forward_Grav_Linear.py | 8 +-- tests/pf/test_forward_Mag_Linear.py | 12 ++-- tests/pf/test_grav_inversion_linear.py | 2 +- tests/pf/test_mag_MVI_Octree.py | 2 +- tests/pf/test_mag_inversion_linear.py | 2 +- tests/pf/test_mag_inversion_linear_Octree.py | 2 +- tests/pf/test_mag_nonLinear_Amplitude.py | 8 +-- tests/pf/test_mag_vector_amplitude.py | 2 +- tests/pf/test_pf_quadtree_inversion_linear.py | 6 +- .../03-gravity/plot_1a_gravity_anomaly.py | 2 +- .../03-gravity/plot_1b_gravity_gradiometry.py | 2 +- .../03-gravity/plot_inv_1a_gravity_anomaly.py | 2 +- .../plot_inv_1b_gravity_anomaly_irls.py | 2 +- .../04-magnetics/plot_2a_magnetics_induced.py | 2 +- .../04-magnetics/plot_2b_magnetics_mvi.py | 2 +- .../plot_inv_2a_magnetics_induced.py | 2 +- .../plot_inv_3_cross_gradient_pf.py | 4 +- ...t_inv_1_joint_pf_pgi_full_info_tutorial.py | 4 +- ...lot_inv_2_joint_pf_pgi_no_info_tutorial.py | 4 +- 35 files changed, 169 insertions(+), 77 deletions(-) diff --git a/examples/01-maps/plot_sumMap.py b/examples/01-maps/plot_sumMap.py index 7cbc4f89c5..be96ed84bf 100644 --- a/examples/01-maps/plot_sumMap.py +++ b/examples/01-maps/plot_sumMap.py @@ -96,7 +96,7 @@ def run(plotIt=True): mesh, survey=survey, chiMap=idenMap, - ind_active=actv, + active_cells=actv, store_sensitivities="forward_only", ) @@ -117,7 +117,7 @@ def run(plotIt=True): # Create the forward model operator prob = magnetics.Simulation3DIntegral( - mesh, survey=survey, chiMap=sumMap, ind_active=actv, store_sensitivities="ram" + mesh, survey=survey, chiMap=sumMap, active_cells=actv, store_sensitivities="ram" ) # Make sensitivity weighting diff --git a/examples/02-gravity/plot_inv_grav_tiled.py b/examples/02-gravity/plot_inv_grav_tiled.py index f5676a5938..823c97a814 100644 --- a/examples/02-gravity/plot_inv_grav_tiled.py +++ b/examples/02-gravity/plot_inv_grav_tiled.py @@ -138,7 +138,7 @@ # Create the forward simulation for the global dataset simulation = gravity.simulation.Simulation3DIntegral( - survey=survey, mesh=mesh, rhoMap=idenMap, ind_active=activeCells + survey=survey, mesh=mesh, rhoMap=idenMap, active_cells=activeCells ) # Compute linear forward operator and compute some data @@ -166,7 +166,7 @@ survey=local_survey, mesh=local_meshes[ii], rhoMap=tile_map, - ind_active=local_actives, + active_cells=local_actives, sensitivity_path=os.path.join("Inversion", f"Tile{ii}.zarr"), ) diff --git a/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py b/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py index c9e1ab7b4d..e6353dc96c 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py @@ -156,7 +156,7 @@ # Create the simulation simulation = magnetics.simulation.Simulation3DIntegral( - survey=survey, mesh=mesh, chiMap=idenMap, ind_active=actv, model_type="vector" + survey=survey, mesh=mesh, chiMap=idenMap, active_cells=actv, model_type="vector" ) # Compute some data and add some random noise diff --git a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py index edd69836d2..8ee515c90e 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py @@ -123,7 +123,7 @@ # Create the simulation simulation = magnetics.simulation.Simulation3DIntegral( - survey=survey, mesh=mesh, chiMap=idenMap, ind_active=actv, model_type="vector" + survey=survey, mesh=mesh, chiMap=idenMap, active_cells=actv, model_type="vector" ) # Compute some data and add some random noise diff --git a/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py b/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py index d114a30609..64bd047b02 100644 --- a/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py +++ b/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py @@ -152,7 +152,7 @@ survey=survey, mesh=mesh, chiMap=idenMap, - ind_active=actv, + active_cells=actv, store_sensitivities="forward_only", ) simulation.M = M_xyz @@ -225,7 +225,11 @@ # Create static map simulation = magnetics.simulation.Simulation3DIntegral( - mesh=mesh, survey=survey, chiMap=idenMap, ind_active=surf, store_sensitivities="ram" + mesh=mesh, + survey=survey, + chiMap=idenMap, + active_cells=surf, + store_sensitivities="ram", ) wr = simulation.getJtJdiag(mstart) ** 0.5 @@ -281,7 +285,11 @@ surveyAmp = magnetics.survey.Survey(srcField) simulation = magnetics.simulation.Simulation3DIntegral( - mesh=mesh, survey=surveyAmp, chiMap=idenMap, ind_active=surf, is_amplitude_data=True + mesh=mesh, + survey=surveyAmp, + chiMap=idenMap, + active_cells=surf, + is_amplitude_data=True, ) bAmp = simulation.fields(mrec) @@ -339,7 +347,11 @@ # Create the forward model operator simulation = magnetics.simulation.Simulation3DIntegral( - survey=surveyAmp, mesh=mesh, chiMap=idenMap, ind_active=actv, is_amplitude_data=True + survey=surveyAmp, + mesh=mesh, + chiMap=idenMap, + active_cells=actv, + is_amplitude_data=True, ) data_obj = data.Data(survey, dobs=bAmp, noise_floor=wd) diff --git a/examples/20-published/plot_laguna_del_maule_inversion.py b/examples/20-published/plot_laguna_del_maule_inversion.py index d4dd2ffca5..2cc6bed0ea 100644 --- a/examples/20-published/plot_laguna_del_maule_inversion.py +++ b/examples/20-published/plot_laguna_del_maule_inversion.py @@ -92,7 +92,7 @@ def run(plotIt=True, cleanAfterRun=True): # Now that we have a model and a survey we can build the linear system ... # Create the forward model operator simulation = gravity.simulation.Simulation3DIntegral( - survey=survey, mesh=mesh, rhoMap=staticCells, ind_active=active + survey=survey, mesh=mesh, rhoMap=staticCells, active_cells=active ) # %% Create inversion objects diff --git a/examples/_archived/plot_inv_grav_linear.py b/examples/_archived/plot_inv_grav_linear.py index 7d6e07ab05..010465a79b 100644 --- a/examples/_archived/plot_inv_grav_linear.py +++ b/examples/_archived/plot_inv_grav_linear.py @@ -84,7 +84,7 @@ def run(plotIt=True): # Create the forward simulation simulation = gravity.simulation.Simulation3DIntegral( - survey=survey, mesh=mesh, rhoMap=idenMap, ind_active=actv + survey=survey, mesh=mesh, rhoMap=idenMap, active_cells=actv ) # Compute linear forward operator and compute some data diff --git a/examples/_archived/plot_inv_mag_linear.py b/examples/_archived/plot_inv_mag_linear.py index 8656f1dce1..041bb69c1b 100644 --- a/examples/_archived/plot_inv_mag_linear.py +++ b/examples/_archived/plot_inv_mag_linear.py @@ -91,7 +91,7 @@ def run(plotIt=True): survey=survey, mesh=mesh, chiMap=idenMap, - ind_active=actv, + active_cells=actv, ) # Compute linear forward operator and compute some data diff --git a/simpeg/potential_fields/base.py b/simpeg/potential_fields/base.py index db63463a1a..a1918a6c46 100644 --- a/simpeg/potential_fields/base.py +++ b/simpeg/potential_fields/base.py @@ -1,4 +1,5 @@ import os +import warnings from multiprocessing.pool import Pool import discretize @@ -9,6 +10,7 @@ from ..simulation import LinearSimulation from ..utils import validate_active_indices, validate_integer, validate_string +from ..utils.code_utils import deprecate_property try: import choclo @@ -40,7 +42,7 @@ class BasePFSimulation(LinearSimulation): ---------- mesh : discretize.TensorMesh or discretize.TreeMesh A 3D tensor or tree mesh. - ind_active : np.ndarray of int or bool + active_cells : np.ndarray of int or bool Indices array denoting the active topography cells. store_sensitivities : {'ram', 'disk', 'forward_only'} Options for storing sensitivities. There are 3 options @@ -60,6 +62,12 @@ class BasePFSimulation(LinearSimulation): If True, the simulation will run in parallel. If False, it will run in serial. If ``engine`` is not ``"choclo"`` this argument will be ignored. + ind_active : np.ndarray of int or bool + + .. deprecated:: 0.23.0 + + Keyword argument ``ind_active`` is deprecated in favor of + ``active_cells`` and will be removed in SimPEG v0.24.0. Notes ----- @@ -81,19 +89,30 @@ class BasePFSimulation(LinearSimulation): def __init__( self, mesh, - ind_active=None, + active_cells=None, store_sensitivities="ram", n_processes=1, sensitivity_dtype=np.float32, engine="geoana", numba_parallel=True, + ind_active=None, **kwargs, ): - # If deprecated property set with kwargs - if "actInd" in kwargs: - raise AttributeError( - "actInd was removed in SimPEG 0.17.0, please use ind_active" + # Deprecate ind_active argument + if ind_active is not None: + if active_cells is not None: + raise TypeError( + "Cannot pass both 'active_cells' and 'ind_active'." + "'ind_active' has been deprecated and will be removed in " + " SimPEG v0.24.0, please use 'active_cells' instead.", + ) + warnings.warn( + "'ind_active' has been deprecated and will be removed in " + " SimPEG v0.24.0, please use 'active_cells' instead.", + FutureWarning, + stacklevel=2, ) + active_cells = ind_active if "forwardOnly" in kwargs: raise AttributeError( @@ -115,13 +134,15 @@ def __init__( self._check_engine_and_mesh_dimensions() # Find non-zero cells indices - if ind_active is None: - ind_active = np.ones(mesh.n_cells, dtype=bool) + if active_cells is None: + active_cells = np.ones(mesh.n_cells, dtype=bool) else: - ind_active = validate_active_indices("ind_active", ind_active, mesh.n_cells) - self._ind_active = ind_active + active_cells = validate_active_indices( + "active_cells", active_cells, mesh.n_cells + ) + self._active_cells = active_cells - self.nC = int(sum(ind_active)) + self.nC = int(sum(active_cells)) if isinstance(mesh, discretize.TensorMesh): nodes = mesh.nodes @@ -144,10 +165,10 @@ def __init__( inds[:-1, 1:, 1:].reshape(-1, order="F"), inds[1:, 1:, 1:].reshape(-1, order="F"), ] - cell_nodes = np.stack(cell_nodes, axis=-1)[ind_active] + cell_nodes = np.stack(cell_nodes, axis=-1)[active_cells] elif isinstance(mesh, discretize.TreeMesh): nodes = np.r_[mesh.nodes, mesh.hanging_nodes] - cell_nodes = mesh.cell_nodes[ind_active] + cell_nodes = mesh.cell_nodes[active_cells] else: raise ValueError("Mesh must be 3D tensor or Octree.") unique, unique_inv = np.unique(cell_nodes.T, return_inverse=True) @@ -259,15 +280,24 @@ def numba_parallel(self, value: bool): self._numba_parallel = value @property - def ind_active(self): - """Active topography cells. + def active_cells(self): + """Active cells in the mesh. Returns ------- (n_cell) numpy.ndarray of bool - Returns the active topography cells + Returns the active cells in the mesh. """ - return self._ind_active + return self._active_cells + + ind_active = deprecate_property( + active_cells, + "ind_active", + "active_cells", + removal_version="0.24.0", + future_warn=True, + error=False, + ) def linear_operator(self): """Return linear operator. @@ -371,7 +401,7 @@ def _get_active_nodes(self): if self.nC == self.mesh.n_cells: return nodes, cell_nodes # Keep only the cell_nodes for active cells - cell_nodes = cell_nodes[self.ind_active] + cell_nodes = cell_nodes[self.active_cells] # Get the unique indices of the nodes that belong to every active cell # (these indices correspond to the original `nodes` array) unique_nodes, active_cell_nodes = np.unique(cell_nodes, return_inverse=True) diff --git a/simpeg/potential_fields/gravity/simulation.py b/simpeg/potential_fields/gravity/simulation.py index 1596b15ec2..0451062d0a 100644 --- a/simpeg/potential_fields/gravity/simulation.py +++ b/simpeg/potential_fields/gravity/simulation.py @@ -81,7 +81,7 @@ class Simulation3DIntegral(BasePFSimulation): Mesh use to run the gravity simulation. survey : simpeg.potential_fields.gravity.Survey Gravity survey with information of the receivers. - ind_active : (n_cells) numpy.ndarray, optional + active_cells : (n_cells) numpy.ndarray, optional Array that indicates which cells in ``mesh`` are active cells. rho : numpy.ndarray, optional Density array for the active cells in the mesh. @@ -106,6 +106,12 @@ class Simulation3DIntegral(BasePFSimulation): If True, the simulation will run in parallel. If False, it will run in serial. If ``engine`` is not ``"choclo"`` this argument will be ignored. + ind_active : np.ndarray of int or bool + + .. deprecated:: 0.23.0 + + Keyword argument ``ind_active`` is deprecated in favor of + ``active_cells`` and will be removed in SimPEG v0.24.0. """ rho, rhoMap, rhoDeriv = props.Invertible("Density") diff --git a/simpeg/potential_fields/magnetics/simulation.py b/simpeg/potential_fields/magnetics/simulation.py index 85d6e53109..8e1b0be6d2 100644 --- a/simpeg/potential_fields/magnetics/simulation.py +++ b/simpeg/potential_fields/magnetics/simulation.py @@ -52,7 +52,7 @@ class Simulation3DIntegral(BasePFSimulation): Mesh use to run the magnetic simulation. survey : simpeg.potential_fields.magnetics.Survey Magnetic survey with information of the receivers. - ind_active : (n_cells) numpy.ndarray, optional + active_cells : (n_cells) numpy.ndarray, optional Array that indicates which cells in ``mesh`` are active cells. chi : numpy.ndarray, optional Susceptibility array for the active cells in the mesh. @@ -83,6 +83,12 @@ class Simulation3DIntegral(BasePFSimulation): If True, the simulation will run in parallel. If False, it will run in serial. If ``engine`` is not ``"choclo"`` this argument will be ignored. + ind_active : np.ndarray of int or bool + + .. deprecated:: 0.23.0 + + Keyword argument ``ind_active`` is deprecated in favor of + ``active_cells`` and will be removed in SimPEG v0.24.0. """ chi, chiMap, chiDeriv = props.Invertible("Magnetic Susceptibility (SI)") diff --git a/tests/dask/test_grav_inversion_linear.py b/tests/dask/test_grav_inversion_linear.py index df68680167..ad8370b438 100644 --- a/tests/dask/test_grav_inversion_linear.py +++ b/tests/dask/test_grav_inversion_linear.py @@ -77,7 +77,7 @@ def setUp(self): self.mesh, survey=survey, rhoMap=idenMap, - ind_active=actv, + active_cells=actv, store_sensitivities="ram", chunk_format="row", ) diff --git a/tests/dask/test_mag_MVI_Octree.py b/tests/dask/test_mag_MVI_Octree.py index cc3984f2ab..5ac9f588b5 100644 --- a/tests/dask/test_mag_MVI_Octree.py +++ b/tests/dask/test_mag_MVI_Octree.py @@ -100,7 +100,7 @@ def setUp(self): survey=survey, model_type="vector", chiMap=idenMap, - ind_active=actv, + active_cells=actv, store_sensitivities="disk", chunk_format="auto", ) diff --git a/tests/dask/test_mag_inversion_linear_Octree.py b/tests/dask/test_mag_inversion_linear_Octree.py index 0bf5c4b6e0..f053dfd3ee 100644 --- a/tests/dask/test_mag_inversion_linear_Octree.py +++ b/tests/dask/test_mag_inversion_linear_Octree.py @@ -107,7 +107,7 @@ def setUp(self): self.mesh, survey=survey, chiMap=idenMap, - ind_active=actv, + active_cells=actv, store_sensitivities="ram", chunk_format="equal", ) diff --git a/tests/dask/test_mag_nonLinear_Amplitude.py b/tests/dask/test_mag_nonLinear_Amplitude.py index eb1775c032..0a71bd66cc 100644 --- a/tests/dask/test_mag_nonLinear_Amplitude.py +++ b/tests/dask/test_mag_nonLinear_Amplitude.py @@ -101,7 +101,7 @@ def setUp(self): survey=survey, mesh=mesh, chiMap=idenMap, - ind_active=actv, + active_cells=actv, store_sensitivities="forward_only", ) simulation.M = M_xyz @@ -135,7 +135,7 @@ def setUp(self): mesh=mesh, survey=survey, chiMap=idenMap, - ind_active=surf, + active_cells=surf, store_sensitivities="ram", ) simulation.model = mstart @@ -201,7 +201,7 @@ def setUp(self): mesh=mesh, survey=surveyAmp, chiMap=idenMap, - ind_active=surf, + active_cells=surf, is_amplitude_data=True, store_sensitivities="forward_only", ) @@ -228,7 +228,7 @@ def setUp(self): survey=surveyAmp, mesh=mesh, chiMap=idenMap, - ind_active=actv, + active_cells=actv, is_amplitude_data=True, ) diff --git a/tests/pf/test_base_pf_simulation.py b/tests/pf/test_base_pf_simulation.py index 9cf7964ee4..7ba37ae1c3 100644 --- a/tests/pf/test_base_pf_simulation.py +++ b/tests/pf/test_base_pf_simulation.py @@ -147,7 +147,7 @@ def test_inactive_cells_tensor(self, tensor_mesh, mock_simulation_class): active_cells = np.zeros(tensor_mesh.n_cells, dtype=bool) active_cells[0] = True # Initialize simulation - simulation = mock_simulation_class(tensor_mesh, ind_active=active_cells) + simulation = mock_simulation_class(tensor_mesh, active_cells=active_cells) # Build expected active_nodes and active_cell_nodes expected_active_nodes = tensor_mesh.nodes[tensor_mesh[0].nodes] expected_active_cell_nodes = np.atleast_2d(np.arange(8, dtype=int)) @@ -165,7 +165,7 @@ def test_inactive_cells_tree(self, tree_mesh, mock_simulation_class): active_cells[0] = True # Initialize simulation - simulation = mock_simulation_class(tree_mesh, ind_active=active_cells) + simulation = mock_simulation_class(tree_mesh, active_cells=active_cells) # Build expected active_nodes (in the right order for a single cell) expected_active_nodes = [ @@ -304,3 +304,41 @@ def test_invalid_mesh_with_choclo(self, mesh, mock_simulation_class): ) with pytest.raises(ValueError, match=msg): mock_simulation_class(mesh, engine="choclo") + + +class TestDeprecationIndActive: + """ + Test if using the deprecated ind_active argument/property raise warnings/errors + """ + + def test_deprecated_argument(self, tensor_mesh, mock_simulation_class): + """Test if passing ind_active argument raises warning.""" + ind_active = np.ones(tensor_mesh.n_cells, dtype=bool) + version_regex = "v[0-9]+.[0-9]+.[0-9]+" + msg = ( + "'ind_active' has been deprecated and will be removed in " + f" SimPEG {version_regex}, please use 'active_cells' instead." + ) + with pytest.warns(FutureWarning, match=msg): + mock_simulation_class(tensor_mesh, ind_active=ind_active) + + def test_error_both_args(self, tensor_mesh, mock_simulation_class): + """Test if passing both ind_active and active_cells raises error.""" + ind_active = np.ones(tensor_mesh.n_cells, dtype=bool) + version_regex = "v[0-9]+.[0-9]+.[0-9]+" + msg = ( + f"Cannot pass both 'active_cells' and 'ind_active'." + "'ind_active' has been deprecated and will be removed in " + f" SimPEG {version_regex}, please use 'active_cells' instead." + ) + with pytest.raises(TypeError, match=msg): + mock_simulation_class( + tensor_mesh, active_cells=ind_active, ind_active=ind_active + ) + + def test_deprecated_property(self, tensor_mesh, mock_simulation_class): + """Test if passing both ind_active and active_cells raises error.""" + ind_active = np.ones(tensor_mesh.n_cells, dtype=bool) + simulation = mock_simulation_class(tensor_mesh, active_cells=ind_active) + with pytest.warns(FutureWarning): + simulation.ind_active diff --git a/tests/pf/test_forward_Grav_Linear.py b/tests/pf/test_forward_Grav_Linear.py index 32a964710a..0b27f43e04 100644 --- a/tests/pf/test_forward_Grav_Linear.py +++ b/tests/pf/test_forward_Grav_Linear.py @@ -143,7 +143,7 @@ def test_accelerations_vs_analytic( mesh, survey=survey, rhoMap=idenMap, - ind_active=active_cells, + active_cells=active_cells, store_sensitivities=store_sensitivities, engine=engine, sensitivity_path=str(sensitivity_path), @@ -199,7 +199,7 @@ def test_tensor_vs_analytic( mesh, survey=survey, rhoMap=idenMap, - ind_active=active_cells, + active_cells=active_cells, store_sensitivities=store_sensitivities, engine=engine, sensitivity_path=str(sensitivity_path), @@ -259,7 +259,7 @@ def test_guv_vs_analytic( mesh, survey=survey, rhoMap=idenMap, - ind_active=active_cells, + active_cells=active_cells, store_sensitivities=store_sensitivities, engine=engine, sensitivity_path=str(sensitivity_path), @@ -301,7 +301,7 @@ def test_sensitivity_dtype( simple_mesh, survey=survey, rhoMap=idenMap, - ind_active=active_cells, + active_cells=active_cells, engine=engine, store_sensitivities=store_sensitivities, sensitivity_path=str(sensitivity_path), diff --git a/tests/pf/test_forward_Mag_Linear.py b/tests/pf/test_forward_Mag_Linear.py index 9798507aa4..b28d847a60 100644 --- a/tests/pf/test_forward_Mag_Linear.py +++ b/tests/pf/test_forward_Mag_Linear.py @@ -230,7 +230,7 @@ def test_magnetic_field_and_tmi_w_susceptibility( mag_mesh, survey=survey, chiMap=identity_map, - ind_active=active_cells, + active_cells=active_cells, sensitivity_path=str(tmp_path / f"{engine}"), store_sensitivities=store_sensitivities, engine=engine, @@ -311,7 +311,7 @@ def test_magnetic_gradiometry_w_susceptibility( mag_mesh, survey=survey, chiMap=identity_map, - ind_active=active_cells, + active_cells=active_cells, sensitivity_path=str(tmp_path / f"{engine}"), store_sensitivities=store_sensitivities, engine=engine, @@ -395,7 +395,7 @@ def test_magnetic_vector_and_tmi_w_magnetization( mag_mesh, survey=survey, chiMap=identity_map, - ind_active=active_cells, + active_cells=active_cells, sensitivity_path=str(tmp_path / f"{engine}"), store_sensitivities=store_sensitivities, model_type="vector", @@ -475,7 +475,7 @@ def test_magnetic_field_amplitude_w_magnetization( mag_mesh, survey=survey, chiMap=identity_map, - ind_active=active_cells, + active_cells=active_cells, sensitivity_path=str(tmp_path / f"{engine}"), store_sensitivities=store_sensitivities, model_type="vector", @@ -537,7 +537,7 @@ def test_sensitivity_dtype( mag_mesh, survey=survey, chiMap=idenMap, - ind_active=active_cells, + active_cells=active_cells, engine=engine, store_sensitivities=store_sensitivities, sensitivity_path=str(sensitivity_path), @@ -723,7 +723,7 @@ def get_block_inds(grid, block): mesh, survey=survey, chiMap=idenMap, - ind_active=active_cells, + active_cells=active_cells, store_sensitivities="forward_only", n_processes=None, ) diff --git a/tests/pf/test_grav_inversion_linear.py b/tests/pf/test_grav_inversion_linear.py index 3657c4668d..925c1dca4f 100644 --- a/tests/pf/test_grav_inversion_linear.py +++ b/tests/pf/test_grav_inversion_linear.py @@ -77,7 +77,7 @@ def test_gravity_inversion_linear(engine): mesh, survey=survey, rhoMap=idenMap, - ind_active=actv, + active_cells=actv, store_sensitivities="ram", engine=engine, **kwargs, diff --git a/tests/pf/test_mag_MVI_Octree.py b/tests/pf/test_mag_MVI_Octree.py index 48aa8e26bb..ebf2dcfb28 100644 --- a/tests/pf/test_mag_MVI_Octree.py +++ b/tests/pf/test_mag_MVI_Octree.py @@ -98,7 +98,7 @@ def setUp(self): survey=survey, model_type="vector", chiMap=idenMap, - ind_active=actv, + active_cells=actv, store_sensitivities="disk", ) self.sim = sim diff --git a/tests/pf/test_mag_inversion_linear.py b/tests/pf/test_mag_inversion_linear.py index 47d8df2321..97af16e1ed 100644 --- a/tests/pf/test_mag_inversion_linear.py +++ b/tests/pf/test_mag_inversion_linear.py @@ -83,7 +83,7 @@ def setUp(self): self.mesh, survey=survey, chiMap=idenMap, - ind_active=actv, + active_cells=actv, store_sensitivities="disk", n_processes=None, ) diff --git a/tests/pf/test_mag_inversion_linear_Octree.py b/tests/pf/test_mag_inversion_linear_Octree.py index d754d64e5c..e969a2b450 100644 --- a/tests/pf/test_mag_inversion_linear_Octree.py +++ b/tests/pf/test_mag_inversion_linear_Octree.py @@ -103,7 +103,7 @@ def setUp(self): self.mesh, survey=survey, chiMap=idenMap, - ind_active=actv, + active_cells=actv, store_sensitivities="ram", n_processes=None, ) diff --git a/tests/pf/test_mag_nonLinear_Amplitude.py b/tests/pf/test_mag_nonLinear_Amplitude.py index d4f11f6ce8..1895a8c1e3 100644 --- a/tests/pf/test_mag_nonLinear_Amplitude.py +++ b/tests/pf/test_mag_nonLinear_Amplitude.py @@ -100,7 +100,7 @@ def setUp(self): survey=survey, mesh=mesh, chiMap=idenMap, - ind_active=actv, + active_cells=actv, store_sensitivities="forward_only", ) simulation.M = M_xyz @@ -134,7 +134,7 @@ def setUp(self): mesh=mesh, survey=survey, chiMap=idenMap, - ind_active=surf, + active_cells=surf, store_sensitivities="ram", ) simulation.model = mstart @@ -200,7 +200,7 @@ def setUp(self): mesh=mesh, survey=surveyAmp, chiMap=idenMap, - ind_active=surf, + active_cells=surf, is_amplitude_data=True, store_sensitivities="forward_only", ) @@ -227,7 +227,7 @@ def setUp(self): survey=surveyAmp, mesh=mesh, chiMap=idenMap, - ind_active=actv, + active_cells=actv, is_amplitude_data=True, ) diff --git a/tests/pf/test_mag_vector_amplitude.py b/tests/pf/test_mag_vector_amplitude.py index 5115e4a22a..6982fba75d 100644 --- a/tests/pf/test_mag_vector_amplitude.py +++ b/tests/pf/test_mag_vector_amplitude.py @@ -98,7 +98,7 @@ def setUp(self): survey=survey, model_type="vector", chiMap=idenMap, - ind_active=actv, + active_cells=actv, store_sensitivities="disk", ) self.sim = sim diff --git a/tests/pf/test_pf_quadtree_inversion_linear.py b/tests/pf/test_pf_quadtree_inversion_linear.py index c6c7f64d8f..1a391d378c 100644 --- a/tests/pf/test_pf_quadtree_inversion_linear.py +++ b/tests/pf/test_pf_quadtree_inversion_linear.py @@ -209,7 +209,7 @@ def create_gravity_sim_active(self, block_value=1.0, noise_floor=0.01): survey=grav_survey, rhoMap=self.idenMap_active, store_sensitivities="ram", - ind_active=self.active_cells, + active_cells=self.active_cells, ) # Already defined @@ -243,7 +243,7 @@ def create_magnetics_sim_active(self, block_value=1.0, noise_floor=0.01): survey=mag_survey, chiMap=self.idenMap_active, store_sensitivities="ram", - ind_active=self.active_cells, + active_cells=self.active_cells, ) # Already defined @@ -458,7 +458,7 @@ def create_xyz_points_flat(x_range, y_range, spacing, altitude=0.0): -5.0 * np.ones(self.mesh.nC), survey=grav_survey, rhoMap=subset_idenMap, - ind_active=ind_active, + active_cells=ind_active, ) print("Z_TOP OR Z_BOTTOM LENGTH MATCHING NACTIVE-CELLS ERROR TEST PASSED.") diff --git a/tutorials/03-gravity/plot_1a_gravity_anomaly.py b/tutorials/03-gravity/plot_1a_gravity_anomaly.py index 15fb56c0c3..25e442ed45 100644 --- a/tutorials/03-gravity/plot_1a_gravity_anomaly.py +++ b/tutorials/03-gravity/plot_1a_gravity_anomaly.py @@ -190,7 +190,7 @@ survey=survey, mesh=mesh, rhoMap=model_map, - ind_active=ind_active, + active_cells=ind_active, store_sensitivities="forward_only", engine="choclo", ) diff --git a/tutorials/03-gravity/plot_1b_gravity_gradiometry.py b/tutorials/03-gravity/plot_1b_gravity_gradiometry.py index 84e4227cf3..8bb79ffb17 100644 --- a/tutorials/03-gravity/plot_1b_gravity_gradiometry.py +++ b/tutorials/03-gravity/plot_1b_gravity_gradiometry.py @@ -211,7 +211,7 @@ survey=survey, mesh=mesh, rhoMap=model_map, - ind_active=ind_active, + active_cells=ind_active, store_sensitivities="forward_only", engine="choclo", ) diff --git a/tutorials/03-gravity/plot_inv_1a_gravity_anomaly.py b/tutorials/03-gravity/plot_inv_1a_gravity_anomaly.py index 89d043d65d..27e679db43 100644 --- a/tutorials/03-gravity/plot_inv_1a_gravity_anomaly.py +++ b/tutorials/03-gravity/plot_inv_1a_gravity_anomaly.py @@ -218,7 +218,7 @@ survey=survey, mesh=mesh, rhoMap=model_map, - ind_active=ind_active, + active_cells=ind_active, engine="choclo", ) diff --git a/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py b/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py index 5bb75bfa9f..322afbd08e 100644 --- a/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py +++ b/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py @@ -220,7 +220,7 @@ survey=survey, mesh=mesh, rhoMap=model_map, - ind_active=ind_active, + active_cells=ind_active, engine="choclo", ) diff --git a/tutorials/04-magnetics/plot_2a_magnetics_induced.py b/tutorials/04-magnetics/plot_2a_magnetics_induced.py index 67889ba62a..9f3ece6ae3 100644 --- a/tutorials/04-magnetics/plot_2a_magnetics_induced.py +++ b/tutorials/04-magnetics/plot_2a_magnetics_induced.py @@ -178,7 +178,7 @@ mesh=mesh, model_type="scalar", chiMap=model_map, - ind_active=ind_active, + active_cells=ind_active, store_sensitivities="forward_only", engine="choclo", ) diff --git a/tutorials/04-magnetics/plot_2b_magnetics_mvi.py b/tutorials/04-magnetics/plot_2b_magnetics_mvi.py index 70c9621221..51c5610f5b 100644 --- a/tutorials/04-magnetics/plot_2b_magnetics_mvi.py +++ b/tutorials/04-magnetics/plot_2b_magnetics_mvi.py @@ -233,7 +233,7 @@ survey=survey, mesh=mesh, chiMap=model_map, - ind_active=ind_active, + active_cells=ind_active, model_type="vector", store_sensitivities="forward_only", ) diff --git a/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py b/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py index 92ab0691e0..a27ebde118 100644 --- a/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py +++ b/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py @@ -237,7 +237,7 @@ mesh=mesh, model_type="scalar", chiMap=model_map, - ind_active=active_cells, + active_cells=active_cells, engine="choclo", ) diff --git a/tutorials/13-joint_inversion/plot_inv_3_cross_gradient_pf.py b/tutorials/13-joint_inversion/plot_inv_3_cross_gradient_pf.py index 951807633a..5450c36dcb 100755 --- a/tutorials/13-joint_inversion/plot_inv_3_cross_gradient_pf.py +++ b/tutorials/13-joint_inversion/plot_inv_3_cross_gradient_pf.py @@ -288,7 +288,7 @@ survey=survey_grav, mesh=mesh, rhoMap=wires.density, - ind_active=ind_active, + active_cells=ind_active, engine="choclo", ) @@ -297,7 +297,7 @@ mesh=mesh, model_type="scalar", chiMap=wires.susceptibility, - ind_active=ind_active, + active_cells=ind_active, ) diff --git a/tutorials/14-pgi/plot_inv_1_joint_pf_pgi_full_info_tutorial.py b/tutorials/14-pgi/plot_inv_1_joint_pf_pgi_full_info_tutorial.py index d4a1e3cc13..7c4170f81f 100644 --- a/tutorials/14-pgi/plot_inv_1_joint_pf_pgi_full_info_tutorial.py +++ b/tutorials/14-pgi/plot_inv_1_joint_pf_pgi_full_info_tutorial.py @@ -233,7 +233,7 @@ survey=data_grav.survey, mesh=mesh, rhoMap=wires.den, - ind_active=actv, + active_cells=actv, engine="choclo", ) dmis_grav = data_misfit.L2DataMisfit(data=data_grav, simulation=simulation_grav) @@ -242,7 +242,7 @@ survey=data_mag.survey, mesh=mesh, chiMap=wires.sus, - ind_active=actv, + active_cells=actv, ) dmis_mag = data_misfit.L2DataMisfit(data=data_mag, simulation=simulation_mag) diff --git a/tutorials/14-pgi/plot_inv_2_joint_pf_pgi_no_info_tutorial.py b/tutorials/14-pgi/plot_inv_2_joint_pf_pgi_no_info_tutorial.py index 78582411f1..1c14dbf021 100644 --- a/tutorials/14-pgi/plot_inv_2_joint_pf_pgi_no_info_tutorial.py +++ b/tutorials/14-pgi/plot_inv_2_joint_pf_pgi_no_info_tutorial.py @@ -234,7 +234,7 @@ survey=data_grav.survey, mesh=mesh, rhoMap=wires.den, - ind_active=actv, + active_cells=actv, engine="choclo", ) dmis_grav = data_misfit.L2DataMisfit(data=data_grav, simulation=simulation_grav) @@ -243,7 +243,7 @@ survey=data_mag.survey, mesh=mesh, chiMap=wires.sus, - ind_active=actv, + active_cells=actv, ) dmis_mag = data_misfit.L2DataMisfit(data=data_mag, simulation=simulation_mag) From 048ef809fa550446305c752b1fd137cf1837bb60 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 18 Sep 2024 14:48:33 -0700 Subject: [PATCH 060/194] Move push to codecov to its own stage (#1493) Make each test job in Azure to publish the coverage reports as artifacts. Create a new stage for pushing the coverage reports to Codecov. --- .ci/azure/codecov.yml | 34 ++++++++++++++++++++++++++++++++++ .ci/azure/test.yml | 17 ++++++++++++----- azure-pipelines.yml | 5 +++++ 3 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 .ci/azure/codecov.yml diff --git a/.ci/azure/codecov.yml b/.ci/azure/codecov.yml new file mode 100644 index 0000000000..402e90e244 --- /dev/null +++ b/.ci/azure/codecov.yml @@ -0,0 +1,34 @@ +jobs: + - job: + pool: + vmImage: "ubuntu-latest" + displayName: Upload to Codecov + steps: + # Checkout simpeg repo. Codecov needs the repo in the file system for + # uploading coverage reports. + - checkout: self + displayName: "Checkout repository" + + - task: DownloadPipelineArtifact@2 + inputs: + patterns: "coverage-*/coverage-*.xml" + displayName: "Download coverage artifacts" + + - bash: ls -la $(Pipeline.Workspace)/coverage-*/coverage-*.xml + displayName: "List downloaded coverage artifacts" + + - bash: | + cp $(Pipeline.Workspace)/coverage-*/coverage-*.xml . + ls -la + displayName: "Copy coverage files" + + - script: | + curl -Os https://uploader.codecov.io/latest/linux/codecov + chmod +x codecov + displayName: "Install codecov cli" + + - script: | + for report in coverage-*.xml; do + ./codecov --verbose upload-process -f "$report" + done + displayName: "Upload coverage to codecov.io" diff --git a/.ci/azure/test.yml b/.ci/azure/test.yml index b9f27d1989..2e6ad30378 100644 --- a/.ci/azure/test.yml +++ b/.ci/azure/test.yml @@ -52,8 +52,15 @@ jobs: displayName: 'Publish documentation artifact' condition: eq('${{ test }}', 'tests/docs -s -v') - - script: | - curl -Os https://uploader.codecov.io/latest/linux/codecov - chmod +x codecov - ./codecov - displayName: 'Upload coverage to codecov.io' + - bash: | + job="${{ os }}_${{ py_vers }}_${{ test }}" + jobhash=$(echo $job | sha256sum | cut -f 1 -d " " | cut -c 1-7) + cp coverage.xml "coverage-$jobhash.xml" + echo "##vso[task.setvariable variable=jobhash]$jobhash" + displayName: 'Rename coverage report' + + - task: PublishPipelineArtifact@1 + inputs: + targetPath: $(Build.SourcesDirectory)/coverage-$(jobhash).xml + artifactName: coverage-$(jobhash) + displayName: 'Publish coverage artifact' diff --git a/azure-pipelines.yml b/azure-pipelines.yml index f755b7f443..7e10fa5d59 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -42,6 +42,11 @@ stages: jobs: - template: .ci/azure/test.yml + - stage: Codecov + dependsOn: Testing + jobs: + - template: .ci/azure/codecov.yml + - stage: Docs dependsOn: - StyleChecks From 060993c2573c886c4ce68fb34812b789aff1a782 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 19 Sep 2024 13:27:03 -0700 Subject: [PATCH 061/194] Minor fix in deprecation notice in docstrings (#1535) Apply a minor fix to the deprecation notice of `ind_active` in the potential field simulations. --- simpeg/potential_fields/base.py | 2 +- simpeg/potential_fields/gravity/simulation.py | 2 +- simpeg/potential_fields/magnetics/simulation.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/simpeg/potential_fields/base.py b/simpeg/potential_fields/base.py index a1918a6c46..33bc556947 100644 --- a/simpeg/potential_fields/base.py +++ b/simpeg/potential_fields/base.py @@ -66,7 +66,7 @@ class BasePFSimulation(LinearSimulation): .. deprecated:: 0.23.0 - Keyword argument ``ind_active`` is deprecated in favor of + Argument ``ind_active`` is deprecated in favor of ``active_cells`` and will be removed in SimPEG v0.24.0. Notes diff --git a/simpeg/potential_fields/gravity/simulation.py b/simpeg/potential_fields/gravity/simulation.py index 0451062d0a..1d1f667e2e 100644 --- a/simpeg/potential_fields/gravity/simulation.py +++ b/simpeg/potential_fields/gravity/simulation.py @@ -110,7 +110,7 @@ class Simulation3DIntegral(BasePFSimulation): .. deprecated:: 0.23.0 - Keyword argument ``ind_active`` is deprecated in favor of + Argument ``ind_active`` is deprecated in favor of ``active_cells`` and will be removed in SimPEG v0.24.0. """ diff --git a/simpeg/potential_fields/magnetics/simulation.py b/simpeg/potential_fields/magnetics/simulation.py index 8e1b0be6d2..a695cd280e 100644 --- a/simpeg/potential_fields/magnetics/simulation.py +++ b/simpeg/potential_fields/magnetics/simulation.py @@ -87,7 +87,7 @@ class Simulation3DIntegral(BasePFSimulation): .. deprecated:: 0.23.0 - Keyword argument ``ind_active`` is deprecated in favor of + Argument ``ind_active`` is deprecated in favor of ``active_cells`` and will be removed in SimPEG v0.24.0. """ From 437fa397952933fae2f8225540bca44dc8250e35 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 20 Sep 2024 11:17:18 -0700 Subject: [PATCH 062/194] Replace `indActive` and `actInd` for `active_cells` in maps (#1534) Deprecate the `indActive`, `actInd` arguments and properties from maps and replace them for `active_cells`. Deprecate the `valInactive` argument and property from `InjectActiveCells` and replace it for `value_inactive`. Add tests for the deprecations. Avoid a try and except pattern in the setter for `value_inactive`: we don't want to ignore any error that might happen in those lines. Update usage of `actInd`, `indActive` and `valInactive` in tutorials and examples. Part of #1121 --- ...cip_dipoledipole_3Dinversion_twospheres.py | 2 +- examples/06-tdem/plot_inv_tdem_1D.py | 2 +- .../06-tdem/plot_inv_tdem_1D_raw_waveform.py | 2 +- .../plot_booky_1D_time_freq_inv.py | 4 +- .../plot_booky_1Dstitched_resolve_inv.py | 2 +- .../20-published/plot_heagyetal2017_casing.py | 10 +- .../plot_heagyetal2017_cyl_inversions.py | 4 +- ...nv_dcip_dipoledipole_2_5Dinversion_irls.py | 4 +- .../natural_source/utils/test_utils.py | 4 +- .../spectral_induced_polarization/run.py | 8 +- simpeg/maps/_injection.py | 178 ++++++++--- simpeg/maps/_parametric.py | 224 ++++++++++---- .../regularizations/test_regularization.py | 2 +- tests/base/test_maps.py | 278 ++++++++++++++++++ .../inversion/test_complex_resistivity.py | 8 +- .../01-models_mapping/plot_1_tensor_models.py | 4 +- .../01-models_mapping/plot_2_cyl_models.py | 4 +- .../01-models_mapping/plot_3_tree_models.py | 4 +- 18 files changed, 622 insertions(+), 122 deletions(-) diff --git a/examples/04-dcip/plot_inv_dcip_dipoledipole_3Dinversion_twospheres.py b/examples/04-dcip/plot_inv_dcip_dipoledipole_3Dinversion_twospheres.py index 7f1ba7dfd2..ecf4fa0deb 100644 --- a/examples/04-dcip/plot_inv_dcip_dipoledipole_3Dinversion_twospheres.py +++ b/examples/04-dcip/plot_inv_dcip_dipoledipole_3Dinversion_twospheres.py @@ -153,7 +153,7 @@ def getCylinderPoints(xc, zc, r): # Setup Problem with exponential mapping and Active cells only in the core mesh expmap = maps.ExpMap(mesh) -mapactive = maps.InjectActiveCells(mesh=mesh, indActive=actind, valInactive=-5.0) +mapactive = maps.InjectActiveCells(mesh=mesh, active_cells=actind, value_inactive=-5.0) mapping = expmap * mapactive problem = DC.Simulation3DCellCentered( mesh, survey=survey, sigmaMap=mapping, solver=Solver, bc_type="Neumann" diff --git a/examples/06-tdem/plot_inv_tdem_1D.py b/examples/06-tdem/plot_inv_tdem_1D.py index 992dbb13fb..8a581ba273 100644 --- a/examples/06-tdem/plot_inv_tdem_1D.py +++ b/examples/06-tdem/plot_inv_tdem_1D.py @@ -56,7 +56,7 @@ def run(plotIt=True): data = simulation.make_synthetic_data(mtrue, relative_error=rel_err) dmisfit = data_misfit.L2DataMisfit(simulation=simulation, data=data) - regMesh = discretize.TensorMesh([mesh.h[2][mapping.maps[-1].indActive]]) + regMesh = discretize.TensorMesh([mesh.h[2][mapping.maps[-1].active_cells]]) reg = regularization.WeightedLeastSquares(regMesh, alpha_s=1e-2, alpha_x=1.0) opt = optimization.InexactGaussNewton(maxIter=5, LSshorten=0.5) invProb = inverse_problem.BaseInvProblem(dmisfit, reg, opt) diff --git a/examples/06-tdem/plot_inv_tdem_1D_raw_waveform.py b/examples/06-tdem/plot_inv_tdem_1D_raw_waveform.py index 06b793bb51..de8aa68f5c 100644 --- a/examples/06-tdem/plot_inv_tdem_1D_raw_waveform.py +++ b/examples/06-tdem/plot_inv_tdem_1D_raw_waveform.py @@ -80,7 +80,7 @@ def run(plotIt=True): data = prb.make_synthetic_data(mtrue, relative_error=0.02, noise_floor=1e-11) dmisfit = data_misfit.L2DataMisfit(simulation=prb, data=data) - regMesh = discretize.TensorMesh([mesh.h[2][mapping.maps[-1].indActive]]) + regMesh = discretize.TensorMesh([mesh.h[2][mapping.maps[-1].active_cells]]) reg = regularization.WeightedLeastSquares(regMesh) opt = optimization.InexactGaussNewton(maxIter=5, LSshorten=0.5) invProb = inverse_problem.BaseInvProblem(dmisfit, reg, opt) diff --git a/examples/20-published/plot_booky_1D_time_freq_inv.py b/examples/20-published/plot_booky_1D_time_freq_inv.py index dd42c3938d..a1b7ee6721 100644 --- a/examples/20-published/plot_booky_1D_time_freq_inv.py +++ b/examples/20-published/plot_booky_1D_time_freq_inv.py @@ -244,7 +244,7 @@ def run(plotIt=True, saveFig=False, cleanup=True): dmisfit = data_misfit.L2DataMisfit(simulation=prb, data=data_resolve) # Regularization - regMesh = discretize.TensorMesh([mesh.h[2][mapping.maps[-1].indActive]]) + regMesh = discretize.TensorMesh([mesh.h[2][mapping.maps[-1].active_cells]]) reg = regularization.WeightedLeastSquares( regMesh, mapping=maps.IdentityMap(regMesh) ) @@ -360,7 +360,7 @@ def run(plotIt=True, saveFig=False, cleanup=True): dmisfit = data_misfit.L2DataMisfit(simulation=prob, data=data_sky) # Regularization - regMesh = discretize.TensorMesh([mesh.h[2][mapping.maps[-1].indActive]]) + regMesh = discretize.TensorMesh([mesh.h[2][mapping.maps[-1].active_cells]]) reg = regularization.WeightedLeastSquares( regMesh, mapping=maps.IdentityMap(regMesh) ) diff --git a/examples/20-published/plot_booky_1Dstitched_resolve_inv.py b/examples/20-published/plot_booky_1Dstitched_resolve_inv.py index ec900f6da8..17192c1157 100644 --- a/examples/20-published/plot_booky_1Dstitched_resolve_inv.py +++ b/examples/20-published/plot_booky_1Dstitched_resolve_inv.py @@ -125,7 +125,7 @@ def resolve_1Dinversions( dmisfit = data_misfit.L2DataMisfit(simulation=prb, data=dat) # regularization - regMesh = discretize.TensorMesh([mesh.h[2][mapping.maps[-1].indActive]]) + regMesh = discretize.TensorMesh([mesh.h[2][mapping.maps[-1].active_cells]]) reg = regularization.WeightedLeastSquares(regMesh) reg.reference_model = mref diff --git a/examples/20-published/plot_heagyetal2017_casing.py b/examples/20-published/plot_heagyetal2017_casing.py index fda876b955..337dc3b1a6 100644 --- a/examples/20-published/plot_heagyetal2017_casing.py +++ b/examples/20-published/plot_heagyetal2017_casing.py @@ -244,13 +244,13 @@ def primaryMapping(self): # inject casing parameters so they are included in the construction # of the layered background + casing injectCasingParams = maps.InjectActiveCells( - None, indActive=np.r_[0, 1, 4, 5], valInactive=valInactive, nC=10 + None, active_cells=np.r_[0, 1, 4, 5], value_inactive=valInactive, nC=10 ) # maps a list of casing parameters to the cyl mesh (below the # subsurface) paramMapPrimary = maps.ParametricCasingAndLayer( - self.meshp, indActive=self.indActivePrimary, slopeFact=1e4 + self.meshp, active_cells=self.indActivePrimary, slopeFact=1e4 ) # inject air cells @@ -538,7 +538,9 @@ def mapping(self): # model on our mesh if getattr(self, "_mapping", None) is None: print("building secondary mapping") - paramMap = maps.ParametricBlockInLayer(self.meshs, indActive=self.indActive) + paramMap = maps.ParametricBlockInLayer( + self.meshs, active_cells=self.indActive + ) self._mapping = ( self.expMap * self.injActMap # log sigma --> sigma @@ -555,7 +557,7 @@ def primaryMap2meshs(self): # block) print("Building primaryMap2meshs") paramMapPrimaryMeshs = maps.ParametricLayer( - self.meshs, indActive=self.indActive + self.meshs, active_cells=self.indActive ) self._primaryMap2mesh = ( diff --git a/examples/20-published/plot_heagyetal2017_cyl_inversions.py b/examples/20-published/plot_heagyetal2017_cyl_inversions.py index 53f328aeaf..0c72149b2e 100644 --- a/examples/20-published/plot_heagyetal2017_cyl_inversions.py +++ b/examples/20-published/plot_heagyetal2017_cyl_inversions.py @@ -102,7 +102,7 @@ def run(plotIt=True, saveFig=False): # FDEM inversion np.random.seed(1) dmisfit = data_misfit.L2DataMisfit(simulation=prbFD, data=dataFD) - regMesh = discretize.TensorMesh([mesh.h[2][mapping.maps[-1].indActive]]) + regMesh = discretize.TensorMesh([mesh.h[2][mapping.maps[-1].active_cells]]) reg = regularization.WeightedLeastSquares(regMesh) opt = optimization.InexactGaussNewton(maxIterCG=10) invProb = inverse_problem.BaseInvProblem(dmisfit, reg, opt) @@ -148,7 +148,7 @@ def run(plotIt=True, saveFig=False): # TDEM inversion dmisfit = data_misfit.L2DataMisfit(simulation=prbTD, data=dataTD) - regMesh = discretize.TensorMesh([mesh.h[2][mapping.maps[-1].indActive]]) + regMesh = discretize.TensorMesh([mesh.h[2][mapping.maps[-1].active_cells]]) reg = regularization.WeightedLeastSquares(regMesh) opt = optimization.InexactGaussNewton(maxIterCG=10) invProb = inverse_problem.BaseInvProblem(dmisfit, reg, opt) diff --git a/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion_irls.py b/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion_irls.py index a2c67c696a..03b43b5003 100644 --- a/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion_irls.py +++ b/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion_irls.py @@ -111,7 +111,9 @@ def run(plotIt=True, survey_type="dipole-dipole", p=0.0, qx=2.0, qz=2.0): plt.show() # Use Exponential Map: m = log(rho) - actmap = maps.InjectActiveCells(mesh, indActive=actind, valInactive=np.log(1e8)) + actmap = maps.InjectActiveCells( + mesh, active_cells=actind, value_inactive=np.log(1e8) + ) mapping = maps.ExpMap(mesh) * actmap # Generate mtrue diff --git a/simpeg/electromagnetics/natural_source/utils/test_utils.py b/simpeg/electromagnetics/natural_source/utils/test_utils.py index 878ddaea82..8effd747de 100644 --- a/simpeg/electromagnetics/natural_source/utils/test_utils.py +++ b/simpeg/electromagnetics/natural_source/utils/test_utils.py @@ -242,7 +242,7 @@ def setupSimpegNSEM_tests_location_assign_list( # Set the mapping actMap = maps.InjectActiveCells( - mesh=mesh, indActive=active, valInactive=np.log(1e-8) + mesh=mesh, active_cells=active, value_inactive=np.log(1e-8) ) mapping = maps.ExpMap(mesh) * actMap # print(survey_ns.source_list) @@ -367,7 +367,7 @@ def setupSimpegNSEM_PrimarySecondary(inputSetup, freqs, comp="Imp", singleFreq=F # Set the mapping actMap = maps.InjectActiveCells( - mesh=mesh, indActive=active, valInactive=np.log(1e-8) + mesh=mesh, active_cells=active, value_inactive=np.log(1e-8) ) mapping = maps.ExpMap(mesh) * actMap # print(survey_ns.source_list) diff --git a/simpeg/electromagnetics/static/spectral_induced_polarization/run.py b/simpeg/electromagnetics/static/spectral_induced_polarization/run.py index 93ee3cf5ed..36bedcbb40 100644 --- a/simpeg/electromagnetics/static/spectral_induced_polarization/run.py +++ b/simpeg/electromagnetics/static/spectral_induced_polarization/run.py @@ -41,12 +41,14 @@ def spectral_ip_mappings( indActive = np.ones(mesh.nC, dtype=bool) actmap_eta = maps.InjectActiveCells( - mesh, indActive=indActive, valInactive=inactive_eta + mesh, active_cells=indActive, value_inactive=inactive_eta ) actmap_tau = maps.InjectActiveCells( - mesh, indActive=indActive, valInactive=inactive_tau + mesh, active_cells=indActive, value_inactive=inactive_tau + ) + actmap_c = maps.InjectActiveCells( + mesh, active_cells=indActive, value_inactive=inactive_c ) - actmap_c = maps.InjectActiveCells(mesh, indActive=indActive, valInactive=inactive_c) wires = maps.Wires( ("eta", indActive.sum()), ("tau", indActive.sum()), ("c", indActive.sum()) diff --git a/simpeg/maps/_injection.py b/simpeg/maps/_injection.py index 4b80d6cb7f..e99c5bad10 100644 --- a/simpeg/maps/_injection.py +++ b/simpeg/maps/_injection.py @@ -2,9 +2,11 @@ Injection and interpolation map classes. """ +import warnings import discretize import numpy as np import scipy.sparse as sp +from numbers import Number from ..utils import ( validate_type, @@ -13,6 +15,7 @@ validate_active_indices, ) from ._base import IdentityMap +from ..utils.code_utils import deprecate_property class Mesh2Mesh(IdentityMap): @@ -20,7 +23,7 @@ class Mesh2Mesh(IdentityMap): Takes a model on one mesh are translates it to another mesh. """ - def __init__(self, meshes, indActive=None, **kwargs): + def __init__(self, meshes, active_cells=None, indActive=None, **kwargs): # Sanity checks for the meshes parameter try: mesh, mesh2 = meshes @@ -36,7 +39,24 @@ def __init__(self, meshes, indActive=None, **kwargs): f"Found meshes with dimensions '{mesh.dim}' and '{mesh2.dim}'. " + "Both meshes must have the same dimension." ) - self.indActive = indActive + + # Deprecate indActive argument + if indActive is not None: + if active_cells is not None: + raise TypeError( + "Cannot pass both 'active_cells' and 'indActive'." + "'indActive' has been deprecated and will be removed in " + " SimPEG v0.24.0, please use 'active_cells' instead.", + ) + warnings.warn( + "'indActive' has been deprecated and will be removed in " + " SimPEG v0.24.0, please use 'active_cells' instead.", + FutureWarning, + stacklevel=2, + ) + active_cells = indActive + + self.active_cells = active_cells # reset to not accepted None for mesh @IdentityMap.mesh.setter @@ -60,28 +80,37 @@ def mesh2(self, value): ) @property - def indActive(self): + def active_cells(self): """Active indices on target mesh. Returns ------- (mesh.n_cells) numpy.ndarray of bool or none """ - return self._indActive + return self._active_cells - @indActive.setter - def indActive(self, value): + @active_cells.setter + def active_cells(self, value): if value is not None: - value = validate_active_indices("indActive", value, self.mesh.n_cells) - self._indActive = value + value = validate_active_indices("active_cells", value, self.mesh.n_cells) + self._active_cells = value + + indActive = deprecate_property( + active_cells, + "indActive", + "active_cells", + removal_version="0.24.0", + future_warn=True, + error=False, + ) @property def P(self): if getattr(self, "_P", None) is None: self._P = self.mesh2.get_interpolation_matrix( ( - self.mesh.cell_centers[self.indActive, :] - if self.indActive is not None + self.mesh.cell_centers[self.active_cells, :] + if self.active_cells is not None else self.mesh.cell_centers ), "CC", @@ -92,8 +121,8 @@ def P(self): @property def shape(self): """Number of parameters in the model.""" - if self.indActive is not None: - return (self.indActive.sum(), self.mesh2.nC) + if self.active_cells is not None: + return (self.active_cells.sum(), self.mesh2.nC) return (self.mesh.nC, self.mesh2.nC) @property @@ -131,50 +160,112 @@ class InjectActiveCells(IdentityMap): ---------- mesh : discretize.BaseMesh A discretize mesh - indActive : numpy.ndarray + active_cells : numpy.ndarray Active cells array. Can be a boolean ``numpy.ndarray`` of length *mesh.nC* or a ``numpy.ndarray`` of ``int`` containing the indices of the active cells. - valInactive : float or numpy.ndarray + value_inactive : float or numpy.ndarray The physical property value assigned to all inactive cells in the mesh + indActive : numpy.ndarray + + .. deprecated:: 0.23.0 + + Argument ``indActive`` is deprecated in favor of ``active_cells`` and will + be removed in SimPEG v0.24.0. + + valInactive : float or numpy.ndarray + + .. deprecated:: 0.23.0 + + Argument ``valInactive`` is deprecated in favor of ``value_inactive`` and + will be removed in SimPEG v0.24.0. """ - def __init__(self, mesh, indActive=None, valInactive=0.0, nC=None): + def __init__( + self, + mesh, + active_cells=None, + value_inactive=0.0, + nC=None, + indActive=None, + valInactive=0.0, + ): self.mesh = mesh self.nC = nC or mesh.nC - self._indActive = validate_active_indices("indActive", indActive, self.nC) - self._nP = np.sum(self.indActive) + # Deprecate indActive argument + if indActive is not None: + if active_cells is not None: + raise TypeError( + "Cannot pass both 'active_cells' and 'indActive'." + "'indActive' has been deprecated and will be removed in " + " SimPEG v0.24.0, please use 'active_cells' instead.", + ) + warnings.warn( + "'indActive' has been deprecated and will be removed in " + " SimPEG v0.24.0, please use 'active_cells' instead.", + FutureWarning, + stacklevel=2, + ) + active_cells = indActive + + # Deprecate valInactive argument + if not isinstance(valInactive, Number) or valInactive != 0.0: + if not isinstance(value_inactive, Number) or value_inactive != 0.0: + raise TypeError( + "Cannot pass both 'value_inactive' and 'valInactive'." + "'valInactive' has been deprecated and will be removed in " + " SimPEG v0.24.0, please use 'value_inactive' instead.", + ) + warnings.warn( + "'valInactive' has been deprecated and will be removed in " + " SimPEG v0.24.0, please use 'value_inactive' instead.", + FutureWarning, + stacklevel=2, + ) + value_inactive = valInactive + + self.active_cells = active_cells + self._nP = np.sum(self.active_cells) - self.P = sp.eye(self.nC, format="csr")[:, self.indActive] + self.P = sp.eye(self.nC, format="csr")[:, self.active_cells] - self.valInactive = valInactive + self.value_inactive = value_inactive @property - def valInactive(self): + def value_inactive(self): """The physical property value assigned to all inactive cells in the mesh. Returns ------- numpy.ndarray """ - return self._valInactive + return self._value_inactive - @valInactive.setter - def valInactive(self, value): + @value_inactive.setter + def value_inactive(self, value): n_inactive = self.nC - self.nP - try: - value = validate_float("valInactive", value) + if isinstance(value, Number): + value = validate_float("value_inactive", value) value = np.full(n_inactive, value) - except Exception: - pass - value = validate_ndarray_with_shape("valInactive", value, shape=(n_inactive,)) - - self._valInactive = np.zeros(self.nC, dtype=float) - self._valInactive[~self.indActive] = value + value = validate_ndarray_with_shape( + "value_inactive", value, shape=(n_inactive,) + ) + value_inactive = np.zeros(self.nC, dtype=float) + value_inactive[~self.active_cells] = value + self._value_inactive = value_inactive + + valInactive = deprecate_property( + value_inactive, + "valInactive", + "value_inactive", + removal_version="0.24.0", + future_warn=True, + error=False, + ) @property - def indActive(self): + def active_cells(self): """ Returns @@ -182,7 +273,22 @@ def indActive(self): numpy.ndarray of bool """ - return self._indActive + return self._active_cells + + @active_cells.setter + def active_cells(self, value): + if value is not None: + value = validate_active_indices("active_cells", value, self.nC) + self._active_cells = value + + indActive = deprecate_property( + active_cells, + "indActive", + "active_cells", + removal_version="0.24.0", + future_warn=True, + error=False, + ) @property def shape(self): @@ -206,12 +312,12 @@ def nP(self): int Number of parameters the model acts on; i.e. the number of active cells """ - return int(self.indActive.sum()) + return int(self.active_cells.sum()) def _transform(self, m): if m.ndim > 1: - return self.P * m + self.valInactive[:, None] - return self.P * m + self.valInactive + return self.P * m + self.value_inactive[:, None] + return self.P * m + self.value_inactive def inverse(self, u): r"""Recover the model parameters (active cells) from a set of physical diff --git a/simpeg/maps/_parametric.py b/simpeg/maps/_parametric.py index db52989620..814808eb84 100644 --- a/simpeg/maps/_parametric.py +++ b/simpeg/maps/_parametric.py @@ -2,6 +2,7 @@ Parametric map classes. """ +import warnings import discretize import numpy as np from numpy.polynomial import polynomial @@ -19,6 +20,7 @@ validate_active_indices, ) from ._base import IdentityMap +from ..utils.code_utils import deprecate_property class ParametricCircleMap(IdentityMap): @@ -329,9 +331,16 @@ class ParametricPolyMap(IdentityMap): If ``True``, parameters :math:`\sigma_1` and :math:`\sigma_2` represent the natural log of a physical property. normal : {'x', 'y', 'z'} - actInd : numpy.ndarray - Active cells array. Can be a boolean ``numpy.ndarray`` of length *mesh.nC* - or a ``numpy.ndarray`` of ``int`` containing the indices of the active cells. + active_cells : (n_cells) numpy.ndarray, optional + Active cells array. Can be a boolean ``numpy.ndarray`` of length + ``mesh.n_cells`` or a ``numpy.ndarray`` of ``int`` containing the + indices of the active cells. + actInd : numpy.ndarray, optional + + .. deprecated:: 0.23.0 + + Argument ``actInd`` is deprecated in favor of ``active_cells`` and will + be removed in SimPEG v0.24.0. Examples -------- @@ -355,7 +364,7 @@ class ParametricPolyMap(IdentityMap): >>> model = np.r_[sig1, sig2, c0, c1] >>> poly_map = ParametricPolyMap( - >>> mesh, order=1, logSigma=False, normal='Y', actInd=ind_active, slope=1e4 + >>> mesh, order=1, logSigma=False, normal='Y', active_cells=ind_active, slope=1e4 >>> ) >>> act_map = InjectActiveCells(mesh, ind_active, 0.) @@ -376,7 +385,7 @@ class ParametricPolyMap(IdentityMap): >>> model = np.r_[sig1, sig2, c0, cx, cy, cxy] >>> >>> poly_map = ParametricPolyMap( - >>> mesh, order=[1, 1], logSigma=False, normal='Z', actInd=ind_active, slope=2 + >>> mesh, order=[1, 1], logSigma=False, normal='Z', active_cells=ind_active, slope=2 >>> ) >>> act_map = InjectActiveCells(mesh, ind_active, 0.) >>> @@ -387,16 +396,41 @@ class ParametricPolyMap(IdentityMap): """ - def __init__(self, mesh, order, logSigma=True, normal="X", actInd=None, slope=1e4): + def __init__( + self, + mesh, + order, + logSigma=True, + normal="X", + active_cells=None, + slope=1e4, + actInd=None, + ): super().__init__(mesh=mesh) self.logSigma = logSigma self.order = order self.normal = normal self.slope = slope - if actInd is None: - actInd = np.ones(mesh.n_cells, dtype=bool) - self.actInd = actInd + # Deprecate actInd argument + if actInd is not None: + if active_cells is not None: + raise TypeError( + "Cannot pass both 'active_cells' and 'actInd'." + "'actInd' has been deprecated and will be removed in " + " SimPEG v0.24.0, please use 'active_cells' instead.", + ) + warnings.warn( + "'actInd' has been deprecated and will be removed in " + " SimPEG v0.24.0, please use 'active_cells' instead.", + FutureWarning, + stacklevel=2, + ) + active_cells = actInd + + if active_cells is None: + active_cells = np.ones(mesh.n_cells, dtype=bool) + self.active_cells = active_cells @property def slope(self): @@ -443,19 +477,30 @@ def normal(self, value): self._normal = validate_string("normal", value, ("x", "y", "z")) @property - def actInd(self): + def active_cells(self): """Active indices of the mesh. Returns ------- (mesh.n_cells) numpy.ndarray of bool """ - return self._actInd + return self._active_cells - @actInd.setter - def actInd(self, value): - self._actInd = validate_active_indices("actInd", value, self.mesh.n_cells) - self._nC = sum(self._actInd) + @active_cells.setter + def active_cells(self, value): + self._active_cells = validate_active_indices( + "active_cells", value, self.mesh.n_cells + ) + self._nC = sum(self._active_cells) + + actInd = deprecate_property( + active_cells, + "actInd", + "active_cells", + removal_version="0.24.0", + future_warn=True, + error=False, + ) @property def shape(self): @@ -467,7 +512,7 @@ def shape(self): The dimensions of the mapping as a tuple of the form (*nC* , *nP*), where *nP* is the number of model parameters the mapping acts on and *nC* is the number of active cells - being mapping to. If *actInd* is ``None``, then + being mapping to. If ``active_cells`` is ``None``, then *nC = mesh.nC*. """ return (self.nC, self.nP) @@ -507,8 +552,8 @@ def _transform(self, m): # 2D if self.mesh.dim == 2: - X = self.mesh.cell_centers[self.actInd, 0] - Y = self.mesh.cell_centers[self.actInd, 1] + X = self.mesh.cell_centers[self.active_cells, 0] + Y = self.mesh.cell_centers[self.active_cells, 1] if self.normal == "x": f = polynomial.polyval(Y, c) - X elif self.normal == "y": @@ -518,9 +563,9 @@ def _transform(self, m): # 3D elif self.mesh.dim == 3: - X = self.mesh.cell_centers[self.actInd, 0] - Y = self.mesh.cell_centers[self.actInd, 1] - Z = self.mesh.cell_centers[self.actInd, 2] + X = self.mesh.cell_centers[self.active_cells, 0] + Y = self.mesh.cell_centers[self.active_cells, 1] + Z = self.mesh.cell_centers[self.active_cells, 2] if self.normal == "x": f = ( @@ -596,8 +641,8 @@ def deriv(self, m, v=None): # 2D if self.mesh.dim == 2: - X = self.mesh.cell_centers[self.actInd, 0] - Y = self.mesh.cell_centers[self.actInd, 1] + X = self.mesh.cell_centers[self.active_cells, 0] + Y = self.mesh.cell_centers[self.active_cells, 1] if self.normal == "x": f = polynomial.polyval(Y, c) - X @@ -610,9 +655,9 @@ def deriv(self, m, v=None): # 3D elif self.mesh.dim == 3: - X = self.mesh.cell_centers[self.actInd, 0] - Y = self.mesh.cell_centers[self.actInd, 1] - Z = self.mesh.cell_centers[self.actInd, 2] + X = self.mesh.cell_centers[self.active_cells, 0] + Y = self.mesh.cell_centers[self.active_cells, 1] + Z = self.mesh.cell_centers[self.active_cells, 2] if self.normal == "x": f = ( @@ -1049,21 +1094,53 @@ class BaseParametric(IdentityMap): ---------- mesh : discretize.BaseMesh A discretize mesh - indActive : numpy.ndarray, optional - Active cells array. Can be a boolean ``numpy.ndarray`` of length *mesh.nC* - or a ``numpy.ndarray`` of ``int`` containing the indices of the active cells. slope : float, optional Directly set the scaling parameter *slope* which sets the sharpness of boundaries between units. slopeFact : float, optional Set sharpness of boundaries between units based on minimum cell size. If set, the scalaing parameter *slope = slopeFact / dh*. + active_cells : numpy.ndarray, optional + Active cells array. Can be a boolean ``numpy.ndarray`` of length *mesh.nC* + or a ``numpy.ndarray`` of ``int`` containing the indices of the active cells. + indActive : numpy.ndarray + + .. deprecated:: 0.23.0 + + Argument ``indActive`` is deprecated in favor of ``active_cells`` and will + be removed in SimPEG v0.24.0. + """ - def __init__(self, mesh, slope=None, slopeFact=1.0, indActive=None, **kwargs): + def __init__( + self, + mesh, + slope=None, + slopeFact=1.0, + active_cells=None, + indActive=None, + **kwargs, + ): super(BaseParametric, self).__init__(mesh, **kwargs) - self.indActive = indActive + + # Deprecate indActive argument + if indActive is not None: + if active_cells is not None: + raise TypeError( + "Cannot pass both 'active_cells' and 'indActive'." + "'indActive' has been deprecated and will be removed in " + " SimPEG v0.24.0, please use 'active_cells' instead.", + ) + warnings.warn( + "'indActive' has been deprecated and will be removed in " + " SimPEG v0.24.0, please use 'active_cells' instead.", + FutureWarning, + stacklevel=2, + ) + active_cells = indActive + + self.active_cells = active_cells self.slopeFact = slopeFact if slope is not None: self.slope = slope @@ -1098,14 +1175,23 @@ def slopeFact(self, value): self.slope = self._slopeFact / self.mesh.edge_lengths.min() @property - def indActive(self): - return self._indActive + def active_cells(self): + return self._active_cells - @indActive.setter - def indActive(self, value): + @active_cells.setter + def active_cells(self, value): if value is not None: - value = validate_active_indices("indActive", value, self.mesh.n_cells) - self._indActive = value + value = validate_active_indices("active_cells", value, self.mesh.n_cells) + self._active_cells = value + + indActive = deprecate_property( + active_cells, + "indActive", + "active_cells", + removal_version="0.24.0", + future_warn=True, + error=False, + ) @property def x(self): @@ -1121,16 +1207,16 @@ def x(self): self._x = [ ( self.mesh.cell_centers - if self.indActive is None - else self.mesh.cell_centers[self.indActive] + if self.active_cells is None + else self.mesh.cell_centers[self.active_cells] ) ][0] else: self._x = [ ( self.mesh.cell_centers[:, 0] - if self.indActive is None - else self.mesh.cell_centers[self.indActive, 0] + if self.active_cells is None + else self.mesh.cell_centers[self.active_cells, 0] ) ][0] return self._x @@ -1149,8 +1235,8 @@ def y(self): self._y = [ ( self.mesh.cell_centers[:, 1] - if self.indActive is None - else self.mesh.cell_centers[self.indActive, 1] + if self.active_cells is None + else self.mesh.cell_centers[self.active_cells, 1] ) ][0] else: @@ -1171,8 +1257,8 @@ def z(self): self._z = [ ( self.mesh.cell_centers[:, 2] - if self.indActive is None - else self.mesh.cell_centers[self.indActive, 2] + if self.active_cells is None + else self.mesh.cell_centers[self.active_cells, 2] ) ][0] else: @@ -1226,7 +1312,7 @@ class ParametricLayer(BaseParametric): ---------- mesh : discretize.BaseMesh A discretize mesh - indActive : numpy.ndarray + active_cells : numpy.ndarray, optional Active cells array. Can be a boolean ``numpy.ndarray`` of length *mesh.nC* or a ``numpy.ndarray`` of ``int`` containing the indices of the active cells. slope : float @@ -1235,6 +1321,12 @@ class ParametricLayer(BaseParametric): slopeFact : float Scaling factor for the sharpness of the boundaries based on cell size. Using this option, we set *a = slopeFact / dh*. + indActive : numpy.ndarray + + .. deprecated:: 0.23.0 + + Argument ``indActive`` is deprecated in favor of ``active_cells`` and will + be removed in SimPEG v0.24.0. Examples -------- @@ -1256,7 +1348,7 @@ class ParametricLayer(BaseParametric): >>> model = np.r_[sig0, sig1, zL, h] >>> layer_map = ParametricLayer( - >>> mesh, indActive=ind_active, slope=4 + >>> mesh, active_cells=ind_active, slope=4 >>> ) >>> act_map = InjectActiveCells(mesh, ind_active, 0.) @@ -1291,8 +1383,8 @@ def shape(self): and *nAct* is the number of active cells in the mesh, **shape** returns a tuple (*nAct* , *4*). """ - if self.indActive is not None: - return (sum(self.indActive), self.nP) + if self.active_cells is not None: + return (sum(self.active_cells), self.nP) return (self.mesh.nC, self.nP) def mDict(self, m): @@ -1484,7 +1576,7 @@ class ParametricBlock(BaseParametric): ---------- mesh : discretize.BaseMesh A discretize mesh - indActive : numpy.ndarray + active_cells : numpy.ndarray Active cells array. Can be a boolean ``numpy.ndarray`` of length *mesh.nC* or a ``numpy.ndarray`` of ``int`` containing the indices of the active cells. slope : float @@ -1497,6 +1589,12 @@ class ParametricBlock(BaseParametric): Epsilon value used in the ekblom representation of the block p : float p-value used in the ekblom representation of the block. + indActive : numpy.ndarray + + .. deprecated:: 0.23.0 + + Argument ``indActive`` is deprecated in favor of ``active_cells`` and will + be removed in SimPEG v0.24.0. Examples -------- @@ -1517,7 +1615,7 @@ class ParametricBlock(BaseParametric): >>> sig0, sigb, xb, Lx, yb, Ly = 5., 10., 5., 4., 4., 2. >>> model = np.r_[sig0, sigb, xb, Lx, yb, Ly] - >>> block_map = ParametricBlock(mesh, indActive=ind_active) + >>> block_map = ParametricBlock(mesh, active_cells=ind_active) >>> act_map = InjectActiveCells(mesh, ind_active, 0.) >>> fig = plt.figure(figsize=(5, 5)) @@ -1591,8 +1689,8 @@ def shape(self): and *nAct* is the number of active cells in the mesh, **shape** returns a tuple (*nAct* , *nP*). """ - if self.indActive is not None: - return (sum(self.indActive), self.nP) + if self.active_cells is not None: + return (sum(self.active_cells), self.nP) return (self.mesh.nC, self.nP) def _mDict1d(self, m): @@ -1829,7 +1927,7 @@ class ParametricEllipsoid(ParametricBlock): ---------- mesh : discretize.BaseMesh A discretize mesh - indActive : numpy.ndarray + active_cells : numpy.ndarray Active cells array. Can be a boolean ``numpy.ndarray`` of length *mesh.nC* or a ``numpy.ndarray`` of ``int`` containing the indices of the active cells. slope : float @@ -1840,6 +1938,12 @@ class ParametricEllipsoid(ParametricBlock): Using this option, we set *a = slopeFact / dh*. epsilon : float Epsilon value used in the ekblom representation of the block + indActive : numpy.ndarray + + .. deprecated:: 0.23.0 + + Argument ``indActive`` is deprecated in favor of ``active_cells`` and will + be removed in SimPEG v0.24.0. Examples -------- @@ -1860,7 +1964,7 @@ class ParametricEllipsoid(ParametricBlock): >>> sig0, sigb, xb, Lx, yb, Ly = 5., 10., 5., 4., 4., 3. >>> model = np.r_[sig0, sigb, xb, Lx, yb, Ly] - >>> ellipsoid_map = ParametricEllipsoid(mesh, indActive=ind_active) + >>> ellipsoid_map = ParametricEllipsoid(mesh, active_cells=ind_active) >>> act_map = InjectActiveCells(mesh, ind_active, 0.) >>> fig = plt.figure(figsize=(5, 5)) @@ -1906,8 +2010,8 @@ def nP(self): @property def shape(self): - if self.indActive is not None: - return (sum(self.indActive), self.nP) + if self.active_cells is not None: + return (sum(self.active_cells), self.nP) return (self.mesh.nC, self.nP) def mDict(self, m): @@ -2234,7 +2338,7 @@ class ParametricBlockInLayer(ParametricLayer): spacing to give the slope of the arctan functions :param float slope: slope of the arctan function - :param numpy.ndarray indActive: bool vector with + :param numpy.ndarray active_cells: bool vector with """ @@ -2250,8 +2354,8 @@ def nP(self): @property def shape(self): - if self.indActive is not None: - return (sum(self.indActive), self.nP) + if self.active_cells is not None: + return (sum(self.active_cells), self.nP) return (self.mesh.nC, self.nP) def _mDict2d(self, m): diff --git a/tests/base/regularizations/test_regularization.py b/tests/base/regularizations/test_regularization.py index 257dca9dba..9c5023c30f 100644 --- a/tests/base/regularizations/test_regularization.py +++ b/tests/base/regularizations/test_regularization.py @@ -469,7 +469,7 @@ def test_nC_residual(self): ) mapping = maps.ExpMap(mesh) * maps.SurjectVertical1D(mesh) * actMap - regMesh = discretize.TensorMesh([mesh.h[2][mapping.maps[-1].indActive]]) + regMesh = discretize.TensorMesh([mesh.h[2][mapping.maps[-1].active_cells]]) reg = regularization.WeightedLeastSquares(regMesh) self.assertTrue(reg._nC_residual == regMesh.nC) diff --git a/tests/base/test_maps.py b/tests/base/test_maps.py index f5a815a819..0441adf7d3 100644 --- a/tests/base/test_maps.py +++ b/tests/base/test_maps.py @@ -1,3 +1,4 @@ +from copy import deepcopy import numpy as np import unittest import discretize @@ -8,6 +9,13 @@ from discretize.utils import mesh_builder_xyz, refine_tree_xyz, active_from_xyz import inspect +from simpeg.maps._parametric import ( + BaseParametric, + ParametricLayer, + ParametricBlock, + ParametricEllipsoid, +) + TOL = 1e-14 np.random.seed(121) @@ -772,5 +780,275 @@ def test_linearity(): assert all(not m.is_linear for m in non_linear_maps) +class DeprecatedIndActive: + """Base class to test deprecated ``actInd`` and ``indActive`` arguments in maps.""" + + @pytest.fixture + def mesh(self): + """Sample mesh.""" + return discretize.TensorMesh([np.ones(10), np.ones(10)], "CN") + + @pytest.fixture + def active_cells(self, mesh): + """Sample active cells for the mesh.""" + active_cells = np.ones(mesh.n_cells, dtype=bool) + active_cells[0] = False + return active_cells + + def get_message_duplicated_error(self, old_name, new_name, version="v0.24.0"): + msg = ( + f"Cannot pass both '{new_name}' and '{old_name}'." + f"'{old_name}' has been deprecated and will be removed in " + f" SimPEG {version}, please use '{new_name}' instead." + ) + return msg + + def get_message_deprecated_warning(self, old_name, new_name, version="v0.24.0"): + msg = ( + f"'{old_name}' has been deprecated and will be removed in " + f" SimPEG {version}, please use '{new_name}' instead." + ) + return msg + + +class TestParametricPolyMap(DeprecatedIndActive): + """Test deprecated ``actInd`` in ParametricPolyMap.""" + + def test_warning_argument(self, mesh, active_cells): + """ + Test if warning is raised after passing ``actInd`` to the constructor. + """ + msg = self.get_message_deprecated_warning("actInd", "active_cells") + with pytest.warns(FutureWarning, match=msg): + maps.ParametricPolyMap(mesh, 2, actInd=active_cells) + + def test_error_duplicated_argument(self, mesh, active_cells): + """ + Test error after passing ``actInd`` and ``active_cells`` to the constructor. + """ + msg = self.get_message_duplicated_error("actInd", "active_cells") + with pytest.raises(TypeError, match=msg): + maps.ParametricPolyMap( + mesh, 2, active_cells=active_cells, actInd=active_cells + ) + + def test_warning_accessing_property(self, mesh, active_cells): + """ + Test warning when trying to access the ``actInd`` property. + """ + mapping = maps.ParametricPolyMap(mesh, 2, active_cells=active_cells) + msg = "actInd has been deprecated, please use active_cells" + with pytest.warns(FutureWarning, match=msg): + old_act_ind = mapping.actInd + np.testing.assert_allclose(mapping.active_cells, old_act_ind) + + def test_warning_setter(self, mesh, active_cells): + """ + Test warning when trying to set the ``actInd`` property. + """ + mapping = maps.ParametricPolyMap(mesh, 2, active_cells=active_cells) + # Define new active cells to pass to the setter + new_active_cells = active_cells.copy() + new_active_cells[-4:] = False + msg = "actInd has been deprecated, please use active_cells" + with pytest.warns(FutureWarning, match=msg): + mapping.actInd = new_active_cells + np.testing.assert_allclose(mapping.active_cells, new_active_cells) + + +class TestMesh2Mesh(DeprecatedIndActive): + """Test deprecated ``indActive`` in ``Mesh2Mesh``.""" + + @pytest.fixture + def meshes(self, mesh): + return [mesh, deepcopy(mesh)] + + def test_warning_argument(self, meshes, active_cells): + """ + Test if warning is raised after passing ``indActive`` to the constructor. + """ + msg = self.get_message_deprecated_warning("indActive", "active_cells") + with pytest.warns(FutureWarning, match=msg): + maps.Mesh2Mesh(meshes, indActive=active_cells) + + def test_error_duplicated_argument(self, meshes, active_cells): + """ + Test error after passing ``indActive`` and ``active_cells`` to the constructor. + """ + msg = self.get_message_duplicated_error("indActive", "active_cells") + with pytest.raises(TypeError, match=msg): + maps.Mesh2Mesh(meshes, active_cells=active_cells, indActive=active_cells) + + def test_warning_accessing_property(self, meshes, active_cells): + """ + Test warning when trying to access the ``indActive`` property. + """ + mapping = maps.Mesh2Mesh(meshes, active_cells=active_cells) + msg = "indActive has been deprecated, please use active_cells" + with pytest.warns(FutureWarning, match=msg): + old_act_ind = mapping.indActive + np.testing.assert_allclose(mapping.active_cells, old_act_ind) + + def test_warning_setter(self, meshes, active_cells): + """ + Test warning when trying to set the ``indActive`` property. + """ + mapping = maps.Mesh2Mesh(meshes, active_cells=active_cells) + # Define new active cells to pass to the setter + new_active_cells = active_cells.copy() + new_active_cells[-4:] = False + msg = "indActive has been deprecated, please use active_cells" + with pytest.warns(FutureWarning, match=msg): + mapping.indActive = new_active_cells + np.testing.assert_allclose(mapping.active_cells, new_active_cells) + + +class TestInjectActiveCells(DeprecatedIndActive): + """Test deprecated ``indActive`` and ``valInactive`` in ``InjectActiveCells``.""" + + def test_indactive_warning_argument(self, mesh, active_cells): + """ + Test if warning is raised after passing ``indActive`` to the constructor. + """ + msg = self.get_message_deprecated_warning("indActive", "active_cells") + with pytest.warns(FutureWarning, match=msg): + maps.InjectActiveCells(mesh, indActive=active_cells) + + def test_indactive_error_duplicated_argument(self, mesh, active_cells): + """ + Test error after passing ``indActive`` and ``active_cells`` to the constructor. + """ + msg = self.get_message_duplicated_error("indActive", "active_cells") + with pytest.raises(TypeError, match=msg): + maps.InjectActiveCells( + mesh, active_cells=active_cells, indActive=active_cells + ) + + def test_indactive_warning_accessing_property(self, mesh, active_cells): + """ + Test warning when trying to access the ``indActive`` property. + """ + mapping = maps.InjectActiveCells(mesh, active_cells=active_cells) + msg = "indActive has been deprecated, please use active_cells" + with pytest.warns(FutureWarning, match=msg): + old_act_ind = mapping.indActive + np.testing.assert_allclose(mapping.active_cells, old_act_ind) + + def test_indactive_warning_setter(self, mesh, active_cells): + """ + Test warning when trying to set the ``indActive`` property. + """ + mapping = maps.InjectActiveCells(mesh, active_cells=active_cells) + # Define new active cells to pass to the setter + new_active_cells = active_cells.copy() + new_active_cells[-4:] = False + msg = "indActive has been deprecated, please use active_cells" + with pytest.warns(FutureWarning, match=msg): + mapping.indActive = new_active_cells + np.testing.assert_allclose(mapping.active_cells, new_active_cells) + + @pytest.mark.parametrize("valInactive", (3.14, np.array([1]))) + def test_valinactive_warning_argument(self, mesh, active_cells, valInactive): + """ + Test if warning is raised after passing ``valInactive`` to the constructor. + """ + msg = self.get_message_deprecated_warning("valInactive", "value_inactive") + with pytest.warns(FutureWarning, match=msg): + maps.InjectActiveCells( + mesh, active_cells=active_cells, valInactive=valInactive + ) + + @pytest.mark.parametrize("valInactive", (3.14, np.array([3.14]))) + @pytest.mark.parametrize("value_inactive", (3.14, np.array([3.14]))) + def test_valinactive_error_duplicated_argument( + self, mesh, active_cells, valInactive, value_inactive + ): + """ + Test error after passing ``valInactive`` and ``value_inactive`` to the + constructor. + """ + msg = self.get_message_duplicated_error("valInactive", "value_inactive") + with pytest.raises(TypeError, match=msg): + maps.InjectActiveCells( + mesh, + active_cells=active_cells, + value_inactive=value_inactive, + valInactive=valInactive, + ) + + def test_valinactive_warning_accessing_property(self, mesh, active_cells): + """ + Test warning when trying to access the ``valInactive`` property. + """ + mapping = maps.InjectActiveCells( + mesh, active_cells=active_cells, value_inactive=3.14 + ) + msg = "valInactive has been deprecated, please use value_inactive" + with pytest.warns(FutureWarning, match=msg): + old_value = mapping.valInactive + np.testing.assert_allclose(mapping.value_inactive, old_value) + + def test_valinactive_warning_setter(self, mesh, active_cells): + """ + Test warning when trying to set the ``valInactive`` property. + """ + mapping = maps.InjectActiveCells( + mesh, active_cells=active_cells, value_inactive=3.14 + ) + msg = "valInactive has been deprecated, please use value_inactive" + with pytest.warns(FutureWarning, match=msg): + mapping.valInactive = 4.5 + np.testing.assert_allclose(mapping.value_inactive[~mapping.active_cells], 4.5) + + +class TestParametric(DeprecatedIndActive): + """Test deprecated ``indActive`` in parametric mappings.""" + + CLASSES = (BaseParametric, ParametricLayer, ParametricBlock, ParametricEllipsoid) + + @pytest.mark.parametrize("map_class", CLASSES) + def test_indactive_warning_argument(self, mesh, active_cells, map_class): + """ + Test if warning is raised after passing ``indActive`` to the constructor. + """ + msg = self.get_message_deprecated_warning("indActive", "active_cells") + with pytest.warns(FutureWarning, match=msg): + map_class(mesh, indActive=active_cells) + + @pytest.mark.parametrize("map_class", CLASSES) + def test_indactive_error_duplicated_argument(self, mesh, active_cells, map_class): + """ + Test error after passing ``indActive`` and ``active_cells`` to the constructor. + """ + msg = self.get_message_duplicated_error("indActive", "active_cells") + with pytest.raises(TypeError, match=msg): + map_class(mesh, active_cells=active_cells, indActive=active_cells) + + @pytest.mark.parametrize("map_class", CLASSES) + def test_indactive_warning_accessing_property(self, mesh, active_cells, map_class): + """ + Test warning when trying to access the ``indActive`` property. + """ + mapping = map_class(mesh, active_cells=active_cells) + msg = "indActive has been deprecated, please use active_cells" + with pytest.warns(FutureWarning, match=msg): + old_act_ind = mapping.indActive + np.testing.assert_allclose(mapping.active_cells, old_act_ind) + + @pytest.mark.parametrize("map_class", CLASSES) + def test_indactive_warning_setter(self, mesh, active_cells, map_class): + """ + Test warning when trying to set the ``indActive`` property. + """ + mapping = map_class(mesh, active_cells=active_cells) + # Define new active cells to pass to the setter + new_active_cells = active_cells.copy() + new_active_cells[-4:] = False + msg = "indActive has been deprecated, please use active_cells" + with pytest.warns(FutureWarning, match=msg): + mapping.indActive = new_active_cells + np.testing.assert_allclose(mapping.active_cells, new_active_cells) + + if __name__ == "__main__": unittest.main() diff --git a/tests/em/nsem/inversion/test_complex_resistivity.py b/tests/em/nsem/inversion/test_complex_resistivity.py index 67cdbbba2d..477a81b7e3 100644 --- a/tests/em/nsem/inversion/test_complex_resistivity.py +++ b/tests/em/nsem/inversion/test_complex_resistivity.py @@ -79,7 +79,7 @@ def create_simulation(self, rx_type="apparent_resistivity", rx_orientation="xy") # Set the mapping actMap = maps.InjectActiveCells( - mesh=self.mesh, indActive=self.active, valInactive=np.log(1e-8) + mesh=self.mesh, active_cells=self.active, value_inactive=np.log(1e-8) ) mapping = maps.ExpMap(self.mesh) * actMap # print(survey_ns.source_list) @@ -121,7 +121,7 @@ def create_simulation_rx(self, rx_type="apparent_resistivity", rx_orientation="x # Set the mapping actMap = maps.InjectActiveCells( - mesh=self.mesh, indActive=self.active, valInactive=np.log(1e-8) + mesh=self.mesh, active_cells=self.active, value_inactive=np.log(1e-8) ) mapping = maps.ExpMap(self.mesh) * actMap # print(survey_ns.source_list) @@ -173,7 +173,7 @@ def create_simulation_1dprimary_assign_mesh1d( # Set the mapping actMap = maps.InjectActiveCells( - mesh=self.mesh, indActive=self.active, valInactive=np.log(1e-8) + mesh=self.mesh, active_cells=self.active, value_inactive=np.log(1e-8) ) mapping = maps.ExpMap(self.mesh) * actMap # print(survey_ns.source_list) @@ -212,7 +212,7 @@ def create_simulation_1dprimary_assign( # Set the mapping actMap = maps.InjectActiveCells( - mesh=self.mesh, indActive=self.active, valInactive=np.log(1e-8) + mesh=self.mesh, active_cells=self.active, value_inactive=np.log(1e-8) ) mapping = maps.ExpMap(self.mesh) * actMap # print(survey_ns.source_list) diff --git a/tutorials/01-models_mapping/plot_1_tensor_models.py b/tutorials/01-models_mapping/plot_1_tensor_models.py index ebcf496e95..d2a06ca020 100644 --- a/tutorials/01-models_mapping/plot_1_tensor_models.py +++ b/tutorials/01-models_mapping/plot_1_tensor_models.py @@ -263,7 +263,9 @@ def make_example_mesh(): # Define the model on subsurface cells model = np.r_[background_value, block_value, xc, dx, yc, dy, zc, dz] -parametric_map = maps.ParametricBlock(mesh, indActive=ind_active, epsilon=1e-10, p=5.0) +parametric_map = maps.ParametricBlock( + mesh, active_cells=ind_active, epsilon=1e-10, p=5.0 +) # Define a single mapping from model to mesh model_map = active_map * parametric_map diff --git a/tutorials/01-models_mapping/plot_2_cyl_models.py b/tutorials/01-models_mapping/plot_2_cyl_models.py index cb118b43f9..89f1ab42d4 100644 --- a/tutorials/01-models_mapping/plot_2_cyl_models.py +++ b/tutorials/01-models_mapping/plot_2_cyl_models.py @@ -161,7 +161,9 @@ def make_example_mesh(): model = np.r_[ background_value, pipe_value, rc, dr, 0.0, 1.0, zc, dz ] # add dummy values for phi -parametric_map = maps.ParametricBlock(mesh, indActive=ind_active, epsilon=1e-10, p=8.0) +parametric_map = maps.ParametricBlock( + mesh, active_cells=ind_active, epsilon=1e-10, p=8.0 +) # Define a single mapping from model to mesh model_map = active_map * parametric_map diff --git a/tutorials/01-models_mapping/plot_3_tree_models.py b/tutorials/01-models_mapping/plot_3_tree_models.py index 2a8549aa6a..252e3ec95d 100644 --- a/tutorials/01-models_mapping/plot_3_tree_models.py +++ b/tutorials/01-models_mapping/plot_3_tree_models.py @@ -279,7 +279,9 @@ def refine_box(mesh): # Define the model on subsurface cells model = np.r_[background_value, block_value, xc, dx, yc, dy, zc, dz] -parametric_map = maps.ParametricBlock(mesh, indActive=ind_active, epsilon=1e-10, p=5.0) +parametric_map = maps.ParametricBlock( + mesh, active_cells=ind_active, epsilon=1e-10, p=5.0 +) # Define a single mapping from model to mesh model_map = active_map * parametric_map From 2bfa4d44cdc7de3d96f3e2bdb4efb5a2b2d8c611 Mon Sep 17 00:00:00 2001 From: domfournier Date: Wed, 25 Sep 2024 14:31:09 -0400 Subject: [PATCH 063/194] Update tests and examples to use the new `UpdateIRLS` (#1472) Update tests, examples and tutorials that were using the deprecated `Update_IRLS` so they use the new `UpdateIRLS` directive class. --- examples/01-maps/plot_sumMap.py | 3 +- examples/02-gravity/plot_inv_grav_tiled.py | 6 +- .../plot_inv_mag_MVI_Sparse_TreeMesh.py | 30 ++++--- .../plot_inv_mag_MVI_VectorAmplitude.py | 4 +- .../plot_inv_mag_nonLinear_Amplitude.py | 16 ++-- .../plot_laguna_del_maule_inversion.py | 7 +- ...nv_dcip_dipoledipole_2_5Dinversion_irls.py | 7 +- examples/_archived/plot_inv_grav_linear.py | 12 +-- examples/_archived/plot_inv_mag_linear.py | 6 +- simpeg/directives/_regularization.py | 19 ++-- tests/base/test_directives.py | 12 ++- tests/dask/test_grav_inversion_linear.py | 2 +- tests/dask/test_mag_MVI_Octree.py | 24 +++-- .../dask/test_mag_inversion_linear_Octree.py | 2 +- tests/dask/test_mag_nonLinear_Amplitude.py | 20 ++--- tests/pf/test_grav_inversion_linear.py | 2 +- tests/pf/test_mag_MVI_Octree.py | 23 +++-- tests/pf/test_mag_inversion_linear.py | 47 +++++++--- tests/pf/test_mag_inversion_linear_Octree.py | 24 ++--- tests/pf/test_mag_nonLinear_Amplitude.py | 89 +++++++++++-------- tests/pf/test_mag_vector_amplitude.py | 48 +++++----- tests/pf/test_pf_quadtree_inversion_linear.py | 5 +- .../plot_inv_2_inversion_irls.py | 18 +++- .../plot_inv_2a_magnetics_induced.py | 6 +- .../05-dcr/plot_inv_1_dcr_sounding_irls.py | 2 +- tutorials/05-dcr/plot_inv_2_dcr2d_irls.py | 4 +- tutorials/07-fdem/plot_inv_1_em1dfm.py | 4 +- tutorials/08-tdem/plot_inv_1_em1dtm.py | 6 +- .../12-seismic/plot_inv_1_tomography_2D.py | 6 +- .../plot_inv_1_em1dtm_stitched_skytem.py | 13 +-- 30 files changed, 266 insertions(+), 201 deletions(-) diff --git a/examples/01-maps/plot_sumMap.py b/examples/01-maps/plot_sumMap.py index be96ed84bf..fd479a9f6d 100644 --- a/examples/01-maps/plot_sumMap.py +++ b/examples/01-maps/plot_sumMap.py @@ -175,7 +175,8 @@ def run(plotIt=True): # Here is where the norms are applied # Use pick a threshold parameter empirically based on the distribution of # model parameters - IRLS = directives.Update_IRLS(f_min_change=1e-3, minGNiter=1) + IRLS = directives.UpdateIRLS(f_min_change=1e-3) + update_Jacobi = directives.UpdatePreconditioner() inv = inversion.BaseInversion(invProb, directiveList=[IRLS, betaest, update_Jacobi]) diff --git a/examples/02-gravity/plot_inv_grav_tiled.py b/examples/02-gravity/plot_inv_grav_tiled.py index 823c97a814..e4ba8823a8 100644 --- a/examples/02-gravity/plot_inv_grav_tiled.py +++ b/examples/02-gravity/plot_inv_grav_tiled.py @@ -236,11 +236,11 @@ # Here is where the norms are applied # Use a threshold parameter empirically based on the distribution of # model parameters -update_IRLS = directives.Update_IRLS( +update_IRLS = directives.UpdateIRLS( f_min_change=1e-4, max_irls_iterations=0, - coolEpsFact=1.5, - beta_tol=1e-2, + irls_cooling_factor=1.5, + misfit_tolerance=1e-2, ) saveDict = directives.SaveOutputEveryIteration(save_txt=False) update_Jacobi = directives.UpdatePreconditioner() diff --git a/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py b/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py index e6353dc96c..5727b29e4a 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py @@ -30,7 +30,7 @@ from simpeg import utils from simpeg.utils import mkvc -from discretize.utils import active_from_xyz, mesh_builder_xyz, refine_tree_xyz +from discretize.utils import active_from_xyz, mesh_builder_xyz from simpeg.potential_fields import magnetics import scipy as sp import numpy as np @@ -112,9 +112,7 @@ mesh = mesh_builder_xyz( xyzLoc, h, padding_distance=padDist, depth_core=100, mesh_type="tree" ) -mesh = refine_tree_xyz( - mesh, topo, method="surface", octree_levels=[4, 4], finalize=True -) +mesh.refine_surface(topo, padding_cells_by_level=[4, 4], finalize=True) # Define an active cells from topo @@ -260,7 +258,9 @@ # Here is where the norms are applied # Use a threshold parameter empirically based on the distribution of # model parameters -IRLS = directives.Update_IRLS(f_min_change=1e-3, max_irls_iterations=2, beta_tol=5e-1) +IRLS = directives.UpdateIRLS( + f_min_change=1e-3, max_irls_iterations=2, misfit_tolerance=5e-1 +) # Pre-conditioner update_Jacobi = directives.UpdatePreconditioner() @@ -344,16 +344,14 @@ invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=beta) # Here is where the norms are applied -irls = directives.Update_IRLS( +irls = directives.UpdateIRLS( f_min_change=1e-4, max_irls_iterations=20, - minGNiter=1, - beta_tol=0.5, - coolingRate=1, - coolEps_q=True, - sphericalDomain=True, + misfit_tolerance=0.5, +) +scale_spherical = directives.SphericalUnitsWeights( + amplitude=wires.amp, angles=[reg_t, reg_p] ) - # Special directive specific to the mag amplitude problem. The sensitivity # weights are updated between each iteration. spherical_projection = directives.ProjectSphericalBounds() @@ -362,7 +360,13 @@ inv = inversion.BaseInversion( invProb, - directiveList=[spherical_projection, irls, sensitivity_weights, update_Jacobi], + directiveList=[ + scale_spherical, + spherical_projection, + irls, + sensitivity_weights, + update_Jacobi, + ], ) mrec_MVI_S = inv.run(m_start) diff --git a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py index 8ee515c90e..f90258ab42 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py @@ -183,7 +183,9 @@ sensitivity_weights = directives.UpdateSensitivityWeights() # Here is where the norms are applied -IRLS = directives.Update_IRLS(f_min_change=1e-3, max_irls_iterations=10, beta_tol=5e-1) +IRLS = directives.UpdateIRLS( + f_min_change=1e-3, max_irls_iterations=10, misfit_tolerance=5e-1 +) # Pre-conditioner update_Jacobi = directives.UpdatePreconditioner() diff --git a/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py b/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py index 64bd047b02..b205152c1d 100644 --- a/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py +++ b/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py @@ -257,9 +257,10 @@ # Target misfit to stop the inversion, # try to fit as much as possible of the signal, we don't want to lose anything -IRLS = directives.Update_IRLS( - f_min_change=1e-3, minGNiter=1, beta_tol=1e-1, max_irls_iterations=5 +IRLS = directives.UpdateIRLS( + f_min_change=1e-3, misfit_tolerance=1e-1, max_irls_iterations=5 ) + update_Jacobi = directives.UpdatePreconditioner() # Put all the parts together inv = inversion.BaseInversion(invProb, directiveList=[betaest, IRLS, update_Jacobi]) @@ -375,13 +376,7 @@ betaest = directives.BetaEstimate_ByEig(beta0_ratio=1) # Specify the sparse norms -IRLS = directives.Update_IRLS( - max_irls_iterations=10, - f_min_change=1e-3, - minGNiter=1, - coolingRate=1, - beta_search=False, -) +IRLS = directives.UpdateIRLS(max_irls_iterations=10, f_min_change=1e-3) # Special directive specific to the mag amplitude problem. The sensitivity # weights are updated between each iteration. @@ -390,7 +385,8 @@ # Put all together inv = inversion.BaseInversion( - invProb, directiveList=[update_SensWeight, betaest, IRLS, update_Jacobi] + invProb, + directiveList=[update_SensWeight, betaest, IRLS, update_Jacobi], ) # Invert diff --git a/examples/20-published/plot_laguna_del_maule_inversion.py b/examples/20-published/plot_laguna_del_maule_inversion.py index 2cc6bed0ea..a80d5be245 100644 --- a/examples/20-published/plot_laguna_del_maule_inversion.py +++ b/examples/20-published/plot_laguna_del_maule_inversion.py @@ -126,8 +126,11 @@ def run(plotIt=True, cleanAfterRun=True): # IRLS sets up the Lp inversion problem # Set the eps parameter parameter in Line 11 of the # input file based on the distribution of model (DEFAULT = 95th %ile) - IRLS = directives.Update_IRLS( - f_min_change=1e-4, max_irls_iterations=40, coolEpsFact=1.5, beta_tol=5e-1 + IRLS = directives.UpdateIRLS( + f_min_change=1e-4, + max_irls_iterations=40, + irls_cooling_factor=1.5, + misfit_tolerance=5e-1, ) # Preconditioning refreshing for each IRLS iteration diff --git a/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion_irls.py b/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion_irls.py index 03b43b5003..392a8d8513 100644 --- a/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion_irls.py +++ b/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion_irls.py @@ -160,14 +160,13 @@ def run(plotIt=True, survey_type="dipole-dipole", p=0.0, qx=2.0, qz=2.0): mesh, active_cells=actind, mapping=regmap, gradient_type="components" ) reg.norms = [p, qx, qz, 0.0] - IRLS = directives.Update_IRLS( - max_irls_iterations=20, minGNiter=1, beta_search=False, fix_Jmatrix=True + irls = directives.UpdateIRLS( + max_irls_iterations=20, ) - opt = optimization.InexactGaussNewton(maxIter=40) invProb = inverse_problem.BaseInvProblem(dmisfit, reg, opt) betaest = directives.BetaEstimate_ByEig(beta0_ratio=1e0) - inv = inversion.BaseInversion(invProb, directiveList=[betaest, IRLS]) + inv = inversion.BaseInversion(invProb, directiveList=[betaest, irls]) prb.counter = opt.counter = utils.Counter() opt.LSshorten = 0.5 opt.remember("xc") diff --git a/examples/_archived/plot_inv_grav_linear.py b/examples/_archived/plot_inv_grav_linear.py index 010465a79b..c860677158 100644 --- a/examples/_archived/plot_inv_grav_linear.py +++ b/examples/_archived/plot_inv_grav_linear.py @@ -120,11 +120,11 @@ def run(plotIt=True): # Here is where the norms are applied # Use pick a threshold parameter empirically based on the distribution of # model parameters - update_IRLS = directives.Update_IRLS( + update_IRLS = directives.UpdateIRLS( f_min_change=1e-4, max_irls_iterations=30, - coolEpsFact=1.5, - beta_tol=1e-2, + irls_cooling_factor=1.5, + misfit_tolerance=1e-2, ) saveDict = directives.SaveOutputEveryIteration(save_txt=False) update_Jacobi = directives.UpdatePreconditioner() @@ -261,7 +261,9 @@ def run(plotIt=True): axs = plt.subplot() axs.plot(saveDict.phi_d, "k", lw=2) axs.plot( - np.r_[update_IRLS.iterStart, update_IRLS.iterStart], + np.r_[ + update_IRLS.metrics.start_irls_iter, update_IRLS.metrics.start_irls_iter + ], np.r_[0, np.max(saveDict.phi_d)], "k:", ) @@ -269,7 +271,7 @@ def run(plotIt=True): twin = axs.twinx() twin.plot(saveDict.phi_m, "k--", lw=2) axs.text( - update_IRLS.iterStart, + update_IRLS.metrics.start_irls_iter, np.max(saveDict.phi_d) / 2.0, "IRLS Steps", va="bottom", diff --git a/examples/_archived/plot_inv_mag_linear.py b/examples/_archived/plot_inv_mag_linear.py index 041bb69c1b..f5383bcc1b 100644 --- a/examples/_archived/plot_inv_mag_linear.py +++ b/examples/_archived/plot_inv_mag_linear.py @@ -128,7 +128,7 @@ def run(plotIt=True): # Here is where the norms are applied # Use pick a threshold parameter empirically based on the distribution of # model parameters - IRLS = directives.Update_IRLS(f_min_change=1e-3, max_irls_iterations=40) + IRLS = directives.UpdateIRLS(f_min_change=1e-3, max_irls_iterations=40) saveDict = directives.SaveOutputEveryIteration(save_txt=False) update_Jacobi = directives.UpdatePreconditioner() # Add sensitivity weights @@ -291,7 +291,7 @@ def run(plotIt=True): axs = plt.subplot() axs.plot(saveDict.phi_d, "k", lw=2) axs.plot( - np.r_[IRLS.iterStart, IRLS.iterStart], + np.r_[IRLS.metrics.start_irls_iter, IRLS.metrics.start_irls_iter], np.r_[0, np.max(saveDict.phi_d)], "k:", ) @@ -299,7 +299,7 @@ def run(plotIt=True): twin = axs.twinx() twin.plot(saveDict.phi_m, "k--", lw=2) axs.text( - IRLS.iterStart, + IRLS.metrics.start_irls_iter, 0, "IRLS Steps", va="bottom", diff --git a/simpeg/directives/_regularization.py b/simpeg/directives/_regularization.py index 686813ed38..bb3a20ef59 100644 --- a/simpeg/directives/_regularization.py +++ b/simpeg/directives/_regularization.py @@ -262,6 +262,9 @@ def endIter(self): """ Check on progress of the inversion and start/update the IRLS process. """ + # Check if misfit is within the tolerance, otherwise scale beta + self.adjust_cooling_schedule() + # After reaching target misfit with l2-norm, switch to IRLS (mode:2) if ( self.metrics.start_irls_iter is None @@ -269,9 +272,6 @@ def endIter(self): ): self.start_irls() - # Check if misfit is within the tolerance, otherwise scale beta - self.adjust_cooling_schedule() - # Only update after GN iterations if ( self.metrics.start_irls_iter is not None @@ -345,16 +345,19 @@ def start_irls(self): # Save l2-model self.invProb.l2model = self.invProb.model.copy() + self.cooling_factor = 1.0 + def validate(self, directiveList=None): directive_list = directiveList.dList self_ind = directive_list.index(self) lin_precond_ind = [isinstance(d, UpdatePreconditioner) for d in directive_list] - if any(lin_precond_ind) and lin_precond_ind.index(True) < self_ind: - raise AssertionError( - "The directive 'UpdatePreconditioner' must be after Update_IRLS " - "in the directiveList" - ) + if any(lin_precond_ind): + if lin_precond_ind.index(True) < self_ind: + raise AssertionError( + "The directive 'UpdatePreconditioner' must be after UpdateIRLS " + "in the directiveList" + ) else: warnings.warn( "Without a Linear preconditioner, convergence may be slow. " diff --git a/tests/base/test_directives.py b/tests/base/test_directives.py index a47ae57205..a54a49cfcf 100644 --- a/tests/base/test_directives.py +++ b/tests/base/test_directives.py @@ -22,8 +22,10 @@ def test_validation_pass(self): betaest = directives.BetaEstimate_ByEig() IRLS = directives.Update_IRLS(f_min_change=1e-4, minGNiter=3, beta_tol=1e-2) + beta_schedule = directives.BetaSchedule(coolingFactor=2, coolingRate=1) + update_Jacobi = directives.UpdatePreconditioner() - dList = [betaest, IRLS, update_Jacobi] + dList = [betaest, IRLS, beta_schedule, update_Jacobi] directiveList = directives.DirectiveList(*dList) self.assertTrue(directiveList.validate()) @@ -33,7 +35,9 @@ def test_validation_fail(self): IRLS = directives.Update_IRLS(f_min_change=1e-4, minGNiter=3, beta_tol=1e-2) update_Jacobi = directives.UpdatePreconditioner() - dList = [betaest, update_Jacobi, IRLS] + beta_schedule = directives.BetaSchedule(coolingFactor=2, coolingRate=1) + + dList = [betaest, update_Jacobi, IRLS, beta_schedule] directiveList = directives.DirectiveList(*dList) with self.assertRaises(AssertionError): @@ -52,7 +56,8 @@ def test_validation_warning(self): betaest = directives.BetaEstimate_ByEig() IRLS = directives.Update_IRLS(f_min_change=1e-4, minGNiter=3, beta_tol=1e-2) - dList = [betaest, IRLS] + beta_schedule = directives.BetaSchedule(coolingFactor=2, coolingRate=1) + dList = [betaest, IRLS, beta_schedule] directiveList = directives.DirectiveList(*dList) with pytest.warns(UserWarning): @@ -110,7 +115,6 @@ def test_validation_in_inversion(self): # Here is where the norms are applied IRLS = directives.Update_IRLS(f_min_change=1e-4, minGNiter=3, beta_tol=1e-2) - update_Jacobi = directives.UpdatePreconditioner() sensitivity_weights = directives.UpdateSensitivityWeights() with self.assertRaises(AssertionError): diff --git a/tests/dask/test_grav_inversion_linear.py b/tests/dask/test_grav_inversion_linear.py index ad8370b438..fb08a9be66 100644 --- a/tests/dask/test_grav_inversion_linear.py +++ b/tests/dask/test_grav_inversion_linear.py @@ -103,7 +103,7 @@ def setUp(self): invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e2) # Here is where the norms are applied - IRLS = directives.Update_IRLS(max_irls_iterations=20, chifact_start=2.0) + IRLS = directives.UpdateIRLS() update_Jacobi = directives.UpdatePreconditioner() sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) self.inv = inversion.BaseInversion( diff --git a/tests/dask/test_mag_MVI_Octree.py b/tests/dask/test_mag_MVI_Octree.py index 5ac9f588b5..49d2ca153e 100644 --- a/tests/dask/test_mag_MVI_Octree.py +++ b/tests/dask/test_mag_MVI_Octree.py @@ -146,8 +146,8 @@ def setUp(self): # Here is where the norms are applied # Use pick a treshold parameter empirically based on the distribution of # model parameters - IRLS = directives.Update_IRLS( - f_min_change=1e-3, max_irls_iterations=0, beta_tol=5e-1 + IRLS = directives.UpdateIRLS( + f_min_change=1e-3, max_irls_iterations=0, misfit_tolerance=5e-1 ) # Pre-conditioner @@ -208,14 +208,14 @@ def setUp(self): invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=beta) # Here is where the norms are applied - IRLS = directives.Update_IRLS( + IRLS = directives.UpdateIRLS( f_min_change=1e-4, max_irls_iterations=5, - minGNiter=1, - beta_tol=0.5, - coolingRate=1, - coolEps_q=True, - sphericalDomain=True, + misfit_tolerance=0.5, + ) + + spherical_scale = directives.SphericalUnitsWeights( + amplitude=wires.amp, angles=[reg_t, reg_p] ) # Special directive specific to the mag amplitude problem. The sensitivity @@ -226,7 +226,13 @@ def setUp(self): self.inv = inversion.BaseInversion( invProb, - directiveList=[ProjSpherical, IRLS, sensitivity_weights, update_Jacobi], + directiveList=[ + spherical_scale, + ProjSpherical, + IRLS, + sensitivity_weights, + update_Jacobi, + ], ) def test_mag_inverse(self): diff --git a/tests/dask/test_mag_inversion_linear_Octree.py b/tests/dask/test_mag_inversion_linear_Octree.py index f053dfd3ee..857f3d297a 100644 --- a/tests/dask/test_mag_inversion_linear_Octree.py +++ b/tests/dask/test_mag_inversion_linear_Octree.py @@ -145,7 +145,7 @@ def setUp(self): # Here is where the norms are applied # Use pick a treshold parameter empirically based on the distribution of # model parameters - IRLS = directives.Update_IRLS() + IRLS = directives.UpdateIRLS() update_Jacobi = directives.UpdatePreconditioner() sensitivity_weights = directives.UpdateSensitivityWeights() self.inv = inversion.BaseInversion( diff --git a/tests/dask/test_mag_nonLinear_Amplitude.py b/tests/dask/test_mag_nonLinear_Amplitude.py index 0a71bd66cc..46935c5455 100644 --- a/tests/dask/test_mag_nonLinear_Amplitude.py +++ b/tests/dask/test_mag_nonLinear_Amplitude.py @@ -167,8 +167,8 @@ def setUp(self): # Target misfit to stop the inversion, # try to fit as much as possible of the signal, we don't want to lose anything - IRLS = directives.Update_IRLS( - f_min_change=1e-3, minGNiter=1, beta_tol=1e-1, max_irls_iterations=5 + IRLS = directives.UpdateIRLS( + f_min_change=1e-3, misfit_tolerance=1e-1, max_irls_iterations=5 ) update_Jacobi = directives.UpdatePreconditioner() # Put all the parts together @@ -253,13 +253,7 @@ def setUp(self): betaest = directives.BetaEstimate_ByEig(beta0_ratio=1) # Specify the sparse norms - IRLS = directives.Update_IRLS( - max_irls_iterations=5, - f_min_change=1e-3, - minGNiter=1, - coolingRate=1, - beta_search=False, - ) + IRLS = directives.UpdateIRLS(max_irls_iterations=5, f_min_change=1e-3) # Special directive specific to the mag amplitude problem. The sensitivity # weights are update between each iteration. @@ -268,7 +262,13 @@ def setUp(self): # Put all together self.inv = inversion.BaseInversion( - invProb, directiveList=[update_SensWeight, betaest, IRLS, update_Jacobi] + invProb, + directiveList=[ + update_SensWeight, + betaest, + IRLS, + update_Jacobi, + ], ) self.mstart = mstart diff --git a/tests/pf/test_grav_inversion_linear.py b/tests/pf/test_grav_inversion_linear.py index 925c1dca4f..1dfabebaee 100644 --- a/tests/pf/test_grav_inversion_linear.py +++ b/tests/pf/test_grav_inversion_linear.py @@ -104,7 +104,7 @@ def test_gravity_inversion_linear(engine): # Here is where the norms are applied starting_beta = directives.BetaEstimateMaxDerivative(10.0) - IRLS = directives.Update_IRLS() + IRLS = directives.UpdateIRLS() update_Jacobi = directives.UpdatePreconditioner() sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) inv = inversion.BaseInversion( diff --git a/tests/pf/test_mag_MVI_Octree.py b/tests/pf/test_mag_MVI_Octree.py index ebf2dcfb28..f03df665f1 100644 --- a/tests/pf/test_mag_MVI_Octree.py +++ b/tests/pf/test_mag_MVI_Octree.py @@ -146,8 +146,8 @@ def setUp(self): # Here is where the norms are applied # Use pick a treshold parameter empirically based on the distribution of # model parameters - IRLS = directives.Update_IRLS( - f_min_change=1e-3, max_irls_iterations=0, beta_tol=5e-1 + IRLS = directives.UpdateIRLS( + f_min_change=1e-3, max_irls_iterations=0, misfit_tolerance=5e-1 ) # Pre-conditioner @@ -207,14 +207,13 @@ def setUp(self): invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=beta) # Here is where the norms are applied - IRLS = directives.Update_IRLS( + IRLS = directives.UpdateIRLS( f_min_change=1e-4, max_irls_iterations=5, - minGNiter=1, - beta_tol=0.5, - coolingRate=1, - coolEps_q=True, - sphericalDomain=True, + misfit_tolerance=0.5, + ) + spherical_scale = directives.SphericalUnitsWeights( + amplitude=wires.amp, angles=[reg_t, reg_p] ) # Special directive specific to the mag amplitude problem. The sensitivity @@ -225,7 +224,13 @@ def setUp(self): self.inv = inversion.BaseInversion( invProb, - directiveList=[ProjSpherical, IRLS, sensitivity_weights, update_Jacobi], + directiveList=[ + spherical_scale, + ProjSpherical, + IRLS, + sensitivity_weights, + update_Jacobi, + ], ) def test_mag_inverse(self): diff --git a/tests/pf/test_mag_inversion_linear.py b/tests/pf/test_mag_inversion_linear.py index 97af16e1ed..e19c4b492b 100644 --- a/tests/pf/test_mag_inversion_linear.py +++ b/tests/pf/test_mag_inversion_linear.py @@ -1,6 +1,8 @@ import unittest import discretize from discretize.utils import active_from_xyz +import pytest +import matplotlib.pyplot as plt from simpeg import ( utils, maps, @@ -93,7 +95,7 @@ def setUp(self): data = sim.make_synthetic_data( self.model, relative_error=0.0, - noise_floor=1.0, + noise_floor=2.0, add_noise=True, random_seed=2, ) @@ -115,11 +117,17 @@ def setUp(self): betaest = directives.BetaEstimate_ByEig() # Here is where the norms are applied - IRLS = directives.Update_IRLS(f_min_change=1e-4, minGNiter=1) + IRLS = directives.UpdateIRLS(f_min_change=1e-4) update_Jacobi = directives.UpdatePreconditioner() sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) self.inv = inversion.BaseInversion( - invProb, directiveList=[IRLS, sensitivity_weights, betaest, update_Jacobi] + invProb, + directiveList=[ + IRLS, + sensitivity_weights, + betaest, + update_Jacobi, + ], ) def test_mag_inverse(self): @@ -127,19 +135,30 @@ def test_mag_inverse(self): mrec = self.inv.run(self.model) residual = np.linalg.norm(mrec - self.model) / np.linalg.norm(self.model) - # plt.figure() - # ax = plt.subplot(1, 2, 1) - # midx = int(self.mesh.shape_cells[0]/2) - # self.mesh.plot_slice(self.actvMap*mrec, ax=ax, normal='Y', ind=midx, - # grid=True, clim=(0, 0.02)) + self.assertTrue(residual < 0.05) - # ax = plt.subplot(1, 2, 2) - # midx = int(self.mesh.shape_cells[0]/2) - # self.mesh.plot_slice(self.actvMap*self.model, ax=ax, normal='Y', ind=midx, - # grid=True, clim=(0, 0.02)) - # plt.show() + @pytest.mark.skip(reason="For validation only.") + def test_plot_results(self): + self.sim.store_sensitivities = "ram" + mrec = self.inv.run(self.model) + plt.figure() + ax = plt.subplot(1, 2, 1) + midx = int(self.mesh.shape_cells[0] / 2) + self.mesh.plot_slice( + self.actvMap * mrec, ax=ax, normal="Y", ind=midx, grid=True, clim=(0, 0.02) + ) - self.assertTrue(residual < 0.05) + ax = plt.subplot(1, 2, 2) + midx = int(self.mesh.shape_cells[0] / 2) + self.mesh.plot_slice( + self.actvMap * self.model, + ax=ax, + normal="Y", + ind=midx, + grid=True, + clim=(0, 0.02), + ) + plt.show() def tearDown(self): # Clean up the working directory diff --git a/tests/pf/test_mag_inversion_linear_Octree.py b/tests/pf/test_mag_inversion_linear_Octree.py index e969a2b450..af6f7c929f 100644 --- a/tests/pf/test_mag_inversion_linear_Octree.py +++ b/tests/pf/test_mag_inversion_linear_Octree.py @@ -1,7 +1,8 @@ import shutil import unittest import numpy as np - +import pytest +import matplotlib.pyplot as plt from discretize.utils import mesh_builder_xyz, refine_tree_xyz, active_from_xyz from simpeg import ( directives, @@ -136,7 +137,7 @@ def setUp(self): ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e6) - IRLS = directives.Update_IRLS() + IRLS = directives.UpdateIRLS() update_Jacobi = directives.UpdatePreconditioner() sensitivity_weights = directives.UpdateSensitivityWeights() self.inv = inversion.BaseInversion( @@ -147,16 +148,19 @@ def test_mag_inverse(self): # Run the inversion mrec = self.inv.run(self.model * 1e-4) residual = np.linalg.norm(mrec - self.model) / np.linalg.norm(self.model) + self.assertLess(residual, 0.5) - # import matplotlib.pyplot as plt - # plt.figure() - # ax = plt.subplot(1, 2, 1) - # self.mesh.plot_slice(self.actvMap*mrec, ax=ax, normal="Y", grid=True) - # ax = plt.subplot(1, 2, 2) - # self.mesh.plot_slice(self.actvMap*self.model, ax=ax, normal="Y", grid=True) - # plt.show() + @pytest.mark.skip(reason="For validation only.") + def test_plot_results(self): + self.sim.store_sensitivities = "ram" + mrec = self.inv.run(self.model * 1e-4) - self.assertLess(residual, 0.5) + plt.figure() + ax = plt.subplot(1, 2, 1) + self.mesh.plot_slice(self.actvMap * mrec, ax=ax, normal="Y", grid=True) + ax = plt.subplot(1, 2, 2) + self.mesh.plot_slice(self.actvMap * self.model, ax=ax, normal="Y", grid=True) + plt.show() def tearDown(self): # Clean up the working directory diff --git a/tests/pf/test_mag_nonLinear_Amplitude.py b/tests/pf/test_mag_nonLinear_Amplitude.py index 1895a8c1e3..b9156246d5 100644 --- a/tests/pf/test_mag_nonLinear_Amplitude.py +++ b/tests/pf/test_mag_nonLinear_Amplitude.py @@ -1,4 +1,6 @@ import numpy as np +import pytest +import matplotlib.pyplot as plt from simpeg import ( data, data_misfit, @@ -166,9 +168,10 @@ def setUp(self): # Target misfit to stop the inversion, # try to fit as much as possible of the signal, we don't want to lose anything - IRLS = directives.Update_IRLS( - f_min_change=1e-3, minGNiter=1, beta_tol=1e-1, max_irls_iterations=5 + IRLS = directives.UpdateIRLS( + f_min_change=1e-3, misfit_tolerance=1e-1, max_irls_iterations=5 ) + update_Jacobi = directives.UpdatePreconditioner() # Put all the parts together inv = inversion.BaseInversion( @@ -252,14 +255,10 @@ def setUp(self): betaest = directives.BetaEstimate_ByEig(beta0_ratio=1) # Specify the sparse norms - IRLS = directives.Update_IRLS( + IRLS = directives.UpdateIRLS( max_irls_iterations=5, f_min_change=1e-3, - minGNiter=1, - coolingRate=1, - beta_search=False, ) - # Special directive specific to the mag amplitude problem. The sensitivity # weights are update between each iteration. update_SensWeight = directives.UpdateSensitivityWeights() @@ -267,7 +266,13 @@ def setUp(self): # Put all together self.inv = inversion.BaseInversion( - invProb, directiveList=[update_SensWeight, betaest, IRLS, update_Jacobi] + invProb, + directiveList=[ + update_SensWeight, + betaest, + IRLS, + update_Jacobi, + ], ) self.mstart = mstart @@ -279,37 +284,47 @@ def test_mag_inverse(self): mrec_Amp = self.inv.run(self.mstart) residual = np.linalg.norm(mrec_Amp - self.model) / np.linalg.norm(self.model) - # print(residual) - # import matplotlib.pyplot as plt - - # # Plot the amplitude model - # plt.figure() - # ax = plt.subplot(2, 1, 1) - # im = self.mesh.plot_slice(self.actvPlot*self.model, - # ax=ax, normal='Y', ind=66, - # pcolor_opts={"vmin":0., "vmax":0.01} - # ) - # plt.colorbar(im[0]) - # ax.set_xlim([-200, 200]) - # ax.set_ylim([-100, 75]) - # ax.set_xlabel('x') - # ax.set_ylabel('y') - # plt.gca().set_aspect('equal', adjustable='box') - - # ax = plt.subplot(2, 1, 2) - # im = self.mesh.plot_slice(self.actvPlot*mrec_Amp, ax=ax, normal='Y', ind=66, - # pcolor_opts={"vmin":0., "vmax":0.01} - # ) - # plt.colorbar(im[0]) - # ax.set_xlim([-200, 200]) - # ax.set_ylim([-100, 75]) - # ax.set_xlabel('x') - # ax.set_ylabel('y') - # plt.gca().set_aspect('equal', adjustable='box') - - # plt.show() self.assertTrue(residual < 1.0) + @pytest.mark.skip(reason="For validation only.") + def test_plot_results(self): + self.sim.store_sensitivities = "ram" + mrec = self.inv.run(self.model) + + # Plot the amplitude model + plt.figure() + ax = plt.subplot(2, 1, 1) + im = self.mesh.plot_slice( + self.actvPlot * self.model, + ax=ax, + normal="Y", + ind=66, + pcolor_opts={"vmin": 0.0, "vmax": 0.01}, + ) + plt.colorbar(im[0]) + ax.set_xlim([-200, 200]) + ax.set_ylim([-100, 75]) + ax.set_xlabel("x") + ax.set_ylabel("y") + plt.gca().set_aspect("equal", adjustable="box") + + ax = plt.subplot(2, 1, 2) + im = self.mesh.plot_slice( + self.actvPlot * mrec, + ax=ax, + normal="Y", + ind=66, + pcolor_opts={"vmin": 0.0, "vmax": 0.01}, + ) + plt.colorbar(im[0]) + ax.set_xlim([-200, 200]) + ax.set_ylim([-100, 75]) + ax.set_xlabel("x") + ax.set_ylabel("y") + plt.gca().set_aspect("equal", adjustable="box") + + plt.show() + def tearDown(self): # Clean up the working directory if self.sim.store_sensitivities == "disk": diff --git a/tests/pf/test_mag_vector_amplitude.py b/tests/pf/test_mag_vector_amplitude.py index 6982fba75d..3c949e22c5 100644 --- a/tests/pf/test_mag_vector_amplitude.py +++ b/tests/pf/test_mag_vector_amplitude.py @@ -1,4 +1,6 @@ import unittest +import pytest +import matplotlib.pyplot as plt from simpeg import ( directives, maps, @@ -138,8 +140,8 @@ def setUp(self): # Here is where the norms are applied # Use pick a treshold parameter empirically based on the distribution of # model parameters - IRLS = directives.Update_IRLS( - f_min_change=1e-3, max_irls_iterations=10, beta_tol=5e-1 + IRLS = directives.UpdateIRLS( + f_min_change=1e-3, max_irls_iterations=10, misfit_tolerance=5e-1 ) # Pre-conditioner @@ -159,28 +161,30 @@ def test_vector_amplitude_inverse(self): nC = int(mrec.shape[0] / 3) vec_xyz = mrec.reshape((nC, 3), order="F") residual = np.linalg.norm(vec_xyz - self.model) / np.linalg.norm(self.model) + self.assertLess(residual, 1) - # import matplotlib.pyplot as plt - # ax = plt.subplot() - # self.mesh.plot_slice( - # self.actvMap * mrec.reshape((-1, 3), order="F"), - # v_type="CCv", - # view="vec", - # ax=ax, - # normal="Y", - # grid=True, - # quiver_opts={ - # "pivot": "mid", - # "scale": 8 * np.abs(mrec).max(), - # "scale_units": "inches", - # }, - # ) - # plt.gca().set_aspect("equal", adjustable="box") - # - # plt.show() + @pytest.mark.skip(reason="For validation only.") + def test_plot_results(self): + self.sim.store_sensitivities = "ram" + mrec = self.inv.run(self.mstart) - self.assertLess(residual, 1) - # self.assertTrue(residual < 0.05) + ax = plt.subplot() + self.mesh.plot_slice( + self.actvMap * mrec.reshape((-1, 3), order="F"), + v_type="CCv", + view="vec", + ax=ax, + normal="Y", + grid=True, + quiver_opts={ + "pivot": "mid", + "scale": 8 * np.abs(mrec).max(), + "scale_units": "inches", + }, + ) + plt.gca().set_aspect("equal", adjustable="box") + + plt.show() def tearDown(self): # Clean up the working directory diff --git a/tests/pf/test_pf_quadtree_inversion_linear.py b/tests/pf/test_pf_quadtree_inversion_linear.py index 1a391d378c..37848db2e7 100644 --- a/tests/pf/test_pf_quadtree_inversion_linear.py +++ b/tests/pf/test_pf_quadtree_inversion_linear.py @@ -291,11 +291,10 @@ def create_inversion(self, sim, data, beta=1e3, all_active=True): invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=beta) # Build directives - IRLS = directives.Update_IRLS( + IRLS = directives.UpdateIRLS( f_min_change=1e-3, max_irls_iterations=30, - beta_tol=1e-1, - beta_search=False, + misfit_tolerance=1e-1, ) sensitivity_weights = directives.UpdateSensitivityWeights() update_Jacobi = directives.UpdatePreconditioner() diff --git a/tutorials/02-linear_inversion/plot_inv_2_inversion_irls.py b/tutorials/02-linear_inversion/plot_inv_2_inversion_irls.py index 4715d3937b..b835433f55 100644 --- a/tutorials/02-linear_inversion/plot_inv_2_inversion_irls.py +++ b/tutorials/02-linear_inversion/plot_inv_2_inversion_irls.py @@ -174,7 +174,7 @@ def g(k): sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) # Reach target misfit for L2 solution, then use IRLS until model stops changing. -IRLS = directives.Update_IRLS(max_irls_iterations=40, minGNiter=1, f_min_change=1e-4) +IRLS = directives.UpdateIRLS(max_irls_iterations=40, f_min_change=1e-4) # Defining a starting value for the trade-off parameter (beta) between the data # misfit and the regularization. @@ -187,7 +187,13 @@ def g(k): saveDict = directives.SaveOutputEveryIteration(save_txt=False) # Define the directives as a list -directives_list = [sensitivity_weights, IRLS, starting_beta, update_Jacobi, saveDict] +directives_list = [ + sensitivity_weights, + IRLS, + starting_beta, + update_Jacobi, + saveDict, +] ##################################################################### @@ -233,9 +239,13 @@ def g(k): twin = ax.twinx() twin.plot(saveDict.phi_m, "k--", lw=2) -ax.plot(np.r_[IRLS.iterStart, IRLS.iterStart], np.r_[0, np.max(saveDict.phi_d)], "k:") +ax.plot( + np.r_[IRLS.metrics.start_irls_iter, IRLS.metrics.start_irls_iter], + np.r_[0, np.max(saveDict.phi_d)], + "k:", +) ax.text( - IRLS.iterStart, + IRLS.metrics.start_irls_iter, 0.0, "IRLS Start", va="bottom", diff --git a/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py b/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py index a27ebde118..61eeae9497 100644 --- a/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py +++ b/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py @@ -307,11 +307,11 @@ # Defines the directives for the IRLS regularization. This includes setting # the cooling schedule for the trade-off parameter. -update_IRLS = directives.Update_IRLS( +update_IRLS = directives.UpdateIRLS( f_min_change=1e-4, max_irls_iterations=30, - coolEpsFact=1.5, - beta_tol=1e-2, + cooling_factor=1.5, + misfit_tolerance=1e-2, ) # Updating the preconditioner if it is model dependent. diff --git a/tutorials/05-dcr/plot_inv_1_dcr_sounding_irls.py b/tutorials/05-dcr/plot_inv_1_dcr_sounding_irls.py index f461b716dd..a9e751bbb4 100644 --- a/tutorials/05-dcr/plot_inv_1_dcr_sounding_irls.py +++ b/tutorials/05-dcr/plot_inv_1_dcr_sounding_irls.py @@ -255,7 +255,7 @@ update_sensitivity_weights = directives.UpdateSensitivityWeights() # Reach target misfit for L2 solution, then use IRLS until model stops changing. -IRLS = directives.Update_IRLS(max_irls_iterations=40, minGNiter=1, f_min_change=1e-5) +IRLS = directives.UpdateIRLS(max_irls_iterations=40, f_min_change=1e-5) # Defining a starting value for the trade-off parameter (beta) between the data # misfit and the regularization. diff --git a/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py b/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py index f84891fb15..5405e6a819 100644 --- a/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py +++ b/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py @@ -332,9 +332,7 @@ update_sensitivity_weighting = directives.UpdateSensitivityWeights() # Reach target misfit for L2 solution, then use IRLS until model stops changing. -update_IRLS = directives.Update_IRLS( - max_irls_iterations=25, minGNiter=1, chifact_start=1.0 -) +update_IRLS = directives.UpdateIRLS(max_irls_iterations=25, chifact_start=1.0) # Defining a starting value for the trade-off parameter (beta) between the data # misfit and the regularization. diff --git a/tutorials/07-fdem/plot_inv_1_em1dfm.py b/tutorials/07-fdem/plot_inv_1_em1dfm.py index f5e5366765..17a2564cee 100644 --- a/tutorials/07-fdem/plot_inv_1_em1dfm.py +++ b/tutorials/07-fdem/plot_inv_1_em1dfm.py @@ -270,9 +270,7 @@ save_iteration = directives.SaveOutputEveryIteration(save_txt=False) # Directive for the IRLS -update_IRLS = directives.Update_IRLS( - max_irls_iterations=30, minGNiter=1, coolEpsFact=1.5, update_beta=True -) +update_IRLS = directives.UpdateIRLS(max_irls_iterations=30, irls_cooling_factor=1.5) # Updating the preconditionner if it is model dependent. update_jacobi = directives.UpdatePreconditioner() diff --git a/tutorials/08-tdem/plot_inv_1_em1dtm.py b/tutorials/08-tdem/plot_inv_1_em1dtm.py index e0dcede58a..2c2cfc7d4c 100644 --- a/tutorials/08-tdem/plot_inv_1_em1dtm.py +++ b/tutorials/08-tdem/plot_inv_1_em1dtm.py @@ -250,7 +250,7 @@ # Defining a starting value for the trade-off parameter (beta) between the data # misfit and the regularization. -starting_beta = directives.BetaEstimate_ByEig(beta0_ratio=1e1) +starting_beta = directives.BetaEstimate_ByEig(beta0_ratio=1e2) # Update the preconditionner update_Jacobi = directives.UpdatePreconditioner() @@ -259,9 +259,7 @@ save_iteration = directives.SaveOutputEveryIteration(save_txt=False) # Directives for the IRLS -update_IRLS = directives.Update_IRLS( - max_irls_iterations=30, minGNiter=1, coolEpsFact=1.5, update_beta=True -) +update_IRLS = directives.UpdateIRLS(max_irls_iterations=30, irls_cooling_factor=1.5) # Updating the preconditionner if it is model dependent. update_jacobi = directives.UpdatePreconditioner() diff --git a/tutorials/12-seismic/plot_inv_1_tomography_2D.py b/tutorials/12-seismic/plot_inv_1_tomography_2D.py index 896def24fd..b60ce62ad6 100644 --- a/tutorials/12-seismic/plot_inv_1_tomography_2D.py +++ b/tutorials/12-seismic/plot_inv_1_tomography_2D.py @@ -240,11 +240,11 @@ # # Reach target misfit for L2 solution, then use IRLS until model stops changing. -update_IRLS = directives.Update_IRLS( +update_IRLS = directives.UpdateIRLS( f_min_change=1e-4, max_irls_iterations=30, - coolEpsFact=1.5, - beta_tol=1e-2, + irls_cooling_factor=1.5, + misfit_tolerance=1e-2, ) # Defining a starting value for the trade-off parameter (beta) between the data diff --git a/tutorials/_temporary/plot_inv_1_em1dtm_stitched_skytem.py b/tutorials/_temporary/plot_inv_1_em1dtm_stitched_skytem.py index 936b238b3c..7248f413f8 100644 --- a/tutorials/_temporary/plot_inv_1_em1dtm_stitched_skytem.py +++ b/tutorials/_temporary/plot_inv_1_em1dtm_stitched_skytem.py @@ -308,7 +308,7 @@ # IRLS = directives.Update_IRLS(max_irls_iterations=40, minGNiter=1, f_min_change=1e-5, chifact_start=2) # IRLS = directives.Update_IRLS( # max_irls_iterations=20, minGNiter=1, fix_Jmatrix=True, coolingRate=2, -# beta_tol=1e-2, f_min_change=1e-5, +# misfit_tolerance=1e-2, f_min_change=1e-5, # chifact_start = 1. # ) @@ -326,14 +326,12 @@ save_iteration = directives.SaveOutputEveryIteration(save_txt=False) -update_IRLS = directives.Update_IRLS( +update_IRLS = directives.UpdateIRLS( max_irls_iterations=20, - minGNiter=1, - fix_Jmatrix=True, f_min_change=1e-3, - coolingRate=3, ) + # Updating the preconditionner if it is model dependent. update_jacobi = directives.UpdatePreconditioner() @@ -347,13 +345,10 @@ # The directives are defined as a list. directives_list = [ - # sensitivity_weights, starting_beta, - beta_schedule, save_iteration, - # target_misfit, update_IRLS, - # update_jacobi, + beta_schedule, ] ##################################################################### From 282ddbb58e8bcf017d6bd85f1e879021f11a6f7d Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 4 Oct 2024 15:23:06 -0700 Subject: [PATCH 064/194] Replace `indActive` in VRM simulations for `active_cells` (#1536) Deprecate `indActive` argument in VRM simulations and replace it by `active_cells`. Add tests for the deprecations. Update tutorials and examples that were using the old `indActive` argument. Part of #1121 --- examples/08-vrm/plot_fwd_vrm.py | 2 +- examples/08-vrm/plot_inv_vrm_eq.py | 4 +- .../simulation.py | 55 +++++++++--- tests/em/vrm/test_vrmfwd.py | 83 +++++++++++++++++++ tutorials/10-vrm/plot_fwd_1_vrm_layer.py | 2 +- tutorials/10-vrm/plot_fwd_2_vrm_topsoil.py | 2 +- tutorials/10-vrm/plot_fwd_3_vrm_tem.py | 2 +- 7 files changed, 132 insertions(+), 18 deletions(-) diff --git a/examples/08-vrm/plot_fwd_vrm.py b/examples/08-vrm/plot_fwd_vrm.py index fba26e608f..9b87566329 100644 --- a/examples/08-vrm/plot_fwd_vrm.py +++ b/examples/08-vrm/plot_fwd_vrm.py @@ -110,7 +110,7 @@ problem_vrm = VRM.Simulation3DLinear( mesh, survey=survey_vrm, - indActive=topoCells, + active_cells=topoCells, refinement_factor=3, refinement_distance=[1.25, 2.5, 3.75], ) diff --git a/examples/08-vrm/plot_inv_vrm_eq.py b/examples/08-vrm/plot_inv_vrm_eq.py index a8558023c8..873a7b7188 100644 --- a/examples/08-vrm/plot_inv_vrm_eq.py +++ b/examples/08-vrm/plot_inv_vrm_eq.py @@ -124,7 +124,7 @@ problem_vrm = VRM.Simulation3DLinear( mesh, survey=survey_vrm, - indActive=topoCells, + active_cells=topoCells, refinement_factor=3, refinement_distance=[1.25, 2.5, 3.75], ) @@ -176,7 +176,7 @@ problem_inv = VRM.Simulation3DLinear( mesh, survey=survey_vrm, - indActive=actCells, + active_cells=actCells, refinement_factor=3, refinement_distance=[1.25, 2.5, 3.75], ) diff --git a/simpeg/electromagnetics/viscous_remanent_magnetization/simulation.py b/simpeg/electromagnetics/viscous_remanent_magnetization/simulation.py index 27cfb040f1..d3d5835351 100644 --- a/simpeg/electromagnetics/viscous_remanent_magnetization/simulation.py +++ b/simpeg/electromagnetics/viscous_remanent_magnetization/simulation.py @@ -1,3 +1,4 @@ +import warnings import discretize import numpy as np import scipy.sparse as sp @@ -15,6 +16,7 @@ from .survey import SurveyVRM from .receivers import Point, SquareLoop +from ...utils.code_utils import deprecate_property ############################################ # BASE VRM PROBLEM CLASS @@ -32,6 +34,7 @@ def __init__( survey=None, refinement_factor=None, refinement_distance=None, + active_cells=None, indActive=None, **kwargs, ): @@ -48,9 +51,26 @@ def __init__( * np.arange(1, refinement_factor + 1) ) self.refinement_distance = refinement_distance - if indActive is None: - indActive = np.ones(self.mesh.n_cells, dtype=bool) - self.indActive = indActive + + # Deprecate indActive argument + if indActive is not None: + if active_cells is not None: + raise TypeError( + "Cannot pass both 'active_cells' and 'indActive'." + "'indActive' has been deprecated and will be removed in " + " SimPEG v0.24.0, please use 'active_cells' instead.", + ) + warnings.warn( + "'indActive' has been deprecated and will be removed in " + " SimPEG v0.24.0, please use 'active_cells' instead.", + FutureWarning, + stacklevel=2, + ) + active_cells = indActive + + if active_cells is None: + active_cells = np.ones(self.mesh.n_cells, dtype=bool) + self.active_cells = active_cells @BaseSimulation.mesh.setter def mesh(self, value): @@ -108,18 +128,29 @@ def refinement_distance(self, value): ) @property - def indActive(self): + def active_cells(self): """Topography active cells. Returns ------- (mesh.n_cells) numpy.ndarray of bool """ - return self._indActive + return self._active_cells - @indActive.setter - def indActive(self, value): - self._indActive = validate_active_indices("indActive", value, self.mesh.n_cells) + @active_cells.setter + def active_cells(self, value): + self._active_cells = validate_active_indices( + "active_cells", value, self.mesh.n_cells + ) + + indActive = deprecate_property( + active_cells, + "indActive", + "active_cells", + removal_version="0.24.0", + future_warn=True, + error=False, + ) def _getH0matrix(self, xyz, pp): """ @@ -697,12 +728,12 @@ def _getGeometryMatrix(self, xyzc, xyzh, pp): def _getAMatricies(self): """Returns the full geometric operator""" - indActive = self.indActive + active_cells = self.active_cells # GET CELL INFORMATION FOR FORWARD MODELING meshObj = self.mesh - xyzc = meshObj.gridCC[indActive, :] - xyzh = meshObj.h_gridded[indActive, :] + xyzc = meshObj.gridCC[active_cells, :] + xyzh = meshObj.h_gridded[active_cells, :] # GET LIST OF A MATRICIES A = [] @@ -811,7 +842,7 @@ def __init__(self, mesh, xi=None, xiMap=None, **kwargs): self.xi = xi self.xiMap = xiMap - nAct = list(self.indActive).count(True) + nAct = list(self.active_cells).count(True) if self.xiMap is None: self.xiMap = maps.IdentityMap(nP=nAct) diff --git a/tests/em/vrm/test_vrmfwd.py b/tests/em/vrm/test_vrmfwd.py index ea751f0afe..78d7eb57ca 100644 --- a/tests/em/vrm/test_vrmfwd.py +++ b/tests/em/vrm/test_vrmfwd.py @@ -1,3 +1,4 @@ +import pytest import unittest import numpy as np import discretize @@ -523,5 +524,87 @@ def test_receiver_types(self): self.assertTrue(Test) +class TestDeprecatedIndActive: + """Test deprecated ``indActive`` argument in viscous remanent mag simulations.""" + + CLASSES = ( + vrm.BaseVRMSimulation, + vrm.Simulation3DLinear, + vrm.Simulation3DLogUniform, + ) + OLD_NAME = "indActive" + NEW_NAME = "active_cells" + + @pytest.fixture + def mesh(self): + """Sample mesh.""" + return discretize.TensorMesh([10, 10, 10], "CCN") + + @pytest.fixture + def active_cells(self, mesh): + """Sample active cells for the mesh.""" + active_cells = np.ones(mesh.n_cells, dtype=bool) + active_cells[0] = False + return active_cells + + def get_message_duplicated_error(self, old_name, new_name, version="v0.24.0"): + msg = ( + f"Cannot pass both '{new_name}' and '{old_name}'." + f"'{old_name}' has been deprecated and will be removed in " + f" SimPEG {version}, please use '{new_name}' instead." + ) + return msg + + def get_message_deprecated_warning(self, old_name, new_name, version="v0.24.0"): + msg = ( + f"'{old_name}' has been deprecated and will be removed in " + f" SimPEG {version}, please use '{new_name}' instead." + ) + return msg + + @pytest.mark.parametrize("simulation", CLASSES) + def test_warning_argument(self, mesh, active_cells, simulation): + """ + Test if warning is raised after passing ``indActive`` to the constructor. + """ + msg = self.get_message_deprecated_warning(self.OLD_NAME, self.NEW_NAME) + with pytest.warns(FutureWarning, match=msg): + simulation(mesh, indActive=active_cells) + + @pytest.mark.parametrize("simulation", CLASSES) + def test_error_duplicated_argument(self, mesh, active_cells, simulation): + """ + Test error after passing ``indActive`` and ``active_cells`` to the constructor. + """ + msg = self.get_message_duplicated_error(self.OLD_NAME, self.NEW_NAME) + with pytest.raises(TypeError, match=msg): + simulation(mesh, active_cells=active_cells, indActive=active_cells) + + @pytest.mark.parametrize("simulation", CLASSES) + def test_warning_accessing_property(self, mesh, active_cells, simulation): + """ + Test warning when trying to access the ``indActive`` property. + """ + sim = simulation(mesh, active_cells=active_cells) + msg = f"{self.OLD_NAME} has been deprecated, please use {self.NEW_NAME}" + with pytest.warns(FutureWarning, match=msg): + old_ind_active = sim.indActive + np.testing.assert_allclose(sim.active_cells, old_ind_active) + + @pytest.mark.parametrize("simulation", CLASSES) + def test_warning_setter(self, mesh, active_cells, simulation): + """ + Test warning when trying to set the ``indActive`` property. + """ + sim = simulation(mesh, active_cells=active_cells) + # Define new active cells to pass to the setter + new_active_cells = active_cells.copy() + new_active_cells[-4:] = False + msg = f"{self.OLD_NAME} has been deprecated, please use {self.NEW_NAME}" + with pytest.warns(FutureWarning, match=msg): + sim.indActive = new_active_cells + np.testing.assert_allclose(sim.active_cells, new_active_cells) + + if __name__ == "__main__": unittest.main() diff --git a/tutorials/10-vrm/plot_fwd_1_vrm_layer.py b/tutorials/10-vrm/plot_fwd_1_vrm_layer.py index b783113a15..4553ce56b1 100644 --- a/tutorials/10-vrm/plot_fwd_1_vrm_layer.py +++ b/tutorials/10-vrm/plot_fwd_1_vrm_layer.py @@ -173,7 +173,7 @@ simulation = vrm.Simulation3DLinear( mesh, survey=survey, - indActive=ind_active, + active_cells=ind_active, refinement_factor=2, refinement_distance=[2.0, 4.0], ) diff --git a/tutorials/10-vrm/plot_fwd_2_vrm_topsoil.py b/tutorials/10-vrm/plot_fwd_2_vrm_topsoil.py index a1a60bec98..6f2857eb77 100644 --- a/tutorials/10-vrm/plot_fwd_2_vrm_topsoil.py +++ b/tutorials/10-vrm/plot_fwd_2_vrm_topsoil.py @@ -228,7 +228,7 @@ simulation = vrm.Simulation3DLinear( mesh, survey=survey, - indActive=ind_active, + active_cells=ind_active, refinement_factor=2, refinement_distance=[2.0, 4.0], ) diff --git a/tutorials/10-vrm/plot_fwd_3_vrm_tem.py b/tutorials/10-vrm/plot_fwd_3_vrm_tem.py index fece2a72ec..bdeb87ac5b 100644 --- a/tutorials/10-vrm/plot_fwd_3_vrm_tem.py +++ b/tutorials/10-vrm/plot_fwd_3_vrm_tem.py @@ -256,7 +256,7 @@ vrm_simulation = vrm.Simulation3DLogUniform( mesh, survey=vrm_survey, - indActive=ind_active, + active_cells=ind_active, refinement_factor=1, refinement_distance=[100.0], chi0=chi0_model, From 95a96170809b20c9258c1649414a702e99ebb438 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 7 Oct 2024 13:08:07 -0700 Subject: [PATCH 065/194] Test assigned values when passing deprecated args (#1544) Extend tests for some of the latest deprecated arguments to ensure that after passing the deprecated argument to the class constructor not only we receive the right warning, but the class still behaves in the same way it will if passing the new argument. --- tests/base/test_maps.py | 24 ++++++++++++++++-------- tests/em/vrm/test_vrmfwd.py | 3 ++- tests/pf/test_base_pf_simulation.py | 3 ++- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/tests/base/test_maps.py b/tests/base/test_maps.py index 0441adf7d3..5d0478bf19 100644 --- a/tests/base/test_maps.py +++ b/tests/base/test_maps.py @@ -820,7 +820,8 @@ def test_warning_argument(self, mesh, active_cells): """ msg = self.get_message_deprecated_warning("actInd", "active_cells") with pytest.warns(FutureWarning, match=msg): - maps.ParametricPolyMap(mesh, 2, actInd=active_cells) + map_instance = maps.ParametricPolyMap(mesh, 2, actInd=active_cells) + np.testing.assert_allclose(map_instance.active_cells, active_cells) def test_error_duplicated_argument(self, mesh, active_cells): """ @@ -869,7 +870,8 @@ def test_warning_argument(self, meshes, active_cells): """ msg = self.get_message_deprecated_warning("indActive", "active_cells") with pytest.warns(FutureWarning, match=msg): - maps.Mesh2Mesh(meshes, indActive=active_cells) + mapping_instance = maps.Mesh2Mesh(meshes, indActive=active_cells) + np.testing.assert_allclose(mapping_instance.active_cells, active_cells) def test_error_duplicated_argument(self, meshes, active_cells): """ @@ -912,7 +914,8 @@ def test_indactive_warning_argument(self, mesh, active_cells): """ msg = self.get_message_deprecated_warning("indActive", "active_cells") with pytest.warns(FutureWarning, match=msg): - maps.InjectActiveCells(mesh, indActive=active_cells) + mapping_instance = maps.InjectActiveCells(mesh, indActive=active_cells) + np.testing.assert_allclose(mapping_instance.active_cells, active_cells) def test_indactive_error_duplicated_argument(self, mesh, active_cells): """ @@ -947,16 +950,20 @@ def test_indactive_warning_setter(self, mesh, active_cells): mapping.indActive = new_active_cells np.testing.assert_allclose(mapping.active_cells, new_active_cells) - @pytest.mark.parametrize("valInactive", (3.14, np.array([1]))) - def test_valinactive_warning_argument(self, mesh, active_cells, valInactive): + @pytest.mark.parametrize("value_inactive", (3.14, np.array([1]))) + def test_valinactive_warning_argument(self, mesh, active_cells, value_inactive): """ Test if warning is raised after passing ``valInactive`` to the constructor. """ msg = self.get_message_deprecated_warning("valInactive", "value_inactive") with pytest.warns(FutureWarning, match=msg): - maps.InjectActiveCells( - mesh, active_cells=active_cells, valInactive=valInactive + mapping_instance = maps.InjectActiveCells( + mesh, active_cells=active_cells, valInactive=value_inactive ) + # Ensure that the value passed to valInactive was correctly used + expected = np.zeros_like(active_cells, dtype=np.float64) + expected[~active_cells] = value_inactive + np.testing.assert_allclose(mapping_instance.value_inactive, expected) @pytest.mark.parametrize("valInactive", (3.14, np.array([3.14]))) @pytest.mark.parametrize("value_inactive", (3.14, np.array([3.14]))) @@ -1013,7 +1020,8 @@ def test_indactive_warning_argument(self, mesh, active_cells, map_class): """ msg = self.get_message_deprecated_warning("indActive", "active_cells") with pytest.warns(FutureWarning, match=msg): - map_class(mesh, indActive=active_cells) + mapping_instance = map_class(mesh, indActive=active_cells) + np.testing.assert_allclose(mapping_instance.active_cells, active_cells) @pytest.mark.parametrize("map_class", CLASSES) def test_indactive_error_duplicated_argument(self, mesh, active_cells, map_class): diff --git a/tests/em/vrm/test_vrmfwd.py b/tests/em/vrm/test_vrmfwd.py index 78d7eb57ca..772867faa6 100644 --- a/tests/em/vrm/test_vrmfwd.py +++ b/tests/em/vrm/test_vrmfwd.py @@ -569,7 +569,8 @@ def test_warning_argument(self, mesh, active_cells, simulation): """ msg = self.get_message_deprecated_warning(self.OLD_NAME, self.NEW_NAME) with pytest.warns(FutureWarning, match=msg): - simulation(mesh, indActive=active_cells) + sim = simulation(mesh, indActive=active_cells) + np.testing.assert_allclose(sim.active_cells, active_cells) @pytest.mark.parametrize("simulation", CLASSES) def test_error_duplicated_argument(self, mesh, active_cells, simulation): diff --git a/tests/pf/test_base_pf_simulation.py b/tests/pf/test_base_pf_simulation.py index 7ba37ae1c3..9d482a20f7 100644 --- a/tests/pf/test_base_pf_simulation.py +++ b/tests/pf/test_base_pf_simulation.py @@ -320,7 +320,8 @@ def test_deprecated_argument(self, tensor_mesh, mock_simulation_class): f" SimPEG {version_regex}, please use 'active_cells' instead." ) with pytest.warns(FutureWarning, match=msg): - mock_simulation_class(tensor_mesh, ind_active=ind_active) + sim = mock_simulation_class(tensor_mesh, ind_active=ind_active) + np.testing.assert_allclose(sim.active_cells, ind_active) def test_error_both_args(self, tensor_mesh, mock_simulation_class): """Test if passing both ind_active and active_cells raises error.""" From 4ca99a735c366575f5b56b630c7fde9f6d6a269e Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 7 Oct 2024 14:51:57 -0700 Subject: [PATCH 066/194] Use random seed when using `make_synthetic_data` in tests (#1545) Pass a `random_seed` when calling `make_synthetic_data` method of simulations in tests so we ensure that they are always run with the same synthetic data. Remove unused calls to `np.random.seed`. --- tests/base/test_data_misfit.py | 4 +--- tests/base/test_directives.py | 6 ++++-- tests/base/test_joint.py | 5 +++-- tests/dask/test_DC_jvecjtvecadj_dask.py | 6 ++---- tests/dask/test_IP_jvecjtvecadj_dask.py | 8 ++++---- tests/dask/test_grav_inversion_linear.py | 6 +++++- tests/dask/test_mag_MVI_Octree.py | 6 +++++- tests/dask/test_mag_inversion_linear_Octree.py | 6 +++++- tests/em/static/test_DC_2D_jvecjtvecadj.py | 2 +- tests/em/static/test_DC_Utils.py | 4 +++- tests/em/static/test_DC_jvecjtvecadj.py | 11 +++++------ tests/em/static/test_IP_2D_jvecjtvecadj.py | 4 ++-- tests/em/static/test_IP_jvecjtvecadj.py | 8 ++++---- tests/em/static/test_SIP_2D_jvecjtvecadj.py | 6 +++--- tests/em/static/test_SIP_jvecjtvecadj.py | 6 +++--- tests/em/static/test_SPjvecjtvecadj.py | 4 ++-- tests/em/vrm/test_vrminv.py | 2 +- tests/utils/test_mat_utils.py | 1 + 18 files changed, 54 insertions(+), 41 deletions(-) diff --git a/tests/base/test_data_misfit.py b/tests/base/test_data_misfit.py index 2e23131da5..a981cd625f 100644 --- a/tests/base/test_data_misfit.py +++ b/tests/base/test_data_misfit.py @@ -6,8 +6,6 @@ from simpeg import maps from simpeg import data_misfit, simulation, survey -np.random.seed(17) - class DataMisfitTest(unittest.TestCase): def setUp(self): @@ -23,7 +21,7 @@ def setUp(self): mesh=mesh, survey=survey.BaseSurvey([source]), model_map=maps.ExpMap(mesh) ) - synthetic_data = sim.make_synthetic_data(model) + synthetic_data = sim.make_synthetic_data(model, random_seed=17) self.relative = 0.01 self.noise_floor = 1e-8 diff --git a/tests/base/test_directives.py b/tests/base/test_directives.py index a54a49cfcf..fbc19a4782 100644 --- a/tests/base/test_directives.py +++ b/tests/base/test_directives.py @@ -88,7 +88,7 @@ def setUp(self): m = np.random.rand(mesh.nC) - data = sim.make_synthetic_data(m, add_noise=True) + data = sim.make_synthetic_data(m, add_noise=True, random_seed=19) dmis = data_misfit.L2DataMisfit(data=data, simulation=sim) dmis.W = 1.0 / data.relative_error @@ -391,7 +391,9 @@ def test_save_output_dict(RegClass): sim = simulation.ExponentialSinusoidSimulation( mesh=mesh, model_map=maps.IdentityMap() ) - data = sim.make_synthetic_data(np.ones(mesh.n_cells), add_noise=True) + data = sim.make_synthetic_data( + np.ones(mesh.n_cells), add_noise=True, random_seed=20 + ) dmis = data_misfit.L2DataMisfit(data, sim) opt = optimization.InexactGaussNewton(maxIter=1) diff --git a/tests/base/test_joint.py b/tests/base/test_joint.py index 3d269cb141..8485425bdc 100644 --- a/tests/base/test_joint.py +++ b/tests/base/test_joint.py @@ -53,8 +53,9 @@ def setUp(self): mesh=mesh, survey=survey1, rhoMap=maps.ExpMap(mesh) ) - dobs0 = simulation0.make_synthetic_data(model) - dobs1 = simulation1.make_synthetic_data(model) + rng = np.random.default_rng(seed=42) + dobs0 = simulation0.make_synthetic_data(model, random_seed=rng) + dobs1 = simulation1.make_synthetic_data(model, random_seed=rng) self.mesh = mesh self.model = model diff --git a/tests/dask/test_DC_jvecjtvecadj_dask.py b/tests/dask/test_DC_jvecjtvecadj_dask.py index a2f32c2e16..bd9c6140bd 100644 --- a/tests/dask/test_DC_jvecjtvecadj_dask.py +++ b/tests/dask/test_DC_jvecjtvecadj_dask.py @@ -15,8 +15,6 @@ from simpeg.electromagnetics import resistivity as dc import shutil -np.random.seed(40) - TOL = 1e-5 FLR = 1e-20 # "zero", so if residual below this --> pass regardless of order @@ -45,7 +43,7 @@ def setUp(self): ) mSynth = np.ones(mesh.nC) - dobs = simulation.make_synthetic_data(mSynth, add_noise=True) + dobs = simulation.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(simulation=simulation, data=dobs) @@ -123,7 +121,7 @@ def setUp(self): ) mSynth = np.ones(mesh.nC) - dobs = simulation.make_synthetic_data(mSynth, add_noise=True) + dobs = simulation.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(simulation=simulation, data=dobs) diff --git a/tests/dask/test_IP_jvecjtvecadj_dask.py b/tests/dask/test_IP_jvecjtvecadj_dask.py index 73ac660054..339b074d4d 100644 --- a/tests/dask/test_IP_jvecjtvecadj_dask.py +++ b/tests/dask/test_IP_jvecjtvecadj_dask.py @@ -162,7 +162,7 @@ def setUp(self): mesh=mesh, survey=survey, sigma=sigma, etaMap=maps.IdentityMap(mesh) ) mSynth = np.ones(mesh.nC) * 0.1 - dobs = simulation.make_synthetic_data(mSynth, add_noise=True) + dobs = simulation.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(data=dobs, simulation=simulation) reg = regularization.WeightedLeastSquares(mesh) @@ -232,7 +232,7 @@ def setUp(self): mesh=mesh, survey=survey, sigma=sigma, etaMap=maps.IdentityMap(mesh) ) mSynth = np.ones(mesh.nC) * 0.1 - dobs = simulation.make_synthetic_data(mSynth, add_noise=True) + dobs = simulation.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(data=dobs, simulation=simulation) reg = regularization.WeightedLeastSquares(mesh) @@ -305,7 +305,7 @@ def setUp(self): storeJ=True, ) mSynth = np.ones(mesh.nC) * 0.1 - dobs = simulation.make_synthetic_data(mSynth, add_noise=True) + dobs = simulation.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(data=dobs, simulation=simulation) reg = regularization.WeightedLeastSquares(mesh) @@ -385,7 +385,7 @@ def setUp(self): storeJ=True, ) mSynth = np.ones(mesh.nC) * 0.1 - dobs = simulation.make_synthetic_data(mSynth, add_noise=True) + dobs = simulation.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(data=dobs, simulation=simulation) reg = regularization.WeightedLeastSquares(mesh) diff --git a/tests/dask/test_grav_inversion_linear.py b/tests/dask/test_grav_inversion_linear.py index fb08a9be66..bef3ea796f 100644 --- a/tests/dask/test_grav_inversion_linear.py +++ b/tests/dask/test_grav_inversion_linear.py @@ -86,7 +86,11 @@ def setUp(self): # computing sensitivities to ram is best using dask processes with dask.config.set(scheduler="processes"): data = sim.make_synthetic_data( - self.model, relative_error=0.0, noise_floor=0.0005, add_noise=True + self.model, + relative_error=0.0, + noise_floor=0.0005, + add_noise=True, + random_seed=42, ) # Create a regularization reg = regularization.Sparse(self.mesh, active_cells=actv, mapping=idenMap) diff --git a/tests/dask/test_mag_MVI_Octree.py b/tests/dask/test_mag_MVI_Octree.py index 49d2ca153e..167a191775 100644 --- a/tests/dask/test_mag_MVI_Octree.py +++ b/tests/dask/test_mag_MVI_Octree.py @@ -108,7 +108,11 @@ def setUp(self): # Compute some data and add some random noise data = sim.make_synthetic_data( - utils.mkvc(self.model), relative_error=0.0, noise_floor=5.0, add_noise=True + utils.mkvc(self.model), + relative_error=0.0, + noise_floor=5.0, + add_noise=True, + random_seed=40, ) # This Mapping connects the regularizations for the three-component diff --git a/tests/dask/test_mag_inversion_linear_Octree.py b/tests/dask/test_mag_inversion_linear_Octree.py index 857f3d297a..69a1f4748a 100644 --- a/tests/dask/test_mag_inversion_linear_Octree.py +++ b/tests/dask/test_mag_inversion_linear_Octree.py @@ -113,7 +113,11 @@ def setUp(self): ) self.sim = sim data = sim.make_synthetic_data( - self.model, relative_error=0.0, noise_floor=1.0, add_noise=True + self.model, + relative_error=0.0, + noise_floor=1.0, + add_noise=True, + random_seed=40, ) # Create a regularization diff --git a/tests/em/static/test_DC_2D_jvecjtvecadj.py b/tests/em/static/test_DC_2D_jvecjtvecadj.py index c974a5b5e7..a06a21c98d 100644 --- a/tests/em/static/test_DC_2D_jvecjtvecadj.py +++ b/tests/em/static/test_DC_2D_jvecjtvecadj.py @@ -52,7 +52,7 @@ def setUp(self): bc_type=self.bc_type, ) mSynth = np.ones(mesh.nC) * 1.0 - data = simulation.make_synthetic_data(mSynth, add_noise=True) + data = simulation.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(simulation=simulation, data=data) diff --git a/tests/em/static/test_DC_Utils.py b/tests/em/static/test_DC_Utils.py index 4c33717cd1..80c783cf28 100644 --- a/tests/em/static/test_DC_Utils.py +++ b/tests/em/static/test_DC_Utils.py @@ -91,7 +91,9 @@ def test_io_rhoa(self): problem.solver = Solver # Create synthetic data - dobs = problem.make_synthetic_data(self.model, relative_error=0.0) + dobs = problem.make_synthetic_data( + self.model, relative_error=0.0, random_seed=40 + ) dobs.noise_floor = 1e-5 # Testing IO diff --git a/tests/em/static/test_DC_jvecjtvecadj.py b/tests/em/static/test_DC_jvecjtvecadj.py index 703bc8e7ce..4b200dbbce 100644 --- a/tests/em/static/test_DC_jvecjtvecadj.py +++ b/tests/em/static/test_DC_jvecjtvecadj.py @@ -16,7 +16,6 @@ from pymatsolver import Pardiso import shutil -np.random.seed(40) TOL = 1e-5 FLR = 1e-20 # "zero", so if residual below this --> pass regardless of order @@ -46,7 +45,7 @@ def setUp(self): ) mSynth = np.ones(mesh.nC) - dobs = simulation.make_synthetic_data(mSynth, add_noise=True) + dobs = simulation.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(simulation=simulation, data=dobs) @@ -191,7 +190,7 @@ def setUp(self): ) mSynth = np.ones(mesh.nC) - dobs = simulation.make_synthetic_data(mSynth, add_noise=True) + dobs = simulation.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(simulation=simulation, data=dobs) @@ -339,7 +338,7 @@ def setUp(self): ) mSynth = np.ones(mesh.nC) - dobs = simulation.make_synthetic_data(mSynth, add_noise=True) + dobs = simulation.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(simulation=simulation, data=dobs) @@ -420,7 +419,7 @@ def setUp(self): ) mSynth = np.ones(mesh.nC) - dobs = simulation.make_synthetic_data(mSynth, add_noise=True) + dobs = simulation.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(simulation=simulation, data=dobs) @@ -505,7 +504,7 @@ def setUp(self): ) mSynth = np.ones(mesh.nC) - dobs = simulation.make_synthetic_data(mSynth, add_noise=True) + dobs = simulation.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(simulation=simulation, data=dobs) diff --git a/tests/em/static/test_IP_2D_jvecjtvecadj.py b/tests/em/static/test_IP_2D_jvecjtvecadj.py index 99e5cfb06b..d7fe6ce54e 100644 --- a/tests/em/static/test_IP_2D_jvecjtvecadj.py +++ b/tests/em/static/test_IP_2D_jvecjtvecadj.py @@ -47,7 +47,7 @@ def setUp(self): ) mSynth = np.ones(mesh.nC) * 0.1 - dobs = problem.make_synthetic_data(mSynth, add_noise=True) + dobs = problem.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(data=dobs, simulation=problem) reg = regularization.WeightedLeastSquares(mesh) @@ -127,7 +127,7 @@ def setUp(self): ) mSynth = np.ones(mesh.nC) * 0.1 - dobs = problem.make_synthetic_data(mSynth, add_noise=True) + dobs = problem.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(data=dobs, simulation=problem) reg = regularization.WeightedLeastSquares(mesh) diff --git a/tests/em/static/test_IP_jvecjtvecadj.py b/tests/em/static/test_IP_jvecjtvecadj.py index 30189d133d..6388c5fb8b 100644 --- a/tests/em/static/test_IP_jvecjtvecadj.py +++ b/tests/em/static/test_IP_jvecjtvecadj.py @@ -39,7 +39,7 @@ def setUp(self): mesh=mesh, survey=survey, sigma=sigma, etaMap=maps.IdentityMap(mesh) ) mSynth = np.ones(mesh.nC) * 0.1 - dobs = simulation.make_synthetic_data(mSynth, add_noise=True) + dobs = simulation.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(data=dobs, simulation=simulation) reg = regularization.WeightedLeastSquares(mesh) @@ -112,7 +112,7 @@ def setUp(self): mesh=mesh, survey=survey, sigma=sigma, etaMap=maps.IdentityMap(mesh) ) mSynth = np.ones(mesh.nC) * 0.1 - dobs = simulation.make_synthetic_data(mSynth, add_noise=True) + dobs = simulation.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(data=dobs, simulation=simulation) reg = regularization.WeightedLeastSquares(mesh) @@ -188,7 +188,7 @@ def setUp(self): storeJ=True, ) mSynth = np.ones(mesh.nC) * 0.1 - dobs = simulation.make_synthetic_data(mSynth, add_noise=True) + dobs = simulation.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(data=dobs, simulation=simulation) reg = regularization.WeightedLeastSquares(mesh) @@ -271,7 +271,7 @@ def setUp(self): storeJ=True, ) mSynth = np.ones(mesh.nC) * 0.1 - dobs = simulation.make_synthetic_data(mSynth, add_noise=True) + dobs = simulation.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(data=dobs, simulation=simulation) reg = regularization.WeightedLeastSquares(mesh) diff --git a/tests/em/static/test_SIP_2D_jvecjtvecadj.py b/tests/em/static/test_SIP_2D_jvecjtvecadj.py index 1df7d48000..d5050d35f1 100644 --- a/tests/em/static/test_SIP_2D_jvecjtvecadj.py +++ b/tests/em/static/test_SIP_2D_jvecjtvecadj.py @@ -66,7 +66,7 @@ def setUp(self): ) mSynth = np.r_[eta, 1.0 / tau] problem.model = mSynth - dobs = problem.make_synthetic_data(mSynth, add_noise=True) + dobs = problem.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(data=dobs, simulation=problem) reg = regularization.WeightedLeastSquares(mesh) @@ -161,7 +161,7 @@ def setUp(self): ) mSynth = np.r_[eta, 1.0 / tau] problem.model = mSynth - dobs = problem.make_synthetic_data(mSynth, add_noise=True) + dobs = problem.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(data=dobs, simulation=problem) reg = regularization.WeightedLeastSquares(mesh) @@ -265,7 +265,7 @@ def setUp(self): survey=survey, ) mSynth = np.r_[eta[~airind], 1.0 / tau[~airind], c[~airind]] - dobs = problem.make_synthetic_data(mSynth, add_noise=True) + dobs = problem.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(data=dobs, simulation=problem) reg_eta = regularization.WeightedLeastSquares( diff --git a/tests/em/static/test_SIP_jvecjtvecadj.py b/tests/em/static/test_SIP_jvecjtvecadj.py index a1cc1aea2f..9b903aaa61 100644 --- a/tests/em/static/test_SIP_jvecjtvecadj.py +++ b/tests/em/static/test_SIP_jvecjtvecadj.py @@ -72,7 +72,7 @@ def setUp(self): problem.solver = Solver mSynth = np.r_[eta, 1.0 / tau] problem.model = mSynth - dobs = problem.make_synthetic_data(mSynth, add_noise=True) + dobs = problem.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(data=dobs, simulation=problem) reg = regularization.WeightedLeastSquares(mesh) @@ -172,7 +172,7 @@ def setUp(self): problem.solver = Solver mSynth = np.r_[eta, 1.0 / tau] print(survey.nD) - dobs = problem.make_synthetic_data(mSynth, add_noise=True) + dobs = problem.make_synthetic_data(mSynth, add_noise=True, random_seed=40) print(survey.nD) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(data=dobs, simulation=problem) @@ -283,7 +283,7 @@ def setUp(self): problem.solver = Solver mSynth = np.r_[eta[~airind], 1.0 / tau[~airind], c[~airind]] - dobs = problem.make_synthetic_data(mSynth, add_noise=True) + dobs = problem.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(data=dobs, simulation=problem) reg_eta = regularization.Sparse(mesh, mapping=wires.eta, active_cells=~airind) diff --git a/tests/em/static/test_SPjvecjtvecadj.py b/tests/em/static/test_SPjvecjtvecadj.py index df26ee3706..d1d8ac20cf 100644 --- a/tests/em/static/test_SPjvecjtvecadj.py +++ b/tests/em/static/test_SPjvecjtvecadj.py @@ -43,8 +43,8 @@ def test_forward(): mesh=mesh, survey=dc_survey, sigma=conductivity ) - dc_dpred = sim_dc.make_synthetic_data(None, add_noise=False) - sp_dpred = sim.make_synthetic_data(q, add_noise=False) + dc_dpred = sim_dc.make_synthetic_data(None, add_noise=False, random_seed=40) + sp_dpred = sim.make_synthetic_data(q, add_noise=False, random_seed=40) np.testing.assert_allclose(dc_dpred.dobs, sp_dpred.dobs) diff --git a/tests/em/vrm/test_vrminv.py b/tests/em/vrm/test_vrminv.py index 1b64627490..6882cfd048 100644 --- a/tests/em/vrm/test_vrminv.py +++ b/tests/em/vrm/test_vrminv.py @@ -58,7 +58,7 @@ def test_basic_inversion(self): Survey.t_active = np.zeros(Survey.nD, dtype=bool) Survey.set_active_interval(-1e6, 1e6) Problem = vrm.Simulation3DLinear(meshObj, survey=Survey, refinement_factor=2) - dobs = Problem.make_synthetic_data(mod) + dobs = Problem.make_synthetic_data(mod, random_seed=40) Survey.noise_floor = 1e-11 dmis = data_misfit.L2DataMisfit(data=dobs, simulation=Problem) diff --git a/tests/utils/test_mat_utils.py b/tests/utils/test_mat_utils.py index 5d85f51804..9aecd7eee9 100644 --- a/tests/utils/test_mat_utils.py +++ b/tests/utils/test_mat_utils.py @@ -51,6 +51,7 @@ def g(k): relative_error=relative_error, noise_floor=noise_floor, add_noise=True, + random_seed=40, ) dmis = data_misfit.L2DataMisfit(simulation=sim, data=data_obj) self.dmis = dmis From 9a07f0a702a0d7fdda72143aa6ba86220c4b3b9e Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 8 Oct 2024 09:03:02 -0700 Subject: [PATCH 067/194] Minor improvements to `UpdateIRLS` class (#1529) Remove unneeded if statement, since the `reg` objects at that point are only `Sparse` due to previous check. Update comments to reflect better what the code does. Add tests to check if `irls_threshold` is correctly updated. --- simpeg/directives/_regularization.py | 9 ++-- tests/base/test_directives.py | 71 ++++++++++++++++++++++++++-- 2 files changed, 72 insertions(+), 8 deletions(-) diff --git a/simpeg/directives/_regularization.py b/simpeg/directives/_regularization.py index bb3a20ef59..fe4f44c9d3 100644 --- a/simpeg/directives/_regularization.py +++ b/simpeg/directives/_regularization.py @@ -262,7 +262,7 @@ def endIter(self): """ Check on progress of the inversion and start/update the IRLS process. """ - # Check if misfit is within the tolerance, otherwise scale beta + # Update the cooling factor (only after IRLS has started) self.adjust_cooling_schedule() # After reaching target misfit with l2-norm, switch to IRLS (mode:2) @@ -272,7 +272,7 @@ def endIter(self): ): self.start_irls() - # Only update after GN iterations + # Perform IRLS (only after `self.cooling_rate` iterations) if ( self.metrics.start_irls_iter is not None and (self.opt.iter - self.metrics.start_irls_iter) % self.cooling_rate == 0 @@ -283,14 +283,13 @@ def endIter(self): else: self.opt.stopNextIteration = False - # Print to screen + # Cool irls thresholds for reg in self.reg.objfcts: if not isinstance(reg, Sparse): continue for obj in reg.objfcts: - if isinstance(reg, (Sparse, BaseSparse)): - obj.irls_threshold /= self.irls_cooling_factor + obj.irls_threshold /= self.irls_cooling_factor self.metrics.irls_iteration_count += 1 diff --git a/tests/base/test_directives.py b/tests/base/test_directives.py index fbc19a4782..132cef2e5a 100644 --- a/tests/base/test_directives.py +++ b/tests/base/test_directives.py @@ -7,15 +7,18 @@ maps, directives, regularization, - data_misfit, optimization, inversion, inverse_problem, simulation, ) +from simpeg.data_misfit import L2DataMisfit from simpeg.potential_fields import magnetics as mag import shutil +from simpeg.regularization.base import Smallness +from simpeg.regularization.sparse import Sparse + class directivesValidation(unittest.TestCase): def test_validation_pass(self): @@ -89,7 +92,7 @@ def setUp(self): m = np.random.rand(mesh.nC) data = sim.make_synthetic_data(m, add_noise=True, random_seed=19) - dmis = data_misfit.L2DataMisfit(data=data, simulation=sim) + dmis = L2DataMisfit(data=data, simulation=sim) dmis.W = 1.0 / data.relative_error # Add directives to the inversion @@ -394,7 +397,7 @@ def test_save_output_dict(RegClass): data = sim.make_synthetic_data( np.ones(mesh.n_cells), add_noise=True, random_seed=20 ) - dmis = data_misfit.L2DataMisfit(data, sim) + dmis = L2DataMisfit(data, sim) opt = optimization.InexactGaussNewton(maxIter=1) @@ -575,5 +578,67 @@ def test_beta_estimate_max_derivative(self): assert directive.seed == seed +class TestUpdateIRLS: + """ + Additional tests to UpdateIRLS directive. + """ + + @pytest.fixture + def mesh(self): + """Sample tensor mesh.""" + return discretize.TensorMesh([4, 4, 4]) + + @pytest.fixture + def data_misfit(self, mesh): + rx = mag.Point(np.vstack([[0.25, 0.25, 0.25], [-0.25, -0.25, 0.25]])) + igrf = mag.UniformBackgroundField( + receiver_list=[rx], amplitude=5000, inclination=90, declination=0 + ) + survey = mag.Survey(igrf) + sim = mag.Simulation3DIntegral( + mesh, survey=survey, chiMap=maps.IdentityMap(mesh) + ) + model = np.random.default_rng(seed=42).normal(size=mesh.n_cells) + data = sim.make_synthetic_data(model, add_noise=True) + dmisfit = L2DataMisfit(data=data, simulation=sim) + return dmisfit + + def test_end_iter_irls_threshold(self, mesh, data_misfit): + """ + Test if irls_threshold is modified in every regularization term after + the IRLS process started. + """ + # Define a regularization combo with sparse and non-sparse terms + irls_threshold = 4.5 + sparse_regularization = Sparse( + mesh, norms=[1, 1, 1, 1], irls_threshold=irls_threshold + ) + non_sparse_regularization = Smallness(mesh) + reg = 0.1 * sparse_regularization + 0.5 * non_sparse_regularization + # Define inversion + opt = optimization.ProjectedGNCG() + opt.iter = 0 # manually set iter to zero + inv_prob = inverse_problem.BaseInvProblem(data_misfit, reg, opt) + inv_prob.phi_d = np.nan # manually set value for phi_d + inv_prob.model = np.zeros(mesh.n_cells) # manually set the model + # Define inversion + inv = inversion.BaseInversion(inv_prob) + irls_cooling_factor = 1.2 + update_irls = directives.UpdateIRLS( + irls_cooling_factor=irls_cooling_factor, + inversion=inv, + dmisfit=data_misfit, + reg=reg, + ) + # Modify metrics to kick in the IRLS process + update_irls.metrics.start_irls_iter = 0 + # Check irls_threshold of the objective function terms after running endIter + update_irls.endIter() + for obj_fun in sparse_regularization.objfcts: + assert obj_fun.irls_threshold == irls_threshold / irls_cooling_factor + # The irls_threshold for the sparse_regularization should not be changed + assert sparse_regularization.irls_threshold == irls_threshold + + if __name__ == "__main__": unittest.main() From 492f940c79d160d95b9b35f237603c2d44ed77e2 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 8 Oct 2024 12:54:06 -0700 Subject: [PATCH 068/194] Replace `seed` for `random_seed` in directives (#1538) Replace `seed` for `random_seed` in directive classes. Replace the `seed` argument for a new `random_seed` argument in `eigenvalue_by_power_iteration`. Update usage of `seed` argument in tests and in codebase. Add tests to check the deprecations. Part of #1461 --- .../plot_inv_fdem_loop_loop_2Dinversion.py | 2 +- .../plot_heagyetal2017_cyl_inversions.py | 2 +- .../plot_laguna_del_maule_inversion.py | 2 +- simpeg/directives/directives.py | 169 ++++++++++++++---- simpeg/directives/sim_directives.py | 4 +- simpeg/utils/mat_utils.py | 27 ++- tests/base/test_directives.py | 101 +++++++++-- tests/utils/test_mat_utils.py | 70 +++++++- 8 files changed, 318 insertions(+), 59 deletions(-) diff --git a/examples/05-fdem/plot_inv_fdem_loop_loop_2Dinversion.py b/examples/05-fdem/plot_inv_fdem_loop_loop_2Dinversion.py index 43699d6743..e346aa7ac9 100644 --- a/examples/05-fdem/plot_inv_fdem_loop_loop_2Dinversion.py +++ b/examples/05-fdem/plot_inv_fdem_loop_loop_2Dinversion.py @@ -288,7 +288,7 @@ def plot_data(data, ax=None, color="C0", label=""): opt = optimization.InexactGaussNewton(maxIterCG=10, remember="xc") invProb = inverse_problem.BaseInvProblem(dmisfit, reg, opt) -betaest = directives.BetaEstimate_ByEig(beta0_ratio=0.05, n_pw_iter=1, seed=1) +betaest = directives.BetaEstimate_ByEig(beta0_ratio=0.05, n_pw_iter=1, random_seed=1) target = directives.TargetMisfit() directiveList = [betaest, target] diff --git a/examples/20-published/plot_heagyetal2017_cyl_inversions.py b/examples/20-published/plot_heagyetal2017_cyl_inversions.py index 0c72149b2e..e41a05f52c 100644 --- a/examples/20-published/plot_heagyetal2017_cyl_inversions.py +++ b/examples/20-published/plot_heagyetal2017_cyl_inversions.py @@ -155,7 +155,7 @@ def run(plotIt=True, saveFig=False): # directives beta = directives.BetaSchedule(coolingFactor=4, coolingRate=3) - betaest = directives.BetaEstimate_ByEig(beta0_ratio=1.0, seed=518936) + betaest = directives.BetaEstimate_ByEig(beta0_ratio=1.0, random_seed=518936) target = directives.TargetMisfit() directiveList = [beta, betaest, target] diff --git a/examples/20-published/plot_laguna_del_maule_inversion.py b/examples/20-published/plot_laguna_del_maule_inversion.py index a80d5be245..b7f212e1c1 100644 --- a/examples/20-published/plot_laguna_del_maule_inversion.py +++ b/examples/20-published/plot_laguna_del_maule_inversion.py @@ -121,7 +121,7 @@ def run(plotIt=True, cleanAfterRun=True): invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) # Specify how the initial beta is found - betaest = directives.BetaEstimate_ByEig(beta0_ratio=0.5, seed=518936) + betaest = directives.BetaEstimate_ByEig(beta0_ratio=0.5, random_seed=518936) # IRLS sets up the Lp inversion problem # Set the eps parameter parameter in Line 11 of the diff --git a/simpeg/directives/directives.py b/simpeg/directives/directives.py index 2932de7af1..7075dcfd2d 100644 --- a/simpeg/directives/directives.py +++ b/simpeg/directives/directives.py @@ -354,22 +354,45 @@ class BaseBetaEstimator(InversionDirective): ---------- beta0_ratio : float Desired ratio between data misfit and model objective function at initial beta iteration. - seed : None or :class:`~simpeg.typing.RandomSeed`, optional + random_seed : None or :class:`~simpeg.typing.RandomSeed`, optional Random seed used for random sampling. It can either be an int, a predefined Numpy random number generator, or any valid input to ``numpy.random.default_rng``. + seed : None or :class:`~simpeg.typing.RandomSeed`, optional + + .. deprecated:: 0.23.0 + + Argument ``seed`` is deprecated in favor of ``random_seed`` and will + be removed in SimPEG v0.24.0. """ def __init__( self, beta0_ratio=1.0, + random_seed: RandomSeed | None = None, seed: RandomSeed | None = None, **kwargs, ): super().__init__(**kwargs) self.beta0_ratio = beta0_ratio - self.seed = seed + + # Deprecate seed argument + if seed is not None: + if random_seed is not None: + raise TypeError( + "Cannot pass both 'random_seed' and 'seed'." + "'seed' has been deprecated and will be removed in " + " SimPEG v0.24.0, please use 'random_seed' instead.", + ) + warnings.warn( + "'seed' has been deprecated and will be removed in " + " SimPEG v0.24.0, please use 'random_seed' instead.", + FutureWarning, + stacklevel=2, + ) + random_seed = seed + self.random_seed = random_seed @property def beta0_ratio(self): @@ -388,17 +411,17 @@ def beta0_ratio(self, value): ) @property - def seed(self): + def random_seed(self): """Random seed to initialize with. Returns ------- int, numpy.random.Generator or None """ - return self._seed + return self._random_seed - @seed.setter - def seed(self, value): + @random_seed.setter + def random_seed(self, value): try: np.random.default_rng(value) except TypeError as err: @@ -407,7 +430,7 @@ def seed(self, value): f"a {type(value).__name__}" ) raise TypeError(msg) from err - self._seed = value + self._random_seed = value def validate(self, directive_list): ind = [isinstance(d, BaseBetaEstimator) for d in directive_list.dList] @@ -418,6 +441,15 @@ def validate(self, directive_list): return True + seed = deprecate_property( + random_seed, + "seed", + "random_seed", + removal_version="0.24.0", + future_warn=True, + error=False, + ) + class BetaEstimateMaxDerivative(BaseBetaEstimator): r"""Estimate initial trade-off parameter (beta) using largest derivatives. @@ -433,10 +465,16 @@ class BetaEstimateMaxDerivative(BaseBetaEstimator): ---------- beta0_ratio: float Desired ratio between data misfit and model objective function at initial beta iteration. - seed : None or :class:`~simpeg.typing.RandomSeed`, optional + random_seed : None or :class:`~simpeg.typing.RandomSeed`, optional Random seed used for random sampling. It can either be an int, a predefined Numpy random number generator, or any valid input to ``numpy.random.default_rng``. + seed : None or :class:`~simpeg.typing.RandomSeed`, optional + + .. deprecated:: 0.23.0 + + Argument ``seed`` is deprecated in favor of ``random_seed`` and will + be removed in SimPEG v0.24.0. Notes ----- @@ -466,11 +504,13 @@ class BetaEstimateMaxDerivative(BaseBetaEstimator): """ - def __init__(self, beta0_ratio=1.0, seed: RandomSeed | None = None, **kwargs): - super().__init__(beta0_ratio=beta0_ratio, seed=seed, **kwargs) + def __init__( + self, beta0_ratio=1.0, random_seed: RandomSeed | None = None, **kwargs + ): + super().__init__(beta0_ratio=beta0_ratio, random_seed=random_seed, **kwargs) def initialize(self): - rng = np.random.default_rng(seed=self.seed) + rng = np.random.default_rng(seed=self.random_seed) if self.verbose: print("Calculating the beta0 parameter.") @@ -505,10 +545,16 @@ class BetaEstimate_ByEig(BaseBetaEstimator): Desired ratio between data misfit and model objective function at initial beta iteration. n_pw_iter : int Number of power iterations used to estimate largest eigenvalues. - seed : None or :class:`~simpeg.typing.RandomSeed`, optional + random_seed : None or :class:`~simpeg.typing.RandomSeed`, optional Random seed used for random sampling. It can either be an int, a predefined Numpy random number generator, or any valid input to ``numpy.random.default_rng``. + seed : None or :class:`~simpeg.typing.RandomSeed`, optional + + .. deprecated:: 0.23.0 + + Argument ``seed`` is deprecated in favor of ``random_seed`` and will + be removed in SimPEG v0.24.0. Notes ----- @@ -541,10 +587,13 @@ def __init__( self, beta0_ratio=1.0, n_pw_iter=4, + random_seed: RandomSeed | None = None, seed: RandomSeed | None = None, **kwargs, ): - super().__init__(beta0_ratio=beta0_ratio, seed=seed, **kwargs) + super().__init__( + beta0_ratio=beta0_ratio, random_seed=random_seed, seed=seed, **kwargs + ) self.n_pw_iter = n_pw_iter @property @@ -563,7 +612,7 @@ def n_pw_iter(self, value): self._n_pw_iter = validate_integer("n_pw_iter", value, min_val=1) def initialize(self): - rng = np.random.default_rng(seed=self.seed) + rng = np.random.default_rng(seed=self.random_seed) if self.verbose: print("Calculating the beta0 parameter.") @@ -574,13 +623,13 @@ def initialize(self): self.dmisfit, m, n_pw_iter=self.n_pw_iter, - seed=rng, + random_seed=rng, ) reg_eigenvalue = eigenvalue_by_power_iteration( self.reg, m, n_pw_iter=self.n_pw_iter, - seed=rng, + random_seed=rng, ) self.ratio = np.asarray(dm_eigenvalue / reg_eigenvalue) @@ -668,13 +717,30 @@ def __init__( self, alpha0_ratio=1.0, n_pw_iter=4, + random_seed: RandomSeed | None = None, seed: RandomSeed | None = None, **kwargs, ): super().__init__(**kwargs) self.alpha0_ratio = alpha0_ratio self.n_pw_iter = n_pw_iter - self.seed = seed + + # Deprecate seed argument + if seed is not None: + if random_seed is not None: + raise TypeError( + "Cannot pass both 'random_seed' and 'seed'." + "'seed' has been deprecated and will be removed in " + " SimPEG v0.24.0, please use 'random_seed' instead.", + ) + warnings.warn( + "'seed' has been deprecated and will be removed in " + " SimPEG v0.24.0, please use 'random_seed' instead.", + FutureWarning, + stacklevel=2, + ) + random_seed = seed + self.random_seed = random_seed @property def alpha0_ratio(self): @@ -707,17 +773,17 @@ def n_pw_iter(self, value): self._n_pw_iter = validate_integer("n_pw_iter", value, min_val=1) @property - def seed(self): + def random_seed(self): """Random seed to initialize with. Returns ------- int, numpy.random.Generator or None """ - return self._seed + return self._random_seed - @seed.setter - def seed(self, value): + @random_seed.setter + def random_seed(self, value): try: np.random.default_rng(value) except TypeError as err: @@ -726,11 +792,20 @@ def seed(self, value): f"a {type(value).__name__}" ) raise TypeError(msg) from err - self._seed = value + self._random_seed = value + + seed = deprecate_property( + random_seed, + "seed", + "random_seed", + removal_version="0.24.0", + future_warn=True, + error=False, + ) def initialize(self): """""" - rng = np.random.default_rng(seed=self.seed) + rng = np.random.default_rng(seed=self.random_seed) smoothness = [] smallness = [] @@ -765,7 +840,7 @@ def initialize(self): smallness[0], self.invProb.model, n_pw_iter=self.n_pw_iter, - seed=rng, + random_seed=rng, ) self.alpha0_ratio = self.alpha0_ratio * np.ones(len(smoothness)) @@ -781,7 +856,7 @@ def initialize(self): obj, self.invProb.model, n_pw_iter=self.n_pw_iter, - seed=rng, + random_seed=rng, ) ratio = smallness_eigenvalue / smooth_i_eigenvalue @@ -807,13 +882,30 @@ def __init__( self, chi0_ratio=None, n_pw_iter=4, + random_seed: RandomSeed | None = None, seed: RandomSeed | None = None, **kwargs, ): super().__init__(**kwargs) self.chi0_ratio = chi0_ratio self.n_pw_iter = n_pw_iter - self.seed = seed + + # Deprecate seed argument + if seed is not None: + if random_seed is not None: + raise TypeError( + "Cannot pass both 'random_seed' and 'seed'." + "'seed' has been deprecated and will be removed in " + " SimPEG v0.24.0, please use 'random_seed' instead.", + ) + warnings.warn( + "'seed' has been deprecated and will be removed in " + " SimPEG v0.24.0, please use 'random_seed' instead.", + FutureWarning, + stacklevel=2, + ) + random_seed = seed + self.random_seed = random_seed @property def chi0_ratio(self): @@ -846,17 +938,17 @@ def n_pw_iter(self, value): self._n_pw_iter = validate_integer("n_pw_iter", value, min_val=1) @property - def seed(self): + def random_seed(self): """Random seed to initialize with Returns ------- int, numpy.random.Generator or None """ - return self._seed + return self._random_seed - @seed.setter - def seed(self, value): + @random_seed.setter + def random_seed(self, value): try: np.random.default_rng(value) except TypeError as err: @@ -865,11 +957,20 @@ def seed(self, value): f"a {type(value).__name__}" ) raise TypeError(msg) from err - self._seed = value + self._random_seed = value + + seed = deprecate_property( + random_seed, + "seed", + "random_seed", + removal_version="0.24.0", + future_warn=True, + error=False, + ) def initialize(self): """""" - rng = np.random.default_rng(seed=self.seed) + rng = np.random.default_rng(seed=self.random_seed) if self.verbose: print("Calculating the scaling parameter.") @@ -892,7 +993,9 @@ def initialize(self): dm_eigenvalue_list = [] for dm in self.dmisfit.objfcts: - dm_eigenvalue_list += [eigenvalue_by_power_iteration(dm, m, seed=rng)] + dm_eigenvalue_list += [ + eigenvalue_by_power_iteration(dm, m, random_seed=rng) + ] self.chi0 = self.chi0_ratio / np.r_[dm_eigenvalue_list] self.chi0 = self.chi0 / np.sum(self.chi0) diff --git a/simpeg/directives/sim_directives.py b/simpeg/directives/sim_directives.py index 480cda76ee..f40b828c7d 100644 --- a/simpeg/directives/sim_directives.py +++ b/simpeg/directives/sim_directives.py @@ -270,7 +270,7 @@ def initialize(self): dmis, m, n_pw_iter=self.n_pw_iter, - seed=rng, + random_seed=rng, ) ) @@ -279,7 +279,7 @@ def initialize(self): reg, m, n_pw_iter=self.n_pw_iter, - seed=rng, + random_seed=rng, ) ) diff --git a/simpeg/utils/mat_utils.py b/simpeg/utils/mat_utils.py index ee99c4c73d..e4c662c05d 100644 --- a/simpeg/utils/mat_utils.py +++ b/simpeg/utils/mat_utils.py @@ -1,3 +1,4 @@ +import warnings import numpy as np from .code_utils import deprecate_function from ..typing import RandomSeed @@ -134,6 +135,7 @@ def eigenvalue_by_power_iteration( model, n_pw_iter=4, fields_list=None, + random_seed: RandomSeed | None = None, seed: RandomSeed | None = None, ): r"""Estimate largest eigenvalue in absolute value using power iteration. @@ -155,10 +157,16 @@ def eigenvalue_by_power_iteration( they will be evaluated within the function. If combo_objfct mixs data misfit and regularization terms, the list should contains simpeg.fields for the data misfit terms and None for the regularization term. - seed : None or :class:`~simpeg.typing.RandomSeed`, optional + random_seed : None or :class:`~simpeg.typing.RandomSeed`, optional Random seed for the initial random guess of eigenvector. It can either be an int, a predefined Numpy random number generator, or any valid input to ``numpy.random.default_rng``. + seed : None or :class:`~simpeg.typing.RandomSeed`, optional + + .. deprecated:: 0.23.0 + + Argument ``seed`` is deprecated in favor of ``random_seed`` and will + be removed in SimPEG v0.24.0. Returns ------- @@ -183,7 +191,22 @@ def eigenvalue_by_power_iteration( selected from a uniform distribution. """ - rng = np.random.default_rng(seed=seed) + # Deprecate seed argument + if seed is not None: + if random_seed is not None: + raise TypeError( + "Cannot pass both 'random_seed' and 'seed'." + "'seed' has been deprecated and will be removed in " + " SimPEG v0.24.0, please use 'random_seed' instead.", + ) + warnings.warn( + "'seed' has been deprecated and will be removed in " + " SimPEG v0.24.0, please use 'random_seed' instead.", + FutureWarning, + stacklevel=2, + ) + random_seed = seed + rng = np.random.default_rng(seed=random_seed) # Initial guess for eigen-vector x0 = rng.random(size=model.shape) diff --git a/tests/base/test_directives.py b/tests/base/test_directives.py index 132cef2e5a..65fbb0f7f2 100644 --- a/tests/base/test_directives.py +++ b/tests/base/test_directives.py @@ -517,9 +517,9 @@ def test_normalization_method_setter_invalid(self, normalization_method): d_temp.normalization_method = normalization_method -class TestSeedProperty: +class TestRandomSeedProperty: """ - Test ``seed`` setter methods of directives. + Test ``random_seed`` setter methods of directives. """ directive_classes = ( @@ -531,22 +531,22 @@ class TestSeedProperty: @pytest.mark.parametrize("directive_class", directive_classes) @pytest.mark.parametrize( - "seed", + "random_seed", (42, np.random.default_rng(seed=1), np.array([1, 2])), ids=("int", "rng", "array"), ) - def test_valid_seed(self, directive_class, seed): + def test_valid_seed(self, directive_class, random_seed): "Test if seed setter works as expected on valid seed arguments." - directive = directive_class(seed=seed) - assert directive.seed is seed + directive = directive_class(random_seed=random_seed) + assert directive.random_seed is random_seed @pytest.mark.parametrize("directive_class", directive_classes) - @pytest.mark.parametrize("seed", (42.1, np.array([1.0, 2.0]))) - def test_invalid_seed(self, directive_class, seed): + @pytest.mark.parametrize("random_seed", (42.1, np.array([1.0, 2.0]))) + def test_invalid_seed(self, directive_class, random_seed): "Test if seed setter works as expected on valid seed arguments." msg = "Unable to initialize the random number generator with " with pytest.raises(TypeError, match=msg): - directive_class(seed=seed) + directive_class(random_seed=random_seed) class TestBetaEstimatorArguments: @@ -559,23 +559,94 @@ def test_beta_estimate_by_eig(self): """Test on directives.BetaEstimate_ByEig.""" beta0_ratio = 3.0 n_pw_iter = 3 - seed = 42 + random_seed = 42 directive = directives.BetaEstimate_ByEig( - beta0_ratio=beta0_ratio, n_pw_iter=n_pw_iter, seed=seed + beta0_ratio=beta0_ratio, n_pw_iter=n_pw_iter, random_seed=random_seed ) assert directive.beta0_ratio == beta0_ratio assert directive.n_pw_iter == n_pw_iter - assert directive.seed == seed + assert directive.random_seed == random_seed def test_beta_estimate_max_derivative(self): """Test on directives.BetaEstimateMaxDerivative.""" beta0_ratio = 3.0 - seed = 42 + random_seed = 42 directive = directives.BetaEstimateMaxDerivative( - beta0_ratio=beta0_ratio, seed=seed + beta0_ratio=beta0_ratio, random_seed=random_seed ) assert directive.beta0_ratio == beta0_ratio - assert directive.seed == seed + assert directive.random_seed == random_seed + + +class TestDeprecateSeedProperty: + """ + Test deprecation of seed property. + """ + + CLASSES = ( + directives.AlphasSmoothEstimate_ByEig, + directives.BetaEstimate_ByEig, + directives.BetaEstimateMaxDerivative, + directives.ScalingMultipleDataMisfits_ByEig, + ) + + def get_message_duplicated_error(self, old_name, new_name, version="v0.24.0"): + msg = ( + f"Cannot pass both '{new_name}' and '{old_name}'." + f"'{old_name}' has been deprecated and will be removed in " + f" SimPEG {version}, please use '{new_name}' instead." + ) + return msg + + def get_message_deprecated_warning(self, old_name, new_name, version="v0.24.0"): + msg = ( + f"'{old_name}' has been deprecated and will be removed in " + f" SimPEG {version}, please use '{new_name}' instead." + ) + return msg + + @pytest.mark.parametrize("directive", CLASSES) + def test_warning_argument(self, directive): + """ + Test if warning is raised after passing ``seed`` to the constructor. + """ + msg = self.get_message_deprecated_warning("seed", "random_seed") + seed = 42135 + with pytest.warns(FutureWarning, match=msg): + directive_instance = directive(seed=42135) + assert directive_instance.random_seed == seed + + @pytest.mark.parametrize("directive", CLASSES) + def test_error_duplicated_argument(self, directive): + """ + Test error after passing ``seed`` and ``random_seed`` to the constructor. + """ + msg = self.get_message_duplicated_error("seed", "random_seed") + with pytest.raises(TypeError, match=msg): + directive(seed=42, random_seed=42) + + @pytest.mark.parametrize("directive", CLASSES) + def test_warning_accessing_property(self, directive): + """ + Test warning when trying to access the ``seed`` property. + """ + directive_obj = directive(random_seed=42) + msg = "seed has been deprecated, please use random_seed" + with pytest.warns(FutureWarning, match=msg): + seed = directive_obj.seed + np.testing.assert_allclose(seed, directive_obj.random_seed) + + @pytest.mark.parametrize("directive", CLASSES) + def test_warning_setter(self, directive): + """ + Test warning when trying to set the ``seed`` property. + """ + directive_obj = directive(random_seed=42) + msg = "seed has been deprecated, please use random_seed" + new_seed = 35 + with pytest.warns(FutureWarning, match=msg): + directive_obj.seed = new_seed + np.testing.assert_allclose(directive_obj.random_seed, new_seed) class TestUpdateIRLS: diff --git a/tests/utils/test_mat_utils.py b/tests/utils/test_mat_utils.py index 9aecd7eee9..c194cfc61b 100644 --- a/tests/utils/test_mat_utils.py +++ b/tests/utils/test_mat_utils.py @@ -1,7 +1,9 @@ +import pytest import unittest import numpy as np from scipy.sparse.linalg import eigsh from discretize import TensorMesh +from simpeg.objective_function import BaseObjectiveFunction from simpeg import simulation, data_misfit from simpeg.maps import IdentityMap from simpeg.regularization import WeightedLeastSquares @@ -80,7 +82,7 @@ def test_dm_eigenvalue_by_power_iteration(self): field = self.dmis.simulation.fields(self.true_model) max_eigenvalue_numpy, _ = eigsh(dmis_matrix, k=1) max_eigenvalue_directive = eigenvalue_by_power_iteration( - self.dmis, self.true_model, fields_list=field, n_pw_iter=30, seed=42 + self.dmis, self.true_model, fields_list=field, n_pw_iter=30, random_seed=42 ) passed = np.isclose(max_eigenvalue_numpy, max_eigenvalue_directive, rtol=1e-2) self.assertTrue(passed, True) @@ -93,7 +95,7 @@ def test_dm_eigenvalue_by_power_iteration(self): dmiscombo_matrix = 2 * self.G.T.dot(WtW.dot(self.G)) max_eigenvalue_numpy, _ = eigsh(dmiscombo_matrix, k=1) max_eigenvalue_directive = eigenvalue_by_power_iteration( - self.dmiscombo, self.true_model, n_pw_iter=30, seed=42 + self.dmiscombo, self.true_model, n_pw_iter=30, random_seed=42 ) passed = np.isclose(max_eigenvalue_numpy, max_eigenvalue_directive, rtol=1e-2) self.assertTrue(passed, True) @@ -103,7 +105,7 @@ def test_reg_eigenvalue_by_power_iteration(self): reg_maxtrix = self.reg.deriv2(self.true_model) max_eigenvalue_numpy, _ = eigsh(reg_maxtrix, k=1) max_eigenvalue_directive = eigenvalue_by_power_iteration( - self.reg, self.true_model, n_pw_iter=100, seed=42 + self.reg, self.true_model, n_pw_iter=100, random_seed=42 ) passed = np.isclose(max_eigenvalue_numpy, max_eigenvalue_directive, rtol=1e-2) self.assertTrue(passed, True) @@ -115,12 +117,72 @@ def test_combo_eigenvalue_by_power_iteration(self): combo_matrix = dmis_matrix + self.beta * reg_maxtrix max_eigenvalue_numpy, _ = eigsh(combo_matrix, k=1) max_eigenvalue_directive = eigenvalue_by_power_iteration( - self.mixcombo, self.true_model, n_pw_iter=100, seed=42 + self.mixcombo, self.true_model, n_pw_iter=100, random_seed=42 ) passed = np.isclose(max_eigenvalue_numpy, max_eigenvalue_directive, rtol=1e-2) self.assertTrue(passed, True) print("Eigenvalue Utils for a mixed ComboObjectiveFunction is validated.") +class TestDeprecatedSeed: + """Test deprecation of ``seed`` argument.""" + + @pytest.fixture + def mock_objfun(self): + """ + Mock objective function class as child of ``BaseObjectiveFunction`` + """ + + class MockObjectiveFunction(BaseObjectiveFunction): + + def deriv2(self, m, v=None, **kwargs): + return np.ones(self.nP) + + return MockObjectiveFunction + + def get_message_duplicated_error(self, old_name, new_name, version="v0.24.0"): + msg = ( + f"Cannot pass both '{new_name}' and '{old_name}'." + f"'{old_name}' has been deprecated and will be removed in " + f" SimPEG {version}, please use '{new_name}' instead." + ) + return msg + + def get_message_deprecated_warning(self, old_name, new_name, version="v0.24.0"): + msg = ( + f"'{old_name}' has been deprecated and will be removed in " + f" SimPEG {version}, please use '{new_name}' instead." + ) + return msg + + def test_warning_argument(self, mock_objfun): + """ + Test if warning is raised after passing ``seed``. + """ + msg = self.get_message_deprecated_warning("seed", "random_seed") + n_params = 5 + combo = mock_objfun(nP=n_params) + 3.0 * mock_objfun(nP=n_params) + model = np.ones(n_params) + with pytest.warns(FutureWarning, match=msg): + result_seed = eigenvalue_by_power_iteration( + combo_objfct=combo, model=model, seed=42 + ) + # Ensure that using `seed` and `random_seed` generate the same output + result_random_seed = eigenvalue_by_power_iteration( + combo_objfct=combo, model=model, random_seed=42 + ) + np.testing.assert_allclose(result_seed, result_random_seed) + + def test_error_duplicated_argument(self): + """ + Test error after passing ``seed`` and ``random_seed``. + """ + msg = self.get_message_duplicated_error("seed", "random_seed") + with pytest.raises(TypeError, match=msg): + eigenvalue_by_power_iteration( + combo_objfct=None, model=None, random_seed=42, seed=42 + ) + + if __name__ == "__main__": unittest.main() From 5b53faee8498663c83204c420c72e4c53cfd556f Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 10 Oct 2024 12:46:44 -0700 Subject: [PATCH 069/194] Minor fixes to magnetic examples (#1547) Rename title of one of the magnetic examples to avoid repeated name and to make it clearer what the example is about. Minor fixes to the rst syntax. --- .../03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py | 9 ++++----- .../03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py | 6 +++--- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py b/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py index 5727b29e4a..9987eceb44 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py @@ -1,8 +1,8 @@ """ -Magnetic inversion on a TreeMesh -================================ +Magnetic inversion on a TreeMesh with remanence +=============================================== -In this example, we demonstrate the use of a Magnetic Vector Inverison +In this example, we demonstrate the use of a Magnetic Vector Inversion on 3D TreeMesh for the inversion of magnetics affected by remanence. The mesh is auto-generated based on the position of the observation locations and topography. @@ -11,8 +11,7 @@ Cartesian coordinate system, and second for a compact model using the Spherical formulation. -The inverse problem uses the :class:'simpeg.regularization.Sparse' -that +The inverse problem uses the :class:`simpeg.regularization.Sparse`. """ diff --git a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py index f90258ab42..993acee2e8 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py @@ -2,11 +2,11 @@ Magnetic inversion on a TreeMesh ================================ -In this example, we demonstrate the use of a Magnetic Vector Inverison +In this example, we demonstrate the use of a Magnetic Vector Inversion on 3D TreeMesh for the inversion of magnetic data. -The inverse problem uses the :class:'simpeg.regularization.VectorAmplitude' -regularization borrowed from ... +The inverse problem uses the :class:`simpeg.regularization.VectorAmplitude` +regularization. """ From 56bebdb103aaaaecb836aabac49eac3832db3608 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 10 Oct 2024 15:01:29 -0700 Subject: [PATCH 070/194] Support magnetic gradiometry using Choclo as engine (#1543) Allow to compute magnetic gradiometry components using Choclo as the engine in the magnetic integral simulation. Add the new magnetic gradiometry kernels to the list of supported components with Choclo as engine and ensure they are being tested. Constrain minimum version of Choclo to v0.3.0. --- .ci/environment_test.yml | 2 +- environment.yml | 2 +- pyproject.toml | 2 +- .../potential_fields/magnetics/simulation.py | 43 ++++++++++++- tests/pf/test_forward_Mag_Linear.py | 63 +++++++++---------- 5 files changed, 74 insertions(+), 38 deletions(-) diff --git a/.ci/environment_test.yml b/.ci/environment_test.yml index d26d956b01..a0be57a840 100644 --- a/.ci/environment_test.yml +++ b/.ci/environment_test.yml @@ -17,7 +17,7 @@ dependencies: - dask - zarr - fsspec>=0.3.3 - - choclo + - choclo>=0.3.0 - scooby - plotly - scikit-learn>=1.2 diff --git a/environment.yml b/environment.yml index 982337f40d..624a5e4584 100644 --- a/environment.yml +++ b/environment.yml @@ -20,7 +20,7 @@ dependencies: - dask - zarr - fsspec>=0.3.3 - - choclo + - choclo>=0.3.0 - scooby - plotly - scikit-learn>=1.2 diff --git a/pyproject.toml b/pyproject.toml index 577d2bad77..26ab96a679 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ Repository = 'http://github.com/simpeg/simpeg.git' [project.optional-dependencies] dask = ["dask", "zarr", "fsspec>=0.3.3"] -choclo = ["choclo"] +choclo = ["choclo>=0.3.0"] reporting = ["scooby"] plotting = ["plotly"] sklearn = ["scikit-learn>=1.2"] diff --git a/simpeg/potential_fields/magnetics/simulation.py b/simpeg/potential_fields/magnetics/simulation.py index a695cd280e..91ec72a7e7 100644 --- a/simpeg/potential_fields/magnetics/simulation.py +++ b/simpeg/potential_fields/magnetics/simulation.py @@ -34,11 +34,52 @@ ) if choclo is not None: - CHOCLO_SUPPORTED_COMPONENTS = {"tmi", "bx", "by", "bz"} + CHOCLO_SUPPORTED_COMPONENTS = { + "tmi", + "bx", + "by", + "bz", + "bxx", + "byy", + "bzz", + "bxy", + "bxz", + "byz", + } CHOCLO_KERNELS = { "bx": (choclo.prism.kernel_ee, choclo.prism.kernel_en, choclo.prism.kernel_eu), "by": (choclo.prism.kernel_en, choclo.prism.kernel_nn, choclo.prism.kernel_nu), "bz": (choclo.prism.kernel_eu, choclo.prism.kernel_nu, choclo.prism.kernel_uu), + "bxx": ( + choclo.prism.kernel_eee, + choclo.prism.kernel_een, + choclo.prism.kernel_eeu, + ), + "byy": ( + choclo.prism.kernel_enn, + choclo.prism.kernel_nnn, + choclo.prism.kernel_nnu, + ), + "bzz": ( + choclo.prism.kernel_euu, + choclo.prism.kernel_nuu, + choclo.prism.kernel_uuu, + ), + "bxy": ( + choclo.prism.kernel_een, + choclo.prism.kernel_enn, + choclo.prism.kernel_enu, + ), + "bxz": ( + choclo.prism.kernel_eeu, + choclo.prism.kernel_enu, + choclo.prism.kernel_euu, + ), + "byz": ( + choclo.prism.kernel_enu, + choclo.prism.kernel_nnu, + choclo.prism.kernel_nuu, + ), } diff --git a/tests/pf/test_forward_Mag_Linear.py b/tests/pf/test_forward_Mag_Linear.py index b28d847a60..b153ba4a1c 100644 --- a/tests/pf/test_forward_Mag_Linear.py +++ b/tests/pf/test_forward_Mag_Linear.py @@ -317,39 +317,34 @@ def test_magnetic_gradiometry_w_susceptibility( engine=engine, **parallel_kwargs, ) - if engine == "choclo": - # gradient simulation not implemented for choclo yet - with pytest.raises(NotImplementedError): - data = sim.dpred(model_reduced) - else: - data = sim.dpred(model_reduced) - d_xx = data[0::6] - d_xy = data[1::6] - d_xz = data[2::6] - d_yy = data[3::6] - d_yz = data[4::6] - d_zz = data[5::6] - - # Compute analytical response from magnetic prism - block1, block2 = two_blocks - prism_1 = MagneticPrism(block1[:, 0], block1[:, 1], chi1 * b0 / mu_0) - prism_2 = MagneticPrism(block2[:, 0], block2[:, 1], -chi1 * b0 / mu_0) - prism_3 = MagneticPrism(block2[:, 0], block2[:, 1], chi2 * b0 / mu_0) - - d = ( - prism_1.magnetic_field_gradient(receiver_locations) - + prism_2.magnetic_field_gradient(receiver_locations) - + prism_3.magnetic_field_gradient(receiver_locations) - ) * mu_0 - - # Check results - rtol, atol = 5e-7, 1e-6 - np.testing.assert_allclose(d_xx, d[..., 0, 0], rtol=rtol, atol=atol) - np.testing.assert_allclose(d_xy, d[..., 0, 1], rtol=rtol, atol=atol) - np.testing.assert_allclose(d_xz, d[..., 0, 2], rtol=rtol, atol=atol) - np.testing.assert_allclose(d_yy, d[..., 1, 1], rtol=rtol, atol=atol) - np.testing.assert_allclose(d_yz, d[..., 1, 2], rtol=rtol, atol=atol) - np.testing.assert_allclose(d_zz, d[..., 2, 2], rtol=rtol, atol=atol) + data = sim.dpred(model_reduced) + d_xx = data[0::6] + d_xy = data[1::6] + d_xz = data[2::6] + d_yy = data[3::6] + d_yz = data[4::6] + d_zz = data[5::6] + + # Compute analytical response from magnetic prism + block1, block2 = two_blocks + prism_1 = MagneticPrism(block1[:, 0], block1[:, 1], chi1 * b0 / mu_0) + prism_2 = MagneticPrism(block2[:, 0], block2[:, 1], -chi1 * b0 / mu_0) + prism_3 = MagneticPrism(block2[:, 0], block2[:, 1], chi2 * b0 / mu_0) + + d = ( + prism_1.magnetic_field_gradient(receiver_locations) + + prism_2.magnetic_field_gradient(receiver_locations) + + prism_3.magnetic_field_gradient(receiver_locations) + ) * mu_0 + + # Check results + rtol, atol = 5e-7, 1e-6 + np.testing.assert_allclose(d_xx, d[..., 0, 0], rtol=rtol, atol=atol) + np.testing.assert_allclose(d_xy, d[..., 0, 1], rtol=rtol, atol=atol) + np.testing.assert_allclose(d_xz, d[..., 0, 2], rtol=rtol, atol=atol) + np.testing.assert_allclose(d_yy, d[..., 1, 1], rtol=rtol, atol=atol) + np.testing.assert_allclose(d_yz, d[..., 1, 2], rtol=rtol, atol=atol) + np.testing.assert_allclose(d_zz, d[..., 2, 2], rtol=rtol, atol=atol) @pytest.mark.parametrize( "engine, parallel_kwargs", @@ -618,7 +613,7 @@ def test_sensitivities_on_disk(self, mag_mesh, receiver_locations, tmp_path): assert sensitivities_path.is_file() assert type(simulation.G) is np.memmap - def test_sensitivities_on_ram(self, mag_mesh, receiver_locations, tmp_path): + def test_sensitivities_on_ram(self, mag_mesh, receiver_locations): """ Test if sensitivity matrix is correctly being allocated in memory when asked """ From 56ce895d4db46834d331602b6e2f2fa42b844620 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 11 Oct 2024 08:24:28 -0700 Subject: [PATCH 071/194] Update usage of `random_seed` in one example (#1549) Replace usage of `seed` argument for `random_seed` in one more example. Part of #1461 --- examples/20-published/plot_heagyetal2017_cyl_inversions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/20-published/plot_heagyetal2017_cyl_inversions.py b/examples/20-published/plot_heagyetal2017_cyl_inversions.py index e41a05f52c..25ee925f32 100644 --- a/examples/20-published/plot_heagyetal2017_cyl_inversions.py +++ b/examples/20-published/plot_heagyetal2017_cyl_inversions.py @@ -109,7 +109,7 @@ def run(plotIt=True, saveFig=False): # Inversion Directives beta = directives.BetaSchedule(coolingFactor=4, coolingRate=3) - betaest = directives.BetaEstimate_ByEig(beta0_ratio=1.0, seed=518936) + betaest = directives.BetaEstimate_ByEig(beta0_ratio=1.0, random_seed=518936) target = directives.TargetMisfit() directiveList = [beta, betaest, target] From 5b1bd39a647fddb25496ce7048c1aacf53089041 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 11 Oct 2024 14:36:05 -0700 Subject: [PATCH 072/194] Replace `seed` for `random_seed` in `model_builder` (#1548) Replace `seed` for `random_seed` in `model_builder.create_random_model.` Test the deprecation warnings and errors. Update examples and code with new argument. Closes #1461 --- examples/01-maps/plot_mesh2mesh.py | 2 +- .../static/utils/static_utils.py | 12 +++- simpeg/utils/model_builder.py | 37 ++++++++++-- tests/utils/test_model_builder.py | 60 +++++++++++++++++++ 4 files changed, 104 insertions(+), 7 deletions(-) create mode 100644 tests/utils/test_model_builder.py diff --git a/examples/01-maps/plot_mesh2mesh.py b/examples/01-maps/plot_mesh2mesh.py index b5621e5ae2..1e76d511ac 100644 --- a/examples/01-maps/plot_mesh2mesh.py +++ b/examples/01-maps/plot_mesh2mesh.py @@ -15,7 +15,7 @@ def run(plotIt=True): h1 = utils.unpack_widths([(6, 7, -1.5), (6, 10), (6, 7, 1.5)]) h1 = h1 / h1.sum() M2 = discretize.TensorMesh([h1, h1]) - V = utils.model_builder.create_random_model(M.vnC, seed=79, its=50) + V = utils.model_builder.create_random_model(M.vnC, random_seed=79, its=50) v = utils.mkvc(V) modh = maps.Mesh2Mesh([M, M2]) modH = maps.Mesh2Mesh([M2, M]) diff --git a/simpeg/electromagnetics/static/utils/static_utils.py b/simpeg/electromagnetics/static/utils/static_utils.py index 7d9d43146c..23c5a01f5a 100644 --- a/simpeg/electromagnetics/static/utils/static_utils.py +++ b/simpeg/electromagnetics/static/utils/static_utils.py @@ -1691,13 +1691,21 @@ def genTopography(mesh, zmin, zmax, seed=None, its=100, anisotropy=None): [mesh.h[0], mesh.h[1]], x0=[mesh.x0[0], mesh.x0[1]] ) out = model_builder.create_random_model( - mesh.vnC[:2], bounds=[zmin, zmax], its=its, seed=seed, anisotropy=anisotropy + mesh.vnC[:2], + bounds=[zmin, zmax], + its=its, + random_seed=seed, + anisotropy=anisotropy, ) return out, mesh2D elif mesh.dim == 2: mesh1D = discretize.TensorMesh([mesh.h[0]], x0=[mesh.x0[0]]) out = model_builder.create_random_model( - mesh.vnC[:1], bounds=[zmin, zmax], its=its, seed=seed, anisotropy=anisotropy + mesh.vnC[:1], + bounds=[zmin, zmax], + its=its, + random_seed=seed, + anisotropy=anisotropy, ) return out, mesh1D else: diff --git a/simpeg/utils/model_builder.py b/simpeg/utils/model_builder.py index 4298748973..285a440a8a 100644 --- a/simpeg/utils/model_builder.py +++ b/simpeg/utils/model_builder.py @@ -1,3 +1,4 @@ +import warnings import numpy as np import scipy.ndimage as ndi import scipy.sparse as sp @@ -418,10 +419,11 @@ def create_layers_model(cell_centers, layer_tops, layer_values): def create_random_model( shape, - seed: RandomSeed | None = 1000, + random_seed: RandomSeed | None = 1000, anisotropy=None, its=100, bounds=None, + **kwargs, ): """ Create random model by convolving a kernel with a uniformly distributed random model. @@ -429,8 +431,9 @@ def create_random_model( Parameters ---------- shape : int or tuple of int - Shape of the model. Can define a vector of size (n_cells) or define the dimensions of a tensor - seed : None or :class:`~simpeg.typing.RandomSeed`, optional + Shape of the model. Can define a vector of size (n_cells) or define the + dimensions of a tensor. + random_seed : None or :class:`~simpeg.typing.RandomSeed`, optional Random seed for random uniform model that is convolved with the kernel. It can either be an int, a predefined Numpy random number generator, or any valid input to ``numpy.random.default_rng``. @@ -440,6 +443,12 @@ def create_random_model( Number of smoothing iterations after convolutions bounds : list of float Lower and upper bound for the model values + seed : None or :class:`~simpeg.typing.RandomSeed`, optional + + .. deprecated:: 0.23.0 + + Argument ``seed`` is deprecated in favor of ``random_seed`` and will + be removed in SimPEG v0.24.0. Returns ------- @@ -458,13 +467,33 @@ def create_random_model( >>> plt.show() """ + # Deprecate seed argument + if "seed" in kwargs: + if random_seed != 1000: + raise TypeError( + "Cannot pass both 'random_seed' and 'seed'." + "'seed' has been deprecated and will be removed in " + " SimPEG v0.24.0, please use 'random_seed' instead.", + ) + warnings.warn( + "'seed' has been deprecated and will be removed in " + " SimPEG v0.24.0, please use 'random_seed' instead.", + FutureWarning, + stacklevel=2, + ) + random_seed = kwargs.pop("seed") + if kwargs: + args = ", ".join([f"'{key}'" for key in kwargs]) + raise TypeError(f"Invalid arguments {args}.") + # --- + if bounds is None: bounds = [0, 1] if isinstance(shape, int): shape = (shape,) # make it a tuple for consistency - rng = np.random.default_rng(seed=seed) + rng = np.random.default_rng(seed=random_seed) mr = rng.random(size=shape) if anisotropy is None: if len(shape) == 1: diff --git a/tests/utils/test_model_builder.py b/tests/utils/test_model_builder.py new file mode 100644 index 0000000000..d35d5901dc --- /dev/null +++ b/tests/utils/test_model_builder.py @@ -0,0 +1,60 @@ +""" +Test functions in model_builder. +""" + +import pytest +import numpy as np +from simpeg.utils.model_builder import create_random_model + + +class TestDeprecateSeedProperty: + """ + Test deprecation of seed property. + """ + + def get_message_duplicated_error(self, old_name, new_name, version="v0.24.0"): + msg = ( + f"Cannot pass both '{new_name}' and '{old_name}'." + f"'{old_name}' has been deprecated and will be removed in " + f" SimPEG {version}, please use '{new_name}' instead." + ) + return msg + + def get_message_deprecated_warning(self, old_name, new_name, version="v0.24.0"): + msg = ( + f"'{old_name}' has been deprecated and will be removed in " + f" SimPEG {version}, please use '{new_name}' instead." + ) + return msg + + @pytest.fixture + def shape(self): + return (5, 5) + + def test_warning_argument(self, shape): + """ + Test if warning is raised after passing ``seed`` as argument. + """ + msg = self.get_message_deprecated_warning("seed", "random_seed") + seed = 42135 + with pytest.warns(FutureWarning, match=msg): + result = create_random_model(shape, seed=seed) + np.testing.assert_allclose(result, create_random_model(shape, random_seed=seed)) + + def test_error_duplicated_argument(self, shape): + """ + Test error after passing ``seed`` and ``random_seed`` as arguments. + """ + msg = self.get_message_duplicated_error("seed", "random_seed") + with pytest.raises(TypeError, match=msg): + create_random_model(shape, seed=42, random_seed=42) + + def test_error_invalid_kwarg(self, shape): + """ + Test error after passing invalid kwargs to the function. + """ + kwargs = {"foo": 1, "bar": 2} + msg = "Invalid arguments 'foo', 'bar'." + with pytest.raises(TypeError, match=msg): + with pytest.warns(FutureWarning): + create_random_model(shape, seed=10, **kwargs) From ca6740dc34e4c474a5d7ddc2985435f41249b1c8 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 17 Oct 2024 13:56:02 -0700 Subject: [PATCH 073/194] Replace old args for `active_cells` in EM static functions (#1550) Replace `ind_active` for `active_cells` in `drapeTopotoLoc`, `drape_electrodes_on_topography` and `spectral_ip_mappings`. Test warnings and errors for the deprecations. Update usage of `active_cells`. Closes #1121 --- .../static/resistivity/survey.py | 25 ++++++- .../spectral_induced_polarization/run.py | 35 +++++++--- .../static/utils/static_utils.py | 40 ++++++++--- tests/em/static/test_dc_survey.py | 24 +++++++ tests/em/static/test_spectral_ip_mappings.py | 63 ++++++++++++++++++ tests/em/static/test_static_utils.py | 66 +++++++++++++++++++ 6 files changed, 232 insertions(+), 21 deletions(-) create mode 100644 tests/em/static/test_spectral_ip_mappings.py create mode 100644 tests/em/static/test_static_utils.py diff --git a/simpeg/electromagnetics/static/resistivity/survey.py b/simpeg/electromagnetics/static/resistivity/survey.py index 411f9ff7a6..1da25060d4 100644 --- a/simpeg/electromagnetics/static/resistivity/survey.py +++ b/simpeg/electromagnetics/static/resistivity/survey.py @@ -285,7 +285,13 @@ def getABMN_locations(self): ) def drape_electrodes_on_topography( - self, mesh, ind_active, option="top", topography=None, force=False + self, + mesh, + active_cells, + option="top", + topography=None, + force=False, + ind_active=None, ): """Shift electrode locations to discrete surface topography. @@ -293,7 +299,7 @@ def drape_electrodes_on_topography( ---------- mesh : discretize.TensorMesh or discretize.TreeMesh The mesh on which the discretized fields are computed - ind_active : numpy.ndarray of int or bool + active_cells : numpy.ndarray of int or bool Active topography cells option :{"top", "center"} Define topography at tops of cells or cell centers. @@ -301,8 +307,21 @@ def drape_electrodes_on_topography( Surface topography force : bool, default = ``False`` If ``True`` force electrodes to surface even if borehole + ind_active : numpy.ndarray of int or bool, optional + + .. deprecated:: 0.23.0 + + Argument ``ind_active`` is deprecated in favor of ``active_cells`` + and will be removed in SimPEG v0.24.0. """ + # Deprecate ind_active argument + if ind_active is not None: + raise TypeError( + "'ind_active' has been deprecated and will be removed in " + " SimPEG v0.24.0, please use 'active_cells' instead." + ) + if self.survey_geometry == "surface": loc_a = self.locations_a[:, :2] loc_b = self.locations_b[:, :2] @@ -316,7 +335,7 @@ def drape_electrodes_on_topography( inv_m, inv_n = inv[: len(loc_m)], inv[len(loc_m) :] electrodes_shifted = drapeTopotoLoc( - mesh, unique_electrodes, ind_active=ind_active, option=option + mesh, unique_electrodes, active_cells=active_cells, option=option ) a_shifted = electrodes_shifted[inv_a] b_shifted = electrodes_shifted[inv_b] diff --git a/simpeg/electromagnetics/static/spectral_induced_polarization/run.py b/simpeg/electromagnetics/static/spectral_induced_polarization/run.py index 36bedcbb40..8a13cd0b88 100644 --- a/simpeg/electromagnetics/static/spectral_induced_polarization/run.py +++ b/simpeg/electromagnetics/static/spectral_induced_polarization/run.py @@ -1,3 +1,4 @@ +import warnings import numpy as np from simpeg import ( maps, @@ -12,13 +13,14 @@ def spectral_ip_mappings( mesh, - indActive=None, + active_cells=None, inactive_eta=1e-4, inactive_tau=1e-4, inactive_c=1e-4, is_log_eta=True, is_log_tau=True, is_log_c=True, + indActive=None, ): """ Generates Mappings for Spectral Induced Polarization Simulation. @@ -36,22 +38,39 @@ def spectral_ip_mappings( TODO: Illustrate input and output variables """ - - if indActive is None: - indActive = np.ones(mesh.nC, dtype=bool) + # Deprecate indActive argument + if indActive is not None: + if active_cells is not None: + raise TypeError( + "Cannot pass both 'active_cells' and 'indActive'." + "'indActive' has been deprecated and will be removed in " + " SimPEG v0.24.0, please use 'active_cells' instead.", + ) + warnings.warn( + "'indActive' has been deprecated and will be removed in " + " SimPEG v0.24.0, please use 'active_cells' instead.", + FutureWarning, + stacklevel=2, + ) + active_cells = indActive + + if active_cells is None: + active_cells = np.ones(mesh.nC, dtype=bool) actmap_eta = maps.InjectActiveCells( - mesh, active_cells=indActive, value_inactive=inactive_eta + mesh, active_cells=active_cells, value_inactive=inactive_eta ) actmap_tau = maps.InjectActiveCells( - mesh, active_cells=indActive, value_inactive=inactive_tau + mesh, active_cells=active_cells, value_inactive=inactive_tau ) actmap_c = maps.InjectActiveCells( - mesh, active_cells=indActive, value_inactive=inactive_c + mesh, active_cells=active_cells, value_inactive=inactive_c ) wires = maps.Wires( - ("eta", indActive.sum()), ("tau", indActive.sum()), ("c", indActive.sum()) + ("eta", active_cells.sum()), + ("tau", active_cells.sum()), + ("c", active_cells.sum()), ) if is_log_eta: diff --git a/simpeg/electromagnetics/static/utils/static_utils.py b/simpeg/electromagnetics/static/utils/static_utils.py index 23c5a01f5a..7a552a03bd 100644 --- a/simpeg/electromagnetics/static/utils/static_utils.py +++ b/simpeg/electromagnetics/static/utils/static_utils.py @@ -1597,7 +1597,9 @@ def gettopoCC(mesh, ind_active, option="top"): raise NotImplementedError(f"{type(mesh)} mesh is not supported.") -def drapeTopotoLoc(mesh, pts, ind_active=None, option="top", topo=None, **kwargs): +def drapeTopotoLoc( + mesh, pts, active_cells=None, option="top", topo=None, ind_active=None +): """Drape locations right below discretized surface topography This function projects the set of locations provided to the discrete @@ -1609,7 +1611,7 @@ def drapeTopotoLoc(mesh, pts, ind_active=None, option="top", topo=None, **kwargs A 2D tensor or tree mesh pts : (n, dim) numpy.ndarray The set of points being projected to the discretize surface topography - ind_active : numpy.ndarray of int or bool, optional + active_cells : numpy.ndarray of int or bool, optional Index array for all cells lying below the surface topography. Surface topography can be specified using the 'ind_active' or 'topo' input parameters. option : {"top", "center"} @@ -1618,10 +1620,28 @@ def drapeTopotoLoc(mesh, pts, ind_active=None, option="top", topo=None, **kwargs topo : (n, dim) numpy.ndarray Surface topography. Can be used if an active indices array cannot be provided for the input parameter 'ind_active' - """ + ind_active : numpy.ndarray of int or bool, optional + + .. deprecated:: 0.23.0 - if "actind" in kwargs: - ind_active = kwargs.pop("actind") + Argument ``ind_active`` is deprecated in favor of ``active_cells`` + and will be removed in SimPEG v0.24.0. + """ + # Deprecate ind_active argument + if ind_active is not None: + if active_cells is not None: + raise TypeError( + "Cannot pass both 'active_cells' and 'ind_active'." + "'ind_active' has been deprecated and will be removed in " + " SimPEG v0.24.0, please use 'active_cells' instead.", + ) + warnings.warn( + "'ind_active' has been deprecated and will be removed in " + " SimPEG v0.24.0, please use 'active_cells' instead.", + FutureWarning, + stacklevel=2, + ) + active_cells = ind_active if isinstance(mesh, discretize.CurvilinearMesh): raise ValueError("Curvilinear mesh is not supported.") @@ -1640,22 +1660,22 @@ def drapeTopotoLoc(mesh, pts, ind_active=None, option="top", topo=None, **kwargs else: raise ValueError("Unsupported mesh dimension") - if ind_active is None: - ind_active = discretize.utils.active_from_xyz(mesh, topo) + if active_cells is None: + active_cells = discretize.utils.active_from_xyz(mesh, topo) if mesh._meshType == "TENSOR": - meshtemp, topoCC = gettopoCC(mesh, ind_active, option=option) + meshtemp, topoCC = gettopoCC(mesh, active_cells, option=option) inds = meshtemp.closest_points_index(pts) topo = topoCC[inds] out = np.c_[pts, topo] elif mesh._meshType == "TREE": if mesh.dim == 3: - uniqXYlocs, topoCC = gettopoCC(mesh, ind_active, option=option) + uniqXYlocs, topoCC = gettopoCC(mesh, active_cells, option=option) inds = closestPointsGrid(uniqXYlocs, pts) out = np.c_[uniqXYlocs[inds, :], topoCC[inds]] else: - uniqXlocs, topoCC = gettopoCC(mesh, ind_active, option=option) + uniqXlocs, topoCC = gettopoCC(mesh, active_cells, option=option) inds = closestPointsGrid(uniqXlocs, pts, dim=1) out = np.c_[uniqXlocs[inds], topoCC[inds]] else: diff --git a/tests/em/static/test_dc_survey.py b/tests/em/static/test_dc_survey.py index 20d7f8619a..f7b88754db 100644 --- a/tests/em/static/test_dc_survey.py +++ b/tests/em/static/test_dc_survey.py @@ -3,7 +3,9 @@ """ import pytest +import numpy as np +from discretize import TensorMesh from simpeg.electromagnetics.static.resistivity import Survey from simpeg.electromagnetics.static.resistivity import sources from simpeg.electromagnetics.static.resistivity import receivers @@ -36,6 +38,28 @@ def test_warning_removed_property(self): survey.survey_type = "dipole-dipole" +class TestDeprecatedIndActive: + """ + Test the deprecated ``ind_active`` argument in ``drape_electrodes_on_topography``. + """ + + @pytest.fixture + def mesh(self): + return TensorMesh((5, 5, 5)) + + def test_error(self, mesh): + """ + Test if error is raised after passing ``ind_active`` as argument. + """ + survey = Survey(source_list=[]) + msg = "'ind_active' has been deprecated and will be removed in " + active_cells = np.ones(mesh.n_cells, dtype=bool) + with pytest.raises(TypeError, match=msg): + survey.drape_electrodes_on_topography( + mesh, active_cells, ind_active=active_cells + ) + + def test_repr(): """ Test the __repr__ method of the survey. diff --git a/tests/em/static/test_spectral_ip_mappings.py b/tests/em/static/test_spectral_ip_mappings.py new file mode 100644 index 0000000000..99f244a2c0 --- /dev/null +++ b/tests/em/static/test_spectral_ip_mappings.py @@ -0,0 +1,63 @@ +""" +Test ``spectral_ip_mappings`` function. +""" + +import pytest +import numpy as np + +import discretize +from simpeg.electromagnetics.static.spectral_induced_polarization import ( + spectral_ip_mappings, +) + + +class TestDeprecatedIndActive: + """Test deprecated ``indActive`` argument ``spectral_ip_mappings``.""" + + OLD_NAME = "indActive" + NEW_NAME = "active_cells" + + @pytest.fixture + def mesh(self): + """Sample mesh.""" + return discretize.TensorMesh([10, 10, 10], "CCN") + + @pytest.fixture + def active_cells(self, mesh): + """Sample active cells for the mesh.""" + active_cells = np.ones(mesh.n_cells, dtype=bool) + active_cells[0] = False + return active_cells + + def get_message_duplicated_error(self, old_name, new_name, version="v0.24.0"): + msg = ( + f"Cannot pass both '{new_name}' and '{old_name}'." + f"'{old_name}' has been deprecated and will be removed in " + f" SimPEG {version}, please use '{new_name}' instead." + ) + return msg + + def get_message_deprecated_warning(self, old_name, new_name, version="v0.24.0"): + msg = ( + f"'{old_name}' has been deprecated and will be removed in " + f" SimPEG {version}, please use '{new_name}' instead." + ) + return msg + + def test_warning_argument(self, mesh, active_cells): + """ + Test if warning is raised after passing ``indActive`` as argument. + """ + msg = self.get_message_deprecated_warning(self.OLD_NAME, self.NEW_NAME) + with pytest.warns(FutureWarning, match=msg): + spectral_ip_mappings(mesh, indActive=active_cells) + + def test_error_duplicated_argument(self, mesh, active_cells): + """ + Test error after passing ``indActive`` and ``active_cells`` as arguments. + """ + msg = self.get_message_duplicated_error(self.OLD_NAME, self.NEW_NAME) + with pytest.raises(TypeError, match=msg): + spectral_ip_mappings( + mesh, active_cells=active_cells, indActive=active_cells + ) diff --git a/tests/em/static/test_static_utils.py b/tests/em/static/test_static_utils.py new file mode 100644 index 0000000000..b02489fac6 --- /dev/null +++ b/tests/em/static/test_static_utils.py @@ -0,0 +1,66 @@ +""" +Test functions in ``static_utils``. +""" + +import pytest +import numpy as np + +import discretize +from simpeg.electromagnetics.static.utils.static_utils import drapeTopotoLoc + + +class TestDeprecatedIndActive: + """Test deprecated ``ind_active`` argument ``drapeTopotoLoc``.""" + + OLD_NAME = "ind_active" + NEW_NAME = "active_cells" + + @pytest.fixture + def mesh(self): + """Sample mesh.""" + return discretize.TensorMesh([10, 10, 10], "CCN") + + @pytest.fixture + def points(self): + """Sample points.""" + return np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]) + + @pytest.fixture + def active_cells(self, mesh): + """Sample active cells for the mesh.""" + active_cells = np.ones(mesh.n_cells, dtype=bool) + active_cells[0] = False + return active_cells + + def get_message_duplicated_error(self, old_name, new_name, version="v0.24.0"): + msg = ( + f"Cannot pass both '{new_name}' and '{old_name}'." + f"'{old_name}' has been deprecated and will be removed in " + f" SimPEG {version}, please use '{new_name}' instead." + ) + return msg + + def get_message_deprecated_warning(self, old_name, new_name, version="v0.24.0"): + msg = ( + f"'{old_name}' has been deprecated and will be removed in " + f" SimPEG {version}, please use '{new_name}' instead." + ) + return msg + + def test_warning_argument(self, mesh, points, active_cells): + """ + Test if warning is raised after passing ``ind_active`` as argument. + """ + msg = self.get_message_deprecated_warning(self.OLD_NAME, self.NEW_NAME) + with pytest.warns(FutureWarning, match=msg): + drapeTopotoLoc(mesh, points, ind_active=active_cells) + + def test_error_duplicated_argument(self, mesh, points, active_cells): + """ + Test error after passing ``ind_active`` and ``active_cells`` as arguments. + """ + msg = self.get_message_duplicated_error(self.OLD_NAME, self.NEW_NAME) + with pytest.raises(TypeError, match=msg): + drapeTopotoLoc( + mesh, points, active_cells=active_cells, ind_active=active_cells + ) From 1a967108f221724247ab71edc4c6971beb5a1303 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 18 Oct 2024 11:14:20 -0700 Subject: [PATCH 074/194] Implement gravity equivalent sources with Choclo as engine (#1527) Implement gravity equivalent sources using Choclo as the engine. Allow gravity equivalent sources to take `engine="choclo"`. Implement new Numba-based forward and sensitivity matrix functions that use Choclo's forward modelling functions for a prism. These new functions can take a 2D mesh and the arrays for the top and bottom boundaries for each cell and compute the forward or build the sensitivity matrix by iterating over each prism in the layer. Add extended tests for the new implementation that also extend the tests for the geoana-based implementation. Part of solution to #1373 --- simpeg/potential_fields/base.py | 31 +- .../gravity/_numba_functions.py | 153 +++++ simpeg/potential_fields/gravity/simulation.py | 229 ++++++- .../potential_fields/magnetics/simulation.py | 23 + tests/pf/test_equivalent_sources.py | 579 ++++++++++++++++++ 5 files changed, 1010 insertions(+), 5 deletions(-) create mode 100644 tests/pf/test_equivalent_sources.py diff --git a/simpeg/potential_fields/base.py b/simpeg/potential_fields/base.py index 33bc556947..9f4e4e3f05 100644 --- a/simpeg/potential_fields/base.py +++ b/simpeg/potential_fields/base.py @@ -395,7 +395,7 @@ def _get_active_nodes(self): nodes = self.mesh.nodes else: raise TypeError(f"Invalid mesh of type {self.mesh.__class__.__name__}.") - # Get original cell_nodes but only for active cells + # Get original cell_nodes cell_nodes = self.mesh.cell_nodes # If all cells in the mesh are active, return nodes and cell_nodes if self.nC == self.mesh.n_cells: @@ -437,6 +437,7 @@ class BaseEquivalentSourceLayerSimulation(BasePFSimulation): """ def __init__(self, mesh, cell_z_top, cell_z_bottom, **kwargs): + if mesh.dim != 2: raise AttributeError("Mesh to equivalent source layer must be 2D.") @@ -454,6 +455,8 @@ def __init__(self, mesh, cell_z_top, cell_z_bottom, **kwargs): "cells, and match the number of active cells.", ) + self._cell_z_top, self._cell_z_bottom = cell_z_top, cell_z_bottom + all_nodes = self._nodes[self._unique_inv] all_nodes = [ np.c_[all_nodes[0], cell_z_bottom], @@ -468,6 +471,32 @@ def __init__(self, mesh, cell_z_top, cell_z_bottom, **kwargs): self._nodes = np.stack(all_nodes, axis=0) self._unique_inv = None + @property + def cell_z_top(self) -> np.ndarray: + """ + Elevations for the top face of all cells in the layer. + """ + return self._cell_z_top + + @property + def cell_z_bottom(self) -> np.ndarray: + """ + Elevations for the bottom face of all cells in the layer. + """ + return self._cell_z_bottom + + def _check_engine_and_mesh_dimensions(self): + """ + Check dimensions of the mesh + + Overwrite the parent's method: the equivalent sources class needs 2D + meshes, while the potential field simulations work only with 3D meshes. + + This check will run for any given engine. + """ + if self.mesh.dim != 2: + raise AttributeError("Mesh to equivalent source layer must be 2D.") + def progress(iteration, prog, final): """Progress (% complete) for constructing sensitivity matrix. diff --git a/simpeg/potential_fields/gravity/_numba_functions.py b/simpeg/potential_fields/gravity/_numba_functions.py index 3eb6ae9bf1..fe2e69e202 100644 --- a/simpeg/potential_fields/gravity/_numba_functions.py +++ b/simpeg/potential_fields/gravity/_numba_functions.py @@ -192,8 +192,161 @@ def _evaluate_kernel( return kernel_func(dx, dy, dz, distance) +def _forward_gravity_2d_mesh( + receivers, + cells_bounds, + top, + bottom, + densities, + fields, + forward_func, + constant_factor, +): + """ + Forward gravity fields of 2D meshes. + + This function is designed to be used with equivalent sources, where the + mesh is a 2D mesh (prism layer). The top and bottom boundaries of each cell + are passed through the ``top`` and ``bottom`` arrays. + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_function = jit(nopython=True, parallel=True)(_forward_gravity_2d_mesh) + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + densities : (n_active_cells) numpy.ndarray + Array with densities of each active cell in the mesh. + fields : (n_receivers) numpy.ndarray + Array full of zeros where the gravity fields on each receiver will be + stored. This could be a preallocated array or a slice of it. + forward_func : callable + Forward function that will be evaluated on each node of the mesh. Choose + one of the forward functions in ``choclo.prism``. + constant_factor : float + Constant factor that will be used to multiply each element of the + ``fields`` array. + + Notes + ----- + The constant factor is applied here to each element of fields because + it's more efficient than doing it afterwards: it would require to + index the elements that corresponds to each component. + """ + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + # Forward model the gravity field of each cell on each receiver location + for i in prange(n_receivers): + for j in range(n_cells): + fields[i] += constant_factor * forward_func( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + densities[j], + ) + + +def _sensitivity_gravity_2d_mesh( + receivers, + cells_bounds, + top, + bottom, + sensitivity_matrix, + forward_func, + constant_factor, +): + """ + Fill the sensitivity matrix + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_function = jit(nopython=True, parallel=True)(_sensitivity_gravity_2d_mesh) + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + sensitivity_matrix : (n_receivers, n_active_nodes) array + Empty 2d array where the sensitivity matrix elements will be filled. + This could be a preallocated empty array or a slice of it. + forward_func : callable + Forward function that will be evaluated on each node of the mesh. Choose + one of the forward functions in ``choclo.prism``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + + Notes + ----- + The constant factor is applied here to each row of the sensitivity matrix + because it's more efficient than doing it afterwards: it would require to + index the rows that corresponds to each component. + """ + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + for j in range(n_cells): + sensitivity_matrix[i, j] = constant_factor * forward_func( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + 1.0, # use unitary density to get sensitivities + ) + + # Define decorated versions of these functions _sensitivity_gravity_parallel = jit(nopython=True, parallel=True)(_sensitivity_gravity) _sensitivity_gravity_serial = jit(nopython=True, parallel=False)(_sensitivity_gravity) _forward_gravity_parallel = jit(nopython=True, parallel=True)(_forward_gravity) _forward_gravity_serial = jit(nopython=True, parallel=False)(_forward_gravity) +_forward_gravity_2d_mesh_serial = jit(nopython=True, parallel=False)( + _forward_gravity_2d_mesh +) +_forward_gravity_2d_mesh_parallel = jit(nopython=True, parallel=True)( + _forward_gravity_2d_mesh +) +_sensitivity_gravity_2d_mesh_serial = jit(nopython=True, parallel=False)( + _sensitivity_gravity_2d_mesh +) +_sensitivity_gravity_2d_mesh_parallel = jit(nopython=True, parallel=True)( + _sensitivity_gravity_2d_mesh +) diff --git a/simpeg/potential_fields/gravity/simulation.py b/simpeg/potential_fields/gravity/simulation.py index 1d1f667e2e..2d68c7c0ca 100644 --- a/simpeg/potential_fields/gravity/simulation.py +++ b/simpeg/potential_fields/gravity/simulation.py @@ -1,6 +1,8 @@ +from __future__ import annotations import warnings import numpy as np import scipy.constants as constants +from discretize import TensorMesh, TreeMesh from geoana.kernels import prism_fz, prism_fzx, prism_fzy, prism_fzz from scipy.constants import G as NewtG @@ -16,6 +18,10 @@ _sensitivity_gravity_parallel, _forward_gravity_serial, _forward_gravity_parallel, + _forward_gravity_2d_mesh_serial, + _forward_gravity_2d_mesh_parallel, + _sensitivity_gravity_2d_mesh_serial, + _sensitivity_gravity_2d_mesh_parallel, ) if choclo is not None: @@ -30,6 +36,48 @@ def kernel_uv(easting, northing, upward, radius): ) return result + @jit(nopython=True) + def gravity_uv( + easting, + northing, + upward, + prism_west, + prism_east, + prism_south, + prism_north, + prism_bottom, + prism_top, + density, + ): + """Forward model the Guv gradiometry component.""" + result = 0.5 * ( + choclo.prism.gravity_nn( + easting, + northing, + upward, + prism_west, + prism_east, + prism_south, + prism_north, + prism_bottom, + prism_top, + density, + ) + - choclo.prism.gravity_ee( + easting, + northing, + upward, + prism_west, + prism_east, + prism_south, + prism_north, + prism_bottom, + prism_top, + density, + ) + ) + return result + CHOCLO_KERNELS = { "gx": choclo.prism.kernel_e, "gy": choclo.prism.kernel_n, @@ -43,6 +91,19 @@ def kernel_uv(easting, northing, upward, radius): "guv": kernel_uv, } + CHOCLO_FORWARD_FUNCS = { + "gx": choclo.prism.gravity_e, + "gy": choclo.prism.gravity_n, + "gz": choclo.prism.gravity_u, + "gxx": choclo.prism.gravity_ee, + "gyy": choclo.prism.gravity_nn, + "gzz": choclo.prism.gravity_uu, + "gxy": choclo.prism.gravity_en, + "gxz": choclo.prism.gravity_eu, + "gyz": choclo.prism.gravity_nu, + "guv": gravity_uv, + } + def _get_conversion_factor(component): """ @@ -57,6 +118,42 @@ def _get_conversion_factor(component): return conversion_factor +def _get_cell_bounds(mesh: TensorMesh | TreeMesh): + """ + Bounds of each cell in a 2D TensorMesh or TreeMesh. + + The bounds are defined as ``x_min``, ``x_max``, ``y_min``, ``y_max``. + + ..note: + + This private function could be replaced by calling some `cell_bounds` + method of the meshes directly from discretize. + + Parameters + ---------- + mesh : discretize.TensorMesh or discretize.TreeMesh + A 2D mesh. + + Returns + ------- + bounds : (n_cells, 4) array + Array with the bounds of each cell in the mesh. + """ + if not isinstance(mesh, (TensorMesh, TreeMesh)): + raise TypeError(f"Invalid mesh of type {mesh.__class__}.") + if mesh.dim != 2: + raise TypeError( + f"Invalid mesh with '{mesh.dim}' dimensions. Only 2D meshes can be passed." + ) + centers, widths = mesh.cell_centers, mesh.h_gridded + xmin = centers[:, 0] - widths[:, 0] / 2 + xmax = centers[:, 0] + widths[:, 0] / 2 + ymin = centers[:, 1] - widths[:, 1] / 2 + ymax = centers[:, 1] + widths[:, 1] / 2 + bounds = np.hstack(tuple(v[:, np.newaxis] for v in (xmin, xmax, ymin, ymax))) + return bounds + + class Simulation3DIntegral(BasePFSimulation): """ Gravity simulation in integral form. @@ -432,13 +529,137 @@ class SimulationEquivalentSourceLayer( mesh : discretize.BaseMesh A 2D tensor or tree mesh defining discretization along the x and y directions cell_z_top : numpy.ndarray or float - Define the elevations for the top face of all cells in the layer. If an array it should be the same size as - the active cell set. + Define the elevations for the top face of all cells in the layer. + If an array it should be the same size as the active cell set. cell_z_bottom : numpy.ndarray or float - Define the elevations for the bottom face of all cells in the layer. If an array it should be the same size as - the active cell set. + Define the elevations for the bottom face of all cells in the layer. + If an array it should be the same size as the active cell set. + engine : {"geoana", "choclo"}, optional + Choose which engine should be used to run the forward model. + numba_parallel : bool, optional + If True, the simulation will run in parallel. If False, it will + run in serial. If ``engine`` is not ``"choclo"`` this argument will be + ignored. """ + def __init__( + self, + mesh, + cell_z_top, + cell_z_bottom, + engine="geoana", + numba_parallel=True, + **kwargs, + ): + super().__init__( + mesh, + cell_z_top, + cell_z_bottom, + engine=engine, + numba_parallel=numba_parallel, + **kwargs, + ) + + if self.engine == "choclo": + if self.numba_parallel: + self._sensitivity_gravity = _sensitivity_gravity_2d_mesh_parallel + self._forward_gravity = _forward_gravity_2d_mesh_parallel + else: + self._sensitivity_gravity = _sensitivity_gravity_2d_mesh_serial + self._forward_gravity = _forward_gravity_2d_mesh_serial + + def _forward(self, densities): + """ + Forward model the fields of active cells in the mesh on receivers. + + Parameters + ---------- + densities : (n_active_cells) numpy.ndarray + Array containing the densities of the active cells in the mesh, in + g/cc. + + Returns + ------- + (nD,) numpy.ndarray + Always return a ``np.float64`` array. + """ + # Get cells in the 2D mesh + cells_bounds = _get_cell_bounds(self.mesh) + # Keep only active cells + cells_bounds_active = cells_bounds[self.active_cells] + # Allocate fields array + fields = np.zeros(self.survey.nD, dtype=self.sensitivity_dtype) + # Compute fields + index_offset = 0 + for components, receivers in self._get_components_and_receivers(): + n_components = len(components) + n_elements = n_components * receivers.shape[0] + for i, component in enumerate(components): + forward_func = CHOCLO_FORWARD_FUNCS[component] + conversion_factor = _get_conversion_factor(component) + vector_slice = slice( + index_offset + i, index_offset + n_elements, n_components + ) + self._forward_gravity( + receivers, + cells_bounds_active, + self.cell_z_top, + self.cell_z_bottom, + densities, + fields[vector_slice], + forward_func, + conversion_factor, + ) + index_offset += n_elements + return fields + + def _sensitivity_matrix(self): + """ + Compute the sensitivity matrix G + + Returns + ------- + (nD, n_active_cells) numpy.ndarray + """ + # Get cells in the 2D mesh + cells_bounds = _get_cell_bounds(self.mesh) + # Keep only active cells + cells_bounds_active = cells_bounds[self.active_cells] + # Allocate sensitivity matrix + shape = (self.survey.nD, self.nC) + if self.store_sensitivities == "disk": + sensitivity_matrix = np.memmap( + self.sensitivity_path, + shape=shape, + dtype=self.sensitivity_dtype, + order="C", # it's more efficient to write in row major + mode="w+", + ) + else: + sensitivity_matrix = np.empty(shape, dtype=self.sensitivity_dtype) + # Start filling the sensitivity matrix + index_offset = 0 + for components, receivers in self._get_components_and_receivers(): + n_components = len(components) + n_rows = n_components * receivers.shape[0] + for i, component in enumerate(components): + forward_func = CHOCLO_FORWARD_FUNCS[component] + conversion_factor = _get_conversion_factor(component) + matrix_slice = slice( + index_offset + i, index_offset + n_rows, n_components + ) + self._sensitivity_gravity( + receivers, + cells_bounds_active, + self.cell_z_top, + self.cell_z_bottom, + sensitivity_matrix[matrix_slice, :], + forward_func, + conversion_factor, + ) + index_offset += n_rows + return sensitivity_matrix + class Simulation3DDifferential(BasePDESimulation): r"""Finite volume simulation class for gravity. diff --git a/simpeg/potential_fields/magnetics/simulation.py b/simpeg/potential_fields/magnetics/simulation.py index 91ec72a7e7..64cf9eba12 100644 --- a/simpeg/potential_fields/magnetics/simulation.py +++ b/simpeg/potential_fields/magnetics/simulation.py @@ -792,6 +792,29 @@ class SimulationEquivalentSourceLayer( """ + def __init__( + self, + mesh, + cell_z_top, + cell_z_bottom, + engine="geoana", + numba_parallel=True, + **kwargs, + ): + if engine == "choclo": + raise NotImplementedError( + "Magnetic equivalent sources with choclo as engine has not been" + " implemented yet. Use 'geoana' instead." + ) + super().__init__( + mesh, + cell_z_top, + cell_z_bottom, + engine=engine, + numba_parallel=numba_parallel, + **kwargs, + ) + class Simulation3DDifferential(BaseMagneticPDESimulation): """ diff --git a/tests/pf/test_equivalent_sources.py b/tests/pf/test_equivalent_sources.py new file mode 100644 index 0000000000..1414768664 --- /dev/null +++ b/tests/pf/test_equivalent_sources.py @@ -0,0 +1,579 @@ +import pytest + +import numpy as np +from discretize import TensorMesh +from discretize.utils import mesh_builder_xyz, mkvc + +import simpeg +from simpeg.optimization import ProjectedGNCG +from simpeg.potential_fields import gravity, magnetics, base + +COMPONENTS = ["gx", "gy", "gz", "gxx", "gyy", "gzz", "gxy", "gxz", "gyz", "guv"] + + +def create_grid(x_range, y_range, size): + """Create a 2D horizontal coordinates grid.""" + x_start, x_end = x_range + y_start, y_end = y_range + x, y = np.meshgrid( + np.linspace(x_start, x_end, size), np.linspace(y_start, y_end, size) + ) + return x, y + + +@pytest.fixture() +def mesh_params(): + """Parameters for building the sample meshes.""" + h = [5, 5] + padding_distances = np.ones((2, 2)) * 50 + return h, padding_distances + + +@pytest.fixture() +def tensor_mesh(mesh_params, coordinates): + """Sample 2D tensor mesh to use with equivalent sources.""" + mesh_type = "tensor" + h, padding_distance = mesh_params + mesh = mesh_builder_xyz( + coordinates[:, :2], h, padding_distance=padding_distance, mesh_type=mesh_type + ) + return mesh + + +@pytest.fixture() +def tree_mesh(mesh_params, coordinates): + """Sample 2D tree mesh to use with equivalent sources.""" + mesh_type = "tree" + h, padding_distance = mesh_params + mesh = mesh_builder_xyz( + coordinates[:, :2], h, padding_distance=padding_distance, mesh_type=mesh_type + ) + mesh.refine_points(coordinates[:, :2], padding_cells_by_level=[2, 4]) + return mesh + + +@pytest.fixture(params=["tensor", "tree"]) +def mesh(tensor_mesh, tree_mesh, request): + """Sample 2D mesh to use with equivalent sources.""" + mesh_type = request.param + if mesh_type == "tree": + return tree_mesh + elif mesh_type == "tensor": + return tensor_mesh + else: + raise ValueError(f"Invalid mesh type: '{mesh_type}'") + + +@pytest.fixture +def mesh_top(): + """Top boundary of the mesh.""" + return -20.0 + + +@pytest.fixture +def mesh_bottom(): + """Bottom boundary of the mesh.""" + return -50.0 + + +@pytest.fixture +def coordinates(): + """Synthetic observation points grid.""" + x, y = create_grid(x_range=(-50, 50), y_range=(-50, 50), size=11) + z = np.full_like(x, fill_value=5.0) + return np.c_[mkvc(x), mkvc(y), mkvc(z)] + + +def get_block_model(mesh, phys_property: float): + """Build a block model.""" + model = simpeg.utils.model_builder.add_block( + mesh.cell_centers, + np.zeros(mesh.n_cells), + np.r_[-20, -20], + np.r_[20, 20], + phys_property, + ) + return model + + +def get_mapping(mesh): + """Get an identity map for the given mesh.""" + return simpeg.maps.IdentityMap(nP=mesh.n_cells) + + +@pytest.fixture +def gravity_survey(coordinates): + """ + Sample survey for the gravity equivalent sources. + """ + return build_gravity_survey(coordinates, components="gz") + + +def build_gravity_survey(coordinates, components): + """ + Build a gravity survey. + """ + receivers = gravity.Point(coordinates, components=components) + source_field = gravity.SourceField([receivers]) + survey = gravity.Survey(source_field) + return survey + + +class Test3DMeshError: + """ + Test if error is raised after passing a 3D mesh to equivalent sources. + """ + + @pytest.fixture + def mesh_3d(self): + mesh = TensorMesh([2, 3, 4]) + return mesh + + @pytest.mark.parametrize("engine", ("geoana", "choclo")) + def test_error_on_gravity(self, mesh_3d, engine): + """ + Test error is raised after passing a 3D mesh to gravity eq source class. + """ + msg = "Mesh to equivalent source layer must be 2D." + with pytest.raises(AttributeError, match=msg): + gravity.SimulationEquivalentSourceLayer( + mesh=mesh_3d, cell_z_top=0.0, cell_z_bottom=-2.0, engine=engine + ) + + @pytest.mark.parametrize("engine", ("geoana",)) + def test_error_on_mag(self, mesh_3d, engine): + """ + Test error is raised after passing a 3D mesh to magnetic eq source class. + """ + msg = "Mesh to equivalent source layer must be 2D." + with pytest.raises(AttributeError, match=msg): + magnetics.SimulationEquivalentSourceLayer( + mesh=mesh_3d, cell_z_top=0.0, cell_z_bottom=-2.0, engine=engine + ) + + def test_error_on_base_class(self, mesh_3d): + """ + Test error is raised after passing a 3D mesh to the eq source base class. + """ + msg = "Mesh to equivalent source layer must be 2D." + with pytest.raises(AttributeError, match=msg): + base.BaseEquivalentSourceLayerSimulation( + mesh=mesh_3d, cell_z_top=0.0, cell_z_bottom=-2.0 + ) + + @pytest.mark.parametrize( + "eq_sources_class", + ( + gravity.SimulationEquivalentSourceLayer, + magnetics.SimulationEquivalentSourceLayer, + ), + ) + def test_overridden_method( + self, mesh_3d, tensor_mesh, mesh_top, mesh_bottom, eq_sources_class + ): + """ + Test for the overridden _check_engine_and_mesh_dimensions method. + + This method is rarely going to trigger an error because if a 3D mesh is + passed to the constructor, it'll catch it and raise an error. + This test is added to extend coverage. + """ + # Initialize instance with a 2D mesh + eq_sources = eq_sources_class( + mesh=tensor_mesh, cell_z_top=mesh_top, cell_z_bottom=mesh_bottom + ) + # Set the mesh to the 3D mesh + eq_sources.mesh = mesh_3d + # Check error in _check_engine_and_mesh_dimensions method + msg = "Mesh to equivalent source layer must be 2D." + with pytest.raises(AttributeError, match=msg): + eq_sources._check_engine_and_mesh_dimensions() + + +class TestGravityEquivalentSourcesForward: + """ + Test the forward capabilities of the gravity equivalent sources. + """ + + def get_mesh_3d(self, mesh, top: float, bottom: float): + """ + Build a 3D mesh analogous to the 2D mesh + the top and bottom bounds. + """ + origin = (*mesh.origin, bottom) + h = (*mesh.h, np.array([top - bottom], dtype=np.float64)) + mesh_3d = TensorMesh(h=h, origin=origin) + return mesh_3d + + @pytest.mark.parametrize("engine", ("geoana", "choclo")) + @pytest.mark.parametrize("store_sensitivities", ("ram", "forward_only")) + def test_forward_vs_simulation( + self, + tensor_mesh, + mesh_bottom, + mesh_top, + gravity_survey, + engine, + store_sensitivities, + ): + """ + Test forward of the eq sources vs. using the integral 3d simulation. + """ + # Build 3D mesh that is analogous to the 2D mesh with bottom and top + mesh_3d = self.get_mesh_3d(tensor_mesh, top=mesh_top, bottom=mesh_bottom) + # Build simulations + mapping = get_mapping(tensor_mesh) + sim_3d = gravity.Simulation3DIntegral( + survey=gravity_survey, mesh=mesh_3d, rhoMap=mapping + ) + eq_sources = gravity.SimulationEquivalentSourceLayer( + mesh=tensor_mesh, + cell_z_top=mesh_top, + cell_z_bottom=mesh_bottom, + survey=gravity_survey, + rhoMap=mapping, + engine=engine, + store_sensitivities=store_sensitivities, + ) + # Compare predictions of both simulations + model = get_block_model(tensor_mesh, 2.67) + np.testing.assert_allclose( + sim_3d.dpred(model), eq_sources.dpred(model), atol=1e-7 + ) + + @pytest.mark.parametrize("engine", ("geoana", "choclo")) + @pytest.mark.parametrize("store_sensitivities", ("ram", "forward_only")) + @pytest.mark.parametrize("components", COMPONENTS + [["gz", "gzz"]]) + def test_forward_vs_simulation_with_components( + self, + coordinates, + tensor_mesh, + mesh_bottom, + mesh_top, + engine, + store_sensitivities, + components, + ): + """ + Test forward vs simulation using different gravity components. + """ + # Build survey + survey = build_gravity_survey(coordinates, components) + # Build 3D mesh that is analogous to the 2D mesh with bottom and top + mesh_3d = self.get_mesh_3d(tensor_mesh, top=mesh_top, bottom=mesh_bottom) + # Build simulations + mapping = get_mapping(tensor_mesh) + sim_3d = gravity.Simulation3DIntegral( + survey=survey, mesh=mesh_3d, rhoMap=mapping + ) + eq_sources = gravity.SimulationEquivalentSourceLayer( + mesh=tensor_mesh, + cell_z_top=mesh_top, + cell_z_bottom=mesh_bottom, + survey=survey, + rhoMap=mapping, + engine=engine, + store_sensitivities=store_sensitivities, + ) + # Compare predictions of both simulations + model = get_block_model(tensor_mesh, 2.67) + np.testing.assert_allclose( + sim_3d.dpred(model), eq_sources.dpred(model), atol=5e-6 + ) + + @pytest.mark.parametrize("engine", ("geoana", "choclo")) + def test_forward_vs_simulation_on_disk( + self, + tensor_mesh, + mesh_bottom, + mesh_top, + gravity_survey, + engine, + tmp_path, + ): + """ + Test forward vs simulation storing sensitivities on disk. + """ + # Build 3D mesh that is analogous to the 2D mesh with bottom and top + mesh_3d = self.get_mesh_3d(tensor_mesh, top=mesh_top, bottom=mesh_bottom) + # Define sensitivity_dir + if engine == "geoana": + sensitivity_path = tmp_path / "sensitivities_geoana" + sensitivity_path.mkdir() + elif engine == "choclo": + sensitivity_path = tmp_path / "sensitivities_choclo" + # Build simulations + mapping = get_mapping(tensor_mesh) + sim_3d = gravity.Simulation3DIntegral( + survey=gravity_survey, mesh=mesh_3d, rhoMap=mapping + ) + eq_sources = gravity.SimulationEquivalentSourceLayer( + mesh=tensor_mesh, + cell_z_top=mesh_top, + cell_z_bottom=mesh_bottom, + survey=gravity_survey, + rhoMap=mapping, + engine=engine, + store_sensitivities="disk", + sensitivity_path=str(sensitivity_path), + ) + # Compare predictions of both simulations + model = get_block_model(tensor_mesh, 2.67) + np.testing.assert_allclose(sim_3d.dpred(model), eq_sources.dpred(model)) + + @pytest.mark.parametrize("engine", ("geoana", "choclo")) + @pytest.mark.parametrize("store_sensitivities", ("ram", "forward_only")) + def test_forward_vs_simulation_with_active_cells( + self, + tensor_mesh, + mesh_bottom, + mesh_top, + gravity_survey, + engine, + store_sensitivities, + ): + """ + Test forward vs simulation using active cells. + """ + model = get_block_model(tensor_mesh, 2.67) + + # Define some inactive cells inside the block + block_cells_indices = np.indices(model.shape).ravel()[model != 0] + inactive_indices = block_cells_indices[ + : block_cells_indices.size // 2 + ] # mark half of the cells in the block as inactive + active_cells = np.ones_like(model, dtype=bool) + active_cells[inactive_indices] = False + assert not np.all(active_cells) # check we do have inactive cells + + # Keep only values of the model in the active cells + model = model[active_cells] + + # Build 3D mesh that is analogous to the 2D mesh with bottom and top + mesh_3d = self.get_mesh_3d(tensor_mesh, top=mesh_top, bottom=mesh_bottom) + + # Build simulations + mapping = simpeg.maps.IdentityMap(nP=model.size) + sim_3d = gravity.Simulation3DIntegral( + survey=gravity_survey, + mesh=mesh_3d, + rhoMap=mapping, + active_cells=active_cells, + ) + eq_sources = gravity.SimulationEquivalentSourceLayer( + mesh=tensor_mesh, + cell_z_top=mesh_top, + cell_z_bottom=mesh_bottom, + survey=gravity_survey, + rhoMap=mapping, + engine=engine, + store_sensitivities=store_sensitivities, + active_cells=active_cells, + ) + # Compare predictions of both simulations + np.testing.assert_allclose( + sim_3d.dpred(model), eq_sources.dpred(model), atol=1e-7 + ) + + @pytest.mark.parametrize("store_sensitivities", ("ram", "forward_only")) + def test_forward_geoana_choclo( + self, mesh, mesh_bottom, mesh_top, gravity_survey, store_sensitivities + ): + """Compare forwards using geoana and choclo.""" + # Build simulations + mapping = get_mapping(mesh) + kwargs = dict( + mesh=mesh, + cell_z_top=mesh_top, + cell_z_bottom=mesh_bottom, + survey=gravity_survey, + rhoMap=mapping, + store_sensitivities=store_sensitivities, + ) + sim_geoana = gravity.SimulationEquivalentSourceLayer(engine="geoana", **kwargs) + sim_choclo = gravity.SimulationEquivalentSourceLayer(engine="choclo", **kwargs) + model = get_block_model(mesh, 2.67) + np.testing.assert_allclose(sim_geoana.dpred(model), sim_choclo.dpred(model)) + + def test_forward_choclo_serial_parallel( + self, mesh, mesh_bottom, mesh_top, gravity_survey + ): + """Test forward using choclo in serial and in parallel.""" + # Build simulations + mapping = get_mapping(mesh) + kwargs = dict( + mesh=mesh, + cell_z_top=mesh_top, + cell_z_bottom=mesh_bottom, + survey=gravity_survey, + rhoMap=mapping, + engine="choclo", + ) + sim_parallel = gravity.SimulationEquivalentSourceLayer( + numba_parallel=True, **kwargs + ) + sim_serial = gravity.SimulationEquivalentSourceLayer( + numba_parallel=False, **kwargs + ) + model = get_block_model(mesh, 2.67) + np.testing.assert_allclose(sim_parallel.dpred(model), sim_serial.dpred(model)) + + +class TestGravityEquivalentSources: + """ + Test fitting equivalent sources with synthetic data. + """ + + def get_mesh_top_bottom(self, mesh, array=False): + """Build the top and bottom boundaries of the mesh. + + If array is True, the outputs are going to be arrays, otherwise they'll + be floats. + """ + top, bottom = -20.0, -50.0 + if array: + rng = np.random.default_rng(seed=42) + mesh_top = np.full(mesh.n_cells, fill_value=top) + rng.normal( + scale=0.5, size=mesh.n_cells + ) + mesh_bottom = np.full(mesh.n_cells, fill_value=bottom) + rng.normal( + scale=0.5, size=mesh.n_cells + ) + else: + mesh_top, mesh_bottom = top, bottom + return mesh_top, mesh_bottom + + def build_synthetic_data(self, simulation, model): + data = simulation.make_synthetic_data( + model, + relative_error=0.0, + noise_floor=1e-3, + add_noise=True, + random_seed=1, + ) + return data + + def build_inversion(self, mesh, simulation, synthetic_data): + """Build inversion problem.""" + # Build data misfit and regularization terms + data_misfit = simpeg.data_misfit.L2DataMisfit( + simulation=simulation, data=synthetic_data + ) + regularization = simpeg.regularization.WeightedLeastSquares(mesh=mesh) + # Choose optimization + optimization = ProjectedGNCG( + maxIterLS=5, + maxIterCG=20, + tolCG=1e-4, + ) + # Build inverse problem + inverse_problem = simpeg.inverse_problem.BaseInvProblem( + data_misfit, regularization, optimization + ) + # Define directives + starting_beta = simpeg.directives.BetaEstimate_ByEig( + beta0_ratio=1e-1, random_seed=42 + ) + beta_schedule = simpeg.directives.BetaSchedule(coolingFactor=3, coolingRate=1) + update_jacobi = simpeg.directives.UpdatePreconditioner() + target_misfit = simpeg.directives.TargetMisfit(chifact=1) + sensitivity_weights = simpeg.directives.UpdateSensitivityWeights( + every_iteration=False + ) + directives = [ + sensitivity_weights, + starting_beta, + beta_schedule, + update_jacobi, + target_misfit, + ] + # Define inversion + inversion = simpeg.inversion.BaseInversion(inverse_problem, directives) + return inversion + + @pytest.mark.parametrize("engine", ("geoana", "choclo")) + @pytest.mark.parametrize( + "top_bottom_as_array", + (False, True), + ids=("top-bottom-float", "top-bottom-array"), + ) + def test_predictions_on_data_points( + self, + tree_mesh, + gravity_survey, + top_bottom_as_array, + engine, + ): + """ + Test eq sources predictions on the same data points. + + The equivalent sources should be able to reproduce the same data with + which they were trained. + """ + # Get mesh top and bottom + mesh_top, mesh_bottom = self.get_mesh_top_bottom( + tree_mesh, array=top_bottom_as_array + ) + # Build simulation + mapping = get_mapping(tree_mesh) + simulation = gravity.SimulationEquivalentSourceLayer( + tree_mesh, + mesh_top, + mesh_bottom, + survey=gravity_survey, + rhoMap=mapping, + engine=engine, + ) + # Generate synthetic data + model = get_block_model(tree_mesh, 2.67) + synthetic_data = self.build_synthetic_data(simulation, model) + # Build inversion + inversion = self.build_inversion(tree_mesh, simulation, synthetic_data) + # Run inversion + starting_model = np.zeros(tree_mesh.n_cells) + recovered_model = inversion.run(starting_model) + # Predict data + prediction = simulation.dpred(recovered_model) + # Check if prediction is close to the synthetic data + atol, rtol = 0.005, 1e-5 + np.testing.assert_allclose( + prediction, synthetic_data.dobs, atol=atol, rtol=rtol + ) + + +class TestMagneticEquivalentSources: + @pytest.fixture + def survey(self, coordinates): + """ + Sample survey for the gravity equivalent sources. + """ + return self._build_survey(coordinates, components="tmi") + + def _build_survey(self, coordinates, components): + """ + Build a magnetic survey. + """ + receivers = magnetics.Point(coordinates, components=components) + source_field = magnetics.UniformBackgroundField( + [receivers], amplitude=50_000, inclination=35, declination=12 + ) + survey = magnetics.Survey(source_field) + return survey + + def test_choclo_not_implemented(self, tensor_mesh, mesh_top, mesh_bottom, survey): + """ + Test if error is raised when passing "choclo" to magnetic eq sources. + """ + msg = ( + "Magnetic equivalent sources with choclo as engine has not been" + " implemented yet. Use 'geoana' instead." + ) + mapping = simpeg.maps.IdentityMap(nP=tensor_mesh.n_cells) + with pytest.raises(NotImplementedError, match=msg): + magnetics.SimulationEquivalentSourceLayer( + tensor_mesh, + mesh_top, + mesh_bottom, + survey=survey, + rhoMap=mapping, + engine="choclo", + ) From 702389ee2a9f2d876fa47b40517cc1d9b7d2f0ba Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 22 Oct 2024 14:47:46 -0700 Subject: [PATCH 075/194] Implement tmi derivatives with Choclo in magnetic simulation (#1553) Extend support for TMI derivatives in the integral magnetic simulation using Choclo as engine. Include new Numba functions to efficiently compute the TMI derivatives. Update tests for TMI derivatives both for Choclo and geoana, create a separate test function for the finite difference comparison. --- .../magnetics/_numba_functions.py | 366 +++++++++++++++ .../potential_fields/magnetics/simulation.py | 74 +++ tests/pf/test_forward_Mag_Linear.py | 425 +++++++++++------- 3 files changed, 712 insertions(+), 153 deletions(-) diff --git a/simpeg/potential_fields/magnetics/_numba_functions.py b/simpeg/potential_fields/magnetics/_numba_functions.py index 92a5d2eacd..611e96cf50 100644 --- a/simpeg/potential_fields/magnetics/_numba_functions.py +++ b/simpeg/potential_fields/magnetics/_numba_functions.py @@ -368,6 +368,191 @@ def _sensitivity_tmi( ) +def _sensitivity_tmi_derivative( + receivers, + nodes, + sensitivity_matrix, + cell_nodes, + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + constant_factor, + scalar_model, +): + r""" + Fill the sensitivity matrix for a TMI derivative. + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_sens = jit(nopython=True, parallel=True)(_sensitivity_tmi_derivative) + + Parameters + ---------- + receivers : (n_receivers, 3) array + Array with the locations of the receivers + nodes : (n_active_nodes, 3) array + Array with the location of the mesh nodes. + sensitivity_matrix : array + Empty 2d array where the sensitivity matrix elements will be filled. + This could be a preallocated empty array or a slice of it. + The array should have a shape of ``(n_receivers, n_active_nodes)`` + if ``scalar_model`` is True. + The array should have a shape of ``(n_receivers, 3 * n_active_nodes)`` + if ``scalar_model`` is False. + cell_nodes : (n_active_cells, 8) array + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz : callables + Kernel functions used for computing the desired TMI derivative. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the sensitivity matrix is build to work with scalar models + (susceptibilities). + If False, the sensitivity matrix is build to work with vector models + (effective susceptibilities). + + Notes + ----- + + About the kernel functions + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + + To compute the :math:`\alpha` derivative of the TMI :math:`\Delta T` (with + :math:`\alpha \in \{x, y, z\}` we need to evaluate third order kernels + functions for the prism. The kernels we need to evaluate can be obtained by + fixing one of the subindices to the direction of the derivative + (:math:`\alpha`) and cycle through combinations of the other two. + + For ``tmi_x`` we need to pass: + + .. code:: + + kernel_xx=kernel_eee, kernel_yy=kernel_enn, kernel_zz=kernel_euu, + kernel_xy=kernel_een, kernel_xz=kernel_eeu, kernel_yz=kernel_enu + + For ``tmi_y`` we need to pass: + + .. code:: + + kernel_xx=kernel_een, kernel_yy=kernel_nnn, kernel_zz=kernel_nuu, + kernel_xy=kernel_enn, kernel_xz=kernel_enu, kernel_yz=kernel_nnu + + For ``tmi_z`` we need to pass: + + .. code:: + + kernel_xx=kernel_eeu, kernel_yy=kernel_nnu, kernel_zz=kernel_uuu, + kernel_xy=kernel_enu, kernel_xz=kernel_euu, kernel_yz=kernel_nuu + + + About the model array + ^^^^^^^^^^^^^^^^^^^^^ + + The ``model`` must always be a 1d array: + + * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with + the same number of elements as active cells in the mesh. It should store + the magnetic susceptibilities of each active cell in SI units. + * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array + with a number of elements equal to three times the active cells in the + mesh. It should store the components of the magnetization vector of each + active cell in :math:`Am^{-1}`. The order in which the components should + be passed are: + * every _easting_ component of each active cell, + * then every _northing_ component of each active cell, + * and finally every _upward_ component of each active cell. + + About the sensitivity matrix + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + Each row of the sensitivity matrix corresponds to a single receiver + location. + + If ``scalar_model`` is True, then each element of the row will + correspond to the partial derivative of the tmi derivative (spatial) + with respect to the susceptibility of each cell in the mesh. + + If ``scalar_model`` is False, then each row can be split in three sections + containing: + + * the partial derivatives of the tmi derivative with respect + to the _x_ component of the effective susceptibility of each cell; then + * the partial derivatives of the tmi derivative with respect + to the _y_ component of the effective susceptibility of each cell; and then + * the partial derivatives of the tmi derivative with respect + to the _z_ component of the effective susceptibility of each cell. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kxx, kyy, kzz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + kxy, kxz, kyz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kxx[j] = kernel_xx(dx, dy, dz, distance) + kyy[j] = kernel_yy(dx, dy, dz, distance) + kzz[j] = kernel_zz(dx, dy, dz, distance) + kxy[j] = kernel_xy(dx, dy, dz, distance) + kxz[j] = kernel_xz(dx, dy, dz, distance) + kyz[j] = kernel_yz(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + uxx = kernels_in_nodes_to_cell(kxx, nodes_indices) + uyy = kernels_in_nodes_to_cell(kyy, nodes_indices) + uzz = kernels_in_nodes_to_cell(kzz, nodes_indices) + uxy = kernels_in_nodes_to_cell(kxy, nodes_indices) + uxz = kernels_in_nodes_to_cell(kxz, nodes_indices) + uyz = kernels_in_nodes_to_cell(kyz, nodes_indices) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + # Fill the sensitivity matrix element(s) that correspond to the + # current active cell + if scalar_model: + sensitivity_matrix[i, k] = ( + constant_factor + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + else: + sensitivity_matrix[i, k] = ( + constant_factor * regional_field_amplitude * bx + ) + sensitivity_matrix[i, k + n_cells] = ( + constant_factor * regional_field_amplitude * by + ) + sensitivity_matrix[i, k + 2 * n_cells] = ( + constant_factor * regional_field_amplitude * bz + ) + + def _forward_mag( receivers, nodes, @@ -649,6 +834,175 @@ def _forward_tmi( ) +def _forward_tmi_derivative( + receivers, + nodes, + model, + fields, + cell_nodes, + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + constant_factor, + scalar_model, +): + r""" + Forward model a TMI derivative. + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_forward = jit(nopython=True, parallel=True)(_forward_tmi_derivative) + + Parameters + ---------- + receivers : (n_receivers, 3) array + Array with the locations of the receivers + nodes : (n_active_nodes, 3) array + Array with the location of the mesh nodes. + model : (n_active_cells) or (3 * n_active_cells) + Array with the susceptibility (scalar model) or the effective + susceptibility (vector model) of each active cell in the mesh. + If the model is scalar, the ``model`` array should have + ``n_active_cells`` elements and ``scalar_model`` should be True. + If the model is vector, the ``model`` array should have + ``3 * n_active_cells`` elements and ``scalar_model`` should be False. + fields : (n_receivers) array + Array full of zeros where the TMI derivative on each receiver will be + stored. This could be a preallocated array or a slice of it. + cell_nodes : (n_active_cells, 8) array + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz : callables + Kernel functions used for computing the desired TMI derivative. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the sensitivity matrix is build to work with scalar models + (susceptibilities). + If False, the sensitivity matrix is build to work with vector models + (effective susceptibilities). + + Notes + ----- + + About the kernel functions + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + + To compute the :math:`\alpha` derivative of the TMI :math:`\Delta T` (with + :math:`\alpha \in \{x, y, z\}` we need to evaluate third order kernels + functions for the prism. The kernels we need to evaluate can be obtained by + fixing one of the subindices to the direction of the derivative + (:math:`\alpha`) and cycle through combinations of the other two. + + For ``tmi_x`` we need to pass: + + .. code:: + + kernel_xx=kernel_eee, kernel_yy=kernel_enn, kernel_zz=kernel_euu, + kernel_xy=kernel_een, kernel_xz=kernel_eeu, kernel_yz=kernel_enu + + For ``tmi_y`` we need to pass: + + .. code:: + + kernel_xx=kernel_een, kernel_yy=kernel_nnn, kernel_zz=kernel_nuu, + kernel_xy=kernel_enn, kernel_xz=kernel_enu, kernel_yz=kernel_nnu + + For ``tmi_z`` we need to pass: + + .. code:: + + kernel_xx=kernel_eeu, kernel_yy=kernel_nnu, kernel_zz=kernel_uuu, + kernel_xy=kernel_enu, kernel_xz=kernel_euu, kernel_yz=kernel_nuu + + + About the model array + ^^^^^^^^^^^^^^^^^^^^^ + + The ``model`` must always be a 1d array: + + * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with + the same number of elements as active cells in the mesh. It should store + the magnetic susceptibilities of each active cell in SI units. + * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array + with a number of elements equal to three times the active cells in the + mesh. It should store the components of the magnetization vector of each + active cell in :math:`Am^{-1}`. The order in which the components should + be passed are: + * every _easting_ component of each active cell, + * then every _northing_ component of each active cell, + * and finally every _upward_ component of each active cell. + + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kxx, kyy, kzz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + kxy, kxz, kyz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kxx[j] = kernel_xx(dx, dy, dz, distance) + kyy[j] = kernel_yy(dx, dy, dz, distance) + kzz[j] = kernel_zz(dx, dy, dz, distance) + kxy[j] = kernel_xy(dx, dy, dz, distance) + kxz[j] = kernel_xz(dx, dy, dz, distance) + kyz[j] = kernel_yz(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + uxx = kernels_in_nodes_to_cell(kxx, nodes_indices) + uyy = kernels_in_nodes_to_cell(kyy, nodes_indices) + uzz = kernels_in_nodes_to_cell(kzz, nodes_indices) + uxy = kernels_in_nodes_to_cell(kxy, nodes_indices) + uxz = kernels_in_nodes_to_cell(kxz, nodes_indices) + uyz = kernels_in_nodes_to_cell(kyz, nodes_indices) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + if scalar_model: + fields[i] += ( + constant_factor + * model[k] + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + else: + fields[i] += ( + constant_factor + * regional_field_amplitude + * ( + bx * model[k] + + by * model[k + n_cells] + + bz * model[k + 2 * n_cells] + ) + ) + + _sensitivity_tmi_serial = jit(nopython=True, parallel=False)(_sensitivity_tmi) _sensitivity_tmi_parallel = jit(nopython=True, parallel=True)(_sensitivity_tmi) _forward_tmi_serial = jit(nopython=True, parallel=False)(_forward_tmi) @@ -657,3 +1011,15 @@ def _forward_tmi( _forward_mag_parallel = jit(nopython=True, parallel=True)(_forward_mag) _sensitivity_mag_serial = jit(nopython=True, parallel=False)(_sensitivity_mag) _sensitivity_mag_parallel = jit(nopython=True, parallel=True)(_sensitivity_mag) +_forward_tmi_derivative_parallel = jit(nopython=True, parallel=True)( + _forward_tmi_derivative +) +_forward_tmi_derivative_serial = jit(nopython=True, parallel=False)( + _forward_tmi_derivative +) +_sensitivity_tmi_derivative_parallel = jit(nopython=True, parallel=True)( + _sensitivity_tmi_derivative +) +_sensitivity_tmi_derivative_serial = jit(nopython=True, parallel=False)( + _sensitivity_tmi_derivative +) diff --git a/simpeg/potential_fields/magnetics/simulation.py b/simpeg/potential_fields/magnetics/simulation.py index 64cf9eba12..77777f01e9 100644 --- a/simpeg/potential_fields/magnetics/simulation.py +++ b/simpeg/potential_fields/magnetics/simulation.py @@ -31,6 +31,10 @@ _forward_tmi_serial, _forward_mag_parallel, _forward_mag_serial, + _forward_tmi_derivative_parallel, + _forward_tmi_derivative_serial, + _sensitivity_tmi_derivative_parallel, + _sensitivity_tmi_derivative_serial, ) if choclo is not None: @@ -45,6 +49,9 @@ "bxy", "bxz", "byz", + "tmi_x", + "tmi_y", + "tmi_z", } CHOCLO_KERNELS = { "bx": (choclo.prism.kernel_ee, choclo.prism.kernel_en, choclo.prism.kernel_eu), @@ -80,6 +87,30 @@ choclo.prism.kernel_nnu, choclo.prism.kernel_nuu, ), + "tmi_x": ( + choclo.prism.kernel_eee, + choclo.prism.kernel_enn, + choclo.prism.kernel_euu, + choclo.prism.kernel_een, + choclo.prism.kernel_eeu, + choclo.prism.kernel_enu, + ), + "tmi_y": ( + choclo.prism.kernel_een, + choclo.prism.kernel_nnn, + choclo.prism.kernel_nuu, + choclo.prism.kernel_enn, + choclo.prism.kernel_enu, + choclo.prism.kernel_nnu, + ), + "tmi_z": ( + choclo.prism.kernel_eeu, + choclo.prism.kernel_nnu, + choclo.prism.kernel_uuu, + choclo.prism.kernel_enu, + choclo.prism.kernel_euu, + choclo.prism.kernel_nuu, + ), } @@ -172,11 +203,15 @@ def __init__( self._sensitivity_mag = _sensitivity_mag_parallel self._forward_tmi = _forward_tmi_parallel self._forward_mag = _forward_mag_parallel + self._forward_tmi_derivative = _forward_tmi_derivative_parallel + self._sensitivity_tmi_derivative = _sensitivity_tmi_derivative_parallel else: self._sensitivity_tmi = _sensitivity_tmi_serial self._sensitivity_mag = _sensitivity_mag_serial self._forward_tmi = _forward_tmi_serial self._forward_mag = _forward_mag_serial + self._forward_tmi_derivative = _forward_tmi_derivative_serial + self._sensitivity_tmi_derivative = _sensitivity_tmi_derivative_serial @property def model_type(self): @@ -684,6 +719,26 @@ def _forward(self, model): constant_factor, scalar_model, ) + elif component in ("tmi_x", "tmi_y", "tmi_z"): + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz = ( + CHOCLO_KERNELS[component] + ) + self._forward_tmi_derivative( + receivers, + active_nodes, + model, + fields[vector_slice], + active_cell_nodes, + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + constant_factor, + scalar_model, + ) else: kernel_x, kernel_y, kernel_z = CHOCLO_KERNELS[component] self._forward_mag( @@ -757,6 +812,25 @@ def _sensitivity_matrix(self): constant_factor, scalar_model, ) + elif component in ("tmi_x", "tmi_y", "tmi_z"): + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz = ( + CHOCLO_KERNELS[component] + ) + self._sensitivity_tmi_derivative( + receivers, + active_nodes, + sensitivity_matrix[matrix_slice, :], + active_cell_nodes, + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + constant_factor, + scalar_model, + ) else: kernel_x, kernel_y, kernel_z = CHOCLO_KERNELS[component] self._sensitivity_mag( diff --git a/tests/pf/test_forward_Mag_Linear.py b/tests/pf/test_forward_Mag_Linear.py index b153ba4a1c..b4050a8085 100644 --- a/tests/pf/test_forward_Mag_Linear.py +++ b/tests/pf/test_forward_Mag_Linear.py @@ -1,3 +1,4 @@ +from __future__ import annotations import discretize import numpy as np import pytest @@ -100,7 +101,6 @@ def create_mag_survey( mag.Survey a magnetic Survey instance """ - receivers = mag.Point(receiver_locations, components=components) strenght, inclination, declination = inducing_field_params source_field = mag.UniformBackgroundField( @@ -112,6 +112,26 @@ def create_mag_survey( return mag.Survey(source_field) +def get_shifted_locations( + receiver_locations: np.ndarray, delta: float, direction: str +) -> tuple[np.ndarray, np.ndarray]: + """ + Shift the locations of receivers along a particular direction. + """ + if direction == "x": + index = 0 + elif direction == "y": + index = 1 + elif direction == "z": + index = 2 + else: + raise ValueError(f"Invalid direction '{direction}'") + plus, minus = receiver_locations.copy(), receiver_locations.copy() + plus[:, index] += delta / 2.0 + minus[:, index] -= delta / 2.0 + return plus, minus + + class TestsMagSimulation: """ Test mag simulation against the analytic solutions single prisms @@ -138,7 +158,10 @@ def mag_mesh(self) -> discretize.TensorMesh: @pytest.fixture def two_blocks(self) -> tuple[np.ndarray, np.ndarray]: """ - The parameters defining two blocks + The parameters defining two blocks. + + The boundaries of the prism should match nodes in the mesh, otherwise + these blocks won't be exactly represented in the mesh model. Returns ------- @@ -146,8 +169,8 @@ def two_blocks(self) -> tuple[np.ndarray, np.ndarray]: Tuple of (3, 2) arrays of (xmin, xmax), (ymin, ymax), (zmin, zmax) dimensions of each block. """ - block1 = np.array([[-1.5, 1.5], [-1.5, 1.5], [-1.5, 1.5]]) - block2 = np.array([[-0.7, 0.7], [-0.7, 0.7], [-0.7, 0.7]]) + block1 = np.array([[-2.5, 0.5], [-3.1, 1.3], [-3.7, 1.5]]) + block2 = np.array([[0.7, 1.9], [-0.7, 2.7], [-1.7, 0.7]]) return block1, block2 @pytest.fixture @@ -246,21 +269,18 @@ def test_magnetic_field_and_tmi_w_susceptibility( # Compute analytical response from magnetic prism block1, block2 = two_blocks prism_1 = MagneticPrism(block1[:, 0], block1[:, 1], chi1 * b0 / mu_0) - prism_2 = MagneticPrism(block2[:, 0], block2[:, 1], -chi1 * b0 / mu_0) - prism_3 = MagneticPrism(block2[:, 0], block2[:, 1], chi2 * b0 / mu_0) + prism_2 = MagneticPrism(block2[:, 0], block2[:, 1], chi2 * b0 / mu_0) - d = ( - prism_1.magnetic_flux_density(receiver_locations) - + prism_2.magnetic_flux_density(receiver_locations) - + prism_3.magnetic_flux_density(receiver_locations) - ) + d = prism_1.magnetic_flux_density( + receiver_locations + ) + prism_2.magnetic_flux_density(receiver_locations) # TMI projection tmi = sim.tmi_projection d_t2 = d_x * tmi[0] + d_y * tmi[1] + d_z * tmi[2] # Check results - rtol, atol = 1e-7, 1e-6 + rtol, atol = 2e-6, 1e-6 np.testing.assert_allclose( d_t, d_t2, rtol=rtol, atol=atol ) # double check internal projection @@ -328,13 +348,11 @@ def test_magnetic_gradiometry_w_susceptibility( # Compute analytical response from magnetic prism block1, block2 = two_blocks prism_1 = MagneticPrism(block1[:, 0], block1[:, 1], chi1 * b0 / mu_0) - prism_2 = MagneticPrism(block2[:, 0], block2[:, 1], -chi1 * b0 / mu_0) - prism_3 = MagneticPrism(block2[:, 0], block2[:, 1], chi2 * b0 / mu_0) + prism_2 = MagneticPrism(block2[:, 0], block2[:, 1], chi2 * b0 / mu_0) d = ( prism_1.magnetic_field_gradient(receiver_locations) + prism_2.magnetic_field_gradient(receiver_locations) - + prism_3.magnetic_field_gradient(receiver_locations) ) * mu_0 # Check results @@ -346,6 +364,85 @@ def test_magnetic_gradiometry_w_susceptibility( np.testing.assert_allclose(d_yz, d[..., 1, 2], rtol=rtol, atol=atol) np.testing.assert_allclose(d_zz, d[..., 2, 2], rtol=rtol, atol=atol) + @pytest.mark.parametrize( + "engine, parallel_kwargs", + [ + ("geoana", {"n_processes": None}), + ("geoana", {"n_processes": 1}), + ("choclo", {"numba_parallel": False}), + ("choclo", {"numba_parallel": True}), + ], + ids=["geoana_serial", "geoana_parallel", "choclo_serial", "choclo_parallel"], + ) + @pytest.mark.parametrize("store_sensitivities", ("ram", "disk", "forward_only")) + def test_tmi_derivatives_w_susceptibility( + self, + engine, + parallel_kwargs, + store_sensitivities, + tmp_path, + mag_mesh, + two_blocks, + receiver_locations, + inducing_field, + ): + """ + Test TMI derivatives (with susceptibility as model) + """ + (h0_amplitude, h0_inclination, h0_declination), b0 = inducing_field + chi1 = 0.01 + chi2 = 0.02 + model, active_cells = create_block_model(mag_mesh, two_blocks, (chi1, chi2)) + model_reduced = model[active_cells] + # Create reduced identity map for Linear Problem + identity_map = maps.IdentityMap(nP=int(sum(active_cells))) + + components = ["tmi_x", "tmi_y", "tmi_z"] + survey = create_mag_survey( + components=components, + receiver_locations=receiver_locations, + inducing_field_params=(h0_amplitude, h0_inclination, h0_declination), + ) + sim = mag.Simulation3DIntegral( + mag_mesh, + survey=survey, + chiMap=identity_map, + active_cells=active_cells, + sensitivity_path=str(tmp_path / f"{engine}"), + store_sensitivities=store_sensitivities, + engine=engine, + **parallel_kwargs, + ) + data = sim.dpred(model_reduced).reshape(-1, len(components)) + tmi_x = data[:, 0] + tmi_y = data[:, 1] + tmi_z = data[:, 2] + + # Compute analytical response from magnetic prism + block1, block2 = two_blocks + prism_1 = MagneticPrism(block1[:, 0], block1[:, 1], chi1 * b0 / mu_0) + prism_2 = MagneticPrism(block2[:, 0], block2[:, 1], chi2 * b0 / mu_0) + + d = ( + prism_1.magnetic_field_gradient(receiver_locations) + + prism_2.magnetic_field_gradient(receiver_locations) + ) * mu_0 + + # Check results + rtol, atol = 5e-7, 1e-6 + expected_tmi_x = ( + d[:, 0, 0] * b0[0] + d[:, 0, 1] * b0[1] + d[:, 0, 2] * b0[2] + ) / h0_amplitude + expected_tmi_y = ( + d[:, 1, 0] * b0[0] + d[:, 1, 1] * b0[1] + d[:, 1, 2] * b0[2] + ) / h0_amplitude + expected_tmi_z = ( + d[:, 2, 0] * b0[0] + d[:, 2, 1] * b0[1] + d[:, 2, 2] * b0[2] + ) / h0_amplitude + np.testing.assert_allclose(tmi_x, expected_tmi_x, rtol=rtol, atol=atol) + np.testing.assert_allclose(tmi_y, expected_tmi_y, rtol=rtol, atol=atol) + np.testing.assert_allclose(tmi_z, expected_tmi_z, rtol=rtol, atol=atol) + @pytest.mark.parametrize( "engine, parallel_kwargs", [ @@ -406,17 +503,12 @@ def test_magnetic_vector_and_tmi_w_magnetization( block1[:, 0], block1[:, 1], M1 * np.linalg.norm(b0) / mu_0 ) prism_2 = MagneticPrism( - block2[:, 0], block2[:, 1], -M1 * np.linalg.norm(b0) / mu_0 - ) - prism_3 = MagneticPrism( block2[:, 0], block2[:, 1], M2 * np.linalg.norm(b0) / mu_0 ) - d = ( - prism_1.magnetic_flux_density(receiver_locations) - + prism_2.magnetic_flux_density(receiver_locations) - + prism_3.magnetic_flux_density(receiver_locations) - ) + d = prism_1.magnetic_flux_density( + receiver_locations + ) + prism_2.magnetic_flux_density(receiver_locations) tmi = sim.tmi_projection # Check results @@ -426,6 +518,93 @@ def test_magnetic_vector_and_tmi_w_magnetization( np.testing.assert_allclose(data[:, 2], d[:, 2], rtol=rtol, atol=atol) np.testing.assert_allclose(data[:, 3], d @ tmi, rtol=rtol, atol=atol) + @pytest.mark.parametrize( + "engine, parallel_kwargs", + [ + ("geoana", {"n_processes": None}), + ("geoana", {"n_processes": 1}), + ("choclo", {"numba_parallel": False}), + ("choclo", {"numba_parallel": True}), + ], + ids=["geoana_serial", "geoana_parallel", "choclo_serial", "choclo_parallel"], + ) + @pytest.mark.parametrize("store_sensitivities", ("ram", "disk", "forward_only")) + def test_tmi_derivatives_w_magnetization( + self, + engine, + parallel_kwargs, + store_sensitivities, + tmp_path, + mag_mesh, + two_blocks, + receiver_locations, + inducing_field, + ): + """ + Test TMI derivatives (using magnetization vectors as model) + """ + (h0_amplitude, h0_inclination, h0_declination), b0 = inducing_field + M1 = (utils.mat_utils.dip_azimuth2cartesian(45, -40) * 0.05).squeeze() + M2 = (utils.mat_utils.dip_azimuth2cartesian(120, 32) * 0.1).squeeze() + + model, active_cells = create_block_model(mag_mesh, two_blocks, (M1, M2)) + model_reduced = model[active_cells].reshape(-1, order="F") + # Create reduced identity map for Linear Problem + identity_map = maps.IdentityMap(nP=int(sum(active_cells)) * 3) + + components = ["tmi_x", "tmi_y", "tmi_z"] + survey = create_mag_survey( + components=components, + receiver_locations=receiver_locations, + inducing_field_params=(h0_amplitude, h0_inclination, h0_declination), + ) + + sim = mag.Simulation3DIntegral( + mag_mesh, + survey=survey, + chiMap=identity_map, + active_cells=active_cells, + sensitivity_path=str(tmp_path / f"{engine}"), + store_sensitivities=store_sensitivities, + model_type="vector", + engine=engine, + **parallel_kwargs, + ) + + data = sim.dpred(model_reduced).reshape(-1, len(components)) + tmi_x = data[:, 0] + tmi_y = data[:, 1] + tmi_z = data[:, 2] + + # Compute analytical response from magnetic prism + block1, block2 = two_blocks + prism_1 = MagneticPrism( + block1[:, 0], block1[:, 1], M1 * np.linalg.norm(b0) / mu_0 + ) + prism_2 = MagneticPrism( + block2[:, 0], block2[:, 1], M2 * np.linalg.norm(b0) / mu_0 + ) + + d = ( + prism_1.magnetic_field_gradient(receiver_locations) + + prism_2.magnetic_field_gradient(receiver_locations) + ) * mu_0 + + # Check results + rtol, atol = 5e-7, 1e-6 + expected_tmi_x = ( + d[:, 0, 0] * b0[0] + d[:, 0, 1] * b0[1] + d[:, 0, 2] * b0[2] + ) / h0_amplitude + expected_tmi_y = ( + d[:, 1, 0] * b0[0] + d[:, 1, 1] * b0[1] + d[:, 1, 2] * b0[2] + ) / h0_amplitude + expected_tmi_z = ( + d[:, 2, 0] * b0[0] + d[:, 2, 1] * b0[1] + d[:, 2, 2] * b0[2] + ) / h0_amplitude + np.testing.assert_allclose(tmi_x, expected_tmi_x, rtol=rtol, atol=atol) + np.testing.assert_allclose(tmi_y, expected_tmi_y, rtol=rtol, atol=atol) + np.testing.assert_allclose(tmi_z, expected_tmi_z, rtol=rtol, atol=atol) + @pytest.mark.parametrize( "engine, parallel_kwargs", [ @@ -487,23 +666,85 @@ def test_magnetic_field_amplitude_w_magnetization( block1[:, 0], block1[:, 1], M1 * np.linalg.norm(b0) / mu_0 ) prism_2 = MagneticPrism( - block2[:, 0], block2[:, 1], -M1 * np.linalg.norm(b0) / mu_0 - ) - prism_3 = MagneticPrism( block2[:, 0], block2[:, 1], M2 * np.linalg.norm(b0) / mu_0 ) - d = ( - prism_1.magnetic_flux_density(receiver_locations) - + prism_2.magnetic_flux_density(receiver_locations) - + prism_3.magnetic_flux_density(receiver_locations) - ) + d = prism_1.magnetic_flux_density( + receiver_locations + ) + prism_2.magnetic_flux_density(receiver_locations) d_amp = np.linalg.norm(d, axis=1) # Check results rtol, atol = 5e-7, 1e-6 np.testing.assert_allclose(data, d_amp, rtol=rtol, atol=atol) + @pytest.mark.parametrize("engine", ("geoana", "choclo")) + @pytest.mark.parametrize("direction", ("x", "y", "z")) + def test_tmi_derivatives_finite_diff( + self, + engine, + direction, + mag_mesh, + two_blocks, + receiver_locations, + inducing_field, + ): + """ + Test tmi derivatives against finite differences. + + Use float64 elements in the sensitivity matrix to avoid numerical + instabilities due to small values of delta. + """ + # Get inducing field and two blocks model + inducing_field_params, b0 = inducing_field + chi1, chi2 = 0.01, 0.02 + model, active_cells = create_block_model(mag_mesh, two_blocks, (chi1, chi2)) + model_reduced = model[active_cells] + identity_map = maps.IdentityMap(nP=int(sum(active_cells))) + # Create survey to compute tmi derivative through analytic solution + survey = create_mag_survey( + components=f"tmi_{direction}", + receiver_locations=receiver_locations, + inducing_field_params=inducing_field_params, + ) + kwargs = dict( + chiMap=identity_map, + active_cells=active_cells, + engine=engine, + sensitivity_dtype=np.float64, + ) + simulation = mag.Simulation3DIntegral( + mag_mesh, + survey=survey, + **kwargs, + ) + # Create shifted surveys to compute tmi derivatives through finite differences + delta = 1e-6 + shifted_surveys = [ + create_mag_survey( + components="tmi", + receiver_locations=shifted_locations, + inducing_field_params=inducing_field_params, + ) + for shifted_locations in get_shifted_locations( + receiver_locations, delta, direction + ) + ] + simulations_tmi = [ + mag.Simulation3DIntegral(mag_mesh, survey=shifted_survey, **kwargs) + for shifted_survey in shifted_surveys + ] + # Compute tmi derivatives + tmi_derivative = simulation.dpred(model_reduced) + # Compute tmi derivatives with finite differences + tmis = [sim.dpred(model_reduced) for sim in simulations_tmi] + tmi_derivative_finite_diff = (tmis[0] - tmis[1]) / delta + # Compare results + rtol, atol = 1e-6, 5e-6 + np.testing.assert_allclose( + tmi_derivative, tmi_derivative_finite_diff, rtol=rtol, atol=atol + ) + @pytest.mark.parametrize("engine", ("choclo", "geoana")) @pytest.mark.parametrize("store_sensitivities", ("ram", "disk", "forward_only")) def test_sensitivity_dtype( @@ -646,128 +887,6 @@ def test_choclo_missing(self, mag_mesh, monkeypatch): mag.Simulation3DIntegral(mag_mesh, engine="choclo") -def test_ana_mag_tmi_grad_forward(): - """ - Test TMI gradiometry using susceptibilities as model - """ - nx = 61 - ny = 61 - - h0_amplitude, h0_inclination, h0_declination = (50000.0, 60.0, 250.0) - b0 = mag.analytics.IDTtoxyz(-h0_inclination, h0_declination, h0_amplitude) - chi1 = 0.01 - chi2 = 0.02 - - # Define a mesh - cs = 0.2 - hxind = [(cs, 41)] - hyind = [(cs, 41)] - hzind = [(cs, 41)] - mesh = discretize.TensorMesh([hxind, hyind, hzind], "CCC") - - # create a model of two blocks, 1 inside the other - block1 = np.array([[-1.5, 1.5], [-1.5, 1.5], [-1.5, 1.5]]) - block2 = np.array([[-0.7, 0.7], [-0.7, 0.7], [-0.7, 0.7]]) - - def get_block_inds(grid, block): - return np.where( - (grid[:, 0] > block[0, 0]) - & (grid[:, 0] < block[0, 1]) - & (grid[:, 1] > block[1, 0]) - & (grid[:, 1] < block[1, 1]) - & (grid[:, 2] > block[2, 0]) - & (grid[:, 2] < block[2, 1]) - ) - - block1_inds = get_block_inds(mesh.cell_centers, block1) - block2_inds = get_block_inds(mesh.cell_centers, block2) - - model = np.zeros(mesh.n_cells) - model[block1_inds] = chi1 - model[block2_inds] = chi2 - - active_cells = model != 0.0 - model_reduced = model[active_cells] - - # Create reduced identity map for Linear Problem - idenMap = maps.IdentityMap(nP=int(sum(active_cells))) - - # Create plane of observations - xr = np.linspace(-20, 20, nx) - dxr = xr[1] - xr[0] - yr = np.linspace(-20, 20, ny) - dyr = yr[1] - yr[0] - X, Y = np.meshgrid(xr, yr) - Z = np.ones_like(X) * 3.0 - locXyz = np.c_[X.reshape(-1), Y.reshape(-1), Z.reshape(-1)] - components = ["tmi", "tmi_x", "tmi_y", "tmi_z"] - - rxLoc = mag.Point(locXyz, components=components) - srcField = mag.UniformBackgroundField( - receiver_list=[rxLoc], - amplitude=h0_amplitude, - inclination=h0_inclination, - declination=h0_declination, - ) - survey = mag.Survey(srcField) - - # Create reduced identity map for Linear Problem - idenMap = maps.IdentityMap(nP=int(sum(active_cells))) - - sim = mag.Simulation3DIntegral( - mesh, - survey=survey, - chiMap=idenMap, - active_cells=active_cells, - store_sensitivities="forward_only", - n_processes=None, - ) - - data = sim.dpred(model_reduced) - tmi = data[0::4] - d_x = data[1::4] - d_y = data[2::4] - d_z = data[3::4] - - # Compute analytical response from magnetic prism - prism_1 = MagneticPrism(block1[:, 0], block1[:, 1], chi1 * b0 / mu_0) - prism_2 = MagneticPrism(block2[:, 0], block2[:, 1], -chi1 * b0 / mu_0) - prism_3 = MagneticPrism(block2[:, 0], block2[:, 1], chi2 * b0 / mu_0) - - d = ( - prism_1.magnetic_field_gradient(locXyz) - + prism_2.magnetic_field_gradient(locXyz) - + prism_3.magnetic_field_gradient(locXyz) - ) * mu_0 - tmi_x = ( - d[:, 0, 0] * b0[0] + d[:, 0, 1] * b0[1] + d[:, 0, 2] * b0[2] - ) / h0_amplitude - tmi_y = ( - d[:, 1, 0] * b0[0] + d[:, 1, 1] * b0[1] + d[:, 1, 2] * b0[2] - ) / h0_amplitude - tmi_z = ( - d[:, 2, 0] * b0[0] + d[:, 2, 1] * b0[1] + d[:, 2, 2] * b0[2] - ) / h0_amplitude - np.testing.assert_allclose(d_x, tmi_x, rtol=1e-10, atol=1e-12) - np.testing.assert_allclose(d_y, tmi_y, rtol=1e-10, atol=1e-12) - np.testing.assert_allclose(d_z, tmi_z, rtol=1e-10, atol=1e-12) - - # finite difference test y-grad - np.testing.assert_allclose( - np.diff(tmi.reshape(nx, ny, order="F")[:, ::2], axis=1) / (2 * dyr), - tmi_y.reshape(nx, ny, order="F")[:, 1::2], - atol=1.0, - rtol=1e-1, - ) - # finite difference test x-grad - np.testing.assert_allclose( - np.diff(tmi.reshape(nx, ny, order="F")[::2, :], axis=0) / (2 * dxr), - tmi_x.reshape(nx, ny, order="F")[1::2, :], - atol=1.0, - rtol=1e-1, - ) - - class TestInvalidMeshChoclo: @pytest.fixture(params=("tensormesh", "treemesh")) def mesh(self, request): From c88de7fdd900b3fc4530e1b61fb2c0a5d24eb7f6 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 23 Oct 2024 09:47:04 -0700 Subject: [PATCH 076/194] Implement magnetic eq sources with Choclo (#1552) Implement magnetic equivalent sources using Choclo as the engine. Allow magnetic equivalent sources to take ``engine="choclo"``. Implement new Numba-based forward and sensitivity matrix functions that use Choclo's forward modelling functions for a prism. These new functions can take a 2D mesh and the arrays for the top and bottom boundaries for each cell and compute the forward or build the sensitivity matrix by iterating over each prism in the layer. Add extended tests for the new implementation that also extend the tests for the geoana-based implementation. --- simpeg/potential_fields/_numba_utils.py | 108 ++ .../magnetics/_numba_functions.py | 958 +++++++++++++++++- .../potential_fields/magnetics/simulation.py | 243 ++++- tests/pf/test_equivalent_sources.py | 408 ++++++-- 4 files changed, 1649 insertions(+), 68 deletions(-) diff --git a/simpeg/potential_fields/_numba_utils.py b/simpeg/potential_fields/_numba_utils.py index 2bdea6da2a..331cbb9e9d 100644 --- a/simpeg/potential_fields/_numba_utils.py +++ b/simpeg/potential_fields/_numba_utils.py @@ -5,6 +5,8 @@ magnetic simulations. """ +import numpy as np + try: from numba import jit except ImportError: @@ -41,3 +43,109 @@ def kernels_in_nodes_to_cell(kernels, nodes_indices): + kernels[nodes_indices[7]] ) return result + + +@jit(nopython=True) +def evaluate_kernels_on_cell( + easting, + northing, + upward, + prism_west, + prism_east, + prism_south, + prism_north, + prism_bottom, + prism_top, + kernel_x, + kernel_y, + kernel_z, +): + r""" + Evaluate three kernel functions on every shifted vertex of a prism. + + .. note:: + + This function was inspired in the ``_evaluate_kernel`` function in + Choclo (released under BSD 3-clause Licence): + https://www.fatiando.org/choclo + + Parameters + ---------- + easting, northing, upward : float + Easting, northing and upward coordinates of the observation point. Must + be in meters. + prism_west, prism_east : floats + The West and East boundaries of the prism. Must be in meters. + prism_south, prism_north : floats + The South and North boundaries of the prism. Must be in meters. + prism_bottom, prism_top : floats + The bottom and top boundaries of the prism. Must be in meters. + kernel_x, kernel_y, kernel_z : callable + Kernel functions that will be evaluated on each one of the shifted + vertices of the prism. + + Returns + ------- + result_x, result_y, result_z : floats + Evaluation of the kernel functions on each one of the vertices of the + prism. + + Notes + ----- + This function evaluates each numerical kernel :math:`k(x, y, z)` on each one + of the vertices of the prism: + + .. math:: + + v(\mathbf{p}) = + \Bigg\lvert \Bigg\lvert \Bigg\lvert + k(x, y, z) + \Bigg\rvert_{X_1}^{X_2} + \Bigg\rvert_{Y_1}^{Y_2} + \Bigg\rvert_{Z_1}^{Z_2} + + where :math:`X_1`, :math:`X_2`, :math:`Y_1`, :math:`Y_2`, :math:`Z_1` and + :math:`Z_2` are boundaries of the rectangular prism in the *shifted + coordinates* defined by the Cartesian coordinate system with its origin + located on the observation point :math:`\mathbf{p}`. + """ + # Initialize result floats to zero + result_x, result_y, result_z = 0, 0, 0 + # Iterate over the vertices of the prism + for i in range(2): + # Compute shifted easting coordinate + if i == 0: + shift_east = prism_east - easting + else: + shift_east = prism_west - easting + shift_east_sq = shift_east**2 + for j in range(2): + # Compute shifted northing coordinate + if j == 0: + shift_north = prism_north - northing + else: + shift_north = prism_south - northing + shift_north_sq = shift_north**2 + for k in range(2): + # Compute shifted upward coordinate + if k == 0: + shift_upward = prism_top - upward + else: + shift_upward = prism_bottom - upward + shift_upward_sq = shift_upward**2 + # Compute the radius + radius = np.sqrt(shift_east_sq + shift_north_sq + shift_upward_sq) + # If i, j or k is 1, the corresponding shifted + # coordinate will refer to the lower boundary, + # meaning the corresponding term should have a minus + # sign. + result_x += (-1) ** (i + j + k) * kernel_x( + shift_east, shift_north, shift_upward, radius + ) + result_y += (-1) ** (i + j + k) * kernel_y( + shift_east, shift_north, shift_upward, radius + ) + result_z += (-1) ** (i + j + k) * kernel_z( + shift_east, shift_north, shift_upward, radius + ) + return result_x, result_y, result_z diff --git a/simpeg/potential_fields/magnetics/_numba_functions.py b/simpeg/potential_fields/magnetics/_numba_functions.py index 611e96cf50..dfda671459 100644 --- a/simpeg/potential_fields/magnetics/_numba_functions.py +++ b/simpeg/potential_fields/magnetics/_numba_functions.py @@ -15,7 +15,7 @@ def jit(*args, **kwargs): else: from numba import jit, prange -from .._numba_utils import kernels_in_nodes_to_cell +from .._numba_utils import kernels_in_nodes_to_cell, evaluate_kernels_on_cell def _sensitivity_mag( @@ -1003,6 +1003,934 @@ def _forward_tmi_derivative( ) +def _forward_mag_2d_mesh( + receivers, + cells_bounds, + top, + bottom, + model, + fields, + regional_field, + forward_func, + scalar_model, +): + """ + Forward model single magnetic component for 2D meshes. + + This function is designed to be used with equivalent sources, where the + mesh is a 2D mesh (prism layer). The top and bottom boundaries of each cell + are passed through the ``top`` and ``bottom`` arrays. + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_forward = jit(nopython=True, parallel=True)(_forward_mag_2d_mesh) + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + model : (n_active_cells) or (3 * n_active_cells) array + Array containing the susceptibilities (scalar) or effective + susceptibilities (vector) of the active cells in the mesh, in SI + units. + Susceptibilities are expected if ``scalar_model`` is True, + and the array should have ``n_active_cells`` elements. + Effective susceptibilities are expected if ``scalar_model`` is False, + and the array should have ``3 * n_active_cells`` elements. + fields : (n_receivers) array + Array full of zeros where the magnetic component on each receiver will + be stored. This could be a preallocated array or a slice of it. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + forward_func : callable + Forward function that will be evaluated on each node of the mesh. Choose + one of the forward functions in ``choclo.prism``. + scalar_model : bool + If True, the forward will be computing assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the forward will be computing assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + + Notes + ----- + + About the model array + ^^^^^^^^^^^^^^^^^^^^^ + + The ``model`` must always be a 1d array: + + * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with + the same number of elements as active cells in the mesh. It should store + the magnetic susceptibilities of each active cell in SI units. + * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array + with a number of elements equal to three times the active cells in the + mesh. It should store the components of the magnetization vector of each + active cell in :math:`Am^{-1}`. The order in which the components should + be passed are: + * every _easting_ component of each active cell, + * then every _northing_ component of each active cell, + * and finally every _upward_ component of each active cell. + + """ + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Forward model the magnetic component of each cell on each receiver location + for i in prange(n_receivers): + for j in range(n_cells): + # Define magnetization vector of the cell + # (we we'll divide by mu_0 when adding the forward modelled field) + if scalar_model: + # model is susceptibility, so the vector is parallel to the + # regional field + magnetization_x = model[j] * fx + magnetization_y = model[j] * fy + magnetization_z = model[j] * fz + else: + # model is effective susceptibility (vector) + magnetization_x = model[j] + magnetization_y = model[j + n_cells] + magnetization_z = model[j + 2 * n_cells] + # Forward the magnetic component + fields[i] += ( + regional_field_amplitude + * forward_func( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + magnetization_x, + magnetization_y, + magnetization_z, + ) + / choclo.constants.VACUUM_MAGNETIC_PERMEABILITY + ) + + +def _forward_tmi_2d_mesh( + receivers, + cells_bounds, + top, + bottom, + model, + fields, + regional_field, + scalar_model, +): + """ + Forward model the TMI for 2D meshes. + + This function is designed to be used with equivalent sources, where the + mesh is a 2D mesh (prism layer). The top and bottom boundaries of each cell + are passed through the ``top`` and ``bottom`` arrays. + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_forward = jit(nopython=True, parallel=True)(_forward_tmi_2d_mesh) + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + model : (n_active_cells) or (3 * n_active_cells) + Array with the susceptibility (scalar model) or the effective + susceptibility (vector model) of each active cell in the mesh. + If the model is scalar, the ``model`` array should have + ``n_active_cells`` elements and ``scalar_model`` should be True. + If the model is vector, the ``model`` array should have + ``3 * n_active_cells`` elements and ``scalar_model`` should be False. + fields : (n_receivers) array + Array full of zeros where the TMI on each receiver will be stored. This + could be a preallocated array or a slice of it. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + scalar_model : bool + If True, the sensitivity matrix is build to work with scalar models + (susceptibilities). + If False, the sensitivity matrix is build to work with vector models + (effective susceptibilities). + + Notes + ----- + + The ``model`` must always be a 1d array: + + * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with + the same number of elements as active cells in the mesh. It should store + the magnetic susceptibilities of each active cell in SI units. + * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array + with a number of elements equal to three times the active cells in the + mesh. It should store the components of the magnetization vector of each + active cell in :math:`Am^{-1}`. The order in which the components should + be passed are: + * every _easting_ component of each active cell, + * then every _northing_ component of each active cell, + * and finally every _upward_ component of each active cell. + + """ + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Forward model the magnetic component of each cell on each receiver location + for i in prange(n_receivers): + for j in range(n_cells): + # Define magnetization vector of the cell + # (we we'll divide by mu_0 when adding the forward modelled field) + if scalar_model: + # model is susceptibility, so the vector is parallel to the + # regional field + magnetization_x = model[j] * fx + magnetization_y = model[j] * fy + magnetization_z = model[j] * fz + else: + # model is effective susceptibility (vector) + magnetization_x = model[j] + magnetization_y = model[j + n_cells] + magnetization_z = model[j + 2 * n_cells] + # Forward the magnetic field vector and compute tmi + bx, by, bz = choclo.prism.magnetic_field( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + magnetization_x, + magnetization_y, + magnetization_z, + ) + fields[i] += ( + regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + / choclo.constants.VACUUM_MAGNETIC_PERMEABILITY + ) + + +def _forward_tmi_derivative_2d_mesh( + receivers, + cells_bounds, + top, + bottom, + model, + fields, + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + scalar_model, +): + r""" + Forward model a TMI derivative for 2D meshes. + + This function is designed to be used with equivalent sources, where the + mesh is a 2D mesh (prism layer). The top and bottom boundaries of each cell + are passed through the ``top`` and ``bottom`` arrays. + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_forward = jit(nopython=True, parallel=True)(_forward_tmi_2d_mesh) + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + model : (n_active_cells) or (3 * n_active_cells) + Array with the susceptibility (scalar model) or the effective + susceptibility (vector model) of each active cell in the mesh. + If the model is scalar, the ``model`` array should have + ``n_active_cells`` elements and ``scalar_model`` should be True. + If the model is vector, the ``model`` array should have + ``3 * n_active_cells`` elements and ``scalar_model`` should be False. + fields : (n_receivers) array + Array full of zeros where the TMI on each receiver will be stored. This + could be a preallocated array or a slice of it. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz : callables + Kernel functions used for computing the desired TMI derivative. + scalar_model : bool + If True, the sensitivity matrix is build to work with scalar models + (susceptibilities). + If False, the sensitivity matrix is build to work with vector models + (effective susceptibilities). + + Notes + ----- + + About the kernel functions + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + + To compute the :math:`\alpha` derivative of the TMI :math:`\Delta T` (with + :math:`\alpha \in \{x, y, z\}` we need to evaluate third order kernels + functions for the prism. The kernels we need to evaluate can be obtained by + fixing one of the subindices to the direction of the derivative + (:math:`\alpha`) and cycle through combinations of the other two. + + For ``tmi_x`` we need to pass: + + .. code:: + + kernel_xx=kernel_eee, kernel_yy=kernel_enn, kernel_zz=kernel_euu, + kernel_xy=kernel_een, kernel_xz=kernel_eeu, kernel_yz=kernel_enu + + For ``tmi_y`` we need to pass: + + .. code:: + + kernel_xx=kernel_een, kernel_yy=kernel_nnn, kernel_zz=kernel_nuu, + kernel_xy=kernel_enn, kernel_xz=kernel_enu, kernel_yz=kernel_nnu + + For ``tmi_z`` we need to pass: + + .. code:: + + kernel_xx=kernel_eeu, kernel_yy=kernel_nnu, kernel_zz=kernel_uuu, + kernel_xy=kernel_enu, kernel_xz=kernel_euu, kernel_yz=kernel_nuu + + + About the model array + ^^^^^^^^^^^^^^^^^^^^^ + + The ``model`` must always be a 1d array: + + * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with + the same number of elements as active cells in the mesh. It should store + the magnetic susceptibilities of each active cell in SI units. + * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array + with a number of elements equal to three times the active cells in the + mesh. It should store the components of the magnetization vector of each + active cell in :math:`Am^{-1}`. The order in which the components should + be passed are: + * every _easting_ component of each active cell, + * then every _northing_ component of each active cell, + * and finally every _upward_ component of each active cell. + + """ + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Forward model the magnetic component of each cell on each receiver location + for i in prange(n_receivers): + for j in range(n_cells): + uxx, uyy, uzz = evaluate_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + kernel_xx, + kernel_yy, + kernel_zz, + ) + uxy, uxz, uyz = evaluate_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + kernel_xy, + kernel_xz, + kernel_yz, + ) + if scalar_model: + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + fields[i] += ( + model[j] + * regional_field_amplitude + * (fx * bx + fy * by + fz * bz) + / (4 * np.pi) + ) + else: + model_x = model[j] + model_y = model[j + n_cells] + model_z = model[j + 2 * n_cells] + bx = uxx * model_x + uxy * model_y + uxz * model_z + by = uxy * model_x + uyy * model_y + uyz * model_z + bz = uxz * model_x + uyz * model_y + uzz * model_z + fields[i] += ( + regional_field_amplitude * (bx * fx + by * fy + bz * fz) / 4 / np.pi + ) + + +def _sensitivity_mag_2d_mesh( + receivers, + cells_bounds, + top, + bottom, + sensitivity_matrix, + regional_field, + kernel_x, + kernel_y, + kernel_z, + scalar_model, +): + r""" + Fill the sensitivity matrix for single mag component for 2d meshes. + + This function is designed to be used with equivalent sources, where the + mesh is a 2D mesh (prism layer). The top and bottom boundaries of each cell + are passed through the ``top`` and ``bottom`` arrays. + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_sensitivity = jit(nopython=True, parallel=True)(_sensitivity_mag_2d_mesh) + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + sensitivity_matrix : (n_receivers, n_active_nodes) array + Empty 2d array where the sensitivity matrix elements will be filled. + This could be a preallocated empty array or a slice of it. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_x, kernel_y, kernel_z : callable + Kernels used to compute the desired magnetic component. For example, + for computing bx we need to use ``kernel_x=kernel_ee``, + ``kernel_y=kernel_en``, ``kernel_z=kernel_eu``. + scalar_model : bool + If True, the sensitivity matrix is built to work with scalar models + (susceptibilities). + If False, the sensitivity matrix is built to work with vector models + (effective susceptibilities). + + Notes + ----- + + About the kernel functions + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + + For computing the ``bx`` component of the magnetic field we need to use the + following kernels: + + .. code:: + + kernel_x=kernel_ee, kernel_y=kernel_en, kernel_z=kernel_eu + + + For computing the ``by`` component of the magnetic field we need to use the + following kernels: + + .. code:: + + kernel_x=kernel_en, kernel_y=kernel_nn, kernel_z=kernel_nu + + For computing the ``bz`` component of the magnetic field we need to use the + following kernels: + + .. code:: + + kernel_x=kernel_eu, kernel_y=kernel_nu, kernel_z=kernel_uu + + + About the model array + ^^^^^^^^^^^^^^^^^^^^^ + + The ``model`` must always be a 1d array: + + * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with + the same number of elements as active cells in the mesh. It should store + the magnetic susceptibilities of each active cell in SI units. + * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array + with a number of elements equal to three times the active cells in the + mesh. It should store the components of the magnetization vector of each + active cell in :math:`Am^{-1}`. The order in which the components should + be passed are: + * every _easting_ component of each active cell, + * then every _northing_ component of each active cell, + * and finally every _upward_ component of each active cell. + + About the sensitivity matrix + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + Each row of the sensitivity matrix corresponds to a single receiver + location. + + If ``scalar_model`` is True, then each element of the row will + correspond to the partial derivative of the selected magnetic component + with respect to the susceptibility of each cell in the mesh. + + If ``scalar_model`` is False, then each row can be split in three sections + containing: + + * the partial derivatives of the selected magnetic component with respect + to the _x_ component of the effective susceptibility of each cell; then + * the partial derivatives of the selected magnetic component with respect + to the _y_ component of the effective susceptibility of each cell; and then + * the partial derivatives of the selected magnetic component with respect + to the _z_ component of the effective susceptibility of each cell. + + So, if we call :math:`B_j` the magnetic field component on the receiver + :math:`j`, and :math:`\bar{\chi}^{(i)} = (\chi_x^{(i)}, \chi_y^{(i)}, + \chi_z^{(i)})` the effective susceptibility of the active cell :math:`i`, + then each row of the sensitivity matrix will be: + + .. math:: + + \left[ + \frac{\partial B_j}{\partial \chi_x^{(1)}}, + \dots, + \frac{\partial B_j}{\partial \chi_x^{(N)}}, + \frac{\partial B_j}{\partial \chi_y^{(1)}}, + \dots, + \frac{\partial B_j}{\partial \chi_y^{(N)}}, + \frac{\partial B_j}{\partial \chi_z^{(1)}}, + \dots, + \frac{\partial B_j}{\partial \chi_z^{(N)}} + \right] + + where :math:`N` is the total number of active cells. + """ + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + + constant_factor = 1 / 4 / np.pi + + # Fill the sensitivity matrix + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + for i in prange(n_receivers): + for j in range(n_cells): + # Evaluate kernels for the current cell and receiver + ux, uy, uz = evaluate_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + kernel_x, + kernel_y, + kernel_z, + ) + if scalar_model: + sensitivity_matrix[i, j] = ( + constant_factor + * regional_field_amplitude + * (ux * fx + uy * fy + uz * fz) + ) + else: + sensitivity_matrix[i, j] = ( + constant_factor * regional_field_amplitude * ux + ) + sensitivity_matrix[i, j + n_cells] = ( + constant_factor * regional_field_amplitude * uy + ) + sensitivity_matrix[i, j + 2 * n_cells] = ( + constant_factor * regional_field_amplitude * uz + ) + + +def _sensitivity_tmi_2d_mesh( + receivers, + cells_bounds, + top, + bottom, + sensitivity_matrix, + regional_field, + scalar_model, +): + r""" + Fill the sensitivity matrix TMI for 2d meshes. + + This function is designed to be used with equivalent sources, where the + mesh is a 2D mesh (prism layer). The top and bottom boundaries of each cell + are passed through the ``top`` and ``bottom`` arrays. + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_tmi = jit(nopython=True, parallel=True)(_sensitivity_tmi_2d_mesh) + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + sensitivity_matrix : array + Empty 2d array where the sensitivity matrix elements will be filled. + This could be a preallocated empty array or a slice of it. + The array should have a shape of ``(n_receivers, n_active_nodes)`` + if ``scalar_model`` is True. + The array should have a shape of ``(n_receivers, 3 * n_active_nodes)`` + if ``scalar_model`` is False. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + scalar_model : bool + If True, the sensitivity matrix is build to work with scalar models + (susceptibilities). + If False, the sensitivity matrix is build to work with vector models + (effective susceptibilities). + + Notes + ----- + + About the model array + ^^^^^^^^^^^^^^^^^^^^^ + + The ``model`` must always be a 1d array: + + * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with + the same number of elements as active cells in the mesh. It should store + the magnetic susceptibilities of each active cell in SI units. + * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array + with a number of elements equal to three times the active cells in the + mesh. It should store the components of the magnetization vector of each + active cell in :math:`Am^{-1}`. The order in which the components should + be passed are: + * every _easting_ component of each active cell, + * then every _northing_ component of each active cell, + * and finally every _upward_ component of each active cell. + + About the sensitivity matrix + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + Each row of the sensitivity matrix corresponds to a single receiver + location. + + If ``scalar_model`` is True, then each element of the row will + correspond to the partial derivative of the tmi + with respect to the susceptibility of each cell in the mesh. + + If ``scalar_model`` is False, then each row can be split in three sections + containing: + + * the partial derivatives of the tmi with respect + to the _x_ component of the effective susceptibility of each cell; then + * the partial derivatives of the tmi with respect + to the _y_ component of the effective susceptibility of each cell; and then + * the partial derivatives of the tmi with respect + to the _z_ component of the effective susceptibility of each cell. + + So, if we call :math:`T_j` the tmi on the receiver + :math:`j`, and :math:`\bar{\chi}^{(i)} = (\chi_x^{(i)}, \chi_y^{(i)}, + \chi_z^{(i)})` the effective susceptibility of the active cell :math:`i`, + then each row of the sensitivity matrix will be: + + .. math:: + + \left[ + \frac{\partial T_j}{\partial \chi_x^{(1)}}, + \dots, + \frac{\partial T_j}{\partial \chi_x^{(N)}}, + \frac{\partial T_j}{\partial \chi_y^{(1)}}, + \dots, + \frac{\partial T_j}{\partial \chi_y^{(N)}}, + \frac{\partial T_j}{\partial \chi_z^{(1)}}, + \dots, + \frac{\partial T_j}{\partial \chi_z^{(N)}} + \right] + + where :math:`N` is the total number of active cells. + """ + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + + constant_factor = 1 / 4 / np.pi + + # Fill the sensitivity matrix + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + for i in prange(n_receivers): + for j in range(n_cells): + # Evaluate kernels for the current cell and receiver + uxx, uyy, uzz = evaluate_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + choclo.prism.kernel_ee, + choclo.prism.kernel_nn, + choclo.prism.kernel_uu, + ) + uxy, uxz, uyz = evaluate_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + choclo.prism.kernel_en, + choclo.prism.kernel_eu, + choclo.prism.kernel_nu, + ) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + if scalar_model: + sensitivity_matrix[i, j] = ( + constant_factor + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + else: + sensitivity_matrix[i, j] = ( + constant_factor * regional_field_amplitude * bx + ) + sensitivity_matrix[i, j + n_cells] = ( + constant_factor * regional_field_amplitude * by + ) + sensitivity_matrix[i, j + 2 * n_cells] = ( + constant_factor * regional_field_amplitude * bz + ) + + +def _sensitivity_tmi_derivative_2d_mesh( + receivers, + cells_bounds, + top, + bottom, + sensitivity_matrix, + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + scalar_model, +): + r""" + Fill the sensitivity matrix TMI for 2d meshes. + + This function is designed to be used with equivalent sources, where the + mesh is a 2D mesh (prism layer). The top and bottom boundaries of each cell + are passed through the ``top`` and ``bottom`` arrays. + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_tmi = jit(nopython=True, parallel=True)(_sensitivity_tmi_2d_mesh) + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + sensitivity_matrix : array + Empty 2d array where the sensitivity matrix elements will be filled. + This could be a preallocated empty array or a slice of it. + The array should have a shape of ``(n_receivers, n_active_nodes)`` + if ``scalar_model`` is True. + The array should have a shape of ``(n_receivers, 3 * n_active_nodes)`` + if ``scalar_model`` is False. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz : callables + Kernel functions used for computing the desired TMI derivative. + scalar_model : bool + If True, the sensitivity matrix is build to work with scalar models + (susceptibilities). + If False, the sensitivity matrix is build to work with vector models + (effective susceptibilities). + + Notes + ----- + + About the model array + ^^^^^^^^^^^^^^^^^^^^^ + + The ``model`` must always be a 1d array: + + * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with + the same number of elements as active cells in the mesh. It should store + the magnetic susceptibilities of each active cell in SI units. + * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array + with a number of elements equal to three times the active cells in the + mesh. It should store the components of the magnetization vector of each + active cell in :math:`Am^{-1}`. The order in which the components should + be passed are: + * every _easting_ component of each active cell, + * then every _northing_ component of each active cell, + * and finally every _upward_ component of each active cell. + """ + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + + constant_factor = 1 / 4 / np.pi + + # Fill the sensitivity matrix + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + for i in prange(n_receivers): + for j in range(n_cells): + # Evaluate kernels for the current cell and receiver + uxx, uyy, uzz = evaluate_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + kernel_xx, + kernel_yy, + kernel_zz, + ) + uxy, uxz, uyz = evaluate_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + kernel_xy, + kernel_xz, + kernel_yz, + ) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + if scalar_model: + sensitivity_matrix[i, j] = ( + constant_factor + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + else: + sensitivity_matrix[i, j] = ( + constant_factor * regional_field_amplitude * bx + ) + sensitivity_matrix[i, j + n_cells] = ( + constant_factor * regional_field_amplitude * by + ) + sensitivity_matrix[i, j + 2 * n_cells] = ( + constant_factor * regional_field_amplitude * bz + ) + + _sensitivity_tmi_serial = jit(nopython=True, parallel=False)(_sensitivity_tmi) _sensitivity_tmi_parallel = jit(nopython=True, parallel=True)(_sensitivity_tmi) _forward_tmi_serial = jit(nopython=True, parallel=False)(_forward_tmi) @@ -1023,3 +1951,31 @@ def _forward_tmi_derivative( _sensitivity_tmi_derivative_serial = jit(nopython=True, parallel=False)( _sensitivity_tmi_derivative ) +_forward_tmi_2d_mesh_serial = jit(nopython=True, parallel=False)(_forward_tmi_2d_mesh) +_forward_tmi_2d_mesh_parallel = jit(nopython=True, parallel=True)(_forward_tmi_2d_mesh) +_forward_mag_2d_mesh_serial = jit(nopython=True, parallel=False)(_forward_mag_2d_mesh) +_forward_mag_2d_mesh_parallel = jit(nopython=True, parallel=True)(_forward_mag_2d_mesh) +_forward_tmi_derivative_2d_mesh_serial = jit(nopython=True, parallel=False)( + _forward_tmi_derivative_2d_mesh +) +_forward_tmi_derivative_2d_mesh_parallel = jit(nopython=True, parallel=True)( + _forward_tmi_derivative_2d_mesh +) +_sensitivity_mag_2d_mesh_serial = jit(nopython=True, parallel=False)( + _sensitivity_mag_2d_mesh +) +_sensitivity_mag_2d_mesh_parallel = jit(nopython=True, parallel=True)( + _sensitivity_mag_2d_mesh +) +_sensitivity_tmi_2d_mesh_serial = jit(nopython=True, parallel=False)( + _sensitivity_tmi_2d_mesh +) +_sensitivity_tmi_2d_mesh_parallel = jit(nopython=True, parallel=True)( + _sensitivity_tmi_2d_mesh +) +_sensitivity_tmi_derivative_2d_mesh_serial = jit(nopython=True, parallel=False)( + _sensitivity_tmi_derivative_2d_mesh +) +_sensitivity_tmi_derivative_2d_mesh_parallel = jit(nopython=True, parallel=True)( + _sensitivity_tmi_derivative_2d_mesh +) diff --git a/simpeg/potential_fields/magnetics/simulation.py b/simpeg/potential_fields/magnetics/simulation.py index 77777f01e9..e5ff6b8c03 100644 --- a/simpeg/potential_fields/magnetics/simulation.py +++ b/simpeg/potential_fields/magnetics/simulation.py @@ -20,6 +20,7 @@ from ..base import BaseEquivalentSourceLayerSimulation, BasePFSimulation from .analytics import CongruousMagBC from .survey import Survey +from ..gravity.simulation import _get_cell_bounds from ._numba_functions import ( choclo, @@ -31,10 +32,22 @@ _forward_tmi_serial, _forward_mag_parallel, _forward_mag_serial, + _forward_tmi_2d_mesh_serial, + _forward_tmi_2d_mesh_parallel, + _forward_mag_2d_mesh_serial, + _forward_mag_2d_mesh_parallel, + _forward_tmi_derivative_2d_mesh_serial, + _forward_tmi_derivative_2d_mesh_parallel, + _sensitivity_mag_2d_mesh_serial, + _sensitivity_mag_2d_mesh_parallel, + _sensitivity_tmi_2d_mesh_serial, + _sensitivity_tmi_2d_mesh_parallel, _forward_tmi_derivative_parallel, _forward_tmi_derivative_serial, _sensitivity_tmi_derivative_parallel, _sensitivity_tmi_derivative_serial, + _sensitivity_tmi_derivative_2d_mesh_serial, + _sensitivity_tmi_derivative_2d_mesh_parallel, ) if choclo is not None: @@ -112,6 +125,17 @@ choclo.prism.kernel_nuu, ), } + CHOCLO_FORWARD_FUNCS = { + "bx": choclo.prism.magnetic_e, + "by": choclo.prism.magnetic_n, + "bz": choclo.prism.magnetic_u, + "bxx": choclo.prism.magnetic_ee, + "byy": choclo.prism.magnetic_nn, + "bzz": choclo.prism.magnetic_uu, + "bxy": choclo.prism.magnetic_en, + "bxz": choclo.prism.magnetic_eu, + "byz": choclo.prism.magnetic_nu, + } class Simulation3DIntegral(BasePFSimulation): @@ -860,9 +884,17 @@ class SimulationEquivalentSourceLayer( mesh : discretize.BaseMesh A 2D tensor or tree mesh defining discretization along the x and y directions cell_z_top : numpy.ndarray or float - Define the elevations for the top face of all cells in the layer + Define the elevations for the top face of all cells in the layer. + If an array it should be the same size as the active cell set. cell_z_bottom : numpy.ndarray or float - Define the elevations for the bottom face of all cells in the layer + Define the elevations for the bottom face of all cells in the layer. + If an array it should be the same size as the active cell set. + engine : {"geoana", "choclo"}, optional + Choose which engine should be used to run the forward model. + numba_parallel : bool, optional + If True, the simulation will run in parallel. If False, it will + run in serial. If ``engine`` is not ``"choclo"`` this argument will be + ignored. """ @@ -875,11 +907,6 @@ def __init__( numba_parallel=True, **kwargs, ): - if engine == "choclo": - raise NotImplementedError( - "Magnetic equivalent sources with choclo as engine has not been" - " implemented yet. Use 'geoana' instead." - ) super().__init__( mesh, cell_z_top, @@ -889,6 +916,208 @@ def __init__( **kwargs, ) + if self.engine == "choclo": + if self.numba_parallel: + self._sensitivity_tmi = _sensitivity_tmi_2d_mesh_parallel + self._sensitivity_mag = _sensitivity_mag_2d_mesh_parallel + self._forward_tmi = _forward_tmi_2d_mesh_parallel + self._forward_mag = _forward_mag_2d_mesh_parallel + self._forward_tmi_derivative = _forward_tmi_derivative_2d_mesh_parallel + self._sensitivity_tmi_derivative = ( + _sensitivity_tmi_derivative_2d_mesh_parallel + ) + else: + self._sensitivity_tmi = _sensitivity_tmi_2d_mesh_serial + self._sensitivity_mag = _sensitivity_mag_2d_mesh_serial + self._forward_tmi = _forward_tmi_2d_mesh_serial + self._forward_mag = _forward_mag_2d_mesh_serial + self._forward_tmi_derivative = _forward_tmi_derivative_2d_mesh_serial + self._sensitivity_tmi_derivative = ( + _sensitivity_tmi_derivative_2d_mesh_serial + ) + + def _forward(self, model): + """ + Forward model the fields of active cells in the mesh on receivers. + + Parameters + ---------- + model : (n_active_cells) or (3 * n_active_cells) array + Array containing the susceptibilities (scalar) or effective + susceptibilities (vector) of the active cells in the mesh, in SI + units. + Susceptibilities are expected if ``model_type`` is ``"scalar"``, + and the array should have ``n_active_cells`` elements. + Effective susceptibilities are expected if ``model_type`` is + ``"vector"``, and the array should have ``3 * n_active_cells`` + elements. + + Returns + ------- + (nD, ) array + Always return a ``np.float64`` array. + """ + # Get cells in the 2D mesh + cells_bounds = _get_cell_bounds(self.mesh) + # Keep only active cells + cells_bounds_active = cells_bounds[self.active_cells] + # Get regional field + regional_field = self.survey.source_field.b0 + # Allocate fields array + fields = np.zeros(self.survey.nD, dtype=self.sensitivity_dtype) + # Start computing the fields + index_offset = 0 + scalar_model = self.model_type == "scalar" + for components, receivers in self._get_components_and_receivers(): + if not CHOCLO_SUPPORTED_COMPONENTS.issuperset(components): + raise NotImplementedError( + f"Other components besides {CHOCLO_SUPPORTED_COMPONENTS} " + "aren't implemented yet." + ) + n_components = len(components) + n_rows = n_components * receivers.shape[0] + for i, component in enumerate(components): + vector_slice = slice( + index_offset + i, index_offset + n_rows, n_components + ) + if component == "tmi": + self._forward_tmi( + receivers, + cells_bounds_active, + self.cell_z_top, + self.cell_z_bottom, + model, + fields[vector_slice], + regional_field, + scalar_model, + ) + elif component in ("tmi_x", "tmi_y", "tmi_z"): + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz = ( + CHOCLO_KERNELS[component] + ) + self._forward_tmi_derivative( + receivers, + cells_bounds_active, + self.cell_z_top, + self.cell_z_bottom, + model, + fields[vector_slice], + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + scalar_model, + ) + else: + forward_func = CHOCLO_FORWARD_FUNCS[component] + self._forward_mag( + receivers, + cells_bounds_active, + self.cell_z_top, + self.cell_z_bottom, + model, + fields[vector_slice], + regional_field, + forward_func, + scalar_model, + ) + index_offset += n_rows + return fields + + def _sensitivity_matrix(self): + """ + Compute the sensitivity matrix G + + Returns + ------- + (nD, n_active_cells) array + """ + # Get cells in the 2D mesh + cells_bounds = _get_cell_bounds(self.mesh) + # Keep only active cells + cells_bounds_active = cells_bounds[self.active_cells] + # Get regional field + regional_field = self.survey.source_field.b0 + # Allocate sensitivity matrix + if self.model_type == "scalar": + n_columns = self.nC + else: + n_columns = 3 * self.nC + shape = (self.survey.nD, n_columns) + if self.store_sensitivities == "disk": + sensitivity_matrix = np.memmap( + self.sensitivity_path, + shape=shape, + dtype=self.sensitivity_dtype, + order="C", # it's more efficient to write in row major + mode="w+", + ) + else: + sensitivity_matrix = np.empty(shape, dtype=self.sensitivity_dtype) + # Start filling the sensitivity matrix + index_offset = 0 + scalar_model = self.model_type == "scalar" + for components, receivers in self._get_components_and_receivers(): + if not CHOCLO_SUPPORTED_COMPONENTS.issuperset(components): + raise NotImplementedError( + f"Other components besides {CHOCLO_SUPPORTED_COMPONENTS} " + "aren't implemented yet." + ) + n_components = len(components) + n_rows = n_components * receivers.shape[0] + for i, component in enumerate(components): + matrix_slice = slice( + index_offset + i, index_offset + n_rows, n_components + ) + if component == "tmi": + self._sensitivity_tmi( + receivers, + cells_bounds_active, + self.cell_z_top, + self.cell_z_bottom, + sensitivity_matrix[matrix_slice, :], + regional_field, + scalar_model, + ) + elif component in ("tmi_x", "tmi_y", "tmi_z"): + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz = ( + CHOCLO_KERNELS[component] + ) + self._sensitivity_tmi_derivative( + receivers, + cells_bounds_active, + self.cell_z_top, + self.cell_z_bottom, + sensitivity_matrix[matrix_slice, :], + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + scalar_model, + ) + else: + kernel_x, kernel_y, kernel_z = CHOCLO_KERNELS[component] + self._sensitivity_mag( + receivers, + cells_bounds_active, + self.cell_z_top, + self.cell_z_bottom, + sensitivity_matrix[matrix_slice, :], + regional_field, + kernel_x, + kernel_y, + kernel_z, + scalar_model, + ) + index_offset += n_rows + return sensitivity_matrix + class Simulation3DDifferential(BaseMagneticPDESimulation): """ diff --git a/tests/pf/test_equivalent_sources.py b/tests/pf/test_equivalent_sources.py index 1414768664..12880ea3f3 100644 --- a/tests/pf/test_equivalent_sources.py +++ b/tests/pf/test_equivalent_sources.py @@ -1,14 +1,29 @@ import pytest +from collections.abc import Iterable import numpy as np from discretize import TensorMesh from discretize.utils import mesh_builder_xyz, mkvc - import simpeg from simpeg.optimization import ProjectedGNCG from simpeg.potential_fields import gravity, magnetics, base -COMPONENTS = ["gx", "gy", "gz", "gxx", "gyy", "gzz", "gxy", "gxz", "gyz", "guv"] +GRAVITY_COMPONENTS = ["gx", "gy", "gz", "gxx", "gyy", "gzz", "gxy", "gxz", "gyz", "guv"] +MAGNETIC_COMPONENTS = [ + "tmi", + "bx", + "by", + "bz", + "bxx", + "byy", + "bzz", + "bxy", + "bxz", + "byz", + "tmi_x", + "tmi_y", + "tmi_z", +] def create_grid(x_range, y_range, size): @@ -84,15 +99,41 @@ def coordinates(): return np.c_[mkvc(x), mkvc(y), mkvc(z)] -def get_block_model(mesh, phys_property: float): - """Build a block model.""" - model = simpeg.utils.model_builder.add_block( - mesh.cell_centers, - np.zeros(mesh.n_cells), - np.r_[-20, -20], - np.r_[20, 20], - phys_property, - ) +def get_block_model(mesh, phys_property: float | tuple): + """ + Build a block model. + + Parameters + ---------- + mesh : discretize.BaseMesh + Mesh. + phys_property : float or tuple of floats + Pass a tuple of floats if you want to generate a vector model. + + Returns + ------- + model : np.ndarray + """ + if not isinstance(phys_property, Iterable): + model = simpeg.utils.model_builder.add_block( + mesh.cell_centers, + np.zeros(mesh.n_cells), + np.r_[-20, -20], + np.r_[20, 20], + phys_property, + ) + else: + models = tuple( + simpeg.utils.model_builder.add_block( + mesh.cell_centers, + np.zeros(mesh.n_cells), + np.r_[-20, -20], + np.r_[20, 20], + p, + ) + for p in phys_property + ) + model = np.hstack(models) return model @@ -101,6 +142,16 @@ def get_mapping(mesh): return simpeg.maps.IdentityMap(nP=mesh.n_cells) +def get_mesh_3d(mesh, top: float, bottom: float): + """ + Build a 3D mesh analogous to the 2D mesh + the top and bottom bounds. + """ + origin = (*mesh.origin, bottom) + h = (*mesh.h, np.array([top - bottom], dtype=np.float64)) + mesh_3d = TensorMesh(h=h, origin=origin) + return mesh_3d + + @pytest.fixture def gravity_survey(coordinates): """ @@ -109,6 +160,14 @@ def gravity_survey(coordinates): return build_gravity_survey(coordinates, components="gz") +@pytest.fixture +def magnetic_survey(coordinates): + """ + Sample survey for the magnetic equivalent sources. + """ + return build_magnetic_survey(coordinates, components="tmi") + + def build_gravity_survey(coordinates, components): """ Build a gravity survey. @@ -119,6 +178,23 @@ def build_gravity_survey(coordinates, components): return survey +def build_magnetic_survey( + coordinates, components, amplitude=51_000.0, inclination=71.0, declination=12.0 +): + """ + Build a magnetic survey. + """ + receivers = magnetics.Point(coordinates, components=components) + source_field = magnetics.UniformBackgroundField( + [receivers], + amplitude=amplitude, + inclination=inclination, + declination=declination, + ) + survey = magnetics.Survey(source_field) + return survey + + class Test3DMeshError: """ Test if error is raised after passing a 3D mesh to equivalent sources. @@ -140,7 +216,7 @@ def test_error_on_gravity(self, mesh_3d, engine): mesh=mesh_3d, cell_z_top=0.0, cell_z_bottom=-2.0, engine=engine ) - @pytest.mark.parametrize("engine", ("geoana",)) + @pytest.mark.parametrize("engine", ("geoana", "choclo")) def test_error_on_mag(self, mesh_3d, engine): """ Test error is raised after passing a 3D mesh to magnetic eq source class. @@ -195,15 +271,6 @@ class TestGravityEquivalentSourcesForward: Test the forward capabilities of the gravity equivalent sources. """ - def get_mesh_3d(self, mesh, top: float, bottom: float): - """ - Build a 3D mesh analogous to the 2D mesh + the top and bottom bounds. - """ - origin = (*mesh.origin, bottom) - h = (*mesh.h, np.array([top - bottom], dtype=np.float64)) - mesh_3d = TensorMesh(h=h, origin=origin) - return mesh_3d - @pytest.mark.parametrize("engine", ("geoana", "choclo")) @pytest.mark.parametrize("store_sensitivities", ("ram", "forward_only")) def test_forward_vs_simulation( @@ -219,7 +286,7 @@ def test_forward_vs_simulation( Test forward of the eq sources vs. using the integral 3d simulation. """ # Build 3D mesh that is analogous to the 2D mesh with bottom and top - mesh_3d = self.get_mesh_3d(tensor_mesh, top=mesh_top, bottom=mesh_bottom) + mesh_3d = get_mesh_3d(tensor_mesh, top=mesh_top, bottom=mesh_bottom) # Build simulations mapping = get_mapping(tensor_mesh) sim_3d = gravity.Simulation3DIntegral( @@ -242,7 +309,7 @@ def test_forward_vs_simulation( @pytest.mark.parametrize("engine", ("geoana", "choclo")) @pytest.mark.parametrize("store_sensitivities", ("ram", "forward_only")) - @pytest.mark.parametrize("components", COMPONENTS + [["gz", "gzz"]]) + @pytest.mark.parametrize("components", GRAVITY_COMPONENTS + [["gz", "gzz"]]) def test_forward_vs_simulation_with_components( self, coordinates, @@ -259,7 +326,7 @@ def test_forward_vs_simulation_with_components( # Build survey survey = build_gravity_survey(coordinates, components) # Build 3D mesh that is analogous to the 2D mesh with bottom and top - mesh_3d = self.get_mesh_3d(tensor_mesh, top=mesh_top, bottom=mesh_bottom) + mesh_3d = get_mesh_3d(tensor_mesh, top=mesh_top, bottom=mesh_bottom) # Build simulations mapping = get_mapping(tensor_mesh) sim_3d = gravity.Simulation3DIntegral( @@ -294,7 +361,7 @@ def test_forward_vs_simulation_on_disk( Test forward vs simulation storing sensitivities on disk. """ # Build 3D mesh that is analogous to the 2D mesh with bottom and top - mesh_3d = self.get_mesh_3d(tensor_mesh, top=mesh_top, bottom=mesh_bottom) + mesh_3d = get_mesh_3d(tensor_mesh, top=mesh_top, bottom=mesh_bottom) # Define sensitivity_dir if engine == "geoana": sensitivity_path = tmp_path / "sensitivities_geoana" @@ -349,7 +416,7 @@ def test_forward_vs_simulation_with_active_cells( model = model[active_cells] # Build 3D mesh that is analogous to the 2D mesh with bottom and top - mesh_3d = self.get_mesh_3d(tensor_mesh, top=mesh_top, bottom=mesh_bottom) + mesh_3d = get_mesh_3d(tensor_mesh, top=mesh_top, bottom=mesh_bottom) # Build simulations mapping = simpeg.maps.IdentityMap(nP=model.size) @@ -418,9 +485,208 @@ def test_forward_choclo_serial_parallel( np.testing.assert_allclose(sim_parallel.dpred(model), sim_serial.dpred(model)) -class TestGravityEquivalentSources: +class TestMagneticEquivalentSourcesForward: """ - Test fitting equivalent sources with synthetic data. + Test the forward capabilities of the magnetic equivalent sources. + """ + + @pytest.mark.parametrize("engine", ("geoana", "choclo")) + @pytest.mark.parametrize("store_sensitivities", ("ram", "forward_only")) + @pytest.mark.parametrize("model_type", ("scalar", "vector")) + @pytest.mark.parametrize("components", MAGNETIC_COMPONENTS + [["tmi", "bx"]]) + def test_forward_vs_simulation( + self, + coordinates, + tensor_mesh, + mesh_bottom, + mesh_top, + engine, + store_sensitivities, + model_type, + components, + ): + """ + Test forward of the eq sources vs. using the integral 3d simulation. + """ + # Build survey + survey = build_magnetic_survey(coordinates, components) + # Build 3D mesh that is analogous to the 2D mesh with bottom and top + mesh_3d = get_mesh_3d(tensor_mesh, top=mesh_top, bottom=mesh_bottom) + # Build model and mapping + if model_type == "scalar": + model = get_block_model(tensor_mesh, 0.2e-3) + else: + model = get_block_model(tensor_mesh, (0.2e-3, -0.1e-3, 0.5e-3)) + mapping = simpeg.maps.IdentityMap(nP=model.size) + # Build simulations + sim_3d = magnetics.Simulation3DIntegral( + survey=survey, + mesh=mesh_3d, + chiMap=mapping, + model_type=model_type, + ) + eq_sources = magnetics.SimulationEquivalentSourceLayer( + mesh=tensor_mesh, + cell_z_top=mesh_top, + cell_z_bottom=mesh_bottom, + survey=survey, + chiMap=mapping, + engine=engine, + store_sensitivities=store_sensitivities, + model_type=model_type, + ) + # Compare predictions of both simulations + np.testing.assert_allclose( + sim_3d.dpred(model), eq_sources.dpred(model), atol=1e-7 + ) + + @pytest.mark.parametrize("engine", ("geoana", "choclo")) + def test_forward_vs_simulation_on_disk( + self, + tensor_mesh, + mesh_bottom, + mesh_top, + magnetic_survey, + engine, + tmp_path, + ): + """ + Test forward vs simulation storing sensitivities on disk. + """ + # Build 3D mesh that is analogous to the 2D mesh with bottom and top + mesh_3d = get_mesh_3d(tensor_mesh, top=mesh_top, bottom=mesh_bottom) + # Define sensitivity_dir + if engine == "geoana": + sensitivity_path = tmp_path / "sensitivities_geoana" + sensitivity_path.mkdir() + elif engine == "choclo": + sensitivity_path = tmp_path / "sensitivities_choclo" + # Build simulations + mapping = get_mapping(tensor_mesh) + sim_3d = magnetics.Simulation3DIntegral( + survey=magnetic_survey, mesh=mesh_3d, chiMap=mapping + ) + eq_sources = magnetics.SimulationEquivalentSourceLayer( + mesh=tensor_mesh, + cell_z_top=mesh_top, + cell_z_bottom=mesh_bottom, + survey=magnetic_survey, + chiMap=mapping, + engine=engine, + store_sensitivities="disk", + sensitivity_path=str(sensitivity_path), + ) + # Compare predictions of both simulations + model = get_block_model(tensor_mesh, 0.2e-3) + np.testing.assert_allclose(sim_3d.dpred(model), eq_sources.dpred(model)) + + @pytest.mark.parametrize("engine", ("geoana", "choclo")) + @pytest.mark.parametrize("store_sensitivities", ("ram", "forward_only")) + def test_forward_vs_simulation_with_active_cells( + self, + tensor_mesh, + mesh_bottom, + mesh_top, + magnetic_survey, + engine, + store_sensitivities, + ): + """ + Test forward vs simulation using active cells. + """ + model = get_block_model(tensor_mesh, 0.2e-3) + + # Define some inactive cells inside the block + block_cells_indices = np.indices(model.shape).ravel()[model != 0] + inactive_indices = block_cells_indices[ + : block_cells_indices.size // 2 + ] # mark half of the cells in the block as inactive + active_cells = np.ones_like(model, dtype=bool) + active_cells[inactive_indices] = False + assert not np.all(active_cells) # check we do have inactive cells + + # Keep only values of the model in the active cells + model = model[active_cells] + + # Build 3D mesh that is analogous to the 2D mesh with bottom and top + mesh_3d = get_mesh_3d(tensor_mesh, top=mesh_top, bottom=mesh_bottom) + + # Build simulations + mapping = simpeg.maps.IdentityMap(nP=model.size) + sim_3d = magnetics.Simulation3DIntegral( + survey=magnetic_survey, + mesh=mesh_3d, + chiMap=mapping, + active_cells=active_cells, + ) + eq_sources = magnetics.SimulationEquivalentSourceLayer( + mesh=tensor_mesh, + cell_z_top=mesh_top, + cell_z_bottom=mesh_bottom, + survey=magnetic_survey, + chiMap=mapping, + engine=engine, + store_sensitivities=store_sensitivities, + active_cells=active_cells, + ) + # Compare predictions of both simulations + np.testing.assert_allclose( + sim_3d.dpred(model), eq_sources.dpred(model), atol=1e-7 + ) + + @pytest.mark.parametrize("store_sensitivities", ("ram", "forward_only")) + def test_forward_geoana_choclo( + self, mesh, mesh_bottom, mesh_top, magnetic_survey, store_sensitivities + ): + """Compare forwards using geoana and choclo.""" + # Build simulations + mapping = get_mapping(mesh) + kwargs = dict( + mesh=mesh, + cell_z_top=mesh_top, + cell_z_bottom=mesh_bottom, + survey=magnetic_survey, + chiMap=mapping, + store_sensitivities=store_sensitivities, + ) + sim_geoana = magnetics.SimulationEquivalentSourceLayer( + engine="geoana", **kwargs + ) + sim_choclo = magnetics.SimulationEquivalentSourceLayer( + engine="choclo", **kwargs + ) + model = get_block_model(mesh, 0.2e-3) + np.testing.assert_allclose(sim_geoana.dpred(model), sim_choclo.dpred(model)) + + @pytest.mark.parametrize("store_sensitivities", ("ram", "forward_only")) + def test_forward_choclo_serial_parallel( + self, mesh, mesh_bottom, mesh_top, magnetic_survey, store_sensitivities + ): + """Test forward using choclo in serial and in parallel.""" + # Build simulations + mapping = get_mapping(mesh) + kwargs = dict( + mesh=mesh, + cell_z_top=mesh_top, + cell_z_bottom=mesh_bottom, + survey=magnetic_survey, + chiMap=mapping, + engine="choclo", + store_sensitivities=store_sensitivities, + ) + sim_parallel = magnetics.SimulationEquivalentSourceLayer( + numba_parallel=True, **kwargs + ) + sim_serial = magnetics.SimulationEquivalentSourceLayer( + numba_parallel=False, **kwargs + ) + model = get_block_model(mesh, 0.2e-3) + np.testing.assert_allclose(sim_parallel.dpred(model), sim_serial.dpred(model)) + + +class BaseFittingEquivalentSources: + """ + Base class to test the fitting of equivalent sources with synthetic data. """ def get_mesh_top_bottom(self, mesh, array=False): @@ -490,6 +756,12 @@ def build_inversion(self, mesh, simulation, synthetic_data): inversion = simpeg.inversion.BaseInversion(inverse_problem, directives) return inversion + +class TestGravityEquivalentSources(BaseFittingEquivalentSources): + """ + Test fitting gravity equivalent sources with synthetic data. + """ + @pytest.mark.parametrize("engine", ("geoana", "choclo")) @pytest.mark.parametrize( "top_bottom_as_array", @@ -540,40 +812,56 @@ def test_predictions_on_data_points( ) -class TestMagneticEquivalentSources: - @pytest.fixture - def survey(self, coordinates): - """ - Sample survey for the gravity equivalent sources. - """ - return self._build_survey(coordinates, components="tmi") +class TestMagneticEquivalentSources(BaseFittingEquivalentSources): + """ + Test fitting magnetic equivalent sources with synthetic data. + """ - def _build_survey(self, coordinates, components): - """ - Build a magnetic survey. + @pytest.mark.parametrize("engine", ("geoana", "choclo")) + @pytest.mark.parametrize( + "top_bottom_as_array", + (False, True), + ids=("top-bottom-float", "top-bottom-array"), + ) + def test_predictions_on_data_points( + self, + tree_mesh, + magnetic_survey, + top_bottom_as_array, + engine, + ): """ - receivers = magnetics.Point(coordinates, components=components) - source_field = magnetics.UniformBackgroundField( - [receivers], amplitude=50_000, inclination=35, declination=12 - ) - survey = magnetics.Survey(source_field) - return survey + Test eq sources predictions on the same data points. - def test_choclo_not_implemented(self, tensor_mesh, mesh_top, mesh_bottom, survey): - """ - Test if error is raised when passing "choclo" to magnetic eq sources. + The equivalent sources should be able to reproduce the same data with + which they were trained. """ - msg = ( - "Magnetic equivalent sources with choclo as engine has not been" - " implemented yet. Use 'geoana' instead." + # Get mesh top and bottom + mesh_top, mesh_bottom = self.get_mesh_top_bottom( + tree_mesh, array=top_bottom_as_array + ) + # Build simulation + mapping = get_mapping(tree_mesh) + simulation = magnetics.SimulationEquivalentSourceLayer( + tree_mesh, + mesh_top, + mesh_bottom, + survey=magnetic_survey, + chiMap=mapping, + engine=engine, + ) + # Generate synthetic data + model = get_block_model(tree_mesh, 1e-3) + synthetic_data = self.build_synthetic_data(simulation, model) + # Build inversion + inversion = self.build_inversion(tree_mesh, simulation, synthetic_data) + # Run inversion + starting_model = np.zeros(tree_mesh.n_cells) + recovered_model = inversion.run(starting_model) + # Predict data + prediction = simulation.dpred(recovered_model) + # Check if prediction is close to the synthetic data + atol, rtol = 0.005, 1e-5 + np.testing.assert_allclose( + prediction, synthetic_data.dobs, atol=atol, rtol=rtol ) - mapping = simpeg.maps.IdentityMap(nP=tensor_mesh.n_cells) - with pytest.raises(NotImplementedError, match=msg): - magnetics.SimulationEquivalentSourceLayer( - tensor_mesh, - mesh_top, - mesh_bottom, - survey=survey, - rhoMap=mapping, - engine="choclo", - ) From 46b900192449e77484fc9327e0b9ee181c9597b8 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 23 Oct 2024 11:43:52 -0700 Subject: [PATCH 077/194] Update links in PR template (#1554) Update the links to documentation pages in checklist of the PR template. --- .github/pull_request_template.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index c6b74e7c19..cd17e1eb9a 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -18,10 +18,10 @@ Feel free to remove lines from this template that do not apply to you pull reque #### PR Checklist * [ ] If this is a work in progress PR, set as a Draft PR -* [ ] Linted my code according to the [style guides](https://docs.simpeg.xyz/content/getting_started/contributing/code-style.html). -* [ ] Added [tests](https://docs.simpeg.xyz/content/getting_started/practices.html#testing) to verify changes to the code. +* [ ] Linted my code according to the [style guides](https://docs.simpeg.xyz/latest/content/getting_started/contributing/code-style.html). +* [ ] Added [tests](https://docs.simpeg.xyz/latest/content/getting_started/contributing/testing.html) to verify changes to the code. * [ ] Added necessary documentation to any new functions/classes following the - expect [style](https://docs.simpeg.xyz/content/getting_started/practices.html#documentation). + expect [style](https://docs.simpeg.xyz/latest/content/getting_started/contributing/documentation.html). * [ ] Marked as ready for review (if this is was a draft PR), and converted to a Pull Request * [ ] Tagged ``@simpeg/simpeg-developers`` when ready for review. From 33c3dbf4ea1ab3bfbbf9900224a3df2cacf81f6f Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Wed, 23 Oct 2024 16:38:45 -0600 Subject: [PATCH 078/194] Default solver (#1511) #### Summary Adds methodology to explicitly set a default solver for simpeg to use, and explicitly lists a priority to try to import them. #### What does this implement/fix? There are too many explicit uses of `Pardiso` in the simpeg tests and examples making them unrunnable on systems that do not implement `Pardiso`. It is also unclear to the user which solver is chosen when using default, thus if the user implicitly uses a default solver in a simulation, or asks for the default solver, a `DefaultSolverWarning` is issued saying explicitly what solver is used. This is a new Warning class that the end user can choose to silence if needed. This also removes the re-implemented Solver wrappers and instead rely on `pymatsolver`'s versions of those wrappers. --- .ci/environment_test.yml | 5 +- environment.yml | 4 +- examples/04-dcip/plot_dc_analytic.py | 9 +- ...cip_dipoledipole_3Dinversion_twospheres.py | 7 +- ..._dcip_dipoledipole_parametric_inversion.py | 10 +- .../plot_inv_fdem_loop_loop_2Dinversion.py | 8 +- examples/06-tdem/plot_fwd_tdem_3d_model.py | 5 - ...fwd_tdem_inductive_src_permeable_target.py | 6 - .../06-tdem/plot_inv_tdem_1D_raw_waveform.py | 7 +- examples/07-nsem/plot_fwd_nsem_MTTipper3D.py | 6 - .../plot_booky_1D_time_freq_inv.py | 7 +- .../plot_booky_1Dstitched_resolve_inv.py | 5 +- .../20-published/plot_heagyetal2017_casing.py | 13 - .../plot_heagyetal2017_cyl_inversions.py | 9 +- .../plot_schenkel_morrison_casing.py | 9 +- .../_archived/plot_inv_dcip_2_5Dinversion.py | 11 +- ...lot_inv_dcip_dipoledipole_2_5Dinversion.py | 7 +- ...nv_dcip_dipoledipole_2_5Dinversion_irls.py | 7 +- pyproject.toml | 11 +- simpeg/__init__.py | 1 - .../natural_source/utils/solutions_1d.py | 2 +- .../natural_source/utils/test_utils.py | 6 - .../static/resistivity/simulation_1d.py | 4 +- .../time_domain/simulation_1d.py | 4 +- .../electromagnetics/utils/testing_utils.py | 8 - simpeg/inverse_problem.py | 7 +- simpeg/optimization.py | 10 +- simpeg/potential_fields/base.py | 1 - .../potential_fields/magnetics/simulation.py | 5 +- simpeg/simulation.py | 18 +- simpeg/utils/__init__.py | 10 + simpeg/utils/solver_utils.py | 408 ++++-------------- .../test_pgi_regularization.py | 12 +- tests/base/test_Solver.py | 77 ---- tests/em/fdem/forward/test_FDEM_analytics.py | 10 +- tests/em/fdem/forward/test_FDEM_primsec.py | 7 - tests/em/fdem/forward/test_permittivity.py | 9 - .../em/nsem/forward/test_1D_finite_volume.py | 3 - tests/em/nsem/inversion/test_BC_Sims.py | 5 - .../inversion/test_complex_resistivity.py | 42 +- tests/em/static/test_DC_2D_analytic.py | 57 +-- tests/em/static/test_DC_2D_jvecjtvecadj.py | 6 - .../static/test_DC_FieldsDipoleFullspace.py | 8 - .../test_DC_FieldsMultipoleFullspace.py | 9 - tests/em/static/test_DC_Utils.py | 6 - tests/em/static/test_DC_analytic.py | 11 - tests/em/static/test_DC_jvecjtvecadj.py | 2 - tests/em/static/test_DC_miniaturize.py | 5 - tests/em/static/test_IP_2D_fwd.py | 14 - tests/em/static/test_IP_fwd.py | 14 - tests/em/static/test_SIP_2D_jvecjtvecadj.py | 8 - tests/em/static/test_SIP_jvecjtvecadj.py | 8 - tests/em/tdem/test_TDEM_DerivAdjoint.py | 3 - .../test_TDEM_DerivAdjoint_RawWaveform.py | 2 - .../tdem/test_TDEM_DerivAdjoint_galvanic.py | 2 - tests/em/tdem/test_TDEM_crosscheck.py | 3 - tests/em/tdem/test_TDEM_forward_Analytic.py | 3 - .../test_TDEM_forward_Analytic_RawWaveform.py | 2 - tests/em/tdem/test_TDEM_grounded.py | 2 - .../em/tdem/test_TDEM_inductive_permeable.py | 3 - tests/flow/test_Richards.py | 7 - tests/pf/test_forward_PFproblem.py | 2 - tests/pf/test_sensitivity_PFproblem.py | 2 - tests/utils/test_default_solver.py | 46 ++ tests/utils/test_solverwrap.py | 64 --- tutorials/05-dcr/plot_fwd_2_dcr2d.py | 6 +- tutorials/05-dcr/plot_fwd_3_dcr3d.py | 8 +- tutorials/05-dcr/plot_inv_2_dcr2d.py | 10 +- tutorials/05-dcr/plot_inv_2_dcr2d_irls.py | 13 +- tutorials/05-dcr/plot_inv_3_dcr3d.py | 6 +- tutorials/06-ip/plot_fwd_2_dcip2d.py | 10 +- tutorials/06-ip/plot_fwd_3_dcip3d.py | 9 +- tutorials/06-ip/plot_inv_2_dcip2d.py | 15 +- tutorials/06-ip/plot_inv_3_dcip3d.py | 8 +- tutorials/07-fdem/plot_fwd_2_fem_cyl.py | 9 +- tutorials/07-fdem/plot_fwd_3_fem_3d.py | 6 +- tutorials/08-tdem/plot_fwd_2_tem_cyl.py | 7 +- tutorials/08-tdem/plot_fwd_3_tem_3d.py | 6 +- tutorials/10-vrm/plot_fwd_3_vrm_tem.py | 9 +- .../plot_inv_3_cross_gradient_pf.py | 4 +- .../_temporary/plot_4c_fdem3d_inversion.py | 9 +- .../plot_fwd_1_em1dtm_stitched_skytem.py | 2 - .../plot_inv_1_em1dtm_stitched_skytem.py | 4 +- 83 files changed, 285 insertions(+), 940 deletions(-) delete mode 100644 tests/base/test_Solver.py create mode 100644 tests/utils/test_default_solver.py delete mode 100644 tests/utils/test_solverwrap.py diff --git a/.ci/environment_test.yml b/.ci/environment_test.yml index a0be57a840..18af7dbaad 100644 --- a/.ci/environment_test.yml +++ b/.ci/environment_test.yml @@ -4,15 +4,12 @@ channels: dependencies: - numpy>=1.21 - scipy>=1.8 - - pymatsolver-base>=0.2 + - pymatsolver>=0.3 - matplotlib-base - discretize>=0.10 - geoana>=0.5.0 - empymod>=2.0.0 -# Solver - - pydiso - # optional dependencies - dask - zarr diff --git a/environment.yml b/environment.yml index 624a5e4584..970f353c70 100644 --- a/environment.yml +++ b/environment.yml @@ -6,7 +6,7 @@ dependencies: - python=3.11 - numpy>=1.21 - scipy>=1.8 - - pymatsolver-base>=0.2 + - pymatsolver>=0.3 - matplotlib-base - discretize>=0.10 - geoana>=0.5.0 @@ -15,6 +15,8 @@ dependencies: # solver # uncomment the next line if you are on an intel platform # - pydiso # if on intel pc +# uncomment this line if you want to install mumps solvers +# - python-mumps # optional dependencies - dask diff --git a/examples/04-dcip/plot_dc_analytic.py b/examples/04-dcip/plot_dc_analytic.py index b47ba4ed2a..b926c247a3 100644 --- a/examples/04-dcip/plot_dc_analytic.py +++ b/examples/04-dcip/plot_dc_analytic.py @@ -12,11 +12,6 @@ import matplotlib.pyplot as plt from simpeg.electromagnetics.static import resistivity as DC -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - cs = 25.0 hx = [(cs, 7, -1.3), (cs, 21), (cs, 7, 1.3)] @@ -35,9 +30,7 @@ rx = DC.Rx.Dipole(xyz_rxP, xyz_rxN) src = DC.Src.Dipole([rx], np.r_[-200, 0, -12.5], np.r_[+200, 0, -12.5]) survey = DC.Survey([src]) -sim = DC.Simulation3DCellCentered( - mesh, survey=survey, solver=Solver, sigma=sigma, bc_type="Neumann" -) +sim = DC.Simulation3DCellCentered(mesh, survey=survey, sigma=sigma, bc_type="Neumann") data = sim.dpred() diff --git a/examples/04-dcip/plot_inv_dcip_dipoledipole_3Dinversion_twospheres.py b/examples/04-dcip/plot_inv_dcip_dipoledipole_3Dinversion_twospheres.py index ecf4fa0deb..b198c330d8 100644 --- a/examples/04-dcip/plot_inv_dcip_dipoledipole_3Dinversion_twospheres.py +++ b/examples/04-dcip/plot_inv_dcip_dipoledipole_3Dinversion_twospheres.py @@ -31,11 +31,6 @@ import numpy as np import matplotlib.pyplot as plt -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - np.random.seed(12345) # 3D Mesh @@ -156,7 +151,7 @@ def getCylinderPoints(xc, zc, r): mapactive = maps.InjectActiveCells(mesh=mesh, active_cells=actind, value_inactive=-5.0) mapping = expmap * mapactive problem = DC.Simulation3DCellCentered( - mesh, survey=survey, sigmaMap=mapping, solver=Solver, bc_type="Neumann" + mesh, survey=survey, sigmaMap=mapping, bc_type="Neumann" ) data = problem.make_synthetic_data(mtrue[actind], relative_error=0.05, add_noise=True) diff --git a/examples/04-dcip/plot_inv_dcip_dipoledipole_parametric_inversion.py b/examples/04-dcip/plot_inv_dcip_dipoledipole_parametric_inversion.py index 8304fce3d6..fec53a57d6 100644 --- a/examples/04-dcip/plot_inv_dcip_dipoledipole_parametric_inversion.py +++ b/examples/04-dcip/plot_inv_dcip_dipoledipole_parametric_inversion.py @@ -34,11 +34,6 @@ import numpy as np from pylab import hist -try: - from pymatsolver import PardisoSolver as Solver -except ImportError: - from simpeg import SolverLU as Solver - def run( plotIt=True, @@ -149,7 +144,10 @@ def run( # Generate 2.5D DC problem # "N" means potential is defined at nodes prb = DC.Simulation2DNodal( - mesh, survey=survey, rhoMap=mapping, storeJ=True, solver=Solver + mesh, + survey=survey, + rhoMap=mapping, + storeJ=True, ) # Make synthetic DC data with 5% Gaussian noise diff --git a/examples/05-fdem/plot_inv_fdem_loop_loop_2Dinversion.py b/examples/05-fdem/plot_inv_fdem_loop_loop_2Dinversion.py index e346aa7ac9..c3a2771b03 100644 --- a/examples/05-fdem/plot_inv_fdem_loop_loop_2Dinversion.py +++ b/examples/05-fdem/plot_inv_fdem_loop_loop_2Dinversion.py @@ -16,10 +16,6 @@ import matplotlib.pyplot as plt import time -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver import discretize from simpeg import ( @@ -208,9 +204,7 @@ def interface(x): # create the survey and problem objects for running the forward simulation survey = FDEM.Survey(source_list) -prob = FDEM.Simulation3DMagneticFluxDensity( - mesh, survey=survey, sigmaMap=mapping, solver=Solver -) +prob = FDEM.Simulation3DMagneticFluxDensity(mesh, survey=survey, sigmaMap=mapping) ############################################################################### # Set up data for inversion diff --git a/examples/06-tdem/plot_fwd_tdem_3d_model.py b/examples/06-tdem/plot_fwd_tdem_3d_model.py index def2b65a74..dbe0d774e4 100644 --- a/examples/06-tdem/plot_fwd_tdem_3d_model.py +++ b/examples/06-tdem/plot_fwd_tdem_3d_model.py @@ -6,10 +6,6 @@ import empymod import discretize -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver import numpy as np from simpeg import maps @@ -316,7 +312,6 @@ mesh, survey=survey, rhoMap=maps.IdentityMap(mesh), - solver=Solver, time_steps=time_steps, ) diff --git a/examples/06-tdem/plot_fwd_tdem_inductive_src_permeable_target.py b/examples/06-tdem/plot_fwd_tdem_inductive_src_permeable_target.py index 68f0b668b0..5c18bf377f 100644 --- a/examples/06-tdem/plot_fwd_tdem_inductive_src_permeable_target.py +++ b/examples/06-tdem/plot_fwd_tdem_inductive_src_permeable_target.py @@ -17,10 +17,6 @@ from matplotlib.colors import LogNorm from scipy.constants import mu_0 -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver import time from simpeg.electromagnetics import time_domain as TDEM @@ -192,14 +188,12 @@ survey=survey_magnetostatic, sigmaMap=maps.IdentityMap(mesh), time_steps=ramp, - solver=Solver, ) prob_ramp_on = TDEM.Simulation3DMagneticFluxDensity( mesh=mesh, survey=survey_ramp_on, sigmaMap=maps.IdentityMap(mesh), time_steps=ramp, - solver=Solver, ) ############################################################################### diff --git a/examples/06-tdem/plot_inv_tdem_1D_raw_waveform.py b/examples/06-tdem/plot_inv_tdem_1D_raw_waveform.py index de8aa68f5c..2ad881b0fc 100644 --- a/examples/06-tdem/plot_inv_tdem_1D_raw_waveform.py +++ b/examples/06-tdem/plot_inv_tdem_1D_raw_waveform.py @@ -23,11 +23,6 @@ import matplotlib.pyplot as plt from scipy.interpolate import interp1d -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - def run(plotIt=True): cs, ncx, ncz, npad = 5.0, 25, 24, 15 @@ -50,7 +45,7 @@ def run(plotIt=True): x = np.r_[30, 50, 70, 90] rxloc = np.c_[x, x * 0.0, np.zeros_like(x)] - prb = TDEM.Simulation3DMagneticFluxDensity(mesh, sigmaMap=mapping, solver=Solver) + prb = TDEM.Simulation3DMagneticFluxDensity(mesh, sigmaMap=mapping) prb.time_steps = [ (1e-3, 5), (1e-4, 5), diff --git a/examples/07-nsem/plot_fwd_nsem_MTTipper3D.py b/examples/07-nsem/plot_fwd_nsem_MTTipper3D.py index c5627008e7..e0f2725378 100644 --- a/examples/07-nsem/plot_fwd_nsem_MTTipper3D.py +++ b/examples/07-nsem/plot_fwd_nsem_MTTipper3D.py @@ -14,11 +14,6 @@ import numpy as np import matplotlib.pyplot as plt -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import Solver - def run(plotIt=True): """ @@ -79,7 +74,6 @@ def run(plotIt=True): problem = NSEM.Simulation3DPrimarySecondary( M, survey=survey, - solver=Solver, sigma=sig, sigmaPrimary=sigBG, forward_only=True, diff --git a/examples/20-published/plot_booky_1D_time_freq_inv.py b/examples/20-published/plot_booky_1D_time_freq_inv.py index a1b7ee6721..bbc0fb480d 100644 --- a/examples/20-published/plot_booky_1D_time_freq_inv.py +++ b/examples/20-published/plot_booky_1D_time_freq_inv.py @@ -33,7 +33,6 @@ import matplotlib import matplotlib.pyplot as plt from scipy.constants import mu_0 -from pymatsolver import Pardiso as Solver import discretize from simpeg import ( @@ -214,7 +213,7 @@ def run(plotIt=True, saveFig=False, cleanup=True): # Set FDEM survey (In-phase and Quadrature) survey = FDEM.Survey(source_list) - prb = FDEM.Simulation3DMagneticFluxDensity(mesh, sigmaMap=mapping, solver=Solver) + prb = FDEM.Simulation3DMagneticFluxDensity(mesh, sigmaMap=mapping) prb.survey = survey # ------------------ RESOLVE Inversion ------------------ # @@ -317,9 +316,7 @@ def run(plotIt=True, saveFig=False, cleanup=True): (1e-4, 10), (5e-4, 15), ] - prob = TDEM.Simulation3DElectricField( - mesh, time_steps=timeSteps, sigmaMap=mapping, solver=Solver - ) + prob = TDEM.Simulation3DElectricField(mesh, time_steps=timeSteps, sigmaMap=mapping) survey = TDEM.Survey(source_list) prob.survey = survey diff --git a/examples/20-published/plot_booky_1Dstitched_resolve_inv.py b/examples/20-published/plot_booky_1Dstitched_resolve_inv.py index 17192c1157..a9d093c96e 100644 --- a/examples/20-published/plot_booky_1Dstitched_resolve_inv.py +++ b/examples/20-published/plot_booky_1Dstitched_resolve_inv.py @@ -27,7 +27,6 @@ import numpy as np import matplotlib.pyplot as plt -from pymatsolver import PardisoSolver from scipy.constants import mu_0 from scipy.spatial import cKDTree @@ -113,9 +112,7 @@ def resolve_1Dinversions( # construct a forward simulation survey = FDEM.Survey(source_list) - prb = FDEM.Simulation3DMagneticFluxDensity( - mesh, sigmaMap=mapping, Solver=PardisoSolver - ) + prb = FDEM.Simulation3DMagneticFluxDensity(mesh, sigmaMap=mapping) prb.survey = survey # ------------------- Inversion ------------------- # diff --git a/examples/20-published/plot_heagyetal2017_casing.py b/examples/20-published/plot_heagyetal2017_casing.py index 337dc3b1a6..4caa786f89 100644 --- a/examples/20-published/plot_heagyetal2017_casing.py +++ b/examples/20-published/plot_heagyetal2017_casing.py @@ -36,15 +36,6 @@ from simpeg.electromagnetics import frequency_domain as FDEM, mu_0 from simpeg.utils.io_utils import download -# try: -# from pymatsolver import MumpsSolver as Solver -# print('using MumpsSolver') -# except ImportError: -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - import numpy as np import scipy.sparse as sp import time @@ -349,7 +340,6 @@ def primaryProblem(self): ) primaryProblem.mu = self.muModel - primaryProblem.solver = Solver self._primaryProblem = primaryProblem print("... done building primary problem") @@ -575,7 +565,6 @@ def setupSecondaryProblem(self, mapping=None): if mapping is None: mapping = [("sigma", maps.IdentityMap(self.meshs))] sec_problem = FDEM.Simulation3DElectricField(self.meshs, sigmaMap=mapping) - sec_problem.solver = Solver print("... done setting up secondary problem") return sec_problem @@ -675,8 +664,6 @@ def plotPrimaryFields(self, primaryFields, saveFig=False): def plotSecondarySource(self, primaryFields, saveFig=False): # get source term secondaryProblem = self.setupSecondaryProblem(mapping=self.mapping) - secondaryProblem.solver = Solver - self.primaryProblem.solver = Solver secondaryProblem.model = self.mtrue secondarySurvey = self.setupSecondarySurvey( self.primaryProblem, self.primarySurvey, self.primaryMap2meshs diff --git a/examples/20-published/plot_heagyetal2017_cyl_inversions.py b/examples/20-published/plot_heagyetal2017_cyl_inversions.py index 25ee925f32..435a3b3f0c 100644 --- a/examples/20-published/plot_heagyetal2017_cyl_inversions.py +++ b/examples/20-published/plot_heagyetal2017_cyl_inversions.py @@ -35,11 +35,6 @@ import matplotlib.pyplot as plt import matplotlib -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - def run(plotIt=True, saveFig=False): # Set up cylindrically symmeric mesh @@ -93,7 +88,7 @@ def run(plotIt=True, saveFig=False): surveyFD = FDEM.Survey(source_list) prbFD = FDEM.Simulation3DMagneticFluxDensity( - mesh, survey=surveyFD, sigmaMap=mapping, solver=Solver + mesh, survey=surveyFD, sigmaMap=mapping ) rel_err = 0.03 dataFD = prbFD.make_synthetic_data(mtrue, relative_error=rel_err, add_noise=True) @@ -138,7 +133,7 @@ def run(plotIt=True, saveFig=False): surveyTD = TDEM.Survey([src]) prbTD = TDEM.Simulation3DMagneticFluxDensity( - mesh, survey=surveyTD, sigmaMap=mapping, solver=Solver + mesh, survey=surveyTD, sigmaMap=mapping ) prbTD.time_steps = [(5e-5, 10), (1e-4, 10), (5e-4, 10)] diff --git a/examples/20-published/plot_schenkel_morrison_casing.py b/examples/20-published/plot_schenkel_morrison_casing.py index 6654e0ad08..5348f0fd99 100644 --- a/examples/20-published/plot_schenkel_morrison_casing.py +++ b/examples/20-published/plot_schenkel_morrison_casing.py @@ -52,11 +52,6 @@ from simpeg.electromagnetics import frequency_domain as FDEM import time -try: - from pymatsolver import Pardiso as Solver -except Exception: - from simpeg import SolverLU as Solver - def run(plotIt=True): # ------------------ MODEL ------------------ @@ -229,7 +224,9 @@ def run(plotIt=True): # ------------ Problem and Survey --------------- survey = FDEM.Survey(sg_p + dg_p) problem = FDEM.Simulation3DMagneticField( - mesh, survey=survey, sigmaMap=maps.IdentityMap(mesh), solver=Solver + mesh, + survey=survey, + sigmaMap=maps.IdentityMap(mesh), ) # ------------- Solve --------------------------- diff --git a/examples/_archived/plot_inv_dcip_2_5Dinversion.py b/examples/_archived/plot_inv_dcip_2_5Dinversion.py index 717cf0cb5c..5d28430207 100644 --- a/examples/_archived/plot_inv_dcip_2_5Dinversion.py +++ b/examples/_archived/plot_inv_dcip_2_5Dinversion.py @@ -27,11 +27,6 @@ import numpy as np from pylab import hist -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - def run(plotIt=True, survey_type="dipole-dipole"): np.random.seed(1) @@ -130,9 +125,7 @@ def run(plotIt=True, survey_type="dipole-dipole"): # Generate 2.5D DC problem # "N" means potential is defined at nodes - prb = DC.Simulation2DNodal( - mesh, survey=survey_dc, rhoMap=mapping, storeJ=True, solver=Solver - ) + prb = DC.Simulation2DNodal(mesh, survey=survey_dc, rhoMap=mapping, storeJ=True) # Make synthetic DC data with 5% Gaussian noise data_dc = prb.make_synthetic_data(mtrue_dc, relative_error=0.05, add_noise=True) @@ -144,7 +137,7 @@ def run(plotIt=True, survey_type="dipole-dipole"): # "N" means potential is defined at nodes survey_ip = IP.from_dc_to_ip_survey(survey_dc, dim="2.5D") prb_ip = IP.Simulation2DNodal( - mesh, survey=survey_ip, etaMap=actmap, storeJ=True, rho=rho, solver=Solver + mesh, survey=survey_ip, etaMap=actmap, storeJ=True, rho=rho ) data_ip = prb_ip.make_synthetic_data(mtrue_ip, relative_error=0.05, add_noise=True) diff --git a/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion.py b/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion.py index dafcfee6a9..26c60fd3c6 100644 --- a/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion.py +++ b/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion.py @@ -30,11 +30,6 @@ import numpy as np from pylab import hist -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - def run(plotIt=True, survey_type="dipole-dipole"): np.random.seed(1) @@ -112,7 +107,7 @@ def run(plotIt=True, survey_type="dipole-dipole"): # Generate 2.5D DC problem # "N" means potential is defined at nodes prb = DC.Simulation2DNodal( - mesh, survey=survey, rhoMap=mapping, storeJ=True, Solver=Solver, verbose=True + mesh, survey=survey, rhoMap=mapping, storeJ=True, verbose=True ) # Make synthetic DC data with 5% Gaussian noise diff --git a/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion_irls.py b/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion_irls.py index 392a8d8513..356fe58804 100644 --- a/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion_irls.py +++ b/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion_irls.py @@ -38,11 +38,6 @@ import numpy as np from pylab import hist -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - def run(plotIt=True, survey_type="dipole-dipole", p=0.0, qx=2.0, qz=2.0): np.random.seed(1) @@ -122,7 +117,7 @@ def run(plotIt=True, survey_type="dipole-dipole", p=0.0, qx=2.0, qz=2.0): # Generate 2.5D DC problem # "N" means potential is defined at nodes prb = DC.Simulation2DNodal( - mesh, survey=survey, rhoMap=mapping, storeJ=True, Solver=Solver, verbose=True + mesh, survey=survey, rhoMap=mapping, storeJ=True, verbose=True ) # Make synthetic DC data with 5% Gaussian noise diff --git a/pyproject.toml b/pyproject.toml index 26ab96a679..00dd797df9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ keywords = [ dependencies = [ "numpy>=1.21", "scipy>=1.8", - "pymatsolver>=0.2", + "pymatsolver>=0.3", "matplotlib", "discretize>=0.10", "geoana>=0.5.0", @@ -121,6 +121,9 @@ exclude_also = [ "if 0:", "if __name__ == .__main__.:", + # Don't complain about default solver choices: + 'if AvailableSolvers["Pardiso"]:', + # Don't complain about abstract methods, they aren't run: "@(abc\\.)?abstractmethod", ] @@ -248,3 +251,9 @@ rst-roles = [ 'meth', 'ref', ] + +# pyproject.toml +[tool.pytest.ini_options] +filterwarnings = [ + "ignore::simpeg.utils.solver_utils.DefaultSolverWarning", +] \ No newline at end of file diff --git a/simpeg/__init__.py b/simpeg/__init__.py index 0dc76dab2d..c6cbe5e65d 100644 --- a/simpeg/__init__.py +++ b/simpeg/__init__.py @@ -155,7 +155,6 @@ from .utils import mkvc from .utils import Report from .utils.solver_utils import ( - _checkAccuracy, SolverWrapD, SolverWrapI, Solver, diff --git a/simpeg/electromagnetics/natural_source/utils/solutions_1d.py b/simpeg/electromagnetics/natural_source/utils/solutions_1d.py index a69239a1ae..54513bab01 100644 --- a/simpeg/electromagnetics/natural_source/utils/solutions_1d.py +++ b/simpeg/electromagnetics/natural_source/utils/solutions_1d.py @@ -1,7 +1,7 @@ import numpy as np from scipy.constants import mu_0 -from .... import Solver +from pymatsolver import Solver from ....utils import sdiag from .analytic_1d import getEHfields diff --git a/simpeg/electromagnetics/natural_source/utils/test_utils.py b/simpeg/electromagnetics/natural_source/utils/test_utils.py index 8effd747de..34943aae44 100644 --- a/simpeg/electromagnetics/natural_source/utils/test_utils.py +++ b/simpeg/electromagnetics/natural_source/utils/test_utils.py @@ -472,12 +472,6 @@ def setupSimpegNSEM_ePrimSec(inputSetup, comp="Imp", singleFreq=False, expMap=Tr ) problem.model = sig problem.verbose = False - try: - from pymatsolver import Pardiso - - problem.solver = Pardiso - except ImportError: - pass return (survey, problem) diff --git a/simpeg/electromagnetics/static/resistivity/simulation_1d.py b/simpeg/electromagnetics/static/resistivity/simulation_1d.py index f2528d9a6f..7f913ea3e2 100644 --- a/simpeg/electromagnetics/static/resistivity/simulation_1d.py +++ b/simpeg/electromagnetics/static/resistivity/simulation_1d.py @@ -225,8 +225,8 @@ def _compute_hankel_coefficients(self): return survey = self.survey - r_min = np.infty - r_max = -np.infty + r_min = np.inf + r_max = -np.inf for src in survey.source_list: src_loc = src.location diff --git a/simpeg/electromagnetics/time_domain/simulation_1d.py b/simpeg/electromagnetics/time_domain/simulation_1d.py index 3343ec3fba..8611c67135 100644 --- a/simpeg/electromagnetics/time_domain/simulation_1d.py +++ b/simpeg/electromagnetics/time_domain/simulation_1d.py @@ -97,8 +97,8 @@ def _compute_coefficients(self): self._compute_hankel_coefficients() survey = self.survey - t_min = np.infty - t_max = -np.infty + t_min = np.inf + t_max = -np.inf x, w = roots_legendre(251) # loop through source and receiver lists to find the minimum and maximum # evaluation times for the step response diff --git a/simpeg/electromagnetics/utils/testing_utils.py b/simpeg/electromagnetics/utils/testing_utils.py index ec7ad01a33..86444a8979 100644 --- a/simpeg/electromagnetics/utils/testing_utils.py +++ b/simpeg/electromagnetics/utils/testing_utils.py @@ -4,7 +4,6 @@ from discretize import TensorMesh from ... import maps, utils -from simpeg import SolverLU from simpeg.electromagnetics import frequency_domain as fdem FLR = 1e-20 # "zero", so if residual below this --> pass regardless of order @@ -126,13 +125,6 @@ def getFDEMProblem(fdemType, comp, SrcList, freq, useMu=False, verbose=False): else: raise NotImplementedError() - - try: - from pymatsolver import Pardiso - - prb.solver = Pardiso - except ImportError: - prb.solver = SolverLU # prb.solver_opts = dict(check_accuracy=True) return prb diff --git a/simpeg/inverse_problem.py b/simpeg/inverse_problem.py index 8bc52a24c0..df53169ab3 100644 --- a/simpeg/inverse_problem.py +++ b/simpeg/inverse_problem.py @@ -13,8 +13,8 @@ validate_type, validate_ndarray_with_shape, ) -from .simulation import DefaultSolver from .version import __version__ as simpeg_version +from .utils.solver_utils import get_default_solver class BaseInvProblem: @@ -202,7 +202,6 @@ def startup(self, m0): self.model = m0 - solver = DefaultSolver set_default = True for objfct in self.dmisfit.objfcts: if ( @@ -222,15 +221,15 @@ def startup(self, m0): set_default = False break if set_default: + solver = get_default_solver() print( """ simpeg.InvProblem is setting bfgsH0 to the inverse of the eval2Deriv. ***Done using the default solver {} and no solver_opts.*** """.format( - DefaultSolver.__name__ + solver.__name__ ) ) - solver = DefaultSolver solver_opts = {} self.opt.bfgsH0 = solver( diff --git a/simpeg/optimization.py b/simpeg/optimization.py index 90837b2a84..4c8aa4ca3a 100644 --- a/simpeg/optimization.py +++ b/simpeg/optimization.py @@ -2,7 +2,7 @@ import scipy import scipy.sparse as sp -from .utils.solver_utils import SolverWrapI, Solver, SolverDiag +from pymatsolver import Solver, Diagonal, SolverCG from .utils import ( call_hooks, check_stoppers, @@ -48,8 +48,6 @@ def __ge__(self, other): "IterationPrinters", ] -SolverICG = SolverWrapI(sp.linalg.cg, checkAccuracy=False) - class StoppingCriteria(object): """docstring for StoppingCriteria""" @@ -971,10 +969,10 @@ def bfgsH0(self): if getattr(self, "_bfgsH0", None) is None: print( """ - Default solver: SolverDiag is being used in bfgsH0 + Default solver: Diagonal is being used in bfgsH0 """ ) - self._bfgsH0 = SolverDiag(sp.identity(self.xc.size)) + self._bfgsH0 = Diagonal(sp.identity(self.xc.size)) return self._bfgsH0 @bfgsH0.setter @@ -1093,7 +1091,7 @@ def findSearchDirection(self): # Choose `rtol` or `tol` argument based on installed scipy version tol_key = "rtol" if SCIPY_1_12 else "tol" inp = {tol_key: self.tolCG, "maxiter": self.maxIterCG} - Hinv = SolverICG(self.H, M=self.approxHinv, **inp) + Hinv = SolverCG(self.H, M=self.approxHinv, **inp) p = Hinv * (-self.g) return p diff --git a/simpeg/potential_fields/base.py b/simpeg/potential_fields/base.py index 9f4e4e3f05..524a4e5415 100644 --- a/simpeg/potential_fields/base.py +++ b/simpeg/potential_fields/base.py @@ -124,7 +124,6 @@ def __init__( self.engine = engine self.numba_parallel = numba_parallel super().__init__(mesh, **kwargs) - self.solver = None self.n_processes = n_processes # Check sensitivity_path when engine is "choclo" diff --git a/simpeg/potential_fields/magnetics/simulation.py b/simpeg/potential_fields/magnetics/simulation.py index e5ff6b8c03..e76cac6228 100644 --- a/simpeg/potential_fields/magnetics/simulation.py +++ b/simpeg/potential_fields/magnetics/simulation.py @@ -12,9 +12,10 @@ ) from scipy.constants import mu_0 -from simpeg import Solver, props, utils +from simpeg import props, utils from simpeg.utils import mat_utils, mkvc, sdiag from simpeg.utils.code_utils import deprecate_property, validate_string, validate_type +from simpeg.utils.solver_utils import get_default_solver from ...base import BaseMagneticPDESimulation from ..base import BaseEquivalentSourceLayerSimulation, BasePFSimulation @@ -1616,7 +1617,7 @@ def MagneticsDiffSecondaryInv(mesh, model, data, **kwargs): # Create an optimization program opt = optimization.InexactGaussNewton(maxIter=miter) - opt.bfgsH0 = Solver(sp.identity(model.nP), flag="D") + opt.bfgsH0 = get_default_solver()(sp.identity(model.nP), flag="D") # Create a regularization program reg = regularization.WeightedLeastSquares(model) # Create an objective function diff --git a/simpeg/simulation.py b/simpeg/simulation.py index 40b885f41c..de6ec12647 100644 --- a/simpeg/simulation.py +++ b/simpeg/simulation.py @@ -9,7 +9,7 @@ from discretize.base import BaseMesh from discretize import TensorMesh -from discretize.utils import unpack_widths, sdiag +from discretize.utils import unpack_widths, sdiag, mkvc from . import props from .typing import RandomSeed @@ -19,7 +19,6 @@ Counter, timeIt, count, - mkvc, validate_ndarray_with_shape, validate_float, validate_type, @@ -28,10 +27,7 @@ ) import uuid -try: - from pymatsolver import Pardiso as DefaultSolver -except ImportError: - from .utils.solver_utils import SolverLU as DefaultSolver +from .utils.solver_utils import get_default_solver __all__ = ["LinearSimulation", "ExponentialSinusoidSimulation"] @@ -91,8 +87,6 @@ def __init__( ): self.mesh = mesh self.survey = survey - if solver is None: - solver = DefaultSolver self.solver = solver if solver_opts is None: solver_opts = {} @@ -197,6 +191,10 @@ def solver(self): pymatsolver.base.Base Numerical solver used to solve the forward problem. """ + if self._solver is None: + # do not cache this, in case the user wants to + # change it after the first time it is requested. + return get_default_solver() return self._solver @solver.setter @@ -756,11 +754,13 @@ class LinearSimulation(BaseSimulation): "The model for a linear problem" ) + # linear simulations do not have a solver so set it to `None` here + solver = None + def __init__(self, mesh=None, linear_model=None, model_map=None, G=None, **kwargs): super().__init__(mesh=mesh, **kwargs) self.linear_model = linear_model self.model_map = model_map - self.solver = None if G is not None: self.G = G diff --git a/simpeg/utils/__init__.py b/simpeg/utils/__init__.py index d5506d510c..de6cb066b7 100644 --- a/simpeg/utils/__init__.py +++ b/simpeg/utils/__init__.py @@ -141,6 +141,16 @@ validate_direction validate_active_indices +Solver utilities +---------------- +This module contains utilities to get and set the default solver +used by SimPEG simulations. + +.. autosummary:: + :toctree: generated/ + + solver_utils.get_default_solver + solver_utils.set_default_solver """ from discretize.utils.interpolation_utils import interpolation_matrix diff --git a/simpeg/utils/solver_utils.py b/simpeg/utils/solver_utils.py index e5b4f343c4..bca12303f4 100644 --- a/simpeg/utils/solver_utils.py +++ b/simpeg/utils/solver_utils.py @@ -1,326 +1,106 @@ -import numpy as np -from scipy.sparse import linalg -from .mat_utils import mkvc +from pymatsolver import ( + AvailableSolvers, + Solver, + SolverLU, + SolverCG, + SolverBiCG, + Diagonal, + Pardiso, + Mumps, + wrap_direct, + wrap_iterative, +) +from pymatsolver.solvers import Base +from .code_utils import deprecate_function import warnings -import inspect - - -def _checkAccuracy(A, b, X, accuracyTol): - nrm = np.linalg.norm(mkvc(A * X - b), np.inf) - nrm_b = np.linalg.norm(mkvc(b), np.inf) - if nrm_b > 0: - nrm /= nrm_b - if nrm > accuracyTol: - msg = "### SolverWarning ###: Accuracy on solve is above tolerance: {0:e} > {1:e}".format( - nrm, accuracyTol - ) - print(msg) - warnings.warn(msg, RuntimeWarning, stacklevel=2) - - -def SolverWrapD(fun, factorize=True, checkAccuracy=True, accuracyTol=1e-6, name=None): - """Wraps a direct Solver.) - - Parameters - ---------- - fun : callable - A function handle that accepts a sparse matrix input. - factorize : bool, default: ``True`` - If True, `fun` returns a solver object that has `solve` and - `factorize` methods. - checkAccuracy : bool, default: ``True`` - If ``True``, verify the accuracy of the solve - accuracyTol : float, default: 1e-6 - Minimum accuracy of the solve - name : str, optional - A name for the function +from typing import Type + +__all__ = [ + "Solver", + "SolverLU", + "SolverCG", + "SolverBiCG", + "Diagonal", + "Pardiso", + "Mumps", + "wrap_direct", + "wrap_iterative", + "get_default_solver", + "set_default_solver", + "SolverWrapD", + "SolverWrapI", + "SolverDiag", +] + +# The default direct solver priority is: +# Pardiso (optional, but available on intel systems) +# Mumps (optional, but available for all systems) +# Scipy's SuperLU (available for all scipy systems) +if AvailableSolvers["Pardiso"]: + _DEFAULT_SOLVER = Pardiso +elif AvailableSolvers["Mumps"]: + _DEFAULT_SOLVER = Mumps +else: + _DEFAULT_SOLVER = SolverLU + + +# Create a specific warning allowing users to silence this if they so choose. +class DefaultSolverWarning(UserWarning): + pass + + +def get_default_solver() -> Type[Base]: + """Return the default solver used by simpeg. Returns ------- - Solver - A new solver class created from a direct solver `fun`. - - Examples - -------- - A solver that does not have a factorize method. - - >>> from simpeg.utils.solver_utils import SolverWrapD - >>> import scipy.sparse as sp - >>> SpSolver = SolverWrapD(sp.linalg.spsolve, factorize=False) - >>> A = sp.diags([1, -1], [0, 1], shape=(10, 10)) - >>> b = np.arange(10) - >>> Ainv = SpSolver(A) - >>> x_solve = Ainv * b - >>> A @ x_solve - array([0., 1., 2., 3., 4., 5., 6., 7., 8., 9.]) - - Or one that has a factorize method (which can be re-used on multiple solves) - - >>> SolverLU = SolverWrapD(sp.linalg.splu, factorize=True) - >>> A = sp.diags([1, -1], [0, 1], shape=(10, 10)) - >>> b = np.arange(10) - >>> Ainv = SolverLU(A) - >>> x_solve = Ainv * b - >>> A @ x_solve - array([0., 1., 2., 3., 4., 5., 6., 7., 8., 9.]) + solver + The default solver class used by simpeg's simulations. """ - - def __init__(self, A, **kwargs): - self.A = A.tocsc() - - self.checkAccuracy = kwargs.pop("checkAccuracy", checkAccuracy) - self.accuracyTol = kwargs.pop("accuracyTol", accuracyTol) - - func_params = inspect.signature(fun).parameters - # First test if function excepts **kwargs, - # in which case we do not need to cull the kwargs - do_cull = True - for param_name in func_params: - param = func_params[param_name] - if param.kind == inspect.Parameter.VAR_KEYWORD: - do_cull = False - if do_cull: - # build a dictionary of valid kwargs - culled_args = {} - for item in kwargs: - if item in func_params: - culled_args[item] = kwargs[item] - else: - warnings.warn( - f"{item} is not a valid keyword for {fun.__name__} and will be ignored", - stacklevel=2, - ) - kwargs = culled_args - - self.kwargs = kwargs - - if factorize: - self.solver = fun(self.A, **kwargs) - - def __mul__(self, b): - if not isinstance(b, np.ndarray): - raise TypeError("Can only multiply by a numpy array.") - - if len(b.shape) == 1 or b.shape[1] == 1: - b = b.flatten() - # Just one RHS - - if b.dtype is np.dtype("O"): - b = b.astype(type(b[0])) - - if factorize: - X = self.solver.solve(b, **self.kwargs) - else: - X = fun(self.A, b, **self.kwargs) - else: # Multiple RHSs - if b.dtype is np.dtype("O"): - b = b.astype(type(b[0, 0])) - - X = np.empty_like(b) - - for i in range(b.shape[1]): - if factorize: - X[:, i] = self.solver.solve(b[:, i]) - else: - X[:, i] = fun(self.A, b[:, i], **self.kwargs) - - if self.checkAccuracy: - _checkAccuracy(self.A, b, X, self.accuracyTol) - return X - - def __matmul__(self, other): - return self * other - - def clean(self): - if factorize and hasattr(self.solver, "clean"): - return self.solver.clean() - - return type( - name if name is not None else fun.__name__, - (object,), - { - "__init__": __init__, - "clean": clean, - "__mul__": __mul__, - "__matmul__": __matmul__, - }, + warnings.warn( + f"Using the default solver: {_DEFAULT_SOLVER.__name__}. \n\n" + f"If you would like to suppress this notification, add \n" + f"warnings.filterwarnings('ignore', simpeg.utils.solver_utils.DefaultSolverWarning)\n" + f" to your script.", + DefaultSolverWarning, + stacklevel=2, ) + return _DEFAULT_SOLVER -def SolverWrapI(fun, checkAccuracy=True, accuracyTol=1e-5, name=None): - """Wraps an iterative Solver. +def set_default_solver(solver_class: Type[Base]): + """Set the default solver used by simpeg. Parameters ---------- - fun : function - A function handle that accepts two arguments, a sparse matrix and a rhs array. - checkAccuracy : bool, default: ``True`` - If ``True``, verify the accuracy of the solve - accuracyTol : float, default: 1e-5 - Minimum accuracy of the solve - name : str, optional - A name for the function - - Returns - ------- - Solver - A new solver class created from the function. - - Examples - -------- - - >>> import scipy.sparse as sp - >>> from simpeg.utils.solver_utils import SolverWrapI - - >>> SolverCG = SolverWrapI(sp.linalg.cg) - >>> A = sp.diags([-1, 2, -1], [-1, 0, 1], shape=(10, 10)) - >>> b = np.arange(10) - >>> Ainv = SolverCG(A) - >>> x_solve = Ainv * b - >>> A @ x_solve - array([3.55271368e-15, 1.00000000e+00, 2.00000000e+00, 3.00000000e+00, - 4.00000000e+00, 5.00000000e+00, 6.00000000e+00, 7.00000000e+00, - 8.00000000e+00, 9.00000000e+00]) + solver_class + A ``pymatsolver.solvers.Base`` subclass used to construct an object + that acts os the inverse of a sparse matrix. """ - - def __init__(self, A, **kwargs): - self.A = A - - self.checkAccuracy = kwargs.pop("checkAccuracy", checkAccuracy) - self.accuracyTol = kwargs.pop("accuracyTol", accuracyTol) - - func_params = inspect.signature(fun).parameters - # First test if function excepts **kwargs, - # in which case we do not need to cull the kwargs - do_cull = True - for param_name in func_params: - param = func_params[param_name] - if param.kind == inspect.Parameter.VAR_KEYWORD: - do_cull = False - if do_cull: - # build a dictionary of valid kwargs - culled_args = {} - for item in kwargs: - if item in func_params: - culled_args[item] = kwargs[item] - else: - warnings.warn( - f"{item} is not a valid keyword for {fun.__name__} and will be ignored", - stacklevel=2, - ) - kwargs = culled_args - - self.kwargs = kwargs - - def __mul__(self, b): - if not isinstance(b, np.ndarray): - raise TypeError("Can only multiply by a numpy array.") - - if len(b.shape) == 1 or b.shape[1] == 1: - b = b.flatten() - # Just one RHS - out = fun(self.A, b, **self.kwargs) - if isinstance(out, tuple) and len(out) == 2: - # We are dealing with scipy output with an info! - X = out[0] - self.info = out[1] - else: - X = out - else: # Multiple RHSs - X = np.empty_like(b) - for i in range(b.shape[1]): - out = fun(self.A, b[:, i], **self.kwargs) - if isinstance(out, tuple) and len(out) == 2: - # We are dealing with scipy output with an info! - X[:, i] = out[0] - self.info = out[1] - else: - X[:, i] = out - - if self.checkAccuracy: - _checkAccuracy(self.A, b, X, self.accuracyTol) - return X - - def __matmul__(self, other): - return self * other - - def clean(self): - pass - - return type( - name if name is not None else fun.__name__, - (object,), - { - "__init__": __init__, - "clean": clean, - "__mul__": __mul__, - "__matmul__": __matmul__, - }, - ) - - -Solver = SolverWrapD(linalg.spsolve, factorize=False, name="Solver") -SolverLU = SolverWrapD(linalg.splu, factorize=True, name="SolverLU") -SolverCG = SolverWrapI(linalg.cg, name="SolverCG") -SolverBiCG = SolverWrapI(linalg.bicgstab, name="SolverBiCG") - - -class SolverDiag(object): - """Solver for a diagonal linear system - - This is a simple solver used for diagonal matrices. - - Parameters - ---------- - A : - A diagonal linear system - - Examples - -------- - >>> import scipy.sparse as sp - >>> from simpeg.utils.solver_utils import SolverDiag - >>> A = sp.diags(np.linspace(1, 2, 10)) - >>> b = np.arange(10) - >>> Ainv = SolverDiag(A) - >>> x_solve = Ainv * b - >>> A @ x_solve - array([0., 1., 2., 3., 4., 5., 6., 7., 8., 9.]) - """ - - def __init__(self, A, **kwargs): - self.A = A - self._diagonal = A.diagonal() - for kwarg in kwargs: - warnings.warn( - f"{kwarg} is not recognized and will be ignored", stacklevel=2 - ) - - def __mul__(self, rhs): - n = self.A.shape[0] - assert rhs.size % n == 0, "Incorrect shape of rhs." - nrhs = rhs.size // n - - if len(rhs.shape) == 1 or rhs.shape[1] == 1: - x = self._solve1(rhs) - else: - x = self._solveM(rhs) - - if nrhs == 1: - return x.flatten() - elif nrhs > 1: - return x.reshape((n, nrhs), order="F") - - def __matmul__(self, other): - return self * other - - def _solve1(self, rhs): - return rhs.flatten() / self._diagonal - - def _solveM(self, rhs): - n = self.A.shape[0] - nrhs = rhs.size // n - return rhs / self._diagonal.repeat(nrhs).reshape((n, nrhs)) - - def clean(self): - """Clean""" - pass + global _DEFAULT_SOLVER + if not issubclass(solver_class, Base): + raise TypeError( + "Default solver must be a subclass of pymatsolver.solvers.Base." + ) + _DEFAULT_SOLVER = solver_class + + +# should likely deprecate these classes in favor of the pymatsolver versions. +SolverWrapD = deprecate_function( + wrap_direct, + old_name="SolverWrapD", + removal_version="0.24.0", + new_location="pymatsolver", +) +SolverWrapI = deprecate_function( + wrap_iterative, + old_name="SolverWrapI", + removal_version="0.24.0", + new_location="pymatsolver", +) +SolverDiag = deprecate_function( + Diagonal, + old_name="SolverDiag", + removal_version="0.24.0", + new_location="pymatsolver", +) diff --git a/tests/base/regularizations/test_pgi_regularization.py b/tests/base/regularizations/test_pgi_regularization.py index d39a733e77..c69bf9e051 100644 --- a/tests/base/regularizations/test_pgi_regularization.py +++ b/tests/base/regularizations/test_pgi_regularization.py @@ -3,12 +3,14 @@ import discretize import numpy as np -from pymatsolver import SolverLU from scipy.stats import multivariate_normal from simpeg import regularization from simpeg.maps import Wires from simpeg.utils import WeightedGaussianMixture, mkvc +from simpeg.utils.solver_utils import get_default_solver + +Solver = get_default_solver() class TestPGI(unittest.TestCase): @@ -104,7 +106,7 @@ def test_full_covariances(self): self.assertTrue(passed_deriv1) print("1st derivatives for PGI & Full Cov. are ok.") - Hinv = SolverLU(reg.deriv2(self.model)) + Hinv = Solver(reg.deriv2(self.model)) p = Hinv * deriv direction2 = np.c_[self.wires * p] passed_derivative = np.allclose( @@ -209,7 +211,7 @@ def test_tied_covariances(self): self.assertTrue(passed_deriv1) print("1st derivatives for PGI & tied Cov. are ok.") - Hinv = SolverLU(reg.deriv2(self.model)) + Hinv = Solver(reg.deriv2(self.model)) p = Hinv * deriv direction2 = np.c_[self.wires * p] passed_derivative = np.allclose( @@ -312,7 +314,7 @@ def test_diag_covariances(self): self.assertTrue(passed_deriv1) print("1st derivatives for PGI & diag Cov. are ok.") - Hinv = SolverLU(reg.deriv2(self.model)) + Hinv = Solver(reg.deriv2(self.model)) p = Hinv * deriv direction2 = np.c_[self.wires * p] passed_derivative = np.allclose( @@ -415,7 +417,7 @@ def test_spherical_covariances(self): self.assertTrue(passed_deriv1) print("1st derivatives for PGI & spherical Cov. are ok.") - Hinv = SolverLU(reg.deriv2(self.model)) + Hinv = Solver(reg.deriv2(self.model)) p = Hinv * deriv direction2 = np.c_[self.wires * p] passed_derivative = np.allclose( diff --git a/tests/base/test_Solver.py b/tests/base/test_Solver.py deleted file mode 100644 index adb753f6c8..0000000000 --- a/tests/base/test_Solver.py +++ /dev/null @@ -1,77 +0,0 @@ -import unittest - -from simpeg import Solver, SolverDiag, SolverCG, SolverLU -from discretize import TensorMesh -from simpeg.utils import sdiag -import numpy as np - -TOLD = 1e-10 -TOLI = 1e-3 -numRHS = 5 - -np.random.seed(77) - - -def dotest(MYSOLVER, multi=False, A=None, **solverOpts): - if A is None: - h1 = np.ones(10) * 100.0 - h2 = np.ones(10) * 100.0 - h3 = np.ones(10) * 100.0 - - h = [h1, h2, h3] - - M = TensorMesh(h) - - D = M.face_divergence - G = -M.face_divergence.T - Msig = M.get_face_inner_product() - A = D * Msig * G - A[-1, -1] *= ( - 1 / M.cell_volumes[-1] - ) # remove the constant null space from the matrix - else: - M = TensorMesh([A.shape[0]]) - - Ainv = MYSOLVER(A, **solverOpts) - if multi: - e = np.ones(M.nC) - else: - e = np.ones((M.nC, numRHS)) - rhs = A * e - x = Ainv * rhs - Ainv.clean() - return np.linalg.norm(e - x, np.inf) - - -class TestSolver(unittest.TestCase): - def test_direct_spsolve_1(self): - self.assertLess(dotest(Solver, False), TOLD) - - def test_direct_spsolve_M(self): - self.assertLess(dotest(Solver, True), TOLD) - - def test_direct_splu_1(self): - self.assertLess(dotest(SolverLU, False), TOLD) - - def test_direct_splu_M(self): - self.assertLess(dotest(SolverLU, True), TOLD) - - def test_iterative_diag_1(self): - self.assertLess( - dotest(SolverDiag, False, A=sdiag(np.random.rand(10) + 1.0)), TOLI - ) - - def test_iterative_diag_M(self): - self.assertLess( - dotest(SolverDiag, True, A=sdiag(np.random.rand(10) + 1.0)), TOLI - ) - - def test_iterative_cg_1(self): - self.assertLess(dotest(SolverCG, False), TOLI) - - def test_iterative_cg_M(self): - self.assertLess(dotest(SolverCG, True), TOLI) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/em/fdem/forward/test_FDEM_analytics.py b/tests/em/fdem/forward/test_FDEM_analytics.py index afda665b43..ebadf7a7ea 100644 --- a/tests/em/fdem/forward/test_FDEM_analytics.py +++ b/tests/em/fdem/forward/test_FDEM_analytics.py @@ -5,7 +5,7 @@ import numpy as np import scipy.sparse as sp from scipy.constants import mu_0 -from simpeg import SolverLU, utils +from simpeg import utils from simpeg.electromagnetics import analytics from simpeg.electromagnetics import frequency_domain as fdem @@ -59,14 +59,6 @@ def setUp(self): sigma[mesh.gridCC[:, 2] > 0] = 1e-8 prb = fdem.Simulation3DMagneticFluxDensity(mesh, survey=survey, sigma=sigma) - - try: - from pymatsolver import Pardiso - - prb.solver = Pardiso - except ImportError: - prb.solver = SolverLU - self.prb = prb self.mesh = mesh self.sig = sig diff --git a/tests/em/fdem/forward/test_FDEM_primsec.py b/tests/em/fdem/forward/test_FDEM_primsec.py index a4e4494871..6d150fe4f9 100644 --- a/tests/em/fdem/forward/test_FDEM_primsec.py +++ b/tests/em/fdem/forward/test_FDEM_primsec.py @@ -5,7 +5,6 @@ from simpeg import maps, tests, utils from simpeg.electromagnetics import frequency_domain as fdem -from pymatsolver import Pardiso as Solver import numpy as np import unittest @@ -169,7 +168,6 @@ def setUpClass(self): self.primarySimulation = fdem.Simulation3DMagneticFluxDensity( meshp, sigmaMap=primaryMapping ) - self.primarySimulation.solver = Solver primarySrc = fdem.Src.MagDipole(self.rxlist, frequency=freq, location=src_loc) self.primarySurvey = fdem.Survey([primarySrc]) @@ -185,7 +183,6 @@ def setUpClass(self): self.secondarySimulation = fdem.Simulation3DMagneticFluxDensity( meshs, survey=self.secondarySurvey, sigmaMap=mapping ) - self.secondarySimulation.solver = Solver # Full 3D problem to compare with self.survey3D = fdem.Survey([primarySrc]) @@ -193,7 +190,6 @@ def setUpClass(self): self.simulation3D = fdem.Simulation3DMagneticFluxDensity( meshs, survey=self.survey3D, sigmaMap=mapping ) - self.simulation3D.solver = Solver # solve and store fields print(" solving primary - secondary") @@ -236,7 +232,6 @@ def setUpClass(self): self.primarySimulation = fdem.Simulation3DCurrentDensity( meshp, sigmaMap=primaryMapping ) - self.primarySimulation.solver = Solver s_e = np.zeros(meshp.nF) inds = meshp.nFx + meshp.closest_points_index(src_loc, grid_loc="Fz") s_e[inds] = 1.0 / csz @@ -260,7 +255,6 @@ def setUpClass(self): survey=self.secondarySurvey, sigmaMap=mapping, ) - self.secondarySimulation.solver = Solver # Full 3D problem to compare with @@ -276,7 +270,6 @@ def setUpClass(self): self.simulation3D = fdem.Simulation3DElectricField( meshs, survey=self.survey3D, sigmaMap=mapping ) - self.simulation3D.solver = Solver self.simulation3D.model = model # solve and store fields diff --git a/tests/em/fdem/forward/test_permittivity.py b/tests/em/fdem/forward/test_permittivity.py index 2ef4abd272..aaaff90b74 100644 --- a/tests/em/fdem/forward/test_permittivity.py +++ b/tests/em/fdem/forward/test_permittivity.py @@ -6,7 +6,6 @@ import geoana import discretize from simpeg.electromagnetics import frequency_domain as fdem -from pymatsolver import Pardiso # set up the mesh @@ -83,7 +82,6 @@ def print_comparison( forward_only=True, sigma=conductivity, permittivity=epsilon, - solver=Pardiso, ), lambda survey, epsilon: fdem.Simulation3DMagneticFluxDensity( mesh, @@ -91,7 +89,6 @@ def print_comparison( forward_only=True, sigma=conductivity, permittivity=epsilon, - solver=Pardiso, ), ], ) @@ -138,7 +135,6 @@ def test_mag_dipole(epsilon, frequency, simulation): forward_only=True, sigma=conductivity, permittivity=epsilon, - solver=Pardiso, ), lambda survey, epsilon: fdem.Simulation3DMagneticField( mesh, @@ -146,7 +142,6 @@ def test_mag_dipole(epsilon, frequency, simulation): forward_only=True, sigma=conductivity, permittivity=epsilon, - solver=Pardiso, ), ], ) @@ -223,7 +218,6 @@ def test_cross_check_e_dipole(epsilon_r, frequency): forward_only=True, sigma=sigma, permittivity=rel_permittivity * epsilon_0, - solver=Pardiso, ) # H-formulation @@ -240,7 +234,6 @@ def test_cross_check_e_dipole(epsilon_r, frequency): forward_only=True, sigma=sigma, permittivity=rel_permittivity * epsilon_0, - solver=Pardiso, ) # compute fields @@ -315,7 +308,6 @@ def test_cross_check_b_dipole(epsilon_r, frequency): forward_only=True, sigma=sigma, permittivity=rel_permittivity * epsilon_0, - solver=Pardiso, ) # E-formulation @@ -330,7 +322,6 @@ def test_cross_check_b_dipole(epsilon_r, frequency): forward_only=True, sigma=sigma, permittivity=rel_permittivity * epsilon_0, - solver=Pardiso, ) # compute fields diff --git a/tests/em/nsem/forward/test_1D_finite_volume.py b/tests/em/nsem/forward/test_1D_finite_volume.py index 4af4dae02f..5a836908d4 100644 --- a/tests/em/nsem/forward/test_1D_finite_volume.py +++ b/tests/em/nsem/forward/test_1D_finite_volume.py @@ -2,7 +2,6 @@ from discretize import TensorMesh from simpeg.electromagnetics import natural_source as nsem from simpeg import maps -from pymatsolver import Pardiso import unittest @@ -50,14 +49,12 @@ def get_simulation(self, formulation="e"): if formulation == "e": return nsem.simulation.Simulation1DElectricField( mesh=self.mesh, - solver=Pardiso, survey=self.survey, sigmaMap=maps.IdentityMap(), ) elif formulation == "h": return nsem.simulation.Simulation1DMagneticField( mesh=self.mesh, - solver=Pardiso, survey=self.survey, sigmaMap=maps.IdentityMap(), ) diff --git a/tests/em/nsem/inversion/test_BC_Sims.py b/tests/em/nsem/inversion/test_BC_Sims.py index b029b2e177..b3174a3365 100644 --- a/tests/em/nsem/inversion/test_BC_Sims.py +++ b/tests/em/nsem/inversion/test_BC_Sims.py @@ -6,7 +6,6 @@ from simpeg.electromagnetics import natural_source as nsem from simpeg import maps from discretize import TensorMesh, TreeMesh, CylindricalMesh -from pymatsolver import Pardiso def check_deriv(sim, test_mod, **kwargs): @@ -90,14 +89,12 @@ def create_simulation_1d(sim_type, deriv_type): mesh, survey=survey, **sim_kwargs, - solver=Pardiso, ) else: sim = nsem.simulation.Simulation1DMagneticField( mesh, survey=survey, **sim_kwargs, - solver=Pardiso, ) return sim, test_mod @@ -193,7 +190,6 @@ def create_simulation_2d(sim_type, deriv_type, mesh_type, fixed_boundary=False): mesh, survey=survey, **sim_kwargs, - solver=Pardiso, ) else: if fixed_boundary: @@ -244,7 +240,6 @@ def create_simulation_2d(sim_type, deriv_type, mesh_type, fixed_boundary=False): mesh, survey=survey, **sim_kwargs, - solver=Pardiso, ) return sim, test_mod diff --git a/tests/em/nsem/inversion/test_complex_resistivity.py b/tests/em/nsem/inversion/test_complex_resistivity.py index 477a81b7e3..b0c71071bc 100644 --- a/tests/em/nsem/inversion/test_complex_resistivity.py +++ b/tests/em/nsem/inversion/test_complex_resistivity.py @@ -6,8 +6,8 @@ from discretize.utils import mkvc from simpeg.electromagnetics import natural_source as ns import numpy as np -from pymatsolver import Pardiso as Solver from discretize.utils import volume_average +import pytest TOLr = 5e-2 TOL = 1e-4 @@ -32,7 +32,7 @@ def setUp(self): # create background conductivity model sigma_back = 1e-2 - sigma_background = np.zeros(mesh.nC) * sigma_back + sigma_background = np.ones(mesh.nC) * sigma_back sigma_background[~active] = 1e-8 # create a model to test with @@ -89,7 +89,6 @@ def create_simulation(self, rx_type="apparent_resistivity", rx_orientation="xy") survey=survey_ns, sigmaPrimary=self.sigma_background, sigmaMap=mapping, - solver=Solver, ) return sim @@ -131,7 +130,6 @@ def create_simulation_rx(self, rx_type="apparent_resistivity", rx_orientation="x survey=survey_ns, sigmaPrimary=self.sigma_background, sigmaMap=mapping, - solver=Solver, ) return sim @@ -182,7 +180,6 @@ def create_simulation_1dprimary_assign_mesh1d( self.mesh, survey=survey_ns, sigmaMap=mapping, - solver=Solver, ) return sim @@ -221,16 +218,17 @@ def create_simulation_1dprimary_assign( self.mesh, survey=survey_ns, sigmaMap=mapping, - solver=Solver, ) return sim def check_deriv(self, sim): def fun(x): - return sim.dpred(x), lambda x: sim.Jvec(self.model, x) + d = sim.dpred(x) + return d, lambda y: sim.Jvec(x, y) - np.random.seed(1983) # set a random seed for check_derivative - passed = tests.check_derivative(fun, self.model, num=3, plotIt=False) + rng = np.random.default_rng(seed=1983) # set a random seed for check_derivative + dx = -rng.uniform(size=len(self.model)) * 0.01 * np.abs(self.model).max() + passed = tests.check_derivative(fun, self.model, dx=dx, num=3, plotIt=False) self.assertTrue(passed) def check_adjoint(self, sim): @@ -277,6 +275,7 @@ def test_apparent_resistivity_yx(self): def test_apparent_resistivity_yy(self): self.check_deriv_adjoint("apparent_resistivity", "yy") + @pytest.mark.xfail() def test_phase_xx(self): self.check_deriv_adjoint("phase", "xx") @@ -286,9 +285,34 @@ def test_phase_xy(self): def test_phase_yx(self): self.check_deriv_adjoint("phase", "yx") + @pytest.mark.xfail() def test_phase_yy(self): self.check_deriv_adjoint("phase", "yy") + def test_real_xx(self): + self.check_deriv_adjoint("real", "xx") + + def test_real_xy(self): + self.check_deriv_adjoint("real", "xy") + + def test_real_yx(self): + self.check_deriv_adjoint("real", "yx") + + def test_real_yy(self): + self.check_deriv_adjoint("real", "yy") + + def test_imag_xx(self): + self.check_deriv_adjoint("imag", "xx") + + def test_imag_xy(self): + self.check_deriv_adjoint("imag", "xy") + + def test_imag_yx(self): + self.check_deriv_adjoint("imag", "yx") + + def test_imag_yy(self): + self.check_deriv_adjoint("imag", "yy") + if __name__ == "__main__": unittest.main() diff --git a/tests/em/static/test_DC_2D_analytic.py b/tests/em/static/test_DC_2D_analytic.py index 9edbf3ca0f..22889d5cf2 100644 --- a/tests/em/static/test_DC_2D_analytic.py +++ b/tests/em/static/test_DC_2D_analytic.py @@ -3,7 +3,7 @@ from discretize import TensorMesh -from simpeg import utils, SolverLU +from simpeg import utils from simpeg.electromagnetics import resistivity as dc from simpeg.electromagnetics import analytics @@ -43,19 +43,11 @@ def setUp(self): self.data_ana = data_ana self.plotIt = False - try: - from pymatsolver import Pardiso - - self.solver = Pardiso - except ImportError: - self.solver = SolverLU - def test_Simulation2DNodal(self, tolerance=0.05): simulation = dc.simulation_2d.Simulation2DNodal( self.mesh, survey=self.survey, sigma=self.sigma, - solver=self.solver, bc_type="Robin", ) data = simulation.dpred() @@ -71,7 +63,6 @@ def test_Simulation2DCellCentered(self, tolerance=0.05): self.mesh, survey=self.survey, sigma=self.sigma, - solver=self.solver, bc_type="Robin", ) data = simulation.dpred() @@ -114,19 +105,11 @@ def setUp(self): self.data_ana = data_ana self.plotIt = False - try: - from pymatsolver import Pardiso - - self.solver = Pardiso - except ImportError: - self.solver = SolverLU - def test_Simulation2DNodal(self, tolerance=0.05): simulation = dc.simulation_2d.Simulation2DNodal( self.mesh, survey=self.survey, sigma=self.sigma, - solver=self.solver, bc_type="Robin", ) data = simulation.dpred() @@ -142,7 +125,6 @@ def test_Simulation2DCellCentered(self, tolerance=0.05): self.mesh, survey=self.survey, sigma=self.sigma, - solver=self.solver, bc_type="Robin", ) data = simulation.dpred() @@ -187,19 +169,11 @@ def setUp(self): self.data_ana = data_ana self.plotIt = False - try: - from pymatsolver import PardisoSolver - - self.solver = PardisoSolver - except ImportError: - self.solver = SolverLU - def test_Simulation2DNodal(self, tolerance=0.05): simulation = dc.simulation_2d.Simulation2DNodal( self.mesh, survey=self.survey, sigma=self.sigma, - solver=self.solver, bc_type="Robin", ) data = simulation.dpred() @@ -215,7 +189,6 @@ def test_Simulation2DCellCentered(self, tolerance=0.05): self.mesh, survey=self.survey, sigma=self.sigma, - solver=self.solver, bc_type="Robin", ) data = simulation.dpred() @@ -259,19 +232,11 @@ def setUp(self): self.sigma = sigma self.data_ana = data_ana - try: - from pymatsolver import PardisoSolver - - self.solver = PardisoSolver - except ImportError: - self.solver = SolverLU - def test_Simulation2DCellCentered(self, tolerance=0.05): simulation = dc.simulation_2d.Simulation2DCellCentered( self.mesh, survey=self.survey, sigma=self.sigma, - solver=self.solver, bc_type="Robin", ) data = simulation.dpred() @@ -287,7 +252,6 @@ def test_Simulation2DNodal(self, tolerance=0.05): self.mesh, survey=self.survey, sigma=self.sigma, - solver=self.solver, bc_type="Robin", ) data = simulation.dpred() @@ -345,19 +309,11 @@ def setUp(self): self.plotIt = False self.ROI_inds = ROI_inds - try: - from pymatsolver import PardisoSolver - - self.solver = PardisoSolver - except ImportError: - self.solver = SolverLU - def test_Simulation2DCellCentered(self, tolerance=0.05): simulation = dc.simulation_2d.Simulation2DCellCentered( self.mesh, survey=self.survey, sigma=self.sigma, - solver=self.solver, ) field = simulation.fields() @@ -374,7 +330,6 @@ def test_Simulation2DCellCentered_Dirichlet(self, tolerance=0.05): self.mesh, survey=self.survey, sigma=self.sigma, - solver=self.solver, bc_type="Dirichlet", ) field = simulation.fields() @@ -392,7 +347,6 @@ def test_Simulation2DNodal(self, tolerance=0.05): self.mesh, survey=self.survey, sigma=self.sigma, - solver=self.solver, ) field = simulation.fields() data = field[:, "phi"][:, 0] @@ -437,19 +391,11 @@ def setUp(self): self.sigma_half = sighalf self.plotIt = False - try: - from pymatsolver import Pardiso - - self.solver = Pardiso - except ImportError: - self.solver = SolverLU - def test_Simulation2DNodal(self, tolerance=0.05): simulation = dc.simulation_2d.Simulation2DNodal( self.mesh, survey=self.survey, sigma=self.sigma, - solver=self.solver, bc_type="Robin", ) with self.assertRaises(KeyError): @@ -468,7 +414,6 @@ def test_Simulation2DCellCentered(self, tolerance=0.05): self.mesh, survey=self.survey, sigma=self.sigma, - solver=self.solver, bc_type="Robin", ) with self.assertRaises(KeyError): diff --git a/tests/em/static/test_DC_2D_jvecjtvecadj.py b/tests/em/static/test_DC_2D_jvecjtvecadj.py index a06a21c98d..876b1a56df 100644 --- a/tests/em/static/test_DC_2D_jvecjtvecadj.py +++ b/tests/em/static/test_DC_2D_jvecjtvecadj.py @@ -13,11 +13,6 @@ ) from simpeg.electromagnetics import resistivity as dc -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - class DCProblem_2DTests(unittest.TestCase): formulation = "Simulation2DCellCentered" @@ -47,7 +42,6 @@ def setUp(self): mesh, rhoMap=maps.IdentityMap(mesh), storeJ=self.storeJ, - solver=Solver, survey=survey, bc_type=self.bc_type, ) diff --git a/tests/em/static/test_DC_FieldsDipoleFullspace.py b/tests/em/static/test_DC_FieldsDipoleFullspace.py index ac33a2f2b9..d36a43595e 100644 --- a/tests/em/static/test_DC_FieldsDipoleFullspace.py +++ b/tests/em/static/test_DC_FieldsDipoleFullspace.py @@ -5,10 +5,6 @@ import numpy as np from simpeg.electromagnetics import resistivity as dc -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver from geoana.em import fdem from scipy.constants import mu_0, epsilon_0 @@ -103,7 +99,6 @@ def test_Simulation3DCellCentered_Dirichlet(self, tolerance=0.1): simulation = dc.Simulation3DCellCentered( self.mesh, survey=self.survey, sigma=self.sigma, bc_type="Dirichlet" ) - simulation.solver = Solver f = simulation.fields() eNumeric = utils.mkvc(f[self.survey.source_list, "e"]) @@ -136,7 +131,6 @@ def test_Simulation3DCellCentered_Mixed(self, tolerance=0.1): simulation = dc.simulation.Simulation3DCellCentered( self.mesh, survey=self.survey, sigma=self.sigma, bc_type="Mixed" ) - simulation.solver = Solver f = simulation.fields() eNumeric = utils.mkvc(f[self.survey.source_list, "e"]) @@ -165,7 +159,6 @@ def test_Simulation3DCellCentered_Neumann(self, tolerance=0.1): simulation = dc.Simulation3DCellCentered( self.mesh, survey=self.survey, sigma=self.sigma, bc_type="Neumann" ) - simulation.solver = Solver f = simulation.fields() eNumeric = utils.mkvc(f[self.survey.source_list, "e"]) @@ -276,7 +269,6 @@ def test_Simulation3DNodal(self, tolerance=0.1): simulation = dc.simulation.Simulation3DNodal( self.mesh, survey=self.survey, sigma=self.sigma ) - simulation.solver = Solver f = simulation.fields() eNumeric = utils.mkvc(f[self.survey.source_list, "e"]) diff --git a/tests/em/static/test_DC_FieldsMultipoleFullspace.py b/tests/em/static/test_DC_FieldsMultipoleFullspace.py index d5e9b8ebfc..d0ff665636 100644 --- a/tests/em/static/test_DC_FieldsMultipoleFullspace.py +++ b/tests/em/static/test_DC_FieldsMultipoleFullspace.py @@ -5,11 +5,6 @@ import numpy as np from simpeg.electromagnetics import resistivity as dc -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - from geoana.em import fdem from scipy.constants import mu_0, epsilon_0 @@ -131,7 +126,6 @@ def test_Simulation3DCellCentered_Dirichlet(self, tolerance=0.1): simulation = dc.Simulation3DCellCentered( self.mesh, survey=self.survey, sigma=self.sigma, bc_type="Dirichlet" ) - simulation.solver = Solver f = simulation.fields() eNumeric = utils.mkvc(f[self.survey.source_list, "e"]) @@ -157,7 +151,6 @@ def test_Simulation3DCellCentered_Mixed(self, tolerance=0.1): simulation = dc.simulation.Simulation3DCellCentered( self.mesh, survey=self.survey, sigma=self.sigma, bc_type="Mixed" ) - simulation.solver = Solver f = simulation.fields() eNumeric = utils.mkvc(f[self.survey.source_list, "e"]) @@ -175,7 +168,6 @@ def test_Simulation3DCellCentered_Neumann(self, tolerance=0.1): simulation = dc.Simulation3DCellCentered( self.mesh, survey=self.survey, sigma=self.sigma, bc_type="Neumann" ) - simulation.solver = Solver f = simulation.fields() eNumeric = utils.mkvc(f[self.survey.source_list, "e"]) @@ -305,7 +297,6 @@ def test_Simulation3DNodal(self, tolerance=0.1): simulation = dc.simulation.Simulation3DNodal( self.mesh, survey=self.survey, sigma=self.sigma ) - simulation.solver = Solver f = simulation.fields() eNumeric = utils.mkvc(f[self.survey.source_list, "e"]) diff --git a/tests/em/static/test_DC_Utils.py b/tests/em/static/test_DC_Utils.py index 80c783cf28..8055041cdf 100644 --- a/tests/em/static/test_DC_Utils.py +++ b/tests/em/static/test_DC_Utils.py @@ -12,11 +12,6 @@ import shutil import os -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - class DCUtilsTests_halfspace(unittest.TestCase): def setUp(self): @@ -88,7 +83,6 @@ def test_io_rhoa(self): problem = dc.Simulation3DCellCentered( self.mesh, sigmaMap=expmap, survey=survey, bc_type="Neumann" ) - problem.solver = Solver # Create synthetic data dobs = problem.make_synthetic_data( diff --git a/tests/em/static/test_DC_analytic.py b/tests/em/static/test_DC_analytic.py index d26c1a1cca..e247877902 100644 --- a/tests/em/static/test_DC_analytic.py +++ b/tests/em/static/test_DC_analytic.py @@ -6,11 +6,6 @@ from simpeg.electromagnetics import resistivity as dc from simpeg.electromagnetics import analytics -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - class DCProblemAnalyticTests(unittest.TestCase): def setUp(self): @@ -55,7 +50,6 @@ def test_Simulation3DNodal(self, tolerance=0.05): self.mesh, survey=self.survey, sigma=self.sigma, - solver=Solver, bc_type="Neumann", ) data = simulation.dpred() @@ -77,7 +71,6 @@ def test_Simulation3DNodal_Robin(self, tolerance=0.05): self.mesh, survey=self.survey, sigma=self.sigma, - solver=Solver, bc_type="Robin", ) data = simulation.dpred() @@ -93,7 +86,6 @@ def test_Simulation3DCellCentered_Mixed(self, tolerance=0.05): survey=self.survey, sigma=self.sigma, bc_type="Mixed", - solver=Solver, ) data = simulation.dpred() @@ -116,7 +108,6 @@ def test_Simulation3DCellCentered_Neumann(self, tolerance=0.05): survey=self.survey, sigma=self.sigma, bc_type="Neumann", - solver=Solver, ) data = simulation.dpred() err = np.sqrt( @@ -178,7 +169,6 @@ def test_Simulation3DCellCentered_Dirichlet(self, tolerance=0.05): survey=self.survey, sigma=self.sigma, bc_type="Dirichlet", - solver=Solver, ) data = simulation.dpred() @@ -234,7 +224,6 @@ def test_Simulation3DCellCentered_Mixed(self, tolerance=0.05): survey=self.survey, sigma=self.sigma, bc_type="Mixed", - solver=Solver, ) data = simulation.dpred() err = np.sqrt( diff --git a/tests/em/static/test_DC_jvecjtvecadj.py b/tests/em/static/test_DC_jvecjtvecadj.py index 4b200dbbce..2eea8b3d71 100644 --- a/tests/em/static/test_DC_jvecjtvecadj.py +++ b/tests/em/static/test_DC_jvecjtvecadj.py @@ -13,7 +13,6 @@ ) from simpeg.utils import mkvc from simpeg.electromagnetics import resistivity as dc -from pymatsolver import Pardiso import shutil @@ -131,7 +130,6 @@ def setUp(self): mesh=mesh, survey=self.survey, sigmaMap=self.sigma_map, - solver=Pardiso, bc_type="Dirichlet", ) diff --git a/tests/em/static/test_DC_miniaturize.py b/tests/em/static/test_DC_miniaturize.py index c2b7bc1c78..317bbe4dd4 100644 --- a/tests/em/static/test_DC_miniaturize.py +++ b/tests/em/static/test_DC_miniaturize.py @@ -2,7 +2,6 @@ from simpeg.electromagnetics.static.utils.static_utils import generate_dcip_sources_line from simpeg import maps import numpy as np -from pymatsolver import Pardiso import discretize import os @@ -173,7 +172,6 @@ def setUp(self): self.sim1 = dc.Simulation2DNodal( survey=survey, mesh=mesh, - solver=Pardiso, storeJ=False, sigmaMap=maps.IdentityMap(mesh), miniaturize=False, @@ -182,7 +180,6 @@ def setUp(self): self.sim2 = dc.Simulation2DNodal( survey=survey, mesh=mesh, - solver=Pardiso, storeJ=False, sigmaMap=maps.IdentityMap(mesh), miniaturize=True, @@ -271,7 +268,6 @@ def setUp(self): self.sim1 = dc.Simulation3DNodal( survey=survey, mesh=mesh, - solver=Pardiso, storeJ=False, sigmaMap=maps.IdentityMap(mesh), miniaturize=False, @@ -280,7 +276,6 @@ def setUp(self): self.sim2 = dc.Simulation3DNodal( survey=survey, mesh=mesh, - solver=Pardiso, storeJ=False, sigmaMap=maps.IdentityMap(mesh), miniaturize=True, diff --git a/tests/em/static/test_IP_2D_fwd.py b/tests/em/static/test_IP_2D_fwd.py index 93c22b41e9..59c883a231 100644 --- a/tests/em/static/test_IP_2D_fwd.py +++ b/tests/em/static/test_IP_2D_fwd.py @@ -6,11 +6,6 @@ from simpeg.electromagnetics import resistivity as dc from simpeg.electromagnetics import induced_polarization as ip -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - class IPProblemAnalyticTests(unittest.TestCase): def setUp(self): @@ -57,7 +52,6 @@ def test_Simulation2DNodal(self): problemDC = dc.Simulation2DNodal( self.mesh, survey=self.surveyDC, sigmaMap=maps.IdentityMap(self.mesh) ) - problemDC.solver = Solver data0 = problemDC.dpred(self.sigma0) datainf = problemDC.dpred(self.sigmaInf) @@ -69,7 +63,6 @@ def test_Simulation2DNodal(self): sigma=self.sigmaInf, etaMap=maps.IdentityMap(self.mesh), ) - problemIP.solver = Solver data_full = data0 - datainf data = problemIP.dpred(self.eta) @@ -87,7 +80,6 @@ def test_Simulation2DCellCentered(self): problemDC = dc.Simulation2DCellCentered( self.mesh, survey=self.surveyDC, rhoMap=maps.IdentityMap(self.mesh) ) - problemDC.solver = Solver data0 = problemDC.dpred(1.0 / self.sigma0) finf = problemDC.fields(1.0 / self.sigmaInf) datainf = problemDC.dpred(1.0 / self.sigmaInf, f=finf) @@ -100,7 +92,6 @@ def test_Simulation2DCellCentered(self): rho=1.0 / self.sigmaInf, etaMap=maps.IdentityMap(self.mesh), ) - problemIP.solver = Solver data_full = data0 - datainf data = problemIP.dpred(self.eta) err = np.linalg.norm((data - data_full) / data_full) ** 2 / data_full.size @@ -165,7 +156,6 @@ def test_Simulation2DNodal(self): simDC = dc.Simulation2DNodal( self.mesh, sigmaMap=maps.IdentityMap(self.mesh), - solver=Solver, survey=self.survey_dc, ) data0 = simDC.dpred(self.sigma0) @@ -176,7 +166,6 @@ def test_Simulation2DNodal(self): self.mesh, sigma=self.sigmaInf, etaMap=maps.IdentityMap(self.mesh), - solver=Solver, survey=self.survey_ip, ) data = simIP.dpred(self.eta) @@ -185,7 +174,6 @@ def test_Simulation2DNodal(self): self.mesh, sigma=self.sigmaInf, etaMap=maps.IdentityMap(self.mesh), - solver=Solver, survey=self.survey_ip, storeJ=True, ) @@ -209,7 +197,6 @@ def test_Simulation2DCellCentered(self): simDC = dc.Simulation2DCellCentered( self.mesh, sigmaMap=maps.IdentityMap(self.mesh), - solver=Solver, survey=self.survey_dc, ) data0 = simDC.dpred(self.sigma0) @@ -220,7 +207,6 @@ def test_Simulation2DCellCentered(self): self.mesh, sigma=self.sigmaInf, etaMap=maps.IdentityMap(self.mesh), - solver=Solver, survey=self.survey_ip, ) data = simIP.dpred(self.eta) diff --git a/tests/em/static/test_IP_fwd.py b/tests/em/static/test_IP_fwd.py index 3d722f931c..1ba373f891 100644 --- a/tests/em/static/test_IP_fwd.py +++ b/tests/em/static/test_IP_fwd.py @@ -7,11 +7,6 @@ from simpeg.electromagnetics import resistivity as dc from simpeg.electromagnetics import induced_polarization as ip -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - class IPProblemAnalyticTests(unittest.TestCase): def setUp(self): @@ -55,7 +50,6 @@ def test_Simulation3DNodal(self): simulationdc = dc.simulation.Simulation3DNodal( mesh=self.mesh, survey=self.surveyDC, sigmaMap=maps.IdentityMap(self.mesh) ) - simulationdc.solver = Solver data0 = simulationdc.dpred(self.sigma0) finf = simulationdc.fields(self.sigmaInf) datainf = simulationdc.dpred(self.sigmaInf, f=finf) @@ -68,7 +62,6 @@ def test_Simulation3DNodal(self): Ainv=simulationdc.Ainv, _f=finf, ) - simulationip.solver = Solver data_full = data0 - datainf data = simulationip.dpred(self.eta) err = np.linalg.norm((data - data_full) / data_full) ** 2 / data_full.size @@ -84,7 +77,6 @@ def test_Simulation3DCellCentered(self): simulationdc = dc.simulation.Simulation3DCellCentered( mesh=self.mesh, survey=self.surveyDC, sigmaMap=maps.IdentityMap(self.mesh) ) - simulationdc.solver = Solver data0 = simulationdc.dpred(self.sigma0) finf = simulationdc.fields(self.sigmaInf) datainf = simulationdc.dpred(self.sigmaInf, f=finf) @@ -97,7 +89,6 @@ def test_Simulation3DCellCentered(self): Ainv=simulationdc.Ainv, _f=finf, ) - simulationip.solver = Solver data_full = data0 - datainf data = simulationip.dpred(self.eta) err = np.linalg.norm((data - data_full) / data_full) ** 2 / data_full.size @@ -157,7 +148,6 @@ def test_Simulation3DNodal(self): simulationdc = dc.simulation.Simulation3DNodal( self.mesh, sigmaMap=maps.IdentityMap(self.mesh), - solver=Solver, survey=self.survey_dc, ) data0 = simulationdc.dpred(self.sigma0) @@ -170,7 +160,6 @@ def test_Simulation3DNodal(self): sigma=self.sigmaInf, etaMap=maps.IdentityMap(self.mesh), Ainv=simulationdc.Ainv, - solver=Solver, ) data = simulationip.dpred(self.eta) @@ -180,7 +169,6 @@ def test_Simulation3DNodal(self): sigma=self.sigmaInf, etaMap=maps.IdentityMap(self.mesh), Ainv=simulationdc.Ainv, - solver=Solver, storeJ=True, ) data2 = simulationip_stored.dpred(self.eta) @@ -203,7 +191,6 @@ def test_Simulation3DCellCentered(self): simulationdc = dc.simulation.Simulation3DCellCentered( self.mesh, sigmaMap=maps.IdentityMap(self.mesh), - solver=Solver, survey=self.survey_dc, ) data0 = simulationdc.dpred(self.sigma0) @@ -216,7 +203,6 @@ def test_Simulation3DCellCentered(self): sigma=self.sigmaInf, etaMap=maps.IdentityMap(self.mesh), Ainv=simulationdc.Ainv, - solver=Solver, ) data = simulationip.dpred(self.eta) diff --git a/tests/em/static/test_SIP_2D_jvecjtvecadj.py b/tests/em/static/test_SIP_2D_jvecjtvecadj.py index d5050d35f1..f86f13b51d 100644 --- a/tests/em/static/test_SIP_2D_jvecjtvecadj.py +++ b/tests/em/static/test_SIP_2D_jvecjtvecadj.py @@ -14,11 +14,6 @@ import numpy as np from simpeg.electromagnetics import spectral_induced_polarization as sip -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - class SIPProblemTestsCC(unittest.TestCase): def setUp(self): @@ -61,7 +56,6 @@ def setUp(self): etaMap=wires.eta, tauiMap=wires.taui, verbose=False, - solver=Solver, survey=survey, ) mSynth = np.r_[eta, 1.0 / tau] @@ -156,7 +150,6 @@ def setUp(self): etaMap=wires.eta, tauiMap=wires.taui, verbose=False, - solver=Solver, survey=survey, ) mSynth = np.r_[eta, 1.0 / tau] @@ -261,7 +254,6 @@ def setUp(self): tauiMap=actmaptau * wires.taui, cMap=actmapc * wires.c, actinds=~airind, - solver=Solver, survey=survey, ) mSynth = np.r_[eta[~airind], 1.0 / tau[~airind], c[~airind]] diff --git a/tests/em/static/test_SIP_jvecjtvecadj.py b/tests/em/static/test_SIP_jvecjtvecadj.py index 9b903aaa61..258f2f0abc 100644 --- a/tests/em/static/test_SIP_jvecjtvecadj.py +++ b/tests/em/static/test_SIP_jvecjtvecadj.py @@ -13,11 +13,6 @@ import numpy as np from simpeg.electromagnetics import spectral_induced_polarization as sip -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - class SIPProblemTestsCC(unittest.TestCase): def setUp(self): @@ -69,7 +64,6 @@ def setUp(self): tauiMap=wires.taui, storeJ=False, ) - problem.solver = Solver mSynth = np.r_[eta, 1.0 / tau] problem.model = mSynth dobs = problem.make_synthetic_data(mSynth, add_noise=True, random_seed=40) @@ -169,7 +163,6 @@ def setUp(self): storeJ=False, ) print(survey.nD) - problem.solver = Solver mSynth = np.r_[eta, 1.0 / tau] print(survey.nD) dobs = problem.make_synthetic_data(mSynth, add_noise=True, random_seed=40) @@ -281,7 +274,6 @@ def setUp(self): verbose=False, ) - problem.solver = Solver mSynth = np.r_[eta[~airind], 1.0 / tau[~airind], c[~airind]] dobs = problem.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization diff --git a/tests/em/tdem/test_TDEM_DerivAdjoint.py b/tests/em/tdem/test_TDEM_DerivAdjoint.py index 9dc8f53cf2..7f2f2e5880 100644 --- a/tests/em/tdem/test_TDEM_DerivAdjoint.py +++ b/tests/em/tdem/test_TDEM_DerivAdjoint.py @@ -5,8 +5,6 @@ from simpeg import maps, tests from simpeg.electromagnetics import time_domain as tdem -from pymatsolver import Pardiso as Solver - plotIt = False testDeriv = True @@ -46,7 +44,6 @@ def get_prob(mesh, mapping, formulation, **kwargs): mesh, sigmaMap=mapping, **kwargs ) prb.time_steps = [(1e-05, 10), (5e-05, 10), (2.5e-4, 10)] - prb.solver = Solver return prb diff --git a/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py b/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py index 3fe03cf1be..e3d24240a8 100644 --- a/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py +++ b/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py @@ -6,7 +6,6 @@ from simpeg.electromagnetics import time_domain as tdem from simpeg.electromagnetics import utils from scipy.interpolate import interp1d -from pymatsolver import Pardiso as Solver import pytest plotIt = False @@ -47,7 +46,6 @@ def get_prob(mesh, mapping, formulation, **kwargs): prb = getattr(tdem, "Simulation3D{}".format(formulation))( mesh, sigmaMap=mapping, **kwargs ) - prb.solver = Solver return prb diff --git a/tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py b/tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py index 7255e54d37..6579121598 100644 --- a/tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py +++ b/tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py @@ -3,7 +3,6 @@ import discretize from simpeg import maps, tests from simpeg.electromagnetics import time_domain as tdem -from pymatsolver import Pardiso as Solver plotIt = False @@ -60,7 +59,6 @@ def setUp_TDEM(prbtype="ElectricField", rxcomp="ElectricFieldx", src_z=0.0): prb = getattr(tdem, "Simulation3D{}".format(prbtype))( mesh, survey=survey, time_steps=time_steps, sigmaMap=mapping ) - prb.solver = Solver return prb, m, mesh diff --git a/tests/em/tdem/test_TDEM_crosscheck.py b/tests/em/tdem/test_TDEM_crosscheck.py index 6792b39bbc..4c1de54eba 100644 --- a/tests/em/tdem/test_TDEM_crosscheck.py +++ b/tests/em/tdem/test_TDEM_crosscheck.py @@ -5,8 +5,6 @@ from simpeg.electromagnetics import time_domain as tdem import numpy as np -from pymatsolver import Pardiso as Solver - TOL = 1e-4 FLR = 1e-20 @@ -70,7 +68,6 @@ def setUp_TDEM( prb = getattr(tdem, "Simulation3D{}".format(prbtype))( mesh, survey=survey, time_steps=time_steps, sigmaMap=mapping ) - prb.solver = Solver rng = np.random.default_rng(seed=42) m = np.log(1e-1) * np.ones(prb.sigmaMap.nP) + 1e-2 * rng.uniform( diff --git a/tests/em/tdem/test_TDEM_forward_Analytic.py b/tests/em/tdem/test_TDEM_forward_Analytic.py index 1c6e85c18e..8eae1b5da3 100644 --- a/tests/em/tdem/test_TDEM_forward_Analytic.py +++ b/tests/em/tdem/test_TDEM_forward_Analytic.py @@ -3,7 +3,6 @@ import discretize import matplotlib.pyplot as plt import numpy as np -from pymatsolver import Pardiso as Solver from scipy.constants import mu_0 from simpeg import maps from simpeg.electromagnetics import analytics @@ -163,7 +162,6 @@ def analytic_wholespace_dipole_comparison( mesh=mesh, survey=survey, sigmaMap=mapping ) - sim.solver = Solver sim.time_steps = [ (1e-06, 40), (5e-06, 40), @@ -267,7 +265,6 @@ def analytic_halfspace_mag_dipole_comparison( sim = tdem.Simulation3DMagneticFluxDensity( mesh, survey=survey, time_steps=time_steps, sigmaMap=mapping ) - sim.solver = Solver sigma = np.ones(mesh.shape_cells[2]) * 1e-8 sigma[active] = sig_half diff --git a/tests/em/tdem/test_TDEM_forward_Analytic_RawWaveform.py b/tests/em/tdem/test_TDEM_forward_Analytic_RawWaveform.py index c5a8e9ba63..5e25069fc0 100644 --- a/tests/em/tdem/test_TDEM_forward_Analytic_RawWaveform.py +++ b/tests/em/tdem/test_TDEM_forward_Analytic_RawWaveform.py @@ -3,7 +3,6 @@ import discretize import matplotlib.pyplot as plt import numpy as np -from pymatsolver import Pardiso as Solver from scipy.constants import mu_0 from scipy.interpolate import interp1d from simpeg import maps @@ -79,7 +78,6 @@ def halfSpaceProblemAnaDiff( prb = tdem.Simulation3DMagneticFluxDensity( mesh, survey=survey, sigmaMap=mapping, time_steps=time_steps ) - prb.solver = Solver sigma = np.ones(mesh.shape_cells[2]) * 1e-8 sigma[active] = sig_half diff --git a/tests/em/tdem/test_TDEM_grounded.py b/tests/em/tdem/test_TDEM_grounded.py index 4f9587234b..436c5f70d5 100644 --- a/tests/em/tdem/test_TDEM_grounded.py +++ b/tests/em/tdem/test_TDEM_grounded.py @@ -6,7 +6,6 @@ import discretize from simpeg.electromagnetics import time_domain as tdem from simpeg import maps, tests -from pymatsolver import Pardiso class TestGroundedSourceTDEM_j(unittest.TestCase): @@ -76,7 +75,6 @@ def setUpClass(self): time_steps=time_steps, mu=mu, sigmaMap=maps.ExpMap(mesh), - solver=Pardiso, ) survey = tdem.Survey([src]) diff --git a/tests/em/tdem/test_TDEM_inductive_permeable.py b/tests/em/tdem/test_TDEM_inductive_permeable.py index 5e0a2cd5f9..b0bd644f34 100644 --- a/tests/em/tdem/test_TDEM_inductive_permeable.py +++ b/tests/em/tdem/test_TDEM_inductive_permeable.py @@ -10,7 +10,6 @@ from simpeg.electromagnetics import time_domain as tdem from simpeg import utils, maps -from pymatsolver import Pardiso plotIt = False TOL = 1e-4 @@ -158,14 +157,12 @@ def populate_target(mur): survey=survey, time_steps=time_steps, sigmaMap=maps.IdentityMap(mesh), - solver=Pardiso, ) prob_late_ontime = tdem.Simulation3DMagneticFluxDensity( mesh=mesh, survey=survey_late_ontime, time_steps=time_steps, sigmaMap=maps.IdentityMap(mesh), - solver=Pardiso, ) fields_dict = {} diff --git a/tests/flow/test_Richards.py b/tests/flow/test_Richards.py index 71e9cc31b0..5b63e1b967 100644 --- a/tests/flow/test_Richards.py +++ b/tests/flow/test_Richards.py @@ -8,12 +8,6 @@ from simpeg import utils from simpeg.flow import richards -try: - from pymatsolver import Pardiso as Solver -except Exception: - from simpeg import Solver - - TOL = 1e-8 np.random.seed(0) @@ -46,7 +40,6 @@ def setUp(self): method="mixed", ) prob.time_steps = time_steps - prob.solver = Solver self.h0 = h self.mesh = mesh diff --git a/tests/pf/test_forward_PFproblem.py b/tests/pf/test_forward_PFproblem.py index 0777e45c56..2c54300d7a 100644 --- a/tests/pf/test_forward_PFproblem.py +++ b/tests/pf/test_forward_PFproblem.py @@ -4,7 +4,6 @@ from simpeg.utils.model_builder import get_indices_sphere from simpeg.potential_fields import magnetics as mag import numpy as np -from pymatsolver import Pardiso class MagFwdProblemTests(unittest.TestCase): @@ -52,7 +51,6 @@ def setUp(self): M, survey=self.survey, muMap=maps.ChiMap(M), - solver=Pardiso, ) self.M = M self.chi = chi diff --git a/tests/pf/test_sensitivity_PFproblem.py b/tests/pf/test_sensitivity_PFproblem.py index eb39c97485..2a1fe6681c 100644 --- a/tests/pf/test_sensitivity_PFproblem.py +++ b/tests/pf/test_sensitivity_PFproblem.py @@ -4,7 +4,6 @@ # #from simpegPF import BaseMag # #import matplotlib.pyplot as plt # import discretize -# from pymatsolver import Pardiso # #import simpeg.PF as PF # from simpeg import maps, utils # from simpeg.potential_fields import magnetics as mag @@ -49,7 +48,6 @@ # M, # survey=self.survey, # muMap=maps.ChiMap(M), -# solver=Pardiso, # ) # dpre = self.sim.dpred(chi) # diff --git a/tests/utils/test_default_solver.py b/tests/utils/test_default_solver.py new file mode 100644 index 0000000000..f95a3cf4d6 --- /dev/null +++ b/tests/utils/test_default_solver.py @@ -0,0 +1,46 @@ +import pytest +from pymatsolver import SolverCG + +from simpeg.utils.solver_utils import ( + get_default_solver, + set_default_solver, + DefaultSolverWarning, +) + + +@pytest.fixture(autouse=True) +def reset_default_solver(): + # This should get automatically used + with pytest.warns(DefaultSolverWarning): + initial_default = get_default_solver() + yield + set_default_solver(initial_default) + + +def test_default_setting(): + set_default_solver(SolverCG) + + with pytest.warns(DefaultSolverWarning, match="Using the default solver: SolverCG"): + new_default = get_default_solver() + + assert new_default == SolverCG + + +def test_default_error(): + class Temp: + pass + + with pytest.warns(DefaultSolverWarning): + initial_default = get_default_solver() + + with pytest.raises( + TypeError, + match="Default solver must be a subclass of pymatsolver.solvers.Base.", + ): + set_default_solver(Temp) + + with pytest.warns(DefaultSolverWarning): + after_default = get_default_solver() + + # make sure we didn't accidentally set the default. + assert initial_default == after_default diff --git a/tests/utils/test_solverwrap.py b/tests/utils/test_solverwrap.py deleted file mode 100644 index 126f4466ee..0000000000 --- a/tests/utils/test_solverwrap.py +++ /dev/null @@ -1,64 +0,0 @@ -import unittest -from simpeg.utils.solver_utils import Solver, SolverLU, SolverCG, SolverBiCG, SolverDiag -import scipy.sparse as sp -import numpy as np - - -class TestSolve(unittest.TestCase): - def setUp(self): - # Create a random matrix - n = 400 - A = sp.random(n, n, density=0.25) - - self.n = n - self.A = 0.5 * (A + A.T) + n * sp.eye(n) - - def test_Solver(self): - x = np.random.rand(self.n) - b = self.A @ x - - with self.assertWarns(Warning): - Ainv = Solver(self.A, bad_kwarg=312) - x2 = Ainv @ b - np.testing.assert_almost_equal(x, x2) - - def test_SolverLU(self): - x = np.random.rand(self.n) - b = self.A @ x - - with self.assertWarns(Warning): - Ainv = SolverLU(self.A, bad_kwarg=312) - x2 = Ainv @ b - np.testing.assert_almost_equal(x, x2) - - def test_SolverCG(self): - x = np.random.rand(self.n) - b = self.A @ x - - with self.assertWarns(Warning): - Ainv = SolverCG(self.A, bad_kwarg=312) - x2 = Ainv @ b - np.testing.assert_almost_equal(x, x2, decimal=4) - - def test_SolverBiCG(self): - x = np.random.rand(self.n) - b = self.A @ x - - with self.assertWarns(Warning): - Ainv = SolverBiCG(self.A, bad_kwarg=312) - x2 = Ainv @ b - np.testing.assert_almost_equal(x, x2, decimal=4) - - def test_SolverDiag(self): - x = np.random.rand(self.n) - A = sp.diags(np.random.randn(self.n)) - b = A @ x - - with self.assertWarns(Warning): - Ainv = SolverDiag(A, bad_kwarg=312) - x2 = Ainv @ b - np.testing.assert_almost_equal(x, x2) - - -if __name__ == "__main__": - unittest.main() diff --git a/tutorials/05-dcr/plot_fwd_2_dcr2d.py b/tutorials/05-dcr/plot_fwd_2_dcr2d.py index 8b88ec4ba7..89f0556cb8 100644 --- a/tutorials/05-dcr/plot_fwd_2_dcr2d.py +++ b/tutorials/05-dcr/plot_fwd_2_dcr2d.py @@ -40,10 +40,6 @@ import matplotlib.pyplot as plt from matplotlib.colors import LogNorm -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver write_output = False mpl.rcParams.update({"font.size": 16}) @@ -233,7 +229,7 @@ # simulation = dc.simulation_2d.Simulation2DNodal( - mesh, survey=survey, sigmaMap=conductivity_map, solver=Solver + mesh, survey=survey, sigmaMap=conductivity_map ) # Predict the data by running the simulation. The data are the raw voltage in diff --git a/tutorials/05-dcr/plot_fwd_3_dcr3d.py b/tutorials/05-dcr/plot_fwd_3_dcr3d.py index ea7293d267..b6ee498bde 100644 --- a/tutorials/05-dcr/plot_fwd_3_dcr3d.py +++ b/tutorials/05-dcr/plot_fwd_3_dcr3d.py @@ -54,10 +54,6 @@ has_plotly = False pass -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver mpl.rcParams.update({"font.size": 16}) write_output = False @@ -257,7 +253,9 @@ # Define the DC simulation simulation = dc.simulation.Simulation3DNodal( - mesh, survey=survey, sigmaMap=conductivity_map, solver=Solver + mesh, + survey=survey, + sigmaMap=conductivity_map, ) # Predict the data by running the simulation. The data are the measured voltage diff --git a/tutorials/05-dcr/plot_inv_2_dcr2d.py b/tutorials/05-dcr/plot_inv_2_dcr2d.py index f28bb8d26d..063ccedc70 100644 --- a/tutorials/05-dcr/plot_inv_2_dcr2d.py +++ b/tutorials/05-dcr/plot_inv_2_dcr2d.py @@ -48,10 +48,6 @@ ) from simpeg.utils.io_utils.io_utils_electromagnetics import read_dcip2d_ubc -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver mpl.rcParams.update({"font.size": 16}) # sphinx_gallery_thumbnail_number = 4 @@ -262,7 +258,7 @@ # Define the problem. Define the cells below topography and the mapping simulation = dc.simulation_2d.Simulation2DNodal( - mesh, survey=survey, sigmaMap=conductivity_map, solver=Solver, storeJ=True + mesh, survey=survey, sigmaMap=conductivity_map, storeJ=True ) ####################################################################### @@ -371,7 +367,7 @@ ind_resistor = model_builder.get_indices_sphere(np.r_[120.0, -180.0], 60.0, mesh.gridCC) true_conductivity_model[ind_resistor] = true_resistor_conductivity -true_conductivity_model[~ind_active] = np.NaN +true_conductivity_model[~ind_active] = np.nan ############################################################ # Plotting True and Recovered Conductivity Model @@ -402,7 +398,7 @@ fig = plt.figure(figsize=(9, 4)) recovered_conductivity = conductivity_map * recovered_conductivity_model -recovered_conductivity[~ind_active] = np.NaN +recovered_conductivity[~ind_active] = np.nan ax1 = fig.add_axes([0.14, 0.17, 0.68, 0.7]) mesh.plot_image( diff --git a/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py b/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py index 5405e6a819..d030b52a0f 100644 --- a/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py +++ b/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py @@ -49,11 +49,6 @@ ) from simpeg.utils.io_utils.io_utils_electromagnetics import read_dcip2d_ubc -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - mpl.rcParams.update({"font.size": 16}) # sphinx_gallery_thumbnail_number = 3 @@ -268,7 +263,7 @@ # Define the problem. Define the cells below topography and the mapping simulation = dc.simulation_2d.Simulation2DNodal( - mesh, survey=survey, sigmaMap=conductivity_map, solver=Solver, storeJ=True + mesh, survey=survey, sigmaMap=conductivity_map, storeJ=True ) ####################################################################### @@ -385,7 +380,7 @@ ind_resistor = model_builder.get_indices_sphere(np.r_[120.0, -180.0], 60.0, mesh.gridCC) true_conductivity_model[ind_resistor] = true_resistor_conductivity -true_conductivity_model[~ind_active] = np.NaN +true_conductivity_model[~ind_active] = np.nan ############################################################ # Plotting True and Recovered Conductivity Model @@ -394,10 +389,10 @@ # Get L2 and sparse recovered model in base 10 l2_conductivity = conductivity_map * inv_prob.l2model -l2_conductivity[~ind_active] = np.NaN +l2_conductivity[~ind_active] = np.nan recovered_conductivity = conductivity_map * recovered_conductivity_model -recovered_conductivity[~ind_active] = np.NaN +recovered_conductivity[~ind_active] = np.nan # Plot True Model norm = LogNorm(vmin=1e-3, vmax=1e-1) diff --git a/tutorials/05-dcr/plot_inv_3_dcr3d.py b/tutorials/05-dcr/plot_inv_3_dcr3d.py index 4ca93a6106..60675957c9 100644 --- a/tutorials/05-dcr/plot_inv_3_dcr3d.py +++ b/tutorials/05-dcr/plot_inv_3_dcr3d.py @@ -61,10 +61,6 @@ has_plotly = False pass -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver mpl.rcParams.update({"font.size": 16}) @@ -275,7 +271,7 @@ # dc_simulation = dc.simulation.Simulation3DNodal( - mesh, survey=dc_survey, sigmaMap=conductivity_map, solver=Solver, storeJ=True + mesh, survey=dc_survey, sigmaMap=conductivity_map, storeJ=True ) ################################################################# diff --git a/tutorials/06-ip/plot_fwd_2_dcip2d.py b/tutorials/06-ip/plot_fwd_2_dcip2d.py index a24187b473..4c89e6bf83 100644 --- a/tutorials/06-ip/plot_fwd_2_dcip2d.py +++ b/tutorials/06-ip/plot_fwd_2_dcip2d.py @@ -49,11 +49,6 @@ import matplotlib.pyplot as plt from matplotlib.colors import LogNorm -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - mpl.rcParams.update({"font.size": 16}) write_output = False @@ -245,9 +240,7 @@ # argument *rhoMap* is defined, the simulation will expect a resistivity model. # -dc_simulation = dc.Simulation2DNodal( - mesh, survey=dc_survey, sigmaMap=conductivity_map, solver=Solver -) +dc_simulation = dc.Simulation2DNodal(mesh, survey=dc_survey, sigmaMap=conductivity_map) # Predict the data by running the simulation. The data are the raw voltage in # units of volts. @@ -397,7 +390,6 @@ survey=ip_survey, etaMap=chargeability_map, sigma=conductivity_map * conductivity_model, - solver=Solver, ) # Run forward simulation and predicted IP data. The data are the voltage (V) diff --git a/tutorials/06-ip/plot_fwd_3_dcip3d.py b/tutorials/06-ip/plot_fwd_3_dcip3d.py index 96d57ba8e4..3a6f1a2ff7 100644 --- a/tutorials/06-ip/plot_fwd_3_dcip3d.py +++ b/tutorials/06-ip/plot_fwd_3_dcip3d.py @@ -57,10 +57,6 @@ has_plotly = False pass -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver mpl.rcParams.update({"font.size": 16}) write_output = False @@ -262,9 +258,7 @@ # # Define the DC simulation -dc_simulation = dc.Simulation3DNodal( - mesh, survey=dc_survey, sigmaMap=conductivity_map, solver=Solver -) +dc_simulation = dc.Simulation3DNodal(mesh, survey=dc_survey, sigmaMap=conductivity_map) # Predict the data by running the simulation. The data are the measured voltage # normalized by the source current in units of V/A. @@ -439,7 +433,6 @@ survey=ip_survey, etaMap=chargeability_map, sigma=conductivity_map * conductivity_model, - solver=Solver, ) # Run forward simulation and predicted IP data. The data are the voltage (V) diff --git a/tutorials/06-ip/plot_inv_2_dcip2d.py b/tutorials/06-ip/plot_inv_2_dcip2d.py index 72cf5c2639..ccf1aba74e 100644 --- a/tutorials/06-ip/plot_inv_2_dcip2d.py +++ b/tutorials/06-ip/plot_inv_2_dcip2d.py @@ -54,10 +54,6 @@ ) from simpeg.utils.io_utils.io_utils_electromagnetics import read_dcip2d_ubc -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver mpl.rcParams.update({"font.size": 16}) # sphinx_gallery_thumbnail_number = 7 @@ -280,7 +276,7 @@ # Define the problem. Define the cells below topography and the mapping dc_simulation = dc.Simulation2DNodal( - mesh, survey=dc_survey, sigmaMap=conductivity_map, solver=Solver, storeJ=True + mesh, survey=dc_survey, sigmaMap=conductivity_map, storeJ=True ) ####################################################################### @@ -395,7 +391,7 @@ ind_resistor = model_builder.get_indices_sphere(np.r_[120.0, -180.0], 60.0, mesh.gridCC) true_conductivity_model[ind_resistor] = true_resistor_conductivity -true_conductivity_model[~ind_active] = np.NaN +true_conductivity_model[~ind_active] = np.nan # Plot True Model norm = LogNorm(vmin=1e-3, vmax=1e-1) @@ -421,7 +417,7 @@ fig = plt.figure(figsize=(9, 4)) recovered_conductivity = conductivity_map * recovered_conductivity_model -recovered_conductivity[~ind_active] = np.NaN +recovered_conductivity[~ind_active] = np.nan ax1 = fig.add_axes([0.14, 0.17, 0.68, 0.7]) mesh.plot_image( @@ -519,7 +515,6 @@ survey=ip_survey, etaMap=chargeability_map, sigma=conductivity_map * recovered_conductivity_model, - solver=Solver, storeJ=True, ) @@ -601,10 +596,10 @@ true_chargeability_model = np.zeros(len(mesh)) true_chargeability_model[ind_conductor] = sphere_chargeability -true_chargeability_model[~ind_active] = np.NaN +true_chargeability_model[~ind_active] = np.nan recovered_chargeability = chargeability_map * recovered_chargeability_model -recovered_chargeability[~ind_active] = np.NaN +recovered_chargeability[~ind_active] = np.nan # Plot True Model fig = plt.figure(figsize=(9, 4)) diff --git a/tutorials/06-ip/plot_inv_3_dcip3d.py b/tutorials/06-ip/plot_inv_3_dcip3d.py index 94b4cd9407..bde2f90ed2 100644 --- a/tutorials/06-ip/plot_inv_3_dcip3d.py +++ b/tutorials/06-ip/plot_inv_3_dcip3d.py @@ -63,11 +63,6 @@ has_plotly = False pass -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - mpl.rcParams.update({"font.size": 16}) # sphinx_gallery_thumbnail_number = 7 @@ -323,7 +318,7 @@ # dc_simulation = dc.Simulation3DNodal( - mesh, survey=dc_survey, sigmaMap=conductivity_map, solver=Solver, storeJ=True + mesh, survey=dc_survey, sigmaMap=conductivity_map, storeJ=True ) ################################################################# @@ -591,7 +586,6 @@ survey=ip_survey, etaMap=chargeability_map, sigma=conductivity_map * recovered_conductivity_model, - solver=Solver, storeJ=True, ) diff --git a/tutorials/07-fdem/plot_fwd_2_fem_cyl.py b/tutorials/07-fdem/plot_fwd_2_fem_cyl.py index 9bace19d92..450db8f27c 100644 --- a/tutorials/07-fdem/plot_fwd_2_fem_cyl.py +++ b/tutorials/07-fdem/plot_fwd_2_fem_cyl.py @@ -34,11 +34,6 @@ import matplotlib as mpl import matplotlib.pyplot as plt -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - write_file = False # sphinx_gallery_thumbnail_number = 2 @@ -184,7 +179,9 @@ # simulation = fdem.simulation.Simulation3DMagneticFluxDensity( - mesh, survey=survey, sigmaMap=model_map, solver=Solver + mesh, + survey=survey, + sigmaMap=model_map, ) ###################################################### diff --git a/tutorials/07-fdem/plot_fwd_3_fem_3d.py b/tutorials/07-fdem/plot_fwd_3_fem_3d.py index f49bb96056..b7c4345257 100644 --- a/tutorials/07-fdem/plot_fwd_3_fem_3d.py +++ b/tutorials/07-fdem/plot_fwd_3_fem_3d.py @@ -40,10 +40,6 @@ import matplotlib as mpl import matplotlib.pyplot as plt -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver save_file = False @@ -224,7 +220,7 @@ # simulation = fdem.simulation.Simulation3DMagneticFluxDensity( - mesh, survey=survey, sigmaMap=model_map, solver=Solver + mesh, survey=survey, sigmaMap=model_map ) ###################################################### diff --git a/tutorials/08-tdem/plot_fwd_2_tem_cyl.py b/tutorials/08-tdem/plot_fwd_2_tem_cyl.py index f3b81c0361..fe6da5d38c 100644 --- a/tutorials/08-tdem/plot_fwd_2_tem_cyl.py +++ b/tutorials/08-tdem/plot_fwd_2_tem_cyl.py @@ -36,11 +36,6 @@ import matplotlib as mpl import matplotlib.pyplot as plt -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - write_file = False # sphinx_gallery_thumbnail_number = 2 @@ -204,7 +199,7 @@ # simulation = tdem.simulation.Simulation3DMagneticFluxDensity( - mesh, survey=survey, sigmaMap=model_map, solver=Solver + mesh, survey=survey, sigmaMap=model_map ) # Set the time-stepping for the simulation diff --git a/tutorials/08-tdem/plot_fwd_3_tem_3d.py b/tutorials/08-tdem/plot_fwd_3_tem_3d.py index 9b9aecbf79..ab7423170a 100644 --- a/tutorials/08-tdem/plot_fwd_3_tem_3d.py +++ b/tutorials/08-tdem/plot_fwd_3_tem_3d.py @@ -40,10 +40,6 @@ import matplotlib.pyplot as plt import os -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver save_file = False @@ -284,7 +280,7 @@ # simulation = tdem.simulation.Simulation3DMagneticFluxDensity( - mesh, survey=survey, sigmaMap=model_map, solver=Solver, t0=-0.002 + mesh, survey=survey, sigmaMap=model_map, t0=-0.002 ) # Set the time-stepping for the simulation diff --git a/tutorials/10-vrm/plot_fwd_3_vrm_tem.py b/tutorials/10-vrm/plot_fwd_3_vrm_tem.py index bdeb87ac5b..0d7adc2f5f 100644 --- a/tutorials/10-vrm/plot_fwd_3_vrm_tem.py +++ b/tutorials/10-vrm/plot_fwd_3_vrm_tem.py @@ -39,11 +39,6 @@ import matplotlib.pyplot as plt import matplotlib as mpl -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - # sphinx_gallery_thumbnail_number = 3 ################################################################### @@ -156,7 +151,9 @@ time_steps = [(5e-06, 20), (0.0001, 20), (0.001, 21)] tdem_simulation = tdem.simulation.Simulation3DMagneticFluxDensity( - mesh, survey=tdem_survey, sigmaMap=model_map, solver=Solver + mesh, + survey=tdem_survey, + sigmaMap=model_map, ) tdem_simulation.time_steps = time_steps diff --git a/tutorials/13-joint_inversion/plot_inv_3_cross_gradient_pf.py b/tutorials/13-joint_inversion/plot_inv_3_cross_gradient_pf.py index 5450c36dcb..e7d6fd3843 100755 --- a/tutorials/13-joint_inversion/plot_inv_3_cross_gradient_pf.py +++ b/tutorials/13-joint_inversion/plot_inv_3_cross_gradient_pf.py @@ -418,10 +418,10 @@ # values on active cells. true_model_dens = np.loadtxt(dir_path + "true_model_dens.txt") -true_model_dens[~ind_active] = np.NaN +true_model_dens[~ind_active] = np.nan true_model_susc = np.loadtxt(dir_path + "true_model_susc.txt") -true_model_susc[~ind_active] = np.NaN +true_model_susc[~ind_active] = np.nan # Plot True Model fig = plt.figure(figsize=(9, 8)) diff --git a/tutorials/_temporary/plot_4c_fdem3d_inversion.py b/tutorials/_temporary/plot_4c_fdem3d_inversion.py index c035e10caf..86633314b3 100644 --- a/tutorials/_temporary/plot_4c_fdem3d_inversion.py +++ b/tutorials/_temporary/plot_4c_fdem3d_inversion.py @@ -46,11 +46,6 @@ utils, ) -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - # sphinx_gallery_thumbnail_number = 3 ############################################# @@ -289,7 +284,9 @@ # simulation = fdem.simulation.Simulation3DMagneticFluxDensity( - mesh, survey=survey, sigmaMap=model_map, Solver=Solver + mesh, + survey=survey, + sigmaMap=model_map, ) diff --git a/tutorials/_temporary/plot_fwd_1_em1dtm_stitched_skytem.py b/tutorials/_temporary/plot_fwd_1_em1dtm_stitched_skytem.py index f4694830de..85a9b66fb8 100644 --- a/tutorials/_temporary/plot_fwd_1_em1dtm_stitched_skytem.py +++ b/tutorials/_temporary/plot_fwd_1_em1dtm_stitched_skytem.py @@ -18,7 +18,6 @@ import matplotlib as mpl from matplotlib import pyplot as plt from discretize import TensorMesh -from pymatsolver import PardisoSolver from simpeg import maps from simpeg.utils import mkvc @@ -265,7 +264,6 @@ def PolygonInd(mesh, pts): parallel=False, n_cpu=2, verbose=True, - Solver=PardisoSolver, ) # simulation.model = sounding_models diff --git a/tutorials/_temporary/plot_inv_1_em1dtm_stitched_skytem.py b/tutorials/_temporary/plot_inv_1_em1dtm_stitched_skytem.py index 7248f413f8..cfdfc04a4c 100644 --- a/tutorials/_temporary/plot_inv_1_em1dtm_stitched_skytem.py +++ b/tutorials/_temporary/plot_inv_1_em1dtm_stitched_skytem.py @@ -18,7 +18,6 @@ import matplotlib as mpl from matplotlib import pyplot as plt from discretize import TensorMesh -from pymatsolver import PardisoSolver from simpeg.utils import mkvc from simpeg import ( @@ -224,12 +223,11 @@ thicknesses=thicknesses, sigmaMap=mapping, topo=topo, - Solver=PardisoSolver, ) # simulation = em1d.simulation.StitchedEM1DTMSimulation( # survey=survey, thicknesses=thicknesses, sigmaMap=mapping, -# topo=topo, parallel=True, n_cpu=4, verbose=True, Solver=PardisoSolver +# topo=topo, parallel=True, n_cpu=4, verbose=True # ) From 9d14515400d85ab4a0c9378c1a76fc852c881a87 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Fri, 25 Oct 2024 00:12:54 -0600 Subject: [PATCH 079/194] Fixes for most recent geoana 0.7 (#1557) #### Summary Updates for geoana 0.7. #### What does this implement/fix? 0.7 geoana will no assume zero at singularities, instead filling with `np.nan`. Now we must explicitly remove any potential singular points after evaluations. --- .ci/environment_test.yml | 2 +- environment.yml | 2 +- pyproject.toml | 2 +- simpeg/electromagnetics/frequency_domain/sources.py | 12 +++++++++--- simpeg/electromagnetics/time_domain/sources.py | 8 ++++++-- 5 files changed, 18 insertions(+), 8 deletions(-) diff --git a/.ci/environment_test.yml b/.ci/environment_test.yml index 18af7dbaad..0e4a0e33b9 100644 --- a/.ci/environment_test.yml +++ b/.ci/environment_test.yml @@ -7,7 +7,7 @@ dependencies: - pymatsolver>=0.3 - matplotlib-base - discretize>=0.10 - - geoana>=0.5.0 + - geoana>=0.7 - empymod>=2.0.0 # optional dependencies diff --git a/environment.yml b/environment.yml index 970f353c70..7fe57d785e 100644 --- a/environment.yml +++ b/environment.yml @@ -9,7 +9,7 @@ dependencies: - pymatsolver>=0.3 - matplotlib-base - discretize>=0.10 - - geoana>=0.5.0 + - geoana>=0.7 - empymod>=2.0.0 # solver diff --git a/pyproject.toml b/pyproject.toml index 00dd797df9..2335c5c4d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ dependencies = [ "pymatsolver>=0.3", "matplotlib", "discretize>=0.10", - "geoana>=0.5.0", + "geoana>=0.7", "empymod>=2.0.0", ] classifiers = [ diff --git a/simpeg/electromagnetics/frequency_domain/sources.py b/simpeg/electromagnetics/frequency_domain/sources.py index c92ab26b3c..9f7ea1a566 100644 --- a/simpeg/electromagnetics/frequency_domain/sources.py +++ b/simpeg/electromagnetics/frequency_domain/sources.py @@ -473,7 +473,9 @@ def _dipole(self): return self.__dipole def _srcFct(self, obsLoc, coordinates="cartesian"): - return self._dipole.vector_potential(obsLoc, coordinates=coordinates) + out = self._dipole.vector_potential(obsLoc, coordinates=coordinates) + out[np.isnan(out)] = 0 + return out def bPrimary(self, simulation): """Compute primary magnetic flux density. @@ -683,7 +685,9 @@ def _srcFct(self, obsLoc, coordinates="cartesian"): location=self.location, moment=self.moment, ) - return self._dipole.magnetic_flux_density(obsLoc, coordinates=coordinates) + out = self._dipole.magnetic_flux_density(obsLoc, coordinates=coordinates) + out[np.isnan(out)] = 0 + return out def bPrimary(self, simulation): """ @@ -869,7 +873,9 @@ def _srcFct(self, obsLoc, coordinates="cartesian"): radius=self.radius, current=self.current, ) - return self.n_turns * self._loop.vector_potential(obsLoc, coordinates) + out = self._loop.vector_potential(obsLoc, coordinates) + out[np.isnan(out)] = 0 + return self.n_turns * out N = deprecate_property( n_turns, "N", "n_turns", removal_version="0.19.0", error=True diff --git a/simpeg/electromagnetics/time_domain/sources.py b/simpeg/electromagnetics/time_domain/sources.py index 8fd82da548..6d18fb42d0 100644 --- a/simpeg/electromagnetics/time_domain/sources.py +++ b/simpeg/electromagnetics/time_domain/sources.py @@ -1239,7 +1239,9 @@ def _srcFct(self, obsLoc, coordinates="cartesian"): location=self.location, moment=self.moment, ) - return self._dipole.vector_potential(obsLoc, coordinates=coordinates) + out = self._dipole.vector_potential(obsLoc, coordinates=coordinates) + out[np.isnan(out)] = 0 + return out def _aSrc(self, simulation): coordinates = "cartesian" @@ -1597,7 +1599,9 @@ def _srcFct(self, obsLoc, coordinates="cartesian"): radius=self.radius, current=self.current, ) - return self.n_turns * self._loop.vector_potential(obsLoc, coordinates) + out = self._loop.vector_potential(obsLoc, coordinates) + out[np.isnan(out)] = 0 + return self.n_turns * out N = deprecate_property( n_turns, "N", "n_turns", removal_version="0.19.0", error=True From aec8889a432d91700b59ad31dc057bb5b3f3ba29 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Fri, 25 Oct 2024 12:45:03 -0600 Subject: [PATCH 080/194] Numpy2.0 and discretize 0.11.0 updates (#1558) #### Summary Necessary updates for `numpy2.0` and `discretize` 0.11.0 #### PR Checklist * [x] If this is a work in progress PR, set as a Draft PR * [x] Linted my code according to the [style guides](https://docs.simpeg.xyz/latest/content/getting_started/contributing/code-style.html). * [x] Added [tests](https://docs.simpeg.xyz/latest/content/getting_started/contributing/testing.html) to verify changes to the code. * [x] Added necessary documentation to any new functions/classes following the expect [style](https://docs.simpeg.xyz/latest/content/getting_started/contributing/documentation.html). * [x] Marked as ready for review (if this is was a draft PR), and converted to a Pull Request * [x] Tagged ``@simpeg/simpeg-developers`` when ready for review. #### What does this implement/fix? Related to Numpy 2.0: * Simplifies `unique_rows` function to more directly use numpy's `unique` function. Related to discretize 0.11: * Uses public `get_containing_cells` function of `TreeMesh` instead of private method. * Adds `random_seed` arguments to `check_derivative` and `assert_isadjoint` functions now that it is supported. #### Additional information We were generally in a good position for numpy 2.0! The only breaking point from discretize 0.11 was due to using a private method of a TreeMesh, which is again a pretty good place to be. It's expected that the `check_derivative` tests need to updated given the new random generator usage. --- .ci/environment_test.yml | 4 +- environment.yml | 4 +- pyproject.toml | 4 +- simpeg/maps/_base.py | 6 +- simpeg/objective_function.py | 10 ++- simpeg/utils/mat_utils.py | 6 +- .../regularizations/test_full_gradient.py | 4 +- .../regularizations/test_regularization.py | 44 +++++----- tests/base/test_data_misfit.py | 2 +- tests/base/test_joint.py | 2 +- tests/base/test_maps.py | 80 ++++++------------- tests/base/test_mass_matrices.py | 24 +++--- tests/dask/test_DC_jvecjtvecadj_dask.py | 14 +++- tests/dask/test_IP_jvecjtvecadj_dask.py | 1 + tests/em/em1d/test_EM1D_FD_jac_layers.py | 12 ++- .../em1d/test_EM1D_TD_general_jac_layers.py | 12 ++- tests/em/em1d/test_EM1D_TD_off_jac_layers.py | 2 +- tests/em/fdem/forward/test_FDEM_casing.py | 19 +++-- tests/em/fdem/forward/test_FDEM_primsec.py | 3 +- .../fdem/inverse/derivs/test_FDEM_derivs.py | 5 +- tests/em/fdem/muinverse/test_muinverse.py | 5 +- tests/em/nsem/inversion/test_BC_Sims.py | 21 +++-- .../nsem/inversion/test_Problem1D_Derivs.py | 10 ++- .../nsem/inversion/test_Problem3D_Derivs.py | 5 +- .../inversion/test_complex_resistivity.py | 6 +- tests/em/static/test_DC_1D_jvecjtvecadj.py | 5 +- tests/em/static/test_DC_2D_jvecjtvecadj.py | 9 ++- tests/em/static/test_DC_jvecjtvecadj.py | 57 ++++++++----- tests/em/static/test_IP_2D_jvecjtvecadj.py | 18 +++-- tests/em/static/test_IP_jvecjtvecadj.py | 36 ++++++--- tests/em/static/test_SIP_2D_jvecjtvecadj.py | 27 ++++--- tests/em/static/test_SIP_jvecjtvecadj.py | 27 ++++--- tests/em/static/test_SPjvecjtvecadj.py | 7 +- tests/em/tdem/test_TDEM_DerivAdjoint.py | 5 +- .../test_TDEM_DerivAdjoint_RawWaveform.py | 7 +- .../tdem/test_TDEM_DerivAdjoint_galvanic.py | 5 +- tests/em/tdem/test_TDEM_grounded.py | 3 +- tests/em/tdem/test_TDEM_sources.py | 33 +++----- tests/flow/test_Richards.py | 8 +- tests/flow/test_Richards_empirical.py | 30 +++++-- tests/pf/test_sensitivity_PFproblem.py | 14 ++-- tests/seis/test_tomo.py | 4 +- 42 files changed, 336 insertions(+), 264 deletions(-) diff --git a/.ci/environment_test.yml b/.ci/environment_test.yml index 0e4a0e33b9..3663e03a66 100644 --- a/.ci/environment_test.yml +++ b/.ci/environment_test.yml @@ -2,11 +2,11 @@ name: simpeg-test channels: - conda-forge dependencies: - - numpy>=1.21 + - numpy>=1.22 - scipy>=1.8 - pymatsolver>=0.3 - matplotlib-base - - discretize>=0.10 + - discretize>=0.11 - geoana>=0.7 - empymod>=2.0.0 diff --git a/environment.yml b/environment.yml index 7fe57d785e..44a3128291 100644 --- a/environment.yml +++ b/environment.yml @@ -4,11 +4,11 @@ channels: dependencies: # dependencies - python=3.11 - - numpy>=1.21 + - numpy>=1.22 - scipy>=1.8 - pymatsolver>=0.3 - matplotlib-base - - discretize>=0.10 + - discretize>=0.11 - geoana>=0.7 - empymod>=2.0.0 diff --git a/pyproject.toml b/pyproject.toml index 2335c5c4d6..024a314997 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,11 +15,11 @@ keywords = [ 'geophysics', 'inverse problem' ] dependencies = [ - "numpy>=1.21", + "numpy>=1.22", "scipy>=1.8", "pymatsolver>=0.3", "matplotlib", - "discretize>=0.10", + "discretize>=0.11", "geoana>=0.7", "empymod>=2.0.0", ] diff --git a/simpeg/maps/_base.py b/simpeg/maps/_base.py index b3bf414af8..ccb40fbee4 100644 --- a/simpeg/maps/_base.py +++ b/simpeg/maps/_base.py @@ -205,8 +205,8 @@ def test(self, m=None, num=4, random_seed: RandomSeed | None = None, **kwargs): Returns ``True`` if the test passes """ print("Testing {0!s}".format(str(self))) + rng = np.random.default_rng(seed=random_seed) if m is None: - rng = np.random.default_rng(seed=random_seed) m = rng.uniform(size=self.nP) if "plotIt" not in kwargs: kwargs["plotIt"] = False @@ -215,7 +215,7 @@ def test(self, m=None, num=4, random_seed: RandomSeed | None = None, **kwargs): self.nP, (int, np.integer) ), "nP must be an integer for {}".format(self.__class__.__name__) return check_derivative( - lambda m: [self * m, self.deriv(m)], m, num=num, **kwargs + lambda m: [self * m, self.deriv(m)], m, num=num, random_seed=rng, **kwargs ) def _assertMatchesPair(self, pair): @@ -1294,7 +1294,7 @@ def P(self): Set the projection matrix with partial volumes """ if getattr(self, "_P", None) is None: - in_local = self.local_mesh._get_containing_cell_indexes( + in_local = self.local_mesh.get_containing_cells( self.global_mesh.cell_centers ) diff --git a/simpeg/objective_function.py b/simpeg/objective_function.py index 92f53fa1e4..f5afd55bc8 100644 --- a/simpeg/objective_function.py +++ b/simpeg/objective_function.py @@ -202,12 +202,17 @@ def _test_deriv( **kwargs, ): print("Testing {0!s} Deriv".format(self.__class__.__name__)) + rng = np.random.default_rng(seed=random_seed) if x is None: - rng = np.random.default_rng(seed=random_seed) n_params = rng.integers(low=100, high=1_000) if self.nP == "*" else self.nP x = rng.standard_normal(size=n_params) return check_derivative( - lambda m: [self(m), self.deriv(m)], x, num=num, plotIt=plotIt, **kwargs + lambda m: [self(m), self.deriv(m)], + x, + num=num, + plotIt=plotIt, + random_seed=rng, + **kwargs, ) def _test_deriv2( @@ -232,6 +237,7 @@ def _test_deriv2( num=num, expectedOrder=expectedOrder, plotIt=plotIt, + random_seed=rng, **kwargs, ) diff --git a/simpeg/utils/mat_utils.py b/simpeg/utils/mat_utils.py index e4c662c05d..6f9954cc2d 100644 --- a/simpeg/utils/mat_utils.py +++ b/simpeg/utils/mat_utils.py @@ -123,11 +123,7 @@ def unique_rows(M): Indices to project from output array to input array """ - b = np.ascontiguousarray(M).view(np.dtype((np.void, M.dtype.itemsize * M.shape[1]))) - _, unqInd = np.unique(b, return_index=True) - _, invInd = np.unique(b, return_inverse=True) - unqM = M[unqInd] - return unqM, unqInd, invInd + return np.unique(M, return_index=True, return_inverse=True, axis=0) def eigenvalue_by_power_iteration( diff --git a/tests/base/regularizations/test_full_gradient.py b/tests/base/regularizations/test_full_gradient.py index ae3b51e27f..4653793919 100644 --- a/tests/base/regularizations/test_full_gradient.py +++ b/tests/base/regularizations/test_full_gradient.py @@ -141,7 +141,7 @@ def test_first_derivatives(dim, alphas, reg_dirs): def func(x): return reg(x), reg.deriv(x) - check_derivative(func, np.ones(mesh.n_cells), plotIt=False) + check_derivative(func, np.ones(mesh.n_cells), plotIt=False, random_seed=7641) @pytest.mark.parametrize( @@ -156,7 +156,7 @@ def test_second_derivatives(dim, alphas, reg_dirs): def func(x): return reg.deriv(x), lambda v: reg.deriv2(x, v) - check_derivative(func, np.ones(mesh.n_cells), plotIt=False) + check_derivative(func, np.ones(mesh.n_cells), plotIt=False, random_seed=5876231) @pytest.mark.parametrize("with_active_cells", [True, False]) diff --git a/tests/base/regularizations/test_regularization.py b/tests/base/regularizations/test_regularization.py index 9c5023c30f..776c07ed65 100644 --- a/tests/base/regularizations/test_regularization.py +++ b/tests/base/regularizations/test_regularization.py @@ -89,15 +89,16 @@ def test_regularization(self): print("--- Checking {} --- \n".format(reg.__class__.__name__)) + rng = np.random.default_rng(seed=639) if mapping.nP != "*": - m = np.random.rand(mapping.nP) + m = rng.random(mapping.nP) else: - m = np.random.rand(mesh.nC) + m = rng.random(mesh.nC) mref = np.ones_like(m) * np.mean(m) reg.reference_model = mref # test derivs - passed = reg.test(m, eps=TOL) + passed = reg.test(m, eps=TOL, random_seed=rng) self.assertTrue(passed) def test_regularization_ActiveCells(self): @@ -139,13 +140,14 @@ def test_regularization_ActiveCells(self): reg = r( mesh, active_cells=active_cells, mapping=maps.IdentityMap(nP=nP) ) - m = np.random.rand(mesh.nC)[active_cells] + rng = np.random.default_rng(seed=532) + m = rng.random(mesh.nC)[active_cells] mref = np.ones_like(m) * np.mean(m) reg.reference_model = mref print("--- Checking {} ---\n".format(reg.__class__.__name__)) - passed = reg.test(m, eps=TOL) + passed = reg.test(m, eps=TOL, random_seed=rng) self.assertTrue(passed) if testRegMesh: @@ -239,7 +241,8 @@ def test_property_mirroring(self): def test_addition(self): mesh = discretize.TensorMesh([8, 7, 6]) - m = np.random.rand(mesh.nC) + rng = np.random.default_rng(seed=523) + m = rng.random(mesh.nC) reg1 = regularization.WeightedLeastSquares(mesh) reg2 = regularization.WeightedLeastSquares(mesh) @@ -247,21 +250,22 @@ def test_addition(self): reg_a = reg1 + reg2 self.assertTrue(len(reg_a) == 2) self.assertTrue(reg1(m) + reg2(m) == reg_a(m)) - reg_a.test(eps=TOL) + reg_a.test(eps=TOL, random_seed=rng) reg_b = 2 * reg1 + reg2 self.assertTrue(len(reg_b) == 2) self.assertTrue(2 * reg1(m) + reg2(m) == reg_b(m)) - reg_b.test(eps=TOL) + reg_b.test(eps=TOL, random_seed=rng) reg_c = reg1 + reg2 / 2 self.assertTrue(len(reg_c) == 2) self.assertTrue(reg1(m) + 0.5 * reg2(m) == reg_c(m)) - reg_c.test(eps=TOL) + reg_c.test(eps=TOL, random_seed=rng) def test_mappings(self): mesh = discretize.TensorMesh([8, 7, 6]) - m = np.random.rand(2 * mesh.nC) + rng = np.random.default_rng(seed=123) + m = rng.random(2 * mesh.nC) wires = maps.Wires(("sigma", mesh.nC), ("mu", mesh.nC)) @@ -276,9 +280,9 @@ def test_mappings(self): self.assertTrue(reg3.nP == 2 * mesh.nC) self.assertTrue(reg3(m) == reg1(m) + reg2(m)) - reg1.test(eps=TOL) - reg2.test(eps=TOL) - reg3.test(eps=TOL) + reg1.test(eps=TOL, random_seed=rng) + reg2.test(eps=TOL, random_seed=rng) + reg3.test(eps=TOL, random_seed=rng) def test_mref_is_zero(self): mesh = discretize.TensorMesh([10, 5, 8]) @@ -577,7 +581,8 @@ def test_sparse_properties(self): def test_vector_amplitude(self): n_comp = 4 mesh = discretize.TensorMesh([8, 7]) - model = np.random.randn(mesh.nC, n_comp) + rng = np.random.default_rng(5412) + model = rng.random((mesh.nC, n_comp)) with pytest.raises(TypeError, match="'regularization_mesh' must be of type"): regularization.VectorAmplitude("abc") @@ -593,7 +598,7 @@ def test_vector_amplitude(self): reg.objfcts[0].f_m(model.flatten(order="F")), np.linalg.norm(model, axis=1) ) - reg.test(model.flatten(order="F")) + reg.test(model.flatten(order="F"), random_seed=rng) def test_WeightedLeastSquares(): @@ -615,6 +620,7 @@ def test_WeightedLeastSquares(): def test_cross_ref_reg(dim): mesh = discretize.TensorMesh([3, 4, 5][:dim]) actives = mesh.cell_centers[:, -1] < 0.6 + rng = np.random.default_rng(6634) n_active = actives.sum() ref_dir = dim * [1] @@ -627,8 +633,8 @@ def test_cross_ref_reg(dim): assert cross_reg._nC_residual == dim * n_active # give it some cell weights, and some cell vector weights to do something with - cell_weights = np.random.rand(n_active) - cell_vec_weights = np.random.rand(n_active, dim) + cell_weights = rng.random(n_active) + cell_vec_weights = rng.random((n_active, dim)) cross_reg.set_weights(cell_weights=cell_weights) cross_reg.set_weights(vec_weights=cell_vec_weights) @@ -637,8 +643,8 @@ def test_cross_ref_reg(dim): else: assert cross_reg.W.shape == (n_active, n_active) - m = np.random.rand(dim * n_active) - cross_reg.test(m) + m = rng.random(dim * n_active) + cross_reg.test(m, random_seed=rng) def test_cross_reg_reg_errors(): diff --git a/tests/base/test_data_misfit.py b/tests/base/test_data_misfit.py index a981cd625f..bf1d0cc088 100644 --- a/tests/base/test_data_misfit.py +++ b/tests/base/test_data_misfit.py @@ -66,7 +66,7 @@ def test_setting_W(self): def test_DataMisfitOrder(self): self.data.relative_error = self.relative self.data.noise_floor = self.noise_floor - self.dmis.test(x=self.model) + self.dmis.test(x=self.model, random_seed=17) if __name__ == "__main__": diff --git a/tests/base/test_joint.py b/tests/base/test_joint.py index 8485425bdc..f1b56f0fe9 100644 --- a/tests/base/test_joint.py +++ b/tests/base/test_joint.py @@ -75,7 +75,7 @@ def setUp(self): def test_multiDataMisfit(self): self.dmis0.test(random_seed=42) self.dmis1.test(random_seed=42) - self.dmiscombo.test(x=self.model) + self.dmiscombo.test(x=self.model, random_seed=42) def test_inv(self): reg = regularization.WeightedLeastSquares(self.mesh) diff --git a/tests/base/test_maps.py b/tests/base/test_maps.py index 5d0478bf19..ba7ac19581 100644 --- a/tests/base/test_maps.py +++ b/tests/base/test_maps.py @@ -192,63 +192,26 @@ def test_invtransforms3D(self): def test_ParametricCasingAndLayer(self): mapping = maps.ParametricCasingAndLayer(self.meshCyl) m = np.r_[-2.0, 1.0, 6.0, 2.0, -0.1, 0.2, 0.5, 0.2, -0.2, 0.2] - self.assertTrue(mapping.test(m=m)) + self.assertTrue(mapping.test(m=m, random_seed=42)) def test_ParametricBlock2D(self): mesh = discretize.TensorMesh([np.ones(30), np.ones(20)], x0=np.array([-15, -5])) mapping = maps.ParametricBlock(mesh) # val_background,val_block, block_x0, block_dx, block_y0, block_dy m = np.r_[-2.0, 1.0, -5, 10, 5, 4] - self.assertTrue(mapping.test(m=m)) + self.assertTrue(mapping.test(m=m, random_seed=42)) def test_transforms_logMap_reciprocalMap(self): - # Note that log/reciprocal maps can be kinda finicky, so we are being - # explicit about the random seed. - - v2 = np.r_[ - 0.40077291, 0.1441044, 0.58452314, 0.96323738, 0.01198519, 0.79754415 - ] - dv2 = np.r_[ - 0.80653921, 0.13132446, 0.4901117, 0.03358737, 0.65473762, 0.44252488 - ] - v3 = np.r_[ - 0.96084865, - 0.34385186, - 0.39430044, - 0.81671285, - 0.65929109, - 0.2235217, - 0.87897526, - 0.5784033, - 0.96876393, - 0.63535864, - 0.84130763, - 0.22123854, - ] - dv3 = np.r_[ - 0.96827838, - 0.26072111, - 0.45090749, - 0.10573893, - 0.65276365, - 0.15646586, - 0.51679682, - 0.23071984, - 0.95106218, - 0.14201845, - 0.25093564, - 0.3732866, - ] mapping = maps.LogMap(self.mesh2) - self.assertTrue(mapping.test(m=v2, dx=dv2)) + self.assertTrue(mapping.test(random_seed=42)) mapping = maps.LogMap(self.mesh3) - self.assertTrue(mapping.test(m=v3, dx=dv3)) + self.assertTrue(mapping.test(random_seed=42)) mapping = maps.ReciprocalMap(self.mesh2) - self.assertTrue(mapping.test(m=v2, dx=dv2)) + self.assertTrue(mapping.test(random_seed=42)) mapping = maps.ReciprocalMap(self.mesh3) - self.assertTrue(mapping.test(m=v3, dx=dv3)) + self.assertTrue(mapping.test(random_seed=42)) def test_Mesh2MeshMap(self): mapping = maps.Mesh2Mesh([self.mesh22, self.mesh2]) @@ -381,7 +344,9 @@ def test_map2Dto3D_z(self): def test_ParametricPolyMap(self): M2 = discretize.TensorMesh([np.ones(10), np.ones(10)], "CN") mParamPoly = maps.ParametricPolyMap(M2, 2, logSigma=True, normal="Y") - self.assertTrue(mParamPoly.test(m=np.r_[1.0, 1.0, 0.0, 0.0, 0.0])) + self.assertTrue( + mParamPoly.test(m=np.r_[1.0, 1.0, 0.0, 0.0, 0.0], random_seed=42) + ) def test_ParametricSplineMap(self): M2 = discretize.TensorMesh([np.ones(10), np.ones(10)], "CN") @@ -394,7 +359,8 @@ def test_parametric_block(self): block = maps.ParametricBlock(M1) self.assertTrue( block.test( - m=np.hstack([np.random.rand(2), np.r_[M1.x0, 2 * M1.h[0].min()]]) + m=np.hstack([np.random.rand(2), np.r_[M1.x0, 2 * M1.h[0].min()]]), + random_seed=42, ) ) @@ -408,7 +374,8 @@ def test_parametric_block(self): np.r_[M2.x0[0], 2 * M2.h[0].min()], np.r_[M2.x0[1], 4 * M2.h[1].min()], ] - ) + ), + random_seed=42, ) ) @@ -423,7 +390,8 @@ def test_parametric_block(self): np.r_[M3.x0[1], 4 * M3.h[1].min()], np.r_[M3.x0[2], 5 * M3.h[2].min()], ] - ) + ), + random_seed=42, ) ) @@ -438,7 +406,8 @@ def test_parametric_ellipsoid(self): np.r_[M2.x0[0], 2 * M2.h[0].min()], np.r_[M2.x0[1], 4 * M2.h[1].min()], ] - ) + ), + random_seed=42, ) ) @@ -453,7 +422,8 @@ def test_parametric_ellipsoid(self): np.r_[M3.x0[1], 4 * M3.h[1].min()], np.r_[M3.x0[2], 5 * M3.h[2].min()], ] - ) + ), + random_seed=42, ) ) @@ -479,8 +449,8 @@ def test_sum(self): self.assertTrue(np.all(summap0 * m0 == summap1 * m0)) - self.assertTrue(summap0.test(m=m0)) - self.assertTrue(summap1.test(m=m0)) + self.assertTrue(summap0.test(m=m0, random_seed=42)) + self.assertTrue(summap1.test(m=m0, random_seed=42)) def test_surject_units(self): M2 = discretize.TensorMesh([np.ones(10), np.ones(20)], "CC") @@ -494,7 +464,7 @@ def test_surject_units(self): self.assertTrue(np.all(m1[unit1] == 0)) self.assertTrue(np.all(m1[unit2] == 1)) - self.assertTrue(surject_units.test(m=m0)) + self.assertTrue(surject_units.test(m=m0, random_seed=42)) def test_Projection(self): nP = 10 @@ -642,16 +612,14 @@ class TestSCEMT(unittest.TestCase): def test_sphericalInclusions(self): mesh = discretize.TensorMesh([4, 5, 3]) mapping = maps.SelfConsistentEffectiveMedium(mesh, sigma0=1e-1, sigma1=1.0) - m = np.random.default_rng(seed=0).random(mesh.n_cells) - mapping.test(m=m, dx=0.05 * np.ones(mesh.n_cells), num=3) + mapping.test(num=3, random_seed=42) def test_spheroidalInclusions(self): mesh = discretize.TensorMesh([4, 3, 2]) mapping = maps.SelfConsistentEffectiveMedium( mesh, sigma0=1e-1, sigma1=1.0, alpha0=0.8, alpha1=0.9, rel_tol=1e-8 ) - m = np.abs(np.random.rand(mesh.nC)) - mapping.test(m=m, dx=0.05 * np.ones(mesh.n_cells), num=3) + mapping.test(num=3, random_seed=42) @pytest.mark.parametrize( diff --git a/tests/base/test_mass_matrices.py b/tests/base/test_mass_matrices.py index 90dc04ea75..97a4adfd60 100644 --- a/tests/base/test_mass_matrices.py +++ b/tests/base/test_mass_matrices.py @@ -493,7 +493,7 @@ def Jvec(v): return d, Jvec - assert check_derivative(f, x0=x0, num=3, plotIt=False) + assert check_derivative(f, x0=x0, num=3, plotIt=False, random_seed=8672354) def test_Mn_deriv(self): u = np.random.randn(self.mesh.n_nodes) @@ -510,7 +510,7 @@ def Jvec(v): return d, Jvec - assert check_derivative(f, x0=x0, num=3, plotIt=False) + assert check_derivative(f, x0=x0, num=3, plotIt=False, random_seed=523876) def test_Me_deriv(self): u = np.random.randn(self.mesh.n_edges) @@ -527,7 +527,7 @@ def Jvec(v): return d, Jvec - assert check_derivative(f, x0=x0, num=3, plotIt=False) + assert check_derivative(f, x0=x0, num=3, plotIt=False, random_seed=9875163) def test_Me_diagonal_anisotropy_deriv(self): u = np.random.randn(self.mesh.n_edges) @@ -544,7 +544,7 @@ def Jvec(v): return d, Jvec - assert check_derivative(f, x0=x0, num=3, plotIt=False) + assert check_derivative(f, x0=x0, num=3, plotIt=False, random_seed=1658372) def test_Me_full_anisotropy_deriv(self): u = np.random.randn(self.mesh.n_edges) @@ -561,7 +561,7 @@ def Jvec(v): return d, Jvec - assert check_derivative(f, x0=x0, num=3, plotIt=False) + assert check_derivative(f, x0=x0, num=3, plotIt=False, random_seed=9867234) def test_Mf_deriv(self): u = np.random.randn(self.mesh.n_faces) @@ -578,7 +578,7 @@ def Jvec(v): return d, Jvec - assert check_derivative(f, x0=x0, num=3, plotIt=False) + assert check_derivative(f, x0=x0, num=3, plotIt=False, random_seed=10523687) def test_Mf_diagonal_anisotropy_deriv(self): u = np.random.randn(self.mesh.n_faces) @@ -595,7 +595,7 @@ def Jvec(v): return d, Jvec - assert check_derivative(f, x0=x0, num=3, plotIt=False) + assert check_derivative(f, x0=x0, num=3, plotIt=False, random_seed=19876354) def test_Mf_full_anisotropy_deriv(self): u = np.random.randn(self.mesh.n_faces) @@ -612,7 +612,7 @@ def Jvec(v): return d, Jvec - assert check_derivative(f, x0=x0, num=3, plotIt=False) + assert check_derivative(f, x0=x0, num=3, plotIt=False, random_seed=102309487) def test_MccI_deriv(self): u = np.random.randn(self.mesh.n_cells) @@ -629,7 +629,7 @@ def Jvec(v): return d, Jvec - assert check_derivative(f, x0=x0, num=3, plotIt=False) + assert check_derivative(f, x0=x0, num=3, plotIt=False, random_seed=89726354) def test_MnI_deriv(self): u = np.random.randn(self.mesh.n_nodes) @@ -646,7 +646,7 @@ def Jvec(v): return d, Jvec - assert check_derivative(f, x0=x0, num=3, plotIt=False) + assert check_derivative(f, x0=x0, num=3, plotIt=False, random_seed=12503698) def test_MeI_deriv(self): u = np.random.randn(self.mesh.n_edges) @@ -663,7 +663,7 @@ def Jvec(v): return d, Jvec - assert check_derivative(f, x0=x0, num=3, plotIt=False) + assert check_derivative(f, x0=x0, num=3, plotIt=False, random_seed=5674129834) def test_MfI_deriv(self): u = np.random.randn(self.mesh.n_faces) @@ -680,7 +680,7 @@ def Jvec(v): return d, Jvec - assert check_derivative(f, x0=x0, num=3, plotIt=False) + assert check_derivative(f, x0=x0, num=3, plotIt=False, random_seed=532349) def test_Mcc_adjoint(self): n_items = self.mesh.n_cells diff --git a/tests/dask/test_DC_jvecjtvecadj_dask.py b/tests/dask/test_DC_jvecjtvecadj_dask.py index bd9c6140bd..c61ad59fcc 100644 --- a/tests/dask/test_DC_jvecjtvecadj_dask.py +++ b/tests/dask/test_DC_jvecjtvecadj_dask.py @@ -69,6 +69,7 @@ def test_misfit(self): self.m0, plotIt=False, num=3, + random_seed=54, ) self.assertTrue(passed) @@ -85,7 +86,11 @@ def test_adjoint(self): def test_dataObj(self): passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=6 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=6, + random_seed=1234, ) self.assertTrue(passed) @@ -147,6 +152,7 @@ def test_misfit(self): self.m0, plotIt=False, num=3, + random_seed=883, ) self.assertTrue(passed) @@ -163,7 +169,11 @@ def test_adjoint(self): def test_dataObj(self): passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=78523, ) self.assertTrue(passed) diff --git a/tests/dask/test_IP_jvecjtvecadj_dask.py b/tests/dask/test_IP_jvecjtvecadj_dask.py index 339b074d4d..65ce0f55cc 100644 --- a/tests/dask/test_IP_jvecjtvecadj_dask.py +++ b/tests/dask/test_IP_jvecjtvecadj_dask.py @@ -117,6 +117,7 @@ def test_misfit(self): self.m0, plotIt=False, num=3, + random_seed=66346, ) self.assertTrue(passed) diff --git a/tests/em/em1d/test_EM1D_FD_jac_layers.py b/tests/em/em1d/test_EM1D_FD_jac_layers.py index 432b647a64..26afc29cb3 100644 --- a/tests/em/em1d/test_EM1D_FD_jac_layers.py +++ b/tests/em/em1d/test_EM1D_FD_jac_layers.py @@ -118,7 +118,7 @@ def derChk(m): dm = m_1D * 0.5 passed = tests.check_derivative( - derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15 + derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15, random_seed=9186724 ) self.assertTrue(passed) if passed: @@ -164,7 +164,9 @@ def misfit(m, dobs): def derChk(m): return misfit(m, dobs) - passed = tests.check_derivative(derChk, m_ini, num=4, plotIt=False, eps=1e-27) + passed = tests.check_derivative( + derChk, m_ini, num=4, plotIt=False, eps=1e-27, random_seed=2345 + ) self.assertTrue(passed) if passed: print("EM1DFM MagDipole Jtvec test works") @@ -279,7 +281,7 @@ def derChk(m): dm = m_1D * 0.5 passed = tests.check_derivative( - derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15 + derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15, random_seed=664 ) self.assertTrue(passed) if passed: @@ -325,7 +327,9 @@ def misfit(m, dobs): def derChk(m): return misfit(m, dobs) - passed = tests.check_derivative(derChk, m_ini, num=4, plotIt=False, eps=1e-27) + passed = tests.check_derivative( + derChk, m_ini, num=4, plotIt=False, eps=1e-27, random_seed=42 + ) self.assertTrue(passed) if passed: print("EM1DFM Circular Loop Jtvec test works") diff --git a/tests/em/em1d/test_EM1D_TD_general_jac_layers.py b/tests/em/em1d/test_EM1D_TD_general_jac_layers.py index 6cce1655aa..f6818ccf81 100644 --- a/tests/em/em1d/test_EM1D_TD_general_jac_layers.py +++ b/tests/em/em1d/test_EM1D_TD_general_jac_layers.py @@ -85,7 +85,7 @@ def derChk(m): return [fwdfun(m), lambda mx: jacfun(m, mx)] passed = tests.check_derivative( - derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15 + derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15, random_seed=9187235 ) self.assertTrue(passed) @@ -118,7 +118,9 @@ def misfit(m, dobs): def derChk(m): return misfit(m, dobs) - passed = tests.check_derivative(derChk, m_ini, num=4, plotIt=False, eps=1e-26) + passed = tests.check_derivative( + derChk, m_ini, num=4, plotIt=False, eps=1e-26, random_seed=42 + ) self.assertTrue(passed) @@ -265,7 +267,7 @@ def derChk(m): return [fwdfun(m), lambda mx: jacfun(m, mx)] passed = tests.check_derivative( - derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15 + derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15, random_seed=42 ) self.assertTrue(passed) @@ -297,7 +299,9 @@ def misfit(m, dobs): def derChk(m): return misfit(m, dobs) - passed = tests.check_derivative(derChk, m_ini, num=4, plotIt=False, eps=1e-26) + passed = tests.check_derivative( + derChk, m_ini, num=4, plotIt=False, eps=1e-26, random_seed=42 + ) self.assertTrue(passed) diff --git a/tests/em/em1d/test_EM1D_TD_off_jac_layers.py b/tests/em/em1d/test_EM1D_TD_off_jac_layers.py index c985b8e758..cb0b559204 100644 --- a/tests/em/em1d/test_EM1D_TD_off_jac_layers.py +++ b/tests/em/em1d/test_EM1D_TD_off_jac_layers.py @@ -116,7 +116,7 @@ def derChk(m): return [fwdfun(m), lambda mx: jacfun(m, mx)] passed = tests.check_derivative( - derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15 + derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15, random_seed=51234 ) self.assertTrue(passed) if passed: diff --git a/tests/em/fdem/forward/test_FDEM_casing.py b/tests/em/fdem/forward/test_FDEM_casing.py index a0ceffc3c8..92d8b85153 100644 --- a/tests/em/fdem/forward/test_FDEM_casing.py +++ b/tests/em/fdem/forward/test_FDEM_casing.py @@ -66,22 +66,27 @@ class Casing_DerivTest(unittest.TestCase): def test_derivs(self): rng = np.random.default_rng(seed=42) - np.random.seed(1983) # set a random seed for check_derivative tests.check_derivative( - CasingMagDipoleDeriv_r, np.ones(n) * 10 + rng.normal(size=n), plotIt=False + CasingMagDipoleDeriv_r, + np.ones(n) * 10 + rng.normal(size=n), + plotIt=False, + random_seed=rng, ) - np.random.seed(1983) # set a random seed for check_derivative - tests.check_derivative(CasingMagDipoleDeriv_z, rng.normal(size=n), plotIt=False) + tests.check_derivative( + CasingMagDipoleDeriv_z, rng.normal(size=n), plotIt=False, random_seed=rng + ) - np.random.seed(1983) # set a random seed for check_derivative tests.check_derivative( CasingMagDipole2Deriv_z_r, np.ones(n) * 10 + rng.normal(size=n), plotIt=False, + random_seed=rng, ) - np.random.seed(1983) # set a random seed for check_derivative tests.check_derivative( - CasingMagDipole2Deriv_z_z, rng.normal(size=n), plotIt=False + CasingMagDipole2Deriv_z_z, + rng.normal(size=n), + plotIt=False, + random_seed=rng, ) diff --git a/tests/em/fdem/forward/test_FDEM_primsec.py b/tests/em/fdem/forward/test_FDEM_primsec.py index 6d150fe4f9..76c09f0c99 100644 --- a/tests/em/fdem/forward/test_FDEM_primsec.py +++ b/tests/em/fdem/forward/test_FDEM_primsec.py @@ -125,8 +125,7 @@ def fun(x): lambda x: self.secondarySimulation.Jvec(x0, x, f=self.fields_primsec), ] - np.random.seed(1983) # set a random seed for check_derivative - return tests.check_derivative(fun, x0, num=2, plotIt=False) + return tests.check_derivative(fun, x0, num=2, plotIt=False, random_seed=515) def AdjointTest(self): print("\nTesting adjoint") diff --git a/tests/em/fdem/inverse/derivs/test_FDEM_derivs.py b/tests/em/fdem/inverse/derivs/test_FDEM_derivs.py index c30005ba36..2aae768832 100644 --- a/tests/em/fdem/inverse/derivs/test_FDEM_derivs.py +++ b/tests/em/fdem/inverse/derivs/test_FDEM_derivs.py @@ -40,8 +40,9 @@ def derivTest(fdemType, comp, src): def fun(x): return prb.dpred(x), lambda x: prb.Jvec(x0, x) - np.random.seed(1983) # set a random seed for check_derivative - return tests.check_derivative(fun, x0, num=2, plotIt=False, eps=FLR) + return tests.check_derivative( + fun, x0, num=2, plotIt=False, eps=FLR, random_seed=6521 + ) class FDEM_DerivTests(unittest.TestCase): diff --git a/tests/em/fdem/muinverse/test_muinverse.py b/tests/em/fdem/muinverse/test_muinverse.py index 7e3841d396..9e45560342 100644 --- a/tests/em/fdem/muinverse/test_muinverse.py +++ b/tests/em/fdem/muinverse/test_muinverse.py @@ -181,8 +181,9 @@ def fun(x): rng = np.random.default_rng(seed=3321) dx = rng.uniform(size=mod.shape) * (mod.max() - mod.min()) * 0.01 - np.random.seed(1983) # set a random seed for check_derivative - return tests.check_derivative(fun, mod, dx=dx, num=4, plotIt=False) + return tests.check_derivative( + fun, mod, dx=dx, num=4, plotIt=False, random_seed=55 + ) def JtvecTest( self, prbtype="ElectricField", sigmaInInversion=False, invertMui=False diff --git a/tests/em/nsem/inversion/test_BC_Sims.py b/tests/em/nsem/inversion/test_BC_Sims.py index b3174a3365..8962b76272 100644 --- a/tests/em/nsem/inversion/test_BC_Sims.py +++ b/tests/em/nsem/inversion/test_BC_Sims.py @@ -19,7 +19,6 @@ def J_func(v): return d, J_func - np.random.seed(1983) # use seed for check_derivative passed = check_derivative(func, x0, plotIt=False, **kwargs) return passed @@ -256,19 +255,19 @@ def test_errors(self): def test_e_sigma_deriv(self): sim, test_mod = create_simulation_1d("e", "sigma") - assert check_deriv(sim, test_mod, num=3) + assert check_deriv(sim, test_mod, num=3, random_seed=235) def test_h_sigma_deriv(self): sim, test_mod = create_simulation_1d("h", "sigma") - assert check_deriv(sim, test_mod, num=3) + assert check_deriv(sim, test_mod, num=3, random_seed=5212) def test_e_mu_deriv(self): sim, test_mod = create_simulation_1d("e", "mu") - assert check_deriv(sim, test_mod, num=3) + assert check_deriv(sim, test_mod, num=3, random_seed=63246) def test_h_mu_deriv(self): sim, test_mod = create_simulation_1d("h", "mu") - assert check_deriv(sim, test_mod, num=3) + assert check_deriv(sim, test_mod, num=3, random_seed=124) def test_e_sigma_adjoint(self): sim, test_mod = create_simulation_1d("e", "sigma") @@ -366,19 +365,19 @@ def test_errors(self): def test_e_sigma_deriv(self): sim, test_mod = create_simulation_2d("e", "sigma", "TensorMesh") - assert check_deriv(sim, test_mod, num=3) + assert check_deriv(sim, test_mod, num=3, random_seed=125) def test_h_sigma_deriv(self): sim, test_mod = create_simulation_2d("h", "sigma", "TensorMesh") - assert check_deriv(sim, test_mod, num=3) + assert check_deriv(sim, test_mod, num=3, random_seed=7425) def test_e_mu_deriv(self): sim, test_mod = create_simulation_2d("e", "mu", "TensorMesh") - assert check_deriv(sim, test_mod, num=3) + assert check_deriv(sim, test_mod, num=3, random_seed=236423) def test_h_mu_deriv(self): sim, test_mod = create_simulation_2d("h", "mu", "TensorMesh") - assert check_deriv(sim, test_mod, num=3) + assert check_deriv(sim, test_mod, num=3, random_seed=34632) def test_e_sigma_adjoint(self): sim, test_mod = create_simulation_2d("e", "sigma", "TensorMesh") @@ -408,13 +407,13 @@ def test_e_sigma_deriv_fixed(self): sim, test_mod = create_simulation_2d( "e", "sigma", "TensorMesh", fixed_boundary=True ) - assert check_deriv(sim, test_mod, num=3) + assert check_deriv(sim, test_mod, num=3, random_seed=2634) def test_h_sigma_deriv_fixed(self): sim, test_mod = create_simulation_2d( "h", "sigma", "TensorMesh", fixed_boundary=True ) - assert check_deriv(sim, test_mod, num=3) + assert check_deriv(sim, test_mod, num=3, random_seed=3651326) def test_e_sigma_adjoint_fixed(self): sim, test_mod = create_simulation_2d( diff --git a/tests/em/nsem/inversion/test_Problem1D_Derivs.py b/tests/em/nsem/inversion/test_Problem1D_Derivs.py index 52c9f80db5..310a7ec4dc 100644 --- a/tests/em/nsem/inversion/test_Problem1D_Derivs.py +++ b/tests/em/nsem/inversion/test_Problem1D_Derivs.py @@ -50,8 +50,9 @@ def DerivJvecTest_1D(halfspace_value, freq=False, expMap=True): def fun(x): return simulation.dpred(x), lambda x: simulation.Jvec(x0, x) - np.random.seed(1983) # use seed for check_derivative - return tests.check_derivative(fun, x0, num=6, plotIt=False, eps=FLR) + return tests.check_derivative( + fun, x0, num=6, plotIt=False, eps=FLR, random_seed=298376 + ) def DerivJvecTest(halfspace_value, freq=False, expMap=True): @@ -74,8 +75,9 @@ def DerivJvecTest(halfspace_value, freq=False, expMap=True): def fun(x): return simulation.dpred(x), lambda x: simulation.Jvec(x0, x) - np.random.seed(1983) # set a random seed for check_derivative - return tests.check_derivative(fun, x0, num=4, plotIt=False, eps=FLR) + return tests.check_derivative( + fun, x0, num=4, plotIt=False, eps=FLR, random_seed=5553 + ) class NSEM_DerivTests(unittest.TestCase): diff --git a/tests/em/nsem/inversion/test_Problem3D_Derivs.py b/tests/em/nsem/inversion/test_Problem3D_Derivs.py index c08eff446c..8540e9f3bc 100644 --- a/tests/em/nsem/inversion/test_Problem3D_Derivs.py +++ b/tests/em/nsem/inversion/test_Problem3D_Derivs.py @@ -88,8 +88,9 @@ def DerivJvecTest(inputSetup, comp="All", freq=False, expMap=True): def fun(x): return simulation.dpred(x), lambda x: simulation.Jvec(m, x) - np.random.seed(1983) # use seed for check_derivative - return tests.check_derivative(fun, m, num=3, plotIt=False, eps=FLR) + return tests.check_derivative( + fun, m, num=3, plotIt=False, eps=FLR, random_seed=1512 + ) class NSEM_DerivTests(unittest.TestCase): diff --git a/tests/em/nsem/inversion/test_complex_resistivity.py b/tests/em/nsem/inversion/test_complex_resistivity.py index b0c71071bc..3c98b3d80c 100644 --- a/tests/em/nsem/inversion/test_complex_resistivity.py +++ b/tests/em/nsem/inversion/test_complex_resistivity.py @@ -226,9 +226,9 @@ def fun(x): d = sim.dpred(x) return d, lambda y: sim.Jvec(x, y) - rng = np.random.default_rng(seed=1983) # set a random seed for check_derivative - dx = -rng.uniform(size=len(self.model)) * 0.01 * np.abs(self.model).max() - passed = tests.check_derivative(fun, self.model, dx=dx, num=3, plotIt=False) + passed = tests.check_derivative( + fun, self.model, num=3, plotIt=False, random_seed=1983 + ) self.assertTrue(passed) def check_adjoint(self, sim): diff --git a/tests/em/static/test_DC_1D_jvecjtvecadj.py b/tests/em/static/test_DC_1D_jvecjtvecadj.py index 679e0d824f..c55e19aaa9 100644 --- a/tests/em/static/test_DC_1D_jvecjtvecadj.py +++ b/tests/em/static/test_DC_1D_jvecjtvecadj.py @@ -130,8 +130,7 @@ def J(v): return d, J - np.random.seed(40) # set a random seed for check_derivative - assert check_derivative(sim_1d_func, model, plotIt=False, num=4) + assert check_derivative(sim_1d_func, model, plotIt=False, num=4, random_seed=125) @pytest.mark.parametrize("deriv_type", ("sigma", "h", "both")) @@ -178,7 +177,7 @@ def J(v): def JT(v): return simulation.Jtvec(model, v) - assert_isadjoint(J, JT, len(model), survey.nD) + assert_isadjoint(J, JT, len(model), survey.nD, random_seed=5512) def test_errors(): diff --git a/tests/em/static/test_DC_2D_jvecjtvecadj.py b/tests/em/static/test_DC_2D_jvecjtvecadj.py index 876b1a56df..3ea979bca6 100644 --- a/tests/em/static/test_DC_2D_jvecjtvecadj.py +++ b/tests/em/static/test_DC_2D_jvecjtvecadj.py @@ -67,12 +67,12 @@ def setUp(self): self.data = data def test_misfit(self): - np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: (self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)), self.m0, plotIt=False, num=3, + random_seed=9582376, ) self.assertTrue(passed) @@ -88,9 +88,12 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): - np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=23, ) self.assertTrue(passed) diff --git a/tests/em/static/test_DC_jvecjtvecadj.py b/tests/em/static/test_DC_jvecjtvecadj.py index 2eea8b3d71..7ab643fb33 100644 --- a/tests/em/static/test_DC_jvecjtvecadj.py +++ b/tests/em/static/test_DC_jvecjtvecadj.py @@ -65,12 +65,12 @@ def setUp(self): self.dobs = dobs def test_misfit(self): - np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)], self.m0, plotIt=False, num=3, + random_seed=918367, ) self.assertTrue(passed) @@ -87,9 +87,12 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): - np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=6 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=6, + random_seed=63, ) self.assertTrue(passed) @@ -140,8 +143,7 @@ def test_e_deriv(self): def fun(x): return self.prob.dpred(x), lambda x: self.prob.Jvec(x0, x) - np.random.seed(40) # set a random seed for check_derivative - return tests.check_derivative(fun, x0, num=3, plotIt=False) + return tests.check_derivative(fun, x0, num=3, plotIt=False, random_seed=98253) def test_e_adjoint(self): print("Adjoint Test for e") @@ -209,12 +211,12 @@ def setUp(self): self.dobs = dobs def test_misfit(self): - np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)], self.m0, plotIt=False, num=3, + random_seed=825, ) self.assertTrue(passed) @@ -231,9 +233,12 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): - np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=3456845, ) self.assertTrue(passed) @@ -283,12 +288,12 @@ def setUp(self): self.dobs = dobs def test_misfit(self): - np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)], self.m0, plotIt=False, num=3, + random_seed=562, ) self.assertTrue(passed) @@ -305,9 +310,12 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): - np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=1254, ) self.assertTrue(passed) @@ -357,12 +365,12 @@ def setUp(self): self.dobs = dobs def test_misfit(self): - np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)], self.m0, plotIt=False, num=3, + random_seed=98670234, ) self.assertTrue(passed) @@ -379,9 +387,12 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): - np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=4 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=4, + random_seed=5613789, ) self.assertTrue(passed) @@ -438,12 +449,12 @@ def setUp(self): self.dobs = dobs def test_misfit(self): - np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)], self.m0, plotIt=False, num=3, + random_seed=346, ) self.assertTrue(passed) @@ -460,9 +471,12 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): - np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=83475902, ) self.assertTrue(passed) @@ -523,12 +537,12 @@ def setUp(self): self.dobs = dobs def test_misfit(self): - np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)], self.m0, plotIt=False, num=3, + random_seed=908762, ) self.assertTrue(passed) @@ -545,9 +559,12 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): - np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=63426, ) self.assertTrue(passed) diff --git a/tests/em/static/test_IP_2D_jvecjtvecadj.py b/tests/em/static/test_IP_2D_jvecjtvecadj.py index d7fe6ce54e..7cf4ed0176 100644 --- a/tests/em/static/test_IP_2D_jvecjtvecadj.py +++ b/tests/em/static/test_IP_2D_jvecjtvecadj.py @@ -66,12 +66,12 @@ def setUp(self): self.dmis = dmis def test_misfit(self): - np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)], self.m0, plotIt=False, num=3, + random_seed=63426, ) self.assertTrue(passed) @@ -88,9 +88,12 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): - np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=7861325, ) self.assertTrue(passed) @@ -146,12 +149,12 @@ def setUp(self): self.dmis = dmis def test_misfit(self): - np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)], self.m0, plotIt=False, num=3, + random_seed=87643, ) self.assertTrue(passed) @@ -168,9 +171,12 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): - np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=25938764, ) self.assertTrue(passed) diff --git a/tests/em/static/test_IP_jvecjtvecadj.py b/tests/em/static/test_IP_jvecjtvecadj.py index 6388c5fb8b..ecc9a8188e 100644 --- a/tests/em/static/test_IP_jvecjtvecadj.py +++ b/tests/em/static/test_IP_jvecjtvecadj.py @@ -59,12 +59,12 @@ def setUp(self): # self.dobe = dobs def test_misfit(self): - np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)], self.m0, plotIt=False, num=3, + random_seed=63426, ) self.assertTrue(passed) @@ -81,9 +81,12 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): - np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=234623, ) self.assertTrue(passed) @@ -131,12 +134,12 @@ def setUp(self): self.dmis = dmis def test_misfit(self): - np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)], self.m0, plotIt=False, num=3, + random_seed=63462, ) self.assertTrue(passed) @@ -153,9 +156,12 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): - np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=5234, ) self.assertTrue(passed) @@ -207,12 +213,12 @@ def setUp(self): self.dmis = dmis def test_misfit(self): - np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)], self.m0, plotIt=False, num=3, + random_seed=4512, ) self.assertTrue(passed) @@ -229,9 +235,12 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): - np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=541, ) self.assertTrue(passed) @@ -290,12 +299,12 @@ def setUp(self): self.dmis = dmis def test_misfit(self): - np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)], self.m0, plotIt=False, num=3, + random_seed=512, ) self.assertTrue(passed) @@ -312,9 +321,12 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): - np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=87623, ) self.assertTrue(passed) diff --git a/tests/em/static/test_SIP_2D_jvecjtvecadj.py b/tests/em/static/test_SIP_2D_jvecjtvecadj.py index f86f13b51d..dd2cb4bb7c 100644 --- a/tests/em/static/test_SIP_2D_jvecjtvecadj.py +++ b/tests/em/static/test_SIP_2D_jvecjtvecadj.py @@ -80,12 +80,12 @@ def setUp(self): self.dobs = dobs def test_misfit(self): - np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)], self.m0, plotIt=False, num=3, + random_seed=51, ) self.assertTrue(passed) @@ -102,9 +102,12 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): - np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=38, ) self.assertTrue(passed) @@ -174,12 +177,12 @@ def setUp(self): self.dobs = dobs def test_misfit(self): - np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)], self.m0, plotIt=False, num=3, + random_seed=643, ) self.assertTrue(passed) @@ -195,9 +198,12 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): - np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=2 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=2, + random_seed=521, ) self.assertTrue(passed) @@ -286,12 +292,12 @@ def setUp(self): self.dobs = dobs def test_misfit(self): - np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)], self.m0, plotIt=False, num=3, + random_seed=575, ) self.assertTrue(passed) @@ -307,9 +313,12 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): - np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=57556, ) self.assertTrue(passed) diff --git a/tests/em/static/test_SIP_jvecjtvecadj.py b/tests/em/static/test_SIP_jvecjtvecadj.py index 258f2f0abc..d1b0181fb4 100644 --- a/tests/em/static/test_SIP_jvecjtvecadj.py +++ b/tests/em/static/test_SIP_jvecjtvecadj.py @@ -86,12 +86,12 @@ def setUp(self): self.dobs = dobs def test_misfit(self): - np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)], self.m0, plotIt=False, num=3, + random_seed=51, ) self.assertTrue(passed) @@ -108,9 +108,12 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): - np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=51, ) self.assertTrue(passed) @@ -186,12 +189,12 @@ def setUp(self): self.dobs = dobs def test_misfit(self): - np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)], self.m0, plotIt=False, num=3, + random_seed=5432, ) self.assertTrue(passed) @@ -207,9 +210,12 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): - np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=553254, ) self.assertTrue(passed) @@ -298,12 +304,12 @@ def setUp(self): self.dobs = dobs def test_misfit(self): - np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( lambda m: [self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)], self.m0, plotIt=False, num=3, + random_seed=754, ) self.assertTrue(passed) @@ -319,9 +325,12 @@ def test_adjoint(self): self.assertTrue(passed) def test_dataObj(self): - np.random.seed(40) # set a random seed for check_derivative passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=2234, ) self.assertTrue(passed) diff --git a/tests/em/static/test_SPjvecjtvecadj.py b/tests/em/static/test_SPjvecjtvecadj.py index d1d8ac20cf..462f6747b9 100644 --- a/tests/em/static/test_SPjvecjtvecadj.py +++ b/tests/em/static/test_SPjvecjtvecadj.py @@ -73,8 +73,7 @@ def Jvec(v): rng = np.random.default_rng(seed=42) m0 = rng.normal(size=q_map.shape[1]) - np.random.seed(40) # set a random seed for check_derivative - check_derivative(func, m0, plotIt=False) + check_derivative(func, m0, plotIt=False, random_seed=rng) @pytest.mark.parametrize( @@ -99,7 +98,9 @@ def Jvec(v): def Jtvec(v): return sim.Jtvec(model, v, f=f) - assert_isadjoint(Jvec, Jtvec, shape_u=(q_map.shape[1],), shape_v=(survey.nD)) + assert_isadjoint( + Jvec, Jtvec, shape_u=(q_map.shape[1],), shape_v=(survey.nD), random_seed=rng + ) def test_errors(): diff --git a/tests/em/tdem/test_TDEM_DerivAdjoint.py b/tests/em/tdem/test_TDEM_DerivAdjoint.py index 7f2f2e5880..69514dd054 100644 --- a/tests/em/tdem/test_TDEM_DerivAdjoint.py +++ b/tests/em/tdem/test_TDEM_DerivAdjoint.py @@ -114,8 +114,9 @@ def derChk(m): prbtype=self.formulation, rxcomp=rxcomp ) ) - np.random.seed(10) # use seed for check_derivative - tests.check_derivative(derChk, self.m, plotIt=False, num=2, eps=1e-20) + tests.check_derivative( + derChk, self.m, plotIt=False, num=2, eps=1e-20, random_seed=12 + ) def JvecVsJtvecTest(self, rxcomp): self.set_receiver_list(rxcomp) diff --git a/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py b/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py index e3d24240a8..8b175a548b 100644 --- a/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py +++ b/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py @@ -131,8 +131,9 @@ def derChk(m): prbtype=self.formulation, rxcomp=rxcomp ) ) - np.random.seed(4) # set seed for check_derivative - tests.check_derivative(derChk, self.m, plotIt=False, num=3, eps=1e-20) + tests.check_derivative( + derChk, self.m, plotIt=False, num=3, eps=1e-20, random_seed=5412 + ) def JvecVsJtvecTest(self, rxcomp): self.set_receiver_list(rxcomp) @@ -306,7 +307,7 @@ def test_Jvec_adjoint_j_dhdtz(self): # return Av, ADeriv_dm # print('\n Testing ADeriv {}'.format(prbtype)) -# tests.check_derivative(AderivFun, m0, plotIt=False, num=4, eps=EPS) +# tests.check_derivative(AderivFun, m0, plotIt=False, num=4, eps=EPS, random_seed=512) # def A_adjointTest(self, prbtype): # prb, m0, mesh = setUp_TDEM(prbtype) diff --git a/tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py b/tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py index 6579121598..9d6e5d192e 100644 --- a/tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py +++ b/tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py @@ -76,8 +76,9 @@ def derChk(m): print("test_Jvec_{prbtype}_{rxcomp}".format(prbtype=prbtype, rxcomp=rxcomp)) - np.random.seed(10) # use seed for check_derivative - tests.check_derivative(derChk, m, plotIt=False, num=2, eps=1e-20) + tests.check_derivative( + derChk, m, plotIt=False, num=2, eps=1e-20, random_seed=52135 + ) def test_Jvec_e_dbzdt(self): self.JvecTest("ElectricField", "MagneticFluxTimeDerivativez") diff --git a/tests/em/tdem/test_TDEM_grounded.py b/tests/em/tdem/test_TDEM_grounded.py index 436c5f70d5..dcd4a451c4 100644 --- a/tests/em/tdem/test_TDEM_grounded.py +++ b/tests/em/tdem/test_TDEM_grounded.py @@ -95,9 +95,8 @@ def derivtest(self, deriv_fct): m0 = np.log(self.sigma) + rng.uniform(size=self.mesh.nC) self.prob.model = m0 - np.random.seed(10) # use seed for check_derivative return tests.check_derivative( - deriv_fct, np.log(self.sigma), num=3, plotIt=False + deriv_fct, np.log(self.sigma), num=3, plotIt=False, random_seed=521 ) def test_deriv_phi(self): diff --git a/tests/em/tdem/test_TDEM_sources.py b/tests/em/tdem/test_TDEM_sources.py index d038766716..e46dbdc9e2 100644 --- a/tests/em/tdem/test_TDEM_sources.py +++ b/tests/em/tdem/test_TDEM_sources.py @@ -73,8 +73,7 @@ def f(t): ) dt = np.min(np.diff(t0)) * 0.5 * np.ones_like(t0) - np.random.seed(10) # use seed for check_derivative - assert check_derivative(f, t0, dx=dt, plotIt=False) + assert check_derivative(f, t0, dx=dt, plotIt=False, random_seed=5421) class TestVTEMWaveform(unittest.TestCase): @@ -116,8 +115,7 @@ def f(t): ) dt = np.min(np.diff(t0)) * 0.5 * np.ones_like(t0) - np.random.seed(10) # use seed for check_derivative - assert check_derivative(f, t0, dx=dt, plotIt=False) + assert check_derivative(f, t0, dx=dt, plotIt=False, random_seed=643) class TestTrapezoidWaveform(unittest.TestCase): @@ -159,8 +157,7 @@ def f(t): ) dt = np.min(np.diff(t0)) * 0.5 * np.ones_like(t0) - np.random.seed(10) # use seed for check_derivative - assert check_derivative(f, t0, dx=dt, plotIt=False) + assert check_derivative(f, t0, dx=dt, plotIt=False, random_seed=5277) class TestTriangularWaveform(unittest.TestCase): @@ -198,8 +195,7 @@ def f(t): ) dt = np.min(np.diff(t0)) * 0.5 * np.ones_like(t0) - np.random.seed(10) # use seed for check_derivative - assert check_derivative(f, t0, dx=dt, plotIt=False) + assert check_derivative(f, t0, dx=dt, plotIt=False, random_seed=874) class TestQuarterSineRampOnWaveform(unittest.TestCase): @@ -272,8 +268,7 @@ def f(t): ) dt = np.min(np.diff(t0)) * 0.5 * np.ones_like(t0) - np.random.seed(10) # use seed for check_derivative - assert check_derivative(f, t0, dx=dt, plotIt=False) + assert check_derivative(f, t0, dx=dt, plotIt=False, random_seed=7564) def test_waveform_without_plateau_derivative(self): # Test the waveform derivative at points between the time_nodes @@ -295,8 +290,7 @@ def f(t): ) dt = np.min(np.diff(t0)) * 0.5 * np.ones_like(t0) - np.random.seed(10) # use seed for check_derivative - assert check_derivative(f, t0, dx=dt, plotIt=False) + assert check_derivative(f, t0, dx=dt, plotIt=False, random_seed=12) def test_waveform_negative_plateau_derivative(self): # Test the waveform derivative at points between the time_nodes @@ -318,8 +312,7 @@ def f(t): ) dt = np.min(np.diff(t0)) * 0.5 * np.ones_like(t0) - np.random.seed(10) # use seed for check_derivative - assert check_derivative(f, t0, dx=dt, plotIt=False) + assert check_derivative(f, t0, dx=dt, plotIt=False, random_seed=52) class TestHalfSineWaveform(unittest.TestCase): @@ -379,8 +372,7 @@ def f(t): ) dt = np.min(np.diff(t0)) * 0.5 * np.ones_like(t0) - np.random.seed(10) # use seed for check_derivative - assert check_derivative(f, t0, dx=dt, plotIt=False) + assert check_derivative(f, t0, dx=dt, plotIt=False, random_seed=5) def test_waveform_without_plateau_derivative(self): # Test the waveform derivative at points between the time_nodes @@ -400,8 +392,7 @@ def f(t): ) dt = np.min(np.diff(t0)) * 0.5 * np.ones_like(t0) - np.random.seed(10) # use seed for check_derivative - assert check_derivative(f, t0, dx=dt, plotIt=False) + assert check_derivative(f, t0, dx=dt, plotIt=False, random_seed=6) class TestPiecewiseLinearWaveform(unittest.TestCase): @@ -439,8 +430,7 @@ def f(t): ) dt = np.min(np.diff(t0)) * 0.5 * np.ones_like(t0) - np.random.seed(10) # use seed for check_derivative - assert check_derivative(f, t0, dx=dt, plotIt=False) + assert check_derivative(f, t0, dx=dt, plotIt=False, random_seed=11) class TestExponentialWaveform(unittest.TestCase): @@ -530,8 +520,7 @@ def f(t): ) dt = np.min(np.diff(t0)) * 0.5 * np.ones_like(t0) - np.random.seed(10) # use seed for check_derivative - assert check_derivative(f, t0, dx=dt, plotIt=False) + assert check_derivative(f, t0, dx=dt, plotIt=False, random_seed=5555) def test_simple_source(): diff --git a/tests/flow/test_Richards.py b/tests/flow/test_Richards.py index 5b63e1b967..39340185de 100644 --- a/tests/flow/test_Richards.py +++ b/tests/flow/test_Richards.py @@ -68,6 +68,7 @@ def _dotest_getResidual(self, newton): self.h0, expectedOrder=2 if newton else 1, plotIt=False, + random_seed=142, ) self.assertTrue(passed, True) @@ -94,6 +95,7 @@ def _dotest_sensitivity(self): self.mtrue, num=3, plotIt=False, + random_seed=6184726, ) self.assertTrue(passed, True) @@ -101,7 +103,11 @@ def _dotest_sensitivity_full(self): print("Testing Richards Derivative FULL dim={}".format(self.mesh.dim)) J = self.prob.Jfull(self.mtrue) passed = check_derivative( - lambda m: [self.prob.dpred(m), J], self.mtrue, num=3, plotIt=False + lambda m: [self.prob.dpred(m), J], + self.mtrue, + num=3, + plotIt=False, + random_seed=97861534, ) self.assertTrue(passed, True) diff --git a/tests/flow/test_Richards_empirical.py b/tests/flow/test_Richards_empirical.py index 4361621e3f..d50ae8af19 100644 --- a/tests/flow/test_Richards_empirical.py +++ b/tests/flow/test_Richards_empirical.py @@ -18,7 +18,10 @@ def test_haverkamp_theta_u(self): mesh = discretize.TensorMesh([50]) hav = richards.empirical.Haverkamp_theta(mesh) passed = check_derivative( - lambda u: (hav(u), hav.derivU(u)), np.random.randn(50), plotIt=False + lambda u: (hav(u), hav.derivU(u)), + np.random.randn(50), + plotIt=False, + random_seed=5662, ) self.assertTrue(passed, True) @@ -53,14 +56,17 @@ def fun(m): print("Haverkamp_theta test m deriv: ", name) - passed = check_derivative(fun, x0, plotIt=False) + passed = check_derivative(fun, x0, plotIt=False, random_seed=444) self.assertTrue(passed, True) def test_vangenuchten_theta_u(self): mesh = discretize.TensorMesh([50]) van = richards.empirical.Vangenuchten_theta(mesh) passed = check_derivative( - lambda u: (van(u), van.derivU(u)), np.random.randn(50), plotIt=False + lambda u: (van(u), van.derivU(u)), + np.random.randn(50), + plotIt=False, + random_seed=5777, ) self.assertTrue(passed, True) @@ -95,7 +101,7 @@ def fun(m): print("Vangenuchten_theta test m deriv: ", name) - passed = check_derivative(fun, x0, plotIt=False) + passed = check_derivative(fun, x0, plotIt=False, random_seed=666) self.assertTrue(passed, True) def test_haverkamp_k_u(self): @@ -104,7 +110,10 @@ def test_haverkamp_k_u(self): hav = richards.empirical.Haverkamp_k(mesh) print("Haverkamp_k test u deriv") passed = check_derivative( - lambda u: (hav(u), hav.derivU(u)), np.random.randn(mesh.nC), plotIt=False + lambda u: (hav(u), hav.derivU(u)), + np.random.randn(mesh.nC), + plotIt=False, + random_seed=5662, ) self.assertTrue(passed, True) @@ -152,7 +161,9 @@ def fun(m): print("Haverkamp_k test m deriv: ", name) - passed = check_derivative(fun, np.random.randn(mesh.nC * nM), plotIt=False) + passed = check_derivative( + fun, np.random.randn(mesh.nC * nM), plotIt=False, random_seed=65 + ) self.assertTrue(passed, True) def test_vangenuchten_k_u(self): @@ -162,7 +173,10 @@ def test_vangenuchten_k_u(self): print("Vangenuchten_k test u deriv") passed = check_derivative( - lambda u: (van(u), van.derivU(u)), np.random.randn(mesh.nC), plotIt=False + lambda u: (van(u), van.derivU(u)), + np.random.randn(mesh.nC), + plotIt=False, + random_seed=777, ) self.assertTrue(passed, True) @@ -201,7 +215,7 @@ def fun(m): print("Vangenuchten_k test m deriv: ", name) - passed = check_derivative(fun, x0, plotIt=False) + passed = check_derivative(fun, x0, plotIt=False, random_seed=918724) self.assertTrue(passed, True) diff --git a/tests/pf/test_sensitivity_PFproblem.py b/tests/pf/test_sensitivity_PFproblem.py index 2a1fe6681c..c7f00c03c7 100644 --- a/tests/pf/test_sensitivity_PFproblem.py +++ b/tests/pf/test_sensitivity_PFproblem.py @@ -77,7 +77,7 @@ # # d_mu = mu*0.8 # derChk = lambda m: [MfmuI(m), lambda mx: dMfmuI(self.chi, mx)] -# passed = Tests.check_derivative(derChk, mu, num=4, dx = d_mu, plotIt=False) +# passed = Tests.check_derivative(derChk, mu, num=4, dx = d_mu, plotIt=False, random_seed=0) # # self.assertTrue(passed) # @@ -119,7 +119,7 @@ # # d_chi = self.chi*0.8 # derChk = lambda m: [Cm_A(m), lambda mx: dCdm_A(self.chi, mx)] -# passed = Tests.check_derivative(derChk, self.chi, num=4, dx = d_chi, plotIt=False) +# passed = Tests.check_derivative(derChk, self.chi, num=4, dx = d_chi, plotIt=False, random_seed=0) # self.assertTrue(passed) # # @@ -167,7 +167,7 @@ # # d_chi = self.chi*0.8 # derChk = lambda m: [Cm_RHS(m), lambda mx: dCdm_RHS(self.chi, mx)] -# passed = Tests.check_derivative(derChk, self.chi, num=4, dx = d_chi, plotIt=False) +# passed = Tests.check_derivative(derChk, self.chi, num=4, dx = d_chi, plotIt=False, random_seed=0) # self.assertTrue(passed) # # @@ -216,7 +216,7 @@ # # # derChk = lambda m: [ufun(m), lambda mx: dudm(self.chi, mx)] # # # TODO: I am not sure why the order get worse as step decreases .. --; -# # passed = Tests.check_derivative(derChk, self.chi, num=2, dx = d_chi, plotIt=False) +# # passed = Tests.check_derivative(derChk, self.chi, num=2, dx = d_chi, plotIt=False, random_seed=0) # # self.assertTrue(passed) # # @@ -268,7 +268,7 @@ # # # derChk = lambda m: [Bfun(m), lambda mx: dBdm(self.chi, mx)] # # # TODO: I am not sure why the order get worse as step decreases .. --; -# # passed = Tests.check_derivative(derChk, self.chi, num=2, dx = d_chi, plotIt=False) +# # passed = Tests.check_derivative(derChk, self.chi, num=2, dx = d_chi, plotIt=False, random_seed=0) # # self.assertTrue(passed) # # @@ -282,7 +282,7 @@ # # derChk = lambda m: (self.survey.dpred(m), lambda v: self.prob.Jvec(m, v)) # # TODO: I am not sure why the order get worse as step decreases .. --; -# passed = Tests.check_derivative(derChk, self.chi, num=2, dx = d_chi, plotIt=False) +# passed = Tests.check_derivative(derChk, self.chi, num=2, dx = d_chi, plotIt=False, random_seed=0) # self.assertTrue(passed) # # def test_Jtvec(self): @@ -298,7 +298,7 @@ # return misfit, dmisfit # # # TODO: I am not sure why the order get worse as step decreases .. --; -# passed = Tests.check_derivative(misfit, self.chi, num=4, plotIt=False) +# passed = Tests.check_derivative(misfit, self.chi, num=4, plotIt=False, random_seed=0) # self.assertTrue(passed) # # if __name__ == '__main__': diff --git a/tests/seis/test_tomo.py b/tests/seis/test_tomo.py index e7125d70fa..081b4bf3d6 100644 --- a/tests/seis/test_tomo.py +++ b/tests/seis/test_tomo.py @@ -35,7 +35,9 @@ def test_deriv(self): def fun(x): return self.problem.dpred(x), lambda x: self.problem.Jvec(s, x) - return tests.check_derivative(fun, s, num=4, plotIt=False, eps=FLR) + return tests.check_derivative( + fun, s, num=4, plotIt=False, eps=FLR, random_seed=664 + ) if __name__ == "__main__": From 695291c14e27b5a4fd0755bd2b10475432df250a Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Sat, 26 Oct 2024 07:25:39 -0600 Subject: [PATCH 081/194] Add missing seeds (#1560) #### Summary Adds more `random_seed` arguments #### What does this implement/fix? Missed a few the first go through, I think this is all of them. --- tests/dask/test_IP_jvecjtvecadj_dask.py | 34 +++++++++++++++++--- tests/em/em1d/test_EM1D_FD_jac_layers.py | 6 ++-- tests/em/em1d/test_EM1D_TD_off_jac_layers.py | 10 ++++-- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/tests/dask/test_IP_jvecjtvecadj_dask.py b/tests/dask/test_IP_jvecjtvecadj_dask.py index 65ce0f55cc..f18fc04793 100644 --- a/tests/dask/test_IP_jvecjtvecadj_dask.py +++ b/tests/dask/test_IP_jvecjtvecadj_dask.py @@ -134,7 +134,11 @@ def test_adjoint(self): def test_dataObj(self): passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=41, ) self.assertTrue(passed) @@ -188,6 +192,7 @@ def test_misfit(self): self.m0, plotIt=False, num=3, + random_seed=41, ) self.assertTrue(passed) @@ -204,7 +209,11 @@ def test_adjoint(self): def test_dataObj(self): passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=41, ) self.assertTrue(passed) @@ -257,6 +266,7 @@ def test_misfit(self): self.m0, plotIt=False, num=3, + random_seed=41, ) self.assertTrue(passed) @@ -273,7 +283,11 @@ def test_adjoint(self): def test_dataObj(self): passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=41, ) self.assertTrue(passed) @@ -330,6 +344,7 @@ def test_misfit(self): self.m0, plotIt=False, num=3, + random_seed=41, ) self.assertTrue(passed) @@ -346,7 +361,11 @@ def test_adjoint(self): def test_dataObj(self): passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=41, ) self.assertTrue(passed) @@ -410,6 +429,7 @@ def test_misfit(self): self.m0, plotIt=False, num=3, + random_seed=41, ) self.assertTrue(passed) @@ -426,7 +446,11 @@ def test_adjoint(self): def test_dataObj(self): passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=41, ) self.assertTrue(passed) diff --git a/tests/em/em1d/test_EM1D_FD_jac_layers.py b/tests/em/em1d/test_EM1D_FD_jac_layers.py index 26afc29cb3..0364ae3992 100644 --- a/tests/em/em1d/test_EM1D_FD_jac_layers.py +++ b/tests/em/em1d/test_EM1D_FD_jac_layers.py @@ -424,7 +424,7 @@ def derChk(m): return [fwdfun(m), lambda mx: jacfun(m, mx)] passed = tests.check_derivative( - derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15 + derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15, random_seed=1123 ) self.assertTrue(passed) if passed: @@ -467,7 +467,9 @@ def misfit(m, dobs): def derChk(m): return misfit(m, dobs) - passed = tests.check_derivative(derChk, m_ini, num=4, plotIt=False, eps=1e-27) + passed = tests.check_derivative( + derChk, m_ini, num=4, plotIt=False, eps=1e-27, random_seed=124 + ) self.assertTrue(passed) if passed: print("EM1DFM Piecewise Linear Loop Jtvec test works") diff --git a/tests/em/em1d/test_EM1D_TD_off_jac_layers.py b/tests/em/em1d/test_EM1D_TD_off_jac_layers.py index cb0b559204..1288b544b6 100644 --- a/tests/em/em1d/test_EM1D_TD_off_jac_layers.py +++ b/tests/em/em1d/test_EM1D_TD_off_jac_layers.py @@ -160,7 +160,9 @@ def misfit(m, dobs): def derChk(m): return misfit(m, dobs) - passed = tests.check_derivative(derChk, m_ini, num=4, plotIt=False, eps=1e-27) + passed = tests.check_derivative( + derChk, m_ini, num=4, plotIt=False, eps=1e-27, random_seed=52 + ) self.assertTrue(passed) if passed: print("EM1DTM MagDipole Jtvec test works") @@ -276,7 +278,7 @@ def derChk(m): return [fwdfun(m), lambda mx: jacfun(m, mx)] passed = tests.check_derivative( - derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15 + derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15, random_seed=523 ) self.assertTrue(passed) if passed: @@ -320,7 +322,9 @@ def misfit(m, dobs): def derChk(m): return misfit(m, dobs) - passed = tests.check_derivative(derChk, m_ini, num=4, plotIt=False, eps=1e-27) + passed = tests.check_derivative( + derChk, m_ini, num=4, plotIt=False, eps=1e-27, random_seed=98234 + ) self.assertTrue(passed) if passed: print("EM1DTM Circular Loop Jtvec test works") From 7dbfc3a8f0c9715f9074c28ee0bfadda213ddac7 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Sun, 27 Oct 2024 07:57:34 -0700 Subject: [PATCH 082/194] Make use of meshes' `cell_bounds` property (#1559) Ditch the private function `_get_cell_bounds` used in potential fields simulations in favor of the new `cell_bounds` property in discretize meshes. --- simpeg/potential_fields/gravity/simulation.py | 49 ++----------------- .../potential_fields/magnetics/simulation.py | 13 ++--- 2 files changed, 8 insertions(+), 54 deletions(-) diff --git a/simpeg/potential_fields/gravity/simulation.py b/simpeg/potential_fields/gravity/simulation.py index 2d68c7c0ca..5d52ab06a7 100644 --- a/simpeg/potential_fields/gravity/simulation.py +++ b/simpeg/potential_fields/gravity/simulation.py @@ -2,7 +2,6 @@ import warnings import numpy as np import scipy.constants as constants -from discretize import TensorMesh, TreeMesh from geoana.kernels import prism_fz, prism_fzx, prism_fzy, prism_fzz from scipy.constants import G as NewtG @@ -118,42 +117,6 @@ def _get_conversion_factor(component): return conversion_factor -def _get_cell_bounds(mesh: TensorMesh | TreeMesh): - """ - Bounds of each cell in a 2D TensorMesh or TreeMesh. - - The bounds are defined as ``x_min``, ``x_max``, ``y_min``, ``y_max``. - - ..note: - - This private function could be replaced by calling some `cell_bounds` - method of the meshes directly from discretize. - - Parameters - ---------- - mesh : discretize.TensorMesh or discretize.TreeMesh - A 2D mesh. - - Returns - ------- - bounds : (n_cells, 4) array - Array with the bounds of each cell in the mesh. - """ - if not isinstance(mesh, (TensorMesh, TreeMesh)): - raise TypeError(f"Invalid mesh of type {mesh.__class__}.") - if mesh.dim != 2: - raise TypeError( - f"Invalid mesh with '{mesh.dim}' dimensions. Only 2D meshes can be passed." - ) - centers, widths = mesh.cell_centers, mesh.h_gridded - xmin = centers[:, 0] - widths[:, 0] / 2 - xmax = centers[:, 0] + widths[:, 0] / 2 - ymin = centers[:, 1] - widths[:, 1] / 2 - ymax = centers[:, 1] + widths[:, 1] / 2 - bounds = np.hstack(tuple(v[:, np.newaxis] for v in (xmin, xmax, ymin, ymax))) - return bounds - - class Simulation3DIntegral(BasePFSimulation): """ Gravity simulation in integral form. @@ -583,10 +546,8 @@ def _forward(self, densities): (nD,) numpy.ndarray Always return a ``np.float64`` array. """ - # Get cells in the 2D mesh - cells_bounds = _get_cell_bounds(self.mesh) - # Keep only active cells - cells_bounds_active = cells_bounds[self.active_cells] + # Get cells in the 2D mesh and keep only active cells + cells_bounds_active = self.mesh.cell_bounds[self.active_cells] # Allocate fields array fields = np.zeros(self.survey.nD, dtype=self.sensitivity_dtype) # Compute fields @@ -621,10 +582,8 @@ def _sensitivity_matrix(self): ------- (nD, n_active_cells) numpy.ndarray """ - # Get cells in the 2D mesh - cells_bounds = _get_cell_bounds(self.mesh) - # Keep only active cells - cells_bounds_active = cells_bounds[self.active_cells] + # Get cells in the 2D mesh and keep only active cells + cells_bounds_active = self.mesh.cell_bounds[self.active_cells] # Allocate sensitivity matrix shape = (self.survey.nD, self.nC) if self.store_sensitivities == "disk": diff --git a/simpeg/potential_fields/magnetics/simulation.py b/simpeg/potential_fields/magnetics/simulation.py index e76cac6228..55db09e546 100644 --- a/simpeg/potential_fields/magnetics/simulation.py +++ b/simpeg/potential_fields/magnetics/simulation.py @@ -21,7 +21,6 @@ from ..base import BaseEquivalentSourceLayerSimulation, BasePFSimulation from .analytics import CongruousMagBC from .survey import Survey -from ..gravity.simulation import _get_cell_bounds from ._numba_functions import ( choclo, @@ -958,10 +957,8 @@ def _forward(self, model): (nD, ) array Always return a ``np.float64`` array. """ - # Get cells in the 2D mesh - cells_bounds = _get_cell_bounds(self.mesh) - # Keep only active cells - cells_bounds_active = cells_bounds[self.active_cells] + # Get cells in the 2D mesh and keep only active cells + cells_bounds_active = self.mesh.cell_bounds[self.active_cells] # Get regional field regional_field = self.survey.source_field.b0 # Allocate fields array @@ -1036,10 +1033,8 @@ def _sensitivity_matrix(self): ------- (nD, n_active_cells) array """ - # Get cells in the 2D mesh - cells_bounds = _get_cell_bounds(self.mesh) - # Keep only active cells - cells_bounds_active = cells_bounds[self.active_cells] + # Get cells in the 2D mesh and keep only active cells + cells_bounds_active = self.mesh.cell_bounds[self.active_cells] # Get regional field regional_field = self.survey.source_field.b0 # Allocate sensitivity matrix From 7538656387424522805c12568c2962ede6607312 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 29 Oct 2024 09:37:11 -0700 Subject: [PATCH 083/194] Fix docstring of ``SmoothnessFullGradient`` (#1562) Fix RST syntax to properly show the math and the examples in the docstring of the regularization class. Solves some typos and improve how the math symbols and arguments are displayed. --- simpeg/regularization/_gradient.py | 54 ++++++++++++++++++------------ 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/simpeg/regularization/_gradient.py b/simpeg/regularization/_gradient.py index 570e4727aa..f27da93ca5 100644 --- a/simpeg/regularization/_gradient.py +++ b/simpeg/regularization/_gradient.py @@ -18,15 +18,16 @@ class SmoothnessFullGradient(BaseRegularization): ---------- mesh : discretize.BaseMesh The mesh object to use for regularization. The mesh should either have - a `cell_gradient` or a `stencil_cell_gradient` defined. + a ``cell_gradient`` or a ``stencil_cell_gradient`` defined. alphas : (mesh.dim,) or (mesh.n_cells, mesh.dim) array_like of float, optional. - The weights of the regularization for each axis. This can be defined for each cell - in the mesh. Default is uniform weights equal to the smallest edge length squared. + The weights of the regularization for each axis. This can be defined + for each cell in the mesh. Default is uniform weights equal to the + smallest edge length squared. reg_dirs : (mesh.dim, mesh.dim) or (mesh.n_cells, mesh.dim, mesh.dim) array_like of float - Matrix or list of matrices whose columns represent the regularization directions. - Each matrix should be orthonormal. Default is Identity. + Matrix or list of matrices whose columns represent the regularization + directions. Each matrix should be orthonormal. Default is Identity. ortho_check : bool, optional - Whether to check `reg_dirs` for orthogonality. + Whether to check ``reg_dirs`` for orthogonality. **kwargs Keyword arguments passed to the parent class ``BaseRegularization``. @@ -41,19 +42,23 @@ class SmoothnessFullGradient(BaseRegularization): We can instead create a measure that smooths twice as much in the 1st dimension than it does in the second dimension. + >>> reg = SmoothnessFullGradient(mesh, [2, 1]) - The `alphas` parameter can also be indepenant for each cell. Here we set all cells - lower than 0.5 in the x2 to twice as much in the first dimension - otherwise it is uniform smoothing. + The ``alphas`` parameter can also be independent for each cell. Here we set + all cells lower than 0.5 in the ``x2`` to twice as much in the first + dimension otherwise it is uniform smoothing. + >>> alphas = np.ones((mesh.n_cells, mesh.dim)) >>> alphas[mesh.cell_centers[:, 1] < 0.5] = [2, 1] >>> reg = SmoothnessFullGradient(mesh, alphas) - We can also rotate the axis in which we want to preferentially smooth. Say we want to - smooth twice as much along the +x1,+x2 diagonal as we do along the -x1,+x2 diagonal, - effectively rotating our smoothing 45 degrees. Note and the columns of the matrix - represent the directional vectors (not the rows). + We can also rotate the axis in which we want to preferentially smooth. Say + we want to smooth twice as much along the +x1,+x2 diagonal as we do along + the -x1,+x2 diagonal, effectively rotating our smoothing 45 degrees. Note + and the columns of the matrix represent the directional vectors (not the + rows). + >>> sqrt2 = np.sqrt(2) >>> reg_dirs = np.array([ ... [sqrt2, -sqrt2], @@ -63,20 +68,25 @@ class SmoothnessFullGradient(BaseRegularization): Notes ----- - The regularization object is the discretized form of the continuous regularization + The regularization object is the discretized form of the continuous + regularization + + .. math:: + + f(m) = \int_V \nabla m \cdot \mathbf{a} \nabla m \, \text{d} V - ..math: - f(m) = \int_V \nabla m \cdot \mathbf{a} \nabla m \hspace{5pt} \partial V + The tensor quantity :math:`\mathbf{a}` is used to represent the potential + preferential directions of regularization. :math:`\mathbf{a}` must be + symmetric positive semi-definite with an eigendecomposition of: - The tensor quantity `a` is used to represent the potential preferential directions of - regularization. `a` must be symmetric positive semi-definite with an eigendecomposition of: + .. math:: - ..math: \mathbf{a} = \mathbf{Q}\mathbf{L}\mathbf{Q}^{-1} - `Q` is then the regularization directions ``reg_dirs``, and `L` is represents the weighting - along each direction, with ``alphas`` along its diagonal. These are multiplied to form the - anisotropic alpha used for rotated gradients. + :math:`\mathbf{Q}` is then the regularization directions ``reg_dirs``, and + :math:`\mathbf{L}` is represents the weighting + along each direction, with ``alphas`` along its diagonal. These are + multiplied to form the anisotropic alpha used for rotated gradients. """ def __init__(self, mesh, alphas=None, reg_dirs=None, ortho_check=True, **kwargs): From b801459545eed546fa7d66f27900bfef10d93e3a Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 29 Oct 2024 13:30:59 -0700 Subject: [PATCH 084/194] Fix math in docstring of eigenvalue_by_power_iteration (#1564) Fix RST syntax to show math in the `eigenvalue_by_power_iteration` function. --- simpeg/utils/mat_utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/simpeg/utils/mat_utils.py b/simpeg/utils/mat_utils.py index 6f9954cc2d..76cee091b1 100644 --- a/simpeg/utils/mat_utils.py +++ b/simpeg/utils/mat_utils.py @@ -175,12 +175,14 @@ def eigenvalue_by_power_iteration( approximated by the Rayleigh quotient: .. math:: + \lambda_k = \frac{\mathbf{x_k^T A x_k}}{\mathbf{x_k^T x_k}} - where :math:`\mathfb{A}` is our matrix and :math:`\mathfb{x_k}` is computed + where :math:`\mathbf{A}` is our matrix and :math:`\mathbf{x_k}` is computed recursively according to: .. math:: + \mathbf{x_{k+1}} = \frac{\mathbf{A x_k}}{\| \mathbf{Ax_k} \|} The elements of the initial vector :math:`\mathbf{x_0}` are randomly From 6c5f7d5d6725579bc6fceb40e0920e528e8c299b Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 30 Oct 2024 11:10:30 -0700 Subject: [PATCH 085/194] Only warn about default solver when set in simulations (#1565) Add a `warn=False` argument to the `get_default_solver()` function. Only when `warn=True`, the warning about the setting of the default solver is raised. This makes the function not to raise a warning when called with the default arguments. When default solvers are being used within simulations, call the function with `warn=True` to let users know about the solver being automatically selected. Update tests accordingly. Fix #1563 --- .../potential_fields/magnetics/simulation.py | 2 +- simpeg/simulation.py | 2 +- simpeg/utils/solver_utils.py | 26 ++++++++++++------- tests/utils/test_default_solver.py | 26 +++++++++++++------ 4 files changed, 37 insertions(+), 19 deletions(-) diff --git a/simpeg/potential_fields/magnetics/simulation.py b/simpeg/potential_fields/magnetics/simulation.py index 55db09e546..a40a28bb97 100644 --- a/simpeg/potential_fields/magnetics/simulation.py +++ b/simpeg/potential_fields/magnetics/simulation.py @@ -1612,7 +1612,7 @@ def MagneticsDiffSecondaryInv(mesh, model, data, **kwargs): # Create an optimization program opt = optimization.InexactGaussNewton(maxIter=miter) - opt.bfgsH0 = get_default_solver()(sp.identity(model.nP), flag="D") + opt.bfgsH0 = get_default_solver(warn=True)(sp.identity(model.nP), flag="D") # Create a regularization program reg = regularization.WeightedLeastSquares(model) # Create an objective function diff --git a/simpeg/simulation.py b/simpeg/simulation.py index de6ec12647..aa0237f0fd 100644 --- a/simpeg/simulation.py +++ b/simpeg/simulation.py @@ -194,7 +194,7 @@ def solver(self): if self._solver is None: # do not cache this, in case the user wants to # change it after the first time it is requested. - return get_default_solver() + return get_default_solver(warn=True) return self._solver @solver.setter diff --git a/simpeg/utils/solver_utils.py b/simpeg/utils/solver_utils.py index bca12303f4..55ffab0f1a 100644 --- a/simpeg/utils/solver_utils.py +++ b/simpeg/utils/solver_utils.py @@ -49,22 +49,30 @@ class DefaultSolverWarning(UserWarning): pass -def get_default_solver() -> Type[Base]: +def get_default_solver(warn=False) -> Type[Base]: """Return the default solver used by simpeg. + Parameters + ---------- + warn : bool, optional + If True, a warning will be raised to let users know that the default + solver is being chosen depending on their system. + Returns ------- solver The default solver class used by simpeg's simulations. """ - warnings.warn( - f"Using the default solver: {_DEFAULT_SOLVER.__name__}. \n\n" - f"If you would like to suppress this notification, add \n" - f"warnings.filterwarnings('ignore', simpeg.utils.solver_utils.DefaultSolverWarning)\n" - f" to your script.", - DefaultSolverWarning, - stacklevel=2, - ) + if warn: + warnings.warn( + f"Using the default solver: {_DEFAULT_SOLVER.__name__}. \n\n" + f"If you would like to suppress this notification, add \n" + f"warnings.filterwarnings(" + "'ignore', simpeg.utils.solver_utils.DefaultSolverWarning)\n" + f" to your script.", + DefaultSolverWarning, + stacklevel=2, + ) return _DEFAULT_SOLVER diff --git a/tests/utils/test_default_solver.py b/tests/utils/test_default_solver.py index f95a3cf4d6..e8e2fc3ba1 100644 --- a/tests/utils/test_default_solver.py +++ b/tests/utils/test_default_solver.py @@ -1,3 +1,4 @@ +import warnings import pytest from pymatsolver import SolverCG @@ -11,18 +12,14 @@ @pytest.fixture(autouse=True) def reset_default_solver(): # This should get automatically used - with pytest.warns(DefaultSolverWarning): - initial_default = get_default_solver() + initial_default = get_default_solver() yield set_default_solver(initial_default) def test_default_setting(): set_default_solver(SolverCG) - - with pytest.warns(DefaultSolverWarning, match="Using the default solver: SolverCG"): - new_default = get_default_solver() - + new_default = get_default_solver() assert new_default == SolverCG @@ -31,7 +28,7 @@ class Temp: pass with pytest.warns(DefaultSolverWarning): - initial_default = get_default_solver() + initial_default = get_default_solver(warn=True) with pytest.raises( TypeError, @@ -40,7 +37,20 @@ class Temp: set_default_solver(Temp) with pytest.warns(DefaultSolverWarning): - after_default = get_default_solver() + after_default = get_default_solver(warn=True) # make sure we didn't accidentally set the default. assert initial_default == after_default + + +def test_warning(): + """Test if warning is raised when warn=True.""" + with pytest.warns(DefaultSolverWarning, match="Using the default solver"): + get_default_solver(warn=True) + + +def test_no_warning(): + """Test if no warning is issued with default parameters.""" + with warnings.catch_warnings(): + warnings.simplefilter("error") # raise error if warning was raised + get_default_solver() From 78b5b5ead9ec34ce0b05aa99e54ea37f778b22fa Mon Sep 17 00:00:00 2001 From: "Devin C. Cowan" Date: Fri, 1 Nov 2024 16:24:50 -0700 Subject: [PATCH 086/194] Augmented receivers for airborne NSEM (#1454) #### Summary Add/modify NSEM receiver classes to simulate various AirMT data types, both theoretical and ones used in practice. Data types include: * Admittance tensor data * ZTEM data (all fields measured at same location before) * Quasi-impedance data * MobileMT data Also includes #1507 --------- Co-authored-by: Santiago Soler Co-authored-by: Joseph Capriotti --- .../natural_source/__init__.py | 4 + .../natural_source/receivers.py | 1201 ++++++++++++++--- .../natural_source/simulation_1d.py | 7 + .../electromagnetics/natural_source/survey.py | 24 +- .../natural_source/utils/plot_utils.py | 6 +- simpeg/utils/code_utils.py | 9 +- .../test_Problem1D_AnalyticVsNumeric.py | 2 +- .../test_Recursive1D_VsAnalyticHalfspace.py | 34 + .../test_Simulation2D_vs_Analytic_pytest.py | 170 +++ .../test_Simulation3D_vs_Analytic_pytest.py | 192 +++ .../test_NSEM_2D_jvecjtvecadj_pytest.py | 232 ++++ .../test_NSEM_3D_jvecjtvecadj_pytest.py | 252 ++++ tests/em/nsem/survey/test_nsem_data.py | 2 +- tests/em/nsem/test_nsem_point_deprecations.py | 215 +++ 14 files changed, 2113 insertions(+), 237 deletions(-) create mode 100644 tests/em/nsem/forward/test_Simulation2D_vs_Analytic_pytest.py create mode 100644 tests/em/nsem/forward/test_Simulation3D_vs_Analytic_pytest.py create mode 100644 tests/em/nsem/inversion/test_NSEM_2D_jvecjtvecadj_pytest.py create mode 100644 tests/em/nsem/inversion/test_NSEM_3D_jvecjtvecadj_pytest.py create mode 100644 tests/em/nsem/test_nsem_point_deprecations.py diff --git a/simpeg/electromagnetics/natural_source/__init__.py b/simpeg/electromagnetics/natural_source/__init__.py index dca9d80cc5..07cf986b76 100644 --- a/simpeg/electromagnetics/natural_source/__init__.py +++ b/simpeg/electromagnetics/natural_source/__init__.py @@ -23,6 +23,10 @@ .. autosummary:: :toctree: generated/ + receivers.Impedance + receivers.Admittance + receivers.ApparentConductivity + receivers.Tipper receivers.PointNaturalSource receivers.Point3DTipper diff --git a/simpeg/electromagnetics/natural_source/receivers.py b/simpeg/electromagnetics/natural_source/receivers.py index 9e76a2fb4e..17e1d4ba6a 100644 --- a/simpeg/electromagnetics/natural_source/receivers.py +++ b/simpeg/electromagnetics/natural_source/receivers.py @@ -1,8 +1,12 @@ -from ...utils.code_utils import validate_string - +from ...utils.code_utils import ( + validate_string, + validate_type, + validate_ndarray_with_shape, + deprecate_class, +) +import warnings import numpy as np from scipy.constants import mu_0 - from ...survey import BaseRx @@ -10,67 +14,236 @@ def _alpha(src): return 1 / (2 * np.pi * mu_0 * src.frequency) -class PointNaturalSource(BaseRx): - """Point receiver class for magnetotelluric simulations. +class BaseNaturalSourceRx(BaseRx): + """ + Base class for natural source electromagnetic receivers. - Assumes that the data locations are standard xyz coordinates; - i.e. (x,y,z) is (Easting, Northing, up). + Parameters + ---------- + locations1, locations2 : (n_loc, n_dim) array_like + Locations where the two fields are measured. + **kwargs + Additional keyword arguments passed to `simpeg.BaseRx`. + """ + + _loc_names = ("First", "Second") + + def __init__(self, locations1, locations2, **kwargs): + super().__init__(locations=(locations1, locations2), **kwargs) + + @property + def locations(self): + """Locations of the two field measurements. + + Locations where the two fields are measured for the receiver. + The name of the field is dependant upon the MT receiver, but + for common MT receivers, these would be the electric field + and magnetic field measurement locations. + + Returns + ------- + locations1, locations2 : (n_loc, n_dim) numpy.ndarray + """ + return self._locations + + @locations.setter + def locations(self, locs): + locs = validate_type("locations", locs, tuple) + try: + loc0, loc1 = locs + except ValueError: + raise ValueError( + f"locations must have two values to unpack, got {len(locs)}" + ) + # check that they are both numpy arrays and have the same shape. + loc0 = validate_ndarray_with_shape( + f"{self._loc_names[0]} locations", loc0, shape=("*", "*") + ) + loc1 = validate_ndarray_with_shape( + f"{self._loc_names[1]} locations", loc1, shape=loc0.shape + ) + self._locations = (loc0, loc1) + # make sure projection matrices are cleared + self._Ps = {} + + @property + def nD(self): + """Number of data associated with the receiver object. + + Returns + ------- + int + Number of data associated with the receiver object. + """ + + return self._locations[0].shape[0] + + def getP(self, mesh, projected_grid, location_id=0): + """Get projection matrix from mesh to specified receiver locations. + + Natural source electromagnetic data may be computed from field measurements + at one or two locations. The `getP` method returns the projection matrix from + the mesh to the appropriate receiver locations. `location_id=0` is used to + project from the mesh to the set of roving receiver locations. `location_id=1` + is used when horizontal fields used to compute NSEM data are measured at a + base station. + + Parameters + ---------- + mesh : discretize.BaseMesh + A discretize mesh. + projected_grid : str + Define what part of the mesh (i.e. edges, faces, centers, nodes) to + project from. Must be one of:: + + 'Ex', 'edges_x' -> x-component of field defined on x edges + 'Ey', 'edges_y' -> y-component of field defined on y edges + 'Ez', 'edges_z' -> z-component of field defined on z edges + 'Fx', 'faces_x' -> x-component of field defined on x faces + 'Fy', 'faces_y' -> y-component of field defined on y faces + 'Fz', 'faces_z' -> z-component of field defined on z faces + 'N', 'nodes' -> scalar field defined on nodes + 'CC', 'cell_centers' -> scalar field defined on cell centers + 'CCVx', 'cell_centers_x' -> x-component of vector field defined on cell centers + 'CCVy', 'cell_centers_y' -> y-component of vector field defined on cell centers + 'CCVz', 'cell_centers_z' -> z-component of vector field defined on cell centers + + locations_id : int + Receiver locations ID. 0 used for roving locations. 1 used for base station locations. + + Returns + ------- + scipy.sparse.csr_matrix + P, the interpolation matrix. + """ + key = (mesh, projected_grid, location_id) + if key in self._Ps: + return self._Ps[key] + locs = self._locations[location_id] + P = mesh.get_interpolation_matrix(locs, projected_grid) + if self.storeProjections: + self._Ps[key] = P + return P + + +class _ElectricAndMagneticReceiver(BaseNaturalSourceRx): + """ + Intermediate class for MT receivers that measure an electric and magnetic field + """ + + _loc_names = ("Electric field", "Magnetic field") + + @property + def locations_e(self): + """Electric field measurement locations + + Returns + ------- + numpy.ndarray + Location where the electric field is measured for all receiver data + """ + return self._locations[0] + + @property + def locations_h(self): + """Magnetic field measurement locations + + Returns + ------- + numpy.ndarray + Location where the magnetic field is measured for all receiver data + """ + return self._locations[1] + + +class Impedance(_ElectricAndMagneticReceiver): + r"""Receiver class for 1D, 2D and 3D impedance data. + + This class is used to simulate data types that can be derived from the impedance tensor: + + .. math:: + \begin{bmatrix} Z_{xx} & Z_{xy} \\ Z_{yx} & Z_{yy} \end{bmatrix} = + \begin{bmatrix} E_x^{(x)} & E_x^{(y)} \\ E_y^{(x)} & E_y^{(y)} \end{bmatrix} \, + \begin{bmatrix} H_x^{(x)} & H_x^{(y)} \\ H_y^{(x)} & H_y^{(y)} \end{bmatrix}^{-1} + + where superscripts :math:`(x)` and :math:`(y)` denote signals corresponding to + incident planewaves whose electric fields are polarized along the x and y-directions + respectively. Electric and magnetic fields do not need to be simulated at the same + location, so this class can be used to simulate quasi-impedance data; i.e. where + the electric fields are measured at a base station. + + Note that in ``simpeg``, natural source EM data are defined according to + standard xyz coordinates; i.e. (x,y,z) is (Easting, Northing, Z +ve up). + + In addition to measuring the real or imaginary component of an impedance tensor + element :math:`Z_{ij}`, the receiver object can be set to measure the + the apparent resistivity: + + .. math:: + \rho_{ij} = \dfrac{| Z_{ij} \, |^2}{\mu_0 \omega} + + or the phase angle: + + .. math:: + \phi_{ij} = \frac{180}{\pi} \, + \tan^{-1} \Bigg ( \dfrac{Im[Z_{ij}]}{Re[Z_{ij}]} \Bigg ) + + where :math:`\mu_0` is the permeability of free-space and :math:`\omega` is the + angular frequency in rad/s. The phase angle is represented in degrees and + is computed by: Parameters ---------- - locations : (n_loc, n_dim) numpy.ndarray - Receiver locations. + locations_e : (n_loc, n_dim) array_like + Locations where the electric fields are measured. + locations_h : (n_loc, n_dim) array_like, optional + Locations where the magnetic fields are measured. Defaults to the same + locations as electric field measurements, `locations_e`. orientation : {'xx', 'xy', 'yx', 'yy'} - MT receiver orientation. - component : {'real', 'imag', 'apparent_resistivity', 'phase'} - MT data type. + Receiver orientation. Specifies whether the receiver's data correspond to + the :math:`Z_{xx}`, :math:`Z_{xy}`, :math:`Z_{yx}` or :math:`Z_{yy}` impedance. + The data type is specified by the `component` input argument. + component : {'real', 'imag', 'apparent_resistivity', 'phase', 'complex'} + Data type. For the impedance element :math:`Z_{ij}` specified by the `orientation` + input argument, the receiver can be set to compute the following: + - 'real': Real component of the impedance (V/A) + - 'imag': Imaginary component of the impedance (V/A) + - 'rho': Apparent resistivity (:math:`\Omega m`) + - 'phase': Phase angle (degrees) + - 'complex': The complex impedance is returned. Do not use for inversion! + storeProjections : bool + Whether to cache to internal projection matrices. """ def __init__( self, - locations=None, - orientation="xy", - component="real", - locations_e=None, + locations_e, locations_h=None, + orientation="xx", + component="real", + storeProjections=False, ): + if locations_h is None: + locations_h = locations_e + super().__init__( + locations1=locations_e, + locations2=locations_h, + storeProjections=storeProjections, + ) self.orientation = orientation self.component = component - # check if locations_e or h have been provided - if (locations_e is not None) and (locations_h is not None): - # check that locations are same size - if locations_e.size == locations_h.size: - self._locations_e = locations_e - self._locations_h = locations_h - else: - raise Exception("location h needs to be same size as location e") - - locations = np.hstack([locations_e, locations_h]) - elif locations is not None: - # check shape of locations - if isinstance(locations, list): - if len(locations) == 2: - self._locations_e = locations[0] - self._locations_h = locations[1] - elif len(locations) == 1: - self._locations_e = locations[0] - self._locations_h = locations[0] - else: - raise Exception("incorrect size of list, must be length of 1 or 2") - locations = locations[0] - elif isinstance(locations, np.ndarray): - self._locations_e = locations - self._locations_h = locations - else: - raise Exception("locations need to be either a list or numpy array") - else: - locations = np.array([[0.0]]) - super().__init__(locations) - @property def component(self): - """Data type; i.e. "real", "imag", "apparent_resistivity", "phase" + r"""Data type; i.e. "real", "imag", "apparent_resistivity", "phase" + + For the impedance element :math:`Z_{ij}`, the `component` property specifies + whether the data are: + - 'real': Real component of the impedance (V/A) + - 'imag': Imaginary component of the impedance (V/A) + - 'rho': Apparent resistivity (:math:`\Omega m`) + - 'phase': Phase angle (degrees) + - 'complex': Complex impedance (V/A) Returns ------- @@ -100,17 +273,22 @@ def component(self, var): "rhoa", ), ("phase", "phi"), + "complex", ], ) @property def orientation(self): - """Orientation of the receiver. + """Receiver orientation. + + Specifies whether the receiver's data correspond to + the :math:`Z_{xx}`, :math:`Z_{xy}`, :math:`Z_{yx}` or :math:`Z_{yy}` impedance. + The data type is specified by the `component` input argument. Returns ------- str - Orientation of the receiver. One of {'xx', 'xy', 'yx', 'yy'} + Receiver orientation. One of {'xx', 'xy', 'yx', 'yy'} """ return self._orientation @@ -120,72 +298,6 @@ def orientation(self, var): "orientation", var, string_list=("xx", "xy", "yx", "yy") ) - @property - def locations_e(self): - """Electric field measurement locations - - Returns - ------- - numpy.ndarray - Location where the electric field is measured for all receiver data - """ - return self._locations_e - - @property - def locations_h(self): - """Magnetic field measurement locations - - Returns - ------- - numpy.ndarray - Location where the magnetic field is measured for all receiver data - """ - return self._locations_h - - def getP(self, mesh, projected_grid, field="e"): - """Projection matrices for all components collected by the receivers - - Note projection matrices are stored as a dictionary listed by meshes. - - Parameters - ---------- - mesh : discretize.base.BaseMesh - The mesh on which the discrete set of equations is solved - projected_grid : str - Define what part of the mesh (i.e. edges, faces, centers, nodes) to - project from. Must be one of:: - - 'Ex', 'edges_x' -> x-component of field defined on x edges - 'Ey', 'edges_y' -> y-component of field defined on y edges - 'Ez', 'edges_z' -> z-component of field defined on z edges - 'Fx', 'faces_x' -> x-component of field defined on x faces - 'Fy', 'faces_y' -> y-component of field defined on y faces - 'Fz', 'faces_z' -> z-component of field defined on z faces - 'N', 'nodes' -> scalar field defined on nodes - 'CC', 'cell_centers' -> scalar field defined on cell centers - 'CCVx', 'cell_centers_x' -> x-component of vector field defined on cell centers - 'CCVy', 'cell_centers_y' -> y-component of vector field defined on cell centers - 'CCVz', 'cell_centers_z' -> z-component of vector field defined on cell centers - - field : str, default = "e" - Whether to project electric or magnetic fields from mesh. - Choose "e" or "h" - """ - if mesh.dim < 3: - return super().getP(mesh, projected_grid) - - if (mesh, projected_grid) in self._Ps: - return self._Ps[(mesh, projected_grid, field)] - - if field == "e": - locs = self.locations_e - else: - locs = self.locations_h - P = mesh.get_interpolation_matrix(locs, projected_grid) - if self.storeProjections: - self._Ps[(mesh, projected_grid, field)] = P - return P - def _eval_impedance(self, src, mesh, f): if mesh.dim < 3 and self.orientation in ["xx", "yy"]: return 0.0 @@ -193,12 +305,12 @@ def _eval_impedance(self, src, mesh, f): h = f[src, "h"] if mesh.dim == 3: if self.orientation[0] == "x": - e = self.getP(mesh, "Ex", "e") @ e + e = self.getP(mesh, "Ex", 0) @ e else: - e = self.getP(mesh, "Ey", "e") @ e + e = self.getP(mesh, "Ey", 0) @ e - hx = self.getP(mesh, "Fx", "h") @ h - hy = self.getP(mesh, "Fy", "h") @ h + hx = self.getP(mesh, "Fx", 1) @ h + hy = self.getP(mesh, "Fy", 1) @ h if self.orientation[1] == "x": h = hy else: @@ -225,7 +337,7 @@ def _eval_impedance(self, src, mesh, f): # need to negate if 'yx' and fields are xy # and as well if 'xy' and fields are 'yx' if mesh.dim == 1 and self.orientation != f.field_directions: - bot = -bot + bot *= -1 return top / bot def _eval_impedance_deriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=False): @@ -238,14 +350,14 @@ def _eval_impedance_deriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=Fals h = f[src, "h"] if mesh.dim == 3: if self.orientation[0] == "x": - Pe = self.getP(mesh, "Ex", "e") + Pe = self.getP(mesh, "Ex", 0) e = Pe @ e else: - Pe = self.getP(mesh, "Ey", "e") + Pe = self.getP(mesh, "Ey", 0) e = Pe @ e - Phx = self.getP(mesh, "Fx", "h") - Phy = self.getP(mesh, "Fy", "h") + Phx = self.getP(mesh, "Fx", 1) + Phy = self.getP(mesh, "Fy", 1) hx = Phx @ h hy = Phy @ h if self.orientation[1] == "x": @@ -274,7 +386,7 @@ def _eval_impedance_deriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=Fals bot = PH @ h[:, 0] if mesh.dim == 1 and self.orientation != f.field_directions: - bot = -bot + bot *= -1 imp = top / bot @@ -355,7 +467,7 @@ def _eval_impedance_deriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=Fals dh_v = PH @ f._hDeriv(src, du_dm_v, v, adjoint=False) if mesh.dim == 1 and self.orientation != f.field_directions: - dh_v = -dh_v + dh_v *= -1 imp_deriv = (de_v - imp * dh_v) / bot @@ -375,29 +487,26 @@ def _eval_impedance_deriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=Fals rx_deriv = getattr(imp_deriv, self.component) return rx_deriv - def eval(self, src, mesh, f, return_complex=False): # noqa: A003 - """ - Project the fields to natural source data. + def eval(self, src, mesh, f): # noqa: A003 + """Compute receiver data from the discrete field solution. Parameters ---------- - src : simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc - NSEM source - mesh : discretize.TensorMesh mesh - Mesh on which the discretize solution is obtained + src : .frequency_domain.sources.BaseFDEMSrc + NSEM source. + mesh : discretize.TensorMesh + Mesh on which the discretize solution is obtained. f : simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM - NSEM fields object of the source - return_complex : bool (optional) - Flag for return the complex evaluation + NSEM fields object of the source. Returns ------- numpy.ndarray - Evaluated data for the receiver + Evaluated data for the receiver. """ imp = self._eval_impedance(src, mesh, f) - if return_complex: + if self.component == "complex": return imp elif self.component == "apparent_resistivity": return _alpha(src) * (imp.real**2 + imp.imag**2) @@ -407,96 +516,206 @@ def eval(self, src, mesh, f, return_complex=False): # noqa: A003 return getattr(imp, self.component) def evalDeriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=False): - """Derivative of projection with respect to the fields + r"""Derivative of data with respect to the fields. + + Let :math:`\mathbf{d}` represent the data corresponding the receiver object. + And let :math:`\mathbf{u}` represent the discrete numerical solution of the + fields on the mesh. Where :math:`\mathbf{P}` is a projection function that + maps from the fields to the data, i.e.: + + .. math:: + \mathbf{d} = \mathbf{P}(\mathbf{u}) + + this method computes and returns the derivative: + + .. math:: + \dfrac{\partial \mathbf{d}}{\partial \mathbf{u}} = + \dfrac{\partial [ \mathbf{P} (\mathbf{u}) ]}{\partial \mathbf{u}} Parameters ---------- - str : simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc - NSEM source + str : .frequency_domain.sources.BaseFDEMSrc + The NSEM source. mesh : discretize.TensorMesh - Mesh on which the discretize solution is obtained + Mesh on which the discretize solution is obtained. f : simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM - NSEM fields object of the source - du_dm_v : None, + NSEM fields object for the source. + du_dm_v : None, optional Supply pre-computed derivative? - v : numpy.ndarray + v : numpy.ndarray, optional Vector of size - adjoint : bool, default = ``False`` - If ``True``, compute the adjoint operation + adjoint : bool, optional + Whether to compute the ajoint operation. Returns ------- numpy.ndarray - Calculated derivative (nD,) (adjoint=False) and (nP,2) (adjoint=True) for both polarizations + Calculated derivative (n_data,) if `adjoint` is ``False``, and (n_param, 2) if `adjoint` + is ``True``, for both polarizations. """ + if self.component == "complex": + raise NotImplementedError( + "complex valued data derivative is not implemented." + ) return self._eval_impedance_deriv( src, mesh, f, du_dm_v=du_dm_v, v=v, adjoint=adjoint ) -class Point3DTipper(PointNaturalSource): - """Point receiver class for Z-axis tipper simulations. +class Tipper(BaseNaturalSourceRx): + r"""Receiver class for tipper data (3D problems only). - Assumes that the data locations are standard xyz coordinates; - i.e. (x,y,z) is (Easting, Northing, up). + This class can be used to simulate AFMag tipper data, defined according to: + + .. math:: + \begin{bmatrix} T_{zx} & T_{zy} \end{bmatrix} = + \begin{bmatrix} H_x^{(x)} & H_y^{(x)} \\ H_x^{(y)} & H_y^{(y)} \end{bmatrix}^{-1} \, + \begin{bmatrix} H_z^{(x)} \\ H_z^{(y)} \end{bmatrix} + + where superscripts :math:`(x)` and :math:`(y)` denote signals corresponding to + incident planewaves whose electric fields are polarized along the x and y-directions + respectively. Note that in ``simpeg``, natural source EM data are defined according to + standard xyz coordinates; i.e. (x,y,z) is (Easting, Northing, Z +ve up). + + The receiver class can also be used to simulate a diverse set of Tipper-like data types + when horizontal magnetic fields are measured at a remote base station. These are defined + according to: + + .. math:: + \begin{bmatrix} T_{xx} & T_{yx} & T_{zx} \\ T_{xy} & T_{yy} & T_{zy} \end{bmatrix} = + \begin{bmatrix} H_x^{(x)} & H_y^{(x)} \\ H_x^{(y)} & H_y^{(y)} \end{bmatrix}_b^{-1} \, + \begin{bmatrix} H_x^{(x)} & H_y^{(x)} & H_z^{(x)} \\ H_x^{(y)} & H_y^{(y)} & H_z^{(y)} \end{bmatrix}_r + + where subscript :math:`b` denotes the base station location and subscript + :math:`r` denotes the mobile receiver location. Parameters ---------- - locations : (n_loc, n_dim) numpy.ndarray - Receiver locations. - orientation : str, default = 'zx' - NSEM receiver orientation. Must be one of {'zx', 'zy'} - component : str, default = 'real' - NSEM data type. Choose one of {'real', 'imag', 'apparent_resistivity', 'phase'} + locations_h : (n_loc, n_dim) array_like + Locations where the roving magnetic fields are measured. + locations_base : (n_loc, n_dim) array_like, optional + Locations where the base station magnetic fields are measured. Defaults to + the same locations as the roving magnetic fields measurements, + `locations_r`. + orientation : {'xx', 'yx', 'zx', 'zy', 'yy', 'zy'} + Specifies the tipper element :math:`T_{ij}` corresponding to the data. + component : {'real', 'imag', 'complex'} + Tipper data type. For the tipper element :math:`T_{ij}` specified by the `orientation` + input argument, the receiver can be set to compute the following: + - 'real': Real component of the tipper (unitless) + - 'imag': Imaginary component of the tipper (unitless) + - 'complex': The complex tipper is returned. Do not use for inversion! + storeProjections : bool + Whether to cache to internal projection matrices. """ + _loc_names = ("Roving magnetic field", "Base station magnetic field") + def __init__( self, - locations, - orientation="zx", + locations_h, + locations_base=None, + orientation="xx", component="real", - locations_e=None, - locations_h=None, + storeProjections=False, ): + if locations_base is None: + locations_base = locations_h super().__init__( - locations=locations, - orientation=orientation, - component=component, - locations_e=locations_e, - locations_h=locations_h, + locations1=locations_h, + locations2=locations_base, + storeProjections=storeProjections, + ) + self.orientation = orientation + self.component = component + + @property + def locations_h(self): + """Roving magnetic field measurement locations. + + Returns + ------- + numpy.ndarray + Roving locations where the magnetic field is measured for all receiver data. + """ + return self._locations[0] + + @property + def locations_base(self): + """Base station magnetic field measurement locations. + + Returns + ------- + numpy.ndarray + Base station locations where the horizontal magnetic fields are measured. + """ + return self._locations[1] + + @property + def component(self): + r"""Tipper data type; i.e. "real", "imag" + + For the tipper element :math:`T_{ij}`, the `component` property specifies + whether the data are: + - 'real': Real component of the tipper (unitless) + - 'imag': Imaginary component of the tipper (unitless) + - 'complex': Complex tipper (unitless) + + Returns + ------- + str + Tipper data type; i.e. "real", "imag", "complex" + """ + return self._component + + @component.setter + def component(self, var): + self._component = validate_string( + "component", + var, + [ + ("real", "re", "in-phase", "in phase"), + ("imag", "imaginary", "im", "out-of-phase", "out of phase"), + "complex", + ], ) @property def orientation(self): - """Orientation of the receiver. + """Specifies the tipper element :math:`T_{ij}` corresponding to the data. Returns ------- str - Orientation of the receiver. One of {'zx', 'zy'} + Specifies the tipper element :math:`T_{ij}` corresponding to the data. + One of {'xx', 'yx', 'zx', 'zy', 'yy', 'zy'}. """ return self._orientation @orientation.setter def orientation(self, var): self._orientation = validate_string( - "orientation", var, string_list=("zx", "zy") + "orientation", var, string_list=("zx", "zy", "xx", "xy", "yx", "yy") ) def _eval_tipper(self, src, mesh, f): # will grab both primary and secondary and sum them! h = f[src, "h"] - hx = self.getP(mesh, "Fx", "h") @ h - hy = self.getP(mesh, "Fy", "h") @ h - hz = self.getP(mesh, "Fz", "h") @ h + Phx = self.getP(mesh, "Fx", 1) + Phy = self.getP(mesh, "Fy", 1) + Pho = self.getP(mesh, "F" + self.orientation[0], 0) + + hx = Phx @ h + hy = Phy @ h + ho = Pho @ h if self.orientation[1] == "x": h = -hy else: h = hx - top = h[:, 0] * hz[:, 1] - h[:, 1] * hz[:, 0] + top = h[:, 0] * ho[:, 1] - h[:, 1] * ho[:, 0] bot = hx[:, 0] * hy[:, 1] - hx[:, 1] * hy[:, 0] return top / bot @@ -504,32 +723,33 @@ def _eval_tipper_deriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=False): # will grab both primary and secondary and sum them! h = f[src, "h"] - Phx = self.getP(mesh, "Fx", "h") - Phy = self.getP(mesh, "Fy", "h") - Phz = self.getP(mesh, "Fz", "h") + Phx = self.getP(mesh, "Fx", 1) + Phy = self.getP(mesh, "Fy", 1) + Pho = self.getP(mesh, "F" + self.orientation[0], 0) + hx = Phx @ h hy = Phy @ h - hz = Phz @ h + ho = Pho @ h if self.orientation[1] == "x": h = -hy else: h = hx - top = h[:, 0] * hz[:, 1] - h[:, 1] * hz[:, 0] + top = h[:, 0] * ho[:, 1] - h[:, 1] * ho[:, 0] bot = hx[:, 0] * hy[:, 1] - hx[:, 1] * hy[:, 0] - imp = top / bot + tip = top / bot if adjoint: # Work backwards! gtop_v = (v / bot)[..., None] - gbot_v = (-imp * v / bot)[..., None] + gbot_v = (-tip * v / bot)[..., None] n_d = self.nD ghx_v = np.c_[hy[:, 1], -hy[:, 0]] * gbot_v ghy_v = np.c_[-hx[:, 1], hx[:, 0]] * gbot_v - ghz_v = np.c_[-h[:, 1], h[:, 0]] * gtop_v - gh_v = np.c_[hz[:, 1], -hz[:, 0]] * gtop_v + gho_v = np.c_[-h[:, 1], h[:, 0]] * gtop_v + gh_v = np.c_[ho[:, 1], -ho[:, 0]] * gtop_v if self.orientation[1] == "x": ghy_v -= gh_v @@ -540,25 +760,25 @@ def _eval_tipper_deriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=False): # collapse into a long list of n_d vectors ghx_v = ghx_v.reshape((n_d, -1)) ghy_v = ghy_v.reshape((n_d, -1)) - ghz_v = ghz_v.reshape((n_d, -1)) + gho_v = gho_v.reshape((n_d, -1)) - gh_v = Phx.T @ ghx_v + Phy.T @ ghy_v + Phz.T @ ghz_v + gh_v = Phx.T @ ghx_v + Phy.T @ ghy_v + Pho.T @ gho_v return f._hDeriv(src, None, gh_v, adjoint=True) dh_v = f._hDeriv(src, du_dm_v, v, adjoint=False) dhx_v = Phx @ dh_v dhy_v = Phy @ dh_v - dhz_v = Phz @ dh_v + dho_v = Pho @ dh_v if self.orientation[1] == "x": dh_v = -dhy_v else: dh_v = dhx_v dtop_v = ( - h[:, 0] * dhz_v[:, 1] - + dh_v[:, 0] * hz[:, 1] - - h[:, 1] * dhz_v[:, 0] - - dh_v[:, 1] * hz[:, 0] + h[:, 0] * dho_v[:, 1] + + dh_v[:, 0] * ho[:, 1] + - h[:, 1] * dho_v[:, 0] + - dh_v[:, 1] * ho[:, 0] ) dbot_v = ( hx[:, 0] * dhy_v[:, 1] @@ -569,61 +789,598 @@ def _eval_tipper_deriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=False): return (bot * dtop_v - top * dbot_v) / (bot * bot) - def eval(self, src, mesh, f, return_complex=False): # noqa: A003 + def eval(self, src, mesh, f): # noqa: A003 + tip = self._eval_tipper(src, mesh, f) + if self.component == "complex": + return tip + else: + return getattr(tip, self.component) + + def evalDeriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=False): + # Docstring inherited from parent class (Impedance). + if self.component == "complex": + raise NotImplementedError( + "complex valued data derivative is not implemented." + ) + if adjoint: + if self.component == "imag": + v = -1j * v + imp_deriv = self._eval_tipper_deriv( + src, mesh, f, du_dm_v=du_dm_v, v=v, adjoint=adjoint + ) + if adjoint: + return imp_deriv + return getattr(imp_deriv, self.component) + + +class Admittance(_ElectricAndMagneticReceiver): + r"""Receiver class for data types derived from the 3D admittance tensor. + + This class is used to simulate data types that can be derived from the admittance tensor: + + .. math:: + \begin{bmatrix} Y_{xx} & Y_{xy} \\ Y_{yx} & Y_{yy} \\ Y_{zx} & Y_{zy} \end{bmatrix} = + \begin{bmatrix} H_x^{(x)} & H_x^{(y)} \\ H_y^{(x)} & H_y^{(y)} \\ H_z^{(x)} & H_z^{(y)} \end{bmatrix}_{\, r} \; + \begin{bmatrix} E_x^{(x)} & E_x^{(y)} \\ E_y^{(x)} & E_y^{(y)} \end{bmatrix}_b^{-1} + + where superscripts :math:`(x)` and :math:`(y)` denote signals corresponding to + incident planewaves whose electric fields are polarized along the x and y-directions + respectively. Note that in simpeg, natural source EM data are defined according to + standard xyz coordinates; i.e. (x,y,z) is (Easting, Northing, Z +ve up). + + Parameters + ---------- + locations_e : (n_loc, n_dim) array_like + Locations where the electric fields are measured. + locations_h : (n_loc, n_dim) array_like, optional + Locations where the magnetic fields are measured. Defaults to the same + locations as electric field measurements, `locations_e`. + orientation : {'xx', 'xy', 'yx', 'yy', 'zx', 'zy'} + Admittance receiver orientation. Specifies the admittance tensor element + :math:`Y_{ij}` corresponding to the data. The data type is specified by + the `component` input argument. + component : {'real', 'imag', 'complex'} + Admittance data type. For the admittance element :math:`Y_{ij}` specified by the + `orientation` input argument, the receiver can be set to compute the following: + - 'real': Real component of the admittance (A/V) + - 'imag': Imaginary component of the admittance (A/V) + - 'complex': The complex admittance is returned. Do not use for inversion! + storeProjections : bool + Whether to cache to internal projection matrices. + """ + + def __init__( + self, + locations_e, + locations_h=None, + orientation="xx", + component="real", + storeProjections=False, + ): + if locations_h is None: + locations_h = locations_e + super().__init__( + locations1=locations_e, + locations2=locations_h, + storeProjections=storeProjections, + ) + self.orientation = orientation + self.component = component + + @property + def orientation(self): + """Receiver orientation. + + Specifies whether the receiver's data correspond to + the :math:`Y_{xx}`, :math:`Y_{xy}`, :math:`Y_{yx}`, :math:`Y_{yy}`, + :math:`Y_{zx}`, or :math:`Y_{zy}` admittance. + + Returns + ------- + str + Receiver orientation. One of {'xx', 'xy', 'yx', 'yy', 'zx', 'zy'} """ - Project the fields to natural source data. + return self._orientation + + @orientation.setter + def orientation(self, var): + self._orientation = validate_string( + "orientation", var, string_list=("xx", "xy", "yx", "yy", "zx", "zy") + ) + + @property + def component(self): + r"""Admittance data type. + + For the admittance element :math:`Y_{ij}`, the `component` property specifies + whether the data are: + - 'real': Real component of the admittance (A/V) + - 'imag': Imaginary component of the admittance (A/V) + - 'complex': Complex admittance (A/V) + + Returns + ------- + str + Data type; i.e. "real", "imag". + """ + return self._component + + @component.setter + def component(self, var): + self._component = validate_string( + "component", + var, + [ + ("real", "re", "in-phase", "in phase"), + ("imag", "imaginary", "im", "out-of-phase", "out of phase"), + "complex", + ], + ) + + def _eval_admittance(self, src, mesh, f): + if mesh.dim < 3: + raise NotImplementedError( + "Admittance receiver not implemented for dim < 3." + ) + + e = f[src, "e"] + h = f[src, "h"] + + ex = self.getP(mesh, "Ex", 0) @ e + ey = self.getP(mesh, "Ey", 0) @ e + + h = self.getP(mesh, "F" + self.orientation[0], 1) @ h + + if self.orientation[1] == "x": + top = h[:, 0] * ey[:, 1] - h[:, 1] * ex[:, 1] + else: + top = -h[:, 0] * ey[:, 0] + h[:, 1] * ex[:, 0] + + bot = ex[:, 0] * ey[:, 1] - ex[:, 1] * ey[:, 0] + + return top / bot + + def _eval_admittance_deriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=False): + if mesh.dim < 3: + raise NotImplementedError( + "Admittance receiver not implemented for dim < 3." + ) + + # Compute admittances + e = f[src, "e"] + h = f[src, "h"] + + Pex = self.getP(mesh, "Ex", 0) + Pey = self.getP(mesh, "Ey", 0) + Ph = self.getP(mesh, "F" + self.orientation[0], 1) + + ex = Pex @ e + ey = Pey @ e + h = Ph @ h + + if self.orientation[1] == "x": + p_ind = 1 + fact = 1.0 + else: + p_ind = 0 + fact = -1.0 + + top = fact * (h[:, 0] * ey[:, p_ind] - h[:, 1] * ex[:, p_ind]) + bot = ex[:, 0] * ey[:, 1] - ex[:, 1] * ey[:, 0] + adm = top / bot + + # ADJOINT + if adjoint: + if self.component == "imag": + v = -1j * v + + # J_T * v = d_top_T * a_v + d_bot_T * b + a_v = fact * v / bot # term 1 + b_v = -adm * v / bot # term 2 + + ex_v = np.c_[ey[:, 1], -ey[:, 0]] * b_v[:, None] # terms dex in bot + ey_v = np.c_[-ex[:, 1], ex[:, 0]] * b_v[:, None] # terms dey in bot + ex_v[:, p_ind] -= h[:, 1] * a_v # add terms dex in top + ey_v[:, p_ind] += h[:, 0] * a_v # add terms dey in top + e_v = Pex.T @ ex_v + Pey.T @ ey_v + + h_v = np.c_[ey[:, p_ind], -ex[:, p_ind]] * a_v[:, None] # h in top + h_v = Ph.T @ h_v + + fu_e_v, fm_e_v = f._eDeriv(src, None, e_v, adjoint=True) + fu_h_v, fm_h_v = f._hDeriv(src, None, h_v, adjoint=True) + + return fu_e_v + fu_h_v, fm_e_v + fm_h_v + + # JVEC + de_v = f._eDeriv(src, du_dm_v, v, adjoint=False) + dh_v = Ph @ f._hDeriv(src, du_dm_v, v, adjoint=False) + + dex_v = Pex @ de_v + dey_v = Pey @ de_v + + dtop_v = fact * ( + h[:, 0] * dey_v[:, p_ind] + + dh_v[:, 0] * ey[:, p_ind] + - h[:, 1] * dex_v[:, p_ind] + - dh_v[:, 1] * ex[:, p_ind] + ) + dbot_v = ( + ex[:, 0] * dey_v[:, 1] + + dex_v[:, 0] * ey[:, 1] + - ex[:, 1] * dey_v[:, 0] + - dex_v[:, 1] * ey[:, 0] + ) + adm_deriv = (bot * dtop_v - top * dbot_v) / (bot * bot) + + return getattr(adm_deriv, self.component) + + def eval(self, src, mesh, f): # noqa: A003 + # Docstring inherited from parent class (Impedance). + adm = self._eval_admittance(src, mesh, f) + if self.component == "complex": + return adm + return getattr(adm, self.component) + + def evalDeriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=False): + # Docstring inherited from parent class (Impedance). + if self.component == "complex": + raise NotImplementedError( + "complex valued data derivative is not implemented." + ) + return self._eval_admittance_deriv( + src, mesh, f, du_dm_v=du_dm_v, v=v, adjoint=adjoint + ) + + +class ApparentConductivity(_ElectricAndMagneticReceiver): + r"""Receiver class for simulating apparent conductivity data (3D problems only). + + This class is used to simulate apparent conductivity data, in S/m, as defined by: + + .. math:: + \sigma_{app} = \mu_0 \omega \dfrac{\big | \vec{H} \big |^2}{\big | \vec{E} \big |^2} + + where :math:`\omega` is the angular frequency in rad/s, + + .. math:: + \big | \vec{H} \big | = \Big [ H_x^2 + H_y^2 + H_z^2 \Big ]^{1/2} + + and + + .. math:: + \big | \vec{E} \big | = \Big [ E_x^2 + E_y^2 \Big ]^{1/2} + + Parameters + ---------- + locations_e : (n_loc, n_dim) array_like + Locations where the electric fields are measured. + locations_h : (n_loc, n_dim) array_like, optional + Locations where the magnetic fields are measured. Defaults to the same + locations as electric field measurements, `locations_e`. + storeProjections : bool + Whether to cache to internal projection matrices. + """ + + def __init__(self, locations_e, locations_h=None, storeProjections=False): + if locations_h is None: + locations_h = locations_e + super().__init__( + locations1=locations_e, + locations2=locations_h, + storeProjections=storeProjections, + ) + + def _eval_apparent_conductivity(self, src, mesh, f): + if mesh.dim < 3: + raise NotImplementedError( + "ApparentConductivity receiver not implemented for dim < 3." + ) + + e = f[src, "e"] + h = f[src, "h"] + + Pex = self.getP(mesh, "Ex", 0) + Pey = self.getP(mesh, "Ey", 0) + Phx = self.getP(mesh, "Fx", 1) + Phy = self.getP(mesh, "Fy", 1) + Phz = self.getP(mesh, "Fz", 1) + + ex = np.sum(Pex @ e, axis=-1) + ey = np.sum(Pey @ e, axis=-1) + hx = np.sum(Phx @ h, axis=-1) + hy = np.sum(Phy @ h, axis=-1) + hz = np.sum(Phz @ h, axis=-1) + + top = np.abs(hx) ** 2 + np.abs(hy) ** 2 + np.abs(hz) ** 2 + bot = np.abs(ex) ** 2 + np.abs(ey) ** 2 + + return (2 * np.pi * src.frequency * mu_0) * top / bot + + def _eval_apparent_conductivity_deriv( + self, src, mesh, f, du_dm_v=None, v=None, adjoint=False + ): + if mesh.dim < 3: + raise NotImplementedError( + "Admittance receiver not implemented for dim < 3." + ) + + # Compute admittances + e = f[src, "e"] + h = f[src, "h"] + + Pex = self.getP(mesh, "Ex", 0) + Pey = self.getP(mesh, "Ey", 0) + Phx = self.getP(mesh, "Fx", 1) + Phy = self.getP(mesh, "Fy", 1) + Phz = self.getP(mesh, "Fz", 1) + + ex = np.sum(Pex @ e, axis=-1) + ey = np.sum(Pey @ e, axis=-1) + hx = np.sum(Phx @ h, axis=-1) + hy = np.sum(Phy @ h, axis=-1) + hz = np.sum(Phz @ h, axis=-1) + + fact = 2 * np.pi * src.frequency * mu_0 + top = np.abs(hx) ** 2 + np.abs(hy) ** 2 + np.abs(hz) ** 2 + bot = np.abs(ex) ** 2 + np.abs(ey) ** 2 + + # ADJOINT + if adjoint: + # Compute: J_T * v = d_top_T * a_v + d_bot_T * b + a_v = fact * v / bot # term 1 + b_v = -fact * top * v / bot**2 # term 2 + + hx *= a_v + hy *= a_v + hz *= a_v + ex *= b_v + ey *= b_v + + e_v = 2 * (Pex.T @ ex + Pey.T @ ey).conjugate() + h_v = 2 * (Phx.T @ hx + Phy.T @ hy + Phz.T @ hz).conjugate() + + fu_e_v, fm_e_v = f._eDeriv(src, None, e_v, adjoint=True) + fu_h_v, fm_h_v = f._hDeriv(src, None, h_v, adjoint=True) + + return fu_e_v + fu_h_v, fm_e_v + fm_h_v + + # JVEC + de_v = f._eDeriv(src, du_dm_v, v, adjoint=False) + dh_v = f._hDeriv(src, du_dm_v, v, adjoint=False) + + dex_v = np.sum(Pex @ de_v, axis=-1) + dey_v = np.sum(Pey @ de_v, axis=-1) + dhx_v = np.sum(Phx @ dh_v, axis=-1) + dhy_v = np.sum(Phy @ dh_v, axis=-1) + dhz_v = np.sum(Phz @ dh_v, axis=-1) + + # Imaginary components cancel and its 2x the real + dtop_v = ( + 2 + * ( + hx * dhx_v.conjugate() + hy * dhy_v.conjugate() + hz * dhz_v.conjugate() + ).real + ) + + dbot_v = 2 * (ex * dex_v.conjugate() + ey * dey_v.conjugate()).real + + return fact * (bot * dtop_v - top * dbot_v) / (bot * bot) + + def eval(self, src, mesh, f): # noqa: A003 + """Compute receiver data from the discrete field solution. Parameters ---------- - src : simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc - NSEM source - mesh : discretize.TensorMesh mesh - Mesh on which the discretize solution is obtained + src : .frequency_domain.sources.BaseFDEMSrc + NSEM source. + mesh : discretize.TensorMesh + Mesh on which the discretize solution is obtained. f : simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM - NSEM fields object of the source - return_complex : bool (optional) - Flag for return the complex evaluation + NSEM fields object of the source. Returns ------- numpy.ndarray - Evaluated data for the receiver + Evaluated data for the receiver. """ + return self._eval_apparent_conductivity(src, mesh, f) + + def evalDeriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=False): + r"""Derivative of data with respect to the fields. - rx_eval_complex = self._eval_tipper(src, mesh, f) + Let :math:`\mathbf{d}` represent the data corresponding the receiver object. + And let :math:`\mathbf{u}` represent the discrete numerical solution of the + fields on the mesh. Where :math:`\mathbf{P}` is a projection function that + maps from the fields to the data, i.e.: - return getattr(rx_eval_complex, self.component) + .. math:: + \mathbf{d} = \mathbf{P}(\mathbf{u}) - def evalDeriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=False): - """Derivative of projection with respect to the fields + this method computes and returns the derivative: + + .. math:: + \dfrac{\partial \mathbf{d}}{\partial \mathbf{u}} = + \dfrac{\partial [ \mathbf{P} (\mathbf{u}) ]}{\partial \mathbf{u}} Parameters ---------- - str : simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc - NSEM source + src : .frequency_domain.sources.BaseFDEMSrc + The NSEM source. mesh : discretize.TensorMesh - Mesh on which the discretize solution is obtained + Mesh on which the discretize solution is obtained. f : simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM - NSEM fields object of the source - du_dm_v : None, + NSEM fields object for the source. + du_dm_v : None, optional Supply pre-computed derivative? - v : numpy.ndarray + v : numpy.ndarray, optional Vector of size - adjoint : bool, default = ``False`` - If ``True``, compute the adjoint operation + adjoint : bool, optional + Whether to compute the ajoint operation. Returns ------- numpy.ndarray - Calculated derivative (nD,) (adjoint=False) and (nP,2) (adjoint=True) for both polarizations + Calculated derivative (n_data,) if `adjoint` is ``False``, and (n_param, 2) if `adjoint` + is ``True``, for both polarizations. """ - - if adjoint: - if self.component == "imag": - v = -1j * v - imp_deriv = self._eval_tipper_deriv( + return self._eval_apparent_conductivity_deriv( src, mesh, f, du_dm_v=du_dm_v, v=v, adjoint=adjoint ) - if adjoint: - return imp_deriv - return getattr(imp_deriv, self.component) + + +@deprecate_class(removal_version="0.24.0", future_warn=True, replace_docstring=False) +class PointNaturalSource(Impedance): + """Point receiver class for magnetotelluric simulations. + + .. warning:: + This class is deprecated and will be removed in SimPEG v0.24.0. + Please use :class:`.natural_source.receivers.Impedance`. + + Assumes that the data locations are standard xyz coordinates; + i.e. (x,y,z) is (Easting, Northing, up). + + Parameters + ---------- + locations : (n_loc, n_dim) numpy.ndarray + Receiver locations. + orientation : {'xx', 'xy', 'yx', 'yy'} + MT receiver orientation. + component : {'real', 'imag', 'apparent_resistivity', 'phase'} + MT data type. + """ + + def __init__( + self, + locations=None, + orientation="xy", + component="real", + locations_e=None, + locations_h=None, + **kwargs, + ): + if locations is None: + if (locations_e is None) ^ ( + locations_h is None + ): # if only one of them is none + raise TypeError( + "Either locations or both locations_e and locations_h must be passed" + ) + if locations_e is None and locations_h is None: + warnings.warn( + "Using the default for locations is deprecated behavior. Please explicitly set locations. ", + FutureWarning, + stacklevel=2, + ) + locations_e = np.array([[0.0]]) + locations_h = locations_e + else: # locations was not None + if locations_e is not None or locations_h is not None: + raise TypeError( + "Cannot pass both locations and locations_e or locations_h at the same time." + ) + if isinstance(locations, list): + if len(locations) == 2: + locations_e = locations[0] + locations_h = locations[1] + elif len(locations) == 1: + locations_e = locations[0] + locations_h = locations[0] + else: + raise ValueError("incorrect size of list, must be length of 1 or 2") + else: + locations_e = locations_h = locations + + super().__init__( + locations_e=locations_e, + locations_h=locations_h, + orientation=orientation, + component=component, + **kwargs, + ) + + def eval(self, src, mesh, f, return_complex=False): # noqa: A003 + if return_complex: + warnings.warn( + "Calling with return_complex=True is deprecated in SimPEG 0.23. Instead set rx.component='complex'", + FutureWarning, + stacklevel=2, + ) + temp = self.component + self.component = "complex" + out = super().eval(src, mesh, f) + self.component = temp + else: + out = super().eval(src, mesh, f) + return out + + locations = property(lambda self: self._locations[0], Impedance.locations.fset) + + +@deprecate_class(removal_version="0.24.0", future_warn=True, replace_docstring=False) +class Point3DTipper(Tipper): + """Point receiver class for Z-axis tipper simulations. + + .. warning:: + This class is deprecated and will be removed in SimPEG v0.24.0. + Please use :class:`.natural_source.receivers.Tipper`. + + Assumes that the data locations are standard xyz coordinates; + i.e. (x,y,z) is (Easting, Northing, up). + + Parameters + ---------- + locations : (n_loc, n_dim) numpy.ndarray + Receiver locations. + orientation : str, default = 'zx' + NSEM receiver orientation. Must be one of {'zx', 'zy'} + component : str, default = 'real' + NSEM data type. Choose one of {'real', 'imag', 'apparent_resistivity', 'phase'} + """ + + def __init__( + self, + locations, + orientation="zx", + component="real", + locations_e=None, + locations_h=None, + **kwargs, + ): + # note locations_e and locations_h never did anything for this class anyways + # so can just issue a warning here... + if locations_e is not None or locations_h is not None: + warnings.warn( + "locations_e and locations_h are unused for this class", + UserWarning, + stacklevel=2, + ) + if isinstance(locations, list): + if len(locations) < 3: + locations = locations[0] + else: + raise ValueError("incorrect size of list, must be length of 1 or 2") + + super().__init__( + locations_h=locations, + orientation=orientation, + component=component, + **kwargs, + ) + + def eval(self, src, mesh, f, return_complex=False): # noqa: A003 + if return_complex: + warnings.warn( + "Calling with return_complex=True is deprecated in SimPEG 0.23. Instead set rx.component='complex'", + FutureWarning, + stacklevel=2, + ) + temp = self.component + self.component = "complex" + out = super().eval(src, mesh, f) + self.component = temp + else: + out = super().eval(src, mesh, f) + return out + + locations = property(lambda self: self._locations[0], Tipper.locations.fset) diff --git a/simpeg/electromagnetics/natural_source/simulation_1d.py b/simpeg/electromagnetics/natural_source/simulation_1d.py index bc05c91759..783d389b95 100644 --- a/simpeg/electromagnetics/natural_source/simulation_1d.py +++ b/simpeg/electromagnetics/natural_source/simulation_1d.py @@ -5,6 +5,7 @@ from ... import props from ...utils import validate_type from ..frequency_domain.survey import Survey +from .receivers import Impedance class Simulation1DRecursive(BaseSimulation): @@ -81,6 +82,12 @@ def survey(self): def survey(self, value): if value is not None: value = validate_type("survey", value, Survey, cast=False) + for src in value.source_list: + for rx in src.receiver_list: + if not isinstance(rx, Impedance): + raise NotImplementedError( + f"{type(self).__name__} does not support {type(rx).__name__} receivers, only implemented for 'Impedance'." + ) self._survey = value @property diff --git a/simpeg/electromagnetics/natural_source/survey.py b/simpeg/electromagnetics/natural_source/survey.py index 0c001fa704..18023547e2 100644 --- a/simpeg/electromagnetics/natural_source/survey.py +++ b/simpeg/electromagnetics/natural_source/survey.py @@ -5,7 +5,7 @@ from ...data import Data as BaseData from ...utils import mkvc from .sources import PlanewaveXYPrimary -from .receivers import PointNaturalSource, Point3DTipper +from .receivers import Impedance, Tipper from .utils.plot_utils import DataNSEMPlotMethods ######### @@ -85,7 +85,7 @@ def toRecArray(self, returnType="RealImag"): # Note: needs to be written more generally, # using diffterent rxTypes and not all the data at the locations # Assume the same locs for all RX - locs = src.receiver_list[0].locations + locs = src.receiver_list[0].locations_e if locs.shape[1] == 1: locs = np.hstack((np.array([[0.0, 0.0]]), locs)) elif locs.shape[1] == 2: @@ -185,32 +185,40 @@ def fromRecArray(cls, recArray, srcType="primary"): if dFreq[rxType].dtype.name in "complex128": if "t" in rxType: receiver_list.append( - Point3DTipper(locs, rxType[1:3], "real") + Tipper(locs, orientation=rxType[1:3], component="real") ) dataList.append(dFreq[rxType][notNaNind].real.copy()) receiver_list.append( - Point3DTipper(locs, rxType[1:3], "imag") + Tipper(locs, orientation=rxType[1:3], component="imag") ) dataList.append(dFreq[rxType][notNaNind].imag.copy()) elif "z" in rxType: receiver_list.append( - PointNaturalSource(locs, rxType[1:3], "real") + Impedance( + locs, orientation=rxType[1:3], component="real" + ) ) dataList.append(dFreq[rxType][notNaNind].real.copy()) receiver_list.append( - PointNaturalSource(locs, rxType[1:3], "imag") + Impedance( + locs, orientation=rxType[1:3], component="imag" + ) ) dataList.append(dFreq[rxType][notNaNind].imag.copy()) else: component = "real" if "r" in rxType else "imag" if "z" in rxType: receiver_list.append( - PointNaturalSource(locs, rxType[1:3], component) + Impedance( + locs, orientation=rxType[1:3], component=component + ) ) dataList.append(dFreq[rxType][notNaNind].copy()) if "t" in rxType: receiver_list.append( - Point3DTipper(locs, rxType[1:3], component) + Tipper( + locs, orientation=rxType[1:3], component=component + ) ) dataList.append(dFreq[rxType][notNaNind].copy()) diff --git a/simpeg/electromagnetics/natural_source/utils/plot_utils.py b/simpeg/electromagnetics/natural_source/utils/plot_utils.py index 261d80591a..4be450445b 100644 --- a/simpeg/electromagnetics/natural_source/utils/plot_utils.py +++ b/simpeg/electromagnetics/natural_source/utils/plot_utils.py @@ -543,7 +543,7 @@ def map_data_locations(self, ax=None, **plot_kwargs): unique_locations = _unique_rows( np.concatenate( [ - rx.locations + rx.locations_e for src in self.survey.source_list for rx in src.receiver_list ] @@ -813,7 +813,7 @@ def _extract_frequency_data( # Should be a more specifice Exeption raise Exception("To many Receivers of the same type, orientation and component") - loc_arr = rx.locations + loc_arr = rx.locations_e data_arr = data[src, rx] if return_uncert: std_arr = data.relative_error[src, rx] @@ -844,7 +844,7 @@ def _extract_location_data(data, location, orientation, component, return_uncert else: rx = rx_list[0] - ind_loc = np.sqrt(np.sum((rx.locations[:, :2] - location) ** 2, axis=1)) < 0.1 + ind_loc = np.sqrt(np.sum((rx.locations_e[:, :2] - location) ** 2, axis=1)) < 0.1 if np.any(ind_loc): freq_list.append(src.frequency) data_list.append(data[src, rx][ind_loc]) diff --git a/simpeg/utils/code_utils.py b/simpeg/utils/code_utils.py index af517056f7..1162150906 100644 --- a/simpeg/utils/code_utils.py +++ b/simpeg/utils/code_utils.py @@ -516,7 +516,11 @@ def __init__(self, add_pckg=None, ncol=3, text_width=80, sort=False): def deprecate_class( - removal_version=None, new_location=None, future_warn=False, error=False + removal_version=None, + new_location=None, + future_warn=False, + error=False, + replace_docstring=True, ): """Utility function to deprecate a class @@ -563,7 +567,8 @@ def __init__(self, *args, **kwargs): cls.__init__ = __init__ if new_location is not None: parent_name = f"{new_location}.{parent_name}" - cls.__doc__ = f""" This class has been deprecated, see `{parent_name}` for documentation""" + if replace_docstring: + cls.__doc__ = f""" This class has been deprecated, see `{parent_name}` for documentation""" return cls return decorator diff --git a/tests/em/nsem/forward/test_Problem1D_AnalyticVsNumeric.py b/tests/em/nsem/forward/test_Problem1D_AnalyticVsNumeric.py index a2432f9135..4a0ef2ee3e 100644 --- a/tests/em/nsem/forward/test_Problem1D_AnalyticVsNumeric.py +++ b/tests/em/nsem/forward/test_Problem1D_AnalyticVsNumeric.py @@ -34,7 +34,7 @@ def calculateAnalyticSolution(source_list, mesh, model): surveyAna = nsem.Survey(source_list) data1D = nsem.Data(surveyAna) for src in surveyAna.source_list: - elev = src.receiver_list[0].locations[0] + elev = src.receiver_list[0].locations_e[0] anaEd, anaEu, anaHd, anaHu = nsem.utils.analytic_1d.getEHfields( mesh, model, src.frequency, elev ) diff --git a/tests/em/nsem/forward/test_Recursive1D_VsAnalyticHalfspace.py b/tests/em/nsem/forward/test_Recursive1D_VsAnalyticHalfspace.py index 16395302a5..3242e6f5aa 100644 --- a/tests/em/nsem/forward/test_Recursive1D_VsAnalyticHalfspace.py +++ b/tests/em/nsem/forward/test_Recursive1D_VsAnalyticHalfspace.py @@ -1,9 +1,15 @@ import unittest +import warnings + from simpeg.electromagnetics import natural_source as nsem from simpeg import maps import numpy as np from scipy.constants import mu_0 +ns_rx = nsem.receivers + +import pytest + def create_survey(freq): receivers_list = [ @@ -61,5 +67,33 @@ def test_4(self): np.testing.assert_allclose(*compute_simulation(100.0, 1.0)) +@pytest.mark.parametrize( + "rx_class", + [ + ns_rx.Impedance, + ns_rx.PointNaturalSource, + ns_rx.Admittance, + ns_rx.Tipper, + ns_rx.Point3DTipper, + ns_rx.ApparentConductivity, + ], +) +def test_incorrect_rx_types(rx_class): + loc = np.zeros((1, 3)) + rx = rx_class(loc) + source = nsem.sources.Planewave(rx, frequency=10) + survey = nsem.Survey(source) + # make sure that only these exact classes do not issue warnings. + if rx_class in [ns_rx.Impedance, ns_rx.PointNaturalSource]: + with warnings.catch_warnings(): + warnings.simplefilter("error") + nsem.Simulation1DRecursive(survey=survey) + else: + with pytest.raises( + NotImplementedError, match="Simulation1DRecursive does not support .*" + ): + nsem.Simulation1DRecursive(survey=survey) + + if __name__ == "__main__": unittest.main() diff --git a/tests/em/nsem/forward/test_Simulation2D_vs_Analytic_pytest.py b/tests/em/nsem/forward/test_Simulation2D_vs_Analytic_pytest.py new file mode 100644 index 0000000000..96833bf431 --- /dev/null +++ b/tests/em/nsem/forward/test_Simulation2D_vs_Analytic_pytest.py @@ -0,0 +1,170 @@ +import pytest +from scipy.constants import mu_0 +import numpy as np +from discretize import TensorMesh +from simpeg.electromagnetics import natural_source as nsem +from simpeg.utils import model_builder +from simpeg import maps + +REL_TOLERANCE = 0.05 +ABS_TOLERANCE = 1e-13 + + +@pytest.fixture +def mesh(): + # Mesh for testing + return TensorMesh( + [ + [(40.0, 10, -1.4), (40.0, 50), (40.0, 10, 1.4)], + [(40.0, 10, -1.4), (40.0, 50), (40.0, 10, 1.4)], + ], + "CC", + ) + + +@pytest.fixture +def mapping(mesh): + return maps.IdentityMap(mesh) + + +def get_model(mesh, model_type): + # Model used for testing + model = 1e-8 * np.ones(mesh.nC) + model[mesh.cell_centers[:, 1] < 0.0] = 1e-2 + + if model_type == "layer": + model[mesh.cell_centers[:, 1] < -500.0] = 1e-1 + elif model_type == "block": + ind_block = model_builder.get_block_indices( + mesh.cell_centers, + np.array([-500, -800]), + np.array([500, -400]), + ) + model[ind_block] = 1e-1 + + return model + + +@pytest.fixture +def locations(): + # Receiver locations + elevation = 0.0 + rx_x = np.arange(-350, 350, 200) + return np.c_[rx_x, elevation + np.zeros_like(rx_x)] + + +@pytest.fixture +def frequencies(): + # Frequencies being evaluated + return [1e1, 2e1] + + +def get_survey(locations, frequencies, survey_type, component, orientation): + source_list = [] + + for f in frequencies: + # MT data types (Zxy, Zyx) + if survey_type == "impedance": + rx_list = [ + nsem.receivers.Impedance( + locations_e=locations, + locations_h=locations, + orientation=orientation, + component=component, + ) + ] + + # ZTEM data types (Tzx, Tzy) + elif survey_type == "tipper": + rx_list = [ + nsem.receivers.Tipper( + locations_h=locations, + locations_base=locations, + orientation=orientation, + component=component, + ) + ] + + source_list.append(nsem.sources.Planewave(rx_list, f)) + + return nsem.survey.Survey(source_list) + + +def get_analytic_halfspace_solution(sigma, f, survey_type, component, orientation): + # MT data types (Zxy, Zyx) + if survey_type == "impedance": + if component in ["real", "imag"]: + ampl = np.sqrt(np.pi * f * mu_0 / sigma) + if orientation == "xy": + return -ampl + else: + return ampl + elif component == "app_res": + return 1 / sigma + elif component == "phase": + if orientation == "xy": + return -135.0 + else: + return 45 + + # ZTEM data types (Tzx, Tzy) + elif survey_type == "tipper": + return 0.0 + + +# Validate impedances, tippers and admittances against analytic +# solution for a halfspace. + +CASES_LIST_HALFSPACE = [ + ("impedance", "real", "xy"), + ("impedance", "real", "yx"), + ("impedance", "imag", "xy"), + ("impedance", "imag", "yx"), + ("impedance", "app_res", "xy"), + ("impedance", "app_res", "yx"), + ("impedance", "phase", "xy"), + ("impedance", "phase", "yx"), + # ("tipper", "real", "zx"), + # ("tipper", "real", "zy"), + # ("tipper", "imag", "zx"), + # ("tipper", "imag", "zy"), +] + + +@pytest.mark.parametrize("survey_type, component, orientation", CASES_LIST_HALFSPACE) +def test_analytic_halfspace_solution( + survey_type, component, orientation, frequencies, locations, mesh, mapping +): + # Numerical solution + survey = get_survey(locations, frequencies, survey_type, component, orientation) + model_hs = get_model(mesh, "halfspace") # 1e-2 halfspace + if orientation in ["xy", "zx"]: + sim = nsem.simulation.Simulation2DElectricField( + mesh, survey=survey, sigmaMap=mapping + ) + elif orientation in ["yx", "zy"]: + sim = nsem.simulation.Simulation2DMagneticField( + mesh, survey=survey, sigmaMap=mapping + ) + + numeric_solution = sim.dpred(model_hs) + + # Analytic solution + sigma_hs = 1e-2 + n_locations = np.shape(locations)[0] + analytic_solution = np.hstack( + [ + get_analytic_halfspace_solution( + sigma_hs, f, survey_type, component, orientation + ) + for f in frequencies + ] + ) + analytic_solution = np.repeat(analytic_solution, n_locations) + + # # Error + err = np.abs( + (numeric_solution - analytic_solution) / (analytic_solution + ABS_TOLERANCE) + ) + + assert np.all(err < REL_TOLERANCE) diff --git a/tests/em/nsem/forward/test_Simulation3D_vs_Analytic_pytest.py b/tests/em/nsem/forward/test_Simulation3D_vs_Analytic_pytest.py new file mode 100644 index 0000000000..0bef1fd56c --- /dev/null +++ b/tests/em/nsem/forward/test_Simulation3D_vs_Analytic_pytest.py @@ -0,0 +1,192 @@ +import pytest +from scipy.constants import mu_0 +import numpy as np +from discretize import TensorMesh +from simpeg.electromagnetics import natural_source as nsem +from simpeg.utils import model_builder, mkvc +from simpeg import maps + +REL_TOLERANCE = 0.05 +ABS_TOLERANCE = 1e-13 + + +@pytest.fixture +def mesh(): + # Mesh for testing + return TensorMesh( + [ + [(200, 6, -1.5), (200.0, 4), (200, 6, 1.5)], + [(200, 6, -1.5), (200.0, 4), (200, 6, 1.5)], + [(200, 8, -1.5), (200.0, 8), (200, 8, 1.5)], + ], + "CCC", + ) + + +@pytest.fixture +def mapping(mesh): + return maps.IdentityMap(mesh) + + +def get_model(mesh, model_type): + # Model used for testing + model = 1e-8 * np.ones(mesh.nC) + model[mesh.cell_centers[:, 2] < 0.0] = 1e-2 + + if model_type == "layer": + model[mesh.cell_centers[:, 2] < -3000.0] = 1e-1 + elif model_type == "block": + ind_block = model_builder.get_block_indices( + mesh.cell_centers, + np.array([-1000, -1000, -1500]), + np.array([1000, 1000, -1000]), + ) + model[ind_block] = 1e-1 + + return model + + +@pytest.fixture +def locations(): + # Receiver locations + elevation = 0.0 + rx_x, rx_y = np.meshgrid(np.arange(-350, 350, 200), np.arange(-350, 350, 200)) + return np.hstack( + (mkvc(rx_x, 2), mkvc(rx_y, 2), elevation + np.zeros((np.prod(rx_x.shape), 1))) + ) + + +@pytest.fixture +def frequencies(): + # Frequencies being evaluated + return [1e-1, 2e-1] + + +def get_survey(locations, frequencies, survey_type, component): + source_list = [] + + for f in frequencies: + # MT data types (Zxx, Zxy, Zyx, Zyy) + if survey_type == "impedance": + if component == "phase": + orientations = ["xy", "yx"] # off-diagonal only!!! + else: + orientations = ["xx", "xy", "yx", "yy"] + rx_list = [ + nsem.receivers.Impedance( + locations_e=locations, + locations_h=locations, + orientation=ij, + component=component, + ) + for ij in orientations + ] + + # ZTEM data types (Txx, Tyx, Tzx, Txy, Tyy, Tzy) + elif survey_type == "tipper": + rx_list = [ + nsem.receivers.Tipper( + locations_h=locations, + locations_base=locations, + orientation=ij, + component=component, + ) + for ij in ["xx", "yx", "zx", "xy", "yy", "zy"] + ] + + # Admittance data types (Yxx, Yyx, Yzx, Yxy, Yyy, Yzy) + elif survey_type == "admittance": + rx_list = [ + nsem.receivers.Admittance( + locations_e=locations, + locations_h=locations, + orientation=ij, + component=component, + ) + for ij in ["xx", "yx", "zx", "xy", "yy", "zy"] + ] + + elif survey_type == "apparent_conductivity": + rx_list = [nsem.receivers.ApparentConductivity(locations)] + + source_list.append(nsem.sources.PlanewaveXYPrimary(rx_list, f)) + + return nsem.survey.Survey(source_list) + + +def get_analytic_halfspace_solution(sigma, f, survey_type, component): + # MT data types (Zxx, Zxy, Zyx, Zyy) + if survey_type == "impedance": + if component in ["real", "imag"]: + ampl = np.sqrt(np.pi * f * mu_0 / sigma) + return np.r_[0.0, -ampl, ampl, 0.0] + elif component == "app_res": + return np.r_[0.0, 1 / sigma, 1 / sigma, 0.0] + elif component == "phase": + return np.r_[-135.0, 45.0] # off-diagonal only! + + # ZTEM data types (Txx, Tyx, Tzx, Txy, Tyy, Tzy) + elif survey_type == "tipper": + if component == "real": + return np.r_[1.0, 0.0, 0.0, 0.0, 1.0, 0.0] + else: + return np.r_[0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + + # Admittance data types (Yxx, Yyx, Yzx, Yxy, Yyy, Yzy) + elif survey_type == "admittance": + ampl = 0.5 * np.sqrt(sigma / (np.pi * f * mu_0)) + if component == "real": + return np.r_[0.0, -ampl, 0.0, ampl, 0.0, 0.0] + else: + return np.r_[0.0, ampl, 0.0, -ampl, 0.0, 0.0] + + # MobileMT data type (app_cond) + elif survey_type == "apparent_conductivity": + return sigma + + +# Validate impedances, tippers and admittances against analytic +# solution for a halfspace. + +CASES_LIST_HALFSPACE = [ + ("impedance", "real"), + ("impedance", "imag"), + ("impedance", "app_res"), + ("impedance", "phase"), + ("tipper", "real"), + ("tipper", "imag"), + ("admittance", "real"), + ("admittance", "imag"), + ("apparent_conductivity", None), +] + + +@pytest.mark.parametrize("survey_type, component", CASES_LIST_HALFSPACE) +def test_analytic_halfspace_solution( + survey_type, component, frequencies, locations, mesh, mapping +): + # Numerical solution + survey = get_survey(locations, frequencies, survey_type, component) + model_hs = get_model(mesh, "halfspace") # 1e-2 halfspace + sim = nsem.simulation.Simulation3DPrimarySecondary( + mesh, survey=survey, sigmaPrimary=model_hs, sigmaMap=mapping + ) + numeric_solution = sim.dpred(model_hs) + + # Analytic solution + sigma_hs = 1e-2 + n_locations = np.shape(locations)[0] + analytic_solution = np.hstack( + [ + get_analytic_halfspace_solution(sigma_hs, f, survey_type, component) + for f in frequencies + ] + ) + analytic_solution = np.repeat(analytic_solution, n_locations) + + # # Error + err = np.abs( + (numeric_solution - analytic_solution) / (analytic_solution + ABS_TOLERANCE) + ) + + assert np.all(err < REL_TOLERANCE) diff --git a/tests/em/nsem/inversion/test_NSEM_2D_jvecjtvecadj_pytest.py b/tests/em/nsem/inversion/test_NSEM_2D_jvecjtvecadj_pytest.py new file mode 100644 index 0000000000..e93aee6089 --- /dev/null +++ b/tests/em/nsem/inversion/test_NSEM_2D_jvecjtvecadj_pytest.py @@ -0,0 +1,232 @@ +import pytest +import numpy as np +from discretize import TensorMesh, tests +from simpeg import ( + maps, + data_misfit, +) +from simpeg.utils import model_builder +from simpeg.electromagnetics import natural_source as nsem + +ADJ_RTOL = 1e-5 + + +@pytest.fixture +def mesh(): + return TensorMesh( + [ + [(40.0, 10, -1.4), (40.0, 50), (40.0, 10, 1.4)], + [(40.0, 10, -1.4), (40.0, 50), (40.0, 10, 1.4)], + ], + "CC", + ) + + +@pytest.fixture +def active_cells(mesh): + return mesh.cell_centers[:, 1] < 0.0 + + +@pytest.fixture +def mapping(mesh, active_cells): + return maps.InjectActiveCells(mesh, active_cells, 1e-8) * maps.ExpMap( + nP=np.sum(active_cells) + ) + + +@pytest.fixture +def sigma_hs(mesh, active_cells): + sigma_hs = 1e-8 * np.ones(mesh.nC) + sigma_hs[active_cells] = 1e1 + return sigma_hs + + +@pytest.fixture +def locations(): + # Receiver locations + elevation = 0.0 + rx_x = np.arange(-350, 350, 200) + return np.c_[rx_x, elevation + np.zeros_like(rx_x)] + + +@pytest.fixture +def frequencies(): + # Frequencies being evaluated + return [1e-1, 2e-1] + + +def get_survey(survey_type, orientation, components, locations, frequencies): + + if not isinstance(components, list): + components = [components] + + source_list = [] + + for f in frequencies: + + # MT data types (Zxy or Zyx) + if survey_type == "impedance": + rx_list = [ + nsem.receivers.Impedance( + locations, + orientation=orientation, + component=comp, + ) + for comp in components + ] + + # ZTEM data types (Tzx or Tzy) + elif survey_type == "tipper": + rx_list = [ + nsem.receivers.Tipper( + locations_h=locations, + locations_base=np.zeros_like(locations), + orientation=orientation, + component=comp, + ) + for comp in components + ] + + # Admittance data types (Yxy or Yyx) + elif survey_type == "admittance": + rx_list = [ + nsem.receivers.Admittance( + locations, + orientation=orientation, + component=comp, + ) + for comp in components + ] + + source_list.append(nsem.sources.Planewave(rx_list, f)) + + return nsem.survey.Survey(source_list) + + +CASES_LIST = [ + ("impedance", "xy", ["real", "imag"]), + ("impedance", "yx", ["real", "imag"]), + ("impedance", "xy", ["app_res"]), + ("impedance", "yx", ["app_res"]), + ("impedance", "xy", ["phase"]), + ("impedance", "yx", ["phase"]), +] + + +@pytest.mark.parametrize("survey_type, orientation, components", CASES_LIST) +class TestDerivatives: + def get_setup_objects( + self, + survey_type, + orientation, + components, + locations, + frequencies, + mesh, + active_cells, + mapping, + sigma_hs, + ): + survey = get_survey( + survey_type, orientation, components, locations, frequencies + ) + + # Define the simulation + if orientation in ["xy", "zy"]: + sim = nsem.simulation.Simulation2DElectricField( + mesh, survey=survey, sigmaMap=mapping + ) + elif orientation in ["yx", "zx"]: + sim = nsem.simulation.Simulation2DMagneticField( + mesh, survey=survey, sigmaMap=mapping + ) + + n_active = np.sum(active_cells) + + rng = np.random.default_rng(4412) + # Model + m0 = np.log(1e1) * np.ones(n_active) + ind = model_builder.get_indices_block( + np.r_[-200.0, -600.0], + np.r_[200.0, -200.0], + mesh.cell_centers[active_cells, :], + ) + m0[ind] = np.log(1e0) + m0 += 0.01 * rng.uniform(low=-1, high=1, size=n_active) + + # Define data and misfit + data = sim.make_synthetic_data(m0, add_noise=True) + dmis = data_misfit.L2DataMisfit(simulation=sim, data=data) + + return m0, dmis + + def test_misfit( + self, + survey_type, + orientation, + components, + locations, + frequencies, + mesh, + active_cells, + mapping, + sigma_hs, + ): + m0, dmis = self.get_setup_objects( + survey_type, + orientation, + components, + locations, + frequencies, + mesh, + active_cells, + mapping, + sigma_hs, + ) + sim = dmis.simulation + + passed = tests.check_derivative( + lambda m: (sim.dpred(m), lambda mx: sim.Jvec(m, mx)), + m0, + plotIt=False, + num=2, + random_seed=42, + ) + + assert passed + + def test_adjoint( + self, + survey_type, + orientation, + components, + locations, + frequencies, + mesh, + active_cells, + mapping, + sigma_hs, + ): + m0, dmis = self.get_setup_objects( + survey_type, + orientation, + components, + locations, + frequencies, + mesh, + active_cells, + mapping, + sigma_hs, + ) + sim = dmis.simulation + n_data = sim.survey.nD + + f = sim.fields(m0) + tests.assert_isadjoint( + lambda u: sim.Jvec(m0, u, f=f), + lambda v: sim.Jtvec(m0, v, f=f), + m0.shape, + (n_data,), + rtol=ADJ_RTOL, + random_seed=44, + ) diff --git a/tests/em/nsem/inversion/test_NSEM_3D_jvecjtvecadj_pytest.py b/tests/em/nsem/inversion/test_NSEM_3D_jvecjtvecadj_pytest.py new file mode 100644 index 0000000000..457499fdc7 --- /dev/null +++ b/tests/em/nsem/inversion/test_NSEM_3D_jvecjtvecadj_pytest.py @@ -0,0 +1,252 @@ +import pytest +import numpy as np +from discretize import TensorMesh, tests +from simpeg import ( + maps, + data_misfit, +) +from simpeg.utils import mkvc, model_builder +from simpeg.electromagnetics import natural_source as nsem + +ADJ_RTOL = 1e-10 + + +@pytest.fixture +def mesh(): + return TensorMesh( + [ + [(200, 6, -1.5), (200.0, 4), (200, 6, 1.5)], + [(200, 6, -1.5), (200.0, 4), (200, 6, 1.5)], + [(200, 8, -1.5), (200.0, 8), (200, 8, 1.5)], + ], + "CCC", + ) + + +@pytest.fixture +def active_cells(mesh): + return mesh.cell_centers[:, 2] < 0.0 + + +@pytest.fixture +def mapping(mesh, active_cells): + return maps.InjectActiveCells(mesh, active_cells, 1e-8) * maps.ExpMap( + nP=np.sum(active_cells) + ) + + +@pytest.fixture +def sigma_hs(mesh, active_cells): + sigma_hs = 1e-8 * np.ones(mesh.nC) + sigma_hs[active_cells] = 1e1 + return sigma_hs + + +@pytest.fixture +def locations(): + # Receiver locations + elevation = 0.0 + rx_x, rx_y = np.meshgrid(np.arange(-350, 350, 200), np.arange(-350, 350, 200)) + return np.hstack( + (mkvc(rx_x, 2), mkvc(rx_y, 2), elevation + np.zeros((np.prod(rx_x.shape), 1))) + ) + + +@pytest.fixture +def frequencies(): + # Frequencies being evaluated + return [1e-1, 2e-1] + + +def get_survey(survey_type, orientations, components, locations, frequencies): + if not isinstance(orientations, list): + orientations = [orientations] + + if not isinstance(components, list): + components = [components] + + source_list = [] + + for f in frequencies: + rx_list = [] + + # MT data types (Zxx, Zxy, Zyx, Zyy) + if survey_type == "impedance": + for orient in orientations: + rx_list.extend( + [ + nsem.receivers.Impedance( + locations, + orientation=orient, + component=comp, + ) + for comp in components + ] + ) + + # ZTEM data types (Txx, Tyx, Tzx, Txy, Tyy, Tzy) + elif survey_type == "tipper": + for orient in orientations: + rx_list.extend( + [ + nsem.receivers.Tipper( + locations_h=locations, + locations_base=np.zeros_like(locations), + orientation=orient, + component=comp, + ) + for comp in components + ] + ) + + # Admittance data types (Yxx, Yyx, Yzx, Yxy, Yyy, Yzy) + elif survey_type == "admittance": + for orient in orientations: + rx_list.extend( + [ + nsem.receivers.Admittance( + locations, + orientation=orient, + component=comp, + ) + for comp in components + ] + ) + + # MobileMT is app_cond + elif survey_type == "apparent_conductivity": + rx_list.extend([nsem.receivers.ApparentConductivity(locations)]) + + source_list.append(nsem.sources.PlanewaveXYPrimary(rx_list, f)) + + return nsem.survey.Survey(source_list) + + +CASES_LIST = [ + ("impedance", ["xy", "yx"], ["real", "imag"]), + ("impedance", ["xx", "yy"], ["real", "imag"]), + ("impedance", ["xy", "yx"], ["app_res"]), + ("impedance", ["xx", "yy"], ["app_res"]), + ("impedance", ["xy", "yx"], ["phase"]), + ("tipper", ["zx", "zy"], ["real", "imag"]), + ("tipper", ["xx", "yy"], ["real", "imag"]), + ("tipper", ["xy", "yx"], ["real", "imag"]), + ("admittance", ["xy", "yx"], ["real", "imag"]), + ("admittance", ["xx", "yy"], ["real", "imag"]), + ("admittance", ["zx", "zy"], ["real", "imag"]), + ("apparent_conductivity", None, None), +] + + +@pytest.mark.parametrize("survey_type, orientations, components", CASES_LIST) +class TestDerivatives: + def get_setup_objects( + self, + survey_type, + orientations, + components, + locations, + frequencies, + mesh, + active_cells, + mapping, + sigma_hs, + ): + survey = get_survey( + survey_type, orientations, components, locations, frequencies + ) + + # Define the simulation + sim = nsem.simulation.Simulation3DPrimarySecondary( + mesh, survey=survey, sigmaMap=mapping, sigmaPrimary=sigma_hs + ) + + n_active = np.sum(active_cells) + rng = np.random.default_rng(4412) + + # Model + m0 = np.log(1e1) * np.ones(n_active) + ind = model_builder.get_indices_block( + np.r_[-200.0, -200.0, -600.0], + np.r_[200.0, 200.0, -200.0], + mesh.cell_centers[active_cells, :], + ) + m0[ind] = np.log(1e0) + m0 += 0.01 * rng.uniform(low=-1, high=1, size=n_active) + + # Define data and misfit + data = sim.make_synthetic_data(m0, add_noise=True) + dmis = data_misfit.L2DataMisfit(simulation=sim, data=data) + + return m0, dmis + + def test_misfit( + self, + survey_type, + orientations, + components, + locations, + frequencies, + mesh, + active_cells, + mapping, + sigma_hs, + ): + m0, dmis = self.get_setup_objects( + survey_type, + orientations, + components, + locations, + frequencies, + mesh, + active_cells, + mapping, + sigma_hs, + ) + sim = dmis.simulation + + passed = tests.check_derivative( + lambda m: (sim.dpred(m), lambda mx: sim.Jvec(m, mx)), + m0, + plotIt=False, + num=3, + random_seed=412, + ) + + assert passed + + def test_adjoint( + self, + survey_type, + orientations, + components, + locations, + frequencies, + mesh, + active_cells, + mapping, + sigma_hs, + ): + m0, dmis = self.get_setup_objects( + survey_type, + orientations, + components, + locations, + frequencies, + mesh, + active_cells, + mapping, + sigma_hs, + ) + sim = dmis.simulation + n_data = sim.survey.nD + + f = sim.fields(m0) + tests.assert_isadjoint( + lambda u: sim.Jvec(m0, u, f=f), + lambda v: sim.Jtvec(m0, v, f=f), + m0.shape, + (n_data,), + rtol=ADJ_RTOL, + random_seed=32, + ) diff --git a/tests/em/nsem/survey/test_nsem_data.py b/tests/em/nsem/survey/test_nsem_data.py index 61babc3818..a6ccfe1604 100644 --- a/tests/em/nsem/survey/test_nsem_data.py +++ b/tests/em/nsem/survey/test_nsem_data.py @@ -34,5 +34,5 @@ def test_from_rec_array(self): for src in data_obj.survey.source_list: assert len(src.receiver_list) == 2 # one real, one imaginary component for rx in src.receiver_list: - np.testing.assert_almost_equal(rx.locations, [self.loc]) + np.testing.assert_almost_equal(rx.locations_e, [self.loc]) np.testing.assert_almost_equal(data_obj.dobs, np.array([0.5, 0.0, 0.5, 1.0])) diff --git a/tests/em/nsem/test_nsem_point_deprecations.py b/tests/em/nsem/test_nsem_point_deprecations.py new file mode 100644 index 0000000000..35128d890e --- /dev/null +++ b/tests/em/nsem/test_nsem_point_deprecations.py @@ -0,0 +1,215 @@ +import inspect +import re + +import pytest +import simpeg.electromagnetics.natural_source as nsem +import numpy as np +import discretize +import numpy.testing as npt + + +@pytest.fixture( + params=[ + "same_location", + "diff_location", + ] +) +def impedance_pairs(request): + test_e_locs = np.array([[0.2, 0.1, 0.3], [-0.1, 0.2, -0.3]]) + test_h_locs = np.array([[-0.2, 0.24, 0.1], [0.5, 0.2, -0.2]]) + + rx_point_type = request.param + if rx_point_type == "same": + rx1 = nsem.receivers.PointNaturalSource(test_e_locs) + rx2 = nsem.receivers.Impedance(test_e_locs, orientation="xy") + else: + rx1 = nsem.receivers.PointNaturalSource( + locations_e=test_e_locs, locations_h=test_h_locs + ) + rx2 = nsem.receivers.Impedance( + locations_e=test_e_locs, locations_h=test_h_locs, orientation="xy" + ) + return rx1, rx2 + + +@pytest.fixture() +def tipper_pairs(): + test_e_locs = np.array([[0.2, 0.1, 0.3], [-0.1, 0.2, -0.3]]) + + rx1 = nsem.receivers.Point3DTipper(test_e_locs) + rx2 = nsem.receivers.Tipper(test_e_locs, orientation="zx") + return rx1, rx2 + + +def test_deprecation(): + test_loc = np.array([10.0, 11.0, 12.0]) + with pytest.warns(FutureWarning, match="PointNaturalSource has been deprecated.*"): + nsem.receivers.PointNaturalSource(test_loc) + + with pytest.warns(FutureWarning, match="Using the default for locations.*"): + nsem.receivers.PointNaturalSource() + + with pytest.warns(FutureWarning, match="Point3DTipper has been deprecated.*"): + nsem.receivers.Point3DTipper(test_loc) + + +def test_imp_consistent_attributes(impedance_pairs): + rx1, rx2 = impedance_pairs + + for item_name in dir(rx1): + is_dunder = re.match(r"__\w+__", item_name) is not None + # skip a few things related to the wrapping, and dunder methods + if not (item_name in ["locations", "_uid", "uid", "_old__init__"] or is_dunder): + item1 = getattr(rx1, item_name) + item2 = getattr(rx2, item_name) + if not (inspect.isfunction(item1) or inspect.ismethod(item1)): + if isinstance(item1, np.ndarray): + npt.assert_array_equal(item1, item2) + else: + assert item1 == item2 + + npt.assert_array_equal(rx1.locations, rx2.locations_e) + + +def test_tip_consistent_attributes(tipper_pairs): + rx1, rx2 = tipper_pairs + + for item_name in dir(rx1): + is_dunder = re.match(r"__\w+__", item_name) is not None + # skip a few things related to the wrapping, and dunder methods + if not ( + item_name in ["locations", "locations_e", "_uid", "uid", "_old__init__"] + or is_dunder + ): + item1 = getattr(rx1, item_name) + item2 = getattr(rx2, item_name) + if not (inspect.isfunction(item1) or inspect.ismethod(item1)): + print(item_name, item1, item2) + if isinstance(item1, np.ndarray): + npt.assert_array_equal(item1, item2) + else: + assert item1 == item2 + + npt.assert_array_equal(rx1.locations, rx2.locations_h) + npt.assert_array_equal(rx1.locations, rx2.locations_base) + + +@pytest.mark.parametrize( + "rx_component", ["real", "imag", "apparent_resistivity", "phase", "complex"] +) +def test_imp_consistent_eval(impedance_pairs, rx_component): + rx1, rx2 = impedance_pairs + rx1.component = rx_component + rx2.component = rx_component + # test that the output of the function eval returns the same thing, + # since it was updated... + mesh = discretize.TensorMesh([3, 4, 5], origin="CCC") + + # create a mock simulation + src = nsem.sources.PlanewaveXYPrimary( + [rx1, rx2], frequency=10, sigma_primary=np.ones(mesh.n_cells) + ) + survey = nsem.Survey(src) + sim_temp = nsem.Simulation3DPrimarySecondary(survey=survey, mesh=mesh, sigma=1) + + # Create a mock field, + f = sim_temp.fieldsPair(sim_temp) + test_u = np.linspace(1, 2, 2 * mesh.n_edges) + 1j * np.linspace( + -1, 1, 2 * mesh.n_edges + ) + f[src, sim_temp._solutionType] = test_u.reshape(mesh.n_edges, 2) + + v1 = rx1.eval(src, mesh, f) + v2 = rx2.eval(src, mesh, f) + + npt.assert_equal(v1, v2) + + if rx_component == "real": + # do a quick test here that calling eval on rx1 is the same as calling + # eval on rx2 with a complex component + rx2.component = "complex" + with pytest.warns(FutureWarning, match="Calling with return_complex=True.*"): + v1 = rx1.eval(src, mesh, f, return_complex=True) + v2 = rx2.eval(src, mesh, f) + + # assert it reset + assert rx1.component == "real" + # assert the outputs are the same + npt.assert_equal(v1, v2) + + +@pytest.mark.parametrize("rx_component", ["real", "imag", "complex"]) +def test_tip_consistent_eval(tipper_pairs, rx_component): + rx1, rx2 = tipper_pairs + rx1.component = rx_component + rx2.component = rx_component + # test that the output of the function eval returns the same thing, + # since it was updated... + mesh = discretize.TensorMesh([3, 4, 5], origin="CCC") + + # create a mock simulation + src = nsem.sources.PlanewaveXYPrimary( + [rx1, rx2], frequency=10, sigma_primary=np.ones(mesh.n_cells) + ) + survey = nsem.Survey(src) + sim_temp = nsem.Simulation3DPrimarySecondary(survey=survey, mesh=mesh, sigma=1) + + # Create a mock field, + f = sim_temp.fieldsPair(sim_temp) + test_u = np.linspace(1, 2, 2 * mesh.n_edges) + 1j * np.linspace( + -1, 1, 2 * mesh.n_edges + ) + f[src, sim_temp._solutionType] = test_u.reshape(mesh.n_edges, 2) + + v1 = rx1.eval(src, mesh, f) + v2 = rx2.eval(src, mesh, f) + + npt.assert_equal(v1, v2) + + if rx_component == "real": + # do a quick test here that calling eval on rx1 is the same as calling + # eval on rx2 with a complex component + rx2.component = "complex" + with pytest.warns(FutureWarning, match="Calling with return_complex=True.*"): + v1 = rx1.eval(src, mesh, f, return_complex=True) + v2 = rx2.eval(src, mesh, f) + + # assert it reset + assert rx1.component == "real" + # assert the outputs are the same + npt.assert_equal(v1, v2) + + +def test_imp_location_initialization(): + loc_1 = np.empty((2, 3)) + loc_2 = np.empty((2, 3)) + with pytest.raises(TypeError, match="Cannot pass both locations and .*"): + nsem.receivers.PointNaturalSource(locations=loc_1, locations_h=loc_2) + + with pytest.raises(TypeError, match="Either locations or both locations_e.*"): + nsem.receivers.PointNaturalSource(locations_e=loc_1) + + rx1 = nsem.receivers.PointNaturalSource(locations=[loc_1]) + rx2 = nsem.receivers.Impedance(loc_1) + npt.assert_equal(rx1.locations, rx2.locations_e) + npt.assert_equal(rx1.locations, rx2.locations_h) + + rx1 = nsem.receivers.PointNaturalSource(locations=[loc_1, loc_2]) + rx2 = nsem.receivers.Impedance(loc_1, loc_2) + npt.assert_equal(rx1.locations_e, rx2.locations_e) + npt.assert_equal(rx1.locations_h, rx2.locations_h) + + with pytest.raises(ValueError, match="incorrect size of list, must be length .*"): + nsem.receivers.PointNaturalSource(locations=[loc_1, loc_2, loc_1]) + + +def test_tip_location_initialization(): + loc_1 = np.empty((2, 3)) + loc_2 = np.empty((2, 3)) + with pytest.warns(UserWarning, match="locations_e and locations_h are unused.*"): + nsem.receivers.Point3DTipper(locations=loc_1, locations_e=loc_2) + + with pytest.raises( + ValueError, match="incorrect size of list, must be length of 1 or 2" + ): + nsem.receivers.Point3DTipper([loc_1, loc_1, loc_1]) From 30b3b420ea0eabace1420760dfb9d9e3e2ec4900 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Fri, 1 Nov 2024 22:18:01 -0600 Subject: [PATCH 087/194] Try uploading all the coverage files at once. (#1569) #### Summary Uploads all the codecov files at once. #### PR Checklist * [x] If this is a work in progress PR, set as a Draft PR * [x] Linted my code according to the [style guides](https://docs.simpeg.xyz/latest/content/getting_started/contributing/code-style.html). * [x] Added [tests](https://docs.simpeg.xyz/latest/content/getting_started/contributing/testing.html) to verify changes to the code. * [x] Added necessary documentation to any new functions/classes following the expect [style](https://docs.simpeg.xyz/latest/content/getting_started/contributing/documentation.html). * [x] Marked as ready for review (if this is was a draft PR), and converted to a Pull Request * [x] Tagged ``@simpeg/simpeg-developers`` when ready for review. --- .ci/azure/codecov.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.ci/azure/codecov.yml b/.ci/azure/codecov.yml index 402e90e244..47ee05929b 100644 --- a/.ci/azure/codecov.yml +++ b/.ci/azure/codecov.yml @@ -22,13 +22,15 @@ jobs: ls -la displayName: "Copy coverage files" - - script: | + - bash: | curl -Os https://uploader.codecov.io/latest/linux/codecov chmod +x codecov displayName: "Install codecov cli" - - script: | + - bash: | + cc_file_args=() for report in coverage-*.xml; do - ./codecov --verbose upload-process -f "$report" + cc_file_args+=( " --file " "$report" ) done + ./codecov --verbose upload-process "$cc_file_args" displayName: "Upload coverage to codecov.io" From e21040475ed1537fa7e1baa9b96e8fe255fdb202 Mon Sep 17 00:00:00 2001 From: Thibaut Astic <97514898+thibaut-kobold@users.noreply.github.com> Date: Sat, 2 Nov 2024 15:20:00 -0400 Subject: [PATCH 088/194] Re-implement distance weighting and add a strategy comparison (#1310) Add a new `distance_weighting` function with a new Numba-based implementation that makes it more memory efficient that using Scipy's `spatial_distance.cdist`. Add more tests and a tutorial comparing depth, distance, and sensitivity weighting. Co-authored-by: Santiago Soler Co-authored-by: Joseph Capriotti --- simpeg/potential_fields/base.py | 6 + simpeg/utils/__init__.py | 3 +- simpeg/utils/model_utils.py | 194 +++++- tests/base/test_model_utils.py | 110 +++- ..._gravity_anomaly_irls_compare_weighting.py | 576 ++++++++++++++++++ 5 files changed, 881 insertions(+), 8 deletions(-) create mode 100644 tutorials/03-gravity/plot_inv_1c_gravity_anomaly_irls_compare_weighting.py diff --git a/simpeg/potential_fields/base.py b/simpeg/potential_fields/base.py index 524a4e5415..f997575336 100644 --- a/simpeg/potential_fields/base.py +++ b/simpeg/potential_fields/base.py @@ -544,6 +544,12 @@ def get_dist_wgt(mesh, receiver_locations, actv, R, R0): wr : (n_cell) numpy.ndarray Distance weighting model; 0 for all inactive cells """ + warnings.warn( + "The get_dist_wgt function has been deprecated, please import " + "simpeg.utils.distance_weighting. This will be removed in SimPEG 0.24.0", + FutureWarning, + stacklevel=2, + ) # Find non-zero cells if actv.dtype == "bool": inds = ( diff --git a/simpeg/utils/__init__.py b/simpeg/utils/__init__.py index de6cb066b7..6e0895fd45 100644 --- a/simpeg/utils/__init__.py +++ b/simpeg/utils/__init__.py @@ -76,6 +76,7 @@ :toctree: generated/ depth_weighting + distance_weighting model_builder.add_block model_builder.create_2_layer_model model_builder.create_block_in_wholespace @@ -235,7 +236,7 @@ rotation_matrix_from_normals, rotate_points_from_normals, ) -from .model_utils import depth_weighting +from .model_utils import depth_weighting, distance_weighting from .plot_utils import plot2Ddata, plotLayer, plot_1d_layer_model from .io_utils import download from .pgi_utils import ( diff --git a/simpeg/utils/model_utils.py b/simpeg/utils/model_utils.py index 91df15da71..dc48a0e17c 100644 --- a/simpeg/utils/model_utils.py +++ b/simpeg/utils/model_utils.py @@ -1,8 +1,27 @@ -from .mat_utils import mkvc +import warnings +from typing import Literal, Optional + +import discretize import numpy as np +import scipy.sparse as sp from scipy.interpolate import griddata from scipy.spatial import cKDTree -import scipy.sparse as sp +from scipy.spatial.distance import cdist + +from .mat_utils import mkvc + +try: + import numba + from numba import njit, prange +except ImportError: + numba = None + + # Define dummy njit decorator + def njit(*args, **kwargs): + return lambda f: f + + # Define dummy prange function + prange = range def surface_layer_index(mesh, topo, index=0): @@ -150,3 +169,174 @@ def depth_weighting( wz = wz[active_cells] return wz / np.nanmax(wz) + + +@njit(parallel=True) +def _distance_weighting_numba( + cell_centers: np.ndarray, + reference_locs: np.ndarray, + threshold: float, + exponent: float = 2.0, +) -> np.ndarray: + r""" + distance weighting kernel in numba. + + If numba is not installed, this will work as a regular for loop. + + Parameters + ---------- + cell_centers : np.ndarray + cell centers of the mesh. + reference_locs : (n, ndim) numpy.ndarray + The coordinate of the reference location, usually the receiver locations, + for the distance weighting. + A 2d array, with multiple reference locations, where each row should + contain the coordinates of a single location point in the following + order: _x_, _y_, _z_ (for 3D meshes) or _x_, _z_ (for 2D meshes). + threshold : float + Threshold parameters used in the distance weighting. + exponent : float, optional + Exponent parameter for distance weighting. + The exponent should match the natural decay power of the potential + field. For example, for gravity acceleration, set it to 2; for magnetic + fields, to 3. + + Returns + ------- + (n_active) numpy.ndarray + Normalized distance weights for the mesh at every active cell as + a 1d-array. + """ + n_active_cells = cell_centers.shape[0] + n_reference_locs = len(reference_locs) + + distance_weights = np.zeros(n_active_cells) + for j in prange(n_active_cells): + cell_center = cell_centers[j] + for i in range(n_reference_locs): + reference_loc = reference_locs[i] + distance = np.sqrt(((cell_center - reference_loc) ** 2).sum()) + distance_weights[j] += (distance + threshold) ** (-2 * exponent) + + distance_weights = np.sqrt(distance_weights) + distance_weights /= np.nanmax(distance_weights) + return distance_weights + + +def distance_weighting( + mesh: discretize.base.BaseMesh, + reference_locs: np.ndarray, + active_cells: Optional[np.ndarray] = None, + exponent: Optional[float] = 2.0, + threshold: Optional[float] = None, + engine: Literal["numba", "scipy"] = "numba", + cdist_opts: Optional[dict] = None, +): + r""" + Construct diagonal elements of a distance weighting matrix + + Builds the model weights following the distance weighting strategy, a method + to generate weights based on the distance between mesh cell centers and some + reference location(s). + Use these weights in regularizations to counteract the natural decay of + potential field data with distance. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + Discretized model space. + reference_locs : (n, ndim) numpy.ndarray + The coordinate of the reference location, usually the receiver locations, + for the distance weighting. + A 2d array, with multiple reference locations, where each row should + contain the coordinates of a single location point in the following + order: _x_, _y_, _z_ (for 3D meshes) or _x_, _z_ (for 2D meshes). + active_cells : (mesh.n_cells) numpy.ndarray of bool, optional + Index vector for the active cells on the mesh. + If ``None``, every cell will be assumed to be active. + exponent : float or None, optional + Exponent parameter for distance weighting. + The exponent should match the natural decay power of the potential + field. For example, for gravity acceleration, set it to 2; for magnetic + fields, to 3. + threshold : float or None, optional + Threshold parameters used in the distance weighting. + If ``None``, it will be set to half of the smallest cell width. + engine: str, 'numba' or 'scipy' + Pick between a ``scipy.spatial.distance.cdist`` computation (memory + intensive) or `for` loop implementation, parallelized with numba if + available. Default to ``"numba"``. + cdist_opts: dict, optional + Only valid with ``engine=="scipy"``. Options to pass to + ``scipy.spatial.distance.cdist``. Default to None. + + Returns + ------- + (n_active) numpy.ndarray + Normalized distance weights for the mesh at every active cell as + a 1d-array. + """ + + active_cells = ( + np.ones(mesh.n_cells, dtype=bool) if active_cells is None else active_cells + ) + + # Default threshold value + if threshold is None: + threshold = 0.5 * mesh.h_gridded.min() + + reference_locs = np.atleast_2d(reference_locs) + cell_centers = mesh.cell_centers[active_cells] + + # address 1D case + if mesh.dim == 1: + cell_centers = cell_centers.reshape(-1, 1) + reference_locs = reference_locs.reshape(-1, 1) + + if reference_locs.shape[1] != mesh.dim: + raise ValueError( + f"Invalid 'reference_locs' with shape '{reference_locs.shape}'. " + "The number of columns of the reference_locs array should match " + f"the dimensions of the mesh ({mesh.dim})." + ) + + if engine == "numba" and cdist_opts is not None: + raise TypeError( + "The `cdist_opts` is valid only when engine is 'scipy'." + "The current engine is 'numba'." + ) + + if engine == "numba": + if numba is None: + warnings.warn( + "Numba is not installed. Distance computations will be slower.", + stacklevel=2, + ) + distance_weights = _distance_weighting_numba( + cell_centers, + reference_locs, + exponent=exponent, + threshold=threshold, + ) + + elif engine == "scipy": + warnings.warn( + "``scipy.spatial.distance.cdist`` computations can be memory intensive. " + "Consider switching to `engine='numba'` " + "if you run into memory overflow issues.", + stacklevel=2, + ) + cdist_opts = cdist_opts or dict() + distance = cdist(cell_centers, reference_locs, **cdist_opts) + + distance_weights = (((distance + threshold) ** exponent) ** -2).sum(axis=1) + + distance_weights = distance_weights**0.5 + distance_weights /= np.nanmax(distance_weights) + + else: + raise ValueError( + f"Invalid engine '{engine}'. Engine should be either 'scipy' or 'numba'." + ) + + return distance_weights diff --git a/tests/base/test_model_utils.py b/tests/base/test_model_utils.py index 97802fbf2d..4cc34beaf1 100644 --- a/tests/base/test_model_utils.py +++ b/tests/base/test_model_utils.py @@ -1,13 +1,10 @@ -import pytest import unittest import numpy as np - +import pytest from discretize import TensorMesh -from simpeg import ( - utils, -) +from simpeg import utils class DepthWeightingTest(unittest.TestCase): @@ -77,6 +74,109 @@ def test_depth_weighting_2D(self): np.testing.assert_allclose(wz, wz2) +class TestDistancehWeighting: + def test_distance_weighting_3D(self): + # Mesh + dh = 5.0 + hx = [(dh, 5, -1.3), (dh, 40), (dh, 5, 1.3)] + hy = [(dh, 5, -1.3), (dh, 40), (dh, 5, 1.3)] + hz = [(dh, 15)] + mesh = TensorMesh([hx, hy, hz], "CCN") + + rng = np.random.default_rng(seed=42) + actv = rng.integers(low=0, high=2, size=mesh.n_cells, dtype=bool) + + # Define reference locs at random locations + reference_locs = rng.uniform( + low=mesh.nodes.min(axis=0), high=mesh.nodes.max(axis=0), size=(1000, 3) + ) + + # distance weighting + with pytest.warns(): + wz_scipy = utils.distance_weighting( + mesh, reference_locs, active_cells=actv, exponent=3, engine="scipy" + ) + wz_numba = utils.distance_weighting( + mesh, reference_locs, active_cells=actv, exponent=3, engine="numba" + ) + np.testing.assert_allclose(wz_scipy, wz_numba) + + with pytest.raises(ValueError): + utils.distance_weighting( + mesh, reference_locs, active_cells=actv, exponent=3, engine="test" + ) + + def test_distance_weighting_2D(self): + # Mesh + dh = 5.0 + hx = [(dh, 5, -1.3), (dh, 40), (dh, 5, 1.3)] + hz = [(dh, 15)] + mesh = TensorMesh([hx, hz], "CN") + + rng = np.random.default_rng(seed=42) + actv = rng.integers(low=0, high=2, size=mesh.n_cells, dtype=bool) + + # Define reference locs at random locations + reference_locs = rng.uniform( + low=mesh.nodes.min(axis=0), high=mesh.nodes.max(axis=0), size=(1000, 2) + ) + + # distance weighting + with pytest.warns(): + wz_scipy = utils.distance_weighting( + mesh, reference_locs, active_cells=actv, exponent=3, engine="scipy" + ) + wz_numba = utils.distance_weighting( + mesh, reference_locs, active_cells=actv, exponent=3, engine="numba" + ) + np.testing.assert_allclose(wz_scipy, wz_numba) + + def test_distance_weighting_1D(self): + # Mesh + dh = 5.0 + hx = [(dh, 5, -1.3), (dh, 40), (dh, 5, 1.3)] + mesh = TensorMesh([hx], "C") + + rng = np.random.default_rng(seed=42) + actv = rng.integers(low=0, high=2, size=mesh.n_cells, dtype=bool) + + # Define reference locs at random locations + reference_locs = rng.uniform( + low=mesh.nodes.min(axis=0), high=mesh.nodes.max(axis=0), size=(1000, 1) + ) + + # distance weighting + with pytest.warns(): + wz_scipy = utils.distance_weighting( + mesh, reference_locs, active_cells=actv, exponent=3, engine="scipy" + ) + wz_numba = utils.distance_weighting( + mesh, reference_locs, active_cells=actv, exponent=3, engine="numba" + ) + np.testing.assert_allclose(wz_scipy, wz_numba) + + @pytest.mark.parametrize("ndim", (2, 3)) + def test_invalid_reference_locs(self, ndim): + """ + Test if errors are raised when invalid reference_locs are passed. + """ + hx = [5.0, 10] + h = [hx] * ndim + origin = "CCN" if ndim == 3 else "CC" + reference_locs = [1.0, 2.0] if ndim == 3 else [1.0] + mesh = TensorMesh(h, origin) + with pytest.raises(ValueError): + utils.distance_weighting(mesh, reference_locs) + + def test_numba_and_cdist_opts_error(self): + """Test error when passing numba and cdist_opts.""" + hx = [5.0, 10] + mesh = TensorMesh([hx, hx, hx]) + msg = "The `cdist_opts` is valid only when engine is 'scipy'." + with pytest.raises(TypeError, match=msg): + utils.distance_weighting(mesh, [1.0, 2.0, 3.0], cdist_opts={"foo": "bar"}) + + @pytest.fixture def mesh(): """Sample mesh.""" diff --git a/tutorials/03-gravity/plot_inv_1c_gravity_anomaly_irls_compare_weighting.py b/tutorials/03-gravity/plot_inv_1c_gravity_anomaly_irls_compare_weighting.py new file mode 100644 index 0000000000..2fc2d9210d --- /dev/null +++ b/tutorials/03-gravity/plot_inv_1c_gravity_anomaly_irls_compare_weighting.py @@ -0,0 +1,576 @@ +""" +Compare weighting strategy with Inversion of surface Gravity Anomaly Data +========================================================================= + +Here we invert gravity anomaly data to recover a density contrast model. We formulate the inverse problem as an iteratively +re-weighted least-squares (IRLS) optimization problem. For this tutorial, we +focus on the following: + + - Setting regularization weights + - Defining the survey from xyz formatted data + - Generating a mesh based on survey geometry + - Including surface topography + - Defining the inverse problem (data misfit, regularization, optimization) + - Specifying directives for the inversion + - Setting sparse and blocky norms + - Plotting the recovered model and data misfit + +Although we consider gravity anomaly data in this tutorial, the same approach +can be used to invert gradiometry and other types of geophysical data. +""" + +######################################################################### +# Import modules +# -------------- +# + +import os +import tarfile + +import matplotlib as mpl +import matplotlib.pyplot as plt +import numpy as np +from discretize import TensorMesh +from discretize.utils import active_from_xyz + +from simpeg import ( + data, + data_misfit, + directives, + inverse_problem, + inversion, + maps, + optimization, + regularization, + utils, +) +from simpeg.potential_fields import gravity +from simpeg.utils import model_builder, plot2Ddata + +# sphinx_gallery_thumbnail_number = 3 + +############################################# +# Define File Names +# ----------------- +# +# File paths for assets we are loading. To set up the inversion, we require +# topography and field observations. The true model defined on the whole mesh +# is loaded to compare with the inversion result. These files are stored as a +# tar-file on our google cloud bucket: +# "https://storage.googleapis.com/simpeg/doc-assets/gravity.tar.gz" +# + +# storage bucket where we have the data +data_source = "https://storage.googleapis.com/simpeg/doc-assets/gravity.tar.gz" + +# download the data +downloaded_data = utils.download(data_source, overwrite=True) + +# unzip the tarfile +tar = tarfile.open(downloaded_data, "r") +tar.extractall() +tar.close() + +# path to the directory containing our data +dir_path = downloaded_data.split(".")[0] + os.path.sep + +# files to work with +topo_filename = dir_path + "gravity_topo.txt" +data_filename = dir_path + "gravity_data.obs" + + +############################################# +# Load Data and Plot +# ------------------ +# +# Here we load and plot synthetic gravity anomaly data. Topography is generally +# defined as an (N, 3) array. Gravity data is generally defined with 4 columns: +# x, y, z and data. +# + +# Load topography +xyz_topo = np.loadtxt(str(topo_filename)) + +# Load field data +dobs = np.loadtxt(str(data_filename)) + +# Define receiver locations and observed data +receiver_locations = dobs[:, 0:3] +dobs = dobs[:, -1] + +# Plot +mpl.rcParams.update({"font.size": 12}) +fig = plt.figure(figsize=(7, 5)) + +ax1 = fig.add_axes([0.1, 0.1, 0.73, 0.85]) +plot2Ddata( + receiver_locations, + dobs, + ax=ax1, + contourOpts={"cmap": "bwr"}, + shade=True, + nx=20, + ny=20, + dataloc=True, +) +ax1.set_title("Gravity Anomaly") +ax1.set_xlabel("x (m)") +ax1.set_ylabel("y (m)") + +ax2 = fig.add_axes([0.8, 0.1, 0.03, 0.85]) +norm = mpl.colors.Normalize(vmin=-np.max(np.abs(dobs)), vmax=np.max(np.abs(dobs))) +cbar = mpl.colorbar.ColorbarBase( + ax2, norm=norm, orientation="vertical", cmap=mpl.cm.bwr, format="%.1e" +) +cbar.set_label("$mGal$", rotation=270, labelpad=15, size=12) + +plt.show() + +############################################# +# Assign Uncertainties +# -------------------- +# +# Inversion with simpeg requires that we define the standard deviation of our data. +# This represents our estimate of the noise in our data. For a gravity inversion, +# a constant floor value is generally applied to all data. For this tutorial, +# the standard deviation on each datum will be 1% of the maximum observed +# gravity anomaly value. +# + +maximum_anomaly = np.max(np.abs(dobs)) + +uncertainties = 0.01 * maximum_anomaly * np.ones(np.shape(dobs)) + +############################################# +# Defining the Survey +# ------------------- +# +# Here, we define the survey that will be used for this tutorial. Gravity +# surveys are simple to create. The user only needs an (N, 3) array to define +# the xyz locations of the observation locations. From this, the user can +# define the receivers and the source field. +# + +# Define the receivers. The data consists of vertical gravity anomaly measurements. +# The set of receivers must be defined as a list. +receiver_list = gravity.receivers.Point(receiver_locations, components="gz") + +receiver_list = [receiver_list] + +# Define the source field +source_field = gravity.sources.SourceField(receiver_list=receiver_list) + +# Define the survey +survey = gravity.survey.Survey(source_field) + +############################################# +# Defining the Data +# ----------------- +# +# Here is where we define the data that is inverted. The data is defined by +# the survey, the observation values and the standard deviation. +# + +data_object = data.Data(survey, dobs=dobs, standard_deviation=uncertainties) + + +############################################# +# Defining a Tensor Mesh +# ---------------------- +# +# Here, we create the tensor mesh that will be used to invert gravity anomaly +# data. If desired, we could define an OcTree mesh. +# + +dh = 5.0 +hx = [(dh, 5, -1.3), (dh, 40), (dh, 5, 1.3)] +hy = [(dh, 5, -1.3), (dh, 40), (dh, 5, 1.3)] +hz = [(dh, 5, -1.3), (dh, 15)] +mesh = TensorMesh([hx, hy, hz], "CCN") + +######################################################## +# Starting/Reference Model and Mapping on Tensor Mesh +# --------------------------------------------------- +# +# Here, we create starting and/or reference models for the inversion as +# well as the mapping from the model space to the active cells. Starting and +# reference models can be a constant background value or contain a-priori +# structures. +# + +# Find the indices of the active cells in forward model (ones below surface) +ind_active = active_from_xyz(mesh, xyz_topo) + +# Define mapping from model to active cells +nC = int(ind_active.sum()) +model_map = maps.IdentityMap(nP=nC) # model consists of a value for each active cell + +# Define and plot starting model +starting_model = np.zeros(nC) + + +############################################## +# Define the Physics and data misfit +# ---------------------------------- +# +# Here, we define the physics of the gravity problem by using the simulation +# class. +# + +simulation = gravity.simulation.Simulation3DIntegral( + survey=survey, mesh=mesh, rhoMap=model_map, ind_active=ind_active +) + +# Define the data misfit. Here the data misfit is the L2 norm of the weighted +# residual between the observed data and the data predicted for a given model. +# Within the data misfit, the residual between predicted and observed data are +# normalized by the data's standard deviation. +dmis = data_misfit.L2DataMisfit(data=data_object, simulation=simulation) + + +####################################################################### +# Running the Depth Weighted inversion +# ------------------------------------ +# +# Here we define the directives, weights, regularization, and optimization +# for a depth-weighted inversion +# + +# inversion directives +# Defining a starting value for the trade-off parameter (beta) between the data +# misfit and the regularization. +starting_beta = directives.BetaEstimate_ByEig(beta0_ratio=1e0) + +# Defines the directives for the IRLS regularization. This includes setting +# the cooling schedule for the trade-off parameter. +update_IRLS = directives.Update_IRLS( + f_min_change=1e-4, + max_irls_iterations=30, + coolEpsFact=1.5, + beta_tol=1e-2, +) + +# Options for outputting recovered models and predicted data for each beta. +save_iteration = directives.SaveOutputEveryIteration(save_txt=False) + +# Updating the preconditionner if it is model dependent. +update_jacobi = directives.UpdatePreconditioner() + +# The directives are defined as a list +directives_list = [ + update_IRLS, + starting_beta, + save_iteration, + update_jacobi, +] + +# Define the regularization (model objective function) with depth weighting. +reg_dpth = regularization.Sparse(mesh, active_cells=ind_active, mapping=model_map) +reg_dpth.norms = [0, 2, 2, 2] +depth_weights = utils.depth_weighting( + mesh, receiver_locations, active_cells=ind_active, exponent=2 +) +reg_dpth.set_weights(depth_weights=depth_weights) + +# Define how the optimization problem is solved. Here we will use a projected +# Gauss-Newton approach that employs the conjugate gradient solver. +opt = optimization.ProjectedGNCG( + maxIter=100, lower=-1.0, upper=1.0, maxIterLS=20, maxIterCG=10, tolCG=1e-3 +) + +# Here we define the inverse problem that is to be solved +inv_prob = inverse_problem.BaseInvProblem(dmis, reg_dpth, opt) + +# Here we combine the inverse problem and the set of directives +inv = inversion.BaseInversion(inv_prob, directives_list) + +# Run inversion +recovered_model_dpth = inv.run(starting_model) + +####################################################################### +# Running the Distance Weighted inversion +# --------------------------------------- +# +# Here we define the directives, weights, regularization, and optimization +# for a distance-weighted inversion +# + +# inversion directives +# Defining a starting value for the trade-off parameter (beta) between the data +# misfit and the regularization. +starting_beta = directives.BetaEstimate_ByEig(beta0_ratio=1e0) + +# Defines the directives for the IRLS regularization. This includes setting +# the cooling schedule for the trade-off parameter. +update_IRLS = directives.Update_IRLS( + f_min_change=1e-4, + max_irls_iterations=30, + coolEpsFact=1.5, + beta_tol=1e-2, +) + +# Options for outputting recovered models and predicted data for each beta. +save_iteration = directives.SaveOutputEveryIteration(save_txt=False) + +# Updating the preconditionner if it is model dependent. +update_jacobi = directives.UpdatePreconditioner() + +# The directives are defined as a list +directives_list = [ + update_IRLS, + starting_beta, + save_iteration, + update_jacobi, +] + +# Define the regularization (model objective function) with distance weighting. +reg_dist = regularization.Sparse(mesh, active_cells=ind_active, mapping=model_map) +reg_dist.norms = [0, 2, 2, 2] +distance_weights = utils.distance_weighting( + mesh, receiver_locations, active_cells=ind_active, exponent=2 +) +reg_dist.set_weights(distance_weights=distance_weights) + +# Define how the optimization problem is solved. Here we will use a projected +# Gauss-Newton approach that employs the conjugate gradient solver. +opt = optimization.ProjectedGNCG( + maxIter=100, lower=-1.0, upper=1.0, maxIterLS=20, maxIterCG=10, tolCG=1e-3 +) + +# Here we define the inverse problem that is to be solved +inv_prob = inverse_problem.BaseInvProblem(dmis, reg_dist, opt) + +# Here we combine the inverse problem and the set of directives +inv = inversion.BaseInversion(inv_prob, directives_list) + +# Run inversion +recovered_model_dist = inv.run(starting_model) + +####################################################################### +# Running the Distance Weighted inversion +# --------------------------------------- +# +# Here we define the directives, weights, regularization, and optimization +# for a sensitivity weighted inversion +# + +# inversion directives +# Defining a starting value for the trade-off parameter (beta) between the data +# misfit and the regularization. +starting_beta = directives.BetaEstimate_ByEig(beta0_ratio=1e0) + +# Defines the directives for the IRLS regularization. This includes setting +# the cooling schedule for the trade-off parameter. +update_IRLS = directives.Update_IRLS( + f_min_change=1e-4, + max_irls_iterations=30, + coolEpsFact=1.5, + beta_tol=1e-2, +) + +# Options for outputting recovered models and predicted data for each beta. +save_iteration = directives.SaveOutputEveryIteration(save_txt=False) + +# Updating the preconditionner if it is model dependent. +update_jacobi = directives.UpdatePreconditioner() + +# Add sensitivity weights +sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) + +# The directives are defined as a list +directives_list = [ + update_IRLS, + sensitivity_weights, + starting_beta, + save_iteration, + update_jacobi, +] + +# Define the regularization (model objective function) for sensitivity weighting. +reg_sensw = regularization.Sparse(mesh, active_cells=ind_active, mapping=model_map) +reg_sensw.norms = [0, 2, 2, 2] + +# Define how the optimization problem is solved. Here we will use a projected +# Gauss-Newton approach that employs the conjugate gradient solver. +opt = optimization.ProjectedGNCG( + maxIter=100, lower=-1.0, upper=1.0, maxIterLS=20, maxIterCG=10, tolCG=1e-3 +) + +# Here we define the inverse problem that is to be solved +inv_prob = inverse_problem.BaseInvProblem(dmis, reg_sensw, opt) + +# Here we combine the inverse problem and the set of directives +inv = inversion.BaseInversion(inv_prob, directives_list) + +# Run inversion +recovered_model_sensw = inv.run(starting_model) + +############################################################ +# Recreate True Model +# ------------------- +# + +# Define density contrast values for each unit in g/cc +background_density = 0.0 +block_density = -0.2 +sphere_density = 0.2 + +# Define model. Models in simpeg are vector arrays. +true_model = background_density * np.ones(nC) + +# You could find the indicies of specific cells within the model and change their +# value to add structures. +ind_block = ( + (mesh.gridCC[ind_active, 0] > -50.0) + & (mesh.gridCC[ind_active, 0] < -20.0) + & (mesh.gridCC[ind_active, 1] > -15.0) + & (mesh.gridCC[ind_active, 1] < 15.0) + & (mesh.gridCC[ind_active, 2] > -50.0) + & (mesh.gridCC[ind_active, 2] < -30.0) +) +true_model[ind_block] = block_density + +# You can also use simpeg utilities to add structures to the model more concisely +ind_sphere = model_builder.get_indices_sphere( + np.r_[35.0, 0.0, -40.0], 15.0, mesh.gridCC +) +ind_sphere = ind_sphere[ind_active] +true_model[ind_sphere] = sphere_density + + +############################################################ +# Plotting True Model and Recovered Models +# ---------------------------------------- +# + +# Plot Models +fig, ax = plt.subplots(2, 2, figsize=(20, 10), sharex=True, sharey=True) +ax = ax.flatten() +plotting_map = maps.InjectActiveCells(mesh, ind_active, np.nan) +cmap = "coolwarm" +slice_y_loc = 0.0 + +mm = mesh.plot_slice( + plotting_map * true_model, + normal="Y", + ax=ax[0], + grid=False, + slice_loc=slice_y_loc, + pcolor_opts={"cmap": cmap, "norm": norm}, +) +ax[0].set_title(f"True model slice at y = {slice_y_loc} m") +plt.colorbar(mm[0], label="$g/cm^3$", ax=ax[0]) + +# plot depth weighting result +vmax = np.abs(recovered_model_dpth).max() +norm = mpl.colors.TwoSlopeNorm(vcenter=0, vmin=-vmax, vmax=vmax) +mm = mesh.plot_slice( + plotting_map * recovered_model_dpth, + normal="Y", + ax=ax[1], + grid=False, + slice_loc=slice_y_loc, + pcolor_opts={"cmap": cmap, "norm": norm}, +) +ax[1].set_title(f"Depth weighting Model slice at y = {slice_y_loc} m") +plt.colorbar(mm[0], label="$g/cm^3$", ax=ax[1]) + +# plot distance weighting result +vmax = np.abs(recovered_model_dist).max() +norm = mpl.colors.TwoSlopeNorm(vcenter=0, vmin=-vmax, vmax=vmax) +mm = mesh.plot_slice( + plotting_map * recovered_model_dist, + normal="Y", + ax=ax[2], + grid=False, + slice_loc=slice_y_loc, + pcolor_opts={"cmap": cmap, "norm": norm}, +) +ax[2].set_title(f"Distance weighting Model slice at y = {slice_y_loc} m") +plt.colorbar(mm[0], label="$g/cm^3$", ax=ax[2]) + +# plot sensitivity weighting result +vmax = np.abs(recovered_model_sensw).max() +norm = mpl.colors.TwoSlopeNorm(vcenter=0, vmin=-vmax, vmax=vmax) +mm = mesh.plot_slice( + plotting_map * recovered_model_sensw, + normal="Y", + ax=ax[3], + grid=False, + slice_loc=slice_y_loc, + pcolor_opts={"cmap": cmap, "norm": norm}, +) +ax[3].set_title(f"Sensitivity weighting Model slice at y = {slice_y_loc} m") +plt.colorbar(mm[0], label="$g/cm^3$", ax=ax[3]) + +# shared plotting +plotting_map = maps.InjectActiveCells(mesh, ind_active, 0.0) +slice_y_ind = ( + mesh.cell_centers[:, 1] == np.abs(mesh.cell_centers[:, 1] - slice_y_loc).min() +) +for axx in ax: + utils.plot2Ddata( + mesh.cell_centers[slice_y_ind][:, [0, 2]], + (plotting_map * true_model)[slice_y_ind], + contourOpts={"alpha": 0}, + level=True, + ncontour=2, + levelOpts={"colors": "grey", "linewidths": 2, "linestyles": "--"}, + method="nearest", + ax=axx, + ) + axx.set_aspect(1) + +plt.tight_layout() + +############################################################ +# Visualize weights +# ----------------- +# +# Plot Weights +fig, ax = plt.subplots(1, 3, figsize=(20, 4), sharex=True, sharey=True) +plotting_map = maps.InjectActiveCells(mesh, ind_active, np.nan) +cmap = "magma" +slice_y_loc = 0.0 + +# plot depth weights +mm = mesh.plot_slice( + plotting_map * np.log10(depth_weights), + normal="Y", + ax=ax[0], + grid=False, + slice_loc=slice_y_loc, + pcolor_opts={"cmap": cmap}, +) +ax[0].set_title(f"log10(depth weights) slice at y = {slice_y_loc} m") +plt.colorbar(mm[0], label="log10(depth weights)", ax=ax[0]) + +# plot distance weights +mm = mesh.plot_slice( + plotting_map * np.log10(distance_weights), + normal="Y", + ax=ax[1], + grid=False, + slice_loc=slice_y_loc, + pcolor_opts={"cmap": cmap}, +) +ax[1].set_title(f"log10(distance weights) slice at y = {slice_y_loc} m") +plt.colorbar(mm[0], label="log10(distance weights)", ax=ax[1]) + +# plot sensitivity weights +mm = mesh.plot_slice( + plotting_map * np.log10(reg_sensw.objfcts[0].get_weights(key="sensitivity")), + normal="Y", + ax=ax[2], + grid=False, + slice_loc=slice_y_loc, + pcolor_opts={"cmap": cmap}, +) +ax[2].set_title(f"log10(sensitivity weights) slice at y = {slice_y_loc} m") +plt.colorbar(mm[0], label="log10(sensitivity weights)", ax=ax[2]) + +# shared plotting +for axx in ax: + axx.set_aspect(1) + +plt.tight_layout() From a86146f0d9d70fbece9d2cbd769442548254c0af Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Sat, 2 Nov 2024 22:43:09 -0600 Subject: [PATCH 089/194] Remove empymod dependency (#1571) #### Summary `empymod` is a pretty heavy dependency for the functionality we are using from it. This removes the dependency and instead uses `libdlf` to get the filters, and a streamlined `get_splined_dlf_points` for our internal use. #### PR Checklist * [x] If this is a work in progress PR, set as a Draft PR * [x] Linted my code according to the [style guides](https://docs.simpeg.xyz/latest/content/getting_started/contributing/code-style.html). * [x] Added [tests](https://docs.simpeg.xyz/latest/content/getting_started/contributing/testing.html) to verify changes to the code. * [x] Added necessary documentation to any new functions/classes following the expect [style](https://docs.simpeg.xyz/latest/content/getting_started/contributing/documentation.html). * [x] Marked as ready for review (if this is was a draft PR), and converted to a Pull Request * [x] Tagged ``@simpeg/simpeg-developers`` when ready for review. #### Additional information I say `empymod` is a heavy dependency because it in turn relies on `numba`. --- .ci/environment_test.yml | 3 +- environment.yml | 3 +- pyproject.toml | 3 +- simpeg/electromagnetics/base_1d.py | 39 +++++++++++------- .../static/resistivity/simulation_1d.py | 38 ++++++++---------- .../time_domain/simulation_1d.py | 40 ++++++++++++------- simpeg/electromagnetics/utils/em1d_utils.py | 36 +++++++++++++++++ simpeg/utils/code_utils.py | 2 +- tests/em/em1d/test_EM1D_TD_general_fwd.py | 9 +++++ tests/em/em1d/test_utils.py | 37 +++++++++++++++++ tests/utils/test_report.py | 2 +- 11 files changed, 155 insertions(+), 57 deletions(-) create mode 100644 tests/em/em1d/test_utils.py diff --git a/.ci/environment_test.yml b/.ci/environment_test.yml index 3663e03a66..e1ca51a0a4 100644 --- a/.ci/environment_test.yml +++ b/.ci/environment_test.yml @@ -8,7 +8,7 @@ dependencies: - matplotlib-base - discretize>=0.11 - geoana>=0.7 - - empymod>=2.0.0 + - libdlf # optional dependencies - dask @@ -28,6 +28,7 @@ dependencies: - nbsphinx - numpydoc - pillow + - empymod>=2.0.0 - sympy - memory_profiler - python-kaleido diff --git a/environment.yml b/environment.yml index 44a3128291..5c017b7e16 100644 --- a/environment.yml +++ b/environment.yml @@ -10,7 +10,7 @@ dependencies: - matplotlib-base - discretize>=0.11 - geoana>=0.7 - - empymod>=2.0.0 + - libdlf # solver # uncomment the next line if you are on an intel platform @@ -33,6 +33,7 @@ dependencies: - sphinx-gallery>=0.1.13 - sphinxcontrib-apidoc - pydata-sphinx-theme + - empymod>=2.0.0 - nbsphinx - numpydoc - pillow diff --git a/pyproject.toml b/pyproject.toml index 024a314997..9076ef11fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ dependencies = [ "matplotlib", "discretize>=0.11", "geoana>=0.7", - "empymod>=2.0.0", + "libdlf", ] classifiers = [ "Development Status :: 4 - Beta", @@ -74,6 +74,7 @@ docs = [ "sphinxcontrib-apidoc", "pydata-sphinx-theme", "nbsphinx", + "empymod>=2.0.0", "numpydoc", "pillow", "sympy", diff --git a/simpeg/electromagnetics/base_1d.py b/simpeg/electromagnetics/base_1d.py index f1c85f44e1..cf73c0ce8e 100644 --- a/simpeg/electromagnetics/base_1d.py +++ b/simpeg/electromagnetics/base_1d.py @@ -1,8 +1,9 @@ +from collections import namedtuple + from scipy.constants import mu_0 import numpy as np from scipy import sparse as sp from scipy.special import roots_legendre -from empymod.transform import get_dlf_points from ..simulation import BaseSimulation @@ -17,10 +18,16 @@ validate_integer, ) from .. import props -from empymod.utils import check_hankel +import libdlf __all__ = ["BaseEM1DSimulation"] +HANKEL_FILTERS = {} +for filter_name in libdlf.hankel.__all__: + hankel_filter = getattr(libdlf.hankel, filter_name) + if "j0" in hankel_filter.values and "j1" in hankel_filter.values: + HANKEL_FILTERS[filter_name] = hankel_filter + ############################################################################### # # # Base EM1D Simulation # @@ -137,30 +144,33 @@ def __init__( self.hankel_filter = hankel_filter self.fix_Jmatrix = fix_Jmatrix - # Check input arguments. If self.hankel_filter is not a valid filter, - # it will set it to the default (key_201_2009). - ht, htarg = check_hankel( - "dlf", {"dlf": self.hankel_filter, "pts_per_dec": 0}, 1 - ) - - self._fhtfilt = htarg["dlf"] # Store filter - # self.hankel_pts_per_dec = htarg["pts_per_dec"] # Store pts_per_dec if self.verbose: print(">> Use " + self.hankel_filter + " filter for Hankel Transform") @property def hankel_filter(self): - """The hankely filter to use. + """The hankel filter used. Returns ------- str + + See Also + -------- + libdlf.hankel + The package housing the filter values. """ return self._hankel_filter @hankel_filter.setter def hankel_filter(self, value): - self._hankel_filter = validate_string("hankel_filter", value) + self._hankel_filter = validate_string( + "hankel_filter", value, list(HANKEL_FILTERS.keys()) + ) + base, j0, j1 = HANKEL_FILTERS[self._hankel_filter]() + hank = namedtuple("HankelFilter", "base j0 j1") + self._fhtfilt = hank(base, j0, j1) + self._coefficients_set = False _hankel_pts_per_dec = 0 # Default: Standard DLF @@ -430,9 +440,8 @@ def _compute_hankel_coefficients(self): offsets = src.radius * np.ones(rx.locations.shape[0]) # computations for hankel transform... - lambd, _ = get_dlf_points( - self._fhtfilt, offsets, self._hankel_pts_per_dec - ) + lambd = self._fhtfilt.base / offsets[:, None] + # calculate the source-rx coefficients for the hankel transform C0 = 0.0 C1 = 0.0 diff --git a/simpeg/electromagnetics/static/resistivity/simulation_1d.py b/simpeg/electromagnetics/static/resistivity/simulation_1d.py index 7f913ea3e2..b57ea3591e 100644 --- a/simpeg/electromagnetics/static/resistivity/simulation_1d.py +++ b/simpeg/electromagnetics/static/resistivity/simulation_1d.py @@ -1,28 +1,22 @@ +from collections import namedtuple + +import libdlf import numpy as np +from ...utils.em1d_utils import get_splined_dlf_points from ....simulation import BaseSimulation from .... import props from .survey import Survey -from empymod.transform import get_dlf_points -from empymod import filters from ....utils import validate_type, validate_string from scipy.interpolate import InterpolatedUnivariateSpline as iuSpline - -HANKEL_FILTERS = [ - "kong_61_2007", - "kong_241_2007", - "key_101_2009", - "key_201_2009", - "key_401_2009", - "anderson_801_1982", - "key_51_2012", - "key_101_2012", - "key_201_2012", - "wer_201_2018", -] +HANKEL_FILTERS = {} +for filter_name in libdlf.hankel.__all__: + hankel_filter = getattr(libdlf.hankel, filter_name) + if "j0" in hankel_filter.values: + HANKEL_FILTERS[filter_name] = hankel_filter def _phi_tilde(rho, thicknesses, lambdas): @@ -200,11 +194,12 @@ def hankel_filter(self): @hankel_filter.setter def hankel_filter(self, value): self._hankel_filter = validate_string( - "hankel_filter", - value, - HANKEL_FILTERS, + "hankel_filter", value, list(HANKEL_FILTERS.keys()) ) - self._fhtfilt = getattr(filters, self._hankel_filter)() + filt = HANKEL_FILTERS[self._hankel_filter]() + hank = namedtuple("HankelFilter", "base j0") + self._fhtfilt = hank(filt[0], filt[1]) + self._coefficients_set = False @property def fix_Jmatrix(self): @@ -241,9 +236,8 @@ def _compute_hankel_coefficients(self): r_max = max(off.max(), r_max) self.survey.set_geometric_factor() - lambdas, r_spline_points = get_dlf_points( - self._fhtfilt, np.r_[r_min, r_max], -1 - ) + lambdas, r_spline_points = get_splined_dlf_points(self._fhtfilt, r_min, r_max) + lambdas = lambdas.reshape(-1) n_lambda = len(lambdas) n_r = len(r_spline_points) diff --git a/simpeg/electromagnetics/time_domain/simulation_1d.py b/simpeg/electromagnetics/time_domain/simulation_1d.py index 8611c67135..e2f5b8a6bb 100644 --- a/simpeg/electromagnetics/time_domain/simulation_1d.py +++ b/simpeg/electromagnetics/time_domain/simulation_1d.py @@ -1,3 +1,5 @@ +from collections import namedtuple + from ..base_1d import BaseEM1DSimulation from .sources import StepOffWaveform from .receivers import ( @@ -12,13 +14,19 @@ from scipy.interpolate import InterpolatedUnivariateSpline as iuSpline from scipy.special import roots_legendre -from empymod import filters -from empymod.transform import get_dlf_points +import libdlf from geoana.kernels.tranverse_electric_reflections import rTE_forward, rTE_gradient +from ..utils.em1d_utils import get_splined_dlf_points from ...utils import validate_type, validate_string +COS_FILTERS = {} +for filter_name in libdlf.fourier.__all__: + fourier_filter = getattr(libdlf.fourier, filter_name) + if "cos" in fourier_filter.values: + COS_FILTERS[filter_name] = fourier_filter + class Simulation1DLayered(BaseEM1DSimulation): """ @@ -26,7 +34,7 @@ class Simulation1DLayered(BaseEM1DSimulation): for a single sounding. """ - def __init__(self, survey=None, time_filter="key_81_CosSin_2009", **kwargs): + def __init__(self, survey=None, time_filter="key_81_2009", **kwargs): super().__init__(survey=survey, **kwargs) self._coefficients_set = False self.time_filter = time_filter @@ -54,18 +62,20 @@ def time_filter(self): @time_filter.setter def time_filter(self, value): + # translate old accepted keys to the names in libdlf for compatibility. + if value in [ + "key_81_CosSin_2009", + "key_201_CosSin_2012", + "key_601_CosSin_2009", + ]: + value = value.replace("CosSin_", "") self._time_filter = validate_string( - "time_filter", - value, - ["key_81_CosSin_2009", "key_201_CosSin_2012", "key_601_CosSin_2009"], + "time_filter", value, list(COS_FILTERS.keys()) ) - - if self._time_filter == "key_81_CosSin_2009": - self._fftfilt = filters.key_81_CosSin_2009() - elif self._time_filter == "key_201_CosSin_2012": - self._fftfilt = filters.key_201_CosSin_2012() - elif self._time_filter == "key_601_CosSin_2009": - self._fftfilt = filters.key_601_CosSin_2009() + filt = COS_FILTERS[self._time_filter]() + cos_filt = namedtuple("CosineFilter", "base cos") + self._fftfilt = cos_filt(filt[0], filt[-1]) + self._coefficients_set = False def get_coefficients(self): if self._coefficients_set is False: @@ -123,8 +133,8 @@ def _compute_coefficients(self): f"Unsupported source waveform object of {src.waveform}" ) - omegas, t_spline_points = get_dlf_points(self._fftfilt, np.r_[t_min, t_max], -1) - omegas = omegas.reshape(-1) + omegas, t_spline_points = get_splined_dlf_points(self._fftfilt, t_min, t_max) + n_omega = len(omegas) n_t = len(t_spline_points) diff --git a/simpeg/electromagnetics/utils/em1d_utils.py b/simpeg/electromagnetics/utils/em1d_utils.py index 91b0ee3f61..c551cb311d 100644 --- a/simpeg/electromagnetics/utils/em1d_utils.py +++ b/simpeg/electromagnetics/utils/em1d_utils.py @@ -219,3 +219,39 @@ def LogUniform(f, chi_inf=0.05, del_chi=0.05, tau1=1e-5, tau2=1e-2): return chi_inf + del_chi * ( 1 - np.log((1 + 1j * w * tau2) / (1 + 1j * w * tau1)) / np.log(tau2 / tau1) ) + + +def get_splined_dlf_points(filt, v_min, v_max): + """ + + Parameters + ---------- + filt : namedtuple of numpy.ndarray + The filter parameters loaded from libdlf, in a named tuple containing a `.base` property. + v_min, v_max : float + The minimum and maximum points needed + + Returns + ------- + lamb, spline_points : numpy.ndarray + The points to evaluate at in the filter space, and the + corresponding spline points in the transformed space. + """ + + # the below is adapted from empymod.transform.get_dlf_points + outmax = filt.base[-1] / v_min + outmin = filt.base[0] / v_max + + factor = np.around([filt.base[1] / filt.base[0]], 15) + + pts_per_dec = np.squeeze(1 / np.log(factor)) + + nout = int(np.ceil(np.log(outmax / outmin) * pts_per_dec) + 1) + if nout - filt.base.size < 3: + nout = filt.base.size + 3 + out = np.exp( + np.arange(np.log(outmin), np.log(outmin) + nout / pts_per_dec, 1 / pts_per_dec) + ) + + spline_points = v_max * np.exp(-np.arange(nout - filt.base.size + 1) / pts_per_dec) + return out, spline_points diff --git a/simpeg/utils/code_utils.py b/simpeg/utils/code_utils.py index 1162150906..5a08cc1039 100644 --- a/simpeg/utils/code_utils.py +++ b/simpeg/utils/code_utils.py @@ -478,8 +478,8 @@ def __init__(self, add_pckg=None, ncol=3, text_width=80, sort=False): "numpy", "scipy", "matplotlib", - "empymod", "geoana", + "libdlf", ] # Optional packages. diff --git a/tests/em/em1d/test_EM1D_TD_general_fwd.py b/tests/em/em1d/test_EM1D_TD_general_fwd.py index 1d4daf5c6d..962b4e8b45 100644 --- a/tests/em/em1d/test_EM1D_TD_general_fwd.py +++ b/tests/em/em1d/test_EM1D_TD_general_fwd.py @@ -276,5 +276,14 @@ def test_em1dtd_mag_dipole_bzdt(self): np.testing.assert_allclose(self.bzdt, empymod_solution, rtol=1e-2) +def test_backwards_compatible_filter_key(): + + srv = tdem.Survey([]) + sim = tdem.Simulation1DLayered(survey=srv) + sim.time_filter = "key_81_CosSin_2009" + + assert sim.time_filter == "key_81_2009" + + if __name__ == "__main__": unittest.main() diff --git a/tests/em/em1d/test_utils.py b/tests/em/em1d/test_utils.py new file mode 100644 index 0000000000..bbeb4b2174 --- /dev/null +++ b/tests/em/em1d/test_utils.py @@ -0,0 +1,37 @@ +from collections import namedtuple + +import pytest +import libdlf +import numpy as np +import numpy.testing as npt +from empymod.transform import get_dlf_points + +from simpeg.electromagnetics.utils.em1d_utils import get_splined_dlf_points + +FILTERS = [f"hankel.{filt}" for filt in libdlf.hankel.__all__] + [ + f"fourier.{filt}" for filt in libdlf.fourier.__all__ +] + + +@pytest.mark.parametrize("filt", FILTERS) +@pytest.mark.parametrize("n_points", [1, 5, 10]) +def test_splined_dlf(filt, n_points): + f_type, f_name = filt.split(".") + f_module = getattr(libdlf, f_type) + filt = getattr(f_module, f_name) + base, *vals = filt() + if len(vals) == 2: + v0, v1 = vals + else: + v0 = v1 = vals[0] + factor = np.around([base[1] / base[0]], 15) + filt_type = namedtuple("Filter", "base v0 v1 factor") + filt = filt_type(base, v0, v1, factor) + + r_s = np.logspace(-1, 1, n_points) + + out1, out2 = get_splined_dlf_points(filt, r_s.min(), r_s.max()) + test1, test2 = get_dlf_points(filt, r_s, -1) + + npt.assert_allclose(out1, test1[0]) + npt.assert_allclose(out2, test2) diff --git a/tests/utils/test_report.py b/tests/utils/test_report.py index bd6238712e..2f38f361c5 100644 --- a/tests/utils/test_report.py +++ b/tests/utils/test_report.py @@ -16,8 +16,8 @@ def test_version_defaults(self): "numpy", "scipy", "matplotlib", - "empymod", "geoana", + "libdlf", ], # Optional packages. optional=[ From 54b9933e2ce99b9258095447e2df17a4629b595c Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Sun, 3 Nov 2024 09:14:05 -0800 Subject: [PATCH 090/194] Changlog for v0.23.0 (#1567) Add changelog for SimPEG v0.23.0. Co-authored-by: Joseph Capriotti --- docs/content/release/0.23.0-notes.rst | 210 ++++++++++++++++++++++++++ docs/content/release/index.rst | 1 + 2 files changed, 211 insertions(+) create mode 100644 docs/content/release/0.23.0-notes.rst diff --git a/docs/content/release/0.23.0-notes.rst b/docs/content/release/0.23.0-notes.rst new file mode 100644 index 0000000000..5c05b36c25 --- /dev/null +++ b/docs/content/release/0.23.0-notes.rst @@ -0,0 +1,210 @@ +.. _0.23.0_notes: + +=========================== +SimPEG 0.23.0 Release Notes +=========================== + +November 1st, 2024 + +.. contents:: Highlights + :depth: 3 + +Updates +======= + +New features +------------ + +Full support for Numpy 2.0 +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +With this release SimPEG is fully compatible with Numpy 2.0. + +Augmented receivers for airborne NSEM +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +We added new types of receivers intended to be used for airborne natural-source +EM simulations: + +- :class:`~simpeg.electromagnetics.natural_source.receivers.Impedance` +- :class:`~simpeg.electromagnetics.natural_source.receivers.Admittance` +- :class:`~simpeg.electromagnetics.natural_source.receivers.ApparentConductivity` +- :class:`~simpeg.electromagnetics.natural_source.receivers.Tipper` + +They extend the type of airborne NSEM surveys that can be simulated in SimPEG. + +See https://github.com/simpeg/simpeg/pull/1454 for more details. + +Automatic selection of Solver +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This release includes a new +:func:`~simpeg.utils.solver_utils.get_default_solver` function that +automatically selects a solver based on what you have installed on +your system. +For example, it'll select ``Pardiso`` as the solver if you are running an Intel +CPU and have `pydiso` installed in your system. Alternatively, it can choose +``Mumps`` if you are using Apple silicon and the `python-mumps` package is installed. +If no fast solver is available, it'll choose SciPy's ``SparseLU`` solver. + +.. note:: + For those installing through `conda-forge`, ``conda install simpeg`` will now also grab + better solvers for your system as well, consistent with `simpeg`'s solver priority. + +Moreover, simulations will also use this function to get the default solver if no +solver is being provided by the user, making it easier to run efficient +finite volume simulations out of the box. +See https://github.com/simpeg/simpeg/pull/1511 for more information. + +Support for magnetic gradiometry in Choclo-based magnetic simulations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Now the magnetic simulations with ``engine="choclo"`` support forward modelling +the magnetic gradiometry components and TMI derivatives. This fully extends the +support of the Numba-based simulations to every field that can also be +computed using ``engine="geoana"``. + +See https://github.com/simpeg/simpeg/pull/1543 and +https://github.com/simpeg/simpeg/pull/1553 for more information. + +Numba-based implementation of gravity and equivalent sources +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This release includes new implementations of the gravity and magnetic +equivalent sources using `Choclo `__'s kernels and +forward modelling functions and `Numba `__ to +just-in-time compilations and parallelization, making them faster and more +memory efficient. + +See https://github.com/simpeg/simpeg/pull/1552 and +https://github.com/simpeg/simpeg/pull/1527. + +New ``UpdateIRLS`` directive +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +We have renamed the :class:`simpeg.directives.UpdateIRLS` directive for +applying the Iterative Re-weighted Least-Squares during sparse norm inversions, +that includes better argument names and an improved implementation. +See https://github.com/simpeg/simpeg/pull/1349 for more details. + +Standardize arguments for active cells and random seeds +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +We have standardize the name of the arguments for active cells in meshes through +all functions and methods in SimPEG. We have deprecated the old +names like ``indAct``, ``ind_active`` and ``indActive`` in favor of +``active_cells``. + +The arguments for passing random seeds have also been standardized to +``random_seed``. These arguments generalize a way to specify random states, +allowing to take seeds as integers or instances of +:class:`numpy.random.Generator`. + + +Upgraded dependencies +--------------------- + +In SimPEG v0.23.0 we dropped support for Python versions <= 3.9. Python 3.8 met +its end-of-life this year (October 2024). Python 3.10 is the minimum required +version for Numpy 2.1.0. To keep up with the latest updates in the scientific +Python ecosystem, we decided to set Python 3.10 as the minimum required version +for SimPEG as well. + +Moreover, we have increased the minimum required versions of ``discretize``, +``geoana`` and ``pymatsolver`` in order to support Numpy 2.0. +Lastly, now ``pandas``, ``scikit-learn`` and ``empymod`` are optional +dependencies (instead of required ones). + + +Documentation +------------- + +This release includes a few fixes to the documentation pages, like improvements +to some magnetic examples, and fixes to docstrings and math of a few classes. + + +Bugfixes +-------- + +We have fixed some issues of Dask-based simulations that were running into +race-conditions after one of the latest Dask updates. See +https://github.com/simpeg/simpeg/pull/1469 for more information. + + +Contributors +============ + +* `@domfournier `__ +* `@jcapriot `__ +* `@prisae `__ +* `@santisoler `__ +* `@thibaut-kobold `__ + +Pull Requests +============= + +- Make ``pandas`` & ``scikit-learn`` optional dependencies by `@prisae `__ in + https://github.com/simpeg/simpeg/pull/1514 +- Dask races by `@jcapriot `__ in https://github.com/simpeg/simpeg/pull/1469 +- Irls refactor by `@domfournier `__ in + https://github.com/simpeg/simpeg/pull/1349 +- Minimum python version update by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1522 +- Replace ``ind_active`` for ``active_cells`` in pf simulations by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1520 +- Move push to codecov to its own stage by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1493 +- Minor fix in deprecation notice in docstrings by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1535 +- Replace ``indActive`` and ``actInd`` for ``active_cells`` in maps by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1534 +- Update tests and examples to use the new ``UpdateIRLS`` by + `@domfournier `__ in https://github.com/simpeg/simpeg/pull/1472 +- Replace ``indActive`` in VRM simulations for ``active_cells`` by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1536 +- Test assigned values when passing deprecated args by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1544 +- Use random seed when using ``make_synthetic_data`` in tests by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1545 +- Minor improvements to ``UpdateIRLS`` class by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1529 +- Replace ``seed`` for ``random_seed`` in directives by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1538 +- Minor fixes to magnetic examples by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1547 +- Support magnetic gradiometry using Choclo as engine by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1543 +- Update usage of ``random_seed`` in one example by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1549 +- Replace ``seed`` for ``random_seed`` in ``model_builder`` by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1548 +- Replace old args for ``active_cells`` in EM static functions by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1550 +- Implement gravity equivalent sources with Choclo as engine by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1527 +- Implement tmi derivatives with Choclo in magnetic simulation by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1553 +- Implement magnetic eq sources with Choclo by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1552 +- Update links in PR template by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1554 +- Default solver by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1511 +- Fixes for most recent geoana 0.7 by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1557 +- Numpy2.0 and discretize 0.11.0 updates by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1558 +- Add missing seeds by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1560 +- Make use of meshes’ ``cell_bounds`` property by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1559 +- Fix docstring of ``SmoothnessFullGradient`` by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1562 +- Fix math in docstring of eigenvalue_by_power_iteration by `@santisoler `__ + in https://github.com/simpeg/simpeg/pull/1564 +- Only warn about default solver when set in simulations by `@santisoler `__ + in https://github.com/simpeg/simpeg/pull/1565 +- Augmented receivers for airborne NSEM by `@dccowan `__ in https://github.com/simpeg/simpeg/pull/1454 +- Try uploading all the coverage files at once. by `@jcapriot `__ in https://github.com/simpeg/simpeg/pull/1569 +- Re-implement distance weighting and add a strategy comparison by `@thibaut-kobold `__ in https://github.com/simpeg/simpeg/pull/1310 +- Remove empymod dependency by `@jcapriot `__ in https://github.com/simpeg/simpeg/pull/1571 diff --git a/docs/content/release/index.rst b/docs/content/release/index.rst index 5e6f5c6c84..bb9bef3492 100644 --- a/docs/content/release/index.rst +++ b/docs/content/release/index.rst @@ -5,6 +5,7 @@ Release Notes .. toctree:: :maxdepth: 2 + 0.23.0 <0.23.0-notes> 0.22.2 <0.22.2-notes> 0.22.1 <0.22.1-notes> 0.22.0 <0.22.0-notes> From f70741a0dab408e526693d15fcc5c65c1b841c09 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Sun, 3 Nov 2024 09:26:45 -0800 Subject: [PATCH 091/194] Add v0.23.0 to the version switcher (#1573) Add entry for v0.23.0 in `_versions.json`. --- docs/_static/versions.json | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/_static/versions.json b/docs/_static/versions.json index d1c70abbca..665a00a498 100644 --- a/docs/_static/versions.json +++ b/docs/_static/versions.json @@ -4,11 +4,16 @@ "url": "https://docs.simpeg.xyz/dev/" }, { - "name": "v0.22.2 (latest)", - "version": "0.22.2", - "url": "https://docs.simpeg.xyz/v0.22.2/", + "name": "v0.23.0 (latest)", + "version": "0.23.0", + "url": "https://docs.simpeg.xyz/v0.23.0/", "preferred": true }, + { + "name": "v0.22.2", + "version": "0.22.2", + "url": "https://docs.simpeg.xyz/v0.22.2/" + }, { "name": "v0.22.1", "version": "0.22.1", From 8c046219caa74efcbfd14177d91da3f2c99ddb86 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Sun, 3 Nov 2024 09:52:09 -0800 Subject: [PATCH 092/194] Fix date in release notes (#1574) Fix release date for v0.23.0. --- docs/content/release/0.23.0-notes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/release/0.23.0-notes.rst b/docs/content/release/0.23.0-notes.rst index 5c05b36c25..a7141dc50f 100644 --- a/docs/content/release/0.23.0-notes.rst +++ b/docs/content/release/0.23.0-notes.rst @@ -4,7 +4,7 @@ SimPEG 0.23.0 Release Notes =========================== -November 1st, 2024 +November 3rd, 2024 .. contents:: Highlights :depth: 3 From 60e0c3a5dea6e9b86762b1109cdfe76b37b3d15d Mon Sep 17 00:00:00 2001 From: Lindsey Heagy Date: Sun, 3 Nov 2024 23:07:14 -0800 Subject: [PATCH 093/194] Bugfix for TDEM magnetic dipole sources (#1572) #### Summary Bugfix so that the initial fields produced for `db/dt`, `dh/dt`, from the B field or H field simulations are zero. There was previously a bug where we set the electric source term to `Zero()` when infact it should have been evaluated. This would only effect `db/dt` (or `dh/dt`) that were measured before the first time-step, but it also impacted visualizations of the fields. To catch this issue, I have extended our cross-check tests for tdem simulation to also include an evaluation of the data at t=0. Running this test on the current main version of SimPEG will fail. --- simpeg/electromagnetics/time_domain/sources.py | 12 ++---------- tests/em/tdem/test_TDEM_crosscheck.py | 2 +- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/simpeg/electromagnetics/time_domain/sources.py b/simpeg/electromagnetics/time_domain/sources.py index 6d18fb42d0..afef51aa4e 100644 --- a/simpeg/electromagnetics/time_domain/sources.py +++ b/simpeg/electromagnetics/time_domain/sources.py @@ -1428,11 +1428,7 @@ def s_e(self, simulation, time): self.waveform.has_initial_fields is True and time < simulation.time_steps[1] ): - if simulation._fieldType == "b": - return Zero() - elif simulation._fieldType == "e": - # Compute s_e from vector potential - return C.T * (MfMui * b) + return C.T * (MfMui * b) else: return C.T * (MfMui * b) * self.waveform.eval(time) @@ -1443,11 +1439,7 @@ def s_e(self, simulation, time): self.waveform.has_initial_fields is True and time < simulation.time_steps[1] ): - if simulation._fieldType == "h": - return Zero() - elif simulation._fieldType == "j": - # Compute s_e from vector potential - return C * h + return C * h else: return C * h * self.waveform.eval(time) diff --git a/tests/em/tdem/test_TDEM_crosscheck.py b/tests/em/tdem/test_TDEM_crosscheck.py index 4c1de54eba..905b1115d9 100644 --- a/tests/em/tdem/test_TDEM_crosscheck.py +++ b/tests/em/tdem/test_TDEM_crosscheck.py @@ -35,7 +35,7 @@ def setUp_TDEM( ) mapping = maps.ExpMap(mesh) * maps.SurjectVertical1D(mesh) * activeMap - rxtimes = np.logspace(-4, -3, 20) + rxtimes = np.hstack([np.r_[0], np.logspace(-4, -3, 20)]) if waveform.upper() == "RAW": t0 = 0.006 From c6036df333137cc49928b4ee442525641b938e9a Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Wed, 13 Nov 2024 10:04:52 -0700 Subject: [PATCH 094/194] Fix ubcstyle printout (#1577) #### Summary UBC style printout on minimizers were never updated for #1326, this fixes that and removes the "factor" property that was used to handle that printout style. #### Reference issue Related to #763 #### What does this implement/fix? Fixes the UBC style printout --- simpeg/directives/directives.py | 2 +- simpeg/optimization.py | 14 ++++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/simpeg/directives/directives.py b/simpeg/directives/directives.py index 7075dcfd2d..ee4a4c2d4d 100644 --- a/simpeg/directives/directives.py +++ b/simpeg/directives/directives.py @@ -1234,7 +1234,7 @@ def print_final_misfit(self): if self.opt.print_type == "ubc": self.opt.print_target = ( ">> Target misfit: %.1f (# of data) is achieved" - ) % (self.target * self.invProb.opt.factor) + ) % (self.target) class MultiTargetMisfits(InversionDirective): diff --git a/simpeg/optimization.py b/simpeg/optimization.py index 4c8aa4ca3a..b016ae887a 100644 --- a/simpeg/optimization.py +++ b/simpeg/optimization.py @@ -205,38 +205,38 @@ class IterationPrinters(object): } phi_d = { "title": "phi_d", - "value": lambda M: M.parent.phi_d * M.parent.opt.factor, + "value": lambda M: M.parent.phi_d, "width": 10, "format": "%1.2e", } phi_m = { "title": "phi_m", - "value": lambda M: M.parent.phi_m * M.parent.opt.factor, + "value": lambda M: M.parent.phi_m, "width": 10, "format": "%1.2e", } phi_s = { "title": "phi_s", - "value": lambda M: M.parent.phi_s * M.parent.opt.factor, + "value": lambda M: M.parent.phi_s, "width": 10, "format": "%1.2e", } phi_x = { "title": "phi_x", - "value": lambda M: M.parent.phi_x * M.parent.opt.factor, + "value": lambda M: M.parent.phi_x, "width": 10, "format": "%1.2e", } phi_y = { "title": "phi_y", - "value": lambda M: M.parent.phi_y * M.parent.opt.factor, + "value": lambda M: M.parent.phi_y, "width": 10, "format": "%1.2e", } phi_z = { "title": "phi_z", - "value": lambda M: M.parent.phi_z * M.parent.opt.factor, + "value": lambda M: M.parent.phi_z, "width": 10, "format": "%1.2e", } @@ -282,7 +282,6 @@ class Minimize(object): parent = None #: This is the parent of the optimization routine. print_type = None - factor = 1.0 def __init__(self, **kwargs): set_kwargs(self, **kwargs) @@ -303,7 +302,6 @@ def __init__(self, **kwargs): ] if self.print_type == "ubc": - self.factor = 2.0 self.stoppers = [StoppingCriteria.iteration] self.printers = [ IterationPrinters.iteration, From 50b79e287227e965cd3ca22ff297615e74d50a28 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 13 Nov 2024 09:05:30 -0800 Subject: [PATCH 095/194] Add docstring to `n_processes` in potential field simulations (#1578) #### Summary Add missing docstring to the `n_processes` property in potential field simulation classes. The missing docstrings were raised by @thibaut-kobold in Mattermost. --- simpeg/potential_fields/base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/simpeg/potential_fields/base.py b/simpeg/potential_fields/base.py index f997575336..1601d38b57 100644 --- a/simpeg/potential_fields/base.py +++ b/simpeg/potential_fields/base.py @@ -222,6 +222,11 @@ def sensitivity_dtype(self, value): @property def n_processes(self): + """ + Number of processes to use for forward modeling. + + If ``engine`` is ``"choclo"``, then this property will be ignored. + """ return self._n_processes @n_processes.setter From 1d1d01c8880ea3804f64ece3075f2ca45962a00e Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Tue, 19 Nov 2024 13:13:58 -0700 Subject: [PATCH 096/194] Move simulation solver from base simulation to PDE simulation (#1582) #### Summary Move `solver` attribute to the `BasePDESimulation`. #### What does this implement/fix? Not all simulations need `solver`, leading to awkward setting of `solver=None` on `LinearSimulation`, and also on the layered 1D simulations. This move the `solver` attribute to the `BasePDESimulation` (the ones that actually need a solver). --- simpeg/base/pde_simulation.py | 90 +++++++++++++++++++ simpeg/dask/simulation.py | 4 - simpeg/flow/richards/simulation.py | 3 +- simpeg/simulation.py | 79 ---------------- ..._mass_matrices.py => test_base_pde_sim.py} | 24 +++++ 5 files changed, 116 insertions(+), 84 deletions(-) rename tests/base/{test_mass_matrices.py => test_base_pde_sim.py} (97%) diff --git a/simpeg/base/pde_simulation.py b/simpeg/base/pde_simulation.py index bf000b4c5c..477b074985 100644 --- a/simpeg/base/pde_simulation.py +++ b/simpeg/base/pde_simulation.py @@ -1,10 +1,15 @@ +import inspect import numpy as np +import pymatsolver import scipy.sparse as sp from discretize.utils import Zero, TensorType from ..simulation import BaseSimulation from .. import props from scipy.constants import mu_0 +from ..utils import validate_type +from ..utils.solver_utils import get_default_solver + def __inner_mat_mul_op(M, u, v=None, adjoint=False): u = np.squeeze(u) @@ -413,6 +418,91 @@ def _clear_on_prop_update(self): class BasePDESimulation(BaseSimulation): + """Base simulation for PDE solutions. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + Mesh on which the forward problem is discretized. + solver : type[pymatsolver.base.Base], optional + Numerical solver used to solve the forward problem. If ``None``, + an appropriate solver specific to the simulation class is set by default. + solver_opts : dict, optional + Solver-specific parameters. If ``None``, default parameters are used for + the solver set by ``solver``. Otherwise, the ``dict`` must contain appropriate + pairs of keyword arguments and parameter values for the solver. Please visit + `pymatsolver `__ to learn more + about solvers and their parameters. + + """ + + def __init__(self, mesh, solver=None, solver_opts=None, **kwargs): + super().__init__(mesh=mesh, **kwargs) + self.solver = solver + if solver_opts is None: + solver_opts = {} + self.solver_opts = solver_opts + + @property + def solver(self): + r"""Numerical solver used in the forward simulation. + + Many forward simulations in SimPEG require solutions to discrete linear + systems of the form: + + .. math:: + \mathbf{A}(\mathbf{m}) \, \mathbf{u} = \mathbf{q} + + where :math:`\mathbf{A}` is an invertible matrix that depends on the + model :math:`\mathbf{m}`. The numerical solver can be set using the + ``solver`` property. In SimPEG, the + `pymatsolver `__ package + is used to create solver objects. Parameters specific to each solver + can be set manually using the ``solver_opts`` property. + + Returns + ------- + type[pymatsolver.solvers.Base] + Numerical solver used to solve the forward problem. + """ + if self._solver is None: + # do not cache this, in case the user wants to + # change it after the first time it is requested. + return get_default_solver(warn=True) + return self._solver + + @solver.setter + def solver(self, cls): + if cls is not None: + if not inspect.isclass(cls): + raise TypeError(f"{type(self).__qualname__}.solver must be a class") + if not issubclass(cls, pymatsolver.solvers.Base): + raise TypeError( + f"{cls.__qualname__} is not a subclass of pymatsolver.base.BaseSolver" + ) + self._solver = cls + + @property + def solver_opts(self): + """Solver-specific parameters. + + The parameters specific to the solver set with the ``solver`` property are set + upon instantiation. The ``solver_opts`` property is used to set solver-specific properties. + This is done by providing a ``dict`` that contains appropriate pairs of keyword arguments + and parameter values. Please visit `pymatsolver `__ + to learn more about solvers and their parameters. + + Returns + ------- + dict + keyword arguments and parameters passed to the solver. + """ + return self._solver_opts + + @solver_opts.setter + def solver_opts(self, value): + self._solver_opts = validate_type("solver_opts", value, dict, cast=False) + @property def Vol(self): return self.Mcc diff --git a/simpeg/dask/simulation.py b/simpeg/dask/simulation.py index 3f64f35ac0..6d17c9f69f 100644 --- a/simpeg/dask/simulation.py +++ b/simpeg/dask/simulation.py @@ -53,8 +53,6 @@ def __init__( self, mesh=None, survey=None, - solver=None, - solver_opts=None, sensitivity_path="./sensitivity/", counter=None, verbose=False, @@ -67,8 +65,6 @@ def __init__( self, mesh=mesh, survey=survey, - solver=solver, - solver_opts=solver_opts, sensitivity_path=sensitivity_path, counter=counter, verbose=verbose, diff --git a/simpeg/flow/richards/simulation.py b/simpeg/flow/richards/simulation.py index c6dae5c408..f7d7c0b2ee 100644 --- a/simpeg/flow/richards/simulation.py +++ b/simpeg/flow/richards/simulation.py @@ -3,6 +3,7 @@ import time from ... import utils +from ...base import BasePDESimulation from ...simulation import BaseTimeSimulation from ... import optimization from ...utils import ( @@ -19,7 +20,7 @@ from .empirical import BaseWaterRetention -class SimulationNDCellCentered(BaseTimeSimulation): +class SimulationNDCellCentered(BaseTimeSimulation, BasePDESimulation): """Richards Simulation""" def __init__( diff --git a/simpeg/simulation.py b/simpeg/simulation.py index aa0237f0fd..36874afa1a 100644 --- a/simpeg/simulation.py +++ b/simpeg/simulation.py @@ -3,7 +3,6 @@ """ import os -import inspect import numpy as np import warnings @@ -27,8 +26,6 @@ ) import uuid -from .utils.solver_utils import get_default_solver - __all__ = ["LinearSimulation", "ExponentialSinusoidSimulation"] @@ -55,15 +52,6 @@ class BaseSimulation(props.HasModel): Mesh on which the forward problem is discretized. survey : simpeg.survey.BaseSurvey, optional The survey for the simulation. - solver : None or pymatsolver.base.Base, optional - Numerical solver used to solve the forward problem. If ``None``, - an appropriate solver specific to the simulation class is set by default. - solver_opts : dict, optional - Solver-specific parameters. If ``None``, default parameters are used for - the solver set by ``solver``. Otherwise, the ``dict`` must contain appropriate - pairs of keyword arguments and parameter values for the solver. Please visit - `pymatsolver `__ to learn more - about solvers and their parameters. sensitivity_path : str, optional Path to directory where sensitivity file is stored. counter : None or simpeg.utils.Counter @@ -78,8 +66,6 @@ def __init__( self, mesh=None, survey=None, - solver=None, - solver_opts=None, sensitivity_path=None, counter=None, verbose=False, @@ -87,10 +73,6 @@ def __init__( ): self.mesh = mesh self.survey = survey - self.solver = solver - if solver_opts is None: - solver_opts = {} - self.solver_opts = solver_opts if sensitivity_path is None: sensitivity_path = os.path.join(".", "sensitivity") self.sensitivity_path = sensitivity_path @@ -169,64 +151,6 @@ def sensitivity_path(self): def sensitivity_path(self, value): self._sensitivity_path = validate_string("sensitivity_path", value) - @property - def solver(self): - r"""Numerical solver used in the forward simulation. - - Many forward simulations in SimPEG require solutions to discrete linear - systems of the form: - - .. math:: - \mathbf{A}(\mathbf{m}) \, \mathbf{u} = \mathbf{q} - - where :math:`\mathbf{A}` is an invertible matrix that depends on the - model :math:`\mathbf{m}`. The numerical solver can be set using the - ``solver`` property. In SimPEG, the - `pymatsolver `__ package - is used to create solver objects. Parameters specific to each solver - can be set manually using the ``solver_opts`` property. - - Returns - ------- - pymatsolver.base.Base - Numerical solver used to solve the forward problem. - """ - if self._solver is None: - # do not cache this, in case the user wants to - # change it after the first time it is requested. - return get_default_solver(warn=True) - return self._solver - - @solver.setter - def solver(self, cls): - if cls is not None: - if not inspect.isclass(cls): - raise TypeError(f"solver must be a class, not a {type(cls)}") - if not hasattr(cls, "__mul__"): - raise TypeError("solver must support the multiplication operator, `*`.") - self._solver = cls - - @property - def solver_opts(self): - """Solver-specific parameters. - - The parameters specific to the solver set with the ``solver`` property are set - upon instantiation. The ``solver_opts`` property is used to set solver-specific properties. - This is done by providing a ``dict`` that contains appropriate pairs of keyword arguments - and parameter values. Please visit `pymatsolver `__ - to learn more about solvers and their parameters. - - Returns - ------- - dict - keyword arguments and parameters passed to the solver. - """ - return self._solver_opts - - @solver_opts.setter - def solver_opts(self, value): - self._solver_opts = validate_type("solver_opts", value, dict, cast=False) - @property def verbose(self): """Verbose progress printout. @@ -754,9 +678,6 @@ class LinearSimulation(BaseSimulation): "The model for a linear problem" ) - # linear simulations do not have a solver so set it to `None` here - solver = None - def __init__(self, mesh=None, linear_model=None, model_map=None, G=None, **kwargs): super().__init__(mesh=mesh, **kwargs) self.linear_model = linear_model diff --git a/tests/base/test_mass_matrices.py b/tests/base/test_base_pde_sim.py similarity index 97% rename from tests/base/test_mass_matrices.py rename to tests/base/test_base_pde_sim.py index 97a4adfd60..56a62b1538 100644 --- a/tests/base/test_mass_matrices.py +++ b/tests/base/test_base_pde_sim.py @@ -1,3 +1,5 @@ +import re + from simpeg.base import with_property_mass_matrices, BasePDESimulation from simpeg import props, maps import unittest @@ -9,6 +11,8 @@ import scipy.sparse as sp import pytest +from simpeg.utils.solver_utils import get_default_solver + # define a very simple class... @with_property_mass_matrices("sigma") @@ -806,3 +810,23 @@ def test_bad_derivative_stash(): with pytest.raises(TypeError): sim.MeSigmaDeriv(u, v) + + +def test_solver_defaults(): + mesh = discretize.TensorMesh([2, 2, 2]) + sim = BasePDESimulation(mesh) + with pytest.warns(UserWarning, match="Using the default solver.*"): + solver_class = sim.solver + + assert solver_class is get_default_solver() + + +def test_bad_solver(): + mesh = discretize.TensorMesh([2, 2, 2]) + msg = re.escape("BasePDESimulation.solver must be a class") + with pytest.raises(TypeError, match=msg): + BasePDESimulation(mesh, solver="f") + + msg = re.escape("str is not a subclass of pymatsolver.base.BaseSolver") + with pytest.raises(TypeError, match=msg): + BasePDESimulation(mesh, solver=str) From e74bc521f1fd8c87315848451eff16b3e7bf1a00 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 20 Nov 2024 11:54:57 -0800 Subject: [PATCH 097/194] Update and fix instructions to build the docs (#1583) Update the instructions on how to build the documentation pages and serve them locally. --------- Co-authored-by: Joseph Capriotti --- .../contributing/documentation.rst | 38 ++++++++++++++++--- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/docs/content/getting_started/contributing/documentation.rst b/docs/content/getting_started/contributing/documentation.rst index 8ef695961a..0d5f8e3962 100644 --- a/docs/content/getting_started/contributing/documentation.rst +++ b/docs/content/getting_started/contributing/documentation.rst @@ -70,18 +70,44 @@ For example: Building the documentation ~~~~~~~~~~~~~~~~~~~~~~~~~~ -If you would like to see the documentation changes. -In the repo's root directory, enter the following in your terminal. +You can build the documentation pages locally to see how the new changes will +look. First, make sure that you have :ref:`created and activated an environment +` with simpeg installed in it. Then, navigate to the +``docs`` folder: -.. code:: +.. code:: bash - make all + cd docs + +And run the following to build the docs: + +.. code:: bash + + make html + +.. note:: + + This command will build all documentation pages, including all the examples + and tutorials. Running the examples might take considerable amount of time. + + If you want to build the docs, but avoid running the examples, you can + alternatively run: + + .. code:: bash + + make html-noplot Serving the documentation locally ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Once the documentation is built. You can view it directly using the following command. This will automatically serve the docs and you can see them in your browser. +Once the documentation is built, you can view it using the following +command (make sure you are in the ``docs`` directory): -.. code:: +.. code:: bash make serve + +It will automatically serve the docs and you can see them in your browser. + +Alternatively, you can open your file manager and open the ``index.html`` file +in the ``docs/_build/html`` folder. From b6c84ccaaf81c823f6591631cfe03cef4f285b1c Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Mon, 25 Nov 2024 12:02:10 -0700 Subject: [PATCH 098/194] Change location of `mesh` attribute (#1585) Removes the `mesh` attribute of `BaseSimulation` as not all simulations require meshes. Not all simulations require a `mesh`, so this makes it more explicit which simulations actually require it, and I've added further constraints on mesh types for simulations which are restricted on their mesh types allowed. #### Additional Information This branches off of #1582. so should be merged after that one. --------- Co-authored-by: Santiago Soler --- simpeg/base/pde_simulation.py | 21 +++++- simpeg/dask/simulation.py | 2 - simpeg/electromagnetics/base_1d.py | 2 +- .../natural_source/simulation_1d.py | 2 +- .../static/induced_polarization/simulation.py | 2 +- .../static/resistivity/simulation_2d.py | 10 +++ .../simulation.py | 18 +++++- simpeg/flow/richards/empirical.py | 11 +++- simpeg/flow/richards/simulation.py | 17 +++++ simpeg/potential_fields/base.py | 64 ++++++++++--------- .../straight_ray_tomography/simulation.py | 23 +++++-- simpeg/simulation.py | 63 ++++++++---------- simpeg/utils/code_utils.py | 35 ++++++++-- tests/base/test_Fields.py | 24 ++++--- tests/base/test_base_pde_sim.py | 11 ++++ tests/base/test_problem.py | 4 +- tests/base/test_simulation.py | 44 ++++++++++++- tests/em/static/test_DC_2D_analytic.py | 9 +++ tests/em/vrm/test_vrm_instantiation.py | 27 ++++++++ tests/flow/test_Richards.py | 17 +++++ tests/flow/test_Richards_empirical.py | 19 ++++++ tests/pf/test_base_pf_simulation.py | 49 +++++++------- tests/pf/test_equivalent_sources.py | 40 ++---------- tests/pf/test_forward_Grav_Linear.py | 37 ++--------- tests/pf/test_forward_Mag_Linear.py | 43 ++----------- tests/pf/test_pf_quadtree_inversion_linear.py | 2 +- tests/seis/test_tomo.py | 25 ++++++++ tests/{base => utils}/test_validators.py | 49 ++++++++++++-- 28 files changed, 435 insertions(+), 235 deletions(-) create mode 100644 tests/em/vrm/test_vrm_instantiation.py rename tests/{base => utils}/test_validators.py (91%) diff --git a/simpeg/base/pde_simulation.py b/simpeg/base/pde_simulation.py index 477b074985..32ba694d5e 100644 --- a/simpeg/base/pde_simulation.py +++ b/simpeg/base/pde_simulation.py @@ -3,6 +3,7 @@ import pymatsolver import scipy.sparse as sp from discretize.utils import Zero, TensorType +import discretize.base from ..simulation import BaseSimulation from .. import props from scipy.constants import mu_0 @@ -437,12 +438,30 @@ class BasePDESimulation(BaseSimulation): """ def __init__(self, mesh, solver=None, solver_opts=None, **kwargs): - super().__init__(mesh=mesh, **kwargs) + self.mesh = mesh + super().__init__(**kwargs) self.solver = solver if solver_opts is None: solver_opts = {} self.solver_opts = solver_opts + @property + def mesh(self): + """Mesh for the simulation. + + For more on meshes, visit :py:class:`discretize.base.BaseMesh`. + + Returns + ------- + discretize.base.BaseMesh + Mesh on which the forward problem is discretized. + """ + return self._mesh + + @mesh.setter + def mesh(self, value): + self._mesh = validate_type("mesh", value, discretize.base.BaseMesh, cast=False) + @property def solver(self): r"""Numerical solver used in the forward simulation. diff --git a/simpeg/dask/simulation.py b/simpeg/dask/simulation.py index 6d17c9f69f..1d5deecb24 100644 --- a/simpeg/dask/simulation.py +++ b/simpeg/dask/simulation.py @@ -51,7 +51,6 @@ def max_chunk_size(self, other): def __init__( self, - mesh=None, survey=None, sensitivity_path="./sensitivity/", counter=None, @@ -63,7 +62,6 @@ def __init__( ): _old_init( self, - mesh=mesh, survey=survey, sensitivity_path=sensitivity_path, counter=counter, diff --git a/simpeg/electromagnetics/base_1d.py b/simpeg/electromagnetics/base_1d.py index cf73c0ce8e..e29802214f 100644 --- a/simpeg/electromagnetics/base_1d.py +++ b/simpeg/electromagnetics/base_1d.py @@ -102,7 +102,7 @@ def __init__( n_points_per_path=3, **kwargs, ): - super().__init__(mesh=None, **kwargs) + super().__init__(**kwargs) self.sigma = sigma self.rho = rho self.sigmaMap = sigmaMap diff --git a/simpeg/electromagnetics/natural_source/simulation_1d.py b/simpeg/electromagnetics/natural_source/simulation_1d.py index 783d389b95..bbadaad775 100644 --- a/simpeg/electromagnetics/natural_source/simulation_1d.py +++ b/simpeg/electromagnetics/natural_source/simulation_1d.py @@ -57,7 +57,7 @@ def __init__( fix_Jmatrix=False, **kwargs, ): - super().__init__(mesh=None, survey=survey, **kwargs) + super().__init__(survey=survey, **kwargs) self.fix_Jmatrix = fix_Jmatrix self.sigma = sigma self.rho = rho diff --git a/simpeg/electromagnetics/static/induced_polarization/simulation.py b/simpeg/electromagnetics/static/induced_polarization/simulation.py index 64b07682bc..cd598bcb9e 100644 --- a/simpeg/electromagnetics/static/induced_polarization/simulation.py +++ b/simpeg/electromagnetics/static/induced_polarization/simulation.py @@ -62,7 +62,7 @@ def _scale(self): def __init__( self, - mesh=None, + mesh, survey=None, sigma=None, rho=None, diff --git a/simpeg/electromagnetics/static/resistivity/simulation_2d.py b/simpeg/electromagnetics/static/resistivity/simulation_2d.py index 239d51536c..1dab7d3da4 100644 --- a/simpeg/electromagnetics/static/resistivity/simulation_2d.py +++ b/simpeg/electromagnetics/static/resistivity/simulation_2d.py @@ -21,6 +21,7 @@ from .utils import _mini_pole_pole from scipy.special import k0e, k1e, k0 from discretize.utils import make_boundary_bool +import discretize.base class BaseDCSimulation2D(BaseElectricalPDESimulation): @@ -128,6 +129,15 @@ def g(k): if miniaturize: self._dipoles, self._invs, self._mini_survey = _mini_pole_pole(self.survey) + @BaseElectricalPDESimulation.mesh.setter + def mesh(self, value): + value = validate_type("mesh", value, discretize.base.BaseMesh, cast=False) + if value.dim != 2: + raise ValueError( + f"{type(self).__name__} mesh must be 2D, received a {value.dim}D mesh." + ) + self._mesh = value + @property def survey(self): """The DC survey object. diff --git a/simpeg/electromagnetics/viscous_remanent_magnetization/simulation.py b/simpeg/electromagnetics/viscous_remanent_magnetization/simulation.py index d3d5835351..fe9f707ad5 100644 --- a/simpeg/electromagnetics/viscous_remanent_magnetization/simulation.py +++ b/simpeg/electromagnetics/viscous_remanent_magnetization/simulation.py @@ -38,7 +38,8 @@ def __init__( indActive=None, **kwargs, ): - super().__init__(mesh=mesh, survey=survey, **kwargs) + self.mesh = mesh + super().__init__(survey=survey, **kwargs) if refinement_distance is None: if refinement_factor is None: @@ -72,14 +73,25 @@ def __init__( active_cells = np.ones(self.mesh.n_cells, dtype=bool) self.active_cells = active_cells - @BaseSimulation.mesh.setter + @property + def mesh(self): + """Mesh for the integral VRM simulations. + + Returns + ------- + discretize.TensorMesh or discretize.TreeMesh + 3D Mesh on which the forward problem is discretized. + """ + return self._mesh + + @mesh.setter def mesh(self, value): value = validate_type( "mesh", value, (discretize.TensorMesh, discretize.TreeMesh), cast=False ) if value.dim != 3: raise ValueError( - f"Mesh must be 3D tensor or 3D tree. Current mesh is {value.dim}" + f"{type(self).__name__} mesh must be 3D, received a {value.dim}D mesh." ) self._mesh = value diff --git a/simpeg/flow/richards/empirical.py b/simpeg/flow/richards/empirical.py index ae74f05b96..97f6e5b640 100644 --- a/simpeg/flow/richards/empirical.py +++ b/simpeg/flow/richards/empirical.py @@ -1,7 +1,9 @@ +import discretize.base import numpy as np import scipy.sparse as sp from scipy import constants from ... import utils, props +from ...utils import validate_type def _get_projections(u): @@ -34,12 +36,19 @@ class NonLinearModel(props.HasModel): """A non linear model that has dependence on the fields and a model""" counter = None #: A simpeg.utils.Counter object - mesh = None #: A discretize Mesh def __init__(self, mesh, **kwargs): self.mesh = mesh super(NonLinearModel, self).__init__(**kwargs) + @property + def mesh(self): + return self._mesh + + @mesh.setter + def mesh(self, value): + self._mesh = validate_type("mesh", value, discretize.base.BaseMesh, cast=False) + @property def nP(self): """Number of parameters in the model.""" diff --git a/simpeg/flow/richards/simulation.py b/simpeg/flow/richards/simulation.py index f7d7c0b2ee..20d5055c9e 100644 --- a/simpeg/flow/richards/simulation.py +++ b/simpeg/flow/richards/simulation.py @@ -1,3 +1,4 @@ +import discretize.base import numpy as np import scipy.sparse as sp import time @@ -54,6 +55,22 @@ def __init__( ) water_retention = NestedModeler(BaseWaterRetention, "water retention curve") + @property + def mesh(self): + """Tensor style mesh for the Richards flow simulation. + + Returns + ------- + discretize.TensorMesh or discretize.TreeMesh + """ + return self._mesh + + @mesh.setter + def mesh(self, value): + self._mesh = validate_type( + "mesh", value, (discretize.TensorMesh, discretize.TreeMesh), cast=False + ) + # TODO: This can also be a function(time, u_ii) @property def boundary_conditions(self): diff --git a/simpeg/potential_fields/base.py b/simpeg/potential_fields/base.py index 1601d38b57..9745ef5613 100644 --- a/simpeg/potential_fields/base.py +++ b/simpeg/potential_fields/base.py @@ -4,13 +4,14 @@ import discretize import numpy as np +from discretize import TensorMesh, TreeMesh from scipy.sparse import csr_matrix as csr from simpeg.utils import mkvc from ..simulation import LinearSimulation from ..utils import validate_active_indices, validate_integer, validate_string -from ..utils.code_utils import deprecate_property +from ..utils.code_utils import deprecate_property, validate_type try: import choclo @@ -119,19 +120,17 @@ def __init__( "forwardOnly was removed in SimPEG 0.17.0, please set store_sensitivities='forward_only'" ) + self.mesh = mesh self.store_sensitivities = store_sensitivities self.sensitivity_dtype = sensitivity_dtype self.engine = engine self.numba_parallel = numba_parallel - super().__init__(mesh, **kwargs) + super().__init__(**kwargs) self.n_processes = n_processes # Check sensitivity_path when engine is "choclo" self._check_engine_and_sensitivity_path() - # Check dimensions of the mesh when engine is "choclo" - self._check_engine_and_mesh_dimensions() - # Find non-zero cells indices if active_cells is None: active_cells = np.ones(mesh.n_cells, dtype=bool) @@ -174,6 +173,26 @@ def __init__( self._nodes = nodes[unique] # unique active nodes self._unique_inv = unique_inv.reshape(cell_nodes.T.shape) + @property + def mesh(self): + """Mesh for the integral potential field simulations. + + Returns + ------- + discretize.TensorMesh or discretize.TreeMesh + 3D Mesh on which the forward problem is discretized. + """ + return self._mesh + + @mesh.setter + def mesh(self, value): + value = validate_type("mesh", value, (TensorMesh, TreeMesh), cast=False) + if value.dim != 3: + raise ValueError( + f"{type(self).__name__} mesh must be 3D, received a {value.dim}D mesh." + ) + self._mesh = value + @property def store_sensitivities(self): """Options for storing sensitivities. @@ -375,16 +394,6 @@ def _check_engine_and_sensitivity_path(self): "should be the path to a new or existing file." ) - def _check_engine_and_mesh_dimensions(self): - """ - Check dimensions of the mesh when using choclo as engine - """ - if self.engine == "choclo" and self.mesh.dim != 3: - raise ValueError( - f"Invalid mesh with {self.mesh.dim} dimensions. " - "Only 3D meshes are supported when using 'choclo' as engine." - ) - def _get_active_nodes(self): """ Return locations of nodes only for active cells @@ -441,11 +450,7 @@ class BaseEquivalentSourceLayerSimulation(BasePFSimulation): """ def __init__(self, mesh, cell_z_top, cell_z_bottom, **kwargs): - - if mesh.dim != 2: - raise AttributeError("Mesh to equivalent source layer must be 2D.") - - super().__init__(mesh, **kwargs) + super().__init__(mesh=mesh, **kwargs) if isinstance(cell_z_top, (int, float)): cell_z_top = float(cell_z_top) * np.ones(self.nC) @@ -489,17 +494,14 @@ def cell_z_bottom(self) -> np.ndarray: """ return self._cell_z_bottom - def _check_engine_and_mesh_dimensions(self): - """ - Check dimensions of the mesh - - Overwrite the parent's method: the equivalent sources class needs 2D - meshes, while the potential field simulations work only with 3D meshes. - - This check will run for any given engine. - """ - if self.mesh.dim != 2: - raise AttributeError("Mesh to equivalent source layer must be 2D.") + @BasePFSimulation.mesh.setter + def mesh(self, value): + value = validate_type("mesh", value, (TensorMesh, TreeMesh), cast=False) + if value.dim != 2: + raise ValueError( + f"{type(self).__name__} mesh must be 2D, received a {value.dim}D mesh." + ) + self._mesh = value def progress(iteration, prog, final): diff --git a/simpeg/seismic/straight_ray_tomography/simulation.py b/simpeg/seismic/straight_ray_tomography/simulation.py index 35916d2ab2..1070f1d544 100644 --- a/simpeg/seismic/straight_ray_tomography/simulation.py +++ b/simpeg/seismic/straight_ray_tomography/simulation.py @@ -1,9 +1,10 @@ +import discretize import numpy as np import scipy.sparse as sp import matplotlib.pyplot as plt from ...simulation import LinearSimulation -from ...utils import sub2ind +from ...utils import sub2ind, validate_type from ... import props @@ -77,13 +78,25 @@ def _lineintegral(M, Tx, Rx): class Simulation2DIntegral(LinearSimulation): slowness, slownessMap, slownessDeriv = props.Invertible("Slowness model (1/v)") - def __init__( - self, mesh=None, survey=None, slowness=None, slownessMap=None, **kwargs - ): - super().__init__(mesh=mesh, survey=survey, **kwargs) + def __init__(self, mesh, survey=None, slowness=None, slownessMap=None, **kwargs): + self.mesh = mesh + super().__init__(survey=survey, **kwargs) self.slowness = slowness self.slownessMap = slownessMap + @property + def mesh(self): + return self._mesh + + @mesh.setter + def mesh(self, value): + value = validate_type("mesh", value, discretize.TensorMesh, cast=False) + if value.dim != 2: + raise ValueError( + f"{type(self).__name__} mesh must be 2D, received a {value.dim}D mesh." + ) + self._mesh = value + @property def A(self): if getattr(self, "_A", None) is not None: diff --git a/simpeg/simulation.py b/simpeg/simulation.py index 36874afa1a..fbfdb3404d 100644 --- a/simpeg/simulation.py +++ b/simpeg/simulation.py @@ -6,7 +6,6 @@ import numpy as np import warnings -from discretize.base import BaseMesh from discretize import TensorMesh from discretize.utils import unpack_widths, sdiag, mkvc @@ -48,8 +47,6 @@ class BaseSimulation(props.HasModel): Parameters ---------- - mesh : discretize.base.BaseMesh, optional - Mesh on which the forward problem is discretized. survey : simpeg.survey.BaseSurvey, optional The survey for the simulation. sensitivity_path : str, optional @@ -64,14 +61,12 @@ class BaseSimulation(props.HasModel): def __init__( self, - mesh=None, survey=None, sensitivity_path=None, counter=None, verbose=False, **kwargs, ): - self.mesh = mesh self.survey = survey if sensitivity_path is None: sensitivity_path = os.path.join(".", "sensitivity") @@ -83,25 +78,6 @@ def __init__( super().__init__(**kwargs) - @property - def mesh(self): - """Mesh for the simulation. - - For more on meshes, visit :py:class:`discretize.base.BaseMesh`. - - Returns - ------- - discretize.base.BaseMesh - Mesh on which the forward problem is discretized. - """ - return self._mesh - - @mesh.setter - def mesh(self, value): - if value is not None: - value = validate_type("mesh", value, BaseMesh, cast=False) - self._mesh = value - @property def survey(self): """The survey for the simulation. @@ -465,9 +441,6 @@ class BaseTimeSimulation(BaseSimulation): Parameters ---------- - mesh : discretize.base.BaseMesh, optional - Mesh on which the forward problem is discretized. This is not necessarily - the same as the mesh on which the simulation is defined. t0 : float, optional Initial time, in seconds, for the time-dependent forward simulation. time_steps : (n_steps, ) numpy.ndarray, optional @@ -496,10 +469,10 @@ class BaseTimeSimulation(BaseSimulation): representation. """ - def __init__(self, mesh=None, t0=0.0, time_steps=None, **kwargs): + def __init__(self, t0=0.0, time_steps=None, **kwargs): self.t0 = t0 self.time_steps = time_steps - super().__init__(mesh=mesh, **kwargs) + super().__init__(**kwargs) @property def time_steps(self): @@ -663,9 +636,6 @@ class LinearSimulation(BaseSimulation): Parameters ---------- - mesh : discretize.BaseMesh, optional - Mesh on which the forward problem is discretized. This is not necessarily - the same as the mesh on which the simulation is defined. model_map : simpeg.maps.BaseMap Mapping from the model parameters to vector that the linear operator acts on. G : (n_data, n_param) numpy.ndarray or scipy.sparse.csr_matrx @@ -678,8 +648,8 @@ class LinearSimulation(BaseSimulation): "The model for a linear problem" ) - def __init__(self, mesh=None, linear_model=None, model_map=None, G=None, **kwargs): - super().__init__(mesh=mesh, **kwargs) + def __init__(self, linear_model=None, model_map=None, G=None, **kwargs): + super().__init__(**kwargs) self.linear_model = linear_model self.model_map = model_map if G is not None: @@ -820,6 +790,8 @@ class ExponentialSinusoidSimulation(LinearSimulation): Parameters ---------- + mesh : discretize.TensorMesh + 1D TensorMesh defining the discretization of the model space. n_kernels : int The number of kernel factors for the linear problem; i.e. the number of :math:`j_i \in [j_0, ... , j_n]`. This sets the number of rows @@ -834,7 +806,8 @@ class ExponentialSinusoidSimulation(LinearSimulation): Maximum value for the spread of the kernel factors. """ - def __init__(self, n_kernels=20, p=-0.25, q=0.25, j0=0.0, jn=60.0, **kwargs): + def __init__(self, mesh, n_kernels=20, p=-0.25, q=0.25, j0=0.0, jn=60.0, **kwargs): + self.mesh = mesh self.n_kernels = n_kernels self.p = p self.q = q @@ -842,6 +815,26 @@ def __init__(self, n_kernels=20, p=-0.25, q=0.25, j0=0.0, jn=60.0, **kwargs): self.jn = jn super(ExponentialSinusoidSimulation, self).__init__(**kwargs) + @property + def mesh(self): + """Mesh for the simulation. + + Returns + ------- + discretize.TensorMesh + Mesh on which the forward problem is discretized. + """ + return self._mesh + + @mesh.setter + def mesh(self, value): + value = validate_type("mesh", value, TensorMesh, cast=False) + if value.dim != 1: + raise ValueError( + f"{type(self).__name__} mesh must be 1D, received a {value.dim}D mesh." + ) + self._mesh = value + @property def n_kernels(self): r"""The number of kernel factors for the linear problem. diff --git a/simpeg/utils/code_utils.py b/simpeg/utils/code_utils.py index 5a08cc1039..e0aef08c5a 100644 --- a/simpeg/utils/code_utils.py +++ b/simpeg/utils/code_utils.py @@ -1116,21 +1116,42 @@ def validate_type(property_name, obj, obj_type, cast=True, strict=False): obj_type Returns the object in the specified type when validated """ + if not isinstance(obj_type, tuple): + obj_type = (obj_type,) + + if len(obj_type) > 1: + type_name = ( + ", ".join(cls.__qualname__ for cls in obj_type[:-1]) + + " or " + + obj_type[-1].__qualname__ + ) + else: + type_name = obj_type[0].__qualname__ + if cast: - try: - obj = obj_type(obj) - except Exception as err: + good_cast = False + err = None + for cls in obj_type: + try: + new_obj = cls(obj) + good_cast = True + except Exception as trial_error: + err = trial_error + if good_cast: + obj = new_obj + break + if not good_cast: raise TypeError( - f"{type(obj).__name__} cannot be converted to type {obj_type.__name__} " + f"{type(obj).__qualname__} cannot be converted to {type_name} " f"required for {property_name}." ) from err - if strict and type(obj) is not obj_type: + if strict and type(obj) not in obj_type: raise TypeError( - f"Object must be exactly a {obj_type.__name__} for {property_name}" + f"{property_name} must be exactly a {type_name}, not {type(obj).__qualname__}" ) if not isinstance(obj, obj_type): raise TypeError( - f"Object must be an instance of {obj_type.__name__} for {property_name}" + f"{property_name} must be an instance of {type_name}, not {type(obj).__qualname__}" ) return obj diff --git a/tests/base/test_Fields.py b/tests/base/test_Fields.py index 99fbf6c01c..0f06c586bd 100644 --- a/tests/base/test_Fields.py +++ b/tests/base/test_Fields.py @@ -35,7 +35,10 @@ def setUp(self): source_list = [Src0, Src1, Src2, Src3, Src4] mysurvey = survey.BaseSurvey(source_list=source_list) - sim = simulation.BaseSimulation(mesh=mesh, survey=mysurvey) + sim = simulation.BaseSimulation(survey=mysurvey) + # insert a mesh into the simulation (required for the Fields objects) + # This should likely be moved to a BasePDESimulation test. + sim.mesh = mesh self.D = data.Data(mysurvey) self.F = fields.Fields( sim, @@ -44,7 +47,6 @@ def setUp(self): ) self.Src0 = Src0 self.Src1 = Src1 - self.mesh = mesh self.XYZ = XYZ self.simulation = sim @@ -159,7 +161,10 @@ def setUp(self): source_list = [Src0, Src1, Src2, Src3, Src4] mysurvey = survey.BaseSurvey(source_list=source_list) - sim = simulation.BaseSimulation(mesh=mesh, survey=mysurvey) + sim = simulation.BaseSimulation(survey=mysurvey) + # insert a mesh into the simulation (required for the Fields objects) + # This should likely be moved to a BasePDESimulation test. + sim.mesh = mesh self.F = fields.Fields( sim, knownFields={"e": "E"}, @@ -167,7 +172,6 @@ def setUp(self): ) self.Src0 = Src0 self.Src1 = Src1 - self.mesh = mesh self.XYZ = XYZ self.simulation = sim @@ -250,14 +254,16 @@ def setUp(self): source_list = [Src0, Src1, Src2, Src3, Src4] mysurvey = survey.BaseSurvey(source_list=source_list) sim = simulation.BaseTimeSimulation( - mesh, time_steps=[(10.0, 3), (20.0, 2)], survey=mysurvey + time_steps=[(10.0, 3), (20.0, 2)], survey=mysurvey ) + # insert a mesh into the simulation (required for the Fields objects) + # This should likely be moved to a BasePDESimulation test. + sim.mesh = mesh self.F = fields.TimeFields( simulation=sim, knownFields={"phi": "CC", "e": "E", "b": "F"} ) self.Src0 = Src0 self.Src1 = Src1 - self.mesh = mesh self.XYZ = XYZ def test_contains(self): @@ -375,8 +381,11 @@ def setUp(self): source_list = [Src0, Src1, Src2, Src3, Src4] mysurvey = survey.BaseSurvey(source_list=source_list) sim = simulation.BaseTimeSimulation( - mesh, time_steps=[(10.0, 3), (20.0, 2)], survey=mysurvey + time_steps=[(10.0, 3), (20.0, 2)], survey=mysurvey ) + # insert a mesh into the simulation (required for the Fields objects) + # This should likely be moved to a BasePDESimulation test. + sim.mesh = mesh def alias(b, srcInd, timeInd): return self.F.mesh.edge_curl.T * b + timeInd @@ -386,7 +395,6 @@ def alias(b, srcInd, timeInd): ) self.Src0 = Src0 self.Src1 = Src1 - self.mesh = mesh self.XYZ = XYZ self.simulation = sim diff --git a/tests/base/test_base_pde_sim.py b/tests/base/test_base_pde_sim.py index 56a62b1538..5b23ec451f 100644 --- a/tests/base/test_base_pde_sim.py +++ b/tests/base/test_base_pde_sim.py @@ -830,3 +830,14 @@ def test_bad_solver(): msg = re.escape("str is not a subclass of pymatsolver.base.BaseSolver") with pytest.raises(TypeError, match=msg): BasePDESimulation(mesh, solver=str) + + +def test_mesh_required(): + with pytest.raises(TypeError): + BasePDESimulation() + + +def test_bad_mesh(): + with pytest.raises(TypeError): + # should error on anything besides a discretize.base.BaseMesh + BasePDESimulation(np.array([1, 2, 3])) diff --git a/tests/base/test_problem.py b/tests/base/test_problem.py index a6afd10479..e39608e774 100644 --- a/tests/base/test_problem.py +++ b/tests/base/test_problem.py @@ -1,13 +1,11 @@ import unittest -import discretize from simpeg import simulation import numpy as np class TestTimeSimulation(unittest.TestCase): def setUp(self): - mesh = discretize.TensorMesh([10, 10]) - self.sim = simulation.BaseTimeSimulation(mesh) + self.sim = simulation.BaseTimeSimulation() def test_timeProblem_setTimeSteps(self): self.sim.time_steps = [(1e-6, 3), 1e-5, (1e-4, 2)] diff --git a/tests/base/test_simulation.py b/tests/base/test_simulation.py index 4f49d4c9fa..12578951db 100644 --- a/tests/base/test_simulation.py +++ b/tests/base/test_simulation.py @@ -1,6 +1,9 @@ +import re import unittest import numpy as np import discretize +import pytest + from simpeg import maps, simulation @@ -25,10 +28,47 @@ def test_make_synthetic_data(self): assert np.all(data.relative_error == 0.05 * np.ones_like(dclean)) +def test_exp_sim_mesh_required(): + msg = ".*missing 1 required positional argument: 'mesh'" + with pytest.raises(TypeError, match=msg): + simulation.ExponentialSinusoidSimulation() + + +def test_exp_sim_bad_mesh_type(): + mesh = discretize.TreeMesh([16, 16]) + msg = "mesh must be an instance of TensorMesh, not TreeMesh" + with pytest.raises(TypeError, match=msg): + simulation.ExponentialSinusoidSimulation(mesh) + + +def test_exp_sim_bad_mesh_dim(): + mesh = discretize.TensorMesh([5, 5]) + msg = "ExponentialSinusoidSimulation mesh must be 1D, received a 2D mesh." + with pytest.raises(ValueError, match=msg): + simulation.ExponentialSinusoidSimulation(mesh) + + +@pytest.mark.parametrize( + "base_class", + [ + simulation.BaseSimulation, + simulation.BaseTimeSimulation, + simulation.LinearSimulation, + ], +) +def test_base_no_mesh(base_class): + # this current message is... not a good one + # # but is what is thrown when an invalid arugment is passed. + msg = re.escape( + "object.__init__() takes exactly one argument (the instance to initialize)" + ) + with pytest.raises(TypeError, match=msg): + base_class(mesh=0) + + class TestTimeSimulation(unittest.TestCase): def setUp(self): - mesh = discretize.TensorMesh([10, 10]) - self.sim = simulation.BaseTimeSimulation(mesh=mesh) + self.sim = simulation.BaseTimeSimulation() def test_time_simulation_time_steps(self): self.sim.time_steps = [(1e-6, 3), 1e-5, (1e-4, 2)] diff --git a/tests/em/static/test_DC_2D_analytic.py b/tests/em/static/test_DC_2D_analytic.py index 22889d5cf2..c53fce458b 100644 --- a/tests/em/static/test_DC_2D_analytic.py +++ b/tests/em/static/test_DC_2D_analytic.py @@ -1,6 +1,8 @@ +import discretize import numpy as np import unittest +import pytest from discretize import TensorMesh from simpeg import utils @@ -428,5 +430,12 @@ def test_Simulation2DCellCentered(self, tolerance=0.05): self.assertLess(err, tolerance) +def test_bad_mesh_dim(): + mesh = discretize.TensorMesh([3, 3, 3]) + msg = "Simulation2DNodal mesh must be 2D, received a 3D mesh." + with pytest.raises(ValueError, match=msg): + dc.Simulation2DNodal(mesh) + + if __name__ == "__main__": unittest.main() diff --git a/tests/em/vrm/test_vrm_instantiation.py b/tests/em/vrm/test_vrm_instantiation.py new file mode 100644 index 0000000000..91c6af90e0 --- /dev/null +++ b/tests/em/vrm/test_vrm_instantiation.py @@ -0,0 +1,27 @@ +import discretize +import pytest +import simpeg.electromagnetics.viscous_remanent_magnetization as vrm + + +def test_mesh_required(): + with pytest.raises( + TypeError, match=".*missing 1 required positional argument: 'mesh'" + ): + vrm.Simulation3DLinear() + + +def test_bad_mesh_type(): + mesh = discretize.CylindricalMesh([3, 3, 3]) + with pytest.raises( + TypeError, + match="mesh must be an instance of TensorMesh or TreeMesh, not CylindricalMesh", + ): + vrm.Simulation3DLinear(mesh) + + +def test_bad_mesh_dim(): + mesh = discretize.TensorMesh([3, 3]) + with pytest.raises( + ValueError, match="Simulation3DLinear mesh must be 3D, received a 2D mesh." + ): + vrm.Simulation3DLinear(mesh) diff --git a/tests/flow/test_Richards.py b/tests/flow/test_Richards.py index 39340185de..5cab334437 100644 --- a/tests/flow/test_Richards.py +++ b/tests/flow/test_Richards.py @@ -1,5 +1,6 @@ import unittest import numpy as np +import pytest from discretize.tests import check_derivative import discretize @@ -285,5 +286,21 @@ def test_sensitivity_full(self): self._dotest_sensitivity_full() +def test_bad_mesh_type(): + mesh = discretize.CylindricalMesh([3, 3, 3]) + params = richards.empirical.HaverkampParams().celia1990 + k_fun, theta_fun = richards.empirical.haverkamp(mesh, **params) + + msg = "mesh must be an instance of TensorMesh or TreeMesh, not CylindricalMesh" + with pytest.raises(TypeError, match=msg): + richards.SimulationNDCellCentered( + mesh, + hydraulic_conductivity=k_fun, + water_retention=theta_fun, + boundary_conditions=np.array([1.0]), + initial_conditions=np.array([1.0]), + ) + + if __name__ == "__main__": unittest.main() diff --git a/tests/flow/test_Richards_empirical.py b/tests/flow/test_Richards_empirical.py index d50ae8af19..1f5e8f9178 100644 --- a/tests/flow/test_Richards_empirical.py +++ b/tests/flow/test_Richards_empirical.py @@ -1,4 +1,5 @@ import unittest +import pytest import numpy as np @@ -219,5 +220,23 @@ def fun(m): self.assertTrue(passed, True) +@pytest.mark.parametrize( + "empirical_class", + [ + richards.empirical.NonLinearModel, + richards.empirical.BaseWaterRetention, + richards.empirical.BaseHydraulicConductivity, + richards.empirical.Haverkamp_theta, + richards.empirical.Haverkamp_k, + richards.empirical.Vangenuchten_theta, + richards.empirical.Vangenuchten_k, + ], +) +def test_bad_mesh_type(empirical_class): + msg = "mesh must be an instance of BaseMesh, not ndarray" + with pytest.raises(TypeError, match=msg): + empirical_class(np.array([1, 2, 3])) + + if __name__ == "__main__": unittest.main() diff --git a/tests/pf/test_base_pf_simulation.py b/tests/pf/test_base_pf_simulation.py index 9d482a20f7..fc05eafdca 100644 --- a/tests/pf/test_base_pf_simulation.py +++ b/tests/pf/test_base_pf_simulation.py @@ -2,6 +2,7 @@ Test BasePFSimulation class """ +import re import pytest import numpy as np from discretize import CylindricalMesh, TensorMesh, TreeMesh @@ -93,7 +94,9 @@ def test_sensitivity_path_as_dir(self, tensor_mesh, mock_simulation_class, tmpdi ``store_sensitivities="disk"``. """ sensitivity_path = str(tmpdir.mkdir("sensitivities")) - msg = f"The passed sensitivity_path '{sensitivity_path}' is a directory." + msg = re.escape( + f"The passed sensitivity_path '{sensitivity_path}' is a directory." + ) with pytest.raises(ValueError, match=msg): mock_simulation_class( tensor_mesh, @@ -108,19 +111,6 @@ class TestGetActiveNodes: Tests _get_active_nodes private method """ - def test_invalid_mesh(self, tensor_mesh, mock_simulation_class): - """ - Test error on invalid mesh class - """ - # Initialize base simulation with valid mesh (so we don't trigger - # errors in the constructor) - simulation = mock_simulation_class(tensor_mesh) - # Assign an invalid mesh to the simulation - simulation.mesh = CylindricalMesh(tensor_mesh.h) - msg = "Invalid mesh of type CylindricalMesh." - with pytest.raises(TypeError, match=msg): - simulation._get_active_nodes() - def test_no_inactive_cells_tensor(self, tensor_mesh, mock_simulation_class): """ Test _get_active_nodes when all cells are active on a tensor mesh @@ -281,9 +271,13 @@ def test_components_and_receivers_magnetics( np.testing.assert_equal(receivers, receiver_locations) -class TestInvalidMeshChoclo: +class TestInvalidMesh: + """ + Test if errors are raised after invalid mesh are passed to the base simulation. + """ + @pytest.fixture(params=("tensormesh", "treemesh")) - def mesh(self, request): + def mesh_2d(self, request): """Sample 2D mesh.""" hx, hy = [(0.1, 8)], [(0.1, 8)] h = (hx, hy) @@ -294,16 +288,25 @@ def mesh(self, request): mesh.finalize() return mesh - def test_invalid_mesh_with_choclo(self, mesh, mock_simulation_class): + @pytest.mark.parametrize("engine", ("choclo", "geoana")) + def test_invalid_mesh_dimensions(self, mesh_2d, mock_simulation_class, engine): """ - Test if simulation raises error when passing an invalid mesh and using choclo + Test error when passing a mesh with invalid dimensions. """ - msg = ( - "Invalid mesh with 2 dimensions. " - "Only 3D meshes are supported when using 'choclo' as engine." - ) + msg = re.escape("MockSimulation mesh must be 3D, received a 2D mesh.") with pytest.raises(ValueError, match=msg): - mock_simulation_class(mesh, engine="choclo") + mock_simulation_class(mesh_2d, engine=engine) + + def test_invalid_mesh_type(self, mock_simulation_class): + """ + Test error when passing an invalid mesh class. + """ + h = (3, 3, 3) + msg = re.escape( + "mesh must be an instance of TensorMesh or TreeMesh, not CylindricalMesh" + ) + with pytest.raises(TypeError, match=msg): + mock_simulation_class(CylindricalMesh(h)) class TestDeprecationIndActive: diff --git a/tests/pf/test_equivalent_sources.py b/tests/pf/test_equivalent_sources.py index 12880ea3f3..84339d8db4 100644 --- a/tests/pf/test_equivalent_sources.py +++ b/tests/pf/test_equivalent_sources.py @@ -210,8 +210,8 @@ def test_error_on_gravity(self, mesh_3d, engine): """ Test error is raised after passing a 3D mesh to gravity eq source class. """ - msg = "Mesh to equivalent source layer must be 2D." - with pytest.raises(AttributeError, match=msg): + msg = "SimulationEquivalentSourceLayer mesh must be 2D, received a 3D mesh." + with pytest.raises(ValueError, match=msg): gravity.SimulationEquivalentSourceLayer( mesh=mesh_3d, cell_z_top=0.0, cell_z_bottom=-2.0, engine=engine ) @@ -221,8 +221,8 @@ def test_error_on_mag(self, mesh_3d, engine): """ Test error is raised after passing a 3D mesh to magnetic eq source class. """ - msg = "Mesh to equivalent source layer must be 2D." - with pytest.raises(AttributeError, match=msg): + msg = "SimulationEquivalentSourceLayer mesh must be 2D, received a 3D mesh." + with pytest.raises(ValueError, match=msg): magnetics.SimulationEquivalentSourceLayer( mesh=mesh_3d, cell_z_top=0.0, cell_z_bottom=-2.0, engine=engine ) @@ -231,40 +231,12 @@ def test_error_on_base_class(self, mesh_3d): """ Test error is raised after passing a 3D mesh to the eq source base class. """ - msg = "Mesh to equivalent source layer must be 2D." - with pytest.raises(AttributeError, match=msg): + msg = "BaseEquivalentSourceLayerSimulation mesh must be 2D, received a 3D mesh." + with pytest.raises(ValueError, match=msg): base.BaseEquivalentSourceLayerSimulation( mesh=mesh_3d, cell_z_top=0.0, cell_z_bottom=-2.0 ) - @pytest.mark.parametrize( - "eq_sources_class", - ( - gravity.SimulationEquivalentSourceLayer, - magnetics.SimulationEquivalentSourceLayer, - ), - ) - def test_overridden_method( - self, mesh_3d, tensor_mesh, mesh_top, mesh_bottom, eq_sources_class - ): - """ - Test for the overridden _check_engine_and_mesh_dimensions method. - - This method is rarely going to trigger an error because if a 3D mesh is - passed to the constructor, it'll catch it and raise an error. - This test is added to extend coverage. - """ - # Initialize instance with a 2D mesh - eq_sources = eq_sources_class( - mesh=tensor_mesh, cell_z_top=mesh_top, cell_z_bottom=mesh_bottom - ) - # Set the mesh to the 3D mesh - eq_sources.mesh = mesh_3d - # Check error in _check_engine_and_mesh_dimensions method - msg = "Mesh to equivalent source layer must be 2D." - with pytest.raises(AttributeError, match=msg): - eq_sources._check_engine_and_mesh_dimensions() - class TestGravityEquivalentSourcesForward: """ diff --git a/tests/pf/test_forward_Grav_Linear.py b/tests/pf/test_forward_Grav_Linear.py index 0b27f43e04..063fff9904 100644 --- a/tests/pf/test_forward_Grav_Linear.py +++ b/tests/pf/test_forward_Grav_Linear.py @@ -1,3 +1,5 @@ +import re + import pytest import discretize import simpeg @@ -351,7 +353,9 @@ def test_choclo_and_sensitivity_path_as_dir(self, simple_mesh, tmp_path): sensitivity_path = tmp_path / "sensitivity_dummy" sensitivity_path.mkdir() # Check if error is raised - msg = f"The passed sensitivity_path '{str(sensitivity_path)}' is a directory" + msg = re.escape( + f"The passed sensitivity_path '{str(sensitivity_path)}' is a directory" + ) with pytest.raises(ValueError, match=msg): gravity.Simulation3DIntegral( simple_mesh, @@ -437,34 +441,3 @@ def test_invalid_conversion_factor(self): component = "invalid-component" with pytest.raises(ValueError, match=f"Invalid component '{component}'"): gravity.simulation._get_conversion_factor(component) - - -class TestInvalidMeshChoclo: - @pytest.fixture(params=("tensormesh", "treemesh")) - def mesh(self, request): - """Sample 2D mesh.""" - hx, hy = [(0.1, 8)], [(0.1, 8)] - h = (hx, hy) - if request.param == "tensormesh": - mesh = discretize.TensorMesh(h, "CC") - else: - mesh = discretize.TreeMesh(h, origin="CC") - mesh.finalize() - return mesh - - def test_invalid_mesh_with_choclo(self, mesh): - """ - Test if simulation raises error when passing an invalid mesh and using choclo - """ - # Build survey - receivers_locations = np.array([[0, 0, 0]]) - receivers = gravity.Point(receivers_locations) - sources = gravity.SourceField([receivers]) - survey = gravity.Survey(sources) - # Check if error is raised - msg = ( - "Invalid mesh with 2 dimensions. " - "Only 3D meshes are supported when using 'choclo' as engine." - ) - with pytest.raises(ValueError, match=msg): - gravity.Simulation3DIntegral(mesh, survey, engine="choclo") diff --git a/tests/pf/test_forward_Mag_Linear.py b/tests/pf/test_forward_Mag_Linear.py index b4050a8085..596813ff09 100644 --- a/tests/pf/test_forward_Mag_Linear.py +++ b/tests/pf/test_forward_Mag_Linear.py @@ -1,4 +1,7 @@ from __future__ import annotations + +import re + import discretize import numpy as np import pytest @@ -821,7 +824,9 @@ def test_choclo_and_sensitivity_path_as_dir(self, mag_mesh, tmp_path): sensitivity_path = tmp_path / "sensitivity_dummy" sensitivity_path.mkdir() # Check if error is raised - msg = f"The passed sensitivity_path '{str(sensitivity_path)}' is a directory" + msg = re.escape( + f"The passed sensitivity_path '{str(sensitivity_path)}' is a directory" + ) with pytest.raises(ValueError, match=msg): mag.Simulation3DIntegral( mag_mesh, @@ -887,42 +892,6 @@ def test_choclo_missing(self, mag_mesh, monkeypatch): mag.Simulation3DIntegral(mag_mesh, engine="choclo") -class TestInvalidMeshChoclo: - @pytest.fixture(params=("tensormesh", "treemesh")) - def mesh(self, request): - """Sample 2D mesh.""" - hx, hy = [(0.1, 8)], [(0.1, 8)] - h = (hx, hy) - if request.param == "tensormesh": - mesh = discretize.TensorMesh(h, "CC") - else: - mesh = discretize.TreeMesh(h, origin="CC") - mesh.finalize() - return mesh - - def test_invalid_mesh_with_choclo(self, mesh): - """ - Test if simulation raises error when passing an invalid mesh and using choclo - """ - # Build survey - receivers_locations = np.array([[0, 0, 0]]) - receivers = mag.Point(receivers_locations) - sources = mag.UniformBackgroundField( - receiver_list=[receivers], - amplitude=50_000, - inclination=45.0, - declination=12.0, - ) - survey = mag.Survey(sources) - # Check if error is raised - msg = ( - "Invalid mesh with 2 dimensions. " - "Only 3D meshes are supported when using 'choclo' as engine." - ) - with pytest.raises(ValueError, match=msg): - mag.Simulation3DIntegral(mesh, survey, engine="choclo") - - def test_removed_modeltype(): """Test if accesing removed modelType property raises error.""" h = [[(2, 2)], [(2, 2)], [(2, 2)]] diff --git a/tests/pf/test_pf_quadtree_inversion_linear.py b/tests/pf/test_pf_quadtree_inversion_linear.py index 37848db2e7..32450621cd 100644 --- a/tests/pf/test_pf_quadtree_inversion_linear.py +++ b/tests/pf/test_pf_quadtree_inversion_linear.py @@ -420,7 +420,7 @@ def create_xyz_points_flat(x_range, y_range, spacing, altitude=0.0): grav_survey = gravity.Survey(grav_srcField) self.assertRaises( - AttributeError, + ValueError, gravity.SimulationEquivalentSourceLayer, mesh3D, 0.0, diff --git a/tests/seis/test_tomo.py b/tests/seis/test_tomo.py index 081b4bf3d6..d9aca46768 100644 --- a/tests/seis/test_tomo.py +++ b/tests/seis/test_tomo.py @@ -1,9 +1,14 @@ +import re + import numpy as np import unittest import discretize +import pytest + from simpeg.seismic import straight_ray_tomography as tomo from simpeg import tests, maps, utils +from simpeg.seismic.straight_ray_tomography.simulation import Simulation2DIntegral TOL = 1e-5 FLR = 1e-14 @@ -40,5 +45,25 @@ def fun(x): ) +def test_required_mesh_arg(): + msg = ".*missing 1 required positional argument: 'mesh'" + with pytest.raises(TypeError, match=msg): + Simulation2DIntegral() + + +def test_bad_mesh_type(): + mesh = discretize.CylindricalMesh([3, 3, 3]) + msg = "mesh must be an instance of TensorMesh, not CylindricalMesh" + with pytest.raises(TypeError, match=msg): + Simulation2DIntegral(mesh) + + +def test_bad_mesh_dim(): + mesh = discretize.TensorMesh([3, 3, 3]) + msg = re.escape("Simulation2DIntegral mesh must be 2D, received a 3D mesh.") + with pytest.raises(ValueError, match=msg): + Simulation2DIntegral(mesh) + + if __name__ == "__main__": unittest.main() diff --git a/tests/base/test_validators.py b/tests/utils/test_validators.py similarity index 91% rename from tests/base/test_validators.py rename to tests/utils/test_validators.py index c80f8d59a4..2a6d1bc0dd 100644 --- a/tests/base/test_validators.py +++ b/tests/utils/test_validators.py @@ -307,31 +307,66 @@ def test_ndarray_validation(): ) -def test_type_validation(): +def test_type_validation_casting(): # should try to cast to type assert type(validate_type("type_prop", 4.0, int)) == int + # should be okay if only able to cast to one of the possible types + assert type(validate_type("type_prop", "four", (float, str))) == str + + # should error if unable to cast to single type + with pytest.raises(TypeError): + validate_type("type_prop", "four", float) + # isinstance without casting should pass through assert type(validate_type("type_prop", 4.0, object, cast=False)) == float - # should return object without casting if isinstance - assert type(validate_type("type_prop", True, int, cast=False)) == bool + # should error if unable to cast to multiple types + with pytest.raises(TypeError): + validate_type("type_prop", "four", (float, int, complex)) + + +def test_type_validation_strict(): # strict type checking - assert type(validate_type("type_prop", 4.0, float, strict=True)) == float + assert ( + type(validate_type("type_prop", 4.0, float, cast=False, strict=True)) == float + ) - # should error if unable to cast to type - with pytest.raises(TypeError): - validate_type("type_prop", "four", float) + # should ok if strict and is exact of one of the classes + assert ( + type( + validate_type( + "type_prop", True, (int, float, bool), cast=False, strict=True + ) + ) + == bool + ) # should error if strict and not an exact same class with pytest.raises(TypeError): validate_type("type_prop", True, int, cast=False, strict=True) + # should error if strict and not any class + with pytest.raises(TypeError): + validate_type("type_prop", True, (int, float, complex), cast=False, strict=True) + + +def test_type_validation_subclasses(): + + # should return object without casting if isinstance + assert type(validate_type("type_prop", True, int, cast=False)) == bool + + # ok if object without casting isinstance of one of multiple classes + assert type(validate_type("type_prop", True, (int, float, str), cast=False)) == bool + # should error if not strict and not subclass with pytest.raises(TypeError): validate_type("type_prop", True, float, cast=False) + with pytest.raises(TypeError): + validate_type("type_prop", True, (float, complex, str), cast=False) + def test_callable_validation(): def func(x): From 84719dcee7d8b121e6470297beccff1c18138d05 Mon Sep 17 00:00:00 2001 From: John Weis <49649694+johnweis0480@users.noreply.github.com> Date: Mon, 25 Nov 2024 13:06:28 -0800 Subject: [PATCH 099/194] Speed up most commonly used deriv/deriv2 in PGI (#1587) Speed up dot products in the derivative methods of the PGI regularization classes by making use of Numpy functions instead of for loops that perform a significant number of iterations. --- simpeg/regularization/pgi.py | 39 ++++++++++++------------------------ 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/simpeg/regularization/pgi.py b/simpeg/regularization/pgi.py index 762f670cad..04d632d9fe 100644 --- a/simpeg/regularization/pgi.py +++ b/simpeg/regularization/pgi.py @@ -567,25 +567,17 @@ def deriv(self, m): if self.non_linear_relationships: raise Exception("Not implemented") - r = mkvc( - np.r_[[np.dot(self.gmm.precisions_, r0[i]) for i in range(len(r0))]] - ) + r = mkvc(np.dot(self.gmm.precisions_, r0.T).T) elif ( self.gmm.covariance_type == "diag" or self.gmm.covariance_type == "spherical" ) and not self.non_linear_relationships: - r = mkvc( - np.r_[ - [ - np.dot( - self.gmm.precisions_[membership[i]] - * np.eye(len(self.wiresmap.maps)), - r0[i], - ) - for i in range(len(r0)) - ] - ] - ) + r = np.zeros_like(r0) + for i in range(self.gmm.n_components): + selection = membership == i + r[selection] = r0[selection] * self.gmm.precisions_[i].T + r = mkvc(r) + else: if self.non_linear_relationships: r = mkvc( @@ -606,14 +598,11 @@ def deriv(self, m): else: r0 = (self.W * (mkvc(dm))).reshape(dm.shape, order="F") - r = mkvc( - np.r_[ - [ - np.dot(self.gmm.precisions_[membership[i]], r0[i]) - for i in range(len(r0)) - ] - ] - ) + r = np.zeros_like(r0) + for i in range(self.gmm.n_components): + selection = membership == i + r[selection] = (self.gmm.precisions_[i] @ r0[selection].T).T + r = mkvc(r) return 2 * mkvc(mD.T * (self.W.T * r)) else: @@ -840,9 +829,7 @@ def deriv2(self, m, v=None): mDv = np.c_[mDv] r0 = (self.W * (mkvc(mDv))).reshape(mDv.shape, order="F") second_deriv_times_r0 = mkvc( - np.r_[ - [np.dot(self._r_second_deriv[i], r0[i]) for i in range(len(r0))] - ] + np.einsum("ijk,ik->ij", self._r_second_deriv, r0) ) return 2 * mkvc(mD.T * (self.W * second_deriv_times_r0)) else: From 54db0a8dffc07e57d6b0eaa1b2a1e5574c8cc45b Mon Sep 17 00:00:00 2001 From: John Weis <49649694+johnweis0480@users.noreply.github.com> Date: Tue, 26 Nov 2024 11:31:30 -0800 Subject: [PATCH 100/194] Improve dot products in `PGIsmallness.__call__` and update docstring (#1588) Change the `__call__` method in PGI smallness to avoid for loops through numpy arrays. Fix the docstring to reflect that physical properties can still be correlated if `approx_eval=True` and `approx_gradient=True`. --- simpeg/regularization/pgi.py | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/simpeg/regularization/pgi.py b/simpeg/regularization/pgi.py index 04d632d9fe..dbc059eaa6 100644 --- a/simpeg/regularization/pgi.py +++ b/simpeg/regularization/pgi.py @@ -70,10 +70,10 @@ class PGIsmallness(Smallness): ``tensor``, ``QuadTree`` or ``Octree`` meshes. approx_gradient : bool If ``True``, use the L2-approximation of the gradient by assuming - physical property values of different types are uncorrelated. + the physical property distributions of each geologic units are distinct approx_eval : bool If ``True``, use the L2-approximation evaluation of the smallness term by assuming - physical property values of different types are uncorrelated. + the physical property distributions of each geologic units are distinct approx_hessian : bool Approximate the Hessian of the regularization function. non_linear_relationship : bool @@ -470,30 +470,22 @@ def __call__(self, m, external_weights=True): r0 = (W * mkvc(dmr)).reshape(dmr.shape, order="F") if self.gmm.covariance_type == "tied": - r1 = np.r_[ - [np.dot(self.gmm.precisions_, np.r_[r0[i]]) for i in range(len(r0))] - ] + r1 = mkvc(np.dot(self.gmm.precisions_, r0.T).T) elif ( self.gmm.covariance_type == "diag" or self.gmm.covariance_type == "spherical" ): - r1 = np.r_[ - [ - np.dot( - self.gmm.precisions_[membership[i]] - * np.eye(len(self.wiresmap.maps)), - np.r_[r0[i]], - ) - for i in range(len(r0)) - ] - ] + r1 = np.zeros_like(r0) + for i in range(self.gmm.n_components): + selection = membership == i + r1[selection] = r0[selection] * self.gmm.precisions_[i].T + r1 = mkvc(r1) else: - r1 = np.r_[ - [ - np.dot(self.gmm.precisions_[membership[i]], np.r_[r0[i]]) - for i in range(len(r0)) - ] - ] + r1 = np.zeros_like(r0) + for i in range(self.gmm.n_components): + selection = membership == i + r1[selection] = (self.gmm.precisions_[i] @ r0[selection].T).T + r1 = mkvc(r1) return mkvc(r0).dot(mkvc(r1)) From da9e2844ab00d445881e5c3e21134cbc3e989ff9 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Wed, 4 Dec 2024 16:35:22 -0700 Subject: [PATCH 101/194] Rename delete on model update (#1589) #### Summary Privatizes the `deleteOnModelUpdate` property, and unify with the `clean_on_model_update`. #### Reference issue Working towards #1429 #### What does this implement/fix? The `deleteOnModelUpdate` property is not needed to be public, this privatizes it. It also removes the somewhat duplicated purpose of the `clean_on_model_change` property, as we should rely on the solvers to clean themselves up properly upon deletion instead of manually triggering it. --- simpeg/base/pde_simulation.py | 8 +- simpeg/electromagnetics/base_1d.py | 4 +- .../frequency_domain/simulation.py | 4 +- .../natural_source/simulation.py | 8 +- .../natural_source/simulation_1d.py | 4 +- .../static/induced_polarization/simulation.py | 2 +- .../static/resistivity/simulation.py | 4 +- .../static/resistivity/simulation_1d.py | 4 +- .../static/resistivity/simulation_2d.py | 4 +- .../static/self_potential/simulation.py | 4 +- .../simulation.py | 2 +- .../time_domain/simulation.py | 9 +- simpeg/inverse_problem.py | 4 +- simpeg/meta/simulation.py | 4 +- .../potential_fields/magnetics/simulation.py | 4 +- simpeg/props.py | 33 ++--- tests/base/props/test_has_model.py | 115 ++++++++++++++++++ .../test_prop_relationships.py} | 0 tests/base/test_base_pde_sim.py | 4 +- tests/em/static/test_SPjvecjtvecadj.py | 6 +- 20 files changed, 174 insertions(+), 53 deletions(-) create mode 100644 tests/base/props/test_has_model.py rename tests/base/{test_Props.py => props/test_prop_relationships.py} (100%) diff --git a/simpeg/base/pde_simulation.py b/simpeg/base/pde_simulation.py index 32ba694d5e..d0cdb46f1f 100644 --- a/simpeg/base/pde_simulation.py +++ b/simpeg/base/pde_simulation.py @@ -615,11 +615,11 @@ def __init__( self.rhoMap = rhoMap @property - def deleteTheseOnModelUpdate(self): + def _delete_on_model_update(self): """ matrices to be deleted if the model for conductivity/resistivity is updated """ - toDelete = super().deleteTheseOnModelUpdate + toDelete = super()._delete_on_model_update if self.sigmaMap is not None or self.rhoMap is not None: toDelete = ( toDelete + self._clear_on_sigma_update + self._clear_on_rho_update @@ -658,11 +658,11 @@ def __setattr__(self, name, value): delattr(self, mat) @property - def deleteTheseOnModelUpdate(self): + def _delete_on_model_update(self): """ items to be deleted if the model for Magnetic Permeability is updated """ - toDelete = super().deleteTheseOnModelUpdate + toDelete = super()._delete_on_model_update if self.muMap is not None or self.muiMap is not None: toDelete = toDelete + self._clear_on_mu_update + self._clear_on_mui_update return toDelete diff --git a/simpeg/electromagnetics/base_1d.py b/simpeg/electromagnetics/base_1d.py index e29802214f..a871d56bfc 100644 --- a/simpeg/electromagnetics/base_1d.py +++ b/simpeg/electromagnetics/base_1d.py @@ -576,8 +576,8 @@ def _compute_hankel_coefficients(self): self._W = self._W.tocsr() @property - def deleteTheseOnModelUpdate(self): - toDelete = super().deleteTheseOnModelUpdate + def _delete_on_model_update(self): + toDelete = super()._delete_on_model_update if self.fix_Jmatrix is False: toDelete += ["_J", "_gtgdiag"] return toDelete diff --git a/simpeg/electromagnetics/frequency_domain/simulation.py b/simpeg/electromagnetics/frequency_domain/simulation.py index 55ad162e71..bb3f68fc8c 100644 --- a/simpeg/electromagnetics/frequency_domain/simulation.py +++ b/simpeg/electromagnetics/frequency_domain/simulation.py @@ -486,7 +486,7 @@ def getSourceTerm(self, freq): return s_m, s_e @property - def deleteTheseOnModelUpdate(self): + def _delete_on_model_update(self): """List of model-dependent attributes to clean upon model update. Some of the FDEM simulation's attributes are model-dependent. This property specifies @@ -497,7 +497,7 @@ def deleteTheseOnModelUpdate(self): list of str List of the model-dependent attributes to clean upon model update. """ - toDelete = super().deleteTheseOnModelUpdate + toDelete = super()._delete_on_model_update return toDelete + ["_Jmatrix", "_gtgdiag"] diff --git a/simpeg/electromagnetics/natural_source/simulation.py b/simpeg/electromagnetics/natural_source/simulation.py index cd531529a2..651a347957 100644 --- a/simpeg/electromagnetics/natural_source/simulation.py +++ b/simpeg/electromagnetics/natural_source/simulation.py @@ -474,8 +474,8 @@ def boundary_fields(self, model=None): return self._boundary_fields @property - def deleteTheseOnModelUpdate(self): - items = super().deleteTheseOnModelUpdate + def _delete_on_model_update(self): + items = super()._delete_on_model_update items.append("_boundary_fields") return items @@ -696,8 +696,8 @@ def boundary_fields(self, model=None): return self._boundary_fields @property - def deleteTheseOnModelUpdate(self): - items = super().deleteTheseOnModelUpdate + def _delete_on_model_update(self): + items = super()._delete_on_model_update items.append("_boundary_fields") return items diff --git a/simpeg/electromagnetics/natural_source/simulation_1d.py b/simpeg/electromagnetics/natural_source/simulation_1d.py index bbadaad775..424c39c9d7 100644 --- a/simpeg/electromagnetics/natural_source/simulation_1d.py +++ b/simpeg/electromagnetics/natural_source/simulation_1d.py @@ -352,8 +352,8 @@ def Jtvec(self, m, v, f=None): return JTvec @property - def deleteTheseOnModelUpdate(self): - toDelete = super().deleteTheseOnModelUpdate + def _delete_on_model_update(self): + toDelete = super()._delete_on_model_update if self.fix_Jmatrix: return toDelete else: diff --git a/simpeg/electromagnetics/static/induced_polarization/simulation.py b/simpeg/electromagnetics/static/induced_polarization/simulation.py index cd598bcb9e..321c86c0ef 100644 --- a/simpeg/electromagnetics/static/induced_polarization/simulation.py +++ b/simpeg/electromagnetics/static/induced_polarization/simulation.py @@ -132,7 +132,7 @@ def Jtvec(self, m, v, f=None): return super().Jtvec(m, v * self._scale, f) @property - def deleteTheseOnModelUpdate(self): + def _delete_on_model_update(self): toDelete = [] return toDelete diff --git a/simpeg/electromagnetics/static/resistivity/simulation.py b/simpeg/electromagnetics/static/resistivity/simulation.py index 2a78b6a17b..476488a8a6 100644 --- a/simpeg/electromagnetics/static/resistivity/simulation.py +++ b/simpeg/electromagnetics/static/resistivity/simulation.py @@ -284,8 +284,8 @@ def getSourceTerm(self): return self._q @property - def deleteTheseOnModelUpdate(self): - toDelete = super().deleteTheseOnModelUpdate + def _delete_on_model_update(self): + toDelete = super()._delete_on_model_update return toDelete + ["_Jmatrix", "_gtgdiag"] def _mini_survey_data(self, d_mini): diff --git a/simpeg/electromagnetics/static/resistivity/simulation_1d.py b/simpeg/electromagnetics/static/resistivity/simulation_1d.py index b57ea3591e..85d72bd2fd 100644 --- a/simpeg/electromagnetics/static/resistivity/simulation_1d.py +++ b/simpeg/electromagnetics/static/resistivity/simulation_1d.py @@ -340,8 +340,8 @@ def Jtvec(self, m, v, f=None): return self.getJ(m, f=f).T @ v @property - def deleteTheseOnModelUpdate(self): - to_delete = super().deleteTheseOnModelUpdate + def _delete_on_model_update(self): + to_delete = super()._delete_on_model_update if not self.fix_Jmatrix: to_delete = to_delete + ["_Jmatrix"] return to_delete diff --git a/simpeg/electromagnetics/static/resistivity/simulation_2d.py b/simpeg/electromagnetics/static/resistivity/simulation_2d.py index 1dab7d3da4..b37158343e 100644 --- a/simpeg/electromagnetics/static/resistivity/simulation_2d.py +++ b/simpeg/electromagnetics/static/resistivity/simulation_2d.py @@ -445,8 +445,8 @@ def getSourceTerm(self, ky): return q @property - def deleteTheseOnModelUpdate(self): - toDelete = super().deleteTheseOnModelUpdate + def _delete_on_model_update(self): + toDelete = super()._delete_on_model_update if self.fix_Jmatrix: return toDelete return toDelete + ["_Jmatrix"] diff --git a/simpeg/electromagnetics/static/self_potential/simulation.py b/simpeg/electromagnetics/static/self_potential/simulation.py index 2ee621464f..05a657ad11 100644 --- a/simpeg/electromagnetics/static/self_potential/simulation.py +++ b/simpeg/electromagnetics/static/self_potential/simulation.py @@ -78,10 +78,10 @@ def getRHSDeriv(self, source, v, adjoint=False): return self.Vol @ (self.qDeriv @ v) @property - def deleteTheseOnModelUpdate(self): + def _delete_on_model_update(self): # When enabling resistivity derivatives, uncomment these lines # if self.rhoMap is not None: - # return super().deleteTheseOnModelUpdate + # return super()._delete_on_model_update if self.storeJ and self.qMap is not None and not self.qMap.is_linear: return ["_Jmatrix", "_gtgdiag"] return [] diff --git a/simpeg/electromagnetics/static/spectral_induced_polarization/simulation.py b/simpeg/electromagnetics/static/spectral_induced_polarization/simulation.py index 04263d3197..5abc4b4e8f 100644 --- a/simpeg/electromagnetics/static/spectral_induced_polarization/simulation.py +++ b/simpeg/electromagnetics/static/spectral_induced_polarization/simulation.py @@ -603,7 +603,7 @@ def Jtvec(self, m, v, f=None): return Jtv @property - def deleteTheseOnModelUpdate(self): + def _delete_on_model_update(self): toDelete = [ "_etaDeriv_store", "_tauiDeriv_store", diff --git a/simpeg/electromagnetics/time_domain/simulation.py b/simpeg/electromagnetics/time_domain/simulation.py index 31720a94c5..c896e3d9d3 100644 --- a/simpeg/electromagnetics/time_domain/simulation.py +++ b/simpeg/electromagnetics/time_domain/simulation.py @@ -608,7 +608,7 @@ def Adcinv(self): return self._Adcinv @property - def clean_on_model_update(self): + def _delete_on_model_update(self): """List of model-dependent attributes to clean upon model update. Some of the TDEM simulation's attributes are model-dependent. This property specifies @@ -619,8 +619,11 @@ def clean_on_model_update(self): list of str List of the model-dependent attributes to clean upon model update. """ - items = super().clean_on_model_update - return items + ["_Adcinv"] #: clear DC matrix factors on any model updates + items = super()._delete_on_model_update + if self.sigmaMap is not None: + items = items + ["_Adcinv"] #: clear DC matrix factors on any model updates + # if there is a sigmaMap + return items ############################################################################### diff --git a/simpeg/inverse_problem.py b/simpeg/inverse_problem.py index df53169ab3..affff7cd73 100644 --- a/simpeg/inverse_problem.py +++ b/simpeg/inverse_problem.py @@ -144,7 +144,7 @@ def opt(self, value): self._opt = validate_type("opt", value, Minimize, cast=False) @property - def deleteTheseOnModelUpdate(self): + def _delete_on_model_update(self): """A list of properties stored on this object to delete when the model is updated Returns @@ -170,7 +170,7 @@ def model(self, value): value = validate_ndarray_with_shape( "model", value, shape=[("*",), ("*", "*")], dtype=None ) - for prop in self.deleteTheseOnModelUpdate: + for prop in self._delete_on_model_update: if hasattr(self, prop): delattr(self, prop) self._model = value diff --git a/simpeg/meta/simulation.py b/simpeg/meta/simulation.py index ae9846475a..f2ebc30fdb 100644 --- a/simpeg/meta/simulation.py +++ b/simpeg/meta/simulation.py @@ -325,8 +325,8 @@ def getJtJdiag(self, m, W=None, f=None): return self._jtjdiag @property - def deleteTheseOnModelUpdate(self): - return super().deleteTheseOnModelUpdate + ["_jtjdiag"] + def _delete_on_model_update(self): + return super()._delete_on_model_update + ["_jtjdiag"] class SumMetaSimulation(MetaSimulation): diff --git a/simpeg/potential_fields/magnetics/simulation.py b/simpeg/potential_fields/magnetics/simulation.py index a40a28bb97..05ddf5b992 100644 --- a/simpeg/potential_fields/magnetics/simulation.py +++ b/simpeg/potential_fields/magnetics/simulation.py @@ -682,8 +682,8 @@ def evaluate_integral(self, receiver_location, components): ) @property - def deleteTheseOnModelUpdate(self): - deletes = super().deleteTheseOnModelUpdate + def _delete_on_model_update(self): + deletes = super()._delete_on_model_update if self.is_amplitude_data: deletes = deletes + ["_gtg_diagonal", "_ampDeriv"] return deletes diff --git a/simpeg/props.py b/simpeg/props.py index 5a55f77b0f..1d9052c4f3 100644 --- a/simpeg/props.py +++ b/simpeg/props.py @@ -1,5 +1,8 @@ +import warnings + import numpy as np +from simpeg.utils import deprecate_property from .maps import IdentityMap, ReciprocalMap from .utils import Zero, validate_type, validate_ndarray_with_shape @@ -358,7 +361,7 @@ def _has_nested_models(self): # TODO: rename to _delete_on_model_update @property - def deleteTheseOnModelUpdate(self): + def _delete_on_model_update(self): """A list of properties stored on this object to delete when the model is updated Returns @@ -368,6 +371,13 @@ def deleteTheseOnModelUpdate(self): """ return [] + deleteTheseOnModelUpdate = deprecate_property( + _delete_on_model_update, + "deleteTheseOnModelUpdate", + removal_version="0.25.0", + future_warn=True, + ) + #: List of matrix names to have their factors cleared on a model update @property def clean_on_model_update(self): @@ -377,6 +387,12 @@ def clean_on_model_update(self): ------- list of str """ + warnings.warn( + "clean_on_model_update has been deprecated due to repeated functionality encompassed" + " by the _delete_on_model_update method", + FutureWarning, + stacklevel=2, + ) return [] @property @@ -447,15 +463,10 @@ def model(self, value): and np.allclose(previous_value, value) ): # cached properties to delete - for prop in self.deleteTheseOnModelUpdate: + for prop in self._delete_on_model_update: if hasattr(self, prop): delattr(self, prop) - # matrix factors to clear - for mat in self.clean_on_model_update: - if getattr(self, mat, None) is not None: - getattr(self, mat).clean() # clean factors - setattr(self, mat, None) # set to none updated = True self._model = value @@ -469,12 +480,6 @@ def model(self, value): def model(self): self._model = (None,) # cached properties to delete - for prop in self.deleteTheseOnModelUpdate: + for prop in self._delete_on_model_update: if hasattr(self, prop): delattr(self, prop) - - # matrix factors to clear - for mat in self.clean_on_model_update: - if getattr(self, mat, None) is not None: - getattr(self, mat).clean() # clean factors - setattr(self, mat, None) # set to none diff --git a/tests/base/props/test_has_model.py b/tests/base/props/test_has_model.py new file mode 100644 index 0000000000..5d7cc0d7db --- /dev/null +++ b/tests/base/props/test_has_model.py @@ -0,0 +1,115 @@ +import re + +import pytest +import numpy as np +import numpy.testing as npt +from simpeg import props, maps + + +@pytest.fixture(scope="module") +def mock_model_class(): + class MockModel(props.HasModel): + prop, prop_map, _prop_deriv = props.Invertible("test physical property") + other, other_map, _other_deriv = props.Invertible("another physical property") + + def __init__( + self, prop=None, prop_map=None, other=None, other_map=None, **kwargs + ): + self.prop = prop + self.prop_map = prop_map + self.other = other + self.other_map = other_map + super().__init__(**kwargs) + + @property + def prop_dependent_property(self): + if (thing := getattr(self, "_prop_dependent_property", None)) is None: + thing = 2 * self.prop + self._prop_dependent_property = thing + return thing + + @property + def _delete_on_model_update(self): + if self.prop_map is not None: + return ["_prop_dependent_property"] + return [] + + return MockModel + + +@pytest.fixture() +def modeler(mock_model_class): + prop_map = maps.ExpMap() + modeler = mock_model_class(prop_map=prop_map) + return modeler + + +def test_clear_on_model_change(modeler): + x = np.ones(10) + modeler.model = x + npt.assert_array_equal(np.exp(x), modeler.prop) + + item_1 = modeler.prop_dependent_property + assert item_1 is not None + + x2 = x + 1 + modeler.model = x2 + assert getattr(modeler, "_prop_dependent_property", None) is None + + item_2 = modeler.prop_dependent_property + assert item_2 is not None + assert item_1 is not item_2 + + +def test_no_clear_on_model_reassign(modeler): + x = np.ones(10) + + modeler.model = x + npt.assert_array_equal(np.exp(x), modeler.prop) + + item_1 = modeler.prop_dependent_property + assert item_1 is not None + + modeler.model = x.copy() + assert getattr(modeler, "_prop_dependent_property", None) is not None + + item_2 = modeler.prop_dependent_property + + assert item_1 is item_2 + + +def test_map_clearing(modeler): + modeler.prop = np.ones(10) + assert modeler.prop_map is None + + +def test_no_clear_without_mapping(modeler): + modeler.prop_map = None + modeler.other_map = maps.ExpMap() + modeler.prop = np.ones(10) + item1 = modeler.prop_dependent_property + assert item1 is not None + + modeler.model = np.zeros(10) + + assert getattr(modeler, "_prop_dependent_property", None) is not None + assert modeler.prop_dependent_property is item1 + + +def test_model_needed(modeler): + assert modeler.needs_model + + +def test_no_model_needed(modeler): + modeler.prop_map = None + assert not modeler.needs_model + + +def test_deletion_deprecation(modeler): + msg = re.escape("HasModel.deleteTheseOnModelUpdate has been deprecated") + ".*" + with pytest.warns(FutureWarning, match=msg): + modeler.deleteTheseOnModelUpdate + + msg = "clean_on_model_update has been deprecated due to repeated functionality.*" + with pytest.warns(FutureWarning, match=msg): + modeler.clean_on_model_update diff --git a/tests/base/test_Props.py b/tests/base/props/test_prop_relationships.py similarity index 100% rename from tests/base/test_Props.py rename to tests/base/props/test_prop_relationships.py diff --git a/tests/base/test_base_pde_sim.py b/tests/base/test_base_pde_sim.py index 5b23ec451f..20501849ec 100644 --- a/tests/base/test_base_pde_sim.py +++ b/tests/base/test_base_pde_sim.py @@ -32,11 +32,11 @@ def __init__( self.muMap = muMap @property - def deleteTheseOnModelUpdate(self): + def _delete_on_model_update(self): """ matrices to be deleted if the model for conductivity/resistivity is updated """ - toDelete = super().deleteTheseOnModelUpdate + toDelete = super()._delete_on_model_update if self.sigmaMap is not None or self.rhoMap is not None: toDelete = toDelete + self._clear_on_sigma_update return toDelete diff --git a/tests/em/static/test_SPjvecjtvecadj.py b/tests/em/static/test_SPjvecjtvecadj.py index 462f6747b9..94fb2b86a4 100644 --- a/tests/em/static/test_SPjvecjtvecadj.py +++ b/tests/em/static/test_SPjvecjtvecadj.py @@ -114,13 +114,11 @@ def test_clears(): # set qMap as a non-linear map to make sure it adds the correct # items to be cleared on model update sim.qMap = maps.IdentityMap() - assert sim.deleteTheseOnModelUpdate == [] - assert sim.clean_on_model_update == [] + assert sim._delete_on_model_update == [] sim.storeJ = True sim.qMap = maps.ExpMap() - assert sim.deleteTheseOnModelUpdate == ["_Jmatrix", "_gtgdiag"] - assert sim.clean_on_model_update == [] + assert sim._delete_on_model_update == ["_Jmatrix", "_gtgdiag"] def test_deprecations(): From f697d245572745c1b77d084f83bfe622cca0ab62 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Tue, 17 Dec 2024 06:17:07 -0700 Subject: [PATCH 102/194] update PGI Example plotting script for deprecated collections (#1595) #### Summary Updates the PGI plotting example for matplotlib 3.10 removals. #### PR Checklist * [ ] If this is a work in progress PR, set as a Draft PR * [ ] Linted my code according to the [style guides](https://docs.simpeg.xyz/latest/content/getting_started/contributing/code-style.html). * [ ] Added [tests](https://docs.simpeg.xyz/latest/content/getting_started/contributing/testing.html) to verify changes to the code. * [ ] Added necessary documentation to any new functions/classes following the expect [style](https://docs.simpeg.xyz/latest/content/getting_started/contributing/documentation.html). * [ ] Marked as ready for review (if this is was a draft PR), and converted to a Pull Request * [ ] Tagged ``@simpeg/simpeg-developers`` when ready for review. --- ...1_PGI_Linear_1D_joint_WithRelationships.py | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/examples/10-pgi/plot_inv_1_PGI_Linear_1D_joint_WithRelationships.py b/examples/10-pgi/plot_inv_1_PGI_Linear_1D_joint_WithRelationships.py index 693139ab38..84b7fecfbc 100644 --- a/examples/10-pgi/plot_inv_1_PGI_Linear_1D_joint_WithRelationships.py +++ b/examples/10-pgi/plot_inv_1_PGI_Linear_1D_joint_WithRelationships.py @@ -11,6 +11,7 @@ import discretize as Mesh import matplotlib.pyplot as plt +import matplotlib.lines as mlines import numpy as np from simpeg import ( data_misfit, @@ -324,10 +325,16 @@ def g(k): alpha=0.25, cmap="viridis", ) -axes[3].scatter(wires.m1 * mcluster_map, wires.m2 * mcluster_map, marker="v") +cs_proxy = mlines.Line2D([], [], label="True Petrophysical Distribution") + +ps = axes[3].scatter( + wires.m1 * mcluster_map, + wires.m2 * mcluster_map, + marker="v", + label="Recovered model crossplot", +) axes[3].set_title("Petrophysical Distribution") -CS.collections[0].set_label("") -axes[3].legend(["True Petrophysical Distribution", "Recovered model crossplot"]) +axes[3].legend(handles=[cs_proxy, ps]) axes[3].set_xlabel("Property 1") axes[3].set_ylabel("Property 2") @@ -372,7 +379,6 @@ def g(k): 500, cmap="viridis", linestyles="--", - label="Modeled Petro. Distribution", ) axes[7].scatter( wires.m1 * mcluster_no_map, @@ -380,8 +386,12 @@ def g(k): marker="v", label="Recovered model crossplot", ) +cs_modeled_proxy = mlines.Line2D( + [], [], linestyle="--", label="Modeled Petro. Distribution" +) + axes[7].set_title("Petrophysical Distribution") -axes[7].legend() +axes[7].legend(handles=[cs_proxy, cs_modeled_proxy, ps]) axes[7].set_xlabel("Property 1") axes[7].set_ylabel("Property 2") @@ -423,8 +433,7 @@ def g(k): ) axes[11].scatter(wires.m1 * mtik, wires.m2 * mtik, marker="v") axes[11].set_title("Petro Distribution") -CS.collections[0].set_label("") -axes[11].legend(["True Petro Distribution", "Recovered model crossplot"]) +axes[11].legend(handles=[cs_proxy, ps]) axes[11].set_xlabel("Property 1") axes[11].set_ylabel("Property 2") plt.subplots_adjust(wspace=0.3, hspace=0.3, top=0.85) From 6dd8800af036dcf38917df6af79d56ee326417a1 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Thu, 19 Dec 2024 15:28:33 -0700 Subject: [PATCH 103/194] Coverage upload on failed test (#1596) #### Summary Make the coverage step run if the succeeded OR failed. #### What does this implement/fix? This makes the azure-pipeline codecov upload step run if the tests succeed or fail, as it can still be useful to see coverage analyses of failed tests. --- .ci/azure/run_tests_with_coverage.sh | 6 ++++-- .ci/azure/test.yml | 4 +++- azure-pipelines.yml | 1 + 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.ci/azure/run_tests_with_coverage.sh b/.ci/azure/run_tests_with_coverage.sh index def90a983c..a975c1daef 100755 --- a/.ci/azure/run_tests_with_coverage.sh +++ b/.ci/azure/run_tests_with_coverage.sh @@ -1,6 +1,8 @@ #!/bin/bash -set -ex #echo on and exit if any line fails +set -x #echo on source activate simpeg-test pytest $TEST_TARGET --cov --cov-config=pyproject.toml -v -W ignore::DeprecationWarning -coverage xml \ No newline at end of file +pytest_retval=$? +coverage xml +exit $pytest_retval \ No newline at end of file diff --git a/.ci/azure/test.yml b/.ci/azure/test.yml index 2e6ad30378..fe6bfebe2b 100644 --- a/.ci/azure/test.yml +++ b/.ci/azure/test.yml @@ -50,17 +50,19 @@ jobs: targetPath: $(Build.SourcesDirectory)/docs/_build/html artifactName: html_docs displayName: 'Publish documentation artifact' - condition: eq('${{ test }}', 'tests/docs -s -v') + condition: and(eq('${{ test }}', 'tests/docs -s -v'), succeededOrFailed()) - bash: | job="${{ os }}_${{ py_vers }}_${{ test }}" jobhash=$(echo $job | sha256sum | cut -f 1 -d " " | cut -c 1-7) cp coverage.xml "coverage-$jobhash.xml" echo "##vso[task.setvariable variable=jobhash]$jobhash" + condition: succeededOrFailed() displayName: 'Rename coverage report' - task: PublishPipelineArtifact@1 inputs: targetPath: $(Build.SourcesDirectory)/coverage-$(jobhash).xml artifactName: coverage-$(jobhash) + condition: succeededOrFailed() displayName: 'Publish coverage artifact' diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 7e10fa5d59..93a3ad717c 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -44,6 +44,7 @@ stages: - stage: Codecov dependsOn: Testing + condition: succeededOrFailed() jobs: - template: .ci/azure/codecov.yml From 717f0592c19e967007d784c9d39e1efb33c880b9 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 20 Dec 2024 09:46:00 -0800 Subject: [PATCH 104/194] Use zizmor to lint GitHub Actions workflows (#1592) Make use of `zizmor` to lint the GitHub Actions workflows under the `.github/workflows` directory for common security vulnerabilities. Add a new `check-actions` target in the `Makefile` that runs `zizmor` on those files. Add a new GitHub Action that runs `zizmor`. Add `zizmor` to the `environment.yml`. Add warning to the `pull_request.yml` workflow about not running code from the PR branch since it makes use of the `pull_request_target` trigger, and make `zizmor` to ignore the `pull-request_target` trigger. Fix some of the warnings raised by `zizmor`. --------- Co-authored-by: Joseph Capriotti --- .github/workflows/pull_request.yml | 47 +++++++++++++++++++++++++++++- .github/workflows/zizmor.yml | 36 +++++++++++++++++++++++ Makefile | 15 ++++++---- environment.yml | 4 +++ 4 files changed, 96 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/zizmor.yml diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index d6af2dcbf8..8932f64a47 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -1,5 +1,40 @@ name : Reviewdog PR Annotations -on: [pull_request_target] + +# ========= +# IMPORTANT +# ========= +# +# TL;DR: +# THIS ACTION SHOULD NOT RUN ANY CODE FROM THE PR BRANCH. +# +# This action is triggered after new events in Pull Requests (as the +# `pull_request` trigger does), but this ones provides the workflow writing +# permissions to the repo. +# The second checkout step in each job checks out code from the PR branch. +# Code from any PR branch should be treated as malicious! +# Therefore, these action **SHOULD NOT RUN ANY CODE** from the PR branch since +# the workflow has writting permisions. +# Doing so introduces a high severity vulnerability that could be exploited to +# gain access to secrets and/or introduce malicious code. +# +# For this particular workflow we need the writting permission in order for +# reviewdog to publish comments in the PR. +# zizmor will complain about the `pull_request_target` trigger, so we will +# ignore it. +# +# Worth noting that the runner will execute the steps specified in the version +# of this workflow file that lives in the **target branch** (usually `main`), +# not the one in the Pull Request branch. This means that even if a contributor +# opens a PR with a change to this file, the change won't be executed. This is +# intended to prevent third-party contributors from running custom code with high +# privileges on the repo. +# +# References: +# * https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/ +# * PR that added this action: https://github.com/simpeg/simpeg/pull/1424 +# * PR that added this warning: https://github.com/simpeg/simpeg/pull/1592 +# +on: [pull_request_target] # zizmor: ignore[dangerous-triggers] jobs: flake8: @@ -8,6 +43,8 @@ jobs: steps: - name: Checkout target repository source uses: actions/checkout@v4 + with: + persist-credentials: false - name: Setup Python env uses: actions/setup-python@v5 @@ -17,11 +54,14 @@ jobs: - name: Install dependencies to run the flake8 checks run: .ci/install_style.sh + # Checkout PR branch. + # TREAT THIS CODE AS MALICIOUS, DON'T RUN CODE FROM THIS BRANCH. - name: checkout pull request source uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} path: pr_source + persist-credentials: false - name: flake8 review uses: reviewdog/action-flake8@v3 @@ -36,6 +76,8 @@ jobs: steps: - name: Checkout target repository source uses: actions/checkout@v4 + with: + persist-credentials: false - name: Setup Python env uses: actions/setup-python@v5 @@ -45,11 +87,14 @@ jobs: - name: Install dependencies to run the black checks run: .ci/install_style.sh + # Checkout PR branch. + # TREAT THIS CODE AS MALICIOUS, DON'T RUN CODE FROM THIS BRANCH. - name: checkout pull request source uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} path: 'pr_source' + persist-credentials: false - uses: reviewdog/action-black@v3 with: diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 0000000000..e6046dc729 --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,36 @@ +# Lint GitHub Actions for common security issues using zizmor. +# Docs: https://woodruffw.github.io/zizmor + +name: Lint GitHub Actions + +on: + pull_request: + push: + branches: + - main + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install zizmor + run: python -m pip install zizmor + + - name: List installed packages + run: python -m pip freeze + + - name: Lint GitHub Actions + run: make check-actions + env: + # Set GH_TOKEN to allow zizmor to check online vulnerabilities + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Makefile b/Makefile index e367bad141..2ede57730f 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,16 @@ STYLE_CHECK_FILES = simpeg examples tutorials tests +GITHUB_ACTIONS=.github/workflows -.PHONY: help docs check black flake +.PHONY: help docs clean check black flake flake-all check-actions help: @echo "Commands:" @echo "" - @echo " check run code style and quality checks (black and flake8)" - @echo " black checks code style with black" - @echo " flake checks code style with flake8" - @echo " flake-all checks code style with flake8 (full set of rules)" + @echo " check run code style and quality checks (black and flake8)" + @echo " black checks code style with black" + @echo " flake checks code style with flake8" + @echo " flake-all checks code style with flake8 (full set of rules)" + @echo " check-actions lint GitHub Actions workflows (with zizmor)" @echo "" docs: @@ -31,3 +33,6 @@ flake: flake-all: flake8 --version flake8 ${FLAKE8_OPTS} --ignore "" ${STYLE_CHECK_FILES} + +check-actions: + zizmor ${GITHUB_ACTIONS} diff --git a/environment.yml b/environment.yml index 5c017b7e16..26fd37631c 100644 --- a/environment.yml +++ b/environment.yml @@ -60,3 +60,7 @@ dependencies: # recommended - jupyter - pyvista + + - pip + - pip: + - zizmor # lint GitHub Actions workflows From d6819fab9f1d2aa853fad9161deba05297bc5e54 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 20 Dec 2024 12:11:11 -0800 Subject: [PATCH 105/194] Update installation instructions in docs (#1597) Apply some improvements to the installation instructions under the Getting Started documentation page. Recommend Miniforge over Anaconda as Python distribution. Remove the admonition that warns about using Python 2.7 to run SimPEG. Replace link to Google Form for one to the Discourse forum. Minor improvements on format and style, with minor changes in wording. --- .github/ISSUE_TEMPLATE/post-install.yml | 3 +- .../contributing/setting-up-environment.rst | 4 +- docs/content/getting_started/installing.rst | 78 +++++++++++-------- 3 files changed, 51 insertions(+), 34 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/post-install.yml b/.github/ISSUE_TEMPLATE/post-install.yml index 10b970aebf..5a86f2f0b1 100644 --- a/.github/ISSUE_TEMPLATE/post-install.yml +++ b/.github/ISSUE_TEMPLATE/post-install.yml @@ -9,7 +9,8 @@ body: label: "Steps to reproduce:" description: > Please describe the installation method (e.g. building from source, - Anaconda, pip), your OS and SimPEG, NumPy, Scipy, and Python version information. + Miniforge, Anaconda, pip), your OS and SimPEG, NumPy, Scipy, and Python + version information. placeholder: | If running in a conda environment you could paste the output of `conda list` here. validations: diff --git a/docs/content/getting_started/contributing/setting-up-environment.rst b/docs/content/getting_started/contributing/setting-up-environment.rst index f1b3d5c992..c5dd3a95ce 100644 --- a/docs/content/getting_started/contributing/setting-up-environment.rst +++ b/docs/content/getting_started/contributing/setting-up-environment.rst @@ -7,8 +7,8 @@ Install Python -------------- First you will need to install Python. You can find instructions in -:ref:`installing_python`. We highly encourage to install Anaconda_ or -Miniforge_. +:ref:`installing_python`. We highly encourage to install Miniforge_ (or +Anaconda_). Create environment ------------------ diff --git a/docs/content/getting_started/installing.rst b/docs/content/getting_started/installing.rst index 06724787e7..a3bc1cac3b 100644 --- a/docs/content/getting_started/installing.rst +++ b/docs/content/getting_started/installing.rst @@ -6,31 +6,39 @@ Getting Started with SimPEG .. _installing_python: -Prerequisite: Installing Python -=============================== +Installing Python +================= SimPEG is written in Python_! -We highly recommend installing it using Anaconda_ (or the alternative Miniforge_). -It installs `Python `_, -`Jupyter `_ and other core -Python libraries for scientific computing. -If you and Python_ are not yet acquainted, we highly -recommend checking out `Software Carpentry `_. +This means we need Python_ in order to run SimPEG. +We highly recommend installing a Python distribution like Miniforge_ that will +install the Python interpreter along with the conda_ package manager. .. note:: - As of version 0.11.0, we will no longer ensure compatibility with Python 2.7. Please use - the latest version of Python 3 with SimPEG. For more information on the transition of the - Python ecosystem to Python 3, please see the `Python 3 Statement `_. + Miniforge_ is a community-driven alternative to Anaconda_, a well-known + Python distribution. + + We recommend Miniforge_ over Anaconda_ because it's more lightweight and + because it makes use of the conda-forge_ community-led channel to download + packages. Downloading packages from Anaconda_ (usually refered as the + ``default`` channel) requires us to adhere to their `Terms of Service + `_. + Make sure to read them and their `FAQs + `_ if you decide to + still use Anaconda_. -.. image:: https://upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Python-logo-notext.svg/220px-Python-logo-notext.svg.png - :align: right - :width: 100 - :target: https://www.python.org/ +.. seealso:: + + If you are starting with Python_ and want to learn more and feel more + comfortable with the language, we recommend checking out + `Software Carpentry `_'s lessons. .. _Python: https://www.python.org/ .. _Anaconda: https://www.anaconda.com/products/individual .. _Miniforge: https://github.com/conda-forge/miniforge +.. _conda: https://docs.conda.io/en/latest +.. _conda-forge: https://conda-forge.org/ .. _installing_simpeg: @@ -41,15 +49,17 @@ Installing SimPEG Conda Forge ----------- -SimPEG is available through `conda-forge` and you can install is using the -`conda package manager `_ that comes with the Anaconda_ -or Miniforge_ distributions: +SimPEG is available through conda-forge_ and you can install is using the +`conda package manager `_ that comes with Miniforge_ (or +Anaconda_): + +.. code:: bash -.. code:: + conda install --channel conda-forge simpeg - conda install SimPEG --channel conda-forge +.. note:: -Installing through `conda` is our recommended method of installation. + Installing through ``conda`` is our recommended method of installation. .. note:: @@ -72,32 +82,36 @@ PyPi SimPEG is on `pypi `_! First, make sure your version of pip is up-to-date -.. code:: +.. code:: bash pip install --upgrade pip Then you can install SimPEG -.. code:: +.. code:: bash - pip install SimPEG + pip install simpeg To update SimPEG, you can run -.. code:: +.. code:: bash - pip install --upgrade SimPEG + pip install --upgrade simpeg Installing from Source ---------------------- -First (you need git):: +First (you need git): + +.. code:: bash git clone https://github.com/simpeg/simpeg -Second (from the root of the SimPEG repository):: +Second (from the root of the SimPEG repository): + +.. code:: bash pip install . @@ -110,9 +124,11 @@ Success? If you have been successful at downloading and installing SimPEG, you should be able to download and run any of the :ref:`examples and tutorials `. -If not, you can reach out to other people developing and using SimPEG on the -`google forum `_ or on -`Mattermost `_. +If not, you can reach out to other people developing and using SimPEG on our +Mattermost_ channel or in our `Discourse forum`_. + +.. _Discourse forum: https://simpeg.discourse.group/ +.. _Mattermost: https://mattermost.softwareunderground.org/simpeg Useful Links ============ From c00b102661dd5816a263447cd553cfbb0814cf44 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 30 Jan 2025 09:41:02 -0800 Subject: [PATCH 106/194] Set `permissions` in Actions to avoid zizmor's `excessive-permissions` (#1602) #### Summary Set `permissions` in Action workflows to solve zizmor's complains about excessive permissions for the `GITHUB_TOKEN`. #### Additional information Some references: * [zizmor docs for the `excessive-permissions` audit rule](https://woodruffw.github.io/zizmor/audits/#excessive-permissions) * [Defining access for the GITHUB_TOKEN permissions](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/controlling-permissions-for-github_token#defining-access-for-the-github_token-permissions) * [Controlling permissions for `GITHUB_TOKEN`](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/controlling-permissions-for-github_token) --- .github/workflows/pull_request.yml | 10 +++++++++- .github/workflows/zizmor.yml | 2 ++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 8932f64a47..3052116a35 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -40,6 +40,10 @@ jobs: flake8: runs-on: ubuntu-latest name: Flake8 check + permissions: + contents: read + pull-requests: write + steps: - name: Checkout target repository source uses: actions/checkout@v4 @@ -73,6 +77,10 @@ jobs: black: name: Black check runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: - name: Checkout target repository source uses: actions/checkout@v4 @@ -100,4 +108,4 @@ jobs: with: workdir: 'pr_source' github_token: ${{ secrets.GITHUB_TOKEN }} - reporter: github-pr-review \ No newline at end of file + reporter: github-pr-review diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index e6046dc729..ca489ee9e1 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -9,6 +9,8 @@ on: branches: - main +permissions: {} + jobs: lint: runs-on: ubuntu-latest From 2e3689625225955cc338dadad13cb3cdb4310e8b Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Thu, 30 Jan 2025 11:53:38 -0700 Subject: [PATCH 107/194] Fix for removed quadrature function on new scipy versions (#1603) #### Summary The function `scipy.integrate.quadrature` was removed in scipy v1.15.0 in favor of `scipy.integrate.quad`. This function is available in 1.8 so no need to update the minimum required scipy version. --- simpeg/electromagnetics/utils/waveform_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simpeg/electromagnetics/utils/waveform_utils.py b/simpeg/electromagnetics/utils/waveform_utils.py index 55e2c2f8ab..8120777712 100644 --- a/simpeg/electromagnetics/utils/waveform_utils.py +++ b/simpeg/electromagnetics/utils/waveform_utils.py @@ -119,6 +119,6 @@ def integral(quad_time, t): # just do not evaluate the integral at negative times... a = np.maximum(a, 0.0) b = np.maximum(b, 0.0) - val, _ = integrate.quadrature(integral, a, b, tol=0.0, maxiter=500, args=t) + val = integrate.quad(integral, a, b, epsabs=0.0, limit=500, args=t)[0] out[it] -= val return out From a918b45d184161677a4b8949c2884e93a926c2cb Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 6 Mar 2025 09:09:46 -0800 Subject: [PATCH 108/194] Install zizmor through conda-forge in `environment.yml` (#1600) Since zizmor is now available through conda-forge, we don't need to install it through `pip` when creating a conda environment from `environment.yml`. --- environment.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/environment.yml b/environment.yml index 26fd37631c..ed09d55f72 100644 --- a/environment.yml +++ b/environment.yml @@ -56,11 +56,8 @@ dependencies: - flake8-mutable==1.2.0 - flake8-rst-docstrings==0.3.0 - flake8-docstrings==1.7.0 + - zizmor # lint GitHub Actions workflows # recommended - jupyter - pyvista - - - pip - - pip: - - zizmor # lint GitHub Actions workflows From 368afb33da6589d255c46c100f926bc733cb9b36 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 6 Mar 2025 13:30:43 -0800 Subject: [PATCH 109/194] Raise errors if dpred in `BaseDataMisfit` has nans (#1615) Make `BaseDataMisfit.residual` to raise an error if the simulation returns an array with `nan`s and/or `inf`s after calling the `dpred` method. Add tests to check if the errors are correctly raised. Remove error in case `BaseDataMisfit.data` is `None`: the `data` setter method doesn't allow `BaseDataMisfit.data` to be `None`, therefore the error will never be raised. --- simpeg/data_misfit.py | 11 ++++++--- tests/base/test_data_misfit.py | 44 ++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/simpeg/data_misfit.py b/simpeg/data_misfit.py index 6b489b425a..7c0f1bbb34 100644 --- a/simpeg/data_misfit.py +++ b/simpeg/data_misfit.py @@ -225,9 +225,14 @@ def residual(self, m, f=None): (n_data, ) numpy.ndarray The data residual vector. """ - if self.data is None: - raise Exception("data must be set before a residual can be calculated.") - return self.simulation.residual(m, self.data.dobs, f=f) + dpred = self.simulation.dpred(m, f=f) + if np.isnan(dpred).any() or np.isinf(dpred).any(): + msg = ( + f"The `{type(self.simulation).__name__}.dpred()` method " + "returned an array that contains `nan`s and/or `inf`s." + ) + raise ValueError(msg) + return dpred - self.data.dobs class L2DataMisfit(BaseDataMisfit): diff --git a/tests/base/test_data_misfit.py b/tests/base/test_data_misfit.py index bf1d0cc088..323673b5b6 100644 --- a/tests/base/test_data_misfit.py +++ b/tests/base/test_data_misfit.py @@ -1,3 +1,5 @@ +import re +import pytest import unittest import numpy as np @@ -5,6 +7,7 @@ from simpeg import maps from simpeg import data_misfit, simulation, survey +from simpeg import Data class DataMisfitTest(unittest.TestCase): @@ -69,5 +72,46 @@ def test_DataMisfitOrder(self): self.dmis.test(x=self.model, random_seed=17) +class MockSimulation(simulation.BaseSimulation): + """ + Mock simulation class that returns nans or infs in the dpred array. + """ + + def __init__(self, invalid_value=np.nan): + self.invalid_value = invalid_value + super().__init__() + + def dpred(self, m=None, f=None): + a = np.arange(4, dtype=np.float64) + a[1] = self.invalid_value + return a + + +class TestNanOrInfInResidual: + """Test errors if the simulation return dpred with nans or infs.""" + + @pytest.fixture + def n_data(self): + return 4 + + @pytest.fixture + def sample_survey(self, n_data): + receivers = survey.BaseRx(np.zeros(n_data)[:, np.newaxis]) + source = survey.BaseSrc([receivers]) + return survey.BaseSurvey([source]) + + @pytest.mark.parametrize("invalid_value", [np.nan, np.inf]) + def test_error(self, sample_survey, invalid_value): + mock_simulation = MockSimulation(invalid_value) + data = Data(sample_survey) + dmisfit = data_misfit.BaseDataMisfit(data, mock_simulation) + msg = re.escape( + "The `MockSimulation.dpred()` method returned an array that contains " + "`nan`s and/or `inf`s." + ) + with pytest.raises(ValueError, match=msg): + dmisfit.residual(m=None) + + if __name__ == "__main__": unittest.main() From 05f9f012aead2099aebbe1ed00d18aed971845cd Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 6 Mar 2025 13:31:14 -0800 Subject: [PATCH 110/194] Update Black's Python versions in `pyproject.toml` (#1620) Remove non-supported Python versions and add latest ones. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9076ef11fe..3ce6d1d6b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -134,7 +134,7 @@ directory = "coverage_html_report" [tool.black] required-version = '24.3.0' -target-version = ['py38', 'py39', 'py310', 'py311'] +target-version = ['py310', 'py311', 'py312'] [tool.flake8] extend-ignore = [ From bb8a5176607753dc45b25d86b237454aa48afdc3 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 6 Mar 2025 13:31:48 -0800 Subject: [PATCH 111/194] Use shell rendering in Bug report template (#1612) Use shell rendering in the runtime information required in the Bug Report Template. --- .github/ISSUE_TEMPLATE/bug-report.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 130f534ba2..153da7708e 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -46,6 +46,7 @@ body: description: > Please include the output from `simpeg.Report()` to describe your system for us. Paste the output from `from simpeg import Report; print(Report())` below. + render: shell validations: required: true From 392959d6a904c80cd1e5d9076d3e02073f8aa552 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 7 Mar 2025 12:44:56 -0800 Subject: [PATCH 112/194] Merge Getting Started and Examples into User Guide (#1619) Move some documentation pages like Getting Started, Examples and Tutorials inside the User Guide. Create a new `docs/content/user-guide` folder. Move Getting Started, Tutorials and Examples inside the new folder. Remove the `docs/content/user_guide.rst` and replace it by a `docs/content/user-guide/index.rst` file. Rename the `getting_started` folder into `getting-started` (to make a consistent use of the dash in urls). Update relevant index files, `docs/c onf.py`, `.gitignore` and `docs/Makefile`. Configure `sphinx-redirects` so Sphinx create dummy `.html` files for old locations of documentation pages that redirect to the new locations. Fix link to Octocat image. --- .ci/environment_test.yml | 1 + .gitignore | 4 +- docs/Makefile | 4 +- docs/conf.py | 61 ++++++- docs/content/getting_started/index.rst | 14 -- .../getting-started}/big_picture.rst | 4 +- .../contributing/advanced.rst | 0 .../contributing/code-style.rst | 0 .../contributing/documentation.rst | 0 .../getting-started}/contributing/index.rst | 0 .../contributing/pull-requests.rst | 0 .../contributing/setting-up-environment.rst | 0 .../getting-started}/contributing/testing.rst | 0 .../contributing/working-with-github.rst | 6 +- .../getting-started}/installing.rst | 7 +- docs/content/user-guide/index.rst | 32 ++++ docs/content/user_guide.rst | 24 --- docs/index.rst | 7 +- docs/old-docs-files.txt | 154 ++++++++++++++++++ environment.yml | 1 + pyproject.toml | 3 +- 21 files changed, 262 insertions(+), 60 deletions(-) delete mode 100644 docs/content/getting_started/index.rst rename docs/content/{getting_started => user-guide/getting-started}/big_picture.rst (98%) rename docs/content/{getting_started => user-guide/getting-started}/contributing/advanced.rst (100%) rename docs/content/{getting_started => user-guide/getting-started}/contributing/code-style.rst (100%) rename docs/content/{getting_started => user-guide/getting-started}/contributing/documentation.rst (100%) rename docs/content/{getting_started => user-guide/getting-started}/contributing/index.rst (100%) rename docs/content/{getting_started => user-guide/getting-started}/contributing/pull-requests.rst (100%) rename docs/content/{getting_started => user-guide/getting-started}/contributing/setting-up-environment.rst (100%) rename docs/content/{getting_started => user-guide/getting-started}/contributing/testing.rst (100%) rename docs/content/{getting_started => user-guide/getting-started}/contributing/working-with-github.rst (89%) rename docs/content/{getting_started => user-guide/getting-started}/installing.rst (98%) create mode 100644 docs/content/user-guide/index.rst delete mode 100644 docs/content/user_guide.rst create mode 100644 docs/old-docs-files.txt diff --git a/.ci/environment_test.yml b/.ci/environment_test.yml index e1ca51a0a4..edb79f80f7 100644 --- a/.ci/environment_test.yml +++ b/.ci/environment_test.yml @@ -24,6 +24,7 @@ dependencies: - sphinx - sphinx-gallery>=0.1.13 - sphinxcontrib-apidoc + - sphinx-reredirects - pydata-sphinx-theme - nbsphinx - numpydoc diff --git a/.gitignore b/.gitignore index 53545d898d..aaddeda18b 100644 --- a/.gitignore +++ b/.gitignore @@ -47,8 +47,8 @@ docs/_build/ docs/warnings.txt docs/content/api/generated/* .DS_Store -docs/content/examples/* -docs/content/tutorials/* +docs/content/user-guide/examples/* +docs/content/user-guide/tutorials/* docs/modules/* docs/sg_execution_times.rst .vscode/* diff --git a/docs/Makefile b/docs/Makefile index 3751f949e1..cf1d326cfb 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -43,8 +43,8 @@ help: clean: rm -rf $(BUILDDIR)/* rm -rf content/api/generated/ - rm -rf content/examples/ - rm -rf content/tutorials/ + rm -rf content/user-guide/examples/ + rm -rf content/user-guide/tutorials/ html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html -j auto diff --git a/docs/conf.py b/docs/conf.py index 5b7c5c9175..a90da0c494 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,7 +10,7 @@ # # All configuration values have a default; values that are commented out # serve to show the default. - +from pathlib import Path import sys import os from sphinx_gallery.sorting import FileNameSortKey @@ -49,6 +49,7 @@ "sphinx_gallery.gen_gallery", "sphinx.ext.todo", "matplotlib.sphinxext.plot_directive", + "sphinx_reredirects", ] # Autosummary pages will be generated by sphinx-autogen instead of sphinx-build @@ -98,8 +99,8 @@ linkcheck_ignore = [ r"https://github.com/simpeg/simpeg*", - "/content/examples/*", - "/content/tutorials/*", + "/content/user-guide/examples/*", + "/content/user-guide/tutorials/*", r"https://www.pardiso-project.org", r"https://docs.github.com/*", # GJI refuses the connexion during the check @@ -464,7 +465,9 @@ def linkcode_resolve(domain, info): ] tutorial_dirs = glob.glob("../tutorials/[!_]*") -tut_gallery_dirs = ["content/tutorials/" + os.path.basename(f) for f in tutorial_dirs] +tut_gallery_dirs = [ + "content/user-guide/tutorials/" + os.path.basename(f) for f in tutorial_dirs +] # Scaping images to generate on website from plotly.io._sg_scraper import plotly_sg_scraper @@ -475,7 +478,7 @@ def linkcode_resolve(domain, info): sphinx_gallery_conf = { # path to your examples scripts "examples_dirs": ["../examples"] + tutorial_dirs, - "gallery_dirs": ["content/examples"] + tut_gallery_dirs, + "gallery_dirs": ["content/user-guide/examples"] + tut_gallery_dirs, "within_subsection_order": FileNameSortKey, "filename_pattern": "\.py", "backreferences_dir": "content/api/generated/backreferences", @@ -531,3 +534,51 @@ def linkcode_resolve(domain, info): ("py:class", "builtins.complex"), ("py:meth", "__call__"), ] + + +# Configure redirects +# ------------------- +# Redirect some pages to support old links +OLD_FILES_FNAME = Path(__file__).parent.resolve() / "old-docs-files.txt" +MAPS = { + "content/tutorials": "content/user-guide/tutorials", + "content/examples": "content/user-guide/examples", + "content/getting_started": "content/user-guide/getting-started", +} +IGNORE = ["content/getting_started/index.html", "content/user_guide.html"] + + +def _get_source_target(old_fname: str) -> tuple[str, str]: + for old_dir, new_dir in MAPS.items(): + if old_fname.startswith(old_dir): + source = old_fname.removesuffix(".html") + n_parents = len(Path(old_fname).parents) + target = "../" * n_parents + old_fname.replace(old_dir, new_dir, 1) + return source, target + raise ValueError() + + +def build_redirects(): + """ + Build redirects dictionary for sphinx-reredirects. + """ + redirects = {} + with OLD_FILES_FNAME.open(mode="r") as f: + for line in f: + if line.startswith("#"): + continue + old_fname = line.strip() + if old_fname in IGNORE: + continue + source, target = _get_source_target(old_fname) + redirects[source] = target + return redirects + + +redirects = build_redirects() +redirects.update( + { + "content/getting_started/index": "../../content/user-guide/index.html", + "content/user_guide": "../../content/user-guide/index.html", + } +) diff --git a/docs/content/getting_started/index.rst b/docs/content/getting_started/index.rst deleted file mode 100644 index dfef8b8d96..0000000000 --- a/docs/content/getting_started/index.rst +++ /dev/null @@ -1,14 +0,0 @@ -.. _getting_started: - -=============== -Getting Started -=============== - -Here you'll find instructions on getting up and running with SimPEG. - -.. toctree:: - :maxdepth: 2 - - big_picture - installing - contributing/index.rst diff --git a/docs/content/getting_started/big_picture.rst b/docs/content/user-guide/getting-started/big_picture.rst similarity index 98% rename from docs/content/getting_started/big_picture.rst rename to docs/content/user-guide/getting-started/big_picture.rst index f8060f0bb6..b5c2ddc43b 100644 --- a/docs/content/getting_started/big_picture.rst +++ b/docs/content/user-guide/getting-started/big_picture.rst @@ -67,7 +67,7 @@ implementation is a model, which, prior to interpretation, must be evaluated. This requires considering, and often re-assessing, the choices and assumptions made in both the input and implementation stages. -.. image:: ../../images/InversionWorkflow-PreSimPEG.png +.. image:: ../../../images/InversionWorkflow-PreSimPEG.png :width: 400 px :alt: Components :align: center @@ -86,7 +86,7 @@ of inversions into various units. We present it in this specific modular style, as each unit contains a targeted subset of choices crucial to the inversion process. -.. image:: ../../images/InversionWorkflow.png +.. image:: ../../../images/InversionWorkflow.png :width: 400 px :alt: Framework :align: center diff --git a/docs/content/getting_started/contributing/advanced.rst b/docs/content/user-guide/getting-started/contributing/advanced.rst similarity index 100% rename from docs/content/getting_started/contributing/advanced.rst rename to docs/content/user-guide/getting-started/contributing/advanced.rst diff --git a/docs/content/getting_started/contributing/code-style.rst b/docs/content/user-guide/getting-started/contributing/code-style.rst similarity index 100% rename from docs/content/getting_started/contributing/code-style.rst rename to docs/content/user-guide/getting-started/contributing/code-style.rst diff --git a/docs/content/getting_started/contributing/documentation.rst b/docs/content/user-guide/getting-started/contributing/documentation.rst similarity index 100% rename from docs/content/getting_started/contributing/documentation.rst rename to docs/content/user-guide/getting-started/contributing/documentation.rst diff --git a/docs/content/getting_started/contributing/index.rst b/docs/content/user-guide/getting-started/contributing/index.rst similarity index 100% rename from docs/content/getting_started/contributing/index.rst rename to docs/content/user-guide/getting-started/contributing/index.rst diff --git a/docs/content/getting_started/contributing/pull-requests.rst b/docs/content/user-guide/getting-started/contributing/pull-requests.rst similarity index 100% rename from docs/content/getting_started/contributing/pull-requests.rst rename to docs/content/user-guide/getting-started/contributing/pull-requests.rst diff --git a/docs/content/getting_started/contributing/setting-up-environment.rst b/docs/content/user-guide/getting-started/contributing/setting-up-environment.rst similarity index 100% rename from docs/content/getting_started/contributing/setting-up-environment.rst rename to docs/content/user-guide/getting-started/contributing/setting-up-environment.rst diff --git a/docs/content/getting_started/contributing/testing.rst b/docs/content/user-guide/getting-started/contributing/testing.rst similarity index 100% rename from docs/content/getting_started/contributing/testing.rst rename to docs/content/user-guide/getting-started/contributing/testing.rst diff --git a/docs/content/getting_started/contributing/working-with-github.rst b/docs/content/user-guide/getting-started/contributing/working-with-github.rst similarity index 89% rename from docs/content/getting_started/contributing/working-with-github.rst rename to docs/content/user-guide/getting-started/contributing/working-with-github.rst index cb944eadd0..979cab63ef 100644 --- a/docs/content/getting_started/contributing/working-with-github.rst +++ b/docs/content/user-guide/getting-started/contributing/working-with-github.rst @@ -3,7 +3,7 @@ Working with Git and GitHub --------------------------- -.. image:: https://github.githubassets.com/images/modules/logos_page/Octocat.png +.. image:: https://octodex.github.com/images/original.png :align: right :width: 100 :target: https://github.com @@ -25,7 +25,7 @@ There are two ways you can clone a repository: 2. Using a desktop client such as SourceTree_ or GitKraken_. - .. image:: ../../../images/sourceTreeSimPEG.png + .. image:: ../../../../images/sourceTreeSimPEG.png :align: center :width: 400 :target: https://www.sourcetreeapp.com/ @@ -34,7 +34,7 @@ There are two ways you can clone a repository: it is also handy to set up the remote account so it remembers your github_ user name and password - .. image:: ../../../images/sourceTreeRemote.png + .. image:: ../../../../images/sourceTreeRemote.png :align: center :width: 400 diff --git a/docs/content/getting_started/installing.rst b/docs/content/user-guide/getting-started/installing.rst similarity index 98% rename from docs/content/getting_started/installing.rst rename to docs/content/user-guide/getting-started/installing.rst index a3bc1cac3b..f3233d4a76 100644 --- a/docs/content/getting_started/installing.rst +++ b/docs/content/user-guide/getting-started/installing.rst @@ -1,7 +1,8 @@ -.. _api_installing: +.. _installing: -Getting Started with SimPEG -*************************** +========== +Installing +========== .. _installing_python: diff --git a/docs/content/user-guide/index.rst b/docs/content/user-guide/index.rst new file mode 100644 index 0000000000..801fe06370 --- /dev/null +++ b/docs/content/user-guide/index.rst @@ -0,0 +1,32 @@ +.. _user_guide: + +SimPEG User Guide +================= + +This guide is aimed to help users to get started with SimPEG and to learn how +to simulate physics and run inversions for different types of geophysical data. + +For details on the available classes and functions in SimPEG, please visit the +:ref:`api`. + +.. toctree:: + :glob: + :maxdepth: 1 + :caption: Getting Started + + getting-started/big_picture + getting-started/installing + getting-started/contributing/index.rst + +.. toctree:: + :glob: + :maxdepth: 1 + :caption: Tutorials + + tutorials/**/index + +.. toctree:: + :maxdepth: 2 + :caption: Examples + + examples/index diff --git a/docs/content/user_guide.rst b/docs/content/user_guide.rst deleted file mode 100644 index aeebe15070..0000000000 --- a/docs/content/user_guide.rst +++ /dev/null @@ -1,24 +0,0 @@ -.. _user_guide: - -========== -User Guide -========== - -We've included some tutorials and gallery examples that will walk you through using -discretize to solve your PDE. For more details on any of the functions, check out the -API documentation. - -Tutorials ---------- -.. toctree:: - :glob: - :maxdepth: 2 - - tutorials/**/index - -Examples --------- -.. toctree:: - :maxdepth: 1 - - examples/index diff --git a/docs/index.rst b/docs/index.rst index 7f742f6860..88de8da538 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,10 +5,9 @@ :hidden: :titlesonly: - content/getting_started/index - content/user_guide - content/api/index - content/release/index + User Guide + API Reference + Release Notes .. Project Index & Search .. ====================== diff --git a/docs/old-docs-files.txt b/docs/old-docs-files.txt new file mode 100644 index 0000000000..9107882ac3 --- /dev/null +++ b/docs/old-docs-files.txt @@ -0,0 +1,154 @@ +# This file contains a list of old html files that got generated when building +# the docs (simpeg v0.23.0). The list is used to create sphinx-reredirects, so +# the old links to these files can be redirected to the new locations. +# See docs/conf.py for more details. +content/examples/01-maps/index.html +content/examples/01-maps/plot_block_in_layer.html +content/examples/01-maps/plot_combo.html +content/examples/01-maps/plot_layer.html +content/examples/01-maps/plot_mesh2mesh.html +content/examples/01-maps/plot_sumMap.html +content/examples/01-maps/sg_execution_times.html +content/examples/02-gravity/index.html +content/examples/02-gravity/plot_inv_grav_tiled.html +content/examples/02-gravity/sg_execution_times.html +content/examples/03-magnetics/index.html +content/examples/03-magnetics/plot_0_analytic.html +content/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.html +content/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.html +content/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.html +content/examples/03-magnetics/sg_execution_times.html +content/examples/04-dcip/index.html +content/examples/04-dcip/plot_dc_analytic.html +content/examples/04-dcip/plot_inv_dcip_dipoledipole_3Dinversion_twospheres.html +content/examples/04-dcip/plot_inv_dcip_dipoledipole_parametric_inversion.html +content/examples/04-dcip/plot_read_DC_data_with_IO_class.html +content/examples/04-dcip/sg_execution_times.html +content/examples/05-fdem/index.html +content/examples/05-fdem/plot_0_fdem_analytic.html +content/examples/05-fdem/plot_inv_fdem_loop_loop_2Dinversion.html +content/examples/05-fdem/sg_execution_times.html +content/examples/06-tdem/index.html +content/examples/06-tdem/plot_0_tdem_analytic.html +content/examples/06-tdem/plot_fwd_tdem_3d_model.html +content/examples/06-tdem/plot_fwd_tdem_inductive_src_permeable_target.html +content/examples/06-tdem/plot_fwd_tdem_waveforms.html +content/examples/06-tdem/plot_inv_tdem_1D.html +content/examples/06-tdem/plot_inv_tdem_1D_raw_waveform.html +content/examples/06-tdem/sg_execution_times.html +content/examples/07-nsem/index.html +content/examples/07-nsem/plot_fwd_nsem_MTTipper3D.html +content/examples/07-nsem/sg_execution_times.html +content/examples/08-vrm/index.html +content/examples/08-vrm/plot_fwd_vrm.html +content/examples/08-vrm/plot_inv_vrm_eq.html +content/examples/08-vrm/sg_execution_times.html +content/examples/09-flow/index.html +content/examples/09-flow/plot_fwd_flow_richards_1D.html +content/examples/09-flow/plot_inv_flow_richards_1D.html +content/examples/09-flow/sg_execution_times.html +content/examples/10-pgi/index.html +content/examples/10-pgi/plot_inv_0_PGI_Linear_1D.html +content/examples/10-pgi/plot_inv_1_PGI_Linear_1D_joint_WithRelationships.html +content/examples/10-pgi/sg_execution_times.html +content/examples/20-published/index.html +content/examples/20-published/plot_booky_1Dstitched_resolve_inv.html +content/examples/20-published/plot_booky_1D_time_freq_inv.html +content/examples/20-published/plot_effective_medium_theory.html +content/examples/20-published/plot_heagyetal2017_casing.html +content/examples/20-published/plot_heagyetal2017_cyl_inversions.html +content/examples/20-published/plot_laguna_del_maule_inversion.html +content/examples/20-published/plot_load_booky.html +content/examples/20-published/plot_richards_celia1990.html +content/examples/20-published/plot_schenkel_morrison_casing.html +content/examples/20-published/plot_tomo_joint_with_volume.html +content/examples/20-published/plot_vadose_vangenuchten.html +content/examples/20-published/sg_execution_times.html +content/examples/index.html +content/examples/sg_execution_times.html +content/getting_started/big_picture.html +content/getting_started/contributing/advanced.html +content/getting_started/contributing/code-style.html +content/getting_started/contributing/documentation.html +content/getting_started/contributing/index.html +content/getting_started/contributing/pull-requests.html +content/getting_started/contributing/setting-up-environment.html +content/getting_started/contributing/testing.html +content/getting_started/contributing/working-with-github.html +content/getting_started/index.html +content/getting_started/installing.html +content/tutorials/01-models_mapping/index.html +content/tutorials/01-models_mapping/plot_1_tensor_models.html +content/tutorials/01-models_mapping/plot_2_cyl_models.html +content/tutorials/01-models_mapping/plot_3_tree_models.html +content/tutorials/01-models_mapping/sg_execution_times.html +content/tutorials/02-linear_inversion/index.html +content/tutorials/02-linear_inversion/plot_inv_1_inversion_lsq.html +content/tutorials/02-linear_inversion/plot_inv_2_inversion_irls.html +content/tutorials/02-linear_inversion/sg_execution_times.html +content/tutorials/03-gravity/index.html +content/tutorials/03-gravity/plot_1a_gravity_anomaly.html +content/tutorials/03-gravity/plot_1b_gravity_gradiometry.html +content/tutorials/03-gravity/plot_inv_1a_gravity_anomaly.html +content/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.html +content/tutorials/03-gravity/plot_inv_1c_gravity_anomaly_irls_compare_weighting.html +content/tutorials/03-gravity/sg_execution_times.html +content/tutorials/04-magnetics/index.html +content/tutorials/04-magnetics/plot_2a_magnetics_induced.html +content/tutorials/04-magnetics/plot_2b_magnetics_mvi.html +content/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.html +content/tutorials/04-magnetics/sg_execution_times.html +content/tutorials/05-dcr/index.html +content/tutorials/05-dcr/plot_fwd_1_dcr_sounding.html +content/tutorials/05-dcr/plot_fwd_2_dcr2d.html +content/tutorials/05-dcr/plot_fwd_3_dcr3d.html +content/tutorials/05-dcr/plot_gen_3_3d_to_2d.html +content/tutorials/05-dcr/plot_inv_1_dcr_sounding.html +content/tutorials/05-dcr/plot_inv_1_dcr_sounding_irls.html +content/tutorials/05-dcr/plot_inv_1_dcr_sounding_parametric.html +content/tutorials/05-dcr/plot_inv_2_dcr2d.html +content/tutorials/05-dcr/plot_inv_2_dcr2d_irls.html +content/tutorials/05-dcr/plot_inv_3_dcr3d.html +content/tutorials/05-dcr/sg_execution_times.html +content/tutorials/06-ip/index.html +content/tutorials/06-ip/plot_fwd_2_dcip2d.html +content/tutorials/06-ip/plot_fwd_3_dcip3d.html +content/tutorials/06-ip/plot_inv_2_dcip2d.html +content/tutorials/06-ip/plot_inv_3_dcip3d.html +content/tutorials/06-ip/sg_execution_times.html +content/tutorials/07-fdem/index.html +content/tutorials/07-fdem/plot_fwd_1_em1dfm_dispersive.html +content/tutorials/07-fdem/plot_fwd_1_em1dfm.html +content/tutorials/07-fdem/plot_fwd_2_fem_cyl.html +content/tutorials/07-fdem/plot_fwd_3_fem_3d.html +content/tutorials/07-fdem/plot_inv_1_em1dfm.html +content/tutorials/07-fdem/sg_execution_times.html +content/tutorials/08-tdem/index.html +content/tutorials/08-tdem/plot_fwd_1_em1dtm_dispersive.html +content/tutorials/08-tdem/plot_fwd_1_em1dtm.html +content/tutorials/08-tdem/plot_fwd_1_em1dtm_waveforms.html +content/tutorials/08-tdem/plot_fwd_2_tem_cyl.html +content/tutorials/08-tdem/plot_fwd_3_tem_3d.html +content/tutorials/08-tdem/plot_inv_1_em1dtm.html +content/tutorials/08-tdem/sg_execution_times.html +content/tutorials/09-nsem/index.html +content/tutorials/09-nsem/sg_execution_times.html +content/tutorials/10-vrm/index.html +content/tutorials/10-vrm/plot_fwd_1_vrm_layer.html +content/tutorials/10-vrm/plot_fwd_2_vrm_topsoil.html +content/tutorials/10-vrm/plot_fwd_3_vrm_tem.html +content/tutorials/10-vrm/sg_execution_times.html +content/tutorials/11-flow/index.html +content/tutorials/11-flow/sg_execution_times.html +content/tutorials/12-seismic/index.html +content/tutorials/12-seismic/plot_fwd_1_tomography_2D.html +content/tutorials/12-seismic/plot_inv_1_tomography_2D.html +content/tutorials/12-seismic/sg_execution_times.html +content/tutorials/13-joint_inversion/index.html +content/tutorials/13-joint_inversion/plot_inv_3_cross_gradient_pf.html +content/tutorials/13-joint_inversion/sg_execution_times.html +content/tutorials/14-pgi/index.html +content/tutorials/14-pgi/plot_inv_1_joint_pf_pgi_full_info_tutorial.html +content/tutorials/14-pgi/plot_inv_2_joint_pf_pgi_no_info_tutorial.html +content/tutorials/14-pgi/sg_execution_times.html +content/user_guide.html diff --git a/environment.yml b/environment.yml index ed09d55f72..7c5b181547 100644 --- a/environment.yml +++ b/environment.yml @@ -32,6 +32,7 @@ dependencies: - sphinx - sphinx-gallery>=0.1.13 - sphinxcontrib-apidoc + - sphinx-reredirects - pydata-sphinx-theme - empymod>=2.0.0 - nbsphinx diff --git a/pyproject.toml b/pyproject.toml index 3ce6d1d6b7..693d1e975d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,6 +72,7 @@ docs = [ "sphinx", "sphinx-gallery>=0.1.13", "sphinxcontrib-apidoc", + "sphinx-reredirects", "pydata-sphinx-theme", "nbsphinx", "empymod>=2.0.0", @@ -257,4 +258,4 @@ rst-roles = [ [tool.pytest.ini_options] filterwarnings = [ "ignore::simpeg.utils.solver_utils.DefaultSolverWarning", -] \ No newline at end of file +] From 55ccf523b4bb08e9730f2948ebccb950070a9d57 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 10 Mar 2025 10:48:40 -0700 Subject: [PATCH 113/194] Fix usage of "bug" label in bug report template (#1624) Correctly use the "bug" label in the issue template for bug reports. --- .github/ISSUE_TEMPLATE/bug-report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 153da7708e..4887a6e685 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -1,7 +1,7 @@ name: Bug report description: Report a bug in SimPEG. title: "BUG: " -labels: [Bug] +labels: ["bug"] body: - type: markdown From 5ae5e7b5210ec82015d37cbaf3084cf0246af2d3 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 10 Mar 2025 10:54:16 -0700 Subject: [PATCH 114/194] Fix redirects links in docs (#1623) Fix the number of `../` should be pre-appended to the target link to generate the right redirect. --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index a90da0c494..0f9795138e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -552,7 +552,7 @@ def _get_source_target(old_fname: str) -> tuple[str, str]: for old_dir, new_dir in MAPS.items(): if old_fname.startswith(old_dir): source = old_fname.removesuffix(".html") - n_parents = len(Path(old_fname).parents) + n_parents = len([p for p in Path(old_fname).parents if p != Path(".")]) target = "../" * n_parents + old_fname.replace(old_dir, new_dir, 1) return source, target raise ValueError() From b2e00068e1eabcc47eb743a4f4b072bbb0dc17e9 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 10 Mar 2025 14:00:22 -0700 Subject: [PATCH 115/194] Fix bug on `getJ` of gravity simulation (#1621) Assign the model before returning the Jacobian matrix. Don't use Numpy's `dot` function: it doesn't work propertly when mixing Numpy arrays and Scipy's sparse arrays. Use the matmul (`@`) operator instead. Add tests that catch the bug. --- simpeg/potential_fields/gravity/simulation.py | 14 +- tests/pf/test_forward_Grav_Linear.py | 146 ++++++++++++++++++ 2 files changed, 159 insertions(+), 1 deletion(-) diff --git a/simpeg/potential_fields/gravity/simulation.py b/simpeg/potential_fields/gravity/simulation.py index 5d52ab06a7..93df595c24 100644 --- a/simpeg/potential_fields/gravity/simulation.py +++ b/simpeg/potential_fields/gravity/simulation.py @@ -226,6 +226,7 @@ def fields(self, m): Gravity fields generated by the given model on every receiver location. """ + # Need to assign the model, so the rho property can be accessed. self.model = m if self.store_sensitivities == "forward_only": # Compute the linear operation without forming the full dense G @@ -241,6 +242,8 @@ def getJtJdiag(self, m, W=None, f=None): """ Return the diagonal of JtJ """ + # Need to assign the model, so the rhoDeriv can be computed (if the + # model is None, the rhoDeriv is going to be Zero). self.model = m if W is None: @@ -260,12 +263,18 @@ def getJ(self, m, f=None): """ Sensitivity matrix """ - return self.G.dot(self.rhoDeriv) + # Need to assign the model, so the rhoDeriv can be computed (if the + # model is None, the rhoDeriv is going to be Zero). + self.model = m + return self.G @ self.rhoDeriv def Jvec(self, m, v, f=None): """ Sensitivity times a vector """ + # Need to assign the model, so the rhoDeriv can be computed (if the + # model is None, the rhoDeriv is going to be Zero). + self.model = m dmu_dm_v = self.rhoDeriv @ v return self.G @ dmu_dm_v.astype(self.sensitivity_dtype, copy=False) @@ -273,6 +282,9 @@ def Jtvec(self, m, v, f=None): """ Sensitivity transposed times a vector """ + # Need to assign the model, so the rhoDeriv can be computed (if the + # model is None, the rhoDeriv is going to be Zero). + self.model = m Jtvec = self.G.T @ v.astype(self.sensitivity_dtype, copy=False) return np.asarray(self.rhoDeriv.T @ Jtvec) diff --git a/tests/pf/test_forward_Grav_Linear.py b/tests/pf/test_forward_Grav_Linear.py index 063fff9904..49f757f70f 100644 --- a/tests/pf/test_forward_Grav_Linear.py +++ b/tests/pf/test_forward_Grav_Linear.py @@ -5,6 +5,7 @@ import simpeg from simpeg import maps from simpeg.potential_fields import gravity +from simpeg.utils import model_builder from geoana.gravity import Prism import numpy as np @@ -417,6 +418,151 @@ def test_choclo_missing(self, simple_mesh, monkeypatch): gravity.Simulation3DIntegral(simple_mesh, engine="choclo") +class TestJacobianGravity: + """ + Test methods related to Jacobian matrix in gravity simulation. + """ + + atol_ratio = 1e-7 + + @pytest.fixture + def survey(self): + # Observation points + x = np.linspace(-20.0, 20.0, 4) + x, y = np.meshgrid(x, x) + z = 5.0 * np.ones_like(x) + coordinates = np.vstack((x.ravel(), y.ravel(), z.ravel())).T + receivers = gravity.receivers.Point(coordinates, components="gz") + source_field = gravity.sources.SourceField(receiver_list=[receivers]) + survey = gravity.survey.Survey(source_field) + return survey + + @pytest.fixture + def mesh(self): + # Mesh + dh = 5.0 + hx = [(dh, 4)] + mesh = discretize.TensorMesh([hx, hx, hx], "CCN") + return mesh + + @pytest.fixture + def densities(self, mesh): + # Define densities + densities = 1e-10 * np.ones(mesh.n_cells) + ind_sphere = model_builder.get_indices_sphere( + np.r_[0.0, 0.0, -20.0], 10.0, mesh.cell_centers + ) + densities[ind_sphere] = 0.2 + return densities + + @pytest.fixture(params=["identity_map", "exp_map"]) + def mapping(self, mesh, request): + mapping = ( + maps.IdentityMap(nP=mesh.n_cells) + if request.param == "identity_map" + else maps.ExpMap(nP=mesh.n_cells) + ) + return mapping + + @pytest.mark.parametrize("engine", ["choclo", "geoana"]) + def test_getJ(self, survey, mesh, densities, mapping, engine): + """ + Test the getJ method. + """ + simulation = gravity.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + rhoMap=mapping, + store_sensitivities="ram", + engine=engine, + ) + model = mapping * densities + jac = simulation.getJ(model) + # With an identity mapping, the jacobian should be the same as G. + # With an exp mapping, the jacobian should be G @ the mapping derivative. + identity_map = type(mapping) is maps.IdentityMap + expected_jac = ( + simulation.G if identity_map else simulation.G @ mapping.deriv(model) + ) + np.testing.assert_allclose(jac, expected_jac) + + @pytest.mark.parametrize("engine", ["choclo", "geoana"]) + def test_Jvec(self, survey, mesh, densities, mapping, engine): + """ + Test the Jvec method. + """ + simulation = gravity.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + rhoMap=mapping, + store_sensitivities="ram", + engine=engine, + ) + model = mapping * densities + + vector = np.random.default_rng(seed=42).uniform(size=densities.size) + dpred = simulation.Jvec(model, vector) + + identity_map = type(mapping) is maps.IdentityMap + expected_jac = ( + simulation.G if identity_map else simulation.G @ mapping.deriv(model) + ) + expected_dpred = expected_jac @ vector + + atol = np.max(np.abs(expected_dpred)) * self.atol_ratio + np.testing.assert_allclose(dpred, expected_dpred, atol=atol) + + @pytest.mark.parametrize("engine", ["choclo", "geoana"]) + def test_Jtvec(self, survey, mesh, densities, mapping, engine): + """ + Test the Jtvec method. + """ + simulation = gravity.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + rhoMap=mapping, + store_sensitivities="ram", + engine=engine, + ) + model = mapping * densities + + vector = np.random.default_rng(seed=42).uniform(size=survey.nD) + result = simulation.Jtvec(model, vector) + + identity_map = type(mapping) is maps.IdentityMap + expected_jac = ( + simulation.G if identity_map else simulation.G @ mapping.deriv(model) + ) + expected = expected_jac.T @ vector + + atol = np.max(np.abs(result)) * self.atol_ratio + np.testing.assert_allclose(result, expected, atol=atol) + + @pytest.mark.parametrize("engine", ["choclo", "geoana"]) + def test_getJtJdiag(self, survey, mesh, densities, mapping, engine): + """ + Test the getJtJdiag method. + """ + simulation = gravity.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + rhoMap=mapping, + store_sensitivities="ram", + engine=engine, + ) + model = mapping * densities + jtj_diag = simulation.getJtJdiag(model) + + identity_map = type(mapping) is maps.IdentityMap + expected_jac = ( + simulation.G if identity_map else simulation.G @ mapping.deriv(model) + ) + expected = np.diag(expected_jac.T @ expected_jac) + + atol = np.max(np.abs(jtj_diag)) * self.atol_ratio + np.testing.assert_allclose(jtj_diag, expected, atol=atol) + + class TestConversionFactor: """Test _get_conversion_factor function.""" From 985593fd655dd4a58443e7be9a938dab6b978a6f Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 11 Mar 2025 16:01:23 +0000 Subject: [PATCH 116/194] Fix redirect to user guide index page (#1627) Fix manual redirect for the index page of the User Guide. --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 0f9795138e..bee3ff60a5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -579,6 +579,6 @@ def build_redirects(): redirects.update( { "content/getting_started/index": "../../content/user-guide/index.html", - "content/user_guide": "../../content/user-guide/index.html", + "content/user_guide": "../content/user-guide/index.html", } ) From c05d6162c7590544591c8526eda185a5a38d70e9 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 18 Mar 2025 21:26:55 +0000 Subject: [PATCH 117/194] Move indexing of flat arrays to Survey classes (#1616) Add new `get_slice` and `get_all_slices` methods to `BaseSurvey` that mimics the behaviour of indexing `Data` objects. `get_slice` returns a `slice` object that can be used to slice flat arrays like data or uncertainty arrays. `get_all_slices` returns a dictionary with source-receiver pairs as keys, and their corresponding `slice` object as values. Optimize the implementation of these methods by working with slices rather than Numpy arrays with indices. Add tests for the new methods. Use `get_slice` in `Data`'s `__getitem__` and `__setitem__` instead of the `indexing_dictionary`. Add `typing_extensions` as a dependency for Python<3.13, and use the `deprecated` decorator to deprecate the `index_dictionary` property. --------- Co-authored-by: Joseph Capriotti --- .ci/environment_test.yml | 1 + environment.yml | 1 + pyproject.toml | 2 + simpeg/data.py | 24 +- .../frequency_domain/simulation.py | 16 +- .../natural_source/utils/plot_utils.py | 10 +- .../static/resistivity/simulation.py | 7 +- .../static/resistivity/simulation_2d.py | 8 +- .../time_domain/simulation.py | 17 +- simpeg/survey.py | 86 +++++++ tests/base/test_survey_data.py | 225 +++++++++++++++--- 11 files changed, 340 insertions(+), 57 deletions(-) diff --git a/.ci/environment_test.yml b/.ci/environment_test.yml index edb79f80f7..a8c6dc91cb 100644 --- a/.ci/environment_test.yml +++ b/.ci/environment_test.yml @@ -9,6 +9,7 @@ dependencies: - discretize>=0.11 - geoana>=0.7 - libdlf + - typing_extensions # optional dependencies - dask diff --git a/environment.yml b/environment.yml index 7c5b181547..66cb9d9c41 100644 --- a/environment.yml +++ b/environment.yml @@ -11,6 +11,7 @@ dependencies: - discretize>=0.11 - geoana>=0.7 - libdlf + - typing_extensions # solver # uncomment the next line if you are on an intel platform diff --git a/pyproject.toml b/pyproject.toml index 693d1e975d..3d0efb96d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ dependencies = [ "discretize>=0.11", "geoana>=0.7", "libdlf", + "typing_extensions; python_version<'3.13'", ] classifiers = [ "Development Status :: 4 - Beta", @@ -258,4 +259,5 @@ rst-roles = [ [tool.pytest.ini_options] filterwarnings = [ "ignore::simpeg.utils.solver_utils.DefaultSolverWarning", + "error:The `index_dictionary` property has been deprecated:FutureWarning", ] diff --git a/simpeg/data.py b/simpeg/data.py index 00be1504a7..b7732a320a 100644 --- a/simpeg/data.py +++ b/simpeg/data.py @@ -4,6 +4,15 @@ from .survey import BaseSurvey from .utils import mkvc, validate_ndarray_with_shape, validate_float, validate_type +try: + from warnings import deprecated +except ImportError: + # Use the deprecated decorator provided by typing_extensions (which + # supports older versions of Python) if it cannot be imported from + # warnings. + from typing_extensions import deprecated + + __all__ = ["Data", "SyntheticData"] @@ -284,6 +293,13 @@ def shape(self): return self.dobs.shape @property + @deprecated( + "The `index_dictionary` property has been deprecated. " + "Use the `get_slice()` or `get_all_slices()` methods provided " + "by the Survey object instead." + "This property will be removed in SimPEG v0.25.0.", + category=FutureWarning, + ) def index_dictionary(self): """Dictionary for indexing data by sources and receiver. @@ -328,12 +344,12 @@ def index_dictionary(self): ########################## def __setitem__(self, key, value): - index = self.index_dictionary[key[0]][key[1]] - self.dobs[index] = mkvc(value) + slice_obj = self.survey.get_slice(*key) + self.dobs[slice_obj] = mkvc(value) def __getitem__(self, key): - index = self.index_dictionary[key[0]][key[1]] - return self.dobs[index] + slice_obj = self.survey.get_slice(*key) + return self.dobs[slice_obj] def tovec(self): """Convert observed data to a vector diff --git a/simpeg/electromagnetics/frequency_domain/simulation.py b/simpeg/electromagnetics/frequency_domain/simulation.py index bb3f68fc8c..7fd14a15fd 100644 --- a/simpeg/electromagnetics/frequency_domain/simulation.py +++ b/simpeg/electromagnetics/frequency_domain/simulation.py @@ -290,9 +290,8 @@ def Jtvec(self, m, v, f=None): self.model = m - # Ensure v is a data object. - if not isinstance(v, Data): - v = Data(self.survey, v) + # Get dict of flat array slices for each source-receiver pair in the survey + survey_slices = self.survey.get_all_slices() Jtv = np.zeros(m.size) @@ -302,8 +301,9 @@ def Jtvec(self, m, v, f=None): df_duT_sum = 0 df_dmT_sum = 0 for rx in src.receiver_list: + src_rx_slice = survey_slices[src, rx] df_duT, df_dmT = rx.evalDeriv( - src, self.mesh, f, v=v[src, rx], adjoint=True + src, self.mesh, f, v=v[src_rx_slice], adjoint=True ) if not isinstance(df_duT, Zero): df_duT_sum += df_duT @@ -355,7 +355,8 @@ def getJ(self, m, f=None): Jmatrix = np.zeros((self.survey.nD, m_size)) - data = Data(self.survey) + # Get dict of flat array slices for each source-receiver pair in the survey + survey_slices = self.survey.get_all_slices() for A_i, freq in zip(Ainv, self.survey.frequencies): for src in self.survey.get_sources_by_frequency(freq): @@ -382,8 +383,9 @@ def getJ(self, m, f=None): du_dmT += np.hstack(df_dmT) block = np.array(du_dmT, dtype=complex).real.T - data_inds = data.index_dictionary[src][rx] - Jmatrix[data_inds] = block + + src_rx_slice = survey_slices[src, rx] + Jmatrix[src_rx_slice] = block self._Jmatrix = Jmatrix diff --git a/simpeg/electromagnetics/natural_source/utils/plot_utils.py b/simpeg/electromagnetics/natural_source/utils/plot_utils.py index 4be450445b..c608940fec 100644 --- a/simpeg/electromagnetics/natural_source/utils/plot_utils.py +++ b/simpeg/electromagnetics/natural_source/utils/plot_utils.py @@ -831,6 +831,10 @@ def _extract_location_data(data, location, orientation, component, return_uncert data_list = [] std_list = [] floor_list = [] + + # Get dict of flat array slices for each source-receiver pair in the survey + survey_slices = data.survey.get_all_slices() + for src in data.survey.source_list: rx_list = [ rx @@ -850,9 +854,9 @@ def _extract_location_data(data, location, orientation, component, return_uncert data_list.append(data[src, rx][ind_loc]) if return_uncert: - index = data.index_dictionary[src][rx] - std_list.append(data.relative_error[index][ind_loc]) - floor_list.append(data.noise_floor[index][ind_loc]) + src_rx_slice = survey_slices[src, rx] + std_list.append(data.relative_error[src_rx_slice][ind_loc]) + floor_list.append(data.noise_floor[src_rx_slice][ind_loc]) if return_uncert: return ( np.array(freq_list), diff --git a/simpeg/electromagnetics/static/resistivity/simulation.py b/simpeg/electromagnetics/static/resistivity/simulation.py index 476488a8a6..cd7c97016b 100644 --- a/simpeg/electromagnetics/static/resistivity/simulation.py +++ b/simpeg/electromagnetics/static/resistivity/simulation.py @@ -215,7 +215,6 @@ def _Jtvec(self, m, v=None, f=None): if isinstance(v, Data): v = v.dobs v = self._mini_survey_dataT(v) - v = Data(survey, v) Jtv = np.zeros(m.size) else: # This is for forming full sensitivity matrix @@ -223,13 +222,17 @@ def _Jtvec(self, m, v=None, f=None): istrt = int(0) iend = int(0) + # Get dict of flat array slices for each source-receiver pair in the survey + survey_slices = survey.get_all_slices() + for source in survey.source_list: u_source = f[source, self._solutionType].copy() for rx in source.receiver_list: # wrt f, need possibility wrt m if v is not None: + src_rx_slice = survey_slices[source, rx] PTv = rx.evalDeriv( - source, self.mesh, f, v[source, rx], adjoint=True + source, self.mesh, f, v[src_rx_slice], adjoint=True ) else: PTv = rx.evalDeriv(source, self.mesh, f).toarray().T diff --git a/simpeg/electromagnetics/static/resistivity/simulation_2d.py b/simpeg/electromagnetics/static/resistivity/simulation_2d.py index b37158343e..a405d0ccdd 100644 --- a/simpeg/electromagnetics/static/resistivity/simulation_2d.py +++ b/simpeg/electromagnetics/static/resistivity/simulation_2d.py @@ -367,16 +367,18 @@ def _Jtvec(self, m, v=None, f=None): v = self._mini_survey_dataT(v) Jtv = np.zeros(m.size, dtype=float) + # Get dict of flat array slices for each source-receiver pair in the survey + survey_slices = survey.get_all_slices() + for iky, ky in enumerate(kys): u_ky = f[:, self._solutionType, iky] - count = 0 for i_src, src in enumerate(survey.source_list): u_src = u_ky[:, i_src] df_duT_sum = 0 df_dmT_sum = 0 for rx in src.receiver_list: - my_v = v[count : count + rx.nD] - count += rx.nD + src_rx_slice = survey_slices[src, rx] + my_v = v[src_rx_slice] # wrt f, need possibility wrt m PTv = rx.evalDeriv(src, self.mesh, f, my_v, adjoint=True) df_duTFun = getattr(f, "_{0!s}Deriv".format(rx.projField), None) diff --git a/simpeg/electromagnetics/time_domain/simulation.py b/simpeg/electromagnetics/time_domain/simulation.py index c896e3d9d3..5f84cacbee 100644 --- a/simpeg/electromagnetics/time_domain/simulation.py +++ b/simpeg/electromagnetics/time_domain/simulation.py @@ -1,7 +1,6 @@ import numpy as np import scipy.sparse as sp -from ...data import Data from ...simulation import BaseTimeSimulation from ...utils import mkvc, sdiag, speye, Zero, validate_type, validate_float from ..base import BaseEMSimulation @@ -321,9 +320,8 @@ def Jtvec(self, m, v, f=None): self.model = m ftype = self._fieldType + "Solution" # the thing we solved for - # Ensure v is a data object. - if not isinstance(v, Data): - v = Data(self.survey, v) + # Get dict of flat array slices for each source-receiver pair in the survey + survey_slices = self.survey.get_all_slices() df_duT_v = self.Fields_Derivs(self) @@ -351,8 +349,9 @@ def Jtvec(self, m, v, f=None): ) for rx in src.receiver_list: + src_rx_slice = survey_slices[src, rx] PT_v[src, "{}Deriv".format(rx.projField), :] = rx.evalDeriv( - src, self.mesh, self.time_mesh, f, mkvc(v[src, rx]), adjoint=True + src, self.mesh, self.time_mesh, f, v[src_rx_slice], adjoint=True ) # this is += # PT_v = np.reshape(curPT_v,(len(curPT_v)/self.time_mesh.nN, @@ -1208,9 +1207,8 @@ def Jtvec(self, m, v, f=None): self.model = m ftype = self._fieldType + "Solution" # the thing we solved for - # Ensure v is a data object. - if not isinstance(v, Data): - v = Data(self.survey, v) + # Get dict of flat array slices for each source-receiver pair in the survey + survey_slices = self.survey.get_all_slices() df_duT_v = self.Fields_Derivs(self) @@ -1238,8 +1236,9 @@ def Jtvec(self, m, v, f=None): ) for rx in src.receiver_list: + src_rx_slice = survey_slices[src, rx] PT_v[src, "{}Deriv".format(rx.projField), :] = rx.evalDeriv( - src, self.mesh, self.time_mesh, f, mkvc(v[src, rx]), adjoint=True + src, self.mesh, self.time_mesh, f, v[src_rx_slice], adjoint=True ) # this is += diff --git a/simpeg/survey.py b/simpeg/survey.py index 57b534ea6a..b06fbc2bdf 100644 --- a/simpeg/survey.py +++ b/simpeg/survey.py @@ -554,6 +554,92 @@ def _n_fields(self): """number of fields required for solution""" return sum(src._fields_per_source for src in self.source_list) + def get_slice(self, source, receiver): + """ + Get slice to index a flat array for a given source-receiver pair. + + Use this method to index a data or uncertainty array for + a source-receiver pair of this survey. + + Parameters + ---------- + source : .BaseSrc + Source object. + receiver : .BaseRx + Receiver object. + + Returns + ------- + slice + + Raises + ------ + KeyError + If the given ``source`` or ``receiver`` do not belong to this survey. + + See also + -------- + .get_all_slices + """ + # Create generator for source and receiver pairs + source_receiver_pairs = ( + (src, rx) for src in self.source_list for rx in src.receiver_list + ) + # Get the start and end offsets for the given source and receiver, and + # build the slice + src_rx_slice = None + end_offset = 0 + for src, rx in source_receiver_pairs: + start_offset = end_offset + end_offset += rx.nD + if src is source and rx is receiver: + src_rx_slice = slice(start_offset, end_offset) + break + # Raise error if the source-receiver pair is not in the survey + if src_rx_slice is None: + msg = ( + f"Source '{source}' and receiver '{receiver}' pair " + "is not part of the survey." + ) + raise KeyError(msg) + return src_rx_slice + + def get_all_slices(self): + """ + Get slices to index a flat array for all source-receiver pairs. + + .. warning:: + + Survey objects are mutable objects. If the sources or receivers in + it get modified, slices generated with this method will not match + the arrays linked to the modified survey. + + Returns + ------- + dict[tuple[.BaseSrc, .BaseRx], slice] + Dictionary with flat array slices for every pair of source and + receiver in the survey. The keys are tuples of a single source and + a single receiver, and the values are the corresponding slice for + each one of them. + + See also + -------- + .get_slice + """ + # Create generator for source and receiver pairs + source_receiver_pairs = ( + (src, rx) for src in self.source_list for rx in src.receiver_list + ) + # Get the start and end offsets for all source-receiver pairs, and + # build the slices. + slices = {} + end_offset = 0 + for src, rx in source_receiver_pairs: + start_offset = end_offset + end_offset += rx.nD + slices[(src, rx)] = slice(start_offset, end_offset) + return slices + class BaseTimeSurvey(BaseSurvey): """Base SimPEG survey class for time-dependent simulations.""" diff --git a/tests/base/test_survey_data.py b/tests/base/test_survey_data.py index 9b06649e07..8b67a3795d 100644 --- a/tests/base/test_survey_data.py +++ b/tests/base/test_survey_data.py @@ -1,7 +1,11 @@ +import re +import pytest import unittest import numpy as np from simpeg import survey, utils, data +from simpeg.survey import BaseRx, BaseSrc, BaseSurvey + np.random.seed(100) @@ -26,35 +30,6 @@ def setUp(self): mysurvey = survey.BaseSurvey(source_list=source_list) self.D = data.Data(mysurvey) - def test_data(self): - V = [] - for src in self.D.survey.source_list: - for rx in src.receiver_list: - v = np.random.rand(rx.nD) - V += [v] - index = self.D.index_dictionary[src][rx] - self.D.dobs[index] = v - V = np.concatenate(V) - self.assertTrue(np.all(V == self.D.dobs)) - - D2 = data.Data(self.D.survey, V) - self.assertTrue(np.all(D2.dobs == self.D.dobs)) - - def test_standard_dev(self): - V = [] - for src in self.D.survey.source_list: - for rx in src.receiver_list: - v = np.random.rand(rx.nD) - V += [v] - index = self.D.index_dictionary[src][rx] - self.D.relative_error[index] = v - self.assertTrue(np.all(v == self.D.relative_error[index])) - V = np.concatenate(V) - self.assertTrue(np.all(V == self.D.relative_error)) - - D2 = data.Data(self.D.survey, relative_error=V) - self.assertTrue(np.all(D2.relative_error == self.D.relative_error)) - def test_uniqueSrcs(self): srcs = self.D.survey.source_list srcs += [srcs[0]] @@ -72,5 +47,197 @@ def test_sourceIndex(self): ) +class BaseFixtures: + @pytest.fixture + def sample_survey(self): + """Create sample Survey object.""" + x = np.linspace(5, 10, 3) + coordinates = utils.ndgrid(x, x, np.r_[0.0]) + source_location = np.r_[0, 0, 0.0] + receivers = [survey.BaseRx(coordinates) for i in range(4)] + sources = [survey.BaseSrc([rx], location=source_location) for rx in receivers] + sources.append(survey.BaseSrc(receivers, location=source_location)) + return survey.BaseSurvey(source_list=sources) + + @pytest.fixture + def sample_data(self, sample_survey): + """Create sample Data object.""" + return data.Data(sample_survey) + + +class TestDataIndexing(BaseFixtures): + """Test indexing of Data object.""" + + def get_source_receiver_pairs(self, survey): + """Return generator for each source-receiver pair in the survey.""" + source_receiver_pairs = ( + (src, rx) for src in survey.source_list for rx in src.receiver_list + ) + return source_receiver_pairs + + def test_getitem(self, sample_data): + """Test the ``Data.__getitem__`` method.""" + # Assign dobs to the data object + dobs = np.random.default_rng(seed=42).uniform(size=sample_data.survey.nD) + sample_data.dobs = dobs + + # Iterate over source-receiver pairs + survey_slices = sample_data.survey.get_all_slices() + for src, rx in self.get_source_receiver_pairs(sample_data.survey): + # Check if the __getitem__ returns the correct slice of the dobs + expected = dobs[survey_slices[src, rx]] + np.testing.assert_allclose(sample_data[src, rx], expected) + + def test_setitem(self, sample_data): + """Test the ``Data.__setitem__`` method.""" + # Assign dobs to the data object + rng = np.random.default_rng(seed=42) + dobs = rng.uniform(size=sample_data.survey.nD) + sample_data.dobs = dobs + + # Override the dobs array for each source-receiver pair + dobs_new = [] + for src, rx in self.get_source_receiver_pairs(sample_data.survey): + _dobs_new_piece = dobs = rng.uniform(size=rx.nD) + sample_data[src, rx] = _dobs_new_piece + dobs_new.append(_dobs_new_piece) + + # Check that the dobs in the data matches the new one + dobs_new = np.hstack(dobs_new) + np.testing.assert_allclose(dobs_new, sample_data.dobs) + + @pytest.mark.filterwarnings( + "ignore:The `index_dictionary` property has been deprecated." + ) + def test_index_dictionary(self, sample_data): + """Test the ``index_dictionary`` property.""" + # Assign dobs to the data object + dobs = np.random.default_rng(seed=42).uniform(size=sample_data.survey.nD) + sample_data.dobs = dobs + + # Check indices in index_dictionary for each source-receiver pair + survey_slices = sample_data.survey.get_all_slices() + for src, rx in self.get_source_receiver_pairs(sample_data.survey): + expected_slice_ = survey_slices[src, rx] + indices = sample_data.index_dictionary[src][rx] + np.testing.assert_allclose(dobs[indices], dobs[expected_slice_]) + + def test_deprecated_index_dictionary(self, sample_data): + """Test deprecation warning in ``index_dictionary``.""" + source = sample_data.survey.source_list[0] + receiver = source.receiver_list[0] + with pytest.warns( + FutureWarning, + match=re.escape("The `index_dictionary` property has been deprecated."), + ): + sample_data.index_dictionary[source][receiver] + + +class TestSurveySlice: + """ + Test BaseSurvey's slices for flat arrays. + """ + + def build_receiver(self, n_locs: int): + locs = np.ones(n_locs)[:, np.newaxis] + return BaseRx(locs) + + @pytest.mark.parametrize( + "all_slices", [True, False], ids=["all_slices", "single_slice"] + ) + def test_single_source(self, all_slices): + """ + Test slicing a survey with a single source. + """ + n_locs = (4, 7) + receivers = [self.build_receiver(n_locs=i) for i in n_locs] + source = BaseSrc(receivers) + test_survey = BaseSurvey([source]) + if all_slices: + expected = { + (source, receivers[0]): slice(0, 4), + (source, receivers[1]): slice(4, 4 + 7), + } + slices = test_survey.get_all_slices() + assert slices == expected + else: + assert test_survey.get_slice(source, receivers[0]) == slice(0, 4) + assert test_survey.get_slice(source, receivers[1]) == slice(4, 4 + 7) + + @pytest.mark.parametrize( + "all_slices", [True, False], ids=["all_slices", "single_slices"] + ) + def test_multiple_sources_shared_receivers(self, all_slices): + """ + Test slicing a survey with multiple sources and shared receivers. + """ + n_locs = (4, 7) + receivers = [self.build_receiver(n_locs=i) for i in n_locs] + sources = [BaseSrc(receivers), BaseSrc(receivers)] + test_survey = BaseSurvey(sources) + if all_slices: + expected = { + (sources[0], receivers[0]): slice(0, 4), + (sources[0], receivers[1]): slice(4, 4 + 7), + (sources[1], receivers[0]): slice(11, 11 + 4), + (sources[1], receivers[1]): slice(15, 15 + 7), + } + slices = test_survey.get_all_slices() + assert slices == expected + else: + assert test_survey.get_slice(sources[0], receivers[0]) == slice(0, 4) + assert test_survey.get_slice(sources[0], receivers[1]) == slice(4, 4 + 7) + assert test_survey.get_slice(sources[1], receivers[0]) == slice(11, 11 + 4) + assert test_survey.get_slice(sources[1], receivers[1]) == slice(15, 15 + 7) + + @pytest.mark.parametrize( + "all_slices", [True, False], ids=["all_slices", "single_slices"] + ) + def test_multiple_sources(self, all_slices): + """ + Test slicing a survey with multiple sources. + """ + receivers_a = [self.build_receiver(n_locs=i) for i in (4, 7)] + receivers_b = [self.build_receiver(n_locs=i) for i in (8, 3)] + srcs = [BaseSrc(receivers_a), BaseSrc(receivers_b)] + test_survey = BaseSurvey(srcs) + if all_slices: + expected = { + (srcs[0], receivers_a[0]): slice(0, 4), + (srcs[0], receivers_a[1]): slice(4, 4 + 7), + (srcs[1], receivers_b[0]): slice(11, 11 + 8), + (srcs[1], receivers_b[1]): slice(19, 19 + 3), + } + slices = test_survey.get_all_slices() + assert slices == expected + else: + assert test_survey.get_slice(srcs[0], receivers_a[0]) == slice(0, 4) + assert test_survey.get_slice(srcs[0], receivers_a[1]) == slice(4, 4 + 7) + assert test_survey.get_slice(srcs[1], receivers_b[0]) == slice(11, 11 + 8) + assert test_survey.get_slice(srcs[1], receivers_b[1]) == slice(19, 19 + 3) + + @pytest.mark.parametrize("missing", ["source", "receiver", "both"]) + def test_missing_source_receiver(self, missing): + """ + Test error on missing source-receiver pair. + """ + # Generate a survey + receivers_a = [self.build_receiver(n_locs=i) for i in (4, 7)] + receivers_b = [self.build_receiver(n_locs=i) for i in (8, 3)] + sources = [BaseSrc(receivers_a), BaseSrc(receivers_b)] + test_survey = BaseSurvey(sources) + # Try to slice with missing source-receiver pair + src, rx = sources[0], receivers_a[1] + if missing in ("source", "both"): + src = BaseSrc() # new src not in the survey + if missing in ("receiver", "both"): + rx = self.build_receiver(1) # new rx not in the survey + msg = re.escape( + f"Source '{src}' and receiver '{rx}' pair " "is not part of the survey." + ) + with pytest.raises(KeyError, match=msg): + test_survey.get_slice(src, rx) + + if __name__ == "__main__": unittest.main() From d84a050bbde37e8be95de7e37ce777e50a6238b7 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 20 Mar 2025 20:21:29 +0000 Subject: [PATCH 118/194] Replace Data indexing for Survey slicing where needed (#1632) --- .../frequency_domain/simulation.py | 11 +++++++---- .../electromagnetics/natural_source/survey.py | 3 ++- .../natural_source/utils/data_utils.py | 5 ++++- .../natural_source/utils/test_utils.py | 7 ++++--- .../static/induced_polarization/simulation.py | 10 ++++++---- .../static/resistivity/survey.py | 7 ++++--- simpeg/simulation.py | 18 +++++++++++------- .../test_Problem1D_AnalyticVsNumeric.py | 11 ++++++++--- 8 files changed, 46 insertions(+), 26 deletions(-) diff --git a/simpeg/electromagnetics/frequency_domain/simulation.py b/simpeg/electromagnetics/frequency_domain/simulation.py index 7fd14a15fd..726052f30b 100644 --- a/simpeg/electromagnetics/frequency_domain/simulation.py +++ b/simpeg/electromagnetics/frequency_domain/simulation.py @@ -3,7 +3,6 @@ from discretize.utils import Zero from ... import props -from ...data import Data from ...utils import mkvc, validate_type from ..base import BaseEMSimulation from ..utils import omega @@ -241,7 +240,8 @@ def Jvec(self, m, v, f=None): self.model = m - Jv = Data(self.survey) + survey_slices = self.survey.get_all_slices() + Jv = np.full(self.survey.nD, fill_value=np.nan) for nf, freq in enumerate(self.survey.frequencies): for src in self.survey.get_sources_by_frequency(freq): @@ -250,9 +250,12 @@ def Jvec(self, m, v, f=None): dRHS_dm_v = self.getRHSDeriv(freq, src, v) du_dm_v = self.Ainv[nf] * (-dA_dm_v + dRHS_dm_v) for rx in src.receiver_list: - Jv[src, rx] = rx.evalDeriv(src, self.mesh, f, du_dm_v=du_dm_v, v=v) + src_rx_slice = survey_slices[src, rx] + Jv[src_rx_slice] = mkvc( + rx.evalDeriv(src, self.mesh, f, du_dm_v=du_dm_v, v=v) + ) - return Jv.dobs + return Jv def Jtvec(self, m, v, f=None): r"""Compute the adjoint sensitivity matrix times a vector. diff --git a/simpeg/electromagnetics/natural_source/survey.py b/simpeg/electromagnetics/natural_source/survey.py index 18023547e2..fc3751dc1d 100644 --- a/simpeg/electromagnetics/natural_source/survey.py +++ b/simpeg/electromagnetics/natural_source/survey.py @@ -80,6 +80,7 @@ def toRecArray(self, returnType="RealImag"): ("tzy", complex), ] + survey_slices = self.survey.get_all_slices() for src in self.survey.source_list: # Temp array for all the receivers of the source. # Note: needs to be written more generally, @@ -100,7 +101,7 @@ def toRecArray(self, returnType="RealImag"): ).view(dtRI) # Get the type and the value for the DataNSEM object as a list typeList = [ - [rx.orientation, rx.component, self[src, rx]] + [rx.orientation, rx.component, self.dobs[survey_slices[src, rx]]] for rx in src.receiver_list ] # Insert the values to the temp array diff --git a/simpeg/electromagnetics/natural_source/utils/data_utils.py b/simpeg/electromagnetics/natural_source/utils/data_utils.py index eb1577be2b..3d032af034 100644 --- a/simpeg/electromagnetics/natural_source/utils/data_utils.py +++ b/simpeg/electromagnetics/natural_source/utils/data_utils.py @@ -61,9 +61,12 @@ def extract_data_info(NSEMdata): """ dL, freqL, rxTL = [], [], [] + survey_slices = NSEMdata.survey.get_all_slices() + for src in NSEMdata.survey.source_list: for rx in src.receiver_list: - dL.append(NSEMdata[src, rx]) + src_rx_slice = survey_slices[src, rx] + dL.append(NSEMdata.dobs[src_rx_slice]) freqL.append(np.ones(rx.nD) * src.frequency) if isinstance(rx, PointNaturalSource): rxTL.extend((("z" + rx.orientation + " ") * rx.nD).split()) diff --git a/simpeg/electromagnetics/natural_source/utils/test_utils.py b/simpeg/electromagnetics/natural_source/utils/test_utils.py index 34943aae44..17e50932f8 100644 --- a/simpeg/electromagnetics/natural_source/utils/test_utils.py +++ b/simpeg/electromagnetics/natural_source/utils/test_utils.py @@ -1,7 +1,7 @@ import numpy as np import discretize -from simpeg import maps, mkvc, utils, Data +from simpeg import maps, mkvc, utils from ....utils import unpack_widths from ..receivers import ( PointNaturalSource, @@ -18,9 +18,9 @@ def getAppResPhs(NSEMdata, survey): - NSEMdata = Data(dobs=NSEMdata, survey=survey) # Make impedance zList = [] + survey_slices = survey.get_all_slices() for src in survey.source_list: zc = [src.frequency] for rx in src.receiver_list: @@ -28,7 +28,8 @@ def getAppResPhs(NSEMdata, survey): m = 1j else: m = 1 - zc.append(m * NSEMdata[src, rx]) + src_rx_slice = survey_slices[src, rx] + zc.append(m * NSEMdata[src_rx_slice]) zList.append(zc) return [ appResPhs(zList[i][0], np.sum(zList[i][1:3])) for i in np.arange(len(zList)) diff --git a/simpeg/electromagnetics/static/induced_polarization/simulation.py b/simpeg/electromagnetics/static/induced_polarization/simulation.py index 321c86c0ef..30c7dae47c 100644 --- a/simpeg/electromagnetics/static/induced_polarization/simulation.py +++ b/simpeg/electromagnetics/static/induced_polarization/simulation.py @@ -5,7 +5,7 @@ from .... import maps, props from ....base import BasePDESimulation -from ....data import Data +from ....utils import mkvc from ..resistivity import Simulation2DCellCentered as DC_2D_CC from ..resistivity import Simulation2DNodal as DC_2D_N from ..resistivity import Simulation3DCellCentered as DC_3D_CC @@ -43,7 +43,8 @@ def rhoDeriv(self): @cached_property def _scale(self): - scale = Data(self.survey, np.ones(self.survey.nD)) + survey_slices = self.survey.get_all_slices() + scale = np.ones(self.survey.nD) if self._f is None: # re-uses the DC simulation's fields method self._f = super().fields(None) @@ -55,8 +56,9 @@ def _scale(self): for src in self.survey.source_list: for rx in src.receiver_list: if rx.data_type == "apparent_chargeability": - scale[src, rx] = 1.0 / rx.eval(src, self.mesh, f) - return scale.dobs + src_rx_slice = survey_slices[src, rx] + scale[src_rx_slice] = mkvc(1.0 / rx.eval(src, self.mesh, f)) + return scale eta, etaMap, etaDeriv = props.Invertible("Electrical Chargeability (V/V)") diff --git a/simpeg/electromagnetics/static/resistivity/survey.py b/simpeg/electromagnetics/static/resistivity/survey.py index 1da25060d4..ce0cdc16d1 100644 --- a/simpeg/electromagnetics/static/resistivity/survey.py +++ b/simpeg/electromagnetics/static/resistivity/survey.py @@ -8,7 +8,6 @@ from . import receivers as Rx from . import sources as Src from ..utils import static_utils -from simpeg import data class Survey(BaseSurvey): @@ -224,13 +223,15 @@ def set_geometric_factor( geometric_factor = static_utils.geometric_factor(self, space_type=space_type) - geometric_factor = data.Data(self, geometric_factor) + # geometric_factor = data.Data(self, geometric_factor) + survey_slices = self.get_all_slices() for source in self.source_list: for rx in source.receiver_list: if data_type is not None: rx.data_type = data_type if rx.data_type == "apparent_resistivity": - rx._geometric_factor[source] = geometric_factor[source, rx] + src_rx_slice = survey_slices[source, rx] + rx._geometric_factor[source] = geometric_factor[src_rx_slice] return geometric_factor def _set_abmn_locations(self): diff --git a/simpeg/simulation.py b/simpeg/simulation.py index fbfdb3404d..bbde7df0f7 100644 --- a/simpeg/simulation.py +++ b/simpeg/simulation.py @@ -11,7 +11,7 @@ from . import props from .typing import RandomSeed -from .data import SyntheticData, Data +from .data import SyntheticData from .survey import BaseSurvey from .utils import ( Counter, @@ -187,11 +187,13 @@ def dpred(self, m=None, f=None): f = self.fields(m) - data = Data(self.survey) + survey_slices = self.survey.get_all_slices() + dpred = np.full(self.survey.nD, np.nan) for src in self.survey.source_list: for rx in src.receiver_list: - data[src, rx] = rx.eval(src, self.mesh, f) - return mkvc(data) + src_rx_slice = survey_slices[src, rx] + dpred[src_rx_slice] = mkvc(rx.eval(src, self.mesh, f)) + return mkvc(dpred) @timeIt def Jvec(self, m, v, f=None): @@ -596,11 +598,13 @@ def dpred(self, m=None, f=None): if f is None: f = self.fields(m) - data = Data(self.survey) + survey_slices = self.survey.get_all_slices() + dpred = np.full(self.survey.nD, np.nan) for src in self.survey.source_list: for rx in src.receiver_list: - data[src, rx] = rx.eval(src, self.mesh, self.time_mesh, f) - return data.dobs + src_rx_slice = survey_slices[src, rx] + dpred[src_rx_slice] = mkvc(rx.eval(src, self.mesh, self.time_mesh, f)) + return dpred ############################################################################## diff --git a/tests/em/nsem/forward/test_Problem1D_AnalyticVsNumeric.py b/tests/em/nsem/forward/test_Problem1D_AnalyticVsNumeric.py index 4a0ef2ee3e..7aa36f6144 100644 --- a/tests/em/nsem/forward/test_Problem1D_AnalyticVsNumeric.py +++ b/tests/em/nsem/forward/test_Problem1D_AnalyticVsNumeric.py @@ -16,6 +16,7 @@ def appResPhs(freq, z): return app_res, app_phs zList = [] + survey_slices = nsemdata.survey.get_all_slices() for src in nsemdata.survey.source_list: zc = [src.frequency] for rx in src.receiver_list: @@ -23,7 +24,8 @@ def appResPhs(freq, z): m = 1j else: m = 1 - zc.append(m * nsemdata[src, rx]) + src_rx_slice = survey_slices[src, rx] + zc.append(m * nsemdata.dobs[src_rx_slice]) zList.append(zc) return [ appResPhs(zList[i][0], np.sum(zList[i][1:3])) for i in np.arange(len(zList)) @@ -32,7 +34,8 @@ def appResPhs(freq, z): def calculateAnalyticSolution(source_list, mesh, model): surveyAna = nsem.Survey(source_list) - data1D = nsem.Data(surveyAna) + survey_slices = surveyAna.get_all_slices() + data1D = np.full(surveyAna.nD, np.nan) for src in surveyAna.source_list: elev = src.receiver_list[0].locations_e[0] anaEd, anaEu, anaHd, anaHu = nsem.utils.analytic_1d.getEHfields( @@ -45,7 +48,9 @@ def calculateAnalyticSolution(source_list, mesh, model): # anaH = (anaHtemp/anaEtemp[-1])#.conj() anaZ = anaE / anaH for rx in src.receiver_list: - data1D[src, rx] = getattr(anaZ, rx.component) + src_rx_slice = survey_slices[src, rx] + data1D[src_rx_slice] = getattr(anaZ, rx.component) + data1D = nsem.Data(surveyAna, data1D) return data1D From 1148ef1e9e8bf6388ce6ff766c5c663e69e50635 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 21 Mar 2025 00:16:50 +0000 Subject: [PATCH 119/194] Implement `G` matrix as `LinearOperator` in gravity simulation (#1622) Make the `G` property in the gravity survey to be either a 2D Numpy array, or a `scipy.sparse.LinearOperator` if `store_sensitivities` is set to `"forward_only"`. The `LinearOperator` is capable of performing operations like `G @ m` and `G.T @ v` without allocating the full `G` matrix on memory or disk. This allows `Jvec` and `Jtvec` to be called and not allocate the full `G` matrix. The `getJ` method also returns either a 2D Numpy array or a `LinearOperator` formed by the product between `G` and the `rhoDeriv` as a `LinearOperator` as well. Reimplement the `getJtJdiag` method: allow it to get the diagonal without building `G` if `"forward_only"`, optimize the computation of the diagonal when `G` is fully formed through `einsum`s, and fix the cache of `gtg_diagonal` by checking the hash of the `W` argument. Deprecate the public `gtg_diagonal` property. Extend docstrings of relevant methods. Add tests for all the new features. --------- Co-authored-by: Joseph Capriotti --- .../gravity/_numba_functions.py | 306 ++++++++++++++++ simpeg/potential_fields/gravity/simulation.py | 334 ++++++++++++++++-- tests/pf/test_forward_Grav_Linear.py | 325 ++++++++++++++++- 3 files changed, 911 insertions(+), 54 deletions(-) diff --git a/simpeg/potential_fields/gravity/_numba_functions.py b/simpeg/potential_fields/gravity/_numba_functions.py index fe2e69e202..b43a45dd73 100644 --- a/simpeg/potential_fields/gravity/_numba_functions.py +++ b/simpeg/potential_fields/gravity/_numba_functions.py @@ -163,6 +163,312 @@ def _sensitivity_gravity( ) +@jit(nopython=True, parallel=False) +def _diagonal_G_T_dot_G_serial( + receivers, + nodes, + cell_nodes, + kernel_func, + constant_factor, + weights, + diagonal, +): + """ + Diagonal of ``G.T @ W.T @ W @ G`` without storing ``G``, in serial. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + nodes : (n_active_nodes, 3) numpy.ndarray + Array with the location of the mesh nodes. + cell_nodes : (n_active_cells, 8) numpy.ndarray + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + kernel_func : callable + Kernel function that will be evaluated on each node of the mesh. Choose + one of the kernel functions in ``choclo.prism``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + weights : (n_receivers,) numpy.ndarray + Array with data weights. It should be the diagonal of the ``W`` matrix, + squared. + diagonal : (n_active_cells,) numpy.ndarray + Array where the diagonal of ``G.T @ G`` will be added to. + + Notes + ----- + This function is meant to be run in serial. Use the + ``_diagonal_G_T_dot_G_parallel`` one for parallelized computations. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vector for kernels evaluated on mesh nodes + kernels = np.empty(n_nodes) + for j in range(n_nodes): + kernels[j] = _evaluate_kernel( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + nodes[j, 0], + nodes[j, 1], + nodes[j, 2], + kernel_func, + ) + # Add diagonal components in the running result. + for k in range(n_cells): + diagonal[k] += ( + weights[i] + * ( + constant_factor + * kernels_in_nodes_to_cell( + kernels, + cell_nodes[k, :], + ) + ) + ** 2 + ) + + +@jit(nopython=True, parallel=True) +def _diagonal_G_T_dot_G_parallel( + receivers, + nodes, + cell_nodes, + kernel_func, + constant_factor, + weights, + diagonal, +): + """ + Diagonal of ``G.T @ W.T @ W @ G`` without storing ``G``, in parallel. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + nodes : (n_active_nodes, 3) numpy.ndarray + Array with the location of the mesh nodes. + cell_nodes : (n_active_cells, 8) numpy.ndarray + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + kernel_func : callable + Kernel function that will be evaluated on each node of the mesh. Choose + one of the kernel functions in ``choclo.prism``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + weights : (n_receivers,) numpy.ndarray + Array with data weights. It should be the diagonal of the ``W`` matrix, + squared. + diagonal : (n_active_cells,) numpy.ndarray + Array where the diagonal of ``G.T @ G`` will be added to. + + Notes + ----- + This function is meant to be run in parallel. Use the + ``_diagonal_G_T_dot_G_serial`` one for serialized computations. + + This implementation instructs each thread to allocate their own array for + the current row of the sensitivity matrix. After computing the elements of + that row, it gets added to the running ``result`` array through a reduction + operation handled by Numba. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vector for kernels evaluated on mesh nodes + kernels = np.empty(n_nodes) + for j in range(n_nodes): + kernels[j] = _evaluate_kernel( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + nodes[j, 0], + nodes[j, 1], + nodes[j, 2], + kernel_func, + ) + # Allocate array for the diagonal elements for the current receiver. + local_diagonal = np.empty(n_cells) + for k in range(n_cells): + local_diagonal[k] = ( + weights[i] + * ( + constant_factor + * kernels_in_nodes_to_cell( + kernels, + cell_nodes[k, :], + ) + ) + ** 2 + ) + # Add the result to the diagonal. + # Apply reduction operation to add the values of the local diagonal to + # the running diagonal array. Avoid slicing the `diagonal` array when + # updating it to avoid racing conditions, just add the `local_diagonal` + # to the `diagonal` variable. + diagonal += local_diagonal + + +@jit(nopython=True, parallel=False) +def _sensitivity_gravity_t_dot_v_serial( + receivers, + nodes, + cell_nodes, + kernel_func, + constant_factor, + vector, + result, +): + """ + Compute ``G.T @ v`` in serial, without building G. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + nodes : (n_active_nodes, 3) numpy.ndarray + Array with the location of the mesh nodes. + cell_nodes : (n_active_cells, 8) numpy.ndarray + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + kernel_func : callable + Kernel function that will be evaluated on each node of the mesh. Choose + one of the kernel functions in ``choclo.prism``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + vector : (n_receivers) numpy.ndarray + Array that represents the vector used in the dot product. + result : (n_active_cells) numpy.ndarray + Running result array where the output of the dot product will be added to. + + Notes + ----- + This function is meant to be run in serial. Writing to the ``result`` array + inside a parallel loop over the receivers generates a race condition that + leads to corrupted outputs. + + A parallel implementation of this function is available in + ``_sensitivity_gravity_t_dot_v_parallel``. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + # Evaluate kernel function on each node, for each receiver location + for i in range(n_receivers): + # Allocate vector for kernels evaluated on mesh nodes + kernels = np.empty(n_nodes) + for j in range(n_nodes): + kernels[j] = _evaluate_kernel( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + nodes[j, 0], + nodes[j, 1], + nodes[j, 2], + kernel_func, + ) + # Compute the i-th row of the sensitivity matrix and multiply it by the + # i-th element of the vector. + for k in range(n_cells): + result[k] += ( + constant_factor + * vector[i] + * kernels_in_nodes_to_cell( + kernels, + cell_nodes[k, :], + ) + ) + + +@jit(nopython=True, parallel=True) +def _sensitivity_gravity_t_dot_v_parallel( + receivers, + nodes, + cell_nodes, + kernel_func, + constant_factor, + vector, + result, +): + """ + Compute ``G.T @ v`` in parallel without building G. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + nodes : (n_active_nodes, 3) numpy.ndarray + Array with the location of the mesh nodes. + cell_nodes : (n_active_cells, 8) numpy.ndarray + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + kernel_func : callable + Kernel function that will be evaluated on each node of the mesh. Choose + one of the kernel functions in ``choclo.prism``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + vector : (n_receivers) numpy.ndarray + Array that represents the vector used in the dot product. + result : (n_active_cells) numpy.ndarray + Running result array where the output of the dot product will be added to. + + Notes + ----- + This function is meant to be run in parallel. + This implementation instructs each thread to allocate their own array for + the current row of the sensitivity matrix. After computing the elements of + that row, it gets added to the running ``result`` array through a reduction + operation handled by Numba. + + A serialized implementation of this function is available in + ``_sensitivity_gravity_t_dot_v_serial``. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vector for kernels evaluated on mesh nodes + kernels = np.empty(n_nodes) + # Allocate array for the current row of the sensitivity matrix + local_row = np.empty(n_cells) + for j in range(n_nodes): + kernels[j] = _evaluate_kernel( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + nodes[j, 0], + nodes[j, 1], + nodes[j, 2], + kernel_func, + ) + # Compute fields from the kernel values + for k in range(n_cells): + local_row[k] = ( + constant_factor + * vector[i] + * kernels_in_nodes_to_cell( + kernels, + cell_nodes[k, :], + ) + ) + # Apply reduction operation to add the values of the row to the running + # result. Avoid slicing the `result` array when updating it to avoid + # racing conditions, just add the `local_row` to the `results` + # variable. + result += local_row + + @jit(nopython=True) def _evaluate_kernel( receiver_x, receiver_y, receiver_z, node_x, node_y, node_z, kernel_func diff --git a/simpeg/potential_fields/gravity/simulation.py b/simpeg/potential_fields/gravity/simulation.py index 93df595c24..9df88fbe74 100644 --- a/simpeg/potential_fields/gravity/simulation.py +++ b/simpeg/potential_fields/gravity/simulation.py @@ -1,9 +1,12 @@ from __future__ import annotations +import hashlib import warnings import numpy as np +from numpy.typing import NDArray import scipy.constants as constants from geoana.kernels import prism_fz, prism_fzx, prism_fzy, prism_fzz from scipy.constants import G as NewtG +from scipy.sparse.linalg import LinearOperator, aslinearoperator from simpeg import props from simpeg.utils import mkvc, sdiag @@ -17,12 +20,24 @@ _sensitivity_gravity_parallel, _forward_gravity_serial, _forward_gravity_parallel, + _sensitivity_gravity_t_dot_v_serial, + _sensitivity_gravity_t_dot_v_parallel, _forward_gravity_2d_mesh_serial, _forward_gravity_2d_mesh_parallel, _sensitivity_gravity_2d_mesh_serial, _sensitivity_gravity_2d_mesh_parallel, + _diagonal_G_T_dot_G_serial, + _diagonal_G_T_dot_G_parallel, ) +try: + from warnings import deprecated +except ImportError: + # Use the deprecated decorator provided by typing_extensions (which + # supports older versions of Python) if it cannot be imported from + # warnings. + from typing_extensions import deprecated + if choclo is not None: from numba import jit @@ -155,7 +170,9 @@ class Simulation3DIntegral(BasePFSimulation): - 'ram': sensitivities are stored in the computer's RAM - 'disk': sensitivities are written to a directory - 'forward_only': you intend only do perform a forward simulation and - sensitivities do not need to be stored + sensitivities do not need to be stored. The sensitivity matrix ``G`` + is never created, but it'll be defined as + a :class:`~scipy.sparse.linalg.LinearOperator`. sensitivity_path : str, optional Path to store the sensitivity matrix if ``store_sensitivities`` is set @@ -188,8 +205,6 @@ def __init__( super().__init__(mesh, engine=engine, numba_parallel=numba_parallel, **kwargs) self.rho = rho self.rhoMap = rhoMap - self._G = None - self._gtg_diagonal = None self.modelMap = self.rhoMap # Warn if n_processes has been passed @@ -207,9 +222,13 @@ def __init__( if self.numba_parallel: self._sensitivity_gravity = _sensitivity_gravity_parallel self._forward_gravity = _forward_gravity_parallel + self._sensitivity_t_dot_v = _sensitivity_gravity_t_dot_v_parallel + self._diagonal_G_T_dot_G = _diagonal_G_T_dot_G_parallel else: self._sensitivity_gravity = _sensitivity_gravity_serial self._forward_gravity = _forward_gravity_serial + self._sensitivity_t_dot_v = _sensitivity_gravity_t_dot_v_serial + self._diagonal_G_T_dot_G = _diagonal_G_T_dot_G_serial def fields(self, m): """ @@ -217,8 +236,8 @@ def fields(self, m): Parameters ---------- - m : (n_active_cells,) numpy.ndarray - Array with values for the model. + m : (n_param,) numpy.ndarray + The model parameters. Returns ------- @@ -239,38 +258,154 @@ def fields(self, m): return np.asarray(fields) def getJtJdiag(self, m, W=None, f=None): - """ - Return the diagonal of JtJ + r""" + Compute diagonal of :math:`\mathbf{J}^T \mathbf{J}``. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. + W : (nD, nD) np.ndarray or scipy.sparse.sparray, optional + Diagonal matrix with the square root of the weights. If not None, + the function returns the diagonal of + :math:`\mathbf{J}^T \mathbf{W}^T \mathbf{W} \mathbf{J}``. + f : Ignored + Not used, present here for API consistency by convention. + + Returns + ------- + (n_active_cells) np.ndarray + Array with the diagonal of ``J.T @ J``. + + Notes + ----- + If ``store_sensitivities`` is ``"forward_only"``, the ``G`` matrix is + never allocated in memory, and the diagonal is obtained by + accumulation, computing each element of the ``G`` matrix on the fly. + + This method caches the diagonal ``G.T @ W.T @ W @ G`` and the sha256 + hash of the diagonal of the ``W`` matrix. This way, if same weights are + passed to it, it reuses the cached diagonal so it doesn't need to be + recomputed. + If new weights are passed, the cache is updated with the latest + diagonal of ``G.T @ W.T @ W @ G``. """ # Need to assign the model, so the rhoDeriv can be computed (if the # model is None, the rhoDeriv is going to be Zero). self.model = m - if W is None: - W = np.ones(self.survey.nD) - else: - W = W.diagonal() ** 2 - if getattr(self, "_gtg_diagonal", None) is None: - diag = np.zeros(self.G.shape[1]) - for i in range(len(W)): - diag += W[i] * (self.G[i] * self.G[i]) - self._gtg_diagonal = diag - else: - diag = self._gtg_diagonal - return mkvc((sdiag(np.sqrt(diag)) @ self.rhoDeriv).power(2).sum(axis=0)) + # We should probably check that W is diagonal. Let's assume it for now. + weights = ( + W.diagonal() ** 2 + if W is not None + else np.ones(self.survey.nD, dtype=np.float64) + ) + + # Compute gtg (G.T @ W.T @ W @ G) if it's not cached, or if the + # weights are not the same. + weights_sha256 = hashlib.sha256(weights) + use_cached_gtg = ( + hasattr(self, "_gtg_diagonal") + and hasattr(self, "_weights_sha256") + and self._weights_sha256.digest() == weights_sha256.digest() + ) + if not use_cached_gtg: + self._gtg_diagonal = self._get_gtg_diagonal(weights) + self._weights_sha256 = weights_sha256 - def getJ(self, m, f=None): + # Multiply the gtg_diagonal by the derivative of the mapping + diagonal = mkvc( + (sdiag(np.sqrt(self._gtg_diagonal)) @ self.rhoDeriv).power(2).sum(axis=0) + ) + return diagonal + + def _get_gtg_diagonal(self, weights: NDArray) -> NDArray: """ - Sensitivity matrix + Compute the diagonal of ``G.T @ W.T @ W @ G``. + + Parameters + ---------- + weights : np.ndarray + Weights array: diagonal of ``W.T @ W``. + + Returns + ------- + np.ndarray + """ + gtg_diagonal = ( + self._gtg_diagonal_without_building_g(weights) + if self.store_sensitivities == "forward_only" + else + # In Einstein notation, the j-th element of the diagonal is: + # d_j = w_i * G_{ij} * G_{ij} + np.asarray(np.einsum("i,ij,ij->j", weights, self.G, self.G)) + ) + return gtg_diagonal + + def getJ(self, m, f=None) -> NDArray[np.float64 | np.float32] | LinearOperator: + r""" + Sensitivity matrix :math:`\mathbf{J}`. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. + f : Ignored + Not used, present here for API consistency by convention. + + Returns + ------- + (nD, n_active_cells) np.ndarray or scipy.sparse.linalg.LinearOperator. + Array or :class:`~scipy.sparse.linalg.LinearOperator` for the + :math:`\mathbf{J}` matrix. + A :class:`~scipy.sparse.linalg.LinearOperator` will be returned if + ``store_sensitivities`` is ``"forward_only"``, otherwise a dense + array will be returned. + + Notes + ----- + If ``store_sensitivities`` is ``"ram"`` or ``"disk"``, a dense array + for the ``J`` matrix is returned. + A :class:`~scipy.sparse.linalg.LinearOperator` is returned if + ``store_sensitivities`` is ``"forward_only"``. This object can perform + operations like ``J @ m`` or ``J.T @ v`` without allocating the full + ``J`` matrix in memory. """ # Need to assign the model, so the rhoDeriv can be computed (if the # model is None, the rhoDeriv is going to be Zero). self.model = m - return self.G @ self.rhoDeriv + rhoDeriv = ( + self.rhoDeriv + if not isinstance(self.G, LinearOperator) + else aslinearoperator(self.rhoDeriv) + ) + return self.G @ rhoDeriv def Jvec(self, m, v, f=None): """ - Sensitivity times a vector + Dot product between sensitivity matrix and a vector. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. This array is used to compute the ``J`` + matrix. + v : (n_param,) numpy.ndarray + Vector used in the matrix-vector multiplication. + f : Ignored + Not used, present here for API consistency by convention. + + Returns + ------- + (nD,) numpy.ndarray + + Notes + ----- + If ``store_sensitivities`` is set to ``"forward_only"``, then the + matrix `G` is never fully constructed, and the dot product is computed + by accumulation, computing the matrix elements on the fly. Otherwise, + the full matrix ``G`` is constructed and stored either in memory or + disk. """ # Need to assign the model, so the rhoDeriv can be computed (if the # model is None, the rhoDeriv is going to be Zero). @@ -280,7 +415,29 @@ def Jvec(self, m, v, f=None): def Jtvec(self, m, v, f=None): """ - Sensitivity transposed times a vector + Dot product between transposed sensitivity matrix and a vector. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. This array is used to compute the ``J`` + matrix. + v : (nD,) numpy.ndarray + Vector used in the matrix-vector multiplication. + f : Ignored + Not used, present here for API consistency by convention. + + Returns + ------- + (nD,) numpy.ndarray + + Notes + ----- + If ``store_sensitivities`` is set to ``"forward_only"``, then the + matrix `G` is never fully constructed, and the dot product is computed + by accumulation, computing the matrix elements on the fly. Otherwise, + the full matrix ``G`` is constructed and stored either in memory or + disk. """ # Need to assign the model, so the rhoDeriv can be computed (if the # model is None, the rhoDeriv is going to be Zero). @@ -289,26 +446,40 @@ def Jtvec(self, m, v, f=None): return np.asarray(self.rhoDeriv.T @ Jtvec) @property - def G(self): + def G(self) -> NDArray | np.memmap | LinearOperator: """ - Gravity forward operator + Gravity forward operator. """ - if getattr(self, "_G", None) is None: - if self.engine == "choclo": - self._G = self._sensitivity_matrix() - else: - self._G = self.linear_operator() + if not hasattr(self, "_G"): + match self.engine, self.store_sensitivities: + case ("choclo", "forward_only"): + self._G = self._sensitivity_matrix_as_operator() + case ("choclo", _): + self._G = self._sensitivity_matrix() + case ("geoana", "forward_only"): + msg = ( + "Accessing matrix G with " + 'store_sensitivities="forward_only" and engine="geoana" ' + "hasn't been implemented yet." + 'Choose store_sensitivities="ram" or "disk", ' + 'or another engine, like "choclo".' + ) + raise NotImplementedError(msg) + case ("geoana", _): + self._G = self.linear_operator() return self._G @property + @deprecated( + "The `gtg_diagonal` property has been deprecated. " + "It will be removed in SimPEG v0.25.0.", + category=FutureWarning, + ) def gtg_diagonal(self): """ Diagonal of GtG """ - if getattr(self, "_gtg_diagonal", None) is None: - return None - - return self._gtg_diagonal + return getattr(self, "_gtg_diagonal", None) def evaluate_integral(self, receiver_location, components): """ @@ -450,7 +621,7 @@ def _forward(self, densities): def _sensitivity_matrix(self): """ - Compute the sensitivity matrix G + Compute the sensitivity matrix ``G``. Returns ------- @@ -492,6 +663,97 @@ def _sensitivity_matrix(self): index_offset += n_rows return sensitivity_matrix + def _sensitivity_matrix_transpose_dot_vec(self, vector): + """ + Compute ``G.T @ v`` without building ``G``. + + Parameters + ---------- + vector : (nD) numpy.ndarray + Vector used in the dot product. + + Returns + ------- + (n_active_cells) numpy.ndarray + """ + # Gather active nodes and the indices of the nodes for each active cell + active_nodes, active_cell_nodes = self._get_active_nodes() + # Allocate resulting array + result = np.zeros(self.nC) + # Start filling the result array + index_offset = 0 + for components, receivers in self._get_components_and_receivers(): + n_components = len(components) + n_rows = n_components * receivers.shape[0] + for i, component in enumerate(components): + kernel_func = CHOCLO_KERNELS[component] + conversion_factor = _get_conversion_factor(component) + vector_slice = slice( + index_offset + i, index_offset + n_rows, n_components + ) + self._sensitivity_t_dot_v( + receivers, + active_nodes, + active_cell_nodes, + kernel_func, + constants.G * conversion_factor, + vector[vector_slice], + result, + ) + index_offset += n_rows + return result + + def _sensitivity_matrix_as_operator(self): + """ + Create a LinearOperator for the sensitivity matrix G. + + Returns + ------- + scipy.sparse.linalg.LinearOperator + """ + shape = (self.survey.nD, self.nC) + linear_op = LinearOperator( + shape=shape, + matvec=self._forward, + rmatvec=self._sensitivity_matrix_transpose_dot_vec, + dtype=np.float64, + ) + return linear_op + + def _gtg_diagonal_without_building_g(self, weights): + """ + Compute the diagonal of ``G.T @ G`` without building the ``G`` matrix. + + Parameters + ----------- + weights : (nD,) array + Array with data weights. It should be the diagonal of the ``W`` + matrix, squared. + + Returns + ------- + (n_active_cells) numpy.ndarray + """ + # Gather active nodes and the indices of the nodes for each active cell + active_nodes, active_cell_nodes = self._get_active_nodes() + # Allocate array for the diagonal of G.T @ G + diagonal = np.zeros(self.nC, dtype=np.float64) + # Start filling the diagonal array + for components, receivers in self._get_components_and_receivers(): + for component in components: + kernel_func = CHOCLO_KERNELS[component] + conversion_factor = _get_conversion_factor(component) + self._diagonal_G_T_dot_G( + receivers, + active_nodes, + active_cell_nodes, + kernel_func, + constants.G * conversion_factor, + weights, + diagonal, + ) + return diagonal + class SimulationEquivalentSourceLayer( BaseEquivalentSourceLayerSimulation, Simulation3DIntegral diff --git a/tests/pf/test_forward_Grav_Linear.py b/tests/pf/test_forward_Grav_Linear.py index 49f757f70f..cc75138520 100644 --- a/tests/pf/test_forward_Grav_Linear.py +++ b/tests/pf/test_forward_Grav_Linear.py @@ -1,6 +1,8 @@ import re import pytest +from scipy.sparse import diags +from scipy.sparse.linalg import LinearOperator, aslinearoperator import discretize import simpeg from simpeg import maps @@ -418,13 +420,11 @@ def test_choclo_missing(self, simple_mesh, monkeypatch): gravity.Simulation3DIntegral(simple_mesh, engine="choclo") -class TestJacobianGravity: +class BaseFixtures: """ - Test methods related to Jacobian matrix in gravity simulation. + Base test class with some fixtures. """ - atol_ratio = 1e-7 - @pytest.fixture def survey(self): # Observation points @@ -455,6 +455,14 @@ def densities(self, mesh): densities[ind_sphere] = 0.2 return densities + +class TestJacobianGravity(BaseFixtures): + """ + Test methods related to Jacobian matrix in gravity simulation. + """ + + atol_ratio = 1e-7 + @pytest.fixture(params=["identity_map", "exp_map"]) def mapping(self, mesh, request): mapping = ( @@ -465,9 +473,9 @@ def mapping(self, mesh, request): return mapping @pytest.mark.parametrize("engine", ["choclo", "geoana"]) - def test_getJ(self, survey, mesh, densities, mapping, engine): + def test_getJ_as_array(self, survey, mesh, densities, mapping, engine): """ - Test the getJ method. + Test the getJ method when J is an array in memory. """ simulation = gravity.simulation.Simulation3DIntegral( survey=survey, @@ -478,6 +486,7 @@ def test_getJ(self, survey, mesh, densities, mapping, engine): ) model = mapping * densities jac = simulation.getJ(model) + assert isinstance(jac, np.ndarray) # With an identity mapping, the jacobian should be the same as G. # With an exp mapping, the jacobian should be G @ the mapping derivative. identity_map = type(mapping) is maps.IdentityMap @@ -486,8 +495,60 @@ def test_getJ(self, survey, mesh, densities, mapping, engine): ) np.testing.assert_allclose(jac, expected_jac) - @pytest.mark.parametrize("engine", ["choclo", "geoana"]) - def test_Jvec(self, survey, mesh, densities, mapping, engine): + def test_getJ_as_linear_operator(self, survey, mesh, densities, mapping): + """ + Test the getJ method when J is a linear operator. + """ + simulation = gravity.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + rhoMap=mapping, + store_sensitivities="forward_only", + engine="choclo", + ) + model = mapping * densities + jac = simulation.getJ(model) + assert isinstance(jac, LinearOperator) + result = jac @ model + expected_result = simulation.G @ (mapping.deriv(model).diagonal() * model) + np.testing.assert_allclose(result, expected_result) + + def test_getJ_as_linear_operator_not_implemented( + self, survey, mesh, densities, mapping + ): + """ + Test getJ raises NotImplementedError when forward only with geoana. + """ + simulation = gravity.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + rhoMap=mapping, + store_sensitivities="forward_only", + engine="geoana", + ) + model = mapping * densities + msg = re.escape( + "Accessing matrix G with " + 'store_sensitivities="forward_only" and engine="geoana" ' + "hasn't been implemented yet." + ) + with pytest.raises(NotImplementedError, match=msg): + simulation.getJ(model) + + @pytest.mark.parametrize( + ("engine", "store_sensitivities"), + [ + ("choclo", "ram"), + ("choclo", "forward_only"), + ("geoana", "ram"), + pytest.param( + "geoana", + "forward_only", + marks=pytest.mark.xfail(reason="not implemented"), + ), + ], + ) + def test_Jvec(self, survey, mesh, densities, mapping, engine, store_sensitivities): """ Test the Jvec method. """ @@ -495,7 +556,7 @@ def test_Jvec(self, survey, mesh, densities, mapping, engine): survey=survey, mesh=mesh, rhoMap=mapping, - store_sensitivities="ram", + store_sensitivities=store_sensitivities, engine=engine, ) model = mapping * densities @@ -505,15 +566,29 @@ def test_Jvec(self, survey, mesh, densities, mapping, engine): identity_map = type(mapping) is maps.IdentityMap expected_jac = ( - simulation.G if identity_map else simulation.G @ mapping.deriv(model) + simulation.G + if identity_map + else simulation.G @ aslinearoperator(mapping.deriv(model)) ) expected_dpred = expected_jac @ vector atol = np.max(np.abs(expected_dpred)) * self.atol_ratio np.testing.assert_allclose(dpred, expected_dpred, atol=atol) - @pytest.mark.parametrize("engine", ["choclo", "geoana"]) - def test_Jtvec(self, survey, mesh, densities, mapping, engine): + @pytest.mark.parametrize( + ("engine", "store_sensitivities"), + [ + ("choclo", "ram"), + ("choclo", "forward_only"), + ("geoana", "ram"), + pytest.param( + "geoana", + "forward_only", + marks=pytest.mark.xfail(reason="not implemented"), + ), + ], + ) + def test_Jtvec(self, survey, mesh, densities, mapping, engine, store_sensitivities): """ Test the Jtvec method. """ @@ -521,7 +596,7 @@ def test_Jtvec(self, survey, mesh, densities, mapping, engine): survey=survey, mesh=mesh, rhoMap=mapping, - store_sensitivities="ram", + store_sensitivities=store_sensitivities, engine=engine, ) model = mapping * densities @@ -531,17 +606,60 @@ def test_Jtvec(self, survey, mesh, densities, mapping, engine): identity_map = type(mapping) is maps.IdentityMap expected_jac = ( - simulation.G if identity_map else simulation.G @ mapping.deriv(model) + simulation.G + if identity_map + else simulation.G @ aslinearoperator(mapping.deriv(model)) ) expected = expected_jac.T @ vector atol = np.max(np.abs(result)) * self.atol_ratio np.testing.assert_allclose(result, expected, atol=atol) + @pytest.mark.parametrize( + "engine", + [ + "choclo", + pytest.param("geoana", marks=pytest.mark.xfail(reason="not implemented")), + ], + ) + @pytest.mark.parametrize("method", ["Jvec", "Jtvec"]) + def test_array_vs_linear_operator( + self, survey, mesh, densities, mapping, engine, method + ): + """ + Test methods when using "ram" and "forward_only". + + They should give the same results. + """ + simulation_lo, simulation_ram = ( + gravity.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + rhoMap=mapping, + store_sensitivities=store, + engine=engine, + ) + for store in ("forward_only", "ram") + ) + match method: + case "Jvec": + vector_size = densities.size + case "Jtvec": + vector_size = survey.nD + case _: + raise ValueError(f"Invalid method '{method}'") + vector = np.random.default_rng(seed=42).uniform(size=vector_size) + model = mapping * densities + result_lo = getattr(simulation_lo, method)(model, vector) + result_ram = getattr(simulation_ram, method)(model, vector) + atol = np.max(np.abs(result_ram)) * self.atol_ratio + np.testing.assert_allclose(result_lo, result_ram, atol=atol) + @pytest.mark.parametrize("engine", ["choclo", "geoana"]) - def test_getJtJdiag(self, survey, mesh, densities, mapping, engine): + @pytest.mark.parametrize("weights", [True, False]) + def test_getJtJdiag(self, survey, mesh, densities, mapping, engine, weights): """ - Test the getJtJdiag method. + Test the ``getJtJdiag`` method with G as an array in memory. """ simulation = gravity.simulation.Simulation3DIntegral( survey=survey, @@ -551,17 +669,188 @@ def test_getJtJdiag(self, survey, mesh, densities, mapping, engine): engine=engine, ) model = mapping * densities - jtj_diag = simulation.getJtJdiag(model) + kwargs = {} + if weights: + w_matrix = diags(np.random.default_rng(seed=42).uniform(size=survey.nD)) + kwargs = {"W": w_matrix} + jtj_diag = simulation.getJtJdiag(model, **kwargs) identity_map = type(mapping) is maps.IdentityMap expected_jac = ( simulation.G if identity_map else simulation.G @ mapping.deriv(model) ) - expected = np.diag(expected_jac.T @ expected_jac) + if weights: + expected = np.diag(expected_jac.T @ w_matrix.T @ w_matrix @ expected_jac) + else: + expected = np.diag(expected_jac.T @ expected_jac) atol = np.max(np.abs(jtj_diag)) * self.atol_ratio np.testing.assert_allclose(jtj_diag, expected, atol=atol) + @pytest.mark.parametrize( + "engine", + [ + "choclo", + pytest.param("geoana", marks=pytest.mark.xfail(reason="not implemented")), + ], + ) + @pytest.mark.parametrize("weights", [True, False]) + def test_getJtJdiag_forward_only( + self, survey, mesh, densities, mapping, engine, weights + ): + """ + Test the ``getJtJdiag`` method without building G. + """ + simulation, simulation_ram = ( + gravity.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + rhoMap=mapping, + store_sensitivities=store, + engine=engine, + ) + for store in ("forward_only", "ram") + ) + model = mapping * densities + kwargs = {} + if weights: + weights = np.random.default_rng(seed=42).uniform(size=survey.nD) + kwargs = {"W": diags(np.sqrt(weights))} + jtj_diag = simulation.getJtJdiag(model, **kwargs) + jtj_diag_ram = simulation_ram.getJtJdiag(model, **kwargs) + + atol = np.max(np.abs(jtj_diag)) * self.atol_ratio + np.testing.assert_allclose(jtj_diag, jtj_diag_ram, atol=atol) + + @pytest.mark.parametrize("engine", ("choclo", "geoana")) + def test_getJtJdiag_caching(self, survey, mesh, densities, mapping, engine): + """ + Test the caching behaviour of the ``getJtJdiag`` method. + """ + simulation = gravity.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + rhoMap=mapping, + store_sensitivities="ram", + engine=engine, + ) + # Get diagonal of J.T @ J without any weight + model = mapping * densities + jtj_diagonal_1 = simulation.getJtJdiag(model) + assert hasattr(simulation, "_gtg_diagonal") + assert hasattr(simulation, "_weights_sha256") + gtg_diagonal_1 = simulation._gtg_diagonal + weights_sha256_1 = simulation._weights_sha256 + + # Compute it again and make sure we get the same result + np.testing.assert_allclose(jtj_diagonal_1, simulation.getJtJdiag(model)) + + # Get a new diagonal with weights + weights_matrix = diags( + np.random.default_rng(seed=42).uniform(size=simulation.survey.nD) + ) + jtj_diagonal_2 = simulation.getJtJdiag(model, W=weights_matrix) + assert hasattr(simulation, "_gtg_diagonal") + assert hasattr(simulation, "_weights_sha256") + gtg_diagonal_2 = simulation._gtg_diagonal + weights_sha256_2 = simulation._weights_sha256 + + # The two results should be different + assert not np.array_equal(jtj_diagonal_1, jtj_diagonal_2) + assert not np.array_equal(gtg_diagonal_1, gtg_diagonal_2) + assert weights_sha256_1.digest() != weights_sha256_2.digest() + + +class TestGLinearOperator(BaseFixtures): + """ + Test G as a linear operator. + """ + + @pytest.fixture + def mapping(self, mesh): + return maps.IdentityMap(nP=mesh.n_cells) + + def test_not_implemented(self, survey, mesh, mapping): + """ + Test NotImplementedError when using geoana as engine. + """ + simulation = gravity.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + rhoMap=mapping, + store_sensitivities="forward_only", + engine="geoana", + ) + msg = re.escape( + "Accessing matrix G with " + 'store_sensitivities="forward_only" and engine="geoana" ' + "hasn't been implemented yet." + ) + with pytest.raises(NotImplementedError, match=msg): + simulation.G + + @pytest.mark.parametrize("parallel", [True, False]) + def test_G_dot_m(self, survey, mesh, mapping, densities, parallel): + """Test G @ m.""" + simulation, simulation_ram = ( + gravity.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + rhoMap=mapping, + store_sensitivities=store, + engine="choclo", + numba_parallel=parallel, + ) + for store in ("forward_only", "ram") + ) + assert isinstance(simulation.G, LinearOperator) + assert isinstance(simulation_ram.G, np.ndarray) + np.testing.assert_allclose( + simulation.G @ densities, simulation_ram.G @ densities + ) + + @pytest.mark.parametrize("parallel", [True, False]) + def test_G_t_dot_v(self, survey, mesh, mapping, parallel): + """Test G.T @ v.""" + simulation, simulation_ram = ( + gravity.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + rhoMap=mapping, + store_sensitivities=store, + engine="choclo", + numba_parallel=parallel, + ) + for store in ("forward_only", "ram") + ) + assert isinstance(simulation.G, LinearOperator) + assert isinstance(simulation_ram.G, np.ndarray) + vector = np.random.default_rng(seed=42).uniform(size=survey.nD) + np.testing.assert_allclose(simulation.G.T @ vector, simulation_ram.G.T @ vector) + + +class TestDeprecationWarning(BaseFixtures): + """ + Test warnings after deprecated properties or methods of the simulation class. + """ + + def test_gtg_diagonal(self, survey, mesh): + """Test deprecation warning on gtg_diagonal property.""" + mapping = maps.IdentityMap(nP=mesh.n_cells) + simulation = gravity.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + rhoMap=mapping, + store_sensitivities="ram", + engine="choclo", + ) + msg = re.escape( + "The `gtg_diagonal` property has been deprecated. " + "It will be removed in SimPEG v0.25.0.", + ) + with pytest.warns(FutureWarning, match=msg): + simulation.gtg_diagonal + class TestConversionFactor: """Test _get_conversion_factor function.""" From 33c4221d8f4d9c2e4e3e51b948ac755c4d3d78c4 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 28 Mar 2025 18:17:02 +0000 Subject: [PATCH 120/194] Set maximum number of iterations in eq sources tests (#1636) Increase maximum number of iterations in magnetic equivalent sources test. --- tests/pf/test_equivalent_sources.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/pf/test_equivalent_sources.py b/tests/pf/test_equivalent_sources.py index 84339d8db4..5596933cd2 100644 --- a/tests/pf/test_equivalent_sources.py +++ b/tests/pf/test_equivalent_sources.py @@ -690,7 +690,7 @@ def build_synthetic_data(self, simulation, model): ) return data - def build_inversion(self, mesh, simulation, synthetic_data): + def build_inversion(self, mesh, simulation, synthetic_data, max_iterations=20): """Build inversion problem.""" # Build data misfit and regularization terms data_misfit = simpeg.data_misfit.L2DataMisfit( @@ -699,6 +699,7 @@ def build_inversion(self, mesh, simulation, synthetic_data): regularization = simpeg.regularization.WeightedLeastSquares(mesh=mesh) # Choose optimization optimization = ProjectedGNCG( + maxIter=max_iterations, maxIterLS=5, maxIterCG=20, tolCG=1e-4, @@ -826,7 +827,9 @@ def test_predictions_on_data_points( model = get_block_model(tree_mesh, 1e-3) synthetic_data = self.build_synthetic_data(simulation, model) # Build inversion - inversion = self.build_inversion(tree_mesh, simulation, synthetic_data) + inversion = self.build_inversion( + tree_mesh, simulation, synthetic_data, max_iterations=40 + ) # Run inversion starting_model = np.zeros(tree_mesh.n_cells) recovered_model = inversion.run(starting_model) From 11b407e05dec2404e5ad1a74692fab9113b43404 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Sun, 30 Mar 2025 16:40:09 -0600 Subject: [PATCH 121/194] Em1d multiple rx locs (#1637) #### Summary Allows multiple locations for EM1D receivers #### PR Checklist * [x] If this is a work in progress PR, set as a Draft PR * [x] Linted my code according to the [style guides](https://docs.simpeg.xyz/latest/content/getting_started/contributing/code-style.html). * [x] Added [tests](https://docs.simpeg.xyz/latest/content/getting_started/contributing/testing.html) to verify changes to the code. * [x] Added necessary documentation to any new functions/classes following the expect [style](https://docs.simpeg.xyz/latest/content/getting_started/contributing/documentation.html). * [x] Marked as ready for review (if this is was a draft PR), and converted to a Pull Request * [x] Tagged ``@simpeg/simpeg-developers`` when ready for review. #### Reference issue Closes #1629 #### What does this implement/fix? Allows for multiple locations in a single receiver for both 1D layered FDEM and TDEM simulations (and adds some simple testing to ensure it works). --- simpeg/electromagnetics/base_1d.py | 5 +- .../frequency_domain/simulation_1d.py | 4 ++ tests/em/em1d/test_EM1D_FD_fwd.py | 58 +++++++++++++++++++ .../em1d/test_EM1D_TD_general_jac_layers.py | 55 ++++++++++++++++++ 4 files changed, 120 insertions(+), 2 deletions(-) diff --git a/simpeg/electromagnetics/base_1d.py b/simpeg/electromagnetics/base_1d.py index a871d56bfc..afba170629 100644 --- a/simpeg/electromagnetics/base_1d.py +++ b/simpeg/electromagnetics/base_1d.py @@ -558,8 +558,9 @@ def _compute_hankel_coefficients(self): C1s.append(np.exp(-lambd * (z + h)[:, None]) * C1 / offsets[:, None]) lambs.append(lambd) n_w_past += n_w - Is.append(np.ones(n_w, dtype=int) * i_count) - i_count += 1 + for _ in range(rx.locations.shape[0]): + Is.append(np.ones(n_w, dtype=int) * i_count) + i_count += 1 # Store these on the simulation for faster future executions self._lambs = np.vstack(lambs) diff --git a/simpeg/electromagnetics/frequency_domain/simulation_1d.py b/simpeg/electromagnetics/frequency_domain/simulation_1d.py index 7d880dd09c..f7fe4d867e 100644 --- a/simpeg/electromagnetics/frequency_domain/simulation_1d.py +++ b/simpeg/electromagnetics/frequency_domain/simulation_1d.py @@ -288,6 +288,10 @@ def _project_to_data(self, v): out[i_dat:i_dat_p1] = v_slice.real elif rx.component == "imag": out[i_dat:i_dat_p1] = v_slice.imag + else: + raise NotImplementedError( + f"receiver component {rx.component} not implemented." + ) i_dat = i_dat_p1 i_v = i_v_p1 return out diff --git a/tests/em/em1d/test_EM1D_FD_fwd.py b/tests/em/em1d/test_EM1D_FD_fwd.py index 8a62a1398f..4986024335 100644 --- a/tests/em/em1d/test_EM1D_FD_fwd.py +++ b/tests/em/em1d/test_EM1D_FD_fwd.py @@ -9,6 +9,7 @@ vertical_magnetic_field_horizontal_loop as mag_field, ) import empymod +import pytest class EM1D_FD_test_failures(unittest.TestCase): @@ -548,5 +549,62 @@ def solution(res): self.assertLess(err, 1e-4) +@pytest.mark.parametrize( + "rx_class", + [fdem.receivers.PointMagneticField, fdem.receivers.PointMagneticFieldSecondary], +) +@pytest.mark.parametrize("n_locs1", [1, 4]) +@pytest.mark.parametrize("n_locs2", [1, 4]) +@pytest.mark.parametrize("orientation", ["x", "y", "z"]) +@pytest.mark.parametrize("component", ["real", "imag", "both"]) +def test_rx_loc_shapes(rx_class, n_locs1, n_locs2, orientation, component): + offsets = np.full(n_locs1, 100.0) + rx1_locs = np.pad(offsets[:, None], ((0, 0), (0, 2)), constant_values=0) + offsets = np.full(n_locs2, 100.0) + rx2_locs = np.pad(offsets[:, None], ((0, 0), (0, 2)), constant_values=0) + + rx_list = [ + rx_class(rx1_locs, orientation=orientation, component=component), + rx_class(rx2_locs, orientation=orientation, component=component), + ] + n_d = n_locs1 + n_locs2 + if component == "both": + n_d *= 2 + + src = fdem.sources.MagDipole(rx_list, frequency=0.1) + srv = fdem.Survey(src) + + sim = fdem.Simulation1DLayered(survey=srv, sigma=[1]) + d = sim.dpred(None) + + # assert the shape is correct + assert d.shape == (n_d,) + + # every value should be the same... + d1 = d[srv.get_slice(src, rx_list[0])] + d2 = d[srv.get_slice(src, rx_list[1])] + + if component == "both": + d1 = d1[::2] + 1j * d1[1::2] + d2 = d2[::2] + 1j * d2[1::2] + d = np.r_[d1, d2] + np.testing.assert_allclose(d, d[0], rtol=1e-12) + + sim.sigmaMap = maps.IdentityMap(nP=1) + # make sure forming J works + J = sim.getJ(np.ones(1))["ds"] + assert J.shape == (n_d, 1) + + # and all of its values are the same too: + j1 = J[srv.get_slice(src, rx_list[0]), 0] + j2 = J[srv.get_slice(src, rx_list[1]), 0] + + if component == "both": + j1 = j1[::2] + 1j * j1[1::2] + j2 = j2[::2] + 1j * j2[1::2] + J = np.r_[j1, j2] + np.testing.assert_allclose(J, J[0], rtol=1e-12) + + if __name__ == "__main__": unittest.main() diff --git a/tests/em/em1d/test_EM1D_TD_general_jac_layers.py b/tests/em/em1d/test_EM1D_TD_general_jac_layers.py index f6818ccf81..0580d5407e 100644 --- a/tests/em/em1d/test_EM1D_TD_general_jac_layers.py +++ b/tests/em/em1d/test_EM1D_TD_general_jac_layers.py @@ -3,6 +3,7 @@ from discretize import tests import numpy as np import simpeg.electromagnetics.time_domain as tdem +import pytest class EM1D_TD_general_Jac_layers_ProblemTests(unittest.TestCase): @@ -305,5 +306,59 @@ def derChk(m): self.assertTrue(passed) +@pytest.mark.parametrize( + "rx_class", + [ + tdem.receivers.PointMagneticField, + tdem.receivers.PointMagneticFluxDensity, + tdem.receivers.PointMagneticFluxTimeDerivative, + ], +) +@pytest.mark.parametrize("n_locs1", [1, 4]) +@pytest.mark.parametrize("n_locs2", [1, 4]) +@pytest.mark.parametrize("orientation", ["x", "y", "z"]) +@pytest.mark.parametrize( + "waveform", [tdem.sources.StepOffWaveform(), tdem.sources.RampOffWaveform(1e-6)] +) +@pytest.mark.parametrize("comparison", ["dpred", "J"]) +def test_rx_loc_shapes(rx_class, n_locs1, n_locs2, orientation, waveform, comparison): + offsets = np.full(n_locs1, 100.0) + rx1_locs = np.pad(offsets[:, None], ((0, 0), (0, 2)), constant_values=0) + offsets = np.full(n_locs2, 100.0) + rx2_locs = np.pad(offsets[:, None], ((0, 0), (0, 2)), constant_values=0) + + times = [1e-5, 1e-4] + rx_list = [ + rx_class(rx1_locs, times=times, orientation=orientation), + rx_class(rx2_locs, times=times, orientation=orientation), + ] + n_d = (n_locs1 + n_locs2) * len(times) + + src = tdem.sources.MagDipole(rx_list, waveform=waveform) + srv = tdem.Survey(src) + + sim = tdem.Simulation1DLayered(survey=srv, sigma=[1]) + if comparison == "dpred": + d = sim.dpred(None) + else: + sim.sigmaMap = maps.IdentityMap(nP=1) + d = sim.getJ(np.ones(1))["ds"][:, 0] + + # assert the shape is correct + assert d.shape == (n_d,) + + # every pair of values should be the same... + d1 = d[srv.get_slice(src, rx_list[0])] + d2 = d[srv.get_slice(src, rx_list[1])] + + # get the data into an n_loc * n_times shape + d = np.r_[ + d1.reshape(2, -1).T, + d2.reshape(2, -1).T, + ] + d_compare = d[0] * np.ones_like(d) + np.testing.assert_equal(d, d_compare) + + if __name__ == "__main__": unittest.main() From d2c49ffa186329d213bde6ce2b7d1fca4debe2e5 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 14 Apr 2025 09:09:53 -0700 Subject: [PATCH 122/194] Fix definition of model in gravity J-related tests (#1643) Fix of the definition of `model` in J-related tests for the gravity simulation: use the inverse of the mapping to define the `model` based on the sample `densities`. --- tests/pf/test_forward_Grav_Linear.py | 47 ++++++++++++++++++---------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/tests/pf/test_forward_Grav_Linear.py b/tests/pf/test_forward_Grav_Linear.py index cc75138520..6c44582ec4 100644 --- a/tests/pf/test_forward_Grav_Linear.py +++ b/tests/pf/test_forward_Grav_Linear.py @@ -484,14 +484,15 @@ def test_getJ_as_array(self, survey, mesh, densities, mapping, engine): store_sensitivities="ram", engine=engine, ) - model = mapping * densities + is_identity_map = type(mapping) is maps.IdentityMap + model = densities if is_identity_map else np.log(densities) + jac = simulation.getJ(model) assert isinstance(jac, np.ndarray) # With an identity mapping, the jacobian should be the same as G. # With an exp mapping, the jacobian should be G @ the mapping derivative. - identity_map = type(mapping) is maps.IdentityMap expected_jac = ( - simulation.G if identity_map else simulation.G @ mapping.deriv(model) + simulation.G if is_identity_map else simulation.G @ mapping.deriv(model) ) np.testing.assert_allclose(jac, expected_jac) @@ -506,7 +507,9 @@ def test_getJ_as_linear_operator(self, survey, mesh, densities, mapping): store_sensitivities="forward_only", engine="choclo", ) - model = mapping * densities + is_identity_map = type(mapping) is maps.IdentityMap + model = densities if is_identity_map else np.log(densities) + jac = simulation.getJ(model) assert isinstance(jac, LinearOperator) result = jac @ model @@ -526,7 +529,9 @@ def test_getJ_as_linear_operator_not_implemented( store_sensitivities="forward_only", engine="geoana", ) - model = mapping * densities + is_identity_map = type(mapping) is maps.IdentityMap + model = densities if is_identity_map else np.log(densities) + msg = re.escape( "Accessing matrix G with " 'store_sensitivities="forward_only" and engine="geoana" ' @@ -559,15 +564,15 @@ def test_Jvec(self, survey, mesh, densities, mapping, engine, store_sensitivitie store_sensitivities=store_sensitivities, engine=engine, ) - model = mapping * densities + is_identity_map = type(mapping) is maps.IdentityMap + model = densities if is_identity_map else np.log(densities) vector = np.random.default_rng(seed=42).uniform(size=densities.size) dpred = simulation.Jvec(model, vector) - identity_map = type(mapping) is maps.IdentityMap expected_jac = ( simulation.G - if identity_map + if is_identity_map else simulation.G @ aslinearoperator(mapping.deriv(model)) ) expected_dpred = expected_jac @ vector @@ -599,15 +604,15 @@ def test_Jtvec(self, survey, mesh, densities, mapping, engine, store_sensitiviti store_sensitivities=store_sensitivities, engine=engine, ) - model = mapping * densities + is_identity_map = type(mapping) is maps.IdentityMap + model = densities if is_identity_map else np.log(densities) vector = np.random.default_rng(seed=42).uniform(size=survey.nD) result = simulation.Jtvec(model, vector) - identity_map = type(mapping) is maps.IdentityMap expected_jac = ( simulation.G - if identity_map + if is_identity_map else simulation.G @ aslinearoperator(mapping.deriv(model)) ) expected = expected_jac.T @ vector @@ -648,8 +653,11 @@ def test_array_vs_linear_operator( vector_size = survey.nD case _: raise ValueError(f"Invalid method '{method}'") + + is_identity_map = type(mapping) is maps.IdentityMap + model = densities if is_identity_map else np.log(densities) + vector = np.random.default_rng(seed=42).uniform(size=vector_size) - model = mapping * densities result_lo = getattr(simulation_lo, method)(model, vector) result_ram = getattr(simulation_ram, method)(model, vector) atol = np.max(np.abs(result_ram)) * self.atol_ratio @@ -668,16 +676,17 @@ def test_getJtJdiag(self, survey, mesh, densities, mapping, engine, weights): store_sensitivities="ram", engine=engine, ) - model = mapping * densities + is_identity_map = type(mapping) is maps.IdentityMap + model = densities if is_identity_map else np.log(densities) + kwargs = {} if weights: w_matrix = diags(np.random.default_rng(seed=42).uniform(size=survey.nD)) kwargs = {"W": w_matrix} jtj_diag = simulation.getJtJdiag(model, **kwargs) - identity_map = type(mapping) is maps.IdentityMap expected_jac = ( - simulation.G if identity_map else simulation.G @ mapping.deriv(model) + simulation.G if is_identity_map else simulation.G @ mapping.deriv(model) ) if weights: expected = np.diag(expected_jac.T @ w_matrix.T @ w_matrix @ expected_jac) @@ -711,7 +720,9 @@ def test_getJtJdiag_forward_only( ) for store in ("forward_only", "ram") ) - model = mapping * densities + is_identity_map = type(mapping) is maps.IdentityMap + model = densities if is_identity_map else np.log(densities) + kwargs = {} if weights: weights = np.random.default_rng(seed=42).uniform(size=survey.nD) @@ -735,7 +746,9 @@ def test_getJtJdiag_caching(self, survey, mesh, densities, mapping, engine): engine=engine, ) # Get diagonal of J.T @ J without any weight - model = mapping * densities + is_identity_map = type(mapping) is maps.IdentityMap + model = densities if is_identity_map else np.log(densities) + jtj_diagonal_1 = simulation.getJtJdiag(model) assert hasattr(simulation, "_gtg_diagonal") assert hasattr(simulation, "_weights_sha256") From b1804b8adea4f2256ebaa0569a9d2bb6cc4c6ac6 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 14 Apr 2025 09:11:29 -0700 Subject: [PATCH 123/194] Improve docstring of dip_azimuth2cartesian (#1642) Fix typo, improve description of parameters and add some examples that can be tested. --- simpeg/utils/mat_utils.py | 42 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/simpeg/utils/mat_utils.py b/simpeg/utils/mat_utils.py index 76cee091b1..307773b25a 100644 --- a/simpeg/utils/mat_utils.py +++ b/simpeg/utils/mat_utils.py @@ -378,9 +378,11 @@ def dip_azimuth2cartesian(dip, azm): Parameters ---------- dip : float or 1D numpy.ndarray - Dip angle in degrees. Values in range [0, 90] + Dip angle in degrees. Values in range [-90, 90]. Positive values correspond to + a vector pointing downwards (negative z component). azm : float or 1D numpy.ndarray - Asimuthal angle (strike) in degrees. Defined clockwise from Northing. Values is range [0, 360] + Azimuthal angle (strike) in degrees. Defined clockwise from Northing. + Values is range [0, 360] or [-180, 180]. Returns ------- @@ -388,6 +390,42 @@ def dip_azimuth2cartesian(dip, azm): Numpy array whose columns represent the x, y and z components of the vector(s) in Cartesian coordinates + Examples + -------- + >>> vector = dip_azimuth2cartesian(0, 45) + >>> x, y, z = vector[0].tolist() + >>> x, y, z # doctest: +NUMBER + (0.707, 0.707, 0.0) + + >>> vector = dip_azimuth2cartesian(0, -45) + >>> x, y, z = vector[0].tolist() + >>> x, y, z # doctest: +NUMBER + (-0.707, 0.707, 0.0) + + >>> vector = dip_azimuth2cartesian(60, 0) + >>> x, y, z = vector[0].tolist() + >>> x, y, z # doctest: +NUMBER + (0.0, 0.5, -0.866) + + >>> vector = dip_azimuth2cartesian(-30, 0) + >>> x, y, z = vector[0].tolist() + >>> x, y, z # doctest: +NUMBER + (0.0, 0.866, 0.5) + + >>> vector = dip_azimuth2cartesian(90, 0) + >>> x, y, z = vector[0].tolist() + >>> x, y, z # doctest: +NUMBER + (0.0, 0.0, -1.0) + + >>> vector = dip_azimuth2cartesian(-90, 0) + >>> x, y, z = vector[0].tolist() + >>> x, y, z # doctest: +NUMBER + (0.0, 0.0, 1.0) + + >>> vector = dip_azimuth2cartesian(30, 60) + >>> x, y, z = vector[0].tolist() + >>> x, y, z # doctest: +NUMBER + (0.75, 0.433, -0.5) """ azm = np.asarray(azm) From 46fd664c274ad71f9d7373d1642e1846ef505266 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 14 Apr 2025 12:23:25 -0700 Subject: [PATCH 124/194] Improve variable names in gravity test (#1641) Avoid using `dpred` in the gravity test that might lead to confusion. When the mapping is not a linear function, the `J @ m` is just a first order approximation of the forward model and not equal to the `dpred`. --- tests/pf/test_forward_Grav_Linear.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/pf/test_forward_Grav_Linear.py b/tests/pf/test_forward_Grav_Linear.py index 6c44582ec4..6b6d2dadd8 100644 --- a/tests/pf/test_forward_Grav_Linear.py +++ b/tests/pf/test_forward_Grav_Linear.py @@ -568,17 +568,17 @@ def test_Jvec(self, survey, mesh, densities, mapping, engine, store_sensitivitie model = densities if is_identity_map else np.log(densities) vector = np.random.default_rng(seed=42).uniform(size=densities.size) - dpred = simulation.Jvec(model, vector) + result = simulation.Jvec(model, vector) expected_jac = ( simulation.G if is_identity_map else simulation.G @ aslinearoperator(mapping.deriv(model)) ) - expected_dpred = expected_jac @ vector + expected = expected_jac @ vector - atol = np.max(np.abs(expected_dpred)) * self.atol_ratio - np.testing.assert_allclose(dpred, expected_dpred, atol=atol) + atol = np.max(np.abs(expected)) * self.atol_ratio + np.testing.assert_allclose(result, expected, atol=atol) @pytest.mark.parametrize( ("engine", "store_sensitivities"), From 9026730b8f4da13bfc31b6247261107c9e2cb195 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 14 Apr 2025 14:51:06 -0700 Subject: [PATCH 125/194] Test transpose of gravity getJ as linear operator (#1644) Extend test of the `getJ` method of the gravity simulation: test the transpose of the `J` linear operator. --- tests/pf/test_forward_Grav_Linear.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/pf/test_forward_Grav_Linear.py b/tests/pf/test_forward_Grav_Linear.py index 6b6d2dadd8..873e38a503 100644 --- a/tests/pf/test_forward_Grav_Linear.py +++ b/tests/pf/test_forward_Grav_Linear.py @@ -496,7 +496,8 @@ def test_getJ_as_array(self, survey, mesh, densities, mapping, engine): ) np.testing.assert_allclose(jac, expected_jac) - def test_getJ_as_linear_operator(self, survey, mesh, densities, mapping): + @pytest.mark.parametrize("transpose", [False, True], ids=["J @ m", "J.T @ v"]) + def test_getJ_as_linear_operator(self, survey, mesh, densities, mapping, transpose): """ Test the getJ method when J is a linear operator. """ @@ -512,8 +513,14 @@ def test_getJ_as_linear_operator(self, survey, mesh, densities, mapping): jac = simulation.getJ(model) assert isinstance(jac, LinearOperator) - result = jac @ model - expected_result = simulation.G @ (mapping.deriv(model).diagonal() * model) + + if transpose: + vector = np.random.default_rng(seed=42).uniform(size=survey.nD) + result = jac.T @ vector + expected_result = mapping.deriv(model).T @ (simulation.G.T @ vector) + else: + result = jac @ model + expected_result = simulation.G @ (mapping.deriv(model).diagonal() * model) np.testing.assert_allclose(result, expected_result) def test_getJ_as_linear_operator_not_implemented( From f2440e63c6ab92191ae4f452808a9e4169af84e3 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 22 Apr 2025 14:42:54 -0700 Subject: [PATCH 126/194] Configure zizmor to pin reviewdog actions with tags (#1650) Add a new `.github/zizmor.yml` configuration file that allows zizmor to use tags to pin the two reviewdog actions we currently use: `reviewdog/action-flake8` and `reviewdog/action-black`. --- .github/zizmor.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/zizmor.yml diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 0000000000..b506949a16 --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,17 @@ +# Zizmor configuration +# -------------------- +# +# This file configures zizmor. This is not a workflow that gets run in GitHub +# Actions. +# +# References: https://woodruffw.github.io/zizmor/configuration + +rules: + unpinned-uses: + config: + policies: + # Mimic default behaviour: official actions can get pinned by tag. + actions/*: ref-pin + # Allow to use tags to pin reviewdog actions. + reviewdog/action-black: ref-pin + reviewdog/action-flake8: ref-pin From 1a0d3fb2cd2d09e7e3d90d785aba0bbc2b53fe2f Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 22 Apr 2025 15:47:09 -0700 Subject: [PATCH 127/194] Deprecate `components` in potential field surveys (#1633) Deprecate the `components` properties in potential field surveys. These properties weren't returning the expected output. Since receivers within the same survey can have different components, returning the components of the first receiver is misleading. --------- Co-authored-by: Joseph Capriotti --- simpeg/potential_fields/gravity/survey.py | 23 +++ .../potential_fields/magnetics/simulation.py | 61 +++++- simpeg/potential_fields/magnetics/survey.py | 23 +++ tests/pf/test_components.py | 192 ++++++++++++++++++ tests/pf/test_forward_PFproblem.py | 6 +- 5 files changed, 298 insertions(+), 7 deletions(-) create mode 100644 tests/pf/test_components.py diff --git a/simpeg/potential_fields/gravity/survey.py b/simpeg/potential_fields/gravity/survey.py index 2808e2489a..447b4b69d1 100644 --- a/simpeg/potential_fields/gravity/survey.py +++ b/simpeg/potential_fields/gravity/survey.py @@ -2,6 +2,14 @@ from ...utils.code_utils import validate_type from .sources import SourceField +try: + from warnings import deprecated +except ImportError: + # Use the deprecated decorator provided by typing_extensions (which + # supports older versions of Python) if it cannot be imported from + # warnings. + from typing_extensions import deprecated + class Survey(BaseSurvey): """Base Gravity Survey @@ -75,9 +83,24 @@ def nD(self): return sum(receiver.nD for receiver in self.source_field.receiver_list) @property + @deprecated( + "The `components` property is deprecated, " + "and will be removed in SimPEG v0.25.0. " + "Within a gravity survey, receivers can contain different components. " + "Iterate over the sources and receivers in the survey to get " + "information about their components.", + category=FutureWarning, + ) def components(self): """Number of components measured at each receiver. + .. deprecated:: 0.24.0 + + The `components` property is deprecated, and will be removed in + SimPEG v0.25.0. Within a gravity survey, receivers can contain + different components. Iterate over the sources and receivers in the + survey to get information about their components. + Returns ------- int diff --git a/simpeg/potential_fields/magnetics/simulation.py b/simpeg/potential_fields/magnetics/simulation.py index 05ddf5b992..72ada84d2f 100644 --- a/simpeg/potential_fields/magnetics/simulation.py +++ b/simpeg/potential_fields/magnetics/simulation.py @@ -1147,6 +1147,7 @@ def survey(self): def survey(self, obj): if obj is not None: obj = validate_type("survey", obj, Survey, cast=False) + self._validate_survey(obj) self._survey = obj @property @@ -1524,8 +1525,8 @@ def projectFields(self, u): \text{TMI} = \vec{B}_s \cdot \hat{B}_0 """ - # TODO: There can be some different tyes of data like |B| or B - components = self.survey.components + # Get components for all receivers, assuming they all have the same components + components = self._get_components() fields = {} if "bx" in components or "tmi" in components: @@ -1560,8 +1561,8 @@ def projectFieldsDeriv(self, B): Especially, this function is for TMI data type """ - - components = self.survey.components + # Get components for all receivers, assuming they all have the same components + components = self._get_components() fields = {} if "bx" in components or "tmi" in components: @@ -1585,6 +1586,58 @@ def projectFieldsDeriv(self, B): return sp.vstack([fields[comp] for comp in components]) + def _get_components(self): + """ + Get components of all receivers in the survey. + + This function assumes that all receivers in the survey have the same + components in the same order. + + Returns + ------- + components : list of str + List of components shared by all receivers in the survey. + + Raises + ------ + ValueError + If the survey doesn't have any receiver, or if any receiver has + a different set of components than the rest. + """ + # Validate survey first to ensure that the receivers have all the same + # components. + self._validate_survey(self.survey) + components = self.survey.source_field.receiver_list[0].components + return components + + def _validate_survey(self, survey): + """ + Validate a survey for the magnetic differential 3D simulation. + + Parameters + ---------- + survey : Survey + Survey object that will get validated. + + Raises + ------ + ValueError + If the survey doesn't have any receiver, or if any receiver has + a different set of components than the rest. + """ + receivers = survey.source_field.receiver_list + if not receivers: + msg = "Found invalid survey without receivers." + raise ValueError(msg) + components = receivers[0].components + if not all(components == rx.components for rx in receivers): + msg = ( + "Found invalid survey with receivers that have mixed components. " + f"Surveys for the {type(self).__name__} class must contain receivers " + "with the same components." + ) + raise ValueError(msg) + def projectFieldsAsVector(self, B): bfx = self.Qfx * B bfy = self.Qfy * B diff --git a/simpeg/potential_fields/magnetics/survey.py b/simpeg/potential_fields/magnetics/survey.py index 56a2c3296c..ac66c7c42f 100644 --- a/simpeg/potential_fields/magnetics/survey.py +++ b/simpeg/potential_fields/magnetics/survey.py @@ -3,6 +3,14 @@ from ...utils.code_utils import validate_type from .sources import UniformBackgroundField +try: + from warnings import deprecated +except ImportError: + # Use the deprecated decorator provided by typing_extensions (which + # supports older versions of Python) if it cannot be imported from + # warnings. + from typing_extensions import deprecated + class Survey(BaseSurvey): """Base Magnetics Survey @@ -69,9 +77,24 @@ def nD(self): return sum(rx.nD for rx in self.source_field.receiver_list) @property + @deprecated( + "The `components` property is deprecated, " + "and will be removed in SimPEG v0.25.0. " + "Within a magnetic survey, receivers can contain different components. " + "Iterate over the sources and receivers in the survey to get " + "information about their components.", + category=FutureWarning, + ) def components(self): """Field components + .. deprecated:: 0.24.0 + + The `components` property is deprecated, and will be removed in + SimPEG v0.25.0. Within a magnetic survey, receivers can contain + different components. Iterate over the sources and receivers in the + survey to get information about their components. + Returns ------- list of str diff --git a/tests/pf/test_components.py b/tests/pf/test_components.py new file mode 100644 index 0000000000..569a33eba2 --- /dev/null +++ b/tests/pf/test_components.py @@ -0,0 +1,192 @@ +""" +Test how potential field surveys and simulations access receiver components. +""" + +import re +import pytest +import numpy as np + +import discretize +from simpeg import maps +from simpeg.potential_fields import gravity, magnetics + + +@pytest.fixture +def receiver_locations(): + x = np.linspace(-20.0, 20.0, 4) + x, y = np.meshgrid(x, x) + z = 5.0 * np.ones_like(x) + return np.vstack((x.ravel(), y.ravel(), z.ravel())).T + + +@pytest.fixture +def mesh(): + dh = 5.0 + hx = [(dh, 10)] + return discretize.TensorMesh([hx, hx, hx], "CCN") + + +class TestComponentsGravitySurvey: + + def test_deprecated_components(self, receiver_locations): + """ + Test FutureError after deprecated ``components`` property. + """ + receivers = gravity.receivers.Point(receiver_locations, components="gz") + source_field = gravity.sources.SourceField(receiver_list=[receivers]) + survey = gravity.survey.Survey(source_field) + msg = re.escape("The `components` property is deprecated") + with pytest.warns(FutureWarning, match=msg): + survey.components + + +class TestComponentsMagneticSurvey: + + def test_deprecated_components(self, receiver_locations): + """ + Test FutureError after deprecated ``components`` property. + """ + receivers = magnetics.receivers.Point(receiver_locations, components="tmi") + source_field = magnetics.sources.UniformBackgroundField( + receiver_list=[receivers], amplitude=55_000, inclination=12, declination=35 + ) + survey = magnetics.survey.Survey(source_field) + msg = re.escape("The `components` property is deprecated") + with pytest.warns(FutureWarning, match=msg): + survey.components + + +class TestMagneticSimulationDifferential: + + def build_survey(self, receivers: list | None): + """ + Build a sample survey. + """ + source_field = magnetics.sources.UniformBackgroundField( + receiver_list=receivers, amplitude=55_000, inclination=12, declination=35 + ) + survey = magnetics.survey.Survey(source_field) + return survey + + @pytest.fixture + def sample_simulation(self, mesh, receiver_locations): + """ + Build a sample simulation with single receiver with "tmi". + """ + receivers = [magnetics.receivers.Point(receiver_locations, components="tmi")] + source_field = magnetics.sources.UniformBackgroundField( + receiver_list=receivers, amplitude=55_000, inclination=12, declination=35 + ) + survey = magnetics.survey.Survey(source_field) + simulation = magnetics.Simulation3DDifferential( + mesh, survey=survey, muMap=maps.IdentityMap(mesh=mesh) + ) + return simulation + + def test_survey_setter(self, receiver_locations, sample_simulation): + """ + Test ``survey`` setter with valid receivers. + """ + receivers = [magnetics.receivers.Point(receiver_locations, components="tmi")] + survey = self.build_survey(receivers) + # Try to override the survey, should pass wo errors + sample_simulation.survey = survey + + @pytest.mark.parametrize("invalid_rx", ["no-rx", "different-components"]) + def test_survey_setter_invalid( + self, receiver_locations, sample_simulation, invalid_rx + ): + """ + Test ``survey`` setter with invalid receivers. + """ + if invalid_rx == "no-rx": + receivers = [] + msg = re.escape("Found invalid survey without receivers.") + else: + receivers = [ + magnetics.receivers.Point(receiver_locations, components=c) + for c in ("tmi", ["bx", "by"]) + ] + msg = re.escape( + "Found invalid survey with receivers that have mixed components." + ) + # Try to override the survey + survey = self.build_survey(receivers) + with pytest.raises(ValueError, match=msg): + sample_simulation.survey = survey + + @pytest.mark.parametrize("components", ["tmi", ["bx", "by", "bz"]]) + def test_get_components(self, mesh, receiver_locations, components): + """ + Test the ``_get_components`` method with valid receivers. + """ + receivers = [ + magnetics.receivers.Point(receiver_locations, components=components), + magnetics.receivers.Point(receiver_locations, components=components), + ] + survey = self.build_survey(receivers) + simulation = magnetics.Simulation3DDifferential( + mesh, survey=survey, muMap=maps.IdentityMap(mesh=mesh) + ) + + expected = components if isinstance(components, list) else [components] + assert expected == simulation._get_components() + + @pytest.mark.parametrize("invalid_rx", ["no-rx", "different-components"]) + def test_get_components_invalid( + self, sample_simulation, receiver_locations, invalid_rx + ): + """ + Test the ``_get_components`` with invalid receivers. + """ + # Override receivers in simulation's survey + if invalid_rx == "no-rx": + receivers = [] + msg = re.escape("Found invalid survey without receivers.") + else: + receivers = [ + magnetics.receivers.Point(receiver_locations, components=c) + for c in ("tmi", ["bx", "by"]) + ] + msg = re.escape( + "Found invalid survey with receivers that have mixed components." + ) + # Override private attribute `_receiver_list` to bypass the setter + sample_simulation.survey.source_field._receiver_list = receivers + + # Try to get components + with pytest.raises(ValueError, match=msg): + sample_simulation._get_components() + + @pytest.mark.parametrize("invalid_rx", ["no-rx", "different-components"]) + @pytest.mark.parametrize("method", ["projectFields", "projectFieldsDeriv"]) + def test_project_fields_invalid( + self, sample_simulation, receiver_locations, invalid_rx, method + ): + """ + Test ``projectFields`` and ``projectFieldsDeriv`` on invalid surveys. + """ + # Override receivers in simulation's survey + if invalid_rx == "no-rx": + receivers = None + msg = re.escape("Found invalid survey without receivers.") + else: + receivers = [ + magnetics.receivers.Point(receiver_locations, components=c) + for c in ("tmi", ["bx", "by", "bz"]) + ] + msg = re.escape( + "Found invalid survey with receivers that have mixed components." + ) + # Override private attribute `_receiver_list` to bypass the setter + sample_simulation.survey.source_field._receiver_list = receivers + + # Compute fields from a random model + n_cells = sample_simulation.mesh.n_cells + model = np.random.default_rng(seed=42).uniform(size=n_cells) + fields = sample_simulation.fields(model) + + # Test errors + method = getattr(sample_simulation, method) + with pytest.raises(ValueError, match=msg): + method(fields) diff --git a/tests/pf/test_forward_PFproblem.py b/tests/pf/test_forward_PFproblem.py index 2c54300d7a..fa2cb3fe2e 100644 --- a/tests/pf/test_forward_PFproblem.py +++ b/tests/pf/test_forward_PFproblem.py @@ -33,11 +33,11 @@ def setUp(self): yr = np.linspace(-300, 300, 41) X, Y = np.meshgrid(xr, yr) Z = np.ones((xr.size, yr.size)) * 150 - components = ["bx", "by", "bz"] + self.components = ["bx", "by", "bz"] self.xr = xr self.yr = yr self.rxLoc = np.c_[utils.mkvc(X), utils.mkvc(Y), utils.mkvc(Z)] - receivers = mag.Point(self.rxLoc, components=components) + receivers = mag.Point(self.rxLoc, components=self.components) srcField = mag.UniformBackgroundField( receiver_list=[receivers], amplitude=Btot, @@ -70,7 +70,7 @@ def test_ana_forward(self): "secondary", ) - n_obs, n_comp = self.rxLoc.shape[0], len(self.survey.components) + n_obs, n_comp = self.rxLoc.shape[0], len(self.components) dx, dy, dz = dpred.reshape(n_comp, n_obs) err_x = np.linalg.norm(dx - bxa) / np.linalg.norm(bxa) From f852c690e6b037f5254ee59f6a375e4f0dc74fd8 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 22 Apr 2025 17:39:33 -0700 Subject: [PATCH 128/194] Fix bug on magnetic simulation `nD` property (#1646) Make use of `survey.nD` instead of just using the total number of receiver locations. Add the special case where `is_amplitude_data` is `True`. Add tests. --- .../potential_fields/magnetics/simulation.py | 3 +- tests/pf/test_forward_Mag_Linear.py | 127 ++++++++++++++++++ 2 files changed, 128 insertions(+), 2 deletions(-) diff --git a/simpeg/potential_fields/magnetics/simulation.py b/simpeg/potential_fields/magnetics/simulation.py index 72ada84d2f..c103fc9cfa 100644 --- a/simpeg/potential_fields/magnetics/simulation.py +++ b/simpeg/potential_fields/magnetics/simulation.py @@ -324,8 +324,7 @@ def nD(self): """ Number of data """ - self._nD = self.survey.receiver_locations.shape[0] - + self._nD = self.survey.nD if not self.is_amplitude_data else self.survey.nD // 3 return self._nD @property diff --git a/tests/pf/test_forward_Mag_Linear.py b/tests/pf/test_forward_Mag_Linear.py index 596813ff09..e593c71872 100644 --- a/tests/pf/test_forward_Mag_Linear.py +++ b/tests/pf/test_forward_Mag_Linear.py @@ -10,6 +10,7 @@ import simpeg from simpeg import maps, utils +from simpeg.utils import model_builder from simpeg.potential_fields import magnetics as mag @@ -907,3 +908,129 @@ def test_removed_modeltype(): message = "modelType has been removed, please use model_type." with pytest.raises(NotImplementedError, match=message): sim.modelType + + +class BaseFixtures: + """ + Base test class with some fixtures. + + Requires that any child class implements a ``scalar_model`` boolean fixture. + It can be a standalone fixture, or it can be a class parametrization. + """ + + def build_survey(self, *, components): + # Observation points + x = np.linspace(-20.0, 20.0, 4) + x, y = np.meshgrid(x, x) + z = 5.0 * np.ones_like(x) + coordinates = np.vstack((x.ravel(), y.ravel(), z.ravel())).T + receivers = mag.receivers.Point(coordinates, components=components) + source_field = mag.UniformBackgroundField( + receiver_list=[receivers], + amplitude=55_000, + inclination=12, + declination=-35, + ) + survey = mag.survey.Survey(source_field) + return survey + + @pytest.fixture( + params=[ + "tmi", + ["bx", "by", "bz"], + ["tmi", "bx"], + ["tmi_x", "tmi_y", "tmi_z"], + ], + ids=["tmi", "mag_components", "tmi_and_mag", "tmi_derivs"], + ) + def survey(self, request): + """ + Return sample magnetic survey. + """ + return self.build_survey(components=request.param) + + @pytest.fixture + def mesh(self): + # Mesh + dh = 5.0 + hx = [(dh, 4)] + mesh = discretize.TensorMesh([hx, hx, hx], "CCN") + return mesh + + @pytest.fixture + def susceptibilities(self, mesh, scalar_model: bool): + """Create sample susceptibilities.""" + susceptibilities = 1e-10 * np.ones( + mesh.n_cells if scalar_model else 3 * mesh.n_cells + ) + ind_sphere = model_builder.get_indices_sphere( + np.r_[0.0, 0.0, -20.0], 10.0, mesh.cell_centers + ) + if scalar_model: + susceptibilities[ind_sphere] = 0.2 + else: + susceptibilities[: mesh.n_cells][ind_sphere] = 0.2 + susceptibilities[mesh.n_cells : 2 * mesh.n_cells][ind_sphere] = 0.3 + susceptibilities[2 * mesh.n_cells : 3 * mesh.n_cells][ind_sphere] = 0.5 + return susceptibilities + + +@pytest.mark.parametrize( + "scalar_model", [True, False], ids=["scalar_model", "vector_model"] +) +class TestnD(BaseFixtures): + """ + Test the ``nD`` property. + """ + + @pytest.fixture + def survey_b_norm(self): + return self.build_survey(components=["bx", "by", "bz"]) + + @pytest.fixture + def mapping(self, mesh, scalar_model): + nparams = mesh.n_cells if scalar_model else 3 * mesh.n_cells + return maps.IdentityMap(nP=nparams) + + def test_nD(self, mesh, survey, mapping, susceptibilities, scalar_model): + """ + Test nD on tmi data. + """ + model_type = "scalar" if scalar_model else "vector" + simulation = mag.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + chiMap=mapping, + store_sensitivities="ram", + engine="choclo", + model_type=model_type, + ) + receivers = survey.source_field.receiver_list + n_data = sum(rx.locations.shape[0] * len(rx.components) for rx in receivers) + assert simulation.nD == n_data + dpred = simulation.dpred(susceptibilities) + assert dpred.size == simulation.nD + + def test_nD_amplitude_data( + self, mesh, survey_b_norm, mapping, susceptibilities, scalar_model + ): + """ + Test nD on amplitude data. + """ + model_type = "scalar" if scalar_model else "vector" + simulation = mag.simulation.Simulation3DIntegral( + survey=survey_b_norm, + mesh=mesh, + chiMap=mapping, + store_sensitivities="ram", + engine="choclo", + model_type=model_type, + is_amplitude_data=True, + ) + receivers = survey_b_norm.source_field.receiver_list + n_data = ( + sum(rx.locations.shape[0] * len(rx.components) for rx in receivers) // 3 + ) + assert simulation.nD == n_data + dpred = simulation.dpred(susceptibilities) + assert dpred.size == simulation.nD From 1dee477bef1eedcb970251dc8ef9231c884c26cb Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Wed, 23 Apr 2025 12:22:26 -0600 Subject: [PATCH 129/194] Make pytest error on random seeded test (#1598) Use pytest's warning filter to make tests error when running a randomized discretize test that doesn't use a random seed. --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3d0efb96d4..75dd690fb8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -259,5 +259,6 @@ rst-roles = [ [tool.pytest.ini_options] filterwarnings = [ "ignore::simpeg.utils.solver_utils.DefaultSolverWarning", + "error:You are running a pytest without setting a random seed.*:UserWarning", "error:The `index_dictionary` property has been deprecated:FutureWarning", -] +] \ No newline at end of file From 5b7b8910279a3fb2e71cfa4fc4179c4f87b47285 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Wed, 23 Apr 2025 13:35:23 -0600 Subject: [PATCH 130/194] Add support for potential fields survey indexing (#1635) Sets source list on potential fields receivers to the source field input (while ensuring their is only 1 item. --- simpeg/potential_fields/gravity/survey.py | 32 +++++++- simpeg/potential_fields/magnetics/survey.py | 37 ++++++++- simpeg/utils/code_utils.py | 23 +++++- tests/pf/test_pf_survey.py | 91 +++++++++++++++++++++ tests/pf/test_survey_counting.py | 41 ---------- tests/utils/test_validators.py | 12 +++ 6 files changed, 184 insertions(+), 52 deletions(-) create mode 100644 tests/pf/test_pf_survey.py delete mode 100644 tests/pf/test_survey_counting.py diff --git a/simpeg/potential_fields/gravity/survey.py b/simpeg/potential_fields/gravity/survey.py index 447b4b69d1..a14f61296a 100644 --- a/simpeg/potential_fields/gravity/survey.py +++ b/simpeg/potential_fields/gravity/survey.py @@ -1,5 +1,5 @@ from ...survey import BaseSurvey -from ...utils.code_utils import validate_type +from ...utils.code_utils import validate_list_of_types from .sources import SourceField try: @@ -21,10 +21,34 @@ class Survey(BaseSurvey): """ def __init__(self, source_field, **kwargs): - self.source_field = validate_type( - "source_field", source_field, SourceField, cast=False + if "source_list" in kwargs: + msg = ( + "source_list is not a valid argument to gravity.Survey. " + "Use source_field instead." + ) + raise TypeError(msg) + super().__init__(source_list=source_field, **kwargs) + + @BaseSurvey.source_list.setter + def source_list(self, new_list): + new_list = validate_list_of_types( + "source_list", new_list, SourceField, ensure_unique=True, min_n=1, max_n=1 ) - super().__init__(source_list=None, **kwargs) + self._source_list = new_list + + @property + def source_field(self): + """A source object that contains the gravity receivers. + + Returns + ------- + simpeg.potential_fields.gravity.sources.SourceField + """ + return self.source_list[0] + + @source_field.setter + def source_field(self, new_src): + self.source_list = new_src def eval(self, fields): # noqa: A003 """Evaluate the field diff --git a/simpeg/potential_fields/magnetics/survey.py b/simpeg/potential_fields/magnetics/survey.py index ac66c7c42f..c55bfcf653 100644 --- a/simpeg/potential_fields/magnetics/survey.py +++ b/simpeg/potential_fields/magnetics/survey.py @@ -1,6 +1,6 @@ import numpy as np from ...survey import BaseSurvey -from ...utils.code_utils import validate_type +from ...utils.code_utils import validate_list_of_types from .sources import UniformBackgroundField try: @@ -22,10 +22,39 @@ class Survey(BaseSurvey): """ def __init__(self, source_field, **kwargs): - self.source_field = validate_type( - "source_field", source_field, UniformBackgroundField, cast=False + if "source_list" in kwargs: + msg = ( + "source_list is not a valid argument to gravity.Survey. " + "Use source_field instead." + ) + raise TypeError(msg) + super().__init__(source_list=source_field, **kwargs) + + @BaseSurvey.source_list.setter + def source_list(self, new_list): + # mag simulations only support 1 source... for now... + self._source_list = validate_list_of_types( + "source_list", + new_list, + UniformBackgroundField, + ensure_unique=True, + min_n=1, + max_n=1, ) - super().__init__(source_list=None, **kwargs) + + @property + def source_field(self): + """A source defining the Earth's inducing field and containing the magnetic receivers. + + Returns + ------- + simpeg.potential_fields.magnetics.sources.UniformBackgroundField + """ + return self.source_list[0] + + @source_field.setter + def source_field(self, new_src): + self.source_list = new_src def eval(self, fields): # noqa: A003 """Compute the fields diff --git a/simpeg/utils/code_utils.py b/simpeg/utils/code_utils.py index e0aef08c5a..0d2895e133 100644 --- a/simpeg/utils/code_utils.py +++ b/simpeg/utils/code_utils.py @@ -1,4 +1,5 @@ import types + import numpy as np from functools import wraps import warnings @@ -933,7 +934,9 @@ def validate_float( return var -def validate_list_of_types(property_name, var, class_type, ensure_unique=False): +def validate_list_of_types( + property_name, var, class_type, ensure_unique=False, min_n=0, max_n=None +): """Validate list of instances of a certain class Parameters @@ -946,6 +949,8 @@ def validate_list_of_types(property_name, var, class_type, ensure_unique=False): Class type(s) that are allowed in the list ensure_unique : bool, optional Checks if all items in the var are unique items. + min_n, max_n : int, optional + Minimum and maximum supported list length. Defaults accept any length list. Returns ------- @@ -959,8 +964,20 @@ def validate_list_of_types(property_name, var, class_type, ensure_unique=False): else: raise TypeError(f"{property_name!r} must be a list of {class_type}") - is_true = [isinstance(x, class_type) for x in var] - if np.all(is_true): + if max_n is not None: + if min_n == max_n and len(var) != max_n: + raise ValueError( + f"{property_name!r} must have exactly {min_n} item{'s' if min_n != 1 else ''}." + ) + elif len(var) > max_n: + raise ValueError( + f"{property_name!r} must have at most {max_n} item{'s' if max_n != 1 else ''}." + ) + if len(var) < min_n: + raise ValueError( + f"{property_name!r} must have at least {min_n} item{'s' if min_n != 1 else ''}." + ) + if all(isinstance(x, class_type) for x in var): if ensure_unique and len(set(var)) != len(var): raise ValueError( f"The {property_name!r} list must be unique. Cannot re-use items" diff --git a/tests/pf/test_pf_survey.py b/tests/pf/test_pf_survey.py new file mode 100644 index 0000000000..0cefa8dd88 --- /dev/null +++ b/tests/pf/test_pf_survey.py @@ -0,0 +1,91 @@ +import functools +import numpy as np +import pytest +from simpeg.potential_fields import gravity as grav +from simpeg.potential_fields import magnetics as mag +from simpeg.data import Data + + +@pytest.fixture(params=["gravity", "magnetics"]) +def survey(request): + rx_locs = np.random.rand(20, 3) + if request.param == "gravity": + rx1_components = ["gx", "gz"] + rx2_components = "gzz" + mod = grav + Source = functools.partial(grav.SourceField) + else: # request.param == "magnetics": + rx1_components = ["bx", "by"] + rx2_components = "tmi" + + mod = mag + Source = functools.partial( + mag.UniformBackgroundField, amplitude=50_000, inclination=90, declination=0 + ) + + rx1 = mod.Point(rx_locs, components=rx1_components) + rx2 = mod.Point(rx_locs, components=rx2_components) + src = Source(receiver_list=[rx1, rx2]) + return mod.Survey(src) + + +def test_survey_counts(survey): + src = survey.source_field + rx1, rx2 = src.receiver_list + + assert rx1.nD == 40 + assert rx2.nD == 20 + assert src.nD == 60 + assert survey.nRx == 40 + np.testing.assert_equal(src.vnD, [40, 20]) + assert survey.nD == 60 + np.testing.assert_equal(survey.vnD, [40, 20]) + + +def test_survey_indexing(survey): + src = survey.source_field + rx1, rx2 = src.receiver_list + d1 = -10 * np.arange(rx1.nD) + d2 = 10 + np.arange(rx2.nD) + data_vec = np.r_[d1, d2] + + data = Data(survey=survey, dobs=data_vec) + + np.testing.assert_equal(data[src, rx1], d1) + np.testing.assert_equal(data[src, rx2], d2) + + +@pytest.mark.parametrize("survey_cls", [grav.Survey, mag.Survey]) +def test_source_list_kwarg(survey_cls): + # cannot pass anything to source list for these classes. + with pytest.raises(TypeError, match=r"source_list is not a valid argument to .*"): + survey_cls("placeholder", source_list=None) + + +@pytest.mark.parametrize( + "survey_cls, source_cls", + [ + (grav.Survey, grav.SourceField), + ( + mag.Survey, + functools.partial( + mag.UniformBackgroundField, + amplitude=50_000, + inclination=90, + declination=0, + ), + ), + ], +) +def test_setting_sourcefield(survey_cls, source_cls): + src1 = source_cls(receiver_list=[]) + survey = survey_cls(source_field=src1) + assert survey.source_field is src1 + assert survey.source_list[0] is src1 + + src2 = source_cls(receiver_list=[]) + survey.source_field = src2 + assert survey.source_field is not src1 + assert survey.source_field is src2 + assert survey.source_list[0] is not src1 + assert survey.source_list[0] is src2 diff --git a/tests/pf/test_survey_counting.py b/tests/pf/test_survey_counting.py deleted file mode 100644 index d0e0d71002..0000000000 --- a/tests/pf/test_survey_counting.py +++ /dev/null @@ -1,41 +0,0 @@ -import numpy as np -from simpeg.potential_fields import gravity as grav -from simpeg.potential_fields import magnetics as mag - - -def test_gravity_survey(): - rx_locs = np.random.rand(20, 3) - rx_components = ["gx", "gz"] - - rx1 = grav.Point(rx_locs, components=rx_components) - rx2 = grav.Point(rx_locs, components="gzz") - src = grav.SourceField([rx1, rx2]) - survey = grav.Survey(src) - - assert rx1.nD == 40 - assert rx2.nD == 20 - assert src.nD == 60 - assert survey.nRx == 40 - np.testing.assert_equal(src.vnD, [40, 20]) - assert survey.nD == 60 - np.testing.assert_equal(survey.vnD, [40, 20]) - - -def test_magnetics_survey(): - rx_locs = np.random.rand(20, 3) - rx_components = ["bx", "by", "bz"] - - rx1 = mag.Point(rx_locs, components=rx_components) - rx2 = mag.Point(rx_locs, components="tmi") - src = mag.UniformBackgroundField( - receiver_list=[rx1, rx2], amplitude=50_000, inclination=90, declination=0 - ) - survey = mag.Survey(src) - - assert rx1.nD == 60 - assert rx2.nD == 20 - assert src.nD == 80 - np.testing.assert_equal(src.vnD, [60, 20]) - assert survey.nRx == 40 - assert survey.nD == 80 - np.testing.assert_equal(survey.vnD, [60, 20]) diff --git a/tests/utils/test_validators.py b/tests/utils/test_validators.py index 2a6d1bc0dd..ad86256ab7 100644 --- a/tests/utils/test_validators.py +++ b/tests/utils/test_validators.py @@ -150,6 +150,18 @@ def test_list_validation(): "ListProperty", ["Hello", "Hello", "Hello"], str, ensure_unique=True ) + # list is not long enough: + with pytest.raises(ValueError, match=r"'ListProperty' must have at least.*"): + validate_list_of_types("ListProperty", [1, 2, 3, 4], int, min_n=5) + + # list is too long: + with pytest.raises(ValueError, match=r"'ListProperty' must have at most.*"): + validate_list_of_types("ListProperty", [1, 2, 3, 4], int, max_n=2) + + # item is not an exact length + with pytest.raises(ValueError, match=r"'ListProperty' must have exactly.*"): + validate_list_of_types("ListProperty", [1, 2, 3, 4], int, min_n=3, max_n=3) + def test_location_validation(): # simple valid location From e61d9b2fbd2edd74fd15024527a4a7eabf32b02f Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 23 Apr 2025 16:08:12 -0700 Subject: [PATCH 131/194] Implement magnetic G as linear operator (#1634) Make the `G` property in the magnetic survey to be either a 2D Numpy array, or a `scipy.sparse.LinearOperator` if `store_sensitivities` is set to `"forward_only"`. The `LinearOperator` is capable of performing operations like `G @ m` and `G.T @ v` without allocating the full `G` matrix on memory or disk. This allows `Jvec` and `Jtvec` to be called and not allocate the full `G` matrix. Overwrite the `getJ` method to return either a 2D Numpy array or a `LinearOperator` formed by the product between `G` and the `chiDeriv` as a `LinearOperator` as well. Reimplement the `getJtJdiag` method: allow it to get the diagonal without building `G` if `"forward_only"`, optimize the computation of the diagonal when `G` is fully formed through `einsum`s, and fix the cache of `gtg_diagonal` by checking the hash of the `W` argument. Raise `NotImplementedError`s in some methods when `is_amplitude_data` is `True`, specially the `"forward_only"` cases where we don't want to allocate the `G` matrix. Extend docstrings of relevant methods. Add tests for all the new features. --- pyproject.toml | 3 +- .../magnetics/_numba_functions.py | 1805 +++++++++++++++-- .../potential_fields/magnetics/simulation.py | 486 ++++- tests/pf/test_forward_Mag_Linear.py | 705 +++++++ 4 files changed, 2736 insertions(+), 263 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 75dd690fb8..a62d394c0b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -261,4 +261,5 @@ filterwarnings = [ "ignore::simpeg.utils.solver_utils.DefaultSolverWarning", "error:You are running a pytest without setting a random seed.*:UserWarning", "error:The `index_dictionary` property has been deprecated:FutureWarning", -] \ No newline at end of file +] +xfail_strict = true diff --git a/simpeg/potential_fields/magnetics/_numba_functions.py b/simpeg/potential_fields/magnetics/_numba_functions.py index dfda671459..031b057a22 100644 --- a/simpeg/potential_fields/magnetics/_numba_functions.py +++ b/simpeg/potential_fields/magnetics/_numba_functions.py @@ -47,9 +47,13 @@ def _sensitivity_mag( Array with the locations of the receivers nodes : (n_active_nodes, 3) array Array with the location of the mesh nodes. - sensitivity_matrix : (n_receivers, n_active_nodes) array + sensitivity_matrix : array Empty 2d array where the sensitivity matrix elements will be filled. This could be a preallocated empty array or a slice of it. + The array should have a shape of ``(n_receivers, n_active_cells)`` + if ``scalar_model`` is True. + The array should have a shape of ``(n_receivers, 3 * n_active_cells)`` + if ``scalar_model`` is False. cell_nodes : (n_active_cells, 8) array Array of integers, where each row contains the indices of the nodes for each active cell in the mesh. @@ -180,135 +184,1560 @@ def _sensitivity_mag( # Compute sensitivity matrix elements from the kernel values for k in range(n_cells): nodes_indices = cell_nodes[k, :] - ux = kernels_in_nodes_to_cell(kx, nodes_indices) - uy = kernels_in_nodes_to_cell(ky, nodes_indices) - uz = kernels_in_nodes_to_cell(kz, nodes_indices) + ux = kernels_in_nodes_to_cell(kx, nodes_indices) + uy = kernels_in_nodes_to_cell(ky, nodes_indices) + uz = kernels_in_nodes_to_cell(kz, nodes_indices) + if scalar_model: + sensitivity_matrix[i, k] = ( + constant_factor + * regional_field_amplitude + * (ux * fx + uy * fy + uz * fz) + ) + else: + sensitivity_matrix[i, k] = ( + constant_factor * regional_field_amplitude * ux + ) + sensitivity_matrix[i, k + n_cells] = ( + constant_factor * regional_field_amplitude * uy + ) + sensitivity_matrix[i, k + 2 * n_cells] = ( + constant_factor * regional_field_amplitude * uz + ) + + +def _sensitivity_tmi( + receivers, + nodes, + sensitivity_matrix, + cell_nodes, + regional_field, + constant_factor, + scalar_model, +): + r""" + Fill the sensitivity matrix for TMI + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_sensitivity_tmi = jit(nopython=True, parallel=True)(_sensitivity_tmi) + + Parameters + ---------- + receivers : (n_receivers, 3) array + Array with the locations of the receivers + nodes : (n_active_nodes, 3) array + Array with the location of the mesh nodes. + sensitivity_matrix : array + Empty 2d array where the sensitivity matrix elements will be filled. + This could be a preallocated empty array or a slice of it. + The array should have a shape of ``(n_receivers, n_active_cells)`` + if ``scalar_model`` is True. + The array should have a shape of ``(n_receivers, 3 * n_active_cells)`` + if ``scalar_model`` is False. + cell_nodes : (n_active_cells, 8) array + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the sensitivity matrix is build to work with scalar models + (susceptibilities). + If False, the sensitivity matrix is build to work with vector models + (effective susceptibilities). + + Notes + ----- + + About the model array + ^^^^^^^^^^^^^^^^^^^^^ + + The ``model`` must always be a 1d array: + + * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with + the same number of elements as active cells in the mesh. It should store + the magnetic susceptibilities of each active cell in SI units. + * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array + with a number of elements equal to three times the active cells in the + mesh. It should store the components of the magnetization vector of each + active cell in :math:`Am^{-1}`. The order in which the components should + be passed are: + * every _easting_ component of each active cell, + * then every _northing_ component of each active cell, + * and finally every _upward_ component of each active cell. + + About the sensitivity matrix + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + Each row of the sensitivity matrix corresponds to a single receiver + location. + + If ``scalar_model`` is True, then each element of the row will + correspond to the partial derivative of the tmi + with respect to the susceptibility of each cell in the mesh. + + If ``scalar_model`` is False, then each row can be split in three sections + containing: + + * the partial derivatives of the tmi with respect + to the _x_ component of the effective susceptibility of each cell; then + * the partial derivatives of the tmi with respect + to the _y_ component of the effective susceptibility of each cell; and then + * the partial derivatives of the tmi with respect + to the _z_ component of the effective susceptibility of each cell. + + So, if we call :math:`T_j` the tmi on the receiver + :math:`j`, and :math:`\bar{\chi}^{(i)} = (\chi_x^{(i)}, \chi_y^{(i)}, + \chi_z^{(i)})` the effective susceptibility of the active cell :math:`i`, + then each row of the sensitivity matrix will be: + + .. math:: + + \left[ + \frac{\partial T_j}{\partial \chi_x^{(1)}}, + \dots, + \frac{\partial T_j}{\partial \chi_x^{(N)}}, + \frac{\partial T_j}{\partial \chi_y^{(1)}}, + \dots, + \frac{\partial T_j}{\partial \chi_y^{(N)}}, + \frac{\partial T_j}{\partial \chi_z^{(1)}}, + \dots, + \frac{\partial T_j}{\partial \chi_z^{(N)}} + \right] + + where :math:`N` is the total number of active cells. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kxx, kyy, kzz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + kxy, kxz, kyz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kxx[j] = choclo.prism.kernel_ee(dx, dy, dz, distance) + kyy[j] = choclo.prism.kernel_nn(dx, dy, dz, distance) + kzz[j] = choclo.prism.kernel_uu(dx, dy, dz, distance) + kxy[j] = choclo.prism.kernel_en(dx, dy, dz, distance) + kxz[j] = choclo.prism.kernel_eu(dx, dy, dz, distance) + kyz[j] = choclo.prism.kernel_nu(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + uxx = kernels_in_nodes_to_cell(kxx, nodes_indices) + uyy = kernels_in_nodes_to_cell(kyy, nodes_indices) + uzz = kernels_in_nodes_to_cell(kzz, nodes_indices) + uxy = kernels_in_nodes_to_cell(kxy, nodes_indices) + uxz = kernels_in_nodes_to_cell(kxz, nodes_indices) + uyz = kernels_in_nodes_to_cell(kyz, nodes_indices) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + # Fill the sensitivity matrix element(s) that correspond to the + # current active cell + if scalar_model: + sensitivity_matrix[i, k] = ( + constant_factor + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + else: + sensitivity_matrix[i, k] = ( + constant_factor * regional_field_amplitude * bx + ) + sensitivity_matrix[i, k + n_cells] = ( + constant_factor * regional_field_amplitude * by + ) + sensitivity_matrix[i, k + 2 * n_cells] = ( + constant_factor * regional_field_amplitude * bz + ) + + +def _sensitivity_tmi_derivative( + receivers, + nodes, + sensitivity_matrix, + cell_nodes, + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + constant_factor, + scalar_model, +): + r""" + Fill the sensitivity matrix for a TMI derivative. + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_sens = jit(nopython=True, parallel=True)(_sensitivity_tmi_derivative) + + Parameters + ---------- + receivers : (n_receivers, 3) array + Array with the locations of the receivers + nodes : (n_active_nodes, 3) array + Array with the location of the mesh nodes. + sensitivity_matrix : array + Empty 2d array where the sensitivity matrix elements will be filled. + This could be a preallocated empty array or a slice of it. + The array should have a shape of ``(n_receivers, n_active_cells)`` + if ``scalar_model`` is True. + The array should have a shape of ``(n_receivers, 3 * n_active_cells)`` + if ``scalar_model`` is False. + cell_nodes : (n_active_cells, 8) array + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz : callables + Kernel functions used for computing the desired TMI derivative. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the sensitivity matrix is build to work with scalar models + (susceptibilities). + If False, the sensitivity matrix is build to work with vector models + (effective susceptibilities). + + Notes + ----- + + About the kernel functions + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + + To compute the :math:`\alpha` derivative of the TMI :math:`\Delta T` (with + :math:`\alpha \in \{x, y, z\}` we need to evaluate third order kernels + functions for the prism. The kernels we need to evaluate can be obtained by + fixing one of the subindices to the direction of the derivative + (:math:`\alpha`) and cycle through combinations of the other two. + + For ``tmi_x`` we need to pass: + + .. code:: + + kernel_xx=kernel_eee, kernel_yy=kernel_enn, kernel_zz=kernel_euu, + kernel_xy=kernel_een, kernel_xz=kernel_eeu, kernel_yz=kernel_enu + + For ``tmi_y`` we need to pass: + + .. code:: + + kernel_xx=kernel_een, kernel_yy=kernel_nnn, kernel_zz=kernel_nuu, + kernel_xy=kernel_enn, kernel_xz=kernel_enu, kernel_yz=kernel_nnu + + For ``tmi_z`` we need to pass: + + .. code:: + + kernel_xx=kernel_eeu, kernel_yy=kernel_nnu, kernel_zz=kernel_uuu, + kernel_xy=kernel_enu, kernel_xz=kernel_euu, kernel_yz=kernel_nuu + + + About the model array + ^^^^^^^^^^^^^^^^^^^^^ + + The ``model`` must always be a 1d array: + + * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with + the same number of elements as active cells in the mesh. It should store + the magnetic susceptibilities of each active cell in SI units. + * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array + with a number of elements equal to three times the active cells in the + mesh. It should store the components of the magnetization vector of each + active cell in :math:`Am^{-1}`. The order in which the components should + be passed are: + * every _easting_ component of each active cell, + * then every _northing_ component of each active cell, + * and finally every _upward_ component of each active cell. + + About the sensitivity matrix + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + Each row of the sensitivity matrix corresponds to a single receiver + location. + + If ``scalar_model`` is True, then each element of the row will + correspond to the partial derivative of the tmi derivative (spatial) + with respect to the susceptibility of each cell in the mesh. + + If ``scalar_model`` is False, then each row can be split in three sections + containing: + + * the partial derivatives of the tmi derivative with respect + to the _x_ component of the effective susceptibility of each cell; then + * the partial derivatives of the tmi derivative with respect + to the _y_ component of the effective susceptibility of each cell; and then + * the partial derivatives of the tmi derivative with respect + to the _z_ component of the effective susceptibility of each cell. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kxx, kyy, kzz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + kxy, kxz, kyz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kxx[j] = kernel_xx(dx, dy, dz, distance) + kyy[j] = kernel_yy(dx, dy, dz, distance) + kzz[j] = kernel_zz(dx, dy, dz, distance) + kxy[j] = kernel_xy(dx, dy, dz, distance) + kxz[j] = kernel_xz(dx, dy, dz, distance) + kyz[j] = kernel_yz(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + uxx = kernels_in_nodes_to_cell(kxx, nodes_indices) + uyy = kernels_in_nodes_to_cell(kyy, nodes_indices) + uzz = kernels_in_nodes_to_cell(kzz, nodes_indices) + uxy = kernels_in_nodes_to_cell(kxy, nodes_indices) + uxz = kernels_in_nodes_to_cell(kxz, nodes_indices) + uyz = kernels_in_nodes_to_cell(kyz, nodes_indices) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + # Fill the sensitivity matrix element(s) that correspond to the + # current active cell + if scalar_model: + sensitivity_matrix[i, k] = ( + constant_factor + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + else: + sensitivity_matrix[i, k] = ( + constant_factor * regional_field_amplitude * bx + ) + sensitivity_matrix[i, k + n_cells] = ( + constant_factor * regional_field_amplitude * by + ) + sensitivity_matrix[i, k + 2 * n_cells] = ( + constant_factor * regional_field_amplitude * bz + ) + + +@jit(nopython=True, parallel=False) +def _mag_sensitivity_t_dot_v_serial( + receivers, + nodes, + cell_nodes, + regional_field, + kernel_x, + kernel_y, + kernel_z, + constant_factor, + scalar_model, + vector, + result, +): + r""" + Compute ``G.T @ v`` in serial, without building G, for a single magnetic component. + + Parameters + ---------- + receivers : (n_receivers, 3) array + Array with the locations of the receivers + nodes : (n_active_nodes, 3) array + Array with the location of the mesh nodes. + cell_nodes : (n_active_cells, 8) array + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_x, kernel_y, kernel_z : callable + Kernels used to compute the desired magnetic component. For example, + for computing bx we need to use ``kernel_x=kernel_ee``, + ``kernel_y=kernel_en``, ``kernel_z=kernel_eu``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the sensitivity matrix is built to work with scalar models + (susceptibilities). + If False, the sensitivity matrix is built to work with vector models + (effective susceptibilities). + vector : (n_receivers) numpy.ndarray + Array that represents the vector used in the dot product. + result : (n_active_cells) or (3 * n_active_cells) numpy.ndarray + Running result array where the output of the dot product will be added to. + The array should have ``n_active_cells`` elements if ``scalar_model`` + is True, or ``3 * n_active_cells`` otherwise. + + Notes + ----- + This function is meant to be run in serial. Writing to the ``result`` array + inside a parallel loop over the receivers generates a race condition that + leads to corrupted outputs. + + A parallel implementation of this function is available in + ``_mag_sensitivity_t_dot_v_parallel``. + + See also + -------- + _sensitivity_mag + Compute the sensitivity matrix for a single magnetic component by + allocating it in memory. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kx, ky, kz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kx[j] = kernel_x(dx, dy, dz, distance) + ky[j] = kernel_y(dx, dy, dz, distance) + kz[j] = kernel_z(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + ux = kernels_in_nodes_to_cell(kx, nodes_indices) + uy = kernels_in_nodes_to_cell(ky, nodes_indices) + uz = kernels_in_nodes_to_cell(kz, nodes_indices) + if scalar_model: + result[k] += ( + constant_factor + * vector[i] + * regional_field_amplitude + * (ux * fx + uy * fy + uz * fz) + ) + else: + result[k] += constant_factor * vector[i] * regional_field_amplitude * ux + result[k + n_cells] += ( + constant_factor * vector[i] * regional_field_amplitude * uy + ) + result[k + 2 * n_cells] += ( + constant_factor * vector[i] * regional_field_amplitude * uz + ) + + +@jit(nopython=True, parallel=True) +def _mag_sensitivity_t_dot_v_parallel( + receivers, + nodes, + cell_nodes, + regional_field, + kernel_x, + kernel_y, + kernel_z, + constant_factor, + scalar_model, + vector, + result, +): + r""" + Compute ``G.T @ v`` in parallel, without building G, for a single magnetic component + + Parameters + ---------- + receivers : (n_receivers, 3) array + Array with the locations of the receivers + nodes : (n_active_nodes, 3) array + Array with the location of the mesh nodes. + cell_nodes : (n_active_cells, 8) array + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_x, kernel_y, kernel_z : callable + Kernels used to compute the desired magnetic component. For example, + for computing bx we need to use ``kernel_x=kernel_ee``, + ``kernel_y=kernel_en``, ``kernel_z=kernel_eu``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the sensitivity matrix is built to work with scalar models + (susceptibilities). + If False, the sensitivity matrix is built to work with vector models + (effective susceptibilities). + vector : (n_receivers) numpy.ndarray + Array that represents the vector used in the dot product. + result : (n_active_cells) or (3 * n_active_cells) numpy.ndarray + Running result array where the output of the dot product will be added to. + The array should have ``n_active_cells`` elements if ``scalar_model`` + is True, or ``3 * n_active_cells`` otherwise. + + Notes + ----- + This function is meant to be run in parallel. + This implementation instructs each thread to allocate their own array for + the current row of the sensitivity matrix. After computing the elements of + that row, it gets added to the running ``result`` array through a reduction + operation handled by Numba. + + A serialized implementation of this function is available in + ``_mag_sensitivity_t_dot_v_serial``. + + See also + -------- + _sensitivity_mag + Compute the sensitivity matrix for a single magnetic component by + allocating it in memory. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + result_size = result.size + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kx, ky, kz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate array for the current row of the sensitivity matrix + local_row = np.empty(result_size) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kx[j] = kernel_x(dx, dy, dz, distance) + ky[j] = kernel_y(dx, dy, dz, distance) + kz[j] = kernel_z(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + ux = kernels_in_nodes_to_cell(kx, nodes_indices) + uy = kernels_in_nodes_to_cell(ky, nodes_indices) + uz = kernels_in_nodes_to_cell(kz, nodes_indices) + if scalar_model: + local_row[k] = ( + constant_factor + * vector[i] + * regional_field_amplitude + * (ux * fx + uy * fy + uz * fz) + ) + else: + local_row[k] = ( + constant_factor * vector[i] * regional_field_amplitude * ux + ) + local_row[k + n_cells] = ( + constant_factor * vector[i] * regional_field_amplitude * uy + ) + local_row[k + 2 * n_cells] = ( + constant_factor * vector[i] * regional_field_amplitude * uz + ) + # Apply reduction operation to add the values of the row to the running + # result. Avoid slicing the `result` array when updating it to avoid + # racing conditions, just add the `local_row` to the `results` + # variable. + result += local_row + + +@jit(nopython=True, parallel=False) +def _tmi_sensitivity_t_dot_v_serial( + receivers, + nodes, + cell_nodes, + regional_field, + constant_factor, + scalar_model, + vector, + result, +): + r""" + Compute ``G.T @ v`` in serial, without building G, for TMI. + + Parameters + ---------- + receivers : (n_receivers, 3) array + Array with the locations of the receivers + nodes : (n_active_nodes, 3) array + Array with the location of the mesh nodes. + cell_nodes : (n_active_cells, 8) array + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the sensitivity matrix is build to work with scalar models + (susceptibilities). + If False, the sensitivity matrix is build to work with vector models + (effective susceptibilities). + vector : (n_receivers) numpy.ndarray + Array that represents the vector used in the dot product. + result : (n_active_cells) or (3 * n_active_cells) numpy.ndarray + Running result array where the output of the dot product will be added to. + The array should have ``n_active_cells`` elements if ``scalar_model`` + is True, or ``3 * n_active_cells`` otherwise. + + Notes + ----- + This function is meant to be run in serial. Writing to the ``result`` array + inside a parallel loop over the receivers generates a race condition that + leads to corrupted outputs. + + A parallel implementation of this function is available in + ``_tmi_sensitivity_t_dot_v_parallel``. + + See also + -------- + _sensitivity_tmi + Compute the sensitivity matrix for TMI by allocating it in memory. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kxx, kyy, kzz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + kxy, kxz, kyz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kxx[j] = choclo.prism.kernel_ee(dx, dy, dz, distance) + kyy[j] = choclo.prism.kernel_nn(dx, dy, dz, distance) + kzz[j] = choclo.prism.kernel_uu(dx, dy, dz, distance) + kxy[j] = choclo.prism.kernel_en(dx, dy, dz, distance) + kxz[j] = choclo.prism.kernel_eu(dx, dy, dz, distance) + kyz[j] = choclo.prism.kernel_nu(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + uxx = kernels_in_nodes_to_cell(kxx, nodes_indices) + uyy = kernels_in_nodes_to_cell(kyy, nodes_indices) + uzz = kernels_in_nodes_to_cell(kzz, nodes_indices) + uxy = kernels_in_nodes_to_cell(kxy, nodes_indices) + uxz = kernels_in_nodes_to_cell(kxz, nodes_indices) + uyz = kernels_in_nodes_to_cell(kyz, nodes_indices) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + # Fill the sensitivity matrix element(s) that correspond to the + # current active cell + if scalar_model: + result[k] += ( + constant_factor + * vector[i] + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + else: + result[k] += constant_factor * vector[i] * regional_field_amplitude * bx + result[k + n_cells] += ( + constant_factor * vector[i] * regional_field_amplitude * by + ) + result[k + 2 * n_cells] += ( + constant_factor * vector[i] * regional_field_amplitude * bz + ) + + +@jit(nopython=True, parallel=True) +def _tmi_sensitivity_t_dot_v_parallel( + receivers, + nodes, + cell_nodes, + regional_field, + constant_factor, + scalar_model, + vector, + result, +): + r""" + Compute ``G.T @ v`` in parallel, without building G, for TMI. + + Parameters + ---------- + receivers : (n_receivers, 3) array + Array with the locations of the receivers + nodes : (n_active_nodes, 3) array + Array with the location of the mesh nodes. + cell_nodes : (n_active_cells, 8) array + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the sensitivity matrix is build to work with scalar models + (susceptibilities). + If False, the sensitivity matrix is build to work with vector models + (effective susceptibilities). + vector : (n_receivers) numpy.ndarray + Array that represents the vector used in the dot product. + result : (n_active_cells) or (3 * n_active_cells) numpy.ndarray + Running result array where the output of the dot product will be added to. + The array should have ``n_active_cells`` elements if ``scalar_model`` + is True, or ``3 * n_active_cells`` otherwise. + + Notes + ----- + This function is meant to be run in parallel. + This implementation instructs each thread to allocate their own array for + the current row of the sensitivity matrix. After computing the elements of + that row, it gets added to the running ``result`` array through a reduction + operation handled by Numba. + + A serialized implementation of this function is available in + ``_tmi_sensitivity_t_dot_v_serial``. + + See also + -------- + _sensitivity_tmi + Compute the sensitivity matrix for TMI by allocating it in memory. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + result_size = result.size + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kxx, kyy, kzz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + kxy, kxz, kyz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate array for the current row of the sensitivity matrix + local_row = np.empty(result_size) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kxx[j] = choclo.prism.kernel_ee(dx, dy, dz, distance) + kyy[j] = choclo.prism.kernel_nn(dx, dy, dz, distance) + kzz[j] = choclo.prism.kernel_uu(dx, dy, dz, distance) + kxy[j] = choclo.prism.kernel_en(dx, dy, dz, distance) + kxz[j] = choclo.prism.kernel_eu(dx, dy, dz, distance) + kyz[j] = choclo.prism.kernel_nu(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + uxx = kernels_in_nodes_to_cell(kxx, nodes_indices) + uyy = kernels_in_nodes_to_cell(kyy, nodes_indices) + uzz = kernels_in_nodes_to_cell(kzz, nodes_indices) + uxy = kernels_in_nodes_to_cell(kxy, nodes_indices) + uxz = kernels_in_nodes_to_cell(kxz, nodes_indices) + uyz = kernels_in_nodes_to_cell(kyz, nodes_indices) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + # Fill the sensitivity matrix element(s) that correspond to the + # current active cell + if scalar_model: + local_row[k] = ( + constant_factor + * vector[i] + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + else: + local_row[k] = ( + constant_factor * vector[i] * regional_field_amplitude * bx + ) + local_row[k + n_cells] = ( + constant_factor * vector[i] * regional_field_amplitude * by + ) + local_row[k + 2 * n_cells] = ( + constant_factor * vector[i] * regional_field_amplitude * bz + ) + # Apply reduction operation to add the values of the row to the running + # result. Avoid slicing the `result` array when updating it to avoid + # racing conditions, just add the `local_row` to the `results` + # variable. + result += local_row + + +@jit(nopython=True, parallel=False) +def _tmi_derivative_sensitivity_t_dot_v_serial( + receivers, + nodes, + cell_nodes, + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + constant_factor, + scalar_model, + vector, + result, +): + r""" + Compute ``G.T @ v`` in serial, without building G, for a spatial TMI derivative. + + Parameters + ---------- + receivers : (n_receivers, 3) array + Array with the locations of the receivers + nodes : (n_active_nodes, 3) array + Array with the location of the mesh nodes. + cell_nodes : (n_active_cells, 8) array + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz : callables + Kernel functions used for computing the desired TMI derivative. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the sensitivity matrix is build to work with scalar models + (susceptibilities). + If False, the sensitivity matrix is build to work with vector models + (effective susceptibilities). + vector : (n_receivers) numpy.ndarray + Array that represents the vector used in the dot product. + result : (n_active_cells) or (3 * n_active_cells) numpy.ndarray + Running result array where the output of the dot product will be added to. + The array should have ``n_active_cells`` elements if ``scalar_model`` + is True, or ``3 * n_active_cells`` otherwise. + + Notes + ----- + This function is meant to be run in serial. Writing to the ``result`` array + inside a parallel loop over the receivers generates a race condition that + leads to corrupted outputs. + + A parallel implementation of this function is available in + ``_tmi_derivative_sensitivity_t_dot_v_parallel``. + + See also + -------- + _sensitivity_tmi_derivative + Compute the sensitivity matrix for a TMI derivative by allocating it in memory. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kxx, kyy, kzz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + kxy, kxz, kyz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kxx[j] = kernel_xx(dx, dy, dz, distance) + kyy[j] = kernel_yy(dx, dy, dz, distance) + kzz[j] = kernel_zz(dx, dy, dz, distance) + kxy[j] = kernel_xy(dx, dy, dz, distance) + kxz[j] = kernel_xz(dx, dy, dz, distance) + kyz[j] = kernel_yz(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + uxx = kernels_in_nodes_to_cell(kxx, nodes_indices) + uyy = kernels_in_nodes_to_cell(kyy, nodes_indices) + uzz = kernels_in_nodes_to_cell(kzz, nodes_indices) + uxy = kernels_in_nodes_to_cell(kxy, nodes_indices) + uxz = kernels_in_nodes_to_cell(kxz, nodes_indices) + uyz = kernels_in_nodes_to_cell(kyz, nodes_indices) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + # Fill the sensitivity matrix element(s) that correspond to the + # current active cell + if scalar_model: + result[k] += ( + constant_factor + * vector[i] + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + else: + result[k] += constant_factor * vector[i] * regional_field_amplitude * bx + result[k + n_cells] += ( + constant_factor * vector[i] * regional_field_amplitude * by + ) + result[k + 2 * n_cells] += ( + constant_factor * vector[i] * regional_field_amplitude * bz + ) + + +@jit(nopython=True, parallel=True) +def _tmi_derivative_sensitivity_t_dot_v_parallel( + receivers, + nodes, + cell_nodes, + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + constant_factor, + scalar_model, + vector, + result, +): + r""" + Compute ``G.T @ v`` in parallel, without building G, for a spatial TMI derivative. + + Parameters + ---------- + receivers : (n_receivers, 3) array + Array with the locations of the receivers + nodes : (n_active_nodes, 3) array + Array with the location of the mesh nodes. + cell_nodes : (n_active_cells, 8) array + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz : callables + Kernel functions used for computing the desired TMI derivative. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the sensitivity matrix is build to work with scalar models + (susceptibilities). + If False, the sensitivity matrix is build to work with vector models + (effective susceptibilities). + vector : (n_receivers) numpy.ndarray + Array that represents the vector used in the dot product. + result : (n_active_cells) or (3 * n_active_cells) numpy.ndarray + Running result array where the output of the dot product will be added to. + The array should have ``n_active_cells`` elements if ``scalar_model`` + is True, or ``3 * n_active_cells`` otherwise. + + Notes + ----- + This function is meant to be run in parallel. + This implementation instructs each thread to allocate their own array for + the current row of the sensitivity matrix. After computing the elements of + that row, it gets added to the running ``result`` array through a reduction + operation handled by Numba. + + A serialized implementation of this function is available in + ``_tmi_derivative_sensitivity_t_dot_v_serial``. + + See also + -------- + _sensitivity_tmi_derivative + Compute the sensitivity matrix for a TMI derivative by allocating it in memory. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + result_size = result.size + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kxx, kyy, kzz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + kxy, kxz, kyz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate array for the current row of the sensitivity matrix + local_row = np.empty(result_size) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kxx[j] = kernel_xx(dx, dy, dz, distance) + kyy[j] = kernel_yy(dx, dy, dz, distance) + kzz[j] = kernel_zz(dx, dy, dz, distance) + kxy[j] = kernel_xy(dx, dy, dz, distance) + kxz[j] = kernel_xz(dx, dy, dz, distance) + kyz[j] = kernel_yz(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + uxx = kernels_in_nodes_to_cell(kxx, nodes_indices) + uyy = kernels_in_nodes_to_cell(kyy, nodes_indices) + uzz = kernels_in_nodes_to_cell(kzz, nodes_indices) + uxy = kernels_in_nodes_to_cell(kxy, nodes_indices) + uxz = kernels_in_nodes_to_cell(kxz, nodes_indices) + uyz = kernels_in_nodes_to_cell(kyz, nodes_indices) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + # Fill the sensitivity matrix element(s) that correspond to the + # current active cell + if scalar_model: + local_row[k] = ( + constant_factor + * vector[i] + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + else: + local_row[k] = ( + constant_factor * vector[i] * regional_field_amplitude * bx + ) + local_row[k + n_cells] = ( + constant_factor * vector[i] * regional_field_amplitude * by + ) + local_row[k + 2 * n_cells] = ( + constant_factor * vector[i] * regional_field_amplitude * bz + ) + # Apply reduction operation to add the values of the row to the running + # result. Avoid slicing the `result` array when updating it to avoid + # racing conditions, just add the `local_row` to the `results` + # variable. + result += local_row + + +@jit(nopython=True, parallel=False) +def _diagonal_G_T_dot_G_mag_serial( + receivers, + nodes, + cell_nodes, + regional_field, + kernel_x, + kernel_y, + kernel_z, + constant_factor, + scalar_model, + weights, + diagonal, +): + """ + Diagonal of ``G.T @ W.T @ W @ G`` for single magnetic component, in serial. + + This function doesn't store the full ``G`` matrix in memory. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + nodes : (n_active_nodes, 3) numpy.ndarray + Array with the location of the mesh nodes. + cell_nodes : (n_active_cells, 8) numpy.ndarray + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_x, kernel_y, kernel_z : callable + Kernels used to compute the desired magnetic component. For example, + for computing bx we need to use ``kernel_x=kernel_ee``, + ``kernel_y=kernel_en``, ``kernel_z=kernel_eu``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the sensitivity matrix is built to work with scalar models + (susceptibilities). + If False, the sensitivity matrix is built to work with vector models + (effective susceptibilities). + weights : (n_receivers,) numpy.ndarray + Array with data weights. It should be the diagonal of the ``W`` matrix, + squared. + diagonal : (n_active_cells) or (3 * n_active_cells) numpy.ndarray + Array where the diagonal of ``G.T @ G`` will be added to. + + Notes + ----- + This function is meant to be run in serial. Use the + ``_diagonal_G_T_dot_G_mag_parallel`` one for parallelized computations. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kx, ky, kz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kx[j] = kernel_x(dx, dy, dz, distance) + ky[j] = kernel_y(dx, dy, dz, distance) + kz[j] = kernel_z(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + ux = kernels_in_nodes_to_cell(kx, nodes_indices) + uy = kernels_in_nodes_to_cell(ky, nodes_indices) + uz = kernels_in_nodes_to_cell(kz, nodes_indices) + if scalar_model: + g_element = ( + constant_factor + * regional_field_amplitude + * (ux * fx + uy * fy + uz * fz) + ) + diagonal[k] += weights[i] * g_element**2 + else: + const = constant_factor * regional_field_amplitude + diagonal[k] += weights[i] * (const * ux) ** 2 + diagonal[k + n_cells] += weights[i] * (const * uy) ** 2 + diagonal[k + 2 * n_cells] += weights[i] * (const * uz) ** 2 + + +@jit(nopython=True, parallel=True) +def _diagonal_G_T_dot_G_mag_parallel( + receivers, + nodes, + cell_nodes, + regional_field, + kernel_x, + kernel_y, + kernel_z, + constant_factor, + scalar_model, + weights, + diagonal, +): + """ + Diagonal of ``G.T @ W.T @ W @ G`` for single magnetic component, in parallel. + + This function doesn't store the full ``G`` matrix in memory. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + nodes : (n_active_nodes, 3) numpy.ndarray + Array with the location of the mesh nodes. + cell_nodes : (n_active_cells, 8) numpy.ndarray + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_x, kernel_y, kernel_z : callable + Kernels used to compute the desired magnetic component. For example, + for computing bx we need to use ``kernel_x=kernel_ee``, + ``kernel_y=kernel_en``, ``kernel_z=kernel_eu``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the sensitivity matrix is built to work with scalar models + (susceptibilities). + If False, the sensitivity matrix is built to work with vector models + (effective susceptibilities). + weights : (n_receivers,) numpy.ndarray + Array with data weights. It should be the diagonal of the ``W`` matrix, + squared. + diagonal : (n_active_cells) or (3 * n_active_cells) numpy.ndarray + Array where the diagonal of ``G.T @ G`` will be added to. + + Notes + ----- + This function is meant to be run in parallel. Use the + ``_diagonal_G_T_dot_G_mag_serial`` one for serialized computations. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + diagonal_size = diagonal.size + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kx, ky, kz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate array for the diagonal elements for the current receiver. + local_diagonal = np.empty(diagonal_size) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kx[j] = kernel_x(dx, dy, dz, distance) + ky[j] = kernel_y(dx, dy, dz, distance) + kz[j] = kernel_z(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + ux = kernels_in_nodes_to_cell(kx, nodes_indices) + uy = kernels_in_nodes_to_cell(ky, nodes_indices) + uz = kernels_in_nodes_to_cell(kz, nodes_indices) + if scalar_model: + g_element = ( + constant_factor + * regional_field_amplitude + * (ux * fx + uy * fy + uz * fz) + ) + local_diagonal[k] = weights[i] * g_element**2 + else: + const = constant_factor * regional_field_amplitude + local_diagonal[k] = weights[i] * (const * ux) ** 2 + local_diagonal[k + n_cells] = weights[i] * (const * uy) ** 2 + local_diagonal[k + 2 * n_cells] = weights[i] * (const * uz) ** 2 + # Add the result to the diagonal. + # Apply reduction operation to add the values of the local diagonal to + # the running diagonal array. Avoid slicing the `diagonal` array when + # updating it to avoid racing conditions, just add the `local_diagonal` + # to the `diagonal` variable. + diagonal += local_diagonal + + +@jit(nopython=True, parallel=False) +def _diagonal_G_T_dot_G_tmi_serial( + receivers, + nodes, + cell_nodes, + regional_field, + constant_factor, + scalar_model, + weights, + diagonal, +): + """ + Diagonal of ``G.T @ W.T @ W @ G`` for TMI, in serial. + + This function doesn't store the full ``G`` matrix in memory. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + nodes : (n_active_nodes, 3) numpy.ndarray + Array with the location of the mesh nodes. + cell_nodes : (n_active_cells, 8) numpy.ndarray + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the sensitivity matrix is built to work with scalar models + (susceptibilities). + If False, the sensitivity matrix is built to work with vector models + (effective susceptibilities). + weights : (n_receivers,) numpy.ndarray + Array with data weights. It should be the diagonal of the ``W`` matrix, + squared. + diagonal : (n_active_cells) or (3 * n_active_cells) numpy.ndarray + Array where the diagonal of ``G.T @ G`` will be added to. + + Notes + ----- + This function is meant to be run in serial. Use the + ``_diagonal_G_T_dot_G_tmi_parallel`` one for parallelized computations. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kxx, kyy, kzz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + kxy, kxz, kyz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kxx[j] = choclo.prism.kernel_ee(dx, dy, dz, distance) + kyy[j] = choclo.prism.kernel_nn(dx, dy, dz, distance) + kzz[j] = choclo.prism.kernel_uu(dx, dy, dz, distance) + kxy[j] = choclo.prism.kernel_en(dx, dy, dz, distance) + kxz[j] = choclo.prism.kernel_eu(dx, dy, dz, distance) + kyz[j] = choclo.prism.kernel_nu(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + uxx = kernels_in_nodes_to_cell(kxx, nodes_indices) + uyy = kernels_in_nodes_to_cell(kyy, nodes_indices) + uzz = kernels_in_nodes_to_cell(kzz, nodes_indices) + uxy = kernels_in_nodes_to_cell(kxy, nodes_indices) + uxz = kernels_in_nodes_to_cell(kxz, nodes_indices) + uyz = kernels_in_nodes_to_cell(kyz, nodes_indices) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + + if scalar_model: + g_element = ( + constant_factor + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + diagonal[k] += weights[i] * g_element**2 + else: + const = constant_factor * regional_field_amplitude + diagonal[k] += weights[i] * (const * bx) ** 2 + diagonal[k + n_cells] += weights[i] * (const * by) ** 2 + diagonal[k + 2 * n_cells] += weights[i] * (const * bz) ** 2 + + +@jit(nopython=True, parallel=True) +def _diagonal_G_T_dot_G_tmi_parallel( + receivers, + nodes, + cell_nodes, + regional_field, + constant_factor, + scalar_model, + weights, + diagonal, +): + """ + Diagonal of ``G.T @ W.T @ W @ G`` for TMI, in parallel. + + This function doesn't store the full ``G`` matrix in memory. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + nodes : (n_active_nodes, 3) numpy.ndarray + Array with the location of the mesh nodes. + cell_nodes : (n_active_cells, 8) numpy.ndarray + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the sensitivity matrix is built to work with scalar models + (susceptibilities). + If False, the sensitivity matrix is built to work with vector models + (effective susceptibilities). + weights : (n_receivers,) numpy.ndarray + Array with data weights. It should be the diagonal of the ``W`` matrix, + squared. + diagonal : (n_active_cells) or (3 * n_active_cells) numpy.ndarray + Array where the diagonal of ``G.T @ G`` will be added to. + + Notes + ----- + This function is meant to be run in parallel. Use the + ``_diagonal_G_T_dot_G_tmi_serial`` one for serialized computations. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + diagonal_size = diagonal.size + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kxx, kyy, kzz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + kxy, kxz, kyz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate array for the diagonal elements for the current receiver. + local_diagonal = np.empty(diagonal_size) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kxx[j] = choclo.prism.kernel_ee(dx, dy, dz, distance) + kyy[j] = choclo.prism.kernel_nn(dx, dy, dz, distance) + kzz[j] = choclo.prism.kernel_uu(dx, dy, dz, distance) + kxy[j] = choclo.prism.kernel_en(dx, dy, dz, distance) + kxz[j] = choclo.prism.kernel_eu(dx, dy, dz, distance) + kyz[j] = choclo.prism.kernel_nu(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + uxx = kernels_in_nodes_to_cell(kxx, nodes_indices) + uyy = kernels_in_nodes_to_cell(kyy, nodes_indices) + uzz = kernels_in_nodes_to_cell(kzz, nodes_indices) + uxy = kernels_in_nodes_to_cell(kxy, nodes_indices) + uxz = kernels_in_nodes_to_cell(kxz, nodes_indices) + uyz = kernels_in_nodes_to_cell(kyz, nodes_indices) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + if scalar_model: - sensitivity_matrix[i, k] = ( + g_element = ( constant_factor * regional_field_amplitude - * (ux * fx + uy * fy + uz * fz) + * (bx * fx + by * fy + bz * fz) ) + local_diagonal[k] = weights[i] * g_element**2 else: - sensitivity_matrix[i, k] = ( - constant_factor * regional_field_amplitude * ux - ) - sensitivity_matrix[i, k + n_cells] = ( - constant_factor * regional_field_amplitude * uy - ) - sensitivity_matrix[i, k + 2 * n_cells] = ( - constant_factor * regional_field_amplitude * uz - ) + const = constant_factor * regional_field_amplitude + local_diagonal[k] = weights[i] * (const * bx) ** 2 + local_diagonal[k + n_cells] = weights[i] * (const * by) ** 2 + local_diagonal[k + 2 * n_cells] = weights[i] * (const * bz) ** 2 + # Add the result to the diagonal. + # Apply reduction operation to add the values of the local diagonal to + # the running diagonal array. Avoid slicing the `diagonal` array when + # updating it to avoid racing conditions, just add the `local_diagonal` + # to the `diagonal` variable. + diagonal += local_diagonal -def _sensitivity_tmi( + +@jit(nopython=True, parallel=False) +def _diagonal_G_T_dot_G_tmi_deriv_serial( receivers, nodes, - sensitivity_matrix, cell_nodes, regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, constant_factor, scalar_model, + weights, + diagonal, ): - r""" - Fill the sensitivity matrix for TMI - - This function should be used with a `numba.jit` decorator, for example: - - .. code:: - - from numba import jit + """ + Diagonal of ``G.T @ W.T @ W @ G`` for TMI derivatives, in serial. - jit_sensitivity_tmi = jit(nopython=True, parallel=True)(_sensitivity_tmi) + This function doesn't store the full ``G`` matrix in memory. Parameters ---------- - receivers : (n_receivers, 3) array + receivers : (n_receivers, 3) numpy.ndarray Array with the locations of the receivers - nodes : (n_active_nodes, 3) array + nodes : (n_active_nodes, 3) numpy.ndarray Array with the location of the mesh nodes. - sensitivity_matrix : array - Empty 2d array where the sensitivity matrix elements will be filled. - This could be a preallocated empty array or a slice of it. - The array should have a shape of ``(n_receivers, n_active_nodes)`` - if ``scalar_model`` is True. - The array should have a shape of ``(n_receivers, 3 * n_active_nodes)`` - if ``scalar_model`` is False. - cell_nodes : (n_active_cells, 8) array + cell_nodes : (n_active_cells, 8) numpy.ndarray Array of integers, where each row contains the indices of the nodes for each active cell in the mesh. regional_field : (3,) array Array containing the x, y and z components of the regional magnetic field (uniform background field). + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz : callables + Kernel functions used for computing the desired TMI derivative. constant_factor : float Constant factor that will be used to multiply each element of the sensitivity matrix. scalar_model : bool - If True, the sensitivity matrix is build to work with scalar models + If True, the sensitivity matrix is built to work with scalar models (susceptibilities). - If False, the sensitivity matrix is build to work with vector models + If False, the sensitivity matrix is built to work with vector models (effective susceptibilities). + weights : (n_receivers,) numpy.ndarray + Array with data weights. It should be the diagonal of the ``W`` matrix, + squared. + diagonal : (n_active_cells) or (3 * n_active_cells) numpy.ndarray + Array where the diagonal of ``G.T @ G`` will be added to. Notes ----- - - About the model array - ^^^^^^^^^^^^^^^^^^^^^ - - The ``model`` must always be a 1d array: - - * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with - the same number of elements as active cells in the mesh. It should store - the magnetic susceptibilities of each active cell in SI units. - * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array - with a number of elements equal to three times the active cells in the - mesh. It should store the components of the magnetization vector of each - active cell in :math:`Am^{-1}`. The order in which the components should - be passed are: - * every _easting_ component of each active cell, - * then every _northing_ component of each active cell, - * and finally every _upward_ component of each active cell. - - About the sensitivity matrix - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - Each row of the sensitivity matrix corresponds to a single receiver - location. - - If ``scalar_model`` is True, then each element of the row will - correspond to the partial derivative of the tmi - with respect to the susceptibility of each cell in the mesh. - - If ``scalar_model`` is False, then each row can be split in three sections - containing: - - * the partial derivatives of the tmi with respect - to the _x_ component of the effective susceptibility of each cell; then - * the partial derivatives of the tmi with respect - to the _y_ component of the effective susceptibility of each cell; and then - * the partial derivatives of the tmi with respect - to the _z_ component of the effective susceptibility of each cell. - - So, if we call :math:`T_j` the tmi on the receiver - :math:`j`, and :math:`\bar{\chi}^{(i)} = (\chi_x^{(i)}, \chi_y^{(i)}, - \chi_z^{(i)})` the effective susceptibility of the active cell :math:`i`, - then each row of the sensitivity matrix will be: - - .. math:: - - \left[ - \frac{\partial T_j}{\partial \chi_x^{(1)}}, - \dots, - \frac{\partial T_j}{\partial \chi_x^{(N)}}, - \frac{\partial T_j}{\partial \chi_y^{(1)}}, - \dots, - \frac{\partial T_j}{\partial \chi_y^{(N)}}, - \frac{\partial T_j}{\partial \chi_z^{(1)}}, - \dots, - \frac{\partial T_j}{\partial \chi_z^{(N)}} - \right] - - where :math:`N` is the total number of active cells. + This function is meant to be run in serial. Use the + ``_diagonal_G_T_dot_G_tmi_deriv_parallel`` one for parallelized computations. """ n_receivers = receivers.shape[0] n_nodes = nodes.shape[0] @@ -330,12 +1759,12 @@ def _sensitivity_tmi( dy = nodes[j, 1] - receivers[i, 1] dz = nodes[j, 2] - receivers[i, 2] distance = np.sqrt(dx**2 + dy**2 + dz**2) - kxx[j] = choclo.prism.kernel_ee(dx, dy, dz, distance) - kyy[j] = choclo.prism.kernel_nn(dx, dy, dz, distance) - kzz[j] = choclo.prism.kernel_uu(dx, dy, dz, distance) - kxy[j] = choclo.prism.kernel_en(dx, dy, dz, distance) - kxz[j] = choclo.prism.kernel_eu(dx, dy, dz, distance) - kyz[j] = choclo.prism.kernel_nu(dx, dy, dz, distance) + kxx[j] = kernel_xx(dx, dy, dz, distance) + kyy[j] = kernel_yy(dx, dy, dz, distance) + kzz[j] = kernel_zz(dx, dy, dz, distance) + kxy[j] = kernel_xy(dx, dy, dz, distance) + kxz[j] = kernel_xz(dx, dy, dz, distance) + kyz[j] = kernel_yz(dx, dy, dz, distance) # Compute sensitivity matrix elements from the kernel values for k in range(n_cells): nodes_indices = cell_nodes[k, :] @@ -348,30 +1777,25 @@ def _sensitivity_tmi( bx = uxx * fx + uxy * fy + uxz * fz by = uxy * fx + uyy * fy + uyz * fz bz = uxz * fx + uyz * fy + uzz * fz - # Fill the sensitivity matrix element(s) that correspond to the - # current active cell + if scalar_model: - sensitivity_matrix[i, k] = ( + g_element = ( constant_factor * regional_field_amplitude * (bx * fx + by * fy + bz * fz) ) + diagonal[k] += weights[i] * g_element**2 else: - sensitivity_matrix[i, k] = ( - constant_factor * regional_field_amplitude * bx - ) - sensitivity_matrix[i, k + n_cells] = ( - constant_factor * regional_field_amplitude * by - ) - sensitivity_matrix[i, k + 2 * n_cells] = ( - constant_factor * regional_field_amplitude * bz - ) + const = constant_factor * regional_field_amplitude + diagonal[k] += weights[i] * (const * bx) ** 2 + diagonal[k + n_cells] += weights[i] * (const * by) ** 2 + diagonal[k + 2 * n_cells] += weights[i] * (const * bz) ** 2 -def _sensitivity_tmi_derivative( +@jit(nopython=True, parallel=True) +def _diagonal_G_T_dot_G_tmi_deriv_parallel( receivers, nodes, - sensitivity_matrix, cell_nodes, regional_field, kernel_xx, @@ -382,32 +1806,21 @@ def _sensitivity_tmi_derivative( kernel_yz, constant_factor, scalar_model, + weights, + diagonal, ): - r""" - Fill the sensitivity matrix for a TMI derivative. - - This function should be used with a `numba.jit` decorator, for example: - - .. code:: - - from numba import jit + """ + Diagonal of ``G.T @ W.T @ W @ G`` for TMI derivatives, in parallel. - jit_sens = jit(nopython=True, parallel=True)(_sensitivity_tmi_derivative) + This function doesn't store the full ``G`` matrix in memory. Parameters ---------- - receivers : (n_receivers, 3) array + receivers : (n_receivers, 3) numpy.ndarray Array with the locations of the receivers - nodes : (n_active_nodes, 3) array + nodes : (n_active_nodes, 3) numpy.ndarray Array with the location of the mesh nodes. - sensitivity_matrix : array - Empty 2d array where the sensitivity matrix elements will be filled. - This could be a preallocated empty array or a slice of it. - The array should have a shape of ``(n_receivers, n_active_nodes)`` - if ``scalar_model`` is True. - The array should have a shape of ``(n_receivers, 3 * n_active_nodes)`` - if ``scalar_model`` is False. - cell_nodes : (n_active_cells, 8) array + cell_nodes : (n_active_cells, 8) numpy.ndarray Array of integers, where each row contains the indices of the nodes for each active cell in the mesh. regional_field : (3,) array @@ -419,85 +1832,25 @@ def _sensitivity_tmi_derivative( Constant factor that will be used to multiply each element of the sensitivity matrix. scalar_model : bool - If True, the sensitivity matrix is build to work with scalar models + If True, the sensitivity matrix is built to work with scalar models (susceptibilities). - If False, the sensitivity matrix is build to work with vector models + If False, the sensitivity matrix is built to work with vector models (effective susceptibilities). + weights : (n_receivers,) numpy.ndarray + Array with data weights. It should be the diagonal of the ``W`` matrix, + squared. + diagonal : (n_active_cells) or (3 * n_active_cells) numpy.ndarray + Array where the diagonal of ``G.T @ G`` will be added to. Notes ----- - - About the kernel functions - ^^^^^^^^^^^^^^^^^^^^^^^^^^ - - To compute the :math:`\alpha` derivative of the TMI :math:`\Delta T` (with - :math:`\alpha \in \{x, y, z\}` we need to evaluate third order kernels - functions for the prism. The kernels we need to evaluate can be obtained by - fixing one of the subindices to the direction of the derivative - (:math:`\alpha`) and cycle through combinations of the other two. - - For ``tmi_x`` we need to pass: - - .. code:: - - kernel_xx=kernel_eee, kernel_yy=kernel_enn, kernel_zz=kernel_euu, - kernel_xy=kernel_een, kernel_xz=kernel_eeu, kernel_yz=kernel_enu - - For ``tmi_y`` we need to pass: - - .. code:: - - kernel_xx=kernel_een, kernel_yy=kernel_nnn, kernel_zz=kernel_nuu, - kernel_xy=kernel_enn, kernel_xz=kernel_enu, kernel_yz=kernel_nnu - - For ``tmi_z`` we need to pass: - - .. code:: - - kernel_xx=kernel_eeu, kernel_yy=kernel_nnu, kernel_zz=kernel_uuu, - kernel_xy=kernel_enu, kernel_xz=kernel_euu, kernel_yz=kernel_nuu - - - About the model array - ^^^^^^^^^^^^^^^^^^^^^ - - The ``model`` must always be a 1d array: - - * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with - the same number of elements as active cells in the mesh. It should store - the magnetic susceptibilities of each active cell in SI units. - * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array - with a number of elements equal to three times the active cells in the - mesh. It should store the components of the magnetization vector of each - active cell in :math:`Am^{-1}`. The order in which the components should - be passed are: - * every _easting_ component of each active cell, - * then every _northing_ component of each active cell, - * and finally every _upward_ component of each active cell. - - About the sensitivity matrix - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - Each row of the sensitivity matrix corresponds to a single receiver - location. - - If ``scalar_model`` is True, then each element of the row will - correspond to the partial derivative of the tmi derivative (spatial) - with respect to the susceptibility of each cell in the mesh. - - If ``scalar_model`` is False, then each row can be split in three sections - containing: - - * the partial derivatives of the tmi derivative with respect - to the _x_ component of the effective susceptibility of each cell; then - * the partial derivatives of the tmi derivative with respect - to the _y_ component of the effective susceptibility of each cell; and then - * the partial derivatives of the tmi derivative with respect - to the _z_ component of the effective susceptibility of each cell. + This function is meant to be run in parallel. Use the + ``_diagonal_G_T_dot_G_tmi_serial`` one for serialized computations. """ n_receivers = receivers.shape[0] n_nodes = nodes.shape[0] n_cells = cell_nodes.shape[0] + diagonal_size = diagonal.size fx, fy, fz = regional_field regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) fx /= regional_field_amplitude @@ -508,6 +1861,8 @@ def _sensitivity_tmi_derivative( # Allocate vectors for kernels evaluated on mesh nodes kxx, kyy, kzz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) kxy, kxz, kyz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate array for the diagonal elements for the current receiver. + local_diagonal = np.empty(diagonal_size) # Allocate small vector for the nodes indices for a given cell nodes_indices = np.empty(8, dtype=cell_nodes.dtype) for j in range(n_nodes): @@ -533,24 +1888,26 @@ def _sensitivity_tmi_derivative( bx = uxx * fx + uxy * fy + uxz * fz by = uxy * fx + uyy * fy + uyz * fz bz = uxz * fx + uyz * fy + uzz * fz - # Fill the sensitivity matrix element(s) that correspond to the - # current active cell + if scalar_model: - sensitivity_matrix[i, k] = ( + g_element = ( constant_factor * regional_field_amplitude * (bx * fx + by * fy + bz * fz) ) + local_diagonal[k] = weights[i] * g_element**2 else: - sensitivity_matrix[i, k] = ( - constant_factor * regional_field_amplitude * bx - ) - sensitivity_matrix[i, k + n_cells] = ( - constant_factor * regional_field_amplitude * by - ) - sensitivity_matrix[i, k + 2 * n_cells] = ( - constant_factor * regional_field_amplitude * bz - ) + const = constant_factor * regional_field_amplitude + local_diagonal[k] = weights[i] * (const * bx) ** 2 + local_diagonal[k + n_cells] = weights[i] * (const * by) ** 2 + local_diagonal[k + 2 * n_cells] = weights[i] * (const * bz) ** 2 + + # Add the result to the diagonal. + # Apply reduction operation to add the values of the local diagonal to + # the running diagonal array. Avoid slicing the `diagonal` array when + # updating it to avoid racing conditions, just add the `local_diagonal` + # to the `diagonal` variable. + diagonal += local_diagonal def _forward_mag( @@ -1463,9 +2820,13 @@ def _sensitivity_mag_2d_mesh( Array with the top boundaries of each active cell in the 2D mesh. bottom : (n_active_cells) np.ndarray Array with the bottom boundaries of each active cell in the 2D mesh. - sensitivity_matrix : (n_receivers, n_active_nodes) array + sensitivity_matrix : array Empty 2d array where the sensitivity matrix elements will be filled. This could be a preallocated empty array or a slice of it. + The array should have a shape of ``(n_receivers, n_active_cells)`` + if ``scalar_model`` is True. + The array should have a shape of ``(n_receivers, 3 * n_active_cells)`` + if ``scalar_model`` is False. regional_field : (3,) array Array containing the x, y and z components of the regional magnetic field (uniform background field). @@ -1651,9 +3012,9 @@ def _sensitivity_tmi_2d_mesh( sensitivity_matrix : array Empty 2d array where the sensitivity matrix elements will be filled. This could be a preallocated empty array or a slice of it. - The array should have a shape of ``(n_receivers, n_active_nodes)`` + The array should have a shape of ``(n_receivers, n_active_cells)`` if ``scalar_model`` is True. - The array should have a shape of ``(n_receivers, 3 * n_active_nodes)`` + The array should have a shape of ``(n_receivers, 3 * n_active_cells)`` if ``scalar_model`` is False. regional_field : (3,) array Array containing the x, y and z components of the regional magnetic @@ -1833,9 +3194,9 @@ def _sensitivity_tmi_derivative_2d_mesh( sensitivity_matrix : array Empty 2d array where the sensitivity matrix elements will be filled. This could be a preallocated empty array or a slice of it. - The array should have a shape of ``(n_receivers, n_active_nodes)`` + The array should have a shape of ``(n_receivers, n_active_cells)`` if ``scalar_model`` is True. - The array should have a shape of ``(n_receivers, 3 * n_active_nodes)`` + The array should have a shape of ``(n_receivers, 3 * n_active_cells)`` if ``scalar_model`` is False. regional_field : (3,) array Array containing the x, y and z components of the regional magnetic diff --git a/simpeg/potential_fields/magnetics/simulation.py b/simpeg/potential_fields/magnetics/simulation.py index c103fc9cfa..2f3314dc3c 100644 --- a/simpeg/potential_fields/magnetics/simulation.py +++ b/simpeg/potential_fields/magnetics/simulation.py @@ -1,5 +1,7 @@ +import hashlib import warnings import numpy as np +from numpy.typing import NDArray import scipy.sparse as sp from geoana.kernels import ( prism_fxxy, @@ -11,6 +13,7 @@ prism_fzzz, ) from scipy.constants import mu_0 +from scipy.sparse.linalg import LinearOperator, aslinearoperator from simpeg import props, utils from simpeg.utils import mat_utils, mkvc, sdiag @@ -48,6 +51,18 @@ _sensitivity_tmi_derivative_serial, _sensitivity_tmi_derivative_2d_mesh_serial, _sensitivity_tmi_derivative_2d_mesh_parallel, + _mag_sensitivity_t_dot_v_serial, + _mag_sensitivity_t_dot_v_parallel, + _tmi_sensitivity_t_dot_v_serial, + _tmi_sensitivity_t_dot_v_parallel, + _tmi_derivative_sensitivity_t_dot_v_serial, + _tmi_derivative_sensitivity_t_dot_v_parallel, + _diagonal_G_T_dot_G_mag_serial, + _diagonal_G_T_dot_G_mag_parallel, + _diagonal_G_T_dot_G_tmi_serial, + _diagonal_G_T_dot_G_tmi_parallel, + _diagonal_G_T_dot_G_tmi_deriv_serial, + _diagonal_G_T_dot_G_tmi_deriv_parallel, ) if choclo is not None: @@ -205,9 +220,7 @@ def __init__( self.chi = chi self.chiMap = chiMap - self._G = None self._M = None - self._gtg_diagonal = None self.is_amplitude_data = is_amplitude_data self.modelMap = self.chiMap @@ -229,6 +242,16 @@ def __init__( self._forward_mag = _forward_mag_parallel self._forward_tmi_derivative = _forward_tmi_derivative_parallel self._sensitivity_tmi_derivative = _sensitivity_tmi_derivative_parallel + self._mag_sensitivity_t_dot_v = _mag_sensitivity_t_dot_v_parallel + self._tmi_sensitivity_t_dot_v = _tmi_sensitivity_t_dot_v_parallel + self._tmi_derivative_sensitivity_t_dot_v = ( + _tmi_derivative_sensitivity_t_dot_v_parallel + ) + self._diagonal_G_T_dot_G_mag = _diagonal_G_T_dot_G_mag_parallel + self._diagonal_G_T_dot_G_tmi = _diagonal_G_T_dot_G_tmi_parallel + self._diagonal_G_T_dot_G_tmi_deriv = ( + _diagonal_G_T_dot_G_tmi_deriv_parallel + ) else: self._sensitivity_tmi = _sensitivity_tmi_serial self._sensitivity_mag = _sensitivity_mag_serial @@ -236,6 +259,16 @@ def __init__( self._forward_mag = _forward_mag_serial self._forward_tmi_derivative = _forward_tmi_derivative_serial self._sensitivity_tmi_derivative = _sensitivity_tmi_derivative_serial + self._mag_sensitivity_t_dot_v = _mag_sensitivity_t_dot_v_serial + self._tmi_sensitivity_t_dot_v = _tmi_sensitivity_t_dot_v_serial + self._tmi_derivative_sensitivity_t_dot_v = ( + _tmi_derivative_sensitivity_t_dot_v_serial + ) + self._diagonal_G_T_dot_G_mag = _diagonal_G_T_dot_G_mag_serial + self._diagonal_G_T_dot_G_tmi = _diagonal_G_T_dot_G_tmi_serial + self._diagonal_G_T_dot_G_tmi_deriv = ( + _diagonal_G_T_dot_G_tmi_deriv_serial + ) @property def model_type(self): @@ -306,13 +339,24 @@ def fields(self, model): return fields @property - def G(self): - if getattr(self, "_G", None) is None: - if self.engine == "choclo": - self._G = self._sensitivity_matrix() - else: - self._G = self.linear_operator() - + def G(self) -> NDArray | np.memmap | LinearOperator: + if not hasattr(self, "_G"): + match self.engine, self.store_sensitivities: + case ("choclo", "forward_only"): + self._G = self._sensitivity_matrix_as_operator() + case ("choclo", _): + self._G = self._sensitivity_matrix() + case ("geoana", "forward_only"): + msg = ( + "Accessing matrix G with " + 'store_sensitivities="forward_only" and engine="geoana" ' + "hasn't been implemented yet." + 'Choose store_sensitivities="ram" or "disk", ' + 'or another engine, like "choclo".' + ) + raise NotImplementedError(msg) + case ("geoana", _): + self._G = self.linear_operator() return self._G modelType = deprecate_property( @@ -338,42 +382,195 @@ def tmi_projection(self): return self._tmi_projection - def getJtJdiag(self, m, W=None, f=None): + def getJ(self, m, f=None) -> NDArray[np.float64 | np.float32] | LinearOperator: + r""" + Sensitivity matrix :math:`\mathbf{J}`. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. + f : Ignored + Not used, present here for API consistency by convention. + + Returns + ------- + (nD, n_params) np.ndarray or scipy.sparse.linalg.LinearOperator. + Array or :class:`~scipy.sparse.linalg.LinearOperator` for the + :math:`\mathbf{J}` matrix. + A :class:`~scipy.sparse.linalg.LinearOperator` will be returned if + ``store_sensitivities`` is ``"forward_only"``, otherwise a dense + array will be returned. + + Notes + ----- + If ``store_sensitivities`` is ``"ram"`` or ``"disk"``, a dense array + for the ``J`` matrix is returned. + A :class:`~scipy.sparse.linalg.LinearOperator` is returned if + ``store_sensitivities`` is ``"forward_only"``. This object can perform + operations like ``J @ m`` or ``J.T @ v`` without allocating the full + ``J`` matrix in memory. """ - Return the diagonal of JtJ + if self.is_amplitude_data: + msg = ( + "The `getJ` method is not yet implemented to work with " + "`is_amplitude_data`." + ) + raise NotImplementedError(msg) + + # Need to assign the model, so the chiDeriv can be computed (if the + # model is None, the chiDeriv is going to be Zero). + self.model = m + chiDeriv = ( + self.chiDeriv + if not isinstance(self.G, LinearOperator) + else aslinearoperator(self.chiDeriv) + ) + return self.G @ chiDeriv + + def getJtJdiag(self, m, W=None, f=None): + r""" + Compute diagonal of :math:`\mathbf{J}^T \mathbf{J}``. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. + W : (nD, nD) np.ndarray or scipy.sparse.sparray, optional + Diagonal matrix with the square root of the weights. If not None, + the function returns the diagonal of + :math:`\mathbf{J}^T \mathbf{W}^T \mathbf{W} \mathbf{J}``. + f : Ignored + Not used, present here for API consistency by convention. + + Returns + ------- + (nparam) np.ndarray + Array with the diagonal of ``J.T @ J``. + + Notes + ----- + If ``store_sensitivities`` is ``"forward_only"``, the ``G`` matrix is + never allocated in memory, and the diagonal is obtained by + accumulation, computing each element of the ``G`` matrix on the fly. + + This method caches the diagonal ``G.T @ W.T @ W @ G`` and the sha256 + hash of the diagonal of the ``W`` matrix. This way, if same weights are + passed to it, it reuses the cached diagonal so it doesn't need to be + recomputed. + If new weights are passed, the cache is updated with the latest + diagonal of ``G.T @ W.T @ W @ G``. """ + # Need to assign the model, so the chiDeriv can be computed (if the + # model is None, the chiDeriv is going to be Zero). self.model = m - if W is None: - W = np.ones(self.survey.nD) - else: - W = W.diagonal() ** 2 - if getattr(self, "_gtg_diagonal", None) is None: - diag = np.zeros(self.G.shape[1]) - if not self.is_amplitude_data: - for i in range(len(W)): - diag += W[i] * (self.G[i] * self.G[i]) - else: + # We should probably check that W is diagonal. Let's assume it for now. + weights = ( + W.diagonal() ** 2 + if W is not None + else np.ones(self.survey.nD, dtype=np.float64) + ) + + # Compute gtg (G.T @ W.T @ W @ G) if it's not cached, or if the + # weights are not the same. + weights_sha256 = hashlib.sha256(weights) + use_cached_gtg = ( + hasattr(self, "_gtg_diagonal") + and hasattr(self, "_weights_sha256") + and self._weights_sha256.digest() == weights_sha256.digest() + ) + if not use_cached_gtg: + self._gtg_diagonal = self._get_gtg_diagonal(weights) + self._weights_sha256 = weights_sha256 + + # Multiply the gtg_diagonal by the derivative of the mapping + diagonal = mkvc( + (sdiag(np.sqrt(self._gtg_diagonal)) @ self.chiDeriv).power(2).sum(axis=0) + ) + return diagonal + + def _get_gtg_diagonal(self, weights: NDArray) -> NDArray: + """ + Compute the diagonal of ``G.T @ W.T @ W @ G``. + + Parameters + ---------- + weights : np.ndarray + Weights array: diagonal of ``W.T @ W``. + + Returns + ------- + np.ndarray + """ + match (self.engine, self.store_sensitivities, self.is_amplitude_data): + case ("geoana", "forward_only", _): + msg = ( + "Computing the diagonal of `G.T @ G` using " + "`'forward_only'` and `'geoana'` as engine hasn't been " + "implemented yet." + ) + raise NotImplementedError(msg) + case ("choclo", "forward_only", True): + msg = ( + "Computing the diagonal of `G.T @ G` using " + "`'forward_only'` and `is_amplitude_data` hasn't been " + "implemented yet." + ) + raise NotImplementedError(msg) + case ("choclo", "forward_only", False): + gtg_diagonal = self._gtg_diagonal_without_building_g(weights) + case (_, _, False): + # In Einstein notation, the j-th element of the diagonal is: + # d_j = w_i * G_{ij} * G_{ij} + gtg_diagonal = np.asarray( + np.einsum("i,ij,ij->j", weights, self.G, self.G) + ) + case (_, _, True): ampDeriv = self.ampDeriv Gx = self.G[::3] Gy = self.G[1::3] Gz = self.G[2::3] - for i in range(len(W)): + gtg_diagonal = np.zeros(self.G.shape[1]) + for i in range(weights.size): row = ( ampDeriv[0, i] * Gx[i] + ampDeriv[1, i] * Gy[i] + ampDeriv[2, i] * Gz[i] ) - diag += W[i] * (row * row) - self._gtg_diagonal = diag - else: - diag = self._gtg_diagonal - return mkvc((sdiag(np.sqrt(diag)) @ self.chiDeriv).power(2).sum(axis=0)) + gtg_diagonal += weights[i] * (row * row) + return gtg_diagonal def Jvec(self, m, v, f=None): + """ + Dot product between sensitivity matrix and a vector. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. This array is used to compute the ``J`` + matrix. + v : (n_param,) numpy.ndarray + Vector used in the matrix-vector multiplication. + f : Ignored + Not used, present here for API consistency by convention. + + Returns + ------- + (nD,) numpy.ndarray + + Notes + ----- + If ``store_sensitivities`` is set to ``"forward_only"``, then the + matrix `G` is never fully constructed, and the dot product is computed + by accumulation, computing the matrix elements on the fly. Otherwise, + the full matrix ``G`` is constructed and stored either in memory or + disk. + """ + # Need to assign the model, so the chiDeriv can be computed (if the + # model is None, the chiDeriv is going to be Zero). self.model = m dmu_dm_v = self.chiDeriv @ v - Jvec = self.G @ dmu_dm_v.astype(self.sensitivity_dtype, copy=False) if self.is_amplitude_data: @@ -381,10 +578,37 @@ def Jvec(self, m, v, f=None): Jvec = Jvec.reshape((-1, 3)).T # reshape((3, -1), order="F") ampDeriv_Jvec = self.ampDeriv * Jvec return ampDeriv_Jvec[0] + ampDeriv_Jvec[1] + ampDeriv_Jvec[2] - else: - return Jvec + + return Jvec def Jtvec(self, m, v, f=None): + """ + Dot product between transposed sensitivity matrix and a vector. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. This array is used to compute the ``J`` + matrix. + v : (nD,) numpy.ndarray + Vector used in the matrix-vector multiplication. + f : Ignored + Not used, present here for API consistency by convention. + + Returns + ------- + (nD,) numpy.ndarray + + Notes + ----- + If ``store_sensitivities`` is set to ``"forward_only"``, then the + matrix `G` is never fully constructed, and the dot product is computed + by accumulation, computing the matrix elements on the fly. Otherwise, + the full matrix ``G`` is constructed and stored either in memory or + disk. + """ + # Need to assign the model, so the chiDeriv can be computed (if the + # model is None, the chiDeriv is going to be Zero). self.model = m if self.is_amplitude_data: @@ -793,10 +1017,8 @@ def _sensitivity_matrix(self): # Get regional field regional_field = self.survey.source_field.b0 # Allocate sensitivity matrix - if self.model_type == "scalar": - n_columns = self.nC - else: - n_columns = 3 * self.nC + scalar_model = self.model_type == "scalar" + n_columns = self.nC if scalar_model else 3 * self.nC shape = (self.survey.nD, n_columns) if self.store_sensitivities == "disk": sensitivity_matrix = np.memmap( @@ -812,7 +1034,6 @@ def _sensitivity_matrix(self): constant_factor = 1 / 4 / np.pi # Start filling the sensitivity matrix index_offset = 0 - scalar_model = self.model_type == "scalar" for components, receivers in self._get_components_and_receivers(): if not CHOCLO_SUPPORTED_COMPONENTS.issuperset(components): raise NotImplementedError( @@ -871,6 +1092,194 @@ def _sensitivity_matrix(self): index_offset += n_rows return sensitivity_matrix + def _sensitivity_matrix_as_operator(self): + """ + Create a LinearOperator for the sensitivity matrix G. + + Returns + ------- + scipy.sparse.linalg.LinearOperator + """ + n_columns = self.nC if self.model_type == "scalar" else self.nC * 3 + shape = (self.survey.nD, n_columns) + linear_op = LinearOperator( + shape=shape, + matvec=self._forward, + rmatvec=self._sensitivity_matrix_transpose_dot_vec, + dtype=np.float64, + ) + return linear_op + + def _sensitivity_matrix_transpose_dot_vec(self, vector): + """ + Compute ``G.T @ v`` without building ``G``. + + Parameters + ---------- + vector : (nD) numpy.ndarray + Vector used in the dot product. + + Returns + ------- + (n_active_cells) or (3 * n_active_cells) numpy.ndarray + """ + # Gather active nodes and the indices of the nodes for each active cell + active_nodes, active_cell_nodes = self._get_active_nodes() + # Get regional field + regional_field = self.survey.source_field.b0 + + # Allocate resulting array. + scalar_model = self.model_type == "scalar" + result = np.zeros(self.nC if scalar_model else 3 * self.nC) + + # Define the constant factor + constant_factor = 1 / 4 / np.pi + + # Fill the result array + index_offset = 0 + for components, receivers in self._get_components_and_receivers(): + if not CHOCLO_SUPPORTED_COMPONENTS.issuperset(components): + raise NotImplementedError( + f"Other components besides {CHOCLO_SUPPORTED_COMPONENTS} " + "aren't implemented yet." + ) + n_components = len(components) + n_rows = n_components * receivers.shape[0] + for i, component in enumerate(components): + vector_slice = slice( + index_offset + i, index_offset + n_rows, n_components + ) + if component == "tmi": + self._tmi_sensitivity_t_dot_v( + receivers, + active_nodes, + active_cell_nodes, + regional_field, + constant_factor, + scalar_model, + vector[vector_slice], + result, + ) + elif component in ("tmi_x", "tmi_y", "tmi_z"): + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz = ( + CHOCLO_KERNELS[component] + ) + self._tmi_derivative_sensitivity_t_dot_v( + receivers, + active_nodes, + active_cell_nodes, + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + constant_factor, + scalar_model, + vector[vector_slice], + result, + ) + else: + kernel_x, kernel_y, kernel_z = CHOCLO_KERNELS[component] + self._mag_sensitivity_t_dot_v( + receivers, + active_nodes, + active_cell_nodes, + regional_field, + kernel_x, + kernel_y, + kernel_z, + constant_factor, + scalar_model, + vector[vector_slice], + result, + ) + index_offset += n_rows + return result + + def _gtg_diagonal_without_building_g(self, weights): + """ + Compute the diagonal of ``G.T @ G`` without building the ``G`` matrix. + + Parameters + ----------- + weights : (nD,) array + Array with data weights. It should be the diagonal of the ``W`` + matrix, squared. + + Returns + ------- + (n_active_cells) numpy.ndarray + """ + # Gather active nodes and the indices of the nodes for each active cell + active_nodes, active_cell_nodes = self._get_active_nodes() + # Get regional field + regional_field = self.survey.source_field.b0 + # Define the constant factor + constant_factor = 1 / 4 / np.pi + + # Allocate array for the diagonal + scalar_model = self.model_type == "scalar" + n_columns = self.nC if scalar_model else 3 * self.nC + diagonal = np.zeros(n_columns, dtype=np.float64) + + # Start filling the diagonal array + for components, receivers in self._get_components_and_receivers(): + if not CHOCLO_SUPPORTED_COMPONENTS.issuperset(components): + raise NotImplementedError( + f"Other components besides {CHOCLO_SUPPORTED_COMPONENTS} " + "aren't implemented yet." + ) + for component in components: + if component == "tmi": + self._diagonal_G_T_dot_G_tmi( + receivers, + active_nodes, + active_cell_nodes, + regional_field, + constant_factor, + scalar_model, + weights, + diagonal, + ) + elif component in ("tmi_x", "tmi_y", "tmi_z"): + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz = ( + CHOCLO_KERNELS[component] + ) + self._diagonal_G_T_dot_G_tmi_deriv( + receivers, + active_nodes, + active_cell_nodes, + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + constant_factor, + scalar_model, + weights, + diagonal, + ) + else: + kernel_x, kernel_y, kernel_z = CHOCLO_KERNELS[component] + self._diagonal_G_T_dot_G_mag( + receivers, + active_nodes, + active_cell_nodes, + regional_field, + kernel_x, + kernel_y, + kernel_z, + constant_factor, + scalar_model, + weights, + diagonal, + ) + return diagonal + class SimulationEquivalentSourceLayer( BaseEquivalentSourceLayerSimulation, Simulation3DIntegral @@ -1037,10 +1446,8 @@ def _sensitivity_matrix(self): # Get regional field regional_field = self.survey.source_field.b0 # Allocate sensitivity matrix - if self.model_type == "scalar": - n_columns = self.nC - else: - n_columns = 3 * self.nC + scalar_model = self.model_type == "scalar" + n_columns = self.nC if scalar_model else 3 * self.nC shape = (self.survey.nD, n_columns) if self.store_sensitivities == "disk": sensitivity_matrix = np.memmap( @@ -1054,7 +1461,6 @@ def _sensitivity_matrix(self): sensitivity_matrix = np.empty(shape, dtype=self.sensitivity_dtype) # Start filling the sensitivity matrix index_offset = 0 - scalar_model = self.model_type == "scalar" for components, receivers in self._get_components_and_receivers(): if not CHOCLO_SUPPORTED_COMPONENTS.issuperset(components): raise NotImplementedError( diff --git a/tests/pf/test_forward_Mag_Linear.py b/tests/pf/test_forward_Mag_Linear.py index e593c71872..9ddc45ac8a 100644 --- a/tests/pf/test_forward_Mag_Linear.py +++ b/tests/pf/test_forward_Mag_Linear.py @@ -5,8 +5,10 @@ import discretize import numpy as np import pytest +from scipy.sparse import diags from geoana.em.static import MagneticPrism from scipy.constants import mu_0 +from scipy.sparse.linalg import LinearOperator, aslinearoperator import simpeg from simpeg import maps, utils @@ -1034,3 +1036,706 @@ def test_nD_amplitude_data( assert simulation.nD == n_data dpred = simulation.dpred(susceptibilities) assert dpred.size == simulation.nD + + +@pytest.mark.parametrize( + "scalar_model", [True, False], ids=["scalar_model", "vector_model"] +) +class TestGLinearOperator(BaseFixtures): + """ + Test G as a linear operator. + """ + + @pytest.fixture + def mapping(self, mesh, scalar_model): + nparams = mesh.n_cells if scalar_model else 3 * mesh.n_cells + return maps.IdentityMap(nP=nparams) + + @pytest.mark.parametrize("parallel", [True, False], ids=["parallel", "serial"]) + def test_G_dot_m( + self, survey, mesh, mapping, susceptibilities, scalar_model, parallel + ): + """Test G @ m.""" + model_type = "scalar" if scalar_model else "vector" + simulation, simulation_ram = ( + mag.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + chiMap=mapping, + store_sensitivities=store, + engine="choclo", + numba_parallel=parallel, + model_type=model_type, + ) + for store in ("forward_only", "ram") + ) + assert isinstance(simulation.G, LinearOperator) + assert isinstance(simulation_ram.G, np.ndarray) + + expected = simulation_ram.G @ susceptibilities + + atol = np.max(np.abs(expected)) * 1e-8 + np.testing.assert_allclose(simulation.G @ susceptibilities, expected, atol=atol) + + @pytest.mark.parametrize("parallel", [True, False], ids=["parallel", "serial"]) + def test_G_t_dot_v(self, survey, mesh, mapping, scalar_model, parallel): + """Test G.T @ v.""" + model_type = "scalar" if scalar_model else "vector" + simulation, simulation_ram = ( + mag.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + chiMap=mapping, + store_sensitivities=store, + engine="choclo", + numba_parallel=parallel, + model_type=model_type, + ) + for store in ("forward_only", "ram") + ) + assert isinstance(simulation.G, LinearOperator) + assert isinstance(simulation_ram.G, np.ndarray) + + vector = np.random.default_rng(seed=42).uniform(size=survey.nD) + expected = simulation_ram.G.T @ vector + + atol = np.max(np.abs(expected)) * 1e-7 + np.testing.assert_allclose(simulation.G.T @ vector, expected, atol=atol) + + def test_not_implemented(self, survey, mesh, mapping, scalar_model): + """ + Test NotImplementedError when forward_only and geoana as engine. + """ + engine, store = "geoana", "forward_only" + model_type = "scalar" if scalar_model else "vector" + simulation = mag.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + chiMap=mapping, + store_sensitivities=store, + engine=engine, + model_type=model_type, + ) + msg = re.escape( + "Accessing matrix G with " + 'store_sensitivities="forward_only" and engine="geoana" ' + "hasn't been implemented yet." + ) + with pytest.raises(NotImplementedError, match=msg): + simulation.G + + +@pytest.mark.parametrize( + "scalar_model", [True, False], ids=["scalar_model", "vector_model"] +) +class TestJacobian(BaseFixtures): + """ + Test methods related to Jacobian matrix in magnetic simulation. + """ + + atol_ratio = 1e-7 + + @pytest.fixture(params=["identity_map", "exp_map"]) + def mapping(self, mesh, scalar_model: bool, request): + nparams = mesh.n_cells if scalar_model else 3 * mesh.n_cells + mapping = ( + maps.IdentityMap(nP=nparams) + if request.param == "identity_map" + else maps.ExpMap(nP=nparams) + ) + return mapping + + @pytest.mark.parametrize("engine", ["choclo", "geoana"]) + def test_getJ_as_array( + self, survey, mesh, mapping, susceptibilities, scalar_model, engine + ): + """ + Test the getJ method when J is an array in memory. + """ + model_type = "scalar" if scalar_model else "vector" + simulation = mag.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + chiMap=mapping, + store_sensitivities="ram", + engine=engine, + model_type=model_type, + ) + is_identity_map = type(mapping) is maps.IdentityMap + model = susceptibilities if is_identity_map else np.log(susceptibilities) + + jac = simulation.getJ(model) + assert isinstance(jac, np.ndarray) + # With an identity mapping, the jacobian should be the same as G. + # With an exp mapping, the jacobian should be G @ the mapping derivative. + expected_jac = ( + simulation.G if is_identity_map else simulation.G @ mapping.deriv(model) + ) + np.testing.assert_allclose(jac, expected_jac) + + @pytest.mark.parametrize( + "engine", + [ + "choclo", + pytest.param( + "geoana", + marks=pytest.mark.xfail( + reason="not implemented", raises=NotImplementedError + ), + ), + ], + ) + @pytest.mark.parametrize("transpose", [False, True], ids=["J @ m", "J.T @ v"]) + def test_getJ_as_linear_operator( + self, survey, mesh, mapping, susceptibilities, scalar_model, engine, transpose + ): + """ + Test the getJ method when J is a linear operator. + """ + model_type = "scalar" if scalar_model else "vector" + simulation = mag.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + chiMap=mapping, + store_sensitivities="forward_only", + engine=engine, + model_type=model_type, + ) + is_identity_map = type(mapping) is maps.IdentityMap + model = susceptibilities if is_identity_map else np.log(susceptibilities) + + jac = simulation.getJ(model) + assert isinstance(jac, LinearOperator) + if transpose: + vector = np.random.default_rng(seed=42).uniform(size=survey.nD) + result = jac.T @ vector + expected_result = mapping.deriv(model).T @ (simulation.G.T @ vector) + else: + result = jac @ model + expected_result = simulation.G @ (mapping.deriv(model).diagonal() * model) + np.testing.assert_allclose(result, expected_result) + + @pytest.mark.parametrize("engine", ["choclo", "geoana"]) + def test_getJ_not_implemented( + self, survey, mesh, mapping, susceptibilities, scalar_model, engine + ): + model_type = "scalar" if scalar_model else "vector" + simulation = mag.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + chiMap=mapping, + store_sensitivities="ram", + engine=engine, + model_type=model_type, + is_amplitude_data=True, + ) + is_identity_map = type(mapping) is maps.IdentityMap + model = susceptibilities if is_identity_map else np.log(susceptibilities) + with pytest.raises(NotImplementedError): + simulation.getJ(model) + + @pytest.mark.parametrize( + ("engine", "store_sensitivities"), + [ + ("choclo", "ram"), + ("choclo", "forward_only"), + ("geoana", "ram"), + pytest.param( + "geoana", + "forward_only", + marks=pytest.mark.xfail(reason="not implemented"), + ), + ], + ) + def test_Jvec( + self, + survey, + mesh, + mapping, + susceptibilities, + scalar_model, + engine, + store_sensitivities, + ): + """ + Test the Jvec method. + """ + model_type = "scalar" if scalar_model else "vector" + simulation = mag.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + chiMap=mapping, + store_sensitivities=store_sensitivities, + engine=engine, + model_type=model_type, + sensitivity_dtype=np.float64, + ) + is_identity_map = type(mapping) is maps.IdentityMap + model = susceptibilities if is_identity_map else np.log(susceptibilities) + + vector = np.random.default_rng(seed=42).uniform(size=susceptibilities.size) + result = simulation.Jvec(model, vector) + + expected_jac = ( + simulation.G + if is_identity_map + else simulation.G @ aslinearoperator(mapping.deriv(model)) + ) + expected = expected_jac @ vector + + atol = np.max(np.abs(expected)) * self.atol_ratio + np.testing.assert_allclose(result, expected, atol=atol) + + @pytest.mark.parametrize( + ("engine", "store_sensitivities"), + [ + ("choclo", "ram"), + ("choclo", "forward_only"), + ("geoana", "ram"), + pytest.param( + "geoana", + "forward_only", + marks=pytest.mark.xfail(reason="not implemented"), + ), + ], + ) + def test_Jtvec( + self, + survey, + mesh, + mapping, + susceptibilities, + scalar_model, + engine, + store_sensitivities, + ): + """ + Test the Jtvec method. + """ + model_type = "scalar" if scalar_model else "vector" + simulation = mag.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + chiMap=mapping, + store_sensitivities=store_sensitivities, + engine=engine, + model_type=model_type, + sensitivity_dtype=np.float64, + ) + is_identity_map = type(mapping) is maps.IdentityMap + model = susceptibilities if is_identity_map else np.log(susceptibilities) + + vector = np.random.default_rng(seed=42).uniform(size=survey.nD) + result = simulation.Jtvec(model, vector) + + expected_jac = ( + simulation.G + if is_identity_map + else simulation.G @ aslinearoperator(mapping.deriv(model)) + ) + expected = expected_jac.T @ vector + + atol = np.max(np.abs(result)) * self.atol_ratio + np.testing.assert_allclose(result, expected, atol=atol) + + @pytest.mark.parametrize( + "engine", + [ + "choclo", + pytest.param("geoana", marks=pytest.mark.xfail(reason="not implemented")), + ], + ) + @pytest.mark.parametrize("method", ["Jvec", "Jtvec"]) + def test_array_vs_linear_operator( + self, survey, mesh, mapping, susceptibilities, scalar_model, engine, method + ): + """ + Test methods when using "ram" and "forward_only". + + They should give the same results. + """ + model_type = "scalar" if scalar_model else "vector" + simulation_lo, simulation_ram = ( + mag.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + chiMap=mapping, + store_sensitivities=store, + engine=engine, + model_type=model_type, + sensitivity_dtype=np.float64, + ) + for store in ("forward_only", "ram") + ) + match method: + case "Jvec": + vector_size = susceptibilities.size + case "Jtvec": + vector_size = survey.nD + case _: # pragma: no cover + raise ValueError(f"Invalid method '{method}'") # pragma: no cover + is_identity_map = type(mapping) is maps.IdentityMap + model = susceptibilities if is_identity_map else np.log(susceptibilities) + + vector = np.random.default_rng(seed=42).uniform(size=vector_size) + result_lo = getattr(simulation_lo, method)(model, vector) + result_ram = getattr(simulation_ram, method)(model, vector) + atol = np.max(np.abs(result_ram)) * self.atol_ratio + np.testing.assert_allclose(result_lo, result_ram, atol=atol) + + @pytest.mark.parametrize("engine", ["choclo", "geoana"]) + @pytest.mark.parametrize("weights", [True, False]) + def test_getJtJdiag( + self, survey, mesh, mapping, susceptibilities, scalar_model, engine, weights + ): + """ + Test the ``getJtJdiag`` method with G as an array in memory. + """ + model_type = "scalar" if scalar_model else "vector" + simulation = mag.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + chiMap=mapping, + store_sensitivities="ram", + engine=engine, + model_type=model_type, + sensitivity_dtype=np.float64, + ) + is_identity_map = type(mapping) is maps.IdentityMap + model = susceptibilities if is_identity_map else np.log(susceptibilities) + + kwargs = {} + if weights: + w_matrix = diags(np.random.default_rng(seed=42).uniform(size=survey.nD)) + kwargs = {"W": w_matrix} + jtj_diag = simulation.getJtJdiag(model, **kwargs) + + expected_jac = ( + simulation.G if is_identity_map else simulation.G @ mapping.deriv(model) + ) + if weights: + expected = np.diag(expected_jac.T @ w_matrix.T @ w_matrix @ expected_jac) + else: + expected = np.diag(expected_jac.T @ expected_jac) + + atol = np.max(np.abs(jtj_diag)) * self.atol_ratio + np.testing.assert_allclose(jtj_diag, expected, atol=atol) + + @pytest.mark.parametrize( + ("engine", "is_amplitude_data"), + [("geoana", True), ("geoana", False), ("choclo", True)], + ids=("geoana-amplitude_data", "geoana-regular_data", "choclo-amplitude_data"), + ) + def test_getJtJdiag_not_implemented( + self, + survey, + mesh, + mapping, + susceptibilities, + scalar_model, + engine, + is_amplitude_data, + ): + """ + Test NotImplementedErrors on ``getJtJdiag``. + """ + model_type = "scalar" if scalar_model else "vector" + simulation = mag.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + chiMap=mapping, + store_sensitivities="forward_only", + engine=engine, + is_amplitude_data=is_amplitude_data, + model_type=model_type, + ) + is_identity_map = type(mapping) is maps.IdentityMap + model = susceptibilities if is_identity_map else np.log(susceptibilities) + with pytest.raises(NotImplementedError): + simulation.getJtJdiag(model) + + @pytest.mark.parametrize("parallel", [True, False], ids=("parallel", "serial")) + def test_getJtJdiag_forward_only( + self, survey, mesh, mapping, susceptibilities, scalar_model, parallel + ): + """ + Test the ``getJtJdiag`` method with ``"forward_only"`` and choclo. + """ + model_type = "scalar" if scalar_model else "vector" + simulation, simulation_ram = ( + mag.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + chiMap=mapping, + store_sensitivities=store, + engine="choclo", + numba_parallel=parallel, + model_type=model_type, + ) + for store in ("forward_only", "ram") + ) + is_identity_map = type(mapping) is maps.IdentityMap + model = susceptibilities if is_identity_map else np.log(susceptibilities) + + expected = simulation_ram.getJtJdiag(model) + result = simulation.getJtJdiag(model) + + atol = np.max(np.abs(expected)) * 1e-8 + np.testing.assert_allclose(result, expected, atol=atol) + + @pytest.mark.parametrize("engine", ["choclo", "geoana"]) + def test_getJtJdiag_caching( + self, survey, mesh, mapping, susceptibilities, scalar_model, engine + ): + """ + Test the caching behaviour of the ``getJtJdiag`` method. + """ + model_type = "scalar" if scalar_model else "vector" + simulation = mag.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + chiMap=mapping, + store_sensitivities="ram", + engine=engine, + model_type=model_type, + ) + + # Get diagonal of J.T @ J without any weight + is_identity_map = type(mapping) is maps.IdentityMap + model = susceptibilities if is_identity_map else np.log(susceptibilities) + jtj_diagonal_1 = simulation.getJtJdiag(model) + assert hasattr(simulation, "_gtg_diagonal") + assert hasattr(simulation, "_weights_sha256") + gtg_diagonal_1 = simulation._gtg_diagonal + weights_sha256_1 = simulation._weights_sha256 + + # Compute it again and make sure we get the same result + np.testing.assert_allclose(jtj_diagonal_1, simulation.getJtJdiag(model)) + + # Get a new diagonal with weights + weights_matrix = diags( + np.random.default_rng(seed=42).uniform(size=simulation.survey.nD) + ) + jtj_diagonal_2 = simulation.getJtJdiag(model, W=weights_matrix) + assert hasattr(simulation, "_gtg_diagonal") + assert hasattr(simulation, "_weights_sha256") + gtg_diagonal_2 = simulation._gtg_diagonal + weights_sha256_2 = simulation._weights_sha256 + + # The two results should be different + assert not np.array_equal(jtj_diagonal_1, jtj_diagonal_2) + assert not np.array_equal(gtg_diagonal_1, gtg_diagonal_2) + assert weights_sha256_1.digest() != weights_sha256_2.digest() + + +@pytest.mark.parametrize( + "scalar_model", [True, False], ids=["scalar_model", "vector_model"] +) +class TestJacobianAmplitudeData(BaseFixtures): + """ + Test Jacobian related methods with ``is_amplitude_data``. + """ + + atol_ratio = 1e-7 + + @pytest.fixture + def survey(self): + """ + Sample survey with fixed components bx, by, bz. + + These components are assumed when working with ``is_amplitude_data=True``. + """ + # Observation points + x = np.linspace(-20.0, 20.0, 4) + x, y = np.meshgrid(x, x) + z = 5.0 * np.ones_like(x) + coordinates = np.vstack((x.ravel(), y.ravel(), z.ravel())).T + receivers = mag.receivers.Point(coordinates, components=["bx", "by", "bz"]) + source_field = mag.UniformBackgroundField( + receiver_list=[receivers], + amplitude=55_000, + inclination=12, + declination=-35, + ) + survey = mag.survey.Survey(source_field) + return survey + + @pytest.fixture(params=["identity_map", "exp_map"]) + def mapping(self, mesh, scalar_model: bool, request): + nparams = mesh.n_cells if scalar_model else 3 * mesh.n_cells + mapping = ( + maps.IdentityMap(nP=nparams) + if request.param == "identity_map" + else maps.ExpMap(nP=nparams) + ) + return mapping + + @pytest.mark.parametrize("engine", ["choclo", "geoana"]) + def test_getJ_not_implemented( + self, survey, mesh, mapping, susceptibilities, scalar_model, engine + ): + """ + Test the getJ method when J is an array in memory. + """ + model_type = "scalar" if scalar_model else "vector" + simulation = mag.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + chiMap=mapping, + store_sensitivities="ram", + engine=engine, + model_type=model_type, + is_amplitude_data=True, + sensitivity_dtype=np.float64, + ) + is_identity_map = type(mapping) is maps.IdentityMap + model = susceptibilities if is_identity_map else np.log(susceptibilities) + with pytest.raises(NotImplementedError): + simulation.getJ(model) + + @pytest.mark.parametrize( + ("engine", "store_sensitivities"), + [ + ("choclo", "ram"), + ("choclo", "forward_only"), + ("geoana", "ram"), + pytest.param( + "geoana", + "forward_only", + marks=pytest.mark.xfail(reason="not implemented"), + ), + ], + ) + def test_Jvec( + self, + survey, + mesh, + mapping, + susceptibilities, + scalar_model, + engine, + store_sensitivities, + ): + r""" + Test the Jvec method. + + Test the Jvec method through an alternative implementation. + Define a :math:`f(\chi)` forward model function that returns the norm of the + magnetic field given the susceptibility values of :math:`\chi`: + + .. math:: + + f(\chi) + = \lvert \mathbf{B} \rvert + = \sqrt{B_x^2(\chi) + B_y^2(\chi) + B_z^2(\chi)} + + The gradient of :math:`f(\chi)` (jacobian matrix :math:`\mathbf{J}`) can be + written as: + + .. math:: + + \mathbf{J} = \mathbf{J}_x + \mathbf{J}_y + \mathbf{J}_z + + where: + + .. math:: + + \mathbf{J}_x = + \frac{1}{\lvert \mathbf{B} \rvert} + B_x(\chi) + \frac{\partial B_x}{\partial \chi} + \frac{\partial \chi}{\partial \mathbf{m}} + + and :math:`\mathbf{m}` is the model. + """ + model_type = "scalar" if scalar_model else "vector" + simulation = mag.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + chiMap=mapping, + store_sensitivities=store_sensitivities, + engine=engine, + model_type=model_type, + is_amplitude_data=True, + sensitivity_dtype=np.float64, + ) + is_identity_map = type(mapping) is maps.IdentityMap + model = susceptibilities if is_identity_map else np.log(susceptibilities) + vector = np.random.default_rng(seed=42).uniform(size=susceptibilities.size) + result = simulation.Jvec(model, vector) + + magnetic_field = simulation.G @ susceptibilities + bx, by, bz = (magnetic_field[i::3] for i in (0, 1, 2)) + inv_amplitude = 1 / np.sqrt(bx**2 + by**2 + bz**2) + + g_dot_chideriv_v = ( + simulation.G @ aslinearoperator(mapping.deriv(model)) @ vector + ) + jac_x_dot_v = diags(inv_amplitude) @ diags(bx) @ g_dot_chideriv_v[0::3] + jac_y_dot_v = diags(inv_amplitude) @ diags(by) @ g_dot_chideriv_v[1::3] + jac_z_dot_v = diags(inv_amplitude) @ diags(bz) @ g_dot_chideriv_v[2::3] + expected = jac_x_dot_v + jac_y_dot_v + jac_z_dot_v + + atol = np.max(np.abs(expected)) * self.atol_ratio + np.testing.assert_allclose(result, expected, atol=atol) + + @pytest.mark.parametrize( + ("engine", "store_sensitivities"), + [ + ("choclo", "ram"), + ("choclo", "forward_only"), + ("geoana", "ram"), + pytest.param( + "geoana", + "forward_only", + marks=pytest.mark.xfail(reason="not implemented"), + ), + ], + ) + def test_Jtvec( + self, + survey, + mesh, + mapping, + susceptibilities, + scalar_model, + engine, + store_sensitivities, + ): + """ + Test the Jtvec method. + + Test it similarly to Jvec, but computing the transpose of the matrices. + """ + model_type = "scalar" if scalar_model else "vector" + simulation = mag.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + chiMap=mapping, + store_sensitivities=store_sensitivities, + engine=engine, + model_type=model_type, + is_amplitude_data=True, + sensitivity_dtype=np.float64, + ) + is_identity_map = type(mapping) is maps.IdentityMap + model = susceptibilities if is_identity_map else np.log(susceptibilities) + + # Need to set size as survey.nD / 3 because there's a bug in simulation.nD. + vector = np.random.default_rng(seed=42).uniform(size=survey.nD // 3) + result = simulation.Jtvec(model, vector) + + magnetic_field = simulation.G @ susceptibilities + bx, by, bz = (magnetic_field[i::3] for i in (0, 1, 2)) + inv_amplitude = 1 / np.sqrt(bx**2 + by**2 + bz**2) + v = np.array( + ( + bx * inv_amplitude * vector, + by * inv_amplitude * vector, + bz * inv_amplitude * vector, + ) + ).T.ravel() # interleave the values for bx, by, bz + expected = mapping.deriv(model).T @ (simulation.G.T @ v) + + atol = np.max(np.abs(result)) * self.atol_ratio + np.testing.assert_allclose(result, expected, atol=atol) From 8f5fe300f79f924c2b7b6562aa61060e6a4dd079 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 23 Apr 2025 17:14:09 -0700 Subject: [PATCH 132/194] Use Numpy's RNG in tests for depth weighting (#1570) Use Numpy's RNG and ditch `unittest` in tests for depth weighting. --- tests/base/test_model_utils.py | 36 +++++++++++++++------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/tests/base/test_model_utils.py b/tests/base/test_model_utils.py index 4cc34beaf1..c77d71fe6f 100644 --- a/tests/base/test_model_utils.py +++ b/tests/base/test_model_utils.py @@ -1,5 +1,3 @@ -import unittest - import numpy as np import pytest from discretize import TensorMesh @@ -7,7 +5,7 @@ from simpeg import utils -class DepthWeightingTest(unittest.TestCase): +class TestDepthWeighting: def test_depth_weighting_3D(self): # Mesh dh = 5.0 @@ -16,19 +14,20 @@ def test_depth_weighting_3D(self): hz = [(dh, 15)] mesh = TensorMesh([hx, hy, hz], "CCN") - actv = np.random.randint(0, 2, mesh.n_cells) == 1 + rng = np.random.default_rng(seed=42) + actv = rng.integers(low=0, high=2, size=mesh.n_cells, dtype=bool) - r_loc = 0.1 # Depth weighting + r_loc = 0.1 wz = utils.depth_weighting( mesh, r_loc, active_cells=actv, exponent=5, threshold=0 ) - reference_locs = ( - np.random.rand(1000, 3) * (mesh.nodes.max(axis=0) - mesh.nodes.min(axis=0)) - + mesh.origin + # Define reference locs at random locations + reference_locs = rng.uniform( + low=mesh.nodes.min(axis=0), high=mesh.nodes.max(axis=0), size=(1000, 3) ) - reference_locs[:, -1] = r_loc + reference_locs[:, -1] = r_loc # set them all at the same elevation wz2 = utils.depth_weighting( mesh, reference_locs, active_cells=actv, exponent=5, threshold=0 @@ -44,8 +43,8 @@ def test_depth_weighting_3D(self): np.testing.assert_allclose(wz, wz2) - with self.assertRaises(ValueError): - wz2 = utils.depth_weighting(mesh, np.random.rand(10, 3, 3)) + with pytest.raises(ValueError): + utils.depth_weighting(mesh, rng.random(size=(10, 3, 3))) def test_depth_weighting_2D(self): # Mesh @@ -54,7 +53,8 @@ def test_depth_weighting_2D(self): hz = [(dh, 15)] mesh = TensorMesh([hx, hz], "CN") - actv = np.random.randint(0, 2, mesh.n_cells) == 1 + rng = np.random.default_rng(seed=42) + actv = rng.integers(low=0, high=2, size=mesh.n_cells, dtype=bool) r_loc = 0.1 # Depth weighting @@ -62,11 +62,11 @@ def test_depth_weighting_2D(self): mesh, r_loc, active_cells=actv, exponent=5, threshold=0 ) - reference_locs = ( - np.random.rand(1000, 2) * (mesh.nodes.max(axis=0) - mesh.nodes.min(axis=0)) - + mesh.origin + # Define reference locs at random locations + reference_locs = rng.uniform( + low=mesh.nodes.min(axis=0), high=mesh.nodes.max(axis=0), size=(1000, 2) ) - reference_locs[:, -1] = r_loc + reference_locs[:, -1] = r_loc # set them all at the same elevation wz2 = utils.depth_weighting( mesh, reference_locs, active_cells=actv, exponent=5, threshold=0 @@ -195,7 +195,3 @@ def test_removed_indactive(mesh): msg = "'indActive' argument has been removed. " "Please use 'active_cells' instead." with pytest.raises(TypeError, match=msg): utils.depth_weighting(mesh, 0, indActive=active_cells) - - -if __name__ == "__main__": - unittest.main() From 725a09be14c2139e776981fa61df026b45b987d8 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 24 Apr 2025 10:50:51 -0700 Subject: [PATCH 133/194] Raise NotImplementedError on getJ for NSEM 1D simulations (#1653) Overwrite the `getJ` method of the NSEM 1D finite volume simulations so they raise a `NotImplementedError`. Add tests for the new errors. First step towards addressing #1541. --- .../natural_source/simulation.py | 34 +++++++++++++ .../nsem/forward/test_getJ_not_implemented.py | 50 +++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 tests/em/nsem/forward/test_getJ_not_implemented.py diff --git a/simpeg/electromagnetics/natural_source/simulation.py b/simpeg/electromagnetics/natural_source/simulation.py index 651a347957..a7cf938a2b 100644 --- a/simpeg/electromagnetics/natural_source/simulation.py +++ b/simpeg/electromagnetics/natural_source/simulation.py @@ -110,6 +110,23 @@ def getADeriv(self, freq, u, v, adjoint=False): freq, u, v, adjoint ) + def getJ(self, m, f=None): + r"""Generate the full sensitivity matrix. + + .. important:: + + This method hasn't been implemented yet for this class. + + Raises + ------- + NotImplementedError + """ + msg = ( + "The getJ method hasn't been implemented for the " + f"{type(self).__name__} yet." + ) + raise NotImplementedError(msg) + class Simulation1DMagneticField(BaseFDEMSimulation): """ @@ -172,6 +189,23 @@ def getADeriv(self, freq, u, v, adjoint=False): freq, u, v, adjoint ) + def getJ(self, m, f=None): + r"""Generate the full sensitivity matrix. + + .. important:: + + This method hasn't been implemented yet for this class. + + Raises + ------- + NotImplementedError + """ + msg = ( + "The getJ method hasn't been implemented for the " + f"{type(self).__name__} yet." + ) + raise NotImplementedError(msg) + class Simulation1DPrimarySecondary(Simulation1DElectricField): r""" diff --git a/tests/em/nsem/forward/test_getJ_not_implemented.py b/tests/em/nsem/forward/test_getJ_not_implemented.py new file mode 100644 index 0000000000..0da2a3ee95 --- /dev/null +++ b/tests/em/nsem/forward/test_getJ_not_implemented.py @@ -0,0 +1,50 @@ +""" +Test NotImplementedError on getJ for NSEM 1D finite volume simulations. +""" + +import pytest +import numpy as np +import discretize +from simpeg import maps +from simpeg.electromagnetics import natural_source as nsem + + +@pytest.fixture +def mesh(): + csz = 100 + nc = 300 + npad = 30 + pf = 1.2 + mesh = discretize.TensorMesh([[(csz, npad, -pf), (csz, nc), (csz, npad)]], "N") + mesh.x0 = np.r_[-mesh.h[0][:-npad].sum()] + return mesh + + +@pytest.fixture +def survey(): + frequencies = np.logspace(-2, 1, 30) + receiver = nsem.receivers.Impedance( + [[0]], orientation="xy", component="apparent_resistivity" + ) + sources = [nsem.sources.Planewave([receiver], frequency=f) for f in frequencies] + survey = nsem.survey.Survey(sources) + return survey + + +@pytest.mark.parametrize( + "simulation_class", [nsem.Simulation1DElectricField, nsem.Simulation1DMagneticField] +) +def test_getJ_not_implemented(mesh, survey, simulation_class): + """ + Test NotImplementedError on getJ for NSEM 1D simulations. + """ + mapping = maps.IdentityMap() + simulation = simulation_class( + mesh=mesh, + survey=survey, + sigmaMap=mapping, + ) + model = np.ones(survey.nD) + msg = "The getJ method hasn't been implemented" + with pytest.raises(NotImplementedError, match=msg): + simulation.getJ(model) From 24bf81e9343b63f7cb4ca48a34b5dda9c7709219 Mon Sep 17 00:00:00 2001 From: Lindsey Heagy Date: Thu, 24 Apr 2025 12:37:55 -0700 Subject: [PATCH 134/194] Set the model when calling `getJ` in DC and SIP simulations (#1361) Make sure we set the model when calling getJ. This will ensure that if the model is changed, the sensitivity matrix is cleared (for nonlinear problems) and the sensitivity recomputed. This will close #1358 --------- Co-authored-by: Santiago Soler --- .../static/resistivity/simulation.py | 1 + .../static/resistivity/simulation_2d.py | 2 +- .../simulation.py | 1 + .../simulation_2d.py | 2 + tests/em/static/test_model_assignment.py | 180 ++++++++++++++++++ 5 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 tests/em/static/test_model_assignment.py diff --git a/simpeg/electromagnetics/static/resistivity/simulation.py b/simpeg/electromagnetics/static/resistivity/simulation.py index cd7c97016b..f901f0390c 100644 --- a/simpeg/electromagnetics/static/resistivity/simulation.py +++ b/simpeg/electromagnetics/static/resistivity/simulation.py @@ -112,6 +112,7 @@ def fields(self, m=None, calcJ=True): return f def getJ(self, m, f=None): + self.model = m if getattr(self, "_Jmatrix", None) is None: if f is None: f = self.fields(m) diff --git a/simpeg/electromagnetics/static/resistivity/simulation_2d.py b/simpeg/electromagnetics/static/resistivity/simulation_2d.py index a405d0ccdd..433d6f00f5 100644 --- a/simpeg/electromagnetics/static/resistivity/simulation_2d.py +++ b/simpeg/electromagnetics/static/resistivity/simulation_2d.py @@ -278,10 +278,10 @@ def getJ(self, m, f=None): """ Generate Full sensitivity matrix """ + self.model = m if getattr(self, "_Jmatrix", None) is None: if self.verbose: print("Calculating J and storing") - self.model = m if f is None: f = self.fields(m) self._Jmatrix = (self._Jtvec(m, v=None, f=f)).T diff --git a/simpeg/electromagnetics/static/spectral_induced_polarization/simulation.py b/simpeg/electromagnetics/static/spectral_induced_polarization/simulation.py index 5abc4b4e8f..b0b3344e93 100644 --- a/simpeg/electromagnetics/static/spectral_induced_polarization/simulation.py +++ b/simpeg/electromagnetics/static/spectral_induced_polarization/simulation.py @@ -359,6 +359,7 @@ def getJ(self, m, f=None): """ Generate Full sensitivity matrix """ + self.model = m if self._Jmatrix is not None: return self._Jmatrix diff --git a/simpeg/electromagnetics/static/spectral_induced_polarization/simulation_2d.py b/simpeg/electromagnetics/static/spectral_induced_polarization/simulation_2d.py index 2a0faf4906..7b1a3be006 100644 --- a/simpeg/electromagnetics/static/spectral_induced_polarization/simulation_2d.py +++ b/simpeg/electromagnetics/static/spectral_induced_polarization/simulation_2d.py @@ -17,6 +17,8 @@ def getJ(self, m, f=None): Generate Full sensitivity matrix """ + self.model = m + if self.verbose: print(">> Compute Sensitivity matrix") diff --git a/tests/em/static/test_model_assignment.py b/tests/em/static/test_model_assignment.py new file mode 100644 index 0000000000..cc21f59bb8 --- /dev/null +++ b/tests/em/static/test_model_assignment.py @@ -0,0 +1,180 @@ +""" +Test model assignment to simulation classes + +Test if the `getJ` method of a few static EM simulations updates the `model`. +These tests have been added as part of the bugfix in #1361. +""" + +import pytest +import numpy as np + +from discretize import TensorMesh +from simpeg import utils +from simpeg.maps import IdentityMap, Wires +from simpeg.electromagnetics import resistivity as dc +from simpeg.electromagnetics import spectral_induced_polarization as sip +from simpeg.electromagnetics.static.utils import generate_dcip_sources_line + + +class TestDCSimulations: + @pytest.fixture + def mesh_3d(self): + """Sample mesh.""" + cell_size = 0.5 + npad = 2 + hx = [(cell_size, npad, -1.5), (cell_size, 10), (cell_size, npad, 1.5)] + hy = [(cell_size, npad, -1.5), (cell_size, 10), (cell_size, npad, 1.5)] + hz = [(cell_size, npad, -1.5), (cell_size, 10), (cell_size, npad, 1.5)] + mesh = TensorMesh([hx, hy, hz], x0="CCC") + return mesh + + @pytest.fixture + def survey_3d(self, mesh_3d): + """Sample survey.""" + xmin, xmax = mesh_3d.nodes_x.min(), mesh_3d.nodes_x.max() + ymin, ymax = mesh_3d.nodes_y.min(), mesh_3d.nodes_y.max() + x = mesh_3d.nodes_x[(mesh_3d.nodes_x > xmin) & (mesh_3d.nodes_x < xmax)] + y = mesh_3d.nodes_y[(mesh_3d.nodes_y > ymin) & (mesh_3d.nodes_y < ymax)] + + Aloc = np.r_[1.25, 0.0, 0.0] + Bloc = np.r_[-1.25, 0.0, 0.0] + M = utils.ndgrid(x - 1.0, y, np.r_[0.0]) + N = utils.ndgrid(x + 1.0, y, np.r_[0.0]) + rx = dc.receivers.Dipole(M, N) + src = dc.sources.Dipole([rx], Aloc, Bloc) + survey = dc.survey.Survey([src]) + return survey + + @pytest.fixture + def mesh_2d(self): + """Sample mesh.""" + cell_size = 0.5 + width = 10.0 + hx = [ + (cell_size, 10, -1.3), + (cell_size, width / cell_size), + (cell_size, 10, 1.3), + ] + hy = [(cell_size, 3, -1.3), (cell_size, 3, 1.3)] + mesh = TensorMesh([hx, hy], "CN") + return mesh + + @pytest.fixture + def survey_2d(self, mesh_2d): + """Sample survey.""" + survey_end_points = np.array([-5.0, 5.0, 0, 0]) + + source_list = generate_dcip_sources_line( + "dipole-dipole", "volt", "2D", survey_end_points, 0.0, 5, 2.5 + ) + survey = dc.survey.Survey(source_list) + return survey + + @pytest.mark.parametrize( + "simulation_class", + (dc.simulation.Simulation3DNodal, dc.simulation.Simulation3DCellCentered), + ) + @pytest.mark.parametrize("storeJ", [True, False]) + def test_simulation_3d(self, mesh_3d, survey_3d, simulation_class, storeJ): + """ + Test model assignment on the ``getJ`` method of 3d simulations + """ + mapping = IdentityMap(mesh_3d) + simulation = simulation_class( + mesh=mesh_3d, survey=survey_3d, sigmaMap=mapping, storeJ=storeJ + ) + model_1 = np.ones(mesh_3d.nC) * 1e-2 + model_2 = np.ones(mesh_3d.nC) * 1e-1 + # Call `getJ` passing a model and check if it was correctly assigned + j_1 = simulation.getJ(model_1) + assert model_1 is simulation.model + # Call `getJ` passing a different model and check if it was correctly assigned + j_2 = simulation.getJ(model_2) + assert model_2 is simulation.model + # Check if the two Js are different + assert not np.allclose(j_1, j_2) + + @pytest.mark.parametrize( + "simulation_class", + (dc.simulation_2d.Simulation2DNodal, dc.simulation_2d.Simulation2DCellCentered), + ) + @pytest.mark.parametrize("storeJ", [True, False]) + def test_simulation_2d(self, mesh_2d, survey_2d, simulation_class, storeJ): + """ + Test model assignment on the ``getJ`` method of 2d simulations + """ + mapping = IdentityMap(mesh_2d) + simulation = simulation_class( + mesh=mesh_2d, survey=survey_2d, sigmaMap=mapping, storeJ=storeJ + ) + model_1 = np.ones(mesh_2d.nC) * 1e-2 + model_2 = np.ones(mesh_2d.nC) * 1e-1 + # Call `getJ` passing a model and check if it was correctly assigned + j_1 = simulation.getJ(model_1) + assert model_1 is simulation.model + # Call `getJ` passing a different model and check if it was correctly assigned + j_2 = simulation.getJ(model_2) + assert model_2 is simulation.model + # Check if the two Js are different + assert not np.allclose(j_1, j_2) + + +class TestSIPSimulations: + @pytest.fixture + def mesh_3d(self): + """Sample mesh.""" + cs = 25.0 + hx = [(cs, 0, -1.3), (cs, 21), (cs, 0, 1.3)] + hy = [(cs, 0, -1.3), (cs, 21), (cs, 0, 1.3)] + hz = [(cs, 0, -1.3), (cs, 20)] + mesh = TensorMesh([hx, hy, hz], x0="CCN") + return mesh + + @pytest.fixture + def survey_3d(self, mesh_3d): + """Sample survey.""" + x = mesh_3d.cell_centers_x[ + (mesh_3d.cell_centers_x > -155.0) & (mesh_3d.cell_centers_x < 155.0) + ] + y = mesh_3d.cell_centers_y[ + (mesh_3d.cell_centers_y > -155.0) & (mesh_3d.cell_centers_y < 155.0) + ] + Aloc = np.r_[-200.0, 0.0, 0.0] + Bloc = np.r_[200.0, 0.0, 0.0] + M = utils.ndgrid(x - 25.0, y, np.r_[0.0]) + + times = np.arange(10) * 1e-3 + 1e-3 + rx = sip.receivers.Pole(M, times) + src = sip.sources.Dipole([rx], Aloc, Bloc) + survey = sip.Survey([src]) + return survey + + @pytest.mark.xfail( + reason=( + "SIP simulation requires some care to pass this test. " + "See #1361 for more details." + ) + ) + def test_simulation_3d(self, mesh_3d, survey_3d): + """ + Test model assignment on the ``getJ`` method of 3d simulations + """ + wires = Wires(("eta", mesh_3d.nC), ("taui", mesh_3d.nC)) + sigma = np.ones(mesh_3d.nC) * 1e-2 + simulation = sip.Simulation3DNodal( + mesh_3d, + sigma=sigma, + survey=survey_3d, + etaMap=wires.eta, + tauiMap=wires.taui, + ) + model_1 = np.r_[sigma, 1.0 / sigma] + model_2 = np.r_[sigma * 2, 1.0 / sigma] + # Call `getJ` passing a model and check if it was correctly assigned + j_1 = simulation.getJ(model_1) + assert model_1 is simulation.model + # Call `getJ` passing a different model and check if it was correctly assigned + j_2 = simulation.getJ(model_2) + assert model_2 is simulation.model + # Check if the two Js are different + assert not np.allclose(j_1, j_2) From deda1727fbcbf7d3406fbde072da3301d5e6b8f7 Mon Sep 17 00:00:00 2001 From: "Williams A. Lima" Date: Thu, 24 Apr 2025 17:52:49 -0300 Subject: [PATCH 135/194] Fix `getJ` method in TDEM and FDEM 1D simulations (#1638) Standardize the `getJ` methods of TDEM and FDEM 1D simulations so they return a dense array instead of a dictionary with blocks of the `J` matrix. Make the old `getJ` method private by renaming it to `_getJ`. The new `getJ` method reuses the `_getJ` and builds the while `J` as a dense array before returning it. Update `Jvec` and `Jtvec` so they use the new private method. Add tests to check the behaviour of the new public method. --------- Co-authored-by: Santiago Soler --- simpeg/electromagnetics/base_1d.py | 6 +- .../frequency_domain/simulation_1d.py | 60 ++- .../time_domain/simulation_1d.py | 61 ++- tests/em/em1d/test_EM1D_FD_fwd.py | 2 +- tests/em/em1d/test_EM1D_FD_getJ.py | 107 ++++ tests/em/em1d/test_EM1D_FD_jac_layers2.py | 490 ++++++++++++++++++ tests/em/em1d/test_EM1D_FD_jac_layers3.py | 461 ++++++++++++++++ .../em1d/test_EM1D_TD_general_jac_layers.py | 3 +- tests/em/em1d/test_EM1D_TD_getJ.py | 161 ++++++ 9 files changed, 1344 insertions(+), 7 deletions(-) create mode 100644 tests/em/em1d/test_EM1D_FD_getJ.py create mode 100644 tests/em/em1d/test_EM1D_FD_jac_layers2.py create mode 100644 tests/em/em1d/test_EM1D_FD_jac_layers3.py create mode 100644 tests/em/em1d/test_EM1D_TD_getJ.py diff --git a/simpeg/electromagnetics/base_1d.py b/simpeg/electromagnetics/base_1d.py index afba170629..22609872e1 100644 --- a/simpeg/electromagnetics/base_1d.py +++ b/simpeg/electromagnetics/base_1d.py @@ -334,7 +334,7 @@ def compute_complex_mu(self, frequencies): return mu_complex def Jvec(self, m, v, f=None): - Js = self.getJ(m, f=f) + Js = self._getJ(m, f=f) out = 0.0 if self.hMap is not None: out = out + Js["dh"] @ (self.hDeriv @ v) @@ -347,7 +347,7 @@ def Jvec(self, m, v, f=None): return out def Jtvec(self, m, v, f=None): - Js = self.getJ(m, f=f) + Js = self._getJ(m, f=f) out = 0.0 if self.hMap is not None: out = out + self.hDeriv.T @ (Js["dh"].T @ v) @@ -601,7 +601,7 @@ def get_threshold(self, uncert): def getJtJdiag(self, m, W=None, f=None): if getattr(self, "_gtgdiag", None) is None: - Js = self.getJ(m, f=f) + Js = self._getJ(m, f=f) if W is None: W = np.ones(self.survey.nD) else: diff --git a/simpeg/electromagnetics/frequency_domain/simulation_1d.py b/simpeg/electromagnetics/frequency_domain/simulation_1d.py index f7fe4d867e..57806b6037 100644 --- a/simpeg/electromagnetics/frequency_domain/simulation_1d.py +++ b/simpeg/electromagnetics/frequency_domain/simulation_1d.py @@ -127,7 +127,27 @@ def fields(self, m): return self._project_to_data(v) - def getJ(self, m, f=None): + def _getJ(self, m, f=None): + """Build Jacobian matrix by blocks. + + This method builds the Jacobian matrix by blocks, each block for a particular + invertible property (receiver height, conductivity, permeability, layer + thickness). Each block of the Jacobian matrix is stored within a dictionary. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. + f : Ignored + Not used, present here for API consistency by convention. + + Returns + ------- + dict + Dictionary containing the blocks of the Jacobian matrix for the invertible + properties. The keys of the dictionary can be `"dh"`, `"ds"`, `"dmu"`, and + `"dthick"`. + """ self.model = m if getattr(self, "_J", None) is None: self._J = {} @@ -244,6 +264,44 @@ def getJ(self, m, f=None): self._J["dthick"] = self._project_to_data(v_dthick) return self._J + def getJ(self, m, f=None): + r"""Get the Jacobian matrix. + + This method generates and stores the full Jacobian matrix for the + model provided. I.e.: + + .. math:: + \mathbf{J} = \dfrac{\partial f(\mu(\mathbf{m}))}{\partial \mathbf{m}} + + where :math:`f()` is the forward modelling function, :math:`\mu()` is the + mapping, and :math:`\mathbf{m}` is the model vector. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. + f : Ignored + Not used, present here for API consistency by convention. + + Returns + ------- + (n_data, n_param) numpy.ndarray + The full Jacobian matrix. + """ + Js = self._getJ(m, f=f) + # Map parameters with their corresponding derivatives + param_and_derivs = { + "dh": self.hDeriv, + "ds": self.sigmaDeriv, + "dmu": self.muDeriv, + "dthick": self.thicknessesDeriv, + } + + # Compute J matrix + J = sum(Js[param] @ param_and_derivs[param] for param in Js) + + return J + def _project_to_data(self, v): i_dat = 0 i_v = 0 diff --git a/simpeg/electromagnetics/time_domain/simulation_1d.py b/simpeg/electromagnetics/time_domain/simulation_1d.py index e2f5b8a6bb..1e72af8527 100644 --- a/simpeg/electromagnetics/time_domain/simulation_1d.py +++ b/simpeg/electromagnetics/time_domain/simulation_1d.py @@ -249,7 +249,27 @@ def fields(self, m): return self._project_to_data(v.T) - def getJ(self, m, f=None): + def _getJ(self, m, f=None): + """Build Jacobian matrix by blocks. + + This method builds the Jacobian matrix by blocks, each block for a particular + invertible property (receiver height, conductivity, permeability, layer + thickness). Each block of the Jacobian matrix is stored within a dictionary. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. + f : Ignored + Not used, present here for API consistency by convention. + + Returns + ------- + dict + Dictionary containing the blocks of the Jacobian matrix for the invertible + properties. The keys of the dictionary can be `"dh"`, `"ds"`, `"dmu"`, and + `"dthick"`. + """ self.model = m if getattr(self, "_J", None) is None: self._J = {} @@ -346,6 +366,45 @@ def getJ(self, m, f=None): self._J["dthick"] = self._project_to_data(v_dthick) return self._J + def getJ(self, m, f=None): + r"""Get the Jacobian matrix. + + This method generates and stores the full Jacobian matrix for the + model provided. I.e.: + + .. math:: + \mathbf{J} = \dfrac{\partial f(\mu(\mathbf{m}))}{\partial \mathbf{m}} + + where :math:`f()` is the forward modelling function, :math:`\mu()` is the + mapping, and :math:`\mathbf{m}` is the model vector. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. + f : Ignored + Not used, present here for API consistency by convention. + + Returns + ------- + (n_data, n_param) numpy.ndarray + The full Jacobian matrix. + """ + Js = self._getJ(m, f=f) + + # Map parameters with their corresponding derivatives + param_and_derivs = { + "dh": self.hDeriv, + "ds": self.sigmaDeriv, + "dmu": self.muDeriv, + "dthick": self.thicknessesDeriv, + } + + # Compute J matrix + J = sum(Js[param] @ param_and_derivs[param] for param in Js) + + return J + def _project_to_data(self, v): As = self._As if v.ndim == 3: diff --git a/tests/em/em1d/test_EM1D_FD_fwd.py b/tests/em/em1d/test_EM1D_FD_fwd.py index 4986024335..88a7498d5a 100644 --- a/tests/em/em1d/test_EM1D_FD_fwd.py +++ b/tests/em/em1d/test_EM1D_FD_fwd.py @@ -592,7 +592,7 @@ def test_rx_loc_shapes(rx_class, n_locs1, n_locs2, orientation, component): sim.sigmaMap = maps.IdentityMap(nP=1) # make sure forming J works - J = sim.getJ(np.ones(1))["ds"] + J = sim.getJ(np.ones(1)) assert J.shape == (n_d, 1) # and all of its values are the same too: diff --git a/tests/em/em1d/test_EM1D_FD_getJ.py b/tests/em/em1d/test_EM1D_FD_getJ.py new file mode 100644 index 0000000000..cf18744e2f --- /dev/null +++ b/tests/em/em1d/test_EM1D_FD_getJ.py @@ -0,0 +1,107 @@ +""" +Test the getJ method of FDEM 1D simulation. +""" + +import numpy as np +import simpeg.electromagnetics.frequency_domain as fdem +from simpeg import maps + + +def create_simulation_and_conductivities(identity_mapping: bool): + # Create Survey + # ------------- + # Source properties + frequencies = np.r_[382, 1822, 7970, 35920, 130100] # frequencies in Hz + source_location = np.array([0.0, 0.0, 30.0]) # (3, ) numpy.array_like + source_orientation = "z" # "x", "y" or "z" + moment = 1.0 # dipole moment in Am^2 + + # Receiver properties + receiver_locations = np.array([10.0, 0.0, 30.0]) # or (N, 3) numpy.ndarray + receiver_orientation = "z" # "x", "y" or "z" + data_type = "ppm" # "secondary", "total" or "ppm" + + source_list = [] # create empty list for source objects + + # loop over all sources + for freq in frequencies: + # Define receivers that measure real and imaginary component + # magnetic field data in ppm. + receiver_list = [] + receiver_list.append( + fdem.receivers.PointMagneticFieldSecondary( + receiver_locations, + orientation=receiver_orientation, + data_type=data_type, + component="real", + ) + ) + receiver_list.append( + fdem.receivers.PointMagneticFieldSecondary( + receiver_locations, + orientation=receiver_orientation, + data_type=data_type, + component="imag", + ) + ) + + # Define a magnetic dipole source at each frequency + source_list.append( + fdem.sources.MagDipole( + receiver_list=receiver_list, + frequency=freq, + location=source_location, + orientation=source_orientation, + moment=moment, + ) + ) + + # Define the survey + survey = fdem.survey.Survey(source_list) + + # Defining a 1D Layered Earth Model + # --------------------------------- + # Define layer thicknesses (m) + thicknesses = np.array([20.0, 40.0]) + + # Define layer conductivities (S/m) + conductivities = np.r_[0.1, 1.0, 0.1] + + # Define a mapping + n_layers = len(conductivities) + model_mapping = ( + maps.IdentityMap(nP=n_layers) if identity_mapping else maps.ExpMap(nP=n_layers) + ) + + # Define the Forward Simulation, Predict Data and Plot + # ---------------------------------------------------- + simulation = fdem.Simulation1DLayered( + survey=survey, + thicknesses=thicknesses, + sigmaMap=model_mapping, + ) + + return simulation, conductivities + + +def test_getJ(): + """ + Test if getJ returns different J matrices after passing different maps. + """ + dpreds, jacobians = [], [] + + # Compute dpred and J using an identity map and an exp map + for identity_mapping in (True, False): + simulation, conductivities = create_simulation_and_conductivities( + identity_mapping + ) + model = conductivities if identity_mapping else np.log(conductivities) + dpreds.append(simulation.dpred(model)) + jac = simulation.getJ(model) + jacobians.append(jac) + + # The two dpreds should be equal + assert np.allclose(*dpreds) + + # The two J matrices should not be equal + assert not np.allclose(*jacobians, atol=0.0) diff --git a/tests/em/em1d/test_EM1D_FD_jac_layers2.py b/tests/em/em1d/test_EM1D_FD_jac_layers2.py new file mode 100644 index 0000000000..dbbe2154b8 --- /dev/null +++ b/tests/em/em1d/test_EM1D_FD_jac_layers2.py @@ -0,0 +1,490 @@ +from simpeg import maps +from discretize import tests, TensorMesh +import simpeg.electromagnetics.frequency_domain as fdem +import numpy as np +from scipy.constants import mu_0 +from scipy.sparse import diags + + +class TestEM1D_FD_Jacobian_MagDipole: + + # Tests 2nd order convergence of Jvec and Jtvec for magnetic dipole sources. + # - All src and rx orientations + # - All rx components + # - Span many frequencies + # - Tests derivatives wrt sigma, mu, thicknesses and h + def setup_class(self): + # Layers and topography + nearthick = np.logspace(-1, 1, 5) + deepthick = np.logspace(1, 2, 10) + thicknesses = np.r_[nearthick, deepthick] + topo = np.r_[0.0, 0.0, 100.0] + + # Survey Geometry + height = 1e-5 + src_location = np.array([0.0, 0.0, 100.0 + height]) + rx_location = np.array([5.0, 5.0, 100.0 + height]) + frequencies = np.logspace(1, 8, 9) + orientations = ["x", "y", "z"] + components = ["real", "imag", "both"] + + # Define sources and receivers + source_list = [] + for f in frequencies: + for tx_orientation in orientations: + receiver_list = [] + + for rx_orientation in orientations: + for comp in components: + receiver_list.append( + fdem.receivers.PointMagneticFieldSecondary( + rx_location, orientation=rx_orientation, component=comp + ) + ) + + source_list.append( + fdem.sources.MagDipole( + receiver_list, + frequency=f, + location=src_location, + orientation=tx_orientation, + ) + ) + + # Survey + survey = fdem.Survey(source_list) + + self.topo = topo + self.survey = survey + self.showIt = False + self.height = height + self.frequencies = frequencies + self.thicknesses = thicknesses + self.nlayers = len(thicknesses) + 1 + + wire_map = maps.Wires( + ("mu", self.nlayers), + ("sigma", self.nlayers), + ("h", 1), + ("thicknesses", self.nlayers - 1), + ) + self.sigma_map = maps.ExpMap(nP=self.nlayers) * wire_map.sigma + self.mu_map = maps.ExpMap(nP=self.nlayers) * wire_map.mu + self.thicknesses_map = maps.ExpMap(nP=self.nlayers - 1) * wire_map.thicknesses + nP = len(source_list) + surject_mesh = TensorMesh([np.ones(nP)]) + self.h_map = maps.SurjectFull(surject_mesh) * maps.ExpMap(nP=1) * wire_map.h + + sim = fdem.Simulation1DLayered( + survey=self.survey, + sigmaMap=self.sigma_map, + muMap=self.mu_map, + thicknessesMap=self.thicknesses_map, + hMap=self.h_map, + topo=self.topo, + ) + + self.sim = sim + + def test_EM1DFDJvec_Layers(self): + # Conductivity + sigma_half = 0.01 + sigma_blk = 0.1 + sig = np.ones(self.nlayers) * sigma_half + sig[3] = sigma_blk + + # Permeability + mu_half = mu_0 + mu_blk = 2 * mu_0 + mu = np.ones(self.nlayers) * mu_half + mu[3] = mu_blk + + # General model + m_1D = np.r_[ + np.log(mu), np.log(sig), np.log(self.height), np.log(self.thicknesses) + ] + + def fwdfun(m): + resp = self.sim.dpred(m) + return resp + # return Hz + + def jacfun(m, dm): + Jvec = self.sim.Jvec(m, dm) + return Jvec + + def derChk(m): + return [fwdfun(m), lambda mx: jacfun(m, mx)] + + dm = m_1D * 0.5 + + passed = tests.check_derivative( + derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15, random_seed=9186724 + ) + assert passed + + def test_EM1DFDJtvec_Layers(self): + # Conductivity + sigma_half = 0.01 + sigma_blk = 0.1 + sig = np.ones(self.nlayers) * sigma_half + sig[3] = sigma_blk + + # Permeability + mu_half = mu_0 + mu_blk = 2 * mu_0 + mu = np.ones(self.nlayers) * mu_half + mu[3] = mu_blk + + # General model + m_true = np.r_[ + np.log(mu), np.log(sig), np.log(self.height), np.log(self.thicknesses) + ] + + dobs = self.sim.dpred(m_true) + + m_ini = np.r_[ + np.log(np.ones(self.nlayers) * 1.5 * mu_half), + np.log(np.ones(self.nlayers) * sigma_half), + np.log(0.5 * self.height), + np.log(self.thicknesses) * 0.9, + ] + resp_ini = self.sim.dpred(m_ini) + dr = resp_ini - dobs + + def misfit(m, dobs): + dpred = self.sim.dpred(m) + misfit = np.linalg.norm(dpred - dobs) ** 2 + dmisfit = 2.0 * self.sim.Jtvec( + m, dr + ) # derivative of ||dpred - dobs||^2 gives factor of 2 + return misfit, dmisfit + + def derChk(m): + return misfit(m, dobs) + + passed = tests.check_derivative( + derChk, m_ini, num=4, plotIt=False, eps=1e-27, random_seed=2345 + ) + assert passed + + def test_jtjdiag(self): + # Conductivity + sigma_half = 0.01 + sigma_blk = 0.1 + sig = np.ones(self.nlayers) * sigma_half + sig[3] = sigma_blk + + # Permeability + mu_half = mu_0 + mu_blk = 2 * mu_0 + mu = np.ones(self.nlayers) * mu_half + mu[3] = mu_blk + + # General model + model = np.r_[ + np.log(mu), np.log(sig), np.log(self.height), np.log(self.thicknesses) + ] + + rng = np.random.default_rng(seed=42) + weights_matrix = diags(rng.random(size=self.sim.survey.nD)) + jtj_diag = self.sim.getJtJdiag(model, W=weights_matrix) + + J = self.sim.getJ(model) + expected = np.diag(J.T @ weights_matrix.T @ weights_matrix @ J) + np.testing.assert_allclose(expected, jtj_diag) + + +class TestEM1D_FD_Jacobian_CircularLoop: + # Tests 2nd order convergence of Jvec and Jtvec for horizontal loop sources. + # - All rx orientations + # - All rx components + # - Span many frequencies + # - Tests derivatives wrt sigma, mu, thicknesses and h + def setup_class(self): + nearthick = np.logspace(-1, 1, 5) + deepthick = np.logspace(1, 2, 10) + thicknesses = np.r_[nearthick, deepthick] + topo = np.r_[0.0, 0.0, 100.0] + height = 1e-5 + + src_location = np.array([0.0, 0.0, 100.0 + height]) + rx_location = np.array([0.0, 0.0, 100.0 + height]) + frequencies = np.logspace(1, 8, 9) + orientations = ["x", "y", "z"] + components = ["real", "imag", "both"] + I = 1.0 + a = 10.0 + + # Define sources and receivers + source_list = [] + for f in frequencies: + receiver_list = [] + + for rx_orientation in orientations: + for comp in components: + receiver_list.append( + fdem.receivers.PointMagneticFieldSecondary( + rx_location, orientation=rx_orientation, component=comp + ) + ) + + source_list.append( + fdem.sources.CircularLoop( + receiver_list, f, src_location, radius=a, current=I + ) + ) + + # Survey + survey = fdem.Survey(source_list) + + self.topo = topo + self.survey = survey + self.showIt = False + self.height = height + self.frequencies = frequencies + self.thicknesses = thicknesses + self.nlayers = len(thicknesses) + 1 + + nP = len(source_list) + + wire_map = maps.Wires( + ("sigma", self.nlayers), + ("mu", self.nlayers), + ("thicknesses", self.nlayers - 1), + ("h", 1), + ) + self.sigma_map = maps.ExpMap(nP=self.nlayers) * wire_map.sigma + self.mu_map = maps.ExpMap(nP=self.nlayers) * wire_map.mu + self.thicknesses_map = maps.ExpMap(nP=self.nlayers - 1) * wire_map.thicknesses + surject_mesh = TensorMesh([np.ones(nP)]) + self.h_map = maps.SurjectFull(surject_mesh) * maps.ExpMap(nP=1) * wire_map.h + + sim = fdem.Simulation1DLayered( + survey=self.survey, + sigmaMap=self.sigma_map, + muMap=self.mu_map, + thicknessesMap=self.thicknesses_map, + hMap=self.h_map, + topo=self.topo, + ) + + self.sim = sim + + def test_EM1DFDJvec_Layers(self): + # Conductivity + sigma_half = 0.01 + sigma_blk = 0.1 + sig = np.ones(self.nlayers) * sigma_half + sig[3] = sigma_blk + + # Permeability + mu_half = mu_0 + mu_blk = 2 * mu_0 + mu = np.ones(self.nlayers) * mu_half + mu[3] = mu_blk + + # General model + m_1D = np.r_[ + np.log(sig), np.log(mu), np.log(self.thicknesses), np.log(self.height) + ] + + def fwdfun(m): + resp = self.sim.dpred(m) + return resp + # return Hz + + def jacfun(m, dm): + Jvec = self.sim.Jvec(m, dm) + return Jvec + + def derChk(m): + return [fwdfun(m), lambda mx: jacfun(m, mx)] + + dm = m_1D * 0.5 + + passed = tests.check_derivative( + derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15, random_seed=664 + ) + assert passed + + def test_EM1DFDJtvec_Layers(self): + # Conductivity + sigma_half = 0.01 + sigma_blk = 0.1 + sig = np.ones(self.nlayers) * sigma_half + sig[3] = sigma_blk + + # Permeability + mu_half = mu_0 + mu_blk = 2 * mu_0 + mu = np.ones(self.nlayers) * mu_half + mu[3] = mu_blk + + # General model + m_true = np.r_[ + np.log(sig), np.log(mu), np.log(self.thicknesses), np.log(self.height) + ] + + dobs = self.sim.dpred(m_true) + + m_ini = np.r_[ + np.log(np.ones(self.nlayers) * sigma_half), + np.log(np.ones(self.nlayers) * 1.5 * mu_half), + np.log(self.thicknesses) * 0.9, + np.log(0.5 * self.height), + ] + resp_ini = self.sim.dpred(m_ini) + dr = resp_ini - dobs + + def misfit(m, dobs): + dpred = self.sim.dpred(m) + misfit = np.linalg.norm(dpred - dobs) ** 2 + dmisfit = 2 * self.sim.Jtvec( + m, dr + ) # derivative of ||dpred - dobs||^2 gives factor of 2 + return misfit, dmisfit + + def derChk(m): + return misfit(m, dobs) + + passed = tests.check_derivative( + derChk, m_ini, num=4, plotIt=False, eps=1e-27, random_seed=42 + ) + assert passed + + +class TestEM1D_FD_Jacobian_LineCurrent: + # Tests 2nd order convergence of Jvec and Jtvec for piecewise linear loop. + # - All rx orientations + # - All rx components + # - Span many frequencies + # - Tests derivatives wrt sigma, mu, thicknesses and h + def setup_class(self): + x_path = np.array([-2, -2, 2, 2, -2]) + y_path = np.array([-1, 1, 1, -1, -1]) + frequencies = np.logspace(0, 4) + + wire_paths = np.c_[x_path, y_path, np.ones(5) * 0.5] + source_list = [] + receiver_list = [] + receiver_location = np.array([9.28, 0.0, 0.45]) + orientations = ["x", "y", "z"] + components = ["real", "imag", "both"] + + # Define sources and receivers + source_list = [] + for f in frequencies: + receiver_list = [] + + for rx_orientation in orientations: + for comp in components: + receiver_list.append( + fdem.receivers.PointMagneticFieldSecondary( + receiver_location, + orientation=rx_orientation, + component=comp, + ) + ) + + source_list.append(fdem.sources.LineCurrent(receiver_list, f, wire_paths)) + + # Survey + survey = fdem.Survey(source_list) + self.thicknesses = np.array([20.0, 40.0]) + + self.nlayers = len(self.thicknesses) + 1 + wire_map = maps.Wires( + ("sigma", self.nlayers), + ("mu", self.nlayers), + ("thicknesses", self.nlayers - 1), + ) + self.sigma_map = maps.ExpMap(nP=self.nlayers) * wire_map.sigma + self.mu_map = maps.ExpMap(nP=self.nlayers) * wire_map.mu + self.thicknesses_map = maps.ExpMap(nP=self.nlayers - 1) * wire_map.thicknesses + + sim = fdem.Simulation1DLayered( + survey=survey, + sigmaMap=self.sigma_map, + muMap=self.mu_map, + thicknessesMap=self.thicknesses_map, + ) + + self.sim = sim + + def test_EM1DFDJvec_Layers(self): + # Conductivity + sigma_half = 0.01 + sigma_blk = 0.1 + sig = np.ones(self.nlayers) * sigma_half + sig[1] = sigma_blk + + # Permeability + mu_half = mu_0 + mu_blk = 1.1 * mu_0 + mu = np.ones(self.nlayers) * mu_half + mu[1] = mu_blk + + # General model + m_1D = np.r_[np.log(sig), np.log(mu), np.log(self.thicknesses)] + + def fwdfun(m): + resp = self.sim.dpred(m) + return resp + # return Hz + + def jacfun(m, dm): + Jvec = self.sim.Jvec(m, dm) + return Jvec + + dm = m_1D * 0.5 + + def derChk(m): + return [fwdfun(m), lambda mx: jacfun(m, mx)] + + passed = tests.check_derivative( + derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15, random_seed=1123 + ) + assert passed + + def test_EM1DFDJtvec_Layers(self): + # Conductivity + sigma_half = 0.01 + sigma_blk = 0.1 + sig = np.ones(self.nlayers) * sigma_half + sig[1] = sigma_blk + + # Permeability + mu_half = mu_0 + mu_blk = 1.1 * mu_0 + mu = np.ones(self.nlayers) * mu_half + mu[1] = mu_blk + + # General model + m_true = np.r_[np.log(sig), np.log(mu), np.log(self.thicknesses)] + + dobs = self.sim.dpred(m_true) + + m_ini = np.r_[ + np.log(np.ones(self.nlayers) * sigma_half), + np.log(np.ones(self.nlayers) * mu_half), + np.log(self.thicknesses) * 0.9, + ] + resp_ini = self.sim.dpred(m_ini) + dr = resp_ini - dobs + + def misfit(m, dobs): + dpred = self.sim.dpred(m) + misfit = np.linalg.norm(dpred - dobs) ** 2 + dmisfit = 2 * self.sim.Jtvec( + m, dr + ) # derivative of ||dpred - dobs||^2 gives factor of 2 + return misfit, dmisfit + + def derChk(m): + return misfit(m, dobs) + + passed = tests.check_derivative( + derChk, m_ini, num=4, plotIt=False, eps=1e-27, random_seed=124 + ) + assert passed diff --git a/tests/em/em1d/test_EM1D_FD_jac_layers3.py b/tests/em/em1d/test_EM1D_FD_jac_layers3.py new file mode 100644 index 0000000000..bc49ac3432 --- /dev/null +++ b/tests/em/em1d/test_EM1D_FD_jac_layers3.py @@ -0,0 +1,461 @@ +from simpeg import maps +from discretize import tests, TensorMesh +import simpeg.electromagnetics.frequency_domain as fdem +import numpy as np +from scipy.constants import mu_0 +from scipy.sparse import diags + + +class TestEM1D_FD_Jacobian_MagDipole: + # Tests 2nd order convergence of Jvec and Jtvec for magnetic dipole sources. + # - All src and rx orientations + # - All rx components + # - Span many frequencies + # - Tests derivatives wrt sigma, mu, thicknesses and h + def setup_class(self): + # Layers and topography + nearthick = np.logspace(-1, 1, 5) + deepthick = np.logspace(1, 2, 10) + thicknesses = np.r_[nearthick, deepthick] + topo = np.r_[0.0, 0.0, 100.0] + + # Survey Geometry + height = 1e-5 + src_location = np.array([0.0, 0.0, 100.0 + height]) + rx_location = np.array([5.0, 5.0, 100.0 + height]) + frequencies = np.logspace(1, 8, 9) + orientations = ["x", "y", "z"] + components = ["real", "imag", "both"] + + # Define sources and receivers + source_list = [] + for f in frequencies: + for tx_orientation in orientations: + receiver_list = [] + + for rx_orientation in orientations: + for comp in components: + receiver_list.append( + fdem.receivers.PointMagneticFieldSecondary( + rx_location, orientation=rx_orientation, component=comp + ) + ) + + source_list.append( + fdem.sources.MagDipole( + receiver_list, + frequency=f, + location=src_location, + orientation=tx_orientation, + ) + ) + + # Survey + survey = fdem.Survey(source_list) + + self.topo = topo + self.survey = survey + self.showIt = False + self.height = height + self.frequencies = frequencies + self.thicknesses = thicknesses + self.nlayers = len(thicknesses) + 1 + + wire_map = maps.Wires( + ("sigma", self.nlayers), + ("h", 1), + ("thicknesses", self.nlayers - 1), + ) + self.sigma_map = maps.ExpMap(nP=self.nlayers) * wire_map.sigma + self.thicknesses_map = maps.ExpMap(nP=self.nlayers - 1) * wire_map.thicknesses + nP = len(source_list) + surject_mesh = TensorMesh([np.ones(nP)]) + self.h_map = maps.SurjectFull(surject_mesh) * maps.ExpMap(nP=1) * wire_map.h + + sim = fdem.Simulation1DLayered( + survey=self.survey, + sigmaMap=self.sigma_map, + thicknessesMap=self.thicknesses_map, + hMap=self.h_map, + topo=self.topo, + ) + + self.sim = sim + + def test_EM1DFDJvec_Layers(self): + # Conductivity + sigma_half = 0.01 + sigma_blk = 0.1 + sig = np.ones(self.nlayers) * sigma_half + sig[3] = sigma_blk + + # General model + m_1D = np.r_[np.log(sig), np.log(self.height), np.log(self.thicknesses)] + + def fwdfun(m): + resp = self.sim.dpred(m) + return resp + # return Hz + + def jacfun(m, dm): + Jvec = self.sim.Jvec(m, dm) + return Jvec + + def derChk(m): + return [fwdfun(m), lambda mx: jacfun(m, mx)] + + dm = m_1D * 0.5 + + passed = tests.check_derivative( + derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15, random_seed=9186724 + ) + assert passed + + def test_EM1DFDJtvec_Layers(self): + # Conductivity + sigma_half = 0.01 + sigma_blk = 0.1 + sig = np.ones(self.nlayers) * sigma_half + sig[3] = sigma_blk + + # General model + m_true = np.r_[np.log(sig), np.log(self.height), np.log(self.thicknesses)] + + dobs = self.sim.dpred(m_true) + + m_ini = np.r_[ + np.log(np.ones(self.nlayers) * sigma_half), + np.log(0.5 * self.height), + np.log(self.thicknesses) * 0.9, + ] + resp_ini = self.sim.dpred(m_ini) + dr = resp_ini - dobs + + def misfit(m, dobs): + dpred = self.sim.dpred(m) + misfit = np.linalg.norm(dpred - dobs) ** 2 + dmisfit = 2.0 * self.sim.Jtvec( + m, dr + ) # derivative of ||dpred - dobs||^2 gives factor of 2 + return misfit, dmisfit + + def derChk(m): + return misfit(m, dobs) + + passed = tests.check_derivative( + derChk, m_ini, num=4, plotIt=False, eps=1e-27, random_seed=2345 + ) + assert passed + + def test_jtjdiag(self): + # Conductivity + sigma_half = 0.01 + sigma_blk = 0.1 + sig = np.ones(self.nlayers) * sigma_half + sig[3] = sigma_blk + + # General model + model = np.r_[np.log(sig), np.log(self.height), np.log(self.thicknesses)] + + rng = np.random.default_rng(seed=42) + weights_matrix = diags(rng.random(size=self.sim.survey.nD)) + jtj_diag = self.sim.getJtJdiag(model, W=weights_matrix) + + J = self.sim.getJ(model) + expected = np.diag(J.T @ weights_matrix.T @ weights_matrix @ J) + np.testing.assert_allclose(expected, jtj_diag) + + +class TestEM1D_FD_Jacobian_CircularLoop: + # Tests 2nd order convergence of Jvec and Jtvec for horizontal loop sources. + # - All rx orientations + # - All rx components + # - Span many frequencies + # - Tests derivatives wrt sigma, mu, thicknesses and h + def setup_class(self): + nearthick = np.logspace(-1, 1, 5) + deepthick = np.logspace(1, 2, 10) + thicknesses = np.r_[nearthick, deepthick] + topo = np.r_[0.0, 0.0, 100.0] + height = 1e-5 + + src_location = np.array([0.0, 0.0, 100.0 + height]) + rx_location = np.array([0.0, 0.0, 100.0 + height]) + frequencies = np.logspace(1, 8, 9) + orientations = ["x", "y", "z"] + components = ["real", "imag", "both"] + I = 1.0 + a = 10.0 + + # Define sources and receivers + source_list = [] + for f in frequencies: + receiver_list = [] + + for rx_orientation in orientations: + for comp in components: + receiver_list.append( + fdem.receivers.PointMagneticFieldSecondary( + rx_location, orientation=rx_orientation, component=comp + ) + ) + + source_list.append( + fdem.sources.CircularLoop( + receiver_list, f, src_location, radius=a, current=I + ) + ) + + # Survey + survey = fdem.Survey(source_list) + + self.topo = topo + self.survey = survey + self.showIt = False + self.height = height + self.frequencies = frequencies + self.thicknesses = thicknesses + self.nlayers = len(thicknesses) + 1 + + nP = len(source_list) + + wire_map = maps.Wires( + ("sigma", self.nlayers), + ("mu", self.nlayers), + ("thicknesses", self.nlayers - 1), + ("h", 1), + ) + self.sigma_map = maps.ExpMap(nP=self.nlayers) * wire_map.sigma + self.mu_map = maps.ExpMap(nP=self.nlayers) * wire_map.mu + self.thicknesses_map = maps.ExpMap(nP=self.nlayers - 1) * wire_map.thicknesses + surject_mesh = TensorMesh([np.ones(nP)]) + self.h_map = maps.SurjectFull(surject_mesh) * maps.ExpMap(nP=1) * wire_map.h + + sim = fdem.Simulation1DLayered( + survey=self.survey, + sigmaMap=self.sigma_map, + muMap=self.mu_map, + thicknessesMap=self.thicknesses_map, + hMap=self.h_map, + topo=self.topo, + ) + + self.sim = sim + + def test_EM1DFDJvec_Layers(self): + # Conductivity + sigma_half = 0.01 + sigma_blk = 0.1 + sig = np.ones(self.nlayers) * sigma_half + sig[3] = sigma_blk + + # Permeability + mu_half = mu_0 + mu_blk = 2 * mu_0 + mu = np.ones(self.nlayers) * mu_half + mu[3] = mu_blk + + # General model + m_1D = np.r_[ + np.log(sig), np.log(mu), np.log(self.thicknesses), np.log(self.height) + ] + + def fwdfun(m): + resp = self.sim.dpred(m) + return resp + # return Hz + + def jacfun(m, dm): + Jvec = self.sim.Jvec(m, dm) + return Jvec + + def derChk(m): + return [fwdfun(m), lambda mx: jacfun(m, mx)] + + dm = m_1D * 0.5 + + passed = tests.check_derivative( + derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15, random_seed=664 + ) + assert passed + + def test_EM1DFDJtvec_Layers(self): + # Conductivity + sigma_half = 0.01 + sigma_blk = 0.1 + sig = np.ones(self.nlayers) * sigma_half + sig[3] = sigma_blk + + # Permeability + mu_half = mu_0 + mu_blk = 2 * mu_0 + mu = np.ones(self.nlayers) * mu_half + mu[3] = mu_blk + + # General model + m_true = np.r_[ + np.log(sig), np.log(mu), np.log(self.thicknesses), np.log(self.height) + ] + + dobs = self.sim.dpred(m_true) + + m_ini = np.r_[ + np.log(np.ones(self.nlayers) * sigma_half), + np.log(np.ones(self.nlayers) * 1.5 * mu_half), + np.log(self.thicknesses) * 0.9, + np.log(0.5 * self.height), + ] + resp_ini = self.sim.dpred(m_ini) + dr = resp_ini - dobs + + def misfit(m, dobs): + dpred = self.sim.dpred(m) + misfit = np.linalg.norm(dpred - dobs) ** 2 + dmisfit = 2 * self.sim.Jtvec( + m, dr + ) # derivative of ||dpred - dobs||^2 gives factor of 2 + return misfit, dmisfit + + def derChk(m): + return misfit(m, dobs) + + passed = tests.check_derivative( + derChk, m_ini, num=4, plotIt=False, eps=1e-27, random_seed=42 + ) + assert passed + + +class TestEM1D_FD_Jacobian_LineCurrent: + # Tests 2nd order convergence of Jvec and Jtvec for piecewise linear loop. + # - All rx orientations + # - All rx components + # - Span many frequencies + # - Tests derivatives wrt sigma, mu, thicknesses and h + def setup_class(self): + x_path = np.array([-2, -2, 2, 2, -2]) + y_path = np.array([-1, 1, 1, -1, -1]) + frequencies = np.logspace(0, 4) + + wire_paths = np.c_[x_path, y_path, np.ones(5) * 0.5] + source_list = [] + receiver_list = [] + receiver_location = np.array([9.28, 0.0, 0.45]) + orientations = ["x", "y", "z"] + components = ["real", "imag", "both"] + + # Define sources and receivers + source_list = [] + for f in frequencies: + receiver_list = [] + + for rx_orientation in orientations: + for comp in components: + receiver_list.append( + fdem.receivers.PointMagneticFieldSecondary( + receiver_location, + orientation=rx_orientation, + component=comp, + ) + ) + + source_list.append(fdem.sources.LineCurrent(receiver_list, f, wire_paths)) + + # Survey + survey = fdem.Survey(source_list) + self.thicknesses = np.array([20.0, 40.0]) + + self.nlayers = len(self.thicknesses) + 1 + wire_map = maps.Wires( + ("sigma", self.nlayers), + ("mu", self.nlayers), + ("thicknesses", self.nlayers - 1), + ) + self.sigma_map = maps.ExpMap(nP=self.nlayers) * wire_map.sigma + self.mu_map = maps.ExpMap(nP=self.nlayers) * wire_map.mu + self.thicknesses_map = maps.ExpMap(nP=self.nlayers - 1) * wire_map.thicknesses + + sim = fdem.Simulation1DLayered( + survey=survey, + sigmaMap=self.sigma_map, + muMap=self.mu_map, + thicknessesMap=self.thicknesses_map, + ) + + self.sim = sim + + def test_EM1DFDJvec_Layers(self): + # Conductivity + sigma_half = 0.01 + sigma_blk = 0.1 + sig = np.ones(self.nlayers) * sigma_half + sig[1] = sigma_blk + + # Permeability + mu_half = mu_0 + mu_blk = 1.1 * mu_0 + mu = np.ones(self.nlayers) * mu_half + mu[1] = mu_blk + + # General model + m_1D = np.r_[np.log(sig), np.log(mu), np.log(self.thicknesses)] + + def fwdfun(m): + resp = self.sim.dpred(m) + return resp + # return Hz + + def jacfun(m, dm): + Jvec = self.sim.Jvec(m, dm) + return Jvec + + dm = m_1D * 0.5 + + def derChk(m): + return [fwdfun(m), lambda mx: jacfun(m, mx)] + + passed = tests.check_derivative( + derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15, random_seed=1123 + ) + assert passed + + def test_EM1DFDJtvec_Layers(self): + # Conductivity + sigma_half = 0.01 + sigma_blk = 0.1 + sig = np.ones(self.nlayers) * sigma_half + sig[1] = sigma_blk + + # Permeability + mu_half = mu_0 + mu_blk = 1.1 * mu_0 + mu = np.ones(self.nlayers) * mu_half + mu[1] = mu_blk + + # General model + m_true = np.r_[np.log(sig), np.log(mu), np.log(self.thicknesses)] + + dobs = self.sim.dpred(m_true) + + m_ini = np.r_[ + np.log(np.ones(self.nlayers) * sigma_half), + np.log(np.ones(self.nlayers) * mu_half), + np.log(self.thicknesses) * 0.9, + ] + resp_ini = self.sim.dpred(m_ini) + dr = resp_ini - dobs + + def misfit(m, dobs): + dpred = self.sim.dpred(m) + misfit = np.linalg.norm(dpred - dobs) ** 2 + dmisfit = 2 * self.sim.Jtvec( + m, dr + ) # derivative of ||dpred - dobs||^2 gives factor of 2 + return misfit, dmisfit + + def derChk(m): + return misfit(m, dobs) + + passed = tests.check_derivative( + derChk, m_ini, num=4, plotIt=False, eps=1e-27, random_seed=124 + ) + assert passed diff --git a/tests/em/em1d/test_EM1D_TD_general_jac_layers.py b/tests/em/em1d/test_EM1D_TD_general_jac_layers.py index 0580d5407e..cc9cfddda9 100644 --- a/tests/em/em1d/test_EM1D_TD_general_jac_layers.py +++ b/tests/em/em1d/test_EM1D_TD_general_jac_layers.py @@ -342,7 +342,8 @@ def test_rx_loc_shapes(rx_class, n_locs1, n_locs2, orientation, waveform, compar d = sim.dpred(None) else: sim.sigmaMap = maps.IdentityMap(nP=1) - d = sim.getJ(np.ones(1))["ds"][:, 0] + J = sim.getJ(np.ones(1)) + d = J[:, 0] # assert the shape is correct assert d.shape == (n_d,) diff --git a/tests/em/em1d/test_EM1D_TD_getJ.py b/tests/em/em1d/test_EM1D_TD_getJ.py new file mode 100644 index 0000000000..42273fbedf --- /dev/null +++ b/tests/em/em1d/test_EM1D_TD_getJ.py @@ -0,0 +1,161 @@ +""" +Test the getJ method of FDEM 1D simulation. +""" + +import pytest +import numpy as np +import simpeg.electromagnetics.time_domain as tdem +from simpeg import maps +from scipy.sparse import diags + + +def create_simulation_and_conductivities(identity_mapping: bool): + # Create Survey + # ------------- + # Source properties + source_location = np.array([0.0, 0.0, 20.0]) + source_orientation = "z" # "x", "y" or "z" + source_current = 1.0 # maximum on-time current + source_radius = 6.0 # source loop radius + + # Receiver properties + receiver_location = np.array([0.0, 0.0, 20.0]) + receiver_orientation = "z" # "x", "y" or "z" + times = np.logspace(-5, -2, 31) # time channels (s) + + # Define receiver list. In our case, we have only a single receiver for each source. + # When simulating the response for multiple component and/or field orientations, + # the list consists of multiple receiver objects. + receiver_list = [] + receiver_list.append( + tdem.receivers.PointMagneticFluxDensity( + receiver_location, times, orientation=receiver_orientation + ) + ) + + # Define the source waveform. Here we define a unit step-off. The definition + # of other waveform types is covered in a separate tutorial. + waveform = tdem.sources.StepOffWaveform() + + # Define source list. In our case, we have only a single source. + source_list = [ + tdem.sources.CircularLoop( + receiver_list=receiver_list, + location=source_location, + orientation=source_orientation, + waveform=waveform, + current=source_current, + radius=source_radius, + ) + ] + + # Define the survey + survey = tdem.Survey(source_list) + + # Defining a 1D Layered Earth Model + # --------------------------------- + # Physical properties + background_conductivity = 1e-1 + layer_conductivity = 1e0 + + # Layer thicknesses + thicknesses = np.array([40.0, 40.0]) + n_layer = len(thicknesses) + 1 + + # Conductivities + conductivities = background_conductivity * np.ones(n_layer) + conductivities[1] = layer_conductivity + + # Define a mapping + model_mapping = ( + maps.IdentityMap(nP=n_layer) if identity_mapping else maps.ExpMap(nP=n_layer) + ) + + # Define the Forward Simulation, Predict Data and Plot + # ---------------------------------------------------- + simulation = tdem.Simulation1DLayered( + survey=survey, + thicknesses=thicknesses, + sigmaMap=model_mapping, + ) + + return simulation, conductivities + + +def test_getJ(): + """ + Test if getJ returns different J matrices after passing different maps. + """ + dpreds, jacobians = [], [] + + # Compute dpred and J using an identity map and an exp map + for identity_mapping in (True, False): + simulation, conductivities = create_simulation_and_conductivities( + identity_mapping + ) + model = conductivities if identity_mapping else np.log(conductivities) + dpreds.append(simulation.dpred(model)) + jac = simulation.getJ(model) + jacobians.append(jac) + + # The two dpreds should be equal + assert np.allclose(*dpreds) + + # The two J matrices should not be equal + assert not np.allclose(*jacobians, atol=0.0) + + +@pytest.mark.parametrize("mapping", ["identity", "expmap"]) +def test_JtJdiag(mapping): + """ + Test the getJtJdiag method of the simulation. + """ + identity_mapping = mapping == "identity" + simulation, conductivities = create_simulation_and_conductivities(identity_mapping) + + model = conductivities if identity_mapping else np.log(conductivities) + rng = np.random.default_rng(seed=42) + weights_matrix = diags(rng.random(size=simulation.survey.nD)) + jtj_diag = simulation.getJtJdiag(model, W=weights_matrix) + + J = simulation.getJ(model) + expected = np.diag(J.T @ weights_matrix.T @ weights_matrix @ J) + np.testing.assert_allclose(expected, jtj_diag) + + +@pytest.mark.parametrize("mapping", ["identity", "expmap"]) +def test_Jvec(mapping): + """ + Test the Jvec method of the simulation. + """ + identity_mapping = mapping == "identity" + simulation, conductivities = create_simulation_and_conductivities(identity_mapping) + + model = conductivities if identity_mapping else np.log(conductivities) + rng = np.random.default_rng(seed=42) + vector = rng.random(size=model.size) + jvec = simulation.Jvec(model, vector) + + J = simulation.getJ(model) + expected = J @ vector + + np.testing.assert_allclose(expected, jvec) + + +@pytest.mark.parametrize("mapping", ["identity", "expmap"]) +def test_Jtvec(mapping): + """ + Test the Jtvec method of the simulation. + """ + identity_mapping = mapping == "identity" + simulation, conductivities = create_simulation_and_conductivities(identity_mapping) + + model = conductivities if identity_mapping else np.log(conductivities) + rng = np.random.default_rng(seed=42) + vector = rng.random(size=simulation.survey.nD) + jtvec = simulation.Jtvec(model, vector) + + J = simulation.getJ(model) + expected = J.T @ vector + + np.testing.assert_allclose(expected, jtvec) From 249e69a880c33a1554f7368397b7bc0051a4fef8 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 24 Apr 2025 19:18:13 -0700 Subject: [PATCH 136/194] Add release notes for v0.24.0 (#1655) Add release notes for SimPEG v0.24.0, and update the version switcher. --- docs/_static/versions.json | 11 +- docs/content/release/0.24.0-notes.rst | 232 ++++++++++++++++++++++++++ docs/content/release/index.rst | 1 + 3 files changed, 241 insertions(+), 3 deletions(-) create mode 100644 docs/content/release/0.24.0-notes.rst diff --git a/docs/_static/versions.json b/docs/_static/versions.json index 665a00a498..b35427db2c 100644 --- a/docs/_static/versions.json +++ b/docs/_static/versions.json @@ -4,11 +4,16 @@ "url": "https://docs.simpeg.xyz/dev/" }, { - "name": "v0.23.0 (latest)", - "version": "0.23.0", - "url": "https://docs.simpeg.xyz/v0.23.0/", + "name": "v0.24.0 (latest)", + "version": "0.24.0", + "url": "https://docs.simpeg.xyz/v0.24.0/", "preferred": true }, + { + "name": "v0.23.0", + "version": "0.23.0", + "url": "https://docs.simpeg.xyz/v0.23.0/" + }, { "name": "v0.22.2", "version": "0.22.2", diff --git a/docs/content/release/0.24.0-notes.rst b/docs/content/release/0.24.0-notes.rst new file mode 100644 index 0000000000..3e24893612 --- /dev/null +++ b/docs/content/release/0.24.0-notes.rst @@ -0,0 +1,232 @@ +.. _0.24.0_notes: + +=========================== +SimPEG 0.24.0 Release Notes +=========================== + +April 24th, 2025 + +.. contents:: Highlights + :depth: 3 + +Updates +======= + +New features +------------ + +Speed up of dot products involved in PGI +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This release includes optimizations of some dot products carried out in the +:class:`~simpeg.regularization.pgi.PGIsmallness`. They significantly reduce the +computation time of Petrophysically and Geologically Guided Inversions (PGI). + +Specifically, these changes optimize the dot products involved when evaluating +the regularization function itself and its derivatives. The optimization takes +advantage of the :func:`numpy.einsum` function. + +See https://github.com/simpeg/simpeg/pull/1587 and +https://github.com/simpeg/simpeg/pull/1588 for more information. + + +Potential field sensitivity matrices as Linear Operators +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The gravity and magnetic field simulations are now capable of building the +sensitivity matrix ``G`` as a SciPy +:class:`~scipy.sparse.linalg.LinearOperator` object when the +``store_sensitivities`` argument is set to ``"forward_only"``. +The :class:`~scipy.sparse.linalg.LinearOperator` objects +can be used to compute the dot product with any vector (``G +@ v``), or the dot product of their transpose (``G.T @ v``) as if they were +arrays, although the dense matrix is never fully built nor allocated in memory. +Instead, the forward computation is carried out whenever a dot product is +requested. + +This change allows to compute the simulation derivatives without requiring +large amount of memory to store large sensitivity matrices, enabling users to +run inversions of large models where the sensitivity matrix is larger than the +available memory. + +Using methods like +:meth:`~simpeg.potential_fields.gravity.Simulation3DIntegral.Jvec`, +:meth:`~simpeg.potential_fields.gravity.Simulation3DIntegral.Jtvec`, +and +:meth:`~simpeg.potential_fields.gravity.Simulation3DIntegral.getJtJdiag`, make +use of +:attr:`~simpeg.potential_fields.gravity.Simulation3DIntegral.G` +a linear operator when ``store_sensitivities="forward_only"``. +Meanwhile, the +:meth:`~simpeg.potential_fields.gravity.Simulation3DIntegral.getJ` +method returns a composite +:class:`~scipy.sparse.linalg.LinearOperator` object that can also be used to +compute dot products with any vector. + +See https://github.com/simpeg/simpeg/pull/1622 and +https://github.com/simpeg/simpeg/pull/1634 for more information. + +Move indexing of arrays from :class:`simpeg.data.Data` to Surveys +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +We moved the indexing capabilities of the :class:`~simpeg.data.Data` objects to +the different ``Survey`` objects. This is useful in case we have some data as +a flat array that is related to a particular survey (or combination of sources +and receivers), and we want to obtain the data values associated to +a particular pair of source and receiver. + +With this change, we don't need to define a new :class:`~simpeg.data.Data` +object to slice an array, we can use the ``Survey`` itself. +For example, let's say we have a survey with two sources, and three receivers +each: + +.. code:: python + + receivers_a = [Recevier([[-2, 0]]), Recevier([[0, 0]]), Recevier([[2, 0]])] + source_a = Source(receiver_list=receivers_a) + receivers_b = [Recevier([[3, 1]]), Recevier([[4, 1]]), Recevier([[5, 1]])] + source_b = Source(receiver_list=receivers_b) + survey = Survey(source_list=[source_a, source_b]) + +And we have a ``dobs`` array that corresponds to this survey. We can obtain the +values of the ``dobs`` array associated with the second receiver and the first +source by using the ``get_slice`` method to obtain a ``slice`` object, and then +use it to index the ``dobs`` array: + +.. code:: python + + slice_obj = survey.get_slice(source_a, receivers_a[1]) + dobs_slice = dobs[slice_obj] + +See https://github.com/simpeg/simpeg/pull/1616 and +https://github.com/simpeg/simpeg/pull/1632 for more information. + +Documentation +------------- + +The documentation pages have been reorganized, merging the _Getting Started_ +section into the :ref:`User Guide `. +This change makes it easier to navigate through the different documentation +pages, with the assistance of a table of contents on the side. + +We updated the :ref:`installation instructions `, with `Miniforge +`_ as the recommended Python +distribution. + +We have also improved the documentation of some classes and methods. + + +Bugfixes +-------- + +This release includes a list of bug fixes. We solved issues related to the +``getJ`` method of the DC, SIP, TDEM, and FDEM simulations. The EM1D +simulations can now work with receivers objects with multiple locations. +The :class:`~simpeg.data_misfit.BaseDataMisfit` class and its children raise errors in case the +simulation is retuning non-numeric values as output. + +We have also improved some of the error messages that users get when things +don't work as expected, aiming to catch those mistakes earlier than late. + +Contributors +============ + +Contributors + +- `@ghwilliams `__ +- `@jcapriot `__ +- `@johnweis0480 `__ +- `@lheagy `__ +- `@santisoler `__ + + +Pull Requests +============= + +- Bugfix for TDEM magnetic dipole sources by `@lheagy `__ in + https://github.com/simpeg/simpeg/pull/1572 +- Fix ubcstyle printout by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1577 +- Add docstring to ``n_processes`` in potential field simulations by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1578 +- Move simulation solver from base simulation to PDE simulation by + `@jcapriot `__ in https://github.com/simpeg/simpeg/pull/1582 +- Update and fix instructions to build the docs by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1583 +- Change location of ``mesh`` attribute by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1585 +- Speed up most commonly used deriv/deriv2 in PGI by `@johnweis0480 `__ in + https://github.com/simpeg/simpeg/pull/1587 +- Improve dot products in ``PGIsmallness.__call__`` and update docstring + by `@johnweis0480 `__ in https://github.com/simpeg/simpeg/pull/1588 +- Rename delete on model update by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1589 +- update PGI Example plotting script for deprecated collections by + `@jcapriot `__ in https://github.com/simpeg/simpeg/pull/1595 +- Coverage upload on failed test by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1596 +- Use zizmor to lint GitHub Actions workflows by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1592 +- Update installation instructions in docs by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1597 +- Set ``permissions`` in Actions to avoid zizmor’s + ``excessive-permissions`` by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1602 +- Fix for removed quadrature function on new scipy versions by `@jcapriot `__ + in https://github.com/simpeg/simpeg/pull/1603 +- Install zizmor through conda-forge in ``environment.yml`` by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1600 +- Raise errors if dpred in ``BaseDataMisfit`` has nans by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1615 +- Update Black’s Python versions in ``pyproject.toml`` by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1620 +- Use shell rendering in Bug report template by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1612 +- Merge Getting Started and Examples into User Guide by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1619 +- Fix usage of “bug” label in bug report template by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1624 +- Fix redirects links in docs by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1623 +- Fix bug on ``getJ`` of gravity simulation by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1621 +- Fix redirect to user guide index page by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1627 +- Move indexing of flat arrays to Survey classes by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1616 +- Replace Data indexing for Survey slicing where needed by `@santisoler `__ + in https://github.com/simpeg/simpeg/pull/1632 +- Implement ``G`` matrix as ``LinearOperator`` in gravity simulation by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1622 +- Set maximum number of iterations in eq sources tests by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1636 +- Em1d multiple rx locs by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1637 +- Fix definition of model in gravity J-related tests by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1643 +- Improve docstring of dip_azimuth2cartesian by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1642 +- Improve variable names in gravity test by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1641 +- Test transpose of gravity getJ as linear operator by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1644 +- Configure zizmor to pin reviewdog actions with tags by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1650 +- Deprecate ``components`` in potential field surveys by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1633 +- Fix bug on magnetic simulation ``nD`` property by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1646 +- Make pytest error on random seeded test by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1598 +- Add support for potential fields survey indexing by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1635 +- Implement magnetic G as linear operator by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1634 +- Use Numpy’s RNG in tests for depth weighting by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1570 +- Raise NotImplementedError on getJ for NSEM 1D simulations by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1653 +- Set the model when calling ``getJ`` in DC and SIP simulations by + `@lheagy `__ in https://github.com/simpeg/simpeg/pull/1361 +- Fix ``getJ`` method in TDEM and FDEM 1D simulations by `@ghwilliams `__ in + https://github.com/simpeg/simpeg/pull/1638 diff --git a/docs/content/release/index.rst b/docs/content/release/index.rst index bb9bef3492..0c954726f6 100644 --- a/docs/content/release/index.rst +++ b/docs/content/release/index.rst @@ -5,6 +5,7 @@ Release Notes .. toctree:: :maxdepth: 2 + 0.24.0 <0.24.0-notes> 0.23.0 <0.23.0-notes> 0.22.2 <0.22.2-notes> 0.22.1 <0.22.1-notes> From 255d2fb41260f5043f967d28931633a77ecaf1e2 Mon Sep 17 00:00:00 2001 From: William Davis <38541020+williamjsdavis@users.noreply.github.com> Date: Wed, 14 May 2025 09:44:54 -0700 Subject: [PATCH 137/194] Update docstring descriptions for gravity gradient component guv (#1665) Adds a description and units for gravity UV component in docstrings. --- simpeg/potential_fields/gravity/receivers.py | 4 ++-- simpeg/potential_fields/gravity/simulation.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/simpeg/potential_fields/gravity/receivers.py b/simpeg/potential_fields/gravity/receivers.py index de54848cf5..c6318b021a 100644 --- a/simpeg/potential_fields/gravity/receivers.py +++ b/simpeg/potential_fields/gravity/receivers.py @@ -20,7 +20,7 @@ class Point(survey.BaseRx): .. important:: - Gradient components ("gxx", "gyy", "gzz", "gxy", "gxz", "gyz") are + Gradient components ("gxx", "gyy", "gzz", "gxy", "gxz", "gyz", "guv") are returned in Eotvos (:math:`10^{-9} s^{-2}`). Parameters @@ -41,7 +41,7 @@ class Point(survey.BaseRx): - "gyy" --> y-derivative of the y-component - "gyz" --> z-derivative of the y-component (and visa versa) - "gzz" --> z-derivative of the z-component - - "guv" --> UV component + - "guv" --> UV component, i.e., (gyy - gxx) / 2 See also -------- diff --git a/simpeg/potential_fields/gravity/simulation.py b/simpeg/potential_fields/gravity/simulation.py index 9df88fbe74..b6d4f50917 100644 --- a/simpeg/potential_fields/gravity/simulation.py +++ b/simpeg/potential_fields/gravity/simulation.py @@ -147,7 +147,7 @@ class Simulation3DIntegral(BasePFSimulation): .. important:: - Gradient components ("gxx", "gyy", "gzz", "gxy", "gxz", "gyz") are + Gradient components ("gxx", "gyy", "gzz", "gxy", "gxz", "gyz", "guv") are returned in Eotvos (:math:`10^{-9} s^{-2}`). Parameters From 50eeac82c80b56c8887e1821421fe83405f3b697 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 15 May 2025 16:27:02 +0000 Subject: [PATCH 138/194] Clean up Numba functions for potential field simulations (#1663) Declutter the potential field simulations by defining dictionaries with Numba functions for gravity and magnetics. This way the simulations don't need to define private attributes for each one of them: they can just get the desired function in a single line. Raise `NotImplementedError` on diagonal of G.T @ G when `"forward_only"` and using `geoana` as engine. Specify which type of error should be raised in the `xfail` parameters present in some tests. --- .../gravity/_numba_functions.py | 44 ++-- simpeg/potential_fields/gravity/simulation.py | 95 ++++----- .../magnetics/_numba_functions.py | 138 ++++++++----- .../potential_fields/magnetics/simulation.py | 190 +++++++----------- tests/pf/test_forward_Grav_Linear.py | 22 +- tests/pf/test_forward_Mag_Linear.py | 23 ++- 6 files changed, 269 insertions(+), 243 deletions(-) diff --git a/simpeg/potential_fields/gravity/_numba_functions.py b/simpeg/potential_fields/gravity/_numba_functions.py index b43a45dd73..5b1ecc91a6 100644 --- a/simpeg/potential_fields/gravity/_numba_functions.py +++ b/simpeg/potential_fields/gravity/_numba_functions.py @@ -639,20 +639,30 @@ def _sensitivity_gravity_2d_mesh( ) -# Define decorated versions of these functions -_sensitivity_gravity_parallel = jit(nopython=True, parallel=True)(_sensitivity_gravity) -_sensitivity_gravity_serial = jit(nopython=True, parallel=False)(_sensitivity_gravity) -_forward_gravity_parallel = jit(nopython=True, parallel=True)(_forward_gravity) -_forward_gravity_serial = jit(nopython=True, parallel=False)(_forward_gravity) -_forward_gravity_2d_mesh_serial = jit(nopython=True, parallel=False)( - _forward_gravity_2d_mesh -) -_forward_gravity_2d_mesh_parallel = jit(nopython=True, parallel=True)( - _forward_gravity_2d_mesh -) -_sensitivity_gravity_2d_mesh_serial = jit(nopython=True, parallel=False)( - _sensitivity_gravity_2d_mesh -) -_sensitivity_gravity_2d_mesh_parallel = jit(nopython=True, parallel=True)( - _sensitivity_gravity_2d_mesh -) +# Define a dictionary with decorated versions of the Numba functions. +NUMBA_FUNCTIONS = { + "sensitivity": { + parallel: jit(nopython=True, parallel=parallel)(_sensitivity_gravity) + for parallel in (True, False) + }, + "forward": { + parallel: jit(nopython=True, parallel=parallel)(_forward_gravity) + for parallel in (True, False) + }, + "sensitivity_2d_mesh": { + parallel: jit(nopython=True, parallel=parallel)(_sensitivity_gravity_2d_mesh) + for parallel in (True, False) + }, + "forward_2d_mesh": { + parallel: jit(nopython=True, parallel=parallel)(_forward_gravity_2d_mesh) + for parallel in (True, False) + }, + "diagonal_gtg": { + False: _diagonal_G_T_dot_G_serial, + True: _diagonal_G_T_dot_G_parallel, + }, + "gt_dot_v": { + False: _sensitivity_gravity_t_dot_v_serial, + True: _sensitivity_gravity_t_dot_v_parallel, + }, +} diff --git a/simpeg/potential_fields/gravity/simulation.py b/simpeg/potential_fields/gravity/simulation.py index b6d4f50917..574612f90f 100644 --- a/simpeg/potential_fields/gravity/simulation.py +++ b/simpeg/potential_fields/gravity/simulation.py @@ -14,21 +14,7 @@ from ...base import BasePDESimulation from ..base import BaseEquivalentSourceLayerSimulation, BasePFSimulation -from ._numba_functions import ( - choclo, - _sensitivity_gravity_serial, - _sensitivity_gravity_parallel, - _forward_gravity_serial, - _forward_gravity_parallel, - _sensitivity_gravity_t_dot_v_serial, - _sensitivity_gravity_t_dot_v_parallel, - _forward_gravity_2d_mesh_serial, - _forward_gravity_2d_mesh_parallel, - _sensitivity_gravity_2d_mesh_serial, - _sensitivity_gravity_2d_mesh_parallel, - _diagonal_G_T_dot_G_serial, - _diagonal_G_T_dot_G_parallel, -) +from ._numba_functions import choclo, NUMBA_FUNCTIONS try: from warnings import deprecated @@ -217,19 +203,6 @@ def __init__( ) self.n_processes = None - # Define jit functions - if self.engine == "choclo": - if self.numba_parallel: - self._sensitivity_gravity = _sensitivity_gravity_parallel - self._forward_gravity = _forward_gravity_parallel - self._sensitivity_t_dot_v = _sensitivity_gravity_t_dot_v_parallel - self._diagonal_G_T_dot_G = _diagonal_G_T_dot_G_parallel - else: - self._sensitivity_gravity = _sensitivity_gravity_serial - self._forward_gravity = _forward_gravity_serial - self._sensitivity_t_dot_v = _sensitivity_gravity_t_dot_v_serial - self._diagonal_G_T_dot_G = _diagonal_G_T_dot_G_serial - def fields(self, m): """ Forward model the gravity field of the mesh on the receivers in the survey @@ -332,14 +305,24 @@ def _get_gtg_diagonal(self, weights: NDArray) -> NDArray: ------- np.ndarray """ - gtg_diagonal = ( - self._gtg_diagonal_without_building_g(weights) - if self.store_sensitivities == "forward_only" - else - # In Einstein notation, the j-th element of the diagonal is: - # d_j = w_i * G_{ij} * G_{ij} - np.asarray(np.einsum("i,ij,ij->j", weights, self.G, self.G)) - ) + match self.store_sensitivities, self.engine: + case ("forward_only", "geoana"): + msg = ( + "Computing the diagonal of G.T @ G with " + 'store_sensitivities="forward_only" and engine="geoana" ' + "hasn't been implemented yet." + 'Choose store_sensitivities="ram" or "disk", ' + 'or another engine, like "choclo".' + ) + raise NotImplementedError(msg) + case ("forward_only", "choclo"): + gtg_diagonal = self._gtg_diagonal_without_building_g(weights) + case (_, _): + # In Einstein notation, the j-th element of the diagonal is: + # d_j = w_i * G_{ij} * G_{ij} + gtg_diagonal = np.asarray( + np.einsum("i,ij,ij->j", weights, self.G, self.G) + ) return gtg_diagonal def getJ(self, m, f=None) -> NDArray[np.float64 | np.float32] | LinearOperator: @@ -592,6 +575,8 @@ def _forward(self, densities): (nD,) numpy.ndarray Always return a ``np.float64`` array. """ + # Get Numba function + forward_func = NUMBA_FUNCTIONS["forward"][self.numba_parallel] # Gather active nodes and the indices of the nodes for each active cell active_nodes, active_cell_nodes = self._get_active_nodes() # Allocate fields array @@ -607,7 +592,7 @@ def _forward(self, densities): vector_slice = slice( index_offset + i, index_offset + n_elements, n_components ) - self._forward_gravity( + forward_func( receivers, active_nodes, densities, @@ -627,6 +612,8 @@ def _sensitivity_matrix(self): ------- (nD, n_active_cells) numpy.ndarray """ + # Get Numba function + sensitivity_func = NUMBA_FUNCTIONS["sensitivity"][self.numba_parallel] # Gather active nodes and the indices of the nodes for each active cell active_nodes, active_cell_nodes = self._get_active_nodes() # Allocate sensitivity matrix @@ -652,7 +639,7 @@ def _sensitivity_matrix(self): matrix_slice = slice( index_offset + i, index_offset + n_rows, n_components ) - self._sensitivity_gravity( + sensitivity_func( receivers, active_nodes, sensitivity_matrix[matrix_slice, :], @@ -676,6 +663,8 @@ def _sensitivity_matrix_transpose_dot_vec(self, vector): ------- (n_active_cells) numpy.ndarray """ + # Get Numba function + sensitivity_t_dot_v_func = NUMBA_FUNCTIONS["gt_dot_v"][self.numba_parallel] # Gather active nodes and the indices of the nodes for each active cell active_nodes, active_cell_nodes = self._get_active_nodes() # Allocate resulting array @@ -691,7 +680,7 @@ def _sensitivity_matrix_transpose_dot_vec(self, vector): vector_slice = slice( index_offset + i, index_offset + n_rows, n_components ) - self._sensitivity_t_dot_v( + sensitivity_t_dot_v_func( receivers, active_nodes, active_cell_nodes, @@ -734,6 +723,8 @@ def _gtg_diagonal_without_building_g(self, weights): ------- (n_active_cells) numpy.ndarray """ + # Get Numba function + diagonal_gtg_func = NUMBA_FUNCTIONS["diagonal_gtg"][self.numba_parallel] # Gather active nodes and the indices of the nodes for each active cell active_nodes, active_cell_nodes = self._get_active_nodes() # Allocate array for the diagonal of G.T @ G @@ -743,7 +734,7 @@ def _gtg_diagonal_without_building_g(self, weights): for component in components: kernel_func = CHOCLO_KERNELS[component] conversion_factor = _get_conversion_factor(component) - self._diagonal_G_T_dot_G( + diagonal_gtg_func( receivers, active_nodes, active_cell_nodes, @@ -797,14 +788,6 @@ def __init__( **kwargs, ) - if self.engine == "choclo": - if self.numba_parallel: - self._sensitivity_gravity = _sensitivity_gravity_2d_mesh_parallel - self._forward_gravity = _forward_gravity_2d_mesh_parallel - else: - self._sensitivity_gravity = _sensitivity_gravity_2d_mesh_serial - self._forward_gravity = _forward_gravity_2d_mesh_serial - def _forward(self, densities): """ Forward model the fields of active cells in the mesh on receivers. @@ -820,6 +803,8 @@ def _forward(self, densities): (nD,) numpy.ndarray Always return a ``np.float64`` array. """ + # Get Numba function + forward_func = NUMBA_FUNCTIONS["forward_2d_mesh"][self.numba_parallel] # Get cells in the 2D mesh and keep only active cells cells_bounds_active = self.mesh.cell_bounds[self.active_cells] # Allocate fields array @@ -830,19 +815,19 @@ def _forward(self, densities): n_components = len(components) n_elements = n_components * receivers.shape[0] for i, component in enumerate(components): - forward_func = CHOCLO_FORWARD_FUNCS[component] + choclo_forward_func = CHOCLO_FORWARD_FUNCS[component] conversion_factor = _get_conversion_factor(component) vector_slice = slice( index_offset + i, index_offset + n_elements, n_components ) - self._forward_gravity( + forward_func( receivers, cells_bounds_active, self.cell_z_top, self.cell_z_bottom, densities, fields[vector_slice], - forward_func, + choclo_forward_func, conversion_factor, ) index_offset += n_elements @@ -856,6 +841,8 @@ def _sensitivity_matrix(self): ------- (nD, n_active_cells) numpy.ndarray """ + # Get Numba function + sensitivity_func = NUMBA_FUNCTIONS["sensitivity_2d_mesh"][self.numba_parallel] # Get cells in the 2D mesh and keep only active cells cells_bounds_active = self.mesh.cell_bounds[self.active_cells] # Allocate sensitivity matrix @@ -876,18 +863,18 @@ def _sensitivity_matrix(self): n_components = len(components) n_rows = n_components * receivers.shape[0] for i, component in enumerate(components): - forward_func = CHOCLO_FORWARD_FUNCS[component] + choclo_forward_func = CHOCLO_FORWARD_FUNCS[component] conversion_factor = _get_conversion_factor(component) matrix_slice = slice( index_offset + i, index_offset + n_rows, n_components ) - self._sensitivity_gravity( + sensitivity_func( receivers, cells_bounds_active, self.cell_z_top, self.cell_z_bottom, sensitivity_matrix[matrix_slice, :], - forward_func, + choclo_forward_func, conversion_factor, ) index_offset += n_rows diff --git a/simpeg/potential_fields/magnetics/_numba_functions.py b/simpeg/potential_fields/magnetics/_numba_functions.py index 031b057a22..43e685dbf9 100644 --- a/simpeg/potential_fields/magnetics/_numba_functions.py +++ b/simpeg/potential_fields/magnetics/_numba_functions.py @@ -3292,51 +3292,93 @@ def _sensitivity_tmi_derivative_2d_mesh( ) -_sensitivity_tmi_serial = jit(nopython=True, parallel=False)(_sensitivity_tmi) -_sensitivity_tmi_parallel = jit(nopython=True, parallel=True)(_sensitivity_tmi) -_forward_tmi_serial = jit(nopython=True, parallel=False)(_forward_tmi) -_forward_tmi_parallel = jit(nopython=True, parallel=True)(_forward_tmi) -_forward_mag_serial = jit(nopython=True, parallel=False)(_forward_mag) -_forward_mag_parallel = jit(nopython=True, parallel=True)(_forward_mag) -_sensitivity_mag_serial = jit(nopython=True, parallel=False)(_sensitivity_mag) -_sensitivity_mag_parallel = jit(nopython=True, parallel=True)(_sensitivity_mag) -_forward_tmi_derivative_parallel = jit(nopython=True, parallel=True)( - _forward_tmi_derivative -) -_forward_tmi_derivative_serial = jit(nopython=True, parallel=False)( - _forward_tmi_derivative -) -_sensitivity_tmi_derivative_parallel = jit(nopython=True, parallel=True)( - _sensitivity_tmi_derivative -) -_sensitivity_tmi_derivative_serial = jit(nopython=True, parallel=False)( - _sensitivity_tmi_derivative -) -_forward_tmi_2d_mesh_serial = jit(nopython=True, parallel=False)(_forward_tmi_2d_mesh) -_forward_tmi_2d_mesh_parallel = jit(nopython=True, parallel=True)(_forward_tmi_2d_mesh) -_forward_mag_2d_mesh_serial = jit(nopython=True, parallel=False)(_forward_mag_2d_mesh) -_forward_mag_2d_mesh_parallel = jit(nopython=True, parallel=True)(_forward_mag_2d_mesh) -_forward_tmi_derivative_2d_mesh_serial = jit(nopython=True, parallel=False)( - _forward_tmi_derivative_2d_mesh -) -_forward_tmi_derivative_2d_mesh_parallel = jit(nopython=True, parallel=True)( - _forward_tmi_derivative_2d_mesh -) -_sensitivity_mag_2d_mesh_serial = jit(nopython=True, parallel=False)( - _sensitivity_mag_2d_mesh -) -_sensitivity_mag_2d_mesh_parallel = jit(nopython=True, parallel=True)( - _sensitivity_mag_2d_mesh -) -_sensitivity_tmi_2d_mesh_serial = jit(nopython=True, parallel=False)( - _sensitivity_tmi_2d_mesh -) -_sensitivity_tmi_2d_mesh_parallel = jit(nopython=True, parallel=True)( - _sensitivity_tmi_2d_mesh -) -_sensitivity_tmi_derivative_2d_mesh_serial = jit(nopython=True, parallel=False)( - _sensitivity_tmi_derivative_2d_mesh -) -_sensitivity_tmi_derivative_2d_mesh_parallel = jit(nopython=True, parallel=True)( - _sensitivity_tmi_derivative_2d_mesh -) +NUMBA_FUNCTIONS = { + "forward": { + "tmi": { + parallel: jit(nopython=True, parallel=parallel)(_forward_tmi) + for parallel in (True, False) + }, + "magnetic_component": { + parallel: jit(nopython=True, parallel=parallel)(_forward_mag) + for parallel in (True, False) + }, + "tmi_derivative": { + parallel: jit(nopython=True, parallel=parallel)(_forward_tmi_derivative) + for parallel in (True, False) + }, + }, + "sensitivity": { + "tmi": { + parallel: jit(nopython=True, parallel=parallel)(_sensitivity_tmi) + for parallel in (True, False) + }, + "magnetic_component": { + parallel: jit(nopython=True, parallel=parallel)(_sensitivity_mag) + for parallel in (True, False) + }, + "tmi_derivative": { + parallel: jit(nopython=True, parallel=parallel)(_sensitivity_tmi_derivative) + for parallel in (True, False) + }, + }, + "forward_2d_mesh": { + "tmi": { + parallel: jit(nopython=True, parallel=parallel)(_forward_tmi_2d_mesh) + for parallel in (True, False) + }, + "magnetic_component": { + parallel: jit(nopython=True, parallel=parallel)(_forward_mag_2d_mesh) + for parallel in (True, False) + }, + "tmi_derivative": { + parallel: jit(nopython=True, parallel=parallel)( + _forward_tmi_derivative_2d_mesh + ) + for parallel in (True, False) + }, + }, + "sensitivity_2d_mesh": { + "tmi": { + parallel: jit(nopython=True, parallel=parallel)(_sensitivity_tmi_2d_mesh) + for parallel in (True, False) + }, + "magnetic_component": { + parallel: jit(nopython=True, parallel=parallel)(_sensitivity_mag_2d_mesh) + for parallel in (True, False) + }, + "tmi_derivative": { + parallel: jit(nopython=True, parallel=parallel)( + _sensitivity_tmi_derivative_2d_mesh + ) + for parallel in (True, False) + }, + }, + "gt_dot_v": { + "tmi": { + False: _tmi_sensitivity_t_dot_v_serial, + True: _tmi_sensitivity_t_dot_v_parallel, + }, + "magnetic_component": { + False: _mag_sensitivity_t_dot_v_serial, + True: _mag_sensitivity_t_dot_v_parallel, + }, + "tmi_derivative": { + False: _tmi_derivative_sensitivity_t_dot_v_serial, + True: _tmi_derivative_sensitivity_t_dot_v_parallel, + }, + }, + "diagonal_gtg": { + "tmi": { + False: _diagonal_G_T_dot_G_tmi_serial, + True: _diagonal_G_T_dot_G_tmi_parallel, + }, + "magnetic_component": { + False: _diagonal_G_T_dot_G_mag_serial, + True: _diagonal_G_T_dot_G_mag_parallel, + }, + "tmi_derivative": { + False: _diagonal_G_T_dot_G_tmi_deriv_serial, + True: _diagonal_G_T_dot_G_tmi_deriv_serial, + }, + }, +} diff --git a/simpeg/potential_fields/magnetics/simulation.py b/simpeg/potential_fields/magnetics/simulation.py index 2f3314dc3c..e789bb5a32 100644 --- a/simpeg/potential_fields/magnetics/simulation.py +++ b/simpeg/potential_fields/magnetics/simulation.py @@ -25,45 +25,7 @@ from .analytics import CongruousMagBC from .survey import Survey -from ._numba_functions import ( - choclo, - _sensitivity_tmi_parallel, - _sensitivity_tmi_serial, - _sensitivity_mag_parallel, - _sensitivity_mag_serial, - _forward_tmi_parallel, - _forward_tmi_serial, - _forward_mag_parallel, - _forward_mag_serial, - _forward_tmi_2d_mesh_serial, - _forward_tmi_2d_mesh_parallel, - _forward_mag_2d_mesh_serial, - _forward_mag_2d_mesh_parallel, - _forward_tmi_derivative_2d_mesh_serial, - _forward_tmi_derivative_2d_mesh_parallel, - _sensitivity_mag_2d_mesh_serial, - _sensitivity_mag_2d_mesh_parallel, - _sensitivity_tmi_2d_mesh_serial, - _sensitivity_tmi_2d_mesh_parallel, - _forward_tmi_derivative_parallel, - _forward_tmi_derivative_serial, - _sensitivity_tmi_derivative_parallel, - _sensitivity_tmi_derivative_serial, - _sensitivity_tmi_derivative_2d_mesh_serial, - _sensitivity_tmi_derivative_2d_mesh_parallel, - _mag_sensitivity_t_dot_v_serial, - _mag_sensitivity_t_dot_v_parallel, - _tmi_sensitivity_t_dot_v_serial, - _tmi_sensitivity_t_dot_v_parallel, - _tmi_derivative_sensitivity_t_dot_v_serial, - _tmi_derivative_sensitivity_t_dot_v_parallel, - _diagonal_G_T_dot_G_mag_serial, - _diagonal_G_T_dot_G_mag_parallel, - _diagonal_G_T_dot_G_tmi_serial, - _diagonal_G_T_dot_G_tmi_parallel, - _diagonal_G_T_dot_G_tmi_deriv_serial, - _diagonal_G_T_dot_G_tmi_deriv_parallel, -) +from ._numba_functions import choclo, NUMBA_FUNCTIONS if choclo is not None: CHOCLO_SUPPORTED_COMPONENTS = { @@ -234,42 +196,6 @@ def __init__( ) self.n_processes = None - if self.engine == "choclo": - if self.numba_parallel: - self._sensitivity_tmi = _sensitivity_tmi_parallel - self._sensitivity_mag = _sensitivity_mag_parallel - self._forward_tmi = _forward_tmi_parallel - self._forward_mag = _forward_mag_parallel - self._forward_tmi_derivative = _forward_tmi_derivative_parallel - self._sensitivity_tmi_derivative = _sensitivity_tmi_derivative_parallel - self._mag_sensitivity_t_dot_v = _mag_sensitivity_t_dot_v_parallel - self._tmi_sensitivity_t_dot_v = _tmi_sensitivity_t_dot_v_parallel - self._tmi_derivative_sensitivity_t_dot_v = ( - _tmi_derivative_sensitivity_t_dot_v_parallel - ) - self._diagonal_G_T_dot_G_mag = _diagonal_G_T_dot_G_mag_parallel - self._diagonal_G_T_dot_G_tmi = _diagonal_G_T_dot_G_tmi_parallel - self._diagonal_G_T_dot_G_tmi_deriv = ( - _diagonal_G_T_dot_G_tmi_deriv_parallel - ) - else: - self._sensitivity_tmi = _sensitivity_tmi_serial - self._sensitivity_mag = _sensitivity_mag_serial - self._forward_tmi = _forward_tmi_serial - self._forward_mag = _forward_mag_serial - self._forward_tmi_derivative = _forward_tmi_derivative_serial - self._sensitivity_tmi_derivative = _sensitivity_tmi_derivative_serial - self._mag_sensitivity_t_dot_v = _mag_sensitivity_t_dot_v_serial - self._tmi_sensitivity_t_dot_v = _tmi_sensitivity_t_dot_v_serial - self._tmi_derivative_sensitivity_t_dot_v = ( - _tmi_derivative_sensitivity_t_dot_v_serial - ) - self._diagonal_G_T_dot_G_mag = _diagonal_G_T_dot_G_mag_serial - self._diagonal_G_T_dot_G_tmi = _diagonal_G_T_dot_G_tmi_serial - self._diagonal_G_T_dot_G_tmi_deriv = ( - _diagonal_G_T_dot_G_tmi_deriv_serial - ) - @property def model_type(self): """Type of magnetization model @@ -956,7 +882,10 @@ def _forward(self, model): index_offset + i, index_offset + n_rows, n_components ) if component == "tmi": - self._forward_tmi( + forward_func = NUMBA_FUNCTIONS["forward"]["tmi"][ + self.numba_parallel + ] + forward_func( receivers, active_nodes, model, @@ -970,7 +899,10 @@ def _forward(self, model): kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz = ( CHOCLO_KERNELS[component] ) - self._forward_tmi_derivative( + forward_func = NUMBA_FUNCTIONS["forward"]["tmi_derivative"][ + self.numba_parallel + ] + forward_func( receivers, active_nodes, model, @@ -988,7 +920,10 @@ def _forward(self, model): ) else: kernel_x, kernel_y, kernel_z = CHOCLO_KERNELS[component] - self._forward_mag( + forward_func = NUMBA_FUNCTIONS["forward"]["magnetic_component"][ + self.numba_parallel + ] + forward_func( receivers, active_nodes, model, @@ -1047,7 +982,10 @@ def _sensitivity_matrix(self): index_offset + i, index_offset + n_rows, n_components ) if component == "tmi": - self._sensitivity_tmi( + sensitivity_func = NUMBA_FUNCTIONS["sensitivity"]["tmi"][ + self.numba_parallel + ] + sensitivity_func( receivers, active_nodes, sensitivity_matrix[matrix_slice, :], @@ -1057,10 +995,13 @@ def _sensitivity_matrix(self): scalar_model, ) elif component in ("tmi_x", "tmi_y", "tmi_z"): + sensitivity_func = NUMBA_FUNCTIONS["sensitivity"]["tmi_derivative"][ + self.numba_parallel + ] kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz = ( CHOCLO_KERNELS[component] ) - self._sensitivity_tmi_derivative( + sensitivity_func( receivers, active_nodes, sensitivity_matrix[matrix_slice, :], @@ -1076,8 +1017,11 @@ def _sensitivity_matrix(self): scalar_model, ) else: + sensitivity_func = NUMBA_FUNCTIONS["sensitivity"][ + "magnetic_component" + ][self.numba_parallel] kernel_x, kernel_y, kernel_z = CHOCLO_KERNELS[component] - self._sensitivity_mag( + sensitivity_func( receivers, active_nodes, sensitivity_matrix[matrix_slice, :], @@ -1150,7 +1094,10 @@ def _sensitivity_matrix_transpose_dot_vec(self, vector): index_offset + i, index_offset + n_rows, n_components ) if component == "tmi": - self._tmi_sensitivity_t_dot_v( + gt_dot_v_func = NUMBA_FUNCTIONS["gt_dot_v"]["tmi"][ + self.numba_parallel + ] + gt_dot_v_func( receivers, active_nodes, active_cell_nodes, @@ -1164,7 +1111,10 @@ def _sensitivity_matrix_transpose_dot_vec(self, vector): kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz = ( CHOCLO_KERNELS[component] ) - self._tmi_derivative_sensitivity_t_dot_v( + gt_dot_v_func = NUMBA_FUNCTIONS["gt_dot_v"]["tmi_derivative"][ + self.numba_parallel + ] + gt_dot_v_func( receivers, active_nodes, active_cell_nodes, @@ -1182,7 +1132,10 @@ def _sensitivity_matrix_transpose_dot_vec(self, vector): ) else: kernel_x, kernel_y, kernel_z = CHOCLO_KERNELS[component] - self._mag_sensitivity_t_dot_v( + gt_dot_v_func = NUMBA_FUNCTIONS["gt_dot_v"]["magnetic_component"][ + self.numba_parallel + ] + gt_dot_v_func( receivers, active_nodes, active_cell_nodes, @@ -1233,7 +1186,10 @@ def _gtg_diagonal_without_building_g(self, weights): ) for component in components: if component == "tmi": - self._diagonal_G_T_dot_G_tmi( + diagonal_gtg_func = NUMBA_FUNCTIONS["diagonal_gtg"]["tmi"][ + self.numba_parallel + ] + diagonal_gtg_func( receivers, active_nodes, active_cell_nodes, @@ -1247,7 +1203,10 @@ def _gtg_diagonal_without_building_g(self, weights): kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz = ( CHOCLO_KERNELS[component] ) - self._diagonal_G_T_dot_G_tmi_deriv( + diagonal_gtg_func = NUMBA_FUNCTIONS["diagonal_gtg"][ + "tmi_derivative" + ][self.numba_parallel] + diagonal_gtg_func( receivers, active_nodes, active_cell_nodes, @@ -1265,7 +1224,10 @@ def _gtg_diagonal_without_building_g(self, weights): ) else: kernel_x, kernel_y, kernel_z = CHOCLO_KERNELS[component] - self._diagonal_G_T_dot_G_mag( + diagonal_gtg_func = NUMBA_FUNCTIONS["diagonal_gtg"][ + "magnetic_component" + ][self.numba_parallel] + diagonal_gtg_func( receivers, active_nodes, active_cell_nodes, @@ -1324,26 +1286,6 @@ def __init__( **kwargs, ) - if self.engine == "choclo": - if self.numba_parallel: - self._sensitivity_tmi = _sensitivity_tmi_2d_mesh_parallel - self._sensitivity_mag = _sensitivity_mag_2d_mesh_parallel - self._forward_tmi = _forward_tmi_2d_mesh_parallel - self._forward_mag = _forward_mag_2d_mesh_parallel - self._forward_tmi_derivative = _forward_tmi_derivative_2d_mesh_parallel - self._sensitivity_tmi_derivative = ( - _sensitivity_tmi_derivative_2d_mesh_parallel - ) - else: - self._sensitivity_tmi = _sensitivity_tmi_2d_mesh_serial - self._sensitivity_mag = _sensitivity_mag_2d_mesh_serial - self._forward_tmi = _forward_tmi_2d_mesh_serial - self._forward_mag = _forward_mag_2d_mesh_serial - self._forward_tmi_derivative = _forward_tmi_derivative_2d_mesh_serial - self._sensitivity_tmi_derivative = ( - _sensitivity_tmi_derivative_2d_mesh_serial - ) - def _forward(self, model): """ Forward model the fields of active cells in the mesh on receivers. @@ -1387,7 +1329,10 @@ def _forward(self, model): index_offset + i, index_offset + n_rows, n_components ) if component == "tmi": - self._forward_tmi( + forward_func = NUMBA_FUNCTIONS["forward_2d_mesh"]["tmi"][ + self.numba_parallel + ] + forward_func( receivers, cells_bounds_active, self.cell_z_top, @@ -1401,7 +1346,10 @@ def _forward(self, model): kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz = ( CHOCLO_KERNELS[component] ) - self._forward_tmi_derivative( + forward_func = NUMBA_FUNCTIONS["forward_2d_mesh"]["tmi_derivative"][ + self.numba_parallel + ] + forward_func( receivers, cells_bounds_active, self.cell_z_top, @@ -1418,8 +1366,11 @@ def _forward(self, model): scalar_model, ) else: - forward_func = CHOCLO_FORWARD_FUNCS[component] - self._forward_mag( + choclo_forward_func = CHOCLO_FORWARD_FUNCS[component] + forward_func = NUMBA_FUNCTIONS["forward_2d_mesh"][ + "magnetic_component" + ][self.numba_parallel] + forward_func( receivers, cells_bounds_active, self.cell_z_top, @@ -1427,7 +1378,7 @@ def _forward(self, model): model, fields[vector_slice], regional_field, - forward_func, + choclo_forward_func, scalar_model, ) index_offset += n_rows @@ -1474,7 +1425,10 @@ def _sensitivity_matrix(self): index_offset + i, index_offset + n_rows, n_components ) if component == "tmi": - self._sensitivity_tmi( + sensitivity_func = NUMBA_FUNCTIONS["sensitivity_2d_mesh"]["tmi"][ + self.numba_parallel + ] + sensitivity_func( receivers, cells_bounds_active, self.cell_z_top, @@ -1487,7 +1441,10 @@ def _sensitivity_matrix(self): kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz = ( CHOCLO_KERNELS[component] ) - self._sensitivity_tmi_derivative( + sensitivity_func = NUMBA_FUNCTIONS["sensitivity_2d_mesh"][ + "tmi_derivative" + ][self.numba_parallel] + sensitivity_func( receivers, cells_bounds_active, self.cell_z_top, @@ -1504,7 +1461,10 @@ def _sensitivity_matrix(self): ) else: kernel_x, kernel_y, kernel_z = CHOCLO_KERNELS[component] - self._sensitivity_mag( + sensitivity_func = NUMBA_FUNCTIONS["sensitivity_2d_mesh"][ + "magnetic_component" + ][self.numba_parallel] + sensitivity_func( receivers, cells_bounds_active, self.cell_z_top, diff --git a/tests/pf/test_forward_Grav_Linear.py b/tests/pf/test_forward_Grav_Linear.py index 873e38a503..e66279e4fc 100644 --- a/tests/pf/test_forward_Grav_Linear.py +++ b/tests/pf/test_forward_Grav_Linear.py @@ -556,7 +556,9 @@ def test_getJ_as_linear_operator_not_implemented( pytest.param( "geoana", "forward_only", - marks=pytest.mark.xfail(reason="not implemented"), + marks=pytest.mark.xfail( + raises=NotImplementedError, reason="not implemented" + ), ), ], ) @@ -596,7 +598,9 @@ def test_Jvec(self, survey, mesh, densities, mapping, engine, store_sensitivitie pytest.param( "geoana", "forward_only", - marks=pytest.mark.xfail(reason="not implemented"), + marks=pytest.mark.xfail( + raises=NotImplementedError, reason="not implemented" + ), ), ], ) @@ -631,7 +635,12 @@ def test_Jtvec(self, survey, mesh, densities, mapping, engine, store_sensitiviti "engine", [ "choclo", - pytest.param("geoana", marks=pytest.mark.xfail(reason="not implemented")), + pytest.param( + "geoana", + marks=pytest.mark.xfail( + raises=NotImplementedError, reason="not implemented" + ), + ), ], ) @pytest.mark.parametrize("method", ["Jvec", "Jtvec"]) @@ -707,7 +716,12 @@ def test_getJtJdiag(self, survey, mesh, densities, mapping, engine, weights): "engine", [ "choclo", - pytest.param("geoana", marks=pytest.mark.xfail(reason="not implemented")), + pytest.param( + "geoana", + marks=pytest.mark.xfail( + raises=NotImplementedError, reason="not implemented" + ), + ), ], ) @pytest.mark.parametrize("weights", [True, False]) diff --git a/tests/pf/test_forward_Mag_Linear.py b/tests/pf/test_forward_Mag_Linear.py index 9ddc45ac8a..e3659e508c 100644 --- a/tests/pf/test_forward_Mag_Linear.py +++ b/tests/pf/test_forward_Mag_Linear.py @@ -1243,7 +1243,9 @@ def test_getJ_not_implemented( pytest.param( "geoana", "forward_only", - marks=pytest.mark.xfail(reason="not implemented"), + marks=pytest.mark.xfail( + reason="not implemented", raises=NotImplementedError + ), ), ], ) @@ -1295,7 +1297,9 @@ def test_Jvec( pytest.param( "geoana", "forward_only", - marks=pytest.mark.xfail(reason="not implemented"), + marks=pytest.mark.xfail( + reason="not implemented", raises=NotImplementedError + ), ), ], ) @@ -1342,7 +1346,12 @@ def test_Jtvec( "engine", [ "choclo", - pytest.param("geoana", marks=pytest.mark.xfail(reason="not implemented")), + pytest.param( + "geoana", + marks=pytest.mark.xfail( + reason="not implemented", raises=NotImplementedError + ), + ), ], ) @pytest.mark.parametrize("method", ["Jvec", "Jtvec"]) @@ -1602,7 +1611,9 @@ def test_getJ_not_implemented( pytest.param( "geoana", "forward_only", - marks=pytest.mark.xfail(reason="not implemented"), + marks=pytest.mark.xfail( + reason="not implemented", raises=NotImplementedError + ), ), ], ) @@ -1688,7 +1699,9 @@ def test_Jvec( pytest.param( "geoana", "forward_only", - marks=pytest.mark.xfail(reason="not implemented"), + marks=pytest.mark.xfail( + reason="not implemented", raises=NotImplementedError + ), ), ], ) From 55cfbfa81df41a3b16c4fa1c27ace2173065e18f Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 15 May 2025 18:38:24 +0000 Subject: [PATCH 139/194] Make directives submodules private (#1667) Make the `simpeg.directives.directives`, `simpeg.directives.pgi_directives`, and `simpeg.directives.sim_directives` submodules private by renaming their files with a leading underscore. Raise `FutureWarning`s when trying to access the deprecated submodules. Add tests to check those warnings. --- pyproject.toml | 1 + simpeg/directives/__init__.py | 6 +- simpeg/directives/_directives.py | 2970 +++++++++++++++++++ simpeg/directives/_pgi_directives.py | 474 ++++ simpeg/directives/_regularization.py | 2 +- simpeg/directives/_sim_directives.py | 390 +++ simpeg/directives/directives.py | 2984 +------------------- simpeg/directives/pgi_directives.py | 490 +--- simpeg/directives/sim_directives.py | 408 +-- tests/base/test_directives_deprecations.py | 18 + 10 files changed, 3908 insertions(+), 3835 deletions(-) create mode 100644 simpeg/directives/_directives.py create mode 100644 simpeg/directives/_pgi_directives.py create mode 100644 simpeg/directives/_sim_directives.py create mode 100644 tests/base/test_directives_deprecations.py diff --git a/pyproject.toml b/pyproject.toml index a62d394c0b..7069369d37 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -261,5 +261,6 @@ filterwarnings = [ "ignore::simpeg.utils.solver_utils.DefaultSolverWarning", "error:You are running a pytest without setting a random seed.*:UserWarning", "error:The `index_dictionary` property has been deprecated:FutureWarning", + 'error:The `simpeg\.directives\.[a-z_]+` submodule has been deprecated', ] xfail_strict = true diff --git a/simpeg/directives/__init__.py b/simpeg/directives/__init__.py index bbffa1ea9c..c8887d1726 100644 --- a/simpeg/directives/__init__.py +++ b/simpeg/directives/__init__.py @@ -99,7 +99,7 @@ """ -from .directives import ( +from ._directives import ( InversionDirective, DirectiveList, BetaEstimateMaxDerivative, @@ -121,7 +121,7 @@ ProjectSphericalBounds, ) -from .pgi_directives import ( +from ._pgi_directives import ( PGI_UpdateParameters, PGI_BetaAlphaSchedule, PGI_AddMrefInSmooth, @@ -129,7 +129,7 @@ from ._regularization import UpdateIRLS, SphericalUnitsWeights -from .sim_directives import ( +from ._sim_directives import ( SimilarityMeasureInversionDirective, SimilarityMeasureSaveOutputEveryIteration, PairedBetaEstimate_ByEig, diff --git a/simpeg/directives/_directives.py b/simpeg/directives/_directives.py new file mode 100644 index 0000000000..ee4a4c2d4d --- /dev/null +++ b/simpeg/directives/_directives.py @@ -0,0 +1,2970 @@ +from typing import TYPE_CHECKING +import numpy as np +import matplotlib.pyplot as plt +import warnings +import os +import scipy.sparse as sp +from ..typing import RandomSeed +from ..data_misfit import BaseDataMisfit +from ..objective_function import BaseObjectiveFunction, ComboObjectiveFunction +from ..maps import IdentityMap, Wires +from ..regularization import ( + WeightedLeastSquares, + BaseRegularization, + BaseSparse, + Smallness, + Sparse, + SparseSmallness, + PGIsmallness, + SmoothnessFirstOrder, + SparseSmoothness, + BaseSimilarityMeasure, +) +from ..utils import ( + mkvc, + set_kwargs, + sdiag, + estimate_diagonal, + spherical2cartesian, + cartesian2spherical, + Zero, + eigenvalue_by_power_iteration, + validate_string, +) +from ..utils.code_utils import ( + deprecate_class, + deprecate_property, + validate_type, + validate_integer, + validate_float, + validate_ndarray_with_shape, +) + +if TYPE_CHECKING: + from ..simulation import BaseSimulation + from ..survey import BaseSurvey + + +class InversionDirective: + """Base inversion directive class. + + SimPEG directives initialize and update parameters used by the inversion algorithm; + e.g. setting the initial beta or updating the regularization. ``InversionDirective`` + is a parent class responsible for connecting directives to the data misfit, regularization + and optimization defining the inverse problem. + + Parameters + ---------- + inversion : simpeg.inversion.BaseInversion, None + An SimPEG inversion object; i.e. an instance of :class:`simpeg.inversion.BaseInversion`. + dmisfit : simpeg.data_misfit.BaseDataMisfit, None + A data data misfit; i.e. an instance of :class:`simpeg.data_misfit.BaseDataMisfit`. + reg : simpeg.regularization.BaseRegularization, None + The regularization, or model objective function; i.e. an instance of :class:`simpeg.regularization.BaseRegularization`. + verbose : bool + Whether or not to print debugging information. + """ + + _REGISTRY = {} + + _regPair = [WeightedLeastSquares, BaseRegularization, ComboObjectiveFunction] + _dmisfitPair = [BaseDataMisfit, ComboObjectiveFunction] + + def __init__(self, inversion=None, dmisfit=None, reg=None, verbose=False, **kwargs): + # Raise error on deprecated arguments + if (key := "debug") in kwargs.keys(): + raise TypeError(f"'{key}' property has been removed. Please use 'verbose'.") + self.inversion = inversion + self.dmisfit = dmisfit + self.reg = reg + self.verbose = verbose + set_kwargs(self, **kwargs) + + @property + def verbose(self): + """Whether or not to print debugging information. + + Returns + ------- + bool + """ + return self._verbose + + @verbose.setter + def verbose(self, value): + self._verbose = validate_type("verbose", value, bool) + + debug = deprecate_property( + verbose, "debug", "verbose", removal_version="0.19.0", error=True + ) + + @property + def inversion(self): + """Inversion object associated with the directive. + + Returns + ------- + simpeg.inversion.BaseInversion + The inversion associated with the directive. + """ + if not hasattr(self, "_inversion"): + return None + return self._inversion + + @inversion.setter + def inversion(self, i): + if getattr(self, "_inversion", None) is not None: + warnings.warn( + "InversionDirective {0!s} has switched to a new inversion.".format( + self.__class__.__name__ + ), + stacklevel=2, + ) + self._inversion = i + + @property + def invProb(self): + """Inverse problem associated with the directive. + + Returns + ------- + simpeg.inverse_problem.BaseInvProblem + The inverse problem associated with the directive. + """ + return self.inversion.invProb + + @property + def opt(self): + """Optimization algorithm associated with the directive. + + Returns + ------- + simpeg.optimization.Minimize + Optimization algorithm associated with the directive. + """ + return self.invProb.opt + + @property + def reg(self) -> BaseObjectiveFunction: + """Regularization associated with the directive. + + Returns + ------- + simpeg.regularization.BaseRegularization + The regularization associated with the directive. + """ + if getattr(self, "_reg", None) is None: + self.reg = self.invProb.reg # go through the setter + return self._reg + + @reg.setter + def reg(self, value): + if value is not None: + assert any( + [isinstance(value, regtype) for regtype in self._regPair] + ), "Regularization must be in {}, not {}".format(self._regPair, type(value)) + + if isinstance(value, WeightedLeastSquares): + value = 1 * value # turn it into a combo objective function + self._reg = value + + @property + def dmisfit(self) -> BaseObjectiveFunction: + """Data misfit associated with the directive. + + Returns + ------- + simpeg.data_misfit.BaseDataMisfit + The data misfit associated with the directive. + """ + if getattr(self, "_dmisfit", None) is None: + self.dmisfit = self.invProb.dmisfit # go through the setter + return self._dmisfit + + @dmisfit.setter + def dmisfit(self, value): + if value is not None: + assert any( + [isinstance(value, dmisfittype) for dmisfittype in self._dmisfitPair] + ), "Misfit must be in {}, not {}".format(self._dmisfitPair, type(value)) + + if not isinstance(value, ComboObjectiveFunction): + value = 1 * value # turn it into a combo objective function + self._dmisfit = value + + @property + def survey(self) -> list["BaseSurvey"]: + """Return survey for all data misfits + + Assuming that ``dmisfit`` is always a ``ComboObjectiveFunction``, + return a list containing the survey for each data misfit; i.e. + [survey1, survey2, ...] + + Returns + ------- + list of simpeg.survey.Survey + Survey for all data misfits. + """ + return [objfcts.simulation.survey for objfcts in self.dmisfit.objfcts] + + @property + def simulation(self) -> list["BaseSimulation"]: + """Return simulation for all data misfits. + + Assuming that ``dmisfit`` is always a ``ComboObjectiveFunction``, + return a list containing the simulation for each data misfit; i.e. + [sim1, sim2, ...]. + + Returns + ------- + list of simpeg.simulation.BaseSimulation + Simulation for all data misfits. + """ + return [objfcts.simulation for objfcts in self.dmisfit.objfcts] + + def initialize(self): + """Initialize inversion parameter(s) according to directive.""" + pass + + def endIter(self): + """Update inversion parameter(s) according to directive at end of iteration.""" + pass + + def finish(self): + """Update inversion parameter(s) according to directive at end of inversion.""" + pass + + def validate(self, directiveList=None): + """Validate directive. + + The `validate` method returns ``True`` if the directive and its location within + the directives list does not encounter conflicts. Otherwise, an appropriate error + message is returned describing the conflict. + + Parameters + ---------- + directive_list : simpeg.directives.DirectiveList + List of directives used in the inversion. + + Returns + ------- + bool + Returns ``True`` if validated, otherwise an approriate error is returned. + """ + return True + + +class DirectiveList(object): + """Directives list + + SimPEG directives initialize and update parameters used by the inversion algorithm; + e.g. setting the initial beta or updating the regularization. ``DirectiveList`` stores + the set of directives used in the inversion algorithm. + + Parameters + ---------- + directives : list of simpeg.directives.InversionDirective + List of directives. + inversion : simpeg.inversion.BaseInversion + The inversion associated with the directives list. + debug : bool + Whether or not to print debugging information. + + """ + + def __init__(self, *directives, inversion=None, debug=False, **kwargs): + super().__init__(**kwargs) + self.dList = [] + for d in directives: + assert isinstance( + d, InversionDirective + ), "All directives must be InversionDirectives not {}".format(type(d)) + self.dList.append(d) + self.inversion = inversion + self.verbose = debug + + @property + def debug(self): + """Whether or not to print debugging information + + Returns + ------- + bool + """ + return getattr(self, "_debug", False) + + @debug.setter + def debug(self, value): + for d in self.dList: + d.debug = value + self._debug = value + + @property + def inversion(self): + """Inversion object associated with the directives list. + + Returns + ------- + simpeg.inversion.BaseInversion + The inversion associated with the directives list. + """ + return getattr(self, "_inversion", None) + + @inversion.setter + def inversion(self, i): + if self.inversion is i: + return + if getattr(self, "_inversion", None) is not None: + warnings.warn( + "{0!s} has switched to a new inversion.".format( + self.__class__.__name__ + ), + stacklevel=2, + ) + for d in self.dList: + d.inversion = i + self._inversion = i + + def call(self, ruleType): + if self.dList is None: + if self.verbose: + print("DirectiveList is None, no directives to call!") + return + + directives = ["initialize", "endIter", "finish"] + assert ruleType in directives, 'Directive type must be in ["{0!s}"]'.format( + '", "'.join(directives) + ) + for r in self.dList: + getattr(r, ruleType)() + + def validate(self): + [directive.validate(self) for directive in self.dList] + return True + + +class BaseBetaEstimator(InversionDirective): + """Base class for estimating initial trade-off parameter (beta). + + This class has properties and methods inherited by directive classes which estimate + the initial trade-off parameter (beta). This class is not used directly to create + directives for the inversion. + + Parameters + ---------- + beta0_ratio : float + Desired ratio between data misfit and model objective function at initial beta iteration. + random_seed : None or :class:`~simpeg.typing.RandomSeed`, optional + Random seed used for random sampling. It can either be an int, + a predefined Numpy random number generator, or any valid input to + ``numpy.random.default_rng``. + seed : None or :class:`~simpeg.typing.RandomSeed`, optional + + .. deprecated:: 0.23.0 + + Argument ``seed`` is deprecated in favor of ``random_seed`` and will + be removed in SimPEG v0.24.0. + + """ + + def __init__( + self, + beta0_ratio=1.0, + random_seed: RandomSeed | None = None, + seed: RandomSeed | None = None, + **kwargs, + ): + super().__init__(**kwargs) + self.beta0_ratio = beta0_ratio + + # Deprecate seed argument + if seed is not None: + if random_seed is not None: + raise TypeError( + "Cannot pass both 'random_seed' and 'seed'." + "'seed' has been deprecated and will be removed in " + " SimPEG v0.24.0, please use 'random_seed' instead.", + ) + warnings.warn( + "'seed' has been deprecated and will be removed in " + " SimPEG v0.24.0, please use 'random_seed' instead.", + FutureWarning, + stacklevel=2, + ) + random_seed = seed + self.random_seed = random_seed + + @property + def beta0_ratio(self): + """The estimated ratio is multiplied by this to obtain beta. + + Returns + ------- + float + """ + return self._beta0_ratio + + @beta0_ratio.setter + def beta0_ratio(self, value): + self._beta0_ratio = validate_float( + "beta0_ratio", value, min_val=0.0, inclusive_min=False + ) + + @property + def random_seed(self): + """Random seed to initialize with. + + Returns + ------- + int, numpy.random.Generator or None + """ + return self._random_seed + + @random_seed.setter + def random_seed(self, value): + try: + np.random.default_rng(value) + except TypeError as err: + msg = ( + "Unable to initialize the random number generator with " + f"a {type(value).__name__}" + ) + raise TypeError(msg) from err + self._random_seed = value + + def validate(self, directive_list): + ind = [isinstance(d, BaseBetaEstimator) for d in directive_list.dList] + assert np.sum(ind) == 1, ( + "Multiple directives for computing initial beta detected in directives list. " + "Only one directive can be used to set the initial beta." + ) + + return True + + seed = deprecate_property( + random_seed, + "seed", + "random_seed", + removal_version="0.24.0", + future_warn=True, + error=False, + ) + + +class BetaEstimateMaxDerivative(BaseBetaEstimator): + r"""Estimate initial trade-off parameter (beta) using largest derivatives. + + The initial trade-off parameter (beta) is estimated by scaling the ratio + between the largest derivatives in the gradient of the data misfit and + model objective function. The estimated trade-off parameter is used to + update the **beta** property in the associated :class:`simpeg.inverse_problem.BaseInvProblem` + object prior to running the inversion. A separate directive is used for updating the + trade-off parameter at successive beta iterations; see :class:`BetaSchedule`. + + Parameters + ---------- + beta0_ratio: float + Desired ratio between data misfit and model objective function at initial beta iteration. + random_seed : None or :class:`~simpeg.typing.RandomSeed`, optional + Random seed used for random sampling. It can either be an int, + a predefined Numpy random number generator, or any valid input to + ``numpy.random.default_rng``. + seed : None or :class:`~simpeg.typing.RandomSeed`, optional + + .. deprecated:: 0.23.0 + + Argument ``seed`` is deprecated in favor of ``random_seed`` and will + be removed in SimPEG v0.24.0. + + Notes + ----- + Let :math:`\phi_d` represent the data misfit, :math:`\phi_m` represent the model + objective function and :math:`\mathbf{m_0}` represent the starting model. The first + model update is obtained by minimizing the a global objective function of the form: + + .. math:: + \phi (\mathbf{m_0}) = \phi_d (\mathbf{m_0}) + \beta_0 \phi_m (\mathbf{m_0}) + + where :math:`\beta_0` represents the initial trade-off parameter (beta). + + We define :math:`\gamma` as the desired ratio between the data misfit and model objective + functions at the initial beta iteration (defined by the 'beta0_ratio' input argument). + Here, the initial trade-off parameter is computed according to: + + .. math:: + \beta_0 = \gamma \frac{| \nabla_m \phi_d (\mathbf{m_0}) |_{max}}{| \nabla_m \phi_m (\mathbf{m_0 + \delta m}) |_{max}} + + where + + .. math:: + \delta \mathbf{m} = \frac{m_{max}}{\mu_{max}} \boldsymbol{\mu} + + and :math:`\boldsymbol{\mu}` is a set of independent samples from the + continuous uniform distribution between 0 and 1. + + """ + + def __init__( + self, beta0_ratio=1.0, random_seed: RandomSeed | None = None, **kwargs + ): + super().__init__(beta0_ratio=beta0_ratio, random_seed=random_seed, **kwargs) + + def initialize(self): + rng = np.random.default_rng(seed=self.random_seed) + + if self.verbose: + print("Calculating the beta0 parameter.") + + m = self.invProb.model + + x0 = rng.random(size=m.shape) + phi_d_deriv = np.abs(self.dmisfit.deriv(m)).max() + dm = x0 / x0.max() * m.max() + phi_m_deriv = np.abs(self.reg.deriv(m + dm)).max() + + self.ratio = np.asarray(phi_d_deriv / phi_m_deriv) + self.beta0 = self.beta0_ratio * self.ratio + self.invProb.beta = self.beta0 + + +class BetaEstimate_ByEig(BaseBetaEstimator): + r"""Estimate initial trade-off parameter (beta) by power iteration. + + The initial trade-off parameter (beta) is estimated by scaling the ratio + between the largest eigenvalue in the second derivative of the data + misfit and the model objective function. The largest eigenvalues are estimated + using the power iteration method; see :func:`simpeg.utils.eigenvalue_by_power_iteration`. + The estimated trade-off parameter is used to update the **beta** property in the + associated :class:`simpeg.inverse_problem.BaseInvProblem` object prior to running the inversion. + Note that a separate directive is used for updating the trade-off parameter at successive + beta iterations; see :class:`BetaSchedule`. + + Parameters + ---------- + beta0_ratio: float + Desired ratio between data misfit and model objective function at initial beta iteration. + n_pw_iter : int + Number of power iterations used to estimate largest eigenvalues. + random_seed : None or :class:`~simpeg.typing.RandomSeed`, optional + Random seed used for random sampling. It can either be an int, + a predefined Numpy random number generator, or any valid input to + ``numpy.random.default_rng``. + seed : None or :class:`~simpeg.typing.RandomSeed`, optional + + .. deprecated:: 0.23.0 + + Argument ``seed`` is deprecated in favor of ``random_seed`` and will + be removed in SimPEG v0.24.0. + + Notes + ----- + Let :math:`\phi_d` represent the data misfit, :math:`\phi_m` represent the model + objective function and :math:`\mathbf{m_0}` represent the starting model. The first + model update is obtained by minimizing the a global objective function of the form: + + .. math:: + \phi (\mathbf{m_0}) = \phi_d (\mathbf{m_0}) + \beta_0 \phi_m (\mathbf{m_0}) + + where :math:`\beta_0` represents the initial trade-off parameter (beta). + Let :math:`\gamma` define the desired ratio between the data misfit and model + objective functions at the initial beta iteration (defined by the 'beta0_ratio' input argument). + Using the power iteration approach, our initial trade-off parameter is given by: + + .. math:: + \beta_0 = \gamma \frac{\lambda_d}{\lambda_m} + + where :math:`\lambda_d` as the largest eigenvalue of the Hessian of the data misfit, and + :math:`\lambda_m` as the largest eigenvalue of the Hessian of the model objective function. + For each Hessian, the largest eigenvalue is computed using power iteration. The input + parameter 'n_pw_iter' sets the number of power iterations used in the estimate. + + For a description of the power iteration approach for estimating the larges eigenvalue, + see :func:`simpeg.utils.eigenvalue_by_power_iteration`. + + """ + + def __init__( + self, + beta0_ratio=1.0, + n_pw_iter=4, + random_seed: RandomSeed | None = None, + seed: RandomSeed | None = None, + **kwargs, + ): + super().__init__( + beta0_ratio=beta0_ratio, random_seed=random_seed, seed=seed, **kwargs + ) + self.n_pw_iter = n_pw_iter + + @property + def n_pw_iter(self): + """Number of power iterations for estimating largest eigenvalues. + + Returns + ------- + int + Number of power iterations for estimating largest eigenvalues. + """ + return self._n_pw_iter + + @n_pw_iter.setter + def n_pw_iter(self, value): + self._n_pw_iter = validate_integer("n_pw_iter", value, min_val=1) + + def initialize(self): + rng = np.random.default_rng(seed=self.random_seed) + + if self.verbose: + print("Calculating the beta0 parameter.") + + m = self.invProb.model + + dm_eigenvalue = eigenvalue_by_power_iteration( + self.dmisfit, + m, + n_pw_iter=self.n_pw_iter, + random_seed=rng, + ) + reg_eigenvalue = eigenvalue_by_power_iteration( + self.reg, + m, + n_pw_iter=self.n_pw_iter, + random_seed=rng, + ) + + self.ratio = np.asarray(dm_eigenvalue / reg_eigenvalue) + self.beta0 = self.beta0_ratio * self.ratio + self.invProb.beta = self.beta0 + + +class BetaSchedule(InversionDirective): + """Reduce trade-off parameter (beta) at successive iterations using a cooling schedule. + + Updates the **beta** property in the associated :class:`simpeg.inverse_problem.BaseInvProblem` + while the inversion is running. + For linear least-squares problems, the optimization problem can be solved in a + single step and the cooling rate can be set to *1*. For non-linear optimization + problems, multiple steps are required obtain the minimizer for a fixed trade-off + parameter. In this case, the cooling rate should be larger than 1. + + Parameters + ---------- + coolingFactor : float + The factor by which the trade-off parameter is decreased when updated. + The preexisting value of the trade-off parameter is divided by the cooling factor. + coolingRate : int + Sets the number of successive iterations before the trade-off parameter is reduced. + Use *1* for linear least-squares optimization problems. Use *2* for weakly non-linear + optimization problems. Use *3* for general non-linear optimization problems. + + """ + + def __init__(self, coolingFactor=8.0, coolingRate=3, **kwargs): + super().__init__(**kwargs) + self.coolingFactor = coolingFactor + self.coolingRate = coolingRate + + @property + def coolingFactor(self): + """Beta is divided by this value every `coolingRate` iterations. + + Returns + ------- + float + """ + return self._coolingFactor + + @coolingFactor.setter + def coolingFactor(self, value): + self._coolingFactor = validate_float( + "coolingFactor", value, min_val=0.0, inclusive_min=False + ) + + @property + def coolingRate(self): + """Cool after this number of iterations. + + Returns + ------- + int + """ + return self._coolingRate + + @coolingRate.setter + def coolingRate(self, value): + self._coolingRate = validate_integer("coolingRate", value, min_val=1) + + def endIter(self): + if self.opt.iter > 0 and self.opt.iter % self.coolingRate == 0: + if self.verbose: + print( + "BetaSchedule is cooling Beta. Iteration: {0:d}".format( + self.opt.iter + ) + ) + self.invProb.beta /= self.coolingFactor + + +class AlphasSmoothEstimate_ByEig(InversionDirective): + """ + Estimate the alphas multipliers for the smoothness terms of the regularization + as a multiple of the ratio between the highest eigenvalue of the + smallness term and the highest eigenvalue of each smoothness term of the regularization. + The highest eigenvalue are estimated through power iterations and Rayleigh quotient. + """ + + def __init__( + self, + alpha0_ratio=1.0, + n_pw_iter=4, + random_seed: RandomSeed | None = None, + seed: RandomSeed | None = None, + **kwargs, + ): + super().__init__(**kwargs) + self.alpha0_ratio = alpha0_ratio + self.n_pw_iter = n_pw_iter + + # Deprecate seed argument + if seed is not None: + if random_seed is not None: + raise TypeError( + "Cannot pass both 'random_seed' and 'seed'." + "'seed' has been deprecated and will be removed in " + " SimPEG v0.24.0, please use 'random_seed' instead.", + ) + warnings.warn( + "'seed' has been deprecated and will be removed in " + " SimPEG v0.24.0, please use 'random_seed' instead.", + FutureWarning, + stacklevel=2, + ) + random_seed = seed + self.random_seed = random_seed + + @property + def alpha0_ratio(self): + """the estimated Alpha_smooth is multiplied by this ratio (int or array). + + Returns + ------- + numpy.ndarray + """ + return self._alpha0_ratio + + @alpha0_ratio.setter + def alpha0_ratio(self, value): + self._alpha0_ratio = validate_ndarray_with_shape( + "alpha0_ratio", value, shape=("*",) + ) + + @property + def n_pw_iter(self): + """Number of power iterations for estimation. + + Returns + ------- + int + """ + return self._n_pw_iter + + @n_pw_iter.setter + def n_pw_iter(self, value): + self._n_pw_iter = validate_integer("n_pw_iter", value, min_val=1) + + @property + def random_seed(self): + """Random seed to initialize with. + + Returns + ------- + int, numpy.random.Generator or None + """ + return self._random_seed + + @random_seed.setter + def random_seed(self, value): + try: + np.random.default_rng(value) + except TypeError as err: + msg = ( + "Unable to initialize the random number generator with " + f"a {type(value).__name__}" + ) + raise TypeError(msg) from err + self._random_seed = value + + seed = deprecate_property( + random_seed, + "seed", + "random_seed", + removal_version="0.24.0", + future_warn=True, + error=False, + ) + + def initialize(self): + """""" + rng = np.random.default_rng(seed=self.random_seed) + + smoothness = [] + smallness = [] + parents = {} + for regobjcts in self.reg.objfcts: + if isinstance(regobjcts, ComboObjectiveFunction): + objfcts = regobjcts.objfcts + else: + objfcts = [regobjcts] + + for obj in objfcts: + if isinstance( + obj, + ( + Smallness, + SparseSmallness, + PGIsmallness, + ), + ): + smallness += [obj] + + elif isinstance(obj, (SmoothnessFirstOrder, SparseSmoothness)): + parents[obj] = regobjcts + smoothness += [obj] + + if len(smallness) == 0: + raise UserWarning( + "Directive 'AlphasSmoothEstimate_ByEig' requires a regularization with at least one Small instance." + ) + + smallness_eigenvalue = eigenvalue_by_power_iteration( + smallness[0], + self.invProb.model, + n_pw_iter=self.n_pw_iter, + random_seed=rng, + ) + + self.alpha0_ratio = self.alpha0_ratio * np.ones(len(smoothness)) + + if len(self.alpha0_ratio) != len(smoothness): + raise ValueError( + f"Input values for 'alpha0_ratio' should be of len({len(smoothness)}). Provided {self.alpha0_ratio}" + ) + + alphas = [] + for user_alpha, obj in zip(self.alpha0_ratio, smoothness): + smooth_i_eigenvalue = eigenvalue_by_power_iteration( + obj, + self.invProb.model, + n_pw_iter=self.n_pw_iter, + random_seed=rng, + ) + ratio = smallness_eigenvalue / smooth_i_eigenvalue + + mtype = obj._multiplier_pair + + new_alpha = getattr(parents[obj], mtype) * user_alpha * ratio + setattr(parents[obj], mtype, new_alpha) + alphas += [new_alpha] + + if self.verbose: + print(f"Alpha scales: {alphas}") + + +class ScalingMultipleDataMisfits_ByEig(InversionDirective): + """ + For multiple data misfits only: multiply each data misfit term + by the inverse of its highest eigenvalue and then + normalize the sum of the data misfit multipliers to one. + The highest eigenvalue are estimated through power iterations and Rayleigh quotient. + """ + + def __init__( + self, + chi0_ratio=None, + n_pw_iter=4, + random_seed: RandomSeed | None = None, + seed: RandomSeed | None = None, + **kwargs, + ): + super().__init__(**kwargs) + self.chi0_ratio = chi0_ratio + self.n_pw_iter = n_pw_iter + + # Deprecate seed argument + if seed is not None: + if random_seed is not None: + raise TypeError( + "Cannot pass both 'random_seed' and 'seed'." + "'seed' has been deprecated and will be removed in " + " SimPEG v0.24.0, please use 'random_seed' instead.", + ) + warnings.warn( + "'seed' has been deprecated and will be removed in " + " SimPEG v0.24.0, please use 'random_seed' instead.", + FutureWarning, + stacklevel=2, + ) + random_seed = seed + self.random_seed = random_seed + + @property + def chi0_ratio(self): + """the estimated Alpha_smooth is multiplied by this ratio (int or array) + + Returns + ------- + numpy.ndarray + """ + return self._chi0_ratio + + @chi0_ratio.setter + def chi0_ratio(self, value): + if value is not None: + value = validate_ndarray_with_shape("chi0_ratio", value, shape=("*",)) + self._chi0_ratio = value + + @property + def n_pw_iter(self): + """Number of power iterations for estimation. + + Returns + ------- + int + """ + return self._n_pw_iter + + @n_pw_iter.setter + def n_pw_iter(self, value): + self._n_pw_iter = validate_integer("n_pw_iter", value, min_val=1) + + @property + def random_seed(self): + """Random seed to initialize with + + Returns + ------- + int, numpy.random.Generator or None + """ + return self._random_seed + + @random_seed.setter + def random_seed(self, value): + try: + np.random.default_rng(value) + except TypeError as err: + msg = ( + "Unable to initialize the random number generator with " + f"a {type(value).__name__}" + ) + raise TypeError(msg) from err + self._random_seed = value + + seed = deprecate_property( + random_seed, + "seed", + "random_seed", + removal_version="0.24.0", + future_warn=True, + error=False, + ) + + def initialize(self): + """""" + rng = np.random.default_rng(seed=self.random_seed) + + if self.verbose: + print("Calculating the scaling parameter.") + + if ( + getattr(self.dmisfit, "objfcts", None) is None + or len(self.dmisfit.objfcts) == 1 + ): + raise TypeError( + "ScalingMultipleDataMisfits_ByEig only applies to joint inversion" + ) + + ndm = len(self.dmisfit.objfcts) + if self.chi0_ratio is not None: + self.chi0_ratio = self.chi0_ratio * np.ones(ndm) + else: + self.chi0_ratio = self.dmisfit.multipliers + + m = self.invProb.model + + dm_eigenvalue_list = [] + for dm in self.dmisfit.objfcts: + dm_eigenvalue_list += [ + eigenvalue_by_power_iteration(dm, m, random_seed=rng) + ] + + self.chi0 = self.chi0_ratio / np.r_[dm_eigenvalue_list] + self.chi0 = self.chi0 / np.sum(self.chi0) + self.dmisfit.multipliers = self.chi0 + + if self.verbose: + print("Scale Multipliers: ", self.dmisfit.multipliers) + + +class JointScalingSchedule(InversionDirective): + """ + For multiple data misfits only: rebalance each data misfit term + during the inversion when some datasets are fit, and others not + using the ratios of current misfits and their respective target. + It implements the strategy described in https://doi.org/10.1093/gji/ggaa378. + """ + + def __init__( + self, warmingFactor=1.0, chimax=1e10, chimin=1e-10, update_rate=1, **kwargs + ): + super().__init__(**kwargs) + self.mode = 1 + self.warmingFactor = warmingFactor + self.chimax = chimax + self.chimin = chimin + self.update_rate = update_rate + + @property + def mode(self): + """The type of update to perform. + + Returns + ------- + {1, 2} + """ + return self._mode + + @mode.setter + def mode(self, value): + self._mode = validate_integer("mode", value, min_val=1, max_val=2) + + @property + def warmingFactor(self): + """Factor to adjust scaling of the data misfits by. + + Returns + ------- + float + """ + return self._warmingFactor + + @warmingFactor.setter + def warmingFactor(self, value): + self._warmingFactor = validate_float( + "warmingFactor", value, min_val=0.0, inclusive_min=False + ) + + @property + def chimax(self): + """Maximum chi factor. + + Returns + ------- + float + """ + return self._chimax + + @chimax.setter + def chimax(self, value): + self._chimax = validate_float("chimax", value, min_val=0.0, inclusive_min=False) + + @property + def chimin(self): + """Minimum chi factor. + + Returns + ------- + float + """ + return self._chimin + + @chimin.setter + def chimin(self, value): + self._chimin = validate_float("chimin", value, min_val=0.0, inclusive_min=False) + + @property + def update_rate(self): + """Will update the data misfit scalings after this many iterations. + + Returns + ------- + int + """ + return self._update_rate + + @update_rate.setter + def update_rate(self, value): + self._update_rate = validate_integer("update_rate", value, min_val=1) + + def initialize(self): + if ( + getattr(self.dmisfit, "objfcts", None) is None + or len(self.dmisfit.objfcts) == 1 + ): + raise TypeError("JointScalingSchedule only applies to joint inversion") + + targetclass = np.r_[ + [ + isinstance(dirpart, MultiTargetMisfits) + for dirpart in self.inversion.directiveList.dList + ] + ] + if ~np.any(targetclass): + self.DMtarget = None + else: + self.targetclass = np.where(targetclass)[0][-1] + self.DMtarget = self.inversion.directiveList.dList[ + self.targetclass + ].DMtarget + + if self.verbose: + print("Initial data misfit scales: ", self.dmisfit.multipliers) + + def endIter(self): + self.dmlist = self.inversion.directiveList.dList[self.targetclass].dmlist + + if np.any(self.dmlist < self.DMtarget): + self.mode = 2 + else: + self.mode = 1 + + if self.opt.iter > 0 and self.opt.iter % self.update_rate == 0: + if self.mode == 2: + if np.all(np.r_[self.dmisfit.multipliers] > self.chimin) and np.all( + np.r_[self.dmisfit.multipliers] < self.chimax + ): + indx = self.dmlist > self.DMtarget + if np.any(indx): + multipliers = self.warmingFactor * np.median( + self.DMtarget[~indx] / self.dmlist[~indx] + ) + if np.sum(indx) == 1: + indx = np.where(indx)[0][0] + self.dmisfit.multipliers[indx] *= multipliers + self.dmisfit.multipliers /= np.sum(self.dmisfit.multipliers) + + if self.verbose: + print("Updating scaling for data misfits by ", multipliers) + print("New scales:", self.dmisfit.multipliers) + + +class TargetMisfit(InversionDirective): + """ + ... note:: Currently this target misfit is not set up for joint inversion. + Check out MultiTargetMisfits + """ + + def __init__(self, target=None, phi_d_star=None, chifact=1.0, **kwargs): + super().__init__(**kwargs) + self.chifact = chifact + self.phi_d_star = phi_d_star + if phi_d_star is not None and target is not None: + raise AttributeError("Attempted to set both target and phi_d_star.") + if target is not None: + self.target = target + + @property + def target(self): + """The target value for the data misfit + + Returns + ------- + float + """ + if getattr(self, "_target", None) is None: + self._target = self.chifact * self.phi_d_star + return self._target + + @target.setter + def target(self, val): + self._target = validate_float("target", val, min_val=0.0, inclusive_min=False) + + @property + def chifact(self): + """The a multiplier for the target data misfit value. + + The target value is `chifact` times `phi_d_star` + + Returns + ------- + float + """ + return self._chifact + + @chifact.setter + def chifact(self, value): + self._chifact = validate_float( + "chifact", value, min_val=0.0, inclusive_min=False + ) + self._target = None + + @property + def phi_d_star(self): + """The target phi_d value for the data misfit. + + The target value is `chifact` times `phi_d_star` + + Returns + ------- + float + """ + # phid = ||dpred - dobs||^2 + if self._phi_d_star is None: + nD = 0 + for survey in self.survey: + nD += survey.nD + self._phi_d_star = nD + return self._phi_d_star + + @phi_d_star.setter + def phi_d_star(self, value): + # phid = ||dpred - dobs||^2 + if value is not None: + value = validate_float( + "phi_d_star", value, min_val=0.0, inclusive_min=False + ) + self._phi_d_star = value + self._target = None + + def endIter(self): + if self.invProb.phi_d < self.target: + self.opt.stopNextIteration = True + self.print_final_misfit() + + def print_final_misfit(self): + if self.opt.print_type == "ubc": + self.opt.print_target = ( + ">> Target misfit: %.1f (# of data) is achieved" + ) % (self.target) + + +class MultiTargetMisfits(InversionDirective): + def __init__( + self, + WeightsInTarget=False, + chifact=1.0, + phi_d_star=None, + TriggerSmall=True, + chiSmall=1.0, + phi_ms_star=None, + TriggerTheta=False, + ToleranceTheta=1.0, + distance_norm=np.inf, + **kwargs, + ): + super().__init__(**kwargs) + + self.WeightsInTarget = WeightsInTarget + # Chi factor for Geophsyical Data Misfit + self.chifact = chifact + self.phi_d_star = phi_d_star + + # Chifact for Clustering/Smallness + self.TriggerSmall = TriggerSmall + self.chiSmall = chiSmall + self.phi_ms_star = phi_ms_star + + # Tolerance for parameters difference with their priors + self.TriggerTheta = TriggerTheta # deactivated by default + self.ToleranceTheta = ToleranceTheta + self.distance_norm = distance_norm + + self._DM = False + self._CL = False + self._DP = False + + @property + def WeightsInTarget(self): + """Whether to account for weights in the petrophysical misfit. + + Returns + ------- + bool + """ + return self._WeightsInTarget + + @WeightsInTarget.setter + def WeightsInTarget(self, value): + self._WeightsInTarget = validate_type("WeightsInTarget", value, bool) + + @property + def chifact(self): + """The a multiplier for the target Geophysical data misfit value. + + The target value is `chifact` times `phi_d_star` + + Returns + ------- + numpy.ndarray + """ + return self._chifact + + @chifact.setter + def chifact(self, value): + self._chifact = validate_ndarray_with_shape("chifact", value, shape=("*",)) + self._DMtarget = None + + @property + def phi_d_star(self): + """The target phi_d value for the Geophysical data misfit. + + The target value is `chifact` times `phi_d_star` + + Returns + ------- + float + """ + # phid = || dpred - dobs||^2 + if getattr(self, "_phi_d_star", None) is None: + # Check if it is a ComboObjective + if isinstance(self.dmisfit, ComboObjectiveFunction): + value = np.r_[[survey.nD for survey in self.survey]] + else: + value = np.r_[[self.survey.nD]] + self._phi_d_star = value + self._DMtarget = None + + return self._phi_d_star + + @phi_d_star.setter + def phi_d_star(self, value): + # phid =|| dpred - dobs||^2 + if value is not None: + value = validate_ndarray_with_shape("phi_d_star", value, shape=("*",)) + self._phi_d_star = value + self._DMtarget = None + + @property + def chiSmall(self): + """The a multiplier for the target petrophysical misfit value. + + The target value is `chiSmall` times `phi_ms_star` + + Returns + ------- + float + """ + return self._chiSmall + + @chiSmall.setter + def chiSmall(self, value): + self._chiSmall = validate_float("chiSmall", value) + self._CLtarget = None + + @property + def phi_ms_star(self): + """The target value for the petrophysical data misfit. + + The target value is `chiSmall` times `phi_ms_star` + + Returns + ------- + float + """ + return self._phi_ms_star + + @phi_ms_star.setter + def phi_ms_star(self, value): + if value is not None: + value = validate_float("phi_ms_star", value) + self._phi_ms_star = value + self._CLtarget = None + + @property + def TriggerSmall(self): + """Whether to trigger the smallness misfit test. + + Returns + ------- + bool + """ + return self._TriggerSmall + + @TriggerSmall.setter + def TriggerSmall(self, value): + self._TriggerSmall = validate_type("TriggerSmall", value, bool) + + @property + def TriggerTheta(self): + """Whether to trigger the GMM misfit test. + + Returns + ------- + bool + """ + return self._TriggerTheta + + @TriggerTheta.setter + def TriggerTheta(self, value): + self._TriggerTheta = validate_type("TriggerTheta", value, bool) + + @property + def ToleranceTheta(self): + """Target value for the GMM misfit. + + Returns + ------- + float + """ + return self._ToleranceTheta + + @ToleranceTheta.setter + def ToleranceTheta(self, value): + self._ToleranceTheta = validate_float("ToleranceTheta", value, min_val=0.0) + + @property + def distance_norm(self): + """Distance norm to use for GMM misfit measure. + + Returns + ------- + float + """ + return self._distance_norm + + @distance_norm.setter + def distance_norm(self, value): + self._distance_norm = validate_float("distance_norm", value, min_val=0.0) + + def initialize(self): + self.dmlist = np.r_[[dmis(self.invProb.model) for dmis in self.dmisfit.objfcts]] + + if getattr(self.invProb.reg.objfcts[0], "objfcts", None) is not None: + smallness = np.r_[ + [ + ( + np.r_[ + i, + j, + isinstance(regpart, PGIsmallness), + ] + ) + for i, regobjcts in enumerate(self.invProb.reg.objfcts) + for j, regpart in enumerate(regobjcts.objfcts) + ] + ] + if smallness[smallness[:, 2] == 1][:, :2].size == 0: + warnings.warn( + "There is no PGI regularization. Smallness target is turned off (TriggerSmall flag)", + stacklevel=2, + ) + self.smallness = -1 + self.pgi_smallness = None + + else: + self.smallness = smallness[smallness[:, 2] == 1][:, :2][0] + self.pgi_smallness = self.invProb.reg.objfcts[ + self.smallness[0] + ].objfcts[self.smallness[1]] + + if self.verbose: + print( + type( + self.invProb.reg.objfcts[self.smallness[0]].objfcts[ + self.smallness[1] + ] + ) + ) + + self._regmode = 1 + + else: + smallness = np.r_[ + [ + ( + np.r_[ + j, + isinstance(regpart, PGIsmallness), + ] + ) + for j, regpart in enumerate(self.invProb.reg.objfcts) + ] + ] + if smallness[smallness[:, 1] == 1][:, :1].size == 0: + if self.TriggerSmall: + warnings.warn( + "There is no PGI regularization. Smallness target is turned off (TriggerSmall flag).", + stacklevel=2, + ) + self.TriggerSmall = False + self.smallness = -1 + else: + self.smallness = smallness[smallness[:, 1] == 1][:, :1][0] + self.pgi_smallness = self.invProb.reg.objfcts[self.smallness[0]] + + if self.verbose: + print(type(self.invProb.reg.objfcts[self.smallness[0]])) + + self._regmode = 2 + + @property + def DM(self): + """Whether the geophysical data misfit target was satisfied. + + Returns + ------- + bool + """ + return self._DM + + @property + def CL(self): + """Whether the petrophysical misfit target was satisified. + + Returns + ------- + bool + """ + return self._CL + + @property + def DP(self): + """Whether the GMM misfit was below the threshold. + + Returns + ------- + bool + """ + return self._DP + + @property + def AllStop(self): + """Whether all target misfit values have been met. + + Returns + ------- + bool + """ + + return self.DM and self.CL and self.DP + + @property + def DMtarget(self): + if getattr(self, "_DMtarget", None) is None: + self._DMtarget = self.chifact * self.phi_d_star + return self._DMtarget + + @DMtarget.setter + def DMtarget(self, val): + self._DMtarget = val + + @property + def CLtarget(self): + if not getattr(self.pgi_smallness, "approx_eval", True): + # if nonlinear prior, compute targer numerically at each GMM update + samples, _ = self.pgi_smallness.gmm.sample( + len(self.pgi_smallness.gmm.cell_volumes) + ) + self.phi_ms_star = self.pgi_smallness( + mkvc(samples), externalW=self.WeightsInTarget + ) + + self._CLtarget = self.chiSmall * self.phi_ms_star + + elif getattr(self, "_CLtarget", None) is None: + # phid = ||dpred - dobs||^2 + if self.phi_ms_star is None: + # Expected value is number of active cells * number of physical + # properties + self.phi_ms_star = len(self.invProb.model) + + self._CLtarget = self.chiSmall * self.phi_ms_star + + return self._CLtarget + + @property + def CLnormalizedConstant(self): + if ~self.WeightsInTarget: + return 1.0 + elif np.any(self.smallness == -1): + return np.sum( + sp.csr_matrix.diagonal(self.invProb.reg.objfcts[0].W) ** 2.0 + ) / len(self.invProb.model) + else: + return np.sum(sp.csr_matrix.diagonal(self.pgi_smallness.W) ** 2.0) / len( + self.invProb.model + ) + + @CLtarget.setter + def CLtarget(self, val): + self._CLtarget = val + + def phims(self): + if np.any(self.smallness == -1): + return self.invProb.reg.objfcts[0](self.invProb.model) + else: + return ( + self.pgi_smallness( + self.invProb.model, external_weights=self.WeightsInTarget + ) + / self.CLnormalizedConstant + ) + + def ThetaTarget(self): + maxdiff = 0.0 + + for i in range(self.invProb.reg.gmm.n_components): + meandiff = np.linalg.norm( + (self.invProb.reg.gmm.means_[i] - self.invProb.reg.gmmref.means_[i]) + / self.invProb.reg.gmmref.means_[i], + ord=self.distance_norm, + ) + maxdiff = np.maximum(maxdiff, meandiff) + + if ( + self.invProb.reg.gmm.covariance_type == "full" + or self.invProb.reg.gmm.covariance_type == "spherical" + ): + covdiff = np.linalg.norm( + ( + self.invProb.reg.gmm.covariances_[i] + - self.invProb.reg.gmmref.covariances_[i] + ) + / self.invProb.reg.gmmref.covariances_[i], + ord=self.distance_norm, + ) + else: + covdiff = np.linalg.norm( + ( + self.invProb.reg.gmm.covariances_ + - self.invProb.reg.gmmref.covariances_ + ) + / self.invProb.reg.gmmref.covariances_, + ord=self.distance_norm, + ) + maxdiff = np.maximum(maxdiff, covdiff) + + pidiff = np.linalg.norm( + [ + ( + self.invProb.reg.gmm.weights_[i] + - self.invProb.reg.gmmref.weights_[i] + ) + / self.invProb.reg.gmmref.weights_[i] + ], + ord=self.distance_norm, + ) + maxdiff = np.maximum(maxdiff, pidiff) + + return maxdiff + + def endIter(self): + self._DM = False + self._CL = True + self._DP = True + self.dmlist = np.r_[[dmis(self.invProb.model) for dmis in self.dmisfit.objfcts]] + self.targetlist = np.r_[ + [dm < tgt for dm, tgt in zip(self.dmlist, self.DMtarget)] + ] + + if np.all(self.targetlist): + self._DM = True + + if self.TriggerSmall and np.any(self.smallness != -1): + if self.phims() > self.CLtarget: + self._CL = False + + if self.TriggerTheta: + if self.ThetaTarget() > self.ToleranceTheta: + self._DP = False + + if self.verbose: + message = "geophys. misfits: " + "; ".join( + map( + str, + [ + "{0} (target {1} [{2}])".format(val, tgt, cond) + for val, tgt, cond in zip( + np.round(self.dmlist, 1), + np.round(self.DMtarget, 1), + self.targetlist, + ) + ], + ) + ) + if self.TriggerSmall: + message += ( + " | smallness misfit: {0:.1f} (target: {1:.1f} [{2}])".format( + self.phims(), self.CLtarget, self.CL + ) + ) + if self.TriggerTheta: + message += " | GMM parameters within tolerance: {}".format(self.DP) + print(message) + + if self.AllStop: + self.opt.stopNextIteration = True + if self.verbose: + print("All targets have been reached") + + +class SaveEveryIteration(InversionDirective): + """SaveEveryIteration + + This directive saves an array at each iteration. The default + directory is the current directory and the models are saved as + ``InversionModel-YYYY-MM-DD-HH-MM-iter.npy`` + """ + + def __init__(self, directory=".", name="InversionModel", **kwargs): + super().__init__(**kwargs) + self.directory = directory + self.name = name + + @property + def directory(self): + """Directory to save results in. + + Returns + ------- + str + """ + return self._directory + + @directory.setter + def directory(self, value): + value = validate_string("directory", value) + fullpath = os.path.abspath(os.path.expanduser(value)) + + if not os.path.isdir(fullpath): + os.mkdir(fullpath) + self._directory = value + + @property + def name(self): + """Root of the filename to be saved. + + Returns + ------- + str + """ + return self._name + + @name.setter + def name(self, value): + self._name = validate_string("name", value) + + @property + def fileName(self): + if getattr(self, "_fileName", None) is None: + from datetime import datetime + + self._fileName = "{0!s}-{1!s}".format( + self.name, datetime.now().strftime("%Y-%m-%d-%H-%M") + ) + return self._fileName + + +class SaveModelEveryIteration(SaveEveryIteration): + """SaveModelEveryIteration + + This directive saves the model as a numpy array at each iteration. The + default directory is the current directoy and the models are saved as + ``InversionModel-YYYY-MM-DD-HH-MM-iter.npy`` + """ + + def initialize(self): + print( + "simpeg.SaveModelEveryIteration will save your models as: " + "'{0!s}###-{1!s}.npy'".format(self.directory + os.path.sep, self.fileName) + ) + + def endIter(self): + np.save( + "{0!s}{1:03d}-{2!s}".format( + self.directory + os.path.sep, self.opt.iter, self.fileName + ), + self.opt.xc, + ) + + +class SaveOutputEveryIteration(SaveEveryIteration): + """SaveOutputEveryIteration""" + + def __init__(self, save_txt=True, **kwargs): + super().__init__(**kwargs) + + self.save_txt = save_txt + + @property + def save_txt(self): + """Whether to save the output as a text file. + + Returns + ------- + bool + """ + return self._save_txt + + @save_txt.setter + def save_txt(self, value): + self._save_txt = validate_type("save_txt", value, bool) + + def initialize(self): + if self.save_txt is True: + print( + "simpeg.SaveOutputEveryIteration will save your inversion " + "progress as: '###-{0!s}.txt'".format(self.fileName) + ) + f = open(self.fileName + ".txt", "w") + header = " # beta phi_d phi_m phi_m_small phi_m_smoomth_x phi_m_smoomth_y phi_m_smoomth_z phi\n" + f.write(header) + f.close() + + # Create a list of each + + self.beta = [] + self.phi_d = [] + self.phi_m = [] + self.phi_m_small = [] + self.phi_m_smooth_x = [] + self.phi_m_smooth_y = [] + self.phi_m_smooth_z = [] + self.phi = [] + + def endIter(self): + phi_s, phi_x, phi_y, phi_z = 0, 0, 0, 0 + + for reg in self.reg.objfcts: + if isinstance(reg, Sparse): + i_s, i_x, i_y, i_z = 0, 1, 2, 3 + else: + i_s, i_x, i_y, i_z = 0, 1, 3, 5 + if getattr(reg, "alpha_s", None): + phi_s += reg.objfcts[i_s](self.invProb.model) * reg.alpha_s + if getattr(reg, "alpha_x", None): + phi_x += reg.objfcts[i_x](self.invProb.model) * reg.alpha_x + + if reg.regularization_mesh.dim > 1 and getattr(reg, "alpha_y", None): + phi_y += reg.objfcts[i_y](self.invProb.model) * reg.alpha_y + if reg.regularization_mesh.dim > 2 and getattr(reg, "alpha_z", None): + phi_z += reg.objfcts[i_z](self.invProb.model) * reg.alpha_z + + self.beta.append(self.invProb.beta) + self.phi_d.append(self.invProb.phi_d) + self.phi_m.append(self.invProb.phi_m) + self.phi_m_small.append(phi_s) + self.phi_m_smooth_x.append(phi_x) + self.phi_m_smooth_y.append(phi_y) + self.phi_m_smooth_z.append(phi_z) + self.phi.append(self.opt.f) + + if self.save_txt: + f = open(self.fileName + ".txt", "a") + f.write( + " {0:3d} {1:1.4e} {2:1.4e} {3:1.4e} {4:1.4e} {5:1.4e} " + "{6:1.4e} {7:1.4e} {8:1.4e}\n".format( + self.opt.iter, + self.beta[self.opt.iter - 1], + self.phi_d[self.opt.iter - 1], + self.phi_m[self.opt.iter - 1], + self.phi_m_small[self.opt.iter - 1], + self.phi_m_smooth_x[self.opt.iter - 1], + self.phi_m_smooth_y[self.opt.iter - 1], + self.phi_m_smooth_z[self.opt.iter - 1], + self.phi[self.opt.iter - 1], + ) + ) + f.close() + + def load_results(self): + results = np.loadtxt(self.fileName + str(".txt"), comments="#") + self.beta = results[:, 1] + self.phi_d = results[:, 2] + self.phi_m = results[:, 3] + self.phi_m_small = results[:, 4] + self.phi_m_smooth_x = results[:, 5] + self.phi_m_smooth_y = results[:, 6] + self.phi_m_smooth_z = results[:, 7] + + self.phi_m_smooth = ( + self.phi_m_smooth_x + self.phi_m_smooth_y + self.phi_m_smooth_z + ) + + self.f = results[:, 7] + + self.target_misfit = self.invProb.dmisfit.simulation.survey.nD + self.i_target = None + + if self.invProb.phi_d < self.target_misfit: + i_target = 0 + while self.phi_d[i_target] > self.target_misfit: + i_target += 1 + self.i_target = i_target + + def plot_misfit_curves( + self, + fname=None, + dpi=300, + plot_small_smooth=False, + plot_phi_m=True, + plot_small=False, + plot_smooth=False, + ): + self.target_misfit = np.sum([dmis.nD for dmis in self.invProb.dmisfit.objfcts]) + self.i_target = None + + if self.invProb.phi_d < self.target_misfit: + i_target = 0 + while self.phi_d[i_target] > self.target_misfit: + i_target += 1 + self.i_target = i_target + + fig = plt.figure(figsize=(5, 2)) + ax = plt.subplot(111) + ax_1 = ax.twinx() + ax.semilogy( + np.arange(len(self.phi_d)), self.phi_d, "k-", lw=2, label=r"$\phi_d$" + ) + + if plot_phi_m: + ax_1.semilogy( + np.arange(len(self.phi_d)), self.phi_m, "r", lw=2, label=r"$\phi_m$" + ) + + if plot_small_smooth or plot_small: + ax_1.semilogy( + np.arange(len(self.phi_d)), self.phi_m_small, "ro", label="small" + ) + if plot_small_smooth or plot_smooth: + ax_1.semilogy( + np.arange(len(self.phi_d)), self.phi_m_smooth_x, "rx", label="smooth_x" + ) + ax_1.semilogy( + np.arange(len(self.phi_d)), self.phi_m_smooth_y, "rx", label="smooth_y" + ) + ax_1.semilogy( + np.arange(len(self.phi_d)), self.phi_m_smooth_z, "rx", label="smooth_z" + ) + + ax.legend(loc=1) + ax_1.legend(loc=2) + + ax.plot( + np.r_[ax.get_xlim()[0], ax.get_xlim()[1]], + np.ones(2) * self.target_misfit, + "k:", + ) + ax.set_xlabel("Iteration") + ax.set_ylabel(r"$\phi_d$") + ax_1.set_ylabel(r"$\phi_m$", color="r") + ax_1.tick_params(axis="y", which="both", colors="red") + + plt.show() + if fname is not None: + fig.savefig(fname, dpi=dpi) + + def plot_tikhonov_curves(self, fname=None, dpi=200): + self.target_misfit = self.invProb.dmisfit.simulation.survey.nD + self.i_target = None + + if self.invProb.phi_d < self.target_misfit: + i_target = 0 + while self.phi_d[i_target] > self.target_misfit: + i_target += 1 + self.i_target = i_target + + fig = plt.figure(figsize=(5, 8)) + ax1 = plt.subplot(311) + ax2 = plt.subplot(312) + ax3 = plt.subplot(313) + + ax1.plot(self.beta, self.phi_d, "k-", lw=2, ms=4) + ax1.set_xlim(np.hstack(self.beta).min(), np.hstack(self.beta).max()) + ax1.set_xlabel(r"$\beta$", fontsize=14) + ax1.set_ylabel(r"$\phi_d$", fontsize=14) + + ax2.plot(self.beta, self.phi_m, "k-", lw=2) + ax2.set_xlim(np.hstack(self.beta).min(), np.hstack(self.beta).max()) + ax2.set_xlabel(r"$\beta$", fontsize=14) + ax2.set_ylabel(r"$\phi_m$", fontsize=14) + + ax3.plot(self.phi_m, self.phi_d, "k-", lw=2) + ax3.set_xlim(np.hstack(self.phi_m).min(), np.hstack(self.phi_m).max()) + ax3.set_xlabel(r"$\phi_m$", fontsize=14) + ax3.set_ylabel(r"$\phi_d$", fontsize=14) + + if self.i_target is not None: + ax1.plot(self.beta[self.i_target], self.phi_d[self.i_target], "k*", ms=10) + ax2.plot(self.beta[self.i_target], self.phi_m[self.i_target], "k*", ms=10) + ax3.plot(self.phi_m[self.i_target], self.phi_d[self.i_target], "k*", ms=10) + + for ax in [ax1, ax2, ax3]: + ax.set_xscale("linear") + ax.set_yscale("linear") + plt.tight_layout() + plt.show() + if fname is not None: + fig.savefig(fname, dpi=dpi) + + +class SaveOutputDictEveryIteration(SaveEveryIteration): + """ + Saves inversion parameters at every iteration. + """ + + # Initialize the output dict + def __init__(self, saveOnDisk=False, **kwargs): + super().__init__(**kwargs) + self.saveOnDisk = saveOnDisk + + @property + def saveOnDisk(self): + """Whether to save the output dict to disk. + + Returns + ------- + bool + """ + return self._saveOnDisk + + @saveOnDisk.setter + def saveOnDisk(self, value): + self._saveOnDisk = validate_type("saveOnDisk", value, bool) + + def initialize(self): + self.outDict = {} + if self.saveOnDisk: + print( + "simpeg.SaveOutputDictEveryIteration will save your inversion progress as dictionary: '###-{0!s}.npz'".format( + self.fileName + ) + ) + + def endIter(self): + # regCombo = ["phi_ms", "phi_msx"] + + # if self.simulation[0].mesh.dim >= 2: + # regCombo += ["phi_msy"] + + # if self.simulation[0].mesh.dim == 3: + # regCombo += ["phi_msz"] + + # Initialize the output dict + iterDict = {} + + # Save the data. + iterDict["iter"] = self.opt.iter + iterDict["beta"] = self.invProb.beta + iterDict["phi_d"] = self.invProb.phi_d + iterDict["phi_m"] = self.invProb.phi_m + + # for label, fcts in zip(regCombo, self.reg.objfcts[0].objfcts): + # iterDict[label] = fcts(self.invProb.model) + + iterDict["f"] = self.opt.f + iterDict["m"] = self.invProb.model + iterDict["dpred"] = self.invProb.dpred + + for reg in self.reg.objfcts: + if isinstance(reg, Sparse): + for reg_part, norm in zip(reg.objfcts, reg.norms): + reg_name = f"{type(reg_part).__name__}" + if hasattr(reg_part, "orientation"): + reg_name = reg_part.orientation + " " + reg_name + iterDict[reg_name + ".irls_threshold"] = reg_part.irls_threshold + iterDict[reg_name + ".norm"] = norm + + # Save the file as a npz + if self.saveOnDisk: + np.savez("{:03d}-{:s}".format(self.opt.iter, self.fileName), iterDict) + + self.outDict[self.opt.iter] = iterDict + + +@deprecate_class(removal_version="0.24.0", error=False) +class Update_IRLS(InversionDirective): + f_old = 0 + f_min_change = 1e-2 + beta_tol = 1e-1 + beta_ratio_l2 = None + prctile = 100 + chifact_start = 1.0 + chifact_target = 1.0 + + # Solving parameter for IRLS (mode:2) + irls_iteration = 0 + minGNiter = 1 + iterStart = 0 + sphericalDomain = False + + # Beta schedule + ComboObjFun = False + mode = 1 + coolEpsOptimized = True + coolEps_p = True + coolEps_q = True + floorEps_p = 1e-8 + floorEps_q = 1e-8 + coolEpsFact = 1.2 + silent = False + fix_Jmatrix = False + + def __init__( + self, + max_irls_iterations=20, + update_beta=True, + beta_search=False, + coolingFactor=2.0, + coolingRate=1, + **kwargs, + ): + super().__init__(**kwargs) + self.max_irls_iterations = max_irls_iterations + self.update_beta = update_beta + self.beta_search = beta_search + self.coolingFactor = coolingFactor + self.coolingRate = coolingRate + + @property + def max_irls_iterations(self): + """Maximum irls iterations. + + Returns + ------- + int + """ + return self._max_irls_iterations + + @max_irls_iterations.setter + def max_irls_iterations(self, value): + self._max_irls_iterations = validate_integer( + "max_irls_iterations", value, min_val=0 + ) + + @property + def coolingFactor(self): + """Beta is divided by this value every `coolingRate` iterations. + + Returns + ------- + float + """ + return self._coolingFactor + + @coolingFactor.setter + def coolingFactor(self, value): + self._coolingFactor = validate_float( + "coolingFactor", value, min_val=0.0, inclusive_min=False + ) + + @property + def coolingRate(self): + """Cool after this number of iterations. + + Returns + ------- + int + """ + return self._coolingRate + + @coolingRate.setter + def coolingRate(self, value): + self._coolingRate = validate_integer("coolingRate", value, min_val=1) + + @property + def update_beta(self): + """Whether to update beta. + + Returns + ------- + bool + """ + return self._update_beta + + @update_beta.setter + def update_beta(self, value): + self._update_beta = validate_type("update_beta", value, bool) + + @property + def beta_search(self): + """Whether to do a beta search. + + Returns + ------- + bool + """ + return self._beta_search + + @beta_search.setter + def beta_search(self, value): + self._beta_search = validate_type("beta_search", value, bool) + + @property + def target(self): + if getattr(self, "_target", None) is None: + nD = 0 + for survey in self.survey: + nD += survey.nD + + self._target = nD * self.chifact_target + + return self._target + + @target.setter + def target(self, val): + self._target = val + + @property + def start(self): + if getattr(self, "_start", None) is None: + if isinstance(self.survey, list): + self._start = 0 + for survey in self.survey: + self._start += survey.nD * self.chifact_start + + else: + self._start = self.survey.nD * self.chifact_start + return self._start + + @start.setter + def start(self, val): + self._start = val + + def initialize(self): + if self.mode == 1: + self.norms = [] + for reg in self.reg.objfcts: + if not isinstance(reg, Sparse): + continue + self.norms.append(reg.norms) + reg.norms = [2.0 for obj in reg.objfcts] + reg.model = self.invProb.model + + # Update the model used by the regularization + for reg in self.reg.objfcts: + if not isinstance(reg, Sparse): + continue + + reg.model = self.invProb.model + + if self.sphericalDomain: + self.angleScale() + + def endIter(self): + if self.sphericalDomain: + self.angleScale() + + # Check if misfit is within the tolerance, otherwise scale beta + if np.all( + [ + np.abs(1.0 - self.invProb.phi_d / self.target) > self.beta_tol, + self.update_beta, + self.mode != 1, + ] + ): + ratio = self.target / self.invProb.phi_d + + if ratio > 1: + ratio = np.mean([2.0, ratio]) + else: + ratio = np.mean([0.75, ratio]) + + self.invProb.beta = self.invProb.beta * ratio + + if np.all([self.mode != 1, self.beta_search]): + print("Beta search step") + # self.update_beta = False + # Re-use previous model and continue with new beta + self.invProb.model = self.reg.objfcts[0].model + self.opt.xc = self.reg.objfcts[0].model + self.opt.iter -= 1 + return + + elif np.all([self.mode == 1, self.opt.iter % self.coolingRate == 0]): + self.invProb.beta = self.invProb.beta / self.coolingFactor + + # After reaching target misfit with l2-norm, switch to IRLS (mode:2) + if np.all([self.invProb.phi_d < self.start, self.mode == 1]): + self.start_irls() + + # Only update after GN iterations + if np.all( + [(self.opt.iter - self.iterStart) % self.minGNiter == 0, self.mode != 1] + ): + if self.stopping_criteria(): + self.opt.stopNextIteration = True + return + + # Print to screen + for reg in self.reg.objfcts: + if not isinstance(reg, Sparse): + continue + + for obj in reg.objfcts: + if isinstance(reg, (Sparse, BaseSparse)): + obj.irls_threshold = obj.irls_threshold / self.coolEpsFact + + self.irls_iteration += 1 + + # Reset the regularization matrices so that it is + # recalculated for current model. Do it to all levels of comboObj + for reg in self.reg.objfcts: + if not isinstance(reg, Sparse): + continue + + reg.update_weights(reg.model) + + self.update_beta = True + self.invProb.phi_m_last = self.reg(self.invProb.model) + + def start_irls(self): + if not self.silent: + print( + "Reached starting chifact with l2-norm regularization:" + + " Start IRLS steps..." + ) + + self.mode = 2 + + if getattr(self.opt, "iter", None) is None: + self.iterStart = 0 + else: + self.iterStart = self.opt.iter + + self.invProb.phi_m_last = self.reg(self.invProb.model) + + # Either use the supplied irls_threshold, or fix base on distribution of + # model values + for reg in self.reg.objfcts: + if not isinstance(reg, Sparse): + continue + + for obj in reg.objfcts: + threshold = np.percentile( + np.abs(obj.mapping * obj._delta_m(self.invProb.model)), self.prctile + ) + if isinstance(obj, SmoothnessFirstOrder): + threshold /= reg.regularization_mesh.base_length + + obj.irls_threshold = threshold + + # Re-assign the norms supplied by user l2 -> lp + for reg, norms in zip(self.reg.objfcts, self.norms): + if not isinstance(reg, Sparse): + continue + reg.norms = norms + + # Save l2-model + self.invProb.l2model = self.invProb.model.copy() + + # Print to screen + for reg in self.reg.objfcts: + if not isinstance(reg, Sparse): + continue + if not self.silent: + print("irls_threshold " + str(reg.objfcts[0].irls_threshold)) + + def angleScale(self): + """ + Update the scales used by regularization for the + different block of models + """ + # Currently implemented for MVI-S only + max_p = [] + for reg in self.reg.objfcts[0].objfcts: + f_m = abs(reg.f_m(reg.model)) + max_p += [np.max(f_m)] + + max_p = np.asarray(max_p).max() + + max_s = [np.pi, np.pi] + + for reg, var in zip(self.reg.objfcts[1:], max_s): + for obj in reg.objfcts: + # TODO Need to make weights_shapes a public method + obj.set_weights( + angle_scale=np.ones(obj._weights_shapes[0]) * max_p / var + ) + + def validate(self, directiveList): + dList = directiveList.dList + self_ind = dList.index(self) + lin_precond_ind = [isinstance(d, UpdatePreconditioner) for d in dList] + + if any(lin_precond_ind): + assert lin_precond_ind.index(True) > self_ind, ( + "The directive 'UpdatePreconditioner' must be after Update_IRLS " + "in the directiveList" + ) + else: + warnings.warn( + "Without a Linear preconditioner, convergence may be slow. " + "Consider adding `Directives.UpdatePreconditioner` to your " + "directives list", + stacklevel=2, + ) + return True + + def stopping_criteria(self): + """ + Check for stopping criteria of max_irls_iteration or minimum change. + """ + phim_new = 0 + for reg in self.reg.objfcts: + if isinstance(reg, (Sparse, BaseSparse)): + reg.model = self.invProb.model + phim_new += reg(reg.model) + + # Check for maximum number of IRLS cycles1 + if self.irls_iteration == self.max_irls_iterations: + if not self.silent: + print( + "Reach maximum number of IRLS cycles:" + + " {0:d}".format(self.max_irls_iterations) + ) + return True + + # Check if the function has changed enough + f_change = np.abs(self.f_old - phim_new) / (self.f_old + 1e-12) + if np.all( + [ + f_change < self.f_min_change, + self.irls_iteration > 1, + np.abs(1.0 - self.invProb.phi_d / self.target) < self.beta_tol, + ] + ): + print("Minimum decrease in regularization." + "End of IRLS") + return True + + self.f_old = phim_new + + return False + + +class UpdatePreconditioner(InversionDirective): + """ + Create a Jacobi preconditioner for the linear problem + """ + + def __init__(self, update_every_iteration=True, **kwargs): + super().__init__(**kwargs) + self.update_every_iteration = update_every_iteration + + @property + def update_every_iteration(self): + """Whether to update the preconditioner at every iteration. + + Returns + ------- + bool + """ + return self._update_every_iteration + + @update_every_iteration.setter + def update_every_iteration(self, value): + self._update_every_iteration = validate_type( + "update_every_iteration", value, bool + ) + + def initialize(self): + # Create the pre-conditioner + regDiag = np.zeros_like(self.invProb.model) + m = self.invProb.model + + for reg in self.reg.objfcts: + # Check if regularization has a projection + rdg = reg.deriv2(m) + if not isinstance(rdg, Zero): + regDiag += rdg.diagonal() + + JtJdiag = np.zeros_like(self.invProb.model) + for sim, dmisfit in zip(self.simulation, self.dmisfit.objfcts): + if getattr(sim, "getJtJdiag", None) is None: + assert getattr(sim, "getJ", None) is not None, ( + "Simulation does not have a getJ attribute." + + "Cannot form the sensitivity explicitly" + ) + JtJdiag += np.sum(np.power((dmisfit.W * sim.getJ(m)), 2), axis=0) + else: + JtJdiag += sim.getJtJdiag(m, W=dmisfit.W) + + diagA = JtJdiag + self.invProb.beta * regDiag + diagA[diagA != 0] = diagA[diagA != 0] ** -1.0 + PC = sdiag((diagA)) + + self.opt.approxHinv = PC + + def endIter(self): + # Cool the threshold parameter + if self.update_every_iteration is False: + return + + # Create the pre-conditioner + regDiag = np.zeros_like(self.invProb.model) + m = self.invProb.model + + for reg in self.reg.objfcts: + # Check if he has wire + regDiag += reg.deriv2(m).diagonal() + + JtJdiag = np.zeros_like(self.invProb.model) + for sim, dmisfit in zip(self.simulation, self.dmisfit.objfcts): + if getattr(sim, "getJtJdiag", None) is None: + assert getattr(sim, "getJ", None) is not None, ( + "Simulation does not have a getJ attribute." + + "Cannot form the sensitivity explicitly" + ) + JtJdiag += np.sum(np.power((dmisfit.W * sim.getJ(m)), 2), axis=0) + else: + JtJdiag += sim.getJtJdiag(m, W=dmisfit.W) + + diagA = JtJdiag + self.invProb.beta * regDiag + diagA[diagA != 0] = diagA[diagA != 0] ** -1.0 + PC = sdiag((diagA)) + self.opt.approxHinv = PC + + +class Update_Wj(InversionDirective): + """ + Create approx-sensitivity base weighting using the probing method + """ + + def __init__(self, k=None, itr=None, **kwargs): + self.k = k + self.itr = itr + super().__init__(**kwargs) + + @property + def k(self): + """Number of probing cycles for the estimator. + + Returns + ------- + int + """ + return self._k + + @k.setter + def k(self, value): + if value is not None: + value = validate_integer("k", value, min_val=1) + self._k = value + + @property + def itr(self): + """Which iteration to update the sensitivity. + + Will always update if `None`. + + Returns + ------- + int or None + """ + return self._itr + + @itr.setter + def itr(self, value): + if value is not None: + value = validate_integer("itr", value, min_val=1) + self._itr = value + + def endIter(self): + if self.itr is None or self.itr == self.opt.iter: + m = self.invProb.model + if self.k is None: + self.k = int(self.survey.nD / 10) + + def JtJv(v): + Jv = self.simulation.Jvec(m, v) + + return self.simulation.Jtvec(m, Jv) + + JtJdiag = estimate_diagonal(JtJv, len(m), k=self.k) + JtJdiag = JtJdiag / max(JtJdiag) + + self.reg.wght = JtJdiag + + +class UpdateSensitivityWeights(InversionDirective): + r""" + Sensitivity weighting for linear and non-linear least-squares inverse problems. + + This directive computes the root-mean squared sensitivities for the + forward simulation(s) attached to the inverse problem, then truncates + and scales the result to create cell weights which are applied in the regularization. + The underlying theory is provided below in the `Notes` section. + + This directive **requires** that the map for the regularization function is either + class:`simpeg.maps.Wires` or class:`simpeg.maps.Identity`. In other words, the + sensitivity weighting cannot be applied for parametric inversion. In addition, + the simulation(s) connected to the inverse problem **must** have a ``getJ`` or + ``getJtJdiag`` method. + + This directive's place in the :class:`DirectivesList` **must** be + before any directives which update the preconditioner for the inverse problem + (i.e. :class:`UpdatePreconditioner`), and **must** be before any directives that + estimate the starting trade-off parameter (i.e. :class:`EstimateBeta_ByEig` + and :class:`EstimateBetaMaxDerivative`). + + Parameters + ---------- + every_iteration : bool + When ``True``, update sensitivity weighting at every model update; non-linear problems. + When ``False``, create sensitivity weights for starting model only; linear problems. + threshold : float + Threshold value for smallest weighting value. + threshold_method : {'amplitude', 'global', 'percentile'} + Threshold method for how `threshold_value` is applied: + + - amplitude: + the smallest root-mean squared sensitivity is a fractional percent of the largest value; must be between 0 and 1. + - global: + `threshold_value` is added to the cell weights prior to normalization; must be greater than 0. + - percentile: + the smallest root-mean squared sensitivity is set using percentile threshold; must be between 0 and 100. + + normalization_method : {'maximum', 'min_value', None} + Normalization method applied to sensitivity weights. + + Options are: + + - maximum: + sensitivity weights are normalized by the largest value such that the largest weight is equal to 1. + - minimum: + sensitivity weights are normalized by the smallest value, after thresholding, such that the smallest weights are equal to 1. + - ``None``: + normalization is not applied. + + Notes + ----- + Let :math:`\mathbf{J}` represent the Jacobian. To create sensitivity weights, root-mean squared (RMS) sensitivities + :math:`\mathbf{s}` are computed by summing the squares of the rows of the Jacobian: + + .. math:: + \mathbf{s} = \Bigg [ \sum_i \, \mathbf{J_{i, \centerdot }}^2 \, \Bigg ]^{1/2} + + The dynamic range of RMS sensitivities can span many orders of magnitude. When computing sensitivity + weights, thresholding is generally applied to set a minimum value. + + **Thresholding:** + + If **global** thresholding is applied, we add a constant :math:`\tau` to the RMS sensitivities: + + .. math:: + \mathbf{\tilde{s}} = \mathbf{s} + \tau + + In the case of **percentile** thresholding, we let :math:`s_{\%}` represent a given percentile. + Thresholding to set a minimum value is applied as follows: + + .. math:: + \tilde{s}_j = \begin{cases} + s_j \;\; for \;\; s_j \geq s_{\%} \\ + s_{\%} \;\; for \;\; s_j < s_{\%} + \end{cases} + + If **absolute** thresholding is applied, we define :math:`\eta` as a fractional percent. + In this case, thresholding is applied as follows: + + .. math:: + \tilde{s}_j = \begin{cases} + s_j \;\; for \;\; s_j \geq \eta s_{max} \\ + \eta s_{max} \;\; for \;\; s_j < \eta s_{max} + \end{cases} + """ + + def __init__( + self, + every_iteration=False, + threshold_value=1e-12, + threshold_method="amplitude", + normalization_method="maximum", + **kwargs, + ): + # Raise errors on deprecated arguments + if (key := "everyIter") in kwargs.keys(): + raise TypeError( + f"'{key}' property has been removed. Please use 'every_iteration'.", + ) + if (key := "threshold") in kwargs.keys(): + raise TypeError( + f"'{key}' property has been removed. Please use 'threshold_value'.", + ) + if (key := "normalization") in kwargs.keys(): + raise TypeError( + f"'{key}' property has been removed. " + "Please define normalization using 'normalization_method'.", + ) + + super().__init__(**kwargs) + + self.every_iteration = every_iteration + self.threshold_value = threshold_value + self.threshold_method = threshold_method + self.normalization_method = normalization_method + + @property + def every_iteration(self): + """Update sensitivity weights when model is updated. + + When ``True``, update sensitivity weighting at every model update; non-linear problems. + When ``False``, create sensitivity weights for starting model only; linear problems. + + Returns + ------- + bool + """ + return self._every_iteration + + @every_iteration.setter + def every_iteration(self, value): + self._every_iteration = validate_type("every_iteration", value, bool) + + everyIter = deprecate_property( + every_iteration, + "everyIter", + "every_iteration", + removal_version="0.20.0", + error=True, + ) + + @property + def threshold_value(self): + """Threshold value used to set minimum weighting value. + + The way thresholding is applied to the weighting model depends on the + `threshold_method` property. The choices for `threshold_method` are: + + - global: + `threshold_value` is added to the cell weights prior to normalization; must be greater than 0. + - percentile: + `threshold_value` is a percentile cutoff; must be between 0 and 100 + - amplitude: + `threshold_value` is the fractional percent of the largest value; must be between 0 and 1 + + + Returns + ------- + float + """ + return self._threshold_value + + @threshold_value.setter + def threshold_value(self, value): + self._threshold_value = validate_float("threshold_value", value, min_val=0.0) + + threshold = deprecate_property( + threshold_value, + "threshold", + "threshold_value", + removal_version="0.20.0", + error=True, + ) + + @property + def threshold_method(self): + """Threshold method for how `threshold_value` is applied: + + - global: + `threshold_value` is added to the cell weights prior to normalization; must be greater than 0. + - percentile: + the smallest root-mean squared sensitivity is set using percentile threshold; must be between 0 and 100 + - amplitude: + the smallest root-mean squared sensitivity is a fractional percent of the largest value; must be between 0 and 1 + + + Returns + ------- + str + """ + return self._threshold_method + + @threshold_method.setter + def threshold_method(self, value): + self._threshold_method = validate_string( + "threshold_method", value, string_list=["global", "percentile", "amplitude"] + ) + + @property + def normalization_method(self): + """Normalization method applied to sensitivity weights. + + Options are: + + - ``None`` + normalization is not applied + - maximum: + sensitivity weights are normalized by the largest value such that the largest weight is equal to 1. + - minimum: + sensitivity weights are normalized by the smallest value, after thresholding, such that the smallest weights are equal to 1. + + Returns + ------- + None, str + """ + return self._normalization_method + + @normalization_method.setter + def normalization_method(self, value): + if value is None: + self._normalization_method = value + else: + self._normalization_method = validate_string( + "normalization_method", value, string_list=["minimum", "maximum"] + ) + + normalization = deprecate_property( + normalization_method, + "normalization", + "normalization_method", + removal_version="0.20.0", + error=True, + ) + + def initialize(self): + """Compute sensitivity weights upon starting the inversion.""" + for reg in self.reg.objfcts: + if not isinstance(reg.mapping, (IdentityMap, Wires)): + raise TypeError( + f"Mapping for the regularization must be of type {IdentityMap} or {Wires}. " + + f"Input mapping of type {type(reg.mapping)}." + ) + + self.update() + + def endIter(self): + """Execute end of iteration.""" + + if self.every_iteration: + self.update() + + def update(self): + """Update sensitivity weights""" + + jtj_diag = np.zeros_like(self.invProb.model) + m = self.invProb.model + + for sim, dmisfit in zip(self.simulation, self.dmisfit.objfcts): + if getattr(sim, "getJtJdiag", None) is None: + if getattr(sim, "getJ", None) is None: + raise AttributeError( + "Simulation does not have a getJ attribute." + + "Cannot form the sensitivity explicitly" + ) + jtj_diag += mkvc(np.sum((dmisfit.W * sim.getJ(m)) ** 2.0, axis=0)) + else: + jtj_diag += sim.getJtJdiag(m, W=dmisfit.W) + + # Compute and sum root-mean squared sensitivities for all objective functions + wr = np.zeros_like(self.invProb.model) + for reg in self.reg.objfcts: + if isinstance(reg, BaseSimilarityMeasure): + continue + + mesh = reg.regularization_mesh + n_cells = mesh.nC + mapped_jtj_diag = reg.mapping * jtj_diag + # reshape the mapped, so you can divide by volume + # (let's say it was a vector or anisotropic model) + mapped_jtj_diag = mapped_jtj_diag.reshape((n_cells, -1), order="F") + wr_temp = mapped_jtj_diag / reg.regularization_mesh.vol[:, None] ** 2.0 + wr_temp = wr_temp.reshape(-1, order="F") + + wr += reg.mapping.deriv(self.invProb.model).T * wr_temp + + wr **= 0.5 + + # Apply thresholding + if self.threshold_method == "global": + wr += self.threshold_value + elif self.threshold_method == "percentile": + wr = np.clip( + wr, a_min=np.percentile(wr, self.threshold_value), a_max=np.inf + ) + else: + wr = np.clip(wr, a_min=self.threshold_value * wr.max(), a_max=np.inf) + + # Apply normalization + if self.normalization_method == "maximum": + wr /= wr.max() + elif self.normalization_method == "minimum": + wr /= wr.min() + + # Add sensitivity weighting to all model objective functions + for reg in self.reg.objfcts: + if not isinstance(reg, BaseSimilarityMeasure): + sub_regs = getattr(reg, "objfcts", [reg]) + for sub_reg in sub_regs: + sub_reg.set_weights(sensitivity=sub_reg.mapping * wr) + + def validate(self, directiveList): + """Validate directive against directives list. + + The ``UpdateSensitivityWeights`` directive impacts the regularization by applying + cell weights. As a result, its place in the :class:`DirectivesList` must be + before any directives which update the preconditioner for the inverse problem + (i.e. :class:`UpdatePreconditioner`), and must be before any directives that + estimate the starting trade-off parameter (i.e. :class:`EstimateBeta_ByEig` + and :class:`EstimateBetaMaxDerivative`). + + + Returns + ------- + bool + Returns ``True`` if validation passes. Otherwise, an error is thrown. + """ + # check if a beta estimator is in the list after setting the weights + dList = directiveList.dList + self_ind = dList.index(self) + + beta_estimator_ind = [isinstance(d, BaseBetaEstimator) for d in dList] + lin_precond_ind = [isinstance(d, UpdatePreconditioner) for d in dList] + + if any(beta_estimator_ind): + assert beta_estimator_ind.index(True) > self_ind, ( + "The directive for setting intial beta must be after UpdateSensitivityWeights " + "in the directiveList" + ) + + if any(lin_precond_ind): + assert lin_precond_ind.index(True) > self_ind, ( + "The directive 'UpdatePreconditioner' must be after UpdateSensitivityWeights " + "in the directiveList" + ) + + return True + + +class ProjectSphericalBounds(InversionDirective): + r""" + Trick for spherical coordinate system. + Project :math:`\theta` and :math:`\phi` angles back to :math:`[-\pi,\pi]` + using back and forth conversion. + spherical->cartesian->spherical + """ + + def initialize(self): + x = self.invProb.model + # Convert to cartesian than back to avoid over rotation + nC = int(len(x) / 3) + + xyz = spherical2cartesian(x.reshape((nC, 3), order="F")) + m = cartesian2spherical(xyz.reshape((nC, 3), order="F")) + + self.invProb.model = m + + for sim in self.simulation: + sim.model = m + + self.opt.xc = m + + def endIter(self): + x = self.invProb.model + nC = int(len(x) / 3) + + # Convert to cartesian than back to avoid over rotation + xyz = spherical2cartesian(x.reshape((nC, 3), order="F")) + m = cartesian2spherical(xyz.reshape((nC, 3), order="F")) + + self.invProb.model = m + + phi_m_last = [] + for reg in self.reg.objfcts: + reg.model = self.invProb.model + phi_m_last += [reg(self.invProb.model)] + + self.invProb.phi_m_last = phi_m_last + + for sim in self.simulation: + sim.model = m + + self.opt.xc = m diff --git a/simpeg/directives/_pgi_directives.py b/simpeg/directives/_pgi_directives.py new file mode 100644 index 0000000000..60f4488b90 --- /dev/null +++ b/simpeg/directives/_pgi_directives.py @@ -0,0 +1,474 @@ +############################################################################### +# # +# Directives for PGI: Petrophysically guided Regularization # +# # +############################################################################### + +import copy + +import numpy as np + +from ..directives import InversionDirective, MultiTargetMisfits +from ..regularization import ( + PGI, + PGIsmallness, + SmoothnessFirstOrder, + SparseSmoothness, +) +from ..utils import ( + GaussianMixtureWithNonlinearRelationships, + GaussianMixtureWithNonlinearRelationshipsWithPrior, + GaussianMixtureWithPrior, + WeightedGaussianMixture, + mkvc, +) + + +class PGI_UpdateParameters(InversionDirective): + """ + This directive is to be used with regularization from regularization.pgi. + It updates: + - the reference model and weights in the smallness (L2-approximation of PGI) + - the GMM as a MAP estimate between the prior and the current model + For more details, please consult: + - https://doi.org/10.1093/gji/ggz389 + """ + + verbose = False # print info. about the GMM at each iteration + update_rate = 1 # updates at each `update_rate` iterations + update_gmm = False # update the GMM + zeta = ( + 1e10 # confidence in the prior proportions; default: high value, keep GMM fixed + ) + nu = ( + 1e10 # confidence in the prior covariances; default: high value, keep GMM fixed + ) + kappa = 1e10 # confidence in the prior means;default: high value, keep GMM fixed + update_covariances = ( + True # Average the covariances, If false: average the precisions + ) + fixed_membership = None # keep the membership of specific cells fixed + keep_ref_fixed_in_Smooth = True # keep mref fixed in the Smoothness + + def initialize(self): + pgi_reg = self.reg.get_functions_of_type(PGIsmallness) + if len(pgi_reg) != 1: + raise UserWarning( + "'PGI_UpdateParameters' requires one 'PGIsmallness' regularization " + "in the objective function." + ) + self.pgi_reg = pgi_reg[0] + + def endIter(self): + if self.opt.iter > 0 and self.opt.iter % self.update_rate == 0: + m = self.invProb.model + modellist = self.pgi_reg.wiresmap * m + model = np.c_[[a * b for a, b in zip(self.pgi_reg.maplist, modellist)]].T + + if self.update_gmm and isinstance( + self.pgi_reg.gmmref, GaussianMixtureWithNonlinearRelationships + ): + clfupdate = GaussianMixtureWithNonlinearRelationshipsWithPrior( + gmmref=self.pgi_reg.gmmref, + zeta=self.zeta, + kappa=self.kappa, + nu=self.nu, + verbose=self.verbose, + prior_type="semi", + update_covariances=self.update_covariances, + max_iter=self.pgi_reg.gmm.max_iter, + n_init=self.pgi_reg.gmm.n_init, + reg_covar=self.pgi_reg.gmm.reg_covar, + weights_init=self.pgi_reg.gmm.weights_, + means_init=self.pgi_reg.gmm.means_, + precisions_init=self.pgi_reg.gmm.precisions_, + random_state=self.pgi_reg.gmm.random_state, + tol=self.pgi_reg.gmm.tol, + verbose_interval=self.pgi_reg.gmm.verbose_interval, + warm_start=self.pgi_reg.gmm.warm_start, + fixed_membership=self.fixed_membership, + ) + clfupdate = clfupdate.fit(model) + + elif self.update_gmm and isinstance( + self.pgi_reg.gmmref, WeightedGaussianMixture + ): + clfupdate = GaussianMixtureWithPrior( + gmmref=self.pgi_reg.gmmref, + zeta=self.zeta, + kappa=self.kappa, + nu=self.nu, + verbose=self.verbose, + prior_type="semi", + update_covariances=self.update_covariances, + max_iter=self.pgi_reg.gmm.max_iter, + n_init=self.pgi_reg.gmm.n_init, + reg_covar=self.pgi_reg.gmm.reg_covar, + weights_init=self.pgi_reg.gmm.weights_, + means_init=self.pgi_reg.gmm.means_, + precisions_init=self.pgi_reg.gmm.precisions_, + random_state=self.pgi_reg.gmm.random_state, + tol=self.pgi_reg.gmm.tol, + verbose_interval=self.pgi_reg.gmm.verbose_interval, + warm_start=self.pgi_reg.gmm.warm_start, + fixed_membership=self.fixed_membership, + ) + clfupdate = clfupdate.fit(model) + + else: + clfupdate = copy.deepcopy(self.pgi_reg.gmmref) + + self.pgi_reg.gmm = clfupdate + membership = self.pgi_reg.gmm.predict(model) + + if self.fixed_membership is not None: + membership[self.fixed_membership[:, 0]] = self.fixed_membership[:, 1] + + mref = mkvc(self.pgi_reg.gmm.means_[membership]) + self.pgi_reg.reference_model = mref + if getattr(self.fixed_membership, "shape", [0, 0])[0] < len(membership): + self.pgi_reg._r_second_deriv = None + + +class PGI_BetaAlphaSchedule(InversionDirective): + """ + This directive is to be used with regularizations from regularization.pgi. + It implements the strategy described in https://doi.org/10.1093/gji/ggz389 + for iteratively updating beta and alpha_s for fitting the + geophysical and smallness targets. + """ + + verbose = False # print information (progress, updates made) + tolerance = 0.0 # tolerance on the geophysical target misfit for cooling + progress = 0.1 # minimum percentage progress (default 10%) before cooling beta + coolingFactor = 2.0 # when cooled, beta is divided by it + warmingFactor = 1.0 # when warmed, alpha_s is multiplied by the ratio of the + # geophysical target with their current misfit, times this factor + mode = 1 # mode 1: start with nothing fitted. Mode 2: warmstart with fitted geophysical data + alphasmax = 1e10 # max alpha_s + betamin = 1e-10 # minimum beta + update_rate = 1 # update every `update_rate` iterations + pgi_reg = None + ratio_in_cooling = ( + False # add the ratio of geophysical misfit with their target in cooling + ) + + def initialize(self): + """Initialize the directive.""" + self.update_previous_score() + self.update_previous_dmlist() + + def endIter(self): + """Run after the end of each iteration in the inversion.""" + # Get some variables from the MultiTargetMisfits directive + data_misfits_achieved = self.multi_target_misfits_directive.DM + data_misfits_target = self.multi_target_misfits_directive.DMtarget + dmlist = self.multi_target_misfits_directive.dmlist + targetlist = self.multi_target_misfits_directive.targetlist + + # Change mode if data misfit targets have been achieved + if data_misfits_achieved: + self.mode = 2 + + # Don't cool beta of warm alpha if we are in the first iteration or if + # the current iteration doesn't match the update rate + if self.opt.iter == 0 or self.opt.iter % self.update_rate != 0: + self.update_previous_score() + self.update_previous_dmlist() + return None + + if self.verbose: + targets = np.round( + np.maximum( + (1.0 - self.progress) * self.previous_dmlist, + (1.0 + self.tolerance) * data_misfits_target, + ), + decimals=1, + ) + dmlist_rounded = np.round(dmlist, decimals=1) + print( + f"Beta cooling evaluation: progress: {dmlist_rounded}; " + f"minimum progress targets: {targets}" + ) + + # Decide if we should cool beta + threshold = np.maximum( + (1.0 - self.progress) * self.previous_dmlist[~targetlist], + data_misfits_target[~targetlist], + ) + if ( + (dmlist[~targetlist] > threshold).all() + and not data_misfits_achieved + and self.mode == 1 + and self.invProb.beta > self.betamin + ): + self.cool_beta() + if self.verbose: + print("Decreasing beta to counter data misfit decrase plateau.") + + # Decide if we should warm alpha instead + elif ( + data_misfits_achieved + and self.mode == 2 + and np.all(self.pgi_regularization.alpha_pgi < self.alphasmax) + ): + self.warm_alpha() + if self.verbose: + print( + "Warming alpha_pgi to favor clustering: ", + self.pgi_regularization.alpha_pgi, + ) + + # Decide if we should cool beta (to counter data misfit increase) + elif ( + np.any(dmlist > (1.0 + self.tolerance) * data_misfits_target) + and self.mode == 2 + and self.invProb.beta > self.betamin + ): + self.cool_beta() + if self.verbose: + print("Decreasing beta to counter data misfit increase.") + + # Update previous score and dmlist + self.update_previous_score() + self.update_previous_dmlist() + + def cool_beta(self): + """Cool beta according to schedule.""" + data_misfits_target = self.multi_target_misfits_directive.DMtarget + dmlist = self.multi_target_misfits_directive.dmlist + ratio = 1.0 + indx = dmlist > (1.0 + self.tolerance) * data_misfits_target + if np.any(indx) and self.ratio_in_cooling: + ratio = np.median([dmlist[indx] / data_misfits_target[indx]]) + self.invProb.beta /= self.coolingFactor * ratio + + def warm_alpha(self): + """Warm alpha according to schedule.""" + data_misfits_target = self.multi_target_misfits_directive.DMtarget + dmlist = self.multi_target_misfits_directive.dmlist + ratio = np.median(data_misfits_target / dmlist) + self.pgi_regularization.alpha_pgi *= self.warmingFactor * ratio + + def update_previous_score(self): + """ + Update the value of the ``previous_score`` attribute. + + Update it with the current value of the petrophysical misfit, obtained + from the :meth:`MultiTargetMisfit.phims()` method. + """ + self.previous_score = copy.deepcopy(self.multi_target_misfits_directive.phims()) + + def update_previous_dmlist(self): + """ + Update the value of the ``previous_dmlist`` attribute. + + Update it with the current value of the data misfits, obtained + from the :meth:`MultiTargetMisfit.dmlist` attribute. + """ + self.previous_dmlist = copy.deepcopy(self.multi_target_misfits_directive.dmlist) + + @property + def directives(self): + """List of all the directives in the :class:`simpeg.inverison.BaseInversion``.""" + return self.inversion.directiveList.dList + + @property + def multi_target_misfits_directive(self): + """``MultiTargetMisfit`` directive in the :class:`simpeg.inverison.BaseInversion``.""" + if not hasattr(self, "_mtm_directive"): + # Obtain multi target misfits directive from the directive list + multi_target_misfits_directive = [ + directive + for directive in self.directives + if isinstance(directive, MultiTargetMisfits) + ] + if not multi_target_misfits_directive: + raise UserWarning( + "No MultiTargetMisfits directive found in the current inversion. " + "A MultiTargetMisfits directive is needed by the " + "PGI_BetaAlphaSchedule directive." + ) + (self._mtm_directive,) = multi_target_misfits_directive + return self._mtm_directive + + @property + def pgi_update_params_directive(self): + """``PGI_UpdateParam``s directive in the :class:`simpeg.inverison.BaseInversion``.""" + if not hasattr(self, "_pgi_update_params"): + # Obtain PGI_UpdateParams directive from the directive list + pgi_update_params_directive = [ + directive + for directive in self.directives + if isinstance(directive, PGI_UpdateParameters) + ] + if pgi_update_params_directive: + (self._pgi_update_params,) = pgi_update_params_directive + else: + self._pgi_update_params = None + return self._pgi_update_params + + @property + def pgi_regularization(self): + """PGI regularization in the :class:`simpeg.inverse_problem.BaseInvProblem``.""" + if not hasattr(self, "_pgi_regularization"): + pgi_regularization = self.reg.get_functions_of_type(PGI) + if len(pgi_regularization) != 1: + raise UserWarning( + "'PGI_UpdateParameters' requires one 'PGI' regularization " + "in the objective function." + ) + self._pgi_regularization = pgi_regularization[0] + return self._pgi_regularization + + +class PGI_AddMrefInSmooth(InversionDirective): + """ + This directive is to be used with regularizations from regularization.pgi. + It implements the strategy described in https://doi.org/10.1093/gji/ggz389 + for including the learned reference model, once stable, in the smoothness terms. + """ + + # Chi factor for Data Misfit + chifact = 1.0 + tolerance_phid = 0.0 + phi_d_target = None + wait_till_stable = True + tolerance = 0.0 + verbose = False + + def initialize(self): + targetclass = np.r_[ + [ + isinstance(dirpart, MultiTargetMisfits) + for dirpart in self.inversion.directiveList.dList + ] + ] + if ~np.any(targetclass): + self.DMtarget = None + else: + self.targetclass = np.where(targetclass)[0][-1] + self._DMtarget = self.inversion.directiveList.dList[ + self.targetclass + ].DMtarget + + self.pgi_updategmm_class = np.r_[ + [ + isinstance(dirpart, PGI_UpdateParameters) + for dirpart in self.inversion.directiveList.dList + ] + ] + + if getattr(self.reg.objfcts[0], "objfcts", None) is not None: + # Find the petrosmallness terms in a two-levels combo-regularization. + petrosmallness = np.where( + np.r_[[isinstance(regpart, PGI) for regpart in self.reg.objfcts]] + )[0][0] + self.petrosmallness = petrosmallness + + # Find the smoothness terms in a two-levels combo-regularization. + Smooth = [] + for i, regobjcts in enumerate(self.reg.objfcts): + for j, regpart in enumerate(regobjcts.objfcts): + Smooth += [ + [ + i, + j, + isinstance( + regpart, (SmoothnessFirstOrder, SparseSmoothness) + ), + ] + ] + self.Smooth = np.r_[Smooth] + + self.nbr = np.sum( + [len(self.reg.objfcts[i].objfcts) for i in range(len(self.reg.objfcts))] + ) + self._regmode = 1 + self.pgi_reg = self.reg.objfcts[self.petrosmallness] + + else: + self._regmode = 2 + self.pgi_reg = self.reg + self.nbr = len(self.reg.objfcts) + self.Smooth = np.r_[ + [ + isinstance(regpart, (SmoothnessFirstOrder, SparseSmoothness)) + for regpart in self.reg.objfcts + ] + ] + self._regmode = 2 + + if ~np.any(self.pgi_updategmm_class): + self.previous_membership = self.pgi_reg.membership(self.invProb.model) + else: + self.previous_membership = self.pgi_reg.compute_quasi_geology_model() + + @property + def DMtarget(self): + if getattr(self, "_DMtarget", None) is None: + self.phi_d_target = self.invProb.dmisfit.survey.nD + self._DMtarget = self.chifact * self.phi_d_target + return self._DMtarget + + @DMtarget.setter + def DMtarget(self, val): + self._DMtarget = val + + def endIter(self): + self.DM = self.inversion.directiveList.dList[self.targetclass].DM + self.dmlist = self.inversion.directiveList.dList[self.targetclass].dmlist + + if ~np.any(self.pgi_updategmm_class): + self.membership = self.pgi_reg.membership(self.invProb.model) + else: + self.membership = self.pgi_reg.compute_quasi_geology_model() + + same_mref = np.all(self.membership == self.previous_membership) + percent_diff = ( + len(self.membership) + - np.count_nonzero(self.previous_membership == self.membership) + ) / len(self.membership) + if self.verbose: + print( + "mref changed in ", + len(self.membership) + - np.count_nonzero(self.previous_membership == self.membership), + " places", + ) + if ( + self.DM or np.all(self.dmlist < (1 + self.tolerance_phid) * self.DMtarget) + ) and ( + same_mref or not self.wait_till_stable or percent_diff <= self.tolerance + ): + self.reg.reference_model_in_smooth = True + self.pgi_reg.reference_model_in_smooth = True + + if self._regmode == 2: + for i in range(self.nbr): + if self.Smooth[i]: + self.reg.objfcts[i].reference_model = mkvc( + self.pgi_reg.gmm.means_[self.membership] + ) + if self.verbose: + print( + "Add mref to Smoothness. Changes in mref happened in {} % of the cells".format( + percent_diff + ) + ) + + elif self._regmode == 1: + for i in range(self.nbr): + if self.Smooth[i, 2]: + idx = self.Smooth[i, :2] + self.reg.objfcts[idx[0]].objfcts[idx[1]].reference_model = mkvc( + self.pgi_reg.gmm.means_[self.membership] + ) + if self.verbose: + print( + "Add mref to Smoothness. Changes in mref happened in {} % of the cells".format( + percent_diff + ) + ) + + self.previous_membership = copy.deepcopy(self.membership) diff --git a/simpeg/directives/_regularization.py b/simpeg/directives/_regularization.py index fe4f44c9d3..c1f49e5ad8 100644 --- a/simpeg/directives/_regularization.py +++ b/simpeg/directives/_regularization.py @@ -6,7 +6,7 @@ from dataclasses import dataclass from ..maps import Projection -from .directives import InversionDirective, UpdatePreconditioner, BetaSchedule +from ._directives import InversionDirective, UpdatePreconditioner, BetaSchedule from ..regularization import ( Sparse, BaseSparse, diff --git a/simpeg/directives/_sim_directives.py b/simpeg/directives/_sim_directives.py new file mode 100644 index 0000000000..5c097ea913 --- /dev/null +++ b/simpeg/directives/_sim_directives.py @@ -0,0 +1,390 @@ +import numpy as np +from ..regularization import BaseSimilarityMeasure +from ..utils import eigenvalue_by_power_iteration +from ..optimization import IterationPrinters, StoppingCriteria +from ._directives import InversionDirective, SaveEveryIteration + + +############################################################################### +# # +# Directives of joint inversion # +# # +############################################################################### +class SimilarityMeasureInversionPrinters: + betas = { + "title": "betas", + "value": lambda M: ["{:.2e}".format(elem) for elem in M.parent.betas], + "width": 26, + "format": "%s", + } + lambd = { + "title": "lambda", + "value": lambda M: M.parent.lambd, + "width": 10, + "format": "%1.2e", + } + phi_d_list = { + "title": "phi_d", + "value": lambda M: ["{:.2e}".format(elem) for elem in M.parent.phi_d_list], + "width": 26, + "format": "%s", + } + phi_m_list = { + "title": "phi_m", + "value": lambda M: ["{:.2e}".format(elem) for elem in M.parent.phi_m_list], + "width": 26, + "format": "%s", + } + phi_sim = { + "title": "phi_sim", + "value": lambda M: M.parent.phi_sim, + "width": 10, + "format": "%1.2e", + } + iterationCG = { + "title": "iterCG", + "value": lambda M: M.cg_count, + "width": 10, + "format": "%3d", + } + + +class SimilarityMeasureInversionDirective(InversionDirective): + """ + Directive for two model similiraty measure joint inversions. Sets Printers and + StoppingCriteria. + + Notes + ----- + Methods assume we are working with two models, and a single similarity measure. + Also, the SimilarityMeasure objective function must be the last regularization. + """ + + printers = [ + IterationPrinters.iteration, + SimilarityMeasureInversionPrinters.betas, + SimilarityMeasureInversionPrinters.lambd, + IterationPrinters.f, + SimilarityMeasureInversionPrinters.phi_d_list, + SimilarityMeasureInversionPrinters.phi_m_list, + SimilarityMeasureInversionPrinters.phi_sim, + SimilarityMeasureInversionPrinters.iterationCG, + ] + + def initialize(self): + if not isinstance(self.reg.objfcts[-1], BaseSimilarityMeasure): + raise TypeError( + f"The last regularization function must be an instance of " + f"BaseSimilarityMeasure, got {type(self.reg.objfcts[-1])}." + ) + + # define relevant attributes + self.betas = self.reg.multipliers[:-1] + self.lambd = self.reg.multipliers[-1] + self.phi_d_list = [] + self.phi_m_list = [] + self.phi_sim = 0.0 + + # pass attributes to invProb + self.invProb.betas = self.betas + self.invProb.num_models = len(self.betas) + self.invProb.lambd = self.lambd + self.invProb.phi_d_list = self.phi_d_list + self.invProb.phi_m_list = self.phi_m_list + self.invProb.phi_sim = self.phi_sim + + self.opt.printers = self.printers + self.opt.stoppers = [StoppingCriteria.iteration] + + def validate(self, directiveList): + # check that this directive is first in the DirectiveList + dList = directiveList.dList + self_ind = dList.index(self) + if self_ind != 0: + raise IndexError( + "The CrossGradientInversionDirective must be first in directive list." + ) + return True + + def endIter(self): + # compute attribute values + phi_d = [] + for dmis in self.dmisfit.objfcts: + phi_d.append(dmis(self.opt.xc)) + + phi_m = [] + for reg in self.reg.objfcts: + phi_m.append(reg(self.opt.xc)) + + # pass attributes values to invProb + self.invProb.phi_d_list = phi_d + self.invProb.phi_m_list = phi_m[:-1] + self.invProb.phi_sim = phi_m[-1] + self.invProb.betas = self.reg.multipliers[:-1] + # Assume last reg.objfct is the coupling + self.invProb.lambd = self.reg.multipliers[-1] + + +class SimilarityMeasureSaveOutputEveryIteration(SaveEveryIteration): + """ + SaveOutputEveryIteration for Joint Inversions. + Saves information on the tradeoff parameters, data misfits, regularizations, + coupling term, number of CG iterations, and value of cost function. + """ + + header = None + save_txt = True + betas = None + phi_d = None + phi_m = None + phi_sim = None + phi = None + + def initialize(self): + if self.save_txt is True: + print( + "CrossGradientSaveOutputEveryIteration will save your inversion " + "progress as: '###-{0!s}.txt'".format(self.fileName) + ) + f = open(self.fileName + ".txt", "w") + self.header = " # betas lambda joint_phi_d joint_phi_m phi_sim iterCG phi \n" + f.write(self.header) + f.close() + + # Create a list of each + self.betas = [] + self.lambd = [] + self.phi_d = [] + self.phi_m = [] + self.phi = [] + self.phi_sim = [] + + def endIter(self): + self.betas.append(["{:.2e}".format(elem) for elem in self.invProb.betas]) + self.phi_d.append(["{:.3e}".format(elem) for elem in self.invProb.phi_d_list]) + self.phi_m.append(["{:.3e}".format(elem) for elem in self.invProb.phi_m_list]) + self.lambd.append("{:.2e}".format(self.invProb.lambd)) + self.phi_sim.append(self.invProb.phi_sim) + self.phi.append(self.opt.f) + + if self.save_txt: + f = open(self.fileName + ".txt", "a") + i = self.opt.iter + f.write( + " {0:2d} {1} {2} {3} {4} {5:1.4e} {6:d} {7:1.4e}\n".format( + i, + self.betas[i - 1], + self.lambd[i - 1], + self.phi_d[i - 1], + self.phi_m[i - 1], + self.phi_sim[i - 1], + self.opt.cg_count, + self.phi[i - 1], + ) + ) + f.close() + + def load_results(self): + results = np.loadtxt(self.fileName + str(".txt"), comments="#") + self.betas = results[:, 1] + self.lambd = results[:, 2] + self.phi_d = results[:, 3] + self.phi_m = results[:, 4] + self.phi_sim = results[:, 5] + self.f = results[:, 7] + + +class PairedBetaEstimate_ByEig(InversionDirective): + """ + Estimate the trade-off parameter, beta, between pairs of data misfit(s) and the + regularization(s) as a multiple of the ratio between the highest eigenvalue of the + data misfit term and the highest eigenvalue of the regularization. + The highest eigenvalues are estimated through power iterations and Rayleigh + quotient. + + Notes + ----- + This class assumes the order of the data misfits for each model parameter match + the order for the respective regularizations, i.e. + + >>> data_misfits = [phi_d_m1, phi_d_m2, phi_d_m3] + >>> regs = [phi_m_m1, phi_m_m2, phi_m_m3] + + In which case it will estimate regularization parameters for each respective pair. + """ + + beta0_ratio = 1.0 #: the estimated ratio is multiplied by this to obtain beta + n_pw_iter = 4 #: number of power iterations for estimation. + seed = None #: Random seed for the directive + + def initialize(self): + r""" + The initial beta is calculated by comparing the estimated + eigenvalues of :math:`J^T J` and :math:`W^T W`. + To estimate the eigenvector of **A**, we will use one iteration + of the *Power Method*: + + .. math:: + + \mathbf{x_1 = A x_0} + + Given this (very course) approximation of the eigenvector, we can + use the *Rayleigh quotient* to approximate the largest eigenvalue. + + .. math:: + + \lambda_0 = \frac{\mathbf{x^\top A x}}{\mathbf{x^\top x}} + + We will approximate the largest eigenvalue for both JtJ and WtW, + and use some ratio of the quotient to estimate beta0. + + .. math:: + + \beta_0 = \gamma \frac{\mathbf{x^\top J^\top J x}}{\mathbf{x^\top W^\top W x}} + + :rtype: float + :return: beta0 + """ + rng = np.random.default_rng(seed=self.seed) + + if self.verbose: + print("Calculating the beta0 parameter.") + + m = self.invProb.model + dmis_eigenvalues = [] + reg_eigenvalues = [] + dmis_objs = self.dmisfit.objfcts + reg_objs = [ + obj + for obj in self.reg.objfcts + if not isinstance(obj, BaseSimilarityMeasure) + ] + if len(dmis_objs) != len(reg_objs): + raise ValueError( + f"There must be the same number of data misfit and regularizations." + f"Got {len(dmis_objs)} and {len(reg_objs)} respectively." + ) + for dmis, reg in zip(dmis_objs, reg_objs): + dmis_eigenvalues.append( + eigenvalue_by_power_iteration( + dmis, + m, + n_pw_iter=self.n_pw_iter, + random_seed=rng, + ) + ) + + reg_eigenvalues.append( + eigenvalue_by_power_iteration( + reg, + m, + n_pw_iter=self.n_pw_iter, + random_seed=rng, + ) + ) + + self.ratios = np.array(dmis_eigenvalues) / np.array(reg_eigenvalues) + self.invProb.betas = self.beta0_ratio * self.ratios + self.reg.multipliers[:-1] = self.invProb.betas + + +class PairedBetaSchedule(InversionDirective): + """ + Directive for beta cooling schedule to determine the tradeoff + parameters when using paired data misfits and regularizations for a joint inversion. + """ + + chifact_target = 1.0 + beta_tol = 1e-1 + update_beta = True + cooling_rate = 1 + cooling_factor = 2 + dmis_met = False + + @property + def target(self): + if getattr(self, "_target", None) is None: + nD = np.array([survey.nD for survey in self.survey]) + + self._target = nD * self.chifact_target + + return self._target + + @target.setter + def target(self, val): + self._target = val + + def initialize(self): + self.dmis_met = np.zeros_like(self.invProb.betas, dtype=bool) + + def endIter(self): + # Check if target misfit has been reached, if so, set dmis_met to True + for i, phi_d in enumerate(self.invProb.phi_d_list): + self.dmis_met[i] = phi_d < self.target[i] + + # check separately if misfits are within the tolerance, + # otherwise, scale beta individually + for i, phi_d in enumerate(self.invProb.phi_d_list): + if self.opt.iter > 0 and self.opt.iter % self.cooling_rate == 0: + target = self.target[i] + ratio = phi_d / target + if self.update_beta and ratio <= (1.0 + self.beta_tol): + if ratio <= 1: + ratio = np.maximum(0.75, ratio) + else: + ratio = np.minimum(1.5, ratio) + + self.invProb.betas[i] /= ratio + elif ratio > 1.0: + self.invProb.betas[i] /= self.cooling_factor + + self.reg.multipliers[:-1] = self.invProb.betas + + +class MovingAndMultiTargetStopping(InversionDirective): + r""" + Directive for setting stopping criteria for a joint inversion. + Ensures both that all target misfits are met and there is a small change in the + model. Computes the percentage change of the current model from the previous model. + + ..math:: + \frac {\| \mathbf{m_i} - \mathbf{m_{i-1}} \|} {\| \mathbf{m_{i-1}} \|} + """ + + tol = 1e-5 + beta_tol = 1e-1 + chifact_target = 1.0 + + @property + def target(self): + if getattr(self, "_target", None) is None: + nD = [] + for survey in self.survey: + nD += [survey.nD] + nD = np.array(nD) + + self._target = nD * self.chifact_target + + return self._target + + @target.setter + def target(self, val): + self._target = val + + def endIter(self): + for phi_d, target in zip(self.invProb.phi_d_list, self.target): + if np.abs(1.0 - phi_d / target) >= self.beta_tol: + return + if ( + np.linalg.norm(self.opt.xc - self.opt.x_last) + / np.linalg.norm(self.opt.x_last) + > self.tol + ): + return + + print( + "stopping criteria met: ", + np.linalg.norm(self.opt.xc - self.opt.x_last) + / np.linalg.norm(self.opt.x_last), + ) + self.opt.stopNextIteration = True diff --git a/simpeg/directives/directives.py b/simpeg/directives/directives.py index ee4a4c2d4d..c40e514918 100644 --- a/simpeg/directives/directives.py +++ b/simpeg/directives/directives.py @@ -1,2970 +1,18 @@ -from typing import TYPE_CHECKING -import numpy as np -import matplotlib.pyplot as plt -import warnings -import os -import scipy.sparse as sp -from ..typing import RandomSeed -from ..data_misfit import BaseDataMisfit -from ..objective_function import BaseObjectiveFunction, ComboObjectiveFunction -from ..maps import IdentityMap, Wires -from ..regularization import ( - WeightedLeastSquares, - BaseRegularization, - BaseSparse, - Smallness, - Sparse, - SparseSmallness, - PGIsmallness, - SmoothnessFirstOrder, - SparseSmoothness, - BaseSimilarityMeasure, -) -from ..utils import ( - mkvc, - set_kwargs, - sdiag, - estimate_diagonal, - spherical2cartesian, - cartesian2spherical, - Zero, - eigenvalue_by_power_iteration, - validate_string, -) -from ..utils.code_utils import ( - deprecate_class, - deprecate_property, - validate_type, - validate_integer, - validate_float, - validate_ndarray_with_shape, -) - -if TYPE_CHECKING: - from ..simulation import BaseSimulation - from ..survey import BaseSurvey - - -class InversionDirective: - """Base inversion directive class. - - SimPEG directives initialize and update parameters used by the inversion algorithm; - e.g. setting the initial beta or updating the regularization. ``InversionDirective`` - is a parent class responsible for connecting directives to the data misfit, regularization - and optimization defining the inverse problem. - - Parameters - ---------- - inversion : simpeg.inversion.BaseInversion, None - An SimPEG inversion object; i.e. an instance of :class:`simpeg.inversion.BaseInversion`. - dmisfit : simpeg.data_misfit.BaseDataMisfit, None - A data data misfit; i.e. an instance of :class:`simpeg.data_misfit.BaseDataMisfit`. - reg : simpeg.regularization.BaseRegularization, None - The regularization, or model objective function; i.e. an instance of :class:`simpeg.regularization.BaseRegularization`. - verbose : bool - Whether or not to print debugging information. - """ - - _REGISTRY = {} - - _regPair = [WeightedLeastSquares, BaseRegularization, ComboObjectiveFunction] - _dmisfitPair = [BaseDataMisfit, ComboObjectiveFunction] - - def __init__(self, inversion=None, dmisfit=None, reg=None, verbose=False, **kwargs): - # Raise error on deprecated arguments - if (key := "debug") in kwargs.keys(): - raise TypeError(f"'{key}' property has been removed. Please use 'verbose'.") - self.inversion = inversion - self.dmisfit = dmisfit - self.reg = reg - self.verbose = verbose - set_kwargs(self, **kwargs) - - @property - def verbose(self): - """Whether or not to print debugging information. - - Returns - ------- - bool - """ - return self._verbose - - @verbose.setter - def verbose(self, value): - self._verbose = validate_type("verbose", value, bool) - - debug = deprecate_property( - verbose, "debug", "verbose", removal_version="0.19.0", error=True - ) - - @property - def inversion(self): - """Inversion object associated with the directive. - - Returns - ------- - simpeg.inversion.BaseInversion - The inversion associated with the directive. - """ - if not hasattr(self, "_inversion"): - return None - return self._inversion - - @inversion.setter - def inversion(self, i): - if getattr(self, "_inversion", None) is not None: - warnings.warn( - "InversionDirective {0!s} has switched to a new inversion.".format( - self.__class__.__name__ - ), - stacklevel=2, - ) - self._inversion = i - - @property - def invProb(self): - """Inverse problem associated with the directive. - - Returns - ------- - simpeg.inverse_problem.BaseInvProblem - The inverse problem associated with the directive. - """ - return self.inversion.invProb - - @property - def opt(self): - """Optimization algorithm associated with the directive. - - Returns - ------- - simpeg.optimization.Minimize - Optimization algorithm associated with the directive. - """ - return self.invProb.opt - - @property - def reg(self) -> BaseObjectiveFunction: - """Regularization associated with the directive. - - Returns - ------- - simpeg.regularization.BaseRegularization - The regularization associated with the directive. - """ - if getattr(self, "_reg", None) is None: - self.reg = self.invProb.reg # go through the setter - return self._reg - - @reg.setter - def reg(self, value): - if value is not None: - assert any( - [isinstance(value, regtype) for regtype in self._regPair] - ), "Regularization must be in {}, not {}".format(self._regPair, type(value)) - - if isinstance(value, WeightedLeastSquares): - value = 1 * value # turn it into a combo objective function - self._reg = value - - @property - def dmisfit(self) -> BaseObjectiveFunction: - """Data misfit associated with the directive. - - Returns - ------- - simpeg.data_misfit.BaseDataMisfit - The data misfit associated with the directive. - """ - if getattr(self, "_dmisfit", None) is None: - self.dmisfit = self.invProb.dmisfit # go through the setter - return self._dmisfit - - @dmisfit.setter - def dmisfit(self, value): - if value is not None: - assert any( - [isinstance(value, dmisfittype) for dmisfittype in self._dmisfitPair] - ), "Misfit must be in {}, not {}".format(self._dmisfitPair, type(value)) - - if not isinstance(value, ComboObjectiveFunction): - value = 1 * value # turn it into a combo objective function - self._dmisfit = value - - @property - def survey(self) -> list["BaseSurvey"]: - """Return survey for all data misfits - - Assuming that ``dmisfit`` is always a ``ComboObjectiveFunction``, - return a list containing the survey for each data misfit; i.e. - [survey1, survey2, ...] - - Returns - ------- - list of simpeg.survey.Survey - Survey for all data misfits. - """ - return [objfcts.simulation.survey for objfcts in self.dmisfit.objfcts] - - @property - def simulation(self) -> list["BaseSimulation"]: - """Return simulation for all data misfits. - - Assuming that ``dmisfit`` is always a ``ComboObjectiveFunction``, - return a list containing the simulation for each data misfit; i.e. - [sim1, sim2, ...]. - - Returns - ------- - list of simpeg.simulation.BaseSimulation - Simulation for all data misfits. - """ - return [objfcts.simulation for objfcts in self.dmisfit.objfcts] - - def initialize(self): - """Initialize inversion parameter(s) according to directive.""" - pass - - def endIter(self): - """Update inversion parameter(s) according to directive at end of iteration.""" - pass - - def finish(self): - """Update inversion parameter(s) according to directive at end of inversion.""" - pass - - def validate(self, directiveList=None): - """Validate directive. - - The `validate` method returns ``True`` if the directive and its location within - the directives list does not encounter conflicts. Otherwise, an appropriate error - message is returned describing the conflict. - - Parameters - ---------- - directive_list : simpeg.directives.DirectiveList - List of directives used in the inversion. - - Returns - ------- - bool - Returns ``True`` if validated, otherwise an approriate error is returned. - """ - return True - - -class DirectiveList(object): - """Directives list - - SimPEG directives initialize and update parameters used by the inversion algorithm; - e.g. setting the initial beta or updating the regularization. ``DirectiveList`` stores - the set of directives used in the inversion algorithm. - - Parameters - ---------- - directives : list of simpeg.directives.InversionDirective - List of directives. - inversion : simpeg.inversion.BaseInversion - The inversion associated with the directives list. - debug : bool - Whether or not to print debugging information. - - """ - - def __init__(self, *directives, inversion=None, debug=False, **kwargs): - super().__init__(**kwargs) - self.dList = [] - for d in directives: - assert isinstance( - d, InversionDirective - ), "All directives must be InversionDirectives not {}".format(type(d)) - self.dList.append(d) - self.inversion = inversion - self.verbose = debug - - @property - def debug(self): - """Whether or not to print debugging information - - Returns - ------- - bool - """ - return getattr(self, "_debug", False) - - @debug.setter - def debug(self, value): - for d in self.dList: - d.debug = value - self._debug = value - - @property - def inversion(self): - """Inversion object associated with the directives list. - - Returns - ------- - simpeg.inversion.BaseInversion - The inversion associated with the directives list. - """ - return getattr(self, "_inversion", None) - - @inversion.setter - def inversion(self, i): - if self.inversion is i: - return - if getattr(self, "_inversion", None) is not None: - warnings.warn( - "{0!s} has switched to a new inversion.".format( - self.__class__.__name__ - ), - stacklevel=2, - ) - for d in self.dList: - d.inversion = i - self._inversion = i - - def call(self, ruleType): - if self.dList is None: - if self.verbose: - print("DirectiveList is None, no directives to call!") - return - - directives = ["initialize", "endIter", "finish"] - assert ruleType in directives, 'Directive type must be in ["{0!s}"]'.format( - '", "'.join(directives) - ) - for r in self.dList: - getattr(r, ruleType)() - - def validate(self): - [directive.validate(self) for directive in self.dList] - return True - - -class BaseBetaEstimator(InversionDirective): - """Base class for estimating initial trade-off parameter (beta). - - This class has properties and methods inherited by directive classes which estimate - the initial trade-off parameter (beta). This class is not used directly to create - directives for the inversion. - - Parameters - ---------- - beta0_ratio : float - Desired ratio between data misfit and model objective function at initial beta iteration. - random_seed : None or :class:`~simpeg.typing.RandomSeed`, optional - Random seed used for random sampling. It can either be an int, - a predefined Numpy random number generator, or any valid input to - ``numpy.random.default_rng``. - seed : None or :class:`~simpeg.typing.RandomSeed`, optional - - .. deprecated:: 0.23.0 - - Argument ``seed`` is deprecated in favor of ``random_seed`` and will - be removed in SimPEG v0.24.0. - - """ - - def __init__( - self, - beta0_ratio=1.0, - random_seed: RandomSeed | None = None, - seed: RandomSeed | None = None, - **kwargs, - ): - super().__init__(**kwargs) - self.beta0_ratio = beta0_ratio - - # Deprecate seed argument - if seed is not None: - if random_seed is not None: - raise TypeError( - "Cannot pass both 'random_seed' and 'seed'." - "'seed' has been deprecated and will be removed in " - " SimPEG v0.24.0, please use 'random_seed' instead.", - ) - warnings.warn( - "'seed' has been deprecated and will be removed in " - " SimPEG v0.24.0, please use 'random_seed' instead.", - FutureWarning, - stacklevel=2, - ) - random_seed = seed - self.random_seed = random_seed - - @property - def beta0_ratio(self): - """The estimated ratio is multiplied by this to obtain beta. - - Returns - ------- - float - """ - return self._beta0_ratio - - @beta0_ratio.setter - def beta0_ratio(self, value): - self._beta0_ratio = validate_float( - "beta0_ratio", value, min_val=0.0, inclusive_min=False - ) - - @property - def random_seed(self): - """Random seed to initialize with. - - Returns - ------- - int, numpy.random.Generator or None - """ - return self._random_seed - - @random_seed.setter - def random_seed(self, value): - try: - np.random.default_rng(value) - except TypeError as err: - msg = ( - "Unable to initialize the random number generator with " - f"a {type(value).__name__}" - ) - raise TypeError(msg) from err - self._random_seed = value - - def validate(self, directive_list): - ind = [isinstance(d, BaseBetaEstimator) for d in directive_list.dList] - assert np.sum(ind) == 1, ( - "Multiple directives for computing initial beta detected in directives list. " - "Only one directive can be used to set the initial beta." - ) - - return True - - seed = deprecate_property( - random_seed, - "seed", - "random_seed", - removal_version="0.24.0", - future_warn=True, - error=False, - ) - - -class BetaEstimateMaxDerivative(BaseBetaEstimator): - r"""Estimate initial trade-off parameter (beta) using largest derivatives. - - The initial trade-off parameter (beta) is estimated by scaling the ratio - between the largest derivatives in the gradient of the data misfit and - model objective function. The estimated trade-off parameter is used to - update the **beta** property in the associated :class:`simpeg.inverse_problem.BaseInvProblem` - object prior to running the inversion. A separate directive is used for updating the - trade-off parameter at successive beta iterations; see :class:`BetaSchedule`. - - Parameters - ---------- - beta0_ratio: float - Desired ratio between data misfit and model objective function at initial beta iteration. - random_seed : None or :class:`~simpeg.typing.RandomSeed`, optional - Random seed used for random sampling. It can either be an int, - a predefined Numpy random number generator, or any valid input to - ``numpy.random.default_rng``. - seed : None or :class:`~simpeg.typing.RandomSeed`, optional - - .. deprecated:: 0.23.0 - - Argument ``seed`` is deprecated in favor of ``random_seed`` and will - be removed in SimPEG v0.24.0. - - Notes - ----- - Let :math:`\phi_d` represent the data misfit, :math:`\phi_m` represent the model - objective function and :math:`\mathbf{m_0}` represent the starting model. The first - model update is obtained by minimizing the a global objective function of the form: - - .. math:: - \phi (\mathbf{m_0}) = \phi_d (\mathbf{m_0}) + \beta_0 \phi_m (\mathbf{m_0}) - - where :math:`\beta_0` represents the initial trade-off parameter (beta). - - We define :math:`\gamma` as the desired ratio between the data misfit and model objective - functions at the initial beta iteration (defined by the 'beta0_ratio' input argument). - Here, the initial trade-off parameter is computed according to: - - .. math:: - \beta_0 = \gamma \frac{| \nabla_m \phi_d (\mathbf{m_0}) |_{max}}{| \nabla_m \phi_m (\mathbf{m_0 + \delta m}) |_{max}} - - where - - .. math:: - \delta \mathbf{m} = \frac{m_{max}}{\mu_{max}} \boldsymbol{\mu} - - and :math:`\boldsymbol{\mu}` is a set of independent samples from the - continuous uniform distribution between 0 and 1. - - """ - - def __init__( - self, beta0_ratio=1.0, random_seed: RandomSeed | None = None, **kwargs - ): - super().__init__(beta0_ratio=beta0_ratio, random_seed=random_seed, **kwargs) - - def initialize(self): - rng = np.random.default_rng(seed=self.random_seed) - - if self.verbose: - print("Calculating the beta0 parameter.") - - m = self.invProb.model - - x0 = rng.random(size=m.shape) - phi_d_deriv = np.abs(self.dmisfit.deriv(m)).max() - dm = x0 / x0.max() * m.max() - phi_m_deriv = np.abs(self.reg.deriv(m + dm)).max() - - self.ratio = np.asarray(phi_d_deriv / phi_m_deriv) - self.beta0 = self.beta0_ratio * self.ratio - self.invProb.beta = self.beta0 - - -class BetaEstimate_ByEig(BaseBetaEstimator): - r"""Estimate initial trade-off parameter (beta) by power iteration. - - The initial trade-off parameter (beta) is estimated by scaling the ratio - between the largest eigenvalue in the second derivative of the data - misfit and the model objective function. The largest eigenvalues are estimated - using the power iteration method; see :func:`simpeg.utils.eigenvalue_by_power_iteration`. - The estimated trade-off parameter is used to update the **beta** property in the - associated :class:`simpeg.inverse_problem.BaseInvProblem` object prior to running the inversion. - Note that a separate directive is used for updating the trade-off parameter at successive - beta iterations; see :class:`BetaSchedule`. - - Parameters - ---------- - beta0_ratio: float - Desired ratio between data misfit and model objective function at initial beta iteration. - n_pw_iter : int - Number of power iterations used to estimate largest eigenvalues. - random_seed : None or :class:`~simpeg.typing.RandomSeed`, optional - Random seed used for random sampling. It can either be an int, - a predefined Numpy random number generator, or any valid input to - ``numpy.random.default_rng``. - seed : None or :class:`~simpeg.typing.RandomSeed`, optional - - .. deprecated:: 0.23.0 - - Argument ``seed`` is deprecated in favor of ``random_seed`` and will - be removed in SimPEG v0.24.0. - - Notes - ----- - Let :math:`\phi_d` represent the data misfit, :math:`\phi_m` represent the model - objective function and :math:`\mathbf{m_0}` represent the starting model. The first - model update is obtained by minimizing the a global objective function of the form: - - .. math:: - \phi (\mathbf{m_0}) = \phi_d (\mathbf{m_0}) + \beta_0 \phi_m (\mathbf{m_0}) - - where :math:`\beta_0` represents the initial trade-off parameter (beta). - Let :math:`\gamma` define the desired ratio between the data misfit and model - objective functions at the initial beta iteration (defined by the 'beta0_ratio' input argument). - Using the power iteration approach, our initial trade-off parameter is given by: - - .. math:: - \beta_0 = \gamma \frac{\lambda_d}{\lambda_m} - - where :math:`\lambda_d` as the largest eigenvalue of the Hessian of the data misfit, and - :math:`\lambda_m` as the largest eigenvalue of the Hessian of the model objective function. - For each Hessian, the largest eigenvalue is computed using power iteration. The input - parameter 'n_pw_iter' sets the number of power iterations used in the estimate. - - For a description of the power iteration approach for estimating the larges eigenvalue, - see :func:`simpeg.utils.eigenvalue_by_power_iteration`. - - """ - - def __init__( - self, - beta0_ratio=1.0, - n_pw_iter=4, - random_seed: RandomSeed | None = None, - seed: RandomSeed | None = None, - **kwargs, - ): - super().__init__( - beta0_ratio=beta0_ratio, random_seed=random_seed, seed=seed, **kwargs - ) - self.n_pw_iter = n_pw_iter - - @property - def n_pw_iter(self): - """Number of power iterations for estimating largest eigenvalues. - - Returns - ------- - int - Number of power iterations for estimating largest eigenvalues. - """ - return self._n_pw_iter - - @n_pw_iter.setter - def n_pw_iter(self, value): - self._n_pw_iter = validate_integer("n_pw_iter", value, min_val=1) - - def initialize(self): - rng = np.random.default_rng(seed=self.random_seed) - - if self.verbose: - print("Calculating the beta0 parameter.") - - m = self.invProb.model - - dm_eigenvalue = eigenvalue_by_power_iteration( - self.dmisfit, - m, - n_pw_iter=self.n_pw_iter, - random_seed=rng, - ) - reg_eigenvalue = eigenvalue_by_power_iteration( - self.reg, - m, - n_pw_iter=self.n_pw_iter, - random_seed=rng, - ) - - self.ratio = np.asarray(dm_eigenvalue / reg_eigenvalue) - self.beta0 = self.beta0_ratio * self.ratio - self.invProb.beta = self.beta0 - - -class BetaSchedule(InversionDirective): - """Reduce trade-off parameter (beta) at successive iterations using a cooling schedule. - - Updates the **beta** property in the associated :class:`simpeg.inverse_problem.BaseInvProblem` - while the inversion is running. - For linear least-squares problems, the optimization problem can be solved in a - single step and the cooling rate can be set to *1*. For non-linear optimization - problems, multiple steps are required obtain the minimizer for a fixed trade-off - parameter. In this case, the cooling rate should be larger than 1. - - Parameters - ---------- - coolingFactor : float - The factor by which the trade-off parameter is decreased when updated. - The preexisting value of the trade-off parameter is divided by the cooling factor. - coolingRate : int - Sets the number of successive iterations before the trade-off parameter is reduced. - Use *1* for linear least-squares optimization problems. Use *2* for weakly non-linear - optimization problems. Use *3* for general non-linear optimization problems. - - """ - - def __init__(self, coolingFactor=8.0, coolingRate=3, **kwargs): - super().__init__(**kwargs) - self.coolingFactor = coolingFactor - self.coolingRate = coolingRate - - @property - def coolingFactor(self): - """Beta is divided by this value every `coolingRate` iterations. - - Returns - ------- - float - """ - return self._coolingFactor - - @coolingFactor.setter - def coolingFactor(self, value): - self._coolingFactor = validate_float( - "coolingFactor", value, min_val=0.0, inclusive_min=False - ) - - @property - def coolingRate(self): - """Cool after this number of iterations. - - Returns - ------- - int - """ - return self._coolingRate - - @coolingRate.setter - def coolingRate(self, value): - self._coolingRate = validate_integer("coolingRate", value, min_val=1) - - def endIter(self): - if self.opt.iter > 0 and self.opt.iter % self.coolingRate == 0: - if self.verbose: - print( - "BetaSchedule is cooling Beta. Iteration: {0:d}".format( - self.opt.iter - ) - ) - self.invProb.beta /= self.coolingFactor - - -class AlphasSmoothEstimate_ByEig(InversionDirective): - """ - Estimate the alphas multipliers for the smoothness terms of the regularization - as a multiple of the ratio between the highest eigenvalue of the - smallness term and the highest eigenvalue of each smoothness term of the regularization. - The highest eigenvalue are estimated through power iterations and Rayleigh quotient. - """ - - def __init__( - self, - alpha0_ratio=1.0, - n_pw_iter=4, - random_seed: RandomSeed | None = None, - seed: RandomSeed | None = None, - **kwargs, - ): - super().__init__(**kwargs) - self.alpha0_ratio = alpha0_ratio - self.n_pw_iter = n_pw_iter - - # Deprecate seed argument - if seed is not None: - if random_seed is not None: - raise TypeError( - "Cannot pass both 'random_seed' and 'seed'." - "'seed' has been deprecated and will be removed in " - " SimPEG v0.24.0, please use 'random_seed' instead.", - ) - warnings.warn( - "'seed' has been deprecated and will be removed in " - " SimPEG v0.24.0, please use 'random_seed' instead.", - FutureWarning, - stacklevel=2, - ) - random_seed = seed - self.random_seed = random_seed - - @property - def alpha0_ratio(self): - """the estimated Alpha_smooth is multiplied by this ratio (int or array). - - Returns - ------- - numpy.ndarray - """ - return self._alpha0_ratio - - @alpha0_ratio.setter - def alpha0_ratio(self, value): - self._alpha0_ratio = validate_ndarray_with_shape( - "alpha0_ratio", value, shape=("*",) - ) - - @property - def n_pw_iter(self): - """Number of power iterations for estimation. - - Returns - ------- - int - """ - return self._n_pw_iter - - @n_pw_iter.setter - def n_pw_iter(self, value): - self._n_pw_iter = validate_integer("n_pw_iter", value, min_val=1) - - @property - def random_seed(self): - """Random seed to initialize with. - - Returns - ------- - int, numpy.random.Generator or None - """ - return self._random_seed - - @random_seed.setter - def random_seed(self, value): - try: - np.random.default_rng(value) - except TypeError as err: - msg = ( - "Unable to initialize the random number generator with " - f"a {type(value).__name__}" - ) - raise TypeError(msg) from err - self._random_seed = value - - seed = deprecate_property( - random_seed, - "seed", - "random_seed", - removal_version="0.24.0", - future_warn=True, - error=False, - ) - - def initialize(self): - """""" - rng = np.random.default_rng(seed=self.random_seed) - - smoothness = [] - smallness = [] - parents = {} - for regobjcts in self.reg.objfcts: - if isinstance(regobjcts, ComboObjectiveFunction): - objfcts = regobjcts.objfcts - else: - objfcts = [regobjcts] - - for obj in objfcts: - if isinstance( - obj, - ( - Smallness, - SparseSmallness, - PGIsmallness, - ), - ): - smallness += [obj] - - elif isinstance(obj, (SmoothnessFirstOrder, SparseSmoothness)): - parents[obj] = regobjcts - smoothness += [obj] - - if len(smallness) == 0: - raise UserWarning( - "Directive 'AlphasSmoothEstimate_ByEig' requires a regularization with at least one Small instance." - ) - - smallness_eigenvalue = eigenvalue_by_power_iteration( - smallness[0], - self.invProb.model, - n_pw_iter=self.n_pw_iter, - random_seed=rng, - ) - - self.alpha0_ratio = self.alpha0_ratio * np.ones(len(smoothness)) - - if len(self.alpha0_ratio) != len(smoothness): - raise ValueError( - f"Input values for 'alpha0_ratio' should be of len({len(smoothness)}). Provided {self.alpha0_ratio}" - ) - - alphas = [] - for user_alpha, obj in zip(self.alpha0_ratio, smoothness): - smooth_i_eigenvalue = eigenvalue_by_power_iteration( - obj, - self.invProb.model, - n_pw_iter=self.n_pw_iter, - random_seed=rng, - ) - ratio = smallness_eigenvalue / smooth_i_eigenvalue - - mtype = obj._multiplier_pair - - new_alpha = getattr(parents[obj], mtype) * user_alpha * ratio - setattr(parents[obj], mtype, new_alpha) - alphas += [new_alpha] - - if self.verbose: - print(f"Alpha scales: {alphas}") - - -class ScalingMultipleDataMisfits_ByEig(InversionDirective): - """ - For multiple data misfits only: multiply each data misfit term - by the inverse of its highest eigenvalue and then - normalize the sum of the data misfit multipliers to one. - The highest eigenvalue are estimated through power iterations and Rayleigh quotient. - """ - - def __init__( - self, - chi0_ratio=None, - n_pw_iter=4, - random_seed: RandomSeed | None = None, - seed: RandomSeed | None = None, - **kwargs, - ): - super().__init__(**kwargs) - self.chi0_ratio = chi0_ratio - self.n_pw_iter = n_pw_iter - - # Deprecate seed argument - if seed is not None: - if random_seed is not None: - raise TypeError( - "Cannot pass both 'random_seed' and 'seed'." - "'seed' has been deprecated and will be removed in " - " SimPEG v0.24.0, please use 'random_seed' instead.", - ) - warnings.warn( - "'seed' has been deprecated and will be removed in " - " SimPEG v0.24.0, please use 'random_seed' instead.", - FutureWarning, - stacklevel=2, - ) - random_seed = seed - self.random_seed = random_seed - - @property - def chi0_ratio(self): - """the estimated Alpha_smooth is multiplied by this ratio (int or array) - - Returns - ------- - numpy.ndarray - """ - return self._chi0_ratio - - @chi0_ratio.setter - def chi0_ratio(self, value): - if value is not None: - value = validate_ndarray_with_shape("chi0_ratio", value, shape=("*",)) - self._chi0_ratio = value - - @property - def n_pw_iter(self): - """Number of power iterations for estimation. - - Returns - ------- - int - """ - return self._n_pw_iter - - @n_pw_iter.setter - def n_pw_iter(self, value): - self._n_pw_iter = validate_integer("n_pw_iter", value, min_val=1) - - @property - def random_seed(self): - """Random seed to initialize with - - Returns - ------- - int, numpy.random.Generator or None - """ - return self._random_seed - - @random_seed.setter - def random_seed(self, value): - try: - np.random.default_rng(value) - except TypeError as err: - msg = ( - "Unable to initialize the random number generator with " - f"a {type(value).__name__}" - ) - raise TypeError(msg) from err - self._random_seed = value - - seed = deprecate_property( - random_seed, - "seed", - "random_seed", - removal_version="0.24.0", - future_warn=True, - error=False, - ) - - def initialize(self): - """""" - rng = np.random.default_rng(seed=self.random_seed) - - if self.verbose: - print("Calculating the scaling parameter.") - - if ( - getattr(self.dmisfit, "objfcts", None) is None - or len(self.dmisfit.objfcts) == 1 - ): - raise TypeError( - "ScalingMultipleDataMisfits_ByEig only applies to joint inversion" - ) - - ndm = len(self.dmisfit.objfcts) - if self.chi0_ratio is not None: - self.chi0_ratio = self.chi0_ratio * np.ones(ndm) - else: - self.chi0_ratio = self.dmisfit.multipliers - - m = self.invProb.model - - dm_eigenvalue_list = [] - for dm in self.dmisfit.objfcts: - dm_eigenvalue_list += [ - eigenvalue_by_power_iteration(dm, m, random_seed=rng) - ] - - self.chi0 = self.chi0_ratio / np.r_[dm_eigenvalue_list] - self.chi0 = self.chi0 / np.sum(self.chi0) - self.dmisfit.multipliers = self.chi0 - - if self.verbose: - print("Scale Multipliers: ", self.dmisfit.multipliers) - - -class JointScalingSchedule(InversionDirective): - """ - For multiple data misfits only: rebalance each data misfit term - during the inversion when some datasets are fit, and others not - using the ratios of current misfits and their respective target. - It implements the strategy described in https://doi.org/10.1093/gji/ggaa378. - """ - - def __init__( - self, warmingFactor=1.0, chimax=1e10, chimin=1e-10, update_rate=1, **kwargs - ): - super().__init__(**kwargs) - self.mode = 1 - self.warmingFactor = warmingFactor - self.chimax = chimax - self.chimin = chimin - self.update_rate = update_rate - - @property - def mode(self): - """The type of update to perform. - - Returns - ------- - {1, 2} - """ - return self._mode - - @mode.setter - def mode(self, value): - self._mode = validate_integer("mode", value, min_val=1, max_val=2) - - @property - def warmingFactor(self): - """Factor to adjust scaling of the data misfits by. - - Returns - ------- - float - """ - return self._warmingFactor - - @warmingFactor.setter - def warmingFactor(self, value): - self._warmingFactor = validate_float( - "warmingFactor", value, min_val=0.0, inclusive_min=False - ) - - @property - def chimax(self): - """Maximum chi factor. - - Returns - ------- - float - """ - return self._chimax - - @chimax.setter - def chimax(self, value): - self._chimax = validate_float("chimax", value, min_val=0.0, inclusive_min=False) - - @property - def chimin(self): - """Minimum chi factor. - - Returns - ------- - float - """ - return self._chimin - - @chimin.setter - def chimin(self, value): - self._chimin = validate_float("chimin", value, min_val=0.0, inclusive_min=False) - - @property - def update_rate(self): - """Will update the data misfit scalings after this many iterations. - - Returns - ------- - int - """ - return self._update_rate - - @update_rate.setter - def update_rate(self, value): - self._update_rate = validate_integer("update_rate", value, min_val=1) - - def initialize(self): - if ( - getattr(self.dmisfit, "objfcts", None) is None - or len(self.dmisfit.objfcts) == 1 - ): - raise TypeError("JointScalingSchedule only applies to joint inversion") - - targetclass = np.r_[ - [ - isinstance(dirpart, MultiTargetMisfits) - for dirpart in self.inversion.directiveList.dList - ] - ] - if ~np.any(targetclass): - self.DMtarget = None - else: - self.targetclass = np.where(targetclass)[0][-1] - self.DMtarget = self.inversion.directiveList.dList[ - self.targetclass - ].DMtarget - - if self.verbose: - print("Initial data misfit scales: ", self.dmisfit.multipliers) - - def endIter(self): - self.dmlist = self.inversion.directiveList.dList[self.targetclass].dmlist - - if np.any(self.dmlist < self.DMtarget): - self.mode = 2 - else: - self.mode = 1 - - if self.opt.iter > 0 and self.opt.iter % self.update_rate == 0: - if self.mode == 2: - if np.all(np.r_[self.dmisfit.multipliers] > self.chimin) and np.all( - np.r_[self.dmisfit.multipliers] < self.chimax - ): - indx = self.dmlist > self.DMtarget - if np.any(indx): - multipliers = self.warmingFactor * np.median( - self.DMtarget[~indx] / self.dmlist[~indx] - ) - if np.sum(indx) == 1: - indx = np.where(indx)[0][0] - self.dmisfit.multipliers[indx] *= multipliers - self.dmisfit.multipliers /= np.sum(self.dmisfit.multipliers) - - if self.verbose: - print("Updating scaling for data misfits by ", multipliers) - print("New scales:", self.dmisfit.multipliers) - - -class TargetMisfit(InversionDirective): - """ - ... note:: Currently this target misfit is not set up for joint inversion. - Check out MultiTargetMisfits - """ - - def __init__(self, target=None, phi_d_star=None, chifact=1.0, **kwargs): - super().__init__(**kwargs) - self.chifact = chifact - self.phi_d_star = phi_d_star - if phi_d_star is not None and target is not None: - raise AttributeError("Attempted to set both target and phi_d_star.") - if target is not None: - self.target = target - - @property - def target(self): - """The target value for the data misfit - - Returns - ------- - float - """ - if getattr(self, "_target", None) is None: - self._target = self.chifact * self.phi_d_star - return self._target - - @target.setter - def target(self, val): - self._target = validate_float("target", val, min_val=0.0, inclusive_min=False) - - @property - def chifact(self): - """The a multiplier for the target data misfit value. - - The target value is `chifact` times `phi_d_star` - - Returns - ------- - float - """ - return self._chifact - - @chifact.setter - def chifact(self, value): - self._chifact = validate_float( - "chifact", value, min_val=0.0, inclusive_min=False - ) - self._target = None - - @property - def phi_d_star(self): - """The target phi_d value for the data misfit. - - The target value is `chifact` times `phi_d_star` - - Returns - ------- - float - """ - # phid = ||dpred - dobs||^2 - if self._phi_d_star is None: - nD = 0 - for survey in self.survey: - nD += survey.nD - self._phi_d_star = nD - return self._phi_d_star - - @phi_d_star.setter - def phi_d_star(self, value): - # phid = ||dpred - dobs||^2 - if value is not None: - value = validate_float( - "phi_d_star", value, min_val=0.0, inclusive_min=False - ) - self._phi_d_star = value - self._target = None - - def endIter(self): - if self.invProb.phi_d < self.target: - self.opt.stopNextIteration = True - self.print_final_misfit() - - def print_final_misfit(self): - if self.opt.print_type == "ubc": - self.opt.print_target = ( - ">> Target misfit: %.1f (# of data) is achieved" - ) % (self.target) - - -class MultiTargetMisfits(InversionDirective): - def __init__( - self, - WeightsInTarget=False, - chifact=1.0, - phi_d_star=None, - TriggerSmall=True, - chiSmall=1.0, - phi_ms_star=None, - TriggerTheta=False, - ToleranceTheta=1.0, - distance_norm=np.inf, - **kwargs, - ): - super().__init__(**kwargs) - - self.WeightsInTarget = WeightsInTarget - # Chi factor for Geophsyical Data Misfit - self.chifact = chifact - self.phi_d_star = phi_d_star - - # Chifact for Clustering/Smallness - self.TriggerSmall = TriggerSmall - self.chiSmall = chiSmall - self.phi_ms_star = phi_ms_star - - # Tolerance for parameters difference with their priors - self.TriggerTheta = TriggerTheta # deactivated by default - self.ToleranceTheta = ToleranceTheta - self.distance_norm = distance_norm - - self._DM = False - self._CL = False - self._DP = False - - @property - def WeightsInTarget(self): - """Whether to account for weights in the petrophysical misfit. - - Returns - ------- - bool - """ - return self._WeightsInTarget - - @WeightsInTarget.setter - def WeightsInTarget(self, value): - self._WeightsInTarget = validate_type("WeightsInTarget", value, bool) - - @property - def chifact(self): - """The a multiplier for the target Geophysical data misfit value. - - The target value is `chifact` times `phi_d_star` - - Returns - ------- - numpy.ndarray - """ - return self._chifact - - @chifact.setter - def chifact(self, value): - self._chifact = validate_ndarray_with_shape("chifact", value, shape=("*",)) - self._DMtarget = None - - @property - def phi_d_star(self): - """The target phi_d value for the Geophysical data misfit. - - The target value is `chifact` times `phi_d_star` - - Returns - ------- - float - """ - # phid = || dpred - dobs||^2 - if getattr(self, "_phi_d_star", None) is None: - # Check if it is a ComboObjective - if isinstance(self.dmisfit, ComboObjectiveFunction): - value = np.r_[[survey.nD for survey in self.survey]] - else: - value = np.r_[[self.survey.nD]] - self._phi_d_star = value - self._DMtarget = None - - return self._phi_d_star - - @phi_d_star.setter - def phi_d_star(self, value): - # phid =|| dpred - dobs||^2 - if value is not None: - value = validate_ndarray_with_shape("phi_d_star", value, shape=("*",)) - self._phi_d_star = value - self._DMtarget = None - - @property - def chiSmall(self): - """The a multiplier for the target petrophysical misfit value. - - The target value is `chiSmall` times `phi_ms_star` - - Returns - ------- - float - """ - return self._chiSmall - - @chiSmall.setter - def chiSmall(self, value): - self._chiSmall = validate_float("chiSmall", value) - self._CLtarget = None - - @property - def phi_ms_star(self): - """The target value for the petrophysical data misfit. - - The target value is `chiSmall` times `phi_ms_star` - - Returns - ------- - float - """ - return self._phi_ms_star - - @phi_ms_star.setter - def phi_ms_star(self, value): - if value is not None: - value = validate_float("phi_ms_star", value) - self._phi_ms_star = value - self._CLtarget = None - - @property - def TriggerSmall(self): - """Whether to trigger the smallness misfit test. - - Returns - ------- - bool - """ - return self._TriggerSmall - - @TriggerSmall.setter - def TriggerSmall(self, value): - self._TriggerSmall = validate_type("TriggerSmall", value, bool) - - @property - def TriggerTheta(self): - """Whether to trigger the GMM misfit test. - - Returns - ------- - bool - """ - return self._TriggerTheta - - @TriggerTheta.setter - def TriggerTheta(self, value): - self._TriggerTheta = validate_type("TriggerTheta", value, bool) - - @property - def ToleranceTheta(self): - """Target value for the GMM misfit. - - Returns - ------- - float - """ - return self._ToleranceTheta - - @ToleranceTheta.setter - def ToleranceTheta(self, value): - self._ToleranceTheta = validate_float("ToleranceTheta", value, min_val=0.0) - - @property - def distance_norm(self): - """Distance norm to use for GMM misfit measure. - - Returns - ------- - float - """ - return self._distance_norm - - @distance_norm.setter - def distance_norm(self, value): - self._distance_norm = validate_float("distance_norm", value, min_val=0.0) - - def initialize(self): - self.dmlist = np.r_[[dmis(self.invProb.model) for dmis in self.dmisfit.objfcts]] - - if getattr(self.invProb.reg.objfcts[0], "objfcts", None) is not None: - smallness = np.r_[ - [ - ( - np.r_[ - i, - j, - isinstance(regpart, PGIsmallness), - ] - ) - for i, regobjcts in enumerate(self.invProb.reg.objfcts) - for j, regpart in enumerate(regobjcts.objfcts) - ] - ] - if smallness[smallness[:, 2] == 1][:, :2].size == 0: - warnings.warn( - "There is no PGI regularization. Smallness target is turned off (TriggerSmall flag)", - stacklevel=2, - ) - self.smallness = -1 - self.pgi_smallness = None - - else: - self.smallness = smallness[smallness[:, 2] == 1][:, :2][0] - self.pgi_smallness = self.invProb.reg.objfcts[ - self.smallness[0] - ].objfcts[self.smallness[1]] - - if self.verbose: - print( - type( - self.invProb.reg.objfcts[self.smallness[0]].objfcts[ - self.smallness[1] - ] - ) - ) - - self._regmode = 1 - - else: - smallness = np.r_[ - [ - ( - np.r_[ - j, - isinstance(regpart, PGIsmallness), - ] - ) - for j, regpart in enumerate(self.invProb.reg.objfcts) - ] - ] - if smallness[smallness[:, 1] == 1][:, :1].size == 0: - if self.TriggerSmall: - warnings.warn( - "There is no PGI regularization. Smallness target is turned off (TriggerSmall flag).", - stacklevel=2, - ) - self.TriggerSmall = False - self.smallness = -1 - else: - self.smallness = smallness[smallness[:, 1] == 1][:, :1][0] - self.pgi_smallness = self.invProb.reg.objfcts[self.smallness[0]] - - if self.verbose: - print(type(self.invProb.reg.objfcts[self.smallness[0]])) - - self._regmode = 2 - - @property - def DM(self): - """Whether the geophysical data misfit target was satisfied. - - Returns - ------- - bool - """ - return self._DM - - @property - def CL(self): - """Whether the petrophysical misfit target was satisified. - - Returns - ------- - bool - """ - return self._CL - - @property - def DP(self): - """Whether the GMM misfit was below the threshold. - - Returns - ------- - bool - """ - return self._DP - - @property - def AllStop(self): - """Whether all target misfit values have been met. - - Returns - ------- - bool - """ - - return self.DM and self.CL and self.DP - - @property - def DMtarget(self): - if getattr(self, "_DMtarget", None) is None: - self._DMtarget = self.chifact * self.phi_d_star - return self._DMtarget - - @DMtarget.setter - def DMtarget(self, val): - self._DMtarget = val - - @property - def CLtarget(self): - if not getattr(self.pgi_smallness, "approx_eval", True): - # if nonlinear prior, compute targer numerically at each GMM update - samples, _ = self.pgi_smallness.gmm.sample( - len(self.pgi_smallness.gmm.cell_volumes) - ) - self.phi_ms_star = self.pgi_smallness( - mkvc(samples), externalW=self.WeightsInTarget - ) - - self._CLtarget = self.chiSmall * self.phi_ms_star - - elif getattr(self, "_CLtarget", None) is None: - # phid = ||dpred - dobs||^2 - if self.phi_ms_star is None: - # Expected value is number of active cells * number of physical - # properties - self.phi_ms_star = len(self.invProb.model) - - self._CLtarget = self.chiSmall * self.phi_ms_star - - return self._CLtarget - - @property - def CLnormalizedConstant(self): - if ~self.WeightsInTarget: - return 1.0 - elif np.any(self.smallness == -1): - return np.sum( - sp.csr_matrix.diagonal(self.invProb.reg.objfcts[0].W) ** 2.0 - ) / len(self.invProb.model) - else: - return np.sum(sp.csr_matrix.diagonal(self.pgi_smallness.W) ** 2.0) / len( - self.invProb.model - ) - - @CLtarget.setter - def CLtarget(self, val): - self._CLtarget = val - - def phims(self): - if np.any(self.smallness == -1): - return self.invProb.reg.objfcts[0](self.invProb.model) - else: - return ( - self.pgi_smallness( - self.invProb.model, external_weights=self.WeightsInTarget - ) - / self.CLnormalizedConstant - ) - - def ThetaTarget(self): - maxdiff = 0.0 - - for i in range(self.invProb.reg.gmm.n_components): - meandiff = np.linalg.norm( - (self.invProb.reg.gmm.means_[i] - self.invProb.reg.gmmref.means_[i]) - / self.invProb.reg.gmmref.means_[i], - ord=self.distance_norm, - ) - maxdiff = np.maximum(maxdiff, meandiff) - - if ( - self.invProb.reg.gmm.covariance_type == "full" - or self.invProb.reg.gmm.covariance_type == "spherical" - ): - covdiff = np.linalg.norm( - ( - self.invProb.reg.gmm.covariances_[i] - - self.invProb.reg.gmmref.covariances_[i] - ) - / self.invProb.reg.gmmref.covariances_[i], - ord=self.distance_norm, - ) - else: - covdiff = np.linalg.norm( - ( - self.invProb.reg.gmm.covariances_ - - self.invProb.reg.gmmref.covariances_ - ) - / self.invProb.reg.gmmref.covariances_, - ord=self.distance_norm, - ) - maxdiff = np.maximum(maxdiff, covdiff) - - pidiff = np.linalg.norm( - [ - ( - self.invProb.reg.gmm.weights_[i] - - self.invProb.reg.gmmref.weights_[i] - ) - / self.invProb.reg.gmmref.weights_[i] - ], - ord=self.distance_norm, - ) - maxdiff = np.maximum(maxdiff, pidiff) - - return maxdiff +""" +Backward compatibility with the ``simpeg.directives.directives`` submodule. - def endIter(self): - self._DM = False - self._CL = True - self._DP = True - self.dmlist = np.r_[[dmis(self.invProb.model) for dmis in self.dmisfit.objfcts]] - self.targetlist = np.r_[ - [dm < tgt for dm, tgt in zip(self.dmlist, self.DMtarget)] - ] +This file will be deleted when the ``simpeg.directives.directives`` submodule is +removed. +""" - if np.all(self.targetlist): - self._DM = True - - if self.TriggerSmall and np.any(self.smallness != -1): - if self.phims() > self.CLtarget: - self._CL = False - - if self.TriggerTheta: - if self.ThetaTarget() > self.ToleranceTheta: - self._DP = False - - if self.verbose: - message = "geophys. misfits: " + "; ".join( - map( - str, - [ - "{0} (target {1} [{2}])".format(val, tgt, cond) - for val, tgt, cond in zip( - np.round(self.dmlist, 1), - np.round(self.DMtarget, 1), - self.targetlist, - ) - ], - ) - ) - if self.TriggerSmall: - message += ( - " | smallness misfit: {0:.1f} (target: {1:.1f} [{2}])".format( - self.phims(), self.CLtarget, self.CL - ) - ) - if self.TriggerTheta: - message += " | GMM parameters within tolerance: {}".format(self.DP) - print(message) - - if self.AllStop: - self.opt.stopNextIteration = True - if self.verbose: - print("All targets have been reached") - - -class SaveEveryIteration(InversionDirective): - """SaveEveryIteration - - This directive saves an array at each iteration. The default - directory is the current directory and the models are saved as - ``InversionModel-YYYY-MM-DD-HH-MM-iter.npy`` - """ - - def __init__(self, directory=".", name="InversionModel", **kwargs): - super().__init__(**kwargs) - self.directory = directory - self.name = name - - @property - def directory(self): - """Directory to save results in. - - Returns - ------- - str - """ - return self._directory - - @directory.setter - def directory(self, value): - value = validate_string("directory", value) - fullpath = os.path.abspath(os.path.expanduser(value)) - - if not os.path.isdir(fullpath): - os.mkdir(fullpath) - self._directory = value - - @property - def name(self): - """Root of the filename to be saved. - - Returns - ------- - str - """ - return self._name - - @name.setter - def name(self, value): - self._name = validate_string("name", value) - - @property - def fileName(self): - if getattr(self, "_fileName", None) is None: - from datetime import datetime - - self._fileName = "{0!s}-{1!s}".format( - self.name, datetime.now().strftime("%Y-%m-%d-%H-%M") - ) - return self._fileName - - -class SaveModelEveryIteration(SaveEveryIteration): - """SaveModelEveryIteration - - This directive saves the model as a numpy array at each iteration. The - default directory is the current directoy and the models are saved as - ``InversionModel-YYYY-MM-DD-HH-MM-iter.npy`` - """ - - def initialize(self): - print( - "simpeg.SaveModelEveryIteration will save your models as: " - "'{0!s}###-{1!s}.npy'".format(self.directory + os.path.sep, self.fileName) - ) - - def endIter(self): - np.save( - "{0!s}{1:03d}-{2!s}".format( - self.directory + os.path.sep, self.opt.iter, self.fileName - ), - self.opt.xc, - ) - - -class SaveOutputEveryIteration(SaveEveryIteration): - """SaveOutputEveryIteration""" - - def __init__(self, save_txt=True, **kwargs): - super().__init__(**kwargs) - - self.save_txt = save_txt - - @property - def save_txt(self): - """Whether to save the output as a text file. - - Returns - ------- - bool - """ - return self._save_txt - - @save_txt.setter - def save_txt(self, value): - self._save_txt = validate_type("save_txt", value, bool) - - def initialize(self): - if self.save_txt is True: - print( - "simpeg.SaveOutputEveryIteration will save your inversion " - "progress as: '###-{0!s}.txt'".format(self.fileName) - ) - f = open(self.fileName + ".txt", "w") - header = " # beta phi_d phi_m phi_m_small phi_m_smoomth_x phi_m_smoomth_y phi_m_smoomth_z phi\n" - f.write(header) - f.close() - - # Create a list of each - - self.beta = [] - self.phi_d = [] - self.phi_m = [] - self.phi_m_small = [] - self.phi_m_smooth_x = [] - self.phi_m_smooth_y = [] - self.phi_m_smooth_z = [] - self.phi = [] - - def endIter(self): - phi_s, phi_x, phi_y, phi_z = 0, 0, 0, 0 - - for reg in self.reg.objfcts: - if isinstance(reg, Sparse): - i_s, i_x, i_y, i_z = 0, 1, 2, 3 - else: - i_s, i_x, i_y, i_z = 0, 1, 3, 5 - if getattr(reg, "alpha_s", None): - phi_s += reg.objfcts[i_s](self.invProb.model) * reg.alpha_s - if getattr(reg, "alpha_x", None): - phi_x += reg.objfcts[i_x](self.invProb.model) * reg.alpha_x - - if reg.regularization_mesh.dim > 1 and getattr(reg, "alpha_y", None): - phi_y += reg.objfcts[i_y](self.invProb.model) * reg.alpha_y - if reg.regularization_mesh.dim > 2 and getattr(reg, "alpha_z", None): - phi_z += reg.objfcts[i_z](self.invProb.model) * reg.alpha_z - - self.beta.append(self.invProb.beta) - self.phi_d.append(self.invProb.phi_d) - self.phi_m.append(self.invProb.phi_m) - self.phi_m_small.append(phi_s) - self.phi_m_smooth_x.append(phi_x) - self.phi_m_smooth_y.append(phi_y) - self.phi_m_smooth_z.append(phi_z) - self.phi.append(self.opt.f) - - if self.save_txt: - f = open(self.fileName + ".txt", "a") - f.write( - " {0:3d} {1:1.4e} {2:1.4e} {3:1.4e} {4:1.4e} {5:1.4e} " - "{6:1.4e} {7:1.4e} {8:1.4e}\n".format( - self.opt.iter, - self.beta[self.opt.iter - 1], - self.phi_d[self.opt.iter - 1], - self.phi_m[self.opt.iter - 1], - self.phi_m_small[self.opt.iter - 1], - self.phi_m_smooth_x[self.opt.iter - 1], - self.phi_m_smooth_y[self.opt.iter - 1], - self.phi_m_smooth_z[self.opt.iter - 1], - self.phi[self.opt.iter - 1], - ) - ) - f.close() - - def load_results(self): - results = np.loadtxt(self.fileName + str(".txt"), comments="#") - self.beta = results[:, 1] - self.phi_d = results[:, 2] - self.phi_m = results[:, 3] - self.phi_m_small = results[:, 4] - self.phi_m_smooth_x = results[:, 5] - self.phi_m_smooth_y = results[:, 6] - self.phi_m_smooth_z = results[:, 7] - - self.phi_m_smooth = ( - self.phi_m_smooth_x + self.phi_m_smooth_y + self.phi_m_smooth_z - ) - - self.f = results[:, 7] - - self.target_misfit = self.invProb.dmisfit.simulation.survey.nD - self.i_target = None - - if self.invProb.phi_d < self.target_misfit: - i_target = 0 - while self.phi_d[i_target] > self.target_misfit: - i_target += 1 - self.i_target = i_target - - def plot_misfit_curves( - self, - fname=None, - dpi=300, - plot_small_smooth=False, - plot_phi_m=True, - plot_small=False, - plot_smooth=False, - ): - self.target_misfit = np.sum([dmis.nD for dmis in self.invProb.dmisfit.objfcts]) - self.i_target = None - - if self.invProb.phi_d < self.target_misfit: - i_target = 0 - while self.phi_d[i_target] > self.target_misfit: - i_target += 1 - self.i_target = i_target - - fig = plt.figure(figsize=(5, 2)) - ax = plt.subplot(111) - ax_1 = ax.twinx() - ax.semilogy( - np.arange(len(self.phi_d)), self.phi_d, "k-", lw=2, label=r"$\phi_d$" - ) - - if plot_phi_m: - ax_1.semilogy( - np.arange(len(self.phi_d)), self.phi_m, "r", lw=2, label=r"$\phi_m$" - ) - - if plot_small_smooth or plot_small: - ax_1.semilogy( - np.arange(len(self.phi_d)), self.phi_m_small, "ro", label="small" - ) - if plot_small_smooth or plot_smooth: - ax_1.semilogy( - np.arange(len(self.phi_d)), self.phi_m_smooth_x, "rx", label="smooth_x" - ) - ax_1.semilogy( - np.arange(len(self.phi_d)), self.phi_m_smooth_y, "rx", label="smooth_y" - ) - ax_1.semilogy( - np.arange(len(self.phi_d)), self.phi_m_smooth_z, "rx", label="smooth_z" - ) - - ax.legend(loc=1) - ax_1.legend(loc=2) - - ax.plot( - np.r_[ax.get_xlim()[0], ax.get_xlim()[1]], - np.ones(2) * self.target_misfit, - "k:", - ) - ax.set_xlabel("Iteration") - ax.set_ylabel(r"$\phi_d$") - ax_1.set_ylabel(r"$\phi_m$", color="r") - ax_1.tick_params(axis="y", which="both", colors="red") - - plt.show() - if fname is not None: - fig.savefig(fname, dpi=dpi) - - def plot_tikhonov_curves(self, fname=None, dpi=200): - self.target_misfit = self.invProb.dmisfit.simulation.survey.nD - self.i_target = None - - if self.invProb.phi_d < self.target_misfit: - i_target = 0 - while self.phi_d[i_target] > self.target_misfit: - i_target += 1 - self.i_target = i_target - - fig = plt.figure(figsize=(5, 8)) - ax1 = plt.subplot(311) - ax2 = plt.subplot(312) - ax3 = plt.subplot(313) - - ax1.plot(self.beta, self.phi_d, "k-", lw=2, ms=4) - ax1.set_xlim(np.hstack(self.beta).min(), np.hstack(self.beta).max()) - ax1.set_xlabel(r"$\beta$", fontsize=14) - ax1.set_ylabel(r"$\phi_d$", fontsize=14) - - ax2.plot(self.beta, self.phi_m, "k-", lw=2) - ax2.set_xlim(np.hstack(self.beta).min(), np.hstack(self.beta).max()) - ax2.set_xlabel(r"$\beta$", fontsize=14) - ax2.set_ylabel(r"$\phi_m$", fontsize=14) - - ax3.plot(self.phi_m, self.phi_d, "k-", lw=2) - ax3.set_xlim(np.hstack(self.phi_m).min(), np.hstack(self.phi_m).max()) - ax3.set_xlabel(r"$\phi_m$", fontsize=14) - ax3.set_ylabel(r"$\phi_d$", fontsize=14) - - if self.i_target is not None: - ax1.plot(self.beta[self.i_target], self.phi_d[self.i_target], "k*", ms=10) - ax2.plot(self.beta[self.i_target], self.phi_m[self.i_target], "k*", ms=10) - ax3.plot(self.phi_m[self.i_target], self.phi_d[self.i_target], "k*", ms=10) - - for ax in [ax1, ax2, ax3]: - ax.set_xscale("linear") - ax.set_yscale("linear") - plt.tight_layout() - plt.show() - if fname is not None: - fig.savefig(fname, dpi=dpi) - - -class SaveOutputDictEveryIteration(SaveEveryIteration): - """ - Saves inversion parameters at every iteration. - """ - - # Initialize the output dict - def __init__(self, saveOnDisk=False, **kwargs): - super().__init__(**kwargs) - self.saveOnDisk = saveOnDisk - - @property - def saveOnDisk(self): - """Whether to save the output dict to disk. - - Returns - ------- - bool - """ - return self._saveOnDisk - - @saveOnDisk.setter - def saveOnDisk(self, value): - self._saveOnDisk = validate_type("saveOnDisk", value, bool) - - def initialize(self): - self.outDict = {} - if self.saveOnDisk: - print( - "simpeg.SaveOutputDictEveryIteration will save your inversion progress as dictionary: '###-{0!s}.npz'".format( - self.fileName - ) - ) - - def endIter(self): - # regCombo = ["phi_ms", "phi_msx"] - - # if self.simulation[0].mesh.dim >= 2: - # regCombo += ["phi_msy"] - - # if self.simulation[0].mesh.dim == 3: - # regCombo += ["phi_msz"] - - # Initialize the output dict - iterDict = {} - - # Save the data. - iterDict["iter"] = self.opt.iter - iterDict["beta"] = self.invProb.beta - iterDict["phi_d"] = self.invProb.phi_d - iterDict["phi_m"] = self.invProb.phi_m - - # for label, fcts in zip(regCombo, self.reg.objfcts[0].objfcts): - # iterDict[label] = fcts(self.invProb.model) - - iterDict["f"] = self.opt.f - iterDict["m"] = self.invProb.model - iterDict["dpred"] = self.invProb.dpred - - for reg in self.reg.objfcts: - if isinstance(reg, Sparse): - for reg_part, norm in zip(reg.objfcts, reg.norms): - reg_name = f"{type(reg_part).__name__}" - if hasattr(reg_part, "orientation"): - reg_name = reg_part.orientation + " " + reg_name - iterDict[reg_name + ".irls_threshold"] = reg_part.irls_threshold - iterDict[reg_name + ".norm"] = norm - - # Save the file as a npz - if self.saveOnDisk: - np.savez("{:03d}-{:s}".format(self.opt.iter, self.fileName), iterDict) - - self.outDict[self.opt.iter] = iterDict - - -@deprecate_class(removal_version="0.24.0", error=False) -class Update_IRLS(InversionDirective): - f_old = 0 - f_min_change = 1e-2 - beta_tol = 1e-1 - beta_ratio_l2 = None - prctile = 100 - chifact_start = 1.0 - chifact_target = 1.0 - - # Solving parameter for IRLS (mode:2) - irls_iteration = 0 - minGNiter = 1 - iterStart = 0 - sphericalDomain = False - - # Beta schedule - ComboObjFun = False - mode = 1 - coolEpsOptimized = True - coolEps_p = True - coolEps_q = True - floorEps_p = 1e-8 - floorEps_q = 1e-8 - coolEpsFact = 1.2 - silent = False - fix_Jmatrix = False - - def __init__( - self, - max_irls_iterations=20, - update_beta=True, - beta_search=False, - coolingFactor=2.0, - coolingRate=1, - **kwargs, - ): - super().__init__(**kwargs) - self.max_irls_iterations = max_irls_iterations - self.update_beta = update_beta - self.beta_search = beta_search - self.coolingFactor = coolingFactor - self.coolingRate = coolingRate - - @property - def max_irls_iterations(self): - """Maximum irls iterations. - - Returns - ------- - int - """ - return self._max_irls_iterations - - @max_irls_iterations.setter - def max_irls_iterations(self, value): - self._max_irls_iterations = validate_integer( - "max_irls_iterations", value, min_val=0 - ) - - @property - def coolingFactor(self): - """Beta is divided by this value every `coolingRate` iterations. - - Returns - ------- - float - """ - return self._coolingFactor - - @coolingFactor.setter - def coolingFactor(self, value): - self._coolingFactor = validate_float( - "coolingFactor", value, min_val=0.0, inclusive_min=False - ) - - @property - def coolingRate(self): - """Cool after this number of iterations. - - Returns - ------- - int - """ - return self._coolingRate - - @coolingRate.setter - def coolingRate(self, value): - self._coolingRate = validate_integer("coolingRate", value, min_val=1) - - @property - def update_beta(self): - """Whether to update beta. - - Returns - ------- - bool - """ - return self._update_beta - - @update_beta.setter - def update_beta(self, value): - self._update_beta = validate_type("update_beta", value, bool) - - @property - def beta_search(self): - """Whether to do a beta search. - - Returns - ------- - bool - """ - return self._beta_search - - @beta_search.setter - def beta_search(self, value): - self._beta_search = validate_type("beta_search", value, bool) - - @property - def target(self): - if getattr(self, "_target", None) is None: - nD = 0 - for survey in self.survey: - nD += survey.nD - - self._target = nD * self.chifact_target - - return self._target - - @target.setter - def target(self, val): - self._target = val - - @property - def start(self): - if getattr(self, "_start", None) is None: - if isinstance(self.survey, list): - self._start = 0 - for survey in self.survey: - self._start += survey.nD * self.chifact_start - - else: - self._start = self.survey.nD * self.chifact_start - return self._start - - @start.setter - def start(self, val): - self._start = val - - def initialize(self): - if self.mode == 1: - self.norms = [] - for reg in self.reg.objfcts: - if not isinstance(reg, Sparse): - continue - self.norms.append(reg.norms) - reg.norms = [2.0 for obj in reg.objfcts] - reg.model = self.invProb.model - - # Update the model used by the regularization - for reg in self.reg.objfcts: - if not isinstance(reg, Sparse): - continue - - reg.model = self.invProb.model - - if self.sphericalDomain: - self.angleScale() - - def endIter(self): - if self.sphericalDomain: - self.angleScale() - - # Check if misfit is within the tolerance, otherwise scale beta - if np.all( - [ - np.abs(1.0 - self.invProb.phi_d / self.target) > self.beta_tol, - self.update_beta, - self.mode != 1, - ] - ): - ratio = self.target / self.invProb.phi_d - - if ratio > 1: - ratio = np.mean([2.0, ratio]) - else: - ratio = np.mean([0.75, ratio]) - - self.invProb.beta = self.invProb.beta * ratio - - if np.all([self.mode != 1, self.beta_search]): - print("Beta search step") - # self.update_beta = False - # Re-use previous model and continue with new beta - self.invProb.model = self.reg.objfcts[0].model - self.opt.xc = self.reg.objfcts[0].model - self.opt.iter -= 1 - return - - elif np.all([self.mode == 1, self.opt.iter % self.coolingRate == 0]): - self.invProb.beta = self.invProb.beta / self.coolingFactor - - # After reaching target misfit with l2-norm, switch to IRLS (mode:2) - if np.all([self.invProb.phi_d < self.start, self.mode == 1]): - self.start_irls() - - # Only update after GN iterations - if np.all( - [(self.opt.iter - self.iterStart) % self.minGNiter == 0, self.mode != 1] - ): - if self.stopping_criteria(): - self.opt.stopNextIteration = True - return - - # Print to screen - for reg in self.reg.objfcts: - if not isinstance(reg, Sparse): - continue - - for obj in reg.objfcts: - if isinstance(reg, (Sparse, BaseSparse)): - obj.irls_threshold = obj.irls_threshold / self.coolEpsFact - - self.irls_iteration += 1 - - # Reset the regularization matrices so that it is - # recalculated for current model. Do it to all levels of comboObj - for reg in self.reg.objfcts: - if not isinstance(reg, Sparse): - continue - - reg.update_weights(reg.model) - - self.update_beta = True - self.invProb.phi_m_last = self.reg(self.invProb.model) - - def start_irls(self): - if not self.silent: - print( - "Reached starting chifact with l2-norm regularization:" - + " Start IRLS steps..." - ) - - self.mode = 2 - - if getattr(self.opt, "iter", None) is None: - self.iterStart = 0 - else: - self.iterStart = self.opt.iter - - self.invProb.phi_m_last = self.reg(self.invProb.model) - - # Either use the supplied irls_threshold, or fix base on distribution of - # model values - for reg in self.reg.objfcts: - if not isinstance(reg, Sparse): - continue - - for obj in reg.objfcts: - threshold = np.percentile( - np.abs(obj.mapping * obj._delta_m(self.invProb.model)), self.prctile - ) - if isinstance(obj, SmoothnessFirstOrder): - threshold /= reg.regularization_mesh.base_length - - obj.irls_threshold = threshold - - # Re-assign the norms supplied by user l2 -> lp - for reg, norms in zip(self.reg.objfcts, self.norms): - if not isinstance(reg, Sparse): - continue - reg.norms = norms - - # Save l2-model - self.invProb.l2model = self.invProb.model.copy() - - # Print to screen - for reg in self.reg.objfcts: - if not isinstance(reg, Sparse): - continue - if not self.silent: - print("irls_threshold " + str(reg.objfcts[0].irls_threshold)) - - def angleScale(self): - """ - Update the scales used by regularization for the - different block of models - """ - # Currently implemented for MVI-S only - max_p = [] - for reg in self.reg.objfcts[0].objfcts: - f_m = abs(reg.f_m(reg.model)) - max_p += [np.max(f_m)] - - max_p = np.asarray(max_p).max() - - max_s = [np.pi, np.pi] - - for reg, var in zip(self.reg.objfcts[1:], max_s): - for obj in reg.objfcts: - # TODO Need to make weights_shapes a public method - obj.set_weights( - angle_scale=np.ones(obj._weights_shapes[0]) * max_p / var - ) - - def validate(self, directiveList): - dList = directiveList.dList - self_ind = dList.index(self) - lin_precond_ind = [isinstance(d, UpdatePreconditioner) for d in dList] - - if any(lin_precond_ind): - assert lin_precond_ind.index(True) > self_ind, ( - "The directive 'UpdatePreconditioner' must be after Update_IRLS " - "in the directiveList" - ) - else: - warnings.warn( - "Without a Linear preconditioner, convergence may be slow. " - "Consider adding `Directives.UpdatePreconditioner` to your " - "directives list", - stacklevel=2, - ) - return True - - def stopping_criteria(self): - """ - Check for stopping criteria of max_irls_iteration or minimum change. - """ - phim_new = 0 - for reg in self.reg.objfcts: - if isinstance(reg, (Sparse, BaseSparse)): - reg.model = self.invProb.model - phim_new += reg(reg.model) - - # Check for maximum number of IRLS cycles1 - if self.irls_iteration == self.max_irls_iterations: - if not self.silent: - print( - "Reach maximum number of IRLS cycles:" - + " {0:d}".format(self.max_irls_iterations) - ) - return True - - # Check if the function has changed enough - f_change = np.abs(self.f_old - phim_new) / (self.f_old + 1e-12) - if np.all( - [ - f_change < self.f_min_change, - self.irls_iteration > 1, - np.abs(1.0 - self.invProb.phi_d / self.target) < self.beta_tol, - ] - ): - print("Minimum decrease in regularization." + "End of IRLS") - return True - - self.f_old = phim_new - - return False - - -class UpdatePreconditioner(InversionDirective): - """ - Create a Jacobi preconditioner for the linear problem - """ - - def __init__(self, update_every_iteration=True, **kwargs): - super().__init__(**kwargs) - self.update_every_iteration = update_every_iteration - - @property - def update_every_iteration(self): - """Whether to update the preconditioner at every iteration. - - Returns - ------- - bool - """ - return self._update_every_iteration - - @update_every_iteration.setter - def update_every_iteration(self, value): - self._update_every_iteration = validate_type( - "update_every_iteration", value, bool - ) - - def initialize(self): - # Create the pre-conditioner - regDiag = np.zeros_like(self.invProb.model) - m = self.invProb.model - - for reg in self.reg.objfcts: - # Check if regularization has a projection - rdg = reg.deriv2(m) - if not isinstance(rdg, Zero): - regDiag += rdg.diagonal() - - JtJdiag = np.zeros_like(self.invProb.model) - for sim, dmisfit in zip(self.simulation, self.dmisfit.objfcts): - if getattr(sim, "getJtJdiag", None) is None: - assert getattr(sim, "getJ", None) is not None, ( - "Simulation does not have a getJ attribute." - + "Cannot form the sensitivity explicitly" - ) - JtJdiag += np.sum(np.power((dmisfit.W * sim.getJ(m)), 2), axis=0) - else: - JtJdiag += sim.getJtJdiag(m, W=dmisfit.W) - - diagA = JtJdiag + self.invProb.beta * regDiag - diagA[diagA != 0] = diagA[diagA != 0] ** -1.0 - PC = sdiag((diagA)) - - self.opt.approxHinv = PC - - def endIter(self): - # Cool the threshold parameter - if self.update_every_iteration is False: - return - - # Create the pre-conditioner - regDiag = np.zeros_like(self.invProb.model) - m = self.invProb.model - - for reg in self.reg.objfcts: - # Check if he has wire - regDiag += reg.deriv2(m).diagonal() - - JtJdiag = np.zeros_like(self.invProb.model) - for sim, dmisfit in zip(self.simulation, self.dmisfit.objfcts): - if getattr(sim, "getJtJdiag", None) is None: - assert getattr(sim, "getJ", None) is not None, ( - "Simulation does not have a getJ attribute." - + "Cannot form the sensitivity explicitly" - ) - JtJdiag += np.sum(np.power((dmisfit.W * sim.getJ(m)), 2), axis=0) - else: - JtJdiag += sim.getJtJdiag(m, W=dmisfit.W) - - diagA = JtJdiag + self.invProb.beta * regDiag - diagA[diagA != 0] = diagA[diagA != 0] ** -1.0 - PC = sdiag((diagA)) - self.opt.approxHinv = PC - - -class Update_Wj(InversionDirective): - """ - Create approx-sensitivity base weighting using the probing method - """ - - def __init__(self, k=None, itr=None, **kwargs): - self.k = k - self.itr = itr - super().__init__(**kwargs) - - @property - def k(self): - """Number of probing cycles for the estimator. - - Returns - ------- - int - """ - return self._k - - @k.setter - def k(self, value): - if value is not None: - value = validate_integer("k", value, min_val=1) - self._k = value - - @property - def itr(self): - """Which iteration to update the sensitivity. - - Will always update if `None`. - - Returns - ------- - int or None - """ - return self._itr - - @itr.setter - def itr(self, value): - if value is not None: - value = validate_integer("itr", value, min_val=1) - self._itr = value - - def endIter(self): - if self.itr is None or self.itr == self.opt.iter: - m = self.invProb.model - if self.k is None: - self.k = int(self.survey.nD / 10) - - def JtJv(v): - Jv = self.simulation.Jvec(m, v) - - return self.simulation.Jtvec(m, Jv) - - JtJdiag = estimate_diagonal(JtJv, len(m), k=self.k) - JtJdiag = JtJdiag / max(JtJdiag) - - self.reg.wght = JtJdiag - - -class UpdateSensitivityWeights(InversionDirective): - r""" - Sensitivity weighting for linear and non-linear least-squares inverse problems. - - This directive computes the root-mean squared sensitivities for the - forward simulation(s) attached to the inverse problem, then truncates - and scales the result to create cell weights which are applied in the regularization. - The underlying theory is provided below in the `Notes` section. - - This directive **requires** that the map for the regularization function is either - class:`simpeg.maps.Wires` or class:`simpeg.maps.Identity`. In other words, the - sensitivity weighting cannot be applied for parametric inversion. In addition, - the simulation(s) connected to the inverse problem **must** have a ``getJ`` or - ``getJtJdiag`` method. - - This directive's place in the :class:`DirectivesList` **must** be - before any directives which update the preconditioner for the inverse problem - (i.e. :class:`UpdatePreconditioner`), and **must** be before any directives that - estimate the starting trade-off parameter (i.e. :class:`EstimateBeta_ByEig` - and :class:`EstimateBetaMaxDerivative`). - - Parameters - ---------- - every_iteration : bool - When ``True``, update sensitivity weighting at every model update; non-linear problems. - When ``False``, create sensitivity weights for starting model only; linear problems. - threshold : float - Threshold value for smallest weighting value. - threshold_method : {'amplitude', 'global', 'percentile'} - Threshold method for how `threshold_value` is applied: - - - amplitude: - the smallest root-mean squared sensitivity is a fractional percent of the largest value; must be between 0 and 1. - - global: - `threshold_value` is added to the cell weights prior to normalization; must be greater than 0. - - percentile: - the smallest root-mean squared sensitivity is set using percentile threshold; must be between 0 and 100. - - normalization_method : {'maximum', 'min_value', None} - Normalization method applied to sensitivity weights. - - Options are: - - - maximum: - sensitivity weights are normalized by the largest value such that the largest weight is equal to 1. - - minimum: - sensitivity weights are normalized by the smallest value, after thresholding, such that the smallest weights are equal to 1. - - ``None``: - normalization is not applied. - - Notes - ----- - Let :math:`\mathbf{J}` represent the Jacobian. To create sensitivity weights, root-mean squared (RMS) sensitivities - :math:`\mathbf{s}` are computed by summing the squares of the rows of the Jacobian: - - .. math:: - \mathbf{s} = \Bigg [ \sum_i \, \mathbf{J_{i, \centerdot }}^2 \, \Bigg ]^{1/2} - - The dynamic range of RMS sensitivities can span many orders of magnitude. When computing sensitivity - weights, thresholding is generally applied to set a minimum value. - - **Thresholding:** - - If **global** thresholding is applied, we add a constant :math:`\tau` to the RMS sensitivities: - - .. math:: - \mathbf{\tilde{s}} = \mathbf{s} + \tau - - In the case of **percentile** thresholding, we let :math:`s_{\%}` represent a given percentile. - Thresholding to set a minimum value is applied as follows: - - .. math:: - \tilde{s}_j = \begin{cases} - s_j \;\; for \;\; s_j \geq s_{\%} \\ - s_{\%} \;\; for \;\; s_j < s_{\%} - \end{cases} - - If **absolute** thresholding is applied, we define :math:`\eta` as a fractional percent. - In this case, thresholding is applied as follows: - - .. math:: - \tilde{s}_j = \begin{cases} - s_j \;\; for \;\; s_j \geq \eta s_{max} \\ - \eta s_{max} \;\; for \;\; s_j < \eta s_{max} - \end{cases} - """ - - def __init__( - self, - every_iteration=False, - threshold_value=1e-12, - threshold_method="amplitude", - normalization_method="maximum", - **kwargs, - ): - # Raise errors on deprecated arguments - if (key := "everyIter") in kwargs.keys(): - raise TypeError( - f"'{key}' property has been removed. Please use 'every_iteration'.", - ) - if (key := "threshold") in kwargs.keys(): - raise TypeError( - f"'{key}' property has been removed. Please use 'threshold_value'.", - ) - if (key := "normalization") in kwargs.keys(): - raise TypeError( - f"'{key}' property has been removed. " - "Please define normalization using 'normalization_method'.", - ) - - super().__init__(**kwargs) - - self.every_iteration = every_iteration - self.threshold_value = threshold_value - self.threshold_method = threshold_method - self.normalization_method = normalization_method - - @property - def every_iteration(self): - """Update sensitivity weights when model is updated. - - When ``True``, update sensitivity weighting at every model update; non-linear problems. - When ``False``, create sensitivity weights for starting model only; linear problems. - - Returns - ------- - bool - """ - return self._every_iteration - - @every_iteration.setter - def every_iteration(self, value): - self._every_iteration = validate_type("every_iteration", value, bool) - - everyIter = deprecate_property( - every_iteration, - "everyIter", - "every_iteration", - removal_version="0.20.0", - error=True, - ) - - @property - def threshold_value(self): - """Threshold value used to set minimum weighting value. - - The way thresholding is applied to the weighting model depends on the - `threshold_method` property. The choices for `threshold_method` are: - - - global: - `threshold_value` is added to the cell weights prior to normalization; must be greater than 0. - - percentile: - `threshold_value` is a percentile cutoff; must be between 0 and 100 - - amplitude: - `threshold_value` is the fractional percent of the largest value; must be between 0 and 1 - - - Returns - ------- - float - """ - return self._threshold_value - - @threshold_value.setter - def threshold_value(self, value): - self._threshold_value = validate_float("threshold_value", value, min_val=0.0) - - threshold = deprecate_property( - threshold_value, - "threshold", - "threshold_value", - removal_version="0.20.0", - error=True, - ) - - @property - def threshold_method(self): - """Threshold method for how `threshold_value` is applied: - - - global: - `threshold_value` is added to the cell weights prior to normalization; must be greater than 0. - - percentile: - the smallest root-mean squared sensitivity is set using percentile threshold; must be between 0 and 100 - - amplitude: - the smallest root-mean squared sensitivity is a fractional percent of the largest value; must be between 0 and 1 - - - Returns - ------- - str - """ - return self._threshold_method - - @threshold_method.setter - def threshold_method(self, value): - self._threshold_method = validate_string( - "threshold_method", value, string_list=["global", "percentile", "amplitude"] - ) - - @property - def normalization_method(self): - """Normalization method applied to sensitivity weights. - - Options are: - - - ``None`` - normalization is not applied - - maximum: - sensitivity weights are normalized by the largest value such that the largest weight is equal to 1. - - minimum: - sensitivity weights are normalized by the smallest value, after thresholding, such that the smallest weights are equal to 1. - - Returns - ------- - None, str - """ - return self._normalization_method - - @normalization_method.setter - def normalization_method(self, value): - if value is None: - self._normalization_method = value - else: - self._normalization_method = validate_string( - "normalization_method", value, string_list=["minimum", "maximum"] - ) - - normalization = deprecate_property( - normalization_method, - "normalization", - "normalization_method", - removal_version="0.20.0", - error=True, - ) - - def initialize(self): - """Compute sensitivity weights upon starting the inversion.""" - for reg in self.reg.objfcts: - if not isinstance(reg.mapping, (IdentityMap, Wires)): - raise TypeError( - f"Mapping for the regularization must be of type {IdentityMap} or {Wires}. " - + f"Input mapping of type {type(reg.mapping)}." - ) - - self.update() - - def endIter(self): - """Execute end of iteration.""" - - if self.every_iteration: - self.update() - - def update(self): - """Update sensitivity weights""" - - jtj_diag = np.zeros_like(self.invProb.model) - m = self.invProb.model - - for sim, dmisfit in zip(self.simulation, self.dmisfit.objfcts): - if getattr(sim, "getJtJdiag", None) is None: - if getattr(sim, "getJ", None) is None: - raise AttributeError( - "Simulation does not have a getJ attribute." - + "Cannot form the sensitivity explicitly" - ) - jtj_diag += mkvc(np.sum((dmisfit.W * sim.getJ(m)) ** 2.0, axis=0)) - else: - jtj_diag += sim.getJtJdiag(m, W=dmisfit.W) - - # Compute and sum root-mean squared sensitivities for all objective functions - wr = np.zeros_like(self.invProb.model) - for reg in self.reg.objfcts: - if isinstance(reg, BaseSimilarityMeasure): - continue - - mesh = reg.regularization_mesh - n_cells = mesh.nC - mapped_jtj_diag = reg.mapping * jtj_diag - # reshape the mapped, so you can divide by volume - # (let's say it was a vector or anisotropic model) - mapped_jtj_diag = mapped_jtj_diag.reshape((n_cells, -1), order="F") - wr_temp = mapped_jtj_diag / reg.regularization_mesh.vol[:, None] ** 2.0 - wr_temp = wr_temp.reshape(-1, order="F") - - wr += reg.mapping.deriv(self.invProb.model).T * wr_temp - - wr **= 0.5 - - # Apply thresholding - if self.threshold_method == "global": - wr += self.threshold_value - elif self.threshold_method == "percentile": - wr = np.clip( - wr, a_min=np.percentile(wr, self.threshold_value), a_max=np.inf - ) - else: - wr = np.clip(wr, a_min=self.threshold_value * wr.max(), a_max=np.inf) - - # Apply normalization - if self.normalization_method == "maximum": - wr /= wr.max() - elif self.normalization_method == "minimum": - wr /= wr.min() - - # Add sensitivity weighting to all model objective functions - for reg in self.reg.objfcts: - if not isinstance(reg, BaseSimilarityMeasure): - sub_regs = getattr(reg, "objfcts", [reg]) - for sub_reg in sub_regs: - sub_reg.set_weights(sensitivity=sub_reg.mapping * wr) - - def validate(self, directiveList): - """Validate directive against directives list. - - The ``UpdateSensitivityWeights`` directive impacts the regularization by applying - cell weights. As a result, its place in the :class:`DirectivesList` must be - before any directives which update the preconditioner for the inverse problem - (i.e. :class:`UpdatePreconditioner`), and must be before any directives that - estimate the starting trade-off parameter (i.e. :class:`EstimateBeta_ByEig` - and :class:`EstimateBetaMaxDerivative`). - - - Returns - ------- - bool - Returns ``True`` if validation passes. Otherwise, an error is thrown. - """ - # check if a beta estimator is in the list after setting the weights - dList = directiveList.dList - self_ind = dList.index(self) - - beta_estimator_ind = [isinstance(d, BaseBetaEstimator) for d in dList] - lin_precond_ind = [isinstance(d, UpdatePreconditioner) for d in dList] - - if any(beta_estimator_ind): - assert beta_estimator_ind.index(True) > self_ind, ( - "The directive for setting intial beta must be after UpdateSensitivityWeights " - "in the directiveList" - ) - - if any(lin_precond_ind): - assert lin_precond_ind.index(True) > self_ind, ( - "The directive 'UpdatePreconditioner' must be after UpdateSensitivityWeights " - "in the directiveList" - ) - - return True - - -class ProjectSphericalBounds(InversionDirective): - r""" - Trick for spherical coordinate system. - Project :math:`\theta` and :math:`\phi` angles back to :math:`[-\pi,\pi]` - using back and forth conversion. - spherical->cartesian->spherical - """ - - def initialize(self): - x = self.invProb.model - # Convert to cartesian than back to avoid over rotation - nC = int(len(x) / 3) - - xyz = spherical2cartesian(x.reshape((nC, 3), order="F")) - m = cartesian2spherical(xyz.reshape((nC, 3), order="F")) - - self.invProb.model = m - - for sim in self.simulation: - sim.model = m - - self.opt.xc = m - - def endIter(self): - x = self.invProb.model - nC = int(len(x) / 3) - - # Convert to cartesian than back to avoid over rotation - xyz = spherical2cartesian(x.reshape((nC, 3), order="F")) - m = cartesian2spherical(xyz.reshape((nC, 3), order="F")) - - self.invProb.model = m - - phi_m_last = [] - for reg in self.reg.objfcts: - reg.model = self.invProb.model - phi_m_last += [reg(self.invProb.model)] - - self.invProb.phi_m_last = phi_m_last - - for sim in self.simulation: - sim.model = m - - self.opt.xc = m +import warnings +from ._directives import * # noqa: F403,F401 + +warnings.warn( + "The `simpeg.directives.directives` submodule has been deprecated, " + "and will be removed in SimPEG v0.26.0." + "Import any directive class directly from the `simpeg.directives` module. " + "E.g.: `from simpeg.directives import BetaSchedule`", + FutureWarning, + stacklevel=2, +) diff --git a/simpeg/directives/pgi_directives.py b/simpeg/directives/pgi_directives.py index 60f4488b90..ec0108210c 100644 --- a/simpeg/directives/pgi_directives.py +++ b/simpeg/directives/pgi_directives.py @@ -1,474 +1,18 @@ -############################################################################### -# # -# Directives for PGI: Petrophysically guided Regularization # -# # -############################################################################### - -import copy - -import numpy as np - -from ..directives import InversionDirective, MultiTargetMisfits -from ..regularization import ( - PGI, - PGIsmallness, - SmoothnessFirstOrder, - SparseSmoothness, -) -from ..utils import ( - GaussianMixtureWithNonlinearRelationships, - GaussianMixtureWithNonlinearRelationshipsWithPrior, - GaussianMixtureWithPrior, - WeightedGaussianMixture, - mkvc, +""" +Backward compatibility with the ``simpeg.directives.pgi_directives`` submodule. + +This file will be deleted when the ``simpeg.directives.pgi_directives`` submodule is +removed. +""" + +import warnings +from ._pgi_directives import * # noqa: F403,F401 + +warnings.warn( + "The `simpeg.directives.pgi_directives` submodule has been deprecated, " + "and will be removed in SimPEG v0.26.0." + "Import any directive class directly from the `simpeg.directives` module. " + "E.g.: `from simpeg.directives import PGI_UpdateParameters`. ", + FutureWarning, + stacklevel=2, ) - - -class PGI_UpdateParameters(InversionDirective): - """ - This directive is to be used with regularization from regularization.pgi. - It updates: - - the reference model and weights in the smallness (L2-approximation of PGI) - - the GMM as a MAP estimate between the prior and the current model - For more details, please consult: - - https://doi.org/10.1093/gji/ggz389 - """ - - verbose = False # print info. about the GMM at each iteration - update_rate = 1 # updates at each `update_rate` iterations - update_gmm = False # update the GMM - zeta = ( - 1e10 # confidence in the prior proportions; default: high value, keep GMM fixed - ) - nu = ( - 1e10 # confidence in the prior covariances; default: high value, keep GMM fixed - ) - kappa = 1e10 # confidence in the prior means;default: high value, keep GMM fixed - update_covariances = ( - True # Average the covariances, If false: average the precisions - ) - fixed_membership = None # keep the membership of specific cells fixed - keep_ref_fixed_in_Smooth = True # keep mref fixed in the Smoothness - - def initialize(self): - pgi_reg = self.reg.get_functions_of_type(PGIsmallness) - if len(pgi_reg) != 1: - raise UserWarning( - "'PGI_UpdateParameters' requires one 'PGIsmallness' regularization " - "in the objective function." - ) - self.pgi_reg = pgi_reg[0] - - def endIter(self): - if self.opt.iter > 0 and self.opt.iter % self.update_rate == 0: - m = self.invProb.model - modellist = self.pgi_reg.wiresmap * m - model = np.c_[[a * b for a, b in zip(self.pgi_reg.maplist, modellist)]].T - - if self.update_gmm and isinstance( - self.pgi_reg.gmmref, GaussianMixtureWithNonlinearRelationships - ): - clfupdate = GaussianMixtureWithNonlinearRelationshipsWithPrior( - gmmref=self.pgi_reg.gmmref, - zeta=self.zeta, - kappa=self.kappa, - nu=self.nu, - verbose=self.verbose, - prior_type="semi", - update_covariances=self.update_covariances, - max_iter=self.pgi_reg.gmm.max_iter, - n_init=self.pgi_reg.gmm.n_init, - reg_covar=self.pgi_reg.gmm.reg_covar, - weights_init=self.pgi_reg.gmm.weights_, - means_init=self.pgi_reg.gmm.means_, - precisions_init=self.pgi_reg.gmm.precisions_, - random_state=self.pgi_reg.gmm.random_state, - tol=self.pgi_reg.gmm.tol, - verbose_interval=self.pgi_reg.gmm.verbose_interval, - warm_start=self.pgi_reg.gmm.warm_start, - fixed_membership=self.fixed_membership, - ) - clfupdate = clfupdate.fit(model) - - elif self.update_gmm and isinstance( - self.pgi_reg.gmmref, WeightedGaussianMixture - ): - clfupdate = GaussianMixtureWithPrior( - gmmref=self.pgi_reg.gmmref, - zeta=self.zeta, - kappa=self.kappa, - nu=self.nu, - verbose=self.verbose, - prior_type="semi", - update_covariances=self.update_covariances, - max_iter=self.pgi_reg.gmm.max_iter, - n_init=self.pgi_reg.gmm.n_init, - reg_covar=self.pgi_reg.gmm.reg_covar, - weights_init=self.pgi_reg.gmm.weights_, - means_init=self.pgi_reg.gmm.means_, - precisions_init=self.pgi_reg.gmm.precisions_, - random_state=self.pgi_reg.gmm.random_state, - tol=self.pgi_reg.gmm.tol, - verbose_interval=self.pgi_reg.gmm.verbose_interval, - warm_start=self.pgi_reg.gmm.warm_start, - fixed_membership=self.fixed_membership, - ) - clfupdate = clfupdate.fit(model) - - else: - clfupdate = copy.deepcopy(self.pgi_reg.gmmref) - - self.pgi_reg.gmm = clfupdate - membership = self.pgi_reg.gmm.predict(model) - - if self.fixed_membership is not None: - membership[self.fixed_membership[:, 0]] = self.fixed_membership[:, 1] - - mref = mkvc(self.pgi_reg.gmm.means_[membership]) - self.pgi_reg.reference_model = mref - if getattr(self.fixed_membership, "shape", [0, 0])[0] < len(membership): - self.pgi_reg._r_second_deriv = None - - -class PGI_BetaAlphaSchedule(InversionDirective): - """ - This directive is to be used with regularizations from regularization.pgi. - It implements the strategy described in https://doi.org/10.1093/gji/ggz389 - for iteratively updating beta and alpha_s for fitting the - geophysical and smallness targets. - """ - - verbose = False # print information (progress, updates made) - tolerance = 0.0 # tolerance on the geophysical target misfit for cooling - progress = 0.1 # minimum percentage progress (default 10%) before cooling beta - coolingFactor = 2.0 # when cooled, beta is divided by it - warmingFactor = 1.0 # when warmed, alpha_s is multiplied by the ratio of the - # geophysical target with their current misfit, times this factor - mode = 1 # mode 1: start with nothing fitted. Mode 2: warmstart with fitted geophysical data - alphasmax = 1e10 # max alpha_s - betamin = 1e-10 # minimum beta - update_rate = 1 # update every `update_rate` iterations - pgi_reg = None - ratio_in_cooling = ( - False # add the ratio of geophysical misfit with their target in cooling - ) - - def initialize(self): - """Initialize the directive.""" - self.update_previous_score() - self.update_previous_dmlist() - - def endIter(self): - """Run after the end of each iteration in the inversion.""" - # Get some variables from the MultiTargetMisfits directive - data_misfits_achieved = self.multi_target_misfits_directive.DM - data_misfits_target = self.multi_target_misfits_directive.DMtarget - dmlist = self.multi_target_misfits_directive.dmlist - targetlist = self.multi_target_misfits_directive.targetlist - - # Change mode if data misfit targets have been achieved - if data_misfits_achieved: - self.mode = 2 - - # Don't cool beta of warm alpha if we are in the first iteration or if - # the current iteration doesn't match the update rate - if self.opt.iter == 0 or self.opt.iter % self.update_rate != 0: - self.update_previous_score() - self.update_previous_dmlist() - return None - - if self.verbose: - targets = np.round( - np.maximum( - (1.0 - self.progress) * self.previous_dmlist, - (1.0 + self.tolerance) * data_misfits_target, - ), - decimals=1, - ) - dmlist_rounded = np.round(dmlist, decimals=1) - print( - f"Beta cooling evaluation: progress: {dmlist_rounded}; " - f"minimum progress targets: {targets}" - ) - - # Decide if we should cool beta - threshold = np.maximum( - (1.0 - self.progress) * self.previous_dmlist[~targetlist], - data_misfits_target[~targetlist], - ) - if ( - (dmlist[~targetlist] > threshold).all() - and not data_misfits_achieved - and self.mode == 1 - and self.invProb.beta > self.betamin - ): - self.cool_beta() - if self.verbose: - print("Decreasing beta to counter data misfit decrase plateau.") - - # Decide if we should warm alpha instead - elif ( - data_misfits_achieved - and self.mode == 2 - and np.all(self.pgi_regularization.alpha_pgi < self.alphasmax) - ): - self.warm_alpha() - if self.verbose: - print( - "Warming alpha_pgi to favor clustering: ", - self.pgi_regularization.alpha_pgi, - ) - - # Decide if we should cool beta (to counter data misfit increase) - elif ( - np.any(dmlist > (1.0 + self.tolerance) * data_misfits_target) - and self.mode == 2 - and self.invProb.beta > self.betamin - ): - self.cool_beta() - if self.verbose: - print("Decreasing beta to counter data misfit increase.") - - # Update previous score and dmlist - self.update_previous_score() - self.update_previous_dmlist() - - def cool_beta(self): - """Cool beta according to schedule.""" - data_misfits_target = self.multi_target_misfits_directive.DMtarget - dmlist = self.multi_target_misfits_directive.dmlist - ratio = 1.0 - indx = dmlist > (1.0 + self.tolerance) * data_misfits_target - if np.any(indx) and self.ratio_in_cooling: - ratio = np.median([dmlist[indx] / data_misfits_target[indx]]) - self.invProb.beta /= self.coolingFactor * ratio - - def warm_alpha(self): - """Warm alpha according to schedule.""" - data_misfits_target = self.multi_target_misfits_directive.DMtarget - dmlist = self.multi_target_misfits_directive.dmlist - ratio = np.median(data_misfits_target / dmlist) - self.pgi_regularization.alpha_pgi *= self.warmingFactor * ratio - - def update_previous_score(self): - """ - Update the value of the ``previous_score`` attribute. - - Update it with the current value of the petrophysical misfit, obtained - from the :meth:`MultiTargetMisfit.phims()` method. - """ - self.previous_score = copy.deepcopy(self.multi_target_misfits_directive.phims()) - - def update_previous_dmlist(self): - """ - Update the value of the ``previous_dmlist`` attribute. - - Update it with the current value of the data misfits, obtained - from the :meth:`MultiTargetMisfit.dmlist` attribute. - """ - self.previous_dmlist = copy.deepcopy(self.multi_target_misfits_directive.dmlist) - - @property - def directives(self): - """List of all the directives in the :class:`simpeg.inverison.BaseInversion``.""" - return self.inversion.directiveList.dList - - @property - def multi_target_misfits_directive(self): - """``MultiTargetMisfit`` directive in the :class:`simpeg.inverison.BaseInversion``.""" - if not hasattr(self, "_mtm_directive"): - # Obtain multi target misfits directive from the directive list - multi_target_misfits_directive = [ - directive - for directive in self.directives - if isinstance(directive, MultiTargetMisfits) - ] - if not multi_target_misfits_directive: - raise UserWarning( - "No MultiTargetMisfits directive found in the current inversion. " - "A MultiTargetMisfits directive is needed by the " - "PGI_BetaAlphaSchedule directive." - ) - (self._mtm_directive,) = multi_target_misfits_directive - return self._mtm_directive - - @property - def pgi_update_params_directive(self): - """``PGI_UpdateParam``s directive in the :class:`simpeg.inverison.BaseInversion``.""" - if not hasattr(self, "_pgi_update_params"): - # Obtain PGI_UpdateParams directive from the directive list - pgi_update_params_directive = [ - directive - for directive in self.directives - if isinstance(directive, PGI_UpdateParameters) - ] - if pgi_update_params_directive: - (self._pgi_update_params,) = pgi_update_params_directive - else: - self._pgi_update_params = None - return self._pgi_update_params - - @property - def pgi_regularization(self): - """PGI regularization in the :class:`simpeg.inverse_problem.BaseInvProblem``.""" - if not hasattr(self, "_pgi_regularization"): - pgi_regularization = self.reg.get_functions_of_type(PGI) - if len(pgi_regularization) != 1: - raise UserWarning( - "'PGI_UpdateParameters' requires one 'PGI' regularization " - "in the objective function." - ) - self._pgi_regularization = pgi_regularization[0] - return self._pgi_regularization - - -class PGI_AddMrefInSmooth(InversionDirective): - """ - This directive is to be used with regularizations from regularization.pgi. - It implements the strategy described in https://doi.org/10.1093/gji/ggz389 - for including the learned reference model, once stable, in the smoothness terms. - """ - - # Chi factor for Data Misfit - chifact = 1.0 - tolerance_phid = 0.0 - phi_d_target = None - wait_till_stable = True - tolerance = 0.0 - verbose = False - - def initialize(self): - targetclass = np.r_[ - [ - isinstance(dirpart, MultiTargetMisfits) - for dirpart in self.inversion.directiveList.dList - ] - ] - if ~np.any(targetclass): - self.DMtarget = None - else: - self.targetclass = np.where(targetclass)[0][-1] - self._DMtarget = self.inversion.directiveList.dList[ - self.targetclass - ].DMtarget - - self.pgi_updategmm_class = np.r_[ - [ - isinstance(dirpart, PGI_UpdateParameters) - for dirpart in self.inversion.directiveList.dList - ] - ] - - if getattr(self.reg.objfcts[0], "objfcts", None) is not None: - # Find the petrosmallness terms in a two-levels combo-regularization. - petrosmallness = np.where( - np.r_[[isinstance(regpart, PGI) for regpart in self.reg.objfcts]] - )[0][0] - self.petrosmallness = petrosmallness - - # Find the smoothness terms in a two-levels combo-regularization. - Smooth = [] - for i, regobjcts in enumerate(self.reg.objfcts): - for j, regpart in enumerate(regobjcts.objfcts): - Smooth += [ - [ - i, - j, - isinstance( - regpart, (SmoothnessFirstOrder, SparseSmoothness) - ), - ] - ] - self.Smooth = np.r_[Smooth] - - self.nbr = np.sum( - [len(self.reg.objfcts[i].objfcts) for i in range(len(self.reg.objfcts))] - ) - self._regmode = 1 - self.pgi_reg = self.reg.objfcts[self.petrosmallness] - - else: - self._regmode = 2 - self.pgi_reg = self.reg - self.nbr = len(self.reg.objfcts) - self.Smooth = np.r_[ - [ - isinstance(regpart, (SmoothnessFirstOrder, SparseSmoothness)) - for regpart in self.reg.objfcts - ] - ] - self._regmode = 2 - - if ~np.any(self.pgi_updategmm_class): - self.previous_membership = self.pgi_reg.membership(self.invProb.model) - else: - self.previous_membership = self.pgi_reg.compute_quasi_geology_model() - - @property - def DMtarget(self): - if getattr(self, "_DMtarget", None) is None: - self.phi_d_target = self.invProb.dmisfit.survey.nD - self._DMtarget = self.chifact * self.phi_d_target - return self._DMtarget - - @DMtarget.setter - def DMtarget(self, val): - self._DMtarget = val - - def endIter(self): - self.DM = self.inversion.directiveList.dList[self.targetclass].DM - self.dmlist = self.inversion.directiveList.dList[self.targetclass].dmlist - - if ~np.any(self.pgi_updategmm_class): - self.membership = self.pgi_reg.membership(self.invProb.model) - else: - self.membership = self.pgi_reg.compute_quasi_geology_model() - - same_mref = np.all(self.membership == self.previous_membership) - percent_diff = ( - len(self.membership) - - np.count_nonzero(self.previous_membership == self.membership) - ) / len(self.membership) - if self.verbose: - print( - "mref changed in ", - len(self.membership) - - np.count_nonzero(self.previous_membership == self.membership), - " places", - ) - if ( - self.DM or np.all(self.dmlist < (1 + self.tolerance_phid) * self.DMtarget) - ) and ( - same_mref or not self.wait_till_stable or percent_diff <= self.tolerance - ): - self.reg.reference_model_in_smooth = True - self.pgi_reg.reference_model_in_smooth = True - - if self._regmode == 2: - for i in range(self.nbr): - if self.Smooth[i]: - self.reg.objfcts[i].reference_model = mkvc( - self.pgi_reg.gmm.means_[self.membership] - ) - if self.verbose: - print( - "Add mref to Smoothness. Changes in mref happened in {} % of the cells".format( - percent_diff - ) - ) - - elif self._regmode == 1: - for i in range(self.nbr): - if self.Smooth[i, 2]: - idx = self.Smooth[i, :2] - self.reg.objfcts[idx[0]].objfcts[idx[1]].reference_model = mkvc( - self.pgi_reg.gmm.means_[self.membership] - ) - if self.verbose: - print( - "Add mref to Smoothness. Changes in mref happened in {} % of the cells".format( - percent_diff - ) - ) - - self.previous_membership = copy.deepcopy(self.membership) diff --git a/simpeg/directives/sim_directives.py b/simpeg/directives/sim_directives.py index f40b828c7d..e2c3f8a5f3 100644 --- a/simpeg/directives/sim_directives.py +++ b/simpeg/directives/sim_directives.py @@ -1,390 +1,18 @@ -import numpy as np -from ..regularization import BaseSimilarityMeasure -from ..utils import eigenvalue_by_power_iteration -from ..optimization import IterationPrinters, StoppingCriteria -from .directives import InversionDirective, SaveEveryIteration - - -############################################################################### -# # -# Directives of joint inversion # -# # -############################################################################### -class SimilarityMeasureInversionPrinters: - betas = { - "title": "betas", - "value": lambda M: ["{:.2e}".format(elem) for elem in M.parent.betas], - "width": 26, - "format": "%s", - } - lambd = { - "title": "lambda", - "value": lambda M: M.parent.lambd, - "width": 10, - "format": "%1.2e", - } - phi_d_list = { - "title": "phi_d", - "value": lambda M: ["{:.2e}".format(elem) for elem in M.parent.phi_d_list], - "width": 26, - "format": "%s", - } - phi_m_list = { - "title": "phi_m", - "value": lambda M: ["{:.2e}".format(elem) for elem in M.parent.phi_m_list], - "width": 26, - "format": "%s", - } - phi_sim = { - "title": "phi_sim", - "value": lambda M: M.parent.phi_sim, - "width": 10, - "format": "%1.2e", - } - iterationCG = { - "title": "iterCG", - "value": lambda M: M.cg_count, - "width": 10, - "format": "%3d", - } - - -class SimilarityMeasureInversionDirective(InversionDirective): - """ - Directive for two model similiraty measure joint inversions. Sets Printers and - StoppingCriteria. - - Notes - ----- - Methods assume we are working with two models, and a single similarity measure. - Also, the SimilarityMeasure objective function must be the last regularization. - """ - - printers = [ - IterationPrinters.iteration, - SimilarityMeasureInversionPrinters.betas, - SimilarityMeasureInversionPrinters.lambd, - IterationPrinters.f, - SimilarityMeasureInversionPrinters.phi_d_list, - SimilarityMeasureInversionPrinters.phi_m_list, - SimilarityMeasureInversionPrinters.phi_sim, - SimilarityMeasureInversionPrinters.iterationCG, - ] - - def initialize(self): - if not isinstance(self.reg.objfcts[-1], BaseSimilarityMeasure): - raise TypeError( - f"The last regularization function must be an instance of " - f"BaseSimilarityMeasure, got {type(self.reg.objfcts[-1])}." - ) - - # define relevant attributes - self.betas = self.reg.multipliers[:-1] - self.lambd = self.reg.multipliers[-1] - self.phi_d_list = [] - self.phi_m_list = [] - self.phi_sim = 0.0 - - # pass attributes to invProb - self.invProb.betas = self.betas - self.invProb.num_models = len(self.betas) - self.invProb.lambd = self.lambd - self.invProb.phi_d_list = self.phi_d_list - self.invProb.phi_m_list = self.phi_m_list - self.invProb.phi_sim = self.phi_sim - - self.opt.printers = self.printers - self.opt.stoppers = [StoppingCriteria.iteration] - - def validate(self, directiveList): - # check that this directive is first in the DirectiveList - dList = directiveList.dList - self_ind = dList.index(self) - if self_ind != 0: - raise IndexError( - "The CrossGradientInversionDirective must be first in directive list." - ) - return True - - def endIter(self): - # compute attribute values - phi_d = [] - for dmis in self.dmisfit.objfcts: - phi_d.append(dmis(self.opt.xc)) - - phi_m = [] - for reg in self.reg.objfcts: - phi_m.append(reg(self.opt.xc)) - - # pass attributes values to invProb - self.invProb.phi_d_list = phi_d - self.invProb.phi_m_list = phi_m[:-1] - self.invProb.phi_sim = phi_m[-1] - self.invProb.betas = self.reg.multipliers[:-1] - # Assume last reg.objfct is the coupling - self.invProb.lambd = self.reg.multipliers[-1] - - -class SimilarityMeasureSaveOutputEveryIteration(SaveEveryIteration): - """ - SaveOutputEveryIteration for Joint Inversions. - Saves information on the tradeoff parameters, data misfits, regularizations, - coupling term, number of CG iterations, and value of cost function. - """ - - header = None - save_txt = True - betas = None - phi_d = None - phi_m = None - phi_sim = None - phi = None - - def initialize(self): - if self.save_txt is True: - print( - "CrossGradientSaveOutputEveryIteration will save your inversion " - "progress as: '###-{0!s}.txt'".format(self.fileName) - ) - f = open(self.fileName + ".txt", "w") - self.header = " # betas lambda joint_phi_d joint_phi_m phi_sim iterCG phi \n" - f.write(self.header) - f.close() - - # Create a list of each - self.betas = [] - self.lambd = [] - self.phi_d = [] - self.phi_m = [] - self.phi = [] - self.phi_sim = [] - - def endIter(self): - self.betas.append(["{:.2e}".format(elem) for elem in self.invProb.betas]) - self.phi_d.append(["{:.3e}".format(elem) for elem in self.invProb.phi_d_list]) - self.phi_m.append(["{:.3e}".format(elem) for elem in self.invProb.phi_m_list]) - self.lambd.append("{:.2e}".format(self.invProb.lambd)) - self.phi_sim.append(self.invProb.phi_sim) - self.phi.append(self.opt.f) - - if self.save_txt: - f = open(self.fileName + ".txt", "a") - i = self.opt.iter - f.write( - " {0:2d} {1} {2} {3} {4} {5:1.4e} {6:d} {7:1.4e}\n".format( - i, - self.betas[i - 1], - self.lambd[i - 1], - self.phi_d[i - 1], - self.phi_m[i - 1], - self.phi_sim[i - 1], - self.opt.cg_count, - self.phi[i - 1], - ) - ) - f.close() - - def load_results(self): - results = np.loadtxt(self.fileName + str(".txt"), comments="#") - self.betas = results[:, 1] - self.lambd = results[:, 2] - self.phi_d = results[:, 3] - self.phi_m = results[:, 4] - self.phi_sim = results[:, 5] - self.f = results[:, 7] - - -class PairedBetaEstimate_ByEig(InversionDirective): - """ - Estimate the trade-off parameter, beta, between pairs of data misfit(s) and the - regularization(s) as a multiple of the ratio between the highest eigenvalue of the - data misfit term and the highest eigenvalue of the regularization. - The highest eigenvalues are estimated through power iterations and Rayleigh - quotient. - - Notes - ----- - This class assumes the order of the data misfits for each model parameter match - the order for the respective regularizations, i.e. - - >>> data_misfits = [phi_d_m1, phi_d_m2, phi_d_m3] - >>> regs = [phi_m_m1, phi_m_m2, phi_m_m3] - - In which case it will estimate regularization parameters for each respective pair. - """ - - beta0_ratio = 1.0 #: the estimated ratio is multiplied by this to obtain beta - n_pw_iter = 4 #: number of power iterations for estimation. - seed = None #: Random seed for the directive - - def initialize(self): - r""" - The initial beta is calculated by comparing the estimated - eigenvalues of :math:`J^T J` and :math:`W^T W`. - To estimate the eigenvector of **A**, we will use one iteration - of the *Power Method*: - - .. math:: - - \mathbf{x_1 = A x_0} - - Given this (very course) approximation of the eigenvector, we can - use the *Rayleigh quotient* to approximate the largest eigenvalue. - - .. math:: - - \lambda_0 = \frac{\mathbf{x^\top A x}}{\mathbf{x^\top x}} - - We will approximate the largest eigenvalue for both JtJ and WtW, - and use some ratio of the quotient to estimate beta0. - - .. math:: - - \beta_0 = \gamma \frac{\mathbf{x^\top J^\top J x}}{\mathbf{x^\top W^\top W x}} - - :rtype: float - :return: beta0 - """ - rng = np.random.default_rng(seed=self.seed) - - if self.verbose: - print("Calculating the beta0 parameter.") - - m = self.invProb.model - dmis_eigenvalues = [] - reg_eigenvalues = [] - dmis_objs = self.dmisfit.objfcts - reg_objs = [ - obj - for obj in self.reg.objfcts - if not isinstance(obj, BaseSimilarityMeasure) - ] - if len(dmis_objs) != len(reg_objs): - raise ValueError( - f"There must be the same number of data misfit and regularizations." - f"Got {len(dmis_objs)} and {len(reg_objs)} respectively." - ) - for dmis, reg in zip(dmis_objs, reg_objs): - dmis_eigenvalues.append( - eigenvalue_by_power_iteration( - dmis, - m, - n_pw_iter=self.n_pw_iter, - random_seed=rng, - ) - ) - - reg_eigenvalues.append( - eigenvalue_by_power_iteration( - reg, - m, - n_pw_iter=self.n_pw_iter, - random_seed=rng, - ) - ) - - self.ratios = np.array(dmis_eigenvalues) / np.array(reg_eigenvalues) - self.invProb.betas = self.beta0_ratio * self.ratios - self.reg.multipliers[:-1] = self.invProb.betas - - -class PairedBetaSchedule(InversionDirective): - """ - Directive for beta cooling schedule to determine the tradeoff - parameters when using paired data misfits and regularizations for a joint inversion. - """ - - chifact_target = 1.0 - beta_tol = 1e-1 - update_beta = True - cooling_rate = 1 - cooling_factor = 2 - dmis_met = False - - @property - def target(self): - if getattr(self, "_target", None) is None: - nD = np.array([survey.nD for survey in self.survey]) - - self._target = nD * self.chifact_target - - return self._target - - @target.setter - def target(self, val): - self._target = val - - def initialize(self): - self.dmis_met = np.zeros_like(self.invProb.betas, dtype=bool) - - def endIter(self): - # Check if target misfit has been reached, if so, set dmis_met to True - for i, phi_d in enumerate(self.invProb.phi_d_list): - self.dmis_met[i] = phi_d < self.target[i] - - # check separately if misfits are within the tolerance, - # otherwise, scale beta individually - for i, phi_d in enumerate(self.invProb.phi_d_list): - if self.opt.iter > 0 and self.opt.iter % self.cooling_rate == 0: - target = self.target[i] - ratio = phi_d / target - if self.update_beta and ratio <= (1.0 + self.beta_tol): - if ratio <= 1: - ratio = np.maximum(0.75, ratio) - else: - ratio = np.minimum(1.5, ratio) - - self.invProb.betas[i] /= ratio - elif ratio > 1.0: - self.invProb.betas[i] /= self.cooling_factor - - self.reg.multipliers[:-1] = self.invProb.betas - - -class MovingAndMultiTargetStopping(InversionDirective): - r""" - Directive for setting stopping criteria for a joint inversion. - Ensures both that all target misfits are met and there is a small change in the - model. Computes the percentage change of the current model from the previous model. - - ..math:: - \frac {\| \mathbf{m_i} - \mathbf{m_{i-1}} \|} {\| \mathbf{m_{i-1}} \|} - """ - - tol = 1e-5 - beta_tol = 1e-1 - chifact_target = 1.0 - - @property - def target(self): - if getattr(self, "_target", None) is None: - nD = [] - for survey in self.survey: - nD += [survey.nD] - nD = np.array(nD) - - self._target = nD * self.chifact_target - - return self._target - - @target.setter - def target(self, val): - self._target = val - - def endIter(self): - for phi_d, target in zip(self.invProb.phi_d_list, self.target): - if np.abs(1.0 - phi_d / target) >= self.beta_tol: - return - if ( - np.linalg.norm(self.opt.xc - self.opt.x_last) - / np.linalg.norm(self.opt.x_last) - > self.tol - ): - return - - print( - "stopping criteria met: ", - np.linalg.norm(self.opt.xc - self.opt.x_last) - / np.linalg.norm(self.opt.x_last), - ) - self.opt.stopNextIteration = True +""" +Backward compatibility with the ``simpeg.directives.sim_directives`` submodule. + +This file will be deleted when the ``simpeg.directives.sim_directives`` submodule +is removed. +""" + +import warnings +from ._sim_directives import * # noqa: F403,F401 + +warnings.warn( + "The `simpeg.directives.sim_directives` submodule has been deprecated, " + "and will be removed in SimPEG v0.26.0." + "Import any directive class directly from the `simpeg.directives` module. " + "E.g.: `from simpeg.directives import PairedBetaEstimate_ByEig`", + FutureWarning, + stacklevel=2, +) diff --git a/tests/base/test_directives_deprecations.py b/tests/base/test_directives_deprecations.py new file mode 100644 index 0000000000..0c930b6b1d --- /dev/null +++ b/tests/base/test_directives_deprecations.py @@ -0,0 +1,18 @@ +""" +Test deprecation of public directives submodules. +""" + +import pytest +import importlib + +REGEX = r"The `simpeg\.directives\.[a-z_]+` submodule has been deprecated, " +DEPRECATED_SUBMODULES = ("directives", "pgi_directives", "sim_directives") + + +@pytest.mark.parametrize("submodule", DEPRECATED_SUBMODULES) +def test_deprecations(submodule): + """ + Test FutureWarning when trying to import the deprecated modules. + """ + with pytest.warns(FutureWarning, match=REGEX): + importlib.import_module(f"simpeg.directives.{submodule}") From 55e42521609a172a329155c45c4ef2d4d258b6a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dieter=20Werthm=C3=BCller?= Date: Fri, 16 May 2025 17:08:14 +0200 Subject: [PATCH 140/194] Ensure misfit is purely real valued (#1524) The misfit is always real valued, even for complex data, as it is the dot-product of the complex-conjugate value with the value itself. However, if the data is complex, `vdot(data, data)` will be a complex number with zero complex part (`x+0j`). This ensures it gives back a real-only value (`x`). --- simpeg/data_misfit.py | 5 ++++- tests/base/test_data_misfit.py | 26 ++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/simpeg/data_misfit.py b/simpeg/data_misfit.py index 7c0f1bbb34..740e8035fc 100644 --- a/simpeg/data_misfit.py +++ b/simpeg/data_misfit.py @@ -271,7 +271,10 @@ def __call__(self, m, f=None): """Evaluate the residual for a given model.""" R = self.W * self.residual(m, f=f) - return np.vdot(R, R) + # Imaginary part is always zero, even for complex data, as it takes the + # complex-conjugate dot-product. Ensure it returns a float + # (``np.vdot(R, R).real`` is the same as ``np.linalg.norm(R)**2``). + return np.vdot(R, R).real @timeIt def deriv(self, m, f=None): diff --git a/tests/base/test_data_misfit.py b/tests/base/test_data_misfit.py index 323673b5b6..76b0905e51 100644 --- a/tests/base/test_data_misfit.py +++ b/tests/base/test_data_misfit.py @@ -71,6 +71,32 @@ def test_DataMisfitOrder(self): self.data.noise_floor = self.noise_floor self.dmis.test(x=self.model, random_seed=17) + def test_real_valued(self): + # Change model + new_model = self.model + 1 + + # Misfit to data + misfit_original = self.dmis(new_model) + + # Test pseudo-complex, with 0 imaginary part; misfit must be the same + d_pseudo = Data(self.sim.survey, dobs=self.data.dobs + 0j * self.data.dobs) + d_pseudo.relative_error = self.relative + d_pseudo.noise_floor = self.noise_floor + dmis_pseudo = data_misfit.L2DataMisfit(simulation=self.sim, data=d_pseudo) + misfit_pseudo = dmis_pseudo(new_model) + # assert_array_equal with strict also checks dtype + np.testing.assert_array_equal(misfit_original, misfit_pseudo, strict=True) + + # Test actually complex; misfit must be different + data_imag = self.sim.make_synthetic_data(self.model, random_seed=17) + d_complex = Data(self.sim.survey, dobs=self.data.dobs + 1j * data_imag.dobs) + d_complex.relative_error = self.relative + d_complex.noise_floor = self.noise_floor + dmis_complex = data_misfit.L2DataMisfit(simulation=self.sim, data=d_complex) + misfit_complex = dmis_complex(new_model) + assert misfit_original != misfit_complex + assert misfit_complex.dtype == np.float64 + class MockSimulation(simulation.BaseSimulation): """ From 0a4cf76364523afba0b8e23d010e340dc6c9d129 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dieter=20Werthm=C3=BCller?= Date: Fri, 16 May 2025 17:10:39 +0200 Subject: [PATCH 141/194] Add key navigation to docs (#1668) Enable moving forward/backward in the docs with the right/left arrow keys on the keyboard. --- docs/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/conf.py b/docs/conf.py index bee3ff60a5..c91087f021 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -297,6 +297,7 @@ def linkcode_resolve(domain, info): "json_url": "https://docs.simpeg.xyz/latest/_static/versions.json", }, "show_version_warning_banner": True, + "navigation_with_keys": True, } html_logo = "images/simpeg-logo.png" From f503c0e2cb405c8eecbd72d1549f01ad40740420 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 23 May 2025 22:43:37 +0000 Subject: [PATCH 142/194] Add missing map classes to the API reference (#1672) Add missing classes in `maps.py` to the API Reference. --- simpeg/__init__.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/simpeg/__init__.py b/simpeg/__init__.py index c6cbe5e65d..baf84190d3 100644 --- a/simpeg/__init__.py +++ b/simpeg/__init__.py @@ -73,24 +73,32 @@ maps.ComboMap maps.ComplexMap maps.ExpMap - maps.LinearMap maps.IdentityMap maps.InjectActiveCells - maps.MuRelative - maps.LogMap + maps.LinearMap maps.LogisticSigmoidMap + maps.LogMap + maps.Mesh2Mesh + maps.MuRelative maps.ParametricBlock + maps.ParametricBlockInLayer + maps.ParametricCasingAndLayer maps.ParametricCircleMap maps.ParametricEllipsoid maps.ParametricLayer maps.ParametricPolyMap + maps.ParametricSplineMap + maps.PolynomialPetroClusterMap maps.Projection maps.ReciprocalMap + maps.SelfConsistentEffectiveMedium maps.SphericalSystem + maps.SumMap maps.Surject2Dto3D maps.SurjectFull maps.SurjectUnits maps.SurjectVertical1D + maps.TileMap maps.Weighting maps.Wires From 1e205f87b601f8d0012582297ecd205b966b3bd5 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 26 May 2025 15:47:17 +0000 Subject: [PATCH 143/194] Replace sklearn deprecated method for `validate_data` function (#1673) Replace the deprecated `BaseEstimator._validate_data` for the `validate_data` provided by `skelarn.utils.validation`. The method will be removed in sklearn 1.7. --- simpeg/utils/pgi_utils.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/simpeg/utils/pgi_utils.py b/simpeg/utils/pgi_utils.py index 1c5c9c8f8a..9fa0d02bef 100644 --- a/simpeg/utils/pgi_utils.py +++ b/simpeg/utils/pgi_utils.py @@ -30,6 +30,14 @@ except ImportError: GaussianMixture = None sklearn = False +else: + # Try to import `validate_data` (added in sklearn 1.6). + # We should remove these bits when we set sklearn>=1.6 as the minimum version, and + # just import `validate_data`. + try: + from sklearn.utils.validation import validate_data + except ImportError: + validate_data = None ############################################################################### @@ -541,7 +549,13 @@ def score_samples_with_sensW(self, X, sensW): Log probabilities of each data point in X. """ check_is_fitted(self) - X = self._validate_data(X, reset=False) + # TODO: Ditch self._validate_data when setting sklearn>=1.6 as the minimum + # required version. + X = ( + validate_data(self, X, reset=False) + if validate_data is not None + else self._validate_data(X, reset=False) + ) return logsumexp(self._estimate_weighted_log_prob_with_sensW(X, sensW), axis=1) @@ -1126,7 +1140,15 @@ def fit_predict(self, X, y=None, debug=False): if self.verbose: print("modified from scikit-learn") - X = self._validate_data(X, dtype=[np.float64, np.float32], ensure_min_samples=2) + # TODO: Ditch self._validate_data when setting sklearn>=1.6 as the minimum + # required version. + X = ( + validate_data(self, X, dtype=[np.float64, np.float32], ensure_min_samples=2) + if validate_data is not None + else self._validate_data( + self, X, dtype=[np.float64, np.float32], ensure_min_samples=2 + ) + ) if X.shape[0] < self.n_components: raise ValueError( "Expected n_samples >= n_components " From b8a15485ef54a37b80e1e140bc055de1767f5d07 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 26 May 2025 16:57:45 +0000 Subject: [PATCH 144/194] Remove `BaseSurvey.counter` property (#1640) Keep the `BaseSurvey._counter` attribute as private and remove the public `BaseSurvey.counter` property. --- simpeg/survey.py | 17 +---------------- tests/base/test_survey.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 16 deletions(-) create mode 100644 tests/base/test_survey.py diff --git a/simpeg/survey.py b/simpeg/survey.py index b06fbc2bdf..aa3c55b7e1 100644 --- a/simpeg/survey.py +++ b/simpeg/survey.py @@ -431,7 +431,7 @@ def __init__(self, source_list, counter=None, **kwargs): self.source_list = source_list if counter is not None: - self.counter = counter + self._counter = validate_type("counter", counter, Counter, cast=False) self._uid = uuid.uuid4() super().__init__(**kwargs) @@ -481,21 +481,6 @@ def uid(self): """ return self._uid - @property - def counter(self): - """A SimPEG counter object for counting iterations and operations - - Returns - ------- - simpeg.utils.counter_utils.Counter - A SimPEG counter object - """ - return self._counter - - @counter.setter - def counter(self, new_obj): - self._counter = validate_type("counter", new_obj, Counter, cast=False) - # TODO: this should be private def get_source_indices(self, sources): if not isinstance(sources, list): diff --git a/tests/base/test_survey.py b/tests/base/test_survey.py new file mode 100644 index 0000000000..83ddc908c2 --- /dev/null +++ b/tests/base/test_survey.py @@ -0,0 +1,34 @@ +""" +Tests for BaseSurvey class. +""" + +import pytest +import numpy as np + +from simpeg.utils import Counter +from simpeg.survey import BaseSurvey, BaseRx, BaseSrc + + +class TestCounterValidation: + + @pytest.fixture + def sample_source(self): + locations = np.array([1.0, 2.0, 3.0]) + receiver = BaseRx(locations=locations) + source = BaseSrc(receiver_list=[receiver]) + return source + + def test_valid_counter(self, sample_source): + """No error should be raise after passing a valid Counter object to Survey.""" + counter = Counter() + BaseSurvey(source_list=[sample_source], counter=counter) + + def test_invalid_counter(self, sample_source): + """Test error upon invalid Counter.""" + + class InvalidCounter: + pass + + invalid_counter = InvalidCounter() + with pytest.raises(TypeError): + BaseSurvey(source_list=[sample_source], counter=invalid_counter) From 3d6bb9dc38b1a6af4bd3c59179105135474cc857 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Wed, 28 May 2025 13:18:47 -0600 Subject: [PATCH 145/194] Fixes sign error in 1D field calculation. (#1662) #### Summary Corrects a sign error in the right-hand side (rhs) of the linear system for 1D electromagnetic field calculations. This ensures the accuracy of the computed electric fields. --- simpeg/electromagnetics/natural_source/utils/solutions_1d.py | 2 +- tests/em/nsem/inversion/test_Problem3D_Derivs.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/simpeg/electromagnetics/natural_source/utils/solutions_1d.py b/simpeg/electromagnetics/natural_source/utils/solutions_1d.py index 54513bab01..7c9b533e0f 100644 --- a/simpeg/electromagnetics/natural_source/utils/solutions_1d.py +++ b/simpeg/electromagnetics/natural_source/utils/solutions_1d.py @@ -34,7 +34,7 @@ def get1DEfields(m1d, sigma, freq, sourceAmp=1.0): ## Note: The analytic solution is derived with e^iwt bc = np.r_[Etot[0], Etot[-1]] # The right hand side - rhs = Aio * bc + rhs = -Aio * bc # Solve the system Aii_inv = Solver(Aii) eii = Aii_inv * rhs diff --git a/tests/em/nsem/inversion/test_Problem3D_Derivs.py b/tests/em/nsem/inversion/test_Problem3D_Derivs.py index 8540e9f3bc..41a99d5ce4 100644 --- a/tests/em/nsem/inversion/test_Problem3D_Derivs.py +++ b/tests/em/nsem/inversion/test_Problem3D_Derivs.py @@ -51,7 +51,7 @@ def test_Jtjdiag_clearing(model_simulation_tuple): def test_Jmatrix(model_simulation_tuple): model, simulation = model_simulation_tuple - rng = np.random.default_rng(4421) + rng = np.random.default_rng(4422) # create random vector vec = rng.standard_normal(simulation.survey.nD) From 61de6d26866bdd60a63c5dbbf35ae03928392881 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 2 Jun 2025 20:54:45 +0000 Subject: [PATCH 146/194] Allow use of `J` as `LinearOperator` in mag equivalent layers (#1676) Add Numba functions to compute `G.T @ v` and the diagonal of `G.T @ G` for the magnetic equivalent sources without storing the `G` matrix in memory. Add needed private methods in the `SimulationEquivalentSourceLayer`. Reorganize the private `_numba_functions.py` file into a `_numba` submodule with files for the functions related to the 3D and 2D meshes. Add tests for the new functions. --- simpeg/potential_fields/_numba_utils.py | 106 +- .../magnetics/_numba/_2d_mesh.py | 2279 +++++++++++++++++ .../_3d_mesh.py} | 1068 +------- .../magnetics/_numba/__init__.py | 14 + .../potential_fields/magnetics/simulation.py | 234 +- tests/pf/test_equivalent_sources.py | 174 +- 6 files changed, 2829 insertions(+), 1046 deletions(-) create mode 100644 simpeg/potential_fields/magnetics/_numba/_2d_mesh.py rename simpeg/potential_fields/magnetics/{_numba_functions.py => _numba/_3d_mesh.py} (70%) create mode 100644 simpeg/potential_fields/magnetics/_numba/__init__.py diff --git a/simpeg/potential_fields/_numba_utils.py b/simpeg/potential_fields/_numba_utils.py index 331cbb9e9d..58f216a1a8 100644 --- a/simpeg/potential_fields/_numba_utils.py +++ b/simpeg/potential_fields/_numba_utils.py @@ -110,7 +110,7 @@ def evaluate_kernels_on_cell( located on the observation point :math:`\mathbf{p}`. """ # Initialize result floats to zero - result_x, result_y, result_z = 0, 0, 0 + result_x, result_y, result_z = 0.0, 0.0, 0.0 # Iterate over the vertices of the prism for i in range(2): # Compute shifted easting coordinate @@ -149,3 +149,107 @@ def evaluate_kernels_on_cell( shift_east, shift_north, shift_upward, radius ) return result_x, result_y, result_z + + +@jit(nopython=True) +def evaluate_six_kernels_on_cell( + easting, + northing, + upward, + prism_west, + prism_east, + prism_south, + prism_north, + prism_bottom, + prism_top, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, +): + r""" + Evaluate six kernel functions on every shifted vertex of a prism. + + Similar to ``evaluate_kernels_on_cell``, but designed to evaluate six kernels + instead of three. This function comes useful for magnetic forwards, when six kernels + are needed to be evaluated. + + .. note:: + + This function was inspired in the ``_evaluate_kernel`` function in + Choclo (released under BSD 3-clause Licence): + https://www.fatiando.org/choclo + + Parameters + ---------- + easting, northing, upward : float + Easting, northing and upward coordinates of the observation point. Must + be in meters. + prism_west, prism_east : floats + The West and East boundaries of the prism. Must be in meters. + prism_south, prism_north : floats + The South and North boundaries of the prism. Must be in meters. + prism_bottom, prism_top : floats + The bottom and top boundaries of the prism. Must be in meters. + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz : callable + Kernel functions that will be evaluated on each one of the shifted + vertices of the prism. + + Returns + ------- + result_xx, result_yy, result_zz, result_xy, result_xz, result_yz : float + Evaluation of the kernel functions on each one of the vertices of the + prism. + """ + # Initialize result floats to zero + result_xx, result_yy, result_zz = 0.0, 0.0, 0.0 + result_xy, result_xz, result_yz = 0.0, 0.0, 0.0 + # Iterate over the vertices of the prism + for i in range(2): + # Compute shifted easting coordinate + if i == 0: + shift_east = prism_east - easting + else: + shift_east = prism_west - easting + shift_east_sq = shift_east**2 + for j in range(2): + # Compute shifted northing coordinate + if j == 0: + shift_north = prism_north - northing + else: + shift_north = prism_south - northing + shift_north_sq = shift_north**2 + for k in range(2): + # Compute shifted upward coordinate + if k == 0: + shift_upward = prism_top - upward + else: + shift_upward = prism_bottom - upward + shift_upward_sq = shift_upward**2 + # Compute the radius + radius = np.sqrt(shift_east_sq + shift_north_sq + shift_upward_sq) + # If i, j or k is 1, the corresponding shifted + # coordinate will refer to the lower boundary, + # meaning the corresponding term should have a minus + # sign. + result_xx += (-1) ** (i + j + k) * kernel_xx( + shift_east, shift_north, shift_upward, radius + ) + result_yy += (-1) ** (i + j + k) * kernel_yy( + shift_east, shift_north, shift_upward, radius + ) + result_zz += (-1) ** (i + j + k) * kernel_zz( + shift_east, shift_north, shift_upward, radius + ) + result_xy += (-1) ** (i + j + k) * kernel_xy( + shift_east, shift_north, shift_upward, radius + ) + result_xz += (-1) ** (i + j + k) * kernel_xz( + shift_east, shift_north, shift_upward, radius + ) + result_yz += (-1) ** (i + j + k) * kernel_yz( + shift_east, shift_north, shift_upward, radius + ) + return result_xx, result_yy, result_zz, result_xy, result_xz, result_yz diff --git a/simpeg/potential_fields/magnetics/_numba/_2d_mesh.py b/simpeg/potential_fields/magnetics/_numba/_2d_mesh.py new file mode 100644 index 0000000000..cb75bc8285 --- /dev/null +++ b/simpeg/potential_fields/magnetics/_numba/_2d_mesh.py @@ -0,0 +1,2279 @@ +""" +Numba functions for magnetic simulation of rectangular prisms on 2D meshes. + +These functions assumes 3D prisms formed by a 2D mesh plus top and bottom boundaries for +each prism. +""" + +import numpy as np + +try: + import choclo +except ImportError: + # Define dummy jit decorator + def jit(*args, **kwargs): + return lambda f: f + + choclo = None +else: + from numba import jit, prange + +from ..._numba_utils import evaluate_kernels_on_cell, evaluate_six_kernels_on_cell + + +def _forward_mag( + receivers, + cells_bounds, + top, + bottom, + model, + fields, + regional_field, + forward_func, + scalar_model, +): + """ + Forward model single magnetic component for 2D meshes. + + This function is designed to be used with equivalent sources, where the + mesh is a 2D mesh (prism layer). The top and bottom boundaries of each cell + are passed through the ``top`` and ``bottom`` arrays. + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_forward = jit(nopython=True, parallel=True)(_forward_mag) + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + model : (n_active_cells) or (3 * n_active_cells) array + Array containing the susceptibilities (scalar) or effective + susceptibilities (vector) of the active cells in the mesh, in SI + units. + Susceptibilities are expected if ``scalar_model`` is True, + and the array should have ``n_active_cells`` elements. + Effective susceptibilities are expected if ``scalar_model`` is False, + and the array should have ``3 * n_active_cells`` elements. + fields : (n_receivers) array + Array full of zeros where the magnetic component on each receiver will + be stored. This could be a preallocated array or a slice of it. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + forward_func : callable + Forward function that will be evaluated on each node of the mesh. Choose + one of the forward functions in ``choclo.prism``. + scalar_model : bool + If True, the forward will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the forward will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + + Notes + ----- + + About the model array + ^^^^^^^^^^^^^^^^^^^^^ + + The ``model`` must always be a 1d array: + + * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with + the same number of elements as active cells in the mesh. It should store + the magnetic susceptibilities of each active cell in SI units. + * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array + with a number of elements equal to three times the active cells in the + mesh. It should store the components of the magnetization vector of each + active cell in :math:`Am^{-1}`. The order in which the components should + be passed are: + * every _easting_ component of each active cell, + * then every _northing_ component of each active cell, + * and finally every _upward_ component of each active cell. + + """ + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Forward model the magnetic component of each cell on each receiver location + for i in prange(n_receivers): + for j in range(n_cells): + # Define magnetization vector of the cell + # (we we'll divide by mu_0 when adding the forward modelled field) + if scalar_model: + # model is susceptibility, so the vector is parallel to the + # regional field + magnetization_x = model[j] * fx + magnetization_y = model[j] * fy + magnetization_z = model[j] * fz + else: + # model is effective susceptibility (vector) + magnetization_x = model[j] + magnetization_y = model[j + n_cells] + magnetization_z = model[j + 2 * n_cells] + # Forward the magnetic component + fields[i] += ( + regional_field_amplitude + * forward_func( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + magnetization_x, + magnetization_y, + magnetization_z, + ) + / choclo.constants.VACUUM_MAGNETIC_PERMEABILITY + ) + + +def _forward_tmi( + receivers, + cells_bounds, + top, + bottom, + model, + fields, + regional_field, + scalar_model, +): + """ + Forward model the TMI for 2D meshes. + + This function is designed to be used with equivalent sources, where the + mesh is a 2D mesh (prism layer). The top and bottom boundaries of each cell + are passed through the ``top`` and ``bottom`` arrays. + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_forward = jit(nopython=True, parallel=True)(_forward_tmi) + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + model : (n_active_cells) or (3 * n_active_cells) + Array with the susceptibility (scalar model) or the effective + susceptibility (vector model) of each active cell in the mesh. + If the model is scalar, the ``model`` array should have + ``n_active_cells`` elements and ``scalar_model`` should be True. + If the model is vector, the ``model`` array should have + ``3 * n_active_cells`` elements and ``scalar_model`` should be False. + fields : (n_receivers) array + Array full of zeros where the TMI on each receiver will be stored. This + could be a preallocated array or a slice of it. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + scalar_model : bool + If True, the forward will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the forward will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + + Notes + ----- + + The ``model`` must always be a 1d array: + + * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with + the same number of elements as active cells in the mesh. It should store + the magnetic susceptibilities of each active cell in SI units. + * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array + with a number of elements equal to three times the active cells in the + mesh. It should store the components of the magnetization vector of each + active cell in :math:`Am^{-1}`. The order in which the components should + be passed are: + * every _easting_ component of each active cell, + * then every _northing_ component of each active cell, + * and finally every _upward_ component of each active cell. + + """ + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Forward model the magnetic component of each cell on each receiver location + for i in prange(n_receivers): + for j in range(n_cells): + # Define magnetization vector of the cell + # (we we'll divide by mu_0 when adding the forward modelled field) + if scalar_model: + # model is susceptibility, so the vector is parallel to the + # regional field + magnetization_x = model[j] * fx + magnetization_y = model[j] * fy + magnetization_z = model[j] * fz + else: + # model is effective susceptibility (vector) + magnetization_x = model[j] + magnetization_y = model[j + n_cells] + magnetization_z = model[j + 2 * n_cells] + # Forward the magnetic field vector and compute tmi + bx, by, bz = choclo.prism.magnetic_field( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + magnetization_x, + magnetization_y, + magnetization_z, + ) + fields[i] += ( + regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + / choclo.constants.VACUUM_MAGNETIC_PERMEABILITY + ) + + +def _forward_tmi_derivative( + receivers, + cells_bounds, + top, + bottom, + model, + fields, + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + scalar_model, +): + r""" + Forward model a TMI derivative for 2D meshes. + + This function is designed to be used with equivalent sources, where the + mesh is a 2D mesh (prism layer). The top and bottom boundaries of each cell + are passed through the ``top`` and ``bottom`` arrays. + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_forward = jit(nopython=True, parallel=True)(_forward_tmi_derivative) + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + model : (n_active_cells) or (3 * n_active_cells) + Array with the susceptibility (scalar model) or the effective + susceptibility (vector model) of each active cell in the mesh. + If the model is scalar, the ``model`` array should have + ``n_active_cells`` elements and ``scalar_model`` should be True. + If the model is vector, the ``model`` array should have + ``3 * n_active_cells`` elements and ``scalar_model`` should be False. + fields : (n_receivers) array + Array full of zeros where the TMI on each receiver will be stored. This + could be a preallocated array or a slice of it. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz : callables + Kernel functions used for computing the desired TMI derivative. + scalar_model : bool + If True, the forward will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the forward will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + + Notes + ----- + + About the kernel functions + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + + To compute the :math:`\alpha` derivative of the TMI :math:`\Delta T` (with + :math:`\alpha \in \{x, y, z\}` we need to evaluate third order kernels + functions for the prism. The kernels we need to evaluate can be obtained by + fixing one of the subindices to the direction of the derivative + (:math:`\alpha`) and cycle through combinations of the other two. + + For ``tmi_x`` we need to pass: + + .. code:: + + kernel_xx=kernel_eee, kernel_yy=kernel_enn, kernel_zz=kernel_euu, + kernel_xy=kernel_een, kernel_xz=kernel_eeu, kernel_yz=kernel_enu + + For ``tmi_y`` we need to pass: + + .. code:: + + kernel_xx=kernel_een, kernel_yy=kernel_nnn, kernel_zz=kernel_nuu, + kernel_xy=kernel_enn, kernel_xz=kernel_enu, kernel_yz=kernel_nnu + + For ``tmi_z`` we need to pass: + + .. code:: + + kernel_xx=kernel_eeu, kernel_yy=kernel_nnu, kernel_zz=kernel_uuu, + kernel_xy=kernel_enu, kernel_xz=kernel_euu, kernel_yz=kernel_nuu + + + About the model array + ^^^^^^^^^^^^^^^^^^^^^ + + The ``model`` must always be a 1d array: + + * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with + the same number of elements as active cells in the mesh. It should store + the magnetic susceptibilities of each active cell in SI units. + * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array + with a number of elements equal to three times the active cells in the + mesh. It should store the components of the magnetization vector of each + active cell in :math:`Am^{-1}`. The order in which the components should + be passed are: + * every _easting_ component of each active cell, + * then every _northing_ component of each active cell, + * and finally every _upward_ component of each active cell. + + """ + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Forward model the magnetic component of each cell on each receiver location + for i in prange(n_receivers): + for j in range(n_cells): + uxx, uyy, uzz, uxy, uxz, uyz = evaluate_six_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + ) + if scalar_model: + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + fields[i] += ( + model[j] + * regional_field_amplitude + * (fx * bx + fy * by + fz * bz) + / (4 * np.pi) + ) + else: + model_x = model[j] + model_y = model[j + n_cells] + model_z = model[j + 2 * n_cells] + bx = uxx * model_x + uxy * model_y + uxz * model_z + by = uxy * model_x + uyy * model_y + uyz * model_z + bz = uxz * model_x + uyz * model_y + uzz * model_z + fields[i] += ( + regional_field_amplitude * (bx * fx + by * fy + bz * fz) / 4 / np.pi + ) + + +def _sensitivity_mag( + receivers, + cells_bounds, + top, + bottom, + sensitivity_matrix, + regional_field, + kernel_x, + kernel_y, + kernel_z, + scalar_model, +): + r""" + Fill the sensitivity matrix for single mag component for 2d meshes. + + This function is designed to be used with equivalent sources, where the + mesh is a 2D mesh (prism layer). The top and bottom boundaries of each cell + are passed through the ``top`` and ``bottom`` arrays. + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_sensitivity = jit(nopython=True, parallel=True)(_sensitivity_mag) + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + sensitivity_matrix : array + Empty 2d array where the sensitivity matrix elements will be filled. + This could be a preallocated empty array or a slice of it. + The array should have a shape of ``(n_receivers, n_active_cells)`` + if ``scalar_model`` is True. + The array should have a shape of ``(n_receivers, 3 * n_active_cells)`` + if ``scalar_model`` is False. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_x, kernel_y, kernel_z : callable + Kernels used to compute the desired magnetic component. For example, + for computing bx we need to use ``kernel_x=kernel_ee``, + ``kernel_y=kernel_en``, ``kernel_z=kernel_eu``. + scalar_model : bool + If True, the sensitivity matrix is built to work with scalar models + (susceptibilities). + If False, the sensitivity matrix is built to work with vector models + (effective susceptibilities). + + Notes + ----- + + About the kernel functions + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + + For computing the ``bx`` component of the magnetic field we need to use the + following kernels: + + .. code:: + + kernel_x=kernel_ee, kernel_y=kernel_en, kernel_z=kernel_eu + + + For computing the ``by`` component of the magnetic field we need to use the + following kernels: + + .. code:: + + kernel_x=kernel_en, kernel_y=kernel_nn, kernel_z=kernel_nu + + For computing the ``bz`` component of the magnetic field we need to use the + following kernels: + + .. code:: + + kernel_x=kernel_eu, kernel_y=kernel_nu, kernel_z=kernel_uu + + + About the model array + ^^^^^^^^^^^^^^^^^^^^^ + + The ``model`` must always be a 1d array: + + * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with + the same number of elements as active cells in the mesh. It should store + the magnetic susceptibilities of each active cell in SI units. + * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array + with a number of elements equal to three times the active cells in the + mesh. It should store the components of the magnetization vector of each + active cell in :math:`Am^{-1}`. The order in which the components should + be passed are: + * every _easting_ component of each active cell, + * then every _northing_ component of each active cell, + * and finally every _upward_ component of each active cell. + + About the sensitivity matrix + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + Each row of the sensitivity matrix corresponds to a single receiver + location. + + If ``scalar_model`` is True, then each element of the row will + correspond to the partial derivative of the selected magnetic component + with respect to the susceptibility of each cell in the mesh. + + If ``scalar_model`` is False, then each row can be split in three sections + containing: + + * the partial derivatives of the selected magnetic component with respect + to the _x_ component of the effective susceptibility of each cell; then + * the partial derivatives of the selected magnetic component with respect + to the _y_ component of the effective susceptibility of each cell; and then + * the partial derivatives of the selected magnetic component with respect + to the _z_ component of the effective susceptibility of each cell. + + So, if we call :math:`B_j` the magnetic field component on the receiver + :math:`j`, and :math:`\bar{\chi}^{(i)} = (\chi_x^{(i)}, \chi_y^{(i)}, + \chi_z^{(i)})` the effective susceptibility of the active cell :math:`i`, + then each row of the sensitivity matrix will be: + + .. math:: + + \left[ + \frac{\partial B_j}{\partial \chi_x^{(1)}}, + \dots, + \frac{\partial B_j}{\partial \chi_x^{(N)}}, + \frac{\partial B_j}{\partial \chi_y^{(1)}}, + \dots, + \frac{\partial B_j}{\partial \chi_y^{(N)}}, + \frac{\partial B_j}{\partial \chi_z^{(1)}}, + \dots, + \frac{\partial B_j}{\partial \chi_z^{(N)}} + \right] + + where :math:`N` is the total number of active cells. + """ + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + + constant_factor = 1 / 4 / np.pi + + # Fill the sensitivity matrix + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + for i in prange(n_receivers): + for j in range(n_cells): + # Evaluate kernels for the current cell and receiver + ux, uy, uz = evaluate_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + kernel_x, + kernel_y, + kernel_z, + ) + if scalar_model: + sensitivity_matrix[i, j] = ( + constant_factor + * regional_field_amplitude + * (ux * fx + uy * fy + uz * fz) + ) + else: + sensitivity_matrix[i, j] = ( + constant_factor * regional_field_amplitude * ux + ) + sensitivity_matrix[i, j + n_cells] = ( + constant_factor * regional_field_amplitude * uy + ) + sensitivity_matrix[i, j + 2 * n_cells] = ( + constant_factor * regional_field_amplitude * uz + ) + + +def _sensitivity_tmi( + receivers, + cells_bounds, + top, + bottom, + sensitivity_matrix, + regional_field, + scalar_model, +): + r""" + Fill the sensitivity matrix TMI for 2d meshes. + + This function is designed to be used with equivalent sources, where the + mesh is a 2D mesh (prism layer). The top and bottom boundaries of each cell + are passed through the ``top`` and ``bottom`` arrays. + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_tmi = jit(nopython=True, parallel=True)(_sensitivity_tmi) + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + sensitivity_matrix : array + Empty 2d array where the sensitivity matrix elements will be filled. + This could be a preallocated empty array or a slice of it. + The array should have a shape of ``(n_receivers, n_active_cells)`` + if ``scalar_model`` is True. + The array should have a shape of ``(n_receivers, 3 * n_active_cells)`` + if ``scalar_model`` is False. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + scalar_model : bool + If True, the sensitivity matrix is build to work with scalar models + (susceptibilities). + If False, the sensitivity matrix is build to work with vector models + (effective susceptibilities). + + Notes + ----- + + About the model array + ^^^^^^^^^^^^^^^^^^^^^ + + The ``model`` must always be a 1d array: + + * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with + the same number of elements as active cells in the mesh. It should store + the magnetic susceptibilities of each active cell in SI units. + * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array + with a number of elements equal to three times the active cells in the + mesh. It should store the components of the magnetization vector of each + active cell in :math:`Am^{-1}`. The order in which the components should + be passed are: + * every _easting_ component of each active cell, + * then every _northing_ component of each active cell, + * and finally every _upward_ component of each active cell. + + About the sensitivity matrix + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + Each row of the sensitivity matrix corresponds to a single receiver + location. + + If ``scalar_model`` is True, then each element of the row will + correspond to the partial derivative of the tmi + with respect to the susceptibility of each cell in the mesh. + + If ``scalar_model`` is False, then each row can be split in three sections + containing: + + * the partial derivatives of the tmi with respect + to the _x_ component of the effective susceptibility of each cell; then + * the partial derivatives of the tmi with respect + to the _y_ component of the effective susceptibility of each cell; and then + * the partial derivatives of the tmi with respect + to the _z_ component of the effective susceptibility of each cell. + + So, if we call :math:`T_j` the tmi on the receiver + :math:`j`, and :math:`\bar{\chi}^{(i)} = (\chi_x^{(i)}, \chi_y^{(i)}, + \chi_z^{(i)})` the effective susceptibility of the active cell :math:`i`, + then each row of the sensitivity matrix will be: + + .. math:: + + \left[ + \frac{\partial T_j}{\partial \chi_x^{(1)}}, + \dots, + \frac{\partial T_j}{\partial \chi_x^{(N)}}, + \frac{\partial T_j}{\partial \chi_y^{(1)}}, + \dots, + \frac{\partial T_j}{\partial \chi_y^{(N)}}, + \frac{\partial T_j}{\partial \chi_z^{(1)}}, + \dots, + \frac{\partial T_j}{\partial \chi_z^{(N)}} + \right] + + where :math:`N` is the total number of active cells. + """ + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + + constant_factor = 1 / 4 / np.pi + + # Fill the sensitivity matrix + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + for i in prange(n_receivers): + for j in range(n_cells): + # Evaluate kernels for the current cell and receiver + uxx, uyy, uzz, uxy, uxz, uyz = evaluate_six_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + choclo.prism.kernel_ee, + choclo.prism.kernel_nn, + choclo.prism.kernel_uu, + choclo.prism.kernel_en, + choclo.prism.kernel_eu, + choclo.prism.kernel_nu, + ) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + if scalar_model: + sensitivity_matrix[i, j] = ( + constant_factor + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + else: + sensitivity_matrix[i, j] = ( + constant_factor * regional_field_amplitude * bx + ) + sensitivity_matrix[i, j + n_cells] = ( + constant_factor * regional_field_amplitude * by + ) + sensitivity_matrix[i, j + 2 * n_cells] = ( + constant_factor * regional_field_amplitude * bz + ) + + +def _sensitivity_tmi_derivative( + receivers, + cells_bounds, + top, + bottom, + sensitivity_matrix, + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + scalar_model, +): + r""" + Fill the sensitivity matrix TMI for 2d meshes. + + This function is designed to be used with equivalent sources, where the + mesh is a 2D mesh (prism layer). The top and bottom boundaries of each cell + are passed through the ``top`` and ``bottom`` arrays. + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_tmi = jit(nopython=True, parallel=True)(_sensitivity_tmi_derivative) + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + sensitivity_matrix : array + Empty 2d array where the sensitivity matrix elements will be filled. + This could be a preallocated empty array or a slice of it. + The array should have a shape of ``(n_receivers, n_active_cells)`` + if ``scalar_model`` is True. + The array should have a shape of ``(n_receivers, 3 * n_active_cells)`` + if ``scalar_model`` is False. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz : callables + Kernel functions used for computing the desired TMI derivative. + scalar_model : bool + If True, the sensitivity matrix is build to work with scalar models + (susceptibilities). + If False, the sensitivity matrix is build to work with vector models + (effective susceptibilities). + + Notes + ----- + + About the model array + ^^^^^^^^^^^^^^^^^^^^^ + + The ``model`` must always be a 1d array: + + * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with + the same number of elements as active cells in the mesh. It should store + the magnetic susceptibilities of each active cell in SI units. + * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array + with a number of elements equal to three times the active cells in the + mesh. It should store the components of the magnetization vector of each + active cell in :math:`Am^{-1}`. The order in which the components should + be passed are: + * every _easting_ component of each active cell, + * then every _northing_ component of each active cell, + * and finally every _upward_ component of each active cell. + """ + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + + constant_factor = 1 / 4 / np.pi + + # Fill the sensitivity matrix + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + for i in prange(n_receivers): + for j in range(n_cells): + # Evaluate kernels for the current cell and receiver + uxx, uyy, uzz, uxy, uxz, uyz = evaluate_six_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + ) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + if scalar_model: + sensitivity_matrix[i, j] = ( + constant_factor + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + else: + sensitivity_matrix[i, j] = ( + constant_factor * regional_field_amplitude * bx + ) + sensitivity_matrix[i, j + n_cells] = ( + constant_factor * regional_field_amplitude * by + ) + sensitivity_matrix[i, j + 2 * n_cells] = ( + constant_factor * regional_field_amplitude * bz + ) + + +@jit(nopython=True, parallel=False) +def _tmi_sensitivity_t_dot_v_serial( + receivers, + cells_bounds, + top, + bottom, + regional_field, + scalar_model, + vector, + result, +): + r""" + Compute ``G.T @ v`` for TMI on 2d meshes, in serial. + + This function doesn't allocates the ``G`` matrix in memory. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + scalar_model : bool + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + vector : (n_receivers) numpy.ndarray + Array that represents the vector used in the dot product. + result : (n_active_cells) or (3 * n_active_cells) numpy.ndarray + Running result array where the output of the dot product will be added to. + The array should have ``n_active_cells`` elements if ``scalar_model`` + is True, or ``3 * n_active_cells`` otherwise. + + Notes + ----- + This function is meant to be run in serial. Writing to the ``result`` array + inside a parallel loop over the receivers generates a race condition that + leads to corrupted outputs. + + A parallel implementation of this function is available in + ``_tmi_sensitivity_t_dot_v_parallel``. + """ + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + + constant_factor = 1 / 4 / np.pi + + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + # Evaluate kernel function on each node, for each receiver location + for i in range(n_receivers): + for j in range(n_cells): + # Evaluate kernels for the current cell and receiver + uxx, uyy, uzz, uxy, uxz, uyz = evaluate_six_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + choclo.prism.kernel_ee, + choclo.prism.kernel_nn, + choclo.prism.kernel_uu, + choclo.prism.kernel_en, + choclo.prism.kernel_eu, + choclo.prism.kernel_nu, + ) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + if scalar_model: + result[j] += ( + constant_factor + * vector[i] + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + else: + result[j] += constant_factor * vector[i] * regional_field_amplitude * bx + result[j + n_cells] += ( + constant_factor * vector[i] * regional_field_amplitude * by + ) + result[j + 2 * n_cells] += ( + constant_factor * vector[i] * regional_field_amplitude * bz + ) + + +@jit(nopython=True, parallel=True) +def _tmi_sensitivity_t_dot_v_parallel( + receivers, + cells_bounds, + top, + bottom, + regional_field, + scalar_model, + vector, + result, +): + r""" + Compute ``G.T @ v`` for TMI on 2d meshes, in parallel. + + This function doesn't allocates the ``G`` matrix in memory. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + scalar_model : bool + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + vector : (n_receivers) numpy.ndarray + Array that represents the vector used in the dot product. + result : (n_active_cells) or (3 * n_active_cells) numpy.ndarray + Running result array where the output of the dot product will be added to. + The array should have ``n_active_cells`` elements if ``scalar_model`` + is True, or ``3 * n_active_cells`` otherwise. + + Notes + ----- + This function is meant to be run in parallel. + This implementation instructs each thread to allocate their own array for + the current row of the sensitivity matrix. After computing the elements of + that row, it gets added to the running ``result`` array through a reduction + operation handled by Numba. + + A parallel implementation of this function is available in + ``_tmi_sensitivity_t_dot_v_serial``. + """ + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + + constant_factor = 1 / 4 / np.pi + + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + result_size = result.size + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate array for the current row of the sensitivity matrix + local_row = np.empty(result_size) + for j in range(n_cells): + # Evaluate kernels for the current cell and receiver + uxx, uyy, uzz, uxy, uxz, uyz = evaluate_six_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + choclo.prism.kernel_ee, + choclo.prism.kernel_nn, + choclo.prism.kernel_uu, + choclo.prism.kernel_en, + choclo.prism.kernel_eu, + choclo.prism.kernel_nu, + ) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + if scalar_model: + local_row[j] = ( + constant_factor + * vector[i] + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + else: + local_row[j] = ( + constant_factor * vector[i] * regional_field_amplitude * bx + ) + local_row[j + n_cells] = ( + constant_factor * vector[i] * regional_field_amplitude * by + ) + local_row[j + 2 * n_cells] = ( + constant_factor * vector[i] * regional_field_amplitude * bz + ) + # Apply reduction operation to add the values of the row to the running + # result. Avoid slicing the `result` array when updating it to avoid + # racing conditions, just add the `local_row` to the `results` + # variable. + result += local_row + + +@jit(nopython=True, parallel=False) +def _mag_sensitivity_t_dot_v_serial( + receivers, + cells_bounds, + top, + bottom, + regional_field, + kernel_x, + kernel_y, + kernel_z, + scalar_model, + vector, + result, +): + r""" + Compute ``G.T @ v`` for a single magnetic component on 2d meshes, in serial. + + This function doesn't allocates the ``G`` matrix in memory. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_x, kernel_y, kernel_z : callable + Kernels used to compute the desired magnetic component. For example, + for computing bx we need to use ``kernel_x=kernel_ee``, + ``kernel_y=kernel_en``, ``kernel_z=kernel_eu``. + scalar_model : bool + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + vector : (n_receivers) numpy.ndarray + Array that represents the vector used in the dot product. + result : (n_active_cells) or (3 * n_active_cells) numpy.ndarray + Running result array where the output of the dot product will be added to. + The array should have ``n_active_cells`` elements if ``scalar_model`` + is True, or ``3 * n_active_cells`` otherwise. + + Notes + ----- + This function is meant to be run in serial. Writing to the ``result`` array + inside a parallel loop over the receivers generates a race condition that + leads to corrupted outputs. + + A parallel implementation of this function is available in + ``_mag_sensitivity_t_dot_v_parallel``. + """ + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + + constant_factor = 1 / 4 / np.pi + + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + # Evaluate kernel function on each node, for each receiver location + for i in range(n_receivers): + for j in range(n_cells): + # Evaluate kernels for the current cell and receiver + ux, uy, uz = evaluate_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + kernel_x, + kernel_y, + kernel_z, + ) + if scalar_model: + result[j] += ( + constant_factor + * vector[i] + * regional_field_amplitude + * (ux * fx + uy * fy + uz * fz) + ) + else: + result[j] += constant_factor * vector[i] * regional_field_amplitude * ux + result[j + n_cells] += ( + constant_factor * vector[i] * regional_field_amplitude * uy + ) + result[j + 2 * n_cells] += ( + constant_factor * vector[i] * regional_field_amplitude * uz + ) + + +@jit(nopython=True, parallel=True) +def _mag_sensitivity_t_dot_v_parallel( + receivers, + cells_bounds, + top, + bottom, + regional_field, + kernel_x, + kernel_y, + kernel_z, + scalar_model, + vector, + result, +): + r""" + Compute ``G.T @ v`` for a single magnetic component on 2d meshes, in parallel. + + This function doesn't allocates the ``G`` matrix in memory. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_x, kernel_y, kernel_z : callable + Kernels used to compute the desired magnetic component. For example, + for computing bx we need to use ``kernel_x=kernel_ee``, + ``kernel_y=kernel_en``, ``kernel_z=kernel_eu``. + scalar_model : bool + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + vector : (n_receivers) numpy.ndarray + Array that represents the vector used in the dot product. + result : (n_active_cells) or (3 * n_active_cells) numpy.ndarray + Running result array where the output of the dot product will be added to. + The array should have ``n_active_cells`` elements if ``scalar_model`` + is True, or ``3 * n_active_cells`` otherwise. + + Notes + ----- + This function is meant to be run in parallel. + This implementation instructs each thread to allocate their own array for + the current row of the sensitivity matrix. After computing the elements of + that row, it gets added to the running ``result`` array through a reduction + operation handled by Numba. + + A parallel implementation of this function is available in + ``_mag_sensitivity_t_dot_v_parallel``. + """ + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + + constant_factor = 1 / 4 / np.pi + + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + result_size = result.size + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate array for the current row of the sensitivity matrix + local_row = np.empty(result_size) + for j in range(n_cells): + # Evaluate kernels for the current cell and receiver + ux, uy, uz = evaluate_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + kernel_x, + kernel_y, + kernel_z, + ) + if scalar_model: + local_row[j] = ( + constant_factor + * vector[i] + * regional_field_amplitude + * (ux * fx + uy * fy + uz * fz) + ) + else: + local_row[j] = ( + constant_factor * vector[i] * regional_field_amplitude * ux + ) + local_row[j + n_cells] = ( + constant_factor * vector[i] * regional_field_amplitude * uy + ) + local_row[j + 2 * n_cells] = ( + constant_factor * vector[i] * regional_field_amplitude * uz + ) + # Apply reduction operation to add the values of the row to the running + # result. Avoid slicing the `result` array when updating it to avoid + # racing conditions, just add the `local_row` to the `results` + # variable. + result += local_row + + +@jit(nopython=True, parallel=False) +def _tmi_derivative_sensitivity_t_dot_v_serial( + receivers, + cells_bounds, + top, + bottom, + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + scalar_model, + vector, + result, +): + r""" + Compute ``G.T @ v`` for a TMI derivative on 2d meshes, in serial. + + This function doesn't allocates the ``G`` matrix in memory. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz : callables + Kernel functions used for computing the desired TMI derivative. + scalar_model : bool + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + vector : (n_receivers) numpy.ndarray + Array that represents the vector used in the dot product. + result : (n_active_cells) or (3 * n_active_cells) numpy.ndarray + Running result array where the output of the dot product will be added to. + The array should have ``n_active_cells`` elements if ``scalar_model`` + is True, or ``3 * n_active_cells`` otherwise. + + Notes + ----- + This function is meant to be run in serial. Writing to the ``result`` array + inside a parallel loop over the receivers generates a race condition that + leads to corrupted outputs. + + A parallel implementation of this function is available in + ``_tmi_derivative_sensitivity_t_dot_v_parallel``. + """ + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + + constant_factor = 1 / 4 / np.pi + + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + # Evaluate kernel function on each node, for each receiver location + for i in range(n_receivers): + for j in range(n_cells): + # Evaluate kernels for the current cell and receiver + uxx, uyy, uzz, uxy, uxz, uyz = evaluate_six_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + ) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + if scalar_model: + result[j] += ( + constant_factor + * vector[i] + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + else: + result[j] += constant_factor * vector[i] * regional_field_amplitude * bx + result[j + n_cells] += ( + constant_factor * vector[i] * regional_field_amplitude * by + ) + result[j + 2 * n_cells] += ( + constant_factor * vector[i] * regional_field_amplitude * bz + ) + + +@jit(nopython=True, parallel=True) +def _tmi_derivative_sensitivity_t_dot_v_parallel( + receivers, + cells_bounds, + top, + bottom, + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + scalar_model, + vector, + result, +): + r""" + Compute ``G.T @ v`` for a TMI derivative on 2d meshes, in parallel. + + This function doesn't allocates the ``G`` matrix in memory. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz : callables + Kernel functions used for computing the desired TMI derivative. + scalar_model : bool + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + vector : (n_receivers) numpy.ndarray + Array that represents the vector used in the dot product. + result : (n_active_cells) or (3 * n_active_cells) numpy.ndarray + Running result array where the output of the dot product will be added to. + The array should have ``n_active_cells`` elements if ``scalar_model`` + is True, or ``3 * n_active_cells`` otherwise. + + Notes + ----- + This function is meant to be run in parallel. + This implementation instructs each thread to allocate their own array for + the current row of the sensitivity matrix. After computing the elements of + that row, it gets added to the running ``result`` array through a reduction + operation handled by Numba. + + A parallel implementation of this function is available in + ``_tmi_derivative_sensitivity_t_dot_v_parallel``. + """ + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + + constant_factor = 1 / 4 / np.pi + + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + result_size = result.size + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate array for the current row of the sensitivity matrix + local_row = np.empty(result_size) + for j in range(n_cells): + # Evaluate kernels for the current cell and receiver + uxx, uyy, uzz, uxy, uxz, uyz = evaluate_six_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + ) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + if scalar_model: + local_row[j] = ( + constant_factor + * vector[i] + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + else: + local_row[j] = ( + constant_factor * vector[i] * regional_field_amplitude * bx + ) + local_row[j + n_cells] = ( + constant_factor * vector[i] * regional_field_amplitude * by + ) + local_row[j + 2 * n_cells] = ( + constant_factor * vector[i] * regional_field_amplitude * bz + ) + # Apply reduction operation to add the values of the row to the running + # result. Avoid slicing the `result` array when updating it to avoid + # racing conditions, just add the `local_row` to the `results` + # variable. + result += local_row + + +@jit(nopython=True, parallel=False) +def _diagonal_G_T_dot_G_tmi_serial( + receivers, + cells_bounds, + top, + bottom, + regional_field, + constant_factor, + scalar_model, + weights, + diagonal, +): + """ + Diagonal of ``G.T @ W.T @ W @ G`` for TMI without storing ``G``, in serial. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + weights : (n_receivers,) numpy.ndarray + Array with data weights. It should be the diagonal of the ``W`` matrix, + squared. + diagonal : (n_active_cells,) numpy.ndarray + Array where the diagonal of ``G.T @ G`` will be added to. + + Notes + ----- + This function is meant to be run in serial. Use the + ``_diagonal_G_T_dot_G_tmi_parallel`` one for parallelized computations. + """ + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + + constant_factor = 1 / 4 / np.pi + + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + # Evaluate kernel function on each node, for each receiver location + for i in range(n_receivers): + for j in range(n_cells): + # Evaluate kernels for the current cell and receiver + uxx, uyy, uzz, uxy, uxz, uyz = evaluate_six_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + choclo.prism.kernel_ee, + choclo.prism.kernel_nn, + choclo.prism.kernel_uu, + choclo.prism.kernel_en, + choclo.prism.kernel_eu, + choclo.prism.kernel_nu, + ) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + + if scalar_model: + g_element = ( + constant_factor + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + diagonal[j] += weights[i] * g_element**2 + else: + const = constant_factor * regional_field_amplitude + diagonal[j] += weights[i] * (const * bx) ** 2 + diagonal[j + n_cells] += weights[i] * (const * by) ** 2 + diagonal[j + 2 * n_cells] += weights[i] * (const * bz) ** 2 + + +@jit(nopython=True, parallel=True) +def _diagonal_G_T_dot_G_tmi_parallel( + receivers, + cells_bounds, + top, + bottom, + regional_field, + constant_factor, + scalar_model, + weights, + diagonal, +): + """ + Diagonal of ``G.T @ W.T @ W @ G`` for TMI without storing ``G``, in parallel. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + weights : (n_receivers,) numpy.ndarray + Array with data weights. It should be the diagonal of the ``W`` matrix, + squared. + diagonal : (n_active_cells,) numpy.ndarray + Array where the diagonal of ``G.T @ G`` will be added to. + + Notes + ----- + This function is meant to be run in parallel. Use the + ``_diagonal_G_T_dot_G_tmi_serial`` one for serialized computations. + """ + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + diagonal_size = diagonal.size + constant_factor = 1 / 4 / np.pi + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate array for the diagonal elements for the current receiver. + local_diagonal = np.empty(diagonal_size) + for j in range(n_cells): + # Evaluate kernels for the current cell and receiver + uxx, uyy, uzz, uxy, uxz, uyz = evaluate_six_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + choclo.prism.kernel_ee, + choclo.prism.kernel_nn, + choclo.prism.kernel_uu, + choclo.prism.kernel_en, + choclo.prism.kernel_eu, + choclo.prism.kernel_nu, + ) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + + if scalar_model: + g_element = ( + constant_factor + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + local_diagonal[j] = weights[i] * g_element**2 + else: + const = constant_factor * regional_field_amplitude + local_diagonal[j] = weights[i] * (const * bx) ** 2 + local_diagonal[j + n_cells] = weights[i] * (const * by) ** 2 + local_diagonal[j + 2 * n_cells] = weights[i] * (const * bz) ** 2 + # Add the result to the diagonal. + # Apply reduction operation to add the values of the local diagonal to + # the running diagonal array. Avoid slicing the `diagonal` array when + # updating it to avoid racing conditions, just add the `local_diagonal` + # to the `diagonal` variable. + diagonal += local_diagonal + + +@jit(nopython=True, parallel=False) +def _diagonal_G_T_dot_G_mag_serial( + receivers, + cells_bounds, + top, + bottom, + regional_field, + kernel_x, + kernel_y, + kernel_z, + constant_factor, + scalar_model, + weights, + diagonal, +): + """ + Diagonal of ``G.T @ W.T @ W @ G`` for components without storing ``G``, in serial. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_x, kernel_y, kernel_z : callable + Kernels used to compute the desired magnetic component. For example, + for computing bx we need to use ``kernel_x=kernel_ee``, + ``kernel_y=kernel_en``, ``kernel_z=kernel_eu``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + weights : (n_receivers,) numpy.ndarray + Array with data weights. It should be the diagonal of the ``W`` matrix, + squared. + diagonal : (n_active_cells,) numpy.ndarray + Array where the diagonal of ``G.T @ G`` will be added to. + + Notes + ----- + This function is meant to be run in serial. Use the + ``_diagonal_G_T_dot_G_mag_parallel`` one for parallelized computations. + """ + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + + constant_factor = 1 / 4 / np.pi + + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + # Evaluate kernel function on each node, for each receiver location + for i in range(n_receivers): + for j in range(n_cells): + # Evaluate kernels for the current cell and receiver + ux, uy, uz = evaluate_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + kernel_x, + kernel_y, + kernel_z, + ) + if scalar_model: + g_element = ( + constant_factor + * regional_field_amplitude + * (ux * fx + uy * fy + uz * fz) + ) + diagonal[j] += weights[i] * g_element**2 + else: + const = constant_factor * regional_field_amplitude + diagonal[j] += weights[i] * (const * ux) ** 2 + diagonal[j + n_cells] += weights[i] * (const * uy) ** 2 + diagonal[j + 2 * n_cells] += weights[i] * (const * uz) ** 2 + + +@jit(nopython=True, parallel=True) +def _diagonal_G_T_dot_G_mag_parallel( + receivers, + cells_bounds, + top, + bottom, + regional_field, + kernel_x, + kernel_y, + kernel_z, + constant_factor, + scalar_model, + weights, + diagonal, +): + """ + Diagonal of ``G.T @ W.T @ W @ G`` for component without storing ``G``, in parallel. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_x, kernel_y, kernel_z : callable + Kernels used to compute the desired magnetic component. For example, + for computing bx we need to use ``kernel_x=kernel_ee``, + ``kernel_y=kernel_en``, ``kernel_z=kernel_eu``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + weights : (n_receivers,) numpy.ndarray + Array with data weights. It should be the diagonal of the ``W`` matrix, + squared. + diagonal : (n_active_cells,) numpy.ndarray + Array where the diagonal of ``G.T @ G`` will be added to. + + Notes + ----- + This function is meant to be run in parallel. Use the + ``_diagonal_G_T_dot_G_mag_serial`` one for serialized computations. + """ + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + diagonal_size = diagonal.size + constant_factor = 1 / 4 / np.pi + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate array for the diagonal elements for the current receiver. + local_diagonal = np.empty(diagonal_size) + for j in range(n_cells): + # Evaluate kernels for the current cell and receiver + ux, uy, uz = evaluate_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + kernel_x, + kernel_y, + kernel_z, + ) + if scalar_model: + g_element = ( + constant_factor + * regional_field_amplitude + * (ux * fx + uy * fy + uz * fz) + ) + local_diagonal[j] = weights[i] * g_element**2 + else: + const = constant_factor * regional_field_amplitude + local_diagonal[j] = weights[i] * (const * ux) ** 2 + local_diagonal[j + n_cells] = weights[i] * (const * uy) ** 2 + local_diagonal[j + 2 * n_cells] = weights[i] * (const * uz) ** 2 + # Add the result to the diagonal. + # Apply reduction operation to add the values of the local diagonal to + # the running diagonal array. Avoid slicing the `diagonal` array when + # updating it to avoid racing conditions, just add the `local_diagonal` + # to the `diagonal` variable. + diagonal += local_diagonal + + +@jit(nopython=True, parallel=False) +def _diagonal_G_T_dot_G_tmi_deriv_serial( + receivers, + cells_bounds, + top, + bottom, + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + constant_factor, + scalar_model, + weights, + diagonal, +): + """ + Diagonal of ``G.T @ W.T @ W @ G`` for TMI derivative, in serial. + + This function doesn't need to store the ``G`` matrix in memory. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz : callables + Kernel functions used for computing the desired TMI derivative. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + weights : (n_receivers,) numpy.ndarray + Array with data weights. It should be the diagonal of the ``W`` matrix, + squared. + diagonal : (n_active_cells,) numpy.ndarray + Array where the diagonal of ``G.T @ G`` will be added to. + + Notes + ----- + This function is meant to be run in serial. Use the + ``_diagonal_G_T_dot_G_tmi_deriv_parallel`` one for parallelized computations. + """ + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + + constant_factor = 1 / 4 / np.pi + + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + # Evaluate kernel function on each node, for each receiver location + for i in range(n_receivers): + for j in range(n_cells): + # Evaluate kernels for the current cell and receiver + uxx, uyy, uzz, uxy, uxz, uyz = evaluate_six_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + ) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + + if scalar_model: + g_element = ( + constant_factor + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + diagonal[j] += weights[i] * g_element**2 + else: + const = constant_factor * regional_field_amplitude + diagonal[j] += weights[i] * (const * bx) ** 2 + diagonal[j + n_cells] += weights[i] * (const * by) ** 2 + diagonal[j + 2 * n_cells] += weights[i] * (const * bz) ** 2 + + +@jit(nopython=True, parallel=True) +def _diagonal_G_T_dot_G_tmi_deriv_parallel( + receivers, + cells_bounds, + top, + bottom, + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + constant_factor, + scalar_model, + weights, + diagonal, +): + """ + Diagonal of ``G.T @ W.T @ W @ G`` for TMI without storing ``G``, in parallel. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz : callables + Kernel functions used for computing the desired TMI derivative. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + weights : (n_receivers,) numpy.ndarray + Array with data weights. It should be the diagonal of the ``W`` matrix, + squared. + diagonal : (n_active_cells,) numpy.ndarray + Array where the diagonal of ``G.T @ G`` will be added to. + + Notes + ----- + This function is meant to be run in parallel. Use the + ``_diagonal_G_T_dot_G_tmi_serial`` one for serialized computations. + """ + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + diagonal_size = diagonal.size + constant_factor = 1 / 4 / np.pi + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate array for the diagonal elements for the current receiver. + local_diagonal = np.empty(diagonal_size) + for j in range(n_cells): + # Evaluate kernels for the current cell and receiver + uxx, uyy, uzz, uxy, uxz, uyz = evaluate_six_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + ) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + + if scalar_model: + g_element = ( + constant_factor + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + local_diagonal[j] = weights[i] * g_element**2 + else: + const = constant_factor * regional_field_amplitude + local_diagonal[j] = weights[i] * (const * bx) ** 2 + local_diagonal[j + n_cells] = weights[i] * (const * by) ** 2 + local_diagonal[j + 2 * n_cells] = weights[i] * (const * bz) ** 2 + # Add the result to the diagonal. + # Apply reduction operation to add the values of the local diagonal to + # the running diagonal array. Avoid slicing the `diagonal` array when + # updating it to avoid racing conditions, just add the `local_diagonal` + # to the `diagonal` variable. + diagonal += local_diagonal + + +NUMBA_FUNCTIONS_2D = { + "forward": { + "tmi": { + parallel: jit(nopython=True, parallel=parallel)(_forward_tmi) + for parallel in (True, False) + }, + "magnetic_component": { + parallel: jit(nopython=True, parallel=parallel)(_forward_mag) + for parallel in (True, False) + }, + "tmi_derivative": { + parallel: jit(nopython=True, parallel=parallel)(_forward_tmi_derivative) + for parallel in (True, False) + }, + }, + "sensitivity": { + "tmi": { + parallel: jit(nopython=True, parallel=parallel)(_sensitivity_tmi) + for parallel in (True, False) + }, + "magnetic_component": { + parallel: jit(nopython=True, parallel=parallel)(_sensitivity_mag) + for parallel in (True, False) + }, + "tmi_derivative": { + parallel: jit(nopython=True, parallel=parallel)(_sensitivity_tmi_derivative) + for parallel in (True, False) + }, + }, + "gt_dot_v": { + "tmi": { + False: _tmi_sensitivity_t_dot_v_serial, + True: _tmi_sensitivity_t_dot_v_parallel, + }, + "magnetic_component": { + False: _mag_sensitivity_t_dot_v_serial, + True: _mag_sensitivity_t_dot_v_parallel, + }, + "tmi_derivative": { + False: _tmi_derivative_sensitivity_t_dot_v_serial, + True: _tmi_derivative_sensitivity_t_dot_v_parallel, + }, + }, + "diagonal_gtg": { + "tmi": { + False: _diagonal_G_T_dot_G_tmi_serial, + True: _diagonal_G_T_dot_G_tmi_parallel, + }, + "magnetic_component": { + False: _diagonal_G_T_dot_G_mag_serial, + True: _diagonal_G_T_dot_G_mag_parallel, + }, + "tmi_derivative": { + False: _diagonal_G_T_dot_G_tmi_deriv_serial, + True: _diagonal_G_T_dot_G_tmi_deriv_parallel, + }, + }, +} diff --git a/simpeg/potential_fields/magnetics/_numba_functions.py b/simpeg/potential_fields/magnetics/_numba/_3d_mesh.py similarity index 70% rename from simpeg/potential_fields/magnetics/_numba_functions.py rename to simpeg/potential_fields/magnetics/_numba/_3d_mesh.py index 43e685dbf9..57ba260371 100644 --- a/simpeg/potential_fields/magnetics/_numba_functions.py +++ b/simpeg/potential_fields/magnetics/_numba/_3d_mesh.py @@ -15,7 +15,7 @@ def jit(*args, **kwargs): else: from numba import jit, prange -from .._numba_utils import kernels_in_nodes_to_cell, evaluate_kernels_on_cell +from ..._numba_utils import kernels_in_nodes_to_cell def _sensitivity_mag( @@ -594,10 +594,10 @@ def _mag_sensitivity_t_dot_v_serial( Constant factor that will be used to multiply each element of the sensitivity matrix. scalar_model : bool - If True, the sensitivity matrix is built to work with scalar models - (susceptibilities). - If False, the sensitivity matrix is built to work with vector models - (effective susceptibilities). + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. vector : (n_receivers) numpy.ndarray Array that represents the vector used in the dot product. result : (n_active_cells) or (3 * n_active_cells) numpy.ndarray @@ -702,10 +702,10 @@ def _mag_sensitivity_t_dot_v_parallel( Constant factor that will be used to multiply each element of the sensitivity matrix. scalar_model : bool - If True, the sensitivity matrix is built to work with scalar models - (susceptibilities). - If False, the sensitivity matrix is built to work with vector models - (effective susceptibilities). + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. vector : (n_receivers) numpy.ndarray Array that represents the vector used in the dot product. result : (n_active_cells) or (3 * n_active_cells) numpy.ndarray @@ -815,10 +815,10 @@ def _tmi_sensitivity_t_dot_v_serial( Constant factor that will be used to multiply each element of the sensitivity matrix. scalar_model : bool - If True, the sensitivity matrix is build to work with scalar models - (susceptibilities). - If False, the sensitivity matrix is build to work with vector models - (effective susceptibilities). + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. vector : (n_receivers) numpy.ndarray Array that represents the vector used in the dot product. result : (n_active_cells) or (3 * n_active_cells) numpy.ndarray @@ -927,10 +927,10 @@ def _tmi_sensitivity_t_dot_v_parallel( Constant factor that will be used to multiply each element of the sensitivity matrix. scalar_model : bool - If True, the sensitivity matrix is build to work with scalar models - (susceptibilities). - If False, the sensitivity matrix is build to work with vector models - (effective susceptibilities). + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. vector : (n_receivers) numpy.ndarray Array that represents the vector used in the dot product. result : (n_active_cells) or (3 * n_active_cells) numpy.ndarray @@ -1059,10 +1059,10 @@ def _tmi_derivative_sensitivity_t_dot_v_serial( Constant factor that will be used to multiply each element of the sensitivity matrix. scalar_model : bool - If True, the sensitivity matrix is build to work with scalar models - (susceptibilities). - If False, the sensitivity matrix is build to work with vector models - (effective susceptibilities). + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. vector : (n_receivers) numpy.ndarray Array that represents the vector used in the dot product. result : (n_active_cells) or (3 * n_active_cells) numpy.ndarray @@ -1179,10 +1179,10 @@ def _tmi_derivative_sensitivity_t_dot_v_parallel( Constant factor that will be used to multiply each element of the sensitivity matrix. scalar_model : bool - If True, the sensitivity matrix is build to work with scalar models - (susceptibilities). - If False, the sensitivity matrix is build to work with vector models - (effective susceptibilities). + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. vector : (n_receivers) numpy.ndarray Array that represents the vector used in the dot product. result : (n_active_cells) or (3 * n_active_cells) numpy.ndarray @@ -1312,10 +1312,10 @@ def _diagonal_G_T_dot_G_mag_serial( Constant factor that will be used to multiply each element of the sensitivity matrix. scalar_model : bool - If True, the sensitivity matrix is built to work with scalar models - (susceptibilities). - If False, the sensitivity matrix is built to work with vector models - (effective susceptibilities). + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. weights : (n_receivers,) numpy.ndarray Array with data weights. It should be the diagonal of the ``W`` matrix, squared. @@ -1408,10 +1408,10 @@ def _diagonal_G_T_dot_G_mag_parallel( Constant factor that will be used to multiply each element of the sensitivity matrix. scalar_model : bool - If True, the sensitivity matrix is built to work with scalar models - (susceptibilities). - If False, the sensitivity matrix is built to work with vector models - (effective susceptibilities). + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. weights : (n_receivers,) numpy.ndarray Array with data weights. It should be the diagonal of the ``W`` matrix, squared. @@ -1506,10 +1506,10 @@ def _diagonal_G_T_dot_G_tmi_serial( Constant factor that will be used to multiply each element of the sensitivity matrix. scalar_model : bool - If True, the sensitivity matrix is built to work with scalar models - (susceptibilities). - If False, the sensitivity matrix is built to work with vector models - (effective susceptibilities). + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. weights : (n_receivers,) numpy.ndarray Array with data weights. It should be the diagonal of the ``W`` matrix, squared. @@ -1606,10 +1606,10 @@ def _diagonal_G_T_dot_G_tmi_parallel( Constant factor that will be used to multiply each element of the sensitivity matrix. scalar_model : bool - If True, the sensitivity matrix is built to work with scalar models - (susceptibilities). - If False, the sensitivity matrix is built to work with vector models - (effective susceptibilities). + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. weights : (n_receivers,) numpy.ndarray Array with data weights. It should be the diagonal of the ``W`` matrix, squared. @@ -1724,10 +1724,10 @@ def _diagonal_G_T_dot_G_tmi_deriv_serial( Constant factor that will be used to multiply each element of the sensitivity matrix. scalar_model : bool - If True, the sensitivity matrix is built to work with scalar models - (susceptibilities). - If False, the sensitivity matrix is built to work with vector models - (effective susceptibilities). + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. weights : (n_receivers,) numpy.ndarray Array with data weights. It should be the diagonal of the ``W`` matrix, squared. @@ -1832,10 +1832,10 @@ def _diagonal_G_T_dot_G_tmi_deriv_parallel( Constant factor that will be used to multiply each element of the sensitivity matrix. scalar_model : bool - If True, the sensitivity matrix is built to work with scalar models - (susceptibilities). - If False, the sensitivity matrix is built to work with vector models - (effective susceptibilities). + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. weights : (n_receivers,) numpy.ndarray Array with data weights. It should be the diagonal of the ``W`` matrix, squared. @@ -1965,9 +1965,9 @@ def _forward_mag( Constant factor that will be used to multiply each element of the sensitivity matrix. scalar_model : bool - If True, the forward will be computing assuming that the ``model`` has + If True, the forward will be computed assuming that the ``model`` has susceptibilities (scalar model) for each active cell. - If False, the forward will be computing assuming that the ``model`` has + If False, the forward will be computed assuming that the ``model`` has effective susceptibilities (vector model) for each active cell. Notes @@ -2360,939 +2360,7 @@ def _forward_tmi_derivative( ) -def _forward_mag_2d_mesh( - receivers, - cells_bounds, - top, - bottom, - model, - fields, - regional_field, - forward_func, - scalar_model, -): - """ - Forward model single magnetic component for 2D meshes. - - This function is designed to be used with equivalent sources, where the - mesh is a 2D mesh (prism layer). The top and bottom boundaries of each cell - are passed through the ``top`` and ``bottom`` arrays. - - This function should be used with a `numba.jit` decorator, for example: - - .. code:: - - from numba import jit - - jit_forward = jit(nopython=True, parallel=True)(_forward_mag_2d_mesh) - - Parameters - ---------- - receivers : (n_receivers, 3) numpy.ndarray - Array with the locations of the receivers - cells_bounds : (n_active_cells, 4) numpy.ndarray - Array with the bounds of each active cell in the 2D mesh. For each row, the - bounds should be passed in the following order: ``x_min``, ``x_max``, - ``y_min``, ``y_max``. - top : (n_active_cells) np.ndarray - Array with the top boundaries of each active cell in the 2D mesh. - bottom : (n_active_cells) np.ndarray - Array with the bottom boundaries of each active cell in the 2D mesh. - model : (n_active_cells) or (3 * n_active_cells) array - Array containing the susceptibilities (scalar) or effective - susceptibilities (vector) of the active cells in the mesh, in SI - units. - Susceptibilities are expected if ``scalar_model`` is True, - and the array should have ``n_active_cells`` elements. - Effective susceptibilities are expected if ``scalar_model`` is False, - and the array should have ``3 * n_active_cells`` elements. - fields : (n_receivers) array - Array full of zeros where the magnetic component on each receiver will - be stored. This could be a preallocated array or a slice of it. - regional_field : (3,) array - Array containing the x, y and z components of the regional magnetic - field (uniform background field). - forward_func : callable - Forward function that will be evaluated on each node of the mesh. Choose - one of the forward functions in ``choclo.prism``. - scalar_model : bool - If True, the forward will be computing assuming that the ``model`` has - susceptibilities (scalar model) for each active cell. - If False, the forward will be computing assuming that the ``model`` has - effective susceptibilities (vector model) for each active cell. - - Notes - ----- - - About the model array - ^^^^^^^^^^^^^^^^^^^^^ - - The ``model`` must always be a 1d array: - - * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with - the same number of elements as active cells in the mesh. It should store - the magnetic susceptibilities of each active cell in SI units. - * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array - with a number of elements equal to three times the active cells in the - mesh. It should store the components of the magnetization vector of each - active cell in :math:`Am^{-1}`. The order in which the components should - be passed are: - * every _easting_ component of each active cell, - * then every _northing_ component of each active cell, - * and finally every _upward_ component of each active cell. - - """ - n_receivers = receivers.shape[0] - n_cells = cells_bounds.shape[0] - fx, fy, fz = regional_field - regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) - fx /= regional_field_amplitude - fy /= regional_field_amplitude - fz /= regional_field_amplitude - # Forward model the magnetic component of each cell on each receiver location - for i in prange(n_receivers): - for j in range(n_cells): - # Define magnetization vector of the cell - # (we we'll divide by mu_0 when adding the forward modelled field) - if scalar_model: - # model is susceptibility, so the vector is parallel to the - # regional field - magnetization_x = model[j] * fx - magnetization_y = model[j] * fy - magnetization_z = model[j] * fz - else: - # model is effective susceptibility (vector) - magnetization_x = model[j] - magnetization_y = model[j + n_cells] - magnetization_z = model[j + 2 * n_cells] - # Forward the magnetic component - fields[i] += ( - regional_field_amplitude - * forward_func( - receivers[i, 0], - receivers[i, 1], - receivers[i, 2], - cells_bounds[j, 0], - cells_bounds[j, 1], - cells_bounds[j, 2], - cells_bounds[j, 3], - bottom[j], - top[j], - magnetization_x, - magnetization_y, - magnetization_z, - ) - / choclo.constants.VACUUM_MAGNETIC_PERMEABILITY - ) - - -def _forward_tmi_2d_mesh( - receivers, - cells_bounds, - top, - bottom, - model, - fields, - regional_field, - scalar_model, -): - """ - Forward model the TMI for 2D meshes. - - This function is designed to be used with equivalent sources, where the - mesh is a 2D mesh (prism layer). The top and bottom boundaries of each cell - are passed through the ``top`` and ``bottom`` arrays. - - This function should be used with a `numba.jit` decorator, for example: - - .. code:: - - from numba import jit - - jit_forward = jit(nopython=True, parallel=True)(_forward_tmi_2d_mesh) - - Parameters - ---------- - receivers : (n_receivers, 3) numpy.ndarray - Array with the locations of the receivers - cells_bounds : (n_active_cells, 4) numpy.ndarray - Array with the bounds of each active cell in the 2D mesh. For each row, the - bounds should be passed in the following order: ``x_min``, ``x_max``, - ``y_min``, ``y_max``. - top : (n_active_cells) np.ndarray - Array with the top boundaries of each active cell in the 2D mesh. - bottom : (n_active_cells) np.ndarray - Array with the bottom boundaries of each active cell in the 2D mesh. - model : (n_active_cells) or (3 * n_active_cells) - Array with the susceptibility (scalar model) or the effective - susceptibility (vector model) of each active cell in the mesh. - If the model is scalar, the ``model`` array should have - ``n_active_cells`` elements and ``scalar_model`` should be True. - If the model is vector, the ``model`` array should have - ``3 * n_active_cells`` elements and ``scalar_model`` should be False. - fields : (n_receivers) array - Array full of zeros where the TMI on each receiver will be stored. This - could be a preallocated array or a slice of it. - regional_field : (3,) array - Array containing the x, y and z components of the regional magnetic - field (uniform background field). - scalar_model : bool - If True, the sensitivity matrix is build to work with scalar models - (susceptibilities). - If False, the sensitivity matrix is build to work with vector models - (effective susceptibilities). - - Notes - ----- - - The ``model`` must always be a 1d array: - - * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with - the same number of elements as active cells in the mesh. It should store - the magnetic susceptibilities of each active cell in SI units. - * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array - with a number of elements equal to three times the active cells in the - mesh. It should store the components of the magnetization vector of each - active cell in :math:`Am^{-1}`. The order in which the components should - be passed are: - * every _easting_ component of each active cell, - * then every _northing_ component of each active cell, - * and finally every _upward_ component of each active cell. - - """ - n_receivers = receivers.shape[0] - n_cells = cells_bounds.shape[0] - fx, fy, fz = regional_field - regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) - fx /= regional_field_amplitude - fy /= regional_field_amplitude - fz /= regional_field_amplitude - # Forward model the magnetic component of each cell on each receiver location - for i in prange(n_receivers): - for j in range(n_cells): - # Define magnetization vector of the cell - # (we we'll divide by mu_0 when adding the forward modelled field) - if scalar_model: - # model is susceptibility, so the vector is parallel to the - # regional field - magnetization_x = model[j] * fx - magnetization_y = model[j] * fy - magnetization_z = model[j] * fz - else: - # model is effective susceptibility (vector) - magnetization_x = model[j] - magnetization_y = model[j + n_cells] - magnetization_z = model[j + 2 * n_cells] - # Forward the magnetic field vector and compute tmi - bx, by, bz = choclo.prism.magnetic_field( - receivers[i, 0], - receivers[i, 1], - receivers[i, 2], - cells_bounds[j, 0], - cells_bounds[j, 1], - cells_bounds[j, 2], - cells_bounds[j, 3], - bottom[j], - top[j], - magnetization_x, - magnetization_y, - magnetization_z, - ) - fields[i] += ( - regional_field_amplitude - * (bx * fx + by * fy + bz * fz) - / choclo.constants.VACUUM_MAGNETIC_PERMEABILITY - ) - - -def _forward_tmi_derivative_2d_mesh( - receivers, - cells_bounds, - top, - bottom, - model, - fields, - regional_field, - kernel_xx, - kernel_yy, - kernel_zz, - kernel_xy, - kernel_xz, - kernel_yz, - scalar_model, -): - r""" - Forward model a TMI derivative for 2D meshes. - - This function is designed to be used with equivalent sources, where the - mesh is a 2D mesh (prism layer). The top and bottom boundaries of each cell - are passed through the ``top`` and ``bottom`` arrays. - - This function should be used with a `numba.jit` decorator, for example: - - .. code:: - - from numba import jit - - jit_forward = jit(nopython=True, parallel=True)(_forward_tmi_2d_mesh) - - Parameters - ---------- - receivers : (n_receivers, 3) numpy.ndarray - Array with the locations of the receivers - cells_bounds : (n_active_cells, 4) numpy.ndarray - Array with the bounds of each active cell in the 2D mesh. For each row, the - bounds should be passed in the following order: ``x_min``, ``x_max``, - ``y_min``, ``y_max``. - top : (n_active_cells) np.ndarray - Array with the top boundaries of each active cell in the 2D mesh. - bottom : (n_active_cells) np.ndarray - Array with the bottom boundaries of each active cell in the 2D mesh. - model : (n_active_cells) or (3 * n_active_cells) - Array with the susceptibility (scalar model) or the effective - susceptibility (vector model) of each active cell in the mesh. - If the model is scalar, the ``model`` array should have - ``n_active_cells`` elements and ``scalar_model`` should be True. - If the model is vector, the ``model`` array should have - ``3 * n_active_cells`` elements and ``scalar_model`` should be False. - fields : (n_receivers) array - Array full of zeros where the TMI on each receiver will be stored. This - could be a preallocated array or a slice of it. - regional_field : (3,) array - Array containing the x, y and z components of the regional magnetic - field (uniform background field). - kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz : callables - Kernel functions used for computing the desired TMI derivative. - scalar_model : bool - If True, the sensitivity matrix is build to work with scalar models - (susceptibilities). - If False, the sensitivity matrix is build to work with vector models - (effective susceptibilities). - - Notes - ----- - - About the kernel functions - ^^^^^^^^^^^^^^^^^^^^^^^^^^ - - To compute the :math:`\alpha` derivative of the TMI :math:`\Delta T` (with - :math:`\alpha \in \{x, y, z\}` we need to evaluate third order kernels - functions for the prism. The kernels we need to evaluate can be obtained by - fixing one of the subindices to the direction of the derivative - (:math:`\alpha`) and cycle through combinations of the other two. - - For ``tmi_x`` we need to pass: - - .. code:: - - kernel_xx=kernel_eee, kernel_yy=kernel_enn, kernel_zz=kernel_euu, - kernel_xy=kernel_een, kernel_xz=kernel_eeu, kernel_yz=kernel_enu - - For ``tmi_y`` we need to pass: - - .. code:: - - kernel_xx=kernel_een, kernel_yy=kernel_nnn, kernel_zz=kernel_nuu, - kernel_xy=kernel_enn, kernel_xz=kernel_enu, kernel_yz=kernel_nnu - - For ``tmi_z`` we need to pass: - - .. code:: - - kernel_xx=kernel_eeu, kernel_yy=kernel_nnu, kernel_zz=kernel_uuu, - kernel_xy=kernel_enu, kernel_xz=kernel_euu, kernel_yz=kernel_nuu - - - About the model array - ^^^^^^^^^^^^^^^^^^^^^ - - The ``model`` must always be a 1d array: - - * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with - the same number of elements as active cells in the mesh. It should store - the magnetic susceptibilities of each active cell in SI units. - * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array - with a number of elements equal to three times the active cells in the - mesh. It should store the components of the magnetization vector of each - active cell in :math:`Am^{-1}`. The order in which the components should - be passed are: - * every _easting_ component of each active cell, - * then every _northing_ component of each active cell, - * and finally every _upward_ component of each active cell. - - """ - n_receivers = receivers.shape[0] - n_cells = cells_bounds.shape[0] - fx, fy, fz = regional_field - regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) - fx /= regional_field_amplitude - fy /= regional_field_amplitude - fz /= regional_field_amplitude - # Forward model the magnetic component of each cell on each receiver location - for i in prange(n_receivers): - for j in range(n_cells): - uxx, uyy, uzz = evaluate_kernels_on_cell( - receivers[i, 0], - receivers[i, 1], - receivers[i, 2], - cells_bounds[j, 0], - cells_bounds[j, 1], - cells_bounds[j, 2], - cells_bounds[j, 3], - bottom[j], - top[j], - kernel_xx, - kernel_yy, - kernel_zz, - ) - uxy, uxz, uyz = evaluate_kernels_on_cell( - receivers[i, 0], - receivers[i, 1], - receivers[i, 2], - cells_bounds[j, 0], - cells_bounds[j, 1], - cells_bounds[j, 2], - cells_bounds[j, 3], - bottom[j], - top[j], - kernel_xy, - kernel_xz, - kernel_yz, - ) - if scalar_model: - bx = uxx * fx + uxy * fy + uxz * fz - by = uxy * fx + uyy * fy + uyz * fz - bz = uxz * fx + uyz * fy + uzz * fz - fields[i] += ( - model[j] - * regional_field_amplitude - * (fx * bx + fy * by + fz * bz) - / (4 * np.pi) - ) - else: - model_x = model[j] - model_y = model[j + n_cells] - model_z = model[j + 2 * n_cells] - bx = uxx * model_x + uxy * model_y + uxz * model_z - by = uxy * model_x + uyy * model_y + uyz * model_z - bz = uxz * model_x + uyz * model_y + uzz * model_z - fields[i] += ( - regional_field_amplitude * (bx * fx + by * fy + bz * fz) / 4 / np.pi - ) - - -def _sensitivity_mag_2d_mesh( - receivers, - cells_bounds, - top, - bottom, - sensitivity_matrix, - regional_field, - kernel_x, - kernel_y, - kernel_z, - scalar_model, -): - r""" - Fill the sensitivity matrix for single mag component for 2d meshes. - - This function is designed to be used with equivalent sources, where the - mesh is a 2D mesh (prism layer). The top and bottom boundaries of each cell - are passed through the ``top`` and ``bottom`` arrays. - - This function should be used with a `numba.jit` decorator, for example: - - .. code:: - - from numba import jit - - jit_sensitivity = jit(nopython=True, parallel=True)(_sensitivity_mag_2d_mesh) - - Parameters - ---------- - receivers : (n_receivers, 3) numpy.ndarray - Array with the locations of the receivers - cells_bounds : (n_active_cells, 4) numpy.ndarray - Array with the bounds of each active cell in the 2D mesh. For each row, the - bounds should be passed in the following order: ``x_min``, ``x_max``, - ``y_min``, ``y_max``. - top : (n_active_cells) np.ndarray - Array with the top boundaries of each active cell in the 2D mesh. - bottom : (n_active_cells) np.ndarray - Array with the bottom boundaries of each active cell in the 2D mesh. - sensitivity_matrix : array - Empty 2d array where the sensitivity matrix elements will be filled. - This could be a preallocated empty array or a slice of it. - The array should have a shape of ``(n_receivers, n_active_cells)`` - if ``scalar_model`` is True. - The array should have a shape of ``(n_receivers, 3 * n_active_cells)`` - if ``scalar_model`` is False. - regional_field : (3,) array - Array containing the x, y and z components of the regional magnetic - field (uniform background field). - kernel_x, kernel_y, kernel_z : callable - Kernels used to compute the desired magnetic component. For example, - for computing bx we need to use ``kernel_x=kernel_ee``, - ``kernel_y=kernel_en``, ``kernel_z=kernel_eu``. - scalar_model : bool - If True, the sensitivity matrix is built to work with scalar models - (susceptibilities). - If False, the sensitivity matrix is built to work with vector models - (effective susceptibilities). - - Notes - ----- - - About the kernel functions - ^^^^^^^^^^^^^^^^^^^^^^^^^^ - - For computing the ``bx`` component of the magnetic field we need to use the - following kernels: - - .. code:: - - kernel_x=kernel_ee, kernel_y=kernel_en, kernel_z=kernel_eu - - - For computing the ``by`` component of the magnetic field we need to use the - following kernels: - - .. code:: - - kernel_x=kernel_en, kernel_y=kernel_nn, kernel_z=kernel_nu - - For computing the ``bz`` component of the magnetic field we need to use the - following kernels: - - .. code:: - - kernel_x=kernel_eu, kernel_y=kernel_nu, kernel_z=kernel_uu - - - About the model array - ^^^^^^^^^^^^^^^^^^^^^ - - The ``model`` must always be a 1d array: - - * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with - the same number of elements as active cells in the mesh. It should store - the magnetic susceptibilities of each active cell in SI units. - * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array - with a number of elements equal to three times the active cells in the - mesh. It should store the components of the magnetization vector of each - active cell in :math:`Am^{-1}`. The order in which the components should - be passed are: - * every _easting_ component of each active cell, - * then every _northing_ component of each active cell, - * and finally every _upward_ component of each active cell. - - About the sensitivity matrix - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - Each row of the sensitivity matrix corresponds to a single receiver - location. - - If ``scalar_model`` is True, then each element of the row will - correspond to the partial derivative of the selected magnetic component - with respect to the susceptibility of each cell in the mesh. - - If ``scalar_model`` is False, then each row can be split in three sections - containing: - - * the partial derivatives of the selected magnetic component with respect - to the _x_ component of the effective susceptibility of each cell; then - * the partial derivatives of the selected magnetic component with respect - to the _y_ component of the effective susceptibility of each cell; and then - * the partial derivatives of the selected magnetic component with respect - to the _z_ component of the effective susceptibility of each cell. - - So, if we call :math:`B_j` the magnetic field component on the receiver - :math:`j`, and :math:`\bar{\chi}^{(i)} = (\chi_x^{(i)}, \chi_y^{(i)}, - \chi_z^{(i)})` the effective susceptibility of the active cell :math:`i`, - then each row of the sensitivity matrix will be: - - .. math:: - - \left[ - \frac{\partial B_j}{\partial \chi_x^{(1)}}, - \dots, - \frac{\partial B_j}{\partial \chi_x^{(N)}}, - \frac{\partial B_j}{\partial \chi_y^{(1)}}, - \dots, - \frac{\partial B_j}{\partial \chi_y^{(N)}}, - \frac{\partial B_j}{\partial \chi_z^{(1)}}, - \dots, - \frac{\partial B_j}{\partial \chi_z^{(N)}} - \right] - - where :math:`N` is the total number of active cells. - """ - fx, fy, fz = regional_field - regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) - fx /= regional_field_amplitude - fy /= regional_field_amplitude - fz /= regional_field_amplitude - - constant_factor = 1 / 4 / np.pi - - # Fill the sensitivity matrix - n_receivers = receivers.shape[0] - n_cells = cells_bounds.shape[0] - for i in prange(n_receivers): - for j in range(n_cells): - # Evaluate kernels for the current cell and receiver - ux, uy, uz = evaluate_kernels_on_cell( - receivers[i, 0], - receivers[i, 1], - receivers[i, 2], - cells_bounds[j, 0], - cells_bounds[j, 1], - cells_bounds[j, 2], - cells_bounds[j, 3], - bottom[j], - top[j], - kernel_x, - kernel_y, - kernel_z, - ) - if scalar_model: - sensitivity_matrix[i, j] = ( - constant_factor - * regional_field_amplitude - * (ux * fx + uy * fy + uz * fz) - ) - else: - sensitivity_matrix[i, j] = ( - constant_factor * regional_field_amplitude * ux - ) - sensitivity_matrix[i, j + n_cells] = ( - constant_factor * regional_field_amplitude * uy - ) - sensitivity_matrix[i, j + 2 * n_cells] = ( - constant_factor * regional_field_amplitude * uz - ) - - -def _sensitivity_tmi_2d_mesh( - receivers, - cells_bounds, - top, - bottom, - sensitivity_matrix, - regional_field, - scalar_model, -): - r""" - Fill the sensitivity matrix TMI for 2d meshes. - - This function is designed to be used with equivalent sources, where the - mesh is a 2D mesh (prism layer). The top and bottom boundaries of each cell - are passed through the ``top`` and ``bottom`` arrays. - - This function should be used with a `numba.jit` decorator, for example: - - .. code:: - - from numba import jit - - jit_tmi = jit(nopython=True, parallel=True)(_sensitivity_tmi_2d_mesh) - - Parameters - ---------- - receivers : (n_receivers, 3) numpy.ndarray - Array with the locations of the receivers - cells_bounds : (n_active_cells, 4) numpy.ndarray - Array with the bounds of each active cell in the 2D mesh. For each row, the - bounds should be passed in the following order: ``x_min``, ``x_max``, - ``y_min``, ``y_max``. - top : (n_active_cells) np.ndarray - Array with the top boundaries of each active cell in the 2D mesh. - bottom : (n_active_cells) np.ndarray - Array with the bottom boundaries of each active cell in the 2D mesh. - sensitivity_matrix : array - Empty 2d array where the sensitivity matrix elements will be filled. - This could be a preallocated empty array or a slice of it. - The array should have a shape of ``(n_receivers, n_active_cells)`` - if ``scalar_model`` is True. - The array should have a shape of ``(n_receivers, 3 * n_active_cells)`` - if ``scalar_model`` is False. - regional_field : (3,) array - Array containing the x, y and z components of the regional magnetic - field (uniform background field). - scalar_model : bool - If True, the sensitivity matrix is build to work with scalar models - (susceptibilities). - If False, the sensitivity matrix is build to work with vector models - (effective susceptibilities). - - Notes - ----- - - About the model array - ^^^^^^^^^^^^^^^^^^^^^ - - The ``model`` must always be a 1d array: - - * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with - the same number of elements as active cells in the mesh. It should store - the magnetic susceptibilities of each active cell in SI units. - * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array - with a number of elements equal to three times the active cells in the - mesh. It should store the components of the magnetization vector of each - active cell in :math:`Am^{-1}`. The order in which the components should - be passed are: - * every _easting_ component of each active cell, - * then every _northing_ component of each active cell, - * and finally every _upward_ component of each active cell. - - About the sensitivity matrix - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - Each row of the sensitivity matrix corresponds to a single receiver - location. - - If ``scalar_model`` is True, then each element of the row will - correspond to the partial derivative of the tmi - with respect to the susceptibility of each cell in the mesh. - - If ``scalar_model`` is False, then each row can be split in three sections - containing: - - * the partial derivatives of the tmi with respect - to the _x_ component of the effective susceptibility of each cell; then - * the partial derivatives of the tmi with respect - to the _y_ component of the effective susceptibility of each cell; and then - * the partial derivatives of the tmi with respect - to the _z_ component of the effective susceptibility of each cell. - - So, if we call :math:`T_j` the tmi on the receiver - :math:`j`, and :math:`\bar{\chi}^{(i)} = (\chi_x^{(i)}, \chi_y^{(i)}, - \chi_z^{(i)})` the effective susceptibility of the active cell :math:`i`, - then each row of the sensitivity matrix will be: - - .. math:: - - \left[ - \frac{\partial T_j}{\partial \chi_x^{(1)}}, - \dots, - \frac{\partial T_j}{\partial \chi_x^{(N)}}, - \frac{\partial T_j}{\partial \chi_y^{(1)}}, - \dots, - \frac{\partial T_j}{\partial \chi_y^{(N)}}, - \frac{\partial T_j}{\partial \chi_z^{(1)}}, - \dots, - \frac{\partial T_j}{\partial \chi_z^{(N)}} - \right] - - where :math:`N` is the total number of active cells. - """ - fx, fy, fz = regional_field - regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) - fx /= regional_field_amplitude - fy /= regional_field_amplitude - fz /= regional_field_amplitude - - constant_factor = 1 / 4 / np.pi - - # Fill the sensitivity matrix - n_receivers = receivers.shape[0] - n_cells = cells_bounds.shape[0] - for i in prange(n_receivers): - for j in range(n_cells): - # Evaluate kernels for the current cell and receiver - uxx, uyy, uzz = evaluate_kernels_on_cell( - receivers[i, 0], - receivers[i, 1], - receivers[i, 2], - cells_bounds[j, 0], - cells_bounds[j, 1], - cells_bounds[j, 2], - cells_bounds[j, 3], - bottom[j], - top[j], - choclo.prism.kernel_ee, - choclo.prism.kernel_nn, - choclo.prism.kernel_uu, - ) - uxy, uxz, uyz = evaluate_kernels_on_cell( - receivers[i, 0], - receivers[i, 1], - receivers[i, 2], - cells_bounds[j, 0], - cells_bounds[j, 1], - cells_bounds[j, 2], - cells_bounds[j, 3], - bottom[j], - top[j], - choclo.prism.kernel_en, - choclo.prism.kernel_eu, - choclo.prism.kernel_nu, - ) - bx = uxx * fx + uxy * fy + uxz * fz - by = uxy * fx + uyy * fy + uyz * fz - bz = uxz * fx + uyz * fy + uzz * fz - if scalar_model: - sensitivity_matrix[i, j] = ( - constant_factor - * regional_field_amplitude - * (bx * fx + by * fy + bz * fz) - ) - else: - sensitivity_matrix[i, j] = ( - constant_factor * regional_field_amplitude * bx - ) - sensitivity_matrix[i, j + n_cells] = ( - constant_factor * regional_field_amplitude * by - ) - sensitivity_matrix[i, j + 2 * n_cells] = ( - constant_factor * regional_field_amplitude * bz - ) - - -def _sensitivity_tmi_derivative_2d_mesh( - receivers, - cells_bounds, - top, - bottom, - sensitivity_matrix, - regional_field, - kernel_xx, - kernel_yy, - kernel_zz, - kernel_xy, - kernel_xz, - kernel_yz, - scalar_model, -): - r""" - Fill the sensitivity matrix TMI for 2d meshes. - - This function is designed to be used with equivalent sources, where the - mesh is a 2D mesh (prism layer). The top and bottom boundaries of each cell - are passed through the ``top`` and ``bottom`` arrays. - - This function should be used with a `numba.jit` decorator, for example: - - .. code:: - - from numba import jit - - jit_tmi = jit(nopython=True, parallel=True)(_sensitivity_tmi_2d_mesh) - - Parameters - ---------- - receivers : (n_receivers, 3) numpy.ndarray - Array with the locations of the receivers - cells_bounds : (n_active_cells, 4) numpy.ndarray - Array with the bounds of each active cell in the 2D mesh. For each row, the - bounds should be passed in the following order: ``x_min``, ``x_max``, - ``y_min``, ``y_max``. - top : (n_active_cells) np.ndarray - Array with the top boundaries of each active cell in the 2D mesh. - bottom : (n_active_cells) np.ndarray - Array with the bottom boundaries of each active cell in the 2D mesh. - sensitivity_matrix : array - Empty 2d array where the sensitivity matrix elements will be filled. - This could be a preallocated empty array or a slice of it. - The array should have a shape of ``(n_receivers, n_active_cells)`` - if ``scalar_model`` is True. - The array should have a shape of ``(n_receivers, 3 * n_active_cells)`` - if ``scalar_model`` is False. - regional_field : (3,) array - Array containing the x, y and z components of the regional magnetic - field (uniform background field). - kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz : callables - Kernel functions used for computing the desired TMI derivative. - scalar_model : bool - If True, the sensitivity matrix is build to work with scalar models - (susceptibilities). - If False, the sensitivity matrix is build to work with vector models - (effective susceptibilities). - - Notes - ----- - - About the model array - ^^^^^^^^^^^^^^^^^^^^^ - - The ``model`` must always be a 1d array: - - * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with - the same number of elements as active cells in the mesh. It should store - the magnetic susceptibilities of each active cell in SI units. - * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array - with a number of elements equal to three times the active cells in the - mesh. It should store the components of the magnetization vector of each - active cell in :math:`Am^{-1}`. The order in which the components should - be passed are: - * every _easting_ component of each active cell, - * then every _northing_ component of each active cell, - * and finally every _upward_ component of each active cell. - """ - fx, fy, fz = regional_field - regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) - fx /= regional_field_amplitude - fy /= regional_field_amplitude - fz /= regional_field_amplitude - - constant_factor = 1 / 4 / np.pi - - # Fill the sensitivity matrix - n_receivers = receivers.shape[0] - n_cells = cells_bounds.shape[0] - for i in prange(n_receivers): - for j in range(n_cells): - # Evaluate kernels for the current cell and receiver - uxx, uyy, uzz = evaluate_kernels_on_cell( - receivers[i, 0], - receivers[i, 1], - receivers[i, 2], - cells_bounds[j, 0], - cells_bounds[j, 1], - cells_bounds[j, 2], - cells_bounds[j, 3], - bottom[j], - top[j], - kernel_xx, - kernel_yy, - kernel_zz, - ) - uxy, uxz, uyz = evaluate_kernels_on_cell( - receivers[i, 0], - receivers[i, 1], - receivers[i, 2], - cells_bounds[j, 0], - cells_bounds[j, 1], - cells_bounds[j, 2], - cells_bounds[j, 3], - bottom[j], - top[j], - kernel_xy, - kernel_xz, - kernel_yz, - ) - bx = uxx * fx + uxy * fy + uxz * fz - by = uxy * fx + uyy * fy + uyz * fz - bz = uxz * fx + uyz * fy + uzz * fz - if scalar_model: - sensitivity_matrix[i, j] = ( - constant_factor - * regional_field_amplitude - * (bx * fx + by * fy + bz * fz) - ) - else: - sensitivity_matrix[i, j] = ( - constant_factor * regional_field_amplitude * bx - ) - sensitivity_matrix[i, j + n_cells] = ( - constant_factor * regional_field_amplitude * by - ) - sensitivity_matrix[i, j + 2 * n_cells] = ( - constant_factor * regional_field_amplitude * bz - ) - - -NUMBA_FUNCTIONS = { +NUMBA_FUNCTIONS_3D = { "forward": { "tmi": { parallel: jit(nopython=True, parallel=parallel)(_forward_tmi) @@ -3321,38 +2389,6 @@ def _sensitivity_tmi_derivative_2d_mesh( for parallel in (True, False) }, }, - "forward_2d_mesh": { - "tmi": { - parallel: jit(nopython=True, parallel=parallel)(_forward_tmi_2d_mesh) - for parallel in (True, False) - }, - "magnetic_component": { - parallel: jit(nopython=True, parallel=parallel)(_forward_mag_2d_mesh) - for parallel in (True, False) - }, - "tmi_derivative": { - parallel: jit(nopython=True, parallel=parallel)( - _forward_tmi_derivative_2d_mesh - ) - for parallel in (True, False) - }, - }, - "sensitivity_2d_mesh": { - "tmi": { - parallel: jit(nopython=True, parallel=parallel)(_sensitivity_tmi_2d_mesh) - for parallel in (True, False) - }, - "magnetic_component": { - parallel: jit(nopython=True, parallel=parallel)(_sensitivity_mag_2d_mesh) - for parallel in (True, False) - }, - "tmi_derivative": { - parallel: jit(nopython=True, parallel=parallel)( - _sensitivity_tmi_derivative_2d_mesh - ) - for parallel in (True, False) - }, - }, "gt_dot_v": { "tmi": { False: _tmi_sensitivity_t_dot_v_serial, diff --git a/simpeg/potential_fields/magnetics/_numba/__init__.py b/simpeg/potential_fields/magnetics/_numba/__init__.py new file mode 100644 index 0000000000..b0acae3ea3 --- /dev/null +++ b/simpeg/potential_fields/magnetics/_numba/__init__.py @@ -0,0 +1,14 @@ +""" +Numba functions for magnetic simulations. +""" + +from ._2d_mesh import NUMBA_FUNCTIONS_2D +from ._3d_mesh import NUMBA_FUNCTIONS_3D + +try: + import choclo +except ImportError: + choclo = None + + +__all__ = ["choclo", "NUMBA_FUNCTIONS_3D", "NUMBA_FUNCTIONS_2D"] diff --git a/simpeg/potential_fields/magnetics/simulation.py b/simpeg/potential_fields/magnetics/simulation.py index e789bb5a32..099ae0b2a8 100644 --- a/simpeg/potential_fields/magnetics/simulation.py +++ b/simpeg/potential_fields/magnetics/simulation.py @@ -25,7 +25,7 @@ from .analytics import CongruousMagBC from .survey import Survey -from ._numba_functions import choclo, NUMBA_FUNCTIONS +from ._numba import choclo, NUMBA_FUNCTIONS_3D, NUMBA_FUNCTIONS_2D if choclo is not None: CHOCLO_SUPPORTED_COMPONENTS = { @@ -882,7 +882,7 @@ def _forward(self, model): index_offset + i, index_offset + n_rows, n_components ) if component == "tmi": - forward_func = NUMBA_FUNCTIONS["forward"]["tmi"][ + forward_func = NUMBA_FUNCTIONS_3D["forward"]["tmi"][ self.numba_parallel ] forward_func( @@ -899,7 +899,7 @@ def _forward(self, model): kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz = ( CHOCLO_KERNELS[component] ) - forward_func = NUMBA_FUNCTIONS["forward"]["tmi_derivative"][ + forward_func = NUMBA_FUNCTIONS_3D["forward"]["tmi_derivative"][ self.numba_parallel ] forward_func( @@ -920,7 +920,7 @@ def _forward(self, model): ) else: kernel_x, kernel_y, kernel_z = CHOCLO_KERNELS[component] - forward_func = NUMBA_FUNCTIONS["forward"]["magnetic_component"][ + forward_func = NUMBA_FUNCTIONS_3D["forward"]["magnetic_component"][ self.numba_parallel ] forward_func( @@ -982,7 +982,7 @@ def _sensitivity_matrix(self): index_offset + i, index_offset + n_rows, n_components ) if component == "tmi": - sensitivity_func = NUMBA_FUNCTIONS["sensitivity"]["tmi"][ + sensitivity_func = NUMBA_FUNCTIONS_3D["sensitivity"]["tmi"][ self.numba_parallel ] sensitivity_func( @@ -995,9 +995,9 @@ def _sensitivity_matrix(self): scalar_model, ) elif component in ("tmi_x", "tmi_y", "tmi_z"): - sensitivity_func = NUMBA_FUNCTIONS["sensitivity"]["tmi_derivative"][ - self.numba_parallel - ] + sensitivity_func = NUMBA_FUNCTIONS_3D["sensitivity"][ + "tmi_derivative" + ][self.numba_parallel] kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz = ( CHOCLO_KERNELS[component] ) @@ -1017,7 +1017,7 @@ def _sensitivity_matrix(self): scalar_model, ) else: - sensitivity_func = NUMBA_FUNCTIONS["sensitivity"][ + sensitivity_func = NUMBA_FUNCTIONS_3D["sensitivity"][ "magnetic_component" ][self.numba_parallel] kernel_x, kernel_y, kernel_z = CHOCLO_KERNELS[component] @@ -1094,7 +1094,7 @@ def _sensitivity_matrix_transpose_dot_vec(self, vector): index_offset + i, index_offset + n_rows, n_components ) if component == "tmi": - gt_dot_v_func = NUMBA_FUNCTIONS["gt_dot_v"]["tmi"][ + gt_dot_v_func = NUMBA_FUNCTIONS_3D["gt_dot_v"]["tmi"][ self.numba_parallel ] gt_dot_v_func( @@ -1111,7 +1111,7 @@ def _sensitivity_matrix_transpose_dot_vec(self, vector): kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz = ( CHOCLO_KERNELS[component] ) - gt_dot_v_func = NUMBA_FUNCTIONS["gt_dot_v"]["tmi_derivative"][ + gt_dot_v_func = NUMBA_FUNCTIONS_3D["gt_dot_v"]["tmi_derivative"][ self.numba_parallel ] gt_dot_v_func( @@ -1132,9 +1132,9 @@ def _sensitivity_matrix_transpose_dot_vec(self, vector): ) else: kernel_x, kernel_y, kernel_z = CHOCLO_KERNELS[component] - gt_dot_v_func = NUMBA_FUNCTIONS["gt_dot_v"]["magnetic_component"][ - self.numba_parallel - ] + gt_dot_v_func = NUMBA_FUNCTIONS_3D["gt_dot_v"][ + "magnetic_component" + ][self.numba_parallel] gt_dot_v_func( receivers, active_nodes, @@ -1186,7 +1186,7 @@ def _gtg_diagonal_without_building_g(self, weights): ) for component in components: if component == "tmi": - diagonal_gtg_func = NUMBA_FUNCTIONS["diagonal_gtg"]["tmi"][ + diagonal_gtg_func = NUMBA_FUNCTIONS_3D["diagonal_gtg"]["tmi"][ self.numba_parallel ] diagonal_gtg_func( @@ -1203,7 +1203,7 @@ def _gtg_diagonal_without_building_g(self, weights): kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz = ( CHOCLO_KERNELS[component] ) - diagonal_gtg_func = NUMBA_FUNCTIONS["diagonal_gtg"][ + diagonal_gtg_func = NUMBA_FUNCTIONS_3D["diagonal_gtg"][ "tmi_derivative" ][self.numba_parallel] diagonal_gtg_func( @@ -1224,7 +1224,7 @@ def _gtg_diagonal_without_building_g(self, weights): ) else: kernel_x, kernel_y, kernel_z = CHOCLO_KERNELS[component] - diagonal_gtg_func = NUMBA_FUNCTIONS["diagonal_gtg"][ + diagonal_gtg_func = NUMBA_FUNCTIONS_3D["diagonal_gtg"][ "magnetic_component" ][self.numba_parallel] diagonal_gtg_func( @@ -1329,7 +1329,7 @@ def _forward(self, model): index_offset + i, index_offset + n_rows, n_components ) if component == "tmi": - forward_func = NUMBA_FUNCTIONS["forward_2d_mesh"]["tmi"][ + forward_func = NUMBA_FUNCTIONS_2D["forward"]["tmi"][ self.numba_parallel ] forward_func( @@ -1346,7 +1346,7 @@ def _forward(self, model): kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz = ( CHOCLO_KERNELS[component] ) - forward_func = NUMBA_FUNCTIONS["forward_2d_mesh"]["tmi_derivative"][ + forward_func = NUMBA_FUNCTIONS_2D["forward"]["tmi_derivative"][ self.numba_parallel ] forward_func( @@ -1367,9 +1367,9 @@ def _forward(self, model): ) else: choclo_forward_func = CHOCLO_FORWARD_FUNCS[component] - forward_func = NUMBA_FUNCTIONS["forward_2d_mesh"][ - "magnetic_component" - ][self.numba_parallel] + forward_func = NUMBA_FUNCTIONS_2D["forward"]["magnetic_component"][ + self.numba_parallel + ] forward_func( receivers, cells_bounds_active, @@ -1425,7 +1425,7 @@ def _sensitivity_matrix(self): index_offset + i, index_offset + n_rows, n_components ) if component == "tmi": - sensitivity_func = NUMBA_FUNCTIONS["sensitivity_2d_mesh"]["tmi"][ + sensitivity_func = NUMBA_FUNCTIONS_2D["sensitivity"]["tmi"][ self.numba_parallel ] sensitivity_func( @@ -1441,7 +1441,7 @@ def _sensitivity_matrix(self): kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz = ( CHOCLO_KERNELS[component] ) - sensitivity_func = NUMBA_FUNCTIONS["sensitivity_2d_mesh"][ + sensitivity_func = NUMBA_FUNCTIONS_2D["sensitivity"][ "tmi_derivative" ][self.numba_parallel] sensitivity_func( @@ -1461,7 +1461,7 @@ def _sensitivity_matrix(self): ) else: kernel_x, kernel_y, kernel_z = CHOCLO_KERNELS[component] - sensitivity_func = NUMBA_FUNCTIONS["sensitivity_2d_mesh"][ + sensitivity_func = NUMBA_FUNCTIONS_2D["sensitivity"][ "magnetic_component" ][self.numba_parallel] sensitivity_func( @@ -1479,6 +1479,190 @@ def _sensitivity_matrix(self): index_offset += n_rows return sensitivity_matrix + def _sensitivity_matrix_transpose_dot_vec(self, vector): + """ + Compute ``G.T @ v`` without building ``G``. + + Parameters + ---------- + vector : (nD) numpy.ndarray + Vector used in the dot product. + + Returns + ------- + (n_active_cells) or (3 * n_active_cells) numpy.ndarray + """ + # Get regional field + regional_field = self.survey.source_field.b0 + # Get cells in the 2D mesh and keep only active cells + cells_bounds_active = self.mesh.cell_bounds[self.active_cells] + # Allocate resulting array + scalar_model = self.model_type == "scalar" + result = np.zeros(self.nC if scalar_model else 3 * self.nC) + # Start filling the result array + index_offset = 0 + for components, receivers in self._get_components_and_receivers(): + if not CHOCLO_SUPPORTED_COMPONENTS.issuperset(components): + raise NotImplementedError( + f"Other components besides {CHOCLO_SUPPORTED_COMPONENTS} " + "aren't implemented yet." + ) + n_components = len(components) + n_rows = n_components * receivers.shape[0] + for i, component in enumerate(components): + vector_slice = slice( + index_offset + i, index_offset + n_rows, n_components + ) + if component == "tmi": + gt_dot_v_func = NUMBA_FUNCTIONS_2D["gt_dot_v"]["tmi"][ + self.numba_parallel + ] + gt_dot_v_func( + receivers, + cells_bounds_active, + self.cell_z_top, + self.cell_z_bottom, + regional_field, + scalar_model, + vector[vector_slice], + result, + ) + elif component in ("tmi_x", "tmi_y", "tmi_z"): + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz = ( + CHOCLO_KERNELS[component] + ) + gt_dot_v_func = NUMBA_FUNCTIONS_2D["gt_dot_v"]["tmi_derivative"][ + self.numba_parallel + ] + gt_dot_v_func( + receivers, + cells_bounds_active, + self.cell_z_top, + self.cell_z_bottom, + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + scalar_model, + vector[vector_slice], + result, + ) + else: + kernel_x, kernel_y, kernel_z = CHOCLO_KERNELS[component] + gt_dot_v_func = NUMBA_FUNCTIONS_2D["gt_dot_v"][ + "magnetic_component" + ][self.numba_parallel] + gt_dot_v_func( + receivers, + cells_bounds_active, + self.cell_z_top, + self.cell_z_bottom, + regional_field, + kernel_x, + kernel_y, + kernel_z, + scalar_model, + vector[vector_slice], + result, + ) + index_offset += n_rows + return result + + def _gtg_diagonal_without_building_g(self, weights): + """ + Compute the diagonal of ``G.T @ G`` without building the ``G`` matrix. + + Parameters + ----------- + weights : (nD,) array + Array with data weights. It should be the diagonal of the ``W`` + matrix, squared. + + Returns + ------- + (n_active_cells) numpy.ndarray + """ + # Get regional field + regional_field = self.survey.source_field.b0 + # Get cells in the 2D mesh and keep only active cells + cells_bounds_active = self.mesh.cell_bounds[self.active_cells] + # Define the constant factor + constant_factor = 1 / 4 / np.pi + # Allocate array for the diagonal + scalar_model = self.model_type == "scalar" + n_columns = self.nC if scalar_model else 3 * self.nC + diagonal = np.zeros(n_columns, dtype=np.float64) + # Start filling the diagonal array + for components, receivers in self._get_components_and_receivers(): + if not CHOCLO_SUPPORTED_COMPONENTS.issuperset(components): + raise NotImplementedError( + f"Other components besides {CHOCLO_SUPPORTED_COMPONENTS} " + "aren't implemented yet." + ) + for component in components: + if component == "tmi": + diagonal_gtg_func = NUMBA_FUNCTIONS_2D["diagonal_gtg"]["tmi"][ + self.numba_parallel + ] + diagonal_gtg_func( + receivers, + cells_bounds_active, + self.cell_z_top, + self.cell_z_bottom, + regional_field, + constant_factor, + scalar_model, + weights, + diagonal, + ) + elif component in ("tmi_x", "tmi_y", "tmi_z"): + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz = ( + CHOCLO_KERNELS[component] + ) + diagonal_gtg_func = NUMBA_FUNCTIONS_2D["diagonal_gtg"][ + "tmi_derivative" + ][self.numba_parallel] + diagonal_gtg_func( + receivers, + cells_bounds_active, + self.cell_z_top, + self.cell_z_bottom, + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + constant_factor, + scalar_model, + weights, + diagonal, + ) + else: + kernel_x, kernel_y, kernel_z = CHOCLO_KERNELS[component] + diagonal_gtg_func = NUMBA_FUNCTIONS_2D["diagonal_gtg"][ + "magnetic_component" + ][self.numba_parallel] + diagonal_gtg_func( + receivers, + cells_bounds_active, + self.cell_z_top, + self.cell_z_bottom, + regional_field, + kernel_x, + kernel_y, + kernel_z, + constant_factor, + scalar_model, + weights, + diagonal, + ) + return diagonal + class Simulation3DDifferential(BaseMagneticPDESimulation): """ diff --git a/tests/pf/test_equivalent_sources.py b/tests/pf/test_equivalent_sources.py index 5596933cd2..bbcbcd2b81 100644 --- a/tests/pf/test_equivalent_sources.py +++ b/tests/pf/test_equivalent_sources.py @@ -25,6 +25,13 @@ "tmi_z", ] +# Define a pytest.mark.xfail to use for engine parametrizations when the method that is +# being tested is not implemented when using geoana as engine. +XFAIL_GEOANA = pytest.param( + "geoana", + marks=pytest.mark.xfail(reason="not implemented", raises=NotImplementedError), +) + def create_grid(x_range, y_range, size): """Create a 2D horizontal coordinates grid.""" @@ -462,10 +469,10 @@ class TestMagneticEquivalentSourcesForward: Test the forward capabilities of the magnetic equivalent sources. """ - @pytest.mark.parametrize("engine", ("geoana", "choclo")) - @pytest.mark.parametrize("store_sensitivities", ("ram", "forward_only")) - @pytest.mark.parametrize("model_type", ("scalar", "vector")) - @pytest.mark.parametrize("components", MAGNETIC_COMPONENTS + [["tmi", "bx"]]) + @pytest.mark.parametrize("engine", ["geoana", "choclo"]) + @pytest.mark.parametrize("store_sensitivities", ["ram", "forward_only"]) + @pytest.mark.parametrize("model_type", ["scalar", "vector"]) + @pytest.mark.parametrize("components", [*MAGNETIC_COMPONENTS, ["tmi", "bx"]]) def test_forward_vs_simulation( self, coordinates, @@ -840,3 +847,162 @@ def test_predictions_on_data_points( np.testing.assert_allclose( prediction, synthetic_data.dobs, atol=atol, rtol=rtol ) + + +@pytest.mark.parametrize("parallel", [True, False], ids=["parallel", "serial"]) +@pytest.mark.parametrize("components", [*MAGNETIC_COMPONENTS, ["tmi", "bx"]]) +@pytest.mark.parametrize("engine", ["choclo", XFAIL_GEOANA]) +@pytest.mark.parametrize("model_type", ["scalar", "vector"]) +class TestMagneticEquivalentSourcesForwardOnly: + """ + Test magnetic equivalent sources methods without building the sensitivity matrix. + """ + + def test_Jvec( + self, + coordinates, + tensor_mesh, + mesh_bottom, + mesh_top, + components, + engine, + parallel, + model_type, + ): + """ + Test Jvec with "forward_only" vs. J @ v with J stored in ram. + """ + # Build survey + magnetic_survey = build_magnetic_survey(coordinates, components) + # Define model + model = ( + get_block_model(tensor_mesh, 0.2e-3) + if model_type == "scalar" + else get_block_model(tensor_mesh, (0.2e-3, -0.1e-3, 0.5e-3)) + ) + # Build simulations + mapping = simpeg.maps.IdentityMap(nP=model.size) + eqs_ram, eqs_forward_only = ( + magnetics.SimulationEquivalentSourceLayer( + tensor_mesh, + mesh_top, + mesh_bottom, + survey=magnetic_survey, + chiMap=mapping, + engine=engine, + store_sensitivities=store, + numba_parallel=parallel, + model_type=model_type, + ) + for store in ("ram", "forward_only") + ) + # Compare predictions of both simulations + vector = np.random.default_rng(seed=42).uniform(size=model.size) + expected = eqs_ram.getJ(model) @ vector + atol = np.max(np.abs(expected)) * 1e-7 + # Test Jvec + np.testing.assert_allclose( + expected, eqs_forward_only.Jvec(model, vector), atol=atol + ) + # Test getJ() @ v + jacobian = eqs_forward_only.getJ(model) + np.testing.assert_allclose(expected, jacobian @ vector, atol=atol) + + def test_Jtvec( + self, + coordinates, + tensor_mesh, + mesh_bottom, + mesh_top, + components, + engine, + parallel, + model_type, + ): + """ + Test Jtvec with "forward_only" vs. J.T @ v with J stored in ram. + """ + # Build survey + magnetic_survey = build_magnetic_survey(coordinates, components) + # Define model + model = ( + get_block_model(tensor_mesh, 0.2e-3) + if model_type == "scalar" + else get_block_model(tensor_mesh, (0.2e-3, -0.1e-3, 0.5e-3)) + ) + # Build simulations + mapping = simpeg.maps.IdentityMap(nP=model.size) + eqs_ram, eqs_forward_only = ( + magnetics.SimulationEquivalentSourceLayer( + tensor_mesh, + mesh_top, + mesh_bottom, + survey=magnetic_survey, + chiMap=mapping, + engine=engine, + store_sensitivities=store, + numba_parallel=parallel, + model_type=model_type, + ) + for store in ("ram", "forward_only") + ) + # Compare predictions of both simulations + vector = np.random.default_rng(seed=42).uniform(size=magnetic_survey.nD) + expected = eqs_ram.getJ(model).T @ vector + atol = np.max(np.abs(expected)) * 1e-7 + # Test Jtvec + np.testing.assert_allclose( + expected, eqs_forward_only.Jtvec(model, vector), atol=atol + ) + # Test getJ().T @ v + jacobian = eqs_forward_only.getJ(model) + np.testing.assert_allclose(expected, jacobian.T @ vector, atol=atol) + + def test_getJtJdiag( + self, + coordinates, + tensor_mesh, + mesh_bottom, + mesh_top, + components, + engine, + parallel, + model_type, + ): + """ + Test the ``getJtJdiag`` method, comparing forward_only with storing J in memory. + """ + # Build survey + magnetic_survey = build_magnetic_survey(coordinates, components) + # Define model + model = ( + get_block_model(tensor_mesh, 0.2e-3) + if model_type == "scalar" + else get_block_model(tensor_mesh, (0.2e-3, -0.1e-3, 0.5e-3)) + ) + # Build simulations + mapping = simpeg.maps.IdentityMap(nP=model.size) + eqs_ram, eqs_forward_only = ( + magnetics.SimulationEquivalentSourceLayer( + tensor_mesh, + mesh_top, + mesh_bottom, + survey=magnetic_survey, + chiMap=mapping, + engine=engine, + store_sensitivities=store, + numba_parallel=parallel, + model_type=model_type, + ) + for store in ("ram", "forward_only") + ) + # Compare methods for both simulations + model = ( + get_block_model(tensor_mesh, 0.2e-3) + if model_type == "scalar" + else get_block_model(tensor_mesh, (0.2e-3, -0.1e-3, 0.5e-3)) + ) + gtgdiag_ram = eqs_ram.getJtJdiag(model) + gtgdiag_linop = eqs_forward_only.getJtJdiag(model) + atol = np.max(np.abs(gtgdiag_ram)) * 1e-7 + np.testing.assert_allclose(gtgdiag_ram, gtgdiag_linop, atol=atol) From 8f743799e6bfdba709e533d28922150a7290c359 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Tue, 3 Jun 2025 10:00:16 -0600 Subject: [PATCH 147/194] Fix beta cooling in `UpdateIRLS` directive (#1659) Fixes beta cooling strategy, and makes behavior closer to previous implementation. --- simpeg/directives/_regularization.py | 21 ++++++++------- tests/base/test_directives.py | 26 ++++++++++++++++--- .../05-dcr/plot_inv_1_dcr_sounding_irls.py | 5 +++- 3 files changed, 38 insertions(+), 14 deletions(-) diff --git a/simpeg/directives/_regularization.py b/simpeg/directives/_regularization.py index c1f49e5ad8..6afe17d172 100644 --- a/simpeg/directives/_regularization.py +++ b/simpeg/directives/_regularization.py @@ -229,19 +229,20 @@ def adjust_cooling_schedule(self): """ Adjust the cooling schedule based on the misfit. """ - ratio = self.invProb.phi_d / self.misfit_from_chi_factor(self.chifact_target) + if self.metrics.start_irls_iter is not None: + ratio = self.invProb.phi_d / self.misfit_from_chi_factor( + self.chifact_target + ) + if np.abs(1.0 - ratio) > self.misfit_tolerance: - if ( - np.abs(1.0 - ratio) > self.misfit_tolerance - and self.metrics.start_irls_iter is not None - ): + if ratio > 1: + update_ratio = 1 / np.mean([0.75, 1 / ratio]) + else: + update_ratio = 1 / np.mean([2.0, 1 / ratio]) - if ratio > 1: - ratio = np.mean([2.0, ratio]) + self.cooling_factor = update_ratio else: - ratio = np.mean([0.75, ratio]) - - self.cooling_factor = ratio + self.cooling_factor = 1.0 def initialize(self): """ diff --git a/tests/base/test_directives.py b/tests/base/test_directives.py index 65fbb0f7f2..cee2882959 100644 --- a/tests/base/test_directives.py +++ b/tests/base/test_directives.py @@ -1,4 +1,6 @@ import unittest +from statistics import harmonic_mean + import pytest import numpy as np @@ -342,16 +344,34 @@ def test_irls_directive(self): irls_directive.max_irls_iterations = 2 assert irls_directive.stopping_criteria() + expected_target = self.dmis.nD # Test beta re-adjustment down invProb.phi_d = 4.0 irls_directive.misfit_tolerance = 0.1 irls_directive.adjust_cooling_schedule() - assert irls_directive.cooling_factor == 2.0 + + ratio = invProb.phi_d / expected_target + expected_factor = harmonic_mean([4 / 3, ratio]) + np.testing.assert_allclose(irls_directive.cooling_factor, expected_factor) # Test beta re-adjustment up - invProb.phi_d = 0.5 + invProb.phi_d = 1 / 2 + ratio = invProb.phi_d / expected_target + expected_factor = harmonic_mean([1 / 2, ratio]) + + irls_directive.adjust_cooling_schedule() + np.testing.assert_allclose(irls_directive.cooling_factor, expected_factor) + + # Test beta no-adjustment + irls_directive.cooling_factor = ( + 2.0 # set this to something not 1 to make sure it changes to 1. + ) + + invProb.phi_d = expected_target * ( + 1 + irls_directive.misfit_tolerance * 0.5 + ) # something within the relative tolerance irls_directive.adjust_cooling_schedule() - assert irls_directive.cooling_factor == 0.5 + assert irls_directive.cooling_factor == 1 def test_spherical_weights(self): reg = regularization.Sparse(self.mesh) diff --git a/tutorials/05-dcr/plot_inv_1_dcr_sounding_irls.py b/tutorials/05-dcr/plot_inv_1_dcr_sounding_irls.py index a9e751bbb4..c8673c1f00 100644 --- a/tutorials/05-dcr/plot_inv_1_dcr_sounding_irls.py +++ b/tutorials/05-dcr/plot_inv_1_dcr_sounding_irls.py @@ -237,7 +237,10 @@ # Define how the optimization problem is solved. Here we will use an inexact # Gauss-Newton approach that employs the conjugate gradient solver. -opt = optimization.ProjectedGNCG(maxIter=100, maxIterLS=20, maxIterCG=20, tolCG=1e-3) +opt = optimization.ProjectedGNCG( + maxIter=100, maxIterLS=50, maxIterCG=20, tolCG=1e-3, upper=1e2, lower=1e-2 +) +# limits here are used to guard against overflow and underflow in the ExpMap. # Define the inverse problem inv_prob = inverse_problem.BaseInvProblem(dmis, reg, opt) From ce7fa0321cdc9fb6142fd833637fc98d7a6b2144 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 4 Jun 2025 16:42:01 +0000 Subject: [PATCH 148/194] Allow use of `J` as `LinearOperator` in gravity equivalent layers (#1674) Add Numba functions to compute `G.T @ v` and the diagonal of `G.T @ G` for the gravity equivalent sources without storing the `G` matrix in memory. Add needed private methods in the `SimulationEquivalentSourceLayer`. Reorganize the private `_numba_functions.py` file into a `_numba` submodule with files for the functions related to the 3D and 2D meshes. Add tests for the new functions. --- .../gravity/_numba/_2d_mesh.py | 455 ++++++++++++++++++ .../_3d_mesh.py} | 155 +----- .../gravity/_numba/__init__.py | 13 + simpeg/potential_fields/gravity/simulation.py | 98 +++- .../potential_fields/magnetics/simulation.py | 2 +- tests/pf/test_equivalent_sources.py | 134 ++++++ 6 files changed, 695 insertions(+), 162 deletions(-) create mode 100644 simpeg/potential_fields/gravity/_numba/_2d_mesh.py rename simpeg/potential_fields/gravity/{_numba_functions.py => _numba/_3d_mesh.py} (75%) create mode 100644 simpeg/potential_fields/gravity/_numba/__init__.py diff --git a/simpeg/potential_fields/gravity/_numba/_2d_mesh.py b/simpeg/potential_fields/gravity/_numba/_2d_mesh.py new file mode 100644 index 0000000000..06cb489a37 --- /dev/null +++ b/simpeg/potential_fields/gravity/_numba/_2d_mesh.py @@ -0,0 +1,455 @@ +""" +Numba functions for gravity simulation on 2D meshes. + +These functions assumes 3D prisms formed by a 2D mesh plus top and bottom boundaries for +each prism. +""" + +import numpy as np + +try: + import choclo +except ImportError: + # Define dummy jit decorator + def jit(*args, **kwargs): + return lambda f: f + + choclo = None +else: + from numba import jit, prange + + +def _forward_gravity( + receivers, + cells_bounds, + top, + bottom, + densities, + fields, + forward_func, + constant_factor, +): + """ + Forward gravity fields of 2D meshes. + + This function is designed to be used with equivalent sources, where the + mesh is a 2D mesh (prism layer). The top and bottom boundaries of each cell + are passed through the ``top`` and ``bottom`` arrays. + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_function = jit(nopython=True, parallel=True)(_forward_gravity) + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + densities : (n_active_cells) numpy.ndarray + Array with densities of each active cell in the mesh. + fields : (n_receivers) numpy.ndarray + Array full of zeros where the gravity fields on each receiver will be + stored. This could be a preallocated array or a slice of it. + forward_func : callable + Forward function that will be evaluated on each node of the mesh. Choose + one of the forward functions in ``choclo.prism``. + constant_factor : float + Constant factor that will be used to multiply each element of the + ``fields`` array. + + Notes + ----- + The constant factor is applied here to each element of fields because + it's more efficient than doing it afterwards: it would require to + index the elements that corresponds to each component. + """ + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + # Forward model the gravity field of each cell on each receiver location + for i in prange(n_receivers): + for j in range(n_cells): + fields[i] += constant_factor * forward_func( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + densities[j], + ) + + +def _sensitivity_gravity( + receivers, + cells_bounds, + top, + bottom, + sensitivity_matrix, + forward_func, + constant_factor, +): + """ + Fill the sensitivity matrix + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_function = jit(nopython=True, parallel=True)(_sensitivity_gravity) + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + sensitivity_matrix : (n_receivers, n_active_nodes) array + Empty 2d array where the sensitivity matrix elements will be filled. + This could be a preallocated empty array or a slice of it. + forward_func : callable + Forward function that will be evaluated on each node of the mesh. Choose + one of the forward functions in ``choclo.prism``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + + Notes + ----- + The constant factor is applied here to each row of the sensitivity matrix + because it's more efficient than doing it afterwards: it would require to + index the rows that corresponds to each component. + """ + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + for j in range(n_cells): + sensitivity_matrix[i, j] = constant_factor * forward_func( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + 1.0, # use unitary density to get sensitivities + ) + + +@jit(nopython=True, parallel=False) +def _g_t_dot_v_serial( + receivers, + cells_bounds, + top, + bottom, + forward_func, + constant_factor, + vector, + result, +): + """ + Compute ``G.T @ v`` in serial, without building G, for a 2D mesh. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + forward_func : callable + Forward function that will be evaluated on each node of the mesh. Choose + one of the forward functions in ``choclo.prism``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + vector : (n_receivers) numpy.ndarray + Array that represents the vector used in the dot product. + result : (n_active_cells) numpy.ndarray + Running result array where the output of the dot product will be added to. + + Notes + ----- + This function is meant to be run in serial. Writing to the ``result`` array + inside a parallel loop over the receivers generates a race condition that + leads to corrupted outputs. + """ + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + for i in range(n_receivers): + for j in range(n_cells): + # Compute the i-th row of the sensitivity matrix and multiply it by the + # i-th element of the vector. + result[j] += constant_factor * forward_func( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + vector[i], + ) + + +@jit(nopython=True, parallel=True) +def _g_t_dot_v_parallel( + receivers, + cells_bounds, + top, + bottom, + forward_func, + constant_factor, + vector, + result, +): + """ + Compute ``G.T @ v`` in parallel, without building G, for a 2D mesh. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + forward_func : callable + Forward function that will be evaluated on each node of the mesh. Choose + one of the forward functions in ``choclo.prism``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + vector : (n_receivers) numpy.ndarray + Array that represents the vector used in the dot product. + result : (n_active_cells) numpy.ndarray + Running result array where the output of the dot product will be added to. + + Notes + ----- + This function is meant to be run in parallel. + This implementation instructs each thread to allocate their own array for + the current row of the sensitivity matrix. After computing the elements of + that row, it gets added to the running ``result`` array through a reduction + operation handled by Numba. + """ + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + for i in prange(n_receivers): + # Allocate array for the current row of the sensitivity matrix + local_row = np.empty(n_cells) + for j in range(n_cells): + # Compute the i-th row of the sensitivity matrix and multiply it by the + # i-th element of the vector. + local_row[j] = constant_factor * forward_func( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + vector[i], + ) + # Apply reduction operation to add the values of the row to the running + # result. Avoid slicing the `result` array when updating it to avoid + # racing conditions, just add the `local_row` to the `results` + # variable. + result += local_row + + +@jit(nopython=True, parallel=False) +def _diagonal_G_T_dot_G_serial( + receivers, + cells_bounds, + top, + bottom, + forward_func, + constant_factor, + weights, + diagonal, +): + """ + Diagonal of ``G.T @ W.T @ W @ G`` without storing ``G``, in serial. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + forward_func : callable + Forward function that will be evaluated on each node of the mesh. Choose + one of the forward functions in ``choclo.prism``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + weights : (n_receivers,) numpy.ndarray + Array with data weights. It should be the diagonal of the ``W`` matrix, + squared. + diagonal : (n_active_cells,) numpy.ndarray + Array where the diagonal of ``G.T @ G`` will be added to. + + Notes + ----- + This function is meant to be run in serial. Use the + ``_diagonal_G_T_dot_G_parallel`` one for parallelized computations. + """ + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + # Evaluate kernel function on each node, for each receiver location + for i in range(n_receivers): + for j in range(n_cells): + g_element = constant_factor * forward_func( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + 1.0, # use unitary density to get sensitivities + ) + diagonal[j] += weights[i] * g_element**2 + + +@jit(nopython=True, parallel=True) +def _diagonal_G_T_dot_G_parallel( + receivers, + cells_bounds, + top, + bottom, + forward_func, + constant_factor, + weights, + diagonal, +): + """ + Diagonal of ``G.T @ W.T @ W @ G`` without storing ``G``, in parallel. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + forward_func : callable + Forward function that will be evaluated on each node of the mesh. Choose + one of the forward functions in ``choclo.prism``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + weights : (n_receivers,) numpy.ndarray + Array with data weights. It should be the diagonal of the ``W`` matrix, + squared. + diagonal : (n_active_cells,) numpy.ndarray + Array where the diagonal of ``G.T @ G`` will be added to. + + Notes + ----- + This function is meant to be run in parallel. Use the + ``_diagonal_G_T_dot_G_serial`` one for serialized computations. + + This implementation instructs each thread to allocate their own array for + the diagonal elements of ``G.T @ G`` that correspond to a single receiver. + After computing them, the ``local_diagonal`` array gets added to the running + ``diagonal`` array through a reduction operation handled by Numba. + """ + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate array for the diagonal elements for the current receiver. + local_diagonal = np.empty(n_cells) + for j in range(n_cells): + g_element = constant_factor * forward_func( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + 1.0, # use unitary density to get sensitivities + ) + local_diagonal[j] = weights[i] * g_element**2 + # Add the result to the diagonal. + # Apply reduction operation to add the values of the local diagonal to + # the running diagonal array. Avoid slicing the `diagonal` array when + # updating it to avoid racing conditions, just add the `local_diagonal` + # to the `diagonal` variable. + diagonal += local_diagonal + + +# Define a dictionary with decorated versions of the Numba functions. +NUMBA_FUNCTIONS_2D = { + "sensitivity": { + parallel: jit(nopython=True, parallel=parallel)(_sensitivity_gravity) + for parallel in (True, False) + }, + "forward": { + parallel: jit(nopython=True, parallel=parallel)(_forward_gravity) + for parallel in (True, False) + }, + "gt_dot_v": { + False: _g_t_dot_v_serial, + True: _g_t_dot_v_parallel, + }, + "diagonal_gtg": { + False: _diagonal_G_T_dot_G_serial, + True: _diagonal_G_T_dot_G_parallel, + }, +} diff --git a/simpeg/potential_fields/gravity/_numba_functions.py b/simpeg/potential_fields/gravity/_numba/_3d_mesh.py similarity index 75% rename from simpeg/potential_fields/gravity/_numba_functions.py rename to simpeg/potential_fields/gravity/_numba/_3d_mesh.py index 5b1ecc91a6..996455cfe9 100644 --- a/simpeg/potential_fields/gravity/_numba_functions.py +++ b/simpeg/potential_fields/gravity/_numba/_3d_mesh.py @@ -1,5 +1,5 @@ """ -Numba functions for gravity simulation using Choclo. +Numba functions for gravity simulation on 3D meshes. """ import numpy as np @@ -15,7 +15,7 @@ def jit(*args, **kwargs): else: from numba import jit, prange -from .._numba_utils import kernels_in_nodes_to_cell +from ..._numba_utils import kernels_in_nodes_to_cell def _forward_gravity( @@ -498,149 +498,8 @@ def _evaluate_kernel( return kernel_func(dx, dy, dz, distance) -def _forward_gravity_2d_mesh( - receivers, - cells_bounds, - top, - bottom, - densities, - fields, - forward_func, - constant_factor, -): - """ - Forward gravity fields of 2D meshes. - - This function is designed to be used with equivalent sources, where the - mesh is a 2D mesh (prism layer). The top and bottom boundaries of each cell - are passed through the ``top`` and ``bottom`` arrays. - - This function should be used with a `numba.jit` decorator, for example: - - .. code:: - - from numba import jit - - jit_function = jit(nopython=True, parallel=True)(_forward_gravity_2d_mesh) - - Parameters - ---------- - receivers : (n_receivers, 3) numpy.ndarray - Array with the locations of the receivers - cells_bounds : (n_active_cells, 4) numpy.ndarray - Array with the bounds of each active cell in the 2D mesh. For each row, the - bounds should be passed in the following order: ``x_min``, ``x_max``, - ``y_min``, ``y_max``. - top : (n_active_cells) np.ndarray - Array with the top boundaries of each active cell in the 2D mesh. - bottom : (n_active_cells) np.ndarray - Array with the bottom boundaries of each active cell in the 2D mesh. - densities : (n_active_cells) numpy.ndarray - Array with densities of each active cell in the mesh. - fields : (n_receivers) numpy.ndarray - Array full of zeros where the gravity fields on each receiver will be - stored. This could be a preallocated array or a slice of it. - forward_func : callable - Forward function that will be evaluated on each node of the mesh. Choose - one of the forward functions in ``choclo.prism``. - constant_factor : float - Constant factor that will be used to multiply each element of the - ``fields`` array. - - Notes - ----- - The constant factor is applied here to each element of fields because - it's more efficient than doing it afterwards: it would require to - index the elements that corresponds to each component. - """ - n_receivers = receivers.shape[0] - n_cells = cells_bounds.shape[0] - # Forward model the gravity field of each cell on each receiver location - for i in prange(n_receivers): - for j in range(n_cells): - fields[i] += constant_factor * forward_func( - receivers[i, 0], - receivers[i, 1], - receivers[i, 2], - cells_bounds[j, 0], - cells_bounds[j, 1], - cells_bounds[j, 2], - cells_bounds[j, 3], - bottom[j], - top[j], - densities[j], - ) - - -def _sensitivity_gravity_2d_mesh( - receivers, - cells_bounds, - top, - bottom, - sensitivity_matrix, - forward_func, - constant_factor, -): - """ - Fill the sensitivity matrix - - This function should be used with a `numba.jit` decorator, for example: - - .. code:: - - from numba import jit - - jit_function = jit(nopython=True, parallel=True)(_sensitivity_gravity_2d_mesh) - - Parameters - ---------- - receivers : (n_receivers, 3) numpy.ndarray - Array with the locations of the receivers - cells_bounds : (n_active_cells, 4) numpy.ndarray - Array with the bounds of each active cell in the 2D mesh. For each row, the - bounds should be passed in the following order: ``x_min``, ``x_max``, - ``y_min``, ``y_max``. - top : (n_active_cells) np.ndarray - Array with the top boundaries of each active cell in the 2D mesh. - bottom : (n_active_cells) np.ndarray - Array with the bottom boundaries of each active cell in the 2D mesh. - sensitivity_matrix : (n_receivers, n_active_nodes) array - Empty 2d array where the sensitivity matrix elements will be filled. - This could be a preallocated empty array or a slice of it. - forward_func : callable - Forward function that will be evaluated on each node of the mesh. Choose - one of the forward functions in ``choclo.prism``. - constant_factor : float - Constant factor that will be used to multiply each element of the - sensitivity matrix. - - Notes - ----- - The constant factor is applied here to each row of the sensitivity matrix - because it's more efficient than doing it afterwards: it would require to - index the rows that corresponds to each component. - """ - n_receivers = receivers.shape[0] - n_cells = cells_bounds.shape[0] - # Evaluate kernel function on each node, for each receiver location - for i in prange(n_receivers): - for j in range(n_cells): - sensitivity_matrix[i, j] = constant_factor * forward_func( - receivers[i, 0], - receivers[i, 1], - receivers[i, 2], - cells_bounds[j, 0], - cells_bounds[j, 1], - cells_bounds[j, 2], - cells_bounds[j, 3], - bottom[j], - top[j], - 1.0, # use unitary density to get sensitivities - ) - - # Define a dictionary with decorated versions of the Numba functions. -NUMBA_FUNCTIONS = { +NUMBA_FUNCTIONS_3D = { "sensitivity": { parallel: jit(nopython=True, parallel=parallel)(_sensitivity_gravity) for parallel in (True, False) @@ -649,14 +508,6 @@ def _sensitivity_gravity_2d_mesh( parallel: jit(nopython=True, parallel=parallel)(_forward_gravity) for parallel in (True, False) }, - "sensitivity_2d_mesh": { - parallel: jit(nopython=True, parallel=parallel)(_sensitivity_gravity_2d_mesh) - for parallel in (True, False) - }, - "forward_2d_mesh": { - parallel: jit(nopython=True, parallel=parallel)(_forward_gravity_2d_mesh) - for parallel in (True, False) - }, "diagonal_gtg": { False: _diagonal_G_T_dot_G_serial, True: _diagonal_G_T_dot_G_parallel, diff --git a/simpeg/potential_fields/gravity/_numba/__init__.py b/simpeg/potential_fields/gravity/_numba/__init__.py new file mode 100644 index 0000000000..8f9cac68a9 --- /dev/null +++ b/simpeg/potential_fields/gravity/_numba/__init__.py @@ -0,0 +1,13 @@ +""" +Numba functions for gravity simulations. +""" + +from ._2d_mesh import NUMBA_FUNCTIONS_2D +from ._3d_mesh import NUMBA_FUNCTIONS_3D + +try: + import choclo +except ImportError: + choclo = None + +__all__ = ["choclo", "NUMBA_FUNCTIONS_3D", "NUMBA_FUNCTIONS_2D"] diff --git a/simpeg/potential_fields/gravity/simulation.py b/simpeg/potential_fields/gravity/simulation.py index 574612f90f..cbefa3843a 100644 --- a/simpeg/potential_fields/gravity/simulation.py +++ b/simpeg/potential_fields/gravity/simulation.py @@ -14,7 +14,7 @@ from ...base import BasePDESimulation from ..base import BaseEquivalentSourceLayerSimulation, BasePFSimulation -from ._numba_functions import choclo, NUMBA_FUNCTIONS +from ._numba import choclo, NUMBA_FUNCTIONS_3D, NUMBA_FUNCTIONS_2D try: from warnings import deprecated @@ -310,7 +310,7 @@ def _get_gtg_diagonal(self, weights: NDArray) -> NDArray: msg = ( "Computing the diagonal of G.T @ G with " 'store_sensitivities="forward_only" and engine="geoana" ' - "hasn't been implemented yet." + "hasn't been implemented yet. " 'Choose store_sensitivities="ram" or "disk", ' 'or another engine, like "choclo".' ) @@ -443,7 +443,7 @@ def G(self) -> NDArray | np.memmap | LinearOperator: msg = ( "Accessing matrix G with " 'store_sensitivities="forward_only" and engine="geoana" ' - "hasn't been implemented yet." + "hasn't been implemented yet. " 'Choose store_sensitivities="ram" or "disk", ' 'or another engine, like "choclo".' ) @@ -576,7 +576,7 @@ def _forward(self, densities): Always return a ``np.float64`` array. """ # Get Numba function - forward_func = NUMBA_FUNCTIONS["forward"][self.numba_parallel] + forward_func = NUMBA_FUNCTIONS_3D["forward"][self.numba_parallel] # Gather active nodes and the indices of the nodes for each active cell active_nodes, active_cell_nodes = self._get_active_nodes() # Allocate fields array @@ -613,7 +613,7 @@ def _sensitivity_matrix(self): (nD, n_active_cells) numpy.ndarray """ # Get Numba function - sensitivity_func = NUMBA_FUNCTIONS["sensitivity"][self.numba_parallel] + sensitivity_func = NUMBA_FUNCTIONS_3D["sensitivity"][self.numba_parallel] # Gather active nodes and the indices of the nodes for each active cell active_nodes, active_cell_nodes = self._get_active_nodes() # Allocate sensitivity matrix @@ -664,7 +664,7 @@ def _sensitivity_matrix_transpose_dot_vec(self, vector): (n_active_cells) numpy.ndarray """ # Get Numba function - sensitivity_t_dot_v_func = NUMBA_FUNCTIONS["gt_dot_v"][self.numba_parallel] + sensitivity_t_dot_v_func = NUMBA_FUNCTIONS_3D["gt_dot_v"][self.numba_parallel] # Gather active nodes and the indices of the nodes for each active cell active_nodes, active_cell_nodes = self._get_active_nodes() # Allocate resulting array @@ -724,7 +724,7 @@ def _gtg_diagonal_without_building_g(self, weights): (n_active_cells) numpy.ndarray """ # Get Numba function - diagonal_gtg_func = NUMBA_FUNCTIONS["diagonal_gtg"][self.numba_parallel] + diagonal_gtg_func = NUMBA_FUNCTIONS_3D["diagonal_gtg"][self.numba_parallel] # Gather active nodes and the indices of the nodes for each active cell active_nodes, active_cell_nodes = self._get_active_nodes() # Allocate array for the diagonal of G.T @ G @@ -804,7 +804,7 @@ def _forward(self, densities): Always return a ``np.float64`` array. """ # Get Numba function - forward_func = NUMBA_FUNCTIONS["forward_2d_mesh"][self.numba_parallel] + forward_func = NUMBA_FUNCTIONS_2D["forward"][self.numba_parallel] # Get cells in the 2D mesh and keep only active cells cells_bounds_active = self.mesh.cell_bounds[self.active_cells] # Allocate fields array @@ -842,7 +842,7 @@ def _sensitivity_matrix(self): (nD, n_active_cells) numpy.ndarray """ # Get Numba function - sensitivity_func = NUMBA_FUNCTIONS["sensitivity_2d_mesh"][self.numba_parallel] + sensitivity_func = NUMBA_FUNCTIONS_2D["sensitivity"][self.numba_parallel] # Get cells in the 2D mesh and keep only active cells cells_bounds_active = self.mesh.cell_bounds[self.active_cells] # Allocate sensitivity matrix @@ -880,6 +880,86 @@ def _sensitivity_matrix(self): index_offset += n_rows return sensitivity_matrix + def _sensitivity_matrix_transpose_dot_vec(self, vector): + """ + Compute ``G.T @ v`` without building ``G``. + + Parameters + ---------- + vector : (nD) numpy.ndarray + Vector used in the dot product. + + Returns + ------- + (n_active_cells) numpy.ndarray + """ + # Get Numba function + g_t_dot_v_func = NUMBA_FUNCTIONS_2D["gt_dot_v"][self.numba_parallel] + # Get cells in the 2D mesh and keep only active cells + cells_bounds_active = self.mesh.cell_bounds[self.active_cells] + # Allocate resulting array + result = np.zeros(self.nC) + # Start filling the result array + index_offset = 0 + for components, receivers in self._get_components_and_receivers(): + n_components = len(components) + n_rows = n_components * receivers.shape[0] + for i, component in enumerate(components): + choclo_forward_func = CHOCLO_FORWARD_FUNCS[component] + conversion_factor = _get_conversion_factor(component) + vector_slice = slice( + index_offset + i, index_offset + n_rows, n_components + ) + g_t_dot_v_func( + receivers, + cells_bounds_active, + self.cell_z_top, + self.cell_z_bottom, + choclo_forward_func, + conversion_factor, + vector[vector_slice], + result, + ) + index_offset += n_rows + return result + + def _gtg_diagonal_without_building_g(self, weights): + """ + Compute the diagonal of ``G.T @ G`` without building the ``G`` matrix. + + Parameters + ----------- + weights : (nD,) array + Array with data weights. It should be the diagonal of the ``W`` + matrix, squared. + + Returns + ------- + (n_active_cells) numpy.ndarray + """ + # Get Numba function + diagonal_gtg_func = NUMBA_FUNCTIONS_2D["diagonal_gtg"][self.numba_parallel] + # Get cells in the 2D mesh and keep only active cells + cells_bounds_active = self.mesh.cell_bounds[self.active_cells] + # Allocate array for the diagonal of G.T @ G + diagonal = np.zeros(self.nC, dtype=np.float64) + # Start filling the diagonal array + for components, receivers in self._get_components_and_receivers(): + for component in components: + choclo_forward_func = CHOCLO_FORWARD_FUNCS[component] + conversion_factor = _get_conversion_factor(component) + diagonal_gtg_func( + receivers, + cells_bounds_active, + self.cell_z_top, + self.cell_z_bottom, + choclo_forward_func, + conversion_factor, + weights, + diagonal, + ) + return diagonal + class Simulation3DDifferential(BasePDESimulation): r"""Finite volume simulation class for gravity. diff --git a/simpeg/potential_fields/magnetics/simulation.py b/simpeg/potential_fields/magnetics/simulation.py index 099ae0b2a8..075673c4d3 100644 --- a/simpeg/potential_fields/magnetics/simulation.py +++ b/simpeg/potential_fields/magnetics/simulation.py @@ -276,7 +276,7 @@ def G(self) -> NDArray | np.memmap | LinearOperator: msg = ( "Accessing matrix G with " 'store_sensitivities="forward_only" and engine="geoana" ' - "hasn't been implemented yet." + "hasn't been implemented yet. " 'Choose store_sensitivities="ram" or "disk", ' 'or another engine, like "choclo".' ) diff --git a/tests/pf/test_equivalent_sources.py b/tests/pf/test_equivalent_sources.py index bbcbcd2b81..0454baedcf 100644 --- a/tests/pf/test_equivalent_sources.py +++ b/tests/pf/test_equivalent_sources.py @@ -464,6 +464,140 @@ def test_forward_choclo_serial_parallel( np.testing.assert_allclose(sim_parallel.dpred(model), sim_serial.dpred(model)) +@pytest.mark.parametrize("parallel", [True, False], ids=["parallel", "serial"]) +@pytest.mark.parametrize("components", [*GRAVITY_COMPONENTS, ["gz", "gzz"]]) +@pytest.mark.parametrize("engine", ["choclo", XFAIL_GEOANA]) +class TestGravityEquivalentSourcesForwardOnly: + """ + Test gravity equivalent sources methods without building the sensitivity matrix. + """ + + def test_Jvec( + self, + coordinates, + tensor_mesh, + mesh_bottom, + mesh_top, + components, + engine, + parallel, + ): + """ + Test Jvec with "forward_only" vs. J @ v with J stored in ram. + """ + # Build survey + gravity_survey = build_gravity_survey(coordinates, components=components) + # Build simulations + mapping = get_mapping(tensor_mesh) + eqs_ram, eqs_forward_only = ( + gravity.SimulationEquivalentSourceLayer( + mesh=tensor_mesh, + cell_z_top=mesh_top, + cell_z_bottom=mesh_bottom, + survey=gravity_survey, + rhoMap=mapping, + engine=engine, + store_sensitivities=store, + numba_parallel=parallel, + ) + for store in ("ram", "forward_only") + ) + # Compare predictions of both simulations + model = get_block_model(tensor_mesh, 2.67) + vector = np.random.default_rng(seed=42).uniform(size=model.size) + expected = eqs_ram.getJ(model) @ vector + atol = np.max(np.abs(expected)) * 1e-7 + # Test Jvec + np.testing.assert_allclose( + expected, eqs_forward_only.Jvec(model, vector), atol=atol + ) + # Test getJ + np.testing.assert_allclose( + expected, eqs_forward_only.getJ(model) @ vector, atol=atol + ) + + def test_Jtvec( + self, + coordinates, + tensor_mesh, + mesh_bottom, + mesh_top, + components, + engine, + parallel, + ): + """ + Test Jtvec with "forward_only" vs. J.T @ v with J stored in ram. + """ + # Build survey + gravity_survey = build_gravity_survey(coordinates, components=components) + # Build simulations + mapping = get_mapping(tensor_mesh) + eqs_ram, eqs_forward_only = ( + gravity.SimulationEquivalentSourceLayer( + mesh=tensor_mesh, + cell_z_top=mesh_top, + cell_z_bottom=mesh_bottom, + survey=gravity_survey, + rhoMap=mapping, + engine=engine, + store_sensitivities=store, + numba_parallel=parallel, + ) + for store in ("ram", "forward_only") + ) + # Compare predictions of both simulations + model = get_block_model(tensor_mesh, 2.67) + vector = np.random.default_rng(seed=42).uniform(size=gravity_survey.nD) + expected = eqs_ram.getJ(model).T @ vector + atol = np.max(np.abs(expected)) * 1e-7 + # Test Jtvec + np.testing.assert_allclose( + expected, eqs_forward_only.Jtvec(model, vector), atol=atol + ) + # Test getJ + np.testing.assert_allclose( + expected, eqs_forward_only.getJ(model).T @ vector, atol=atol + ) + + def test_getJtJdiag( + self, + coordinates, + tensor_mesh, + mesh_bottom, + mesh_top, + components, + engine, + parallel, + ): + """ + Test the ``getJtJdiag`` method, comparing forward_only with storing J in memory. + """ + # Build survey + gravity_survey = build_gravity_survey(coordinates, components=components) + # Build simulations + mapping = get_mapping(tensor_mesh) + eqs_ram, eqs_forward_only = ( + gravity.SimulationEquivalentSourceLayer( + mesh=tensor_mesh, + cell_z_top=mesh_top, + cell_z_bottom=mesh_bottom, + survey=gravity_survey, + rhoMap=mapping, + engine=engine, + store_sensitivities=store, + numba_parallel=parallel, + ) + for store in ("ram", "forward_only") + ) + # Compare predictions of both simulations + model = get_block_model(tensor_mesh, 2.67) + gtgdiag_ram = eqs_ram.getJtJdiag(model) + gtgdiag_linop = eqs_forward_only.getJtJdiag(model) + atol = np.max(np.abs(gtgdiag_ram)) * 1e-7 + np.testing.assert_allclose(gtgdiag_ram, gtgdiag_linop, atol=atol) + + class TestMagneticEquivalentSourcesForward: """ Test the forward capabilities of the magnetic equivalent sources. From 64f7e569276359ab8e048dbbce49ab74f7e553bb Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 4 Jun 2025 16:45:00 +0000 Subject: [PATCH 149/194] Improve admonitions in gravity simulation (#1677) Add note on the sign of the "gz" component. Group admonitions related to units into a single one. --- simpeg/potential_fields/gravity/simulation.py | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/simpeg/potential_fields/gravity/simulation.py b/simpeg/potential_fields/gravity/simulation.py index cbefa3843a..c4b9430842 100644 --- a/simpeg/potential_fields/gravity/simulation.py +++ b/simpeg/potential_fields/gravity/simulation.py @@ -119,22 +119,25 @@ def _get_conversion_factor(component): class Simulation3DIntegral(BasePFSimulation): - """ + r""" Gravity simulation in integral form. - .. important:: + .. note:: - Density model is assumed to be in g/cc. + The gravity simulation assumes the following units for its inputs and outputs: - .. important:: - - Acceleration components ("gx", "gy", "gz") are returned in mgal - (:math:`10^{-5} m/s^2`). + - Density model is assumed to be in gram per cubic centimeter (g/cc). + - Acceleration components (``"gx"``, ``"gy"``, ``"gz"``) are returned in mgal + (:math:`10^{-5} \text{m}/\text{s}^2`). + - Gradient components (``"gxx"``, ``"gyy"``, ``"gzz"``, ``"gxy"``, ``"gxz"``, + ``"gyz"``, ``"guv"``) are returned in Eotvos (:math:`10^{-9} s^{-2}`). .. important:: - Gradient components ("gxx", "gyy", "gzz", "gxy", "gxz", "gyz", "guv") are - returned in Eotvos (:math:`10^{-9} s^{-2}`). + Following SimPEG convention for the right-handed xyz coordinate system, the + z axis points *upwards*. Therefore, the ``"gz"`` component corresponds to the + **upward** component of the gravity acceleration vector. + Parameters ---------- From 285e0f0588ec0ca47d63355745c37b14072051fc Mon Sep 17 00:00:00 2001 From: Lindsey Heagy Date: Wed, 11 Jun 2025 19:30:02 -0700 Subject: [PATCH 150/194] Have an option to take a step when the Linesearch breaks (#1581) #### Summary As we chatted about in the [November 13 meeting (and earlier!)](https://curvenote.com/@simpeg/meeting-notes/11-13-2024/) for many types of non-linear problems it can be useful to allow the inversion to take a step even if the line search fails. This is a first cut at allowing that. I used the keyword `aggressive_stepping` for the flag, but happy to think about this with others' input. #### PR Checklist * [ ] If this is a work in progress PR, set as a Draft PR * [ ] Linted my code according to the [style guides](https://docs.simpeg.xyz/latest/content/getting_started/contributing/code-style.html). * [ ] Added [tests](https://docs.simpeg.xyz/latest/content/getting_started/contributing/testing.html) to verify changes to the code. * [ ] Added necessary documentation to any new functions/classes following the expect [style](https://docs.simpeg.xyz/latest/content/getting_started/contributing/documentation.html). * [ ] Marked as ready for review (if this is was a draft PR), and converted to a Pull Request * [ ] Tagged ``@simpeg/simpeg-developers`` when ready for review. #### Reference issue #### What does this implement/fix? #### Additional information --- simpeg/optimization.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/simpeg/optimization.py b/simpeg/optimization.py index b016ae887a..dd5bd1552e 100644 --- a/simpeg/optimization.py +++ b/simpeg/optimization.py @@ -268,6 +268,7 @@ class Minimize(object): tolX = 1e-1 #: Tolerance on norm(x) movement tolG = 1e-1 #: Tolerance on gradient norm eps = 1e-5 #: Small value + require_decrease = True #: Require decrease in the objective function. If False, we will still take a step when the linesearch fails stopNextIteration = False #: Stops the optimization program nicely. use_WolfeCurvature = False #: add the Wolfe Curvature criteria for line search @@ -404,9 +405,12 @@ def evalFunction(x, return_g=False, return_H=False): p = self.scaleSearchDirection(self.searchDirection) xt, passLS = self.modifySearchDirection(p) if not passLS: - xt, caught = self.modifySearchDirectionBreak(p) - if not caught: - return self.xc + if self.require_decrease is True: + xt, caught = self.modifySearchDirectionBreak(p) + if not caught: + return self.xc + else: + print("Linesearch failed. Stepping anyways...") self.doEndIteration(xt) if self.stopNextIteration: break From e44476b14e1b5fe76884afa46be5442d3c2e2951 Mon Sep 17 00:00:00 2001 From: "Devin C. Cowan" Date: Thu, 12 Jun 2025 16:37:12 -0700 Subject: [PATCH 151/194] Fix bug in phase for recursive 1d NSEM simulation (#1679) The recursive solution exclusively computes the impedance, apparent resistivity and/or phase of the Zxy impedance at all frequencies. In the convention adopted by SimPEG, Zxy is in the lower-left quadrant of the complex plane. Using np.atan(imag/real) results in phase values between -90 and +90 which is wrong. Suggest we use ``np.angle`` instead. ![image](https://github.com/user-attachments/assets/d751640b-1d23-419e-abe9-2e29fbb0f1af) #### PR Checklist * [ ] If this is a work in progress PR, set as a Draft PR * [x] Linted my code according to the [style guides](https://docs.simpeg.xyz/latest/content/getting_started/contributing/code-style.html). * [x] Added [tests](https://docs.simpeg.xyz/latest/content/getting_started/contributing/testing.html) to verify changes to the code. * [x] Added necessary documentation to any new functions/classes following the expect [style](https://docs.simpeg.xyz/latest/content/getting_started/contributing/documentation.html). * [x] Marked as ready for review (if this is was a draft PR), and converted to a Pull Request * [x] Tagged ``@simpeg/simpeg-developers`` when ready for review. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- simpeg/electromagnetics/natural_source/simulation_1d.py | 3 +-- tests/em/nsem/forward/test_Recursive1D_VsAnalyticHalfspace.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/simpeg/electromagnetics/natural_source/simulation_1d.py b/simpeg/electromagnetics/natural_source/simulation_1d.py index 424c39c9d7..ef0357718c 100644 --- a/simpeg/electromagnetics/natural_source/simulation_1d.py +++ b/simpeg/electromagnetics/natural_source/simulation_1d.py @@ -248,8 +248,7 @@ def dpred(self, m, f=None): ) elif rx.component == "phase": d.append( - (180.0 / np.pi) - * np.arctan(np.imag(Z[i_freq]) / np.real(Z[i_freq])) + (180.0 / np.pi) * np.arctan2(Z[i_freq].imag, Z[i_freq].real) ) return np.array(d) diff --git a/tests/em/nsem/forward/test_Recursive1D_VsAnalyticHalfspace.py b/tests/em/nsem/forward/test_Recursive1D_VsAnalyticHalfspace.py index 3242e6f5aa..5d185bc28b 100644 --- a/tests/em/nsem/forward/test_Recursive1D_VsAnalyticHalfspace.py +++ b/tests/em/nsem/forward/test_Recursive1D_VsAnalyticHalfspace.py @@ -30,7 +30,7 @@ def true_solution(freq, sigma_half): -np.sqrt(np.pi * freq * mu_0 / sigma_half), -np.sqrt(np.pi * freq * mu_0 / sigma_half), 1 / sigma_half, - 45.0, + -135.0, ] return soln From 6146a5ebe449cfc4c228b8888a593a7056be6f43 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 18 Jul 2025 16:57:13 +0000 Subject: [PATCH 152/194] Use conda-forge as only channel in Azure pipelines (#1688) Use `conda-forge` instead of `defaults` channel when setting up the environment in Azure pipelines. This avoid us having to accept Anaconda's ToS. Fix bash syntax when defining `is_azure` variable. Use `python-build` instead of `build` in `.ci/azure/setup_env.sh` since `build` is not available in `conda-forge`. --- .ci/azure/setup_env.sh | 11 +++++++++-- .ci/environment_test.yml | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.ci/azure/setup_env.sh b/.ci/azure/setup_env.sh index 8d873a601d..a23942d3d5 100755 --- a/.ci/azure/setup_env.sh +++ b/.ci/azure/setup_env.sh @@ -2,10 +2,17 @@ set -ex #echo on and exit if any line fails # TF_BUILD is set to True on azure pipelines. -is_azure=$(${TF_BUILD:-false} | tr '[:upper:]' '[:lower:]') +is_azure=$(echo "${TF_BUILD:-false}" | tr '[:upper:]' '[:lower:]') if ${is_azure} then + # Add conda-forge as channel + conda config --add channels conda-forge + # Remove defaults channels + # (both from ~/.condarc and from /usr/share/miniconda/.condarc) + conda config --remove channels defaults + conda config --remove channels defaults --system + # Update conda conda update --yes -n base conda fi @@ -30,4 +37,4 @@ echo "Conda Environment:" conda list echo "Installed SimPEG version:" -python -c "import simpeg; print(simpeg.__version__)" \ No newline at end of file +python -c "import simpeg; print(simpeg.__version__)" diff --git a/.ci/environment_test.yml b/.ci/environment_test.yml index a8c6dc91cb..da07821f9a 100644 --- a/.ci/environment_test.yml +++ b/.ci/environment_test.yml @@ -45,4 +45,4 @@ dependencies: # PyPI uploading - wheel - twine - - build + - python-build From 4a94b2ccf7c5b82d52240eec627e38eda4c92447 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 18 Jul 2025 18:54:18 +0000 Subject: [PATCH 153/194] Expose solver utility functions in `simpeg.utils` (#1678) Allow users to import `get_default_solver` and `set_default_solver` directly from `simpeg.utils` instead of having to do so from `simpeg.utils.solver_utils`, to avoid redundant words in import statements. --- simpeg/base/pde_simulation.py | 3 +-- simpeg/inverse_problem.py | 2 +- simpeg/potential_fields/magnetics/simulation.py | 3 +-- simpeg/utils/__init__.py | 8 ++++---- tests/base/regularizations/test_pgi_regularization.py | 2 +- tests/base/test_base_pde_sim.py | 2 +- tests/utils/test_default_solver.py | 7 ++----- 7 files changed, 11 insertions(+), 16 deletions(-) diff --git a/simpeg/base/pde_simulation.py b/simpeg/base/pde_simulation.py index d0cdb46f1f..ee0d4dbfb7 100644 --- a/simpeg/base/pde_simulation.py +++ b/simpeg/base/pde_simulation.py @@ -8,8 +8,7 @@ from .. import props from scipy.constants import mu_0 -from ..utils import validate_type -from ..utils.solver_utils import get_default_solver +from ..utils import validate_type, get_default_solver def __inner_mat_mul_op(M, u, v=None, adjoint=False): diff --git a/simpeg/inverse_problem.py b/simpeg/inverse_problem.py index affff7cd73..1bd2ae974e 100644 --- a/simpeg/inverse_problem.py +++ b/simpeg/inverse_problem.py @@ -14,7 +14,7 @@ validate_ndarray_with_shape, ) from .version import __version__ as simpeg_version -from .utils.solver_utils import get_default_solver +from .utils import get_default_solver class BaseInvProblem: diff --git a/simpeg/potential_fields/magnetics/simulation.py b/simpeg/potential_fields/magnetics/simulation.py index 075673c4d3..4b95ed2cfa 100644 --- a/simpeg/potential_fields/magnetics/simulation.py +++ b/simpeg/potential_fields/magnetics/simulation.py @@ -16,9 +16,8 @@ from scipy.sparse.linalg import LinearOperator, aslinearoperator from simpeg import props, utils -from simpeg.utils import mat_utils, mkvc, sdiag +from simpeg.utils import mat_utils, mkvc, sdiag, get_default_solver from simpeg.utils.code_utils import deprecate_property, validate_string, validate_type -from simpeg.utils.solver_utils import get_default_solver from ...base import BaseMagneticPDESimulation from ..base import BaseEquivalentSourceLayerSimulation, BasePFSimulation diff --git a/simpeg/utils/__init__.py b/simpeg/utils/__init__.py index 6e0895fd45..c4e3874313 100644 --- a/simpeg/utils/__init__.py +++ b/simpeg/utils/__init__.py @@ -144,14 +144,13 @@ Solver utilities ---------------- -This module contains utilities to get and set the default solver -used by SimPEG simulations. +Functions to get and set the default solver meant to be used in PDE simulations. .. autosummary:: :toctree: generated/ - solver_utils.get_default_solver - solver_utils.set_default_solver + get_default_solver + set_default_solver """ from discretize.utils.interpolation_utils import interpolation_matrix @@ -289,3 +288,4 @@ rotatePointsFromNormals, rotationMatrixFromNormals, ) +from .solver_utils import get_default_solver, set_default_solver diff --git a/tests/base/regularizations/test_pgi_regularization.py b/tests/base/regularizations/test_pgi_regularization.py index c69bf9e051..f59a012396 100644 --- a/tests/base/regularizations/test_pgi_regularization.py +++ b/tests/base/regularizations/test_pgi_regularization.py @@ -8,7 +8,7 @@ from simpeg import regularization from simpeg.maps import Wires from simpeg.utils import WeightedGaussianMixture, mkvc -from simpeg.utils.solver_utils import get_default_solver +from simpeg.utils import get_default_solver Solver = get_default_solver() diff --git a/tests/base/test_base_pde_sim.py b/tests/base/test_base_pde_sim.py index 20501849ec..57c3d2cadc 100644 --- a/tests/base/test_base_pde_sim.py +++ b/tests/base/test_base_pde_sim.py @@ -11,7 +11,7 @@ import scipy.sparse as sp import pytest -from simpeg.utils.solver_utils import get_default_solver +from simpeg.utils import get_default_solver # define a very simple class... diff --git a/tests/utils/test_default_solver.py b/tests/utils/test_default_solver.py index e8e2fc3ba1..5fb8791f9e 100644 --- a/tests/utils/test_default_solver.py +++ b/tests/utils/test_default_solver.py @@ -2,11 +2,8 @@ import pytest from pymatsolver import SolverCG -from simpeg.utils.solver_utils import ( - get_default_solver, - set_default_solver, - DefaultSolverWarning, -) +from simpeg.utils import get_default_solver, set_default_solver +from simpeg.utils.solver_utils import DefaultSolverWarning @pytest.fixture(autouse=True) From cba34e9e7f53d3c4ef6691492f92400090f92aa3 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 23 Jul 2025 16:08:04 +0000 Subject: [PATCH 154/194] Use logging while setting default solver in PDE simulations (#1670) Ditch the `DefaultSolverWarning` in `get_default_solver` and make the base PDE simulation class to handle the information message to users about using a default solver. The information message is sent through a new custom logger defined using `logging`. The information is sent from the constructor after obtaining the solver through `get_default_solver`. The solver class gets cached on instantiation, so the simulation will use the same solver even if the default solver changes later on. Warn users regarding poor performance when setting `SolverLU` or `Solver` as the solver. Adjust and extend tests. Also, add a `simpeg.utils.get_logger` function that returns the logger object used across the project, and a new `PerformanceWarning` class in a new `simpeg.utils.warnings` submodule. --- pyproject.toml | 1 - simpeg/base/pde_simulation.py | 30 ++++++++++++--- .../potential_fields/magnetics/simulation.py | 2 +- simpeg/utils/__init__.py | 22 +++++++++++ simpeg/utils/logger.py | 38 +++++++++++++++++++ simpeg/utils/solver_utils.py | 21 ++++------ simpeg/utils/warnings.py | 11 ++++++ tests/base/test_base_pde_sim.py | 24 ++++++++++-- tests/utils/test_default_solver.py | 27 ++++++------- 9 files changed, 136 insertions(+), 40 deletions(-) create mode 100644 simpeg/utils/logger.py create mode 100644 simpeg/utils/warnings.py diff --git a/pyproject.toml b/pyproject.toml index 7069369d37..2bede500cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -258,7 +258,6 @@ rst-roles = [ # pyproject.toml [tool.pytest.ini_options] filterwarnings = [ - "ignore::simpeg.utils.solver_utils.DefaultSolverWarning", "error:You are running a pytest without setting a random seed.*:UserWarning", "error:The `index_dictionary` property has been deprecated:FutureWarning", 'error:The `simpeg\.directives\.[a-z_]+` submodule has been deprecated', diff --git a/simpeg/base/pde_simulation.py b/simpeg/base/pde_simulation.py index ee0d4dbfb7..5d5360918d 100644 --- a/simpeg/base/pde_simulation.py +++ b/simpeg/base/pde_simulation.py @@ -1,4 +1,5 @@ import inspect +import warnings import numpy as np import pymatsolver import scipy.sparse as sp @@ -8,7 +9,7 @@ from .. import props from scipy.constants import mu_0 -from ..utils import validate_type, get_default_solver +from ..utils import validate_type, get_default_solver, get_logger, PerformanceWarning def __inner_mat_mul_op(M, u, v=None, adjoint=False): @@ -433,12 +434,24 @@ class BasePDESimulation(BaseSimulation): pairs of keyword arguments and parameter values for the solver. Please visit `pymatsolver `__ to learn more about solvers and their parameters. - """ def __init__(self, mesh, solver=None, solver_opts=None, **kwargs): self.mesh = mesh super().__init__(**kwargs) + if solver is None: + solver = get_default_solver() + get_logger().info( + f"Setting the default solver '{solver.__name__}' for the " + f"'{type(self).__name__}'.\n" + "To avoid receiving this message, pass a solver to the simulation. " + "For example:" + "\n\n" + " from simpeg.utils import get_default_solver\n" + "\n" + " solver = get_default_solver()\n" + f" simulation = {type(self).__name__}(solver=solver, ...)" + ) self.solver = solver if solver_opts is None: solver_opts = {} @@ -483,10 +496,6 @@ def solver(self): type[pymatsolver.solvers.Base] Numerical solver used to solve the forward problem. """ - if self._solver is None: - # do not cache this, in case the user wants to - # change it after the first time it is requested. - return get_default_solver(warn=True) return self._solver @solver.setter @@ -498,6 +507,15 @@ def solver(self, cls): raise TypeError( f"{cls.__qualname__} is not a subclass of pymatsolver.base.BaseSolver" ) + if cls in (pymatsolver.SolverLU, pymatsolver.Solver): + warnings.warn( + f"The 'pymatsolver.{cls.__name__}' solver might lead to high " + "computation times. " + "We recommend using a faster alternative such as 'pymatsolver.Pardiso' " + "or 'pymatsolver.Mumps'.", + PerformanceWarning, + stacklevel=2, + ) self._solver = cls @property diff --git a/simpeg/potential_fields/magnetics/simulation.py b/simpeg/potential_fields/magnetics/simulation.py index 4b95ed2cfa..57ca242d8c 100644 --- a/simpeg/potential_fields/magnetics/simulation.py +++ b/simpeg/potential_fields/magnetics/simulation.py @@ -2213,7 +2213,7 @@ def MagneticsDiffSecondaryInv(mesh, model, data, **kwargs): # Create an optimization program opt = optimization.InexactGaussNewton(maxIter=miter) - opt.bfgsH0 = get_default_solver(warn=True)(sp.identity(model.nP), flag="D") + opt.bfgsH0 = get_default_solver()(sp.identity(model.nP), flag="D") # Create a regularization program reg = regularization.WeightedLeastSquares(model) # Create an objective function diff --git a/simpeg/utils/__init__.py b/simpeg/utils/__init__.py index c4e3874313..8e9dfedf93 100644 --- a/simpeg/utils/__init__.py +++ b/simpeg/utils/__init__.py @@ -11,6 +11,17 @@ documentation for many details on items. +Logger +====== +Function to fetch the SimPEG logger. It can be used to stream messages to the logger, +and to temporarily adjust its configuration (e.g. change log level). + +.. autosummary:: + :toctree: generated/ + + get_logger + + Counter Utility Functions ========================= @@ -151,10 +162,20 @@ get_default_solver set_default_solver + +Custom warnings +--------------- +List of custom warnings used in SimPEG. + +.. autosummary:: + :toctree: generated/ + + PerformanceWarning """ from discretize.utils.interpolation_utils import interpolation_matrix +from .logger import get_logger from .code_utils import ( mem_profile_class, hook, @@ -289,3 +310,4 @@ rotationMatrixFromNormals, ) from .solver_utils import get_default_solver, set_default_solver +from .warnings import PerformanceWarning diff --git a/simpeg/utils/logger.py b/simpeg/utils/logger.py new file mode 100644 index 0000000000..16936abeb7 --- /dev/null +++ b/simpeg/utils/logger.py @@ -0,0 +1,38 @@ +""" +Define logger for SimPEG. +""" + +import logging + +__all__ = ["get_logger"] + + +def _create_logger(): + """ + Create logger for SimPEG. + """ + logger = logging.getLogger("SimPEG") + logger.setLevel(logging.INFO) + handler = logging.StreamHandler() + formatter = logging.Formatter("{levelname}: {message}", style="{") + handler.setFormatter(formatter) + logger.addHandler(handler) + return logger + + +LOGGER = _create_logger() + + +def get_logger(): + r""" + Get the default event logger. + + The logger records events and relevant information while setting up simulations and + inversions. By default the logger will stream to stderr and using the INFO level. + + Returns + ------- + logger : :class:`logging.Logger` + The logger object for SimPEG. + """ + return LOGGER diff --git a/simpeg/utils/solver_utils.py b/simpeg/utils/solver_utils.py index 55ffab0f1a..d180871fad 100644 --- a/simpeg/utils/solver_utils.py +++ b/simpeg/utils/solver_utils.py @@ -44,19 +44,17 @@ _DEFAULT_SOLVER = SolverLU -# Create a specific warning allowing users to silence this if they so choose. -class DefaultSolverWarning(UserWarning): - pass - - def get_default_solver(warn=False) -> Type[Base]: """Return the default solver used by simpeg. Parameters ---------- warn : bool, optional - If True, a warning will be raised to let users know that the default - solver is being chosen depending on their system. + + .. deprecated:: 0.25.0 + + Argument ``warn`` is deprecated and will be removed in + SimPEG v0.26.0. Returns ------- @@ -65,12 +63,9 @@ def get_default_solver(warn=False) -> Type[Base]: """ if warn: warnings.warn( - f"Using the default solver: {_DEFAULT_SOLVER.__name__}. \n\n" - f"If you would like to suppress this notification, add \n" - f"warnings.filterwarnings(" - "'ignore', simpeg.utils.solver_utils.DefaultSolverWarning)\n" - f" to your script.", - DefaultSolverWarning, + "The `warn` argument has been deprecated and will be " + "removed in SimPEG v0.26.0.", + FutureWarning, stacklevel=2, ) return _DEFAULT_SOLVER diff --git a/simpeg/utils/warnings.py b/simpeg/utils/warnings.py new file mode 100644 index 0000000000..c6ead5491d --- /dev/null +++ b/simpeg/utils/warnings.py @@ -0,0 +1,11 @@ +""" +Custom warnings that can be used across SimPEG. +""" + +__all__ = ["PerformanceWarning"] + + +class PerformanceWarning(Warning): + """ + Warning raised when there is a possible performance impact. + """ diff --git a/tests/base/test_base_pde_sim.py b/tests/base/test_base_pde_sim.py index 57c3d2cadc..ff750385d5 100644 --- a/tests/base/test_base_pde_sim.py +++ b/tests/base/test_base_pde_sim.py @@ -1,7 +1,9 @@ import re +import pymatsolver from simpeg.base import with_property_mass_matrices, BasePDESimulation from simpeg import props, maps +from simpeg.utils import PerformanceWarning import unittest import discretize import numpy as np @@ -812,13 +814,27 @@ def test_bad_derivative_stash(): sim.MeSigmaDeriv(u, v) -def test_solver_defaults(): +def test_solver_defaults(caplog): mesh = discretize.TensorMesh([2, 2, 2]) sim = BasePDESimulation(mesh) - with pytest.warns(UserWarning, match="Using the default solver.*"): - solver_class = sim.solver + # Check that logging.info was created + assert "Setting the default solver" in caplog.text + # Test if default solver was properly set + assert sim.solver is get_default_solver() - assert solver_class is get_default_solver() + +@pytest.mark.parametrize("solver_class", [pymatsolver.SolverLU, pymatsolver.Solver]) +def test_performance_warning_on_solver(solver_class): + """ + Test PerformanceWarning when setting an inefficient solver. + """ + mesh = discretize.TensorMesh([2, 2, 2]) + regex = re.escape( + f"The 'pymatsolver.{solver_class.__name__}' solver might lead to high " + "computation times." + ) + with pytest.warns(PerformanceWarning, match=regex): + BasePDESimulation(mesh, solver=solver_class) def test_bad_solver(): diff --git a/tests/utils/test_default_solver.py b/tests/utils/test_default_solver.py index 5fb8791f9e..4c86b47333 100644 --- a/tests/utils/test_default_solver.py +++ b/tests/utils/test_default_solver.py @@ -1,9 +1,9 @@ +import re import warnings import pytest from pymatsolver import SolverCG from simpeg.utils import get_default_solver, set_default_solver -from simpeg.utils.solver_utils import DefaultSolverWarning @pytest.fixture(autouse=True) @@ -24,30 +24,27 @@ def test_default_error(): class Temp: pass - with pytest.warns(DefaultSolverWarning): - initial_default = get_default_solver(warn=True) + initial_default = get_default_solver() - with pytest.raises( - TypeError, - match="Default solver must be a subclass of pymatsolver.solvers.Base.", - ): + regex = re.escape("Default solver must be a subclass of pymatsolver.solvers.Base.") + with pytest.raises(TypeError, match=regex): set_default_solver(Temp) - with pytest.warns(DefaultSolverWarning): - after_default = get_default_solver(warn=True) + after_default = get_default_solver() # make sure we didn't accidentally set the default. - assert initial_default == after_default + assert initial_default is after_default -def test_warning(): - """Test if warning is raised when warn=True.""" - with pytest.warns(DefaultSolverWarning, match="Using the default solver"): +def test_deprecation_warning(): + """Test deprecation warning for the warn argument.""" + regex = re.escape("The `warn` argument has been deprecated and will be removed in") + with pytest.warns(FutureWarning, match=regex): get_default_solver(warn=True) -def test_no_warning(): - """Test if no warning is issued with default parameters.""" +def test_no_deprecation_warning(): + """Test if no deprecation warning is issued with default parameters.""" with warnings.catch_warnings(): warnings.simplefilter("error") # raise error if warning was raised get_default_solver() From 3aed38df47330ef968118c4aeb63c90f748d43b0 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 12 Aug 2025 15:52:41 +0000 Subject: [PATCH 155/194] Use `Impedance` and `Tipper` in examples and tests (#1690) Replace the deprecated `PointNaturalSource` and `Point3DTipper` NSEM receiver classes for their new counterparts (`Impedance` and `Tipper`) in examples and tests. --- examples/07-nsem/plot_fwd_nsem_MTTipper3D.py | 16 +++- .../natural_source/utils/data_utils.py | 14 +-- .../natural_source/utils/test_utils.py | 88 +++++++++---------- .../em/nsem/forward/test_1D_finite_volume.py | 12 +-- .../test_Recursive1D_VsAnalyticHalfspace.py | 8 +- tests/em/nsem/inversion/test_BC_Sims.py | 48 ++++------ .../nsem/inversion/test_Problem1D_Adjoint.py | 8 +- .../nsem/inversion/test_Problem1D_Derivs.py | 8 +- .../inversion/test_complex_resistivity.py | 18 ++-- 9 files changed, 109 insertions(+), 111 deletions(-) diff --git a/examples/07-nsem/plot_fwd_nsem_MTTipper3D.py b/examples/07-nsem/plot_fwd_nsem_MTTipper3D.py index e0f2725378..014b112a6b 100644 --- a/examples/07-nsem/plot_fwd_nsem_MTTipper3D.py +++ b/examples/07-nsem/plot_fwd_nsem_MTTipper3D.py @@ -56,11 +56,19 @@ def run(plotIt=True): # Make a receiver list receiver_list = [] for rx_orientation in ["xx", "xy", "yx", "yy"]: - receiver_list.append(NSEM.Rx.PointNaturalSource(rx_loc, rx_orientation, "real")) - receiver_list.append(NSEM.Rx.PointNaturalSource(rx_loc, rx_orientation, "imag")) + receiver_list.append( + NSEM.Rx.Impedance(rx_loc, orientation=rx_orientation, component="real") + ) + receiver_list.append( + NSEM.Rx.Impedance(rx_loc, orientation=rx_orientation, component="imag") + ) for rx_orientation in ["zx", "zy"]: - receiver_list.append(NSEM.Rx.Point3DTipper(rx_loc, rx_orientation, "real")) - receiver_list.append(NSEM.Rx.Point3DTipper(rx_loc, rx_orientation, "imag")) + receiver_list.append( + NSEM.Rx.Tipper(rx_loc, orientation=rx_orientation, component="real") + ) + receiver_list.append( + NSEM.Rx.Tipper(rx_loc, orientation=rx_orientation, component="imag") + ) # Source list source_list = [ diff --git a/simpeg/electromagnetics/natural_source/utils/data_utils.py b/simpeg/electromagnetics/natural_source/utils/data_utils.py index 3d032af034..af5c113f4f 100644 --- a/simpeg/electromagnetics/natural_source/utils/data_utils.py +++ b/simpeg/electromagnetics/natural_source/utils/data_utils.py @@ -8,6 +8,8 @@ from simpeg.electromagnetics.natural_source.receivers import ( PointNaturalSource, Point3DTipper, + Impedance, + Tipper, ) from simpeg.electromagnetics.natural_source.sources import PlanewaveXYPrimary from simpeg.electromagnetics.natural_source.utils import ( @@ -68,9 +70,9 @@ def extract_data_info(NSEMdata): src_rx_slice = survey_slices[src, rx] dL.append(NSEMdata.dobs[src_rx_slice]) freqL.append(np.ones(rx.nD) * src.frequency) - if isinstance(rx, PointNaturalSource): + if isinstance(rx, (Impedance, PointNaturalSource)): rxTL.extend((("z" + rx.orientation + " ") * rx.nD).split()) - if isinstance(rx, Point3DTipper): + if isinstance(rx, (Tipper, Point3DTipper)): rxTL.extend((("t" + rx.orientation + " ") * rx.nD).split()) return np.concatenate(dL), np.concatenate(freqL), np.array(rxTL) @@ -124,9 +126,9 @@ def resample_data(NSEMdata, locs="All", freqs="All", rxs="All", verbose=False): rx_comp = [] for rxT in rxs: if "z" in rxT[0]: - rxtype = PointNaturalSource + rxtype = Impedance elif "t" in rxT[0]: - rxtype = Point3DTipper + rxtype = Tipper else: raise IOError("Unknown rx type string") orient = rxT[1:3] @@ -258,8 +260,8 @@ def convert3Dto1Dobject(NSEMdata, rxType3D="yx"): for loc in uniLocs: # Make the receiver list rx1DList = [] - rx1DList.append(PointNaturalSource(simpeg.mkvc(loc, 2).T, "real")) - rx1DList.append(PointNaturalSource(simpeg.mkvc(loc, 2).T, "imag")) + rx1DList.append(Impedance(simpeg.mkvc(loc, 2).T, component="real")) + rx1DList.append(Impedance(simpeg.mkvc(loc, 2).T, component="imag")) # Source list locrecData = recData[ np.sqrt( diff --git a/simpeg/electromagnetics/natural_source/utils/test_utils.py b/simpeg/electromagnetics/natural_source/utils/test_utils.py index 17e50932f8..1f0e7297c6 100644 --- a/simpeg/electromagnetics/natural_source/utils/test_utils.py +++ b/simpeg/electromagnetics/natural_source/utils/test_utils.py @@ -4,8 +4,8 @@ from simpeg import maps, mkvc, utils from ....utils import unpack_widths from ..receivers import ( - PointNaturalSource, - Point3DTipper, + Impedance, + Tipper, ) from ..survey import Survey from ..sources import PlanewaveXYPrimary, Planewave @@ -68,10 +68,10 @@ def setup1DSurvey(sigmaHalf, tD=False, structure=False): receiver_list = [] for _ in range(len(["z1d", "z1d"])): receiver_list.append( - PointNaturalSource(mkvc(np.array([0.0]), 2).T, component="real") + Impedance(mkvc(np.array([0.0]), 2).T, component="real", orientation="xy") ) receiver_list.append( - PointNaturalSource(mkvc(np.array([0.0]), 2).T, component="imag") + Impedance(mkvc(np.array([0.0]), 2).T, component="imag", orientation="xy") ) # Source list source_list = [] @@ -113,8 +113,8 @@ def setup1DSurveyElectricMagnetic(sigmaHalf, tD=False, structure=False): rxList = [] for _ in range(len(["z1d", "z1d"])): - rxList.append(PointNaturalSource(mkvc(np.array([0.0]), 2).T, component="real")) - rxList.append(PointNaturalSource(mkvc(np.array([0.0]), 2).T, component="imag")) + rxList.append(Impedance(mkvc(np.array([0.0]), 2).T, component="real")) + rxList.append(Impedance(mkvc(np.array([0.0]), 2).T, component="imag")) # Source list # srcList = [] src_list = [Planewave([], frequency=f) for f in frequencies] @@ -176,50 +176,44 @@ def setupSimpegNSEM_tests_location_assign_list( if comp == "Res": if singleList: rxList.append( - PointNaturalSource( - locations=[rx_loc], + Impedance( + rx_loc, orientation=rx_type, component="apparent_resistivity", ) ) rxList.append( - PointNaturalSource( - locations=[rx_loc], orientation=rx_type, component="phase" - ) + Impedance(rx_loc, orientation=rx_type, component="phase") ) else: rxList.append( - PointNaturalSource( - locations=[rx_loc, rx_loc], + Impedance( + locations_e=rx_loc, + locations_h=rx_loc, orientation=rx_type, component="apparent_resistivity", ) ) rxList.append( - PointNaturalSource( - locations=[rx_loc, rx_loc], + Impedance( + locations_e=rx_loc, + locations_h=rx_loc, orientation=rx_type, component="phase", ) ) else: + rxList.append(Impedance(rx_loc, orientation=rx_type, component="real")) rxList.append( - PointNaturalSource( - orientation=rx_type, component="real", locations=[rx_loc] - ) - ) - rxList.append( - PointNaturalSource( - orientation=rx_type, component="imag", locations=[rx_loc] + Impedance( + rx_loc, + orientation=rx_type, + component="imag", ) ) if rx_type in ["zx", "zy"]: - rxList.append( - Point3DTipper(orientation=rx_type, component="real", locations=[rx_loc]) - ) - rxList.append( - Point3DTipper(orientation=rx_type, component="imag", locations=[rx_loc]) - ) + rxList.append(Tipper(rx_loc, orientation=rx_type, component="real")) + rxList.append(Tipper(rx_loc, orientation=rx_type, component="imag")) srcList = [] if singleFreq: @@ -328,23 +322,19 @@ def setupSimpegNSEM_PrimarySecondary(inputSetup, freqs, comp="Imp", singleFreq=F if rx_type in ["xx", "xy", "yx", "yy"]: if comp == "Res": rxList.append( - PointNaturalSource( - locations=rx_loc, + Impedance( + rx_loc, orientation=rx_type, component="apparent_resistivity", ) ) - rxList.append( - PointNaturalSource( - locations=rx_loc, orientation=rx_type, component="phase" - ) - ) + rxList.append(Impedance(rx_loc, orientation=rx_type, component="phase")) else: - rxList.append(PointNaturalSource(rx_loc, rx_type, "real")) - rxList.append(PointNaturalSource(rx_loc, rx_type, "imag")) + rxList.append(Impedance(rx_loc, orientation=rx_type, component="real")) + rxList.append(Impedance(rx_loc, orientation=rx_type, component="imag")) if rx_type in ["zx", "zy"]: - rxList.append(Point3DTipper(rx_loc, rx_type, "real")) - rxList.append(Point3DTipper(rx_loc, rx_type, "imag")) + rxList.append(Tipper(rx_loc, orientation=rx_type, component="real")) + rxList.append(Tipper(rx_loc, orientation=rx_type, component="imag")) srcList = [] if singleFreq: @@ -428,7 +418,7 @@ def setupSimpegNSEM_ePrimSec(inputSetup, comp="Imp", singleFreq=False, expMap=Tr if rx_type in ["xx", "xy", "yx", "yy"]: if comp == "Res": receiver_list.append( - PointNaturalSource( + Impedance( locations_e=rx_loc, locations_h=rx_loc, orientation=rx_type, @@ -436,7 +426,7 @@ def setupSimpegNSEM_ePrimSec(inputSetup, comp="Imp", singleFreq=False, expMap=Tr ) ) receiver_list.append( - PointNaturalSource( + Impedance( locations_e=rx_loc, locations_h=rx_loc, orientation=rx_type, @@ -444,11 +434,19 @@ def setupSimpegNSEM_ePrimSec(inputSetup, comp="Imp", singleFreq=False, expMap=Tr ) ) else: - receiver_list.append(PointNaturalSource(rx_loc, rx_type, "real")) - receiver_list.append(PointNaturalSource(rx_loc, rx_type, "imag")) + receiver_list.append( + Impedance(rx_loc, orientation=rx_type, component="real") + ) + receiver_list.append( + Impedance(rx_loc, orientation=rx_type, component="imag") + ) if rx_type in ["zx", "zy"]: - receiver_list.append(Point3DTipper(rx_loc, rx_type, "real")) - receiver_list.append(Point3DTipper(rx_loc, rx_type, "imag")) + receiver_list.append( + Impedance(rx_loc, orientation=rx_type, component="real") + ) + receiver_list.append( + Impedance(rx_loc, orientation=rx_type, component="imag") + ) # Source list source_list = [] diff --git a/tests/em/nsem/forward/test_1D_finite_volume.py b/tests/em/nsem/forward/test_1D_finite_volume.py index 5a836908d4..07b8ecd8b5 100644 --- a/tests/em/nsem/forward/test_1D_finite_volume.py +++ b/tests/em/nsem/forward/test_1D_finite_volume.py @@ -26,18 +26,14 @@ def setUp(self): self.frequencies = np.logspace(-2, 1, 30) rx_list = [ - nsem.receivers.PointNaturalSource( + nsem.receivers.Impedance( [[0]], orientation="xy", component="apparent_resistivity" ), - nsem.receivers.PointNaturalSource( - [[0]], orientation="xy", component="phase" - ), - nsem.receivers.PointNaturalSource( + nsem.receivers.Impedance([[0]], orientation="xy", component="phase"), + nsem.receivers.Impedance( [[0]], orientation="yx", component="apparent_resistivity" ), - nsem.receivers.PointNaturalSource( - [[0]], orientation="yx", component="phase" - ), + nsem.receivers.Impedance([[0]], orientation="yx", component="phase"), ] # simulation src_list = [ diff --git a/tests/em/nsem/forward/test_Recursive1D_VsAnalyticHalfspace.py b/tests/em/nsem/forward/test_Recursive1D_VsAnalyticHalfspace.py index 5d185bc28b..fb2e6fa5ac 100644 --- a/tests/em/nsem/forward/test_Recursive1D_VsAnalyticHalfspace.py +++ b/tests/em/nsem/forward/test_Recursive1D_VsAnalyticHalfspace.py @@ -13,10 +13,10 @@ def create_survey(freq): receivers_list = [ - nsem.receivers.PointNaturalSource(component="real"), - nsem.receivers.PointNaturalSource(component="imag"), - nsem.receivers.PointNaturalSource(component="app_res"), - nsem.receivers.PointNaturalSource(component="phase"), + nsem.receivers.Impedance([[]], component="real"), + nsem.receivers.Impedance([[]], component="imag"), + nsem.receivers.Impedance([[]], component="app_res"), + nsem.receivers.Impedance([[]], component="phase"), ] source_list = [nsem.sources.Planewave(receivers_list, f) for f in freq] diff --git a/tests/em/nsem/inversion/test_BC_Sims.py b/tests/em/nsem/inversion/test_BC_Sims.py index 8962b76272..84eecaacf8 100644 --- a/tests/em/nsem/inversion/test_BC_Sims.py +++ b/tests/em/nsem/inversion/test_BC_Sims.py @@ -53,18 +53,18 @@ def create_simulation_1d(sim_type, deriv_type): frequencies = np.logspace(-2, 1, 30) rx_list = [ - nsem.receivers.PointNaturalSource([[0]], orientation="xy", component="real"), - nsem.receivers.PointNaturalSource([[0]], orientation="xy", component="imag"), - nsem.receivers.PointNaturalSource( + nsem.receivers.Impedance([[0]], orientation="xy", component="real"), + nsem.receivers.Impedance([[0]], orientation="xy", component="imag"), + nsem.receivers.Impedance( [[0]], orientation="xy", component="apparent_resistivity" ), - nsem.receivers.PointNaturalSource([[0]], orientation="xy", component="phase"), - nsem.receivers.PointNaturalSource([[0]], orientation="yx", component="real"), - nsem.receivers.PointNaturalSource([[0]], orientation="yx", component="imag"), - nsem.receivers.PointNaturalSource( + nsem.receivers.Impedance([[0]], orientation="xy", component="phase"), + nsem.receivers.Impedance([[0]], orientation="yx", component="real"), + nsem.receivers.Impedance([[0]], orientation="yx", component="imag"), + nsem.receivers.Impedance( [[0]], orientation="yx", component="apparent_resistivity" ), - nsem.receivers.PointNaturalSource([[0]], orientation="yx", component="phase"), + nsem.receivers.Impedance([[0]], orientation="yx", component="phase"), ] src_list = [nsem.sources.Planewave(rx_list, frequency=f) for f in frequencies] survey = nsem.Survey(src_list) @@ -169,18 +169,12 @@ def create_simulation_2d(sim_type, deriv_type, mesh_type, fixed_boundary=False): sim_kwargs["h_bc"] = h_bc rx_list = [ - nsem.receivers.PointNaturalSource( - rx_locs, orientation="xy", component="real" - ), - nsem.receivers.PointNaturalSource( - rx_locs, orientation="xy", component="imag" - ), - nsem.receivers.PointNaturalSource( + nsem.receivers.Impedance(rx_locs, orientation="xy", component="real"), + nsem.receivers.Impedance(rx_locs, orientation="xy", component="imag"), + nsem.receivers.Impedance( rx_locs, orientation="xy", component="apparent_resistivity" ), - nsem.receivers.PointNaturalSource( - rx_locs, orientation="xy", component="phase" - ), + nsem.receivers.Impedance(rx_locs, orientation="xy", component="phase"), ] src_list = [nsem.sources.Planewave(rx_list, frequency=f) for f in frequencies] survey = nsem.Survey(src_list) @@ -219,18 +213,12 @@ def create_simulation_2d(sim_type, deriv_type, mesh_type, fixed_boundary=False): sim_kwargs["e_bc"] = e_bc rx_list = [ - nsem.receivers.PointNaturalSource( - rx_locs, orientation="yx", component="real" - ), - nsem.receivers.PointNaturalSource( - rx_locs, orientation="yx", component="imag" - ), - nsem.receivers.PointNaturalSource( + nsem.receivers.Impedance(rx_locs, orientation="yx", component="real"), + nsem.receivers.Impedance(rx_locs, orientation="yx", component="imag"), + nsem.receivers.Impedance( rx_locs, orientation="yx", component="apparent_resistivity" ), - nsem.receivers.PointNaturalSource( - rx_locs, orientation="yx", component="phase" - ), + nsem.receivers.Impedance(rx_locs, orientation="yx", component="phase"), ] src_list = [nsem.sources.Planewave(rx_list, frequency=f) for f in frequencies] survey = nsem.Survey(src_list) @@ -291,10 +279,10 @@ def test_errors(self): rx_locs = np.c_[np.linspace(-8000, 8000, 3), np.zeros(3)] mesh_1d = TensorMesh([5]) mesh_2d = TensorMesh([5, 5]) - r_xy = nsem.receivers.PointNaturalSource( + r_xy = nsem.receivers.Impedance( rx_locs, orientation="xy", component="apparent_resistivity" ) - r_yx = nsem.receivers.PointNaturalSource( + r_yx = nsem.receivers.Impedance( rx_locs, orientation="yx", component="apparent_resistivity" ) survey_xy = nsem.Survey([nsem.sources.Planewave([r_xy], frequency=10)]) diff --git a/tests/em/nsem/inversion/test_Problem1D_Adjoint.py b/tests/em/nsem/inversion/test_Problem1D_Adjoint.py index b81cda78b6..28449e0395 100644 --- a/tests/em/nsem/inversion/test_Problem1D_Adjoint.py +++ b/tests/em/nsem/inversion/test_Problem1D_Adjoint.py @@ -18,10 +18,10 @@ def JvecAdjointTest_1D(sigmaHalf, formulation="PrimSec"): # Define a receiver for each data type as a list receivers_list = [ - nsem.receivers.PointNaturalSource(component="real"), - nsem.receivers.PointNaturalSource(component="imag"), - nsem.receivers.PointNaturalSource(component="app_res"), - nsem.receivers.PointNaturalSource(component="phase"), + nsem.receivers.Impedance([[]], component="real"), + nsem.receivers.Impedance([[]], component="imag"), + nsem.receivers.Impedance([[]], component="app_res"), + nsem.receivers.Impedance([[]], component="phase"), ] # Use a list to define the planewave source at each frequency and assign receivers diff --git a/tests/em/nsem/inversion/test_Problem1D_Derivs.py b/tests/em/nsem/inversion/test_Problem1D_Derivs.py index 310a7ec4dc..de11c736b4 100644 --- a/tests/em/nsem/inversion/test_Problem1D_Derivs.py +++ b/tests/em/nsem/inversion/test_Problem1D_Derivs.py @@ -16,10 +16,10 @@ def DerivJvecTest_1D(halfspace_value, freq=False, expMap=True): # Define a receiver for each data type as a list receivers_list = [ - nsem.receivers.PointNaturalSource(component="real"), - nsem.receivers.PointNaturalSource(component="imag"), - nsem.receivers.PointNaturalSource(component="app_res"), - nsem.receivers.PointNaturalSource(component="phase"), + nsem.receivers.Impedance([[]], component="real"), + nsem.receivers.Impedance([[]], component="imag"), + nsem.receivers.Impedance([[]], component="app_res"), + nsem.receivers.Impedance([[]], component="phase"), ] # Use a list to define the planewave source at each frequency and assign receivers diff --git a/tests/em/nsem/inversion/test_complex_resistivity.py b/tests/em/nsem/inversion/test_complex_resistivity.py index 3c98b3d80c..ad3955dfd0 100644 --- a/tests/em/nsem/inversion/test_complex_resistivity.py +++ b/tests/em/nsem/inversion/test_complex_resistivity.py @@ -68,7 +68,9 @@ def create_simulation(self, rx_type="apparent_resistivity", rx_orientation="xy") rx_loc[:, 2] = -50 # Make a receiver list - rxList = [ns.Rx.PointNaturalSource(rx_loc, rx_orientation, rx_type)] + rxList = [ + ns.Rx.Impedance(rx_loc, orientation=rx_orientation, component=rx_type) + ] # Source list freqs = [10, 50, 200] @@ -103,11 +105,11 @@ def create_simulation_rx(self, rx_type="apparent_resistivity", rx_orientation="x # Make a receiver list rxList = [ - ns.Rx.PointNaturalSource( - orientation=rx_orientation, - component=rx_type, + ns.Rx.Impedance( locations_e=rx_loc, locations_h=rx_loc, + orientation=rx_orientation, + component=rx_type, ) ] @@ -145,7 +147,9 @@ def create_simulation_1dprimary_assign_mesh1d( rx_loc[:, 2] = -50 # Make a receiver list - rxList = [ns.Rx.PointNaturalSource(rx_loc, rx_orientation, rx_type)] + rxList = [ + ns.Rx.Impedance(rx_loc, orientation=rx_orientation, component=rx_type) + ] # give background a value x0 = self.mesh.x0 @@ -195,7 +199,9 @@ def create_simulation_1dprimary_assign( rx_loc[:, 2] = -50 # Make a receiver list - rxList = [ns.Rx.PointNaturalSource(rx_loc, rx_orientation, rx_type)] + rxList = [ + ns.Rx.Impedance(rx_loc, orientation=rx_orientation, component=rx_type) + ] # Source list freqs = [10, 50, 200] From f191fc774dc7903ad4d0677170f6df709e52653e Mon Sep 17 00:00:00 2001 From: "Devin C. Cowan" Date: Wed, 13 Aug 2025 09:39:30 -0700 Subject: [PATCH 156/194] Fix bug on `Impedance.eval` when orientation is "xx" or "yy" (#1692) Make `Impedance.eval()` to return an array full of zeros in case that the receiver's orientation is `"xx"` or "`yy`" and the mesh is not 3D. This solves a bug that was causing the NSEM 1D simulation to error out after trying to cast the output of `eval()` into an array. Add test for the bugfix. --------- Co-authored-by: Santiago Soler --- .../natural_source/receivers.py | 2 +- .../natural_source/utils/test_utils.py | 10 ++++-- tests/em/nsem/forward/test_receiver_eval.py | 36 +++++++++++++++++++ 3 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 tests/em/nsem/forward/test_receiver_eval.py diff --git a/simpeg/electromagnetics/natural_source/receivers.py b/simpeg/electromagnetics/natural_source/receivers.py index 17e1d4ba6a..ddeb5723ae 100644 --- a/simpeg/electromagnetics/natural_source/receivers.py +++ b/simpeg/electromagnetics/natural_source/receivers.py @@ -300,7 +300,7 @@ def orientation(self, var): def _eval_impedance(self, src, mesh, f): if mesh.dim < 3 and self.orientation in ["xx", "yy"]: - return 0.0 + return np.zeros((self.nD, 1), dtype=complex) e = f[src, "e"] h = f[src, "h"] if mesh.dim == 3: diff --git a/simpeg/electromagnetics/natural_source/utils/test_utils.py b/simpeg/electromagnetics/natural_source/utils/test_utils.py index 1f0e7297c6..88f8131db9 100644 --- a/simpeg/electromagnetics/natural_source/utils/test_utils.py +++ b/simpeg/electromagnetics/natural_source/utils/test_utils.py @@ -36,7 +36,7 @@ def getAppResPhs(NSEMdata, survey): ] -def setup1DSurvey(sigmaHalf, tD=False, structure=False): +def setup1DSurvey(sigmaHalf, tD=False, structure=False, rx_orientation="xy"): # Frequency num_frequencies = 33 freqs = np.logspace(3, -3, num_frequencies) @@ -68,10 +68,14 @@ def setup1DSurvey(sigmaHalf, tD=False, structure=False): receiver_list = [] for _ in range(len(["z1d", "z1d"])): receiver_list.append( - Impedance(mkvc(np.array([0.0]), 2).T, component="real", orientation="xy") + Impedance( + mkvc(np.array([0.0]), 2).T, component="real", orientation=rx_orientation + ) ) receiver_list.append( - Impedance(mkvc(np.array([0.0]), 2).T, component="imag", orientation="xy") + Impedance( + mkvc(np.array([0.0]), 2).T, component="imag", orientation=rx_orientation + ) ) # Source list source_list = [] diff --git a/tests/em/nsem/forward/test_receiver_eval.py b/tests/em/nsem/forward/test_receiver_eval.py new file mode 100644 index 0000000000..1f953198e9 --- /dev/null +++ b/tests/em/nsem/forward/test_receiver_eval.py @@ -0,0 +1,36 @@ +""" +Test receiver's ``eval`` method. +""" + +import numpy as np +import pytest +from simpeg.electromagnetics import natural_source as nsem +from simpeg.electromagnetics.natural_source.utils.test_utils import setup1DSurvey +from simpeg.utils.solver_utils import get_default_solver + + +@pytest.mark.parametrize("orientation", ["xx", "yy"]) +def test_zero_value(orientation): + """ + Test if ``Impedance.eval()`` returns an array of zeros on 1D problem + when orientation is ``"xx"`` or ``"yy"``. + + Test bugfix introduced in #1692. + """ + survey, sigma, _, mesh = setup1DSurvey(sigmaHalf=1e-2, rx_orientation=orientation) + + # Define simulation and precompute fields + solver = get_default_solver() + simulation = nsem.Simulation1DPrimarySecondary( + mesh, sigmaPrimary=sigma, sigma=sigma, survey=survey, solver=solver + ) + fields = simulation.fields() + + # Check if calling eval on each receiver returns the expected result + sources_and_receivers = ( + (src, rx) for src in survey.source_list for rx in src.receiver_list + ) + for source, receiver in sources_and_receivers: + result = receiver.eval(source, mesh, fields) + np.testing.assert_allclose(result, 0) + assert result.shape == (receiver.nD, 1) From d58b57fa8df57f7b5806e63877f32f7e6ae8a596 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Thu, 21 Aug 2025 10:36:45 -0600 Subject: [PATCH 157/194] Remove deprecated objects missed in v0.24.0 (#1658) Updates expired deprecation notifications missed in the 0.24.0 release. --------- Co-authored-by: Santiago Soler --- examples/09-flow/plot_fwd_flow_richards_1D.py | 1 - examples/09-flow/plot_inv_flow_richards_1D.py | 1 - simpeg/directives/__init__.py | 4 +- simpeg/directives/_directives.py | 506 +----------------- simpeg/directives/_regularization.py | 7 +- .../frequency_domain/sources.py | 9 - .../natural_source/__init__.py | 2 - .../natural_source/receivers.py | 152 +----- .../natural_source/utils/data_utils.py | 6 +- .../natural_source/utils/data_viewer.py | 4 +- .../static/resistivity/survey.py | 83 +-- .../spectral_induced_polarization/run.py | 22 +- .../spectral_induced_polarization/survey.py | 43 +- .../static/spontaneous_potential/__init__.py | 70 +-- .../static/utils/static_utils.py | 44 +- .../electromagnetics/time_domain/sources.py | 25 +- simpeg/electromagnetics/utils/__init__.py | 1 - .../electromagnetics/utils/current_utils.py | 8 - .../receivers.py | 17 - .../simulation.py | 26 +- simpeg/flow/richards/simulation.py | 12 - simpeg/maps/_injection.py | 86 +-- simpeg/maps/_parametric.py | 84 +-- simpeg/potential_fields/base.py | 147 +---- simpeg/potential_fields/gravity/simulation.py | 6 - simpeg/potential_fields/magnetics/__init__.py | 2 +- .../potential_fields/magnetics/simulation.py | 12 +- simpeg/potential_fields/magnetics/sources.py | 28 +- simpeg/regularization/__init__.py | 85 --- simpeg/regularization/base.py | 84 +-- simpeg/regularization/pgi.py | 9 - simpeg/regularization/regularization_mesh.py | 24 +- simpeg/regularization/sparse.py | 26 - simpeg/survey.py | 2 +- simpeg/utils/__init__.py | 43 -- simpeg/utils/code_utils.py | 35 -- simpeg/utils/coord_utils.py | 15 - simpeg/utils/curv_utils.py | 18 - simpeg/utils/mat_utils.py | 65 --- simpeg/utils/mesh_utils.py | 15 - simpeg/utils/model_builder.py | 22 - simpeg/utils/solver_utils.py | 3 + .../test_pgi_regularization.py | 14 - .../regularizations/test_regularization.py | 72 +-- tests/base/test_directives.py | 135 ++--- tests/base/test_maps.py | 253 +++------ tests/em/fdem/forward/test_FDEM_sources.py | 18 - .../test_Recursive1D_VsAnalyticHalfspace.py | 4 +- tests/em/nsem/test_nsem_point_deprecations.py | 215 -------- tests/em/static/test_SPjvecjtvecadj.py | 23 +- tests/em/static/test_dc_survey.py | 24 +- tests/em/static/test_sip_survey.py | 22 +- tests/em/static/test_spectral_ip_mappings.py | 36 +- tests/em/static/test_static_utils.py | 33 +- tests/em/tdem/test_TDEM_sources.py | 18 - tests/em/vrm/test_vrmfwd.py | 56 +- tests/flow/test_Richards.py | 1 - tests/pf/test_base_pf_simulation.py | 37 +- tests/pf/test_forward_Mag_Linear.py | 6 +- tests/pf/test_mag_uniform_background_field.py | 11 +- tests/utils/test_mat_utils.py | 44 +- tests/utils/test_model_builder.py | 40 +- ..._gravity_anomaly_irls_compare_weighting.py | 14 +- 63 files changed, 336 insertions(+), 2594 deletions(-) delete mode 100644 tests/em/nsem/test_nsem_point_deprecations.py diff --git a/examples/09-flow/plot_fwd_flow_richards_1D.py b/examples/09-flow/plot_fwd_flow_richards_1D.py index 1ce540fa12..9cbdd370f3 100644 --- a/examples/09-flow/plot_fwd_flow_richards_1D.py +++ b/examples/09-flow/plot_fwd_flow_richards_1D.py @@ -77,7 +77,6 @@ def run(plotIt=True): initial_conditions=h, do_newton=False, method="mixed", - debug=False, ) prob.time_steps = [(5, 25, 1.1), (60, 40)] diff --git a/examples/09-flow/plot_inv_flow_richards_1D.py b/examples/09-flow/plot_inv_flow_richards_1D.py index 567e41f112..20940bcf05 100644 --- a/examples/09-flow/plot_inv_flow_richards_1D.py +++ b/examples/09-flow/plot_inv_flow_richards_1D.py @@ -71,7 +71,6 @@ def run(plotIt=True): initial_conditions=h, do_newton=False, method="mixed", - debug=False, ) prob.time_steps = [(5, 25, 1.1), (60, 40)] diff --git a/simpeg/directives/__init__.py b/simpeg/directives/__init__.py index c8887d1726..4d542245cc 100644 --- a/simpeg/directives/__init__.py +++ b/simpeg/directives/__init__.py @@ -117,7 +117,6 @@ ScalingMultipleDataMisfits_ByEig, JointScalingSchedule, UpdateSensitivityWeights, - Update_IRLS, ProjectSphericalBounds, ) @@ -136,3 +135,6 @@ PairedBetaSchedule, MovingAndMultiTargetStopping, ) + +### Deprecated class +from ._regularization import Update_IRLS diff --git a/simpeg/directives/_directives.py b/simpeg/directives/_directives.py index ee4a4c2d4d..0cd7ef6bfe 100644 --- a/simpeg/directives/_directives.py +++ b/simpeg/directives/_directives.py @@ -11,7 +11,6 @@ from ..regularization import ( WeightedLeastSquares, BaseRegularization, - BaseSparse, Smallness, Sparse, SparseSmallness, @@ -32,7 +31,6 @@ validate_string, ) from ..utils.code_utils import ( - deprecate_class, deprecate_property, validate_type, validate_integer, @@ -71,9 +69,6 @@ class InversionDirective: _dmisfitPair = [BaseDataMisfit, ComboObjectiveFunction] def __init__(self, inversion=None, dmisfit=None, reg=None, verbose=False, **kwargs): - # Raise error on deprecated arguments - if (key := "debug") in kwargs.keys(): - raise TypeError(f"'{key}' property has been removed. Please use 'verbose'.") self.inversion = inversion self.dmisfit = dmisfit self.reg = reg @@ -94,10 +89,6 @@ def verbose(self): def verbose(self, value): self._verbose = validate_type("verbose", value, bool) - debug = deprecate_property( - verbose, "debug", "verbose", removal_version="0.19.0", error=True - ) - @property def inversion(self): """Inversion object associated with the directive. @@ -358,12 +349,6 @@ class BaseBetaEstimator(InversionDirective): Random seed used for random sampling. It can either be an int, a predefined Numpy random number generator, or any valid input to ``numpy.random.default_rng``. - seed : None or :class:`~simpeg.typing.RandomSeed`, optional - - .. deprecated:: 0.23.0 - - Argument ``seed`` is deprecated in favor of ``random_seed`` and will - be removed in SimPEG v0.24.0. """ @@ -371,27 +356,16 @@ def __init__( self, beta0_ratio=1.0, random_seed: RandomSeed | None = None, - seed: RandomSeed | None = None, **kwargs, ): - super().__init__(**kwargs) - self.beta0_ratio = beta0_ratio - # Deprecate seed argument - if seed is not None: - if random_seed is not None: - raise TypeError( - "Cannot pass both 'random_seed' and 'seed'." - "'seed' has been deprecated and will be removed in " - " SimPEG v0.24.0, please use 'random_seed' instead.", - ) - warnings.warn( - "'seed' has been deprecated and will be removed in " + if kwargs.pop("seed", None) is not None: + raise TypeError( + "'seed' has been removed in " " SimPEG v0.24.0, please use 'random_seed' instead.", - FutureWarning, - stacklevel=2, ) - random_seed = seed + super().__init__(**kwargs) + self.beta0_ratio = beta0_ratio self.random_seed = random_seed @property @@ -446,8 +420,7 @@ def validate(self, directive_list): "seed", "random_seed", removal_version="0.24.0", - future_warn=True, - error=False, + error=True, ) @@ -469,12 +442,6 @@ class BetaEstimateMaxDerivative(BaseBetaEstimator): Random seed used for random sampling. It can either be an int, a predefined Numpy random number generator, or any valid input to ``numpy.random.default_rng``. - seed : None or :class:`~simpeg.typing.RandomSeed`, optional - - .. deprecated:: 0.23.0 - - Argument ``seed`` is deprecated in favor of ``random_seed`` and will - be removed in SimPEG v0.24.0. Notes ----- @@ -549,12 +516,6 @@ class BetaEstimate_ByEig(BaseBetaEstimator): Random seed used for random sampling. It can either be an int, a predefined Numpy random number generator, or any valid input to ``numpy.random.default_rng``. - seed : None or :class:`~simpeg.typing.RandomSeed`, optional - - .. deprecated:: 0.23.0 - - Argument ``seed`` is deprecated in favor of ``random_seed`` and will - be removed in SimPEG v0.24.0. Notes ----- @@ -588,12 +549,9 @@ def __init__( beta0_ratio=1.0, n_pw_iter=4, random_seed: RandomSeed | None = None, - seed: RandomSeed | None = None, **kwargs, ): - super().__init__( - beta0_ratio=beta0_ratio, random_seed=random_seed, seed=seed, **kwargs - ) + super().__init__(beta0_ratio=beta0_ratio, random_seed=random_seed, **kwargs) self.n_pw_iter = n_pw_iter @property @@ -718,28 +676,17 @@ def __init__( alpha0_ratio=1.0, n_pw_iter=4, random_seed: RandomSeed | None = None, - seed: RandomSeed | None = None, **kwargs, ): - super().__init__(**kwargs) - self.alpha0_ratio = alpha0_ratio - self.n_pw_iter = n_pw_iter - # Deprecate seed argument - if seed is not None: - if random_seed is not None: - raise TypeError( - "Cannot pass both 'random_seed' and 'seed'." - "'seed' has been deprecated and will be removed in " - " SimPEG v0.24.0, please use 'random_seed' instead.", - ) - warnings.warn( - "'seed' has been deprecated and will be removed in " + if kwargs.pop("seed", None) is not None: + raise TypeError( + "'seed' has been removed in " " SimPEG v0.24.0, please use 'random_seed' instead.", - FutureWarning, - stacklevel=2, ) - random_seed = seed + super().__init__(**kwargs) + self.alpha0_ratio = alpha0_ratio + self.n_pw_iter = n_pw_iter self.random_seed = random_seed @property @@ -799,8 +746,7 @@ def random_seed(self, value): "seed", "random_seed", removal_version="0.24.0", - future_warn=True, - error=False, + error=True, ) def initialize(self): @@ -883,28 +829,17 @@ def __init__( chi0_ratio=None, n_pw_iter=4, random_seed: RandomSeed | None = None, - seed: RandomSeed | None = None, **kwargs, ): - super().__init__(**kwargs) - self.chi0_ratio = chi0_ratio - self.n_pw_iter = n_pw_iter - # Deprecate seed argument - if seed is not None: - if random_seed is not None: - raise TypeError( - "Cannot pass both 'random_seed' and 'seed'." - "'seed' has been deprecated and will be removed in " - " SimPEG v0.24.0, please use 'random_seed' instead.", - ) - warnings.warn( - "'seed' has been deprecated and will be removed in " + if kwargs.pop("seed", None) is not None: + raise TypeError( + "'seed' has been removed in " " SimPEG v0.24.0, please use 'random_seed' instead.", - FutureWarning, - stacklevel=2, ) - random_seed = seed + super().__init__(**kwargs) + self.chi0_ratio = chi0_ratio + self.n_pw_iter = n_pw_iter self.random_seed = random_seed @property @@ -964,8 +899,7 @@ def random_seed(self, value): "seed", "random_seed", removal_version="0.24.0", - future_warn=True, - error=False, + error=True, ) def initialize(self): @@ -2071,366 +2005,6 @@ def endIter(self): self.outDict[self.opt.iter] = iterDict -@deprecate_class(removal_version="0.24.0", error=False) -class Update_IRLS(InversionDirective): - f_old = 0 - f_min_change = 1e-2 - beta_tol = 1e-1 - beta_ratio_l2 = None - prctile = 100 - chifact_start = 1.0 - chifact_target = 1.0 - - # Solving parameter for IRLS (mode:2) - irls_iteration = 0 - minGNiter = 1 - iterStart = 0 - sphericalDomain = False - - # Beta schedule - ComboObjFun = False - mode = 1 - coolEpsOptimized = True - coolEps_p = True - coolEps_q = True - floorEps_p = 1e-8 - floorEps_q = 1e-8 - coolEpsFact = 1.2 - silent = False - fix_Jmatrix = False - - def __init__( - self, - max_irls_iterations=20, - update_beta=True, - beta_search=False, - coolingFactor=2.0, - coolingRate=1, - **kwargs, - ): - super().__init__(**kwargs) - self.max_irls_iterations = max_irls_iterations - self.update_beta = update_beta - self.beta_search = beta_search - self.coolingFactor = coolingFactor - self.coolingRate = coolingRate - - @property - def max_irls_iterations(self): - """Maximum irls iterations. - - Returns - ------- - int - """ - return self._max_irls_iterations - - @max_irls_iterations.setter - def max_irls_iterations(self, value): - self._max_irls_iterations = validate_integer( - "max_irls_iterations", value, min_val=0 - ) - - @property - def coolingFactor(self): - """Beta is divided by this value every `coolingRate` iterations. - - Returns - ------- - float - """ - return self._coolingFactor - - @coolingFactor.setter - def coolingFactor(self, value): - self._coolingFactor = validate_float( - "coolingFactor", value, min_val=0.0, inclusive_min=False - ) - - @property - def coolingRate(self): - """Cool after this number of iterations. - - Returns - ------- - int - """ - return self._coolingRate - - @coolingRate.setter - def coolingRate(self, value): - self._coolingRate = validate_integer("coolingRate", value, min_val=1) - - @property - def update_beta(self): - """Whether to update beta. - - Returns - ------- - bool - """ - return self._update_beta - - @update_beta.setter - def update_beta(self, value): - self._update_beta = validate_type("update_beta", value, bool) - - @property - def beta_search(self): - """Whether to do a beta search. - - Returns - ------- - bool - """ - return self._beta_search - - @beta_search.setter - def beta_search(self, value): - self._beta_search = validate_type("beta_search", value, bool) - - @property - def target(self): - if getattr(self, "_target", None) is None: - nD = 0 - for survey in self.survey: - nD += survey.nD - - self._target = nD * self.chifact_target - - return self._target - - @target.setter - def target(self, val): - self._target = val - - @property - def start(self): - if getattr(self, "_start", None) is None: - if isinstance(self.survey, list): - self._start = 0 - for survey in self.survey: - self._start += survey.nD * self.chifact_start - - else: - self._start = self.survey.nD * self.chifact_start - return self._start - - @start.setter - def start(self, val): - self._start = val - - def initialize(self): - if self.mode == 1: - self.norms = [] - for reg in self.reg.objfcts: - if not isinstance(reg, Sparse): - continue - self.norms.append(reg.norms) - reg.norms = [2.0 for obj in reg.objfcts] - reg.model = self.invProb.model - - # Update the model used by the regularization - for reg in self.reg.objfcts: - if not isinstance(reg, Sparse): - continue - - reg.model = self.invProb.model - - if self.sphericalDomain: - self.angleScale() - - def endIter(self): - if self.sphericalDomain: - self.angleScale() - - # Check if misfit is within the tolerance, otherwise scale beta - if np.all( - [ - np.abs(1.0 - self.invProb.phi_d / self.target) > self.beta_tol, - self.update_beta, - self.mode != 1, - ] - ): - ratio = self.target / self.invProb.phi_d - - if ratio > 1: - ratio = np.mean([2.0, ratio]) - else: - ratio = np.mean([0.75, ratio]) - - self.invProb.beta = self.invProb.beta * ratio - - if np.all([self.mode != 1, self.beta_search]): - print("Beta search step") - # self.update_beta = False - # Re-use previous model and continue with new beta - self.invProb.model = self.reg.objfcts[0].model - self.opt.xc = self.reg.objfcts[0].model - self.opt.iter -= 1 - return - - elif np.all([self.mode == 1, self.opt.iter % self.coolingRate == 0]): - self.invProb.beta = self.invProb.beta / self.coolingFactor - - # After reaching target misfit with l2-norm, switch to IRLS (mode:2) - if np.all([self.invProb.phi_d < self.start, self.mode == 1]): - self.start_irls() - - # Only update after GN iterations - if np.all( - [(self.opt.iter - self.iterStart) % self.minGNiter == 0, self.mode != 1] - ): - if self.stopping_criteria(): - self.opt.stopNextIteration = True - return - - # Print to screen - for reg in self.reg.objfcts: - if not isinstance(reg, Sparse): - continue - - for obj in reg.objfcts: - if isinstance(reg, (Sparse, BaseSparse)): - obj.irls_threshold = obj.irls_threshold / self.coolEpsFact - - self.irls_iteration += 1 - - # Reset the regularization matrices so that it is - # recalculated for current model. Do it to all levels of comboObj - for reg in self.reg.objfcts: - if not isinstance(reg, Sparse): - continue - - reg.update_weights(reg.model) - - self.update_beta = True - self.invProb.phi_m_last = self.reg(self.invProb.model) - - def start_irls(self): - if not self.silent: - print( - "Reached starting chifact with l2-norm regularization:" - + " Start IRLS steps..." - ) - - self.mode = 2 - - if getattr(self.opt, "iter", None) is None: - self.iterStart = 0 - else: - self.iterStart = self.opt.iter - - self.invProb.phi_m_last = self.reg(self.invProb.model) - - # Either use the supplied irls_threshold, or fix base on distribution of - # model values - for reg in self.reg.objfcts: - if not isinstance(reg, Sparse): - continue - - for obj in reg.objfcts: - threshold = np.percentile( - np.abs(obj.mapping * obj._delta_m(self.invProb.model)), self.prctile - ) - if isinstance(obj, SmoothnessFirstOrder): - threshold /= reg.regularization_mesh.base_length - - obj.irls_threshold = threshold - - # Re-assign the norms supplied by user l2 -> lp - for reg, norms in zip(self.reg.objfcts, self.norms): - if not isinstance(reg, Sparse): - continue - reg.norms = norms - - # Save l2-model - self.invProb.l2model = self.invProb.model.copy() - - # Print to screen - for reg in self.reg.objfcts: - if not isinstance(reg, Sparse): - continue - if not self.silent: - print("irls_threshold " + str(reg.objfcts[0].irls_threshold)) - - def angleScale(self): - """ - Update the scales used by regularization for the - different block of models - """ - # Currently implemented for MVI-S only - max_p = [] - for reg in self.reg.objfcts[0].objfcts: - f_m = abs(reg.f_m(reg.model)) - max_p += [np.max(f_m)] - - max_p = np.asarray(max_p).max() - - max_s = [np.pi, np.pi] - - for reg, var in zip(self.reg.objfcts[1:], max_s): - for obj in reg.objfcts: - # TODO Need to make weights_shapes a public method - obj.set_weights( - angle_scale=np.ones(obj._weights_shapes[0]) * max_p / var - ) - - def validate(self, directiveList): - dList = directiveList.dList - self_ind = dList.index(self) - lin_precond_ind = [isinstance(d, UpdatePreconditioner) for d in dList] - - if any(lin_precond_ind): - assert lin_precond_ind.index(True) > self_ind, ( - "The directive 'UpdatePreconditioner' must be after Update_IRLS " - "in the directiveList" - ) - else: - warnings.warn( - "Without a Linear preconditioner, convergence may be slow. " - "Consider adding `Directives.UpdatePreconditioner` to your " - "directives list", - stacklevel=2, - ) - return True - - def stopping_criteria(self): - """ - Check for stopping criteria of max_irls_iteration or minimum change. - """ - phim_new = 0 - for reg in self.reg.objfcts: - if isinstance(reg, (Sparse, BaseSparse)): - reg.model = self.invProb.model - phim_new += reg(reg.model) - - # Check for maximum number of IRLS cycles1 - if self.irls_iteration == self.max_irls_iterations: - if not self.silent: - print( - "Reach maximum number of IRLS cycles:" - + " {0:d}".format(self.max_irls_iterations) - ) - return True - - # Check if the function has changed enough - f_change = np.abs(self.f_old - phim_new) / (self.f_old + 1e-12) - if np.all( - [ - f_change < self.f_min_change, - self.irls_iteration > 1, - np.abs(1.0 - self.invProb.phi_d / self.target) < self.beta_tol, - ] - ): - print("Minimum decrease in regularization." + "End of IRLS") - return True - - self.f_old = phim_new - - return False - - class UpdatePreconditioner(InversionDirective): """ Create a Jacobi preconditioner for the linear problem @@ -2670,20 +2244,6 @@ def __init__( normalization_method="maximum", **kwargs, ): - # Raise errors on deprecated arguments - if (key := "everyIter") in kwargs.keys(): - raise TypeError( - f"'{key}' property has been removed. Please use 'every_iteration'.", - ) - if (key := "threshold") in kwargs.keys(): - raise TypeError( - f"'{key}' property has been removed. Please use 'threshold_value'.", - ) - if (key := "normalization") in kwargs.keys(): - raise TypeError( - f"'{key}' property has been removed. " - "Please define normalization using 'normalization_method'.", - ) super().__init__(**kwargs) @@ -2709,14 +2269,6 @@ def every_iteration(self): def every_iteration(self, value): self._every_iteration = validate_type("every_iteration", value, bool) - everyIter = deprecate_property( - every_iteration, - "everyIter", - "every_iteration", - removal_version="0.20.0", - error=True, - ) - @property def threshold_value(self): """Threshold value used to set minimum weighting value. @@ -2742,14 +2294,6 @@ def threshold_value(self): def threshold_value(self, value): self._threshold_value = validate_float("threshold_value", value, min_val=0.0) - threshold = deprecate_property( - threshold_value, - "threshold", - "threshold_value", - removal_version="0.20.0", - error=True, - ) - @property def threshold_method(self): """Threshold method for how `threshold_value` is applied: @@ -2802,14 +2346,6 @@ def normalization_method(self, value): "normalization_method", value, string_list=["minimum", "maximum"] ) - normalization = deprecate_property( - normalization_method, - "normalization", - "normalization_method", - removal_version="0.20.0", - error=True, - ) - def initialize(self): """Compute sensitivity weights upon starting the inversion.""" for reg in self.reg.objfcts: diff --git a/simpeg/directives/_regularization.py b/simpeg/directives/_regularization.py index 6afe17d172..92f9dac962 100644 --- a/simpeg/directives/_regularization.py +++ b/simpeg/directives/_regularization.py @@ -13,7 +13,7 @@ SmoothnessFirstOrder, WeightedLeastSquares, ) -from ..utils import validate_integer, validate_float +from ..utils import validate_integer, validate_float, deprecate_class @dataclass @@ -493,3 +493,8 @@ def update_scaling(self): continue obj.set_weights(angle_scale=np.ones_like(amplitude) * max_p / np.pi) + + +@deprecate_class(removal_version="0.24.0", error=True) +class Update_IRLS(UpdateIRLS): + pass diff --git a/simpeg/electromagnetics/frequency_domain/sources.py b/simpeg/electromagnetics/frequency_domain/sources.py index 9f7ea1a566..a5a8ea19c8 100644 --- a/simpeg/electromagnetics/frequency_domain/sources.py +++ b/simpeg/electromagnetics/frequency_domain/sources.py @@ -14,7 +14,6 @@ validate_direction, validate_integer, ) -from ...utils.code_utils import deprecate_property from ..utils import omega from ..utils import segmented_line_current_source_term, line_through_faces @@ -773,10 +772,6 @@ def __init__( **kwargs, ): kwargs.pop("moment", None) - - # Raise error on deprecated arguments - if (key := "N") in kwargs.keys(): - raise TypeError(f"'{key}' property has been removed. Please use 'n_turns'.") self.n_turns = n_turns super().__init__( @@ -877,10 +872,6 @@ def _srcFct(self, obsLoc, coordinates="cartesian"): out[np.isnan(out)] = 0 return self.n_turns * out - N = deprecate_property( - n_turns, "N", "n_turns", removal_version="0.19.0", error=True - ) - class PrimSecSigma(BaseFDEMSrc): def __init__( diff --git a/simpeg/electromagnetics/natural_source/__init__.py b/simpeg/electromagnetics/natural_source/__init__.py index 07cf986b76..337d6da777 100644 --- a/simpeg/electromagnetics/natural_source/__init__.py +++ b/simpeg/electromagnetics/natural_source/__init__.py @@ -27,8 +27,6 @@ receivers.Admittance receivers.ApparentConductivity receivers.Tipper - receivers.PointNaturalSource - receivers.Point3DTipper Sources ======= diff --git a/simpeg/electromagnetics/natural_source/receivers.py b/simpeg/electromagnetics/natural_source/receivers.py index ddeb5723ae..dcda2c43fd 100644 --- a/simpeg/electromagnetics/natural_source/receivers.py +++ b/simpeg/electromagnetics/natural_source/receivers.py @@ -4,7 +4,6 @@ validate_ndarray_with_shape, deprecate_class, ) -import warnings import numpy as np from scipy.constants import mu_0 from ...survey import BaseRx @@ -1229,158 +1228,19 @@ def evalDeriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=False): ) -@deprecate_class(removal_version="0.24.0", future_warn=True, replace_docstring=False) +@deprecate_class(removal_version="0.24.0", error=True, replace_docstring=False) class PointNaturalSource(Impedance): - """Point receiver class for magnetotelluric simulations. - + """ .. warning:: - This class is deprecated and will be removed in SimPEG v0.24.0. + This class was removed in SimPEG v0.24.0. Please use :class:`.natural_source.receivers.Impedance`. - - Assumes that the data locations are standard xyz coordinates; - i.e. (x,y,z) is (Easting, Northing, up). - - Parameters - ---------- - locations : (n_loc, n_dim) numpy.ndarray - Receiver locations. - orientation : {'xx', 'xy', 'yx', 'yy'} - MT receiver orientation. - component : {'real', 'imag', 'apparent_resistivity', 'phase'} - MT data type. """ - def __init__( - self, - locations=None, - orientation="xy", - component="real", - locations_e=None, - locations_h=None, - **kwargs, - ): - if locations is None: - if (locations_e is None) ^ ( - locations_h is None - ): # if only one of them is none - raise TypeError( - "Either locations or both locations_e and locations_h must be passed" - ) - if locations_e is None and locations_h is None: - warnings.warn( - "Using the default for locations is deprecated behavior. Please explicitly set locations. ", - FutureWarning, - stacklevel=2, - ) - locations_e = np.array([[0.0]]) - locations_h = locations_e - else: # locations was not None - if locations_e is not None or locations_h is not None: - raise TypeError( - "Cannot pass both locations and locations_e or locations_h at the same time." - ) - if isinstance(locations, list): - if len(locations) == 2: - locations_e = locations[0] - locations_h = locations[1] - elif len(locations) == 1: - locations_e = locations[0] - locations_h = locations[0] - else: - raise ValueError("incorrect size of list, must be length of 1 or 2") - else: - locations_e = locations_h = locations - - super().__init__( - locations_e=locations_e, - locations_h=locations_h, - orientation=orientation, - component=component, - **kwargs, - ) - - def eval(self, src, mesh, f, return_complex=False): # noqa: A003 - if return_complex: - warnings.warn( - "Calling with return_complex=True is deprecated in SimPEG 0.23. Instead set rx.component='complex'", - FutureWarning, - stacklevel=2, - ) - temp = self.component - self.component = "complex" - out = super().eval(src, mesh, f) - self.component = temp - else: - out = super().eval(src, mesh, f) - return out - - locations = property(lambda self: self._locations[0], Impedance.locations.fset) - -@deprecate_class(removal_version="0.24.0", future_warn=True, replace_docstring=False) +@deprecate_class(removal_version="0.24.0", error=True, replace_docstring=False) class Point3DTipper(Tipper): - """Point receiver class for Z-axis tipper simulations. - + """ .. warning:: - This class is deprecated and will be removed in SimPEG v0.24.0. + This class was removed in SimPEG v0.24.0. Please use :class:`.natural_source.receivers.Tipper`. - - Assumes that the data locations are standard xyz coordinates; - i.e. (x,y,z) is (Easting, Northing, up). - - Parameters - ---------- - locations : (n_loc, n_dim) numpy.ndarray - Receiver locations. - orientation : str, default = 'zx' - NSEM receiver orientation. Must be one of {'zx', 'zy'} - component : str, default = 'real' - NSEM data type. Choose one of {'real', 'imag', 'apparent_resistivity', 'phase'} """ - - def __init__( - self, - locations, - orientation="zx", - component="real", - locations_e=None, - locations_h=None, - **kwargs, - ): - # note locations_e and locations_h never did anything for this class anyways - # so can just issue a warning here... - if locations_e is not None or locations_h is not None: - warnings.warn( - "locations_e and locations_h are unused for this class", - UserWarning, - stacklevel=2, - ) - if isinstance(locations, list): - if len(locations) < 3: - locations = locations[0] - else: - raise ValueError("incorrect size of list, must be length of 1 or 2") - - super().__init__( - locations_h=locations, - orientation=orientation, - component=component, - **kwargs, - ) - - def eval(self, src, mesh, f, return_complex=False): # noqa: A003 - if return_complex: - warnings.warn( - "Calling with return_complex=True is deprecated in SimPEG 0.23. Instead set rx.component='complex'", - FutureWarning, - stacklevel=2, - ) - temp = self.component - self.component = "complex" - out = super().eval(src, mesh, f) - self.component = temp - else: - out = super().eval(src, mesh, f) - return out - - locations = property(lambda self: self._locations[0], Tipper.locations.fset) diff --git a/simpeg/electromagnetics/natural_source/utils/data_utils.py b/simpeg/electromagnetics/natural_source/utils/data_utils.py index af5c113f4f..1d226b0104 100644 --- a/simpeg/electromagnetics/natural_source/utils/data_utils.py +++ b/simpeg/electromagnetics/natural_source/utils/data_utils.py @@ -6,8 +6,6 @@ import simpeg as simpeg from simpeg.electromagnetics.natural_source.survey import Survey, Data from simpeg.electromagnetics.natural_source.receivers import ( - PointNaturalSource, - Point3DTipper, Impedance, Tipper, ) @@ -70,9 +68,9 @@ def extract_data_info(NSEMdata): src_rx_slice = survey_slices[src, rx] dL.append(NSEMdata.dobs[src_rx_slice]) freqL.append(np.ones(rx.nD) * src.frequency) - if isinstance(rx, (Impedance, PointNaturalSource)): + if isinstance(rx, Impedance): rxTL.extend((("z" + rx.orientation + " ") * rx.nD).split()) - if isinstance(rx, (Tipper, Point3DTipper)): + if isinstance(rx, Tipper): rxTL.extend((("t" + rx.orientation + " ") * rx.nD).split()) return np.concatenate(dL), np.concatenate(freqL), np.array(rxTL) diff --git a/simpeg/electromagnetics/natural_source/utils/data_viewer.py b/simpeg/electromagnetics/natural_source/utils/data_viewer.py index 1e8270fcaa..6054d4406f 100644 --- a/simpeg/electromagnetics/natural_source/utils/data_viewer.py +++ b/simpeg/electromagnetics/natural_source/utils/data_viewer.py @@ -71,9 +71,9 @@ def __init__(self, data, data_dict=None, backend="qt"): ] ) ) - if rx.PointNaturalSource in unique_rx: + if rx.Impedance in unique_rx: self.station_figs.append(ApparentResPhsStationPlot()) - if rx.Point3DTipper in unique_rx: + if rx.Tipper in unique_rx: self.station_figs.append(TipperAmplitudeStationPlot()) self.freqency_figs = [] diff --git a/simpeg/electromagnetics/static/resistivity/survey.py b/simpeg/electromagnetics/static/resistivity/survey.py index ce0cdc16d1..b1ad5bfbf7 100644 --- a/simpeg/electromagnetics/static/resistivity/survey.py +++ b/simpeg/electromagnetics/static/resistivity/survey.py @@ -1,4 +1,3 @@ -import warnings import numpy as np from ....utils.code_utils import validate_string @@ -27,16 +26,12 @@ def __init__( survey_geometry="surface", **kwargs, ): - if (key := "survey_type") in kwargs: - warnings.warn( - f"Argument '{key}' is ignored and will be removed in future " - "versions of SimPEG. Types of sources and their corresponding " - "receivers are obtained from their respective classes, without " + if kwargs.pop("survey_type", None) is not None: + raise TypeError( + "Argument 'survey_type' has been removed in SimPEG 0.24.0. Types of sources and" + "their corresponding receivers are obtained from their respective classes, without " "the need to specify the survey type.", - FutureWarning, - stacklevel=0, ) - kwargs.pop(key) super(Survey, self).__init__(source_list, **kwargs) self.survey_geometry = survey_geometry @@ -44,8 +39,6 @@ def __init__( def survey_geometry(self): """Survey geometry - This property is deprecated. - Returns ------- str @@ -65,36 +58,17 @@ def survey_type(self): """ ``survey_type`` has been removed. - Survey type; one of {"dipole-dipole", "pole-dipole", "dipole-pole", "pole-pole"} - .. important: The `survey_type` property has been removed. Types of sources and their corresponding receivers are obtained from their respective classes, without the need to specify the survey type. - - Returns - ------- - str - Survey type; one of {"dipole-dipole", "pole-dipole", "dipole-pole", "pole-pole"} """ - warnings.warn( - "Property 'survey_type' has been removed." - "Types of sources and their corresponding receivers are obtained from " - "their respective classes, without the need to specify the survey type.", - FutureWarning, - stacklevel=0, - ) + raise AttributeError("'survey_type' has been removed.") @survey_type.setter def survey_type(self, var): - warnings.warn( - "Property 'survey_type' has been removed." - "Types of sources and their corresponding receivers are obtained from " - "their respective classes, without the need to specify the survey type.", - FutureWarning, - stacklevel=0, - ) + raise AttributeError("'survey_type' has been removed.") def __repr__(self): return f"{self.__class__.__name__}(#sources: {self.nSrc}; #data: {self.nD})" @@ -193,8 +167,6 @@ def source_locations(self): def set_geometric_factor( self, space_type="halfspace", - data_type=None, - survey_type=None, ): """ Set and return the geometric factor for all data @@ -203,32 +175,18 @@ def set_geometric_factor( ---------- space_type : {'halfspace', 'wholespace'} Calculate geometric factors using a half-space or whole-space formula. - data_type : str, default = ``None`` - This input argument is now deprecated - survey_type : str, default = ``None`` - This input argument is now deprecated Returns ------- (nD) numpy.ndarray The geometric factor for each datum """ - if data_type is not None: - raise TypeError( - "The data_type kwarg has been removed, please set the data_type on the " - "receiver object itself." - ) - if survey_type is not None: - raise TypeError("The survey_type parameter is no longer needed") - geometric_factor = static_utils.geometric_factor(self, space_type=space_type) # geometric_factor = data.Data(self, geometric_factor) survey_slices = self.get_all_slices() for source in self.source_list: for rx in source.receiver_list: - if data_type is not None: - rx.data_type = data_type if rx.data_type == "apparent_resistivity": src_rx_slice = survey_slices[source, rx] rx._geometric_factor[source] = geometric_factor[src_rx_slice] @@ -277,14 +235,6 @@ def _set_abmn_locations(self): self._locations_m = np.vstack(locations_m) self._locations_n = np.vstack(locations_n) - def getABMN_locations(self): - """The 'getABMN_locations' method has been removed.""" - raise TypeError( - "The getABMN_locations method has been Removed. Please instead " - "ask for the property of interest: survey.locations_a, " - "survey.locations_b, survey.locations_m, or survey.locations_n." - ) - def drape_electrodes_on_topography( self, mesh, @@ -292,7 +242,6 @@ def drape_electrodes_on_topography( option="top", topography=None, force=False, - ind_active=None, ): """Shift electrode locations to discrete surface topography. @@ -308,20 +257,7 @@ def drape_electrodes_on_topography( Surface topography force : bool, default = ``False`` If ``True`` force electrodes to surface even if borehole - ind_active : numpy.ndarray of int or bool, optional - - .. deprecated:: 0.23.0 - - Argument ``ind_active`` is deprecated in favor of ``active_cells`` - and will be removed in SimPEG v0.24.0. - """ - # Deprecate ind_active argument - if ind_active is not None: - raise TypeError( - "'ind_active' has been deprecated and will be removed in " - " SimPEG v0.24.0, please use 'active_cells' instead." - ) if self.survey_geometry == "surface": loc_a = self.locations_a[:, :2] @@ -369,10 +305,3 @@ def drape_electrodes_on_topography( raise Exception( f"Input valid survey survey_geometry: {self.survey_geometry}" ) - - def drapeTopo(self, *args, **kwargs): - """This method is deprecated. See :meth:`drape_electrodes_on_topography`""" - raise TypeError( - "The drapeTopo method has been removed. Please instead " - "use the drape_electrodes_on_topography method." - ) diff --git a/simpeg/electromagnetics/static/spectral_induced_polarization/run.py b/simpeg/electromagnetics/static/spectral_induced_polarization/run.py index 8a13cd0b88..34d6963be1 100644 --- a/simpeg/electromagnetics/static/spectral_induced_polarization/run.py +++ b/simpeg/electromagnetics/static/spectral_induced_polarization/run.py @@ -1,4 +1,3 @@ -import warnings import numpy as np from simpeg import ( maps, @@ -20,7 +19,7 @@ def spectral_ip_mappings( is_log_eta=True, is_log_tau=True, is_log_c=True, - indActive=None, + **kwargs, ): """ Generates Mappings for Spectral Induced Polarization Simulation. @@ -38,21 +37,14 @@ def spectral_ip_mappings( TODO: Illustrate input and output variables """ + # Deprecate indActive argument - if indActive is not None: - if active_cells is not None: - raise TypeError( - "Cannot pass both 'active_cells' and 'indActive'." - "'indActive' has been deprecated and will be removed in " - " SimPEG v0.24.0, please use 'active_cells' instead.", - ) - warnings.warn( - "'indActive' has been deprecated and will be removed in " - " SimPEG v0.24.0, please use 'active_cells' instead.", - FutureWarning, - stacklevel=2, + if kwargs.pop("indActive", None) is not None: + raise TypeError( + "'indActive' was removed in SimPEG v0.24.0, please use 'active_cells' instead." ) - active_cells = indActive + if kwargs: # TODO Remove this when removing kwargs argument. + raise TypeError("Unsupported keyword argument " + kwargs.popitem()[0]) if active_cells is None: active_cells = np.ones(mesh.nC, dtype=bool) diff --git a/simpeg/electromagnetics/static/spectral_induced_polarization/survey.py b/simpeg/electromagnetics/static/spectral_induced_polarization/survey.py index 0826d84f4b..0fdc0c54a4 100644 --- a/simpeg/electromagnetics/static/spectral_induced_polarization/survey.py +++ b/simpeg/electromagnetics/static/spectral_induced_polarization/survey.py @@ -1,4 +1,3 @@ -import warnings from ....survey import BaseTimeSurvey from . import sources from . import receivers @@ -20,20 +19,13 @@ class Survey(BaseTimeSurvey): _n_pulse = 2 _T = 8.0 - def __init__(self, source_list=None, survey_geometry="surface", **kwargs): - if (key := "survey_type") in kwargs: - warnings.warn( - f"Argument '{key}' is ignored and will be removed in future " - "versions of SimPEG. Types of sources and their corresponding " - "receivers are obtained from their respective classes, without " + def __init__(self, source_list, survey_geometry="surface", **kwargs): + if kwargs.pop("survey_type", None) is not None: + raise TypeError( + "Argument 'survey_type' has been removed in SimPEG 0.24.0. Types of sources and" + "their corresponding receivers are obtained from their respective classes, without " "the need to specify the survey type.", - FutureWarning, - stacklevel=1, ) - kwargs.pop(key) - - if source_list is None: - raise AttributeError("Survey cannot be instantiated without sources") super(Survey, self).__init__(source_list, **kwargs) self.survey_geometry = survey_geometry @@ -61,7 +53,7 @@ def T(self): @property def survey_geometry(self): - """Survey geometry; one of {"surface", "borehole", "general"} + """Survey geometry Returns ------- @@ -81,36 +73,17 @@ def survey_type(self): """ ``survey_type`` has been removed. - Survey type; one of {"dipole-dipole", "pole-dipole", "dipole-pole", "pole-pole"} - .. important: The `survey_type` property has been removed. Types of sources and their corresponding receivers are obtained from their respective classes, without the need to specify the survey type. - - Returns - ------- - str - Survey type; one of {"dipole-dipole", "pole-dipole", "dipole-pole", "pole-pole"} """ - warnings.warn( - "Property 'survey_type' has been removed." - "Types of sources and their corresponding receivers are obtained from " - "their respective classes, without the need to specify the survey type.", - FutureWarning, - stacklevel=1, - ) + raise AttributeError("'survey_type' has been removed.") @survey_type.setter def survey_type(self, var): - warnings.warn( - "Property 'survey_type' has been removed." - "Types of sources and their corresponding receivers are obtained from " - "their respective classes, without the need to specify the survey type.", - FutureWarning, - stacklevel=1, - ) + raise AttributeError("'survey_type' has been removed.") @property def n_locations(self): diff --git a/simpeg/electromagnetics/static/spontaneous_potential/__init__.py b/simpeg/electromagnetics/static/spontaneous_potential/__init__.py index 86323379dd..758d141a51 100644 --- a/simpeg/electromagnetics/static/spontaneous_potential/__init__.py +++ b/simpeg/electromagnetics/static/spontaneous_potential/__init__.py @@ -1,69 +1,3 @@ -""" -============================================================================================ -Spontaneous Potential (:mod:`simpeg.electromagnetics.static.spontaneous_potential`) -============================================================================================ -.. currentmodule:: simpeg.electromagnetics.static.spontaneous_potential - -.. admonition:: important - - This module will be deprecated in favour of ``simpeg.electromagnetics.static.self_potential`` - - -Simulations -=========== -.. autosummary:: - :toctree: generated/ - - Simulation3DCellCentered - -Receivers -========= -This module makes use of the receivers in :mod:`simpeg.electromagnetics.static.resistivity` - -Sources -======= -.. autosummary:: - :toctree: generated/ - - sources.StreamingCurrents - -Surveys -======= -.. autosummary:: - :toctree: generated/ - - Survey - -Maps -==== -The spontaneous potential simulation provides two specialized maps to extend to inversions -with different types of model sources. - -.. autosummary:: - :toctree: generated/ - - CurrentDensityMap - HydraulicHeadMap - -""" - -import warnings - -warnings.warn( - ( - "The 'spontaneous_potential' module has been renamed to 'self_potential'. " - "Please use the 'self_potential' module instead. " - "The 'spontaneous_potential' module will be removed in SimPEG 0.23." - ), - FutureWarning, - stacklevel=2, +raise ImportError( + "The 'spontaneous_potential' module has been moved to 'self_potential'." ) - -from ..self_potential.simulation import ( - Simulation3DCellCentered, - Survey, - CurrentDensityMap, - HydraulicHeadMap, -) -from ..self_potential import sources -from ..self_potential import simulation diff --git a/simpeg/electromagnetics/static/utils/static_utils.py b/simpeg/electromagnetics/static/utils/static_utils.py index 7a552a03bd..8722e02e66 100644 --- a/simpeg/electromagnetics/static/utils/static_utils.py +++ b/simpeg/electromagnetics/static/utils/static_utils.py @@ -197,12 +197,10 @@ def pseudo_locations(survey, wenner_tolerance=0.1, **kwargs): if not isinstance(survey, dc.Survey): raise TypeError(f"Input must be instance of {dc.Survey}, not {type(survey)}") - if len(kwargs) > 0: - warnings.warn( - "The keyword arguments of this function have been deprecated." + if kwargs: + raise TypeError( + "The keyword arguments of this function have been removed." " All of the necessary information is now in the DC survey class", - DeprecationWarning, - stacklevel=2, ) # Pre-allocate @@ -571,11 +569,6 @@ def plot_pseudosection( The axis object that holds the plot """ - - removed_kwargs = ["dim", "y_values", "sameratio", "survey_type"] - for kwarg in removed_kwargs: - if kwarg in kwargs: - raise TypeError(r"The {kwarg} keyword has been removed.") if len(kwargs) > 0: warnings.warn( f"plot_pseudosection unused kwargs: {list(kwargs.keys())}", stacklevel=2 @@ -1597,9 +1590,7 @@ def gettopoCC(mesh, ind_active, option="top"): raise NotImplementedError(f"{type(mesh)} mesh is not supported.") -def drapeTopotoLoc( - mesh, pts, active_cells=None, option="top", topo=None, ind_active=None -): +def drapeTopotoLoc(mesh, pts, active_cells=None, option="top", topo=None, **kwargs): """Drape locations right below discretized surface topography This function projects the set of locations provided to the discrete @@ -1620,28 +1611,15 @@ def drapeTopotoLoc( topo : (n, dim) numpy.ndarray Surface topography. Can be used if an active indices array cannot be provided for the input parameter 'ind_active' - ind_active : numpy.ndarray of int or bool, optional - - .. deprecated:: 0.23.0 - - Argument ``ind_active`` is deprecated in favor of ``active_cells`` - and will be removed in SimPEG v0.24.0. """ - # Deprecate ind_active argument - if ind_active is not None: - if active_cells is not None: - raise TypeError( - "Cannot pass both 'active_cells' and 'ind_active'." - "'ind_active' has been deprecated and will be removed in " - " SimPEG v0.24.0, please use 'active_cells' instead.", - ) - warnings.warn( - "'ind_active' has been deprecated and will be removed in " - " SimPEG v0.24.0, please use 'active_cells' instead.", - FutureWarning, - stacklevel=2, + + # Deprecate indActive argument + if kwargs.pop("indActive", None) is not None: + raise TypeError( + "'indActive' was removed in SimPEG v0.24.0, please use 'active_cells' instead." ) - active_cells = ind_active + if kwargs: # TODO Remove this when removing kwargs argument. + raise TypeError("Unsupported keyword argument " + kwargs.popitem()[0]) if isinstance(mesh, discretize.CurvilinearMesh): raise ValueError("Curvilinear mesh is not supported.") diff --git a/simpeg/electromagnetics/time_domain/sources.py b/simpeg/electromagnetics/time_domain/sources.py index afef51aa4e..c51b2869d4 100644 --- a/simpeg/electromagnetics/time_domain/sources.py +++ b/simpeg/electromagnetics/time_domain/sources.py @@ -6,7 +6,6 @@ from ...utils import Zero, sdiag from ...utils.code_utils import ( - deprecate_property, validate_callable, validate_direction, validate_float, @@ -528,18 +527,6 @@ class TriangularWaveform(TrapezoidWaveform): """ def __init__(self, start_time, off_time, peak_time, **kwargs): - if kwargs.get("startTime", None): - AttributeError( - "startTime will be deprecated in 0.17.0. Please update your code to use start_time instead", - ) - if kwargs.get("peak_time", None): - AttributeError( - "peak_time will be deprecated in 0.17.0. Please update your code to use peak_time instead", - ) - if kwargs.get("offTime", None): - AttributeError( - "offTime will be deprecated in 0.17.0. Please update your code to use off_time instead", - ) ramp_on = np.r_[start_time, peak_time] ramp_off = np.r_[peak_time, off_time] @@ -1489,16 +1476,10 @@ def __init__( if location is None: location = np.r_[0.0, 0.0, 0.0] - if "moment" in kwargs: - kwargs.pop("moment") - - # Raise error on deprecated arguments - if (key := "N") in kwargs.keys(): - raise TypeError(f"'{key}' property has been removed. Please use 'n_turns'.") self.n_turns = n_turns BaseTDEMSrc.__init__( - self, receiver_list=receiver_list, location=location, moment=None, **kwargs + self, receiver_list=receiver_list, location=location, **kwargs ) self.orientation = orientation @@ -1595,10 +1576,6 @@ def _srcFct(self, obsLoc, coordinates="cartesian"): out[np.isnan(out)] = 0 return self.n_turns * out - N = deprecate_property( - n_turns, "N", "n_turns", removal_version="0.19.0", error=True - ) - class LineCurrent(BaseTDEMSrc): """Line current source. diff --git a/simpeg/electromagnetics/utils/__init__.py b/simpeg/electromagnetics/utils/__init__.py index bf7fd197b9..ab1970bcbe 100644 --- a/simpeg/electromagnetics/utils/__init__.py +++ b/simpeg/electromagnetics/utils/__init__.py @@ -41,7 +41,6 @@ from .current_utils import ( edge_basis_function, getStraightLineCurrentIntegral, - getSourceTermLineCurrentPolygon, segmented_line_current_source_term, line_through_faces, ) diff --git a/simpeg/electromagnetics/utils/current_utils.py b/simpeg/electromagnetics/utils/current_utils.py index 959f6e1260..698c7a45b2 100644 --- a/simpeg/electromagnetics/utils/current_utils.py +++ b/simpeg/electromagnetics/utils/current_utils.py @@ -511,11 +511,3 @@ def not_aligned_error(i): ) return current - - -def getSourceTermLineCurrentPolygon(xorig, hx, hy, hz, px, py, pz): - """getSourceTermLineCurrentPolygon is deprecated. Use :func:`segmented_line_current_source_term`""" - raise NotImplementedError( - "getSourceTermLineCurrentPolygon has been deprecated and will be" - "removed in SimPEG 0.17.0. Please use segmented_line_current_source_term.", - ) diff --git a/simpeg/electromagnetics/viscous_remanent_magnetization/receivers.py b/simpeg/electromagnetics/viscous_remanent_magnetization/receivers.py index 5d5823311f..cc1c730704 100644 --- a/simpeg/electromagnetics/viscous_remanent_magnetization/receivers.py +++ b/simpeg/electromagnetics/viscous_remanent_magnetization/receivers.py @@ -30,11 +30,6 @@ class Point(BaseRx): def __init__( self, locations=None, times=None, field_type=None, orientation="z", **kwargs ): - if kwargs.pop("fieldType", None): - raise AttributeError( - "'fieldType' is a deprecated property. Please use 'field_type' instead." - "'fieldType' be removed in SimPEG 0.17.0." - ) if field_type is None: raise AttributeError( "VRM receiver class cannot be instantiated witout 'field_type" @@ -179,18 +174,6 @@ def __init__( quadrature_order=3, **kwargs, ): - if "nTurns" in kwargs: - raise AttributeError( - "'nTurns' is a deprecated property. Please use 'n_turns' instead." - "'nTurns' be removed in SimPEG 0.17.0." - ) - - if "quadOrder" in kwargs: - raise AttributeError( - "'quadOrder' is a deprecated property. Please use 'quadrature_order' instead." - "'quadOrder' be removed in SimPEG 0.17.0." - ) - super(SquareLoop, self).__init__( locations=locations, times=times, diff --git a/simpeg/electromagnetics/viscous_remanent_magnetization/simulation.py b/simpeg/electromagnetics/viscous_remanent_magnetization/simulation.py index fe9f707ad5..6d4a63cecc 100644 --- a/simpeg/electromagnetics/viscous_remanent_magnetization/simulation.py +++ b/simpeg/electromagnetics/viscous_remanent_magnetization/simulation.py @@ -1,4 +1,3 @@ -import warnings import discretize import numpy as np import scipy.sparse as sp @@ -35,9 +34,13 @@ def __init__( refinement_factor=None, refinement_distance=None, active_cells=None, - indActive=None, **kwargs, ): + # Deprecate indActive argument + if kwargs.pop("indActive", None) is not None: + raise TypeError( + "'indActive' was removed in SimPEG v0.24.0, please use 'active_cells' instead." + ) self.mesh = mesh super().__init__(survey=survey, **kwargs) @@ -53,22 +56,6 @@ def __init__( ) self.refinement_distance = refinement_distance - # Deprecate indActive argument - if indActive is not None: - if active_cells is not None: - raise TypeError( - "Cannot pass both 'active_cells' and 'indActive'." - "'indActive' has been deprecated and will be removed in " - " SimPEG v0.24.0, please use 'active_cells' instead.", - ) - warnings.warn( - "'indActive' has been deprecated and will be removed in " - " SimPEG v0.24.0, please use 'active_cells' instead.", - FutureWarning, - stacklevel=2, - ) - active_cells = indActive - if active_cells is None: active_cells = np.ones(self.mesh.n_cells, dtype=bool) self.active_cells = active_cells @@ -160,8 +147,7 @@ def active_cells(self, value): "indActive", "active_cells", removal_version="0.24.0", - future_warn=True, - error=False, + error=True, ) def _getH0matrix(self, xyz, pp): diff --git a/simpeg/flow/richards/simulation.py b/simpeg/flow/richards/simulation.py index 20d5055c9e..29e5b10a53 100644 --- a/simpeg/flow/richards/simulation.py +++ b/simpeg/flow/richards/simulation.py @@ -10,7 +10,6 @@ from ...utils import ( validate_type, validate_ndarray_with_shape, - deprecate_property, validate_string, validate_integer, validate_float, @@ -37,9 +36,6 @@ def __init__( root_finder_tol=1e-4, **kwargs, ): - debug = kwargs.pop("debug", None) - if debug is not None: - self.debug = debug super().__init__(mesh=mesh, **kwargs) self.hydraulic_conductivity = hydraulic_conductivity self.water_retention = water_retention @@ -104,14 +100,6 @@ def initial_conditions(self, value): "initial_conditions", value ) - debug = deprecate_property( - BaseTimeSimulation.verbose, - "debug", - "verbose", - removal_version="0.19.0", - future_warn=True, - ) - @property def method(self): """Formulation used. diff --git a/simpeg/maps/_injection.py b/simpeg/maps/_injection.py index e99c5bad10..492fca468e 100644 --- a/simpeg/maps/_injection.py +++ b/simpeg/maps/_injection.py @@ -2,7 +2,6 @@ Injection and interpolation map classes. """ -import warnings import discretize import numpy as np import scipy.sparse as sp @@ -23,13 +22,19 @@ class Mesh2Mesh(IdentityMap): Takes a model on one mesh are translates it to another mesh. """ - def __init__(self, meshes, active_cells=None, indActive=None, **kwargs): + def __init__(self, meshes, active_cells=None, **kwargs): # Sanity checks for the meshes parameter try: mesh, mesh2 = meshes except TypeError: raise TypeError("Couldn't unpack 'meshes' into two meshes.") + # Deprecate indActive argument + if kwargs.pop("indActive", None) is not None: + raise TypeError( + "'indActive' was removed in SimPEG v0.24.0, please use 'active_cells' instead.", + ) + super().__init__(mesh=mesh, **kwargs) self.mesh2 = mesh2 @@ -40,22 +45,6 @@ def __init__(self, meshes, active_cells=None, indActive=None, **kwargs): + "Both meshes must have the same dimension." ) - # Deprecate indActive argument - if indActive is not None: - if active_cells is not None: - raise TypeError( - "Cannot pass both 'active_cells' and 'indActive'." - "'indActive' has been deprecated and will be removed in " - " SimPEG v0.24.0, please use 'active_cells' instead.", - ) - warnings.warn( - "'indActive' has been deprecated and will be removed in " - " SimPEG v0.24.0, please use 'active_cells' instead.", - FutureWarning, - stacklevel=2, - ) - active_cells = indActive - self.active_cells = active_cells # reset to not accepted None for mesh @@ -100,8 +89,7 @@ def active_cells(self, value): "indActive", "active_cells", removal_version="0.24.0", - future_warn=True, - error=False, + error=True, ) @property @@ -165,20 +153,6 @@ class InjectActiveCells(IdentityMap): or a ``numpy.ndarray`` of ``int`` containing the indices of the active cells. value_inactive : float or numpy.ndarray The physical property value assigned to all inactive cells in the mesh - indActive : numpy.ndarray - - .. deprecated:: 0.23.0 - - Argument ``indActive`` is deprecated in favor of ``active_cells`` and will - be removed in SimPEG v0.24.0. - - valInactive : float or numpy.ndarray - - .. deprecated:: 0.23.0 - - Argument ``valInactive`` is deprecated in favor of ``value_inactive`` and - will be removed in SimPEG v0.24.0. - """ def __init__( @@ -187,43 +161,23 @@ def __init__( active_cells=None, value_inactive=0.0, nC=None, - indActive=None, - valInactive=0.0, + **kwargs, ): self.mesh = mesh self.nC = nC or mesh.nC # Deprecate indActive argument - if indActive is not None: - if active_cells is not None: - raise TypeError( - "Cannot pass both 'active_cells' and 'indActive'." - "'indActive' has been deprecated and will be removed in " - " SimPEG v0.24.0, please use 'active_cells' instead.", - ) - warnings.warn( - "'indActive' has been deprecated and will be removed in " - " SimPEG v0.24.0, please use 'active_cells' instead.", - FutureWarning, - stacklevel=2, + if kwargs.pop("indActive", None) is not None: + raise TypeError( + "'indActive' was removed in SimPEG v0.24.0, please use 'active_cells' instead." ) - active_cells = indActive - # Deprecate valInactive argument - if not isinstance(valInactive, Number) or valInactive != 0.0: - if not isinstance(value_inactive, Number) or value_inactive != 0.0: - raise TypeError( - "Cannot pass both 'value_inactive' and 'valInactive'." - "'valInactive' has been deprecated and will be removed in " - " SimPEG v0.24.0, please use 'value_inactive' instead.", - ) - warnings.warn( - "'valInactive' has been deprecated and will be removed in " - " SimPEG v0.24.0, please use 'value_inactive' instead.", - FutureWarning, - stacklevel=2, + if kwargs.pop("valInactive", None) is not None: + raise TypeError( + "'valInactive' was removed in SimPEG v0.24.0, please use 'value_inactive' instead." ) - value_inactive = valInactive + if kwargs: # TODO Remove this when removing kwargs argument. + raise TypeError("Unsupported keyword argument " + kwargs.popitem()[0]) self.active_cells = active_cells self._nP = np.sum(self.active_cells) @@ -260,8 +214,7 @@ def value_inactive(self, value): "valInactive", "value_inactive", removal_version="0.24.0", - future_warn=True, - error=False, + error=True, ) @property @@ -286,8 +239,7 @@ def active_cells(self, value): "indActive", "active_cells", removal_version="0.24.0", - future_warn=True, - error=False, + error=True, ) @property diff --git a/simpeg/maps/_parametric.py b/simpeg/maps/_parametric.py index 814808eb84..06b8e05b7c 100644 --- a/simpeg/maps/_parametric.py +++ b/simpeg/maps/_parametric.py @@ -2,7 +2,6 @@ Parametric map classes. """ -import warnings import discretize import numpy as np from numpy.polynomial import polynomial @@ -335,12 +334,6 @@ class ParametricPolyMap(IdentityMap): Active cells array. Can be a boolean ``numpy.ndarray`` of length ``mesh.n_cells`` or a ``numpy.ndarray`` of ``int`` containing the indices of the active cells. - actInd : numpy.ndarray, optional - - .. deprecated:: 0.23.0 - - Argument ``actInd`` is deprecated in favor of ``active_cells`` and will - be removed in SimPEG v0.24.0. Examples -------- @@ -404,7 +397,7 @@ def __init__( normal="X", active_cells=None, slope=1e4, - actInd=None, + **kwargs, ): super().__init__(mesh=mesh) self.logSigma = logSigma @@ -412,21 +405,13 @@ def __init__( self.normal = normal self.slope = slope - # Deprecate actInd argument - if actInd is not None: - if active_cells is not None: - raise TypeError( - "Cannot pass both 'active_cells' and 'actInd'." - "'actInd' has been deprecated and will be removed in " - " SimPEG v0.24.0, please use 'active_cells' instead.", - ) - warnings.warn( - "'actInd' has been deprecated and will be removed in " - " SimPEG v0.24.0, please use 'active_cells' instead.", - FutureWarning, - stacklevel=2, + # Deprecate indActive argument + if kwargs.pop("indActive", None) is not None: + raise TypeError( + "'indActive' was removed in SimPEG v0.24.0, please use 'active_cells' instead." ) - active_cells = actInd + if kwargs: # TODO Remove this when removing kwargs argument. + raise TypeError("Unsupported keyword argument " + kwargs.popitem()[0]) if active_cells is None: active_cells = np.ones(mesh.n_cells, dtype=bool) @@ -498,8 +483,7 @@ def active_cells(self, value): "actInd", "active_cells", removal_version="0.24.0", - future_warn=True, - error=False, + error=True, ) @property @@ -1103,13 +1087,6 @@ class BaseParametric(IdentityMap): active_cells : numpy.ndarray, optional Active cells array. Can be a boolean ``numpy.ndarray`` of length *mesh.nC* or a ``numpy.ndarray`` of ``int`` containing the indices of the active cells. - indActive : numpy.ndarray - - .. deprecated:: 0.23.0 - - Argument ``indActive`` is deprecated in favor of ``active_cells`` and will - be removed in SimPEG v0.24.0. - """ @@ -1119,26 +1096,15 @@ def __init__( slope=None, slopeFact=1.0, active_cells=None, - indActive=None, **kwargs, ): - super(BaseParametric, self).__init__(mesh, **kwargs) - # Deprecate indActive argument - if indActive is not None: - if active_cells is not None: - raise TypeError( - "Cannot pass both 'active_cells' and 'indActive'." - "'indActive' has been deprecated and will be removed in " - " SimPEG v0.24.0, please use 'active_cells' instead.", - ) - warnings.warn( - "'indActive' has been deprecated and will be removed in " - " SimPEG v0.24.0, please use 'active_cells' instead.", - FutureWarning, - stacklevel=2, + if kwargs.pop("indActive", None) is not None: + raise TypeError( + "'indActive' was removed in SimPEG v0.24.0, please use 'active_cells' instead." ) - active_cells = indActive + + super(BaseParametric, self).__init__(mesh, **kwargs) self.active_cells = active_cells self.slopeFact = slopeFact @@ -1189,8 +1155,7 @@ def active_cells(self, value): "indActive", "active_cells", removal_version="0.24.0", - future_warn=True, - error=False, + error=True, ) @property @@ -1321,12 +1286,6 @@ class ParametricLayer(BaseParametric): slopeFact : float Scaling factor for the sharpness of the boundaries based on cell size. Using this option, we set *a = slopeFact / dh*. - indActive : numpy.ndarray - - .. deprecated:: 0.23.0 - - Argument ``indActive`` is deprecated in favor of ``active_cells`` and will - be removed in SimPEG v0.24.0. Examples -------- @@ -1358,9 +1317,6 @@ class ParametricLayer(BaseParametric): """ - def __init__(self, mesh, **kwargs): - super().__init__(mesh, **kwargs) - @property def nP(self): """Number of model parameters the mapping acts on; i.e 4 @@ -1589,12 +1545,6 @@ class ParametricBlock(BaseParametric): Epsilon value used in the ekblom representation of the block p : float p-value used in the ekblom representation of the block. - indActive : numpy.ndarray - - .. deprecated:: 0.23.0 - - Argument ``indActive`` is deprecated in favor of ``active_cells`` and will - be removed in SimPEG v0.24.0. Examples -------- @@ -1938,12 +1888,6 @@ class ParametricEllipsoid(ParametricBlock): Using this option, we set *a = slopeFact / dh*. epsilon : float Epsilon value used in the ekblom representation of the block - indActive : numpy.ndarray - - .. deprecated:: 0.23.0 - - Argument ``indActive`` is deprecated in favor of ``active_cells`` and will - be removed in SimPEG v0.24.0. Examples -------- diff --git a/simpeg/potential_fields/base.py b/simpeg/potential_fields/base.py index 9745ef5613..2781c26c14 100644 --- a/simpeg/potential_fields/base.py +++ b/simpeg/potential_fields/base.py @@ -1,13 +1,9 @@ import os -import warnings from multiprocessing.pool import Pool import discretize import numpy as np from discretize import TensorMesh, TreeMesh -from scipy.sparse import csr_matrix as csr - -from simpeg.utils import mkvc from ..simulation import LinearSimulation from ..utils import validate_active_indices, validate_integer, validate_string @@ -63,12 +59,6 @@ class BasePFSimulation(LinearSimulation): If True, the simulation will run in parallel. If False, it will run in serial. If ``engine`` is not ``"choclo"`` this argument will be ignored. - ind_active : np.ndarray of int or bool - - .. deprecated:: 0.23.0 - - Argument ``ind_active`` is deprecated in favor of - ``active_cells`` and will be removed in SimPEG v0.24.0. Notes ----- @@ -96,28 +86,13 @@ def __init__( sensitivity_dtype=np.float32, engine="geoana", numba_parallel=True, - ind_active=None, **kwargs, ): - # Deprecate ind_active argument - if ind_active is not None: - if active_cells is not None: - raise TypeError( - "Cannot pass both 'active_cells' and 'ind_active'." - "'ind_active' has been deprecated and will be removed in " - " SimPEG v0.24.0, please use 'active_cells' instead.", - ) - warnings.warn( - "'ind_active' has been deprecated and will be removed in " - " SimPEG v0.24.0, please use 'active_cells' instead.", - FutureWarning, - stacklevel=2, - ) - active_cells = ind_active - - if "forwardOnly" in kwargs: - raise AttributeError( - "forwardOnly was removed in SimPEG 0.17.0, please set store_sensitivities='forward_only'" + # removed ind_active argument + if kwargs.pop("ind_active", None) is not None: + raise TypeError( + "'ind_active' has been removed in " + "SimPEG v0.24.0, please use 'active_cells' instead.", ) self.mesh = mesh @@ -318,8 +293,7 @@ def active_cells(self): "ind_active", "active_cells", removal_version="0.24.0", - future_warn=True, - error=False, + error=True, ) def linear_operator(self): @@ -530,110 +504,7 @@ def progress(iteration, prog, final): return prog -def get_dist_wgt(mesh, receiver_locations, actv, R, R0): - """Compute distance weights for potential field simulations. - - Parameters - ---------- - mesh : discretize.BaseMesh - A discretize mesh - receiver_locations : (n, 3) numpy.ndarray - Observation locations [x, y, z] - actv : (n_cell) numpy.ndarray of bool - Active cells vector [0:air , 1: ground] - R : float - Decay factor (mag=3, grav =2) - R0 : float - Stabilization factor. Usually a fraction of the minimum cell size - - Returns - ------- - wr : (n_cell) numpy.ndarray - Distance weighting model; 0 for all inactive cells - """ - warnings.warn( - "The get_dist_wgt function has been deprecated, please import " - "simpeg.utils.distance_weighting. This will be removed in SimPEG 0.24.0", - FutureWarning, - stacklevel=2, - ) - # Find non-zero cells - if actv.dtype == "bool": - inds = ( - np.asarray([inds for inds, elem in enumerate(actv, 1) if elem], dtype=int) - - 1 - ) - else: - inds = actv - - nC = len(inds) - - # Create active cell projector - P = csr((np.ones(nC), (inds, range(nC))), shape=(mesh.nC, nC)) - - # Geometrical constant - p = 1 / np.sqrt(3) - - # Create cell center location - Ym, Xm, Zm = np.meshgrid( - mesh.cell_centers_y, mesh.cell_centers_x, mesh.cell_centers_z +def get_dist_wgt(*args, **kwargs): + raise NotImplementedError( + "The get_dist_wgt function has been removed in SimPEG 0.24.0, please import simpeg.utils.distance_weighting." ) - hY, hX, hZ = np.meshgrid(mesh.h[1], mesh.h[0], mesh.h[2]) - - # Remove air cells - Xm = P.T * mkvc(Xm) - Ym = P.T * mkvc(Ym) - Zm = P.T * mkvc(Zm) - - hX = P.T * mkvc(hX) - hY = P.T * mkvc(hY) - hZ = P.T * mkvc(hZ) - - V = P.T * mkvc(mesh.cell_volumes) - wr = np.zeros(nC) - - ndata = receiver_locations.shape[0] - count = -1 - print("Begin calculation of distance weighting for R= " + str(R)) - - for dd in range(ndata): - nx1 = (Xm - hX * p - receiver_locations[dd, 0]) ** 2 - nx2 = (Xm + hX * p - receiver_locations[dd, 0]) ** 2 - - ny1 = (Ym - hY * p - receiver_locations[dd, 1]) ** 2 - ny2 = (Ym + hY * p - receiver_locations[dd, 1]) ** 2 - - nz1 = (Zm - hZ * p - receiver_locations[dd, 2]) ** 2 - nz2 = (Zm + hZ * p - receiver_locations[dd, 2]) ** 2 - - R1 = np.sqrt(nx1 + ny1 + nz1) - R2 = np.sqrt(nx1 + ny1 + nz2) - R3 = np.sqrt(nx2 + ny1 + nz1) - R4 = np.sqrt(nx2 + ny1 + nz2) - R5 = np.sqrt(nx1 + ny2 + nz1) - R6 = np.sqrt(nx1 + ny2 + nz2) - R7 = np.sqrt(nx2 + ny2 + nz1) - R8 = np.sqrt(nx2 + ny2 + nz2) - - temp = ( - (R1 + R0) ** -R - + (R2 + R0) ** -R - + (R3 + R0) ** -R - + (R4 + R0) ** -R - + (R5 + R0) ** -R - + (R6 + R0) ** -R - + (R7 + R0) ** -R - + (R8 + R0) ** -R - ) - - wr = wr + (V * temp / 8.0) ** 2.0 - - count = progress(dd, count, ndata) - - wr = np.sqrt(wr) / V - wr = mkvc(wr) - wr = np.sqrt(wr / (np.max(wr))) - - print("Done 100% ...distance weighting completed!!\n") - - return wr diff --git a/simpeg/potential_fields/gravity/simulation.py b/simpeg/potential_fields/gravity/simulation.py index c4b9430842..a5ebb7b0e0 100644 --- a/simpeg/potential_fields/gravity/simulation.py +++ b/simpeg/potential_fields/gravity/simulation.py @@ -172,12 +172,6 @@ class Simulation3DIntegral(BasePFSimulation): If True, the simulation will run in parallel. If False, it will run in serial. If ``engine`` is not ``"choclo"`` this argument will be ignored. - ind_active : np.ndarray of int or bool - - .. deprecated:: 0.23.0 - - Argument ``ind_active`` is deprecated in favor of - ``active_cells`` and will be removed in SimPEG v0.24.0. """ rho, rhoMap, rhoDeriv = props.Invertible("Density") diff --git a/simpeg/potential_fields/magnetics/__init__.py b/simpeg/potential_fields/magnetics/__init__.py index 52612898b8..b757d912ed 100644 --- a/simpeg/potential_fields/magnetics/__init__.py +++ b/simpeg/potential_fields/magnetics/__init__.py @@ -48,5 +48,5 @@ Simulation3DDifferential, ) from .survey import Survey -from .sources import SourceField, UniformBackgroundField +from .sources import UniformBackgroundField from .receivers import Point diff --git a/simpeg/potential_fields/magnetics/simulation.py b/simpeg/potential_fields/magnetics/simulation.py index 57ca242d8c..da5acc31c2 100644 --- a/simpeg/potential_fields/magnetics/simulation.py +++ b/simpeg/potential_fields/magnetics/simulation.py @@ -17,7 +17,7 @@ from simpeg import props, utils from simpeg.utils import mat_utils, mkvc, sdiag, get_default_solver -from simpeg.utils.code_utils import deprecate_property, validate_string, validate_type +from simpeg.utils.code_utils import validate_string, validate_type from ...base import BaseMagneticPDESimulation from ..base import BaseEquivalentSourceLayerSimulation, BasePFSimulation @@ -155,12 +155,6 @@ class Simulation3DIntegral(BasePFSimulation): If True, the simulation will run in parallel. If False, it will run in serial. If ``engine`` is not ``"choclo"`` this argument will be ignored. - ind_active : np.ndarray of int or bool - - .. deprecated:: 0.23.0 - - Argument ``ind_active`` is deprecated in favor of - ``active_cells`` and will be removed in SimPEG v0.24.0. """ chi, chiMap, chiDeriv = props.Invertible("Magnetic Susceptibility (SI)") @@ -284,10 +278,6 @@ def G(self) -> NDArray | np.memmap | LinearOperator: self._G = self.linear_operator() return self._G - modelType = deprecate_property( - model_type, "modelType", "model_type", removal_version="0.18.0", error=True - ) - @property def nD(self): """ diff --git a/simpeg/potential_fields/magnetics/sources.py b/simpeg/potential_fields/magnetics/sources.py index d63797a99a..56c9e8662c 100644 --- a/simpeg/potential_fields/magnetics/sources.py +++ b/simpeg/potential_fields/magnetics/sources.py @@ -1,6 +1,6 @@ from ...survey import BaseSrc from ...utils.mat_utils import dip_azimuth2cartesian -from ...utils.code_utils import deprecate_class, validate_float, validate_list_of_types +from ...utils.code_utils import validate_float, validate_list_of_types from .receivers import Point @@ -107,29 +107,3 @@ def b0(self): self.amplitude * dip_azimuth2cartesian(self.inclination, self.declination).squeeze() ) - - -@deprecate_class(removal_version="0.19.0", error=True) -class SourceField(UniformBackgroundField): - """Source field for magnetics integral formulation - - Parameters - ---------- - receivers_list : list of simpeg.potential_fields.receivers.Point - List of magnetics receivers - parameters : (3) array_like of float - Define the Earth's inducing field according to - [*amplitude*, *inclination*, *declination*] where: - - - *amplitude* is the field intensity in nT - - *inclination* is the inclination of the Earth's field in degrees - - *declination* is the declination of the Earth's field in degrees - """ - - def __init__(self, receiver_list=None, parameters=(50000, 90, 0)): - super().__init__( - receiver_list=receiver_list, - amplitude=parameters[0], - inclination=parameters[1], - declination=parameters[2], - ) diff --git a/simpeg/regularization/__init__.py b/simpeg/regularization/__init__.py index e28f24ca0a..cd8b64b821 100644 --- a/simpeg/regularization/__init__.py +++ b/simpeg/regularization/__init__.py @@ -148,7 +148,6 @@ """ -from ..utils.code_utils import deprecate_class from .base import ( BaseRegularization, WeightedLeastSquares, @@ -172,87 +171,3 @@ AmplitudeSmoothnessFirstOrder, ) from ._gradient import SmoothnessFullGradient - - -@deprecate_class(removal_version="0.19.0", error=True) -class SimpleSmall(Smallness): - """Deprecated class, replaced by Smallness.""" - - pass - - -@deprecate_class(removal_version="0.19.0", error=True) -class SimpleSmoothDeriv(SmoothnessFirstOrder): - """Deprecated class, replaced by SmoothnessFirstOrder.""" - - pass - - -@deprecate_class(removal_version="0.19.0", error=True) -class Simple(WeightedLeastSquares): - """Deprecated class, replaced by WeightedLeastSquares.""" - - def __init__(self, mesh=None, alpha_x=1.0, alpha_y=1.0, alpha_z=1.0, **kwargs): - # These alphas are now refered to as length_scalse in the - # new WeightedLeastSquares regularization - super().__init__( - mesh=mesh, - length_scale_x=alpha_x, - length_scale_y=alpha_y, - length_scale_z=alpha_z, - **kwargs, - ) - - -@deprecate_class(removal_version="0.19.0", error=True) -class Tikhonov(WeightedLeastSquares): - """Deprecated class, replaced by WeightedLeastSquares.""" - - def __init__( - self, mesh=None, alpha_s=1e-6, alpha_x=1.0, alpha_y=1.0, alpha_z=1.0, **kwargs - ): - super().__init__( - mesh=mesh, - alpha_s=alpha_s, - alpha_x=alpha_x, - alpha_y=alpha_y, - alpha_z=alpha_z, - **kwargs, - ) - - -@deprecate_class(removal_version="0.19.0", error=True) -class Small(Smallness): - """Deprecated class, replaced by Smallness.""" - - pass - - -@deprecate_class(removal_version="0.19.0", error=True) -class SmoothDeriv(SmoothnessFirstOrder): - """Deprecated class, replaced by SmoothnessFirstOrder.""" - - pass - - -@deprecate_class(removal_version="0.19.0", error=True) -class SmoothDeriv2(SmoothnessSecondOrder): - """Deprecated class, replaced by SmoothnessSecondOrder.""" - - pass - - -@deprecate_class(removal_version="0.19.0", error=True) -class PGIwithNonlinearRelationshipsSmallness(PGIsmallness): - """Deprecated class, replaced by PGIsmallness.""" - - def __init__(self, gmm, **kwargs): - super().__init__(gmm, non_linear_relationships=True, **kwargs) - - -@deprecate_class(removal_version="0.19.0", error=True) -class PGIwithRelationships(PGI): - """Deprecated class, replaced by PGI.""" - - def __init__(self, mesh, gmmref, **kwargs): - super().__init__(mesh, gmmref, non_linear_relationships=True, **kwargs) diff --git a/simpeg/regularization/base.py b/simpeg/regularization/base.py index a5c59aee3f..4665d779b6 100644 --- a/simpeg/regularization/base.py +++ b/simpeg/regularization/base.py @@ -5,7 +5,7 @@ from .. import utils from .regularization_mesh import RegularizationMesh -from simpeg.utils.code_utils import deprecate_property, validate_ndarray_with_shape +from simpeg.utils.code_utils import validate_ndarray_with_shape from scipy.sparse import csr_matrix @@ -67,18 +67,6 @@ def __init__( "It must be a dictionary with strings as keys and arrays as values." ) - # Raise errors on deprecated arguments: avoid old code that still uses - # them to silently fail - if (key := "indActive") in kwargs: - raise TypeError( - f"'{key}' argument has been removed. " - "Please use 'active_cells' instead." - ) - if (key := "cell_weights") in kwargs: - raise TypeError( - f"'{key}' argument has been removed. Please use 'weights' instead." - ) - super().__init__(nP=None, mapping=None, **kwargs) self._regularization_mesh = mesh self._weights = {} @@ -121,14 +109,6 @@ def active_cells(self, values: np.ndarray | None): if volume_term: self.set_weights(volume=self.regularization_mesh.vol) - indActive = deprecate_property( - active_cells, - "indActive", - "active_cells", - "0.19.0", - error=True, - ) - @property def model(self) -> np.ndarray: """The model parameters. @@ -256,14 +236,6 @@ def reference_model(self, values: np.ndarray | float): ) self._reference_model = values - mref = deprecate_property( - reference_model, - "mref", - "reference_model", - "0.19.0", - error=True, - ) - @property def regularization_mesh(self) -> RegularizationMesh: """Regularization mesh. @@ -278,31 +250,6 @@ def regularization_mesh(self) -> RegularizationMesh: """ return self._regularization_mesh - regmesh = deprecate_property( - regularization_mesh, - "regmesh", - "regularization_mesh", - "0.19.0", - error=True, - ) - - @property - def cell_weights(self) -> np.ndarray: - """Deprecated property for 'volume' and user defined weights.""" - raise AttributeError( - "'cell_weights' has been removed. " - "Please access weights using the `set_weights`, `get_weights`, and " - "`remove_weights` methods." - ) - - @cell_weights.setter - def cell_weights(self, value): - raise AttributeError( - "'cell_weights' has been removed. " - "Please access weights using the `set_weights`, `get_weights`, and " - "`remove_weights` methods." - ) - def get_weights(self, key) -> np.ndarray: """Cell weights for a given key. @@ -1558,19 +1505,6 @@ def __init__( ) self._regularization_mesh = mesh - # Raise errors on deprecated arguments: avoid old code that still uses - # them to silently fail - if (key := "indActive") in kwargs: - raise TypeError( - f"'{key}' argument has been removed. " - "Please use 'active_cells' instead." - ) - - if (key := "cell_weights") in kwargs: - raise TypeError( - f"'{key}' argument has been removed. Please use 'weights' instead." - ) - self.alpha_s = alpha_s if alpha_x is not None: if length_scale_x is not None: @@ -2078,14 +2012,6 @@ def active_cells(self, values: np.ndarray): for objfct in self.objfcts: objfct.active_cells = active_cells - indActive = deprecate_property( - active_cells, - "indActive", - "active_cells", - "0.19.0", - error=True, - ) - @property def reference_model(self) -> np.ndarray: """Reference model. @@ -2108,14 +2034,6 @@ def reference_model(self, values: np.ndarray | float): self._reference_model = values - mref = deprecate_property( - reference_model, - "mref", - "reference_model", - "0.19.0", - error=True, - ) - @property def model(self) -> np.ndarray: """The model associated with regularization. diff --git a/simpeg/regularization/pgi.py b/simpeg/regularization/pgi.py index dbc059eaa6..0193647251 100644 --- a/simpeg/regularization/pgi.py +++ b/simpeg/regularization/pgi.py @@ -8,7 +8,6 @@ from ..objective_function import ComboObjectiveFunction from ..utils import ( Identity, - deprecate_property, mkvc, sdiag, timeIt, @@ -1365,11 +1364,3 @@ def reference_model(self, values: np.ndarray | float): for fct in self.objfcts: fct.reference_model = values - - mref = deprecate_property( - reference_model, - "mref", - "reference_model", - "0.19.0", - error=True, - ) diff --git a/simpeg/regularization/regularization_mesh.py b/simpeg/regularization/regularization_mesh.py index dea11bb2f1..38b97db5e3 100755 --- a/simpeg/regularization/regularization_mesh.py +++ b/simpeg/regularization/regularization_mesh.py @@ -1,7 +1,7 @@ import numpy as np import scipy.sparse as sp -from simpeg.utils.code_utils import deprecate_property, validate_active_indices +from simpeg.utils.code_utils import validate_active_indices from .. import props, utils @@ -518,28 +518,6 @@ def cell_gradient_z(self) -> sp.csr_matrix: ) return self._cell_gradient_z - cellDiffx = deprecate_property( - cell_gradient_x, - "cellDiffx", - "cell_gradient_x", - "0.19.0", - error=True, - ) - cellDiffy = deprecate_property( - cell_gradient_y, - "cellDiffy", - "cell_gradient_y", - "0.19.0", - error=True, - ) - cellDiffz = deprecate_property( - cell_gradient_z, - "cellDiffz", - "cell_gradient_z", - "0.19.0", - error=True, - ) - @property def cell_distances_x(self) -> np.ndarray: """Cell center distance array along the x-direction. diff --git a/simpeg/regularization/sparse.py b/simpeg/regularization/sparse.py index b5ba938866..14c6aa9f04 100644 --- a/simpeg/regularization/sparse.py +++ b/simpeg/regularization/sparse.py @@ -9,7 +9,6 @@ Smallness, SmoothnessFirstOrder, ) -from .. import utils from ..utils import ( validate_ndarray_with_shape, validate_float, @@ -575,12 +574,6 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): """ def __init__(self, mesh, orientation="x", gradient_type="total", **kwargs): - # Raise error if removed arguments were passed - if (key := "gradientType") in kwargs: - raise TypeError( - f"'{key}' argument has been removed. " - "Please use 'gradient_type' instead." - ) self.gradient_type = gradient_type super().__init__(mesh=mesh, orientation=orientation, **kwargs) @@ -691,14 +684,6 @@ def gradient_type(self, value: str): "gradient_type", value, ["total", "components"] ) - gradientType = utils.code_utils.deprecate_property( - gradient_type, - "gradientType", - new_name="gradient_type", - removal_version="0.19.0", - error=True, - ) - class Sparse(WeightedLeastSquares): r"""Sparse norm weighted least squares regularization. @@ -931,13 +916,6 @@ def __init__( f"Value of type {type(mesh)} provided." ) - # Raise error if removed arguments were passed - if (key := "gradientType") in kwargs: - raise TypeError( - f"'{key}' argument has been removed. " - "Please use 'gradient_type' instead." - ) - self._regularization_mesh = mesh if active_cells is not None: self._regularization_mesh.active_cells = active_cells @@ -995,10 +973,6 @@ def gradient_type(self, value: str): self._gradient_type = value - gradientType = utils.code_utils.deprecate_property( - gradient_type, "gradientType", "0.19.0", error=True - ) - @property def norms(self): """Norms for the child regularization classes. diff --git a/simpeg/survey.py b/simpeg/survey.py index aa3c55b7e1..e471ab1b0c 100644 --- a/simpeg/survey.py +++ b/simpeg/survey.py @@ -290,7 +290,7 @@ class BaseSrc: Location of the source """ - def __init__(self, receiver_list=None, location=None, **kwargs): + def __init__(self, receiver_list=None, location=None): if receiver_list is None: receiver_list = [] self.receiver_list = receiver_list diff --git a/simpeg/utils/__init__.py b/simpeg/utils/__init__.py index 8e9dfedf93..bb3afdd470 100644 --- a/simpeg/utils/__init__.py +++ b/simpeg/utils/__init__.py @@ -266,48 +266,5 @@ GaussianMixtureWithNonlinearRelationships, GaussianMixtureWithNonlinearRelationshipsWithPrior, ) - -# Deprecated imports -interpmat = deprecate_function( - interpolation_matrix, "interpmat", removal_version="0.19.0", error=True -) - -from .code_utils import ( - memProfileWrapper, - setKwargs, - printTitles, - printLine, - checkStoppers, - printStoppers, - printDone, - callHooks, - dependentProperty, - asArray_N_x_Dim, -) -from .mat_utils import ( - sdInv, - getSubArray, - inv3X3BlockDiagonal, - inv2X2BlockDiagonal, - makePropertyTensor, - invPropertyTensor, - diagEst, - uniqueRows, -) -from .mesh_utils import ( - meshTensor, - closestPoints, - ExtractCoreMesh, -) -from .curv_utils import ( - volTetra, - faceInfo, - indexCube, - exampleLrmGrid, -) -from .coord_utils import ( - rotatePointsFromNormals, - rotationMatrixFromNormals, -) from .solver_utils import get_default_solver, set_default_solver from .warnings import PerformanceWarning diff --git a/simpeg/utils/code_utils.py b/simpeg/utils/code_utils.py index 0d2895e133..cf7ea5486e 100644 --- a/simpeg/utils/code_utils.py +++ b/simpeg/utils/code_utils.py @@ -1279,38 +1279,3 @@ def validate_active_indices(property_name, index_arr, n_cells): if index_arr.shape != (n_cells,): raise ValueError(f"Input 'active_cells' must have shape {(n_cells,)}") return index_arr - - -############################################################### -# DEPRECATIONS -############################################################### -memProfileWrapper = deprecate_function( - mem_profile_class, "memProfileWrapper", removal_version="0.18.0", error=True -) -setKwargs = deprecate_function( - set_kwargs, "setKwargs", removal_version="0.18.0", error=True -) -printTitles = deprecate_function( - print_titles, "printTitles", removal_version="0.18.0", error=True -) -printLine = deprecate_function( - print_line, "printLine", removal_version="0.18.0", error=True -) -printStoppers = deprecate_function( - print_stoppers, "printStoppers", removal_version="0.18.0", error=True -) -checkStoppers = deprecate_function( - check_stoppers, "checkStoppers", removal_version="0.18.0", error=True -) -printDone = deprecate_function( - print_done, "printDone", removal_version="0.18.0", error=True -) -callHooks = deprecate_function( - call_hooks, "callHooks", removal_version="0.18.0", error=True -) -dependentProperty = deprecate_function( - dependent_property, "dependentProperty", removal_version="0.18.0", error=True -) -asArray_N_x_Dim = deprecate_function( - as_array_n_by_dim, "asArray_N_x_Dim", removal_version="0.19.0", error=True -) diff --git a/simpeg/utils/coord_utils.py b/simpeg/utils/coord_utils.py index e1d17c5dbf..84491b42f7 100644 --- a/simpeg/utils/coord_utils.py +++ b/simpeg/utils/coord_utils.py @@ -2,18 +2,3 @@ rotation_matrix_from_normals, rotate_points_from_normals, ) -from .code_utils import deprecate_function - -# deprecated functions -rotationMatrixFromNormals = deprecate_function( - rotation_matrix_from_normals, - "rotationMatrixFromNormals", - removal_version="0.19.0", - error=True, -) -rotatePointsFromNormals = deprecate_function( - rotate_points_from_normals, - "rotatePointsFromNormals", - removal_version="0.19.0", - error=True, -) diff --git a/simpeg/utils/curv_utils.py b/simpeg/utils/curv_utils.py index 71e764ce60..93b4e393ec 100644 --- a/simpeg/utils/curv_utils.py +++ b/simpeg/utils/curv_utils.py @@ -4,21 +4,3 @@ face_info, example_curvilinear_grid, ) -from .code_utils import deprecate_function - -# deprecated functions -volTetra = deprecate_function( - volume_tetrahedron, "volTetra", removal_version="0.19.0", error=True -) -indexCube = deprecate_function( - index_cube, "indexCube", removal_version="0.19.0", error=True -) -faceInfo = deprecate_function( - face_info, "faceInfo", removal_version="0.19.0", error=True -) -exampleLrmGrid = deprecate_function( - example_curvilinear_grid, - "exampleLrmGrid", - removal_version="0.19.0", - error=True, -) diff --git a/simpeg/utils/mat_utils.py b/simpeg/utils/mat_utils.py index 307773b25a..67b5d83187 100644 --- a/simpeg/utils/mat_utils.py +++ b/simpeg/utils/mat_utils.py @@ -1,6 +1,4 @@ -import warnings import numpy as np -from .code_utils import deprecate_function from ..typing import RandomSeed from discretize.utils import ( # noqa: F401 Zero, @@ -132,7 +130,6 @@ def eigenvalue_by_power_iteration( n_pw_iter=4, fields_list=None, random_seed: RandomSeed | None = None, - seed: RandomSeed | None = None, ): r"""Estimate largest eigenvalue in absolute value using power iteration. @@ -157,12 +154,6 @@ def eigenvalue_by_power_iteration( Random seed for the initial random guess of eigenvector. It can either be an int, a predefined Numpy random number generator, or any valid input to ``numpy.random.default_rng``. - seed : None or :class:`~simpeg.typing.RandomSeed`, optional - - .. deprecated:: 0.23.0 - - Argument ``seed`` is deprecated in favor of ``random_seed`` and will - be removed in SimPEG v0.24.0. Returns ------- @@ -189,21 +180,6 @@ def eigenvalue_by_power_iteration( selected from a uniform distribution. """ - # Deprecate seed argument - if seed is not None: - if random_seed is not None: - raise TypeError( - "Cannot pass both 'random_seed' and 'seed'." - "'seed' has been deprecated and will be removed in " - " SimPEG v0.24.0, please use 'random_seed' instead.", - ) - warnings.warn( - "'seed' has been deprecated and will be removed in " - " SimPEG v0.24.0, please use 'random_seed' instead.", - FutureWarning, - stacklevel=2, - ) - random_seed = seed rng = np.random.default_rng(seed=random_seed) # Initial guess for eigen-vector @@ -507,44 +483,3 @@ def define_plane_from_points(xyz1, xyz2, xyz3): d = -(a * xyz1[0] + b * xyz1[1] + c * xyz1[2]) return a, b, c, d - - -################################################ -# DEPRECATED FUNCTIONS -################################################ - - -diagEst = deprecate_function( - estimate_diagonal, "diagEst", removal_version="0.19.0", error=True -) -uniqueRows = deprecate_function( - unique_rows, "uniqueRows", removal_version="0.19.0", error=True -) -sdInv = deprecate_function(sdinv, "sdInv", removal_version="0.19.0", error=True) -getSubArray = deprecate_function( - get_subarray, "getSubArray", removal_version="0.19.0", error=True -) -inv3X3BlockDiagonal = deprecate_function( - inverse_3x3_block_diagonal, - "inv3X3BlockDiagonal", - removal_version="0.19.0", - error=True, -) -inv2X2BlockDiagonal = deprecate_function( - inverse_2x2_block_diagonal, - "inv2X2BlockDiagonal", - removal_version="0.19.0", - error=True, -) -makePropertyTensor = deprecate_function( - make_property_tensor, - "makePropertyTensor", - removal_version="0.19.0", - error=True, -) -invPropertyTensor = deprecate_function( - inverse_property_tensor, - "invPropertyTensor", - removal_version="0.19.0", - error=True, -) diff --git a/simpeg/utils/mesh_utils.py b/simpeg/utils/mesh_utils.py index 1fc3a8d580..3161859288 100644 --- a/simpeg/utils/mesh_utils.py +++ b/simpeg/utils/mesh_utils.py @@ -1,5 +1,4 @@ import numpy as np -from .code_utils import deprecate_function from discretize.utils import ( # noqa: F401 unpack_widths, @@ -95,17 +94,3 @@ def surface2inds(vrtx, trgl, mesh, boundaries=True, internal=True): # Return the indexes inside return insideGrid - - -################################################ -# DEPRECATED FUNCTIONS -################################################ -meshTensor = deprecate_function( - unpack_widths, "meshTensor", removal_version="0.19.0", error=True -) -closestPoints = deprecate_function( - closest_points_index, "closestPoints", removal_version="0.19.0", error=True -) -ExtractCoreMesh = deprecate_function( - extract_core_mesh, "ExtractCoreMesh", removal_version="0.19.0", error=True -) diff --git a/simpeg/utils/model_builder.py b/simpeg/utils/model_builder.py index 285a440a8a..97f785fedb 100644 --- a/simpeg/utils/model_builder.py +++ b/simpeg/utils/model_builder.py @@ -1,4 +1,3 @@ -import warnings import numpy as np import scipy.ndimage as ndi import scipy.sparse as sp @@ -443,12 +442,6 @@ def create_random_model( Number of smoothing iterations after convolutions bounds : list of float Lower and upper bound for the model values - seed : None or :class:`~simpeg.typing.RandomSeed`, optional - - .. deprecated:: 0.23.0 - - Argument ``seed`` is deprecated in favor of ``random_seed`` and will - be removed in SimPEG v0.24.0. Returns ------- @@ -467,21 +460,6 @@ def create_random_model( >>> plt.show() """ - # Deprecate seed argument - if "seed" in kwargs: - if random_seed != 1000: - raise TypeError( - "Cannot pass both 'random_seed' and 'seed'." - "'seed' has been deprecated and will be removed in " - " SimPEG v0.24.0, please use 'random_seed' instead.", - ) - warnings.warn( - "'seed' has been deprecated and will be removed in " - " SimPEG v0.24.0, please use 'random_seed' instead.", - FutureWarning, - stacklevel=2, - ) - random_seed = kwargs.pop("seed") if kwargs: args = ", ".join([f"'{key}'" for key in kwargs]) raise TypeError(f"Invalid arguments {args}.") diff --git a/simpeg/utils/solver_utils.py b/simpeg/utils/solver_utils.py index d180871fad..70af2dae68 100644 --- a/simpeg/utils/solver_utils.py +++ b/simpeg/utils/solver_utils.py @@ -94,16 +94,19 @@ def set_default_solver(solver_class: Type[Base]): old_name="SolverWrapD", removal_version="0.24.0", new_location="pymatsolver", + error=True, ) SolverWrapI = deprecate_function( wrap_iterative, old_name="SolverWrapI", removal_version="0.24.0", new_location="pymatsolver", + error=True, ) SolverDiag = deprecate_function( Diagonal, old_name="SolverDiag", removal_version="0.24.0", new_location="pymatsolver", + error=True, ) diff --git a/tests/base/regularizations/test_pgi_regularization.py b/tests/base/regularizations/test_pgi_regularization.py index f59a012396..98bfa5fb69 100644 --- a/tests/base/regularizations/test_pgi_regularization.py +++ b/tests/base/regularizations/test_pgi_regularization.py @@ -472,20 +472,6 @@ def test_spherical_covariances(self): plt.show() -def test_removed_mref(): - """Test if PGI raises error when accessing removed mref property.""" - h = [[(2, 2)], [(2, 2)], [(2, 2)]] - mesh = discretize.TensorMesh(h) - n_components = 1 - gmm = WeightedGaussianMixture(mesh=mesh, n_components=n_components) - samples = np.random.default_rng(seed=42).normal(size=(mesh.n_cells, 2)) - gmm.fit(samples) - pgi = regularization.PGI(mesh=mesh, gmmref=gmm) - message = "mref has been removed, please use reference_model." - with pytest.raises(NotImplementedError, match=message): - pgi.mref - - class TestCheckWeights: """Test the ``WeightedGaussianMixture._check_weights`` method.""" diff --git a/tests/base/regularizations/test_regularization.py b/tests/base/regularizations/test_regularization.py index 776c07ed65..cf0c0a33d5 100644 --- a/tests/base/regularizations/test_regularization.py +++ b/tests/base/regularizations/test_regularization.py @@ -791,25 +791,18 @@ def mesh(self, request): ) def test_mref_property(self, mesh, regularization_class): """Test mref property.""" - msg = "mref has been removed, please use reference_model." reg = regularization_class(mesh) - with pytest.raises(NotImplementedError, match=msg): - reg.mref + assert not hasattr(reg, "mref") def test_regmesh_property(self, mesh): """Test regmesh property.""" - msg = "regmesh has been removed, please use regularization_mesh." reg = BaseRegularization(mesh) - with pytest.raises(NotImplementedError, match=msg): - reg.regmesh + assert not hasattr(reg, "regmesh") @pytest.mark.parametrize("regularization_class", (Sparse, SparseSmoothness)) def test_gradient_type(self, mesh, regularization_class): """Test gradientType argument.""" - msg = ( - "'gradientType' argument has been removed. " - "Please use 'gradient_type' instead." - ) + msg = "got an unexpected keyword argument" with pytest.raises(TypeError, match=msg): regularization_class(mesh, gradientType="total") @@ -820,10 +813,7 @@ def test_gradient_type(self, mesh, regularization_class): def test_ind_active(self, mesh, regularization_class): """Test if error is raised when passing the indActive argument.""" active_cells = np.ones(len(mesh), dtype=bool) - msg = ( - "'indActive' argument has been removed. " - "Please use 'active_cells' instead." - ) + msg = "got an unexpected keyword argument" with pytest.raises(TypeError, match=msg): regularization_class(mesh, indActive=active_cells) @@ -835,9 +825,7 @@ def test_ind_active_property(self, mesh, regularization_class): """Test if error is raised when trying to access the indActive property.""" active_cells = np.ones(len(mesh), dtype=bool) reg = regularization_class(mesh, active_cells=active_cells) - msg = "indActive has been removed, please use active_cells." - with pytest.raises(NotImplementedError, match=msg): - reg.indActive + assert not hasattr(reg, "indActive") @pytest.mark.parametrize( "regularization_class", @@ -846,7 +834,7 @@ def test_ind_active_property(self, mesh, regularization_class): def test_cell_weights_argument(self, mesh, regularization_class): """Test if error is raised when passing the cell_weights argument.""" weights = np.ones(len(mesh)) - msg = "'cell_weights' argument has been removed. Please use 'weights' instead." + msg = "got an unexpected keyword argument" with pytest.raises(TypeError, match=msg): regularization_class(mesh, cell_weights=weights) @@ -856,54 +844,8 @@ def test_cell_weights_argument(self, mesh, regularization_class): def test_cell_weights_property(self, mesh, regularization_class): """Test if error is raised when trying to access the cell_weights property.""" weights = {"weights": np.ones(len(mesh))} - msg = ( - "'cell_weights' has been removed. " - "Please access weights using the `set_weights`, `get_weights`, and " - "`remove_weights` methods." - ) reg = regularization_class(mesh, weights=weights) - with pytest.raises(AttributeError, match=msg): - reg.cell_weights - - @pytest.mark.parametrize( - "regularization_class", (BaseRegularization, WeightedLeastSquares) - ) - def test_cell_weights_setter(self, mesh, regularization_class): - """Test if error is raised when trying to set the cell_weights property.""" - msg = ( - "'cell_weights' has been removed. " - "Please access weights using the `set_weights`, `get_weights`, and " - "`remove_weights` methods." - ) - reg = regularization_class(mesh) - with pytest.raises(AttributeError, match=msg): - reg.cell_weights = "dummy variable" - - -class TestRemovedRegularizations: - """ - Test if errors are raised after creating removed regularization classes. - """ - - @pytest.mark.parametrize( - "regularization_class", - ( - regularization.PGIwithNonlinearRelationshipsSmallness, - regularization.PGIwithRelationships, - regularization.Simple, - regularization.SimpleSmall, - regularization.SimpleSmoothDeriv, - regularization.Small, - regularization.SmoothDeriv, - regularization.SmoothDeriv2, - regularization.Tikhonov, - ), - ) - def test_removed_class(self, regularization_class): - class_name = regularization_class.__name__ - msg = f"{class_name} has been removed, please use." - with pytest.raises(NotImplementedError, match=msg): - regularization_class() + assert not hasattr(reg, "cell_weights") @pytest.mark.parametrize( diff --git a/tests/base/test_directives.py b/tests/base/test_directives.py index cee2882959..df53015109 100644 --- a/tests/base/test_directives.py +++ b/tests/base/test_directives.py @@ -23,14 +23,26 @@ class directivesValidation(unittest.TestCase): + + def test_error_irls_and_beta_scheduling(self): + """ + Test if validation error when ``UpdateIRLS`` and ``BetaSchedule`` are present. + """ + directives_list = directives.DirectiveList( + directives.UpdateIRLS(), + directives.BetaSchedule(coolingFactor=2, coolingRate=1), + ) + msg = "Beta scheduling is handled by the" + with pytest.raises(AssertionError, match=msg): + directives_list.validate() + def test_validation_pass(self): betaest = directives.BetaEstimate_ByEig() - IRLS = directives.Update_IRLS(f_min_change=1e-4, minGNiter=3, beta_tol=1e-2) - beta_schedule = directives.BetaSchedule(coolingFactor=2, coolingRate=1) + IRLS = directives.UpdateIRLS() update_Jacobi = directives.UpdatePreconditioner() - dList = [betaest, IRLS, beta_schedule, update_Jacobi] + dList = [betaest, IRLS, update_Jacobi] directiveList = directives.DirectiveList(*dList) self.assertTrue(directiveList.validate()) @@ -38,11 +50,10 @@ def test_validation_pass(self): def test_validation_fail(self): betaest = directives.BetaEstimate_ByEig() - IRLS = directives.Update_IRLS(f_min_change=1e-4, minGNiter=3, beta_tol=1e-2) + IRLS = directives.UpdateIRLS() update_Jacobi = directives.UpdatePreconditioner() - beta_schedule = directives.BetaSchedule(coolingFactor=2, coolingRate=1) - dList = [betaest, update_Jacobi, IRLS, beta_schedule] + dList = [betaest, update_Jacobi, IRLS] directiveList = directives.DirectiveList(*dList) with self.assertRaises(AssertionError): @@ -60,9 +71,8 @@ def test_validation_initial_beta_fail(self): def test_validation_warning(self): betaest = directives.BetaEstimate_ByEig() - IRLS = directives.Update_IRLS(f_min_change=1e-4, minGNiter=3, beta_tol=1e-2) - beta_schedule = directives.BetaSchedule(coolingFactor=2, coolingRate=1) - dList = [betaest, IRLS, beta_schedule] + IRLS = directives.UpdateIRLS() + dList = [betaest, IRLS] directiveList = directives.DirectiveList(*dList) with pytest.warns(UserWarning): @@ -119,7 +129,7 @@ def test_validation_in_inversion(self): betaest = directives.BetaEstimate_ByEig() # Here is where the norms are applied - IRLS = directives.Update_IRLS(f_min_change=1e-4, minGNiter=3, beta_tol=1e-2) + IRLS = directives.UpdateIRLS(f_min_change=1e-4) update_Jacobi = directives.UpdatePreconditioner() sensitivity_weights = directives.UpdateSensitivityWeights() with self.assertRaises(AssertionError): @@ -446,53 +456,6 @@ def test_save_output_dict(RegClass): assert "x SparseSmoothness.norm" in out_dict -class TestDeprecatedArguments: - """ - Test if directives raise errors after passing deprecated arguments. - """ - - def test_debug(self): - """ - Test if InversionDirective raises error after passing 'debug'. - """ - msg = "'debug' property has been removed. Please use 'verbose'." - with pytest.raises(TypeError, match=msg): - directives.InversionDirective(debug=True) - - -class TestUpdateSensitivityWeightsRemovedArgs: - """ - Test if `UpdateSensitivityWeights` raises errors after passing removed arguments. - """ - - def test_every_iter(self): - """ - Test if `UpdateSensitivityWeights` raises error after passing `everyIter`. - """ - msg = "'everyIter' property has been removed. Please use 'every_iteration'." - with pytest.raises(TypeError, match=msg): - directives.UpdateSensitivityWeights(everyIter=True) - - def test_threshold(self): - """ - Test if `UpdateSensitivityWeights` raises error after passing `threshold`. - """ - msg = "'threshold' property has been removed. Please use 'threshold_value'." - with pytest.raises(TypeError, match=msg): - directives.UpdateSensitivityWeights(threshold=True) - - def test_normalization(self): - """ - Test if `UpdateSensitivityWeights` raises error after passing `normalization`. - """ - msg = ( - "'normalization' property has been removed. " - "Please define normalization using 'normalization_method'." - ) - with pytest.raises(TypeError, match=msg): - directives.UpdateSensitivityWeights(normalization=True) - - class TestUpdateSensitivityNormalization: """ Test the `normalization` property and setter in `UpdateSensitivityWeights` @@ -598,9 +561,9 @@ def test_beta_estimate_max_derivative(self): assert directive.random_seed == random_seed -class TestDeprecateSeedProperty: +class TestRemovedSeedProperty: """ - Test deprecation of seed property. + Test removal of seed property. """ CLASSES = ( @@ -610,63 +573,31 @@ class TestDeprecateSeedProperty: directives.ScalingMultipleDataMisfits_ByEig, ) - def get_message_duplicated_error(self, old_name, new_name, version="v0.24.0"): - msg = ( - f"Cannot pass both '{new_name}' and '{old_name}'." - f"'{old_name}' has been deprecated and will be removed in " - f" SimPEG {version}, please use '{new_name}' instead." - ) - return msg - - def get_message_deprecated_warning(self, old_name, new_name, version="v0.24.0"): + def get_message_removed_error(self, old_name, new_name, version="v0.24.0"): msg = ( - f"'{old_name}' has been deprecated and will be removed in " + f"'{old_name}' has been removed in " f" SimPEG {version}, please use '{new_name}' instead." ) return msg @pytest.mark.parametrize("directive", CLASSES) - def test_warning_argument(self, directive): - """ - Test if warning is raised after passing ``seed`` to the constructor. - """ - msg = self.get_message_deprecated_warning("seed", "random_seed") - seed = 42135 - with pytest.warns(FutureWarning, match=msg): - directive_instance = directive(seed=42135) - assert directive_instance.random_seed == seed - - @pytest.mark.parametrize("directive", CLASSES) - def test_error_duplicated_argument(self, directive): + def test_error_argument(self, directive): """ - Test error after passing ``seed`` and ``random_seed`` to the constructor. + Test if error is raised after passing ``seed`` to the constructor. """ - msg = self.get_message_duplicated_error("seed", "random_seed") + msg = self.get_message_removed_error("seed", "random_seed") with pytest.raises(TypeError, match=msg): - directive(seed=42, random_seed=42) - - @pytest.mark.parametrize("directive", CLASSES) - def test_warning_accessing_property(self, directive): - """ - Test warning when trying to access the ``seed`` property. - """ - directive_obj = directive(random_seed=42) - msg = "seed has been deprecated, please use random_seed" - with pytest.warns(FutureWarning, match=msg): - seed = directive_obj.seed - np.testing.assert_allclose(seed, directive_obj.random_seed) + directive(seed=42135) @pytest.mark.parametrize("directive", CLASSES) - def test_warning_setter(self, directive): + def test_error_accessing_property(self, directive): """ - Test warning when trying to set the ``seed`` property. + Test error when trying to access the ``seed`` property. """ directive_obj = directive(random_seed=42) - msg = "seed has been deprecated, please use random_seed" - new_seed = 35 - with pytest.warns(FutureWarning, match=msg): - directive_obj.seed = new_seed - np.testing.assert_allclose(directive_obj.random_seed, new_seed) + msg = "seed has been removed, please use random_seed" + with pytest.raises(NotImplementedError, match=msg): + directive_obj.seed class TestUpdateIRLS: diff --git a/tests/base/test_maps.py b/tests/base/test_maps.py index ba7ac19581..8cb99803fb 100644 --- a/tests/base/test_maps.py +++ b/tests/base/test_maps.py @@ -748,8 +748,8 @@ def test_linearity(): assert all(not m.is_linear for m in non_linear_maps) -class DeprecatedIndActive: - """Base class to test deprecated ``actInd`` and ``indActive`` arguments in maps.""" +class RemovedIndActive: + """Base class to test removed ``actInd`` and ``indActive`` arguments in maps.""" @pytest.fixture def mesh(self): @@ -763,267 +763,174 @@ def active_cells(self, mesh): active_cells[0] = False return active_cells - def get_message_duplicated_error(self, old_name, new_name, version="v0.24.0"): + def get_message_removed_error(self, old_name, new_name, version="v0.24.0"): msg = ( - f"Cannot pass both '{new_name}' and '{old_name}'." - f"'{old_name}' has been deprecated and will be removed in " - f" SimPEG {version}, please use '{new_name}' instead." - ) - return msg - - def get_message_deprecated_warning(self, old_name, new_name, version="v0.24.0"): - msg = ( - f"'{old_name}' has been deprecated and will be removed in " - f" SimPEG {version}, please use '{new_name}' instead." + f"'{old_name}' was removed in " + f"SimPEG {version}, please use '{new_name}' instead." ) return msg -class TestParametricPolyMap(DeprecatedIndActive): - """Test deprecated ``actInd`` in ParametricPolyMap.""" - - def test_warning_argument(self, mesh, active_cells): - """ - Test if warning is raised after passing ``actInd`` to the constructor. - """ - msg = self.get_message_deprecated_warning("actInd", "active_cells") - with pytest.warns(FutureWarning, match=msg): - map_instance = maps.ParametricPolyMap(mesh, 2, actInd=active_cells) - np.testing.assert_allclose(map_instance.active_cells, active_cells) +class TestParametricPolyMap(RemovedIndActive): + """Test removed ``actInd`` in ParametricPolyMap.""" - def test_error_duplicated_argument(self, mesh, active_cells): + def test_error_argument(self, mesh, active_cells): """ - Test error after passing ``actInd`` and ``active_cells`` to the constructor. + Test if error is raised after passing ``actInd`` to the constructor. """ - msg = self.get_message_duplicated_error("actInd", "active_cells") + msg = "Unsupported keyword argument actInd" with pytest.raises(TypeError, match=msg): - maps.ParametricPolyMap( - mesh, 2, active_cells=active_cells, actInd=active_cells - ) + maps.ParametricPolyMap(mesh, 2, actInd=active_cells) - def test_warning_accessing_property(self, mesh, active_cells): + def test_error_accessing_property(self, mesh, active_cells): """ - Test warning when trying to access the ``actInd`` property. + Test error when trying to access the ``actInd`` property. """ mapping = maps.ParametricPolyMap(mesh, 2, active_cells=active_cells) - msg = "actInd has been deprecated, please use active_cells" - with pytest.warns(FutureWarning, match=msg): - old_act_ind = mapping.actInd - np.testing.assert_allclose(mapping.active_cells, old_act_ind) + msg = "actInd has been removed, please use active_cells" + with pytest.raises(NotImplementedError, match=msg): + mapping.actInd - def test_warning_setter(self, mesh, active_cells): + def test_error_setter(self, mesh, active_cells): """ - Test warning when trying to set the ``actInd`` property. + Test error when trying to set the ``actInd`` property. """ mapping = maps.ParametricPolyMap(mesh, 2, active_cells=active_cells) - # Define new active cells to pass to the setter - new_active_cells = active_cells.copy() - new_active_cells[-4:] = False - msg = "actInd has been deprecated, please use active_cells" - with pytest.warns(FutureWarning, match=msg): - mapping.actInd = new_active_cells - np.testing.assert_allclose(mapping.active_cells, new_active_cells) + msg = "actInd has been removed, please use active_cells" + with pytest.raises(NotImplementedError, match=msg): + mapping.actInd = active_cells -class TestMesh2Mesh(DeprecatedIndActive): - """Test deprecated ``indActive`` in ``Mesh2Mesh``.""" +class TestMesh2Mesh(RemovedIndActive): + """Test removed ``indActive`` in ``Mesh2Mesh``.""" @pytest.fixture def meshes(self, mesh): return [mesh, deepcopy(mesh)] - def test_warning_argument(self, meshes, active_cells): - """ - Test if warning is raised after passing ``indActive`` to the constructor. - """ - msg = self.get_message_deprecated_warning("indActive", "active_cells") - with pytest.warns(FutureWarning, match=msg): - mapping_instance = maps.Mesh2Mesh(meshes, indActive=active_cells) - np.testing.assert_allclose(mapping_instance.active_cells, active_cells) - - def test_error_duplicated_argument(self, meshes, active_cells): + def test_error_argument(self, meshes, active_cells): """ - Test error after passing ``indActive`` and ``active_cells`` to the constructor. + Test if error is raised after passing ``indActive`` to the constructor. """ - msg = self.get_message_duplicated_error("indActive", "active_cells") + msg = self.get_message_removed_error("indActive", "active_cells") with pytest.raises(TypeError, match=msg): - maps.Mesh2Mesh(meshes, active_cells=active_cells, indActive=active_cells) + maps.Mesh2Mesh(meshes, indActive=active_cells) - def test_warning_accessing_property(self, meshes, active_cells): + def test_error_accessing_property(self, meshes, active_cells): """ - Test warning when trying to access the ``indActive`` property. + Test error when trying to access the ``indActive`` property. """ mapping = maps.Mesh2Mesh(meshes, active_cells=active_cells) - msg = "indActive has been deprecated, please use active_cells" - with pytest.warns(FutureWarning, match=msg): - old_act_ind = mapping.indActive - np.testing.assert_allclose(mapping.active_cells, old_act_ind) + msg = "indActive has been removed, please use active_cells" + with pytest.raises(NotImplementedError, match=msg): + mapping.indActive def test_warning_setter(self, meshes, active_cells): """ Test warning when trying to set the ``indActive`` property. """ mapping = maps.Mesh2Mesh(meshes, active_cells=active_cells) - # Define new active cells to pass to the setter - new_active_cells = active_cells.copy() - new_active_cells[-4:] = False - msg = "indActive has been deprecated, please use active_cells" - with pytest.warns(FutureWarning, match=msg): - mapping.indActive = new_active_cells - np.testing.assert_allclose(mapping.active_cells, new_active_cells) + msg = "indActive has been removed, please use active_cells" + with pytest.raises(NotImplementedError, match=msg): + mapping.indActive = active_cells -class TestInjectActiveCells(DeprecatedIndActive): - """Test deprecated ``indActive`` and ``valInactive`` in ``InjectActiveCells``.""" - - def test_indactive_warning_argument(self, mesh, active_cells): - """ - Test if warning is raised after passing ``indActive`` to the constructor. - """ - msg = self.get_message_deprecated_warning("indActive", "active_cells") - with pytest.warns(FutureWarning, match=msg): - mapping_instance = maps.InjectActiveCells(mesh, indActive=active_cells) - np.testing.assert_allclose(mapping_instance.active_cells, active_cells) +class TestInjectActiveCells(RemovedIndActive): + """Test removed ``indActive`` and ``valInactive`` in ``InjectActiveCells``.""" - def test_indactive_error_duplicated_argument(self, mesh, active_cells): + def test_indactive_error_argument(self, mesh, active_cells): """ - Test error after passing ``indActive`` and ``active_cells`` to the constructor. + Test if error is raised after passing ``indActive`` to the constructor. """ - msg = self.get_message_duplicated_error("indActive", "active_cells") + msg = self.get_message_removed_error("indActive", "active_cells") with pytest.raises(TypeError, match=msg): - maps.InjectActiveCells( - mesh, active_cells=active_cells, indActive=active_cells - ) + maps.InjectActiveCells(mesh, indActive=active_cells) - def test_indactive_warning_accessing_property(self, mesh, active_cells): + def test_indactive_error_accessing_property(self, mesh, active_cells): """ - Test warning when trying to access the ``indActive`` property. + Test error when trying to access the ``indActive`` property. """ mapping = maps.InjectActiveCells(mesh, active_cells=active_cells) - msg = "indActive has been deprecated, please use active_cells" - with pytest.warns(FutureWarning, match=msg): - old_act_ind = mapping.indActive - np.testing.assert_allclose(mapping.active_cells, old_act_ind) + msg = "indActive has been removed, please use active_cells" + with pytest.raises(NotImplementedError, match=msg): + mapping.indActive - def test_indactive_warning_setter(self, mesh, active_cells): + def test_indactive_error_setter(self, mesh, active_cells): """ - Test warning when trying to set the ``indActive`` property. + Test error when trying to set the ``indActive`` property. """ mapping = maps.InjectActiveCells(mesh, active_cells=active_cells) - # Define new active cells to pass to the setter - new_active_cells = active_cells.copy() - new_active_cells[-4:] = False - msg = "indActive has been deprecated, please use active_cells" - with pytest.warns(FutureWarning, match=msg): - mapping.indActive = new_active_cells - np.testing.assert_allclose(mapping.active_cells, new_active_cells) + msg = "indActive has been removed, please use active_cells" + with pytest.raises(NotImplementedError, match=msg): + mapping.indActive = active_cells @pytest.mark.parametrize("value_inactive", (3.14, np.array([1]))) - def test_valinactive_warning_argument(self, mesh, active_cells, value_inactive): - """ - Test if warning is raised after passing ``valInactive`` to the constructor. - """ - msg = self.get_message_deprecated_warning("valInactive", "value_inactive") - with pytest.warns(FutureWarning, match=msg): - mapping_instance = maps.InjectActiveCells( - mesh, active_cells=active_cells, valInactive=value_inactive - ) - # Ensure that the value passed to valInactive was correctly used - expected = np.zeros_like(active_cells, dtype=np.float64) - expected[~active_cells] = value_inactive - np.testing.assert_allclose(mapping_instance.value_inactive, expected) - - @pytest.mark.parametrize("valInactive", (3.14, np.array([3.14]))) - @pytest.mark.parametrize("value_inactive", (3.14, np.array([3.14]))) - def test_valinactive_error_duplicated_argument( - self, mesh, active_cells, valInactive, value_inactive - ): + def test_valinactive_error_argument(self, mesh, active_cells, value_inactive): """ - Test error after passing ``valInactive`` and ``value_inactive`` to the - constructor. + Test if error is raised after passing ``valInactive`` to the constructor. """ - msg = self.get_message_duplicated_error("valInactive", "value_inactive") + msg = self.get_message_removed_error("valInactive", "value_inactive") with pytest.raises(TypeError, match=msg): maps.InjectActiveCells( - mesh, - active_cells=active_cells, - value_inactive=value_inactive, - valInactive=valInactive, + mesh, active_cells=active_cells, valInactive=value_inactive ) - def test_valinactive_warning_accessing_property(self, mesh, active_cells): + def test_valinactive_error_accessing_property(self, mesh, active_cells): """ - Test warning when trying to access the ``valInactive`` property. + Test error when trying to access the ``valInactive`` property. """ mapping = maps.InjectActiveCells( mesh, active_cells=active_cells, value_inactive=3.14 ) - msg = "valInactive has been deprecated, please use value_inactive" - with pytest.warns(FutureWarning, match=msg): - old_value = mapping.valInactive - np.testing.assert_allclose(mapping.value_inactive, old_value) + msg = "valInactive has been removed, please use value_inactive" + with pytest.raises(NotImplementedError, match=msg): + mapping.valInactive - def test_valinactive_warning_setter(self, mesh, active_cells): + def test_valinactive_error_setter(self, mesh, active_cells): """ - Test warning when trying to set the ``valInactive`` property. + Test error when trying to set the ``valInactive`` property. """ mapping = maps.InjectActiveCells( mesh, active_cells=active_cells, value_inactive=3.14 ) - msg = "valInactive has been deprecated, please use value_inactive" - with pytest.warns(FutureWarning, match=msg): + msg = "valInactive has been removed, please use value_inactive" + with pytest.raises(NotImplementedError, match=msg): mapping.valInactive = 4.5 - np.testing.assert_allclose(mapping.value_inactive[~mapping.active_cells], 4.5) -class TestParametric(DeprecatedIndActive): - """Test deprecated ``indActive`` in parametric mappings.""" +class TestParametric(RemovedIndActive): + """Test removed ``indActive`` in parametric mappings.""" CLASSES = (BaseParametric, ParametricLayer, ParametricBlock, ParametricEllipsoid) @pytest.mark.parametrize("map_class", CLASSES) - def test_indactive_warning_argument(self, mesh, active_cells, map_class): + def test_indactive_error_argument(self, mesh, active_cells, map_class): """ - Test if warning is raised after passing ``indActive`` to the constructor. + Test if error is raised after passing ``indActive`` to the constructor. """ - msg = self.get_message_deprecated_warning("indActive", "active_cells") - with pytest.warns(FutureWarning, match=msg): - mapping_instance = map_class(mesh, indActive=active_cells) - np.testing.assert_allclose(mapping_instance.active_cells, active_cells) - - @pytest.mark.parametrize("map_class", CLASSES) - def test_indactive_error_duplicated_argument(self, mesh, active_cells, map_class): - """ - Test error after passing ``indActive`` and ``active_cells`` to the constructor. - """ - msg = self.get_message_duplicated_error("indActive", "active_cells") + msg = self.get_message_removed_error("indActive", "active_cells") with pytest.raises(TypeError, match=msg): - map_class(mesh, active_cells=active_cells, indActive=active_cells) + map_class(mesh, indActive=active_cells) @pytest.mark.parametrize("map_class", CLASSES) - def test_indactive_warning_accessing_property(self, mesh, active_cells, map_class): + def test_indactive_error_accessing_property(self, mesh, active_cells, map_class): """ - Test warning when trying to access the ``indActive`` property. + Test error when trying to access the ``indActive`` property. """ mapping = map_class(mesh, active_cells=active_cells) - msg = "indActive has been deprecated, please use active_cells" - with pytest.warns(FutureWarning, match=msg): - old_act_ind = mapping.indActive - np.testing.assert_allclose(mapping.active_cells, old_act_ind) + msg = "indActive has been removed, please use active_cells" + with pytest.raises(NotImplementedError, match=msg): + mapping.indActive @pytest.mark.parametrize("map_class", CLASSES) - def test_indactive_warning_setter(self, mesh, active_cells, map_class): + def test_indactive_error_setter(self, mesh, active_cells, map_class): """ - Test warning when trying to set the ``indActive`` property. + Test error when trying to set the ``indActive`` property. """ mapping = map_class(mesh, active_cells=active_cells) - # Define new active cells to pass to the setter - new_active_cells = active_cells.copy() - new_active_cells[-4:] = False - msg = "indActive has been deprecated, please use active_cells" - with pytest.warns(FutureWarning, match=msg): - mapping.indActive = new_active_cells - np.testing.assert_allclose(mapping.active_cells, new_active_cells) + msg = "indActive has been removed, please use active_cells" + with pytest.raises(NotImplementedError, match=msg): + mapping.indActive = active_cells if __name__ == "__main__": diff --git a/tests/em/fdem/forward/test_FDEM_sources.py b/tests/em/fdem/forward/test_FDEM_sources.py index 7ba8de8170..640790e534 100644 --- a/tests/em/fdem/forward/test_FDEM_sources.py +++ b/tests/em/fdem/forward/test_FDEM_sources.py @@ -376,24 +376,6 @@ def test_CircularLoop_bPrimaryMu50_h(self): assert self.bPrimaryTest(src, "j") -def test_removal_circular_loop_n(): - """ - Test if passing the N argument to CircularLoop raises an error - """ - msg = "'N' property has been removed. Please use 'n_turns'." - with pytest.raises(TypeError, match=msg): - fdem.sources.CircularLoop( - [], - frequency=1e-3, - radius=np.sqrt(1 / np.pi), - location=[0, 0, 0], - orientation="Z", - mu=mu_0, - current=0.5, - N=2, - ) - - def test_line_current_failures(): rx_locs = [[0.5, 0.5, 0]] tx_locs = [[0, 0, 0], [0, 1, 0], [1, 1, 0], [1, 0, 0], [0, 0, 0]] diff --git a/tests/em/nsem/forward/test_Recursive1D_VsAnalyticHalfspace.py b/tests/em/nsem/forward/test_Recursive1D_VsAnalyticHalfspace.py index fb2e6fa5ac..be624bb19f 100644 --- a/tests/em/nsem/forward/test_Recursive1D_VsAnalyticHalfspace.py +++ b/tests/em/nsem/forward/test_Recursive1D_VsAnalyticHalfspace.py @@ -71,10 +71,8 @@ def test_4(self): "rx_class", [ ns_rx.Impedance, - ns_rx.PointNaturalSource, ns_rx.Admittance, ns_rx.Tipper, - ns_rx.Point3DTipper, ns_rx.ApparentConductivity, ], ) @@ -84,7 +82,7 @@ def test_incorrect_rx_types(rx_class): source = nsem.sources.Planewave(rx, frequency=10) survey = nsem.Survey(source) # make sure that only these exact classes do not issue warnings. - if rx_class in [ns_rx.Impedance, ns_rx.PointNaturalSource]: + if rx_class is ns_rx.Impedance: with warnings.catch_warnings(): warnings.simplefilter("error") nsem.Simulation1DRecursive(survey=survey) diff --git a/tests/em/nsem/test_nsem_point_deprecations.py b/tests/em/nsem/test_nsem_point_deprecations.py deleted file mode 100644 index 35128d890e..0000000000 --- a/tests/em/nsem/test_nsem_point_deprecations.py +++ /dev/null @@ -1,215 +0,0 @@ -import inspect -import re - -import pytest -import simpeg.electromagnetics.natural_source as nsem -import numpy as np -import discretize -import numpy.testing as npt - - -@pytest.fixture( - params=[ - "same_location", - "diff_location", - ] -) -def impedance_pairs(request): - test_e_locs = np.array([[0.2, 0.1, 0.3], [-0.1, 0.2, -0.3]]) - test_h_locs = np.array([[-0.2, 0.24, 0.1], [0.5, 0.2, -0.2]]) - - rx_point_type = request.param - if rx_point_type == "same": - rx1 = nsem.receivers.PointNaturalSource(test_e_locs) - rx2 = nsem.receivers.Impedance(test_e_locs, orientation="xy") - else: - rx1 = nsem.receivers.PointNaturalSource( - locations_e=test_e_locs, locations_h=test_h_locs - ) - rx2 = nsem.receivers.Impedance( - locations_e=test_e_locs, locations_h=test_h_locs, orientation="xy" - ) - return rx1, rx2 - - -@pytest.fixture() -def tipper_pairs(): - test_e_locs = np.array([[0.2, 0.1, 0.3], [-0.1, 0.2, -0.3]]) - - rx1 = nsem.receivers.Point3DTipper(test_e_locs) - rx2 = nsem.receivers.Tipper(test_e_locs, orientation="zx") - return rx1, rx2 - - -def test_deprecation(): - test_loc = np.array([10.0, 11.0, 12.0]) - with pytest.warns(FutureWarning, match="PointNaturalSource has been deprecated.*"): - nsem.receivers.PointNaturalSource(test_loc) - - with pytest.warns(FutureWarning, match="Using the default for locations.*"): - nsem.receivers.PointNaturalSource() - - with pytest.warns(FutureWarning, match="Point3DTipper has been deprecated.*"): - nsem.receivers.Point3DTipper(test_loc) - - -def test_imp_consistent_attributes(impedance_pairs): - rx1, rx2 = impedance_pairs - - for item_name in dir(rx1): - is_dunder = re.match(r"__\w+__", item_name) is not None - # skip a few things related to the wrapping, and dunder methods - if not (item_name in ["locations", "_uid", "uid", "_old__init__"] or is_dunder): - item1 = getattr(rx1, item_name) - item2 = getattr(rx2, item_name) - if not (inspect.isfunction(item1) or inspect.ismethod(item1)): - if isinstance(item1, np.ndarray): - npt.assert_array_equal(item1, item2) - else: - assert item1 == item2 - - npt.assert_array_equal(rx1.locations, rx2.locations_e) - - -def test_tip_consistent_attributes(tipper_pairs): - rx1, rx2 = tipper_pairs - - for item_name in dir(rx1): - is_dunder = re.match(r"__\w+__", item_name) is not None - # skip a few things related to the wrapping, and dunder methods - if not ( - item_name in ["locations", "locations_e", "_uid", "uid", "_old__init__"] - or is_dunder - ): - item1 = getattr(rx1, item_name) - item2 = getattr(rx2, item_name) - if not (inspect.isfunction(item1) or inspect.ismethod(item1)): - print(item_name, item1, item2) - if isinstance(item1, np.ndarray): - npt.assert_array_equal(item1, item2) - else: - assert item1 == item2 - - npt.assert_array_equal(rx1.locations, rx2.locations_h) - npt.assert_array_equal(rx1.locations, rx2.locations_base) - - -@pytest.mark.parametrize( - "rx_component", ["real", "imag", "apparent_resistivity", "phase", "complex"] -) -def test_imp_consistent_eval(impedance_pairs, rx_component): - rx1, rx2 = impedance_pairs - rx1.component = rx_component - rx2.component = rx_component - # test that the output of the function eval returns the same thing, - # since it was updated... - mesh = discretize.TensorMesh([3, 4, 5], origin="CCC") - - # create a mock simulation - src = nsem.sources.PlanewaveXYPrimary( - [rx1, rx2], frequency=10, sigma_primary=np.ones(mesh.n_cells) - ) - survey = nsem.Survey(src) - sim_temp = nsem.Simulation3DPrimarySecondary(survey=survey, mesh=mesh, sigma=1) - - # Create a mock field, - f = sim_temp.fieldsPair(sim_temp) - test_u = np.linspace(1, 2, 2 * mesh.n_edges) + 1j * np.linspace( - -1, 1, 2 * mesh.n_edges - ) - f[src, sim_temp._solutionType] = test_u.reshape(mesh.n_edges, 2) - - v1 = rx1.eval(src, mesh, f) - v2 = rx2.eval(src, mesh, f) - - npt.assert_equal(v1, v2) - - if rx_component == "real": - # do a quick test here that calling eval on rx1 is the same as calling - # eval on rx2 with a complex component - rx2.component = "complex" - with pytest.warns(FutureWarning, match="Calling with return_complex=True.*"): - v1 = rx1.eval(src, mesh, f, return_complex=True) - v2 = rx2.eval(src, mesh, f) - - # assert it reset - assert rx1.component == "real" - # assert the outputs are the same - npt.assert_equal(v1, v2) - - -@pytest.mark.parametrize("rx_component", ["real", "imag", "complex"]) -def test_tip_consistent_eval(tipper_pairs, rx_component): - rx1, rx2 = tipper_pairs - rx1.component = rx_component - rx2.component = rx_component - # test that the output of the function eval returns the same thing, - # since it was updated... - mesh = discretize.TensorMesh([3, 4, 5], origin="CCC") - - # create a mock simulation - src = nsem.sources.PlanewaveXYPrimary( - [rx1, rx2], frequency=10, sigma_primary=np.ones(mesh.n_cells) - ) - survey = nsem.Survey(src) - sim_temp = nsem.Simulation3DPrimarySecondary(survey=survey, mesh=mesh, sigma=1) - - # Create a mock field, - f = sim_temp.fieldsPair(sim_temp) - test_u = np.linspace(1, 2, 2 * mesh.n_edges) + 1j * np.linspace( - -1, 1, 2 * mesh.n_edges - ) - f[src, sim_temp._solutionType] = test_u.reshape(mesh.n_edges, 2) - - v1 = rx1.eval(src, mesh, f) - v2 = rx2.eval(src, mesh, f) - - npt.assert_equal(v1, v2) - - if rx_component == "real": - # do a quick test here that calling eval on rx1 is the same as calling - # eval on rx2 with a complex component - rx2.component = "complex" - with pytest.warns(FutureWarning, match="Calling with return_complex=True.*"): - v1 = rx1.eval(src, mesh, f, return_complex=True) - v2 = rx2.eval(src, mesh, f) - - # assert it reset - assert rx1.component == "real" - # assert the outputs are the same - npt.assert_equal(v1, v2) - - -def test_imp_location_initialization(): - loc_1 = np.empty((2, 3)) - loc_2 = np.empty((2, 3)) - with pytest.raises(TypeError, match="Cannot pass both locations and .*"): - nsem.receivers.PointNaturalSource(locations=loc_1, locations_h=loc_2) - - with pytest.raises(TypeError, match="Either locations or both locations_e.*"): - nsem.receivers.PointNaturalSource(locations_e=loc_1) - - rx1 = nsem.receivers.PointNaturalSource(locations=[loc_1]) - rx2 = nsem.receivers.Impedance(loc_1) - npt.assert_equal(rx1.locations, rx2.locations_e) - npt.assert_equal(rx1.locations, rx2.locations_h) - - rx1 = nsem.receivers.PointNaturalSource(locations=[loc_1, loc_2]) - rx2 = nsem.receivers.Impedance(loc_1, loc_2) - npt.assert_equal(rx1.locations_e, rx2.locations_e) - npt.assert_equal(rx1.locations_h, rx2.locations_h) - - with pytest.raises(ValueError, match="incorrect size of list, must be length .*"): - nsem.receivers.PointNaturalSource(locations=[loc_1, loc_2, loc_1]) - - -def test_tip_location_initialization(): - loc_1 = np.empty((2, 3)) - loc_2 = np.empty((2, 3)) - with pytest.warns(UserWarning, match="locations_e and locations_h are unused.*"): - nsem.receivers.Point3DTipper(locations=loc_1, locations_e=loc_2) - - with pytest.raises( - ValueError, match="incorrect size of list, must be length of 1 or 2" - ): - nsem.receivers.Point3DTipper([loc_1, loc_1, loc_1]) diff --git a/tests/em/static/test_SPjvecjtvecadj.py b/tests/em/static/test_SPjvecjtvecadj.py index 94fb2b86a4..583d530c6d 100644 --- a/tests/em/static/test_SPjvecjtvecadj.py +++ b/tests/em/static/test_SPjvecjtvecadj.py @@ -123,25 +123,8 @@ def test_clears(): def test_deprecations(): """ - Test warning after importing deprecated `spontaneous_potential` module + Test error after importing deprecated `spontaneous_potential` module """ - msg = ( - "The 'spontaneous_potential' module has been renamed to 'self_potential'. " - "Please use the 'self_potential' module instead. " - "The 'spontaneous_potential' module will be removed in SimPEG 0.23." - ) - with pytest.warns(FutureWarning, match=msg): + msg = "The 'spontaneous_potential' module has been moved to 'self_potential'" + with pytest.raises(ImportError, match=msg): import simpeg.electromagnetics.static.spontaneous_potential # noqa: F401 - - -def test_imported_objects_on_deprecated_module(): - """ - Test if the new `self_potential` module and the deprecated `spontaneous - potential` have the same members. - """ - import simpeg.electromagnetics.static.spontaneous_potential as spontaneous - - members_self = set([m for m in dir(sp) if not m.startswith("_")]) - members_spontaneous = set([m for m in dir(spontaneous) if not m.startswith("_")]) - difference = members_self - members_spontaneous - assert not difference diff --git a/tests/em/static/test_dc_survey.py b/tests/em/static/test_dc_survey.py index f7b88754db..7087cce873 100644 --- a/tests/em/static/test_dc_survey.py +++ b/tests/em/static/test_dc_survey.py @@ -16,25 +16,23 @@ class TestRemovedSourceType: Tests after removing the source_type argument and property. """ - def test_warning_after_argument(self): + def test_error_after_argument(self): """ - Test warning after passing source_type as argument to the constructor. + Test error after passing ``source_type`` as argument to the constructor. """ - msg = "Argument 'survey_type' is ignored and will be removed in future" - with pytest.warns(FutureWarning, match=msg): - survey = Survey(source_list=[], survey_type="dipole-dipole") - # Check if the object doesn't have a `_survey_type` attribute - assert not hasattr(survey, "_survey_type") + msg = "Argument 'survey_type' has been removed" + with pytest.raises(TypeError, match=msg): + Survey(source_list=[], survey_type="dipole-dipole") - def test_warning_removed_property(self): + def test_error_removed_property(self): """ - Test if warning is raised when accessing the survey_type property. + Test if error is raised when accessing the ``survey_type`` property. """ survey = Survey(source_list=[]) - msg = "Property 'survey_type' has been removed." - with pytest.warns(FutureWarning, match=msg): + msg = "'survey_type' has been removed." + with pytest.raises(AttributeError, match=msg): survey.survey_type - with pytest.warns(FutureWarning, match=msg): + with pytest.raises(AttributeError, match=msg): survey.survey_type = "dipole-dipole" @@ -52,7 +50,7 @@ def test_error(self, mesh): Test if error is raised after passing ``ind_active`` as argument. """ survey = Survey(source_list=[]) - msg = "'ind_active' has been deprecated and will be removed in " + msg = "got an unexpected keyword argument 'ind_active'" active_cells = np.ones(mesh.n_cells, dtype=bool) with pytest.raises(TypeError, match=msg): survey.drape_electrodes_on_topography( diff --git a/tests/em/static/test_sip_survey.py b/tests/em/static/test_sip_survey.py index ff3ecc3b51..c39523c0de 100644 --- a/tests/em/static/test_sip_survey.py +++ b/tests/em/static/test_sip_survey.py @@ -12,23 +12,21 @@ class TestRemovedSourceType: Tests after removing the source_type argument and property. """ - def test_warning_after_argument(self): + def test_error_after_argument(self): """ - Test warning after passing source_type as argument to the constructor. + Test error after passing ``source_type`` as argument to the constructor. """ - msg = "Argument 'survey_type' is ignored and will be removed in future" - with pytest.warns(FutureWarning, match=msg): - survey = Survey(source_list=[], survey_type="dipole-dipole") - # Check if the object doesn't have a `_survey_type` attribute - assert not hasattr(survey, "_survey_type") + msg = "Argument 'survey_type' has been removed" + with pytest.raises(TypeError, match=msg): + Survey(source_list=[], survey_type="dipole-dipole") - def test_warning_removed_property(self): + def test_error_removed_property(self): """ - Test if warning is raised when accessing the survey_type property. + Test if error is raised when accessing the ``survey_type`` property. """ survey = Survey(source_list=[]) - msg = "Property 'survey_type' has been removed." - with pytest.warns(FutureWarning, match=msg): + msg = "'survey_type' has been removed." + with pytest.raises(AttributeError, match=msg): survey.survey_type - with pytest.warns(FutureWarning, match=msg): + with pytest.raises(AttributeError, match=msg): survey.survey_type = "dipole-dipole" diff --git a/tests/em/static/test_spectral_ip_mappings.py b/tests/em/static/test_spectral_ip_mappings.py index 99f244a2c0..f82379c90c 100644 --- a/tests/em/static/test_spectral_ip_mappings.py +++ b/tests/em/static/test_spectral_ip_mappings.py @@ -29,35 +29,13 @@ def active_cells(self, mesh): active_cells[0] = False return active_cells - def get_message_duplicated_error(self, old_name, new_name, version="v0.24.0"): - msg = ( - f"Cannot pass both '{new_name}' and '{old_name}'." - f"'{old_name}' has been deprecated and will be removed in " - f" SimPEG {version}, please use '{new_name}' instead." - ) - return msg - - def get_message_deprecated_warning(self, old_name, new_name, version="v0.24.0"): - msg = ( - f"'{old_name}' has been deprecated and will be removed in " - f" SimPEG {version}, please use '{new_name}' instead." - ) - return msg - - def test_warning_argument(self, mesh, active_cells): + def test_error_argument(self, mesh, active_cells): """ - Test if warning is raised after passing ``indActive`` as argument. + Test if error is raised after passing ``indActive`` as argument. """ - msg = self.get_message_deprecated_warning(self.OLD_NAME, self.NEW_NAME) - with pytest.warns(FutureWarning, match=msg): - spectral_ip_mappings(mesh, indActive=active_cells) - - def test_error_duplicated_argument(self, mesh, active_cells): - """ - Test error after passing ``indActive`` and ``active_cells`` as arguments. - """ - msg = self.get_message_duplicated_error(self.OLD_NAME, self.NEW_NAME) + msg = ( + "'indActive' was removed in SimPEG v0.24.0, " + "please use 'active_cells' instead." + ) with pytest.raises(TypeError, match=msg): - spectral_ip_mappings( - mesh, active_cells=active_cells, indActive=active_cells - ) + spectral_ip_mappings(mesh, indActive=active_cells) diff --git a/tests/em/static/test_static_utils.py b/tests/em/static/test_static_utils.py index b02489fac6..c7213d6236 100644 --- a/tests/em/static/test_static_utils.py +++ b/tests/em/static/test_static_utils.py @@ -32,35 +32,10 @@ def active_cells(self, mesh): active_cells[0] = False return active_cells - def get_message_duplicated_error(self, old_name, new_name, version="v0.24.0"): - msg = ( - f"Cannot pass both '{new_name}' and '{old_name}'." - f"'{old_name}' has been deprecated and will be removed in " - f" SimPEG {version}, please use '{new_name}' instead." - ) - return msg - - def get_message_deprecated_warning(self, old_name, new_name, version="v0.24.0"): - msg = ( - f"'{old_name}' has been deprecated and will be removed in " - f" SimPEG {version}, please use '{new_name}' instead." - ) - return msg - - def test_warning_argument(self, mesh, points, active_cells): + def test_error_argument(self, mesh, points, active_cells): """ - Test if warning is raised after passing ``ind_active`` as argument. + Test if error is raised after passing ``ind_active`` as argument. """ - msg = self.get_message_deprecated_warning(self.OLD_NAME, self.NEW_NAME) - with pytest.warns(FutureWarning, match=msg): - drapeTopotoLoc(mesh, points, ind_active=active_cells) - - def test_error_duplicated_argument(self, mesh, points, active_cells): - """ - Test error after passing ``ind_active`` and ``active_cells`` as arguments. - """ - msg = self.get_message_duplicated_error(self.OLD_NAME, self.NEW_NAME) + msg = "Unsupported keyword argument ind_active" with pytest.raises(TypeError, match=msg): - drapeTopotoLoc( - mesh, points, active_cells=active_cells, ind_active=active_cells - ) + drapeTopotoLoc(mesh, points, ind_active=active_cells) diff --git a/tests/em/tdem/test_TDEM_sources.py b/tests/em/tdem/test_TDEM_sources.py index e46dbdc9e2..6c7d248543 100644 --- a/tests/em/tdem/test_TDEM_sources.py +++ b/tests/em/tdem/test_TDEM_sources.py @@ -1,4 +1,3 @@ -import pytest import unittest import numpy as np @@ -6,7 +5,6 @@ from discretize.tests import check_derivative from numpy.testing import assert_array_almost_equal from simpeg.electromagnetics.time_domain.sources import ( - CircularLoop, ExponentialWaveform, HalfSineWaveform, PiecewiseLinearWaveform, @@ -526,19 +524,3 @@ def f(t): def test_simple_source(): waveform = StepOffWaveform() assert waveform.eval(0.0) == 1.0 - - -def test_removal_circular_loop_n(): - """ - Test if passing the N argument to CircularLoop raises an error - """ - msg = "'N' property has been removed. Please use 'n_turns'." - with pytest.raises(TypeError, match=msg): - CircularLoop( - [], - waveform=StepOffWaveform(), - location=np.array([0.0, 0.0, 0.0]), - radius=1.0, - current=0.5, - N=2, - ) diff --git a/tests/em/vrm/test_vrmfwd.py b/tests/em/vrm/test_vrmfwd.py index 772867faa6..70756dfaa3 100644 --- a/tests/em/vrm/test_vrmfwd.py +++ b/tests/em/vrm/test_vrmfwd.py @@ -547,64 +547,40 @@ def active_cells(self, mesh): active_cells[0] = False return active_cells - def get_message_duplicated_error(self, old_name, new_name, version="v0.24.0"): - msg = ( - f"Cannot pass both '{new_name}' and '{old_name}'." - f"'{old_name}' has been deprecated and will be removed in " - f" SimPEG {version}, please use '{new_name}' instead." - ) - return msg - - def get_message_deprecated_warning(self, old_name, new_name, version="v0.24.0"): - msg = ( - f"'{old_name}' has been deprecated and will be removed in " - f" SimPEG {version}, please use '{new_name}' instead." - ) - return msg - @pytest.mark.parametrize("simulation", CLASSES) - def test_warning_argument(self, mesh, active_cells, simulation): + def test_error_argument(self, mesh, active_cells, simulation): """ - Test if warning is raised after passing ``indActive`` to the constructor. + Test if error is raised after passing ``indActive`` to the constructor. """ - msg = self.get_message_deprecated_warning(self.OLD_NAME, self.NEW_NAME) - with pytest.warns(FutureWarning, match=msg): - sim = simulation(mesh, indActive=active_cells) - np.testing.assert_allclose(sim.active_cells, active_cells) - - @pytest.mark.parametrize("simulation", CLASSES) - def test_error_duplicated_argument(self, mesh, active_cells, simulation): - """ - Test error after passing ``indActive`` and ``active_cells`` to the constructor. - """ - msg = self.get_message_duplicated_error(self.OLD_NAME, self.NEW_NAME) + msg = ( + "'indActive' was removed in SimPEG v0.24.0, " + "please use 'active_cells' instead." + ) with pytest.raises(TypeError, match=msg): - simulation(mesh, active_cells=active_cells, indActive=active_cells) + simulation(mesh, indActive=active_cells) @pytest.mark.parametrize("simulation", CLASSES) - def test_warning_accessing_property(self, mesh, active_cells, simulation): + def test_error_accessing_property(self, mesh, active_cells, simulation): """ - Test warning when trying to access the ``indActive`` property. + Test error when trying to access the ``indActive`` property. """ sim = simulation(mesh, active_cells=active_cells) - msg = f"{self.OLD_NAME} has been deprecated, please use {self.NEW_NAME}" - with pytest.warns(FutureWarning, match=msg): - old_ind_active = sim.indActive - np.testing.assert_allclose(sim.active_cells, old_ind_active) + msg = f"{self.OLD_NAME} has been removed, please use {self.NEW_NAME}" + with pytest.raises(NotImplementedError, match=msg): + sim.indActive @pytest.mark.parametrize("simulation", CLASSES) - def test_warning_setter(self, mesh, active_cells, simulation): + def test_error_setter(self, mesh, active_cells, simulation): """ - Test warning when trying to set the ``indActive`` property. + Test error when trying to set the ``indActive`` property. """ sim = simulation(mesh, active_cells=active_cells) # Define new active cells to pass to the setter new_active_cells = active_cells.copy() new_active_cells[-4:] = False - msg = f"{self.OLD_NAME} has been deprecated, please use {self.NEW_NAME}" - with pytest.warns(FutureWarning, match=msg): + msg = f"{self.OLD_NAME} has been removed, please use {self.NEW_NAME}" + with pytest.raises(NotImplementedError, match=msg): sim.indActive = new_active_cells - np.testing.assert_allclose(sim.active_cells, new_active_cells) if __name__ == "__main__": diff --git a/tests/flow/test_Richards.py b/tests/flow/test_Richards.py index 5cab334437..d234cc82f4 100644 --- a/tests/flow/test_Richards.py +++ b/tests/flow/test_Richards.py @@ -34,7 +34,6 @@ def setUp(self): hydraulic_conductivity=k_fun, water_retention=theta_fun, root_finder_tol=1e-6, - debug=False, boundary_conditions=bc, initial_conditions=h, do_newton=False, diff --git a/tests/pf/test_base_pf_simulation.py b/tests/pf/test_base_pf_simulation.py index fc05eafdca..94f3a0efe1 100644 --- a/tests/pf/test_base_pf_simulation.py +++ b/tests/pf/test_base_pf_simulation.py @@ -309,40 +309,25 @@ def test_invalid_mesh_type(self, mock_simulation_class): mock_simulation_class(CylindricalMesh(h)) -class TestDeprecationIndActive: +class TestRemovedIndActive: """ - Test if using the deprecated ind_active argument/property raise warnings/errors + Test if using the removed ``ind_active`` argument/property raise errors. """ - def test_deprecated_argument(self, tensor_mesh, mock_simulation_class): - """Test if passing ind_active argument raises warning.""" + def test_removed_argument(self, tensor_mesh, mock_simulation_class): + """Test if passing ind_active argument raises error.""" ind_active = np.ones(tensor_mesh.n_cells, dtype=bool) - version_regex = "v[0-9]+.[0-9]+.[0-9]+" msg = ( - "'ind_active' has been deprecated and will be removed in " - f" SimPEG {version_regex}, please use 'active_cells' instead." - ) - with pytest.warns(FutureWarning, match=msg): - sim = mock_simulation_class(tensor_mesh, ind_active=ind_active) - np.testing.assert_allclose(sim.active_cells, ind_active) - - def test_error_both_args(self, tensor_mesh, mock_simulation_class): - """Test if passing both ind_active and active_cells raises error.""" - ind_active = np.ones(tensor_mesh.n_cells, dtype=bool) - version_regex = "v[0-9]+.[0-9]+.[0-9]+" - msg = ( - f"Cannot pass both 'active_cells' and 'ind_active'." - "'ind_active' has been deprecated and will be removed in " - f" SimPEG {version_regex}, please use 'active_cells' instead." + "'ind_active' has been removed in " + "SimPEG v0.24.0, please use 'active_cells' instead." ) with pytest.raises(TypeError, match=msg): - mock_simulation_class( - tensor_mesh, active_cells=ind_active, ind_active=ind_active - ) + mock_simulation_class(tensor_mesh, ind_active=ind_active) - def test_deprecated_property(self, tensor_mesh, mock_simulation_class): - """Test if passing both ind_active and active_cells raises error.""" + def test_removed_property(self, tensor_mesh, mock_simulation_class): + """Test if accessing the ind_active property raises an error.""" ind_active = np.ones(tensor_mesh.n_cells, dtype=bool) simulation = mock_simulation_class(tensor_mesh, active_cells=ind_active) - with pytest.warns(FutureWarning): + msg = "ind_active has been removed, please use active_cells." + with pytest.raises(NotImplementedError, match=msg): simulation.ind_active diff --git a/tests/pf/test_forward_Mag_Linear.py b/tests/pf/test_forward_Mag_Linear.py index e3659e508c..45a523cb97 100644 --- a/tests/pf/test_forward_Mag_Linear.py +++ b/tests/pf/test_forward_Mag_Linear.py @@ -896,7 +896,7 @@ def test_choclo_missing(self, mag_mesh, monkeypatch): def test_removed_modeltype(): - """Test if accesing removed modelType property raises error.""" + """Test if accessing removed modelType property raises error.""" h = [[(2, 2)], [(2, 2)], [(2, 2)]] mesh = discretize.TensorMesh(h) receiver_location = np.array([[0, 0, 100]]) @@ -907,8 +907,8 @@ def test_removed_modeltype(): survey = mag.Survey(background_field) mapping = maps.IdentityMap(mesh, nP=mesh.n_cells) sim = mag.Simulation3DIntegral(mesh, survey=survey, chiMap=mapping) - message = "modelType has been removed, please use model_type." - with pytest.raises(NotImplementedError, match=message): + message = "has no attribute 'modelType'" + with pytest.raises(AttributeError, match=message): sim.modelType diff --git a/tests/pf/test_mag_uniform_background_field.py b/tests/pf/test_mag_uniform_background_field.py index feeb65e909..3f8164aa1c 100644 --- a/tests/pf/test_mag_uniform_background_field.py +++ b/tests/pf/test_mag_uniform_background_field.py @@ -4,7 +4,7 @@ import pytest import numpy as np -from simpeg.potential_fields.magnetics import UniformBackgroundField, SourceField, Point +from simpeg.potential_fields.magnetics import UniformBackgroundField, Point def test_invalid_parameters_argument(): @@ -17,15 +17,6 @@ def test_invalid_parameters_argument(): UniformBackgroundField(parameters=parameters) -def test_deprecated_source_field(): - """ - Test if instantiating a magnetics.source.SourceField object raises an error - """ - msg = "SourceField has been removed, please use UniformBackgroundField." - with pytest.raises(NotImplementedError, match=msg): - SourceField() - - @pytest.mark.parametrize("receiver_as_list", (True, False)) def test_invalid_receiver_type(receiver_as_list): """ diff --git a/tests/utils/test_mat_utils.py b/tests/utils/test_mat_utils.py index c194cfc61b..f7e30402f0 100644 --- a/tests/utils/test_mat_utils.py +++ b/tests/utils/test_mat_utils.py @@ -124,8 +124,8 @@ def test_combo_eigenvalue_by_power_iteration(self): print("Eigenvalue Utils for a mixed ComboObjectiveFunction is validated.") -class TestDeprecatedSeed: - """Test deprecation of ``seed`` argument.""" +class TestRemovedSeed: + """Test removed ``seed`` argument.""" @pytest.fixture def mock_objfun(self): @@ -140,48 +140,16 @@ def deriv2(self, m, v=None, **kwargs): return MockObjectiveFunction - def get_message_duplicated_error(self, old_name, new_name, version="v0.24.0"): - msg = ( - f"Cannot pass both '{new_name}' and '{old_name}'." - f"'{old_name}' has been deprecated and will be removed in " - f" SimPEG {version}, please use '{new_name}' instead." - ) - return msg - - def get_message_deprecated_warning(self, old_name, new_name, version="v0.24.0"): - msg = ( - f"'{old_name}' has been deprecated and will be removed in " - f" SimPEG {version}, please use '{new_name}' instead." - ) - return msg - - def test_warning_argument(self, mock_objfun): + def test_error_argument(self, mock_objfun): """ - Test if warning is raised after passing ``seed``. + Test if error is raised after passing ``seed``. """ - msg = self.get_message_deprecated_warning("seed", "random_seed") + msg = "got an unexpected keyword argument 'seed'" n_params = 5 combo = mock_objfun(nP=n_params) + 3.0 * mock_objfun(nP=n_params) model = np.ones(n_params) - with pytest.warns(FutureWarning, match=msg): - result_seed = eigenvalue_by_power_iteration( - combo_objfct=combo, model=model, seed=42 - ) - # Ensure that using `seed` and `random_seed` generate the same output - result_random_seed = eigenvalue_by_power_iteration( - combo_objfct=combo, model=model, random_seed=42 - ) - np.testing.assert_allclose(result_seed, result_random_seed) - - def test_error_duplicated_argument(self): - """ - Test error after passing ``seed`` and ``random_seed``. - """ - msg = self.get_message_duplicated_error("seed", "random_seed") with pytest.raises(TypeError, match=msg): - eigenvalue_by_power_iteration( - combo_objfct=None, model=None, random_seed=42, seed=42 - ) + eigenvalue_by_power_iteration(combo_objfct=combo, model=model, seed=42) if __name__ == "__main__": diff --git a/tests/utils/test_model_builder.py b/tests/utils/test_model_builder.py index d35d5901dc..6530b815d3 100644 --- a/tests/utils/test_model_builder.py +++ b/tests/utils/test_model_builder.py @@ -3,51 +3,26 @@ """ import pytest -import numpy as np from simpeg.utils.model_builder import create_random_model -class TestDeprecateSeedProperty: +class TestRemovalSeedProperty: """ - Test deprecation of seed property. + Test removed seed property. """ - def get_message_duplicated_error(self, old_name, new_name, version="v0.24.0"): - msg = ( - f"Cannot pass both '{new_name}' and '{old_name}'." - f"'{old_name}' has been deprecated and will be removed in " - f" SimPEG {version}, please use '{new_name}' instead." - ) - return msg - - def get_message_deprecated_warning(self, old_name, new_name, version="v0.24.0"): - msg = ( - f"'{old_name}' has been deprecated and will be removed in " - f" SimPEG {version}, please use '{new_name}' instead." - ) - return msg - @pytest.fixture def shape(self): return (5, 5) - def test_warning_argument(self, shape): + def test_error_argument(self, shape): """ - Test if warning is raised after passing ``seed`` as argument. + Test if error is raised after passing ``seed`` as argument. """ - msg = self.get_message_deprecated_warning("seed", "random_seed") + msg = "Invalid arguments 'seed'" seed = 42135 - with pytest.warns(FutureWarning, match=msg): - result = create_random_model(shape, seed=seed) - np.testing.assert_allclose(result, create_random_model(shape, random_seed=seed)) - - def test_error_duplicated_argument(self, shape): - """ - Test error after passing ``seed`` and ``random_seed`` as arguments. - """ - msg = self.get_message_duplicated_error("seed", "random_seed") with pytest.raises(TypeError, match=msg): - create_random_model(shape, seed=42, random_seed=42) + create_random_model(shape, seed=seed) def test_error_invalid_kwarg(self, shape): """ @@ -56,5 +31,4 @@ def test_error_invalid_kwarg(self, shape): kwargs = {"foo": 1, "bar": 2} msg = "Invalid arguments 'foo', 'bar'." with pytest.raises(TypeError, match=msg): - with pytest.warns(FutureWarning): - create_random_model(shape, seed=10, **kwargs) + create_random_model(shape, **kwargs) diff --git a/tutorials/03-gravity/plot_inv_1c_gravity_anomaly_irls_compare_weighting.py b/tutorials/03-gravity/plot_inv_1c_gravity_anomaly_irls_compare_weighting.py index 2fc2d9210d..eae20d8780 100644 --- a/tutorials/03-gravity/plot_inv_1c_gravity_anomaly_irls_compare_weighting.py +++ b/tutorials/03-gravity/plot_inv_1c_gravity_anomaly_irls_compare_weighting.py @@ -218,7 +218,7 @@ # simulation = gravity.simulation.Simulation3DIntegral( - survey=survey, mesh=mesh, rhoMap=model_map, ind_active=ind_active + survey=survey, mesh=mesh, rhoMap=model_map, active_cells=ind_active ) # Define the data misfit. Here the data misfit is the L2 norm of the weighted @@ -243,11 +243,9 @@ # Defines the directives for the IRLS regularization. This includes setting # the cooling schedule for the trade-off parameter. -update_IRLS = directives.Update_IRLS( +update_IRLS = directives.UpdateIRLS( f_min_change=1e-4, max_irls_iterations=30, - coolEpsFact=1.5, - beta_tol=1e-2, ) # Options for outputting recovered models and predicted data for each beta. @@ -302,11 +300,9 @@ # Defines the directives for the IRLS regularization. This includes setting # the cooling schedule for the trade-off parameter. -update_IRLS = directives.Update_IRLS( +update_IRLS = directives.UpdateIRLS( f_min_change=1e-4, max_irls_iterations=30, - coolEpsFact=1.5, - beta_tol=1e-2, ) # Options for outputting recovered models and predicted data for each beta. @@ -361,11 +357,9 @@ # Defines the directives for the IRLS regularization. This includes setting # the cooling schedule for the trade-off parameter. -update_IRLS = directives.Update_IRLS( +update_IRLS = directives.UpdateIRLS( f_min_change=1e-4, max_irls_iterations=30, - coolEpsFact=1.5, - beta_tol=1e-2, ) # Options for outputting recovered models and predicted data for each beta. From 88b5f57159ce12d9dc22c151120ec20605228bc8 Mon Sep 17 00:00:00 2001 From: John Weis <49649694+johnweis0480@users.noreply.github.com> Date: Fri, 5 Sep 2025 11:36:33 -0700 Subject: [PATCH 158/194] Update magnetic simulation using differential formulation (#1682) Update the implementation of the magnetic simulation using a differential approach solving the governing equation with finite volume. Support forward modelling TMI and the three components of the magnetic field. The simulation can take the magnetic permeability and/or remanent magnetization vectors as models. Test the simulation against analytic solution for ellipsoids. Add a new `EffectiveSusceptibilityMap` that converts effective susceptibility values into magnetic polarization. --------- Co-authored-by: domfournier Co-authored-by: Santiago Soler --- simpeg/maps/__init__.py | 1 + simpeg/maps/_property_maps.py | 48 + .../potential_fields/magnetics/simulation.py | 859 +++++++++--------- tests/base/test_maps.py | 6 + tests/pf/test_components.py | 137 --- tests/pf/test_forward_PFproblem.py | 86 -- tests/pf/test_forward_mag_differential.py | 414 +++++++++ .../pf/test_mag_differential_functionality.py | 177 ++++ tests/pf/test_mag_differential_jvecjtvec.py | 190 ++++ tests/utils/ellipsoid.py | 553 +++++++++++ 10 files changed, 1810 insertions(+), 661 deletions(-) delete mode 100644 tests/pf/test_forward_PFproblem.py create mode 100644 tests/pf/test_forward_mag_differential.py create mode 100644 tests/pf/test_mag_differential_functionality.py create mode 100644 tests/pf/test_mag_differential_jvecjtvec.py create mode 100644 tests/utils/ellipsoid.py diff --git a/simpeg/maps/__init__.py b/simpeg/maps/__init__.py index d24d07aa9f..e912ef6bef 100644 --- a/simpeg/maps/__init__.py +++ b/simpeg/maps/__init__.py @@ -13,6 +13,7 @@ from ._property_maps import ( ChiMap, ComplexMap, + EffectiveSusceptibilityMap, ExpMap, LogisticSigmoidMap, LogMap, diff --git a/simpeg/maps/_property_maps.py b/simpeg/maps/_property_maps.py index 87bf56b48b..88005dc2d6 100644 --- a/simpeg/maps/_property_maps.py +++ b/simpeg/maps/_property_maps.py @@ -3,6 +3,7 @@ """ import warnings +from numbers import Real import numpy as np import scipy.sparse as sp from scipy.sparse.linalg import LinearOperator @@ -554,6 +555,53 @@ def inverse(self, m): return m / mu_0 - 1 +class EffectiveSusceptibilityMap(IdentityMap): + r"""Effective susceptibility Map + + Parameters + ---------- + mesh : discretize.BaseMesh + The number of parameters accepted by the mapping is set to equal the number + of mesh cells. + nP : int + Set the number of parameters accepted by the mapping directly. Used if the + number of parameters is known. Used generally when the number of parameters + is not equal to the number of cells in a mesh. + ambient_field_magnitude : float + The magnitude of the ambient geomagnetic field in nT. + + Notes + ----- + This map converts effective susceptibility values (:math:`\chi_\text{eff}`) into magnetic + polarization (:math:`\mathbf{I}`): + + .. math:: + \mathbf{I} = \mu_0 \mathbf{M} = \chi_\text{eff} \lVert \mathbf{B}_0 \rVert + + where :math:`\mathbf{M}` is the magnetization vector, and + :math:`\lVert \mathbf{B}_0 \rVert` is the magnitude of the ambient field in nT. + """ + + def __init__(self, ambient_field_magnitude, mesh=None, nP=None, **kwargs): + super().__init__(mesh=mesh, nP=nP, **kwargs) + if not isinstance(ambient_field_magnitude, Real): + raise TypeError( + "ambient_field_magnitude must be a float (or int convertible to float)" + ) + self.ambient_field_magnitude = ambient_field_magnitude + + def _transform(self, m): + return m * self.ambient_field_magnitude + + def deriv(self, m, v=None): + if v is not None: + return self.ambient_field_magnitude * v + return self.ambient_field_magnitude * sp.eye(self.nP) + + def inverse(self, m): + return m / self.ambient_field_magnitude + + class MuRelative(IdentityMap): r"""Mapping that computes the magnetic permeability given a set of relative permeabilities. diff --git a/simpeg/potential_fields/magnetics/simulation.py b/simpeg/potential_fields/magnetics/simulation.py index da5acc31c2..a44cc552d7 100644 --- a/simpeg/potential_fields/magnetics/simulation.py +++ b/simpeg/potential_fields/magnetics/simulation.py @@ -3,6 +3,7 @@ import numpy as np from numpy.typing import NDArray import scipy.sparse as sp +from functools import cached_property from geoana.kernels import ( prism_fxxy, prism_fxxz, @@ -16,12 +17,11 @@ from scipy.sparse.linalg import LinearOperator, aslinearoperator from simpeg import props, utils -from simpeg.utils import mat_utils, mkvc, sdiag, get_default_solver +from simpeg.utils import mat_utils, mkvc, sdiag from simpeg.utils.code_utils import validate_string, validate_type from ...base import BaseMagneticPDESimulation from ..base import BaseEquivalentSourceLayerSimulation, BasePFSimulation -from .analytics import CongruousMagBC from .survey import Survey from ._numba import choclo, NUMBA_FUNCTIONS_3D, NUMBA_FUNCTIONS_2D @@ -1654,562 +1654,545 @@ def _gtg_diagonal_without_building_g(self, weights): class Simulation3DDifferential(BaseMagneticPDESimulation): - """ - Secondary field approach using differential equations! + r"""A secondary field simulation for magnetic data. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + survey : magnetics.survey.Survey + mu : float, array_like + Magnetic Permeability Model (H/ m). Set this for forward + modeling or to fix while inverting for remanence. This is used if + ``muMap`` is None. + muMap : simpeg.maps.IdentityMap, optional + The mapping used to go from the simulation model to ``mu``. Set this + to invert for ``mu``. + rem : float, array_like + Magnetic Polarization :math:`\mu_0 \mathbf{M}` (nT). Set this for forward + modeling or to fix remanent magnetization while inverting for permeability. + This is used if ``remMap`` is None. + remMap : simpeg.maps.IdentityMap, optional + The mapping used to go from the simulation model to :math:`\mu_0 \mathbf{M}`. + Set this to invert for :math:`\mu_0 \mathbf{M}`. + storeJ: bool + Whether to store the sensitivity matrix. If set to True + solver_dtype: dtype, optional + Data type to use for the matrix that gets passed to the ``solver``. + Default to `numpy.float64`. + + + Notes + ----- + This simulation solves for the magnetostatic PDE: + + .. math:: + \nabla \cdot \Vec{B} = 0 + + where the constitutive relation is specified as: + + .. math:: + \Vec{B} = \mu\Vec{H} + \mu_0\Vec{M_r} + + where :math:`\Vec{M_r}` is a fixed magnetization unaffected by the inducing field + and :math:`\mu\Vec{H}` is the induced magnetization. """ - def __init__(self, mesh, survey=None, **kwargs): - super().__init__(mesh, survey=survey, **kwargs) + _Ainv = None - Pbc, Pin, self._Pout = self.mesh.get_BC_projections( - "neumann", discretization="CC" - ) + rem, remMap, remDeriv = props.Invertible( + "Magnetic Polarization (nT)", optional=True + ) + + _supported_components = ("tmi", "bx", "by", "bz") + + def __init__( + self, + mesh, + survey=None, + mu=None, + muMap=None, + rem=None, + remMap=None, + storeJ=False, + solver_dtype=np.float64, + **kwargs, + ): + if mu is None: + mu = mu_0 + + super().__init__(mesh=mesh, survey=survey, mu=mu, muMap=muMap, **kwargs) + + self.rem = rem + self.remMap = remMap + + self.storeJ = storeJ + self.solver_dtype = solver_dtype + + self._MfMu0i = self.mesh.get_face_inner_product(1.0 / mu_0) + self._Div = self.Mcc * self.mesh.face_divergence + self._DivT = self._Div.T.tocsr() + self._Mf_vec_deriv = self.mesh.get_face_inner_product_deriv( + np.ones(self.mesh.n_cells * 3) + )(np.ones(self.mesh.n_faces)) - Dface = self.mesh.face_divergence - Mc = sdiag(self.mesh.cell_volumes) - self._Div = Mc * Dface * Pin.T.tocsr() * Pin + self.solver_opts = {"is_symmetric": True, "is_positive_definite": True} + + self._Jmatrix = None + self._stored_fields = None @property def survey(self): - """The survey for this simulation. + """The magnetic survey object. Returns ------- - simpeg.potential_fields.magnetics.survey.Survey + simpeg.potential_fields.magnetics.Survey """ if self._survey is None: raise AttributeError("Simulation must have a survey") return self._survey @survey.setter - def survey(self, obj): - if obj is not None: - obj = validate_type("survey", obj, Survey, cast=False) - self._validate_survey(obj) - self._survey = obj + def survey(self, value): + if value is not None: + value = validate_type("survey", value, Survey, cast=False) + unsupported_components = { + component + for source in value.source_list + for receiver in source.receiver_list + for component in receiver.components + if component not in self._supported_components + } + if unsupported_components: + msg = ( + f"Found unsupported magnetic components " + f"'{', '.join(c for c in unsupported_components)}' in the survey." + f"The {type(self).__name__} currently supports the following " + f"components: {', '.join(c for c in self._supported_components)}" + ) + raise NotImplementedError(msg) + self._survey = value @property - def MfMuI(self): - return self._MfMuI + def storeJ(self): + """Whether to store the sensitivity matrix - @property - def MfMui(self): - return self._MfMui + Returns + ------- + bool + """ + return self._storeJ + + @storeJ.setter + def storeJ(self, value): + self._storeJ = validate_type("storeJ", value, bool) @property - def MfMu0(self): - return self._MfMu0 - - def makeMassMatrices(self, m): - mu = self.muMap * m - self._MfMui = self.mesh.get_face_inner_product(1.0 / mu) / self.mesh.dim - # self._MfMui = self.mesh.get_face_inner_product(1./mu) - # TODO: this will break if tensor mu - self._MfMuI = sdiag(1.0 / self._MfMui.diagonal()) - self._MfMu0 = self.mesh.get_face_inner_product(1.0 / mu_0) / self.mesh.dim - # self._MfMu0 = self.mesh.get_face_inner_product(1/mu_0) + def solver_dtype(self): + """ + Data type used by the solver. + + Returns + ------- + numpy.dtype + Either np.float32 or np.float64 + """ + return self._solver_dtype + @solver_dtype.setter + def solver_dtype(self, value): + """ + Set the solver dtype. Must be np.float32 or np.float64. + """ + if value not in (np.float32, np.float64): + msg = ( + f"Invalid `solver_dtype` '{value}'. " + "It must be np.float32 or np.float64." + ) + raise ValueError(msg) + self._solver_dtype = value + + @cached_property @utils.requires("survey") - def getB0(self): + def _b0(self): + # Todo: Experiment with avoiding array of constants b0 = self.survey.source_field.b0 - B0 = np.r_[ + b0 = np.r_[ b0[0] * np.ones(self.mesh.nFx), b0[1] * np.ones(self.mesh.nFy), b0[2] * np.ones(self.mesh.nFz), ] - return B0 + return b0 - def getRHS(self, m): - r""" + @property + def _stored_fields(self): + return self.__stored_fields - .. math :: + @_stored_fields.setter + def _stored_fields(self, value): + self.__stored_fields = value - \mathbf{rhs} = - \Div(\MfMui)^{-1}\mathbf{M}^f_{\mu_0^{-1}}\mathbf{B}_0 - - \Div\mathbf{B}_0 - +\diag(v)\mathbf{D} \mathbf{P}_{out}^T \mathbf{B}_{sBC} + @_stored_fields.deleter + def _stored_fields(self): + self.__stored_fields = None - """ - B0 = self.getB0() + def _getRHS(self, m): + self.model = m - mu = self.muMap * m - chi = mu / mu_0 - 1 + rhs = 0 - # Temporary fix - Bbc, Bbc_const = CongruousMagBC(self.mesh, self.survey.source_field.b0, chi) - self.Bbc = Bbc - self.Bbc_const = Bbc_const - # return self._Div*self.MfMuI*self.MfMu0*B0 - self._Div*B0 + - # Mc*Dface*self._Pout.T*Bbc - return self._Div * self.MfMuI * self.MfMu0 * B0 - self._Div * B0 + if not np.isscalar(self.mu) or not np.allclose(self.mu, mu_0): + rhs += ( + self._Div * self.MfMuiI * self._MfMu0i * self._b0 - self._Div * self._b0 + ) - def getA(self, m): - r""" - GetA creates and returns the A matrix for the Magnetics problem + if self.rem is not None: + rhs += ( + self._Div + * ( + self.MfMuiI + * self.mesh.get_face_inner_product( + self.rem + / np.tile(self.mu * np.ones(self.mesh.n_cells), self.mesh.dim) + ) + ).diagonal() + ) - The A matrix has the form: + return rhs - .. math :: + def _getA(self): + A = self._Div * self.MfMuiI * self._DivT - \mathbf{A} = \Div(\MfMui)^{-1}\Div^{T} + A = A.astype(self.solver_dtype) - """ - return self._Div * self.MfMuI * self._Div.T.tocsr() + return A def fields(self, m): - r""" - Return magnetic potential (u) and flux (B) - - u: defined on the cell center [nC x 1] - B: defined on the cell center [nG x 1] - - After we compute u, then we update B. - - .. math :: - - \mathbf{B}_s = - (\MfMui)^{-1}\mathbf{M}^f_{\mu_0^{-1}}\mathbf{B}_0 - - \mathbf{B}_0 - - (\MfMui)^{-1}\Div^T \mathbf{u} - - """ - self.makeMassMatrices(m) - A = self.getA(m) - rhs = self.getRHS(m) - Ainv = self.solver(A, **self.solver_opts) - u = Ainv * rhs - B0 = self.getB0() - B = self.MfMuI * self.MfMu0 * B0 - B0 - self.MfMuI * self._Div.T * u - Ainv.clean() - - return {"B": B, "u": u} - - @utils.timeIt - def Jvec(self, m, v, u=None): - r""" - Computing Jacobian multiplied by vector - - By setting our problem as - - .. math :: - - \mathbf{C}(\mathbf{m}, \mathbf{u}) = \mathbf{A}\mathbf{u} - \mathbf{rhs} = 0 - - And taking derivative w.r.t m - - .. math :: - - \nabla \mathbf{C}(\mathbf{m}, \mathbf{u}) = - \nabla_m \mathbf{C}(\mathbf{m}) \delta \mathbf{m} + - \nabla_u \mathbf{C}(\mathbf{u}) \delta \mathbf{u} = 0 - - \frac{\delta \mathbf{u}}{\delta \mathbf{m}} = - - [\nabla_u \mathbf{C}(\mathbf{u})]^{-1}\nabla_m \mathbf{C}(\mathbf{m}) - - With some linear algebra we can have - - .. math :: - - \nabla_u \mathbf{C}(\mathbf{u}) = \mathbf{A} - - \nabla_m \mathbf{C}(\mathbf{m}) = - \frac{\partial \mathbf{A}} {\partial \mathbf{m}} (\mathbf{m}) \mathbf{u} - - \frac{\partial \mathbf{rhs}(\mathbf{m})}{\partial \mathbf{m}} + self.model = m - .. math :: + if self._stored_fields is None: - \frac{\partial \mathbf{A}}{\partial \mathbf{m}}(\mathbf{m})\mathbf{u} = - \frac{\partial \mathbf{\mu}}{\partial \mathbf{m}} - \left[\Div \diag (\Div^T \mathbf{u}) \dMfMuI \right] + if self._Ainv is None: + self._Ainv = self.solver(self._getA(), **self.solver_opts) - \dMfMuI = - \diag(\MfMui)^{-1}_{vec} - \mathbf{Av}_{F2CC}^T\diag(\mathbf{v})\diag(\frac{1}{\mu^2}) + rhs = self._getRHS(m) - \frac{\partial \mathbf{rhs}(\mathbf{m})}{\partial \mathbf{m}} = - \frac{\partial \mathbf{\mu}}{\partial \mathbf{m}} - \left[ - \Div \diag(\M^f_{\mu_{0}^{-1}}\mathbf{B}_0) \dMfMuI - \right] - - \diag(\mathbf{v}) \mathbf{D} \mathbf{P}_{out}^T - \frac{\partial B_{sBC}}{\partial \mathbf{m}} + u = self._Ainv * rhs + b_field = -self.MfMuiI * self._DivT * u - In the end, + if not np.isscalar(self.mu) or not np.allclose(self.mu, mu_0): + b_field += self._MfMu0i * self.MfMuiI * self._b0 - self._b0 - .. math :: + if self.rem is not None: + b_field += ( + self.MfMuiI + * self.mesh.get_face_inner_product( + self.rem + / np.tile(self.mu * np.ones(self.mesh.n_cells), self.mesh.dim) + ) + ).diagonal() - \frac{\delta \mathbf{u}}{\delta \mathbf{m}} = - - [ \mathbf{A} ]^{-1} - \left[ - \frac{\partial \mathbf{A}}{\partial \mathbf{m}}(\mathbf{m})\mathbf{u} - - \frac{\partial \mathbf{rhs}(\mathbf{m})}{\partial \mathbf{m}} - \right] + fields = {"b": b_field, "u": u} + self._stored_fields = fields - A little tricky point here is we are not interested in potential (u), but interested in magnetic flux (B). - Thus, we need sensitivity for B. Now we take derivative of B w.r.t m and have + else: + fields = self._stored_fields - .. math :: + return fields - \frac{\delta \mathbf{B}} {\delta \mathbf{m}} = - \frac{\partial \mathbf{\mu} } {\partial \mathbf{m} } - \left[ - \diag(\M^f_{\mu_{0}^{-1} } \mathbf{B}_0) \dMfMuI \ - - \diag (\Div^T\mathbf{u})\dMfMuI - \right ] + def dpred(self, m=None, f=None): + self.model = m + if f is not None: + return self._projectFields(f) - - (\MfMui)^{-1}\Div^T\frac{\delta\mathbf{u}}{\delta \mathbf{m}} + if f is None: + f = self.fields(m) - Finally we evaluate the above, but we should remember that + dpred = self._projectFields(f) - .. note :: + return dpred - We only want to evaluate + def magnetic_polarization(self, m=None): + r""" + Computes the total magnetic polarization :math:`\mu_0\mathbf{M}`. - .. math :: + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. - \mathbf{J}\mathbf{v} = - \frac{\delta \mathbf{P}\mathbf{B}} {\delta \mathbf{m}}\mathbf{v} + Returns + ------- + mu0_m : np.ndarray + The magnetic polarization :math:`\mu_0 \mathbf{M}` in nanoteslas (nT), defined on the mesh faces. + The result is ordered as a concatenation of the x, y, and z face components + (i.e., ``[Mx_faces, My_faces, Mz_faces]``). - Since forming sensitivity matrix is very expensive in that this - monster is "big" and "dense" matrix!! """ - if u is None: - u = self.fields(m) - - B, u = u["B"], u["u"] - mu = self.muMap * (m) - dmu_dm = self.muDeriv - # dchidmu = sdiag(1 / mu_0 * np.ones(self.mesh.nC)) - - vol = self.mesh.cell_volumes - Div = self._Div - P = self.survey.projectFieldsDeriv(B) # Projection matrix - B0 = self.getB0() - - MfMuIvec = 1 / self.MfMui.diagonal() - dMfMuI = sdiag(MfMuIvec**2) * self.mesh.aveF2CC.T * sdiag(vol * 1.0 / mu**2) - - # A = self._Div*self.MfMuI*self._Div.T - # RHS = Div*MfMuI*MfMu0*B0 - Div*B0 + Mc*Dface*Pout.T*Bbc - # C(m,u) = A*m-rhs - # dudm = -(dCdu)^(-1)dCdm - - dCdu = self.getA(m) # = A - dCdm_A = Div * (sdiag(Div.T * u) * dMfMuI * dmu_dm) - dCdm_RHS1 = Div * (sdiag(self.MfMu0 * B0) * dMfMuI) - # temp1 = (Dface * (self._Pout.T * self.Bbc_const * self.Bbc)) - # dCdm_RHS2v = (sdiag(vol) * temp1) * \ - # np.inner(vol, dchidmu * dmu_dm * v) - - # dCdm_RHSv = dCdm_RHS1*(dmu_dm*v) + dCdm_RHS2v - dCdm_RHSv = dCdm_RHS1 * (dmu_dm * v) - dCdm_v = dCdm_A * v - dCdm_RHSv - - Ainv = self.solver(dCdu, **self.solver_opts) - sol = Ainv * dCdm_v - - dudm = -sol - dBdmv = ( - sdiag(self.MfMu0 * B0) * (dMfMuI * (dmu_dm * v)) - - sdiag(Div.T * u) * (dMfMuI * (dmu_dm * v)) - - self.MfMuI * (Div.T * (dudm)) - ) + self.model = m + f = self.fields(m) + b_field, u = f["b"], f["u"] + MfMu0iI = self.mesh.get_face_inner_product(1.0 / mu_0, invert_matrix=True) - Ainv.clean() + mu0_h = -MfMu0iI * self._DivT * u + mu0_m = b_field - mu0_h - return mkvc(P * dBdmv) + return mu0_m - @utils.timeIt - def Jtvec(self, m, v, u=None): - r""" - Computing Jacobian^T multiplied by vector. + def Jvec(self, m, v, f=None): + self.model = m - .. math :: + if f is None: + f = self.fields(m) - (\frac{\delta \mathbf{P}\mathbf{B}} {\delta \mathbf{m}})^{T} = - \left[ - \mathbf{P}_{deriv}\frac{\partial \mathbf{\mu} } {\partial \mathbf{m} } - \left[ - \diag(\M^f_{\mu_{0}^{-1} } \mathbf{B}_0) \dMfMuI - - \diag (\Div^T\mathbf{u})\dMfMuI - \right ] - \right]^{T} - - - \left[ - \mathbf{P}_{deriv}(\MfMui)^{-1} \Div^T - \frac{\delta\mathbf{u}}{\delta \mathbf{m}} - \right]^{T} + if self.storeJ: + J = self.getJ(m, f=f) + return J.dot(v) - where + return self._Jvec(m, v, f) - .. math :: + def Jtvec(self, m, v, f=None): + self.model = m - \mathbf{P}_{derv} = \frac{\partial \mathbf{P}}{\partial\mathbf{B}} + if f is None: + f = self.fields(m) - .. note :: + if self.storeJ: + J = self.getJ(m, f=f) + return np.asarray(J.T.dot(v)) - Here we only want to compute + return self._Jtvec(m, v, f) - .. math :: + def getJ(self, m, f=None): + self.model = m + if self._Jmatrix: + return self._Jmatrix + if f is None: + f = self.fields(m) + if m.size < self.survey.nD: + J = self._Jvec(m, v=None, f=f) + else: + J = self._Jtvec(m, v=None, f=f).T - \mathbf{J}^{T}\mathbf{v} = - (\frac{\delta \mathbf{P}\mathbf{B}} {\delta \mathbf{m}})^{T} \mathbf{v} + if self.storeJ: + self._Jmatrix = J + return J - """ - if u is None: - u = self.fields(m) + def _Jtvec(self, m, v, f): + b_field, u = f["b"], f["u"] - B, u = u["B"], u["u"] - mu = self.mapping * (m) - dmu_dm = self.mapping.deriv(m) - # dchidmu = sdiag(1 / mu_0 * np.ones(self.mesh.nC)) + Q = self._projectFieldsDeriv(b_field) - vol = self.mesh.cell_volumes - Div = self._Div - P = self.survey.projectFieldsDeriv(B) # Projection matrix - B0 = self.getB0() + if v is None: + v = np.eye(Q.shape[0]) + divt_solve_q = ( + self._DivT * (self._Ainv * ((Q * self.MfMuiI * -self._DivT).T * v)) + + Q.T * v + ) + del v + else: + divt_solve_q = ( + self._DivT * (self._Ainv * ((-self._Div * (self.MfMuiI.T * (Q.T * v))))) + + Q.T * v + ) - MfMuIvec = 1 / self.MfMui.diagonal() - dMfMuI = sdiag(MfMuIvec**2) * self.mesh.aveF2CC.T * sdiag(vol * 1.0 / mu**2) + mu_vec = np.tile(self.mu * np.ones(self.mesh.n_cells), self.mesh.dim) - # A = self._Div*self.MfMuI*self._Div.T - # RHS = Div*MfMuI*MfMu0*B0 - Div*B0 + Mc*Dface*Pout.T*Bbc - # C(m,u) = A*m-rhs - # dudm = -(dCdu)^(-1)dCdm + Jtv = 0 - dCdu = self.getA(m) - s = Div * (self.MfMuI.T * (P.T * v)) + if self.remMap is not None: + Mf_rem_deriv = self._Mf_vec_deriv * sp.diags(1 / mu_vec) * self.remDeriv + Jtv += (self.MfMuiI * Mf_rem_deriv).T * (divt_solve_q) - Ainv = self.solver(dCdu.T, **self.solver_opts) - sol = Ainv * s + if self.muMap is not None: + Jtv += self.MfMuiIDeriv(self._DivT * u, -divt_solve_q, adjoint=True) + Jtv += self.MfMuiIDeriv( + self._b0, self._MfMu0i.T * (divt_solve_q), adjoint=True + ) - Ainv.clean() + if self.rem is not None: + Mf_r_mui = self.mesh.get_face_inner_product( + self.rem / mu_vec + ).diagonal() + mu_vec_i_deriv = sp.vstack( + (self.muiDeriv, self.muiDeriv, self.muiDeriv) + ) - # dCdm_A = Div * ( sdiag( Div.T * u )* dMfMuI *dmu_dm ) - # dCdm_Atsol = ( dMfMuI.T*( sdiag( Div.T * u ) * (Div.T * dmu_dm)) ) * sol - dCdm_Atsol = (dmu_dm.T * dMfMuI.T * (sdiag(Div.T * u) * Div.T)) * sol + Mf_r_mui_deriv = ( + self._Mf_vec_deriv * sp.diags(self.rem) * mu_vec_i_deriv + ) - # dCdm_RHS1 = Div * (sdiag( self.MfMu0*B0 ) * dMfMuI) - # dCdm_RHS1tsol = (dMfMuI.T*( sdiag( self.MfMu0*B0 ) ) * Div.T * dmu_dm) * sol - dCdm_RHS1tsol = (dmu_dm.T * dMfMuI.T * (sdiag(self.MfMu0 * B0)) * Div.T) * sol + Jtv += ( + self.MfMuiIDeriv(Mf_r_mui, divt_solve_q, adjoint=True) + + (Mf_r_mui_deriv.T * self.MfMuiI.T) * divt_solve_q + ) - # temp1 = (Dface*(self._Pout.T*self.Bbc_const*self.Bbc)) - # temp1sol = (Dface.T * (sdiag(vol) * sol)) - # temp2 = self.Bbc_const * (self._Pout.T * self.Bbc).T - # dCdm_RHS2v = (sdiag(vol)*temp1)*np.inner(vol, dchidmu*dmu_dm*v) - # dCdm_RHS2tsol = (dmu_dm.T * dchidmu.T * vol) * np.inner(temp2, temp1sol) + return Jtv - # dCdm_RHSv = dCdm_RHS1*(dmu_dm*v) + dCdm_RHS2v + def _Jvec(self, m, v, f): - # temporary fix - # dCdm_RHStsol = dCdm_RHS1tsol - dCdm_RHS2tsol - dCdm_RHStsol = dCdm_RHS1tsol + if v is None: + v = np.eye(m.shape[0]) - # dCdm_RHSv = dCdm_RHS1*(dmu_dm*v) + dCdm_RHS2v - # dCdm_v = dCdm_A*v - dCdm_RHSv + b_field, u = f["b"], f["u"] - Ctv = dCdm_Atsol - dCdm_RHStsol + Q = self._projectFieldsDeriv(b_field) + C = -self.MfMuiI * self._DivT - # B = self.MfMuI*self.MfMu0*B0-B0-self.MfMuI*self._Div.T*u - # dBdm = d\mudm*dBd\mu - # dPBdm^T*v = Atemp^T*P^T*v - Btemp^T*P^T*v - Ctv + db_dm = 0 + dCmu_dm = 0 - Atemp = sdiag(self.MfMu0 * B0) * (dMfMuI * (dmu_dm)) - Btemp = sdiag(Div.T * u) * (dMfMuI * (dmu_dm)) - Jtv = Atemp.T * (P.T * v) - Btemp.T * (P.T * v) - Ctv + mu_vec = np.tile(self.mu * np.ones(self.mesh.n_cells), self.mesh.dim) - return mkvc(Jtv) + if self.remMap is not None: + Mf_rem_deriv = self._Mf_vec_deriv * sp.diags(1 / mu_vec) * self.remDeriv + db_dm += self.MfMuiI * Mf_rem_deriv * v - @property - def Qfx(self): - if getattr(self, "_Qfx", None) is None: - self._Qfx = self.mesh.get_interpolation_matrix( - self.survey.receiver_locations, "Fx" - ) - return self._Qfx + if self.muMap is not None: + dCmu_dm += self.MfMuiIDeriv(self._DivT @ u, v, adjoint=False) + db_dm += self._MfMu0i * self.MfMuiIDeriv(self._b0, v, adjoint=False) - @property - def Qfy(self): - if getattr(self, "_Qfy", None) is None: - self._Qfy = self.mesh.get_interpolation_matrix( - self.survey.receiver_locations, "Fy" - ) - return self._Qfy + if self.rem is not None: + Mf_r_mui = self.mesh.get_face_inner_product( + self.rem / mu_vec + ).diagonal() + mu_vec_i_deriv = sp.vstack( + (self.muiDeriv, self.muiDeriv, self.muiDeriv) + ) + Mf_r_mui_deriv = ( + self._Mf_vec_deriv * sp.diags(self.rem) * mu_vec_i_deriv + ) + db_dm += self.MfMuiIDeriv(Mf_r_mui, v, adjoint=False) + ( + self.MfMuiI * Mf_r_mui_deriv * v + ) - @property - def Qfz(self): - if getattr(self, "_Qfz", None) is None: - self._Qfz = self.mesh.get_interpolation_matrix( - self.survey.receiver_locations, "Fz" - ) - return self._Qfz + Ainv_Ddm = self._Ainv * (self._Div * (-dCmu_dm + db_dm)) - def projectFields(self, u): - r""" - This function projects the fields onto the data space. - Especially, here for we use total magnetic intensity (TMI) data, - which is common in practice. - First we project our B on to data location + Jv = Q * (C * Ainv_Ddm + (-dCmu_dm + db_dm)) - .. math:: + return Jv - \mathbf{B}_{rec} = \mathbf{P} \mathbf{B} + @cached_property + def _Qfx(self): + Qfx = self.mesh.get_interpolation_matrix(self.survey.receiver_locations, "Fx") + return Qfx - then we take the dot product between B and b_0 + @cached_property + def _Qfy(self): + Qfy = self.mesh.get_interpolation_matrix(self.survey.receiver_locations, "Fy") + return Qfy - .. math :: + @cached_property + def _Qfz(self): + Qfz = self.mesh.get_interpolation_matrix(self.survey.receiver_locations, "Fz") + return Qfz - \text{TMI} = \vec{B}_s \cdot \hat{B}_0 + def _projectFields(self, f): - """ - # Get components for all receivers, assuming they all have the same components - components = self._get_components() + rx_list = self.survey.source_field.receiver_list + components = [] + for rx in rx_list: + components.extend(rx.components) + components = set(components) - fields = {} if "bx" in components or "tmi" in components: - fields["bx"] = self.Qfx * u["B"] + bx = self._Qfx * f["b"] if "by" in components or "tmi" in components: - fields["by"] = self.Qfy * u["B"] + by = self._Qfy * f["b"] if "bz" in components or "tmi" in components: - fields["bz"] = self.Qfz * u["B"] + bz = self._Qfz * f["b"] if "tmi" in components: - bx = fields["bx"] - by = fields["by"] - bz = fields["bz"] - # Generate unit vector - B0 = self.survey.source_field.b0 - Bot = np.sqrt(B0[0] ** 2 + B0[1] ** 2 + B0[2] ** 2) - box = B0[0] / Bot - boy = B0[1] / Bot - boz = B0[2] / Bot - fields["tmi"] = bx * box + by * boy + bz * boz - - return np.concatenate([fields[comp] for comp in components]) - - @utils.count - def projectFieldsDeriv(self, B): - r""" - This function projects the fields onto the data space. - - .. math:: + b0 = self.survey.source_field.b0 + tmi = np.sqrt( + (bx + b0[0]) ** 2 + (by + b0[1]) ** 2 + (bz + b0[2]) ** 2 + ) - np.sqrt(b0[0] ** 2 + b0[1] ** 2 + b0[2] ** 2) + + n_total = 0 + total_data_list = [] + for rx in rx_list: + data = {} + rx_n_locs = rx.locations.shape[0] + if "bx" in rx.components: + data["bx"] = bx[n_total : n_total + rx_n_locs] + if "by" in rx.components: + data["by"] = by[n_total : n_total + rx_n_locs] + if "bz" in rx.components: + data["bz"] = bz[n_total : n_total + rx_n_locs] + if "tmi" in rx.components: + data["tmi"] = tmi[n_total : n_total + rx_n_locs] + + n_total += rx_n_locs + + total_data_list.append( + np.concatenate([data[comp] for comp in rx.components]) + ) - \frac{\partial d_\text{pred}}{\partial \mathbf{B}} = \mathbf{P} + if len(total_data_list) == 1: + return total_data_list[0] - Especially, this function is for TMI data type - """ - # Get components for all receivers, assuming they all have the same components - components = self._get_components() + return np.concatenate(total_data_list, axis=0) - fields = {} - if "bx" in components or "tmi" in components: - fields["bx"] = self.Qfx - if "by" in components or "tmi" in components: - fields["by"] = self.Qfy - if "bz" in components or "tmi" in components: - fields["bz"] = self.Qfz + @utils.count + def _projectFieldsDeriv(self, bs): + rx_list = self.survey.source_field.receiver_list + components = [] + for rx in rx_list: + components.extend(rx.components) + components = set(components) if "tmi" in components: - bx = fields["bx"] - by = fields["by"] - bz = fields["bz"] - # Generate unit vector - B0 = self.survey.source_field.b0 - Bot = np.sqrt(B0[0] ** 2 + B0[1] ** 2 + B0[2] ** 2) - box = B0[0] / Bot - boy = B0[1] / Bot - boz = B0[2] / Bot - fields["tmi"] = bx * box + by * boy + bz * boz - - return sp.vstack([fields[comp] for comp in components]) - - def _get_components(self): - """ - Get components of all receivers in the survey. + b0 = self.survey.source_field.b0 + bot = np.sqrt(b0[0] ** 2 + b0[1] ** 2 + b0[2] ** 2) - This function assumes that all receivers in the survey have the same - components in the same order. + bx = self._Qfx * bs + by = self._Qfy * bs + bz = self._Qfz * bs - Returns - ------- - components : list of str - List of components shared by all receivers in the survey. - - Raises - ------ - ValueError - If the survey doesn't have any receiver, or if any receiver has - a different set of components than the rest. - """ - # Validate survey first to ensure that the receivers have all the same - # components. - self._validate_survey(self.survey) - components = self.survey.source_field.receiver_list[0].components - return components - - def _validate_survey(self, survey): - """ - Validate a survey for the magnetic differential 3D simulation. - - Parameters - ---------- - survey : Survey - Survey object that will get validated. - - Raises - ------ - ValueError - If the survey doesn't have any receiver, or if any receiver has - a different set of components than the rest. - """ - receivers = survey.source_field.receiver_list - if not receivers: - msg = "Found invalid survey without receivers." - raise ValueError(msg) - components = receivers[0].components - if not all(components == rx.components for rx in receivers): - msg = ( - "Found invalid survey with receivers that have mixed components. " - f"Surveys for the {type(self).__name__} class must contain receivers " - "with the same components." + dpred = ( + np.sqrt((bx + b0[0]) ** 2 + (by + b0[1]) ** 2 + (bz + b0[2]) ** 2) - bot ) - raise ValueError(msg) - def projectFieldsAsVector(self, B): - bfx = self.Qfx * B - bfy = self.Qfy * B - bfz = self.Qfz * B + dDhalf_dD = sdiag(1 / (dpred + bot)) - return np.r_[bfx, bfy, bfz] + xterm = sdiag(b0[0] + bx) * self._Qfx + yterm = sdiag(b0[1] + by) * self._Qfy + zterm = sdiag(b0[2] + bz) * self._Qfz + Qtmi = dDhalf_dD * (xterm + yterm + zterm) -def MagneticsDiffSecondaryInv(mesh, model, data, **kwargs): - """ - Inversion module for MagneticsDiffSecondary + n_total = 0 + total_data_list = [] + for rx in rx_list: + data = {} + rx_n_locs = rx.locations.shape[0] + if "bx" in rx.components: + data["bx"] = self._Qfx[n_total : n_total + rx_n_locs][:] + if "by" in rx.components: + data["by"] = self._Qfy[n_total : n_total + rx_n_locs][:] + if "bz" in rx.components: + data["bz"] = self._Qfz[n_total : n_total + rx_n_locs][:] + if "tmi" in rx.components: + data["tmi"] = Qtmi[n_total : n_total + rx_n_locs][:] - """ - from simpeg import ( - directives, - inversion, - objective_function, - optimization, - regularization, - ) + n_total += rx_n_locs - prob = Simulation3DDifferential(mesh, survey=data, mu=model) + total_data_list.append(sp.vstack([data[comp] for comp in rx.components])) - miter = kwargs.get("maxIter", 10) + if len(total_data_list) == 1: + return total_data_list[0] - # Create an optimization program - opt = optimization.InexactGaussNewton(maxIter=miter) - opt.bfgsH0 = get_default_solver()(sp.identity(model.nP), flag="D") - # Create a regularization program - reg = regularization.WeightedLeastSquares(model) - # Create an objective function - beta = directives.BetaSchedule(beta0=1e0) - obj = objective_function.BaseObjFunction(prob, reg, beta=beta) - # Create an inversion object - inv = inversion.BaseInversion(obj, opt) + return sp.vstack(total_data_list) - return inv, reg + @property + def _delete_on_model_update(self): + toDelete = super()._delete_on_model_update + if self._stored_fields is not None: + toDelete = toDelete + ["_stored_fields"] + if self.muMap is not None: + if self._Ainv is not None: + toDelete = toDelete + ["_Ainv"] + if self._Jmatrix is not None: + toDelete = toDelete + ["_Jmatrix"] + return toDelete diff --git a/tests/base/test_maps.py b/tests/base/test_maps.py index 8cb99803fb..966ef8a819 100644 --- a/tests/base/test_maps.py +++ b/tests/base/test_maps.py @@ -32,6 +32,7 @@ MAPS_TO_EXCLUDE_2D = [ "ComboMap", "ActiveCells", + "EffectiveSusceptibilityMap", "InjectActiveCells", "LogMap", "LinearMap", @@ -59,6 +60,7 @@ MAPS_TO_EXCLUDE_3D = [ "ComboMap", "ActiveCells", + "EffectiveSusceptibilityMap", "InjectActiveCells", "LogMap", "LinearMap", @@ -213,6 +215,10 @@ def test_transforms_logMap_reciprocalMap(self): mapping = maps.ReciprocalMap(self.mesh3) self.assertTrue(mapping.test(random_seed=42)) + def test_EffectiveSusceptibilityMap(self): + mapping = maps.EffectiveSusceptibilityMap(50000.0, mesh=self.mesh3) + self.assertTrue(mapping.test(random_seed=42)) + def test_Mesh2MeshMap(self): mapping = maps.Mesh2Mesh([self.mesh22, self.mesh2]) self.assertTrue(mapping.test(random_seed=42)) diff --git a/tests/pf/test_components.py b/tests/pf/test_components.py index 569a33eba2..30c66202ee 100644 --- a/tests/pf/test_components.py +++ b/tests/pf/test_components.py @@ -7,7 +7,6 @@ import numpy as np import discretize -from simpeg import maps from simpeg.potential_fields import gravity, magnetics @@ -54,139 +53,3 @@ def test_deprecated_components(self, receiver_locations): msg = re.escape("The `components` property is deprecated") with pytest.warns(FutureWarning, match=msg): survey.components - - -class TestMagneticSimulationDifferential: - - def build_survey(self, receivers: list | None): - """ - Build a sample survey. - """ - source_field = magnetics.sources.UniformBackgroundField( - receiver_list=receivers, amplitude=55_000, inclination=12, declination=35 - ) - survey = magnetics.survey.Survey(source_field) - return survey - - @pytest.fixture - def sample_simulation(self, mesh, receiver_locations): - """ - Build a sample simulation with single receiver with "tmi". - """ - receivers = [magnetics.receivers.Point(receiver_locations, components="tmi")] - source_field = magnetics.sources.UniformBackgroundField( - receiver_list=receivers, amplitude=55_000, inclination=12, declination=35 - ) - survey = magnetics.survey.Survey(source_field) - simulation = magnetics.Simulation3DDifferential( - mesh, survey=survey, muMap=maps.IdentityMap(mesh=mesh) - ) - return simulation - - def test_survey_setter(self, receiver_locations, sample_simulation): - """ - Test ``survey`` setter with valid receivers. - """ - receivers = [magnetics.receivers.Point(receiver_locations, components="tmi")] - survey = self.build_survey(receivers) - # Try to override the survey, should pass wo errors - sample_simulation.survey = survey - - @pytest.mark.parametrize("invalid_rx", ["no-rx", "different-components"]) - def test_survey_setter_invalid( - self, receiver_locations, sample_simulation, invalid_rx - ): - """ - Test ``survey`` setter with invalid receivers. - """ - if invalid_rx == "no-rx": - receivers = [] - msg = re.escape("Found invalid survey without receivers.") - else: - receivers = [ - magnetics.receivers.Point(receiver_locations, components=c) - for c in ("tmi", ["bx", "by"]) - ] - msg = re.escape( - "Found invalid survey with receivers that have mixed components." - ) - # Try to override the survey - survey = self.build_survey(receivers) - with pytest.raises(ValueError, match=msg): - sample_simulation.survey = survey - - @pytest.mark.parametrize("components", ["tmi", ["bx", "by", "bz"]]) - def test_get_components(self, mesh, receiver_locations, components): - """ - Test the ``_get_components`` method with valid receivers. - """ - receivers = [ - magnetics.receivers.Point(receiver_locations, components=components), - magnetics.receivers.Point(receiver_locations, components=components), - ] - survey = self.build_survey(receivers) - simulation = magnetics.Simulation3DDifferential( - mesh, survey=survey, muMap=maps.IdentityMap(mesh=mesh) - ) - - expected = components if isinstance(components, list) else [components] - assert expected == simulation._get_components() - - @pytest.mark.parametrize("invalid_rx", ["no-rx", "different-components"]) - def test_get_components_invalid( - self, sample_simulation, receiver_locations, invalid_rx - ): - """ - Test the ``_get_components`` with invalid receivers. - """ - # Override receivers in simulation's survey - if invalid_rx == "no-rx": - receivers = [] - msg = re.escape("Found invalid survey without receivers.") - else: - receivers = [ - magnetics.receivers.Point(receiver_locations, components=c) - for c in ("tmi", ["bx", "by"]) - ] - msg = re.escape( - "Found invalid survey with receivers that have mixed components." - ) - # Override private attribute `_receiver_list` to bypass the setter - sample_simulation.survey.source_field._receiver_list = receivers - - # Try to get components - with pytest.raises(ValueError, match=msg): - sample_simulation._get_components() - - @pytest.mark.parametrize("invalid_rx", ["no-rx", "different-components"]) - @pytest.mark.parametrize("method", ["projectFields", "projectFieldsDeriv"]) - def test_project_fields_invalid( - self, sample_simulation, receiver_locations, invalid_rx, method - ): - """ - Test ``projectFields`` and ``projectFieldsDeriv`` on invalid surveys. - """ - # Override receivers in simulation's survey - if invalid_rx == "no-rx": - receivers = None - msg = re.escape("Found invalid survey without receivers.") - else: - receivers = [ - magnetics.receivers.Point(receiver_locations, components=c) - for c in ("tmi", ["bx", "by", "bz"]) - ] - msg = re.escape( - "Found invalid survey with receivers that have mixed components." - ) - # Override private attribute `_receiver_list` to bypass the setter - sample_simulation.survey.source_field._receiver_list = receivers - - # Compute fields from a random model - n_cells = sample_simulation.mesh.n_cells - model = np.random.default_rng(seed=42).uniform(size=n_cells) - fields = sample_simulation.fields(model) - - # Test errors - method = getattr(sample_simulation, method) - with pytest.raises(ValueError, match=msg): - method(fields) diff --git a/tests/pf/test_forward_PFproblem.py b/tests/pf/test_forward_PFproblem.py deleted file mode 100644 index fa2cb3fe2e..0000000000 --- a/tests/pf/test_forward_PFproblem.py +++ /dev/null @@ -1,86 +0,0 @@ -import unittest -import discretize -from simpeg import utils, maps -from simpeg.utils.model_builder import get_indices_sphere -from simpeg.potential_fields import magnetics as mag -import numpy as np - - -class MagFwdProblemTests(unittest.TestCase): - def setUp(self): - Inc = 45.0 - Dec = 45.0 - Btot = 51000 - - self.b0 = mag.analytics.IDTtoxyz(-Inc, Dec, Btot) - - cs = 25.0 - hxind = [(cs, 5, -1.3), (cs / 2.0, 41), (cs, 5, 1.3)] - hyind = [(cs, 5, -1.3), (cs / 2.0, 41), (cs, 5, 1.3)] - hzind = [(cs, 5, -1.3), (cs / 2.0, 40), (cs, 5, 1.3)] - M = discretize.TensorMesh([hxind, hyind, hzind], "CCC") - - chibkg = 0.0 - self.chiblk = 0.01 - chi = np.ones(M.nC) * chibkg - - self.rad = 100 - self.sphere_center = [0.0, 0.0, 0.0] - sph_ind = get_indices_sphere(self.sphere_center, self.rad, M.gridCC) - chi[sph_ind] = self.chiblk - - xr = np.linspace(-300, 300, 41) - yr = np.linspace(-300, 300, 41) - X, Y = np.meshgrid(xr, yr) - Z = np.ones((xr.size, yr.size)) * 150 - self.components = ["bx", "by", "bz"] - self.xr = xr - self.yr = yr - self.rxLoc = np.c_[utils.mkvc(X), utils.mkvc(Y), utils.mkvc(Z)] - receivers = mag.Point(self.rxLoc, components=self.components) - srcField = mag.UniformBackgroundField( - receiver_list=[receivers], - amplitude=Btot, - inclination=Inc, - declination=Dec, - ) - - self.survey = mag.Survey(srcField) - - self.sim = mag.simulation.Simulation3DDifferential( - M, - survey=self.survey, - muMap=maps.ChiMap(M), - ) - self.M = M - self.chi = chi - - def test_ana_forward(self): - u = self.sim.fields(self.chi) - dpred = self.sim.projectFields(u) - - bxa, bya, bza = mag.analytics.MagSphereAnaFunA( - self.rxLoc[:, 0], - self.rxLoc[:, 1], - self.rxLoc[:, 2], - self.rad, - *self.sphere_center, - self.chiblk, - self.b0, - "secondary", - ) - - n_obs, n_comp = self.rxLoc.shape[0], len(self.components) - dx, dy, dz = dpred.reshape(n_comp, n_obs) - - err_x = np.linalg.norm(dx - bxa) / np.linalg.norm(bxa) - err_y = np.linalg.norm(dy - bya) / np.linalg.norm(bya) - err_z = np.linalg.norm(dz - bza) / np.linalg.norm(bza) - - self.assertLess(err_x, 0.08) - self.assertLess(err_y, 0.08) - self.assertLess(err_z, 0.08) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/pf/test_forward_mag_differential.py b/tests/pf/test_forward_mag_differential.py new file mode 100644 index 0000000000..f6571073fe --- /dev/null +++ b/tests/pf/test_forward_mag_differential.py @@ -0,0 +1,414 @@ +import re +import pytest +import discretize +import simpeg.potential_fields as PF +from simpeg import utils, maps +from discretize.utils import mkvc, refine_tree_xyz +import numpy as np +from tests.utils.ellipsoid import ProlateEllipsoid + + +@pytest.fixture +def mesh(): + + dhx, dhy, dhz = 50.0, 50.0, 50.0 # minimum cell width (base mesh cell width) + nbcx = 512 # number of base mesh cells in x + nbcy = 512 + nbcz = 512 + + # Define base mesh (domain and finest discretization) + hx = dhx * np.ones(nbcx) + hy = dhy * np.ones(nbcy) + hz = dhz * np.ones(nbcz) + _mesh = discretize.TreeMesh([hx, hy, hz], x0="CCC") + + xp, yp, zp = np.meshgrid([-1400.0, 1400.0], [-1400.0, 1400.0], [-1000.0, 200.0]) + xy = np.c_[mkvc(xp), mkvc(yp), mkvc(zp)] + _mesh = refine_tree_xyz( + _mesh, + xy, + method="box", + finalize=False, + octree_levels=[1, 1, 1, 1], + ) + _mesh.finalize() + + return _mesh + + +def get_survey(components=("bx", "by", "bz")): + ccx = np.linspace(-1400, 1400, num=57) + ccy = np.linspace(-1400, 1400, num=57) + ccx, ccy = np.meshgrid(ccx, ccy) + ccz = 50.0 * np.ones_like(ccx) + rxLoc = PF.magnetics.receivers.Point( + np.c_[utils.mkvc(ccy.T), utils.mkvc(ccx.T), utils.mkvc(ccz.T)], + components=components, + ) + inducing_field = [55000.0, 60.0, 90.0] + srcField = PF.magnetics.sources.UniformBackgroundField( + [rxLoc], inducing_field[0], inducing_field[1], inducing_field[2] + ) + _survey = PF.magnetics.survey.Survey(srcField) + + return _survey + + +@pytest.mark.parametrize("model_type", ("mu_rem", "mu", "rem")) +def test_forward(model_type, mesh): + """ + Test against the analytic solution for an ellipse with + uniform intrinsic remanence and susceptibility in a + uniform ambient geomagnetic field + """ + tol = 0.1 + + survey = get_survey() + + amplitude = survey.source_field.amplitude + inclination = survey.source_field.inclination + declination = survey.source_field.declination + inducing_field = [amplitude, inclination, declination] + + if model_type == "mu_rem": + susceptibility = 5 + MrX = 150000 + MrY = 150000 + MrZ = 150000 + if model_type == "mu": + susceptibility = 5 + MrX = 0 + MrY = 0 + MrZ = 0 + if model_type == "rem": + susceptibility = 0 + MrX = 150000 + MrY = 150000 + MrZ = 150000 + + center = np.array([00, 0, -400.0]) + axes = [600.0, 200.0] + strike_dip_rake = [0, 0, 90] + + ellipsoid = ProlateEllipsoid( + center, + axes, + strike_dip_rake, + susceptibility=susceptibility, + Mr=(MrX, MrY, MrZ), + inducing_field=inducing_field, + ) + ind_ellipsoid = ellipsoid.get_indices(mesh.cell_centers) + + sus_model = np.zeros(mesh.n_cells) + sus_model[ind_ellipsoid] = susceptibility + mu_model = maps.ChiMap() * sus_model + + Rx = np.zeros(mesh.n_cells) + Ry = np.zeros(mesh.n_cells) + Rz = np.zeros(mesh.n_cells) + + Rx[ind_ellipsoid] = MrX + Ry[ind_ellipsoid] = MrY + Rz[ind_ellipsoid] = MrZ + + u0_Mr_model = mkvc(np.array([Rx, Ry, Rz]).T) + + if model_type == "mu": + u0_Mr_model = None + if model_type == "rem": + mu_model = None + + simulation = PF.magnetics.simulation.Simulation3DDifferential( + survey=survey, mesh=mesh, mu=mu_model, rem=u0_Mr_model, solver_dtype=np.float32 + ) + + dpred_numeric = simulation.dpred() + dpred_analytic = mkvc(ellipsoid.anomalous_bfield(survey.receiver_locations)) + + assert np.allclose( + dpred_numeric, + dpred_analytic, + rtol=0.1, + atol=0.05 * np.max(np.abs(dpred_analytic)), + ) + + err = np.linalg.norm(dpred_numeric - dpred_analytic) / np.linalg.norm( + dpred_analytic + ) + + print( + "\n||dpred_analytic-dpred_numeric||/||dpred_analytic|| = " + + "{:.{}f}".format(err, 2) + + ", tol = " + + str(tol) + ) + + assert err < tol + + u0_M_analytic = ellipsoid.Magnetization() + u0_M_numeric = mesh.average_face_to_cell_vector * simulation.magnetic_polarization() + u0_M_numeric = u0_M_numeric.reshape((mesh.n_cells, 3), order="F") + u0_M_numeric = np.mean(u0_M_numeric[ind_ellipsoid, :], axis=0) + + assert np.allclose( + u0_M_numeric, + u0_M_analytic, + rtol=0.1, + atol=0.01 * np.max(np.abs(u0_M_analytic)), + ) + + +def test_exact_tmi(mesh): + """ + Test against the analytic solution for an ellipse with + uniform intrinsic remanence and susceptibility in a + uniform ambient geomagnetic field + """ + tol = 1e-8 + + survey = get_survey(components=["bx", "by", "bz", "tmi"]) + + amplitude = survey.source_field.amplitude + inclination = survey.source_field.inclination + declination = survey.source_field.declination + inducing_field = [amplitude, inclination, declination] + + susceptibility = 5 + MrX = 150000 + MrY = 150000 + MrZ = 150000 + + center = np.array([00, 0, -400.0]) + axes = [600.0, 200.0] + strike_dip_rake = [0, 0, 90] + + ellipsoid = ProlateEllipsoid( + center, + axes, + strike_dip_rake, + susceptibility=susceptibility, + Mr=(MrX, MrY, MrZ), + inducing_field=inducing_field, + ) + ind_ellipsoid = ellipsoid.get_indices(mesh.cell_centers) + + sus_model = np.zeros(mesh.n_cells) + sus_model[ind_ellipsoid] = susceptibility + mu_model = maps.ChiMap() * sus_model + + Rx = np.zeros(mesh.n_cells) + Ry = np.zeros(mesh.n_cells) + Rz = np.zeros(mesh.n_cells) + + Rx[ind_ellipsoid] = MrX + Ry[ind_ellipsoid] = MrY + Rz[ind_ellipsoid] = MrZ + + u0_Mr_model = mkvc(np.array([Rx, Ry, Rz]).T) + + simulation = PF.magnetics.simulation.Simulation3DDifferential( + survey=survey, + mesh=mesh, + mu=mu_model, + rem=u0_Mr_model, + ) + + dpred_numeric = simulation.dpred() + + dpred_fields = np.reshape(dpred_numeric[: survey.nRx * 4], (4, survey.nRx)).T + + B0 = survey.source_field.b0 + + TMI_exact_analytic = np.linalg.norm(dpred_fields[:, :3] + B0, axis=1) - amplitude + dpred_TMI_exact = dpred_fields[:, 3] + + TMI_exact_err = np.max(np.abs(dpred_TMI_exact - TMI_exact_analytic)) + + assert TMI_exact_err < tol + print( + "max(TMI_exact_err) = " + + "{:.{}e}".format(TMI_exact_err, 2) + + ", tol = " + + str(tol) + ) + + +def test_differential_magnetization_against_integral(mesh): + + survey = get_survey() + + amplitude = survey.source_field.amplitude + inclination = survey.source_field.inclination + declination = survey.source_field.declination + inducing_field = [amplitude, inclination, declination] + + MrX = 150000 + MrY = 150000 + MrZ = 150000 + + center = np.array([00, 0, -400.0]) + axes = [600.0, 200.0] + strike_dip_rake = [0, 0, 90] + + ellipsoid = ProlateEllipsoid( + center, + axes, + strike_dip_rake, + Mr=np.array([MrX, MrY, MrZ]), + inducing_field=inducing_field, + ) + ind_ellipsoid = ellipsoid.get_indices(mesh.cell_centers) + + Rx = np.zeros(mesh.n_cells) + Ry = np.zeros(mesh.n_cells) + Rz = np.zeros(mesh.n_cells) + + Rx[ind_ellipsoid] = MrX + Ry[ind_ellipsoid] = MrY + Rz[ind_ellipsoid] = MrZ + + u0_Mr_model = mkvc(np.array([Rx, Ry, Rz]).T) + eff_sus_model = (u0_Mr_model / amplitude)[ + np.hstack((ind_ellipsoid, ind_ellipsoid, ind_ellipsoid)) + ] + + simulation_differential = PF.magnetics.simulation.Simulation3DDifferential( + survey=survey, + mesh=mesh, + rem=u0_Mr_model, + ) + + simulation_integral = PF.magnetics.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + chi=eff_sus_model, + model_type="vector", + store_sensitivities="forward_only", + active_cells=ind_ellipsoid, + ) + + dpred_numeric_differential = simulation_differential.dpred() + dni = simulation_integral.dpred() + dpred_numeric_integral = np.hstack((dni[0::3], dni[1::3], dni[2::3])) + dpred_analytic = mkvc(ellipsoid.anomalous_bfield(survey.receiver_locations)) + + diff_numeric = np.linalg.norm( + dpred_numeric_differential - dpred_numeric_integral + ) / np.linalg.norm(dpred_numeric_integral) + diff_differential = np.linalg.norm( + dpred_numeric_differential - dpred_analytic + ) / np.linalg.norm(dpred_analytic) + diff_integral = np.linalg.norm( + dpred_numeric_integral - dpred_analytic + ) / np.linalg.norm(dpred_analytic) + + # Check both discretized solutions are closer to each other than to the analytic + assert diff_numeric < diff_differential + assert diff_numeric < diff_integral + + print( + "\n||dpred_integral-dpred_pde||/||dpred_integral|| = " + + "{:.{}f}".format(diff_numeric, 2) + ) + print( + "||dpred_integral-dpred_analytic||/||dpred_analytic|| = " + + "{:.{}f}".format(diff_integral, 2) + ) + print( + "||dpred_pde-dpred_analytic||/||dpred_analytic|| = " + + "{:.{}f}".format(diff_differential, 2) + ) + + +def test_invalid_solver_dtype(mesh): + """ + Test error upon invalid `solver_dtype`. + """ + survey = get_survey() + invalid_dtype = np.int64 + msg = re.escape( + f"Invalid `solver_dtype` '{invalid_dtype}'. " + "It must be np.float32 or np.float64." + ) + with pytest.raises(ValueError, match=msg): + PF.magnetics.simulation.Simulation3DDifferential( + survey=survey, mesh=mesh, solver_dtype=invalid_dtype + ) + + +@pytest.mark.parametrize( + "components", + ["bx", "by", "bz", "tmi", ["bx", "by", "bz"]], + ids=["bx", "by", "bz", "tmi", "b_field"], +) +class TestGetJ: + + @pytest.fixture + def mesh_small(self): + """ + Define a small mesh that would generate a J matrix small enough to fit in memory + """ + h = [(10.0, 8)] + mesh = discretize.TreeMesh([h, h, h], x0="CCC", diagonal_balance=True) + mesh.refine_points((0, 0, 0), level=-1) + mesh.finalize() + return mesh + + @pytest.fixture + def survey_small(self, components): + """ + Define a small survey. + """ + x = np.linspace(-20, 20, 11) + x, y = tuple(c.ravel() for c in np.meshgrid(x, x)) + z = np.ones_like(x) + locations = np.vstack((x, y, z)).T + receiver = PF.magnetics.receivers.Point( + locations, + components=components, + ) + inducing_field = (55_000, -71, 12) + source = PF.magnetics.sources.UniformBackgroundField( + [receiver], *inducing_field + ) + survey = PF.magnetics.survey.Survey(source) + return survey + + def test_getJ_vs_Jvec(self, mesh_small, survey_small): + """ + Test the getJ method against Jvec. + """ + rng = np.random.default_rng(seed=41) + model = rng.uniform(0, 1e-1, size=mesh_small.n_cells) + mapping = maps.IdentityMap(nP=model.size) + simulation = PF.magnetics.simulation.Simulation3DDifferential( + survey=survey_small, + mesh=mesh_small, + muMap=mapping, + storeJ=False, # explicitly not storing J + ) + + vector = rng.uniform(0, 1e-1, size=mesh_small.n_cells) + result = simulation.getJ(model) @ vector + expected = simulation.Jvec(model, vector) + np.testing.assert_allclose(result, expected) + + def test_getJ_vs_Jtvec(self, mesh_small, survey_small): + """ + Test the getJ method against Jtvec. + """ + rng = np.random.default_rng(seed=41) + model = rng.uniform(0, 1e-1, size=mesh_small.n_cells) + mapping = maps.IdentityMap(nP=model.size) + simulation = PF.magnetics.simulation.Simulation3DDifferential( + survey=survey_small, + mesh=mesh_small, + muMap=mapping, + storeJ=False, # explicitly not storing J + ) + + vector = rng.uniform(0, 1e-1, size=survey_small.nD) + result = simulation.getJ(model).T @ vector + expected = simulation.Jtvec(model, vector) + np.testing.assert_allclose(result, expected) diff --git a/tests/pf/test_mag_differential_functionality.py b/tests/pf/test_mag_differential_functionality.py new file mode 100644 index 0000000000..f682b39fe6 --- /dev/null +++ b/tests/pf/test_mag_differential_functionality.py @@ -0,0 +1,177 @@ +import pytest +import discretize +import simpeg.potential_fields as PF +from simpeg import utils, maps +from discretize.utils import mkvc, refine_tree_xyz +import numpy as np +from tests.utils.ellipsoid import ProlateEllipsoid + + +@pytest.fixture +def mesh(): + + dhx, dhy, dhz = 75.0, 75.0, 75.0 # minimum cell width (base mesh cell width) + nbcx = 512 # number of base mesh cells in x + nbcy = 512 + nbcz = 512 + + # Define base mesh (domain and finest discretization) + hx = dhx * np.ones(nbcx) + hy = dhy * np.ones(nbcy) + hz = dhz * np.ones(nbcz) + _mesh = discretize.TreeMesh([hx, hy, hz], x0="CCC") + + xp, yp, zp = np.meshgrid([-1400.0, 1400.0], [-1400.0, 1400.0], [-1000.0, 200.0]) + xy = np.c_[mkvc(xp), mkvc(yp), mkvc(zp)] + _mesh = refine_tree_xyz( + _mesh, + xy, + method="box", + finalize=False, + octree_levels=[1, 1, 1, 1], + ) + _mesh.finalize() + + return _mesh + + +def test_recievers(mesh): + """ + Test that multiple point recievers with different components work. + """ + + ccx = np.linspace(-1400, 1400, num=57) + ccy = np.linspace(-1400, 1400, num=57) + ccx, ccy = np.meshgrid(ccx, ccy) + ccz = 50.0 * np.ones_like(ccx) + components_1 = ["bx", "by", "bz", "tmi"] + components_2 = ["by", "tmi"] + rxLoc_1 = PF.magnetics.receivers.Point( + np.c_[utils.mkvc(ccy.T), utils.mkvc(ccx.T), utils.mkvc(ccz.T)], + components=components_1, + ) + rxLoc_2 = PF.magnetics.receivers.Point( + np.c_[utils.mkvc(ccy.T), utils.mkvc(ccx.T), utils.mkvc(ccz.T + 20)], + components=components_2, + ) + inducing_field = [55000.0, 60.0, 90.0] + + srcField_1 = PF.magnetics.sources.UniformBackgroundField( + [rxLoc_1], inducing_field[0], inducing_field[1], inducing_field[2] + ) + survey_1 = PF.magnetics.survey.Survey(srcField_1) + + srcField_2 = PF.magnetics.sources.UniformBackgroundField( + [rxLoc_2], inducing_field[0], inducing_field[1], inducing_field[2] + ) + survey_2 = PF.magnetics.survey.Survey(srcField_2) + + srcField_all = PF.magnetics.sources.UniformBackgroundField( + [rxLoc_1, rxLoc_2], inducing_field[0], inducing_field[1], inducing_field[2] + ) + survey_all = PF.magnetics.survey.Survey(srcField_all) + + amplitude = survey_1.source_field.amplitude + inclination = survey_1.source_field.inclination + declination = survey_1.source_field.declination + inducing_field = [amplitude, inclination, declination] + + susceptibility = 5 + MrX = 150000 + MrY = 150000 + MrZ = 150000 + + center = np.array([00, 0, -400.0]) + axes = [600.0, 200.0] + strike_dip_rake = [0, 0, 90] + + ellipsoid = ProlateEllipsoid( + center, + axes, + strike_dip_rake, + susceptibility=susceptibility, + Mr=(MrX, MrY, MrZ), + inducing_field=inducing_field, + ) + + ind_ellipsoid = ellipsoid.get_indices(mesh.cell_centers) + + sus_model = np.zeros(mesh.n_cells) + sus_model[ind_ellipsoid] = susceptibility + + Rx = np.zeros(mesh.n_cells) + Ry = np.zeros(mesh.n_cells) + Rz = np.zeros(mesh.n_cells) + + Rx[ind_ellipsoid] = MrX / 55000 + Ry[ind_ellipsoid] = MrY / 55000 + Rz[ind_ellipsoid] = MrZ / 55000 + + EsusRem = mkvc(np.array([Rx, Ry, Rz]).T) + + chimap = maps.ChiMap(mesh) + eff_sus_map = maps.EffectiveSusceptibilityMap( + nP=mesh.n_cells * 3, ambient_field_magnitude=survey_1.source_field.amplitude + ) + + wire_map = maps.Wires(("mu", mesh.n_cells), ("rem", mesh.n_cells * 3)) + mu_map = chimap * wire_map.mu + rem_map = eff_sus_map * wire_map.rem + m = np.r_[sus_model, EsusRem] + + simulation_1 = PF.magnetics.simulation.Simulation3DDifferential( + mesh=mesh, + survey=survey_1, + muMap=mu_map, + remMap=rem_map, + ) + + simulation_2 = PF.magnetics.simulation.Simulation3DDifferential( + survey=survey_2, + mesh=mesh, + muMap=mu_map, + remMap=rem_map, + ) + + simulation_all = PF.magnetics.simulation.Simulation3DDifferential( + survey=survey_all, + mesh=mesh, + muMap=mu_map, + remMap=rem_map, + ) + dpred_numeric_all = simulation_all.dpred(m) + dpred_numeric_1 = simulation_1.dpred(m) + dpred_numeric_2 = simulation_2.dpred(m) + dpred_stack = np.hstack((dpred_numeric_1, dpred_numeric_2)) + + rvec = np.random.randn(mesh.n_cells * 4) * 0.001 + + jv_all = simulation_all.Jvec(m, v=rvec) + jv_1 = simulation_1.Jvec(m, v=rvec) + jv_2 = simulation_2.Jvec(m, v=rvec) + jv_stack = np.hstack((jv_1, jv_2)) + + assert np.allclose(dpred_numeric_all, dpred_stack, atol=1e-8) + assert np.allclose(jv_all, jv_stack, atol=1e-8) + + +def test_unsupported_components(mesh): + """ + Test error when survey has unsupported components. + """ + supported_components = ["tmi", "bx", "by", "bz"] + unsupported_components = ["bxx", "byy", "bzz"] + receivers = [ + PF.magnetics.Point( + np.array([[0, 0, 0], [1, 2, 3]]), + components=components, + ) + for components in (*supported_components, *unsupported_components) + ] + inducing_field = [55000.0, 60.0, 90.0] + source = PF.magnetics.sources.UniformBackgroundField(receivers, *inducing_field) + survey = PF.magnetics.survey.Survey(source) + + msg = "Found unsupported magnetic components " + with pytest.raises(NotImplementedError, match=msg): + PF.magnetics.simulation.Simulation3DDifferential(survey=survey, mesh=mesh) diff --git a/tests/pf/test_mag_differential_jvecjtvec.py b/tests/pf/test_mag_differential_jvecjtvec.py new file mode 100644 index 0000000000..91daa643d0 --- /dev/null +++ b/tests/pf/test_mag_differential_jvecjtvec.py @@ -0,0 +1,190 @@ +from discretize.tests import check_derivative, assert_isadjoint +import numpy as np +import pytest +from simpeg import maps, utils +from discretize.utils import mkvc, refine_tree_xyz +import discretize +import simpeg.potential_fields as PF + + +@pytest.fixture +def mesh(): + dhx, dhy, dhz = 400.0, 400.0, 400.0 # minimum cell width (base mesh cell width) + nbcx = 512 # number of base mesh cells in x + nbcy = 512 + nbcz = 512 + + # Define base mesh (domain and finest discretization) + hx = dhx * np.ones(nbcx) + hy = dhy * np.ones(nbcy) + hz = dhz * np.ones(nbcz) + _mesh = discretize.TreeMesh([hx, hy, hz], x0="CCC") + + xp, yp, zp = np.meshgrid([-1400.0, 1400.0], [-1400.0, 1400.0], [-1000.0, 200.0]) + xy = np.c_[mkvc(xp), mkvc(yp), mkvc(zp)] + _mesh = refine_tree_xyz( + _mesh, + xy, + method="box", + finalize=False, + octree_levels=[1, 1, 1, 1], + ) + _mesh.finalize() + return _mesh + + +@pytest.fixture +def survey(): + ccx = np.linspace(-1400, 1400, num=57) + ccy = np.copy(ccx) + + ccx, ccy = np.meshgrid(ccx, ccy) + + ccz = 50.0 * np.ones_like(ccx) + + components = ["bx", "by", "bz", "tmi"] + rxLoc = PF.magnetics.receivers.Point( + np.c_[utils.mkvc(ccy.T), utils.mkvc(ccx.T), utils.mkvc(ccz.T)], + components=components, + ) + inducing_field = [55000.0, 60.0, 90.0] + srcField = PF.magnetics.sources.UniformBackgroundField( + [rxLoc], inducing_field[0], inducing_field[1], inducing_field[2] + ) + _survey = PF.magnetics.survey.Survey(srcField) + + return _survey + + +@pytest.mark.parametrize( + "deriv_type", ("mu", "rem", "mu_fix_rem", "rem_fix_mu", "both") +) +def test_derivative(deriv_type, mesh, survey): + np.random.seed(40) + + chimap = maps.ChiMap(mesh) + eff_sus_map = maps.EffectiveSusceptibilityMap( + ambient_field_magnitude=survey.source_field.amplitude, nP=mesh.n_cells * 3 + ) + + sus_model = np.abs(np.random.randn(mesh.n_cells)) + mu_model = chimap * sus_model + + Rx = np.random.randn(mesh.n_cells) + Ry = np.random.randn(mesh.n_cells) + Rz = np.random.randn(mesh.n_cells) + EsusRem = mkvc(np.array([Rx, Ry, Rz]).T) + + u0_Mr_model = eff_sus_map * EsusRem + + if deriv_type == "mu": + mu_map = chimap + mu = None + rem_map = None + rem = None + m = sus_model + if deriv_type == "rem": + mu_map = None + mu = None + rem_map = eff_sus_map + rem = None + m = EsusRem + if deriv_type == "mu_fix_rem": + mu_map = chimap + mu = None + rem_map = None + rem = u0_Mr_model + m = sus_model + if deriv_type == "rem_fix_mu": + mu_map = None + mu = mu_model + rem_map = eff_sus_map + rem = None + m = EsusRem + if deriv_type == "both": + wire_map = maps.Wires(("mu", mesh.n_cells), ("rem", mesh.n_cells * 3)) + mu_map = chimap * wire_map.mu + rem_map = eff_sus_map * wire_map.rem + m = np.r_[sus_model, EsusRem] + mu = None + rem = None + + simulation = PF.magnetics.simulation.Simulation3DDifferential( + survey=survey, mesh=mesh, mu=mu, rem=rem, muMap=mu_map, remMap=rem_map + ) + + def sim_func(m): + d = simulation.dpred(m) + + def J(v): + return simulation.Jvec(m, v) + + return d, J + + assert check_derivative(sim_func, m, plotIt=False, num=6, eps=1e-8, random_seed=40) + + +@pytest.mark.parametrize( + "deriv_type", ("mu", "rem", "mu_fix_rem", "rem_fix_mu", "both") +) +def test_adjoint(deriv_type, mesh, survey): + np.random.seed(40) + + chimap = maps.ChiMap(mesh) + eff_sus_map = maps.EffectiveSusceptibilityMap( + ambient_field_magnitude=survey.source_field.amplitude, nP=mesh.n_cells * 3 + ) + + sus_model = np.abs(np.random.randn(mesh.n_cells)) + mu_model = chimap * sus_model + + Rx = np.random.randn(mesh.n_cells) + Ry = np.random.randn(mesh.n_cells) + Rz = np.random.randn(mesh.n_cells) + EsusRem = mkvc(np.array([Rx, Ry, Rz]).T) + + u0_Mr_model = eff_sus_map * EsusRem + + if deriv_type == "mu": + mu_map = chimap + mu = None + rem_map = None + rem = None + m = sus_model + if deriv_type == "rem": + mu_map = None + mu = None + rem_map = eff_sus_map + rem = None + m = EsusRem + if deriv_type == "mu_fix_rem": + mu_map = chimap + mu = None + rem_map = None + rem = u0_Mr_model + m = sus_model + if deriv_type == "rem_fix_mu": + mu_map = None + mu = mu_model + rem_map = eff_sus_map + rem = None + m = EsusRem + if deriv_type == "both": + wire_map = maps.Wires(("mu", mesh.n_cells), ("rem", mesh.n_cells * 3)) + mu_map = chimap * wire_map.mu + rem_map = eff_sus_map * wire_map.rem + m = np.r_[sus_model, EsusRem] + mu = None + rem = None + + simulation = PF.magnetics.simulation.Simulation3DDifferential( + survey=survey, mesh=mesh, mu=mu, rem=rem, muMap=mu_map, remMap=rem_map + ) + + def J(v): + return simulation.Jvec(m, v) + + def JT(v): + return simulation.Jtvec(m, v) + + assert_isadjoint(J, JT, len(m), survey.nD, random_seed=40) diff --git a/tests/utils/ellipsoid.py b/tests/utils/ellipsoid.py new file mode 100644 index 0000000000..7b97dfb210 --- /dev/null +++ b/tests/utils/ellipsoid.py @@ -0,0 +1,553 @@ +from simpeg import utils +import numpy as np + +""" +The code for forward modelling magnetic field of ellipsoids present in this +file is based on the implementation made by Diego Takahashi Tomazella and +Vanderlei C. Oliveira Jr., which is available in +https://github.com/pinga-lab/magnetic-ellipsoid, and has been released under +the BSD 3-Clause license: + +> Copyright (c) Diego Takahashi Tomazella and Vanderlei C. Oliveira Jr. +> All rights reserved. +> +> Redistribution and use in source and binary forms, with or without +> modification, are permitted provided that the following conditions are met: +> +> * Redistributions of source code must retain the above copyright notice, this +> list of conditions and the following disclaimer. +> * Redistributions in binary form must reproduce the above copyright notice, +> this list of conditions and the following disclaimer in the documentation +> and/or other materials provided with the distribution. +> * Neither the names of the copyright holders nor the names of any +> contributors may be used to endorse or promote products derived from this +> software without specific prior written permission. +> +> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +> AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +> IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +> ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +> LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +> CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +> SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +> INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +> CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +> ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +> POSSIBILITY OF SUCH DAMAGE. +""" + + +class ProlateEllipsoid: + r"""Class for magnetostatic solution for a permeable and remanently + magnetized prolate ellipsoid in a uniform magnetostatic field + based on: https://github.com/pinga-lab/magnetic-ellipsoid + + The ``ProlateEllipse`` class is used to analytically compute the external and internal + secondary magnetic flux density + + Parameters + ---------- + center : (3) array_like, optional + center of ellipsoid (m). + axis : (2) array_like, optional + major and both minor axes of ellipsoid (m). + strike_dip_rake : (3) array_like, optional + strike, dip, and rake of ellipsoid, defined in paper (degrees) + Sets property V (rotation matrix) + susceptibility : float + susceptibility of ellipsoid (SI). + Mr : (3) array_like, optional + Intrinsic remanent magnetic polarization (\mu_0 M) of ellipsoid. + If susceptibility = 0,equivalent to total resultant magnetization. (nT) + inducing_field : (3) array_like, optional + Ambient Geomagnetic Field. (strength(nT),inclination (degrees), declination (degrees) + """ + + def __init__( + self, + center=(0, 0, 0), + axes=(100.1, 100), + strike_dip_rake=(0, 0, 0), + susceptibility=0.0, + Mr=(0, 0, 0), + inducing_field=(50000, 0, 90), + **kwargs, + ): + self.center = self.__redefine_coords(center) + self.axes = axes + self.susceptibility = susceptibility + self.V = strike_dip_rake + Mr = np.array(Mr) + self.Mr = Mr.T + self.B_0 = inducing_field + + @property + def center(self): + """Center of the sphere + + Returns + ------- + (3) numpy.ndarray of float + Center of the sphere. Default = np.r_[0,0,0] + """ + return self._center + + @center.setter + def center(self, vec): + + try: + vec = np.atleast_1d(vec).astype(float) + except (TypeError, AttributeError, ValueError): + raise TypeError(f"location must be array_like, got {type(vec)}") + + if len(vec) != 3: + raise ValueError( + f"location must be array_like with shape (3,), got {len(vec)}" + ) + + self._center = vec + + @property + def axes(self): + """The major axis and shared minor axes of the prolate ellipsoid + + Returns + ------- + (2) numpy.ndarray of float + Center of the sphere. Default = np.r_[100.1,100] + """ + return self._axes + + @axes.setter + def axes(self, vec): + + try: + vec = np.atleast_1d(vec).astype(float) + except (TypeError, AttributeError, ValueError): + raise TypeError(f"location must be array_like, got {type(vec)}") + + if len(vec) != 2: + raise ValueError( + "location must be array_like with shape (2,), got {len(vec)}" + ) + + if vec[0] <= vec[1]: + raise ValueError( + "The major axis of the ellipsoid must be greater then the minor axes" + ) + + if np.any(np.less(vec, 0)): + raise ValueError("The axes must be positive") + axes = np.zeros(3) + axes[:2] = vec + axes[2] = vec[1] + self._axes = axes + + @property + def V(self): + """Rotation Matrix of Ellipsoid + + Returns + ------- + (3,3) numpy.ndarray of float + Rotation Matrix of Ellipsoid + """ + return self._V + + @V.setter + def V(self, vec): + + try: + vec = np.atleast_1d(vec).astype(float) + except (TypeError, AttributeError, ValueError): + raise TypeError(f"strike_dip_rake must be array_like, got {type(vec)}") + + if len(vec) != 3: + raise ValueError( + f"strike_dip_rake must be array_like with shape (3,), got {len(vec)}" + ) + + self._V = self.__rotation_matrix(np.radians(vec)) + + @property + def susceptibility(self): + """Magnetic susceptibility (SI) + + Returns + ------- + float + Magnetic Susceptibility (SI) + """ + return self._susceptibility + + @susceptibility.setter + def susceptibility(self, item): + item = float(item) + if item < 0.0: + raise ValueError("Susceptibility must be positive") + self._susceptibility = item + + @property + def Mr(self): + r"""The remanent polarization (\mu0 M), (nT) + + Returns + ------- + (3) numpy.ndarray of float + Remanent Polarization (nT) + """ + return self._Mr + + @Mr.setter + def Mr(self, vec): + + try: + vec = np.atleast_1d(vec).astype(float) + except (TypeError, AttributeError, ValueError): + raise TypeError(f"location must be array_like, got {type(vec)}") + + if len(vec) != 3: + raise ValueError( + f"location must be array_like with shape (3,), got {len(vec)}" + ) + self._Mr = self.__redefine_coords(vec) + + @property + def B_0(self): + """Amplitude of the inducing field (nT). + + Returns + ------- + (3) numpy.ndarray of float + Amplitude of the primary current density. Default = np.r_[1,0,0] + """ + return self._B_0 + + @B_0.setter + def B_0(self, vec): + + try: + vec = np.atleast_1d(vec).astype(float) + except (TypeError, AttributeError, ValueError): + raise TypeError(f"primary_field must be array_like, got {type(vec)}") + + if len(vec) != 3: + raise ValueError( + f"primary_field must be array_like with shape (3,), got {len(vec)}" + ) + + mag = utils.mat_utils.dip_azimuth2cartesian( + vec[1], + vec[2], + ) + + B_0 = np.array([mag[:, 0] * vec[0], mag[:, 1] * vec[0], mag[:, 2] * vec[0]])[ + :, 0 + ] + + B_0 = self.__redefine_coords(B_0) + + self._B_0 = B_0 + + def get_indices(self, xyz): + """Returns Boolean of provided points internal to ellipse + + Parameters + ---------- + xyz : (..., 3) numpy.ndarray + Locations to evaluate at in units m. + + Returns + ------- + ind: Boolean array, True if internal to ellipse + + """ + + V = self.V + a = self.axes[0] + b = self.axes[1] + c = self.axes[1] + A = np.identity(3) + A[0, 0] = a**-2 + A[1, 1] = b**-2 + A[2, 2] = c**-2 + A = V @ A @ V.T + center = self.center + + t1 = xyz[:, 1] - center[0] + t2 = xyz[:, 0] - center[1] + t3 = -xyz[:, 2] - center[2] + + r_m_rc = np.array([t1, t2, t3]) + b = A @ r_m_rc + + values = np.sum(r_m_rc * b, axis=0) + + ind = values < 1 + + return ind + + def Magnetization(self): + """Returns the resultant magnetization of the ellipsoid as a function + of susceptibility and remanent magnetization + + Parameters + ---------- + + Returns + ------- + M: (3) numpy.ndarray of float + + """ + + V = self.V + + K = self.susceptibility * np.identity(3) # /(4*np.pi) + + N1 = self.__depolarization_prolate() + + I = np.identity(3) + + inv = np.linalg.inv(I + K @ N1) + + M = V @ inv @ V.T @ (K @ self.B_0.T + self.Mr.T) + + M = self.__redefine_coords(M.T) + + return M + + def anomalous_bfield(self, xyz): + """Returns the internal and external secondary magnetic field B_s + + Parameters + ---------- + xyz : (..., 3) numpy.ndarray + Locations to evaluate at in units m. + + Returns + ------- + B_s : (..., 3) np.ndarray + Units of nT + + """ + a = self.axes[0] + b = self.axes[1] + axes_array = np.array([a, b, b]) + + internal_indices = self.get_indices(xyz) + xyz = self.__redefine_coords(xyz) + xyz_m_center = xyz - self.center + + body_axis_coords = (self.V.T @ xyz_m_center.T).T + + x1 = body_axis_coords[:, 0] + x2 = body_axis_coords[:, 1] + x3 = body_axis_coords[:, 2] + + xyz = [x1, x2, x3] + + M = self.__redefine_coords(self.Magnetization()) + + lam = self.__get_lam(x1, x2, x3) + + dlam = self.__d_lam(x1, x2, x3, lam) + + R = np.sqrt((a**2 + lam) * (b**2 + lam) * (b**2 + lam)) + + h = [] + for i in range(len(axes_array)): + h.append(-1 / ((axes_array[i] ** 2 + lam) * R)) + + g = self.__g(lam) + + N2 = self.__N2(h, g, dlam, xyz) + + B_s = self.V @ N2 @ self.V.T @ M + + N1 = self.__depolarization_prolate() + + M_norotate = self.Magnetization() + + B_s = self.__redefine_coords(B_s) + + B_s[internal_indices, :] = M_norotate - N1 @ M_norotate + + return B_s + + def TMI(self, xyz): + """Returns the internal and external exact TMI data + + Parameters + ---------- + xyz : (..., 3) numpy.ndarray + Locations to evaluate at in units m. + + Returns + ------- + TMI : (...,) np.ndarray + Units of nT + + """ + + B_0 = self.__redefine_coords(self.B_0) + + B = self.anomalous_bfield(xyz) + + TMI = np.linalg.norm(B_0 + B, axis=1) - np.linalg.norm(self.B_0) + + return TMI + + def TMI_approx(self, xyz): + """Returns the internal and external approximate TMI data + + Parameters + ---------- + xyz : (..., 3) numpy.ndarray + Locations to evaluate at in units m. + + Returns + ------- + TMI_approx : (...,) np.ndarray + Units of nT + + """ + + B = self.anomalous_bfield(xyz) + B0 = self.__redefine_coords(self.B_0) + + TMI_approx = (B @ B0.T) / np.linalg.norm(B0) + + return TMI_approx + + def __redefine_coords(self, coords): + coords_copy = np.copy(coords) + if len(np.shape(coords)) == 1: + + temp = np.copy(coords[0]) + coords_copy[0] = coords[1] + coords_copy[1] = temp + coords_copy[2] *= -1 + else: + temp = np.copy(coords[:, 0]) + coords_copy[:, 0] = coords[:, 1] + coords_copy[:, 1] = temp + coords_copy[:, 2] *= -1 + + return coords_copy + + def __rotation_matrix(self, strike_dip_rake): + strike = strike_dip_rake[0] + dip = strike_dip_rake[1] + rake = strike_dip_rake[2] + + def R1(theta): + return np.array( + [ + [1, 0, 0], + [0, np.cos(theta), np.sin(theta)], + [0, -np.sin(theta), np.cos(theta)], + ] + ) + + def R2(theta): + return np.array( + [ + [np.cos(theta), 0, -np.sin(theta)], + [0, 1, 0], + [np.sin(theta), 0, np.cos(theta)], + ] + ) + + def R3(theta): + return np.array( + [ + [np.cos(theta), np.sin(theta), 0], + [-np.sin(theta), np.cos(theta), 0], + [0, 0, 1], + ] + ) + + V = R1(np.pi / 2) @ R2(strike) @ R1(np.pi / 2 - dip) @ R3(rake) + + return V + + def __depolarization_prolate(self): + a = self.axes[0] + b = self.axes[1] + + m = a / b + + t11 = 1 / (m**2 - 1) + t22 = m / (m**2 - 1) ** 0.5 + t33 = np.log(m + (m**2 - 1) ** 0.5) + + n11 = t11 * (t22 * t33 - 1) + n22 = 0.5 * (1 - n11) + n33 = n22 + + N1 = np.zeros((3, 3)) + N1[0, 0] = n11 + N1[1, 1] = n22 + N1[2, 2] = n33 + + return N1 + + def __N2(self, h, g, dlam, xyz): + size = np.shape(g[0])[0] + N2 = np.zeros((size, 3, 3)) + abc_2 = self.axes[0] * self.axes[1] * self.axes[2] / 2 + for i in range(3): + for j in range(3): + if i == j: + N2[:, i, j] = -abc_2 * (dlam[i] * h[i] * xyz[i] + g[i]) + else: + N2[:, i, j] = -abc_2 * (dlam[i] * h[j] * xyz[j]) + + return N2 + + def __get_lam(self, x1, x2, x3): + a = self.axes[0] + b = self.axes[1] + p1 = a**2 + b**2 - x1**2 - x2**2 - x3**2 + p0 = a**2 * b**2 - b**2 * x1**2 - a**2 * (x2**2 + x3**2) + lam = (-p1 + np.sqrt(p1**2 - 4 * p0)) / 2 + + return lam + + def __d_lam(self, x1, x2, x3, lam): + + dlam = [] + xyz = [x1, x2, x3] + + den = ( + (x1 / (self.axes[0] ** 2 + lam)) ** 2 + + (x2 / (self.axes[1] ** 2 + lam)) ** 2 + + (x3 / (self.axes[1] ** 2 + lam)) ** 2 + ) + + for i in range(3): + num = (2 * xyz[i]) / (self.axes[i] ** 2 + lam) + dlam.append(num / den) + + return dlam + + def __g(self, lam): + a = self.axes[0] + b = self.axes[1] + a2lam = a**2 + lam + b2lam = b**2 + lam + a2mb2 = a**2 - b**2 + + gmul = 1 / (a2mb2**1.5) + g1t1 = np.log((a2mb2**0.5 + a2lam**0.5) / b2lam**0.5) + g1t2 = (a2mb2 / a2lam) ** 0.5 + + g2t2 = (a2mb2 * a2lam) ** 0.5 / b2lam + + g1 = 2 * gmul * (g1t1 - g1t2) + g2 = gmul * (g2t2 - g1t1) + g3 = g2 + + g = [g1, g2, g3] + + return g From 22f835392d66a07abd2ec9d9f24ad7955f5e99be Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Fri, 12 Sep 2025 14:50:37 -0600 Subject: [PATCH 159/194] Standardize output directives and make them more reliable (#1657) Fixes and standardizes the `SaveEveryIteration` child directives. They now reliably use the passed `directory` argument, and this also adds a few more safeguards to the file writes. --------- Co-authored-by: Santiago Soler --- simpeg/directives/_directives.py | 361 ++++++++++----- simpeg/directives/_sim_directives.py | 79 ++-- tests/base/test_directives.py | 627 +++++++++++++++++++++++++++ 3 files changed, 925 insertions(+), 142 deletions(-) diff --git a/simpeg/directives/_directives.py b/simpeg/directives/_directives.py index 0cd7ef6bfe..77b3a3c5af 100644 --- a/simpeg/directives/_directives.py +++ b/simpeg/directives/_directives.py @@ -1,8 +1,11 @@ +from abc import ABCMeta, abstractmethod from typing import TYPE_CHECKING + +from datetime import datetime +import pathlib import numpy as np import matplotlib.pyplot as plt import warnings -import os import scipy.sparse as sp from ..typing import RandomSeed from ..data_misfit import BaseDataMisfit @@ -1631,40 +1634,75 @@ def endIter(self): print("All targets have been reached") -class SaveEveryIteration(InversionDirective): +class SaveEveryIteration(InversionDirective, metaclass=ABCMeta): """SaveEveryIteration - This directive saves an array at each iteration. The default - directory is the current directory and the models are saved as - ``InversionModel-YYYY-MM-DD-HH-MM-iter.npy`` + This directive saves information at each iteration. + + Parameters + ---------- + directory : pathlib.Path or str, optional + The directory to store output information to, defaults to current directory. + name : str, optional + Root of the filename to be saved, commonly this will get iteration specific + details appended to it. + on_disk : bool, optional + Whether this directive will save a log file to disk. """ - def __init__(self, directory=".", name="InversionModel", **kwargs): + def __init__(self, directory=".", name="InversionModel", on_disk=True, **kwargs): + self._on_disk = validate_type("on_disk", on_disk, bool) + super().__init__(**kwargs) - self.directory = directory + if self.on_disk: + self.directory = directory + else: + self.directory = None self.name = name + self._time_string_format = "%Y-%m-%d-%H-%M" + self._iter_format = "03d" + self._iter_string = "###" + self._start_time = self._time_string_format + + def initialize(self): + self._start_time = datetime.now().strftime(self._time_string_format) + if opt := getattr(self, "opt", None): + max_digit = len(str(opt.maxIter)) + self._iter_format = f"0{max_digit}d" + + @property + def on_disk(self) -> bool: + """Whether this object stores information to `file_abs_path`.""" + return self._on_disk + + @on_disk.setter + def on_disk(self, value): + self._on_disk = validate_type("on_disk", value, bool) @property - def directory(self): + def directory(self) -> pathlib.Path: """Directory to save results in. Returns ------- - str + pathlib.Path """ + if not self.on_disk: + raise AttributeError( + f"'{type(self).__qualname__}.directory' is only available if saving to disk." + ) return self._directory @directory.setter def directory(self, value): - value = validate_string("directory", value) - fullpath = os.path.abspath(os.path.expanduser(value)) - - if not os.path.isdir(fullpath): - os.mkdir(fullpath) + if value is None and self.on_disk: + raise ValueError("Directory is not optional if 'on_disk==True'.") + if value is not None: + value = validate_type("directory", value, pathlib.Path).resolve() self._directory = value @property - def name(self): + def name(self) -> str: """Root of the filename to be saved. Returns @@ -1678,74 +1716,164 @@ def name(self, value): self._name = validate_string("name", value) @property - def fileName(self): - if getattr(self, "_fileName", None) is None: - from datetime import datetime + def _time_iter_file_name(self) -> pathlib.Path: + time_string = self._start_time + if not getattr(self, "opt", None): + iter_string = "###" + else: + itr = getattr(self.opt, "iter", 0) + iter_string = f"{itr:{self._iter_format}}" + + return pathlib.Path(f"{self.name}_{time_string}_{iter_string}") - self._fileName = "{0!s}-{1!s}".format( - self.name, datetime.now().strftime("%Y-%m-%d-%H-%M") + @property + def _time_file_name(self) -> pathlib.Path: + return pathlib.Path(f"{self.name}_{self._start_time}") + + def _mkdir_and_check_output_file(self, should_exist=False): + """ + Use this to ensure a directory exists, and to check if file_abs_path exists. + Issues a warning if the output file exists but should not, + or if it doesn't exist but does. + + Parameters + ---------- + should_exist : bool, optional + Whether file_abs_path should exist. + """ + self.directory.mkdir(exist_ok=True) + fp = self.file_abs_path + exists = fp.exists() + if exists and not should_exist: + warnings.warn(f"Overwriting file {fp}", UserWarning, stacklevel=2) + if not exists and should_exist: + warnings.warn( + f"File {fp} was not found, creating a new one.", + UserWarning, + stacklevel=2, ) - return self._fileName + + @property + def fileName(self): + warnings.warn( + "'fileName' has been deprecated and will be removed in SimPEG 0.26.0 use 'file_abs_path'", + FutureWarning, + stacklevel=2, + ) + return self.file_abs_path.stem + + @property + @abstractmethod + def file_abs_path(self) -> pathlib.Path: + """The absolute path to the saved output file. + + Returns + ------- + pathlib.Path + """ class SaveModelEveryIteration(SaveEveryIteration): - """SaveModelEveryIteration + """Saves the inversion model at the end of every iteration to a directory + + Parameters + ---------- + directory : pathlib.Path or str, optional + The directory to store output information to, defaults to current directory. + name : str, optional + Root of the filename to be saved, defaults to ``'InversionModel'`` + + Notes + ----- This directive saves the model as a numpy array at each iteration. The - default directory is the current directoy and the models are saved as - ``InversionModel-YYYY-MM-DD-HH-MM-iter.npy`` + default directory is the current directory and the models are saved as + `name` + ``'_YYYY-MM-DD-HH-MM_iter.npy'`` """ + def __init__(self, **kwargs): + if "on_disk" in kwargs: + msg = ( + f"The 'on_disk' argument is ignored by the '{type(self).__name__}' " + "directive, it's always True." + ) + warnings.warn(msg, UserWarning, stacklevel=2) + kwargs.pop("on_disk") + super().__init__(on_disk=True, **kwargs) + def initialize(self): + super().initialize() print( - "simpeg.SaveModelEveryIteration will save your models as: " - "'{0!s}###-{1!s}.npy'".format(self.directory + os.path.sep, self.fileName) + f"{type(self).__qualname__} will save your models as: " + f"'{self.file_abs_path}'" ) - def endIter(self): - np.save( - "{0!s}{1:03d}-{2!s}".format( - self.directory + os.path.sep, self.opt.iter, self.fileName - ), - self.opt.xc, - ) - - -class SaveOutputEveryIteration(SaveEveryIteration): - """SaveOutputEveryIteration""" - - def __init__(self, save_txt=True, **kwargs): - super().__init__(**kwargs) - - self.save_txt = save_txt - @property - def save_txt(self): - """Whether to save the output as a text file. + def on_disk(self) -> bool: + """This class always saves to disk. Returns ------- bool """ - return self._save_txt + return True + + @on_disk.setter + def on_disk(self, value): # noqa: F811 + """This class always saves to disk.""" + msg = ( + f"Cannot modify value of 'on_disk' for {type(self).__name__}' directive. " + "It's always True." + ) + raise AttributeError(msg) + + @property + def file_abs_path(self) -> pathlib.Path: + return self.directory / self._time_iter_file_name.with_suffix(".npy") - @save_txt.setter - def save_txt(self, value): - self._save_txt = validate_type("save_txt", value, bool) + def endIter(self): + self._mkdir_and_check_output_file(should_exist=False) + np.save(self.file_abs_path, self.opt.xc) + + +class SaveOutputEveryIteration(SaveEveryIteration): + """Keeps track of the objective function values. + + Parameters + ---------- + on_disk : bool, optional + Whether this directive additionally stores the log to a text file. + directory : pathlib.Path, optional + The directory to store output information to if `on_disk`, defaults to current directory. + name : str, optional + The root name of the file to save to, will append the inversion start time to this value. + """ + + def __init__(self, on_disk=True, **kwargs): + if (save_txt := kwargs.pop("save_txt", None)) is not None: + self.save_txt = save_txt + on_disk = self.save_txt + super().__init__(on_disk=on_disk, **kwargs) def initialize(self): - if self.save_txt is True: + super().initialize() + if self.on_disk: + fp = self.file_abs_path print( - "simpeg.SaveOutputEveryIteration will save your inversion " - "progress as: '###-{0!s}.txt'".format(self.fileName) + f"'{type(self).__qualname__}' will save your inversion " + f"progress to: '{fp}'" ) - f = open(self.fileName + ".txt", "w") - header = " # beta phi_d phi_m phi_m_small phi_m_smoomth_x phi_m_smoomth_y phi_m_smoomth_z phi\n" - f.write(header) - f.close() + self._mkdir_and_check_output_file(should_exist=False) + with open(fp, "w") as f: + f.write(f"{self._header}\n") + self._initialize_lists() - # Create a list of each + @property + def _header(self): + return " # beta phi_d phi_m phi_m_small phi_m_smoomth_x phi_m_smoomth_y phi_m_smoomth_z phi" + def _initialize_lists(self): + # Create a list of each self.beta = [] self.phi_d = [] self.phi_m = [] @@ -1755,6 +1883,19 @@ def initialize(self): self.phi_m_smooth_z = [] self.phi = [] + @property + def file_abs_path(self) -> pathlib.Path | None: + """The absolute path to the saved log file.""" + if self.on_disk: + return self.directory / self._time_file_name.with_suffix(".txt") + + save_txt = deprecate_property( + SaveEveryIteration.on_disk, + "save_txt", + removal_version="0.26.0", + future_warn=True, + ) + def endIter(self): phi_s, phi_x, phi_y, phi_z = 0, 0, 0, 0 @@ -1782,26 +1923,35 @@ def endIter(self): self.phi_m_smooth_z.append(phi_z) self.phi.append(self.opt.f) - if self.save_txt: - f = open(self.fileName + ".txt", "a") - f.write( - " {0:3d} {1:1.4e} {2:1.4e} {3:1.4e} {4:1.4e} {5:1.4e} " - "{6:1.4e} {7:1.4e} {8:1.4e}\n".format( - self.opt.iter, - self.beta[self.opt.iter - 1], - self.phi_d[self.opt.iter - 1], - self.phi_m[self.opt.iter - 1], - self.phi_m_small[self.opt.iter - 1], - self.phi_m_smooth_x[self.opt.iter - 1], - self.phi_m_smooth_y[self.opt.iter - 1], - self.phi_m_smooth_z[self.opt.iter - 1], - self.phi[self.opt.iter - 1], + if self.on_disk: + self._mkdir_and_check_output_file(should_exist=True) + with open(self.file_abs_path, "a") as f: + f.write( + " {0:3d} {1:1.4e} {2:1.4e} {3:1.4e} {4:1.4e} {5:1.4e} " + "{6:1.4e} {7:1.4e} {8:1.4e}\n".format( + self.opt.iter, + self.beta[-1], + self.phi_d[-1], + self.phi_m[-1], + self.phi_m_small[-1], + self.phi_m_smooth_x[-1], + self.phi_m_smooth_y[-1], + self.phi_m_smooth_z[-1], + self.phi[-1], + ) ) - ) - f.close() - def load_results(self): - results = np.loadtxt(self.fileName + str(".txt"), comments="#") + def load_results(self, file_name=None): + if file_name is None: + if not self.on_disk: + raise TypeError( + f"'file_name' is a required argument if '{type(self).__qualname__}.on_disk' is `False`" + ) + file_name = self.file_abs_path + results = np.loadtxt(file_name, comments="#") + if results.shape[1] != 9: + raise ValueError(f"{file_name} does not have valid results") + self.beta = results[:, 1] self.phi_d = results[:, 2] self.phi_m = results[:, 3] @@ -1809,13 +1959,12 @@ def load_results(self): self.phi_m_smooth_x = results[:, 5] self.phi_m_smooth_y = results[:, 6] self.phi_m_smooth_z = results[:, 7] + self.f = results[:, 8] self.phi_m_smooth = ( self.phi_m_smooth_x + self.phi_m_smooth_y + self.phi_m_smooth_z ) - self.f = results[:, 7] - self.target_misfit = self.invProb.dmisfit.simulation.survey.nD self.i_target = None @@ -1932,36 +2081,47 @@ def plot_tikhonov_curves(self, fname=None, dpi=200): class SaveOutputDictEveryIteration(SaveEveryIteration): - """ - Saves inversion parameters at every iteration. + """Saves inversion parameters to a dictionary at every iteration. + + At the end of every iteration, information about the current iteration is + saved to the `outDict` property of this object. + + Parameters + ---------- + on_disk : bool, optional + Whether to also save the parameters to an `npz` file at the end of each iteration. + directory : pathlib.Path or str, optional + Directory to save inversion parameters to if `on_disk`, defaults to current directory. + name : str, optional + Root name of the output file. The inversion start time and the iteration are appended to this. """ # Initialize the output dict - def __init__(self, saveOnDisk=False, **kwargs): - super().__init__(**kwargs) - self.saveOnDisk = saveOnDisk + def __init__(self, on_disk=False, **kwargs): + if (save_on_disk := kwargs.pop("saveOnDisk", None)) is not None: + self.saveOnDisk = save_on_disk + on_disk = self.saveOnDisk + super().__init__(on_disk=on_disk, **kwargs) + + saveOnDisk = deprecate_property( + SaveEveryIteration.on_disk, + "saveOnDisk", + removal_version="0.26.0", + future_warn=True, + ) @property - def saveOnDisk(self): - """Whether to save the output dict to disk. - - Returns - ------- - bool - """ - return self._saveOnDisk - - @saveOnDisk.setter - def saveOnDisk(self, value): - self._saveOnDisk = validate_type("saveOnDisk", value, bool) + def file_abs_path(self) -> pathlib.Path | None: + if self.on_disk: + return self.directory / self._time_iter_file_name.with_suffix(".npz") def initialize(self): + super().initialize() self.outDict = {} - if self.saveOnDisk: + if self.on_disk: print( - "simpeg.SaveOutputDictEveryIteration will save your inversion progress as dictionary: '###-{0!s}.npz'".format( - self.fileName - ) + f"'{type(self).__qualname__}' will save your inversion progress as a dictionary to: " + f"'{self.file_abs_path}'" ) def endIter(self): @@ -1999,8 +2159,9 @@ def endIter(self): iterDict[reg_name + ".norm"] = norm # Save the file as a npz - if self.saveOnDisk: - np.savez("{:03d}-{:s}".format(self.opt.iter, self.fileName), iterDict) + if self.on_disk: + self._mkdir_and_check_output_file(should_exist=False) + np.savez(self.file_abs_path, iterDict) self.outDict[self.opt.iter] = iterDict diff --git a/simpeg/directives/_sim_directives.py b/simpeg/directives/_sim_directives.py index 5c097ea913..126b335129 100644 --- a/simpeg/directives/_sim_directives.py +++ b/simpeg/directives/_sim_directives.py @@ -2,7 +2,7 @@ from ..regularization import BaseSimilarityMeasure from ..utils import eigenvalue_by_power_iteration from ..optimization import IterationPrinters, StoppingCriteria -from ._directives import InversionDirective, SaveEveryIteration +from ._directives import InversionDirective, SaveOutputEveryIteration ############################################################################### @@ -125,32 +125,18 @@ def endIter(self): self.invProb.lambd = self.reg.multipliers[-1] -class SimilarityMeasureSaveOutputEveryIteration(SaveEveryIteration): +class SimilarityMeasureSaveOutputEveryIteration(SaveOutputEveryIteration): """ SaveOutputEveryIteration for Joint Inversions. Saves information on the tradeoff parameters, data misfits, regularizations, coupling term, number of CG iterations, and value of cost function. """ - header = None - save_txt = True - betas = None - phi_d = None - phi_m = None - phi_sim = None - phi = None - - def initialize(self): - if self.save_txt is True: - print( - "CrossGradientSaveOutputEveryIteration will save your inversion " - "progress as: '###-{0!s}.txt'".format(self.fileName) - ) - f = open(self.fileName + ".txt", "w") - self.header = " # betas lambda joint_phi_d joint_phi_m phi_sim iterCG phi \n" - f.write(self.header) - f.close() + @property + def _header(self): + return " # betas lambda joint_phi_d joint_phi_m phi_sim iterCG phi " + def _initialize_lists(self): # Create a list of each self.betas = [] self.lambd = [] @@ -160,38 +146,47 @@ def initialize(self): self.phi_sim = [] def endIter(self): - self.betas.append(["{:.2e}".format(elem) for elem in self.invProb.betas]) - self.phi_d.append(["{:.3e}".format(elem) for elem in self.invProb.phi_d_list]) - self.phi_m.append(["{:.3e}".format(elem) for elem in self.invProb.phi_m_list]) - self.lambd.append("{:.2e}".format(self.invProb.lambd)) + self.betas.append(self.invProb.betas) + self.phi_d.append(self.invProb.phi_d_list) + self.phi_m.append(self.invProb.phi_m_list) + self.lambd.append(self.invProb.lambd) self.phi_sim.append(self.invProb.phi_sim) self.phi.append(self.opt.f) - if self.save_txt: - f = open(self.fileName + ".txt", "a") - i = self.opt.iter - f.write( - " {0:2d} {1} {2} {3} {4} {5:1.4e} {6:d} {7:1.4e}\n".format( - i, - self.betas[i - 1], - self.lambd[i - 1], - self.phi_d[i - 1], - self.phi_m[i - 1], - self.phi_sim[i - 1], - self.opt.cg_count, - self.phi[i - 1], + if self.on_disk: + self._mkdir_and_check_output_file(should_exist=True) + with open(self.file_abs_path, "a") as f: + f.write( + " {0:2d} {1} {2:.2e} {3} {4} {5:1.4e} {6:d} {7:1.4e}\n".format( + self.opt.iter, + [f"{el:.2e}" for el in self.betas[-1]], + self.lambd[-1], + [f"{el:.3e}" for el in self.phi_d[-1]], + [f"{el:.3e}" for el in self.phi_m[-1]], + self.phi_sim[-1], + self.opt.cg_count, + self.phi[-1], + ) ) - ) - f.close() - def load_results(self): - results = np.loadtxt(self.fileName + str(".txt"), comments="#") + def load_results(self, file_name=None): + if file_name is None: + if not self.on_disk: + raise TypeError( + f"'file_name' is a required argument if '{type(self).__qualname__}.on_disk' is `False`" + ) + file_name = self.file_abs_path + results = np.loadtxt(file_name, comments="#") + + if results.shape[1] != 8: + raise ValueError(f"{file_name} does not have valid results") + self.betas = results[:, 1] self.lambd = results[:, 2] self.phi_d = results[:, 3] self.phi_m = results[:, 4] self.phi_sim = results[:, 5] - self.f = results[:, 7] + self.phi = results[:, 7] class PairedBetaEstimate_ByEig(InversionDirective): diff --git a/tests/base/test_directives.py b/tests/base/test_directives.py index df53015109..407148ed1b 100644 --- a/tests/base/test_directives.py +++ b/tests/base/test_directives.py @@ -1,10 +1,16 @@ +import re +from collections import namedtuple +from datetime import datetime +import pathlib import unittest +import warnings from statistics import harmonic_mean import pytest import numpy as np import discretize +import simpeg from simpeg import ( maps, directives, @@ -15,6 +21,7 @@ simulation, ) from simpeg.data_misfit import L2DataMisfit +from simpeg.potential_fields import gravity from simpeg.potential_fields import magnetics as mag import shutil @@ -662,5 +669,625 @@ def test_end_iter_irls_threshold(self, mesh, data_misfit): assert sparse_regularization.irls_threshold == irls_threshold +class DummySaveEveryIteration(directives.SaveEveryIteration): + """ + Dummy non-abstract class to test SaveEveryIteration. + """ + + @property + def file_abs_path(self) -> pathlib.Path: + """ + Simple implementation of abstract property file_abs_path. + """ + return self.directory / self.name + + +class MockOpt: + """Mock Opt object.""" + + def __init__(self, xc=None, maxIter=100): + if xc is None: + xc = np.random.default_rng(seed=42).uniform(size=23) + self.xc = xc + self.maxIter = maxIter + + +class MockInvProb: + """Mock InvProb object.""" + + def __init__(self, opt): + self.opt = opt + + +class MockInversion: + """Mock Inversion object.""" + + def __init__(self, xc=None, maxIter=100): + opt = MockOpt(xc=xc, maxIter=maxIter) + inv_prob = MockInvProb(opt) + self.invProb = inv_prob + + +class TestSaveEveryIteration: + """Test the SaveEveryIteration directive.""" + + @pytest.mark.parametrize("directory", ["dummy/path", "../dummy/path"]) + def test_directory(self, directory): + """Test the directory property.""" + directive = DummySaveEveryIteration(directory=directory) + assert directive.directory == pathlib.Path(directory).resolve() + + def test_no_directory(self): + """Test if the directory property is None when on_disk is False""" + directive = DummySaveEveryIteration(directory="blah", on_disk=False) + assert directive._directory is None + + # accessing the directive property should raise error when on_disk is False + msg = re.escape("directory' is only available") + with pytest.raises(AttributeError, match=msg): + directive.directory + + # using the directive setter should raise error when on_disk is False + + @pytest.mark.parametrize("directory", ["dummy/path", "../dummy/path"]) + def test_directory_setter(self, directory): + """Test the directory setter.""" + directive = DummySaveEveryIteration() + directive.directory = directory + assert directive.directory == pathlib.Path(directory).resolve() + + def test_directory_setter_error_none(self): + """Test error when trying to set directory=None if on_disk is True.""" + directive = DummySaveEveryIteration() + msg = re.escape("Directory is not optional if 'on_disk==True'") + with pytest.raises(ValueError, match=msg): + directive.directory = None + + def test_name(self): + """Test the name property.""" + name = "blah" + directive = DummySaveEveryIteration(name=name) + assert directive.name == name + + def test_name_setter(self): + """Test the name setter.""" + directive = DummySaveEveryIteration() + name = "blah" + directive.name = name + assert directive.name == name + + def test_mkdir(self, tmp_path): + """Test _mkdir_and_check_output_file.""" + directory = tmp_path / "blah" + directive = DummySaveEveryIteration(directory=directory) + directive._mkdir_and_check_output_file() + assert directory.exists() + fname = directory / directive.name + assert not fname.exists() + + @pytest.mark.parametrize( + "should_exist", [True, False], ids=["should_exist", "should_not_exist"] + ) + def test_check_output_file_exists(self, tmp_path, should_exist): + """Test _mkdir_and_check_output_file when file exists.""" + directory = tmp_path / "blah" + directory.mkdir(parents=True) + directive = DummySaveEveryIteration(directory=directory) + fname = directive.file_abs_path + fname.touch() + assert fname.exists() + + if should_exist: + # No warning should be raised if exists and should exist + with warnings.catch_warnings(): + warnings.simplefilter("error") + directive._mkdir_and_check_output_file(should_exist=should_exist) + else: + # Warning should be raised if exists and should not exist + with pytest.warns(UserWarning, match="Overwriting file"): + directive._mkdir_and_check_output_file(should_exist=should_exist) + + @pytest.mark.parametrize( + "should_exist", [True, False], ids=["should_exist", "should_not_exist"] + ) + def test_check_output_file_doesnt_exist(self, tmp_path, should_exist): + """Test _mkdir_and_check_output_file when file doesn't exist.""" + directory = tmp_path / "blah" + directory.mkdir(parents=True) + directive = DummySaveEveryIteration(directory=directory) + fname = directive.file_abs_path + + if should_exist: + # Warning should be raised if doesn't exist and should exist + with pytest.warns( + UserWarning, match=re.escape(f"File {fname} was not found") + ): + directive._mkdir_and_check_output_file(should_exist=should_exist) + else: + # No warning should be raised if doesn't exist and should not exist + with warnings.catch_warnings(): + warnings.simplefilter("error") + directive._mkdir_and_check_output_file(should_exist=should_exist) + + @pytest.mark.parametrize("opt", [True, False], ids=["with-opt", "without-opt"]) + def test_initialize(self, opt): + """ + Test the initialize method. + """ + directive = DummySaveEveryIteration() + if opt: + directive.inversion = MockInversion(maxIter=10000) + + expected_start_time = datetime.now().strftime("%Y-%m-%d-%H-%M") + directive.initialize() + assert directive._start_time == expected_start_time + + if opt: + # maxIter was set to 10000, so the _iter_format should be "05d" + assert directive._iter_format == "05d" + + def test_time_iter_no_opt(self): + directive = DummySaveEveryIteration(name="dummy") + time_name = directive._time_file_name.name + assert directive._time_iter_file_name.name == time_name + "_###" + + def test_deprecated_fileName(self): + directive = DummySaveEveryIteration(name="dummy") + + with pytest.warns(FutureWarning, match=r"'fileName' has been deprecated .*"): + f_name = directive.fileName + + assert f_name == "dummy" + + +class TestSaveModelEveryIteration: + """Test the SaveModelEveryIteration directive.""" + + def test_on_disk(self): + """ + Test on_disk is always True. + """ + directive = directives.SaveModelEveryIteration() + assert directive.on_disk + + def test_on_disk_argument(self): + """ + Test warning after passing on_disk as argument. + """ + msg = re.escape("The 'on_disk' argument is ignored") + with pytest.warns(UserWarning, match=msg): + directive = directives.SaveModelEveryIteration(on_disk=False) + assert directive.on_disk + + def test_on_disk_setter(self): + """ + Test error after trying to modify value of on_disk. + """ + directive = directives.SaveModelEveryIteration() + msg = re.escape("Cannot modify value of 'on_disk'") + with pytest.raises(AttributeError, match=msg): + directive.on_disk = False + + def test_end_iter(self, tmp_path): + """ + Test if endIter saves the model to a file. + """ + directory = tmp_path / "dummy_dir" + directive = directives.SaveModelEveryIteration(directory=directory) + + # Add a mock inversion to the directive + mock_inversion = MockInversion() + directive.inversion = mock_inversion + + # Initialize and call endIter + directive.initialize() + directive.endIter() + + # Check if file exists + assert directory.exists() + assert directive.file_abs_path.exists() + array = np.load(directive.file_abs_path) + + np.testing.assert_equal(array, mock_inversion.invProb.opt.xc) + + +class BaseTestOutputDirective: + """ + Base class to test directives that need a full inversion. + """ + + def get_inversion_problem(self): + """ + Simple gravity inversion problem to test the directive. + """ + # Mesh + # ---- + h = [(1.0, 6)] + mesh = discretize.TensorMesh([h, h, h], origin="CCN") + + # Survey + # ------ + x = np.linspace(-2.0, 2.0, 5) + xx, yy = np.meshgrid(x, x) + zz = 1.0 * np.ones_like(xx) + receiver_locations = np.vstack([c.ravel() for c in (xx, yy, zz)]).T + receivers = gravity.Point(locations=receiver_locations, components="gz") + source_field = gravity.SourceField([receivers]) + survey = gravity.Survey(source_field) + + # Simulation + # ---------- + mapping = simpeg.maps.IdentityMap(mesh=mesh) + simulation = gravity.Simulation3DIntegral( + mesh=mesh, survey=survey, rhoMap=mapping, engine="choclo" + ) + + # Synthetic data + # -------------- + model = np.zeros(mesh.n_cells) + model = simpeg.utils.model_builder.add_block( + mesh.cell_centers, + model, + p0=[-1.0, -1.0, -2.0], + p1=[1.0, 1.0, -1.0], + prop_value=200, + ) + synthetic_data = simulation.make_synthetic_data( + model, + relative_error=0.1, + random_seed=4, + add_noise=True, + ) + + # Inversion problem + # ----------------- + data_misfit = simpeg.data_misfit.L2DataMisfit( + data=synthetic_data, simulation=simulation + ) + regularization = simpeg.regularization.WeightedLeastSquares(mesh) + optimizer = optimization.ProjectedGNCG() + inv_prob = simpeg.inverse_problem.BaseInvProblem( + data_misfit, regularization, optimizer + ) + + return inv_prob + + def get_directives(self, save_output_directive: directives.SaveEveryIteration): + """ + Get list of directives to use in the sample gravity inversion. + + Include the save_output_directive passed as argument in the list. + """ + sensitivity_weights = simpeg.directives.UpdateSensitivityWeights( + every_iteration=False + ) + update_jacobi = simpeg.directives.UpdatePreconditioner( + update_every_iteration=True + ) + starting_beta = simpeg.directives.BetaEstimate_ByEig(beta0_ratio=10) + beta_schedule = simpeg.directives.BetaSchedule(coolingFactor=2.0, coolingRate=1) + target_misfit = simpeg.directives.TargetMisfit(chifact=1.0) + + directives_list = [ + sensitivity_weights, + starting_beta, + update_jacobi, + beta_schedule, + save_output_directive, + target_misfit, + ] + return directives_list + + +class TestSaveOutputEveryIteration(BaseTestOutputDirective): + """ + Test the SaveOutputEveryIteration directive. + """ + + @pytest.mark.parametrize("on_disk", [True, False]) + def test_initialize(self, tmp_path, on_disk): + """Test the initialize method.""" + directory = tmp_path / "dummy" + directive = directives.SaveOutputEveryIteration( + on_disk=on_disk, directory=directory + ) + directive.initialize() + + # Check directory was created + if on_disk: + assert directory.exists() + + # Check that the file was created + assert directive.file_abs_path is not None + assert directive.file_abs_path.exists() + + # Check header in file + with directive.file_abs_path.open(mode="r") as f: + lines = f.readlines() + assert len(lines) == 1 + assert "beta" in lines[0] + assert "phi_d" in lines[0] + assert "phi_m" in lines[0] + else: + assert directive.file_abs_path is None + + assert directive.beta == [] + assert directive.phi_d == [] + assert directive.phi_m == [] + assert directive.phi_m_smooth_z == [] + assert directive.phi == [] + + @pytest.mark.parametrize( + ("on_disk", "test_load_results"), + [ + pytest.param( + True, + True, + marks=pytest.mark.xfail( + reason="bug in load_results", raises=AttributeError + ), + ), + (True, False), + (False, None), + ], + ids=["on_disk-test_load_results", "on_disk", "not_on_disk"], + ) + def test_end_iter(self, tmp_path, on_disk, test_load_results): + """Test the endIter method.""" + inv_prob = self.get_inversion_problem() + + directory = tmp_path / "dummy" + directive = directives.SaveOutputEveryIteration( + directory=directory, on_disk=on_disk + ) + directives_list = self.get_directives(directive) + inversion = simpeg.inversion.BaseInversion(inv_prob, directives_list) + + initial_model = np.zeros(inv_prob.dmisfit.nP) + inversion.run(initial_model) + + # Check that lists are not empty + lists = [ + "beta", + "phi_d", + "phi_m", + "phi_m_small", + "phi_m_smooth_x", + "phi_m_smooth_y", + "phi_m_smooth_z", + "phi", + ] + for attribute in lists: + assert getattr(directive, attribute) + + # Just exit the test if on_disk is False + if not on_disk: + return + + # Check that the file was created if on_disk + assert directive.file_abs_path is not None + assert directive.file_abs_path.exists() + + # Check content of file + with directive.file_abs_path.open(mode="r") as f: + lines = f.readlines() + assert "beta" in lines[0] + assert "phi_d" in lines[0] + assert "phi_m" in lines[0] + assert len(lines) > 1 + + # Test load_results + if test_load_results: + original_values = {attr: getattr(directive, attr) for attr in lists} + for attribute in lists: + # Clean the lists + setattr(directive, attribute, []) + + # Load results and check if they are the same as the original ones + directive.load_results() + + # Check that the original values were recovered + for attribute in lists: + np.testing.assert_equal( + getattr(directive, attribute), + original_values[attribute], + ) + + def test_load_results_error(self, tmp_path): + """ + Test error when no file_name is passed to load_results. + """ + directory = tmp_path / "dummy" + directive = directives.SaveOutputEveryIteration( + directory=directory, on_disk=False + ) + msg = re.escape("'file_name' is a required argument") + with pytest.raises(TypeError, match=msg): + directive.load_results() + + +class TestSaveOutputDictEveryIteration(BaseTestOutputDirective): + """ + Test the SaveOutputDictEveryIteration directive. + """ + + def test_initialize(self): + """Test the initialize method.""" + directive = directives.SaveOutputDictEveryIteration() + directive.initialize() + + # Check outDict was created and is empty + assert hasattr(directive, "outDict") + assert not directive.outDict + + @pytest.mark.parametrize("on_disk", [True, False], ids=["on_disk", "not_on_disk"]) + def test_end_iter(self, tmp_path, on_disk): + """Test the endIter method.""" + inv_prob = self.get_inversion_problem() + + directory = tmp_path / "dummy" + directive = directives.SaveOutputDictEveryIteration( + directory=directory, on_disk=on_disk + ) + directives_list = self.get_directives(directive) + inversion = simpeg.inversion.BaseInversion(inv_prob, directives_list) + + initial_model = np.zeros(inv_prob.dmisfit.nP) + inversion.run(initial_model) + + # Check if the outDict is not empty + assert directive.outDict + fields = [ + "iter", + "beta", + "phi_d", + "phi_m", + "f", + "m", + "dpred", + ] + for iteration in directive.outDict: + for field in fields: + assert field in directive.outDict[iteration] + + # Check if output files were created + if on_disk: + assert directory.exists() + assert directory.is_dir() + files = list(directory.iterdir()) + assert len(files) == len(directive.outDict) + + def test_deprecated(self): + with pytest.warns(FutureWarning, match=".*saveOnDisk has been deprecated.*"): + directive = directives.SaveOutputDictEveryIteration(saveOnDisk=True) + + assert directive.on_disk + + @pytest.mark.parametrize("on_disk", [True, False], ids=["on_disk", "not_on_disk"]) + def test_file_abs_path_optional(self, on_disk): + directive = directives.SaveOutputDictEveryIteration(on_disk=on_disk) + if on_disk: + assert directive.file_abs_path is not None + else: + assert directive.file_abs_path is None + + +class MockJointInvProb: + + def __init__(self): + self.opt = namedtuple("Opt", "f iter cg_count")(0.1, 10, 200) + self.betas = [1, 2, 3] + self.phi_d_list = [0.1, 0.2, 0.3] + self.phi_m_list = [0.2, 0.3, 0.4] + self.lambd = 1e5 + self.phi_sim = 10 + + +class TestSimMeasureSaveOutputEveryIteration: + + @pytest.mark.parametrize("on_disk", [True, False]) + def test_initialize(self, tmp_path, on_disk): + """Test the initialize method.""" + directory = tmp_path / "dummy" + directive = directives.SimilarityMeasureSaveOutputEveryIteration( + on_disk=on_disk, directory=directory + ) + directive.initialize() + + # Check directory was created + if on_disk: + assert directory.exists() + + # Check that the file was created + assert directive.file_abs_path is not None + assert directive.file_abs_path.exists() + + # Check header in file + with directive.file_abs_path.open(mode="r") as f: + lines = f.readlines() + assert len(lines) == 1 + assert "betas" in lines[0] + assert "joint_phi_d" in lines[0] + assert "joint_phi_m" in lines[0] + assert "phi_sim" in lines[0] + else: + assert directive.file_abs_path is None + + assert directive.betas == [] + assert directive.lambd == [] + assert directive.phi_d == [] + assert directive.phi_m == [] + assert directive.phi_sim == [] + assert directive.phi == [] + + @pytest.mark.parametrize("on_disk", [True, False]) + def test_end_iter(self, tmp_path, on_disk): + directory = tmp_path / "dummy" + directive = directives.SimilarityMeasureSaveOutputEveryIteration( + on_disk=on_disk, directory=directory + ) + directive.initialize() + + joint_problem = MockJointInvProb() + joint_inv = namedtuple("JointInversion", "invProb")(joint_problem) + directive.inversion = joint_inv + + assert directive.invProb is joint_inv.invProb + + directive.endIter() + + assert directive.betas == [joint_problem.betas] + assert directive.phi_d == [joint_problem.phi_d_list] + assert directive.phi_m == [joint_problem.phi_m_list] + assert directive.lambd == [joint_problem.lambd] + assert directive.phi_sim == [joint_problem.phi_sim] + assert directive.phi == [joint_problem.opt.f] + + if on_disk: + out_file = directive.file_abs_path + assert out_file.exists() + + n_lines = 0 + with open(out_file) as f: + while f.readline(): + n_lines += 1 + assert n_lines == 2 # header plus one line + + @pytest.mark.xfail( + reason="np.loadtxt will not work to read in log file that has nested lists." + ) + @pytest.mark.parametrize("pass_file_name", [True, False]) + def test_load_results(self, tmp_path, pass_file_name): + directory = tmp_path / "dummy" + directive = directives.SimilarityMeasureSaveOutputEveryIteration( + directory=directory, on_disk=True + ) + directive.initialize() + + joint_problem = MockJointInvProb() + joint_inv = namedtuple("JointInversion", "invProb")(joint_problem) + directive.inversion = joint_inv + + directive.endIter() + + if pass_file_name: + log_file = directive.file_abs_path + directive.load_results(log_file) + else: + directive.load_results() + + assert directive.betas == [joint_problem.betas] + assert directive.phi_d == [joint_problem.phi_d_list] + assert directive.phi_m == [joint_problem.phi_m_list] + assert directive.lambd == [joint_problem.lambd] + assert directive.phi_sim == [joint_problem.phi_sim] + assert directive.phi == [joint_problem.opt.f] + + def test_load_results_error(self): + directive = directives.SimilarityMeasureSaveOutputEveryIteration(on_disk=False) + with pytest.raises(TypeError, match=r"'file_name' is a required argument.*"): + directive.load_results() + + if __name__ == "__main__": unittest.main() From 5e877979d60c0ec7aba174a2f914f8f1de50b8a4 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Fri, 12 Sep 2025 16:23:06 -0600 Subject: [PATCH 160/194] Make tests error on implicit complex to real (#1696) Makes pytest error on implicit conversions of complex to real values. --- pyproject.toml | 1 + simpeg/electromagnetics/frequency_domain/simulation_1d.py | 6 +++--- simpeg/electromagnetics/time_domain/simulation_1d.py | 7 ++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2bede500cc..001a3d82ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -261,5 +261,6 @@ filterwarnings = [ "error:You are running a pytest without setting a random seed.*:UserWarning", "error:The `index_dictionary` property has been deprecated:FutureWarning", 'error:The `simpeg\.directives\.[a-z_]+` submodule has been deprecated', + 'error:Casting complex values to real discards the imaginary part', ] xfail_strict = true diff --git a/simpeg/electromagnetics/frequency_domain/simulation_1d.py b/simpeg/electromagnetics/frequency_domain/simulation_1d.py index 57806b6037..c51e8e3d33 100644 --- a/simpeg/electromagnetics/frequency_domain/simulation_1d.py +++ b/simpeg/electromagnetics/frequency_domain/simulation_1d.py @@ -194,9 +194,9 @@ def _getJ(self, m, f=None): rTE = rTE_forward(frequencies, unique_lambs, sig, mu, self.thicknesses) rTE = rTE[i_freq] rTE = np.take_along_axis(rTE, inv_lambs, axis=1) - v_dh_temp = (C0s_dh * rTE) @ self._fhtfilt.j0 + ( - C1s_dh * rTE - ) @ self._fhtfilt.j1 + v_dh_temp = ((C0s_dh * rTE) @ self._fhtfilt.j0).real + ( + (C1s_dh * rTE) @ self._fhtfilt.j1 + ).real v_dh_temp += W @ v_dh_temp # need to re-arange v_dh as it's currently (n_data x 1) # however it already contains all the relevant information... diff --git a/simpeg/electromagnetics/time_domain/simulation_1d.py b/simpeg/electromagnetics/time_domain/simulation_1d.py index 1e72af8527..a62bb9701e 100644 --- a/simpeg/electromagnetics/time_domain/simulation_1d.py +++ b/simpeg/electromagnetics/time_domain/simulation_1d.py @@ -243,7 +243,8 @@ def fields(self, m): sig = self.compute_complex_sigma(frequencies) mu = self.compute_complex_mu(frequencies) - rTE = rTE_forward(frequencies, unique_lambs, sig, mu, self.thicknesses) + # TODO geoana currently only support real mu input. + rTE = rTE_forward(frequencies, unique_lambs, sig, mu.real, self.thicknesses) rTE = rTE[:, inv_lambs] v = ((C0s * rTE) @ self._fhtfilt.j0 + (C1s * rTE) @ self._fhtfilt.j1) @ W.T @@ -308,8 +309,8 @@ def _getJ(self, m, f=None): v_dh_temp = ( W @ ( - (C0s_dh * rTE) @ self._fhtfilt.j0 - + (C1s_dh * rTE) @ self._fhtfilt.j1 + ((C0s_dh * rTE) @ self._fhtfilt.j0).real + + ((C1s_dh * rTE) @ self._fhtfilt.j1).real ).T ) # need to re-arange v_dh as it's currently (n_data x n_freqs) From fbea6dbcbe837a5e550ec16397e9ce2260920c62 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Fri, 12 Sep 2025 18:06:48 -0600 Subject: [PATCH 161/194] Avoids calculating unused values for boundary conditions on DC 2D simulations (#1698) Avoid performing calculations for DC boundary conditions on the top of the mesh, where the simulation always explicitly uses a zero Neumann condition. --- .../static/resistivity/simulation_2d.py | 67 ++++++++++--------- 1 file changed, 36 insertions(+), 31 deletions(-) diff --git a/simpeg/electromagnetics/static/resistivity/simulation_2d.py b/simpeg/electromagnetics/static/resistivity/simulation_2d.py index 433d6f00f5..5657af8988 100644 --- a/simpeg/electromagnetics/static/resistivity/simulation_2d.py +++ b/simpeg/electromagnetics/static/resistivity/simulation_2d.py @@ -574,23 +574,7 @@ def setBC(self, ky=None): else: mesh = self.mesh boundary_faces = mesh.boundary_faces - boundary_normals = mesh.boundary_face_outward_normals - n_bf = len(boundary_faces) - - # Top gets 0 Neumann - alpha = np.zeros(n_bf) - beta = np.ones(n_bf) - gamma = 0 - - # assume a source point at the middle of the top of the mesh - middle = np.median(mesh.nodes, axis=0) top_v = np.max(mesh.nodes[:, -1]) - source_point = np.r_[middle[:-1], top_v] - - r_vec = boundary_faces - source_point - r = np.linalg.norm(r_vec, axis=-1) - r_hat = r_vec / r[:, None] - r_dot_n = np.einsum("ij,ij->i", r_hat, boundary_normals) if self.surface_faces is None: # determine faces that are on the sides and bottom of the mesh... @@ -612,11 +596,29 @@ def setBC(self, ky=None): else: not_top = ~self.surface_faces + n_bf = len(boundary_faces) + + # Top gets 0 Neumann + alpha = np.zeros(n_bf) + beta = np.ones(n_bf) + gamma = 0 + + # assume a source point at the middle of the top of the mesh + middle = np.median(mesh.nodes, axis=0) + source_point = np.r_[middle[:-1], top_v] + + boundary_faces = boundary_faces[not_top] + boundary_normals = mesh.boundary_face_outward_normals[not_top] + r_vec = boundary_faces - source_point + r = np.linalg.norm(r_vec, axis=-1) + r_hat = r_vec / r[:, None] # small stabilizer to avoid divide by zero + r_dot_n = np.einsum("ij,ij->i", r_hat, boundary_normals) + # use the exponentialy scaled modified bessel function of second kind, # (the division will cancel out the scaling) # This is more stable for large values of ky * r # actual ratio is k1/k0... - alpha[not_top] = (ky * k1e(ky * r) / k0e(ky * r) * r_dot_n)[not_top] + alpha[not_top] = ky * k1e(ky * r) / k0e(ky * r) * r_dot_n B, bc = self.mesh.cell_gradient_weak_form_robin(alpha, beta, gamma) # bc should always be 0 because gamma was always 0 above @@ -753,20 +755,7 @@ def setBC(self, ky=None): mesh = self.mesh # calculate alpha, beta, gamma at the boundary faces boundary_faces = mesh.boundary_faces - boundary_normals = mesh.boundary_face_outward_normals - n_bf = len(boundary_faces) - - alpha = np.zeros(n_bf) - - # assume a source point at the middle of the top of the mesh - middle = np.median(mesh.nodes, axis=0) top_v = np.max(mesh.nodes[:, -1]) - source_point = np.r_[middle[:-1], top_v] - - r_vec = boundary_faces - source_point - r = np.linalg.norm(r_vec, axis=-1) - r_hat = r_vec / r[:, None] - r_dot_n = np.einsum("ij,ij->i", r_hat, boundary_normals) if self.surface_faces is None: # determine faces that are on the sides and bottom of the mesh... @@ -788,11 +777,27 @@ def setBC(self, ky=None): else: not_top = ~self.surface_faces + n_bf = len(boundary_faces) + + boundary_faces = boundary_faces[not_top] + boundary_normals = mesh.boundary_face_outward_normals[not_top] + + alpha = np.zeros(n_bf) + + # assume a source point at the middle of the top of the mesh + middle = np.median(mesh.nodes, axis=0) + source_point = np.r_[middle[:-1], top_v] + + r_vec = boundary_faces - source_point + r = np.linalg.norm(r_vec, axis=-1) + r_hat = r_vec / r[:, None] + r_dot_n = np.einsum("ij,ij->i", r_hat, boundary_normals) + # use the exponentiall scaled modified bessel function of second kind, # (the division will cancel out the scaling) # This is more stable for large values of ky * r # actual ratio is k1/k0... - alpha[not_top] = (ky * k1e(ky * r) / k0e(ky * r) * r_dot_n)[not_top] + alpha[not_top] = ky * k1e(ky * r) / k0e(ky * r) * r_dot_n P_bf = self.mesh.project_face_to_boundary_face From a3d45aa33bbccd9913bcf6bbfb2aff2a014cb691 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 12 Sep 2025 17:08:25 -0700 Subject: [PATCH 162/194] Add How to Guide page on how to choose a solver (#1695) Create a new section in the User Guide: a How to Guide. Add a new document on how to choose a solver for PDE simulations. --------- Co-authored-by: Joseph Capriotti --- .../how-to-guide/choosing-solvers.rst | 110 ++++++++++++++++++ docs/content/user-guide/index.rst | 7 ++ 2 files changed, 117 insertions(+) create mode 100644 docs/content/user-guide/how-to-guide/choosing-solvers.rst diff --git a/docs/content/user-guide/how-to-guide/choosing-solvers.rst b/docs/content/user-guide/how-to-guide/choosing-solvers.rst new file mode 100644 index 0000000000..b46c12da9e --- /dev/null +++ b/docs/content/user-guide/how-to-guide/choosing-solvers.rst @@ -0,0 +1,110 @@ +.. _choosing-solvers: + +================ +Choosing solvers +================ + +Several simulations available in SimPEG need to numerically solve a partial +differential equations system (PDE), such as +:class:`~simpeg.electromagnetics.static.resistivity.Simulation3DNodal` (DC) +:class:`~simpeg.electromagnetics.time_domain.Simulation3DMagneticFluxDensity` +(TDEM) +and +:class:`~simpeg.electromagnetics.frequency_domain.Simulation3DMagneticFluxDensity` +(FDEM). +A numerical solver is needed to solve the PDEs. +SimPEG can make use of the solvers available in :mod:`pymatsolver`, like +:class:`pymatsolver.Pardiso`, :class:`pymatsolver.Mumps` or +:class:`pymatsolver.SolverLU`. +The choice of an appropriate solver can affect the computation time required to +solve the PDE. Generally we recommend using direct solvers over iterative solvers +for SimPEG, but be aware that direct solvers have much larger memory requirements. + +The ``Pardiso`` solver wraps the `oneMKL PARDISO +`_ +solver available for x86_64 CPUs. + +The ``Mumps`` solver wraps `MUMPS +`_, a fast solver available for +all CPU brands, including Apple silicon architecture. + +The ``SolverLU`` wraps SciPy's :func:`scipy.sparse.linalg.splu`. The +performance of this solver is not up to the level of ``Mumps`` and ``Pardiso``. +Usage of the ``SolveLU`` is recommended only when it's not possible to use +other faster solvers. + + +The default solver +------------------ + +We can use :func:`simpeg.utils.get_default_solver` to obtain a reasonable default +solver available for our system: + +.. code:: python + + import simpeg + import simpeg.electromagnetics.static.resistivity as dc + + # Get default solver + solver = simpeg.utils.get_default_solver() + print(f"Solver: {solver}") + +which would print out on an x86_64 cpu: + +.. code:: + + Solver: + +We can then use this solver in a simulation: + +.. code:: python + + # Define a simple mesh + h = [(1.0, 10)] + mesh = discretize.TensorMesh([h, h, h], origin="CCC") + + # And a DC survey + receiver = dc.receivers.Dipole(locations_m=(-1, 0, 0), locations_n=(1, 0, 0)) + source = dc.sources.Dipole( + receiver_list=[receiver], location_a=(-2, 0, 0), location_b=(2, 0, 0) + ) + survey = dc.Survey([source]) + + # Use the default solver in the simulation + simulation = dc.Simulation3DNodal(mesh=mesh, survey=survey, solver=solver) + +.. note:: + + The priority list used to choose a default solver is: + + 1) ``Pardiso`` + 2) ``Mumps`` + 3) ``SolverLU`` + + +Setting solvers manually +------------------------ + +Alternatively, we can manually set a solver. For example, if we want to use +``Mumps`` in our DC resistivity simulation, we can import +:class:`pymatsolver.Mumps` and pass it to our simulation: + +.. code:: python + + import simpeg.electromagnetics.static.resistivity as dc + from pymatsolver import Mumps + + # Manually set Mumps as our solver + simulation = dc.Simulation3DNodal(mesh=mesh, survey=survey, solver=Mumps) + +.. note:: + + When sharing your notebook or script with a colleague, keep in mind that + your code might not work if ``Pardiso`` is not available in their system. + + For such scenarios, we recommend using the + :func:`simpeg.utils.get_default_solver` function, that will always return + a suitable solver for the current system. + +Ultimately, choosing the best solver is a mixture of the problem you are solving and your current system. Experiment with different solvers yourself to choose the best. + diff --git a/docs/content/user-guide/index.rst b/docs/content/user-guide/index.rst index 801fe06370..4581bbe166 100644 --- a/docs/content/user-guide/index.rst +++ b/docs/content/user-guide/index.rst @@ -18,6 +18,13 @@ For details on the available classes and functions in SimPEG, please visit the getting-started/installing getting-started/contributing/index.rst +.. toctree:: + :glob: + :maxdepth: 1 + :caption: How to Guide + + how-to-guide/choosing-solvers + .. toctree:: :glob: :maxdepth: 1 From e1af3e81af37d9e2dbe9fbe7a4b6eae48f2e05d0 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Mon, 15 Sep 2025 12:25:45 -0600 Subject: [PATCH 163/194] Make Logger a bit quieter when running pytest (#1697) Make Logger a bit quieter when running pytest by using a session wide auto-used fixture. --- tests/base/test_base_pde_sim.py | 2 +- tests/conftest.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 tests/conftest.py diff --git a/tests/base/test_base_pde_sim.py b/tests/base/test_base_pde_sim.py index ff750385d5..e9406d1339 100644 --- a/tests/base/test_base_pde_sim.py +++ b/tests/base/test_base_pde_sim.py @@ -814,7 +814,7 @@ def test_bad_derivative_stash(): sim.MeSigmaDeriv(u, v) -def test_solver_defaults(caplog): +def test_solver_defaults(caplog, info_logging): mesh = discretize.TensorMesh([2, 2, 2]) sim = BasePDESimulation(mesh) # Check that logging.info was created diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000000..6947cdffd6 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,30 @@ +import pytest +from simpeg.utils import get_logger +import logging + + +@pytest.fixture(scope="session", autouse=True) +def quiet_logger_for_tests(request): + logger = get_logger() + + init_level = logger.level + # default solver log is issued at the INFO level. + # set the logger to the higher WARNING level to + # ignore the default solver messages. + logger.setLevel(logging.WARNING) + + yield + + logger.setLevel(init_level) + + +@pytest.fixture() +def info_logging(): + # provide a fixture to temporarily set the logging level to info + logger = get_logger() + init_level = logger.level + logger.setLevel(logging.INFO) + + yield + + logger.setLevel(init_level) From 0fe4df8c5663e676721f8a3d707620ae601fdd9e Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Wed, 17 Sep 2025 18:01:28 -0600 Subject: [PATCH 164/194] CG Minimizer Updates (#1656) #### Summary Contains several updates for the CG based minimizer routines and their options to bring in line with more recent versions of SciPy. Also contains a bit of common code movement by using a `Bounded` and an `InexactCG` mix in classes for the minimizers. --- .ci/environment_test.yml | 2 +- environment.yml | 2 +- pyproject.toml | 2 +- simpeg/directives/_directives.py | 11 +- simpeg/inverse_problem.py | 65 ++-- simpeg/inversion.py | 9 +- simpeg/optimization.py | 589 +++++++++++++++++++++++-------- tests/base/test_directives.py | 10 + tests/base/test_inversion.py | 69 ++++ tests/base/test_optimizers.py | 542 +++++++++++++++++++++++++--- 10 files changed, 1062 insertions(+), 239 deletions(-) create mode 100644 tests/base/test_inversion.py diff --git a/.ci/environment_test.yml b/.ci/environment_test.yml index da07821f9a..2d8b61fb49 100644 --- a/.ci/environment_test.yml +++ b/.ci/environment_test.yml @@ -3,7 +3,7 @@ channels: - conda-forge dependencies: - numpy>=1.22 - - scipy>=1.8 + - scipy>=1.12 - pymatsolver>=0.3 - matplotlib-base - discretize>=0.11 diff --git a/environment.yml b/environment.yml index 66cb9d9c41..70b94106f4 100644 --- a/environment.yml +++ b/environment.yml @@ -5,7 +5,7 @@ dependencies: # dependencies - python=3.11 - numpy>=1.22 - - scipy>=1.8 + - scipy>=1.12 - pymatsolver>=0.3 - matplotlib-base - discretize>=0.11 diff --git a/pyproject.toml b/pyproject.toml index 001a3d82ba..773936cfae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ keywords = [ ] dependencies = [ "numpy>=1.22", - "scipy>=1.8", + "scipy>=1.12", "pymatsolver>=0.3", "matplotlib", "discretize>=0.11", diff --git a/simpeg/directives/_directives.py b/simpeg/directives/_directives.py index 77b3a3c5af..3ff9b94bf4 100644 --- a/simpeg/directives/_directives.py +++ b/simpeg/directives/_directives.py @@ -257,12 +257,12 @@ class DirectiveList(object): Parameters ---------- - directives : list of simpeg.directives.InversionDirective - List of directives. + *directives : simpeg.directives.InversionDirective + Directives for the inversion. inversion : simpeg.inversion.BaseInversion The inversion associated with the directives list. debug : bool - Whether or not to print debugging information. + Whether to print debugging information. """ @@ -333,9 +333,12 @@ def call(self, ruleType): getattr(r, ruleType)() def validate(self): - [directive.validate(self) for directive in self.dList] + [directive.validate(self) for directive in self] return True + def __iter__(self): + return iter(self.dList) + class BaseBetaEstimator(InversionDirective): """Base class for estimating initial trade-off parameter (beta). diff --git a/simpeg/inverse_problem.py b/simpeg/inverse_problem.py index 1bd2ae974e..4a8ceb3bf2 100644 --- a/simpeg/inverse_problem.py +++ b/simpeg/inverse_problem.py @@ -4,7 +4,7 @@ from .data_misfit import BaseDataMisfit from .regularization import BaseRegularization, WeightedLeastSquares, Sparse from .objective_function import BaseObjectiveFunction, ComboObjectiveFunction -from .optimization import Minimize +from .optimization import Minimize, BFGS from .utils import ( call_hooks, timeIt, @@ -29,6 +29,7 @@ def __init__( debug=False, counter=None, print_version=True, + init_bfgs=True, **kwargs, ): super().__init__(**kwargs) @@ -45,6 +46,7 @@ def __init__( self.counter = counter self.model = None self.print_version = print_version + self.init_bfgs = init_bfgs # TODO: Remove: (and make iteration printers better!) self.opt.parent = self self.reg.parent = self @@ -175,6 +177,15 @@ def model(self, value): delattr(self, prop) self._model = value + @property + def init_bfgs(self): + """Initialize BFGS minimizers with the inverse of the regularization's Hessian.""" + return self._init_bfgs + + @init_bfgs.setter + def init_bfgs(self, value): + self._init_bfgs = validate_type("init_bfgs", value, bool) + @call_hooks("startup") def startup(self, m0): """startup(m0) @@ -203,38 +214,40 @@ def startup(self, m0): self.model = m0 set_default = True - for objfct in self.dmisfit.objfcts: - if ( - isinstance(objfct, BaseDataMisfit) - and getattr(objfct.simulation, "solver", None) is not None - ): - solver = objfct.simulation.solver - solver_opts = objfct.simulation.solver_opts + + if self.init_bfgs and isinstance(self.opt, BFGS): + for objfct in self.dmisfit.objfcts: + if ( + isinstance(objfct, BaseDataMisfit) + and getattr(objfct.simulation, "solver", None) is not None + ): + solver = objfct.simulation.solver + solver_opts = objfct.simulation.solver_opts + print( + """ + simpeg.InvProblem is setting bfgsH0 to the inverse of the eval2Deriv. + ***Done using same Solver, and solver_opts as the {} problem*** + """.format( + objfct.simulation.__class__.__name__ + ) + ) + set_default = False + break + if set_default: + solver = get_default_solver() print( """ simpeg.InvProblem is setting bfgsH0 to the inverse of the eval2Deriv. - ***Done using same Solver, and solver_opts as the {} problem*** + ***Done using the default solver {} and no solver_opts.*** """.format( - objfct.simulation.__class__.__name__ + solver.__name__ ) ) - set_default = False - break - if set_default: - solver = get_default_solver() - print( - """ - simpeg.InvProblem is setting bfgsH0 to the inverse of the eval2Deriv. - ***Done using the default solver {} and no solver_opts.*** - """.format( - solver.__name__ - ) - ) - solver_opts = {} + solver_opts = {} - self.opt.bfgsH0 = solver( - sp.csr_matrix(self.reg.deriv2(self.model)), **solver_opts - ) + self.opt.bfgsH0 = solver( + sp.csr_matrix(self.reg.deriv2(self.model)), **solver_opts + ) @property def warmstart(self): diff --git a/simpeg/inversion.py b/simpeg/inversion.py index a3e51cf541..9b1a6730ea 100644 --- a/simpeg/inversion.py +++ b/simpeg/inversion.py @@ -1,7 +1,7 @@ import numpy as np -from .optimization import IterationPrinters, StoppingCriteria -from .directives import DirectiveList +from .optimization import IterationPrinters, StoppingCriteria, InexactGaussNewton +from .directives import DirectiveList, UpdatePreconditioner from .utils import timeIt, Counter, validate_type, validate_string @@ -107,6 +107,11 @@ def run(self, m0): Runs the inversion! """ + if isinstance(self.opt, InexactGaussNewton) and any( + isinstance(drctv, UpdatePreconditioner) for drctv in self.directiveList + ): + self.invProb.init_bfgs = False + self.invProb.startup(m0) self.directiveList.call("initialize") print("model has any nan: {:b}".format(np.any(np.isnan(self.invProb.model)))) diff --git a/simpeg/optimization.py b/simpeg/optimization.py index dd5bd1552e..2ed5474429 100644 --- a/simpeg/optimization.py +++ b/simpeg/optimization.py @@ -1,5 +1,7 @@ +import warnings + import numpy as np -import scipy +import numpy.typing as npt import scipy.sparse as sp from pymatsolver import Solver, Diagonal, SolverCG @@ -13,28 +15,16 @@ print_line, print_stoppers, print_done, + validate_float, + validate_integer, + validate_type, + validate_ndarray_with_shape, + deprecate_property, ) norm = np.linalg.norm -# Create a flag if the installed version of SciPy is newer or equal to 1.12.0 -# (Used to choose whether to pass `tol` or `rtol` to the solvers. See #1516). -class Version: - def __init__(self, version): - self.version = version - - def as_tuple(self) -> tuple[int, int]: - major, minor = tuple(int(p) for p in self.version.split(".")[:2]) - return (major, minor) - - def __ge__(self, other): - return self.as_tuple() >= other.as_tuple() - - -SCIPY_1_12 = Version(scipy.__version__) >= Version("1.12.0") - - __all__ = [ "Minimize", "Remember", @@ -248,6 +238,22 @@ class IterationPrinters(object): "format": "%3d", } + iteration_CG_rel_residual = { + "title": "CG |Ax-b|/|b|", + "value": lambda M: M.cg_rel_resid, + "width": 15, + "format": "%1.2e", + # "format": lambda v: f"{v:1.2e}", + } + + iteration_CG_abs_residual = { + "title": "CG |Ax-b|", + "value": lambda M: M.cg_abs_resid, + "width": 11, + "format": "%1.2e", + # "format": lambda v: f"{v:1.2e}", + } + class Minimize(object): """ @@ -341,7 +347,7 @@ def callback(self, value): self._callback = value @timeIt - def minimize(self, evalFunction, x0): + def minimize(self, evalFunction, x0) -> np.ndarray: """minimize(evalFunction, x0) Minimizes the function (evalFunction) starting at the location x0. @@ -440,7 +446,11 @@ def startup(self, x0): self.iterLS = 0 self.stopNextIteration = False - x0 = self.projection(x0) # ensure that we start of feasible. + try: + x0 = self.projection(x0) # ensure that we start of feasible. + except Exception as err: + raise RuntimeError("Initial model is not projectable") from err + self.x0 = x0 self.xc = x0 self.f_last = np.nan @@ -778,66 +788,93 @@ def _doEndIterationRemember(self, *args): self._rememberList[param[0]].append(param[1](self)) -class ProjectedGradient(Minimize, Remember): - name = "Projected Gradient" - - maxIterCG = 5 - tolCG = 1e-1 +class Bounded(object): + """Mixin class for bounded minimizers - lower = -np.inf - upper = np.inf + Parameters + ---------- + lower, upper : float or numpy.ndarray, optional + The lower and upper bounds. + """ - def __init__(self, **kwargs): - super(ProjectedGradient, self).__init__(**kwargs) + def __init__( + self, + *, + lower: None | float | npt.NDArray[np.float64], + upper: None | float | npt.NDArray[np.float64] = None, + **kwargs, + ): + self.lower = lower + self.upper = upper + super().__init__(**kwargs) - self.stoppers.append(StoppingCriteria.bindingSet) - self.stoppersLS.append(StoppingCriteria.bindingSet_LS) + @property + def lower(self) -> None | float | npt.NDArray[np.float64]: + """The lower bound value. - self.printers.extend( - [ - IterationPrinters.itType, - IterationPrinters.aSet, - IterationPrinters.bSet, - IterationPrinters.comment, - ] - ) + Returns + ------- + lower : None, float, numpy.ndarray + """ + return self._lower - def _startup(self, x0): - # ensure bound vectors are the same size as the model - if not isinstance(self.lower, np.ndarray): - self.lower = np.ones_like(x0) * self.lower - if not isinstance(self.upper, np.ndarray): - self.upper = np.ones_like(x0) * self.upper + @lower.setter + def lower(self, value): + if value is not None: + try: + value = validate_float("lower", value) + except TypeError: + value = validate_ndarray_with_shape("lower", value, shape=("*",)) + self._lower = value - self.explorePG = True - self.exploreCG = False - self.stopDoingPG = False + @property + def upper(self) -> None | float | npt.NDArray[np.float64]: + """The upper bound value. - self._itType = "SD" - self.comment = "" + Returns + ------- + upper : None, float, numpy.ndarray + """ + return self._upper - self.aSet_prev = self.activeSet(x0) + @upper.setter + def upper(self, value): + if value is not None: + try: + value = validate_float("upper", value) + except TypeError: + value = validate_ndarray_with_shape("upper", value, shape=("*",)) + self._upper = value @count - def projection(self, x): + def projection(self, x: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]: """projection(x) Make sure we are feasible. """ - return np.median(np.c_[self.lower, x, self.upper], axis=1) + if self.lower is not None: + x = np.maximum(x, self.lower) + if self.upper is not None: + x = np.minimum(x, self.upper) + return x @count - def activeSet(self, x): + def activeSet(self, x: npt.NDArray[np.float64]) -> npt.NDArray[bool]: """activeSet(x) If we are on a bound """ - return np.logical_or(x == self.lower, x == self.upper) + out = np.zeros(x.shape, dtype=bool) + if self.lower is not None: + out |= x <= self.lower + if self.upper is not None: + out |= x >= self.upper + return out @count - def inactiveSet(self, x): + def inactiveSet(self, x: npt.NDArray[np.float64]) -> npt.NDArray[bool]: """inactiveSet(x) The free variables. @@ -846,7 +883,7 @@ def inactiveSet(self, x): return np.logical_not(self.activeSet(x)) @count - def bindingSet(self, x): + def bindingSet(self, x: npt.NDArray[np.float64]) -> npt.NDArray[bool]: """bindingSet(x) If we are on a bound and the negative gradient points away from the @@ -855,9 +892,160 @@ def bindingSet(self, x): Optimality condition. (Satisfies Kuhn-Tucker) MoreToraldo91 """ - bind_up = np.logical_and(x == self.lower, self.g >= 0) - bind_low = np.logical_and(x == self.upper, self.g <= 0) - return np.logical_or(bind_up, bind_low) + out = np.zeros(x.shape, dtype=bool) + if self.lower is not None: + out |= (x <= self.lower) & (self.g >= 0) + if self.upper is not None: + out |= (x >= self.upper) & (self.g <= 0) + return out + + +class InexactCG(object): + """Mixin to hold common parameters for a CG solver. + + Parameters + ---------- + cg_rtol : float, optional + Relative tolerance stopping condition on the CG residual + cg_atol : float, optional + Absolute tolerance stopping condition on the CG residual + cg_maxiter : int, optional + Maximum number of CG iterations to perform + + Notes + ----- + + The convergence check for CG is: + >>> norm(A @ x_k - b) <= max(cg_rtol * norm(A @ x_0 - b), cg_atol) + + See Also + -------- + scipy.sparse.linalg.cg + + """ + + def __init__( + self, + *, + cg_rtol: float = 1e-1, + cg_atol: float = 0, + cg_maxiter: int = 5, + **kwargs, + ): + + if (val := kwargs.pop("tolCG", None)) is not None: + self.tolCG = val # Deprecated cg_rtol + else: + self.cg_rtol = cg_rtol + self.cg_atol = cg_atol + + if (val := kwargs.pop("maxIterCG", None)) is not None: + self.maxIterCG = val + else: + self.cg_maxiter = cg_maxiter + + super().__init__(**kwargs) + + @property + def cg_atol(self) -> float: + """Absolute tolerance for inner CG iterations. + + CG iterations are terminated if: + >>> norm(A @ x_k - b) <= max(cg_rtol * norm(A @ x_0 - b), cg_atol) + + or if the maximum number of CG iterations is reached. + + Returns + ------- + float + + See Also + -------- + cg_rtol, scipy.sparse.linalg.cg + """ + return self._cg_atol + + @cg_atol.setter + def cg_atol(self, value): + self._cg_atol = validate_float("cg_atol", value, min_val=0, inclusive_min=True) + + @property + def cg_rtol(self) -> float: + """Relative tolerance for inner CG iterations. + + CG iterations are terminated if: + >>> norm(A @ x_k - b) <= max(cg_rtol * norm(A @ x_0 - b), cg_atol) + + or if the maximum number of CG iterations is reached. + + Returns + ------- + float + + See Also + -------- + cg_rtol, scipy.sparse.linalg.cg + """ + return self._cg_rtol + + @cg_rtol.setter + def cg_rtol(self, value): + self._cg_rtol = validate_float("cg_rtol", value, min_val=0, inclusive_min=True) + + @property + def cg_maxiter(self) -> int: + """Maximum number of CG iterations. + Returns + ------- + int + """ + return self._cg_maxiter + + @cg_maxiter.setter + def cg_maxiter(self, value): + self._cg_maxiter = validate_integer("cg_maxiter", value, min_val=1) + + maxIterCG = deprecate_property( + cg_maxiter, old_name="maxIterCG", removal_version="0.26.0", future_warn=True + ) + tolCG = deprecate_property( + cg_rtol, old_name="tolCG", removal_version="0.26.0", future_warn=True + ) + + +class ProjectedGradient(Bounded, InexactCG, Minimize, Remember): + name = "Projected Gradient" + + def __init__( + self, *, lower=-np.inf, upper=np.inf, cg_rtol=1e-1, cg_maxiter=5, **kwargs + ): + super().__init__( + lower=lower, upper=upper, cg_rtol=cg_rtol, cg_maxiter=cg_maxiter, **kwargs + ) + + self.stoppers.append(StoppingCriteria.bindingSet) + self.stoppersLS.append(StoppingCriteria.bindingSet_LS) + + self.printers.extend( + [ + IterationPrinters.itType, + IterationPrinters.aSet, + IterationPrinters.bSet, + IterationPrinters.comment, + ] + ) + + def startup(self, x0): + super().startup(x0) + + self.explorePG = True + self.exploreCG = False + self.stopDoingPG = False + + self._itType = "SD" + self.comment = "" + + self.aSet_prev = self.activeSet(x0) @timeIt def findSearchDirection(self): @@ -911,11 +1099,13 @@ def reduceHess(v): (shape[1], shape[1]), reduceHess, dtype=self.xc.dtype ) - # Choose `rtol` or `tol` argument based on installed scipy version - tol_key = "rtol" if SCIPY_1_12 else "tol" - - inp = {tol_key: self.tolCG, "maxiter": self.maxIterCG} - p, info = sp.linalg.cg(operator, -Z.T * self.g, **inp) + p, info = sp.linalg.cg( + operator, + -Z.T * self.g, + rtol=self.cg_rtol, + atol=self.cg_atol, + maxiter=self.cg_maxiter, + ) p = Z * p # bring up to full size # aSet_after = self.activeSet(self.xc+p) return p @@ -958,9 +1148,6 @@ class BFGS(Minimize, Remember): name = "BFGS" nbfgs = 10 - def __init__(self, **kwargs): - Minimize.__init__(self, **kwargs) - @property def bfgsH0(self): """ @@ -1042,7 +1229,7 @@ def findSearchDirection(self): return Solver(self.H) * (-self.g) -class InexactGaussNewton(BFGS, Minimize, Remember): +class InexactGaussNewton(InexactCG, BFGS): r""" Minimizes using CG as the inexact solver of @@ -1059,13 +1246,21 @@ class InexactGaussNewton(BFGS, Minimize, Remember): """ - def __init__(self, **kwargs): - Minimize.__init__(self, **kwargs) + def __init__( + self, + *, + cg_rtol: float = 1e-1, + cg_atol: float = 0.0, + cg_maxiter: int = 5, + **kwargs, + ): + super().__init__( + cg_rtol=cg_rtol, cg_atol=cg_atol, cg_maxiter=cg_maxiter, **kwargs + ) - name = "Inexact Gauss Newton" + self._was_default_hinv = False - maxIterCG = 5 - tolCG = 1e-1 + name = "Inexact Gauss Newton" @property def approxHinv(self): @@ -1081,7 +1276,9 @@ def approxHinv(self): M = sp.linalg.LinearOperator( (self.xc.size, self.xc.size), self.bfgs, dtype=self.xc.dtype ) + self._was_default_hinv = True return M + self._was_default_hinv = False return _approxHinv @approxHinv.setter @@ -1090,13 +1287,20 @@ def approxHinv(self, value): @timeIt def findSearchDirection(self): - # Choose `rtol` or `tol` argument based on installed scipy version - tol_key = "rtol" if SCIPY_1_12 else "tol" - inp = {tol_key: self.tolCG, "maxiter": self.maxIterCG} - Hinv = SolverCG(self.H, M=self.approxHinv, **inp) + Hinv = SolverCG( + self.H, + M=self.approxHinv, + rtol=self.cg_rtol, + atol=self.cg_atol, + maxiter=self.cg_maxiter, + ) p = Hinv * (-self.g) return p + def _doEndIteration_BFGS(self, xt): + if self._was_default_hinv: + super()._doEndIteration_BFGS(xt) + class SteepestDescent(Minimize, Remember): name = "Steepest Descent" @@ -1200,65 +1404,109 @@ def evalFunction(x, return_g=False): return x -class ProjectedGNCG(BFGS, Minimize, Remember): - def __init__(self, **kwargs): - Minimize.__init__(self, **kwargs) - - name = "Projected GNCG" - - maxIterCG = 5 - tolCG = 1e-1 - cg_count = 0 - stepOffBoundsFact = 1e-2 # perturbation of the inactive set off the bounds - stepActiveset = True - lower = -np.inf - upper = np.inf - - def _startup(self, x0): - # ensure bound vectors are the same size as the model - if not isinstance(self.lower, np.ndarray): - self.lower = np.ones_like(x0) * self.lower - if not isinstance(self.upper, np.ndarray): - self.upper = np.ones_like(x0) * self.upper +class ProjectedGNCG(Bounded, InexactGaussNewton): + def __init__( + self, + *, + lower: None | float | npt.NDArray[np.float64] = -np.inf, + upper: None | float | npt.NDArray[np.float64] = np.inf, + cg_maxiter: int = 5, + cg_rtol: float = None, + cg_atol: float = None, + step_active_set: bool = True, + active_set_grad_scale: float = 1e-2, + **kwargs, + ): + if (val := kwargs.pop("tolCG", None)) is not None: + # Deprecated path when tolCG is passed. + self.tolCG = val + cg_atol = val + cg_rtol = 0.0 + elif cg_rtol is None and cg_atol is None: + # Note these defaults match previous settings... + # but they're not good in general... + # Ideally they will change to cg_rtol=1E-3 and cg_atol=0.0 + warnings.warn( + "The defaults for ProjectedGNCG will change in SimPEG 0.26.0. If you want to maintain the " + "previous behavior, explicitly set 'cg_atol=1E-3' and 'cg_rtol=0.0'.", + FutureWarning, + stacklevel=2, + ) + cg_atol = 1e-3 + cg_rtol = 0.0 + # defaults for if someone passes just cg_rtol or just cg_atol (to be removed on deprecation removal) + # These will likely be the future defaults + elif cg_atol is None: + cg_atol = 0.0 + elif cg_rtol is None: + cg_rtol = 1e-3 + + if (val := kwargs.pop("stepActiveSet", None)) is not None: + self.stepActiveSet = val + else: + self.step_active_set = step_active_set - @count - def projection(self, x): - """projection(x) + if (val := kwargs.pop("stepOffBoundsFact", None)) is not None: + self.stepOffBoundsFact = val + else: + self.active_set_grad_scale = active_set_grad_scale + + super().__init__( + lower=lower, + upper=upper, + cg_maxiter=cg_maxiter, + cg_rtol=cg_rtol, + cg_atol=cg_atol, + **kwargs, + ) - Make sure we are feasible. + # initialize some tracking parameters + self.cg_count = 0 + self.cg_abs_resid = np.inf + self.cg_rel_resid = np.inf - """ - return np.median(np.c_[self.lower, x, self.upper], axis=1) + self.printers.extend( + [ + IterationPrinters.iterationCG, + IterationPrinters.iteration_CG_rel_residual, + IterationPrinters.iteration_CG_abs_residual, + ] + ) - @count - def activeSet(self, x): - """activeSet(x) + name = "Projected GNCG" - If we are on a bound + @property + def step_active_set(self) -> bool: + """Whether to include the active set's gradient in the step direction. + Returns + ------- + bool """ - return np.logical_or(x <= self.lower, x >= self.upper) + return self._step_active_set + + @step_active_set.setter + def step_active_set(self, value: bool): + self._step_active_set = validate_type("step_active_set", value, bool) @property - def approxHinv(self): - """ - The approximate Hessian inverse is used to precondition CG. + def active_set_grad_scale(self) -> float: + """Scalar to apply to the active set's gradient - Default uses BFGS, with an initial H0 of *bfgsH0*. + if `step_active_set` is `True`, then the active set's gradient is multiplied by this value + when including it in the search direction. - Must be a scipy.sparse.linalg.LinearOperator + Returns + ------- + float """ - _approxHinv = getattr(self, "_approxHinv", None) - if _approxHinv is None: - M = sp.linalg.LinearOperator( - (self.xc.size, self.xc.size), self.bfgs, dtype=self.xc.dtype - ) - return M - return _approxHinv + return self._active_set_grad_scale - @approxHinv.setter - def approxHinv(self, value): - self._approxHinv = value + @active_set_grad_scale.setter + def active_set_grad_scale(self, value: float): + self._active_set_grad_scale = validate_float( + "active_set_grad_scale", value, min_val=0, inclusive_min=True + ) @timeIt def findSearchDirection(self): @@ -1266,58 +1514,107 @@ def findSearchDirection(self): findSearchDirection() Finds the search direction based on projected CG """ + # remember, "active" means cell with values equal to the limit + # "inactive" are cells with values inside the limits. + + # The basic logic of this method is to do CG iterations only + # on the inactive set, then also add a scaled gradient for the + # active set, (if that gradient points away from the limits.) + self.cg_count = 0 - Active = self.activeSet(self.xc) - temp = sum((np.ones_like(self.xc.size) - Active)) + active = self.activeSet(self.xc) + inactive = ~active step = np.zeros(self.g.size) - resid = -(1 - Active) * self.g + resid = inactive * (-self.g) - r = resid - (1 - Active) * (self.H * step) + r = resid # - Inactive * (self.H * step)# step is zero p = self.approxHinv * r sold = np.dot(r, p) count = 0 + r_norm0 = norm(r) - while np.all([np.linalg.norm(r) > self.tolCG, count < self.maxIterCG]): + atol = max(self.cg_rtol * norm(r_norm0), self.cg_atol) + if self.debug: + print(f"CG Target tolerance: {atol}") + r_norm = r_norm0 + while r_norm > atol and count < self.cg_maxiter: + if self.debug: + print(f"CG Iteration: {count}, residual norm: {r_norm}") count += 1 - q = (1 - Active) * (self.H * p) + q = inactive * (self.H * p) alpha = sold / (np.dot(p, q)) step += alpha * p r -= alpha * q + r_norm = norm(r) h = self.approxHinv * r snew = np.dot(r, h) - p = h + (snew / sold * p) + p = h + (snew / sold) * p sold = snew # End CG Iterations - self.cg_count += count - - # Take a gradient step on the active cells if exist - if temp != self.xc.size: - rhs_a = (Active) * -self.g - + self.cg_count = count + self.cg_abs_resid = r_norm + self.cg_rel_resid = r_norm / r_norm0 + + # Also include the gradient for cells on the boundary + # if that gradient would move them away from the boundary. + # aka, active and not bound. + bound = self.bindingSet(self.xc) + active_not_bound = active & (~bound) + if self.step_active_set and np.any(active_not_bound): + rhs_a = active_not_bound * -self.g + + # active means x == boundary + # bound means x == boundary and g == 0 or -g points beyond boundary + # active and not bound means + # x == boundary and g neq 0 and g points inside + # so can safely discard a non-zero check on + # if np.any(rhs_a) + + # reasonable guess at the step length for the gradient on the + # active cell boundaries. Basically scale it to have the same + # maximum as the cg step on the cells that are not on the + # boundary. dm_i = max(abs(step)) dm_a = max(abs(rhs_a)) - # perturb inactive set off of bounds so that they are included - # in the step - step = step + self.stepOffBoundsFact * (rhs_a * dm_i / dm_a) + # add the active set's gradients. + step += self.active_set_grad_scale * (rhs_a * dm_i / dm_a) - # Only keep gradients going in the right direction on the active - # set - indx = ((self.xc <= self.lower) & (step < 0)) | ( - (self.xc >= self.upper) & (step > 0) - ) - step[indx] = 0.0 + # Only keep search directions going in the right direction + step[bound] = 0 return step + + stepActiveSet = deprecate_property( + step_active_set, + old_name="stepActiveSet", + removal_version="0.26.0", + future_warn=True, + ) + + stepOffBoundsFact = deprecate_property( + active_set_grad_scale, + old_name="stepOffBoundsFact", + removal_version="0.26.0", + future_warn=True, + ) + + # This was the weird part from before... the default tolerance was used as an absolute tolerance... + tolCG = deprecate_property( + InexactGaussNewton.cg_atol, + old_name="tolCG", + removal_version="0.26.0", + future_warn=True, + ) diff --git a/tests/base/test_directives.py b/tests/base/test_directives.py index 407148ed1b..ea39e2413f 100644 --- a/tests/base/test_directives.py +++ b/tests/base/test_directives.py @@ -86,6 +86,16 @@ def test_validation_warning(self): self.assertTrue(directiveList.validate()) +def test_directive_list_iterable(): + directs = [ + directives.UpdateIRLS(), + directives.BetaSchedule(coolingFactor=2, coolingRate=1), + ] + directives_list = directives.DirectiveList(*directs) + for item1, item2 in zip(directives_list, directs): + assert item1 is item2 + + class ValidationInInversion(unittest.TestCase): def setUp(self): mesh = discretize.TensorMesh([4, 4, 4]) diff --git a/tests/base/test_inversion.py b/tests/base/test_inversion.py new file mode 100644 index 0000000000..f56e8ac744 --- /dev/null +++ b/tests/base/test_inversion.py @@ -0,0 +1,69 @@ +import discretize +import pytest +import numpy as np + +import simpeg.optimization as smp_opt +import simpeg.inversion as smp_inv +import simpeg.directives as smp_drcs +import simpeg.simulation + +from simpeg.inverse_problem import BaseInvProblem + +SIMPEG_OPTIMIZERS = [ + smp_opt.ProjectedGradient, + smp_opt.BFGS, + smp_opt.InexactGaussNewton, + smp_opt.SteepestDescent, + smp_opt.ProjectedGNCG, +] + + +@pytest.fixture(params=SIMPEG_OPTIMIZERS) +def inversion(request): + opt = request.param(maxIter=0) + + mesh = discretize.TensorMesh([10]) + n_d = 5 + sim = simpeg.simulation.ExponentialSinusoidSimulation( + mesh=mesh, + n_kernels=n_d, + model_map=simpeg.maps.IdentityMap(mesh), + ) + m0 = np.zeros(mesh.n_cells) + data = sim.make_synthetic_data( + m0, add_noise=True, relative_error=0, noise_floor=0.1, seed=0 + ) + dmis = simpeg.data_misfit.L2DataMisfit(data, sim) + reg = simpeg.regularization.Smallness(mesh) + + prob = BaseInvProblem(dmis, reg, opt) + return smp_inv.BaseInversion(prob) + + +@pytest.mark.parametrize("dlist", [[], [smp_drcs.UpdatePreconditioner()]]) +def test_bfgs_init_logic(inversion, dlist, capsys): + dlist = smp_drcs.DirectiveList(*dlist, inversion=inversion) + inversion.directiveList = dlist + + inv_prb = inversion.invProb + + # Always defaults to trying to initialize bfgs with reg.deriv2 + assert inv_prb.init_bfgs + + m0 = np.zeros(10) + inversion.run(m0) + captured = capsys.readouterr() + + if isinstance(inv_prb.opt, smp_opt.InexactGaussNewton) and any( + isinstance(dr, smp_drcs.UpdatePreconditioner) for dr in dlist + ): + assert not inv_prb.init_bfgs + assert "bfgsH0" not in captured.out + elif isinstance(inv_prb.opt, (smp_opt.BFGS, smp_opt.InexactGaussNewton)): + assert inv_prb.init_bfgs + assert "bfgsH0" in captured.out + else: + assert inv_prb.init_bfgs # defaults to True even if opt would not use it. + assert ( + "bfgsH0" not in captured.out + ) # But shouldn't say anything if it doesn't use it. diff --git a/tests/base/test_optimizers.py b/tests/base/test_optimizers.py index 45ef588f25..bec853c058 100644 --- a/tests/base/test_optimizers.py +++ b/tests/base/test_optimizers.py @@ -1,67 +1,493 @@ -import unittest +import re +import pytest + +from simpeg.optimization import ProjectedGNCG from simpeg.utils import sdiag import numpy as np +import numpy.testing as npt import scipy.sparse as sp from simpeg import optimization from discretize.tests import get_quadratic, rosenbrock TOL = 1e-2 +OPTIMIZERS = [ + optimization.GaussNewton, + optimization.InexactGaussNewton, + optimization.BFGS, + optimization.ProjectedGradient, + optimization.SteepestDescent, + optimization.ProjectedGNCG, +] + +OPT_KWARGS = { + optimization.GaussNewton: {}, + optimization.InexactGaussNewton: dict(cg_rtol=1e-6, cg_maxiter=100), + optimization.BFGS: dict(maxIter=100, tolG=1e-2, maxIterLS=20), + optimization.ProjectedGradient: dict(maxIter=100, cg_rtol=1e-6, cg_maxiter=100), + optimization.SteepestDescent: dict(maxIter=10000, tolG=1e-5, tolX=1e-8, eps=1e-8), + optimization.ProjectedGNCG: dict(cg_rtol=1e-6, cg_maxiter=100), +} + + +@pytest.mark.parametrize("optimizer", OPTIMIZERS) +@pytest.mark.parametrize( + ("func", "x_true", "x0"), + [ + (rosenbrock, np.array([1.0, 1.0]), np.array([0, 0])), + ( + get_quadratic(sp.identity(2).tocsr(), np.array([-5, 5])), + np.array([5, -5]), + np.zeros(2), + ), + ], + ids=["rosenbrock", "quadratic"], +) +class TestUnboundOptimizers: + + def test_minimizer(self, optimizer, func, x_true, x0): + opt = optimizer(**OPT_KWARGS[optimizer]) + xopt = opt.minimize(func, x0) + npt.assert_allclose(xopt, x_true, rtol=TOL) + + +def test_NewtonRoot(): + def fun(x, return_g=True): + if return_g: + return np.sin(x), sdiag(np.cos(x)) + return np.sin(x) + + x = np.array([np.pi - 0.3, np.pi + 0.1, 0]) + xopt = optimization.NewtonRoot(comments=False).root(fun, x) + x_true = np.array([np.pi, np.pi, 0]) + npt.assert_allclose(xopt, x_true, rtol=0, atol=TOL) + + +@pytest.mark.parametrize( + "optimizer", filter(lambda x: issubclass(x, optimization.Bounded), OPTIMIZERS) +) +@pytest.mark.parametrize( + ("lower", "upper", "x_true", "x0"), + [ + (-2, 2, np.array([2.0, -2.0]), np.zeros(2)), + (-2, 8, np.array([5, -2]), np.zeros(2)), + (-8, 2, np.array([2, -5]), np.zeros(2)), + ], + ids=["both active", "lower active", "upper active"], +) +class TestBoundedOptimizers: + def test_minimizer(self, optimizer, lower, upper, x_true, x0): + func = get_quadratic(sp.identity(2).tocsr(), np.array([-5, 5])) + opt = optimizer(lower=lower, upper=upper) + xopt = opt.minimize(func, x0) + npt.assert_allclose(xopt, x_true, rtol=TOL) + + +@pytest.mark.parametrize( + ("x0", "bounded"), + [(np.array([8, 2]), False), (np.array([4, 0]), True)], + ids=["active not bound", "active and bound"], +) +def test_projected_gncg_active_not_bound_branch(x0, bounded): + # tests designed to test the branches of the + # projected gncg when a point is in the active set but not in the binding set. + func = get_quadratic(sp.identity(2).tocsr(), np.array([-5, 5])) + opt = ProjectedGNCG(upper=8, lower=0) + _, g = func(x0, return_g=True, return_H=False) + + opt.g = g + active = opt.activeSet(x0) + bound = opt.bindingSet(x0) + + # assert that the initial point is what we intend to hit the correct branch + # in the minimizer. + assert not np.any(active & ~bound) is bounded + + xopt = opt.minimize(func, x0) + x_true = np.array([5, 0]) + npt.assert_allclose(xopt, x_true, rtol=TOL) + + +@pytest.mark.parametrize("lower", [None, 0.0, np.zeros(10)]) +@pytest.mark.parametrize("upper", [None, 1.0, np.ones(10)]) +class TestBounded: + + def test_project(self, lower, upper): + x = np.linspace(-9.5, 8.2, 10) + bnd = optimization.Bounded(lower=lower, upper=upper) + + x_proj = bnd.projection(x) + if lower is not None: + assert x_proj.min() == 0.0 + else: + assert x_proj.min() == x.min() + + if upper is not None: + assert x_proj.max() == 1.0 + else: + assert x_proj.max() == x.max() + + def test_active_set(self, lower, upper): + x = np.linspace(-9.5, 8.2, 10) + bnd = optimization.Bounded(lower=lower, upper=upper) + + active_set = bnd.activeSet(x) + + if lower is not None: + assert all(active_set[x <= lower]) + else: + assert not any(active_set[x <= 0]) + + if upper is not None: + assert all(active_set[x >= upper]) + else: + assert not any(active_set[x >= 1]) + + def test_inactive_set(self, lower, upper): + x = np.linspace(-9.5, 8.2, 10) + bnd = optimization.Bounded(lower=lower, upper=upper) + + inactive_set = bnd.inactiveSet(x) + + if lower is not None: + assert not any(inactive_set[x <= lower]) + else: + assert all(inactive_set[x <= 0]) + + if upper is not None: + assert not any(inactive_set[x >= upper]) + else: + assert all(inactive_set[x >= 1]) + + def test_binding_set(self, lower, upper): + x = np.linspace(-9.5, 8.2, 10) + g = (np.ones(5)[:, None] * np.array([-1, 1])).reshape(-1) + assert len(x) == len(g) + assert g[0] == -1 and g[1] == 1 and g[2] == -1 # and so on + bnd = optimization.Bounded(lower=lower, upper=upper) + bnd.g = g + + bnd_set = bnd.bindingSet(x) + + if lower is not None: + assert all(bnd_set[(x <= lower) & (g >= 0)]) + else: + assert not any(bnd_set[(x <= 0) & (g >= 0)]) + + if upper is not None: + assert all(bnd_set[(x >= upper) & (g <= 0)]) + else: + assert not any(bnd_set[(x >= 1) & (g <= 0)]) + + +def test_bounded_kwargs_only(): + with pytest.raises( + TypeError, + match=re.escape( + "Bounded.__init__() takes 1 positional argument but 2 were given" + ), + ): + optimization.Bounded(None) + + +@pytest.mark.parametrize( + ("lower", "upper"), + [ + (np.zeros(11), None), + (None, np.ones(11)), + (np.zeros(11), np.ones(10)), + (np.zeros(10), np.ones(11)), + (np.zeros(11), np.ones(11)), + ], + ids=["only_lower", "only_upper", "bad_lower", "bad_upper", "both_bad"], +) +@pytest.mark.parametrize( + "opt_class", [optimization.ProjectedGradient, optimization.ProjectedGNCG] +) +def test_bad_bounds(lower, upper, opt_class): + m = np.linspace(-9.5, 8.2, 10) + opt = opt_class(lower=lower, upper=upper) + with pytest.raises(RuntimeError, match="Initial model is not projectable"): + opt.startup(m) + + +class TestInexactCGParams: + + def test_defaults(self): + cg_pars = optimization.InexactCG() + assert cg_pars.cg_atol == 0.0 + assert cg_pars.cg_rtol == 1e-1 + assert cg_pars.cg_maxiter == 5 + + def test_init(self): + cg_pars = optimization.InexactCG(cg_rtol=1e-3, cg_atol=1e-5, cg_maxiter=10) + assert cg_pars.cg_atol == 1e-5 + assert cg_pars.cg_rtol == 1e-3 + assert cg_pars.cg_maxiter == 10 + + def test_kwargs_only(self): + with pytest.raises( + TypeError, + match=re.escape( + "InexactCG.__init__() takes 1 positional argument but 2 were given" + ), + ): + optimization.InexactCG(1e-3) + + def test_deprecated(self): + with pytest.warns(FutureWarning, match=".*tolCG has been deprecated.*"): + cg_pars = optimization.InexactCG(tolCG=1e-3) + assert cg_pars.cg_atol == 0.0 + assert cg_pars.cg_rtol == 1e-3 + + with pytest.warns(FutureWarning, match=".*maxIterCG has been deprecated.*"): + cg_pars = optimization.InexactCG(maxIterCG=3) + assert cg_pars.cg_atol == 0.0 + assert cg_pars.cg_rtol == 1e-1 + assert cg_pars.cg_maxiter == 3 + + +class TestProjectedGradient: + + def test_defaults(self): + opt = optimization.ProjectedGradient() + assert opt.cg_rtol == 1e-1 + assert opt.cg_atol == 0.0 + assert opt.cg_maxiter == 5 + assert np.isneginf(opt.lower) + assert np.isposinf(opt.upper) + + def test_init(self): + opt = optimization.ProjectedGradient( + cg_rtol=1e-3, cg_atol=1e-5, cg_maxiter=10, lower=0.0, upper=1.0 + ) + assert opt.cg_rtol == 1e-3 + assert opt.cg_atol == 1e-5 + assert opt.cg_maxiter == 10 + assert opt.lower == 0.0 + assert opt.upper == 1.0 + + def test_kwargs_only(self): + with pytest.raises( + TypeError, + match=re.escape( + "ProjectedGradient.__init__() takes 1 positional argument but 2 were given" + ), + ): + optimization.ProjectedGradient(10) + + @pytest.mark.parametrize("on_init", [True, False], ids=["init", "attribute setter"]) + def test_deprecated_tolCG(self, on_init): + match = ".*tolCG has been deprecated.*cg_rtol.*" + if on_init: + with pytest.warns(FutureWarning, match=match): + opt = optimization.ProjectedGradient(tolCG=1e-3) + else: + opt = optimization.ProjectedGradient() + with pytest.warns(FutureWarning, match=match): + opt.tolCG = 1e-3 + + with pytest.warns(FutureWarning, match=match): + assert opt.tolCG == 1e-3 + assert opt.cg_atol == 0.0 + assert opt.cg_rtol == 1e-3 + + # test setting new changes old + opt.cg_rtol = 1e-4 + + with pytest.warns(FutureWarning, match=match): + assert opt.tolCG == 1e-4 + + @pytest.mark.parametrize("on_init", [True, False], ids=["init", "attribute setter"]) + def test_deprecated_maxIterCG(self, on_init): + + match = ".*maxIterCG has been deprecated.*" + if on_init: + with pytest.warns(FutureWarning, match=match): + opt = optimization.ProjectedGradient(maxIterCG=3) + else: + opt = optimization.ProjectedGradient() + with pytest.warns(FutureWarning, match=match): + opt.maxIterCG = 3 + + with pytest.warns(FutureWarning, match=match): + assert opt.maxIterCG == 3 + + assert opt.cg_maxiter == 3 + + # test setting new changes old + opt.cg_maxiter = 8 + with pytest.warns(FutureWarning, match=match): + assert opt.maxIterCG == 8 + + +class TestInexactGaussNewton: + + def test_defaults(self): + opt = optimization.InexactGaussNewton() + assert opt.cg_rtol == 1e-1 + assert opt.cg_atol == 0.0 + assert opt.cg_maxiter == 5 + + def test_init(self): + opt = optimization.InexactGaussNewton(cg_rtol=1e-3, cg_atol=1e-5, cg_maxiter=10) + assert opt.cg_rtol == 1e-3 + assert opt.cg_atol == 1e-5 + assert opt.cg_maxiter == 10 + + def test_kwargs_only(self): + with pytest.raises( + TypeError, + match=re.escape( + "InexactGaussNewton.__init__() takes 1 positional argument but 2 were given" + ), + ): + optimization.InexactGaussNewton(10) + + @pytest.mark.parametrize("on_init", [True, False], ids=["init", "attribute setter"]) + def test_deprecated_tolCG(self, on_init): + match = ".*tolCG has been deprecated.*cg_rtol.*" + if on_init: + with pytest.warns(FutureWarning, match=match): + opt = optimization.InexactGaussNewton(tolCG=1e-3) + else: + opt = optimization.InexactGaussNewton() + with pytest.warns(FutureWarning, match=match): + opt.tolCG = 1e-3 + + with pytest.warns(FutureWarning, match=match): + assert opt.tolCG == 1e-3 + assert opt.cg_atol == 0.0 + assert opt.cg_rtol == 1e-3 + + # test setting new changes old + opt.cg_rtol = 1e-4 + + with pytest.warns(FutureWarning, match=match): + assert opt.tolCG == 1e-4 + + @pytest.mark.parametrize("on_init", [True, False], ids=["init", "attribute setter"]) + def test_deprecated_maxIterCG(self, on_init): + + match = ".*maxIterCG has been deprecated.*" + if on_init: + with pytest.warns(FutureWarning, match=match): + opt = optimization.InexactGaussNewton(maxIterCG=3) + else: + opt = optimization.InexactGaussNewton() + with pytest.warns(FutureWarning, match=match): + opt.maxIterCG = 3 + + with pytest.warns(FutureWarning, match=match): + assert opt.maxIterCG == 3 + + assert opt.cg_maxiter == 3 + + # test setting new changes old + opt.cg_maxiter = 8 + with pytest.warns(FutureWarning, match=match): + assert opt.maxIterCG == 8 + + +class TestProjectedGNCG: + + @pytest.mark.parametrize("cg_tol_defaults", ["atol", "rtol", "both"]) + def test_defaults(self, cg_tol_defaults): + # testing setting the new default value of rtol if only atol is passed + if cg_tol_defaults == "rtol": + opt = optimization.ProjectedGNCG(cg_atol=1e-5) + assert opt.cg_atol == 1e-5 + assert opt.cg_rtol == 1e-3 + # testing setting the new default value of atol if only rtol is passed + elif cg_tol_defaults == "atol": + opt = optimization.ProjectedGNCG(cg_rtol=1e-4) + assert opt.cg_atol == 0.0 + assert opt.cg_rtol == 1e-4 + # test the old defaults + else: + with pytest.warns( + FutureWarning, match="The defaults for ProjectedGNCG will change.*" + ): + opt = optimization.ProjectedGNCG() + assert opt.cg_rtol == 0.0 + assert opt.cg_atol == 1e-3 + assert opt.cg_maxiter == 5 + assert np.isneginf(opt.lower) + assert np.isposinf(opt.upper) + + def test_init(self): + opt = optimization.ProjectedGNCG( + cg_rtol=1e-3, cg_atol=1e-5, cg_maxiter=10, lower=0.0, upper=1.0 + ) + assert opt.cg_rtol == 1e-3 + assert opt.cg_atol == 1e-5 + assert opt.cg_maxiter == 10 + assert opt.lower == 0.0 + assert opt.upper == 1.0 + + def test_kwargs_only(self): + with pytest.raises( + TypeError, + match=re.escape( + "ProjectedGNCG.__init__() takes 1 positional argument but 2 were given" + ), + ): + optimization.ProjectedGNCG(10) + + @pytest.mark.parametrize("on_init", [True, False], ids=["init", "attribute setter"]) + def test_deprecated_tolCG(self, on_init): + if on_init: + with pytest.warns( + FutureWarning, match=".*tolCG has been deprecated.*cg_atol.*" + ): + opt = optimization.ProjectedGNCG(tolCG=1e-5) + else: + opt = optimization.ProjectedGNCG() + with pytest.warns( + FutureWarning, match=".*tolCG has been deprecated.*cg_atol.*" + ): + opt.tolCG = 1e-5 + + with pytest.warns(FutureWarning, match=".*tolCG has been deprecated.*"): + assert opt.tolCG == 1e-5 + + assert opt.cg_atol == 1e-5 + assert opt.cg_rtol == 0.0 + + # test setting new changes old + opt.cg_atol = 1e-4 + + with pytest.warns(FutureWarning, match=".*tolCG has been deprecated.*"): + assert opt.tolCG == 1e-4 + + @pytest.mark.parametrize("on_init", [True, False], ids=["init", "attribute setter"]) + @pytest.mark.parametrize( + ("old_name", "new_name", "val1", "val2"), + [ + ("maxIterCG", "cg_maxiter", 3, 8), + ("stepActiveSet", "step_active_set", True, False), + ("stepOffBoundsFact", "active_set_grad_scale", 1.2, 1.4), + ], + ids=["maxIterCG", "stepActiveSet", "stepOffBoundsFact"], + ) + def test_deprecated_maxIterCG(self, on_init, old_name, new_name, val1, val2): + + match = f".*{old_name} has been deprecated.*" + if on_init: + with pytest.warns(FutureWarning, match=match): + opt = optimization.ProjectedGNCG(**{old_name: val1}) + else: + opt = optimization.ProjectedGNCG() + with pytest.warns(FutureWarning, match=match): + setattr(opt, old_name, val1) + opt.maxIterCG = 3 + + with pytest.warns(FutureWarning, match=match): + assert getattr(opt, old_name) == val1 + + assert getattr(opt, old_name) == val1 + + setattr(opt, new_name, val2) -class TestOptimizers(unittest.TestCase): - def setUp(self): - self.A = sp.identity(2).tocsr() - self.b = np.array([-5, -5]) - - def test_GN_rosenbrock(self): - GN = optimization.GaussNewton() - xopt = GN.minimize(rosenbrock, np.array([0, 0])) - x_true = np.array([1.0, 1.0]) - print("xopt: ", xopt) - print("x_true: ", x_true) - self.assertTrue(np.linalg.norm(xopt - x_true, 2) < TOL, True) - - def test_GN_quadratic(self): - GN = optimization.GaussNewton() - xopt = GN.minimize(get_quadratic(self.A, self.b), np.array([0, 0])) - x_true = np.array([5.0, 5.0]) - print("xopt: ", xopt) - print("x_true: ", x_true) - self.assertTrue(np.linalg.norm(xopt - x_true, 2) < TOL, True) - - def test_ProjGradient_quadraticBounded(self): - PG = optimization.ProjectedGradient(debug=True) - PG.lower, PG.upper = -2, 2 - xopt = PG.minimize(get_quadratic(self.A, self.b), np.array([0, 0])) - x_true = np.array([2.0, 2.0]) - print("xopt: ", xopt) - print("x_true: ", x_true) - self.assertTrue(np.linalg.norm(xopt - x_true, 2) < TOL, True) - - def test_ProjGradient_quadratic1Bound(self): - myB = np.array([-5, 1]) - PG = optimization.ProjectedGradient() - PG.lower, PG.upper = -2, 2 - xopt = PG.minimize(get_quadratic(self.A, myB), np.array([0, 0])) - x_true = np.array([2.0, -1.0]) - print("xopt: ", xopt) - print("x_true: ", x_true) - self.assertTrue(np.linalg.norm(xopt - x_true, 2) < TOL, True) - - def test_NewtonRoot(self): - def fun(x, return_g=True): - if return_g: - return np.sin(x), sdiag(np.cos(x)) - return np.sin(x) - - x = np.array([np.pi - 0.3, np.pi + 0.1, 0]) - xopt = optimization.NewtonRoot(comments=False).root(fun, x) - x_true = np.array([np.pi, np.pi, 0]) - print("Newton Root Finding") - print("xopt: ", xopt) - print("x_true: ", x_true) - self.assertTrue(np.linalg.norm(xopt - x_true, 2) < TOL, True) - - -if __name__ == "__main__": - unittest.main() + with pytest.warns(FutureWarning, match=match): + assert getattr(opt, old_name) == val2 From 256406f922c6b8a95a7008622f7560ac256aa748 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Thu, 18 Sep 2025 09:47:56 -0600 Subject: [PATCH 165/194] Add top level descriptions to missing to functions (#1702) #### Summary Linting failed with a new released version of some styles in between merges, Fix those errors. --- simpeg/electromagnetics/utils/em1d_utils.py | 2 +- simpeg/maps/_base.py | 32 ++++++++++----------- simpeg/maps/_injection.py | 2 +- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/simpeg/electromagnetics/utils/em1d_utils.py b/simpeg/electromagnetics/utils/em1d_utils.py index c551cb311d..fa77c48e1c 100644 --- a/simpeg/electromagnetics/utils/em1d_utils.py +++ b/simpeg/electromagnetics/utils/em1d_utils.py @@ -222,7 +222,7 @@ def LogUniform(f, chi_inf=0.05, del_chi=0.05, tau1=1e-5, tau2=1e-2): def get_splined_dlf_points(filt, v_min, v_max): - """ + """Get the splined points used for the digital linear filter. Parameters ---------- diff --git a/simpeg/maps/_base.py b/simpeg/maps/_base.py index ccb40fbee4..9369f1fb75 100644 --- a/simpeg/maps/_base.py +++ b/simpeg/maps/_base.py @@ -1178,11 +1178,24 @@ def nP(self): class TileMap(IdentityMap): - """ - Mapping for tiled inversion. + """Mapping for tiled inversion. Uses volume averaging to map a model defined on a global mesh to the local mesh. Everycell in the local mesh must also be in the global mesh. + + Parameters + ---------- + global_mesh : discretize.TreeMesh + Global TreeMesh defining the entire domain. + global_active : numpy.ndarray of bool or int + Defines the active cells in the global mesh. + local_mesh : discretize.TreeMesh + Local TreeMesh for the simulation. + tol : float, optional + Tolerance to avoid zero division + components : int, optional + Number of components in the model. E.g. a vector model in 3D would have 3 + components. """ def __init__( @@ -1194,21 +1207,6 @@ def __init__( components=1, **kwargs, ): - """ - Parameters - ---------- - global_mesh : discretize.TreeMesh - Global TreeMesh defining the entire domain. - global_active : numpy.ndarray of bool or int - Defines the active cells in the global mesh. - local_mesh : discretize.TreeMesh - Local TreeMesh for the simulation. - tol : float, optional - Tolerance to avoid zero division - components : int, optional - Number of components in the model. E.g. a vector model in 3D would have 3 - components. - """ super().__init__(mesh=None, **kwargs) self._global_mesh = validate_type( "global_mesh", global_mesh, discretize.TreeMesh, cast=False diff --git a/simpeg/maps/_injection.py b/simpeg/maps/_injection.py index 492fca468e..68c02c2e61 100644 --- a/simpeg/maps/_injection.py +++ b/simpeg/maps/_injection.py @@ -219,7 +219,7 @@ def value_inactive(self, value): @property def active_cells(self): - """ + """A boolean array representing the active values in the map's output array. Returns ------- From f04ba47c11c94ed9aa8f003d72d9dd4abcde2431 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 22 Sep 2025 10:10:07 -0700 Subject: [PATCH 166/194] Update meeting times in README.rst (#1700) Update meeting times to Wednesday afternoons in the `README.rst`. --- README.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 54041d24e3..8875b3a6c5 100644 --- a/README.rst +++ b/README.rst @@ -48,7 +48,7 @@ The vision is to create a package for finite volume simulation with applications You are welcome to join our forum and engage with people who use and develop SimPEG at: https://simpeg.discourse.group/. -Weekly meetings are open to all. They are generally held on Wednesdays at 10:30am PDT. Please see the calendar (`GCAL `_, `ICAL `_) for information on the next meeting. +Weekly meetings are open to all. They are generally held on Wednesdays at 3:00 PM Pacific Time. Please see the calendar (`GCAL `_, `ICAL `_) for information on the next meeting. Overview Video ============== @@ -120,9 +120,8 @@ Meetings SimPEG hosts weekly meetings for users to interact with each other, for developers to discuss upcoming changes to the code base, and for discussing topics related to geophysics in general. -Currently our meetings are held every Wednesday, alternating between -a mornings (10:30 am pacific time) and afternoons (3:00 pm pacific time) -on even numbered Wednesdays. Find more info on our +Currently our meetings are held every Wednesday at 3:00 PM Pacific Time. +Find more info on our `Mattermost `_. From b6b90cfb6642814aee0077447ba02d995549c1c6 Mon Sep 17 00:00:00 2001 From: Lindsey Heagy Date: Tue, 23 Sep 2025 09:34:14 -0700 Subject: [PATCH 167/194] Add `_faceDiv` attribute to FDEM H `Fields` (#1346) Add new `_faceDiv` attribute to the `Fields3DMagneticField` and `Fields3DCurrentDensity` that holds the face divergence, needed to compute charge density. Add tests. --- .../frequency_domain/fields.py | 2 + .../em/fdem/forward/test_fields_crosscheck.py | 142 ++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 tests/em/fdem/forward/test_fields_crosscheck.py diff --git a/simpeg/electromagnetics/frequency_domain/fields.py b/simpeg/electromagnetics/frequency_domain/fields.py index 429829b58e..d23392e0ad 100644 --- a/simpeg/electromagnetics/frequency_domain/fields.py +++ b/simpeg/electromagnetics/frequency_domain/fields.py @@ -1128,6 +1128,7 @@ def startup(self): self._nC = self.simulation.mesh.nC self._MeI = self.simulation.MeI self._MfI = self.simulation.MfI + self._faceDiv = self.simulation.mesh.face_divergence def _GLoc(self, fieldType): if fieldType in ["h", "hSecondary", "hPrimary", "b"]: @@ -1556,6 +1557,7 @@ def startup(self): self._nC = self.simulation.mesh.nC self._MfI = self.simulation.MfI self._MeI = self.simulation.MeI + self._faceDiv = self.simulation.mesh.face_divergence def _GLoc(self, fieldType): if fieldType in ["h", "hSecondary", "hPrimary", "b"]: diff --git a/tests/em/fdem/forward/test_fields_crosscheck.py b/tests/em/fdem/forward/test_fields_crosscheck.py new file mode 100644 index 0000000000..627407b792 --- /dev/null +++ b/tests/em/fdem/forward/test_fields_crosscheck.py @@ -0,0 +1,142 @@ +import numpy as np +import pytest + +import discretize + +from simpeg import maps + +from simpeg.electromagnetics import frequency_domain as fdem +from simpeg.utils.solver_utils import get_default_solver + +SOLVER = get_default_solver() + +# relative tolerances +RELTOL = 1e-2 +MINTOL = 1e-20 # minimum tolerance we test, anything below this is "ZERO" + +FREQUENCY = 5e-1 +SIMULATION_TYPES = ["e", "b", "h", "j"] +FIELDS_TEST = ["e", "b", "h", "j", "charge", "charge_density"] + +VERBOSE = True + + +def get_fdem_simulation(mesh, fdem_type, frequency): + mapping = maps.ExpMap(mesh) + + source_list = [ + fdem.sources.MagDipole([], frequency=frequency, location=np.r_[0.0, 0.0, 10.0]) + ] + survey = fdem.Survey(source_list) + + if fdem_type == "e": + sim = fdem.Simulation3DElectricField( + mesh, survey=survey, sigmaMap=mapping, solver=SOLVER + ) + + elif fdem_type == "b": + sim = fdem.Simulation3DMagneticFluxDensity( + mesh, survey=survey, sigmaMap=mapping, solver=SOLVER + ) + + elif fdem_type == "j": + sim = fdem.Simulation3DCurrentDensity( + mesh, survey=survey, sigmaMap=mapping, solver=SOLVER + ) + + elif fdem_type == "h": + sim = fdem.Simulation3DMagneticField( + mesh, survey=survey, sigmaMap=mapping, solver=SOLVER + ) + + return sim + + +class TestFieldsCrosscheck: + + @property + def mesh(self): + if getattr(self, "_mesh", None) is None: + cs = 10.0 + ncx, ncy, ncz = 4, 4, 4 + npad = 4 + pf = 1.3 + hx = [(cs, npad, -pf), (cs, ncx), (cs, npad, pf)] + hy = [(cs, npad, -pf), (cs, ncy), (cs, npad, pf)] + hz = [(cs, npad, -pf), (cs, ncz), (cs, npad, pf)] + self._mesh = discretize.TensorMesh([hx, hy, hz], ["C", "C", "C"]) + return self._mesh + + @property + def model(self): + if getattr(self, "_model", None) is None: + sigma_background = 10 + sigma_target = 1e-2 + sigma_air = 1e-8 + + target_width = 40 + target_depth = -20 + + inds_target = ( + (self.mesh.cell_centers[:, 0] > -target_width / 2) + & (self.mesh.cell_centers[:, 0] < target_width / 2) + & (self.mesh.cell_centers[:, 1] > -target_width / 2) + & (self.mesh.cell_centers[:, 1] < target_width / 2) + & (self.mesh.cell_centers[:, 2] > -target_width / 2 + target_depth) + & (self.mesh.cell_centers[:, 2] < target_width / 2 + target_depth) + ) + + sigma_model = sigma_background * np.ones(self.mesh.n_cells) + sigma_model[self.mesh.cell_centers[:, 2] > 0] = sigma_air + + sigma_model[inds_target] = sigma_target + + self._model = np.log(sigma_model) + return self._model + + @property + def simulation_dict(self): + if getattr(self, "_simulation_dict", None) is None: + self._simulation_dict = { + key: get_fdem_simulation(self.mesh, key, FREQUENCY) + for key in SIMULATION_TYPES + } + return self._simulation_dict + + @property + def fields_dict(self): + if getattr(self, "_fields_dict", None) is None: + self._fields_dict = { + key: sim.fields(self.model) for key, sim in self.simulation_dict.items() + } + return self._fields_dict + + def compare_fields(self, field1, field2, relative_tolerance, verbose=False): + norm_diff = np.linalg.norm(field1 - field2) + abs_tol = np.max( + [ + relative_tolerance + * (np.linalg.norm(field1) + np.linalg.norm(field2)) + / 2, + MINTOL, + ] + ) + test = norm_diff < abs_tol + + if verbose is True: + print(f"||diff||: {norm_diff:1.2e} < TOL: {abs_tol:1.2e} ? {test}") + + return test + + @pytest.mark.parametrize("sim_pairs", [("e", "b"), ("h", "j")], ids=["eb", "hj"]) + @pytest.mark.parametrize("field_test", FIELDS_TEST) + def test_fields_cross_check_EBHJ( + self, sim_pairs, field_test, relative_tolerance=RELTOL, verbose=VERBOSE + ): + field1 = self.fields_dict[sim_pairs[0]][:, field_test] + field2 = self.fields_dict[sim_pairs[1]][:, field_test] + + if verbose is True: + print(f"Testing simulations {sim_pairs} for field {field_test}") + + assert self.compare_fields(field1, field2, relative_tolerance, verbose) From f0086d16c44b9c334e55cba3b9439b6b5305fde5 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 26 Sep 2025 09:46:35 -0700 Subject: [PATCH 168/194] Improve landing page of docs (#1701) Create a custom RST landing page using `sphinx-design` that includes cards to the main sections of the docs. Move some content from the old landing page (present in the `README.rst`) into new sections in the User Guide: `citing.rst` and `about-simpeg.rst`. --- .ci/environment_test.yml | 1 + CITATION.rst | 33 ++++++- docs/conf.py | 1 + .../getting-started/about-simpeg.rst | 44 +++++++++ .../getting-started/big_picture.rst | 10 +-- .../user-guide/getting-started/citing.rst | 3 + docs/content/user-guide/index.rst | 2 + docs/index.rst | 89 +++++++++++++++++-- environment.yml | 1 + pyproject.toml | 1 + 10 files changed, 164 insertions(+), 21 deletions(-) create mode 100644 docs/content/user-guide/getting-started/about-simpeg.rst create mode 100644 docs/content/user-guide/getting-started/citing.rst diff --git a/.ci/environment_test.yml b/.ci/environment_test.yml index 2d8b61fb49..42401cd9ab 100644 --- a/.ci/environment_test.yml +++ b/.ci/environment_test.yml @@ -26,6 +26,7 @@ dependencies: - sphinx-gallery>=0.1.13 - sphinxcontrib-apidoc - sphinx-reredirects + - sphinx-design - pydata-sphinx-theme - nbsphinx - numpydoc diff --git a/CITATION.rst b/CITATION.rst index b0b2a76ff7..dcd90a96f4 100644 --- a/CITATION.rst +++ b/CITATION.rst @@ -1,13 +1,17 @@ +============= Citing SimPEG -------------- +============= -There is a `paper about SimPEG `_, if you use this code, please help our scientific visibility by citing our work! +There is a paper about SimPEG! If you are using this library in your research, +we greatly appreciate that the software gets cited! - Cockett, R., Kang, S., Heagy, L. J., Pidlisecky, A., & Oldenburg, D. W. (2015). SimPEG: An open source framework for simulation and gradient based parameter estimation in geophysical applications. Computers & Geosciences. + Cockett, R., Kang, S., Heagy, L. J., Pidlisecky, A., & Oldenburg, D. W. + (2015). SimPEG: An open source framework for simulation and gradient based + parameter estimation in geophysical applications. Computers & Geosciences. -BibTex: +Here is a Bibtex entry to make things easier if you’re using Latex: .. code:: @@ -19,3 +23,24 @@ BibTex: publisher={Elsevier} } +Electromagnetics +---------------- + +If you are using the :mod:`simpeg.electromagnetics` module, please also cite: + + Lindsey J. Heagy, Rowan Cockett, Seogi Kang, Gudni K. Rosenkjaer, Douglas W. Oldenburg, A framework for simulation and inversion in electromagnetics, Computers & Geosciences, Volume 107, 2017, Pages 1-19, ISSN 0098-3004, http://dx.doi.org/10.1016/j.cageo.2017.06.018. + +Here is a Bibtex entry to make things easier if you’re using Latex: + +.. code:: + + @article{heagy2017, + title={A framework for simulation and inversion in electromagnetics}, + author={Lindsey J. Heagy and Rowan Cockett and Seogi Kang and Gudni K. Rosenkjaer and Douglas W. Oldenburg}, + journal={Computers & Geosciences}, + volume={107}, + pages={1 - 19}, + year={2017}, + issn={0098-3004}, + doi={http://dx.doi.org/10.1016/j.cageo.2017.06.018} + } diff --git a/docs/conf.py b/docs/conf.py index c91087f021..f5a72fcf30 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -44,6 +44,7 @@ "sphinx.ext.coverage", "sphinx.ext.doctest", "sphinx.ext.extlinks", + "sphinx_design", "sphinx.ext.intersphinx", "sphinx.ext.mathjax", "sphinx_gallery.gen_gallery", diff --git a/docs/content/user-guide/getting-started/about-simpeg.rst b/docs/content/user-guide/getting-started/about-simpeg.rst new file mode 100644 index 0000000000..225122ca43 --- /dev/null +++ b/docs/content/user-guide/getting-started/about-simpeg.rst @@ -0,0 +1,44 @@ +.. _about_simpeg: + +============== +What's SimPEG? +============== + +SimPEG (Simulation and Parameter Estimation in Geophysics) is a +**Python library** for simulations, inversions and parameter estimation for +geophysical applications. + +The vision is to create a package for finite volume simulation with +applications to geophysical imaging and subsurface flow. To enable the +understanding of the many different components, this package has the following +features: + +* Modular with respect to the spacial discretization, optimization routine, and + geophysical problem. +* Built with the inverse problem in mind. +* Provides a framework for geophysical and hydrogeologic problems. +* Supports 1D, 2D and 3D problems. +* Designed for large-scale inversions. + +Overview Talk at SciPy 2016 +--------------------------- + +.. raw:: html + + diff --git a/docs/content/user-guide/getting-started/big_picture.rst b/docs/content/user-guide/getting-started/big_picture.rst index b5c2ddc43b..8a152f7b5a 100644 --- a/docs/content/user-guide/getting-started/big_picture.rst +++ b/docs/content/user-guide/getting-started/big_picture.rst @@ -133,15 +133,7 @@ be exploited through inheritance of base classes, and differences can be expressed through subtype polymorphism. Please look at the documentation here for more in-depth information. - -.. include:: ../../../CITATION.rst - -Authors -------- - -.. include:: ../../../AUTHORS.rst - License ------- -.. include:: ../../../LICENSE +.. include:: ../../../../LICENSE diff --git a/docs/content/user-guide/getting-started/citing.rst b/docs/content/user-guide/getting-started/citing.rst new file mode 100644 index 0000000000..1ae6828364 --- /dev/null +++ b/docs/content/user-guide/getting-started/citing.rst @@ -0,0 +1,3 @@ +.. _citing: + +.. include:: ../../../../CITATION.rst diff --git a/docs/content/user-guide/index.rst b/docs/content/user-guide/index.rst index 4581bbe166..1de62a4bf7 100644 --- a/docs/content/user-guide/index.rst +++ b/docs/content/user-guide/index.rst @@ -14,9 +14,11 @@ For details on the available classes and functions in SimPEG, please visit the :maxdepth: 1 :caption: Getting Started + getting-started/about-simpeg.rst getting-started/big_picture getting-started/installing getting-started/contributing/index.rst + getting-started/citing.rst .. toctree:: :glob: diff --git a/docs/index.rst b/docs/index.rst index 88de8da538..8769985ee9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,4 +1,84 @@ -.. include:: ../README.rst +:html_theme.sidebar_secondary.remove: true + +.. image:: ./images/simpeg-logo.png + :alt: SimPEG logo + +==================== +SimPEG Documentation +==================== + +Simulation and Parameter Estimation in Geophysics. +An open-source Python library for simulation, inversion and parameter +estimation for geophysical applications. + +**Useful links:** +:ref:`Install ` | +`GitHub Repository `_ | +`Bugs and Issues `_ | +`SimPEG website `_ | +`License `_ + +.. grid:: 1 2 1 2 + :margin: 5 5 0 0 + :padding: 0 0 0 0 + :gutter: 4 + + .. grid-item-card:: :fas:`book-open` User Guide + :text-align: center + :class-title: sd-fs-5 + :class-card: sd-p-3 + + Learn how to use SimPEG. + + .. button-ref:: user_guide + :ref-type: ref + :click-parent: + :color: primary + :shadow: + :expand: + + .. grid-item-card:: :fas:`book` User Tutorials + :text-align: center + :class-title: sd-fs-5 + :class-card: sd-p-3 + + Apply SimPEG to geophysical problems. + + .. button-link:: https://simpeg.xyz/user-tutorials + :click-parent: + :color: primary + :shadow: + :expand: + + Checkout the User Tutorials :octicon:`link-external` + + .. grid-item-card:: :fas:`code` API Reference + :text-align: center + :class-title: sd-fs-5 + :class-card: sd-p-3 + + A list of modules, classes and functions. + + .. button-ref:: api + :ref-type: ref + :color: primary + :shadow: + :expand: + + .. grid-item-card:: :fas:`comments` Contact + :text-align: center + :class-title: sd-fs-5 + :class-card: sd-p-3 + + Chat with the rest of the community. + + .. button-link:: https://mattermost.softwareunderground.org/simpeg + :click-parent: + :color: primary + :shadow: + :expand: + + Join our Mattermost channel :octicon:`link-external` .. toctree:: :maxdepth: 2 @@ -8,10 +88,3 @@ User Guide API Reference Release Notes - -.. Project Index & Search -.. ====================== - -.. * :ref:`genindex` -.. * :ref:`modindex` -.. * :ref:`search` diff --git a/environment.yml b/environment.yml index 70b94106f4..58bc664044 100644 --- a/environment.yml +++ b/environment.yml @@ -34,6 +34,7 @@ dependencies: - sphinx-gallery>=0.1.13 - sphinxcontrib-apidoc - sphinx-reredirects + - sphinx-design - pydata-sphinx-theme - empymod>=2.0.0 - nbsphinx diff --git a/pyproject.toml b/pyproject.toml index 773936cfae..ef97c3bc56 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,6 +74,7 @@ docs = [ "sphinx-gallery>=0.1.13", "sphinxcontrib-apidoc", "sphinx-reredirects", + "sphinx-design", "pydata-sphinx-theme", "nbsphinx", "empymod>=2.0.0", From 42a5db944655044bc7e6e8f72e20572ea5d4da93 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 26 Sep 2025 13:46:08 -0700 Subject: [PATCH 169/194] Add How to Guide page on moving mesh to survey area (#1699) Add a new page to the How to Guide that explains how to move a mesh to a given survey area. This is handy when working with real-world data, whose coordinates are going to be given in projected plain coordinates. --- .../how-to-guide/move-mesh-to-survey.rst | 202 ++++++++++++++++++ docs/content/user-guide/index.rst | 1 + 2 files changed, 203 insertions(+) create mode 100644 docs/content/user-guide/how-to-guide/move-mesh-to-survey.rst diff --git a/docs/content/user-guide/how-to-guide/move-mesh-to-survey.rst b/docs/content/user-guide/how-to-guide/move-mesh-to-survey.rst new file mode 100644 index 0000000000..754e209dde --- /dev/null +++ b/docs/content/user-guide/how-to-guide/move-mesh-to-survey.rst @@ -0,0 +1,202 @@ +============================ +Locating mesh on survey area +============================ + +The :mod:`discretize` package allows us to define 3D meshes that can be used +for running SimPEG's forward and inverse problems. +Mesh dimensions for :class:`discretize.TensorMesh` and +:class:`discretize.TreeMesh` are assumed to be in meters, and by default their +origin (the westmost-southmost-lowest point) is located in the origin of the +coordinate system (the ``(0, 0, 0)``). + +When working with real-world data, we want our mesh to be located around the +survey area. We can move our mesh location by by shifting its ``origin``. + +For example, suppose we want to invert some magnetic data from Osborne Mine in +Australia that spans in a region between 448353.0 m and 482422.0 m along the +easting, and between 7578158.0 m and 7594834.0 m along the northing +(UTM zone 54). +Let's also assume that the maximum topographic height of the area is 417 +m. + +We can build a mesh that spans 34 km on the easting, 17 km on the northing, and +5500 m vertically: + +.. code:: python + + import discretize + + dx, dy, dz = 200.0, 200.0, 100.0 + nx, ny, nz = 170, 85, 55 + hx, hy, hz = [(dx, nx)], [(dy, ny)], [(dz, nz)] + + mesh = discretize.TensorMesh([hx, hy, hz]) + print(mesh) + + +.. code:: + + TensorMesh: 794,750 cells + + MESH EXTENT CELL WIDTH FACTOR + dir nC min max min max max + --- --- --------------------------- ------------------ ------ + x 170 0.00 34,000.00 200.00 200.00 1.00 + y 85 0.00 17,000.00 200.00 200.00 1.00 + z 55 0.00 5,500.00 100.00 100.00 1.00 + + +The ``origin`` of this mesh is located on ``(0, 0, 0)``. We can move it to the +survey area by shifting it to (448353.0 m, 7578158.0 m, -5000 m): + +.. code:: python + + mesh.origin = (448353.0, 7578158.0, -5000) + print(mesh) + +.. code:: + + TensorMesh: 794,750 cells + + MESH EXTENT CELL WIDTH FACTOR + dir nC min max min max max + --- --- --------------------------- ------------------ ------ + x 170 448,353.00 482,353.00 200.00 200.00 1.00 + y 85 7,578,158.00 7,595,158.00 200.00 200.00 1.00 + z 55 -5,000.00 500.00 100.00 100.00 1.00 + + +By shifting the ``origin`` we are not changing the number of cells in the mesh +nor their dimensions. We are just moving the location of the mesh in the three +directions. + +.. note:: + + We shift the z coordinate of the origin to -5000 m so we leave 500 m above + the zeroth height to possibly account for topography. + + +.. tip:: + + Alternatively, we can set the ``origin`` when defining the mesh, by passing + it as an argument. For example: + + .. code:: python + + origin = (448353.0, 7578158.0, -5000) + mesh = discretize.TensorMesh([hx, hy, hz], origin=origin) + print(mesh) + + +Considering padding: simple case +-------------------------------- + +It's best practice to add some padding to the mesh when using it in an +inversion. The padding cells will allocate any potential body outside the +survey area, which effect might be present in the data. + +Let's take the previous example and build a mesh that has 3 km of padding +on each horizontal direction: + +.. code:: python + + hx = [(200.0, 15), (dx, nx), (200.0, 15)] + hy = [(200.0, 15), (dy, ny), (200.0, 15)] + hz = [(dz, nz)] + + mesh = discretize.TensorMesh([hx, hy, hz]) + print(mesh) + +.. code:: + + TensorMesh: 1,265,000 cells + + MESH EXTENT CELL WIDTH FACTOR + dir nC min max min max max + --- --- --------------------------- ------------------ ------ + x 200 0.00 40,000.00 200.00 200.00 1.00 + y 115 0.00 23,000.00 200.00 200.00 1.00 + z 55 0.00 5,500.00 100.00 100.00 1.00 + +Now we can shift the ``origin``, but we also need to take into account the +padding cells. We will set the origin to the westmost-southmost corner of the +survey minus the padding distance we added to the mesh (3km): + +.. code:: python + + mesh.origin = (448353.0 - 3000, 7578158.0 - 3000, -5000) + print(mesh) + +.. code:: + + TensorMesh: 1,265,000 cells + + MESH EXTENT CELL WIDTH FACTOR + dir nC min max min max max + --- --- --------------------------- ------------------ ------ + x 200 445,353.00 485,353.00 200.00 200.00 1.00 + y 115 7,575,158.00 7,598,158.00 200.00 200.00 1.00 + z 55 -5,000.00 500.00 100.00 100.00 1.00 + + +Considering padding: padding factor +----------------------------------- + +Alternatively, we can introduce padding through a *padding factor*. Instead of +creating padding cells of the same size, we can use the padding factor to +create padding cells that increase in volume as they move away from the survey +area. +This is the usual approach to add padding cells to +:class:`discretize.TensorMesh` since it reduces the amount of cells in the +mesh, making inversions less expensive. + +Following the previous example, let's add 7 cells to each side of the +horizontal directions. Let's make the first cells the same size of the ones in +the mesh, and then start increasing their size with a factor of 1.5: + +.. code:: python + + n_pad_cells = 7 + factor = 1.5 + + hx = [(dx, n_pad_cells, -factor), (dx, nx), (dx, n_pad_cells, factor)] + hy = [(dy, n_pad_cells, -factor), (dy, ny), (dy, n_pad_cells, factor)] + hz = [(dz, nz)] + + mesh = discretize.TensorMesh([hx, hy, hz]) + print(mesh) + +.. code:: + + TensorMesh: 1,001,880 cells + + MESH EXTENT CELL WIDTH FACTOR + dir nC min max min max max + --- --- --------------------------- ------------------ ------ + x 184 0.00 53,303.12 200.00 3,417.19 1.50 + y 99 0.00 36,303.12 200.00 3,417.19 1.50 + z 55 0.00 5,500.00 100.00 100.00 1.00 + + +As before, we need to consider the padding cells when shifting the ``origin`` +of the mesh. Since we know that we added 7 cells to each side, we can leverage +that by shifting the 7th node of the x and y axes to the westmost-southmost +corner of the survey: + +.. code:: python + + x_node_7th = mesh.nodes_x[n_pad_cells] + y_node_7th = mesh.nodes_y[n_pad_cells] + mesh.origin = (448353.0 - x_node_7th, 7578158.0 - y_node_7th, -5000) + print(mesh) + +.. code:: + + TensorMesh: 1,001,880 cells + + MESH EXTENT CELL WIDTH FACTOR + dir nC min max min max max + --- --- --------------------------- ------------------ ------ + x 184 438,701.44 492,004.56 200.00 3,417.19 1.50 + y 99 7,568,506.44 7,604,809.56 200.00 3,417.19 1.50 + z 55 -5,000.00 500.00 100.00 100.00 1.00 diff --git a/docs/content/user-guide/index.rst b/docs/content/user-guide/index.rst index 1de62a4bf7..0df94350fc 100644 --- a/docs/content/user-guide/index.rst +++ b/docs/content/user-guide/index.rst @@ -26,6 +26,7 @@ For details on the available classes and functions in SimPEG, please visit the :caption: How to Guide how-to-guide/choosing-solvers + how-to-guide/move-mesh-to-survey.rst .. toctree:: :glob: From 3fc8383aa04fe0c67df9bbd92d0b8509dd346c8e Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 29 Sep 2025 08:56:08 -0700 Subject: [PATCH 170/194] Remove gravity and magnetic tutorials (#1704) Add an admonition pointing users to the respective tutorials in User Tutorials. --- .../03-gravity/plot_1a_gravity_anomaly.py | 261 +------- .../03-gravity/plot_1b_gravity_gradiometry.py | 285 +-------- .../03-gravity/plot_inv_1a_gravity_anomaly.py | 442 +------------- .../plot_inv_1b_gravity_anomaly_irls.py | 448 +------------- ..._gravity_anomaly_irls_compare_weighting.py | 569 +----------------- .../04-magnetics/plot_2a_magnetics_induced.py | 246 +------- .../04-magnetics/plot_2b_magnetics_mvi.py | 297 +-------- .../plot_inv_2a_magnetics_induced.py | 466 +------------- 8 files changed, 48 insertions(+), 2966 deletions(-) diff --git a/tutorials/03-gravity/plot_1a_gravity_anomaly.py b/tutorials/03-gravity/plot_1a_gravity_anomaly.py index 25e442ed45..8665fc8189 100644 --- a/tutorials/03-gravity/plot_1a_gravity_anomaly.py +++ b/tutorials/03-gravity/plot_1a_gravity_anomaly.py @@ -2,262 +2,13 @@ Forward Simulation of Gravity Anomaly Data on a Tensor Mesh =========================================================== -Here we use the module *simpeg.potential_fields.gravity* to predict gravity -anomaly data for a synthetic density contrast model. The simulation is -carried out on a tensor mesh. For this tutorial, we focus on the following: +.. important:: - - How to create gravity surveys - - How to predict gravity anomaly data for a density contrast model - - How to include surface topography - - The units of the density contrast model and resulting data + This tutorial has been moved to `User Tutorials + `_. + Checkout the `3D Forward Simulation of Gravity Anomaly Data + `_ tutorial. -""" - -######################################################################### -# Import Modules -# -------------- -# - -import numpy as np -from scipy.interpolate import LinearNDInterpolator -import matplotlib as mpl -import matplotlib.pyplot as plt -import os - -from discretize import TensorMesh -from discretize.utils import mkvc, active_from_xyz - -from simpeg.utils import plot2Ddata, model_builder -from simpeg import maps -from simpeg.potential_fields import gravity - -save_output = False - -# sphinx_gallery_thumbnail_number = 2 - -############################################# -# Defining Topography -# ------------------- -# -# Surface topography is defined as an (N, 3) numpy array. We create it here but -# the topography could also be loaded from a file. -# - -[x_topo, y_topo] = np.meshgrid(np.linspace(-200, 200, 41), np.linspace(-200, 200, 41)) -z_topo = -15 * np.exp(-(x_topo**2 + y_topo**2) / 80**2) -x_topo, y_topo, z_topo = mkvc(x_topo), mkvc(y_topo), mkvc(z_topo) -topo_xyz = np.c_[x_topo, y_topo, z_topo] - - -############################################# -# Defining the Survey -# ------------------- -# -# Here, we define survey that will be used for the forward simulation. Gravity -# surveys are simple to create. The user only needs an (N, 3) array to define -# the xyz locations of the observation locations, and a list of field components -# which are to be measured. -# - -# Define the observation locations as an (N, 3) numpy array or load them. -x = np.linspace(-80.0, 80.0, 17) -y = np.linspace(-80.0, 80.0, 17) -x, y = np.meshgrid(x, y) -x, y = mkvc(x.T), mkvc(y.T) -fun_interp = LinearNDInterpolator(np.c_[x_topo, y_topo], z_topo) -z = fun_interp(np.c_[x, y]) + 5.0 -receiver_locations = np.c_[x, y, z] - -# Define the component(s) of the field we want to simulate as strings within -# a list. Here we simulate only the vertical component of gravity anomaly. -components = ["gz"] - -# Use the observation locations and components to define the receivers. To -# simulate data, the receivers must be defined as a list. -receiver_list = gravity.receivers.Point(receiver_locations, components=components) - -receiver_list = [receiver_list] - -# Defining the source field. -source_field = gravity.sources.SourceField(receiver_list=receiver_list) - -# Defining the survey -survey = gravity.survey.Survey(source_field) - - -############################################# -# Defining a Tensor Mesh -# ---------------------- -# -# Here, we create the tensor mesh that will be used to predict gravity anomaly -# data. -# - -dh = 5.0 -hx = [(dh, 5, -1.3), (dh, 40), (dh, 5, 1.3)] -hy = [(dh, 5, -1.3), (dh, 40), (dh, 5, 1.3)] -hz = [(dh, 5, -1.3), (dh, 15)] -mesh = TensorMesh([hx, hy, hz], "CCN") - -######################################################## -# Density Contrast Model and Mapping on Tensor Mesh -# ------------------------------------------------- -# -# Here, we create the density contrast model that will be used to predict -# gravity anomaly data and the mapping from the model to the mesh. The model -# consists of a less dense block and a more dense sphere. -# - -# Define density contrast values for each unit in g/cc -background_density = 0.0 -block_density = -0.2 -sphere_density = 0.2 - -# Find the indices for the active mesh cells (e.g. cells below surface) -ind_active = active_from_xyz(mesh, topo_xyz) - -# Define mapping from model to active cells. The model consists of a value for -# each cell below the Earth's surface. -nC = int(ind_active.sum()) -model_map = maps.IdentityMap(nP=nC) - -# Define model. Models in SimPEG are vector arrays. -model = background_density * np.ones(nC) -# You could find the indicies of specific cells within the model and change their -# value to add structures. -ind_block = ( - (mesh.gridCC[ind_active, 0] > -50.0) - & (mesh.gridCC[ind_active, 0] < -20.0) - & (mesh.gridCC[ind_active, 1] > -15.0) - & (mesh.gridCC[ind_active, 1] < 15.0) - & (mesh.gridCC[ind_active, 2] > -50.0) - & (mesh.gridCC[ind_active, 2] < -30.0) -) -model[ind_block] = block_density - -# You can also use SimPEG utilities to add structures to the model more concisely -ind_sphere = model_builder.get_indices_sphere( - np.r_[35.0, 0.0, -40.0], 15.0, mesh.gridCC -) -ind_sphere = ind_sphere[ind_active] -model[ind_sphere] = sphere_density - -# Plot Density Contrast Model -fig = plt.figure(figsize=(9, 4)) -plotting_map = maps.InjectActiveCells(mesh, ind_active, np.nan) - -ax1 = fig.add_axes([0.1, 0.12, 0.73, 0.78]) -mesh.plot_slice( - plotting_map * model, - normal="Y", - ax=ax1, - ind=int(mesh.shape_cells[1] / 2), - grid=True, - clim=(np.min(model), np.max(model)), - pcolor_opts={"cmap": "viridis"}, -) -ax1.set_title("Model slice at y = 0 m") -ax1.set_xlabel("x (m)") -ax1.set_ylabel("z (m)") - -ax2 = fig.add_axes([0.85, 0.12, 0.05, 0.78]) -norm = mpl.colors.Normalize(vmin=np.min(model), vmax=np.max(model)) -cbar = mpl.colorbar.ColorbarBase( - ax2, norm=norm, orientation="vertical", cmap=mpl.cm.viridis -) -cbar.set_label("$g/cm^3$", rotation=270, labelpad=15, size=12) - -plt.show() - - -####################################################### -# Simulation: Gravity Anomaly Data on Tensor Mesh -# ----------------------------------------------- -# -# Here we demonstrate how to predict gravity anomaly data using the integral -# formulation. -# - -############################################################################### -# Define the forward simulation. By setting the ``store_sensitivities`` keyword -# argument to ``"forward_only"``, we simulate the data without storing the -# sensitivities. -# - -simulation = gravity.simulation.Simulation3DIntegral( - survey=survey, - mesh=mesh, - rhoMap=model_map, - active_cells=ind_active, - store_sensitivities="forward_only", - engine="choclo", -) - -############################################################################### -# .. tip:: -# -# Since SimPEG v0.21.0 we can use `Choclo -# `_ as the engine for running the gravity -# simulations, which results in faster and more memory efficient runs. Just -# pass ``engine="choclo"`` when constructing the simulation. -# - -############################################################################### -# Compute predicted data for some model -# SimPEG uses right handed coordinate where Z is positive upward. -# This causes gravity signals look "inconsistent" with density values in visualization. - -dpred = simulation.dpred(model) - -# Plot -fig = plt.figure(figsize=(7, 5)) - -v_max = np.max(np.abs(dpred)) - -ax1 = fig.add_axes([0.1, 0.1, 0.75, 0.85]) -plot2Ddata( - receiver_list[0].locations, - dpred, - clim=(-v_max, v_max), - ax=ax1, - contourOpts={"cmap": "bwr"}, -) -ax1.set_title("Gravity Anomaly (Z-component)") -ax1.set_xlabel("x (m)") -ax1.set_ylabel("y (m)") - -ax2 = fig.add_axes([0.82, 0.1, 0.03, 0.85]) -norm = mpl.colors.Normalize(vmin=-v_max, vmax=v_max) -cbar = mpl.colorbar.ColorbarBase( - ax2, norm=norm, orientation="vertical", cmap=mpl.cm.bwr, format="%.1e" -) -cbar.set_label("$mgal$", rotation=270, labelpad=15, size=12) - -plt.show() - - -####################################################### -# Optional: Exporting Results -# --------------------------- -# -# Write the data, topography and true model -# - -if save_output: - dir_path = os.path.dirname(__file__).split(os.path.sep) - dir_path.extend(["outputs"]) - dir_path = os.path.sep.join(dir_path) + os.path.sep - - if not os.path.exists(dir_path): - os.mkdir(dir_path) - - fname = dir_path + "gravity_topo.txt" - np.savetxt(fname, np.c_[topo_xyz], fmt="%.4e") - - np.random.seed(737) - maximum_anomaly = np.max(np.abs(dpred)) - noise = 0.01 * maximum_anomaly * np.random.randn(len(dpred)) - fname = dir_path + "gravity_data.obs" - np.savetxt(fname, np.c_[receiver_locations, dpred + noise], fmt="%.4e") +""" diff --git a/tutorials/03-gravity/plot_1b_gravity_gradiometry.py b/tutorials/03-gravity/plot_1b_gravity_gradiometry.py index 8bb79ffb17..1c26443b9d 100644 --- a/tutorials/03-gravity/plot_1b_gravity_gradiometry.py +++ b/tutorials/03-gravity/plot_1b_gravity_gradiometry.py @@ -2,287 +2,12 @@ Forward Simulation of Gradiometry Data on a Tree Mesh ===================================================== -Here we use the module *simpeg.potential_fields.gravity* to predict gravity -gradiometry data for a synthetic density contrast model. The simulation is -carried out on a tree mesh. For this tutorial, we focus on the following: +.. important:: - - How to define the survey when we want multiple field components - - How to predict gravity gradiometry data for a density contrast model - - How to construct tree meshes based on topography and survey geometry - - The units of the density contrast model and resulting data + This tutorial has been moved to `User Tutorials + `_. + Checkout the `3D Forward Simulation of Gravity Gradiometry Data + `_ tutorial. """ - -######################################################################### -# Import Modules -# -------------- -# - -import numpy as np -from scipy.interpolate import LinearNDInterpolator -import matplotlib as mpl -import matplotlib.pyplot as plt - -from discretize import TreeMesh -from discretize.utils import mkvc, refine_tree_xyz, active_from_xyz -from simpeg.utils import plot2Ddata, model_builder -from simpeg import maps -from simpeg.potential_fields import gravity - -# sphinx_gallery_thumbnail_number = 2 - -############################################# -# Defining Topography -# ------------------- -# -# Surface topography is defined as an (N, 3) numpy array. We create it here but -# the topography could also be loaded from a file. -# - -[x_topo, y_topo] = np.meshgrid(np.linspace(-200, 200, 41), np.linspace(-200, 200, 41)) -z_topo = -15 * np.exp(-(x_topo**2 + y_topo**2) / 80**2) -x_topo, y_topo, z_topo = mkvc(x_topo), mkvc(y_topo), mkvc(z_topo) -xyz_topo = np.c_[x_topo, y_topo, z_topo] - - -############################################# -# Defining the Survey -# ------------------- -# -# Here, we define survey that will be used for the forward simulation. Gravity -# surveys are simple to create. The user only needs an (N, 3) array to define -# the xyz locations of the observation locations, and a list of field components -# which are to be measured. -# - -# Define the observation locations as an (N, 3) numpy array or load them -x = np.linspace(-80.0, 80.0, 17) -y = np.linspace(-80.0, 80.0, 17) -x, y = np.meshgrid(x, y) -x, y = mkvc(x.T), mkvc(y.T) -fun_interp = LinearNDInterpolator(np.c_[x_topo, y_topo], z_topo) -z = fun_interp(np.c_[x, y]) + 5 -receiver_locations = np.c_[x, y, z] - -# Define the component(s) of the field we want to simulate as strings within -# a list. Here we measure the x, y and z components of the gravity anomaly at -# each observation location. -components = ["gxz", "gyz", "gzz"] - -# Use the observation locations and components to define the receivers. To -# simulate data, the receivers must be defined as a list. -receiver_list = gravity.receivers.Point(receiver_locations, components=components) - -receiver_list = [receiver_list] - -# Defining the source field. -source_field = gravity.sources.SourceField(receiver_list=receiver_list) - -# Defining the survey -survey = gravity.survey.Survey(source_field) - - -########################################################## -# Defining an OcTree Mesh -# ----------------------- -# -# Here, we create the OcTree mesh that will be used in the forward simulation. -# - -dx = 5 # minimum cell width (base mesh cell width) in x -dy = 5 # minimum cell width (base mesh cell width) in y -dz = 5 # minimum cell width (base mesh cell width) in z - -x_length = 240.0 # domain width in x -y_length = 240.0 # domain width in y -z_length = 120.0 # domain width in z - -# Compute number of base mesh cells required in x and y -nbcx = 2 ** int(np.round(np.log(x_length / dx) / np.log(2.0))) -nbcy = 2 ** int(np.round(np.log(y_length / dy) / np.log(2.0))) -nbcz = 2 ** int(np.round(np.log(z_length / dz) / np.log(2.0))) - -# Define the base mesh -hx = [(dx, nbcx)] -hy = [(dy, nbcy)] -hz = [(dz, nbcz)] -mesh = TreeMesh([hx, hy, hz], x0="CCN") - -# Refine based on surface topography -mesh = refine_tree_xyz( - mesh, xyz_topo, octree_levels=[2, 2], method="surface", finalize=False -) - -# Refine box based on region of interest -xp, yp, zp = np.meshgrid([-100.0, 100.0], [-100.0, 100.0], [-80.0, 0.0]) -xyz = np.c_[mkvc(xp), mkvc(yp), mkvc(zp)] - -mesh = refine_tree_xyz(mesh, xyz, octree_levels=[2, 2], method="box", finalize=False) - -mesh.finalize() - -####################################################### -# Density Contrast Model and Mapping on OcTree Mesh -# ------------------------------------------------- -# -# Here, we create the density contrast model that will be used to simulate gravity -# gradiometry data and the mapping from the model to the mesh. The model -# consists of a less dense block and a more dense sphere. -# - -# Define density contrast values for each unit in g/cc -background_density = 0.0 -block_density = -0.1 -sphere_density = 0.1 - -# Find the indecies for the active mesh cells (e.g. cells below surface) -ind_active = active_from_xyz(mesh, xyz_topo) - -# Define mapping from model to active cells. The model consists of a value for -# each cell below the Earth's surface. -nC = int(ind_active.sum()) -model_map = maps.IdentityMap(nP=nC) # model will be value of active cells - -# Define model. Models in SimPEG are vector arrays. -model = background_density * np.ones(nC) - -# You could find the indicies of specific cells within the model and change their -# value to add structures. -ind_block = ( - (mesh.gridCC[ind_active, 0] > -50.0) - & (mesh.gridCC[ind_active, 0] < -20.0) - & (mesh.gridCC[ind_active, 1] > -15.0) - & (mesh.gridCC[ind_active, 1] < 15.0) - & (mesh.gridCC[ind_active, 2] > -50.0) - & (mesh.gridCC[ind_active, 2] < -30.0) -) -model[ind_block] = block_density - -# You can also use SimPEG utilities to add structures to the model more concisely -ind_sphere = model_builder.get_indices_sphere( - np.r_[35.0, 0.0, -40.0], 15.0, mesh.gridCC -) -ind_sphere = ind_sphere[ind_active] -model[ind_sphere] = sphere_density - -# Plot Density Contrast Model -fig = plt.figure(figsize=(9, 4)) -plotting_map = maps.InjectActiveCells(mesh, ind_active, np.nan) - -ax1 = fig.add_axes([0.1, 0.12, 0.73, 0.78]) -mesh.plot_slice( - plotting_map * model, - normal="Y", - ax=ax1, - ind=int(mesh.h[1].size / 2), - grid=True, - clim=(np.min(model), np.max(model)), - pcolor_opts={"cmap": "viridis"}, -) -ax1.set_title("Model slice at y = 0 m") -ax1.set_xlabel("x (m)") -ax1.set_ylabel("z (m)") - -ax2 = fig.add_axes([0.85, 0.12, 0.05, 0.78]) -norm = mpl.colors.Normalize(vmin=np.min(model), vmax=np.max(model)) -cbar = mpl.colorbar.ColorbarBase( - ax2, norm=norm, orientation="vertical", cmap=mpl.cm.viridis -) -cbar.set_label("$g/cm^3$", rotation=270, labelpad=15, size=12) - -plt.show() - -############################################################## -# Simulation: Gravity Gradiometry Data on an OcTree Mesh -# ------------------------------------------------------ -# -# Here we demonstrate how to predict gravity anomaly data using the integral -# formulation. -# - -############################################################################### -# Define the forward simulation. By setting the ``store_sensitivities`` keyword -# argument to ``"forward_only"``, we simulate the data without storing the -# sensitivities -# - -simulation = gravity.simulation.Simulation3DIntegral( - survey=survey, - mesh=mesh, - rhoMap=model_map, - active_cells=ind_active, - store_sensitivities="forward_only", - engine="choclo", -) - -############################################################################### -# .. tip:: -# -# Since SimPEG v0.21.0 we can use `Choclo -# `_ as the engine for running the gravity -# simulations, which results in faster and more memory efficient runs. Just -# pass ``engine="choclo"`` when constructing the simulation. -# - -############################################################################### -# Compute predicted data for some model - -dpred = simulation.dpred(model) -n_data = len(dpred) - -# Plot -fig = plt.figure(figsize=(10, 3)) -n_locations = receiver_locations.shape[0] -v_max = np.max(np.abs(dpred)) - -ax1 = fig.add_axes([0.1, 0.15, 0.25, 0.78]) -cplot1 = plot2Ddata( - receiver_locations, - dpred[0:n_data:3], - ax=ax1, - ncontour=30, - clim=(-v_max, v_max), - contourOpts={"cmap": "bwr"}, -) -cplot1[0].set_clim((-v_max, v_max)) -ax1.set_title(r"$\partial g /\partial x$") -ax1.set_xlabel("x (m)") -ax1.set_ylabel("y (m)") - -ax2 = fig.add_axes([0.36, 0.15, 0.25, 0.78]) -cplot2 = plot2Ddata( - receiver_locations, - dpred[1:n_data:3], - ax=ax2, - ncontour=30, - clim=(-v_max, v_max), - contourOpts={"cmap": "bwr"}, -) -cplot2[0].set_clim((-v_max, v_max)) -ax2.set_title(r"$\partial g /\partial y$") -ax2.set_xlabel("x (m)") -ax2.set_yticks([]) - -ax3 = fig.add_axes([0.62, 0.15, 0.25, 0.78]) -cplot3 = plot2Ddata( - receiver_locations, - dpred[2:n_data:3], - ax=ax3, - ncontour=30, - clim=(-v_max, v_max), - contourOpts={"cmap": "bwr"}, -) -cplot3[0].set_clim((-v_max, v_max)) -ax3.set_title(r"$\partial g /\partial z$") -ax3.set_xlabel("x (m)") -ax3.set_yticks([]) - -ax4 = fig.add_axes([0.89, 0.13, 0.02, 0.79]) -norm = mpl.colors.Normalize(vmin=-v_max, vmax=v_max) -cbar = mpl.colorbar.ColorbarBase( - ax4, norm=norm, orientation="vertical", cmap=mpl.cm.bwr -) -cbar.set_label("Eotvos", rotation=270, labelpad=15, size=12) - -plt.show() diff --git a/tutorials/03-gravity/plot_inv_1a_gravity_anomaly.py b/tutorials/03-gravity/plot_inv_1a_gravity_anomaly.py index 27e679db43..3f3b9cda85 100644 --- a/tutorials/03-gravity/plot_inv_1a_gravity_anomaly.py +++ b/tutorials/03-gravity/plot_inv_1a_gravity_anomaly.py @@ -2,444 +2,12 @@ Least-Squares Inversion of Gravity Anomaly Data =============================================== -Here we invert gravity anomaly data to recover a density contrast model. We -formulate the inverse problem as a least-squares optimization problem. For -this tutorial, we focus on the following: +.. important:: - - Defining the survey from xyz formatted data - - Generating a mesh based on survey geometry - - Including surface topography - - Defining the inverse problem (data misfit, regularization, optimization) - - Specifying directives for the inversion - - Plotting the recovered model and data misfit - -Although we consider gravity anomaly data in this tutorial, the same approach -can be used to invert gradiometry and other types of geophysical data. + This tutorial has been moved to `User Tutorials + `_. + Checkout the `3D Inversion of Gravity Anomaly Data + `_ tutorial. """ - -######################################################################### -# Import modules -# -------------- -# - -import os -import numpy as np -import matplotlib as mpl -import matplotlib.pyplot as plt -import tarfile - -from discretize import TensorMesh -from discretize.utils import active_from_xyz -from simpeg.utils import plot2Ddata, model_builder -from simpeg.potential_fields import gravity -from simpeg import ( - maps, - data, - data_misfit, - inverse_problem, - regularization, - optimization, - directives, - inversion, - utils, -) - -# sphinx_gallery_thumbnail_number = 3 - -############################################# -# Define File Names -# ----------------- -# -# File paths for assets we are loading. To set up the inversion, we require -# topography and field observations. The true model defined on the whole mesh -# is loaded to compare with the inversion result. These files are stored as a -# tar-file on our google cloud bucket: -# "https://storage.googleapis.com/simpeg/doc-assets/gravity.tar.gz" -# - -# storage bucket where we have the data -data_source = "https://storage.googleapis.com/simpeg/doc-assets/gravity.tar.gz" - -# download the data -downloaded_data = utils.download(data_source, overwrite=True) - -# unzip the tarfile -tar = tarfile.open(downloaded_data, "r") -tar.extractall() -tar.close() - -# path to the directory containing our data -dir_path = downloaded_data.split(".")[0] + os.path.sep - -# files to work with -topo_filename = dir_path + "gravity_topo.txt" -data_filename = dir_path + "gravity_data.obs" - - -############################################# -# Load Data and Plot -# ------------------ -# -# Here we load and plot synthetic gravity anomaly data. Topography is generally -# defined as an (N, 3) array. Gravity data is generally defined with 4 columns: -# x, y, z and data. -# - -# Load topography -xyz_topo = np.loadtxt(str(topo_filename)) - -# Load field data -dobs = np.loadtxt(str(data_filename)) - -# Define receiver locations and observed data -receiver_locations = dobs[:, 0:3] -dobs = dobs[:, -1] - -# Plot -mpl.rcParams.update({"font.size": 12}) -fig = plt.figure(figsize=(7, 5)) - -ax1 = fig.add_axes([0.1, 0.1, 0.73, 0.85]) -plot2Ddata(receiver_locations, dobs, ax=ax1, contourOpts={"cmap": "bwr"}) -ax1.set_title("Gravity Anomaly") -ax1.set_xlabel("x (m)") -ax1.set_ylabel("y (m)") - -ax2 = fig.add_axes([0.8, 0.1, 0.03, 0.85]) -norm = mpl.colors.Normalize(vmin=-np.max(np.abs(dobs)), vmax=np.max(np.abs(dobs))) -cbar = mpl.colorbar.ColorbarBase( - ax2, norm=norm, orientation="vertical", cmap=mpl.cm.bwr, format="%.1e" -) -cbar.set_label("$mgal$", rotation=270, labelpad=15, size=12) - -plt.show() - -############################################# -# Assign Uncertainties -# -------------------- -# -# Inversion with SimPEG requires that we define the standard deviation of our data. -# This represents our estimate of the noise in our data. For a gravity inversion, -# a constant floor value is generally applied to all data. For this tutorial, -# the standard deviation on each datum will be 1% of the maximum observed -# gravity anomaly value. -# - -maximum_anomaly = np.max(np.abs(dobs)) - -uncertainties = 0.01 * maximum_anomaly * np.ones(np.shape(dobs)) - -############################################# -# Defining the Survey -# ------------------- -# -# Here, we define the survey that will be used for this tutorial. Gravity -# surveys are simple to create. The user only needs an (N, 3) array to define -# the xyz locations of the observation locations. From this, the user can -# define the receivers and the source field. -# - -# Define the receivers. The data consists of vertical gravity anomaly measurements. -# The set of receivers must be defined as a list. -receiver_list = gravity.receivers.Point(receiver_locations, components="gz") - -receiver_list = [receiver_list] - -# Define the source field -source_field = gravity.sources.SourceField(receiver_list=receiver_list) - -# Define the survey -survey = gravity.survey.Survey(source_field) - -############################################# -# Defining the Data -# ----------------- -# -# Here is where we define the data that is inverted. The data is defined by -# the survey, the observation values and the standard deviation. -# - -data_object = data.Data(survey, dobs=dobs, standard_deviation=uncertainties) - - -############################################# -# Defining a Tensor Mesh -# ---------------------- -# -# Here, we create the tensor mesh that will be used to invert gravity anomaly -# data. If desired, we could define an OcTree mesh. -# - -dh = 5.0 -hx = [(dh, 5, -1.3), (dh, 40), (dh, 5, 1.3)] -hy = [(dh, 5, -1.3), (dh, 40), (dh, 5, 1.3)] -hz = [(dh, 5, -1.3), (dh, 15)] -mesh = TensorMesh([hx, hy, hz], "CCN") - -######################################################## -# Starting/Reference Model and Mapping on Tensor Mesh -# --------------------------------------------------- -# -# Here, we create starting and/or reference models for the inversion as -# well as the mapping from the model space to the active cells. Starting and -# reference models can be a constant background value or contain a-priori -# structures. -# - -# Find the indices of the active cells in forward model (ones below surface) -ind_active = active_from_xyz(mesh, xyz_topo) - -# Define mapping from model to active cells -nC = int(ind_active.sum()) -model_map = maps.IdentityMap(nP=nC) # model consists of a value for each active cell - -# Define and plot starting model -starting_model = np.zeros(nC) - - -############################################## -# Define the Physics -# ------------------ -# -# Here, we define the physics of the gravity problem by using the simulation -# class. -# -# .. tip:: -# -# Since SimPEG v0.21.0 we can use `Choclo -# `_ as the engine for running the gravity -# simulations, which results in faster and more memory efficient runs. Just -# pass ``engine="choclo"`` when constructing the simulation. -# - -simulation = gravity.simulation.Simulation3DIntegral( - survey=survey, - mesh=mesh, - rhoMap=model_map, - active_cells=ind_active, - engine="choclo", -) - - -####################################################################### -# Define the Inverse Problem -# -------------------------- -# -# The inverse problem is defined by 3 things: -# -# 1) Data Misfit: a measure of how well our recovered model explains the field data -# 2) Regularization: constraints placed on the recovered model and a priori information -# 3) Optimization: the numerical approach used to solve the inverse problem -# - -# Define the data misfit. Here the data misfit is the L2 norm of the weighted -# residual between the observed data and the data predicted for a given model. -# Within the data misfit, the residual between predicted and observed data are -# normalized by the data's standard deviation. -dmis = data_misfit.L2DataMisfit(data=data_object, simulation=simulation) - -# Define the regularization (model objective function). -reg = regularization.WeightedLeastSquares( - mesh, active_cells=ind_active, mapping=model_map -) - -# Define how the optimization problem is solved. Here we will use a projected -# Gauss-Newton approach that employs the conjugate gradient solver. -opt = optimization.ProjectedGNCG( - maxIter=10, lower=-1.0, upper=1.0, maxIterLS=20, maxIterCG=10, tolCG=1e-3 -) - -# Here we define the inverse problem that is to be solved -inv_prob = inverse_problem.BaseInvProblem(dmis, reg, opt) - -####################################################################### -# Define Inversion Directives -# --------------------------- -# -# Here we define any directiveas that are carried out during the inversion. This -# includes the cooling schedule for the trade-off parameter (beta), stopping -# criteria for the inversion and saving inversion results at each iteration. -# - -# Defining a starting value for the trade-off parameter (beta) between the data -# misfit and the regularization. -starting_beta = directives.BetaEstimate_ByEig(beta0_ratio=1e1) - -# Defining the fractional decrease in beta and the number of Gauss-Newton solves -# for each beta value. -beta_schedule = directives.BetaSchedule(coolingFactor=5, coolingRate=1) - -# Options for outputting recovered models and predicted data for each beta. -save_iteration = directives.SaveOutputEveryIteration(save_txt=False) - -# Updating the preconditionner if it is model dependent. -update_jacobi = directives.UpdatePreconditioner() - -# Setting a stopping criteria for the inversion. -target_misfit = directives.TargetMisfit(chifact=1) - -# Add sensitivity weights -sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) - -# The directives are defined as a list. -directives_list = [ - sensitivity_weights, - starting_beta, - beta_schedule, - save_iteration, - update_jacobi, - target_misfit, -] - -##################################################################### -# Running the Inversion -# --------------------- -# -# To define the inversion object, we need to define the inversion problem and -# the set of directives. We can then run the inversion. -# - -# Here we combine the inverse problem and the set of directives -inv = inversion.BaseInversion(inv_prob, directives_list) - -# Run inversion -recovered_model = inv.run(starting_model) - - -############################################################ -# Recreate True Model -# ------------------- -# - -# Define density contrast values for each unit in g/cc -background_density = 0.0 -block_density = -0.2 -sphere_density = 0.2 - -# Define model. Models in SimPEG are vector arrays. -true_model = background_density * np.ones(nC) - -# You could find the indicies of specific cells within the model and change their -# value to add structures. -ind_block = ( - (mesh.gridCC[ind_active, 0] > -50.0) - & (mesh.gridCC[ind_active, 0] < -20.0) - & (mesh.gridCC[ind_active, 1] > -15.0) - & (mesh.gridCC[ind_active, 1] < 15.0) - & (mesh.gridCC[ind_active, 2] > -50.0) - & (mesh.gridCC[ind_active, 2] < -30.0) -) -true_model[ind_block] = block_density - -# You can also use SimPEG utilities to add structures to the model more concisely -ind_sphere = model_builder.get_indices_sphere( - np.r_[35.0, 0.0, -40.0], 15.0, mesh.gridCC -) -ind_sphere = ind_sphere[ind_active] -true_model[ind_sphere] = sphere_density - - -############################################################ -# Plotting True Model and Recovered Model -# --------------------------------------- -# - -# Plot True Model -fig = plt.figure(figsize=(9, 4)) -plotting_map = maps.InjectActiveCells(mesh, ind_active, np.nan) - -ax1 = fig.add_axes([0.1, 0.1, 0.73, 0.8]) -mesh.plot_slice( - plotting_map * true_model, - normal="Y", - ax=ax1, - ind=int(mesh.shape_cells[1] / 2), - grid=True, - clim=(np.min(true_model), np.max(true_model)), - pcolor_opts={"cmap": "viridis"}, -) -ax1.set_title("Model slice at y = 0 m") - - -ax2 = fig.add_axes([0.85, 0.1, 0.05, 0.8]) -norm = mpl.colors.Normalize(vmin=np.min(true_model), vmax=np.max(true_model)) -cbar = mpl.colorbar.ColorbarBase( - ax2, norm=norm, orientation="vertical", cmap=mpl.cm.viridis, format="%.1e" -) -cbar.set_label("$g/cm^3$", rotation=270, labelpad=15, size=12) - -plt.show() - -# Plot Recovered Model -fig = plt.figure(figsize=(9, 4)) -plotting_map = maps.InjectActiveCells(mesh, ind_active, np.nan) - -ax1 = fig.add_axes([0.1, 0.1, 0.73, 0.8]) -mesh.plot_slice( - plotting_map * recovered_model, - normal="Y", - ax=ax1, - ind=int(mesh.shape_cells[1] / 2), - grid=True, - clim=(np.min(recovered_model), np.max(recovered_model)), - pcolor_opts={"cmap": "viridis"}, -) -ax1.set_title("Model slice at y = 0 m") - -ax2 = fig.add_axes([0.85, 0.1, 0.05, 0.8]) -norm = mpl.colors.Normalize(vmin=np.min(recovered_model), vmax=np.max(recovered_model)) -cbar = mpl.colorbar.ColorbarBase( - ax2, norm=norm, orientation="vertical", cmap=mpl.cm.viridis -) -cbar.set_label("$g/cm^3$", rotation=270, labelpad=15, size=12) - -plt.show() - -################################################################### -# Plotting Predicted Data and Normalized Misfit -# --------------------------------------------- -# - -# Predicted data with final recovered model -# SimPEG uses right handed coordinate where Z is positive upward. -# This causes gravity signals look "inconsistent" with density values in visualization. -dpred = inv_prob.dpred - -# Observed data | Predicted data | Normalized data misfit -data_array = np.c_[dobs, dpred, (dobs - dpred) / uncertainties] - -fig = plt.figure(figsize=(17, 4)) -plot_title = ["Observed", "Predicted", "Normalized Misfit"] -plot_units = ["mgal", "mgal", ""] - -ax1 = 3 * [None] -ax2 = 3 * [None] -norm = 3 * [None] -cbar = 3 * [None] -cplot = 3 * [None] -v_lim = [np.max(np.abs(dobs)), np.max(np.abs(dobs)), np.max(np.abs(data_array[:, 2]))] - -for ii in range(0, 3): - ax1[ii] = fig.add_axes([0.33 * ii + 0.03, 0.11, 0.23, 0.84]) - cplot[ii] = plot2Ddata( - receiver_list[0].locations, - data_array[:, ii], - ax=ax1[ii], - ncontour=30, - clim=(-v_lim[ii], v_lim[ii]), - contourOpts={"cmap": "bwr"}, - ) - ax1[ii].set_title(plot_title[ii]) - ax1[ii].set_xlabel("x (m)") - ax1[ii].set_ylabel("y (m)") - - ax2[ii] = fig.add_axes([0.33 * ii + 0.25, 0.11, 0.01, 0.85]) - norm[ii] = mpl.colors.Normalize(vmin=-v_lim[ii], vmax=v_lim[ii]) - cbar[ii] = mpl.colorbar.ColorbarBase( - ax2[ii], norm=norm[ii], orientation="vertical", cmap=mpl.cm.bwr - ) - cbar[ii].set_label(plot_units[ii], rotation=270, labelpad=15, size=12) - -plt.show() diff --git a/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py b/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py index 322afbd08e..9b4808a4cf 100644 --- a/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py +++ b/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py @@ -2,446 +2,16 @@ Sparse Norm Inversion of Gravity Anomaly Data ============================================= -Here we invert gravity anomaly data to recover a density contrast model. We formulate the inverse problem as an iteratively -re-weighted least-squares (IRLS) optimization problem. For this tutorial, we -focus on the following: +.. important:: - - Defining the survey from xyz formatted data - - Generating a mesh based on survey geometry - - Including surface topography - - Defining the inverse problem (data misfit, regularization, optimization) - - Specifying directives for the inversion - - Setting sparse and blocky norms - - Plotting the recovered model and data misfit - -Although we consider gravity anomaly data in this tutorial, the same approach -can be used to invert gradiometry and other types of geophysical data. + This tutorial has been moved to `User Tutorials + `_. + Checkout the + `Iteratively Re-weighted Least-Squares (IRLS) Inversion on a Tree Mesh + `_ + section in the + `3D Inversion of Gravity Anomaly Data + `_ tutorial. """ - -######################################################################### -# Import modules -# -------------- -# - -import os -import numpy as np -import matplotlib as mpl -import matplotlib.pyplot as plt -import tarfile - -from discretize import TensorMesh -from discretize.utils import active_from_xyz -from simpeg.utils import plot2Ddata, model_builder -from simpeg.potential_fields import gravity -from simpeg import ( - maps, - data, - data_misfit, - inverse_problem, - regularization, - optimization, - directives, - inversion, - utils, -) - -# sphinx_gallery_thumbnail_number = 3 - -############################################# -# Define File Names -# ----------------- -# -# File paths for assets we are loading. To set up the inversion, we require -# topography and field observations. The true model defined on the whole mesh -# is loaded to compare with the inversion result. These files are stored as a -# tar-file on our google cloud bucket: -# "https://storage.googleapis.com/simpeg/doc-assets/gravity.tar.gz" -# - -# storage bucket where we have the data -data_source = "https://storage.googleapis.com/simpeg/doc-assets/gravity.tar.gz" - -# download the data -downloaded_data = utils.download(data_source, overwrite=True) - -# unzip the tarfile -tar = tarfile.open(downloaded_data, "r") -tar.extractall() -tar.close() - -# path to the directory containing our data -dir_path = downloaded_data.split(".")[0] + os.path.sep - -# files to work with -topo_filename = dir_path + "gravity_topo.txt" -data_filename = dir_path + "gravity_data.obs" -model_filename = dir_path + "true_model.txt" - - -############################################# -# Load Data and Plot -# ------------------ -# -# Here we load and plot synthetic gravity anomaly data. Topography is generally -# defined as an (N, 3) array. Gravity data is generally defined with 4 columns: -# x, y, z and data. -# - -# Load topography -xyz_topo = np.loadtxt(str(topo_filename)) - -# Load field data -dobs = np.loadtxt(str(data_filename)) - -# Define receiver locations and observed data -receiver_locations = dobs[:, 0:3] -dobs = dobs[:, -1] - -# Plot -mpl.rcParams.update({"font.size": 12}) -fig = plt.figure(figsize=(7, 5)) - -ax1 = fig.add_axes([0.1, 0.1, 0.73, 0.85]) -plot2Ddata(receiver_locations, dobs, ax=ax1, contourOpts={"cmap": "bwr"}) -ax1.set_title("Gravity Anomaly") -ax1.set_xlabel("x (m)") -ax1.set_ylabel("y (m)") - -ax2 = fig.add_axes([0.8, 0.1, 0.03, 0.85]) -norm = mpl.colors.Normalize(vmin=-np.max(np.abs(dobs)), vmax=np.max(np.abs(dobs))) -cbar = mpl.colorbar.ColorbarBase( - ax2, norm=norm, orientation="vertical", cmap=mpl.cm.bwr, format="%.1e" -) -cbar.set_label("$mgal$", rotation=270, labelpad=15, size=12) - -plt.show() - -############################################# -# Assign Uncertainties -# -------------------- -# -# Inversion with SimPEG requires that we define standard deviation on our data. -# This represents our estimate of the noise in our data. For gravity inversion, -# a constant floor value is generally applied to all data. For this tutorial, -# the standard deviation on each datum will be 1% of the maximum observed -# gravity anomaly value. -# - -maximum_anomaly = np.max(np.abs(dobs)) - -uncertainties = 0.01 * maximum_anomaly * np.ones(np.shape(dobs)) - -############################################# -# Defining the Survey -# ------------------- -# -# Here, we define survey that will be used for this tutorial. Gravity -# surveys are simple to create. The user only needs an (N, 3) array to define -# the xyz locations of the observation locations. From this, the user can -# define the receivers and the source field. -# - -# Define the receivers. The data consist of vertical gravity anomaly measurements. -# The set of receivers must be defined as a list. -receiver_list = gravity.receivers.Point(receiver_locations, components="gz") - -receiver_list = [receiver_list] - -# Define the source field -source_field = gravity.sources.SourceField(receiver_list=receiver_list) - -# Define the survey -survey = gravity.survey.Survey(source_field) - -############################################# -# Defining the Data -# ----------------- -# -# Here is where we define the data that are inverted. The data are defined by -# the survey, the observation values and the standard deviation. -# - -data_object = data.Data(survey, dobs=dobs, standard_deviation=uncertainties) - - -############################################# -# Defining a Tensor Mesh -# ---------------------- -# -# Here, we create the tensor mesh that will be used to invert gravity anomaly -# data. If desired, we could define an OcTree mesh. -# - -dh = 5.0 -hx = [(dh, 5, -1.3), (dh, 40), (dh, 5, 1.3)] -hy = [(dh, 5, -1.3), (dh, 40), (dh, 5, 1.3)] -hz = [(dh, 5, -1.3), (dh, 15)] -mesh = TensorMesh([hx, hy, hz], "CCN") - -######################################################## -# Starting/Reference Model and Mapping on Tensor Mesh -# --------------------------------------------------- -# -# Here, we create starting and/or reference models for the inversion as -# well as the mapping from the model space to the active cells. Starting and -# reference models can be a constant background value or contain a-priori -# structures. -# - -# Find the indices of the active cells in forward model (ones below surface) -ind_active = active_from_xyz(mesh, xyz_topo) - -# Define mapping from model to active cells -nC = int(ind_active.sum()) -model_map = maps.IdentityMap(nP=nC) # model consists of a value for each active cell - -# Define and plot starting model -starting_model = np.zeros(nC) - - -############################################## -# Define the Physics -# ------------------ -# -# Here, we define the physics of the gravity problem by using the simulation -# class. -# -# .. tip:: -# -# Since SimPEG v0.21.0 we can use `Choclo -# `_ as the engine for running the gravity -# simulations, which results in faster and more memory efficient runs. Just -# pass ``engine="choclo"`` when constructing the simulation. -# - -simulation = gravity.simulation.Simulation3DIntegral( - survey=survey, - mesh=mesh, - rhoMap=model_map, - active_cells=ind_active, - engine="choclo", -) - - -####################################################################### -# Define the Inverse Problem -# -------------------------- -# -# The inverse problem is defined by 3 things: -# -# 1) Data Misfit: a measure of how well our recovered model explains the field data -# 2) Regularization: constraints placed on the recovered model and a priori information -# 3) Optimization: the numerical approach used to solve the inverse problem -# - -# Define the data misfit. Here the data misfit is the L2 norm of the weighted -# residual between the observed data and the data predicted for a given model. -# Within the data misfit, the residual between predicted and observed data are -# normalized by the data's standard deviation. -dmis = data_misfit.L2DataMisfit(data=data_object, simulation=simulation) -dmis.W = utils.sdiag(1 / uncertainties) - -# Define the regularization (model objective function). -reg = regularization.Sparse(mesh, active_cells=ind_active, mapping=model_map) -reg.norms = [0, 2, 2, 2] - -# Define how the optimization problem is solved. Here we will use a projected -# Gauss-Newton approach that employs the conjugate gradient solver. -opt = optimization.ProjectedGNCG( - maxIter=100, lower=-1.0, upper=1.0, maxIterLS=20, maxIterCG=10, tolCG=1e-3 -) - -# Here we define the inverse problem that is to be solved -inv_prob = inverse_problem.BaseInvProblem(dmis, reg, opt) - -####################################################################### -# Define Inversion Directives -# --------------------------- -# -# Here we define any directiveas that are carried out during the inversion. This -# includes the cooling schedule for the trade-off parameter (beta), stopping -# criteria for the inversion and saving inversion results at each iteration. -# - -# Defining a starting value for the trade-off parameter (beta) between the data -# misfit and the regularization. -starting_beta = directives.BetaEstimate_ByEig(beta0_ratio=1e0) - -# Defines the directives for the IRLS regularization. This includes setting -# the cooling schedule for the trade-off parameter. -update_IRLS = directives.UpdateIRLS( - f_min_change=1e-4, - max_irls_iterations=30, - irls_cooling_factor=1.5, - misfit_tolerance=1e-2, -) -# Options for outputting recovered models and predicted data for each beta. -save_iteration = directives.SaveOutputEveryIteration(save_txt=False) - -# Updating the preconditionner if it is model dependent. -update_jacobi = directives.UpdatePreconditioner() - -# Add sensitivity weights -sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) - -# The directives are defined as a list. -directives_list = [ - update_IRLS, - sensitivity_weights, - starting_beta, - save_iteration, - update_jacobi, -] - -##################################################################### -# Running the Inversion -# --------------------- -# -# To define the inversion object, we need to define the inversion problem and -# the set of directives. We can then run the inversion. -# - -# Here we combine the inverse problem and the set of directives -inv = inversion.BaseInversion(inv_prob, directives_list) - -# Run inversion -recovered_model = inv.run(starting_model) - - -############################################################ -# Recreate True Model -# ------------------- -# - -# Define density contrast values for each unit in g/cc -background_density = 0.0 -block_density = -0.2 -sphere_density = 0.2 - -# Define model. Models in SimPEG are vector arrays. -true_model = background_density * np.ones(nC) - -# You could find the indicies of specific cells within the model and change their -# value to add structures. -ind_block = ( - (mesh.gridCC[ind_active, 0] > -50.0) - & (mesh.gridCC[ind_active, 0] < -20.0) - & (mesh.gridCC[ind_active, 1] > -15.0) - & (mesh.gridCC[ind_active, 1] < 15.0) - & (mesh.gridCC[ind_active, 2] > -50.0) - & (mesh.gridCC[ind_active, 2] < -30.0) -) -true_model[ind_block] = block_density - -# You can also use SimPEG utilities to add structures to the model more concisely -ind_sphere = model_builder.get_indices_sphere( - np.r_[35.0, 0.0, -40.0], 15.0, mesh.gridCC -) -ind_sphere = ind_sphere[ind_active] -true_model[ind_sphere] = sphere_density - - -############################################################ -# Plotting True Model and Recovered Model -# --------------------------------------- -# - -# Plot True Model -fig = plt.figure(figsize=(9, 4)) -plotting_map = maps.InjectActiveCells(mesh, ind_active, np.nan) - -ax1 = fig.add_axes([0.1, 0.1, 0.73, 0.8]) -mesh.plot_slice( - plotting_map * true_model, - normal="Y", - ax=ax1, - ind=int(mesh.shape_cells[1] / 2), - grid=True, - clim=(np.min(true_model), np.max(true_model)), - pcolor_opts={"cmap": "viridis"}, -) -ax1.set_title("Model slice at y = 0 m") - - -ax2 = fig.add_axes([0.85, 0.1, 0.05, 0.8]) -norm = mpl.colors.Normalize(vmin=np.min(true_model), vmax=np.max(true_model)) -cbar = mpl.colorbar.ColorbarBase( - ax2, norm=norm, orientation="vertical", cmap=mpl.cm.viridis, format="%.1e" -) -cbar.set_label("$g/cm^3$", rotation=270, labelpad=15, size=12) - -plt.show() - -# Plot Recovered Model -fig = plt.figure(figsize=(9, 4)) -plotting_map = maps.InjectActiveCells(mesh, ind_active, np.nan) - -ax1 = fig.add_axes([0.1, 0.1, 0.73, 0.8]) -mesh.plot_slice( - plotting_map * recovered_model, - normal="Y", - ax=ax1, - ind=int(mesh.shape_cells[1] / 2), - grid=True, - clim=(np.min(recovered_model), np.max(recovered_model)), - pcolor_opts={"cmap": "viridis"}, -) -ax1.set_title("Model slice at y = 0 m") - -ax2 = fig.add_axes([0.85, 0.1, 0.05, 0.8]) -norm = mpl.colors.Normalize(vmin=np.min(recovered_model), vmax=np.max(recovered_model)) -cbar = mpl.colorbar.ColorbarBase( - ax2, norm=norm, orientation="vertical", cmap=mpl.cm.viridis -) -cbar.set_label("$g/cm^3$", rotation=270, labelpad=15, size=12) - -plt.show() - -################################################################### -# Plotting Predicted Data and Normalized Misfit -# --------------------------------------------- -# - -# Predicted data with final recovered model -# SimPEG uses right handed coordinate where Z is positive upward. -# This causes gravity signals look "inconsistent" with density values in visualization. -dpred = inv_prob.dpred - -# Observed data | Predicted data | Normalized data misfit -data_array = np.c_[dobs, dpred, (dobs - dpred) / uncertainties] - -fig = plt.figure(figsize=(17, 4)) -plot_title = ["Observed", "Predicted", "Normalized Misfit"] -plot_units = ["mgal", "mgal", ""] - -ax1 = 3 * [None] -ax2 = 3 * [None] -norm = 3 * [None] -cbar = 3 * [None] -cplot = 3 * [None] -v_lim = [np.max(np.abs(dobs)), np.max(np.abs(dobs)), np.max(np.abs(data_array[:, 2]))] - -for ii in range(0, 3): - ax1[ii] = fig.add_axes([0.33 * ii + 0.03, 0.11, 0.23, 0.84]) - cplot[ii] = plot2Ddata( - receiver_list[0].locations, - data_array[:, ii], - ax=ax1[ii], - ncontour=30, - clim=(-v_lim[ii], v_lim[ii]), - contourOpts={"cmap": "bwr"}, - ) - ax1[ii].set_title(plot_title[ii]) - ax1[ii].set_xlabel("x (m)") - ax1[ii].set_ylabel("y (m)") - - ax2[ii] = fig.add_axes([0.33 * ii + 0.25, 0.11, 0.01, 0.85]) - norm[ii] = mpl.colors.Normalize(vmin=-v_lim[ii], vmax=v_lim[ii]) - cbar[ii] = mpl.colorbar.ColorbarBase( - ax2[ii], norm=norm[ii], orientation="vertical", cmap=mpl.cm.bwr - ) - cbar[ii].set_label(plot_units[ii], rotation=270, labelpad=15, size=12) - -plt.show() diff --git a/tutorials/03-gravity/plot_inv_1c_gravity_anomaly_irls_compare_weighting.py b/tutorials/03-gravity/plot_inv_1c_gravity_anomaly_irls_compare_weighting.py index eae20d8780..8e61754a98 100644 --- a/tutorials/03-gravity/plot_inv_1c_gravity_anomaly_irls_compare_weighting.py +++ b/tutorials/03-gravity/plot_inv_1c_gravity_anomaly_irls_compare_weighting.py @@ -2,569 +2,12 @@ Compare weighting strategy with Inversion of surface Gravity Anomaly Data ========================================================================= -Here we invert gravity anomaly data to recover a density contrast model. We formulate the inverse problem as an iteratively -re-weighted least-squares (IRLS) optimization problem. For this tutorial, we -focus on the following: +.. important:: - - Setting regularization weights - - Defining the survey from xyz formatted data - - Generating a mesh based on survey geometry - - Including surface topography - - Defining the inverse problem (data misfit, regularization, optimization) - - Specifying directives for the inversion - - Setting sparse and blocky norms - - Plotting the recovered model and data misfit + This tutorial has been moved to `User Tutorials + `_. -Although we consider gravity anomaly data in this tutorial, the same approach -can be used to invert gradiometry and other types of geophysical data. -""" - -######################################################################### -# Import modules -# -------------- -# - -import os -import tarfile - -import matplotlib as mpl -import matplotlib.pyplot as plt -import numpy as np -from discretize import TensorMesh -from discretize.utils import active_from_xyz - -from simpeg import ( - data, - data_misfit, - directives, - inverse_problem, - inversion, - maps, - optimization, - regularization, - utils, -) -from simpeg.potential_fields import gravity -from simpeg.utils import model_builder, plot2Ddata - -# sphinx_gallery_thumbnail_number = 3 - -############################################# -# Define File Names -# ----------------- -# -# File paths for assets we are loading. To set up the inversion, we require -# topography and field observations. The true model defined on the whole mesh -# is loaded to compare with the inversion result. These files are stored as a -# tar-file on our google cloud bucket: -# "https://storage.googleapis.com/simpeg/doc-assets/gravity.tar.gz" -# - -# storage bucket where we have the data -data_source = "https://storage.googleapis.com/simpeg/doc-assets/gravity.tar.gz" - -# download the data -downloaded_data = utils.download(data_source, overwrite=True) - -# unzip the tarfile -tar = tarfile.open(downloaded_data, "r") -tar.extractall() -tar.close() - -# path to the directory containing our data -dir_path = downloaded_data.split(".")[0] + os.path.sep - -# files to work with -topo_filename = dir_path + "gravity_topo.txt" -data_filename = dir_path + "gravity_data.obs" - - -############################################# -# Load Data and Plot -# ------------------ -# -# Here we load and plot synthetic gravity anomaly data. Topography is generally -# defined as an (N, 3) array. Gravity data is generally defined with 4 columns: -# x, y, z and data. -# - -# Load topography -xyz_topo = np.loadtxt(str(topo_filename)) - -# Load field data -dobs = np.loadtxt(str(data_filename)) - -# Define receiver locations and observed data -receiver_locations = dobs[:, 0:3] -dobs = dobs[:, -1] - -# Plot -mpl.rcParams.update({"font.size": 12}) -fig = plt.figure(figsize=(7, 5)) - -ax1 = fig.add_axes([0.1, 0.1, 0.73, 0.85]) -plot2Ddata( - receiver_locations, - dobs, - ax=ax1, - contourOpts={"cmap": "bwr"}, - shade=True, - nx=20, - ny=20, - dataloc=True, -) -ax1.set_title("Gravity Anomaly") -ax1.set_xlabel("x (m)") -ax1.set_ylabel("y (m)") - -ax2 = fig.add_axes([0.8, 0.1, 0.03, 0.85]) -norm = mpl.colors.Normalize(vmin=-np.max(np.abs(dobs)), vmax=np.max(np.abs(dobs))) -cbar = mpl.colorbar.ColorbarBase( - ax2, norm=norm, orientation="vertical", cmap=mpl.cm.bwr, format="%.1e" -) -cbar.set_label("$mGal$", rotation=270, labelpad=15, size=12) - -plt.show() - -############################################# -# Assign Uncertainties -# -------------------- -# -# Inversion with simpeg requires that we define the standard deviation of our data. -# This represents our estimate of the noise in our data. For a gravity inversion, -# a constant floor value is generally applied to all data. For this tutorial, -# the standard deviation on each datum will be 1% of the maximum observed -# gravity anomaly value. -# - -maximum_anomaly = np.max(np.abs(dobs)) - -uncertainties = 0.01 * maximum_anomaly * np.ones(np.shape(dobs)) - -############################################# -# Defining the Survey -# ------------------- -# -# Here, we define the survey that will be used for this tutorial. Gravity -# surveys are simple to create. The user only needs an (N, 3) array to define -# the xyz locations of the observation locations. From this, the user can -# define the receivers and the source field. -# - -# Define the receivers. The data consists of vertical gravity anomaly measurements. -# The set of receivers must be defined as a list. -receiver_list = gravity.receivers.Point(receiver_locations, components="gz") - -receiver_list = [receiver_list] - -# Define the source field -source_field = gravity.sources.SourceField(receiver_list=receiver_list) - -# Define the survey -survey = gravity.survey.Survey(source_field) - -############################################# -# Defining the Data -# ----------------- -# -# Here is where we define the data that is inverted. The data is defined by -# the survey, the observation values and the standard deviation. -# - -data_object = data.Data(survey, dobs=dobs, standard_deviation=uncertainties) - - -############################################# -# Defining a Tensor Mesh -# ---------------------- -# -# Here, we create the tensor mesh that will be used to invert gravity anomaly -# data. If desired, we could define an OcTree mesh. -# - -dh = 5.0 -hx = [(dh, 5, -1.3), (dh, 40), (dh, 5, 1.3)] -hy = [(dh, 5, -1.3), (dh, 40), (dh, 5, 1.3)] -hz = [(dh, 5, -1.3), (dh, 15)] -mesh = TensorMesh([hx, hy, hz], "CCN") - -######################################################## -# Starting/Reference Model and Mapping on Tensor Mesh -# --------------------------------------------------- -# -# Here, we create starting and/or reference models for the inversion as -# well as the mapping from the model space to the active cells. Starting and -# reference models can be a constant background value or contain a-priori -# structures. -# - -# Find the indices of the active cells in forward model (ones below surface) -ind_active = active_from_xyz(mesh, xyz_topo) - -# Define mapping from model to active cells -nC = int(ind_active.sum()) -model_map = maps.IdentityMap(nP=nC) # model consists of a value for each active cell - -# Define and plot starting model -starting_model = np.zeros(nC) - - -############################################## -# Define the Physics and data misfit -# ---------------------------------- -# -# Here, we define the physics of the gravity problem by using the simulation -# class. -# - -simulation = gravity.simulation.Simulation3DIntegral( - survey=survey, mesh=mesh, rhoMap=model_map, active_cells=ind_active -) - -# Define the data misfit. Here the data misfit is the L2 norm of the weighted -# residual between the observed data and the data predicted for a given model. -# Within the data misfit, the residual between predicted and observed data are -# normalized by the data's standard deviation. -dmis = data_misfit.L2DataMisfit(data=data_object, simulation=simulation) - - -####################################################################### -# Running the Depth Weighted inversion -# ------------------------------------ -# -# Here we define the directives, weights, regularization, and optimization -# for a depth-weighted inversion -# - -# inversion directives -# Defining a starting value for the trade-off parameter (beta) between the data -# misfit and the regularization. -starting_beta = directives.BetaEstimate_ByEig(beta0_ratio=1e0) - -# Defines the directives for the IRLS regularization. This includes setting -# the cooling schedule for the trade-off parameter. -update_IRLS = directives.UpdateIRLS( - f_min_change=1e-4, - max_irls_iterations=30, -) - -# Options for outputting recovered models and predicted data for each beta. -save_iteration = directives.SaveOutputEveryIteration(save_txt=False) - -# Updating the preconditionner if it is model dependent. -update_jacobi = directives.UpdatePreconditioner() - -# The directives are defined as a list -directives_list = [ - update_IRLS, - starting_beta, - save_iteration, - update_jacobi, -] + Checkout the `Compare weighting strategy with Inversion of surface Gravity Anomaly + Data `_ tutorial. -# Define the regularization (model objective function) with depth weighting. -reg_dpth = regularization.Sparse(mesh, active_cells=ind_active, mapping=model_map) -reg_dpth.norms = [0, 2, 2, 2] -depth_weights = utils.depth_weighting( - mesh, receiver_locations, active_cells=ind_active, exponent=2 -) -reg_dpth.set_weights(depth_weights=depth_weights) - -# Define how the optimization problem is solved. Here we will use a projected -# Gauss-Newton approach that employs the conjugate gradient solver. -opt = optimization.ProjectedGNCG( - maxIter=100, lower=-1.0, upper=1.0, maxIterLS=20, maxIterCG=10, tolCG=1e-3 -) - -# Here we define the inverse problem that is to be solved -inv_prob = inverse_problem.BaseInvProblem(dmis, reg_dpth, opt) - -# Here we combine the inverse problem and the set of directives -inv = inversion.BaseInversion(inv_prob, directives_list) - -# Run inversion -recovered_model_dpth = inv.run(starting_model) - -####################################################################### -# Running the Distance Weighted inversion -# --------------------------------------- -# -# Here we define the directives, weights, regularization, and optimization -# for a distance-weighted inversion -# - -# inversion directives -# Defining a starting value for the trade-off parameter (beta) between the data -# misfit and the regularization. -starting_beta = directives.BetaEstimate_ByEig(beta0_ratio=1e0) - -# Defines the directives for the IRLS regularization. This includes setting -# the cooling schedule for the trade-off parameter. -update_IRLS = directives.UpdateIRLS( - f_min_change=1e-4, - max_irls_iterations=30, -) - -# Options for outputting recovered models and predicted data for each beta. -save_iteration = directives.SaveOutputEveryIteration(save_txt=False) - -# Updating the preconditionner if it is model dependent. -update_jacobi = directives.UpdatePreconditioner() - -# The directives are defined as a list -directives_list = [ - update_IRLS, - starting_beta, - save_iteration, - update_jacobi, -] - -# Define the regularization (model objective function) with distance weighting. -reg_dist = regularization.Sparse(mesh, active_cells=ind_active, mapping=model_map) -reg_dist.norms = [0, 2, 2, 2] -distance_weights = utils.distance_weighting( - mesh, receiver_locations, active_cells=ind_active, exponent=2 -) -reg_dist.set_weights(distance_weights=distance_weights) - -# Define how the optimization problem is solved. Here we will use a projected -# Gauss-Newton approach that employs the conjugate gradient solver. -opt = optimization.ProjectedGNCG( - maxIter=100, lower=-1.0, upper=1.0, maxIterLS=20, maxIterCG=10, tolCG=1e-3 -) - -# Here we define the inverse problem that is to be solved -inv_prob = inverse_problem.BaseInvProblem(dmis, reg_dist, opt) - -# Here we combine the inverse problem and the set of directives -inv = inversion.BaseInversion(inv_prob, directives_list) - -# Run inversion -recovered_model_dist = inv.run(starting_model) - -####################################################################### -# Running the Distance Weighted inversion -# --------------------------------------- -# -# Here we define the directives, weights, regularization, and optimization -# for a sensitivity weighted inversion -# - -# inversion directives -# Defining a starting value for the trade-off parameter (beta) between the data -# misfit and the regularization. -starting_beta = directives.BetaEstimate_ByEig(beta0_ratio=1e0) - -# Defines the directives for the IRLS regularization. This includes setting -# the cooling schedule for the trade-off parameter. -update_IRLS = directives.UpdateIRLS( - f_min_change=1e-4, - max_irls_iterations=30, -) - -# Options for outputting recovered models and predicted data for each beta. -save_iteration = directives.SaveOutputEveryIteration(save_txt=False) - -# Updating the preconditionner if it is model dependent. -update_jacobi = directives.UpdatePreconditioner() - -# Add sensitivity weights -sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) - -# The directives are defined as a list -directives_list = [ - update_IRLS, - sensitivity_weights, - starting_beta, - save_iteration, - update_jacobi, -] - -# Define the regularization (model objective function) for sensitivity weighting. -reg_sensw = regularization.Sparse(mesh, active_cells=ind_active, mapping=model_map) -reg_sensw.norms = [0, 2, 2, 2] - -# Define how the optimization problem is solved. Here we will use a projected -# Gauss-Newton approach that employs the conjugate gradient solver. -opt = optimization.ProjectedGNCG( - maxIter=100, lower=-1.0, upper=1.0, maxIterLS=20, maxIterCG=10, tolCG=1e-3 -) - -# Here we define the inverse problem that is to be solved -inv_prob = inverse_problem.BaseInvProblem(dmis, reg_sensw, opt) - -# Here we combine the inverse problem and the set of directives -inv = inversion.BaseInversion(inv_prob, directives_list) - -# Run inversion -recovered_model_sensw = inv.run(starting_model) - -############################################################ -# Recreate True Model -# ------------------- -# - -# Define density contrast values for each unit in g/cc -background_density = 0.0 -block_density = -0.2 -sphere_density = 0.2 - -# Define model. Models in simpeg are vector arrays. -true_model = background_density * np.ones(nC) - -# You could find the indicies of specific cells within the model and change their -# value to add structures. -ind_block = ( - (mesh.gridCC[ind_active, 0] > -50.0) - & (mesh.gridCC[ind_active, 0] < -20.0) - & (mesh.gridCC[ind_active, 1] > -15.0) - & (mesh.gridCC[ind_active, 1] < 15.0) - & (mesh.gridCC[ind_active, 2] > -50.0) - & (mesh.gridCC[ind_active, 2] < -30.0) -) -true_model[ind_block] = block_density - -# You can also use simpeg utilities to add structures to the model more concisely -ind_sphere = model_builder.get_indices_sphere( - np.r_[35.0, 0.0, -40.0], 15.0, mesh.gridCC -) -ind_sphere = ind_sphere[ind_active] -true_model[ind_sphere] = sphere_density - - -############################################################ -# Plotting True Model and Recovered Models -# ---------------------------------------- -# - -# Plot Models -fig, ax = plt.subplots(2, 2, figsize=(20, 10), sharex=True, sharey=True) -ax = ax.flatten() -plotting_map = maps.InjectActiveCells(mesh, ind_active, np.nan) -cmap = "coolwarm" -slice_y_loc = 0.0 - -mm = mesh.plot_slice( - plotting_map * true_model, - normal="Y", - ax=ax[0], - grid=False, - slice_loc=slice_y_loc, - pcolor_opts={"cmap": cmap, "norm": norm}, -) -ax[0].set_title(f"True model slice at y = {slice_y_loc} m") -plt.colorbar(mm[0], label="$g/cm^3$", ax=ax[0]) - -# plot depth weighting result -vmax = np.abs(recovered_model_dpth).max() -norm = mpl.colors.TwoSlopeNorm(vcenter=0, vmin=-vmax, vmax=vmax) -mm = mesh.plot_slice( - plotting_map * recovered_model_dpth, - normal="Y", - ax=ax[1], - grid=False, - slice_loc=slice_y_loc, - pcolor_opts={"cmap": cmap, "norm": norm}, -) -ax[1].set_title(f"Depth weighting Model slice at y = {slice_y_loc} m") -plt.colorbar(mm[0], label="$g/cm^3$", ax=ax[1]) - -# plot distance weighting result -vmax = np.abs(recovered_model_dist).max() -norm = mpl.colors.TwoSlopeNorm(vcenter=0, vmin=-vmax, vmax=vmax) -mm = mesh.plot_slice( - plotting_map * recovered_model_dist, - normal="Y", - ax=ax[2], - grid=False, - slice_loc=slice_y_loc, - pcolor_opts={"cmap": cmap, "norm": norm}, -) -ax[2].set_title(f"Distance weighting Model slice at y = {slice_y_loc} m") -plt.colorbar(mm[0], label="$g/cm^3$", ax=ax[2]) - -# plot sensitivity weighting result -vmax = np.abs(recovered_model_sensw).max() -norm = mpl.colors.TwoSlopeNorm(vcenter=0, vmin=-vmax, vmax=vmax) -mm = mesh.plot_slice( - plotting_map * recovered_model_sensw, - normal="Y", - ax=ax[3], - grid=False, - slice_loc=slice_y_loc, - pcolor_opts={"cmap": cmap, "norm": norm}, -) -ax[3].set_title(f"Sensitivity weighting Model slice at y = {slice_y_loc} m") -plt.colorbar(mm[0], label="$g/cm^3$", ax=ax[3]) - -# shared plotting -plotting_map = maps.InjectActiveCells(mesh, ind_active, 0.0) -slice_y_ind = ( - mesh.cell_centers[:, 1] == np.abs(mesh.cell_centers[:, 1] - slice_y_loc).min() -) -for axx in ax: - utils.plot2Ddata( - mesh.cell_centers[slice_y_ind][:, [0, 2]], - (plotting_map * true_model)[slice_y_ind], - contourOpts={"alpha": 0}, - level=True, - ncontour=2, - levelOpts={"colors": "grey", "linewidths": 2, "linestyles": "--"}, - method="nearest", - ax=axx, - ) - axx.set_aspect(1) - -plt.tight_layout() - -############################################################ -# Visualize weights -# ----------------- -# -# Plot Weights -fig, ax = plt.subplots(1, 3, figsize=(20, 4), sharex=True, sharey=True) -plotting_map = maps.InjectActiveCells(mesh, ind_active, np.nan) -cmap = "magma" -slice_y_loc = 0.0 - -# plot depth weights -mm = mesh.plot_slice( - plotting_map * np.log10(depth_weights), - normal="Y", - ax=ax[0], - grid=False, - slice_loc=slice_y_loc, - pcolor_opts={"cmap": cmap}, -) -ax[0].set_title(f"log10(depth weights) slice at y = {slice_y_loc} m") -plt.colorbar(mm[0], label="log10(depth weights)", ax=ax[0]) - -# plot distance weights -mm = mesh.plot_slice( - plotting_map * np.log10(distance_weights), - normal="Y", - ax=ax[1], - grid=False, - slice_loc=slice_y_loc, - pcolor_opts={"cmap": cmap}, -) -ax[1].set_title(f"log10(distance weights) slice at y = {slice_y_loc} m") -plt.colorbar(mm[0], label="log10(distance weights)", ax=ax[1]) - -# plot sensitivity weights -mm = mesh.plot_slice( - plotting_map * np.log10(reg_sensw.objfcts[0].get_weights(key="sensitivity")), - normal="Y", - ax=ax[2], - grid=False, - slice_loc=slice_y_loc, - pcolor_opts={"cmap": cmap}, -) -ax[2].set_title(f"log10(sensitivity weights) slice at y = {slice_y_loc} m") -plt.colorbar(mm[0], label="log10(sensitivity weights)", ax=ax[2]) - -# shared plotting -for axx in ax: - axx.set_aspect(1) - -plt.tight_layout() +""" diff --git a/tutorials/04-magnetics/plot_2a_magnetics_induced.py b/tutorials/04-magnetics/plot_2a_magnetics_induced.py index 9f3ece6ae3..ead80fc2a9 100644 --- a/tutorials/04-magnetics/plot_2a_magnetics_induced.py +++ b/tutorials/04-magnetics/plot_2a_magnetics_induced.py @@ -2,248 +2,12 @@ Forward Simulation of Total Magnetic Intensity Data =================================================== -Here we use the module *simpeg.potential_fields.magnetics* to predict magnetic -data for a magnetic susceptibility model. We simulate the data on a tensor mesh. -For this tutorial, we focus on the following: +.. important:: - - How to define the survey - - How to predict magnetic data for a susceptibility model - - How to include surface topography - - The units of the physical property model and resulting data + This tutorial has been moved to `User Tutorials + `_. + Checkout the `3D Forward Simulation of TMI Data + `_ tutorial. """ - -######################################################################### -# Import Modules -# -------------- -# - -import numpy as np -from scipy.interpolate import LinearNDInterpolator -import matplotlib as mpl -import matplotlib.pyplot as plt -import os - -from discretize import TensorMesh -from discretize.utils import mkvc, active_from_xyz -from simpeg.utils import plot2Ddata, model_builder -from simpeg import maps -from simpeg.potential_fields import magnetics - -write_output = False - -# sphinx_gallery_thumbnail_number = 2 - - -############################################# -# Topography -# ---------- -# -# Surface topography is defined as an (N, 3) numpy array. We create it here but -# topography could also be loaded from a file. -# - -[x_topo, y_topo] = np.meshgrid(np.linspace(-200, 200, 41), np.linspace(-200, 200, 41)) -z_topo = -15 * np.exp(-(x_topo**2 + y_topo**2) / 80**2) -x_topo, y_topo, z_topo = mkvc(x_topo), mkvc(y_topo), mkvc(z_topo) -xyz_topo = np.c_[x_topo, y_topo, z_topo] - -############################################# -# Defining the Survey -# ------------------- -# -# Here, we define survey that will be used for the simulation. Magnetic -# surveys are simple to create. The user only needs an (N, 3) array to define -# the xyz locations of the observation locations, the list of field components -# which are to be modeled and the properties of the Earth's field. -# - -# Define the observation locations as an (N, 3) numpy array or load them. -x = np.linspace(-80.0, 80.0, 17) -y = np.linspace(-80.0, 80.0, 17) -x, y = np.meshgrid(x, y) -x, y = mkvc(x.T), mkvc(y.T) -fun_interp = LinearNDInterpolator(np.c_[x_topo, y_topo], z_topo) -z = fun_interp(np.c_[x, y]) + 10 # Flight height 10 m above surface. -receiver_locations = np.c_[x, y, z] - -# Define the component(s) of the field we want to simulate as a list of strings. -# Here we simulation total magnetic intensity data. -components = ["tmi"] - -# Use the observation locations and components to define the receivers. To -# simulate data, the receivers must be defined as a list. -receiver_list = magnetics.receivers.Point(receiver_locations, components=components) - -receiver_list = [receiver_list] - -# Define the inducing field H0 = (intensity [nT], inclination [deg], declination [deg]) -inclination = 90 -declination = 0 -strength = 50000 - -source_field = magnetics.sources.UniformBackgroundField( - receiver_list=receiver_list, - amplitude=strength, - inclination=inclination, - declination=declination, -) - -# Define the survey -survey = magnetics.survey.Survey(source_field) - - -############################################# -# Defining a Tensor Mesh -# ---------------------- -# -# Here, we create the tensor mesh that will be used for the forward simulation. -# - -dh = 5.0 -hx = [(dh, 5, -1.3), (dh, 40), (dh, 5, 1.3)] -hy = [(dh, 5, -1.3), (dh, 40), (dh, 5, 1.3)] -hz = [(dh, 5, -1.3), (dh, 15)] -mesh = TensorMesh([hx, hy, hz], "CCN") - - -############################################# -# Defining a Susceptibility Model -# ------------------------------- -# -# Here, we create the model that will be used to predict magnetic data -# and the mapping from the model to the mesh. The model -# consists of a susceptible sphere in a less susceptible host. -# - -# Define susceptibility values for each unit in SI -background_susceptibility = 0.0001 -sphere_susceptibility = 0.01 - -# Find cells that are active in the forward modeling (cells below surface) -ind_active = active_from_xyz(mesh, xyz_topo) - -# Define mapping from model to active cells -nC = int(ind_active.sum()) -model_map = maps.IdentityMap(nP=nC) # model is a vlue for each active cell - -# Define model. Models in SimPEG are vector arrays -model = background_susceptibility * np.ones(ind_active.sum()) -ind_sphere = model_builder.get_indices_sphere( - np.r_[0.0, 0.0, -45.0], 15.0, mesh.cell_centers -) -ind_sphere = ind_sphere[ind_active] -model[ind_sphere] = sphere_susceptibility - -# Plot Model -fig = plt.figure(figsize=(9, 4)) - -plotting_map = maps.InjectActiveCells(mesh, ind_active, np.nan) -ax1 = fig.add_axes([0.1, 0.12, 0.73, 0.78]) -mesh.plot_slice( - plotting_map * model, - normal="Y", - ax=ax1, - ind=int(mesh.shape_cells[1] / 2), - grid=True, - clim=(np.min(model), np.max(model)), -) -ax1.set_title("Model slice at y = 0 m") -ax1.set_xlabel("x (m)") -ax1.set_ylabel("z (m)") - -ax2 = fig.add_axes([0.85, 0.12, 0.05, 0.78]) -norm = mpl.colors.Normalize(vmin=np.min(model), vmax=np.max(model)) -cbar = mpl.colorbar.ColorbarBase(ax2, norm=norm, orientation="vertical") -cbar.set_label("Magnetic Susceptibility (SI)", rotation=270, labelpad=15, size=12) - -plt.show() - - -################################################################### -# Simulation: TMI Data for a Susceptibility Model -# ----------------------------------------------- -# -# Here we demonstrate how to predict magnetic data for a magnetic -# susceptibility model using the integral formulation. -# - -############################################################################### -# Define the forward simulation. By setting the 'store_sensitivities' keyword -# argument to "forward_only", we simulate the data without storing the sensitivities - -simulation = magnetics.simulation.Simulation3DIntegral( - survey=survey, - mesh=mesh, - model_type="scalar", - chiMap=model_map, - active_cells=ind_active, - store_sensitivities="forward_only", - engine="choclo", -) - -############################################################################### -# .. tip:: -# -# Since SimPEG v0.22.0 we can use `Choclo -# `_ as the engine for running the magnetic -# simulations, which results in faster and more memory efficient runs. Just -# pass ``engine="choclo"`` when constructing the simulation. -# - -############################################################################### -# Compute predicted data for a susceptibility model - -dpred = simulation.dpred(model) - -# Plot -fig = plt.figure(figsize=(6, 5)) -v_max = np.max(np.abs(dpred)) - -ax1 = fig.add_axes([0.1, 0.1, 0.8, 0.85]) -plot2Ddata( - receiver_list[0].locations, - dpred, - ax=ax1, - ncontour=30, - clim=(-v_max, v_max), - contourOpts={"cmap": "bwr"}, -) -ax1.set_title("TMI Anomaly") -ax1.set_xlabel("x (m)") -ax1.set_ylabel("y (m)") - -ax2 = fig.add_axes([0.87, 0.1, 0.03, 0.85]) -norm = mpl.colors.Normalize(vmin=-np.max(np.abs(dpred)), vmax=np.max(np.abs(dpred))) -cbar = mpl.colorbar.ColorbarBase( - ax2, norm=norm, orientation="vertical", cmap=mpl.cm.bwr -) -cbar.set_label("$nT$", rotation=270, labelpad=15, size=12) - -plt.show() - - -####################################################### -# Optional: Export Data -# --------------------- -# -# Write the data and topography -# - -if write_output: - dir_path = os.path.dirname(__file__).split(os.path.sep) - dir_path.extend(["outputs"]) - dir_path = os.path.sep.join(dir_path) + os.path.sep - - if not os.path.exists(dir_path): - os.mkdir(dir_path) - - fname = dir_path + "magnetics_topo.txt" - np.savetxt(fname, np.c_[xyz_topo], fmt="%.4e") - - np.random.seed(211) - maximum_anomaly = np.max(np.abs(dpred)) - noise = 0.02 * maximum_anomaly * np.random.randn(len(dpred)) - fname = dir_path + "magnetics_data.obs" - np.savetxt(fname, np.c_[receiver_locations, dpred + noise], fmt="%.4e") diff --git a/tutorials/04-magnetics/plot_2b_magnetics_mvi.py b/tutorials/04-magnetics/plot_2b_magnetics_mvi.py index 51c5610f5b..95cbad69b4 100644 --- a/tutorials/04-magnetics/plot_2b_magnetics_mvi.py +++ b/tutorials/04-magnetics/plot_2b_magnetics_mvi.py @@ -2,299 +2,12 @@ Forward Simulation of Gradiometry Data for Magnetic Vector Models ================================================================= -Here we use the module *simpeg.potential_fields.magnetics* to predict magnetic -gradiometry data for magnetic vector models. The simulation is performed on a -Tree mesh. For this tutorial, we focus on the following: +.. important:: - - How to define the survey when we want to measured multiple field components - - How to predict magnetic data in the case of remanence - - How to include surface topography - - How to construct tree meshes based on topography and survey geometry - - The units of the physical property model and resulting data + This tutorial has been moved to `User Tutorials + `_. + Checkout the `3D Forward Simulation of Magnetic Gradiometry Data for Magnetic Vector + Models `_ tutorial. """ - -######################################################################### -# Import Modules -# -------------- -# - -import numpy as np -from scipy.interpolate import LinearNDInterpolator -import matplotlib as mpl -import matplotlib.pyplot as plt - -from discretize import TreeMesh -from discretize.utils import mkvc, refine_tree_xyz, active_from_xyz -from simpeg.utils import plot2Ddata, model_builder, mat_utils -from simpeg import maps -from simpeg.potential_fields import magnetics - -# sphinx_gallery_thumbnail_number = 2 - - -############################################# -# Topography -# ---------- -# -# Here we define surface topography as an (N, 3) numpy array. Topography could -# also be loaded from a file. -# - -[x_topo, y_topo] = np.meshgrid(np.linspace(-200, 200, 41), np.linspace(-200, 200, 41)) -z_topo = -15 * np.exp(-(x_topo**2 + y_topo**2) / 80**2) -x_topo, y_topo, z_topo = mkvc(x_topo), mkvc(y_topo), mkvc(z_topo) -xyz_topo = np.c_[x_topo, y_topo, z_topo] - -############################################# -# Defining the Survey -# ------------------- -# -# Here, we define survey that will be used for the simulation. Magnetic -# surveys are simple to create. The user only needs an (N, 3) array to define -# the xyz locations of the observation locations, the list of field components -# which are to be modeled and the properties of the Earth's field. -# - -# Define the observation locations as an (N, 3) numpy array or load them. -x = np.linspace(-80.0, 80.0, 17) -y = np.linspace(-80.0, 80.0, 17) -x, y = np.meshgrid(x, y) -x, y = mkvc(x.T), mkvc(y.T) -fun_interp = LinearNDInterpolator(np.c_[x_topo, y_topo], z_topo) -z = fun_interp(np.c_[x, y]) + 10 # Flight height 10 m above surface. -receiver_locations = np.c_[x, y, z] - -# Define the component(s) of the field we want to simulate as strings within -# a list. Here we measure the x, y and z derivatives of the Bz anomaly at -# each observation location. -components = ["bxz", "byz", "bzz"] - -# Use the observation locations and components to define the receivers. To -# simulate data, the receivers must be defined as a list. -receiver_list = magnetics.receivers.Point(receiver_locations, components=components) - -receiver_list = [receiver_list] - -# Define the inducing field H0 = (intensity [nT], inclination [deg], declination [deg]) -field_inclination = 60 -field_declination = 30 -field_strength = 50000 - -source_field = magnetics.sources.UniformBackgroundField( - receiver_list=receiver_list, - amplitude=field_strength, - inclination=field_inclination, - declination=field_declination, -) - -# Define the survey -survey = magnetics.survey.Survey(source_field) - - -########################################################## -# Defining an OcTree Mesh -# ----------------------- -# -# Here, we create the OcTree mesh that will be used to predict magnetic -# gradiometry data for the forward simuulation. -# - -dx = 5 # minimum cell width (base mesh cell width) in x -dy = 5 # minimum cell width (base mesh cell width) in y -dz = 5 # minimum cell width (base mesh cell width) in z - -x_length = 240.0 # domain width in x -y_length = 240.0 # domain width in y -z_length = 120.0 # domain width in y - -# Compute number of base mesh cells required in x and y -nbcx = 2 ** int(np.round(np.log(x_length / dx) / np.log(2.0))) -nbcy = 2 ** int(np.round(np.log(y_length / dy) / np.log(2.0))) -nbcz = 2 ** int(np.round(np.log(z_length / dz) / np.log(2.0))) - -# Define the base mesh -hx = [(dx, nbcx)] -hy = [(dy, nbcy)] -hz = [(dz, nbcz)] -mesh = TreeMesh([hx, hy, hz], x0="CCN") - -# Refine based on surface topography -mesh = refine_tree_xyz( - mesh, xyz_topo, octree_levels=[2, 2], method="surface", finalize=False -) - -# Refine box base on region of interest -xp, yp, zp = np.meshgrid([-100.0, 100.0], [-100.0, 100.0], [-80.0, 0.0]) -xyz = np.c_[mkvc(xp), mkvc(yp), mkvc(zp)] - -mesh = refine_tree_xyz(mesh, xyz, octree_levels=[2, 2], method="box", finalize=False) - -mesh.finalize() - -########################################################## -# Create Magnetic Vector Intensity Model (MVI) -# -------------------------------------------- -# -# Magnetic vector models are defined by three-component effective -# susceptibilities. To create a magnetic vector -# model, we must -# -# 1) Define the magnetic susceptibility for each cell. Then multiply by the -# unit vector direction of the inducing field. (induced contribution) -# 2) Define the remanent magnetization vector for each cell and normalize -# by the magnitude of the Earth's field (remanent contribution) -# 3) Sum the induced and remanent contributions -# 4) Define as a vector np.r_[chi_1, chi_2, chi_3] -# -# - -# Define susceptibility values for each unit in SI -background_susceptibility = 0.0001 -sphere_susceptibility = 0.01 - -# Find cells active in the forward modeling (cells below surface) -ind_active = active_from_xyz(mesh, xyz_topo) - -# Define mapping from model to active cells -nC = int(ind_active.sum()) -model_map = maps.IdentityMap(nP=3 * nC) # model has 3 parameters for each cell - -# Define susceptibility for each cell -susceptibility_model = background_susceptibility * np.ones(ind_active.sum()) -ind_sphere = model_builder.get_indices_sphere(np.r_[0.0, 0.0, -45.0], 15.0, mesh.gridCC) -ind_sphere = ind_sphere[ind_active] -susceptibility_model[ind_sphere] = sphere_susceptibility - -# Compute the unit direction of the inducing field in Cartesian coordinates -field_direction = mat_utils.dip_azimuth2cartesian(field_inclination, field_declination) - -# Multiply susceptibility model to obtain the x, y, z components of the -# effective susceptibility contribution from induced magnetization. -susceptibility_model = np.outer(susceptibility_model, field_direction) - -# Define the effective susceptibility contribution for remanent magnetization to have a -# magnitude of 0.006 SI, with inclination -45 and declination 90 -remanence_inclination = -45.0 -remanence_declination = 90.0 -remanence_susceptibility = 0.01 - -remanence_model = np.zeros(np.shape(susceptibility_model)) -effective_susceptibility_sphere = ( - remanence_susceptibility - * mat_utils.dip_azimuth2cartesian(remanence_inclination, remanence_declination) -) -remanence_model[ind_sphere, :] = effective_susceptibility_sphere - -# Define effective susceptibility model as a vector np.r_[chi_x, chi_y, chi_z] -plotting_model = susceptibility_model + remanence_model -model = mkvc(plotting_model) - -# Plot Effective Susceptibility Model -fig = plt.figure(figsize=(9, 4)) - -plotting_map = maps.InjectActiveCells(mesh, ind_active, np.nan) -plotting_model = np.sqrt(np.sum(plotting_model, axis=1) ** 2) -ax1 = fig.add_axes([0.1, 0.12, 0.73, 0.78]) -mesh.plot_slice( - plotting_map * plotting_model, - normal="Y", - ax=ax1, - ind=int(mesh.h[1].size / 2), - grid=True, - clim=(np.min(plotting_model), np.max(plotting_model)), -) -ax1.set_title("MVI Model at y = 0 m") -ax1.set_xlabel("x (m)") -ax1.set_ylabel("z (m)") - -ax2 = fig.add_axes([0.85, 0.12, 0.05, 0.78]) -norm = mpl.colors.Normalize(vmin=np.min(plotting_model), vmax=np.max(plotting_model)) -cbar = mpl.colorbar.ColorbarBase(ax2, norm=norm, orientation="vertical") -cbar.set_label( - "Effective Susceptibility Amplitude (SI)", rotation=270, labelpad=15, size=12 -) - - -################################################################### -# Simulation: Gradiometry Data for an MVI Model -# --------------------------------------------- -# -# Here we predict magnetic gradiometry data for an effective susceptibility model -# in the case of remanent magnetization. -# - -############################################################################### -# Define the forward simulation. By setting the 'store_sensitivities' keyword -# argument to "forward_only", we simulate the data without storing the sensitivities - -simulation = magnetics.simulation.Simulation3DIntegral( - survey=survey, - mesh=mesh, - chiMap=model_map, - active_cells=ind_active, - model_type="vector", - store_sensitivities="forward_only", -) - - -############################################################################### -# Compute predicted data for some model - -dpred = simulation.dpred(model) -n_data = len(dpred) - -# Plot -fig = plt.figure(figsize=(13, 4)) -v_max = np.max(np.abs(dpred)) - -ax1 = fig.add_axes([0.1, 0.15, 0.25, 0.78]) -plot2Ddata( - receiver_list[0].locations, - dpred[0:n_data:3], - ax=ax1, - ncontour=30, - clim=(-v_max, v_max), - contourOpts={"cmap": "bwr"}, -) -ax1.set_title("$dBz/dx$") -ax1.set_xlabel("x (m)") -ax1.set_ylabel("y (m)") - -ax2 = fig.add_axes([0.36, 0.15, 0.25, 0.78]) -cplot2 = plot2Ddata( - receiver_list[0].locations, - dpred[1:n_data:3], - ax=ax2, - ncontour=30, - clim=(-v_max, v_max), - contourOpts={"cmap": "bwr"}, -) -cplot2[0].set_clim((-v_max, v_max)) -ax2.set_title("$dBz/dy$") -ax2.set_xlabel("x (m)") -ax2.set_yticks([]) - -ax3 = fig.add_axes([0.62, 0.15, 0.25, 0.78]) -cplot3 = plot2Ddata( - receiver_list[0].locations, - dpred[2:n_data:3], - ax=ax3, - ncontour=30, - clim=(-v_max, v_max), - contourOpts={"cmap": "bwr"}, -) -cplot3[0].set_clim((-v_max, v_max)) -ax3.set_title("$dBz/dz$") -ax3.set_xlabel("x (m)") -ax3.set_yticks([]) - -ax4 = fig.add_axes([0.88, 0.15, 0.02, 0.79]) -norm = mpl.colors.Normalize(vmin=-v_max, vmax=v_max) -cbar = mpl.colorbar.ColorbarBase( - ax4, norm=norm, orientation="vertical", cmap=mpl.cm.bwr -) -cbar.set_label("$nT/m$", rotation=270, labelpad=15, size=12) - -plt.show() diff --git a/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py b/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py index 61eeae9497..403f9c9598 100644 --- a/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py +++ b/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py @@ -2,466 +2,14 @@ Sparse Norm Inversion for Total Magnetic Intensity Data on a Tensor Mesh ======================================================================== -Here we invert total magnetic intensity (TMI) data to recover a magnetic -susceptibility model. We formulate the inverse problem as an iteratively -re-weighted least-squares (IRLS) optimization problem. For this tutorial, we -focus on the following: +.. important:: - - Defining the survey from xyz formatted data - - Generating a mesh based on survey geometry - - Including surface topography - - Defining the inverse problem (data misfit, regularization, optimization) - - Specifying directives for the inversion - - Setting sparse and blocky norms - - Plotting the recovered model and data misfit - -Although we consider TMI data in this tutorial, the same approach -can be used to invert other types of geophysical data. + This tutorial has been moved to `User Tutorials + `_. + Checkout the `Iteratively Re-weighted Least-Squares Inversion + `_ + section of the `3D Inversion of TMI Data to Recover a Susceptibility Model Models + `_ tutorial. """ - -######################################################################### -# Import modules -# -------------- -# - -import os -import numpy as np -import matplotlib as mpl -import matplotlib.pyplot as plt -import tarfile - -from discretize import TensorMesh -from discretize.utils import active_from_xyz -from simpeg.potential_fields import magnetics -from simpeg.utils import plot2Ddata, model_builder -from simpeg import ( - maps, - data, - inverse_problem, - data_misfit, - regularization, - optimization, - directives, - inversion, - utils, -) - -# sphinx_gallery_thumbnail_number = 3 - -############################################# -# Load Data and Plot -# ------------------ -# -# File paths for assets we are loading. To set up the inversion, we require -# topography and field observations. The true model defined on the whole mesh -# is loaded to compare with the inversion result. These files are stored as a -# tar-file on our google cloud bucket: -# "https://storage.googleapis.com/simpeg/doc-assets/magnetics.tar.gz" -# - -# storage bucket where we have the data -data_source = "https://storage.googleapis.com/simpeg/doc-assets/magnetics.tar.gz" - -# download the data -downloaded_data = utils.download(data_source, overwrite=True) - -# unzip the tarfile -tar = tarfile.open(downloaded_data, "r") -tar.extractall() -tar.close() - -# path to the directory containing our data -dir_path = downloaded_data.split(".")[0] + os.path.sep - -# files to work with -topo_filename = dir_path + "magnetics_topo.txt" -data_filename = dir_path + "magnetics_data.obs" - - -############################################# -# Load Data and Plot -# ------------------ -# -# Here we load and plot synthetic TMI data. Topography is generally -# defined as an (N, 3) array. TMI data is generally defined with 4 columns: -# x, y, z and data. -# - -topo_xyz = np.loadtxt(str(topo_filename)) -dobs = np.loadtxt(str(data_filename)) - -receiver_locations = dobs[:, 0:3] -dobs = dobs[:, -1] - -# Plot -fig = plt.figure(figsize=(6, 5)) -v_max = np.max(np.abs(dobs)) - -ax1 = fig.add_axes([0.1, 0.1, 0.75, 0.85]) -plot2Ddata( - receiver_locations, - dobs, - ax=ax1, - ncontour=30, - clim=(-v_max, v_max), - contourOpts={"cmap": "bwr"}, -) -ax1.set_title("TMI Anomaly") -ax1.set_xlabel("x (m)") -ax1.set_ylabel("y (m)") - -ax2 = fig.add_axes([0.85, 0.05, 0.05, 0.9]) -norm = mpl.colors.Normalize(vmin=-np.max(np.abs(dobs)), vmax=np.max(np.abs(dobs))) -cbar = mpl.colorbar.ColorbarBase( - ax2, norm=norm, orientation="vertical", cmap=mpl.cm.bwr -) -cbar.set_label("$nT$", rotation=270, labelpad=15, size=12) - -plt.show() - -############################################# -# Assign Uncertainty -# ------------------ -# -# Inversion with SimPEG requires that we define standard deviation on our data. -# This represents our estimate of the noise in our data. For magnetic inversions, -# a constant floor value is generall applied to all data. For this tutorial, the -# standard deviation on each datum will be 2% of the maximum observed magnetics -# anomaly value. -# - -maximum_anomaly = np.max(np.abs(dobs)) - -std = 0.02 * maximum_anomaly * np.ones(len(dobs)) - -############################################# -# Defining the Survey -# ------------------- -# -# Here, we define survey that will be used for the simulation. Magnetic -# surveys are simple to create. The user only needs an (N, 3) array to define -# the xyz locations of the observation locations, the list of field components -# which are to be modeled and the properties of the Earth's field. -# - -# Define the component(s) of the field we are inverting as a list. Here we will -# invert total magnetic intensity data. -components = ["tmi"] - -# Use the observation locations and components to define the receivers. To -# simulate data, the receivers must be defined as a list. -receiver_list = magnetics.receivers.Point(receiver_locations, components=components) - -receiver_list = [receiver_list] - -# Define the inducing field H0 = (intensity [nT], inclination [deg], declination [deg]) -inclination = 90 -declination = 0 -strength = 50000 - -source_field = magnetics.sources.UniformBackgroundField( - receiver_list=receiver_list, - amplitude=strength, - inclination=inclination, - declination=declination, -) - -# Define the survey -survey = magnetics.survey.Survey(source_field) - -############################################# -# Defining the Data -# ----------------- -# -# Here is where we define the data that is inverted. The data is defined by -# the survey, the observation values and the standard deviations. -# - -data_object = data.Data(survey, dobs=dobs, standard_deviation=std) - - -############################################# -# Defining a Tensor Mesh -# ---------------------- -# -# Here, we create the tensor mesh that will be used to invert TMI data. -# If desired, we could define an OcTree mesh. -# - -dh = 5.0 -hx = [(dh, 5, -1.3), (dh, 40), (dh, 5, 1.3)] -hy = [(dh, 5, -1.3), (dh, 40), (dh, 5, 1.3)] -hz = [(dh, 5, -1.3), (dh, 15)] -mesh = TensorMesh([hx, hy, hz], "CCN") - -######################################################## -# Starting/Reference Model and Mapping on Tensor Mesh -# --------------------------------------------------- -# -# Here, we would create starting and/or reference models for the inversion as -# well as the mapping from the model space to the active cells. Starting and -# reference models can be a constant background value or contain a-priori -# structures. Here, the background is 1e-4 SI. -# - -# Define background susceptibility model in SI. Don't make this 0! -# Otherwise the gradient for the 1st iteration is zero and the inversion will -# not converge. -background_susceptibility = 1e-4 - -# Find the indecies of the active cells in forward model (ones below surface) -active_cells = active_from_xyz(mesh, topo_xyz) - -# Define mapping from model to active cells -nC = int(active_cells.sum()) -model_map = maps.IdentityMap(nP=nC) # model consists of a value for each cell - -# Define starting model -starting_model = background_susceptibility * np.ones(nC) - -############################################## -# Define the Physics -# ------------------ -# -# Here, we define the physics of the magnetics problem by using the simulation -# class. -# - -############################################################################### -# Define the problem. Define the cells below topography and the mapping - -simulation = magnetics.simulation.Simulation3DIntegral( - survey=survey, - mesh=mesh, - model_type="scalar", - chiMap=model_map, - active_cells=active_cells, - engine="choclo", -) - -############################################################################### -# .. tip:: -# -# Since SimPEG v0.22.0 we can use `Choclo -# `_ as the engine for running the magnetic -# simulations, which results in faster and more memory efficient runs. Just -# pass ``engine="choclo"`` when constructing the simulation. -# - - -####################################################################### -# Define Inverse Problem -# ---------------------- -# -# The inverse problem is defined by 3 things: -# -# 1) Data Misfit: a measure of how well our recovered model explains the field data -# 2) Regularization: constraints placed on the recovered model and a priori information -# 3) Optimization: the numerical approach used to solve the inverse problem -# - -# Define the data misfit. Here the data misfit is the L2 norm of the weighted -# residual between the observed data and the data predicted for a given model. -# Within the data misfit, the residual between predicted and observed data are -# normalized by the data's standard deviation. -dmis = data_misfit.L2DataMisfit(data=data_object, simulation=simulation) - -# Define the regularization (model objective function) -reg = regularization.Sparse( - mesh, - active_cells=active_cells, - mapping=model_map, - reference_model=starting_model, - gradient_type="total", -) - -# Define sparse and blocky norms p, qx, qy, qz -reg.norms = [0, 0, 0, 0] - -# Define how the optimization problem is solved. Here we will use a projected -# Gauss-Newton approach that employs the conjugate gradient solver. -opt = optimization.ProjectedGNCG( - maxIter=20, lower=0.0, upper=1.0, maxIterLS=20, maxIterCG=10, tolCG=1e-3 -) - -# Here we define the inverse problem that is to be solved -inv_prob = inverse_problem.BaseInvProblem(dmis, reg, opt) - -####################################################################### -# Define Inversion Directives -# --------------------------- -# -# Here we define any directives that are carried out during the inversion. This -# includes the cooling schedule for the trade-off parameter (beta), stopping -# criteria for the inversion and saving inversion results at each iteration. -# - -# Defining a starting value for the trade-off parameter (beta) between the data -# misfit and the regularization. -starting_beta = directives.BetaEstimate_ByEig(beta0_ratio=5) - -# Options for outputting recovered models and predicted data for each beta. -save_iteration = directives.SaveOutputEveryIteration(save_txt=False) - -# Defines the directives for the IRLS regularization. This includes setting -# the cooling schedule for the trade-off parameter. -update_IRLS = directives.UpdateIRLS( - f_min_change=1e-4, - max_irls_iterations=30, - cooling_factor=1.5, - misfit_tolerance=1e-2, -) - -# Updating the preconditioner if it is model dependent. -update_jacobi = directives.UpdatePreconditioner() - -# Setting a stopping criteria for the inversion. -target_misfit = directives.TargetMisfit(chifact=1) - -# Add sensitivity weights -sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) - -# The directives are defined as a list. -directives_list = [ - sensitivity_weights, - starting_beta, - save_iteration, - update_IRLS, - update_jacobi, -] - -##################################################################### -# Running the Inversion -# --------------------- -# -# To define the inversion object, we need to define the inversion problem and -# the set of directives. We can then run the inversion. -# - -# Here we combine the inverse problem and the set of directives -inv = inversion.BaseInversion(inv_prob, directives_list) - -# Print target misfit to compare with convergence -# print("Target misfit is " + str(target_misfit.target)) - -# Run the inversion -recovered_model = inv.run(starting_model) - -############################################################## -# Recreate True Model -# ------------------- -# - - -background_susceptibility = 0.0001 -sphere_susceptibility = 0.01 - -true_model = background_susceptibility * np.ones(nC) -ind_sphere = model_builder.get_indices_sphere( - np.r_[0.0, 0.0, -45.0], 15.0, mesh.cell_centers -) -ind_sphere = ind_sphere[active_cells] -true_model[ind_sphere] = sphere_susceptibility - - -############################################################ -# Plotting True Model and Recovered Model -# --------------------------------------- -# - -# Plot True Model -fig = plt.figure(figsize=(9, 4)) -plotting_map = maps.InjectActiveCells(mesh, active_cells, np.nan) - -ax1 = fig.add_axes([0.08, 0.1, 0.75, 0.8]) -mesh.plot_slice( - plotting_map * true_model, - normal="Y", - ax=ax1, - ind=int(mesh.shape_cells[1] / 2), - grid=True, - clim=(np.min(true_model), np.max(true_model)), - pcolor_opts={"cmap": "viridis"}, -) -ax1.set_title("Model slice at y = 0 m") - -ax2 = fig.add_axes([0.85, 0.1, 0.05, 0.8]) -norm = mpl.colors.Normalize(vmin=np.min(true_model), vmax=np.max(true_model)) -cbar = mpl.colorbar.ColorbarBase( - ax2, norm=norm, orientation="vertical", cmap=mpl.cm.viridis, format="%.1e" -) -cbar.set_label("SI", rotation=270, labelpad=15, size=12) - -plt.show() - -# Plot Recovered Model -fig = plt.figure(figsize=(9, 4)) -plotting_map = maps.InjectActiveCells(mesh, active_cells, np.nan) - -ax1 = fig.add_axes([0.08, 0.1, 0.75, 0.8]) -mesh.plot_slice( - plotting_map * recovered_model, - normal="Y", - ax=ax1, - ind=int(mesh.shape_cells[1] / 2), - grid=True, - clim=(np.min(recovered_model), np.max(recovered_model)), - pcolor_opts={"cmap": "viridis"}, -) -ax1.set_title("Model slice at y = 0 m") - -ax2 = fig.add_axes([0.85, 0.1, 0.05, 0.8]) -norm = mpl.colors.Normalize(vmin=np.min(recovered_model), vmax=np.max(recovered_model)) -cbar = mpl.colorbar.ColorbarBase( - ax2, norm=norm, orientation="vertical", cmap=mpl.cm.viridis, format="%.1e" -) -cbar.set_label("SI", rotation=270, labelpad=15, size=12) - -plt.show() - -################################################################### -# Plotting Predicted Data and Misfit -# ---------------------------------- -# - -# Predicted data with final recovered model -dpred = inv_prob.dpred - -# Observed data | Predicted data | Normalized data misfit -data_array = np.c_[dobs, dpred, (dobs - dpred) / std] - -fig = plt.figure(figsize=(17, 4)) -plot_title = ["Observed", "Predicted", "Normalized Misfit"] -plot_units = ["nT", "nT", ""] - -ax1 = 3 * [None] -ax2 = 3 * [None] -norm = 3 * [None] -cbar = 3 * [None] -cplot = 3 * [None] -v_lim = [np.max(np.abs(dobs)), np.max(np.abs(dobs)), np.max(np.abs(data_array[:, 2]))] - -for ii in range(0, 3): - ax1[ii] = fig.add_axes([0.33 * ii + 0.03, 0.11, 0.25, 0.84]) - cplot[ii] = plot2Ddata( - receiver_list[0].locations, - data_array[:, ii], - ax=ax1[ii], - ncontour=30, - clim=(-v_lim[ii], v_lim[ii]), - contourOpts={"cmap": "bwr"}, - ) - ax1[ii].set_title(plot_title[ii]) - ax1[ii].set_xlabel("x (m)") - ax1[ii].set_ylabel("y (m)") - - ax2[ii] = fig.add_axes([0.33 * ii + 0.27, 0.11, 0.01, 0.84]) - norm[ii] = mpl.colors.Normalize(vmin=-v_lim[ii], vmax=v_lim[ii]) - cbar[ii] = mpl.colorbar.ColorbarBase( - ax2[ii], norm=norm[ii], orientation="vertical", cmap=mpl.cm.bwr - ) - cbar[ii].set_label(plot_units[ii], rotation=270, labelpad=15, size=12) - -plt.show() From 15bd44b1196c6136205cfefe89111cf662e38f99 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 2 Oct 2025 10:23:15 -0700 Subject: [PATCH 171/194] Minor fixes to docs of `UpdateSensitivityWeights` (#1705) Apply some minor improvements to the docs of `UpdateSensitivityWeights`: add admonition, fix usage of the `class` directive, update class names mentioned in the docs, improve list of options for different arguments. --- simpeg/directives/_directives.py | 63 ++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/simpeg/directives/_directives.py b/simpeg/directives/_directives.py index 3ff9b94bf4..5ff37a47e4 100644 --- a/simpeg/directives/_directives.py +++ b/simpeg/directives/_directives.py @@ -2317,22 +2317,24 @@ class UpdateSensitivityWeights(InversionDirective): r""" Sensitivity weighting for linear and non-linear least-squares inverse problems. - This directive computes the root-mean squared sensitivities for the - forward simulation(s) attached to the inverse problem, then truncates - and scales the result to create cell weights which are applied in the regularization. - The underlying theory is provided below in the `Notes` section. - - This directive **requires** that the map for the regularization function is either - class:`simpeg.maps.Wires` or class:`simpeg.maps.Identity`. In other words, the - sensitivity weighting cannot be applied for parametric inversion. In addition, - the simulation(s) connected to the inverse problem **must** have a ``getJ`` or - ``getJtJdiag`` method. - - This directive's place in the :class:`DirectivesList` **must** be - before any directives which update the preconditioner for the inverse problem - (i.e. :class:`UpdatePreconditioner`), and **must** be before any directives that - estimate the starting trade-off parameter (i.e. :class:`EstimateBeta_ByEig` - and :class:`EstimateBetaMaxDerivative`). + This directive computes the root-mean squared sensitivities for the forward + simulation(s) attached to the inverse problem, then truncates and scales the result + to create cell weights which are applied in the regularization. + + .. important:: + + This directive **requires** that the map for the regularization function is + either :class:`simpeg.maps.Wires` or :class:`simpeg.maps.IdentityMap`. In other + words, the sensitivity weighting cannot be applied for parametric inversion. In + addition, the simulation(s) connected to the inverse problem **must** have + a ``getJ`` or ``getJtJdiag`` method. + + .. important:: + + This directive **must** be placed before any directives which update the + preconditioner for the inverse problem (i.e. :class:`UpdatePreconditioner`), and + **must** be before any directives that estimate the starting trade-off parameter + (i.e. :class:`BetaEstimate_ByEig` and :class:`BetaEstimateMaxDerivative`). Parameters ---------- @@ -2344,24 +2346,29 @@ class UpdateSensitivityWeights(InversionDirective): threshold_method : {'amplitude', 'global', 'percentile'} Threshold method for how `threshold_value` is applied: - - amplitude: - the smallest root-mean squared sensitivity is a fractional percent of the largest value; must be between 0 and 1. - - global: - `threshold_value` is added to the cell weights prior to normalization; must be greater than 0. - - percentile: - the smallest root-mean squared sensitivity is set using percentile threshold; must be between 0 and 100. + - amplitude: + The smallest root-mean squared sensitivity is a fractional percent of the + largest value; must be between 0 and 1. + - global: + The ``threshold_value`` is added to the cell weights prior to normalization; + must be greater than 0. + - percentile: + The smallest root-mean squared sensitivity is set using percentile + threshold; must be between 0 and 100. normalization_method : {'maximum', 'min_value', None} Normalization method applied to sensitivity weights. Options are: - - maximum: - sensitivity weights are normalized by the largest value such that the largest weight is equal to 1. - - minimum: - sensitivity weights are normalized by the smallest value, after thresholding, such that the smallest weights are equal to 1. - - ``None``: - normalization is not applied. + - maximum: + Sensitivity weights are normalized by the largest value such that the + largest weight is equal to 1. + - minimum: + Sensitivity weights are normalized by the smallest value, after + thresholding, such that the smallest weights are equal to 1. + - ``None``: + Normalization is not applied. Notes ----- From 3cc6605c54b0f6062c1a139ece2ecf69cddc8295 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Thu, 2 Oct 2025 16:36:15 -0600 Subject: [PATCH 172/194] Update iteration print out (#1626) Update the iteration write out order. For the longest time, the iteration printers have written out mismatched values at each iteration, and also did not print the last value of the inversion. This updates the write outs to happen after the line-search (matching the behavior of the save directives), but before the directives themselves alter the objective function. I've also removed several un-needed notifications: * setting bfgsH0 * only set on BFGS subclasses and * if the UpdatePreconditioner directive is not in the directive list (It handles the appoxHinv). * Removes the repeated notifications about no nans in the model, and instead raises an error if the model vector has any nans. Writers: * The iteration writer formatting logic was rather convoluted to me, so I refactored it a bit to make it a little easier to understand what it was doing... --- docs/content/api/index.rst | 10 +- docs/content/api/simpeg.optimization.rst | 1 + simpeg/directives/_directives.py | 10 +- simpeg/directives/_sim_directives.py | 50 ++- simpeg/inverse_problem.py | 46 +-- simpeg/inversion.py | 3 - simpeg/objective_function.py | 4 + simpeg/optimization.py | 465 ++++++++++++++--------- simpeg/typing/__init__.py | 31 +- simpeg/utils/code_utils.py | 26 +- tests/base/test_inversion.py | 10 +- tests/base/test_optimizers.py | 16 +- 12 files changed, 419 insertions(+), 253 deletions(-) create mode 100644 docs/content/api/simpeg.optimization.rst diff --git a/docs/content/api/index.rst b/docs/content/api/index.rst index e401b4422d..a2de68f773 100644 --- a/docs/content/api/index.rst +++ b/docs/content/api/index.rst @@ -32,6 +32,15 @@ Regularizations simpeg.regularization +Optimizers +---------- +Optimizers used within SimPEG inversions. + +.. toctree:: + :maxdepth: 2 + + simpeg.optimization + Directives ---------- .. toctree:: @@ -41,7 +50,6 @@ Directives Utilities --------- - Classes and functions for performing useful operations. .. toctree:: diff --git a/docs/content/api/simpeg.optimization.rst b/docs/content/api/simpeg.optimization.rst new file mode 100644 index 0000000000..822bee4abe --- /dev/null +++ b/docs/content/api/simpeg.optimization.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.optimization \ No newline at end of file diff --git a/simpeg/directives/_directives.py b/simpeg/directives/_directives.py index 5ff37a47e4..aec4683746 100644 --- a/simpeg/directives/_directives.py +++ b/simpeg/directives/_directives.py @@ -32,6 +32,7 @@ Zero, eigenvalue_by_power_iteration, validate_string, + get_logger, ) from ..utils.code_utils import ( deprecate_property, @@ -659,7 +660,8 @@ def coolingRate(self, value): self._coolingRate = validate_integer("coolingRate", value, min_val=1) def endIter(self): - if self.opt.iter > 0 and self.opt.iter % self.coolingRate == 0: + it = self.opt.iter + if 0 < it < self.opt.maxIter and it % self.coolingRate == 0: if self.verbose: print( "BetaSchedule is cooling Beta. Iteration: {0:d}".format( @@ -1165,6 +1167,12 @@ def phi_d_star(self, value): self._phi_d_star = value self._target = None + def initialize(self): + logger = get_logger() + logger.info( + f"Directive {self.__class__.__name__}: Target data misfit is {self.target}" + ) + def endIter(self): if self.invProb.phi_d < self.target: self.opt.stopNextIteration = True diff --git a/simpeg/directives/_sim_directives.py b/simpeg/directives/_sim_directives.py index 126b335129..7a5f13a5ef 100644 --- a/simpeg/directives/_sim_directives.py +++ b/simpeg/directives/_sim_directives.py @@ -11,41 +11,37 @@ # # ############################################################################### class SimilarityMeasureInversionPrinters: - betas = { + beta = { "title": "betas", - "value": lambda M: ["{:.2e}".format(elem) for elem in M.parent.betas], + "value": lambda M: [f"{elem:1.2e}" for elem in M.parent.betas], "width": 26, - "format": "%s", + "format": lambda v: f"{v!s}", } lambd = { "title": "lambda", "value": lambda M: M.parent.lambd, "width": 10, - "format": "%1.2e", + "format": lambda v: f"{v:1.2e}", } - phi_d_list = { + phi_d = { "title": "phi_d", - "value": lambda M: ["{:.2e}".format(elem) for elem in M.parent.phi_d_list], + "value": lambda M: [f"{elem:1.2e}" for elem in M.parent.dmisfit._last_obj_vals], "width": 26, - "format": "%s", + "format": lambda v: f"{v!s}", } - phi_m_list = { + phi_m = { "title": "phi_m", - "value": lambda M: ["{:.2e}".format(elem) for elem in M.parent.phi_m_list], + "value": lambda M: [ + f"{elem:1.2e}" for elem in M.parent.reg._last_obj_vals[:-1] + ], "width": 26, - "format": "%s", + "format": lambda v: f"{v!s}", } phi_sim = { "title": "phi_sim", - "value": lambda M: M.parent.phi_sim, + "value": lambda M: M.parent.reg._last_obj_vals[-1], "width": 10, - "format": "%1.2e", - } - iterationCG = { - "title": "iterCG", - "value": lambda M: M.cg_count, - "width": 10, - "format": "%3d", + "format": lambda v: f"{v:1.2e}", } @@ -62,13 +58,15 @@ class SimilarityMeasureInversionDirective(InversionDirective): printers = [ IterationPrinters.iteration, - SimilarityMeasureInversionPrinters.betas, + SimilarityMeasureInversionPrinters.beta, SimilarityMeasureInversionPrinters.lambd, IterationPrinters.f, - SimilarityMeasureInversionPrinters.phi_d_list, - SimilarityMeasureInversionPrinters.phi_m_list, + SimilarityMeasureInversionPrinters.phi_d, + SimilarityMeasureInversionPrinters.phi_m, SimilarityMeasureInversionPrinters.phi_sim, - SimilarityMeasureInversionPrinters.iterationCG, + IterationPrinters.iterationCG, + IterationPrinters.iteration_CG_rel_residual, + IterationPrinters.iteration_CG_abs_residual, ] def initialize(self): @@ -108,13 +106,9 @@ def validate(self, directiveList): def endIter(self): # compute attribute values - phi_d = [] - for dmis in self.dmisfit.objfcts: - phi_d.append(dmis(self.opt.xc)) + phi_d = self.dmisfit._last_obj_vals - phi_m = [] - for reg in self.reg.objfcts: - phi_m.append(reg(self.opt.xc)) + phi_m = self.reg._last_obj_vals # pass attributes values to invProb self.invProb.phi_d_list = phi_d diff --git a/simpeg/inverse_problem.py b/simpeg/inverse_problem.py index 4a8ceb3bf2..ec24a154ef 100644 --- a/simpeg/inverse_problem.py +++ b/simpeg/inverse_problem.py @@ -1,6 +1,9 @@ +import textwrap + import numpy as np import scipy.sparse as sp import gc + from .data_misfit import BaseDataMisfit from .regularization import BaseRegularization, WeightedLeastSquares, Sparse from .objective_function import BaseObjectiveFunction, ComboObjectiveFunction @@ -12,6 +15,7 @@ validate_float, validate_type, validate_ndarray_with_shape, + get_logger, ) from .version import __version__ as simpeg_version from .utils import get_default_solver @@ -198,12 +202,14 @@ def startup(self, m0): if self.print_version: print(f"\nRunning inversion with SimPEG v{simpeg_version}") + logger = get_logger() + for fct in self.reg.objfcts: if ( hasattr(fct, "reference_model") and getattr(fct, "reference_model", None) is None ): - print( + logger.info( "simpeg.InvProblem will set Regularization.reference_model to m0." ) fct.reference_model = m0 @@ -213,38 +219,32 @@ def startup(self, m0): self.model = m0 - set_default = True - if self.init_bfgs and isinstance(self.opt, BFGS): + + sim = None # Find the first sim in data misfits that has a non None solver attribute for objfct in self.dmisfit.objfcts: if ( isinstance(objfct, BaseDataMisfit) and getattr(objfct.simulation, "solver", None) is not None ): - solver = objfct.simulation.solver - solver_opts = objfct.simulation.solver_opts - print( - """ - simpeg.InvProblem is setting bfgsH0 to the inverse of the eval2Deriv. - ***Done using same Solver, and solver_opts as the {} problem*** - """.format( - objfct.simulation.__class__.__name__ - ) - ) - set_default = False + sim = objfct.simulation break - if set_default: + if sim is not None: + solver = sim.solver + solver_opts = sim.solver_opts + msg = f""" + simpeg.InvProblem is setting bfgsH0 to the inverse of the reg.deriv2 + using the same solver as the {sim.__class__.__name__} simulation with the 'is_symmetric=True` option set. + """ + else: solver = get_default_solver() - print( + msg = f""" + simpeg.InvProblem is setting bfgsH0 to the inverse of the reg.deriv2. + using the default solver {solver.__name__} with the 'is_symmetric=True` option set. """ - simpeg.InvProblem is setting bfgsH0 to the inverse of the eval2Deriv. - ***Done using the default solver {} and no solver_opts.*** - """.format( - solver.__name__ - ) - ) - solver_opts = {} + solver_opts = dict(is_symmetric=True) + logger.info(textwrap.dedent(msg)) self.opt.bfgsH0 = solver( sp.csr_matrix(self.reg.deriv2(self.model)), **solver_opts ) diff --git a/simpeg/inversion.py b/simpeg/inversion.py index 9b1a6730ea..e444b32f1e 100644 --- a/simpeg/inversion.py +++ b/simpeg/inversion.py @@ -1,5 +1,3 @@ -import numpy as np - from .optimization import IterationPrinters, StoppingCriteria, InexactGaussNewton from .directives import DirectiveList, UpdatePreconditioner from .utils import timeIt, Counter, validate_type, validate_string @@ -114,7 +112,6 @@ def run(self, m0): self.invProb.startup(m0) self.directiveList.call("initialize") - print("model has any nan: {:b}".format(np.any(np.isnan(self.invProb.model)))) self.m = self.opt.minimize(self.invProb.evalFunction, self.invProb.model) self.directiveList.call("finish") diff --git a/simpeg/objective_function.py b/simpeg/objective_function.py index f5afd55bc8..9188168b79 100644 --- a/simpeg/objective_function.py +++ b/simpeg/objective_function.py @@ -416,6 +416,7 @@ def __init__( self.objfcts = objfcts self._multipliers = multipliers self._unpack_on_add = unpack_on_add + self._last_obj_vals = [np.nan] * len(objfcts) def __len__(self): return len(self.multipliers) @@ -454,6 +455,7 @@ def multipliers(self, value): def __call__(self, m, f=None): """Evaluate the objective functions for a given model.""" fct = 0.0 + obj_vals = [] for i, phi in enumerate(self): multiplier, objfct = phi if multiplier == 0.0: # don't evaluate the fct @@ -463,6 +465,8 @@ def __call__(self, m, f=None): else: objective_func_value = objfct(m) fct += multiplier * objective_func_value + obj_vals.append(objective_func_value) + self._last_obj_vals = obj_vals return fct def deriv(self, m, f=None): diff --git a/simpeg/optimization.py b/simpeg/optimization.py index 2ed5474429..76c328e3cd 100644 --- a/simpeg/optimization.py +++ b/simpeg/optimization.py @@ -1,10 +1,89 @@ +""" +======================================================== +SimPEG Optimizers (:mod:`simpeg.optimization`) +======================================================== +.. currentmodule:: simpeg.optimization + +Optimizers +========== + +These optimizers are available within SimPEG for use during inversion. + +Unbound Optimizers +------------------ + +These optimizers all work on unbound minimization functions. + +.. autosummary:: + :toctree: generated/ + + SteepestDescent + BFGS + GaussNewton + InexactGaussNewton + +Box Bounded Optimizers +---------------------- +These optimizers support box bound constraints on the model parameters + +.. autosummary:: + :toctree: generated/ + + ProjectedGradient + ProjectedGNCG + +Root Finding +------------ +.. autosummary:: + :toctree: generated/ + + NewtonRoot + +Minimization Base Classes +=========================== + +These classes are usually inherited or used by the optimization algorithms +above to control their execution. + +Base Minimizer +-------------- +.. autosummary:: + :toctree: generated/ + + Minimize + + +Minimizer Mixins +---------------- +.. autosummary:: + :toctree: generated/ + + Remember + Bounded + InexactCG + +Iteration Printers and Stoppers +------------------------------- +.. autosummary:: + :toctree: generated/ + + IterationPrinters + StoppingCriteria + +""" + import warnings +from collections.abc import Callable +from typing import Any, Optional import numpy as np import numpy.typing as npt import scipy.sparse as sp +from discretize.utils import Identity -from pymatsolver import Solver, Diagonal, SolverCG +from pymatsolver import Solver, SolverCG + +from .typing import MinimizeCallable from .utils import ( call_hooks, check_stoppers, @@ -33,6 +112,7 @@ "GaussNewton", "InexactGaussNewton", "ProjectedGradient", + "ProjectedGNCG", "NewtonRoot", "StoppingCriteria", "IterationPrinters", @@ -130,128 +210,153 @@ class StoppingCriteria(object): class IterationPrinters(object): """docstring for IterationPrinters""" - iteration = {"title": "#", "value": lambda M: M.iter, "width": 5, "format": "%3d"} - f = {"title": "f", "value": lambda M: M.f, "width": 10, "format": "%1.2e"} + iteration = { + "title": "#", + "value": lambda M: M.iter, + "width": 5, + "format": lambda v: f"{v:3d}", + } + f = { + "title": "f", + "value": lambda M: M.f, + "width": 10, + "format": lambda v: f"{v:1.2e}", + } norm_g = { "title": "|proj(x-g)-x|", - "value": lambda M: norm(M.projection(M.xc - M.g) - M.xc), + "value": lambda M: ( + None if M.iter == 0 else norm(M.projection(M.xc - M.g) - M.xc) + ), "width": 15, - "format": "%1.2e", + "format": lambda v: f"{v:1.2e}", + } + totalLS = { + "title": "LS", + "value": lambda M: None if M.iter == 0 else M.iterLS, + "width": 5, + "format": lambda v: f"{v:d}", } - totalLS = {"title": "LS", "value": lambda M: M.iterLS, "width": 5, "format": "%d"} iterationLS = { "title": "#", "value": lambda M: (M.iter, M.iterLS), "width": 5, - "format": "%3d.%d", + "format": lambda v: f"{v[0]:3d}.{v[1]:d}", + } + LS_ft = { + "title": "ft", + "value": lambda M: M._LS_ft, + "width": 10, + "format": lambda v: f"{v:1.2e}", + } + LS_t = { + "title": "t", + "value": lambda M: M._LS_t, + "width": 10, + "format": lambda v: f"{v:0.5f}", } - LS_ft = {"title": "ft", "value": lambda M: M._LS_ft, "width": 10, "format": "%1.2e"} - LS_t = {"title": "t", "value": lambda M: M._LS_t, "width": 10, "format": "%0.5f"} LS_armijoGoldstein = { "title": "f + alp*g.T*p", "value": lambda M: M.f + M.LSreduction * M._LS_descent, "width": 16, - "format": "%1.2e", + "format": lambda v: f"{v:1.2e}", } LS_WolfeCurvature = { "title": "alp*g.T*p", "str": "%d : ft = %1.4e >= alp*descent = %1.4e", "value": lambda M: M.LScurvature * M._LS_descent, "width": 16, - "format": "%1.2e", + "format": lambda v: f"{v:1.2e}", } itType = { "title": "itType", "value": lambda M: M._itType, "width": 8, - "format": "%s", + "format": lambda v: f"{v:s}", } aSet = { "title": "aSet", - "value": lambda M: np.sum(M.activeSet(M.xc)), + "value": lambda M: None if M.iter == 0 else np.sum(M.activeSet(M.xc)), "width": 8, - "format": "%d", + "format": lambda v: f"{v:d}", } bSet = { "title": "bSet", - "value": lambda M: np.sum(M.bindingSet(M.xc)), + "value": lambda M: None if M.iter == 0 else np.sum(M.bindingSet(M.xc)), "width": 8, - "format": "%d", + "format": lambda v: f"{v:d}", } comment = { "title": "Comment", "value": lambda M: M.comment, "width": 12, - "format": "%s", + "format": lambda v: f"{v:s}", } beta = { "title": "beta", "value": lambda M: M.parent.beta, "width": 10, - "format": "%1.2e", + "format": lambda v: f"{v:1.2e}", } phi_d = { "title": "phi_d", "value": lambda M: M.parent.phi_d, "width": 10, - "format": "%1.2e", + "format": lambda v: f"{v:1.2e}", } phi_m = { "title": "phi_m", "value": lambda M: M.parent.phi_m, "width": 10, - "format": "%1.2e", + "format": lambda v: f"{v:1.2e}", } phi_s = { "title": "phi_s", "value": lambda M: M.parent.phi_s, "width": 10, - "format": "%1.2e", + "format": lambda v: f"{v:1.2e}", } phi_x = { "title": "phi_x", "value": lambda M: M.parent.phi_x, "width": 10, - "format": "%1.2e", + "format": lambda v: f"{v:1.2e}", } phi_y = { "title": "phi_y", "value": lambda M: M.parent.phi_y, "width": 10, - "format": "%1.2e", + "format": lambda v: f"{v:1.2e}", } phi_z = { "title": "phi_z", "value": lambda M: M.parent.phi_z, "width": 10, - "format": "%1.2e", + "format": lambda v: f"{v:1.2e}", } iterationCG = { - "title": "iterCG", - "value": lambda M: M.cg_count, + "title": "iter_CG", + "value": lambda M: getattr(M, "cg_count", None), "width": 10, - "format": "%3d", + "format": lambda v: f"{v:d}", } iteration_CG_rel_residual = { "title": "CG |Ax-b|/|b|", - "value": lambda M: M.cg_rel_resid, + "value": lambda M: getattr(M, "cg_rel_resid", None), "width": 15, - "format": "%1.2e", - # "format": lambda v: f"{v:1.2e}", + "format": lambda v: f"{v:1.2e}", } iteration_CG_abs_residual = { "title": "CG |Ax-b|", - "value": lambda M: M.cg_abs_resid, + "value": lambda M: getattr(M, "cg_abs_resid", None), "width": 11, - "format": "%1.2e", - # "format": lambda v: f"{v:1.2e}", + "format": lambda v: f"{v:1.2e}", } @@ -334,74 +439,71 @@ def __init__(self, **kwargs): ] @property - def callback(self): + def callback(self) -> Optional[Callable[[np.ndarray], Any]]: + """A used defined callback function. + + Returns + ------- + None or Callable[[np.ndarray], Any] + The optional user supplied callback function accepting the current iteration + value as an input. + """ return getattr(self, "_callback", None) @callback.setter - def callback(self, value): + def callback(self, value: Callable[[np.ndarray], Any]): if self.callback is not None: print( - "The callback on the {0!s} Optimization was " - "replaced.".format(self.__class__.__name__) + f"The callback on the {self.__class__.__name__} minimizer was replaced." ) self._callback = value @timeIt - def minimize(self, evalFunction, x0) -> np.ndarray: + def minimize(self, evalFunction: MinimizeCallable, x0: np.ndarray) -> np.ndarray: """minimize(evalFunction, x0) Minimizes the function (evalFunction) starting at the location x0. - :param callable evalFunction: function handle that evaluates: f, g, H = F(x) - :param numpy.ndarray x0: starting location - :rtype: numpy.ndarray - :return: x, the last iterate of the optimization algorithm - - evalFunction is a function handle:: - - (f[, g][, H]) = evalFunction(x, return_g=False, return_H=False ) - - def evalFunction(x, return_g=False, return_H=False): - out = (f,) - if return_g: - out += (g,) - if return_H: - out += (H,) - return out if len(out) > 1 else out[0] - - - The algorithm for general minimization is as follows:: - - startup(x0) - printInit() - - while True: - doStartIteration() - f, g, H = evalFunction(xc) - printIter() - if stoppingCriteria(): break - p = findSearchDirection() - p = scaleSearchDirection(p) - xt, passLS = modifySearchDirection(p) - if not passLS: - xt, caught = modifySearchDirectionBreak(p) - if not caught: return xc - doEndIteration(xt) - - print_done() - finish() - return xc + Parameters + ---------- + evalFunction : callable + The objective function to be minimized:: + + evalFunction( + x: numpy.ndarray, + return_g: bool, + return_H: bool + ) -> ( + float + | tuple[float, numpy.ndarray] + | tuple[float, LinearOperator] + | tuple[float, numpy.ndarray, LinearOperator] + ) + + That will optionally return the gradient as a ``numpy.ndarray`` and the Hessian as any class + that supports matrix vector multiplication using the `*` operator. + + x0 : numpy.ndarray + Initial guess. + + Returns + ------- + x_min : numpy.ndarray + The last iterate of the optimization algorithm. """ self.evalFunction = evalFunction self.startup(x0) self.printInit() - if self.print_type != "ubc": - print("x0 has any nan: {:b}".format(np.any(np.isnan(x0)))) + if np.any(np.isnan(x0)): + raise ValueError("x0 has a nan.") + self.f = evalFunction( + self.xc, return_g=False, return_H=False + ) # will stash the fields objects + self.printIter() while True: self.doStartIteration() self.f, self.g, self.H = evalFunction(self.xc, return_g=True, return_H=True) - self.printIter() if self.stoppingCriteria(): break self.searchDirection = self.findSearchDirection() @@ -427,9 +529,8 @@ def evalFunction(x, return_g=False, return_H=False): return self.xc @call_hooks("startup") - def startup(self, x0): - """ - **startup** is called at the start of any new minimize call. + def startup(self, x0: np.ndarray) -> None: + """Called at the start of any new minimize call. This will set:: @@ -437,9 +538,10 @@ def startup(self, x0): xc = x0 iter = iterLS = 0 - :param numpy.ndarray x0: initial x - :rtype: None - :return: None + Parameters + ---------- + x0 : numpy.ndarray + initial x """ self.iter = 0 @@ -458,34 +560,34 @@ def startup(self, x0): @count @call_hooks("doStartIteration") - def doStartIteration(self): - """doStartIteration() - - **doStartIteration** is called at the start of each minimize - iteration. - - :rtype: None - :return: None - """ + def doStartIteration(self) -> None: + """Called at the start of each minimize iteration.""" pass - def printInit(self, inLS=False): - """ - **printInit** is called at the beginning of the optimization - routine. + def printInit(self, inLS: bool = False) -> None: + """Called at the beginning of the optimization routine. If there is a parent object, printInit will check for a parent.printInit function and call that. + Parameters + ---------- + inLS : bool + Whether this is being called from a line search. + """ pad = " " * 10 if inLS else "" name = self.name if not inLS else self.nameLS print_titles(self, self.printers if not inLS else self.printersLS, name, pad) @call_hooks("printIter") - def printIter(self, inLS=False): - """ - **printIter** is called directly after function evaluations. + def printIter(self, inLS: bool = False) -> None: + """Called directly after function evaluations. + + Parameters + ---------- + inLS : bool + Whether this is being called from a line search. If there is a parent object, printIter will check for a parent.printIter function and call that. @@ -494,13 +596,17 @@ def printIter(self, inLS=False): pad = " " * 10 if inLS else "" print_line(self, self.printers if not inLS else self.printersLS, pad=pad) - def printDone(self, inLS=False): - """ - **printDone** is called at the end of the optimization routine. + def printDone(self, inLS: bool = False) -> None: + """Called at the end of the optimization routine. If there is a parent object, printDone will check for a parent.printDone function and call that. + Parameters + ---------- + inLS : bool + Whether this is being called from a line search. + """ pad = " " * 10 if inLS else "" stop, done = ( @@ -520,7 +626,6 @@ def printDone(self, inLS=False): self.printers, pad=pad, ) - print(self.print_target) except AttributeError: print_done( self, @@ -531,18 +636,11 @@ def printDone(self, inLS=False): print_stoppers(self, stoppers, pad="", stop=stop, done=done) @call_hooks("finish") - def finish(self): - """finish() - - **finish** is called at the end of the optimization. - - :rtype: None - :return: None - - """ + def finish(self) -> None: + """Called at the end of the optimization.""" pass - def stoppingCriteria(self, inLS=False): + def stoppingCriteria(self, inLS: bool = False) -> bool: if self.iter == 0: self.f0 = self.f self.g0 = self.g @@ -550,63 +648,70 @@ def stoppingCriteria(self, inLS=False): @timeIt @call_hooks("projection") - def projection(self, p): - """projection(p) + def projection(self, p: np.ndarray) -> np.ndarray: + """Projects a model onto bounds (if given) - projects the search direction. + By default, no projection is applied. - by default, no projection is applied. + Parameters + ---------- + p : numpy.ndarray + The model to project - :param numpy.ndarray p: searchDirection - :rtype: numpy.ndarray - :return: p, projected search direction + Returns + ------- + numpy.ndarray + The projected model. """ return p @timeIt - def findSearchDirection(self): - """findSearchDirection() + def findSearchDirection(self) -> np.ndarray: + """Return the direction to search along for a minimum value. - **findSearchDirection** should return an approximation of: + Returns + ------- + numpy.ndarray + The search direction. - .. math:: + Notes + ----- + This should usually return an approximation of: - H p = - g + .. math:: - Where you are solving for the search direction, p + p = - H^{-1} g The default is: .. math:: - H = I - p = - g - And corresponds to SteepestDescent. + Corresponding to the steepest descent direction The latest function evaluations are present in:: self.f, self.g, self.H - - :rtype: numpy.ndarray - :return: p, Search Direction """ return -self.g @count - def scaleSearchDirection(self, p): - """scaleSearchDirection(p) + def scaleSearchDirection(self, p: np.ndarray) -> np.ndarray: + """Scales the search direction if appropriate. - **scaleSearchDirection** should scale the search direction if - appropriate. + Set the parameter ``maxStep`` in the minimize object, to scale back + the search direction to a maximum size. - Set the parameter **maxStep** in the minimize object, to scale back - the gradient to a maximum size. + Parameters + ---------- + p : numpy.ndarray + The current search direction. - :param numpy.ndarray p: searchDirection - :rtype: numpy.ndarray - :return: p, Scaled Search Direction + Returns + ------- + numpy.ndarray + The scaled search direction. """ if self.maxStep < np.abs(p.max()): @@ -616,12 +721,21 @@ def scaleSearchDirection(self, p): nameLS = "Armijo linesearch" #: The line-search name @timeIt - def modifySearchDirection(self, p): - """modifySearchDirection(p) + def modifySearchDirection(self, p: np.ndarray) -> np.ndarray: + """Changes the search direction based on some sort of linesearch or trust-region criteria. + + Parameters + ---------- + p : numpy.ndarray + The current search direction. - **modifySearchDirection** changes the search direction based on - some sort of linesearch or trust-region criteria. + Returns + ------- + numpy.ndarray + The modified search direction. + Notes + ----- By default, an Armijo backtracking linesearch is preformed with the following parameters: @@ -632,11 +746,7 @@ def modifySearchDirection(self, p): If the linesearch is completed, and a descent direction is found, passLS is returned as True. - Else, a modifySearchDirectionBreak call is preformed. - - :param numpy.ndarray p: searchDirection - :rtype: tuple - :return: (xt, passLS) numpy.ndarray, bool + Else, a `modifySearchDirectionBreak` call is preformed. """ # Projected Armijo linesearch self._LS_t = 1.0 @@ -672,11 +782,8 @@ def modifySearchDirection(self, p): return self._LS_xt, self.iterLS < self.maxIterLS @count - def modifySearchDirectionBreak(self, p): - """modifySearchDirectionBreak(p) - - Code is called if modifySearchDirection fails - to find a descent direction. + def modifySearchDirectionBreak(self, p: np.ndarray) -> np.ndarray: + """Called if modifySearchDirection fails to find a descent direction. The search direction is passed as input and this function must pass back both a new searchDirection, @@ -685,9 +792,18 @@ def modifySearchDirectionBreak(self, p): By default, no additional work is done, and the evalFunction returns a False indicating the break was not caught. - :param numpy.ndarray p: searchDirection - :rtype: tuple - :return: (xt, breakCaught) numpy.ndarray, bool + Parameters + ---------- + p : numpy.ndarray + The failed search direction. + + Returns + ------- + xt : numpy.ndarray + An alternative search direction to use. + was_caught : bool + Whether the break was caught. The minimization algorithm will + break early if ``not was_caught``. """ self.printDone(inLS=True) print("The linesearch got broken. Boo.") @@ -695,24 +811,26 @@ def modifySearchDirectionBreak(self, p): @count @call_hooks("doEndIteration") - def doEndIteration(self, xt): - """doEndIteration(xt) - - **doEndIteration** is called at the end of each minimize iteration. + def doEndIteration(self, xt: np.ndarray) -> None: + """Operation called at the end of each minimize iteration. By default, function values and x locations are shuffled to store 1 past iteration in memory. - self.xc must be updated in this code. - - :param numpy.ndarray xt: tested new iterate that ensures a descent direction. - :rtype: None - :return: None + Parameters + ---------- + xt : numpy.ndarray + An accepted model at the end of each iteration. """ # store old values self.f_last = self.f + if hasattr(self, "_LS_ft"): + self.f = self._LS_ft + + # the current iterate, `self.xc`, must be set in this function if overridden in a base class self.x_last, self.xc = self.xc, xt self.iter += 1 + self.printIter() # before callbacks (from directives...) if self.debug: self.printDone() @@ -1156,12 +1274,7 @@ def bfgsH0(self): Must be a simpeg.Solver """ if getattr(self, "_bfgsH0", None) is None: - print( - """ - Default solver: Diagonal is being used in bfgsH0 - """ - ) - self._bfgsH0 = Diagonal(sp.identity(self.xc.size)) + self._bfgsH0 = Identity() return self._bfgsH0 @bfgsH0.setter diff --git a/simpeg/typing/__init__.py b/simpeg/typing/__init__.py index f0d4d57b78..9f68652973 100644 --- a/simpeg/typing/__init__.py +++ b/simpeg/typing/__init__.py @@ -13,12 +13,15 @@ :toctree: generated/ RandomSeed + MinimizeCallable """ import numpy as np import numpy.typing as npt from typing import Union, TypeAlias +from collections.abc import Callable +from scipy.sparse.linalg import LinearOperator RandomSeed: TypeAlias = Union[ int, @@ -27,8 +30,7 @@ np.random.BitGenerator, np.random.Generator, ] - -RandomSeed.__doc__ = """ +""" A ``typing.Union`` for random seeds and Numpy's random number generators. These type of variables can be used throughout ``simpeg`` to control random @@ -46,3 +48,28 @@ ... rng = np.random.default_rng(seed=seed) ... ... """ + +MinimizeCallable: TypeAlias = Callable[ + [np.ndarray, bool, bool], + float + | tuple[float, np.ndarray | LinearOperator] + | tuple[float, np.ndarray, LinearOperator], +] +""" +The callable expected for the minimization operations. + +The function's signature should look like:: + + func(x: numpy.ndarray, return_g: bool, return_H: bool) + +It should output up to three values ordered as:: + + f_val : float + gradient : numpy.ndarray + H : LinearOperator + +`f_val` is always returned, `gradient` is returned if `return_g`, and `H_func` is returned if `return_H`. +`f_val` should always be the first value returned, `gradient` will always be the second, and `H_func` will +always be the last. If `return_g == return_H == False`, then only the single argument `f_val` is +returned. +""" diff --git a/simpeg/utils/code_utils.py b/simpeg/utils/code_utils.py index cf7ea5486e..ceb8ac68e7 100644 --- a/simpeg/utils/code_utils.py +++ b/simpeg/utils/code_utils.py @@ -257,9 +257,13 @@ def print_line(obj, printers, pad=""): """ values = "" for printer in printers: - values += ("{{:^{0:d}}}".format(printer["width"])).format( - printer["format"] % printer["value"](obj) - ) + value = printer["value"](obj) + format_string = f"^{printer['width']}s" + if value is not None: + formatted_val = printer["format"](value) + else: + formatted_val = "" + values += f"{formatted_val:{format_string}}" print(pad + values) @@ -316,12 +320,12 @@ def print_stoppers(obj, stoppers, pad="", stop="STOP!", done="DONE!"): done : str, default: "DONE!" String for statement when stopping criterian not encountered """ - print(pad + "{0!s}{1!s}{2!s}".format("-" * 25, stop, "-" * 25)) + print(pad + "-" * 25 + stop + "-" * 25) for stopper in stoppers: l = stopper["left"](obj) r = stopper["right"](obj) print(pad + stopper["str"] % (l <= r, l, r)) - print(pad + "{0!s}{1!s}{2!s}".format("-" * 25, done, "-" * 25)) + print(pad + "-" * 25 + done + "-" * 25) def call_hooks(match, mainFirst=False): @@ -374,17 +378,15 @@ def wrapper(self, *args, **kwargs): return out - extra = """ - If you have things that also need to run in the method {0!s}, you can create a method:: + extra = f""" + If you have things that also need to run in the method {match}, you can create a method:: - def _{1!s}*(self, ... ): + def _{match}*(self, ... ): pass - Where the * can be any string. If present, _{2!s}* will be called at the start of the default {3!s} call. + Where the * can be any string. If present, _{match}* will be called at the start of the default {match} call. You may also completely overwrite this function. - """.format( - match, match, match, match - ) + """ doc = wrapper.__doc__ wrapper.__doc__ = ("" if doc is None else doc) + extra return wrapper diff --git a/tests/base/test_inversion.py b/tests/base/test_inversion.py index f56e8ac744..6e19703e9f 100644 --- a/tests/base/test_inversion.py +++ b/tests/base/test_inversion.py @@ -41,7 +41,7 @@ def inversion(request): @pytest.mark.parametrize("dlist", [[], [smp_drcs.UpdatePreconditioner()]]) -def test_bfgs_init_logic(inversion, dlist, capsys): +def test_bfgs_init_logic(inversion, dlist, caplog, info_logging): dlist = smp_drcs.DirectiveList(*dlist, inversion=inversion) inversion.directiveList = dlist @@ -52,18 +52,18 @@ def test_bfgs_init_logic(inversion, dlist, capsys): m0 = np.zeros(10) inversion.run(m0) - captured = capsys.readouterr() + captured = caplog.text if isinstance(inv_prb.opt, smp_opt.InexactGaussNewton) and any( isinstance(dr, smp_drcs.UpdatePreconditioner) for dr in dlist ): assert not inv_prb.init_bfgs - assert "bfgsH0" not in captured.out + assert "bfgsH0" not in captured elif isinstance(inv_prb.opt, (smp_opt.BFGS, smp_opt.InexactGaussNewton)): assert inv_prb.init_bfgs - assert "bfgsH0" in captured.out + assert "bfgsH0" in captured else: assert inv_prb.init_bfgs # defaults to True even if opt would not use it. assert ( - "bfgsH0" not in captured.out + "bfgsH0" not in captured ) # But shouldn't say anything if it doesn't use it. diff --git a/tests/base/test_optimizers.py b/tests/base/test_optimizers.py index bec853c058..f8c80bb3c8 100644 --- a/tests/base/test_optimizers.py +++ b/tests/base/test_optimizers.py @@ -1,7 +1,6 @@ import re import pytest -from simpeg.optimization import ProjectedGNCG from simpeg.utils import sdiag import numpy as np import numpy.testing as npt @@ -51,6 +50,19 @@ def test_minimizer(self, optimizer, func, x_true, x0): npt.assert_allclose(xopt, x_true, rtol=TOL) +@pytest.mark.parametrize("optimizer", OPTIMIZERS) +class TestNanInit: + + def test_nan(self, optimizer): + opt = optimizer(maxIter=0) + with pytest.raises(ValueError, match=re.escape("x0 has a nan.")): + opt.minimize(rosenbrock, np.array([np.nan, 0.0])) + + def test_no_nan(self, optimizer): + opt = optimizer(maxIter=0) + opt.minimize(rosenbrock, np.array([0.0, 0.0])) + + def test_NewtonRoot(): def fun(x, return_g=True): if return_g: @@ -92,7 +104,7 @@ def test_projected_gncg_active_not_bound_branch(x0, bounded): # tests designed to test the branches of the # projected gncg when a point is in the active set but not in the binding set. func = get_quadratic(sp.identity(2).tocsr(), np.array([-5, 5])) - opt = ProjectedGNCG(upper=8, lower=0) + opt = optimization.ProjectedGNCG(upper=8, lower=0) _, g = func(x0, return_g=True, return_H=False) opt.g = g From f5e5be547393979ad5cc209a409dee5351d16365 Mon Sep 17 00:00:00 2001 From: Lindsey Heagy Date: Fri, 3 Oct 2025 09:30:09 -0700 Subject: [PATCH 173/194] Fix magnetic dipole source for for HJ formulation (#1575) Fix wrong B field when using the magnetic dipole source with the H-J formulation. When using the magnetic vector potential to define the source term in the H-J formulation, multiply by the face integration matrix before taking the curl and multiply by the inverse edge integration matrix afterwards. --- .../frequency_domain/sources.py | 95 +++++++++------ .../electromagnetics/time_domain/sources.py | 57 +++++---- .../fdem/forward/test_FDEM_dipolar_sources.py | 107 +++++++++++++++++ tests/em/tdem/test_TDEM_dipolar_sources.py | 110 ++++++++++++++++++ 4 files changed, 314 insertions(+), 55 deletions(-) create mode 100644 tests/em/fdem/forward/test_FDEM_dipolar_sources.py create mode 100644 tests/em/tdem/test_TDEM_dipolar_sources.py diff --git a/simpeg/electromagnetics/frequency_domain/sources.py b/simpeg/electromagnetics/frequency_domain/sources.py index a5a8ea19c8..8b71287245 100644 --- a/simpeg/electromagnetics/frequency_domain/sources.py +++ b/simpeg/electromagnetics/frequency_domain/sources.py @@ -359,6 +359,12 @@ class MagDipole(BaseFDEMSrc): \mathbf{M_{\sigma}^e} \mathbf{e^S} = -\mathbf{C}^T \mathbf{{M_{\mu^{-1}}^f}^S} \mathbf{b^P}} + To obtain $\mathbf{b^P}$, we compute it by taking the curl of the vector potential due to a point dipole. This is provided by :py:meth:`geoana.em.static.MagneticDipoleWholeSpace.vector_potential`. Specifically, + + .. math:: + + \vec{B}^P = \nabla \times \vec{A} + Parameters ---------- receiver_list : list of simpeg.electromagnetics.frequency_domain.receivers.BaseRx @@ -492,40 +498,31 @@ def bPrimary(self, simulation): numpy.ndarray Primary magnetic flux density """ - formulation = simulation._formulation coordinates = "cartesian" - if formulation == "EB": - gridX = simulation.mesh.gridEx - gridY = simulation.mesh.gridEy - gridZ = simulation.mesh.gridEz + if simulation._formulation == "EB": C = simulation.mesh.edge_curl - elif formulation == "HJ": - gridX = simulation.mesh.gridFx - gridY = simulation.mesh.gridFy - gridZ = simulation.mesh.gridFz - C = simulation.mesh.edge_curl.T - - if simulation.mesh._meshType == "CYL": - coordinates = "cylindrical" - - if simulation.mesh.is_symmetric is True: - if not (np.linalg.norm(self.orientation - np.r_[0.0, 0.0, 1.0]) < 1e-6): - raise AssertionError( - "for cylindrical symmetry, the dipole must be oriented" - " in the Z direction" - ) - a = self._srcFct(gridY)[:, 1] + if simulation.mesh._meshType == "CYL": + coordinates = "cylindrical" - return C * a + if simulation.mesh.is_symmetric is True: + if not ( + np.linalg.norm(self.orientation - np.r_[0.0, 0.0, 1.0]) < 1e-6 + ): + raise AssertionError( + "for cylindrical symmetry, the dipole must be oriented" + " in the Z direction" + ) + a = self._srcFct(simulation.mesh.edges_y, coordinates)[:, 1] + return C * a - ax = self._srcFct(gridX, coordinates)[:, 0] - ay = self._srcFct(gridY, coordinates)[:, 1] - az = self._srcFct(gridZ, coordinates)[:, 2] - a = np.concatenate((ax, ay, az)) + avec = self._srcFct(simulation.mesh.edges, coordinates) + a = simulation.mesh.project_edge_vector(avec) + return C * a - return C * a + elif simulation._formulation == "HJ": + return self.mu * self.hPrimary(simulation) def hPrimary(self, simulation): """Compute primary magnetic field. @@ -556,8 +553,37 @@ def hPrimary(self, simulation): out.append(h_rx @ rx.orientation) self._1d_h = out return self._1d_h - b = self.bPrimary(simulation) - return 1.0 / self.mu * b + + if simulation._formulation == "EB": + b = self.bPrimary(simulation) + return ( + 1.0 / self.mu * b + ) # same as MfI * Mfmui * b (mu primary must be a scalar) + + elif simulation._formulation == "HJ": + coordinates = "cartesian" + if simulation.mesh._meshType == "CYL": + coordinates = "cylindrical" + if simulation.mesh.is_symmetric is True: + raise AssertionError( + "for cylindrical symmetry, you must use the EB formulation for the simulation" + ) + + avec = self._srcFct(simulation.mesh.faces, coordinates) + a = simulation.mesh.project_face_vector(avec) + + a_boundary = mkvc(self._srcFct(simulation.mesh.boundary_edges)) + a_bc = simulation.mesh.boundary_edge_vector_integral * a_boundary + + return ( + 1.0 + / self.mu + * simulation.MeI + * simulation.mesh.edge_curl.T + * simulation.Mf + * a + - 1 / self.mu * simulation.MeI * a_bc + ) def s_m(self, simulation): """Magnetic source term (s_m) @@ -573,10 +599,13 @@ def s_m(self, simulation): Magnetic source term on mesh. """ - b_p = self.bPrimary(simulation) - if simulation._formulation == "HJ": - b_p = simulation.Me * b_p - return -1j * omega(self.frequency) * b_p + if simulation._formulation == "EB": + b_p = self.bPrimary(simulation) + return -1j * omega(self.frequency) * b_p + elif simulation._formulation == "HJ": + h_p = self.hPrimary(simulation) + MeMu = simulation.MeMu + return -1j * omega(self.frequency) * MeMu * h_p def s_e(self, simulation): """Electric source term (s_e) diff --git a/simpeg/electromagnetics/time_domain/sources.py b/simpeg/electromagnetics/time_domain/sources.py index c51b2869d4..67b684bbe5 100644 --- a/simpeg/electromagnetics/time_domain/sources.py +++ b/simpeg/electromagnetics/time_domain/sources.py @@ -1,5 +1,7 @@ import warnings +from discretize.utils import mkvc + import numpy as np from geoana.em.static import CircularLoopWholeSpace, MagneticDipoleWholeSpace from scipy.constants import mu_0 @@ -1232,27 +1234,23 @@ def _srcFct(self, obsLoc, coordinates="cartesian"): def _aSrc(self, simulation): coordinates = "cartesian" - if simulation._formulation == "EB": - gridX = simulation.mesh.gridEx - gridY = simulation.mesh.gridEy - gridZ = simulation.mesh.gridEz - - elif simulation._formulation == "HJ": - gridX = simulation.mesh.gridFx - gridY = simulation.mesh.gridFy - gridZ = simulation.mesh.gridFz if simulation.mesh._meshType == "CYL": coordinates = "cylindrical" if simulation.mesh.is_symmetric: - return self._srcFct(gridY)[:, 1] + if simulation._formulation != "EB": + raise AssertionError( + "For cylindrical symmtery, we must use the EB formulation of Maxwell's equations" + ) + return self._srcFct(simulation.mesh.edges, coordinates)[:, 1] - ax = self._srcFct(gridX, coordinates)[:, 0] - ay = self._srcFct(gridY, coordinates)[:, 1] - az = self._srcFct(gridZ, coordinates)[:, 2] - a = np.concatenate((ax, ay, az)) + if simulation._formulation == "EB": + avec = self._srcFct(simulation.mesh.edges, coordinates) + return simulation.mesh.project_edge_vector(avec) - return a + elif simulation._formulation == "HJ": + avec = self._srcFct(simulation.mesh.faces, coordinates) + return simulation.mesh.project_face_vector(avec) def _getAmagnetostatic(self, simulation): if simulation._formulation == "EB": @@ -1309,11 +1307,30 @@ def _phiSrc(self, simulation): def _bSrc(self, simulation): if simulation._formulation == "EB": C = simulation.mesh.edge_curl + return C * self._aSrc(simulation) elif simulation._formulation == "HJ": - C = simulation.mesh.edge_curl.T + return self.mu * self._hSrc(simulation) - return C * self._aSrc(simulation) + def _hSrc(self, simulation): + if simulation._formulation == "EB": + return 1 / self.mu * self._bSrc(simulation) + + elif simulation._formulation == "HJ": + a = self._aSrc(simulation) + + a_boundary = mkvc(self._srcFct(simulation.mesh.boundary_edges)) + a_bc = simulation.mesh.boundary_edge_vector_integral * a_boundary + + return ( + 1.0 + / self.mu + * simulation.MeI + * simulation.mesh.edge_curl.T + * simulation.Mf + * a + - 1 / self.mu * simulation.MeI * a_bc + ) def bInitial(self, simulation): """Compute initial magnetic flux density. @@ -1365,11 +1382,7 @@ def hInitial(self, simulation): if self.waveform.has_initial_fields is False: return Zero() - # if simulation._formulation == 'EB': - # return simulation.MfMui * self.bInitial(simulation) - # elif simulation._formulation == 'HJ': - # return simulation.MeMuI * self.bInitial(simulation) - return 1.0 / self.mu * self.bInitial(simulation) + return self._hSrc(simulation) def s_m(self, simulation, time): """Magnetic source term (s_m) at a given time diff --git a/tests/em/fdem/forward/test_FDEM_dipolar_sources.py b/tests/em/fdem/forward/test_FDEM_dipolar_sources.py new file mode 100644 index 0000000000..f06843cf89 --- /dev/null +++ b/tests/em/fdem/forward/test_FDEM_dipolar_sources.py @@ -0,0 +1,107 @@ +from scipy.constants import mu_0 +import numpy as np +import pytest + +from discretize import TensorMesh +from geoana.em.static import MagneticDipoleWholeSpace +import simpeg.electromagnetics.frequency_domain as fdem +from simpeg import maps + +from simpeg.utils.solver_utils import get_default_solver + +Solver = get_default_solver() + +TOL = 5e-2 # relative tolerance + +# Defining transmitter locations +source_location = np.r_[0, 0, 0] + + +def create_survey(source_type="MagDipole", mu=mu_0, orientation="Z"): + + freq = 10 + + # Must define the transmitter properties and associated receivers + source_list = [ + getattr(fdem.sources, source_type)( + [], + location=source_location, + frequency=freq, + moment=1.0, + orientation=orientation, + mu=mu, + ) + ] + + survey = fdem.Survey(source_list) + return survey + + +def create_mesh_model(): + cell_size = 20 + n_core = 10 + padding_factor = 1.3 + n_padding = 10 + + h = [ + (cell_size, n_padding, -padding_factor), + (cell_size, n_core), + (cell_size, n_padding, padding_factor), + ] + mesh = TensorMesh([h, h, h], origin="CCC") + + # Conductivity in S/m + air_conductivity = 1e-8 + background_conductivity = 1e-1 + + model = air_conductivity * np.ones(mesh.n_cells) + model[mesh.cell_centers[:, 2] < 0] = background_conductivity + + return mesh, model + + +@pytest.mark.parametrize("simulation_type", ["e", "b", "h", "j"]) +@pytest.mark.parametrize("field_test", ["bPrimary", "hPrimary"]) +@pytest.mark.parametrize("mur", [1, 50]) +def test_dipolar_fields(simulation_type, field_test, mur, orientation="Z"): + + mesh, model = create_mesh_model() + survey = create_survey("MagDipole", mu=mur * mu_0, orientation="Z") + + if simulation_type in ["e", "b"]: + grid = mesh.faces + projection = mesh.project_face_vector + if simulation_type == "e": + sim = fdem.simulation.Simulation3DElectricField( + mesh, survey=survey, sigmaMap=maps.IdentityMap(), solver=Solver + ) + elif simulation_type == "b": + sim = fdem.simulation.Simulation3DMagneticFluxDensity( + mesh, survey=survey, sigmaMap=maps.IdentityMap(), solver=Solver + ) + + elif simulation_type in ["h", "j"]: + grid = mesh.edges + projection = mesh.project_edge_vector + if simulation_type == "h": + sim = fdem.simulation.Simulation3DMagneticField( + mesh, survey=survey, sigmaMap=maps.IdentityMap(), solver=Solver + ) + elif simulation_type == "j": + sim = fdem.simulation.Simulation3DCurrentDensity( + mesh, survey=survey, sigmaMap=maps.IdentityMap(), solver=Solver + ) + + # get numeric solution + src = survey.source_list[0] + numeric = getattr(src, field_test)(sim) + + # get analytic + dipole = MagneticDipoleWholeSpace(orientation=orientation, mu=mur * mu_0) + + if field_test == "bPrimary": + analytic = projection(dipole.magnetic_flux_density(grid)) + elif field_test == "hPrimary": + analytic = projection(dipole.magnetic_field(grid)) + + assert np.abs(np.mean((numeric / analytic)) - 1) < TOL diff --git a/tests/em/tdem/test_TDEM_dipolar_sources.py b/tests/em/tdem/test_TDEM_dipolar_sources.py new file mode 100644 index 0000000000..58dfd00981 --- /dev/null +++ b/tests/em/tdem/test_TDEM_dipolar_sources.py @@ -0,0 +1,110 @@ +from discretize import TensorMesh + +from simpeg import maps +import simpeg.electromagnetics.time_domain as tdem + +import numpy as np + +from simpeg.utils.solver_utils import get_default_solver + +Solver = get_default_solver() + +TOL = 1e-2 # relative tolerance + +# Observation times for response (time channels) +n_times = 30 +time_channels = np.logspace(-4, -1, n_times) + +# Defining transmitter locations +source_locations = np.r_[0, 0, 15.5] +receiver_locations = np.atleast_2d(np.r_[0, 0, 15.5]) + + +def create_survey(src_type="MagDipole"): + + bz_receiver = tdem.receivers.PointMagneticFluxDensity( + receiver_locations, time_channels, "z" + ) + dbdtz_receiver = tdem.receivers.PointMagneticFluxTimeDerivative( + receiver_locations, time_channels, "z" + ) + receivers_list = [bz_receiver, dbdtz_receiver] + + source_list = [ + getattr(tdem.sources, src_type)( + receivers_list, + location=source_locations, + waveform=tdem.sources.StepOffWaveform(), + moment=1.0, + orientation="z", + ) + ] + survey = tdem.Survey(source_list) + return survey + + +def test_BH_dipole(): + survey_b = create_survey() + survey_h = create_survey() + + cell_size = 20 + n_core = 10 + padding_factor = 1.3 + n_padding = 15 + + h = [ + (cell_size, n_padding, -padding_factor), + (cell_size, n_core), + (cell_size, n_padding, padding_factor), + ] + mesh = TensorMesh([h, h, h], origin="CCC") + + air_conductivity = 1e-8 + background_conductivity = 1e-1 + + model = air_conductivity * np.ones(mesh.n_cells) + model[mesh.cell_centers[:, 2] < 0] = background_conductivity + + nsteps = 10 + time_steps = [ + (1e-5, nsteps), + (3e-5, nsteps), + (1e-4, nsteps), + (3e-4, nsteps), + (1e-3, nsteps), + (3e-3, nsteps), + (1e-2, nsteps - 4), + ] + + simulation_b = tdem.simulation.Simulation3DMagneticFluxDensity( + mesh, + survey=survey_b, + sigmaMap=maps.IdentityMap(), + solver=Solver, + time_steps=time_steps, + ) + + simulation_h = tdem.simulation.Simulation3DMagneticField( + mesh, + survey=survey_h, + sigmaMap=maps.IdentityMap(), + solver=Solver, + time_steps=time_steps, + ) + + fields_b = simulation_b.fields(model) + dpred_b = simulation_b.dpred(model, f=fields_b) + + fields_h = simulation_h.fields(model) + dpred_h = simulation_h.dpred(model, f=fields_h) + + assert ( + np.abs( + np.mean(dpred_b[: len(time_channels)] / dpred_h[: len(time_channels)]) - 1 + ) + < TOL + ) + assert np, ( + abs(np.mean(dpred_b[len(time_channels) :] / dpred_h[len(time_channels) :]) - 1) + < TOL + ) From 1ccc373509cf153e7100907d8ac7d595cf04ad6a Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 10 Oct 2025 17:57:32 +0000 Subject: [PATCH 174/194] Drop support for Python 3.10 (#1708) Bump minimum required version of Python to 3.11. --- .ci/azure/docs.yml | 2 +- .ci/azure/pypi.yml | 4 ++-- .ci/azure/test.yml | 2 +- pyproject.toml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.ci/azure/docs.yml b/.ci/azure/docs.yml index d2b976d8b5..1c5a982e7b 100644 --- a/.ci/azure/docs.yml +++ b/.ci/azure/docs.yml @@ -5,7 +5,7 @@ jobs: pool: vmImage: ubuntu-latest variables: - python.version: "3.10" + python.version: "3.11" timeoutInMinutes: 240 steps: # Checkout simpeg repo. diff --git a/.ci/azure/pypi.yml b/.ci/azure/pypi.yml index c6eaa93dad..237f5c4b1d 100644 --- a/.ci/azure/pypi.yml +++ b/.ci/azure/pypi.yml @@ -12,7 +12,7 @@ jobs: - task: UsePythonVersion@0 inputs: - versionSpec: "3.10" + versionSpec: "3.11" displayName: "Setup Python" - bash: | @@ -57,7 +57,7 @@ jobs: - task: UsePythonVersion@0 inputs: - versionSpec: "3.10" + versionSpec: "3.11" displayName: "Setup Python" - bash: | diff --git a/.ci/azure/test.yml b/.ci/azure/test.yml index fe6bfebe2b..403a4d69f6 100644 --- a/.ci/azure/test.yml +++ b/.ci/azure/test.yml @@ -1,6 +1,6 @@ parameters: os : ['ubuntu-latest'] - py_vers: ['3.10'] + py_vers: ['3.11'] test: ['tests/em', 'tests/base tests/flow tests/seis tests/utils', 'tests/meta', diff --git a/pyproject.toml b/pyproject.toml index ef97c3bc56..538672381f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta" name = 'simpeg' description = "SimPEG: Simulation and Parameter Estimation in Geophysics" readme = 'README.rst' -requires-python = '>=3.10' +requires-python = '>=3.11' authors = [ {name = 'SimPEG developers', email = 'rowanc1@gmail.com'}, ] From 1218acd6be3775856fb297d6fa4db3c9f64a6390 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 10 Oct 2025 19:40:11 +0000 Subject: [PATCH 175/194] Add documentation page for version compatibility (#1707) Add a new page to the docs that specify the guidelines we follow to support older versions of Python and Numpy. --- docs/content/release/index.rst | 2 ++ .../getting-started/version-compatibility.rst | 36 +++++++++++++++++++ docs/content/user-guide/index.rst | 1 + 3 files changed, 39 insertions(+) create mode 100644 docs/content/user-guide/getting-started/version-compatibility.rst diff --git a/docs/content/release/index.rst b/docs/content/release/index.rst index 0c954726f6..b99e649f57 100644 --- a/docs/content/release/index.rst +++ b/docs/content/release/index.rst @@ -1,3 +1,5 @@ +.. _release_notes: + ************* Release Notes ************* diff --git a/docs/content/user-guide/getting-started/version-compatibility.rst b/docs/content/user-guide/getting-started/version-compatibility.rst new file mode 100644 index 0000000000..a4a65b625f --- /dev/null +++ b/docs/content/user-guide/getting-started/version-compatibility.rst @@ -0,0 +1,36 @@ +Version compatibility +===================== + +SimPEG follows the time-window based policy for support of Python and Numpy +versions introduced in `NEP29 +`_. In summary, SimPEG supports: + +- all minor versions of Python released in the **prior 42 months** before + a SimPEG release, and +- all minor versions of Numpy released in the **prior 24 months** before + a SimPEG release. + +We follow these guidelines conservatively, meaning that we might not drop +support for older versions of our dependencies if they are not causing any +issue. We include notes in the :ref:`release_notes` every time we drop support +for a Python or Numpy version. + + +Supported Python versions +------------------------- + +If you require support for older Python versions, please pin SimPEG to the +following releases to ensure compatibility: + + +.. list-table:: + :widths: 40 60 + + * - **Python version** + - **Last compatible release** + * - 3.8 + - 0.22.2 + * - 3.9 + - 0.22.2 + * - 3.10 + - 0.24.0 diff --git a/docs/content/user-guide/index.rst b/docs/content/user-guide/index.rst index 0df94350fc..8ca423fb58 100644 --- a/docs/content/user-guide/index.rst +++ b/docs/content/user-guide/index.rst @@ -19,6 +19,7 @@ For details on the available classes and functions in SimPEG, please visit the getting-started/installing getting-started/contributing/index.rst getting-started/citing.rst + getting-started/version-compatibility.rst .. toctree:: :glob: From 80be6061082348ca7be9f83249b3625d1c84c85f Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 10 Oct 2025 22:26:28 +0000 Subject: [PATCH 176/194] Remove DC resistivity tutorials (#1710) Remove most of the DC tutorials, and add an admonition pointing users to the respective tutorials in User Tutorials. Part of #1555 --- tutorials/05-dcr/plot_fwd_1_dcr_sounding.py | 166 +----- tutorials/05-dcr/plot_fwd_2_dcr2d.py | 322 +---------- tutorials/05-dcr/plot_fwd_3_dcr3d.py | 379 +------------ tutorials/05-dcr/plot_inv_1_dcr_sounding.py | 324 +---------- .../05-dcr/plot_inv_1_dcr_sounding_irls.py | 328 +----------- .../plot_inv_1_dcr_sounding_parametric.py | 325 +----------- tutorials/05-dcr/plot_inv_2_dcr2d.py | 457 +--------------- tutorials/05-dcr/plot_inv_2_dcr2d_irls.py | 475 +---------------- tutorials/05-dcr/plot_inv_3_dcr3d.py | 501 +----------------- 9 files changed, 62 insertions(+), 3215 deletions(-) diff --git a/tutorials/05-dcr/plot_fwd_1_dcr_sounding.py b/tutorials/05-dcr/plot_fwd_1_dcr_sounding.py index 7ea20813c7..f4c1c78023 100644 --- a/tutorials/05-dcr/plot_fwd_1_dcr_sounding.py +++ b/tutorials/05-dcr/plot_fwd_1_dcr_sounding.py @@ -3,169 +3,13 @@ Simulate a 1D Sounding over a Layered Earth =========================================== -Here we use the module *simpeg.electromangetics.static.resistivity* to predict -sounding data over a 1D layered Earth. In this tutorial, we focus on the following: +.. important:: - - General definition of sources and receivers - - How to define the survey - - How to predict voltage or apparent resistivity data - - The units of the model and resulting data + This tutorial has been moved to `User Tutorials + `_. -For this tutorial, we will simulate sounding data over a layered Earth using -a Wenner array. The end product is a sounding curve which tells us how the -electrical resistivity changes with depth. + Checkout the `1D Forward Simulation for a Single Sounding + `_ tutorial. """ - -######################################################################### -# Import modules -# -------------- -# - -import os -import numpy as np -import matplotlib as mpl -import matplotlib.pyplot as plt - -from simpeg import maps -from simpeg.electromagnetics.static import resistivity as dc -from simpeg.utils import plot_1d_layer_model - -mpl.rcParams.update({"font.size": 16}) - -write_output = False - -# sphinx_gallery_thumbnail_number = 2 - - -##################################################################### -# Create Survey -# ------------- -# -# Here we demonstrate a general way to define sources and receivers. -# For pole and dipole sources, we must define the A or AB electrode locations, -# respectively. For the pole and dipole receivers, we must define the M or -# MN electrode locations, respectively. -# - -a_min = 20.0 -a_max = 500.0 -n_stations = 25 - -# Define the 'a' spacing for Wenner array measurements for each reading -electrode_separations = np.linspace(a_min, a_max, n_stations) - -source_list = [] # create empty array for sources to live - -for ii in range(0, len(electrode_separations)): - # Extract separation parameter for sources and receivers - a = electrode_separations[ii] - - # AB electrode locations for source. Each is a (1, 3) numpy array - A_location = np.r_[-1.5 * a, 0.0, 0.0] - B_location = np.r_[1.5 * a, 0.0, 0.0] - - # MN electrode locations for receivers. Each is an (N, 3) numpy array - M_location = np.r_[-0.5 * a, 0.0, 0.0] - N_location = np.r_[0.5 * a, 0.0, 0.0] - - # Create receivers list. Define as pole or dipole. - receiver_list = dc.receivers.Dipole( - M_location, N_location, data_type="apparent_resistivity" - ) - receiver_list = [receiver_list] - - # Define the source properties and associated receivers - source_list.append(dc.sources.Dipole(receiver_list, A_location, B_location)) - -# Define survey -survey = dc.Survey(source_list) - - -############################################### -# Defining a 1D Layered Earth Model -# --------------------------------- -# -# Here, we define the layer thicknesses and electrical resistivities for our -# 1D simulation. If we have N layers, we define N electrical resistivity -# values and N-1 layer thicknesses. The lowest layer is assumed to extend to -# infinity. In the case of a halfspace, the layer thicknesses would be -# an empty array. -# - -# Define layer thicknesses. -layer_thicknesses = np.r_[100.0, 100.0] - -# Define layer resistivities. -model = np.r_[1e3, 4e3, 2e2] - -# Define mapping from model to 1D layers. -model_map = maps.IdentityMap(nP=len(model)) - -############################################################### -# Plot Resistivity Model -# ---------------------- -# -# Here we plot the 1D resistivity model. -# - -# Plot the 1D model -ax = plot_1d_layer_model(layer_thicknesses, model_map * model) -ax.set_xlabel(r"Resistivity ($\Omega m$)") - -####################################################################### -# Define the Forward Simulation and Predict DC Resistivity Data -# ------------------------------------------------------------- -# -# Here we predict DC resistivity data. If the keyword argument *rhoMap* is -# defined, the simulation will expect a resistivity model. If the keyword -# argument *sigmaMap* is defined, the simulation will expect a conductivity model. -# - -simulation = dc.simulation_1d.Simulation1DLayers( - survey=survey, - rhoMap=model_map, - thicknesses=layer_thicknesses, -) - -# Predict data for a given model -dpred = simulation.dpred(model) - -# Plot apparent resistivities on sounding curve -fig = plt.figure(figsize=(11, 5)) -ax1 = fig.add_axes([0.1, 0.1, 0.75, 0.85]) -ax1.semilogy(1.5 * electrode_separations, dpred, "b") -ax1.set_xlabel("AB/2 (m)") -ax1.set_ylabel(r"Apparent Resistivity ($\Omega m$)") -plt.show() - - -######################################################################### -# Optional: Export Data -# --------------------- -# -# Export data and true model -# - -if write_output: - dir_path = os.path.dirname(__file__).split(os.path.sep) - dir_path.extend(["outputs"]) - dir_path = os.path.sep.join(dir_path) + os.path.sep - - if not os.path.exists(dir_path): - os.mkdir(dir_path) - - np.random.seed(145) - noise = 0.025 * dpred * np.random.randn(len(dpred)) - - data_array = np.c_[ - survey.locations_a, - survey.locations_b, - survey.locations_m, - survey.locations_n, - dpred + noise, - ] - - fname = dir_path + "app_res_1d_data.dobs" - np.savetxt(fname, data_array, fmt="%.4e") diff --git a/tutorials/05-dcr/plot_fwd_2_dcr2d.py b/tutorials/05-dcr/plot_fwd_2_dcr2d.py index 89f0556cb8..c1ca6be0f1 100644 --- a/tutorials/05-dcr/plot_fwd_2_dcr2d.py +++ b/tutorials/05-dcr/plot_fwd_2_dcr2d.py @@ -3,323 +3,13 @@ DC Resistivity Forward Simulation in 2.5D ========================================= -Here we use the module *simpeg.electromagnetics.static.resistivity* to predict -DC resistivity data and plot using a pseudosection. In this tutorial, we focus -on the following: +.. important:: - - How to define the survey - - How to define the forward simulation - - How to predict normalized voltage data for a synthetic conductivity model - - How to include surface topography - - The units of the model and resulting data + This tutorial has been moved to `User Tutorials + `_. + Checkout the `2.5D Forward Simulation + `_ tutorial. -""" - -######################################################################### -# Import modules -# -------------- -# - -from discretize import TreeMesh -from discretize.utils import mkvc, active_from_xyz - -from simpeg.utils import model_builder -from simpeg.utils.io_utils.io_utils_electromagnetics import write_dcip2d_ubc -from simpeg import maps, data -from simpeg.electromagnetics.static import resistivity as dc -from simpeg.electromagnetics.static.utils.static_utils import ( - generate_dcip_sources_line, - apparent_resistivity_from_voltage, - plot_pseudosection, -) - -import os -import numpy as np -import matplotlib as mpl -import matplotlib.pyplot as plt -from matplotlib.colors import LogNorm - - -write_output = False -mpl.rcParams.update({"font.size": 16}) -# sphinx_gallery_thumbnail_number = 3 - - -############################################################### -# Defining Topography -# ------------------- -# -# Here we define surface topography as an (N, 3) numpy array. Topography could -# also be loaded from a file. In our case, our survey takes place within a set -# of valleys that run North-South. -# - -x_topo, y_topo = np.meshgrid( - np.linspace(-3000, 3000, 601), np.linspace(-3000, 3000, 101) -) -z_topo = 40.0 * np.sin(2 * np.pi * x_topo / 800) - 40.0 -x_topo, y_topo, z_topo = mkvc(x_topo), mkvc(y_topo), mkvc(z_topo) -topo_xyz = np.c_[x_topo, y_topo, z_topo] - -# Create 2D topography. Since our 3D topography only changes in the x direction, -# it is easy to define the 2D topography projected along the survey line. For -# arbitrary topography and for an arbitrary survey orientation, the user must -# define the 2D topography along the survey line. -topo_2d = np.unique(topo_xyz[:, [0, 2]], axis=0) - -##################################################################### -# Create Dipole-Dipole Survey -# --------------------------- -# -# Here we define a single EW survey line that uses a dipole-dipole configuration. -# For the source, we must define the AB electrode locations. For the receivers -# we must define the MN electrode locations. Instead of creating the survey -# from scratch (see 1D example), we will use the *generat_dcip_survey_line* utility. -# - -# Define survey line parameters -survey_type = "dipole-dipole" -dimension_type = "2D" -data_type = "volt" -end_locations = np.r_[-400.0, 400.0] -station_separation = 40.0 -num_rx_per_src = 10 - -# Generate source list for DC survey line -source_list = generate_dcip_sources_line( - survey_type, - data_type, - dimension_type, - end_locations, - topo_2d, - num_rx_per_src, - station_separation, -) - -# Define survey -survey = dc.survey.Survey(source_list) - -############################################################### -# Create Tree Mesh -# ------------------ -# -# Here, we create the Tree mesh that will be used to predict DC data. -# - -dh = 4 # base cell width -dom_width_x = 3200.0 # domain width x -dom_width_z = 2400.0 # domain width z -nbcx = 2 ** int(np.round(np.log(dom_width_x / dh) / np.log(2.0))) # num. base cells x -nbcz = 2 ** int(np.round(np.log(dom_width_z / dh) / np.log(2.0))) # num. base cells z - -# Define the base mesh -hx = [(dh, nbcx)] -hz = [(dh, nbcz)] -mesh = TreeMesh([hx, hz], x0="CN") - -# Mesh refinement based on topography -mesh.refine_surface( - topo_xyz[:, [0, 2]], - padding_cells_by_level=[0, 0, 4, 4], - finalize=False, -) - -# Mesh refinement near transmitters and receivers. First we need to obtain the -# set of unique electrode locations. -electrode_locations = np.c_[ - survey.locations_a, - survey.locations_b, - survey.locations_m, - survey.locations_n, -] - -unique_locations = np.unique( - np.reshape(electrode_locations, (4 * survey.nD, 2)), axis=0 -) - -mesh.refine_points(unique_locations, padding_cells_by_level=[4, 4], finalize=False) - -# Refine core mesh region -xp, zp = np.meshgrid([-600.0, 600.0], [-400.0, 0.0]) -xyz = np.c_[mkvc(xp), mkvc(zp)] -mesh.refine_bounding_box(xyz, padding_cells_by_level=[0, 0, 2, 8], finalize=False) - -mesh.finalize() - -############################################################### -# Create Conductivity Model and Mapping for Tree Mesh -# ----------------------------------------------------- -# -# It is important that electrodes are not modeled as being in the air. Even if the -# electrodes are properly located along surface topography, they may lie above -# the discretized topography. This step is carried out to ensure all electrodes -# lie on the discretized surface. -# - -# Define conductivity model in S/m (or resistivity model in Ohm m) -air_conductivity = 1e-8 -background_conductivity = 1e-2 -conductor_conductivity = 1e-1 -resistor_conductivity = 1e-3 - -# Find active cells in forward modeling (cell below surface) -ind_active = active_from_xyz(mesh, topo_xyz[:, [0, 2]]) -# Define mapping from model to active cells -nC = int(ind_active.sum()) -conductivity_map = maps.InjectActiveCells(mesh, ind_active, air_conductivity) - -# Define model -conductivity_model = background_conductivity * np.ones(nC) - -ind_conductor = model_builder.get_indices_sphere( - np.r_[-120.0, -160.0], 60.0, mesh.gridCC -) -ind_conductor = ind_conductor[ind_active] -conductivity_model[ind_conductor] = conductor_conductivity - -ind_resistor = model_builder.get_indices_sphere(np.r_[120.0, -100.0], 60.0, mesh.gridCC) -ind_resistor = ind_resistor[ind_active] -conductivity_model[ind_resistor] = resistor_conductivity - -# Plot Conductivity Model -fig = plt.figure(figsize=(9, 4)) - -plotting_map = maps.InjectActiveCells(mesh, ind_active, np.nan) -norm = LogNorm(vmin=1e-3, vmax=1e-1) - -ax1 = fig.add_axes([0.14, 0.17, 0.68, 0.7]) -mesh.plot_image( - plotting_map * conductivity_model, ax=ax1, grid=False, pcolor_opts={"norm": norm} -) -ax1.set_xlim(-600, 600) -ax1.set_ylim(-600, 0) -ax1.set_title("Conductivity Model") -ax1.set_xlabel("x (m)") -ax1.set_ylabel("z (m)") - -ax2 = fig.add_axes([0.84, 0.17, 0.03, 0.7]) -cbar = mpl.colorbar.ColorbarBase(ax2, norm=norm, orientation="vertical") -cbar.set_label(r"$\sigma$ (S/m)", rotation=270, labelpad=15, size=12) - -plt.show() - - -############################################################### -# Project Survey to Discretized Topography -# ---------------------------------------- -# -# It is important that electrodes are not model as being in the air. Even if the -# electrodes are properly located along surface topography, they may lie above -# the discretized topography. This step is carried out to ensure all electrodes -# like on the discretized surface. -# - -survey.drape_electrodes_on_topography(mesh, ind_active, option="top") - - -####################################################################### -# Predict DC Resistivity Data -# --------------------------- -# -# Here we predict DC resistivity data. If the keyword argument *sigmaMap* is -# defined, the simulation will expect a conductivity model. If the keyword -# argument *rhoMap* is defined, the simulation will expect a resistivity model. -# - -simulation = dc.simulation_2d.Simulation2DNodal( - mesh, survey=survey, sigmaMap=conductivity_map -) - -# Predict the data by running the simulation. The data are the raw voltage in -# units of volts. -dpred = simulation.dpred(conductivity_model) - -####################################################################### -# Plotting in Pseudo-Section -# -------------------------- -# -# Here, we demonstrate how to plot 2D data in pseudo-section. -# First, we plot the voltages in pseudo-section as a scatter plot. This -# allows us to visualize the pseudo-sensitivity locations for our survey. -# Next, we plot the apparent conductivities in pseudo-section as a filled -# contour plot. -# - -# Plot voltages pseudo-section -fig = plt.figure(figsize=(12, 5)) -ax1 = fig.add_axes([0.1, 0.15, 0.75, 0.78]) -plot_pseudosection( - survey, - dobs=np.abs(dpred), - plot_type="scatter", - ax=ax1, - scale="log", - cbar_label="V/A", - scatter_opts={"cmap": mpl.cm.viridis}, -) -ax1.set_title("Normalized Voltages") -plt.show() - -# Get apparent conductivities from volts and survey geometry -apparent_conductivities = 1 / apparent_resistivity_from_voltage(survey, dpred) - -# Plot apparent conductivity pseudo-section -fig = plt.figure(figsize=(12, 5)) -ax1 = fig.add_axes([0.1, 0.15, 0.75, 0.78]) -plot_pseudosection( - survey, - dobs=apparent_conductivities, - plot_type="contourf", - ax=ax1, - scale="log", - cbar_label="S/m", - mask_topography=True, - contourf_opts={"levels": 20, "cmap": mpl.cm.viridis}, -) -ax1.set_title("Apparent Conductivity") -plt.show() - -####################################################################### -# Optional: Write out dpred -# ------------------------- -# -# Write DC resistivity data, topography and true model -# - -if write_output: - dir_path = os.path.dirname(__file__).split(os.path.sep) - dir_path.extend(["outputs"]) - dir_path = os.path.sep.join(dir_path) + os.path.sep - - if not os.path.exists(dir_path): - os.mkdir(dir_path) - - # Add 10% Gaussian noise to each datum - np.random.seed(225) - std = 0.05 * np.abs(dpred) - dc_noise = std * np.random.randn(len(dpred)) - dobs = dpred + dc_noise - - # Create a survey with the original electrode locations - # and not the shifted ones - # Generate source list for DC survey line - source_list = generate_dcip_sources_line( - survey_type, - data_type, - dimension_type, - end_locations, - topo_xyz, - num_rx_per_src, - station_separation, - ) - survey_original = dc.survey.Survey(source_list) - - # Write out data at their original electrode locations (not shifted) - data_obj = data.Data(survey_original, dobs=dobs, standard_deviation=std) - fname = dir_path + "dc_data.obs" - write_dcip2d_ubc(fname, data_obj, "volt", "dobs") - - fname = dir_path + "topo_xyz.txt" - np.savetxt(fname, topo_xyz, fmt="%.4e") +""" diff --git a/tutorials/05-dcr/plot_fwd_3_dcr3d.py b/tutorials/05-dcr/plot_fwd_3_dcr3d.py index b6ee498bde..d731fbc8fd 100644 --- a/tutorials/05-dcr/plot_fwd_3_dcr3d.py +++ b/tutorials/05-dcr/plot_fwd_3_dcr3d.py @@ -3,382 +3,13 @@ DC Resistivity Forward Simulation in 3D ======================================= -Here we use the module *simpeg.electromagnetics.static.resistivity* to predict -DC resistivity data on an OcTree mesh. In this tutorial, we focus on the following: +.. important:: - - How to define the survey - - How to definine a tree mesh based on the survey geometry - - How to define the forward simulations - - How to predict DC data for a synthetic conductivity model - - How to include surface topography - - The units of the model and resulting data - - Plotting DC data in 3D + This tutorial has been moved to `User Tutorials + `_. - -In this case, we simulate dipole-dipole data for three East-West lines and two -North-South lines. + Checkout the `3D Forward Simulation + `_ tutorial. """ - -############################################################## -# Import modules -# -------------- -# -# - -import os -import numpy as np -import matplotlib as mpl -import matplotlib.pyplot as plt - -from discretize import TreeMesh -from discretize.utils import mkvc, refine_tree_xyz, active_from_xyz - -from simpeg import maps, data -from simpeg.utils import model_builder -from simpeg.utils.io_utils.io_utils_electromagnetics import write_dcip_xyz -from simpeg.electromagnetics.static import resistivity as dc -from simpeg.electromagnetics.static.utils.static_utils import ( - generate_dcip_sources_line, - apparent_resistivity_from_voltage, -) - -# To plot DC data in 3D, the user must have the plotly package -try: - import plotly - from simpeg.electromagnetics.static.utils.static_utils import plot_3d_pseudosection - - has_plotly = True -except ImportError: - has_plotly = False - pass - - -mpl.rcParams.update({"font.size": 16}) -write_output = False - -# sphinx_gallery_thumbnail_number = 2 - -######################################################################### -# Defining Topography -# ------------------- -# -# Here we define surface topography as an (N, 3) numpy array. Topography could -# also be loaded from a file. In our case, our survey takes place within a circular -# depression. -# - -x_topo, y_topo = np.meshgrid( - np.linspace(-2100, 2100, 141), np.linspace(-2000, 2000, 141) -) -s = np.sqrt(x_topo**2 + y_topo**2) -z_topo = 10 + (1 / np.pi) * 140 * (-np.pi / 2 + np.arctan((s - 600.0) / 160.0)) -x_topo, y_topo, z_topo = mkvc(x_topo), mkvc(y_topo), mkvc(z_topo) -topo_xyz = np.c_[x_topo, y_topo, z_topo] - -######################################################################### -# Construct the DC Survey -# ----------------------- -# -# Here we define 5 DC lines that use a dipole-dipole electrode configuration; -# three lines along the East-West direction and 2 lines along the North-South direction. -# For each source, we must define the AB electrode locations. For each receiver -# we must define the MN electrode locations. Instead of creating the survey -# from scratch (see 1D example), we will use the *generat_dcip_sources_line* utility. -# This utility will give us the source list for a given DC/IP line. We can append -# the sources for multiple lines to create the survey. -# - -# Define the parameters for each survey line -survey_type = "dipole-dipole" -data_type = "volt" -dimension_type = "3D" -end_locations_list = [ - np.r_[-1000.0, 1000.0, 0.0, 0.0], - np.r_[-350.0, -350.0, -1000.0, 1000.0], - np.r_[350.0, 350.0, -1000.0, 1000.0], -] -station_separation = 100.0 -num_rx_per_src = 8 - -# The source lists for each line can be appended to create the source -# list for the whole survey. -source_list = [] -for ii in range(0, len(end_locations_list)): - source_list += generate_dcip_sources_line( - survey_type, - data_type, - dimension_type, - end_locations_list[ii], - topo_xyz, - num_rx_per_src, - station_separation, - ) - -# Define the survey -survey = dc.survey.Survey(source_list) - -################################################################# -# Create OcTree Mesh -# ------------------ -# -# Here, we create the OcTree mesh that will be used to predict DC data. -# -# - -# Defining domain side and minimum cell size -dh = 25.0 # base cell width -dom_width_x = 6000.0 # domain width x -dom_width_y = 6000.0 # domain width y -dom_width_z = 4000.0 # domain width z -nbcx = 2 ** int(np.round(np.log(dom_width_x / dh) / np.log(2.0))) # num. base cells x -nbcy = 2 ** int(np.round(np.log(dom_width_y / dh) / np.log(2.0))) # num. base cells y -nbcz = 2 ** int(np.round(np.log(dom_width_z / dh) / np.log(2.0))) # num. base cells z - -# Define the base mesh -hx = [(dh, nbcx)] -hy = [(dh, nbcy)] -hz = [(dh, nbcz)] -mesh = TreeMesh([hx, hy, hz], x0="CCN") - -# Mesh refinement based on topography -k = np.sqrt(np.sum(topo_xyz[:, 0:2] ** 2, axis=1)) < 1200 -mesh = refine_tree_xyz( - mesh, topo_xyz[k, :], octree_levels=[0, 6, 8], method="surface", finalize=False -) - -# Mesh refinement near sources and receivers. -electrode_locations = np.r_[ - survey.locations_a, survey.locations_b, survey.locations_m, survey.locations_n -] -unique_locations = np.unique(electrode_locations, axis=0) -mesh = refine_tree_xyz( - mesh, unique_locations, octree_levels=[4, 6, 4], method="radial", finalize=False -) - -# Finalize the mesh -mesh.finalize() - -################################################################ -# Create Conductivity Model and Mapping for OcTree Mesh -# ----------------------------------------------------- -# -# Here we define the conductivity model that will be used to predict DC -# resistivity data. The model consists of a conductive sphere and a -# resistive sphere within a moderately conductive background. Note that -# you can carry through this work flow with a resistivity model if desired. -# - -# Define conductivity model in S/m (or resistivity model in Ohm m) -air_value = 1e-8 -background_value = 1e-2 -conductor_value = 1e-1 -resistor_value = 1e-3 - -# Find active cells in forward modeling (cell below surface) -ind_active = active_from_xyz(mesh, topo_xyz) - -# Define mapping from model to active cells -nC = int(ind_active.sum()) -conductivity_map = maps.InjectActiveCells(mesh, ind_active, air_value) - -# Define model -conductivity_model = background_value * np.ones(nC) - -ind_conductor = model_builder.get_indices_sphere( - np.r_[-350.0, 0.0, -300.0], 160.0, mesh.cell_centers[ind_active, :] -) -conductivity_model[ind_conductor] = conductor_value - -ind_resistor = model_builder.get_indices_sphere( - np.r_[350.0, 0.0, -300.0], 160.0, mesh.cell_centers[ind_active, :] -) -conductivity_model[ind_resistor] = resistor_value - -# Plot Conductivity Model -fig = plt.figure(figsize=(10, 4)) - -plotting_map = maps.InjectActiveCells(mesh, ind_active, np.nan) -log_mod = np.log10(conductivity_model) - -ax1 = fig.add_axes([0.15, 0.15, 0.68, 0.75]) -mesh.plot_slice( - plotting_map * log_mod, - ax=ax1, - normal="Y", - ind=int(len(mesh.h[1]) / 2), - grid=True, - clim=(np.log10(resistor_value), np.log10(conductor_value)), - pcolor_opts={"cmap": mpl.cm.viridis}, -) -ax1.set_title("Conductivity Model") -ax1.set_xlabel("x (m)") -ax1.set_ylabel("z (m)") -ax1.set_xlim([-1000, 1000]) -ax1.set_ylim([-1000, 0]) - -ax2 = fig.add_axes([0.84, 0.15, 0.03, 0.75]) -norm = mpl.colors.Normalize( - vmin=np.log10(resistor_value), vmax=np.log10(conductor_value) -) -cbar = mpl.colorbar.ColorbarBase( - ax2, cmap=mpl.cm.viridis, norm=norm, orientation="vertical", format="$10^{%.1f}$" -) -cbar.set_label("Conductivity [S/m]", rotation=270, labelpad=15, size=12) - -########################################################## -# Project Survey to Discretized Topography -# ---------------------------------------- -# -# It is important that electrodes are not modeled as being in the air. Even if the -# electrodes are properly located along surface topography, they may lie above -# the *discretized* topography. This step is carried out to ensure all electrodes -# lie on the discretized surface. -# -# - -survey.drape_electrodes_on_topography(mesh, ind_active, option="top") - -############################################################ -# Predict DC Resistivity Data -# --------------------------- -# -# Here we predict DC resistivity data. If the keyword argument *sigmaMap* is -# defined, the simulation will expect a conductivity model. If the keyword -# argument *rhoMap* is defined, the simulation will expect a resistivity model. -# -# -# - -# Define the DC simulation -simulation = dc.simulation.Simulation3DNodal( - mesh, - survey=survey, - sigmaMap=conductivity_map, -) - -# Predict the data by running the simulation. The data are the measured voltage -# normalized by the source current in units of V/A. -dpred = simulation.dpred(conductivity_model) - -######################################################### -# Plot DC Data in 3D Pseudosection -# -------------------------------- -# -# Here we demonstrate how 3D DC resistivity data can be represented on a 3D -# pseudosection plot. To use this utility, you must have Python's *plotly* -# package. Here, we represent the data as apparent conductivities. -# -# The *plot_3d_pseudosection* utility allows the user to plot all pseudosection -# points, or plot the pseudosection plots that lie within some distance of -# one or more planes. -# - -# Since the data are normalized voltage, we must convert predicted -# to apparent conductivities. -apparent_conductivity = 1 / apparent_resistivity_from_voltage( - survey, - dpred, -) - -# For large datasets or for surveys with unconventional electrode geometry, -# interpretation can be challenging if we plot every datum. Here, we plot -# 3 out of the 5 survey lines to better image anomalous structures. -# To plot ALL of the data, simply remove the keyword argument *plane_points* -# when calling *plot_3d_pseudosection*. -plane_points = [] -p1, p2, p3 = np.array([-1000, 0, 0]), np.array([1000, 0, 0]), np.array([0, 0, -1000]) -plane_points.append([p1, p2, p3]) -p1, p2, p3 = ( - np.array([-350, -1000, 0]), - np.array([-350, 1000, 0]), - np.array([-350, 0, -1000]), -) -plane_points.append([p1, p2, p3]) -p1, p2, p3 = ( - np.array([350, -1000, 0]), - np.array([350, 1000, 0]), - np.array([350, 0, -1000]), -) -plane_points.append([p1, p2, p3]) - -if has_plotly: - fig = plot_3d_pseudosection( - survey, - apparent_conductivity, - scale="log", - units="S/m", - plane_points=plane_points, - plane_distance=15, - ) - - fig.update_layout( - title_text="Apparent Conductivity", - title_x=0.5, - title_font_size=24, - width=650, - height=500, - scene_camera=dict(center=dict(x=0.05, y=0, z=-0.4)), - ) - - plotly.io.show(fig) - -else: - print("INSTALL 'PLOTLY' TO VISUALIZE 3D PSEUDOSECTIONS") - -######################################################## -# Optional: Write Predicted DC Data -# --------------------------------- -# -# Write DC resistivity data, topography and true model -# - -if write_output: - dir_path = os.path.dirname(__file__).split(os.path.sep) - dir_path.extend(["outputs"]) - dir_path = os.path.sep.join(dir_path) + os.path.sep - - if not os.path.exists(dir_path): - os.mkdir(dir_path) - - # Add 5% Gaussian noise to each datum - np.random.seed(433) - std = 0.1 * np.abs(dpred) - noise = std * np.random.randn(len(dpred)) - dobs = dpred + noise - - # Create dictionary that stores line IDs - N = int(survey.nD / len(end_locations_list)) - lineID = np.r_[np.ones(N), 2 * np.ones(N), 3 * np.ones(N)] - out_dict = {"LINEID": lineID} - - # Create a survey with the original electrode locations - # and not the shifted ones - source_list = [] - for ii in range(0, len(end_locations_list)): - source_list += generate_dcip_sources_line( - survey_type, - data_type, - dimension_type, - end_locations_list[ii], - topo_xyz, - num_rx_per_src, - station_separation, - ) - survey_original = dc.survey.Survey(source_list) - - # Write out data at their original electrode locations (not shifted) - data_obj = data.Data(survey_original, dobs=dobs, standard_deviation=std) - - fname = dir_path + "dc_data.xyz" - write_dcip_xyz( - fname, - data_obj, - data_header="V/A", - uncertainties_header="UNCERT", - out_dict=out_dict, - ) - - fname = dir_path + "topo_xyz.txt" - np.savetxt(fname, topo_xyz, fmt="%.4e") diff --git a/tutorials/05-dcr/plot_inv_1_dcr_sounding.py b/tutorials/05-dcr/plot_inv_1_dcr_sounding.py index 75deb01d18..54159c7a65 100644 --- a/tutorials/05-dcr/plot_inv_1_dcr_sounding.py +++ b/tutorials/05-dcr/plot_inv_1_dcr_sounding.py @@ -3,324 +3,16 @@ Least-Squares 1D Inversion of Sounding Data =========================================== -Here we use the module *simpeg.electromangetics.static.resistivity* to invert -DC resistivity sounding data and recover a 1D electrical resistivity model. -In this tutorial, we focus on the following: +.. important:: - - How to define sources and receivers from a survey file - - How to define the survey - - 1D inversion of DC resistivity data + This tutorial has been moved to `User Tutorials + `_. -For this tutorial, we will invert sounding data collected over a layered Earth using -a Wenner array. The end product is layered Earth model which explains the data. + Checkout the `Weighted Least-Squares Inversion + `_ + section in the + `1D Inversion for a Single Sounding + `_ tutorial. """ - -######################################################################### -# Import modules -# -------------- -# - -import os -import numpy as np -import matplotlib as mpl -import matplotlib.pyplot as plt -import tarfile - -from discretize import TensorMesh - -from simpeg import ( - maps, - data, - data_misfit, - regularization, - optimization, - inverse_problem, - inversion, - directives, - utils, -) -from simpeg.electromagnetics.static import resistivity as dc -from simpeg.utils import plot_1d_layer_model - -mpl.rcParams.update({"font.size": 16}) - -# sphinx_gallery_thumbnail_number = 2 - -############################################# -# Define File Names -# ----------------- -# -# Here we provide the file paths to assets we need to run the inversion. The -# Path to the true model is also provided for comparison with the inversion -# results. These files are stored as a tar-file on our google cloud bucket: -# "https://storage.googleapis.com/simpeg/doc-assets/dcr1d.tar.gz" -# - -# storage bucket where we have the data -data_source = "https://storage.googleapis.com/simpeg/doc-assets/dcr1d.tar.gz" - -# download the data -downloaded_data = utils.download(data_source, overwrite=True) - -# unzip the tarfile -tar = tarfile.open(downloaded_data, "r") -tar.extractall() -tar.close() - -# path to the directory containing our data -dir_path = downloaded_data.split(".")[0] + os.path.sep - -# files to work with -data_filename = dir_path + "app_res_1d_data.dobs" - - -############################################# -# Load Data, Define Survey and Plot -# --------------------------------- -# -# Here we load the observed data, define the DC survey geometry and plot the -# data values. -# - -# Load data -dobs = np.loadtxt(str(data_filename)) - -# Extract source and receiver electrode locations and the observed data -A_electrodes = dobs[:, 0:3] -B_electrodes = dobs[:, 3:6] -M_electrodes = dobs[:, 6:9] -N_electrodes = dobs[:, 9:12] -dobs = dobs[:, -1] - -# Define survey -unique_tx, k = np.unique(np.c_[A_electrodes, B_electrodes], axis=0, return_index=True) -n_sources = len(k) -k = np.sort(k) -k = np.r_[k, len(k) + 1] - -source_list = [] -for ii in range(0, n_sources): - # MN electrode locations for receivers. Each is an (N, 3) numpy array - M_locations = M_electrodes[k[ii] : k[ii + 1], :] - N_locations = N_electrodes[k[ii] : k[ii + 1], :] - receiver_list = [ - dc.receivers.Dipole( - M_locations, - N_locations, - data_type="apparent_resistivity", - ) - ] - - # AB electrode locations for source. Each is a (1, 3) numpy array - A_location = A_electrodes[k[ii], :] - B_location = B_electrodes[k[ii], :] - source_list.append(dc.sources.Dipole(receiver_list, A_location, B_location)) - -# Define survey -survey = dc.Survey(source_list) - -# Plot apparent resistivities on sounding curve as a function of Wenner separation -# parameter. -electrode_separations = 0.5 * np.sqrt( - np.sum((survey.locations_a - survey.locations_b) ** 2, axis=1) -) - -fig = plt.figure(figsize=(11, 5)) -mpl.rcParams.update({"font.size": 14}) -ax1 = fig.add_axes([0.15, 0.1, 0.7, 0.85]) -ax1.semilogy(electrode_separations, dobs, "b") -ax1.set_xlabel("AB/2 (m)") -ax1.set_ylabel(r"Apparent Resistivity ($\Omega m$)") -plt.show() - -############################################### -# Assign Uncertainties -# -------------------- -# -# Inversion with SimPEG requires that we define standard deviation on our data. -# This represents our estimate of the noise in our data. For DC sounding data, -# a relative error is applied to each datum. For this tutorial, the relative -# error on each datum will be 2%. - -std = 0.02 * np.abs(dobs) - - -############################################### -# Define Data -# -------------------- -# -# Here is where we define the data that are inverted. The data are defined by -# the survey, the observation values and the standard deviation. -# - -data_object = data.Data(survey, dobs=dobs, standard_deviation=std) - - -############################################### -# Defining a 1D Layered Earth (1D Tensor Mesh) -# -------------------------------------------- -# -# Here, we define the layer thicknesses for our 1D simulation. To do this, we use -# the TensorMesh class. -# - -# Define layer thicknesses -layer_thicknesses = 5 * np.logspace(0, 1, 25) - -# Define a mesh for plotting and regularization. -mesh = TensorMesh([(np.r_[layer_thicknesses, layer_thicknesses[-1]])], "0") - -print(mesh) - -############################################################### -# Define a Starting and Reference Model -# ------------------------------------- -# -# Here, we create starting and/or reference models for the inversion as -# well as the mapping from the model space to the active cells. Starting and -# reference models can be a constant background value or contain a-priori -# structures. Here, the starting model is log(1000) Ohm meters. -# -# Define log-resistivity values for each layer since our model is the -# log-resistivity. Don't make the values 0! -# Otherwise the gradient for the 1st iteration is zero and the inversion will -# not converge. - -# Define model. A resistivity (Ohm meters) or conductivity (S/m) for each layer. -starting_model = np.log(2e2 * np.ones((len(layer_thicknesses) + 1))) - -# Define mapping from model to active cells. -model_map = maps.IdentityMap(nP=len(starting_model)) * maps.ExpMap() - -####################################################################### -# Define the Physics -# ------------------ -# -# Here we define the physics of the problem using the Simulation1DLayers class. -# - -simulation = dc.simulation_1d.Simulation1DLayers( - survey=survey, - rhoMap=model_map, - thicknesses=layer_thicknesses, -) - - -####################################################################### -# Define Inverse Problem -# ---------------------- -# -# The inverse problem is defined by 3 things: -# -# 1) Data Misfit: a measure of how well our recovered model explains the field data -# 2) Regularization: constraints placed on the recovered model and a priori information -# 3) Optimization: the numerical approach used to solve the inverse problem -# -# - -# Define the data misfit. Here the data misfit is the L2 norm of the weighted -# residual between the observed data and the data predicted for a given model. -# Within the data misfit, the residual between predicted and observed data are -# normalized by the data's standard deviation. -dmis = data_misfit.L2DataMisfit(simulation=simulation, data=data_object) - -# Define the regularization (model objective function) -reg = regularization.WeightedLeastSquares( - mesh, alpha_s=1.0, alpha_x=1.0, reference_model=starting_model -) - -# Define how the optimization problem is solved. Here we will use an inexact -# Gauss-Newton approach that employs the conjugate gradient solver. -opt = optimization.InexactGaussNewton(maxIter=30, maxIterCG=20) - -# Define the inverse problem -inv_prob = inverse_problem.BaseInvProblem(dmis, reg, opt) - -####################################################################### -# Define Inversion Directives -# --------------------------- -# -# Here we define any directives that are carried out during the inversion. This -# includes the cooling schedule for the trade-off parameter (beta), stopping -# criteria for the inversion and saving inversion results at each iteration. -# - -# Defining a starting value for the trade-off parameter (beta) between the data -# misfit and the regularization. -starting_beta = directives.BetaEstimate_ByEig(beta0_ratio=1e0) - -# Set the rate of reduction in trade-off parameter (beta) each time the -# the inverse problem is solved. And set the number of Gauss-Newton iterations -# for each trade-off paramter value. -beta_schedule = directives.BetaSchedule(coolingFactor=5.0, coolingRate=3.0) - -# Apply and update sensitivity weighting as the model updates -update_sensitivity_weights = directives.UpdateSensitivityWeights() - -# Options for outputting recovered models and predicted data for each beta. -save_iteration = directives.SaveOutputEveryIteration(save_txt=False) - -# Setting a stopping criteria for the inversion. -target_misfit = directives.TargetMisfit(chifact=1) - -# The directives are defined as a list. -directives_list = [ - update_sensitivity_weights, - starting_beta, - beta_schedule, - save_iteration, - target_misfit, -] - -##################################################################### -# Running the Inversion -# --------------------- -# -# To define the inversion object, we need to define the inversion problem and -# the set of directives. We can then run the inversion. -# - -# Here we combine the inverse problem and the set of directives -inv = inversion.BaseInversion(inv_prob, directives_list) - -# Run the inversion -recovered_model = inv.run(starting_model) - -############################################################ -# Examining the Results -# --------------------- -# - -# Define true model and layer thicknesses -true_model = np.r_[1e3, 4e3, 2e2] -true_layers = np.r_[100.0, 100.0] - -# Plot true model and recovered model -fig = plt.figure(figsize=(6, 4)) -x_min = np.min([np.min(model_map * recovered_model), np.min(true_model)]) -x_max = np.max([np.max(model_map * recovered_model), np.max(true_model)]) - -ax1 = fig.add_axes([0.2, 0.15, 0.7, 0.7]) -plot_1d_layer_model(true_layers, true_model, ax=ax1, plot_elevation=True, color="b") -plot_1d_layer_model( - layer_thicknesses, - model_map * recovered_model, - ax=ax1, - plot_elevation=True, - color="r", -) -ax1.set_xlabel(r"Resistivity ($\Omega m$)") -ax1.set_xlim(0.9 * x_min, 1.1 * x_max) -ax1.legend(["True Model", "Recovered Model"]) - -# Plot the true and apparent resistivities on a sounding curve -fig = plt.figure(figsize=(11, 5)) -ax1 = fig.add_axes([0.2, 0.1, 0.6, 0.8]) -ax1.semilogy(electrode_separations, dobs, "b") -ax1.semilogy(electrode_separations, inv_prob.dpred, "r") -ax1.set_xlabel("AB/2 (m)") -ax1.set_ylabel(r"Apparent Resistivity ($\Omega m$)") -ax1.legend(["True Sounding Curve", "Predicted Sounding Curve"]) -plt.show() diff --git a/tutorials/05-dcr/plot_inv_1_dcr_sounding_irls.py b/tutorials/05-dcr/plot_inv_1_dcr_sounding_irls.py index c8673c1f00..8d203ad958 100644 --- a/tutorials/05-dcr/plot_inv_1_dcr_sounding_irls.py +++ b/tutorials/05-dcr/plot_inv_1_dcr_sounding_irls.py @@ -3,328 +3,16 @@ Sparse 1D Inversion of Sounding Data ==================================== -Here we use the module *simpeg.electromangetics.static.resistivity* to invert -DC resistivity sounding data and recover a 1D electrical resistivity model. -In this tutorial, we focus on the following: +.. important:: - - How to define sources and receivers from a survey file - - How to define the survey - - 1D inversion of DC resistivity data with iteratively re-weighted least-squares + This tutorial has been moved to `User Tutorials + `_. -For this tutorial, we will invert sounding data collected over a layered Earth using -a Wenner array. The end product is layered Earth model which explains the data. + Checkout the `Iteratively Re-weighted Least-Squares Inversion + `_ + section in the + `1D Inversion for a Single Sounding + `_ tutorial. """ - -######################################################################### -# Import modules -# -------------- -# - -import os -import numpy as np -import matplotlib as mpl -import matplotlib.pyplot as plt -import tarfile - -from discretize import TensorMesh - -from simpeg import ( - maps, - data, - data_misfit, - regularization, - optimization, - inverse_problem, - inversion, - directives, - utils, -) -from simpeg.electromagnetics.static import resistivity as dc -from simpeg.utils import plot_1d_layer_model - -mpl.rcParams.update({"font.size": 16}) - -# sphinx_gallery_thumbnail_number = 2 - -############################################# -# Define File Names -# ----------------- -# -# Here we provide the file paths to assets we need to run the inversion. The -# Path to the true model is also provided for comparison with the inversion -# results. These files are stored as a tar-file on our google cloud bucket: -# "https://storage.googleapis.com/simpeg/doc-assets/dcr1d.tar.gz" -# - -# storage bucket where we have the data -data_source = "https://storage.googleapis.com/simpeg/doc-assets/dcr1d.tar.gz" - -# download the data -downloaded_data = utils.download(data_source, overwrite=True) - -# unzip the tarfile -tar = tarfile.open(downloaded_data, "r") -tar.extractall() -tar.close() - -# path to the directory containing our data -dir_path = downloaded_data.split(".")[0] + os.path.sep - -# files to work with -data_filename = dir_path + "app_res_1d_data.dobs" - - -############################################# -# Load Data, Define Survey and Plot -# --------------------------------- -# -# Here we load the observed data, define the DC survey geometry and plot the -# data values. -# - -# Load data -dobs = np.loadtxt(str(data_filename)) - -# Extract source and receiver electrode locations and the observed data -A_electrodes = dobs[:, 0:3] -B_electrodes = dobs[:, 3:6] -M_electrodes = dobs[:, 6:9] -N_electrodes = dobs[:, 9:12] -dobs = dobs[:, -1] - -# Define survey -unique_tx, k = np.unique(np.c_[A_electrodes, B_electrodes], axis=0, return_index=True) -n_sources = len(k) -k = np.sort(k) -k = np.r_[k, len(k) + 1] - -source_list = [] -for ii in range(0, n_sources): - # MN electrode locations for receivers. Each is an (N, 3) numpy array - M_locations = M_electrodes[k[ii] : k[ii + 1], :] - N_locations = N_electrodes[k[ii] : k[ii + 1], :] - receiver_list = [ - dc.receivers.Dipole( - M_locations, - N_locations, - data_type="apparent_resistivity", - ) - ] - - # AB electrode locations for source. Each is a (1, 3) numpy array - A_location = A_electrodes[k[ii], :] - B_location = B_electrodes[k[ii], :] - source_list.append(dc.sources.Dipole(receiver_list, A_location, B_location)) - -# Define survey -survey = dc.Survey(source_list) - -# Plot apparent resistivities on sounding curve as a function of Wenner separation -# parameter. -electrode_separations = 0.5 * np.sqrt( - np.sum((survey.locations_a - survey.locations_b) ** 2, axis=1) -) - -fig = plt.figure(figsize=(11, 5)) -mpl.rcParams.update({"font.size": 14}) -ax1 = fig.add_axes([0.15, 0.1, 0.7, 0.85]) -ax1.semilogy(electrode_separations, dobs, "b") -ax1.set_xlabel("AB/2 (m)") -ax1.set_ylabel(r"Apparent Resistivity ($\Omega m$)") -plt.show() - -############################################### -# Assign Uncertainties -# -------------------- -# -# Inversion with SimPEG requires that we define standard deviation on our data. -# This represents our estimate of the noise in our data. For DC sounding data, -# a relative error is applied to each datum. For this tutorial, the relative -# error on each datum will be 2%. - -std = 0.02 * np.abs(dobs) - - -############################################### -# Define Data -# -------------------- -# -# Here is where we define the data that are inverted. The data are defined by -# the survey, the observation values and the standard deviation. -# - -data_object = data.Data(survey, dobs=dobs, standard_deviation=std) - - -############################################### -# Defining a 1D Layered Earth (1D Tensor Mesh) -# -------------------------------------------- -# -# Here, we define the layer thicknesses for our 1D simulation. To do this, we use -# the TensorMesh class. -# - -# Define layer thicknesses -layer_thicknesses = 5 * np.logspace(0, 1, 25) - -# Define a mesh for plotting and regularization. -mesh = TensorMesh([(np.r_[layer_thicknesses, layer_thicknesses[-1]])], "0") - -print(mesh) - -############################################################### -# Define a Starting and Reference Model -# ------------------------------------- -# -# Here, we create starting and/or reference models for the inversion as -# well as the mapping from the model space to the active cells. Starting and -# reference models can be a constant background value or contain a-priori -# structures. Here, the starting model is log(1000) Ohm meters. -# -# Define log-resistivity values for each layer since our model is the -# log-resistivity. Don't make the values 0! -# Otherwise the gradient for the 1st iteration is zero and the inversion will -# not converge. - -# Define model. A resistivity (Ohm meters) or conductivity (S/m) for each layer. -starting_model = np.log(2e2 * np.ones((len(layer_thicknesses) + 1))) - -# Define mapping from model to active cells. -model_map = maps.IdentityMap(nP=len(starting_model)) * maps.ExpMap() - -####################################################################### -# Define the Physics -# ------------------ -# -# Here we define the physics of the problem using the Simulation1DLayers class. -# - -simulation = dc.simulation_1d.Simulation1DLayers( - survey=survey, - rhoMap=model_map, - thicknesses=layer_thicknesses, -) - - -####################################################################### -# Define Inverse Problem -# ---------------------- -# -# The inverse problem is defined by 3 things: -# -# 1) Data Misfit: a measure of how well our recovered model explains the field data -# 2) Regularization: constraints placed on the recovered model and a priori information -# 3) Optimization: the numerical approach used to solve the inverse problem -# -# - -# Define the data misfit. Here the data misfit is the L2 norm of the weighted -# residual between the observed data and the data predicted for a given model. -# Within the data misfit, the residual between predicted and observed data are -# normalized by the data's standard deviation. -dmis = data_misfit.L2DataMisfit(simulation=simulation, data=data_object) - -# Define the regularization (model objective function). Here, 'p' defines the -# the norm of the smallness term and 'q' defines the norm of the smoothness -# term. -reg = regularization.Sparse(mesh, mapping=model_map) -reg.reference_model = starting_model -p = 0 -q = 0 -reg.norms = [p, q] - -# Define how the optimization problem is solved. Here we will use an inexact -# Gauss-Newton approach that employs the conjugate gradient solver. -opt = optimization.ProjectedGNCG( - maxIter=100, maxIterLS=50, maxIterCG=20, tolCG=1e-3, upper=1e2, lower=1e-2 -) -# limits here are used to guard against overflow and underflow in the ExpMap. - -# Define the inverse problem -inv_prob = inverse_problem.BaseInvProblem(dmis, reg, opt) - -####################################################################### -# Define Inversion Directives -# --------------------------- -# -# Here we define any directives that are carried out during the inversion. This -# includes the cooling schedule for the trade-off parameter (beta), stopping -# criteria for the inversion and saving inversion results at each iteration. -# - -# Apply and update sensitivity weighting as the model updates -update_sensitivity_weights = directives.UpdateSensitivityWeights() - -# Reach target misfit for L2 solution, then use IRLS until model stops changing. -IRLS = directives.UpdateIRLS(max_irls_iterations=40, f_min_change=1e-5) - -# Defining a starting value for the trade-off parameter (beta) between the data -# misfit and the regularization. -starting_beta = directives.BetaEstimate_ByEig(beta0_ratio=20) - -# Update the preconditionner -update_Jacobi = directives.UpdatePreconditioner() - -# Options for outputting recovered models and predicted data for each beta. -save_iteration = directives.SaveOutputEveryIteration(save_txt=False) - -# The directives are defined as a list. -directives_list = [ - update_sensitivity_weights, - IRLS, - starting_beta, - update_Jacobi, - save_iteration, -] - -##################################################################### -# Running the Inversion -# --------------------- -# -# To define the inversion object, we need to define the inversion problem and -# the set of directives. We can then run the inversion. -# - -# Here we combine the inverse problem and the set of directives -inv = inversion.BaseInversion(inv_prob, directives_list) - -# Run the inversion -recovered_model = inv.run(starting_model) - -############################################################ -# Examining the Results -# --------------------- -# - -# Define true model and layer thicknesses -true_model = np.r_[1e3, 4e3, 2e2] -true_layers = np.r_[100.0, 100.0] - -# Extract Least-Squares model -l2_model = inv_prob.l2model - -# Plot true model and recovered model -fig = plt.figure(figsize=(6, 4)) -x_min = np.min(np.r_[model_map * recovered_model, model_map * l2_model, true_model]) -x_max = np.max(np.r_[model_map * recovered_model, model_map * l2_model, true_model]) - -ax1 = fig.add_axes([0.2, 0.15, 0.7, 0.7]) -plot_1d_layer_model(true_layers, true_model, ax=ax1, color="k") -plot_1d_layer_model(layer_thicknesses, model_map * l2_model, ax=ax1, color="b") -plot_1d_layer_model(layer_thicknesses, model_map * recovered_model, ax=ax1, color="r") -ax1.set_xlabel(r"Resistivity ($\Omega m$)") -ax1.set_xlim(0.9 * x_min, 1.1 * x_max) -ax1.legend(["True Model", "L2-Model", "Sparse Model"]) - -# Plot the true and apparent resistivities on a sounding curve -fig = plt.figure(figsize=(11, 5)) -ax1 = fig.add_axes([0.2, 0.1, 0.6, 0.8]) -ax1.semilogy(electrode_separations, dobs, "k") -ax1.semilogy(electrode_separations, simulation.dpred(l2_model), "b") -ax1.semilogy(electrode_separations, simulation.dpred(recovered_model), "r") -ax1.set_xlabel("AB/2 (m)") -ax1.set_ylabel(r"Apparent Resistivity ($\Omega m$)") -ax1.legend(["True Sounding Curve", "Predicted (L2-Model)", "Predicted (Sparse)"]) -plt.show() diff --git a/tutorials/05-dcr/plot_inv_1_dcr_sounding_parametric.py b/tutorials/05-dcr/plot_inv_1_dcr_sounding_parametric.py index 63936d4e9d..b0686b6662 100644 --- a/tutorials/05-dcr/plot_inv_1_dcr_sounding_parametric.py +++ b/tutorials/05-dcr/plot_inv_1_dcr_sounding_parametric.py @@ -3,326 +3,17 @@ Parametric 1D Inversion of Sounding Data ======================================== -Here we use the module *simpeg.electromangetics.static.resistivity* to invert -DC resistivity sounding data and recover the resistivities and layer thicknesses -for a 1D layered Earth. In this tutorial, we focus on the following: +.. important:: - - How to define sources and receivers from a survey file - - How to define the survey - - Defining a model that consists of resistivities and layer thicknesses + This tutorial has been moved to `User Tutorials + `_. -For this tutorial, we will invert sounding data collected over a layered Earth using -a Wenner array. The end product is layered Earth model which explains the data. + Checkout the `Parametric Inversion + `_ + section in the + `1D Inversion for a Single Sounding + `_ tutorial. """ - -######################################################################### -# Import modules -# -------------- -# - -import os -import numpy as np -import matplotlib as mpl -import matplotlib.pyplot as plt -import tarfile - -from discretize import TensorMesh - -from simpeg import ( - maps, - data, - data_misfit, - regularization, - optimization, - inverse_problem, - inversion, - directives, - utils, -) -from simpeg.electromagnetics.static import resistivity as dc -from simpeg.utils import plot_1d_layer_model - -mpl.rcParams.update({"font.size": 16}) - -# sphinx_gallery_thumbnail_number = 2 - - -############################################# -# Define File Names -# ----------------- -# -# Here we provide the file paths to assets we need to run the inversion. The -# Path to the true model is also provided for comparison with the inversion -# results. These files are stored as a tar-file on our google cloud bucket: -# "https://storage.googleapis.com/simpeg/doc-assets/dcr1d.tar.gz" -# - -# storage bucket where we have the data -data_source = "https://storage.googleapis.com/simpeg/doc-assets/dcr1d.tar.gz" - -# download the data -downloaded_data = utils.download(data_source, overwrite=True) - -# unzip the tarfile -tar = tarfile.open(downloaded_data, "r") -tar.extractall() -tar.close() - -# path to the directory containing our data -dir_path = downloaded_data.split(".")[0] + os.path.sep - -# files to work with -data_filename = dir_path + "app_res_1d_data.dobs" - - -############################################# -# Load Data, Define Survey and Plot -# --------------------------------- -# -# Here we load the observed data, define the DC survey geometry and plot the -# data values. -# - -# Load data -dobs = np.loadtxt(str(data_filename)) - -A_electrodes = dobs[:, 0:3] -B_electrodes = dobs[:, 3:6] -M_electrodes = dobs[:, 6:9] -N_electrodes = dobs[:, 9:12] -dobs = dobs[:, -1] - -# Define survey -unique_tx, k = np.unique(np.c_[A_electrodes, B_electrodes], axis=0, return_index=True) -n_sources = len(k) -k = np.sort(k) -k = np.r_[k, len(k) + 1] - -source_list = [] -for ii in range(0, n_sources): - # MN electrode locations for receivers. Each is an (N, 3) numpy array - M_locations = M_electrodes[k[ii] : k[ii + 1], :] - N_locations = N_electrodes[k[ii] : k[ii + 1], :] - receiver_list = [ - dc.receivers.Dipole( - M_locations, - N_locations, - data_type="apparent_resistivity", - ) - ] - - # AB electrode locations for source. Each is a (1, 3) numpy array - A_location = A_electrodes[k[ii], :] - B_location = B_electrodes[k[ii], :] - source_list.append(dc.sources.Dipole(receiver_list, A_location, B_location)) - -# Define survey -survey = dc.Survey(source_list) - -# Plot apparent resistivities on sounding curve as a function of Wenner separation -# parameter. -electrode_separations = 0.5 * np.sqrt( - np.sum((survey.locations_a - survey.locations_b) ** 2, axis=1) -) - -fig = plt.figure(figsize=(11, 5)) -mpl.rcParams.update({"font.size": 14}) -ax1 = fig.add_axes([0.15, 0.1, 0.7, 0.85]) -ax1.semilogy(electrode_separations, dobs, "b") -ax1.set_xlabel("AB/2 (m)") -ax1.set_ylabel(r"Apparent Resistivity ($\Omega m$)") -plt.show() - -############################################### -# Assign Uncertainties -# -------------------- -# -# Inversion with SimPEG requires that we define standard deviation on our data. -# This represents our estimate of the noise in our data. For DC sounding data, -# a relative error is applied to each datum. For this tutorial, the relative -# error on each datum will be 2.5%. -# - -std = 0.025 * dobs - - -############################################### -# Define Data -# -------------------- -# -# Here is where we define the data that are inverted. The data are defined by -# the survey, the observation values and the standard deviation. -# - -data_object = data.Data(survey, dobs=dobs, standard_deviation=std) - -############################################################### -# Defining the Starting Model and Mapping -# --------------------------------------- -# -# In this case, the model consists of parameters which define the respective -# resistivities and thickness for a set of horizontal layer. Here, we choose to -# define a model consisting of 3 layers. -# - -# Define the resistivities and thicknesses for the starting model. The thickness -# of the bottom layer is assumed to extend downward to infinity so we don't -# need to define it. -resistivities = np.r_[1e3, 1e3, 1e3] -layer_thicknesses = np.r_[50.0, 50.0] - -# Define a mesh for plotting and regularization. -mesh = TensorMesh([(np.r_[layer_thicknesses, layer_thicknesses[-1]])], "0") -print(mesh) - -# Define model. We are inverting for the layer resistivities and layer thicknesses. -# Since the bottom layer extends to infinity, it is not a model parameter for -# which we need to invert. For a 3 layer model, there is a total of 5 parameters. -# For stability, our model is the log-resistivity and log-thickness. -starting_model = np.r_[np.log(resistivities), np.log(layer_thicknesses)] - -# Since the model contains two different properties for each layer, we use -# wire maps to distinguish the properties. -wire_map = maps.Wires(("rho", mesh.nC), ("t", mesh.nC - 1)) -resistivity_map = maps.ExpMap(nP=mesh.nC) * wire_map.rho -layer_map = maps.ExpMap(nP=mesh.nC - 1) * wire_map.t - -####################################################################### -# Define the Physics -# ------------------ -# -# Here we define the physics of the problem. The data consists of apparent -# resistivity values. This is defined here. -# - -simulation = dc.simulation_1d.Simulation1DLayers( - survey=survey, - rhoMap=resistivity_map, - thicknessesMap=layer_map, -) - -####################################################################### -# Define Inverse Problem -# ---------------------- -# -# The inverse problem is defined by 3 things: -# -# 1) Data Misfit: a measure of how well our recovered model explains the field data -# 2) Regularization: constraints placed on the recovered model and a priori information -# 3) Optimization: the numerical approach used to solve the inverse problem -# -# - -# Define the data misfit. Here the data misfit is the L2 norm of the weighted -# residual between the observed data and the data predicted for a given model. -# Within the data misfit, the residual between predicted and observed data are -# normalized by the data's standard deviation. -dmis = data_misfit.L2DataMisfit(simulation=simulation, data=data_object) - -# Define the regularization on the parameters related to resistivity -mesh_rho = TensorMesh([mesh.h[0].size]) -reg_rho = regularization.WeightedLeastSquares( - mesh_rho, alpha_s=0.01, alpha_x=1, mapping=wire_map.rho -) - -# Define the regularization on the parameters related to layer thickness -mesh_t = TensorMesh([mesh.h[0].size - 1]) -reg_t = regularization.WeightedLeastSquares( - mesh_t, alpha_s=0.01, alpha_x=1, mapping=wire_map.t -) - -# Combine to make regularization for the inversion problem -reg = reg_rho + reg_t - -# Define how the optimization problem is solved. Here we will use an inexact -# Gauss-Newton approach that employs the conjugate gradient solver. -opt = optimization.InexactGaussNewton(maxIter=50, maxIterCG=30) - -# Define the inverse problem -inv_prob = inverse_problem.BaseInvProblem(dmis, reg, opt) - -####################################################################### -# Define Inversion Directives -# --------------------------- -# -# Here we define any directives that are carried out during the inversion. This -# includes the cooling schedule for the trade-off parameter (beta), stopping -# criteria for the inversion and saving inversion results at each iteration. -# - -# Defining a starting value for the trade-off parameter (beta) between the data -# misfit and the regularization. -starting_beta = directives.BetaEstimate_ByEig(beta0_ratio=1e1) - -# Set the rate of reduction in trade-off parameter (beta) each time the -# the inverse problem is solved. And set the number of Gauss-Newton iterations -# for each trade-off paramter value. -beta_schedule = directives.BetaSchedule(coolingFactor=5.0, coolingRate=3.0) - -# Options for outputting recovered models and predicted data for each beta. -save_iteration = directives.SaveOutputEveryIteration(save_txt=False) - -# Setting a stopping criteria for the inversion. -target_misfit = directives.TargetMisfit(chifact=0.1) - -# The directives are defined in a list -directives_list = [ - starting_beta, - beta_schedule, - target_misfit, -] - -##################################################################### -# Running the Inversion -# --------------------- -# -# To define the inversion object, we need to define the inversion problem and -# the set of directives. We can then run the inversion. -# - -# Here we combine the inverse problem and the set of directives -inv = inversion.BaseInversion(inv_prob, directiveList=directives_list) - -# Run the inversion -recovered_model = inv.run(starting_model) - -############################################################ -# Examining the Results -# --------------------- -# - -# Define true model and layer thicknesses -true_model = np.r_[1e3, 4e3, 2e2] -true_layers = np.r_[100.0, 100.0] - -# Plot true model and recovered model -fig = plt.figure(figsize=(5, 5)) - -x_min = np.min([np.min(resistivity_map * recovered_model), np.min(true_model)]) -x_max = np.max([np.max(resistivity_map * recovered_model), np.max(true_model)]) - -ax1 = fig.add_axes([0.2, 0.15, 0.7, 0.7]) -plot_1d_layer_model(true_layers, true_model, ax=ax1, plot_elevation=True, color="b") -plot_1d_layer_model( - layer_map * recovered_model, - resistivity_map * recovered_model, - ax=ax1, - plot_elevation=True, - color="r", -) -ax1.set_xlabel(r"Resistivity ($\Omega m$)") -ax1.set_xlim(0.9 * x_min, 1.1 * x_max) -ax1.legend(["True Model", "Recovered Model"]) - -# Plot the true and apparent resistivities on a sounding curve -fig = plt.figure(figsize=(11, 5)) -ax1 = fig.add_axes([0.2, 0.05, 0.6, 0.8]) -ax1.semilogy(electrode_separations, dobs, "b") -ax1.semilogy(electrode_separations, inv_prob.dpred, "r") -ax1.set_xlabel("AB/2 (m)") -ax1.set_ylabel(r"Apparent Resistivity ($\Omega m$)") -ax1.legend(["True Sounding Curve", "Predicted Sounding Curve"]) -plt.show() diff --git a/tutorials/05-dcr/plot_inv_2_dcr2d.py b/tutorials/05-dcr/plot_inv_2_dcr2d.py index 063ccedc70..e0766f3be1 100644 --- a/tutorials/05-dcr/plot_inv_2_dcr2d.py +++ b/tutorials/05-dcr/plot_inv_2_dcr2d.py @@ -2,456 +2,15 @@ 2.5D DC Resistivity Least-Squares Inversion =========================================== -Here we invert a line of DC resistivity data to recover an electrical -conductivity model. We formulate the inverse problem as a least-squares -optimization problem. For this tutorial, we focus on the following: +.. important:: - - Defining the survey - - Generating a mesh based on survey geometry - - Including surface topography - - Defining the inverse problem (data misfit, regularization, directives) - - Applying sensitivity weighting - - Plotting the recovered model and data misfit + This tutorial has been moved to `User Tutorials + `_. + Checkout the `Weighted Least-Squares Inversion + `_ + section in the + `2.5D DC Resistivity Inversion + `_ tutorial. """ - -######################################################################### -# Import modules -# -------------- -# - -import os -import numpy as np -import matplotlib as mpl -import matplotlib.pyplot as plt -from matplotlib.colors import LogNorm -import tarfile - -from discretize import TreeMesh -from discretize.utils import mkvc, active_from_xyz - -from simpeg.utils import model_builder -from simpeg import ( - maps, - data_misfit, - regularization, - optimization, - inverse_problem, - inversion, - directives, - utils, -) -from simpeg.electromagnetics.static import resistivity as dc -from simpeg.electromagnetics.static.utils.static_utils import ( - plot_pseudosection, -) -from simpeg.utils.io_utils.io_utils_electromagnetics import read_dcip2d_ubc - - -mpl.rcParams.update({"font.size": 16}) -# sphinx_gallery_thumbnail_number = 4 - - -############################################# -# Download Assets -# --------------- -# -# Here we provide the file paths to assets we need to run the inversion. The -# path to the true model conductivity and chargeability models are also -# provided for comparison with the inversion results. These files are stored as a -# tar-file on our google cloud bucket: -# "https://storage.googleapis.com/simpeg/doc-assets/dcr2d.tar.gz" -# - -# storage bucket where we have the data -data_source = "https://storage.googleapis.com/simpeg/doc-assets/dcr2d.tar.gz" - -# download the data -downloaded_data = utils.download(data_source, overwrite=True) - -# unzip the tarfile -tar = tarfile.open(downloaded_data, "r") -tar.extractall() -tar.close() - -# path to the directory containing our data -dir_path = downloaded_data.split(".")[0] + os.path.sep - -# files to work with -topo_filename = dir_path + "topo_xyz.txt" -data_filename = dir_path + "dc_data.obs" - - -############################################# -# Load Data, Define Survey and Plot -# --------------------------------- -# -# Here we load the observed data, define the DC and IP survey geometry and -# plot the data values using pseudo-sections. -# **Warning**: In the following example, the observations file is assumed to be -# sorted by sources -# - -# Load data -topo_xyz = np.loadtxt(str(topo_filename)) -dc_data = read_dcip2d_ubc(data_filename, "volt", "general") - -####################################################################### -# Plot Observed Data in Pseudo-Section -# ------------------------------------ -# -# Here, we demonstrate how to plot 2D data in pseudo-section. -# First, we plot the actual data (voltages) in pseudo-section as a scatter plot. -# This allows us to visualize the pseudo-sensitivity locations for our survey. -# Next, we plot the data as apparent conductivities in pseudo-section with a filled -# contour plot. -# - -# Plot voltages pseudo-section -fig = plt.figure(figsize=(12, 5)) -ax1 = fig.add_axes([0.1, 0.15, 0.75, 0.78]) -plot_pseudosection( - dc_data, - plot_type="scatter", - ax=ax1, - scale="log", - cbar_label="V/A", - scatter_opts={"cmap": mpl.cm.viridis}, -) -ax1.set_title("Normalized Voltages") -plt.show() - -# Plot apparent conductivity pseudo-section -fig = plt.figure(figsize=(12, 5)) -ax1 = fig.add_axes([0.1, 0.15, 0.75, 0.78]) -plot_pseudosection( - dc_data, - plot_type="contourf", - ax=ax1, - scale="log", - data_type="apparent conductivity", - cbar_label="S/m", - mask_topography=True, - contourf_opts={"levels": 20, "cmap": mpl.cm.viridis}, -) -ax1.set_title("Apparent Conductivity") -plt.show() - -#################################################### -# Assign Uncertainties -# -------------------- -# -# Inversion with SimPEG requires that we define the uncertainties on our data. -# This represents our estimate of the standard deviation of the -# noise in our data. For DC data, the uncertainties are 10% of the absolute value -# -# - -dc_data.standard_deviation = 0.05 * np.abs(dc_data.dobs) - -######################################################## -# Create Tree Mesh -# ------------------ -# -# Here, we create the Tree mesh that will be used to invert DC data. -# - -dh = 4 # base cell width -dom_width_x = 3200.0 # domain width x -dom_width_z = 2400.0 # domain width z -nbcx = 2 ** int(np.round(np.log(dom_width_x / dh) / np.log(2.0))) # num. base cells x -nbcz = 2 ** int(np.round(np.log(dom_width_z / dh) / np.log(2.0))) # num. base cells z - -# Define the base mesh -hx = [(dh, nbcx)] -hz = [(dh, nbcz)] -mesh = TreeMesh([hx, hz], x0="CN") - -# Mesh refinement based on topography -mesh.refine_surface( - topo_xyz[:, [0, 2]], - padding_cells_by_level=[0, 0, 4, 4], - finalize=False, -) - -# Mesh refinement near transmitters and receivers. First we need to obtain the -# set of unique electrode locations. -electrode_locations = np.c_[ - dc_data.survey.locations_a, - dc_data.survey.locations_b, - dc_data.survey.locations_m, - dc_data.survey.locations_n, -] - -unique_locations = np.unique( - np.reshape(electrode_locations, (4 * dc_data.survey.nD, 2)), axis=0 -) - -mesh.refine_points(unique_locations, padding_cells_by_level=[4, 4], finalize=False) - -# Refine core mesh region -xp, zp = np.meshgrid([-600.0, 600.0], [-400.0, 0.0]) -xyz = np.c_[mkvc(xp), mkvc(zp)] -mesh.refine_bounding_box(xyz, padding_cells_by_level=[0, 0, 2, 8], finalize=False) - -mesh.finalize() - - -############################################################### -# Project Surveys to Discretized Topography -# ----------------------------------------- -# -# It is important that electrodes are not model as being in the air. Even if the -# electrodes are properly located along surface topography, they may lie above -# the discretized topography. This step is carried out to ensure all electrodes -# like on the discretized surface. -# - -# Create 2D topography. Since our 3D topography only changes in the x direction, -# it is easy to define the 2D topography projected along the survey line. For -# arbitrary topography and for an arbitrary survey orientation, the user must -# define the 2D topography along the survey line. -topo_2d = np.unique(topo_xyz[:, [0, 2]], axis=0) - -# Find cells that lie below surface topography -ind_active = active_from_xyz(mesh, topo_2d) - -# Extract survey from data object -survey = dc_data.survey - -# Shift electrodes to the surface of discretized topography -survey.drape_electrodes_on_topography(mesh, ind_active, option="top") - -# Reset survey in data object -dc_data.survey = survey - - -######################################################## -# Starting/Reference Model and Mapping on Tree Mesh -# --------------------------------------------------- -# -# Here, we would create starting and/or reference models for the DC inversion as -# well as the mapping from the model space to the active cells. Starting and -# reference models can be a constant background value or contain a-priori -# structures. Here, the starting model is the natural log of 0.01 S/m. -# - -# Define conductivity model in S/m (or resistivity model in Ohm m) -air_conductivity = np.log(1e-8) -background_conductivity = np.log(1e-2) - -active_map = maps.InjectActiveCells(mesh, ind_active, np.exp(air_conductivity)) -nC = int(ind_active.sum()) - -conductivity_map = active_map * maps.ExpMap() - -# Define model -starting_conductivity_model = background_conductivity * np.ones(nC) - -############################################## -# Define the Physics of the DC Simulation -# --------------------------------------- -# -# Here, we define the physics of the DC resistivity problem. -# - -# Define the problem. Define the cells below topography and the mapping -simulation = dc.simulation_2d.Simulation2DNodal( - mesh, survey=survey, sigmaMap=conductivity_map, storeJ=True -) - -####################################################################### -# Define DC Inverse Problem -# ------------------------- -# -# The inverse problem is defined by 3 things: -# -# 1) Data Misfit: a measure of how well our recovered model explains the field data -# 2) Regularization: constraints placed on the recovered model and a priori information -# 3) Optimization: the numerical approach used to solve the inverse problem -# -# - -# Define the data misfit. Here the data misfit is the L2 norm of the weighted -# residual between the observed data and the data predicted for a given model. -# Within the data misfit, the residual between predicted and observed data are -# normalized by the data's standard deviation. -dmis = data_misfit.L2DataMisfit(data=dc_data, simulation=simulation) - -# Define the regularization (model objective function) -reg = regularization.WeightedLeastSquares( - mesh, - active_cells=ind_active, - reference_model=starting_conductivity_model, -) - -reg.reference_model_in_smooth = True # Reference model in smoothness term - -# Define how the optimization problem is solved. Here we will use an -# Inexact Gauss Newton approach. -opt = optimization.InexactGaussNewton(maxIter=40) - -# Here we define the inverse problem that is to be solved -inv_prob = inverse_problem.BaseInvProblem(dmis, reg, opt) - -####################################################################### -# Define DC Inversion Directives -# ------------------------------ -# -# Here we define any directives that are carried out during the inversion. This -# includes the cooling schedule for the trade-off parameter (beta), stopping -# criteria for the inversion and saving inversion results at each iteration. -# - -# Apply and update sensitivity weighting as the model updates -update_sensitivity_weighting = directives.UpdateSensitivityWeights() - -# Defining a starting value for the trade-off parameter (beta) between the data -# misfit and the regularization. -starting_beta = directives.BetaEstimate_ByEig(beta0_ratio=1e1) - -# Set the rate of reduction in trade-off parameter (beta) each time the -# the inverse problem is solved. And set the number of Gauss-Newton iterations -# for each trade-off paramter value. -beta_schedule = directives.BetaSchedule(coolingFactor=3, coolingRate=2) - -# Options for outputting recovered models and predicted data for each beta. -save_iteration = directives.SaveOutputEveryIteration(save_txt=False) - -# Setting a stopping criteria for the inversion. -target_misfit = directives.TargetMisfit(chifact=1) - -# Update preconditioner -update_jacobi = directives.UpdatePreconditioner() - -directives_list = [ - update_sensitivity_weighting, - starting_beta, - beta_schedule, - save_iteration, - target_misfit, - update_jacobi, -] - -##################################################################### -# Running the DC Inversion -# ------------------------ -# -# To define the inversion object, we need to define the inversion problem and -# the set of directives. We can then run the inversion. -# - -# Here we combine the inverse problem and the set of directives -dc_inversion = inversion.BaseInversion(inv_prob, directiveList=directives_list) - -# Run inversion -recovered_conductivity_model = dc_inversion.run(starting_conductivity_model) - -############################################################ -# Recreate True Conductivity Model -# -------------------------------- -# - -true_background_conductivity = 1e-2 -true_conductor_conductivity = 1e-1 -true_resistor_conductivity = 1e-3 - -true_conductivity_model = true_background_conductivity * np.ones(len(mesh)) - -ind_conductor = model_builder.get_indices_sphere( - np.r_[-120.0, -180.0], 60.0, mesh.gridCC -) -true_conductivity_model[ind_conductor] = true_conductor_conductivity - -ind_resistor = model_builder.get_indices_sphere(np.r_[120.0, -180.0], 60.0, mesh.gridCC) -true_conductivity_model[ind_resistor] = true_resistor_conductivity - -true_conductivity_model[~ind_active] = np.nan - -############################################################ -# Plotting True and Recovered Conductivity Model -# ---------------------------------------------- -# - -# Plot True Model -norm = LogNorm(vmin=1e-3, vmax=1e-1) - -fig = plt.figure(figsize=(9, 4)) -ax1 = fig.add_axes([0.14, 0.17, 0.68, 0.7]) -im = mesh.plot_image( - true_conductivity_model, ax=ax1, grid=False, pcolor_opts={"norm": norm} -) -ax1.set_xlim(-600, 600) -ax1.set_ylim(-600, 0) -ax1.set_title("True Conductivity Model") -ax1.set_xlabel("x (m)") -ax1.set_ylabel("z (m)") - -ax2 = fig.add_axes([0.84, 0.17, 0.03, 0.7]) -cbar = mpl.colorbar.ColorbarBase(ax2, norm=norm, orientation="vertical") -cbar.set_label(r"$\sigma$ (S/m)", rotation=270, labelpad=15, size=12) - -plt.show() - -# # Plot Recovered Model -fig = plt.figure(figsize=(9, 4)) - -recovered_conductivity = conductivity_map * recovered_conductivity_model -recovered_conductivity[~ind_active] = np.nan - -ax1 = fig.add_axes([0.14, 0.17, 0.68, 0.7]) -mesh.plot_image( - recovered_conductivity, normal="Y", ax=ax1, grid=False, pcolor_opts={"norm": norm} -) -ax1.set_xlim(-600, 600) -ax1.set_ylim(-600, 0) -ax1.set_title("Recovered Conductivity Model") -ax1.set_xlabel("x (m)") -ax1.set_ylabel("z (m)") - -ax2 = fig.add_axes([0.84, 0.17, 0.03, 0.7]) -cbar = mpl.colorbar.ColorbarBase(ax2, norm=norm, orientation="vertical") -cbar.set_label(r"$\sigma$ (S/m)", rotation=270, labelpad=15, size=12) - -plt.show() - -################################################################### -# Plotting Predicted DC Data and Misfit -# ------------------------------------- -# - -# Predicted data from recovered model -dpred = inv_prob.dpred -dobs = dc_data.dobs -std = dc_data.standard_deviation - -# Plot -fig = plt.figure(figsize=(9, 13)) -data_array = [np.abs(dobs), np.abs(dpred), (dobs - dpred) / std] -plot_title = ["Observed Voltage", "Predicted Voltage", "Normalized Misfit"] -plot_units = ["V/A", "V/A", ""] -scale = ["log", "log", "linear"] - -ax1 = 3 * [None] -cax1 = 3 * [None] -cbar = 3 * [None] -cplot = 3 * [None] - -for ii in range(0, 3): - ax1[ii] = fig.add_axes([0.15, 0.72 - 0.33 * ii, 0.65, 0.21]) - cax1[ii] = fig.add_axes([0.81, 0.72 - 0.33 * ii, 0.03, 0.21]) - cplot[ii] = plot_pseudosection( - survey, - data_array[ii], - "contourf", - ax=ax1[ii], - cax=cax1[ii], - scale=scale[ii], - cbar_label=plot_units[ii], - mask_topography=True, - contourf_opts={"levels": 25, "cmap": mpl.cm.viridis}, - ) - ax1[ii].set_title(plot_title[ii]) - -plt.show() diff --git a/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py b/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py index d030b52a0f..7eedf1d219 100644 --- a/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py +++ b/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py @@ -2,473 +2,16 @@ 2.5D DC Resistivity Inversion with Sparse Norms =============================================== -Here we invert a line of DC resistivity data to recover an electrical -conductivity model. We formulate the inverse problem as a least-squares -optimization problem. For this tutorial, we focus on the following: +.. important:: - - Defining the survey - - Generating a mesh based on survey geometry - - Including surface topography - - Defining the inverse problem (data misfit, regularization, directives) - - Applying sensitivity weighting - - Plotting the recovered model and data misfit + This tutorial has been moved to `User Tutorials + `_. + Checkout the `Iteratively Re-weighted Least-Squares Inversion + `_ + section in the + `2.5D DC Resistivity Inversion + `_ tutorial. -""" - -######################################################################### -# Import modules -# -------------- -# - -import os -import numpy as np -import matplotlib as mpl -import matplotlib.pyplot as plt -from matplotlib.colors import LogNorm -import tarfile - -from discretize import TreeMesh -from discretize.utils import mkvc, active_from_xyz - -from simpeg.utils import model_builder -from simpeg import ( - maps, - data_misfit, - regularization, - optimization, - inverse_problem, - inversion, - directives, - utils, -) -from simpeg.electromagnetics.static import resistivity as dc -from simpeg.electromagnetics.static.utils.static_utils import ( - plot_pseudosection, - apparent_resistivity_from_voltage, -) -from simpeg.utils.io_utils.io_utils_electromagnetics import read_dcip2d_ubc - -mpl.rcParams.update({"font.size": 16}) -# sphinx_gallery_thumbnail_number = 3 - - -############################################# -# Define File Names -# ----------------- -# -# Here we provide the file paths to assets we need to run the inversion. The -# path to the true model conductivity and chargeability models are also -# provided for comparison with the inversion results. These files are stored as a -# tar-file on our google cloud bucket: -# "https://storage.googleapis.com/simpeg/doc-assets/dcr2d.tar.gz" -# - -# storage bucket where we have the data -data_source = "https://storage.googleapis.com/simpeg/doc-assets/dcr2d.tar.gz" - -# download the data -downloaded_data = utils.download(data_source, overwrite=True) - -# unzip the tarfile -tar = tarfile.open(downloaded_data, "r") -tar.extractall() -tar.close() - -# path to the directory containing our data -dir_path = downloaded_data.split(".")[0] + os.path.sep - -# files to work with -topo_filename = dir_path + "topo_xyz.txt" -data_filename = dir_path + "dc_data.obs" - - -############################################# -# Load Data, Define Survey and Plot -# --------------------------------- -# -# Here we load the observed data, define the DC and IP survey geometry and -# plot the data values using pseudo-sections. -# **Warning**: In the following example, the observations file is assumed to be -# sorted by sources -# - -# Load data -topo_xyz = np.loadtxt(str(topo_filename)) -dc_data = read_dcip2d_ubc(data_filename, "volt", "general") - -####################################################################### -# Plot Observed Data in Pseudo-Section -# ------------------------------------ -# -# Here, we demonstrate how to plot 2D data in pseudo-section. -# First, we plot the actual data (voltages) in pseudo-section as a scatter plot. -# This allows us to visualize the pseudo-sensitivity locations for our survey. -# Next, we plot the data as apparent conductivities in pseudo-section with a filled -# contour plot. -# - -# Plot voltages pseudo-section -fig = plt.figure(figsize=(12, 5)) -ax1 = fig.add_axes([0.1, 0.15, 0.75, 0.78]) -plot_pseudosection( - dc_data, - plot_type="scatter", - ax=ax1, - scale="log", - cbar_label="V/A", - scatter_opts={"cmap": mpl.cm.viridis}, -) -ax1.set_title("Normalized Voltages") -plt.show() - -# Get apparent conductivities from volts and survey geometry -apparent_conductivities = 1 / apparent_resistivity_from_voltage( - dc_data.survey, dc_data.dobs -) - -# Plot apparent conductivity pseudo-section -fig = plt.figure(figsize=(12, 5)) -ax1 = fig.add_axes([0.1, 0.15, 0.75, 0.78]) -plot_pseudosection( - dc_data.survey, - apparent_conductivities, - plot_type="contourf", - ax=ax1, - scale="log", - cbar_label="S/m", - mask_topography=True, - contourf_opts={"levels": 20, "cmap": mpl.cm.viridis}, -) -ax1.set_title("Apparent Conductivity") -plt.show() - -#################################################### -# Assign Uncertainties -# -------------------- -# -# Inversion with SimPEG requires that we define the uncertainties on our data. -# This represents our estimate of the standard deviation of the -# noise in our data. For DC data, the uncertainties are 10% of the absolute value. -# -# - -dc_data.standard_deviation = 0.05 * np.abs(dc_data.dobs) - -######################################################## -# Create Tree Mesh -# ------------------ -# -# Here, we create the Tree mesh that will be used invert the DC data -# - -dh = 4 # base cell width -dom_width_x = 3200.0 # domain width x -dom_width_z = 2400.0 # domain width z -nbcx = 2 ** int(np.round(np.log(dom_width_x / dh) / np.log(2.0))) # num. base cells x -nbcz = 2 ** int(np.round(np.log(dom_width_z / dh) / np.log(2.0))) # num. base cells z - -# Define the base mesh -hx = [(dh, nbcx)] -hz = [(dh, nbcz)] -mesh = TreeMesh([hx, hz], x0="CN") - -# Mesh refinement based on topography -mesh.refine_surface( - topo_xyz[:, [0, 2]], - padding_cells_by_level=[0, 0, 4, 4], - finalize=False, -) - -# Mesh refinement near transmitters and receivers. First we need to obtain the -# set of unique electrode locations. -electrode_locations = np.c_[ - dc_data.survey.locations_a, - dc_data.survey.locations_b, - dc_data.survey.locations_m, - dc_data.survey.locations_n, -] - -unique_locations = np.unique( - np.reshape(electrode_locations, (4 * dc_data.survey.nD, 2)), axis=0 -) - -mesh.refine_points(unique_locations, padding_cells_by_level=[4, 4], finalize=False) - -# Refine core mesh region -xp, zp = np.meshgrid([-600.0, 600.0], [-400.0, 0.0]) -xyz = np.c_[mkvc(xp), mkvc(zp)] -mesh.refine_bounding_box(xyz, padding_cells_by_level=[0, 0, 2, 8], finalize=False) - -mesh.finalize() - - -############################################################### -# Project Surveys to Discretized Topography -# ----------------------------------------- -# -# It is important that electrodes are not model as being in the air. Even if the -# electrodes are properly located along surface topography, they may lie above -# the discretized topography. This step is carried out to ensure all electrodes -# like on the discretized surface. -# - -# Create 2D topography. Since our 3D topography only changes in the x direction, -# it is easy to define the 2D topography projected along the survey line. For -# arbitrary topography and for an arbitrary survey orientation, the user must -# define the 2D topography along the survey line. -topo_2d = np.unique(topo_xyz[:, [0, 2]], axis=0) - -# Find cells that lie below surface topography -ind_active = active_from_xyz(mesh, topo_2d) - -# Extract survey from data object -survey = dc_data.survey - -# Shift electrodes to the surface of discretized topography -survey.drape_electrodes_on_topography(mesh, ind_active, option="top") - -# Reset survey in data object -dc_data.survey = survey - - -######################################################## -# Starting/Reference Model and Mapping on Tree Mesh -# --------------------------------------------------- -# -# Here, we would create starting and/or reference models for the DC inversion as -# well as the mapping from the model space to the active cells. Starting and -# reference models can be a constant background value or contain a-priori -# structures. Here, the starting model is the natural log of 0.01 S/m. -# - -# Define conductivity model in S/m (or resistivity model in Ohm m) -air_conductivity = np.log(1e-8) -background_conductivity = np.log(1e-2) - -active_map = maps.InjectActiveCells(mesh, ind_active, np.exp(air_conductivity)) -nC = int(ind_active.sum()) -conductivity_map = active_map * maps.ExpMap() - -# Define model -starting_conductivity_model = background_conductivity * np.ones(nC) - -############################################## -# Define the Physics of the DC Simulation -# --------------------------------------- -# -# Here, we define the physics of the DC resistivity problem. -# - -# Define the problem. Define the cells below topography and the mapping -simulation = dc.simulation_2d.Simulation2DNodal( - mesh, survey=survey, sigmaMap=conductivity_map, storeJ=True -) - -####################################################################### -# Define DC Inverse Problem -# ------------------------- -# -# The inverse problem is defined by 3 things: -# -# 1) Data Misfit: a measure of how well our recovered model explains the field data -# 2) Regularization: constraints placed on the recovered model and a priori information -# 3) Optimization: the numerical approach used to solve the inverse problem -# -# - -# Define the data misfit. Here the data misfit is the L2 norm of the weighted -# residual between the observed data and the data predicted for a given model. -# Within the data misfit, the residual between predicted and observed data are -# normalized by the data's standard deviation. -dmis = data_misfit.L2DataMisfit(data=dc_data, simulation=simulation) - -# Define the regularization (model objective function). Here, 'p' defines the -# the norm of the smallness term, 'qx' defines the norm of the smoothness -# in x and 'qz' defines the norm of the smoothness in z. -regmap = maps.IdentityMap(nP=int(ind_active.sum())) - -reg = regularization.Sparse( - mesh, - active_cells=ind_active, - reference_model=starting_conductivity_model, - mapping=regmap, - gradient_type="total", - alpha_s=0.01, - alpha_x=1, - alpha_y=1, -) - -reg.reference_model_in_smooth = True # Include reference model in smoothness - -p = 0 -qx = 1 -qz = 1 -reg.norms = [p, qx, qz] - -# Define how the optimization problem is solved. Here we will use an inexact -# Gauss-Newton approach. -opt = optimization.InexactGaussNewton(maxIter=40) - -# Here we define the inverse problem that is to be solved -inv_prob = inverse_problem.BaseInvProblem(dmis, reg, opt) - -####################################################################### -# Define DC Inversion Directives -# ------------------------------ -# -# Here we define any directives that are carried out during the inversion. This -# includes the cooling schedule for the trade-off parameter (beta), stopping -# criteria for the inversion and saving inversion results at each iteration. -# - -# Apply and update sensitivity weighting as the model updates -update_sensitivity_weighting = directives.UpdateSensitivityWeights() - -# Reach target misfit for L2 solution, then use IRLS until model stops changing. -update_IRLS = directives.UpdateIRLS(max_irls_iterations=25, chifact_start=1.0) - -# Defining a starting value for the trade-off parameter (beta) between the data -# misfit and the regularization. -starting_beta = directives.BetaEstimate_ByEig(beta0_ratio=1e1) - -# Options for outputting recovered models and predicted data for each beta. -save_iteration = directives.SaveOutputEveryIteration(save_txt=False) - -# Update preconditioner -update_jacobi = directives.UpdatePreconditioner() - -directives_list = [ - update_sensitivity_weighting, - update_IRLS, - starting_beta, - save_iteration, - update_jacobi, -] - -##################################################################### -# Running the DC Inversion -# ------------------------ -# -# To define the inversion object, we need to define the inversion problem and -# the set of directives. We can then run the inversion. -# - -# Here we combine the inverse problem and the set of directives -dc_inversion = inversion.BaseInversion(inv_prob, directiveList=directives_list) - -# Run inversion -recovered_conductivity_model = dc_inversion.run(starting_conductivity_model) - -############################################################ -# Recreate True Conductivity Model -# -------------------------------- -# - -true_background_conductivity = 1e-2 -true_conductor_conductivity = 1e-1 -true_resistor_conductivity = 1e-3 - -true_conductivity_model = true_background_conductivity * np.ones(len(mesh)) - -ind_conductor = model_builder.get_indices_sphere( - np.r_[-120.0, -180.0], 60.0, mesh.gridCC -) -true_conductivity_model[ind_conductor] = true_conductor_conductivity - -ind_resistor = model_builder.get_indices_sphere(np.r_[120.0, -180.0], 60.0, mesh.gridCC) -true_conductivity_model[ind_resistor] = true_resistor_conductivity - -true_conductivity_model[~ind_active] = np.nan - -############################################################ -# Plotting True and Recovered Conductivity Model -# ---------------------------------------------- -# - -# Get L2 and sparse recovered model in base 10 -l2_conductivity = conductivity_map * inv_prob.l2model -l2_conductivity[~ind_active] = np.nan - -recovered_conductivity = conductivity_map * recovered_conductivity_model -recovered_conductivity[~ind_active] = np.nan - -# Plot True Model -norm = LogNorm(vmin=1e-3, vmax=1e-1) - -fig = plt.figure(figsize=(9, 15)) -ax1 = 3 * [None] -ax2 = 3 * [None] -title_str = [ - "True Conductivity Model", - "Smooth Recovered Model", - "Sparse Recovered Model", -] -plotting_model = [ - true_conductivity_model, - l2_conductivity, - recovered_conductivity, -] - -for ii in range(0, 3): - ax1[ii] = fig.add_axes([0.14, 0.75 - 0.3 * ii, 0.68, 0.2]) - mesh.plot_image( - plotting_model[ii], - ax=ax1[ii], - grid=False, - range_x=[-700, 700], - range_y=[-600, 0], - pcolor_opts={"norm": norm}, - ) - ax1[ii].set_xlim(-600, 600) - ax1[ii].set_ylim(-600, 0) - ax1[ii].set_title(title_str[ii]) - ax1[ii].set_xlabel("x (m)") - ax1[ii].set_ylabel("z (m)") - - ax2[ii] = fig.add_axes([0.84, 0.75 - 0.3 * ii, 0.03, 0.2]) - cbar = mpl.colorbar.ColorbarBase(ax2[ii], norm=norm, orientation="vertical") - cbar.set_label(r"$\sigma$ (S/m)", rotation=270, labelpad=15, size=12) - -plt.show() - -################################################################### -# Plotting Predicted DC Data and Misfit -# ------------------------------------- -# - -# Predicted data from recovered model -dpred = inv_prob.dpred -dobs = dc_data.dobs -std = dc_data.standard_deviation - -# Plot -fig = plt.figure(figsize=(9, 13)) -data_array = [np.abs(dobs), np.abs(dpred), (dobs - dpred) / std] -plot_title = ["Observed Voltage", "Predicted Voltage", "Normalized Misfit"] -plot_units = ["V/A", "V/A", ""] -scale = ["log", "log", "linear"] - -ax1 = 3 * [None] -cax1 = 3 * [None] -cbar = 3 * [None] -cplot = 3 * [None] - -for ii in range(0, 3): - ax1[ii] = fig.add_axes([0.15, 0.72 - 0.33 * ii, 0.65, 0.21]) - cax1[ii] = fig.add_axes([0.81, 0.72 - 0.33 * ii, 0.03, 0.21]) - cplot[ii] = plot_pseudosection( - survey, - data_array[ii], - "contourf", - ax=ax1[ii], - cax=cax1[ii], - scale=scale[ii], - cbar_label=plot_units[ii], - mask_topography=True, - contourf_opts={"levels": 25, "cmap": mpl.cm.viridis}, - ) - ax1[ii].set_title(plot_title[ii]) - -plt.show() +""" diff --git a/tutorials/05-dcr/plot_inv_3_dcr3d.py b/tutorials/05-dcr/plot_inv_3_dcr3d.py index 60675957c9..fb156a3d32 100644 --- a/tutorials/05-dcr/plot_inv_3_dcr3d.py +++ b/tutorials/05-dcr/plot_inv_3_dcr3d.py @@ -3,504 +3,13 @@ 3D Least-Squares Inversion of DC Resistivity Data ================================================= -Here we invert 5 lines of DC data to recover an electrical -conductivity model. We formulate the corresponding -inverse problem as a least-squares optimization problem. -For this tutorial, we focus on the following: +.. important:: - - Generating a mesh based on survey geometry - - Including surface topography - - Defining the inverse problem (data misfit, regularization, directives) - - Applying sensitivity weighting - - Plotting the recovered model and data misfit + This tutorial has been moved to `User Tutorials + `_. -The DC data are measured voltages normalized by the source current in V/A. + Checkout the `3D DC Resistivity Inversion + `_ tutorial. """ - -################################################################# -# Import Modules -# -------------- -# - - -import os -import numpy as np -import matplotlib as mpl -import matplotlib.pyplot as plt -import tarfile - -from discretize import TreeMesh -from discretize.utils import refine_tree_xyz, active_from_xyz - -from simpeg.utils import model_builder -from simpeg.utils.io_utils.io_utils_electromagnetics import read_dcip_xyz -from simpeg import ( - maps, - data_misfit, - regularization, - optimization, - inverse_problem, - inversion, - directives, - utils, -) -from simpeg.electromagnetics.static import resistivity as dc -from simpeg.electromagnetics.static.utils.static_utils import ( - apparent_resistivity_from_voltage, -) - -# To plot DC/IP data in 3D, the user must have the plotly package -try: - import plotly - from simpeg.electromagnetics.static.utils.static_utils import plot_3d_pseudosection - - has_plotly = True -except ImportError: - has_plotly = False - pass - - -mpl.rcParams.update({"font.size": 16}) - -# sphinx_gallery_thumbnail_number = 3 - -########################################################## -# Download Assets -# --------------- -# -# Here we provide the file paths to assets we need to run the inversion. The -# path to the true model conductivity and chargeability models are also -# provided for comparison with the inversion results. These files are stored as a -# tar-file on our google cloud bucket: -# "https://storage.googleapis.com/simpeg/doc-assets/dcr3d.tar.gz" -# -# -# - -# storage bucket where we have the data -data_source = "https://storage.googleapis.com/simpeg/doc-assets/dcr3d.tar.gz" - -# download the data -downloaded_data = utils.download(data_source, overwrite=True) - -# unzip the tarfile -tar = tarfile.open(downloaded_data, "r") -tar.extractall() -tar.close() - -# path to the directory containing our data -dir_path = downloaded_data.split(".")[0] + os.path.sep - -# files to work with -topo_filename = dir_path + "topo_xyz.txt" -dc_data_filename = dir_path + "dc_data.xyz" - -######################################################## -# Load Data and Topography -# ------------------------ -# -# Here we load the observed data and topography. -# -# - -topo_xyz = np.loadtxt(str(topo_filename)) - -dc_data = read_dcip_xyz( - dc_data_filename, - "volt", - data_header="V/A", - uncertainties_header="UNCERT", - is_surface_data=False, -) - - -########################################################## -# Plot Observed Data in Pseudosection -# ----------------------------------- -# -# Here we plot the observed DC and IP data in 3D pseudosections. -# To use this utility, you must have Python's *plotly* package. -# Here, we represent the DC data as apparent conductivities -# and the IP data as apparent chargeabilities. -# - -# Convert predicted data to apparent conductivities -apparent_conductivity = 1 / apparent_resistivity_from_voltage( - dc_data.survey, - dc_data.dobs, -) - -if has_plotly: - fig = plot_3d_pseudosection( - dc_data.survey, - apparent_conductivity, - scale="log", - units="S/m", - plane_distance=15, - ) - - fig.update_layout( - title_text="Apparent Conductivity", - title_x=0.5, - title_font_size=24, - width=650, - height=500, - scene_camera=dict( - center=dict(x=0, y=0, z=-0.4), eye=dict(x=1.5, y=-1.5, z=1.8) - ), - ) - - plotly.io.show(fig) - -else: - print("INSTALL 'PLOTLY' TO VISUALIZE 3D PSEUDOSECTIONS") - - -#################################################### -# Assign Uncertainties -# -------------------- -# -# Inversion with SimPEG requires that we define the uncertainties on our data. -# This represents our estimate of the standard deviation of the -# noise in our data. For DC data, the uncertainties are 10% of the absolute value. -# -# - -dc_data.standard_deviation = 0.1 * np.abs(dc_data.dobs) - - -################################################################ -# Create Tree Mesh -# ---------------- -# -# Here, we create the Tree mesh that will be used to invert -# DC data. -# - - -dh = 25.0 # base cell width -dom_width_x = 6000.0 # domain width x -dom_width_y = 6000.0 # domain width y -dom_width_z = 4000.0 # domain width z -nbcx = 2 ** int(np.round(np.log(dom_width_x / dh) / np.log(2.0))) # num. base cells x -nbcy = 2 ** int(np.round(np.log(dom_width_y / dh) / np.log(2.0))) # num. base cells y -nbcz = 2 ** int(np.round(np.log(dom_width_z / dh) / np.log(2.0))) # num. base cells z - -# Define the base mesh -hx = [(dh, nbcx)] -hy = [(dh, nbcy)] -hz = [(dh, nbcz)] -mesh = TreeMesh([hx, hy, hz], x0="CCN") - -# Mesh refinement based on topography -k = np.sqrt(np.sum(topo_xyz[:, 0:2] ** 2, axis=1)) < 1200 -mesh = refine_tree_xyz( - mesh, topo_xyz[k, :], octree_levels=[0, 6, 8], method="surface", finalize=False -) - -# Mesh refinement near sources and receivers. -electrode_locations = np.r_[ - dc_data.survey.locations_a, - dc_data.survey.locations_b, - dc_data.survey.locations_m, - dc_data.survey.locations_n, -] -unique_locations = np.unique(electrode_locations, axis=0) -mesh = refine_tree_xyz( - mesh, unique_locations, octree_levels=[4, 6, 4], method="radial", finalize=False -) - -# Finalize the mesh -mesh.finalize() - -####################################################### -# Project Electrodes to Discretized Topography -# -------------------------------------------- -# -# It is important that electrodes are not modeled as being in the air. Even if the -# electrodes are properly located along surface topography, they may lie above -# the discretized topography. This step is carried out to ensure all electrodes -# lie on the discretized surface. -# - -# Find cells that lie below surface topography -ind_active = active_from_xyz(mesh, topo_xyz) - -# Extract survey from data object -dc_survey = dc_data.survey - -# Shift electrodes to the surface of discretized topography -dc_survey.drape_electrodes_on_topography(mesh, ind_active, option="top") - -# Reset survey in data object -dc_data.survey = dc_survey - -################################################################# -# Starting/Reference Model and Mapping on OcTree Mesh -# --------------------------------------------------- -# -# Here, we create starting and/or reference models for the DC inversion as -# well as the mapping from the model space to the active cells. Starting and -# reference models can be a constant background value or contain a-priori -# structures. Here, the starting model is the natural log of 0.01 S/m. -# -# - -# Define conductivity model in S/m (or resistivity model in Ohm m) -air_conductivity = np.log(1e-8) -background_conductivity = np.log(1e-2) - -# Define the mapping from active cells to the entire domain -active_map = maps.InjectActiveCells(mesh, ind_active, np.exp(air_conductivity)) -nC = int(ind_active.sum()) - -# Define the mapping from the model to the conductivity of the entire domain -conductivity_map = active_map * maps.ExpMap() - -# Define starting model -starting_conductivity_model = background_conductivity * np.ones(nC) - -############################################################### -# Define the Physics of the DC Simulation -# --------------------------------------- -# -# Here, we define the physics of the DC resistivity simulation. -# -# - -dc_simulation = dc.simulation.Simulation3DNodal( - mesh, survey=dc_survey, sigmaMap=conductivity_map, storeJ=True -) - -################################################################# -# Define DC Inverse Problem -# ------------------------- -# -# The inverse problem is defined by 3 things: -# -# 1) Data Misfit: a measure of how well our recovered model explains the field data -# 2) Regularization: constraints placed on the recovered model and a priori information -# 3) Optimization: the numerical approach used to solve the inverse problem -# -# - - -# Define the data misfit. Here the data misfit is the L2 norm of the weighted -# residual between the observed data and the data predicted for a given model. -# Within the data misfit, the residual between predicted and observed data are -# normalized by the data's standard deviation. -dc_data_misfit = data_misfit.L2DataMisfit(data=dc_data, simulation=dc_simulation) - -# Define the regularization (model objective function) -dc_regularization = regularization.WeightedLeastSquares( - mesh, - active_cells=ind_active, - reference_model=starting_conductivity_model, -) - -dc_regularization.reference_model_in_smooth = ( - True # Include reference model in smoothness -) - -# Define how the optimization problem is solved. -dc_optimization = optimization.InexactGaussNewton( - maxIter=15, maxIterLS=20, maxIterCG=30, tolCG=1e-2 -) - -# Here we define the inverse problem that is to be solved -dc_inverse_problem = inverse_problem.BaseInvProblem( - dc_data_misfit, dc_regularization, dc_optimization -) - -################################################# -# Define DC Inversion Directives -# ------------------------------ -# -# Here we define any directives that are carried out during the inversion. This -# includes the cooling schedule for the trade-off parameter (beta), stopping -# criteria for the inversion and saving inversion results at each iteration. -# -# - -# Apply and update sensitivity weighting as the model updates -update_sensitivity_weighting = directives.UpdateSensitivityWeights() - -# Defining a starting value for the trade-off parameter (beta) between the data -# misfit and the regularization. -starting_beta = directives.BetaEstimate_ByEig(beta0_ratio=1e1) - -# Set the rate of reduction in trade-off parameter (beta) each time the -# the inverse problem is solved. And set the number of Gauss-Newton iterations -# for each trade-off paramter value. -beta_schedule = directives.BetaSchedule(coolingFactor=2.5, coolingRate=2) - -# Options for outputting recovered models and predicted data for each beta. -save_iteration = directives.SaveOutputEveryIteration(save_txt=False) - -# Setting a stopping criteria for the inversion. -target_misfit = directives.TargetMisfit(chifact=1) - -# Apply and update preconditioner as the model updates -update_jacobi = directives.UpdatePreconditioner() - -directives_list = [ - update_sensitivity_weighting, - starting_beta, - beta_schedule, - save_iteration, - target_misfit, - update_jacobi, -] - -######################################################### -# Running the DC Inversion -# ------------------------ -# -# To define the inversion object, we need to define the inversion problem and -# the set of directives. We can then run the inversion. -# - -# Here we combine the inverse problem and the set of directives -dc_inversion = inversion.BaseInversion( - dc_inverse_problem, directiveList=directives_list -) - -# Run inversion -recovered_conductivity_model = dc_inversion.run(starting_conductivity_model) - - -############################################################### -# Recreate True Conductivity Model -# -------------------------------- -# - -# Define conductivity model in S/m (or resistivity model in Ohm m) -background_value = 1e-2 -conductor_value = 1e-1 -resistor_value = 1e-3 - -# Define model -true_conductivity_model = background_value * np.ones(nC) - -ind_conductor = model_builder.get_indices_sphere( - np.r_[-350.0, 0.0, -300.0], 160.0, mesh.cell_centers[ind_active, :] -) -true_conductivity_model[ind_conductor] = conductor_value - -ind_resistor = model_builder.get_indices_sphere( - np.r_[350.0, 0.0, -300.0], 160.0, mesh.cell_centers[ind_active, :] -) -true_conductivity_model[ind_resistor] = resistor_value -true_conductivity_model_log10 = np.log10(true_conductivity_model) - - -############################################################### -# Plotting True and Recovered Conductivity Model -# ---------------------------------------------- -# - - -# Plot True Model -fig = plt.figure(figsize=(10, 4)) - -plotting_map = maps.InjectActiveCells(mesh, ind_active, np.nan) - -ax1 = fig.add_axes([0.15, 0.15, 0.67, 0.75]) -mesh.plot_slice( - plotting_map * true_conductivity_model_log10, - ax=ax1, - normal="Y", - ind=int(len(mesh.h[1]) / 2), - grid=False, - clim=(true_conductivity_model_log10.min(), true_conductivity_model_log10.max()), - pcolor_opts={"cmap": mpl.cm.viridis}, -) -ax1.set_title("True Conductivity Model") -ax1.set_xlabel("x (m)") -ax1.set_ylabel("z (m)") -ax1.set_xlim([-1000, 1000]) -ax1.set_ylim([-1000, 0]) - -ax2 = fig.add_axes([0.84, 0.15, 0.03, 0.75]) -norm = mpl.colors.Normalize( - vmin=true_conductivity_model_log10.min(), vmax=true_conductivity_model_log10.max() -) -cbar = mpl.colorbar.ColorbarBase( - ax2, cmap=mpl.cm.viridis, norm=norm, orientation="vertical", format="$10^{%.1f}$" -) -cbar.set_label("Conductivity [S/m]", rotation=270, labelpad=15, size=12) - -# Plot recovered model -recovered_conductivity_model_log10 = np.log10(np.exp(recovered_conductivity_model)) - -fig = plt.figure(figsize=(10, 4)) - -ax1 = fig.add_axes([0.15, 0.15, 0.67, 0.75]) -mesh.plot_slice( - plotting_map * recovered_conductivity_model_log10, - ax=ax1, - normal="Y", - ind=int(len(mesh.h[1]) / 2), - grid=False, - clim=(true_conductivity_model_log10.min(), true_conductivity_model_log10.max()), - pcolor_opts={"cmap": mpl.cm.viridis}, -) -ax1.set_title("Recovered Conductivity Model") -ax1.set_xlabel("x (m)") -ax1.set_ylabel("z (m)") -ax1.set_xlim([-1000, 1000]) -ax1.set_ylim([-1000, 0]) - -ax2 = fig.add_axes([0.84, 0.15, 0.03, 0.75]) -norm = mpl.colors.Normalize( - vmin=true_conductivity_model_log10.min(), vmax=true_conductivity_model_log10.max() -) -cbar = mpl.colorbar.ColorbarBase( - ax2, cmap=mpl.cm.viridis, norm=norm, orientation="vertical", format="$10^{%.1f}$" -) -cbar.set_label("Conductivity [S/m]", rotation=270, labelpad=15, size=12) -plt.show() - -####################################################################### -# Plotting Normalized Data Misfit or Predicted DC Data -# ---------------------------------------------------- -# -# To see how well the recovered model reproduces the observed data, -# it is a good idea to compare the predicted and observed data. -# Here, we accomplish this by plotting the normalized misfit. -# - -# Predicted data from recovered model -dpred_dc = dc_inverse_problem.dpred - -# Compute the normalized data misfit -dc_normalized_misfit = (dc_data.dobs - dpred_dc) / dc_data.standard_deviation - -if has_plotly: - # Plot IP Data - fig = plot_3d_pseudosection( - dc_data.survey, - dc_normalized_misfit, - scale="linear", - units="", - vlim=[-2, 2], - plane_distance=15, - ) - - fig.update_layout( - title_text="Normalized Data Misfit", - title_x=0.5, - title_font_size=24, - width=650, - height=500, - scene_camera=dict( - center=dict(x=0, y=0, z=-0.4), eye=dict(x=1.5, y=-1.5, z=1.8) - ), - ) - - plotly.io.show(fig) - -else: - print("INSTALL 'PLOTLY' TO VISUALIZE 3D PSEUDOSECTIONS") From ed40b813dbf2c0730dcbc1222e3bd90eb7ef4fd6 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Tue, 14 Oct 2025 18:48:34 +0000 Subject: [PATCH 177/194] Improve dipole source tests (#1711) Improve the assertions in dipole source tests for EM. Compute the root mean square of the difference between the numerical and analytic fields in FDEM, and check it's below a certain tolerance. Use `np.testing.assert_allclose` in TDEM tests to compare the predicted data for both the B and H formulations. --- tests/em/fdem/forward/test_FDEM_dipolar_sources.py | 8 ++++++-- tests/em/tdem/test_TDEM_dipolar_sources.py | 14 +++----------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/tests/em/fdem/forward/test_FDEM_dipolar_sources.py b/tests/em/fdem/forward/test_FDEM_dipolar_sources.py index f06843cf89..fe3fe48bbb 100644 --- a/tests/em/fdem/forward/test_FDEM_dipolar_sources.py +++ b/tests/em/fdem/forward/test_FDEM_dipolar_sources.py @@ -11,7 +11,7 @@ Solver = get_default_solver() -TOL = 5e-2 # relative tolerance +TOL = 2e-2 # relative tolerance # Defining transmitter locations source_location = np.r_[0, 0, 0] @@ -104,4 +104,8 @@ def test_dipolar_fields(simulation_type, field_test, mur, orientation="Z"): elif field_test == "hPrimary": analytic = projection(dipole.magnetic_field(grid)) - assert np.abs(np.mean((numeric / analytic)) - 1) < TOL + # Check that the rms is below a tolerance + diff = analytic - numeric + rms = np.sqrt(np.mean(diff**2)) + maxabs = np.max(np.abs(analytic)) + assert rms < maxabs * TOL diff --git a/tests/em/tdem/test_TDEM_dipolar_sources.py b/tests/em/tdem/test_TDEM_dipolar_sources.py index 58dfd00981..9f6bad07f1 100644 --- a/tests/em/tdem/test_TDEM_dipolar_sources.py +++ b/tests/em/tdem/test_TDEM_dipolar_sources.py @@ -9,7 +9,7 @@ Solver = get_default_solver() -TOL = 1e-2 # relative tolerance +TOL = 0.06 # relative tolerance # Observation times for response (time channels) n_times = 30 @@ -98,13 +98,5 @@ def test_BH_dipole(): fields_h = simulation_h.fields(model) dpred_h = simulation_h.dpred(model, f=fields_h) - assert ( - np.abs( - np.mean(dpred_b[: len(time_channels)] / dpred_h[: len(time_channels)]) - 1 - ) - < TOL - ) - assert np, ( - abs(np.mean(dpred_b[len(time_channels) :] / dpred_h[len(time_channels) :]) - 1) - < TOL - ) + # Check if the two predicted fields are close enough + np.testing.assert_allclose(dpred_h, dpred_b, rtol=TOL) From 23a91db1be469547c75bb8040e719686c60fb371 Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Tue, 14 Oct 2025 17:57:11 -0600 Subject: [PATCH 178/194] Update deprecated calls in examples, tutorials, and tests to inexact CG minimizers (#1703) #### Summary Updates the minimizer constructors used in exampled, tutorials, and tests to no longer use the deprecated arguments related to conjugate gradient minimizers. #### What does this implement/fix? #### Additional information After passing build, we'll want to check how well the examples & tutorials that use `ProjectedGNCG` match previous results. As these now use a relative tolerance on CG vs. an absolute tolerance. I suspect they will not change much as most of them were terminated by the `cg_maxiter` condition previously. --------- Co-authored-by: Santiago Soler --- examples/01-maps/plot_sumMap.py | 4 ++-- examples/02-gravity/plot_inv_grav_tiled.py | 2 +- .../03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py | 8 ++++---- .../03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py | 2 +- .../03-magnetics/plot_inv_mag_nonLinear_Amplitude.py | 4 ++-- .../05-fdem/plot_inv_fdem_loop_loop_2Dinversion.py | 2 +- examples/08-vrm/plot_inv_vrm_eq.py | 2 +- examples/09-flow/plot_inv_flow_richards_1D.py | 2 +- examples/10-pgi/plot_inv_0_PGI_Linear_1D.py | 4 ++-- ...ot_inv_1_PGI_Linear_1D_joint_WithRelationships.py | 12 ++++++------ .../plot_heagyetal2017_cyl_inversions.py | 4 ++-- .../20-published/plot_laguna_del_maule_inversion.py | 4 ++-- examples/_archived/plot_inv_grav_linear.py | 2 +- examples/_archived/plot_inv_mag_linear.py | 2 +- tests/base/test_directives.py | 2 +- tests/base/test_joint.py | 2 +- tests/dask/test_DC_jvecjtvecadj_dask.py | 4 ++-- tests/dask/test_IP_jvecjtvecadj_dask.py | 10 +++++----- tests/dask/test_grav_inversion_linear.py | 7 ++++++- tests/dask/test_mag_MVI_Octree.py | 8 ++++---- tests/dask/test_mag_inversion_linear_Octree.py | 6 +++--- tests/dask/test_mag_nonLinear_Amplitude.py | 6 +++--- tests/em/static/test_DC_2D_jvecjtvecadj.py | 2 +- tests/em/static/test_DC_jvecjtvecadj.py | 12 ++++++------ tests/em/static/test_IP_2D_jvecjtvecadj.py | 4 ++-- tests/em/static/test_IP_jvecjtvecadj.py | 8 ++++---- tests/em/static/test_SIP_2D_jvecjtvecadj.py | 6 +++--- tests/em/static/test_SIP_jvecjtvecadj.py | 6 +++--- tests/em/vrm/test_vrminv.py | 2 +- tests/pf/test_equivalent_sources.py | 4 ++-- tests/pf/test_grav_inversion_linear.py | 2 +- tests/pf/test_mag_MVI_Octree.py | 8 ++++---- tests/pf/test_mag_inversion_linear.py | 2 +- tests/pf/test_mag_inversion_linear_Octree.py | 6 +++--- tests/pf/test_mag_nonLinear_Amplitude.py | 6 +++--- tests/pf/test_mag_vector_amplitude.py | 2 +- tests/pf/test_pf_quadtree_inversion_linear.py | 4 ++-- .../02-linear_inversion/plot_inv_2_inversion_irls.py | 2 +- tutorials/06-ip/plot_inv_2_dcip2d.py | 2 +- tutorials/06-ip/plot_inv_3_dcip3d.py | 6 ++++-- tutorials/07-fdem/plot_inv_1_em1dfm.py | 2 +- tutorials/08-tdem/plot_inv_1_em1dtm.py | 2 +- tutorials/12-seismic/plot_inv_1_tomography_2D.py | 2 +- .../plot_inv_3_cross_gradient_pf.py | 4 ++-- .../plot_inv_1_joint_pf_pgi_full_info_tutorial.py | 4 ++-- .../plot_inv_2_joint_pf_pgi_no_info_tutorial.py | 4 ++-- tutorials/_temporary/plot_4c_fdem3d_inversion.py | 4 ++-- .../_temporary/plot_inv_1_em1dtm_stitched_skytem.py | 2 +- 48 files changed, 107 insertions(+), 100 deletions(-) diff --git a/examples/01-maps/plot_sumMap.py b/examples/01-maps/plot_sumMap.py index fd479a9f6d..369d2ab868 100644 --- a/examples/01-maps/plot_sumMap.py +++ b/examples/01-maps/plot_sumMap.py @@ -164,8 +164,8 @@ def run(plotIt=True): lower=0.0, upper=1.0, maxIterLS=20, - maxIterCG=10, - tolCG=1e-3, + cg_maxiter=10, + cg_rtol=1e-3, tolG=1e-3, eps=1e-6, ) diff --git a/examples/02-gravity/plot_inv_grav_tiled.py b/examples/02-gravity/plot_inv_grav_tiled.py index e4ba8823a8..816315ae80 100644 --- a/examples/02-gravity/plot_inv_grav_tiled.py +++ b/examples/02-gravity/plot_inv_grav_tiled.py @@ -228,7 +228,7 @@ # Add directives to the inversion opt = optimization.ProjectedGNCG( - maxIter=100, lower=-1.0, upper=1.0, maxIterLS=20, maxIterCG=10, tolCG=1e-3 + maxIter=100, lower=-1.0, upper=1.0, maxIterLS=20, cg_maxiter=10, cg_rtol=1e-3 ) invProb = inverse_problem.BaseInvProblem(global_misfit, reg, opt) betaest = directives.BetaEstimate_ByEig(beta0_ratio=1e-1) diff --git a/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py b/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py index 9987eceb44..7f465146bf 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py @@ -243,7 +243,7 @@ # Add directives to the inversion opt = optimization.ProjectedGNCG( - maxIter=10, lower=-10, upper=10.0, maxIterLS=20, maxIterCG=20, tolCG=1e-4 + maxIter=10, lower=-10, upper=10.0, maxIterLS=20, cg_maxiter=20, cg_rtol=1e-3 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) @@ -334,9 +334,9 @@ lower=lower_bound, upper=upper_bound, maxIterLS=20, - maxIterCG=30, - tolCG=1e-3, - stepOffBoundsFact=1e-3, + cg_maxiter=30, + cg_rtol=1e-3, + active_set_grad_scale=1e-3, ) opt.approxHinv = None diff --git a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py index 993acee2e8..c2e76d5e60 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py @@ -170,7 +170,7 @@ # The optimization scheme opt = optimization.ProjectedGNCG( - maxIter=20, lower=-10, upper=10.0, maxIterLS=20, maxIterCG=20, tolCG=1e-4 + maxIter=20, lower=-10, upper=10.0, maxIterLS=20, cg_maxiter=20, cg_rtol=1e-3 ) # The inverse problem diff --git a/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py b/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py index b205152c1d..263697df88 100644 --- a/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py +++ b/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py @@ -243,7 +243,7 @@ # Specify how the optimization will proceed, set susceptibility bounds to inf opt = optimization.ProjectedGNCG( - maxIter=20, lower=-np.inf, upper=np.inf, maxIterLS=20, maxIterCG=20, tolCG=1e-3 + maxIter=20, lower=-np.inf, upper=np.inf, maxIterLS=20, cg_maxiter=20, cg_rtol=1e-3 ) # Define misfit function (obs-calc) @@ -367,7 +367,7 @@ # Add directives to the inversion opt = optimization.ProjectedGNCG( - maxIter=30, lower=0.0, upper=1.0, maxIterLS=20, maxIterCG=20, tolCG=1e-3 + maxIter=30, lower=0.0, upper=1.0, maxIterLS=20, cg_maxiter=20, cg_rtol=1e-3 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) diff --git a/examples/05-fdem/plot_inv_fdem_loop_loop_2Dinversion.py b/examples/05-fdem/plot_inv_fdem_loop_loop_2Dinversion.py index c3a2771b03..99bb11c07b 100644 --- a/examples/05-fdem/plot_inv_fdem_loop_loop_2Dinversion.py +++ b/examples/05-fdem/plot_inv_fdem_loop_loop_2Dinversion.py @@ -279,7 +279,7 @@ def plot_data(data, ax=None, color="C0", label=""): dmisfit = data_misfit.L2DataMisfit(simulation=prob, data=data) reg = regularization.WeightedLeastSquares(inversion_mesh) -opt = optimization.InexactGaussNewton(maxIterCG=10, remember="xc") +opt = optimization.InexactGaussNewton(cg_maxiter=10, remember="xc") invProb = inverse_problem.BaseInvProblem(dmisfit, reg, opt) betaest = directives.BetaEstimate_ByEig(beta0_ratio=0.05, n_pw_iter=1, random_seed=1) diff --git a/examples/08-vrm/plot_inv_vrm_eq.py b/examples/08-vrm/plot_inv_vrm_eq.py index 873a7b7188..60e9176edb 100644 --- a/examples/08-vrm/plot_inv_vrm_eq.py +++ b/examples/08-vrm/plot_inv_vrm_eq.py @@ -201,7 +201,7 @@ ) opt = optimization.ProjectedGNCG( - maxIter=20, lower=0.0, upper=1e-2, maxIterLS=20, tolCG=1e-4 + maxIter=20, lower=0.0, upper=1e-2, maxIterLS=20, cg_rtol=1e-4 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) directives = [ diff --git a/examples/09-flow/plot_inv_flow_richards_1D.py b/examples/09-flow/plot_inv_flow_richards_1D.py index 20940bcf05..9d83f53153 100644 --- a/examples/09-flow/plot_inv_flow_richards_1D.py +++ b/examples/09-flow/plot_inv_flow_richards_1D.py @@ -99,7 +99,7 @@ def run(plotIt=True): # Setup a pretty standard inversion reg = regularization.WeightedLeastSquares(M, alpha_s=1e-1) dmis = data_misfit.L2DataMisfit(simulation=prob, data=data) - opt = optimization.InexactGaussNewton(maxIter=20, maxIterCG=10) + opt = optimization.InexactGaussNewton(maxIter=20, cg_maxiter=10) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) beta = directives.BetaSchedule(coolingFactor=4) betaest = directives.BetaEstimate_ByEig(beta0_ratio=1e2) diff --git a/examples/10-pgi/plot_inv_0_PGI_Linear_1D.py b/examples/10-pgi/plot_inv_0_PGI_Linear_1D.py index 86ba4c2572..0f69e45d27 100644 --- a/examples/10-pgi/plot_inv_0_PGI_Linear_1D.py +++ b/examples/10-pgi/plot_inv_0_PGI_Linear_1D.py @@ -73,7 +73,7 @@ def g(k): # Setup the inverse problem reg = regularization.WeightedLeastSquares(mesh, alpha_s=1.0, alpha_x=1.0) dmis = data_misfit.L2DataMisfit(data=survey, simulation=prob) -opt = optimization.ProjectedGNCG(maxIter=10, maxIterCG=50, tolCG=1e-4) +opt = optimization.ProjectedGNCG(maxIter=10, cg_maxiter=50, cg_rtol=1e-3) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) directiveslist = [ directives.BetaEstimate_ByEig(beta0_ratio=1e-5), @@ -114,7 +114,7 @@ def g(k): ) # Optimization -opt = optimization.ProjectedGNCG(maxIter=20, maxIterCG=50, tolCG=1e-4) +opt = optimization.ProjectedGNCG(maxIter=20, cg_maxiter=50, cg_rtol=1e-3) opt.remember("xc") # Setup new inverse problem diff --git a/examples/10-pgi/plot_inv_1_PGI_Linear_1D_joint_WithRelationships.py b/examples/10-pgi/plot_inv_1_PGI_Linear_1D_joint_WithRelationships.py index 84b7fecfbc..b46d68f494 100644 --- a/examples/10-pgi/plot_inv_1_PGI_Linear_1D_joint_WithRelationships.py +++ b/examples/10-pgi/plot_inv_1_PGI_Linear_1D_joint_WithRelationships.py @@ -147,8 +147,8 @@ def g(k): opt = optimization.ProjectedGNCG( maxIter=50, tolX=1e-6, - maxIterCG=100, - tolCG=1e-3, + cg_maxiter=100, + cg_rtol=1e-3, lower=-10, upper=10, ) @@ -195,8 +195,8 @@ def g(k): opt = optimization.ProjectedGNCG( maxIter=50, tolX=1e-6, - maxIterCG=100, - tolCG=1e-3, + cg_maxiter=100, + cg_rtol=1e-3, lower=-10, upper=10, ) @@ -246,8 +246,8 @@ def g(k): opt = optimization.ProjectedGNCG( maxIter=50, tolX=1e-6, - maxIterCG=100, - tolCG=1e-3, + cg_maxiter=100, + cg_rtol=1e-3, lower=-10, upper=10, ) diff --git a/examples/20-published/plot_heagyetal2017_cyl_inversions.py b/examples/20-published/plot_heagyetal2017_cyl_inversions.py index 435a3b3f0c..8187ae3382 100644 --- a/examples/20-published/plot_heagyetal2017_cyl_inversions.py +++ b/examples/20-published/plot_heagyetal2017_cyl_inversions.py @@ -99,7 +99,7 @@ def run(plotIt=True, saveFig=False): dmisfit = data_misfit.L2DataMisfit(simulation=prbFD, data=dataFD) regMesh = discretize.TensorMesh([mesh.h[2][mapping.maps[-1].active_cells]]) reg = regularization.WeightedLeastSquares(regMesh) - opt = optimization.InexactGaussNewton(maxIterCG=10) + opt = optimization.InexactGaussNewton(cg_maxiter=10) invProb = inverse_problem.BaseInvProblem(dmisfit, reg, opt) # Inversion Directives @@ -145,7 +145,7 @@ def run(plotIt=True, saveFig=False): dmisfit = data_misfit.L2DataMisfit(simulation=prbTD, data=dataTD) regMesh = discretize.TensorMesh([mesh.h[2][mapping.maps[-1].active_cells]]) reg = regularization.WeightedLeastSquares(regMesh) - opt = optimization.InexactGaussNewton(maxIterCG=10) + opt = optimization.InexactGaussNewton(cg_maxiter=10) invProb = inverse_problem.BaseInvProblem(dmisfit, reg, opt) # directives diff --git a/examples/20-published/plot_laguna_del_maule_inversion.py b/examples/20-published/plot_laguna_del_maule_inversion.py index b7f212e1c1..90bdefc85f 100644 --- a/examples/20-published/plot_laguna_del_maule_inversion.py +++ b/examples/20-published/plot_laguna_del_maule_inversion.py @@ -110,8 +110,8 @@ def run(plotIt=True, cleanAfterRun=True): lower=driver.bounds[0], upper=driver.bounds[1], maxIterLS=10, - maxIterCG=20, - tolCG=1e-4, + cg_maxiter=20, + cg_rtol=1e-4, ) # Define misfit function (obs-calc) diff --git a/examples/_archived/plot_inv_grav_linear.py b/examples/_archived/plot_inv_grav_linear.py index c860677158..68752be2ee 100644 --- a/examples/_archived/plot_inv_grav_linear.py +++ b/examples/_archived/plot_inv_grav_linear.py @@ -112,7 +112,7 @@ def run(plotIt=True): # Add directives to the inversion opt = optimization.ProjectedGNCG( - maxIter=100, lower=-1.0, upper=1.0, maxIterLS=20, maxIterCG=10, tolCG=1e-3 + maxIter=100, lower=-1.0, upper=1.0, maxIterLS=20, cg_maxiter=10, cg_rtol=1e-3 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) betaest = directives.BetaEstimate_ByEig(beta0_ratio=1e-1) diff --git a/examples/_archived/plot_inv_mag_linear.py b/examples/_archived/plot_inv_mag_linear.py index f5383bcc1b..438ab3dea2 100644 --- a/examples/_archived/plot_inv_mag_linear.py +++ b/examples/_archived/plot_inv_mag_linear.py @@ -120,7 +120,7 @@ def run(plotIt=True): # Add directives to the inversion opt = optimization.ProjectedGNCG( - maxIter=20, lower=0.0, upper=1.0, maxIterLS=20, maxIterCG=20, tolCG=1e-3 + maxIter=20, lower=0.0, upper=1.0, maxIterLS=20, cg_maxiter=20, cg_rtol=1e-3 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) betaest = directives.BetaEstimate_ByEig(beta0_ratio=1e-1) diff --git a/tests/base/test_directives.py b/tests/base/test_directives.py index ea39e2413f..387f74d6c3 100644 --- a/tests/base/test_directives.py +++ b/tests/base/test_directives.py @@ -126,7 +126,7 @@ def setUp(self): # Add directives to the inversion opt = optimization.ProjectedGNCG( - maxIter=2, lower=-10.0, upper=10.0, maxIterCG=2 + maxIter=2, lower=-10.0, upper=10.0, cg_maxiter=2 ) self.model = m diff --git a/tests/base/test_joint.py b/tests/base/test_joint.py index f1b56f0fe9..5ea1467ad9 100644 --- a/tests/base/test_joint.py +++ b/tests/base/test_joint.py @@ -98,7 +98,7 @@ def test_inv_mref_setting(self): reg2 = regularization.WeightedLeastSquares(self.mesh) reg = reg1 + reg2 opt = optimization.ProjectedGNCG( - maxIter=30, lower=-10, upper=10, maxIterLS=20, maxIterCG=50, tolCG=1e-4 + maxIter=30, lower=-10, upper=10, maxIterLS=20, cg_maxiter=50, cg_rtol=1e-3 ) invProb = inverse_problem.BaseInvProblem(self.dmiscombo, reg, opt) directives_list = [ diff --git a/tests/dask/test_DC_jvecjtvecadj_dask.py b/tests/dask/test_DC_jvecjtvecadj_dask.py index c61ad59fcc..7440cadd34 100644 --- a/tests/dask/test_DC_jvecjtvecadj_dask.py +++ b/tests/dask/test_DC_jvecjtvecadj_dask.py @@ -49,7 +49,7 @@ def setUp(self): dmis = data_misfit.L2DataMisfit(simulation=simulation, data=dobs) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -132,7 +132,7 @@ def setUp(self): dmis = data_misfit.L2DataMisfit(simulation=simulation, data=dobs) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) diff --git a/tests/dask/test_IP_jvecjtvecadj_dask.py b/tests/dask/test_IP_jvecjtvecadj_dask.py index f18fc04793..2e34848b60 100644 --- a/tests/dask/test_IP_jvecjtvecadj_dask.py +++ b/tests/dask/test_IP_jvecjtvecadj_dask.py @@ -97,7 +97,7 @@ def setUp(self): dmis = data_misfit.L2DataMisfit(data=dobs, simulation=simulation) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=5, maxIter=1, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=5 + maxIterLS=5, maxIter=1, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=5 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -172,7 +172,7 @@ def setUp(self): dmis = data_misfit.L2DataMisfit(data=dobs, simulation=simulation) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -247,7 +247,7 @@ def setUp(self): dmis = data_misfit.L2DataMisfit(data=dobs, simulation=simulation) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -325,7 +325,7 @@ def setUp(self): dmis = data_misfit.L2DataMisfit(data=dobs, simulation=simulation) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -410,7 +410,7 @@ def setUp(self): dmis = data_misfit.L2DataMisfit(data=dobs, simulation=simulation) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) diff --git a/tests/dask/test_grav_inversion_linear.py b/tests/dask/test_grav_inversion_linear.py index bef3ea796f..3b3f97ff02 100644 --- a/tests/dask/test_grav_inversion_linear.py +++ b/tests/dask/test_grav_inversion_linear.py @@ -102,7 +102,12 @@ def setUp(self): # Add directives to the inversion opt = optimization.ProjectedGNCG( - maxIter=100, lower=-1.0, upper=1.0, maxIterLS=20, maxIterCG=10, tolCG=1e-4 + maxIter=100, + lower=-1.0, + upper=1.0, + maxIterLS=20, + cg_maxiter=10, + cg_rtol=1e-4, ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e2) diff --git a/tests/dask/test_mag_MVI_Octree.py b/tests/dask/test_mag_MVI_Octree.py index 167a191775..cb337e8437 100644 --- a/tests/dask/test_mag_MVI_Octree.py +++ b/tests/dask/test_mag_MVI_Octree.py @@ -139,7 +139,7 @@ def setUp(self): # Add directives to the inversion opt = optimization.ProjectedGNCG( - maxIter=10, lower=-10, upper=10.0, maxIterLS=5, maxIterCG=5, tolCG=1e-4 + maxIter=10, lower=-10, upper=10.0, maxIterLS=5, cg_maxiter=5, cg_rtol=1e-3 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) @@ -203,9 +203,9 @@ def setUp(self): lower=Lbound, upper=Ubound, maxIterLS=5, - maxIterCG=5, - tolCG=1e-3, - stepOffBoundsFact=1e-3, + cg_maxiter=5, + cg_rtol=1e-3, + active_set_grad_scale=1e-3, ) opt.approxHinv = None diff --git a/tests/dask/test_mag_inversion_linear_Octree.py b/tests/dask/test_mag_inversion_linear_Octree.py index 69a1f4748a..e01da350cd 100644 --- a/tests/dask/test_mag_inversion_linear_Octree.py +++ b/tests/dask/test_mag_inversion_linear_Octree.py @@ -139,9 +139,9 @@ def setUp(self): lower=0.0, upper=10.0, maxIterLS=5, - maxIterCG=10, - tolCG=1e-4, - stepOffBoundsFact=1e-4, + cg_maxiter=10, + cg_rtol=1e-3, + active_set_grad_scale=1e-4, ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e6) diff --git a/tests/dask/test_mag_nonLinear_Amplitude.py b/tests/dask/test_mag_nonLinear_Amplitude.py index 46935c5455..d78993048c 100644 --- a/tests/dask/test_mag_nonLinear_Amplitude.py +++ b/tests/dask/test_mag_nonLinear_Amplitude.py @@ -152,8 +152,8 @@ def setUp(self): lower=-np.inf, upper=np.inf, maxIterLS=5, - maxIterCG=5, - tolCG=1e-3, + cg_maxiter=5, + cg_rtol=1e-3, ) # Define misfit function (obs-calc) @@ -244,7 +244,7 @@ def setUp(self): # Add directives to the inversion opt = optimization.ProjectedGNCG( - maxIter=10, lower=0.0, upper=1.0, maxIterLS=5, maxIterCG=5, tolCG=1e-3 + maxIter=10, lower=0.0, upper=1.0, maxIterLS=5, cg_maxiter=5, cg_rtol=1e-3 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) diff --git a/tests/em/static/test_DC_2D_jvecjtvecadj.py b/tests/em/static/test_DC_2D_jvecjtvecadj.py index 3ea979bca6..83c12bff25 100644 --- a/tests/em/static/test_DC_2D_jvecjtvecadj.py +++ b/tests/em/static/test_DC_2D_jvecjtvecadj.py @@ -52,7 +52,7 @@ def setUp(self): dmis = data_misfit.L2DataMisfit(simulation=simulation, data=data) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e0) inv = inversion.BaseInversion(invProb) diff --git a/tests/em/static/test_DC_jvecjtvecadj.py b/tests/em/static/test_DC_jvecjtvecadj.py index 7ab643fb33..c99cb4e274 100644 --- a/tests/em/static/test_DC_jvecjtvecadj.py +++ b/tests/em/static/test_DC_jvecjtvecadj.py @@ -50,7 +50,7 @@ def setUp(self): dmis = data_misfit.L2DataMisfit(simulation=simulation, data=dobs) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -196,7 +196,7 @@ def setUp(self): dmis = data_misfit.L2DataMisfit(simulation=simulation, data=dobs) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -273,7 +273,7 @@ def setUp(self): dmis = data_misfit.L2DataMisfit(simulation=simulation, data=dobs) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -350,7 +350,7 @@ def setUp(self): dmis = data_misfit.L2DataMisfit(simulation=simulation, data=dobs) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -434,7 +434,7 @@ def setUp(self): dmis = data_misfit.L2DataMisfit(simulation=simulation, data=dobs) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -522,7 +522,7 @@ def setUp(self): dmis = data_misfit.L2DataMisfit(simulation=simulation, data=dobs) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) diff --git a/tests/em/static/test_IP_2D_jvecjtvecadj.py b/tests/em/static/test_IP_2D_jvecjtvecadj.py index 7cf4ed0176..46ff8fe203 100644 --- a/tests/em/static/test_IP_2D_jvecjtvecadj.py +++ b/tests/em/static/test_IP_2D_jvecjtvecadj.py @@ -52,7 +52,7 @@ def setUp(self): dmis = data_misfit.L2DataMisfit(data=dobs, simulation=problem) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -135,7 +135,7 @@ def setUp(self): dmis = data_misfit.L2DataMisfit(data=dobs, simulation=problem) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) diff --git a/tests/em/static/test_IP_jvecjtvecadj.py b/tests/em/static/test_IP_jvecjtvecadj.py index ecc9a8188e..f3906d5026 100644 --- a/tests/em/static/test_IP_jvecjtvecadj.py +++ b/tests/em/static/test_IP_jvecjtvecadj.py @@ -44,7 +44,7 @@ def setUp(self): dmis = data_misfit.L2DataMisfit(data=dobs, simulation=simulation) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -120,7 +120,7 @@ def setUp(self): dmis = data_misfit.L2DataMisfit(data=dobs, simulation=simulation) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -199,7 +199,7 @@ def setUp(self): dmis = data_misfit.L2DataMisfit(data=dobs, simulation=simulation) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -285,7 +285,7 @@ def setUp(self): dmis = data_misfit.L2DataMisfit(data=dobs, simulation=simulation) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) diff --git a/tests/em/static/test_SIP_2D_jvecjtvecadj.py b/tests/em/static/test_SIP_2D_jvecjtvecadj.py index dd2cb4bb7c..9068da25d4 100644 --- a/tests/em/static/test_SIP_2D_jvecjtvecadj.py +++ b/tests/em/static/test_SIP_2D_jvecjtvecadj.py @@ -65,7 +65,7 @@ def setUp(self): dmis = data_misfit.L2DataMisfit(data=dobs, simulation=problem) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -162,7 +162,7 @@ def setUp(self): dmis = data_misfit.L2DataMisfit(data=dobs, simulation=problem) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -277,7 +277,7 @@ def setUp(self): ) reg = reg_eta + reg_taui + reg_c opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) diff --git a/tests/em/static/test_SIP_jvecjtvecadj.py b/tests/em/static/test_SIP_jvecjtvecadj.py index d1b0181fb4..4929aae0fe 100644 --- a/tests/em/static/test_SIP_jvecjtvecadj.py +++ b/tests/em/static/test_SIP_jvecjtvecadj.py @@ -71,7 +71,7 @@ def setUp(self): dmis = data_misfit.L2DataMisfit(data=dobs, simulation=problem) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -174,7 +174,7 @@ def setUp(self): dmis = data_misfit.L2DataMisfit(data=dobs, simulation=problem) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -289,7 +289,7 @@ def setUp(self): reg_c = regularization.Sparse(mesh, mapping=wires.c, active_cells=~airind) reg = reg_eta + reg_taui + reg_c opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) diff --git a/tests/em/vrm/test_vrminv.py b/tests/em/vrm/test_vrminv.py index 6882cfd048..78f14dd3fc 100644 --- a/tests/em/vrm/test_vrminv.py +++ b/tests/em/vrm/test_vrminv.py @@ -75,7 +75,7 @@ def test_basic_inversion(self): weights={"weights": W}, ) opt = optimization.ProjectedGNCG( - maxIter=20, lower=0.0, upper=1e-2, maxIterLS=20, tolCG=1e-4 + maxIter=20, lower=0.0, upper=1e-2, maxIterLS=20, cg_rtol=1e-3 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) directives = [BetaSchedule(coolingFactor=2, coolingRate=1), TargetMisfit()] diff --git a/tests/pf/test_equivalent_sources.py b/tests/pf/test_equivalent_sources.py index 0454baedcf..4a726bbfd6 100644 --- a/tests/pf/test_equivalent_sources.py +++ b/tests/pf/test_equivalent_sources.py @@ -842,8 +842,8 @@ def build_inversion(self, mesh, simulation, synthetic_data, max_iterations=20): optimization = ProjectedGNCG( maxIter=max_iterations, maxIterLS=5, - maxIterCG=20, - tolCG=1e-4, + cg_maxiter=20, + cg_rtol=1e-3, ) # Build inverse problem inverse_problem = simpeg.inverse_problem.BaseInvProblem( diff --git a/tests/pf/test_grav_inversion_linear.py b/tests/pf/test_grav_inversion_linear.py index 1dfabebaee..a561436fcd 100644 --- a/tests/pf/test_grav_inversion_linear.py +++ b/tests/pf/test_grav_inversion_linear.py @@ -98,7 +98,7 @@ def test_gravity_inversion_linear(engine): # Add directives to the inversion opt = optimization.ProjectedGNCG( - maxIter=100, lower=-1.0, upper=1.0, maxIterLS=20, maxIterCG=10, tolCG=1e-3 + maxIter=100, lower=-1.0, upper=1.0, maxIterLS=20, cg_maxiter=10, cg_rtol=1e-3 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) diff --git a/tests/pf/test_mag_MVI_Octree.py b/tests/pf/test_mag_MVI_Octree.py index f03df665f1..40a2e89850 100644 --- a/tests/pf/test_mag_MVI_Octree.py +++ b/tests/pf/test_mag_MVI_Octree.py @@ -135,7 +135,7 @@ def setUp(self): # Add directives to the inversion opt = optimization.ProjectedGNCG( - maxIter=10, lower=-10, upper=10.0, maxIterLS=5, maxIterCG=5, tolCG=1e-4 + maxIter=10, lower=-10, upper=10.0, maxIterLS=5, cg_maxiter=5, cg_rtol=1e-3 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) @@ -198,9 +198,9 @@ def setUp(self): lower=Lbound, upper=Ubound, maxIterLS=5, - maxIterCG=5, - tolCG=1e-3, - stepOffBoundsFact=1e-3, + cg_maxiter=5, + cg_rtol=1e-3, + active_set_grad_scale=1e-3, ) opt.approxHinv = None diff --git a/tests/pf/test_mag_inversion_linear.py b/tests/pf/test_mag_inversion_linear.py index e19c4b492b..c5cb052ecc 100644 --- a/tests/pf/test_mag_inversion_linear.py +++ b/tests/pf/test_mag_inversion_linear.py @@ -110,7 +110,7 @@ def setUp(self): # Add directives to the inversion opt = optimization.ProjectedGNCG( - maxIter=100, lower=0.0, upper=1.0, maxIterLS=20, maxIterCG=10, tolCG=1e-3 + maxIter=100, lower=0.0, upper=1.0, maxIterLS=20, cg_maxiter=10, cg_rtol=1e-3 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) diff --git a/tests/pf/test_mag_inversion_linear_Octree.py b/tests/pf/test_mag_inversion_linear_Octree.py index af6f7c929f..46e483c8d8 100644 --- a/tests/pf/test_mag_inversion_linear_Octree.py +++ b/tests/pf/test_mag_inversion_linear_Octree.py @@ -131,9 +131,9 @@ def setUp(self): lower=0.0, upper=10.0, maxIterLS=5, - maxIterCG=20, - tolCG=1e-4, - stepOffBoundsFact=1e-4, + cg_maxiter=20, + cg_rtol=1e-3, + active_set_grad_scale=1e-4, ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e6) diff --git a/tests/pf/test_mag_nonLinear_Amplitude.py b/tests/pf/test_mag_nonLinear_Amplitude.py index b9156246d5..e43596926f 100644 --- a/tests/pf/test_mag_nonLinear_Amplitude.py +++ b/tests/pf/test_mag_nonLinear_Amplitude.py @@ -153,8 +153,8 @@ def setUp(self): lower=-np.inf, upper=np.inf, maxIterLS=5, - maxIterCG=5, - tolCG=1e-3, + cg_maxiter=5, + cg_rtol=1e-3, ) # Define misfit function (obs-calc) @@ -246,7 +246,7 @@ def setUp(self): # Add directives to the inversion opt = optimization.ProjectedGNCG( - maxIter=10, lower=0.0, upper=1.0, maxIterLS=5, maxIterCG=5, tolCG=1e-3 + maxIter=10, lower=0.0, upper=1.0, maxIterLS=5, cg_maxiter=5, cg_rtol=1e-3 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) diff --git a/tests/pf/test_mag_vector_amplitude.py b/tests/pf/test_mag_vector_amplitude.py index 3c949e22c5..12b52097e7 100644 --- a/tests/pf/test_mag_vector_amplitude.py +++ b/tests/pf/test_mag_vector_amplitude.py @@ -129,7 +129,7 @@ def setUp(self): # Add directives to the inversion opt = optimization.ProjectedGNCG( - maxIter=10, lower=-10, upper=10.0, maxIterLS=5, maxIterCG=5, tolCG=1e-4 + maxIter=10, lower=-10, upper=10.0, maxIterLS=5, cg_maxiter=5, cg_rtol=1e-3 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) diff --git a/tests/pf/test_pf_quadtree_inversion_linear.py b/tests/pf/test_pf_quadtree_inversion_linear.py index 32450621cd..ec1c71ec89 100644 --- a/tests/pf/test_pf_quadtree_inversion_linear.py +++ b/tests/pf/test_pf_quadtree_inversion_linear.py @@ -284,8 +284,8 @@ def create_inversion(self, sim, data, beta=1e3, all_active=True): lower=-1.0, upper=1.0, maxIterLS=5, - maxIterCG=20, - tolCG=1e-4, + cg_maxiter=20, + cg_rtol=1e-3, ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=beta) diff --git a/tutorials/02-linear_inversion/plot_inv_2_inversion_irls.py b/tutorials/02-linear_inversion/plot_inv_2_inversion_irls.py index b835433f55..b8d8c6317e 100644 --- a/tutorials/02-linear_inversion/plot_inv_2_inversion_irls.py +++ b/tutorials/02-linear_inversion/plot_inv_2_inversion_irls.py @@ -155,7 +155,7 @@ def g(k): # Define how the optimization problem is solved. opt = optimization.ProjectedGNCG( - maxIter=100, lower=-2.0, upper=2.0, maxIterLS=20, maxIterCG=30, tolCG=1e-4 + maxIter=100, lower=-2.0, upper=2.0, maxIterLS=20, cg_maxiter=30, cg_rtol=1e-3 ) # Here we define the inverse problem that is to be solved diff --git a/tutorials/06-ip/plot_inv_2_dcip2d.py b/tutorials/06-ip/plot_inv_2_dcip2d.py index ccf1aba74e..19529d1eaa 100644 --- a/tutorials/06-ip/plot_inv_2_dcip2d.py +++ b/tutorials/06-ip/plot_inv_2_dcip2d.py @@ -541,7 +541,7 @@ # Define how the optimization problem is solved. Here it is a projected # Gauss Newton with Conjugate Gradient solver. ip_optimization = optimization.ProjectedGNCG( - maxIter=15, lower=0.0, upper=1000.0, maxIterCG=30, tolCG=1e-2 + maxIter=15, lower=0.0, upper=1000.0, cg_maxiter=30, cg_rtol=1e-3 ) # Here we define the inverse problem that is to be solved diff --git a/tutorials/06-ip/plot_inv_3_dcip3d.py b/tutorials/06-ip/plot_inv_3_dcip3d.py index bde2f90ed2..1c5aa8f9bc 100644 --- a/tutorials/06-ip/plot_inv_3_dcip3d.py +++ b/tutorials/06-ip/plot_inv_3_dcip3d.py @@ -352,7 +352,9 @@ ) # Define how the optimization problem is solved. -dc_optimization = optimization.InexactGaussNewton(maxIter=15, maxIterCG=30, tolCG=1e-2) +dc_optimization = optimization.InexactGaussNewton( + maxIter=15, cg_maxiter=30, cg_rtol=1e-2 +) # Here we define the inverse problem that is to be solved dc_inverse_problem = inverse_problem.BaseInvProblem( @@ -612,7 +614,7 @@ # Define how the optimization problem is solved. ip_optimization = optimization.ProjectedGNCG( - maxIter=15, lower=0.0, upper=10, maxIterCG=30, tolCG=1e-2 + maxIter=15, lower=0.0, upper=10, cg_maxiter=30, cg_rtol=1e-3 ) # Here we define the inverse problem that is to be solved diff --git a/tutorials/07-fdem/plot_inv_1_em1dfm.py b/tutorials/07-fdem/plot_inv_1_em1dfm.py index 17a2564cee..bb6b88a90a 100644 --- a/tutorials/07-fdem/plot_inv_1_em1dfm.py +++ b/tutorials/07-fdem/plot_inv_1_em1dfm.py @@ -244,7 +244,7 @@ # Define how the optimization problem is solved. Here we will use an inexact # Gauss-Newton approach that employs the conjugate gradient solver. -opt = optimization.ProjectedGNCG(maxIter=50, maxIterLS=20, maxIterCG=30, tolCG=1e-3) +opt = optimization.ProjectedGNCG(maxIter=50, maxIterLS=20, cg_maxiter=30, cg_rtol=1e-3) # Define the inverse problem inv_prob = inverse_problem.BaseInvProblem(dmis, reg, opt) diff --git a/tutorials/08-tdem/plot_inv_1_em1dtm.py b/tutorials/08-tdem/plot_inv_1_em1dtm.py index 2c2cfc7d4c..f5e4bfa0a0 100644 --- a/tutorials/08-tdem/plot_inv_1_em1dtm.py +++ b/tutorials/08-tdem/plot_inv_1_em1dtm.py @@ -233,7 +233,7 @@ # Define how the optimization problem is solved. Here we will use an inexact # Gauss-Newton approach that employs the conjugate gradient solver. -opt = optimization.ProjectedGNCG(maxIter=100, maxIterLS=20, maxIterCG=30, tolCG=1e-3) +opt = optimization.ProjectedGNCG(maxIter=100, maxIterLS=20, cg_maxiter=30, cg_rtol=1e-3) # Define the inverse problem inv_prob = inverse_problem.BaseInvProblem(dmis, reg, opt) diff --git a/tutorials/12-seismic/plot_inv_1_tomography_2D.py b/tutorials/12-seismic/plot_inv_1_tomography_2D.py index b60ce62ad6..c25adc23c9 100644 --- a/tutorials/12-seismic/plot_inv_1_tomography_2D.py +++ b/tutorials/12-seismic/plot_inv_1_tomography_2D.py @@ -223,7 +223,7 @@ # Define how the optimization problem is solved. opt = optimization.ProjectedGNCG( - maxIter=100, lower=0.0, upper=1e6, maxIterLS=20, maxIterCG=10, tolCG=1e-4 + maxIter=100, lower=0.0, upper=1e6, maxIterLS=20, cg_maxiter=10, cg_rtol=1e-3 ) # Here we define the inverse problem that is to be solved diff --git a/tutorials/13-joint_inversion/plot_inv_3_cross_gradient_pf.py b/tutorials/13-joint_inversion/plot_inv_3_cross_gradient_pf.py index e7d6fd3843..ae20614ebe 100755 --- a/tutorials/13-joint_inversion/plot_inv_3_cross_gradient_pf.py +++ b/tutorials/13-joint_inversion/plot_inv_3_cross_gradient_pf.py @@ -343,8 +343,8 @@ lower=-2.0, upper=2.0, maxIterLS=20, - maxIterCG=100, - tolCG=1e-3, + cg_maxiter=100, + cg_rtol=1e-3, tolX=1e-3, ) diff --git a/tutorials/14-pgi/plot_inv_1_joint_pf_pgi_full_info_tutorial.py b/tutorials/14-pgi/plot_inv_1_joint_pf_pgi_full_info_tutorial.py index 7c4170f81f..de1c71c773 100644 --- a/tutorials/14-pgi/plot_inv_1_joint_pf_pgi_full_info_tutorial.py +++ b/tutorials/14-pgi/plot_inv_1_joint_pf_pgi_full_info_tutorial.py @@ -394,8 +394,8 @@ lower=lowerbound, upper=upperbound, maxIterLS=20, - maxIterCG=100, - tolCG=1e-4, + cg_maxiter=100, + cg_rtol=1e-3, ) # create inverse problem invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) diff --git a/tutorials/14-pgi/plot_inv_2_joint_pf_pgi_no_info_tutorial.py b/tutorials/14-pgi/plot_inv_2_joint_pf_pgi_no_info_tutorial.py index 1c14dbf021..5132e90456 100644 --- a/tutorials/14-pgi/plot_inv_2_joint_pf_pgi_no_info_tutorial.py +++ b/tutorials/14-pgi/plot_inv_2_joint_pf_pgi_no_info_tutorial.py @@ -424,8 +424,8 @@ lower=lowerbound, upper=upperbound, maxIterLS=20, - maxIterCG=100, - tolCG=1e-4, + cg_maxiter=100, + cg_rtol=1e-3, ) # create inverse problem invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) diff --git a/tutorials/_temporary/plot_4c_fdem3d_inversion.py b/tutorials/_temporary/plot_4c_fdem3d_inversion.py index 86633314b3..06006bf9a0 100644 --- a/tutorials/_temporary/plot_4c_fdem3d_inversion.py +++ b/tutorials/_temporary/plot_4c_fdem3d_inversion.py @@ -321,9 +321,9 @@ # Define how the optimization problem is solved. Here we will use a projected # Gauss-Newton approach that employs the conjugate gradient solver. # opt = optimization.ProjectedGNCG( -# maxIterCG=5, tolCG=1e-2, lower=-10, upper=5 +# cg_maxiter=5, cg_rtol=1e-2, lower=-10, upper=5 # ) -opt = optimization.InexactGaussNewton(maxIterCG=5, tolCG=1e-2) +opt = optimization.InexactGaussNewton(cg_maxiter=5, cg_rtol=1e-2) # Here we define the inverse problem that is to be solved inv_prob = inverse_problem.BaseInvProblem(dmis, reg, opt) diff --git a/tutorials/_temporary/plot_inv_1_em1dtm_stitched_skytem.py b/tutorials/_temporary/plot_inv_1_em1dtm_stitched_skytem.py index cfdfc04a4c..0643bca063 100644 --- a/tutorials/_temporary/plot_inv_1_em1dtm_stitched_skytem.py +++ b/tutorials/_temporary/plot_inv_1_em1dtm_stitched_skytem.py @@ -284,7 +284,7 @@ # Define how the optimization problem is solved. Here we will use an inexact # Gauss-Newton approach that employs the conjugate gradient solver. -opt = optimization.InexactGaussNewton(maxIter=40, maxIterCG=20) +opt = optimization.InexactGaussNewton(maxIter=40, cg_maxiter=20) # Define the inverse problem inv_prob = inverse_problem.BaseInvProblem(dmis, reg, opt) From 248dd3fd8e1ccf7b017d00bd167583147dd0b6b1 Mon Sep 17 00:00:00 2001 From: Lindsey Heagy Date: Wed, 15 Oct 2025 10:39:00 -0700 Subject: [PATCH 179/194] Make `ComplexMap.deriv` to return a sparse diagonal matrix (#1686) Make the `ComplexMap.deriv` to return a sparse diagonal matrix instead of a `LinearOperator`. This makes the method to match the signature of other mappings. Add tests that check the expected behaviour. --------- Co-authored-by: Joseph Capriotti Co-authored-by: Santiago Soler --- simpeg/maps/_property_maps.py | 25 ++++++----- tests/base/test_maps.py | 82 +++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 11 deletions(-) diff --git a/simpeg/maps/_property_maps.py b/simpeg/maps/_property_maps.py index 88005dc2d6..ad16ba15ef 100644 --- a/simpeg/maps/_property_maps.py +++ b/simpeg/maps/_property_maps.py @@ -6,7 +6,6 @@ from numbers import Real import numpy as np import scipy.sparse as sp -from scipy.sparse.linalg import LinearOperator from scipy.constants import mu_0 from scipy.special import expit, logit from discretize.utils import mkvc, sdiag, rotation_matrix_from_normals @@ -950,6 +949,18 @@ def deriv(self, m, v=None): where :math:`\mathbf{I}` is the identity matrix of shape (*nP/2*, *nP/2*) and :math:`j = \sqrt{-1}`. + .. important:: + + Calculating the transpose of the derivative of the + :class:`~simpeg.maps.ComplexMap` as follows doesn't return the adjoint of + the matrix, but its transpose: + + .. code:: python + + complex_map = ComplexMap(...) + derivative = complex_map.deriv(m) + derivative.T # this is not the complex adjoint + Parameters ---------- m : (nP) numpy.ndarray @@ -1001,17 +1012,9 @@ def deriv(self, m, v=None): """ nC = self.shape[0] - shp = (nC, nC * 2) - - def fwd(v): - return v[:nC] + v[nC:] * 1j - - def adj(v): - return np.r_[v.real, v.imag] - if v is not None: - return LinearOperator(shp, matvec=fwd, rmatvec=adj) * v - return LinearOperator(shp, matvec=fwd, rmatvec=adj) + return v[:nC] + v[nC:] * 1j + return sp.diags([1, 1j], [0, nC], [nC, 2 * nC]) class SelfConsistentEffectiveMedium(IdentityMap): diff --git a/tests/base/test_maps.py b/tests/base/test_maps.py index 966ef8a819..cafc89b120 100644 --- a/tests/base/test_maps.py +++ b/tests/base/test_maps.py @@ -939,5 +939,87 @@ def test_indactive_error_setter(self, mesh, active_cells, map_class): mapping.indActive = active_cells +class TestComplexMapDerivative: + """ + Test deriv method of ComplexMap. + """ + + @pytest.fixture + def mesh(self): + return discretize.TensorMesh([4]) + + @pytest.fixture + def active_cells(self, mesh): + return mesh.cell_centers < 0.5 + + def test_deriv(self, mesh, active_cells): + """ + Test the deriv method. + + Since the mapping is linear, the derivative matrix times a vector should return + the same as evaluating the mapping on the same vector. + """ + n_cells = mesh.n_cells + n_active_cells = np.sum(active_cells) + mapping = maps.ComplexMap(nP=n_active_cells * 2) + m = np.random.default_rng(seed=12).uniform(size=n_cells) + derivative = mapping.deriv(m) + expected = mapping * m + np.testing.assert_allclose(expected, derivative @ m) + + def test_deriv_with_vector(self, mesh, active_cells): + """ + Test the deriv method with a ``v`` argument. + + Since the mapping is linear, the derivative matrix times a vector should return + the same as evaluating the mapping on the same vector. + """ + n_cells = mesh.n_cells + n_active_cells = np.sum(active_cells) + mapping = maps.ComplexMap(nP=n_active_cells * 2) + rng = np.random.default_rng(seed=12) + m = rng.uniform(size=n_cells) + v = rng.uniform(size=n_cells) + derivative = mapping.deriv(m, v=v) + expected = mapping * v + np.testing.assert_allclose(expected, derivative) + + def test_deriv_within_combo(self, mesh, active_cells): + """ + Test the deriv method when being called within a ``ComboMap``. + """ + n_cells = mesh.n_cells + n_active_cells = np.sum(active_cells) + inject_map = maps.InjectActiveCells( + mesh, active_cells=active_cells, value_inactive=0 + ) + complex_map = maps.ComplexMap(nP=n_active_cells * 2) + mapping = inject_map * complex_map + rng = np.random.default_rng(seed=12) + m = rng.uniform(size=n_cells) + expected = mapping * m + derivative = mapping.deriv(m) + np.testing.assert_allclose(expected, derivative @ m) + + def test_deriv_within_combo_with_vector(self, mesh, active_cells): + """ + Test the deriv method when being called within a ``ComboMap`` and ``v`` as an + array. + """ + n_cells = mesh.n_cells + n_active_cells = np.sum(active_cells) + inject_map = maps.InjectActiveCells( + mesh, active_cells=active_cells, value_inactive=0 + ) + complex_map = maps.ComplexMap(nP=n_active_cells * 2) + mapping = inject_map * complex_map + rng = np.random.default_rng(seed=12) + m = rng.uniform(size=n_cells) + v = rng.uniform(size=n_cells) + derivative = mapping.deriv(m, v=v) + expected = mapping * v + np.testing.assert_allclose(expected, derivative) + + if __name__ == "__main__": unittest.main() From 818451c94422952de27acd3123cf58839fc84026 Mon Sep 17 00:00:00 2001 From: Ying Hu <64567062+YingHuuu@users.noreply.github.com> Date: Thu, 16 Oct 2025 02:29:45 +0800 Subject: [PATCH 180/194] Standardize signature of mappings' `deriv` method (#1407) Standardize signature of `deriv` method across mappings: make sure they are all defined as `deriv(m, v=None)`. Implement the dot product between the derivative matrix and `v` if `v` is not `None`. Add tests for the modified methods. --------- Co-authored-by: Santiago Soler --- simpeg/maps/_parametric.py | 38 +++++++++------ simpeg/maps/_property_maps.py | 7 ++- tests/base/test_maps.py | 90 +++++++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 16 deletions(-) diff --git a/simpeg/maps/_parametric.py b/simpeg/maps/_parametric.py index 06b8e05b7c..4837b7c6fd 100644 --- a/simpeg/maps/_parametric.py +++ b/simpeg/maps/_parametric.py @@ -1431,7 +1431,7 @@ def _deriv_layer_thickness(self, mDict): mDict["val_layer"] - mDict["val_background"] ) * self._atanLayerDeriv_layer_thickness(mDict) - def deriv(self, m): + def deriv(self, m, v=None): r"""Derivative of the mapping with respect to the input parameters. Let :math:`\mathbf{m} = [\sigma_0, \;\sigma_1,\; z_L , \; h]` be the set of @@ -1471,10 +1471,8 @@ def deriv(self, m): input argument *v* is not ``None``, the method returns the derivative times the vector *v*. """ - mDict = self.mDict(m) - - return sp.csr_matrix( + derivative = sp.csr_matrix( np.vstack( [ self._deriv_val_background(mDict), @@ -1484,6 +1482,9 @@ def deriv(self, m): ] ).T ) + if v is not None: + return derivative @ v + return derivative class ParametricBlock(BaseParametric): @@ -1790,7 +1791,7 @@ def _deriv3D(self, mDict): ] ).T - def deriv(self, m): + def deriv(self, m, v=None): r"""Derivative of the mapping with respect to the input parameters. Let :math:`\mathbf{m} = [\sigma_0, \;\sigma_1,\; x_b, \; dx, (\; y_b, \; dy, \; z_b , dz)]` @@ -1827,9 +1828,12 @@ def deriv(self, m): input argument *v* is not ``None``, the method returns the derivative times the vector *v*. """ - return sp.csr_matrix( + derivative = sp.csr_matrix( getattr(self, "_deriv{}D".format(self.mesh.dim))(self.mDict(m)) ) + if v is not None: + return derivative @ v + return derivative class ParametricEllipsoid(ParametricBlock): @@ -2219,10 +2223,9 @@ def _deriv_casing_top(self, mDict): + d_insideCasing_cont_dcasing_top ) - def deriv(self, m): + def deriv(self, m, v=None): mDict = self.mDict(m) - - return sp.csr_matrix( + derivative = sp.csr_matrix( np.vstack( [ self._deriv_val_background(mDict), @@ -2238,6 +2241,9 @@ def deriv(self, m): ] ).T ) + if v is not None: + return derivative @ v + return derivative class ParametricBlockInLayer(ParametricLayer): @@ -2675,8 +2681,12 @@ def _transform(self, m): elif self.mesh.dim == 3: return self._transform3d(m) - def deriv(self, m): - if self.mesh.dim == 2: - return sp.csr_matrix(self._deriv2d(m)) - elif self.mesh.dim == 3: - return sp.csr_matrix(self._deriv3d(m)) + def deriv(self, m, v=None): + derivative = ( + sp.csr_matrix(self._deriv2d(m)) + if self.mesh.dim == 2 + else sp.csr_matrix(self._deriv3d(m)) + ) + if v is not None: + return derivative @ v + return derivative diff --git a/simpeg/maps/_property_maps.py b/simpeg/maps/_property_maps.py index ad16ba15ef..0da639e23e 100644 --- a/simpeg/maps/_property_maps.py +++ b/simpeg/maps/_property_maps.py @@ -1506,13 +1506,16 @@ def _sc2phaseEMTSpheroidstransformDeriv(self, sige, phi1): def _transform(self, m): return self._sc2phaseEMTSpheroidstransform(m) - def deriv(self, m): + def deriv(self, m, v=None): """ Derivative of the effective conductivity with respect to the volume fraction of phase 2 material """ sige = self._transform(m) - return self._sc2phaseEMTSpheroidstransformDeriv(sige, m) + derivative = self._sc2phaseEMTSpheroidstransformDeriv(sige, m) + if v is not None: + return derivative @ v + return derivative def inverse(self, sige): """ diff --git a/tests/base/test_maps.py b/tests/base/test_maps.py index cafc89b120..c697ad820a 100644 --- a/tests/base/test_maps.py +++ b/tests/base/test_maps.py @@ -939,6 +939,96 @@ def test_indactive_error_setter(self, mesh, active_cells, map_class): mapping.indActive = active_cells +class TestParametricDeriv: + """ + Test the ``deriv`` method of parametric maps. + + + Test if ``map.deriv(m) @ v`` is equivalent to ``map.deriv(m, v=v)``. + """ + + @pytest.fixture + def mesh_2d(self): + """Sample mesh.""" + h = 10 + return discretize.TensorMesh([h, h], "CC") + + @pytest.fixture + def mesh_3d(self): + """Sample mesh.""" + h = 10 + return discretize.TensorMesh([h, h, h], "CCN") + + @pytest.fixture + def cyl_mesh(self): + """Sample cylindrical mesh.""" + return discretize.CylindricalMesh([4, 6, 5]) + + @pytest.mark.parametrize( + "map_class", + [ + maps.ParametricBlock, + maps.ParametricBlockInLayer, + maps.ParametricEllipsoid, + maps.ParametricLayer, + maps.ParametricPolyMap, + ], + ) + def test_deriv_mesh_3d(self, mesh_3d, map_class): + """ + Test maps on a 3d mesh. + """ + kwargs = {} + if map_class is maps.ParametricPolyMap: + kwargs["order"] = [1, 1] + mapping = map_class(mesh_3d, **kwargs) + model_size = mapping.shape[1] + rng = np.random.default_rng(seed=48) + model = rng.uniform(size=model_size) + v = rng.uniform(size=model_size) + derivative = mapping.deriv(model) + np.testing.assert_allclose(derivative @ v, mapping.deriv(model, v=v)) + + def test_deriv_mesh_2d(self, mesh_2d): + """ + Test maps on a 2d mesh. + """ + mapping = maps.ParametricCircleMap(mesh_2d) + model_size = mapping.shape[1] + rng = np.random.default_rng(seed=48) + model = rng.uniform(size=model_size) + v = rng.uniform(size=model_size) + derivative = mapping.deriv(model) + np.testing.assert_allclose(derivative @ v, mapping.deriv(model, v=v)) + + def test_deriv_cyl_mesh(self, cyl_mesh): + """ + Test maps on a cylindrical mesh. + """ + mapping = maps.ParametricCasingAndLayer(cyl_mesh) + model_size = mapping.shape[1] + rng = np.random.default_rng(seed=48) + model = rng.uniform(size=model_size) + v = rng.uniform(size=model_size) + derivative = mapping.deriv(model) + np.testing.assert_allclose(derivative @ v, mapping.deriv(model, v=v)) + + +def test_deriv_SelfConsistentEffectiveMedium(): + """ + Test deriv method of ``SelfConsistentEffectiveMedium``. + """ + h = 10 + mesh = discretize.TensorMesh([h, h, h], "CCN") + mapping = maps.SelfConsistentEffectiveMedium(mesh, sigma0=1, sigma1=2) + model_size = mapping.shape[1] + rng = np.random.default_rng(seed=48) + model = rng.uniform(size=model_size) + v = rng.uniform(size=model_size) + derivative = mapping.deriv(model) + np.testing.assert_allclose(derivative @ v, mapping.deriv(model, v=v), rtol=1e-6) + + class TestComplexMapDerivative: """ Test deriv method of ComplexMap. From 7db3f9cdee1f0af674ad40255cbd3d572c9ff3cd Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Wed, 15 Oct 2025 23:10:56 +0000 Subject: [PATCH 181/194] Update how mappings are applied in regularizations (#1605) Update how mappings are being used in regularizations: regularize on `mapping(m) - mapping(m_ref)` instead of `mapping(m - m_ref)`. Update implementations and documentation of `Smallness`, `SmoothnessFirstOrder`, `SmoothnessSecondOrder`, and `SmoothnessFullGradient`: the `f_m` will compute difference of the mapped model and reference model, while the `f_m_deriv` will not make use of the reference model now, since it's not carried out in the derivative. Add tests to check this intended behaviour. --- simpeg/regularization/_gradient.py | 18 +- simpeg/regularization/base.py | 529 ++++++++++++++------ tests/base/regularizations/test_mappings.py | 345 +++++++++++++ 3 files changed, 745 insertions(+), 147 deletions(-) create mode 100644 tests/base/regularizations/test_mappings.py diff --git a/simpeg/regularization/_gradient.py b/simpeg/regularization/_gradient.py index f27da93ca5..fbf3451483 100644 --- a/simpeg/regularization/_gradient.py +++ b/simpeg/regularization/_gradient.py @@ -180,18 +180,28 @@ def __init__(self, mesh, alphas=None, reg_dirs=None, ortho_check=True, **kwargs) def __call__(self, m): G = self.cell_gradient M_f = self.W - r = G @ (self.mapping * (self._delta_m(m))) + dm = ( + self.mapping * m - self.mapping * self.reference_model + if self.reference_model is not None + else self.mapping * m + ) + r = G @ dm return r @ M_f @ r def deriv(self, m): - m_d = self.mapping.deriv(self._delta_m(m)) + m_d = self.mapping.deriv(m) G = self.cell_gradient M_f = self.W - r = G @ (self.mapping * (self._delta_m(m))) + dm = ( + self.mapping * m - self.mapping * self.reference_model + if self.reference_model is not None + else self.mapping * m + ) + r = G @ dm return 2 * (m_d.T * (G.T @ (M_f @ r))) def deriv2(self, m, v=None): - m_d = self.mapping.deriv(self._delta_m(m)) + m_d = self.mapping.deriv(m) G = self.cell_gradient M_f = self.W if v is None: diff --git a/simpeg/regularization/base.py b/simpeg/regularization/base.py index 4665d779b6..882d2ef2ef 100644 --- a/simpeg/regularization/base.py +++ b/simpeg/regularization/base.py @@ -486,8 +486,9 @@ class Smallness(BaseRegularization): Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` cells that are active in the inversion. If ``None``, all cells are active. mapping : None, simpeg.maps.BaseMap - The mapping from the model parameters to the active cells in the inversion. - If ``None``, the mapping is the identity map. + The mapping function applied to the model parameters. Use a mapping to + get from model parameters to physical properties in active cells in the + mesh. If ``None``, the mapping is the identity map. reference_model : None, (n_param, ) numpy.ndarray Reference model. If ``None``, the reference model in the inversion is set to the starting model. @@ -501,38 +502,85 @@ class Smallness(BaseRegularization): Notes ----- - We define the regularization function (objective function) for smallness as: + In case no mapping or reference model is used, we define the regularization function + (objective function) for smallness as: .. math:: - \phi (m) = \int_\Omega \, w(r) \, - \Big [ m(r) - m^{(ref)}(r) \Big ]^2 \, dv - where :math:`m(r)` is the model, :math:`m^{(ref)}(r)` is the reference model and :math:`w(r)` - is a user-defined weighting function. + \phi (m) = \int_\Omega \, w(\mathbf{r}) \, + \left\lvert + m(\mathbf{r}) - m^\text{ref}(\mathbf{r}) + \right\rvert^2 \, + d\mathbf{r} - For implementation within SimPEG, the regularization function and its variables - must be discretized onto a `mesh`. The discretized approximation for the regularization - function (objective function) is expressed in linear form as: + where :math:`m(\mathbf{r})` is the model, :math:`m^\text{ref}(\mathbf{r})` is the + reference model, and :math:`w(\mathbf{r})` is a user-defined weighting function. + + For the implementation within SimPEG, the regularization function and its + variables must be discretized onto a `mesh`. The discretized approximation + for the regularization function (objective function) is expressed in linear + form as: .. math:: - \phi (\mathbf{m}) = \sum_i - \tilde{w}_i \, \bigg | \, m_i - m_i^{(ref)} \, \bigg |^2 - where :math:`m_i \in \mathbf{m}` are the discrete model parameter values defined on the mesh and - :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that 1) account - for cell dimensions in the discretization and 2) apply any user-defined weighting. + \phi (\mathbf{m}) = \sum_i \tilde{w}_i \, + \left\lvert \, + m_i - m_i^\text{ref} \, + \right\rvert^2 + + where :math:`m_i \in \mathbf{m}` are the discrete model parameter values defined on + the mesh, and :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting + constants that: + + 1. account for cell dimensions in the discretization, and + 2. apply any user-defined weighting. + This is equivalent to an objective function of the form: .. math:: + \phi (\mathbf{m}) = - \Big \| \mathbf{W} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 + \left\lVert + \mathbf{W} \left[ \mathbf{m} - \mathbf{m}^\text{ref} \right] + \right\rVert^2 - where + where :math:`\mathbf{m}` is the model vector, :math:`\mathbf{m}^\text{ref}` is the + reference model vector, and :math:`\mathbf{W}` is the weighting matrix, - - :math:`\mathbf{m}^{(ref)}` is a reference model (set using `reference_model`), and - - :math:`\mathbf{W}` is the weighting matrix. + **Mapping function** - **Custom weights and the weighting matrix:** + In case make use of a mapping function :math:`\mu` that maps values of the model + into a different space, then the regularization function for smallness gets defined + as: + + .. math:: + + \phi (m) = \int_\Omega \, w(\mathbf{r}) \, + \left\lvert + \mu(m(\mathbf{r})) - \mu(m^\text{ref}(\mathbf{r})) + \right\rvert^2 \, d\mathbf{r} + + In a discretized form, the previous equation is expressed as: + + .. math:: + + \phi (\mathbf{m}) = \sum_i + \tilde{w}_i \, + \left\lvert \, + \mu(m_i) - \mu(m_i^\text{ref}) \, + \right\rvert^2 + + And in matrix form: + + .. math:: + + \phi (\mathbf{m}) = + \left\lVert + \mathbf{W} \left[ \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) \right] + \right\rVert^2. + + + **Custom weights and the weighting matrix** Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of custom cell weights. The weighting applied within the objective function is given by: @@ -574,10 +622,13 @@ def f_m(self, m) -> np.ndarray: For smallness regularization, the regularization kernel function is given by: .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{m} - \mathbf{m}^{(ref)} - where :math:`\mathbf{m}` are the discrete model parameters and :math:`\mathbf{m}^{(ref)}` - is a reference model. For a more detailed description, see the *Notes* section below. + \mathbf{f_m}(\mathbf{m}) = \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) + + where :math:`\mathbf{m}` are the discrete model parameters, + :math:`\mathbf{m}^\text{ref}` + is a reference model, and :math:`\mu` is the mapping function. + For a more detailed description, see the *Notes* section below. Parameters ---------- @@ -594,36 +645,59 @@ def f_m(self, m) -> np.ndarray: The objective function for smallness regularization is given by: .. math:: + \phi_m (\mathbf{m}) = - \Big \| \mathbf{W} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 + \left\lVert + \mathbf{W} + \left[ \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) \right] + \right\rVert^2 - where :math:`\mathbf{m}` are the discrete model parameters defined on the mesh (model), - :math:`\mathbf{m}^{(ref)}` is the reference model, and :math:`\mathbf{W}` is - the weighting matrix. See the :class:`Smallness` class documentation for more detail. + where :math:`\mathbf{m}` are the discrete model parameters defined on + the mesh (model), :math:`\mathbf{m}^\text{ref}` is the reference + model, :math:`\mu` is the mapping function, and :math:`\mathbf{W}` is + the weighting matrix. + See the :class:`Smallness` class documentation for more details. We define the regularization kernel function :math:`\mathbf{f_m}` as: .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{m} - \mathbf{m}^{(ref)} + + \mathbf{f_m}(\mathbf{m}) = + \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) such that .. math:: - \phi_m (\mathbf{m}) = \Big \| \mathbf{W} \, \mathbf{f_m} \Big \|^2 + + \phi_m(\mathbf{m}) = \left\lVert \mathbf{W} \, \mathbf{f_m} \right\rVert^2 """ - return self.mapping * self._delta_m(m) + f_m = ( + self.mapping * m - self.mapping * self.reference_model + if self.reference_model is not None + else self.mapping * m + ) + return f_m def f_m_deriv(self, m) -> csr_matrix: r"""Derivative of the regularization kernel function. - For ``Smallness`` regularization, the derivative of the regularization kernel function - with respect to the model is given by: + For ``Smallness`` regularization, the derivative of the regularization + kernel function with respect to the model is given by: .. math:: - \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{I} - where :math:`\mathbf{I}` is the identity matrix. + \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = + \frac{\partial \mu(\mathbf{m})}{\partial \mathbf{m}} + + where :math:`\mu` is the mapping function. If the mapping is the + identity function (:math:`\mu(\mathbf{m}) = \mathbf{m}`) then the + derivative of the kernel function is the is the identity matrix + :math:`\mathbf{I}`: + + .. math:: + + \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{I} Parameters ---------- @@ -640,31 +714,45 @@ def f_m_deriv(self, m) -> csr_matrix: The objective function for smallness regularization is given by: .. math:: + \phi_m (\mathbf{m}) = - \Big \| \mathbf{W} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 + \left\lVert + \mathbf{W} + \left[ \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) \right] + \right\rVert^2 - where :math:`\mathbf{m}` are the discrete model parameters defined on the mesh (model), - :math:`\mathbf{m}^{(ref)}` is the reference model, and :math:`\mathbf{W}` is - the weighting matrix. See the :class:`Smallness` class documentation for more detail. + where :math:`\mathbf{m}` are the discrete model parameters defined on + the mesh (model), :math:`\mathbf{m}^{(ref)}` is the reference model, + :math:`\mu` is the mapping function, and :math:`\mathbf{W}` is the + weighting matrix. See the :class:`Smallness` class documentation for + more details. We define the regularization kernel function :math:`\mathbf{f_m}` as: .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{m} - \mathbf{m}^{(ref)} + + \mathbf{f_m}(\mathbf{m}) = \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) such that .. math:: - \phi_m (\mathbf{m}) = \Big \| \mathbf{W} \, \mathbf{f_m} \Big \|^2 + + \phi_m (\mathbf{m}) = + \left\lVert + \mathbf{W} \, \mathbf{f_m} + \right\rVert^2 Thus, the derivative with respect to the model is: .. math:: - \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{I} - where :math:`\mathbf{I}` is the identity matrix. + \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = + \frac{\partial \mu(\mathbf{m})}{\partial \mathbf{m}} + + where :math:`\mu` is the mapping function, and :math:`\mathbf{I}` is + the identity matrix. """ - return self.mapping.deriv(self._delta_m(m)) + return self.mapping.deriv(m) class SmoothnessFirstOrder(BaseRegularization): @@ -714,48 +802,96 @@ class SmoothnessFirstOrder(BaseRegularization): along the x-direction as: .. math:: - \phi (m) = \int_\Omega \, w(r) \, - \bigg [ \frac{\partial m}{\partial x} \bigg ]^2 \, dv - where :math:`m(r)` is the model and :math:`w(r)` is a user-defined weighting function. + \phi (m) = \int_\Omega \, w(\mathbf{r}) \, + \left\lvert + \frac{\partial m(\mathbf{r})}{\partial x} + \right\rvert^2 \, d\mathbf{r} + + where :math:`m(\mathbf{r})` is the model, and :math:`w(\mathbf{r})` is + a user-defined weighting function. - For implementation within SimPEG, the regularization function and its variables - must be discretized onto a `mesh`. The discretized approximation for the regularization - function (objective function) is expressed in linear form as: + For the implementation within SimPEG, the regularization function and its + variables must be discretized onto a `mesh`. The discretized approximation + for the regularization function (objective function) is expressed in linear + form as: .. math:: - \phi (\mathbf{m}) = \sum_i - \tilde{w}_i \, \bigg | \, \frac{\partial m_i}{\partial x} \, \bigg |^2 - where :math:`m_i \in \mathbf{m}` are the discrete model parameter values defined on the mesh - and :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that - 1) account for cell dimensions in the discretization and 2) apply any user-defined weighting. + \phi (\mathbf{m}) = \sum_i \tilde{w}_i \, + \left\lvert \, + \frac{\partial m_i}{\partial x} \, + \right\rvert^2 + + where :math:`m_i \in \mathbf{m}` are the discrete model parameter values defined on + the mesh, and :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting + constants that: + + 1. account for cell dimensions in the discretization, and + 2. apply any user-defined weighting. + This is equivalent to an objective function of the form: .. math:: - \phi (\mathbf{m}) = \Big \| \mathbf{W \, G_x m } \, \Big \|^2 - where + \phi (\mathbf{m}) = + \left\lVert + \mathbf{W} \mathbf{G_x} \mathbf{m} + \right\rVert^2 + + where :math:`\mathbf{m}` is the model vector, + :math:`\mathbf{G_x}` is the partial cell gradient operator along the x-direction, + and :math:`\mathbf{W}` is the weighting matrix. + + .. note:: - - :math:`\mathbf{G_x}` is the partial cell gradient operator along the x-direction, and - - :math:`\mathbf{W}` is the weighting matrix. + Note that since :math:`\mathbf{G_x}` maps from cell centers to x-faces, + :math:`\mathbf{W}` is an operator that acts on variables living on x-faces. - Note that since :math:`\mathbf{G_x}` maps from cell centers to x-faces, - :math:`\mathbf{W}` is an operator that acts on variables living on x-faces. **Reference model in smoothness:** - Gradients/interfaces within a discrete reference model :math:`\mathbf{m}^{(ref)}` can be - preserved by including the reference model the regularization. + Gradients/interfaces within a discrete reference model :math:`\mathbf{m}^\text{ref}` + can be preserved by including the reference model the regularization. In this case, the objective function becomes: .. math:: - \phi (\mathbf{m}) = \Big \| \mathbf{W G_x} - \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 - This functionality is used by setting a reference model with the - `reference_model` property, and by setting the `reference_model_in_smooth` parameter - to ``True``. + \phi(\mathbf{m}) = + \lVert + \mathbf{W G_x} + \left[ \mathbf{m} - \mathbf{m}^\text{ref} \right] + \rVert^2 + + This functionality is used by setting a reference model with the `reference_model` + property, and by setting the `reference_model_in_smooth` parameter to ``True``. + + + **Mapping function:** + + In case make use of a mapping function :math:`\mu` that maps values of the model + into a different space, then the regularization function for first-order smoothness + along the x-direction as: + + .. math:: + + \phi (m) = \int_\Omega \, w(\mathbf{r}) \, + \lvert + \frac{\partial}{\partial x} + \left[ \mu(m) - \mu(m^\text{ref}) \right] + \rvert^2 \, d\mathbf{r} + + In matrix form, the previous equation is expressed as: + + .. math:: + + \phi (\mathbf{m}) = + \left\lVert + \mathbf{W} + \mathbf{G_x} + \left[ \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) \right] + \right\rVert^2. + **Custom weights and the weighting matrix:** @@ -885,11 +1021,14 @@ def f_m(self, m): the regularization kernel function is given by: .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] - where :math:`\mathbf{G_x}` is the partial cell gradient operator along the x-direction - (i.e. x-derivative), :math:`\mathbf{m}` are the discrete model parameters defined on the - mesh and :math:`\mathbf{m}^{(ref)}` is the reference model (optional). + \mathbf{f_m}(\mathbf{m}) = + \mathbf{G_x} \left[ \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) \right] + + where :math:`\mathbf{G_x}` is the partial cell gradient operator along the + x-direction (i.e. x-derivative), :math:`\mathbf{m}` are the discrete model + parameters defined on the mesh, :math:`\mathbf{m}^{(ref)}` is the reference + model (optional), and :math:`\mu` is the mapping function. Similarly for smoothness along y and z. Parameters @@ -908,45 +1047,63 @@ def f_m(self, m): is given by: .. math:: - \phi_m (\mathbf{m}) = - \Big \| \mathbf{W G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 + + \phi (\mathbf{m}) = + \lVert + \mathbf{W} + \mathbf{G_x} + \left[ \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) \right] + \rVert^2. where :math:`\mathbf{m}` are the discrete model parameters (model), - :math:`\mathbf{m}^{(ref)}` is the reference model, :math:`\mathbf{G_x}` is the partial - cell gradient operator along the x-direction (i.e. x-derivative), and :math:`\mathbf{W}` is - the weighting matrix. Similar for smoothness along y and z. + :math:`\mathbf{m}^\text{ref}` is the reference model, :math:`\mathbf{G_x}` is the + partial cell gradient operator along the x-direction (i.e. x-derivative), + :math:`\mu` is the mapping function, and :math:`\mathbf{W}` is the weighting + matrix. + Similar for smoothness along y and z. See the :class:`SmoothnessFirstOrder` class documentation for more detail. We define the regularization kernel function :math:`\mathbf{f_m}` as: .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] - such that + \mathbf{f_m}(\mathbf{m}) = + \mathbf{G_x} \left[ \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) \right] + + such that: .. math:: - \phi_m (\mathbf{m}) = \Big \| \mathbf{W \, f_m} \Big \|^2 + + \phi_m(\mathbf{m}) = \lVert \mathbf{W \, f_m} \rVert^2. + """ - dfm_dl = self.mapping * self._delta_m(m) + dfm_dl = ( + self.mapping * m - self.mapping * self.reference_model + if self.reference_model is not None and self.reference_model_in_smooth + else self.mapping * m + ) if self.units is not None and self.units.lower() == "radian": return ( utils.mat_utils.coterminal(self.cell_gradient.sign() @ dfm_dl) / self._cell_distances ) + return self.cell_gradient @ dfm_dl def f_m_deriv(self, m) -> csr_matrix: r"""Derivative of the regularization kernel function. - For first-order smoothness regularization in the x-direction, the derivative of the - regularization kernel function with respect to the model is given by: + For first-order smoothness regularization in the x-direction, the derivative of + the regularization kernel function with respect to the model is given by: .. math:: - \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{G_x} + + \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = + \mathbf{G_x} \frac{\partial \mu(\mathbf{m})}{\partial \mathbf{m}} where :math:`\mathbf{G_x}` is the partial cell gradient operator along x - (i.e. the x-derivative). + (i.e. the x-derivative), and :math:`\mu` is the mapping function. Parameters ---------- @@ -964,31 +1121,44 @@ def f_m_deriv(self, m) -> csr_matrix: is given by: .. math:: - \phi_m (\mathbf{m}) = - \Big \| \mathbf{W G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 + + \phi (\mathbf{m}) = + \left\lVert + \mathbf{W} + \mathbf{G_x} + \left[ \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) \right] + \right\rVert^2. + where :math:`\mathbf{m}` are the discrete model parameters (model), - :math:`\mathbf{m}^{(ref)}` is the reference model, :math:`\mathbf{G_x}` is the partial - cell gradient operator along the x-direction (i.e. x-derivative), and :math:`\mathbf{W}` is - the weighting matrix. Similar for smoothness along y and z. + :math:`\mathbf{m}^\text{ref}` is the reference model, :math:`\mathbf{G_x}` is + the partial cell gradient operator along the x-direction (i.e. x-derivative), + :math:`\mu` is the mapping function, and :math:`\mathbf{W}` is the weighting + matrix. + Similar for smoothness along y and z. See the :class:`SmoothnessFirstOrder` class documentation for more detail. We define the regularization kernel function :math:`\mathbf{f_m}` as: .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] - such that + \mathbf{f_m}(\mathbf{m}) = + \mathbf{G_x} \left[ \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) \right] + + such that: .. math:: - \phi_m (\mathbf{m}) = \Big \| \mathbf{W \, f_m} \Big \|^2 + + \phi_m(\mathbf{m}) = \lVert \mathbf{W \, f_m} \rVert^2. The derivative with respect to the model is therefore: .. math:: - \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{G_x} + + \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} + = \mathbf{G_x} \frac{\partial \mu(\mathbf{m})}{\partial \mathbf{m}} """ - return self.cell_gradient @ self.mapping.deriv(self._delta_m(m)) + return self.cell_gradient @ self.mapping.deriv(m) @property def W(self) -> csr_matrix: @@ -1079,45 +1249,92 @@ class SmoothnessSecondOrder(SmoothnessFirstOrder): smoothness along the x-direction as: .. math:: - \phi (m) = \int_\Omega \, w(r) \, - \bigg [ \frac{\partial^2 m}{\partial x^2} \bigg ]^2 \, dv - where :math:`m(r)` is the model and :math:`w(r)` is a user-defined weighting function. + \phi (m) = \int_\Omega \, w(\mathbf{r}) \, + \left\lvert + \frac{\partial^2 m(\mathbf{r})}{\partial x^2} + \right\rvert^2 \, d\mathbf{r} - For implementation within SimPEG, the regularization function and its variables - must be discretized onto a `mesh`. The discretized approximation for the regularization - function (objective function) is expressed in linear form as: + where :math:`m(\mathbf{r})` is the model, and :math:`w(\mathbf{r})` is + a user-defined weighting function. + + For the implementation within SimPEG, the regularization function and its + variables must be discretized onto a `mesh`. The discretized approximation + for the regularization function (objective function) is expressed in linear + form as: .. math:: - \phi (\mathbf{m}) = \sum_i - \tilde{w}_i \, \bigg | \, \frac{\partial^2 m_i}{\partial x^2} \, \bigg |^2 - where :math:`m_i \in \mathbf{m}` are the discrete model parameter values defined on the - mesh and :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that - 1) account for cell dimensions in the discretization and 2) apply any user-defined weighting. + \phi (\mathbf{m}) = \sum_i \tilde{w}_i \, + \left\lvert \, + \frac{\partial^2 m_i}{\partial x^2} \, + \right\rvert^2 + + where :math:`m_i \in \mathbf{m}` are the discrete model parameter values defined on + the mesh, and :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting + constants that: + + 1. account for cell dimensions in the discretization, and + 2. apply any user-defined weighting. + This is equivalent to an objective function of the form: .. math:: - \phi (\mathbf{m}) = \big \| \mathbf{W \, L_x \, m } \, \big \|^2 - where + \phi (\mathbf{m}) = + \left\lVert + \mathbf{W} \mathbf{L_x} \mathbf{m} + \right\rVert^2 + + where :math:`\mathbf{m}` is the model vector, + :math:`\mathbf{L_x}` is the second-order derivative operator with respect to + :math:`x`, and :math:`\mathbf{W}` is the weighting matrix. - - :math:`\mathbf{L_x}` is a second-order derivative operator with respect to :math:`x`, and - - :math:`\mathbf{W}` is the weighting matrix. **Reference model in smoothness:** - Second-order smoothness within a discrete reference model :math:`\mathbf{m}^{(ref)}` can be - preserved by including the reference model the smoothness regularization function. + Second-order smoothness within a discrete reference model + :math:`\mathbf{m}^\text{ref}` can be preserved by including the reference model the + smoothness regularization function. In this case, the objective function becomes: .. math:: - \phi (\mathbf{m}) = \Big \| \mathbf{W L_x} - \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 - This functionality is used by setting a reference model with the - `reference_model` property, and by setting the `reference_model_in_smooth` parameter - to ``True``. + \phi(\mathbf{m}) = + \lVert + \mathbf{W L_x} + \left[ \mathbf{m} - \mathbf{m}^\text{ref} \right] + \rVert^2 + + This functionality is used by setting a reference model with the `reference_model` + property, and by setting the `reference_model_in_smooth` parameter to ``True``. + + + **Mapping function:** + + In case make use of a mapping function :math:`\mu` that maps values of the model + into a different space, then the regularization function for second-order smoothness + along the x-direction as: + + .. math:: + + \phi (m) = \int_\Omega \, w(\mathbf{r}) \, + \lvert + \frac{\partial^2}{\partial x^2} + \left[ \mu(m) - \mu(m^\text{ref}) \right] + t\rvert^2 \, d\mathbf{r} + + In matrix form, the previous equation is expressed as: + + .. math:: + + \phi (\mathbf{m}) = + \lVert + \mathbf{W} + \mathbf{L_x} + \left[ \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) \right] + \right\rVert^2. + **Custom weights and the weighting matrix:** @@ -1153,11 +1370,14 @@ def f_m(self, m): the regularization kernel function is given by: .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] - where where :math:`\mathbf{m}` are the discrete model parameters (model), - :math:`\mathbf{m}^{(ref)}` is the reference model (optional), :math:`\mathbf{L_x}` - is the discrete second order x-derivative operator. + \mathbf{f_m}(\mathbf{m}) = + \mathbf{L_x} \left[ \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) \right] + + where :math:`\mathbf{L_x}` is the discrete second order x-derivative operator, + :math:`\mathbf{m}` are the discrete model parameters defined on the mesh, + :math:`\mathbf{m}^{(ref)}` is the reference model (optional), and :math:`\mu` is + the mapping function. Parameters ---------- @@ -1175,26 +1395,39 @@ def f_m(self, m): is given by: .. math:: - \phi_m (\mathbf{m}) = - \Big \| \mathbf{W L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 + + \phi(\mathbf{m}) = + \lVert + \mathbf{W} + \mathbf{L_x} + \left[ \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) \right] + \rVert^2. where :math:`\mathbf{m}` are the discrete model parameters (model), - :math:`\mathbf{m}^{(ref)}` is the reference model, :math:`\mathbf{L_x}` is the - second-order x-derivative operator, and :math:`\mathbf{W}` is - the weighting matrix. Similar for smoothness along y and z. + :math:`\mathbf{m}^\text{ref}` is the reference model, :math:`\mathbf{L_x}` is + the second-order x-derivative operator, :math:`\mu` is the mapping function, and + :math:`\mathbf{W}` is the weighting matrix. + Similar for smoothness along y and z. See the :class:`SmoothnessSecondOrder` class documentation for more detail. We define the regularization kernel function :math:`\mathbf{f_m}` as: .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] + + \mathbf{f_m}(\mathbf{m}) = + \mathbf{L_x} \left[ \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) \right] such that .. math:: - \phi_m (\mathbf{m}) = \Big \| \mathbf{W \, f_m} \Big \|^2 + + \phi_m(\mathbf{m}) = \lVert \mathbf{W \, f_m} \rVert^2. """ - dfm_dl = self.mapping * self._delta_m(m) + dfm_dl = ( + self.mapping * m - self.mapping * self.reference_model + if self.reference_model is not None and self.reference_model_in_smooth + else self.mapping * m + ) if self.units is not None and self.units.lower() == "radian": return self.cell_gradient.T @ ( @@ -1213,9 +1446,12 @@ def f_m_deriv(self, m) -> csr_matrix: regularization kernel function with respect to the model is given by: .. math:: - \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{L_x} - where :math:`\mathbf{L_x}` is the second-order derivative operator with respect to x. + \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = + \mathbf{L_x} \frac{\partial \mu(\mathbf{m})}{\partial \mathbf{m}} + + where :math:`\mathbf{L_x}` is the second-order derivative operator with respect + to x, and :math:`\mu` is the mapping function. Parameters ---------- @@ -1233,35 +1469,42 @@ def f_m_deriv(self, m) -> csr_matrix: is given by: .. math:: - \phi_m (\mathbf{m}) = - \Big \| \mathbf{W L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 + + \phi (\mathbf{m}) = + \left\lVert + \mathbf{W} + \mathbf{L_x} + \left[ \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) \right] + \right\rVert^2. where :math:`\mathbf{m}` are the discrete model parameters (model), - :math:`\mathbf{m}^{(ref)}` is the reference model, :math:`\mathbf{L_x}` is the - second-order x-derivative operator, and :math:`\mathbf{W}` is - the weighting matrix. Similar for smoothness along y and z. + :math:`\mathbf{m}^\text{ref}` is the reference model, :math:`\mathbf{L_x}` is + the second-order x-derivative operator, :math:`\mu` is the mapping function and + :math:`\mathbf{W}` is the weighting matrix. + Similar for smoothness along y and z. See the :class:`SmoothnessSecondOrder` class documentation for more detail. We define the regularization kernel function :math:`\mathbf{f_m}` as: .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] + + \mathbf{f_m}(\mathbf{m}) = + \mathbf{L_x} \left[ \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) \right] such that .. math:: - \phi_m (\mathbf{m}) = \Big \| \mathbf{W \, f_m} \Big \|^2 + + \phi_m(\mathbf{m}) = \lVert \mathbf{W \, f_m} \rVert^2. The derivative of the regularization kernel function with respect to the model is: .. math:: - \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{L_x} + + \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} + = \mathbf{L_x} \frac{\partial \mu(\mathbf{m})}{\partial \mathbf{m}} """ - return ( - self.cell_gradient.T - @ self.cell_gradient - @ self.mapping.deriv(self._delta_m(m)) - ) + return self.cell_gradient.T @ self.cell_gradient @ self.mapping.deriv(m) @property def W(self) -> csr_matrix: diff --git a/tests/base/regularizations/test_mappings.py b/tests/base/regularizations/test_mappings.py new file mode 100644 index 0000000000..cc23000ab3 --- /dev/null +++ b/tests/base/regularizations/test_mappings.py @@ -0,0 +1,345 @@ +""" +Test mapping functions in regularizations. +""" + +import numpy as np +import pytest +from discretize import TensorMesh +from scipy.sparse import diags + +from simpeg.maps import IdentityMap, LinearMap, LogMap +from simpeg.regularization import Smallness, SmoothnessFirstOrder, SmoothnessSecondOrder + + +@pytest.fixture +def tensor_mesh(): + hx = [(2.0, 10)] + h = [hx, hx, hx] + return TensorMesh(h) + + +@pytest.fixture +def active_cells(tensor_mesh): + return np.ones(tensor_mesh.n_cells, dtype=bool) + + +@pytest.fixture +def model(tensor_mesh): + n = tensor_mesh.n_cells + return np.linspace(1.0, 51.0, n) + + +@pytest.fixture +def reference_model(tensor_mesh): + return np.random.default_rng(seed=5959).uniform(size=tensor_mesh.n_cells) + + +class TestMappingInSmallness: + """ + Test mapping in Smallness regularization. + """ + + def test_default_mapping(self, tensor_mesh, active_cells, model, reference_model): + """ + Test regularization using the default (identity) mapping. + """ + reg = Smallness( + mesh=tensor_mesh, active_cells=active_cells, reference_model=reference_model + ) + assert isinstance(reg.mapping, IdentityMap) + volume_weights = tensor_mesh.cell_volumes + expected = np.sum(volume_weights * (model - reference_model) ** 2) + np.testing.assert_allclose(reg(model), expected) + expected_gradient = 2 * volume_weights * (model - reference_model) + np.testing.assert_allclose(reg.deriv(model), expected_gradient) + + def test_linear_mapping(self, tensor_mesh, active_cells, model, reference_model): + """ + Test regularization using a linear mapping. + """ + n_active_cells = active_cells.sum() + a = np.full(n_active_cells, 3.5) + a_matrix = diags(a) + linear_mapping = LinearMap(a_matrix, b=None) + reg = Smallness( + mesh=tensor_mesh, + active_cells=active_cells, + mapping=linear_mapping, + reference_model=reference_model, + ) + assert reg.mapping is linear_mapping + volume_weights = tensor_mesh.cell_volumes + expected = np.sum(volume_weights * (a * model - a * reference_model) ** 2) + np.testing.assert_allclose(reg(model), expected) + expected_gradient = 2 * a * volume_weights * (a * model - a * reference_model) + np.testing.assert_allclose(reg.deriv(model), expected_gradient) + + def test_nonlinear_mapping(self, tensor_mesh, active_cells, model, reference_model): + """ + Test regularization using a non-linear mapping. + """ + log_mapping = LogMap() + reg = Smallness( + mesh=tensor_mesh, + active_cells=active_cells, + mapping=log_mapping, + reference_model=reference_model, + ) + assert reg.mapping is log_mapping + volume_weights = tensor_mesh.cell_volumes + expected = np.sum( + volume_weights * (np.log(model) - np.log(reference_model)) ** 2 + ) + np.testing.assert_allclose(reg(model), expected) + expected_gradient = ( + 2 * (1 / model) * volume_weights * (np.log(model) - np.log(reference_model)) + ) + np.testing.assert_allclose(reg.deriv(model), expected_gradient) + + +@pytest.mark.parametrize( + "use_reference_model", [True, False], ids=["m_ref", "no m_ref"] +) +class TestMappingInSmoothnessFirstOrder: + """ + Test mapping in SmoothnessFirstOrder regularization. + """ + + @pytest.mark.parametrize("orientation", ["x", "y", "z"]) + def test_default_mapping( + self, + tensor_mesh, + active_cells, + model, + reference_model, + use_reference_model, + orientation, + ): + """ + Test regularization using the default (identity) mapping. + """ + reg = SmoothnessFirstOrder( + mesh=tensor_mesh, + active_cells=active_cells, + orientation=orientation, + reference_model=reference_model, + reference_model_in_smooth=use_reference_model, + ) + + # Test call + gradients = getattr(reg.regularization_mesh, f"cell_gradient_{orientation}") + model_diff = model - reference_model if use_reference_model else model + r = reg.W @ gradients @ model_diff + expected = r.T @ r + np.testing.assert_allclose(reg(model), expected) + + # Test deriv + expected_gradient = 2 * gradients.T @ reg.W.T @ reg.W @ gradients @ model_diff + np.testing.assert_allclose(reg.deriv(model), expected_gradient, atol=1e-10) + + @pytest.mark.parametrize("orientation", ["x", "y", "z"]) + def test_linear_mapping( + self, + tensor_mesh, + active_cells, + model, + reference_model, + use_reference_model, + orientation, + ): + """ + Test regularization using a linear mapping. + """ + n_active_cells = active_cells.sum() + a = np.full(n_active_cells, 3.5) + a_matrix = diags(a) + linear_mapping = LinearMap(a_matrix, b=None) + reg = SmoothnessFirstOrder( + mesh=tensor_mesh, + active_cells=active_cells, + orientation=orientation, + reference_model=reference_model, + reference_model_in_smooth=use_reference_model, + mapping=linear_mapping, + ) + assert reg.mapping is linear_mapping + + # Test call + gradients = getattr(reg.regularization_mesh, f"cell_gradient_{orientation}") + model_diff = ( + a * model - a * reference_model if use_reference_model else a * model + ) + r = reg.W @ gradients @ model_diff + expected = r.T @ r + np.testing.assert_allclose(reg(model), expected) + + # Test deriv + f_m_deriv = gradients @ a_matrix + expected_gradient = 2 * f_m_deriv.T @ reg.W.T @ reg.W @ gradients @ model_diff + np.testing.assert_allclose(reg.deriv(model), expected_gradient, atol=1e-10) + + @pytest.mark.parametrize("orientation", ["x", "y", "z"]) + def test_nonlinear_mapping( + self, + tensor_mesh, + active_cells, + model, + reference_model, + use_reference_model, + orientation, + ): + """ + Test regularization using a non-linear mapping. + """ + log_mapping = LogMap() + reg = SmoothnessFirstOrder( + mesh=tensor_mesh, + active_cells=active_cells, + orientation=orientation, + reference_model=reference_model, + reference_model_in_smooth=use_reference_model, + mapping=log_mapping, + ) + assert reg.mapping is log_mapping + + # Test call + gradients = getattr(reg.regularization_mesh, f"cell_gradient_{orientation}") + model_diff = ( + np.log(model) - np.log(reference_model) + if use_reference_model + else np.log(model) + ) + r = reg.W @ gradients @ model_diff + expected = r.T @ r + np.testing.assert_allclose(reg(model), expected) + + # Test deriv + f_m_deriv = gradients @ diags(1 / model) + expected_gradient = 2 * f_m_deriv.T @ reg.W.T @ reg.W @ gradients @ model_diff + np.testing.assert_allclose(reg.deriv(model), expected_gradient, atol=1e-10) + + +@pytest.mark.parametrize( + "use_reference_model", [True, False], ids=["m_ref", "no m_ref"] +) +class TestMappingInSmoothnessSecondOrder: + """ + Test mapping in SmoothnessSecondOrder regularization. + """ + + @pytest.mark.parametrize("orientation", ["x", "y", "z"]) + def test_default_mapping( + self, + tensor_mesh, + active_cells, + model, + reference_model, + use_reference_model, + orientation, + ): + """ + Test regularization using the default (identity) mapping. + """ + reg = SmoothnessSecondOrder( + mesh=tensor_mesh, + active_cells=active_cells, + orientation=orientation, + reference_model=reference_model, + reference_model_in_smooth=use_reference_model, + ) + + # Test call + gradients = getattr(reg.regularization_mesh, f"cell_gradient_{orientation}") + model_diff = model - reference_model if use_reference_model else model + l_matrix = gradients.T @ gradients # 2nd order derivative matrix + r = reg.W @ l_matrix @ model_diff + expected = r.T @ r + np.testing.assert_allclose(reg(model), expected) + + # Test deriv + f_m_deriv = l_matrix + expected_gradient = 2 * f_m_deriv.T @ reg.W.T @ reg.W @ l_matrix @ model_diff + np.testing.assert_allclose(reg.deriv(model), expected_gradient, atol=1e-10) + + @pytest.mark.parametrize("orientation", ["x", "y", "z"]) + def test_linear_mapping( + self, + tensor_mesh, + active_cells, + model, + reference_model, + use_reference_model, + orientation, + ): + """ + Test regularization using a linear mapping. + """ + n_active_cells = active_cells.sum() + a = np.full(n_active_cells, 3.5) + a_matrix = diags(a) + linear_mapping = LinearMap(a_matrix, b=None) + reg = SmoothnessSecondOrder( + mesh=tensor_mesh, + active_cells=active_cells, + orientation=orientation, + reference_model=reference_model, + reference_model_in_smooth=use_reference_model, + mapping=linear_mapping, + ) + assert reg.mapping is linear_mapping + + # Test call + gradients = getattr(reg.regularization_mesh, f"cell_gradient_{orientation}") + model_diff = ( + a * model - a * reference_model if use_reference_model else a * model + ) + l_matrix = gradients.T @ gradients # 2nd order derivative matrix + r = reg.W @ l_matrix @ model_diff + expected = r.T @ r + np.testing.assert_allclose(reg(model), expected) + + # Test deriv + f_m_deriv = l_matrix @ a_matrix + expected_gradient = 2 * f_m_deriv.T @ reg.W.T @ reg.W @ l_matrix @ model_diff + np.testing.assert_allclose(reg.deriv(model), expected_gradient, atol=1e-10) + + @pytest.mark.parametrize("orientation", ["x", "y", "z"]) + def test_nonlinear_mapping( + self, + tensor_mesh, + active_cells, + model, + reference_model, + use_reference_model, + orientation, + ): + """ + Test regularization using a non-linear mapping. + """ + log_mapping = LogMap() + reg = SmoothnessSecondOrder( + mesh=tensor_mesh, + active_cells=active_cells, + orientation=orientation, + reference_model=reference_model, + reference_model_in_smooth=use_reference_model, + mapping=log_mapping, + ) + assert reg.mapping is log_mapping + + # Test call + gradients = getattr(reg.regularization_mesh, f"cell_gradient_{orientation}") + model_diff = ( + np.log(model) - np.log(reference_model) + if use_reference_model + else np.log(model) + ) + l_matrix = gradients.T @ gradients # 2nd order derivative matrix + r = reg.W @ l_matrix @ model_diff + expected = r.T @ r + np.testing.assert_allclose(reg(model), expected) + + # Test deriv + f_m_deriv = l_matrix @ diags(1 / model) + expected_gradient = 2 * f_m_deriv.T @ reg.W.T @ reg.W @ l_matrix @ model_diff + np.testing.assert_allclose(reg.deriv(model), expected_gradient, atol=1e-10) From 1d7390791e48ca54a9c11f124824902bcc5ece8e Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Thu, 16 Oct 2025 14:27:53 -0600 Subject: [PATCH 182/194] Simple fix for pymatsolver 0.4.0 (#1717) #### Fixes for pymatsolver 0.4.0 #### PR Checklist * [x] If this is a work in progress PR, set as a Draft PR * [x] Linted my code according to the [style guides](https://docs.simpeg.xyz/latest/content/getting_started/contributing/code-style.html). * [x] Added [tests](https://docs.simpeg.xyz/latest/content/getting_started/contributing/testing.html) to verify changes to the code. * [x] Added necessary documentation to any new functions/classes following the expect [style](https://docs.simpeg.xyz/latest/content/getting_started/contributing/documentation.html). * [x] Marked as ready for review (if this is was a draft PR), and converted to a Pull Request * [x] Tagged ``@simpeg/simpeg-developers`` when ready for review. #### What does this implement/fix? Squeeze these two locations in the SimulationDC operation (to be consistent on output of `__inner_mat_mul `operation) #### Additional information The `__inner_mat_mul` function will "squeeze" out single length second axes, so this fix causes it to be consistent for this operation in CG. This function had to perform this way because of the way pymatsoler previously handled single length array inputs. --- simpeg/base/pde_simulation.py | 18 +++-------- .../static/resistivity/simulation.py | 10 ++---- .../static/resistivity/simulation_2d.py | 10 ++---- .../time_domain/simulation.py | 32 +++++++++++++------ 4 files changed, 32 insertions(+), 38 deletions(-) diff --git a/simpeg/base/pde_simulation.py b/simpeg/base/pde_simulation.py index 5d5360918d..7a999e4fdd 100644 --- a/simpeg/base/pde_simulation.py +++ b/simpeg/base/pde_simulation.py @@ -12,7 +12,7 @@ from ..utils import validate_type, get_default_solver, get_logger, PerformanceWarning -def __inner_mat_mul_op(M, u, v=None, adjoint=False): +def _inner_mat_mul_op(M, u, v=None, adjoint=False): u = np.squeeze(u) if sp.issparse(M): if v is not None: @@ -239,9 +239,7 @@ def MccDeriv_prop(self, u, v=None, adjoint=False): self, f"{arg.lower()}Deriv" ) setattr(self, stash_name, M_prop_deriv) - return __inner_mat_mul_op( - getattr(self, stash_name), u, v=v, adjoint=adjoint - ) + return _inner_mat_mul_op(getattr(self, stash_name), u, v=v, adjoint=adjoint) setattr(cls, f"Mcc{arg}Deriv", MccDeriv_prop) @@ -261,9 +259,7 @@ def MnDeriv_prop(self, u, v=None, adjoint=False): * getattr(self, f"{arg.lower()}Deriv") ) setattr(self, stash_name, M_prop_deriv) - return __inner_mat_mul_op( - getattr(self, stash_name), u, v=v, adjoint=adjoint - ) + return _inner_mat_mul_op(getattr(self, stash_name), u, v=v, adjoint=adjoint) setattr(cls, f"Mn{arg}Deriv", MnDeriv_prop) @@ -293,9 +289,7 @@ def MfDeriv_prop(self, u, v=None, adjoint=False): else: setattr(self, stash_name, (M_deriv_func, prop_deriv)) - return __inner_mat_mul_op( - getattr(self, stash_name), u, v=v, adjoint=adjoint - ) + return _inner_mat_mul_op(getattr(self, stash_name), u, v=v, adjoint=adjoint) setattr(cls, f"Mf{arg}Deriv", MfDeriv_prop) @@ -324,9 +318,7 @@ def MeDeriv_prop(self, u, v=None, adjoint=False): setattr(self, stash_name, M_prop_deriv) else: setattr(self, stash_name, (M_deriv_func, prop_deriv)) - return __inner_mat_mul_op( - getattr(self, stash_name), u, v=v, adjoint=adjoint - ) + return _inner_mat_mul_op(getattr(self, stash_name), u, v=v, adjoint=adjoint) setattr(cls, f"Me{arg}Deriv", MeDeriv_prop) diff --git a/simpeg/electromagnetics/static/resistivity/simulation.py b/simpeg/electromagnetics/static/resistivity/simulation.py index f901f0390c..c84939afcf 100644 --- a/simpeg/electromagnetics/static/resistivity/simulation.py +++ b/simpeg/electromagnetics/static/resistivity/simulation.py @@ -10,6 +10,7 @@ ) from ....data import Data from ....base import BaseElectricalPDESimulation +from ....base.pde_simulation import _inner_mat_mul_op from .survey import Survey from .fields import Fields3DCellCentered, Fields3DNodal from .utils import _mini_pole_pole @@ -566,14 +567,7 @@ def getADeriv(self, u, v, adjoint=False): if self.bc_type != "Neumann" and self.sigmaMap is not None: if getattr(self, "_MBC_sigma", None) is None: self._MBC_sigma = self._AvgBC @ self.sigmaDeriv - if not isinstance(u, Zero): - u = u.flatten() - if v.ndim > 1: - u = u[:, None] - if not adjoint: - out += u * (self._MBC_sigma @ v) - else: - out += self._MBC_sigma.T @ (u * v) + out += _inner_mat_mul_op(self._MBC_sigma, u, v, adjoint) return out def setBC(self): diff --git a/simpeg/electromagnetics/static/resistivity/simulation_2d.py b/simpeg/electromagnetics/static/resistivity/simulation_2d.py index 5657af8988..16b591f420 100644 --- a/simpeg/electromagnetics/static/resistivity/simulation_2d.py +++ b/simpeg/electromagnetics/static/resistivity/simulation_2d.py @@ -13,6 +13,7 @@ validate_active_indices, ) from ....base import BaseElectricalPDESimulation +from ....base.pde_simulation import _inner_mat_mul_op from ....data import Data from .survey import Survey @@ -707,14 +708,7 @@ def getADeriv(self, ky, u, v, adjoint=False): self._MBC_sigma = {} if ky not in self._MBC_sigma: self._MBC_sigma[ky] = self._AvgBC[ky] @ self.sigmaDeriv - if not isinstance(u, Zero): - u = u.flatten() - if v.ndim > 1: - u = u[:, None] - if not adjoint: - out += u * (self._MBC_sigma[ky] @ v) - else: - out += self._MBC_sigma[ky].T @ (u * v) + out += _inner_mat_mul_op(self._MBC_sigma[ky], u, v, adjoint) return out def getRHS(self, ky): diff --git a/simpeg/electromagnetics/time_domain/simulation.py b/simpeg/electromagnetics/time_domain/simulation.py index 5f84cacbee..94ad7a4b4f 100644 --- a/simpeg/electromagnetics/time_domain/simulation.py +++ b/simpeg/electromagnetics/time_domain/simulation.py @@ -403,11 +403,18 @@ def Jtvec(self, m, v, f=None): ATinv_df_duT_v[isrc, :] = ( AdiagTinv * df_duT_v[src, "{}Deriv".format(self._fieldType), tInd + 1] - ) + ).squeeze() elif tInd > -1: - ATinv_df_duT_v[isrc, :] = AdiagTinv * ( - mkvc(df_duT_v[src, "{}Deriv".format(self._fieldType), tInd + 1]) - - Asubdiag.T * mkvc(ATinv_df_duT_v[isrc, :]) + ATinv_df_duT_v[isrc, :] = ( + AdiagTinv + * ( + mkvc( + df_duT_v[ + src, "{}Deriv".format(self._fieldType), tInd + 1 + ] + ) + - Asubdiag.T * mkvc(ATinv_df_duT_v[isrc, :]) + ).squeeze() ) dAsubdiagT_dm_v = self.getAsubdiagDeriv( @@ -1292,11 +1299,18 @@ def Jtvec(self, m, v, f=None): ATinv_df_duT_v[isrc, :] = ( AdiagTinv * df_duT_v[src, "{}Deriv".format(self._fieldType), tInd + 1] - ) + ).squeeze() elif tInd > -1: - ATinv_df_duT_v[isrc, :] = AdiagTinv * ( - mkvc(df_duT_v[src, "{}Deriv".format(self._fieldType), tInd + 1]) - - Asubdiag.T * mkvc(ATinv_df_duT_v[isrc, :]) + ATinv_df_duT_v[isrc, :] = ( + AdiagTinv + * ( + mkvc( + df_duT_v[ + src, "{}Deriv".format(self._fieldType), tInd + 1 + ] + ) + - Asubdiag.T * mkvc(ATinv_df_duT_v[isrc, :]) + ).squeeze() ) dAsubdiagT_dm_v = self.getAsubdiagDeriv( @@ -1333,7 +1347,7 @@ def Jtvec(self, m, v, f=None): ) - Asubdiag.T * mkvc(ATinv_df_duT_v[isrc, :]) ) - ) + ).squeeze() ) dRHST_dm_v = self.getRHSDeriv( From 6e3ddb0ca35152410238a672937372877de58138 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 17 Oct 2025 16:17:56 +0000 Subject: [PATCH 183/194] Fix bug with duplicated current in `LineCurrent.Mejs` (#1718) Remove the duplicated current being multiplied when computing the `Mejs` in `LineCurrent`. Add a test that checks that the electric fields are linear in the current. --- .../frequency_domain/sources.py | 2 +- tests/em/fdem/forward/test_FDEM_sources.py | 34 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/simpeg/electromagnetics/frequency_domain/sources.py b/simpeg/electromagnetics/frequency_domain/sources.py index 8b71287245..dc6624823b 100644 --- a/simpeg/electromagnetics/frequency_domain/sources.py +++ b/simpeg/electromagnetics/frequency_domain/sources.py @@ -1332,7 +1332,7 @@ def Mejs(self, simulation): if getattr(self, "_Mejs", None) is None: mesh = simulation.mesh locs = self.location - self._Mejs = self.current * segmented_line_current_source_term(mesh, locs) + self._Mejs = segmented_line_current_source_term(mesh, locs) return self.current * self._Mejs def Mfjs(self, simulation): diff --git a/tests/em/fdem/forward/test_FDEM_sources.py b/tests/em/fdem/forward/test_FDEM_sources.py index 640790e534..07fa24bb62 100644 --- a/tests/em/fdem/forward/test_FDEM_sources.py +++ b/tests/em/fdem/forward/test_FDEM_sources.py @@ -384,3 +384,37 @@ def test_line_current_failures(): ) with pytest.raises(ValueError): fdem.sources.LineCurrent([rx], 10, tx_locs) + + +class TestBugFixDuplicatedCurrent: + """Test that the duplicated current in LinearCurrent.Mejs has been removed.""" + + sigma = 1e-2 + + def compute_e_field(self, current): + """ + Calculate electric field on a mesh with a line current as the source. + """ + hx = [(20.0, 10)] + mesh = discretize.TensorMesh([hx, hx, hx], origin="CCC") + line_path = np.array([[-50, 0, -50], [50, 0, -50]]) + source = fdem.sources.LineCurrent( + location=line_path, current=current, frequency=1.0, receiver_list=[] + ) + survey = fdem.Survey([source]) + simulation = fdem.Simulation3DElectricField( + mesh=mesh, survey=survey, sigma=self.sigma + ) + fields = simulation.fields() + return fields[source, "e"] + + def test_fields_linear_on_current(self): + """ + Test that the electric fields are linear on the current. + + When the current is multiplied twice, the fields are not linear. + """ + current_1, current_2 = 2.5, 10.3 + expected = self.compute_e_field(current_1 + current_2) + actual = self.compute_e_field(current_1) + self.compute_e_field(current_2) + np.testing.assert_allclose(expected, actual, atol=1e-14) From 5c17fd288891c0d8b71e0c5e1f1f92c153c0ed96 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 17 Oct 2025 18:39:49 +0000 Subject: [PATCH 184/194] Minor fixes to LaTeX equations in regularizations (#1720) Apply some fixes to LaTeX equations in some regularization classes. --- simpeg/regularization/base.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/simpeg/regularization/base.py b/simpeg/regularization/base.py index 882d2ef2ef..54d939cfb8 100644 --- a/simpeg/regularization/base.py +++ b/simpeg/regularization/base.py @@ -858,10 +858,10 @@ class SmoothnessFirstOrder(BaseRegularization): .. math:: \phi(\mathbf{m}) = - \lVert + \left\lVert \mathbf{W G_x} \left[ \mathbf{m} - \mathbf{m}^\text{ref} \right] - \rVert^2 + \right\rVert^2 This functionality is used by setting a reference model with the `reference_model` property, and by setting the `reference_model_in_smooth` parameter to ``True``. @@ -876,10 +876,10 @@ class SmoothnessFirstOrder(BaseRegularization): .. math:: \phi (m) = \int_\Omega \, w(\mathbf{r}) \, - \lvert + \left\lvert \frac{\partial}{\partial x} \left[ \mu(m) - \mu(m^\text{ref}) \right] - \rvert^2 \, d\mathbf{r} + \right\rvert^2 \, d\mathbf{r} In matrix form, the previous equation is expressed as: @@ -1301,10 +1301,10 @@ class SmoothnessSecondOrder(SmoothnessFirstOrder): .. math:: \phi(\mathbf{m}) = - \lVert + \left\lVert \mathbf{W L_x} \left[ \mathbf{m} - \mathbf{m}^\text{ref} \right] - \rVert^2 + \right\rVert^2 This functionality is used by setting a reference model with the `reference_model` property, and by setting the `reference_model_in_smooth` parameter to ``True``. @@ -1319,17 +1319,17 @@ class SmoothnessSecondOrder(SmoothnessFirstOrder): .. math:: \phi (m) = \int_\Omega \, w(\mathbf{r}) \, - \lvert + \left\lvert \frac{\partial^2}{\partial x^2} \left[ \mu(m) - \mu(m^\text{ref}) \right] - t\rvert^2 \, d\mathbf{r} + \right\rvert^2 \, d\mathbf{r} In matrix form, the previous equation is expressed as: .. math:: \phi (\mathbf{m}) = - \lVert + \left\lVert \mathbf{W} \mathbf{L_x} \left[ \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) \right] From 656f31c65df1d48a8119eaba7bf2f4af1feec995 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 17 Oct 2025 20:11:12 +0000 Subject: [PATCH 185/194] Fix return of `get_indices_block` (#1713) Make the `get_indices_block` function to always return an array of integers for the cells that belong to the specified block, instead of returning a tuple with just a single array. Improve its implementation. Fix it for 1D mesh since it wasn't working properly. Improve error messages: raise better errors than just `AssertError`s and improve messages. Avoid overwriting the `p0` and `p1` arrays in-place if they are not provided in the right order (west-south-bottom and then east-north-top). Add tests for the function. Update usage of the function across tests and examples. This introduces a backward incompatible change. --- examples/02-gravity/plot_inv_grav_tiled.py | 2 +- .../plot_inv_mag_MVI_Sparse_TreeMesh.py | 6 +- .../plot_inv_mag_MVI_VectorAmplitude.py | 2 +- .../plot_inv_mag_nonLinear_Amplitude.py | 2 +- pyproject.toml | 1 + simpeg/utils/__init__.py | 3 +- simpeg/utils/model_builder.py | 110 ++++++++++++------ simpeg/utils/warnings.py | 8 +- tests/dask/test_mag_MVI_Octree.py | 8 +- tests/dask/test_mag_nonLinear_Amplitude.py | 2 +- tests/em/static/test_DC_2D_analytic.py | 4 +- .../static/test_DC_FieldsDipoleFullspace.py | 8 +- .../test_DC_FieldsMultipoleFullspace.py | 8 +- tests/pf/test_mag_MVI_Octree.py | 6 +- tests/pf/test_mag_nonLinear_Amplitude.py | 2 +- tests/pf/test_mag_vector_amplitude.py | 8 +- tests/utils/test_model_builder.py | 70 ++++++++++- 17 files changed, 183 insertions(+), 67 deletions(-) diff --git a/examples/02-gravity/plot_inv_grav_tiled.py b/examples/02-gravity/plot_inv_grav_tiled.py index 816315ae80..2178532cc3 100644 --- a/examples/02-gravity/plot_inv_grav_tiled.py +++ b/examples/02-gravity/plot_inv_grav_tiled.py @@ -120,7 +120,7 @@ np.r_[-10, -10, -30], np.r_[10, 10, -10], mesh.gridCC, -)[0] +) # Assign magnetization values model[ind] = 0.3 diff --git a/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py b/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py index 7f465146bf..02088a0950 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py @@ -132,15 +132,15 @@ # Convert the inclination declination to vector in Cartesian M_xyz = utils.mat_utils.dip_azimuth2cartesian(M[0], M[1]) -# Get the indicies of the magnetized block +# Get the indices of the magnetized block ind = utils.model_builder.get_indices_block( np.r_[-20, -20, -10], np.r_[20, 20, 25], mesh.gridCC, -)[0] +) # Assign magnetization values -model[ind, :] = np.kron(np.ones((ind.shape[0], 1)), M_xyz * 0.05) +model[ind, :] = np.kron(np.ones((ind.size, 1)), M_xyz * 0.05) # Remove air cells model = model[actv, :] diff --git a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py index c2e76d5e60..5cf55b5eeb 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py @@ -106,7 +106,7 @@ np.r_[-30, -20, -10], np.r_[30, 20, 25], mesh.gridCC, -)[0] +) model_amp[ind] = 0.05 model_azm_dip[ind, 0] = 45.0 model_azm_dip[ind, 1] = 90.0 diff --git a/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py b/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py index 263697df88..643df614ca 100644 --- a/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py +++ b/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py @@ -134,7 +134,7 @@ np.r_[-20, -20, -10], np.r_[20, 20, 25], mesh.gridCC, -)[0] +) # Assign magnetization value, inducing field strength will # be applied in by the :class:`simpeg.PF.Magnetics` problem diff --git a/pyproject.toml b/pyproject.toml index 538672381f..3d54d4d354 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -263,5 +263,6 @@ filterwarnings = [ "error:The `index_dictionary` property has been deprecated:FutureWarning", 'error:The `simpeg\.directives\.[a-z_]+` submodule has been deprecated', 'error:Casting complex values to real discards the imaginary part', + 'ignore::simpeg.utils.BreakingChangeWarning', ] xfail_strict = true diff --git a/simpeg/utils/__init__.py b/simpeg/utils/__init__.py index bb3afdd470..b6b4992b0d 100644 --- a/simpeg/utils/__init__.py +++ b/simpeg/utils/__init__.py @@ -170,6 +170,7 @@ .. autosummary:: :toctree: generated/ + BreakingChangeWarning PerformanceWarning """ @@ -267,4 +268,4 @@ GaussianMixtureWithNonlinearRelationshipsWithPrior, ) from .solver_utils import get_default_solver, set_default_solver -from .warnings import PerformanceWarning +from .warnings import BreakingChangeWarning, PerformanceWarning diff --git a/simpeg/utils/model_builder.py b/simpeg/utils/model_builder.py index 97f785fedb..0e31cfdc99 100644 --- a/simpeg/utils/model_builder.py +++ b/simpeg/utils/model_builder.py @@ -1,3 +1,4 @@ +import warnings import numpy as np import scipy.ndimage as ndi import scipy.sparse as sp @@ -6,6 +7,7 @@ from discretize.base import BaseMesh from ..typing import RandomSeed +from ..utils.warnings import BreakingChangeWarning def add_block(cell_centers, model, p0, p1, prop_value): @@ -52,7 +54,7 @@ def get_indices_block(p0, p1, cell_centers): Returns ------- - tuple of int + array of int Indices of the cells whose center lie within the specified block """ @@ -61,51 +63,89 @@ def get_indices_block(p0, p1, cell_centers): cell_centers = cell_centers.cell_centers # Validation: p0 and p1 live in the same dimensional space - assert len(p0) == len(p1), "Dimension mismatch. len(p0) != len(p1)" + if len(p0) != len(p1): + msg = ( + "Dimension mismatch between `p0` and `p1`. " + f"`p0` has {len(p0)} elements, while " + f"`p1` has {len(p0)} elements. " + "They should have the same amount of elements." + ) + raise ValueError(msg) # Validation: mesh and points live in the same dimensional space - dimMesh = np.size(cell_centers[0, :]) - assert len(p0) == dimMesh, "Dimension mismatch. len(p0) != dimMesh" + mesh_ndim = len(p0) + if cell_centers.size % mesh_ndim != 0: + msg = ( + "Dimension mismatch between `cell_centers` and dimensions of block " + "corners. " + f"The `cell_centers` have {cell_centers.size} elements that don't match " + f"the expected number of dimensions of the block corners ({len(p0)})." + ) + raise ValueError(msg) - for ii in range(len(p0)): - p0[ii], p1[ii] = np.min([p0[ii], p1[ii]]), np.max([p0[ii], p1[ii]]) + # Redefine the block corners to ensure they are correctly defined as min and max + p0_new = [min(p0[i], p1[i]) for i in range(mesh_ndim)] + p1_new = [max(p0[i], p1[i]) for i in range(mesh_ndim)] - if dimMesh == 1: + if mesh_ndim == 1: # Define the reference points - x1 = p0[0] - x2 = p1[0] + (x1,) = p0_new + (x2,) = p1_new - indX = (x1 <= cell_centers[:, 0]) & (cell_centers[:, 0] <= x2) - ind = np.where(indX) + x_centers = cell_centers + indX = (x1 <= x_centers) & (x_centers <= x2) + (ind,) = np.where(indX) - elif dimMesh == 2: + elif mesh_ndim == 2: # Define the reference points - x1 = p0[0] - y1 = p0[1] - - x2 = p1[0] - y2 = p1[1] + x1, y1 = p0_new + x2, y2 = p1_new - indX = (x1 <= cell_centers[:, 0]) & (cell_centers[:, 0] <= x2) - indY = (y1 <= cell_centers[:, 1]) & (cell_centers[:, 1] <= y2) + x_centers = cell_centers[:, 0] + y_centers = cell_centers[:, 1] + indX = (x1 <= x_centers) & (x_centers <= x2) + indY = (y1 <= y_centers) & (y_centers <= y2) - ind = np.where(indX & indY) + (ind,) = np.where(indX & indY) - elif dimMesh == 3: + elif mesh_ndim == 3: # Define the points - x1 = p0[0] - y1 = p0[1] - z1 = p0[2] - - x2 = p1[0] - y2 = p1[1] - z2 = p1[2] - - indX = (x1 <= cell_centers[:, 0]) & (cell_centers[:, 0] <= x2) - indY = (y1 <= cell_centers[:, 1]) & (cell_centers[:, 1] <= y2) - indZ = (z1 <= cell_centers[:, 2]) & (cell_centers[:, 2] <= z2) - - ind = np.where(indX & indY & indZ) + x1, y1, z1 = p0_new + x2, y2, z2 = p1_new + + x_centers = cell_centers[:, 0] + y_centers = cell_centers[:, 1] + z_centers = cell_centers[:, 2] + indX = (x1 <= x_centers) & (x_centers <= x2) + indY = (y1 <= y_centers) & (y_centers <= y2) + indZ = (z1 <= z_centers) & (z_centers <= z2) + + (ind,) = np.where(indX & indY & indZ) + + # Warn users about the breaking change introduced in the return of this function + msg = ( + "Since SimPEG v0.25.0, the 'get_indices_block' function returns a single array " + "with the cell indices, instead of a tuple with a single element. " + "This means that we don't need to unpack the tuple anymore to access to the " + "cell indices." + "\n" + "If you were using this function as in:" + "\n\n" + " ind = get_indices_block(p0, p1, mesh.cell_centers)[0]" + "\n\n" + "Make sure you update it to:" + "\n\n" + " ind = get_indices_block(p0, p1, mesh.cell_centers)" + "\n\n" + "To hide this warning, add this to your script or notebook:" + "\n" + "\n import warnings" + "\n from simpeg.utils import BreakingChangeWarning" + "\n" + "\n warnings.filterwarnings(action='ignore', category=BreakingChangeWarning)" + "\n" + ) + warnings.warn(msg, BreakingChangeWarning, stacklevel=2) # Return a tuple return ind @@ -220,7 +260,7 @@ def get_indices_sphere(center, radius, cell_centers): Returns ------- - tuple of int + array of int Indices of the cells whose center lie within the specified sphere """ diff --git a/simpeg/utils/warnings.py b/simpeg/utils/warnings.py index c6ead5491d..cea674e8f7 100644 --- a/simpeg/utils/warnings.py +++ b/simpeg/utils/warnings.py @@ -2,7 +2,13 @@ Custom warnings that can be used across SimPEG. """ -__all__ = ["PerformanceWarning"] +__all__ = ["BreakingChangeWarning", "PerformanceWarning"] + + +class BreakingChangeWarning(Warning): + """ + Warning to let users know about a breaking change that was introduced. + """ class PerformanceWarning(Warning): diff --git a/tests/dask/test_mag_MVI_Octree.py b/tests/dask/test_mag_MVI_Octree.py index cb337e8437..5516589dd5 100644 --- a/tests/dask/test_mag_MVI_Octree.py +++ b/tests/dask/test_mag_MVI_Octree.py @@ -75,15 +75,15 @@ def setUp(self): # Convert the inclination declination to vector in Cartesian M_xyz = utils.mat_utils.dip_azimuth2cartesian(M[0], M[1]) - # Get the indicies of the magnetized block - ind = utils.model_builder.get_indices_block( + # Get the indices of the magnetized block + indices = utils.model_builder.get_indices_block( np.r_[-20, -20, -10], np.r_[20, 20, 25], mesh.gridCC, - )[0] + ) # Assign magnetization values - model[ind, :] = np.kron(np.ones((ind.shape[0], 1)), M_xyz * 0.05) + model[indices, :] = np.kron(np.ones((indices.size, 1)), M_xyz * 0.05) # Remove air cells self.model = model[actv, :] diff --git a/tests/dask/test_mag_nonLinear_Amplitude.py b/tests/dask/test_mag_nonLinear_Amplitude.py index d78993048c..8da94479b1 100644 --- a/tests/dask/test_mag_nonLinear_Amplitude.py +++ b/tests/dask/test_mag_nonLinear_Amplitude.py @@ -83,7 +83,7 @@ def setUp(self): np.r_[-20, -20, -10], np.r_[20, 20, 25], mesh.gridCC, - )[0] + ) # Assign magnetization value, inducing field strength will # be applied in by the :class:`simpeg.PF.Magnetics` problem diff --git a/tests/em/static/test_DC_2D_analytic.py b/tests/em/static/test_DC_2D_analytic.py index c53fce458b..0c93b80360 100644 --- a/tests/em/static/test_DC_2D_analytic.py +++ b/tests/em/static/test_DC_2D_analytic.py @@ -292,14 +292,14 @@ def setUp(self): ROI_large_TSE = np.array([200, 0]) ROI_largeInds = utils.model_builder.get_indices_block( ROI_large_BNW, ROI_large_TSE, mesh.gridN - )[0] + ) # print(ROI_largeInds.shape) ROI_small_BNW = np.array([-50, -25]) ROI_small_TSE = np.array([50, 0]) ROI_smallInds = utils.model_builder.get_indices_block( ROI_small_BNW, ROI_small_TSE, mesh.gridN - )[0] + ) # print(ROI_smallInds.shape) ROI_inds = np.setdiff1d(ROI_largeInds, ROI_smallInds) diff --git a/tests/em/static/test_DC_FieldsDipoleFullspace.py b/tests/em/static/test_DC_FieldsDipoleFullspace.py index d36a43595e..a81b8f34c7 100644 --- a/tests/em/static/test_DC_FieldsDipoleFullspace.py +++ b/tests/em/static/test_DC_FieldsDipoleFullspace.py @@ -74,14 +74,14 @@ def setUp(self): ROI_large_TSE = np.array([75, -75, 75]) ROI_largeInds = utils.model_builder.get_indices_block( ROI_large_BNW, ROI_large_TSE, faceGrid - )[0] + ) # print(ROI_largeInds.shape) ROI_small_BNW = np.array([-4, 4, -4]) ROI_small_TSE = np.array([4, -4, 4]) ROI_smallInds = utils.model_builder.get_indices_block( ROI_small_BNW, ROI_small_TSE, faceGrid - )[0] + ) # print(ROI_smallInds.shape) ROIfaceInds = np.setdiff1d(ROI_largeInds, ROI_smallInds) @@ -244,14 +244,14 @@ def setUp(self): ROI_large_TSE = np.array([75, -75, 75]) ROI_largeInds = utils.model_builder.get_indices_block( ROI_large_BNW, ROI_large_TSE, edgeGrid - )[0] + ) # print(ROI_largeInds.shape) ROI_small_BNW = np.array([-4, 4, -4]) ROI_small_TSE = np.array([4, -4, 4]) ROI_smallInds = utils.model_builder.get_indices_block( ROI_small_BNW, ROI_small_TSE, edgeGrid - )[0] + ) # print(ROI_smallInds.shape) ROIedgeInds = np.setdiff1d(ROI_largeInds, ROI_smallInds) diff --git a/tests/em/static/test_DC_FieldsMultipoleFullspace.py b/tests/em/static/test_DC_FieldsMultipoleFullspace.py index d0ff665636..4f9c63ed04 100644 --- a/tests/em/static/test_DC_FieldsMultipoleFullspace.py +++ b/tests/em/static/test_DC_FieldsMultipoleFullspace.py @@ -101,14 +101,14 @@ def setUp(self): ROI_large_TSE = np.array([75, -75, 75]) ROI_largeInds = utils.model_builder.get_indices_block( ROI_large_BNW, ROI_large_TSE, faceGrid - )[0] + ) # print(ROI_largeInds.shape) ROI_small_BNW = np.array([-4, 4, -4]) ROI_small_TSE = np.array([4, -4, 4]) ROI_smallInds = utils.model_builder.get_indices_block( ROI_small_BNW, ROI_small_TSE, faceGrid - )[0] + ) # print(ROI_smallInds.shape) ROIfaceInds = np.setdiff1d(ROI_largeInds, ROI_smallInds) @@ -272,14 +272,14 @@ def setUp(self): ROI_large_TSE = np.array([75, -75, 75]) ROI_largeInds = utils.model_builder.get_indices_block( ROI_large_BNW, ROI_large_TSE, edgeGrid - )[0] + ) # print(ROI_largeInds.shape) ROI_small_BNW = np.array([-4, 4, -4]) ROI_small_TSE = np.array([4, -4, 4]) ROI_smallInds = utils.model_builder.get_indices_block( ROI_small_BNW, ROI_small_TSE, edgeGrid - )[0] + ) # print(ROI_smallInds.shape) ROIedgeInds = np.setdiff1d(ROI_largeInds, ROI_smallInds) diff --git a/tests/pf/test_mag_MVI_Octree.py b/tests/pf/test_mag_MVI_Octree.py index 40a2e89850..667e6d05fa 100644 --- a/tests/pf/test_mag_MVI_Octree.py +++ b/tests/pf/test_mag_MVI_Octree.py @@ -74,14 +74,14 @@ def setUp(self): M_xyz = utils.mat_utils.dip_azimuth2cartesian(M[0], M[1]) # Get the indicies of the magnetized block - ind = utils.model_builder.get_indices_block( + indices = utils.model_builder.get_indices_block( np.r_[-20, -20, -10], np.r_[20, 20, 25], mesh.gridCC, - )[0] + ) # Assign magnetization values - model[ind, :] = np.kron(np.ones((ind.shape[0], 1)), M_xyz * 0.05) + model[indices, :] = np.kron(np.ones((indices.size, 1)), M_xyz * 0.05) # Remove air cells self.model = model[actv, :] diff --git a/tests/pf/test_mag_nonLinear_Amplitude.py b/tests/pf/test_mag_nonLinear_Amplitude.py index e43596926f..80033d516b 100644 --- a/tests/pf/test_mag_nonLinear_Amplitude.py +++ b/tests/pf/test_mag_nonLinear_Amplitude.py @@ -84,7 +84,7 @@ def setUp(self): np.r_[-20, -20, -10], np.r_[20, 20, 25], mesh.gridCC, - )[0] + ) # Assign magnetization value, inducing field strength will # be applied in by the :class:`simpeg.PF.Magnetics` problem diff --git a/tests/pf/test_mag_vector_amplitude.py b/tests/pf/test_mag_vector_amplitude.py index 12b52097e7..87ca2adf7c 100644 --- a/tests/pf/test_mag_vector_amplitude.py +++ b/tests/pf/test_mag_vector_amplitude.py @@ -75,15 +75,15 @@ def setUp(self): # Convert the inclination declination to vector in Cartesian M_xyz = utils.mat_utils.dip_azimuth2cartesian(M[0], M[1]) - # Get the indicies of the magnetized block - ind = utils.model_builder.get_indices_block( + # Get the indices of the magnetized block + indices = utils.model_builder.get_indices_block( np.r_[-20, -20, -10], np.r_[20, 20, 25], mesh.gridCC, - )[0] + ) # Assign magnetization values - model[ind, :] = np.kron(np.ones((ind.shape[0], 1)), M_xyz * 0.05) + model[indices, :] = np.kron(np.ones((indices.size, 1)), M_xyz * 0.05) # Remove air cells self.model = model[actv, :] diff --git a/tests/utils/test_model_builder.py b/tests/utils/test_model_builder.py index 6530b815d3..aa9f823b0a 100644 --- a/tests/utils/test_model_builder.py +++ b/tests/utils/test_model_builder.py @@ -2,8 +2,11 @@ Test functions in model_builder. """ +import re import pytest -from simpeg.utils.model_builder import create_random_model +import numpy as np +import discretize +from simpeg.utils.model_builder import create_random_model, get_indices_block class TestRemovalSeedProperty: @@ -32,3 +35,68 @@ def test_error_invalid_kwarg(self, shape): msg = "Invalid arguments 'foo', 'bar'." with pytest.raises(TypeError, match=msg): create_random_model(shape, **kwargs) + + +class TestGetIndicesBlock: + """ + Test the ``get_indices_block`` function. + """ + + block_cells_per_dim = 2 + + def get_mesh_and_block_corners(self, ndims): + + p0_template = [-4, 2, -10] + if ndims == 1: + origin = "C" + p0 = np.array(p0_template[:1]) + elif ndims == 2: + origin = "CC" + p0 = np.array(p0_template[:2]) + else: + origin = "CCN" + p0 = np.array(p0_template) + + cell_size = 2.0 + hx = [(cell_size, 10)] + h = [hx for _ in range(ndims)] + mesh = discretize.TensorMesh(h, origin=origin) + + p1 = np.array([c + self.block_cells_per_dim * cell_size for c in p0]) + return (mesh, p0, p1) + + @pytest.mark.parametrize("ndim", [1, 2, 3]) + def test_get_indices_block(self, ndim): + """Test the funciton returns the right indices.""" + mesh, p0, p1 = self.get_mesh_and_block_corners(ndim) + indices = get_indices_block(p0, p1, mesh.cell_centers) + assert len(indices) == self.block_cells_per_dim**ndim + + def test_invalid_p0_p1(self): + # Dummy mesh (not really used) + hx = [(2.0, 10)] + mesh = discretize.TensorMesh([hx]) + + # Define block corners with different amount of elements + p0 = np.array([1.0, 2.0, 3.0, 4.0]) + p1 = np.array([10.0, 11.0, 12.0]) + + msg = re.escape("Dimension mismatch between `p0` and `p1`.") + with pytest.raises(ValueError, match=msg): + get_indices_block(p0, p1, mesh.cell_centers) + + def test_invalid_mesh_dimensions(self): + # Define a 2d mesh + hx = [(2.0, 10)] + mesh = discretize.TensorMesh([hx, hx]) + + # Define block corners in 3d + p0 = np.array([1.0, 2.0, 3.0]) + p1 = np.array([2.0, 3.0, 4.0]) + + msg = re.escape( + "Dimension mismatch between `cell_centers` and dimensions of " + "block corners." + ) + with pytest.raises(ValueError, match=msg): + get_indices_block(p0, p1, mesh.cell_centers) From 1845349da224ea9d4d2d631eb1fe3766b2b889ab Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Fri, 17 Oct 2025 21:09:41 +0000 Subject: [PATCH 186/194] Remove deprecated bits marked for removal in v0.25.0 (#1719) Remove the `Data.index_dictionary` property. Remove the `gtg_diagonal` property from gravity simulation. Remove the `components` property from gravity and magnetic surveys. Remove the `HasModel.deleteTheseOnModelUpdate` property. --- pyproject.toml | 1 - simpeg/data.py | 58 +------------------ simpeg/potential_fields/gravity/simulation.py | 19 ------ simpeg/potential_fields/gravity/survey.py | 34 ----------- simpeg/potential_fields/magnetics/survey.py | 37 ------------ simpeg/props.py | 2 +- tests/base/props/test_has_model.py | 8 ++- tests/base/test_survey_data.py | 26 --------- tests/pf/test_components.py | 55 ------------------ tests/pf/test_forward_Grav_Linear.py | 23 -------- 10 files changed, 7 insertions(+), 256 deletions(-) delete mode 100644 tests/pf/test_components.py diff --git a/pyproject.toml b/pyproject.toml index 3d54d4d354..4205bb7b56 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -260,7 +260,6 @@ rst-roles = [ [tool.pytest.ini_options] filterwarnings = [ "error:You are running a pytest without setting a random seed.*:UserWarning", - "error:The `index_dictionary` property has been deprecated:FutureWarning", 'error:The `simpeg\.directives\.[a-z_]+` submodule has been deprecated', 'error:Casting complex values to real discards the imaginary part', 'ignore::simpeg.utils.BreakingChangeWarning', diff --git a/simpeg/data.py b/simpeg/data.py index b7732a320a..b461e455ba 100644 --- a/simpeg/data.py +++ b/simpeg/data.py @@ -4,14 +4,6 @@ from .survey import BaseSurvey from .utils import mkvc, validate_ndarray_with_shape, validate_float, validate_type -try: - from warnings import deprecated -except ImportError: - # Use the deprecated decorator provided by typing_extensions (which - # supports older versions of Python) if it cannot be imported from - # warnings. - from typing_extensions import deprecated - __all__ = ["Data", "SyntheticData"] @@ -292,53 +284,6 @@ def shape(self): """ return self.dobs.shape - @property - @deprecated( - "The `index_dictionary` property has been deprecated. " - "Use the `get_slice()` or `get_all_slices()` methods provided " - "by the Survey object instead." - "This property will be removed in SimPEG v0.25.0.", - category=FutureWarning, - ) - def index_dictionary(self): - """Dictionary for indexing data by sources and receiver. - - FULL DESCRIPTION REQUIRED - - Returns - ------- - dict - Dictionary for indexing data by source and receiver - - Examples - -------- - NEED EXAMPLE (1D TEM WOULD BE GOOD) - - """ - if getattr(self, "_index_dictionary", None) is None: - if self.survey is None: - raise Exception( - "To set or get values by source-receiver pairs, a survey must " - "first be set. `data.survey = survey`" - ) - - # create an empty dict - self._index_dictionary = {} - - # create an empty dict associated with each source - for src in self.survey.source_list: - self._index_dictionary[src] = {} - - # loop over sources and find the associated data indices - indBot, indTop = 0, 0 - for src in self.survey.source_list: - for rx in src.receiver_list: - indTop += rx.nD - self._index_dictionary[src][rx] = np.arange(indBot, indTop) - indBot += rx.nD - - return self._index_dictionary - ########################## # Methods ########################## @@ -429,12 +374,11 @@ def dclean(self): Notes -------- This array should be indexing the data object - using the a tuple of the survey's sources and receivers. + using a tuple of the survey's sources and receivers. >>> data = Data(survey) >>> for src in survey.source_list: ... for rx in src.receiver_list: - ... index = data.index_dictionary(src, rx) ... data.dclean[src, rx] = datum """ diff --git a/simpeg/potential_fields/gravity/simulation.py b/simpeg/potential_fields/gravity/simulation.py index a5ebb7b0e0..434ba52383 100644 --- a/simpeg/potential_fields/gravity/simulation.py +++ b/simpeg/potential_fields/gravity/simulation.py @@ -16,13 +16,6 @@ from ._numba import choclo, NUMBA_FUNCTIONS_3D, NUMBA_FUNCTIONS_2D -try: - from warnings import deprecated -except ImportError: - # Use the deprecated decorator provided by typing_extensions (which - # supports older versions of Python) if it cannot be imported from - # warnings. - from typing_extensions import deprecated if choclo is not None: from numba import jit @@ -449,18 +442,6 @@ def G(self) -> NDArray | np.memmap | LinearOperator: self._G = self.linear_operator() return self._G - @property - @deprecated( - "The `gtg_diagonal` property has been deprecated. " - "It will be removed in SimPEG v0.25.0.", - category=FutureWarning, - ) - def gtg_diagonal(self): - """ - Diagonal of GtG - """ - return getattr(self, "_gtg_diagonal", None) - def evaluate_integral(self, receiver_location, components): """ Compute the forward linear relationship between the model and the physics at a point diff --git a/simpeg/potential_fields/gravity/survey.py b/simpeg/potential_fields/gravity/survey.py index a14f61296a..7a9cb84c2a 100644 --- a/simpeg/potential_fields/gravity/survey.py +++ b/simpeg/potential_fields/gravity/survey.py @@ -2,14 +2,6 @@ from ...utils.code_utils import validate_list_of_types from .sources import SourceField -try: - from warnings import deprecated -except ImportError: - # Use the deprecated decorator provided by typing_extensions (which - # supports older versions of Python) if it cannot be imported from - # warnings. - from typing_extensions import deprecated - class Survey(BaseSurvey): """Base Gravity Survey @@ -106,32 +98,6 @@ def nD(self): """ return sum(receiver.nD for receiver in self.source_field.receiver_list) - @property - @deprecated( - "The `components` property is deprecated, " - "and will be removed in SimPEG v0.25.0. " - "Within a gravity survey, receivers can contain different components. " - "Iterate over the sources and receivers in the survey to get " - "information about their components.", - category=FutureWarning, - ) - def components(self): - """Number of components measured at each receiver. - - .. deprecated:: 0.24.0 - - The `components` property is deprecated, and will be removed in - SimPEG v0.25.0. Within a gravity survey, receivers can contain - different components. Iterate over the sources and receivers in the - survey to get information about their components. - - Returns - ------- - int - Number of components measured at each receiver. - """ - return self.source_field.receiver_list[0].components - def _location_component_iterator(self): for rx in self.source_field.receiver_list: for loc in rx.locations: diff --git a/simpeg/potential_fields/magnetics/survey.py b/simpeg/potential_fields/magnetics/survey.py index c55bfcf653..d06f090811 100644 --- a/simpeg/potential_fields/magnetics/survey.py +++ b/simpeg/potential_fields/magnetics/survey.py @@ -3,14 +3,6 @@ from ...utils.code_utils import validate_list_of_types from .sources import UniformBackgroundField -try: - from warnings import deprecated -except ImportError: - # Use the deprecated decorator provided by typing_extensions (which - # supports older versions of Python) if it cannot be imported from - # warnings. - from typing_extensions import deprecated - class Survey(BaseSurvey): """Base Magnetics Survey @@ -105,35 +97,6 @@ def nD(self): """ return sum(rx.nD for rx in self.source_field.receiver_list) - @property - @deprecated( - "The `components` property is deprecated, " - "and will be removed in SimPEG v0.25.0. " - "Within a magnetic survey, receivers can contain different components. " - "Iterate over the sources and receivers in the survey to get " - "information about their components.", - category=FutureWarning, - ) - def components(self): - """Field components - - .. deprecated:: 0.24.0 - - The `components` property is deprecated, and will be removed in - SimPEG v0.25.0. Within a magnetic survey, receivers can contain - different components. Iterate over the sources and receivers in the - survey to get information about their components. - - Returns - ------- - list of str - Components of the field being measured - """ - comps = [] - for rx in self.source_field.receiver_list: - comps += rx.components - return comps - def _location_component_iterator(self): for rx in self.source_field.receiver_list: for loc in rx.locations: diff --git a/simpeg/props.py b/simpeg/props.py index 1d9052c4f3..ee9ac01ce3 100644 --- a/simpeg/props.py +++ b/simpeg/props.py @@ -375,7 +375,7 @@ def _delete_on_model_update(self): _delete_on_model_update, "deleteTheseOnModelUpdate", removal_version="0.25.0", - future_warn=True, + error=True, ) #: List of matrix names to have their factors cleared on a model update diff --git a/tests/base/props/test_has_model.py b/tests/base/props/test_has_model.py index 5d7cc0d7db..d794f3d05d 100644 --- a/tests/base/props/test_has_model.py +++ b/tests/base/props/test_has_model.py @@ -105,11 +105,13 @@ def test_no_model_needed(modeler): assert not modeler.needs_model -def test_deletion_deprecation(modeler): - msg = re.escape("HasModel.deleteTheseOnModelUpdate has been deprecated") + ".*" - with pytest.warns(FutureWarning, match=msg): +def test_deletion_removal(modeler): + msg = re.escape("HasModel.deleteTheseOnModelUpdate has been removed") + with pytest.raises(NotImplementedError, match=msg): modeler.deleteTheseOnModelUpdate + +def test_clean_on_model_update_deprecation(modeler): msg = "clean_on_model_update has been deprecated due to repeated functionality.*" with pytest.warns(FutureWarning, match=msg): modeler.clean_on_model_update diff --git a/tests/base/test_survey_data.py b/tests/base/test_survey_data.py index 8b67a3795d..6b7da90a68 100644 --- a/tests/base/test_survey_data.py +++ b/tests/base/test_survey_data.py @@ -106,32 +106,6 @@ def test_setitem(self, sample_data): dobs_new = np.hstack(dobs_new) np.testing.assert_allclose(dobs_new, sample_data.dobs) - @pytest.mark.filterwarnings( - "ignore:The `index_dictionary` property has been deprecated." - ) - def test_index_dictionary(self, sample_data): - """Test the ``index_dictionary`` property.""" - # Assign dobs to the data object - dobs = np.random.default_rng(seed=42).uniform(size=sample_data.survey.nD) - sample_data.dobs = dobs - - # Check indices in index_dictionary for each source-receiver pair - survey_slices = sample_data.survey.get_all_slices() - for src, rx in self.get_source_receiver_pairs(sample_data.survey): - expected_slice_ = survey_slices[src, rx] - indices = sample_data.index_dictionary[src][rx] - np.testing.assert_allclose(dobs[indices], dobs[expected_slice_]) - - def test_deprecated_index_dictionary(self, sample_data): - """Test deprecation warning in ``index_dictionary``.""" - source = sample_data.survey.source_list[0] - receiver = source.receiver_list[0] - with pytest.warns( - FutureWarning, - match=re.escape("The `index_dictionary` property has been deprecated."), - ): - sample_data.index_dictionary[source][receiver] - class TestSurveySlice: """ diff --git a/tests/pf/test_components.py b/tests/pf/test_components.py deleted file mode 100644 index 30c66202ee..0000000000 --- a/tests/pf/test_components.py +++ /dev/null @@ -1,55 +0,0 @@ -""" -Test how potential field surveys and simulations access receiver components. -""" - -import re -import pytest -import numpy as np - -import discretize -from simpeg.potential_fields import gravity, magnetics - - -@pytest.fixture -def receiver_locations(): - x = np.linspace(-20.0, 20.0, 4) - x, y = np.meshgrid(x, x) - z = 5.0 * np.ones_like(x) - return np.vstack((x.ravel(), y.ravel(), z.ravel())).T - - -@pytest.fixture -def mesh(): - dh = 5.0 - hx = [(dh, 10)] - return discretize.TensorMesh([hx, hx, hx], "CCN") - - -class TestComponentsGravitySurvey: - - def test_deprecated_components(self, receiver_locations): - """ - Test FutureError after deprecated ``components`` property. - """ - receivers = gravity.receivers.Point(receiver_locations, components="gz") - source_field = gravity.sources.SourceField(receiver_list=[receivers]) - survey = gravity.survey.Survey(source_field) - msg = re.escape("The `components` property is deprecated") - with pytest.warns(FutureWarning, match=msg): - survey.components - - -class TestComponentsMagneticSurvey: - - def test_deprecated_components(self, receiver_locations): - """ - Test FutureError after deprecated ``components`` property. - """ - receivers = magnetics.receivers.Point(receiver_locations, components="tmi") - source_field = magnetics.sources.UniformBackgroundField( - receiver_list=[receivers], amplitude=55_000, inclination=12, declination=35 - ) - survey = magnetics.survey.Survey(source_field) - msg = re.escape("The `components` property is deprecated") - with pytest.warns(FutureWarning, match=msg): - survey.components diff --git a/tests/pf/test_forward_Grav_Linear.py b/tests/pf/test_forward_Grav_Linear.py index e66279e4fc..4dfb85fde4 100644 --- a/tests/pf/test_forward_Grav_Linear.py +++ b/tests/pf/test_forward_Grav_Linear.py @@ -863,29 +863,6 @@ def test_G_t_dot_v(self, survey, mesh, mapping, parallel): np.testing.assert_allclose(simulation.G.T @ vector, simulation_ram.G.T @ vector) -class TestDeprecationWarning(BaseFixtures): - """ - Test warnings after deprecated properties or methods of the simulation class. - """ - - def test_gtg_diagonal(self, survey, mesh): - """Test deprecation warning on gtg_diagonal property.""" - mapping = maps.IdentityMap(nP=mesh.n_cells) - simulation = gravity.simulation.Simulation3DIntegral( - survey=survey, - mesh=mesh, - rhoMap=mapping, - store_sensitivities="ram", - engine="choclo", - ) - msg = re.escape( - "The `gtg_diagonal` property has been deprecated. " - "It will be removed in SimPEG v0.25.0.", - ) - with pytest.warns(FutureWarning, match=msg): - simulation.gtg_diagonal - - class TestConversionFactor: """Test _get_conversion_factor function.""" From 45393c3f9158d2072832ca2a4228f1415857fad9 Mon Sep 17 00:00:00 2001 From: "Devin C. Cowan" Date: Mon, 20 Oct 2025 14:44:46 -0700 Subject: [PATCH 187/194] Add shift to discrete topography for NSEM (#1683) Add new `shift_to_discrete_topography` function that shifts locations relative to discrete surface topography. Add also a new `get_discrete_topography` function that generates discrete topography locations out of a mesh and its active cells. We measure electric fields at the Earth's surface when performing MT surveys. Like DC/IP, the original measurement locations of the electric fields can end up in air cells when we define discrete surface topography. These functions allow the user to shift locations relative to discrete topography on Tensor and Tree meshes. For Airborne NSEM, they also allow the user to preserve the original flight heights. Deprecate the following functions: `gettopoCC`, `drapeTopotoLoc`, and `closestPointsGrid`. --------- Co-authored-by: Santiago Soler --- .ci/environment_test.yml | 2 +- environment.yml | 2 +- pyproject.toml | 2 +- .../static/resistivity/survey.py | 53 +++- .../static/utils/static_utils.py | 281 +++++++++++------- simpeg/utils/__init__.py | 4 + simpeg/utils/mesh_utils.py | 192 +++++++++++- tests/dask/test_IP_jvecjtvecadj_dask.py | 8 +- tests/em/static/test_dc_survey.py | 44 +-- tests/em/static/test_static_utils.py | 85 ++++-- tests/utils/test_mesh_utils.py | 118 ++++++++ 11 files changed, 623 insertions(+), 168 deletions(-) create mode 100644 tests/utils/test_mesh_utils.py diff --git a/.ci/environment_test.yml b/.ci/environment_test.yml index 42401cd9ab..e2f488baa1 100644 --- a/.ci/environment_test.yml +++ b/.ci/environment_test.yml @@ -6,7 +6,7 @@ dependencies: - scipy>=1.12 - pymatsolver>=0.3 - matplotlib-base - - discretize>=0.11 + - discretize>=0.12 - geoana>=0.7 - libdlf - typing_extensions diff --git a/environment.yml b/environment.yml index 58bc664044..66139319ae 100644 --- a/environment.yml +++ b/environment.yml @@ -8,7 +8,7 @@ dependencies: - scipy>=1.12 - pymatsolver>=0.3 - matplotlib-base - - discretize>=0.11 + - discretize>=0.12 - geoana>=0.7 - libdlf - typing_extensions diff --git a/pyproject.toml b/pyproject.toml index 4205bb7b56..0496ef5fb8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ dependencies = [ "scipy>=1.12", "pymatsolver>=0.3", "matplotlib", - "discretize>=0.11", + "discretize>=0.12", "geoana>=0.7", "libdlf", "typing_extensions; python_version<'3.13'", diff --git a/simpeg/electromagnetics/static/resistivity/survey.py b/simpeg/electromagnetics/static/resistivity/survey.py index b1ad5bfbf7..cbc6d9a522 100644 --- a/simpeg/electromagnetics/static/resistivity/survey.py +++ b/simpeg/electromagnetics/static/resistivity/survey.py @@ -1,16 +1,17 @@ import numpy as np +import warnings from ....utils.code_utils import validate_string from ....survey import BaseSurvey -from ..utils import drapeTopotoLoc +from ....utils import shift_to_discrete_topography from . import receivers as Rx from . import sources as Src from ..utils import static_utils class Survey(BaseSurvey): - """DC/IP survey class + """DC/IP survey class. Parameters ---------- @@ -37,7 +38,7 @@ def __init__( @property def survey_geometry(self): - """Survey geometry + """Survey geometry. Returns ------- @@ -76,7 +77,7 @@ def __repr__(self): @property def locations_a(self): """ - Locations of the positive (+) current electrodes in the survey + Locations of the positive (+) current electrodes in the survey. Returns ------- @@ -90,7 +91,7 @@ def locations_a(self): @property def locations_b(self): """ - Locations of the negative (-) current electrodes in the survey + Locations of the negative (-) current electrodes in the survey. Returns ------- @@ -104,7 +105,7 @@ def locations_b(self): @property def locations_m(self): """ - Locations of the positive (+) potential electrodes in the survey + Locations of the positive (+) potential electrodes in the survey. Returns ------- @@ -118,7 +119,7 @@ def locations_m(self): @property def locations_n(self): """ - Locations of the negative (-) potential electrodes in the survey + Locations of the negative (-) potential electrodes in the survey. Returns ------- @@ -132,7 +133,7 @@ def locations_n(self): @property def unique_electrode_locations(self): """ - Unique locations of the A, B, M, N electrodes + Unique locations of the A, B, M, N electrodes. Returns ------- @@ -169,7 +170,7 @@ def set_geometric_factor( space_type="halfspace", ): """ - Set and return the geometric factor for all data + Set and return the geometric factor for all data. Parameters ---------- @@ -239,9 +240,11 @@ def drape_electrodes_on_topography( self, mesh, active_cells, - option="top", + topo_cell_cutoff="top", topography=None, force=False, + shift_horizontal=True, + option=None, ): """Shift electrode locations to discrete surface topography. @@ -251,13 +254,31 @@ def drape_electrodes_on_topography( The mesh on which the discretized fields are computed active_cells : numpy.ndarray of int or bool Active topography cells - option :{"top", "center"} + topo_cell_cutoff : {"top", "center"} Define topography at tops of cells or cell centers. topography : (n, dim) numpy.ndarray, default = ``None`` Surface topography force : bool, default = ``False`` - If ``True`` force electrodes to surface even if borehole + If ``True`` force electrodes to surface even if borehole. + shift_horizontal : bool + When True, locations are shifted horizontally to lie vertically over cell + centers. When False, the original horizontal locations are preserved. + option : {"top", "center"} + Define topography at tops of cells or cell centers. + + .. deprecated:: 0.25.0 + + Argument ``option`` is deprecated in favor of ``topo_cell_cutoff`` + and will be removed in SimPEG v0.27.0. + """ + if option is not None: + msg = ( + "Argument ``option`` is deprecated in favor of ``topo_cell_cutoff`` " + "and will be removed in SimPEG v0.27.0." + ) + warnings.warn(msg, FutureWarning, stacklevel=2) + topo_cell_cutoff = option if self.survey_geometry == "surface": loc_a = self.locations_a[:, :2] @@ -271,8 +292,12 @@ def drape_electrodes_on_topography( inv_b, inv = inv[: len(loc_b)], inv[len(loc_b) :] inv_m, inv_n = inv[: len(loc_m)], inv[len(loc_m) :] - electrodes_shifted = drapeTopotoLoc( - mesh, unique_electrodes, active_cells=active_cells, option=option + electrodes_shifted = shift_to_discrete_topography( + mesh, + unique_electrodes, + active_cells=active_cells, + topo_cell_cutoff=topo_cell_cutoff, + shift_horizontal=shift_horizontal, ) a_shifted = electrodes_shifted[inv_a] b_shifted = electrodes_shifted[inv_b] diff --git a/simpeg/electromagnetics/static/utils/static_utils.py b/simpeg/electromagnetics/static/utils/static_utils.py index 8722e02e66..43bb8e1b01 100644 --- a/simpeg/electromagnetics/static/utils/static_utils.py +++ b/simpeg/electromagnetics/static/utils/static_utils.py @@ -5,7 +5,6 @@ import matplotlib.pyplot as plt import matplotlib as mpl from matplotlib import ticker -import warnings from ..resistivity import sources, receivers from .. import resistivity as dc from ....utils import ( @@ -20,6 +19,16 @@ write_dcip3d_ubc, ) +import warnings + +try: + from warnings import deprecated +except ImportError: + # Use the deprecated decorator provided by typing_extensions (which + # supports older versions of Python) if it cannot be imported from + # warnings. + from typing_extensions import deprecated + from ....utils.plot_utils import plot_1d_layer_model # noqa: F401 @@ -76,7 +85,7 @@ def electrode_separations(survey_object, electrode_pair="all", **kwargs): Parameters ---------- survey_object : simpeg.electromagnetics.static.survey.Survey - A DC or IP survey object + A DC or IP survey object. electrode_pair : {'all', 'AB', 'MN', 'AM', 'AN', 'BM', 'BN} Which electrode separation pairs to compute. @@ -85,9 +94,7 @@ def electrode_separations(survey_object, electrode_pair="all", **kwargs): list of numpy.ndarray For each electrode pair specified, the electrode distance is returned in a list. - """ - if not isinstance(electrode_pair, list): if electrode_pair.lower() == "all": electrode_pair = ["AB", "MN", "AM", "AN", "BM", "BN"] @@ -179,11 +186,12 @@ def pseudo_locations(survey, wenner_tolerance=0.1, **kwargs): Parameters ---------- survey : simpeg.electromagnetics.static.resistivity.Survey - A DC or IP survey + A DC or IP survey. wenner_tolerance : float, default=0.1 - If the center location for a source and receiver pair are within wenner_tolerance, - we assume the datum was collected with a wenner configuration and the pseudo-location - is computed based on the AB electrode spacing. + If the center location for a source and receiver pair are within + wenner_tolerance, we assume the datum was collected with a wenner + configuration and the pseudo-location is computed based on the AB + electrode spacing. Returns ------- @@ -191,9 +199,7 @@ def pseudo_locations(survey, wenner_tolerance=0.1, **kwargs): For 2D surveys, *midxy* is a vector containing the along line position. For 3D surveys, *midxy* is an (n, 2) numpy array containing the (x,y) positions. In eithere case, *midz* is a vector containing the pseudo-depth locations. - """ - if not isinstance(survey, dc.Survey): raise TypeError(f"Input must be instance of {dc.Survey}, not {type(survey)}") @@ -261,22 +267,22 @@ def geometric_factor(survey_object, space_type="half space", **kwargs): The geometric factor is given by: .. math:: - G = \frac{1}{C} \bigg [ \frac{1}{R_{AM}} - \frac{1}{R_{BM}} - \frac{1}{R_{AN}} + \frac{1}{R_{BN}} \bigg ] + G = \frac{1}{C} \bigg [ \frac{1}{R_{AM}} - \frac{1}{R_{BM}} + - \frac{1}{R_{AN}} + \frac{1}{R_{BN}} \bigg ] where :math:`C=2\pi` for a halfspace and :math:`C=4\pi` for a wholespace. Parameters ---------- survey_object : simpeg.electromagnetics.static.resistivity.Survey - A DC (or IP) survey object + A DC (or IP) survey object. space_type : {'half space', 'whole space'} Compute geometric factor for a halfspace or wholespace. Returns ------- (nD) numpy.ndarray - Geometric factor for each datum - + Geometric factor for each datum. """ # Set factor for whole-space or half-space assumption if space_type.lower() in SPACE_TYPES["whole space"]: @@ -311,20 +317,19 @@ def apparent_resistivity_from_voltage( Parameters ---------- survey : simpeg.electromagnetics.static.resistivity.Survey - A DC survey + A DC survey. volts : (nD) numpy.ndarray - Normalized voltage measurements [V/A] + Normalized voltage measurements [V/A]. space_type : {'half space', 'whole space'} Compute apparent resistivity assume a half space or whole space. eps : float, default=1e-10 - Stabilization constant in case of a null geometric factor + Stabilization constant in case of a null geometric factor. Returns ------- numpy.ndarray - Apparent resistivities for all data + Apparent resistivities for all data. """ - G = geometric_factor(survey, space_type=space_type) # Calculate apparent resistivity @@ -351,10 +356,10 @@ def convert_survey_3d_to_2d_lines( Parameters ---------- survey : simpeg.electromagnetics.static.resistivity.Survey - A DC (or IP) survey + A DC (or IP) survey. lineID : (n_data) numpy.ndarray - Defines the corresponding line ID for each datum - data_type : {'volt', 'apparent_resistivity', 'apparent_conductivity', 'apparent_chargeability'} + Defines the corresponding line ID for each datum. + data_type : {'volt', 'apparent_resistivity', 'apparent_conductivity', 'apparent_chargeability'} # E501 Data type for the survey. output_indexing : bool, default=False, optional If ``True`` output a list of indexing arrays that map from the original 3D @@ -527,33 +532,33 @@ def plot_pseudosection( plot_type : {"contourf", "scatter", "pcolor"} Which plot type to create. ax : mpl_toolkits.mplot3d.axes.Axes, optional - An axis for the plot + An axis for the plot. clim : (2) list, optional list containing the minimum and maximum value for the color range, - i.e. [vmin, vmax] + i.e. [vmin, vmax]. scale : {'linear', 'log'} Plot on linear or log base 10 scale. pcolor_opts : dict, optional - Dictionary defining kwargs for pcolor plot if `plot_type=='pcolor'` + Dictionary defining kwargs for pcolor plot if `plot_type=='pcolor'`. contourf_opts : dict, optional - Dictionary defining kwargs for filled contour plot if `plot_type=='contourf'` + Dictionary defining kwargs for filled contour plot if `plot_type=='contourf'`. scatter_opts : dict, optional - Dictionary defining kwargs for scatter plot if `plot_type=='scatter'` + Dictionary defining kwargs for scatter plot if `plot_type=='scatter'`. mask_topography : bool - This freature should be set to True when there is significant topography and the user - would like to mask interpolated locations in the filled contour plot that lie - above the surface topography. + This freature should be set to True when there is significant topography and + the user would like to mask interpolated locations in the filled contour plot + that lie above the surface topography. create_colorbar : bool If *True*, a colorbar is automatically generated. If *False*, it is not. If multiple planes are being plotted, only set the first scatter plot - to *True* + to *True*. cbar_opts : dict - Dictionary defining kwargs for the colorbar + Dictionary defining kwargs for the colorbar. cbar_label : str A string stating the color bar label for the - data; e.g. 'S/m', '$\Omega m$', '%' + data; e.g. 'S/m', '$\Omega m$', '%'. cax : mpl_toolkits.mplot3d.axes.Axes, optional - An axis object for the colorbar + An axis object for the colorbar. data_type : str, optional If dobs is ``None``, this will transform the data vector in the `survey` parameter when it is a simpeg.data.Data object from voltage to the requested `data_type`. @@ -567,7 +572,6 @@ def plot_pseudosection( ------- mpl_toolkits.mplot3d.axes3d.Axes3D The axis object that holds the plot - """ if len(kwargs) > 0: warnings.warn( @@ -776,15 +780,15 @@ def plot_3d_pseudosection( Parameters ---------- survey : simpeg.electromagnetics.static.survey.Survey - A DC or IP survey object + A DC or IP survey object. dvec : numpy.ndarray A data vector containing volts, integrated chargeabilities, apparent resistivities or apparent chargeabilities. marker_size : int - Sets the marker size for the points on the scatter plot + Sets the marker size for the points on the scatter plot. vlim : (2) list list containing the minimum and maximum value for the color range, - i.e. [vmin, vmax] + i.e. [vmin, vmax]. scale : {'linear', 'log'} Plot on linear or log base 10 scale. units : str @@ -799,18 +803,20 @@ def plot_3d_pseudosection( **plane_points**. A list is used if the *plane_distance* is different for each plane. cbar_opts: dict - Dictionary containing colorbar properties formatted according to plotly.graph_objects.scatter3d.cbar + Dictionary containing colorbar properties formatted according to + plotly.graph_objects.scatter3d.cbar. marker_opts : dict - Dictionary containing marker properties formatted according to plotly.graph_objects.scatter3d + Dictionary containing marker properties formatted according to + plotly.graph_objects.scatter3d. layout_opts : dict - Dictionary defining figure layout properties, formatted according to plotly.Layout + Dictionary defining figure layout properties, + formatted according to plotly.Layout. Returns ------- fig : A plotly figure """ - locations = pseudo_locations(survey) # Scaling @@ -966,7 +972,7 @@ def generate_survey_from_abmn_locations( locations_n : numpy.array An (n, dim) numpy array containing N electrode locations. If None, we assume all receivers are Pole receivers. - data_type : {'volt', 'apparent_conductivity', 'apparent_resistivity', 'apparent_chargeability'} + data_type : {'volt', 'apparent_conductivity', 'apparent_resistivity', 'apparent_chargeability'} # E501 Data type of the receivers. output_sorting : bool This option is used if the ABMN locations are sorted during the creation of the survey @@ -975,17 +981,14 @@ def generate_survey_from_abmn_locations( If True, the function will output a tuple containing the survey object and a numpy array (n,) that will sort the data vector to match the order of the electrodes in the survey. - Returns ------- survey - A simpeg.electromagnetic.static.survey.Survey object + A simpeg.electromagnetic.static.survey.Survey object. sort_index - A numpy array which defines any sorting that took place when creating the survey - + A numpy array which defines any sorting that took place when creating the survey. """ - if locations_a is None: raise TypeError("Locations for A electrodes must be provided.") if locations_m is None: @@ -996,7 +999,8 @@ def generate_survey_from_abmn_locations( "apparent_conductivity", "apparent_resistivity", "apparent_chargeability", - ], "data_type must be one of 'volt', 'apparent_conductivity', 'apparent_resistivity', 'apparent_chargeability'" + ], "data_type must be one of 'volt', 'apparent_conductivity', " + "'apparent_resistivity', 'apparent_chargeability'" if locations_b is None: locations_b = locations_a @@ -1091,22 +1095,22 @@ def generate_dcip_survey(endl, survey_type, a, b, n, dim=3, **kwargs): Parameters ---------- endl : numpy.ndarray - End points for survey line [x1, y1, z1, x2, y2, z2] + End points for survey line [x1, y1, z1, x2, y2, z2]. survey_type : {'dipole-dipole', 'pole-dipole', 'dipole-pole', 'pole-pole', 'gradient'} Survey type to generate. a : int - pole seperation + pole seperation. b : int - dipole separation + dipole separation. n : int - number of receiver dipoles per source + number of receiver dipoles per source. dim : int, default=3 - Create 2D or 3D survey + Create 2D or 3D survey. Returns ------- simpeg.electromagnetics.static.resistivity.Survey - A DC survey object + A DC survey object. """ def xy_2_r(x1, x2, y1, y2): @@ -1279,38 +1283,39 @@ def generate_dcip_sources_line( ---------- survey_type : {'dipole-dipole', 'pole-dipole', 'dipole-pole', 'pole-pole'} Survey type. - data_type : {'volt', 'apparent_conductivity', 'apparent_resistivity', 'apparent_chargeability'} + data_type : {'volt', 'apparent_conductivity', 'apparent_resistivity', 'apparent_chargeability'} # E501 Data type. dimension_type : {'2D', '3D'} Which dimension you are using. end_points : numpy.array Horizontal end points [x1, x2] or [x1, x2, y1, y2] topo : (n, dim) numpy.ndarray - Define survey topography + Define survey topography. num_rx_per_src : int - Maximum number of receivers per souces + Maximum number of receivers per souces. station_spacing : float - Distance between stations + Distance between stations. Returns ------- simpeg.electromagnetics.static.resistivity.Survey - A DC survey object + A DC survey object. """ - assert survey_type.lower() in [ "pole-pole", "pole-dipole", "dipole-pole", "dipole-dipole", - ], "survey_type must be one of 'pole-pole', 'pole-dipole', 'dipole-pole', 'dipole-dipole'" + ], "survey_type must be one of 'pole-pole', 'pole-dipole', " + "'dipole-pole', 'dipole-dipole'" assert data_type.lower() in [ "volt", "apparent_conductivity", "apparent_resistivity", "apparent_chargeability", - ], "data_type must be one of 'volt', 'apparent_conductivity', 'apparent_resistivity', 'apparent_chargeability'" + ], "data_type must be one of 'volt', 'apparent_conductivity', " + "'apparent_resistivity', 'apparent_chargeability'" assert dimension_type.upper() in [ "2D", @@ -1413,19 +1418,18 @@ def xy_2_lineID(dc_survey): Read DC survey class and append line ID. Assumes that the locations are listed in the order they were collected. May need to generalize for random - point locations, but will be more expensive + point locations, but will be more expensive. Parameters ---------- dc_survey : dict - Vectors of station location + Vectors of station location. Returns ------- numpy.ndarray - LineID Vector of integers + LineID Vector of integers. """ - # Compute unit vector between two points nstn = dc_survey.nSrc @@ -1488,21 +1492,20 @@ def xy_2_lineID(dc_survey): def r_unit(p1, p2): - """Compute unit vector between two points + """Compute unit vector between two points. Parameters ---------- p1 : (dim) numpy.array - Start point + Start point. p2 : (dim) numpy.array - End point + End point. Returns ------- (dim) numpy.array - Unit vector + Unit vector. """ - assert len(p1) == len(p2), "locs must be the same shape." dx = [] @@ -1521,10 +1524,24 @@ def r_unit(p1, p2): return vec, r +@deprecated( + "The `gettopoCC` function is deprecated, " + "and will be removed in SimPEG v0.27.0. " + "This functionality has been replaced by the " + "'get_discrete_topography' function, which can be imported from" + "simpeg.utils", + category=FutureWarning, +) def gettopoCC(mesh, ind_active, option="top"): """ Generate surface topography from active indices of mesh. + .. deprecated:: 0.25.0 + + ``gettopoCC`` will be removed in SimPEG v0.27.0. + Please, use the :func:`simpeg.utils.get_discrete_topography` function + instead. + Parameters ---------- mesh : discretize.TensorMesh or discretize.TreeMesh @@ -1590,29 +1607,48 @@ def gettopoCC(mesh, ind_active, option="top"): raise NotImplementedError(f"{type(mesh)} mesh is not supported.") +@deprecated( + "The `drapeTopotoLoc` function is deprecated, " + "and will be removed in SimPEG v0.27.0. " + "This functionality has been replaced by the " + "'shift_to_discrete_topography' function, which can be imported from" + "simpeg.utils", + category=FutureWarning, +) def drapeTopotoLoc(mesh, pts, active_cells=None, option="top", topo=None, **kwargs): - """Drape locations right below discretized surface topography + """Drape locations right below discretized surface topography. This function projects the set of locations provided to the discrete surface topography. + .. deprecated:: 0.25.0 + + ``drapeTopotoLoc`` will be removed in SimPEG v0.27.0. + Please, use the :func:`simpeg.utils.shift_to_discrete_topography` function + instead. + Parameters ---------- mesh : discretize.TensorMesh or discretize.TreeMesh - A 2D tensor or tree mesh + A 2D tensor or tree mesh. pts : (n, dim) numpy.ndarray The set of points being projected to the discretize surface topography active_cells : numpy.ndarray of int or bool, optional - Index array for all cells lying below the surface topography. Surface topography - can be specified using the 'ind_active' or 'topo' input parameters. + Index array for all cells lying below the surface topography. + Surface topography can be specified using the 'active_cells' or + 'topo' input parameters. option : {"top", "center"} - Define whether the cell center or entire cell of actice cells must be below the topography. - The topography is defined using the 'topo' input parameter. + Define whether the cell center or entire cell of actice cells must be below + the topography. The topography is defined using the 'topo' input parameter. topo : (n, dim) numpy.ndarray Surface topography. Can be used if an active indices array cannot be provided - for the input parameter 'ind_active' - """ + for the input parameter 'active_cells'. + Returns + ------- + (n, dim) numpy.ndarray + The discrete topography locations. + """ # Deprecate indActive argument if kwargs.pop("indActive", None) is not None: raise TypeError( @@ -1642,18 +1678,39 @@ def drapeTopotoLoc(mesh, pts, active_cells=None, option="top", topo=None, **kwar active_cells = discretize.utils.active_from_xyz(mesh, topo) if mesh._meshType == "TENSOR": - meshtemp, topoCC = gettopoCC(mesh, active_cells, option=option) + # Ignore FutureWarning coming from gettopoCC's deprecation + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", + message="The `gettopoCC` function is deprecated", + category=FutureWarning, + ) + meshtemp, topoCC = gettopoCC(mesh, active_cells, option=option) inds = meshtemp.closest_points_index(pts) topo = topoCC[inds] out = np.c_[pts, topo] elif mesh._meshType == "TREE": if mesh.dim == 3: - uniqXYlocs, topoCC = gettopoCC(mesh, active_cells, option=option) + # Ignore FutureWarning coming from gettopoCC's deprecation + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", + message="The `gettopoCC` function is deprecated", + category=FutureWarning, + ) + uniqXYlocs, topoCC = gettopoCC(mesh, active_cells, option=option) inds = closestPointsGrid(uniqXYlocs, pts) out = np.c_[uniqXYlocs[inds, :], topoCC[inds]] else: - uniqXlocs, topoCC = gettopoCC(mesh, active_cells, option=option) + # Ignore FutureWarning coming from gettopoCC's deprecation + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", + message="The `gettopoCC` function is deprecated", + category=FutureWarning, + ) + uniqXlocs, topoCC = gettopoCC(mesh, active_cells, option=option) inds = closestPointsGrid(uniqXlocs, pts, dim=1) out = np.c_[uniqXlocs[inds], topoCC[inds]] else: @@ -1663,24 +1720,24 @@ def drapeTopotoLoc(mesh, pts, active_cells=None, option="top", topo=None, **kwar def genTopography(mesh, zmin, zmax, seed=None, its=100, anisotropy=None): - """Generate random topography + """Generate random topography. Parameters ---------- mesh : discretize.BaseMesh - A 2D or 3D mesh + A 2D or 3D mesh. zmin : float - Minimum topography [m] + Minimum topography [m]. zmax : float - Maximum topography [m] + Maximum topography [m]. seed : int, default=``None`` - Set the seed for the random generated model or leave as ``None`` + Set the seed for the random generated model or leave as ``None``. its : int, default=100 - Number of smoothing iterations after convolutions + Number of smoothing iterations after convolutions. anisotropy : (3, n) numpy.ndarray, default=``None`` - Apply a (3, n) blurring kernel that is used or leave as ``None`` in the case of isotropy. + Apply a (3, n) blurring kernel that is used or leave as ``None`` + in the case of isotropy. """ - if isinstance(mesh, discretize.CurvilinearMesh): raise ValueError("Curvilinear mesh is not supported.") @@ -1710,22 +1767,31 @@ def genTopography(mesh, zmin, zmax, seed=None, its=100, anisotropy=None): raise Exception("Only works for 2D and 3D models") +@deprecated( + "The `closestPointsGrid` function is now deprecated. " + "It will be removed in SimPEG v0.27.0.", + category=FutureWarning, +) def closestPointsGrid(grid, pts, dim=2): - """Move a list of points to the closest points on a grid. + """Return indices of closest gridded points for a set of input points. + + .. deprecated:: 0.25.0 + + ``closestPointsGrid`` will be removed in SimPEG v0.27.0. Parameters ---------- grid : (n, dim) numpy.ndarray - A gridded set of points + A gridded set of points. pts : (m, dim) numpy.ndarray - Points being projected to gridded locations + Points being projected to gridded locations. dim : int, default=2 - Dimension of the points + Dimension of the points. Returns ------- - (m) numpy.ndarray - indices for the closest gridded location for all *pts* supplied. + (n,) numpy.ndarray + Indices of the closest gridded points for all *pts* supplied. """ if dim == 1: nodeInds = np.asarray( @@ -1759,27 +1825,28 @@ def gen_3d_survey_from_2d_lines( Parameters ---------- survey_type : str - Survey type. Choose one of {'dipole-dipole', 'pole-dipole', 'dipole-pole', 'pole-pole', 'gradient'} + Survey type. Choose one of {'dipole-dipole', 'pole-dipole', + 'dipole-pole', 'pole-pole', 'gradient'}. a : int - pole seperation + pole seperation. b : int - dipole separation + dipole separation. n_spacing : int - number of receiver dipoles per source + number of receiver dipoles per source. n_lines : int, default=5 - Number of survey lines + Number of survey lines. line_length : float, default=200. Line length line_spacing : float, default=20. - Line spacing + Line spacing. x0, y0, z0 : float, default=0. - The origin for the 3D survey + The origin for the 3D survey. src_offset_y : float, default=0. - Source y offset + Source y offset. dim : int, default=3 - Define 2D or 3D survey + Define 2D or 3D survey. is_IO : bool, default=``True`` - If ``True``, is an IO class + If ``True``, is an IO class. Returns ------- diff --git a/simpeg/utils/__init__.py b/simpeg/utils/__init__.py index b6b4992b0d..d6ecd0393a 100644 --- a/simpeg/utils/__init__.py +++ b/simpeg/utils/__init__.py @@ -78,6 +78,8 @@ :toctree: generated/ surface2inds + get_discrete_topography + shift_to_discrete_topography Model Utility Functions @@ -242,6 +244,8 @@ closest_points_index, extract_core_mesh, surface2inds, + get_discrete_topography, + shift_to_discrete_topography, ) from .curv_utils import ( volume_tetrahedron, diff --git a/simpeg/utils/mesh_utils.py b/simpeg/utils/mesh_utils.py index 3161859288..42a85bde14 100644 --- a/simpeg/utils/mesh_utils.py +++ b/simpeg/utils/mesh_utils.py @@ -1,23 +1,27 @@ import numpy as np +from scipy.spatial import cKDTree + +from discretize import TensorMesh from discretize.utils import ( # noqa: F401 unpack_widths, closest_points_index, extract_core_mesh, + active_from_xyz, ) def surface2inds(vrtx, trgl, mesh, boundaries=True, internal=True): - """Takes a triangulated surface and determine which mesh cells it intersects. + """Takes a triangulated surface and determines which mesh cells it intersects. Parameters ---------- vrtx : (n_nodes, 3) numpy.ndarray of float The location of the vertices of the triangles trgl : (n_triang, 3) numpy.ndarray of int - Each row describes the 3 indices into the `vrtx` array that make up a triangle's - vertices. - mesh : discretize.TensorMesh + Each row describes the 3 indices into the `vrtx` array that make up a + triangle's vertices. + mesh : TensorMesh boundaries : bool, optional internal : bool, optional @@ -94,3 +98,183 @@ def surface2inds(vrtx, trgl, mesh, boundaries=True, internal=True): # Return the indexes inside return insideGrid + + +def _closest_grid_indices(grid, pts): + """Return indices of closest gridded points for a set of input points. + + Parameters + ---------- + grid : (n, dim) numpy.ndarray + A gridded set of points. + pts : (m, dim) numpy.ndarray + Points being projected to gridded locations. + + Returns + ------- + (n,) numpy.ndarray + Indices of the closest gridded points for all *pts* supplied. + """ + if grid.squeeze().ndim == 1: + grid_inds = np.asarray( + [np.abs(pt - grid).argmin() for pt in pts.tolist()], dtype=int + ) + else: + tree = cKDTree(grid) + _, grid_inds = tree.query(pts) + + return grid_inds + + +def get_discrete_topography(mesh, active_cells, topo_cell_cutoff="top"): + """ + Generate discrete topography locations from mesh and active cells. + + Parameters + ---------- + mesh : TensorMesh or discretize.TreeMesh + A tensor or tree mesh. + active_cells : numpy.ndarray of bool or int + Active cells index; i.e. indices of cells below surface + topo_cell_cutoff : {"top", "center"} + String to specify the cutoff for ground cells and the locations of + the discrete topography. For "top", ground cells lie entirely below + the surface topography and the discrete topography is defined on the + top faces of surface cells. For "center", only the cell centers must + lie below the surface topography and the discrete topography is defined + at the centers of surface cells. + + Returns + ------- + (n, dim) numpy.ndarray + Discrete topography locations; i.e. xy[z]. + """ + if mesh._meshType == "TENSOR": + if mesh.dim == 3: + mesh2D = TensorMesh([mesh.h[0], mesh.h[1]], mesh.x0[:2]) + zc = mesh.cell_centers[:, 2] + ACTIND = active_cells.reshape( + (mesh.vnC[0] * mesh.vnC[1], mesh.vnC[2]), order="F" + ) + ZC = zc.reshape((mesh.vnC[0] * mesh.vnC[1], mesh.vnC[2]), order="F") + topoCC = np.zeros(ZC.shape[0]) + + for i in range(ZC.shape[0]): + if topo_cell_cutoff == "top": + ind = np.argmax(ZC[i, :][ACTIND[i, :]]) + dz = mesh.h[2][ACTIND[i, :]][ind] * 0.5 + elif topo_cell_cutoff == "center": + dz = 0.0 + else: + raise ValueError("'topo_cell_cutoff' must be 'top' or 'center'.") + topoCC[i] = ZC[i, :][ACTIND[i, :]].max() + dz + return np.c_[mesh2D.cell_centers, topoCC] + + elif mesh.dim == 2: + mesh1D = TensorMesh([mesh.h[0]], [mesh.x0[0]]) + yc = mesh.cell_centers[:, 1] + ACTIND = active_cells.reshape((mesh.vnC[0], mesh.vnC[1]), order="F") + YC = yc.reshape((mesh.vnC[0], mesh.vnC[1]), order="F") + topoCC = np.zeros(YC.shape[0]) + for i in range(YC.shape[0]): + ind = np.argmax(YC[i, :][ACTIND[i, :]]) + if topo_cell_cutoff == "top": + dy = mesh.h[1][ACTIND[i, :]][ind] * 0.5 + elif topo_cell_cutoff == "center": + dy = 0.0 + else: + raise ValueError("'topo_cell_cutoff' must be 'top' or 'center'.") + topoCC[i] = YC[i, :][ACTIND[i, :]].max() + dy + return np.c_[mesh1D.cell_centers, topoCC] + + elif mesh._meshType == "TREE": + inds = mesh.get_boundary_cells(active_cells, direction="zu")[0] + + if topo_cell_cutoff == "top": + dz = mesh.h_gridded[inds, -1] * 0.5 + elif topo_cell_cutoff == "center": + dz = 0.0 + return np.c_[mesh.cell_centers[inds, :-1], mesh.cell_centers[inds, -1] + dz] + else: + raise NotImplementedError(f"{type(mesh)} mesh is not supported.") + + +def shift_to_discrete_topography( + mesh, + pts, + active_cells, + topo_cell_cutoff="top", + shift_horizontal=True, + heights=0.0, +): + """ + Shift locations relative to discrete surface topography. + + Parameters + ---------- + mesh : discretize.TensorMesh or discretize.TreeMesh + The mesh (2D or 3D) defining the discrete domain. + pts : (n, dim) numpy.ndarray + The original set of points being shifted relative to the discretize + surface topography. + active_cells : numpy.ndarray of int or bool, optional + Index array for all cells lying below the surface topography. + topo_cell_cutoff : {"top", "center"} + String to specify the cutoff for ground cells and the locations of the discrete + topography. For "top", ground cells lie entirely below the surface topography + and the discrete topography is defined on the top faces of surface cells. + For "center", only the cell centers must lie below the surface topography and + the discrete topography is defined at the centers of surface cells. + The topography is defined using the 'topo' input parameter. + heights : float or (n,) numpy.ndarray, optional + Height(s) relative to the true surface topography. Used to preserve flight + heights or borehole depths. + shift_horizontal : bool, optional + When True, locations are shifted horizontally to lie vertically over cell + centers. When False, the original horizontal locations are preserved. + + Returns + ------- + (n, dim) numpy.ndarray + The set of points shifted relative to the discretize surface topography. + """ + if mesh._meshType != "TENSOR" and mesh._meshType != "TREE": + raise NotImplementedError( + "shift_to_discrete_topography only supported for TensorMesh and TreeMesh'." + ) + + if not isinstance(heights, (int, float)) and len(pts) != len(heights): + raise ValueError( + ( + "If supplied as a `numpy.ndarray`, the number of heights must ", + "equal the number of points.", + ) + ) + + discrete_topography = get_discrete_topography( + mesh, active_cells, topo_cell_cutoff=topo_cell_cutoff + ) + + if pts.ndim == 2 and pts.shape[1] == mesh.dim: + has_elevation = True + horizontal_pts = pts[:, :-1] + else: + has_elevation = False + horizontal_pts = pts.squeeze() # in case (n, 1) array + + topo_inds = _closest_grid_indices(discrete_topography[:, :-1], horizontal_pts) + + if shift_horizontal: + if has_elevation: + cell_inds = mesh.point2index(pts) + out = np.c_[ + mesh.cell_centers[cell_inds, :-1], discrete_topography[topo_inds, -1] + ] + else: + out = discrete_topography[topo_inds, :] + else: + out = np.c_[horizontal_pts, discrete_topography[topo_inds, -1]] + + out[:, -1] += heights + + return out diff --git a/tests/dask/test_IP_jvecjtvecadj_dask.py b/tests/dask/test_IP_jvecjtvecadj_dask.py index 2e34848b60..6e23a40107 100644 --- a/tests/dask/test_IP_jvecjtvecadj_dask.py +++ b/tests/dask/test_IP_jvecjtvecadj_dask.py @@ -73,8 +73,12 @@ def setUp(self): # Find cells that lie below surface topography ind_active = ds.utils.active_from_xyz(mesh, topo_2d) # Shift electrodes to the surface of discretized topography - dc_data.survey.drape_electrodes_on_topography(mesh, ind_active, option="top") - ip_data.survey.drape_electrodes_on_topography(mesh, ind_active, option="top") + dc_data.survey.drape_electrodes_on_topography( + mesh, ind_active, option="top", shift_horizontal=False + ) + ip_data.survey.drape_electrodes_on_topography( + mesh, ind_active, option="top", shift_horizontal=False + ) # Define conductivity model in S/m (or resistivity model in Ohm m) air_conductivity = 1e-8 diff --git a/tests/em/static/test_dc_survey.py b/tests/em/static/test_dc_survey.py index 7087cce873..e08ba1cb3b 100644 --- a/tests/em/static/test_dc_survey.py +++ b/tests/em/static/test_dc_survey.py @@ -12,9 +12,7 @@ class TestRemovedSourceType: - """ - Tests after removing the source_type argument and property. - """ + """Tests after removing the source_type argument and property.""" def test_error_after_argument(self): """ @@ -36,32 +34,38 @@ def test_error_removed_property(self): survey.survey_type = "dipole-dipole" -class TestDeprecatedIndActive: - """ - Test the deprecated ``ind_active`` argument in ``drape_electrodes_on_topography``. - """ +class TestDeprecatedOption: + """Test the deprecated ``option`` argument in ``drape_electrodes_on_topography``.""" @pytest.fixture def mesh(self): return TensorMesh((5, 5, 5)) - def test_error(self, mesh): - """ - Test if error is raised after passing ``ind_active`` as argument. - """ - survey = Survey(source_list=[]) - msg = "got an unexpected keyword argument 'ind_active'" - active_cells = np.ones(mesh.n_cells, dtype=bool) - with pytest.raises(TypeError, match=msg): - survey.drape_electrodes_on_topography( - mesh, active_cells, ind_active=active_cells + def test_warning(self, mesh): + """Test if warning is raised after passing ``option`` as argument.""" + receivers_list = [ + receivers.Dipole( + locations_m=[[1, 2, 3], [4, 5, 6]], + locations_n=[[7, 8, 9], [10, 11, 12]], + ) + ] + sources_list = [ + sources.Dipole( + receivers_list, location_a=[0.5, 1.5, 2.5], location_b=[4.5, 5.5, 6.5] ) + ] + survey = Survey(source_list=sources_list) + msg = ( + "Argument ``option`` is deprecated in favor of ``topo_cell_cutoff`` " + "and will be removed in SimPEG v0.27.0." + ) + active_cells = np.ones(mesh.n_cells, dtype=bool) + with pytest.warns(FutureWarning, match=msg): + survey.drape_electrodes_on_topography(mesh, active_cells, option="top") def test_repr(): - """ - Test the __repr__ method of the survey. - """ + """Test the __repr__ method of the survey.""" receivers_list = [ receivers.Dipole( locations_m=[[1, 2, 3], [4, 5, 6]], locations_n=[[7, 8, 9], [10, 11, 12]] diff --git a/tests/em/static/test_static_utils.py b/tests/em/static/test_static_utils.py index c7213d6236..e2aaa1691c 100644 --- a/tests/em/static/test_static_utils.py +++ b/tests/em/static/test_static_utils.py @@ -2,11 +2,36 @@ Test functions in ``static_utils``. """ +import re import pytest import numpy as np import discretize -from simpeg.electromagnetics.static.utils.static_utils import drapeTopotoLoc +from simpeg.electromagnetics.static.utils.static_utils import ( + drapeTopotoLoc, + gettopoCC, + closestPointsGrid, +) + + +@pytest.fixture +def mesh(): + """Sample mesh.""" + return discretize.TensorMesh([10, 10, 10], "CCN") + + +@pytest.fixture +def points(): + """Sample points.""" + return np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]) + + +@pytest.fixture +def active_cells(mesh): + """Sample active cells for the mesh.""" + active_cells = np.ones(mesh.n_cells, dtype=bool) + active_cells[0] = False + return active_cells class TestDeprecatedIndActive: @@ -15,23 +40,7 @@ class TestDeprecatedIndActive: OLD_NAME = "ind_active" NEW_NAME = "active_cells" - @pytest.fixture - def mesh(self): - """Sample mesh.""" - return discretize.TensorMesh([10, 10, 10], "CCN") - - @pytest.fixture - def points(self): - """Sample points.""" - return np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]) - - @pytest.fixture - def active_cells(self, mesh): - """Sample active cells for the mesh.""" - active_cells = np.ones(mesh.n_cells, dtype=bool) - active_cells[0] = False - return active_cells - + @pytest.mark.filterwarnings("ignore:The `drapeTopotoLoc` function is deprecated") def test_error_argument(self, mesh, points, active_cells): """ Test if error is raised after passing ``ind_active`` as argument. @@ -39,3 +48,43 @@ def test_error_argument(self, mesh, points, active_cells): msg = "Unsupported keyword argument ind_active" with pytest.raises(TypeError, match=msg): drapeTopotoLoc(mesh, points, ind_active=active_cells) + + +class TestDeprecatedFunctions: + """Test deprecated functions.""" + + def test_drape_topo_warning(self, mesh, points, active_cells): + """ + Test deprecation warning for `drapeTopotoLoc`. + """ + msg = re.escape( + "The `drapeTopotoLoc` function is deprecated, " + "and will be removed in SimPEG v0.27.0. " + "This functionality has been replaced by the " + "'shift_to_discrete_topography' function, which can be imported from" + "simpeg.utils" + ) + with pytest.warns(FutureWarning, match=msg): + drapeTopotoLoc(mesh, points, active_cells=active_cells) + + def test_topo_cells_warning(self, mesh, active_cells): + """ + Test deprecation warning for `drapeTopotoLoc`. + """ + msg = re.escape( + "The `gettopoCC` function is deprecated, " + "and will be removed in SimPEG v0.27.0. " + "This functionality has been replaced by the " + "'get_discrete_topography' function, which can be imported from" + "simpeg.utils" + ) + with pytest.warns(FutureWarning, match=msg): + gettopoCC(mesh, active_cells) + + def test_closest_points(self, mesh, points): + """ + Test deprecation warning for `closestPointsGrid`. + """ + msg = re.escape("The `closestPointsGrid` function is now deprecated.") + with pytest.warns(FutureWarning, match=msg): + closestPointsGrid(mesh.cell_centers, points) diff --git a/tests/utils/test_mesh_utils.py b/tests/utils/test_mesh_utils.py new file mode 100644 index 0000000000..71c7d08a27 --- /dev/null +++ b/tests/utils/test_mesh_utils.py @@ -0,0 +1,118 @@ +import pytest +import numpy as np +from discretize import TensorMesh, TreeMesh, SimplexMesh +from discretize.utils import example_simplex_mesh +from simpeg.utils import shift_to_discrete_topography, get_discrete_topography + +DH = 1.0 +N = 16 + + +def get_mesh(mesh_type, dim): + """Generate test mesh.""" + h = dim * [[(DH, N)]] + origin = dim * "C" + if mesh_type == "tensor": + return TensorMesh(h, origin) + elif mesh_type == "tree": + tree_mesh = TreeMesh(h, origin) + tree_mesh.refine(-1) + return tree_mesh + else: + points, simplices = example_simplex_mesh(dim * [N]) + points = N * points - N / 2 + return SimplexMesh(points, simplices) + + +def get_points(dim): + """Test points.""" + if dim == 2: + return np.array([[1.1, 3.0], [-3.9, -2.0]]) + else: + return np.array([[1.1, -3.6, 3.0], [-3.9, 4.4, -2.0]]) + + +def get_active_cells(mesh): + """Test active cells for the mesh.""" + active_cells = np.zeros(mesh.n_cells, dtype=bool) + if mesh.dim == 1: + active_cells[mesh.cell_centers < 0.0] = True + else: + active_cells[mesh.cell_centers[:, -1] < 0.0] = True + return active_cells + + +CASES_LIST_SUCCESS = [ + ("tensor", 2, "center", False, 0.0), + ("tensor", 3, "top", False, np.r_[1.25, 1.25]), + ("tree", 2, "top", False, np.r_[1.25, 1.25]), + ("tree", 3, "center", False, 0.0), + ("tensor", 2, "center", True, 0.0), + ("tensor", 3, "top", True, np.r_[1.25, 1.25]), + ("tree", 2, "top", True, np.r_[1.25, 1.25]), + ("tree", 3, "center", True, 0.0), +] + + +@pytest.mark.parametrize( + "mesh_type, dim, option, shift_horizontal, heights", CASES_LIST_SUCCESS +) +def test_function_success(mesh_type, dim, option, shift_horizontal, heights): + """Test cases that run properly.""" + mesh = get_mesh(mesh_type, dim) + active_cells = get_active_cells(mesh) + pts = get_points(dim) + + pts_shifted = shift_to_discrete_topography( + mesh, + pts, + active_cells, + topo_cell_cutoff=option, + shift_horizontal=shift_horizontal, + heights=heights, + ) + + if isinstance(heights, (int, float)): + heights = np.array([heights]) + + correct_elevations = heights.copy() + if option == "center": + correct_elevations -= 0.5 * DH + + if shift_horizontal: + correct_locations = np.round(pts) + 0.5 * DH + else: + correct_locations = pts.copy() + correct_locations[:, -1] = correct_elevations + + np.testing.assert_allclose(correct_locations, pts_shifted) + + +def test_mesh_type_error(): + """Throw unsupported mesh type error.""" + mesh_type = "simplex" + dim = 2 + + mesh = get_mesh(mesh_type, dim) + active_cells = get_active_cells(mesh) + pts = get_points(dim) + + with pytest.raises(NotImplementedError): + shift_to_discrete_topography(mesh, pts, active_cells) + + with pytest.raises(NotImplementedError): + get_discrete_topography(mesh, active_cells) + + +def test_size_errors(): + """Throw array size mismatch error.""" + mesh_type = "tensor" + dim = 3 + + mesh = get_mesh(mesh_type, dim) + active_cells = get_active_cells(mesh) + pts = get_points(dim) + heights = np.r_[1.0, 2.0, 3.0] + + with pytest.raises(ValueError): + shift_to_discrete_topography(mesh, pts, active_cells, heights=heights) From 70c7b2a23104bb43f74bfa254d28e26b88b4d2b7 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 20 Oct 2025 23:15:43 +0000 Subject: [PATCH 188/194] Deprecate unused arguments in `drape_electrodes_on_topography` (#1723) Deprecate the unused `force` and `topography` arguments of the method. Add tests to check the warnings are raised. --- .../static/resistivity/survey.py | 39 ++++++++-- tests/em/static/test_dc_survey.py | 71 +++++++++++++++++-- 2 files changed, 102 insertions(+), 8 deletions(-) diff --git a/simpeg/electromagnetics/static/resistivity/survey.py b/simpeg/electromagnetics/static/resistivity/survey.py index cbc6d9a522..0d607c86b6 100644 --- a/simpeg/electromagnetics/static/resistivity/survey.py +++ b/simpeg/electromagnetics/static/resistivity/survey.py @@ -1,5 +1,5 @@ -import numpy as np import warnings +import numpy as np from ....utils.code_utils import validate_string @@ -241,10 +241,9 @@ def drape_electrodes_on_topography( mesh, active_cells, topo_cell_cutoff="top", - topography=None, - force=False, shift_horizontal=True, option=None, + **kwargs, ): """Shift electrode locations to discrete surface topography. @@ -258,8 +257,19 @@ def drape_electrodes_on_topography( Define topography at tops of cells or cell centers. topography : (n, dim) numpy.ndarray, default = ``None`` Surface topography + + .. deprecated:: v0.25.0 + + The ``topography`` argument is not used in this function. It will be + removed in SimPEG v0.27.0. + force : bool, default = ``False`` - If ``True`` force electrodes to surface even if borehole. + If ``True`` force electrodes to surface even if borehole + + .. deprecated:: v0.25.0 + + The ``force`` argument is not used in this function. It will be removed + in SimPEG v0.27.0. shift_horizontal : bool When True, locations are shifted horizontally to lie vertically over cell centers. When False, the original horizontal locations are preserved. @@ -280,6 +290,27 @@ def drape_electrodes_on_topography( warnings.warn(msg, FutureWarning, stacklevel=2) topo_cell_cutoff = option + if (key := "topography") in kwargs: + msg = ( + "The `topography` argument is not used in the " + "`drape_electrodes_on_topography` and will be removed in " + "SimPEG v0.27.0." + ) + warnings.warn(msg, category=FutureWarning, stacklevel=2) + kwargs.pop(key) + + if (key := "force") in kwargs: + msg = ( + "The `force` argument is not used in the " + "`drape_electrodes_on_topography` and will be removed in " + "SimPEG v0.27.0." + ) + warnings.warn(msg, category=FutureWarning, stacklevel=2) + kwargs.pop(key) + + if kwargs: # TODO Remove this when removing kwargs argument. + raise TypeError("Unsupported keyword argument " + kwargs.popitem()[0]) + if self.survey_geometry == "surface": loc_a = self.locations_a[:, :2] loc_b = self.locations_b[:, :2] diff --git a/tests/em/static/test_dc_survey.py b/tests/em/static/test_dc_survey.py index e08ba1cb3b..9e5f85c814 100644 --- a/tests/em/static/test_dc_survey.py +++ b/tests/em/static/test_dc_survey.py @@ -2,6 +2,7 @@ Tests for resistivity (DC) survey objects. """ +import re import pytest import numpy as np @@ -34,14 +35,25 @@ def test_error_removed_property(self): survey.survey_type = "dipole-dipole" -class TestDeprecatedOption: - """Test the deprecated ``option`` argument in ``drape_electrodes_on_topography``.""" +class TestDeprecatedArgsDrapeElectrodes: + """ + Test the deprecated arguments in ``drape_electrodes_on_topography``. + + Deprecated arguments: + + - ``ind_active`` was removed, + - ``option`` was deprecated, + - ``topography`` is not used and was deprecated, + - ``force`` is not used and was deprecated, + - non-empty ``kwargs`` raise error. + """ @pytest.fixture def mesh(self): return TensorMesh((5, 5, 5)) - def test_warning(self, mesh): + @pytest.fixture + def survey(self): """Test if warning is raised after passing ``option`` as argument.""" receivers_list = [ receivers.Dipole( @@ -54,7 +66,22 @@ def test_warning(self, mesh): receivers_list, location_a=[0.5, 1.5, 2.5], location_b=[4.5, 5.5, 6.5] ) ] - survey = Survey(source_list=sources_list) + return Survey(source_list=sources_list) + + def test_error_ind_active(self, mesh): + """ + Test if error is raised after passing ``ind_active`` as argument. + """ + survey = Survey(source_list=[]) + active_cells = np.ones(mesh.n_cells, dtype=bool) + msg = re.escape("Unsupported keyword argument") + with pytest.raises(TypeError, match=msg): + survey.drape_electrodes_on_topography( + mesh, active_cells, ind_active=active_cells + ) + + def test_deprecated_option(self, mesh, survey): + """Test if warning is raised after passing ``option`` as argument.""" msg = ( "Argument ``option`` is deprecated in favor of ``topo_cell_cutoff`` " "and will be removed in SimPEG v0.27.0." @@ -63,6 +90,42 @@ def test_warning(self, mesh): with pytest.warns(FutureWarning, match=msg): survey.drape_electrodes_on_topography(mesh, active_cells, option="top") + def test_deprecated_topography(self, mesh, survey): + """ + Test warning after passing ``topography`` as argument. + """ + active_cells = np.ones(mesh.n_cells, dtype=bool) + msg = re.escape("The `topography` argument is not used") + with pytest.warns(FutureWarning, match=msg): + survey.drape_electrodes_on_topography(mesh, active_cells, topography="blah") + + def test_deprecated_force(self, mesh, survey): + """ + Test warning after passing ``force`` as argument. + """ + active_cells = np.ones(mesh.n_cells, dtype=bool) + msg = re.escape("The `force` argument is not used") + with pytest.warns(FutureWarning, match=msg): + survey.drape_electrodes_on_topography(mesh, active_cells, force="blah") + + @pytest.mark.filterwarnings( + r"ignore:The `force` argument is not used:FutureWarning" + ) + @pytest.mark.filterwarnings( + r"ignore:The `topography` argument is not used:FutureWarning" + ) + def test_non_empty_kwargs(self, mesh): + """ + Test error after passing non empty kwargs. + """ + survey = Survey(source_list=[]) + active_cells = np.ones(mesh.n_cells, dtype=bool) + msg = re.escape("Unsupported keyword argument") + with pytest.raises(TypeError, match=msg): + survey.drape_electrodes_on_topography( + mesh, active_cells, force="blah", topography="blah", other_arg="blah" + ) + def test_repr(): """Test the __repr__ method of the survey.""" From 4378511f7cc81057f529b41c36d3c1cd216d5cd0 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Mon, 20 Oct 2025 23:58:17 +0000 Subject: [PATCH 189/194] Fix LaTeX equations in `Simulation3DDifferential` (#1726) Fix a few LaTeX equations in `simpeg.potential_fields.magnetics.Simulation3DDifferential` that weren't rendering correctly. --- simpeg/potential_fields/magnetics/simulation.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/simpeg/potential_fields/magnetics/simulation.py b/simpeg/potential_fields/magnetics/simulation.py index a44cc552d7..6b403ddda8 100644 --- a/simpeg/potential_fields/magnetics/simulation.py +++ b/simpeg/potential_fields/magnetics/simulation.py @@ -1686,15 +1686,17 @@ class Simulation3DDifferential(BaseMagneticPDESimulation): This simulation solves for the magnetostatic PDE: .. math:: - \nabla \cdot \Vec{B} = 0 + + \nabla \cdot \mathbf{B} = 0 where the constitutive relation is specified as: .. math:: - \Vec{B} = \mu\Vec{H} + \mu_0\Vec{M_r} - where :math:`\Vec{M_r}` is a fixed magnetization unaffected by the inducing field - and :math:`\mu\Vec{H}` is the induced magnetization. + \mathbf{B} = \mu\mathbf{H} + \mu_0\mathbf{M_r} + + where :math:`\mathbf{M_r}` is a fixed magnetization unaffected by the inducing field + and :math:`\mu\mathbf{H}` is the induced magnetization. """ _Ainv = None From 45c3bc9fe3c6ed7a2a8469b6a8002a9f91ec8adb Mon Sep 17 00:00:00 2001 From: Lindsey Heagy Date: Tue, 21 Oct 2025 12:09:29 -0700 Subject: [PATCH 190/194] Implement a closed loop as a TDEM source (#1651) Implement the B field computation from a closed-loop wire source in the TDEM code. We first compute the vector potential using the Biot-Savart Law (implemented in geoana) and then take the curl to get the initial B-field. Adds the computation of B with a step-off waveform using the B or H formulations. --------- Co-authored-by: Joseph Capriotti Co-authored-by: Santiago Soler --- .../electromagnetics/time_domain/sources.py | 125 ++++++++---- tests/em/tdem/test_large_loop.py | 180 ++++++++++++++++++ 2 files changed, 267 insertions(+), 38 deletions(-) create mode 100644 tests/em/tdem/test_large_loop.py diff --git a/simpeg/electromagnetics/time_domain/sources.py b/simpeg/electromagnetics/time_domain/sources.py index 67b684bbe5..3658e5eb11 100644 --- a/simpeg/electromagnetics/time_domain/sources.py +++ b/simpeg/electromagnetics/time_domain/sources.py @@ -3,7 +3,11 @@ from discretize.utils import mkvc import numpy as np -from geoana.em.static import CircularLoopWholeSpace, MagneticDipoleWholeSpace +from geoana.em.static import ( + CircularLoopWholeSpace, + MagneticDipoleWholeSpace, + LineCurrentWholeSpace, +) from scipy.constants import mu_0 from ...utils import Zero, sdiag @@ -1620,7 +1624,9 @@ def __init__( srcType=None, **kwargs, ): - super().__init__(receiver_list=receiver_list, location=location, **kwargs) + super().__init__( + receiver_list=receiver_list, location=location, srcType=srcType, **kwargs + ) for rx in self.receiver_list: if getattr(rx, "use_source_receiver_offset", False): raise ValueError( @@ -1898,37 +1904,56 @@ def jInitialDeriv(self, simulation, v=None, adjoint=False, f=None): ) def _getAmmr(self, simulation): - if simulation._formulation != "HJ": + if simulation._formulation == "EB": raise NotImplementedError - - vol = simulation.mesh.cell_volumes - Div = sdiag(vol) * simulation.mesh.face_divergence - return ( - simulation.mesh.edge_curl - * simulation.MeMuI - * simulation.mesh.edge_curl.T.tocsr() - - Div.T.tocsr() - * sdiag(1.0 / vol * simulation.mui) - * Div # stabalizing term. See (Chen, Haber & Oldenburg 2002) - ) + elif simulation._formulation == "HJ": + vol = simulation.mesh.cell_volumes + Div = sdiag(vol) * simulation.mesh.face_divergence + return ( + simulation.mesh.edge_curl + * simulation.MeMuI + * simulation.mesh.edge_curl.T.tocsr() + - Div.T.tocsr() + * sdiag(1.0 / vol * simulation.mui) + * Div # stabalizing term. See (Chen, Haber & Oldenburg 2002) + ) def _aInitial(self, simulation): - A = self._getAmmr(simulation) - Ainv = simulation.solver(A) # todo: store this - s_e = self.s_e(simulation, 0) - rhs = s_e + self.jInitial(simulation) - return Ainv * rhs + if self.srcType == "inductive": + # for an inductive source, use the Biot Savart law (from geoana) to compute the vector potential + line_current = LineCurrentWholeSpace(self.location) + if simulation._formulation == "EB": + vector_potential = simulation.mesh.project_edge_vector( + line_current.vector_potential(simulation.mesh.edges) + ) + elif simulation._formulation == "HJ": + vector_potential = simulation.mesh.project_face_vector( + line_current.vector_potential(simulation.mesh.faces) + ) + return self.current * vector_potential + else: + # if a grounded source, solve the MMR problem + A = self._getAmmr(simulation) + Ainv = simulation.solver(A) # todo: store this + s_e = self.s_e(simulation, 0) + rhs = s_e + self.jInitial(simulation) + return Ainv * rhs def _aInitialDeriv(self, simulation, v, adjoint=False): - A = self._getAmmr(simulation) - Ainv = simulation.solver(A) # todo: store this - move it to the simulation + if self.srcType == "inductive": + # the vector potential doesn't depend on the model for an inductive source + return Zero() + else: + # for a grounded source, the derivatives are obtained from the MMR problem + A = self._getAmmr(simulation) + Ainv = simulation.solver(A) # todo: store this - move it to the simulation - if adjoint is True: - return self.jInitialDeriv( - simulation, Ainv * v, adjoint=True - ) # A is symmetric + if adjoint is True: + return self.jInitialDeriv( + simulation, Ainv * v, adjoint=True + ) # A is symmetric - return Ainv * self.jInitialDeriv(simulation, v) + return Ainv * self.jInitialDeriv(simulation, v) def hInitial(self, simulation): """Compute initial magnetic field. @@ -1950,8 +1975,29 @@ def hInitial(self, simulation): if self.waveform.has_initial_fields is False: return Zero() - b = self.bInitial(simulation) - return simulation.MeMuI * b + if simulation._formulation == "EB": + return 1 / self.mu * self.bInitial(simulation) + elif simulation._formulation == "HJ": + a = self._aInitial(simulation) + + if self.srcType == "inductive": + line_current = LineCurrentWholeSpace(self.location) + a_boundary = mkvc( + line_current.vector_potential(simulation.mesh.boundary_edges) + ) + a_bc = simulation.mesh.boundary_edge_vector_integral * a_boundary + + return ( + 1.0 + / self.mu + * simulation.MeI + * simulation.mesh.edge_curl.T + * simulation.Mf + * a + - 1 / self.mu * simulation.MeI * a_bc + ) + else: + return simulation.MeMuI * simulation.mesh.edge_curl.T * a def hInitialDeriv(self, simulation, v, adjoint=False, f=None): """Compute derivative of intitial magnetic field times a vector @@ -1996,11 +2042,13 @@ def bInitial(self, simulation): if self.waveform.has_initial_fields is False: return Zero() - elif simulation._formulation != "HJ": - raise NotImplementedError - a = self._aInitial(simulation) - return simulation.mesh.edge_curl.T * a + if simulation._formulation == "EB": + a = self._aInitial(simulation) + return simulation.mesh.edge_curl * a + elif simulation._formulation == "HJ": + # return simulation.mesh.edge_curl.T * a + return self.mu * self.hInitial(simulation) def bInitialDeriv(self, simulation, v, adjoint=False, f=None): """Compute derivative of intitial magnetic flux density times a vector @@ -2021,8 +2069,6 @@ def bInitialDeriv(self, simulation, v, adjoint=False, f=None): """ if self.waveform.has_initial_fields is False: return Zero() - elif simulation._formulation != "HJ": - raise NotImplementedError if adjoint is True: return self._aInitialDeriv( @@ -2059,11 +2105,14 @@ def s_e(self, simulation, time): # on faces class RawVec_Grounded(LineCurrent): def __init__(self, receiver_list=None, s_e=None, **kwargs): + + srcType = kwargs.pop("srcType", None) + if srcType is not None and srcType != "galvanic": + raise ValueError( + "expected srcType to be 'galvanic' for the RawVec_Grounded" + ) + super().__init__(receiver_list, srcType="galvanic", **kwargs) self.integrate = False - kwargs.pop("srcType", None) - super(RawVec_Grounded, self).__init__( - receiver_list, srcType="galvanic", **kwargs - ) if s_e is not None: self._Mfjs = self._s_e = s_e diff --git a/tests/em/tdem/test_large_loop.py b/tests/em/tdem/test_large_loop.py new file mode 100644 index 0000000000..88e08904a1 --- /dev/null +++ b/tests/em/tdem/test_large_loop.py @@ -0,0 +1,180 @@ +import numpy as np +import pytest + +import discretize +from simpeg.electromagnetics import time_domain as tdem +from simpeg import maps + +# solver +from simpeg.utils.solver_utils import get_default_solver + + +Solver = get_default_solver() + +# conductivity values +rho_back = 500 +sigma_air = 1e-8 +sigma_back = 1 / rho_back + +current = 2 +REL_TOL = 0.35 + + +def setup_mesh_model(tx_halfwidth=50): + + # design a tensor mesh + cell_size = 20 + padding_factor = 1.5 + + n_cells_x = int(tx_halfwidth * 2 / cell_size) + n_cells_z = int((tx_halfwidth) / cell_size) + 5 + n_padding_x = 11 + n_padding_z = 11 + + hx = [ + (cell_size, n_padding_x, -padding_factor), + (cell_size, n_cells_x), + (cell_size, n_padding_z, padding_factor), + ] + + hz = [ + (cell_size, n_padding_z, -padding_factor), + (cell_size, n_cells_z), + (cell_size, n_padding_z, padding_factor), + ] + + mesh = discretize.TensorMesh([hx, hx, hz], origin="CC0") + mesh.origin = mesh.origin - np.r_[0, 0, mesh.h[2][: n_padding_z + n_cells_z].sum()] + mesh.n_cells + + # define model + model = sigma_air * np.ones(mesh.n_cells) + model[mesh.cell_centers[:, 2] < 0] = sigma_back + + return mesh, model + + +def setup_survey(rx_locs, rx_times, tx_halfwidth=50, tx_z=0.5): + # transmitter + tx_halfwidth = 50 + tx_z = 0.5 # put slightly above the surface + tx_points = np.array( + [ + [-tx_halfwidth, -tx_halfwidth, tx_z], + [tx_halfwidth, -tx_halfwidth, tx_z], + [tx_halfwidth, tx_halfwidth, tx_z], + [-tx_halfwidth, tx_halfwidth, tx_z], + [-tx_halfwidth, -tx_halfwidth, tx_z], # close the loop + ] + ) + + # define survey for 3D simulation + dbdt_receivers = [ + tdem.receivers.PointMagneticFluxTimeDerivative( + locations=rx_locs, times=rx_times, orientation="z" + ) + ] + + b_receivers = [ + tdem.receivers.PointMagneticFluxDensity( + locations=rx_locs, times=rx_times, orientation=orientation + ) + for orientation in ["z"] + ] + + waveform = tdem.sources.StepOffWaveform() + + src = tdem.sources.LineCurrent( + receiver_list=b_receivers + dbdt_receivers, + location=tx_points, + waveform=waveform, + srcType="inductive", + current=current, + ) + + survey = tdem.Survey([src]) + + return survey + + +def setup_simulation(mesh, survey, simulation_type="EB"): + + nsteps = 20 + time_steps = [ + (3e-6, nsteps), + (1e-5, nsteps), + (3e-5, nsteps), + (1e-4, nsteps), + (3e-4, nsteps + 4), + ] + + if simulation_type == "EB": + simulation = tdem.simulation.Simulation3DMagneticFluxDensity( + mesh=mesh, + survey=survey, + time_steps=time_steps, + solver=Solver, + sigmaMap=maps.IdentityMap(mesh), + ) + elif simulation_type == "HJ": + simulation = tdem.simulation.Simulation3DMagneticField( + mesh=mesh, + survey=survey, + time_steps=time_steps, + solver=Solver, + sigmaMap=maps.IdentityMap(mesh), + ) + return simulation + + +@pytest.mark.parametrize("simulation_type", ["EB", "HJ"]) +def test_large_loop(simulation_type): + + tx_halfwidth = 50 + + # receiver times + rx_times = 1e-3 * np.logspace(-1, 1, 30) + + rx_x = np.r_[20] # np.linspace(-100, 100, 10) + rx_y = np.r_[20] # np.linspace(-100, 100, 10) + rx_z = np.r_[0] + + rx_locs = discretize.utils.ndgrid(rx_x, rx_y, rx_z) + + mesh, model = setup_mesh_model(tx_halfwidth=tx_halfwidth) + survey = setup_survey(rx_locs=rx_locs, rx_times=rx_times, tx_halfwidth=tx_halfwidth) + simulation = setup_simulation(mesh, survey, simulation_type) + + fields = simulation.fields(model) + dpred_numeric = simulation.dpred(model, f=fields) + + # define 1D simulation + dbdt_receivers1d = [ + tdem.receivers.PointMagneticFluxTimeDerivative( + locations=rx_locs, times=rx_times, orientation="z" + ) + ] + + b_receivers1d = [ + tdem.receivers.PointMagneticFluxDensity( + locations=rx_locs, times=rx_times, orientation="z" + ) + ] + + waveform = tdem.sources.StepOffWaveform() + src1d = tdem.sources.LineCurrent( + receiver_list=b_receivers1d + dbdt_receivers1d, + location=survey.source_list[0].location, + waveform=waveform, + srcType="inductive", + ) + + survey1d = tdem.Survey([src1d]) + + simulation_1D = tdem.simulation_1d.Simulation1DLayered( + survey=survey1d, sigmaMap=maps.IdentityMap() + ) + + dpred1d = simulation_1D.dpred(sigma_back) * current + + np.testing.assert_allclose(dpred_numeric, dpred1d, rtol=REL_TOL) From 625bad33466c9050188a8bd1362afde119d90a7e Mon Sep 17 00:00:00 2001 From: Joseph Capriotti Date: Wed, 22 Oct 2025 15:52:26 -0600 Subject: [PATCH 191/194] Allow for specifying ramp start and ramp end in `RampOffWaveform` (#1714) Allow passing `ramp_start` and `ramp_end` to the `RampOffWaveform` TDEM source and fix issue with waveform being wrong at times before `-epsilon`. Deprecate the `off_time` argument. --- .../electromagnetics/time_domain/sources.py | 130 ++++++++++++-- tests/em/tdem/test_TDEM_sources.py | 168 ++++++++++++++++-- 2 files changed, 271 insertions(+), 27 deletions(-) diff --git a/simpeg/electromagnetics/time_domain/sources.py b/simpeg/electromagnetics/time_domain/sources.py index 3658e5eb11..c0d7473237 100644 --- a/simpeg/electromagnetics/time_domain/sources.py +++ b/simpeg/electromagnetics/time_domain/sources.py @@ -181,13 +181,22 @@ def eval(self, time): # noqa: A003 class RampOffWaveform(BaseWaveform): - """ + """RampOffWaveform([ramp_start,] ramp_end) + A waveform with a linear ramp-off. + ``RampOffWaveform`` can be called with a varying number of positional arguments: + + * ``RampOffWaveform(ramp_end)``: Specify only the ramp end time, with an implied ramp + start time of zero. + * ``RampOffWaveform(ramp_start, ramp_end)``: Specify both the ramp start and end times. + Parameters ---------- - off_time : float, default: 0.0 - time at which the transmitter is turned off in units of seconds + ramp_start : float, optional + Time the ramp off portion of the waveform starts. The default start value is 0. + ramp_end : float + Time at which the ramp off ends. Examples -------- @@ -197,20 +206,115 @@ class RampOffWaveform(BaseWaveform): >>> from simpeg.electromagnetics import time_domain as tdem >>> times = np.linspace(0, 1e-4, 1000) - >>> waveform = tdem.sources.RampOffWaveform(off_time=1e-5) + >>> waveform = tdem.sources.RampOffWaveform(1e-5) >>> plt.plot(times, [waveform.eval(t) for t in times]) >>> plt.show() """ - def __init__(self, off_time=0.0, **kwargs): - super().__init__(off_time=off_time, has_initial_fields=True, **kwargs) + def __init__(self, *args, ramp_start=None, ramp_end=None, **kwargs): + off_time = kwargs.pop("off_time", None) + if off_time is not None: + if len(args) > 1 or ramp_end is not None: + raise TypeError( + "Can not specify both `off_time` and a `ramp_end` value." + ) + ramp_end = off_time + warnings.warn( + "`off_time` keyword arg has been deprecated and will be removed in " + "SimPEG v0.27.0, pass the ramp end time as the last positional argument.`", + DeprecationWarning, + stacklevel=2, + ) + + nargs = len(args) + if nargs == 0: + if ramp_end is None: + raise TypeError( + "RampOffWaveform() requires `ramp_end` to be specified." + ) + if ramp_start is None: + ramp_start = 0.0 + elif nargs == 1: + if ramp_start is not None: + raise TypeError( + "argument for RampOffWaveform() given by name ('ramp_start') and position (position 0)" + ) + if ramp_end is not None: + ramp_start = args[0] + else: + ramp_start = 0 + ramp_end = args[0] + elif nargs == 2: + if ramp_start is not None: + raise TypeError( + "argument for RampOffWaveform() given by name ('ramp_start') and position (position 0)" + ) + if ramp_end is not None: + raise TypeError( + "argument for RampOffWaveform() given by name ('ramp_end') and position (position 1)" + ) + ramp_start, ramp_end = args + else: + raise TypeError( + "Must specify one or two positional arguments for the RampOffWaveform." + ) + + self.ramp_start = ramp_start + super().__init__(off_time=ramp_end, has_initial_fields=True, **kwargs) + + @property + def ramp_start(self): + """Ramp start time + + Sets the time the ramp off begins. + + Returns + ------- + float + The start time of the ramp off for the waveform + """ + return self._ramp_start + + @ramp_start.setter + def ramp_start(self, value): + self._ramp_start = validate_float("ramp_start", value) + + @property + def ramp_end(self): + """Ramp end time, when the current is off. + + Sets the time the ramp off ends. + + Returns + ------- + float + The end time of the ramp off for the waveform + """ + return self._ramp_end + + @ramp_end.setter + def ramp_end(self, value): + """ "off-time of the source""" + self._ramp_end = validate_float( + "ramp_end", value, min_val=self.ramp_start, inclusive_min=False + ) + + @property + def off_time(self): + return self.ramp_end + + @off_time.setter + def off_time(self, value): + self.ramp_end = value def eval(self, time): # noqa: A003 - if abs(time - 0.0) < self.epsilon: + dt = time - self.ramp_start + if dt < self.epsilon: return 1.0 - elif time < self.off_time: - return -1.0 / self.off_time * (time - self.off_time) + elif time < self.ramp_end: + ramp_width = self.ramp_end - self.ramp_start + return 1 - dt / ramp_width else: return 0.0 @@ -218,8 +322,10 @@ def eval_deriv(self, time): t = np.asarray(time, dtype=float) out = np.zeros_like(t) - if self.off_time > 0: - out[(t < self.off_time) & (t >= self.epsilon)] = -1.0 / self.off_time + ramp_width = self.ramp_end - self.ramp_start + out[(t < self.ramp_end) & ((t - self.ramp_start) >= self.epsilon)] = ( + -1.0 / ramp_width + ) if out.ndim == 0: out = out.item() @@ -227,7 +333,7 @@ def eval_deriv(self, time): @property def time_nodes(self): - return np.r_[0.0, self.off_time] + return np.r_[self.ramp_start, self.ramp_end] class RawWaveform(BaseWaveform): diff --git a/tests/em/tdem/test_TDEM_sources.py b/tests/em/tdem/test_TDEM_sources.py index 6c7d248543..5eac6e252f 100644 --- a/tests/em/tdem/test_TDEM_sources.py +++ b/tests/em/tdem/test_TDEM_sources.py @@ -1,5 +1,7 @@ import unittest +import re +import pytest import numpy as np import scipy.sparse as sp from discretize.tests import check_derivative @@ -36,26 +38,30 @@ def test_waveform_with_custom_off_time(self): assert_array_almost_equal(result, expected) -class TestRampOffWaveform(unittest.TestCase): - @classmethod - def setUpClass(cls): - cls.times = np.linspace(start=0, stop=1e-2, num=11) +@pytest.mark.parametrize("ramp_start", [-1e-3, None, 0, 1e-3]) +@pytest.mark.parametrize("ramp_end", [1e-2, 5e-3]) +class TestRampOffWaveform: + times = np.linspace(start=-1e-2, stop=2e-2, num=31) - def test_waveform_with_whole_offtime(self): - ramp_off = RampOffWaveform(off_time=1e-2) + def test_waveform_evaluate(self, ramp_start, ramp_end): + if ramp_start is None: + args = (ramp_end,) + else: + args = (ramp_start, ramp_end) + ramp_off = RampOffWaveform(*args) + if ramp_start is None: + assert ramp_off.ramp_start == 0.0 result = [ramp_off.eval(t) for t in self.times] - expected = np.array([1.0, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1, 0.0]) + expected = np.interp(self.times, ramp_off.time_nodes, [1, 0]) assert_array_almost_equal(result, expected) - def test_waveform_with_partial_off_time(self): - ramp_off = RampOffWaveform(off_time=5e-3) - result = [ramp_off.eval(t) for t in self.times] - expected = np.array([1.0, 0.8, 0.6, 0.4, 0.2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) - assert_array_almost_equal(result, expected) - - def test_waveform_derivative(self): + def test_waveform_derivative(self, ramp_start, ramp_end): # Test the waveform derivative at points between the time_nodes - wave = RampOffWaveform(off_time=1e-2) + if ramp_start is None: + args = (ramp_end,) + else: + args = (ramp_start, ramp_end) + wave = RampOffWaveform(*args) def f(t): wave_eval = np.array([wave.eval(ti) for ti in t]) @@ -74,6 +80,138 @@ def f(t): assert check_derivative(f, t0, dx=dt, plotIt=False, random_seed=5421) +@pytest.mark.parametrize("attr", ["ramp_end", "off_time"]) +def test_ramp_off_time_is_ramp_end(attr): + t_off = 0.01 + if attr == "ramp_end": + ramp = RampOffWaveform(t_off) + else: + with pytest.warns( + DeprecationWarning, match="`off_time` keyword arg has been deprecated.*" + ): + ramp = RampOffWaveform(off_time=t_off) + assert ramp.ramp_end == t_off + assert ramp.off_time == t_off + + t2_off = 0.02 + setattr(ramp, attr, t2_off) + assert ramp.ramp_end == t2_off + assert ramp.off_time == t2_off + + +def test_ramp_off_bad_end(): + with pytest.raises( + ValueError, + match=re.escape("'ramp_end' must be a value in the range (0.1, inf]"), + ): + RampOffWaveform(0.1, 0.0) + + +def test_ramp_off_good_args(): + with pytest.warns( + DeprecationWarning, match="`off_time` keyword arg has been deprecated.*" + ): + ramp = RampOffWaveform(off_time=0.1) + assert ramp.ramp_start == 0.0 + assert ramp.ramp_end == 0.1 + + ramp = RampOffWaveform(0.1) + assert ramp.ramp_start == 0.0 + assert ramp.ramp_end == 0.1 + + ramp = RampOffWaveform(ramp_end=0.1) + assert ramp.ramp_start == 0.0 + assert ramp.ramp_end == 0.1 + + ramp = RampOffWaveform(0.1, 0.2) + assert ramp.ramp_start == 0.1 + assert ramp.ramp_end == 0.2 + + ramp = RampOffWaveform(0.1, ramp_end=0.2) + assert ramp.ramp_start == 0.1 + assert ramp.ramp_end == 0.2 + + ramp = RampOffWaveform(ramp_start=0.1, ramp_end=0.2) + assert ramp.ramp_start == 0.1 + assert ramp.ramp_end == 0.2 + + ramp = RampOffWaveform(ramp_end=0.2, ramp_start=0.1) + assert ramp.ramp_start == 0.1 + assert ramp.ramp_end == 0.2 + + with pytest.warns( + DeprecationWarning, match="`off_time` keyword arg has been deprecated.*" + ): + ramp = RampOffWaveform(0.1, off_time=0.2) + assert ramp.ramp_start == 0.1 + assert ramp.ramp_end == 0.2 + + with pytest.warns( + DeprecationWarning, match="`off_time` keyword arg has been deprecated.*" + ): + ramp = RampOffWaveform(ramp_start=0.1, off_time=0.2) + assert ramp.ramp_start == 0.1 + assert ramp.ramp_end == 0.2 + + +def test_ramp_off_bad_args(): + with pytest.raises( + TypeError, + match=re.escape("Can not specify both `off_time` and a `ramp_end` value."), + ): + RampOffWaveform(0.01, 0.2, off_time=0.1) + with pytest.raises( + TypeError, + match=re.escape("Can not specify both `off_time` and a `ramp_end` value."), + ): + RampOffWaveform(ramp_end=0.2, off_time=0.1) + with pytest.raises( + TypeError, + match=re.escape("RampOffWaveform() requires `ramp_end` to be specified."), + ): + RampOffWaveform() + with pytest.raises( + TypeError, + match=re.escape("RampOffWaveform() requires `ramp_end` to be specified."), + ): + RampOffWaveform(ramp_start=0.0) + with pytest.raises( + TypeError, + match=re.escape( + "Must specify one or two positional arguments for the RampOffWaveform." + ), + ): + RampOffWaveform(0.1, 0.2, 0.3) + with pytest.raises( + TypeError, + match=re.escape( + "argument for RampOffWaveform() given by name ('ramp_start') and position (position 0)" + ), + ): + RampOffWaveform(0.1, ramp_start=0.0) + with pytest.raises( + TypeError, + match=re.escape( + "argument for RampOffWaveform() given by name ('ramp_start') and position (position 0)" + ), + ): + RampOffWaveform(0.1, 0.2, ramp_start=0.0) + with pytest.raises( + TypeError, + match=re.escape( + "argument for RampOffWaveform() given by name ('ramp_start') and position (position 0)" + ), + ): + RampOffWaveform(0.1, 0.2, ramp_start=0.1, ramp_end=0.2) + with pytest.raises( + TypeError, + match=re.escape( + "argument for RampOffWaveform() given by name ('ramp_end') and position (position 1)" + ), + ): + RampOffWaveform(0.1, 0.2, ramp_end=0.0) + + class TestVTEMWaveform(unittest.TestCase): @classmethod def setUpClass(cls): From 5f0e7c713234ed5ee25bb0826d3fa5d8f8396f64 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 23 Oct 2025 00:31:59 +0000 Subject: [PATCH 192/194] Add changelog for SimPEG v0.25.0 (#1725) Add changelog for next release of SimPEG: v0.25.0. Add the new version to the version switcher. --------- Co-authored-by: Joseph Capriotti Co-authored-by: Lindsey Heagy --- docs/_static/versions.json | 11 +- docs/content/release/0.25.0-notes.rst | 372 ++++++++++++++++++ .../getting-started/version-compatibility.rst | 2 + .../how-to-guide/move-mesh-to-survey.rst | 2 + 4 files changed, 384 insertions(+), 3 deletions(-) create mode 100644 docs/content/release/0.25.0-notes.rst diff --git a/docs/_static/versions.json b/docs/_static/versions.json index b35427db2c..c5a34bd9c5 100644 --- a/docs/_static/versions.json +++ b/docs/_static/versions.json @@ -4,11 +4,16 @@ "url": "https://docs.simpeg.xyz/dev/" }, { - "name": "v0.24.0 (latest)", - "version": "0.24.0", - "url": "https://docs.simpeg.xyz/v0.24.0/", + "name": "v0.25.0 (latest)", + "version": "0.25.0", + "url": "https://docs.simpeg.xyz/v0.25.0/", "preferred": true }, + { + "name": "v0.24.0", + "version": "0.24.0", + "url": "https://docs.simpeg.xyz/v0.24.0/" + }, { "name": "v0.23.0", "version": "0.23.0", diff --git a/docs/content/release/0.25.0-notes.rst b/docs/content/release/0.25.0-notes.rst new file mode 100644 index 0000000000..970c7fada3 --- /dev/null +++ b/docs/content/release/0.25.0-notes.rst @@ -0,0 +1,372 @@ +.. _0.25.0_notes: + +============================ +SimPEG v0.25.0 Release Notes +============================ + +October 21st, 2025 + +.. contents:: Highlights + :depth: 3 + +Updates +======= + +New features +------------ + +New differential simulation for magnetic fields +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This release ships with a new and improved version of the magnetic differential +simulation +:class:`~simpeg.potential_fields.magnetics.Simulation3DDifferential` by +`@johnweis0480 `__. +This simulation computes the magnetic field on every cell of the mesh by +numerically solving the magnetostatic PDE. It accounts for +self-demagnetization effects, model both induced and remanent magnetizations, +and is faster and less memory intensive than the integral simulation for large +problems. + +See https://github.com/simpeg/simpeg/pull/1682 for more details. + +Add utility function to shift electrodes to discrete topography +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A new `shift_to_discrete_topography` function shifts locations relative +to discrete surface topography. When performing MT surveys, we measure +electric fields at the Earth's surface. +Similar to DC/IP, the original measurement locations of the electric fields can +end up in air cells when we discretize surface topography. This function allows +the user to shift locations relative to discrete topography on Tensor and Tree +meshes. +For Airborne NSEM, they also allow the user to preserve the original flight +heights. + +See https://github.com/simpeg/simpeg/pull/1683 for more details. + +Calculate B/H fields with a step-off waveform closed-loop wire source in TDEM +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +We can now calculate the B and H fields using a closed-loop wire as source in +the TDEM code with a step-off waveform. +The simulation will first compute the vector potential using the Biot-Savart +Law and then take the curl of it to get the initial :math:`\mathbf{B}` field. + +See https://github.com/simpeg/simpeg/pull/1651 for more details. + +Sensitivity matrix as a ``LinearOperator`` in gravity and magnetic equivalent layers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Gravity and magnetic equivalent sources can now define the sensitivity +matrix ``J`` as a :class:`scipy.sparse.linalg.LinearOperator` when +``store_sensitivities="forward_only"``. This extends the behaviour previously +implemented in the integral gravity and magnetic simulations to the +equivalent layer classes. + +See https://github.com/simpeg/simpeg/pull/1674 and +https://github.com/simpeg/simpeg/pull/1676 for more details. + +Choosing the default solver is easier now +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :func:`~simpeg.utils.get_default_solver` can now be imported directly from +:mod:`simpeg.utils` making it easier to use it. + +Check out the new How to Guide on :ref:`choosing-solvers` for more information +on how we can use this function. + +Improved inversion printout +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The information table displayed during an inversion has been improved. Now, the +zero-th iteration corresponds to the status of the inversion problem **before** +any optimization steps, while subsequent iterations show information **after** +each iteration but before directives are applied. +The final iteration of the inversion is shown in the last row of the table. +Additionally, some non-very-useful messages have been removed to produce +a cleaner output. + +See https://github.com/simpeg/simpeg/pull/1626 for more details. + +Standardized directives for saving outputs +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Directives that store and save inversion outputs have been +standardized and made more reliable. They now respect the output directory +chosen by the user, and output filenames follow a standardized +``name-timestamp-iteration`` format to make it easier to sort and identify +files from different inversions. + +See https://github.com/simpeg/simpeg/pull/1657 for more details. + +Updates to the Conjugate Gradient minimizers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The conjugate gradient minimizers were updated to be consistent with the latest +versions of SciPy. They can now accept both relative and absolute tolerances +through the ``cg_rtol`` and ``cg_atol`` arguments, respectively. + +The ``tolCG`` argument will be removed in the future, making ``cg_rtol`` and +``cg_atol`` the preferred way to set tolerances in these minimizers. + +See https://github.com/simpeg/simpeg/pull/1656 for more details. + + + +Documentation +------------- + +This release introduces a fresh new landing page for SimPEG docs, and a new +**How to Guide** section in our :ref:`user_guide` with pages on +:ref:`choosing-solvers` and :ref:`how-to-move-mesh`. + +We also included a new page that clarifies Python and Numpy +:ref:`version-compatibility` with SimPEG, and explain the criteria for dropping +older versions of our dependencies. + +We started removing the gravity, magnetic and DC tutorials from SimPEG's docs, +as part of our plan of moving all tutorials to our `User Tutorials +`_. + +Now we can navigate our docs using our arrow keys in the keyboard (for those +power users that don't want to leave the keyboard) thanks to `@prisae +`__. + +Finally, we improved and fixed a few things in the docs: mathematical +expressions, added missing classes to the API reference, updated admonitions in +docstrings, and more. + +Bugfixes +-------- + +In this release we included a few bugfixes: + +- Fixes sign error in 1D field calculation. by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1662 +- Fix beta cooling in ``UpdateIRLS`` directive by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1659 +- Fix bug in phase for recursive 1d NSEM simulation by `@dccowan `__ in + https://github.com/simpeg/simpeg/pull/1679 +- Fix bug on ``Impedance.eval`` when orientation is “xx” or “yy” by + `@dccowan `__ in https://github.com/simpeg/simpeg/pull/1692 +- Fix magnetic dipole source for for HJ formulation by `@lheagy `__ in + https://github.com/simpeg/simpeg/pull/1575 +- Fix bug with duplicated current in ``LineCurrent.Mejs`` by `@santisoler `__ + in https://github.com/simpeg/simpeg/pull/1718 + +Breaking changes +---------------- + +We introduce a few breaking changes in SimPEG v0.25.0. + +Dropped support for Python 3.10 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +We dropped support for Python 3.10, inline with our +:ref:`version-compatibility` schedule. So, remember to use Python 3.11 or higher +when installing SimPEG v0.25.0. If you still need to use Python 3.10, please +pin your SimPEG version to v0.24.0. + +Modified how mappings are applied in regularizations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +We updated how mappings are applied in most of our regularization classes +(:class:`~simpeg.regularization.WeightedLeastSquares`, +:class:`~simpeg.regularization.Smallness`, +:class:`~simpeg.regularization.SmoothnessFirstOrder`, +:class:`~simpeg.regularization.Sparse`, +etc.). The ``mapping`` was applied, for example in the +:class:`~simpeg.regularization.Smallness` regularization, to the difference +between the ``model`` and the ``reference_model``: + +.. math:: + + \phi (\mathbf{m}) = + \left\lVert + \mathbf{W} \left[ \mu(\mathbf{m} - \mathbf{m}^\text{ref}) \right] + \right\rVert^2. + +where :math:`\mu()` is the ``mapping``. + +Since SimPEG v0.25.0 the regularizations are applied to the difference between +the *mapped* model and the *mapped* regularization model: + +.. math:: + + \phi (\mathbf{m}) = + \left\lVert + \mathbf{W} \left[ \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) \right] + \right\rVert^2. + +This impacts only non-linear mappings, since the two expressions are equivalent +for linear ones. + +Changed the output of :func:`~simpeg.utils.model_builder.get_indices_block` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :func:`~simpeg.utils.model_builder.get_indices_block` function previously +returned a tuple with just a single element: the array with cell indices that +correspond the given block. We standardized its output to be in agreement with +similar functions in the module. It now returns a single NumPy array with the +cell indices of the block. + +If you were using this function as follows, where you used to extract the first +element of the tuple: + +.. code:: python + + ind = get_indices_block(p0, p1, mesh.cell_centers)[0] + +You'll need to update it to: + +.. code:: python + + ind = get_indices_block(p0, p1, mesh.cell_centers) + +An informative warning will be printed out every time the function is used to +remind users of this new behaviour. + +Removals +~~~~~~~~ + +We also removed several deprecated items marked for removal in previous +releases, including: + +- The `Data.index_dictionary` property. Use the new ``get_slice`` method of + ``Survey`` (for example: + :meth:`simpeg.potential_fields.gravity.Survey.get_slice`). +- The `gtg_diagonal` property from gravity simulation. +- The `components` property from gravity and magnetic surveys. + + +Contributors +============ + +Contributors: + +* `@dccowan `__ +* `@jcapriot `__ +* `@johnweis0480 `__ +* `@lheagy `__ +* `@prisae `__ +* `@santisoler `__ +* `@williamjsdavis `__ +* `@YingHuuu `__ +* `@domfournier `__ + + +Pull Requests +============= + +- Update docstring descriptions for gravity gradient component guv by + `@williamjsdavis `__ in https://github.com/simpeg/simpeg/pull/1665 +- Clean up Numba functions for potential field simulations by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1663 +- Make directives submodules private by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1667 +- Ensure misfit is purely real valued by `@prisae `__ in + https://github.com/simpeg/simpeg/pull/1524 +- Add key navigation to docs by `@prisae `__ in + https://github.com/simpeg/simpeg/pull/1668 +- Add missing map classes to the API reference by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1672 +- Replace sklearn deprecated method for ``validate_data`` function by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1673 +- Remove ``BaseSurvey.counter`` property by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1640 +- Fixes sign error in 1D field calculation. by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1662 +- Allow use of ``J`` as ``LinearOperator`` in mag equivalent layers by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1676 +- Fix beta cooling in ``UpdateIRLS`` directive by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1659 +- Allow use of ``J`` as ``LinearOperator`` in gravity equivalent layers + by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1674 +- Improve admonitions in gravity simulation by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1677 +- Have an option to take a step when the Linesearch breaks by `@lheagy `__ in + https://github.com/simpeg/simpeg/pull/1581 +- Fix bug in phase for recursive 1d NSEM simulation by `@dccowan `__ in + https://github.com/simpeg/simpeg/pull/1679 +- Use conda-forge as only channel in Azure pipelines by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1688 +- Expose solver utility functions in ``simpeg.utils`` by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1678 +- Use logging while setting default solver in PDE simulations by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1670 +- Use ``Impedance`` and ``Tipper`` in examples and tests by `@santisoler `__ + in https://github.com/simpeg/simpeg/pull/1690 +- Fix bug on ``Impedance.eval`` when orientation is “xx” or “yy” by + `@dccowan `__ in https://github.com/simpeg/simpeg/pull/1692 +- Remove deprecated objects missed in v0.24.0 by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1658 +- Update magnetic simulation using differential formulation by + `@johnweis0480 `__ in https://github.com/simpeg/simpeg/pull/1682 +- Standardize output directives and make them more reliable by `@jcapriot `__ + in https://github.com/simpeg/simpeg/pull/1657 +- Make tests error on implicit complex to real by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1696 +- Avoids calculating unused values for boundary conditions on DC 2D + simulations by `@jcapriot `__ in https://github.com/simpeg/simpeg/pull/1698 +- Add How to Guide page on how to choose a solver by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1695 +- Make Logger a bit quieter when running pytest by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1697 +- CG Minimizer Updates by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1656 +- Add top level descriptions to missing to functions by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1702 +- Update meeting times in README.rst by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1700 +- Add ``_faceDiv`` attribute to FDEM H ``Fields`` by `@lheagy `__ in + https://github.com/simpeg/simpeg/pull/1346 +- Improve landing page of docs by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1701 +- Add How to Guide page on moving mesh to survey area by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1699 +- Remove gravity and magnetic tutorials by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1704 +- Minor fixes to docs of ``UpdateSensitivityWeights`` by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1705 +- Update iteration print out by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1626 +- Fix magnetic dipole source for for HJ formulation by `@lheagy `__ in + https://github.com/simpeg/simpeg/pull/1575 +- Drop support for Python 3.10 by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1708 +- Add documentation page for version compatibility by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1707 +- Remove DC resistivity tutorials by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1710 +- Improve dipole source tests by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1711 +- Update deprecated calls in examples, tutorials, and tests to inexact + CG minimizers by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1703 +- Make ``ComplexMap.deriv`` to return a sparse diagonal matrix by + `@lheagy `__ in https://github.com/simpeg/simpeg/pull/1686 +- Standardize signature of mappings’ ``deriv`` method by `@YingHuuu `__ in + https://github.com/simpeg/simpeg/pull/1407 +- Update how mappings are applied in regularizations by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1605 +- Simple fix for pymatsolver 0.4.0 by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1717 +- Fix bug with duplicated current in ``LineCurrent.Mejs`` by `@santisoler `__ + in https://github.com/simpeg/simpeg/pull/1718 +- Minor fixes to LaTeX equations in regularizations by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1720 +- Fix return of ``get_indices_block`` by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1713 +- Remove deprecated bits marked for removal in v0.25.0 by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1719 +- Add shift to discrete topography for NSEM by `@dccowan `__ in + https://github.com/simpeg/simpeg/pull/1683 +- Deprecate unused arguments in ``drape_electrodes_on_topography`` by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1723 +- Fix LaTeX equations in ``Simulation3DDifferential`` by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1726 +- Implement a closed loop as a TDEM source by `@lheagy `__ in + https://github.com/simpeg/simpeg/pull/1651 +- Allow for specifying ramp start and ramp end in ``RampOffWaveform`` by + `@jcapriot `__ in https://github.com/simpeg/simpeg/pull/1714 diff --git a/docs/content/user-guide/getting-started/version-compatibility.rst b/docs/content/user-guide/getting-started/version-compatibility.rst index a4a65b625f..b4d5117c6d 100644 --- a/docs/content/user-guide/getting-started/version-compatibility.rst +++ b/docs/content/user-guide/getting-started/version-compatibility.rst @@ -1,3 +1,5 @@ +.. _version-compatibility: + Version compatibility ===================== diff --git a/docs/content/user-guide/how-to-guide/move-mesh-to-survey.rst b/docs/content/user-guide/how-to-guide/move-mesh-to-survey.rst index 754e209dde..0d0c0a84a3 100644 --- a/docs/content/user-guide/how-to-guide/move-mesh-to-survey.rst +++ b/docs/content/user-guide/how-to-guide/move-mesh-to-survey.rst @@ -1,3 +1,5 @@ +.. _how-to-move-mesh: + ============================ Locating mesh on survey area ============================ From 5f060ec0a808815c97e32dada523d3be5ccba945 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 23 Oct 2025 00:57:30 +0000 Subject: [PATCH 193/194] Add latest release notes to index (#1728) Forgot to add the new release notes to the index, so they can show up in the website. Update the release date. Minor corrections to RST syntax. --- docs/content/release/0.25.0-notes.rst | 16 ++++++++-------- docs/content/release/index.rst | 1 + 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/content/release/0.25.0-notes.rst b/docs/content/release/0.25.0-notes.rst index 970c7fada3..1c2ec6de42 100644 --- a/docs/content/release/0.25.0-notes.rst +++ b/docs/content/release/0.25.0-notes.rst @@ -4,7 +4,7 @@ SimPEG v0.25.0 Release Notes ============================ -October 21st, 2025 +October 22nd, 2025 .. contents:: Highlights :depth: 3 @@ -33,9 +33,9 @@ See https://github.com/simpeg/simpeg/pull/1682 for more details. Add utility function to shift electrodes to discrete topography ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -A new `shift_to_discrete_topography` function shifts locations relative -to discrete surface topography. When performing MT surveys, we measure -electric fields at the Earth's surface. +A new :func:`simpeg.utils.shift_to_discrete_topography` function shifts +locations relative to discrete surface topography. When performing MT surveys, +we measure electric fields at the Earth's surface. Similar to DC/IP, the original measurement locations of the electric fields can end up in air cells when we discretize surface topography. This function allows the user to shift locations relative to discrete topography on Tensor and Tree @@ -158,7 +158,7 @@ In this release we included a few bugfixes: Breaking changes ---------------- -We introduce a few breaking changes in SimPEG v0.25.0. +We introduced a few breaking changes in SimPEG v0.25.0. Dropped support for Python 3.10 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -233,11 +233,11 @@ Removals We also removed several deprecated items marked for removal in previous releases, including: -- The `Data.index_dictionary` property. Use the new ``get_slice`` method of +- The ``Data.index_dictionary`` property. Use the new ``get_slice`` method of ``Survey`` (for example: :meth:`simpeg.potential_fields.gravity.Survey.get_slice`). -- The `gtg_diagonal` property from gravity simulation. -- The `components` property from gravity and magnetic surveys. +- The ``gtg_diagonal`` property from gravity simulation. +- The ``components`` property from gravity and magnetic surveys. Contributors diff --git a/docs/content/release/index.rst b/docs/content/release/index.rst index b99e649f57..dfa6fc1f20 100644 --- a/docs/content/release/index.rst +++ b/docs/content/release/index.rst @@ -7,6 +7,7 @@ Release Notes .. toctree:: :maxdepth: 2 + 0.25.0 <0.25.0-notes> 0.24.0 <0.24.0-notes> 0.23.0 <0.23.0-notes> 0.22.2 <0.22.2-notes> From 9a8c46e8801651711ffed092719f0cb756490489 Mon Sep 17 00:00:00 2001 From: Santiago Soler Date: Thu, 27 Nov 2025 17:48:57 +0000 Subject: [PATCH 194/194] Fix argument name in `UpdateSensitivityWeights` docs (#1737) Fix the name of the `threshold_value` argument in the docstring of the `UpdateSensitivityWeights` directive. --- simpeg/directives/_directives.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simpeg/directives/_directives.py b/simpeg/directives/_directives.py index aec4683746..5cbaa81ffd 100644 --- a/simpeg/directives/_directives.py +++ b/simpeg/directives/_directives.py @@ -2349,7 +2349,7 @@ class UpdateSensitivityWeights(InversionDirective): every_iteration : bool When ``True``, update sensitivity weighting at every model update; non-linear problems. When ``False``, create sensitivity weights for starting model only; linear problems. - threshold : float + threshold_value : float Threshold value for smallest weighting value. threshold_method : {'amplitude', 'global', 'percentile'} Threshold method for how `threshold_value` is applied: