From 74dacd01803cadbdf62c31daad6bc99edddbcbb4 Mon Sep 17 00:00:00 2001 From: dominik Date: Tue, 5 Apr 2022 16:54:12 +0200 Subject: [PATCH] added Gaussian, Lorentzian, Pseudo-Voigt --- nmreval/gui_qt/_py/basewindow.py | 11 +- nmreval/gui_qt/lib/stuff.py | 311 ++++++++++++++++++++++++++++-- nmreval/gui_qt/main/mainwindow.py | 7 + nmreval/models/__init__.py | 1 + nmreval/models/spectrum.py | 92 +++++++++ nmreval/models/wideline.py | 12 +- resources/_ui/basewindow.ui | 8 +- 7 files changed, 418 insertions(+), 24 deletions(-) create mode 100644 nmreval/models/spectrum.py diff --git a/nmreval/gui_qt/_py/basewindow.py b/nmreval/gui_qt/_py/basewindow.py index ab7e51c..5e9fe2a 100644 --- a/nmreval/gui_qt/_py/basewindow.py +++ b/nmreval/gui_qt/_py/basewindow.py @@ -2,12 +2,14 @@ # Form implementation generated from reading ui file 'resources/_ui/basewindow.ui' # -# Created by: PyQt5 UI code generator 5.9.2 +# Created by: PyQt5 UI code generator 5.12.3 # # WARNING! All changes made in this file will be lost! + from PyQt5 import QtCore, QtGui, QtWidgets + class Ui_BaseWindow(object): def setupUi(self, BaseWindow): BaseWindow.setObjectName("BaseWindow") @@ -355,6 +357,8 @@ class Ui_BaseWindow(object): self.actionTetris.setObjectName("actionTetris") self.actionUpdate = QtWidgets.QAction(BaseWindow) self.actionUpdate.setObjectName("actionUpdate") + self.actionMine = QtWidgets.QAction(BaseWindow) + self.actionMine.setObjectName("actionMine") self.menuSave.addAction(self.actionSave) self.menuSave.addAction(self.actionExportGraphic) self.menuSave.addAction(self.action_save_fit_parameter) @@ -444,6 +448,7 @@ class Ui_BaseWindow(object): self.menuStuff.addAction(self.actionSnake) self.menuStuff.addAction(self.actionLife) self.menuStuff.addAction(self.actionTetris) + self.menuStuff.addAction(self.actionMine) self.menubar.addAction(self.menuFile.menuAction()) self.menubar.addAction(self.menuWindow.menuAction()) self.menubar.addAction(self.menuData.menuAction()) @@ -594,12 +599,12 @@ class Ui_BaseWindow(object): self.action_no_range.setText(_translate("BaseWindow", "None")) self.action_x_range.setText(_translate("BaseWindow", "Visible x range")) self.action_custom_range.setText(_translate("BaseWindow", "Custom")) - self.actionSnake.setText(_translate("BaseWindow", "Worms")) + self.actionSnake.setText(_translate("BaseWindow", "Worm")) self.actionFunction_editor.setText(_translate("BaseWindow", "Function editor...")) self.actionLife.setText(_translate("BaseWindow", "Life...")) self.actionTetris.setText(_translate("BaseWindow", "Not Tetris")) self.actionUpdate.setText(_translate("BaseWindow", "Look for updates")) - + self.actionMine.setText(_translate("BaseWindow", "Mine")) from ..data.datawidget.datawidget import DataWidget from ..data.point_select import PointSelectWidget from ..data.signaledit.editsignalwidget import EditSignalWidget diff --git a/nmreval/gui_qt/lib/stuff.py b/nmreval/gui_qt/lib/stuff.py index 3c2440c..9af81bb 100644 --- a/nmreval/gui_qt/lib/stuff.py +++ b/nmreval/gui_qt/lib/stuff.py @@ -1,4 +1,5 @@ import random +import time import numpy as np @@ -144,9 +145,9 @@ class Board(QtWidgets.QFrame): class SnakeBoard(Board): - SPEED = 100 - WIDTH = 30 - HEIGHT = 30 + SPEED = 125 + WIDTH = 35 + HEIGHT = 35 def __init__(self, parent=None): super().__init__(parent=parent) @@ -155,6 +156,9 @@ class SnakeBoard(Board): [int(SnakeBoard.WIDTH//2)+1, int(SnakeBoard.HEIGHT//2)]] self.current_x_head, self.current_y_head = self.snake[0] self.direction = 'l' + self.next_direction = [] + + self._direction_pairs = {'l': 'r', 'u': 'd', 'r': 'l', 'd': 'u'} self.food = None self.grow_snake = False @@ -176,6 +180,12 @@ class SnakeBoard(Board): self.update() def snake_move(self): + print(self.next_direction) + if self.next_direction: + turn = self.next_direction.pop() + if self.direction != self._direction_pairs[turn]: + self.direction = turn + if self.direction == 'l': self.current_x_head -= 1 elif self.direction == 'r': @@ -187,7 +197,7 @@ class SnakeBoard(Board): elif self.direction == 'd': self.current_y_head += 1 - head = [self.current_x_head, self.current_y_head] + head = (self.current_x_head, self.current_y_head) self.snake.insert(0, head) if not self.grow_snake: @@ -207,11 +217,11 @@ class SnakeBoard(Board): x = random.randint(3, SnakeBoard.WIDTH-3) y = random.randint(3, SnakeBoard.HEIGHT-3) - while [x, y] == self.snake[0]: + while (x, y) in self.snake: x = random.randint(3, SnakeBoard.WIDTH-3) y = random.randint(3, SnakeBoard.HEIGHT-3) - self.food = [x, y] + self.food = (x, y) def check_death(self): rip_message = '' @@ -234,20 +244,16 @@ class SnakeBoard(Board): def keyPressEvent(self, event): key = event.key() if key in (QtCore.Qt.Key_Left, QtCore.Qt.Key_A): - if self.direction != 'r': - self.direction = 'l' + self.next_direction.append('l') elif key in (QtCore.Qt.Key_Right, QtCore.Qt.Key_D): - if self.direction != 'l': - self.direction = 'r' + self.next_direction.append('r') elif key in (QtCore.Qt.Key_Down, QtCore.Qt.Key_S): - if self.direction != 'u': - self.direction = 'd' + self.next_direction.append('d') elif key in (QtCore.Qt.Key_Up, QtCore.Qt.Key_W): - if self.direction != 'd': - self.direction = 'u' + self.next_direction.append('u') else: return super().keyPressEvent(event) @@ -493,3 +499,280 @@ class MirrorL(Tetromino): SHAPE = np.array([[1, 0, 0, 0], [-1, -1, 0, 1]]) color = 'lightGray' + + +class Field(QtWidgets.QToolButton): + NUM_COLORS = { + 1: '#1e46a4', + 2: '#f28e2b', + 3: '#e15759', + 4: '#76b7b2', + 5: '#59a14f', + 6: '#edc948', + 7: '#b07aa1', + 8: '#ff9da7', + 'X': '#ff0000', + } + + flag_change = QtCore.pyqtSignal(bool) + + def __init__(self, x, y, parent=None): + super(Field, self).__init__(parent=parent) + + self.setFixedSize(QtCore.QSize(30, 30)) + f = self.font() + f.setPointSize(24) + f.setWeight(75) + self.setFont(f) + + self.x = x + self.y = y + + self.neighbors = [] + self.grid = self.parent() + + _size = self.grid.gridsize + for x_i in range(max(0, self.x-1), min(self.x+2, _size[0])): + for y_i in range(max(0, self.y-1), min(self.y+2, _size[1])): + if (x_i, y_i) == (self.x, self.y): + continue + self.neighbors.append((x_i, y_i)) + + self.is_mine = False + self.is_flagged = False + self.is_known = False + self.has_died = False + + def mousePressEvent(self, evt: QtGui.QMouseEvent) -> None: + if self.grid.status == 'finished': + return + + if evt.button() == QtCore.Qt.RightButton: + self.set_flag() + elif evt.button() == QtCore.Qt.LeftButton: + self.select() + else: + super().mousePressEvent(evt) + + def __str__(self) -> str: + if self.is_mine: + return 'X' + else: + return str(self.visible_mines) + + @property + def visible_mines(self) -> int: + return sum(self.grid.map[x_i][y_i].is_mine for x_i, y_i in self.neighbors) + + @QtCore.pyqtSlot() + def select(self): + if self.is_flagged: + return + + if self.is_mine: + self.setText('X') + num = 'X' + self.has_died = True + self.setStyleSheet(f'color: {self.NUM_COLORS[num]}') + else: + self.is_known = True + self.setEnabled(False) + self.setAutoRaise(True) + + num = self.visible_mines + if num != 0: + self.setText(str(num)) + self.setStyleSheet(f'color: {self.NUM_COLORS[num]}') + else: + self.grid.reveal_neighbors(self) + + self.clicked.emit() + + @QtCore.pyqtSlot() + def set_flag(self): + if self.is_flagged: + self.setText('') + else: + self.setText('!') + self.is_flagged = not self.is_flagged + self.has_died = False + + self.clicked.emit() + self.flag_change.emit(self.is_flagged) + + +class QMines(QtWidgets.QMainWindow): + LEVELS = { + 'easy': ((9, 9), 10), + 'middle': ((16, 16), 40), + 'hard': ((16, 30), 99), + 'very hard': ((16, 30), 10), + } + + def __init__(self, parent=None): + super().__init__(parent=parent) + + self.map = [] + + self.status = 'waiting' + self.open_fields = 0 + self.num_flags = 0 + + self._start = 0 + + self.gridsize, self.mines = (1, 1), 1 + + self._init_ui() + + def _init_ui(self): + layout = QtWidgets.QGridLayout() + layout.setSpacing(0) + self.central = QtWidgets.QWidget() + self.setCentralWidget(self.central) + + layout.addItem(QtWidgets.QSpacerItem(0, 0, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding), + 0, 0) + + self.grid_layout = QtWidgets.QGridLayout() + self.grid_layout.setSpacing(0) + self.map_widget = QtWidgets.QFrame() + self.map_widget.setFrameStyle(2) + self.map_widget.setLayout(self.grid_layout) + layout.addWidget(self.map_widget, 1, 1) + + self.new_game = QtWidgets.QPushButton('New game') + self.new_game.pressed.connect(self._init_map) + layout.addWidget(self.new_game, 0, 1) + + layout.addItem(QtWidgets.QSpacerItem(0, 0, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding), + 2, 2) + + self.central.setLayout(layout) + + self.timer = QtCore.QTimer() + self.timer.setInterval(1000) + self.timer.timeout.connect(self.update_time) + + self.timer_message = QtWidgets.QLabel('0 s') + self.statusBar().addWidget(self.timer_message) + + self.mine_message = QtWidgets.QLabel(f'0 / {self.mines}') + self.statusBar().addWidget(self.mine_message) + + self.dead_message = QtWidgets.QLabel('') + self.statusBar().addWidget(self.dead_message) + + self.level_cb = QtWidgets.QComboBox() + self.level_cb.addItems(list(self.LEVELS.keys())) + self.level_cb.currentTextChanged.connect(self._init_map) + self.statusBar().addPermanentWidget(self.level_cb) + + self._init_map('easy') + + def _init_map(self, lvl: str = None): + if lvl is None: + lvl = self.level_cb.currentText() + + self._clear_map() + + self.gridsize, self.mines = QMines.LEVELS[lvl] + w, h = self.gridsize + self.map = [[None] * h for _ in range(w)] + + for x in range(w): + for y in range(h): + pos = Field(x, y, parent=self) + pos.clicked.connect(self.start_game) + pos.clicked.connect(self.update_status) + pos.flag_change.connect(self.update_flag) + self.grid_layout.addWidget(pos, x+1, y+1) + self.map[x][y] = pos + + self.set_mines() + + def _clear_map(self): + self.status = 'waiting' + self.open_fields = 0 + self.num_flags = 0 + + self._start = 0 + + self.map = [] + + while self.grid_layout.count(): + child = self.grid_layout.takeAt(0) + w = child.widget() + if w: + self.grid_layout.removeWidget(w) + w.deleteLater() + else: + self.grid_layout.removeItem(child) + + def set_mines(self): + count = 0 + w, h = self.gridsize + while count < self.mines: + n = random.randint(0, w * h - 1) + row = n // h + col = n % h + pos = self.map[row][col] # type: Field + if pos.is_mine: + continue + pos.is_mine = True + count += 1 + + self.status = 'waiting' + self.open_fields = 0 + self.num_flags = 0 + self._start = 0 + + self.timer_message.setText('0 s') + self.mine_message.setText(f'0 / {self.mines}') + self.dead_message.setText('') + + def reveal_neighbors(self, pos: Field): + for x_i, y_i in pos.neighbors: + field_i = self.map[x_i][y_i] # type: Field + if field_i.isEnabled(): + field_i.select() + + def start_game(self): + if self.status == 'waiting': + self.status = 'running' + self._start = time.time() + self.timer.start(1000) + + def update_time(self): + if self.status == 'running': + self.timer_message.setText(f'{time.time()-self._start:3.0f} s') + + def update_status(self): + if self.status == 'finished': + return + + pos = self.sender() # type: Field + if pos.is_known: + self.open_fields += 1 + if self.open_fields == self.gridsize[0] * self.gridsize[1] - self.mines: + self.timer.stop() + + _ = QtWidgets.QMessageBox.information(self, 'Game finished', 'Game finished!!!') + self.status = 'finished' + + elif pos.has_died: + dead_cnt = self.dead_message.text() + if dead_cnt == '': + self.dead_message.setText('(Deaths: 1)') + else: + self.dead_message.setText(f'(Deaths: {int(dead_cnt[9:-1])+1})') + + @QtCore.pyqtSlot(bool) + def update_flag(self, state: bool): + num_mines = int(self.mine_message.text().split()[0]) + if state: + num_mines += 1 + else: + num_mines -= 1 + + self.mine_message.setText(f'{num_mines} / {self.mines}') + diff --git a/nmreval/gui_qt/main/mainwindow.py b/nmreval/gui_qt/main/mainwindow.py index 5eb8e42..cca3b5b 100644 --- a/nmreval/gui_qt/main/mainwindow.py +++ b/nmreval/gui_qt/main/mainwindow.py @@ -905,6 +905,7 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): @QtCore.pyqtSlot(name='on_actionSnake_triggered') @QtCore.pyqtSlot(name='on_actionTetris_triggered') @QtCore.pyqtSlot(name='on_actionLife_triggered') + @QtCore.pyqtSlot(name='on_actionMine_triggered') def spannung_spiel_und_spass(self): if self.sender() == self.actionLife: @@ -913,6 +914,12 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): game.setWindowModality(QtCore.Qt.NonModal) game.show() + elif self.sender() == self.actionMine: + from ..lib.stuff import QMines + game = QMines(parent=self) + game.setWindowModality(QtCore.Qt.NonModal) + game.show() + else: from ..lib.stuff import Game if self.sender() == self.actionSnake: diff --git a/nmreval/models/__init__.py b/nmreval/models/__init__.py index 56158df..68c2498 100755 --- a/nmreval/models/__init__.py +++ b/nmreval/models/__init__.py @@ -31,4 +31,5 @@ from .bds import * from .temperature import * from .transitions import * from .correlationfuncs import * +from .spectrum import * from .wideline import * diff --git a/nmreval/models/spectrum.py b/nmreval/models/spectrum.py new file mode 100644 index 0000000..2a7627f --- /dev/null +++ b/nmreval/models/spectrum.py @@ -0,0 +1,92 @@ +from typing import Union + +import numpy as np + +__all__ = ['Gaussian', 'Lorentzian', 'PseudoVoigt'] + +ArrayLike = Union[float, np.ndarray] + + +class Gaussian: + name = 'Gaussian' + equation = 'A*((4ln2/\pi)^{0.5}*(1/w) exp(-4ln2 [(x-\mu)/w]^{2})+A_{0}' + params = ['A', r'\mu', 'w', 'A_{0}'] + type = 'Spectrum' + bounds = [(0, None), (None, None), (0, None), (None, None)] + + @staticmethod + def func(x: ArrayLike, a: float, mu: float, sigma: float, off: float) -> ArrayLike: + r""" + + Calculates Gaussian lineshape + + .. math:: + f(x) = \frac{A}{\sqrt{2\pi} \sigma} \exp\left[-\frac{(x-\mu)^2}{2\sigma^2} \right] + A_0 + + Args: + x (float): Frequncy axis + a (float): Amplitude + mu (float): Maximum position + sigma (float): FWHM + off (float): Baseline + + """ + return a / (np.sqrt(4*np.log(2)/np.pi) * sigma) * np.exp(-4*np.log(2) * ((x-mu) / sigma)**2) + off + + +class Lorentzian: + type = 'Spectrum' + name = 'Lorentzian' + equation = 'A (2/\pi)w/[4*(x-\mu)^{2} + w^{2}] + A_{0}' + params = ['A', '\mu', 'w', 'A_{0}'] + ext_params = None + bounds = [(0, None), (None, None), (0, None), (None, None)] + + @staticmethod + def func(x: ArrayLike, a: float, mu: float, sigma: float, off: float) -> ArrayLike: + r""" + Calculate Lorentzian lineshape + + .. math:: + f(x) = \frac{A\sigma}{(x-\mu)^2 + \sigma^2} + A_0 + + Args: + x (float): Frequency axis + a (float): Amplitude + mu (float): Maximum position + sigma (float): FWHM + off (float): baseline + + """ + return (a/np.pi) * 2*sigma / (4*(x-mu)**2 + sigma**2) + off + + +class PseudoVoigt: + type = 'Spectrum' + name = 'Pseudo Voigt' + equation = 'A [R*2/\pi*w/[4*(x-\mu)^{2} + w^{2}] + ' \ + '(1-R)*sqrt(4*ln(2)/pi)/w*exp(-4*ln(2)[(x-\mu)/w]^{2})] + A_{0}' + params = ['A', 'R', '\mu', 'w', 'A_{0}'] + ext_params = None + bounds = [(0, None), (0, 1), (None, None), (0, None)] + + @staticmethod + def func(x: ArrayLike, a: float, r_lor: float, mu: float, sigma: float, off: float) -> ArrayLike: + r""" + Calculate Pseudo-Voigt lineshape, superposition of Lorentzian and Gaussian with shared center and FWHM. + + .. math:: + f(x) = A \left[ R \frac{2}{\pi} \frac{\sigma}{4(x-\mu)^2 + \sigma^2} + + (1-R)\sqrt{\frac{4\ln2}{\pi}} \frac{1}{\sigma} \exp\left(-4\ln2 \frac{(x-\mu)^2}{\sigma^2} \right) \right] + A_0 + + Args: + x (array-like): Frequency + a (float): Amplitude + r_lor (float): Relative Lorentzian contribution + mu (float): Maximum position + sigma (float): FWHM + off (float): Baseline + + """ + return a * (r_lor * (2/np.pi) * (sigma/(4*(x-mu)**2 + sigma**2)) + + (1-r_lor) * np.sqrt(4*np.log(2)/np.pi)/sigma * np.exp(-4*np.log(2)*((x-mu)/sigma)**2)) + off diff --git a/nmreval/models/wideline.py b/nmreval/models/wideline.py index ccdf5a2..ac5176b 100644 --- a/nmreval/models/wideline.py +++ b/nmreval/models/wideline.py @@ -9,7 +9,7 @@ from ..math.orientations import zcw_spherical as crystallites class Pake: - type = 'Wideline' + type = 'Spectrum' name = 'Pake' equation = '' params = ['A', r'\delta', r'\eta', r'\Sigma_{B}', r't_{pulse}'] @@ -44,7 +44,7 @@ class Pake: omega_1 = pi/2/t_pulse attn = omega_1 * np.sin(t_pulse*np.sqrt(omega_1**2+0.5*(2*pi*x)**2)) / \ - np.sqrt(omega_1**2+(np.pi*x)**2) + np.sqrt(omega_1**2+(np.pi*x)**2) ret_val *= attn @@ -52,7 +52,7 @@ class Pake: class CSA: - type = 'Wideline' + type = 'Spectrum' name = 'CSA' equation = '' params = ['A', r'\delta', r'\eta', r'\omega_{iso}', r'\Sigma_{B}'] @@ -85,8 +85,8 @@ class CSA: class SecCentralLine: - type = 'Wideline' - name = 'Central Transition 2nd order' + type = 'Spectrum' + name = 'Central Transition (2nd order)' equation = '' params = ['A', 'C_{Q}', r'\eta', r'\omega_{iso}', 'GB', r'\omega_{L}'] bounds = [(0, None), (0, None), (0, 1), (None, None), (0, None), (0, None)] @@ -129,4 +129,4 @@ class SecCentralLine: else: ret_val = s - return c * ret_val / trapz(ret_val, x) + return c * ret_val / simpson(ret_val, x) diff --git a/resources/_ui/basewindow.ui b/resources/_ui/basewindow.ui index 7d72d0e..23dfb11 100644 --- a/resources/_ui/basewindow.ui +++ b/resources/_ui/basewindow.ui @@ -308,6 +308,7 @@ + @@ -970,7 +971,7 @@ - Worms + Worm @@ -993,6 +994,11 @@ Look for updates + + + Mine + +