├── .gitignore
├── .readthedocs.yaml
├── CHANGELOG.rst
├── CITATION
├── INSTALL.rst
├── LICENSE
├── README.rst
├── build_package.sh
├── chaos_examples.py
├── chaosmagpy
├── __init__.py
├── chaos.py
├── config_utils.py
├── coordinate_utils.py
├── data_utils.py
├── lib
│ ├── Earth_conductivity.dat
│ ├── RC_index.h5
│ ├── frequency_spectrum_gsm.npz
│ ├── frequency_spectrum_sm.npz
│ └── ne_110m_coastline.zip
├── model_utils.py
└── plot_utils.py
├── docs
├── Makefile
└── source
│ ├── .static
│ ├── examples
│ │ ├── CHAOS-7.mat
│ │ ├── README.txt
│ │ ├── plot_global_map.py
│ │ ├── plot_global_polar.py
│ │ ├── plot_spectrum.py
│ │ └── plot_stations.py
│ ├── plot_maps_static.png
│ └── plot_maps_tdep.png
│ ├── .templates
│ └── myclass.rst
│ ├── changelog.rst
│ ├── conf.py
│ ├── configuration.rst
│ ├── index.rst
│ ├── installation.rst
│ ├── license.rst
│ ├── readme.rst
│ ├── references.rst
│ └── usage.rst
├── environment.yml
├── pyproject.toml
├── requirements.txt
├── setup.py
├── test_package.sh
└── tests
├── __init__.py
├── helpers.py
├── test_chaos.py
├── test_coordinate_utils.py
├── test_data_utils.py
└── test_model_utils.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | build/
11 | dist/
12 | *.egg-info/
13 | *.egg
14 | *.eggs
15 | MANIFEST
16 |
17 | # Sphinx documentation
18 | docs/_build/
19 | docs/source/classes
20 | docs/source/functions
21 | docs/source/gallery
22 | docs/source/sg_execution_times.rst
23 |
24 | # Jupyter Notebook
25 | .ipynb_checkpoints
26 |
27 | # IPython
28 | profile_default/
29 | ipython_config.py
30 |
31 | # pyenv
32 | .python-version
33 |
34 | # Environments
35 | .env
36 | .venv
37 | env/
38 | venv/
39 | ENV/
40 | env.bak/
41 | venv.bak/
42 |
43 | # Spyder project settings
44 | .spyderproject
45 | .spyproject
46 |
--------------------------------------------------------------------------------
/.readthedocs.yaml:
--------------------------------------------------------------------------------
1 | # Read the Docs configuration file for Sphinx projects
2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
3 |
4 | # Required
5 | version: 2
6 |
7 | # Set the OS, Python version and other tools you might need
8 | build:
9 | os: "ubuntu-22.04"
10 | tools:
11 | python: "mambaforge-22.9"
12 |
13 | conda:
14 | environment: environment.yml
15 |
16 | # Build documentation in the "docs/" directory with Sphinx
17 | sphinx:
18 | configuration: docs/source/conf.py
19 |
--------------------------------------------------------------------------------
/CHANGELOG.rst:
--------------------------------------------------------------------------------
1 | Changelog
2 | =========
3 |
4 | Version 0.14
5 | ------------
6 | | **Date:** June 21, 2024
7 | | **Release:** v0.14
8 |
9 | News
10 | ^^^^
11 | At the moment, only Numpy <= 1.26 is supported due to a deprecation affecting
12 | a ChaosMagPy dependency. This will hopefully be fixed in the near future.
13 |
14 | Features
15 | ^^^^^^^^
16 | * Updated RC-index file to RC_1997-2024_June24_v5.dat (used during the
17 | construction of CHAOS-7.18).
18 | * Increased precision of :func:`chaosmagpy.data_utils.timestamp` and
19 | :func:`chaosmagpy.data_utils.mjd2000` to nanosecond.
20 | * Added function to compute the unit base vector in the direction of the
21 | geomagetic north pole: :func:`chaosmagpy.coordinate_utils.dipole_to_vec`.
22 |
23 | Version 0.13.1
24 | --------------
25 | | **Date:** June 4, 2024
26 | | **Release:** v0.13.1
27 |
28 | Features
29 | ^^^^^^^^
30 | * Made Matplotlib an optional dependency of ChaosMagPy.
31 | * Added optional dependencies to ``pyproject.toml``.
32 |
33 | Bugfixes
34 | ^^^^^^^^
35 | * Removed deprecated matplotlib call to register a colormap. The previous
36 | version caused an ImportError with matplotlib >=3.9.
37 |
38 | Version 0.13
39 | ------------
40 | | **Date:** January 31, 2024
41 | | **Release:** v0.13
42 |
43 | Features
44 | ^^^^^^^^
45 | * Updated RC-index file to RC_1997-2023_Dec23_v5.dat (used during the
46 | construction of CHAOS-7.17).
47 | * Extended :func:`chaosmagpy.coordinate_utils.geo_to_gg` and
48 | :func:`chaosmagpy.coordinate_utils.gg_to_geo` to also allow for the
49 | rotation of vector components.
50 | * Removed Cartopy from the dependencies (basic map projections are done with
51 | Matplotlib and coastlines are loaded from Natural Earth). New dependency for
52 | reading the coastline shapefile is PyShp (>=2.3.1).
53 | * Added function :func:`chaosmagpy.chaos.load_IGRF_txtfile` to load the IGRF
54 | model from the coefficient TXT-file.
55 |
56 | Version 0.12
57 | ------------
58 | | **Date:** April 27, 2023
59 | | **Release:** v0.12
60 |
61 | Features
62 | ^^^^^^^^
63 | * Updated RC-index file to RC_1997-2023_Feb_v8_Dst_mod.dat (used during the
64 | construction of CHAOS-7.14).
65 | * Plotting function do not return figure and axes handles anymore. If needed,
66 | they can be accessed through matplotlib's ``gcf()`` and ``gca()``.
67 | * Added function :func:`chaosmagpy.model_utils.sensitivity` to compute the
68 | sensitivity of spherical harmonic coefficients.
69 | * Changed the default colormap for the component maps to ``'PuOr_r'`` (orange
70 | for positive values and purple for negative ones).
71 |
72 | Bugfixes
73 | ^^^^^^^^
74 | * Fixed error in :func:`chaosmagpy.data_utils.load_shcfile` when reading
75 | single piece, quadratic splines. Error was due to a failure to identify the
76 | shc-parameter line as the first non-comment line in the file.
77 | * Fixed KeyError that is raised when no name is given to the
78 | :class:`chaosmagpy.chaos.CHAOS` constructor. This only affected direct calls
79 | to the constructor due to an outdated config keyword.
80 |
81 | Version 0.11
82 | ------------
83 | | **Date:** September 29, 2022
84 | | **Release:** v0.11
85 | | **Version of CHAOS:** CHAOS-7.12 (0712) and CHAOS-7.13 (0713)
86 |
87 | Features
88 | ^^^^^^^^
89 | * Updated RC-index file to RC_1997-2022_Sept_v3.dat.
90 | * Improved time conversion paragraph in the usage section.
91 | * Added option to :func:`chaosmagpy.coordinate_utils.sh_analysis` to increase
92 | the grid size for the numerical integration.
93 | * Made time conversion functions (:func:`chaosmagpy.data_utils.mjd2000`,
94 | :func:`chaosmagpy.data_utils.timestamp`,
95 | :func:`chaosmagpy.data_utils.dyear_to_mjd`,
96 | :func:`chaosmagpy.data_utils.mjd_to_dyear`) directly available upon import.
97 | * Extended :func:`chaosmagpy.model_utils.power_spectrum` to accept NumPy style
98 | ``axis`` keyword argument, and added new tests for this function.
99 | * Added more detailed error message when requesting near-magnetospheric
100 | coefficients outside the domain covered by the RC-index file.
101 | * Added input parameters ``rc_e`` and ``rc_i`` to the model call method.
102 |
103 | Version 0.10
104 | ------------
105 | | **Date:** July 1, 2022
106 | | **Release:** v0.10
107 | | **Version of CHAOS:** CHAOS-7.11 (0711)
108 |
109 | Features
110 | ^^^^^^^^
111 | * Updated RC-index file to RC_1997-2022_June_v3.
112 |
113 | Version 0.9
114 | -----------
115 | | **Date:** April 1, 2022
116 | | **Release:** v0.9
117 | | **Version of CHAOS:** CHAOS-7.10 (0710)
118 |
119 | Features
120 | ^^^^^^^^
121 | * Updated RC-index file to RC_1997-2022_Feb_v3.
122 | * Changed the default leap year setting when loading/saving shc-files using
123 | the model classes to ``leap_year=False``.
124 | * Added function :func:`chaosmagpy.chaos.load_CALS7K_txtfile` to read the
125 | CALS7K coefficients file.
126 | * Function :func:`chaosmagpy.model_utils.design_gauss` now accepts
127 | multidimensional shapes of input grids and preserves them in the output.
128 | * Added new method :meth:`chaosmagpy.chaos.Base.to_ppdict`, which returns a
129 | dictionary of the pp-form compatible with MATLAB.
130 | * Added support for calibration parameters.
131 | * Renamed "params.version" in basicConfig to "params.CHAOS_version".
132 |
133 | Bugfixes
134 | ^^^^^^^^
135 | * Functions :func:`chaosmagpy.data_utils.dyear_to_mjd` and
136 | :func:`chaosmagpy.data_utils.mjd_to_dyear` now correctly convert
137 | negative decimal years and negative modified Julian dates (erroneous offset
138 | of 1 day due to rounding to integer values).
139 |
140 | Version 0.8
141 | -----------
142 | | **Date:** December 9, 2021
143 | | **Release:** v0.8
144 | | **Version of CHAOS:** CHAOS-7.9 (0709)
145 |
146 | Features
147 | ^^^^^^^^
148 | * Updated RC-index file to RC_1997-2021_November_v3.
149 | * Added ability to compute field components at the geographic poles.
150 | * Removed cdot from SV, SA units in :func:`chaosmagpy.data_utils.gauss_units`.
151 | * Added :func:`chaosmagpy.coordinate_utils.sh_analysis`, which performs a
152 | spherical harmonic expansion on a callable.
153 |
154 | Bugfixes
155 | ^^^^^^^^
156 | * Removed Euler pre-rotation, which was not correctly implemented, and added
157 | a warning.
158 | * Fixed shc-file loader to correctly exclude extrapolation sites.
159 | * Fixed numpy broadcasting error in :func:`chaosmagpy.data_utils.mjd2000`.
160 |
161 | Version 0.7.1
162 | -------------
163 | | **Date:** August 05, 2021
164 | | **Release:** v0.7.1
165 | | **Version of CHAOS:** CHAOS-7.8 (0708)
166 |
167 | Bugfixes
168 | ^^^^^^^^
169 | * Fixed CHAOS shc-file loader.
170 |
171 | Version 0.7
172 | -----------
173 | | **Date:** August 05, 2021
174 | | **Release:** v0.7
175 | | **Version of CHAOS:** CHAOS-7.8 (0708)
176 |
177 | Features
178 | ^^^^^^^^
179 | * Added matplotlib's plot_directive for sphinx and added more examples to a
180 | new gallery section in the documentation.
181 | * Added :func:`chaosmagpy.model_utils.pp_from_bspline` to convert the spline
182 | coefficients from B-spline to PP format.
183 | * Changed the way piecewise polynomials are produced from the coefficients in
184 | shc-files. A B-spline representation is now created in an intermediate step
185 | to ensure coefficient time series that are smooth.
186 | * Changed the number format to ``'16.8f'`` when writing shc-files to increase
187 | precision.
188 | * Configuration parameters in ``chaosmagpy.basicConfig`` are now saved to and
189 | loaded from a json-formatted txt-file.
190 | * Added keyword arguments to :meth:`chaosmagpy.chaos.CHAOS.synth_coeffs_sm`
191 | and :meth:`chaosmagpy.chaos.CHAOS.synth_values_sm` to provide the RC-index
192 | values directly instead of using the built-in RC-index file.
193 |
194 | Version 0.6
195 | -----------
196 | | **Date:** March 22, 2021
197 | | **Release:** v0.6
198 | | **Version of CHAOS:** CHAOS-7.6 (0706), CHAOS-7.7 (0707)
199 |
200 | News
201 | ^^^^
202 | The latest version of CHAOS (CHAOS-7.7) corrects an error in the distributed
203 | CHAOS-7.6 model files. The mat-file and shc-file for CHAOS-7.6 were due to a
204 | bug identical to CHAOS-7.5, i.e. not correctly updated. The distributed spline
205 | coefficient file for CHAOS-7.6 was correct. The CHAOS-7.7 release corrects the
206 | errors and all CHAOS-7.7 files use updated data to March 2021.
207 |
208 | ChaosMagPy v0.6 also works with CHAOS-7.7 and does not need to be
209 | updated (2021-06-15).
210 |
211 | Features
212 | ^^^^^^^^
213 | * Added new usage sections to the documentation
214 |
215 | Bugfixes
216 | ^^^^^^^^
217 | * Fixed broken link to RC-index file (GitHub issue #5).
218 | * Added lxml to installation instructions
219 | (needed for webpage requests, optional).
220 | * Require hdf5storage version 0.1.17 (fixed read/write intent)
221 |
222 | Version 0.5
223 | -----------
224 | | **Date:** December 23, 2020
225 | | **Release:** v0.5
226 | | **Version of CHAOS:** CHAOS-7.5 (0705)
227 |
228 | Features
229 | ^^^^^^^^
230 | * Modified "nio" colormap to be white-centered.
231 | * Added spatial power spectrum of toroidal sources
232 | (:func:`chaosmagpy.model_utils.power_spectrum`)
233 |
234 | Version 0.4
235 | -----------
236 | | **Date:** September 10, 2020
237 | | **Release:** v0.4
238 | | **Version of CHAOS:** CHAOS-7.3 (0703), CHAOS-7.4 (0704)
239 |
240 | Features
241 | ^^^^^^^^
242 | * Updated RC-index file to RC_1997-2020_Aug_v4.dat.
243 | * Model name defaults to the filename it was loaded from.
244 | * Added function to read the COV-OBS.x2 model
245 | (:func:`chaosmagpy.chaos.load_CovObs_txtfile`) from a text file.
246 | * Added function to read the gufm1 model
247 | (:func:`chaosmagpy.chaos.load_gufm1_txtfile`) from a text file.
248 | * Added class method to initialize :class:`chaosmagpy.chaos.BaseModel` from a
249 | B-spline representation.
250 |
251 | Version 0.3
252 | -----------
253 | | **Date:** April 20, 2020
254 | | **Release:** v0.3
255 | | **Version of CHAOS:** CHAOS-7.2 (0702)
256 |
257 | News
258 | ^^^^
259 | The version identifier of the CHAOS model using ``x``, which stands for an
260 | extension of the model, has been replaced in favor of a simple version
261 | numbering. For example, ``CHAOS-6.x9`` is the 9th extension of the CHAOS-6
262 | series. But starting with the release of the CHAOS-7 series, the format
263 | ``CHAOS-7.1`` has been adopted to indicate the first release of the series,
264 | ``CHAOS-7.2`` the second release (formerly the first extension) and so on.
265 |
266 | Features
267 | ^^^^^^^^
268 | * Updated RC-index file to RC_1997-2020_Feb_v4.dat.
269 | * Removed version keyword of :class:`chaosmagpy.chaos.CHAOS` to avoid
270 | confusion.
271 | * Added ``verbose`` keyword to the ``call`` method of
272 | :class:`chaosmagpy.chaos.CHAOS` class to avoid printing messages.
273 | * Added :func:`chaosmagpy.data_utils.timestamp` function to convert modified
274 | Julian date to NumPy's datetime format.
275 | * Added more examples to the :class:`chaosmagpy.chaos.CHAOS` methods.
276 | * Added optional ``nmin`` and ``mmax`` to
277 | :func:`chaosmagpy.model_utils.design_gauss` and
278 | :func:`chaosmagpy.model_utils.synth_values` (nmin has been redefined).
279 | * Added optional derivative to :func:`chaosmagpy.model_utils.colloc_matrix`
280 | of the B-Spline collocation.
281 | New implementation does not have the missing endpoint problem.
282 | * Added ``satellite`` keyword to change default satellite names when loading
283 | CHAOS mat-file.
284 |
285 | Version 0.2.1
286 | -------------
287 | | **Date:** November 20, 2019
288 | | **Release:** v0.2.1
289 | | **Version of CHAOS:** CHAOS-7.1 (0701)
290 |
291 | Bugfixes
292 | ^^^^^^^^
293 | * Corrected function :func:`chaosmagpy.coordinate_utils.zenith_angle` which was
294 | computing the solar zenith angle from ``phi`` defined as the hour angle and
295 | NOT the geographic longitude. The hour angle is measure positive towards West
296 | and negative towards East.
297 |
298 | Version 0.2
299 | -----------
300 | | **Date:** October 3, 2019
301 | | **Release:** v0.2
302 | | **Version of CHAOS:** CHAOS-7.1 (0701)
303 |
304 | Features
305 | ^^^^^^^^
306 | * Updated RC-index file to recent version (August 2019, v6)
307 | * Added option ``nmin`` to :func:`chaosmagpy.model_utils.synth_values`.
308 | * Vectorized :func:`chaosmagpy.data_utils.mjd2000`,
309 | :func:`chaosmagpy.data_utils.mjd_to_dyear` and
310 | :func:`chaosmagpy.data_utils.dyear_to_mjd`.
311 | * New function :func:`chaosmagpy.coordinate_utils.local_time` for a simple
312 | computation of the local time.
313 | * New function :func:`chaosmagpy.coordinate_utils.zenith_angle` for computing
314 | the solar zenith angle.
315 | * New function :func:`chaosmagpy.coordinate_utils.gg_to_geo` and
316 | :func:`chaosmagpy.coordinate_utils.geo_to_gg` for transforming geodetic and
317 | geocentric coordinates.
318 | * Added keyword ``start_date`` to
319 | :func:`chaosmagpy.coordinate_utils.rotate_gauss_fft`
320 | * Improved performance of :meth:`chaosmagpy.chaos.CHAOS.synth_coeffs_sm` and
321 | :meth:`chaosmagpy.chaos.CHAOS.synth_coeffs_gsm`.
322 | * Automatically import :func:`chaosmagpy.model_utils.synth_values`.
323 |
324 | Deprecations
325 | ^^^^^^^^^^^^
326 | * Rewrote :func:`chaosmagpy.data_utils.load_matfile`: now traverses matfile
327 | and outputs dictionary.
328 | * Removed ``breaks_euler`` and ``coeffs_euler`` from
329 | :class:`chaosmagpy.chaos.CHAOS` class
330 | attributes. Euler angles are now handled as :class:`chaosmagpy.chaos.Base`
331 | class instance.
332 |
333 | Bugfixes
334 | ^^^^^^^^
335 | * Fixed collocation matrix for unordered collocation sites. Endpoint now
336 | correctly taken into account.
337 |
338 | Version 0.1
339 | -----------
340 | | **Date:** May 10, 2019
341 | | **Release:** v0.1
342 | | **Version of CHAOS:** CHAOS-6-x9
343 |
344 | Features
345 | ^^^^^^^^
346 | * New CHAOS class method :meth:`chaosmagpy.chaos.CHAOS.synth_euler_angles` to
347 | compute Euler angles for the satellites from the CHAOS model (used to rotate
348 | vectors from magnetometer frame to the satellite frame).
349 | * Added CHAOS class methods :meth:`chaosmagpy.chaos.CHAOS.synth_values_tdep`,
350 | :meth:`chaosmagpy.chaos.CHAOS.synth_values_static`,
351 | :meth:`chaosmagpy.chaos.CHAOS.synth_values_gsm` and
352 | :meth:`chaosmagpy.chaos.CHAOS.synth_values_sm` for field value computation.
353 | * RC index file now stored in HDF5 format.
354 | * Filepaths and other parameters are now handled by a configuration dictionary
355 | called ``chaosmagpy.basicConfig``.
356 | * Added extrapolation keyword to the BaseModel class
357 | :meth:`chaosmagpy.chaos.Base.synth_coeffs`, linear by default.
358 | * :func:`chaosmagpy.data_utils.mjd2000` now also accepts datetime class
359 | instances.
360 | * :func:`chaosmagpy.data_utils.load_RC_datfile` downloads latest RC-index file
361 | from the website if no file is given.
362 |
363 | Bugfixes
364 | ^^^^^^^^
365 | * Resolved issue in :func:`chaosmagpy.model_utils.degree_correlation`.
366 | * Changed the date conversion to include hours and seconds not just the day
367 | when plotting the timeseries.
368 |
369 | Version 0.1a3
370 | -------------
371 | | **Date:** February 19, 2019
372 | | **Release:** v0.1a3
373 |
374 | Features
375 | ^^^^^^^^
376 | * New CHAOS class method :meth:`chaosmagpy.chaos.CHAOS.save_matfile` to output
377 | MATLAB compatible files of the CHAOS model (using the ``hdf5storage``
378 | package).
379 | * Added ``epoch`` keyword to basevector input arguments of GSM, SM and MAG
380 | coordinate systems.
381 |
382 | Bugfixes
383 | ^^^^^^^^
384 | * Fixed problem of the setup configuration for ``pip`` which caused importing
385 | the package to fail although installation was indicated as successful.
386 |
387 | Version 0.1a2
388 | -------------
389 | | **Date:** January 26, 2019
390 | | **Release:** v0.1a2
391 |
392 | Features
393 | ^^^^^^^^
394 | * :func:`chaosmagpy.data_utils.mjd_to_dyear` and
395 | :func:`chaosmagpy.data_utils.dyear_to_mjd` convert time with microseconds
396 | precision to prevent round-off errors in seconds.
397 | * Time conversion now uses built-in ``calendar`` module to identify leap year.
398 |
399 | Bugfixes
400 | ^^^^^^^^
401 | * Fixed wrong package requirement that caused the installation of
402 | ChaosMagPy v0.1a1 to fail with ``pip``. If installation of v0.1a1 is needed,
403 | use ``pip install --no-deps chaosmagpy==0.1a1`` to ignore faulty
404 | requirements.
405 |
406 | Version 0.1a1
407 | -------------
408 | | **Date:** January 5, 2019
409 | | **Release:** v0.1a1
410 |
411 | Features
412 | ^^^^^^^^
413 | * Package now supports Matplotlib v3 and Cartopy v0.17.
414 | * Loading shc-file now converts decimal year to ``mjd2000`` taking leap years
415 | into account by default.
416 | * Moved ``mjd2000`` from ``coordinate_utils`` to ``data_utils``.
417 | * Added function to compute degree correlation.
418 | * Added functions to compute and plot the power spectrum.
419 | * Added flexibility to the function synth_values: now supports NumPy
420 | broadcasting rules.
421 | * Fixed CHAOS class method synth_coeffs_sm default source parameter: now
422 | defaults to ``'external'``.
423 |
424 | Deprecations
425 | ^^^^^^^^^^^^
426 | * Optional argument ``source`` when saving shc-file has been renamed to
427 | ``model``.
428 | * ``plot_external_map`` has been renamed to ``plot_maps_external``
429 | * ``synth_sm_field`` has been renamed to ``synth_coeffs_sm``
430 | * ``synth_gsm_field`` has been renamed to ``synth_coeffs_gsm``
431 | * ``plot_static_map`` has been renamed to ``plot_maps_static``
432 | * ``synth_static_field`` has been renamed to ``synth_coeffs_static``
433 | * ``plot_tdep_maps`` has been renamed to ``plot_maps_tdep``
434 | * ``synth_tdep_field`` has been renamed to ``synth_coeffs_tdep``
435 |
436 | Version 0.1a0
437 | -------------
438 | | **Date:** October 13, 2018
439 | | **Release:** v0.1a0
440 |
441 | Initial release to the users for testing.
442 |
--------------------------------------------------------------------------------
/CITATION:
--------------------------------------------------------------------------------
1 | Citation
2 | --------
3 |
4 | To reference ChaosMagPy in publications, please cite the package itself
5 |
6 | https://doi.org/10.5281/zenodo.3352398
7 |
8 | and, for CHAOS-7, the relevant journal publication:
9 |
10 | Finlay, C.C., Kloss, C., Olsen, N., Hammer, M. Toeffner-Clausen, L.,
11 | Grayver, A and Kuvshinov, A. (2020), The CHAOS-7 geomagnetic field model and
12 | observed changes in the South Atlantic Anomaly, Earth Planets and Space 72,
13 | doi:10.1186/s40623-020-01252-9
14 |
15 | or, for the earlier CHAOS models, some of the following:
16 |
17 | Finlay, C.C., Olsen, N., Kotsiaros, S., Gillet, N. and Toeffner-Clausen, L. (2016),
18 | Recent geomagnetic secular variation from Swarm and ground observatories
19 | as estimated in the CHAOS-6 geomagnetic field model Earth Planets Space,
20 | Vol 68, 112. doi: 10.1186/s40623-016-0486-1
21 |
22 | Olsen, N., Luehr, H., Finlay, C.C., Sabaka, T. J., Michaelis, I., Rauberg, J. and Toeffner-Clausen, L. (2014),
23 | The CHAOS-4 geomagnetic field model, Geophys. J. Int., Vol 197, 815-827,
24 | doi: 10.1093/gji/ggu033.
25 |
26 | Olsen, N., Luehr, H., Sabaka, T.J., Mandea, M. ,Rother, M., Toeffner-Clausen, L. and Choi, S. (2006),
27 | CHAOS — a model of Earth's magnetic field derived from CHAMP, Ørsted, and SAC-C magnetic satellite data,
28 | Geophys. J. Int., vol. 166 67-75
29 |
--------------------------------------------------------------------------------
/INSTALL.rst:
--------------------------------------------------------------------------------
1 | Installation
2 | ============
3 |
4 | ChaosMagPy relies on the following (some are optional):
5 |
6 | * python>=3.6
7 | * numpy>=2
8 | * scipy
9 | * pandas
10 | * cython
11 | * h5py
12 | * hdf5storage>=0.2 (for compatibility with numpy v2.0)
13 | * pyshp>=2.3.1
14 | * matplotlib>=3.6 (optional, used for plotting)
15 | * lxml (optional, used for downloading latest RC-index file)
16 |
17 | Specific installation steps using the conda/pip package managers are as follows:
18 |
19 | 1. Install packages with conda:
20 |
21 | >>> conda install python "numpy>=2" scipy pandas cython pyshp h5py matplotlib lxml
22 |
23 | 2. Install remaining packages with pip:
24 |
25 | >>> pip install "hdf5storage>=0.2"
26 |
27 | 3. Finally install ChaosMagPy either with pip from PyPI:
28 |
29 | >>> pip install chaosmagpy
30 |
31 | Or, if you have downloaded the distribution archives from the Python Package
32 | Index (PyPI) at https://pypi.org/project/chaosmagpy/#files, install
33 | ChaosMagPy using the built distribution:
34 |
35 | >>> pip install chaosmagpy-x.x-py3-none-any.whl
36 |
37 | replacing ``x.x`` with the relevant version, or using the source
38 | distribution:
39 |
40 | >>> pip install chaosmagpy-x.x.tar.gz
41 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | License
3 | =======
4 |
5 | All ChaosMagPy source code and documentation is Copyright (C) Clemens Kloss
6 | and, unless explicitly stated, licensed under the MIT license.
7 |
8 | The exception is with regard to some of the stand-alone example scripts in the
9 | ChaosMagPy documentation gallery, which are licensed under the GNU Lesser
10 | General Public License as published by the Free Software Foundation, either
11 | version 3 of the License, or (at your option) any later version. This is
12 | explicitly stated in the header of these example scripts.
13 |
14 | MIT License
15 | -----------
16 |
17 | Copyright (C) 2024 Clemens Kloss.
18 |
19 | Permission is hereby granted, free of charge, to any person obtaining a copy
20 | of this software and associated documentation files (the "Software"), to deal
21 | in the Software without restriction, including without limitation the rights
22 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
23 | copies of the Software, and to permit persons to whom the Software is
24 | furnished to do so, subject to the following conditions:
25 |
26 | The above copyright notice and this permission notice shall be included in all
27 | copies or substantial portions of the Software.
28 |
29 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
30 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
31 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
32 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
33 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
34 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
35 | SOFTWARE.
36 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 |
2 | Overview
3 | ========
4 |
5 | ChaosMagPy is a simple Python package for evaluating the CHAOS geomagnetic
6 | field model and other models of Earth's magnetic field. The latest CHAOS model
7 | is available at http://www.spacecenter.dk/files/magnetic-models/CHAOS-7/. To
8 | quickly get started, download the complete working example including the latest
9 | model under the "Forward code" section.
10 |
11 | Documentation
12 | -------------
13 |
14 | The documentation of the current release is available on Read the Docs
15 | (https://chaosmagpy.readthedocs.io/en/)
16 |
17 | |pypi| |docs| |doi| |license|
18 |
19 | .. |pypi| image:: https://badge.fury.io/py/chaosmagpy.svg
20 | :target: https://badge.fury.io/py/chaosmagpy/
21 |
22 | .. |docs| image:: https://readthedocs.org/projects/chaosmagpy/badge/
23 | :target: https://chaosmagpy.readthedocs.io/en/
24 | :alt: Documentation Status
25 |
26 | .. |license| image:: https://img.shields.io/badge/License-MIT-blue.svg
27 | :target: license.html
28 |
29 | .. |doi| image:: https://zenodo.org/badge/DOI/10.5281/zenodo.3352398.svg
30 | :target: https://doi.org/10.5281/zenodo.3352398
31 |
--------------------------------------------------------------------------------
/build_package.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | read -e -p "Path to the recent CHAOS mat-file: " chaos
4 | read -e -p "Version of CHAOS [e.g.: 0706]: " vchaos
5 |
6 | # extract from __init__.py on line with __version__ the expr between ""
7 | version=$(grep __version__ chaosmagpy/__init__.py | sed 's/.*"\(.*\)".*/\1/')
8 | out=chaosmagpy_package_"$version"_"$vchaos".zip
9 |
10 | echo -e "\n------- ChaosMagPy Version $version / CHAOS Version $vchaos -------\n"
11 | echo "Building package with CHAOS-matfile in '$chaos'."
12 |
13 | while true; do
14 | read -p "Do you wish to continue building v$version (y/n)?: " yn
15 | case $yn in
16 | [Yy]* ) break;;
17 | [Nn]* ) echo Exit.; exit;;
18 | * ) echo "Please answer yes or no.";;
19 | esac
20 | done
21 |
22 | # delete old dist files if exist
23 | rm -rf chaosmagpy.egg-info
24 |
25 | # create source distribution and built distribution
26 | python -m build --sdist --wheel
27 |
28 | # clean build and compile documentary as html
29 | make --directory ./docs clean html
30 |
31 | # make temporary directory
32 | tempdir=$(mktemp -t -d XXXXXX)
33 | echo "Creating temporary directory '$tempdir'"
34 | mkdir $tempdir/data $tempdir/html
35 |
36 | # copy files to tmp directory
37 | cp dist/chaosmagpy-$version.tar.gz $tempdir/.
38 | cp chaos_examples.py $tempdir/.
39 | cp $chaos $tempdir/data/.
40 |
41 | # run example
42 | cd $tempdir/
43 | python $tempdir/chaos_examples.py
44 | cat example1_output.txt # print example file output for checking
45 | cd -
46 |
47 | cp data/SW_OPER_MAGA_LR_1B_20180801T000000_20180801T235959_PT15S.cdf $tempdir/data/SW_OPER_MAGA_LR_1B_20180801T000000_20180801T235959_PT15S.cdf
48 | cp -r docs/build/html/* $tempdir/html/
49 |
50 | # create readme.txt and write introduction (without citation rst link)
51 | cat > $tempdir/readme.txt <(echo) README.rst
52 |
53 | # create readme.txt and write introduction
54 | cat >> $tempdir/readme.txt <(echo) CITATION
55 |
56 | # include License
57 | cat >> $tempdir/readme.txt <(echo) LICENSE
58 |
59 | # include installation instructions
60 | cat >> $tempdir/readme.txt <(echo) INSTALL.rst
61 |
62 | # append explanations of package's contents
63 | cat >> $tempdir/readme.txt << EOF
64 |
65 | Contents
66 | ========
67 |
68 | The directory contains the files/directories:
69 |
70 | 1. "chaosmagpy-x.x*.tar.gz": pip installable archive of the chaosmagpy package
71 | (version x.x*)
72 |
73 | 2. "chaos_examples.py": executable Python script containing several examples
74 | that can be run by changing the examples in line 16, save and run in the
75 | command line:
76 |
77 | >>> python chaos_examples.py
78 |
79 | example 1: Calculate CHAOS model field predictions from input coordinates
80 | and time and output simple data file
81 | example 2: Calculate and plot residuals between CHAOS model and
82 | Swarm A data (from L1b MAG cdf data file, example from May 2014).
83 | example 3: Calculate core field and its time derivatives for specified times
84 | and radii and plot maps
85 | example 4: Calculate static (i.e. small-scale crustal) magnetic field and
86 | plot maps (may take a few seconds)
87 | example 5: Calculate timeseries of the magnetic field at a ground
88 | observatory and plot
89 | example 6: Calculate external and associated induced fields described in SM
90 | and GSM reference systems and plot maps
91 |
92 | 3. "data/CHAOS-x.x.mat": mat-file containing the CHAOS model version x.x.
93 |
94 | 4. "SW_OPER_MAGA_LR_1B_20180801T000000_20180801T235959_PT15S.cdf":
95 | cdf-file containing Swarm-A magnetic field data from August 1, 2018.
96 |
97 | 5. directory called "html" containing the built documentation as
98 | html-files. Open "index.html" in your browser to access the main site.
99 |
100 |
101 | Clemens Kloss (ancklo@space.dtu.dk)
102 |
103 | EOF
104 |
105 | # include changelog
106 | cat >> $tempdir/readme.txt <(echo) CHANGELOG.rst
107 |
108 | # build archive recursively in tmp directory
109 | cd $tempdir/
110 | zip $out -r *
111 | cd -
112 |
113 | # move archive to build
114 | mv -i $tempdir/$out build/$out
115 |
116 | # clean up
117 | while true; do
118 | read -p "Do you wish to delete temporary files in '$tempdir' (y/n)?: " yn
119 | case $yn in
120 | [Yy]* ) rm -r $tempdir; break;;
121 | [Nn]* ) echo Exit.; exit;;
122 | * ) echo "Please answer yes or no.";;
123 | esac
124 | done
125 |
126 | cat << EOF
127 | -------------------------------------------------------------
128 | Check that the produced package files have been updated:
129 | EOF
130 |
131 | ls -lrt build | grep --color=auto chaosmagpy_package_$version
132 | ls -lrt dist | grep --color=auto chaosmagpy-$version
133 |
134 | cat << EOF
135 | -------------------------------------------------------------
136 | Check that the correct RC-index file has been included.
137 | Check that the CHAOS version in basicConfig was built with the included RC-index file.
138 | Check that example_script.py output agrees with MATLAB example output.
139 | Check the copyright notice and update the year if needed.
140 | Check changelog dates and entries.
141 | Check urls in the config file of the documentation.
142 | Check the readme file in the root of the repository.
143 | -------------------------------------------------------------
144 | EOF
145 |
146 | # upload to PyPI (requires access token in .pypirc)
147 | # twine check dist/chaosmagpy-$version*
148 | # twine upload --repository chaosmagpy dist/chaosmagpy-$version*
149 |
--------------------------------------------------------------------------------
/chaos_examples.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2024 Clemens Kloss
2 | #
3 | # This file is part of ChaosMagPy.
4 | #
5 | # ChaosMagPy is released under the MIT license. See LICENSE in the root of the
6 | # repository for full licensing details.
7 |
8 | import numpy as np
9 | import glob
10 | import matplotlib.pyplot as plt
11 | from chaosmagpy import load_CHAOS_matfile
12 | from chaosmagpy.coordinate_utils import transform_points
13 | from chaosmagpy.data_utils import mjd2000
14 |
15 | FILEPATH_CHAOS = glob.glob('data/CHAOS-*.mat')[0]
16 |
17 | R_REF = 6371.2
18 |
19 |
20 | def main():
21 | """
22 | Run examples by uncommenting lines below.
23 | """
24 |
25 | example1()
26 | # example2()
27 | # example3()
28 | # example4()
29 | # example5()
30 | # example6()
31 |
32 |
33 | def example1():
34 |
35 | # give inputs
36 | theta = np.array([55.676, 51.507, 64.133]) # colat in deg
37 | phi = np.array([12.568, 0.1275, -21.933]) # longitude in deg
38 | radius = np.array([0.0, 0.0, 500.0]) + R_REF # radius from altitude in km
39 | time = np.array([3652.0, 5113.0, 5287.5]) # time in mjd2000
40 |
41 | # load the CHAOS model
42 | model = load_CHAOS_matfile(FILEPATH_CHAOS)
43 | print(model)
44 |
45 | print('Computing core field.')
46 | B_core = model.synth_values_tdep(time, radius, theta, phi)
47 |
48 | print('Computing crustal field up to degree 110.')
49 | B_crust = model.synth_values_static(radius, theta, phi, nmax=110)
50 |
51 | # complete internal contribution
52 | B_radius_int = B_core[0] + B_crust[0]
53 | B_theta_int = B_core[1] + B_crust[1]
54 | B_phi_int = B_core[2] + B_crust[2]
55 |
56 | print('Computing field due to external sources, incl. induced field: GSM.')
57 | B_gsm = model.synth_values_gsm(time, radius, theta, phi, source='all')
58 |
59 | print('Computing field due to external sources, incl. induced field: SM.')
60 | B_sm = model.synth_values_sm(time, radius, theta, phi, source='all')
61 |
62 | # complete external field contribution
63 | B_radius_ext = B_gsm[0] + B_sm[0]
64 | B_theta_ext = B_gsm[1] + B_sm[1]
65 | B_phi_ext = B_gsm[2] + B_sm[2]
66 |
67 | # complete forward computation
68 | B_radius = B_radius_int + B_radius_ext
69 | B_theta = B_theta_int + B_theta_ext
70 | B_phi = B_phi_int + B_phi_ext
71 |
72 | # save to output file
73 | data_CHAOS = np.stack([
74 | time, radius, theta, phi, B_radius, B_theta, B_phi,
75 | B_radius_int, B_theta_int, B_phi_int, B_radius_ext,
76 | B_theta_ext, B_phi_ext
77 | ], axis=-1)
78 |
79 | header = (' t (mjd2000) r (km) theta (deg) phi (deg) B_r '
80 | 'B_theta B_phi B_r B_theta B_phi B_r '
81 | 'B_theta B_phi\n'
82 | ' '
83 | 'model total model internal '
84 | 'model external')
85 | np.savetxt('example1_output.txt', data_CHAOS, delimiter=' ', header=header,
86 | fmt=['%15.8f', '%9.3f', '%11.5f', '%11.5f'] + 9*['%9.2f'])
87 |
88 | print('Saved output to example1_output.txt.')
89 |
90 |
91 | def example2():
92 | """
93 | Plot difference between modelled and observed field strength using Swarm A
94 | data in August 2018 from a cdf-file.
95 |
96 | """
97 | import cdflib
98 |
99 | model = load_CHAOS_matfile(FILEPATH_CHAOS)
100 | print(model)
101 |
102 | cdf_file = cdflib.CDF('data/SW_OPER_MAGA_LR_1B_'
103 | '20180801T000000_20180801T235959'
104 | '_PT15S.cdf', 'r')
105 | # print(cdf_file.cdf_info()) # print cdf info/contents
106 |
107 | radius = cdf_file.varget('Radius') / 1000 # km
108 | theta = 90. - cdf_file.varget('Latitude') # colat deg
109 | phi = cdf_file.varget('Longitude') # deg
110 | time = cdf_file.varget('Timestamp') # milli seconds since year 1
111 | time = time / (1e3*3600*24) - 730485 # time in modified Julian date 2000
112 | F_swarm = cdf_file.varget('F')
113 |
114 | theta_gsm, phi_gsm = transform_points(
115 | theta, phi, time=time, reference='gsm')
116 | index_day = np.logical_and(phi_gsm < 90, phi_gsm > -90)
117 | index_night = np.logical_not(index_day)
118 |
119 | # complete forward computation: pre-built not customizable (see ex. 1)
120 | B_radius, B_theta, B_phi = model(time, radius, theta, phi)
121 |
122 | # compute field strength and plot together with data
123 | F = np.sqrt(B_radius**2 + B_theta**2 + B_phi**2)
124 |
125 | print('RMSE of F: {:.5f} nT'.format(np.std(F-F_swarm)))
126 |
127 | plt.scatter(theta_gsm[index_day], F_swarm[index_day]-F[index_day],
128 | s=0.5, c='r', label='dayside')
129 | plt.scatter(theta_gsm[index_night], F_swarm[index_night]-F[index_night],
130 | s=0.5, c='b', label='nightside')
131 | plt.xlabel('dipole colatitude ($^\\circ$)')
132 | plt.ylabel('$\\mathrm{d} F$ (nT)')
133 | plt.legend(loc=2)
134 | plt.show()
135 |
136 |
137 | def example3():
138 | """
139 | Plot maps of core field and its derivatives for different times and
140 | radii.
141 |
142 | """
143 |
144 | model = load_CHAOS_matfile(FILEPATH_CHAOS)
145 |
146 | radius = 0.53*R_REF # radial distance in km of core-mantle boundary
147 | time = mjd2000(2015, 9, 1) # year, month, day
148 |
149 | model.plot_maps_tdep(time, radius, nmax=16, deriv=1)
150 |
151 |
152 | def example4():
153 | """
154 | Plot maps of static (i.e. small-scale crustal) magnetic field.
155 |
156 | """
157 |
158 | model = load_CHAOS_matfile(FILEPATH_CHAOS)
159 |
160 | radius = R_REF
161 |
162 | model.plot_maps_static(radius, nmax=85)
163 |
164 |
165 | def example5():
166 | """
167 | Plot timeseries of the magnetic field at the ground observatory in MBour
168 | MBO (lat: 75.62°, east lon: 343.03°).
169 |
170 | """
171 |
172 | model = load_CHAOS_matfile(FILEPATH_CHAOS)
173 |
174 | # observatory location
175 | radius = 6.376832e+03
176 | theta = 75.69200
177 | phi = 343.05000
178 |
179 | model.plot_timeseries_tdep(radius, theta, phi, nmax=16, deriv=1)
180 |
181 |
182 | def example6():
183 | """
184 | Plot maps of external and internal sources described in SM and GSM
185 | reference systems.
186 |
187 | """
188 |
189 | model = load_CHAOS_matfile(FILEPATH_CHAOS)
190 |
191 | radius = R_REF + 450
192 | time = mjd2000(2015, 9, 1, 12)
193 |
194 | model.plot_maps_external(time, radius, reference='all', source='all')
195 |
196 |
197 | if __name__ == '__main__':
198 |
199 | main()
200 |
--------------------------------------------------------------------------------
/chaosmagpy/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2024 Clemens Kloss
2 | #
3 | # This file is part of ChaosMagPy.
4 | #
5 | # ChaosMagPy is released under the MIT license. See LICENSE in the root of the
6 | # repository for full licensing details.
7 |
8 | """
9 | ChaosMagPy is a simple Python package for evaluating the CHAOS geomagnetic
10 | field model and other models of Earth's magnetic field. The latest CHAOS model
11 | is available at http://www.spacecenter.dk/files/magnetic-models/CHAOS-7/.
12 |
13 | >>> import chaosmagpy as cp
14 | >>> chaos = cp.load_CHAOS_matfile('CHAOS-7.mat')
15 |
16 | The following modules are available:
17 |
18 | * chaosmagpy.chaos (functions for loading well-known field models)
19 | * chaosmagpy.coordinate_utils (tools for transforming coordinates/components)
20 | * chaosmagpy.model_utils (tools for evaluating field models)
21 | * chaosmagpy.plot_utils (tools for plotting)
22 | * chaosmagpy.data_utils (tools for reading/writing data files)
23 | * chaosmagpy.config_utils (tools for customizing ChaosMagPy parameters)
24 |
25 | More information on how to use ChaosMagPy can be found in the documentation
26 | available at https://chaosmagpy.readthedocs.io/en/.
27 |
28 | """
29 |
30 | __all__ = [
31 | "CHAOS", "load_CHAOS_matfile", "load_CHAOS_shcfile",
32 | "load_CovObs_txtfile", "load_gufm1_txtfile", "load_CALS7K_txtfile",
33 | "load_IGRF_txtfile", "basicConfig", "synth_values", "mjd2000",
34 | "timestamp", "dyear_to_mjd", "mjd_to_dyear"
35 | ]
36 |
37 | __version__ = "0.14"
38 |
39 | from .chaos import (
40 | CHAOS,
41 | load_CHAOS_matfile,
42 | load_CHAOS_shcfile,
43 | load_CovObs_txtfile,
44 | load_gufm1_txtfile,
45 | load_CALS7K_txtfile,
46 | load_IGRF_txtfile
47 | )
48 |
49 | from .config_utils import basicConfig
50 | from .model_utils import synth_values
51 | from .data_utils import mjd2000, timestamp, dyear_to_mjd, mjd_to_dyear
52 |
--------------------------------------------------------------------------------
/chaosmagpy/config_utils.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2024 Clemens Kloss
2 | #
3 | # This file is part of ChaosMagPy.
4 | #
5 | # ChaosMagPy is released under the MIT license. See LICENSE in the root of the
6 | # repository for full licensing details.
7 |
8 | """
9 | `chaosmagpy.config_utils` contains functions and classes to manipulate the
10 | configuration dictionary, which contains parameters and options in ChaosMagPy.
11 | The following list gives an overview of the possible keywords. The keywords can
12 | be accessed after importing chaosmagpy through:
13 |
14 | >>> import chaosmagpy as cp
15 | >>> cp.basicConfig['params.r_surf'] # get Earth's mean radius in km
16 | 6371.2
17 |
18 | **Parameters**
19 |
20 | ====================== ============= =======================================
21 | Value Type Description
22 | ====================== ============= =======================================
23 | 'params.r_surf' `float` Reference radius in kilometers
24 | (defaults to Earth's surface radius of
25 | 6371.2 km).
26 | 'params.r_cmb' `float` Core-mantle boundary radius in
27 | kilometers (defaults to 3485.0 km).
28 | 'params.dipole' `list`, Coefficients of the dipole (used for
29 | `ndarray`, GSM/SM coordinate transformations) in
30 | `shape (3,)` units of nanotesla.
31 | 'params.ellipsoid' `list`, Equatorial (index 0) and polar radius
32 | `ndarray` (index 1) of the spheroid describing
33 | `shape (2,)` Earth (WGS84) in units of kilometers.
34 | 'params.CHAOS_version' `str` Version of the CHAOS model that was
35 | constructed with the built-in RC-index.
36 | 'params.cdf_to_mjd' `int` Number of days on Jan 01, 2000 since
37 | Jan 01, 0000 (CDF start epoch)
38 | ====================== ============= =======================================
39 |
40 | **Files**
41 |
42 | ========================== ============ ====================================
43 | Value Type Description
44 | ========================== ============ ====================================
45 | 'file.RC_index' `HDF5-file`, RC-index file (used for external
46 | `TXT-file` field computation). See also
47 | :func:`data_utils.save_RC_h5file`.
48 | 'file.GSM_spectrum' `NPZ-file` GSM transformation coefficients. See
49 | also :func:`coordinate_utils.\\
50 | rotate_gauss_fft`.
51 | 'file.SM_spectrum' `NPZ-file` SM transformation coefficients. See
52 | also :func:`coordinate_utils.\\
53 | rotate_gauss_fft`.
54 | 'file.Earth_conductivity' `TXT-file` Conductivity model of a layered
55 | Earth (deprecated).
56 | 'file.shp_coastline' `SHP-file`, Coastline shapefile (default file
57 | `ZIP-file` provided by Natural Earth).
58 | ========================== ============ ====================================
59 |
60 | **Plots**
61 |
62 | ========================== =========== =====================================
63 | Value Type Description
64 | ========================== =========== =====================================
65 | 'plots.figure_width' `float` Plot width in inches (defaults to 6.3
66 | inches or 16 cm)
67 | ========================== =========== =====================================
68 |
69 | .. autosummary::
70 | :toctree: classes
71 | :template: myclass.rst
72 |
73 | BasicConfig
74 |
75 | """
76 |
77 | import os
78 | import re
79 | import json
80 | import numpy as np
81 | import warnings
82 | from contextlib import contextmanager
83 |
84 | ROOT = os.path.abspath(os.path.dirname(__file__))
85 | LIB = os.path.join(ROOT, 'lib')
86 |
87 |
88 | # copied/inspired by matplotlib.rcsetup
89 | def check_path_exists(s):
90 | """Check that path to file exists."""
91 | if s is None or s == 'None':
92 | return None
93 | if os.path.exists(s):
94 | return s
95 | else:
96 | raise FileNotFoundError(f'{s} does not exist.')
97 |
98 |
99 | def check_float(s):
100 | """Convert to float."""
101 | try:
102 | return float(s)
103 | except ValueError:
104 | raise ValueError(f'Could not convert {s} to float.')
105 |
106 |
107 | def check_int(s):
108 | """Convert to integer."""
109 | try:
110 | return int(s)
111 | except ValueError:
112 | raise ValueError(f'Could not convert {s} to integer.')
113 |
114 |
115 | def check_string(s):
116 | """Convert to string."""
117 | try:
118 | return str(s)
119 | except ValueError:
120 | raise ValueError(f'Could not convert {s} to string.')
121 |
122 |
123 | def check_vector(s, len=None):
124 | """Check that input is vector of required length."""
125 | try:
126 | s = np.array(s)
127 | assert s.ndim == 1
128 | if len is not None:
129 | if s.size != len:
130 | raise ValueError(f'Wrong length: {s.size} != {len}.')
131 | return s
132 | except Exception as err:
133 | raise ValueError(f'Not a valid vector. {err}')
134 |
135 |
136 | def check_version_string(s):
137 | """Check correct format of version string."""
138 |
139 | s = check_string(s)
140 |
141 | match = re.search(r'\d+\.\d+', s)
142 | if match:
143 | return s
144 | else:
145 | raise ValueError(f'Not supported version format "{s}".'
146 | 'Must be of the form "x.x" with x an integer.')
147 |
148 |
149 | DEFAULTS = {
150 | 'params.r_surf': [6371.2, check_float],
151 | 'params.r_cmb': [3485.0, check_float],
152 | 'params.dipole': [np.array([-29442.0, -1501.0, 4797.1]),
153 | lambda x: check_vector(x, len=3)],
154 | 'params.ellipsoid': [np.array([6378.137, 6356.752]),
155 | lambda x: check_vector(x, len=2)],
156 | 'params.CHAOS_version': ['7.18', check_version_string],
157 | 'params.cdf_to_mjd': [730485, check_int],
158 |
159 | # location of coefficient files
160 | 'file.RC_index': [os.path.join(LIB, 'RC_index.h5'),
161 | check_path_exists],
162 | 'file.GSM_spectrum': [os.path.join(LIB, 'frequency_spectrum_gsm.npz'),
163 | check_path_exists],
164 | 'file.SM_spectrum': [os.path.join(LIB, 'frequency_spectrum_sm.npz'),
165 | check_path_exists],
166 | 'file.Earth_conductivity': [os.path.join(LIB, 'Earth_conductivity.dat'),
167 | check_path_exists],
168 | 'file.shp_coastline': [os.path.join(LIB, 'ne_110m_coastline.zip'),
169 | check_path_exists],
170 |
171 | # plot related configuration
172 | 'plots.figure_width': [6.3, check_float],
173 | }
174 |
175 |
176 | class BasicConfig(dict):
177 | """Class for creating CHAOS configuration dictionary."""
178 |
179 | defaults = DEFAULTS
180 |
181 | def __init__(self, *args, **kwargs):
182 | super().update(*args, **kwargs)
183 |
184 | def __setitem__(self, key, value):
185 | """Set and check value before updating dictionary."""
186 |
187 | try:
188 | try:
189 | cval = self.defaults[key][1](value)
190 | except ValueError as err:
191 | raise ValueError(f'Key "{key}": {err}')
192 | super().__setitem__(key, cval)
193 | except KeyError:
194 | raise KeyError(f'"{key}" is not a valid parameter.')
195 |
196 | def __str__(self):
197 | return '\n'.join(map('{0[0]}: {0[1]}'.format, sorted(self.items())))
198 |
199 | def reset(self, key):
200 | """
201 | Load default values.
202 |
203 | Parameters
204 | ----------
205 | key : str
206 | Single keyword that is reset to the default.
207 |
208 | """
209 | self.__setitem__(key, self.defaults[key][0])
210 |
211 | def fullreset(self):
212 | """
213 | Load all default values.
214 |
215 | """
216 | super().update({key: val for key, (val, _) in self.defaults.items()})
217 |
218 | def load(self, filepath):
219 | """
220 | Load configuration dictionary from file.
221 |
222 | Parameters
223 | ----------
224 | filepath : str
225 | Filepath and name to json-formatted configuration txt-file.
226 |
227 | """
228 |
229 | with open(filepath, 'r') as f:
230 | kwargs = json.load(f)
231 |
232 | if len(kwargs) == 0:
233 | warnings.warn(
234 | 'Configuration dictionary loaded from file is empty.')
235 |
236 | for key, value in kwargs.items():
237 | # check format and set key value pairs
238 | self.__setitem__(key, value)
239 |
240 | def save(self, filepath):
241 | """
242 | Save configuration dictionary to a file.
243 |
244 | Parameters
245 | ----------
246 | filepath : str
247 | Filepath and name of the textfile that will be saved with the
248 | configuration values.
249 |
250 | """
251 |
252 | def default(obj):
253 | if isinstance(obj, np.ndarray):
254 | return obj.tolist()
255 |
256 | with open(filepath, 'w') as f:
257 | json.dump(self, f, default=default, indent=4, sort_keys=True)
258 |
259 | print(f'Saved configuration textfile to {filepath}.')
260 |
261 | @contextmanager
262 | def context(self, key, value):
263 | """
264 | Use context manager to temporarily change setting.
265 |
266 | Parameters
267 | ----------
268 | key : str
269 | BasicConfig configuration key.
270 | value
271 | Value compatible with ``key``.
272 |
273 | Examples
274 | --------
275 | Temporarily change the radius of Earth's surface for a computation
276 | and then change it back to the original value.
277 |
278 | .. code-block:: python
279 |
280 | from chaosmagpy import basicConfig
281 |
282 | print('Before: ', basicConfig['params.r_surf'])
283 |
284 | # change Earth's radius to 10 km
285 | with basicConfig.context('params.r_surf', 10):
286 | # do something at r_surf = 10 km ...
287 | print('Inside: ', basicConfig['params.r_surf'])
288 |
289 | print('After: ', basicConfig['params.r_surf'])
290 |
291 | """
292 | old_value = self.__getitem__(key)
293 | self.__setitem__(key, value)
294 | yield
295 | self.__setitem__(key, old_value)
296 |
297 |
298 | # load defaults
299 | basicConfig = BasicConfig({key: val for key, (val, _) in DEFAULTS.items()})
300 |
301 |
302 | if __name__ == '__main__':
303 | # ensure default passes tests
304 | for key, (value, test) in DEFAULTS.items():
305 | if not np.all(test(value) == value):
306 | print(f"{key}: {test(value)} != {value}")
307 |
--------------------------------------------------------------------------------
/chaosmagpy/data_utils.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2024 Clemens Kloss
2 | #
3 | # This file is part of ChaosMagPy.
4 | #
5 | # ChaosMagPy is released under the MIT license. See LICENSE in the root of the
6 | # repository for full licensing details.
7 |
8 | """
9 | `chaosmagpy.data_utils` provides functions for loading and writing data and
10 | geomagnetic field models. It also offers functions to do common time
11 | conversions.
12 |
13 | .. autosummary::
14 | :toctree: functions
15 |
16 | load_matfile
17 | load_RC_datfile
18 | save_RC_h5file
19 | load_shcfile
20 | save_shcfile
21 | mjd2000
22 | timestamp
23 | is_leap_year
24 | dyear_to_mjd
25 | mjd_to_dyear
26 | memory_usage
27 | gauss_units
28 |
29 | """
30 |
31 |
32 | import pandas as pd
33 | import numpy as np
34 | import hdf5storage as hdf
35 | import warnings
36 | import h5py
37 | import os
38 | import datetime as dt
39 | import textwrap
40 |
41 |
42 | def load_matfile(filepath, variable_names=None, **kwargs):
43 | """
44 | Load MAT-file and return dictionary.
45 |
46 | Function loads MAT-file by traversing the structure converting data into
47 | low-level numpy arrays of different types. There is no guarantee that any
48 | kind of data is read in correctly. The data dtype can also vary depending
49 | on the MAT-file (v7.3 returns floats instead of integers). But it should
50 | work identically for v7.3 and prior MAT-files. Arrays are squeezed if
51 | possible. Relies on the :mod:`hdf5storage` package.
52 |
53 | Parameters
54 | ----------
55 | filepath : str
56 | Filepath and name of MAT-file.
57 | variable_names : list of strings
58 | Top-level variables to be loaded.
59 | **kwargs : keywords
60 | Additional keyword arguments are passed to :func:`hdf5storage.loadmat`.
61 |
62 | Returns
63 | -------
64 | data : dict
65 | Dictionary containing the data as dictionaries or numpy arrays.
66 |
67 | """
68 |
69 | # define a recursively called function to traverse structure
70 | def traverse_struct(struct):
71 |
72 | # for dictionaries, iterate through keys
73 | if isinstance(struct, dict):
74 | out = dict()
75 | for key, value in struct.items():
76 | out[key] = traverse_struct(value)
77 | return out
78 |
79 | # for ndarray, iterate through dtype names
80 | elif isinstance(struct, np.ndarray):
81 |
82 | # collect dtype names if available
83 | names = struct.dtype.names
84 |
85 | # if no fields in array
86 | if names is None:
87 | if struct.dtype == np.dtype('O') and struct.shape == (1, 1):
88 | return traverse_struct(struct[0, 0])
89 | else:
90 | return struct.squeeze()
91 |
92 | else: # if there are fields, iterate through fields
93 | out = dict()
94 | for name in names:
95 | out[name] = traverse_struct(struct[name])
96 | return out
97 |
98 | else:
99 | return struct
100 |
101 | output = hdf.loadmat(filepath, variable_names=variable_names, **kwargs)
102 |
103 | # loadmat returns dictionary, go through keys and call traverse_struct
104 | for key, value in output.items():
105 | if key.startswith('__') and key.endswith('__'):
106 | pass
107 | else:
108 | output[key] = traverse_struct(value)
109 |
110 | return output
111 |
112 |
113 | def load_RC_datfile(filepath=None, parse_dates=None):
114 | """
115 | Load RC-index data file into pandas data frame.
116 |
117 | Parameters
118 | ----------
119 | filepath : str, optional
120 | Filepath to RC index ``*.dat``. If ``None``, the RC
121 | index will be fetched from `spacecenter.dk `_.
123 | parse_dates : bool, optional
124 | Replace index with datetime object for time-series manipulations.
125 | Default is ``False``.
126 |
127 | Returns
128 | -------
129 | df : dataframe
130 | Pandas dataframe with names {'time', 'RC', 'RC_e', 'RC_i', 'flag'},
131 | where ``'time'`` is given in modified Julian dates.
132 |
133 | """
134 |
135 | if filepath is None:
136 | from lxml import html
137 | import requests
138 |
139 | link = "http://www.spacecenter.dk/files/magnetic-models/RC/current/"
140 |
141 | page = requests.get(link)
142 | print(f'Accessing {page.url}.')
143 |
144 | tree = html.fromstring(page.content)
145 | file = tree.xpath('//tr[5]//td[2]//a/@href')[0] # get name from list
146 | date = tree.xpath('//tr[5]//td[3]/text()')[0]
147 |
148 | print(f'Downloading RC-index file "{file}" '
149 | f'(last modified on {date.strip()}).')
150 |
151 | filepath = link + file
152 |
153 | column_names = ['time', 'RC', 'RC_e', 'RC_i', 'flag']
154 | column_types = {'time': 'float64', 'RC': 'float64', 'RC_e': 'float64',
155 | 'RC_i': 'float64', 'flag': 'category'}
156 |
157 | df = pd.read_csv(filepath, sep=r'\s+', comment='#',
158 | dtype=column_types, names=column_names)
159 |
160 | parse_dates = False if parse_dates is None else parse_dates
161 |
162 | # set datetime as index
163 | if parse_dates:
164 | df.index = pd.to_datetime(
165 | df['time'].values, unit='D', origin=pd.Timestamp('2000-1-1'))
166 | df.drop(['time'], axis=1, inplace=True) # delete redundant time column
167 |
168 | return df
169 |
170 |
171 | def save_RC_h5file(filepath, read_from=None):
172 | """
173 | Return HDF5-file of the RC index.
174 |
175 | Parameters
176 | ----------
177 | filepath : str
178 | Filepath and name of ``*.h5`` output file.
179 | read_from : str, optional
180 | Filepath of RC index ``*.dat``. If ``None``, the RC
181 | index will be fetched from :rc_url:`spacecenter.dk <>`.
182 |
183 | Notes
184 | -----
185 | Saves an HDF5-file of the RC index with keywords
186 | ['time', 'RC', 'RC_e', 'RC_i', 'flag']. Time is given in modified Julian
187 | dates 2000.
188 |
189 | Examples
190 | --------
191 | Save RC-index TXT-file (``RC_1997-2020_Aug_v4.dat``) as file in HDF5 format
192 | (``RC_index.h5``).
193 |
194 | >>> save_RC_h5file('RC_index.h5', read_from='RC_1997-2020_Aug_v4.dat')
195 | Successfully saved to RC_index.h5.
196 |
197 | """
198 |
199 | try:
200 | df_rc = load_RC_datfile(read_from, parse_dates=False)
201 |
202 | with h5py.File(filepath, 'w') as f:
203 |
204 | for column in df_rc.columns:
205 | variable = df_rc[column].values
206 | if column == 'flag':
207 | dset = f.create_dataset(column, variable.shape, dtype="S1")
208 | dset[:] = variable.astype('bytes')
209 |
210 | else:
211 | f.create_dataset(column, data=variable) # just save floats
212 |
213 | print(f'Successfully saved to {f.filename}.')
214 |
215 | except Exception as err:
216 | warnings.warn(f"Can't save new RC index. Raised exception: '{err}'.")
217 |
218 |
219 | def load_shcfile(filepath, leap_year=None, comment=None):
220 | """
221 | Load SHC-file and return coefficient arrays.
222 |
223 | Parameters
224 | ----------
225 | filepath : str
226 | File path to spherical harmonic coefficient SHC-file.
227 | leap_year : {True, False}, optional
228 | Take leap year in time conversion into account (default). Otherwise,
229 | use conversion factor of 365.25 days per year.
230 | comment : str, optional
231 | Character at the start of a line to indicate a comment
232 | (defaults to ``#``). This can also be a tuple of characters.
233 |
234 | Returns
235 | -------
236 | time : ndarray, shape (N,)
237 | Array containing `N` times for each model snapshot in modified
238 | Julian dates with origin January 1, 2000 0:00 UTC.
239 | coeffs : ndarray, shape (nmax(nmax+2), N)
240 | Coefficients of model snapshots. Each column is a snapshot up to
241 | spherical degree and order `nmax`.
242 | parameters : dict, {'SHC', 'nmin', 'nmax', 'N', 'order', 'step'}
243 | Dictionary containing parameters of the model snapshots and the
244 | following keys: ``'SHC'`` SHC-file name, ``'nmin'`` minimum degree,
245 | ``'nmax'`` maximum degree, ``'N'`` number of snapshot models,
246 | ``'order'`` piecewise polynomial order and ``'step'`` number of
247 | snapshots until next break point. Extract break points of the
248 | piecewise polynomial with ``breaks = time[::step]``.
249 |
250 | """
251 |
252 | leap_year = True if leap_year is None else leap_year
253 | comment = '#' if comment is None else comment
254 |
255 | first_line = True
256 |
257 | with open(filepath, 'r') as f:
258 |
259 | data = np.array([])
260 | for line in f.readlines():
261 |
262 | if line.strip().startswith(comment):
263 | continue
264 |
265 | newline = np.fromstring(line, sep=' ')
266 |
267 | if first_line: # first non-comment line contains shc params
268 | name = os.path.split(filepath)[1] # file name string
269 | values = [name] + newline.astype(int).tolist()
270 |
271 | first_line = False
272 |
273 | else:
274 | data = np.append(data, newline)
275 |
276 | # unpack parameter line
277 | keys = ['SHC', 'nmin', 'nmax', 'N', 'order', 'step']
278 | parameters = dict(zip(keys, values))
279 |
280 | time = data[:parameters['N']]
281 | coeffs = data[parameters['N']:].reshape((-1, parameters['N']+2))
282 | coeffs = coeffs[:, 2:].copy() # discard columns with n and m
283 |
284 | mjd = dyear_to_mjd(time, leap_year=leap_year)
285 |
286 | return mjd, coeffs, parameters
287 |
288 |
289 | def save_shcfile(time, coeffs, order=None, filepath=None, nmin=None, nmax=None,
290 | leap_year=None, header=None):
291 | """
292 | Save Gauss coefficients as SHC-file.
293 |
294 | Parameters
295 | ----------
296 | time : float, list, ndarray, shape (n,)
297 | Time of model coeffcients in modified Julian date.
298 | coeffs : ndarray, shape (N,) or (n, N)
299 | Gauss coefficients as vector or array. The first dimension of the array
300 | must be equal to the length `n` of the given ``time``.
301 | order : int, optional (defaults to 1)
302 | Order of the piecewise polynomial with which the coefficients are
303 | parameterized in time (breaks are given by ``time[::order]``).
304 | filepath : str, optional
305 | Filepath and name of the output file. Defaults to the current working
306 | directory and filename `model.shc`.
307 | nmin : int, optional
308 | Minimum spherical harmonic degree (defaults to 1). This will remove
309 | first values from coeffs if greater than 1.
310 | nmax : int, optional
311 | Maximum spherical harmonic degree (defaults to degree compatible with
312 | number of coeffcients, otherwise coeffcients are truncated).
313 | leap_year : {True, False}, optional
314 | Take leap years for decimal year conversion into account
315 | (defaults to ``True``).
316 | header : str, optional
317 | Optional header at beginning of file. Defaults to an empty string.
318 |
319 | """
320 |
321 | time = np.asarray(time, dtype=float)
322 |
323 | order = 1 if order is None else int(order)
324 |
325 | nmin = 1 if nmin is None else int(nmin)
326 |
327 | if nmax is None:
328 | nmax = int(np.sqrt(coeffs.shape[-1] + 1) - 1)
329 | else:
330 | nmax = int(nmax)
331 |
332 | if nmin > nmax:
333 | raise ValueError('``nmin`` must be smaller than or equal to ``nmax``.')
334 |
335 | filepath = 'model.shc' if filepath is None else filepath
336 |
337 | header = '' if header is None else header
338 |
339 | leap_year = True if leap_year is None else bool(leap_year)
340 |
341 | if coeffs.ndim == 1:
342 | coeffs = coeffs.reshape((1, -1))
343 |
344 | coeffs = coeffs[:, (nmin**2-1):((nmax+1)**2-1)]
345 |
346 | # compute all possible degree and orders
347 | deg = np.array([], dtype=int)
348 | ord = np.array([], dtype=int)
349 | for n in range(nmin, nmax+1):
350 | deg = np.append(deg, np.repeat(n, 2*n+1))
351 | ord = np.append(ord, [0])
352 | for m in range(1, n+1):
353 | ord = np.append(ord, [m, -m])
354 |
355 | comment = header + textwrap.dedent(f"""\
356 | # Created on {dt.datetime.now(dt.timezone.utc)} UTC.
357 | # Leap years are accounted for in decimal years format ({leap_year}).
358 | {nmin} {nmax} {time.size} {order} {order-1}
359 | """)
360 |
361 | with open(filepath, 'w') as f:
362 | # write comment line
363 | f.write(comment)
364 |
365 | f.write(' ') # to represent two missing values
366 | for t in time:
367 | f.write(' {:16.8f}'.format(mjd_to_dyear(t, leap_year=leap_year)))
368 | f.write('\n')
369 |
370 | # write coefficient table to 8 significants
371 | for row, (n, m) in enumerate(zip(deg, ord)):
372 |
373 | f.write('{:} {:}'.format(n, m))
374 |
375 | for value in coeffs[:, row]:
376 | f.write(' {:16.8f}'.format(value))
377 |
378 | f.write('\n')
379 |
380 | print('Created SHC-file {}.'.format(
381 | os.path.join(os.getcwd(), filepath)))
382 |
383 |
384 | def mjd2000(year, month=1, day=1, hour=0, minute=0, second=0, microsecond=0,
385 | nanosecond=0):
386 | """
387 | Computes the modified Julian date as floating point number (epoch 2000).
388 |
389 | It assigns 0 to 0h00 January 1, 2000. Leap seconds are not accounted for.
390 |
391 | Parameters
392 | ----------
393 | time : :class:`datetime.datetime`, ndarray, shape (...)
394 | Datetime class instance, `OR ...`
395 | year : int, ndarray, shape (...)
396 | month : int, ndarray, shape (...), optional
397 | Month of the year `[1, 12]` (defaults to 1).
398 | day : int, ndarray, shape (...), optional
399 | Day of the corresponding month (defaults to 1).
400 | hour : int , ndarray, shape (...), optional
401 | Hour of the day (default is 0).
402 | minute : int, ndarray, shape (...), optional
403 | Minutes (default is 0).
404 | second : int, ndarray, shape (...), optional
405 | Seconds (default is 0).
406 | microsecond : int, ndarray, shape (...), optional
407 | Microseconds (default is 0).
408 | nanosecond : int, ndarray, shape (...), optional
409 | Nanoseconds (default is 0).
410 |
411 | Returns
412 | -------
413 | time : ndarray, shape (...)
414 | Modified Julian date (units of days).
415 |
416 | Examples
417 | --------
418 | >>> a = np.array([datetime.datetime(2000, 1, 1), \
419 | datetime.datetime(2002, 3, 4)])
420 | >>> mjd2000(a)
421 | array([ 0., 793.])
422 |
423 | >>> mjd2000(2003, 5, 3, 13, 52, 15) # May 3, 2003, 13:52:15 (hh:mm:ss)
424 | 1218.5779513888888
425 |
426 | >>> mjd2000(np.arange(2000, 2005)) # January 1 in each year
427 | array([ 0., 366., 731., 1096., 1461.])
428 |
429 | >>> mjd2000(np.arange(2000, 2005), 2, 1) # February 1 in each year
430 | array([ 31., 397., 762., 1127., 1492.])
431 |
432 | >>> mjd2000(np.arange(2000, 2005), 2, np.arange(1, 6))
433 | array([ 31., 398., 764., 1130., 1496.])
434 |
435 | """
436 |
437 | year = np.asarray(year)
438 |
439 | if (np.issubdtype(year.dtype, np.dtype(dt.datetime).type) or
440 | np.issubdtype(year.dtype, np.datetime64)):
441 | datetime = year.astype('datetime64[ns]')
442 |
443 | else:
444 | # build iso datetime string with str_ (supported in NumPy >= 2.0)
445 | year = np.asarray(year, dtype=np.str_)
446 | month = np.char.zfill(np.asarray(month, dtype=np.str_), 2)
447 | day = np.char.zfill(np.asarray(day, dtype=np.str_), 2)
448 |
449 | year_month = np.char.add(np.char.add(year, '-'), month)
450 | datetime = np.char.add(np.char.add(year_month, '-'), day)
451 |
452 | datetime = datetime.astype('datetime64[ns]')
453 |
454 | # not use iadd here because it doesn't broadcast arrays
455 | datetime = (datetime + np.asarray(hour, dtype='timedelta64[h]')
456 | + np.asarray(minute, dtype='timedelta64[m]')
457 | + np.asarray(second, dtype='timedelta64[s]')
458 | + np.asarray(microsecond, dtype='timedelta64[us]')
459 | + np.asarray(nanosecond, dtype='timedelta64[ns]'))
460 |
461 | nanoseconds = datetime - np.datetime64('2000-01-01', 'ns')
462 |
463 | return nanoseconds / np.timedelta64(1, 'D') # fraction of days
464 |
465 |
466 | def timestamp(time):
467 | """
468 | Convert modified Julian date to NumPy's datetime format.
469 |
470 | Parameters
471 | ----------
472 | time : ndarray, shape (...)
473 | Modified Julian date (units of days).
474 |
475 | Returns
476 | -------
477 | time : ndarray, shape (...)
478 | Array of ``numpy.datetime64[ns]``.
479 |
480 | Examples
481 | --------
482 | >>> timestamp(0.53245)
483 | numpy.datetime64('2000-01-01T12:46:43.680000000')
484 |
485 | >>> timestamp(np.linspace(0., 1.5, 2))
486 | array(['2000-01-01T00:00:00.000000000', \
487 | '2000-01-02T12:00:00.000000000'], dtype='datetime64[ns]')
488 |
489 | """
490 |
491 | # convert mjd2000 to timedelta64[ns]
492 | ns = np.asarray(time) * 86400e9 * np.timedelta64(1, 'ns')
493 |
494 | # add datetime offset with ns precision
495 | return ns + np.datetime64('2000-01-01', 'ns')
496 |
497 |
498 | def is_leap_year(year):
499 | """
500 | Determine if input year is a leap year.
501 |
502 | Parameters
503 | ----------
504 | year : int, ndarray, shape (...)
505 | Years to test for leap year.
506 |
507 | Returns
508 | -------
509 | leap_year : ndarray of bools, shape (...)
510 | ``True`` for leap year in array.
511 |
512 | Examples
513 | --------
514 | >>> is_leap_year([2000, 2001, 2004])
515 | array([ True, False, True])
516 |
517 | Raises
518 | ------
519 | TypeError if ``year`` is not of type integer.
520 |
521 | """
522 |
523 | year = np.asarray(year)
524 |
525 | if not np.issubdtype(year.dtype, int):
526 | raise TypeError('Expected integer values as the input year. Use '
527 | 'numpy.floor to extract the integer year '
528 | 'from decimal years.')
529 |
530 | return np.logical_and(np.remainder(year, 4) == 0,
531 | np.logical_or(np.remainder(year, 100) != 0,
532 | np.remainder(year, 400) == 0))
533 |
534 |
535 | def dyear_to_mjd(time, leap_year=None):
536 | """
537 | Convert time from decimal years to modified Julian date 2000.
538 |
539 | Leap years are accounted for by default.
540 |
541 | Parameters
542 | ----------
543 | time : float, ndarray, shape (...)
544 | Time in decimal years.
545 | leap_year : {True, False}, optional
546 | Take leap years into account by using a conversion factor of 365 or 366
547 | days in a year (leap year, used by default). If ``False`` a conversion
548 | factor of 365.25 days in a year is used.
549 |
550 | Returns
551 | -------
552 | time : ndarray, shape (...)
553 | Time in modified Julian date 2000.
554 |
555 | Examples
556 | --------
557 | >>> dyear_to_mjd([2000.5, 2001.5]) # account for leap years
558 | array([183. , 548.5])
559 |
560 | >>> dyear_to_mjd([2000.5, 2001.5], leap_year=False)
561 | array([182.625, 547.875])
562 |
563 | """
564 |
565 | leap_year = True if leap_year is None else leap_year
566 |
567 | if leap_year:
568 | year = np.asarray(np.floor(time), dtype=int) # note: -0.1 is year -1
569 | frac_of_year = np.remainder(time, 1.)
570 |
571 | isleap = is_leap_year(year) # do provide integer years
572 | days_per_year = np.where(isleap, 366., 365.)
573 |
574 | days = frac_of_year * days_per_year
575 |
576 | mjd = mjd2000(year, 1, 1) + days
577 |
578 | elif not leap_year:
579 | days_per_year = 365.25
580 |
581 | mjd = (np.asarray(time) - 2000.0) * days_per_year
582 |
583 | else:
584 | raise ValueError('Unknown leap year option: use either True or False')
585 |
586 | return mjd
587 |
588 |
589 | def mjd_to_dyear(time, leap_year=None):
590 | """
591 | Convert time from modified Julian date 2000 to decimal years.
592 |
593 | Leap years are accounted for by default.
594 |
595 | Parameters
596 | ----------
597 | time : float, ndarray, shape (...)
598 | Time in modified Julian date 2000.
599 | leap_year : {True, False}, optional
600 | Take leap years into account by using a conversion factor of 365 or 366
601 | days in a year (leap year, used by default). If ``False`` a conversion
602 | factor of 365.25 days in a year is used.
603 |
604 | Returns
605 | -------
606 | time : ndarray, shape (...)
607 | Time in decimal years.
608 |
609 | Examples
610 | --------
611 | >>> mjd_to_dyear([183. , 548.5]) # account for leap years
612 | array([2000.5, 2001.5])
613 |
614 | >>> mjd_to_dyear([0. , -1., 365.])
615 | array([2000., 1999.99726027, 2000.99726776])
616 |
617 | >>> mjd_to_dyear([182.625, 547.875], leap_year=False)
618 | array([2000.5, 2001.5])
619 |
620 | """
621 |
622 | leap_year = True if leap_year is None else leap_year
623 |
624 | if leap_year:
625 | date = (np.asarray(np.floor(time), dtype=int)*np.timedelta64(1, 'D')
626 | + np.datetime64('2000-01-01')) # only precise to date
627 |
628 | year = date.astype('datetime64[Y]').astype(int) + 1970
629 | days = np.asarray(time) - mjd2000(year, 1, 1) # days of that year
630 |
631 | isleap = is_leap_year(year)
632 | days_per_year = np.where(isleap, 366., 365.)
633 |
634 | dyear = year + days / days_per_year
635 |
636 | elif not leap_year:
637 | dyear = np.asarray(time) / 365.25 + 2000.
638 |
639 | else:
640 | raise ValueError('Unknown leap year option: use either True or False')
641 |
642 | return dyear
643 |
644 |
645 | def memory_usage(pandas_obj):
646 | """
647 | Compute memory usage of pandas object.
648 |
649 | For full report, use: ``df.info(memory_usage='deep')``.
650 |
651 | """
652 |
653 | if isinstance(pandas_obj, pd.DataFrame):
654 | usage_b = pandas_obj.memory_usage(deep=True).sum()
655 | else: # we assume if not a df it's a series
656 | usage_b = pandas_obj.memory_usage(deep=True)
657 | usage_mb = usage_b / 1024 ** 2 # convert bytes to megabytes
658 | return "{:03.2f} MB".format(usage_mb)
659 |
660 |
661 | def gauss_units(deriv=None):
662 | """
663 | Return string of the magnetic field units given the derivative with time.
664 |
665 | String is meant to be used in plot labels.
666 |
667 | Parameters
668 | ----------
669 | deriv : int, optional
670 | Derivative (defaults to 0).
671 |
672 | Returns
673 | -------
674 | units : str
675 | Tex-style unit string.
676 |
677 | Examples
678 | --------
679 | >>> gauss_units()
680 | 'nT'
681 |
682 | >>> gauss_units(1)
683 | '$\\\\mathrm{nT}/\\\\mathrm{yr}$'
684 |
685 | >>> gauss_units(2)
686 | '$\\\\mathrm{nT}/\\\\mathrm{yr}^{2}$'
687 |
688 | """
689 |
690 | deriv = 0 if deriv is None else deriv
691 |
692 | if deriv == 0:
693 | units = 'nT'
694 | elif deriv == 1:
695 | units = '$\\mathrm{{nT}}/\\mathrm{{yr}}$'
696 | else:
697 | units = '$\\mathrm{{nT}}/\\mathrm{{yr}}^{{{:}}}$'.format(deriv)
698 |
699 | return units
700 |
--------------------------------------------------------------------------------
/chaosmagpy/lib/Earth_conductivity.dat:
--------------------------------------------------------------------------------
1 | # Conductivity model of Earth (Grayver et al. 2017)
2 | # including core + 7kS top layer conductance (oceans)
3 | # depth (km), sigma (S/m)
4 | 0 7
5 | 1 0.0002258505181
6 | 10 0.0002300043494
7 | 22 0.0002467308811
8 | 37 0.0002971504977
9 | 52 0.0004164700344
10 | 65 0.0006525989493
11 | 80 0.001142292034
12 | 95 0.002142944398
13 | 112 0.00416244864
14 | 132 0.008305126583
15 | 155 0.01604748379
16 | 180 0.02832674342
17 | 210 0.04343537883
18 | 245 0.05713617533
19 | 280 0.06781208082
20 | 320 0.07669171609
21 | 360 0.08645067971
22 | 400 0.09896647522
23 | 440 0.1198745701
24 | 490 0.1635300279
25 | 550 0.2716671224
26 | 600 0.5297014807
27 | 660 1.026451136
28 | 730 1.35285022
29 | 800 1.429460733
30 | 880 1.441169054
31 | 980 1.446636682
32 | 1080 1.461772762
33 | 1190 1.481617611
34 | 1300 1.540456721
35 | 1400 1.60095963
36 | 1500 2.107276831
37 | 1600 2.561250576
38 | 1700 2.978558186
39 | 1800 3.301925981
40 | 1900 3.506691788
41 | 2000 3.644103242
42 | 2100 3.682761591
43 | 2200 3.703778436
44 | 2300 3.707901746
45 | 2400 3.697769525
46 | 2500 3.693743273
47 | 2600 3.688096839
48 | 2700 3.682981594
49 | 2800 3.674064006
50 | 2900 100000
51 |
--------------------------------------------------------------------------------
/chaosmagpy/lib/RC_index.h5:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ancklo/ChaosMagPy/f91dba5cca25e4d110611dcd62dda881e27ed710/chaosmagpy/lib/RC_index.h5
--------------------------------------------------------------------------------
/chaosmagpy/lib/frequency_spectrum_gsm.npz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ancklo/ChaosMagPy/f91dba5cca25e4d110611dcd62dda881e27ed710/chaosmagpy/lib/frequency_spectrum_gsm.npz
--------------------------------------------------------------------------------
/chaosmagpy/lib/frequency_spectrum_sm.npz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ancklo/ChaosMagPy/f91dba5cca25e4d110611dcd62dda881e27ed710/chaosmagpy/lib/frequency_spectrum_sm.npz
--------------------------------------------------------------------------------
/chaosmagpy/lib/ne_110m_coastline.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ancklo/ChaosMagPy/f91dba5cca25e4d110611dcd62dda881e27ed710/chaosmagpy/lib/ne_110m_coastline.zip
--------------------------------------------------------------------------------
/chaosmagpy/plot_utils.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2024 Clemens Kloss
2 | #
3 | # This file is part of ChaosMagPy.
4 | #
5 | # ChaosMagPy is released under the MIT license. See LICENSE in the root of the
6 | # repository for full licensing details.
7 |
8 | """
9 | `chaosmagpy.plot_utils` provides functions for plotting model outputs.
10 |
11 | .. autosummary::
12 | :toctree: functions
13 |
14 | plot_timeseries
15 | plot_maps
16 | plot_power_spectrum
17 | nio_colormap
18 |
19 | """
20 |
21 | import numpy as np
22 | import warnings
23 | import shapefile
24 | from . import data_utils
25 | from . import config_utils
26 |
27 | try:
28 | import matplotlib.pyplot as plt
29 | import matplotlib.ticker as ticker
30 | import matplotlib.dates as mdates
31 | from matplotlib.colors import LinearSegmentedColormap
32 | except ImportError:
33 | warnings.warn('Could not import Matplotlib. Plotting methods and '
34 | 'functions will raise a NameError.')
35 |
36 |
37 | def plot_timeseries(time, *args, **kwargs):
38 | """
39 | Creates line plots for the timeseries of the input arguments.
40 |
41 | Parameters
42 | ----------
43 | time : ndarray, shape (N,)
44 | Array containing time in modified Julian dates.
45 | *args : ndarray, shape (N, k)
46 | Array containing `k` columns of values to plot against time. Several
47 | arrays can be provided as separated arguments.
48 |
49 | Other Parameters
50 | ----------------
51 | figsize : 2-tuple of floats
52 | Figure dimension (width, height) in inches.
53 | titles : list of strings
54 | Subplot titles (defaults to empty strings).
55 | ylabel : string
56 | Label of the vertical axis (defaults to an empty string).
57 | layout : 2-tuple of int
58 | Layout of the subplots (defaults to vertically stacked subplots).
59 | **kwargs : keywords
60 | Other options to pass to matplotlib plotting method.
61 |
62 | Notes
63 | -----
64 | For more customization get access to the figure and axes handles
65 | through matplotlib by using ``fig = plt.gcf()`` and ``axes = fig.axes``
66 | right after the call to this plotting function.
67 |
68 | """
69 |
70 | n = len(args) # number of subplots
71 |
72 | defaults = {
73 | 'figsize': (config_utils.basicConfig['plots.figure_width'],
74 | 0.8*n*config_utils.basicConfig['plots.figure_width']),
75 | 'titles': n*[''],
76 | 'ylabel': '',
77 | 'layout': (n, 1)
78 | }
79 |
80 | kwargs = defaultkeys(defaults, kwargs)
81 |
82 | # remove keywords that are not intended for plot
83 | figsize = kwargs.pop('figsize')
84 | layout = kwargs.pop('layout')
85 | titles = kwargs.pop('titles')
86 | ylabel = kwargs.pop('ylabel')
87 |
88 | if layout[0]*layout[1] != n:
89 | raise ValueError('Plot layout is not compatible with the number of '
90 | 'produced subplots.')
91 |
92 | date_time = data_utils.timestamp(np.ravel(time))
93 |
94 | fig, axes = plt.subplots(layout[0], layout[1], sharex='all',
95 | figsize=figsize, squeeze=False)
96 |
97 | for ax, component, title in zip(axes.flat, args, titles):
98 | ax.plot(date_time, component, **kwargs)
99 | ax.set_title(title)
100 | ax.grid(True)
101 | ax.set(ylabel=ylabel, xlabel='time')
102 | fig.autofmt_xdate()
103 | ax.fmt_xdata = mdates.DateFormatter('%Y-%m-%d-%h')
104 |
105 | fig.tight_layout(rect=(0, 0.02, 1, 1))
106 |
107 |
108 | def plot_maps(theta_grid, phi_grid, *args, **kwargs):
109 | """
110 | Plots global maps of the input arguments.
111 |
112 | Parameters
113 | ----------
114 | theta_grid : ndarray
115 | Array containing the colatitude in degrees.
116 | phi_grid : ndarray
117 | Array containing the longitude in degrees.
118 | *args : ndarray
119 | Array of values to plot on the global map. Several
120 | arrays can be provided as separated arguments.
121 |
122 | Other Parameters
123 | ----------------
124 | figsize : 2-tuple of floats
125 | Figure dimension (width, height) in inches.
126 | titles : list of strings
127 | Subplot titles (defaults to empty strings).
128 | label : string
129 | Colorbar label (defaults to an empty string).
130 | layout : 2-tuple of int
131 | Layout of the subplots (defaults to vertically stacked subplots).
132 | cmap : str
133 | Colormap code (defaults to ``'PuOr_r'`` colormap).
134 | limiter : function, lambda expression
135 | Function to compute symmetric colorbar limits (defaults to maximum of
136 | the absolute values in the input, or use ``'vmin'``, ``'vmax'``
137 | keywords instead).
138 | projection : str
139 | Projection of the target frame (defaults to `'mollweide'`).
140 | **kwargs : keywords
141 | Other options to pass to matplotlib :func:`pcolormesh` method.
142 |
143 | Notes
144 | -----
145 | For more customization get access to the figure and axes handles
146 | through matplotlib by using ``fig = plt.gcf()`` and ``axes = fig.axes``
147 | right after the call to this plotting function.
148 |
149 | """
150 |
151 | n = len(args) # number of plots
152 |
153 | defaults = {
154 | 'figsize': (config_utils.basicConfig['plots.figure_width'],
155 | 0.4*n*config_utils.basicConfig['plots.figure_width']),
156 | 'titles': n*[''],
157 | 'label': '',
158 | 'layout': (n, 1),
159 | 'cmap': 'PuOr_r',
160 | 'limiter': lambda x: np.amax(np.abs(x)), # maximum value
161 | 'projection': 'mollweide',
162 | 'shading': 'auto'
163 | }
164 |
165 | kwargs = defaultkeys(defaults, kwargs)
166 |
167 | # remove keywords that are not intended for pcolormesh
168 | figsize = kwargs.pop('figsize')
169 | titles = kwargs.pop('titles')
170 | label = kwargs.pop('label')
171 | limiter = kwargs.pop('limiter')
172 | projection = kwargs.pop('projection')
173 | layout = kwargs.pop('layout')
174 |
175 | if layout[0]*layout[1] != n:
176 | raise ValueError('Plot layout is not compatible with the number of '
177 | 'produced subplots.')
178 |
179 | # load shapefile with the coastline
180 | shp = config_utils.basicConfig['file.shp_coastline']
181 |
182 | # create axis handle
183 | fig, axes = plt.subplots(layout[0], layout[1], figsize=figsize,
184 | subplot_kw=dict(projection=projection),
185 | squeeze=False)
186 |
187 | # make subplots
188 | for ax, component, title in zip(axes.flat, args, titles):
189 |
190 | # evaluate colorbar limits depending on vmax/vmin in kwargs
191 | kwargs.setdefault('vmax', limiter(component))
192 | kwargs.setdefault('vmin', -limiter(component))
193 |
194 | with shapefile.Reader(shp) as sf:
195 |
196 | for rec in sf.shapeRecords():
197 | lon = np.radians([point[0] for point in rec.shape.points[:]])
198 | lat = np.radians([point[1] for point in rec.shape.points[:]])
199 |
200 | ax.plot(lon, lat, color='k', linewidth=0.8)
201 |
202 | # produce colormesh and evaluate keywords (defaults and input)
203 | pc = ax.pcolormesh(np.radians(phi_grid), np.radians(90. - theta_grid),
204 | component, **kwargs)
205 |
206 | plt.colorbar(pc, ax=ax, extend='both', label=label)
207 |
208 | ax.xaxis.set_ticks(np.radians(np.linspace(-180., 180., num=13)))
209 | ax.yaxis.set_ticks(np.radians(np.linspace(-60., 60., num=5)))
210 | ax.xaxis.set_major_formatter('')
211 | ax.grid(True)
212 |
213 | ax.set_title(title)
214 |
215 | fig.tight_layout()
216 |
217 |
218 | def plot_power_spectrum(spectrum, **kwargs):
219 | """
220 | Plot the spherical harmonic spectrum.
221 |
222 | Parameters
223 | ----------
224 | spectrum : ndarray, shape (N,)
225 | Spherical harmonics spectrum of degree `N`.
226 |
227 | Other Parameters
228 | ----------------
229 | figsize : 2-tuple of floats
230 | Figure dimension (width, height) in inches.
231 | titles : list of strings
232 | Subplot titles (defaults to empty strings).
233 | ylabel : string
234 | Label of the vertical axis (defaults to an empty string).
235 | **kwargs
236 | Keywords passed to :func:`matplotlib.pyplot.semilogy`
237 |
238 | Notes
239 | -----
240 | For more customization get access to the figure and axes handles
241 | through matplotlib by using ``fig = plt.gcf()`` and ``axes = fig.axes``
242 | right after the call to this plotting function.
243 |
244 | """
245 |
246 | defaults = {
247 | 'figsize': (config_utils.basicConfig['plots.figure_width'],
248 | 0.8*config_utils.basicConfig['plots.figure_width']),
249 | 'titles': '',
250 | 'ylabel': ''
251 | }
252 |
253 | kwargs = defaultkeys(defaults, kwargs)
254 |
255 | figsize = kwargs.pop('figsize')
256 | titles = kwargs.pop('titles')
257 | ylabel = kwargs.pop('ylabel')
258 |
259 | degrees = np.arange(1, spectrum.shape[0] + 1, step=1.0)
260 | spectrum[spectrum == 0] = np.nan # remove non-positive values for log
261 |
262 | # create axis handle
263 | fig, ax = plt.subplots(1, 1, sharex=True, sharey=True, figsize=figsize)
264 |
265 | ax.semilogy(degrees, spectrum, **kwargs)
266 | ax.set_title(titles)
267 | ax.grid(True, which='minor', linestyle=':')
268 | ax.grid(True, which='major', linestyle='-', axis='both')
269 | ax.set(ylabel=ylabel, xlabel='degree')
270 |
271 | ax.xaxis.set_major_locator(ticker.MaxNLocator(integer=True))
272 |
273 | fig.tight_layout()
274 |
275 |
276 | def fmt(x, pos):
277 | # format=ticker.FuncFormatter(fmt)
278 | a, b = '{:.1e}'.format(x).split('e')
279 | b = int(b)
280 |
281 | if a == '0.0':
282 | return r'${}$'.format(a)
283 | else:
284 | return r'${}$e${}$'.format(a, b)
285 |
286 |
287 | def defaultkeys(defaults, keywords):
288 | """
289 | Return dictionary of default keywords. Overwrite any keywords in
290 | ``defaults`` using keywords from ``keywords``, except if they are None.
291 |
292 | Parameters
293 | ----------
294 | defaults, keywords : dict
295 | Dictionary of default and replacing keywords.
296 |
297 | Returns
298 | -------
299 | keywords : dict
300 |
301 | """
302 |
303 | # overwrite value with the one in kwargs, if not then use the default
304 | for key, value in defaults.items():
305 | if keywords.setdefault(key, value) is None:
306 | keywords[key] = value
307 |
308 | return keywords
309 |
310 |
311 | def nio_colormap():
312 | """
313 | Define custom-built colormap 'nio' and register. Can be called in plots as
314 | ``cmap='nio'`` after importing this module.
315 |
316 | .. plot::
317 | :include-source: false
318 |
319 | import numpy as np
320 | import chaosmagpy as cp
321 | import matplotlib.pyplot as plt
322 |
323 | gradient = np.linspace(0, 1, 256)
324 | gradient = np.vstack((gradient, gradient))
325 |
326 | figh = 0.35 + 0.15 + 0.22
327 | fig, ax = plt.subplots(1, 1, figsize=(6.4, figh))
328 | fig.subplots_adjust(top=0.792, bottom=0.208, left=0.023, right=0.977)
329 | ax.imshow(gradient, aspect='auto', cmap='nio')
330 | # ax.set_axis_off()
331 | ax.get_xaxis().set_visible(False)
332 | ax.get_yaxis().set_visible(False)
333 | plt.show()
334 |
335 | """
336 |
337 | cdict = {'red': ((0.0000, 0.0000, 0.0000),
338 | (0.1667, 0.0000, 0.0000),
339 | (0.3333, 0.0000, 0.0000),
340 | (0.5020, 1.0000, 1.0000),
341 | (0.6667, 1.0000, 1.0000),
342 | (0.8333, 1.0000, 1.0000),
343 | (1.0000, 1.0000, 1.0000)),
344 |
345 | 'green': ((0.0000, 0.0000, 0.0000),
346 | (0.1667, 0.0000, 0.0000),
347 | (0.3333, 1.0000, 1.0000),
348 | (0.5020, 1.0000, 1.0000),
349 | (0.6667, 1.0000, 1.0000),
350 | (0.8333, 0.0000, 0.0000),
351 | (1.0000, 0.0000, 0.0000)),
352 |
353 | 'blue': ((0.0000, 0.0000, 0.0000),
354 | (0.1667, 1.0000, 1.0000),
355 | (0.3333, 1.0000, 1.0000),
356 | (0.5020, 1.0000, 1.0000), # >0.5 for white (intpl bug?)
357 | (0.6667, 0.0000, 0.0000),
358 | (0.8333, 0.0000, 0.0000),
359 | (1.0000, 1.0000, 1.0000))}
360 |
361 | return LinearSegmentedColormap('nio', cdict)
362 |
363 |
364 | # register cmap name for convenient use
365 | try:
366 | plt.colormaps.register(cmap=nio_colormap())
367 | except NameError:
368 | pass
369 |
370 |
371 | if __name__ == '__main__':
372 | pass
373 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | # Ensure indentation is done via tabs: check with "cat -e -t -v Makefile"
3 | # where tabs are "^I" and line endings are "$"
4 | #
5 |
6 | # You can set these variables from the command line.
7 | SPHINXOPTS =
8 | SPHINXBUILD = sphinx-build
9 | SPHINXPROJ = ChaosMagPy
10 | SOURCEDIR = source
11 | BUILDDIR = build
12 |
13 | # Put it first so that "make" without argument is like "make help".
14 | help:
15 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
16 |
17 | clean:
18 | rm -rf $(BUILDDIR)/*
19 | rm -rf $(SOURCEDIR)/classes/
20 | rm -rf $(SOURCEDIR)/functions/
21 | rm -rf $(SOURCEDIR)/gallery/
22 |
23 | html-noplot:
24 | @$(SPHINXBUILD) -D plot_gallery=0 -b html $(SPHINXOPTS) $(SOURCEDIR) $(BUILDDIR)/html
25 | @echo
26 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
27 |
28 | .PHONY: help Makefile clean html-noplot
29 |
30 | # Catch-all target: route all unknown targets to Sphinx using the new
31 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
32 | %: Makefile
33 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
34 |
--------------------------------------------------------------------------------
/docs/source/.static/examples/CHAOS-7.mat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ancklo/ChaosMagPy/f91dba5cca25e4d110611dcd62dda881e27ed710/docs/source/.static/examples/CHAOS-7.mat
--------------------------------------------------------------------------------
/docs/source/.static/examples/README.txt:
--------------------------------------------------------------------------------
1 | Gallery
2 | =======
3 |
4 | The gallery provides more examples that show how to use ChaosMagPy.
5 |
6 | If you are unfamiliar with ChaosMagPy, the :ref:`sec-usage` section may be a
7 | good place to start.
8 |
--------------------------------------------------------------------------------
/docs/source/.static/examples/plot_global_map.py:
--------------------------------------------------------------------------------
1 | """
2 | Create a Global Map
3 | ===================
4 |
5 | This script creates a map of the radial magnetic field from the CHAOS
6 | geomagnetic field model at the core surface in 2016. The map projections are
7 | handled by the Cartopy package, which you need to install in addition to
8 | ChaosMagPy to execute the script.
9 |
10 | """
11 |
12 | # Copyright (C) 2024 Clemens Kloss
13 | #
14 | # This script is free software: you can redistribute it and/or modify it under
15 | # the terms of the GNU Lesser General Public License as published by the Free
16 | # Software Foundation, either version 3 of the License, or (at your option) any
17 | # later version.
18 | #
19 | # This script is distributed in the hope that it will be useful, but WITHOUT
20 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
21 | # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
22 | # details.
23 | #
24 | # You should have received a copy of the GNU Lesser General Public License
25 | # along with this script. If not, see .
26 |
27 | import chaosmagpy as cp
28 | import numpy as np
29 | import matplotlib.pyplot as plt
30 | import cartopy.crs as ccrs
31 |
32 | model = cp.CHAOS.from_mat('CHAOS-7.mat') # load the mat-file of CHAOS-7
33 |
34 | time = cp.mjd2000(2016, 1, 1) # convert date to mjd2000
35 | radius = 3485. # radius of the core surface in km
36 | theta = np.linspace(1., 179., 181) # colatitude in degrees
37 | phi = np.linspace(-180., 180, 361) # longitude in degrees
38 |
39 | # compute radial magnetic field from CHAOS up to degree 13
40 | Br, _, _ = model.synth_values_tdep(time, radius, theta, phi,
41 | nmax=13, deriv=0, grid=True)
42 |
43 | limit = 1.0 # mT colorbar limit
44 |
45 | # create figure
46 | fig, ax = plt.subplots(1, 1, subplot_kw={'projection': ccrs.EqualEarth()},
47 | figsize=(12, 8))
48 |
49 | fig.subplots_adjust(
50 | top=0.981,
51 | bottom=0.019,
52 | left=0.013,
53 | right=0.987,
54 | hspace=0.0,
55 | wspace=1.0
56 | )
57 |
58 | pc = ax.pcolormesh(phi, 90. - theta, Br/1e6, cmap='PuOr', vmin=-limit,
59 | vmax=limit, transform=ccrs.PlateCarree())
60 |
61 | ax.gridlines(linewidth=0.5, linestyle='dashed', color='grey',
62 | ylocs=np.linspace(-90, 90, num=7), # parallels
63 | xlocs=np.linspace(-180, 180, num=13)) # meridians
64 |
65 | ax.coastlines(linewidth=0.8, color='k')
66 |
67 | # create colorbar
68 | clb = plt.colorbar(pc, ax=ax, extend='both', shrink=0.8, pad=0.05,
69 | orientation='horizontal')
70 | clb.set_label('$B_r$ (mT)', fontsize=14)
71 |
72 | plt.show()
73 |
--------------------------------------------------------------------------------
/docs/source/.static/examples/plot_global_polar.py:
--------------------------------------------------------------------------------
1 | """
2 | Create a Global Map and Polar Views
3 | ===================================
4 |
5 | This script creates a map of the first time-derivative of the radial field
6 | component from the CHAOS geomagnetic field model on the core surface
7 | in 2016. The map projections are handled by the Cartopy package, which you
8 | need to install in addition to ChaosMagPy to execute the script.
9 |
10 | """
11 |
12 | # Copyright (C) 2024 Clemens Kloss
13 | #
14 | # This script is free software: you can redistribute it and/or modify it under
15 | # the terms of the GNU Lesser General Public License as published by the Free
16 | # Software Foundation, either version 3 of the License, or (at your option) any
17 | # later version.
18 | #
19 | # This script is distributed in the hope that it will be useful, but WITHOUT
20 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
21 | # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
22 | # details.
23 | #
24 | # You should have received a copy of the GNU Lesser General Public License
25 | # along with this script. If not, see .
26 |
27 | import chaosmagpy as cp
28 | import numpy as np
29 | import matplotlib.pyplot as plt
30 | import matplotlib.gridspec as gridspec
31 | from mpl_toolkits.axes_grid1.inset_locator import inset_axes
32 | import cartopy.crs as ccrs
33 |
34 | model = cp.CHAOS.from_mat('CHAOS-7.mat') # load the mat-file of CHAOS-7
35 |
36 | time = cp.data_utils.mjd2000(2016, 1, 1) # convert date to mjd2000
37 | radius = 3485. # radius of the core surface in km
38 | theta = np.linspace(1., 179., 181) # colatitude in degrees
39 | phi = np.linspace(-180., 180, 361) # longitude in degrees
40 |
41 | # compute radial SV up to degree 16 using CHAOS
42 | B, _, _ = model.synth_values_tdep(time, radius, theta, phi,
43 | nmax=16, deriv=1, grid=True)
44 |
45 | limit = 30e3 # nT/yr colorbar limit
46 |
47 | # create figure
48 | fig = plt.figure(figsize=(12, 8))
49 |
50 | fig.subplots_adjust(
51 | top=0.98,
52 | bottom=0.02,
53 | left=0.013,
54 | right=0.988,
55 | hspace=0.0,
56 | wspace=1.0
57 | )
58 |
59 | # make array of axes
60 | gs = gridspec.GridSpec(2, 2, width_ratios=[0.5, 0.5], height_ratios=[0.35, 0.65])
61 |
62 | axes = [
63 | plt.subplot(gs[0, 0], projection=ccrs.NearsidePerspective(central_latitude=90.)),
64 | plt.subplot(gs[0, 1], projection=ccrs.NearsidePerspective(central_latitude=-90.)),
65 | plt.subplot(gs[1, :], projection=ccrs.Mollweide())
66 | ]
67 |
68 | for ax in axes:
69 | pc = ax.pcolormesh(phi, 90. - theta, B, cmap='PuOr', vmin=-limit,
70 | vmax=limit, transform=ccrs.PlateCarree())
71 | ax.gridlines(linewidth=0.5, linestyle='dashed', color='grey',
72 | ylocs=np.linspace(-90, 90, num=7), # parallels
73 | xlocs=np.linspace(-180, 180, num=13)) # meridians
74 | ax.coastlines(linewidth=0.8, color='k')
75 |
76 | # inset axes into global map and move upwards
77 | cax = inset_axes(axes[-1], width="45%", height="5%", loc='upper center',
78 | borderpad=-12)
79 |
80 | # use last artist for the colorbar
81 | clb = plt.colorbar(pc, cax=cax, extend='both', orientation='horizontal')
82 | clb.set_label('nT/yr', fontsize=14)
83 |
84 | plt.show()
85 |
--------------------------------------------------------------------------------
/docs/source/.static/examples/plot_spectrum.py:
--------------------------------------------------------------------------------
1 | """
2 | Spatial Power Spectra
3 | =====================
4 |
5 | This script creates a plot of the spatial power spectrum of the time-dependent
6 | internal field from the CHAOS geomagnetic field model.
7 |
8 | """
9 |
10 | import chaosmagpy as cp
11 | import matplotlib.pyplot as plt
12 | import matplotlib.ticker as ticker
13 | import numpy as np
14 |
15 | model = cp.CHAOS.from_mat('CHAOS-7.mat') # load the mat-file of CHAOS-7
16 |
17 | nmax = 20
18 | time = cp.data_utils.mjd2000(2018, 1, 1)
19 | degrees = np.arange(1, nmax+1, dtype=int)
20 |
21 | fig, ax = plt.subplots(1, 1, figsize=(12, 7))
22 |
23 | for deriv, label in enumerate(['nT', 'nT/yr', 'nT/yr$^2$']):
24 |
25 | # get spatial power spectrum from time-dependent internal field in CHAOS
26 | spec = model.model_tdep.power_spectrum(time, nmax=nmax, deriv=deriv)
27 | ax.semilogy(degrees, spec, label=label)
28 |
29 | ax.legend()
30 | ax.grid(which='both')
31 |
32 | ax.yaxis.set_major_locator(ticker.LogLocator(base=10.0, numticks=15))
33 | ax.xaxis.set_major_locator(ticker.MultipleLocator(1))
34 |
35 | ax.set_title("Spatial power spectra at Earth's surface", fontsize=14)
36 | ax.set_xlabel('Spherical harmonic degree')
37 |
38 | plt.tight_layout()
39 | plt.show()
40 |
--------------------------------------------------------------------------------
/docs/source/.static/examples/plot_stations.py:
--------------------------------------------------------------------------------
1 | """
2 | Evaluate CHAOS at a Ground Observatory
3 | ======================================
4 |
5 | This script creates a time series plot of the first time-derivative of
6 | the magnetic field components (SV) by evaluating the CHAOS geomagnetic field
7 | model.
8 |
9 | In this example the location of the ground observatory in Niemegk (Germany)
10 | is used. The spherical harmonic coefficients of the SV are truncated
11 | at degree 16. Note that the SV vector components are spherical geographic and
12 | not geodetic.
13 |
14 | """
15 |
16 | import chaosmagpy as cp
17 | import matplotlib.pyplot as plt
18 | import numpy as np
19 |
20 | model = cp.CHAOS.from_mat('CHAOS-7.mat') # load the mat-file of CHAOS-7
21 |
22 | height = 0. # geodetic height of the observatory in Niemegk (WGS84)
23 | lat_gg = 52.07 # geodetic latitude of Niemegk in degrees
24 | lon = 12.68 # longitude in degrees
25 |
26 | radius, theta = cp.coordinate_utils.gg_to_geo(height, 90. - lat_gg)
27 |
28 | data = {
29 | 'Time': np.linspace(cp.mjd2000(1998, 1, 1), cp.mjd2000(2018, 1, 1), 500), # time in mjd2000
30 | 'Radius': radius, # spherical geographic radius in kilometers
31 | 'Theta': theta, # spherical geographic colatitude in degrees
32 | 'Phi': lon # longitude in degrees
33 | }
34 |
35 | # compute SV components up to degree 16 (note deriv=1)
36 | dBr, dBt, dBp = model.synth_values_tdep(
37 | data['Time'], data['Radius'], data['Theta'], data['Phi'], nmax=16, deriv=1)
38 |
39 | fig, axes = plt.subplots(1, 3, figsize=(12, 5))
40 | fig.subplots_adjust(
41 | top=0.874,
42 | bottom=0.117,
43 | left=0.061,
44 | right=0.985,
45 | hspace=0.2,
46 | wspace=0.242
47 | )
48 |
49 | fig.suptitle(f'SV components at Niemegk given by {model.name}', fontsize=14)
50 |
51 | axes[0].plot(cp.timestamp(data['Time']), dBr)
52 | axes[1].plot(cp.timestamp(data['Time']), dBt)
53 | axes[2].plot(cp.timestamp(data['Time']), dBp)
54 |
55 | axes[0].set_title('d$B_r$/d$t$')
56 | axes[1].set_title('d$B_{\\theta}$/d$t$')
57 | axes[2].set_title('d$B_{\\phi}$/d$t$')
58 |
59 | for ax in axes:
60 | ax.grid()
61 | ax.set_xlabel('Year')
62 | ax.set_ylabel('nT/yr')
63 |
64 | plt.show()
65 |
--------------------------------------------------------------------------------
/docs/source/.static/plot_maps_static.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ancklo/ChaosMagPy/f91dba5cca25e4d110611dcd62dda881e27ed710/docs/source/.static/plot_maps_static.png
--------------------------------------------------------------------------------
/docs/source/.static/plot_maps_tdep.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ancklo/ChaosMagPy/f91dba5cca25e4d110611dcd62dda881e27ed710/docs/source/.static/plot_maps_tdep.png
--------------------------------------------------------------------------------
/docs/source/.templates/myclass.rst:
--------------------------------------------------------------------------------
1 | {{ fullname }}
2 | {{ underline }}
3 |
4 | .. currentmodule:: {{ module }}
5 |
6 | .. autoclass:: {{ objname }}
7 | :members:
8 | :special-members: __call__
9 | :show-inheritance:
10 |
--------------------------------------------------------------------------------
/docs/source/changelog.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../../CHANGELOG.rst
2 |
--------------------------------------------------------------------------------
/docs/source/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Configuration file for the Sphinx documentation builder.
4 | #
5 | # This file does only contain a selection of the most common options. For a
6 | # full list see the documentation:
7 | # http://www.sphinx-doc.org/en/stable/config
8 |
9 | # -- Path setup --------------------------------------------------------------
10 |
11 | # If extensions (or modules to document with autodoc) are in another directory,
12 | # add these directories to sys.path here. If the directory is relative to the
13 | # documentation root, use os.path.abspath to make it absolute, like shown here.
14 |
15 | import os
16 | import sys
17 |
18 | root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
19 | sys.path.insert(0, root)
20 | autodoc_mock_imports = ['_tkinter']
21 |
22 | import matplotlib # import so that 'agg' can be given, as readthedocs fails
23 | import matplotlib.pyplot as plt
24 | matplotlib.use('agg')
25 | plt.ioff()
26 |
27 | import datetime
28 | import chaosmagpy
29 |
30 | # -- Project information -----------------------------------------------------
31 |
32 | project = 'ChaosMagPy'
33 | copyright = (str(datetime.date.today().year) + ' Clemens Kloss')
34 | author = 'Clemens Kloss'
35 |
36 | # The short X.Y version
37 | version = chaosmagpy.__version__
38 | # The full version, including alpha/beta/rc tags
39 | release = version
40 |
41 |
42 | # -- General configuration ---------------------------------------------------
43 |
44 | # If your documentation needs a minimal Sphinx version, state it here.
45 | #
46 | # needs_sphinx = '1.0'
47 |
48 | # Add any Sphinx extension module names here, as strings. They can be
49 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
50 | # ones.
51 | extensions = [
52 | 'sphinx.ext.autodoc',
53 | 'numpydoc',
54 | 'sphinx.ext.intersphinx',
55 | 'sphinx_copybutton',
56 | 'sphinx.ext.todo',
57 | 'sphinx.ext.ifconfig',
58 | 'sphinx.ext.viewcode',
59 | 'sphinx.ext.mathjax',
60 | 'sphinx.ext.extlinks',
61 | 'sphinx_gallery.gen_gallery',
62 | 'matplotlib.sphinxext.plot_directive',
63 | ]
64 |
65 | # Add any paths that contain templates here, relative to this directory.
66 | templates_path = ['.templates']
67 |
68 | # The suffix(es) of source filenames.
69 | # You can specify multiple suffix as a list of string:
70 | #
71 | # source_suffix = ['.rst', '.md']
72 | source_suffix = '.rst'
73 |
74 | # The master toctree document.
75 | master_doc = 'index'
76 |
77 | # The language for content autogenerated by Sphinx. Refer to documentation
78 | # for a list of supported languages.
79 | #
80 | # This is also used if you do content translation via gettext catalogs.
81 | # Usually you set "language" from the command line for these cases.
82 | language = 'en'
83 |
84 | # List of patterns, relative to source directory, that match files and
85 | # directories to ignore when looking for source files.
86 | # This pattern also affects html_static_path and html_extra_path .
87 | exclude_patterns = []
88 |
89 | # The name of the Pygments (syntax highlighting) style to use.
90 | pygments_style = 'sphinx'
91 |
92 |
93 | # -- Options for HTML output -------------------------------------------------
94 |
95 | # The theme to use for HTML and HTML Help pages. See the documentation for
96 | # a list of builtin themes.
97 | #
98 | html_theme = 'nature'
99 |
100 | # Theme options are theme-specific and customize the look and feel of a theme
101 | # further. For a list of options available for each theme, see the
102 | # documentation.
103 | #
104 | # html_theme_options = {}
105 |
106 | # Add any paths that contain custom static files (such as style sheets) here,
107 | # relative to this directory. They are copied after the builtin static files,
108 | # so a file named "default.css" will overwrite the builtin "default.css".
109 | html_static_path = ['.static']
110 |
111 | # Custom sidebar templates, must be a dictionary that maps document names
112 | # to template names.
113 | #
114 | # The default sidebars (for documents that don't match any pattern) are
115 | # defined by theme itself. Builtin themes are using these templates by
116 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
117 | # 'searchbox.html']``.
118 |
119 | html_sidebars = {'**': ['globaltoc.html', 'relations.html', 'sourcelink.html', 'searchbox.html']}
120 |
121 |
122 | # -- Options for HTMLHelp output ---------------------------------------------
123 |
124 | # Output file base name for HTML help builder.
125 | htmlhelp_basename = 'ChaosMagPydoc'
126 |
127 |
128 | # -- Options for LaTeX output ------------------------------------------------
129 |
130 | latex_elements = {
131 | # The paper size ('letterpaper' or 'a4paper').
132 | #
133 | # 'papersize': 'letterpaper',
134 |
135 | # The font size ('10pt', '11pt' or '12pt').
136 | #
137 | # 'pointsize': '10pt',
138 |
139 | # Additional stuff for the LaTeX preamble.
140 | #
141 | # 'preamble': '',
142 |
143 | # Latex figure (float) alignment
144 | #
145 | # 'figure_align': 'htbp',
146 | }
147 |
148 | # Grouping the document tree into LaTeX files. List of tuples
149 | # (source start file, target name, title,
150 | # author, documentclass [howto, manual, or own class]).
151 | latex_documents = [
152 | (master_doc, 'chaosmagpy.tex', 'ChaosMagPy Documentation',
153 | author, 'manual'),
154 | ]
155 |
156 |
157 | # -- Options for manual page output ------------------------------------------
158 |
159 | # One entry per manual page. List of tuples
160 | # (source start file, name, description, authors, manual section).
161 | man_pages = [
162 | (master_doc, 'chaosmagpy', 'ChaosMagPy Documentation',
163 | [author], 1)
164 | ]
165 |
166 |
167 | # -- Options for Texinfo output ----------------------------------------------
168 |
169 | # Grouping the document tree into Texinfo files. List of tuples
170 | # (source start file, target name, title, author,
171 | # dir menu entry, description, category)
172 | texinfo_documents = [
173 | (master_doc, 'chaosmagpy', 'ChaosMagPy Documentation',
174 | author, 'ChaosMagPy', 'Package to evaluate the CHAOS model and '
175 | 'other geomagnetic field models.',
176 | 'Miscellaneous'),
177 | ]
178 |
179 |
180 | # -- Extension configuration -------------------------------------------------
181 |
182 | # -- Options for autodoc extension ---------------------------------------
183 |
184 | # generate rst of member functions on the fly
185 | autosummary_generate = True
186 | autodata_content = 'both'
187 |
188 | # -- Options for numpydoc extension ---------------------------------------
189 |
190 | # Remove class members to suppress error message when compiling
191 | # (removes module list)
192 | numpydoc_show_class_members = True
193 | numpydoc_show_inherited_class_members = False
194 | numpydoc_class_members_toctree = False
195 |
196 | # -- Options for intersphinx extension ---------------------------------------
197 |
198 | # Example configuration for intersphinx: refer to the Python standard library.
199 | intersphinx_mapping = {'python': ('https://docs.python.org/3', None)}
200 |
201 | # -- Options for todo extension ----------------------------------------------
202 |
203 | # If true, `todo` and `todoList` produce output, else they produce nothing.
204 | todo_include_todos = True
205 |
206 | # -- Options for extlinks extension ------------------------------------------
207 | # Note the "%s" in the base url. You can display the link itself with
208 | # :chaos_url:`\\ `, or insert a clickable caption with
209 | # :chaos_url:`click here <>`
210 |
211 | extlinks = {
212 | 'chaos_url': ('http://www.spacecenter.dk/files/magnetic-models/CHAOS-7/%s', None),
213 | 'docs_url': ('https://chaosmagpy.readthedocs.io/en/%s', None),
214 | 'zenodo_url': ('https://doi.org/10.5281/zenodo.3352398%s', None),
215 | 'rc_url': ('http://www.spacecenter.dk/files/magnetic-models/RC/current/%s', None)
216 | }
217 |
218 | # -- Options for sphinx gallery ----------------------------------------------
219 |
220 | sphinx_gallery_conf = {
221 | 'examples_dirs': '.static/examples', # path to your example scripts
222 | 'gallery_dirs': 'gallery', # path to gallery generated output
223 |
224 | 'download_all_examples': False,
225 | }
226 |
227 | # -- Options for sphinx copybutton--------------------------------------------
228 |
229 | copybutton_prompt_text = ">>> "
230 |
231 | # -- Matplotlib plot_directive options ---------------------------------------
232 |
233 | plot_pre_code = ''
234 | plot_include_source = True
235 | plot_formats = [('png', 96)]
236 | plot_html_show_formats = False
237 | plot_html_show_source_link = False
238 |
239 | fontsize = 13*72/96.0 # 13 px
240 |
241 | plot_rcparams = {
242 | 'font.size': fontsize,
243 | 'axes.titlesize': fontsize,
244 | 'axes.labelsize': fontsize,
245 | 'xtick.labelsize': fontsize,
246 | 'ytick.labelsize': fontsize,
247 | 'legend.fontsize': fontsize,
248 | 'figure.figsize': (5*1.618, 5),
249 | 'text.usetex': False,
250 | }
251 |
--------------------------------------------------------------------------------
/docs/source/configuration.rst:
--------------------------------------------------------------------------------
1 | Configuration
2 | =============
3 |
4 | ChaosMagPy internally uses a number of parameters and coefficient/data files,
5 | whose numerical values and filepaths are stored in a dictionary-like container,
6 | called ``basicConfig``. Usually, the content of it need not be changed.
7 | However, if for example one wishes to compute a time series of the
8 | external field beyond the limit of the builtin RC-index file, then ChaosMagPy
9 | can be configured to use an updates RC-index file.
10 |
11 | To view the parameters in ``basicConfig``, do the following:
12 |
13 | .. code-block:: python
14 |
15 | import chaosmagpy as cp
16 |
17 | print(cp.basicConfig)
18 |
19 | This will print a list of the parameters than can in principle be changed.
20 | For example, it contains Earth's surface radius ``params.r_surf``, which is
21 | used as reference radius for the spherical harmonic representation of the
22 | magnetic potential field. For a complete list, see
23 | :ref:`sec-configuration-utilities`.
24 |
25 | .. _sec-configuration-change-rc-index-file:
26 |
27 | Change RC index file
28 | --------------------
29 |
30 | .. note::
31 |
32 | Find more information about why this may be necessary in the documentation
33 | of :meth:`~.chaos.CHAOS.synth_coeffs_sm`.
34 |
35 | With the latest version of the CHAOS model, it is recommended to use the latest
36 | version of the RC-index that can be downloaded as TXT-file (``*.dat``) from the
37 | RC website at :rc_url:`spacecenter.dk <>`, or by using the function
38 | :func:`~.data_utils.save_RC_h5file` (locally saves the RC-index TXT-file as
39 | HDF5-file):
40 |
41 | .. code-block:: python
42 |
43 | import chaosmagpy as cp
44 |
45 | cp.data_utils.save_RC_h5file('my_RC_file.h5')
46 |
47 | There is no significant difference in speed when using TXT-file or HDF5-file
48 | formats in this case. After importing ChaosMagPy, provide the path to the new
49 | RC-index file:
50 |
51 | .. code-block:: python
52 |
53 | cp.basicConfig['file.RC_index'] = './my_RC_file.h5'
54 |
55 | This should be done at the top of the script after the import statements,
56 | otherwise ChaosMagPy uses the builtin RC-index file.
57 |
58 | If you use are using an older version of CHAOS-7, and are interested in the
59 | external field part of that model, it is recommended to use the RC file from
60 | the archive at the bottom of the :chaos_url:`CHAOS-7 website <>` associated
61 | with the specific version of CHAOS-7 that you are using.
62 |
63 | Save and load custom configuration
64 | ----------------------------------
65 |
66 | The configuration values can also be read from and written to a simple text
67 | file in json format.
68 |
69 | For the correct format, it is best to change the configuration parameters
70 | during a python session and then save them to a file. For example, the
71 | following code sets the Earth's surface radius to 6371 km (there is no reason
72 | to do this except for the sake of this example):
73 |
74 | .. code-block:: python
75 |
76 | import chaosmagpy as cp
77 |
78 | cp.basicConfig['params.r_surf'] = 6371
79 |
80 | Then save the configuration dictionary to a file:
81 |
82 | .. code-block:: python
83 |
84 | cp.basicConfig.save('myconfig.json')
85 |
86 | To load this configuration file, use the following at the start of the script:
87 |
88 | .. code-block:: python
89 |
90 | cp.basicConfig.load('myconfig.json')
91 |
--------------------------------------------------------------------------------
/docs/source/index.rst:
--------------------------------------------------------------------------------
1 | .. ChaosMagPy documentation master file, created by
2 | sphinx-quickstart on Thu Apr 19 20:57:12 2018.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | ChaosMagPy documentation
7 | ========================
8 |
9 | .. toctree::
10 | :maxdepth: 2
11 |
12 | readme
13 | installation
14 | references
15 | usage
16 | gallery/index
17 | configuration
18 | changelog
19 | license
20 |
21 | Indices and tables
22 | ==================
23 |
24 | * :ref:`genindex`
25 | * :ref:`modindex`
26 | * :ref:`search`
27 |
--------------------------------------------------------------------------------
/docs/source/installation.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../../INSTALL.rst
2 |
--------------------------------------------------------------------------------
/docs/source/license.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../../LICENSE
2 |
--------------------------------------------------------------------------------
/docs/source/readme.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../../README.rst
2 |
3 | .. include:: ../../CITATION
4 |
--------------------------------------------------------------------------------
/docs/source/references.rst:
--------------------------------------------------------------------------------
1 | API References
2 | ==============
3 |
4 | The ChaosMagPy package consists of several modules, which contain classes and
5 | functions that are relevant in geomagnetic field modelling:
6 |
7 | * **chaosmagpy.chaos**: Everything related to loading specific geomagnetic
8 | field models, such as CHAOS, Cov-Obs, and other piecewise-polynomial
9 | spherical harmonic models (see Sect. `Core Functionality`_).
10 | * **chaosmagpy.coordinate_utils**: Everything related to coordinate
11 | transformations and change of reference frames
12 | (see Sect. `Coordinate Transformations`_).
13 | * **chaosmagpy.model_utils**: Everything related to evaluating geomagnetic
14 | field models, which includes functions for evaluating B-splines, Legendre
15 | polynomials and spherical harmonics (see Sect. `Model Utilities`_)
16 | * **chaosmagpy.plot_utils**: Everything related to plotting geomagnetic field
17 | model outputs, i.e. plotting of timeseries, maps and spatial power
18 | spectra (see Sect. `Plotting Utilities`_).
19 | * **chaosmagpy.data_utils**: Everything related to loading and saving datasets
20 | and model coefficients from and to files. This also includes functions for
21 | transforming datetime formats (see Sect. `Data Utilities`_).
22 | * **chaosmagpy.config_utils**: Everything related to the configuration of
23 | ChaosMagPy, which includes setting parameters and paths to builtin data files
24 | (see Sect. `Configuration Utilities`_).
25 |
26 | Core Functionality
27 | ------------------
28 |
29 | .. automodule:: chaosmagpy.chaos
30 | :noindex:
31 | :no-members:
32 |
33 | Coordinate Transformations
34 | --------------------------
35 |
36 | .. automodule:: chaosmagpy.coordinate_utils
37 | :noindex:
38 | :no-members:
39 |
40 | Model Utilities
41 | ---------------
42 |
43 | .. automodule:: chaosmagpy.model_utils
44 | :noindex:
45 | :no-members:
46 |
47 | Plotting Utilities
48 | ------------------
49 |
50 | .. automodule:: chaosmagpy.plot_utils
51 | :noindex:
52 | :no-members:
53 |
54 | Data Utilities
55 | --------------
56 |
57 | .. automodule:: chaosmagpy.data_utils
58 | :noindex:
59 | :no-members:
60 |
61 | .. _sec-configuration-utilities:
62 |
63 | Configuration Utilities
64 | -----------------------
65 |
66 | .. automodule:: chaosmagpy.config_utils
67 | :noindex:
68 | :no-members:
69 |
--------------------------------------------------------------------------------
/docs/source/usage.rst:
--------------------------------------------------------------------------------
1 | .. _sec-usage:
2 |
3 | Usage
4 | =====
5 |
6 | Here are some simple examples on how to use the package. This only requires a
7 | CHAOS model MAT-file, e.g. "CHAOS-6-x7.mat" in the current working directory.
8 | The model coefficients of the latest CHAOS model can be
9 | downloaded :chaos_url:`here <>`.
10 |
11 | Computing the field components on a grid
12 | ----------------------------------------
13 |
14 | Use ChaosMagPy to compute the magnetic field components of the different
15 | sources that are accounted for in the model. For example, the time-dependent
16 | internal field:
17 |
18 | .. code-block:: python
19 |
20 | import numpy as np
21 | import chaosmagpy as cp
22 |
23 | # create full grid
24 | radius = 3485. # km, core-mantle boundary
25 | theta = np.linspace(0., 180., num=181) # colatitude in degrees
26 | phi = np.linspace(-180., 180., num=361) # longitude in degrees
27 |
28 | phi_grid, theta_grid = np.meshgrid(phi, theta)
29 | radius_grid = radius*np.ones(phi_grid.shape)
30 |
31 | time = cp.data_utils.mjd2000(2000, 1, 1) # modified Julian date
32 |
33 | # create a "model" instance by loading the CHAOS model from a mat-file
34 | model = cp.load_CHAOS_matfile('CHAOS-6-x7.mat')
35 |
36 | # compute field components on the grid using the method "synth_values_tdep"
37 | B_radius, B_theta, B_phi = model.synth_values_tdep(time, radius_grid, theta_grid, phi_grid)
38 |
39 | When using a *regular* grid, consider ``grid=True`` option for
40 | speed. It will internally compute a grid in ``theta`` and ``phi`` similar to
41 | :func:`numpy.meshgrid` in the example above while saving time with some of the
42 | computations (note the usage of, for example, ``theta`` instead of
43 | ``theta_grid``):
44 |
45 | .. code-block:: python
46 |
47 | B_radius, B_theta, B_phi = model.synth_values_tdep(time, radius, theta, phi, grid=True)
48 |
49 | The same computation can be done with other sources described by the model:
50 |
51 | +----------+-----------------+---------------------------------------------------+
52 | | Source | Type | Method in :class:`~.CHAOS` class |
53 | +==========+=================+===================================================+
54 | | internal | time-dependent | :meth:`~.CHAOS.synth_values_tdep` (see example) |
55 | + +-----------------+---------------------------------------------------+
56 | | | static | :meth:`~.CHAOS.synth_values_static` |
57 | +----------+-----------------+---------------------------------------------------+
58 | | external | time-dep. (GSM) | :meth:`~.CHAOS.synth_values_gsm` |
59 | + +-----------------+---------------------------------------------------+
60 | | | time-dep. (SM) | :meth:`~.CHAOS.synth_values_sm` |
61 | +----------+-----------------+---------------------------------------------------+
62 |
63 | Computing timeseries of Gauss coefficients
64 | ------------------------------------------
65 |
66 | ChaosMagPy can also be used to synthesize a timeseries of the spherical
67 | harmonic coefficients. For example, in the case of the time-dependent
68 | internal field:
69 |
70 | .. code-block:: python
71 |
72 | import numpy as np
73 | import chaosmagpy as cp
74 |
75 | # load the CHAOS model from the mat-file
76 | model = cp.load_CHAOS_matfile('CHAOS-6-x7.mat')
77 |
78 | print('Model timespan is:', model.model_tdep.breaks[[0, -1]])
79 |
80 | # create vector of time points in modified Julian date from 2000 to 2004
81 | time = np.linspace(0., 4*365.25, 10) # 10 equally-spaced time instances
82 |
83 | # compute the Gauss coefficients of the MF, SV and SA of the internal field
84 | coeffs_MF = model.synth_coeffs_tdep(time, nmax=13, deriv=0) # shape: (10, 195)
85 | coeffs_SV = model.synth_coeffs_tdep(time, nmax=14, deriv=1) # shape: (10, 224)
86 | coeffs_SA = model.synth_coeffs_tdep(time, nmax=9, deriv=2) # shape: (10, 99)
87 |
88 | # save time and coefficients to a txt-file: each column starts with the time
89 | # point in decimal years followed by the Gauss coefficients in
90 | # natural order, i.e. g(n,m): g(1,0), g(1, 1), h(1, 1), ...
91 |
92 | dyear = cp.data_utils.mjd_to_dyear(time) # convert mjd2000 to decimal year
93 |
94 | np.savetxt('MF.txt', np.concatenate([dyear[None, :], coeffs_MF.T]), fmt='%10.5f', delimiter=' ')
95 | np.savetxt('SV.txt', np.concatenate([dyear[None, :], coeffs_SV.T]), fmt='%10.5f', delimiter=' ')
96 | np.savetxt('SA.txt', np.concatenate([dyear[None, :], coeffs_SA.T]), fmt='%10.5f', delimiter=' ')
97 |
98 | The same can be done with other sources accounted for in CHAOS. However, except
99 | for the time-dependent internal field, there are no time derivatives available.
100 |
101 | +----------+-----------------+---------------------------------------------------+
102 | | Source | Type | Method in :class:`~.CHAOS` class |
103 | +==========+=================+===================================================+
104 | | internal | time-dependent | :meth:`~.CHAOS.synth_coeffs_tdep` (see example) |
105 | + +-----------------+---------------------------------------------------+
106 | | | static | :meth:`~.CHAOS.synth_coeffs_static` |
107 | +----------+-----------------+---------------------------------------------------+
108 | | external | time-dep. (GSM) | :meth:`~.CHAOS.synth_coeffs_gsm` |
109 | + +-----------------+---------------------------------------------------+
110 | | | time-dep. (SM) | :meth:`~.CHAOS.synth_coeffs_sm` |
111 | +----------+-----------------+---------------------------------------------------+
112 |
113 | Converting time formats in ChaosMagPy
114 | -------------------------------------
115 |
116 | The models in ChaosMagPy only accept modified Julian date. But
117 | sometimes it is easier to work in different units such as decimal years or
118 | Numpy's datetime. For those cases, ChaosMagPy offers simple conversion
119 | functions. First, import ChaosMagPy and Numpy:
120 |
121 | .. code-block:: python
122 |
123 | import chaosmagpy as cp
124 | import numpy as np
125 |
126 | From Modified Julian Dates
127 | ^^^^^^^^^^^^^^^^^^^^^^^^^^
128 |
129 | Convert to decimal years (account for leap years) with
130 | :func:`chaosmagpy.data_utils.mjd_to_dyear`:
131 |
132 | >>> cp.data_utils.mjd_to_dyear(412.)
133 | 2001.1260273972603
134 |
135 | Convert to Numpy's datetime object with
136 | :func:`chaosmagpy.data_utils.timestamp`:
137 |
138 | >>> cp.data_utils.timestamp(412.)
139 | numpy.datetime64('2001-02-16T00:00:00.000000')
140 |
141 | To Modified Julian Dates
142 | ^^^^^^^^^^^^^^^^^^^^^^^^
143 |
144 | Convert from decimal years (account for leap years) with
145 | :func:`chaosmagpy.data_utils.dyear_to_mjd`:
146 |
147 | >>> cp.data_utils.dyear_to_mjd(2001.25)
148 | 457.25
149 |
150 | Convert from Numpy's datetime object with
151 | :func:`chaosmagpy.data_utils.mjd2000`:
152 |
153 | >>> cp.data_utils.mjd2000(np.datetime64('2001-02-01T12:00:00'))
154 | 397.5
155 |
156 | Note also that :func:`chaosmagpy.data_utils.mjd2000` (click to see
157 | documentation) accepts a wide range of inputs. You can also give the date in
158 | terms of integers for the year, month, and so on:
159 |
160 | >>> cp.data_utils.mjd2000(2002, 1, 19, 15) # 2002-01-19 15:00:00
161 | 749.625
162 |
163 | Plotting maps of the time-dependent internal field
164 | --------------------------------------------------
165 |
166 | Here, we make a map of the first time-derivative of the time-dependent internal
167 | part of the model. We will plot it on the surface at 3485 km (core-mantle
168 | boundary) from the center of Earth and on January 1, 2000:
169 |
170 | .. code-block:: python
171 |
172 | import chaosmagpy as cp
173 |
174 | model = cp.load_CHAOS_matfile('CHAOS-6-x7.mat')
175 |
176 | radius = 3485.0 # km, here core-mantle boundary
177 | time = 0.0 # mjd2000, here Jan 1, 2000 0:00 UTC
178 |
179 | model.plot_maps_tdep(time, radius, nmax=16, deriv=1) # plots the SV up to degree 16
180 |
181 | .. figure:: .static/plot_maps_tdep.png
182 | :align: center
183 |
184 | Secular variation at the core-mantle-boundary up to degree 16 in
185 | January 1, 2000 0:00 UTC.
186 |
187 | Save Gauss coefficients of the time-dependent internal (i.e. large-scale core)
188 | field in shc-format to a file:
189 |
190 | .. code-block:: python
191 |
192 | model.save_shcfile('CHAOS-6-x7_tdep.shc', model='tdep')
193 |
194 | Plotting maps of the static internal field
195 | ------------------------------------------
196 |
197 | Similarly, the static internal (i.e. small-scale crustal) part of the model can
198 | be plotted on a map:
199 |
200 | .. code-block:: python
201 |
202 | import chaosmagpy as cp
203 |
204 | model = cp.load_CHAOS_matfile('CHAOS-6-x7.mat')
205 | model.plot_maps_static(radius=6371.2, nmax=85)
206 |
207 | .. figure:: .static/plot_maps_static.png
208 | :align: center
209 |
210 | Static internal small-scale field at Earth's surface up to degree 85.
211 |
212 | and saved
213 |
214 | .. code-block:: python
215 |
216 | model.save_shcfile('CHAOS-6-x7_static.shc', model='static')
217 |
--------------------------------------------------------------------------------
/environment.yml:
--------------------------------------------------------------------------------
1 | dependencies:
2 | - python >= 3.6
3 | - numpy <= 1.26
4 | - numpydoc
5 | - scipy
6 | - pandas
7 | - cython
8 | - h5py
9 | - pyshp >= 2.3.1
10 | - lxml
11 | - matplotlib >= 3.6
12 | - cartopy
13 | - sphinx
14 | - sphinx-copybutton
15 | - build
16 | - flake8
17 | - pip
18 | - pip:
19 | - cdflib
20 | - hdf5storage >= 0.1.17
21 | - sphinx-gallery
22 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools>=61.0"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [project]
6 | name = "chaosmagpy"
7 | dynamic = ["version", "readme"]
8 | dependencies = [
9 | "numpy>=2",
10 | "scipy",
11 | "pandas",
12 | "Cython",
13 | "h5py",
14 | "pyshp>=2.3.1",
15 | "hdf5storage>=0.2"
16 | ]
17 | requires-python = ">=3.6"
18 | authors = [
19 | {name = "Clemens Kloss", email = "ancklo@space.dtu.dk"}
20 | ]
21 | maintainers = [
22 | {name = "Clemens Kloss", email = "ancklo@space.dtu.dk"}
23 | ]
24 | description = "Evaluates the CHAOS geomagnetic field model and other models of Earth's magnetic field."
25 | license = {file = "LICENSE"}
26 | keywords = [
27 | "CHAOS",
28 | "geomagnetic field",
29 | "spherical harmonics model",
30 | "secular variation",
31 | "core field",
32 | "crustal field",
33 | "magnetospheric field"
34 | ]
35 | classifiers = [
36 | "Intended Audience :: Science/Research",
37 | "License :: OSI Approved :: MIT License",
38 | "Operating System :: Unix",
39 | "Operating System :: POSIX",
40 | "Operating System :: Microsoft :: Windows",
41 | "Programming Language :: Python :: 3.6",
42 | "Programming Language :: Python :: 3.7",
43 | "Programming Language :: Python :: 3.8",
44 | "Programming Language :: Python :: 3.9",
45 | "Programming Language :: Python :: 3.10",
46 | "Programming Language :: Python :: 3.11",
47 | "Topic :: Scientific/Engineering :: Physics"
48 | ]
49 |
50 | [project.optional-dependencies]
51 | full = [
52 | "matplotlib>=3.6",
53 | "lxml>=4.3.4"
54 | ]
55 |
56 | [project.urls]
57 | Homepage = "https://github.com/ancklo/ChaosMagPy"
58 | Documentation = "https://chaosmagpy.readthedocs.io/en/"
59 | Repository = "https://github.com/ancklo/ChaosMagPy.git"
60 | Issues = "https://github.com/ancklo/ChaosMagPy/issues"
61 | Changelog = "https://github.com/ancklo/ChaosMagPy/blob/master/CHANGELOG.rst"
62 |
63 | [tool.setuptools.packages.find]
64 | include = ["chaosmagpy*"]
65 | exclude = ["data*"]
66 | namespaces = true
67 |
68 | [tool.setuptools.package-data]
69 | "chaosmagpy.lib" = ["*"]
70 |
71 | [tool.setuptools.dynamic]
72 | version = {attr = "chaosmagpy.__version__"}
73 | readme = {file = ["README.rst", "CITATION", "INSTALL.rst"], content-type = "text/x-rst"}
74 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | numpy>=1.15
2 | scipy>=1.1
3 | pandas>=0.23
4 | Cython>=0.28
5 | h5py>=2.8.0
6 | pyshp>=2.3.1
7 | matplotlib>=3.6
8 | hdf5storage>=0.1.17
9 | lxml>=4.3.4
10 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | try:
2 | from setuptools import setup, find_packages
3 | have_setuptools = True
4 | except ImportError:
5 | from distutils.core import setup
6 | have_setuptools = False
7 |
8 | if __name__ == '__main__':
9 | setup()
10 |
--------------------------------------------------------------------------------
/test_package.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # need to build the package first
3 | # run script from conda environment
4 | if [ -f "$HOME/miniconda3/etc/profile.d/conda.sh" ]; then
5 | . "$HOME/miniconda3/etc/profile.d/conda.sh"
6 | else
7 | export PATH="$HOME/miniconda3/bin:$PATH"
8 | fi
9 |
10 | # extract from __init__.py on line with __version__ the expr between ""
11 | latest=$(grep __version__ chaosmagpy/__init__.py | sed 's/.*"\(.*\)".*/\1/')
12 |
13 | read -p "Enter version [$latest]: " version
14 | version=${version:-$latest}
15 |
16 | env_name=Test_$version
17 |
18 | printf "\n%s %s %s %s\n\n" -------Test ChaosMagPy Version $version-------
19 |
20 | # echo Setting up fresh conda environment.
21 | conda env create --name $env_name -f environment.yml
22 |
23 | conda activate $env_name
24 |
25 | conda env list
26 |
27 | while true; do
28 | read -p "Install ChaosMagPy in the starred environment (y/n)?: " yn
29 | case $yn in
30 | [Yy]* ) break;;
31 | [Nn]* ) echo Abort.; exit;;
32 | * ) echo "Please answer yes or no.";;
33 | esac
34 | done
35 |
36 | echo Installing requested version of ChaosMagPy v$version.
37 | pip install dist/chaosmagpy-$version.tar.gz --dry-run
38 | pip install dist/chaosmagpy-$version-py3-none-any.whl
39 |
40 | echo Entering test directory.
41 | cd tests
42 |
43 | echo python -m unittest test_chaos
44 | python -m unittest test_chaos
45 |
46 | echo python -m unittest test_coordinate_utils
47 | python -m unittest test_coordinate_utils
48 |
49 | echo python -m unittest test_model_utils
50 | python -m unittest test_model_utils
51 |
52 | echo python -m unittest test_data_utils
53 | python -m unittest test_data_utils
54 |
55 | conda activate base
56 | conda remove --name $env_name --all
57 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ancklo/ChaosMagPy/f91dba5cca25e4d110611dcd62dda881e27ed710/tests/__init__.py
--------------------------------------------------------------------------------
/tests/helpers.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2024 Clemens Kloss
2 | #
3 | # This file is part of ChaosMagPy.
4 | #
5 | # ChaosMagPy is released under the MIT license. See LICENSE in the root of the
6 | # repository for full licensing details.
7 |
8 | import hdf5storage as hdf
9 |
10 |
11 | def load_matfile(filepath, test_name):
12 | """
13 | Load matfile and return dictionary of MATLAB outputs for comparing with
14 | Python outputs.
15 |
16 | Parameters
17 | ----------
18 | filepath : str
19 | Filepath to mat-file.
20 | test_name : str
21 | Name of test which is stored as structure in mat-file.
22 |
23 | Returns
24 | -------
25 | test : dict
26 | Dictionary containing MATLAB output.
27 | """
28 |
29 | mat_contents = hdf.loadmat(str(filepath), variable_names=[str(test_name)])
30 | test = mat_contents[str(test_name)]
31 |
32 | if test.ndim == 2:
33 | return test[0, 0] # before v7.3
34 | else:
35 | return test[0] # hdf5 compatible v7.3
36 |
--------------------------------------------------------------------------------
/tests/test_chaos.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2024 Clemens Kloss
2 | #
3 | # This file is part of ChaosMagPy.
4 | #
5 | # ChaosMagPy is released under the MIT license. See LICENSE in the root of the
6 | # repository for full licensing details.
7 |
8 | import os
9 | import numpy as np
10 | import textwrap
11 | import chaosmagpy as cp
12 | import matplotlib.pyplot as plt
13 | from unittest import TestCase, main
14 |
15 | try:
16 | from tests.helpers import load_matfile
17 | except ImportError:
18 | from helpers import load_matfile
19 |
20 | R_REF = 6371.2 # reference radius in km
21 | ROOT = os.path.abspath(os.path.dirname(__file__))
22 | MATFILE_PATH = os.path.join(ROOT, 'data/CHAOS_test.mat')
23 | CHAOS_PATH = os.path.join(ROOT, 'data/CHAOS-7.18.mat')
24 | CHAOS_PATH_SHC = os.path.join(ROOT, 'data/CHAOS-7.18_core.shc')
25 |
26 | # check if mat-file exists in tests directory
27 | if os.path.isfile(MATFILE_PATH) is False:
28 | MATFILE_PATH = str(input('Matfile path for chaosmagpy test?: '))
29 |
30 |
31 | class Chaos(TestCase):
32 | def setUp(self):
33 |
34 | print(textwrap.dedent(f"""\
35 |
36 | {"":-^70}
37 | Running {self._testMethodName}:
38 | """))
39 |
40 | def test_synth_euler_angles(self):
41 |
42 | model = cp.load_CHAOS_matfile(CHAOS_PATH)
43 |
44 | test = load_matfile(MATFILE_PATH, 'test_synth_euler_angles')
45 | time = np.squeeze(test['time'])
46 |
47 | swarm_c = model.synth_euler_angles(time, 'swarm_c')
48 |
49 | self.assertIsNone(np.testing.assert_allclose(
50 | swarm_c, test['swarm_c']))
51 |
52 | def test_load_CHAOS(self):
53 |
54 | cp.load_CHAOS_matfile(CHAOS_PATH)
55 | cp.load_CHAOS_shcfile(CHAOS_PATH_SHC)
56 | cp.chaos.BaseModel.from_shc(CHAOS_PATH_SHC)
57 |
58 | def test_save_matfile(self):
59 |
60 | seq = np.random.randint(0, 10, size=(5,))
61 | filename = 'CHAOS-tmp_' + ''.join([str(a) for a in seq]) + '.mat'
62 | filepath = os.path.join(ROOT, filename)
63 |
64 | model = cp.load_CHAOS_matfile(CHAOS_PATH)
65 | print(' ', end='') # indent line by two withespaces
66 | model.save_matfile(filepath)
67 |
68 | def test(x, y):
69 | if isinstance(x, str) or isinstance(y, str):
70 | # convert unicode to str
71 | np.testing.assert_string_equal(str(x), str(y))
72 | else:
73 | np.testing.assert_allclose(x, y, atol=1e-10)
74 |
75 | chaos = cp.data_utils.load_matfile(CHAOS_PATH, variable_names=[
76 | 'pp', 'model_ext', 'model_Euler', 'g'])
77 | chaos_out = cp.data_utils.load_matfile(filepath, variable_names=[
78 | 'pp', 'model_ext', 'model_Euler', 'g'])
79 |
80 | pp = chaos['pp']
81 | pp_out = chaos_out['pp']
82 |
83 | for key in ['order', 'dim', 'pieces', 'form', 'coefs', 'breaks']:
84 | test(pp[key], pp_out[key])
85 |
86 | test(chaos['g'], chaos_out['g'])
87 |
88 | model_ext = chaos['model_ext']
89 | model_ext_out = chaos_out['model_ext']
90 |
91 | for key in ['m_Dst', 'm_gsm', 'm_sm', 'q10', 'qs11', 't_break_q10',
92 | 't_break_qs11']:
93 | test(model_ext[key], model_ext_out[key])
94 |
95 | model_Euler = chaos['model_Euler']
96 | model_Euler_out = chaos_out['model_Euler']
97 |
98 | for key in ['alpha', 'beta', 'gamma', 't_break_Euler']:
99 | var = model_Euler[key]
100 | var_out = model_Euler_out[key]
101 | test(var.shape, var_out.shape)
102 |
103 | for value, value_out in zip(np.ravel(var), np.ravel(var_out)):
104 | test(value, value_out)
105 |
106 | print(f" Removing file {filepath}")
107 | os.remove(filepath)
108 |
109 | def test_save_shcfile(self):
110 |
111 | model = cp.load_CHAOS_matfile(CHAOS_PATH)
112 |
113 | filepath = os.path.join(ROOT, 'CHAOS_tdep.shc')
114 | model.save_shcfile(filepath, model='tdep')
115 | model2 = cp.load_CHAOS_shcfile(filepath)
116 |
117 | coeffs_tdep_mat = model.model_tdep.coeffs
118 | coeffs_tdep_shc = model2.model_tdep.coeffs
119 |
120 | np.testing.assert_allclose(coeffs_tdep_shc, coeffs_tdep_mat, atol=1e-8)
121 |
122 | print(f"Removing file {filepath}")
123 | os.remove(filepath)
124 |
125 | # static internal field
126 |
127 | filepath = os.path.join(ROOT, 'CHAOS_static.shc')
128 | model.save_shcfile(filepath, model='static')
129 | model2 = cp.load_CHAOS_shcfile(filepath)
130 |
131 | coeffs_stat_mat = model.model_static.coeffs
132 | coeffs_stat_shc = model2.model_static.coeffs
133 |
134 | np.testing.assert_allclose(coeffs_stat_shc, coeffs_stat_mat, atol=1e-8)
135 |
136 | print(f"Removing file {filepath}")
137 | os.remove(filepath)
138 |
139 | def test_io_shcfile(self):
140 |
141 | # piecewise constant, one piece
142 |
143 | order = 1
144 | breaks = [0., 10.]
145 | knots = cp.model_utils.augment_breaks(breaks, order)
146 |
147 | model = cp.chaos.BaseModel.from_bspline(
148 | name='BaseModel',
149 | knots=knots,
150 | coeffs=np.random.uniform(-100., 100., 3).reshape((1, 3)),
151 | order=order
152 | )
153 |
154 | filepath = os.path.join(ROOT, 'BaseModel_order1_piece1.shc')
155 | model.to_shc(filepath)
156 | model2 = cp.chaos.BaseModel.from_shc(filepath)
157 |
158 | np.testing.assert_allclose(model.coeffs, model2.coeffs)
159 |
160 | print(f"Removing file {filepath}")
161 | os.remove(filepath)
162 |
163 | # piecewise constant, multiple pieces
164 |
165 | order = 1
166 | breaks = [0., 10., 30., 52., 79., 100.] # 5 pieces
167 | knots = cp.model_utils.augment_breaks(breaks, order)
168 |
169 | model = cp.chaos.BaseModel.from_bspline(
170 | name='BaseModel',
171 | knots=knots,
172 | coeffs=np.random.uniform(-100., 100., 15).reshape((5, 3)),
173 | order=order
174 | )
175 |
176 | filepath = os.path.join(ROOT, 'BaseModel_order1_piece5.shc')
177 | model.to_shc(filepath)
178 | model2 = cp.chaos.BaseModel.from_shc(filepath)
179 |
180 | np.testing.assert_allclose(model.coeffs, model2.coeffs)
181 |
182 | print(f"Removing file {filepath}")
183 | os.remove(filepath)
184 |
185 | # piecewise linear, one piece
186 |
187 | order = 2
188 | breaks = [0., 10.]
189 | knots = cp.model_utils.augment_breaks(breaks, order)
190 |
191 | model = cp.chaos.BaseModel.from_bspline(
192 | name='BaseModel',
193 | knots=knots,
194 | coeffs=np.random.uniform(-100., 100., 6).reshape((2, 3)),
195 | order=order
196 | )
197 |
198 | filepath = os.path.join(ROOT, 'BaseModel_order2_piece1.shc')
199 | model.to_shc(filepath)
200 | model2 = cp.chaos.BaseModel.from_shc(filepath)
201 |
202 | np.testing.assert_allclose(model.coeffs, model2.coeffs)
203 |
204 | print(f"Removing file {filepath}")
205 | os.remove(filepath)
206 |
207 | # piecewise linear, multiple pieces
208 |
209 | order = 2
210 | breaks = [0., 10., 30., 52., 79., 100.]
211 | knots = cp.model_utils.augment_breaks(breaks, order)
212 |
213 | model = cp.chaos.BaseModel.from_bspline(
214 | name='BaseModel',
215 | knots=knots,
216 | coeffs=np.random.uniform(-100., 100., 18).reshape((6, 3)),
217 | order=order
218 | )
219 |
220 | filepath = os.path.join(ROOT, 'BaseModel_order2_piece5.shc')
221 | model.to_shc(filepath)
222 | model2 = cp.chaos.BaseModel.from_shc(filepath)
223 |
224 | np.testing.assert_allclose(model.coeffs, model2.coeffs, atol=2e-5)
225 |
226 | print(f"Removing file {filepath}")
227 | os.remove(filepath)
228 |
229 | # higher order B-spline, one pieces
230 |
231 | order = 3
232 | breaks = [0., 10.]
233 | knots = cp.model_utils.augment_breaks(breaks, order)
234 |
235 | model = cp.chaos.BaseModel.from_bspline(
236 | name='BaseModel',
237 | knots=knots,
238 | coeffs=np.random.uniform(-100., 100., 9).reshape((3, 3)),
239 | order=order
240 | )
241 |
242 | filepath = os.path.join(ROOT, 'BaseModel_order3_piece1.shc')
243 | model.to_shc(filepath)
244 | model2 = cp.chaos.BaseModel.from_shc(filepath)
245 |
246 | np.testing.assert_allclose(model.coeffs, model2.coeffs, atol=2e-5)
247 |
248 | print(f"Removing file {filepath}")
249 | os.remove(filepath)
250 |
251 | # higher order B-spline, multiple pieces
252 |
253 | order = 3
254 | breaks = [0., 10., 30., 52., 79., 100.]
255 | knots = cp.model_utils.augment_breaks(breaks, order)
256 |
257 | model = cp.chaos.BaseModel.from_bspline(
258 | name='BaseModel',
259 | knots=knots,
260 | coeffs=np.random.uniform(-100., 100., 21).reshape((7, 3)),
261 | order=order
262 | )
263 |
264 | filepath = os.path.join(ROOT, 'BaseModel_order3_piece5.shc')
265 | model.to_shc(filepath)
266 | model2 = cp.chaos.BaseModel.from_shc(filepath)
267 |
268 | np.testing.assert_allclose(model.coeffs, model2.coeffs, atol=2e-5)
269 |
270 | print(f"Removing file {filepath}")
271 | os.remove(filepath)
272 |
273 | def test_complete_forward(self):
274 |
275 | n_data = int(300)
276 | t_start = -200.0
277 | t_end = 6000.0
278 | time = np.linspace(t_start, t_end, num=n_data)
279 | radius = R_REF * np.ones(time.shape)
280 | theta = np.linspace(1, 179, num=n_data)
281 | phi = np.linspace(-180, 179, num=n_data)
282 |
283 | model = cp.load_CHAOS_matfile(CHAOS_PATH)
284 |
285 | B_radius, B_theta, B_phi = model(time, radius, theta, phi)
286 |
287 | # load matfile
288 | test = load_matfile(MATFILE_PATH, 'test_complete_forward')
289 |
290 | B_radius_mat = np.ravel(test['B_radius'])
291 | B_theta_mat = np.ravel(test['B_theta'])
292 | B_phi_mat = np.ravel(test['B_phi'])
293 |
294 | for component in ['B_radius', 'B_theta', 'B_phi']:
295 | res = np.abs(eval(component) - eval('_'.join((component, 'mat'))))
296 | print(' -------------------')
297 | print(f' {component}:')
298 | print(' MAE =', np.mean(res), 'nT')
299 | print(' RMSE =', np.sqrt(np.mean(res**2)), 'nT')
300 | print(' Max Error =', np.amax(res), 'nT')
301 | print(' Min Error =', np.amin(res), 'nT')
302 |
303 | np.testing.assert_allclose(
304 | B_radius, B_radius_mat, rtol=1e-7, atol=1e-2)
305 | np.testing.assert_allclose(
306 | B_theta, B_theta_mat, rtol=1e-7, atol=1e-2)
307 | np.testing.assert_allclose(
308 | B_phi, B_phi_mat, rtol=1e-7, atol=1e-2)
309 |
310 | def test_surface_field(self):
311 |
312 | model = cp.load_CHAOS_matfile(CHAOS_PATH)
313 |
314 | time = 2015.
315 | radius = R_REF
316 | theta = np.linspace(1, 179, num=180)
317 | phi = np.linspace(-180, 179, num=360)
318 |
319 | B_radius, B_theta, B_phi = model.synth_values_tdep(
320 | time, radius, theta, phi, nmax=13, grid=True, deriv=0)
321 |
322 | B_radius = np.ravel(B_radius, order='F') # ravel column-major
323 | B_theta = np.ravel(B_theta, order='F')
324 | B_phi = np.ravel(B_phi, order='F')
325 |
326 | # load matfile
327 | test = load_matfile(MATFILE_PATH, 'test_surface_field')
328 |
329 | B_radius_mat = np.ravel(test['B_radius'])
330 | B_theta_mat = np.ravel(test['B_theta'])
331 | B_phi_mat = np.ravel(test['B_phi'])
332 |
333 | for component in ['B_radius', 'B_theta', 'B_phi']:
334 | res = np.abs(eval(component) - eval('_'.join((component, 'mat'))))
335 | print(' -------------------')
336 | print(f' {component}:')
337 | print(' MAE =', np.mean(res), 'nT')
338 | print(' RMSE =', np.sqrt(np.mean(res**2)), 'nT')
339 | print(' Max Error =', np.amax(res), 'nT')
340 | print(' Min Error =', np.amin(res), 'nT')
341 |
342 | self.assertIsNone(np.testing.assert_allclose(
343 | B_radius, B_radius_mat))
344 | self.assertIsNone(np.testing.assert_allclose(
345 | B_theta, B_theta_mat))
346 | self.assertIsNone(np.testing.assert_allclose(
347 | B_phi, B_phi_mat))
348 |
349 | def test_mf_timeseries(self):
350 |
351 | # some observatory location
352 | radius = R_REF*0.35
353 | theta = 90-14.308
354 | phi = -16.950
355 |
356 | model = cp.load_CHAOS_matfile(CHAOS_PATH)
357 |
358 | time = np.linspace(model.model_tdep.breaks[0],
359 | model.model_tdep.breaks[-1], num=1000)
360 |
361 | B_radius, B_theta, B_phi = model.synth_values_tdep(
362 | time, radius, theta, phi, nmax=14, deriv=0)
363 |
364 | # load matfile
365 | test = load_matfile(MATFILE_PATH, 'test_mf_timeseries')
366 |
367 | B_radius_mat = np.ravel(test['B_radius'])
368 | B_theta_mat = np.ravel(test['B_theta'])
369 | B_phi_mat = np.ravel(test['B_phi'])
370 |
371 | for component in ['B_radius', 'B_theta', 'B_phi']:
372 | res = np.abs(eval(component) - eval('_'.join((component, 'mat'))))
373 | print(' -------------------')
374 | print(f' {component}:')
375 | print(' MAE =', np.mean(res), 'nT')
376 | print(' RMSE =', np.sqrt(np.mean(res**2)), 'nT')
377 | print(' Max Error =', np.amax(res), 'nT')
378 | print(' Min Error =', np.amin(res), 'nT')
379 |
380 | np.testing.assert_allclose(B_radius, B_radius_mat)
381 | np.testing.assert_allclose(B_theta, B_theta_mat)
382 | np.testing.assert_allclose(B_phi, B_phi_mat)
383 |
384 | def test_sv_timeseries(self):
385 |
386 | # some observatory location
387 | radius = R_REF*0.35
388 | theta = 90-14.308
389 | phi = -16.950
390 |
391 | model = cp.load_CHAOS_matfile(CHAOS_PATH)
392 |
393 | time = np.linspace(model.model_tdep.breaks[0],
394 | model.model_tdep.breaks[-1], num=1000)
395 |
396 | B_radius, B_theta, B_phi = model.synth_values_tdep(
397 | time, radius, theta, phi, nmax=16, deriv=1)
398 |
399 | # load matfile
400 | test = load_matfile(MATFILE_PATH, 'test_sv_timeseries')
401 |
402 | B_radius_mat = np.ravel(test['B_radius'])
403 | B_theta_mat = np.ravel(test['B_theta'])
404 | B_phi_mat = np.ravel(test['B_phi'])
405 |
406 | for component in ['B_radius', 'B_theta', 'B_phi']:
407 | res = np.abs(eval(component) - eval('_'.join((component, 'mat'))))
408 | print(' -------------------')
409 | print(f' {component}:')
410 | print(' MAE =', np.mean(res), 'nT')
411 | print(' RMSE =', np.sqrt(np.mean(res**2)), 'nT')
412 | print(' Max Error =', np.amax(res), 'nT')
413 | print(' Min Error =', np.amin(res), 'nT')
414 |
415 | np.testing.assert_allclose(B_radius, B_radius_mat)
416 | np.testing.assert_allclose(B_theta, B_theta_mat)
417 | np.testing.assert_allclose(B_phi, B_phi_mat)
418 |
419 | def test_sa_timeseries(self):
420 |
421 | # some observatory location
422 | radius = R_REF*0.35
423 | theta = 90-14.308
424 | phi = -16.950
425 |
426 | model = cp.load_CHAOS_matfile(CHAOS_PATH)
427 |
428 | time = np.linspace(model.model_tdep.breaks[0],
429 | model.model_tdep.breaks[-1], num=1000)
430 |
431 | B_radius, B_theta, B_phi = model.synth_values_tdep(
432 | time, radius, theta, phi, nmax=10, deriv=2)
433 |
434 | # load matfile
435 | test = load_matfile(MATFILE_PATH, 'test_sa_timeseries')
436 |
437 | B_radius_mat = np.ravel(test['B_radius'])
438 | B_theta_mat = np.ravel(test['B_theta'])
439 | B_phi_mat = np.ravel(test['B_phi'])
440 |
441 | for component in ['B_radius', 'B_theta', 'B_phi']:
442 | res = np.abs(eval(component) - eval('_'.join((component, 'mat'))))
443 | print(' -------------------')
444 | print(f' {component}:')
445 | print(' MAE =', np.mean(res), 'nT')
446 | print(' RMSE =', np.sqrt(np.mean(res**2)), 'nT')
447 | print(' Max Error =', np.amax(res), 'nT')
448 | print(' Min Error =', np.amin(res), 'nT')
449 |
450 | np.testing.assert_allclose(B_radius, B_radius_mat)
451 | np.testing.assert_allclose(B_theta, B_theta_mat)
452 | np.testing.assert_allclose(B_phi, B_phi_mat)
453 |
454 | def test_synth_sm_field(self):
455 |
456 | # load matfile
457 | test = load_matfile(MATFILE_PATH, 'test_synth_sm_field')
458 |
459 | model = cp.load_CHAOS_matfile(CHAOS_PATH)
460 |
461 | N = int(1000)
462 |
463 | time = np.linspace(-200, 6000, num=N)
464 | radius = R_REF
465 | theta = np.linspace(1, 179, num=N)
466 | phi = np.linspace(-180, 179, num=N)
467 |
468 | B_radius = np.zeros(time.shape)
469 | B_theta = np.zeros(time.shape)
470 | B_phi = np.zeros(time.shape)
471 |
472 | for source in ['internal', 'external']:
473 | B_radius_new, B_theta_new, B_phi_new = model.synth_values_sm(
474 | time, radius, theta, phi, source=source)
475 |
476 | B_radius += B_radius_new
477 | B_theta += B_theta_new
478 | B_phi += B_phi_new
479 |
480 | B_radius_mat = np.ravel(test['B_radius'])
481 | B_theta_mat = np.ravel(test['B_theta'])
482 | B_phi_mat = np.ravel(test['B_phi'])
483 |
484 | for component in ['B_radius', 'B_theta', 'B_phi']:
485 | res = np.abs(eval(component) - eval('_'.join((component, 'mat'))))
486 | print(' -------------------')
487 | print(f' {component}:')
488 | print(' MAE =', np.mean(res), 'nT')
489 | print(' RMSE =', np.sqrt(np.mean(res**2)), 'nT')
490 | print(' Max Error =', np.amax(res), 'nT')
491 | print(' Min Error =', np.amin(res), 'nT')
492 |
493 | np.testing.assert_allclose(
494 | B_radius, B_radius_mat, rtol=1e-2, atol=1e-2)
495 | np.testing.assert_allclose(B_theta, B_theta_mat, rtol=1e-2, atol=1e-2)
496 | np.testing.assert_allclose(B_phi, B_phi_mat, rtol=1e-2, atol=1e-2)
497 |
498 | def test_synth_gsm_field(self):
499 |
500 | # load matfile
501 | test = load_matfile(MATFILE_PATH, 'test_synth_gsm_field')
502 |
503 | model = cp.load_CHAOS_matfile(CHAOS_PATH)
504 |
505 | N = int(1000)
506 |
507 | time = np.linspace(-1000, 6000, num=N)
508 | radius = R_REF
509 | theta = np.linspace(1, 179, num=N)
510 | phi = np.linspace(-180, 179, num=N)
511 |
512 | B_radius = np.zeros(time.shape)
513 | B_theta = np.zeros(time.shape)
514 | B_phi = np.zeros(time.shape)
515 |
516 | for source in ['internal', 'external']:
517 | B_radius_new, B_theta_new, B_phi_new = model.synth_values_gsm(
518 | time, radius, theta, phi, source=source)
519 |
520 | B_radius += B_radius_new
521 | B_theta += B_theta_new
522 | B_phi += B_phi_new
523 |
524 | B_radius_mat = np.ravel(test['B_radius'])
525 | B_theta_mat = np.ravel(test['B_theta'])
526 | B_phi_mat = np.ravel(test['B_phi'])
527 |
528 | for component in ['B_radius', 'B_theta', 'B_phi']:
529 | res = np.abs(eval(component) - eval('_'.join((component, 'mat'))))
530 | print(' -------------------')
531 | print(f' {component}:')
532 | print(' MAE =', np.mean(res), 'nT')
533 | print(' RMSE =', np.sqrt(np.mean(res**2)), 'nT')
534 | print(' Max Error =', np.amax(res), 'nT')
535 | print(' Min Error =', np.amin(res), 'nT')
536 |
537 | np.testing.assert_allclose(
538 | B_radius, B_radius_mat, rtol=1e-2, atol=1e-2)
539 | np.testing.assert_allclose(
540 | B_theta, B_theta_mat, rtol=1e-2, atol=1e-2)
541 | np.testing.assert_allclose(
542 | B_phi, B_phi_mat, rtol=1e-2, atol=1e-2)
543 |
544 | def test_rotate_gsm_vector(self):
545 |
546 | time = 20
547 | nmax = 1
548 | kmax = 1
549 | radius = R_REF
550 | theta_geo = 90/6
551 | phi_geo = 90/8
552 | g_gsm = np.array([1, 2, -1])
553 |
554 | base_1, base_2, base_3 = cp.coordinate_utils.basevectors_gsm(time)
555 | matrix = cp.coordinate_utils.rotate_gauss(nmax, kmax, base_1,
556 | base_2, base_3)
557 |
558 | g_geo = np.matmul(matrix, g_gsm)
559 |
560 | B_geo_1 = cp.model_utils.synth_values(g_geo, radius,
561 | theta_geo, phi_geo)
562 | B_geo_1 = np.array(B_geo_1)
563 |
564 | theta_gsm, phi_gsm, R = cp.coordinate_utils.matrix_geo_to_base(
565 | theta_geo, phi_geo, base_1, base_2, base_3)
566 |
567 | B_gsm = cp.model_utils.synth_values(g_gsm, radius, theta_gsm, phi_gsm)
568 | B_gsm = np.array(B_gsm)
569 |
570 | B_geo_2 = np.matmul(R.transpose(), B_gsm)
571 |
572 | np.testing.assert_allclose(B_geo_1, B_geo_2, rtol=1e-5)
573 |
574 | def test_plot_maps(self):
575 |
576 | model = cp.load_CHAOS_matfile(CHAOS_PATH)
577 |
578 | model.plot_maps_tdep(0., 6371.2)
579 | plt.close('all')
580 |
581 | model.plot_maps_static(6371.2, nmax=min(50, model.model_static.nmax))
582 | plt.close('all')
583 |
584 |
585 | def profiler_complete_forward(n_data=300):
586 | """
587 | Example:
588 |
589 | .. code-block:: python
590 |
591 | %load_ext line_profiler
592 |
593 | import sys
594 | sys.path.insert(0, )
595 | from test_chaosmagpy import profiler_complete_forward as profiler
596 | import chaosmagpy as cp
597 |
598 | %lprun -f cp.coordinate_utils.synth_rotate_gauss profiler(n_data=300)
599 |
600 | """
601 |
602 | t_start = -200.0
603 | t_end = 6000.0
604 | time = np.linspace(t_start, t_end, num=n_data)
605 | radius = R_REF * np.ones(time.shape)
606 | theta = np.linspace(1, 179, num=n_data)
607 | phi = np.linspace(-180, 179, num=n_data)
608 |
609 | model = cp.load_CHAOS_matfile(CHAOS_PATH)
610 |
611 | # B_radius, B_theta, B_phi = model(time, radius, theta, phi)
612 | B_radius, B_theta, B_phi = model(time, radius, theta, phi)
613 | print('Ran "profiler_complete_forward"')
614 |
615 |
616 | if __name__ == '__main__':
617 | main()
618 |
--------------------------------------------------------------------------------
/tests/test_coordinate_utils.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2024 Clemens Kloss
2 | #
3 | # This file is part of ChaosMagPy.
4 | #
5 | # ChaosMagPy is released under the MIT license. See LICENSE in the root of the
6 | # repository for full licensing details.
7 |
8 | import numpy as np
9 | import os
10 | import textwrap
11 | import chaosmagpy as cp
12 | from chaosmagpy import coordinate_utils as cpc
13 | from unittest import TestCase, main
14 | from math import pi
15 | from timeit import default_timer as timer
16 |
17 | try:
18 | from tests.helpers import load_matfile
19 | except ImportError:
20 | from helpers import load_matfile
21 |
22 | ROOT = os.path.abspath(os.path.dirname(__file__))
23 | MATFILE_PATH = os.path.join(ROOT, 'data/CHAOS_test.mat')
24 |
25 | # check if mat-file exists in tests directory
26 | if os.path.isfile(MATFILE_PATH) is False:
27 | MATFILE_PATH = str(input('Matfile path for coordinate_utils test?: '))
28 |
29 |
30 | class CoordinateUtils(TestCase):
31 | def setUp(self):
32 |
33 | print(textwrap.dedent(f"""\
34 |
35 | {"":-^70}
36 | Running {self._testMethodName}:
37 | """))
38 |
39 | def test_dipole_to_unit(self):
40 |
41 | np.testing.assert_allclose(
42 | cpc.igrf_dipole('2010'),
43 | cpc._dipole_to_unit(11.32, 289.59))
44 |
45 | np.testing.assert_allclose(
46 | cpc.igrf_dipole('2015'),
47 | cpc._dipole_to_unit(np.array([-29442.0, -1501.0, 4797.1])))
48 |
49 | np.testing.assert_allclose(
50 | cpc.igrf_dipole('2015'),
51 | cpc._dipole_to_unit(-29442.0, -1501.0, 4797.1))
52 |
53 | # test higher-dimensional arrays
54 |
55 | desired = np.tile(cpc.igrf_dipole('2010'), (5, 3, 1)) # (5, 3, 3)
56 | result = cpc._dipole_to_unit(
57 | 11.32*np.ones((5, 3)), 289.59*np.ones((5, 3))
58 | )
59 | np.testing.assert_allclose(desired, result)
60 |
61 | desired = np.tile(cpc.igrf_dipole('2015'), (5, 2, 1)) # (5, 2, 3)
62 | result = cpc._dipole_to_unit(
63 | np.tile(np.array([-29442.0, -1501.0, 4797.1]), (5, 2, 1))
64 | )
65 | np.testing.assert_allclose(desired, result)
66 |
67 | desired = np.tile(cpc.igrf_dipole('2015'), (5, 2, 1)) # (5, 2, 3)
68 | result = cpc._dipole_to_unit(
69 | -29442.0*np.ones((5, 2)), -1501.0*np.ones((5, 2)),
70 | 4797.1*np.ones((5, 2))
71 | )
72 | np.testing.assert_allclose(desired, result)
73 |
74 | def test_zenith_angle(self):
75 | """
76 | Compared zenith angle with NOAA
77 | `https://www.esrl.noaa.gov/gmd/grad/antuv/SolarCalc.jsp`_ .
78 |
79 | DANGER: longitude means the hour angle which is negative geographic
80 | longitude
81 |
82 | """
83 |
84 | zeta = cpc.zenith_angle(cp.mjd2000(2019, 8, 1), 90., 0.)
85 | self.assertIsNone(np.testing.assert_allclose(
86 | zeta, 161.79462, rtol=1e-5))
87 |
88 | zeta = cpc.zenith_angle(cp.mjd2000(2013, 8, 1), 77., -54.)
89 | self.assertIsNone(np.testing.assert_allclose(
90 | zeta, 117.00128, rtol=1e-5))
91 |
92 | zeta = cpc.zenith_angle(cp.mjd2000(2013, 3, 20, 12), 90., 0.)
93 | self.assertIsNone(np.testing.assert_allclose(
94 | zeta, 1.85573, rtol=1e-4))
95 |
96 | zeta = cpc.zenith_angle(cp.mjd2000(2013, 3, 20, 10), 90., 0.)
97 | self.assertIsNone(np.testing.assert_allclose(
98 | zeta, 31.85246, rtol=1e-3))
99 |
100 | zeta = cpc.zenith_angle(cp.mjd2000(2013, 3, 20, 14), 90., 0.)
101 | self.assertIsNone(np.testing.assert_allclose(
102 | zeta, 28.14153, rtol=1e-3))
103 |
104 | def test_q_response_sphere_pre7(self):
105 |
106 | a = 6371.2
107 | n = 1
108 |
109 | # load matfile
110 | test = load_matfile(MATFILE_PATH, 'test_conducting_sphere')
111 |
112 | C_n_mat = np.ravel(test['C_n'])
113 | rho_a_mat = np.ravel(test['rho_a'])
114 | phi_mat = np.ravel(test['phi'])
115 | Q_n_mat = np.ravel(test['Q_n'])
116 |
117 | model = np.loadtxt(
118 | os.path.join(ROOT, '../data/gsm_sm_coefficients',
119 | 'conductivity_Utada2003.dat'))
120 |
121 | radius = a - model[:, 0]
122 | # perfectly conducting core will automatically be removed
123 | sigma = model[:, 1]
124 |
125 | periods = np.logspace(np.log10(1/48), np.log10(365*24))*3600
126 |
127 | C_n, rho_a, phi, Q_n = cpc.q_response_1D(periods, sigma, radius, n,
128 | kind='constant')
129 |
130 | self.assertIsNone(np.testing.assert_allclose(C_n, C_n_mat))
131 | self.assertIsNone(np.testing.assert_allclose(rho_a, rho_a_mat))
132 | self.assertIsNone(np.testing.assert_allclose(phi, phi_mat))
133 | self.assertIsNone(np.testing.assert_allclose(Q_n, Q_n_mat))
134 |
135 | def test_q_response_sphere(self):
136 |
137 | a = 6371.2
138 | n = 1
139 |
140 | # load matfile
141 | test = load_matfile(MATFILE_PATH, 'test_conducting_sphere_thinlayer')
142 |
143 | C_n_mat = np.ravel(test['C_n'])
144 | rho_a_mat = np.ravel(test['rho_a'])
145 | phi_mat = np.ravel(test['phi'])
146 | Q_n_mat = np.ravel(test['Q_n'])
147 |
148 | model = np.loadtxt(cp.basicConfig['file.Earth_conductivity'])
149 |
150 | radius = a - model[:, 0]
151 | sigma = model[:, 1]
152 |
153 | periods = np.logspace(np.log10(1/48), np.log10(365*24))*3600
154 |
155 | C_n, rho_a, phi, Q_n = cpc.q_response_1D(
156 | periods, sigma, radius, n, kind='quadratic')
157 |
158 | self.assertIsNone(np.testing.assert_allclose(C_n, C_n_mat))
159 | self.assertIsNone(np.testing.assert_allclose(rho_a, rho_a_mat))
160 | self.assertIsNone(np.testing.assert_allclose(phi, phi_mat))
161 | self.assertIsNone(np.testing.assert_allclose(Q_n, Q_n_mat))
162 |
163 | def test_rotate_gauss_coeffs(self):
164 | """
165 | Compare GSM/SM rotation matrices synthezised from chaosmagpy and
166 | Matlab.
167 |
168 | """
169 |
170 | for reference in ['GSM', 'SM']:
171 |
172 | print(f' Testing {reference} frame of reference.')
173 |
174 | # load spectrum to synthesize matrices in time-domain
175 | filepath = cp.basicConfig[f'file.{reference}_spectrum']
176 |
177 | try:
178 | data = np.load(filepath)
179 | except FileNotFoundError as e:
180 | raise ValueError(
181 | f'{reference} file not found in "chaosmagpy/lib/".'
182 | ' Correct reference?') from e
183 |
184 | data_mat = load_matfile(MATFILE_PATH, 'test_rotate_gauss_coeffs')
185 |
186 | time = 10*365.25*np.random.random_sample((30,))
187 |
188 | for freq, spec in zip(['frequency', 'frequency_ind'],
189 | ['spectrum', 'spectrum_ind']):
190 |
191 | matrix = cpc.synth_rotate_gauss(
192 | time, data[freq], data[spec], scaled=True)
193 |
194 | matrix_mat = cpc.synth_rotate_gauss(
195 | time, data_mat[reference + '_' + freq],
196 | data_mat[reference + '_' + spec], scaled=True)
197 |
198 | self.assertIsNone(np.testing.assert_allclose(
199 | matrix, matrix_mat, atol=1e-3))
200 |
201 | def test_synth_rotate_gauss(self):
202 | """
203 | Tests the accuracy of the Fourier representation of tranformation
204 | matrices by comparing them with directly computed matrices (they are
205 | considered correct).
206 |
207 | """
208 |
209 | for reference in ['gsm', 'sm']:
210 |
211 | print(f' Testing {reference.upper()} frame of reference.')
212 |
213 | # load spectrum to synthesize matrices in time-domain
214 | filepath = cp.basicConfig[f'file.{reference.upper()}_spectrum']
215 |
216 | try:
217 | data = np.load(filepath)
218 | except FileNotFoundError as e:
219 | raise ValueError(
220 | 'Reference file "frequency_spectrum_{reference}.npz"'
221 | ' not found in "chaosmagpy/lib/".'
222 | ' Correct reference?') from e
223 |
224 | frequency = data['frequency'] # oscillations per day
225 | spectrum = data['spectrum']
226 |
227 | print(" Testing 50 times within 1996 and 2024.")
228 | for time in np.linspace(-4*365.25, 24*365.25, 50):
229 |
230 | matrix_time = cpc.synth_rotate_gauss(
231 | time, frequency, spectrum, scaled=data['scaled'])
232 |
233 | nmax = int(np.sqrt(spectrum.shape[1] + 1) - 1)
234 | kmax = int(np.sqrt(spectrum.shape[2] + 1) - 1)
235 |
236 | if reference == 'gsm':
237 | base_1, base_2, base_3 = cpc.basevectors_gsm(time)
238 | elif reference == 'sm':
239 | base_1, base_2, base_3 = cpc.basevectors_sm(time)
240 |
241 | matrix = cpc.rotate_gauss(nmax, kmax, base_1, base_2, base_3)
242 |
243 | stat = np.amax(np.abs(matrix-np.squeeze(matrix_time)))
244 | print(' Computed year {:4.2f}, '
245 | 'max. abs. error = {:.3e}'.format(
246 | time/365.25 + 2000, stat), end='')
247 | if stat > 0.001:
248 | print(' ' + min(int(stat/0.001), 10) * '*')
249 | else:
250 | print('')
251 |
252 | self.assertIsNone(np.testing.assert_allclose(
253 | matrix, np.squeeze(matrix_time), rtol=1e-1, atol=1e-1))
254 |
255 | def test_rotate_gauss_fft(self):
256 |
257 | nmax = 2
258 | kmax = 2
259 |
260 | # load matfile
261 | test = load_matfile(MATFILE_PATH, 'test_rotate_gauss_fft')
262 |
263 | for reference in ['gsm', 'sm']:
264 |
265 | frequency, amplitude, _, _ = cpc.rotate_gauss_fft(
266 | nmax, kmax, step=1., N=int(365*24), filter=20,
267 | save_to=False, reference=reference, scaled=False)
268 |
269 | omega = 2*pi*frequency / (24*3600)
270 |
271 | # transpose and take complex conjugate (to match original output)
272 | omega_mat = \
273 | test['omega_{:}'.format(reference)].transpose((2, 1, 0))
274 | amplitude_mat = \
275 | test['amplitude_{:}'.format(reference)].transpose((2, 1, 0))
276 | amplitude_mat = amplitude_mat.conj()
277 |
278 | # absolute tolerance to account for close-to-zero matrix entries
279 | self.assertIsNone(np.testing.assert_allclose(
280 | omega_mat, omega, atol=1e-3))
281 | self.assertIsNone(np.testing.assert_allclose(
282 | amplitude_mat, amplitude, atol=1e-10))
283 |
284 | def test_rotate_gauss(self):
285 |
286 | # test 1
287 | time = 20 # random day
288 | nmax = 4
289 | kmax = 2
290 |
291 | s = timer()
292 |
293 | base_1, base_2, base_3 = cpc.basevectors_gsm(time)
294 | matrix = cpc.rotate_gauss(nmax, kmax, base_1, base_2, base_3)
295 |
296 | e = timer()
297 |
298 | # load matfile
299 | test = load_matfile(MATFILE_PATH, 'test_rotate_gauss')
300 |
301 | matrix_mat = test['m_all'].transpose() # transposed in Matlab code
302 | runtime = test['runtime']
303 |
304 | print(" Time for matrix computation (Python): ", e - s)
305 | print(" Time for matrix computation (Matlab): {:}".format(
306 | runtime[0, 0]))
307 |
308 | # absolute tolerance to account for close-to-zero matrix entries
309 | self.assertIsNone(np.testing.assert_allclose(
310 | matrix, matrix_mat, atol=1e-8))
311 |
312 | # test 2
313 | # rotate around y-axis to have dipole axis aligned with x-axis
314 | base_1 = np.array([0, 0, -1])
315 | base_2 = np.array([0, 1, 0])
316 | base_3 = np.array([1, 0, 0])
317 |
318 | nmax = 1 # only dipole terms
319 | kmax = 1
320 |
321 | matrix = cpc.rotate_gauss(nmax, kmax, base_1, base_2, base_3)
322 | desired = np.array([[0, -1, 0], [1, 0, 0], [0, 0, 1]])
323 |
324 | self.assertIsNone(np.testing.assert_allclose(
325 | matrix, desired, atol=1e-10))
326 |
327 | # test 3
328 | time = np.arange(9).reshape((3, 3)) # random day
329 | nmax = 4
330 | kmax = 2
331 |
332 | base_1, base_2, base_3 = cpc.basevectors_gsm(time)
333 | matrix = cpc.rotate_gauss(nmax, kmax, base_1, base_2, base_3)
334 |
335 | self.assertEqual(matrix.shape, (3, 3, 24, 8))
336 |
337 | def test_cartesian_to_spherical(self):
338 |
339 | x = -1.0
340 | y = 0.0
341 | z = 0.0
342 |
343 | radius = 1.0
344 | theta = 90.
345 | phi = 180.
346 |
347 | result = (radius, theta, phi)
348 | self.assertAlmostEqual(cpc.cartesian_to_spherical(x, y, z), result)
349 |
350 | def test_spherical_to_cartesian(self):
351 |
352 | radius = 1.
353 | theta = pi/2
354 | phi = pi
355 |
356 | x = np.array(radius) * np.sin(theta) * np.cos(phi)
357 | y = np.array(radius) * np.sin(theta) * np.sin(phi)
358 | z = np.array(radius) * np.cos(theta)
359 |
360 | result = (x, y, z)
361 |
362 | theta, phi = np.degrees(theta), np.degrees(phi)
363 |
364 | self.assertEqual(cpc.spherical_to_cartesian(radius, theta, phi), result)
365 | self.assertEqual(cpc.spherical_to_cartesian(1, theta, phi), result)
366 | self.assertEqual(cpc.spherical_to_cartesian(
367 | theta=theta, radius=radius, phi=phi), result)
368 | self.assertEqual(cpc.spherical_to_cartesian(
369 | radius=1, phi=phi, theta=theta), result)
370 |
371 | def test_gg_to_geo(self):
372 |
373 | mat = load_matfile(MATFILE_PATH, 'test_gg_to_geo')
374 |
375 | radius, theta, Br, Bt = cpc.gg_to_geo(mat['height'], mat['beta'],
376 | mat['X'], mat['Z'])
377 |
378 | np.testing.assert_allclose(radius, mat['radius'])
379 | np.testing.assert_allclose(theta, mat['theta'])
380 | np.testing.assert_allclose(Br, mat['Br'], atol=1e-6)
381 | np.testing.assert_allclose(Bt, mat['Bt'], atol=1e-6)
382 |
383 | def test_geo_to_gg(self):
384 |
385 | mat = load_matfile(MATFILE_PATH, 'test_geo_to_gg')
386 |
387 | height, beta, X, Z = cpc.geo_to_gg(mat['radius'], mat['theta'],
388 | mat['Br'], mat['Bt'])
389 |
390 | np.testing.assert_allclose(height, mat['height'])
391 | np.testing.assert_allclose(beta, mat['beta'])
392 | np.testing.assert_allclose(X, mat['X'], atol=1e-6)
393 | np.testing.assert_allclose(Z, mat['Z'], atol=1e-6)
394 |
395 | def test_gg_geo_gg(self):
396 |
397 | mat = load_matfile(MATFILE_PATH, 'test_gg_to_geo')
398 |
399 | radius, theta = cpc.gg_to_geo(mat['height'], mat['beta'])
400 |
401 | height, beta = cpc.geo_to_gg(radius, theta)
402 |
403 | np.testing.assert_allclose(height, mat['height'], atol=1e-10)
404 | np.testing.assert_allclose(beta, mat['beta'], atol=1e-10)
405 |
406 | def test_matrix_geo_to_base(self):
407 |
408 | theta_geo = np.linspace(1, 179, 10)
409 | phi_geo = np.linspace(-179, 179, 10)
410 | time = np.linspace(-300, 10000, 10)
411 |
412 | for reference in ['sm', 'gsm', 'mag']:
413 | print(f' Testing {reference.upper()}')
414 |
415 | if reference == 'mag':
416 | base_1, base_2, base_3 = cpc.basevectors_mag()
417 | elif reference == 'sm':
418 | base_1, base_2, base_3 = cpc.basevectors_sm(time)
419 | elif reference == 'gsm':
420 | base_1, base_2, base_3 = cpc.basevectors_gsm(time)
421 |
422 | theta_ref, phi_ref, R = cpc.matrix_geo_to_base(
423 | theta_geo, phi_geo, base_1, base_2, base_3, inverse=False)
424 |
425 | theta_geo2, phi_geo2, R2 = cpc.matrix_geo_to_base(
426 | theta_ref, phi_ref, base_1, base_2, base_3, inverse=True)
427 |
428 | self.assertIsNone(
429 | np.testing.assert_allclose(theta_geo, theta_geo2))
430 | self.assertIsNone(
431 | np.testing.assert_allclose(phi_geo, phi_geo2))
432 |
433 | R_full = np.matmul(R, R2)
434 | R_full2 = np.zeros((theta_geo.size, 3, 3))
435 | for n in range(3):
436 | R_full2[..., n, n] = 1.
437 |
438 | self.assertIsNone(
439 | np.testing.assert_allclose(R_full, R_full2, atol=1e-7))
440 |
441 | def test_geo_to_sm(self):
442 |
443 | time = np.linspace(1, 100, 10)
444 | theta_geo = np.linspace(1, 179, 10)
445 | phi_geo = np.linspace(-179, 179, 10)
446 |
447 | # load matfile
448 | test = load_matfile(MATFILE_PATH, 'test_geo_to_sm')
449 |
450 | # reduce 2-D matrix to 1-D vectors
451 | theta_sm_mat = np.ravel(test['theta_sm'])
452 | phi_sm_mat = np.ravel(test['phi_sm'])
453 |
454 | theta_sm, phi_sm = cpc.transform_points(theta_geo, phi_geo,
455 | time=time, reference='sm')
456 |
457 | self.assertIsNone(np.testing.assert_allclose(theta_sm, theta_sm_mat))
458 | self.assertIsNone(np.testing.assert_allclose(phi_sm, phi_sm_mat))
459 |
460 | def test_geo_to_gsm(self):
461 |
462 | time = np.linspace(1, 100, 10)
463 | theta_geo = np.linspace(1, 179, 10)
464 | phi_geo = np.linspace(-179, 179, 10)
465 |
466 | # load matfile
467 | test = load_matfile(MATFILE_PATH, 'test_geo_to_gsm')
468 |
469 | # reduce 2-D matrix to 1-D vectors
470 | theta_gsm_mat = np.ravel(test['theta_gsm'])
471 | phi_gsm_mat = np.ravel(test['phi_gsm'])
472 |
473 | theta_gsm, phi_gsm = cpc.transform_points(
474 | theta_geo, phi_geo, time=time, reference='gsm')
475 |
476 | self.assertIsNone(np.testing.assert_allclose(theta_gsm, theta_gsm_mat))
477 | self.assertIsNone(np.testing.assert_allclose(phi_gsm, phi_gsm_mat))
478 |
479 | def test_geo_to_base(self):
480 |
481 | time = np.linspace(1, 100, 10)
482 | theta_geo = np.linspace(1, 179, 10)
483 | phi_geo = np.linspace(-179, 179, 10)
484 |
485 | # TEST GSM COORDINATES
486 |
487 | # GSM test: load matfile
488 | test = load_matfile(MATFILE_PATH, 'test_geo_to_gsm')
489 |
490 | # reduce 2-D matrix to 1-D vectors
491 | theta_gsm_mat = np.ravel(test['theta_gsm'])
492 | phi_gsm_mat = np.ravel(test['phi_gsm'])
493 |
494 | gsm_1, gsm_2, gsm_3 = cpc.basevectors_gsm(time)
495 |
496 | theta_gsm, phi_gsm = cpc.geo_to_base(
497 | theta_geo, phi_geo, gsm_1, gsm_2, gsm_3)
498 |
499 | np.testing.assert_allclose(theta_gsm, theta_gsm_mat)
500 | np.testing.assert_allclose(phi_gsm, phi_gsm_mat)
501 |
502 | # test the inverse option: GEO -> GSM -> GEO
503 | theta_geo2, phi_geo2 = cpc.geo_to_base(
504 | theta_gsm, phi_gsm, gsm_1, gsm_2, gsm_3, inverse=True)
505 |
506 | np.testing.assert_allclose(theta_geo, theta_geo2)
507 | np.testing.assert_allclose(phi_geo, phi_geo2)
508 |
509 | # test the inverse option: GSM -> GEO -> GSM
510 | theta_gsm2, phi_gsm2 = cpc.geo_to_base(
511 | theta_geo2, phi_geo2, gsm_1, gsm_2, gsm_3)
512 |
513 | np.testing.assert_allclose(theta_gsm, theta_gsm2)
514 | np.testing.assert_allclose(phi_gsm, phi_gsm2)
515 |
516 | # TEST SM COORDINATES
517 |
518 | # SM test: load matfile
519 | test = load_matfile(MATFILE_PATH, 'test_geo_to_sm')
520 |
521 | # reduce 2-D matrix to 1-D vectors
522 | theta_sm_mat = np.ravel(test['theta_sm'])
523 | phi_sm_mat = np.ravel(test['phi_sm'])
524 |
525 | sm_1, sm_2, sm_3 = cpc.basevectors_sm(time)
526 |
527 | theta_sm, phi_sm = cpc.geo_to_base(
528 | theta_geo, phi_geo, sm_1, sm_2, sm_3)
529 |
530 | np.testing.assert_allclose(theta_sm, theta_sm_mat)
531 | np.testing.assert_allclose(phi_sm, phi_sm_mat)
532 |
533 | # test the inverse option: GEO -> SM -> GEO
534 | theta_geo2, phi_geo2 = cpc.geo_to_base(
535 | theta_sm, phi_sm, sm_1, sm_2, sm_3, inverse=True)
536 |
537 | np.testing.assert_allclose(theta_geo, theta_geo2)
538 | np.testing.assert_allclose(phi_geo, phi_geo2)
539 |
540 | # test the inverse option: SM -> GEO -> SM
541 | theta_sm2, phi_sm2 = cpc.geo_to_base(
542 | theta_geo2, phi_geo2, sm_1, sm_2, sm_3)
543 |
544 | np.testing.assert_allclose(theta_sm, theta_sm2)
545 | np.testing.assert_allclose(phi_sm, phi_sm2)
546 |
547 | def test_basevectors_use(self):
548 |
549 | # test local transformation at specific point
550 | R = np.stack(cpc.basevectors_use(
551 | theta=90., phi=180.), axis=-1)
552 | desired = np.array([[-1, 0, 0], [0, 0, -1], [0, -1, 0]])
553 |
554 | self.assertIsNone(np.testing.assert_allclose(R, desired, atol=1e-10))
555 |
556 | # test local transformation at specific point
557 | R = np.stack(cpc.basevectors_use(
558 | theta=90., phi=-90.), axis=-1)
559 | desired = np.array([[0, 0, 1], [-1, 0, 0], [0, -1, 0]])
560 |
561 | self.assertIsNone(np.testing.assert_allclose(R, desired, atol=1e-10))
562 |
563 | def test_sun_position(self):
564 |
565 | start = cp.mjd2000(1910, 1, 1)
566 | end = cp.mjd2000(2090, 12, 31)
567 | time = np.linspace(start, end, 100)
568 |
569 | theta, phi = cpc.sun_position(time)
570 |
571 | # load matfile
572 | test = load_matfile(MATFILE_PATH, 'test_sun_position')
573 |
574 | # reduce 2-D matrix to 1-D vectors
575 | theta_mat = np.ravel(test['theta'])
576 | phi_mat = np.ravel(test['phi'])
577 |
578 | self.assertIsNone(np.testing.assert_allclose(theta, theta_mat))
579 | self.assertIsNone(np.testing.assert_allclose(phi, phi_mat))
580 |
581 | # test position of sun close to zenith in Greenwich
582 | # (lat 51.48, lon -0.0077) on July 18, 2018, 12h06 (from
583 | # sunearthtools.com):
584 |
585 | time_gmt = cp.mjd2000(2018, 7, 18, 12, 6)
586 | lat = 51.4825766 # geogr. latitude
587 | lon = -0.0076589 # geogr. longitude
588 | el = 59.59 # elevation above horizon
589 | az = 179.86 # angle measured from north (sun close to zenith)
590 |
591 | theta_gmt = 180. - (el + lat) # subsolar colat. given geogr. lat.
592 | phi_gmt = 180. + (lon - az) # subsolar phi given geogr. longitude
593 |
594 | theta, phi = cpc.sun_position(time_gmt)
595 |
596 | self.assertAlmostEqual(theta_gmt, theta, places=0)
597 | self.assertAlmostEqual(phi_gmt, phi, places=0)
598 |
599 | def test_center_azimuth(self):
600 |
601 | angle = 180.
602 |
603 | self.assertAlmostEqual(cpc.center_azimuth(0.25*angle), 0.25*angle)
604 | self.assertAlmostEqual(cpc.center_azimuth(0.75*angle), 0.75*angle)
605 | self.assertAlmostEqual(cpc.center_azimuth(1.25*angle), -0.75*angle)
606 | self.assertAlmostEqual(cpc.center_azimuth(1.75*angle), -0.25*angle)
607 | self.assertAlmostEqual(cpc.center_azimuth(-0.25*angle), -0.25*angle)
608 | self.assertAlmostEqual(cpc.center_azimuth(-0.75*angle), -0.75*angle)
609 | self.assertAlmostEqual(cpc.center_azimuth(-1.25*angle), 0.75*angle)
610 | self.assertAlmostEqual(cpc.center_azimuth(-1.75*angle), 0.25*angle)
611 |
612 | def test_dipole_input(self):
613 | """
614 | Ensure that dipole keyword for basevectors accepts various input types.
615 | """
616 |
617 | dipole = [-29442.0, -1501.0, 4797.1]
618 | desired = cpc.basevectors_mag(dipole=dipole)
619 |
620 | dipole = np.array([-29442.0, -1501.0, 4797.1])
621 | result = cpc.basevectors_mag(dipole=dipole)
622 | np.testing.assert_allclose(result, desired)
623 |
624 | dipole = (9.688340430, 287.374764610)
625 | result = cpc.basevectors_mag(dipole=dipole)
626 | np.testing.assert_allclose(result, desired)
627 |
628 | dipole = ([-29442.0], [-1501.0], [4797.1])
629 | result = cpc.basevectors_mag(dipole=dipole)
630 | np.testing.assert_allclose(
631 | np.stack(result, axis=-1)[0, ...], # unpack shape (1, 3, 3)
632 | np.stack(desired, axis=-1) # shape (3, 3)
633 | )
634 |
635 |
636 | if __name__ == '__main__':
637 | main()
638 |
--------------------------------------------------------------------------------
/tests/test_data_utils.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2024 Clemens Kloss
2 | #
3 | # This file is part of ChaosMagPy.
4 | #
5 | # ChaosMagPy is released under the MIT license. See LICENSE in the root of the
6 | # repository for full licensing details.
7 |
8 | import numpy as np
9 | import os
10 | import textwrap
11 | from unittest import TestCase, main
12 | from datetime import datetime, timedelta
13 | from chaosmagpy import data_utils as cpd
14 |
15 | ROOT = os.path.abspath(os.path.dirname(__file__))
16 | MATFILE_PATH = os.path.join(ROOT, 'data/CHAOS_test.mat')
17 |
18 | # check if mat-file exists in tests directory
19 | if os.path.isfile(MATFILE_PATH) is False:
20 | MATFILE_PATH = str(input('Matfile path for data_utils test?: '))
21 |
22 |
23 | class DataUtils(TestCase):
24 | def setUp(self):
25 |
26 | print(textwrap.dedent(f"""\
27 |
28 | {"":-^70}
29 | Running {self._testMethodName}:
30 | """))
31 |
32 | def test_load_RC_datfile(self):
33 |
34 | # make sure link is not broken
35 | cpd.load_RC_datfile(filepath=None, parse_dates=None)
36 |
37 | def test_time_conversion(self):
38 |
39 | size = (10,)
40 | days = np.random.randint(365., size=size)
41 | hours = np.random.randint(24, size=size)
42 | minutes = np.random.randint(60, size=size)
43 | seconds = np.random.randint(60, size=size)
44 |
45 | for day, hour, minute, second in zip(days, hours, minutes, seconds):
46 | date = (timedelta(days=int(day)) +
47 | datetime(1990, 1, 1, hour, minute, second))
48 |
49 | mjd = cpd.mjd2000(date.year, date.month, date.day, date.hour,
50 | date.minute, date.second)
51 |
52 | dyear = cpd.mjd_to_dyear(mjd, leap_year=True)
53 | mjd2 = cpd.dyear_to_mjd(dyear, leap_year=True)
54 | self.assertIsNone(np.testing.assert_allclose(mjd2, mjd, atol=1e-8))
55 |
56 | dyear = cpd.mjd_to_dyear(mjd, leap_year=False)
57 | mjd2 = cpd.dyear_to_mjd(dyear, leap_year=False)
58 | self.assertIsNone(np.testing.assert_allclose(mjd2, mjd, atol=1e-8))
59 |
60 | dyear = cpd.mjd_to_dyear(mjd, leap_year=True)
61 | mjd2 = cpd.dyear_to_mjd(dyear, leap_year=False)
62 | self.assertRaises(
63 | AssertionError, lambda: np.testing.assert_allclose(
64 | mjd2, mjd, atol=1e-8))
65 |
66 | dyear = cpd.mjd_to_dyear(mjd, leap_year=False)
67 | mjd2 = cpd.dyear_to_mjd(dyear, leap_year=True)
68 | self.assertRaises(
69 | AssertionError, lambda: np.testing.assert_allclose(
70 | mjd2, mjd, atol=1e-8))
71 |
72 | dyear = cpd.mjd_to_dyear(mjd, leap_year=False)
73 | mjd2 = cpd.dyear_to_mjd(dyear, leap_year=None)
74 | self.assertRaises(
75 | AssertionError, lambda: np.testing.assert_allclose(
76 | mjd2, mjd, atol=1e-8))
77 |
78 | def test_mjd2000_broadcasting(self):
79 | """
80 | Ensure broadcasting works
81 | """
82 |
83 | actual = cpd.mjd2000(np.arange(2000, 2003), 2, 1)
84 | desired = np.array([ 31., 397., 762.])
85 | np.testing.assert_allclose(actual, desired)
86 |
87 | actual = cpd.mjd2000(2000, 2, 1, np.arange(3))
88 | desired = 31. + np.arange(3)/24
89 | np.testing.assert_allclose(actual, desired)
90 |
91 | def test_mjd_to_dyear(self):
92 |
93 | self.assertEqual(cpd.mjd_to_dyear(0.0), 2000.0)
94 | self.assertEqual(cpd.mjd_to_dyear(366.0), 2001.0)
95 | self.assertEqual(cpd.mjd_to_dyear(731.0), 2002.0)
96 | self.assertEqual(cpd.mjd_to_dyear(1096.0), 2003.0)
97 | self.assertEqual(cpd.mjd_to_dyear(4*365.25), 2004.0)
98 |
99 | self.assertEqual(cpd.mjd_to_dyear(0.0, leap_year=False), 2000.0)
100 | self.assertEqual(cpd.mjd_to_dyear(365.25, leap_year=False), 2001.0)
101 | self.assertEqual(cpd.mjd_to_dyear(2*365.25, leap_year=False), 2002.0)
102 | self.assertEqual(cpd.mjd_to_dyear(3*365.25, leap_year=False), 2003.0)
103 | self.assertEqual(cpd.mjd_to_dyear(4*365.25, leap_year=False), 2004.0)
104 |
105 |
106 | if __name__ == '__main__':
107 | main()
108 |
--------------------------------------------------------------------------------
/tests/test_model_utils.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2024 Clemens Kloss
2 | #
3 | # This file is part of ChaosMagPy.
4 | #
5 | # ChaosMagPy is released under the MIT license. See LICENSE in the root of the
6 | # repository for full licensing details.
7 |
8 | import numpy as np
9 | import os
10 | import textwrap
11 | from unittest import TestCase, main
12 | from chaosmagpy import model_utils as m
13 | from timeit import default_timer as timer
14 | from math import pi
15 |
16 | try:
17 | from tests.helpers import load_matfile
18 | except ImportError:
19 | from helpers import load_matfile
20 |
21 |
22 | RAD = pi / 180
23 | R_REF = 6371.2 # reference radius in kilometers
24 |
25 | ROOT = os.path.abspath(os.path.dirname(__file__))
26 | MATFILE_PATH = os.path.join(ROOT, 'data/CHAOS_test.mat')
27 |
28 | # check if mat-file exists in tests directory
29 | if os.path.isfile(MATFILE_PATH) is False:
30 | MATFILE_PATH = str(input('Matfile path for coordinate_utils test?: '))
31 |
32 |
33 | class ModelUtils(TestCase):
34 | def setUp(self):
35 |
36 | print(textwrap.dedent(f"""\
37 |
38 | {"":-^70}
39 | Running {self._testMethodName}:
40 | """))
41 |
42 | def test_design_gauss(self):
43 |
44 | radius = 6371*np.ones((15,))
45 | theta = np.arange(15)
46 | phi = np.arange(15)
47 |
48 | # order equal to zero
49 | A = np.array(m.design_gauss(radius, theta, phi, nmax=3))
50 | A2 = np.array(m.design_gauss(radius, theta, phi, nmax=3, mmax=0))
51 |
52 | index = [0, 3, 8]
53 | np.testing.assert_equal(A[:, :, index], A2)
54 |
55 | # order equal or less than 1
56 | A = np.array(m.design_gauss(radius, theta, phi, nmax=3))
57 | A2 = np.array(m.design_gauss(radius, theta, phi, nmax=3, mmax=1))
58 |
59 | index = [0, 1, 2, 3, 4, 5, 8, 9, 10]
60 | np.testing.assert_equal(A[:, :, index], A2)
61 |
62 | def test_design_gauss_multidim(self):
63 |
64 | radius = 6371.
65 | theta = np.arange(30).reshape((3, 1, 10))
66 | phi = np.arange(30).reshape((3, 10, 1))
67 | coeffs = np.arange(8) + 1.
68 |
69 | Br, Bt, Bp = m.synth_values(coeffs, radius, theta, phi, nmax=2)
70 | Ar, At, Ap = m.design_gauss(radius, theta, phi, nmax=2)
71 |
72 | np.testing.assert_allclose(Br, Ar@coeffs)
73 | np.testing.assert_allclose(Bt, At@coeffs)
74 | np.testing.assert_allclose(Bp, Ap@coeffs)
75 |
76 | def test_degree_correlation(self):
77 |
78 | nmax = 4
79 | coeffs = np.random.random((int(nmax*(nmax+2)),))
80 |
81 | np.testing.assert_equal(
82 | m.degree_correlation(coeffs, coeffs), np.ones((nmax,)))
83 |
84 | def test_power_spectrum(self):
85 |
86 | shape = (15, 24, 3) # nmax = (3, 4, 1)
87 | coeffs = np.ones(shape)
88 |
89 | R_n = m.power_spectrum(coeffs, source='internal') # axis=-1
90 | R_n_desired = 6. * np.ones((15, 24, 1))
91 | np.testing.assert_equal(R_n, R_n_desired)
92 |
93 | R_n = m.power_spectrum(coeffs, source='internal', axis=0)
94 | R_n_desired = np.array([6., 15., 28.])[:, None, None] * np.ones(
95 | (3, 24, 3))
96 | np.testing.assert_equal(R_n, R_n_desired)
97 |
98 | R_n = m.power_spectrum(coeffs, source='external', axis=1)
99 | R_n_desired = np.array([3., 10., 21., 36])[None, :, None] * np.ones(
100 | (15, 4, 3))
101 | np.testing.assert_equal(R_n, R_n_desired)
102 |
103 | R_n = m.power_spectrum(coeffs, source='toroidal')
104 | R_n_desired = 2. * np.ones((15, 24, 1))
105 | np.testing.assert_equal(R_n, R_n_desired)
106 |
107 | def test_synth_values_mmax(self):
108 |
109 | theta = 79.
110 | phi = 13.
111 | radius = R_REF
112 |
113 | # function for quick testing with "true" solution
114 | np.testing.assert_allclose(
115 | m.synth_values([1., 2., 3., 4., 5., 6., 7., 8.],
116 | radius, theta, phi),
117 | m.synth_values([1., 2., 3., 4., 5., 6., 7., 8.],
118 | radius, theta, phi, nmax=None, mmax=None))
119 |
120 | np.testing.assert_allclose(
121 | m.synth_values([1., 2., 3., 0., 0., 0., 0., 0.],
122 | radius, theta, phi),
123 | m.synth_values([1., 2., 3., 4., 5., 6., 7., 8.],
124 | radius, theta, phi, nmax=1, mmax=None))
125 |
126 | np.testing.assert_allclose(
127 | m.synth_values([1., 2., 3., 4., 5., 6., 0., 0.],
128 | radius, theta, phi),
129 | m.synth_values([1., 2., 3., 4., 5., 6.],
130 | radius, theta, phi, nmax=2, mmax=1))
131 |
132 | np.testing.assert_allclose(
133 | m.synth_values([1., 0., 0., 0., 0., 0., 0., 0.],
134 | radius, theta, phi),
135 | m.synth_values([1.],
136 | radius, theta, phi, nmax=1, mmax=0))
137 |
138 | np.testing.assert_allclose(
139 | m.synth_values([1., 2., 3., 4., 5., 6., 0., 0.],
140 | radius, theta, phi),
141 | m.synth_values([1., 2., 3., 4., 5., 6.],
142 | radius, theta, phi, nmax=None, mmax=1))
143 |
144 | def test_synth_values_nmin(self):
145 |
146 | theta = 79.
147 | phi = 13.
148 | radius = R_REF
149 |
150 | theta_ls = theta*np.ones((15,))
151 | phi_ls = phi*np.ones((27,))
152 | phi_grid, theta_grid = np.meshgrid(phi_ls, theta_ls) # 2-D
153 | radius_grid = R_REF * np.ones(phi_grid.shape) # 2-D
154 | coeffs_grid = np.random.random()*np.ones(phi_grid.shape + (24,)) # 3-D
155 |
156 | coeffs_grid_min = np.copy(coeffs_grid)
157 | coeffs_grid_min[..., :8] = 0. # nmax=4
158 |
159 | # exhaustive inputs
160 | B = m.synth_values(coeffs_grid_min, radius_grid, theta_grid, phi_grid)
161 |
162 | # function for quick testing with "true" solution
163 | def test(field):
164 | self.assertIsNone(np.testing.assert_allclose(B, field))
165 |
166 | test(m.synth_values(coeffs_grid[..., 8:], radius, theta_grid, phi_grid,
167 | nmin=3, nmax=4))
168 | test(m.synth_values(coeffs_grid_min[..., 8:], radius, theta_grid,
169 | phi_grid, nmin=3, nmax=4))
170 |
171 | def test_synth_values_inputs(self):
172 |
173 | theta = 79.
174 | phi = 13.
175 | radius = R_REF
176 |
177 | theta_ls = theta*np.ones((15,))
178 | phi_ls = phi*np.ones((27,))
179 | phi_grid, theta_grid = np.meshgrid(phi_ls, theta_ls) # 2-D
180 | radius_grid = R_REF * np.ones(phi_grid.shape) # 2-D
181 | coeffs_grid = np.random.random()*np.ones(phi_grid.shape + (24,)) # 3-D
182 |
183 | # exhaustive inputs
184 | B = m.synth_values(coeffs_grid, radius_grid, theta_grid, phi_grid)
185 |
186 | # function for quick testing with "true" solution
187 | def test(field):
188 | self.assertIsNone(np.testing.assert_allclose(B, field))
189 |
190 | # one input () dimension: float
191 | test(m.synth_values(coeffs_grid, radius, theta_grid, phi_grid))
192 | test(m.synth_values(coeffs_grid, radius_grid, theta, phi_grid))
193 | test(m.synth_values(coeffs_grid, radius_grid, theta_grid, phi))
194 |
195 | # one input (1,) dimension: one element array
196 | test(m.synth_values(
197 | coeffs_grid[0, 0], radius_grid, theta_grid, phi_grid))
198 | test(m.synth_values(
199 | coeffs_grid, radius_grid[0, 0], theta_grid, phi_grid))
200 | test(m.synth_values(
201 | coeffs_grid, radius_grid, theta_grid[0, 0], phi_grid))
202 | test(m.synth_values(
203 | coeffs_grid, radius_grid, theta_grid, phi_grid[0, 0]))
204 |
205 | # GRID MODE
206 | test(m.synth_values(
207 | coeffs_grid, radius_grid, theta_ls, phi_ls, grid=True))
208 | test(m.synth_values(coeffs_grid, radius, theta_ls, phi_ls, grid=True))
209 | test(m.synth_values(
210 | coeffs_grid[0, 0], radius_grid, theta_ls, phi_ls, grid=True))
211 | test(m.synth_values(
212 | coeffs_grid[0, 0], radius_grid[0, 0], theta_ls, phi_ls, grid=True))
213 |
214 | # test multi-dimensional grid
215 |
216 | # full grid
217 | coeffs_grid = np.random.random((24,))
218 | radius_grid = radius * np.ones((2, 5, 6, 4))
219 | phi_grid = phi * np.ones((2, 5, 6, 4))
220 | theta_grid = theta * np.ones((2, 5, 6, 4))
221 |
222 | B = m.synth_values(coeffs_grid, radius_grid, theta_grid, phi_grid)
223 |
224 | # reduced grid
225 | radius_grid2 = radius * np.ones((2, 1, 6, 4))
226 | phi_grid2 = phi * np.ones((2, 5, 1, 1))
227 | theta_grid2 = theta * np.ones((1, 5, 6, 4))
228 |
229 | test(m.synth_values(coeffs_grid, radius_grid2, theta_grid2, phi_grid2))
230 |
231 | def test_synth_values_grid(self):
232 |
233 | radius = R_REF
234 | theta = np.linspace(0., 180., num=4000)
235 | phi = np.linspace(-180., 180., num=3000)
236 | phi_grid, theta_grid = np.meshgrid(phi, theta)
237 | coeffs = np.random.random((24,))
238 |
239 | s = timer()
240 | B_grid = m.synth_values(
241 | coeffs, radius, theta_grid, phi_grid, grid=False)
242 | e = timer()
243 | print(" Time for 'grid=False' computation: ", e - s)
244 |
245 | s = timer()
246 | B_grid2 = m.synth_values(
247 | coeffs, radius, theta[..., None], phi[None, ...], grid=False)
248 | e = timer()
249 | print(" Time for broadcasted 'grid=False' computation: ", e - s)
250 |
251 | s = timer()
252 | B = m.synth_values(
253 | coeffs, radius, theta, phi, grid=True)
254 | e = timer()
255 | print(" Time for 'grid=True' computation: ", e - s)
256 |
257 | for comp, comp_grid in zip(B_grid, B_grid2):
258 | np.testing.assert_allclose(comp, comp_grid)
259 |
260 | for comp, comp_grid in zip(B_grid2, B):
261 | np.testing.assert_allclose(comp, comp_grid)
262 |
263 | def test_synth_values_poles(self):
264 |
265 | radius = 6371.2
266 | theta = np.array([0., 180.])
267 | phi = np.linspace(0., 360., num=10, endpoint=False)
268 |
269 | # dipole aligned with x-axis, B = -grad(V)
270 | # North pole: Bx = -1, Bt = -1
271 | # South pole: Bx = -1, Bt = 1
272 | Br, Bt, Bp = m.synth_values([0., 1., 0], radius, theta, phi, grid=True)
273 |
274 | # north pole (theta, phi) = (0., 0.)
275 | npole = (0, 0)
276 | np.testing.assert_allclose(Br[npole], 0.)
277 | np.testing.assert_allclose(Bt[npole], -1.)
278 | np.testing.assert_allclose(Bp[npole], 0.)
279 |
280 | Bx = (np.cos(np.radians(0.))*np.cos(np.radians(phi))*Bt[0, :]
281 | - np.sin(np.radians(phi))*Bp[0, :])
282 | np.testing.assert_allclose(Bx, -np.ones((10,)))
283 |
284 | # south pole (theta, phi) = (180., 0.)
285 | spole = (1, 0)
286 | np.testing.assert_allclose(Br[spole], 0.)
287 | np.testing.assert_allclose(Bt[spole], 1.)
288 | np.testing.assert_allclose(Bp[spole], 0.)
289 |
290 | Bx = (np.cos(np.radians(180.))*np.cos(np.radians(phi))*Bt[1, :]
291 | - np.sin(np.radians(phi))*Bp[1, :])
292 | np.testing.assert_allclose(Bx, -np.ones((10,)))
293 |
294 | def test_design_matrix(self):
295 | """
296 | Test matrices for time-dependent field model using B-spline basis.
297 |
298 | """
299 |
300 | n_data = int(300)
301 | t_start = 1997.1
302 | t_end = 2018.1
303 | n_breaks = int((t_end - t_start) / 0.5 + 1)
304 | time = np.linspace(t_start, t_end, num=n_data)
305 | radius = R_REF * np.ones(time.shape)
306 | theta = np.linspace(1, 179, num=n_data)
307 | phi = np.linspace(-180, 179, num=n_data)
308 | n_static = int(80)
309 | n_tdep = int(20)
310 | order = int(6) # order of spline basis functions (4 = cubic)
311 |
312 | # create a knot vector without endpoint repeats and # add endpoint
313 | # repeats as appropriate for spline degree p
314 | knots = np.linspace(t_start, t_end, num=n_breaks)
315 | knots = m.augment_breaks(knots, order)
316 |
317 | s = timer()
318 | G_radius, G_theta, G_phi = m.design_matrix(knots, order, n_tdep, time,
319 | radius, theta, phi,
320 | n_static=n_static)
321 | e = timer()
322 |
323 | # load matfile
324 | test = load_matfile(MATFILE_PATH, 'test_design_matrix')
325 |
326 | G_radius_mat = test['G_radius']
327 | G_theta_mat = test['G_theta']
328 | G_phi_mat = test['G_phi']
329 | runtime = test['runtime']
330 |
331 | print(" Time for design_matrix computation (Python): ", e - s)
332 | print(" Time for design_matrix computation (Matlab): {:}".format(
333 | runtime[0, 0]))
334 |
335 | self.assertIsNone(
336 | np.testing.assert_allclose(G_radius, G_radius_mat, atol=1e-5))
337 | self.assertIsNone(
338 | np.testing.assert_allclose(G_theta, G_theta_mat, atol=1e-5))
339 | self.assertIsNone(
340 | np.testing.assert_allclose(G_phi, G_phi_mat, atol=1e-5))
341 |
342 | def test_colloc_matrix(self):
343 |
344 | breaks = np.linspace(0., 300., 20)
345 | order = 6
346 | knots = m.augment_breaks(breaks, order)
347 |
348 | x = np.linspace(0., 300., 1000)
349 |
350 | test = load_matfile(MATFILE_PATH, 'test_colloc_matrix')
351 |
352 | colloc_m = test['colloc']
353 |
354 | for deriv in range(order):
355 | print(f"Checking deriv = {deriv} of collocation matrix.")
356 |
357 | colloc = m.colloc_matrix(x, knots, order, deriv=deriv)
358 |
359 | self.assertIsNone(np.testing.assert_allclose(
360 | colloc, colloc_m[deriv::order], atol=1e-5))
361 |
362 |
363 | if __name__ == '__main__':
364 | main()
365 |
--------------------------------------------------------------------------------