├── .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 | --------------------------------------------------------------------------------