Commit ecab3303 authored by Maximilian Schanner's avatar Maximilian Schanner
Browse files

Secular variation CLI + minors in formatting.

parent b6ab13c2
......@@ -111,6 +111,8 @@ test-debian:
- ./docs/pic_dip.png
- ./docs/pic_cfe.png
- ./docs/pic_cfs.png
- ./docs/pic_sve.png
- ./docs/pic_svs.png
only:
refs:
- master
......@@ -135,3 +137,4 @@ pages:
- public
only:
- master
Changelog
=========
2021-07-28 v0.1.4
-----------------
* CLI for secular variation
* minor changes in formatting
2021-07-26 v0.1.3
-----------------
* Add secular variation (No cli and uncertainties yet!)
......
......@@ -46,4 +46,4 @@ from pymagglobal.core import master_curve, dipole_series, file2splines, \
Model
from pymagglobal import utils
__version__ = '0.1.3'
__version__ = '0.1.4'
......@@ -36,12 +36,24 @@ cfe_ex_ofn = 'pic_cfe.png'
cfe_ex_cmd = 'pymagglobal coeffs-epoch 1700 arhimag1k'
'''CLI usage example for coeff-epoch'''
sve_ex_ofn = 'pic_sve.png'
'''Output file name for the secular variations at epoch example
:py:attr:`~sve_ex_cmd`'''
sve_ex_cmd = 'pymagglobal sec-vars-epoch 1700 arhimag1k'
'''CLI usage example for secular-variation-epoch'''
cfs_ex_ofn = 'pic_cfs.png'
'''Output file name for the coefficient series example
:py:attr:`~cfs_ex_cmd`'''
cfs_ex_cmd = 'pymagglobal coeffs-series --longterm -d 1 1 1 -o 0 1 -1 GGF100k'
'''CLI usage example for coeff-series'''
svs_ex_ofn = 'pic_svs.png'
'''Output file name for the secular variation series example
:py:attr:`~cfs_ex_cmd`'''
svs_ex_cmd = 'pymagglobal sec-vars-series -d 1 -o 0 gufm1'
'''CLI usage example for secular-variation-series'''
dip_ex_ofn = 'pic_dip.png'
'''Output file name for the dipole moment example :py:attr:`~dip_ex_cmd`'''
dip_ex_cmd = 'pymagglobal dipole CALS10k.2'
......@@ -77,11 +89,11 @@ class ListModelsAction(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
models_list = list(_models.keys())
models_list.sort(key=str.lower)
print(f'Currently available models are:')
print('Currently available models are:')
for it in models_list:
print(it)
print(f'Visit https://git.gfz-potsdam.de/sec23/korte/pymagglobal/'
f'-/tree/master/pymagglobal/dat for detailed information.')
print('Visit https://git.gfz-potsdam.de/sec23/korte/pymagglobal/'
'-/tree/master/pymagglobal/dat for detailed information.')
parser.exit()
......@@ -120,14 +132,14 @@ def argument_parser():
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('-h', '--help', action='help',
default=argparse.SUPPRESS,
help=f'Show this message and exit. Each of the '
f'commands above has its own --help, so use e.g. '
f'master --help to get information on how to create a '
f'master curve.')
help='Show this message and exit. Each of the '
'commands above has its own --help, so use e.g. '
'master --help to get information on how to create a '
'master curve.')
parser.add_argument('--version', action='version',
version=f'pymagglobal {__version__}')
parser.add_argument('--list-models', help=f'List available models and '
f'exit.', action=ListModelsAction)
parser.add_argument('--list-models', help='List available models and '
'exit.', action=ListModelsAction)
# parser for arguments that are shared between all commands
base_parser = argparse.ArgumentParser(add_help=False)
......@@ -135,19 +147,19 @@ def argument_parser():
help='Do not show the results.')
base_parser.add_argument('--output', metavar='<path/to/output>',
type=str, help='Where to store the outputs. If '
f'not given, no output is stored. The outputs '
f'are produced as simple tables and are self-'
f'explaining.')
'not given, no output is stored. The outputs '
'are produced as simple tables and are self-'
'explaining.')
base_parser.add_argument('--savefig', metavar='<path/to/figure.pdf>',
type=str, help=f'If given, save a figure at the '
f'given location. Use the file ending to '
f'specify the format.')
type=str, help='If given, save a figure at the '
'given location. Use the file ending to '
'specify the format.')
base_parser.add_argument('--longterm', action=LongtermAction,
help=f'This flag is intended for longterm models.'
f' If given, all times will be interpreted as '
f'ages before the year 1950 in ka (kiloyears), '
f'i.e. the epoch 10 refers to the year -8050 '
f'without the flag.')
help='This flag is intended for longterm models. '
'If given, all times will be interpreted as '
'ages before the year 1950 in ka (kiloyears), '
'i.e. the epoch 10 refers to the year -8050 '
'without the flag.')
# The default time unit is years AD
base_parser.set_defaults(t_unit='yrs.', t_label='t [yrs.]',
longterm=False)
......@@ -158,26 +170,26 @@ def argument_parser():
res_default = 'knots'
series_excl_group = series_base_parser.add_mutually_exclusive_group()
series_excl_group.add_argument('--res', metavar='n',
help=f'The resolution of the time series as'
f' a number of points. May also be "knots".'
f'In this case the time series is '
f'evaluated at the knots of the spline'
f'model. Default is {res_default}. '
f'Cannot be used together '
f'with --every.',
help='The resolution of the time series as '
'a number of points. May also be "knots".'
'In this case the time series is '
'evaluated at the knots of the spline'
'model. Default is {res_default}. '
'Cannot be used together '
'with --every.',
default=res_default)
series_excl_group.add_argument('--every', metavar='n', type=int,
help=f'Output a value of the time series '
f'every n years. Cannot be used together '
f'with --res.')
help='Output a value of the time series '
'every n years. Cannot be used together '
'with --res.')
series_base_parser.add_argument('--begin', metavar='b', type=float,
help=f'Beginning of the time series. If '
f'not given, use the earliest year '
f'possible.')
help='Beginning of the time series. If '
'not given, use the earliest year '
'possible.')
series_base_parser.add_argument('--end', metavar='e', type=float,
help=f'End of the time series. If '
f'not given, use the latest year '
f'possible.')
help='End of the time series. If '
'not given, use the latest year '
'possible.')
# the subparser for all commands
subparsers = parser.add_subparsers(dest='command', required=True)
......@@ -186,33 +198,33 @@ def argument_parser():
mst_epilog = f'''Example of use: :command:`{mst_ex_cmd}`
.. image:: ./{mst_ex_ofn}''' # sphinx-argparse requires a multi line string
mst_parser = subparsers.add_parser('master', help=f'Create a master curve '
f'for a specific location.',
mst_parser = subparsers.add_parser('master', help='Create a master curve '
'for a specific location.',
parents=[base_parser,
series_base_parser],
epilog=mst_epilog)
mst_parser.set_defaults(func=_commands.master_curve)
mst_parser.add_argument('lat', type=float,
help=f'The latitude at which to create the master '
f'curve.')
help='The latitude at which to create the master '
'curve.')
mst_parser.add_argument('lon', type=float,
help=f'The longitude at which to create the '
f'master curve.')
help='The longitude at which to create the '
'master curve.')
mst_parser.add_argument('--type', choices=['dif', 'nez'], type=str,
help=f'The type of the resulting field. '
f'May be either "dif" for declination, '
f'inclination and intensity (default) or "nez" '
f'for north, east and down components.',
help='The type of the resulting field. '
'May be either "dif" for declination, '
'inclination and intensity (default) or "nez" '
'for north, east and down components.',
default='dif')
# parser for the dipole moment series
dip_epilog = f'''Example of use: :command:`{dip_ex_cmd}`
.. image:: ./{dip_ex_ofn}''' # sphinx-argparse requires a multi line string
dip_parser = subparsers.add_parser('dipole', help=f'Create a time series '
f'of the dipole moment.',
dip_parser = subparsers.add_parser('dipole', help='Create a time series '
'of the dipole moment.',
parents=[base_parser,
series_base_parser],
epilog=dip_epilog)
......@@ -222,29 +234,49 @@ def argument_parser():
cfs_epilog = f'''Example of use: :command:`{cfs_ex_cmd}`
.. image:: ./{cfs_ex_ofn}'''
cfs_parser = subparsers.add_parser('coeffs-series', help=f'Output time '
f'series for specific coefficients.',
cfs_parser = subparsers.add_parser('coeffs-series', help='Output time '
'series for specific coefficients.',
parents=[base_parser,
series_base_parser],
epilog=cfs_epilog)
cfs_parser.set_defaults(func=_commands.coefficient_series)
cfs_parser.set_defaults(func=_commands.coefficients_series)
cfs_parser.add_argument('-d', '--degree', nargs='*', type=int,
help=f'the '
f'Degree for which to calculate the time series. '
f'Multiple values may be given.',
help='The Degree for which to calculate the time '
'series. Multiple values may be given.',
required=True)
cfs_parser.add_argument('-o', '--order', nargs='*', type=int, help='The '
'order for which to calculate the time series. '
'The same number as degrees has to be given.',
required=True)
# parser for secular variation time series
svs_epilog = f'''Example of use: :command:`{svs_ex_cmd}`
.. image:: ./{svs_ex_ofn}'''
svs_parser = subparsers.add_parser('sec-vars-series',
help='Output secular variation time '
'series for specific degree and order.',
parents=[base_parser,
series_base_parser],
epilog=svs_epilog)
svs_parser.set_defaults(func=_commands.secular_variations_series)
svs_parser.add_argument('-d', '--degree', nargs='*', type=int,
help='The Degree for which to calculate the time '
'series. Multiple values may be given.',
required=True)
cfs_parser.add_argument('-o', '--order', nargs='*', type=int, help=f'The '
f'order for which to calculate the time series. '
f'The same number as degrees has to be given.',
svs_parser.add_argument('-o', '--order', nargs='*', type=int, help='The '
'order for which to calculate the time series. '
'The same number as degrees has to be given.',
required=True)
# parser for coefficients at epoch
cfe_epilog = f'''Example of use: :command:`{cfe_ex_cmd}`
.. image:: ./{cfe_ex_ofn}'''
cfe_parser = subparsers.add_parser('coeffs-epoch', help=f'Output '
f'coefficients for a specific epoch.',
cfe_parser = subparsers.add_parser('coeffs-epoch', help='Output '
'coefficients for a specific epoch.',
parents=[base_parser],
epilog=cfe_epilog)
cfe_parser.set_defaults(func=_commands.coefficients_epoch)
......@@ -252,25 +284,39 @@ def argument_parser():
cfe_parser.add_argument('epoch', type=float, help='The epoch for which '
'the coeffients are returned.')
# parser for secular variations at epoch
sve_epilog = f'''Example of use: :command:`{sve_ex_cmd}`
.. image:: ./{sve_ex_ofn}'''
sve_parser = subparsers.add_parser('sec-vars-epoch',
help='Output secular variations for a '
'specific epoch.',
parents=[base_parser],
epilog=sve_epilog)
sve_parser.set_defaults(func=_commands.secular_variations_epoch)
sve_parser.add_argument('epoch', type=float, help='The epoch for which '
'the secular variations are returned.')
# parser for maps
map_epilog = f'''Example of use: :command:`{map_ex_cmd}`
.. image:: ./{map_ex_ofn}'''
map_parser = subparsers.add_parser('map', help=f'Output a grid of lat-lon-'
f'pairs and field components at the '
f'locations for a specific epoch. Note'
f' that the grid is equidistributed '
f' over the globe, so it may appear '
f'unconventional.',
map_parser = subparsers.add_parser('map', help='Output a grid of lat-lon-'
'pairs and field components at the '
'locations for a specific epoch. Note '
'that the grid is equidistributed '
'over the globe, so it may appear '
'unconventional.',
parents=[base_parser],
epilog=map_epilog)
map_parser.set_defaults(func=_commands.maps)
map_parser.add_argument('--type', choices=['dif', 'nez'], type=str,
help=f'The type of the resulting field. '
f'May be either "dif" for declination, '
f'inclination and intensity (default) or "nez" '
f'for north, east and down components.',
help='The type of the resulting field. '
'May be either "dif" for declination, '
'inclination and intensity (default) or "nez" '
'for north, east and down components.',
default='dif')
map_parser.add_argument('epoch', type=float, help='The epoch for which '
'the field map is returned.')
......@@ -282,10 +328,10 @@ def argument_parser():
default=map_res_default)
# the model to be used
parser.add_argument('model', type=str, help=f'The model to be used.'
f'Use pymagglobal --list-models to get a list of '
f'the included models or pass a <path/to/your_model> '
f'to parse your own model.', action=ModelAction)
parser.add_argument('model', type=str, help='The model to be used.'
'Use pymagglobal --list-models to get a list of '
'the included models or pass a <path/to/your_model> '
'to parse your own model.', action=ModelAction)
return parser
......
......@@ -242,7 +242,7 @@ def dipole_series(args):
return fig
def coefficient_series(args):
def coefficients_series(args):
'''Handle the coeff-series command and provide the model time series for
given coefficients.
......@@ -286,7 +286,7 @@ def coefficient_series(args):
fig = plt.figure(figsize=(10, 7))
ax = fig.add_subplot(111)
ax.set_title(f'Coefficients series for {args.model.name}')
ax.set_ylabel(fr'$g_\ell^m$ [nT]')
ax.set_ylabel(r'$g_\ell^m$ [nT]')
for ind, ell, m in zip(inds, args.degree, args.order):
ax.plot(times, coeffs[:, ind], label=rf'$\ell={ell}, m={m}$')
if args.longterm:
......@@ -299,6 +299,63 @@ def coefficient_series(args):
return fig
def secular_variations_series(args):
'''Handle the secular-variations-series command and provide the model time
series for given degree and order.
Parameters
----------
args : object
the object returned by ArgumentParser.parse_args().
Returns
-------
matplotlib.figure.Figure
If the --no-show flag is not set or a savefig output is specified,
return a matplotlib.figure.Figure object, containing a plot of the
coefficient series.
'''
inds = args.model.valid_degrees_orders(args.degree, args.order)
times = args2times(args)
# evaluate the splines at the epoch, check is performed in args2times
_, _, svs = core.secular_variation(times, args.model, check=False)
if args.longterm:
times = yr2lt(times)
# if an output is specified, save the result to text
if args.output is not None:
s = ''
fmt = ['%.2f']
for ell, m in zip(args.degree, args.order):
s = ''.join([s, f'({int(ell)}, {m}),'])
fmt.append('%1.7e')
np.savetxt(args.output,
np.concatenate((np.atleast_2d(times).T,
svs[:, inds]),
axis=1),
fmt=fmt,
delimiter=',',
header=f'Secular variation series for {args.model.name} in '
'nT/yr. The tuples give the degree and order of the '
f'series.\ntime [{args.t_unit}],' + s[:-1])
# if the --no-show flag is not set, plot the results
if not args.no_show or args.savefig is not None:
fig = plt.figure(figsize=(10, 7))
ax = fig.add_subplot(111)
ax.set_title(f'Secular variation series for {args.model.name}')
ax.set_ylabel(r'$\dot{g}_\ell^m$ [nT/yr]')
for ind, ell, m in zip(inds, args.degree, args.order):
ax.plot(times, svs[:, ind], label=rf'$\ell={ell}, m={m}$')
if args.longterm:
ax.invert_xaxis()
ax.set_xlabel(args.t_label)
else:
ax.set_xlabel(utils._nicelabel(args.t_label))
ax.legend()
fig.tight_layout()
return fig
def coefficients_epoch(args):
'''Handle the coeffs-epoch command and provide the model coefficients for
a given epoch.
......@@ -323,14 +380,46 @@ def coefficients_epoch(args):
np.array([ls, ms, coeffs]).T,
fmt=('%d', '%d', '%1.7e'),
delimiter=',',
header=f'Coefficients for {args.model.name} at epoch '
header=f'Coefficients for {args.model.name} in nT at epoch '
f'{args.epoch} {args.t_unit}\n'
f'l,m,g_l^m')
if not args.no_show or args.savefig is not None:
return plot_coeffs(coeffs, ls, ms, args)
def plot_coeffs(gs, ls, ms, args):
def secular_variations_epoch(args):
'''Handle the secular-variation-epoch command and provide the model
secular variation for a given epoch.
Parameters
----------
args : object
the object returned by ArgumentParser.parse_args().
Returns
-------
matplotlib.figure.Figure
If the --no-show flag is not set or a savefig output is specified,
return a matplotlib.figure.Figure object, containing a plot of the
coefficients.
'''
# evaluate the splines at the epoch
ls, ms, svs = core.secular_variation(args.epoch, args.model)
# if an output is specified, save the result to text
if args.output is not None:
np.savetxt(args.output,
np.array([ls, ms, svs]).T,
fmt=('%d', '%d', '%1.7e'),
delimiter=',',
header=f'Secular variations for {args.model.name} in nT/yr '
f'at epoch {args.epoch} {args.t_unit}\n'
f'l,m,g_l^m')
if not args.no_show or args.savefig is not None:
return plot_coeffs(svs, ls, ms, args, unit='nT/yr',
name=r'Secular variations $\dot{g}_\ell^m$')
def plot_coeffs(gs, ls, ms, args, unit='nT', name=r'Coefficients $g_\ell^m$'):
'''Plot Gauss coefficients.
Parameters
......@@ -341,6 +430,10 @@ def plot_coeffs(gs, ls, ms, args):
SH degrees.
ms : ndarray
SH orders.
unit : str, optional
The unit of the coeffs. Default is nT.
name : str, optional
Headline name of what is being plotted.
Returns
-------
......@@ -352,15 +445,15 @@ def plot_coeffs(gs, ls, ms, args):
fig, axs = plt.subplots(ncols=1, nrows=lmax, sharex=True,
figsize=(10, 10),
subplot_kw={'xlim': (-lmax-0.1, lmax+0.1)})
fig.suptitle(f'Coefficients $g_\ell^m$ for {args.model.name} at epoch '
fig.suptitle(f'{name} for {args.model.name} at epoch '
f'{args.epoch} {args.t_unit}\n'
f'Units are nT.')
for l, ax in enumerate(axs, start=1):
mask = np.equal(ls, l)
f'Units are {unit}.')
for ell, ax in enumerate(axs, start=1):
mask = np.equal(ls, ell)
ax.scatter(ms[mask], gs[mask], marker='x')
ymax = 1.1*np.abs(gs[mask]).max()
ax.set_ylim(-ymax, ymax)
ax.set_ylabel(f'$\ell = {l}$')
ax.set_ylabel(fr'$\ell = {ell}$')
ax.hlines(0, *ax.get_xlim(), color='gray', lw=0.5)
axs[-1].set_xticks(range(-lmax, lmax+1))
axs[-1].set_xlabel('$m$')
......
......@@ -167,6 +167,22 @@ class ParserTestCoefficientsEpoch(_SetUpCls):
fig.savefig(f'./docs/{ofn}', bbox_inches='tight')
class ParserTestSecularVariationsEpoch(_SetUpCls):
'''Test-cases from the `sec-vars-epoch` sub-command'''
def test_usage_valid(self):
'''Check if the example of use is parsed without error'''
from pymagglobal.__main__ import sve_ex_cmd as cmd
self.parser.parse_args(cmd.split()[1:])
def test_command_run(self):
''' Check if the example runs flawlessly '''
from pymagglobal.__main__ import sve_ex_cmd as cmd, sve_ex_ofn as ofn
nsp = self.parser.parse_args(cmd.split()[1:])
fig = nsp.func(nsp)
fig.savefig(f'./docs/{ofn}', bbox_inches='tight')
class ParserTestCoefficientSeries(_SetUpCls):
'''Test-cases from the `coeff-series` sub-command'''
......@@ -180,5 +196,20 @@ class ParserTestCoefficientSeries(_SetUpCls):
from pymagglobal.__main__ import cfs_ex_cmd as cmd, cfs_ex_ofn as ofn
nsp = self.parser.parse_args(cmd.split()[1:])
fig = nsp.func(nsp)
# TODO: use --no-show together with --savefig
fig.savefig(f'./docs/{ofn}', bbox_inches='tight')
class ParserTestSecularVariationsSeries(_SetUpCls):
'''Test-cases from the `sec-vars-series` sub-command'''
def test_usage_valid(self):
'''Check if the example of use is parsed without error'''
from pymagglobal.__main__ import svs_ex_cmd as cmd
self.parser.parse_args(cmd.split()[1:])
def test_command_run(self):
''' Check if the example runs flawlessly '''
from pymagglobal.__main__ import svs_ex_cmd as cmd, svs_ex_ofn as ofn
nsp = self.parser.parse_args(cmd.split()[1:])
fig = nsp.func(nsp)
fig.savefig(f'./docs/{ofn}', bbox_inches='tight')
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment