diff --git a/README.md b/README.md index e77fb6babf1310336fd10bb1428223596208a54d..44869c1164b9cc6a336bdbc444b2bee36a76edbf 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,12 @@ $ pip3 install pymagglobal --extra-index-url https://public:5mz_iyigu-WE3HySBH1J Since [conda](https://docs.conda.io/) version 4.6, conda and pip get along well. So you can also run `pip3 install ...` from inside your conda environment. ## Documentation -Check out the extended documention [here](https://sec23.git-pages.gfz-potsdam.de/korte/pymagglobal). From the command line, you can use `pymagglobal` to get various results from the models. For example, +Check out the extended documention [here](https://sec23.git-pages.gfz-potsdam.de/korte/pymagglobal). +pymagglobal comes with a GUI, that can be started from the command line via +```console +$ pymagglobal-gui +``` +You can also use pymagglobal directly from the command line, to get various results from the models. For example, ```console $ pymagglobal dipole gufm1 ``` @@ -85,26 +90,19 @@ We can first use `built_in_models`, to access a dictionary of available models: ```python models = built_in_models() ``` -Using the function `file2splines` you can get a spline object, representing the model. For example, to get a spline object for gufm1, use -```python -gufm1_splines = pymagglobal.file2splines(models['gufm1']) -``` -This object can be evaluated to get the coefficients for a specific epoch +pymagglobal provides a `Model` class. Built-in models can be accessed directly, custom models are set up with a name and a path: ```python -gufm1_1600 = gufm1_splines(1600) +gufm1 = pymagglobal.Model('gufm1') +my_model = pymagglobal.Model('My model', '<path/to/my_model.dat>') ``` -or passed to other routines in pymagglobal. For example, to get the dipole series from above use +The model can be passed to routines in pymagglobal. For example, to get the dipole series from above use ```python import numpy as np times = np.linspace(1590, 1990, 201) -gufm1_dipoles = pymagglobal.dipole_series(times, gufm1_splines) -``` -Additionally, pymagglobal provides a `Model` class, which is set up with a path and a name: -```python -gufm1 = pymagglobal.Model('gufm1', models['gufm1']) +gufm1_dipoles = pymagglobal.dipole_series(times, gufm1) ``` -The object now contains several quantities of interest, for example the minimal and maximal time for which the model is valid +Additionally, the object contains several quantities of interest, for example the minimal and maximal time for which the model is valid ```python >>> gufm1.t_min 1590.0 diff --git a/docs/index.rst b/docs/index.rst index 8b26d86c1c18819c45b7f176efc11eea491396a8..3d538e62c779fa7f00f2f9b9781f56f32cae31ff 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,22 +1,20 @@ A python interface for global geomagnetic field models ====================================================== -`pymagglobal` serves the purpose of replacing some Fortran scripts, which are used in the geomagnetism community to evaluate global field models. +:code:`pymagglobal` serves the purpose of replacing some Fortran scripts, which are used in the geomagnetism community to evaluate global field models. It can be applied to all cubic-spline based geomagnetic field models stored in the same file format as `gufm1` or the `CALSxk` model series. -.. toctree:: - :maxdepth: 2 +:code:`pymagglobal` comes with a GUI, that can be started from the command line via - package_documentation - command_line_interface - examples - CHANGELOG +.. code-block:: bash + + $ pymagglobal-gui Installation ------------ .. note:: - pymagglobal depends on `cartopy <https://scitools.org.uk/cartopy>`_. You have to install it, before running the install command. +:code:`pymagglobal` depends on :code:`cartopy <https://scitools.org.uk/cartopy>`_. You have to install it, before running the install command. This should also help if you receive :code:`ImportError: NumPy 1.10+ is required to install cartopy.` diff --git a/pymagglobal/GUI.py b/pymagglobal/GUI.py new file mode 100644 index 0000000000000000000000000000000000000000..ee48376889d17d19349ee2aba4ba57e53a8ae652 --- /dev/null +++ b/pymagglobal/GUI.py @@ -0,0 +1,554 @@ +# This file is part of pymagglobal +# +# Copyright (C) 2020 Helmholtz Centre Potsdam +# GFZ German Research Centre for Geosciences, Potsdam, Germany +# (https://www.gfz-potsdam.de) +# +# pymagglobal is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# pymagglobal is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +'''This is a graphic interface for pymagglobal based on pyQt5 widgets +''' + +import sys + +from pathlib import Path + +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QLabel, \ + QLineEdit, QVBoxLayout, QHBoxLayout, QComboBox, QFrame, QFileDialog, \ + QRadioButton +from PyQt5.QtGui import QDoubleValidator, QValidator + +import matplotlib +from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg, \ + NavigationToolbar2QT +from matplotlib import pyplot as plt +from cartopy import crs as ccrs + +from pymagglobal import built_in_models, Model +from pymagglobal.__main__ import argument_parser +from pymagglobal._commands import local_curve, maps, dipole_series + +matplotlib.use('Qt5Agg') + +plt.rcParams.update({'font.size': 16}) + +titleStyle = 'font-size: 18pt; font-weight: bold;' + + +class MainWindow(QWidget): + def __init__(self): + super().__init__() + self.parser = argument_parser() + self.saveWindow = SaveWindow() + + self.initUI() + self.updateModel() + self.plotDipole() + + def initUI(self): + ''' Set up the layout and positions of menus and plots on the window. + ''' + # create vertical layout for the parameters + paramBox = QVBoxLayout() + # add 'model selection' panel + paramBox.addWidget(self.modelSelect()) + # add seperation line + paramBox.addWidget(QHLine()) + # add'site parameters' panel + paramBox.addWidget(self.localMenu()) + # add seperation line + paramBox.addWidget(QHLine()) + # add 'global calculate' panel + paramBox.addWidget(self.globalMenu()) + # add seperation line + paramBox.addWidget(QHLine()) + # add 'save output' panel + paramBox.addWidget(self.saveMenu()) + paramBox.setAlignment(Qt.AlignTop) + + # put the vertical layout paramBox into the widget paramWidget + paramWidget = QWidget() + paramWidget.setLayout(paramBox) + paramWidget.setFixedWidth(300) + paramWidget.move(0, 0) + + # create plotting environment + self.fig = plt.figure() + self.canvas = FigureCanvasQTAgg(self.fig) + + # make the background transparent + self.fig.patch.set_facecolor('None') + self.canvas.setStyleSheet('background-color: transparent;') + + # add the typical mpl toolbar + toolbar = NavigationToolbar2QT(self.canvas, self) + + # put the plotting environment into a widget + plotBox = QVBoxLayout() + plotBox.addWidget(toolbar) + plotBox.addWidget(self.canvas) + plotWidget = QWidget() + plotWidget.setLayout(plotBox) + + # combine the parameter and plotting widget into a global one + mwBox = QHBoxLayout() + mwBox.addWidget(paramWidget) + mwBox.addWidget(plotWidget) + + # set up the main widget + self.setLayout(mwBox) + self.setGeometry(300, 300, 1300, 900) + self.setWindowTitle('pymagglobal') + self.show() + + def updateModel(self): + ''' updates the model, when the selected model changes ''' + model = Model(self.modelSelector.currentText()) + # update the range + self.epoch.min = model.t_min + self.epoch.min = model.t_max + self.epoch.default = int((model.t_max + model.t_min) / 2) + self.range.setText(f'[{model.t_min}, {model.t_max}]') + # update the model for saving + self.saveWindow.model = model + + def modelSelect(self): + ''' Model selection widget ''' + # title string + title = QLabel('Select model') + title.setStyleSheet(titleStyle) + title.setAlignment(Qt.AlignCenter) + # drop down menu + self.modelSelector = QComboBox() + self.modelSelector.addItems(built_in_models().keys()) + self.modelSelector.currentIndexChanged.connect(self.updateModel) + self.modelSelector.currentIndexChanged.connect(self.plotDipole) + self.modelSelector.setFixedHeight(25) + # HOTFIX: this fixed scaling issues with the text... + self.modelSelector.setStyleSheet("QAbstractItemView {" + " selection-background-color:" + " lightgray;" + "}") + # show the model range + rangeLabel = QLabel('Time range (yrs.):') + rangeLabel.setAlignment(Qt.AlignCenter) + self.range = QLabel() + self.range.setAlignment(Qt.AlignCenter) + # put into widget + hbox = QVBoxLayout() + hbox.addWidget(title) + hbox.addWidget(self.modelSelector) + hbox.addWidget(rangeLabel) + hbox.addWidget(self.range) + hbox.setAlignment(Qt.AlignTop) + # return a widget containing the selector + q = QWidget() + q.setLayout(hbox) + q.setFixedHeight(140) + return q + + def localMenu(self): + ''' Local plot widget ''' + # title string + title = QLabel('Local plots') + title.setStyleSheet(titleStyle) + title.setAlignment(Qt.AlignCenter) + # lat and lon input fields + latLonLabel = QLabel('Position (lat, lon)') + latLonLabel.setAlignment(Qt.AlignCenter) + + self.lat = QRangeEdit('0', -90, 90) + self.lat.setFixedWidth(50) + self.lat.setFixedHeight(22) + self.lon = QRangeEdit('0', -180, 180) + self.lon.setFixedWidth(50) + self.lon.setFixedHeight(22) + # set up a layout (looks nicer than adding the widgets directly) + latLonBox = QHBoxLayout() + latLonBox.addWidget(self.lat) + latLonBox.addWidget(self.lon) + latLonWidget = QWidget() + latLonWidget.setLayout(latLonBox) + # radio buttons for field type + typeLabel = QLabel('Field type') + typeLabel.setAlignment(Qt.AlignCenter) + + self.locNezButton = QRadioButton("NEZ") + self.locDifButton = QRadioButton("DIF") + self.locDifButton.setChecked(True) + # set up a layout + typeBox = QHBoxLayout() + typeBox.addWidget(self.locNezButton) + typeBox.addWidget(self.locDifButton) + typeBox.setAlignment(Qt.AlignCenter) + typeWidget = QWidget() + typeWidget.setLayout(typeBox) + # Plot button + sButton = QPushButton('Plot local') + sButton.clicked.connect(self.plotLocal) + # Put into widget + vbox = QVBoxLayout() + vbox.addWidget(title) + vbox.addWidget(latLonLabel) + vbox.addWidget(latLonWidget) + vbox.addWidget(typeLabel) + vbox.addWidget(typeWidget) + vbox.addWidget(sButton) + vbox.setAlignment(Qt.AlignTop) + # return a widget containing the local stuff + q = QWidget() + q.setLayout(vbox) + q.setFixedHeight(240) + return q + + def globalMenu(self): + ''' Global plot widget ''' + # title string + title = QLabel('Global plots') + title.setStyleSheet(titleStyle) + title.setAlignment(Qt.AlignCenter) + # epoch input field + epochLabel = QLabel('Epoch [yrs.]') + epochLabel.setAlignment(Qt.AlignCenter) + self.epoch = QRangeEdit('1900', -1000, 2000) + self.epoch.setFixedWidth(100) + self.epoch.setFixedHeight(22) + # radio buttons for field type + typeLabel = QLabel('Field type') + typeLabel.setAlignment(Qt.AlignCenter) + + self.mapNezButton = QRadioButton("NEZ") + self.mapDifButton = QRadioButton("DIF") + self.mapDifButton.setChecked(True) + + typeBox = QHBoxLayout() + typeBox.addWidget(self.mapNezButton) + typeBox.addWidget(self.mapDifButton) + typeBox.setAlignment(Qt.AlignCenter) + typeWidget = QWidget() + typeWidget.setLayout(typeBox) + # redundant, but looks better as the location selector is in a hbox + epochBox = QHBoxLayout() + epochBox.addWidget(self.epoch) + epochWidget = QWidget() + epochWidget.setLayout(epochBox) + + # buttons for map and dipole + gButton = QPushButton('Plot map') + gButton.clicked.connect(self.plotMap) + + dButton = QPushButton('Plot dipole') + dButton.clicked.connect(self.plotDipole) + # put into widget + vbox = QVBoxLayout() + vbox.addWidget(title) + vbox.addWidget(epochLabel) + vbox.addWidget(epochWidget) + vbox.addWidget(typeLabel) + vbox.addWidget(typeWidget) + vbox.addWidget(gButton) + vbox.addWidget(dButton) + vbox.setAlignment(Qt.AlignTop) + # return a widget containing the global stuff + q = QWidget() + q.setLayout(vbox) + q.setFixedHeight(270) + return q + + def saveMenu(self): + ''' Save button widget ''' + # title string + title = QLabel('Model output') + title.setStyleSheet(titleStyle) + title.setAlignment(Qt.AlignCenter) + # the button will open another window, which handles the output + gButton = QPushButton('Generate') + gButton.clicked.connect(self.saveWindow.show) + + vbox = QVBoxLayout() + vbox.addWidget(title) + vbox.addWidget(gButton) + # return the save button widget + q = QWidget() + q.setLayout(vbox) + q.setFixedHeight(120) + return q + + def plotLocal(self): + ''' This will update the plotting section, when the plot local button + is pressed. It basically turns the inputs into arguments for the CLI. + ''' + # clear the figure + self.fig.clf() + # get the field type + if self.locDifButton.isChecked(): + _type = 'dif' + else: + _type = 'nez' + # run the _command with the respective arguments + local_curve(self.parser.parse_args(['local', + '--type', + _type, + self.lat.text(), + self.lon.text(), + self.modelSelector.currentText()]), + fig=self.fig) + # update the plotting canvas + self.canvas.draw() + + def plotMap(self): + ''' This will update the plotting section, when the plot map button + is pressed. It basically turns the inputs into arguments for the CLI. + ''' + # clear the figure + self.fig.clf() + # set up and configure grid for the maps + self.fig.add_subplot(221, projection=ccrs.Mollweide()) + self.fig.add_subplot(222, projection=ccrs.Mollweide()) + self.fig.add_subplot(212, projection=ccrs.Mollweide()) + self.fig.tight_layout() + self.fig.subplots_adjust(top=0.9, bottom=0.1, hspace=0.4) + # get the field type + if self.mapDifButton.isChecked(): + _type = 'dif' + else: + _type = 'nez' + # run the _command with the respective arguments + maps(self.parser.parse_args(['map', + '--type', + _type, + self.epoch.text(), + self.modelSelector.currentText()]), + fig=self.fig) + # update the canvas + self.canvas.draw() + + def plotDipole(self): + ''' This will update the plotting section, when the plot dipole button + is pressed. It basically turns the inputs into arguments for the CLI. + ''' + self.fig.clf() + dipole_series(self.parser.parse_args(['dipole', + self.modelSelector + .currentText()]), + fig=self.fig) + self.canvas.draw() + + +class SaveWindow(QWidget): + + def __init__(self): + ''' This is another window, handling the output ''' + super().__init__() + # set up a custom parser + self.parser = argument_parser() + # set up an initial model + self.model = Model('gufm1') + # configure the window + self.resize(300, 300) + self.setWindowTitle('pymagglobal - output') + # set up a dict for the dropdown menu + self.selectorDict = {'model coefficients at an epoch': 'coeffs-epoch', + 'field values at a specific location': 'local'} + # drop down menu of what to store + self.typeSelector = QComboBox() + self.typeSelector.addItems(self.selectorDict.keys()) + self.typeSelector.currentIndexChanged.connect(self.updateInputBox) + # title string + info = QLabel('I want to store') + info.setAlignment(Qt.AlignLeft) + # dummy widget, will be updated according to the dropdown + self.inputWidget = QLabel() + # a button for storing + gButton = QPushButton('Store') + gButton.clicked.connect(self.save) + + self.swBox = QVBoxLayout() + self.swBox.addWidget(info) + self.swBox.addWidget(self.typeSelector) + self.swBox.addWidget(self.inputWidget) + + self.swBox.addWidget(gButton) + + self.swBox.setAlignment(Qt.AlignTop) + + # set up the main widget + self.setLayout(self.swBox) + self.updateInputBox() + + def save(self): + ''' This will assemble the selections and pass them to the + argument_parser ''' + # get the type of output + selected = self.selectorDict[self.typeSelector.currentText()] + # depending on the type of output, assemble the arguments + if selected == 'local': + if self.saveDifButton.isChecked(): + _type = 'dif' + else: + _type = 'nez' + params = '--type ' + _type + ' ' + self.lat.text() + ' ' \ + + self.lon.text() + elif selected == 'coeffs-epoch': + params = self.epoch.text() + # get an ouput filename + fname, _ = QFileDialog.getSaveFileName(self, 'Choose filename', + str(Path.home())) + # fname will be empty, if the cancel button is pressed or the dialog + # is closed. In this case, go back to the window (do nothing) + # otherwise, pass the arguments to the parser and close + if fname != '': + parse = selected + ' --no-show --output ' + fname + ' ' + params \ + + ' ' + self.model.name + + args = self.parser.parse_args(parse.split()) + args.func(args) + self.close() + + def updateInputBox(self): + ''' This will update the argument inputs, depending on the dropdown + selection ''' + # first clear the widget + self.inputWidget.deleteLater() + # get the input + selected = self.selectorDict[self.typeSelector.currentText()] + # according to the input, set up a new widget + if selected == 'local': + self.inputWidget = self.locSelector() + elif selected == 'coeffs-epoch': + self.inputWidget = self.epochSelector() + else: + self.inputWidget = QLabel(self.selectorDict[self.typeSelector + .currentText()]) + # insert the widget after the dropdown (position 3) + self.swBox.insertWidget(3, self.inputWidget) + # update + self.swBox.update() + self.updateGeometry() + + def locSelector(self): + ''' The argument widget for local output ''' + latLonLabel = QLabel('Position (lat, lon)') + latLonLabel.setAlignment(Qt.AlignCenter) + + self.lat = QRangeEdit('0', -90, 90) + self.lat.setFixedWidth(50) + self.lon = QRangeEdit('0', -180, 180) + self.lon.setFixedWidth(50) + + latLonBox = QHBoxLayout() + latLonBox.addWidget(self.lat) + latLonBox.addWidget(self.lon) + + latLonWidget = QWidget() + latLonWidget.setLayout(latLonBox) + # Radio buttons for field type + typeLabel = QLabel('Field type') + typeLabel.setAlignment(Qt.AlignCenter) + + self.saveNezButton = QRadioButton("NEZ") + self.saveDifButton = QRadioButton("DIF") + self.saveDifButton.setChecked(True) + + typeBox = QHBoxLayout() + typeBox.addWidget(self.saveNezButton) + typeBox.addWidget(self.saveDifButton) + typeBox.setAlignment(Qt.AlignCenter) + typeWidget = QWidget() + typeWidget.setLayout(typeBox) + # Put into widget + vbox = QVBoxLayout() + vbox.addWidget(latLonLabel) + vbox.addWidget(latLonWidget) + vbox.addWidget(typeLabel) + vbox.addWidget(typeWidget) + vbox.setAlignment(Qt.AlignTop) + + q = QWidget() + q.setLayout(vbox) + return q + + def epochSelector(self): + ''' The argument widget for coefficient output ''' + epochLabel = QLabel('Epoch [yrs.]') + epochLabel.setAlignment(Qt.AlignCenter) + + default = int((self.model.t_max + self.model.t_min) / 2) + self.epoch = QRangeEdit(str(default), + self.model.t_min, + self.model.t_max, + default) + self.epoch.setFixedWidth(100) + # redundant, but looks better as the location selector is in a hbox + epochBox = QHBoxLayout() + epochBox.addWidget(self.epoch) + epochWidget = QWidget() + epochWidget.setLayout(epochBox) + + # Put into widget + vbox = QVBoxLayout() + vbox.addWidget(epochLabel) + vbox.addWidget(epochWidget) + vbox.setAlignment(Qt.AlignTop) + + q = QWidget() + q.setLayout(vbox) + return q + + +class QHLine(QFrame): + ''' Horizontal separation lines on pyQt5 layout + ''' + def __init__(self): + super(QHLine, self).__init__() + self.setFrameShape(QFrame.HLine) + self.setFrameShadow(QFrame.Sunken) + + +class QRangeEdit(QLineEdit): + ''' A line edit that checks for input in a given range. If the input is + outside, a default value will be inserted. ''' + def __init__(self, init, _min, _max, default=0): + super().__init__(init) + self.min = _min + self.max = _max + self.default = default + + self.editingFinished.connect(self.validating) + + def validating(self): + validation_rule = QDoubleValidator(self.min, self.max, 4) + if (validation_rule.validate(self.text(), + 14)[0] == QValidator.Acceptable): + self.setFocus() + else: + self.setText(str(self.default)) + + +def main(): + app = QApplication(sys.argv) + app.setStyleSheet("QLabel{font-size: 16pt;}" + "QComboBox{font-size: 16pt;}" + "QPushButton{font-size: 16pt;}" + "QRadioButton{font-size: 16pt; height: 100px}" + "QLineEdit{font-size: 16pt;}") + + _ = MainWindow() + + sys.exit(app.exec_()) + + +if __name__ == '__main__': + main() diff --git a/pymagglobal/__init__.py b/pymagglobal/__init__.py index 94747ccf6e91816f0656ba84e4d4ce3e3f1913f2..ea1c7be253fa89bef7e59f246d1d934830067d23 100644 --- a/pymagglobal/__init__.py +++ b/pymagglobal/__init__.py @@ -46,4 +46,4 @@ from pymagglobal.core import local_curve, dipole_series, file2splines, \ Model from pymagglobal import utils -__version__ = '0.1.4' +__version__ = '1.0.0' diff --git a/pymagglobal/_commands.py b/pymagglobal/_commands.py index 31d04dadb4fff3a62c4ede7d8d0caeaf528e7e4a..d848fd558a4cb17593ad2146be11a954ddfdffcc 100644 --- a/pymagglobal/_commands.py +++ b/pymagglobal/_commands.py @@ -121,7 +121,7 @@ def lt2yr(times): return -times*1000 + 1950 -def local_curve(args): +def local_curve(args, fig=None): '''Handle the local command and create a local curve at a given location. @@ -129,6 +129,8 @@ def local_curve(args): ---------- args : object The SimpleNamespace object returned by ArgumentParser.parse_args(). + fig : matplotlib.figure.Figure, optional + A figure to plot into. This is used for the GUI. Returns ------- @@ -142,7 +144,7 @@ def local_curve(args): # create a local curve using the core function, check is performed # in args2times curves = core.local_curve(times, (args.lat, args.lon), args.model, - field_type=args.type, check=False) + field_type=args.type, check=False) # output formats for dif and nez components fmts = {'dif': ('%.2f', '%2.6f', '%2.6f', '%1.7e'), 'nez': ('%.2f', '%1.7e', '%1.7e', '%1.7e')} @@ -169,7 +171,8 @@ def local_curve(args): # if the --no-show flag is not set, plot the local curve if not args.no_show or args.savefig is not None: - fig = plt.figure(figsize=(10, 7)) + if fig is None: + fig = plt.figure(figsize=(10, 7)) fig.suptitle(f'Local curves at ({args.lat}°, {args.lon}°) ' f'for {args.model.name}') axs = np.empty(3, dtype=object) @@ -190,7 +193,7 @@ def local_curve(args): return fig -def dipole_series(args): +def dipole_series(args, fig=None): '''Handle the dipole command and create a dipole-moment time series for the given model. @@ -198,6 +201,8 @@ def dipole_series(args): ---------- args : object The object returned by ArgumentParser.parse_args(). + fig : matplotlib.figure.Figure, optional + A figure to plot into. This is used for the GUI. Returns ------- @@ -228,7 +233,8 @@ def dipole_series(args): # if the --no-show flag is not set, plot the time series if not args.no_show or args.savefig is not None: - fig = plt.figure(figsize=(10, 7)) + if fig is None: + fig = plt.figure(figsize=(10, 7)) ax = fig.add_subplot(111) ax.plot(times, dip_ser) ax.set_title(f'Dipole moment series for {args.model.name}') @@ -462,7 +468,7 @@ def plot_coeffs(gs, ls, ms, args, unit='nT', name=r'Coefficients $g_\ell^m$'): return fig -def maps(args): +def maps(args, fig=None): '''Handle the map command and create field map of the model for a given epoch. @@ -470,6 +476,8 @@ def maps(args): ---------- args : object the object returned by ArgumentParser.parse_args(). + fig : matplotlib.figure.Figure, optional + A figure to plot into. This is used for the GUI. Returns ------- @@ -507,49 +515,72 @@ def maps(args): # if the --no-show flag is not set, plot the map using cartopy if not args.no_show or args.savefig is not None: - vmaxs = np.max(np.abs(field), axis=1) - vmins = -vmaxs cmaps = ['RdBu', 'RdBu', 'RdBu'] units = [r'$\mu$T', r'$\mu$T', r'$\mu$T'] + levels = [10, 10, 10] + extends = ['neither', 'neither', 'neither'] if args.type == 'dif': field[2] /= 1000 - vmaxs[0] = 40 - vmins[0] = -vmaxs[0] - vmaxs[2] /= 1000 - vmins[2] = np.min(field[2]) + + levels[0] = [-40, -30, -20, -10, 0, 10, 20, 30, 40] + levels[1] = np.linspace(-90, 90, 11) + extends[0] = 'both' cmaps[2] = 'Blues' units[0] = r'deg.' units[1] = r'deg.' else: - vmaxs /= 1000 - vmins /= 1000 field /= 1000 + vmax = np.ceil(np.max(np.abs(field), axis=1)) + + for it in range(3): + levels[it] = np.linspace(-vmax[it], vmax[it], 11) + + cbar_hght = 0.08 + if fig is None: + proj = ccrs.Mollweide() + fig, axs = plt.subplots(1, 3, figsize=(13, 3.4), + subplot_kw={'projection': proj}) + fig.tight_layout() + fig.subplots_adjust(top=0.8, bottom=0.25, wspace=0.1) + colaxs = [] + for it in range(3): + bnds = axs[it].get_position().bounds + + colaxs.append(fig.add_axes([bnds[0], + bnds[1]-0.06-cbar_hght, + bnds[2], + cbar_hght])) + cbar_orientation = 'horizontal' + else: + axs = fig.get_axes() + proj = axs[0].projection + + colaxs = [] + for it in range(3): + bnds = axs[it].get_position().bounds + colaxs.append(fig.add_axes([bnds[0], + bnds[1]-0.02-0.5*cbar_hght, + bnds[2], + 0.5*cbar_hght])) + cbar_orientation = 'horizontal' - cbar_hght = 0.06 - proj = ccrs.Mollweide() plt_lat, plt_lon, _ = proj.transform_points(ccrs.Geodetic(), z_at[1], 90 - z_at[0]).T - fig, axs = plt.subplots(1, 3, figsize=(13, 3), - subplot_kw={'projection': proj}) fig.suptitle(f'Field maps for {args.model.name} at epoch ' f'{args.epoch} {args.t_unit}') for it in range(3): - mappable = axs[it].tripcolor(plt_lat, plt_lon, field[it], - vmin=vmins[it], vmax=vmaxs[it], - rasterized=True, cmap=cmaps[it]) + mappable = axs[it].tricontourf(plt_lat, plt_lon, field[it], + cmap=cmaps[it], + levels=levels[it], + extend=extends[it]) axs[it].coastlines(alpha=0.8, lw=0.5) axs[it].set_global() axs[it].set_title(f'{utils._names[args.type][it]} [{units[it]}]') - bnds = axs[it].get_position().bounds - # colorbar for the mean - colax = fig.add_axes([bnds[0], - bnds[1]-0.1-cbar_hght, - bnds[2], - cbar_hght]) + colaxs[it].tick_params(labelsize=12) fig.colorbar(mappable, - cax=colax, - orientation='horizontal') + cax=colaxs[it], + orientation=cbar_orientation) return fig diff --git a/setup.py b/setup.py index 1e4f1ae24655bb07fb97630f5d6b47ea4d1d8817..c6dbf51511a88164faaa4d4a329520f89da1c011 100644 --- a/setup.py +++ b/setup.py @@ -44,8 +44,8 @@ with open("README.md", "r") as fh: name = 'pymagglobal' version = get_version('pymagglobal/__init__.py') description = '''python interface for global geomagnetic field models ''' -copyright = f'2020 Helmholtz Centre Potsdam GFZ, ' \ - + f'German Research Centre for Geosciences, Potsdam, Germany' +copyright = '2020 Helmholtz Centre Potsdam GFZ, ' \ + + 'German Research Centre for Geosciences, Potsdam, Germany' setuptools.setup( @@ -61,11 +61,14 @@ setuptools.setup( 'numpy>=1.18', 'scipy>=1.5.4', 'matplotlib>=2.2.5', - 'cartopy>=0.17' + 'cartopy>=0.17', + 'PyQt5>=5.12' ], extras_require={'tests': ['orthopoly>=0.9', 'packaging']}, package_data={'pymagglobal': ['dat/*']}, include_package_data=True, - entry_points={'console_scripts': [f'pymagglobal = ' - f'pymagglobal.__main__:main']}, + entry_points={'console_scripts': ['pymagglobal = ' + 'pymagglobal.__main__:main', + 'pymagglobal-gui = ' + 'pymagglobal.GUI:main']}, )