├── docs ├── dipole_equations.pdf ├── dipole_equations.synctex.gz ├── bibdata.bib ├── dipole_equations.bbl ├── dipole_equations.blg ├── dipole_equations.aux ├── dipole_equations.fls ├── dipole_equations.tex ├── dipole_equations.fdb_latexmk └── dipole_equations.log ├── src └── dipole │ ├── __init__.py │ └── dipole.py ├── .gitignore ├── pyproject.toml ├── LICENSE ├── CITATION.cff ├── README.md └── scripts └── base_vector_comparison.py /docs/dipole_equations.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klaundal/dipole/HEAD/docs/dipole_equations.pdf -------------------------------------------------------------------------------- /src/dipole/__init__.py: -------------------------------------------------------------------------------- 1 | from .dipole import Dipole, sph_to_car, car_to_sph, enu_to_ecef, ecef_to_enu, subsol -------------------------------------------------------------------------------- /docs/dipole_equations.synctex.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klaundal/dipole/HEAD/docs/dipole_equations.synctex.gz -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | *.egg-info/ 4 | *.egg 5 | *.py[cod] 6 | __pycache__/ 7 | *.so 8 | *~ 9 | 10 | .DS_Store 11 | .tox 12 | .cache 13 | .coverage 14 | -------------------------------------------------------------------------------- /docs/bibdata.bib: -------------------------------------------------------------------------------- 1 | @Article{Richmond95, 2 | author = {A.~D. Richmond}, 3 | title = {Ionospheric electrodynamics using magnetic apex coordinates}, 4 | year = 1995, 5 | journal = "J. Geomag. Geoelectr.", 6 | volume = 47, 7 | pages = {191-212}, 8 | doi = {10.5636/jgg.47.191} 9 | } 10 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "setuptools_scm[toml]"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "dipole" 7 | dynamic = ["version"] 8 | dependencies = [ 9 | "numpy", 10 | "pandas", 11 | "ppigrf", 12 | ] 13 | 14 | [tool.setuptools.packages.find] 15 | where = ["src"] 16 | 17 | [tool.setuptools_scm] 18 | -------------------------------------------------------------------------------- /docs/dipole_equations.bbl: -------------------------------------------------------------------------------- 1 | \begin{thebibliography}{1} 2 | \providecommand{\natexlab}[1]{#1} 3 | \providecommand{\url}[1]{\texttt{#1}} 4 | \expandafter\ifx\csname urlstyle\endcsname\relax 5 | \providecommand{\doi}[1]{doi: #1}\else 6 | \providecommand{\doi}{doi: \begingroup \urlstyle{rm}\Url}\fi 7 | 8 | \bibitem[Richmond(1995)]{Richmond95} 9 | A.~D. Richmond. 10 | \newblock Ionospheric electrodynamics using magnetic apex coordinates. 11 | \newblock \emph{J. Geomag. Geoelectr.}, 47:\penalty0 191--212, 1995. 12 | \newblock \doi{10.5636/jgg.47.191}. 13 | 14 | \end{thebibliography} 15 | -------------------------------------------------------------------------------- /docs/dipole_equations.blg: -------------------------------------------------------------------------------- 1 | This is BibTeX, Version 0.99d (TeX Live 2017) 2 | Capacity: max_strings=100000, hash_size=100000, hash_prime=85009 3 | The top-level auxiliary file: dipole_equations.aux 4 | The style file: plainnat.bst 5 | Database file #1: bibdata.bib 6 | You've used 1 entry, 7 | 2773 wiz_defined-function locations, 8 | 601 strings with 4996 characters, 9 | and the built_in function-call counts, 423 in all, are: 10 | = -- 38 11 | > -- 10 12 | < -- 1 13 | + -- 4 14 | - -- 3 15 | * -- 29 16 | := -- 71 17 | add.period$ -- 4 18 | call.type$ -- 1 19 | change.case$ -- 4 20 | chr.to.int$ -- 1 21 | cite$ -- 2 22 | duplicate$ -- 20 23 | empty$ -- 40 24 | format.name$ -- 4 25 | if$ -- 85 26 | int.to.chr$ -- 1 27 | int.to.str$ -- 1 28 | missing$ -- 1 29 | newline$ -- 14 30 | num.names$ -- 4 31 | pop$ -- 6 32 | preamble$ -- 1 33 | purify$ -- 3 34 | quote$ -- 0 35 | skip$ -- 14 36 | stack$ -- 0 37 | substring$ -- 23 38 | swap$ -- 1 39 | text.length$ -- 0 40 | text.prefix$ -- 0 41 | top$ -- 0 42 | type$ -- 11 43 | warning$ -- 0 44 | while$ -- 4 45 | width$ -- 0 46 | write$ -- 22 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Karl M. Laundal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | # This CITATION.cff file was generated with cffinit. 2 | # Visit https://bit.ly/cffinit to generate yours today! 3 | 4 | cff-version: 1.2.0 5 | title: dipole 6 | message: >- 7 | If you use this software, please cite it using the 8 | metadata from this file. 9 | type: software 10 | authors: 11 | - orcid: 'https://orcid.org/0000-0001-5028-4943' 12 | given-names: Karl 13 | family-names: Laundal 14 | email: karl.laundal@uib.no 15 | name-particle: Karl 16 | affiliation: 'University of Bergen, Norway' 17 | - given-names: Jone 18 | family-names: Reistad 19 | affiliation: 'University of Bergen, Norway' 20 | orcid: 'https://orcid.org/0000-0003-3509-5479' 21 | - given-names: Ashley 22 | family-names: Smith 23 | affiliation: 'University of Edinburgh, UK' 24 | orcid: 'https://orcid.org/0000-0001-5198-9574' 25 | - given-names: Amalie 26 | family-names: Hovland 27 | affiliation: 'University of Bergen, Norway' 28 | orcid: 'https://orcid.org/0000-0001-5028-4943' 29 | identifiers: 30 | - type: url 31 | value: 'https://github.com/klaundal/dipole' 32 | abstract: >- 33 | Code for working with magnetic dipole model of Earth's 34 | magnetic field, including dipole coordinates and magnetic 35 | apex coordinates and base vectors for a dipole field. 36 | license: MIT 37 | -------------------------------------------------------------------------------- /docs/dipole_equations.aux: -------------------------------------------------------------------------------- 1 | \relax 2 | \@writefile{toc}{\contentsline {section}{\numberline {1}Definitions}{1}} 3 | \@writefile{toc}{\contentsline {section}{\numberline {2}Equation for a dipole}{1}} 4 | \newlabel{eq:dipole}{{1}{1}} 5 | \citation{Richmond95} 6 | \newlabel{eq:dipolestrength}{{2}{2}} 7 | \newlabel{eq:radius}{{4}{2}} 8 | \newlabel{eq:dipole_potential}{{5}{2}} 9 | \@writefile{toc}{\contentsline {section}{\numberline {3}Conversion}{2}} 10 | \@writefile{toc}{\contentsline {subsection}{\numberline {3.1}Modified apex}{2}} 11 | \newlabel{eq:apexlat}{{7}{2}} 12 | \newlabel{eq:apex_lat_to_dipole_lat}{{8}{2}} 13 | \@writefile{toc}{\contentsline {subsection}{\numberline {3.2}Quasi-dipole}{2}} 14 | \citation{Richmond95} 15 | \citation{Richmond95} 16 | \@writefile{toc}{\contentsline {section}{\numberline {4}The base vectors}{3}} 17 | \@writefile{toc}{\contentsline {subsection}{\numberline {4.1}Modified apex base vectors}{3}} 18 | \newlabel{eq:d1}{{10}{3}} 19 | \newlabel{eq:d2}{{11}{3}} 20 | \@writefile{toc}{\contentsline {subsubsection}{\numberline {4.1.1}$\mathbf {d}_1$}{3}} 21 | \@writefile{toc}{\contentsline {subsubsection}{\numberline {4.1.2}$\mathbf {d}_2$}{4}} 22 | \@writefile{toc}{\contentsline {subsection}{\numberline {4.2}$\mathbf {d}_3$}{4}} 23 | \bibstyle{plainnat} 24 | \bibdata{bibdata} 25 | \bibcite{Richmond95}{{1}{1995}{{Richmond}}{{}}} 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dipole - calculations involving dipole model of Earth's magnetic field 2 | Python code to calculate 3 | 4 | - dipole magnetic field 5 | - dipole tilt angle 6 | - centered dipole coordinates and vector components 7 | - magnetic local time 8 | - dipole pole locations 9 | - magnetic flux poleward of some boundary 10 | - apex base vectors for a dipole magnetic field 11 | 12 | The calculations use time-dependendent IGRF coefficients (see https://www.ngdc.noaa.gov/IAGA/vmod/igrf.html for details and https://doi.org/10.1186/s40623-020-01163-9 for even more details) accessed through the ppigrf Python module (https://github.com/klaundal/ppigrf). 13 | 14 | For a definition of centered dipole coordinates, see Section 3.1 in https://doi.org/10.1007/s11214-016-0275-y 15 | 16 | The code is vectorized, so all calculations should be pretty fast. 17 | 18 | ## Examples 19 | 20 | Here is an example calculation of dipole tilt angle. In this example, a Dipole object is initialized for epoch 2022.5, and dipole tilt angle is calculated for two different times 21 | 22 | ```python 23 | import dipole 24 | from datetime import datetime 25 | 26 | dates = [datetime(2022, 7, 13, 10, 0, 0), datetime(2022, 7, 13, 22, 0, 0)] 27 | tilts = dipole.Dipole(2022.5).tilt(dates) 28 | ``` 29 | 30 | Here is an example converting northward-pointing unit vectors from geocentric to centered dipole coordinates. This example also illustrates how numpy broadcasting rules are used for input with different shapes 31 | 32 | ```python 33 | import dipole 34 | import numpy as np 35 | 36 | lat = 70 37 | lon = np.r_[0:360:15] 38 | north = 1 39 | east = 0 40 | 41 | d = dipole.Dipole(2010.) # IGRF epoch 2010. 42 | cdlat, cdlon, cd_east, cd_north = d.geo2mag(lat, lon, Ae = east, An = north) 43 | 44 | ``` 45 | 46 | 47 | 48 | ## Contact 49 | If you find errors, please let me know! 50 | 51 | You don't need permission to copy or use this code. 52 | 53 | karl.laundal at uib.no 54 | -------------------------------------------------------------------------------- /scripts/base_vector_comparison.py: -------------------------------------------------------------------------------- 1 | """ compare base vector components with dipole field and IGRF """ 2 | 3 | import numpy as np 4 | import matplotlib.pyplot as plt 5 | import datetime 6 | import apexpy 7 | import dipole 8 | 9 | def longitude_difference(lon1, lon2): 10 | difference = (lon2 - lon1 + 180) % 360 - 180 11 | return np.where(difference > 180, difference - 360, difference) 12 | 13 | 14 | RE = 6371.2e3 15 | 16 | date = datetime.datetime(2020, 1, 1) 17 | apx = apexpy.Apex(date.year) 18 | dpl = dipole.Dipole(date.year) 19 | 20 | 21 | lat_, lon_ = np.linspace(-89, 89, 50), np.linspace(-180, 180, 100) 22 | lat, lon = np.meshgrid(lat_, lon_) 23 | la, lo = np.ravel(lat), np.ravel(lon) 24 | 25 | # calculate base vectors in apex coordinates: 26 | f1, f2, f3, g1, g2, g3, d1, d2, d3, e1, e2, e3 = apx.basevectors_apex(la, lo, 0) 27 | 28 | # calculate dipole coordinates: 29 | lat_dp, lon_dp = dpl.geo2mag(lat, lon) 30 | 31 | # calculate dipole base vectors in dipole coordinates: 32 | dpl_basevectors = dpl.get_apex_base_vectors(lat_dp, r = RE, R = RE) 33 | 34 | # convert the components to geographic: 35 | dpl_basevectors_geo = [] 36 | for basevector in dpl_basevectors: 37 | lat_, lon_, east, north = dpl.mag2geo(lat_dp.flatten(), lon_dp.flatten(), basevector[0], basevector[1]) 38 | basevector_geo = np.vstack((east, north, basevector[2])) 39 | 40 | assert np.all(np.isclose(lat_ - lat.flatten(), 0)) 41 | assert np.all(np.isclose(longitude_difference(lon_, lon.flatten()), 0)) 42 | 43 | dpl_basevectors_geo.append(basevector_geo) 44 | 45 | 46 | 47 | # plot components vs each other: 48 | fig, axes = plt.subplots(ncols = 3, nrows = 6, figsize = (8, 20)) 49 | axes[0, 0].scatter(dpl_basevectors_geo[0][0], d1[0], label = '$d_{1e}$') 50 | axes[0, 1].scatter(dpl_basevectors_geo[0][1], d1[1], label = '$d_{1n}$') 51 | axes[0, 2].scatter(dpl_basevectors_geo[0][2], d1[2], label = '$d_{1u}$') 52 | 53 | axes[1, 0].scatter(dpl_basevectors_geo[1][0], d2[0], label = '$d_{2e}$') 54 | axes[1, 1].scatter(dpl_basevectors_geo[1][1], d2[1], label = '$d_{2n}$') 55 | axes[1, 2].scatter(dpl_basevectors_geo[1][2], d2[2], label = '$d_{2u}$') 56 | 57 | axes[2, 0].scatter(dpl_basevectors_geo[2][0], d3[0], label = '$d_{3e}$') 58 | axes[2, 1].scatter(dpl_basevectors_geo[2][1], d3[1], label = '$d_{3n}$') 59 | axes[2, 2].scatter(dpl_basevectors_geo[2][2], d3[2], label = '$d_{3u}$') 60 | 61 | axes[3, 0].scatter(dpl_basevectors_geo[0][0], e1[0], label = '$e_{1e}$') 62 | axes[3, 1].scatter(dpl_basevectors_geo[0][1], e1[1], label = '$e_{1n}$') 63 | axes[3, 2].scatter(dpl_basevectors_geo[0][2], e1[2], label = '$e_{1u}$') 64 | 65 | axes[4, 0].scatter(dpl_basevectors_geo[1][0], e2[0], label = '$e_{2e}$') 66 | axes[4, 1].scatter(dpl_basevectors_geo[1][1], e2[1], label = '$e_{2n}$') 67 | axes[4, 2].scatter(dpl_basevectors_geo[1][2], e2[2], label = '$e_{2u}$') 68 | 69 | axes[5, 0].scatter(dpl_basevectors_geo[2][0], e3[0], label = '$e_{3e}$') 70 | axes[5, 1].scatter(dpl_basevectors_geo[2][1], e3[1], label = '$e_{3n}$') 71 | axes[5, 2].scatter(dpl_basevectors_geo[2][2], e3[2], label = '$e_{3u}$') 72 | 73 | axes[0, 0].set_title('east') 74 | axes[0, 1].set_title('north') 75 | axes[0, 2].set_title('up') 76 | 77 | for ax in axes.flatten(): 78 | ax.set_xlabel('dipole') 79 | ax.set_ylabel('IGRF / apex') 80 | ax.legend(frameon = False) 81 | 82 | 83 | 84 | plt.show() -------------------------------------------------------------------------------- /docs/dipole_equations.fls: -------------------------------------------------------------------------------- 1 | PWD /Users/laundal/BCSS-DAG Dropbox/Karl Laundal/git/dipole/docs 2 | INPUT /usr/local/texlive/2017/texmf.cnf 3 | INPUT /usr/local/texlive/2017/texmf-dist/web2c/texmf.cnf 4 | INPUT /usr/local/texlive/2017/texmf-var/web2c/pdftex/pdflatex.fmt 5 | INPUT dipole_equations.tex 6 | OUTPUT dipole_equations.log 7 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/base/article.cls 8 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/base/article.cls 9 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/base/size11.clo 10 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/base/size11.clo 11 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/map/fontname/texfonts.map 12 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmr10.tfm 13 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/amsmath/amsmath.sty 14 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/amsmath/amsmath.sty 15 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/amsmath/amstext.sty 16 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/amsmath/amstext.sty 17 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/amsmath/amsgen.sty 18 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/amsmath/amsgen.sty 19 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/amsmath/amsbsy.sty 20 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/amsmath/amsbsy.sty 21 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/amsmath/amsopn.sty 22 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/amsmath/amsopn.sty 23 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/graphics/graphicx.sty 24 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/graphics/graphicx.sty 25 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/graphics/keyval.sty 26 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/graphics/keyval.sty 27 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/graphics/graphics.sty 28 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/graphics/graphics.sty 29 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/graphics/trig.sty 30 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/graphics/trig.sty 31 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/graphics-cfg/graphics.cfg 32 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/graphics-cfg/graphics.cfg 33 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/graphics-def/pdftex.def 34 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/graphics-def/pdftex.def 35 | INPUT /usr/local/texlive/2017/texmf-dist/tex/generic/oberdiek/infwarerr.sty 36 | INPUT /usr/local/texlive/2017/texmf-dist/tex/generic/oberdiek/infwarerr.sty 37 | INPUT /usr/local/texlive/2017/texmf-dist/tex/generic/oberdiek/ltxcmds.sty 38 | INPUT /usr/local/texlive/2017/texmf-dist/tex/generic/oberdiek/ltxcmds.sty 39 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/lineno/lineno.sty 40 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/lineno/lineno.sty 41 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/natbib/natbib.sty 42 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/natbib/natbib.sty 43 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/appendix/appendix.sty 44 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/appendix/appendix.sty 45 | INPUT dipole_equations.aux 46 | INPUT dipole_equations.aux 47 | OUTPUT dipole_equations.aux 48 | INPUT /usr/local/texlive/2017/texmf-dist/tex/context/base/mkii/supp-pdf.mkii 49 | INPUT /usr/local/texlive/2017/texmf-dist/tex/context/base/mkii/supp-pdf.mkii 50 | INPUT /usr/local/texlive/2017/texmf-dist/tex/generic/oberdiek/pdftexcmds.sty 51 | INPUT /usr/local/texlive/2017/texmf-dist/tex/generic/oberdiek/pdftexcmds.sty 52 | INPUT /usr/local/texlive/2017/texmf-dist/tex/generic/oberdiek/ifluatex.sty 53 | INPUT /usr/local/texlive/2017/texmf-dist/tex/generic/oberdiek/ifluatex.sty 54 | INPUT /usr/local/texlive/2017/texmf-dist/tex/generic/oberdiek/ifpdf.sty 55 | INPUT /usr/local/texlive/2017/texmf-dist/tex/generic/oberdiek/ifpdf.sty 56 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/oberdiek/epstopdf-base.sty 57 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/oberdiek/epstopdf-base.sty 58 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/oberdiek/grfext.sty 59 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/oberdiek/grfext.sty 60 | INPUT /usr/local/texlive/2017/texmf-dist/tex/generic/oberdiek/kvdefinekeys.sty 61 | INPUT /usr/local/texlive/2017/texmf-dist/tex/generic/oberdiek/kvdefinekeys.sty 62 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/oberdiek/kvoptions.sty 63 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/oberdiek/kvoptions.sty 64 | INPUT /usr/local/texlive/2017/texmf-dist/tex/generic/oberdiek/kvsetkeys.sty 65 | INPUT /usr/local/texlive/2017/texmf-dist/tex/generic/oberdiek/kvsetkeys.sty 66 | INPUT /usr/local/texlive/2017/texmf-dist/tex/generic/oberdiek/etexcmds.sty 67 | INPUT /usr/local/texlive/2017/texmf-dist/tex/generic/oberdiek/etexcmds.sty 68 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/latexconfig/epstopdf-sys.cfg 69 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/latexconfig/epstopdf-sys.cfg 70 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmr17.tfm 71 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmr12.tfm 72 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmr8.tfm 73 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmr6.tfm 74 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmmi12.tfm 75 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmmi8.tfm 76 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmmi6.tfm 77 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmsy10.tfm 78 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmsy8.tfm 79 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmsy6.tfm 80 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmex10.tfm 81 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/tfm/public/amsfonts/cmextra/cmex8.tfm 82 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/tfm/public/amsfonts/cmextra/cmex7.tfm 83 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmbx10.tfm 84 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmr12.tfm 85 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmbx12.tfm 86 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/base/omscmr.fd 87 | INPUT /usr/local/texlive/2017/texmf-dist/tex/latex/base/omscmr.fd 88 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmsy10.tfm 89 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmmi10.tfm 90 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmex10.tfm 91 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmbx10.tfm 92 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmbx8.tfm 93 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmbx6.tfm 94 | OUTPUT dipole_equations.pdf 95 | INPUT /usr/local/texlive/2017/texmf-var/fonts/map/pdftex/updmap/pdftex.map 96 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmbx12.tfm 97 | INPUT dipole_equations.bbl 98 | INPUT dipole_equations.bbl 99 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmti10.tfm 100 | INPUT dipole_equations.aux 101 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/type1/public/amsfonts/cm/cmbx10.pfb 102 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/type1/public/amsfonts/cm/cmbx12.pfb 103 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/type1/public/amsfonts/cm/cmex10.pfb 104 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/type1/public/amsfonts/cmextra/cmex8.pfb 105 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi10.pfb 106 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi6.pfb 107 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi8.pfb 108 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/type1/public/amsfonts/cm/cmr10.pfb 109 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/type1/public/amsfonts/cm/cmr12.pfb 110 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/type1/public/amsfonts/cm/cmr17.pfb 111 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/type1/public/amsfonts/cm/cmr6.pfb 112 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/type1/public/amsfonts/cm/cmr8.pfb 113 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy10.pfb 114 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy8.pfb 115 | INPUT /usr/local/texlive/2017/texmf-dist/fonts/type1/public/amsfonts/cm/cmti10.pfb 116 | -------------------------------------------------------------------------------- /docs/dipole_equations.tex: -------------------------------------------------------------------------------- 1 | \documentclass[11pt]{article} 2 | 3 | \usepackage{amsmath} 4 | \usepackage{graphicx} 5 | \title{Apex coordinates in dipole magnetic field} 6 | \author{Karl Laundal, karl.laundal@uib.no} 7 | 8 | \RequirePackage{lineno} 9 | \usepackage{natbib} 10 | \usepackage[toc,page]{appendix} 11 | 12 | \begin{document} 13 | \maketitle 14 | \modulolinenumbers[2] 15 | 16 | \begin{abstract} 17 | This document presents some fundamental equations for a magnetic dipole model, and the equations for apex base vectors in a dipole field for a spherical Earth. The purpose of this is to be able to easily use the equations developed for apex coordinates with a dipole magnetic field. 18 | \end{abstract} 19 | 20 | \section{Definitions} 21 | 22 | \begin{itemize} 23 | \item $\lambda$ is the dipole latitude. 24 | \item $\phi$ is the dipole longitude 25 | \item Subscript $ma$ is used to denote Modified Apex (MA) coordinates 26 | \item Subscript $qd$ is used to dentoe Quasi-Dipole (QD) coordinates 27 | \item $r$ is the radius (same for dipole, MA, and QD coordinates) 28 | \item $B_0$ is the reference magnetic field, equal to the norm of a vector formed by the dipole Gauss coefficients: $B_0 = \sqrt{(g_0^1)^2 + (g_1^1)^2 + (h_1^1)^2}$ 29 | \item $R$ is the modified apex reference radius 30 | \item $R_E=6371.2$~km is the Earth radius 31 | \end{itemize} 32 | 33 | \section{Equation for a dipole} 34 | The equation for a dipole magnetic field in dipole coordinates is 35 | \begin{equation} 36 | \mathbf B (r, \lambda) = B_0\left(\frac{R_E}{r}\right)^3(-2\sin \lambda \hat{\mathbf e}_r + \cos \lambda \hat{ \mathbf e}_\lambda). \label{eq:dipole} 37 | \end{equation} 38 | The field magnitude is 39 | \begin{equation} 40 | B (r, \lambda) = B_0\left(\frac{R_E}{r}\right)^3\sqrt{4 - 3\cos^2\lambda} \label{eq:dipolestrength} 41 | \end{equation} 42 | The equation for a dipole field line is: 43 | \begin{equation} 44 | r(\lambda) = r_{eq}\cos^2\lambda 45 | \end{equation} 46 | which implies that the apex radius of the field line at $(r, \lambda)$ is 47 | \begin{equation} 48 | r_{eq}(r, \lambda) = r/\cos^2\lambda \label{eq:radius} 49 | \end{equation} 50 | Equation \ref{eq:dipole} can be written as $\mathbf{B} = -\nabla V$ where the magnetic potential $V$ is: 51 | \begin{equation} 52 | V = -B_0 \frac{R_E^3}{r^2}\sin\lambda \label{eq:dipole_potential} 53 | \end{equation} 54 | 55 | 56 | \section{Conversion} 57 | Below are equations for converting between dipole, modified apex, and quasi-dipole coordinates for a dipole field. The longitudes are all equal: 58 | 59 | \begin{equation} 60 | \boxed{\phi = \phi_{qd} = \phi_{ma}} 61 | \end{equation} 62 | 63 | \subsection{Modified apex} 64 | Equation \ref{eq:radius} can be used directly in the equation for modified apex latitude: 65 | \begin{equation} 66 | \boxed{\lambda_{ma}(r, \lambda) = \pm \cos^{-1}\sqrt{\left(\frac{R}{r}\right)\cos^2\lambda}.}\label{eq:apexlat} 67 | \end{equation} 68 | where $\pm$ refers to the Northern ($+$) and Southern ($-$) hemisphere. The opposite conversion, from $\lambda_{ma}$ to $\lambda$ is 69 | \begin{equation} 70 | \boxed{\lambda(r, \lambda_{ma}) = \pm\cos^{-1}\sqrt{\left(\frac{r}{R}\right)\cos^2\lambda_{ma}}.} \label{eq:apex_lat_to_dipole_lat} 71 | \end{equation} 72 | 73 | \subsection{Quasi-dipole} 74 | For quasi-dipole coordinates, the above equations are the same except that instad of $R$ we trace back to $r$. All the ratios in the parentheses above are 1, so that 75 | \begin{equation} 76 | \boxed{\lambda_{qd} = \lambda \hspace{2cm} \forall\hspace{1mm} r} 77 | \end{equation} 78 | 79 | 80 | 81 | 82 | \section{The base vectors} 83 | From the equations above, we can define base vectors similar to those in \citet{Richmond95}, only that in this case they hold for a dipole magnetic field, and thus they can be found analytically. I will express the vectors in ($E, N, U$)-directions, which here refer to dipole coordinates. All equations below are derived for $\lambda \in [0^\circ, 90^\circ]$, so the sign of $\lambda$ should be changed for points in the Southern hemisphere. $\pm$ and $\mp$ are used to keep track of the sign of each hemisphere (North on top). 84 | 85 | \subsection{Modified apex base vectors} 86 | In a dipole field, the modified apex base vectors are everywhere perpendicular, but they are unit length only at $r=R$. Only at $r=R$ is $\mathbf{d}_i = \mathbf{e}_i$, although they are parallel everywhere, and $\mathbf{d}_i\cdot\mathbf{e}_j = \delta_{ij}$ holds. This is because they are defined to scale differently with the magnetic field. 87 | 88 | The modified apex base vectors are defined as (Eqs. 3.8--3.9 in \citet{Richmond95}) 89 | \begin{align} 90 | \mathbf{d}_1 &= R\cos \lambda_{ma}\nabla\phi_{ma} \label{eq:d1}\\ 91 | \mathbf{d}_2 &= -R\sin I_{ma}\nabla\lambda_{ma} \label{eq:d2}\\ 92 | \mathbf{d}_3 &= \frac{-\nabla V}{BD} = \frac{\mathbf{d}_1\times\mathbf{d}_2}{\|\mathbf{d}_1\times\mathbf{d}_2\|^2} 93 | \end{align} 94 | where the last expression for $\mathbf{d}_3$ can be found using (3.13) and (3.15) in \citet{Richmond95}. 95 | 96 | 97 | 98 | \subsubsection{$\mathbf{d}_1$} 99 | The $\mathbf{d}_1$ base vector (Equation \ref{eq:d1}) depends on the gradient of $\phi_{ma} = \phi$. Using (\ref{eq:apexlat}) to replace $\cos\lambda_{ma}$, we get: 100 | \begin{eqnarray} 101 | \mathbf{d}_1(r, \lambda) = R\left(\frac{R}{r}\right)^{3/2}\cos\lambda \frac{1}{r\cos\lambda}\frac{\partial \phi}{\partial \phi} \begin{pmatrix} 1\\ 0\\ 0 \end{pmatrix} \\ 102 | \boxed{\mathbf{d}_1(r, \lambda) = \left(\frac{R}{r}\right)^{3/2}\begin{pmatrix}1\\ 0\\ 0\end{pmatrix} } 103 | \end{eqnarray} 104 | 105 | \subsubsection{$\mathbf{d}_2$} 106 | The $\mathbf{d}_2$ base vector can be found by calculating the gradient of Eq. \ref{eq:apexlat} and inserting the result into the definition (\ref{eq:d2}). We also need to express $\sin I_{ma}$ in terms of $(r, \lambda)$. I start by the latter: 107 | \begin{align} 108 | \sin I_{ma} &= 2\sin \lambda_{ma} (4 - 3\cos^2\lambda_{ma})^{-1/2} \nonumber\\ 109 | &= \pm2\sqrt{\frac{1 - \frac{R}{r}\cos^2 \lambda}{4 - 3\frac{R}{r}\cos^2\lambda}} 110 | \end{align} 111 | where Equation \ref{eq:apexlat} was used. 112 | 113 | The gradient of $\lambda_{ma}$ can be written 114 | \begin{align} 115 | \nabla\lambda_{ma} =& \pm\begin{pmatrix} 0\\ 116 | \frac{1}{r}\frac{\partial}{\partial \lambda}\\ 117 | \frac{\partial}{\partial r} \end{pmatrix}\left[\cos^{-1}\sqrt{\left(\frac{R}{r}\right)\cos^2\lambda}\right]\nonumber\\ 118 | =& \pm\begin{pmatrix} 0\\ 119 | \sqrt{\frac{R}{r}}\frac{\sin\lambda}{r\sqrt{1-\frac{R}{r}\cos^2\lambda}}\\ 120 | \sqrt{\frac{R}{r}}\frac{\cos\lambda}{2r\sqrt{1 - \frac{R}{r}\cos^2\lambda}} 121 | \end{pmatrix} = \pm\frac{\sqrt{R/r}}{r\sqrt{1 - \frac{R}{r}\cos^2\lambda}}\begin{pmatrix} 0\\ 122 | \sin\lambda\\ 123 | \cos\lambda/2 124 | \end{pmatrix} 125 | \end{align} 126 | Inserting this expression, and the expression for $\sin I_{ma}$ into the definition of $\mathbf{d}_2$ (\ref{eq:d2}) we get 127 | \begin{equation} 128 | \boxed{\mathbf{d}_2(r, \lambda) = \mp\left(\frac{R}{r}\right)^{3/2}\left(4 - 3\frac{R}{r}\cos^2\lambda\right)^{-1/2}\left(\begin{array}{c} 129 | 0\\ 130 | 2\sin \lambda\\ 131 | \cos \lambda\\ 132 | \end{array}\right)} 133 | \end{equation} 134 | This expression can be used to confirm that $\|\mathbf{d}_2(r = R, \lambda)\| = 1$, as it should be for a dipole field and spherical Earth. 135 | 136 | \subsection{$\mathbf{d}_3$} 137 | The last base vector can be found by crossing $\mathbf{d}_1$ and $\mathbf{d}_2$. Multiplication of the scalar coefficients give: 138 | \begin{equation} 139 | \mp\left(\frac{R}{r}\right)^{3}\left(4 - 3\frac{R}{r}\cos^2\lambda\right)^{-1/2}, \nonumber 140 | \end{equation} 141 | and crossing the vector parts give 142 | \begin{align} 143 | \left( \begin{array}{c} 144 | 1\\ 145 | 0\\ 146 | 0\end{array} \right) \times \left(\begin{array}{c} 147 | 0\\ 148 | 2\sin \lambda\\ 149 | \cos \lambda\\ 150 | \end{array}\right) = \left(\begin{array}{c} 151 | 0\\ 152 | -\cos \lambda\\ 153 | 2\sin \lambda\\ 154 | \end{array}\right) 155 | \end{align} 156 | We calculate the quantity $D = \|\mathbf{d}_1\times\mathbf{d}_2\|$: 157 | \begin{equation} 158 | D =\left(\frac{R}{r}\right)^{3}\sqrt{\frac{4 - 3\cos^2\lambda}{4 - 3\frac{R}{r}\cos^2\lambda}} 159 | \end{equation} 160 | The $\mathbf{d}_3$ base vector is 161 | \begin{equation} 162 | \boxed{\mathbf{d}_3(r, \lambda) = \left(\frac{r}{R}\right)^3\frac{\sqrt{4 - 3\frac{R}{r}\cos^2\lambda}}{4 - 3\cos^2\lambda}\left(\begin{array}{c} 163 | 0\\ 164 | \cos \lambda\\ 165 | -2\sin \lambda\\ 166 | \end{array}\right)} 167 | \end{equation} 168 | which is parallel to the dipole field. $B_{e_3}$ is supposed to be constant along a field line, so we can now check that this is true, by calculating 169 | \begin{equation} 170 | B_{e_3} = \mathbf{B}\cdot\mathbf{d}_3 = B_0\left(\frac{R_E}{R}\right)^3\sqrt{4-3R/r_{eq}} 171 | \end{equation} 172 | which is constant. $B_{e_3}$ should be equal to $B/D$. Let's check that also: 173 | \begin{equation} 174 | B/D = \frac{B_0\left(\frac{R_E}{r}\right)^3\sqrt{4 - 3\cos^2\lambda}} {\left(\frac{R}{r}\right)^3\sqrt{\frac{4 - 3\cos^2\lambda}{4 - 3\frac{R}{r}\cos^2\lambda}}} = B_0\left(\frac{R_E}{R}\right)^3\sqrt{4-3R/r_{eq}} 175 | \end{equation} 176 | as expected. Also, at $r = R$, $B_{e_3}$ is the magnetic field strength. This can be seen by replacing $r_{eq}$ with $r/\cos^2\lambda$, and $r$ by $R$. 177 | 178 | 179 | %\begin{thebibliography}{} 180 | \bibliographystyle{plainnat} 181 | \bibliography{bibdata} 182 | %\end{thebibliography} 183 | 184 | 185 | 186 | 187 | \end{document} 188 | -------------------------------------------------------------------------------- /docs/dipole_equations.fdb_latexmk: -------------------------------------------------------------------------------- 1 | # Fdb version 3 2 | ["bibtex dipole_equations"] 1680276629 "dipole_equations.aux" "dipole_equations.bbl" "dipole_equations" 1680287161 3 | "/usr/local/texlive/2017/texmf-dist/bibtex/bst/natbib/plainnat.bst" 1480098435 26816 ffe5423d2019860bad9f54bc402fac15 "" 4 | "bibdata.bib" 1680276464 254 fcbaf65b9e32d223d88b56e82ccfe269 "" 5 | "dipole_equations.aux" 1680287161 1238 4a23e08df0e14dab93f1f25fcc131c98 "" 6 | (generated) 7 | "dipole_equations.bbl" 8 | "dipole_equations.blg" 9 | ["pdflatex"] 1680287160 "dipole_equations.tex" "dipole_equations.pdf" "dipole_equations" 1680287161 10 | "/usr/local/texlive/2017/texmf-dist/fonts/map/fontname/texfonts.map" 1480098670 3287 e6b82fe08f5336d4d5ebc73fb1152e87 "" 11 | "/usr/local/texlive/2017/texmf-dist/fonts/tfm/public/amsfonts/cmextra/cmex7.tfm" 1480098698 1004 54797486969f23fa377b128694d548df "" 12 | "/usr/local/texlive/2017/texmf-dist/fonts/tfm/public/amsfonts/cmextra/cmex8.tfm" 1480098698 988 bdf658c3bfc2d96d3c8b02cfc1c94c20 "" 13 | "/usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmbx10.tfm" 1480098701 1328 c834bbb027764024c09d3d2bf908b5f0 "" 14 | "/usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmbx12.tfm" 1480098701 1324 c910af8c371558dc20f2d7822f66fe64 "" 15 | "/usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmbx6.tfm" 1480098701 1344 8a0be4fe4d376203000810ad4dc81558 "" 16 | "/usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmbx8.tfm" 1480098701 1332 1fde11373e221473104d6cc5993f046e "" 17 | "/usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmex10.tfm" 1480098701 992 662f679a0b3d2d53c1b94050fdaa3f50 "" 18 | "/usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmmi10.tfm" 1480098701 1528 abec98dbc43e172678c11b3b9031252a "" 19 | "/usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmmi12.tfm" 1480098701 1524 4414a8315f39513458b80dfc63bff03a "" 20 | "/usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmmi6.tfm" 1480098701 1512 f21f83efb36853c0b70002322c1ab3ad "" 21 | "/usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmmi8.tfm" 1480098701 1520 eccf95517727cb11801f4f1aee3a21b4 "" 22 | "/usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmr10.tfm" 1480098701 1296 45809c5a464d5f32c8f98ba97c1bb47f "" 23 | "/usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmr12.tfm" 1480098701 1288 655e228510b4c2a1abe905c368440826 "" 24 | "/usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmr17.tfm" 1480098701 1292 296a67155bdbfc32aa9c636f21e91433 "" 25 | "/usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmr6.tfm" 1480098701 1300 b62933e007d01cfd073f79b963c01526 "" 26 | "/usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmr8.tfm" 1480098701 1292 21c1c5bfeaebccffdb478fd231a0997d "" 27 | "/usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmsy10.tfm" 1480098701 1124 6c73e740cf17375f03eec0ee63599741 "" 28 | "/usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmsy6.tfm" 1480098701 1116 933a60c408fc0a863a92debe84b2d294 "" 29 | "/usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmsy8.tfm" 1480098701 1120 8b7d695260f3cff42e636090a8002094 "" 30 | "/usr/local/texlive/2017/texmf-dist/fonts/tfm/public/cm/cmti10.tfm" 1480098701 1480 aa8e34af0eb6a2941b776984cf1dfdc4 "" 31 | "/usr/local/texlive/2017/texmf-dist/fonts/type1/public/amsfonts/cm/cmbx10.pfb" 1480098733 34811 78b52f49e893bcba91bd7581cdc144c0 "" 32 | "/usr/local/texlive/2017/texmf-dist/fonts/type1/public/amsfonts/cm/cmbx12.pfb" 1480098733 32080 340ef9bf63678554ee606688e7b5339d "" 33 | "/usr/local/texlive/2017/texmf-dist/fonts/type1/public/amsfonts/cm/cmex10.pfb" 1480098733 30251 6afa5cb1d0204815a708a080681d4674 "" 34 | "/usr/local/texlive/2017/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi10.pfb" 1480098733 36299 5f9df58c2139e7edcf37c8fca4bd384d "" 35 | "/usr/local/texlive/2017/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi6.pfb" 1480098733 37166 8ab3487cbe3ab49ebce74c29ea2418db "" 36 | "/usr/local/texlive/2017/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi8.pfb" 1480098733 35469 70d41d2b9ea31d5d813066df7c99281c "" 37 | "/usr/local/texlive/2017/texmf-dist/fonts/type1/public/amsfonts/cm/cmr10.pfb" 1480098733 35752 024fb6c41858982481f6968b5fc26508 "" 38 | "/usr/local/texlive/2017/texmf-dist/fonts/type1/public/amsfonts/cm/cmr12.pfb" 1480098733 32722 d7379af29a190c3f453aba36302ff5a9 "" 39 | "/usr/local/texlive/2017/texmf-dist/fonts/type1/public/amsfonts/cm/cmr17.pfb" 1480098733 32362 179c33bbf43f19adbb3825bb4e36e57a "" 40 | "/usr/local/texlive/2017/texmf-dist/fonts/type1/public/amsfonts/cm/cmr6.pfb" 1480098733 32734 69e00a6b65cedb993666e42eedb3d48f "" 41 | "/usr/local/texlive/2017/texmf-dist/fonts/type1/public/amsfonts/cm/cmr8.pfb" 1480098733 32726 0a1aea6fcd6468ee2cf64d891f5c43c8 "" 42 | "/usr/local/texlive/2017/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy10.pfb" 1480098733 32569 5e5ddc8df908dea60932f3c484a54c0d "" 43 | "/usr/local/texlive/2017/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy8.pfb" 1480098733 32626 4f5c1b83753b1dd3a97d1b399a005b4b "" 44 | "/usr/local/texlive/2017/texmf-dist/fonts/type1/public/amsfonts/cm/cmti10.pfb" 1480098733 37944 359e864bd06cde3b1cf57bb20757fb06 "" 45 | "/usr/local/texlive/2017/texmf-dist/fonts/type1/public/amsfonts/cmextra/cmex8.pfb" 1480098733 30273 87a352d78b6810ae5cfdc68d2fb827b2 "" 46 | "/usr/local/texlive/2017/texmf-dist/tex/context/base/mkii/supp-pdf.mkii" 1480098806 71627 94eb9990bed73c364d7f53f960cc8c5b "" 47 | "/usr/local/texlive/2017/texmf-dist/tex/generic/oberdiek/etexcmds.sty" 1480098815 7612 729a8cc22a1ee0029997c7f74717ae05 "" 48 | "/usr/local/texlive/2017/texmf-dist/tex/generic/oberdiek/ifluatex.sty" 1480098815 7324 2310d1247db0114eb4726807c8837a0e "" 49 | "/usr/local/texlive/2017/texmf-dist/tex/generic/oberdiek/ifpdf.sty" 1490564930 1251 d170e11a3246c3392bc7f59595af42cb "" 50 | "/usr/local/texlive/2017/texmf-dist/tex/generic/oberdiek/infwarerr.sty" 1480098815 8253 473e0e41f9adadb1977e8631b8f72ea6 "" 51 | "/usr/local/texlive/2017/texmf-dist/tex/generic/oberdiek/kvdefinekeys.sty" 1480098815 5152 b67a3a964ad9851e095110c854a1d461 "" 52 | "/usr/local/texlive/2017/texmf-dist/tex/generic/oberdiek/kvsetkeys.sty" 1480098815 14040 ac8866aac45982ac84021584b0abb252 "" 53 | "/usr/local/texlive/2017/texmf-dist/tex/generic/oberdiek/ltxcmds.sty" 1480098815 18425 5b3c0c59d76fac78978b5558e83c1f36 "" 54 | "/usr/local/texlive/2017/texmf-dist/tex/generic/oberdiek/pdftexcmds.sty" 1490564930 20151 72b3c7cacb61f7dd527505c39a23f7c1 "" 55 | "/usr/local/texlive/2017/texmf-dist/tex/latex/amsmath/amsbsy.sty" 1480098820 2210 5c54ab129b848a5071554186d0168766 "" 56 | "/usr/local/texlive/2017/texmf-dist/tex/latex/amsmath/amsgen.sty" 1480098820 4160 c115536cf8d4ff25aa8c1c9bc4ecb79a "" 57 | "/usr/local/texlive/2017/texmf-dist/tex/latex/amsmath/amsmath.sty" 1485129665 84329 81aa65c5042562f79cb421feff9b8bdc "" 58 | "/usr/local/texlive/2017/texmf-dist/tex/latex/amsmath/amsopn.sty" 1480098820 4115 318a66090112f3aa3f415aeb6fe8540f "" 59 | "/usr/local/texlive/2017/texmf-dist/tex/latex/amsmath/amstext.sty" 1480098820 2431 fe3078ec12fc30287f568596f8e0b948 "" 60 | "/usr/local/texlive/2017/texmf-dist/tex/latex/appendix/appendix.sty" 1480098820 8526 d0d9b5e2dd0c996c69c3bd05eb25b943 "" 61 | "/usr/local/texlive/2017/texmf-dist/tex/latex/base/article.cls" 1480098821 19821 310da678527a7dfe2a02c88af38079b7 "" 62 | "/usr/local/texlive/2017/texmf-dist/tex/latex/base/omscmr.fd" 1480098821 2256 80ce1168fb4ce6a85583a9cf8972c013 "" 63 | "/usr/local/texlive/2017/texmf-dist/tex/latex/base/size11.clo" 1480098821 8308 f91506bb4df6d8527331f7bbeaf0e63c "" 64 | "/usr/local/texlive/2017/texmf-dist/tex/latex/graphics-cfg/graphics.cfg" 1480098830 1224 978390e9c2234eab29404bc21b268d1e "" 65 | "/usr/local/texlive/2017/texmf-dist/tex/latex/graphics-def/pdftex.def" 1485129666 58250 3792a9d2d1d664ee8c742498e295b051 "" 66 | "/usr/local/texlive/2017/texmf-dist/tex/latex/graphics/graphics.sty" 1492297155 14603 b288c52bd5d46d593af31dbc7e548236 "" 67 | "/usr/local/texlive/2017/texmf-dist/tex/latex/graphics/graphicx.sty" 1480098830 8125 557ab9f1bfa80d369fb45a914aa8a3b4 "" 68 | "/usr/local/texlive/2017/texmf-dist/tex/latex/graphics/keyval.sty" 1480098830 2594 d18d5e19aa8239cf867fa670c556d2e9 "" 69 | "/usr/local/texlive/2017/texmf-dist/tex/latex/graphics/trig.sty" 1480098830 3980 0a268fbfda01e381fa95821ab13b6aee "" 70 | "/usr/local/texlive/2017/texmf-dist/tex/latex/latexconfig/epstopdf-sys.cfg" 1480098833 678 4792914a8f45be57bb98413425e4c7af "" 71 | "/usr/local/texlive/2017/texmf-dist/tex/latex/lineno/lineno.sty" 1480098833 151738 8cd767481920f0eb785302dacfc87057 "" 72 | "/usr/local/texlive/2017/texmf-dist/tex/latex/natbib/natbib.sty" 1480098835 45456 1c8843383c0bd05870c45fa0ebea6cc2 "" 73 | "/usr/local/texlive/2017/texmf-dist/tex/latex/oberdiek/epstopdf-base.sty" 1480098836 12095 5337833c991d80788a43d3ce26bd1c46 "" 74 | "/usr/local/texlive/2017/texmf-dist/tex/latex/oberdiek/grfext.sty" 1480098836 7075 2fe3d848bba95f139de11ded085e74aa "" 75 | "/usr/local/texlive/2017/texmf-dist/tex/latex/oberdiek/kvoptions.sty" 1480098836 22417 1d9df1eb66848aa31b18a593099cf45c "" 76 | "/usr/local/texlive/2017/texmf-dist/web2c/texmf.cnf" 1494087824 32646 eadc4ca26cdbe7105ac7c593aa8c4f72 "" 77 | "/usr/local/texlive/2017/texmf-var/fonts/map/pdftex/updmap/pdftex.map" 1495593472 2350277 a699055bee05bf8a40b0504752487295 "" 78 | "/usr/local/texlive/2017/texmf-var/web2c/pdftex/pdflatex.fmt" 1508246722 4129593 7e5aa63d9c99e444fefaa118fa9f9aa4 "" 79 | "/usr/local/texlive/2017/texmf.cnf" 1495593465 577 2b71d4d888f9e5560b2e99985915a9fa "" 80 | "dipole_equations.aux" 1680287161 1238 4a23e08df0e14dab93f1f25fcc131c98 "" 81 | "dipole_equations.bbl" 1680276629 505 e842868ce8f59fa49104b5f9e5d5e04d "bibtex dipole_equations" 82 | "dipole_equations.tex" 1680287158 8598 d2dc758c68b20e3346af8cad2f792aeb "" 83 | (generated) 84 | "dipole_equations.log" 85 | "dipole_equations.pdf" 86 | "dipole_equations.aux" 87 | -------------------------------------------------------------------------------- /docs/dipole_equations.log: -------------------------------------------------------------------------------- 1 | This is pdfTeX, Version 3.14159265-2.6-1.40.18 (TeX Live 2017) (preloaded format=pdflatex 2017.10.17) 31 MAR 2023 20:26 2 | entering extended mode 3 | restricted \write18 enabled. 4 | %&-line parsing enabled. 5 | **dipole_equations.tex 6 | (./dipole_equations.tex 7 | LaTeX2e <2017-04-15> 8 | Babel <3.10> and hyphenation patterns for 84 language(s) loaded. 9 | (/usr/local/texlive/2017/texmf-dist/tex/latex/base/article.cls 10 | Document Class: article 2014/09/29 v1.4h Standard LaTeX document class 11 | (/usr/local/texlive/2017/texmf-dist/tex/latex/base/size11.clo 12 | File: size11.clo 2014/09/29 v1.4h Standard LaTeX file (size option) 13 | ) 14 | \c@part=\count79 15 | \c@section=\count80 16 | \c@subsection=\count81 17 | \c@subsubsection=\count82 18 | \c@paragraph=\count83 19 | \c@subparagraph=\count84 20 | \c@figure=\count85 21 | \c@table=\count86 22 | \abovecaptionskip=\skip41 23 | \belowcaptionskip=\skip42 24 | \bibindent=\dimen102 25 | ) 26 | (/usr/local/texlive/2017/texmf-dist/tex/latex/amsmath/amsmath.sty 27 | Package: amsmath 2016/11/05 v2.16a AMS math features 28 | \@mathmargin=\skip43 29 | 30 | For additional information on amsmath, use the `?' option. 31 | (/usr/local/texlive/2017/texmf-dist/tex/latex/amsmath/amstext.sty 32 | Package: amstext 2000/06/29 v2.01 AMS text 33 | 34 | (/usr/local/texlive/2017/texmf-dist/tex/latex/amsmath/amsgen.sty 35 | File: amsgen.sty 1999/11/30 v2.0 generic functions 36 | \@emptytoks=\toks14 37 | \ex@=\dimen103 38 | )) 39 | (/usr/local/texlive/2017/texmf-dist/tex/latex/amsmath/amsbsy.sty 40 | Package: amsbsy 1999/11/29 v1.2d Bold Symbols 41 | \pmbraise@=\dimen104 42 | ) 43 | (/usr/local/texlive/2017/texmf-dist/tex/latex/amsmath/amsopn.sty 44 | Package: amsopn 2016/03/08 v2.02 operator names 45 | ) 46 | \inf@bad=\count87 47 | LaTeX Info: Redefining \frac on input line 213. 48 | \uproot@=\count88 49 | \leftroot@=\count89 50 | LaTeX Info: Redefining \overline on input line 375. 51 | \classnum@=\count90 52 | \DOTSCASE@=\count91 53 | LaTeX Info: Redefining \ldots on input line 472. 54 | LaTeX Info: Redefining \dots on input line 475. 55 | LaTeX Info: Redefining \cdots on input line 596. 56 | \Mathstrutbox@=\box26 57 | \strutbox@=\box27 58 | \big@size=\dimen105 59 | LaTeX Font Info: Redeclaring font encoding OML on input line 712. 60 | LaTeX Font Info: Redeclaring font encoding OMS on input line 713. 61 | \macc@depth=\count92 62 | \c@MaxMatrixCols=\count93 63 | \dotsspace@=\muskip10 64 | \c@parentequation=\count94 65 | \dspbrk@lvl=\count95 66 | \tag@help=\toks15 67 | \row@=\count96 68 | \column@=\count97 69 | \maxfields@=\count98 70 | \andhelp@=\toks16 71 | \eqnshift@=\dimen106 72 | \alignsep@=\dimen107 73 | \tagshift@=\dimen108 74 | \tagwidth@=\dimen109 75 | \totwidth@=\dimen110 76 | \lineht@=\dimen111 77 | \@envbody=\toks17 78 | \multlinegap=\skip44 79 | \multlinetaggap=\skip45 80 | \mathdisplay@stack=\toks18 81 | LaTeX Info: Redefining \[ on input line 2817. 82 | LaTeX Info: Redefining \] on input line 2818. 83 | ) 84 | (/usr/local/texlive/2017/texmf-dist/tex/latex/graphics/graphicx.sty 85 | Package: graphicx 2014/10/28 v1.0g Enhanced LaTeX Graphics (DPC,SPQR) 86 | 87 | (/usr/local/texlive/2017/texmf-dist/tex/latex/graphics/keyval.sty 88 | Package: keyval 2014/10/28 v1.15 key=value parser (DPC) 89 | \KV@toks@=\toks19 90 | ) 91 | (/usr/local/texlive/2017/texmf-dist/tex/latex/graphics/graphics.sty 92 | Package: graphics 2017/04/14 v1.1b Standard LaTeX Graphics (DPC,SPQR) 93 | 94 | (/usr/local/texlive/2017/texmf-dist/tex/latex/graphics/trig.sty 95 | Package: trig 2016/01/03 v1.10 sin cos tan (DPC) 96 | ) 97 | (/usr/local/texlive/2017/texmf-dist/tex/latex/graphics-cfg/graphics.cfg 98 | File: graphics.cfg 2016/06/04 v1.11 sample graphics configuration 99 | ) 100 | Package graphics Info: Driver file: pdftex.def on input line 99. 101 | 102 | (/usr/local/texlive/2017/texmf-dist/tex/latex/graphics-def/pdftex.def 103 | File: pdftex.def 2017/01/12 v0.06k Graphics/color for pdfTeX 104 | 105 | (/usr/local/texlive/2017/texmf-dist/tex/generic/oberdiek/infwarerr.sty 106 | Package: infwarerr 2016/05/16 v1.4 Providing info/warning/error messages (HO) 107 | ) 108 | (/usr/local/texlive/2017/texmf-dist/tex/generic/oberdiek/ltxcmds.sty 109 | Package: ltxcmds 2016/05/16 v1.23 LaTeX kernel commands for general use (HO) 110 | ) 111 | \Gread@gobject=\count99 112 | )) 113 | \Gin@req@height=\dimen112 114 | \Gin@req@width=\dimen113 115 | ) 116 | (/usr/local/texlive/2017/texmf-dist/tex/latex/lineno/lineno.sty 117 | Package: lineno 2005/11/02 line numbers on paragraphs v4.41 118 | \linenopenalty=\count100 119 | \output=\toks20 120 | \linenoprevgraf=\count101 121 | \linenumbersep=\dimen114 122 | \linenumberwidth=\dimen115 123 | \c@linenumber=\count102 124 | \c@pagewiselinenumber=\count103 125 | \c@LN@truepage=\count104 126 | \c@internallinenumber=\count105 127 | \c@internallinenumbers=\count106 128 | \quotelinenumbersep=\dimen116 129 | \bframerule=\dimen117 130 | \bframesep=\dimen118 131 | \bframebox=\box28 132 | LaTeX Info: Redefining \\ on input line 3056. 133 | ) 134 | (/usr/local/texlive/2017/texmf-dist/tex/latex/natbib/natbib.sty 135 | Package: natbib 2010/09/13 8.31b (PWD, AO) 136 | \bibhang=\skip46 137 | \bibsep=\skip47 138 | LaTeX Info: Redefining \cite on input line 694. 139 | \c@NAT@ctr=\count107 140 | ) 141 | (/usr/local/texlive/2017/texmf-dist/tex/latex/appendix/appendix.sty 142 | Package: appendix 2009/09/02 v1.2b extra appendix facilities 143 | \c@@pps=\count108 144 | \c@@ppsavesec=\count109 145 | \c@@ppsaveapp=\count110 146 | ) 147 | (./dipole_equations.aux) 148 | \openout1 = `dipole_equations.aux'. 149 | 150 | LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 12. 151 | LaTeX Font Info: ... okay on input line 12. 152 | LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 12. 153 | LaTeX Font Info: ... okay on input line 12. 154 | LaTeX Font Info: Checking defaults for OT1/cmr/m/n on input line 12. 155 | LaTeX Font Info: ... okay on input line 12. 156 | LaTeX Font Info: Checking defaults for OMS/cmsy/m/n on input line 12. 157 | LaTeX Font Info: ... okay on input line 12. 158 | LaTeX Font Info: Checking defaults for OMX/cmex/m/n on input line 12. 159 | LaTeX Font Info: ... okay on input line 12. 160 | LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 12. 161 | LaTeX Font Info: ... okay on input line 12. 162 | 163 | (/usr/local/texlive/2017/texmf-dist/tex/context/base/mkii/supp-pdf.mkii 164 | [Loading MPS to PDF converter (version 2006.09.02).] 165 | \scratchcounter=\count111 166 | \scratchdimen=\dimen119 167 | \scratchbox=\box29 168 | \nofMPsegments=\count112 169 | \nofMParguments=\count113 170 | \everyMPshowfont=\toks21 171 | \MPscratchCnt=\count114 172 | \MPscratchDim=\dimen120 173 | \MPnumerator=\count115 174 | \makeMPintoPDFobject=\count116 175 | \everyMPtoPDFconversion=\toks22 176 | ) (/usr/local/texlive/2017/texmf-dist/tex/generic/oberdiek/pdftexcmds.sty 177 | Package: pdftexcmds 2017/03/19 v0.25 Utility functions of pdfTeX for LuaTeX (HO 178 | ) 179 | 180 | (/usr/local/texlive/2017/texmf-dist/tex/generic/oberdiek/ifluatex.sty 181 | Package: ifluatex 2016/05/16 v1.4 Provides the ifluatex switch (HO) 182 | Package ifluatex Info: LuaTeX not detected. 183 | ) 184 | (/usr/local/texlive/2017/texmf-dist/tex/generic/oberdiek/ifpdf.sty 185 | Package: ifpdf 2017/03/15 v3.2 Provides the ifpdf switch 186 | ) 187 | Package pdftexcmds Info: LuaTeX not detected. 188 | Package pdftexcmds Info: \pdf@primitive is available. 189 | Package pdftexcmds Info: \pdf@ifprimitive is available. 190 | Package pdftexcmds Info: \pdfdraftmode found. 191 | ) 192 | (/usr/local/texlive/2017/texmf-dist/tex/latex/oberdiek/epstopdf-base.sty 193 | Package: epstopdf-base 2016/05/15 v2.6 Base part for package epstopdf 194 | 195 | (/usr/local/texlive/2017/texmf-dist/tex/latex/oberdiek/grfext.sty 196 | Package: grfext 2016/05/16 v1.2 Manage graphics extensions (HO) 197 | 198 | (/usr/local/texlive/2017/texmf-dist/tex/generic/oberdiek/kvdefinekeys.sty 199 | Package: kvdefinekeys 2016/05/16 v1.4 Define keys (HO) 200 | )) 201 | (/usr/local/texlive/2017/texmf-dist/tex/latex/oberdiek/kvoptions.sty 202 | Package: kvoptions 2016/05/16 v3.12 Key value format for package options (HO) 203 | 204 | (/usr/local/texlive/2017/texmf-dist/tex/generic/oberdiek/kvsetkeys.sty 205 | Package: kvsetkeys 2016/05/16 v1.17 Key value parser (HO) 206 | 207 | (/usr/local/texlive/2017/texmf-dist/tex/generic/oberdiek/etexcmds.sty 208 | Package: etexcmds 2016/05/16 v1.6 Avoid name clashes with e-TeX commands (HO) 209 | Package etexcmds Info: Could not find \expanded. 210 | (etexcmds) That can mean that you are not using pdfTeX 1.50 or 211 | (etexcmds) that some package has redefined \expanded. 212 | (etexcmds) In the latter case, load this package earlier. 213 | ))) 214 | Package epstopdf-base Info: Redefining graphics rule for `.eps' on input line 4 215 | 38. 216 | Package grfext Info: Graphics extension search list: 217 | (grfext) [.png,.pdf,.jpg,.mps,.jpeg,.jbig2,.jb2,.PNG,.PDF,.JPG,.JPE 218 | G,.JBIG2,.JB2,.eps] 219 | (grfext) \AppendGraphicsExtensions on input line 456. 220 | 221 | (/usr/local/texlive/2017/texmf-dist/tex/latex/latexconfig/epstopdf-sys.cfg 222 | File: epstopdf-sys.cfg 2010/07/13 v1.3 Configuration of (r)epstopdf for TeX Liv 223 | e 224 | )) 225 | LaTeX Font Info: Try loading font information for OMS+cmr on input line 23. 226 | 227 | (/usr/local/texlive/2017/texmf-dist/tex/latex/base/omscmr.fd 228 | File: omscmr.fd 2014/09/29 v2.5h Standard LaTeX font definitions 229 | ) 230 | LaTeX Font Info: Font shape `OMS/cmr/m/n' in size <10.95> not available 231 | (Font) Font shape `OMS/cmsy/m/n' tried instead on input line 23. 232 | [1 233 | 234 | {/usr/local/texlive/2017/texmf-var/fonts/map/pdftex/updmap/pdftex.map}] [2] [3] 235 | [4] 236 | (./dipole_equations.bbl) [5] (./dipole_equations.aux) ) 237 | Here is how much of TeX's memory you used: 238 | 2782 strings out of 492995 239 | 37742 string characters out of 6132703 240 | 107215 words of memory out of 5000000 241 | 6307 multiletter control sequences out of 15000+600000 242 | 10984 words of font info for 39 fonts, out of 8000000 for 9000 243 | 1141 hyphenation exceptions out of 8191 244 | 39i,16n,25p,564b,192s stack positions out of 5000i,500n,10000p,200000b,80000s 245 | 261 | Output written on dipole_equations.pdf (5 pages, 170198 bytes). 262 | PDF statistics: 263 | 80 PDF objects out of 1000 (max. 8388607) 264 | 57 compressed objects within 1 object stream 265 | 0 named destinations out of 1000 (max. 500000) 266 | 1 words of extra memory for PDF output out of 10000 (max. 10000000) 267 | 268 | -------------------------------------------------------------------------------- /src/dipole/dipole.py: -------------------------------------------------------------------------------- 1 | """ 2 | Dipole class - Calculate parameters that involve the Earth's magnetic dipole 3 | 4 | The Dipole class is initialized with an epoch (decimal year) that is used to 5 | get the relevant dipole Gauss coefficients from the IGRF coefficients. After initialization, 6 | the following methods are available (all are vectorized): 7 | * B(lat, r) - calculate dipole magnetic field values 8 | * tilt(times) - calculate dipole tilt angle for one or more times 9 | * geo2mag(lat, lon) - convert from geocentric to centered dipole coords and components 10 | * mag2geo(lat, lon) - convert from centered dipole to geocentric coords and components 11 | * mlt2mlon(mlt , time) - convert magnetic local time to magnetic longitude 12 | * mlon2mlt(mlon, time) - convert magnetic longitude to magnetic local time 13 | * get_apex_base_vectors(lat, r, R = 6371.2) - get apex basis vectors appropriate for dipole field **not using full IGRF** 14 | * get_flux(lon, lat) - calculate magnetic flux inside boundar(y/ies) defined by lon, lat 15 | * get_flux_numerical(lon, lat) - alternative way to calculate flux (but only for one boundary at a time) 16 | 17 | Note: As an alterntive to the epoch initialization, the dipole pole location and reference magnetic field can be specified 18 | manually. This allows for conversions of coordinates and components between arbitrary spherical coordinate systems. 19 | 20 | and the following parameters: 21 | * north_pole - dipole pole position in northern hemisphere 22 | * south_pole - dipole pole position in southern hemisphere 23 | * axis - ECEF dipole axis unit vector (pointing to north) 24 | * B0 - reference magnetic field 25 | 26 | For definitions see Section 3 in 27 | "Magnetic Coordinate Systems" by Laundal & Richmond (2017), DOI 10.1007/s11214-016-0275-y 28 | 29 | 30 | In addition to the Dipole class, this script includes the following helper functions: 31 | * sph_to_car(sph, deg = True) - convert from spherical to cartesian coordinates 32 | * car_to_sph(car, deg = True) - convert from cartesian to spherical coordinates 33 | * enu_to_ecef(v, lon, lat) - convert from ENU to ECEF components 34 | * ecef_to_enu(v, lon, lat) - convert from ECEF to ENU components 35 | * subsol(datetimes) - calculate location(s) of subsolar point 36 | 37 | """ 38 | 39 | import numpy as np 40 | import pandas as pd 41 | from ppigrf.ppigrf import read_shc, yearfrac_to_datetime 42 | d2r = np.pi/180 43 | r2d = 1/d2r 44 | 45 | RE = 6371.2 # reference radius in km 46 | MU0 = 4 * np.pi * 1e-7 47 | 48 | # HELPER FUNCTIONS - SPHERICAL COORDINATES 49 | ########################################## 50 | def sph_to_car(sph, deg = True): 51 | """ Convert from spherical to cartesian coordinates 52 | 53 | Parameters 54 | ---------- 55 | sph : 3 x N array 56 | 3 x N array, where the rows are, from top to bottom: 57 | radius, colatitude, and longitude 58 | deg : bool, optional 59 | set to True if input is given in degrees. False if radians 60 | 61 | Returns 62 | ------- 63 | car : 3 x N array 64 | 3 x N array, where the rows are, from top to bottom: 65 | x, y, z, in ECEF coordinates 66 | """ 67 | 68 | r, theta, phi = sph 69 | 70 | if deg == False: 71 | conv = 1. 72 | else: 73 | conv = d2r 74 | 75 | return np.vstack((r * np.sin(theta * conv) * np.cos(phi * conv), 76 | r * np.sin(theta * conv) * np.sin(phi * conv), 77 | r * np.cos(theta * conv))) 78 | 79 | 80 | def car_to_sph(car, deg = True): 81 | """ Convert from spherical to cartesian coordinates 82 | 83 | Parameters 84 | ---------- 85 | car : 3 x N array 86 | 3 x N array, where the rows are, from top to bottom: 87 | x, y, z, in ECEF coordinates 88 | 89 | Returns 90 | ------- 91 | sph : 3 x N array 92 | 3 x N array, where the rows are, from top to bottom: 93 | radius, colatitude, and longitude 94 | deg : bool, optional 95 | set to True if output is wanted in degrees. False if radians 96 | """ 97 | 98 | x, y, z = car 99 | 100 | if deg == False: 101 | conv = 1. 102 | else: 103 | conv = r2d 104 | 105 | r = np.sqrt(x**2 + y**2 + z**2) 106 | theta = np.arccos(z/r)*conv 107 | np.seterr(invalid='ignore', divide='ignore') 108 | phi = ((np.arctan2(y, x)*180/np.pi) % 360)/180*np.pi * conv 109 | 110 | return np.vstack((r, theta, phi)) 111 | 112 | 113 | def enu_to_ecef(v, lon, lat, reverse = False): 114 | """ convert vector(s) v from ENU to ECEF (or opposite) 115 | 116 | Parameters 117 | ---------- 118 | v: array 119 | N x 3 array of east, north, up components 120 | lat: array 121 | N array of latitudes (degrees) 122 | lon: array 123 | N array of longitudes (degrees) 124 | reverse: bool (optional) 125 | perform the reverse operation (ecef -> enu). Default False 126 | 127 | Returns 128 | ------- 129 | v_ecef: array 130 | N x 3 array of x, y, z components 131 | 132 | """ 133 | 134 | # construct unit vectors in east, north, up directions: 135 | ph = lon * d2r 136 | th = (90 - lat) * d2r 137 | 138 | e = np.vstack((-np.sin(ph) , np.cos(ph), np.zeros_like(ph))).T # (N, 3) 139 | n = np.vstack((-np.cos(th) * np.cos(ph), -np.cos(th) * np.sin(ph), np.sin(th) )).T # (N, 3) 140 | u = np.vstack(( np.sin(th) * np.cos(ph), np.sin(th) * np.sin(ph), np.cos(th) )).T # (N, 3) 141 | 142 | # rotation matrices (enu in columns if reverse, in rows otherwise): 143 | R_EN_2_ECEF = np.stack((e, n, u), axis = 1 if reverse else 2) # (N, 3, 3) 144 | 145 | # perform the rotations: 146 | return np.einsum('nij, nj -> ni', R_EN_2_ECEF, v) 147 | 148 | 149 | def ecef_to_enu(v, lon, lat): 150 | """ convert vector(s) v from ECEF to ENU 151 | 152 | Parameters 153 | ---------- 154 | v: array 155 | N x 3 array of x, y, z components 156 | lat: array 157 | N array of latitudes (degrees) 158 | lon: array 159 | N array of longitudes (degrees) 160 | 161 | Returns 162 | ------- 163 | v_ecef: array 164 | N x 3 array of east, north, up components 165 | 166 | See enu_to_ecef for implementation details 167 | """ 168 | return enu_to_ecef(v, lon, lat, reverse = True) 169 | 170 | 171 | 172 | # HELPER FUNCTIONS - SUNLIGHT 173 | ############################# 174 | def subsol(datetimes): 175 | """ 176 | calculate subsolar point at given datetime(s) 177 | 178 | Parameters 179 | ---------- 180 | datetimes : datetime or list of datetimes 181 | datetime or list (or other iterable) of datetimes 182 | 183 | Returns 184 | ------- 185 | subsol_lat : ndarray 186 | latitude(s) of the subsolar point 187 | subsol_lon : ndarray 188 | longiutde(s) of the subsolar point 189 | 190 | Note 191 | ---- 192 | The code is vectorized, so it should be fast. 193 | 194 | After Fortran code by: 961026 A. D. Richmond, NCAR 195 | 196 | Documentation from original code: 197 | Find subsolar geographic latitude and longitude from date and time. 198 | Based on formulas in Astronomical Almanac for the year 1996, p. C24. 199 | (U.S. Government Printing Office, 1994). 200 | Usable for years 1601-2100, inclusive. According to the Almanac, 201 | results are good to at least 0.01 degree latitude and 0.025 degree 202 | longitude between years 1950 and 2050. Accuracy for other years 203 | has not been tested. Every day is assumed to have exactly 204 | 86400 seconds; thus leap seconds that sometimes occur on December 205 | 31 are ignored: their effect is below the accuracy threshold of 206 | the algorithm. 207 | """ 208 | 209 | # use pandas DatetimeIndex for fast access to year, month day etc... 210 | if hasattr(datetimes, '__iter__'): 211 | datetimes = pd.DatetimeIndex(datetimes) 212 | else: 213 | datetimes = pd.DatetimeIndex([datetimes]) 214 | 215 | year = np.float64(datetimes.year) 216 | # day of year: 217 | doy = np.float64(datetimes.dayofyear) 218 | # seconds since start of day: 219 | ut = np.float64(datetimes.hour * 60.**2 + datetimes.minute*60. + datetimes.second ) 220 | 221 | yr = year - 2000 222 | 223 | if year.max() >= 2100 or year.min() <= 1600: 224 | raise ValueError('subsol.py: subsol invalid after 2100 and before 1600') 225 | 226 | nleap = np.floor((year-1601)/4.) 227 | nleap = np.array(nleap) - 99 228 | 229 | # exception for years <= 1900: 230 | ncent = np.floor((year-1601)/100.) 231 | ncent = 3 - ncent 232 | nleap[year <= 1900] = nleap[year <= 1900] + ncent[year <= 1900] 233 | 234 | l0 = -79.549 + (-.238699*(yr-4*nleap) + 3.08514e-2*nleap) 235 | 236 | g0 = -2.472 + (-.2558905*(yr-4*nleap) - 3.79617e-2*nleap) 237 | 238 | # Days (including fraction) since 12 UT on January 1 of IYR: 239 | df = (ut/86400. - 1.5) + doy 240 | 241 | # Addition to Mean longitude of Sun since January 1 of IYR: 242 | lf = .9856474*df 243 | 244 | # Addition to Mean anomaly since January 1 of IYR: 245 | gf = .9856003*df 246 | 247 | # Mean longitude of Sun: 248 | l = l0 + lf 249 | 250 | # Mean anomaly: 251 | g = g0 + gf 252 | grad = g*np.pi/180. 253 | 254 | # Ecliptic longitude: 255 | lmbda = l + 1.915*np.sin(grad) + .020*np.sin(2.*grad) 256 | lmrad = lmbda*np.pi/180. 257 | sinlm = np.sin(lmrad) 258 | 259 | # Days (including fraction) since 12 UT on January 1 of 2000: 260 | n = df + 365.*yr + nleap 261 | 262 | # Obliquity of ecliptic: 263 | epsilon = 23.439 - 4.e-7*n 264 | epsrad = epsilon*np.pi/180. 265 | 266 | # Right ascension: 267 | alpha = np.arctan2(np.cos(epsrad)*sinlm, np.cos(lmrad)) * 180./np.pi 268 | 269 | # Declination: 270 | delta = np.arcsin(np.sin(epsrad)*sinlm) * 180./np.pi 271 | 272 | # Subsolar latitude: 273 | sbsllat = delta 274 | 275 | # Equation of time (degrees): 276 | etdeg = l - alpha 277 | nrot = np.round(etdeg/360.) 278 | etdeg = etdeg - 360.*nrot 279 | 280 | # Apparent time (degrees): 281 | aptime = ut/240. + etdeg # Earth rotates one degree every 240 s. 282 | 283 | # Subsolar longitude: 284 | sbsllon = 180. - aptime 285 | nrot = np.round(sbsllon/360.) 286 | sbsllon = sbsllon - 360.*nrot 287 | 288 | return sbsllat, sbsllon 289 | 290 | 291 | 292 | # DIPOLE CLASS 293 | ############## 294 | class Dipole(object): 295 | def __init__(self, epoch = 2020., dipole_pole = None, B0 = None): 296 | """ Initialize Dipole object 297 | 298 | Parameters 299 | ---------- 300 | epoch: float, optional 301 | epoch for IGRF dipole Gauss coefficients in decimal year. 302 | Must be scalar. Default is 2020. The dipole pole location and the 303 | reference magnetic field B0 will be calculated from the IGRF coefficients 304 | for the given epoch. 305 | dipole_pole: tuple, optional 306 | dipole pole location in geocentric coordinates, as a tuple of (latitude, longitude) in 307 | degrees. If specified, it will override the epoch parameter. Default is that it is not 308 | given, and that the dipole pole is determined by the IGRF coefficients for given epoch. 309 | If B0 is specified, but dipole_pole is not, dipole_pole will be set to (90, 0) 310 | B0: scalar, optional 311 | reference magnetic field in nT. If specified, it will override the epoch parameter. 312 | Default is that it is not given, and that the reference magnetic field is 313 | from IGRF coefficients for given epoch. If dipole_pole is specified, but B0 is not, 314 | B0 will be set to 1. 315 | """ 316 | 317 | if dipole_pole is None and B0 is None: # Calculate dipole parameters from IGRF 318 | if np.array(epoch).size != 1: 319 | raise Exception('epoch must be scalar') 320 | 321 | # read coefficient file: 322 | g, h = read_shc() 323 | 324 | date = yearfrac_to_datetime(np.array([epoch])) 325 | 326 | if (date > g.index[-1]) or (date < g.index[0]): 327 | print('Warning: You provided date(s) not covered by coefficient file \n({} to {})'.format( 328 | g.index[0].date(), g.index[-1].date())) 329 | 330 | # Interpolate IGRF coefficients to the given epoch: 331 | index = g.index.union(date) 332 | g = g.reindex(index).groupby(index).first() # reindex and skip duplicates 333 | h = h.reindex(index).groupby(index).first() # reindex and skip duplicates 334 | g = g.interpolate(method = 'time').loc[date, :] 335 | h = h.interpolate(method = 'time').loc[date, :] 336 | 337 | # Select dipole coefficicents 338 | g10, g11, h11 = g[(1, 0)].values[0], g[(1, 1)].values[0], h[(1, 1)].values[0] 339 | 340 | # Reference magnetic field: 341 | self.B0 = np.sqrt(g10**2 + g11**2 + h11**2) 342 | 343 | # Unit vector pointing to dipole pole in the north (negative dipole axis in physics convention) 344 | self.axis = -np.hstack((g11, h11, g10))/self.B0 345 | 346 | # Pole locations: 347 | colat, longitude = car_to_sph( self.axis.reshape((-1, 1)), deg = True)[1:] 348 | self.north_pole = np.array(( 90 - colat, longitude) ).flatten() 349 | self.south_pole = np.array((- 90 + colat, (longitude + 180) % 360)).flatten() 350 | 351 | self.string_representation = 'Dipole object for epoch {}'.format(str(date[0].date())) 352 | 353 | else: 354 | if dipole_pole is None: 355 | dipole_pole = (90, 0) # default value if B0 is specified 356 | if B0 is None: 357 | B0 = 1 # default value if dipole_pole is specified 358 | 359 | self.B0 = B0 360 | 361 | self.north_pole = np.array(( dipole_pole[0], dipole_pole[1])) 362 | self.south_pole = np.array((-dipole_pole[0], (dipole_pole[1] + 180) % 360)) 363 | 364 | # calculate dipole axis 365 | ph, th = np.deg2rad(self.north_pole[1]), np.deg2rad(90 - self.north_pole[0]) 366 | self.axis = np.hstack((np.sin(th) * np.cos(ph), np.sin(th) * np.sin(ph), np.cos(th))) 367 | 368 | self.string_representation = 'Dipole object, user-specified pole at {} degrees and B0 = {} nT'.format(dipole_pole, B0) 369 | 370 | 371 | 372 | 373 | def __str__(self): 374 | return self.string_representation 375 | 376 | def __repr__(self): 377 | return str(self) 378 | 379 | def set_epoch(self, epoch): 380 | """ Initialize the Dipole object again with new epoch """ 381 | self.__init__(epoch) 382 | 383 | def B(self, lat, r): 384 | """ 385 | Calculate components of the dipole field in dipole coordinates 386 | 387 | Parameters 388 | ---------- 389 | lat : array 390 | latitude in centered dipole coordinates - in degrees 391 | r : array 392 | radius in km 393 | 394 | Returns 395 | ------- 396 | Bn : array 397 | dipole field in northward direction, in nT. Shape implied by lat and r 398 | using numpy broadcasting rules 399 | Br : array 400 | dipole field in radial direction, in nT. Shape implied by lat and r 401 | using numpy broadcasting rules 402 | """ 403 | 404 | shape = np.broadcast(lat, r).shape 405 | colat = np.deg2rad(90 - (lat * np.ones_like(r)).flatten()) 406 | r = (np.ones_like(lat) * r).flatten() 407 | 408 | Bn = self.B0 * (RE / r) ** 3 * np.sin( colat ) 409 | Br = -2 * self.B0 * (RE / r) ** 3 * np.cos( colat ) 410 | 411 | return Bn.reshape(shape), Br.reshape(shape) 412 | 413 | 414 | def tilt(self, times): 415 | """ 416 | Calculate dipole tilt angle for selected time(s) 417 | 418 | Parameters 419 | ---------- 420 | times: array 421 | array of datetimes for which dipole tilt angle shall be calculated 422 | 423 | Returns 424 | ------- 425 | tilt: array 426 | array of dipole tilt angles, in degrees, with same shape as input 427 | 428 | """ 429 | 430 | shape = np.array(times).shape 431 | times = np.squeeze(np.array(times)).flatten() 432 | 433 | # get subsolar point coordinates 434 | sslat, sslon = subsol(times) 435 | 436 | s = np.vstack(( np.cos(sslat * d2r) * np.cos(sslon * d2r), 437 | np.cos(sslat * d2r) * np.sin(sslon * d2r), 438 | np.sin(sslat * d2r))).T 439 | m = self.axis 440 | 441 | # calculate tilt angle: 442 | return np.arcsin( np.sum( s * m , axis = 1 )).reshape(shape) * r2d 443 | 444 | 445 | def geo2mag(self, lat, lon, Ae = None, An = None, inverse = False, epsilon = 1e-12): 446 | """ Convert geographic (geocentric) to centered dipole coordinates 447 | 448 | The conversion uses IGRF coefficients directly, interpolated 449 | to the provided epoch. The construction of the rotation matrix 450 | follows Laundal & Richmond (2017) [4]_ . 451 | 452 | Preserves shape. glat, glon, Ae, and An should have matching shapes 453 | 454 | Parameters 455 | ---------- 456 | lat : array_like 457 | array of geographic latitudes 458 | lon : array_like 459 | array of geographic longitudes 460 | Ae : array-like, optional 461 | array of eastward vector components to be converted. Default 462 | is None, and no converted vector components will be returned 463 | An : array-like, optional 464 | array of northtward vector components to be converted. Default 465 | is None, and no converted vector components will be returned 466 | inverse: bool, optional 467 | set to True to convert from magnetic to geographic. 468 | Default is False 469 | epsilon: float, optional 470 | this number limits how small the norm of the vectors can be. Your 471 | vectors (with components Ae and An) should be typically larger 472 | than this. If they are not, decrease epsilon 473 | 474 | Returns 475 | ------- 476 | cdlat : array 477 | array of centered dipole latitudes [degrees] 478 | cdlon : array 479 | array of centered dipole longitudes [degrees] 480 | Ae_cd : array 481 | array of eastward vector components in dipole coords 482 | (if Ae != None and An != None) 483 | An_cd : ndarray 484 | array of northward vector components in dipole coords 485 | (if Ae != None and An != None) 486 | 487 | """ 488 | 489 | if any([Ae is None, An is None]): 490 | Ae, An = 1, 1 491 | return_components = False 492 | else: 493 | return_components = True 494 | 495 | try: 496 | lat, lon, Ae, An = np.broadcast_arrays(lat, lon, Ae, An) 497 | shape = lat.shape 498 | lat, lon, Ae, An = lat.flatten(), lon.flatten(), Ae.flatten(), An.flatten() 499 | except: 500 | raise Exception('Input have inconsistent shapes') 501 | 502 | 503 | # make rotation matrix from geo to cd 504 | Zcd = self.axis 505 | Zgeo_x_Zcd = np.cross(np.array([0, 0, 1]), Zcd) 506 | Ycd = Zgeo_x_Zcd / np.linalg.norm(Zgeo_x_Zcd) 507 | Xcd = np.cross(Ycd, Zcd) 508 | 509 | Rgeo_to_cd = np.vstack((Xcd, Ycd, Zcd)) 510 | 511 | if inverse: # transpose rotation matrix to get inverse operation 512 | Rgeo_to_cd = Rgeo_to_cd.T 513 | 514 | # convert input to ECEF: 515 | colat = 90 - lat 516 | r_geo = sph_to_car(np.vstack((np.ones_like(colat), colat, lon)), deg = True) 517 | 518 | # rotate: 519 | r_cd = Rgeo_to_cd.dot(r_geo) 520 | 521 | # convert result back to spherical: 522 | _, colat_cd, lon_cd = car_to_sph(r_cd, deg = True) 523 | 524 | # return coords if vector components are not to be calculated 525 | if return_components == False: 526 | return 90 - colat_cd.reshape(shape), lon_cd.reshape(shape) 527 | 528 | # convert components: 529 | A_geo_enu = np.vstack((Ae, An, np.zeros(Ae.size))) 530 | A = np.sqrt(Ae**2 + An**2) 531 | A[A < epsilon] = epsilon # to avoid zeros 532 | A_geo_ecef = enu_to_ecef((A_geo_enu / A).T, lon, lat ) # rotate normalized vectors to ecef 533 | A_cd_ecef = Rgeo_to_cd.dot(A_geo_ecef.T) 534 | A_cd_enu = ecef_to_enu(A_cd_ecef.T, lon_cd, 90 - colat_cd).T * A 535 | 536 | # return coords and vector components: 537 | return 90 - colat_cd.reshape(shape), lon_cd.reshape(shape), A_cd_enu[0].reshape(shape), A_cd_enu[1].reshape(shape) 538 | 539 | 540 | def mag2geo(self, lat, lon, Ae = None, An = None): 541 | """ 542 | Convert centered dipole coordinates to geocentric coordinates 543 | 544 | The conversion uses IGRF coefficients directly, interpolated 545 | to the provided epoch. The construction of the rotation matrix 546 | follows Laundal & Richmond (2017) [4]_ . 547 | 548 | Preserves shape. glat, glon, Ae, and An should have matching shapes 549 | 550 | Parameters 551 | ---------- 552 | lat : array_like 553 | array of centered dipole latitudes 554 | lon : array_like 555 | array of centered dipole longitudes 556 | Ae : array-like, optional 557 | array of eastward vector components to be converted. Default 558 | is None, and no converted vector components will be returned 559 | An : array-like, optional 560 | array of northtward vector components to be converted. Default 561 | is None, and no converted vector components will be returned 562 | 563 | Returns 564 | ------- 565 | gclat : array 566 | array of geocentric latitudes [degrees] 567 | gcon : array 568 | array of geocentric longitudes [degrees] 569 | Ae_cd : array 570 | array of eastward vector components in geocentric 571 | (if Ae != None and An != None) 572 | An_cd : ndarray 573 | array of northward vector components in geocentric 574 | (if Ae != None and An != None) 575 | 576 | """ 577 | return self.geo2mag(lat, lon, Ae = Ae, An = An, inverse = True) 578 | 579 | 580 | def mlon2mlt(self, mlon, times): 581 | """ 582 | Convert magnetic longitude to magnetic local time using equation (93) 583 | in Laundal & Richmond (2017) [4]_. This equation is valid for longitudes 584 | given in several different coordinate systems, including Apex coordinates, 585 | AACGM, eccentric dipole coordinates, and of course dipole coordinates. 586 | 587 | Calculations are vectorized, and shapes are preserved, using numpy 588 | broadcasting rules 589 | 590 | Parameters 591 | ---------- 592 | mlon: array 593 | array of magnetic longitudes [degrees] 594 | times: array 595 | array of datetimes 596 | 597 | Returns 598 | ------- 599 | mlt: array 600 | array of magnetic local times [hours], with shape implied by mlon and times 601 | 602 | """ 603 | 604 | shape = np.broadcast(mlon, times).shape 605 | mlon = np.broadcast_to(mlon , shape).flatten() 606 | times = np.broadcast_to(times, shape).flatten() 607 | 608 | ssglat, ssglon = subsol(times) 609 | sqlat, ssqlon = self.geo2mag(ssglat, ssglon) 610 | 611 | londiff = mlon - ssqlon 612 | londiff = (londiff + 180) % 360 - 180 # signed difference in longitude 613 | 614 | mlt = (180. + londiff)/15. # convert to mlt with ssqlon at noon 615 | 616 | return mlt.reshape(shape) 617 | 618 | 619 | def mlt2mlon(self, mlt, times): 620 | """ 621 | Convert magnetic local time to magnetic longitude, using the inverse 622 | operation of mlon2mlt 623 | 624 | Parameters 625 | ---------- 626 | mlt: array 627 | array of magnetic local times [hours] 628 | times: array 629 | array of datetimes 630 | 631 | Returns 632 | ------- 633 | mlon: array 634 | array of magnetic longitudes [degrees], hape implied by input 635 | 636 | """ 637 | 638 | shape = np.broadcast(mlt, times).shape 639 | mlt = np.broadcast_to(mlt , shape).flatten() 640 | times = np.broadcast_to(times, shape).flatten() 641 | 642 | ssglat, ssglon = map(np.array, subsol(times)) 643 | sqlat, ssqlon = self.geo2mag(ssglat, ssglon) 644 | 645 | mlon = (15 * mlt - 180 + ssqlon + 360) % 360 646 | 647 | return mlon.reshape(shape) 648 | 649 | 650 | def get_length(self, lat, r = RE, quiet = False): 651 | """ Calculate the length of the field line that maps to lat 652 | 653 | Not tested! 654 | 655 | Parameters 656 | ---------- 657 | lat : scalar 658 | latitude in degrees 659 | 660 | Returns 661 | ------- 662 | Length of the dipole magnetic field line, integrated from -lat to lat, in 663 | same units as r. 664 | 665 | """ 666 | 667 | if not quiet: 668 | print('warning: not checked/tested, use with caution') 669 | 670 | from scipy.integrate import quad 671 | 672 | # define function that returns arc length as colatitude theta 673 | def ds(theta): return(np.sin(theta) * np.sqrt(4 - 3 * np.sin(theta)**2)) 674 | 675 | length_unit_r, error = quad(ds, np.deg2rad(90 - np.abs(lat)), np.deg2rad( 90 + np.abs(lat))) 676 | 677 | return( length_unit_r / np.sin(np.deg2rad(90 - lat))**2 * r ) 678 | 679 | 680 | def get_apex_base_vectors(self, lat, r, R = 6371.2): 681 | """ Calculate apex coordinate base vectors d_i and e_i (i = 1, 2, 3) 682 | 683 | The base vectors are defined in Richmond (1995). They can be calculated analytically 684 | for a dipole magnetic field and spherical Earth. 685 | 686 | The output vectors will have shape (3, N) where N is the combined size of the input, 687 | after numpy broadcasting. The three rows correspond to east, north and radial components 688 | for a spherical Earth. The components are given in dipole coordinates (unlike in e.g., 689 | apexpy where they are given in geodetic coordinates) 690 | 691 | Note 692 | ---- 693 | This function only calculates Modified Apex base vectors. QD base vectors f_i and g_i are 694 | just eastward, northward, and radial unit vectors for a dipole field (and f_i = g_i) 695 | 696 | Parameters 697 | ---------- 698 | r : array-like 699 | radii of the points where the base vectors shall be calculated, in same unit as R 700 | lat : array-like 701 | centered dipole latitude [deg] of the points where the base vectors shall be calculated 702 | R : float, optional 703 | Reference radius used in modified apex coordinates. Default is 6371.2 km 704 | 705 | Returns 706 | ------- 707 | d1 : array-like 708 | modified apex base vector d1 for a dipole magnetic field, shape (3, N) 709 | d2 : array-like 710 | modified apex base vector d2 for a dipole magnetic field, shape (3, N) 711 | d3 : array-like 712 | modified apex base vector d3 for a dipole magnetic field, shape (3, N) 713 | e1 : array-like 714 | modified apex base vector e1 for a dipole magnetic field, shape (3, N) 715 | e2 : array-like 716 | modified apex base vector e2 for a dipole magnetic field, shape (3, N) 717 | e3 : array-like 718 | modified apex base vector e3 for a dipole magnetic field, shape (3, N) 719 | 720 | See also 721 | -------- 722 | get_apex_base_vectors_geo: same as get_apex_base_vectors but input and output in geocentric coords 723 | """ 724 | 725 | try: 726 | r, la = map(np.ravel, np.broadcast_arrays(r, lat)) 727 | except: 728 | raise ValueError('get_apex_base_vectors: Input not broadcastable') 729 | 730 | la = np.deg2rad(la) 731 | if np.any(r / np.cos(la)**2 < R): 732 | raise ValueError('get_apex_base_vectors: Some points have apex height < R. Apex height is r/cos(lat)**2.') 733 | 734 | N = r.size 735 | 736 | R2r = R / r 737 | C = np.sqrt(4 - 3 * R2r * np.cos(la)**2) 738 | 739 | d1 = R2r ** (3./2) * np.vstack((np.ones(N), np.zeros(N), np.zeros(N))) 740 | d2 = -R2r ** (3./2) / C * np.vstack((np.zeros(N), 2 * np.sin(la), np.cos(la))) 741 | d3 = R2r ** (-3) * C / (4 - 3 * np.cos(la)**2) * np.vstack((np.zeros(N), np.cos(la), -2*np.sin(la))) 742 | 743 | e1 = np.cross(d2.T, d3.T).T 744 | e2 = np.cross(d3.T, d1.T).T 745 | e3 = np.cross(d1.T, d2.T).T 746 | 747 | return (d1, d2, d3, e1, e2, e3) 748 | 749 | def get_apex_base_vectors_geo(self, lon, lat, r, R = 6371.2): 750 | """ Calculate apex coordinate base vectors d_i and e_i (i = 1, 2, 3) 751 | 752 | The base vectors are defined in Richmond (1995). They can be calculated analytically 753 | for a dipole magnetic field and spherical Earth. 754 | 755 | The output vectors will have shape (3, N) where N is the combined size of the input, 756 | after numpy broadcasting. The three rows correspond to east, north and radial components 757 | for a spherical Earth. The components are given in geocentric coordinates 758 | 759 | Note 760 | ---- 761 | This function only calculates Modified Apex base vectors. In dipole coordinates, 762 | QD base vectors f_i and g_i are just eastward, northward, and radial unit vectors 763 | for a dipole field (and f_i = g_i). To calculate these vectors in geocentric coordinates, 764 | convert the unit vectors with mag2geo. 765 | 766 | Parameters 767 | ---------- 768 | r : array-like 769 | radii of the points where the base vectors shall be calculated, in same unit as R 770 | lon : array-like 771 | geocentric longitude [deg] of the points where the base vectors shall be calculated 772 | lat : array-like 773 | geocentric latitude [deg] of the points where the base vectors shall be calculated 774 | R : float, optional 775 | Reference radius used in modified apex coordinates. Default is 6371.2 km 776 | 777 | Returns 778 | ------- 779 | d1 : array-like 780 | modified apex base vector d1 for a dipole magnetic field, shape (3, N) 781 | d2 : array-like 782 | modified apex base vector d2 for a dipole magnetic field, shape (3, N) 783 | d3 : array-like 784 | modified apex base vector d3 for a dipole magnetic field, shape (3, N) 785 | e1 : array-like 786 | modified apex base vector e1 for a dipole magnetic field, shape (3, N) 787 | e2 : array-like 788 | modified apex base vector e2 for a dipole magnetic field, shape (3, N) 789 | e3 : array-like 790 | modified apex base vector e3 for a dipole magnetic field, shape (3, N) 791 | 792 | See also 793 | -------- 794 | get_apex_base_vectors: same as get_apex_base_vectors_geo but input and output in dipole coords 795 | """ 796 | 797 | # convert input to magnetic: 798 | mlat, mlon = self.geo2mag(lat, lon) 799 | # calculate base vectors: 800 | basevectors_m = self.get_apex_base_vectors(mlat, r, R = R) 801 | 802 | # convert base vectors to geocentric components 803 | basevectors_g = [] 804 | for b in basevectors_m: 805 | lat_, lon_, east, north = self.mag2geo(mlat, mlon, Ae = b[0], An = b[1]) 806 | basevectors_g.append(np.vstack((east, north, b[2]))) 807 | 808 | return tuple(basevectors_g) 809 | 810 | def map_vperp(self, lon, lat, alt, v, alt_target, R = 6371.2): 811 | ''' 812 | Function that takes in a set of N velocity vectors (v) and its location (lon,lat,alt), 813 | and map the part of the vector that is perpendicular to the dipole field to the 814 | desired altitude, alt_target. 815 | 816 | Parameters 817 | ---------- 818 | lon : array-like 819 | geocentric longitude [deg] of the points where the vector is to be mapped from 820 | lat : array-like 821 | geocentric latitude [deg] of the points where the vector is to be mapped from 822 | alt : array-like 823 | altitude of the points where the vector is to be mapped from, in same unit as R 824 | v : 2D array 825 | Contain the ENU geographic components of the vel. vector to be mapped, with shape (3,N) 826 | alt_target : array-like 827 | altitude of the mapped locations, in same unit as R. Must be of same length as alt 828 | R : float, optional 829 | Reference radius used in modified apex coordinates. Default is 6371.2 km 830 | 831 | Returns 832 | ------- 833 | lon_target : array-like 834 | geographic longitude [deg] of the mapped location 835 | lat_target : array-like 836 | geographic latitude [deg] of the mapped location 837 | vperpe : array-like 838 | geographic east component of the mapped vector 839 | vperpn : array-like 840 | geographic north component of the mapped vector 841 | vperpu : array-like 842 | geographic up component of the mapped vector 843 | modified apex base vector d1 for a dipole magnetic field, shape (3, N) 844 | ''' 845 | 846 | r = R + alt 847 | r_target = R + alt_target 848 | 849 | # Centered Dipole input locations 850 | mlat, mlon = self.geo2mag(lat, lon) 851 | 852 | # Calculate the mapped locations. Map from observed location (r) 853 | # to the target height (target_r) using dipole formula 854 | colat_r = 90-mlat 855 | colat_target = np.arcsin(np.sin(np.radians(colat_r)) * np.sqrt(r_target/r)) 856 | mlat_target = 90 - np.degrees(colat_target) 857 | mlon_target = mlon # in degrees 858 | 859 | # Geocentric coordinates of the mapped locations 860 | lat_target, lon_target = self.mag2geo(mlat_target, mlon_target) 861 | 862 | # Get base vectors at input altitudes, r 863 | d1, d2, d3, _1, _2, _3 = self.get_apex_base_vectors_geo(lon, lat, r, R=R) 864 | 865 | #Calculate the quantities that is constant along the field-lines 866 | ve1 = (d1[0,:]*v[0,:] + d1[1,:]*v[1,:] + d1[2,:]*v[2,:]) 867 | ve2 = (d2[0,:]*v[0,:] + d2[1,:]*v[1,:] + d2[2,:]*v[2,:]) 868 | 869 | # Calculate basis vectors at the mapped locations 870 | _1, _2, _3, e1, e2, e3 = self.get_apex_base_vectors_geo(lon_target, lat_target, r_target, R=R) 871 | 872 | 873 | #Calculate the mapped velocity using eq 4.17 in Richmond 1995. geographic components, ENU 874 | vperpmappede = (ve1.flatten()*e1[0,:] + ve2.flatten()*e2[0,:]) 875 | vperpmappedn = (ve1.flatten()*e1[1,:] + ve2.flatten()*e2[1,:]) 876 | vperpmappedu = (ve1.flatten()*e1[2,:] + ve2.flatten()*e2[2,:]) 877 | 878 | return (lon_target, lat_target, vperpmappede, vperpmappedn, vperpmappedu) 879 | 880 | def map_E(self, lon, lat, r, E, target_r): 881 | pass 882 | 883 | def get_flux(self, lon, lat, R = 6371.2): 884 | """ Calculate magnetic flux poleward of one or more closed curves described by lon and lat 885 | 886 | The magnetic flux calculation is performed by integrating the analytically calculated expression for 887 | latitude-integrated flux over longitude. The integration is carried out using cubic splines. 888 | Thanks to Anders Ohma for coming up with this algorithm 889 | 890 | Note 891 | ---- 892 | This function does not take into account boundaries that are not bijective; each longitude 893 | should have only one latitude 894 | 895 | 896 | Parameters 897 | ---------- 898 | lon : N-element 1D array 899 | longitudes [deg] describing the longitudes at the contour(s) that define the area in which 900 | we calculate magnetic flux. The array should be strictly increasing, start at 0 and end at 360, 901 | because we use interpolation with periodic boundary conditions defined at lon[0] and lon[-1] 902 | lat : M x N element 1D array (or N element 1D array) 903 | latitudes [deg] describing M boundaries above which the flux should be calculated. If lat is not 904 | already M x N, it must be possible to reshape. 905 | R : float, optional 906 | radius [km] at which to calcualte flux. Default is 6371.2 km 907 | 908 | Returns 909 | ------- 910 | flux : array 911 | M-element array with magnetic flux poleward of the boundar(y/ies), in Weber 912 | """ 913 | 914 | try: 915 | from scipy.interpolate import CubicSpline 916 | except: 917 | raise Exception('get_flux: Could not import scipy.interpolate.CubicSpline') 918 | 919 | lon = lon.flatten() 920 | 921 | if np.any(np.diff(lon) < 0): 922 | raise ValueError('get_flux: lon should be strictly increasing') 923 | 924 | if not np.allclose([lon[0], lon[-1]-360], 0): 925 | print("""Warning: lon is assumed to be periodic at lon[0] and lon[-1]. 926 | Your input does not start at 0 and end at 360. 927 | This can cause inaccuracies if the boundary latitudes vary a lot""") 928 | 929 | if np.any(lat < 0): 930 | raise ValueError('all latitudes should be > 0') 931 | 932 | M = lat.size / lon.size 933 | if not np.isclose(M % 1, 0): # M not integer 934 | raise ValueError('lat can not be reshaped to (M, lon.size)') 935 | 936 | M = int(M) 937 | if M == 1: 938 | lat = lat.flatten() 939 | else: 940 | lat = lat.reshape((M, lon.size)) 941 | 942 | C = self.B0 * 1e-9 * (RE*1e3)** 3 / (R * 1e3) # convert all quantities to SI units 943 | 944 | dflux = CubicSpline(np.deg2rad(lon), 945 | C * np.cos(np.deg2rad(lat))**2, # flux integrated over latitude 946 | axis = 1, bc_type = 'periodic') 947 | 948 | return dflux.integrate(0, 2 * np.pi) 949 | 950 | 951 | def get_flux_numerical(self, lon, lat, dlon = 1., dlat = 0.1, R = 6371.2): 952 | """ Calculate magnetic flux poleward of a closed curve described by lon, lat 953 | 954 | The magnetic flux calculation is performed by first interpolating the given boundary points to a constant 955 | step size, then applying Richmond95's Equation 4.15, and then numerically integrate over longitude and 956 | latitude. 957 | 958 | Note 959 | ---- 960 | This function does not take into account boundaries that are not bijective; each longitude 961 | should have only one latitude 962 | 963 | 964 | Parameters 965 | ---------- 966 | lon : array 967 | longitudes [deg] describing the closed contour poleward of which we calculate magnetic flux 968 | lat : array 969 | latitudes [deg] describing the closed contour poleward of which we calculate magnetic flux 970 | dlon : float, optional 971 | longitude resolution to use in the integral. Default is 1 degree 972 | dlat: float, optional 973 | latitude resolution to use in the integral. Default is 0.1 degree 974 | R : float, optional 975 | radius [km] at which to calcualte flux. Default is 6371.2 km 976 | 977 | Returns 978 | ------- 979 | flux : float 980 | Magnetic flux poleward of the boundary, in Weber 981 | """ 982 | 983 | assert np.all(lat >= 0) # only use positive latitudes 984 | lon, lat = map(np.ravel, np.broadcast_arrays(lon, lat)) 985 | 986 | N = np.int32(360 / dlon) + 1 # number of points in longitude direction 987 | lonxx = np.linspace(0, 360, N) # longitude coordinate 988 | boundary = np.interp(lonxx, lon, lat, period = 360) # interpolate input to constant step size 989 | 990 | minlat = lat.min() 991 | latxx = np.r_[90 - dlat/2:minlat:-dlat][::-1] # latitude integration steps 992 | 993 | d1, d2, d3, e1, e2, e3 = self.get_apex_base_vectors(latxx, R, R = R) # get Apex base vectors 994 | Bn, Br = self.B(latxx, R) 995 | Be3 = d3[1] * Bn + d3[2] * Br 996 | Be3 = Be3 * 1e-9 # nT -> T 997 | 998 | # in the equations below, I use expressions appropriate for apex coordinates, which is ok since we 999 | # use apex reference radius equal to evaluation radius 1000 | sinIm = 2 * np.sin(np.deg2rad(latxx)) / np.sqrt(4 - 3 * np.cos(np.deg2rad(latxx))**2) 1001 | dF = (R * 1e3)**2 * np.cos(np.deg2rad(latxx)) * np.abs(sinIm) * Be3 * np.deg2rad(dlon) * np.deg2rad(dlat) # flux per lon and lat according to Richmond95 Eq 4.15 1002 | dF = np.tile(dF, (lonxx.size, 1)) 1003 | dF[latxx.reshape((1, -1)) < boundary.reshape((-1, 1))] = 0 # mask elements equatorward of boundary 1004 | 1005 | return( np.sum(dF) ) 1006 | 1007 | 1008 | 1009 | 1010 | if __name__ == '__main__': 1011 | 1012 | print('Running tests on apex base vectors') 1013 | N = 1000 # number of points in random cloud 1014 | R = 6371.2+200 # apex reference height 1015 | x, y, z = np.random.random(N), np.random.random(N), np.random.random(N) 1016 | iii = x**2 + y**2 + z**2 <= 1 1017 | r = np.sqrt(x**2 + y**2 + z**2)[iii] 1018 | la = np.rad2deg(np.arcsin(z[iii] / r)) 1019 | r = R * (1 + r) 1020 | 1021 | iii = r / np.cos(np.deg2rad(la))**2 >= R 1022 | r = r[iii] 1023 | la = la[iii] 1024 | 1025 | 1026 | d = Dipole(2020) 1027 | d1, d2, d3, e1, e2, e3 = d.get_apex_base_vectors(la, r, R = R) 1028 | Bn, Br = d.B(la, r) 1029 | BB = np.vstack((np.zeros_like(Bn), Bn, Br)) 1030 | 1031 | # test orthogonality properties 1032 | assert np.allclose(np.abs(np.sum(d1*d2, axis = 0)), 0) 1033 | assert np.allclose(np.abs(np.sum(d1*d3, axis = 0)), 0) 1034 | assert np.allclose(np.abs(np.sum(d2*d3, axis = 0)), 0) 1035 | assert np.allclose(np.abs(np.sum(e1*e2, axis = 0)), 0) 1036 | assert np.allclose(np.abs(np.sum(e1*e3, axis = 0)), 0) 1037 | assert np.allclose(np.abs(np.sum(e2*e3, axis = 0)), 0) 1038 | assert np.allclose(np.abs(np.sum(d1*e2, axis = 0)), 0) 1039 | assert np.allclose(np.abs(np.sum(d1*e3, axis = 0)), 0) 1040 | assert np.allclose(np.abs(np.sum(d2*e1, axis = 0)), 0) 1041 | assert np.allclose(np.abs(np.sum(d2*e3, axis = 0)), 0) 1042 | assert np.allclose(np.abs(np.sum(d3*e1, axis = 0)), 0) 1043 | assert np.allclose(np.abs(np.sum(d3*e2, axis = 0)), 0) 1044 | assert np.allclose(np.linalg.norm(np.cross(e3.T, BB.T), axis = 1), 0) # e3 perpendicular to B 1045 | assert np.allclose(np.linalg.norm(np.cross(d3.T, BB.T), axis = 1), 0) # d3 perpendicular to B 1046 | assert np.all(np.sum(d3 * BB, axis = 0) > 0) # e3 along B 1047 | assert np.all(np.sum(e3 * BB, axis = 0) > 0) # d3 along B 1048 | 1049 | # test scaling properties 1050 | la = -np.linspace(1e-3, .7 * np.pi/2, 100) 1051 | req = 10*R 1052 | r = req * np.cos(la)**2 1053 | d1, d2, d3, e1, e2, e3 = d.get_apex_base_vectors(np.rad2deg(la), r, R = R) 1054 | Bn, Br = d.B(np.rad2deg(la), r) 1055 | BB = np.vstack((np.zeros_like(Bn), Bn, Br)) 1056 | Be3 = d3[1] * Bn + d3[2] * Br # should be constant since all d3 are on same field line 1057 | D = np.linalg.norm(np.cross(d1.T, d2.T), axis = 1) # B / D should be equal to Be3 1058 | B = np.sqrt(Bn**2 + Br**2) 1059 | 1060 | assert np.allclose(Be3 - B/D, 0) 1061 | assert np.allclose(Be3 - Be3[0], 0) 1062 | 1063 | print ('testing flux calculation') 1064 | lon = np.linspace(0, 360, 100) 1065 | lat0s = [80, 70, 40, 20] 1066 | lats = np.array([5 * np.cos(4 * np.deg2rad(lon)) + l for l in lat0s]) 1067 | num_flux = np.array([d.get_flux_numerical(lon, l, dlon = 0.1, dlat = 0.1, R = R) for l in lats]) 1068 | flux = d.get_flux(lon, lats, R = R) 1069 | 1070 | assert np.allclose(np.abs(num_flux - flux)/num_flux, 0, atol = 1e-2) 1071 | 1072 | # Test the user-specified pole: First use current dipole object to rotate a bunch a of eastward unit 1073 | # vectors to geographic. Then make a new dipole object with same pole location to rotate back. 1074 | print('Testing coordinate conversion with user-specified dipole location') 1075 | 1076 | x, y, z = np.random.random(N), np.random.random(N), np.random.random(N) 1077 | iii = x**2 + y**2 + z**2 <= 1 1078 | r = np.sqrt(x**2 + y**2 + z**2)[iii] 1079 | lat = np.rad2deg(np.arcsin(z[iii] / r)) 1080 | lon = np.rad2deg(np.arctan2(y[iii], x[iii])) 1081 | 1082 | gla, glo, gAe, gAn = d.mag2geo(lat, lon, Ae = np.ones(np.sum(iii)), An = np.zeros(np.sum(iii))) 1083 | print(d) 1084 | 1085 | pole_location = d.north_pole 1086 | B0 = d.B0 1087 | dd = Dipole(dipole_pole = pole_location, B0 = B0) 1088 | lat_, lon_, Ae_, An_ = dd.geo2mag(gla, glo, Ae = gAe, An = gAn) 1089 | assert np.allclose(lat_ - lat, 0) 1090 | assert np.allclose(Ae_, 1) 1091 | assert np.allclose(An_, 0) 1092 | print(dd) 1093 | 1094 | 1095 | 1096 | --------------------------------------------------------------------------------