Commit 6e3e015e authored by Maximilian Schanner's avatar Maximilian Schanner
Browse files

Merge branch 'GUI'

parents 440dbe51 3dab85d6
......@@ -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
......
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: 1
: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.`
......
# 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)