diff --git a/CHANGELOG.md b/CHANGELOG.md index a715ac29..5d864643 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,24 @@ Changelog All notable changes to this project will be documented in this file. -## [Unreleased] +## [1.2] - 2019-12-06 +### Added +- New plot type: 2D Colour Plot +- Added experimental support for Bruker ParaVision imaging data +- File browser tree: added 'open file external' and 'open browser here' option +- Reference Manual: added 'Roll' and Diffusion fitting +- Reference Manual: more on fitting 'AddData' and 'ConnectPar' +- Added many docstring to the source code + +### Changed +- Minimum required Matplotlib version is now 1.5.0 + +### Fixed +- Temperature Calibration Tool: crash on start +- Baseline correction in macro did not work +- Fixed cos2 apodization (was not squared) + +## [1.1] - 2019-05-03 ### Added - Tooltips have been added - A fixed startup directory for ssNake can be set from preferences diff --git a/DocSrc/Images/ConnectPars.png b/DocSrc/Images/ConnectPars.png new file mode 100644 index 00000000..94aa350f Binary files /dev/null and b/DocSrc/Images/ConnectPars.png differ diff --git a/DocSrc/ReferenceManual.pdf b/DocSrc/ReferenceManual.pdf deleted file mode 100644 index 470107dd..00000000 Binary files a/DocSrc/ReferenceManual.pdf and /dev/null differ diff --git a/DocSrc/ReferenceManual.tex b/DocSrc/ReferenceManual.tex index bf94d2a5..cb2a486b 100755 --- a/DocSrc/ReferenceManual.tex +++ b/DocSrc/ReferenceManual.tex @@ -120,7 +120,7 @@ \subsection{Python and library versions} For the library version, the following are needed: \begin{itemize} \item \texttt{numpy} $>=$ 1.11.0 - \item \texttt{matplotlib} $>=$ 1.4.2 + \item \texttt{matplotlib} $>=$ 1.5.0 \item \texttt{scipy} $>=$ 0.14.1 \end{itemize} ssNake also needs the \texttt{PyQt} (version 4 or 5) and \texttt{h5py} libraries. @@ -306,18 +306,19 @@ \subsection{Formats} Varian/Agilent & VnmrJ 2 and newer. \texttt{fid} and \texttt{data} (spectrum)\\ Bruker & Topspin and XWinNMR (\texttt{fid} \& \texttt{ser},\texttt{1r/i} \& \texttt{2rr/ii}) \\ \rowcolor{gray!30!white} +Bruker ParaVision & processed data (\texttt{2dseq})\\ Bruker minispec & \texttt{.sig} file\\ -JEOL Delta & 1D and 2D only at the moment \\ \rowcolor{gray!30!white} +JEOL Delta & 1D and 2D only at the moment \\ Chemagnetics & -- \\ -Magritek & Both 1D and 2D data \\ \rowcolor{gray!30!white} +Magritek & Both 1D and 2D data \\ SIMPSON & 1D and 2D, -ascii and -binary \\ -JCAMP & 1D JCAMP-DX file\\ \rowcolor{gray!30!white} +JCAMP & 1D JCAMP-DX file\\ Siemens ima & single voxel only\\ -JSON & ssNake JSON file (ascii)\\ \rowcolor{gray!30!white} +JSON & ssNake JSON file (ascii)\\ Matlab & ssNake \texttt{.mat} file (binary)\\ \bottomrule \end{tabular} @@ -335,6 +336,9 @@ \subsubsection*{Bruker} Bruker processed spectra can be loaded from \textit{1r} and \textit{1i} files for 1D data, \textit{2rr} and \textit{2ii} for 2D data, and \textit{2rr} and \textit{2ir} for hypercomplex data. It additionally takes parameters from \textit{procs} or \textit{proc2s} files in that directory. Moreover, the spectral frequency is extracted from the \textit{acqus} and/or \textit{acqu2s} files located \textit{two directories up}. +\subsubsection*{Bruker ParaVision} +Loads processed data acquired via Bruker ParaVision. This feature is experimental at the moment. It need several files: \texttt{2dseq}, \texttt{d3proc} and \texttt{procs}. + \subsubsection*{Bruker minispec} In some cases, the Bruker minispec also outputs a \texttt{.sig} data file. These can be loaded in ssNake, although this is experimental at the moment. @@ -386,18 +390,19 @@ \subsection{Open} & with \texttt{procs}/\texttt{proc2s},&\\ &\texttt{../../acqus}, \texttt{../../acqu2s} & \\ \rowcolor{gray!30!white} +Bruker ParaVision & \texttt{2dseq}, \texttt{procs} and \texttt{d3proc} &\\ Bruker minispec & & \texttt{.sig} \\ -Chemagnetics & \texttt{acq} (and \texttt{acq\_2} for 2D) + \texttt{data} &\\ \rowcolor{gray!30!white} +Chemagnetics & \texttt{acq} (and \texttt{acq\_2} for 2D) + \texttt{data} &\\ Magritek & \texttt{acqu.par}, \texttt{*.1d} or \texttt{*.2d}&\\ -JEOL Delta & & \texttt{.jdf}\\ \rowcolor{gray!30!white} +JEOL Delta & & \texttt{.jdf}\\ NMRpipe & & \texttt{.fid} and first byte is 0\\ -SIMPSON & & \texttt{.fid} or \texttt{.spe} \\ \rowcolor{gray!30!white} +SIMPSON & & \texttt{.fid} or \texttt{.spe} \\ JCAMP & & \texttt{.jcamp} or \texttt{.dx} or \texttt{.jdx}\\ -JSON & & \texttt{.json} or \texttt{.JSON}\\ \rowcolor{gray!30!white} +JSON & & \texttt{.json} or \texttt{.JSON}\\ Matlab & & \texttt{.mat} or \texttt{.MAT}\\ \bottomrule \end{tabular} @@ -963,6 +968,20 @@ \subsection{Shift data} The shifting is always applied in the time domain. In the frequency domain Fourier transforms are used to transform to and from the time domain to apply the shifting. + +\subsection{Roll data} +Rolls the data to the left or to the right, by removing data points from one side, and inserting +them on the other side of the data. Negative values roll to the left, positive values to the +right. Non-integer values can also be used. In this case, Fourier-based tricks are used to accomplish +this. + +Starting the roll data tool creates an input window. In this window, a number must be +filled in that equals the number of data points of the desired \textit{right} roll. Left rolls +can be made by using negative values. Alternatively, the arrows on the right and left side of the +input box can be used to roll the data in the direction of the arrow. The effect of the operation +on the current 1D can be seen in the main window. When pushing `Apply' the roll is performed on +all traces of the nD data. + \subsection{Integrate} Integrates a specific part of the active dimension on all traces. @@ -1371,6 +1390,68 @@ \subsection*{Spherical tensors} + +\subsection{Connecting parameters} +In all iterative fitting routines supported by ssNake, it is possible to connect parameters, thereby using +extra information that the user has about the system, and reducing the number of free parameters in +the fit. This can for example be used to link integral values, which should be the same for multiple +sites (i.e. in a solid material with a 1:1 ratio between different sites). + +Linking parameters can be done by right-clicking on an input box in an iterative fitting +routine, and selecting the option `Connect Parameter'. This gives the following pop-up window: +\begin{center} +\includegraphics[width=0.3\linewidth]{Images/ConnectPars.png} +\end{center} + +Here several values need to put in: +\begin{itemize} + \item Parameter: the name of the parameter that should be linked to + \item Data: the name of the fitting data tab that should be linked to + \item Line: the number of the row that should be linked to (first row is `0') + \item Multiplier: multiply the linked value by this number + \item Offset: offset the linked value by this number +\end{itemize} +When doing this, the original parameter is replaced with the number `$\text{Multiplier} \cdot x + +\text{Offset}$' during the fit (with $x$ the value of the parameter that is linked to). + +When pushing `Ok' in the pop-up window, the original value in the input box is replaced by a value +like `('Integral', 0, 1.0, 0.0, 0)'. The input represents: ('Par name', Line, Multiplier, Offset, +Data index) The pop-up menu is essentially an easy tool to create this input. + +Note that chain-linking parameters will lead to errors. That is, do not link \texttt{par2} to +\texttt{par1}, and \texttt{par3} +to \texttt{par2}. Always refer to the original parameter (link \texttt{par2} to \texttt{par1}, and +\texttt{par3} to \texttt{par1}). + +A tutorial on fitting using connected parameters can be found in the `MultiFitQuadrupole' advanced +tutorial. + + +\subsection{Simultaneous fitting of multiple data sources} +Apart from connecting parameters during a fit, as described in the previous section, ssNake also +allows for the simultaneous fit of multiple data sources (i.e. spectra or time series). When fitting +in such a case, ssNake tries to minimize the difference between all the experimental and simulated +data. Fitting multiple data simultaneously only makes sense if there are connected parameters between +the fitting parameters of the different data sets (otherwise the results are identical to the case +where each data set is fit individually). + +When in a fitting window, extra data can be added using the `+Add data+' tab that can be found in +the vertical tabs on the left hand side of the window. Clicking this prompts a pop-up window, where +the workspace name and the desired fitting routine can must be supplied. The new data tab will be +added to the vertical tab bar. + +The new data will have its own parameters. Note that in the `extra' fitting tabs, several buttons are +missing. These are universal buttons, which are only displayed in the first (i.e. master) fitting +tab. Pushing `Fit' fits all the data sets simultaneously. + +Be very careful with the vertical scaling of the spectra. If spectrum1 has 10000x more intensity +than spectrum2, spectrum2 will be almost ignored during the fit. To avoid this, scale the spectra to +have the same integral (scaling can be done via Matrix $\longrightarrow$ Normalize), or to have the +same noise intensity. Tread carefully\ldots + +A tutorial on simultaneous fitting of multiple data sets can be found in the `MultiFitQuadrupole' advanced +tutorial. + \subsection{S/N} S/N (or SNR) calculates the signal-to-noise ratio between a defined section of noise, and a peak. SNR is a useful way to describe the quality of a spectrum, with too low values being a sign for inaccurate data. In ssNake, opening the S/N tool creates a window, in which the limits of the section of noise has to be set, as well as the region with the peak of interest. Left clicking in the plot can set these values (noise limits first, then signal limits). Note that it is essential that the region that is specified as `noise' really is noise. Any remaining signal will disrupt the SNR calculation. When the limits are set, ssNake directly calculates the SNR, using: \begin{equation} @@ -1410,10 +1491,20 @@ \subsection{Relaxation Curve} view. When switching to the log axis, sometimes a recalculation of the curve is necessary to get a nice plot. + \subsection{Diffusion Curve} +This routine can be used to fit diffusion curves. The equation +used for this is: \begin{equation} y = \text{amp} \cdot (\text{const} + \text{coeff} \cdot \exp(-(\gamma \delta x)^2 D (\Delta -\delta / 3.0))) \end{equation} +Here, $\gamma$ represent the $\gamma$-value of the isotope that is measured, $\delta$ is the length +of the gradient pulse, $\Delta$ is the time between the gradient pulses (i.e. the time from the +start of gradient pulse 1, to the start of gradient pulse 2). $D$ is the diffusion constant, which +is what we will fit. The `const' and `coeff' parameters have the same meaning as in the relaxation +curve fit. Note that the $x$-axis should be in terms of the gradient strength (in Tesla). ssNake +will still show `seconds' as a unit though (1 s = 1 T). + @@ -1429,7 +1520,7 @@ \subsection{CSA} \item The MAS method: static, finite or infinite \item The spinning speed in kHz (when MAS is on `finite'). \item The number of sidebands (when MAS is on `finite'). This value must always be larger then the - expected number of spinning sidebands. Higher values take more time to simulate. + expected number of spinning sidebands. When too few sidebands are simulated, the sideband intensities will be off. Larger values take more time to simulate. For efficiency powers of two should be used. \item The rotor angle in radian (when MAS is not static). Default is the magic angle: $\text{arctan}(\sqrt{2})$ \end{itemize} @@ -1492,12 +1583,12 @@ \subsection{Quadrupole} \begin{itemize} \item The spin quantum number $I$ \item The MAS type: static, finite MAS or infinite MAS -\item The satellites (on or off, not that for integer $I$ values, the must be on) +\item The satellites (on or off, note that for integer $I$ values, they must be on) \item The Cheng number (number of powder averages) \item The rotor angle in radian (when MAS is not `static'). Default is the magic angle: $\text{arctan}(\sqrt{2})$ \item The number of sidebands (when MAS is on `finite'). This value must always be larger then the - expected number of spinning sidebands. Higher values take more time to simulate. + expected number of spinning sidebands. When too few sidebands are simulated, the sideband intensities will be off. Larger values take more time to simulate. For efficiency powers of two should be used. \item The spinning speed in kHz (when MAS is on `finite'). \item The background (constant value added to all data points) \end{itemize} @@ -1512,7 +1603,7 @@ \subsection{Quadrupole} \end{itemize} A maximum of 10 sites can be fitted simultaneously. -When simulating an infinite MAS spectrum, a more efficient routine can be used that for finite MAS. +When simulating an infinite MAS spectrum, a more efficient routine can be used than for finite MAS. If spinning sidebands are not important in the calculation, it might be useful to switch to `infinite' for speed considerations. @@ -1713,7 +1804,7 @@ \subsection{External} SIMPSON file. -If, for example, we want to fit a spectrum were we want to change the chemical shift anisotropy +If, for example, we want to fit the chemical shift anisotropy ($\delta_\text{aniso}$) and asymmetry ($\eta$), ($\delta_\text{aniso}$) and asymmetry ($\eta$), we could use the following SIMPSON spinsys: \lstset{language=tcl} % Set your language (you can change the language for each code-block optionally) @@ -1929,6 +2020,12 @@ \subsubsection*{Diagonal} Therefore, if a data set with two identical axes is plot, only if the two units are the same, the diagonal is plot in a way that (probably) is the correct way. Alternatively, there is a box called `Multiplier', in which a value can be put that multiplies the x-axis (only for the calculation of the diagonal). +\subsection{2D Colour Plot} +A 2D Colour plot shows the height profile of the data using a colour scheme. +This plot type essentially show the same information as a Contour plot, but might +be more applicable for certain types of data. It is used often for viewing NMR imaging (MRI) data. + +As with the Contour plot, the 2D Colour plot shows projections, of which the settings can be changed in the side frame (see the Section on Contour Plot for more information). \subsection{Multi plot} A multi plot is on overlay of multiple workspaces. It shows the 1D Plot of the active workspace, and a button diff --git a/DocSrc/Title.tex b/DocSrc/Title.tex index 20602c3a..170cf162 100755 --- a/DocSrc/Title.tex +++ b/DocSrc/Title.tex @@ -27,7 +27,7 @@ \large Wouter Franssen \& Bas van Meerten \vspace{1cm} -\large Version 1.1 +\large Version 1.2 \vfill \includegraphics[width=0.5\textwidth]{Images/logo.pdf}\ diff --git a/README.md b/README.md index d3b63953..7fd1ee5e 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ ssNake requires: And the following python packages are required[1]: - [numpy](http://sourceforge.net/projects/numpy/files/NumPy/) >= 1.11.0 -- [matplotlib](http://matplotlib.org/) >= 1.4.2 +- [matplotlib](http://matplotlib.org/) >= 1.5.0 - [scipy](http://sourceforge.net/projects/scipy/files/scipy/) >= 0.14.1 - [PyQt4](http://www.riverbankcomputing.com/software/pyqt/download) >= 4.11.4 - [h5py](http://www.h5py.org/) >= 2.5.0 (for loading Matlab data) @@ -56,7 +56,7 @@ Contributing ------------ 1. Fork it -2. Create your feature branch +2. Create your feature branch (preferably based on the develop branch) 3. Commit your changes 4. Push to the branch 5. Submit a pull request diff --git a/ReferenceManual.pdf b/ReferenceManual.pdf index 470107dd..0eaca37a 100644 Binary files a/ReferenceManual.pdf and b/ReferenceManual.pdf differ diff --git a/Tutorial/Tutorial.pdf b/Tutorial/Tutorial.pdf index 51929ec5..e220810b 100644 Binary files a/Tutorial/Tutorial.pdf and b/Tutorial/Tutorial.pdf differ diff --git a/Tutorial/src/Title.tex b/Tutorial/src/Title.tex index e4111a54..40546297 100644 --- a/Tutorial/src/Title.tex +++ b/Tutorial/src/Title.tex @@ -27,7 +27,7 @@ \large Wouter Franssen \& Bas van Meerten \vspace{1cm} -\large Version 1.1 +\large Version 1.2 \vfill \includegraphics[width=0.7\textwidth]{Images/logo.pdf}\ diff --git a/src/Czjzek.py b/src/Czjzek.py index db5facac..c8484f72 100644 --- a/src/Czjzek.py +++ b/src/Czjzek.py @@ -18,20 +18,41 @@ # You should have received a copy of the GNU General Public License # along with ssNake. If not, see . +import math +import multiprocessing import numpy as np import scipy.integrate as SI import scipy.special as SP -import math -import multiprocessing try: #If numba exists, use jit, otherwise make a mock decorator from numba import jit -except Exception: +except ImportError: def jit(func): return func @jit def gammaFunc(gamma, a11pa15, a51pre, a55part): + """ + Calculates the gamma angle part of an extended Czjzek distribution. This + Is the last integral of the calculation, so all terms are bundled here. + + Parameters + ---------- + gamma: float + The gamma angle in radians + a11pa15: float + The a11 term with part of the a15 mixed in + a51pre: float + Part of the a51 term + a55part: float + Part of the a55 term + + Returns + ------- + float + Exponent of all relevant terms and the gamma angle. + """ + cos2G = math.cos(gamma) sin2G = math.sin(gamma) a51 = a51pre * cos2G @@ -39,6 +60,31 @@ def gammaFunc(gamma, a11pa15, a51pre, a55part): return math.exp(a11pa15 + a51 + a55) def alphaFunc(alpha, preCalc, a11, a15pre, a55pre, a51prepre1, a51prepre2): + """ + Calculates the alpha angle part of an extended Czjzek distribution. + + Parameters + ---------- + alpha: float + The alpha angle in radians + preCalc: float + Some pre-calculated constants + a11: float + The a11 term + a15pre: + Precalc of the a15 term + a55pre: float + Precalc of the a55 term + a51prepre1: float + Precalc of a part of the a51 term + a51prepre2: float + Precalc of a part of the a51 term + + Returns + ------- + float + Integral over the gamma angle + """ cos2A = math.cos(alpha) sin2A = math.sin(alpha) a11pa15 = a11 - a15pre * cos2A @@ -46,24 +92,57 @@ def alphaFunc(alpha, preCalc, a11, a15pre, a55pre, a51prepre1, a51prepre2): a51pre = a51prepre1 + a51prepre2 * cos2A Amp = SI.quad(gammaFunc, 0, 2 * np.pi, args=(a11pa15, a51pre, a55part), epsrel=0.1, epsabs=0) #Scale with 0.5, as integral is done to 2pi instead of pi (and sin(gamma) is used not sin(2 * gamma) - return Amp[0] * 0.5 + return Amp[0] * 0.5 def betaFunc(beta, eta, eta0, preCalc): + """ + Calculates the alpha angle part of an extended Czjzek distribution. + + Parameters + ---------- + beta: float + The beta angle in radians + eta: float + The eta value for which the Czjzek intensity needs to be found + eta0: float + The base eta value of the extended Czjzek distribution + preCalc: float + Some pre-calculated constants + + Returns + ------- + float + Integral over the alpha and gamma angle + """ cosB = math.cos(beta) cosBS = cosB**2 sinB = math.sin(beta) sinBS = sinB**2 preVal = preCalc[0] * sinB - a11 = - 0.5 * (3 * cosBS - 1) * preCalc[1] * preCalc[2] + preCalc[3] + a11 = - 0.5 * (3 * cosBS - 1) * preCalc[1] * preCalc[2] + preCalc[3] a15pre = eta0 * preCalc[1] / 2 * sinBS * preCalc[2] a55pre = -cosB * preCalc[4] * preCalc[2] a51prepre1 = preCalc[1] / 2 * sinBS * eta * preCalc[2] a51prepre2 = 0.5 * (1 + cosBS) * preCalc[4] * preCalc[2] - Amp = SI.quad(alphaFunc, 0, np.pi, args=(preCalc,a11,a15pre,a55pre,a51prepre1,a51prepre2), epsrel=0.1, epsabs=0) + Amp = SI.quad(alphaFunc, 0, np.pi, args=(preCalc, a11, a15pre, a55pre, a51prepre1, a51prepre2), epsrel=0.1, epsabs=0) #Scale with 0.5, as integral is done to pi instead of pi/2 (and sin(alpha) is used not sin(2 * alpha) return Amp[0] * preVal * 0.5 def extendedCzjzek(inp): + """ + Calculates the intensity of a extended Czjzek distribution for a particular Cq + and eta value. + + Parameters + ---------- + inp: list[floats] + List with the inputs [cq, eta, cq0, eta0, sigma, d] + + Returns + ------- + float + Extended Czjzek intensity for the Cq and eta value of the input + """ cq, eta, cq0, eta0, sigma, d = inp #Prevent overflow error (if this condition is true, the point is far outside the distribution, and is 0 anyway) if abs(cq**2*(1+eta**2/3) - cq0**2)/(2 * sigma**2) > 1000: @@ -73,41 +152,119 @@ def extendedCzjzek(inp): return 0.0 N1 = cq ** (d - 1) / (sigma ** d) * eta * (1 - eta**2 / 9) afterfact = -1.0 / (2 * sigma ** 2) - preCalc =[N1,np.sqrt(3), 2.0/np.sqrt(3) * cq * cq0 * afterfact, (cq0**2 * (1 + eta0**2 / 3) + cq**2 * (1 + eta**2/3)) * afterfact] + preCalc = [N1,np.sqrt(3), 2.0/np.sqrt(3) * cq * cq0 * afterfact, (cq0**2 * (1 + eta0**2 / 3) + cq**2 * (1 + eta**2/3)) * afterfact] eta0deveta = eta0 / preCalc[1] * eta # eta0 divided by sqrt3 multiply by eta preCalc.append(eta0deveta) - Amp = SI.quad(betaFunc, 0, np.pi, args=(eta,eta0,preCalc), epsrel=0.1, epsabs=0) + Amp = SI.quad(betaFunc, 0, np.pi, args=(eta, eta0, preCalc), epsrel=0.1, epsabs=0) return Amp[0] @jit -def tFunc(t, pre, pre2, fact, eta): +def tFunc(t, pre2, fact, eta): + """ + Function used for integrating over 't' in in extended Czjzek distribution + with eta0 == 0. + + Parameters + ---------- + t: float + Value of t + pre: float + A pre-calculated constant (see the function 'extendedCzjzekNoEta0') + pre2: float + A pre-calculated constant (see the function 'extendedCzjzekNoEta0') + fact: float + A pre-calculated factor (see the function 'extendedCzjzekNoEta0') + eta: float + The eta value for which the Czjzek intensity is to be found + + Returns + ------- + float + Term that should be integrated over + """ expo = math.exp(fact * (3*t**2-1) + pre2) z = eta * abs(fact) * (1-t**2) - bessel = SP.iv(0,z) + bessel = SP.iv(0, z) return expo * bessel def extendedCzjzekNoEta0(inp): + """ + Function used to calculate the extended Czjzek distribution intensity for a + particular value of Cq and eta. The function is optimized for the case + eta0 == 0. + + Parameters + ---------- + inp: list of floats + Value of [cq, eta, cq0, sigma, d] + + Returns + ------- + float + Extended Czjzek intensity for the Cq and eta value of the input + """ cq, eta, cq0, sigma, d = inp #Prevent overflow error (if this condition is true, the point is far outside the distribution, and is 0 anyway) - if abs(cq**2*(1+eta**2/3) - cq0**2)/(2 * sigma**2) > 1000: + if abs(cq**2*(1+eta**2/3) - cq0**2)/(2 * sigma**2) > 1000: return 0.0 #Prevent overflow error on eta range if cq0 / sigma * eta > 10: return 0.0 - pre = cq**(d-1) / sigma**d * eta * (1 - eta**2 / 9.0) + pre = cq**(d-1) / sigma**d * eta * (1 - eta**2 / 9.0) pre2 = -(cq0**2 + cq**2 * (1 + eta**2 / 3.0)) / (2 * sigma**2) fact = cq * cq0 / (2*sigma**2) - Amp = SI.quad(tFunc,0, 1, args=(pre,pre2,fact,eta), epsrel=0.0001, epsabs=0)[0] + Amp = SI.quad(tFunc, 0, 1, args=(pre2, fact, eta), epsrel=0.0001, epsabs=0)[0] return Amp * pre def normalCzjzekFunc(cq, eta, sigma, d): + """ + Function used to calculate the normal Czjzek distribution intensity for a + Cq and eta value. + + Parameters + ---------- + cq: float + Cq value of the quadrupole interaction + eta: float + eta value of the quadrupole interaction + sigma: float + Sigma value (i.e. width) of the distribution + d: float + D value of the distribution + + Returns + ------- + float + Normal Czjzek intensity for the Cq and eta value of the input + """ return cq**(d - 1) * eta / (np.sqrt(2 * np.pi) * sigma**d) * (1 - eta**2 / 9.0) * np.exp(-cq**2 / (2.0 * sigma**2) * (1 + eta**2 / 3.0)) def czjzekIntensities(sigma, d, cq, eta, cq0=0, eta0=0): - #Calculates an intensity distribution for a Czjzek library - #Depending on et0 and cq0, either a regular Czjzek, or an extended Czjzek is used - #cq: C_q grid (2D flattened) - #eta: eta grid (2D flattened) + """ + Function used to calculate the (extended) Czjzek distribution intensity for a + Cq and eta grid. Based on the optional input of cq0 and eta0 values, it either + calculates a normal Czjzek, or an extended Czjzek. + + Parameters + ---------- + sigma: float + Sigma value (i.e. width) of the distribution + d: float + D value of the distribution + cq: ndarray + A 1-D array with cq values + eta: ndarray + A 1-D array with eta values + cq0: float, optional + Base cq value of the distribution + eta0: float, optional + Base eta value of the distribution + + Returns + ------- + ndarray + 1-D array of the normalized Czjzek intensity distribution + """ if cq0 == 0.0 and eta0 == 0.0: if sigma == 0.0: # protect against divide by zero czjzek = np.zeros_like(cq) @@ -117,9 +274,9 @@ def czjzekIntensities(sigma, d, cq, eta, cq0=0, eta0=0): pool = multiprocessing.Pool(multiprocessing.cpu_count()) if eta0 != 0.0: eta0 = 1 - abs(abs(eta0)%2 - 1) #scale continuously between 0--1 - fit = pool.map_async(extendedCzjzek, [(cq[i],eta[i],cq0,eta0,sigma,d) for i in range(len(cq))]) + fit = pool.map_async(extendedCzjzek, [(cq[i], eta[i], cq0, eta0, sigma, d) for i in range(len(cq))]) else: - fit = pool.map_async(extendedCzjzekNoEta0, [(cq[i],eta[i],cq0,sigma,d) for i in range(len(cq))]) + fit = pool.map_async(extendedCzjzekNoEta0, [(cq[i], eta[i], cq0, sigma, d) for i in range(len(cq))]) pool.close() pool.join() czjzek = np.array(fit.get()) diff --git a/src/Icons/2DColour.png b/src/Icons/2DColour.png new file mode 100644 index 00000000..f91294b4 Binary files /dev/null and b/src/Icons/2DColour.png differ diff --git a/src/Icons/Sources/2DColour.svg b/src/Icons/Sources/2DColour.svg new file mode 100644 index 00000000..d7d0bb5f --- /dev/null +++ b/src/Icons/Sources/2DColour.svg @@ -0,0 +1,278 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/src/Icons/Sources/quadcsa.svg b/src/Icons/Sources/quadcsa.svg index d80b003e..bd475c8f 100644 --- a/src/Icons/Sources/quadcsa.svg +++ b/src/Icons/Sources/quadcsa.svg @@ -15,7 +15,7 @@ id="svg2" inkscape:version="0.92.3 (2405546, 2018-03-11)" sodipodi:docname="quadcsa.svg" - inkscape:export-filename="/media/HDD/ssnake/ssnake/src/Icons/Sources/quadconversion.png" + inkscape:export-filename="/home/wouter/Documents/ssNake/ssnake/src/Icons/quadcsa.png" inkscape:export-xdpi="162" inkscape:export-ydpi="162"> . -import numpy as np -import matplotlib as mpl -from matplotlib.figure import Figure -import scipy.optimize import multiprocessing import re import time import datetime import os import copy +import numpy as np +import matplotlib as mpl +from matplotlib.figure import Figure +import scipy.optimize from safeEval import safeEval from views import Current1D, CurrentContour import widgetClasses as wc @@ -36,7 +36,7 @@ import specIO as io import spectrum as sc from ssNake import SideFrame, VERSION, QtGui, QtCore, QtWidgets, FigureCanvas -import Czjzek as Czjzek +import Czjzek stopDict = {} # Global dictionary with stopping commands for fits @@ -64,15 +64,34 @@ class FittingException(sc.SpectrumException): class TabFittingWindow(QtWidgets.QWidget): + """ + The base widget of the fitting window. + This handles the different tabs for multifitting. + """ PRECIS = 4 MINMETHOD = 'Powell' NUMFEVAL = 150 def __init__(self, father, oldMainWindow, mainFitType): + """ + Initializes the tab fitting window. + + Parameters + ---------- + father : MainProgram + The main program of ssnake. + oldMainWindow : Main1DWindow + The window that this fitting window replaces. + mainFitType : str + The name of the base fit type to be performed. + Should be a key in FITTYPEDICT. + """ super(TabFittingWindow, self).__init__(father) self.father = father self.oldMainWindow = oldMainWindow + self.get_masterData = oldMainWindow.get_masterData # Connect function + self.get_current = oldMainWindow.get_current # Connect function self.mainFitType = mainFitType self.subFitWindows = [] self.process1 = None @@ -85,8 +104,8 @@ def __init__(self, father, oldMainWindow, mainFitType): self.tabs.tabCloseRequested.connect(self.closeTab) self.tabs.addTab(self.mainFitWindow, self.current.data.name) self.tabs.addTab(QtWidgets.QWidget(), '+Add data+') - self.tabs.tabBar().setTabButton(0, QtWidgets.QTabBar.RightSide, QtWidgets.QLabel('')); - self.tabs.tabBar().setTabButton(1, QtWidgets.QTabBar.RightSide, QtWidgets.QLabel('')); + self.tabs.tabBar().setTabButton(0, QtWidgets.QTabBar.RightSide, QtWidgets.QLabel('')) + self.tabs.tabBar().setTabButton(1, QtWidgets.QTabBar.RightSide, QtWidgets.QLabel('')) self.tabs.currentChanged.connect(self.changeTab) self.oldTabIndex = 0 grid3 = QtWidgets.QGridLayout(self) @@ -95,18 +114,30 @@ def __init__(self, father, oldMainWindow, mainFitType): grid3.setRowStretch(0, 1) def getTabNames(self): + """ + Returns a list of the tab names. + """ return [self.tabs.tabText(i) for i in range(self.tabs.count()-1)] def getCurrentTabName(self): + """ + Returns the name of the tab currently open. + """ return self.tabs.tabText(self.tabs.currentIndex()) def getParamTextList(self): + """ + Returns a list of all unique parameter names of all tabs. + """ parametertxtlist = self.mainFitWindow.paramframe.SINGLENAMES + self.mainFitWindow.paramframe.MULTINAMES for subfit in self.subFitWindows: parametertxtlist += subfit.paramframe.SINGLENAMES+subfit.paramframe.MULTINAMES return list(set(parametertxtlist)) def addSpectrum(self): + """ + Asks the user for a workspace name and a fitting type and adds this as a new tab. + """ wsIndex, fitName, accept = NewTabDialog.getFitInput(self, self.father.workspaceNames, list(FITTYPEDICT.keys()), self.mainFitType) if not accept: return @@ -116,28 +147,72 @@ def addSpectrum(self): self.oldTabIndex = len(self.subFitWindows) def removeSpectrum(self, spec): + """ + Removes a spectrum from the tabs. + + Parameters + ---------- + spec : str + The name of the spectrum to remove. + """ num = self.subFitWindows.index(spec) - self.tabs.setCurrentIndex(num) + self.tabs.setCurrentIndex(num) self.oldTabIndex = num self.tabs.removeTab(num + 1) del self.subFitWindows[num] def changeTab(self, index): + """ + Changes the active fitting tab. + + Parameters + ---------- + index : int + The index of the tab to open. + """ if index == self.tabs.count() - 1: - self.tabs.setCurrentIndex(self.oldTabIndex) #Quickly set to old tab, to avoid showing the `add data' tab + self.tabs.setCurrentIndex(self.oldTabIndex) # Quickly set to old tab, to avoid showing the `add data' tab self.addSpectrum() else: self.oldTabIndex = index def closeTab(self, num): + """ + Closes a tab. + + Parameters + ---------- + num : int + The index of the tab to close. + """ count = self.tabs.count() if num > 0: if num != count - 1: - self.tabs.setCurrentIndex(num - 1) #Set one step lower to avoid 'addSpectrum' to be run + self.tabs.setCurrentIndex(num - 1) # Set one step lower to avoid 'addSpectrum' to be run self.tabs.removeTab(num) del self.subFitWindows[num - 1] def fitProcess(self, xax, data1D, guess, args, funcs): + """ + Creates a new process to fit the spectra. + + Parameters + ---------- + xax : list + The list with xaxArrays from the various spectra. + data1D : ndarray + The concatenated data from the various spectra. + guess : list + The initial guesses of the fit parameters. + args : tuple + The additional parameters of the fit. + funcs : list of functions + The fit function for each of the spectra. + Returns + ------- + OptimizeResult + The results of the fit. + """ self.queue = multiprocessing.Queue() self.process1 = multiprocessing.Process(target=mpFit, args=(xax, data1D, guess, args, self.queue, funcs, self.MINMETHOD, self.NUMFEVAL)) self.process1.start() @@ -154,11 +229,14 @@ def fitProcess(self, xax, data1D, guess, args, funcs): self.stopMP() if fitVal is None: raise FittingException('Optimal parameters not found') - elif isinstance(fitVal, str): + if isinstance(fitVal, str): raise FittingException(fitVal) return fitVal def stopMP(self, *args): + """ + Stops the running fitting process. + """ if self.queue is not None: self.process1.terminate() self.queue.close() @@ -170,11 +248,17 @@ def stopMP(self, *args): self.mainFitWindow.paramframe.stopButton.hide() def stopAll(self, *args): + """ + Stops all the fitting processes started by fitAll. + """ self.runningAll = False self.stopMP() self.mainFitWindow.paramframe.stopAllButton.hide() def fitAll(self, *args): + """ + sequentially opens slices from an ND spectrum and runs a fit. + """ self.runningAll = True self.mainFitWindow.paramframe.stopAllButton.show() tmp = np.array(self.mainFitWindow.current.data.shape()) @@ -191,28 +275,28 @@ def fitAll(self, *args): self.fit() self.mainFitWindow.sideframe.upd() self.mainFitWindow.paramframe.stopAllButton.hide() - + def fit(self): + """ + Fits a spectrum on the current slice. + """ value = self.mainFitWindow.paramframe.getFitParams() if value is None: return - else: - xax, data1D, guess, args, out = value + xax, data1D, guess, args, out = value xax = [xax] data1D = [data1D] - out = [out] selectList = [slice(0, len(guess))] funcs = [self.mainFitWindow.paramframe.FITFUNC] for i in range(len(self.subFitWindows)): xax_tmp, data1D_tmp, guess_tmp, args_tmp, out_tmp = self.subFitWindows[i].paramframe.getFitParams() - out.append(out_tmp) xax.append(xax_tmp) selectList.append(slice(len(guess), len(guess) + len(guess_tmp))) data1D.append(data1D_tmp) funcs.append(self.subFitWindows[i].paramframe.FITFUNC) guess += guess_tmp new_args = () - for n in range(len(args)): + for n, _ in enumerate(args): new_args += (args[n] + args_tmp[n],) args = new_args # tuples are immutable new_args = (selectList,) + args @@ -222,28 +306,48 @@ def fit(self): allFitVal = allFitVal['x'] fitVal = [] for length in selectList: - if allFitVal.ndim is 0: + if allFitVal.ndim == 0: fitVal.append(np.array([allFitVal])) else: fitVal.append(allFitVal[length]) args_out = [] - for n in range(len(args)): + for n, _ in enumerate(args): args_out.append([args[n][0]]) - self.mainFitWindow.paramframe.setResults(fitVal[0], args_out, out[0]) - for i in (range(len(self.subFitWindows))): + self.mainFitWindow.paramframe.setResults(fitVal[0], args_out) + for i, _ in enumerate(self.subFitWindows): args_out = [] - for n in range(len(args)): + for n, _ in enumerate(args): args_out.append([args[n][i + 1]]) - self.subFitWindows[i].paramframe.setResults(fitVal[i + 1], args_out, out[i + 1]) + self.subFitWindows[i].paramframe.setResults(fitVal[i + 1], args_out) def getNum(self, paramfitwindow): + """ + Returns the index of a parameter fit window. + + Parameters + ---------- + paramfitwindow : AbstractParamFrame + The parameter frame of which to determine the index. + + Returns + ------- + int + The index. + """ fitwindow = paramfitwindow.rootwindow if fitwindow is self.mainFitWindow: return 0 - else: - return self.subFitWindows.index(fitwindow) + 1 + return self.subFitWindows.index(fitwindow) + 1 def getParams(self): + """ + Collect the fitting parameters from all tabs. + + Returns + ------- + ndarray + The fitting parameters. + """ params = [self.mainFitWindow.paramframe.getSimParams()] for window in self.subFitWindows: tmp_params = window.paramframe.getSimParams() @@ -251,9 +355,18 @@ def getParams(self): if params[0] is None: return None return params - + def disp(self, *args, **kwargs): - #self.mainFitWindow.paramframe.simButton.hide() + """ + Simulate all spectra and display them. + + Parameters + ---------- + *args + All arguments are passed to the disp functions of the parameter frames. + **kwargs + All keyword arguments are passed to the disp functions of the parameter frames. + """ self.mainFitWindow.paramframe.simBusyButton.show() QtWidgets.qApp.processEvents() try: @@ -263,34 +376,41 @@ def disp(self, *args, **kwargs): self.mainFitWindow.paramframe.disp(params, 0, *args, **kwargs) for i in range(len(self.subFitWindows)): self.subFitWindows[i].paramframe.disp(params, i + 1, *args, **kwargs) - except: + except Exception: raise finally: self.mainFitWindow.paramframe.simBusyButton.hide() - #self.mainFitWindow.paramframe.simButton.show() - - def get_masterData(self): - return self.oldMainWindow.get_masterData() - - def get_current(self): - return self.oldMainWindow.get_current() def kill(self): - self.tabs.currentChanged.disconnect() #Prevent call for data on close + """ + Closes the fitting window. + """ + self.tabs.currentChanged.disconnect() # Prevent call for data on close self.mainFitWindow.kill() ############################################################################## class ResultsExportWindow(QtWidgets.QWidget): + """ + The window for exporting or importing parameters. + """ def __init__(self, parent): + """ + Initializes the import export window. + + Parameters + ---------- + parent : AbstractParamFrame + The parameter frame from which this window was called. + """ super(ResultsExportWindow, self).__init__(parent) self.setWindowFlags(QtCore.Qt.Window | QtCore.Qt.Tool) self.father = parent self.setWindowTitle("Export results") grid = QtWidgets.QGridLayout(self) - exportGroup = QtWidgets.QGroupBox('Export:') + exportGroup = QtWidgets.QGroupBox("Export:") exportGrid = QtWidgets.QGridLayout() self.parToWorkButton = QtWidgets.QPushButton("Parameters to Workspace") self.parToWorkButton.clicked.connect(self.parToWork) @@ -303,44 +423,72 @@ def __init__(self, parent): exportGrid.addWidget(self.curvesToWorkButton, 2, 0) exportGroup.setLayout(exportGrid) grid.addWidget(exportGroup, 0, 0) - importGroup = QtWidgets.QGroupBox('Import:') + importGroup = QtWidgets.QGroupBox("Import:") importGrid = QtWidgets.QGridLayout() self.fileToParButton = QtWidgets.QPushButton("File to parameters") self.fileToParButton.clicked.connect(self.fileToPar) - importGrid.addWidget(self.fileToParButton, 3, 0) + importGrid.addWidget(self.fileToParButton, 3, 0) importGroup.setLayout(importGrid) grid.addWidget(importGroup, 1, 0) cancelButton = QtWidgets.QPushButton("&Cancel") cancelButton.clicked.connect(self.closeEvent) - grid.addWidget(cancelButton,4, 0) + grid.addWidget(cancelButton, 4, 0) grid.setRowStretch(100, 1) self.show() self.setGeometry(self.frameSize().width() - self.geometry().width(), self.frameSize().height() - self.geometry().height(), 0, 0) def closeEvent(self, *args): + """ + Closes the import export window. + """ self.deleteLater() def parToWork(self, *args): + """ + Exports parameters to a workspace. + """ self.deleteLater() self.father.paramToWorkspaceWindow() def parToFile(self, *args): + """ + Exports parameters to a file. + """ if self.father.paramToFile(): self.deleteLater() def fileToPar(self, *args): + """ + Imports parameters from a file. + """ if self.father.fileToParam(): self.deleteLater() def curvesToWork(self, *args): + """ + Exports curves to a workspace. + """ self.deleteLater() self.father.resultToWorkspaceWindow() ################################################################################################## class FitCopySettingsWindow(QtWidgets.QWidget): + """ + The window for exporting curves to a workspace. + """ def __init__(self, parent, single=False): + """ + Initializes the curve export window. + + Parameters + ---------- + parent : AbstractParamFrame + The parameter frame from where the export window was opened. + single : bool, optional + True when the data has more than one dimension. + """ super(FitCopySettingsWindow, self).__init__(parent) self.setWindowFlags(QtCore.Qt.Window | QtCore.Qt.Tool) self.father = parent @@ -369,17 +517,23 @@ def __init__(self, parent, single=False): okButton.setFocus() box = QtWidgets.QDialogButtonBox() box.setOrientation(QtCore.Qt.Horizontal) - box.addButton(cancelButton,QtWidgets.QDialogButtonBox.RejectRole) - box.addButton(okButton,QtWidgets.QDialogButtonBox.AcceptRole) + box.addButton(cancelButton, QtWidgets.QDialogButtonBox.RejectRole) + box.addButton(okButton, QtWidgets.QDialogButtonBox.AcceptRole) layout.addWidget(box, 2, 0) grid.setRowStretch(100, 1) self.show() self.setGeometry(self.frameSize().width() - self.geometry().width(), self.frameSize().height() - self.geometry().height(), 0, 0) def closeEvent(self, *args): + """ + Closes the curve export window. + """ self.deleteLater() def applyAndClose(self, *args): + """ + Exports the curves and closes the window. + """ self.deleteLater() self.father.resultToWorkspace([self.allSlices.isChecked(), self.original.isChecked(), self.subFits.isChecked(), self.difference.isChecked()]) @@ -387,8 +541,23 @@ def applyAndClose(self, *args): class ParamCopySettingsWindow(QtWidgets.QWidget): + """ + The window for exporting parameters to a workspace. + """ def __init__(self, parent, paramNames, single=False): + """ + Initializes the export parameters window. + + Parameters + ---------- + parent : AbstractParamFrame + The parameter frame from where the export window was opened. + paramNames : list of str + A list of unique parameter names of all tabs. + single : bool, optional + True when the data has more than one dimension. + """ super(ParamCopySettingsWindow, self).__init__(parent) self.setWindowFlags(QtCore.Qt.Window | QtCore.Qt.Tool) self.father = parent @@ -403,7 +572,7 @@ def __init__(self, parent, paramNames, single=False): line.setFrameShape(QtWidgets.QFrame.HLine) grid.addWidget(line, 1, 0, 1, 2) self.exportList = [] - for i in range(len(paramNames)): + for i, _ in enumerate(paramNames): self.exportList.append(QtWidgets.QCheckBox(paramNames[i])) grid.addWidget(self.exportList[-1], i + 2, 0) cancelButton = QtWidgets.QPushButton("&Cancel") @@ -412,17 +581,23 @@ def __init__(self, parent, paramNames, single=False): okButton.clicked.connect(self.applyAndClose) okButton.setFocus() box = QtWidgets.QDialogButtonBox() - box.addButton(cancelButton,QtWidgets.QDialogButtonBox.RejectRole) - box.addButton(okButton,QtWidgets.QDialogButtonBox.AcceptRole) + box.addButton(cancelButton, QtWidgets.QDialogButtonBox.RejectRole) + box.addButton(okButton, QtWidgets.QDialogButtonBox.AcceptRole) layout.addWidget(box, 2, 0) grid.setRowStretch(100, 1) self.show() self.setGeometry(self.frameSize().width() - self.geometry().width(), self.frameSize().height() - self.geometry().height(), 0, 0) def closeEvent(self, *args): + """ + Closes the parameter export window. + """ self.deleteLater() def applyAndClose(self, *args): + """ + Exports the parameters and closes the window. + """ self.deleteLater() answers = [] for checkbox in self.exportList: @@ -433,14 +608,42 @@ def applyAndClose(self, *args): class FittingWindow(QtWidgets.QWidget): - # Inherited by the fitting windows + """ + A fitting tab window. + """ def __init__(self, father, oldMainWindow, tabWindow, fitType, isMain=True): + """ + Initializes the fitting tab window. + + Parameters + ---------- + father : MainProgram + The main program of ssnake. + oldMainWindow : Main1DWindow + The window that this fitting window replaces. + tabWindow : TabFittingWindow + The fitting window that holds this tab. + fitType : str + The name of the fit type of this frame. + Should be a key in FITTYPEDICT. + isMain : bool, optional + True if this frame is the main tab of tabWindow. + By default True. + """ super(FittingWindow, self).__init__(father) self.isMain = isMain self.father = father self.oldMainWindow = oldMainWindow + self.get_masterData = self.oldMainWindow.get_masterData # Connect functions + self.get_current = self.oldMainWindow.get_current # Connect functions self.tabWindow = tabWindow + self.getCurrentTabName = self.tabWindow.getCurrentTabName # Connect functions + self.getTabNames = self.tabWindow.getTabNames # Connect functions + self.getParams = self.tabWindow.getParams # Connect functions + self.getNum = self.tabWindow.getNum # Connect functions + self.getParamTextList = self.tabWindow.getParamTextList # Connect functions + self.addSpectrum = self.tabWindow.addSpectrum # Connect functions self.fitType = fitType self.fig = Figure() self.canvas = FigureCanvas(self.fig) @@ -448,6 +651,10 @@ def __init__(self, father, oldMainWindow, tabWindow, fitType, isMain=True): grid.addWidget(self.canvas, 0, 0) self.current = FITTYPEDICT[self.fitType][1](self, self.fig, self.canvas, self.oldMainWindow.get_current()) self.paramframe = FITTYPEDICT[self.fitType][2](self.current, self, isMain=self.isMain) + self.buttonPress = self.current.buttonPress # Connect functions + self.buttonRelease = self.current.buttonRelease # Connect functions + self.pan = self.current.pan # Connect functions + self.scroll = self.current.scroll # Connect functions grid.addWidget(self.paramframe, 1, 0, 1, 2) grid.setColumnStretch(0, 1) grid.setRowStretch(0, 1) @@ -460,42 +667,56 @@ def __init__(self, father, oldMainWindow, tabWindow, fitType, isMain=True): self.canvas.mpl_connect('scroll_event', self.scroll) def rescue(self, *args): - # This data has too little dimensions for the fitAll + """ + If rescue is called, the window does not have enough dimensions. + """ raise FittingException("This data has too little dimensions for this type of fit") - + def updAllFrames(self, *args): pass - + def fit(self): + """ + Perform a fit. + """ self.tabWindow.fit() self.paramframe.togglePick() def sim(self, *args, **kwargs): + """ + Perform a simulation. + + Parameters + ---------- + **kwargs + Keyword arguments are passed to disp of the TabFittingWindow. + """ self.tabWindow.disp(**kwargs) self.paramframe.togglePick() - def getCurrentTabName(self, *args, **kwargs): - return self.tabWindow.getCurrentTabName(*args, **kwargs) - - def getTabNames(self, *args, **kwargs): - return self.tabWindow.getTabNames(*args, **kwargs) - - def getParams(self, *args, **kwargs): - return self.tabWindow.getParams(*args, **kwargs) - - def getNum(self, *args, **kwargs): - return self.tabWindow.getNum(*args, **kwargs) - - def getParamTextList(self, *args, **kwargs): - return self.tabWindow.getParamTextList(*args, **kwargs) - - def addSpectrum(self): - self.tabWindow.addSpectrum() - def removeSpectrum(self): + """ + Removes itself from the tabs. + """ self.tabWindow.removeSpectrum(self) def createNewData(self, data, axis, params=False, fitAll=False): + """ + Creates the data for a new workspace. + + Parameters + ---------- + data : ndarray + The data to be used in the new workspace. + axis : int + The axis from which the data should be taken from the masterData. + params : bool, optional + Whether the data was created from parameters. + False by default. + fitAll : bool, optional + Whether the data was created from all slices of the data. + False by default. + """ masterData = self.get_masterData() if fitAll: if params: @@ -541,31 +762,27 @@ def createNewData(self, data, axis, params=False, fitAll=False): 0) def rename(self, name): + """ + Renames the workspace. + + Parameters + ---------- + name : str + The new name of the workspace. + """ self.canvas.draw() self.oldMainWindow.rename(name) - def buttonPress(self, event): - self.current.buttonPress(event) - - def buttonRelease(self, event): - self.current.buttonRelease(event) - - def pan(self, event): - self.current.pan(event) - - def scroll(self, event): - self.current.scroll(event) - def get_mainWindow(self): + """ + Returns the original workspace window. + """ return self.oldMainWindow - def get_masterData(self): - return self.oldMainWindow.get_masterData() - - def get_current(self): - return self.oldMainWindow.get_current() - def kill(self): + """ + Completely closes the workspace. + """ for i in reversed(range(self.grid.count())): self.grid.itemAt(i).widget().deleteLater() self.grid.deleteLater() @@ -578,7 +795,10 @@ def kill(self): self.deleteLater() def cancel(self): - self.tabWindow.tabs.currentChanged.disconnect() #Disconnect tabs before closing, to avoid change index signal + """ + Closes the fitting window and restores the original workspace window. + """ + self.tabWindow.tabs.currentChanged.disconnect() # Disconnect tabs before closing, to avoid change index signal for i in reversed(range(self.grid.count())): self.grid.itemAt(i).widget().deleteLater() self.grid.deleteLater() @@ -601,12 +821,29 @@ class FittingSideFrame(SideFrame): class FitPlotFrame(Current1D): + """ + The frame to plot the spectra during fitting. + """ MARKER = '' LINESTYLE = '-' - FITNUM = 20 # Standard number of fits + FITNUM = 20 # Standard number of fits def __init__(self, rootwindow, fig, canvas, current): + """ + Initializes the fitting plot window. + + Parameters + ---------- + rootwindow : FittingWindow + The window that contains the figure. + fig : Figure + The figure used in this frame. + canvas : FigureCanvas + The canvas of fig. + current : PlotFrame + The view of the original workspace. + """ self.data = current.data tmp = np.array(current.data.shape(), dtype=int) tmp = np.delete(tmp, self.fixAxes(current.axes)) @@ -616,9 +853,22 @@ def __init__(self, rootwindow, fig, canvas, current): super(FitPlotFrame, self).__init__(rootwindow, fig, canvas, current.data, current) def getRedLocList(self): + """ + Returns the reduced location list with the displayed axis removed. + """ return tuple(np.delete(self.locList, self.axes)) - + def setSlice(self, axes, locList): + """ + Changes the displayed slice. + + Parameters + ---------- + axes : array_like of int + The list of axes of the slice to be displayed. + locList : array_like of int + The location of the slice to be displayed. + """ self.rootwindow.paramframe.checkInputs() self.pickWidth = False super(FitPlotFrame, self).setSlice(axes, locList) @@ -627,9 +877,15 @@ def setSlice(self, axes, locList): self.rootwindow.paramframe.togglePick() def getData1D(self): + """ + Returns the raw data. + """ return np.real(self.getDataType(self.data1D.getHyperData(0))) def showFid(self): + """ + Displays the plot and fit curves. + """ extraX = [] extraY = [] self.locList = np.array(self.locList, dtype=int) @@ -652,24 +908,40 @@ def showFid(self): class AbstractParamFrame(QtWidgets.QWidget): - - FITFUNC = None # Function used for fitting and simulation - SINGLENAMES = [] - MULTINAMES = [] - EXTRANAMES = [] - TICKS = True # Fitting parameters can be fixed by checkboxes - FFT_AXES = () # Which axes should be transformed after simulation - FFTSHIFT_AXES = () # Which axes should be transformed after simulation - DIM = 1 # Number of dimensions of the fit + """ + The base class of all parameter frames. + """ + + FITFUNC = None # Function used for fitting and simulation + SINGLENAMES = [] # The names of the parameters which are common for all sites + MULTINAMES = [] # The names of the parameters which increase with the number of sites + EXTRANAMES = [] # The names of additional parameters + TICKS = True # Fitting parameters can be fixed by checkboxes + FFT_AXES = () # Which axes should be transformed after simulation + FFTSHIFT_AXES = () # Which axes should be transformed after simulation + DIM = 1 # Number of dimensions of the fit def __init__(self, parent, rootwindow, isMain=True): + """ + Initializes the parameter frame. + + Parameters + ---------- + parent : FitPlotFrame + The plot frame connected to this parameter frame. + rootwindow : FittingWindow + The fitting tab that holds this parameter frame. + isMain : bool, optional + True if this frame is part of the main tab. + """ super(AbstractParamFrame, self).__init__(rootwindow) self.parent = parent + self.getRedLocList = self.parent.getRedLocList # Connect function self.FITNUM = self.parent.FITNUM self.rootwindow = rootwindow - self.isMain = isMain # display fitting buttons - self.ticks = {key: [] for key in (self.SINGLENAMES + self.MULTINAMES)} - self.entries = {key: [] for key in (self.SINGLENAMES + self.MULTINAMES + self.EXTRANAMES)} + self.isMain = isMain # display fitting buttons + self.ticks = {key: [] for key in self.SINGLENAMES + self.MULTINAMES} + self.entries = {key: [] for key in self.SINGLENAMES + self.MULTINAMES + self.EXTRANAMES} tmp = np.array(self.parent.data.shape(), dtype=int) tmp = np.delete(tmp, self.parent.axes) self.fitParamList = np.zeros(tmp, dtype=object) @@ -715,10 +987,10 @@ def __init__(self, parent, rootwindow, isMain=True): self.frame3.setAlignment(QtCore.Qt.AlignTop) self.simButton = QtWidgets.QPushButton("Sim") self.simButton.clicked.connect(self.rootwindow.sim) - self.frame1.addWidget(self.simButton, 0, 0,1,1) + self.frame1.addWidget(self.simButton, 0, 0, 1, 1) self.simBusyButton = QtWidgets.QPushButton("Busy") - self.simBusyButton.setEnabled(False) - self.frame1.addWidget(self.simBusyButton, 0, 0,1,1) + self.simBusyButton.setEnabled(False) + self.frame1.addWidget(self.simBusyButton, 0, 0, 1, 1) self.simBusyButton.hide() if self.isMain: fitButton = QtWidgets.QPushButton("Fit") @@ -726,7 +998,7 @@ def __init__(self, parent, rootwindow, isMain=True): self.frame1.addWidget(fitButton, 0, 1) self.stopButton = QtWidgets.QPushButton("Stop") self.stopButton.clicked.connect(self.rootwindow.tabWindow.stopMP) - self.stopButton.setStyleSheet('background-color: green') + self.stopButton.setStyleSheet('background-color: green') self.frame1.addWidget(self.stopButton, 0, 1) self.stopButton.hide() fitAllButton = QtWidgets.QPushButton("Fit all") @@ -734,7 +1006,7 @@ def __init__(self, parent, rootwindow, isMain=True): self.frame1.addWidget(fitAllButton, 1, 0) self.stopAllButton = QtWidgets.QPushButton("Stop all") self.stopAllButton.clicked.connect(self.rootwindow.tabWindow.stopAll) - self.stopAllButton.setStyleSheet('background-color: green') + self.stopAllButton.setStyleSheet('background-color: green') self.frame1.addWidget(self.stopAllButton, 1, 0) self.stopAllButton.hide() prefButton = QtWidgets.QPushButton("Preferences") @@ -758,28 +1030,44 @@ def __init__(self, parent, rootwindow, isMain=True): self.frame1.addWidget(cancelButton, 4, 0, 1, 2) self.checkFitParamList(self.getRedLocList()) - def getRedLocList(self): - return self.parent.getRedLocList() - def togglePick(self): # Dummy function for fitting routines which require peak picking pass def reset(self): + """ + Resets all fit parameters to their default values. + """ locList = self.getRedLocList() self.fitNumList[locList] = 0 - self.fitParamList[locList] = self.defaultValues(0) + self.fitParamList[locList] = self.defaultValues() self.dispParams() - + def checkFitParamList(self, locList): + """ + Checks whether the fit parameters exist for a given slice of the data. + When the fit parameters are not available they will be set to the default values. + + Parameters + ---------- + locList : array_like of int + The location (indices) for which to check the fit parameters. + """ locList = tuple(locList) if not self.fitParamList[locList]: - self.fitParamList[locList] = self.defaultValues(0) - - def defaultValues(self, inp): - if inp: - return inp - tmpVal = {key: None for key in (self.SINGLENAMES + self.MULTINAMES)} + self.fitParamList[locList] = self.defaultValues() + + def defaultValues(self): + """ + Creates a dictionary with default values based on self.DEFAULTS. + When no default is available for a parameter it is set to [0.0, False] + + Returns + ------- + dict + The dictionary with defaults for all parameters. + """ + tmpVal = {key: None for key in self.SINGLENAMES + self.MULTINAMES} for name in self.SINGLENAMES: if name in self.DEFAULTS.keys(): tmpVal[name] = self.DEFAULTS[name] @@ -793,24 +1081,59 @@ def defaultValues(self, inp): return tmpVal def addMultiLabel(self, name, text, num): + """ + Creates a label for a parameter with multiple sites and adds it to frame3. + + Parameters + ---------- + name : str + Name of the parameter. + text : str + The text on the label. + num : int + The column to place the label widget. + + Returns + ------- + QCheckBox + The checkbox next to the label. + QLabel + The label. + """ tick = QtWidgets.QCheckBox('') tick.setChecked(self.DEFAULTS[name][1]) tick.stateChanged.connect(lambda state, self=self: self.changeAllTicks(state, name)) self.frame3.addWidget(tick, 1, num) label = wc.QLabel(text) self.frame3.addWidget(label, 1, num+1) - return (tick, label) - + return tick, label + def changeAllTicks(self, state, name): + """ + Sets or unsets all checkboxes of a given parameter. + + Parameters + ---------- + state : bool + The checkboxes will be set to this state. + name : str + The name of the parameter. + """ self.DEFAULTS[name][1] = state for tick in self.ticks[name]: tick.setChecked(state) - + def closeWindow(self, *args): + """ + Closes the fitting window. + """ self.rootwindow.tabWindow.stopMP() self.rootwindow.cancel() def copyParams(self): + """ + Copies the parameters of the current slice to all other slices of the data. + """ self.checkInputs() locList = self.getRedLocList() self.checkFitParamList(locList) @@ -820,6 +1143,9 @@ def copyParams(self): elem[...] = self.fitNumList[locList] def dispParams(self): + """ + Displays the values from the fit parameter list in the window. + """ locList = self.getRedLocList() val = self.fitNumList[locList] + 1 for name in self.SINGLENAMES: @@ -829,7 +1155,7 @@ def dispParams(self): self.entries[name][0].setText(str(self.fitParamList[locList][name][0])) if self.TICKS: self.ticks[name][0].setChecked(self.fitParamList[locList][name][1]) - self.setNumExp() + self.numExp.setCurrentIndex(self.fitNumList[locList]) for i in range(self.FITNUM): for name in self.MULTINAMES: if isinstance(self.fitParamList[locList][name][i][0], (float, int)): @@ -847,11 +1173,10 @@ def dispParams(self): self.ticks[name][i].hide() self.entries[name][i].hide() - def setNumExp(self): - locList = self.getRedLocList() - self.numExp.setCurrentIndex(self.fitNumList[locList]) - def changeNum(self, *args): + """ + Set the number of sites to the value shown in the box. + """ val = self.numExp.currentIndex() + 1 locList = self.getRedLocList() self.fitNumList[locList] = self.numExp.currentIndex() @@ -867,6 +1192,14 @@ def changeNum(self, *args): self.entries[name][i].hide() def checkInputs(self): + """ + Checks the values set in the parameter boxes for validity. + + Returns + ------- + bool + True if all inputs are valid. + """ locList = self.getRedLocList() numExp = self.getNumExp() for name in self.SINGLENAMES: @@ -875,7 +1208,7 @@ def checkInputs(self): inp = safeEval(self.entries[name][0].text()) if inp is None: return False - elif isinstance(inp, float): + if isinstance(inp, float): self.entries[name][0].setText(('%#.' + str(self.rootwindow.tabWindow.PRECIS) + 'g') % inp) else: self.entries[name][0].setText(str(inp)) @@ -887,7 +1220,7 @@ def checkInputs(self): inp = safeEval(self.entries[name][i].text()) if inp is None: return False - elif isinstance(inp, float): + if isinstance(inp, float): self.entries[name][i].setText(('%#.' + str(self.rootwindow.tabWindow.PRECIS) + 'g') % inp) else: self.entries[name][i].setText(str(inp)) @@ -895,16 +1228,30 @@ def checkInputs(self): return True def getNumExp(self): + """ + Returns the number of sites as set in the box. + """ return self.numExp.currentIndex() + 1 def getExtraParams(self, out): + """ + Returns the extra parameters of the fit. + """ return (out, []) def getFitParams(self): + """ + Creates the parameters and data required for the fit. + + Returns + ------- + tuple + The tuple with required fitting information. + """ if not self.checkInputs(): raise FittingException("Fitting: One of the inputs is not valid") struc = {} - for name in (self.SINGLENAMES + self.MULTINAMES): + for name in self.SINGLENAMES + self.MULTINAMES: struc[name] = [] guess = [] argu = [] @@ -943,7 +1290,17 @@ def getFitParams(self): args = ([numExp], [struc], [argu], [self.parent.data1D.freq], [self.parent.data1D.sw], [self.axMult], [self.FFT_AXES], [self.FFTSHIFT_AXES], [self.SINGLENAMES], [self.MULTINAMES]) return (self.parent.data1D.xaxArray[-self.DIM:], self.parent.getData1D(), guess, args, out) - def setResults(self, fitVal, args, out): + def setResults(self, fitVal, args): + """ + Set the results in the fit parameter list based on the given fit results. + + Parameters + ---------- + fitVal : array_like + The results of the fit. + args : list + The arguments to the fit. + """ locList = self.getRedLocList() numExp = args[0][0] struc = args[1][0] @@ -954,15 +1311,18 @@ def setResults(self, fitVal, args, out): for name in self.MULTINAMES: if struc[name][i][0] == 1: self.fitParamList[locList][name][i][0] = fitVal[struc[name][i][1]] - self.checkResults(numExp,struc) + self.checkResults(numExp, struc) self.dispParams() self.rootwindow.sim() - def checkResults(self, struc, numExp): - #A placeholder for a function that checks the fit results (e.g. makes values absolute, etc) + def checkResults(self, numExp, struc): + # A dummy function that is replaced by a function that checks the fit results (e.g., makes values absolute, etc) pass def getSimParams(self): + """ + Returns the dictionary with simulation parameters. + """ if not self.checkInputs(): raise FittingException("Fitting: One of the inputs is not valid") numExp = self.getNumExp() @@ -982,12 +1342,22 @@ def getSimParams(self): return out def extraParamToFile(self): + # Dummy function return ({}, {}) def extraFileToParam(self, preParams, postParams): + # Dummy function pass def paramToFile(self): + """ + Writes the fit parameters to a file. + + Returns + ------- + bool + True if an output file was written. + """ if not self.checkInputs(): raise FittingException("Fitting: One of the inputs is not valid") fileName = QtWidgets.QFileDialog.getSaveFileName(self, 'Save parameters', self.rootwindow.father.lastLocation + os.path.sep + self.parent.data.name + '_fit.txt', 'txt (*.txt)') @@ -996,13 +1366,13 @@ def paramToFile(self): if not fileName: return False printLocList = list(self.parent.locList) - for i in range(len(printLocList)): + for i, _ in enumerate(printLocList): if i in self.parent.axes: - printLocList[i] = '*' + printLocList[i] = '*' else: printLocList[i] = str(printLocList[i]) printLocList = "(" + ", ".join(printLocList) + ")" - report = "#########" + '#'*len(VERSION) + "##\n" + report = "#########" + '#'*len(VERSION) + "##\n" report += "# ssNake " + VERSION + " #\n" report += "#########" + '#'*len(VERSION) + "##\n" report += "#\n" @@ -1057,22 +1427,30 @@ def paramToFile(self): return True def fileToParam(self): + """ + Reads the fit parameters from a file as generated by paramToFile. + + Returns + ------- + bool + True if the file was read successfully. + """ fileName = QtWidgets.QFileDialog.getOpenFileName(self, 'Load parameters', self.rootwindow.father.lastLocation) if isinstance(fileName, tuple): fileName = fileName[0] - if len(fileName) == 0: - return + if not fileName: + return False with open(fileName, "r") as fp: report = fp.read() splitReport = re.split("#{20,}\n", report) postReport = splitReport[1] postReport = re.split("#!", postReport) - if len(postReport)==1: + if len(postReport) == 1: postReport = [] else: postReport = postReport[1:] postParams = {} - for i in range(len(postReport)): + for i, _ in enumerate(postReport): tmp = postReport[i].split('\n', 1) postParams[tmp[0].strip()] = tmp[1].strip() splitReport = re.split("#\?", splitReport[0]) @@ -1089,7 +1467,7 @@ def fileToParam(self): singleVals = [] for line in singleReport[1:]: tmp = line.strip() - if tmp and tmp[0]!='#': + if tmp and tmp[0] != '#': singleVals.append(tmp.split()) if len(singleVals) > 1: raise FittingException("Incorrect number of parameters in file") @@ -1098,7 +1476,7 @@ def fileToParam(self): multiVals = [] for line in multiReport[1:]: tmp = line.strip() - if tmp and tmp[0]!='#': + if tmp and tmp[0] != '#': multiVals.append(tmp.split()) multiVals = self.__interpretParam(multiVals) self.extraFileToParam(preParams, postParams) @@ -1107,10 +1485,22 @@ def fileToParam(self): return True def __interpretParam(self, strList): + """ + Checks a given list of strings to interpret them as fit parameters. + + Parameters + ---------- + strList : list of lists of str + + Returns + ------- + list + List of values or tuples (as long as they match a link tuple). + """ data = [] - for i in range(len(strList)): + for i, _ in enumerate(strList): tmp = [] - for j in range(len(strList[i])): + for j, _ in enumerate(strList[i]): if strList[i][j][0] == '*': tmp.append([safeEval(strList[i][j][1:]), True]) else: @@ -1119,34 +1509,58 @@ def __interpretParam(self, strList): tmp[-1][0] = checkLinkTuple(tmp[-1][0]) data.append(tmp) return data - + def setParamFromList(self, singleNames, singleVals, multiNames, multiVals): + """ + Set the values in the fit parameter list to the given values. + + Parameters + ---------- + singleNames : list of str + The names of the parameters shared by all sites. + singleVals : list + The fit values corresponding to singleNames. + multiNames : list of str + The names of the parameters for individual sites. + multiVals : list + The fit values corresponding to multiNames. + """ locList = self.getRedLocList() keys = self.fitParamList[locList].keys() if singleVals: singleVals = singleVals[0] if len(singleNames) != len(singleVals): raise FittingException("The number of parameters does not match the parameter names") - for i in range(len(singleNames)): + for i, _ in enumerate(singleNames): if singleNames[i] in keys: self.fitParamList[locList][singleNames[i]] = singleVals[i] - for j in range(len(multiVals)): + for j, _ in enumerate(multiVals): if len(multiNames) != len(multiVals[j]): raise FittingException("The number of parameters does not match the parameter names") - for i in range(len(multiNames)): + for i, _ in enumerate(multiNames): if multiNames[i] in keys: self.fitParamList[locList][multiNames[i]][j] = multiVals[j][i] self.fitNumList[locList] = len(multiVals) - 1 def paramToWorkspaceWindow(self): + """ + Opens the window for exporting parameters to a workspace. + """ paramNameList = self.SINGLENAMES + self.MULTINAMES - if self.parent.data.ndim() == 1: - single = True - else: - single = False + single = self.parent.data.ndim() == 1 ParamCopySettingsWindow(self, paramNameList, single) def paramToWorkspace(self, allSlices, settings): + """ + Exports parameters to a new workspace. + + Parameters + ---------- + allSlices : bool + True to export the parameters from all data slices. + settings : list of bool + A list of booleans in the order of SINGLENAMES+MULTINAMES whether to export a certain parameter type. + """ if not self.checkInputs(): raise FittingException("Fitting: One of the inputs is not valid") paramNameList = np.array(self.SINGLENAMES + self.MULTINAMES, dtype=object) @@ -1167,7 +1581,7 @@ def paramToWorkspace(self, allSlices, settings): grid = np.array([i.flatten() for i in np.meshgrid(*tmp2)]).T for i in grid: self.checkFitParamList(tuple(i)) - for j in range(len(names)): + for j, _ in enumerate(names): if names[j] in self.SINGLENAMES: inp = self.fitParamList[tuple(i)][names[j]][0] if isinstance(inp, tuple): @@ -1179,7 +1593,7 @@ def paramToWorkspace(self, allSlices, settings): data[(j,) + (slice(None),) + tuple(i)].fill(inp) else: tmpInp = self.fitParamList[tuple(i)][names[j]].T[0][:(self.fitNumList[tuple(i)] + 1)] - for n in range(len(tmpInp)): + for n, _ in enumerate(tmpInp): inp = tmpInp[n] if isinstance(inp, tuple): inp = checkLinkTuple(inp) @@ -1191,7 +1605,7 @@ def paramToWorkspace(self, allSlices, settings): self.rootwindow.createNewData(data, self.parent.axes[-1], True, True) else: data = np.zeros((sum(settings), self.fitNumList[locList] + 1)) - for i in range(len(names)): + for i, _ in enumerate(names): if names[i] in self.SINGLENAMES: inp = self.fitParamList[locList][names[i]][0] if isinstance(inp, tuple): @@ -1200,7 +1614,7 @@ def paramToWorkspace(self, allSlices, settings): data[i].fill(inp) else: tmpInp = self.fitParamList[locList][names[i]].T[0][:(self.fitNumList[locList] + 1)] - for j in range(len(tmpInp)): + for j, _ in enumerate(tmpInp): inp = tmpInp[j] if isinstance(inp, tuple): inp = checkLinkTuple(inp) @@ -1212,13 +1626,21 @@ def exportResultWindow(self): ResultsExportWindow(self) def resultToWorkspaceWindow(self): - if self.parent.data.ndim() == 1: - single = True - else: - single = False + """ + Opens the window for exporting curves to a workspace. + """ + single = self.parent.data.ndim() == 1 FitCopySettingsWindow(self, single) def resultToWorkspace(self, settings): + """ + Exports curves to a new workspace. + + Parameters + ---------- + settings : list of bool + A list of booleans whether to include [all slices, the original, the subfits, the difference]. + """ if settings is None: return if settings[0]: @@ -1249,6 +1671,21 @@ def resultToWorkspace(self, settings): self.rootwindow.createNewData(data, self.parent.axes[-1], False) def prepareResultToWorkspace(self, settings, minLength=1): + """ + Generates the data required to export curves to a workspace. + + Parameters + ---------- + settings : list of bool + A list of booleans whether to include [all slices, the original, the subfits, the difference]. + minLength : int + The minimum length of the data to export. + + Returns + ------- + ndarray + The curves to export. + """ self.calculateResultsToWorkspace() locList = self.getRedLocList() fitData = self.parent.fitDataList[locList] @@ -1268,7 +1705,7 @@ def prepareResultToWorkspace(self, settings, minLength=1): outCurvePart.append(fitData[1]) return np.array(outCurvePart) - def calculateResultsToWorkspace(self, *args): + def calculateResultsToWorkspace(self): # Some fitting methods need to recalculate the curves before exporting pass @@ -1277,8 +1714,22 @@ def createPrefWindow(self, *args): def getDispX(self, *args): return self.parent.data1D.xaxArray[-self.DIM:] - + def disp(self, params, num, display=True): + """ + Simulate the spectrum and displays it. + + Parameters + ---------- + params : list + The list of parameters of all tabs. + num : int + The tab number. + The parameters at position num in params belong to this tab. + display : bool, optional + When True the simulated data will also be displayed. + True by default. + """ out = params[num] try: for name in self.SINGLENAMES: @@ -1286,7 +1737,7 @@ def disp(self, params, num, display=True): if isinstance(inp, tuple): inp = checkLinkTuple(inp) out[name][0] = inp[2] * params[inp[4]][inp[0]][inp[1]] + inp[3] - if len(self.MULTINAMES) == 0: #Abort if no names + if not self.MULTINAMES: #Abort if no names return numExp = len(out[self.MULTINAMES[0]]) for i in range(numExp): @@ -1332,17 +1783,57 @@ def disp(self, params, num, display=True): ############################################################################## def lstSqrs(dataList, *args): + """ + Simulates spectra and calculates the least squares value with a given list of data. + + Parameters + ---------- + dataList : list of arrays + The list of spectra to compare with the simulations. + *args + All other arguments are passed to fitFunc. + + Returns + ------- + float + The sum of the least squares values of the spectra. + """ simData = fitFunc(*args) if simData is None: return np.inf costValue = 0 - for i in range(len(dataList)): + for i,_ in enumerate(dataList): costValue += np.sum((dataList[i] - simData[i])**2) return costValue def mpFit(xax, data1D, guess, args, queue, funcs, minmethod, numfeval): + """ + The minimization function running in an separate process. + + Parameters + ---------- + xax : list of arrays + List of the x-axes of the data. + data1D : array or list of arrays + Array with the data to be fit. + guess : list + List with the initial guess values. + args : tuple + The tuple with additional values. + queue : Queue + The queue to communicate with the main process. + On success the results are put in this queue. + When a SimException is raised, the error message is put on this queue. + When the simulation fails otherwise, None is put on this queue. + funcs : list of functions + The functions to run per data in data1D. + minmethod : str + The minimization method of Scipy minimize to use. + numfeval : int + The maximum number of function evaluations. + """ try: - fitVal = scipy.optimize.minimize(lambda *param: lstSqrs(data1D, funcs, param, xax, args), guess, method=minmethod, options = {'maxfev': numfeval}) + fitVal = scipy.optimize.minimize(lambda *param: lstSqrs(data1D, funcs, param, xax, args), guess, method=minmethod, options={'maxfev': numfeval}) except simFunc.SimException as e: fitVal = str(e) except Exception: @@ -1350,6 +1841,25 @@ def mpFit(xax, data1D, guess, args, queue, funcs, minmethod, numfeval): queue.put(fitVal) def fitFunc(funcs, params, allX, args): + """ + Reconstructs all linked parameters and executes the fitting function for each set of data. + + Parameters + ---------- + funcs : list of functions + The list of fitting functions to execute. + params : tuple + The tuple with the function parameters generated by minimize. + allX : list of arrays + The list with x-axes. + args : tuple + Additional arguments for the fitting functions. + + Returns + ------- + list of arrays + A list with the simulated data. + """ params = params[0] specSlices = args[0] allParam = [] @@ -1358,7 +1868,7 @@ def fitFunc(funcs, params, allX, args): allStruc = args[2] allArgu = args[3] fullTestFunc = [] - for n in range(len(allX)): + for n, _ in enumerate(allX): x = allX[n] testFunc = np.zeros([len(item) for item in x], dtype=complex) param = allParam[n] @@ -1418,10 +1928,21 @@ def fitFunc(funcs, params, allX, args): class PrefWindow(QtWidgets.QWidget): + """ + Window for setting the fitting preferences. + """ METHODLIST = ['Powell', 'Nelder-Mead'] def __init__(self, parent): + """ + Initializes the fitting preference window. + + Parameters + ---------- + parent : TabFittingWindow + The fitting window that opened the preference window. + """ super(PrefWindow, self).__init__(parent) self.setWindowFlags(QtCore.Qt.Window | QtCore.Qt.Tool) self.father = parent @@ -1456,9 +1977,15 @@ def __init__(self, parent): self.setGeometry(self.frameSize().width() - self.geometry().width(), self.frameSize().height() - self.geometry().height(), 0, 0) def closeEvent(self, *args): + """ + Closes the window. + """ self.deleteLater() def applyAndClose(self, *args): + """ + Sets the preferences in the fitting window and closes. + """ self.father.PRECIS = self.precisBox.value() self.father.MINMETHOD = self.METHODLIST[self.minmethodBox.currentIndex()] self.father.NUMFEVAL = self.numFevalBox.value() @@ -1479,8 +2006,20 @@ class RelaxParamFrame(AbstractParamFrame): SINGLENAMES = ['Amplitude', 'Constant'] MULTINAMES = ['Coefficient', 'T'] - + def __init__(self, parent, rootwindow, isMain=True): + """ + Initializes the relaxation fit parameter frame. + + Parameters + ---------- + parent : FitPlotFrame + The plot frame connected to this parameter frame. + rootwindow : FittingWindow + The fitting tab that holds this parameter frame. + isMain : bool, optional + True if this frame is part of the main tab. + """ self.FITFUNC = simFunc.relaxationFunc self.fullInt = np.max(parent.getData1D()) self.DEFAULTS = {'Amplitude': [self.fullInt, False], 'Constant': [1.0, False], 'Coefficient': [-1.0, False], 'T': [1.0, False]} @@ -1518,23 +2057,38 @@ def __init__(self, parent, rootwindow, isMain=True): self.reset() def reset(self): + """ + Resets all fit parameters to their default values. + """ self.xlog.setChecked(self.extraDefaults['xlog']) self.ylog.setChecked(self.extraDefaults['ylog']) super(RelaxParamFrame, self).reset() def setLog(self, *args): + """ + Set the plot to logarithmic or linear scale. + """ self.parent.setLog(self.xlog.isChecked(), self.ylog.isChecked()) self.rootwindow.sim() def getExtraParams(self, out): + """ + Returns the extra parameters of the fit. + """ out['extra'] = [] return (out, out['extra']) - + def calculateResultsToWorkspace(self): + """ + Recalculate the curves to have points located at the same locations as the experimental data. + """ self.rootwindow.sim(display=False) def getDispX(self, *args): - numCurve = 256 # number of points in output curve + """ + Return the x-axis of the plot. + """ + numCurve = 256 # number of points in output curve realx = self.parent.xax() minx = min(realx) maxx = max(realx) @@ -1544,10 +2098,13 @@ def getDispX(self, *args): x = np.linspace(minx, maxx, numCurve) return [x] - def checkResults(self,numExp,struc): + def checkResults(self, numExp, struc): + """ + Sets the relaxation times to absolute values. + """ locList = self.getRedLocList() for i in range(numExp): - if struc['T'][i][0] == 1: + if struc['T'][i][0] == 1: self.fitParamList[locList]['T'][i][0] = abs(self.fitParamList[locList]['T'][i][0]) @@ -1558,14 +2115,25 @@ class DiffusionParamFrame(AbstractParamFrame): SINGLENAMES = ['Amplitude', 'Constant'] MULTINAMES = ['Coefficient', 'D'] - + def __init__(self, parent, rootwindow, isMain=True): + """ + Initializes the diffusion fit parameter frame. + + Parameters + ---------- + parent : FitPlotFrame + The plot frame connected to this parameter frame. + rootwindow : FittingWindow + The fitting tab that holds this parameter frame. + isMain : bool, optional + True if this frame is part of the main tab. + """ self.FITFUNC = simFunc.diffusionFunc self.fullInt = np.max(parent.getData1D()) self.DEFAULTS = {'Amplitude': [self.fullInt, False], 'Constant': [0.0, True], 'Coefficient': [1.0, False], 'D': [1.0e-9, False]} self.extraDefaults = {'xlog': False, 'ylog': False, 'gamma': "42.576", 'delta': '1.0', 'triangle': '1.0'} super(DiffusionParamFrame, self).__init__(parent, rootwindow, isMain) - locList = self.getRedLocList() self.optframe.addWidget(wc.QLabel(u"γ [MHz/T]:"), 0, 1) self.gammaEntry = wc.QLineEdit() self.optframe.addWidget(self.gammaEntry, 1, 1) @@ -1608,6 +2176,9 @@ def __init__(self, parent, rootwindow, isMain=True): self.reset() def reset(self): + """ + Resets all fit parameters to their default values. + """ self.xlog.setChecked(self.extraDefaults['xlog']) self.ylog.setChecked(self.extraDefaults['ylog']) self.gammaEntry.setText(self.extraDefaults['gamma']) @@ -1616,16 +2187,25 @@ def reset(self): super(DiffusionParamFrame, self).reset() def setLog(self, *args): + """ + Set the plot to logarithmic or linear scale. + """ self.parent.setLog(self.xlog.isChecked(), self.ylog.isChecked()) self.rootwindow.sim() def extraParamToFile(self): + """ + Extra parameters to export. + """ extraDict = {"gamma": self.gammaEntry.text(), "delta": self.deltaEntry.text(), "DELTA": self.triangleEntry.text()} return (extraDict, {}) def extraFileToParam(self, preParams, postParams): + """ + Extra parameters to import. + """ keys = preParams.keys() if "gamma" in keys: self.gammaEntry.setText(preParams["gamma"]) @@ -1635,6 +2215,9 @@ def extraFileToParam(self, preParams, postParams): self.triangleEntry.setText(preParams["DELTA"]) def getExtraParams(self, out): + """ + Returns the extra parameters of the fit. + """ gamma = safeEval(self.gammaEntry.text()) delta = safeEval(self.deltaEntry.text()) triangle = safeEval(self.triangleEntry.text()) @@ -1642,10 +2225,16 @@ def getExtraParams(self, out): return (out, out['extra']) def calculateResultsToWorkspace(self): + """ + Recalculate the curves to have points located at the same locations as the experimental data. + """ self.rootwindow.sim(display=False) def getDispX(self, *args): - numCurve = 256 # number of points in output curve + """ + Return the x-axis of the plot. + """ + numCurve = 256 # number of points in output curve realx = self.parent.xax() minx = min(realx) maxx = max(realx) @@ -1655,17 +2244,20 @@ def getDispX(self, *args): x = np.linspace(minx, maxx, numCurve) return [x] - def checkResults(self,numExp,struc): + def checkResults(self, numExp, struc): + """ + Sets the relaxation times to absolute values. + """ locList = self.getRedLocList() for i in range(numExp): - if struc['D'][i][0] == 1: + if struc['D'][i][0] == 1: self.fitParamList[locList]['D'][i][0] = abs(self.fitParamList[locList]['D'][i][0]) ############################################################################## class PeakDeconvFrame(FitPlotFrame): - + def togglePick(self, var): self.peakPickReset() if var == 1 and self.fitPickNumList[self.getRedLocList()] < self.FITNUM: @@ -1679,7 +2271,6 @@ def pickDeconv(self, pos): locList = self.getRedLocList() pickNum = self.fitPickNumList[locList] if self.pickWidth: - axMult = self.getAxMult(self.spec(), self.getAxType(), self.getppm(), self.freq(), self.ref()) width = (2 * abs(float(self.rootwindow.paramframe.entries["Position"][pickNum].text()) - pos[1])) / self.getAxMult(self.spec(), self.getAxType(), self.getppm(), self.freq(), self.ref()) self.rootwindow.paramframe.entries["Integral"][pickNum].setText(('%#.' + str(self.rootwindow.tabWindow.PRECIS) + 'g') % (float(self.rootwindow.paramframe.entries["Integral"][pickNum].text()) * width)) self.rootwindow.paramframe.entries["Lorentz"][pickNum].setText(('%#.' + str(self.rootwindow.tabWindow.PRECIS) + 'g') % abs(width)) @@ -1708,12 +2299,24 @@ def pickDeconv(self, pos): class PeakDeconvParamFrame(AbstractParamFrame): - FFT_AXES = (0,) # Which axes should be transformed after simulation + FFT_AXES = (0,) # Which axes should be transformed after simulation FFTSHIFT_AXES = (0,) # Which axes should be transformed after simulation SINGLENAMES = ["Offset", "Multiplier"] MULTINAMES = ["Position", "Integral", "Lorentz", "Gauss"] - + def __init__(self, parent, rootwindow, isMain=True): + """ + Initializes the peak deconvolution parameter frame. + + Parameters + ---------- + parent : FitPlotFrame + The plot frame connected to this parameter frame. + rootwindow : FittingWindow + The fitting tab that holds this parameter frame. + isMain : bool, optional + True if this frame is part of the main tab. + """ self.fullInt = np.sum(parent.getData1D()) * parent.sw() / float(len(parent.getData1D())) self.FITFUNC = simFunc.peakSim self.DEFAULTS = {"Offset": [0.0, True], "Multiplier": [1.0, True], "Position": [0.0, False], "Integral": [self.fullInt, False], "Lorentz": [1.0, False], "Gauss": [0.0, True]} @@ -1746,8 +2349,11 @@ def __init__(self, parent, rootwindow, isMain=True): self.entries[self.MULTINAMES[j]].append(wc.FitQLineEdit(self, self.MULTINAMES[j])) self.frame3.addWidget(self.entries[self.MULTINAMES[j]][i], i + 2, 2 * j + 1) self.reset() - + def reset(self): + """ + Resets all fit parameters to their default values. + """ locList = self.getRedLocList() self.pickTick.setChecked(True) self.togglePick() @@ -1758,13 +2364,15 @@ def reset(self): def togglePick(self): self.parent.togglePick(self.pickTick.isChecked()) - def checkResults(self,numExp,struc): - #After fit, set lor and gauss absolute + def checkResults(self, numExp, struc): + """ + Sets the Lorentzian and Gaussian broadenings to absolute values. + """ locList = self.getRedLocList() for i in range(numExp): - if struc["Lorentz"][i][0] == 1: + if struc["Lorentz"][i][0] == 1: self.fitParamList[locList]["Lorentz"][i][0] = abs(self.fitParamList[locList]["Lorentz"][i][0]) - if struc["Gauss"][i][0] == 1: + if struc["Gauss"][i][0] == 1: self.fitParamList[locList]["Gauss"][i][0] = abs(self.fitParamList[locList]["Gauss"][i][0]) ############################################################################## @@ -1815,7 +2423,6 @@ class CsaDeconvParamFrame(AbstractParamFrame): SINGLENAMES = ["Offset", "Multiplier", "Spinspeed"] MULTINAMES = ["Definition1", "Definition2", "Definition3", "Integral", "Lorentz", "Gauss"] EXTRANAMES = ['spinType', 'angle', 'shiftdef', 'cheng', 'numssb'] - MASTYPES = ["Static", "Finite MAS", "Infinite MAS"] DEFTYPES = [u'δ11 - δ22 - δ33', u'δxx - δyy - δzz', @@ -1827,6 +2434,18 @@ class CsaDeconvParamFrame(AbstractParamFrame): "delta_iso - omega - kappa"] def __init__(self, parent, rootwindow, isMain=True): + """ + Initializes the CSA fit parameter frame. + + Parameters + ---------- + parent : FitPlotFrame + The plot frame connected to this parameter frame. + rootwindow : FittingWindow + The fitting tab that holds this parameter frame. + isMain : bool, optional + True if this frame is part of the main tab. + """ self.FITFUNC = simFunc.csaFunc self.fullInt = np.sum(parent.getData1D()) * parent.sw() / float(len(parent.getData1D())) self.DEFAULTS = {"Offset": [0.0, True], "Multiplier": [1.0, True], "Spinspeed": [10.0, True], "Definition1": [0.0, False], "Definition2": [0.0, False], "Definition3": [0.0, False], "Integral": [self.fullInt, False], "Lorentz": [1.0, False], "Gauss": [0.0, True]} @@ -1935,6 +2554,14 @@ def __init__(self, parent, rootwindow, isMain=True): self.reset() def MASChange(self, MAStype): + """ + Change between different MAS types. + + Parameters + ---------- + MAStype : int + The MAS type (0=static, 1=finite MAS, 2=infinite MAS). + """ if MAStype > 0: self.angleLabel.setEnabled(True) self.entries['angle'][-1].setEnabled(True) @@ -1956,6 +2583,9 @@ def MASChange(self, MAStype): self.sidebandLabel.setEnabled(False) def reset(self): + """ + Resets all fit parameters to their default values. + """ self.entries['cheng'][-1].setValue(self.extraDefaults['cheng']) self.entries['shiftdef'][-1].setCurrentIndex(self.extraDefaults['shiftdef']) self.shiftDefType = self.extraDefaults['shiftdef'] @@ -1974,6 +2604,9 @@ def togglePick(self): self.parent.togglePick(self.pickTick.isChecked()) def changeShiftDef(self): + """ + Change between different chemical shift definitions. + """ NewType = self.entries['shiftdef'][-1].currentIndex() OldType = self.shiftDefType if NewType == 0: @@ -2061,6 +2694,9 @@ def changeShiftDef(self): self.shiftDefType = NewType def extraParamToFile(self): + """ + Extra parameters to export. + """ extraDict = {"MAS": self.MASTYPES[self.entries['spinType'][0].currentIndex()], "Definition": self.DEFNAMES[self.entries['shiftdef'][0].currentIndex()], "Cheng": self.entries['cheng'][0].text(), @@ -2069,6 +2705,9 @@ def extraParamToFile(self): return (extraDict, {}) def extraFileToParam(self, preParams, postParams): + """ + Extra parameters to import. + """ keys = preParams.keys() if "MAS" in keys: self.entries['spinType'][0].setCurrentIndex(self.MASTYPES.index(preParams["MAS"])) @@ -2082,6 +2721,9 @@ def extraFileToParam(self, preParams, postParams): self.entries['numssb'][0].setValue(int(preParams["Sidebands"])) def getExtraParams(self, out): + """ + Returns the extra parameters of the fit. + """ shiftdef = self.entries['shiftdef'][0].currentIndex() angle = safeEval(self.entries['angle'][-1].text()) if angle is None: @@ -2094,8 +2736,10 @@ def getExtraParams(self, out): out['extra'] = [shiftdef, numssb, angle, D2, weight, MAStype] return (out, out['extra']) - def checkResults(self,numExp,struc): - #After fit, set lor and gauss absolute + def checkResults(self, numExp, struc): + """ + Sets the Lorentzian and Gaussian broadenings to absolute values. + """ locList = self.getRedLocList() for i in range(numExp): if struc["Lorentz"][i][0] == 1: @@ -2121,16 +2765,27 @@ class QuadDeconvFrame(FitPlotFrame): class QuadDeconvParamFrame(AbstractParamFrame): - FFT_AXES = (0,) + FFT_AXES = (0,) Ioptions = ['1', '3/2', '2', '5/2', '3', '7/2', '4', '9/2'] Ivalues = [1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5] SINGLENAMES = ["Offset", "Multiplier", "Spinspeed"] MULTINAMES = ["Position", "Cq", 'eta', "Integral", "Lorentz", "Gauss"] EXTRANAMES = ['spinType', 'satBool', 'angle', 'cheng', 'I', 'numssb'] - MASTYPES = ["Static", "Finite MAS", "Infinite MAS"] def __init__(self, parent, rootwindow, isMain=True): + """ + Initializes the quadrupole fit parameter frame. + + Parameters + ---------- + parent : FitPlotFrame + The plot frame connected to this parameter frame. + rootwindow : FittingWindow + The fitting tab that holds this parameter frame. + isMain : bool, optional + True if this frame is part of the main tab. + """ self.FITFUNC = simFunc.quadFunc self.fullInt = np.sum(parent.getData1D()) * parent.sw() / float(len(parent.getData1D())) self.DEFAULTS = {"Offset": [0.0, True], "Multiplier": [1.0, True], "Spinspeed": [10.0, True], "Position": [0.0, False], "Cq": [1.0, False], 'eta': [0.0, False], "Integral": [self.fullInt, False], "Lorentz": [1.0, False], "Gauss": [0.0, True]} @@ -2203,6 +2858,14 @@ def __init__(self, parent, rootwindow, isMain=True): self.reset() def MASChange(self, MAStype): + """ + Change between different MAS types. + + Parameters + ---------- + MAStype : int + The MAS type (0=static, 1=finite MAS, 2=infinite MAS). + """ if MAStype > 0: self.angleLabel.setEnabled(True) self.entries['angle'][-1].setEnabled(True) @@ -2224,6 +2887,9 @@ def MASChange(self, MAStype): self.sidebandLabel.setEnabled(False) def reset(self): + """ + Resets all fit parameters to their default values. + """ self.entries['cheng'][-1].setValue(self.extraDefaults['cheng']) self.entries['spinType'][-1].setCurrentIndex(self.extraDefaults['spinType']) self.MASChange(self.extraDefaults['spinType']) @@ -2235,6 +2901,9 @@ def reset(self): super(QuadDeconvParamFrame, self).reset() def extraParamToFile(self): + """ + Extra parameters to export. + """ extraDict = {"I": self.Ioptions[self.entries['I'][-1].currentIndex()], "MAS": self.MASTYPES[self.entries['spinType'][-1].currentIndex()], "Satellites": str(self.entries['satBool'][-1].isChecked()), @@ -2244,6 +2913,9 @@ def extraParamToFile(self): return (extraDict, {}) def extraFileToParam(self, preParams, postParams): + """ + Extra parameters to import. + """ keys = preParams.keys() if "I" in keys: self.entries['I'][0].setCurrentIndex(self.Ioptions.index(preParams["I"])) @@ -2259,6 +2931,9 @@ def extraFileToParam(self, preParams, postParams): self.entries['numssb'][0].setValue(int(preParams["Sidebands"])) def getExtraParams(self, out): + """ + Returns the extra parameters of the fit. + """ satBool = self.entries['satBool'][-1].isChecked() angle = safeEval(self.entries['angle'][-1].text()) if angle is None: @@ -2273,18 +2948,21 @@ def getExtraParams(self, out): out['extra'] = [satBool, I, numssb, angle, D2, D4, weight, MAStype] return (out, out['extra']) - def checkResults(self,numExp,struc): - #After fit, set lor and gauss absolute + def checkResults(self, numExp, struc): + """ + Sets the Lorentzian and Gaussian broadenings to absolute values. + Sets eta between 0 and 1. + Makes Cq positive. + """ locList = self.getRedLocList() for i in range(numExp): - if struc["Lorentz"][i][0] == 1: + if struc["Lorentz"][i][0] == 1: self.fitParamList[locList]["Lorentz"][i][0] = abs(self.fitParamList[locList]["Lorentz"][i][0]) - if struc["Gauss"][i][0] == 1: + if struc["Gauss"][i][0] == 1: self.fitParamList[locList]["Gauss"][i][0] = abs(self.fitParamList[locList]["Gauss"][i][0]) - if struc['eta'][i][0] == 1: - #eta is between 0--1 in a continuous way. + if struc['eta'][i][0] == 1: self.fitParamList[locList]['eta'][i][0] = 1 - abs(abs(self.fitParamList[locList]['eta'][i][0]) % 2 - 1) - if struc["Cq"][i][0] == 1: + if struc["Cq"][i][0] == 1: self.fitParamList[locList]["Cq"][i][0] = abs(self.fitParamList[locList]["Cq"][i][0]) ################################################################################# @@ -2292,13 +2970,12 @@ def checkResults(self,numExp,struc): class QuadCSADeconvParamFrame(AbstractParamFrame): - FFT_AXES = (0,) + FFT_AXES = (0,) Ioptions = ['1/2', '1', '3/2', '2', '5/2', '3', '7/2', '4', '9/2'] Ivalues = [0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5] SINGLENAMES = ["Offset", "Multiplier", "Spinspeed"] MULTINAMES = ["Definition1", "Definition2", "Definition3", "Cq", 'eta', "Alpha", "Beta", "Gamma", "Integral", "Lorentz", "Gauss"] EXTRANAMES = ['spinType', 'satBool', 'angle', 'shiftdef', 'cheng', 'I', 'numssb'] - MASTYPES = ["Static", "Finite MAS", "Infinite MAS"] DEFTYPES = [u'δ11 - δ22 - δ33', u'δxx - δyy - δzz', @@ -2310,9 +2987,25 @@ class QuadCSADeconvParamFrame(AbstractParamFrame): "delta_iso - omega - kappa"] def __init__(self, parent, rootwindow, isMain=True): + """ + Initializes the quadrupole+CSA fit parameter frame. + + Parameters + ---------- + parent : FitPlotFrame + The plot frame connected to this parameter frame. + rootwindow : FittingWindow + The fitting tab that holds this parameter frame. + isMain : bool, optional + True if this frame is part of the main tab. + """ self.FITFUNC = simFunc.quadCSAFunc self.fullInt = np.sum(parent.getData1D()) * parent.sw() / float(len(parent.getData1D())) - self.DEFAULTS = {"Offset": [0.0, True], "Multiplier": [1.0, True], "Spinspeed": [10.0, True], "Definition1": [0.0, False], "Definition2": [0.0, False], "Definition3": [0.0, False], "Cq": [1.0, False], 'eta': [0.0, False], "Integral": [self.fullInt, False], "Lorentz": [1.0, False], "Gauss": [0.0, True], "Alpha": [0.0, True], "Beta": [0.0, True], "Gamma": [0.0, True]} + self.DEFAULTS = {"Offset": [0.0, True], "Multiplier": [1.0, True], "Spinspeed": [10.0, True], + "Definition1": [0.0, False], "Definition2": [0.0, False], "Definition3": [0.0, False], + "Cq": [1.0, False], 'eta': [0.0, False], "Integral": [self.fullInt, False], + "Lorentz": [1.0, False], "Gauss": [0.0, True], "Alpha": [0.0, True], + "Beta": [0.0, True], "Gamma": [0.0, True]} self.extraDefaults = {'I': 2, 'Satellites': False, 'cheng': 15, 'spinType': 0, 'shiftdef': 0, 'rotorAngle': "arctan(sqrt(2))", 'numssb': 32, "Spinspeed": '10.0'} super(QuadCSADeconvParamFrame, self).__init__(parent, rootwindow, isMain) self.optframe.addWidget(wc.QLabel("MAS:"), 2, 0) @@ -2426,6 +3119,14 @@ def __init__(self, parent, rootwindow, isMain=True): self.reset() def MASChange(self, MAStype): + """ + Change between different MAS types. + + Parameters + ---------- + MAStype : int + The MAS type (0=static, 1=finite MAS, 2=infinite MAS). + """ if MAStype > 0: self.angleLabel.setEnabled(True) self.entries['angle'][-1].setEnabled(True) @@ -2447,6 +3148,9 @@ def MASChange(self, MAStype): self.sidebandLabel.setEnabled(False) def reset(self): + """ + Resets all fit parameters to their default values. + """ self.entries['cheng'][-1].setValue(self.extraDefaults['cheng']) self.entries['shiftdef'][-1].setCurrentIndex(self.extraDefaults['shiftdef']) self.shiftDefType = self.extraDefaults['shiftdef'] @@ -2460,6 +3164,9 @@ def reset(self): super(QuadCSADeconvParamFrame, self).reset() def changeShiftDef(self): + """ + Change between different chemical shift definitions. + """ NewType = self.entries['shiftdef'][-1].currentIndex() OldType = self.shiftDefType if NewType == 0: @@ -2539,6 +3246,9 @@ def changeShiftDef(self): self.shiftDefType = NewType def extraParamToFile(self): + """ + Extra parameters to export. + """ extraDict = {"I": self.Ioptions[self.entries['I'][-1].currentIndex()], "Definition": self.DEFNAMES[self.entries['shiftdef'][0].currentIndex()], "MAS": self.MASTYPES[self.entries['spinType'][-1].currentIndex()], @@ -2549,6 +3259,9 @@ def extraParamToFile(self): return (extraDict, {}) def extraFileToParam(self, preParams, postParams): + """ + Extra parameters to import. + """ keys = preParams.keys() if "I" in keys: self.entries['I'][0].setCurrentIndex(self.Ioptions.index(preParams["I"])) @@ -2566,6 +3279,9 @@ def extraFileToParam(self, preParams, postParams): self.entries['numssb'][0].setValue(int(preParams["Sidebands"])) def getExtraParams(self, out): + """ + Returns the extra parameters of the fit. + """ shiftdef = self.entries['shiftdef'][0].currentIndex() satBool = self.entries['satBool'][-1].isChecked() angle = safeEval(self.entries['angle'][-1].text()) @@ -2581,8 +3297,10 @@ def getExtraParams(self, out): out['extra'] = [satBool, I, numssb, angle, D2, D4, weight, MAStype, shiftdef] return (out, out['extra']) - def checkResults(self,numExp,struc): - #After fit, set lor and gauss absolute + def checkResults(self, numExp, struc): + """ + Fixes the fit results. + """ locList = self.getRedLocList() for i in range(numExp): if struc["Lorentz"][i][0] == 1: @@ -2616,11 +3334,25 @@ def checkResults(self,numExp,struc): class CzjzekPrefWindow(QtWidgets.QWidget): + """ + The window with Czjzek distribution settings. + """ Ioptions = ['1', '3/2', '2', '5/2', '3', '7/2', '4', '9/2'] MASTYPES = ["Static", "Finite MAS", "Infinite MAS"] def __init__(self, parent, mqmas=False): + """ + Initializes the Czjzek preference window. + + Parameters + ---------- + parent : QuadCzjzekParamFrame, MqmasCzjzekParamFrame + The Czjzek parameter frame. + mqmas : bool, optional + True if the fit is of an MQMAS spectrum. + By default False. + """ super(CzjzekPrefWindow, self).__init__(parent) self.setWindowFlags(QtCore.Qt.Window | QtCore.Qt.Tool) self.father = parent @@ -2645,13 +3377,13 @@ def __init__(self, parent, mqmas=False): self.cqsteps = QtWidgets.QSpinBox() self.cqsteps.setMinimum(2) self.cqsteps.setMaximum(1000) - self.cqsteps.setAlignment(QtCore.Qt.AlignHCenter) + self.cqsteps.setAlignment(QtCore.Qt.AlignHCenter) grid.addWidget(self.cqsteps, 3, 0) grid.addWidget(wc.QLabel(u"η grid size:"), 2, 1) self.etasteps = QtWidgets.QSpinBox() self.etasteps.setMinimum(2) self.etasteps.setMaximum(1000) - self.etasteps.setAlignment(QtCore.Qt.AlignHCenter) + self.etasteps.setAlignment(QtCore.Qt.AlignHCenter) grid.addWidget(self.etasteps, 3, 1) grid.addWidget(wc.QLabel(u"CQ limits [MHz]:"), 4, 0, 1, 2) self.cqmin = wc.QLineEdit(str(self.father.cqmin), self.checkCq) @@ -2706,7 +3438,7 @@ def __init__(self, parent, mqmas=False): self.site = QtWidgets.QSpinBox() self.site.setMinimum(1) self.site.setMaximum(self.father.numExp.currentIndex() + 1) - self.site.setAlignment(QtCore.Qt.AlignHCenter) + self.site.setAlignment(QtCore.Qt.AlignHCenter) grid.addWidget(self.site, 14, 3) self.libLabel = wc.QLabel("") layout.addWidget(self.libLabel, 4, 3) @@ -2732,6 +3464,14 @@ def __init__(self, parent, mqmas=False): self.resize(800, 600) def MASChange(self, MAStype): + """ + Change between different MAS types. + + Parameters + ---------- + MAStype : int + The MAS type (0=static, 1=finite MAS, 2=infinite MAS). + """ if self.mqmas: return if MAStype > 0: @@ -2750,12 +3490,15 @@ def MASChange(self, MAStype): self.spinLabel.setEnabled(False) self.numssbEntry.setEnabled(False) self.sidebandLabel.setEnabled(False) - + def upd(self): + """ + Update the boxes of the grid values. + """ if self.mqmas: self.Ientry.setCurrentIndex(int(self.father.I-1.5)) else: - self.Ientry.setCurrentIndex(int(self.father.I*2.0-2.0)) + self.Ientry.setCurrentIndex(int(self.father.I*2.0-2.0)) self.chengEntry.setValue(self.father.cheng) if not self.mqmas: self.masEntry.setCurrentIndex(self.father.mas) @@ -2772,22 +3515,25 @@ def upd(self): self.libLabel.setText("Library: " + self.father.libName) def plotDist(self): + """ + Plot the Czjzek distribution. + """ self.ax.cla() cqsteps = self.cqsteps.value() etasteps = self.etasteps.value() - cqmax = safeEval(self.cqmax.text(), type='FI') - cqmin = safeEval(self.cqmin.text(), type='FI') - etamax = safeEval(self.etamax.text(), type='FI') - etamin = safeEval(self.etamin.text(), type='FI') + cqmax = safeEval(self.cqmax.text(), Type='FI') + cqmin = safeEval(self.cqmin.text(), Type='FI') + etamax = safeEval(self.etamax.text(), Type='FI') + etamin = safeEval(self.etamin.text(), Type='FI') cqArray = np.linspace(cqmin, cqmax, cqsteps) etaArray = np.linspace(etamin, etamax, etasteps) cq, eta = np.meshgrid(cqArray, etaArray) method = self.father.entries['method'][0].currentIndex() d = self.father.entries['d'][0].currentIndex() + 1 site = self.site.value() - 1 - sigma = safeEval(self.father.entries['Sigma'][site].text(), type='FI') - cq0 = safeEval(self.father.entries['Cq0'][site].text(), type='FI') - eta0 = safeEval(self.father.entries['eta0'][site].text(), type='FI') + sigma = safeEval(self.father.entries['Sigma'][site].text(), Type='FI') + cq0 = safeEval(self.father.entries['Cq0'][site].text(), Type='FI') + eta0 = safeEval(self.father.entries['eta0'][site].text(), Type='FI') if method == 0: self.czjzek = Czjzek.czjzekIntensities(sigma, d, cq.flatten(), eta.flatten()) else: @@ -2799,6 +3545,9 @@ def plotDist(self): self.canvas.draw() def saveDist(self): + """ + Save the Czjzek distribution values to an ASCII file. + """ name = QtWidgets.QFileDialog.getSaveFileName(self, 'Save Distribution', self.father.rootwindow.father.lastLocation + os.path.sep + 'czjzek.txt', 'ASCII file (*.txt)') if isinstance(name, tuple): name = name[0] @@ -2807,32 +3556,38 @@ def saveDist(self): self.plotDist() cqsteps = self.cqsteps.value() etasteps = self.etasteps.value() - cqmax = safeEval(self.cqmax.text(), type='FI') - cqmin = safeEval(self.cqmin.text(), type='FI') - etamax = safeEval(self.etamax.text(), type='FI') - etamin = safeEval(self.etamin.text(), type='FI') + cqmax = safeEval(self.cqmax.text(), Type='FI') + cqmin = safeEval(self.cqmin.text(), Type='FI') + etamax = safeEval(self.etamax.text(), Type='FI') + etamin = safeEval(self.etamin.text(), Type='FI') header = "Cq_min=" + str(cqmin) + "\nCq_max=" + str(cqmax) + "\nCq_steps=" + str(cqsteps) + "eta_min=" + str(etamin) + "\neta_max=" + str(etamax) + "\neta_steps=" + str(etasteps) np.savetxt(name, self.czjzek, header=header) def checkCq(self): - inp = safeEval(self.cqmax.text(), type='FI') + """ + Check the input values for Cq. + """ + inp = safeEval(self.cqmax.text(), Type='FI') if inp is None: return False self.cqmax.setText(str(float(inp))) - inp = safeEval(self.cqmin.text(), type='FI') + inp = safeEval(self.cqmin.text(), Type='FI') if inp is None: return False self.cqmin.setText(str(float(inp))) return True def checkEta(self): - inp = safeEval(self.etamax.text(), type='FI') + """ + Check the input values for eta. + """ + inp = safeEval(self.etamax.text(), Type='FI') if inp is None: return False if inp < 0.0 or inp > 1.0: return False self.etamax.setText(str(abs(float(inp)))) - inp = safeEval(self.etamin.text(), type='FI') + inp = safeEval(self.etamin.text(), Type='FI') if inp is None: return False if inp < 0.0 or inp > 1.0: @@ -2840,9 +3595,15 @@ def checkEta(self): self.etamin.setText(str(abs(float(inp)))) def closeEvent(self, *args): + """ + Closes the Czjzek window. + """ self.deleteLater() def generate(self, *args): + """ + Generate the Czjzek library. + """ self.busyButton.show() self.loadButton.setEnabled(False) self.cancelButton.setEnabled(False) @@ -2854,7 +3615,7 @@ def generate(self, *args): if not self.mqmas: self.father.I = self.Ientry.currentIndex() * 0.5 + 1.0 self.father.mas = self.masEntry.currentIndex() - inp = safeEval(self.spinEntry.text(), type='FI') + inp = safeEval(self.spinEntry.text(), Type='FI') if inp is None: raise FittingException("Spin speed value not valid.") self.father.spinspeed = inp @@ -2863,22 +3624,22 @@ def generate(self, *args): self.father.satBool = self.satBoolEntry.isChecked() else: self.father.I = self.Ientry.currentIndex() + 1.5 - inp = safeEval(self.cqmax.text(), type='FI') + inp = safeEval(self.cqmax.text(), Type='FI') if inp is None: raise FittingException(u"C_Q_max value not valid.") self.father.cqmax = abs(safeEval(self.cqmax.text())) - inp = abs(safeEval(self.cqmin.text(), type='FI')) + inp = abs(safeEval(self.cqmin.text(), Type='FI')) if inp is None: raise FittingException(u"C_Q_min value not valid.") self.father.cqmin = abs(safeEval(self.cqmin.text())) #eta - inp = safeEval(self.etamax.text(), type='FI') + inp = safeEval(self.etamax.text(), Type='FI') if inp is None: raise FittingException(u"η_max value not valid.") if inp < 0.0 or inp > 1.0: raise FittingException(u"η_max value not valid.") self.father.etamax = abs(float(inp)) - inp = safeEval(self.etamin.text(), type='FI') + inp = safeEval(self.etamin.text(), Type='FI') if inp is None: raise FittingException(u"η_min value not valid.") if inp < 0.0 or inp > 1.0: @@ -2886,7 +3647,7 @@ def generate(self, *args): self.father.etamin = abs(float(inp)) self.father.libName = "Generated" self.father.simLib() - except: + except Exception: raise finally: self.busyButton.hide() @@ -2895,6 +3656,9 @@ def generate(self, *args): self.upd() def loadLib(self, *args): + """ + Load the Czjzek library from a set of files. + """ fileName = self.father.rootwindow.father.loadFitLibDir() if not fileName: return @@ -2952,10 +3716,21 @@ class QuadCzjzekParamFrame(AbstractParamFrame): SINGLENAMES = ["Offset", "Multiplier"] MULTINAMES = ["Position", "Sigma", "Cq0", 'eta0', "Integral", "Lorentz", "Gauss"] EXTRANAMES = ['method', 'd'] - TYPES = ['Normal', 'Extended'] - + def __init__(self, parent, rootwindow, isMain=True): + """ + Initializes the Czjzek fit parameter frame. + + Parameters + ---------- + parent : FitPlotFrame + The plot frame connected to this parameter frame. + rootwindow : FittingWindow + The fitting tab that holds this parameter frame. + isMain : bool, optional + True if this frame is part of the main tab. + """ self.FITFUNC = simFunc.quadCzjzekFunc self.fullInt = np.sum(parent.getData1D()) * parent.sw() / float(len(parent.getData1D())) self.DEFAULTS = {"Offset": [0.0, True], "Multiplier": [1.0, True], "Position": [0.0, False], "Sigma": [1.0, False], "Cq0": [0.0, True], 'eta0': [0.0, True], "Integral": [self.fullInt, False], "Lorentz": [10.0, False], "Gauss": [0.0, True]} @@ -2985,7 +3760,7 @@ def __init__(self, parent, rootwindow, isMain=True): self.optframe.addWidget(self.entries['method'][0], 1, 0) self.optframe.addWidget(wc.QLabel("d:"), 2, 0) self.entries['d'].append(QtWidgets.QComboBox()) - self.entries['d'][0].addItems(['1', '2','3','4','5']) + self.entries['d'][0].addItems(['1', '2', '3', '4', '5']) self.optframe.addWidget(self.entries['d'][0], 3, 0) self.numExp = QtWidgets.QComboBox() self.numExp.addItems([str(x + 1) for x in range(self.FITNUM)]) @@ -3011,6 +3786,9 @@ def __init__(self, parent, rootwindow, isMain=True): self.reset() def reset(self): + """ + Resets all fit parameters to their default values. + """ self.cqsteps = self.extraDefaults['cqsteps'] self.etasteps = self.extraDefaults['etasteps'] self.cqmax = self.extraDefaults['cqmax'] @@ -3034,8 +3812,16 @@ def reset(self): super(QuadCzjzekParamFrame, self).reset() def changeType(self, index): + """ + Enables or disables the extended Czjzek fitting. + + Parameters + ---------- + index : int + Czjzek fitting type (0=regular, 1=extended). + """ if index == 0: - for i in range(self.FITNUM): + for i in range(self.FITNUM): self.entries["Cq0"][i].setEnabled(False) self.entries['eta0'][i].setEnabled(False) self.ticks["Cq0"][i].setChecked(True) @@ -3043,7 +3829,7 @@ def changeType(self, index): self.ticks["Cq0"][i].setEnabled(False) self.ticks['eta0'][i].setEnabled(False) elif index == 1: - for i in range(self.FITNUM): + for i in range(self.FITNUM): self.entries["Cq0"][i].setEnabled(True) self.entries['eta0'][i].setEnabled(True) self.ticks["Cq0"][i].setEnabled(True) @@ -3053,7 +3839,10 @@ def createCzjzekPrefWindow(self, *args): CzjzekPrefWindow(self) def simLib(self): - angle = safeEval(self.angle, type='FI') + """ + Simulate the spectra for the Czjzek library. + """ + angle = safeEval(self.angle, Type='FI') alpha, beta, weight = simFunc.zcw_angles(self.cheng, 2) D2 = simFunc.D2tens(alpha, beta, np.zeros_like(alpha)) D4 = simFunc.D4tens(alpha, beta, np.zeros_like(alpha)) @@ -3061,6 +3850,9 @@ def simLib(self): self.lib, self.cqLib, self.etaLib = simFunc.genLib(len(self.parent.xax()), self.cqmin, self.cqmax, self.etamin, self.etamax, self.cqsteps, self.etasteps, extra, self.parent.freq(), self.parent.sw(), self.spinspeed) def extraParamToFile(self): + """ + Extra parameters to export. + """ extraDict = {"Method": self.TYPES[self.entries['method'][-1].currentIndex()], "d": str(self.entries['d'][0].currentIndex() + 1), "I": CzjzekPrefWindow.Ioptions[int(self.I*2.0-2.0)], @@ -3080,6 +3872,9 @@ def extraParamToFile(self): return (extraDict, {}) def extraFileToParam(self, preParams, postParams): + """ + Extra parameters to import. + """ keys = preParams.keys() if "Method" in keys: self.entries['method'][0].setCurrentIndex(self.TYPES.index(preParams["Method"])) @@ -3113,6 +3908,9 @@ def extraFileToParam(self, preParams, postParams): self.numssb = int(preParams["Sidebands"]) def getExtraParams(self, out): + """ + Returns the extra parameters of the fit. + """ if self.lib is None: raise FittingException("No library available") method = self.entries['method'][0].currentIndex() @@ -3120,19 +3918,21 @@ def getExtraParams(self, out): out['extra'] = [method, d, self.lib, self.cqLib, self.etaLib] return (out, out['extra']) - def checkResults(self,numExp,struc): - #After fit, set lor and gauss absolute + def checkResults(self, numExp, struc): + """ + Fixes the fit results. + """ locList = self.getRedLocList() for i in range(numExp): - if struc["Lorentz"][i][0] == 1: + if struc["Lorentz"][i][0] == 1: self.fitParamList[locList]["Lorentz"][i][0] = abs(self.fitParamList[locList]["Lorentz"][i][0]) - if struc["Gauss"][i][0] == 1: + if struc["Gauss"][i][0] == 1: self.fitParamList[locList]["Gauss"][i][0] = abs(self.fitParamList[locList]["Gauss"][i][0]) - if struc["Sigma"][i][0] == 1: + if struc["Sigma"][i][0] == 1: self.fitParamList[locList]["Sigma"][i][0] = abs(self.fitParamList[locList]["Sigma"][i][0]) - if struc["Cq0"][i][0] == 1: + if struc["Cq0"][i][0] == 1: self.fitParamList[locList]["Cq0"][i][0] = abs(self.fitParamList[locList]["Cq0"][i][0]) - if struc['eta0'][i][0] == 1: + if struc['eta0'][i][0] == 1: self.fitParamList[locList]['eta0'][i][0] = 1 - abs(abs(self.fitParamList[locList]['eta0'][i][0])%2 - 1) ################################################################################# @@ -3149,8 +3949,20 @@ class ExternalFitDeconvParamFrame(AbstractParamFrame): SINGLENAMES = [] MULTINAMES = [] - + def __init__(self, parent, rootwindow, isMain=True): + """ + Initializes the external fit parameter frame. + + Parameters + ---------- + parent : FitPlotFrame + The plot frame connected to this parameter frame. + rootwindow : FittingWindow + The fitting tab that holds this parameter frame. + isMain : bool, optional + True if this frame is part of the main tab. + """ self.FITFUNC = simFunc.externalFitRunScript self.resetDefaults() super(ExternalFitDeconvParamFrame, self).__init__(parent, rootwindow, isMain) @@ -3175,14 +3987,17 @@ def __init__(self, parent, rootwindow, isMain=True): def resetDefaults(self): self.DEFAULTS = {"Offset": [0.0, True], "Multiplier": [1.0, True], "Integral": [1.0, False], "Lorentz": [10.0, False], "Gauss": [0.0, True]} - + def txtOutputWindow(self): TxtOutputWindow(self.rootwindow, self.txtOutput[0], self.txtOutput[1]) def loadScript(self): + """ + Asks the user for a script file and analyses it. + """ self.resetDefaults() fileName = self.rootwindow.father.loadSIMPSONScript() - if len(fileName) == 0: + if not fileName: return try: with open(fileName, "r") as myfile: @@ -3192,6 +4007,14 @@ def loadScript(self): self.analyseScript(inFile) def analyseScript(self, inFile): + """ + Analyses a given script for external fitting. + + Parameters + ---------- + inFile : str + Script to analyse. + """ matches = np.unique(re.findall("(@\w+@)", inFile)) self.script = inFile for n in self.SINGLENAMES+self.MULTINAMES: @@ -3231,32 +4054,43 @@ def analyseScript(self, inFile): self.reset() def extraParamToFile(self): + """ + Extra parameters to export. + """ extraDict = {"Command": self.commandLine.text()} return (extraDict, {"Script": self.script}) def extraFileToParam(self, preParams, postParams): + """ + Extra parameters to import. + """ if "Script" in postParams.keys(): self.analyseScript(postParams["Script"]) if "Command" in preParams.keys(): self.commandLine.setText(preParams["Command"]) def getExtraParams(self, out): + """ + Returns the extra parameters of the fit. + """ out['extra'] = [self.MULTINAMES, self.commandLine.text(), self.script, self.txtOutput, self.parent.spec()] return (out, out['extra']) - def checkResults(self,numExp,struc): - #After fit, set lor and gauss absolute + def checkResults(self, numExp, struc): + """ + Fixes the fit results. + """ locList = self.getRedLocList() for i in range(numExp): - if struc["Lorentz"][i][0] == 1: + if struc["Lorentz"][i][0] == 1: self.fitParamList[locList]["Lorentz"][i][0] = abs(self.fitParamList[locList]["Lorentz"][i][0]) - if struc["Gauss"][i][0] == 1: + if struc["Gauss"][i][0] == 1: self.fitParamList[locList]["Gauss"][i][0] = abs(self.fitParamList[locList]["Gauss"][i][0]) ############################################################################## -class TxtOutputWindow(wc.ToolWindows): +class TxtOutputWindow(wc.ToolWindow): NAME = "Script Output" RESIZABLE = True @@ -3293,8 +4127,20 @@ class FunctionFitParamFrame(AbstractParamFrame): SINGLENAMES = [] MULTINAMES = [] - + def __init__(self, parent, rootwindow, isMain=True): + """ + Initializes the function fit parameter frame. + + Parameters + ---------- + parent : FitPlotFrame + The plot frame connected to this parameter frame. + rootwindow : FittingWindow + The fitting tab that holds this parameter frame. + isMain : bool, optional + True if this frame is part of the main tab. + """ self.FITFUNC = simFunc.functionRun self.numExp = QtWidgets.QComboBox() self.function = "" @@ -3312,11 +4158,14 @@ def __init__(self, parent, rootwindow, isMain=True): def resetDefaults(self): self.DEFAULTS = {} - + def functionInput(self): FunctionInputWindow(self, self.function) def functionInputSetup(self): + """ + Interprets the input function and makes labels and entries. + """ self.resetDefaults() matches = np.unique(re.findall("(@\w+@)", self.function)) for n in self.SINGLENAMES+self.MULTINAMES: @@ -3344,15 +4193,24 @@ def functionInputSetup(self): self.reset() def extraParamToFile(self): + """ + Extra parameters to export. + """ extraDict = {"Function": self.function} return (extraDict, {}) def extraFileToParam(self, preParams, postParams): + """ + Extra parameters to import. + """ if "Function" in preParams.keys(): self.function = preParams["Function"] self.functionInputSetup() def getExtraParams(self, out): + """ + Returns the extra parameters of the fit. + """ if self.function == "": raise FittingException("Fitting: No function defined") out["extra"] = [self.MULTINAMES, self.function] @@ -3361,7 +4219,7 @@ def getExtraParams(self, out): ############################################################################## -class FunctionInputWindow(wc.ToolWindows): +class FunctionInputWindow(wc.ToolWindow): NAME = "Fitting Function" RESIZABLE = True @@ -3387,8 +4245,25 @@ def closeEvent(self, *args): class FitContourFrame(CurrentContour, FitPlotFrame): + """ + The frame to plot contour spectra during fitting. + """ def __init__(self, rootwindow, fig, canvas, current): + """ + Initializes the fitting contour plot window. + + Parameters + ---------- + rootwindow : FittingWindow + The window that contains the figure. + fig : Figure + The figure used in this frame. + canvas : FigureCanvas + The canvas of fig. + current : PlotFrame + The view of the original workspace. + """ self.data = current.data tmp = np.array(current.data.shape(), dtype=int) tmp = np.delete(tmp, self.fixAxes(current.axes)) @@ -3398,6 +4273,9 @@ def __init__(self, rootwindow, fig, canvas, current): CurrentContour.__init__(self, rootwindow, fig, canvas, current.data, current) def showFid(self): + """ + Displays the plot and fit curves. + """ extraX = [] extraY = [] extraZ = [] @@ -3428,21 +4306,35 @@ class MqmasDeconvFrame(FitContourFrame): class MqmasDeconvParamFrame(AbstractParamFrame): - FFT_AXES = (0,1) # Which axes should be transformed after simulation + FFT_AXES = (0, 1) # Which axes should be transformed after simulation DIM = 2 Ioptions = ['3/2', '5/2', '7/2', '9/2'] Ivalues = [1.5, 2.5, 3.5, 4.5] MQvalues = [3, 5, 7, 9] SINGLENAMES = ["Offset", "Multiplier", "Spinspeed"] MULTINAMES = ["Position", "Cq", 'eta', "Integral", "Lorentz2", "Gauss2", "Lorentz1", "Gauss1"] - EXTRANAMES = ['spinType', 'angle', 'numssb','cheng', 'I', 'MQ', 'shear', 'scale'] - + EXTRANAMES = ['spinType', 'angle', 'numssb', 'cheng', 'I', 'MQ', 'shear', 'scale'] MASTYPES = ["Static", "Finite MAS", "Infinite MAS"] - + def __init__(self, parent, rootwindow, isMain=True): + """ + Initializes the MQMAS fit parameter frame. + + Parameters + ---------- + parent : FitPlotFrame + The plot frame connected to this parameter frame. + rootwindow : FittingWindow + The fitting tab that holds this parameter frame. + isMain : bool, optional + True if this frame is part of the main tab. + """ self.FITFUNC = simFunc.mqmasFunc self.fullInt = np.sum(parent.getData1D()) * parent.sw() / float(parent.getData1D().shape[-1]) * parent.sw(-2) / float(parent.getData1D().shape[-2]) - self.DEFAULTS = {"Offset": [0.0, True], "Multiplier": [1.0, True], "Spinspeed": [10.0, True], "Position": [0.0, False], "Cq": [1.0, False], 'eta': [0.0, False], "Integral": [self.fullInt, False], "Lorentz2": [10.0, False], "Gauss2": [0.0, True], "Lorentz1": [10.0, False], "Gauss1": [0.0, True]} + self.DEFAULTS = {"Offset": [0.0, True], "Multiplier": [1.0, True], "Spinspeed": [10.0, True], + "Position": [0.0, False], "Cq": [1.0, False], 'eta': [0.0, False], + "Integral": [self.fullInt, False], "Lorentz2": [10.0, False], "Gauss2": [0.0, True], + "Lorentz1": [10.0, False], "Gauss1": [0.0, True]} self.extraDefaults = {'spinType': 2, 'angle': "arctan(sqrt(2))", 'numssb': 32, 'cheng': 15, 'I': 0, 'MQ': 0, 'shear': '0.0', 'scale': '1.0'} super(MqmasDeconvParamFrame, self).__init__(parent, rootwindow, isMain) self.optframe.addWidget(wc.QLabel("MAS:"), 2, 0) @@ -3525,6 +4417,9 @@ def __init__(self, parent, rootwindow, isMain=True): self.reset() def reset(self): + """ + Resets all fit parameters to their default values. + """ self.entries['angle'][-1].setText(self.extraDefaults['angle']) self.entries['spinType'][-1].setCurrentIndex(self.extraDefaults['spinType']) self.entries['numssb'][-1].setValue(self.extraDefaults['numssb']) @@ -3537,19 +4432,30 @@ def reset(self): super(MqmasDeconvParamFrame, self).reset() def autoShearScale(self, *args): - from fractions import gcd + """ + Calculates the auto shearing values. + """ + from math import gcd I = self.entries['I'][-1].currentIndex() + 3/2.0 mq = self.MQvalues[self.entries['MQ'][-1].currentIndex()] m = 0.5 * mq numerator = m * (18 * I * (I + 1) - 34 * m**2 - 5) denomenator = 0.5 * (18 * I * (I + 1) - 34 * 0.5**2 - 5) - divis = gcd(numerator, denomenator) + divis = gcd(int(numerator), int(denomenator)) numerator /= divis denomenator /= divis self.entries['shear'][-1].setText(str(numerator) + '/' + str(denomenator)) self.entries['scale'][-1].setText(str(denomenator) + '/' + str(mq * denomenator - numerator)) - + def MASChange(self, MAStype): + """ + Change between different MAS types. + + Parameters + ---------- + MAStype : int + The MAS type (0=static, 1=finite MAS, 2=infinite MAS). + """ if MAStype > 0: self.angleLabel.setEnabled(True) self.entries['angle'][-1].setEnabled(True) @@ -3569,8 +4475,11 @@ def MASChange(self, MAStype): self.spinLabel.setEnabled(False) self.entries['numssb'][-1].setEnabled(False) self.sidebandLabel.setEnabled(False) - + def extraParamToFile(self): + """ + Extra parameters to export. + """ extraDict = {"I": self.Ioptions[self.entries['I'][-1].currentIndex()], "MQ": str(self.MQvalues[self.entries['MQ'][-1].currentIndex()]), "Shear": self.entries['shear'][-1].text(), @@ -3582,6 +4491,9 @@ def extraParamToFile(self): return (extraDict, {}) def extraFileToParam(self, preParams, postParams): + """ + Extra parameters to import. + """ keys = preParams.keys() if "I" in keys: self.entries['I'][0].setCurrentIndex(self.Ioptions.index(preParams["I"])) @@ -3601,6 +4513,9 @@ def extraFileToParam(self, preParams, postParams): self.entries['numssb'][0].setValue(int(preParams["Sidebands"])) def getExtraParams(self, out): + """ + Returns the extra parameters of the fit. + """ angle = safeEval(self.entries['angle'][-1].text()) if angle is None: raise FittingException("Fitting: Rotoe Angle is not valid") @@ -3619,22 +4534,25 @@ def getExtraParams(self, out): out['extra'] = [I, MQ, numssb, angle, D2, D4, weight, shear, scale, MAStype] return (out, out['extra']) - def checkResults(self,numExp,struc): - #After fit, set lor and gauss absolute + def checkResults(self, numExp, struc): + """ + Sets the Lorentzian and Gaussian broadenings to absolute values. + Sets eta between 0 and 1. + Makes Cq positive. + """ locList = self.getRedLocList() for i in range(numExp): - if struc["Lorentz1"][i][0] == 1: + if struc["Lorentz1"][i][0] == 1: self.fitParamList[locList]["Lorentz1"][i][0] = abs(self.fitParamList[locList]["Lorentz1"][i][0]) - if struc["Gauss1"][i][0] == 1: + if struc["Gauss1"][i][0] == 1: self.fitParamList[locList]["Gauss1"][i][0] = abs(self.fitParamList[locList]["Gauss1"][i][0]) - if struc["Lorentz2"][i][0] == 1: + if struc["Lorentz2"][i][0] == 1: self.fitParamList[locList]["Lorentz2"][i][0] = abs(self.fitParamList[locList]["Lorentz2"][i][0]) - if struc["Gauss2"][i][0] == 1: + if struc["Gauss2"][i][0] == 1: self.fitParamList[locList]["Gauss2"][i][0] = abs(self.fitParamList[locList]["Gauss2"][i][0]) - if struc['eta'][i][0] == 1: - #eta is between 0--1 in a continuous way. + if struc['eta'][i][0] == 1: self.fitParamList[locList]['eta'][i][0] = 1 - abs(abs(self.fitParamList[locList]['eta'][i][0]) % 2 - 1) - if struc["Cq"][i][0] == 1: + if struc["Cq"][i][0] == 1: self.fitParamList[locList]["Cq"][i][0] = abs(self.fitParamList[locList]["Cq"][i][0]) ############################################################################## @@ -3649,13 +4567,27 @@ class MqmasCzjzekParamFrame(AbstractParamFrame): SINGLENAMES = ["Offset", "Multiplier"] MULTINAMES = ["Position", "Sigma", 'SigmaCS', "Cq0", 'eta0', "Integral", "Lorentz2", "Gauss2", "Lorentz1", "Gauss1"] EXTRANAMES = ['method', 'd', 'MQ', 'shear', 'scale'] - TYPES = ['Normal', 'Extended'] def __init__(self, parent, rootwindow, isMain=True): + """ + Initializes the Czjzek MQMAS fit parameter frame. + + Parameters + ---------- + parent : FitPlotFrame + The plot frame connected to this parameter frame. + rootwindow : FittingWindow + The fitting tab that holds this parameter frame. + isMain : bool, optional + True if this frame is part of the main tab. + """ self.FITFUNC = simFunc.mqmasCzjzekFunc self.fullInt = np.sum(parent.getData1D()) * parent.sw() / float(parent.getData1D().shape[-1]) * parent.sw(-2) / float(parent.getData1D().shape[-2]) - self.DEFAULTS = {"Offset": [0.0, True], "Multiplier": [1.0, True], "Position": [0.0, False], "Sigma": [1.0, False], 'SigmaCS': [10.0, False], "Cq0": [0.0, True], 'eta0': [0.0, True], "Integral": [self.fullInt, False], "Lorentz2": [10.0, False], "Gauss2": [0.0, True], "Lorentz1": [10.0, False], "Gauss1": [0.0, True]} + self.DEFAULTS = {"Offset": [0.0, True], "Multiplier": [1.0, True], "Position": [0.0, False], + "Sigma": [1.0, False], 'SigmaCS': [10.0, False], "Cq0": [0.0, True], + 'eta0': [0.0, True], "Integral": [self.fullInt, False], "Lorentz2": [10.0, False], + "Gauss2": [0.0, True], "Lorentz1": [10.0, False], "Gauss1": [0.0, True]} self.extraDefaults = {'mas': 2, 'method': 0, 'd': 5, 'cheng': 15, 'I': 3/2.0, 'MQ': 0, 'shear': '0.0', 'scale': '1.0', 'cqsteps': 50, 'etasteps': 10, 'cqmax': 4, 'cqmin': 0, 'etamax': 1, 'etamin': 0, 'libName': "Not available", 'lib': None, 'cqLib': None, 'etaLib': None} super(MqmasCzjzekParamFrame, self).__init__(parent, rootwindow, isMain) @@ -3723,6 +4655,9 @@ def __init__(self, parent, rootwindow, isMain=True): self.reset() def reset(self): + """ + Resets all fit parameters to their default values. + """ self.cqsteps = self.extraDefaults['cqsteps'] self.etasteps = self.extraDefaults['etasteps'] self.cqmax = self.extraDefaults['cqmax'] @@ -3745,20 +4680,31 @@ def reset(self): super(MqmasCzjzekParamFrame, self).reset() def autoShearScale(self, *args): - from fractions import gcd + """ + Calculates the auto shearing values. + """ + from math import gcd mq = self.MQvalues[self.entries['MQ'][-1].currentIndex()] m = 0.5 * mq numerator = m * (18 * self.I * (self.I + 1) - 34 * m**2 - 5) denomenator = 0.5 * (18 * self.I * (self.I + 1) - 34 * 0.5**2 - 5) - divis = gcd(numerator, denomenator) + divis = gcd(int(numerator), int(denomenator)) numerator /= divis denomenator /= divis self.entries['shear'][-1].setText(str(numerator) + '/' + str(denomenator)) self.entries['scale'][-1].setText(str(denomenator) + '/' + str(mq * denomenator - numerator)) - + def changeType(self, index): + """ + Enables or disables the extended Czjzek fitting. + + Parameters + ---------- + index : int + Czjzek fitting type (0=regular, 1=extended). + """ if index == 0: - for i in range(self.FITNUM): + for i in range(self.FITNUM): self.entries["Cq0"][i].setEnabled(False) self.entries['eta0'][i].setEnabled(False) self.ticks["Cq0"][i].setChecked(True) @@ -3766,7 +4712,7 @@ def changeType(self, index): self.ticks["Cq0"][i].setEnabled(False) self.ticks['eta0'][i].setEnabled(False) elif index == 1: - for i in range(self.FITNUM): + for i in range(self.FITNUM): self.entries["Cq0"][i].setEnabled(True) self.entries['eta0'][i].setEnabled(True) self.ticks["Cq0"][i].setEnabled(True) @@ -3776,6 +4722,9 @@ def createCzjzekPrefWindow(self, *args): CzjzekPrefWindow(self, mqmas=True) def simLib(self): + """ + Simulate the spectra for the Czjzek library. + """ angle = np.arctan(np.sqrt(2)) alpha, beta, weight = simFunc.zcw_angles(self.cheng, 2) D2 = simFunc.D2tens(alpha, beta, np.zeros_like(alpha)) @@ -3784,6 +4733,9 @@ def simLib(self): self.lib, self.cqLib, self.etaLib = simFunc.genLib(len(self.parent.xax()), self.cqmin, self.cqmax, self.etamin, self.etamax, self.cqsteps, self.etasteps, extra, self.parent.freq(), self.parent.sw(), np.inf) def extraParamToFile(self): + """ + Extra parameters to export. + """ extraDict = {"Method": self.TYPES[self.entries['method'][-1].currentIndex()], "d": str(self.entries['d'][0].currentIndex() + 1), "I": CzjzekPrefWindow.Ioptions[int(self.I*2.0-2.0)], @@ -3800,6 +4752,9 @@ def extraParamToFile(self): return (extraDict, {}) def extraFileToParam(self, preParams, postParams): + """ + Extra parameters to import. + """ keys = preParams.keys() if "Method" in keys: self.entries['method'][0].setCurrentIndex(self.TYPES.index(preParams["Method"])) @@ -3827,6 +4782,9 @@ def extraFileToParam(self, preParams, postParams): self.cheng = int(preParams["Cheng"]) def getExtraParams(self, out): + """ + Returns the extra parameters of the fit. + """ if self.lib is None: raise FittingException("No library available") MQ = self.MQvalues[self.entries['MQ'][-1].currentIndex()] @@ -3840,22 +4798,24 @@ def getExtraParams(self, out): out['extra'] = [I, MQ, self.cqLib, self.etaLib, self.lib, shear, scale, method, d] return (out, out['extra']) - def checkResults(self,numExp,struc): - #After fit, set lor and gauss absolute + def checkResults(self, numExp, struc): + """ + Fixes the fit results. + """ locList = self.getRedLocList() for i in range(numExp): - if struc["Lorentz1"][i][0] == 1: + if struc["Lorentz1"][i][0] == 1: self.fitParamList[locList]["Lorentz1"][i][0] = abs(self.fitParamList[locList]["Lorentz1"][i][0]) - if struc["Gauss1"][i][0] == 1: + if struc["Gauss1"][i][0] == 1: self.fitParamList[locList]["Gauss1"][i][0] = abs(self.fitParamList[locList]["Gauss1"][i][0]) - if struc["Lorentz2"][i][0] == 1: + if struc["Lorentz2"][i][0] == 1: self.fitParamList[locList]["Lorentz2"][i][0] = abs(self.fitParamList[locList]["Lorentz2"][i][0]) - if struc["Gauss2"][i][0] == 1: + if struc["Gauss2"][i][0] == 1: self.fitParamList[locList]["Gauss2"][i][0] = abs(self.fitParamList[locList]["Gauss2"][i][0]) - if struc['eta0'][i][0] == 1: + if struc['eta0'][i][0] == 1: #eta is between 0--1 in a continuous way. self.fitParamList[locList]['eta0'][i][0] = 1 - abs(abs(self.fitParamList[locList]['eta0'][i][0]) % 2 - 1) - if struc["Cq0"][i][0] == 1: + if struc["Cq0"][i][0] == 1: self.fitParamList[locList]["Cq0"][i][0] = abs(self.fitParamList[locList]["Cq0"][i][0]) @@ -3882,13 +4842,13 @@ def __init__(self, parent, nameList, fitList, fitDefault): def getInputs(self): return (self.dataEntry.currentIndex(), self.fitList[self.fitEntry.currentIndex()]) - + @staticmethod def getFitInput(*args): dialog = NewTabDialog(*args) result = dialog.exec_() data, fitName = dialog.getInputs() - return (data, fitName, result == QtWidgets.QDialog.Accepted) + return (data, fitName, result == QtWidgets.QDialog.Accepted) # full_name, plot_frame, parameter_frame @@ -3903,4 +4863,3 @@ def getFitInput(*args): 'function': ("Function", FunctionFitFrame, FunctionFitParamFrame), 'mqmas': ("MQMAS", MqmasDeconvFrame, MqmasDeconvParamFrame), 'mqmasczjzek': ("Czjzek MQMAS", MqmasDeconvFrame, MqmasCzjzekParamFrame)} - diff --git a/src/functions.py b/src/functions.py index d4f1e505..91c2aee5 100644 --- a/src/functions.py +++ b/src/functions.py @@ -23,44 +23,107 @@ import scipy.constants as SC import scipy.linalg -def apodize(t, shift, sw, axLen, lor, gauss, cos2, hamming, wholeEcho=False): +def apodize(t, shift=0.0, lor=None, gauss=None, cos2=[None, None], hamming=None, wholeEcho=False): + """ + Calculates the window function for apodization. + + Parameters + ---------- + t : array_like + The time values at which to calculate the apodization function. + shift : float, optional + A shift in time of the function. + A positive value shift the curve to later times. + By default a shift of 0.0 is used. + lor : float, optional + The Lorentzian component of the apodization window. + By default Lorentzian apodization is not applied. + gauss : float, optional + The Gaussian component of the apodization window. + By default Gaussian apodization is not applied. + cos2 : array_like, optional + Defines the squared cosine apodization component. + Should have a length of at least two. + The first value is the frequency (two times the number of periods in the time domain). + The second value is the phase shift in degrees. + By default squared cosine apodization is not applied. + hamming : float, optional + The Hamming window component. + By default Hamming apodization is not applied. + wholeEcho : bool, optional + When True the apodization window is made symmetric in time to match echo experiments. + By default wholeEcho is False. + + Returns + ------- + ndarray + The apodization curve. + """ t2 = t - shift + axLen = len(t2) + sw = 1.0/(t[1]-t[0]) x = np.ones(axLen) if lor is not None: - x = x * np.exp(-np.pi * lor * abs(t2)) + x *= np.exp(-np.pi * lor * abs(t2)) if gauss is not None: - x = x * np.exp(-((np.pi * gauss * t2)**2) / (4 * np.log(2))) - if cos2[0] is not None and cos2[1] is not None : - x = x * (np.cos(np.radians(cos2[1]) + cos2[0] * (-0.5 * shift * np.pi * sw / axLen + np.linspace(0, 0.5 * np.pi, axLen)))**2) + x *= np.exp(-((np.pi * gauss * t2)**2) / (4 * np.log(2))) + if cos2[0] is not None and cos2[1] is not None: + x *= (np.cos(np.radians(cos2[1]) + cos2[0] * (-0.5 * shift * np.pi * sw / axLen + np.linspace(0, 0.5 * np.pi, axLen)))**2) if hamming is not None: alpha = 0.53836 # constant for hamming window - x = x * (alpha + (1 - alpha) * np.cos(hamming * (-0.5 * shift * np.pi * sw / axLen + np.linspace(0, np.pi, axLen)))) + x *= (alpha + (1 - alpha) * np.cos(hamming * (-0.5 * shift * np.pi * sw / axLen + np.linspace(0, np.pi, axLen)))) if wholeEcho: x[-1:-(int(len(x) / 2) + 1):-1] = x[:int(len(x) / 2)] return x def lpsvd(fullFid, nPredict, maxFreq, forward=False, L=None): + """ + Performs a LPSVD (Linear Predictive Singular Value Decomposition) on a given FID. + Both forward and backward prediction are available. + + Parameters + ---------- + fullFid : array_like + The FID to use for linear prediction. + nPredict : int + The number of points to predict. + For backward prediction, the points are added to the start of the FID. + For forward prediction, the points are added at the end of the FID. + maxFreq : int + Maximum number of frequencies to include in the prediction. + forward : bool, optional + True for forward prediction, False for backward prediction. + By default backward prediction is used. + L : int, optional + Number of datapoints from the beginning of the FID to use in the prediction of the frequency components. + By default all datapoints from fullFid are used. + + Returns + ------- + ndarray + The data including the LPSVD predicted values. + """ fid = fullFid[:L] N = len(fid) M = int(np.floor(N * 3 / 4.0)) H = scipy.linalg.hankel(fid[1:N-M+1], fid[N-M:]) U, S, Vh = np.linalg.svd(H, full_matrices=1) - sigVal = len(S[S>(S[0]*1e-6)]) # Number of significant singular values + sigVal = len(S[S > (S[0]*1e-6)]) # Number of significant singular values if sigVal > maxFreq: sigVal = maxFreq bias = np.mean(S[sigVal:]) S = S[:sigVal] - bias - U = U[:,:sigVal] + U = U[:, :sigVal] Sinv = np.diag(1.0/S) Vh = Vh[:sigVal] q = np.dot(np.dot(np.conj(Vh.T), np.dot(Sinv, np.conj(U.T))), fid[:(N-M)]) s = np.roots(np.append(-q[::-1], 1)) # Find the roots of the polynomial - s = s[np.abs(s)<1.0] # Accept only values within the unit circle + s = s[np.abs(s) < 1.0] # Accept only values within the unit circle sLog = np.log(s) freq = np.imag(sLog) lb = np.real(sLog) Nfull = len(fullFid) - Z = s**np.arange(Nfull)[:,np.newaxis] + Z = s**np.arange(Nfull)[:, np.newaxis] a = np.linalg.lstsq(Z, fullFid)[0] amp = np.abs(a) phase = np.angle(a) @@ -69,7 +132,7 @@ def lpsvd(fullFid, nPredict, maxFreq, forward=False, L=None): else: xpredict = np.arange(-nPredict, 0) if len(amp) > 0: - reconstructed = amp * np.exp(xpredict[:,np.newaxis] * (1j * freq + lb) + 1j * phase) + reconstructed = amp * np.exp(xpredict[:, np.newaxis] * (1j * freq + lb) + 1j * phase) reconstructed = np.sum(reconstructed, axis=1) else: reconstructed = np.zeros_like(xpredict) @@ -80,6 +143,27 @@ def lpsvd(fullFid, nPredict, maxFreq, forward=False, L=None): return data def euro(val, num): + """ + Generates a given number of elements from the Euro series (e.g., 0.1, 0.2, 0.5, 1.0, 2.0, ...) starting at a given value. + + Parameters + ---------- + val : float + The value at which to start the Euro series. + When this value is not part of the euro series an exception is thrown. + num : int + Number of elements from the Euro series should be generated. + + Returns + ------- + ndarray + The Euro series. + + Raise + ----- + ValueError + If val is not in the Euro series + """ firstDigit = '%.0e' % val firstDigit = int(firstDigit[0]) order = int(np.floor(np.log10(val))) @@ -93,18 +177,30 @@ def euro(val, num): elif firstDigit == 5: subset = [5, 10, 20] else: - return + raise ValueError(str(val) + " is not part of the Euro series") returnVal = np.tile(subset, numStep) orderArray = np.repeat(range(numStep), 3) + order returnVal = returnVal * 10.0**orderArray return returnVal[:num] def shiftConversion(Values, Type): - # Calculates the chemical shift tensor based on: - # Values: a list with three numbers - # Type: an integer defining the input shift convention - # Returns a list of list with all calculated values + """ + Generates a list of lists of the various chemical shift tensor definitions in solid-state NMR from a specific set of tensor values. + + Parameters + ---------- + Values : array_like + The chemical shift values defined in a specific definition. + Should have a length of 3. + Type : int + The definition in which Values is defined. + 0=standard, 1=xyz, 2=Haeberlen, 3=Hertzfeld-Berger. + Returns + ------- + list of lists + The chemical shift tensor definitions, which are (in order) standard, xyz, Haeberlen, and Herzfeld-Berger. + """ if Type == 0: # If from standard deltaArray = Values if Type == 1: # If from xyz @@ -162,73 +258,137 @@ def shiftConversion(Values, Type): Results.append([iso, span, skew]) return Results -def quadConversion(Values,I, Type, Q = None): - # Calculates the chemical shift tensor based on: - # Values: a list with two or three numbers (Cq/Wq and Eta, Or Vxx Vyy Vzz) - # Type: an integer defining the input shift convention - # I: spin quantum number - # Q: Quad moment in fm^2 - # Returns a list of list with all calculated values - if Type == 0: # Cq as input - # Cq, eta - # Czz is equal to Cq, via same definition (scale) Cxx and Cyy can be found - Czz = Values[0] - Eta = Values[1] - if Eta > 1.0: - Eta = 1.0 - elif Eta < 0.0: - Eta = 0.0 - Cxx = Czz * (Eta - 1) / 2 - Cyy = -Cxx - Czz - Values = [ Cxx, Cyy, Czz] - if Type == 1: - #Wq, eta - Vmax = Values[0] - Eta = Values[1] - if Eta > 1.0: - Eta = 1.0 - elif Eta < 0.0: - Eta = 0.0 - Czz = Vmax * (2.0 * I * (2 * I - 1)) / 3.0 - Cxx = Czz * (Eta - 1) / 2 - Cyy = -Cxx - Czz - Values = [ Cxx, Cyy, Czz] - if Type == 2: - #Vxx, Vyy, Vzz - Vxx = Values[0] - Vyy = Values[1] - Vzz = Values[2] - # Force traceless - if not np.isclose(Vxx + Vyy + Vzz, 0.0): - Diff = (Vxx + Vyy + Vzz) / 3.0 - Vxx = Vxx - Diff - Vyy = Vyy - Diff - Vzz = Vzz - Diff - Scaling = SC.elementary_charge * Q / SC.Planck - Czz = Vzz * Scaling / 1e6 # scale for Cq definition in MHz - Cxx = Vxx * Scaling / 1e6 - Cyy = Vyy * Scaling / 1e6 - Values = [ Cxx, Cyy, Czz] - #Conversion - CArray = np.array(Values) - Cindex = np.argsort(np.abs(CArray)) - Csort = CArray[Cindex] - if Csort[2] < 0: # If Czz negative due to weird input, make it positive - Csort = -Csort - CqNew = Csort[2] - if CqNew == 0.0: - EtaNew = None - else: - EtaNew = np.abs((Csort[0] - Csort[1]) / Csort[2]) # Abs to avoid -0.0 rounding error - WqNew = CqNew * 3.0 / (2.0 * I * (2 * I - 1)) - if Q != None: - Scaling = SC.elementary_charge * Q / SC.Planck - Vxx = Csort[0] / Scaling * 1e6 - Vyy = Csort[1] / Scaling * 1e6 - Vzz = Csort[2] / Scaling * 1e6 - else: - Vxx = None - Vyy = None - Vzz = None - return [[CqNew, EtaNew], [WqNew, EtaNew], [Vxx, Vyy, Vzz]] +def quadConversion(Values, I, Type, Q=None): + """ + Generates a list of lists of the various quadrupole tensor definitions in solid-state NMR from a specific set of tensor values. + Parameters + ---------- + Values : array_like + The chemical shift values defined in a specific definition. + Should have a length of 2 for the Cq/eta and Wq/eta definitions and 3 for the Vxx/Vyy/Vzz definition. + I : float + Spin quantum number. + Type : int + The definition in which Values is defined. + 0=Cq/eta, 1=Wq/eta, 2=Vxx/Vyy/Vzz. + Q : float, optional + Quadrupole moment in fm^2. + Has to be defined when Type is 2. + By default None. + + Returns + ------- + list of lists + The quadrupole tensor definitions, which are (in order) Cq/eta, Wq/eta, and Vxx/Vyy/Vzz. + When Q is None, Vxx=None, Vyy=None, Vzz=None. + + Raises + ------ + ValueError + When Q is undefined when Type is 2. + """ + if Type == 0: # Cq as input + # Cq, eta + # Czz is equal to Cq, via same definition (scale) Cxx and Cyy can be found + Czz = Values[0] + Eta = Values[1] + if Eta > 1.0: + Eta = 1.0 + elif Eta < 0.0: + Eta = 0.0 + Cxx = Czz * (Eta - 1) / 2 + Cyy = -Cxx - Czz + Values = [Cxx, Cyy, Czz] + if Type == 1: + # Wq, eta + Vmax = Values[0] + Eta = Values[1] + if Eta > 1.0: + Eta = 1.0 + elif Eta < 0.0: + Eta = 0.0 + Czz = Vmax * (2.0 * I * (2 * I - 1)) / 3.0 + Cxx = Czz * (Eta - 1) / 2 + Cyy = -Cxx - Czz + Values = [Cxx, Cyy, Czz] + if Type == 2: + #Vxx, Vyy, Vzz + if Q is None: + raise ValueError("Q cannot be None, when quadrupole type is Vxx/Vyy/Vzz") + Vxx = Values[0] + Vyy = Values[1] + Vzz = Values[2] + # Force traceless + if not np.isclose(Vxx + Vyy + Vzz, 0.0): + Diff = (Vxx + Vyy + Vzz) / 3.0 + Vxx = Vxx - Diff + Vyy = Vyy - Diff + Vzz = Vzz - Diff + Scaling = SC.elementary_charge * Q / SC.Planck + Czz = Vzz * Scaling / 1e6 # scale for Cq definition in MHz + Cxx = Vxx * Scaling / 1e6 + Cyy = Vyy * Scaling / 1e6 + Values = [Cxx, Cyy, Czz] + #Conversion + CArray = np.array(Values) + Cindex = np.argsort(np.abs(CArray)) + Csort = CArray[Cindex] + if Csort[2] < 0: # If Czz negative due to weird input, make it positive + Csort = -Csort + CqNew = Csort[2] + if CqNew == 0.0: + EtaNew = None + else: + EtaNew = np.abs((Csort[0] - Csort[1]) / Csort[2]) # Abs to avoid -0.0 rounding error + WqNew = CqNew * 3.0 / (2.0 * I * (2 * I - 1)) + if Q is not None: + Scaling = SC.elementary_charge * Q / SC.Planck + Vxx = Csort[0] / Scaling * 1e6 + Vyy = Csort[1] / Scaling * 1e6 + Vzz = Csort[2] / Scaling * 1e6 + else: + Vxx = None + Vyy = None + Vzz = None + return [[CqNew, EtaNew], [WqNew, EtaNew], [Vxx, Vyy, Vzz]] + +def ACMEentropy(phaseIn, data, x, firstOrder=True): + """ + Calculates the cost value for autophasing. + + Parameters + ---------- + phaseIn : list of float + Should contain two values, the first one is the zero order phase, and the second one the first order phase. + data : ndarray + The data to be phased. + x : ndarray + The x-axis corresponding to the first order phasing. + firstOrder : bool, optional + If True, the first order phasing is included. + True by default. + + Returns + ------- + The cost value. + """ + phase0 = phaseIn[0] + if firstOrder: + phase1 = phaseIn[1] + else: + phase1 = 0.0 + L = len(data) + s0 = data * np.exp(1j * (phase0 + phase1 * x)) + s2 = np.real(s0) + ds1 = np.abs((s2[3:L] - s2[1:L - 2]) / 2.0) + p1 = ds1 / sum(ds1) + p1[np.where(p1 == 0)] = 1 + h1 = -p1 * np.log(p1) + H1 = sum(h1) + Pfun = 0.0 + as1 = s2 - np.abs(s2) + sumas = sum(as1) + if np.real(sumas) < 0: + Pfun = Pfun + sum(as1**2) / 4 / L**2 + return H1 + 1000 * Pfun diff --git a/src/hypercomplex.py b/src/hypercomplex.py index 38f7cb23..06a4886f 100644 --- a/src/hypercomplex.py +++ b/src/hypercomplex.py @@ -17,13 +17,13 @@ # You should have received a copy of the GNU General Public License # along with ssNake. If not, see . -import numpy as np import warnings +import numpy as np def parity(x): # Find the parity of an integer parity = False - if x<0: + if x < 0: raise RuntimeError('Parity does not work for negative values') while x: parity = not parity @@ -39,8 +39,35 @@ class HComplexException(Exception): class HComplexData(object): + """ + Data object for hypercomplex data. + Methods which depend on the hypercomplex nature of the data are part of this class. + + The first dimension of self.data contains the hypercomplex matrices. + self.hyper has the same length as self.data and represents whether a matrix is imaginary along a certain dimension. + self.hyper is encoded in a binary way (0 means real, 1 means imaginary) with the least significant bit representing the first dimension. + For example, when self.hyper = [0,1,2,3] this means self.data[0] is real in all dimensions, + self.data[1] is imaginary in the first dimension and real in the second, + self.data[2] is real in the first dimension and imaginary in the second, + self.data[3] is imaginary in both first and second dimension. + self.data contains complex values, the imaginary values are from the last dimension which is always complex and is not listed in self.hyper. + """ def __init__(self, data=None, hyper=None): + """ + Initializes the HComplexData + + Parameters + ---------- + data : array_like, optional + Data to be used as hypercomplex data. + If data is None, an empty hypercomplex array is created. + hyper : array of ints, optional + should have the same length as data. + Is used as the hyper list of the hypercomplex data + If hyper is None, data is assumed to be regular complex data and an additional dimension + is added to hold the hypercomplex information. + """ if data is None: self.data = np.array([], dtype=complex) self.hyper = np.array([]) @@ -56,17 +83,46 @@ def __init__(self, data=None, hyper=None): self.hyper = np.array(hyper) def ndim(self): - return self.data.ndim - 1 # One extra dimension to contain the hypercomplex information + """ + Number of dimensions of the hypercomplex data, which is one less than the dimension of self.data. + + Returns + ------- + int + Number of dimensions. + """ + return self.data.ndim - 1 def shape(self): + """ + Shape of the hypercomplex data, which does not include the first dimension of self.data. + + Returns + ------- + tuple of ints + Shape of the data. + """ return self.data[0].shape def getHyperData(self, hyperVal): + """ + Returns the complex data corresponding to a specific hyper value. + + Parameters + ---------- + hyperVal : int + The value of hyper for which the complex data should be returned. + + Returns + ------- + ndarray + Complex data corresponding to hyperValue. + """ return self.data[hyperVal == self.hyper][0] def __repr__(self, *args): return self.__class__.__name__ + '(' + repr(self.data) + ', ' + repr(self.hyper) + ')' - + def __len__(self): if len(self.data) > 0: return len(self.data[0]) @@ -75,17 +131,17 @@ def __eq__(self, other): if isinstance(other, self.__class__): return np.all(other.data == self.data) and np.all(other.hyper == self.hyper) return False - + def __ne__(self, other): if isinstance(other, self.__class__): return np.any(other.data != self.data) and np.any(other.hyper != self.hyper) return True - + def __neg__(self): return HComplexData(-self.data, np.copy(self.hyper)) def __pos__(self): - return HComplexData(+self.data, np.copy(self.hyper)) + return HComplexData(self.data, np.copy(self.hyper)) def __abs__(self): return HComplexData(np.abs(self.data), np.copy(self.hyper)) @@ -103,9 +159,9 @@ def __iadd__(self, other): tmpHyper.sort() tmpData = np.zeros((len(tmpHyper),) + np.broadcast(self.data[0], other.data[0]).shape, dtype=complex) for i in self.hyper: - tmpData[i==tmpHyper] = self.data[i==self.hyper] + tmpData[i == tmpHyper] = self.data[i == self.hyper] for i in other.hyper: - tmpData[i==tmpHyper] += other.data[i==other.hyper] + tmpData[i == tmpHyper] += other.data[i == other.hyper] self.data = tmpData self.hyper = tmpHyper else: @@ -116,7 +172,7 @@ def __sub__(self, other): if isinstance(other, list): other = np.asarray(other) return self.__add__(-other) - + def __rsub__(self, other): return (-self).__add__(other) @@ -124,7 +180,7 @@ def __isub__(self, other): if isinstance(other, list): other = np.asarray(other) return self.__iadd__(-other) - + def __mul__(self, other): tmpData = self.copy() return tmpData.__imul__(other) @@ -156,7 +212,7 @@ def __imul__(self, other): def __div__(self, other): tmpData = self.copy() return tmpData.__idiv__(other) - + # TODO: implement inverse division def __idiv__(self, other): @@ -184,7 +240,7 @@ def __truediv__(self, other): def __pow__(self, other): tmpData = self.copy() return tmpData.__ipow__(other) - + def __rpow__(self, other): if len(self.hyper) > 1: raise HComplexException('Power with more than one complex axis is not permitted') @@ -222,26 +278,58 @@ def __setitem__(self, key, value): self.hyper = np.insert(self.hyper, insertOrder, diffList) self.data[(np.in1d(self.hyper, value.hyper), ) + key] = value.data else: - self.data[(slice(0,1), ) + key] = value - self.data[(slice(1,None), ) + key] = 0 + self.data[(slice(0, 1),) + key] = value + self.data[(slice(1, None),) + key] = 0 def conj(self, axis): + """ + Compute the complex conjugate along a specific axis. + + Parameters + ---------- + axis : int + The axis along which to calculate the complex conjugate. + If the axis is hypercomplex, the hypercomplex conjugate of that axis is returned. + Otherwise the complex conjugate of the entire data is returned. + + Returns + ------- + HComplexData + The complex conjugated data. + """ if axis < 0: axis = self.ndim() + axis if not self.isHyperComplex(axis) or axis == (self.ndim()-1): return HComplexData(np.conj(self.data), np.copy(self.hyper)) - else: - tmpData = np.copy(self.data) - imagBool = np.array(self.hyper & (2**axis), dtype=bool) - tmpData[imagBool] = -tmpData[imagBool] - return HComplexData(tmpData, np.copy(self.hyper)) + tmpData = np.copy(self.data) + imagBool = np.array(self.hyper & (2**axis), dtype=bool) + tmpData[imagBool] = -tmpData[imagBool] + return HComplexData(tmpData, np.copy(self.hyper)) def conjAll(self): + """ + Compute the complex conjugate of all dimensions. + + Returns + ------- + HComplexData + The complex conjugated data. + """ tmpData = np.conj(self.data) tmpData[1:] = -tmpData[1:] return HComplexData(tmpData, np.copy(self.hyper)) def isAllReal(self): + """ + Test if the data contains only real values + + The data is allowed to have hypercomplex dimensions as long as they only contain zeros. + + Returns + ------- + bool + True if all imaginary dimensions contain only zeros. + """ tmp = 0 if len(self.hyper) > 1: tmp = np.count_nonzero(self.data[1:]) @@ -249,35 +337,98 @@ def isAllReal(self): return not bool(tmp) def insertDim(self, axis): + """ + Add a dimension in self.hyper. + This should always be accompanied with a modification of self.data. + + Parameters + ---------- + axis : int + The axis along which a dimension was added. + """ watershedBits = 2**axis - 1 lowBits = self.hyper & watershedBits self.hyper = (self.hyper - lowBits) * 2 + lowBits - + def removeDim(self, axis): + """ + Remove a dimension from self.hyper. + This should always be accompanied with a modification of self.data. + + Parameters + ---------- + axis : int + The axis along which a dimension was removed. + If the axis had hypercomplex data associated with it, the imaginary parts are dropped. + If axis was the last dimension, the second last dimension is promoted to last dimension. + """ if axis < 0: axis = self.data.ndim - axis if self.isHyperComplex(axis): tmpdata = self.real(axis) self.data = tmpdata.data self.hyper = tmpdata.hyper - if axis == self.ndim(): + if axis == self.ndim() and self.isHyperComplex(axis-1): tmpdata = self.real(axis-1) self.data = tmpdata.data self.hyper = tmpdata.hyper watershedBits = 2**axis - 1 lowBits = self.hyper & watershedBits self.hyper = (self.hyper - lowBits) // 2 + lowBits - + def isComplex(self, axis): - # Axis ndim-1 are the regular complex numbers, which are always complex + """ + Test whether an axis is complex. + + Parameters + ---------- + axis : int + The axis to test. + + Returns + ------- + bool + True means the data along axis is complex. + This includes the last dimension. + """ if axis == (self.ndim()-1): return True return self.isHyperComplex(axis) def isHyperComplex(self, axis): + """ + Test whether an axis is hypercomplex. + + Parameters + ---------- + axis : int + The axis to test. + + Returns + ------- + bool + True means the data along axis is hypercomplex. + This excludes the last dimension. + """ return bool(np.max(self.hyper) & (2**axis)) def real(self, axis=-1): + """ + Return the real part along axis. + + Parameters + ---------- + axis : int, optional + The axis along which to get the real data. + If the axis is hypercomplex the imaginary data along that axis is removed. + Otherwise the imaginary data (the last dimension) is set to zero. + By default the last dimension is used. + + Returns + ------- + HComplexData + Real data along axis. + """ if axis < 0: axis = self.ndim() + axis if not self.isHyperComplex(axis): @@ -285,8 +436,24 @@ def real(self, axis=-1): bit = 2**axis select = np.logical_not(self.hyper & bit) return HComplexData(self.data[select], self.hyper[select]) - + def imag(self, axis=-1): + """ + Return the imaginary part along axis. + + Parameters + ---------- + axis : int, optional + The axis along which to get the imaginary data. + If the axis is hypercomplex the imaginary data is made the new real data. + The old real data along that dimension is removed. + By default the last dimension is used. + + Returns + ------- + HComplexData + Imaginary data along axis. + """ if axis < 0: axis = self.ndim() + axis if not self.isHyperComplex(axis): @@ -296,6 +463,22 @@ def imag(self, axis=-1): return HComplexData(self.data[select], self.hyper[select]-bit) def abs(self, axis=-1): + """ + Return the absolute data along axis. + + Parameters + ---------- + axis : int, optional + The axis along which to get the absolute data. + If the axis is hypercomplex the absolute data is calculated from the hypercomplex pair. + Otherwise the regular complex values are used (the last dimension). + By default the last dimension is used. + + Returns + ------- + HComplexData + Absolute data along axis. + """ if axis < 0: axis = self.ndim() + axis if not self.isHyperComplex(axis): @@ -308,19 +491,50 @@ def abs(self, axis=-1): tmpData = np.zeros((len(tmpHyper),) + self.data[0].shape, dtype=complex) for i, idim in enumerate(tmpHyper): if idim in self.hyper and (idim+bit) in self.hyper: - tmpData[i] += np.sqrt(np.real(self.data[idim==self.hyper][0])**2 + np.real(self.data[(idim+bit)==self.hyper][0])**2) - tmpData[i] += 1j * np.sqrt(np.imag(self.data[idim==self.hyper][0])**2 + np.imag(self.data[(idim+bit)==self.hyper][0])**2) + tmpData[i] += np.sqrt(np.real(self.data[idim == self.hyper][0])**2 + np.real(self.data[(idim+bit) == self.hyper][0])**2) + tmpData[i] += 1j * np.sqrt(np.imag(self.data[idim == self.hyper][0])**2 + np.imag(self.data[(idim+bit) == self.hyper][0])**2) elif idim in self.hyper: - tmpData[i] = self.data[idim==self.hyper] - elif (idim+bit) in self.hyper: - tmpData[i] = self.data[idim==self.hyper] + tmpData[i] = self.data[idim == self.hyper] + elif idim + bit in self.hyper: + tmpData[i] = self.data[idim == self.hyper] return HComplexData(tmpData, tmpHyper) def complexReorder(self, axis=0): + """ + Return data where the regular imaginary values are exchanged with those of axis. + + Parameters + ---------- + axis : int, optional + The axis along which to exchange with the regular imaginary values. + When the data along this axis is not hypercomplex, the data is returned unchanged. + By default the first dimension is used. + + Returns + ------- + HComplexData + Reordered hypercomplex data. + """ tmpData = self.copy() return tmpData.icomplexReorder(axis) - + def icomplexReorder(self, axis=0): + """ + Reorders data so that the regular imaginary values are exchanged with those of axis. + The operation is applied in place for efficiency. + + Parameters + ---------- + axis : int, optional + The axis along which to exchange with the regular imaginary values. + When the data along this axis is not hypercomplex, the data is returned unchanged. + By default the first dimension is used. + + Returns + ------- + HComplexData + A pointer to self + """ if not self.isHyperComplex(axis): # If the data is not complex along that axis return the data unchanged return self @@ -338,6 +552,22 @@ def icomplexReorder(self, axis=0): return self def moveaxis(self, axis1, axis2): + """ + Move axes of an array to new positions. + Other axes remain in their original order. + + Parameters + ---------- + axis1 : int or sequence of int + Original positions of the axes to move. These must be unique. + axis2 : int or sequence of int + Destination positions for each of the original axes. These must also be unique. + + Returns + ------- + HComplexData + A copy of the data with moved axes. + """ if isinstance(axis1, (int, float)): axis1 = [axis1] if isinstance(axis2, (int, float)): @@ -348,8 +578,29 @@ def moveaxis(self, axis1, axis2): axis2[axis2 >= 0] += 1 tmpData = np.moveaxis(self.data, axis1, axis2) return HComplexData(tmpData, np.copy(self.hyper)) - + def insert(self, pos, other, axis=-1): + """ + Insert data along the given axis before the given position. + + The resulting data will have hypercomplex dimensions of both input matrices. + + Parameters + ---------- + pos : int + Position where to insert the data. + other : HComplexData or ndarray + Data to insert. + Should have the same dimensions as self.data, except for dimension axis. + axis : int, optional + The axis along which to insert the data. + Defaults to the last dimension. + + Returns + ------- + HComplexData + A copy of the data with the inserted data. + """ if axis < 0: axis = self.ndim() - axis if not isinstance(other, HComplexData): @@ -357,23 +608,51 @@ def insert(self, pos, other, axis=-1): tmpHyper = np.unique(np.concatenate((self.hyper, other.hyper))) tmpHyper.sort() tmpData = [] - for i, idim in enumerate(tmpHyper): + for idim in tmpHyper: if idim in self.hyper and idim in other.hyper: - tmpData.append(np.insert(self.data[idim==self.hyper][0], [pos], other.data[idim==other.hyper][0], axis=axis)) + tmpData.append(np.insert(self.data[idim == self.hyper][0], [pos], other.data[idim == other.hyper][0], axis=axis)) elif idim in self.hyper: - tmpData.append(np.insert(self.data[idim==self.hyper][0], [pos], np.zeros_like(other.data[0]), axis=axis)) + tmpData.append(np.insert(self.data[idim == self.hyper][0], [pos], np.zeros_like(other.data[0]), axis=axis)) elif idim in other.hyper: - tmpData.append(np.insert(np.zeros_like(self.data[0]), [pos], other.data[idim==other.hyper][0], axis=axis)) + tmpData.append(np.insert(np.zeros_like(self.data[0]), [pos], other.data[idim == other.hyper][0], axis=axis)) else: tmpData.append(np.insert(np.zeros_like(self.data[0]), [pos], np.zeros_like(other.data[0]), axis=axis)) return HComplexData(np.array(tmpData), tmpHyper) def delete(self, pos, axis): + """ + Return data with sub-arrays deleted along axis. + + Parameters + ---------- + pos : int or array of int + Positions to delete. + axis : int + The axis along which to delete the data. + + Returns + ------- + HComplexData + A copy of the data with the subarrays deleted. + """ if axis >= 0: axis += 1 return HComplexData(np.delete(self.data, pos, axis), np.copy(self.hyper)) - + def concatenate(self, axis): + """ + Return data with the data concatenated along axis. + + Parameters + ---------- + axis : int + The axis along which to concatenate. + + Returns + ------- + HComplexData + The concatenated data. + """ if axis >= 0: axis += 1 tmpData = np.swapaxes(self.data, 0, 1) @@ -381,11 +660,41 @@ def concatenate(self, axis): return HComplexData(tmpData, np.copy(self.hyper)) def split(self, sections, axis): + """ + Return data which is split into multiple sections. + + Parameters + ---------- + sections : int + The number of sections into which the data should be split. + The length of the data along axis should be divisible by the number of sections. + axis : int + The axis along which to split the data. + + Returns + ------- + HComplexData + The split data. + """ if axis >= 0: axis += 1 return HComplexData(np.swapaxes(np.split(self.data, sections, axis), 0, 1), np.copy(self.hyper)) def states(self, axis, TPPI=False): + """ + Converts the data for States or States-TPPI. + This can only be performed on an axis which is not yet hypercomplex. + After the operation this axis will be hypercomplex. + This operation is performed in place. + + Parameters + ---------- + axis : int + The axis along which to perform the States transform. + TPPI : bool, optional + When True a States-TPPI transform is performed, otherwise a States transform is performed. + Defaults to False. + """ if axis < 0: axis = self.ndim() + axis if self.isComplex(axis): @@ -405,6 +714,17 @@ def states(self, axis, TPPI=False): self.hyper = np.insert(self.hyper, insertOrder, addHyper) def echoAntiEcho(self, axis): + """ + Converts the data for echo-antiecho. + This can only be performed on an axis which is not yet hypercomplex. + After the operation this axis will be hypercomplex. + This operation is performed in place. + + Parameters + ---------- + axis : int + The axis along which to perform the echo-antiecho transform. + """ if axis < 0: axis = self.ndim() + axis if self.isComplex(axis): @@ -421,62 +741,242 @@ def echoAntiEcho(self, axis): self.hyper = np.insert(self.hyper, insertOrder, addHyper) def mean(self, axis=-1, **kwargs): + """ + Compute the arithmetic mean along the specified axis. + + Parameters + ---------- + axis : int, optional + The axis along which to calcule the mean values. + Defaults to the last dimension. + **kwargs + Additional keyword arguments are passed to Numpy mean. + + Returns + ------- + HComplexData + The mean values. + """ if axis >= 0: axis += 1 return HComplexData(np.mean(self.data, axis=axis, **kwargs), np.copy(self.hyper)) def sum(self, axis=-1, **kwargs): + """ + Compute the sum along the specified axis. + + Parameters + ---------- + axis : int, optional + The axis along which to calcule the sum. + Defaults to the last dimension. + **kwargs + Additional keyword arguments are passed to Numpy sum. + + Returns + ------- + HComplexData + The sum. + """ if axis >= 0: axis += 1 return HComplexData(np.sum(self.data, axis=axis, **kwargs), np.copy(self.hyper)) def max(self, axis=-1): + """ + Returns the data maxima along the specified axis. + The maxima are defined with respect to the real data. + The hypercomplex imaginary data is ignored in determining the maxima. + + Parameters + ---------- + axis : int, optional + The axis along which to calcule the maximum. + Defaults to the last dimension. + + Returns + ------- + HComplexData + The maxima. + """ argVals = np.argmax(self.data[0], axis=axis) ind = list(np.indices(argVals.shape)) ind.insert(axis, argVals) return HComplexData(self.data[(slice(None), ) + tuple(ind)], np.copy(self.hyper)) def min(self, axis=-1): + """ + Returns the data minima along the specified axis. + The minima are defined with respect to the real data. + The hypercomplex imaginary data is ignored in determining the minima. + + Parameters + ---------- + axis : int, optional + The axis along which to calcule the minimum. + Defaults to the last dimension. + + Returns + ------- + HComplexData + The minima. + """ argVals = np.argmin(self.data[0], axis=axis) ind = list(np.indices(argVals.shape)) ind.insert(axis, argVals) return HComplexData(self.data[(slice(None), ) + tuple(ind)], np.copy(self.hyper)) def argmax(self, axis=-1): + """ + Return the positions of the maxima along the specified axis as a hypercomplex object. + The maxima are defined with respect to the real data. + The hypercomplex imaginary data is ignored in determining the positions of the maxima. + + Parameters + ---------- + axis : int, optional + The axis along which to calcule the postions of the maxima. + Defaults to the last dimension. + + Returns + ------- + HComplexData + The positions of the maxima. + """ return HComplexData(np.argmax(self.data[0], axis=axis)) def argmin(self, axis=-1): + """ + Return the positions of the minima along the specified axis as a hypercomplex object. + The minima are defined with respect to the real data. + The hypercomplex imaginary data is ignored in determining the positions of the minima. + + Parameters + ---------- + axis : int, optional + The axis along which to calcule the postions of the minima. + Defaults to the last dimension. + + Returns + ------- + HComplexData + The positions of the minima. + """ return HComplexData(np.argmin(self.data[0], axis=axis)) def expand_dims(self, axis=-1): + """ + Expand the shape of the hypercomplex data. + + Parameters + ---------- + axis : int, optional + Position in the expanded axes where the new axis is placed. + Defaults to the last dimension. + + Returns + ------- + HComplexData + A copy of the data with an additional dimension. + """ if axis >= 0: axis += 1 return HComplexData(np.expand_dims(self.data, axis), np.copy(self.hyper)) def append(self, values, axis=-1): + """ + Returns a copy of the hypercomplex data with data added along a specified axis. + + Parameters + ---------- + values : HComplexData or array_like + Data to be appended. It should have the correct shape (the same shape as self.data, excluding axis). + axis : int, optional + The axis along which the values are appended. + Defaults to the last dimension. + + Returns + ------- + HComplexData + The appended data. + """ if axis >= 0: axis += 1 if isinstance(values, HComplexData): # Fix for unequal hyper return HComplexData(np.append(self.data, values.data, axis=axis), np.copy(self.hyper)) - else: - return HComplexData(np.append(self.data, values, axis=axis), np.copy(self.hyper)) + return HComplexData(np.append(self.data, values, axis=axis), np.copy(self.hyper)) def reshape(self, shape): + """ + Returns a reshaped version of the hypercomplex data. + + Parameters + ---------- + shape : tuple of ints + New shape of the data. Should be compatible with the old shape. + + Returns + ------- + HComplexData + The reshaped data. + """ newShape = tuple(len(self.data)) + shape return HComplexData(self.data.reshape(newShape), np.copy(self.hyper)) - + def diff(self, axis=-1): + """ + Calculate the discrete difference along the given axis. + + Parameters + ---------- + axis : int, optional + The axis along which to calcule the difference. + Defaults to the last dimension. + + Returns + ------- + HComplexData + The difference data. + """ if axis >= 0: axis += 1 return HComplexData(np.diff(self.data, axis=axis), np.copy(self.hyper)) def cumsum(self, axis=-1): + """ + Calculate the cumulative sum along the given axis. + + Parameters + ---------- + axis : int, optional + The axis along which to calcule the cumulative sum. + Defaults to the last dimension. + + Returns + ------- + HComplexData + The cumulative sum data. + """ if axis >= 0: axis += 1 return HComplexData(np.cumsum(self.data, axis=axis), np.copy(self.hyper)) def hilbert(self, axis=-1): + """ + Performs a Hilbert transform on the data. + + Parameters + ---------- + axis : int, optional + The axis over which the Hilbert transform is performed. + Defaults to the last dimension. + + Returns + ------- + HComplexData + A copy of the data which has been Hilbert transformed. + """ import scipy.signal if axis >= 0: axis += 1 @@ -484,13 +984,52 @@ def hilbert(self, axis=-1): return HComplexData(tmpData, np.copy(self.hyper)) def regrid(self, newX, oldX, axis=-1): + """ + Regrid the data from specified x-values to new x-values. + The interpolation is done using the interp1d function from scipy.interpolate. + + Parameters + ---------- + newX : array_like + A 1-D array with the new x-values. + oldX : array_like + A 1-D array with the old x-values. It should have the same length as the data along axis. + axis : int, optional + The axis along which the interpolation is applied. + Defaults to the last dimension. + + Returns + ------- + HComplexData + A copy of the data which has been regrid. + """ from scipy import interpolate as intp if axis >= 0: axis += 1 tmpData = np.apply_along_axis(lambda data, newX, oldX: intp.interp1d(oldX, data, fill_value=0, bounds_error=False)(newX), axis, self.data, newX, oldX) return HComplexData(tmpData, np.copy(self.hyper)) - def resize(self, size, pos, axis): + def resize(self, size, pos, axis=-1): + """ + Resizes the data along a specified axis. + When the new size is larger than the old size, zeros are added at a specified position. + When the new size is smaller, datapoints are removed symmetric around the specified position. + + Parameters + ---------- + size : int + The new size of the data along axis. + pos : int + The position where zeros should be added or datapoints should be removed. + axis : int, optional + The axis along which the data is resized. + Defaults to the last dimension. + + Returns + ------- + HComplexData + A copy of the data which has been resized. + """ if axis >= 0: axis += 1 oldSize = self.data.shape[axis] @@ -517,6 +1056,26 @@ def resize(self, size, pos, axis): return HComplexData(tmpData, np.copy(self.hyper)) def reorder(self, pos, newLength=None, axis=-1): + """ + Reorders the data along a specified axis. + The rest of the data is filled with zeros. + + Parameters + ---------- + pos : array_like + The positions of the subarrays in the new data. + newLength : int, optional + The new length of the data along axis. + By default one more than the largest value in pos is used. + axis : int, optional + The axis along which the data is reordered. + Defaults to the last dimension. + + Returns + ------- + HComplexData + A copy of the data which has been resized. + """ if axis >= 0: axis += 1 if newLength is None: @@ -532,35 +1091,131 @@ def reorder(self, pos, newLength=None, axis=-1): return HComplexData(tmpData, np.copy(self.hyper)) def apply_along_axis(self, func, axis, *args, **kwargs): + """ + Applies a function to 1-D slices of the data. + Behaves similar to the Numpy apply_along_axis function. + + Parameters + ---------- + func : function + This function should accept 1-D arrays. It is applied to 1-D slices of arr along the specified axis. + axis : int + Axis along which the data is sliced. + + Returns + ------- + HComplexData + A copy of the data to which the function has been applied. + """ if axis >= 0: axis += 1 tmpData = np.apply_along_axis(func, axis, self.data, *args, **kwargs) return HComplexData(tmpData, np.copy(self.hyper)) - def roll(self, shift, axis): + def roll(self, shift, axis=-1): + """ + Rolls the data along a given axis. + + Parameters + ---------- + shift : int + Number of places to roll the data. + axis : int, optional + The axis along which the data is rolled. + Defaults to the last dimension. + + Returns + ------- + HComplexData + A copy of the rolled data. + """ if axis >= 0: axis += 1 return HComplexData(np.roll(self.data, shift, axis=axis), np.copy(self.hyper)) - - def fft(self, axis): + + def fft(self, axis=-1): + """ + Performs a Fast Fourier Transform on the data along a given axis. + + Parameters + ---------- + axis : int, optional + The axis over which the Fourier transform is performed. + Defaults to the last dimension. + + Returns + ------- + HComplexData + A copy of the data which has been Fourier transformed. + """ if axis >= 0: axis += 1 return HComplexData(np.fft.fft(self.data, axis=axis), np.copy(self.hyper)) - def ifft(self, axis): + def ifft(self, axis=-1): + """ + Performs a inverse Fast Fourier Transform on the data along a given axis. + + Parameters + ---------- + axis : int, optional + The axis over which the inverse Fourier transform is performed. + Defaults to the last dimension. + + Returns + ------- + HComplexData + A copy of the data which has been inverse Fourier transformed. + """ if axis >= 0: axis += 1 return HComplexData(np.fft.ifft(self.data, axis=axis), np.copy(self.hyper)) - def fftshift(self, axis): + def fftshift(self, axis=-1): + """ + Shift the zero-frequency component to the center of the spectrum. + + Parameters + ---------- + axis : int, optional + The axis over which to shift the data. + Defaults to the last dimension. + + Returns + ------- + HComplexData + A copy of the shifted data. + """ if axis >= 0: axis += 1 return HComplexData(np.fft.fftshift(self.data, axes=axis), np.copy(self.hyper)) - def ifftshift(self, axis): + def ifftshift(self, axis=-1): + """ + Inverse of fftshift. + + Parameters + ---------- + axis : int, optional + The axis over which to shift the data. + Defaults to the last dimension. + + Returns + ------- + HComplexData + A copy of the shifted data. + """ if axis >= 0: axis += 1 return HComplexData(np.fft.ifftshift(self.data, axes=axis), np.copy(self.hyper)) def copy(self): + """ + Returns a copy of the hypercomplex data. + + Returns + ------- + HComplexData + A copy of the data. + """ return HComplexData(np.copy(self.data), np.copy(self.hyper)) diff --git a/src/loadIsotopes.py b/src/loadIsotopes.py index fb8e4f83..d9783f1e 100644 --- a/src/loadIsotopes.py +++ b/src/loadIsotopes.py @@ -18,26 +18,35 @@ # You should have received a copy of the GNU General Public License # along with ssNake. If not, see . -import os import sys def fOrNone(inp): - #Convert to float is possible, otherwise None + """Converts a string to a float and dashes to None""" if inp == '-': return None - else: - return float(inp) - + return float(inp) def getIsotopeInfo(isoPath): + """ + Loads the isotope table from a given path. + + Parameters + ---------- + isoPath : str + The path to the file with the isotope properties. - if sys.version_info < (3,): + Returns + ------- + dict + A dictionary with the isotope properties. + Unknown or undefined values are set to None. + """ + if sys.version_info < (3,): with open(isoPath) as isoFile: isoList = [line.strip().split('\t') for line in isoFile] else: - with open(isoPath, encoding = 'UTF-8') as isoFile: + with open(isoPath, encoding='UTF-8') as isoFile: isoList = [line.strip().split('\t') for line in isoFile] - isoList = isoList[1:] #Cut off header nameList = [] fullNameList = [] @@ -45,17 +54,16 @@ def getIsotopeInfo(isoPath): atomNumList = [] atomMassList = [] spinList = [] - abundanceList = [] - gammaList = [] + abundanceList = [] + gammaList = [] qList = [] freqRatioList = [] refSampleList = [] sampleConditionList = [] - linewidthFactorList = [] + linewidthFactorList = [] lifetimeList = [] sensList = [] - - for i in range(len(isoList)): + for i, _ in enumerate(isoList): isoN = isoList[i] atomNumList.append(int(isoN[0])) nameList.append(isoN[1]) @@ -74,18 +82,14 @@ def getIsotopeInfo(isoPath): if isoN[4] == '0.5' or spinList[i] is None or qList[i] is None: linewidthFactorList.append(None) else: - #Linewidth due to quadrupolar broadening: (2I + 3) * Q /(I^2 * (2I - 1)) - linewidthFactorList.append((2 * spinList[i] + 3) * qList[i]**2 / (spinList[i]**2 * (2 * spinList[i] - 1))) - + linewidthFactorList.append((2 * spinList[i] + 3) * qList[i]**2 / (spinList[i]**2 * (2 * spinList[i] - 1))) # Linewidth due to quadrupolar broadening: (2I + 3) * Q /(I^2 * (2I - 1)) if gammaList[-1] is not None and abundanceList[-1] is not None and spinList[-1] is not None: - #Sensitivity: chi * gamma**3 * I * (I + 1) - sensList.append(abundanceList[-1] * abs(gammaList[-1])**3 * spinList[-1] * (spinList[-1] + 1)) + sensList.append(abundanceList[-1] * abs(gammaList[-1])**3 * spinList[-1] * (spinList[-1] + 1)) # Sensitivity: chi * gamma**3 * I * (I + 1) else: sensList.append(None) lifetimeList.append(isoN[11]) - - isotopes = {'atomNum':atomNumList, 'name':nameList,'fullName':fullNameList, 'atomMass':atomMassList, - 'formatName':formatNameList, 'spin':spinList, 'abundance':abundanceList,'q':qList,'freqRatio':freqRatioList, - 'refSample':refSampleList,'sampleCondition':sampleConditionList,'linewidthFactor':linewidthFactorList, - 'sensitivity':sensList,'lifetime':lifetimeList,'gamma':gammaList} + isotopes = {'atomNum':atomNumList, 'name':nameList, 'fullName':fullNameList, 'atomMass':atomMassList, + 'formatName':formatNameList, 'spin':spinList, 'abundance':abundanceList, 'q':qList, 'freqRatio':freqRatioList, + 'refSample':refSampleList, 'sampleCondition':sampleConditionList, 'linewidthFactor':linewidthFactorList, + 'sensitivity':sensList, 'lifetime':lifetimeList, 'gamma':gammaList} return isotopes diff --git a/src/nmrTable.py b/src/nmrTable.py index 178d5198..3f48743c 100644 --- a/src/nmrTable.py +++ b/src/nmrTable.py @@ -137,10 +137,10 @@ def initUI(self): self.electronEntry.returnPressed.connect(self.setElectron) grid.addWidget(self.electronEntry, 1, 3) self.detailsPush = QtWidgets.QPushButton('Details') - self.detailsPush.pressed.connect(lambda : self.openWindow(None, 0)) + self.detailsPush.clicked.connect(lambda : self.openWindow(None, 0)) grid.addWidget(self.detailsPush, 1, 4,1,2) self.listPush = QtWidgets.QPushButton('List') - self.listPush.pressed.connect(lambda : self.openList()) + self.listPush.clicked.connect(lambda : self.openList()) grid.addWidget(self.listPush, 1, 6,1,2) grid.addWidget(PtQLabel('Spin:'), 0, 4) for i in range(len(SPINNAMES)): diff --git a/src/nus.py b/src/nus.py index 447768e5..317c6981 100644 --- a/src/nus.py +++ b/src/nus.py @@ -23,6 +23,22 @@ def ent_ffm(missingPoints, fid, posArray): + """ + Performs FFM NUS reconstruction of a 1D FID. + + Parameters + ---------- + missingPoints: ndarray + 1D array which holds the reconstructed spectrum that will be optimized + fid: ndarray + 1D array with the 'bad' FID + posArray: ndarray + 1D array with the indexes of the 'bad' points of the FID + + Returns + ------- + + """ fid[posArray] = missingPoints[:len(posArray)] + 1j * missingPoints[len(posArray):] spec = np.fft.fft(fid) zn = np.fft.fft((np.imag(spec) + 1j * np.real(spec)) / np.abs(spec)) @@ -30,6 +46,20 @@ def ent_ffm(missingPoints, fid, posArray): def ffm(inp): + """ + Performs FFM NUS reconstruction of a 1D FID. + + Parameters + ---------- + inp: list with paramyters: + 0: 1D ndarray with the 'bad' FID + 1: 1D ndarray with the indexes of the 'bad' points of the FID + + Returns + ------- + ndarray: + 1D array of the corrected spectrum. + """ l = len(inp[1]) res = scipy.optimize.minimize(ent_ffm, np.zeros(l * 2), @@ -41,6 +71,23 @@ def ffm(inp): def clean(inp): + """ + Performs CLEAN NUS reconstruction of a 1D spectrum. + + Parameters + ---------- + inp: list with parameters: + 0: 1D ndarray with the 'bad' spectrum + 1: 1D array of the fft of the mask + 2: float, gamma value of the CLEAN calculation + 3: float, stopping limit (0 < x < 1) (stop if residual intensity below this point) + 4: int, maximum number of iterations + + Returns + ------- + ndarray: + 1D array of the corrected spectrum. + """ residuals = inp[0] mask = inp[1] gamma = inp[2] @@ -61,6 +108,24 @@ def clean(inp): def ist(inp): # Iterative soft thresholding + """ + Performs Iterative Soft Thresholding of a 1D FID + + Parameters + ---------- + inp: list with paramyters: + 0: 1D ndarray with the FID (reshaped to contain the zeros) + 1: 1D array with the 'zero' positions + 2: float, threshold. The level (0 < x < 1) at which the data is cut every iteration + 3: int, maximum number of iterations + 4: float, stopping limit (0 < x < 1) (stop if residual intensity below this point) + 5: float, maxmimum of the ND data, needed for the stopping limit + + Returns + ------- + ndarray: + 1D array of the corrected spectrum. + """ data = inp[0] posList = inp[1] # data points that must be set to zero threshold = inp[2] # level at which the data is cut @@ -69,7 +134,7 @@ def ist(inp): # Iterative soft thresholding NDmax = inp[5] # max of ND data. Needed for stopping limit. result = np.zeros_like(data, dtype=complex) data[0] = data[0] * 0.5 - for itt in range(0, ittnum): + for itt in range(ittnum): spectrum = np.real(np.fft.fft(data, axis=0)) signmatrix = np.sign(spectrum) height = np.max(np.abs(spectrum)) @@ -83,8 +148,6 @@ def ist(inp): # Iterative soft thresholding spectrum = np.conj(scipy.signal.hilbert(spectrum, axis=0)) data = np.fft.ifft(spectrum, axis=0) data[posList] = 0 - result = np.real(result + spectrum) - - result = np.fft.fftshift(result,axes=0) + result = np.fft.fftshift(result, axes=0) return result diff --git a/src/reimplement.py b/src/reimplement.py index 73489751..d6e98482 100644 --- a/src/reimplement.py +++ b/src/reimplement.py @@ -19,6 +19,20 @@ def floatSlice(*args): + """ + Function to create Slice objects with float input values. + + Parameters + ---------- + *args + All arguments that aren't None are converted to int. + The arguments are then used to create the Slice object. + + Returns + ------- + Slice + The output Slice object. + """ tmp = () for arg in args: if arg is None: diff --git a/src/safeEval.py b/src/safeEval.py index 55429eb5..2c042bdd 100644 --- a/src/safeEval.py +++ b/src/safeEval.py @@ -17,12 +17,35 @@ # You should have received a copy of the GNU General Public License # along with ssNake. If not, see . -import numpy as np import re -import hypercomplex as hc +import numpy as np import scipy.special +import hypercomplex as hc + +def safeEval(inp, length=None, Type='All', x=None): + """ + Creates a more restricted eval environment. + Note that this method is still not acceptable to process strings from untrusted sources. + + Parameters + ---------- + inp : str + String to evaluate. + length : int or float, optional + The variable length will be set to this value. + By default the variable length is not set. + Type : {'All', 'FI', 'C'}, optional + Type of expected output. 'All' will return all types, 'FI' will return a float or int, and 'C' will return a complex number. + By default Type is set to 'All' + x : array_like, optional + The variable x is set to this variable, + By default the variable x is not used. -def safeEval(inp, length=None, keywords=[], type='All', x=None): + Returns + ------- + Object + The result of the evaluated string. + """ env = vars(np).copy() env.update(vars(hc).copy()) env.update(vars(scipy.special).copy()) @@ -38,20 +61,17 @@ def safeEval(inp, length=None, keywords=[], type='All', x=None): if x is not None: env["x"] = x inp = re.sub('([0-9]+)[kK]', '\g<1>*1024', str(inp)) - for i in keywords: - if i in inp: - return inp try: val = eval(inp, env) if isinstance(val, str): return None - if type == 'All': + if Type == 'All': return val - elif type == 'FI': #single float/int type + if Type == 'FI': #single float/int type if isinstance(val, (float, int)) and not np.isnan(val) and not np.isinf(val): return val return None - elif type == 'C': #single complex number + if Type == 'C': #single complex number if isinstance(val, (float, int, complex)) and not np.isnan(val) and not np.isinf(val): return val return None diff --git a/src/saveFigure.py b/src/saveFigure.py index 1e82cf2b..389ec536 100644 --- a/src/saveFigure.py +++ b/src/saveFigure.py @@ -17,25 +17,42 @@ # You should have received a copy of the GNU General Public License # along with ssNake. If not, see . +import numpy as np +import os +import copy import matplotlib matplotlib.rc('svg', fonttype='none') from matplotlib.colors import colorConverter import widgetClasses as wc from safeEval import safeEval -import numpy as np -import os -import copy from ssNake import QtGui, QtCore, QtWidgets, FigureCanvas ##################################################################################### class SaveFigureWindow(QtWidgets.QWidget): + """ + The window for saving a figure. + This window replaces the original figure. + """ def __init__(self, father, oldMainWindow): + """ + Initializes the SaveFigureWindow. + + Parameters + ---------- + father : MainProgram + The main program of ssNake. + oldMainWindow : Main1DWindow + The figure window that this window replaces. + """ super(SaveFigureWindow, self).__init__(father) self.father = father self.oldMainWindow = oldMainWindow + self.rename = self.oldMainWindow.rename # Forward function + self.get_masterData = self.oldMainWindow.get_masterData # Forward function + self.get_current = self.oldMainWindow.get_current # Forward function self.fig = oldMainWindow.current.fig self.canvas = FigureCanvas(self.fig) self.canvas.mpl_connect('pick_event', self.pickHandler) @@ -188,10 +205,9 @@ def __init__(self, father, oldMainWindow): self.ytickFontSizeEntry.valueChanged.connect(self.updatePlot) self.ytickFontSizeEntry.hide() self.fontFrame.addWidget(self.ytickFontSizeEntry, 6, 1) - self.legend = self.ax.legend() - if self.legend is not None: #Fix for matplotlid 2.0, were for contour self.legend becomes None - if len(self.legend.get_texts()) == 0: + if self.legend is not None: # Fix for matplotlib 2.0, were for contour self.legend becomes None + if not self.legend.get_texts(): self.legend.set_visible(False) self.legend = None else: @@ -218,7 +234,10 @@ def __init__(self, father, oldMainWindow): self.legendOrder = list(np.arange(0, len(self.legend.get_texts())))[::-1] else: self.legendOrder = list(np.arange(0, len(self.legend.get_texts()))) - self.legend.draggable(True) + try: + self.legend.set_draggable(True) + except AttributeError: + self.legend.draggable(True) # For older Matplotlib versions self.legendPos = 'best' self.legendTextList = [] for line in self.legend.get_texts(): @@ -250,8 +269,8 @@ def __init__(self, father, oldMainWindow): okButton.clicked.connect(self.save) box = QtWidgets.QDialogButtonBox() box.setOrientation(QtCore.Qt.Horizontal) - box.addButton(cancelButton,QtWidgets.QDialogButtonBox.RejectRole) - box.addButton(okButton,QtWidgets.QDialogButtonBox.AcceptRole) + box.addButton(cancelButton, QtWidgets.QDialogButtonBox.RejectRole) + box.addButton(okButton, QtWidgets.QDialogButtonBox.AcceptRole) self.inFrame.addWidget(box, 2, 0) grid.setColumnStretch(0, 1) grid.setRowStretch(0, 1) @@ -261,10 +280,15 @@ def __init__(self, father, oldMainWindow): scroll.setMinimumWidth(content.sizeHint().width() + scroll.verticalScrollBar().sizeHint().width()) self.updatePlot() - def rename(self, name): - self.oldMainWindow.rename(name) - def fontCheckChanged(self, val): + """ + Shows or hides the details of the font settings. + + Parameters + ---------- + val : bool + When True the font details are shown, otherwise they are hidden. + """ if val: # If active self.mainFontLabel.setEnabled(False) self.mainFontSizeEntry.setEnabled(False) @@ -300,6 +324,9 @@ def fontCheckChanged(self, val): self.updatePlot() def updateLegend(self, *args): + """ + Updates the figure legend. + """ if self.legend is None: return if self.legendGroup.isChecked(): @@ -309,18 +336,24 @@ def updateLegend(self, *args): size = self.legendFontSizeEntry.value() else: size = self.mainFontSizeEntry.value() - self.legend = self.ax.legend(orderedLines, orderedLegendText, framealpha = 1.0, loc=self.legendPos, prop={'size': size }) - self.legend.draggable(True) + self.legend = self.ax.legend(orderedLines, orderedLegendText, framealpha=1.0, loc=self.legendPos, prop={'size': size}) + try: + self.legend.set_draggable(True) + except AttributeError: + self.legend.draggable(True) # For older Matplotlib versions else: self.legend.set_visible(False) def updatePlot(self, *args): + """ + Updates the plot. + """ if self.fontDetailsCheck.checkState(): # If details checked self.fig.suptitle(self.titleEntry.text(), fontsize=self.titleFontSizeEntry.value()) self.ax.set_xlabel(self.xlabelEntry.text(), fontsize=self.xlabelFontSizeEntry.value()) self.ax.set_ylabel(self.ylabelEntry.text(), fontsize=self.ylabelFontSizeEntry.value()) - self.ax.set_xlim((safeEval(self.xlimLeftEntry.text(),type = 'FI'), safeEval(self.xlimRightEntry.text(),type = 'FI'))) - self.ax.set_ylim((safeEval(self.ylimLeftEntry.text(),type = 'FI'), safeEval(self.ylimRightEntry.text(),type = 'FI'))) + self.ax.set_xlim((safeEval(self.xlimLeftEntry.text(), Type='FI'), safeEval(self.xlimRightEntry.text(), Type='FI'))) + self.ax.set_ylim((safeEval(self.ylimLeftEntry.text(), Type='FI'), safeEval(self.ylimRightEntry.text(), Type='FI'))) self.ax.tick_params(axis='x', labelsize=self.xtickFontSizeEntry.value()) self.ax.xaxis.get_offset_text().set_fontsize(self.xtickFontSizeEntry.value()) self.ax.tick_params(axis='y', labelsize=self.ytickFontSizeEntry.value()) @@ -329,8 +362,8 @@ def updatePlot(self, *args): self.fig.suptitle(self.titleEntry.text(), fontsize=self.mainFontSizeEntry.value()) self.ax.set_xlabel(self.xlabelEntry.text(), fontsize=self.mainFontSizeEntry.value()) self.ax.set_ylabel(self.ylabelEntry.text(), fontsize=self.mainFontSizeEntry.value()) - self.ax.set_xlim((safeEval(self.xlimLeftEntry.text(),type = 'FI'), safeEval(self.xlimRightEntry.text(),type = 'FI'))) - self.ax.set_ylim((safeEval(self.ylimLeftEntry.text(),type = 'FI'), safeEval(self.ylimRightEntry.text(),type = 'FI'))) + self.ax.set_xlim((safeEval(self.xlimLeftEntry.text(), Type='FI'), safeEval(self.xlimRightEntry.text(), Type='FI'))) + self.ax.set_ylim((safeEval(self.ylimLeftEntry.text(), Type='FI'), safeEval(self.ylimRightEntry.text(), Type='FI'))) self.ax.tick_params(axis='x', labelsize=self.mainFontSizeEntry.value()) self.ax.xaxis.get_offset_text().set_fontsize(self.mainFontSizeEntry.value()) self.ax.tick_params(axis='y', labelsize=self.mainFontSizeEntry.value()) @@ -343,11 +376,23 @@ def updatePlot(self, *args): self.canvas.adjustSize() def pickHandler(self, pickEvent): + """ + The handler for the peak picking in the save figure window. + + Parameters + ---------- + pickEvent : QEvent + The peak picking event. + """ if pickEvent.mouseevent.dblclick and (pickEvent.mouseevent.button == 1): if isinstance(pickEvent.artist, matplotlib.lines.Line2D): EditLineWindow(self, pickEvent.artist) def exFile(self): + """ + Executes an external file. + Note: Any code in the file is executed, so the file should only come from trusted sources. + """ warning_msg = "This is an advanced feature. Do not execute files you haven't inspected yourself. Are you sure you want to continue?" reply = QtWidgets.QMessageBox.question(self, 'Warning', warning_msg, QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No) if reply == QtWidgets.QMessageBox.Yes: @@ -364,15 +409,13 @@ def exFile(self): self.canvas.draw() def get_mainWindow(self): + """Returns the mainwindow that has been replaced by the save figure window.""" return self.oldMainWindow - def get_masterData(self): - return self.oldMainWindow.get_masterData() - - def get_current(self): - return self.oldMainWindow.get_current() - def kill(self): + """ + Closes the save figure window. + """ for i in reversed(range(self.grid.count())): item = self.grid.itemAt(i).widget() if item is not None: @@ -384,6 +427,9 @@ def kill(self): self.deleteLater() def save(self): + """ + Asks the user for a filename and saves the plot in the format set in the window. + """ self.updatePlot() self.fig.set_size_inches(self.widthEntry.value() / 2.54, self.heightEntry.value() / 2.54) WorkspaceName = self.father.workspaceNames[self.father.workspaceNum] # Set name of file to be saved to workspace name to start @@ -403,6 +449,9 @@ def save(self): fd.write(s.replace('stroke-miterlimit:100000;', '')) def cancel(self): + """ + Closes the save figure by the cancel button. + """ if self.oldMainWindow.current.viewSettings['showTitle']: self.fig.suptitle(self.titleBackup, fontsize=self.titleFontSizeBackup) else: @@ -429,8 +478,19 @@ def cancel(self): class LegendWindow(QtWidgets.QWidget): + """ + The window with the settings of the plot legend. + """ def __init__(self, parent): + """ + Initializes the legend window. + + Parameters + ---------- + parent : SaveFigureWindow + The save figure window from which the legend window is called. + """ super(LegendWindow, self).__init__(parent) self.setWindowFlags(QtCore.Qt.Window | QtCore.Qt.Tool) self.father = parent @@ -469,11 +529,22 @@ def __init__(self, parent): self.setGeometry(self.frameSize().width() - self.geometry().width(), self.frameSize().height() - self.geometry().height(), 0, 0) def changeEdit(self, num): + """ + Changes the line for which the legend label is edited. + + Parameters + ---------- + num : int + The line number. + """ for i in range(len(self.legendEditList)): self.legendEditList[i].setVisible(False) self.legendEditList[num].setVisible(True) def preview(self, *args): + """ + Preview the changes made to the legend. + """ tmp = copy.deepcopy(self.father.legendTextList) order = eval(self.orderEntry.text()) for i in range(len(self.legendEditList)): @@ -486,14 +557,23 @@ def preview(self, *args): orderedLines = [self.father.ax.lines[x] for x in order] orderedLegendText = [tmp[x] for x in order] self.father.ax.legend(orderedLines, orderedLegendText, loc=inp) - self.father.legend.draggable(True) + try: + self.father.legend.set_draggable(True) + except AttributeError: + self.father.legend.draggable(True) # For older Matplotlib versions self.father.canvas.draw() def closeEvent(self, *args): + """ + Closes the legend window. + """ self.deleteLater() self.father.updatePlot() def applyAndClose(self): + """ + Applies the changes made to the legend and closes the window. + """ for i in range(len(self.legendEditList)): self.father.legendTextList[i] = self.legendEditList[i].text() self.father.legendOrder = eval(self.orderEntry.text()) @@ -510,8 +590,21 @@ def applyAndClose(self): class EditLineWindow(QtWidgets.QWidget): + """ + The window for editing the line styles. + """ def __init__(self, parent, line=None): + """ + Initializes the line edit window. + + Parameters + ---------- + parent : SaveFigureWindow + The save figure window from which the line edit window is called. + line : Line2D, optional + The line for which to show the line style. + """ super(EditLineWindow, self).__init__(parent) self.setWindowFlags(QtCore.Qt.Window | QtCore.Qt.Tool) self.father = parent @@ -576,6 +669,9 @@ def __init__(self, parent, line=None): self.show() def setup(self): + """ + Stores backup information about the styles to restore the figure. + """ self.backupColor = colorConverter.to_rgba(self.line.get_color()) self.backupLineWidth = self.line.get_linewidth() self.lwSpinBox.setValue(self.backupLineWidth) @@ -591,11 +687,22 @@ def setup(self): self.msSpinBox.setValue(self.backupMarkerSize) def setIndex(self, val): + """ + Changes the line of which the information is shown. + + Parameters + ---------- + val : int + The line number of which to show information. + """ self.reset() self.line = self.lineList[val] self.setup() def setColor(self, *args): + """ + Sets the color of the selected line. + """ color = QtGui.QColor() color.setRgbF(*self.backupColor) color = QtWidgets.QColorDialog.getColor(color, self, 'Color', QtWidgets.QColorDialog.ColorDialogOption(1)) @@ -605,6 +712,9 @@ def setColor(self, *args): self.canvas.draw() def setEdgeColor(self, *args): + """ + Sets the edgecolor of the selected line. + """ color = QtGui.QColor() color.setRgbF(*self.backupEdgeColor) color = QtWidgets.QColorDialog.getColor(color, self, 'Edgecolor', QtWidgets.QColorDialog.ColorDialogOption(1)) @@ -614,6 +724,9 @@ def setEdgeColor(self, *args): self.canvas.draw() def setFaceColor(self, *args): + """ + Sets the facecolor of the selected line. + """ color = QtGui.QColor() color.setRgbF(*self.backupFaceColor) color = QtWidgets.QColorDialog.getColor(color, self, 'Facecolor', QtWidgets.QColorDialog.ColorDialogOption(1)) @@ -623,22 +736,57 @@ def setFaceColor(self, *args): self.canvas.draw() def setLineWidth(self, val): + """ + Sets the linewidth of the selected line. + + Parameters + ---------- + val : float + Linewidth in points. + """ self.line.set_linewidth(val) self.canvas.draw() def setLineStyle(self, val): + """ + Sets the line style of the selected line. + + Parameters + ---------- + val : {'-', '--', '-.', ':', '', (offset, on-off-seq), ...} + The line style. + """ self.line.set_linestyle(self.LINESTYLES[val]) self.canvas.draw() def setMarker(self, val): + """ + Sets the marker of the selected line. + + Parameters + ---------- + val : str + The marker style. + """ self.line.set_marker(self.MARKERSTYLES[val]) self.canvas.draw() def setMarkerSize(self, val): + """ + Sets the marker size of the selected line. + + Parameters + ---------- + val : float + The marker size in points. + """ self.line.set_markersize(val) self.canvas.draw() def reset(self): + """ + Resets the line to the backup values. + """ self.line.set_color(self.backupColor) self.line.set_linewidth(self.backupLineWidth) self.line.set_linestyle(self.backupLineStyle) @@ -649,12 +797,21 @@ def reset(self): self.canvas.draw() def closeEvent(self, *args): + """ + Closes the line edit window. + """ self.deleteLater() self.father.updatePlot() def apply(self): + """ + Applies the changes to the line. + """ self.setup() def cancelAndClose(self): + """ + Cancels the changes to the line and closes the line edit window. + """ self.reset() self.closeEvent() diff --git a/src/simFunctions.py b/src/simFunctions.py index 2dfa2531..351b8690 100644 --- a/src/simFunctions.py +++ b/src/simFunctions.py @@ -17,22 +17,34 @@ # You should have received a copy of the GNU General Public License # along with ssNake. If not, see . -import numpy as np import tempfile import os import shutil import subprocess +import numpy as np from safeEval import safeEval import functions as func import specIO as io -import Czjzek as Czjzek +import Czjzek class SimException(Exception): pass def d2tens(b): + """ + Calculates the Wigner (small) d-matrices of rank 2 for an array of beta angles. + + Parameters + ---------- + b : ndarray + A 1-D array with beta angles in radians. + + Returns + ------- + ndarray + A 3-D matrix with the wigner d-matrices as a function of the beta angles. + """ cosb = np.cos(b) - sinb = np.sin(b) cosb2 = np.cos(b/2.0) sinb2 = np.sin(b/2.0) # -2 @@ -57,15 +69,46 @@ def d2tens(b): return np.rollaxis(d, 2, 0) def D2tens(a, b, g): + """ + Calculates the Wigner D-matrices of rank 2 for an array of alpha, beta, gamma angles. + + Parameters + ---------- + a : ndarray + A 1-D array with alpha angles in radians. + b : ndarray + A 1-D array with beta angles in radians. + Should have the same length as a. + g : ndarray + A 1-D array with gamma angles in radians. + Should have the same length as a. + + Returns + ------- + ndarray + A 3-D matrix with the wigner d-matrices as a function of the beta angles. + """ d = d2tens(b) m = np.arange(-2, 3) - atmp = np.exp(1j * m[:,np.newaxis] * a[:,np.newaxis,np.newaxis]) - gtmp = np.exp(1j * m * g[:,np.newaxis,np.newaxis]) + atmp = np.exp(1j * m[:, np.newaxis] * a[:, np.newaxis, np.newaxis]) + gtmp = np.exp(1j * m * g[:, np.newaxis, np.newaxis]) return atmp * gtmp * d def d4tens(b): + """ + Calculates the Wigner (small) d-matrices of rank 4 for an array of beta angles. + + Parameters + ---------- + b : ndarray + A 1-D array with beta angles in radians. + + Returns + ------- + ndarray + A 3-D matrix with the wigner d-matrices as a function of the beta angles. + """ cosb = np.cos(b) - sinb = np.sin(b) sinb2 = np.sin(b/2.0) cosb2 = np.cos(b/2.0) # -4 @@ -102,47 +145,136 @@ def d4tens(b): d34 = -2 * np.sqrt(2) * sinb2 * cosb2**7 # 4 d44 = cosb2**8 - d = np.array([[d44, d34, d24, d14, d04, dm14, dm24, dm34, dm44], + d = np.array([[d44, d34, d24, d14, d04, dm14, dm24, dm34, dm44], [-d34, d33, d23, d13, d03, dm13, dm23, dm33, dm34], [d24, -d23, d22, d12, d02, dm12, dm22, dm23, dm24], - [-d14, d13, -d12, d11, d01, dm11, dm12, dm13, dm14], + [-d14, d13, -d12, d11, d01, dm11, dm12, dm13, dm14], [d04, -d03, d02, -d01, d00, d01, d02, d03, d04], [-dm14, dm13, -dm12, dm11, -d01, d11, d12, d13, d14], [dm24, -dm23, dm22, -dm12, d02, -d12, d22, d23, d24], [-dm34, dm33, -dm23, dm13, -d03, d13, -d23, d33, d34], [dm44, -dm34, dm24, -dm14, d04, -d14, d24, -d34, d44]]) - return np.rollaxis(d, 2, 0) # make the b values lie along the first dim + return np.rollaxis(d, 2, 0) def D4tens(a, b, g): + """ + Calculates the Wigner D-matrices of rank 4 for an array of alpha, beta, gamma angles. + + Parameters + ---------- + a : ndarray + A 1-D array with alpha angles in radians. + b : ndarray + A 1-D array with beta angles in radians. + Should have the same length as a. + g : ndarray + A 1-D array with gamma angles in radians. + Should have the same length as a. + + Returns + ------- + ndarray + A 3-D matrix with the wigner d-matrices as a function of the beta angles. + """ d = d4tens(b) m = np.arange(-4, 5) - atmp = np.exp(1j * m[:,np.newaxis] * a[:,np.newaxis,np.newaxis]) - gtmp = np.exp(1j * m * g[:,np.newaxis,np.newaxis]) + atmp = np.exp(1j * m[:, np.newaxis] * a[:, np.newaxis, np.newaxis]) + gtmp = np.exp(1j * m * g[:, np.newaxis, np.newaxis]) return atmp * gtmp * d -def csaSpace(delta): # CSA spherical tensor cartesian space functions - # delta in Hz - V00 = - np.sqrt(1.0/3.0) * (delta[0] + delta[1] + delta[2]) - V20 = np.sqrt(1.0/6.0) * (2*delta[2] - delta[0] - delta[1]) +def csaSpace(delta): + """ + The space part of the CSA Hamiltonian defined in irreducible spherical tensor operator format. + + Parameters + ---------- + delta : ndarray + A 1-D array (of length 3) with the CSA tensor values (xyz definition) in Hz. + + Returns + ------- + float + The V00 element. + ndarray + The V2n elements ordered as [V2-2, V2-1, V20, V21, V22]. + """ + V00 = -np.sqrt(1.0/3.0) * (delta[0] + delta[1] + delta[2]) + V20 = np.sqrt(1.0/6.0) * (2*delta[2] - delta[0] - delta[1]) V2pm2 = 0.5 * (delta[1] - delta[0]) return V00, np.array([V2pm2, 0, V20, 0, V2pm2]) -def csaSpin(): # CSA spherical tensor spin space functions - T00 = -np.sqrt(1.0 / 3) # times Iz and B0, but is 1 - T20 = np.sqrt(1.0 / 6.0) * 2 # times Iz and B0, but is 1 +def csaSpin(): + """ + The spin part of the CSA Hamiltonian defined in irreducible spherical tensor operator format. + Does not include the spin operator Iz and the magnetic field B0. + + Returns + ------- + float + The T00 element. + float + The T20 element. + """ + T00 = -np.sqrt(1.0 / 3) + T20 = np.sqrt(1.0 / 6.0) * 2 return T00, T20 def firstQuadSpace(eta): - V20 = 1.0 + """ + The space part of the first order quadrupole Hamiltonian defined in irreducible spherical tensor operator format. + + Parameters + ---------- + eta : float + The eta value of the quadrupole interaction. + + Returns + ------- + ndarray + The V2n elements ordered as [V2-2, V2-1, V20, V21, V22]. + """ + V20 = 1.0 V2pm2 = np.sqrt(1 / 6.0) * eta return np.array([V2pm2, 0, V20, 0, V2pm2]) -def firstQuadSpin(I, m1, m2): # T values +def firstQuadSpin(I, m1, m2): + """ + The spin part of the first order quadrupole Hamiltonian defined in irreducible spherical tensor operator format. + + Parameters + ---------- + I : float + The spin quantum number. + m1, m2: float + The quantum numbers of the energy levels which are involved. + + Returns + ------- + float + The T20 element. + """ spin20_1 = 3 * m1**2 - I * (I + 1) spin20_2 = 3 * m2**2 - I * (I + 1) return spin20_1 - spin20_2 def secQuadSpace(eta): + """ + The space part of the second order quadrupole Hamiltonian defined in irreducible spherical tensor operator format. + + Parameters + ---------- + eta : float + The eta value of the quadrupole interaction. + + Returns + ------- + float + The V00 element. + ndarray + The V2n elements ordered as [V2-2, V2-1, V20, V21, V22]. + ndarray + The V4n elements ordered as [V4-4, V4-3, V4-2, V4-1, V40, V41, V42, V43, V44]. + """ V00 = -1.0 / 5 * (3 + eta**2) V20 = 1.0 / 14 * (eta**2 - 3) V2pm2 = 1.0 / 7 * np.sqrt(3.0 / 2) * eta @@ -151,7 +283,26 @@ def secQuadSpace(eta): V4pm4 = 1 / (4 * np.sqrt(70)) * eta**2 return V00, np.array([V2pm2, 0, V20, 0, V2pm2]), np.array([V4pm4, 0, V4pm2, 0, V40, 0, V4pm2, 0, V4pm4]) -def secQuadSpin(I, m1, m2): # T values +def secQuadSpin(I, m1, m2): + """ + The spin part of the second order quadrupole Hamiltonian defined in irreducible spherical tensor operator format. + + Parameters + ---------- + I : float + The spin quantum number. + m1, m2: float + The quantum numbers of the energy levels which are involved. + + Returns + ------- + float + The T00 element. + float + The T20 element. + float + The T40 element. + """ spin00_1 = m1 * (I * (I + 1) - 3 * m1**2) spin20_1 = m1 * (8 * I * (I + 1) - 12 * m1**2 - 3) spin40_1 = m1 * (18 * I * (I + 1) - 34 * m1**2 - 5) @@ -161,15 +312,102 @@ def secQuadSpin(I, m1, m2): # T values return spin00_1 - spin00_2, spin20_1 - spin20_2, spin40_1 - spin40_2 def relaxationFunc(x, freq, sw, axMult, extra, amp, const, coeff, T): + """ + Simulation function used for fitting relaxation curves. + + Parameters + ---------- + x : list of ndarray + A list of axes values for the simulation. + As this is a 1-D method only the last array in the list is used. + freq : list of float + The list of frequency per dimension in Hz (not used). + sw : list of float + The list of spectral width per dimension in Hz (not used). + axMult : float + The multiplier of the x-axis (not used). + extra : list + The extra parameters of the function (not used). + amp : float + The amplitude of the curve. + const : float + The constant. + coeff : float + The coefficient. + T : float + The relaxation time. Has the same units as x. + + Returns + ------- + ndarray + The relaxation curve. Has the same length as x[-1]. + """ x = x[-1] return amp * (const + coeff * np.exp(-x / abs(T))) def diffusionFunc(x, freq, sw, axMult, extra, amp, const, coeff, D): + """ + Simulation function used for fitting diffusion curves. + + Parameters + ---------- + x : list of ndarray + A list of axes values for the simulation. + As this is a 1-D method only the last array in the list is used. + freq : list of float + The list of frequency per dimension in Hz (not used). + sw : list of float + The list of spectral width per dimension in Hz (not used). + axMult : float + The multiplier of the x-axis (not used). + extra : list + The extra parameters of the function [gamma, delta, triangle]. + Where gamma is the gyromagnetic ratio in MHz/T, delta is the duration of the gradient in seconds, and triangle (capital delta) is the time between the start of gradients in seconds. + amp : float + The amplitude of the curve. + const : float + The constant. + coeff : float + The coefficient. + D : float + The diffusion constant in m^2/s. + + Returns + ------- + ndarray + The diffusion curve. Has the same length as x[-1]. + """ x = x[-1] gamma, delta, triangle = extra - return amp * (const + coeff * np.exp(-(abs(gamma) * abs(delta) * x)**2 * abs(D) * (abs(triangle) - abs(delta) / 3.0))) + return amp * (const + coeff * np.exp(-(abs(gamma) *1e6 * abs(delta) * x)**2 * abs(D) * (abs(triangle) - abs(delta) / 3.0))) def functionRun(x, freq, sw, axMult, extra, *parameters): + """ + Simulation function used for function fitting. + The words between @ symbols are replaced by the fit values and the resulting string is evaluated. + + Parameters + ---------- + x : list of ndarray + A list of axes values for the simulation. + As this is a 1-D method only the last array in the list is used. + freq : list of float + The list of frequency per dimension in Hz (not used). + sw : list of float + The list of spectral width per dimension in Hz (not used). + axMult : float + The multiplier of the x-axis (not used). + extra : list + The extra parameters of the function [names, function]. + Where names is a list of strings with the names of the parameters and function is a string with the function for fitting. + *parameters + The parameters used in the fit. Should have the same length as names. + + Returns + ------- + ndarray + The curve resulting from the evaluation of the function. + """ names, function = extra x = x[-1] for i, elem in enumerate(names): @@ -177,6 +415,39 @@ def functionRun(x, freq, sw, axMult, extra, *parameters): return safeEval(function, length=len(x), x=x) def externalFitRunScript(x, freq, sw, axMult, extra, bgrnd, mult, *parameters): + """ + Simulation function used for external fitting. + The words between @ symbols are replaced by the fit values and the resulting string is used as an input script for the given command. + The output of the command is processed (apodization, Fourier, regrid) as specified. + + Parameters + ---------- + x : list of ndarray + A list of axes values for the simulation. + As this is a 1-D method only the last array in the list is used. + freq : list of float + The list of frequency per dimension in Hz (not used). + sw : list of float + The list of spectral width per dimension in Hz (not used). + axMult : float + The multiplier of the x-axis (not used). + extra : list + The extra parameters of the function [names, command, script, output, spec]. + Where names is a list of strings with the names of the parameters, command is a string with the command for fitting, script is a string with the script to be modified, output is a list of two strings to which the stdout and sterr are written, and spec is a boolean which is True when the output should be a spectrum. + bgrnd : float + The offset value added to the output curve. + mult : float + The value by which the output curve is multiplied. + *parameters + The parameters used in the fit. + The last three values are interpreted as [amp, lor, gauss] and are used as the amplitude, the Lorentzian apodization (Hz) and Gaussian apodization (Hz). + Should have be three longer than names. + + Returns + ------- + ndarray + The curve result from the command. + """ names, command, script, output, spec = extra amp, lor, gauss = parameters[-3:] x = x[-1] @@ -202,14 +473,31 @@ def externalFitRunScript(x, freq, sw, axMult, extra, bgrnd, mult, *parameters): outputFileName = fileList[0] masterData = io.autoLoad(os.path.join(directory_name, outputFileName)) masterData.noUndo = True - masterData.apodize(lor, gauss, [None,None], 0, 0, 0, 0, 0) + masterData.apodize(lor, gauss, [None, None], 0, 0, 0, 0, 0) if masterData.spec[0] != spec: - masterData.fourier(0) + masterData.complexFourier(0) masterData.regrid([x[0], x[-1]], len(x), 0) shutil.rmtree(directory_name, ignore_errors=True) return mult * amp * np.real(masterData.getHyperData(0)) def fib(n): + """ + Calculates three Fibonacci numbers starting from a given point in the series. + + Parameters + ---------- + n : int + Start at the nth Fibonacci number. + + Returns + ------- + int + The n Fibonacci number. + int + The n+1 Fibonacci number. + int + The n+2 Fibonacci number. + """ start = np.array([[1, 1], [1, 0]], dtype='int64') temp = start[:] for i in range(n): @@ -217,24 +505,77 @@ def fib(n): return temp[0, 0], temp[0, 1], temp[1, 1] def zcw_angles(m, symm=0): + """ + Calculates two angle sets for powder averaging based on the Zaremba, Conroy, and Wolfsberg (ZCW) method. + The number of orientations depends on the given Cheng number. + + Parameters + ---------- + m : int + The Cheng number. + The number of orientations is equal to the m+2 Fibonacci number. + symm : {0, 1, 2}, optional + The symmetry of the problem. When 0 the orientations run over the entire sphere, when 1 the orientations run over a hemispere, and when 2 the orientations run over and octant. + + Returns + ------- + ndarray + The phi angles. + ndarray + The theta angles. + ndarray + The weights of the different orientations. + """ samples, fib_1, fib_2 = fib(m) js = np.arange(samples, dtype='Float64') / samples if symm == 0: - # full c = (1., 2., 1.) elif symm == 1: - # hemi c = (-1., 1., 1.) elif symm == 2: - # oct c = (-1., 1., 4.) j_samples = fib_2 * js phi = 2 * np.pi / c[2] * np.mod(j_samples, 1.0) theta = np.arccos(c[0] * (c[1] * np.mod(js, 1.0) - 1)) weight = np.ones(samples) / samples return phi, theta, weight - + def peakSim(x, freq, sw, axMult, extra, bgrnd, mult, pos, amp, lor, gauss): + """ + Simulates an FID with Lorentzian and Gaussian broadening. + + Parameters + ---------- + x : list of ndarray + A list of axes values for the simulation. + As this is a 1-D method only the last array in the list is used. + freq : list of float + The list of frequency per dimension in Hz (not used). + sw : list of float + The list of spectral width per dimension in Hz. + The last value is used to determine the dwell time. + axMult : float + The multiplier of the x-axis. + extra : list + The extra parameters of the function (not used). + bgrnd : float + The offset value added to the FID. + mult : float + The value by which the FID is multiplied. + pos : float + The frequency of the peak (in Hz*axMult). + amp : float + The amplitude of the peak. + lor : float + The Lorentzian broadening of the peak. + gauss : float + The Gaussian broadening of the peak. + + Returns + ------- + ndarray + The simulated FID + """ x = x[-1] pos /= axMult if pos < np.min(x) or pos > np.max(x): @@ -246,17 +587,69 @@ def peakSim(x, freq, sw, axMult, extra, bgrnd, mult, pos, amp, lor, gauss): return float(mult) * float(amp) / sw[-1] * np.exp(2j * np.pi * (pos - x[length//2]) * t - np.pi * np.abs(lor * t) - ((np.pi * np.abs(gauss) * t)**2) / (4 * np.log(2))) def makeSpectrum(x, sw, v, gauss, lor, weight): - # Takes axis, frequencies and intensities and makes a spectrum with lorentz and gaussian broadening + """ + Creates an FID from a list of frequencies with corresponding weights. + Also applies Lorentzian and Gaussian broadening. + + Parameters + ---------- + x : list of ndarray + A list of axes values for the simulation. + As this is a 1-D method only the last array in the list is used. + sw : list of float + The list of spectral width per dimension in Hz. + v : ndarray + The list of frequencies to be used in the FID. + gauss : float + Gaussian broadening in Hz. + lor : float + Lorentzian broadening in Hz. + weight : ndarray + The weights corresponding to the frequencies. Should have the same length as v. + + Returns + ------- + ndarray + The FID. Has the same length as x[-1]. + """ length = len(x) - t = np.abs(np.fft.fftfreq(length, sw/float(length))) + t = np.abs(np.fft.fftfreq(length, sw / float(length))) diff = (x[1] - x[0]) * 0.5 - final, junk = np.histogram(v, length, range=[x[0]-diff, x[-1]+diff], weights=weight) + final, _ = np.histogram(v, length, range=[x[0]-diff, x[-1]+diff], weights=weight) apod = np.exp(-np.pi * np.abs(lor) * t - ((np.pi * np.abs(gauss) * t)**2) / (4 * np.log(2))) inten = np.fft.ifft(final) * apod inten *= len(inten) / sw return inten def makeMQMASSpectrum(x, sw, v, gauss, lor, weight): + """ + Creates an 2D FID from a list of frequencies with corresponding weights. + Also applies Lorentzian and Gaussian broadening. + + Parameters + ---------- + x : list of ndarray + A list of axes values for the simulation. + As this is a 2-D method only the last two arrays in the list are used for the first and second dimension respectively. + sw : list of float + The list of spectral width per dimension in Hz. + The second last value is used for the first dimension and the last value is used for the second dimension. + v : list of ndarray + The list of frequencies to be used in the 2D FID. + The first ndarray contains in frequencies in the first (indirect) dimension and the second the frequencies in the second (direct) dimension. + Both ndarray should have equal length. + gauss : array_like + Gaussian broadening in Hz for the first and second dimension. + lor : array_like + Lorentzian broadening in Hz for the first and second dimension. + weight : ndarray + The weights corresponding to the frequencies. Should have the same length as the arrays in v. + + Returns + ------- + ndarray + The 2D FID. Has a shape of (len(x[-2]), len(x[-1])). + """ length1 = len(x[-2]) t1 = np.fft.fftfreq(length1, sw[-2]/float(length1)) diff1 = (x[-2][1] - x[-2][0])*0.5 @@ -264,76 +657,146 @@ def makeMQMASSpectrum(x, sw, v, gauss, lor, weight): t2 = np.fft.fftfreq(length2, sw[-1]/float(length2)) diff2 = (x[-1][1] - x[-1][0])*0.5 t1 = t1[:, np.newaxis] - final, junk, junk = np.histogram2d(v[0], v[1], [length1, length2], range=[[x[-2][0]-diff1, x[-2][-1]+diff1], [x[-1][0]-diff2, x[-1][-1]+diff2]], weights=weight) + final, _, _ = np.histogram2d(v[0], v[1], [length1, length2], range=[[x[-2][0]-diff1, x[-2][-1]+diff1], [x[-1][0]-diff2, x[-1][-1]+diff2]], weights=weight) final = np.fft.ifftn(final) apod2 = np.exp(-np.pi * np.abs(lor[1] * t2) - ((np.pi * np.abs(gauss[1]) * t2)**2) / (4 * np.log(2))) apod1 = np.exp(-np.pi * np.abs(lor[0] * t1) - ((np.pi * np.abs(gauss[0]) * t1)**2) / (4 * np.log(2))) final *= apod1 * apod2 * length1 / sw[-2] * length2 / sw[-1] return final -def carouselAveraging(spinspeed, numssb, v, weight, vConstant): - # Perform carousel averaging for finite MAS powder patterns +def carouselAveraging(spinspeed, v, weight, vConstant): + """ + Performs carousel averaging for finite spinning samples. + + Parameters + ---------- + spinspeed : float + The spinning speed in Hz. + v : 2-D ndarray + The anisotropic part of the frequency. + The first dimension contains the contributions of different alpha and beta angles. + The second dimension contains a full rotation over gamma. + weight : array_like + Gaussian broadening in Hz for the first and second dimension. + vConstant : float + The offset frequency (isotropic value). + + Returns + ------- + ndarray + The 2D FID. Has a shape of (len(x[-2]), len(x[-1])). + """ + numssb = v.shape[1] dt = 1.0 / spinspeed / numssb prod = np.exp(1j * np.cumsum(v * dt * 2 * np.pi, axis=1)) tot = np.fft.fft(prod, axis=1) tot *= np.conj(tot) - weight2 = weight[:,np.newaxis] / numssb**2 + weight2 = weight[:, np.newaxis] / numssb**2 tot *= weight2 v = np.fft.fftfreq(numssb, 1.0 / numssb) * spinspeed - v = v + vConstant[:,np.newaxis] - return v, tot + return v + vConstant[:, np.newaxis], tot def csaFreqBase(angle, tensor, D2, spinspeed, numssb): - d2 = d2tens(np.array([angle]))[0,:,2] + """ + Calculates the CSA frequencies for given alpha and beta angles and over a full circle over gamma. + + Parameters + ---------- + angle : float + The spinning angle in radians. + tensor : array_like + The three CSA tensor values in the xyz format (in Hz). + D2 : array_like + The second rank wigner rotation matrices used to calculate the CSA frequencies. + The first dimension should contain the angular dependence. + spinspeed : float + The spinning frequency in Hz. + numssb : int + The number of alpha angles to calculate. + + Returns + ------- + ndarray + The array with frequencies. Has the same length as D2. + float + The isotropic frequency. + """ + d2 = d2tens(np.array([angle]))[0, :, 2] A0, A2 = csaSpace(tensor) T0, T2 = csaSpin() dat0 = A0 * T0 dat2 = A2 * T2 dat2 = np.matmul(dat2, D2) factor2 = d2[2] - vConstant = 0 + vConstant = 0.0 if spinspeed == np.inf: - v = np.real(dat2[:,2] * factor2 + dat0) + v = np.real(dat2[:, 2] * factor2 + dat0) elif spinspeed == 0.0: - v = np.real(dat2[:,2] + dat0) + v = np.real(dat2[:, 2] + dat0) else: gammastep = 2 * np.pi / numssb gval = np.arange(numssb) * gammastep - spinD2 = np.exp(1j * np.arange(-2, 3)[:,np.newaxis] * gval) * d2[:,np.newaxis] - vConstant = np.real(dat0 + dat2[:,2] * factor2) - dat2[:,2] = 0 + spinD2 = np.exp(1j * np.arange(-2, 3)[:, np.newaxis] * gval) * d2[:, np.newaxis] + vConstant = np.real(dat0 + dat2[:, 2] * factor2) + dat2[:, 2] = 0 v = np.matmul(dat2, spinD2) return v, vConstant -def csaFreq(angle, tensor, D2, spinspeed, numssb, weight): - tot = weight - v, vConstant = csaFreqBase(angle, tensor, D2, spinspeed, numssb) - if spinspeed != np.inf and spinspeed != 0.0: - v, tot = carouselAveraging(spinspeed, numssb, v, weight, vConstant) - return v, tot - def csaFunc(x, freq, sw, axMult, extra, bgrnd, mult, spinspeed, t11, t22, t33, amp, lor, gauss): + """ + Uses the quadCSAFunc function for the specific case where the quadrupole interaction is zero. + """ shiftdef, numssb, angle, D2, weight, MAStype = extra extra = [False, 0.5, numssb, angle, D2, None, weight, MAStype, shiftdef] return quadCSAFunc(x, freq, sw, axMult, extra, bgrnd, mult, spinspeed, t11, t22, t33, 0.0, 0.0, 0.0, 0.0, 0.0, amp, lor, gauss) - + def quadFunc(x, freq, sw, axMult, extra, bgrnd, mult, spinspeed, pos, cq, eta, amp, lor, gauss): + """ + Uses the quadCSAFunc function for the specific case where the CSA interaction is zero. + """ satBool, I, numssb, angle, D2, D4, weight, MAStype = extra extra = [satBool, I, numssb, angle, D2, D4, weight, MAStype, 0] return quadCSAFunc(x, freq, sw, axMult, extra, bgrnd, mult, spinspeed, pos, pos, pos, cq, eta, 0.0, 0.0, 0.0, amp, lor, gauss) -def quadFreq(I, m1, m2, spinspeed, numssb, angle, D2, D4, weight, freq, cq, eta): +def quadFreqBase(I, m1, m2, cq, eta, freq, angle, D2, D4, numssb, spinspeed): + """ + Calculates the quadrupole frequencies for given alpha and beta angles and over a full circle over gamma. + + Parameters + ---------- + I : float + The spin quantum number. + m1, m2 : float + The levels between which to calculate the frequency. + cq : float + The Cq value of the quadrupole coupling in Hz. + eta : float + The asymmetry parameter of the quadrupole coupling. + freq : float + The Larmor frequency of the nucleus in Hz. + angle : float + The spinning angle in radians. + D2 : array_like + The second rank wigner rotation matrices used to calculate the quadrupole frequencies. + The first dimension should contain the angular dependence. + D4 : array_like + The fourth rank wigner rotation matrices used to calculate the quadrupole frequencies. + The first dimension should contain the angular dependence. + Should have the same length as D2. + numssb : int + The number of alpha angles to calculate. + spinspeed : float + The spinning frequency in Hz. + + Returns + ------- + ndarray + The array with frequencies. Has the same length as D2 and D4. + float + The isotropic frequency. + """ if freq == 0.0: raise SimException("Sim: Frequency cannot be zero") - tot = weight - v, vConstant = quadFreqBase(I, m1, m2, cq, eta, freq, angle, D2, D4, numssb, spinspeed) - if spinspeed != np.inf and spinspeed != 0.0: - v, tot = carouselAveraging(spinspeed, numssb, v, weight, vConstant) - return v, tot - -def quadFreqBase(I, m1, m2, cq, eta, freq, angle, D2, D4, numssb, spinspeed): - #Establish the frequencies for all powder orientations. - #Prepare for carousel averaging, but do not perform yet (return gamma angle depend acne) pre2 = -cq**2 / (4 * I *(2 * I - 1))**2 * 2 / freq pre1 = cq / (4 * I *(2 * I - 1)) firstA2 = pre1 * firstQuadSpace(eta) @@ -341,8 +804,8 @@ def quadFreqBase(I, m1, m2, cq, eta, freq, angle, D2, D4, numssb, spinspeed): secA0 *= pre2 secA2 *= pre2 secA4 *= pre2 - d2 = d2tens(np.array([angle]))[0,:,2] - d4 = d4tens(np.array([angle]))[0,:,4] + d2 = d2tens(np.array([angle]))[0, :, 2] + d4 = d4tens(np.array([angle]))[0, :, 4] factor2 = d2[2] factor4 = d4[4] firstspin2 = firstQuadSpin(I, m1, m2) @@ -354,25 +817,76 @@ def quadFreqBase(I, m1, m2, cq, eta, freq, angle, D2, D4, numssb, spinspeed): dat4 = np.matmul(dat4, D4) vConstant = 0 if spinspeed == np.inf: - v = np.real(dat4[:,4] * factor4 + dat2[:,2] * factor2 + dat0) + v = np.real(dat4[:, 4] * factor4 + dat2[:, 2] * factor2 + dat0) elif spinspeed == 0.0: - v = np.real(dat4[:,4] + dat2[:,2] + dat0) + v = np.real(dat4[:, 4] + dat2[:, 2] + dat0) else: gammastep = 2 * np.pi / numssb gval = np.arange(numssb) * gammastep - spinD2 = np.exp(1j * np.arange(-2, 3)[:,np.newaxis] * gval) * d2[:,np.newaxis] - spinD4 = np.exp(1j * np.arange(-4, 5)[:,np.newaxis] * gval) * d4[:,np.newaxis] - vConstant = np.real(dat0 + dat2[:,2] * factor2 + dat4[:,4] * factor4) - dat4[:,4] = 0 - dat2[:,2] = 0 + spinD2 = np.exp(1j * np.arange(-2, 3)[:, np.newaxis] * gval) * d2[:, np.newaxis] + spinD4 = np.exp(1j * np.arange(-4, 5)[:, np.newaxis] * gval) * d4[:, np.newaxis] + vConstant = np.real(dat0 + dat2[:, 2] * factor2 + dat4[:, 4] * factor4) + dat4[:, 4] = 0 + dat2[:, 2] = 0 v = np.matmul(dat2, spinD2) + np.matmul(dat4, spinD4) return v, vConstant def quadCSAFunc(x, freq, sw, axMult, extra, bgrnd, mult, spinspeed, t11, t22, t33, cq, eta, alphaCSA, betaCSA, gammaCSA, amp, lor, gauss): - # Degrees to radians - alphaCSA *= np.pi/180.0 - betaCSA *= np.pi/180.0 - gammaCSA *= np.pi/180.0 + """ + Calculates an FID of a powder averaged site under influence of CSA and a quadrupole interaction. + This function works for static, finite, and infinite spinnning. + + Parameters + ---------- + x : list of ndarray + A list of axes values for the simulation. + As this is a 1-D method only the last array in the list is used. + freq : list of float + The list of frequency per dimension in Hz. Only the last value is used. + sw : list of float + The list of spectral width per dimension in Hz. Only the last value is used. + axMult : float + The multiplier of the x-axis. + extra : list + The extra parameters defined as [satBool, I, numssb, angle, D2, D4, weight, MAStype, shiftdef]. + satBool is a boolean when True will include the quadrupole satellites. + I is the spin quantum number. + numssb is the number of sidebands to be simulated. + angle is the spinning angle in radians. + D2 is the second rank wigner rotation matrices. + D4 is the fourth rank wigner rotation matrices. + weight are the weights corresponding to the orientations of D2 and D4. + MAStype=0 performs a static simulation, MAStype=1 performs a finite spinning simulation, and MAStype=2 performs an infinite spinning simulation. + shiftdef is the definition in which t11, t22, and t33 are given (see shiftConversion). + bgrnd : float + The offset value added to the FID. + mult : float + The value by which the FID is multiplied. + spinspeed : float + The spinning speed in kHz. + t11, t22, t33 : float + The tensor values given in the definition specified by shiftdef. + cq : float + The quadrupole coupling constant Cq given in MHz. + eta : float + The asymmetry parameter of the quadrupole coupling. + alphaC, betaC, alphaC : float + The angles of the relative orientation of the CSA tensor with respect to the quadrupole tensor given in degrees. + amp : float + The amplitude of the peak. + lor : float + The Lorentzian broadening of the peak. + gauss : float + The Gaussian broadening of the peak. + + Returns + ------- + ndarray + The simulated FID + """ + alphaCSA *= np.pi / 180.0 # Degrees to radians + betaCSA *= np.pi / 180.0 # Degrees to radians + gammaCSA *= np.pi / 180.0 # Degrees to radians x = x[-1] satBool, I, numssb, angle, D2, D4, weight, MAStype, shiftdef = extra if MAStype == 0: @@ -380,17 +894,15 @@ def quadCSAFunc(x, freq, sw, axMult, extra, bgrnd, mult, spinspeed, t11, t22, t3 elif MAStype == 2: spinspeed = np.inf if not satBool and (I % 1) == 0.0: - # Integer spins have no central transition - return np.zeros_like(x) - if shiftdef == 2: #if heaberlen, make eta continuous, and between 0--1 + return np.zeros_like(x) # Integer spins have no central transition + if shiftdef == 2: # If heaberlen, make eta continuous, and between 0--1 t33 = 1 - abs(abs(t33) % 2 - 1) - elif shiftdef == 3: #For Hertzfeld-Berger + elif shiftdef == 3: # For Hertzfeld-Berger t33 = 1 - abs(abs(t33 + 1)%4 - 2) tensor = np.array(func.shiftConversion([t11, t22, t33], shiftdef)[1], dtype=float) tensor /= float(axMult) cq *= 1e6 - #Force eta to 0--1 in a continuous way: 0.9 == 1.1, 0 == 2 - eta = 1 - abs(abs(eta) % 2 - 1) + eta = 1 - abs(abs(eta) % 2 - 1) # Force eta to 0--1 in a continuous way: 0.9 == 1.1, 0 == 2 spinspeed *= 1e3 freq = freq[-1] sw = sw[-1] @@ -412,14 +924,57 @@ def quadCSAFunc(x, freq, sw, axMult, extra, bgrnd, mult, spinspeed, t11, t22, t3 v += vCSA vConstant += vConstantCSA tot = weight - if spinspeed != np.inf and spinspeed != 0.0: - v, tot = carouselAveraging(spinspeed, numssb, v, weight, vConstant) + if spinspeed not in (0.0, np.inf): + v, tot = carouselAveraging(spinspeed, v, weight, vConstant) spectrum += eff * makeSpectrum(x, sw, v, gauss, lor, tot) return mult * amp * spectrum def quadCzjzekFunc(x, freq, sw, axMult, extra, bgrnd, mult, pos, sigma, cq0, eta0, amp, lor, gauss): + """ + Calculates an FID of a quadrupole spectrum with an (extended) Czjzek distribution using a library of spectra. + + Parameters + ---------- + x : list of ndarray + A list of axes values for the simulation. + As this is a 1-D method only the last array in the list is used. + freq : list of float + The list of frequency per dimension (not used). + sw : list of float + The list of spectral width per dimension in Hz. Only the last value is used. + axMult : float + The multiplier of the x-axis. + extra : list + The extra parameters defined as [method, d, lib, cq, eta]. + method is a boolean when True the extended version of the Czjzek distribution is used. + d is the order parameter of the distribution. + lib is the library (array) of FIDs. + cq (in Hz) and eta are the quadrupole parameters corresponding to the FIDs in lib and both should have the same length as lib. + bgrnd : float + The offset value added to the FID. + mult : float + The value by which the FID is multiplied. + pos : float + The isotropic chemical shift in the units defined by axMult. + sigma : float + The width of the Czjzek distribution in MHz. + cq0 : float + The extended Czjzek quadrupole coupling constant Cq0 given in MHz (only used when method=True). + eta0 : float + The extended Czjzek asymmetry parameter of the quadrupole coupling (only used when method=True). + amp : float + The amplitude of the peak. + lor : float + The Lorentzian broadening of the peak. + gauss : float + The Gaussian broadening of the peak. + + Returns + ------- + ndarray + The simulated FID + """ x = x[-1] - freq = freq[-1] sw = sw[-1] method, d, lib, cq, eta = extra if method == 0: @@ -437,8 +992,63 @@ def quadCzjzekFunc(x, freq, sw, axMult, extra, bgrnd, mult, pos, sigma, cq0, eta return mult * amp * fid * apod def mqmasFunc(x, freq, sw, axMult, extra, bgrnd, mult, spinspeed, pos, cq, eta, amp, lor2, gauss2, lor1, gauss1): - x1 = x[-2] - x2 = x[-1] + """ + Calculates a 2-D FID of an MQMAS spectrum. + This function works for static, finite, and infinite spinnning. + + Parameters + ---------- + x : list of ndarray + A list of axes values for the simulation. + As this is a 2-D method only the last two arrays in the list are used for the first and second dimension respectively. + freq : list of float + The list of frequency per dimension in Hz. + The second last value is used for the first dimension and the last value is used for the second dimension. + sw : list of float + The list of spectral width per dimension in Hz. + The second last value is used for the first dimension and the last value is used for the second dimension. + axMult : float + The multiplier of the x-axis. + extra : list + The extra parameters defined as [I, mq, numssb, angle, D2, D4, weight, shear, scale, MAStype]. + I is the spin quantum number. + mq is the multiple quantum transition that is used in the indirect dimension. + numssb is the number of sidebands to be simulated. + angle is the spinning angle in radians. + D2 is the second rank wigner rotation matrices. + D4 is the fourth rank wigner rotation matrices. + weight are the weights corresponding to the orientations of D2 and D4. + shear is the shearing factor. + scale is the scaling factor of the indirect axis. + MAStype=0 performs a static simulation, MAStype=1 performs a finite spinning simulation, and MAStype=2 performs an infinite spinning simulation. + bgrnd : float + The offset value added to the FID. + mult : float + The value by which the FID is multiplied. + spinspeed : float + The spinning speed in kHz. + pos : float + The isotropic chemical shift value defined in terms of axMult. + cq : float + The quadrupole coupling constant Cq given in MHz. + eta : float + The asymmetry parameter of the quadrupole coupling. + amp : float + The amplitude of the peak. + lor2 : float + The Lorentzian broadening in the direct dimension. + gauss2 : float + The Gaussian broadening in the direct dimension. + lor1 : float + The Lorentzian broadening in the indirect dimension. + gauss1 : float + The Gaussian broadening in the indirect dimension. + + Returns + ------- + ndarray + The simulated 2-D FID + """ freq1 = freq[-2] freq2 = freq[-1] I, mq, numssb, angle, D2, D4, weight, shear, scale, MAStype = extra @@ -450,25 +1060,75 @@ def mqmasFunc(x, freq, sw, axMult, extra, bgrnd, mult, spinspeed, pos, cq, eta, spinspeed *= 1e3 cq *= 1e6 eta = 1 - abs(abs(eta)%2 - 1) - v2, tot2 = quadFreq(I, -0.5, 0.5, spinspeed, numssb, angle, D2, D4, weight, freq2, cq, eta) - v1, tot1 = quadFreq(I, -mq/2.0, mq/2.0, spinspeed, numssb, angle, D2, D4, np.ones_like(weight), freq1, cq, eta) + tot = weight + v2, vConstant2 = quadFreqBase(I, -0.5, 0.5, cq, eta, freq2, angle, D2, D4, numssb, spinspeed) + v1, vConstant1 = quadFreqBase(I, -0.5*mq, 0.5*mq, cq, eta, freq2, angle, D2, D4, numssb, spinspeed) + if spinspeed not in (0.0, np.inf): + v2, tot2 = carouselAveraging(spinspeed, v2, weight, vConstant2) + v1, tot1 = carouselAveraging(spinspeed, v1, np.ones_like(weight), vConstant1) + tot = tot1*tot2 v2 += pos - v1 += mq*pos - v1 -= v2 * shear + v1 += mq*pos - v2 * shear v1 *= scale - return mult * amp * makeMQMASSpectrum(x, sw, [np.real(v1.flatten()), np.real(v2.flatten())], [gauss1, gauss2], [lor1, lor2], np.real(tot1*tot2).flatten()) - -def genLib(length, minCq, maxCq, minEta, maxEta, numCq, numEta, extra, freq, sw, spinspeed): - cq, eta = np.meshgrid(np.linspace(minCq, maxCq, numCq), np.linspace(minEta, maxEta, numEta)) - cq = cq.flatten() - eta = eta.flatten() - x = np.fft.fftshift(np.fft.fftfreq(length, 1/float(sw))) - lib = np.zeros((len(cq), length), dtype=complex) - for i, (cqi, etai) in enumerate(zip(cq, eta)): - lib[i] = quadFunc([x], [freq], [sw], 1.0, extra, 0.0, 1.0, spinspeed, 0.0, cqi, etai, 1.0, 0.0, 0.0) - return lib, cq*1e6, eta + return mult * amp * makeMQMASSpectrum(x, sw, [np.real(v1.flatten()), np.real(v2.flatten())], [gauss1, gauss2], [lor1, lor2], np.real(tot).flatten()) def mqmasCzjzekFunc(x, freq, sw, axMult, extra, bgrnd, mult, pos, sigma, sigmaCS, cq0, eta0, amp, lor2, gauss2, lor1, gauss1): + """ + Calculates a 2-D FID of an MQMAS spectrum with an (extended) Czjzek distribution using a library of 1-D spectra. + + Parameters + ---------- + x : list of ndarray + A list of axes values for the simulation. + As this is a 1-D method only the last array in the list is used. + freq : list of float + The list of frequency per dimension in Hz. + The second last value is used for the first dimension and the last value is used for the second dimension. + sw : list of float + The list of spectral width per dimension in Hz. + The second last value is used for the first dimension and the last value is used for the second dimension. + axMult : float + The multiplier of the x-axis. + extra : list + The extra parameters defined as [I, mq, cq, eta, lib, shear, scale, method, d]. + I is the spin quantum number. + mq is the multiple quantum transition that is used in the indirect dimension. + cq (in Hz) and eta are the quadrupole parameters corresponding to the FIDs in lib and both should have the same length as lib. + lib is the library (array) of FIDs. + shear is the shearing factor. + scale is the scaling factor of the indirect axis. + method is a boolean when True the extended version of the Czjzek distribution is used. + d is the order parameter of the distribution. + bgrnd : float + The offset value added to the FID. + mult : float + The value by which the FID is multiplied. + pos : float + The isotropic chemical shift in the units defined by axMult. + sigma : float + The width of the Czjzek distribution in MHz. + sigmaCS : float + The Gaussian broadening along the chemical shift axis in Hz. + cq0 : float + The extended Czjzek quadrupole coupling constant Cq0 given in MHz (only used when method=True). + eta0 : float + The extended Czjzek asymmetry parameter of the quadrupole coupling (only used when method=True). + amp : float + The amplitude of the peak. + lor2 : float + The Lorentzian broadening in the direct dimension. + gauss2 : float + The Gaussian broadening in the direct dimension. + lor1 : float + The Lorentzian broadening in the indirect dimension. + gauss1 : float + The Gaussian broadening in the indirect dimension. + + Returns + ------- + ndarray + The simulated 2-D FID + """ if freq[-1] == 0.0 or freq[-2] == 0.0: raise SimException("Sim: Frequency cannot be zero") I, mq, cq, eta, lib, shear, scale, method, d = extra @@ -481,14 +1141,12 @@ def mqmasCzjzekFunc(x, freq, sw, axMult, extra, bgrnd, mult, pos, sigma, sigmaCS sigma *= 1e6 czjzek = Czjzek.czjzekIntensities(sigma, d, cq, eta, cq0, eta0) length2 = len(x[-1]) - czjzek *= length2 / sw[-2] - newLib = czjzek[...,np.newaxis]*lib + czjzek *= length2 / sw[-2] + newLib = czjzek[..., np.newaxis]*lib length1 = len(x[-2]) t1 = np.fft.fftfreq(length1, sw[-2]/float(length1)) t1 = t1[:, np.newaxis] - diff1 = (x[-2][1] - x[-2][0])*0.5 t2 = np.fft.fftfreq(length2, sw[-1]/float(length2)) - diff2 = (x[-1][1] - x[-1][0])*0.5 apod2 = np.exp(-np.pi * np.abs(lor2 * t2) - ((np.pi * np.abs(gauss2) * t2)**2) / (4 * np.log(2))) apod1 = np.exp(-np.pi * np.abs(lor1 * t1) - ((np.pi * np.abs(gauss1) * t1)**2) / (4 * np.log(2))) V40 = 1.0 / 140 * (18 + eta**2) @@ -502,13 +1160,52 @@ def mqmasCzjzekFunc(x, freq, sw, axMult, extra, bgrnd, mult, pos, sigma, sigmaCS offset *= scale ind = np.digitize(offset, x[-2]-(x[-2][1]-x[-2][0])/2.0) fid = np.zeros((length1, length2), dtype=complex) - for i in range(len(ind)): + for i, _ in enumerate(ind): fid[ind[i]-1] += newLib[i] fid = np.fft.ifft(fid, axis=0) posIndirect = pos * (mq - shearFactor) * scale - offsetMat = np.exp(2j * np.pi * ((posIndirect )*t1 + (pos - x[-1][length2//2])*t2)) + offsetMat = np.exp(2j * np.pi * (posIndirect * t1 + (pos - x[-1][length2//2])*t2)) shiftGauss = np.exp(-((np.pi * np.abs(sigmaCS) * (t2 + t1*(mq-shearFactor)*scale))**2) / (4 * np.log(2))) fid *= offsetMat * apod1 * apod2 * shiftGauss shearMat = np.exp((shearFactor-shear) * 2j * np.pi * t1 * x[-1]) fid = np.fft.fft(fid, axis=1) * shearMat return mult * amp * fid * length1 / length2 + +def genLib(length, minCq, maxCq, minEta, maxEta, numCq, numEta, extra, freq, sw, spinspeed): + """ + Generate a library of FIDs for Czjzek distribution fitting. + + Parameters + ---------- + length : int + The length of the FIDs to generate. + minCq, maxCq, minEta, maxEta : float + The Cq (in MHz) and eta values between which to generate the FIDs. + numCq, numEta : int + The number of FIDs to generate along Cq and eta respectively. + extra : list + The extra parameters as used by quadFunc. + freq : float + The Larmor frequency of the nucleus in Hz. + sw : float + The spectral width in Hz. + spinspeed : float + The spinning frequency in Hz. + + Returns + ------- + ndarray + The library of FIDs with shape (numCq*numEta, length). + ndarray + The Cq values corresponding to the FIDs in library (in Hz). + ndarray + The eta values corresponding to the FIDs in library. + """ + cq, eta = np.meshgrid(np.linspace(minCq, maxCq, numCq), np.linspace(minEta, maxEta, numEta)) + cq = cq.flatten() + eta = eta.flatten() + x = np.fft.fftshift(np.fft.fftfreq(length, 1/float(sw))) + lib = np.zeros((len(cq), length), dtype=complex) + for i, (cqi, etai) in enumerate(zip(cq, eta)): + lib[i] = quadFunc([x], [freq], [sw], 1.0, extra, 0.0, 1.0, spinspeed, 0.0, cqi, etai, 1.0, 0.0, 0.0) + return lib, cq*1e6, eta diff --git a/src/specIO.py b/src/specIO.py index 658e8b33..f9137ea3 100644 --- a/src/specIO.py +++ b/src/specIO.py @@ -17,9 +17,9 @@ # You should have received a copy of the GNU General Public License # along with ssNake. If not, see . -import numpy as np import re import os +import numpy as np from six import string_types import spectrum as sc import hypercomplex as hc @@ -28,6 +28,33 @@ class LoadException(sc.SpectrumException): pass def autoLoad(filePathList, asciiInfoList=None): + """ + Loads and combines a list of files using the automatic routine. + All data file should have the same shape in order for merging to work. + + Parameters + ---------- + filePathList: list of strings + Paths to the files that should be loaded + asciiInfoList: list of lists (optional) + Extra info needed for loading ASCII data. Each entry consist of: + [dim, order, spec, delim, sw] + dim: int + Number of dimensions (1 or 2) + order: string + Data column description ('XRI','XR','XI','RI','R') + spec: bool + If True spectrum, otherwise FID + delim: string + Delimiter ('Tab','Space','Comma') + sw: float + Spectral width in kHz + If no info needs to be given 'None' should be passed + Returns + ------- + SpectrumClass: + SpectrumClass object of the merged data + """ if isinstance(filePathList, string_types): filePathList = [filePathList] if asciiInfoList is None: @@ -50,6 +77,31 @@ def autoLoad(filePathList, asciiInfoList=None): return masterData def autoLoadSingle(filePath, asciiInfo=None): + """ + Loads a single file using the automatic routine. + + Parameters + ---------- + filePath: string + Path to the file that should be loaded + asciiInfo: list (optional) + Extra info needed for loading ASCII data + [dim, order, spec, delim, sw] + dim: int + Number of dimensions (1 or 2) + order: string + Data column description ('XRI','XR','XI','RI','R') + spec: bool + If True spectrum, otherwise FID + delim: string + Delimiter ('Tab','Space','Comma') + sw: float + Spectral width in kHz + Returns + ------- + SpectrumClass: + SpectrumClass object of the loaded data + """ if filePath.endswith('.zip'): import tempfile import shutil @@ -70,14 +122,39 @@ def autoLoadSingle(filePath, asciiInfo=None): return tmpSpec def loadFile(filePath, realpath=False, asciiInfo=None): - val = fileTypeCheck(filePath) - num =val[0] - filePath = val[1] + """ + Loads file from filePath using the correct routine. + + Parameters + ---------- + filePath: string + Path to the file that should be loaded + realpath: string (optional) + The path to the real data (used for temporary unpacks of .zip files) + asciiInfo: list (optional) + Extra info needed for loading ASCII data + [dim, order, spec, delim, sw] + dim: int + Number of dimensions (1 or 2) + order: string + Data column description ('XRI','XR','XI','RI','R') + spec: bool + If True spectrum, otherwise FID + delim: string + Delimiter ('Tab','Space','Comma') + sw: float + Spectral width in kHz + Returns + ------- + SpectrumClass: + SpectrumClass object of the loaded data + """ + num, filePath = fileTypeCheck(filePath) if realpath: # If there is a temp file, use the real path for name name = os.path.splitext(os.path.basename(realpath))[0] else: name = os.path.splitext(os.path.basename(filePath))[0] - if val[0] is None: + if num is None: return if num == 0: masterData = loadVarianFile(filePath) @@ -115,11 +192,27 @@ def loadFile(filePath, realpath=False, asciiInfo=None): masterData = loadBrukerWinNMR(filePath) elif num == 16: masterData = loadMestreC(filePath) + elif num == 17: + masterData = loadBrukerImaging(filePath) masterData.rename(name) return masterData def fileTypeCheck(filePath): - returnVal = 0 + """ + Detects which file file type in contained in the filepath. + + Parameters + ---------- + filePath: string + Path to the file that should be loaded + + Returns + ------- + int: + The number used in ssNake for each different file format + string: + Path to the file + """ fileBase = '' direc = filePath if os.path.isfile(filePath): @@ -128,61 +221,73 @@ def fileTypeCheck(filePath): direc = os.path.dirname(filePath) if filename.lower().endswith('.fid') or filename.lower().endswith('.spe'): if os.path.exists(filePath[:-3] + 'AQS') or os.path.exists(filePath[:-3] + 'aqs'): - return (15, filePath, returnVal) #Bruker WinNMR suspected + return 15, filePath #Bruker WinNMR suspected with open(filePath, 'r') as f: check = int(np.fromfile(f, np.float32, 1)) if check == 0: - return (8, filePath, returnVal) # Suspected NMRpipe format - else: # SIMPSON - return (4, filePath, returnVal) + return 8, filePath # Suspected NMRpipe format + return 4, filePath # SIMPSON if filename.endswith('.ft') or filename.endswith('.ft1') or filename.endswith('.ft2') or filename.endswith('.ft3') or filename.endswith('.ft4'): with open(filePath, 'r') as f: check = int(np.fromfile(f, np.float32, 1)) if check == 0: - return (8, filePath, returnVal) # Suspected NMRpipe format + return 8, filePath # Suspected NMRpipe format elif filename.lower().endswith('.json'): - return (5, filePath, returnVal) + return 5, filePath elif filename.lower().endswith('.mat'): - return (6, filePath, returnVal) + return 6, filePath elif filename.endswith('.jdf'): # JEOL delta format - return (9, filePath, returnVal) + return 9, filePath elif filename.endswith('.dx') or filename.endswith('.jdx') or filename.endswith('.jcamp'): # JCAMP format - return (10, filePath, returnVal) + return 10, filePath elif filename.endswith('.sig'): # Bruker minispec - return (12, filePath, returnVal) + return 12, filePath elif filename.lower().endswith('.ima'): # Siemens ima format - return (14, filePath, returnVal) - elif filename.lower().endswith('.1r') or filename.lower().endswith('.1i') : # Bruker WinNMR format - return (15, filePath, returnVal) - elif filename.lower().endswith('.mrc') : # MestreC - return (16, filePath, returnVal) - returnVal = 1 + return 14, filePath + elif filename.lower().endswith('.1r') or filename.lower().endswith('.1i'): # Bruker WinNMR format + return 15, filePath + elif filename.lower().endswith('.mrc'): # MestreC + return 16, filePath direc = os.path.dirname(filePath) if os.path.exists(direc + os.path.sep + 'procpar') and os.path.exists(direc + os.path.sep + 'fid'): - return (0, direc, returnVal) + return 0, direc # And for varian processed data if (os.path.exists(direc + os.path.sep + '..' + os.path.sep + 'procpar') or os.path.exists(direc + os.path.sep + 'procpar')) and os.path.exists(direc + os.path.sep + 'data'): - return (0, direc, returnVal) + return 0, direc elif os.path.exists(direc + os.path.sep + 'acqus') and (os.path.exists(direc + os.path.sep + 'fid') or os.path.exists(direc + os.path.sep + 'ser')): - return (1, direc, returnVal) + return 1, direc elif os.path.exists(direc + os.path.sep + 'procs') and (os.path.exists(direc + os.path.sep + '1r') or os.path.exists(direc + os.path.sep + '2rr') or os.path.exists(direc + os.path.sep + '3rrr')): - return (7, direc, returnVal) + return 7, direc + elif os.path.exists(direc + os.path.sep + 'procs') and os.path.exists(direc + os.path.sep + 'd3proc') and os.path.exists(direc + os.path.sep + '2dseq'): + return 17, direc elif os.path.exists(direc + os.path.sep + 'acq') and os.path.exists(direc + os.path.sep + 'data'): - return (2, direc, returnVal) + return 2, direc elif os.path.exists(direc + os.path.sep + 'acqu.par'): dirFiles = os.listdir(direc) files2D = [x for x in dirFiles if '.2d' in x] files1D = [x for x in dirFiles if '.1d' in x] - if len(files2D) != 0 or len(files1D) != 0: - return (3, direc, returnVal) + if files2D or files1D: + return 3, direc elif os.path.exists(direc + os.path.sep + fileBase + '.spc') and os.path.exists(direc + os.path.sep + fileBase + '.par'): - return (13, direc + os.path.sep + fileBase, returnVal) + return 13, direc + os.path.sep + fileBase elif os.path.isfile(filePath): # If not recognised, load as ascii - return (11, filePath, returnVal) - return (None, filePath, 2) + return 11, filePath + return None, filePath def varianGetPars(procpar): - """ A routine to load all pars to a dictionary for Varian procpar data """ + """ + Loads all parameters from Varian procpar file. + + Parameters + ---------- + procpar: string + Path to the procpar file that should be loaded + + Returns + ------- + Dictionary: + Dict with all the parameters + """ with open(procpar, 'r') as f: data = f.read().split('\n') pos = 0 @@ -219,7 +324,19 @@ def varianGetPars(procpar): return pars def loadVarianFile(filePath): - from struct import unpack + """ + Loads a Varian/Agilent file. + + Parameters + ---------- + filePath: string + Path to the file that should be loaded + + Returns + ------- + SpectrumClass + SpectrumClass object of the loaded data + """ if os.path.isfile(filePath): Dir = os.path.dirname(filePath) else: @@ -230,7 +347,7 @@ def loadVarianFile(filePath): file = Dir + os.path.sep + '..' + os.path.sep + 'procpar' else: file = None - sw1, reffreq1, freq1 = (1,None,0) #pre initialize + sw1, reffreq1, freq1 = (1, None, 0) #pre initialize indirectRef = 'dfrq' if file is not None: pars = varianGetPars(file) @@ -253,7 +370,7 @@ def loadVarianFile(filePath): nblocks, ntraces, npoints, ebytes, tbytes, bbytes = np.fromfile(f, np.int32, 6).newbyteorder('>l') status = np.fromfile(f, np.int16, 2).newbyteorder('>h')[1] status = '{0:016b}'.format(status)[::-1] #send to zeropadded string, and put order correct - spec, fid32, fidfloat, hypercomplex, flipped = np.array([bool(int(x)) for x in status])[[1,2,3,5,9]] + spec, fid32, fidfloat, hypercomplex, flipped = np.array([bool(int(x)) for x in status])[[1, 2, 3, 5, 9]] nbheaders = np.fromfile(f, np.int32, 1).newbyteorder('>l')[0] SizeTD2 = npoints SizeTD1 = nblocks * ntraces @@ -299,15 +416,28 @@ def loadVarianFile(filePath): return masterData def loadPipe(filePath): + """ + Loads a NMRpipe file. + + Parameters + ---------- + filePath: string + Path to the file that should be loaded + + Returns + ------- + SpectrumClass + SpectrumClass object of the loaded data + """ with open(filePath, 'r') as f: header = np.fromfile(f, np.float32, 512) NDIM = int(header[9]) - SIZE = [int(header[32]),int(header[15]),int(header[219]),int(header[99])] - quadFlag = [int(header[54]),int(header[51]),int(header[55]),int(header[56])] #0 complex, 1 real - spec = [int(header[31]),int(header[13]),int(header[222]),int(header[220])] # 1 if ft, 0 if time - freq = np.array([header[28],header[10],header[218],header[119]]) * 1e6 - sw = [header[29],header[11],header[229],header[100]] - ref = [header[30],header[12],header[249],header[101]] # frequency of last point in Hz + SIZE = [int(header[32]), int(header[15]), int(header[219]), int(header[99])] + quadFlag = [int(header[54]), int(header[51]), int(header[55]), int(header[56])] #0 complex, 1 real + spec = [int(header[31]), int(header[13]), int(header[222]), int(header[220])] # 1 if ft, 0 if time + freq = np.array([header[28], header[10], header[218], header[119]]) * 1e6 + sw = [header[29], header[11], header[229], header[100]] + ref = [header[30], header[12], header[249], header[101]] # frequency of last point in Hz numFiles = int(header[442]) #Number of files to be loaded pipeFlag = int(header[57]) #Indicates stream, or separate files cubeFlag = int(header[447]) #1 if 4D, and saved as sets of 3D @@ -323,7 +453,7 @@ def loadPipe(filePath): TotP *= SIZE[0] * SIZE[1] if NDIM == 3 and pipeFlag > 0: TotP *= SIZE[1] - if numFiles == 1: + if numFiles == 1: files = [filePath] else: #Get the names of the files, if more than 1 dir, file = os.path.split(filePath) @@ -335,37 +465,36 @@ def loadPipe(filePath): data = [] for file in files: #Load all the data from the files with open(file, 'r') as f: - tmp = np.fromfile(f, np.float32, 512) - data.append( np.fromfile(f, np.float32, TotP)) - for i in range(len(data)): #Reshape all the data + data.append(np.fromfile(f, np.float32, TotP)) + for i, _ in enumerate(data): #Reshape all the data if NDIM > 1 and cubeFlag == 0 and pipeFlag == 0: #Reshape 2D sets if needed - data[i] = np.reshape(data[i], (SIZE[2],int(TotP/SIZE[2]))) + data[i] = np.reshape(data[i], (SIZE[2], int(TotP/SIZE[2]))) elif NDIM == 4 and cubeFlag == 1 and pipeFlag == 0: #For 4D, in sets of 3D - data[i] = np.reshape(data[i], (SIZE[1],SIZE[2],int(TotP/SIZE[2]/SIZE[1]))) + data[i] = np.reshape(data[i], (SIZE[1], SIZE[2], int(TotP/SIZE[2]/SIZE[1]))) elif NDIM == 4 and pipeFlag > 0: #For stream - data[i] = np.reshape(data[i], (SIZE[0],SIZE[1],SIZE[2],int(TotP/SIZE[2]/SIZE[1]/SIZE[0]))) - elif NDIM ==3 and pipeFlag > 0: #For stream - data[i] = np.reshape(data[i], (SIZE[1],SIZE[2],int(TotP/SIZE[2]/SIZE[1]))) + data[i] = np.reshape(data[i], (SIZE[0], SIZE[1], SIZE[2], int(TotP/SIZE[2]/SIZE[1]/SIZE[0]))) + elif NDIM == 3 and pipeFlag > 0: #For stream + data[i] = np.reshape(data[i], (SIZE[1], SIZE[2], int(TotP/SIZE[2]/SIZE[1]))) #For 3D or 4D data, merge the datasets if NDIM > 2 and pipeFlag == 0: data = [np.array(data)] eS = (slice(None),) #empty slice if quadFlag[3] == 0: #If complex along last dim useSlice = eS * (NDIM - 1) - data[0] = data[0][useSlice + (slice(None,SIZE[3],None),)] + 1j * data[0][useSlice + (slice(SIZE[3],None,None),)] + data[0] = data[0][useSlice + (slice(None, SIZE[3], None),)] + 1j * data[0][useSlice + (slice(SIZE[3], None, None),)] hyper = np.array([0]) if NDIM > 1: #Reorder data, if hypercomplex along an axis for dim in range(NDIM - 1): newdata = [] if quadFlag[4 - NDIM + dim] == 0: - useSlice1 = eS * dim + (slice(None,None,2),) + eS * (NDIM - dim - 1) - useSlice2 = eS * dim + (slice(1,None,2),) + eS * (NDIM - dim - 1) + useSlice1 = eS * dim + (slice(None, None, 2),) + eS * (NDIM - dim - 1) + useSlice2 = eS * dim + (slice(1, None, 2),) + eS * (NDIM - dim - 1) for dat in data: newdata.append(dat[useSlice1]) newdata.append(dat[useSlice2]) data = newdata hyper = np.append(hyper, hyper + 2**dim) - for k in range(len(data)): #Flip LR if spectrum axis + for k, _ in enumerate(data): #Flip LR if spectrum axis for i in range(NDIM): if spec[-1 - i] == 1: data[k] = np.flip(data[k], NDIM -1 - i) @@ -373,7 +502,24 @@ def loadPipe(filePath): masterData.addHistory("NMRpipe data loaded from " + filePath) return masterData -def getJEOLpars(filePath,endian,start,length): +def getJEOLpars(filePath, start, length): + """ + Get the parameters from JEOL delta file header + + Parameters + ---------- + filePath: string + Location of the file + start: int + Byte wise start of the header + length: int + Number of bytes in the header + + Returns + ------- + Dictionary: + Dictionary with the converted header parameters + """ from struct import unpack with open(filePath, "rb") as f: _ = f.read(start + 16) @@ -381,16 +527,16 @@ def getJEOLpars(filePath,endian,start,length): numpars = int(length/64) parsOut = {} for i in range(numpars): - valueType = multiUP(pars,'> 4) & 15 if scale > 7: scale = scale - 16 return 10.0**(-scale * 3) def getJEOLdFilter(pars): + """ + Get the digital filter delay from a JEOL header + + Parameters + ---------- + pars: dict + Dictionary of the parameters + + Returns + ------- + float: + Digital filter in radian units (first order phasing correction) + """ try: orders = np.array([int(x) for x in pars['orders'].split()]) factors = np.array([int(x) for x in pars['factors'].split()]) - prodFact = np.cumprod(factors[::-1])[::-1] #Inverse cumprod of factors - filterDelay = np.sum((np.array(orders[1:]) - 1) / prodFact ) / 2 + prodFact = np.cumprod(factors[::-1])[::-1] #Inverse cumprod of factors + filterDelay = np.sum((np.array(orders[1:]) - 1) / prodFact) / 2 return filterDelay * 2 * np.pi except Exception: return None -def multiUP(header,typ, bit, num, start): +def multiUP(header, typ, bit, num, start): + """ + Unpacks an array of numbers from a JEOL header + + Parameters + ---------- + header: bytes + Bytes header of the file + type: string + Type of the encode (e.g. '>I' 'd','B', 1, 1, 8)[0]] - NDIM = multiUP(header,'>B', 1, 1, 12)[0] + endian =['>d','B', 1, 1, 8)[0]] + NDIM = multiUP(header, '>B', 1, 1, 12)[0] #data_dimension_exist = multiUP(header,'>B', 1, 1, 13)[0] #data_type = multiUP(header,'>B', 1, 1, 14)[0] #translate = multiUP(header,'>B', 1, 8, 16) - dataType = multiUP(header,'>B', 1, 8, 24) - dataUnits = multiUP(header,'>B', 1, 16, 32).reshape(8, 2) - NP = multiUP(header,'>I', 4, 8, 176) - dataStart = multiUP(header,'>I', 4, 8, 208) - dataStop = multiUP(header,'>I', 4, 8, 240) - axisStart = multiUP(header,'>d', 8, 8, 272) - axisStop = multiUP(header,'>d', 8, 8, 336) - baseFreq = multiUP(header,'>d', 8, 8, 1064) + dataType = multiUP(header, '>B', 1, 8, 24) + dataUnits = multiUP(header, '>B', 1, 16, 32).reshape(8, 2) + NP = multiUP(header, '>I', 4, 8, 176) + dataStart = multiUP(header, '>I', 4, 8, 208) + dataStop = multiUP(header, '>I', 4, 8, 240) + axisStart = multiUP(header, '>d', 8, 8, 272) + axisStop = multiUP(header, '>d', 8, 8, 336) + baseFreq = multiUP(header, '>d', 8, 8, 1064) #zero_point = multiUP(header,'>d', 8, 8, 1128) - reverse = multiUP(header,'>B', 1, 8, 1192) - paramStart = multiUP(header,'>I', 4, 1, 1212)[0] - paramLength = multiUP(header,'>I', 4, 1, 1216)[0] - readStart = multiUP(header,'>I', 4, 1, 1284)[0] + reverse = multiUP(header, '>B', 1, 8, 1192) + paramStart = multiUP(header, '>I', 4, 1, 1212)[0] + paramLength = multiUP(header, '>I', 4, 1, 1216)[0] + readStart = multiUP(header, '>I', 4, 1, 1284)[0] #data_length = multiUP(header,'>Q', 8, 1, 1288)[0] - hdrPars = getJEOLpars(filePath,endian,paramStart,paramLength) + hdrPars = getJEOLpars(filePath, paramStart, paramLength) dFilter = getJEOLdFilter(hdrPars) - loadSize = np.prod(NP[:NDIM]) if NDIM == 1 and (dataType[0] == 3 or dataType[0] == 4): #Complex 1D loadSize *= 2 @@ -467,23 +672,22 @@ def loadJEOLDelta(filePath): data = data[:int(loadSize/2)] - 1j * data[int(loadSize/2):] data = np.reshape(data, [int(NP[1] / Step), int(NP[0] / Step), Step, Step]) data = [np.concatenate(np.concatenate(data, 1), 1)] - elif NDIM == 2 and dataType[0] == 3 and dataType[1] == 3 : #2D Complex-Complex (Hypercomplex) + elif NDIM == 2 and dataType[0] == 3 and dataType[1] == 3: #2D Complex-Complex (Hypercomplex) hyper = np.array([0, 1]) Step = 32 # Step size of block - tmp = np.split(data,4) + tmp = np.split(data, 4) data = [tmp[0] - 1j * tmp[1], tmp[2] - 1j * tmp[3]] del tmp - for i in range(len(data)): + for i, _ in enumerate(data): data[i] = np.reshape(data[i], [int(NP[1] / Step), int(NP[0] / Step), Step, Step]) data[i] = np.concatenate(np.concatenate(data[i], 1), 1) - eS = (slice(None),) #empty slice for dim in range(NDIM): #Cut data for every dim - useSlice = eS * (NDIM - dim - 1) +(slice(0,dataStop[dim] + 1,None),) + eS * dim - for i in range(len(data)): + useSlice = eS * (NDIM - dim - 1) +(slice(0, dataStop[dim] + 1, None),) + eS * dim + for i, _ in enumerate(data): data[i] = data[i][useSlice] freq = baseFreq[0:NDIM][::-1] * 1e6 - spec = dataUnits[0:NDIM,1][::-1] != 28 #If not 28 (sec), then spec = true + spec = dataUnits[0:NDIM, 1][::-1] != 28 #If not 28 (sec), then spec = true sw = [] ref = [] for axisNum in reversed(range(NDIM)): @@ -491,7 +695,7 @@ def loadJEOLDelta(filePath): axisScale = dataUnits[axisNum][0] if axisType == 28: # Sec scale = convJEOLunit(axisScale) - dw = (axisStop[axisNum] - axisStart[axisNum]) / (dataStop[axisNum] + 1 - 1) * scale + dw = (axisStop[axisNum] - axisStart[axisNum]) / (dataStop[axisNum] + 1 - 1) * scale sw.append(1.0 / dw) ref.append(baseFreq[axisNum] * 1e6) if axisType == 13: # Hz @@ -502,15 +706,25 @@ def loadJEOLDelta(filePath): sw.append(np.abs(axisStart[axisNum] - axisStop[axisNum]) * baseFreq[axisNum]) sidefreq = -np.floor((dataStop[axisNum] + 1) / 2) / (dataStop[axisNum] + 1) * sw[-1] # frequency of last point on axis ref.append(sidefreq + baseFreq[axisNum] * 1e6 - axisStop[axisNum] * baseFreq[axisNum]) - for k in range(len(data)): #Flip LR if spectrum axis + for k, _ in enumerate(data): #Flip LR if spectrum axis for i in range(NDIM): if spec[-1 - i] == 1: data[k] = np.flip(data[k], NDIM -1 - i) - masterData = sc.Spectrum(hc.HComplexData(np.array(data), hyper), (filePath, None), freq, sw, spec, ref=ref, dFilter = dFilter) + masterData = sc.Spectrum(hc.HComplexData(np.array(data), hyper), (filePath, None), freq, sw, spec, ref=ref, dFilter=dFilter) masterData.addHistory("JEOL Delta data loaded from " + filePath) return masterData def saveJSONFile(filePath, spectrum): + """ + Saves a spectrumclass object to a .json file. + + Parameters + ---------- + filePath: string + Path to the file that should be created + spectrum: SpectrumClass + The spectrum class object + """ import json struct = {} item = spectrum.data @@ -534,6 +748,19 @@ def saveJSONFile(filePath, spectrum): json.dump(struct, outfile) def loadJSONFile(filePath): + """ + Loads a ssNake .json file. + + Parameters + ---------- + filePath: string + Path to the file that should be loaded + + Returns + ------- + SpectrumClass + SpectrumClass object of the loaded data + """ import json with open(filePath, 'r') as inputfile: struct = json.load(inputfile) @@ -572,11 +799,24 @@ def loadJSONFile(filePath): xaxA, history=history, metaData=metaData, - dFilter = dFilter) + dFilter=dFilter) masterData.addHistory("JSON data loaded from " + filePath) return masterData def saveMatlabFile(filePath, spectrum, name='spectrum'): + """ + Saves a spectrumclass object to a .mat file. + + Parameters + ---------- + filePath: string + Path to the file that should be created + spectrum: SpectrumClass + The spectrum class object + name: string (optional) + Name of the data set within the .mat file + + """ import scipy.io struct = {} struct['dim'] = spectrum.ndim() @@ -596,6 +836,19 @@ def saveMatlabFile(filePath, spectrum, name='spectrum'): scipy.io.savemat(filePath, matlabStruct) def loadMatlabFile(filePath): + """ + Loads a ssNake .mat file. + + Parameters + ---------- + filePath: string + Path to the file that should be loaded + + Returns + ------- + SpectrumClass + SpectrumClass object of the loaded data + """ import scipy.io with open(filePath, 'rb') as inputfile: # read first several bytes the check .mat version teststring = inputfile.read(13) @@ -605,10 +858,10 @@ def loadMatlabFile(filePath): var = [k for k in matlabStruct.keys() if not k.startswith('__')][0] mat = matlabStruct[var] if 'hyper' in mat.dtype.names: - if len(mat['hyper'][0,0]) == 0: + if len(mat['hyper'][0, 0]) == 0: hyper = [0] else: - hyper = mat['hyper'][0,0][0] + hyper = mat['hyper'][0, 0][0] else: hyper = None if 'dFilter' in mat.dtype.names: @@ -621,7 +874,7 @@ def loadMatlabFile(filePath): data = np.array(mat['data'][0][0][0]) else: data = np.array(mat['data'][0][0]) - xaxA = [k[0] for k in (mat['xaxArray'][0])] + xaxA = [k[0] for k in mat['xaxArray'][0]] else: if hyper is None: #If old format data = np.array(mat['data'][0, 0]) @@ -661,8 +914,8 @@ def loadMatlabFile(filePath): list(ref), xaxA, history=history, - metaData = metaData, - dFilter = dFilter) + metaData=metaData, + dFilter=dFilter) masterData.addHistory("Matlab data loaded from " + filePath) return masterData else: # If the version is 7.3, use HDF5 type loading @@ -728,14 +981,25 @@ def loadMatlabFile(filePath): xaxA, history=history, metaData=metaData, - dFilter = dFilter) + dFilter=dFilter) masterData.addHistory("Matlab data loaded from " + filePath) return masterData def brukerTopspinGetPars(file): - """ A routine to load all pars to a dictionary for Bruker Topsin acqus type - file """ + """ + Loads Bruker Topspin parameter file. + + Parameters + ---------- + file: string + Path to the parameter file. + + Returns + ------- + dict: + Dictionary with all parameters + """ with open(file, 'r') as f: data = f.read().split('\n') pos = 0 @@ -749,9 +1013,9 @@ def brukerTopspinGetPars(file): if val[0] == '<': val = val.strip('<>') elif val[0] == '(': #If list of values (always int/floats) - pos +=1 + pos += 1 val = [] - while not data[pos].startswith('##$'): + while not data[pos].startswith('##$') and not data[pos].startswith('$$'): try: val = val + [float(x) for x in data[pos].strip('<>').split()] except Exception: @@ -772,12 +1036,25 @@ def brukerTopspinGetPars(file): return pars def getBrukerFilter(pars): + """ + Get phase delay of a Bruker topspin data set. + + Parameters + ---------- + pars: dict + Dictionary holding the parameters + + Returns + ------- + float: + Phase delay (first order phasing correction) of the data + """ delay = -1 if 'GRPDLY' in pars.keys(): delay = pars['GRPDLY'] * 2 * np.pi if delay >= 0.0: return delay - elif pars['DSPFVS'] == 10 or pars['DSPFVS'] == 11 or pars['DSPFVS'] == 12: # get from table + if pars['DSPFVS'] == 10 or pars['DSPFVS'] == 11 or pars['DSPFVS'] == 12: # get from table CorrectionList = [{'2': 44.7500, '3': 33.5000, '4': 66.6250, '6': 59.0833, '8': 68.5625, '12': 60.3750, '16': 69.5313, '24': 61.0208, '32': 70.0156, '48': 61.3438, '64': 70.2578, '96': 61.5052, '128': 70.3789, '192': 61.5859, '256': 70.4395, '384': 61.6263, '512': 70.4697, '768': 61.6465, @@ -796,26 +1073,39 @@ def getBrukerFilter(pars): def loadBrukerTopspin(filePath): + """ + Loads Bruker Topspin/Xwinnmr data (i.e. time-domain data). + + Parameters + ---------- + filePath: string + Path to the file that should be loaded + + Returns + ------- + SpectrumClass + SpectrumClass object of the loaded data + """ if os.path.isfile(filePath): Dir = os.path.dirname(filePath) else: Dir = filePath pars = [] - for File in ['acqus','acqu2s','acqu3s']: + for File in ['acqus', 'acqu2s', 'acqu3s']: if os.path.exists(Dir + os.path.sep + File): pars.append(brukerTopspinGetPars(Dir + os.path.sep + File)) SIZE = [x['TD'] for x in pars] FREQ = [x['SFO1'] * 1e6 for x in pars] SW = [x['SW_h'] for x in pars] REF = [x['O1'] for x in pars] - ByteOrder = ['l','b'][pars[0]['BYTORDA']] #The byte orders that is used + ByteOrder = ['l', 'b'][pars[0]['BYTORDA']] #The byte orders that is used REF = list(- np.array(REF) + np.array(FREQ)) dFilter = getBrukerFilter(pars[0]) totsize = np.prod(SIZE) dim = len(SIZE) directSize = int(np.ceil(float(SIZE[0]) / 256)) * 256 #Size of direct dimension including #blocking size of 256 data points - for file in ['fid','ser']: + for file in ['fid', 'ser']: if os.path.exists(Dir + os.path.sep + file): if file == 'ser': totsize = int(totsize / SIZE[0]) * directSize #Always load full 1024 byte blocks (256 data points) for >1D @@ -828,10 +1118,10 @@ def loadBrukerTopspin(filePath): newSize[0] = int(directSize / 2) ComplexData = ComplexData.reshape(*newSize[-1::-1]) if dim == 2: - ComplexData = ComplexData[:,0:int(SIZE[0]/2)] #Cut off placeholder data + ComplexData = ComplexData[:, 0:int(SIZE[0]/2)] #Cut off placeholder data elif dim == 3: - ComplexData = ComplexData[:,:,0:int(SIZE[0]/2)] #Cut off placeholder data - masterData = sc.Spectrum(ComplexData, (filePath, None), FREQ[-1::-1], SW[-1::-1], [False] * dim, ref = REF[-1::-1], dFilter = dFilter) + ComplexData = ComplexData[:, :, 0:int(SIZE[0]/2)] #Cut off placeholder data + masterData = sc.Spectrum(ComplexData, (filePath, None), FREQ[-1::-1], SW[-1::-1], [False] * dim, ref = REF[-1::-1], dFilter=dFilter) # TODO: Inserting metadata should be made more generic try: masterData.metaData['# Scans'] = str(pars[0]['NS']) @@ -857,18 +1147,31 @@ def loadBrukerTopspin(filePath): return masterData def loadBrukerWinNMR(filePath): + """ + Loads Bruker WinNMR data. + + Parameters + ---------- + filePath: string + Path to the file that should be loaded + + Returns + ------- + SpectrumClass + SpectrumClass object of the loaded data + """ base, extension = os.path.splitext(filePath) #Check if upper or lower case - names = ['.fqs','.aqs','.fid','.1r','.1i'] + names = ['.fqs', '.aqs', '.fid', '.1r', '.1i'] if extension == extension.upper(): #If uppercase names = [x.upper() for x in names] present = [os.path.exists(base + x) for x in names] - if extension == names[3] or extension == names[4]: #If spec loaded + if extension in (names[3], names[4]): #If spec loaded pars = brukerTopspinGetPars(base + names[0]) SIZE = pars['XDIM'] - FREQ = pars['SF'] * 1e6 + FREQ = pars['SF'] * 1e6 SW = pars['SW_p'] - ByteOrder = ['l','b'][pars['BYTORDP']] #The byte orders that is used + ByteOrder = ['l', 'b'][pars['BYTORDP']] #The byte orders that is used OFFSET = pars['OFFSET'] pos = np.fft.fftshift(np.fft.fftfreq(SIZE, 1.0 / SW))[-1] #Get last point of axis pos2 = OFFSET * 1e-6 * FREQ #offset in Hz @@ -877,11 +1180,11 @@ def loadBrukerWinNMR(filePath): else: pars = brukerTopspinGetPars(base + names[1]) SIZE = pars['TD'] - FREQ = pars['SFO1'] * 1e6 + FREQ = pars['SFO1'] * 1e6 SW = pars['SW_h'] - REF = pars['O1'] + REF = pars['O1'] REF = - REF+ FREQ - ByteOrder = ['l','b'][pars['BYTORDA']] #The byte orders that is used + ByteOrder = ['l', 'b'][pars['BYTORDA']] #The byte orders that is used spec = False if spec: #If spec loaded with open(base + names[3], "rb") as f: @@ -900,7 +1203,7 @@ def loadBrukerWinNMR(filePath): raw = np.fromfile(f, np.float32, SIZE) raw = raw.newbyteorder(ByteOrder) #Load with right byte order ComplexData = np.array(raw[0:len(raw):2]) + 1j * np.array(raw[1:len(raw):2]) - masterData = sc.Spectrum(ComplexData, (filePath, None), [FREQ], [SW], [spec], ref = [REF]) + masterData = sc.Spectrum(ComplexData, (filePath, None), [FREQ], [SW], [spec], ref=[REF]) if not spec: try: masterData.metaData['# Scans'] = str(pars['NS']) @@ -915,12 +1218,25 @@ def loadBrukerWinNMR(filePath): return masterData def loadBrukerSpectrum(filePath): + """ + Loads Bruker spectrum data (processed data). Supports 1-3D. + + Parameters + ---------- + filePath: string + Path to the file that should be loaded + + Returns + ------- + SpectrumClass + SpectrumClass object of the loaded data + """ if os.path.isfile(filePath): Dir = os.path.dirname(filePath) else: Dir = filePath pars = [] - for File in ['procs','proc2s','proc3s']: + for File in ['procs', 'proc2s', 'proc3s']: if os.path.exists(Dir + os.path.sep + File): pars.append(brukerTopspinGetPars(Dir + os.path.sep + File)) SIZE = [x['SI'] for x in pars] @@ -928,17 +1244,16 @@ def loadBrukerSpectrum(filePath): SW = [x['SW_p'] for x in pars] FREQ = [x['SF'] * 1e6 for x in pars] OFFSET = [x['OFFSET'] for x in pars] - ByteOrder = ['l','b'][pars[0]['BYTORDP']] #The byte orders that is used - + ByteOrder = ['l', 'b'][pars[0]['BYTORDP']] #The byte orders that is used REF = [] - for index in range(len(SIZE)): #For each axis + for index, _ in enumerate(SIZE): #For each axis pos = np.fft.fftshift(np.fft.fftfreq(SIZE[index], 1.0 / SW[index]))[-1] #Get last point of axis pos2 = OFFSET[index] * 1e-6 * FREQ[index] #offset in Hz REF.append(FREQ[index] + pos - pos2) - totsize = np.prod(SIZE) + totsize = np.prod(SIZE) dim = len(SIZE) DATA = [] - files = [['1r','1i'],['2rr','2ir','2ri','2ii'],['3rrr','3irr','3rir','3iir','3rri','3iri','3rii','3iii']] + files = [['1r','1i'], ['2rr', '2ir', '2ri', '2ii'], ['3rrr', '3irr', '3rir', '3iir', '3rri', '3iri', '3rii', '3iii']] counter = 0 for file in files[dim - 1]: # For all the files if os.path.exists(Dir + os.path.sep + file): @@ -956,16 +1271,16 @@ def loadBrukerSpectrum(filePath): if len(DATA) != 1: hyper = np.array([0, 1]) if len(SIZE) == 2: - for index in range(len(DATA)): # For each data set + for index, _ in enumerate(DATA): # For each data set # Reshape DATA to 4D data using the block information # Twice concat along axis 1 constructs the regular x-y data - DATA[index] = np.reshape(DATA[index],[int(SIZE[1]/XDIM[1]),int(SIZE[0]/XDIM[0]),XDIM[1],XDIM[0]]) - DATA[index] = np.concatenate(np.concatenate(DATA[index],1),1) + DATA[index] = np.reshape(DATA[index], [int(SIZE[1]/XDIM[1]), int(SIZE[0]/XDIM[0]), XDIM[1], XDIM[0]]) + DATA[index] = np.concatenate(np.concatenate(DATA[index], 1), 1) elif len(SIZE) == 3: - for index in range(len(DATA)): + for index, _ in enumerate(DATA): # The same as 2D, but now split to 6D data, and concat along 2 - DATA[index] = np.reshape(DATA[index],[int(SIZE[2]/XDIM[2]),int(SIZE[1]/XDIM[1]),int(SIZE[0]/XDIM[0]),XDIM[2],XDIM[1],XDIM[0]]) - DATA[index] = np.concatenate(np.concatenate(np.concatenate(DATA[index],2),2),2) + DATA[index] = np.reshape(DATA[index], [int(SIZE[2]/XDIM[2]), int(SIZE[1]/XDIM[1]), int(SIZE[0]/XDIM[0]), XDIM[2], XDIM[1], XDIM[0]]) + DATA[index] = np.concatenate(np.concatenate(np.concatenate(DATA[index], 2), 2), 2) spec = [True] masterData = sc.Spectrum(hc.HComplexData(DATA, hyper), (filePath, None), FREQ[-1::-1], SW[-1::-1], spec * dim, ref=REF[-1::-1]) masterData.addHistory("Bruker spectrum data loaded from " + filePath) @@ -981,7 +1296,59 @@ def loadBrukerSpectrum(filePath): pass #Do nothing on error return masterData +def loadBrukerImaging(filePath): + """ + Load bruker topspin/paravision processed data. + Experimental at the moment. + """ + if os.path.isfile(filePath): + Dir = os.path.dirname(filePath) + else: + Dir = filePath + + File = 'd3proc' + if os.path.exists(Dir + os.path.sep + File): + Im_pars = brukerTopspinGetPars(Dir + os.path.sep + File) + + pars = [] + for File in ['procs', 'proc2s', 'proc3s']: + if os.path.exists(Dir + os.path.sep + File): + pars.append(brukerTopspinGetPars(Dir + os.path.sep + File)) + + + SIZE = [Im_pars['IM_SIZ'], Im_pars['IM_SIY'], Im_pars['IM_SIX']] + FREQ = [0]*len(SIZE) + ByteOrder = ['l', 'b'][pars[0]['BYTORDP']] #The byte orders that is used + SPEC = [False,True,True] + SW = SIZE + + totsize = np.prod(SIZE) + + file = '2dseq' + + with open(Dir + os.path.sep + file, "rb") as f: + raw = np.fromfile(f, np.int16, totsize) + raw = raw.newbyteorder(ByteOrder) # Set right byteorder +# DATA = np.flipud(raw) + + DATA = raw.reshape(SIZE) + masterData = sc.Spectrum(DATA, (filePath, None), FREQ, SW, SPEC) + return masterData + def chemGetPars(folder): + """ + Loads Chemagentic parameter file. + + Parameters + ---------- + folder: string + Path to the folder that holds the files. + + Returns + ------- + dict: + Dictionary with all parameters + """ import collections with open(folder + os.path.sep + 'acq', 'r') as f: data = f.read().split('\n') @@ -1014,12 +1381,25 @@ def convertChemVal(val): elif val.endswith('us'): num = float(val[:-2]) * 1e-6 elif val.endswith('s'): - num = float(val[:-1]) + num = float(val[:-1]) else: num = float(val) return num def loadChemFile(filePath): + """ + Loads Chemagentic data. + + Parameters + ---------- + filePath: string + Path to the file that should be loaded + + Returns + ------- + SpectrumClass + SpectrumClass object of the loaded data + """ if os.path.isfile(filePath): Dir = os.path.dirname(filePath) else: @@ -1029,11 +1409,11 @@ def loadChemFile(filePath): pars = chemGetPars(Dir) sizeTD2 = int(float(pars['al'])) freq = pars['sf' + str(int(float(pars['ch1'])))] - if type(freq) is list: #load only first value when list + if isinstance(freq, list): #load only first value when list freq = float(freq[0]) else: freq = float(freq) - sw = 1 / convertChemVal(pars['dw']) + sw = 1 / convertChemVal(pars['dw']) if any('array_num_values_' in s for s in pars): if int(float(pars['use_array'])) == 1: for s in pars: @@ -1057,14 +1437,14 @@ def loadChemFile(filePath): fid = np.reshape(fid, (len(fid)//sizeTD2, sizeTD2)) data = np.array(fid) spec = [False] - if sizeTD1 is 1: + if sizeTD1 == 1: data = data[0][:] masterData = sc.Spectrum(data, (filePath, None), [freq * 1e6], [sw], spec) else: masterData = sc.Spectrum(data, (filePath, None), [freq * 1e6] * 2, [sw1, sw], spec * 2) masterData.addHistory("Chemagnetics data loaded from " + filePath) try: - if isinstance(pars['na'],list): + if isinstance(pars['na'], list): masterData.metaData['# Scans'] = pars['na'][0] else: masterData.metaData['# Scans'] = pars['na'] @@ -1078,6 +1458,19 @@ def loadChemFile(filePath): return masterData def loadMagritek(filePath): + """ + Loads Magritek data. + + Parameters + ---------- + filePath: string + Path to the file that should be loaded + + Returns + ------- + SpectrumClass + SpectrumClass object of the loaded data + """ # Magritek load script based on some Matlab files by Ole Brauckman if os.path.isfile(filePath): Dir = os.path.dirname(filePath) @@ -1092,7 +1485,7 @@ def loadMagritek(filePath): ref1 = None # Start pars extraction H = [line.strip().split('=') for line in open(Dir + os.path.sep + 'acqu.par', 'r')] - H = [[x[0].strip(),x[1].strip()] for x in H] + H = [[x[0].strip(), x[1].strip()] for x in H] H = dict(H) sw = float(H['bandwidth']) * 1000 sizeTD2 = int(H['nrPnts']) @@ -1115,9 +1508,9 @@ def loadMagritek(filePath): Data = raw[-2 * sizeTD2 * sizeTD1::] ComplexData = Data[0:Data.shape[0]:2] - 1j * Data[1:Data.shape[0]:2] ComplexData = ComplexData.reshape((sizeTD1, sizeTD2)) - ComplexData[:,0] *= 2 + ComplexData[:, 0] *= 2 masterData = sc.Spectrum(ComplexData, (filePath, None), [freq] * 2, [sw1, sw], [False] * 2, ref=[ref1, ref]) - elif len(Files1D) != 0: + elif Files1D: File = 'data.1d' with open(Dir + os.path.sep + File, 'rb') as f: raw = np.fromfile(f, np.float32) @@ -1137,6 +1530,17 @@ def loadMagritek(filePath): return masterData def saveSimpsonFile(filePath, spectrum): + """ + Save to simpson format data. Only for 1D or 2D data. 2D data can be + either Spec-Spec, or FID-FID. + + Parameters + ---------- + filePath: string + Path of the file that should be saved + spectrum: SpectrumClass + The spectrum object to be saved + """ data = spectrum.getHyperData(0) # SIMPSON does not support hypercomplex with open(filePath, 'w') as f: f.write('SIMP\n') @@ -1164,6 +1568,20 @@ def saveSimpsonFile(filePath, spectrum): f.write('END') def loadSimpsonFile(filePath): + """ + Loads SIMPSON file. Both ASCII and binary data are supported. As well + as 1D and 2D data. + + Parameters + ---------- + filePath: string + Path to the file that should be loaded + + Returns + ------- + SpectrumClass + SpectrumClass object of the loaded data + """ with open(filePath, 'r') as f: Lines = f.read().split('\n') NP, NI, SW, SW1, TYPE, FORMAT = 0, 1, 0, 0, '', 'Normal' @@ -1226,7 +1644,7 @@ def LAST(f, x): return ((x) & (~0 << (8 - f))) spec = [False] elif 'SPE' in TYPE: spec = [True] - if NI is 1: + if NI == 1: masterData = sc.Spectrum(data, (filePath, None), [0], [SW], spec) else: masterData = sc.Spectrum(data, (filePath, None), [0, 0], [SW1, SW], spec * 2) @@ -1234,6 +1652,19 @@ def LAST(f, x): return ((x) & (~0 << (8 - f))) return masterData def convertDIFDUB(dat): + """ + Converts string of DIFDUB character encode to a array of floats + + Parameters + ---------- + dat: string + String with all the DIFDUB characters + + Returns + ------- + ndarray + 1-D array with the extracted numbers + """ def checkWrite(dup, currentNum, step, numberList): if dup != '': for dupli in range(int(dup)): @@ -1281,10 +1712,22 @@ def checkWrite(dup, currentNum, step, numberList): dup, currentNum, step, numberList = checkWrite(dup, currentNum, step, numberList) if last: return np.array(numberList) - else: - return np.array(numberList)[:-1] + return np.array(numberList)[:-1] def loadJCAMP(filePath): + """ + Loads JCAMP-DX file. + + Parameters + ---------- + filePath: string + Path to the file that should be loaded + + Returns + ------- + SpectrumClass + SpectrumClass object of the loaded data + """ with open(filePath, 'r') as f: data = f.read().split('\n') realDataPos = [] @@ -1327,7 +1770,7 @@ def loadJCAMP(filePath): factor = re.sub(',[\t ][\t ]*', ' ', factor) factor = re.sub('[\t\r]*', '', factor) factor = factor.split() - for elem in range(len(factor)): + for elem in enumerate(factor): factor[elem] = float(factor[elem].replace(',', '.')) elif '(X++(R..R))' in testline: realDataPos.append(currentPos + 1) @@ -1390,6 +1833,18 @@ def loadJCAMP(filePath): return masterData def saveASCIIFile(filePath, spectrum, axMult=1): + """ + Save to ASCII format data. + + Parameters + ---------- + filePath: string + Path of the file that should be saved + spectrum: SpectrumClass + The spectrum object to be saved + axMult: float (optional) + Axis multiplier, needed to save in other unit than s or Hz + """ axis = np.array([spectrum.xaxArray[-1] * axMult]).transpose() tmpData = spectrum.data.getHyperData(0) if tmpData.ndim == 1: # create nx1 matrix if it is a 1d data set @@ -1404,6 +1859,31 @@ def saveASCIIFile(filePath, spectrum, axMult=1): np.savetxt(filePath, data, delimiter='\t') def loadAscii(filePath, asciiInfo=None): + """ + Loads general ASCII format data. + + Parameters + ---------- + filePath: string + Path to the file that should be loaded + asciiInfo: list + [dim, order, spec, delim, sw] + dim: int + Number of dimensions (1 or 2) + order: string + Data column description ('XRI','XR','XI','RI','R') + spec: bool + If True spectrum, otherwise FID + delim: string + Delimiter ('Tab','Space','Comma') + sw: float + Spectral width in kHz + + Returns + ------- + SpectrumClass + SpectrumClass object of the loaded data + """ if asciiInfo is None: return dataDimension = asciiInfo[0] @@ -1465,6 +1945,19 @@ def loadAscii(filePath, asciiInfo=None): return masterData def loadMinispec(filePath): + """ + Loads Bruker minispec data. The format is recognized by its .sig file. + + Parameters + ---------- + filePath: string + Path to the file that should be loaded + + Returns + ------- + SpectrumClass + SpectrumClass object of the loaded data + """ with open(filePath, 'r') as f: data = f.read().split('\n') dataType = int(data[1][data[1].index('=') + 1:]) @@ -1477,11 +1970,11 @@ def loadMinispec(filePath): totaldata = np.array([]) if dataType == 1: # real data? for line in data[7:]: - if len(line) > 0: + if line: totaldata = np.append(totaldata, float(line)) if dataType == 2: # Complex data for line in data[7:]: - if len(line) > 0: + if line: temp = np.fromstring(line, sep='\t') totaldata = np.append(totaldata, temp[0] + 1j * temp[1]) masterData = sc.Spectrum(totaldata, (filePath, None), [0], [sw], [False]) @@ -1489,6 +1982,22 @@ def loadMinispec(filePath): return masterData def loadBrukerEPR(filePath): + """ + Loads Bruker EPR data. The format has both a .par and .spc file. + Note that ssNake at the moment only has s/Hz axes. In this case, + 1 Hz is the same as 1 Gauss. + + + Parameters + ---------- + filePath: string + Path to the file that should be loaded + + Returns + ------- + SpectrumClass + SpectrumClass object of the loaded data + """ with open(filePath + '.par', mode='r') as f: textdata = [row.split() for row in f.read().replace('\r', '\n').split('\n')] for row in textdata: @@ -1513,8 +2022,18 @@ def loadSiemensIMA(filePath): Siemens specific fields. It's not a nice format to work with. It is a combination of binary and text data. I, Vincent Breukels, have little understanding in the file format. Rather it is a - rewrite of the the following source: VeSPA, versatile simulation + rewrite of the following source: VeSPA, versatile simulation pulses and analysis for magnetic resonance spectroscopy. + + Parameters + ---------- + filePath: string + Path to the file that should be loaded + + Returns + ------- + SpectrumClass + SpectrumClass object of the loaded data """ import struct try: @@ -1530,52 +2049,64 @@ def loadSiemensIMA(filePath): # 'NumberOfFrames',) relevantParameterFloats = ('RealDwellTime', 'ImagingFrequency') - csaHeader = ds['0029','1110'].value + csaHeader = ds['0029', '1110'].value # I assume all relevant header information I need is here. I have not yet # encountered a file in which the relevant info. was in '0029','1120'[VB] - if struct.unpack_from('8s',csaHeader,0)[0].decode() !='SV10\4\3\2\1' or struct.unpack_from('I',csaHeader,12)[0]!=77: + if struct.unpack_from('8s', csaHeader, 0)[0].decode() != 'SV10\4\3\2\1' or struct.unpack_from('I', csaHeader, 12)[0] != 77: raise ValueError("IMA file not as expected: wrong first bytes") - n_elems = struct.unpack_from('I',csaHeader,8)[0] + n_elems = struct.unpack_from('I', csaHeader, 8)[0] currentIdx = 16 ParDict = {} for i in range(n_elems): - tagName = scrubber(struct.unpack_from('64s',csaHeader,currentIdx)[0].decode('utf-8','ignore')) - n_items = struct.unpack_from('I',csaHeader,currentIdx + 76)[0] - checkBit = struct.unpack_from('I',csaHeader,currentIdx + 80)[0] + tagName = scrubber(struct.unpack_from('64s', csaHeader, currentIdx)[0].decode('utf-8', 'ignore')) + n_items = struct.unpack_from('I', csaHeader, currentIdx + 76)[0] + checkBit = struct.unpack_from('I', csaHeader, currentIdx + 80)[0] currentIdx += 84 - if checkBit not in [77,205]: + if checkBit not in (77, 205): raise ValueError("IMA file not as expected: missing checkBit") for idx in range(n_items): - header = struct.unpack_from('4I',csaHeader,currentIdx) - if (header[0]!=header[1]!=header[3]) or header[2] not in [77,205]: + header = struct.unpack_from('4I', csaHeader, currentIdx) + if (header[0] != header[1] != header[3]) or header[2] not in [77, 205]: raise ValueError("IMA file does not seem to be correct") length = header[0] if idx == 0: - data = struct.unpack_from('{0:d}s'.format(length),csaHeader,currentIdx + 16)[0] + data = struct.unpack_from('{0:d}s'.format(length), csaHeader, currentIdx + 16)[0] currentIdx += int(np.ceil(length/4.) * 4) + 16 # Let's see if we got anything I want to keep: if tagName in relevantParameterFloats: - ParDict[tagName] = float(scrubber(data.decode('utf-8','ignore'))) + ParDict[tagName] = float(scrubber(data.decode('utf-8', 'ignore'))) elif tagName in relevantParameterInts: - ParDict[tagName] = int(scrubber(data.decode('utf-8','ignore'))) + ParDict[tagName] = int(scrubber(data.decode('utf-8', 'ignore'))) # Statement below does not work in python 2.7, as struct_iter does not exist #data = np.array([item[0]-1j*item[1] for item in struct.iter_unpack('2f',ds['7fe1','1010'].value)]) fmtString = str(ParDict['DataPointColumns'] * 2) + 'f' - dataTuple = struct.unpack(fmtString,ds['7fe1','1010'].value) - data = np.array(dataTuple[::2]) - 1j * np.array( dataTuple[1::2]) + dataTuple = struct.unpack(fmtString, ds['7fe1', '1010'].value) + data = np.array(dataTuple[::2]) - 1j * np.array(dataTuple[1::2]) # First, I alway assume the data is in the tag 7fe1,1010. I haven't seen anything else # Second, I don't understand the logic, but complex conjugate seems to be the correct ones # I found this reshape shown below, possibly for 2d or 3d mrsi data, but i dont have an example data to test # data.reshape((ParDict['DataPointColumns'],ParDict['Columns'],ParDict['Rows'],ParDict['NumberOfFrames'])) - sw = 1.0 / (ParDict['RealDwellTime'] * 1e-9) + sw = 1.0 / (ParDict['RealDwellTime'] * 1e-9) freq = ParDict['ImagingFrequency'] * 1e6 masterData = sc.Spectrum(data, filePath, [freq], [sw]) masterData.addHistory("Siemens IMA data loaded from " + filePath) return masterData def scrubber(item): - """Item is a string, scrubber returns the string up to the first \0 with - leading/trailing whitespaces removed""" + """ + Cleans the input string, returns the string up to the first \0 with + leading/trailing whitespaces removed + + Parameters + ---------- + item: string + The string that is to be cleaned + + Returns + ------- + string + Cleaned string + """ item = item.split(chr(0))[0] return item.strip() @@ -1583,43 +2114,50 @@ def scrubber(item): def loadMestreC(filePath): - import base64 - import xml.etree.ElementTree - import re + """ + Loads MestreC type data. MestreC data ends on .mrc, and is essentially XML data. + Parameters + ---------- + filePath: string + Path to the file that should be loaded + Returns + ------- + SpectrumClass + SpectrumClass object of the loaded data + """ + import base64 + import xml.etree.ElementTree invalid_xml = re.compile(u'[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]') - - with open(filePath,'r') as f: + with open(filePath, 'r') as f: lines = f.read().split('\n') parser = xml.etree.ElementTree.XMLParser() for line in lines: - line, count = invalid_xml.subn('', line) + line, _ = invalid_xml.subn('', line) parser.feed(line) main = parser.close().find('Spectrum').find('Main') dim = int(main.find('Dimensions').text) - Points = main.find('Values').find('Points').text data = base64.b64decode(Points) data = np.fromstring(data, dtype='. -import numpy as np -import scipy.optimize import copy -from nus import ffm, clean, ist import multiprocessing import itertools -import reimplement as reim +import scipy.optimize +import numpy as np +import nus import functions as func import hypercomplex as hc @@ -38,9 +37,58 @@ class SpectrumException(Exception): # the generic spectrum class -class Spectrum(object): +class Spectrum: + """ + The object that contains all relevant spectral information. + The functions for processing are methods of this object. + """ def __init__(self, data, filePath, freq, sw, spec=None, wholeEcho=None, ref=None, xaxArray=None, history=None, metaData=None, name='', dFilter=None): + """ + Initializes the Spectrum object. + + Parameters + ---------- + data : HComplexData + The NMR data. + filePath : tuple + A tuple with the input parameters for the autoLoad function that was used to obtain this data. + freq : array_like + The frequency per dimension. + The list should have the same number of entries as the number of dimensions of data. + sw : array_like + The spectral width per dimension. + The list should have the same number of entries as the number of dimensions of data. + spec : array_like, optional + An array with booleans representing whether a dimension is in the frequency domain (True) or in the time domain (False). + The array should have the same number of entries as the number of dimensions of data. + By default all dimensions are set to time domain. + wholeEcho : array_like, optional + An array with booleans representing whether a dimension is recorded as a whole echo. + The array should have the same number of entries as the number of dimensions of data. + By default all dimensions are set to False. + ref : array_like, optional + The reference frequency per dimension. + The array should have the same number of entries as the number of dimensions of data. + By default the reference frequencies are set to None. + xaxArray : list of arrays, optional + The x-axis values per dimension. + The list should have the same number of entries as the number of dimensions of data. + By default the x-axes are generated based on the sw values. + history : list of strings, optional + The processing history of the data. + By default the history is set to an empty list. + metaData : dict, optional + Contains the metadata. + The dictionary contains the the keys ('# Scans', 'Acquisition Time [s]', 'Experiment Name','Receiver Gain', 'Recycle Delay [s]', 'Sample', 'Offset [Hz]', 'Time Completed'). + By default all metadata is set to unknown. + name : str, optional + A string with the name of the spectrum. + By default the name is set to an empty string. + dFilter : float or None, optional + For a (Bruker) digital filter this value contains the first order phase correction required to correct for the digital filter. + By default this parameter is set to None. + """ self.name = name if isinstance(data, hc.HComplexData): self.data = data @@ -49,7 +97,7 @@ def __init__(self, data, filePath, freq, sw, spec=None, wholeEcho=None, ref=None self.filePath = filePath self.freq = np.array(freq) # array of center frequency (length is dim, MHz) self.sw = np.array(sw, dtype=float) # array of sweepwidths - self.dFilter = dFilter #Digital filter first order phase in radian + self.dFilter = dFilter # Digital filter first order phase in radian self.undoList = [] self.redoList = [] self.noUndo = False @@ -75,8 +123,8 @@ def __init__(self, data, filePath, freq, sw, spec=None, wholeEcho=None, ref=None else: self.history = history if metaData is None: - self.metaData = {'# Scans': '-', 'Acquisition Time [s]': '-', 'Experiment Name': '-','Receiver Gain': '-', 'Recycle Delay [s]': '-', - 'Sample': '-', 'Offset [Hz]': '-', 'Time Completed': '-'} + self.metaData = {'# Scans': '-', 'Acquisition Time [s]': '-', 'Experiment Name': '-', 'Receiver Gain': '-', 'Recycle Delay [s]': '-', + 'Sample': '-', 'Offset [Hz]': '-', 'Time Completed': '-'} else: self.metaData = metaData @@ -86,7 +134,7 @@ def ndim(self): def shape(self): return self.data.shape() - def getData(self): # Returns a copy of the data + def getData(self): # Returns a copy of the data return copy.deepcopy(self.data) def getHyperData(self, *args): @@ -96,27 +144,80 @@ def isComplex(self, *args): return self.data.isComplex(*args) def rename(self, name): + """ + Changes the name of the spectrum. + + Parameters + ---------- + name : str + The new name. + """ self.name = name def getHistory(self): + """ + Returns the history separated by newlines. + """ return "\n".join(self.history) def addHistory(self, msg): + """ + Adds a message to the data history. + + Parameters + ---------- + msg : str + The message to add to the history list. + """ self.history.append(msg) def removeFromHistory(self, num=1): + """ + Gives the history where a given number of messages have been removed from the end of the list. + + Parameters + ---------- + num : int + The number of entries to remove. + + Returns + ------- + list of str + The history list. + """ for i in range(num): - if len(self.history) > 0: + if self.history: val = self.history.pop() return val def setNoUndo(self, val): + """ + Sets the "no undo" mode of the data. + + Parameters + ---------- + val : bool + When True, the undo and redo lists are cleared and the "no undo" mode is turned on. + """ self.noUndo = bool(val) if self.noUndo: self.undoList = [] self.redoList = [] def undo(self): + """ + Undoes the last operation and puts it in the redo list. + + Returns + ------- + str + The undo message. + + Raises + ------ + SpectrumException + When the undo list is empty. + """ undoFunc = None while undoFunc is None and self.undoList: undoFunc = self.undoList.pop() @@ -130,6 +231,14 @@ def undo(self): return "Undo: " + message def redo(self): + """ + Redoes the last undo operation and puts it in the undo list. + + Raises + ------ + SpectrumException + When the redo list is empty. + """ if not self.redoList: raise SpectrumException("No redo information") tmpRedo = self.redoList # Protect the redo list @@ -137,22 +246,56 @@ def redo(self): self.redoList = tmpRedo # Restore the redo list def clearUndo(self): + """ + Clears the undo and redo lists. + """ self.undoList = [] self.redoList = [] def reload(self): + """ + Reloads the data based on the filePath of this spectrum. + """ import specIO as io loadData = io.autoLoad(*self.filePath) self.restoreData(loadData, None) - + def checkAxis(self, axis): + """ + Checks whether a given axis is a valid index for this data. + + Parameters + ---------- + axis : int + The index to be tested. + + Returns + ------- + int + The index converted to a positive value. + + Raises + ------ + IndexError + When the axis is not valid. + """ if axis < 0: axis += self.ndim() - if not (0 <= axis < self.ndim()): + if not 0 <= axis < self.ndim(): raise IndexError("Not a valid axis for Spectrum") return axis def resetXax(self, axis=None): + """ + Resets the x-axis based on the spectral width. + + Parameters + ---------- + axis : int or None, optional + The axis for which to reset the x-axis. + When None, all x-axes are reset. + By default axis is None. + """ if axis is not None: axis = self.checkAxis(axis) val = [axis] @@ -167,6 +310,23 @@ def resetXax(self, axis=None): self.xaxArray[i] += self.freq[i] - self.ref[i] def setXax(self, xax, axis=-1): + """ + Sets the x-axis of a given dimension. + + Parameters + ---------- + xax : array_like + The x-axis. + It should have the same length as the size of the data along dimension axis. + axis : int, optional + The dimension for which to set the x-axis. + By default the last axis is used. + + Raises + ------ + SpectrumException + When the length of xax does not match the length of the data + """ axis = self.checkAxis(axis) if len(xax) != self.shape()[axis]: raise SpectrumException("Length of new x-axis does not match length of the data") @@ -178,6 +338,19 @@ def setXax(self, xax, axis=-1): self.undoList.append(lambda self: self.setXax(oldXax, axis)) def insert(self, data, pos, axis=-1): + """ + Insert data in a given position along a certain dimension. + + Parameters + ---------- + data : HComplexData or array_like + The data to insert. + pos : int + The index after which to add the data. + axis : int, optional + The dimension along which to add the data. + By default the last dimension is used. + """ if not isinstance(data, hc.HComplexData): data = hc.HComplexData(data) if self.noUndo: @@ -198,6 +371,22 @@ def insert(self, data, pos, axis=-1): self.undoList.append(returnValue) def delete(self, pos, axis=-1): + """ + Deletes a given range of indices of a certain dimension from the data. + + Parameters + ---------- + pos : int or array_like + The indices to remove. + axis : int, optional + The dimension along which to delete the data. + By default the last dimension is used. + + Raises + ------ + SpectrumException + When the deletion removes all data from a dimension. + """ axis = self.checkAxis(axis) if not self.noUndo: copyData = copy.deepcopy(self) @@ -226,8 +415,23 @@ def __radd__(self, other): def __iadd__(self, other): self.add(other) return self - + def add(self, data, axis=None, select=slice(None)): + """ + Adds given data to the spectrum data. + The addition follows the Numpy broadcasting rules. + + Parameters + ---------- + data : Spectrum, HComplexData or ndarray + The data to add. + axis : int, optional + The dimension along which to add the data. + By default the last dimension is used. + select : Slice, optional + An optional selection of the spectrum data on which the addition is performed. + By default the entire data is used. + """ if axis is not None: axis = self.checkAxis(axis) if isinstance(data, Spectrum): @@ -246,7 +450,7 @@ def add(self, data, axis=None, select=slice(None)): copyData = copy.deepcopy(self) returnValue = lambda self: self.restoreData(copyData, lambda self: self.add(data, axis, select)) self.data[select] += data - if isinstance(data, (float,int)): + if isinstance(data, (float, int)): self.addHistory("Added " + str(data) + " to data[" + str(select) + "]") else: self.addHistory("Added to data[" + str(select) + "]") @@ -265,8 +469,23 @@ def __rsub__(self, other): def __isub__(self, other): self.subtract(other) return self - + def subtract(self, data, axis=None, select=slice(None)): + """ + Subtract given data from the spectrum data. + The subtraction follows the Numpy broadcasting rules. + + Parameters + ---------- + data : Spectrum, HComplexData or ndarray + The data to subtract. + axis : int, optional + The dimension along which to subtract the data. + By default the last dimension is used. + select : Slice, optional + An optional selection of the spectrum data on which the subtraction is performed. + By default the entire data is used. + """ if axis is not None: axis = self.checkAxis(axis) if isinstance(data, Spectrum): @@ -285,7 +504,7 @@ def subtract(self, data, axis=None, select=slice(None)): copyData = copy.deepcopy(self) returnValue = lambda self: self.restoreData(copyData, lambda self: self.subtract(data, axis, select)) self.data[select] -= data - if isinstance(data, (float,int)): + if isinstance(data, (float, int)): self.addHistory("Subtracted " + str(data) + " from data[" + str(select) + "]") else: self.addHistory("Subtracted from data[" + str(select) + "]") @@ -304,8 +523,23 @@ def __rmul__(self, other): def __imul__(self, other): self.multiply(other) return self - + def multiply(self, data, axis=None, select=slice(None)): + """ + Multiply given data with the spectrum data. + The multiplication follows the Numpy broadcasting rules. + + Parameters + ---------- + data : Spectrum, HComplexData or ndarray + The data to multiply. + axis : int, optional + The dimension along which to multiply the data. + By default the last dimension is used. + select : Slice, optional + An optional selection of the spectrum data on which the multiplication is performed. + By default the entire data is used. + """ if axis is not None: axis = self.checkAxis(axis) if isinstance(data, Spectrum): @@ -318,13 +552,13 @@ def multiply(self, data, axis=None, select=slice(None)): if not self.noUndo: if not isinstance(data, hc.HComplexData): returnValue = lambda self: self.divide(data, axis, select=select) - elif self.data.hyper == data.hyper: #If both sets have same hyper: easy subtract can be used for undo + elif self.data.hyper == data.hyper: # If both sets have same hyper: easy subtract can be used for undo returnValue = lambda self: self.divide(data, axis, select=select) else: # Otherwise: do a deep copy of the class copyData = copy.deepcopy(self) returnValue = lambda self: self.restoreData(copyData, lambda self: self.multiply(data, axis, select)) self.data[select] *= data - if isinstance(data, (float,int)): + if isinstance(data, (float, int)): self.addHistory("Multiplied data[" + str(select) + "] with " + str(data)) else: self.addHistory("Multiplied data[" + str(select) + "]") @@ -342,8 +576,23 @@ def __div__(self, other): def __idiv__(self, other): self.divide(self, other) return self - + def divide(self, data, axis=None, select=slice(None)): + """ + Divide the spectrum data with the given data. + The division follows the Numpy broadcasting rules. + + Parameters + ---------- + data : Spectrum, HComplexData or ndarray + The data to divide with. + axis : int, optional + The dimension along which to divide the data. + By default the last dimension is used. + select : Slice, optional + An optional selection of the spectrum data on which the division is performed. + By default the entire data is used. + """ if axis is not None: axis = self.checkAxis(axis) if isinstance(data, Spectrum): @@ -362,7 +611,7 @@ def divide(self, data, axis=None, select=slice(None)): copyData = copy.deepcopy(self) returnValue = lambda self: self.restoreData(copyData, lambda self: self.divide(data, axis, select)) self.data[select] /= data - if isinstance(data, (float,int)): + if isinstance(data, (float, int)): self.addHistory("Divided data[" + str(select) + "] with " + str(data)) else: self.addHistory("Divided data[" + str(select) + "]") @@ -371,9 +620,34 @@ def divide(self, data, axis=None, select=slice(None)): self.undoList.append(returnValue) def normalize(self, mult, scale=1.0, type=0, axis=-1, select=slice(None)): + """ + Method used by the normalization. + + Parameters + ---------- + mult : float + The multiplier to normalize the data. + scale : float, optional + The scale to which to set the data. + By default the scale is 1. + type : int, optional + The type of normalization (0=integral, 1=maximum, 2=minimum, only used for the history message). + By default 0. + axis : int, optional + The dimension along which the normalization was performed. + By default the last dimension is used. + select : Slice, optional + An optional selection of the spectrum data on which the normalization was performed. + By default the entire data is used. + + Raises + ------ + SpectrumException + When the multiplication results in an error. + """ axis = self.checkAxis(axis) try: - self.data *= mult * scale + self.data *= mult * scale except ValueError as error: raise SpectrumException('Normalize: ' + str(error)) if type == 0: @@ -387,11 +661,26 @@ def normalize(self, mult, scale=1.0, type=0, axis=-1, select=slice(None)): self.undoList.append(lambda self: self.normalize(1.0 / mult, scale, type, axis, select=select)) def baselineCorrection(self, baseline, axis=-1, select=slice(None)): + """ + Applies a baseline correction. + + Parameters + ---------- + baseline : array_like + The baseline to subtract. + It follows the Numpy broadcasting rules. + axis : int, optional + The dimension along which the baseline correction is performed. + By default the last dimension is used. + select : Slice, optional + An optional selection of the spectrum data on which the baseline correction is performed. + By default the entire data is used. + """ axis = self.checkAxis(axis) baselinetmp = baseline.reshape((self.shape()[axis], ) + (1, ) * (self.ndim() - axis - 1)) self.data[select] -= baselinetmp Message = "Baseline corrected dimension " + str(axis + 1) - if type(select) is not slice: + if not isinstance(select, slice): Message = Message + " with slice " + str(select) elif select != slice(None, None, None): Message = Message + " with slice " + str(select) @@ -401,6 +690,15 @@ def baselineCorrection(self, baseline, axis=-1, select=slice(None)): self.undoList.append(lambda self: self.baselineCorrection(-baseline, axis, select=select)) def concatenate(self, axis=-1): + """ + Concatenates the data along a given dimension. + + Parameters + ---------- + axis : int, optional + The dimension along which to concatenate. + By default the last dimension is used. + """ axis = self.checkAxis(axis) splitVal = self.shape()[axis] copyData = None @@ -427,6 +725,18 @@ def concatenate(self, axis=-1): self.undoList.append(lambda self: self.split(splitVal, axis)) def split(self, sections, axis=-1): + """ + Splits the data into a given number of sections. + + Parameters + ---------- + sections : int + The number of sections. + The length of the data along axis should be divisible by this number. + axis : int, optional + The dimension along which to split. + By default the last dimension is used. + """ axis = self.checkAxis(axis) self.data = self.data.split(sections, axis) self.data.insertDim(0) @@ -444,6 +754,15 @@ def split(self, sections, axis=-1): self.undoList.append(lambda self: self.concatenate(axis)) def real(self, axis=-1): + """ + Sets the data to the real values along a given dimension. + + Parameters + ---------- + axis : int, optional + The dimension. + By default the last dimension is used. + """ if not self.noUndo: copyData = copy.deepcopy(self) axis = self.checkAxis(axis) @@ -454,6 +773,15 @@ def real(self, axis=-1): self.undoList.append(lambda self: self.restoreData(copyData, lambda self: self.real(axis))) def imag(self, axis=-1): + """ + Sets the data to the imaginary values along a given dimension. + + Parameters + ---------- + axis : int, optional + The dimension. + By default the last dimension is used. + """ if not self.noUndo: copyData = copy.deepcopy(self) axis = self.checkAxis(axis) @@ -464,6 +792,15 @@ def imag(self, axis=-1): self.undoList.append(lambda self: self.restoreData(copyData, lambda self: self.imag(axis))) def abs(self, axis=-1): + """ + Sets the data to the absolute values along a given dimension. + + Parameters + ---------- + axis : int, optional + The dimension. + By default the last dimension is used. + """ if not self.noUndo: copyData = copy.deepcopy(self) axis = self.checkAxis(axis) @@ -472,8 +809,17 @@ def abs(self, axis=-1): self.redoList = [] if not self.noUndo: self.undoList.append(lambda self: self.restoreData(copyData, lambda self: self.abs(axis))) - + def conj(self, axis=-1): + """ + Complex conjugates the data along a given dimension. + + Parameters + ---------- + axis : int, optional + The dimension. + By default the last dimension is used. + """ self.data = self.data.conj(axis) self.addHistory("Complex conjugate along" + str(axis+1)) self.redoList = [] @@ -481,6 +827,15 @@ def conj(self, axis=-1): self.undoList.append(lambda self: self.conj(axis)) def states(self, axis=-1): + """ + Converts the data to hypercomplex based on States acquisition along a given dimension. + + Parameters + ---------- + axis : int, optional + The dimension. + By default the last dimension is used. + """ axis = self.checkAxis(axis) if not self.noUndo: copyData = copy.deepcopy(self) @@ -492,6 +847,15 @@ def states(self, axis=-1): self.undoList.append(lambda self: self.restoreData(copyData, lambda self: self.states(axis))) def statesTPPI(self, axis=-1): + """ + Converts the data to hypercomplex based on States-TPPI acquisition along a given dimension. + + Parameters + ---------- + axis : int, optional + The dimension. + By default the last dimension is used. + """ axis = self.checkAxis(axis) if not self.noUndo: copyData = copy.deepcopy(self) @@ -503,6 +867,15 @@ def statesTPPI(self, axis=-1): self.undoList.append(lambda self: self.restoreData(copyData, lambda self: self.statesTPPI(axis))) def echoAntiEcho(self, axis=-1): + """ + Converts the data to hypercomplex based on echo/antiecho acquisition along a given dimension. + + Parameters + ---------- + axis : int, optional + The dimension. + By default the last dimension is used. + """ axis = self.checkAxis(axis) if not self.noUndo: copyData = copy.deepcopy(self) @@ -514,14 +887,34 @@ def echoAntiEcho(self, axis=-1): self.undoList.append(lambda self: self.restoreData(copyData, lambda self: self.echoAntiEcho(axis))) def subtractAvg(self, pos1=None, pos2=None, axis=-1): + """ + Subtracts the average values between given indices along a dimension. + + Parameters + ---------- + pos1 : int, optional + First index to determine the average. + 0 by default. + pos2 : int, optional + Second index to determine the average. + Length of the data along axis by default. + axis : int, optional + The dimension. + By default the last dimension is used. + + Raises + ------ + SpectrumException + When pos1 or pos2 are invalid. + """ axis = self.checkAxis(axis) if pos1 is None: pos1 = 0 if pos2 is None: pos2 = self.shape()[axis] - if not (0 <= pos1 <= self.shape()[axis]): + if not 0 <= pos1 <= self.shape()[axis]: raise SpectrumException("Indices not within range") - if not (0 <= pos2 <= self.shape()[axis]): + if not 0 <= pos2 <= self.shape()[axis]: raise SpectrumException("Indices not within range") if pos1 == pos2: raise SpectrumException("Indices cannot be equal") @@ -536,6 +929,30 @@ def subtractAvg(self, pos1=None, pos2=None, axis=-1): self.undoList.append(lambda self: self.add(averages)) def matrixManip(self, pos1=None, pos2=None, axis=-1, which=0): + """ + Performs the matrix manipulation methods such as integrate, sum, average, maximum, minimum, maximum position, and minimum position. + + Parameters + ---------- + pos1 : int or array_like, optional + First index/indices of the matrix manipulation. + 0 by default. + pos2 : int or array_like, optional + Second index/indices of the matrix manipulation. + Should have the same length as pos1. + Length of the data along axis by default. + axis : int, optional + The dimension. + By default the last dimension is used. + which : int, optional + The type of matrix manipulation to perform (0=integrate, 1=max, 2=min, 3=maxpos, 4=minpos, 5=sum, 6=average). + 0 by default. + + Raises + ------ + SpectrumException + When pos1 and pos2 have unequal lengths or when they contain invalid indices. + """ axis = self.checkAxis(axis) if pos1 is None: pos1 = 0 @@ -547,17 +964,14 @@ def matrixManip(self, pos1=None, pos2=None, axis=-1, which=0): if len(pos1) != len(pos2): raise SpectrumException("Length of the two arrays is not equal") if len(pos1) == 1: - if self.ndim() == 1: - keepdims = True - else: - keepdims = False + keepdims = self.ndim() == 1 else: keepdims = True tmpdata = () - for i in range(len(pos1)): - if not (0 <= pos1[i] <= self.shape()[axis]): + for i, _ in enumerate(pos1): + if not 0 <= pos1[i] <= self.shape()[axis]: raise SpectrumException("Indices not within range") - if not (0 <= pos2[i] <= self.shape()[axis]): + if not 0 <= pos2[i] <= self.shape()[axis]: raise SpectrumException("Indices not within range") if pos1[i] == pos2[i]: raise SpectrumException("Indices cannot be equal") @@ -611,6 +1025,22 @@ def matrixManip(self, pos1=None, pos2=None, axis=-1, which=0): self.resetXax(axis) def integrate(self, pos1=None, pos2=None, axis=-1): + """ + Reduce the data to the integrals between given indices. + + Parameters + ---------- + pos1 : array_like, optional + First indices to determine the integrals. + 0 by default. + pos2 : array_like, optional + Second indices to determine the integrals. + Should have the same length as pos1. + Length of the data along axis by default. + axis : int, optional + The dimension. + By default the last dimension is used. + """ axis = self.checkAxis(axis) if not self.noUndo: copyData = copy.deepcopy(self) @@ -621,6 +1051,22 @@ def integrate(self, pos1=None, pos2=None, axis=-1): self.addHistory("Integrate between " + str(pos1) + " and " + str(pos2) + " of dimension " + str(axis + 1)) def max(self, pos1=None, pos2=None, axis=-1): + """ + Reduce the data to the maxima between given indices. + + Parameters + ---------- + pos1 : array_like, optional + First indices to determine the maxima. + 0 by default. + pos2 : array_like, optional + Second indices to determine the maxima. + Should have the same length as pos1. + Length of the data along axis by default. + axis : int, optional + The dimension. + By default the last dimension is used. + """ axis = self.checkAxis(axis) if not self.noUndo: copyData = copy.deepcopy(self) @@ -631,6 +1077,22 @@ def max(self, pos1=None, pos2=None, axis=-1): self.addHistory("Maximum between " + str(pos1) + " and " + str(pos2) + " of dimension " + str(axis + 1)) def min(self, pos1=None, pos2=None, axis=-1): + """ + Reduce the data to the minima between given indices. + + Parameters + ---------- + pos1 : array_like, optional + First indices to determine the minima. + 0 by default. + pos2 : array_like, optional + Second indices to determine the minima. + Should have the same length as pos1. + Length of the data along axis by default. + axis : int, optional + The dimension. + By default the last dimension is used. + """ axis = self.checkAxis(axis) if not self.noUndo: copyData = copy.deepcopy(self) @@ -641,6 +1103,22 @@ def min(self, pos1=None, pos2=None, axis=-1): self.addHistory("Minimum between " + str(pos1) + " and " + str(pos2) + " of dimension " + str(axis + 1)) def argmax(self, pos1=None, pos2=None, axis=-1): + """ + Reduce the data to the maxima positions between given indices. + + Parameters + ---------- + pos1 : array_like, optional + First indices to determine the maxima positions. + 0 by default. + pos2 : array_like, optional + Second indices to determine the maxima positions. + Should have the same length as pos1. + Length of the data along axis by default. + axis : int, optional + The dimension. + By default the last dimension is used. + """ axis = self.checkAxis(axis) if not self.noUndo: copyData = copy.deepcopy(self) @@ -651,6 +1129,22 @@ def argmax(self, pos1=None, pos2=None, axis=-1): self.addHistory("Maximum position between " + str(pos1) + " and " + str(pos2) + " of dimension " + str(axis + 1)) def argmin(self, pos1=None, pos2=None, axis=-1): + """ + Reduce the data to the minima positions between given indices. + + Parameters + ---------- + pos1 : array_like, optional + First indices to determine the minima positions. + 0 by default. + pos2 : array_like, optional + Second indices to determine the minima positions. + Should have the same length as pos1. + Length of the data along axis by default. + axis : int, optional + The dimension. + By default the last dimension is used. + """ axis = self.checkAxis(axis) if not self.noUndo: copyData = copy.deepcopy(self) @@ -661,6 +1155,22 @@ def argmin(self, pos1=None, pos2=None, axis=-1): self.addHistory("Minimum position between " + str(pos1) + " and " + str(pos2) + " of dimension " + str(axis + 1)) def sum(self, pos1=None, pos2=None, axis=-1): + """ + Reduce the data to the sum between given indices. + + Parameters + ---------- + pos1 : array_like, optional + First indices to determine the sum. + 0 by default. + pos2 : array_like, optional + Second indices to determine the sum. + Should have the same length as pos1. + Length of the data along axis by default. + axis : int, optional + The dimension. + By default the last dimension is used. + """ axis = self.checkAxis(axis) if not self.noUndo: copyData = copy.deepcopy(self) @@ -671,6 +1181,22 @@ def sum(self, pos1=None, pos2=None, axis=-1): self.addHistory("Sum between " + str(pos1) + " and " + str(pos2) + " of dimension " + str(axis + 1)) def average(self, pos1=None, pos2=None, axis=-1): + """ + Reduce the data to the average between given indices. + + Parameters + ---------- + pos1 : array_like, optional + First indices to determine the average. + 0 by default. + pos2 : array_like, optional + Second indices to determine the average. + Should have the same length as pos1. + Length of the data along axis by default. + axis : int, optional + The dimension. + By default the last dimension is used. + """ axis = self.checkAxis(axis) if not self.noUndo: copyData = copy.deepcopy(self) @@ -681,6 +1207,21 @@ def average(self, pos1=None, pos2=None, axis=-1): self.addHistory("Average between " + str(pos1) + " and " + str(pos2) + " of dimension " + str(axis + 1)) def extract(self, pos1=None, pos2=None, axis=-1): + """ + Extract the data between two given indices along a dimension and make it the new data. + + Parameters + ---------- + pos1 : int, optional + First index to extract. + 0 by default. + pos2 : int, optional + Second index to extract. + Length of the data along axis by default. + axis : int, optional + The dimension. + By default the last dimension is used. + """ axis = self.checkAxis(axis) if pos1 is None: pos1 = 0 @@ -707,6 +1248,25 @@ def extract(self, pos1=None, pos2=None, axis=-1): self.undoList.append(lambda self: self.restoreData(copyData, lambda self: self.extract(pos1, pos2, axis))) def fiddle(self, refSpec, lb, axis=-1): + """ + Performs reference deconvolution using the FIDDLE algorithm. + + Parameters + ---------- + refSpec : array_like + The reference spectrum with which to deconvolute the spectrum. + Should have the same length as the data along axis. + lb : float + The linebroadening (in Hz) to be applied during reference deconvolution. + axis : int, optional + The dimension. + By default the last dimension is used. + + Raises + ------ + SpectrumException + When the reference FID does not have the same length as the data along axis. + """ axis = self.checkAxis(axis) axLen = self.shape()[axis] if len(refSpec) != axLen: @@ -740,6 +1300,15 @@ def fiddle(self, refSpec, lb, axis=-1): self.undoList.append(lambda self: self.restoreData(copyData, lambda self: self.fiddle(refSpec, lb, axis))) def diff(self, axis=-1): + """ + The discrete difference along a given dimension. + + Parameters + ---------- + axis : int, optional + The dimension. + By default the last dimension is used. + """ axis = self.checkAxis(axis) if not self.noUndo: copyData = copy.deepcopy(self) @@ -751,6 +1320,15 @@ def diff(self, axis=-1): self.undoList.append(lambda self: self.restoreData(copyData, lambda self: self.diff(axis))) def cumsum(self, axis=-1): + """ + The cumulative sum along a given dimension. + + Parameters + ---------- + axis : int, optional + The dimension. + By default the last dimension is used. + """ axis = self.checkAxis(axis) if not self.noUndo: copyData = copy.deepcopy(self) @@ -761,6 +1339,15 @@ def cumsum(self, axis=-1): self.undoList.append(lambda self: self.restoreData(copyData, lambda self: self.cumsum(axis))) def flipLR(self, axis=-1): + """ + Flips the data along a given dimension. + + Parameters + ---------- + axis : int, optional + The dimension. + By default the last dimension is used. + """ axis = self.checkAxis(axis) slicing = (slice(None), ) * axis + (slice(None, None, -1), ) self.data = self.data[slicing] @@ -770,6 +1357,15 @@ def flipLR(self, axis=-1): self.undoList.append(lambda self: self.flipLR(axis)) def hilbert(self, axis=-1): + """ + Performs a Hilbert transform along a given dimension. + + Parameters + ---------- + axis : int, optional + The dimension. + By default the last dimension is used. + """ axis = self.checkAxis(axis) if not self.noUndo: copyData = copy.deepcopy(self) @@ -782,6 +1378,19 @@ def hilbert(self, axis=-1): self.undoList.append(lambda self: self.restoreData(copyData, lambda self: self.hilbert(axis))) def autoPhaseAll(self, phaseNum=0, axis=-1): + """ + Autophases all traces along a given axis individually. + + Parameters + ---------- + phaseNum : {0, 1}, optional + Order up to which to perform the autophasing. + For 0 only zero order phasing is performed, for 1 both zero and first order phasing is performed. + 0 by default. + axis : int, optional + The dimension. + By default the last dimension is used. + """ axis = self.checkAxis(axis) if not self.noUndo: copyData = copy.deepcopy(self) @@ -790,7 +1399,7 @@ def autoPhaseAll(self, phaseNum=0, axis=-1): rangeList = [range(i) for i in shape] for i in itertools.product(*rangeList): locList = np.insert(i, axis, 0) - selectList = np.insert(np.array(i,dtype=object), axis, slice(None)) + selectList = np.insert(np.array(i, dtype=object), axis, slice(None)) self.autoPhase(phaseNum, axis, locList, False, selectList) if phaseNum == 1: self.addHistory("Autophased per trace for 0 + 1 order along axis " + str(axis + 1)) @@ -801,6 +1410,33 @@ def autoPhaseAll(self, phaseNum=0, axis=-1): self.undoList.append(lambda self: self.restoreData(copyData, lambda self: self.autoPhaseAll(phaseNum, axis))) def autoPhase(self, phaseNum=0, axis=-1, locList=None, returnPhases=False, select=slice(None)): + """ + Autophases a spectrum. + + Parameters + ---------- + phaseNum : {0, 1}, optional + Order up to which to perform the autophasing. + For 0 only zero order phasing is performed, for 1 both zero and first order phasing is performed. + 0 by default. + axis : int, optional + The dimension. + By default the last dimension is used. + locList : array_like of int, optional + The indices of the trace to determine the phase values. + By default the first index of each dimension is used. + returnPhases : bool, optional + If True the determined phases are return. + False by default. + select : Slice, optional + An optional selection of the spectrum data on which the phasing is performed. + By default the entire data is used. + + Raises + ------ + SpectrumException + When locList does not have the same length as the number of dimensions or when locList contains invalid indices. + """ axis = self.checkAxis(axis) if locList is None: locList = [0]*self.ndim() @@ -814,15 +1450,14 @@ def autoPhase(self, phaseNum=0, axis=-1, locList=None, returnPhases=False, selec if self.spec[axis] == 0: self.__fourier(axis, tmp=True) tmp = self.data[locList] - tmp = tmp.getHyperData(0) + tmp = tmp.getHyperData(0) # only optimize on the hyper real data x = np.fft.fftshift(np.fft.fftfreq(len(tmp), 1.0 / self.sw[axis])) / self.sw[axis] - # only optimize on the hyper real data if phaseNum == 0: - phases = scipy.optimize.minimize(self.ACMEentropy, [0], (tmp, x, False), method='Powell',options = {'xtol': AUTOPHASETOL}) + phases = scipy.optimize.minimize(func.ACMEentropy, [0], (tmp, x, False), method='Powell', options={'xtol': AUTOPHASETOL}) phase0 = phases['x'] phase1 = 0.0 elif phaseNum == 1: - phases = scipy.optimize.minimize(self.ACMEentropy, [0, 0], (tmp, x), method='Powell', options = {'xtol': AUTOPHASETOL}) + phases = scipy.optimize.minimize(func.ACMEentropy, [0, 0], (tmp, x), method='Powell', options={'xtol': AUTOPHASETOL}) phase0 = phases['x'][0] phase1 = phases['x'][1] if self.ref[axis] is None: @@ -840,33 +1475,11 @@ def autoPhase(self, phaseNum=0, axis=-1, locList=None, returnPhases=False, selec if returnPhases: if phaseNum == 0: return [phases['x']] - else: - return phases['x'] + return phases['x'] self.redoList = [] if not self.noUndo: self.undoList.append(lambda self: self.phase(-phase0, -phase1, axis)) - def ACMEentropy(self, phaseIn, data, x, phaseAll=True): - phase0 = phaseIn[0] - if phaseAll: - phase1 = phaseIn[1] - else: - phase1 = 0.0 - L = len(data) - s0 = data * np.exp(1j * (phase0 + phase1 * x)) - s2 = np.real(s0) - ds1 = np.abs((s2[3:L] - s2[1:L - 2]) / 2.0) - p1 = ds1 / sum(ds1) - p1[np.where(p1 == 0)] = 1 - h1 = -p1 * np.log(p1) - H1 = sum(h1) - Pfun = 0.0 - as1 = s2 - np.abs(s2) - sumas = sum(as1) - if (np.real(sumas) < 0): - Pfun = Pfun + sum(as1**2) / 4 / L**2 - return H1 + 1000 * Pfun - def __phase(self, phase0, phase1, offset, axis, select=slice(None)): vector = np.exp(np.fft.fftshift(np.fft.fftfreq(self.shape()[axis], 1.0 / self.sw[axis]) + offset) / self.sw[axis] * phase1 * 1j) if self.spec[axis] == 0: @@ -878,34 +1491,103 @@ def __phase(self, phase0, phase1, offset, axis, select=slice(None)): if self.spec[axis] == 0: self.__invFourier(axis, tmp=True) - def phase(self, phase0=0.0, phase1=0.0, axis=-1, select=slice(None), internal = False): + def phase(self, phase0=0.0, phase1=0.0, axis=-1, select=slice(None)): + """ + Phases a spectrum along a given dimension. + + Parameters + ---------- + phase0 : float, optional + Zero order phase. + 0.0 by default. + phase1 : float, optional + First order phase. + 0.0 by default. + axis : int, optional + The dimension. + By default the last dimension is used. + select : Slice, optional + An optional selection of the spectrum data on which the phasing is performed. + By default the entire data is used. + """ axis = self.checkAxis(axis) if self.ref[axis] is None: offset = 0 else: offset = self.freq[axis] - self.ref[axis] self.__phase(phase0, phase1, offset, axis, select=select) - if not internal: - Message = "Phasing: phase0 = " + str(phase0 * 180 / np.pi) + " and phase1 = " + str(phase1 * 180 / np.pi) + " for dimension " + str(axis + 1) - if type(select) is not slice: - Message = Message + " with slice " + str(select) - elif select != slice(None, None, None): - Message = Message + " with slice " + str(select) - self.addHistory(Message) - self.redoList = [] - if not self.noUndo: - self.undoList.append(lambda self: self.phase(-phase0, -phase1, axis, select=select)) + Message = "Phasing: phase0 = " + str(phase0 * 180 / np.pi) + " and phase1 = " + str(phase1 * 180 / np.pi) + " for dimension " + str(axis + 1) + if not isinstance(select, slice): + Message = Message + " with slice " + str(select) + elif select != slice(None, None, None): + Message = Message + " with slice " + str(select) + self.addHistory(Message) + self.redoList = [] + if not self.noUndo: + self.undoList.append(lambda self: self.phase(-phase0, -phase1, axis, select=select)) + + def correctDFilter(self, axis=-1): + """ + Corrects the digital filter via first order phasing along a given dimension. - def correctDFilter(self, axis=-1, undo = False): - #Corrects the digital filter via first order phasing - self.phase(0,self.dFilter,axis,internal = True) + Parameters + ---------- + axis : int, optional + The dimension. + By default the last dimension is used. + """ + axis = self.checkAxis(axis) + if self.ref[axis] is None: + offset = 0 + else: + offset = self.freq[axis] - self.ref[axis] + self.__phase(0, self.dFilter, offset, axis) Message = "Corrected digital filter" self.addHistory(Message) self.redoList = [] if not self.noUndo: - self.undoList.append(lambda self: self.phase(0,-self.dFilter,axis,internal = False,)) + self.undoList.append(lambda self: self.phase(0, -self.dFilter, offset, axis)) def apodize(self, lor=None, gauss=None, cos2=[None, None], hamming=None, shift=0.0, shifting=0.0, shiftingAxis=None, axis=-1, select=slice(None), preview=False): + """ + Apodizes an FID or spectrum along a given dimension. + + Parameters + ---------- + lor : float, optional + The Lorentzian component of the apodization window. + By default Lorentzian apodization is not applied. + gauss : float, optional + The Gaussian component of the apodization window. + By default Gaussian apodization is not applied. + cos2 : array_like, optional + Defines the squared cosine apodization component. + Should have a length of at least two. + The first value is the frequency (two times the number of periods in the time domain). + The second value is the phase shift in degrees. + By default squared cosine apodization is not applied. + hamming : float, optional + The Hamming window component. + By default Hamming apodization is not applied. + shift : float, optional + A shift in time of the function. + A positive value shift the curve to later times. + By default a shift of 0.0 is used. + shifting : float, optional + A shift in time of the function as a function of the x-axis values along shiftingAxis. + A positive value shift the curve to later times. + By default a shifting of 0.0 is used. + shiftingAxis : int, optional + The dimension for the shifting. + If this parameter is None, no shifting is applied. + By default shifting is not used. + axis : int, optional + The dimension along which the apodization is applied. + By default the last dimension is used. + select : Slice, optional + An optional selection of the spectrum data on which the apodization is performed. + By default the entire data is used. + """ axis = self.checkAxis(axis) if shiftingAxis is None: shiftingAxis = 0 @@ -919,7 +1601,7 @@ def apodize(self, lor=None, gauss=None, cos2=[None, None], hamming=None, shift=0 shift1 = shift + shifting * np.arange(self.shape()[shiftingAxis]) / self.sw[shiftingAxis] else: shift1 = shift + shifting * self.xaxArray[shiftingAxis] - previewData = np.array([func.apodize(t, s, self.sw[axis], axLen, lor, gauss, cos2, hamming, self.wholeEcho[axis]) for s in shift1]) + previewData = np.array([func.apodize(t, s, lor, gauss, cos2, hamming, self.wholeEcho[axis]) for s in shift1]) if axis < shiftingAxis: previewData = np.swapaxis(previewData, 0, 1) multShape = np.ones(len(self.shape()), dtype=int) @@ -937,7 +1619,7 @@ def apodize(self, lor=None, gauss=None, cos2=[None, None], hamming=None, shift=0 if self.spec[axis] > 0: self.__fourier(axis, tmp=True) else: - x = func.apodize(t, shift, self.sw[axis], axLen, lor, gauss, cos2, hamming, self.wholeEcho[axis]) + x = func.apodize(t, shift, lor, gauss, cos2, hamming, self.wholeEcho[axis]) if preview: previewData = [x] * int(np.prod(self.data.shape()) / self.data.shape()[axis]) if self.spec[axis] > 0: @@ -966,7 +1648,7 @@ def apodize(self, lor=None, gauss=None, cos2=[None, None], hamming=None, shift=0 if lor is None and gauss is None and cos2 is None and hamming is None: # If all none, make special message with `zero apodization' Message = Message + "zero apodization" Message = Message + " for dimension " + str(axis + 1) - if type(select) is not slice: + if not isinstance(select, slice): Message = Message + " with slice " + str(select) elif select != slice(None, None, None): Message = Message + " with slice " + str(select) @@ -978,6 +1660,21 @@ def apodize(self, lor=None, gauss=None, cos2=[None, None], hamming=None, shift=0 self.undoList.append(lambda self: self.restoreData(copyData, lambda self: self.apodize(lor, gauss, cos2, hamming, shift, shifting, shiftingAxis, axis, select=select))) def setFreq(self, freq=None, sw=None, axis=-1): + """ + Sets the frequency or spectral width of a certain dimension to a given value. + + Parameters + ---------- + freq : float, optional + The new frequency of axis in Hz + By default the frequency is unchanged. + sw : float, optional + The new spectral width of axis in Hz. + By default the spectral width is unchanged. + axis : int, optional + The dimension. + By default the last dimension is used. + """ axis = self.checkAxis(axis) oldFreq = self.freq[axis] oldSw = self.sw[axis] @@ -994,6 +1691,17 @@ def setFreq(self, freq=None, sw=None, axis=-1): self.undoList.append(lambda self: self.setFreq(oldFreq, oldSw, axis)) def scaleSw(self, scale, axis=-1): + """ + Scales the spectral with of a dimension by a given scaling factor. + + Parameters + ---------- + scale : float + The scaling factor. + axis : int, optional + The dimension. + By default the last dimension is used. + """ axis = self.checkAxis(axis) oldSw = self.sw[axis] self.sw[axis] = float(scale) * oldSw @@ -1004,6 +1712,19 @@ def scaleSw(self, scale, axis=-1): self.undoList.append(lambda self: self.scaleSw(1.0 / scale, axis)) def setRef(self, ref=None, axis=-1): + """ + Sets the reference frequency to a given value. + + Parameters + ---------- + ref : float or None, optional + The reference frequency in Hz. + If None, the reference is removed. + None by default. + axis : int, optional + The dimension. + By default the last dimension is used. + """ axis = self.checkAxis(axis) oldRef = self.ref[axis] if ref is None: @@ -1018,7 +1739,19 @@ def setRef(self, ref=None, axis=-1): self.undoList.append(lambda self: self.setRef(oldRef, axis)) def regrid(self, limits, numPoints, axis=-1): - oldLimits = [self.xaxArray[axis][0], self.xaxArray[axis][-1]] + """ + Regrids the data along a dimension to match a given number of points. + + Parameters + ---------- + limits : list of float + The left and right limit of the new x-axis for the regrid. + numPoints : int + The number of points the data should have after the regrid. + axis : int, optional + The dimension. + By default the last dimension is used. + """ if not self.noUndo: copyData = copy.deepcopy(self) newSw = (limits[1] - limits[0]) / (numPoints - 1) * numPoints @@ -1041,6 +1774,17 @@ def regrid(self, limits, numPoints, axis=-1): self.undoList.append(lambda self: self.restoreData(copyData, lambda self: self.regrid(limits, numPoints, axis))) def setWholeEcho(self, val, axis=-1): + """ + Sets the Whole Echo attribute for a given dimension. + + Parameters + ---------- + val : bool + The new Whole Echo setting. + axis : int, optional + The dimension. + By default the last dimension is used. + """ axis = self.checkAxis(axis) self.wholeEcho[axis] = val self.addHistory("Whole echo set to " + str(val) + " for dimension " + str(axis + 1)) @@ -1049,6 +1793,19 @@ def setWholeEcho(self, val, axis=-1): self.undoList.append(lambda self: self.setWholeEcho(not val, axis)) def resize(self, size, pos, axis=-1): + """ + Resizes the data along a dimension by zerofilling in the time domain. + + Parameters + ---------- + size : int + The new size along axis. + pos : int + The index after which the zeros are added in the time domain. + axis : int, optional + The dimension. + By default the last dimension is used. + """ axis = self.checkAxis(axis) if not self.noUndo: copyData = copy.deepcopy(self) @@ -1064,6 +1821,31 @@ def resize(self, size, pos, axis=-1): self.undoList.append(lambda self: self.restoreData(copyData, lambda self: self.resize(size, pos, axis))) def lpsvd(self, nPredict, maxFreq, forward=False, numPoints=None, axis=-1): + """ + Performs linear prediction using the LPSVD algorithm. + + Parameters + ---------- + nPredict : int + The number of datapoints to predict. + maxFreq : int + The maximum number of frequencies to take from the SVD. + forward : bool, optional + If True, a forward prediction is performed, otherwise a backward prediction is performed. + False by default. + numPoints : int, optional + The number of points to use for SVD. + For efficiency this number can be reduced. + By default the entire data is used. + axis : int, optional + The dimension. + By default the last dimension is used. + + Raises + ------ + SpectrumException + When the LPSVD algorithm resulted in an error. + """ failed = False axis = self.checkAxis(axis) if not self.noUndo: @@ -1090,6 +1872,18 @@ def lpsvd(self, nPredict, maxFreq, forward=False, numPoints=None, axis=-1): self.undoList.append(lambda self: self.restoreData(copyData, lambda self: self.lpsvd(nPredict, maxFreq, forward, numPoints, axis))) def setSpec(self, val, axis=-1): + """ + Sets the spectrum flag for a given dimension. + + Parameters + ---------- + val : bool + If True, the dimension is treated as a spectral domain. + If False, the dimension is treated as a time domain. + axis : int, optional + The dimension. + By default the last dimension is used. + """ axis = self.checkAxis(axis) oldVal = self.spec[axis] self.spec[axis] = val @@ -1103,6 +1897,17 @@ def setSpec(self, val, axis=-1): self.undoList.append(lambda self: self.setSpec(oldVal, axis)) def swapEcho(self, idx, axis=-1): + """ + Swaps an echo signal over a given index. + + Parameters + ---------- + idx : int + The index over which the data is swapped. + axis : int, optional + The dimension. + By default the last dimension is used. + """ axis = self.checkAxis(axis) slicing1 = (slice(None), ) * axis + (slice(None, idx), ) slicing2 = (slice(None), ) * axis + (slice(idx, None), ) @@ -1113,7 +1918,21 @@ def swapEcho(self, idx, axis=-1): if not self.noUndo: self.undoList.append(lambda self: self.swapEcho(-idx, axis)) - def shift(self, shift, axis=-1, select=slice(None), zeros=True): + def shift(self, shift, axis=-1, select=slice(None)): + """ + Shifts the data a given number of datapoints. + + Parameters + ---------- + shift : int + The number of datapoints to shift. + axis : int, optional + The dimension. + By default the last dimension is used. + select : Slice, optional + An optional selection of the spectrum data on which the shift is performed. + By default the entire data is used. + """ axis = self.checkAxis(axis) if not self.noUndo: copyData = copy.deepcopy(self) @@ -1125,21 +1944,35 @@ def shift(self, shift, axis=-1, select=slice(None), zeros=True): else: mask[slice(None, shift)] = 0 self.data[select] = self.data.roll(shift, axis)[select] - if zeros: - self.data[select] *= mask.reshape(mask.shape + (1,)*(self.ndim()-axis-1)) + self.data[select] *= mask.reshape(mask.shape + (1,)*(self.ndim()-axis-1)) if self.spec[axis] > 0: self.__fourier(axis, tmp=True) Message = "Shifted " + str(shift) + " points in dimension " + str(axis + 1) - if type(select) is not slice: + if not isinstance(select, slice): Message = Message + " with slice " + str(select) elif select != slice(None, None, None): Message = Message + " with slice " + str(select) self.addHistory(Message) self.redoList = [] if not self.noUndo: - self.undoList.append(lambda self: self.restoreData(copyData, lambda self: self.shift(shift, axis, select=select, zeros=zeros))) + self.undoList.append(lambda self: self.restoreData(copyData, lambda self: self.shift(shift, axis, select=select))) def roll(self, shift, axis=-1, select=slice(None)): + """ + Rolls the data by using a first order phase change. + This allows rolling also using non-integer values. + + Parameters + ---------- + roll : float + The distance to roll the data along axis. + axis : int, optional + The dimension. + By default the last dimension is used. + select : Slice, optional + An optional selection of the spectrum data on which the shift is performed. + By default the entire data is used. + """ axis = self.checkAxis(axis) if self.spec[axis] == 0: self.__phase(0, -2 * np.pi * shift, 0, axis, select=select) @@ -1149,20 +1982,38 @@ def roll(self, shift, axis=-1, select=slice(None)): self.__invFourier(axis, tmp=True) t = t.reshape(t.shape + (1, )*(self.ndim()-axis-1)) self.data.icomplexReorder(axis) - self.data[select] *= np.exp( - 1j * t * freq * shift * 2 * np.pi) + self.data[select] *= np.exp(-1j * t * freq * shift * 2 * np.pi) self.data.icomplexReorder(axis) self.__fourier(axis, tmp=True) Message = "Rolled " + str(shift) + " points in dimension " + str(axis + 1) - if type(select) is not slice: + if not isinstance(select, slice): Message = Message + " with slice " + str(select) elif select != slice(None, None, None): Message = Message + " with slice " + str(select) self.addHistory(Message) self.redoList = [] if not self.noUndo: - self.undoList.append(lambda self: self.roll(-shift,axis)) + self.undoList.append(lambda self: self.roll(-shift, axis)) def align(self, pos1=None, pos2=None, axis=-1): + """ + Aligns the maxima between given indices along a certain dimension. + + Parameters + ---------- + pos1 : int + The first index between which to determine the maximum. + pos2 : int + The second index between which to determine the maximum. + axis : int, optional + The dimension. + By default the last dimension is used. + + Raises + ------ + SpectrumException + When pos1 or pos2 is invalid. + """ axis = self.checkAxis(axis) if not self.noUndo: copyData = copy.deepcopy(self) @@ -1170,9 +2021,9 @@ def align(self, pos1=None, pos2=None, axis=-1): pos1 = 0 if pos2 is None: pos2 = self.shape()[axis] - if not (0 <= pos1 <= self.shape()[axis]): + if not 0 <= pos1 <= self.shape()[axis]: raise SpectrumException("Indices not within range") - if not (0 <= pos2 <= self.shape()[axis]): + if not 0 <= pos2 <= self.shape()[axis]: raise SpectrumException("Indices not within range") if pos1 == pos2: raise SpectrumException("Indices cannot be equal") @@ -1186,15 +2037,17 @@ def align(self, pos1=None, pos2=None, axis=-1): shape = np.delete(shape, axis) rangeList = [range(i) for i in shape] for i in itertools.product(*rangeList): - selectList = np.insert(np.array(i,dtype=object), axis, slice(None)) + selectList = np.insert(np.array(i, dtype=object), axis, slice(None)) self.data[selectList] = self.data[selectList].roll(maxArgPos[0][tuple(i)], 0) self.addHistory("Maxima aligned between " + str(minPos) + " and " + str(maxPos) + " along axis " + str(axis)) self.redoList = [] if not self.noUndo: self.undoList.append(lambda self: self.restoreData(copyData, lambda self: self.align(pos1, pos2, axis))) - - def __fourier(self, axis, tmp=False, reorder=[True,True]): + + def __fourier(self, axis, tmp=False, reorder=None): axis = self.checkAxis(axis) + if reorder is None: + reorder = [True, True] if reorder[0]: self.data.icomplexReorder(axis) if not self.wholeEcho[axis] and not tmp: @@ -1207,8 +2060,10 @@ def __fourier(self, axis, tmp=False, reorder=[True,True]): self.data.icomplexReorder(axis) self.resetXax(axis) - def __invFourier(self, axis, tmp=False, reorder=[True,True]): + def __invFourier(self, axis, tmp=False, reorder=None): axis = self.checkAxis(axis) + if reorder is None: + reorder = [True, True] if reorder[0]: self.data.icomplexReorder(axis) self.data = self.data.ifftshift(axis).ifft(axis) @@ -1222,6 +2077,15 @@ def __invFourier(self, axis, tmp=False, reorder=[True,True]): self.resetXax(axis) def complexFourier(self, axis=-1): + """ + Perform a complex Fourier transform along a given dimension. + + Parameters + ---------- + axis : int, optional + The dimension. + By default the last dimension is used. + """ if self.spec[axis] == 0: self.__fourier(axis) self.addHistory("Fourier transform dimension " + str(axis + 1)) @@ -1233,6 +2097,15 @@ def complexFourier(self, axis=-1): self.undoList.append(lambda self: self.complexFourier(axis)) def realFourier(self, axis=-1): + """ + Perform a real Fourier transform along a given dimension. + + Parameters + ---------- + axis : int, optional + The dimension. + By default the last dimension is used. + """ if not self.noUndo: copyData = copy.deepcopy(self) axis = self.checkAxis(axis) @@ -1248,6 +2121,19 @@ def realFourier(self, axis=-1): self.undoList.append(lambda self: self.restoreData(copyData, lambda self: self.realFourier(axis))) def fftshift(self, axis=-1, inv=False): + """ + Perform an fftshift or inverse fftshift along a given dimension. + This shifts the zero-frequency component to the center of the spectrum. + + Parameters + ---------- + axis : int, optional + The dimension. + By default the last dimension is used. + inv : bool, optional + If True, the inverse fftshift is performed. + False by default . + """ axis = self.checkAxis(axis) if inv: self.data = self.data.ifftshift(axis=axis) @@ -1260,6 +2146,29 @@ def fftshift(self, axis=-1, inv=False): self.undoList.append(lambda self: self.fftshift(axis, not(inv))) def shear(self, shear, axis=-1, axis2=-2, toRef=False): + """ + Shears the data along given axes. + + Parameters + ---------- + shear : float + The shearing factor defined in ratio between the x-axes of axis and axis2. + axis : int, optional + The dimension along which the shearing is applied. + By default the last dimension is used. + axis2 : int, optional + The shearing axis. + By default the second last dimension is used. + toRef : bool, optional + If True, the shearing is applied relative to the reference frequencies of both dimensions. + Otherwise the shearing is applied relative to the center of the data. + False by default. + + Raises + ------ + SpectrumException + When axis2 equals axis or when the data does not have more one dimension. + """ axis = self.checkAxis(axis) axis2 = self.checkAxis(axis2) if axis == axis2: @@ -1267,7 +2176,7 @@ def shear(self, shear, axis=-1, axis2=-2, toRef=False): if self.ndim() < 2: raise SpectrumException("The data does not have enough dimensions for a shearing transformation") if self.spec[axis] > 0: #rorder and fft for spec - self.__invFourier(axis, tmp=True, reorder=[True,False]) + self.__invFourier(axis, tmp=True, reorder=[True, False]) else: #Reorder if FID self.data.icomplexReorder(axis) shape = self.shape() @@ -1285,7 +2194,7 @@ def shear(self, shear, axis=-1, axis2=-2, toRef=False): shearMatrix = np.exp(1j * np.outer(vec1, vec2)) self.data *= shearMatrix.reshape(shape) if self.spec[axis] > 0: - self.__fourier(axis, tmp=True, reorder=[False,True]) + self.__fourier(axis, tmp=True, reorder=[False, True]) else: self.data.icomplexReorder(axis) self.addHistory("Shearing transform with shearing value " + str(shear) + " over dimensions " + str(axis + 1) + " and " + str(axis2 + 1)) @@ -1293,7 +2202,21 @@ def shear(self, shear, axis=-1, axis2=-2, toRef=False): if not self.noUndo: self.undoList.append(lambda self: self.shear(-shear, axis, axis2, toRef)) - def reorder(self, pos, newLength, axis=-1): + def reorder(self, pos, newLength=None, axis=-1): + """ + Reorders the data based on a given list of indices. + + Parameters + ---------- + pos : array_like + The positions of the subarrays in the new data. + newLength : int, optional + The new length of the data along axis. + By default one more than the largest value in pos is used. + axis : int, optional + The axis along which the data is reordered. + By default the last dimension is used. + """ axis = self.checkAxis(axis) if not self.noUndo: copyData = copy.deepcopy(self) @@ -1305,11 +2228,25 @@ def reorder(self, pos, newLength, axis=-1): self.undoList.append(lambda self: self.restoreData(copyData, lambda self: self.reorder(pos, newLength, axis))) def ffm(self, pos, typeVal, axis=-1): + """ + Uses the fast forward maximum entropy algorithm to reconstruct non-uniform sampled data. + + Parameters + ---------- + pos : array_like + A list of indices that are recorded datapoints. + All other datapoints will be reconstructed. + typeVal : {0, 1, 2} + The type of data to be reconstructed. + 0=complex, 1=States or States-TPPI, 2=TPPI. + axis : int, optional + The axis along which the data is reconstructed. + By default the last dimension is used. + """ axis = self.checkAxis(axis) if not self.noUndo: copyData = copy.deepcopy(self) - # pos contains the values of fixed points which not to be translated to missing points - posList = np.delete(range(self.shape()[axis]), pos) + posList = np.delete(range(self.shape()[axis]), pos) # pos contains the values of fixed points which not to be translated to missing points if typeVal == 1: # type is States or States-TPPI, the positions need to be divided by 2 posList = np.array(np.floor(posList / 2), dtype=int) if typeVal == 2: # type is TPPI, for now handle the same as Complex @@ -1321,24 +2258,42 @@ def ffm(self, pos, typeVal, axis=-1): tmpShape = tmpData.shape tmpData = tmpData.reshape((int(tmpData.size / tmpShape[-1]), tmpShape[-1])) pool = multiprocessing.Pool(multiprocessing.cpu_count()) - fit = pool.map_async(ffm, [(i, posList) for i in tmpData]) + fit = pool.map_async(nus.ffm, [(i, posList) for i in tmpData]) pool.close() pool.join() tmpData = np.rollaxis(np.array(fit.get()).reshape(tmpShape), -1, axis) self.data = hc.HComplexData(tmpData) - #Transform back to FID - self.__invFourier(axis, tmp=True) + self.__invFourier(axis, tmp=True) # Transform back to FID self.addHistory("Fast Forward Maximum Entropy reconstruction of dimension " + str(axis + 1) + " at positions " + str(pos)) self.redoList = [] if not self.noUndo: self.undoList.append(lambda self: self.restoreData(copyData, None)) def clean(self, pos, typeVal, axis, gamma, threshold, maxIter): + """ + Uses the CLEAN algorithm to reconstruct non-uniform sampled data. + + Parameters + ---------- + pos : array_like + A list of indices that are recorded datapoints. + All other datapoints will be reconstructed. + typeVal : {0, 1, 2} + The type of data to be reconstructed. + 0=complex, 1=States or States-TPPI, 2=TPPI. + axis : int + The axis along which the data is reconstructed. + gamma : float + Gamma value of the CLEAN calculation. + threshold : float + Stopping limit (0 < x < 1) (stop if residual intensity below this point). + maxIter : int + Maximum number of iterations. + """ axis = self.checkAxis(axis) if not self.noUndo: copyData = copy.deepcopy(self) - # pos contains the values of fixed points which not to be translated to missing points - posList = np.delete(range(self.shape()[axis]), pos) + posList = np.delete(range(self.shape()[axis]), pos) # pos contains the values of fixed points which not to be translated to missing points if typeVal == 1: # type is States or States-TPPI, the positions need to be divided by 2 posList = np.array(np.floor(posList / 2), dtype=int) if typeVal == 2: # type is TPPI, for now handle the same as Complex @@ -1353,28 +2308,44 @@ def clean(self, pos, typeVal, axis, gamma, threshold, maxIter): mask[posList] = 0.0 mask = np.fft.fft(mask) # abs or real??? pool = multiprocessing.Pool(multiprocessing.cpu_count()) - fit = pool.map_async(clean, [(i, mask, gamma, threshold, maxIter) for i in tmpData]) + fit = pool.map_async(nus.clean, [(i, mask, gamma, threshold, maxIter) for i in tmpData]) pool.close() pool.join() tmpData = np.rollaxis(np.array(fit.get()).reshape(tmpShape), -1, axis) self.data = hc.HComplexData(tmpData) - #Transform back to FID - self.__invFourier(axis, tmp=True) - self.addHistory("CLEAN reconstruction (gamma = " + str(gamma) + " , threshold = " + str(threshold) + " , maxIter = " + str(maxIter) + ") " + - "of dimension " + str(axis + 1) + " at positions " + str(pos)) + self.__invFourier(axis, tmp=True) # Transform back to FID + self.addHistory("CLEAN reconstruction (gamma = " + str(gamma) + " , threshold = " + str(threshold) + " , maxIter = " + str(maxIter) + ") " + "of dimension " + str(axis + 1) + " at positions " + str(pos)) self.redoList = [] if not self.noUndo: self.undoList.append(lambda self: self.restoreData(copyData, None)) - - def ist(self,pos, typeVal, axis, threshold, maxIter, tracelimit): - import scipy.signal + + def ist(self, pos, typeVal, axis, threshold, maxIter, tracelimit): + """ + Uses the Iterative Soft Thresholding algorithm to reconstruct non-uniform sampled data. + + Parameters + ---------- + pos : array_like + A list of indices that are recorded datapoints. + All other datapoints will be reconstructed. + typeVal : {0, 1, 2} + The type of data to be reconstructed. + 0=complex, 1=States or States-TPPI, 2=TPPI. + axis : int + The axis along which the data is reconstructed. + threshold : float + threshold. The level (0 < x < 1) at which the data is cut every iteration. + maxIter : int + Maximum number of iterations. + tracelimit : float + Stopping limit (0 < x < 1) (stop if residual intensity below this point). + """ axis = self.checkAxis(axis) if not self.noUndo: copyData = copy.deepcopy(self) - # pos contains the values of fixed points which not to be translated to missing points self.data.icomplexReorder(axis) tmpData = self.data.getHyperData(0) - posList = np.delete(range(tmpData.shape[axis]), pos) + posList = np.delete(range(tmpData.shape[axis]), pos) # pos contains the values of fixed points which not to be translated to missing points if typeVal == 1: # type is States or States-TPPI, the positions need to be divided by 2 posList = np.array(np.floor(posList / 2), dtype=int) elif typeVal == 2: # type is TPPI, for now handle the same as Complex @@ -1384,20 +2355,37 @@ def ist(self,pos, typeVal, axis, threshold, maxIter, tracelimit): tmpShape = tmpData.shape tmpData = tmpData.reshape((int(tmpData.size / tmpShape[-1]), tmpShape[-1])) pool = multiprocessing.Pool(multiprocessing.cpu_count()) - fit = pool.map_async(ist, [(i, posList, threshold, maxIter, tracelimit, NDmax) for i in tmpData]) + fit = pool.map_async(nus.ist, [(i, posList, threshold, maxIter, tracelimit, NDmax) for i in tmpData]) pool.close() pool.join() tmpData = np.rollaxis(np.array(fit.get()).reshape(tmpShape), -1, axis) self.data = hc.HComplexData(tmpData) - #Transform back to FID - self.__invFourier(axis, tmp=True) - self.addHistory("IST reconstruction (threshold = " + str(threshold) + " , maxIter = " + str(maxIter) + " , tracelimit = " + str(tracelimit*100) + ") " + - "of dimension " + str(axis + 1) + " at positions " + str(pos)) + self.__invFourier(axis, tmp=True) # Transform back to FID + self.addHistory("IST reconstruction (threshold = " + str(threshold) + " , maxIter = " + str(maxIter) + " , tracelimit = " + str(tracelimit*100) + ") " + "of dimension " + str(axis + 1) + " at positions " + str(pos)) self.redoList = [] if not self.noUndo: self.undoList.append(lambda self: self.restoreData(copyData, None)) def getSlice(self, axes, locList, stack=None): + """ + Generate a new Spectrum object which is a subset of this object. + + Parameters + ---------- + axes : array_like + A list with the axes desired in the new object. + locList : array_like + The indices to specify the desired data subset. + stack : array_like, optional + Slice objects to extract part of an axis. + Often used for stack or array plots. + By default the entire data along all axes are used. + + Returns + ------- + Spectrum + The subset spectrum. + """ locList = np.array(locList, dtype=object) if stack is None: stack = [slice(None)]*len(axes) @@ -1420,8 +2408,19 @@ def getSlice(self, axes, locList, stack=None): name=self.name)) sliceSpec.noUndo = True return sliceSpec - - def restoreData(self, copyData, returnValue): # restore data from an old copy for undo purposes + + def restoreData(self, copyData, returnValue): + """ + Restore data from an old copy. + Often used for undo purposes. + + Parameters + ---------- + copyData : Spectrum + The old Spectrum object to restore from. + returnValue + A return value that should be appended to the undolist. + """ if (not self.noUndo) and returnValue is None: copyData2 = copy.deepcopy(self) self.data = copyData.data diff --git a/src/spectrumFrame.py b/src/spectrumFrame.py index b9f7915a..62fa0997 100644 --- a/src/spectrumFrame.py +++ b/src/spectrumFrame.py @@ -21,7 +21,7 @@ import numpy as np import matplotlib as mpl import matplotlib.gridspec as gridspec -from ssNake import QtGui, QtCore, QtWidgets +from ssNake import QtCore, QtWidgets TIMELABELLIST = [u'[s]', u'[ms]', u'[μs]'] FREQLABELLIST = [u'[Hz]', u'[kHz]', u'[MHz]'] @@ -30,14 +30,30 @@ # the class from which the 1d data is displayed, the operations which only edit the content of this class are for previewing -class PlotFrame(object): +class PlotFrame: + """ + The frame used to display the plot. + It handles the mouse actions applied to the figure. + """ + + INVERT_X = True # Invert the x-axis when spectrum + INVERT_Y = False # Invert the y-axis when spectrum + GRID_PLOT = False # Grid plot for contour + ZERO_SCROLL_ALLOWED = True # Scroll with respect to 0 on the y-axis - INVERT_X = True # Invert the x-axis when spectrum - INVERT_Y = False # Invert the y-axis when spectrum - GRID_PLOT = False # Grid plot for contour - ZERO_SCROLL_ALLOWED = True # Scroll with respect to 0 on the y-axis - def __init__(self, root, fig, canvas): + """ + Initializes the PlotFrame. + + Parameters + ---------- + root : Main1DWindow + The window that contains the figure. + fig : Figure + The figure used in this frame. + canvas : FigureCanvas + The canvas of fig. + """ self.root = root self.fig = fig self.canvas = canvas @@ -62,7 +78,7 @@ def __init__(self, root, fig, canvas): self.x_ax.axes.get_xaxis().set_visible(False) self.x_ax.axes.get_yaxis().set_visible(False) self.y_ax.axes.get_xaxis().set_visible(False) - self.y_ax.axes.get_yaxis().set_visible(False) + self.y_ax.axes.get_yaxis().set_visible(False) else: self.ax = self.fig.add_subplot(111) self.leftMouse = False # is the left mouse button currently pressed @@ -79,9 +95,11 @@ def __init__(self, root, fig, canvas): # variables to be initialized def kill(self): + """Dummy method""" pass - def plotReset(self): # this function needs to be overriden by the classes who inherit from PlotFrame + def plotReset(self): + """Dummy method""" pass ################ @@ -89,6 +107,10 @@ def plotReset(self): # this function needs to be overriden by the classes who i ################ def peakPickReset(self): + """ + Resets the figure after peak picking. + It removes any lines created by peak peaking still in the figure. + """ if self.rect[0] is not None: try: self.rect[0].remove() @@ -106,8 +128,17 @@ def peakPickReset(self): self.peakPickFunc = None def scroll(self, event): + """ + Handles scrolling in the figure. + The effect depends on the buttons that were held during scrolling. + + Parameters + ---------- + event : QEvent + The event. + """ zoomStep = self.root.father.defaultZoomStep - event.step = event.step * zoomStep #Apply zoom sensitivity + event.step = event.step * zoomStep # Apply zoom sensitivity modifiers = QtWidgets.QApplication.keyboardModifiers() if modifiers == QtCore.Qt.ShiftModifier: self.altScroll(event) @@ -161,18 +192,29 @@ def scroll(self, event): self.yminlim *= 0.9**event.step self.ax.set_ylim(self.yminlim, self.ymaxlim) if self.INVERT_Y: - if self.spec(-2) > 0 : + if self.spec(-2) > 0: self.ax.set_ylim(self.ymaxlim, self.yminlim) self.canvas.update() self.canvas.draw_idle() def altScroll(self, event): + """Dummy method""" pass def altReset(self): + """Dummy method""" pass def buttonPress(self, event): + """ + Handles button presses in the figure. + The effect depends on the buttons that were held during the button press and whether peak picking is enabled. + + Parameters + ---------- + event : QEvent + The event. + """ if event.button == 1 and not self.peakPick: self.leftMouse = True self.zoomX1 = event.xdata @@ -189,6 +231,15 @@ def buttonPress(self, event): self.panY = event.ydata def buttonRelease(self, event): + """ + Handles button release in the figure. + The effect depends on the buttons that were held during the button release and whether peak picking is enabled. + + Parameters + ---------- + event : QEvent + The event. + """ if event.button == 1: if self.peakPick: if self.rect[0] is not None: @@ -242,7 +293,15 @@ def buttonRelease(self, event): self.canvas.draw_idle() def pan(self, event): - modifiers = QtWidgets.QApplication.keyboardModifiers() + """ + Handles panning in the figure. + The effect depends on the buttons that were held during panning and whether peak picking is enabled. + + Parameters + ---------- + event : QEvent + The event. + """ if self.rightMouse and self.panX is not None and self.panY is not None: if self.logx or self.logy: x = event.xdata @@ -338,7 +397,32 @@ def pan(self, event): self.canvas.draw_idle() def getAxMult(self, spec, axType, ppm, freq, ref=None): - if spec == 1: + """ + Calculates the x-axis multiplier to convert the xax to a given axis type. + + Parameters + ---------- + spec : bool + True if the axis is for a spectrum, False it is for an FID. + axType : int + The multiplier defined as 1000^axType for spectra and 1000^-axType for FIDs. + When ppm is True this is not applied. + ppm : bool + If True the multiplier is calculated to convert to a ppm axis. + This is only used when spec is True. + freq : float + The spectrometer frequency (only used when spec is True). + ref : float, optional + The reference frequency if None the spectrometer frequency is used. + None by default. + Only used when spec is True. + + Returns + ------- + float + The x-axis multiplier. + """ + if spec: if ppm: if ref is not None: axMult = 1e6 / ref @@ -346,24 +430,51 @@ def getAxMult(self, spec, axType, ppm, freq, ref=None): axMult = 1e6 / freq else: axMult = 1.0 / (1000.0**axType) - elif spec == 0: + else: axMult = 1000.0**axType return axMult def getLabel(self, spec, axis, axType, ppm): + """ + Generates the axis label for a given axis type and dimension. + + Parameters + ---------- + spec : bool + True if the axis is for a spectrum, False it is for an FID. + axis : int + The dimension of the axis. + axType : int + 0 for Hz/s, 1 for kHz/ms, 2 for MHz/us. + ppm : bool + True if the frequency axis is in ppm. + + Returns + ------- + str + The axis label. + """ if not axType in range(3): return 'User defined D' + str(axis+1) if spec: tmpString = "Frequency D" + str(axis+1) + ' ' if ppm: return tmpString + '[ppm]' - else: - return tmpString + FREQLABELLIST[axType] + return tmpString + FREQLABELLIST[axType] else: - return "Time D" + str(axis+1) + ' ' + TIMELABELLIST[axType] def setLog(self, logx, logy): + """ + Sets the x and y axis to logarithmic or linear. + + Parameters + ---------- + logx : bool + True for a logarithmic x-axis. + logy : bool + True for a logarithmic y-axis. + """ self.logx = logx self.logy = logy self.ax.set_xlim(self.xminlim, self.xmaxlim) diff --git a/src/ssNake.py b/src/ssNake.py index 81c3f474..6971e70f 100644 --- a/src/ssNake.py +++ b/src/ssNake.py @@ -62,7 +62,7 @@ def splashProgressStep(splashStep): # A function to easily increase the progres root.processEvents() return splashStep -def import_lib(name,nameAs,className,splashStep): +def import_lib(name, nameAs, className, splashStep): # Function to load a library from string names if className is None: globals()[nameAs] = importlib.import_module(name) @@ -77,7 +77,7 @@ def import_lib(name,nameAs,className,splashStep): ['traceback', 'tb', None], ['numpy', 'np', None], ['copy', 'copy', None], - ['multiprocessing','multiprocessing',None], + ['multiprocessing', 'multiprocessing', None], ['datetime', 'datetime', None], ['webbrowser', 'webbrowser', None], ['spectrum', 'sc', None], @@ -91,31 +91,29 @@ def import_lib(name,nameAs,className,splashStep): ['specIO', 'io', None], ['views', 'views', None], ['simFunctions', 'sim', None], - ['loadIsotopes','loadIsotopes',None], - ['scipy','optimize','optimize']] + ['loadIsotopes', 'loadIsotopes', None], + ['scipy', 'optimize', 'optimize']] splashSteps = len(importList) / 100.0 splashStep = 0 # Import everything else for elem in importList: - splashStep = import_lib(elem[0],elem[1],elem[2],splashStep) - + splashStep = import_lib(elem[0], elem[1], elem[2], splashStep) isoPath = os.path.dirname(os.path.realpath(__file__)) + os.path.sep + "IsotopeProperties" ISOTOPES = loadIsotopes.getIsotopeInfo(isoPath) - matplotlib.rc('font', family='DejaVu Sans') np.set_printoptions(threshold=sys.maxsize) QtCore.QLocale.setDefault(QtCore.QLocale('en_US')) -VERSION = 'v1.1' +VERSION = 'v1.2' # Required library version NPVERSION = '1.11.0' -MPLVERSION = '1.4.2' +MPLVERSION = '1.5.0' SPVERSION = '0.14.1' PY2VERSION = '2.7' PY3VERSION = '3.4' -def splitString(val,size): +def splitString(val, size): #Split a string at spaces, with maxsize 'size' total = '' while len(val) > size: @@ -126,16 +124,15 @@ def splitString(val,size): total = total + val return total - #Prepare TOOLTIPS dictionary TOOLTIPS = dict() -with open(os.path.dirname(os.path.realpath(__file__)) + os.path.sep + 'Tooltips','r') as f: +with open(os.path.dirname(os.path.realpath(__file__)) + os.path.sep + 'Tooltips', 'r') as f: data = f.read().split('\n') for line in data: - if len(line) > 0: + if line: tmp = line.split('\t') if len(tmp) > 2: - text = tmp[1] + '\n\n' + splitString(tmp[2],80) #Header plus split main text + text = tmp[1] + '\n\n' + splitString(tmp[2], 80) #Header plus split main text else: text = tmp[1] TOOLTIPS[tmp[0]] = text @@ -171,9 +168,12 @@ def __init__(self, root): self.lastLocation = os.path.expanduser('~') else: self.lastLocation = os.getcwd() + if not self.defaultTooltips: #Disable tooltips by setting them to empty strings + for elem in TOOLTIPS.keys(): + TOOLTIPS[elem] = '' self.initMenu() self.menuCheck() - self.main_widget = QtWidgets.QSplitter(self) + self.main_widget = QtWidgets.QSplitter(self) self.main_widget.setHandleWidth(10) self.gridWidget = QtWidgets.QWidget(self) self.mainFrame = QtWidgets.QGridLayout(self.gridWidget) @@ -222,7 +222,7 @@ def handleCopy(self): """ Makes a pixelwise copy of the currently viewed canvas """ if self.mainWindow is None: return - if issubclass(type(self.mainWindow),fit.TabFittingWindow): #If fitting, take canvas from current tab + if issubclass(type(self.mainWindow), fit.TabFittingWindow): #If fitting, take canvas from current tab canvas = self.mainWindow.tabs.currentWidget().canvas else: canvas = self.mainWindow.canvas @@ -260,6 +260,7 @@ def resetDefaults(self): self.defaultNegColor = '#FF7F0E' self.defaultStartupBool = False self.defaultStartupDir = '~' + self.defaultTooltips = True self.defaultToolbarActionList = ['File --> Open', 'File -- > Save --> Matlab', 'File --> Export --> Figure', @@ -347,6 +348,7 @@ def loadDefaults(self): self.defaultToolBar = settings.value("toolbar", self.defaultToolBar, bool) self.defaultStartupBool = settings.value("startupdiron", self.defaultStartupBool, bool) self.defaultStartupDir = settings.value("startupdir", self.defaultStartupDir, str) + self.defaultTooltips = settings.value("tooltips", self.defaultTooltips, bool) try: self.defaultWidthRatio = settings.value("contour/width_ratio", self.defaultWidthRatio, float) except TypeError: @@ -381,6 +383,7 @@ def saveDefaults(self): settings.setValue("toolbar", self.defaultToolBar) settings.setValue("startupdiron", self.defaultStartupBool) settings.setValue("startupdir", self.defaultStartupDir) + settings.setValue("tooltips", self.defaultTooltips) settings.setValue("contour/colourmap", self.defaultColorMap) settings.setValue("contour/constantcolours", self.defaultContourConst) settings.setValue("contour/poscolour", self.defaultPosColor) @@ -505,6 +508,7 @@ def initToolbar(self): ['Plot --> Stack Plot', self.stackplotAct], ['Plot --> Array Plot', self.arrayplotAct], ['Plot --> Contour Plot', self.contourplotAct], + ['Plot --> 2D Colour Plot', self.colour2DplotAct], ['Plot --> Multi Plot', self.multiplotAct], ['Plot --> Set Reference', self.setrefAct], ['Plot --> Clear Current Reference', self.delrefAct], @@ -567,7 +571,7 @@ def initMenu(self): self.menubar.addMenu(self.workspacemenu) self.newAct = self.workspacemenu.addAction(QtGui.QIcon(IconDirectory + 'duplicate.png'), 'D&uplicate', self.duplicateWorkspace, QtGui.QKeySequence.New) self.newAct.setToolTip('Duplicate Workspace') - self.newSlice = self.workspacemenu.addAction(QtGui.QIcon(IconDirectory + 'duplicate.png'), 'Slice to Workspace', lambda: self.duplicateWorkspace(sliceOnly = True)) + self.newSlice = self.workspacemenu.addAction(QtGui.QIcon(IconDirectory + 'duplicate.png'), 'Slice to Workspace', lambda: self.duplicateWorkspace(sliceOnly=True)) self.newSlice.setToolTip('Copy Current Slice to New Workspace') self.closeAct = self.workspacemenu.addAction(QtGui.QIcon(IconDirectory + 'delete.png'), '&Delete', self.destroyWorkspace, QtGui.QKeySequence.Close) self.closeAct.setToolTip('Delete Workspace') @@ -582,7 +586,7 @@ def initMenu(self): self.workInfoAct = self.workspacemenu.addAction(QtGui.QIcon(IconDirectory + 'about.png'), '&Info', lambda: self.mainWindowCheck(lambda mainWindow: WorkInfoWindow(mainWindow))) self.workInfoAct.setToolTip('Workspace Information') self.workspaceActList = [self.newAct, self.newSlice, self.closeAct, self.renameWorkspaceAct, - self.forwardAct, self.backAct,self.workInfoAct] + self.forwardAct, self.backAct, self.workInfoAct] # Macro menu self.macromenu = QtWidgets.QMenu('&Macros', self) self.menubar.addMenu(self.macromenu) @@ -660,7 +664,7 @@ def initMenu(self): self.digitalFilterAct.setToolTip("Correct Digital Filter") self.lpsvdAct = self.toolMenu.addAction(QtGui.QIcon(IconDirectory + 'LPSVD.png'), "&LPSVD", lambda: self.mainWindowCheck(lambda mainWindow: LPSVDWindow(mainWindow))) self.lpsvdAct.setToolTip('LPSVD linear prediction') - self.scaleSWAct = self.toolMenu.addAction(QtGui.QIcon(IconDirectory + 'ScaleSW.png'),"Scale SW", lambda: self.mainWindowCheck(lambda mainWindow: ScaleSWWindow(mainWindow))) + self.scaleSWAct = self.toolMenu.addAction(QtGui.QIcon(IconDirectory + 'ScaleSW.png'), "Scale SW", lambda: self.mainWindowCheck(lambda mainWindow: ScaleSWWindow(mainWindow))) self.scaleSWAct.setToolTip('Scale the Current Spectral Width') self.referencelistmenu = QtWidgets.QMenu('&Reference', self) self.toolMenu.addMenu(self.referencelistmenu) @@ -678,10 +682,10 @@ def initMenu(self): self.referencelistmenu.addMenu(self.referencesavemenu) self.loadrefAct = self.referencelistmenu.addAction(QtGui.QIcon(IconDirectory + 'open.png'), "&Load", self.referenceLoad) self.loadrefAct.setToolTip('Load Reference') - self.toolsActList = [self.realAct, self.imagAct, self.absAct,self.conjAct, + self.toolsActList = [self.realAct, self.imagAct, self.absAct, self.conjAct, self.apodizeAct, self.phaseAct, self.autoPhaseAct0, - self.autoPhaseAct1, self.autoPhaseAllAct0, self.phasingmenu, - self.autoPhaseAllAct1,self.swapEchoAct, self.corOffsetAct, + self.autoPhaseAct1, self.autoPhaseAllAct0, self.phasingmenu, + self.autoPhaseAllAct1, self.swapEchoAct, self.corOffsetAct, self.baselineAct, self.subAvgAct, self.refDeconvAct, self.lpsvdAct, self.digitalFilterAct, self.scaleSWAct] # the matrix drop down menu @@ -691,9 +695,9 @@ def initMenu(self): self.sizingAct.setToolTip('Set Size') self.shiftAct = self.matrixMenu.addAction(QtGui.QIcon(IconDirectory + 'shift.png'), "S&hift Data", lambda: self.mainWindowCheck(lambda mainWindow: ShiftDataWindow(mainWindow))) self.shiftAct.setToolTip('Shift Data') - self.rollAct = self.matrixMenu.addAction(QtGui.QIcon(IconDirectory + 'roll.png'),"Roll Data", lambda: self.mainWindowCheck(lambda mainWindow: RollDataWindow(mainWindow))) + self.rollAct = self.matrixMenu.addAction(QtGui.QIcon(IconDirectory + 'roll.png'), "Roll Data", lambda: self.mainWindowCheck(lambda mainWindow: RollDataWindow(mainWindow))) self.rollAct.setToolTip('Roll Data') - self.alignAct = self.matrixMenu.addAction(QtGui.QIcon(IconDirectory + 'alignMax.png'),"Align Maxima", lambda: self.mainWindowCheck(lambda mainWindow: AlignDataWindow(mainWindow))) + self.alignAct = self.matrixMenu.addAction(QtGui.QIcon(IconDirectory + 'alignMax.png'), "Align Maxima", lambda: self.mainWindowCheck(lambda mainWindow: AlignDataWindow(mainWindow))) self.alignAct.setToolTip('Align Maxima') self.regionMenu = QtWidgets.QMenu("Region", self) self.matrixMenu.addMenu(self.regionMenu) @@ -774,8 +778,8 @@ def initMenu(self): self.echoantiAct = self.hypercomplexMenu.addAction(QtGui.QIcon(IconDirectory + 'echoantiecho.png'), "Ec&ho-antiecho", lambda: self.mainWindowCheck(lambda mainWindow: mainWindow.echoAntiEcho())) self.echoantiAct.setToolTip('Ec&ho-antiecho Hypercomplex Data Processing') self.transformActList = [self.fourierAct, self.realFourierAct, self.fftshiftAct, - self.invfftshiftAct, self.hilbertAct, self.ffmAct, - self.cleanAct, self.istAct,self.statesAct,self.statesTPPIAct,self.echoantiAct] + self.invfftshiftAct, self.hilbertAct, self.ffmAct, + self.cleanAct, self.istAct, self.statesAct, self.statesTPPIAct, self.echoantiAct] # the fitting drop down menu self.fittingMenu = QtWidgets.QMenu("F&itting", self) self.menubar.addMenu(self.fittingMenu) @@ -785,7 +789,7 @@ def initMenu(self): self.fwhmAct.setToolTip('Full Width at Half Maximum') self.massAct = self.fittingMenu.addAction(QtGui.QIcon(IconDirectory + 'mass.png'), "Centre of Mass", lambda: self.mainWindowCheck(lambda mainWindow: COMWindow(mainWindow))) self.massAct.setToolTip('Centre of Mass') - self.intfitAct = self.fittingMenu.addAction(QtGui.QIcon(IconDirectory + 'int.png'),"&Integrals", lambda: self.mainWindowCheck(lambda mainWindow: IntegralsWindow(mainWindow))) + self.intfitAct = self.fittingMenu.addAction(QtGui.QIcon(IconDirectory + 'int.png'), "&Integrals", lambda: self.mainWindowCheck(lambda mainWindow: IntegralsWindow(mainWindow))) self.intfitAct.setToolTip('Get Integrals') self.relaxAct = self.fittingMenu.addAction(QtGui.QIcon(IconDirectory + 'relaxation.png'), "&Relaxation Curve", lambda: self.mainWindowCheck(lambda mainWindow: mainWindow.createRelaxWindow())) self.relaxAct.setToolTip('Fit Relaxation Curve') @@ -797,17 +801,17 @@ def initMenu(self): self.csastaticAct.setToolTip('Fit CSA') self.quadAct = self.fittingMenu.addAction(QtGui.QIcon(IconDirectory + 'quadconversion.png'), "&Quadrupole", lambda: self.mainWindowCheck(lambda mainWindow: mainWindow.createQuadDeconvWindow())) self.quadAct.setToolTip('Fit Quadrupole') - self.quadCSAAct = self.fittingMenu.addAction(QtGui.QIcon(IconDirectory + 'quadcsa.png'),"Q&uadrupole+CSA", lambda: self.mainWindowCheck(lambda mainWindow: mainWindow.createQuadCSADeconvWindow())) + self.quadCSAAct = self.fittingMenu.addAction(QtGui.QIcon(IconDirectory + 'quadcsa.png'), "Q&uadrupole+CSA", lambda: self.mainWindowCheck(lambda mainWindow: mainWindow.createQuadCSADeconvWindow())) self.quadCSAAct.setToolTip('Fit Quadrupole+CSA') self.czjzekAct = self.fittingMenu.addAction(QtGui.QIcon(IconDirectory + 'czjzekstatic.png'), "C&zjzek", lambda: self.mainWindowCheck(lambda mainWindow: mainWindow.createQuadCzjzekWindow())) self.czjzekAct.setToolTip('Fit Czjzek Pattern') - self.mqmasAct = self.fittingMenu.addAction(QtGui.QIcon(IconDirectory + 'mqmas.png'),"&MQMAS", lambda: self.mainWindowCheck(lambda mainWindow: mainWindow.createMQMASWindow())) + self.mqmasAct = self.fittingMenu.addAction(QtGui.QIcon(IconDirectory + 'mqmas.png'), "&MQMAS", lambda: self.mainWindowCheck(lambda mainWindow: mainWindow.createMQMASWindow())) self.mqmasAct.setToolTip('Fit MQMAS') - self.mqmasCzjzekAct = self.fittingMenu.addAction(QtGui.QIcon(IconDirectory + 'mqmas.png'),"Cz&jzek MQMAS", lambda: self.mainWindowCheck(lambda mainWindow: mainWindow.createMQMASCzjzekWindow())) + self.mqmasCzjzekAct = self.fittingMenu.addAction(QtGui.QIcon(IconDirectory + 'mqmas.png'), "Cz&jzek MQMAS", lambda: self.mainWindowCheck(lambda mainWindow: mainWindow.createMQMASCzjzekWindow())) self.mqmasCzjzekAct.setToolTip('Fit Czjzek MQMAS') - self.externalFitAct = self.fittingMenu.addAction(QtGui.QIcon(IconDirectory + 'simpson.png'),"&External", lambda: self.mainWindowCheck(lambda mainWindow: mainWindow.createExternalFitWindow())) + self.externalFitAct = self.fittingMenu.addAction(QtGui.QIcon(IconDirectory + 'simpson.png'), "&External", lambda: self.mainWindowCheck(lambda mainWindow: mainWindow.createExternalFitWindow())) self.externalFitAct.setToolTip('Fit External') - self.functionFitAct = self.fittingMenu.addAction(QtGui.QIcon(IconDirectory + 'function.png'),"F&unction fit", lambda: self.mainWindowCheck(lambda mainWindow: mainWindow.createFunctionFitWindow())) + self.functionFitAct = self.fittingMenu.addAction(QtGui.QIcon(IconDirectory + 'function.png'), "F&unction fit", lambda: self.mainWindowCheck(lambda mainWindow: mainWindow.createFunctionFitWindow())) self.functionFitAct.setToolTip('Fit Function') self.fittingActList = [self.snrAct, self.fwhmAct, self.massAct, self.intfitAct, self.relaxAct, self.diffusionAct, @@ -846,6 +850,10 @@ def initMenu(self): self.contourplotAct = self.plotMenu.addAction(QtGui.QIcon(IconDirectory + 'contour.png'), "&Contour Plot", lambda: self.mainWindowCheck(lambda mainWindow: mainWindow.plotContour())) self.contourplotAct.setToolTip('Contour Plot') self.multiDActions.append(self.contourplotAct) + + self.colour2DplotAct = self.plotMenu.addAction(QtGui.QIcon(IconDirectory + '2DColour.png'), "2D Colour Plot", lambda: self.mainWindowCheck(lambda mainWindow: mainWindow.plotColour2D())) + self.colour2DplotAct.setToolTip('2D Colour Plot') + self.multiDActions.append(self.colour2DplotAct) self.multiplotAct = self.plotMenu.addAction(QtGui.QIcon(IconDirectory + 'multi.png'), "&Multi Plot", lambda: self.mainWindowCheck(lambda mainWindow: mainWindow.plotMulti())) self.multiplotAct.setToolTip('Multi Plot') #========== @@ -854,7 +862,7 @@ def initMenu(self): self.plotprefAct = self.plotMenu.addAction(QtGui.QIcon(IconDirectory + 'preferences.png'), "&Plot Settings", lambda: self.mainWindowCheck(lambda mainWindow: PlotSettingsWindow(mainWindow))) self.plotprefAct.setToolTip('Plot Settings') self.plotActList = [self.onedplotAct, self.scatterplotAct, self.stackplotAct, - self.arrayplotAct, self.contourplotAct, self.multiplotAct, + self.arrayplotAct, self.contourplotAct,self.colour2DplotAct, self.multiplotAct, self.setrefAct, self.delrefAct, self.userxAct, self.plotprefAct] # the history drop down menu self.historyMenu = QtWidgets.QMenu("&History", self) @@ -869,64 +877,46 @@ def initMenu(self): self.menubar.addMenu(self.utilitiesMenu) self.shiftconvAct = self.utilitiesMenu.addAction(QtGui.QIcon(IconDirectory + 'shifttool.png'), "&Chemical Shift Conversion Tool", self.createShiftConversionWindow) self.shiftconvAct.setToolTip('Chemical Shift Conversion Tool') - self.dipolarconvAct = self.utilitiesMenu.addAction(QtGui.QIcon(IconDirectory + 'dipolar.png'),"Dipolar Distance Tool", self.createDipolarDistanceWindow) + self.dipolarconvAct = self.utilitiesMenu.addAction(QtGui.QIcon(IconDirectory + 'dipolar.png'), "Dipolar Distance Tool", self.createDipolarDistanceWindow) self.dipolarconvAct.setToolTip('Dipolar Distance Tool') self.quadconvAct = self.utilitiesMenu.addAction(QtGui.QIcon(IconDirectory + 'quadconversion.png'), "&Quadrupole Coupling Conversion Tool", self.createQuadConversionWindow) self.quadconvAct.setToolTip('Quadrupole Coupling Conversion Tool') - self.mqmasconvAct = self.utilitiesMenu.addAction(QtGui.QIcon(IconDirectory + 'mqmas.png'),"MQMAS Parameter Extraction Tool", self.createMqmasExtractWindow) + self.mqmasconvAct = self.utilitiesMenu.addAction(QtGui.QIcon(IconDirectory + 'mqmas.png'), "MQMAS Parameter Extraction Tool", self.createMqmasExtractWindow) self.mqmasconvAct.setToolTip('MQMAS Parameter Extraction Tool') - self.tempcalAct = self.utilitiesMenu.addAction(QtGui.QIcon(IconDirectory + 'temperature.png'),"Temperature Calibration Tool", self.createTempcalWindow) + self.tempcalAct = self.utilitiesMenu.addAction(QtGui.QIcon(IconDirectory + 'temperature.png'), "Temperature Calibration Tool", self.createTempcalWindow) self.tempcalAct.setToolTip('Dipolar Distance Tool') self.nmrtableAct = self.utilitiesMenu.addAction(QtGui.QIcon(IconDirectory + 'table.png'), "&NMR Table", self.nmrTable) self.nmrtableAct.setToolTip('NMR Periodic Table') - self.utilitiesActList = [self.shiftconvAct, self.quadconvAct, self.nmrtableAct, self.dipolarconvAct,self.mqmasconvAct,self.tempcalAct ] + self.utilitiesActList = [self.shiftconvAct, self.quadconvAct, self.nmrtableAct, self.dipolarconvAct, self.mqmasconvAct, self.tempcalAct] # the help drop down menu self.helpMenu = QtWidgets.QMenu("&Help", self) self.menubar.addMenu(self.helpMenu) if not EXE: self.updateAct = self.helpMenu.addAction(QtGui.QIcon(IconDirectory + 'update.png'), "&Update", self.updateMenu) self.updateAct.setToolTip('Update ssNake') - self.helpActList = [self.updateAct] + self.helpActList = [self.updateAct] else: - self.helpActList = [] - self.refmanAct = self.helpMenu.addAction(QtGui.QIcon(IconDirectory + 'manual.png'),"Reference Manual", lambda: self.openRefMan()) + self.helpActList = [] + self.refmanAct = self.helpMenu.addAction(QtGui.QIcon(IconDirectory + 'manual.png'), "Reference Manual", openRefMan) self.refmanAct.setToolTip('Open the Reference Manual') - self.basTutorialAct = self.helpMenu.addAction(QtGui.QIcon(IconDirectory + 'Tutorial.png'),"Basic Tutorial", lambda: self.openTutorial()) + self.basTutorialAct = self.helpMenu.addAction(QtGui.QIcon(IconDirectory + 'Tutorial.png'), "Basic Tutorial", openTutorial) self.basTutorialAct.setToolTip('Open the Tutorial Folder') - self.tutorialAct = self.helpMenu.addAction(QtGui.QIcon(IconDirectory + 'Tutorial.png'),"Advanced Tutorials", lambda: webbrowser.open('https://github.com/smeerten/ssnake_tutorials/')) + self.tutorialAct = self.helpMenu.addAction(QtGui.QIcon(IconDirectory + 'Tutorial.png'), "Advanced Tutorials", lambda: webbrowser.open('https://github.com/smeerten/ssnake_tutorials/')) self.tutorialAct.setToolTip('Link to ssNake Advanced Processing Tutorials') - self.githubAct = self.helpMenu.addAction(QtGui.QIcon(IconDirectory + 'GitHub.png'),"GitHub Page", lambda: webbrowser.open('https://github.com/smeerten/ssnake/')) + self.githubAct = self.helpMenu.addAction(QtGui.QIcon(IconDirectory + 'GitHub.png'), "GitHub Page", lambda: webbrowser.open('https://github.com/smeerten/ssnake/')) self.githubAct.setToolTip('ssNake GitHub Page') self.aboutAct = self.helpMenu.addAction(QtGui.QIcon(IconDirectory + 'about.png'), "&About", lambda: aboutWindow(self)) self.aboutAct.setToolTip('About Menu') - self.helpActList = self.helpActList + [self.shiftconvAct, self.quadconvAct, - self.nmrtableAct,self.githubAct,self.tutorialAct, self.aboutAct,self.basTutorialAct ] + self.helpActList = self.helpActList + [self.shiftconvAct, self.quadconvAct, self.nmrtableAct, self.githubAct, + self.tutorialAct, self.aboutAct, self.basTutorialAct] # Extra event lists: self.specOnlyList = [self.regridAct, self.csastaticAct, self.quadAct, self.quadCSAAct, self.czjzekAct] - self.fidOnlyList = [self.relaxAct, self.diffusionAct,self.swapEchoAct] - self.Only1DPlot = [self.snrAct, self.fwhmAct, self.massAct, self.intfitAct] - self.notInArrayPlot = [self.userxAct,self.setrefAct,self.swapEchoAct,self.corOffsetAct,self.baselineAct,self.subAvgAct, - self.refDeconvAct,self.intRegionAct,self.sumRegionAct,self.maxRegionAct,self.maxRegionAct, - self.minRegionAct, self.maxposRegionAct,self.minposRegionAct,self.averageRegionAct, - self.extractpartAct,self.matrixdelAct,self.normalizeAct,self.regridAct] - - def openRefMan(self): - file = os.path.dirname(os.path.realpath(__file__)) + os.path.sep + '..' + os.path.sep + 'ReferenceManual.pdf' - if sys.platform.startswith( 'linux' ): - os.system("xdg-open " + '"' + file + '"') - elif sys.platform.startswith( 'darwin' ): - os.system("open " + '"' + file + '"') - elif sys.platform.startswith( 'win' ): - os.startfile(file) - - def openTutorial(self): - path = os.path.dirname(os.path.realpath(__file__)) + os.path.sep + '..' + os.path.sep + '/Tutorial' - if sys.platform.startswith( 'linux' ): - os.system("xdg-open " + '"' + path + '"') - elif sys.platform.startswith( 'darwin' ): - os.system("open " + '"' + path + '"') - elif sys.platform.startswith( 'win' ): - os.startfile(path) + self.fidOnlyList = [self.relaxAct, self.diffusionAct, self.swapEchoAct] + self.Only1DPlot = [self.snrAct, self.fwhmAct, self.massAct, self.intfitAct] + self.notInArrayPlot = [self.userxAct, self.setrefAct, self.swapEchoAct, self.corOffsetAct, self.baselineAct, self.subAvgAct, + self.refDeconvAct, self.intRegionAct, self.sumRegionAct, self.maxRegionAct, self.maxRegionAct, + self.minRegionAct, self.maxposRegionAct, self.minposRegionAct, self.averageRegionAct, + self.extractpartAct, self.matrixdelAct, self.normalizeAct, self.regridAct] def mainWindowCheck(self, transfer): # checks if mainWindow exist to execute the function @@ -987,7 +977,7 @@ def menuCheck(self): self.noUndoAct.setChecked(True) else: self.noUndoAct.setChecked(False) - if (len(self.mainWindow.masterData.shape()) < 2): + if len(self.mainWindow.masterData.shape()) < 2: for i in self.multiDActions: i.setEnabled(False) else: @@ -1079,12 +1069,12 @@ def askName(self, filePath=None, name=None): name = 'spectrum' + str(count) givenName, ok = QtWidgets.QInputDialog.getText(self, message, 'Name:', text=name) if not ok: - return + return None while (givenName in self.workspaceNames) or givenName == '': self.dispMsg("Workspace name '" + givenName + "' already exists") givenName, ok = QtWidgets.QInputDialog.getText(self, message, 'Name:', text=name) if not ok: - return + return None return givenName def undo(self, *args): @@ -1108,7 +1098,7 @@ def macroCreate(self): givenName, ok = QtWidgets.QInputDialog.getText(self, 'Macro name', 'Name:', text=name) if not ok: return - while (givenName in self.macros.keys()) or givenName is '': + while (givenName in self.macros.keys()) or (givenName == ''): self.dispMsg("Macro name '" + givenName + "' already exists") givenName, ok = QtWidgets.QInputDialog.getText(self, 'Macro name', 'Name:', text=name) if not ok: @@ -1202,7 +1192,7 @@ def loadMacro(self): filename = filename[0] if filename: # if not cancelled self.lastLocation = os.path.dirname(filename) # Save used path - if len(filename) == 0: + if not filename: return self.stopMacro() count = 0 @@ -1211,7 +1201,7 @@ def loadMacro(self): count += 1 name = 'macro' + str(count) givenName, ok = QtWidgets.QInputDialog.getText(self, 'Macro name', 'Name:', text=name) - while (givenName in self.macros.keys()) or givenName is '': + while (givenName in self.macros.keys()) or (givenName == ''): if not ok: return self.dispMsg("Macro name '" + givenName + "' already exists") @@ -1266,7 +1256,7 @@ def referenceRename(self, oldName): givenName, ok = QtWidgets.QInputDialog.getText(self, 'Reference name', 'Name:', text=oldName) if givenName == oldName or not ok: return - while (givenName in self.referenceName) or givenName is '': + while (givenName in self.referenceName) or (givenName == ''): self.dispMsg('Name exists') givenName, ok = QtWidgets.QInputDialog.getText(self, 'Reference name', 'Name:', text=oldName) if not ok: @@ -1291,8 +1281,7 @@ def referenceSave(self, name): fileName = fileName[0] if not fileName: return - else: - self.lastLocation = os.path.dirname(fileName) + self.lastLocation = os.path.dirname(fileName) reffreq = self.referenceValue[self.referenceName.index(name)] with open(fileName, 'w') as f: f.write(str(reffreq)) @@ -1303,7 +1292,7 @@ def referenceLoad(self): filename = filename[0] if filename: # if not cancelled self.lastLocation = os.path.dirname(filename) # Save used path - if len(filename) == 0: + if not filename: return name = os.path.basename(filename) if name.endswith('.txt'): #If regular extension, name becomes filename - extension @@ -1313,7 +1302,7 @@ def referenceLoad(self): name = 'ref' + str(count) count += 1 givenName, ok = QtWidgets.QInputDialog.getText(self, 'Reference name', 'Name:', text=name) - while (givenName in self.macros.keys()) or givenName is '': + while (givenName in self.macros.keys()) or (givenName == ''): if not ok: return self.dispMsg('Name exists') @@ -1349,7 +1338,7 @@ def changeMainWindow(self, var): self.workspaceNum = num self.mainWindow = self.workspaces[num] self.tabs.setCurrentIndex(num) - self.updWorkspaceMenu(var) + self.updWorkspaceMenu() self.menuCheck() try: if type(self.mainWindow.current) is views.CurrentMulti: @@ -1373,7 +1362,7 @@ def stepWorkspace(self, step): self.workspaceNum = self.workspaceNum % len(self.workspaces) self.mainWindow = self.workspaces[self.workspaceNum] self.tabs.setCurrentIndex(self.workspaceNum) - self.updWorkspaceMenu(self.workspaceNames[self.workspaceNum]) + self.updWorkspaceMenu() self.menuCheck() if type(self.mainWindow) is not SaveFigureWindow: if type(self.mainWindow.current) is views.CurrentMulti: @@ -1388,7 +1377,7 @@ def duplicateWorkspace(self, sliceOnly=False, *args): data = copy.deepcopy(self.mainWindow.get_masterData()) if name is None: return - self.workspaces.append(Main1DWindow(self, data , self.mainWindow.get_current())) + self.workspaces.append(Main1DWindow(self, data, self.mainWindow.get_current())) self.workspaces[-1].rename(name) self.tabs.addTab(self.workspaces[-1], name) self.workspaceNames.append(name) @@ -1403,7 +1392,7 @@ def renameWorkspace(self, *args): return self.workspaceNames[self.workspaceNum] = name self.tabs.setTabText(self.workspaceNum, name) - self.updWorkspaceMenu(name) + self.updWorkspaceMenu() self.workspaces[self.workspaceNum].rename(name) def destroyWorkspace(self, num=None): @@ -1426,14 +1415,14 @@ def destroyWorkspace(self, num=None): self.workspaceNum = num - 1 if num < self.workspaceNum: self.workspaceNum -= 1 - if len(self.workspaces) > 0: + if self.workspaces: self.changeMainWindow(self.workspaceNames[self.workspaceNum]) else: self.logo.show() self.tabs.hide() - self.updWorkspaceMenu(None) + self.updWorkspaceMenu() - def updWorkspaceMenu(self, var): + def updWorkspaceMenu(self): self.activemenu.clear() for i in self.workspaceNames: self.activemenu.addAction(i, lambda i=i: self.changeMainWindow(i)) @@ -1488,12 +1477,12 @@ def loadData(self, fileList): for filePath in fileList: if filePath: # if not cancelled self.lastLocation = os.path.dirname(filePath) # Save used path - if len(filePath) == 0: + if not filePath: return masterData = io.autoLoad(filePath) if masterData is None: return - elif masterData == -1: + if masterData == -1: dialog = AsciiLoadWindow(self, filePath) if dialog.exec_(): if dialog.closed: @@ -1648,7 +1637,7 @@ def createTempcalWindow(self): def nmrTable(self): import nmrTable - nmrTable.PeriodicTable() + nmrTable.PeriodicTable() def fileQuit(self): self.close() @@ -1758,7 +1747,7 @@ def menuCheck(self): self.father.menuCheck() def runMacro(self, macro, display=True): - for i in range(len(macro)): + for i, _ in enumerate(macro): iter1 = macro[i] # Do not loop over the macro list itself to prevent recursion if the running macro is also the one being recorded self.addMacro(iter1) try: @@ -1956,9 +1945,8 @@ def autoPhaseAll(self, phaseNum): def CorrectDigitalFilter(self): if self.current.data.dFilter is None: raise SsnakeException('Digital filter: no value defined') - else: - self.current.correctDFilter() - self.menuCheck() + self.current.correctDFilter() + self.menuCheck() def createRelaxWindow(self): self.father.createFitWindow(fit.TabFittingWindow(self.father, self.father.mainWindow, 'relax')) @@ -2053,6 +2041,16 @@ def plotContour(self): self.updAllFrames() self.menuCheck() + def plotColour2D(self): + if len(self.masterData.shape()) < 2: + raise SsnakeException("Data does not have enough dimensions") + tmpcurrent = views.CurrentColour2D(self, self.fig, self.canvas, self.masterData, self.current) + self.current.kill() + del self.current + self.current = tmpcurrent + self.updAllFrames() + self.menuCheck() + def plotMulti(self): tmpcurrent = views.CurrentMulti(self, self.fig, self.canvas, self.masterData, self.current) self.current.kill() @@ -2096,7 +2094,7 @@ def clearUndo(self): class SideFrame(QtWidgets.QScrollArea): FITTING = False - + def __init__(self, parent): super(SideFrame, self).__init__(parent) self.father = parent @@ -2181,7 +2179,7 @@ def upd(self): self.entries[num].setValue(current.locList[num]) if self.FITTING and num in current.axes: self.entries[num].setDisabled(True) - self.entries[num].valueChanged.connect(lambda event=None, num=num: self.getSlice(event, num)) + self.entries[num].valueChanged.connect(lambda event, num=num: self.getSlice(num)) if type(current) is views.CurrentStacked or type(current) is views.CurrentArrayed: if current.viewSettings["stackBegin"] is not None: from2D = current.viewSettings["stackBegin"] @@ -2220,68 +2218,69 @@ def upd(self): self.spacingEntry.returnPressed.connect(self.setSpacing) self.frame2.addWidget(self.spacingEntry, 8, 0) if isinstance(current, (views.CurrentContour)): - self.contourTypeGroup = QtWidgets.QGroupBox('Contour type:') - self.contourTypeFrame = QtWidgets.QGridLayout() - self.contourNumberLabel = wc.QLeftLabel("Number:", self) - self.contourTypeFrame.addWidget(self.contourNumberLabel, 0, 0) - self.numLEntry = wc.SsnakeSpinBox() - self.numLEntry.setMaximum(100000) - self.numLEntry.setMinimum(1) - self.numLEntry.setToolTip(TOOLTIPS['contourNumber']) - self.numLEntry.setValue(current.viewSettings["numLevels"]) - self.numLEntry.valueChanged.connect(self.setContour) - self.contourTypeFrame.addWidget(self.numLEntry, 0, 1) - self.contourTypeFrame.addWidget(wc.QLeftLabel("Sign:", self), 1, 0) - self.contourSignEntry = QtWidgets.QComboBox() - self.contourSignEntry.setToolTip(TOOLTIPS['contourSign']) - self.contourSignEntry.addItems(['Both', '+ only', '- only']) - self.contourSignEntry.setCurrentIndex(current.viewSettings["contourSign"]) - self.contourSignEntry.currentIndexChanged.connect(self.setContour) - self.contourTypeFrame.addWidget(self.contourSignEntry, 1, 1) - self.contourTypeLabel = wc.QLeftLabel("Type:", self) - self.contourTypeFrame.addWidget(self.contourTypeLabel, 2, 0) - self.contourTypeEntry = QtWidgets.QComboBox() - self.contourTypeEntry.setToolTip(TOOLTIPS['contourType']) - self.contourTypeEntry.addItems(['Linear', 'Multiplier']) - self.contourTypeEntry.setCurrentIndex(current.viewSettings["contourType"]) - self.contourTypeEntry.currentIndexChanged.connect(self.setContour) - self.contourTypeFrame.addWidget(self.contourTypeEntry, 2, 1) - self.multiValueLabel = wc.QLeftLabel("Multiplier:", self) - self.contourTypeFrame.addWidget(self.multiValueLabel, 3, 0) - self.multiValue = wc.QLineEdit(current.viewSettings["multiValue"], self.setContour) - self.multiValue.setToolTip(TOOLTIPS['contourMultiplier']) - self.multiValue.setMaximumWidth(120) - self.contourTypeFrame.addWidget(self.multiValue, 3, 1) - if current.viewSettings["contourType"] != 1: - self.multiValueLabel.hide() - self.multiValue.hide() - self.contourTypeGroup.setLayout(self.contourTypeFrame) - self.frame2.addWidget(self.contourTypeGroup, 6, 0, 1, 3) - # Contour limits - self.contourLimitsGroup = QtWidgets.QGroupBox('Contour limits [%]:') - self.contourLimitsFrame = QtWidgets.QGridLayout() - self.maxLEntry = wc.QLineEdit(format(current.viewSettings["maxLevels"] * 100.0, '.7g'), self.setContour) - self.maxLEntry.setMaximumWidth(120) - self.maxLEntry.setToolTip(TOOLTIPS['contourMax']) - self.contourLimitsFrame.addWidget(self.maxLEntry, 1, 1) - self.minLEntry = wc.QLineEdit(format(current.viewSettings["minLevels"] * 100.0, '.7g'), self.setContour) - self.minLEntry.setMaximumWidth(120) - self.minLEntry.setToolTip(TOOLTIPS['contourMin']) - self.contourLimitsFrame.addWidget(self.minLEntry, 2, 1) - self.contourLimType = QtWidgets.QComboBox() - self.contourLimType.addItems(['Current 2D', 'Full data']) - self.contourLimType.setCurrentIndex(current.viewSettings["limitType"]) - self.contourLimType.setToolTip(TOOLTIPS['contourLimType']) - self.contourLimType.currentIndexChanged.connect(self.setContour) - self.contourLimitsFrame.addWidget(self.contourLimType, 0, 1) - self.maxLabel = wc.QLeftLabel("Max:", self) - self.minLabel = wc.QLeftLabel("Min:", self) - self.relLabel = wc.QLeftLabel("Rel. to:", self) - self.contourLimitsFrame.addWidget(self.relLabel, 0, 0) - self.contourLimitsFrame.addWidget(self.maxLabel, 1, 0) - self.contourLimitsFrame.addWidget(self.minLabel, 2, 0) - self.contourLimitsGroup.setLayout(self.contourLimitsFrame) - self.frame2.addWidget(self.contourLimitsGroup, 7, 0, 1, 3) + if type(current) is views.CurrentContour: + self.contourTypeGroup = QtWidgets.QGroupBox('Contour type:') + self.contourTypeFrame = QtWidgets.QGridLayout() + self.contourNumberLabel = wc.QLeftLabel("Number:", self) + self.contourTypeFrame.addWidget(self.contourNumberLabel, 0, 0) + self.numLEntry = wc.SsnakeSpinBox() + self.numLEntry.setMaximum(100000) + self.numLEntry.setMinimum(1) + self.numLEntry.setToolTip(TOOLTIPS['contourNumber']) + self.numLEntry.setValue(current.viewSettings["numLevels"]) + self.numLEntry.valueChanged.connect(self.setContour) + self.contourTypeFrame.addWidget(self.numLEntry, 0, 1) + self.contourTypeFrame.addWidget(wc.QLeftLabel("Sign:", self), 1, 0) + self.contourSignEntry = QtWidgets.QComboBox() + self.contourSignEntry.setToolTip(TOOLTIPS['contourSign']) + self.contourSignEntry.addItems(['Both', '+ only', '- only']) + self.contourSignEntry.setCurrentIndex(current.viewSettings["contourSign"]) + self.contourSignEntry.currentIndexChanged.connect(self.setContour) + self.contourTypeFrame.addWidget(self.contourSignEntry, 1, 1) + self.contourTypeLabel = wc.QLeftLabel("Type:", self) + self.contourTypeFrame.addWidget(self.contourTypeLabel, 2, 0) + self.contourTypeEntry = QtWidgets.QComboBox() + self.contourTypeEntry.setToolTip(TOOLTIPS['contourType']) + self.contourTypeEntry.addItems(['Linear', 'Multiplier']) + self.contourTypeEntry.setCurrentIndex(current.viewSettings["contourType"]) + self.contourTypeEntry.currentIndexChanged.connect(self.setContour) + self.contourTypeFrame.addWidget(self.contourTypeEntry, 2, 1) + self.multiValueLabel = wc.QLeftLabel("Multiplier:", self) + self.contourTypeFrame.addWidget(self.multiValueLabel, 3, 0) + self.multiValue = wc.QLineEdit(current.viewSettings["multiValue"], self.setContour) + self.multiValue.setToolTip(TOOLTIPS['contourMultiplier']) + self.multiValue.setMaximumWidth(120) + self.contourTypeFrame.addWidget(self.multiValue, 3, 1) + if current.viewSettings["contourType"] != 1: + self.multiValueLabel.hide() + self.multiValue.hide() + self.contourTypeGroup.setLayout(self.contourTypeFrame) + self.frame2.addWidget(self.contourTypeGroup, 6, 0, 1, 3) + # Contour limits + self.contourLimitsGroup = QtWidgets.QGroupBox('Contour limits [%]:') + self.contourLimitsFrame = QtWidgets.QGridLayout() + self.maxLEntry = wc.QLineEdit(format(current.viewSettings["maxLevels"] * 100.0, '.7g'), self.setContour) + self.maxLEntry.setMaximumWidth(120) + self.maxLEntry.setToolTip(TOOLTIPS['contourMax']) + self.contourLimitsFrame.addWidget(self.maxLEntry, 1, 1) + self.minLEntry = wc.QLineEdit(format(current.viewSettings["minLevels"] * 100.0, '.7g'), self.setContour) + self.minLEntry.setMaximumWidth(120) + self.minLEntry.setToolTip(TOOLTIPS['contourMin']) + self.contourLimitsFrame.addWidget(self.minLEntry, 2, 1) + self.contourLimType = QtWidgets.QComboBox() + self.contourLimType.addItems(['Current 2D', 'Full data']) + self.contourLimType.setCurrentIndex(current.viewSettings["limitType"]) + self.contourLimType.setToolTip(TOOLTIPS['contourLimType']) + self.contourLimType.currentIndexChanged.connect(self.setContour) + self.contourLimitsFrame.addWidget(self.contourLimType, 0, 1) + self.maxLabel = wc.QLeftLabel("Max:", self) + self.minLabel = wc.QLeftLabel("Min:", self) + self.relLabel = wc.QLeftLabel("Rel. to:", self) + self.contourLimitsFrame.addWidget(self.relLabel, 0, 0) + self.contourLimitsFrame.addWidget(self.maxLabel, 1, 0) + self.contourLimitsFrame.addWidget(self.minLabel, 2, 0) + self.contourLimitsGroup.setLayout(self.contourLimitsFrame) + self.frame2.addWidget(self.contourLimitsGroup, 7, 0, 1, 3) # Projections self.contourProjGroup = QtWidgets.QGroupBox('Projections:') self.contourProjFrame = QtWidgets.QGridLayout() @@ -2472,7 +2471,7 @@ def upd(self): buttons1 = [] self.extraButtons1.append(buttons1) self.extraButtons1Group.append(QtWidgets.QButtonGroup(self)) - self.extraButtons1Group[i].buttonClicked.connect(lambda: self.setExtraAxes(True)) + self.extraButtons1Group[i].buttonClicked.connect(self.setExtraAxes) if current.viewSettings["extraData"][i].ndim() > 1: for num in range(current.viewSettings["extraData"][i].ndim()): buttons1.append(QtWidgets.QRadioButton('')) @@ -2484,7 +2483,7 @@ def upd(self): entries[-1].setToolTip(TOOLTIPS['sideFrameDimensionSlice']) frame.addWidget(entries[num], num * 3 + 6, 1) entries[num].setValue(current.viewSettings["extraLoc"][i][num]) - entries[num].valueChanged.connect(lambda event=None, num=num, i=i: self.getExtraSlice(event, num, i)) + entries[num].valueChanged.connect(lambda event, num=num, i=i: self.getExtraSlice(num, i)) self.extraButtons1Group[i].button(current.viewSettings["extraAxes"][i][-1]).toggle() iter1 += 1 addButton = QtWidgets.QPushButton("Add plot", self) @@ -2511,19 +2510,19 @@ def scrollSpacing(self, var): self.spacingEntry.setText('%#.3g' % var) def setSpacing(self, *args): - var = safeEval(self.spacingEntry.text(), length=self.father.current.len(), type='FI') + var = safeEval(self.spacingEntry.text(), length=self.father.current.len(), Type='FI') self.spacingEntry.setText('%#.3g' % var) self.father.current.setSpacing(var) def setContour(self, *args): var1 = self.numLEntry.value() - maxC = safeEval(self.maxLEntry.text(), length=self.father.current.len(), type='FI') + maxC = safeEval(self.maxLEntry.text(), length=self.father.current.len(), Type='FI') if maxC is None: maxC = self.father.current.viewSettings["maxLevels"] * 100 self.father.father.dispMsg('Invalid value for contour maximum') else: maxC = abs(float(maxC)) - minC = safeEval(self.minLEntry.text(), length=self.father.current.len(), type='FI') + minC = safeEval(self.minLEntry.text(), length=self.father.current.len(), Type='FI') if minC is None: minC = self.father.current.viewSettings["minLevels"] * 100 self.father.father.dispMsg('Invalid value for contour minimum') @@ -2541,7 +2540,7 @@ def setContour(self, *args): else: self.multiValue.show() self.multiValueLabel.show() - multi = safeEval(self.multiValue.text(), length=self.father.current.len(), type='FI') + multi = safeEval(self.multiValue.text(), length=self.father.current.len(), Type='FI') if multi is None: multi = self.father.current.viewSettings["multiValue"] self.father.father.dispMsg('Invalid value for contour multiplier') @@ -2552,19 +2551,19 @@ def setContour(self, *args): self.father.current.setLevels(var1, maxC / 100.0, minC / 100.0, limitType, cSign, cType, multi) def changeProj(self, pType, direc): - if pType is 4: - if direc is 1: + if pType == 4: + if direc == 1: self.projTraceTop.show() else: self.projTraceRight.show() else: self.selectTraceButton.hide() - if direc is 1: + if direc == 1: self.projTraceTop.hide() else: self.projTraceRight.hide() self.father.current.setProjType(pType, direc) - if (self.father.current.viewSettings["projTop"] is 4) or (self.father.current.viewSettings["projRight"] is 4): + if (self.father.current.viewSettings["projTop"] == 4) or (self.father.current.viewSettings["projRight"] == 4): self.selectTraceButton.show() else: self.selectTraceButton.hide() @@ -2591,7 +2590,7 @@ def pickedTraces(self, pos): self.projTraceTop.setValue(pos[3]) if self.father.current.viewSettings["projRight"] == 4 and pos[3] != self.projTraceRight.value(): self.projTraceRight.setValue(pos[0]) - + def changeRanges(self): check = self.rangeCheckbox.isChecked() ranges = [self.projTopRangeMax.value(), self.projTopRangeMin.value(), self.projRightRangeMax.value(), self.projRightRangeMin.value()] @@ -2639,7 +2638,7 @@ def setAxes(self, first=True): self.father.current.setProjTraces(self.projTraceTop.value(), 1) self.father.current.setProjTraces(self.projTraceRight.value(), 0) #Flip diagonal multiplier: - inp = safeEval(self.diagonalEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.diagonalEntry.text(), length=self.father.current.len(), Type='FI') if inp is not None: self.father.current.viewSettings["diagonalMult"] = 1.0 / inp #Make sure the bottom frame nicely inverts the axis units @@ -2659,14 +2658,13 @@ def setAxes(self, first=True): tmp2 = freq2 else: tmp2 = time2 - self.father.bottomframe.changeAxis(tmp2, update = False) - self.father.bottomframe.changeAxis2(tmp1, update = False) + self.father.bottomframe.changeAxis(tmp2, update=False) + self.father.bottomframe.changeAxis2(tmp1, update=False) self.buttons2Group.button(axes2).toggle() - self.getSlice(None, axes, True) + self.getSlice(axes, True) self.upd() - # self.father.menuCheck() - def getSlice(self, event, entryNum, button=False): + def getSlice(self, entryNum, button=False): axisChange = False if button: dimNum = entryNum @@ -2701,13 +2699,13 @@ def getSlice(self, event, entryNum, button=False): self.father.menuCheck() self.upd() - def setExtraAxes(self, first=True): + def setExtraAxes(self, *args): for i in range(len(self.extraButtons1Group)): axes = self.extraButtons1Group[i].checkedId() - self.getExtraSlice(None, axes, i, True) + self.getExtraSlice(axes, i, True) self.father.current.showFid() - def getExtraSlice(self, event, entryNum, entryi, button=False): + def getExtraSlice(self, entryNum, entryi, button=False): length = self.father.current.viewSettings["extraData"][entryi].ndim() if button: dimNum = entryNum @@ -2741,7 +2739,7 @@ def switchDiagonal(self, val): self.father.current.setDiagonal(bool(val)) def setDiagonal(self): - inp = safeEval(self.diagonalEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.diagonalEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: inp = self.father.current.viewSettings["diagonalMult"] self.father.father.dispMsg('Invalid value for diagonal multiplier') @@ -2920,8 +2918,8 @@ def changeSpec(self): self.father.menuCheck() def changeFreq(self): - freq = safeEval(self.freqEntry.text(), length=self.father.current.len(), type='FI') - sw = safeEval(self.swEntry.text(), length=self.father.current.len(), type='FI') + freq = safeEval(self.freqEntry.text(), length=self.father.current.len(), Type='FI') + sw = safeEval(self.swEntry.text(), length=self.father.current.len(), Type='FI') if sw is None: self.father.father.dispMsg('Invalid sweepwidth') elif sw == 0.0: @@ -3121,16 +3119,16 @@ def __init__(self, parent, file): okButton = QtWidgets.QPushButton("&Ok") okButton.clicked.connect(self.applyAndClose) box = QtWidgets.QDialogButtonBox() - box.addButton(cancelButton,QtWidgets.QDialogButtonBox.RejectRole) - box.addButton(okButton,QtWidgets.QDialogButtonBox.AcceptRole) - grid.addWidget(box, 13, 0,1,2) + box.addButton(cancelButton, QtWidgets.QDialogButtonBox.RejectRole) + box.addButton(okButton, QtWidgets.QDialogButtonBox.AcceptRole) + grid.addWidget(box, 13, 0, 1, 2) self.show() self.setFixedSize(self.size()) self.checkType(file) def checkDatOrder(self): tmp = self.dataOrders[self.datOrderBox.currentIndex()] - if tmp == 'RI' or tmp == 'R': + if tmp in ('RI', 'R'): self.swLabel.show() self.swEntry.show() else: @@ -3166,7 +3164,7 @@ def applyAndClose(self): self.dataOrder = self.dataOrders[self.datOrderBox.currentIndex()] self.delim = self.delimiters[self.datDelimBox.currentIndex()] if self.dataOrder == 'RI' or self.dataOrder == 'R': - self.sw = safeEval(self.swEntry.text(), type='FI') + self.sw = safeEval(self.swEntry.text(), Type='FI') if self.sw == 0 or self.sw is None: raise SsnakeException('Spectral Width input is not valid') self.dataDimension = self.numDims.value() @@ -3188,7 +3186,6 @@ def __init__(self, parent): self.setWindowFlags(QtCore.Qt.Window | QtCore.Qt.Tool | QtCore.Qt.WindowContextHelpButtonHint) self.setWindowTitle("Workspace Info") grid = QtWidgets.QGridLayout(self) - workGroup = QtWidgets.QGroupBox('Data:') workFrame = QtWidgets.QGridLayout() workGroup.setLayout(workFrame) @@ -3218,20 +3215,17 @@ def __init__(self, parent): workFrame.addWidget(QtWidgets.QLabel('Type:'), 7, 0) workFrame.addWidget(QtWidgets.QLabel('Complex:'), 8, 0) workFrame.addWidget(QtWidgets.QLabel('Whole Echo:'), 9, 0) - grid.addWidget(workGroup, 0, 0,1,3) - - + grid.addWidget(workGroup, 0, 0, 1, 3) metaGroup = QtWidgets.QGroupBox('Metadata:') metaFrame = QtWidgets.QGridLayout() metaGroup.setLayout(metaFrame) for pos, key in enumerate(self.father.masterData.metaData): metaFrame.addWidget(QtWidgets.QLabel(key), pos, 0) metaFrame.addWidget(wc.QSelectLabel(self.father.masterData.metaData[key]), pos, 1) - grid.addWidget(metaGroup, 1, 0,1,3) - + grid.addWidget(metaGroup, 1, 0, 1, 3) okButton = QtWidgets.QPushButton("&Close") okButton.clicked.connect(self.closeEvent) - grid.addWidget(okButton, 2, 1 ) + grid.addWidget(okButton, 2, 1) self.show() self.setFixedSize(self.size()) @@ -3241,7 +3235,7 @@ def closeEvent(self, *args): self.deleteLater() ################################################################################# -class PhaseWindow(wc.ToolWindows): +class PhaseWindow(wc.ToolWindow): NAME = "Phasing" SINGLESLICE = True @@ -3308,7 +3302,7 @@ def __init__(self, parent): self.firstOrderGroup.setLayout(self.firstOrderFrame) self.grid.addWidget(self.firstOrderGroup, 1, 0, 1, 3) - def setModifierTexts(self,event): + def setModifierTexts(self, event): sign = u"\u00D7" if event.modifiers() & QtCore.Qt.AltModifier: sign = '/' @@ -3340,7 +3334,7 @@ def setZeroOrder(self, value, *args): self.father.current.setPhaseInter(np.pi * self.zeroVal / 180.0, np.pi * self.firstVal / 180.0) def inputZeroOrder(self, *args): - inp = safeEval(self.zeroEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.zeroEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: raise SsnakeException('Phasing: zero order value input is not valid!') self.zeroVal = np.mod(inp + 180, 360) - 180 @@ -3364,7 +3358,7 @@ def setFirstOrder(self, value, *args): self.father.current.setPhaseInter(np.pi * self.zeroVal / 180.0, np.pi * self.firstVal / 180.0) def inputFirstOrder(self, *args): - value = safeEval(self.firstEntry.text(), length=self.father.current.len(), type='FI') + value = safeEval(self.firstEntry.text(), length=self.father.current.len(), Type='FI') if value is None: raise SsnakeException('Phasing: first order value input is not valid!') newZero = (self.zeroVal - (value - self.firstVal) * self.pivotVal / self.father.current.sw()) @@ -3408,12 +3402,12 @@ def stepPhase(self, phase0, phase1): phase0 = step * phase0 phase1 = step * phase1 - inp = safeEval(self.zeroEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.zeroEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: raise SsnakeException('Phasing: zero order value input is not valid!') inp += phase0 * self.PHASE0STEP self.zeroVal = np.mod(inp + 180, 360) - 180 - value = safeEval(self.firstEntry.text(), length=self.father.current.len(), type='FI') + value = safeEval(self.firstEntry.text(), length=self.father.current.len(), Type='FI') if value is None: raise SsnakeException('Phasing: first order value input is not valid!') value += phase1 * self.PHASE1STEP @@ -3432,7 +3426,7 @@ def stepPhase(self, phase0, phase1): self.father.current.setPhaseInter(np.pi * self.zeroVal / 180.0, np.pi * self.firstVal / 180.0) def inputRef(self, *args): - Val = safeEval(self.refEntry.text(), length=self.father.current.len(), type='FI') + Val = safeEval(self.refEntry.text(), length=self.father.current.len(), Type='FI') if Val is None: raise SsnakeException('Phasing: pivot input is not valid!') self.pivotVal = Val @@ -3456,7 +3450,7 @@ def applyFunc(self): ################################################################ -class ApodWindow(wc.ToolWindows): +class ApodWindow(wc.ToolWindow): RESOLUTION = 10000 NAME = "Apodize" @@ -3466,7 +3460,7 @@ def __init__(self, parent): super(ApodWindow, self).__init__(parent) self.entries = {} self.ticks = {} - boldFont=QtGui.QFont() + boldFont = QtGui.QFont() boldFont.setBold(True) self.maximum = 100.0 * self.father.current.sw() / (self.father.current.len()) self.lbstep = 1.0 @@ -3493,7 +3487,7 @@ def __init__(self, parent): self.lorFrame.addWidget(self.rightLor, 1, 2) self.lorScale = wc.SsnakeSlider(QtCore.Qt.Horizontal) self.lorScale.setRange(0, self.RESOLUTION) - self.lorScale.valueChanged.connect(lambda x: self.setLorGauss(x,'lor')) + self.lorScale.valueChanged.connect(lambda x: self.setLorGauss(x, 'lor')) self.lorFrame.addWidget(self.lorScale, 2, 0, 1, 3) self.lorMax = 100.0 * self.father.current.sw() / (self.father.current.len()) self.lorGroup.setLayout(self.lorFrame) @@ -3520,7 +3514,7 @@ def __init__(self, parent): self.gaussFrame.addWidget(self.rightGauss, 4, 2) self.gaussScale = wc.SsnakeSlider(QtCore.Qt.Horizontal) self.gaussScale.setRange(0, self.RESOLUTION) - self.gaussScale.valueChanged.connect(lambda x: self.setLorGauss(x,'gauss')) + self.gaussScale.valueChanged.connect(lambda x: self.setLorGauss(x, 'gauss')) self.gaussFrame.addWidget(self.gaussScale, 5, 0, 1, 3) self.gaussMax = 100.0 * self.father.current.sw() / (self.father.current.len()) self.gaussGroup.setLayout(self.gaussFrame) @@ -3590,7 +3584,7 @@ def __init__(self, parent): self.shiftFrame.addWidget(QtWidgets.QWidget(), 1, 1) shiftEntry.setMinimumSize(widthHint) shiftEntry.setEnabled(False) - self.entries['shift'] = [shiftEntry,shiftLabel] + self.entries['shift'] = [shiftEntry, shiftLabel] self.shiftFrame.addWidget(shiftEntry, 1, 2) self.shiftGroup.setLayout(self.shiftFrame) self.shiftFrame.setColumnStretch(1, 1) @@ -3638,11 +3632,11 @@ def __init__(self, parent): self.shiftingFrame.addWidget(shiftingAxisLabel, 3, 0) self.shiftingFrame.addWidget(QtWidgets.QWidget(), 1, 1) self.shiftingFrame.setColumnStretch(1, 1) - self.entries['shifting'] = [self.shiftingDropdown,shiftingTypeLabel,self.shiftingEntry,shiftingValueLabel,self.shiftingAxis,shiftingAxisLabel] + self.entries['shifting'] = [self.shiftingDropdown, shiftingTypeLabel, self.shiftingEntry, shiftingValueLabel, self.shiftingAxis, shiftingAxisLabel] self.shiftingGroup.setLayout(self.shiftingFrame) self.grid.addWidget(self.shiftingGroup, 5, 0, 1, 3) - def setModifierTexts(self,event): + def setModifierTexts(self, event): sign = u"\u00D7" if event.modifiers() & QtCore.Qt.AltModifier: sign = '/' @@ -3667,7 +3661,7 @@ def keyPressEvent(self, event): def keyReleaseEvent(self, event): self.setModifierTexts(event) - def dropdownChanged(self,update = True): + def dropdownChanged(self, update=True): index = self.shiftingDropdown.currentIndex() if index == 0: self.shiftingEntry.setEnabled(True) @@ -3686,9 +3680,9 @@ def checkEval(self, key): elem.setEnabled(False) if self.father.current.data.ndim() > 1: if self.ticks['shifting'].isChecked(): - self.dropdownChanged(update = False) #Check dropdown state - if key == 'lor' or key == 'gauss': # for lorentzian and gaussian - if safeEval(self.entries[key][0].text(), length=self.father.current.len(), type='FI') != 0.0: # only update if value was not zero + self.dropdownChanged(update=False) #Check dropdown state + if key in ('lor', 'gauss'): # for lorentzian and gaussian + if safeEval(self.entries[key][0].text(), length=self.father.current.len(), Type='FI') != 0.0: # only update if value was not zero self.apodPreview() else: self.apodPreview() @@ -3739,41 +3733,41 @@ def checkInput(self): shifting = 0.0 shiftingAxis = None if self.ticks['lor'].isChecked(): - lor = safeEval(self.entries['lor'][0].text(), length=self.father.current.len(), type='FI') + lor = safeEval(self.entries['lor'][0].text(), length=self.father.current.len(), Type='FI') if lor is None: self.father.current.showFid() raise SsnakeException('Apodize: Lorentzian value is not valid!') self.lorScale.setValue(round(lor * self.RESOLUTION / self.maximum)) if self.ticks['gauss'].isChecked(): - gauss = safeEval(self.entries['gauss'][0].text(), length=self.father.current.len(), type='FI') + gauss = safeEval(self.entries['gauss'][0].text(), length=self.father.current.len(), Type='FI') if gauss is None: self.father.current.showFid() raise SsnakeException('Apodize: Gaussian value is not valid!') self.gaussScale.setValue(round(gauss * self.RESOLUTION / self.maximum)) if self.ticks['cos2'].isChecked(): - cos2 = safeEval(self.entries['cos2'][0].text(), length=self.father.current.len(), type='FI') + cos2 = safeEval(self.entries['cos2'][0].text(), length=self.father.current.len(), Type='FI') if cos2 is None: self.father.current.showFid() raise SsnakeException('Apodize: cos^2 frequency value is not valid!') if self.ticks['cos2'].isChecked(): - cos2Ph = safeEval(self.entries['cos2'][2].text(), length=self.father.current.len(), type='FI') + cos2Ph = safeEval(self.entries['cos2'][2].text(), length=self.father.current.len(), Type='FI') if cos2Ph is None: self.father.current.showFid() raise SsnakeException('Apodize: cos^2 phase value is not valid!') if self.ticks['hamming'].isChecked(): - hamming = safeEval(self.entries['hamming'][1].text(), length=self.father.current.len(), type='FI') + hamming = safeEval(self.entries['hamming'][1].text(), length=self.father.current.len(), Type='FI') if hamming is None: self.father.current.showFid() raise SsnakeException('Apodize: Hamming value is not valid!') if self.ticks['shift'].isChecked(): - shift = safeEval(self.entries['shift'][0].text(), length=self.father.current.len(), type='FI') + shift = safeEval(self.entries['shift'][0].text(), length=self.father.current.len(), Type='FI') if shift is None: self.father.current.showFid() raise SsnakeException('Apodize: Shift value is not valid!') if self.father.current.data.ndim() > 1: if self.ticks['shifting'].isChecked(): if self.father.current.data.ndim() > 1: - shifting = safeEval(self.shiftingEntry.text(), length=self.father.current.len(), type='FI') + shifting = safeEval(self.shiftingEntry.text(), length=self.father.current.len(), Type='FI') if shifting is None: self.father.current.showFid() raise SsnakeException('Apodize: Shifting value is not valid!') @@ -3788,7 +3782,7 @@ def applyFunc(self): ####################################################################################### -class SizeWindow(wc.ToolWindows): +class SizeWindow(wc.ToolWindow): NAME = "Set size" @@ -3825,7 +3819,7 @@ def __init__(self, parent): self.father.current.peakPick = True def stepSize(self, forward): - inp = safeEval(self.sizeEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.sizeEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: raise SsnakeException('Sizing: \'Size\' input is not valid') inp = int(round(inp)) @@ -3841,14 +3835,14 @@ def stepSize(self, forward): self.sizePreview() def sizePreview(self, *args): - inp = safeEval(self.sizeEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.sizeEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: raise SsnakeException('Sizing: \'Size\' input is not valid') self.sizeVal = int(round(inp)) if self.sizeVal < 1: raise SsnakeException('Sizing: \'Size\' cannot be below 1') self.sizeEntry.setText(str(self.sizeVal)) - inp = safeEval(self.posEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.posEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: raise SsnakeException('Sizing: \'Offset\' input is not valid') self.posVal = int(round(inp)) @@ -3858,13 +3852,13 @@ def sizePreview(self, *args): self.father.current.resizePreview(self.sizeVal, self.posVal) def applyFunc(self): - inp = safeEval(self.sizeEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.sizeEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: raise SsnakeException('Sizing: \'Size\' input is not valid') self.sizeVal = int(round(inp)) if self.sizeVal < 1: raise SsnakeException('Sizing: \'Size\' cannot be below 1') - inp = safeEval(self.posEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.posEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: raise SsnakeException('Sizing: \'Offset\' input is not valid') self.posVal = int(round(inp)) @@ -3882,7 +3876,7 @@ def picked(self, pos): ########################################################################################## -class SwapEchoWindow(wc.ToolWindows): +class SwapEchoWindow(wc.ToolWindow): NAME = "Swap echo" @@ -3896,7 +3890,7 @@ def __init__(self, parent): self.father.current.peakPick = True def swapEchoPreview(self, *args): - inp = safeEval(self.posEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.posEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: raise SsnakeException("Swap echo: not a valid index") self.posVal = int(round(inp)) @@ -3907,7 +3901,7 @@ def swapEchoPreview(self, *args): def applyFunc(self): self.father.current.peakPickReset() - inp = safeEval(self.posEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.posEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: raise SsnakeException("Swap echo: not a valid index") self.posVal = int(round(inp)) @@ -3925,7 +3919,7 @@ def picked(self, pos): ########################################################################### -class LPSVDWindow(wc.ToolWindows): +class LPSVDWindow(wc.ToolWindow): NAME = "LPSVD" @@ -3953,13 +3947,13 @@ def __init__(self, parent): self.grid.addWidget(self.nPredictEntry, 8, 0) def applyFunc(self): - analPoints = safeEval(self.aPointsEntry.text(), length=self.father.current.len(), type='FI') + analPoints = safeEval(self.aPointsEntry.text(), length=self.father.current.len(), Type='FI') if analPoints is None: raise SsnakeException('LPSVD: Number of points for analysis is not valid') - numberFreq = safeEval(self.nFreqEntry.text(), length=self.father.current.len(), type='FI') + numberFreq = safeEval(self.nFreqEntry.text(), length=self.father.current.len(), Type='FI') if numberFreq is None: raise SsnakeException('LPSVD: Number of frequencies is not valid') - predictPoints = safeEval(self.nPredictEntry.text(), length=self.father.current.len(), type='FI') + predictPoints = safeEval(self.nPredictEntry.text(), length=self.father.current.len(), Type='FI') if predictPoints is None: raise SsnakeException('LPSVD: Number of points to predict is not valid') if analPoints > self.father.current.len(): @@ -3975,13 +3969,12 @@ def applyFunc(self): ########################################################################### -class ScaleSWWindow(wc.ToolWindows): +class ScaleSWWindow(wc.ToolWindow): NAME = "Scale SW" def __init__(self, parent): super(ScaleSWWindow, self).__init__(parent) - options = list(map(str, range(1, self.father.masterData.ndim() + 1))) self.grid.addWidget(wc.QLabel("Scale Factor:"), 0, 0) self.scaleDropdown = QtWidgets.QComboBox() self.scaleDropdown.addItems(['User Defined', 'Spin 3/2, -3Q (9/34)', 'Spin 5/2, 3Q (-12/17)', 'Spin 5/2, -5Q (12/85)', 'Spin 7/2, 3Q (-45/34)', @@ -3997,7 +3990,7 @@ def dropdownChanged(self): self.scaleEntry.setText("%.9f" % self.scaleList[index]) def applyFunc(self): - scale = safeEval(self.scaleEntry.text(), length=self.father.current.len(), type='FI') + scale = safeEval(self.scaleEntry.text(), length=self.father.current.len(), Type='FI') if scale is None: raise SsnakeException("Scale SW: Factor not a valid value") self.father.current.scaleSw(scale) @@ -4005,7 +3998,7 @@ def applyFunc(self): ########################################################################### -class ShiftDataWindow(wc.ToolWindows): +class ShiftDataWindow(wc.ToolWindow): NAME = "Shifting data" SINGLESLICE = True @@ -4027,40 +4020,39 @@ def __init__(self, parent): self.grid.addWidget(rightShift, 1, 2) def stepUpShift(self, *args): - inp = safeEval(self.shiftEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.shiftEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: raise SsnakeException("Shift data: shift value not valid") self.shiftVal = int(round(inp)) - shift = +1 + shift = 1 if QtWidgets.qApp.keyboardModifiers() & QtCore.Qt.ControlModifier and QtWidgets.qApp.keyboardModifiers() & QtCore.Qt.ShiftModifier: - shift *= +1000 + shift *= 1000 elif QtWidgets.qApp.keyboardModifiers() & QtCore.Qt.ControlModifier: - shift *= +10 + shift *= 10 elif QtWidgets.qApp.keyboardModifiers() & QtCore.Qt.ShiftModifier: - shift *= +100 + shift *= 100 self.shiftVal = self.shiftVal + shift self.shiftEntry.setText(str(self.shiftVal)) self.shiftPreview() def stepDownShift(self, *args): - inp = safeEval(self.shiftEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.shiftEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: raise SsnakeException("Shift data: shift value not valid") self.shiftVal = int(round(inp)) - shift = -1 + shift = -1 if QtWidgets.qApp.keyboardModifiers() & QtCore.Qt.ControlModifier and QtWidgets.qApp.keyboardModifiers() & QtCore.Qt.ShiftModifier: - shift *= +1000 + shift *= 1000 elif QtWidgets.qApp.keyboardModifiers() & QtCore.Qt.ControlModifier: - shift *= +10 + shift *= 10 elif QtWidgets.qApp.keyboardModifiers() & QtCore.Qt.ShiftModifier: - shift *= +100 - + shift *= 100 self.shiftVal = self.shiftVal + shift self.shiftEntry.setText(str(self.shiftVal)) self.shiftPreview() def shiftPreview(self, *args): - inp = safeEval(self.shiftEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.shiftEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: raise SsnakeException("Shift data: shift value not valid") self.shiftVal = int(round(inp)) @@ -4068,7 +4060,7 @@ def shiftPreview(self, *args): self.father.current.shiftPreview(self.shiftVal) def applyFunc(self): - inp = safeEval(self.shiftEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.shiftEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: raise SsnakeException("Shift data: shift value not valid") shift = int(round(inp)) @@ -4076,7 +4068,7 @@ def applyFunc(self): ########################################################################### -class RollDataWindow(wc.ToolWindows): +class RollDataWindow(wc.ToolWindow): NAME = "Roll data" SINGLESLICE = True @@ -4098,39 +4090,39 @@ def __init__(self, parent): self.grid.addWidget(rightShift, 1, 2) def stepUpShift(self, *args): - inp = safeEval(self.shiftEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.shiftEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: raise SsnakeException("Roll data: roll value not valid") self.shiftVal = inp - shift = +1 + shift = 1 if QtWidgets.qApp.keyboardModifiers() & QtCore.Qt.ControlModifier and QtWidgets.qApp.keyboardModifiers() & QtCore.Qt.ShiftModifier: - shift *= +1000 + shift *= 1000 elif QtWidgets.qApp.keyboardModifiers() & QtCore.Qt.ControlModifier: - shift *= +10 + shift *= 10 elif QtWidgets.qApp.keyboardModifiers() & QtCore.Qt.ShiftModifier: - shift *= +100 + shift *= 100 self.shiftVal = self.shiftVal + shift self.shiftEntry.setText(str(self.shiftVal)) self.rollPreview() def stepDownShift(self, *args): - inp = safeEval(self.shiftEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.shiftEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: raise SsnakeException("Roll data: roll value not valid") self.shiftVal = inp - shift = -1 + shift = -1 if QtWidgets.qApp.keyboardModifiers() & QtCore.Qt.ControlModifier and QtWidgets.qApp.keyboardModifiers() & QtCore.Qt.ShiftModifier: - shift *= +1000 + shift *= 1000 elif QtWidgets.qApp.keyboardModifiers() & QtCore.Qt.ControlModifier: - shift *= +10 + shift *= 10 elif QtWidgets.qApp.keyboardModifiers() & QtCore.Qt.ShiftModifier: - shift *= +100 + shift *= 100 self.shiftVal = self.shiftVal + shift self.shiftEntry.setText(str(self.shiftVal)) self.rollPreview() def rollPreview(self, *args): - inp = safeEval(self.shiftEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.shiftEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: raise SsnakeException("Roll data: roll value not valid") self.shiftVal = inp @@ -4138,7 +4130,7 @@ def rollPreview(self, *args): self.father.current.rollPreview(self.shiftVal) def applyFunc(self): - inp = safeEval(self.shiftEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.shiftEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: raise SsnakeException("Roll data: roll value not valid") shift = inp @@ -4147,7 +4139,7 @@ def applyFunc(self): ############################################################# -class DCWindow(wc.ToolWindows): +class DCWindow(wc.ToolWindow): NAME = "Offset correction" SINGLESLICE = True @@ -4156,10 +4148,10 @@ def __init__(self, parent): super(DCWindow, self).__init__(parent) self.startVal = int(round(0.8 * parent.current.len())) self.endVal = parent.current.len() - self.grid.addWidget(wc.QLabel("Start point:"), 0, 0) + self.grid.addWidget(wc.QLabel("Start index:"), 0, 0) self.startEntry = wc.QLineEdit(self.startVal, self.offsetPreview) self.grid.addWidget(self.startEntry, 1, 0) - self.grid.addWidget(wc.QLabel("End point:"), 2, 0) + self.grid.addWidget(wc.QLabel("End index:"), 2, 0) self.endEntry = wc.QLineEdit(self.endVal, self.offsetPreview) self.grid.addWidget(self.endEntry, 3, 0) self.grid.addWidget(wc.QLabel("Offset:"), 4, 0) @@ -4172,7 +4164,7 @@ def __init__(self, parent): def picked(self, pos, second=False): dataLength = self.father.current.len() if second: - inp = safeEval(self.startEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.startEntry.text(), length=self.father.current.len(), Type='FI') if inp is not None: self.startVal = int(round(inp)) if self.startVal < 0: @@ -4192,7 +4184,7 @@ def picked(self, pos, second=False): self.father.current.peakPick = True else: self.startEntry.setText(str(pos[0])) - inp = safeEval(self.endEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.endEntry.text(), length=self.father.current.len(), Type='FI') if inp is not None: self.endVal = int(round(inp)) if self.endVal < 0: @@ -4205,18 +4197,18 @@ def picked(self, pos, second=False): self.offsetEntry.setText('{:.2e}'.format(val)) else: self.offsetEntry.setText('') - self.father.current.peakPickFunc = lambda pos, self= self: self.picked(pos, True) + self.father.current.peakPickFunc = lambda pos, self=self: self.picked(pos, True) self.father.current.peakPick = True def offsetPreview(self, inserted=False): if inserted: - dcVal = safeEval(self.offsetEntry.text(), length=self.father.current.len(), type='C') + dcVal = safeEval(self.offsetEntry.text(), length=self.father.current.len(), Type='C') if dcVal is None: raise SsnakeException("Offset correction: offset value not valid") self.father.current.dcOffset(dcVal) else: dataLength = self.father.current.len() - inp = safeEval(self.startEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.startEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: raise SsnakeException("Offset correction: start value not valid") self.startVal = int(round(inp)) @@ -4225,7 +4217,7 @@ def offsetPreview(self, inserted=False): elif self.startVal > dataLength: self.startVal = dataLength self.startEntry.setText(str(self.startVal)) - inp = safeEval(self.endEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.endEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: raise SsnakeException("Offset correction: end value not valid") self.endVal = int(round(inp)) @@ -4239,7 +4231,7 @@ def offsetPreview(self, inserted=False): self.father.current.dcOffset(val) def applyFunc(self): - inp = safeEval(self.offsetEntry.text(), length=self.father.current.len(), type='C') + inp = safeEval(self.offsetEntry.text(), length=self.father.current.len(), Type='C') if inp is None: raise SsnakeException("Offset correction: offset value not valid") self.father.current.peakPickReset() @@ -4248,7 +4240,7 @@ def applyFunc(self): ############################################################# -class BaselineWindow(wc.ToolWindows): +class BaselineWindow(wc.ToolWindow): NAME = "Baseline correction" SINGLESLICE = True @@ -4303,7 +4295,7 @@ def closeEvent(self, *args): def applyFunc(self): inp = self.degreeEntry.value() if self.allFitButton.isChecked(): - self.father.current.baselineCorrectionAll(inp, self.removeList, self.singleSlice.isChecked(), invert=self.invertButton.isChecked()) + self.father.current.baselineCorrectionAll(inp, self.removeList, invert=self.invertButton.isChecked()) else: self.father.current.baselineCorrection(inp, self.removeList, self.singleSlice.isChecked(), invert=self.invertButton.isChecked()) self.father.current.peakPickReset() @@ -4312,15 +4304,15 @@ def applyFunc(self): ############################################################# -class regionWindow(wc.ToolWindows): +class regionWindow(wc.ToolWindow): def __init__(self, parent, name): self.NAME = name super(regionWindow, self).__init__(parent) self.startVal = [0] # dummy variables self.endVal = [parent.current.len()] # dummy variables - self.grid.addWidget(wc.QLabel("Start point:"), 0, 0) - self.grid.addWidget(wc.QLabel("End point:"), 0, 1) + self.grid.addWidget(wc.QLabel("Start index:"), 0, 0) + self.grid.addWidget(wc.QLabel("End index:"), 0, 1) self.startEntry = [] self.endEntry = [] self.deleteButton = [] @@ -4397,7 +4389,7 @@ def picked(self, pos): self.father.current.peakPick = True def setVal(self, entry, isMin=False): - inp = safeEval(entry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(entry.text(), length=self.father.current.len(), Type='FI') error = False if inp is not None: inp = int(inp) @@ -4580,17 +4572,17 @@ def apply(self, maximum, minimum, newSpec): ############################################################# -class regionWindow2(wc.ToolWindows): +class regionWindow2(wc.ToolWindow): def __init__(self, parent, name, newSpecOption): self.NAME = name super(regionWindow2, self).__init__(parent) self.startVal = 0 self.endVal = parent.current.len() - self.grid.addWidget(wc.QLabel("Start point:"), 0, 0) + self.grid.addWidget(wc.QLabel("Start index:"), 0, 0) self.startEntry = wc.QLineEdit(self.startVal, self.checkValues) self.grid.addWidget(self.startEntry, 1, 0) - self.grid.addWidget(wc.QLabel("End point:"), 2, 0) + self.grid.addWidget(wc.QLabel("End index:"), 2, 0) self.endEntry = wc.QLineEdit(self.endVal, self.checkValues) self.grid.addWidget(self.endEntry, 3, 0) self.newSpec = QtWidgets.QCheckBox("Result in new workspace") @@ -4606,7 +4598,7 @@ def preview(self, maximum, minimum): def picked(self, pos, second=False): if second: dataLength = self.father.current.len() - inp = safeEval(self.startEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.startEntry.text(), length=self.father.current.len(), Type='FI') if inp is not None: self.startVal = int(round(inp)) if self.startVal < 0: @@ -4626,7 +4618,7 @@ def picked(self, pos, second=False): def checkValues(self, *args): dataLength = self.father.current.len() - inp = safeEval(self.startEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.startEntry.text(), length=self.father.current.len(), Type='FI') if inp is not None: self.startVal = int(round(inp)) if self.startVal < 0: @@ -4634,7 +4626,7 @@ def checkValues(self, *args): elif self.startVal > dataLength: self.startVal = dataLength self.startEntry.setText(str(self.startVal)) - inp = safeEval(self.endEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.endEntry.text(), length=self.father.current.len(), Type='FI') if inp is not None: self.endVal = int(round(inp)) if self.endVal < 0: @@ -4646,7 +4638,7 @@ def checkValues(self, *args): def applyFunc(self): dataLength = self.father.current.len() - inp = safeEval(self.startEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.startEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: raise SsnakeException(self.NAME + ": value not valid") self.startVal = int(round(inp)) @@ -4654,7 +4646,7 @@ def applyFunc(self): self.startVal = 0 elif self.startVal > dataLength: self.startVal = dataLength - inp = safeEval(self.endEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.endEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: raise SsnakeException(self.NAME + ": value not valid") self.endVal = int(round(inp)) @@ -4719,7 +4711,7 @@ def apply(self, maximum, minimum, newSpec): ############################################################# -class FiddleWindow(wc.ToolWindows): +class FiddleWindow(wc.ToolWindow): NAME = "Reference deconvolution" @@ -4727,10 +4719,10 @@ def __init__(self, parent): super(FiddleWindow, self).__init__(parent) self.startVal = 0 self.endVal = parent.current.len() - self.grid.addWidget(wc.QLabel("Start point:"), 0, 0) + self.grid.addWidget(wc.QLabel("Start index:"), 0, 0) self.startEntry = wc.QLineEdit(self.startVal, self.checkValues) self.grid.addWidget(self.startEntry, 1, 0) - self.grid.addWidget(wc.QLabel("End point:"), 2, 0) + self.grid.addWidget(wc.QLabel("End index:"), 2, 0) self.endEntry = wc.QLineEdit(self.endVal, self.checkValues) self.grid.addWidget(self.endEntry, 3, 0) self.grid.addWidget(wc.QLabel("Linebroadening [Hz]:"), 4, 0) @@ -4742,7 +4734,7 @@ def __init__(self, parent): def picked(self, pos, second=False): if second: dataLength = self.father.current.len() - inp = safeEval(self.startEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.startEntry.text(), length=self.father.current.len(), Type='FI') if inp is not None: self.startVal = int(round(inp)) if self.startVal < 0: @@ -4761,7 +4753,7 @@ def picked(self, pos, second=False): def checkValues(self, *args): dataLength = self.father.current.len() - inp = safeEval(self.startEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.startEntry.text(), length=self.father.current.len(), Type='FI') if inp is not None: self.startVal = int(round(inp)) if self.startVal < 0: @@ -4769,7 +4761,7 @@ def checkValues(self, *args): elif self.startVal > dataLength: self.startVal = dataLength self.startEntry.setText(str(self.startVal)) - inp = safeEval(self.endEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.endEntry.text(), length=self.father.current.len(), Type='FI') if inp is not None: self.endVal = int(round(inp)) if self.endVal < 0: @@ -4777,13 +4769,13 @@ def checkValues(self, *args): elif self.endVal > dataLength: self.endVal = dataLength self.endEntry.setText(str(self.endVal)) - inp = safeEval(self.lbEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.lbEntry.text(), length=self.father.current.len(), Type='FI') if inp is not None: self.lbEntry.setText(str(inp)) def applyFunc(self): dataLength = self.father.current.len() - inp = safeEval(self.startEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.startEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: raise SsnakeException("Reference deconv: start entry not valid") self.startVal = int(round(inp)) @@ -4791,7 +4783,7 @@ def applyFunc(self): self.startVal = 0 elif self.startVal > dataLength: self.startVal = dataLength - inp = safeEval(self.endEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.endEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: raise SsnakeException("Reference deconv: end entry not valid") self.endVal = int(round(inp)) @@ -4799,7 +4791,7 @@ def applyFunc(self): self.endVal = 0 elif self.endVal > dataLength: self.endVal = dataLength - lb = safeEval(self.lbEntry.text(), length=self.father.current.len(), type='FI') + lb = safeEval(self.lbEntry.text(), length=self.father.current.len(), Type='FI') if lb is None: raise SsnakeException("Reference deconv: Linebroadening entry not valid") self.father.current.fiddle(self.startVal, self.endVal, lb) @@ -4807,7 +4799,7 @@ def applyFunc(self): ############################################################## -class DeleteWindow(wc.ToolWindows): +class DeleteWindow(wc.ToolWindow): NAME = "Delete" @@ -4845,7 +4837,7 @@ def applyFunc(self): ############################################################## -class SplitWindow(wc.ToolWindows): +class SplitWindow(wc.ToolWindow): NAME = "Split" @@ -4856,13 +4848,13 @@ def __init__(self, parent): self.grid.addWidget(self.splitEntry, 1, 0) def preview(self, *args): - val = safeEval(self.splitEntry.text(), length=self.father.current.len(), type='FI') + val = safeEval(self.splitEntry.text(), length=self.father.current.len(), Type='FI') if val is None: raise SsnakeException("Split: input not valid") self.splitEntry.setText(str(int(round(val)))) def applyFunc(self): - val = safeEval(self.splitEntry.text(), length=self.father.current.len(), type='FI') + val = safeEval(self.splitEntry.text(), length=self.father.current.len(), Type='FI') if val is None: raise SsnakeException("Split: input not valid") val = int(val) @@ -4873,7 +4865,7 @@ def applyFunc(self): ############################################################## -class ConcatenateWindow(wc.ToolWindows): +class ConcatenateWindow(wc.ToolWindow): NAME = "Concatenate" @@ -4890,7 +4882,7 @@ def applyFunc(self): ############################################################## -class InsertWindow(wc.ToolWindows): +class InsertWindow(wc.ToolWindow): NAME = "Insert" @@ -4905,7 +4897,7 @@ def __init__(self, parent): self.grid.addWidget(self.wsEntry, 3, 0) def preview(self, *args): - pos = safeEval(self.posEntry.text(), length=self.father.current.len(), type='FI') + pos = safeEval(self.posEntry.text(), length=self.father.current.len(), Type='FI') if pos is None: return pos = int(round(pos)) @@ -4916,7 +4908,7 @@ def preview(self, *args): self.posEntry.setText(str(pos)) def applyFunc(self): - pos = safeEval(self.posEntry.text(), length=self.father.current.len(), type='FI') + pos = safeEval(self.posEntry.text(), length=self.father.current.len(), Type='FI') if pos is None: raise SsnakeException("Not a valid value") pos = int(round(pos)) @@ -4930,7 +4922,7 @@ def applyFunc(self): ############################################################## -class CombineWindow(wc.ToolWindows): +class CombineWindow(wc.ToolWindow): SINGLESLICE = True RESIZABLE = True @@ -4938,38 +4930,38 @@ class CombineWindow(wc.ToolWindows): def __init__(self, parent, combType): super(CombineWindow, self).__init__(parent) self.combType = combType # 0 = add, 1 = subtract, 2 = multiply, 3 = divide - if self.combType is 0: + if self.combType == 0: self.WindowTitle = "Add" self.grid.addWidget(wc.QLabel("Workspace to add:"), 0, 0) - elif self.combType is 1: + elif self.combType == 1: self.WindowTitle = "Subtract" self.grid.addWidget(wc.QLabel("Workspace to subtract:"), 0, 0) - elif self.combType is 2: + elif self.combType == 2: self.WindowTitle = "Multiply" self.grid.addWidget(wc.QLabel("Workspace to multiply:"), 0, 0) - elif self.combType is 3: + elif self.combType == 3: self.WindowTitle = "Divide" self.grid.addWidget(wc.QLabel("Workspace to divide:"), 0, 0) - self.setWindowTitle(self.WindowTitle) + self.setWindowTitle(self.WindowTitle) self.wsEntry = QtWidgets.QComboBox() self.wsEntry.addItems(self.father.father.workspaceNames) self.grid.addWidget(self.wsEntry, 1, 0) def applyFunc(self): ws = self.wsEntry.currentIndex() - if self.combType is 0: + if self.combType == 0: returnValue = self.father.current.add(self.father.father.workspaces[ws].masterData.getData(), self.singleSlice.isChecked()) - elif self.combType is 1: + elif self.combType == 1: returnValue = self.father.current.subtract(self.father.father.workspaces[ws].masterData.getData(), self.singleSlice.isChecked()) - elif self.combType is 2: + elif self.combType == 2: returnValue = self.father.current.multiply(self.father.father.workspaces[ws].masterData.getData(), self.singleSlice.isChecked()) - elif self.combType is 3: + elif self.combType == 3: returnValue = self.father.current.divide(self.father.father.workspaces[ws].masterData.getData(), self.singleSlice.isChecked()) ############################################################## -class SNWindow(wc.ToolWindows): +class SNWindow(wc.ToolWindow): NAME = "Signal to noise" CANCELNAME = "&Close" @@ -4978,16 +4970,16 @@ class SNWindow(wc.ToolWindows): def __init__(self, parent): super(SNWindow, self).__init__(parent) - self.grid.addWidget(wc.QLabel("Start point noise:"), 0, 0) + self.grid.addWidget(wc.QLabel("Start index noise:"), 0, 0) self.minNoiseEntry = wc.QLineEdit('0', self.checkValues) self.grid.addWidget(self.minNoiseEntry, 1, 0) - self.grid.addWidget(wc.QLabel("End point noise:"), 2, 0) + self.grid.addWidget(wc.QLabel("End index noise:"), 2, 0) self.maxNoiseEntry = wc.QLineEdit(parent.current.len(), self.checkValues) self.grid.addWidget(self.maxNoiseEntry, 3, 0) - self.grid.addWidget(wc.QLabel("Start point signal:"), 4, 0) + self.grid.addWidget(wc.QLabel("Start index signal:"), 4, 0) self.minEntry = wc.QLineEdit('0', self.checkValues) self.grid.addWidget(self.minEntry, 5, 0) - self.grid.addWidget(wc.QLabel("End point signal:"), 6, 0) + self.grid.addWidget(wc.QLabel("End index signal:"), 6, 0) self.maxEntry = wc.QLineEdit(parent.current.len(), self.checkValues) self.grid.addWidget(self.maxEntry, 7, 0) self.grid.addWidget(wc.QLabel("S/N:"), 8, 0) @@ -5017,7 +5009,7 @@ def picked(self, pos, num=0): def checkValues(self, *args): dataLength = self.father.current.len() - inp = safeEval(self.minNoiseEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.minNoiseEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: return minimum = int(round(inp)) @@ -5026,7 +5018,7 @@ def checkValues(self, *args): elif minimum > dataLength: minimum = dataLength self.minNoiseEntry.setText(str(minimum)) - inp = safeEval(self.maxNoiseEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.maxNoiseEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: return maximum = int(round(inp)) @@ -5035,7 +5027,7 @@ def checkValues(self, *args): elif maximum > dataLength: maximum = dataLength self.maxNoiseEntry.setText(str(maximum)) - inp = safeEval(self.minEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.minEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: return minimum = int(round(inp)) @@ -5044,7 +5036,7 @@ def checkValues(self, *args): elif minimum > dataLength: minimum = dataLength self.minEntry.setText(str(minimum)) - inp = safeEval(self.maxEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.maxEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: return maximum = int(round(inp)) @@ -5057,7 +5049,7 @@ def checkValues(self, *args): def applyFunc(self): dataLength = self.father.current.len() - inp = safeEval(self.minNoiseEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.minNoiseEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: raise SsnakeException("S/N: invalid range") minimumNoise = int(round(inp)) @@ -5066,7 +5058,7 @@ def applyFunc(self): elif minimumNoise > dataLength: minimumNoise = dataLength self.minNoiseEntry.setText(str(minimumNoise)) - inp = safeEval(self.maxNoiseEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.maxNoiseEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: raise SsnakeException("S/N: invalid range") maximumNoise = int(round(inp)) @@ -5075,7 +5067,7 @@ def applyFunc(self): elif maximumNoise > dataLength: maximumNoise = dataLength self.maxNoiseEntry.setText(str(maximumNoise)) - inp = safeEval(self.minEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.minEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: raise SsnakeException("S/N: invalid range") minimum = int(round(inp)) @@ -5084,7 +5076,7 @@ def applyFunc(self): elif minimum > dataLength: minimum = dataLength self.minEntry.setText(str(minimum)) - inp = safeEval(self.maxEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.maxEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: raise SsnakeException("S/N: invalid range") maximum = int(round(inp)) @@ -5098,7 +5090,7 @@ def applyFunc(self): ############################################################## -class FWHMWindow(wc.ToolWindows): +class FWHMWindow(wc.ToolWindow): NAME = "FWHM" CANCELNAME = "&Close" @@ -5107,10 +5099,10 @@ class FWHMWindow(wc.ToolWindows): def __init__(self, parent): super(FWHMWindow, self).__init__(parent) - self.grid.addWidget(wc.QLabel("Start point:"), 0, 0) + self.grid.addWidget(wc.QLabel("Start index:"), 0, 0) self.minEntry = wc.QLineEdit('0', self.checkValues) self.grid.addWidget(self.minEntry, 1, 0) - self.grid.addWidget(wc.QLabel("End point:"), 2, 0) + self.grid.addWidget(wc.QLabel("End index:"), 2, 0) self.maxEntry = wc.QLineEdit(parent.current.len(), self.checkValues) self.grid.addWidget(self.maxEntry, 3, 0) self.grid.addWidget(wc.QLabel("Units:"), 4, 0) @@ -5148,7 +5140,7 @@ def picked(self, pos, num=0): def checkValues(self, *args): dataLength = self.father.current.len() - inp = safeEval(self.minEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.minEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: return minimum = int(round(inp)) @@ -5157,40 +5149,7 @@ def checkValues(self, *args): elif minimum > dataLength: minimum = dataLength self.minEntry.setText(str(minimum)) - inp = safeEval(self.maxEntry.text(), length=self.father.current.len(), type='FI') - if inp is None: - return - maximum = int(round(inp)) - if maximum < 0: - maximum = 0 - elif maximum > dataLength: - maximum = dataLength - self.maxEntry.setText(str(maximum)) - self.applyFunc() - - def picked(self, pos, num=0): - if num == 0: - self.minEntry.setText(str(pos[0])) - self.father.current.peakPickFunc = lambda pos, self=self: self.picked(pos, 1) - self.father.current.peakPick = True - elif num == 1: - self.maxEntry.setText(str(pos[0])) - self.father.current.peakPickFunc = lambda pos, self=self: self.picked(pos, 0) - self.father.current.peakPick = True - self.applyFunc() - - def checkValues(self, *args): - dataLength = self.father.current.len() - inp = safeEval(self.minEntry.text(), length=self.father.current.len(), type='FI') - if inp is None: - return - minimum = int(round(inp)) - if minimum < 0: - minimum = 0 - elif minimum > dataLength: - minimum = dataLength - self.minEntry.setText(str(minimum)) - inp = safeEval(self.maxEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.maxEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: return maximum = int(round(inp)) @@ -5203,7 +5162,7 @@ def checkValues(self, *args): def applyFunc(self): dataLength = self.father.current.len() - inp = safeEval(self.minEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.minEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: raise SsnakeException("FWHM: invalid range") minimum = int(round(inp)) @@ -5212,7 +5171,7 @@ def applyFunc(self): elif minimum > dataLength: minimum = dataLength self.minEntry.setText(str(minimum)) - inp = safeEval(self.maxEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.maxEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: raise SsnakeException("FWHM: invalid range") maximum = int(round(inp)) @@ -5227,7 +5186,7 @@ def applyFunc(self): ############################################################## -class COMWindow(wc.ToolWindows): # Centre of Mass Window +class COMWindow(wc.ToolWindow): # Centre of Mass Window NAME = "Centre of Mass" CANCELNAME = "&Close" @@ -5236,11 +5195,10 @@ class COMWindow(wc.ToolWindows): # Centre of Mass Window def __init__(self, parent): super(COMWindow, self).__init__(parent) - self.pickDim = 1 + self.pickDim = 1 if isinstance(self.father.current, views.CurrentContour): - self.pickDim = 2 - - self.grid.addWidget(wc.QLabel("X axis:"), 0, 0,1,2) + self.pickDim = 2 + self.grid.addWidget(wc.QLabel("X axis:"), 0, 0, 1, 2) self.grid.addWidget(wc.QLabel("Start:"), 1, 0) self.grid.addWidget(wc.QLabel("End:"), 2, 0) if self.pickDim == 2: @@ -5251,12 +5209,12 @@ def __init__(self, parent): unitSelectY = 3 else: unitListY = ['s', 'ms', u'μs'] - self.grid.addWidget(wc.QLabel("Y axis:"), 3, 0,1,2) + self.grid.addWidget(wc.QLabel("Y axis:"), 3, 0, 1, 2) self.grid.addWidget(wc.QLabel("Start:"), 4, 0) self.grid.addWidget(wc.QLabel("End:"), 5, 0) - self.minEntryY = wc.QLineEdit("0",lambda: self.applyFunc(False)) + self.minEntryY = wc.QLineEdit("0", lambda: self.applyFunc(False)) self.grid.addWidget(self.minEntryY, 4, 1) - self.maxEntryY = wc.QLineEdit(parent.current.len(-2),lambda: self.applyFunc(False)) + self.maxEntryY = wc.QLineEdit(parent.current.len(-2), lambda: self.applyFunc(False)) self.grid.addWidget(self.maxEntryY, 5, 1) self.grid.addWidget(wc.QLabel("Y Unit:"), 11, 0) self.unitDropY = QtWidgets.QComboBox() @@ -5267,20 +5225,18 @@ def __init__(self, parent): self.comEntryY = wc.QLineEdit("0.0") self.grid.addWidget(wc.QLabel(u"Y COM:"), 12, 0) self.grid.addWidget(self.comEntryY, 12, 1) - if self.father.current.spec() == 1: unitList = ['Hz', 'kHz', 'MHz', 'ppm'] if self.father.current.getppm(): unitSelect = 3 else: unitList = ['s', 'ms', u'μs'] - - self.minEntry = wc.QLineEdit("0",lambda: self.applyFunc(False)) + self.minEntry = wc.QLineEdit("0", lambda: self.applyFunc(False)) self.grid.addWidget(self.minEntry, 1, 1) - self.maxEntry = wc.QLineEdit(parent.current.len(),lambda: self.applyFunc(False)) + self.maxEntry = wc.QLineEdit(parent.current.len(), lambda: self.applyFunc(False)) self.grid.addWidget(self.maxEntry, 2, 1) unitSelect = self.father.current.getAxType() - self.grid.addWidget(wc.QLabel(u"Centre of Mass:"), 8, 0,1,2) + self.grid.addWidget(wc.QLabel(u"Centre of Mass:"), 8, 0, 1, 2) self.grid.addWidget(wc.QLabel("X Unit:"), 9, 0) self.unitDrop = QtWidgets.QComboBox() self.unitDrop.addItems(unitList) @@ -5308,9 +5264,9 @@ def picked(self, pos, num=0): self.father.current.peakPick = self.pickDim self.applyFunc() - def applyFunc(self,calc=True): + def applyFunc(self, calc=True): dataLength = self.father.current.len() - inp = safeEval(self.minEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.minEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: raise SsnakeException("Centre of Mass: invalid range") minimum = int(round(inp)) @@ -5319,7 +5275,7 @@ def applyFunc(self,calc=True): elif minimum > dataLength: minimum = dataLength self.minEntry.setText(str(minimum)) - inp = safeEval(self.maxEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.maxEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: raise SsnakeException("Centre of Mass: invalid range") maximum = int(round(inp)) @@ -5331,7 +5287,7 @@ def applyFunc(self,calc=True): #For contour if self.pickDim == 2: dataLengthY = self.father.current.len(-2) - inp = safeEval(self.minEntryY.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.minEntryY.text(), length=self.father.current.len(), Type='FI') if inp is None: raise SsnakeException("Centre of Mass: invalid range") minimumY = int(round(inp)) @@ -5340,7 +5296,7 @@ def applyFunc(self,calc=True): elif minimumY > dataLengthY: minimumY = dataLengthY self.minEntryY.setText(str(minimumY)) - inp = safeEval(self.maxEntryY.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.maxEntryY.text(), length=self.father.current.len(), Type='FI') if inp is None: raise SsnakeException("Centre of Mass: invalid range") maximumY = int(round(inp)) @@ -5352,9 +5308,9 @@ def applyFunc(self,calc=True): if calc: if self.pickDim == 1: - self.comEntry.setText(str(self.father.current.COM([minimum], [maximum],[self.unitDrop.currentIndex()])[0])) + self.comEntry.setText(str(self.father.current.COM([minimum], [maximum], [self.unitDrop.currentIndex()])[0])) elif self.pickDim == 2: - com = self.father.current.COM([minimum, minimumY], [maximum,maximumY],[self.unitDrop.currentIndex(),self.unitDropY.currentIndex()]) + com = self.father.current.COM([minimum, minimumY], [maximum, maximumY], [self.unitDrop.currentIndex(), self.unitDropY.currentIndex()]) self.comEntry.setText(str(com[0])) self.comEntryY.setText(str(com[1])) @@ -5362,7 +5318,7 @@ def applyFunc(self,calc=True): ########################################################################################## -class IntegralsWindow(wc.ToolWindows): +class IntegralsWindow(wc.ToolWindow): NAME = "Integrals" CANCELNAME = "&Close" OKNAME = "C&alc" @@ -5370,16 +5326,16 @@ class IntegralsWindow(wc.ToolWindows): def __init__(self, parent): super(IntegralsWindow, self).__init__(parent) - self.pickDim = 1 + self.pickDim = 1 if isinstance(self.father.current, views.CurrentContour): - self.pickDim = 2 - self.grid.addWidget(wc.QLabel("Start point X:"), 0, 0) - self.grid.addWidget(wc.QLabel("End point X:"), 0, 1) + self.pickDim = 2 + self.grid.addWidget(wc.QLabel("Start index X:"), 0, 0) + self.grid.addWidget(wc.QLabel("End index X:"), 0, 1) if self.pickDim == 2: - self.grid.addWidget(wc.QLabel("Start point Y:"), 0, 2) - self.grid.addWidget(wc.QLabel("End point Y:"), 0, 3) + self.grid.addWidget(wc.QLabel("Start index Y:"), 0, 2) + self.grid.addWidget(wc.QLabel("End index Y:"), 0, 3) self.grid.addWidget(wc.QLabel("Integral:"), 0, 4) - self.scaling = 1 + self.scaling = 1 self.num = 0 self.pickType = 0 self.minEntries = [] @@ -5406,14 +5362,14 @@ def picked(self, pos): self.xValues.append(None) self.yValues.append(None) self.intEntries[-1].setMinimumWidth(120) - self.grid.addWidget(self.minEntries[-1],self.num + 1, 0) - self.grid.addWidget(self.maxEntries[-1],self.num + 1, 1) - self.grid.addWidget(self.intEntries[-1],self.num + 1, 4) + self.grid.addWidget(self.minEntries[-1], self.num+1, 0) + self.grid.addWidget(self.maxEntries[-1], self.num+1, 1) + self.grid.addWidget(self.intEntries[-1], self.num+1, 4) if self.pickDim == 2: self.minEntriesY.append(wc.QLineEdit(posY, self.applyFunc)) self.maxEntriesY.append(wc.QLineEdit('', self.applyFunc)) - self.grid.addWidget(self.minEntriesY[-1],self.num + 1, 2) - self.grid.addWidget(self.maxEntriesY[-1],self.num + 1, 3) + self.grid.addWidget(self.minEntriesY[-1], self.num+1, 2) + self.grid.addWidget(self.maxEntriesY[-1], self.num+1, 3) self.pickType = 1 elif self.pickType == 1: self.maxEntries[-1].setText(pos) @@ -5435,10 +5391,10 @@ def preview(self): xMax = [int(x.text()) for x in self.maxEntries] yMin = [int(x.text()) for x in self.minEntriesY] yMax = [int(x.text()) for x in self.maxEntriesY] - self.father.current.integralsPreview(xMin,xMax,yMin,yMax) + self.father.current.integralsPreview(xMin, xMax, yMin, yMax) - def setScaling(self,num): - inp = safeEval(self.intEntries[num].text(), length=self.father.current.len(), type='FI') + def setScaling(self, num): + inp = safeEval(self.intEntries[num].text(), length=self.father.current.len(), Type='FI') int = self.intValues[num] if inp is None: return @@ -5448,25 +5404,24 @@ def setScaling(self,num): def applyFunc(self): dataLength = [self.father.current.shape()[-1] - 1] - Parts = [[self.minEntries],[self.maxEntries]] + Parts = [[self.minEntries], [self.maxEntries]] if self.pickDim == 2: dataLength.append(self.father.current.shape()[-2] - 1) Parts[0].append(self.minEntriesY) Parts[1].append(self.maxEntriesY) - for num in range(len(self.minEntries)): - results = [[],[]] #The min/max results + results = [[], []] #The min/max results ok = [] - for place in range(len(Parts)): + for place, _ in enumerate(Parts): for i, part in enumerate(Parts[place]): - inp = safeEval(part[num].text(), length=dataLength, type='FI') + inp = safeEval(part[num].text(), length=dataLength, Type='FI') if inp is None: part[num].setText('') ok.append(False) else: ok.append(True) tmp = int(round(inp)) - tmp = min(max(tmp,0),dataLength[i]) #makes sure that 0 < value < Length + tmp = min(max(tmp, 0), dataLength[i]) #makes sure that 0 < value < Length results[place].append(tmp) part[num].setText(str(tmp)) if all(ok): @@ -5479,7 +5434,7 @@ def applyFunc(self): ########################################################################################## -class ReorderWindow(wc.ToolWindows): +class ReorderWindow(wc.ToolWindow): NAME = "Reorder" @@ -5504,7 +5459,7 @@ def getPosFromFile(self): filename = filename[0] if filename: # if not cancelled self.father.father.lastLocation = os.path.dirname(filename) # Save used path - if len(filename) == 0: + if not filename: return self.valEntry.setText(repr(np.loadtxt(filename, dtype=int))) @@ -5513,7 +5468,7 @@ def applyFunc(self): if newLength == '': newLength = None else: - newLength = safeEval(self.lengthEntry.text(), length=self.father.current.len(), type='FI') + newLength = safeEval(self.lengthEntry.text(), length=self.father.current.len(), Type='FI') if newLength is None: raise SsnakeException("Reorder: `Length' input is not valid") val = safeEval(self.valEntry.text(), length=int(self.father.current.len())) @@ -5529,7 +5484,7 @@ def applyFunc(self): ########################################################################################## -class RegridWindow(wc.ToolWindows): +class RegridWindow(wc.ToolWindow): NAME = "Regrid" @@ -5583,13 +5538,13 @@ def __init__(self, parent): self.closeEvent() def applyFunc(self): - maxVal = safeEval(self.maxValue.text(), length=self.father.current.len(), type='FI') + maxVal = safeEval(self.maxValue.text(), length=self.father.current.len(), Type='FI') if maxVal is None: raise SsnakeException("Regrid: 'Max' input not valid") - minVal = safeEval(self.minValue.text(), length=self.father.current.len(), type='FI') + minVal = safeEval(self.minValue.text(), length=self.father.current.len(), Type='FI') if minVal is None: raise SsnakeException("Regrid: 'Min' input not valid") - numPoints = safeEval(self.points.text(), length=self.father.current.len(), type='FI') + numPoints = safeEval(self.points.text(), length=self.father.current.len(), Type='FI') if numPoints is None or numPoints == 1: raise SsnakeException("Regrid: '# of points' input not valid") numPoints = int(numPoints) @@ -5608,7 +5563,7 @@ def applyFunc(self): ########################################################################################## -class FFMWindow(wc.ToolWindows): +class FFMWindow(wc.ToolWindow): NAME = "FFM" @@ -5635,7 +5590,7 @@ def getPosFromFile(self): filename = filename[0] if filename: # if not cancelled self.father.father.lastLocation = os.path.dirname(filename) # Save used path - if len(filename) == 0: + if not filename: return self.valEntry.setText(repr(np.loadtxt(filename, dtype=int))) @@ -5651,7 +5606,7 @@ def applyFunc(self): ########################################################################################## -class CLEANWindow(wc.ToolWindows): +class CLEANWindow(wc.ToolWindow): NAME = "CLEAN" @@ -5686,7 +5641,7 @@ def getPosFromFile(self): filename = filename[0] if filename: # if not cancelled self.father.father.lastLocation = os.path.dirname(filename) # Save used path - if len(filename) == 0: + if not filename: return self.valEntry.setText(repr(np.loadtxt(filename, dtype=int))) @@ -5695,14 +5650,14 @@ def applyFunc(self): if not isinstance(val, (list, np.ndarray)): raise SsnakeException("CLEAN: 'Positions' is not a list or array") val = np.array(val, dtype=int) - gamma = safeEval(self.gammaEntry.text(), length=self.father.current.len(), type='FI') + gamma = safeEval(self.gammaEntry.text(), length=self.father.current.len(), Type='FI') if gamma is None: raise SsnakeException("CLEAN: 'Gamma' input is not valid") - threshold = safeEval(self.thresholdEntry.text(), length=self.father.current.len(), type='FI') + threshold = safeEval(self.thresholdEntry.text(), length=self.father.current.len(), Type='FI') if threshold is None: raise SsnakeException("CLEAN: 'Threshold' input is not valid") threshold = threshold - maxIter = safeEval(self.maxIterEntry.text(), length=self.father.current.len(), type='FI') + maxIter = safeEval(self.maxIterEntry.text(), length=self.father.current.len(), Type='FI') if maxIter is None: raise SsnakeException("CLEAN: 'Max. iter.' is not valid") maxIter = int(maxIter) @@ -5713,7 +5668,7 @@ def applyFunc(self): ################################################################ -class ISTWindow(wc.ToolWindows): +class ISTWindow(wc.ToolWindow): NAME = "IST" @@ -5748,7 +5703,7 @@ def getPosFromFile(self): filename = filename[0] if filename: # if not cancelled self.father.father.lastLocation = os.path.dirname(filename) # Save used path - if len(filename) == 0: + if not filename: return self.valEntry.setText(repr(np.loadtxt(filename, dtype=int))) @@ -5757,14 +5712,14 @@ def applyFunc(self): if not isinstance(val, (list, np.ndarray)): raise SsnakeException("IST: 'Positions' input is not a list or array") val = np.array(val, dtype=int) - tracelimit = safeEval(self.tracelimitEntry.text(), length=self.father.current.len(), type='FI') + tracelimit = safeEval(self.tracelimitEntry.text(), length=self.father.current.len(), Type='FI') if tracelimit is None: raise SsnakeException("IST: 'Residual' input is not valid") tracelimit /= 100 - threshold = safeEval(self.thresholdEntry.text(), length=self.father.current.len(), type='FI') + threshold = safeEval(self.thresholdEntry.text(), length=self.father.current.len(), Type='FI') if threshold is None: raise SsnakeException("IST: 'Threshold' input is not valid") - maxIter = safeEval(self.maxIterEntry.text(), length=self.father.current.len(), type='FI') + maxIter = safeEval(self.maxIterEntry.text(), length=self.father.current.len(), Type='FI') if maxIter is None: raise SsnakeException("IST: 'Max. iter.' input is not valid") maxIter = int(maxIter) @@ -5775,7 +5730,7 @@ def applyFunc(self): ################################################################ -class ShearingWindow(wc.ToolWindows): +class ShearingWindow(wc.ToolWindow): NAME = "Shearing" @@ -5809,12 +5764,12 @@ def dropdownChanged(self): self.shearEntry.setText("%.9f" % self.shearList[index]) def shearPreview(self, *args): - shear = safeEval(self.shearEntry.text(), length=self.father.current.len(), type='FI') + shear = safeEval(self.shearEntry.text(), length=self.father.current.len(), Type='FI') if shear is not None: self.shearEntry.setText(str(float(shear))) def applyFunc(self): - shear = safeEval(self.shearEntry.text(), length=self.father.current.len(), type='FI') + shear = safeEval(self.shearEntry.text(), length=self.father.current.len(), Type='FI') if shear is None: raise SsnakeException("Shearing: 'constant' not a valid value") axis = self.dirEntry.currentIndex() @@ -5826,7 +5781,7 @@ def applyFunc(self): ########################################################################################## -class MultiplyWindow(wc.ToolWindows): +class MultiplyWindow(wc.ToolWindow): NAME = "Multiply" SINGLESLICE = True @@ -5851,22 +5806,22 @@ def applyFunc(self): ########################################################################################## -class NormalizeWindow(wc.ToolWindows): +class NormalizeWindow(wc.ToolWindow): NAME = "Normalize" SINGLESLICE = True def __init__(self, parent): super(NormalizeWindow, self).__init__(parent) - self.grid.addWidget(wc.QLabel("Start point:"), 0, 0) + self.grid.addWidget(wc.QLabel("Start index:"), 0, 0) self.minEntry = wc.QLineEdit("0", self.checkValues) self.grid.addWidget(self.minEntry, 1, 0) - self.grid.addWidget(wc.QLabel("End point:"), 2, 0) + self.grid.addWidget(wc.QLabel("End index:"), 2, 0) self.maxEntry = wc.QLineEdit(parent.current.len(), self.checkValues) self.grid.addWidget(self.maxEntry, 3, 0) self.grid.addWidget(wc.QLabel("Type:"), 4, 0) self.typeDrop = QtWidgets.QComboBox() - self.typeDrop.addItems(['Integral','Maximum','Minimum']) + self.typeDrop.addItems(['Integral', 'Maximum', 'Minimum']) self.typeDrop.setCurrentIndex(0) self.typeDrop.currentIndexChanged.connect(self.checkValues) self.grid.addWidget(self.typeDrop, 5, 0) @@ -5889,7 +5844,7 @@ def picked(self, pos, num=0): def checkValues(self, *args): dataLength = self.father.current.len() - inp = safeEval(self.minEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.minEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: return minimum = int(round(inp)) @@ -5898,7 +5853,7 @@ def checkValues(self, *args): elif minimum > dataLength: minimum = dataLength self.minEntry.setText(str(minimum)) - inp = safeEval(self.maxEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.maxEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: return maximum = int(round(inp)) @@ -5911,7 +5866,7 @@ def checkValues(self, *args): def applyFunc(self): dataLength = self.father.current.len() - inp = safeEval(self.minEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.minEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: raise SsnakeException("Normalize: invalid range") minimum = int(round(inp)) @@ -5920,7 +5875,7 @@ def applyFunc(self): elif minimum > dataLength: minimum = dataLength self.minEntry.setText(str(minimum)) - inp = safeEval(self.maxEntry.text(), length=self.father.current.len(), type='FI') + inp = safeEval(self.maxEntry.text(), length=self.father.current.len(), Type='FI') if inp is None: raise SsnakeException("Normalize: invalid range") maximum = int(round(inp)) @@ -5930,7 +5885,7 @@ def applyFunc(self): maximum = dataLength self.maxEntry.setText(str(maximum)) try: - scale = safeEval(self.valEntry.text(), length=self.father.current.len(), type='FI') + scale = safeEval(self.valEntry.text(), length=self.father.current.len(), Type='FI') except Exception: raise SsnakeException("Normalize: invalid multiplier") type = self.typeDrop.currentIndex() @@ -5944,7 +5899,7 @@ def applyFunc(self): ########################################################################################## -class XaxWindow(wc.ToolWindows): +class XaxWindow(wc.ToolWindow): RESIZABLE = True NAME = "User defined x-axis" @@ -5952,7 +5907,7 @@ class XaxWindow(wc.ToolWindows): def __init__(self, parent): super(XaxWindow, self).__init__(parent) self.axisSize = self.father.current.len() - self.grid.addWidget(wc.QLabel("Input x-axis values:"), 0, 0, 1, 2) + self.grid.addWidget(wc.QLabel("Input x-axis values:"), 0, 0, 1, 2) self.typeDropdown = QtWidgets.QComboBox() self.typeDropdown.addItems(['Expression', 'Linear', 'Logarithmic']) self.typeDropdown.activated.connect(self.typeChanged) @@ -6042,14 +5997,16 @@ def getValues(self): env['euro'] = lambda fVal, num=self.axisSize: func.euro(fVal, num) try: val = np.array(eval(self.exprEntry.text(), env),dtype=float) # find a better solution, also add catch for exceptions - except Exception: + except SyntaxError: try: val = np.fromstring(self.exprEntry.text(), sep=' ') val2 = np.fromstring(self.exprEntry.text(), sep=',') if len(val2) > len(val): val = val2 - except Exception: - val = None + except Exception as e: + raise SsnakeException(str(e)) + except Exception as e: + raise SsnakeException(str(e)) if not isinstance(val, (list, np.ndarray)): raise SsnakeException("X-axis: Input is not a list or array") if len(val) != self.father.current.len(): @@ -6057,16 +6014,16 @@ def getValues(self): if not all(isinstance(x, (int, float)) for x in val): raise SsnakeException("X-axis: Array is not all of int or float type") elif self.typeDropdown.currentIndex() == 1: - start = safeEval(self.linStartEntry.text(), type='FI') - stop = safeEval(self.linStopEntry.text(), type='FI') + start = safeEval(self.linStartEntry.text(), Type='FI') + stop = safeEval(self.linStopEntry.text(), Type='FI') if start is None: raise SsnakeException("X-axis: linear start value is not valid") if stop is None: raise SsnakeException("X-axis: linear stop value is not valid") val = np.linspace(start, stop, self.axisSize) elif self.typeDropdown.currentIndex() == 2: - start = safeEval(self.logStartEntry.text(), type='FI') - stop = safeEval(self.logStopEntry.text(), type='FI') + start = safeEval(self.logStartEntry.text(), Type='FI') + stop = safeEval(self.logStopEntry.text(), Type='FI') if start is None or start <= 0.0: raise SsnakeException("X-axis: logarithmic start value is not valid") if stop is None or stop <= 0.0: @@ -6093,7 +6050,7 @@ def applyFunc(self): ########################################################################################## -class RefWindow(wc.ToolWindows): +class RefWindow(wc.ToolWindow): NAME = "Reference" @@ -6131,8 +6088,8 @@ def __init__(self, parent): self.father.current.peakPick = True def preview(self, *args): - freq = safeEval(self.freqEntry.text(), length=self.father.current.len(), type='FI') - ref = safeEval(self.refEntry.text(), length=self.father.current.len(), type='FI') + freq = safeEval(self.freqEntry.text(), length=self.father.current.len(), Type='FI') + ref = safeEval(self.refEntry.text(), length=self.father.current.len(), Type='FI') if freq is None or ref is None: return self.freqEntry.setText("%.7f" % (freq)) @@ -6143,8 +6100,8 @@ def fillSecondaryRef(self): def applyAndClose(self): self.father.current.peakPickReset() - freq = safeEval(self.freqEntry.text(), length=self.father.current.len(), type='FI') - ref = safeEval(self.refEntry.text(), length=self.father.current.len(), type='FI') + freq = safeEval(self.freqEntry.text(), length=self.father.current.len(), Type='FI') + ref = safeEval(self.refEntry.text(), length=self.father.current.len(), Type='FI') if freq is None or ref is None: raise SsnakeException("Not a valid value") freq = freq * 1e6 @@ -6170,7 +6127,7 @@ def picked(self, pos): ########################################################################################## -class HistoryWindow(wc.ToolWindows): +class HistoryWindow(wc.ToolWindow): NAME = "Processing history" RESIZABLE = True @@ -6191,7 +6148,7 @@ def __init__(self, parent): class OrigListWidget(QtWidgets.QListWidget): - def __init__(self, parent=None,dest=None): + def __init__(self, parent=None, dest=None): super(OrigListWidget, self).__init__(parent) self.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop) self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) @@ -6206,7 +6163,7 @@ def dropEvent(self, event): for item in self.dest.selectedItems(): self.dest.takeItem(self.dest.row(item)) - def mouseDoubleClickEvent(self,event): + def mouseDoubleClickEvent(self, event): for item in self.selectedItems(): QtWidgets.QListWidgetItem(item.text(), self.dest) @@ -6238,13 +6195,13 @@ def deleteSelected(self): for item in self.selectedItems(): self.takeItem(self.row(item)) - def mouseDoubleClickEvent(self,event): + def mouseDoubleClickEvent(self, event): self.deleteSelected() ########################################################################################## -class CombineWorkspaceWindow(wc.ToolWindows): +class CombineWorkspaceWindow(wc.ToolWindow): NAME = "Combine workspaces" RESIZABLE = True @@ -6254,7 +6211,7 @@ def __init__(self, parent): self.grid.addWidget(wc.QLabel("Workspaces:"), 0, 0) self.grid.addWidget(wc.QLabel("Combined spectrum:"), 0, 1) self.listB = DestListWidget(self) - self.listA = OrigListWidget(self,self.listB) + self.listA = OrigListWidget(self, self.listB) for i in self.father.workspaceNames: QtWidgets.QListWidgetItem(i, self.listA).setToolTip(i) self.grid.addWidget(self.listA, 1, 0) @@ -6266,7 +6223,7 @@ def applyFunc(self, *args): items = [] for index in range(self.listB.count()): items.append(self.listB.item(index).text()) - if len(items) == 0: + if not items: raise SsnakeException("Please select at least one workspace to combine") self.father.combineWorkspace(items) @@ -6278,7 +6235,7 @@ def closeEvent(self, *args): ########################################################################################## -class CombineLoadWindow(wc.ToolWindows): +class CombineLoadWindow(wc.ToolWindow): NAME = "Open & Combine" BROWSE = True @@ -6310,7 +6267,7 @@ def browse(self): for filePath in fileList: if filePath: # if not cancelled self.father.lastLocation = os.path.dirname(filePath) # Save used path - if len(filePath) == 0: + if not filePath: return self.specList.addItem(filePath) @@ -6318,7 +6275,7 @@ def applyFunc(self, *args): items = [] for index in range(self.specList.count()): items.append(self.specList.item(index).text()) - if len(items) == 0: + if not items: raise SsnakeException("Please select at least one workspace to combine") self.father.loadAndCombine(items) @@ -6349,7 +6306,7 @@ def __init__(self, parent): self.listB = DestListWidget(self) for i in self.father.monitorMacros: QtWidgets.QListWidgetItem(i, self.listB).setToolTip(i) - self.listA = OrigListWidget(self,self.listB) + self.listA = OrigListWidget(self, self.listB) for i in self.father.father.macros.keys(): QtWidgets.QListWidgetItem(i, self.listA).setToolTip(i) grid.addWidget(self.listA, 1, 0) @@ -6369,9 +6326,9 @@ def __init__(self, parent): unwatchButton.clicked.connect(self.stopAndClose) box = QtWidgets.QDialogButtonBox() - box.addButton(cancelButton,QtWidgets.QDialogButtonBox.RejectRole) - box.addButton(watchButton,QtWidgets.QDialogButtonBox.ActionRole) - box.addButton(unwatchButton,QtWidgets.QDialogButtonBox.ActionRole) + box.addButton(cancelButton, QtWidgets.QDialogButtonBox.RejectRole) + box.addButton(watchButton, QtWidgets.QDialogButtonBox.ActionRole) + box.addButton(unwatchButton, QtWidgets.QDialogButtonBox.ActionRole) layout.addWidget(box, 3, 0) layout.setColumnStretch(4, 1) self.show() @@ -6399,7 +6356,7 @@ def closeEvent(self, *args): ############################################################################## -class PlotSettingsWindow(wc.ToolWindows): +class PlotSettingsWindow(wc.ToolWindow): NAME = "Preferences" @@ -6477,7 +6434,7 @@ def preview(self, *args): self.father.current.setLw(self.lwSpinBox.value()) tmpXTicks = self.father.current.viewSettings["minXTicks"] tmpYTicks = self.father.current.viewSettings["minYTicks"] - self.father.current.setTickNum(self.xTicksSpinBox.value(),self.yTicksSpinBox.value()) + self.father.current.setTickNum(self.xTicksSpinBox.value(), self.yTicksSpinBox.value()) tmpColor = self.father.current.viewSettings["color"] self.father.current.setColor(self.color) tmpColorRange = self.father.current.getColorRange() @@ -6492,7 +6449,7 @@ def preview(self, *args): self.father.current.setContourColors([self.posColor, self.negColor]) self.father.current.showFid() self.father.current.setLw(tmpLw) - self.father.current.setTickNum(tmpXTicks,tmpYTicks) + self.father.current.setTickNum(tmpXTicks, tmpYTicks) self.father.current.setColor(tmpColor) self.father.current.setColorRange(tmpColorRange) self.father.current.setColorMap(tmpColorMap) @@ -6521,7 +6478,7 @@ def setNegColor(self, *args): def applyFunc(self, *args): self.father.current.setColor(self.color) self.father.current.setLw(self.lwSpinBox.value()) - self.father.current.setTickNum(self.xTicksSpinBox.value(),self.yTicksSpinBox.value()) + self.father.current.setTickNum(self.xTicksSpinBox.value(), self.yTicksSpinBox.value()) self.father.current.setGrids([self.xgridCheck.isChecked(), self.ygridCheck.isChecked()]) self.father.current.setColorRange(self.crEntry.currentIndex()) self.father.current.setColorMap(self.cmEntry.currentIndex()) @@ -6531,7 +6488,7 @@ def applyFunc(self, *args): ############################################################################## -class errorWindow(wc.ToolWindows): +class errorWindow(wc.ToolWindow): NAME = "Error Messages" RESIZABLE = True @@ -6619,6 +6576,9 @@ def __init__(self, parent): self.toolbarCheck = QtWidgets.QCheckBox("Show Shortcut Toolbar") self.toolbarCheck.setChecked(self.father.defaultToolBar) grid1.addWidget(self.toolbarCheck, 5, 0, 1, 2) + self.tooltipCheck = QtWidgets.QCheckBox("Show Tooltips") + self.tooltipCheck.setChecked(self.father.defaultTooltips) + grid1.addWidget(self.tooltipCheck, 7, 0, 1, 2) editToolbarButton = QtWidgets.QPushButton("Edit Toolbar") editToolbarButton.clicked.connect(lambda: ToolbarWindow(self)) grid1.addWidget(editToolbarButton, 6, 0, 1, 2) @@ -6626,7 +6586,7 @@ def __init__(self, parent): self.startupgroupbox = QtWidgets.QGroupBox("Startup Directory") self.startupgroupbox.setCheckable(True) self.startupgroupbox.setChecked(self.father.defaultStartupBool) - grid1.addWidget(self.startupgroupbox, 7, 0, 1, 2) + grid1.addWidget(self.startupgroupbox, 8, 0, 1, 2) startupgrid = QtWidgets.QGridLayout() self.startupgroupbox.setLayout(startupgrid) self.startupDirEntry = QtWidgets.QLineEdit(self) @@ -6726,9 +6686,9 @@ def __init__(self, parent): resetButton = QtWidgets.QPushButton("&Reset") resetButton.clicked.connect(self.reset) box = QtWidgets.QDialogButtonBox() - box.addButton(cancelButton,QtWidgets.QDialogButtonBox.RejectRole) - box.addButton(okButton,QtWidgets.QDialogButtonBox.ActionRole) - box.addButton(resetButton,QtWidgets.QDialogButtonBox.ActionRole) + box.addButton(cancelButton, QtWidgets.QDialogButtonBox.RejectRole) + box.addButton(okButton, QtWidgets.QDialogButtonBox.ActionRole) + box.addButton(resetButton, QtWidgets.QDialogButtonBox.ActionRole) layout.addWidget(box, 1,0) layout.setColumnStretch(3, 1) self.show() @@ -6737,7 +6697,7 @@ def browseStartup(self, *args): newDir = QtWidgets.QFileDialog.getExistingDirectory(self, 'Select Directory', self.father.lastLocation, QtWidgets.QFileDialog.ShowDirsOnly) if newDir: self.startupDirEntry.setText(newDir) - + def setColor(self, *args): tmp = QtWidgets.QColorDialog.getColor(QtGui.QColor(self.color)) if tmp.isValid(): @@ -6761,6 +6721,7 @@ def applyAndClose(self, *args): self.father.defaultMaximized = self.maximizedCheck.isChecked() self.father.defaultAskName = self.askNameCheck.isChecked() self.father.defaultToolBar = self.toolbarCheck.isChecked() + self.father.defaultTooltips = self.tooltipCheck.isChecked() self.father.defaultToolbarActionList = self.currentToolbar self.father.defaultStartupBool = self.startupgroupbox.isChecked() self.father.defaultStartupDir = self.startupDirEntry.text() @@ -6794,7 +6755,7 @@ def closeEvent(self, *args): ############################################################################## -class ToolbarWindow(wc.ToolWindows): +class ToolbarWindow(wc.ToolWindow): NAME = "Change Toolbar" RESIZABLE = True @@ -6807,7 +6768,7 @@ def __init__(self, parent): self.listB = DestListWidget(self) for i in self.father.father.defaultToolbarActionList: QtWidgets.QListWidgetItem(i, self.listB).setToolTip(i) - self.listA = OrigListWidget(self,self.listB) + self.listA = OrigListWidget(self, self.listB) for i in self.father.father.allActionsList: QtWidgets.QListWidgetItem(i[0], self.listA).setToolTip(i[0]) self.grid.addWidget(self.listA, 1, 0) @@ -6827,7 +6788,7 @@ def closeEvent(self, *args): ############################################################################## -class aboutWindow(wc.ToolWindows): +class aboutWindow(wc.ToolWindow): NAME = "About ssNake" RESIZABLE = True @@ -6852,7 +6813,7 @@ def __init__(self, parent): pythonVersion = pythonVersion[:pythonVersion.index(' ')] from scipy import __version__ as scipyVersion self.text.setText('

ssNake ' + VERSION + '

' + - '

Copyright (©) 2016–2019 Bas van Meerten & Wouter Franssen<\p>' + '

Email: ssnake@science.ru.nl

' + + '

Copyright (©) 2016–2019 Bas van Meerten & Wouter Franssen

' + '

Email: ssnake@science.ru.nl

' + '

Publication: https://doi.org/10.1016/j.jmr.2019.02.006

' + 'Library versions:
Python ' + pythonVersion + '
numpy ' + np.__version__ + '
SciPy ' + scipyVersion + @@ -6875,7 +6836,7 @@ def closeEvent(self, *args): ############################################################################## -class shiftConversionWindow(wc.ToolWindows): +class shiftConversionWindow(wc.ToolWindow): NAME = "Chemical Shift Conversions" MENUDISABLE = False @@ -6989,33 +6950,33 @@ def __init__(self, parent): def shiftCalc(self, Type): if Type == 0: # If from standard try: - delta11 = float(safeEval(self.D11.text(), type='FI')) - delta22 = float(safeEval(self.D22.text(), type='FI')) - delta33 = float(safeEval(self.D33.text(), type='FI')) + delta11 = float(safeEval(self.D11.text(), Type='FI')) + delta22 = float(safeEval(self.D22.text(), Type='FI')) + delta33 = float(safeEval(self.D33.text(), Type='FI')) Values = [delta11, delta22, delta33] except Exception: raise SsnakeException("Shift Conversion: Invalid input in Standard Convention") if Type == 1: # If from xyz try: - delta11 = float(safeEval(self.dxx.text(), type='FI')) # Treat xyz as 123, as it reorders them anyway - delta22 = float(safeEval(self.dyy.text(), type='FI')) - delta33 = float(safeEval(self.dzz.text(), type='FI')) + delta11 = float(safeEval(self.dxx.text(), Type='FI')) # Treat xyz as 123, as it reorders them anyway + delta22 = float(safeEval(self.dyy.text(), Type='FI')) + delta33 = float(safeEval(self.dzz.text(), Type='FI')) Values = [delta11, delta22, delta33] except Exception: raise SsnakeException("Shift Conversion: Invalid input in xyz Convention") if Type == 2: # From haeberlen try: - eta = float(safeEval(self.eta.text(), type='FI')) - delta = float(safeEval(self.daniso.text(), type='FI')) - iso = float(safeEval(self.diso.text(), type='FI')) + eta = float(safeEval(self.eta.text(), Type='FI')) + delta = float(safeEval(self.daniso.text(), Type='FI')) + iso = float(safeEval(self.diso.text(), Type='FI')) Values = [iso, delta, eta] except Exception: raise SsnakeException("Shift Conversion: Invalid input in Haeberlen Convention") if Type == 3: # From Hertzfeld-Berger try: - iso = float(safeEval(self.hbdiso.text(), type='FI')) - span = float(safeEval(self.hbdaniso.text(), type='FI')) - skew = float(safeEval(self.hbskew.text(), type='FI')) + iso = float(safeEval(self.hbdiso.text(), Type='FI')) + span = float(safeEval(self.hbdaniso.text(), Type='FI')) + skew = float(safeEval(self.hbskew.text(), Type='FI')) Values = [iso, span, skew] except Exception: raise SsnakeException("Shift Conversion: Invalid input in Hertzfeld-Berger Convention") @@ -7063,7 +7024,7 @@ def closeEvent(self): ############################################################################## -class quadConversionWindow(wc.ToolWindows): +class quadConversionWindow(wc.ToolWindow): Ioptions = ['1', '3/2', '2', '5/2', '3', '7/2', '4', '9/2', '5', '6', '7'] Ivalues = [1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 6.0, 7.0] @@ -7077,16 +7038,14 @@ def __init__(self, parent): self.comFrame = QtWidgets.QGridLayout() nucLabel = wc.QLabel("Nucleus:") self.comFrame.addWidget(nucLabel, 0, 0) - - qindex = [x for (x,val) in enumerate(ISOTOPES['q']) if val is not None and ISOTOPES['spin'][x] != 0.5] - self.names = ['User'] + [val for (x,val) in enumerate(ISOTOPES['formatName']) if x in qindex] - self.I = [0.0] + [val for (x,val) in enumerate(ISOTOPES['spin']) if x in qindex] - self.Qvalues = [0.0] + [val for (x,val) in enumerate(ISOTOPES['q']) if x in qindex] + qindex = [x for (x, val) in enumerate(ISOTOPES['q']) if val is not None and ISOTOPES['spin'][x] != 0.5] + self.names = ['User'] + [val for (x, val) in enumerate(ISOTOPES['formatName']) if x in qindex] + self.I = [0.0] + [val for (x, val) in enumerate(ISOTOPES['spin']) if x in qindex] + self.Qvalues = [0.0] + [val for (x, val) in enumerate(ISOTOPES['q']) if x in qindex] self.nucDrop = QtWidgets.QComboBox() self.nucDrop.addItems(self.names) self.nucDrop.currentIndexChanged.connect(self.setNuc) self.comFrame.addWidget(self.nucDrop, 1, 0) - Itext = wc.QLabel("I:") self.comFrame.addWidget(Itext, 0, 1) self.IEntry = QtWidgets.QComboBox() @@ -7161,13 +7120,13 @@ def __init__(self, parent): self.okButton.clicked.disconnect() self.okButton.clicked.connect(self.valueReset) - def setNuc(self,index): + def setNuc(self, index): I = self.I[index] if int(I * 2) > 0: self.IEntry.setCurrentIndex(int(I * 2 - 2)) self.Moment.setText(str(self.Qvalues[index])) - def userChange(self,index = None): + def userChange(self, index=None): self.nucDrop.setCurrentIndex(0) def quadCalc(self, Type): @@ -7175,42 +7134,42 @@ def quadCalc(self, Type): if Type == 0: # Cq as input # Czz is equal to Cq, via same definition (scale) Cxx and Cyy can be found try: - Cq = float(safeEval(self.Cq.text(), type='FI')) - Eta = float(safeEval(self.Eta.text(), type='FI')) - Values = [ Cq, Eta] + Cq = float(safeEval(self.Cq.text(), Type='FI')) + Eta = float(safeEval(self.Eta.text(), Type='FI')) + Values = [Cq, Eta] except Exception: raise SsnakeException("Quad Conversion: Invalid input in Cq definition") if Type == 1: try: - Wq = float(safeEval(self.Wq.text(), type='FI')) - Eta = float(safeEval(self.Eta.text(), type='FI')) - Values = [ Wq, Eta] + Wq = float(safeEval(self.Wq.text(), Type='FI')) + Eta = float(safeEval(self.Eta.text(), Type='FI')) + Values = [Wq, Eta] except Exception: raise SsnakeException("Quad Conversion: Invalid input in Wq definition") if Type == 2: try: - Vxx = float(safeEval(self.Vxx.text(), type='FI')) - Vyy = float(safeEval(self.Vyy.text(), type='FI')) - Vzz = float(safeEval(self.Vzz.text(), type='FI')) - Values = [ Vxx, Vyy, Vzz] + Vxx = float(safeEval(self.Vxx.text(), Type='FI')) + Vyy = float(safeEval(self.Vyy.text(), Type='FI')) + Vzz = float(safeEval(self.Vzz.text(), Type='FI')) + Values = [Vxx, Vyy, Vzz] except Exception: raise SsnakeException("Quad Conversion: Invalid input in field gradients") try: - Q = float(safeEval(self.Moment.text(), type='FI')) * 1e-30 # get moment and convert from fm^2 + Q = float(safeEval(self.Moment.text(), Type='FI')) * 1e-30 # get moment and convert from fm^2 except Exception: - if Type == 0 or Type == 1: + if Type in (0, 1): Q = None else: raise SsnakeException("Quad Conversion: Invalid input in quadrupole moment Q") #Do conversion - Result = func.quadConversion(Values,I, Type, Q) - if Result[0][1] == None: + Result = func.quadConversion(Values, I, Type, Q) + if Result[0][1] is None: self.Eta.setText('ND') else: self.Eta.setText('%#.4g' % Result[0][1]) self.Cq.setText('%#.4g' % Result[0][0]) self.Wq.setText('%#.4g' % Result[1][0]) - if Result[2][0] == None: + if Result[2][0] is None: self.Moment.setText('ND') self.Vxx.setText('ND') self.Vyy.setText('ND') @@ -7234,7 +7193,7 @@ def closeEvent(self): ############################################################################## -class dipolarDistanceWindow(wc.ToolWindows): +class dipolarDistanceWindow(wc.ToolWindow): NAME = "Dipolar Distance Calculation" RESIZABLE = True @@ -7247,10 +7206,9 @@ def __init__(self, parent): gamma1label = wc.QLabel(u'γ1 [107 rad/s/T]:') self.comFrame.addWidget(gamma1label, 0, 0) - gammaindex = [x for (x,val) in enumerate(ISOTOPES['gamma']) if val is not None] - self.gammaValues = [0.0] + [val for (x,val) in enumerate(ISOTOPES['gamma']) if x in gammaindex] - self.names = ['User'] + [val for (x,val) in enumerate(ISOTOPES['formatName']) if x in gammaindex] - + gammaindex = [x for (x, val) in enumerate(ISOTOPES['gamma']) if val is not None] + self.gammaValues = [0.0] + [val for (x, val) in enumerate(ISOTOPES['gamma']) if x in gammaindex] + self.names = ['User'] + [val for (x, val) in enumerate(ISOTOPES['formatName']) if x in gammaindex] self.gamma1Drop = QtWidgets.QComboBox() self.gamma1Drop.addItems(self.names) self.gamma1Drop.currentIndexChanged.connect(self.setGamma1) @@ -7259,7 +7217,6 @@ def __init__(self, parent): self.gamma2Drop.addItems(self.names) self.gamma2Drop.currentIndexChanged.connect(self.setGamma2) self.comFrame.addWidget(self.gamma2Drop, 1, 1) - self.gamma1 = wc.QLineEdit("0.0") self.gamma1.setMinimumWidth(100) self.gamma1.textEdited.connect(self.gamma1Changed) @@ -7272,7 +7229,6 @@ def __init__(self, parent): self.comFrame.addWidget(self.gamma2, 2, 1) self.comGroup.setLayout(self.comFrame) self.grid.addWidget(self.comGroup, 1, 0, 1, 3) - self.distanceGroup = QtWidgets.QGroupBox("Distance:") self.distanceFrame = QtWidgets.QGridLayout() distancelabel = wc.QLabel(u'r [Å]') @@ -7313,32 +7269,30 @@ def gamma1Changed(self): def gamma2Changed(self): self.gamma2Drop.setCurrentIndex(0) - def setGamma1(self,index): - if index !=0: + def setGamma1(self, index): + if index != 0: self.gamma1.setText(str(self.gammaValues[index])) - def setGamma2(self,index): - if index !=0: + def setGamma2(self, index): + if index != 0: self.gamma2.setText(str(self.gammaValues[index])) def Calc(self, Type): try: - gamma1 = float(safeEval(self.gamma1.text(), type='FI')) * 1e7 - gamma2 = float(safeEval(self.gamma2.text(), type='FI')) * 1e7 + gamma1 = float(safeEval(self.gamma1.text(), Type='FI')) * 1e7 + gamma2 = float(safeEval(self.gamma2.text(), Type='FI')) * 1e7 except Exception: raise SsnakeException("Dipolar Distance: Invalid input in gamma values") - if Type == 0: # Distance as input try: - r = abs(float(safeEval(self.distance.text(), type='FI'))) + r = abs(float(safeEval(self.distance.text(), Type='FI'))) except Exception: raise SsnakeException("Dipolar Distance: Invalid input in r") if Type == 1: try: - D = abs(float(safeEval(self.dipolar.text(), type='FI'))) + D = abs(float(safeEval(self.dipolar.text(), Type='FI'))) except Exception: raise SsnakeException("Dipolar Distance: Invalid input in D") - hbar = 1.054573e-34 if Type == 0: if r == 0.0: @@ -7352,7 +7306,6 @@ def Calc(self, Type): else: r = 1 / abs(D * 1000 /gamma1 / gamma2 / hbar / 1e-7 * (2 * np.pi))**(1.0/3) r *= 1e10 - self.dipolar.setText('%#.5g' % D) self.distance.setText('%#.5g' % r) @@ -7381,7 +7334,6 @@ class tempCalWindow(QtWidgets.QWidget): lambda Delta: 466.5 - 102.00 * Delta, lambda Temp: (Temp - 466.5) / -102.0, 'absShift',None,None,'Ammann et al., JMR, 46, 319 (1982)'] - PBNO3 = [143, 423, lambda Delta, Delta0, T0: (Delta0 - Delta) / 0.753 + T0 , lambda T, Delta0, T0: (T0 - T) * 0.753 + Delta0 , @@ -7390,12 +7342,9 @@ class tempCalWindow(QtWidgets.QWidget): lambda Delta, Delta0, T0: (Delta0 - Delta) / 0.0250 + T0 , lambda T, Delta0, T0: (T0 - T) * 0.0250 + Delta0 , 'relShift','0','293','Thurber et al., JMR, 196, 84 (2009)'] - - DEFINITIONS = [METHANOL, ETH_GLYCOL, PBNO3 ,KBR] TEXTLIST = ['1H: Methanol (178 K < T < 330 K)', '1H: Ethylene Glycol (273 K < T < 416 K)', '207Pb: Lead Nitrate (143 K < T < 423 K)','79Br: KBr (170 K < T < 320 K)'] - T1_KBr = [20,296, lambda Relax: optimize.brentq(lambda T,T1: 0.0145 + 5330/T**2 + 1.42e7/T**4 + 2.48e9/T**6 - T1, 20, 296 ,args=(Relax,)), lambda T: 0.0145 + 5330/T**2 + 1.42e7/T**4 + 2.48e9/T**6, @@ -7404,10 +7353,8 @@ class tempCalWindow(QtWidgets.QWidget): lambda Relax: optimize.brentq(lambda T,T1: -1.6e-3 + 1.52e3/T**2 + 0.387e6/T**4 + 0.121e9/T**6 - T1, 8, 104 ,args=(Relax,)), lambda T: -1.6e-3 + 1.52e3/T**2 + 0.387e6/T**4 + 0.121e9/T**6, 'Sarkar et al., JMR, 212, 460 (2011)'] - T1_DEFINITIONS = [T1_KBr, T1_CsI] - T1_TEXTLIST = ['79Br: KBr (20 K < T < 296 K, 9.4 T)','127I: CsI (8 K < T < 104 K, 9.4 T)'] - + T1_TEXTLIST = ['79Br: KBr (20 K < T < 296 K, 9.4 T)', '127I: CsI (8 K < T < 104 K, 9.4 T)'] def __init__(self, parent): super(tempCalWindow, self).__init__(parent) @@ -7419,13 +7366,11 @@ def __init__(self, parent): tabWidget.addTab(tab1, "Chemical Shift Based") grid1 = QtWidgets.QGridLayout() tab1.setLayout(grid1) - - #Shift based + # Shift based self.typeDrop = QtWidgets.QComboBox() self.typeDrop.addItems(self.TEXTLIST) grid1.addWidget(self.typeDrop, 0, 0) self.typeDrop.currentIndexChanged.connect(self.changeType) - self.RefGroup = QtWidgets.QGroupBox("Relative to:") self.RefFrame = QtWidgets.QGridLayout() self.Delta0Label = wc.QLabel(u'δ [ppm]') @@ -7441,7 +7386,6 @@ def __init__(self, parent): self.RefGroup.setLayout(self.RefFrame) grid1.addWidget(self.RefGroup, 1, 0) self.RefGroup.hide() - self.DeltaGroup = QtWidgets.QGroupBox("Shift to Temperature:") self.DeltaFrame = QtWidgets.QGridLayout() self.DeltaLabel = wc.QLabel(u'Δδ [ppm]') @@ -7454,7 +7398,6 @@ def __init__(self, parent): self.DeltaFrame.addWidget(self.Delta, 1, 1) self.DeltaGroup.setLayout(self.DeltaFrame) grid1.addWidget(self.DeltaGroup, 2, 0) - self.TempGroup = QtWidgets.QGroupBox("Temperature to Shift:") self.TempFrame = QtWidgets.QGridLayout() TempLabel = wc.QLabel(u'Temperature [K]') @@ -7468,19 +7411,16 @@ def __init__(self, parent): self.TempGroup.setLayout(self.TempFrame) grid1.addWidget(self.TempGroup, 3, 0) self.refname = wc.QLabel(self.DEFINITIONS[0][7]) - grid1.addWidget(self.refname,4,0) - - #T1 based + grid1.addWidget(self.refname, 4, 0) + # T1 based tab2 = QtWidgets.QWidget() tabWidget.addTab(tab2, "T1 Based") grid2 = QtWidgets.QGridLayout() tab2.setLayout(grid2) - self.T1typeDrop = QtWidgets.QComboBox() self.T1typeDrop.addItems(self.T1_TEXTLIST) grid2.addWidget(self.T1typeDrop, 0, 0) self.T1typeDrop.currentIndexChanged.connect(self.changeTypeT1) - self.T1Group = QtWidgets.QGroupBox("T1 to Temperature:") self.T1Frame = QtWidgets.QGridLayout() self.T1Label = wc.QLabel(u'T1 [s]') @@ -7493,7 +7433,6 @@ def __init__(self, parent): self.T1Frame.addWidget(self.T1, 1, 1) self.T1Group.setLayout(self.T1Frame) grid2.addWidget(self.T1Group, 2, 0) - self.TempT1Group = QtWidgets.QGroupBox("Temperature to T1:") self.TempT1Frame = QtWidgets.QGridLayout() TempT1Label = wc.QLabel(u'Temperature [K]') @@ -7507,23 +7446,22 @@ def __init__(self, parent): self.TempT1Group.setLayout(self.TempT1Frame) grid2.addWidget(self.TempT1Group, 3, 0) self.refnameT1 = wc.QLabel(self.T1_DEFINITIONS[0][4]) - grid2.addWidget(self.refnameT1,4,0) - + grid2.addWidget(self.refnameT1, 4, 0) layout = QtWidgets.QGridLayout(self) layout.addWidget(tabWidget, 0, 0, 1, 4) cancelButton = QtWidgets.QPushButton("&Close") cancelButton.clicked.connect(self.closeEvent) box = QtWidgets.QDialogButtonBox() - box.addButton(cancelButton,QtWidgetsQ.DialogButtonBox.RejectRole) - layout.addWidget(box, 1,0,1,4) + box.addButton(cancelButton, QtWidgets.QDialogButtonBox.RejectRole) + layout.addWidget(box, 1, 0, 1, 4) layout.setColumnStretch(3, 1) +# layout.setRowStretch(3, 1) self.show() self.setFixedSize(self.size()) - def changeType(self,index): + def changeType(self, index): self.Temp.setText('') self.Delta.setText('') - self.refname.setText(self.DEFINITIONS[index][7]) if self.DEFINITIONS[self.typeDrop.currentIndex()][4] == 'absShift': self.DeltaLabel.setText(u'Δδ [ppm]') @@ -7535,15 +7473,18 @@ def changeType(self,index): self.RefGroup.show() self.Delta0.setText(self.DEFINITIONS[self.typeDrop.currentIndex()][5]) self.T0.setText(self.DEFINITIONS[self.typeDrop.currentIndex()][6]) + self.father.root.processEvents() + self.setFixedSize(self.sizeHint()) - def changeTypeT1(self,index): + def changeTypeT1(self, index): self.refnameT1.setText(self.T1_DEFINITIONS[index][4]) - + self.father.root.processEvents() + self.setFixedSize(self.sizeHint()) def tempToT1(self): Data = self.T1_DEFINITIONS[self.T1typeDrop.currentIndex()] try: - Temp = float(safeEval(self.TempT1.text(), type='FI')) + Temp = float(safeEval(self.TempT1.text(), Type='FI')) except Exception: self.T1.setText('?') raise SsnakeException("Temperature Calibration: Invalid input in Temp value") @@ -7551,17 +7492,15 @@ def tempToT1(self): if Temp < Data[0] or Temp > Data[1]: self.T1.setText('?') raise SsnakeException("Temperature Calibration: Temperature outside calibration range") - else: - self.T1.setText('%#.6g' % T1) + self.T1.setText('%#.6g' % T1) def t1ToTemp(self): Data = self.T1_DEFINITIONS[self.T1typeDrop.currentIndex()] try: - T1 = float(safeEval(self.T1.text(), type='FI')) + T1 = float(safeEval(self.T1.text(), Type='FI')) except Exception: self.TempT1.setText('?') raise SsnakeException("Temperature Calibration: Invalid input in Temp value") - try: Temp = Data[2](T1) except Exception: @@ -7572,62 +7511,57 @@ def t1ToTemp(self): def shiftToTemp(self): Data = self.DEFINITIONS[self.typeDrop.currentIndex()] try: - Delta = float(safeEval(self.Delta.text(), type='FI')) + Delta = float(safeEval(self.Delta.text(), Type='FI')) except Exception: self.Temp.setText('?') raise SsnakeException("Temperature Calibration: Invalid input in Delta value") if Data[4] == 'relShift': try: - Delta0 = float(safeEval(self.Delta0.text(), type='FI')) - T0 = float(safeEval(self.T0.text(), type='FI')) + Delta0 = float(safeEval(self.Delta0.text(), Type='FI')) + T0 = float(safeEval(self.T0.text(), Type='FI')) except Exception: self.Temp.setText('?') raise SsnakeException("Temperature Calibration: Invalid input in References values") - Temp = Data[2](Delta,Delta0,T0) + Temp = Data[2](Delta, Delta0, T0) else: Temp = Data[2](Delta) - if Temp < Data[0] or Temp > Data[1]: self.Temp.setText('?') raise SsnakeException("Temperature Calibration: Temperature outside calibration range") - else: - self.Temp.setText('%#.6g' % Temp) + self.Temp.setText('%#.6g' % Temp) def tempToShift(self): Data = self.DEFINITIONS[self.typeDrop.currentIndex()] try: - Temp = float(safeEval(self.Temp.text(), type='FI')) + Temp = float(safeEval(self.Temp.text(), Type='FI')) except Exception: self.Delta.setText('?') raise SsnakeException("Temperature Calibration: Invalid input in Temp value") if Data[4] == 'relShift': try: - Delta0 = float(safeEval(self.Delta0.text(), type='FI')) - T0 = float(safeEval(self.T0.text(), type='FI')) + Delta0 = float(safeEval(self.Delta0.text(), Type='FI')) + T0 = float(safeEval(self.T0.text(), Type='FI')) except Exception: self.Delta.setText('?') raise SsnakeException("Temperature Calibration: Invalid input in References values") - Delta = Data[3](Temp,Delta0,T0) + Delta = Data[3](Temp, Delta0, T0) else: Delta = Data[3](Temp) if Temp < Data[0] or Temp > Data[1]: self.Delta.setText('?') raise SsnakeException("Temperature Calibration: Temperature outside calibration range") - else: - self.Delta.setText('%#.6g' % Delta) - - + self.Delta.setText('%#.6g' % Delta) def closeEvent(self, *args): self.deleteLater() ############################################################################## -class mqmasExtractWindow(wc.ToolWindows): +class mqmasExtractWindow(wc.ToolWindow): Ioptions = ['3/2','5/2', '7/2', '9/2'] Ivalues = [1.5, 2.5, 3.5, 4.5] - z = [680.0/27.0,8500.0/81.0,6664.0/27.0,1360.0/3.0] - BdevA = [1.0/68.0,3.0/850.0,5.0/3332.0,1.0/1224.0] + z = [680.0/27.0, 8500.0/81.0, 6664.0/27.0, 1360.0/3.0] + BdevA = [1.0/68.0, 3.0/850.0, 5.0/3332.0, 1.0/1224.0] NAME = "MQMAS" RESIZABLE = True MENUDISABLE = False @@ -7657,7 +7591,7 @@ def __init__(self, parent): self.delta1.setMinimumWidth(200) self.calcIsoPqButton = QtWidgets.QPushButton("Calc δiso/PQ", self) self.calcIsoPqButton.clicked.connect(self.calcIsoPq) - self.onetwoFrame.addWidget(self.calcIsoPqButton, 4, 0,1,2) + self.onetwoFrame.addWidget(self.calcIsoPqButton, 4, 0, 1, 2) self.onetwoGroup.setLayout(self.onetwoFrame) self.grid.addWidget(self.onetwoGroup, 2, 0, 4, 2) self.isopqGroup = QtWidgets.QGroupBox("δiso/PQ:") @@ -7670,7 +7604,7 @@ def __init__(self, parent): self.isopqFrame.addWidget(self.pq, 7, 1) self.calc12Button = QtWidgets.QPushButton("Calc δ1/δ2", self) self.calc12Button.clicked.connect(self.calc12) - self.isopqFrame.addWidget(self.calc12Button, 8, 0,1,2) + self.isopqFrame.addWidget(self.calc12Button, 8, 0, 1, 2) self.isopqGroup.setLayout(self.isopqFrame) self.grid.addWidget(self.isopqGroup, 6, 0, 4, 2) self.cancelButton.setText("Close") @@ -7681,16 +7615,16 @@ def __init__(self, parent): self.okButton.clicked.connect(self.valueReset) def calcIsoPq(self): - nu0 = safeEval(self.nu0.text(), type='FI') + nu0 = safeEval(self.nu0.text(), Type='FI') wrong = False if nu0 is None: self.father.dispMsg("MQMAS Extract: Invalid input in V0") wrong = True - delta1 = safeEval(self.delta1.text(), type='FI') + delta1 = safeEval(self.delta1.text(), Type='FI') if delta1 is None and wrong is False: self.father.dispMsg("MQMAS Extract: Invalid input in Delta1") wrong = True - delta2 = safeEval(self.delta2.text(), type='FI') + delta2 = safeEval(self.delta2.text(), Type='FI') if delta2 is None and wrong is False: self.father.dispMsg("MQMAS Extract: Invalid input in Delta2") wrong = True @@ -7709,16 +7643,16 @@ def calcIsoPq(self): self.pq.setText(str(pq)) def calc12(self): - nu0 = safeEval(self.nu0.text(), type='FI') + nu0 = safeEval(self.nu0.text(), Type='FI') wrong = False if nu0 is None or nu0 == 0.0: self.father.dispMsg("MQMAS Extract: Invalid input in V0") wrong = True - iso = safeEval(self.deltaIso.text(), type='FI') + iso = safeEval(self.deltaIso.text(), Type='FI') if iso is None and wrong is False: self.father.dispMsg("MQMAS Extract: Invalid input in DeltaIso") wrong = True - pq = safeEval(self.pq.text(), type='FI') + pq = safeEval(self.pq.text(), Type='FI') if pq is None and wrong is False: self.father.dispMsg("MQMAS Extract: Invalid input in PQ") wrong = True @@ -7729,7 +7663,7 @@ def calc12(self): BdevA = self.BdevA[self.IEntry.currentIndex()] delta1 = iso + BdevA * pq**2/nu0**2 * 1e6 self.delta1.setText(str(delta1)) - delta2 = (27 * iso - 17 * delta1) / 10 + delta2 = (27 * iso - 17 * delta1) / 10 self.delta2.setText(str(delta2)) def valueReset(self): # Resets all the boxes to 0 @@ -7772,9 +7706,9 @@ def checkVersions(): If the values are to low, an error message is returned. """ from scipy import __version__ as scipyVersion # Scipy is not fully imported, so only load version - libs = [['numpy',np.__version__,NPVERSION], - ['matplotlib',matplotlib.__version__,MPLVERSION], - ['scipy',scipyVersion,SPVERSION]] + libs = [['numpy', np.__version__, NPVERSION], + ['matplotlib', matplotlib.__version__, MPLVERSION], + ['scipy', scipyVersion, SPVERSION]] if sys.version_info.major == 3: libs.append(['python', str(sys.version_info.major) + '.' + str(sys.version_info.minor), PY3VERSION]) elif sys.version_info.major == 2: @@ -7796,12 +7730,30 @@ def popupVersionError(messages): msg = "" for elem in messages: msg = msg + elem + '\n' - reply = QtWidgets.QMessageBox.warning(QtWidgets.QWidget(),'Invalid software version', msg, QtWidgets.QMessageBox.Ignore, QtWidgets.QMessageBox.Abort) + reply = QtWidgets.QMessageBox.warning(QtWidgets.QWidget(), 'Invalid software version', msg, QtWidgets.QMessageBox.Ignore, QtWidgets.QMessageBox.Abort) quit = False if reply == QtWidgets.QMessageBox.Abort: quit = True return quit +def openRefMan(): + file = os.path.dirname(os.path.realpath(__file__)) + os.path.sep + '..' + os.path.sep + 'ReferenceManual.pdf' + if sys.platform.startswith('linux'): + os.system("xdg-open " + '"' + file + '"') + elif sys.platform.startswith('darwin'): + os.system("open " + '"' + file + '"') + elif sys.platform.startswith('win'): + os.startfile(file) + +def openTutorial(): + path = os.path.dirname(os.path.realpath(__file__)) + os.path.sep + '..' + os.path.sep + '/Tutorial' + if sys.platform.startswith('linux'): + os.system("xdg-open " + '"' + path + '"') + elif sys.platform.startswith('darwin'): + os.system("open " + '"' + path + '"') + elif sys.platform.startswith('win'): + os.startfile(path) + if __name__ == '__main__': error, messages = checkVersions() quit = False @@ -7820,10 +7772,9 @@ def exception_hook(exctype, value, traceback): if not isinstance(value, Exception): # Do not catch keyboard interrupts sys._excepthook(exctype, value, traceback) elif isinstance(value, (sc.SpectrumException, hc.HComplexException, sim.SimException)): - mainProgram.dispMsg(str(value)) + mainProgram.dispMsg(str(value)) else: mainProgram.dispError([exctype, value, traceback]) sys.excepthook = exception_hook sys.exit(root.exec_()) - diff --git a/src/updateWindow.py b/src/updateWindow.py index bb746c58..7ed6b38c 100644 --- a/src/updateWindow.py +++ b/src/updateWindow.py @@ -28,11 +28,23 @@ else: from urllib import urlopen, urlretrieve import ssNake as sc -from ssNake import QtGui, QtCore, QtWidgets +from ssNake import QtCore, QtWidgets + class UpdateWindow(QtWidgets.QWidget): + """ + The window for updating ssNake. + """ def __init__(self, parent): + """ + Initializes the update window. + + Parameters + ---------- + parent : MainProgram + The mainprogram object of ssNake. + """ super(UpdateWindow, self).__init__(parent) self.setWindowFlags(QtCore.Qt.Window | QtCore.Qt.Tool) self.father = parent @@ -45,7 +57,7 @@ def __init__(self, parent): req.close() self.nameList = [u'develop'] self.urlList = [u'https://api.github.com/repos/smeerten/ssnake/zipball/develop'] - for i in range(len(info)): + for i, _ in enumerate(info): self.nameList.append(info[i]['name']) self.urlList.append(info[i]['zipball_url']) except Exception: @@ -65,16 +77,22 @@ def __init__(self, parent): okButton = QtWidgets.QPushButton("&Ok") okButton.clicked.connect(self.applyAndClose) box = QtWidgets.QDialogButtonBox() - box.addButton(cancelButton,QtWidgets.QDialogButtonBox.RejectRole) - box.addButton(okButton,QtWidgets.QDialogButtonBox.AcceptRole) + box.addButton(cancelButton, QtWidgets.QDialogButtonBox.RejectRole) + box.addButton(okButton, QtWidgets.QDialogButtonBox.AcceptRole) layout.addWidget(box, 2, 0) layout.setColumnStretch(1, 1) self.show() def closeEvent(self, *args): + """ + Closes the update window. + """ self.deleteLater() def applyAndClose(self, *args): + """ + Asks the user to update and closes the window. + """ ssnake_location = os.path.dirname(os.path.dirname(__file__)) try: os.mkdir(ssnake_location + os.path.sep + 'test') diff --git a/src/views.py b/src/views.py index 07179220..c335f534 100644 --- a/src/views.py +++ b/src/views.py @@ -17,8 +17,8 @@ # You should have received a copy of the GNU General Public License # along with ssNake. If not, see . -import numpy as np import copy +import numpy as np from matplotlib.pyplot import get_cmap, colormaps import matplotlib import matplotlib.ticker as ticker @@ -62,8 +62,8 @@ def __init__(self, root, fig, canvas, data, duplicateCurrent=None): "ppm": np.array([self.root.father.defaultPPM] * self.NDIM_PLOT, dtype=bool), # display frequency as ppm "color": self.root.father.defaultColor, "linewidth": self.root.father.defaultLinewidth, - "minXTicks": self.root.father.defaultMinXTicks, - "minYTicks": self.root.father.defaultMinYTicks, + "minXTicks": self.root.father.defaultMinXTicks, + "minYTicks": self.root.father.defaultMinYTicks, "grids": self.root.father.defaultGrids, "colorRange": self.root.father.defaultColorRange, "colorMap": self.root.father.defaultColorMap, @@ -96,8 +96,7 @@ def __init__(self, root, fig, canvas, data, duplicateCurrent=None): "stackBegin": None, "stackEnd": None, "stackStep": None, - "spacing": 0, - } + "spacing": 0} self.upd() # get the first slice of data if self.viewSettings['showTitle']: self.fig.suptitle(self.data.name) @@ -140,65 +139,280 @@ def __init__(self, root, fig, canvas, data, duplicateCurrent=None): self.startUp(xReset, yReset) def shape(self): + """ + Returns the shape of the data + + Returns + ------- + tuple: + The shape along each dimension + """ return self.data1D.shape() def len(self, dim=-1): + """ + Returns the length of a specific dimension of the data. By default + the last dimension (-1) is selected. + + Parameters + ---------- + dim (optional = -1): int + The dimension of which the length should be returned. + + Returns + ------- + int: + The length + """ return self.shape()[dim] - + def ndim(self): + """ + Returns the number of dimensions of the data. + + Returns + ------- + int: + The number of dimension + """ return self.data1D.ndim() def freq(self, dim=-1): + """ + Returns the spectrum frequency of a specified dimension + (i.e. centre frequency) in hertz. + By default, the last dimension (-1) is selected. + + Parameters + ---------- + dim (optional = -1): int + The dimension of which the frequency should be returned. + + Returns + ------- + float: + The frequency + """ return self.data1D.freq[dim] def ref(self, dim=-1): + """ + Returns the reference frequency in Hz for a selected dimension. + By default, the last dimension (-1) is selected. + If no reference is defined for the dimension, the centre frequency + is retuned instead. + + Parameters + ---------- + dim (optional = -1): int + The dimension of which the reference frequency should be returned. + + Returns + ------- + float: + The reference frequency of the dimension + """ if self.data1D.ref[dim] is not None: return self.data1D.ref[dim] - else: - return self.data1D.freq[dim] + return self.data1D.freq[dim] def sw(self, dim=-1): + """ + Returns the spectral width (sweep width) in Hz for a selected dimension. + By default, the last dimension (-1) is selected. + + Parameters + ---------- + dim (optional = -1): int + The dimension of which the sw should be returned. + + Returns + ------- + float: + The spectral width of the dimension + """ return self.data1D.sw[dim] def spec(self, dim=-1): + """ + Returns the state of a specified dimension (spectrum or FID). + By default, the last dimension (-1) is selected. + + Parameters + ---------- + dim (optional = -1): int + The dimension of which the spec/fid state should be returned. + + Returns + ------- + bool: + True for spectrum, False for FID. + """ return self.data1D.spec[dim] def xax(self, dim=-1): + """ + Returns the x-axis for the selected dimension. + By default, the last dimension (-1) is selected. + + Parameters + ---------- + dim (optional = -1): int + The dimension of which the x-axis state should be returned. + + Returns + ------- + ndarray: + 1D array with the x-axis. + """ return self.data1D.xaxArray[dim] def wholeEcho(self, dim=-1): + """ + Returns the whole Echo boolean for the selected dimension. + By default, the last dimension (-1) is selected. + + Parameters + ---------- + dim (optional = -1): int + The dimension of which the whole echo state should be returned. + + Returns + ------- + bool: + The whole echo state of the dimension. + """ return self.data1D.wholeEcho[dim] - + def getCurrentAxMult(self, axis=-1): + """ + Returns the axis multiplier (i.e. unit) for the selected dimension. + By default, the last dimension (-1) is selected. + + Parameters + ---------- + axis (optional = -1): int + The dimension of which the multiplier should be returned. + + Returns + ------- + float: + The multiplier for the dimension. + """ return self.getAxMult(self.spec(axis), self.getAxType(), self.getppm(), self.freq(axis), self.ref(axis)) def getAxType(self, num=-1): + """ + Returns the axis type (i.e. unit) for the selected dimension. + By default, the last dimension (-1) is selected. + + The type can be 0,1,2 or 3. + For a spectrum axis: + 0: Hz + 1: kHz + 2: MHz + 3: ppm + + For an FID axis: + 0: s + 1: ms + 2: us + + Parameters + ---------- + num (optional = -1): int + The dimension of which the type should be returned. + + Returns + ------- + int: + The type index for the dimension. + """ return self.viewSettings["axType"][num] def getppm(self, num=-1): + """ + Get the ppm boolean of the selected dimension. + By default, the last dimension (-1) is selected. + Is always False for FID axes. + + Parameters + ---------- + num (optional = -1): int + The dimension of which the ppm boolean should be returned. + + Returns + ------- + bool: + True if the axis is in ppm, False if not. + """ if self.data1D.ref[num] == 0.0 or self.data1D.freq[num] == 0.0: return False - else: - return self.viewSettings["ppm"][num] + return self.viewSettings["ppm"][num] def getDataType(self, data): + """ + Returns the input data in the current representation + (real, imag, both, abs). + + Parameters + ---------- + data: ndarray + The data that should be converted to the representation + + Returns + ------- + ndarray: + The changed data + """ + typeList = [np.real, np.imag, np.array, np.abs] return typeList[self.viewSettings["plotType"]](data) - + def startUp(self, xReset=True, yReset=True): + """ + Plot the data, with optional resets of the x an y axis limits. + + Parameters + ---------- + xReset (optional = True): bool + Whether to reset the x-axis limits + yReset (optional = True): bool + Whether to reset the y-axis limits + """ self.showFid() # plot the data self.plotReset(xReset, yReset) # reset the axes limits def fixAxes(self, axes): + """ + Fixes the axes. + + Parameters + ---------- + axes: list + List of the axes indexes that should be changed. + + Returns + ------- + list: + The new axis list + """ if len(axes) != self.NDIM_PLOT: fullAxes = np.arange(self.data.ndim()) fullAxes = np.delete(fullAxes, axes) diff = self.NDIM_PLOT - len(axes) return np.append(fullAxes[-diff:], axes[-self.NDIM_PLOT:]) - else: - return axes + return axes def rename(self, name): + """ + Rename the data set. + + Parameters + ---------- + name: str + The new name + """ self.data.rename(name) if self.viewSettings['showTitle']: self.fig.suptitle(self.data.name) @@ -207,9 +421,28 @@ def rename(self, name): self.canvas.draw() def copyCurrent(self, root, fig, canvas, data): + """ + Make a copy of the current data structure and the + associated canvas and axis information. + + Parameters + ---------- + root: main1d window class + fig: matplotlib figure + canvas: matplotlib figure canvas + data: spectrum class instance + """ return Current1D(root, fig, canvas, data, self) - def upd(self): # get new data from the data instance + def upd(self): + """ + Get new data from the data instance. + + Returns + ------- + bool: + True if update was OK + """ if self.data.ndim() <= self.axes[-1]: self.axes = np.array([len(self.data.shape()) - 1]) if len(self.locList) != self.data.ndim(): @@ -225,7 +458,19 @@ def upd(self): # get new data from the data instance self.viewSettings["ppm"][-1] = False return True - def setSlice(self, axes, locList): # change the slice + def setSlice(self, axes, locList): + """ + Change the slice + + Parameters + ---------- + axes: ndarray + List of the axes along which the data is plot + (contains a single entry for this 1D plot) + locList: list + List with the location (i.e. data positions) of each dimension + of the data. + """ axesSame = True if not np.array_equal(self.axes, axes): axesSame = False @@ -237,17 +482,48 @@ def setSlice(self, axes, locList): # change the slice self.plotReset() def resetLocList(self): + """ + Resets the locations (i.e. data positions) of all dimensions. + Default values are [0] for all dimensions. + """ self.locList = [0] * self.data.ndim() def getSelect(self): + """ + Get thew slice operator to construct the 1D data from the total + data. + + Returns + ------- + ndarray: + Object array with the slices for each dimension. + """ tmp = np.array(self.locList, dtype=object) tmp[self.axes] = slice(None) return tmp def setGrids(self, grids): + """ + Turn the plot grid along x and y axis on or off. + + Parameters + ---------- + grids: list of booleans + [xgrid,ygrid] + """ self.viewSettings["grids"] = grids def setDiagonal(self, diagonalBool=None, diagonalMult=None): + """ + Change the plotting of a diagonal line in a 2D plot. + + Parameters + ---------- + diagonalBool (optional = None): bool + True if diagonal should be plot + diagonalMult (optional = None): float + Multiplier of the diagonal. + """ if diagonalBool is not None: self.viewSettings["diagonalBool"] = diagonalBool if diagonalMult is not None: @@ -255,51 +531,100 @@ def setDiagonal(self, diagonalBool=None, diagonalMult=None): self.showFid() def reload(self, *args): + """ + Reloads the data, updates and resets the plot. + """ self.data.reload() self.upd() self.showFid() self.plotReset() self.root.addMacro(['reload']) - + def isComplex(self, *args): + """ + Checks if an axis is complex. The last axis (direct dimension) is always complex. + """ return self.data.isComplex(*args) - def setNoUndo(self,val): + def setNoUndo(self, val): + """ + Sets the 'no undo' flag. When this flag is enabled + no undo information is saved. + + Parameters + ---------- + val: bool + The no undo flag + """ self.root.addMacro(['setNoUndo', (val,)]) self.data.setNoUndo(val) def real(self, *args): + """ + Takes the real value along the current axis. + """ self.root.addMacro(['real', (self.axes[-1] - self.data.ndim(), )]) self.data.real(self.axes[-1]) self.upd() self.showFid() - + def imag(self, *args): + """ + Takes the imaginary value along the current axis. + """ self.root.addMacro(['imag', (self.axes[-1] - self.data.ndim(), )]) self.data.imag(self.axes[-1]) self.upd() self.showFid() - + def abs(self, *args): + """ + Takes the absolute value along the current axis. + """ self.root.addMacro(['abs', (self.axes[-1] - self.data.ndim(), )]) self.data.abs(self.axes[-1]) self.upd() self.showFid() def conj(self, *args): + """ + Takes the complex conjugate value along the current axis. + """ self.root.addMacro(['conj', (self.axes[-1] - self.data.ndim(), )]) self.data.conj(self.axes[-1]) self.upd() self.showFid() - - def setPhaseInter(self, phase0in, phase1in): # interactive changing the phase without editing the actual data + + def setPhaseInter(self, phase0in, phase1in): + """ + Interactive changing the phase without editing the actual data. + + Parameters + ---------- + phase0in: float + The 0th order phase + phase1in: float + The 1st order phase + """ phase0 = float(phase0in) phase1 = float(phase1in) self.data1D.phase(phase0, phase1, -1) self.showFid() self.upd() - def applyPhase(self, phase0, phase1, select=False): # apply the phase to the actual data + def applyPhase(self, phase0, phase1, select=False): + """ + Phase the data. + + Parameters + ---------- + phase0in: float + The 0th order phase + phase1in: float + The 1st order phase + select (optional = False): bool + If true, apply only for current slice + """ phase0 = float(phase0) phase1 = float(phase1) if select: @@ -312,13 +637,20 @@ def applyPhase(self, phase0, phase1, select=False): # apply the phase to the ac self.showFid() def correctDFilter(self): - #Corrects the digital filter via first order phasing + """ + Corrects the digital filter via first order phasing. + The filter value (if any) is set upon loading the data. + """ self.root.addMacro(['correctDFilter', (self.axes[-1] - self.data.ndim(),)]) self.data.correctDFilter(self.axes[-1]) self.upd() self.showFid() - def complexFourier(self): # fourier the actual data and replot + def complexFourier(self): + """ + Complex Fourier transform along the current axis. + Redraw the plot. + """ self.root.addMacro(['complexFourier', (self.axes[-1] - self.data.ndim(), )]) self.data.complexFourier(self.axes[-1]) self.upd() @@ -327,7 +659,11 @@ def complexFourier(self): # fourier the actual data and replot self.showFid() self.plotReset() - def realFourier(self): # fourier the real data and replot + def realFourier(self): + """ + Real Fourier transform along the current axis. + Redraw the plot. + """ self.root.addMacro(['realFourier', (self.axes[-1] - self.data.ndim(), )]) self.data.realFourier(self.axes[-1]) self.upd() @@ -337,12 +673,44 @@ def realFourier(self): # fourier the real data and replot self.plotReset() def fftshift(self, inv=False): # fftshift the actual data and replot + """ + fftshift along the current axis. + Redraw the plot. + + Parameters + ---------- + inv: bool + True if inverse shift, False if normal. + """ self.root.addMacro(['fftshift', (self.axes[-1] - self.data.ndim(), inv)]) self.data.fftshift(self.axes[-1], inv) self.upd() self.showFid() - def apodPreview(self, lor=None, gauss=None, cos2= [None, None], hamming=None, shift=0.0, shifting=0.0, shiftingAxis=None): # display the 1D data including the apodization function + def apodPreview(self, lor=None, gauss=None, cos2=None, hamming=None, shift=0.0, shifting=0.0, shiftingAxis=None): + """ + Preview the effect of apodization without touching the data structure. + + Parameters + ---------- + lor (optional = None): float + Lorentzian broadening in Hz + gauss (optional = None): float + Gaussian broadening in Hz + cos2 (optional = [None, None]): list + List of floats. The first value is the frequency (two times the number of periods in the time domain). + The second value is the phase shift in degrees. + hamming (optional = None): float + Hamming apodization frequency + shift (optional = 0.0): float + The amount of time shift to the function (positive is to the right). + shifting (optional = 0.0): float + A shift in time of the function as a function of the x-axis values along shiftingAxis. + shiftingAxis (optional = None): int + The dimension for the shifting. + """ + if cos2 is None: + cos2 = [None, None] y = self.data1D.data.copy() preview = True if shiftingAxis is None: @@ -363,7 +731,32 @@ def apodPreview(self, lor=None, gauss=None, cos2= [None, None], hamming=None, sh self.showFid(y) self.upd() - def applyApod(self, lor=None, gauss=None, cos2= [None,None], hamming=None, shift=0.0, shifting=0.0, shiftingAxis=0, select=False): # apply the apodization to the actual data + def applyApod(self, lor=None, gauss=None, cos2=None, hamming=None, shift=0.0, shifting=0.0, shiftingAxis=0, select=False): # apply the apodization to the actual data + """ + Apply apodization effects. + + Parameters + ---------- + lor (optional = None): float + Lorentzian broadening in Hz + gauss (optional = None): float + Gaussian broadening in Hz + cos2 (optional = [None, None]): list + List of floats. The first value is the frequency (two times the number of periods in the time domain). + The second value is the phase shift in degrees. + hamming (optional = None): float + Hamming apodization frequency + shift (optional = 0.0): float + The amount of time shift to the function (positive is to the right). + shifting (optional = 0.0): float + A shift in time of the function as a function of the x-axis values along shiftingAxis. + shiftingAxis (optional = None): int + The dimension for the shifting. + select (optional = False): boolean + If True, apply only to the current slice. + """ + if cos2 is None: + cos2 = [None, None] if select: selectSlice = self.getSelect() else: @@ -374,18 +767,45 @@ def applyApod(self, lor=None, gauss=None, cos2= [None,None], hamming=None, shift self.showFid() def setFreq(self, freq, sw): # set the frequency of the actual data + """ + Set the center frequency and spectral width (sweep width). + + Parameters + ---------- + freq: float + Center frequency in Hz + sw: float + Spectral width in Hz + + """ self.root.addMacro(['setFreq', (freq, sw, self.axes[-1] - self.data.ndim())]) self.data.setFreq(freq, sw, self.axes[-1]) self.upd() self.showFid() - def scaleSw(self,scale): + def scaleSw(self, scale): + """ + Scale the spectral width (sweep width). + + Parameters + ---------- + scale: float + The multiplier + """ self.root.addMacro(['scaleSw', (scale, self.axes[-1] - self.data.ndim())]) self.data.scaleSw(scale, self.axes[-1]) self.upd() self.showFid() - def setRef(self, ref): # set the frequency of the actual data + def setRef(self, ref): + """ + Set the reference frequency (0 Hz position). + + Parameters + ---------- + ref: float + The reference frequency + """ oldref = self.ref() self.root.addMacro(['setRef', (ref, self.axes[-1] - self.data.ndim())]) self.data.setRef(ref, self.axes[-1]) @@ -403,6 +823,17 @@ def setRef(self, ref): # set the frequency of the actual data self.showFid() def regrid(self, limits, numPoints): + """ + Regrind along the current dimension. This creates a new x-axis, and interpolates to + construct the new y-values. + + Parameters + ---------- + limits: list + List with the minimum and maximum position of the new x-axis (in Hz). + numPoints: int + Number of points in the new x-axis + """ self.root.addMacro(['regrid', (limits, numPoints, self.axes[-1] - self.data.ndim())]) self.data.regrid(limits, numPoints, self.axes[-1]) self.upd() @@ -410,6 +841,25 @@ def regrid(self, limits, numPoints): self.plotReset() def SN(self, minNoise, maxNoise, minPeak, maxPeak): + """ + Get the signal-to-noise ratio of a specific region. + + Parameters + ---------- + minNoise: int + Minimum position of the noise region. + maxNoise: int + Maximum position of the noise region. + minPeak: int + Minimum position of the region with the peak inside. + maxPeak: int + Maximum position of the region with the peak inside. + + Returns + ------- + float: + The SNR + """ minN = min(minNoise, maxNoise) maxN = max(minNoise, maxNoise) minP = min(minPeak, maxPeak) @@ -417,9 +867,38 @@ def SN(self, minNoise, maxNoise, minPeak, maxPeak): tmpData = self.data1D.getHyperData(0) tmpData = tmpData[(0,)*(self.ndim()-1) + (slice(None), )] tmpData = np.real(self.getDataType(tmpData)) - return (np.max(tmpData[minP:maxP]) / (np.std(tmpData[minN:maxN]))) + return np.max(tmpData[minP:maxP]) / (np.std(tmpData[minN:maxN])) def fwhm(self, minPeak, maxPeak, level, unitType=None): + """ + Get the Full Width at Half Maximum (FWHM) for a peak in + a specific region. The level can be pout in by the user, + so a level of 0.5 will give the FWHM, while other values + report the width at other heights. + + Parameters + ---------- + minPeak: int + Minimum position of the region with the peak inside. + maxPeak: int + Maximum position of the region with the peak inside. + level: float + The level for which the width is to be calculated. For + the half width, this should be 0.5. + unitType (optional = None): int + The unit type, only needs to be supplied when the type should + be different than in the current plot. + 0: Hz + 1: kHz + 2: MHz + 3: ppm + + Returns + ------- + float: + The width + """ + from scipy.interpolate import UnivariateSpline if unitType is None: axType = self.getAxType() @@ -444,10 +923,32 @@ def fwhm(self, minPeak, maxPeak, level, unitType=None): right = zeroPos[zeroPos < maxX] if right.size > 0 and left.size > 0: return abs(left[0] - right[-1]) - else: - return 0.0 + return 0.0 def COM(self, minPeak, maxPeak, unitType=None): # Centre of Mass + """ + Get the centre of mass (COM) for a peak in + a specific region. + + Parameters + ---------- + minPeak: int + Minimum position of the region with the peak inside. + maxPeak: int + Maximum position of the region with the peak inside. + unitType (optional = None): int + The unit type, only needs to be supplied when the type should + be different than in the current plot. + 0: Hz + 1: kHz + 2: MHz + 3: ppm + + Returns + ------- + float: + The centre of mass + """ dim = len(minPeak) ppm = [] axType = [] @@ -470,7 +971,7 @@ def COM(self, minPeak, maxPeak, unitType=None): # Centre of Mass #Slice data slc = () for i in range(dim): - slc = slc + (slice(minPeak[-(i+1)],maxPeak[-(i+1)],None),) + slc = slc + (slice(minPeak[-(i+1)], maxPeak[-(i+1)], None),) tmpData = tmpData[slc] COM = [] axes = list(range(dim)) @@ -479,12 +980,33 @@ def COM(self, minPeak, maxPeak, unitType=None): # Centre of Mass del sumAxes[-(i+1)] tmpAxis = self.xax(-(i+1)) tmpAxis = tmpAxis[minPeak[i]:maxPeak[i]] * self.getAxMult(self.spec(-(i+1)), axType[i], ppm[i], self.freq(-(i+1)), self.ref(-(i+1))) - tmpDataNew = np.sum(tmpData,tuple(sumAxes)) + tmpDataNew = np.sum(tmpData, tuple(sumAxes)) # COM = 1/M *sum(m_i * r_i) COM.append(1.0 / np.sum(tmpDataNew) * np.sum(tmpDataNew * tmpAxis)) return COM def Integrals(self, minPeak, maxPeak): + """ + Get the integral for a region. + + Parameters + ---------- + minPeak: int + Minimum position of the region. + maxPeak: int + Maximum position of the region. + + Returns + ------- + float: + Integral + ndarray: + The x-axis of the selected range + ndarray: + The cumulative sum of the y-values of the range + float: + The absolute maximum of y-values + """ dim = len(minPeak) for i in range(dim): #Check max > min if minPeak[i] > maxPeak[i]: @@ -495,14 +1017,14 @@ def Integrals(self, minPeak, maxPeak): totShape = tmpData.shape tmpData = np.real(self.getDataType(tmpData)) maxim = np.max(np.abs(tmpData)) - tmpAxis = tmpAxis[minPeak[0]:maxPeak[0]+1] + tmpAxis = tmpAxis[minPeak[0]:maxPeak[0]+1] slc = tuple() for i in reversed(range(dim)): #Make slice operator along all dimensions - slc = slc + (slice(minPeak[i],maxPeak[i] + 1), ) + slc = slc + (slice(minPeak[i],maxPeak[i] + 1),) tmpData = tmpData[slc] #slice data - if self.spec() == 0 and dim ==1: + if self.spec() == 0 and dim == 1: intSum = np.cumsum(tmpData) - elif self.spec() == 1 and dim ==1: + elif self.spec() == 1 and dim == 1: intSum = np.cumsum(tmpData[-1::-1])[-1::-1] else: intSum = None @@ -515,6 +1037,23 @@ def Integrals(self, minPeak, maxPeak): return inte, tmpAxis, intSum, maxim def MaxMin(self, minPeak, maxPeak, type='max'): + """ + Get the maximum or minimum value of a region. + + Parameters + ---------- + minPeak: int + Minimum position of the region. + maxPeak: int + Maximum position of the region. + type (optional = 'max'): str + 'max' or 'min' + + Returns + ------- + float: + The min/max value of the region + """ minP = min(minPeak, maxPeak) maxP = max(minPeak, maxPeak) tmpData = self.data1D.getHyperData(0) @@ -522,23 +1061,47 @@ def MaxMin(self, minPeak, maxPeak, type='max'): tmpData = np.real(self.getDataType(tmpData)) if type == 'max': return np.max(tmpData[minP:maxP]) - elif type == 'min': + if type == 'min': return np.min(tmpData[minP:maxP]) def integralsPreview(self, x, y, maxim): + """ + Plot preview lines for the integrals tool. + + Parameters + ---------- + x: list of ndarrays + x-values of the extra lines + y: list of ndarrays + y-values of the extra lines + maxim: float + Maximum values the each integrated regions + + """ xNew = [] yNew = [] scale = 0 - for num in range(len(x)): + for num, _ in enumerate(x): if x[num] is not None and y[num] is not None: xNew.append(x[num]) yNew.append(y[num]) - scale = np.max([scale,abs(yNew[-1][0]),abs(yNew[-1][-1])]) - for num in range(len(yNew)): + scale = np.max([scale, abs(yNew[-1][0]), abs(yNew[-1][-1])]) + for num, _ in enumerate(yNew): yNew[num] = yNew[num] / scale * maxim self.showFid(extraX=xNew, extraY=yNew, extraColor=['g']) - def resizePreview(self, size, pos): # set size only on local data + def resizePreview(self, size, pos): + """ + Preview a resizing (zero filling) operation on the data. + + Parameters + ---------- + size: int + The new number of data points + pos: int + The position where any data points should be inserted + or removed. + """ self.data1D.resize(size, pos, -1) self.showFid() if not self.spec(): @@ -546,6 +1109,17 @@ def resizePreview(self, size, pos): # set size only on local data self.upd() def resize(self, size, pos): # set size to the actual data + """ + Apply a resizing (zero filling) operation on the data. + + Parameters + ---------- + size: int + The new number of data points + pos: int + The position where any data points should be inserted + or removed. + """ self.root.addMacro(['resize', (size, pos, self.axes[-1] - self.data.ndim())]) self.data.resize(size, pos, self.axes[-1]) self.upd() @@ -554,6 +1128,21 @@ def resize(self, size, pos): # set size to the actual data self.plotReset(True, False) def lpsvd(self, nPredict, maxFreq, forward, numPoints): + """ + Apply linear prediction on the data. Both forward and backwards predictions + are supported. + + Parameters + ---------- + nPredict : int + The number of datapoints to predict. + maxFreq : int + The maximum number of frequencies to take from the SVD. + forward : bool + If True, a forward prediction is performed, otherwise a backward prediction is performed. + numPoints : int, optional + The number of points to use for SVD. + """ self.root.addMacro(['lpsvd', (nPredict, maxFreq, forward, numPoints, self.axes[-1] - self.data.ndim())]) self.data.lpsvd(nPredict, maxFreq, forward, numPoints, self.axes[-1]) self.upd() @@ -561,7 +1150,15 @@ def lpsvd(self, nPredict, maxFreq, forward, numPoints): if not self.spec(): self.plotReset(True, False) - def setSpec(self, val): # change from time to freq domain of the actual data + def setSpec(self, val): + """ + Change the time/spectrum domain type of the current dimension. + + Parameters + ---------- + val: bool + True: spectrum, False: FID + """ self.root.addMacro(['setSpec', (val, self.axes[-1] - self.data.ndim())]) self.data.setSpec(val, self.axes[-1]) self.upd() @@ -571,23 +1168,59 @@ def setSpec(self, val): # change from time to freq domain of the actual data self.plotReset() def swapEcho(self, idx): + """ + Apply a swap echo operation. + + Parameters + ---------- + idx: int + Position where the swap should occur. + """ self.root.addMacro(['swapEcho', (idx, self.axes[-1] - self.data.ndim())]) self.data.swapEcho(idx, self.axes[-1]) self.upd() self.showFid() def swapEchoPreview(self, idx): + """ + Preview a swap echo operation. + + Parameters + ---------- + idx: int + Position where the swap should occur. + """ self.data1D.swapEcho(idx, -1) self.showFid() self.upd() def setWholeEcho(self, value): + """ + Set the Whole Echo toggle for the current dimension. + + Parameters + ---------- + value: bool + The new Whole Echo value + """ valBool = value != 0 self.root.addMacro(['setWholeEcho', (valBool, self.axes[-1] - self.data.ndim())]) self.data.setWholeEcho(valBool, self.axes[-1]) self.upd() def shift(self, shift, select=False): + """ + Shifts the data along the current dimension. + Shifting is always done in the time domain (i.e. is the current data is a spectrum + it is Fourier transformed back and forward to do the shift). + + Parameters + ---------- + shift: int + The amount of data points to shift (negative is left shift, positive right shift) + select (optional = False): boolean + If True, apply only to the current slice. + """ if select: selectSlice = self.getSelect() else: @@ -598,11 +1231,29 @@ def shift(self, shift, select=False): self.showFid() def shiftPreview(self, shift): + """ + Shows a preview of the shift data operation. + + Parameters + ---------- + shift: int + The amount of data points to shift (negative is left shift, positive right shift) + """ self.data1D.shift(shift, -1) self.showFid() self.upd() def roll(self, shift, select=False): + """ + Circularly rolls the data along the current dimension. Non-integer shift values are allowed. + + Parameters + ---------- + shift: float + The amount of data points to roll (negative is left roll, positive right roll) + select (optional = False): boolean + If True, apply only to the current slice. + """ if select: selectSlice = self.getSelect() else: @@ -613,36 +1264,108 @@ def roll(self, shift, select=False): self.showFid() def rollPreview(self, shift): + """ + Shows a preview of the roll data operation. + + Parameters + ---------- + shift: float + The amount of data points to roll (negative is left roll, positive right roll) + """ self.data1D.roll(shift, -1) self.showFid() self.upd() def align(self, pos1, pos2): + """ + Aligns the maximum of each slice along this dimension, within the pos1-pos2 region. + + + Parameters + ---------- + pos1: int + First data position. + pos2: int + Second data position. + """ self.root.addMacro(['align', (pos1, pos2, self.axes[-1] - self.data.ndim())]) self.data.align(pos1, pos2, self.axes[-1]) self.upd() self.showFid() def getdcOffset(self, pos1, pos2): + """ + Gets the average (i.e. DC offset) of a selected region. + + Parameters + ---------- + pos1: int + First data position. + pos2: int + Second data position. + + Returns + ------- + complex value: + The offset value + """ minPos = int(min(pos1, pos2)) maxPos = int(max(pos1, pos2)) if minPos != maxPos: tmpData = self.data1D.data[(len(self.shape()) - 1) * (slice(None), ) + (slice(minPos, maxPos), )] return np.mean(tmpData.getHyperData(0)) - else: - return 0 + return 0 def dcOffset(self, offset): + """ + Corrects the DC offset (i.e. subtracts the offset value from all data points). + + Parameters + ---------- + offset: complex value + The amount of offset that is to be subtracted. + """ self.data1D.subtract([offset]) self.showFid() self.upd() def baselinePolyFit(self, x, data, bArray, degree): + """ + Fit a polynomial through the selected data. + + Parameters + ---------- + x: ndarray + The x-axis + data: ndarray + The data along this slice + bArray: ndarray, boolian + The points that should be used + degree: int + Number of polynomial orders + + Returns + ------- + ndarray: + The fitted polynomial + """ import numpy.polynomial.polynomial as poly polyCoeff = poly.polyfit(x[bArray], data[bArray], degree) return poly.polyval(x, polyCoeff) - def baselineCorrectionAll(self, degree, removeList, select=False, invert=False): + def baselineCorrectionAll(self, degree, removeList, invert=False): + """ + Correct baseline of a series of data + + Parameters + ---------- + degree: int + Polynomial degree + removeList: list + Indexes of points not include in polyfit + invert (optional = False): boolean + If True, the removeList is treated as an include list (i.e. inverting the selection) + """ tmpAx = np.arange(self.len()) bArray = np.array([True] * self.len()) for i in range(int(np.floor(len(removeList) / 2.0))): @@ -653,10 +1376,24 @@ def baselineCorrectionAll(self, degree, removeList, select=False, invert=False): bArray = np.logical_not(bArray) y = np.apply_along_axis(lambda data: self.baselinePolyFit(self.xax(), data, bArray, degree), self.axes[-1], self.data.getHyperData(0)) y = np.real(self.getDataType(y)) - self.root.addMacro(['subtract', (y)]) + self.root.addMacro(['subtract', (y,)]) self.data.subtract(y) def baselineCorrection(self, degree, removeList, select=False, invert=False): + """ + Correct baseline of spectrum/fid. + + Parameters + ---------- + degree: int + Polynomial degree + removeList: list + Indexes of points not include in polyfit + select (optional = False): boolean + If True, apply only to the current slice. + invert (optional = False): boolean + If True, the removeList is treated as an include list (i.e. inverting the selection) + """ if select: selectSlice = self.getSelect() else: @@ -677,6 +1414,18 @@ def baselineCorrection(self, degree, removeList, select=False, invert=False): self.data.baselineCorrection(y, self.axes[-1], select=selectSlice) def previewBaselineCorrection(self, degree, removeList, invert=False): + """ + Preview the baseline correction of a spectrum/fid. + + Parameters + ---------- + degree: int + Polynomial degree + removeList: list + Indexes of points not include in polyfit + invert (optional = False): boolean + If True, the removeList is treated as an include list (i.e. inverting the selection) + """ tmpData = self.data1D.getHyperData(0) tmpData = tmpData[(0,)*(self.ndim()-1) + (slice(None), )] tmpAx = np.arange(self.len()) @@ -701,6 +1450,16 @@ def previewBaselineCorrection(self, degree, removeList, invert=False): self.upd() def previewRemoveList(self, removeList, invert=False): + """ + Preview the removelist of a baseline correction. + + Parameters + ---------- + removeList: list + Indexes of points not include in polyfit + invert (optional = False): boolean + If True, the removeList is treated as an include list (i.e. inverting the selection) + """ axMult = self.getAxMult(self.spec(), self.getAxType(), self.getppm(), self.freq(), self.ref()) self.resetPreviewRemoveList() lineColor = 'r' @@ -714,69 +1473,211 @@ def previewRemoveList(self, removeList, invert=False): self.canvas.draw() def resetPreviewRemoveList(self): + """ + Resets the preview remove list of the baseline correction. + """ if hasattr(self, 'removeListLines'): for i in self.removeListLines: i.remove() self.removeListLines = [] def states(self): + """ + Performs a States data conversion along the current dimension. + """ self.root.addMacro(['states', (self.axes[-1] - self.data.ndim(), )]) self.data.states(self.axes[-1]) self.upd() self.showFid() def statesTPPI(self): + """ + Performs a States-TPPI data conversion along the current dimension. + """ self.root.addMacro(['statesTPPI', (self.axes[-1] - self.data.ndim(), )]) self.data.statesTPPI(self.axes[-1]) self.upd() self.showFid() def echoAntiEcho(self): + """ + Performs an Echo-Antiecho data conversion along the current dimension. + """ self.root.addMacro(['echoAntiEcho', (self.axes[-1] - self.data.ndim(), )]) self.data.echoAntiEcho(self.axes[-1]) self.upd() self.showFid() def matrixFuncs(self, func, name, pos1, pos2, newSpec=False): + """ + Function that handles multiple matrix operations. + + name: str + Name of operation: integrate, sum, max, min, argmax, argmin, averge + pos1: int + First data point limit of the selected region. + pos2: int + Second data point limit of the selected region. + newSpec (optional = False): boolean + If True, a new spectrum class is returned, holding the output of the matrix function. + + Returns + ------- + Spectrum object: + If newSpec is True, a new spectrum object is returned. + """ if newSpec: tmpData = copy.deepcopy(self.data) func(tmpData, pos1, pos2, self.axes[-1]) return tmpData - else: - self.root.addMacro([name, (pos1, pos2, self.axes[-1] - self.data.ndim(), )]) - func(self.data, pos1, pos2, self.axes[-1]) - if self.upd(): - self.showFid() - self.plotReset() - + self.root.addMacro([name, (pos1, pos2, self.axes[-1] - self.data.ndim(), )]) + func(self.data, pos1, pos2, self.axes[-1]) + if self.upd(): + self.showFid() + self.plotReset() + def integrate(self, pos1, pos2, newSpec=False): + """ + Integrate all slices over the selected region. + + Parameters + ---------- + pos1: int + First data point limit of the selected region. + pos2: int + Second data point limit of the selected region. + newSpec (optional = False): boolean + If True, a new spectrum class is returned, holding the output of the matrix function. + + Returns + ------- + Spectrum object: + If newSpec is True, a new spectrum object is returned. Else, 'None' is returned. + """ return self.matrixFuncs(lambda obj, *args: obj.integrate(*args), 'integrate', pos1, pos2, newSpec) def sum(self, pos1, pos2, newSpec=False): return self.matrixFuncs(lambda obj, *args: obj.sum(*args), 'sum', pos1, pos2, newSpec) - + def max(self, pos1, pos2, newSpec=False): + """ + Get the maximum of all slices over the selected region. + + Parameters + ---------- + pos1: int + First data point limit of the selected region. + pos2: int + Second data point limit of the selected region. + newSpec (optional = False): boolean + If True, a new spectrum class is returned, holding the output of the matrix function. + + Returns + ------- + Spectrum object: + If newSpec is True, a new spectrum object is returned. Else, 'None' is returned. + """ return self.matrixFuncs(lambda obj, *args: obj.max(*args), 'max', pos1, pos2, newSpec) def min(self, pos1, pos2, newSpec=False): + """ + Get the minimum of all slices over the selected region. + + Parameters + ---------- + pos1: int + First data point limit of the selected region. + pos2: int + Second data point limit of the selected region. + newSpec (optional = False): boolean + If True, a new spectrum class is returned, holding the output of the matrix function. + + Returns + ------- + Spectrum object: + If newSpec is True, a new spectrum object is returned. Else, 'None' is returned. + """ return self.matrixFuncs(lambda obj, *args: obj.min(*args), 'min', pos1, pos2, newSpec) def argmax(self, pos1, pos2, newSpec=False): + """ + Get the arg maxmimum of all slices over the selected region. + + Parameters + ---------- + pos1: int + First data point limit of the selected region. + pos2: int + Second data point limit of the selected region. + newSpec (optional = False): boolean + If True, a new spectrum class is returned, holding the output of the matrix function. + + Returns + ------- + Spectrum object: + If newSpec is True, a new spectrum object is returned. Else, 'None' is returned. + """ return self.matrixFuncs(lambda obj, *args: obj.argmax(*args), 'argmax', pos1, pos2, newSpec) def argmin(self, pos1, pos2, newSpec=False): + """ + Get the arg minimum of all slices over the selected region. + + Parameters + ---------- + pos1: int + First data point limit of the selected region. + pos2: int + Second data point limit of the selected region. + newSpec (optional = False): boolean + If True, a new spectrum class is returned, holding the output of the matrix function. + + Returns + ------- + Spectrum object: + If newSpec is True, a new spectrum object is returned. Else, 'None' is returned. + """ return self.matrixFuncs(lambda obj, *args: obj.argmin(*args), 'argmin', pos1, pos2, newSpec) def average(self, pos1, pos2, newSpec=False): + """ + Get the average of all slices over the selected region. + + Parameters + ---------- + pos1: int + First data point limit of the selected region. + pos2: int + Second data point limit of the selected region. + newSpec (optional = False): boolean + If True, a new spectrum class is returned, holding the output of the matrix function. + + Returns + ------- + Spectrum object: + If newSpec is True, a new spectrum object is returned. Else, 'None' is returned. + """ return self.matrixFuncs(lambda obj, *args: obj.average(*args), 'average', pos1, pos2, newSpec) def flipLR(self): + """ + Flip (i.e. mirror) the data over the x-axis. + """ self.data.flipLR(self.axes[-1]) self.upd() self.showFid() self.root.addMacro(['flipLR', (self.axes[-1] - self.data.ndim(), )]) def concatenate(self, axes): + """ + Concatenate the data along an input axis. + + Parameters + ---------- + axes: int + The concatenation axis + + """ self.data.concatenate(axes) self.upd() self.showFid() @@ -784,6 +1685,14 @@ def concatenate(self, axes): self.root.addMacro(['concatenate', (axes - self.data.ndim() - 1, )]) def split(self, sections): + """ + Split the data long the current dimension in part + + Parameters + ---------- + sections: int + Split in this number of sections. + """ self.data.split(sections, self.axes[-1]) self.upd() self.showFid() @@ -791,18 +1700,42 @@ def split(self, sections): self.root.addMacro(['split', (sections, self.axes[-1] - self.data.ndim() + 1)]) def diff(self): + """ + Get the difference between data points along the current dimension. + This reduces the size of the data along this dimension by 1. + + Diff is taken from left to right in the fid, but from right to left in the spectrum (i.e. the spectrum is + displayed in a mirrored way). + """ self.data.diff(self.axes[-1]) self.upd() self.showFid() self.root.addMacro(['diff', (self.axes[-1] - self.data.ndim(), )]) def cumsum(self): + """ + Get the cumulative sum of the data points along the current dimension. + + Cumsum is taken from left to right in the fid, but from right to left in the spectrum (i.e. the spectrum is + displayed in a mirrored way). + """ self.data.cumsum(self.axes[-1]) self.upd() self.showFid() self.root.addMacro(['cumsum', (self.axes[-1] - self.data.ndim(), )]) def insert(self, data, pos): + """ + Insert data at a position along the current dimension. + + Parameters + ---------- + data: hypercomplex data class + The data to be inserted + pos: int + Position where the data should be inserted + """ + self.root.addMacro(['insert', (data, pos, self.axes[-1] - self.data.ndim())]) self.data.insert(data, pos, self.axes[-1]) self.upd() @@ -810,17 +1743,43 @@ def insert(self, data, pos): self.plotReset() def delete(self, pos): + """ + Delete the data from specified positions along the current dimension. + + Parameters + ---------- + pos: int or array_like + The indices to remove. + """ self.data.delete(pos, self.axes[-1]) self.upd() self.showFid() self.root.addMacro(['delete', (pos, self.axes[-1] - self.data.ndim())]) def deletePreview(self, pos): + """ + Preview the delete operation. + + Parameters + ---------- + pos: int or array_like + The indices to remove. + """ self.data1D.delete(pos, -1) self.showFid() self.upd() def add(self, data, select=False): + """ + Adds (sums) data to the current data. + + Parameters + ---------- + data: hypercomplex data class + The data to be added. + select (optional = False): boolean + If True, apply only to the current slice. + """ if select: selectSlice = self.getSelect() else: @@ -831,6 +1790,16 @@ def add(self, data, select=False): self.showFid() def subtract(self, data, select=False): + """ + Subtract data from to the current data. + + Parameters + ---------- + data: hypercomplex data class + The data to be subtracted. + select (optional = False): boolean + If True, apply only to the current slice. + """ if select: selectSlice = self.getSelect() else: @@ -841,6 +1810,17 @@ def subtract(self, data, select=False): self.showFid() def multiply(self, data, select=False): + """ + Multiply the current data with extra data. Note that a complex data + multiplication is used. + + Parameters + ---------- + data: hypercomplex data class + The data to be multiplied with. + select (optional = False): boolean + If True, apply only to the current slice. + """ if select: selectSlice = self.getSelect() else: @@ -851,11 +1831,30 @@ def multiply(self, data, select=False): self.showFid() def multiplyPreview(self, data): + """ + Preview the multiplication of data to the current data. + + Parameters + ---------- + data: hypercomplex data class + The data to be multiplied with. + """ self.data1D.multiply(data, -1) self.showFid() self.upd() - + def divide(self, data, select=False): + """ + Divide the current data with extra data. Note that a complex data + division is used. + + Parameters + ---------- + data: hypercomplex data class + The data to be divided with. + select (optional = False): boolean + If True, apply only to the current slice. + """ if select: selectSlice = self.getSelect() else: @@ -866,6 +1865,21 @@ def divide(self, data, select=False): self.showFid() def normalize(self, value, scale, type, select=False): + """ + Normalize the data, relative to a selected region. + Different types are supported: maximum, minimum, and integral. + + Parameters + ---------- + value: float + Value necessary to scale the selected region to 1. + scale: float + Extra scaling to get to this value + type: int + 0:, integral. 1: max. 2: min + select (optional = False): boolean + If True, apply only to the current slice. + """ if select: selectSlice = self.getSelect() else: @@ -874,31 +1888,81 @@ def normalize(self, value, scale, type, select=False): self.data.normalize(value, scale, type, self.axes[-1], select=selectSlice) self.upd() self.showFid() - + def subtractAvg(self, pos1, pos2): + """ + Subtract the average of a region off all slices from that slice. This can be used + to correct an offset, for example. + + Parameters + ---------- + pos1: int + First data point limit of the selected region. + pos2: int + Second data point limit of the selected region. + """ self.root.addMacro(['subtractAvg', (pos1, pos2, self.axes[-1] - self.data.ndim())]) self.data.subtractAvg(pos1, pos2, self.axes[-1]) self.upd() self.showFid() def subtractAvgPreview(self, pos1, pos2): + """ + Preview of the effect of the "subtractAvg" function. + + Parameters + ---------- + pos1: int + First data point limit of the selected region. + pos2: int + Second data point limit of the selected region. + """ self.data1D.subtractAvg(pos1, pos2, -1) self.showFid() self.upd() def extract(self, pos1, pos2, newSpec=False): + """ + Extract a region from the data (i.e. remove data outside the region. + Can return a new data class if newSpec==True. + + Parameters + ---------- + pos1: int + First data point limit of the selected region. + pos2: int + Second data point limit of the selected region. + newSpec (optional = False): boolean + Return a new data class object + + Returns + ------- + Spectrum object: + If newSpec is True, a new spectrum object is returned. Else, 'None' is returned. + """ if newSpec: tmpData = copy.deepcopy(self.data) tmpData.extract(pos1, pos2, self.axes[-1]) return tmpData - else: - self.root.addMacro(['extract', (pos1, pos2, self.axes[-1] - self.data.ndim())]) - self.data.extract(pos1, pos2, self.axes[-1]) - self.upd() - self.showFid() - self.plotReset() + self.root.addMacro(['extract', (pos1, pos2, self.axes[-1] - self.data.ndim())]) + self.data.extract(pos1, pos2, self.axes[-1]) + self.upd() + self.showFid() + self.plotReset() def fiddle(self, pos1, pos2, lb): + """ + Do a reference deconvolution using the fiddle algorithm. + + Parameters + ---------- + pos1: int + First data point limit of the selected region. + pos2: int + Second data point limit of the selected region. + lb: float + Added line broadening (Lorentzian) in Hz. This is needed to avoid artifacts. + """ minPos = min(pos1, pos2) maxPos = max(pos1, pos2) tmpData = self.data1D.getHyperData(0) @@ -911,62 +1975,190 @@ def fiddle(self, pos1, pos2, lb): self.showFid() def shearing(self, shear, axes, axes2, toRef=False): + """ + Apply a shearing transform to the data. + + Parameters + ---------- + shear: float + Shearing constant + axes: int + The axis number over which the amount of shearing differs. + axes2: int + The axis over which the data must be rolled. + toRef (optional = False): boolean + Whether shearing should be relative to the reference (otherwise to the centre of the spectrum) + """ self.root.addMacro(['shear', (shear, axes - self.data.ndim(), axes2 - self.data.ndim()), toRef]) self.data.shear(shear, axes, axes2, toRef) self.upd() self.showFid() def reorder(self, pos, newLength): + """ + Reorder the current data to the new positions. Missing points are set to zero, and + zeroes are appended to reach 'newLength'. + + Parameters + ---------- + pos: list of ints + List with the new positions of each data point + newLength: int + The new length of the data + """ self.root.addMacro(['reorder', (pos, newLength, self.axes[-1] - self.data.ndim())]) self.data.reorder(pos, newLength, self.axes[-1]) self.upd() self.showFid() def ffm(self, posList, typeVal): + """ + Apply the Fast Forward Maximum Entropy reconstruction method (for NUS data). + + Parameters + ---------- + pos: list of ints + List with the measured (non-zero) positions + typeVal : {0, 1, 2} + The type of data to be reconstructed. + 0=complex, 1=States or States-TPPI, 2=TPPI. + """ self.root.addMacro(['ffm', (posList, typeVal, self.axes[-1] - self.data.ndim())]) self.data.ffm(posList, typeVal, self.axes[-1]) self.upd() self.showFid() def clean(self, posList, typeVal, gamma, threshold, maxIter): + """ + Apply the CLEAN reconstruction method (for NUS data). + + Parameters + ---------- + posList : array_like + A list of indices that are recorded datapoints. + All other datapoints will be reconstructed. + typeVal : {0, 1, 2} + The type of data to be reconstructed. + 0=complex, 1=States or States-TPPI, 2=TPPI. + gamma : float + Gamma value of the CLEAN calculation. + threshold : float + Stopping limit (0 < x < 1) (stop if residual intensity below this point). + maxIter : int + Maximum number of iterations. + """ self.root.addMacro(['clean', (posList, typeVal, self.axes[-1] - self.data.ndim(), gamma, threshold, maxIter)]) self.data.clean(posList, typeVal, self.axes[-1], gamma, threshold, maxIter) self.upd() self.showFid() def ist(self, posList, typeVal, threshold, maxIter, tracelimit): + """ + Apply the IST (Iterative Soft Thresholding) reconstruction method (for NUS data). + + Parameters + ---------- + posList : array_like + A list of indices that are recorded datapoints. + All other datapoints will be reconstructed. + typeVal : {0, 1, 2} + The type of data to be reconstructed. + 0=complex, 1=States or States-TPPI, 2=TPPI. + threshold : float + threshold. The level (0 < x < 1) at which the data is cut every iteration. + maxIter : int + Maximum number of iterations. + tracelimit : float + Stopping limit (0 < x < 1) (stop if residual intensity below this point). + """ self.root.addMacro(['ist', (posList, typeVal, self.axes[-1] - self.data.ndim(), threshold, maxIter, tracelimit)]) self.data.ist(posList, typeVal, self.axes[-1], threshold, maxIter, tracelimit) self.upd() self.showFid() def autoPhase(self, phaseNum): + """ + Automatically phase the data along the current dimension. + This function returns the phasing answers, but does not execute the phasing yet. + + Parameters + ---------- + phaseNum: int + Order up to which to perform the autophasing. + For 0 only zero order phasing is performed, for 1 both zero and first order phasing is performed. + + Returns + ------- + list: + List with 0th and 1st order phase. + """ phases = self.data1D.autoPhase(phaseNum, -1, [0]*self.ndim(), returnPhases=True) self.upd() return phases def directAutoPhase(self, phaseNum): + """ + Automatically phase the data along the current dimension. + This function applies the phasing directly to the data. + + Parameters + ---------- + phaseNum: int + Order up to which to perform the autophasing. + For 0 only zero order phasing is performed, for 1 both zero and first order phasing is performed. + """ tmpLocList = copy.copy(self.locList) if self.ndim() > 1: - tmpLocList[self.axes[:-1]] = self.viewSettings["stackBegin"] + if self.viewSettings["stackBegin"] is None: + tmpLocList[self.axes[-1]] = 0 + else: + tmpLocList[self.axes[-1]] = self.viewSettings["stackBegin"] self.root.addMacro(['autoPhase', (phaseNum, self.axes[-1] - self.data.ndim(), tmpLocList)]) self.data.autoPhase(phaseNum, self.axes[-1], tmpLocList) self.upd() self.showFid() def autoPhaseAll(self, phaseNum): + """ + Automatically phase the data along the current dimension, for + each trace individually. + + Parameters + ---------- + phaseNum: int + Order up to which to perform the autophasing. + For 0 only zero order phasing is performed, for 1 both zero and first order phasing is performed. + """ self.root.addMacro(['autoPhaseAll', (phaseNum, self.axes[-1] - self.data.ndim())]) self.data.autoPhaseAll(phaseNum, self.axes[-1]) self.upd() self.showFid() def setXaxPreview(self, xax): + """ + Preview the plot with a new x-axis. + + Parameters + ---------- + xax : array_like + The x-axis. + It should have the same length as the size of the data along dimension axis. + """ self.data1D.setXax(xax, -1) self.showFid() self.plotReset() self.upd() def setXax(self, xax): + """ + Change the x-axis of the data. + + Parameters + ---------- + xax : array_like + The x-axis. + It should have the same length as the size of the data along dimension axis. + """ self.root.addMacro(['setXax', (xax, self.axes[-1] - self.data.ndim())]) self.data.setXax(xax, self.axes[-1]) self.upd() @@ -974,6 +2166,30 @@ def setXax(self, xax): self.plotReset() def setAxType(self, val, update=True, num=-1): + """ + Change the axis type if the x-axis. + + The type can be 0,1,2 or 3. + For a spectrum axis: + 0: Hz + 1: kHz + 2: MHz + 3: ppm + + For an FID axis: + 0: s + 1: ms + 2: us + + Parameters + ---------- + val: int + The new axis type + update (optional = True): boolean + If True, update the displays with the new axis. + num (optional = -1): int + Which axis to change (default -1 is the x-axis, -2 would be the y axis, etc.) + """ oldAxMult = self.getAxMult(self.spec(num), self.getAxType(num), self.getppm(num), self.freq(num), self.ref(num)) if val == 3: self.viewSettings["ppm"][num] = True @@ -981,57 +2197,148 @@ def setAxType(self, val, update=True, num=-1): self.viewSettings["ppm"][num] = False self.viewSettings["axType"][num] = val newAxMult = self.getAxMult(self.spec(num), self.getAxType(num), self.getppm(num), self.freq(num), self.ref(num)) - if num == -1 or num == (self.ndim()-1): + if num in (-1, self.ndim()-1): self.xminlim = self.xminlim * newAxMult / oldAxMult self.xmaxlim = self.xmaxlim * newAxMult / oldAxMult - elif num == -2 or num == (self.ndim()-2): + elif num in (-2, self.ndim()-2): self.yminlim = self.yminlim * newAxMult / oldAxMult self.ymaxlim = self.ymaxlim * newAxMult / oldAxMult if update: self.showFid() def hilbert(self): + """ + Apply a Hilbert transform along the current dimension. + This reconstructs the imaginary part based on the real part of the data. + """ self.root.addMacro(['hilbert', (self.axes[-1] - self.data.ndim(), )]) self.data.hilbert(self.axes[-1]) self.upd() self.showFid() def getColorMap(self): + """ + Get the current color map + + Returns + ------- + colormap object + """ return COLORMAPLIST.index(self.viewSettings["colorMap"]) def setColorMap(self, num): + """ + Set the color map to an input number. + + Parameters + ---------- + num: int + The number of the color map. + """ self.viewSettings["colorMap"] = COLORMAPLIST[num] def getColorRange(self): + """ + Returns the name of the current color range. + """ return COLORRANGELIST.index(self.viewSettings["colorRange"]) def setColorRange(self, num): + """ + Set the color range to an input number. + + Parameters + ---------- + num: int + The number of the color range. + """ self.viewSettings["colorRange"] = COLORRANGELIST[num] def setColor(self, color): + """ + Set line color. + + Parameters + ---------- + color: string + Color string like '#1F77B4' + """ self.viewSettings["color"] = color def setLw(self, lw): + """ + Set the line width of the plot line. + + Parameters + lw: float: + The new line width + """ self.viewSettings["linewidth"] = lw def setTickNum(self, x, y): + """ + Set suggested minimum number of ticks for the x and y axis. + + Parameters + ---------- + x: int + Number of x ticks + y: int + Number of y ticks + """ self.viewSettings["minXTicks"] = x self.viewSettings["minYTicks"] = y def setContourColors(self, colors): + """ + Sets the positive/negative colors for the contour plot. + + Parameters + ---------- + colors: list of color strings + The positive/negative color string + """ self.viewSettings["contourColors"] = colors def setContourConst(self, constant): + """ + Parameters + ---------- + constant: bool + If True, use constant colors for negative/positive contours. + If False, use the color gradient + """ self.viewSettings["contourConst"] = constant def getOOM(self): + """ + Get order of magnitude for the intensity of the current data. + + Returns + ------- + int: + Order of magnitude + """ absVal = np.max(np.abs(self.data.getHyperData(0))) if absVal == 0.0: return 1 - else: - return int(np.floor(np.log10(absVal))) + return int(np.floor(np.log10(absVal))) - def showFid(self, oldData=None, extraX=None, extraY=None, extraColor=None): # display the 1D data + def showFid(self, oldData=None, extraX=None, extraY=None, extraColor=None): + """ + Display the data + + Parameters + ---------- + oldData (optional = None): hypercomplex data type + The old data, to display under the current (i.e. during apodization). + extraX (optional = None): list of ndarrays + List of extra x-axes for 1D data curves + extray (optional = None): list of ndarrays + List of extra intensity data for 1D data curves + extraColor (optional = None): list of color strings + List of color strings for the extra data + """ self.peakPickReset() tmpdata = self.data1D.getHyperData(0) self.ax.cla() @@ -1051,7 +2358,7 @@ def showFid(self, oldData=None, extraX=None, extraY=None, extraColor=None): # d self.line_ydata_extra.append(tmp) self.ax.plot(self.line_xdata_extra[-1], self.line_ydata_extra[-1], marker=marker, linestyle=linestyle, c='k', alpha=0.2, linewidth=self.viewSettings["linewidth"], label=self.data.name + '_old', picker=True) if extraX is not None: - for num in range(len(extraX)): + for num, _ in enumerate(extraX): self.line_xdata_extra.append(extraX[num] * axMult) self.line_ydata_extra.append(extraY[num]) if extraColor is None: @@ -1063,7 +2370,7 @@ def showFid(self, oldData=None, extraX=None, extraY=None, extraColor=None): # d color = extraColor[num] self.ax.plot(self.line_xdata_extra[-1], self.line_ydata_extra[-1], marker='', linestyle='-', c=color, linewidth=self.viewSettings["linewidth"], picker=True) tmpdata = self.getDataType(tmpdata) - if(self.viewSettings["plotType"] == 2): + if self.viewSettings["plotType"] == 2: self.line_xdata.append(self.line_xdata[-1]) self.line_ydata = [np.imag(tmpdata), np.real(tmpdata)] self.ax.plot(self.line_xdata[-2], self.line_ydata[-2], marker=marker, linestyle=linestyle, c='#FF7F0E', linewidth=self.viewSettings["linewidth"], label=self.data.name + '_imag', picker=True) @@ -1091,15 +2398,34 @@ def showFid(self, oldData=None, extraX=None, extraY=None, extraColor=None): # d self.setTicks() self.canvas.draw() - def setTicks(self,Xset = True,Yset = True): + def setTicks(self, Xset=True, Yset=True): + """ + Set ticks for the current plot. + + Parameters + ---------- + Xset (optional = True): boolean + If False, do not draw x-ticks + Yset (optional = True): boolean + If False, do not draw y-ticks + """ if matplotlib.__version__[0] > '1': if Xset: - self.ax.xaxis.set_major_locator(ticker.MaxNLocator(nbins='auto', steps=[1,2,2.5,5,10], min_n_ticks=self.viewSettings["minXTicks"])) + self.ax.xaxis.set_major_locator(ticker.MaxNLocator(nbins='auto', steps=[1, 2, 2.5, 5, 10], min_n_ticks=self.viewSettings["minXTicks"])) if Yset: - self.ax.yaxis.set_major_locator(ticker.MaxNLocator(nbins='auto', steps=[1,2,2.5,5,10], min_n_ticks=self.viewSettings["minYTicks"])) - - def plotReset(self, xReset=True, yReset=True): # set the plot limits to min and max values - showDat = self.data1D.data[0] + self.ax.yaxis.set_major_locator(ticker.MaxNLocator(nbins='auto', steps=[1, 2, 2.5, 5, 10], min_n_ticks=self.viewSettings["minYTicks"])) + + def plotReset(self, xReset=True, yReset=True): + """ + Reset plot limits. + + Parameters + ---------- + xReset (optional = True): boolean + If True, reset the x-axis limits + yReset (optional = True): boolean + If True, reset the y-axis limits + """ miny = np.min(self.line_ydata) maxy = np.max(self.line_ydata) for line in self.line_ydata_extra: @@ -1157,17 +2483,58 @@ class CurrentMulti(Current1D): X_RESIZE = False Y_RESIZE = True - def setExtraSlice(self, extraNum, axes, locList): # change the slice + def setExtraSlice(self, extraNum, axes, locList): + """ + Change the slice of one of the extra data sets + + Parameters + ---------- + extraNum: int + Index of the extra data + axes: 1darray + The new axis + locList: list + New slice information for this data + """ self.viewSettings["extraAxes"][extraNum] = axes self.viewSettings["extraLoc"][extraNum] = locList def copyCurrent(self, root, fig, canvas, data): + """ + Make a copy of the current data structure and the + associated canvas and axis information. + + Parameters + ---------- + root: main1Dwindow + The basic window + fig: matplotib figure + The figure + canvas: matplotlib canvas + The plot canvas + data: spectrum class instance + The data class + + Returns + ------- + CurrentMulti view class + """ return CurrentMulti(root, fig, canvas, data, self) def addExtraData(self, data, name): + """ + Add extra data to the multiview + + Parameters + ---------- + data: spectrum class data + The extra data + name: str + The name of the extra data + """ self.viewSettings["extraName"].append(name) self.viewSettings["extraData"].append(data) - self.viewSettings["extraLoc"].append([0] * (len(self.viewSettings["extraData"][-1].shape()) )) + self.viewSettings["extraLoc"].append([0] * (len(self.viewSettings["extraData"][-1].shape()))) self.viewSettings["extraColor"].append(COLORCONVERTER.to_rgb(COLORCYCLE[np.mod(len(self.viewSettings["extraData"]), len(COLORCYCLE))]['color'])) # find a good color system self.viewSettings["extraAxes"].append([len(data.shape()) - 1]) self.viewSettings["extraScale"].append(1.0) @@ -1176,6 +2543,14 @@ def addExtraData(self, data, name): self.showFid() def delExtraData(self, num): + """ + Delete extra data + + Parameters + ---------- + num: int + Index of the data to be removed. + """ del self.viewSettings["extraData"][num] del self.viewSettings["extraLoc"][num] del self.viewSettings["extraColor"][num] @@ -1187,36 +2562,115 @@ def delExtraData(self, num): self.showFid() def setExtraColor(self, num, color): + """ + Set the color of a specified extra data set + + Parameters + ---------- + num: int + Index of the extra data + color: tuple + Color tuple (R,G,B,Alpha) of the new color + """ self.viewSettings["extraColor"][num] = color self.showFid() def getExtraColor(self, num): + """ + Returns the colour tuple for a specified extra data + + Parameters + ---------- + num: int + Index of the extra data + + Returns + ------- + tuple: + The colour tuple + """ return tuple(np.array(255 * np.array(self.viewSettings["extraColor"][num]), dtype=int)) def resetLocList(self): + """ + Resets the location list (slices) of all data. + """ super(CurrentMulti, self).resetLocList() self.resetExtraLocList() def setExtraScale(self, num, scale): + """ + Set the vertical scaling of additional plotted data. + + Parameters + ---------- + num: int + Index of the extra data + scale: float + The new scaling factor + """ self.viewSettings["extraScale"][num] = scale self.showFid() def setExtraOffset(self, num, offset): + """ + Set the vertical offset of additional plotted data. + + Parameters + ---------- + num: int + Index of the extra data + offset: float + The new offset + """ self.viewSettings["extraOffset"][num] = offset self.showFid() def setExtraShift(self, num, shift): + """ + Set the horizontal offset of additional plotted data. + + Parameters + ---------- + num: int + Index of the extra data + shift: float + The new shift in units of the current axis + """ self.viewSettings["extraShift"][num] = shift self.showFid() def resetExtraLocList(self, num=None): + """ + Resets the location list (active slice) of all extra data or + a specific data set. + + Parameters + ---------- + num (optional = None): int + Index of the data to be adjusted. If None, reset all. + """ if num is None: for i in range(len(self.viewSettings["extraLoc"])): self.viewSettings["extraLoc"][i] = [0] * (len(self.viewSettings["extraData"][i].shape())) else: self.viewSettings["extraLoc"][num] = [0] * (len(self.viewSettings["extraData"][num].shape())) - def showFid(self, oldData=None, extraX=None, extraY=None, extraColor=None): # display the 1D data + def showFid(self, oldData=None, extraX=None, extraY=None, extraColor=None): + """ + Plot all data. + + Parameters + ---------- + oldData (optional = None): hypercomplex data type + The old data, to display under the current (i.e. during apodization). + extraX (optional = None): list of ndarrays + List of extra x-axes for 1D data curves + extray (optional = None): list of ndarrays + List of extra intensity data for 1D data curves + extraColor (optional = None): list of color strings + List of color strings for the extra data + """ self.peakPickReset() tmpdata = self.data1D.getHyperData(0) self.ax.cla() @@ -1268,8 +2722,8 @@ def showFid(self, oldData=None, extraX=None, extraY=None, extraColor=None): # d self.line_xdata_extra.append(self.xax() * axMult) self.line_ydata_extra.append(tmp) self.ax.plot(self.line_xdata_extra[-1], self.line_ydata_extra[-1], marker=marker, linestyle=linestyle, c='k', alpha=0.2, linewidth=self.viewSettings["linewidth"], label=self.data.name + '_old', picker=True) - if (extraX is not None): - for num in range(len(extraX)): + if extraX is not None: + for num, _ in enumerate(extraX): self.line_xdata_extra.append(extraX[num] * axMult) self.line_ydata_extra.append(extraY[num]) if extraColor is None: @@ -1281,7 +2735,7 @@ def showFid(self, oldData=None, extraX=None, extraY=None, extraColor=None): # d color = extraColor[num] self.ax.plot(self.line_xdata_extra[-1], self.line_ydata_extra[-1], marker=marker, linestyle=linestyle, linewidth=self.viewSettings["linewidth"], c=color, picker=True) tmpdata = self.getDataType(tmpdata) - if(self.viewSettings["plotType"] == 2): + if self.viewSettings["plotType"] == 2: self.line_xdata.append(self.line_xdata[-1]) self.line_ydata = [np.imag(tmpdata), np.real(tmpdata)] self.ax.plot(self.line_xdata[-2], self.line_ydata[-2], marker=marker, linestyle=linestyle, c='#FF7F0E', linewidth=self.viewSettings["linewidth"], label=self.data.name + '_imag', picker=True) @@ -1300,7 +2754,7 @@ def showFid(self, oldData=None, extraX=None, extraY=None, extraColor=None): # d self.ax.set_xlim(self.xminlim, self.xmaxlim) self.ax.set_ylim(self.yminlim, self.ymaxlim) self.canvas.draw() - + ######################################################################################################### # the class from which the stacked data is displayed, the operations which only edit the content of this class are for previewing @@ -1313,18 +2767,51 @@ class CurrentStacked(Current1D): NDIM_PLOT = 2 def startUp(self, xReset=True, yReset=True): + """ + Run when starting this plot. + + Parameters + ---------- + xReset (optional = True): boolean + Reset the x-axis if True + + yReset (optional = True): boolean + Reset the y-axis if True + """ self.resetSpacing() self.showFid() self.plotReset(xReset, yReset) def copyCurrent(self, root, fig, canvas, data): + """ + Make a copy of the current data structure and the + associated canvas and axis information. + + Parameters + ---------- + root: main1Dwindow + The basic window + fig: matplotib figure + The figure + canvas: matplotlib canvas + The plot canvas + data: spectrum class instance + The data class + + Returns + ------- + CurrentStacked view class + """ return CurrentStacked(root, fig, canvas, data, self) - def upd(self): # get new data from the data instance + def upd(self): + """ + Get new data from the data instance + """ if self.data.ndim() < 2: self.root.rescue() return False - if self.data.ndim() <= self.axes[-1] or self.data.ndim() <= self.axes[-2] or self.axes[-1]==self.axes[-2]: + if self.data.ndim() <= self.axes[-1] or self.data.ndim() <= self.axes[-2] or self.axes[-1] == self.axes[-2]: self.axes = np.array([len(self.data.shape()) - 2, len(self.data.shape()) - 1]) if len(self.locList) != self.data.ndim(): self.resetLocList() @@ -1343,6 +2830,20 @@ def upd(self): # get new data from the data instance return True def stackSelect(self, stackBegin, stackEnd, stackStep): + """ + Select which data to plot in the stack plot. + The data indexes go from stackBegin to stackEnd with stackStep as + step size. + + Parameters + ---------- + stackBegin: int + Bgin value of the series + stackEnd: int + End value of the series. Note that in python, this value is not inluded (0:2 gives 0,1) + stackStep: int + Step size + """ self.viewSettings["stackBegin"] = stackBegin self.viewSettings["stackEnd"] = stackEnd self.viewSettings["stackStep"] = stackStep @@ -1351,11 +2852,27 @@ def stackSelect(self, stackBegin, stackEnd, stackStep): self.plotReset(self.X_RESIZE, self.Y_RESIZE) def setSpacing(self, spacing): + """ + Sets the vertical spacing between the different traces. + + Parameters + ---------- + spacing: float + The new spacing + """ self.viewSettings["spacing"] = spacing self.showFid() self.plotReset(self.X_RESIZE, self.Y_RESIZE) - def resetSpacing(self): + def resetSpacing(self, zlims=True): + """ + Reset plot spacing + + Parameters + ---------- + zlims: + Not used + """ difference = np.diff(self.data1D.getHyperData(0), axis=0) if difference.size == 0: self.viewSettings["spacing"] = 0 @@ -1367,16 +2884,40 @@ def resetSpacing(self): self.viewSettings["spacing"] = np.abs(difference) + 0.1 * amp def altScroll(self, event): + """ + Scroll spacing + + Parameters + ---------- + event: mouse event + """ self.viewSettings["spacing"] = self.viewSettings["spacing"] * 1.1**event.step self.root.sideframe.scrollSpacing(self.viewSettings["spacing"]) self.showFid() def altReset(self): + """ + Reset the spacing. + """ self.resetSpacing() self.root.sideframe.scrollSpacing(self.viewSettings["spacing"]) self.showFid() - def showFid(self, oldData=None, extraX=None, extraY=None, extraColor=None): # display the 1D data + def showFid(self, oldData=None, extraX=None, extraY=None, extraColor=None): + """ + Plot the data. + + Parameters + ---------- + oldData (optional = None): hypercomplex data type + The old data, to display under the current (i.e. during apodization). + extraX (optional = None): list of ndarrays + List of extra x-axes for 1D data curves + extray (optional = None): list of ndarrays + List of extra intensity data for 1D data curves + extraColor (optional = None): list of color strings + List of color strings for the extra data + """ self.peakPickReset() tmpdata = self.data1D.getHyperData(0) self.ax.cla() @@ -1398,9 +2939,9 @@ def showFid(self, oldData=None, extraX=None, extraY=None, extraColor=None): # d self.line_xdata_extra.append(tmp_line_xdata) self.line_ydata_extra.append(num * self.viewSettings["spacing"] + tmp) self.ax.plot(self.line_xdata_extra[-1], self.line_ydata_extra[-1], marker=marker, linestyle=linestyle, c='k', alpha=0.2, linewidth=self.viewSettings["linewidth"], label=self.data.name + '_old', picker=True) - if (extraX is not None): + if extraX is not None: tmpx = extraX[0] * axMult - for num in range(len(extraY)): + for num, _ in enumerate(extraY): self.line_xdata_extra.append(tmpx) self.line_ydata_extra.append(num * self.viewSettings["spacing"] + extraY[num]) if extraColor is None: @@ -1416,8 +2957,8 @@ def showFid(self, oldData=None, extraX=None, extraY=None, extraColor=None): # d colorRange = None else: colorRange = get_cmap(self.viewSettings["colorRange"]) - for num in range(len(tmpdata)): - if (self.viewSettings["plotType"] == 2): + for num, _ in enumerate(tmpdata): + if self.viewSettings["plotType"] == 2: self.line_xdata.append(tmp_line_xdata) self.line_ydata.append(num * self.viewSettings["spacing"] + np.imag(tmpdata[num])) self.ax.plot(self.line_xdata[-1], self.line_ydata[-1], marker=marker, linestyle=linestyle, c='#FF7F0E', linewidth=self.viewSettings["linewidth"], label=self.data.name + '_imag', picker=True) @@ -1468,31 +3009,107 @@ def __init__(self, root, fig, canvas, data, duplicateCurrent=None): super(CurrentArrayed, self).__init__(root, fig, canvas, data, duplicateCurrent) def startUp(self, xReset=True, yReset=True): + """ + Run when starting this plot. + + Parameters + ---------- + xReset (optional = True): boolean + Reset the x-axis if True + + yReset (optional = True): boolean + Reset the y-axis if True + """ self.resetSpacing(False) self.showFid() self.plotReset(xReset, yReset) def copyCurrent(self, root, fig, canvas, data): + """ + Make a copy of the current data structure and the + associated canvas and axis information. + + Parameters + ---------- + root: main1Dwindow + The basic window + fig: matplotib figure + The figure + canvas: matplotlib canvas + The plot canvas + data: spectrum class instance + The data class + + Returns + ------- + CurrentArrayed view class + """ return CurrentArrayed(root, fig, canvas, data, self) def setAxType(self, val, update=True, num=-1): #Reimplement of base function. Prevent change of yaxis limits - yminlimBack = self.yminlim - ymaxlimBack = self.ymaxlim + """ + Change the axis type if the x-axis. + + The type can be 0,1,2 or 3. + For a spectrum axis: + 0: Hz + 1: kHz + 2: MHz + 3: ppm + + For an FID axis: + 0: s + 1: ms + 2: us + + Parameters + ---------- + val: int + The new axis type + update (optional = True): boolean + If True, update the displays with the new axis. + num (optional = -1): int + Which axis to change (default -1 is the x-axis, -2 would be the y axis, etc.) + """ + yminlimBack = self.yminlim + ymaxlimBack = self.ymaxlim super(CurrentArrayed, self).setAxType(val, False, num) - self.yminlim = yminlimBack - self.ymaxlim = ymaxlimBack + self.yminlim = yminlimBack + self.ymaxlim = ymaxlimBack if update: self.showFid() def resetSpacing(self, zlims=True): + """ + Reset spacing + + Parameters + ---------- + zlims (optional = True): boolean + If True, reset the limits of the individual x-axes too. + """ if zlims: self.zminlim = min(self.xax()) self.zmaxlim = max(self.xax()) xaxZlims = (self.xax() > self.zminlim) & (self.xax() < self.zmaxlim) self.viewSettings["spacing"] = (self.xax()[xaxZlims][-1] - self.xax()[xaxZlims][0]) * 1.1 - def showFid(self, oldData=None, extraX=None, extraY=None, extraColor=None): # display the 1D data + def showFid(self, oldData=None, extraX=None, extraY=None, extraColor=None): + """ + Plot the data. + + Parameters + ---------- + oldData (optional = None): hypercomplex data type + The old data, to display under the current (i.e. during apodization). + extraX (optional = None): list of ndarrays + List of extra x-axes for 1D data curves + extray (optional = None): list of ndarrays + List of extra intensity data for 1D data curves + extraColor (optional = None): list of color strings + List of color strings for the extra data + """ self.peakPickReset() tmpdata = self.data1D.getHyperData(0) self.ax.cla() @@ -1519,9 +3136,9 @@ def showFid(self, oldData=None, extraX=None, extraY=None, extraColor=None): # d self.line_xdata_extra.append((num * self.viewSettings["spacing"] + self.xax()[xaxZlims]) * axMult) self.line_ydata_extra.append(tmp[xaxZlims][direc]) self.ax.plot(self.line_xdata_extra[-1], self.line_ydata_extra[-1], marker=marker, linestyle=linestyle, c='k', alpha=0.2, linewidth=self.viewSettings["linewidth"], label=self.data.name + '_old', picker=True) - if (extraX is not None): + if extraX is not None: extraZlims = (extraX[0] > self.zminlim) & (extraX[0] < self.zmaxlim) - for num in range(len(extraY)): + for num, _ in enumerate(extraY): self.line_xdata_extra.append((num * self.viewSettings["spacing"] + extraX[0][extraZlims]) * axMult) self.line_ydata_extra.append(extraY[num][extraZlims][direc]) if extraColor is None: @@ -1538,8 +3155,8 @@ def showFid(self, oldData=None, extraX=None, extraY=None, extraColor=None): # d colorRange = None else: colorRange = get_cmap(self.viewSettings["colorRange"]) - for num in range(len(tmpdata)): - if (self.viewSettings["plotType"] == 2): + for num, _ in enumerate(tmpdata): + if self.viewSettings["plotType"] == 2: self.line_xdata.append((num * self.viewSettings["spacing"] + self.xax()[xaxZlims]) * axMult) self.line_ydata.append(np.imag(tmpdata[num][xaxZlims])[direc]) self.ax.plot(self.line_xdata[-1], self.line_ydata[-1], marker=marker, linestyle=linestyle, c='#FF7F0E', linewidth=self.viewSettings["linewidth"], label=self.data.name + '_imag', picker=True) @@ -1557,7 +3174,7 @@ def showFid(self, oldData=None, extraX=None, extraY=None, extraColor=None): # d self.ax.set_xlim(self.xminlim, self.xmaxlim) self.ax.set_ylim(self.yminlim, self.ymaxlim) self.ax.get_yaxis().get_major_formatter().set_powerlimits((-4, 4)) - self.setTicks(Xset = False) + self.setTicks(Xset=False) self.ax.xaxis.grid(self.viewSettings["grids"][0]) self.ax.yaxis.grid(self.viewSettings["grids"][1]) self.canvas.draw() @@ -1566,6 +3183,14 @@ def showFid(self, oldData=None, extraX=None, extraY=None, extraColor=None): # d def add_diagonal(axes, mult, *line_args, **line_kwargs): + """ + Add a diagonal to the plot. + + Parameters + ---------- + axes: matplotlib axes + mult: diagonal multiplier (1 is true diagonal). + """ identity, = axes.plot([], [], *line_args, **line_kwargs) def callback(axes): @@ -1596,12 +3221,30 @@ class CurrentContour(CurrentStacked): GRID_PLOT = True INVERT_Y = True ZERO_SCROLL_ALLOWED = False - + def startUp(self, xReset=True, yReset=True): + """ + Run when starting this plot. + + Parameters + ---------- + xReset (optional = True): boolean + Reset the x-axis if True + + yReset (optional = True): boolean + Reset the y-axis if True + """ self.showFid() self.plotReset(xReset, yReset) - - def altScroll(self, event): # Shift scroll scrolls contour limits + + def altScroll(self, event): + """ + Scroll contour level limit. + + Parameters + ---------- + event: mouse event + """ minLevels = self.viewSettings["minLevels"] / 1.1**event.step if minLevels > 1: minLevels = 1 @@ -1613,9 +3256,48 @@ def altScroll(self, event): # Shift scroll scrolls contour limits self.showFid() def copyCurrent(self, root, fig, canvas, data): + """ + Make a copy of the current data structure and the + associated canvas and axis information. + + Parameters + ---------- + root: main1Dwindow + The basic window + fig: matplotib figure + The figure + canvas: matplotlib canvas + The plot canvas + data: spectrum class instance + The data class + + Returns + ------- + CurrenContour view class + """ return CurrentContour(root, fig, canvas, data, self) def setLevels(self, numLevels, maxLevels, minLevels, limitType, contourSign, contourType, multiValue): + """ + Sets the contour settings + + Parameters + ---------- + numLevels: int + Number of contours + maxLevels: float + Maximum value (1 is max) of the contours + minLevels: float + Minimum level of the contours + limitType: int + 0: relative to current 2D slice 1: relative to full data + contourSign: int + 0: both, 1: + only 2: - only + contourType: int + 0: linear 1: multiplier + multiValue: float + Value of the multiplier + """ self.viewSettings["numLevels"] = numLevels self.viewSettings["maxLevels"] = maxLevels self.viewSettings["minLevels"] = minLevels @@ -1626,23 +3308,76 @@ def setLevels(self, numLevels, maxLevels, minLevels, limitType, contourSign, con self.showFid() def setProjLimits(self, ProjBool, Limits): + """ + Set projection limits (i.e. ranges). + + Parameters + ---------- + ProjBool: boolean + If True, projection ranges are taken into account. + Limits: list of 4 ints + Slice positions that limit the projections + """ self.viewSettings["projLimits"] = Limits self.viewSettings["projLimitsBool"] = ProjBool def setProjPos(self, pos): + """ + Sets the projection slice, if a specific slice is plot as the projection. + + Parameters + ---------- + pos: list of ints + The slices to be taken + """ self.viewSettings["projPos"] = pos def setProjType(self, val, direc): + """ + Set the type of projection + + Parameters + ---------- + val: int + The type. 0: sum 1: max 2: min 3: off 4: slice 5: diagonal + direct: int + 1: top 2: right + """ if direc == 1: self.viewSettings["projTop"] = val if direc == 2: self.viewSettings["projRight"] = val def setProjTraces(self, val, direc): + """ + Set a specific trace for a projection. + + Parameters + ---------- + val: int + The trace index + direct: int + 1: top 2: right + """ self.viewSettings["projPos"][direc] = val def integralsPreview(self, xMin, xMax, yMin, yMax): - nPatches = min(len(xMin),len(xMax),len(yMin),len(yMax)) + """ + Draw different rectanglur patches, for a preview of + the intergral selection tool. + + Parameters + ---------- + xMin: list of int + Minimum x positions of the rectangles + xMax: list of int + Maximum x positions of the rectangles + yMin: list of int + Minimum y positions of the rectangles + yMax: list of int + Maximum y positions of the rectangles + """ + nPatches = min(len(xMin), len(xMax), len(yMin), len(yMax)) self.resetPreviewRemoveList() xax = self.xax() yax = self.xax(-2) @@ -1654,41 +3389,60 @@ def integralsPreview(self, xMin, xMax, yMin, yMax): xmaxTmp = xax[xMax[i]] * xaxMult yminTmp = yax[yMin[i]] * yaxMult ymaxTmp = yax[yMax[i]] * yaxMult - self.removeListLines.append(self.ax.fill([xminTmp,xminTmp,xmaxTmp,xmaxTmp],[yminTmp,ymaxTmp,ymaxTmp,yminTmp],color=color,fill = False, linestyle='--')[0]) + self.removeListLines.append(self.ax.fill([xminTmp, xminTmp, xmaxTmp, xmaxTmp], [yminTmp, ymaxTmp, ymaxTmp, yminTmp],color=color, fill=False, linestyle='--')[0]) self.canvas.draw() - def updateAxes(self, oldAx, newAx, axis): - scale = newAx / oldAx - # Scale the path vertices, so no new contours need to be calculated - cols = self.ax.collections - for col in cols: - paths = col.get_paths() - for path in paths: - tmp = path.vertices - tmp[:,axis] = tmp[:,axis] * scale - path.vertices = tmp - # Scale the projections - if axis == 1: # Yaxis - line = self.y_ax.lines - line[0].set_ydata(line[0].get_ydata() * scale) - else: - line = self.x_ax.lines - line[0].set_xdata(line[0].get_xdata() * scale) - # Set the labels - self.ax.set_xlabel(self.getLabel(self.spec(), self.axes[-1], self.getAxType(), self.getppm())) - self.ax.set_ylabel(self.getLabel(self.spec(-2), self.axes[-2], self.getAxType(-2), self.getppm(-2))) - # Set the zoom - if axis == 1: - ylim = self.ax.get_ylim() - self.ax.set_ylim(ylim[0] * scale, ylim[1] * scale) - self.line_ydata = [item*scale for item in self.line_ydata] - else: - xlim = self.ax.get_xlim() - self.ax.set_xlim(xlim[0] * scale, xlim[1] * scale) - self.line_xdata = [item*scale for item in self.line_xdata] - self.canvas.draw() + #def updateAxes(self, oldAx, newAx, axis): + # """ + # Update the axis without recalculating the contours. + # """ + # scale = newAx / oldAx + # # Scale the path vertices, so no new contours need to be calculated + # cols = self.ax.collections + # for col in cols: + # paths = col.get_paths() + # for path in paths: + # tmp = path.vertices + # tmp[:, axis] = tmp[:, axis] * scale + # path.vertices = tmp + # # Scale the projections + # if axis == 1: # Yaxis + # line = self.y_ax.lines + # line[0].set_ydata(line[0].get_ydata() * scale) + # else: + # line = self.x_ax.lines + # line[0].set_xdata(line[0].get_xdata() * scale) + # # Set the labels + # self.ax.set_xlabel(self.getLabel(self.spec(), self.axes[-1], self.getAxType(), self.getppm())) + # self.ax.set_ylabel(self.getLabel(self.spec(-2), self.axes[-2], self.getAxType(-2), self.getppm(-2))) + # # Set the zoom + # if axis == 1: + # ylim = self.ax.get_ylim() + # self.ax.set_ylim(ylim[0] * scale, ylim[1] * scale) + # self.line_ydata = [item*scale for item in self.line_ydata] + # else: + # xlim = self.ax.get_xlim() + # self.ax.set_xlim(xlim[0] * scale, xlim[1] * scale) + # self.line_xdata = [item*scale for item in self.line_xdata] + # self.canvas.draw() def showFid(self, oldData=None, extraX=None, extraY=None, extraZ=None, extraColor=None): + """ + Plot the data. + + Parameters + ---------- + oldData (optional = None): hypercomplex data type + The old data, to display under the current (i.e. during apodization). + extraX (optional = None): list of ndarrays + List of extra x-axes for data curves + extraY (optional = None): list of ndarrays + List of extra y-axes for data curves + extraZ (optional = None): list of ndarrays + List of extra intensity data for 1D data curves + extraColor (optional = None): list of color strings + List of color strings for the extra data + """ # The oldData and extra plots are not displayed in the contourplot for now self.line_xdata_extra = [] self.line_ydata_extra = [] @@ -1711,10 +3465,10 @@ def showFid(self, oldData=None, extraX=None, extraY=None, extraZ=None, extraColo self.line_xdata_extra.append(self.xax() * axMult) self.line_ydata_extra.append(self.xax(-2) * axMult2) self.line_zdata_extra.append(tmp) - self.plotContour(self.line_xdata_extra[-1], self.line_ydata_extra[-1], self.line_zdata_extra[-1], color=['k','k'], alpha=0.2) + self.plotContour(self.line_xdata_extra[-1], self.line_ydata_extra[-1], self.line_zdata_extra[-1], color=['k', 'k'], alpha=0.2) self.showProj(self.line_xdata_extra[-1], self.line_ydata_extra[-1], self.line_zdata_extra[-1], 'k') if extraX is not None: - for num in range(len(extraX)): + for num, _ in enumerate(extraX): self.line_xdata_extra.append(extraX[num] * axMult) self.line_ydata_extra.append(extraY[num] * axMult2) self.line_zdata_extra.append(extraZ[num]) @@ -1752,7 +3506,25 @@ def showFid(self, oldData=None, extraX=None, extraY=None, extraZ=None, extraColo self.setTicks() self.canvas.draw() - def plotContour(self, line_xdata, line_ydata, line_zdata, color=None, alpha=1, updateOnly=False): # Plots the contour plot + def plotContour(self, line_xdata, line_ydata, line_zdata, color=None, alpha=1, updateOnly=False): + """ + Make the contour plot + + Parameters + ---------- + line_xdata: 1darray + xaxis + line_ydata: 1darray + yaxis + line_zdata: 2darray + Intensity (z) data + color (optional = None): list of colors + If not None, positive and negative contour colors should be in here + alpha (optional = 1): float + Opacity of the lines (1 is solid) + updateOnly (optional = False): booleans + If True, update only the contour plot + """ if color is None and self.viewSettings["contourConst"]: color = self.viewSettings["contourColors"] X, Y = np.meshgrid(line_xdata, line_ydata) @@ -1780,7 +3552,7 @@ def plotContour(self, line_xdata, line_ydata, line_zdata, color=None, alpha=1, u PlotPositive = True PlotNegative = False if self.viewSettings["contourSign"] == 0 or self.viewSettings["contourSign"] == 2: - if not self.viewSettings["plotType"] == 3: # for Absolute plot no negative + if self.viewSettings["plotType"] != 3: # for Absolute plot no negative if line_zdata.shape[0] > 2: YposMin = np.where(np.convolve(np.min(line_zdata, 1) < -contourLevels[0], [True, True, True], 'same'))[0] else: @@ -1795,23 +3567,41 @@ def plotContour(self, line_xdata, line_ydata, line_zdata, color=None, alpha=1, u vmin = -vmax if color is not None: if PlotPositive: - self.ax.contour(X[YposMax[:,None],XposMax],Y[YposMax[:,None],XposMax],line_zdata[YposMax[:,None],XposMax], colors=color[0], alpha=alpha, levels=contourLevels, vmax=vmax, vmin=vmin, linewidths=self.viewSettings["linewidth"], linestyles='solid') + self.ax.contour(X[YposMax[:, None], XposMax], Y[YposMax[:, None], XposMax], line_zdata[YposMax[:, None], XposMax], colors=color[0], alpha=alpha, levels=contourLevels, vmax=vmax, vmin=vmin, linewidths=self.viewSettings["linewidth"], linestyles='solid') if PlotNegative: - self.ax.contour(X[YposMin[:,None],XposMin],Y[YposMin[:,None],XposMin],line_zdata[YposMin[:,None],XposMin], colors=color[1], alpha=alpha, levels=-contourLevels[::-1], vmax=vmax, vmin=vmin, linewidths=self.viewSettings["linewidth"], linestyles='solid') + self.ax.contour(X[YposMin[:, None], XposMin], Y[YposMin[:, None], XposMin], line_zdata[YposMin[:, None], XposMin], colors=color[1], alpha=alpha, levels=-contourLevels[::-1], vmax=vmax, vmin=vmin, linewidths=self.viewSettings["linewidth"], linestyles='solid') else: if PlotPositive: - self.ax.contour(X[YposMax[:,None],XposMax],Y[YposMax[:,None],XposMax],line_zdata[YposMax[:,None],XposMax], cmap=get_cmap(self.viewSettings["colorMap"]), levels=contourLevels, vmax=vmax, vmin=vmin, linewidths=self.viewSettings["linewidth"], linestyles='solid') - if PlotNegative: - self.ax.contour(X[YposMin[:,None],XposMin],Y[YposMin[:,None],XposMin],line_zdata[YposMin[:,None],XposMin], cmap=get_cmap(self.viewSettings["colorMap"]), levels=-contourLevels[::-1], vmax=vmax, vmin=vmin, linewidths=self.viewSettings["linewidth"], linestyles='solid') + self.ax.contour(X[YposMax[:, None], XposMax], Y[YposMax[:, None], XposMax], line_zdata[YposMax[:, None], XposMax], cmap=get_cmap(self.viewSettings["colorMap"]), levels=contourLevels, vmax=vmax, vmin=vmin, linewidths=self.viewSettings["linewidth"], linestyles='solid') + if PlotNegative: + self.ax.contour(X[YposMin[:, None], XposMin], Y[YposMin[:, None], XposMin], line_zdata[YposMin[:, None], XposMin], cmap=get_cmap(self.viewSettings["colorMap"]), levels=-contourLevels[::-1], vmax=vmax, vmin=vmin, linewidths=self.viewSettings["linewidth"], linestyles='solid') self.setTicks() if updateOnly: self.canvas.draw() def clearProj(self): + """ + Clear the projections. + """ self.x_ax.cla() self.y_ax.cla() def showProj(self, line_xdata=None, line_ydata=None, line_zdata=None, color=None): + """ + Show the projections + + + Parameters + ---------- + line_xdata (optional = None): 1darray + xaxis + line_ydata (optional = None): 1darray + yaxis + line_zdata (optional = None): 2darray + Intensity (z) data + color (optional = None): color string + Colour of the projection lines + """ xLimOld = self.x_ax.get_xlim() if line_xdata is None: x = self.line_xdata[-1] @@ -1861,11 +3651,11 @@ def showProj(self, line_xdata=None, line_ydata=None, line_zdata=None, color=None dist2 = y[indices2] - x distSum = (dist1 + dist2) xprojdata = dist2 * tmpdata[indices1, np.arange(len(x))] + dist1 * tmpdata[indices2, np.arange(len(x))] - xprojdata[distSum!=0.0] /= distSum[distSum!=0.0] + xprojdata[distSum != 0.0] /= distSum[distSum != 0.0] xprojdata[x > np.max(y)] = np.nan xprojdata[x < np.min(y)] = np.nan if self.viewSettings["projTop"] != 3: - self.x_ax.plot(x, xprojdata, color=color, linewidth=self.viewSettings["linewidth"], picker=True) + self.x_ax.plot(x, xprojdata, color=color, linewidth=self.viewSettings["linewidth"], picker=True) xmin, xmax = np.nanmin(xprojdata), np.nanmax(xprojdata) self.x_ax.set_ylim([xmin - 0.15 * (xmax - xmin), xmax + 0.05 * (xmax - xmin)]) # Set projection limits, and force 15% whitespace below plot self.x_ax.set_xlim(xLimOld) @@ -1890,7 +3680,7 @@ def showProj(self, line_xdata=None, line_ydata=None, line_zdata=None, color=None dist2 = x[indices2] - y distSum = (dist1 + dist2) yprojdata = dist2 * tmpdata[np.arange(len(y)), indices1] + dist1 * tmpdata[np.arange(len(y)), indices2] - yprojdata[distSum!=0.0] /= distSum[distSum!=0.0] + yprojdata[distSum != 0.0] /= distSum[distSum != 0.0] yprojdata[y > np.max(x)] = np.nan yprojdata[y < np.min(x)] = np.nan if self.viewSettings["projRight"] != 3: @@ -1953,3 +3743,42 @@ def buttonRelease(self, event): self.rightMouse = False self.canvas.draw() + + +class CurrentColour2D(CurrentContour): + """ + 2D colour plot class. Currently a child of CurrentContour. Probably a lower level 2D class + should be made. + """ + + X_RESIZE = False + Y_RESIZE = True + GRID_PLOT = True + INVERT_Y = True + ZERO_SCROLL_ALLOWED = False + def plotContour(self, line_xdata, line_ydata, line_zdata, color=None, alpha=1, updateOnly=False): + """ + Make the contour plot + + Parameters + ---------- + line_xdata: 1darray + xaxis + line_ydata: 1darray + yaxis + line_zdata: 2darray + Intensity (z) data + color (optional = None): list of colors + If not None, positive and negative contour colors should be in here + alpha (optional = 1): float + Opacity of the lines (1 is solid) + updateOnly (optional = False): booleans + If True, update only the contour plot + """ + if updateOnly: # Set some extra stuff if only the contour plot needs updating + del self.ax.collections[:] # Clear all plot collections + + self.ax.imshow(np.flipud(line_zdata),extent = [line_xdata[0],line_xdata[-1],line_ydata[0],line_ydata[-1]],aspect='auto') + self.setTicks() + if updateOnly: + self.canvas.draw() diff --git a/src/widgetClasses.py b/src/widgetClasses.py index 33622f0a..220be51a 100644 --- a/src/widgetClasses.py +++ b/src/widgetClasses.py @@ -17,21 +17,47 @@ # You should have received a copy of the GNU General Public License # along with ssNake. If not, see . -from safeEval import safeEval import os +import sys +from safeEval import safeEval from ssNake import QtGui, QtCore, QtWidgets, QT class SsnakeTabs(QtWidgets.QTabWidget): - # A tab widget were tabs can be closed with the middle mouse button + """ + A reimplementation of the PyQt QTabWidget. + A tab widget were tabs can be closed with the middle mouse button. + """ def mousePressEvent(self, event): + """ + Reimplementation from QTabWidget. + Middle mousebutton closes the tab. + + Parameters + ---------- + event : QMouseEvent + The mouse event. + """ if event.button() == QtCore.Qt.MidButton: index = self.tabBar().tabAt(event.pos()) if index >= 0: self.tabCloseRequested.emit(index) class SsnakeTreeWidget(QtWidgets.QTreeView): - def __init__(self, parent, startDir=None): + """ + A reimplementation of the PyQt QTreeView. + Allows the loading of files and directories in ssNake. + """ + + def __init__(self, parent): + """ + Initializes the SsnakeTreeWidget. + + Parameters + ---------- + parent : QWidget + Parent of the treeview. + """ super(SsnakeTreeWidget, self).__init__(parent) self.father = parent self.dirmodel = QtWidgets.QFileSystemModel() @@ -56,26 +82,52 @@ def __init__(self, parent, startDir=None): self.hideColumn(3) self.expand_all(self.dirmodel.index(self.father.lastLocation)) - def mouseDoubleClickEvent(self,event): + def mouseDoubleClickEvent(self, event): + """ + Reimplementation from QTreeView. + Middle mousebutton loads the data. + Left mousebutton loads the data from a file. + + Parameters + ---------- + event : QMouseEvent + The mouse event. + """ index = self.indexAt(event.pos()) - path = self.dirmodel.filePath(index) + path = self.dirmodel.filePath(index) if event.button() == QtCore.Qt.MidButton: - self.loadAct([path]) + self.father.loadData([path]) elif event.button() == QtCore.Qt.LeftButton and not self.dirmodel.isDir(index): - self.loadAct([path]) + self.father.loadData([path]) super(SsnakeTreeWidget, self).mouseDoubleClickEvent(event) - def mousePressEvent(self,event): + def mousePressEvent(self, event): + """ + Reimplementation from QTreeView. + Middle mousebutton loads the data. + + Parameters + ---------- + event : QMouseEvent + The mouse event. + """ if event.button() == QtCore.Qt.MidButton: index = self.indexAt(event.pos()) - path = self.dirmodel.filePath(index) - self.loadAct([path]) + path = self.dirmodel.filePath(index) + self.father.loadData([path]) else: #If not, let the QTreeView handle the event super(SsnakeTreeWidget, self).mousePressEvent(event) def expand_all(self, index): - path = self.dirmodel.filePath(index) - run = True + """ + Expand all folders up to a certain index. + + Parameters + ---------- + event : QModelIndex + The index to which the folder should be expanded in the treeview. + """ + path = self.dirmodel.filePath(index) pathOld = '-1' while pathOld != path: self.setExpanded(self.dirmodel.index(path), True) @@ -83,23 +135,83 @@ def expand_all(self, index): path = os.path.dirname(path) def openMenu(self, position): + """ + Creates the contextmenu of the treeview at a given position. + The contents of the contextmenu depend on whether the selected item is a file or a directory. + + Parameters + ---------- + position : QPoint + The position to place the contextmenu. + """ index = self.selectedIndexes() path = [self.dirmodel.filePath(x) for x in index] menu = QtWidgets.QMenu() if len(path) == 1: if self.dirmodel.isDir(index[0]): - menu.addAction("Load Directory", lambda: self.loadAct(path)) + menu.addAction("Load Directory", lambda: self.father.loadData(path)) else: - menu.addAction("Load File", lambda: self.loadAct(path)) + menu.addAction("Load File", lambda: self.father.loadData(path)) + menu.addAction("Open File Externally", lambda: self.openExtAct(path)) + menu.addAction("Open in File Browser", lambda: self.openBrowser(path)) else: - menu.addAction("Load Selection", lambda: self.loadAct(path)) + menu.addAction("Load Selection", lambda: self.father.loadData(path)) menu.exec_(self.viewport().mapToGlobal(position)) - def loadAct(self,path): - self.father.loadData(path) - + def openExtAct(self, fileNames): + """ + Opens a file externally. + + Parameters + ---------- + fileNames : list of str + The first string from the list is used as a path to open externally. + """ + fileName = fileNames[0] + if sys.platform.startswith('linux'): + os.system("xdg-open " + '"' + fileName + '"') + elif sys.platform.startswith('darwin'): + os.system("open " + '"' + fileName + '"') + elif sys.platform.startswith('win'): + os.startfile(fileName) + + def openBrowser(self, path): + """ + Opens a directory using the filebrowser. + When the a file is given the containing directory is used. + + Parameters + ---------- + path : list of str + The first string from the list is used as the path to open in a filebrowser. + """ + path = os.path.dirname(path[0]) + if sys.platform.startswith('linux'): + os.system("xdg-open " + '"' + path + '"') + elif sys.platform.startswith('darwin'): + os.system("open " + '"' + path + '"') + elif sys.platform.startswith('win'): + os.startfile(path) + + class SsnakeSlider(QtWidgets.QSlider): + """ + A reimplementation of the PyQt QSlider. + The behaviour is modified when the Control or Shift buttons are pressed. + """ + def wheelEvent(self, event): + """ + Reimplementation from QSlider. + When the Control button is held the stepsize is multiplied by 10. + When the Shift button is held the stepsize is multiplied by 100. + When both Control and Shift buttons are held the stepsize is multiplied by 1000. + + Parameters + ---------- + event : QMouseEvent + The mouse event. + """ if QT == 4: delta = event.delta() else: @@ -115,24 +227,54 @@ def wheelEvent(self, event): self.setValue(self.value() + step) else: self.setValue(self.value() - step) - + class SplitterEventFilter(QtCore.QObject): + """ + The event filter for the splitter between the treeview and the canvas. + """ def __init__(self, root, *args): + """ + Initializes the splitter event filter. + + Parameters + ---------- + root : QSplitter + The splitter to which the event filter is installed. + *args + Additional arguments are passed to QObject. + """ super(SplitterEventFilter, self).__init__(*args) self.root = root self.sizeBak = 0 def eventFilter(self, receiver, event): - Select = False + """ + The event filter function. + A double mouseclick or a single middle mouseclick minimizes or restores the treeview. + + Parameters + ---------- + receiver : QObject + The receiver. + Not used. + event : QEvent + The event. + + Returns + ------- + bool + True if the event matched the resize event, False otherwise. + """ + select = False if event.type() == QtCore.QEvent.MouseButtonDblClick: - Select = True - #If single click with middle mouse button + select = True + # If single click with middle mouse button if event.type() == QtCore.QEvent.MouseButtonPress: if event.button() == QtCore.Qt.MidButton: - Select = True - if Select: + select = True + if select: sizes = self.root.sizes() if sizes[0] == 0: self.root.setSizes([self.sizeBak, 1]) @@ -143,37 +285,81 @@ def eventFilter(self, receiver, event): return False class SsnakeEventFilter(QtCore.QObject): + """ + The event filter of ssNake for undo and redo. + """ def __init__(self, root, *args): + """ + Initializes the ssNake event filter. + + Parameters + ---------- + root : MainProgram + The main program to which the event filter is installed. + *args + Additional arguments are passed to QObject. + """ super(SsnakeEventFilter, self).__init__(*args) self.root = root def eventFilter(self, receiver, event): + """ + The event filter function. + Control + Z triggers the undo function. + Control + Shift + Z triggers the redo function. + + Parameters + ---------- + receiver : QObject + The receiver. + Not used. + event : QEvent + The event. + + Returns + ------- + bool + True if the event matched undo or redo, False otherwise. + """ if event.type() == QtCore.QEvent.KeyPress: if event.key() == QtCore.Qt.Key_Z: if (event.modifiers() & QtCore.Qt.ControlModifier) and (event.modifiers() & QtCore.Qt.ShiftModifier): self.root.redo() return True - elif event.modifiers() == (QtCore.Qt.ControlModifier): + if event.modifiers() == (QtCore.Qt.ControlModifier): self.root.undo() return True return False -class ToolWindows(QtWidgets.QWidget): +class ToolWindow(QtWidgets.QWidget): + """ + The base class of the toolwindows. + Implements the basic features shared by all toolwindows. + Toolwindows inherit this class and alter its behaviour by the constants or by reimplementing functions. + """ - NAME = "" - PICK = False - SINGLESLICE = False - BROWSE = False - RESIZABLE = False - MENUDISABLE = True - APPLYANDCLOSE = True - CANCELNAME = "&Cancel" - OKNAME = "&Ok" + NAME = "" # The name displayed in the title of the window + PICK = False # Does the window use peak picking + SINGLESLICE = False # Should the single slice button be displayed + BROWSE = False # Should the window have a browse button + RESIZABLE = False # should the window be resizable + MENUDISABLE = True # Should the window disable the menu of the main window + APPLYANDCLOSE = True # Should the window close after the ok button is pressed + CANCELNAME = "&Cancel" # The name on the cancel button + OKNAME = "&Ok" # The name on the ok button def __init__(self, parent): - super(ToolWindows, self).__init__(parent) + """ + Initializes the ToolWindow. + + Parameters + ---------- + parent : Main1DWindow or AbstractParamFrame + Parent of the toolwindow. + """ + super(ToolWindow, self).__init__(parent) self.setWindowFlags(QtCore.Qt.Window | QtCore.Qt.Tool) self.father = parent self.setWindowTitle(self.NAME) @@ -196,10 +382,10 @@ def __init__(self, parent): self.okButton = QtWidgets.QPushButton(self.OKNAME) self.okButton.clicked.connect(self.applyAndClose) self.okButton.setFocus() - self.box.addButton(self.cancelButton,QtWidgets.QDialogButtonBox.RejectRole) - self.box.addButton(self.okButton,QtWidgets.QDialogButtonBox.AcceptRole) + self.box.addButton(self.cancelButton, QtWidgets.QDialogButtonBox.RejectRole) + self.box.addButton(self.okButton, QtWidgets.QDialogButtonBox.AcceptRole) self.show() - self.layout.addWidget(self.box,3,0) + self.layout.addWidget(self.box, 3, 0) if not self.RESIZABLE: self.setFixedSize(self.size()) if self.MENUDISABLE: @@ -207,17 +393,36 @@ def __init__(self, parent): self.setGeometry(self.frameSize().width() - self.geometry().width(), self.frameSize().height(), 0, 0) def browse(self): + """ + Dummy function for the Browse button. + Should be reimplemented by the toolwindows using BROWSE=True. + """ pass def applyFunc(self): + """ + Dummy function for the apply function. + Should be reimplemented by the toolwindows. + """ pass def applyAndClose(self): + """ + Runs the apply function and when APPLYANDCLOSE is set, closes the window. + """ self.applyFunc() if self.APPLYANDCLOSE: self.closeEvent() def closeEvent(self, *args): + """ + Updates the view and closes the toolwindow. + + Parameters + ---------- + *args + Any arguments are ignored. + """ self.father.current.upd() self.father.current.showFid() if self.MENUDISABLE: @@ -227,8 +432,31 @@ def closeEvent(self, *args): class SliceValidator(QtGui.QValidator): + """ + A reimplementation of the QValidator. + It uses the safeEval to validate a string for slice selection. + """ def validate(self, string, position): + """ + Validates a given string using safeEval. + + Parameters + ---------- + string : str + String to be validated. + position : int + Position of the cursor. + + Returns + ------- + State + Acceptable if the string is parsable by safeEval, Intermediate otherwise. + string : str + The input string. + position : int + The input position of the cursor. + """ string = str(string) try: int(safeEval(string)) @@ -238,42 +466,104 @@ def validate(self, string, position): class SliceSpinBox(QtWidgets.QSpinBox): + """ + A reimplementation of the QSpinBox. + This spinbox is designed for the slice selection of spectra. + """ def __init__(self, parent, minimum, maximum, *args, **kwargs): + """ + Initializes the SliceSpinBox. + + Parameters + ---------- + parent : SideFrame + The sideframe which contains the SliceSpinBox. + minimum : int + The minimum value of the spinbox. + maximum : int + The maximum value of the spinbox. + *args + Additional arguments are passed to QSpinBox. + **kwargs + Keyword arguments are passed to QSpinBox + """ self.validator = SliceValidator() + self.validate = self.validator.validate + self.fixup = self.validator.fixup super(SliceSpinBox, self).__init__(parent, *args, **kwargs) self.setMinimum(minimum) self.setMaximum(maximum) self.setKeyboardTracking(False) - def validate(self, text, position): - return self.validator.validate(text, position) - - def fixup(self, text): - return self.validator.fixup(text) - def valueFromText(self, text): + """ + Parses a string to a slice number. + + Parameters + ---------- + text : str + String to be parsed. + + Returns + ------- + int + The slice number. + """ inp = int(safeEval(str(text))) if inp < 0: inp = inp + self.maximum() + 1 return inp def textFromValue(self, value): + """ + Parses a value to a slice number. + + Parameters + ---------- + text : int or float + Value to be parsed. + + Returns + ------- + int + The slice number. + """ inp = int(value) if inp < 0: inp = inp + self.maximum() + 1 return str(inp) - def stepBy(self,steps): + def stepBy(self, steps): + """ + Increases or decreases the spinbox by a given value. + When the Control button is held the stepsize is multiplied 10. + When the Shift button is held the stepsize is multiplied 100. + When both the Control and Shift buttons are held the stepsize is multiplied 1000. + + Parameters + ---------- + steps : int + The number of steps to increase the spinbox + """ + mod = 1 if QtWidgets.qApp.keyboardModifiers() & QtCore.Qt.ControlModifier and QtWidgets.qApp.keyboardModifiers() & QtCore.Qt.ShiftModifier: - steps *= 1000 + mod = 1000 elif QtWidgets.qApp.keyboardModifiers() & QtCore.Qt.ControlModifier: - steps *= 10 + mod = 10 elif QtWidgets.qApp.keyboardModifiers() & QtCore.Qt.ShiftModifier: - steps *= 100 - self.setValue(self.value() + steps) + mod = 100 + self.setValue(self.value() + mod * steps) def wheelEvent(self, event): + """ + The function for handling the scroll event on the spinbox. + + Parameters + ---------- + event : QWheelEvent + The event on the spinbox. + """ if QT == 4: delta = event.delta() else: @@ -285,20 +575,46 @@ def wheelEvent(self, event): self.stepBy(-step) event.accept() + class SsnakeDoubleSpinBox(QtWidgets.QDoubleSpinBox): - def stepBy(self,steps): + """ + A reimplementation of the QDoubleSpinBox. + """ + + def stepBy(self, steps): + """ + Increases or decreases the spinbox by a given value. + When the Control button is held the stepsize is multiplied 10. + When the Shift button is held the stepsize is multiplied 100. + When both the Control and Shift buttons are held the stepsize is multiplied 1000. + When the Alt button is held the inverse + + Parameters + ---------- + steps : int or float + The value to increase the spinbox + """ + mod = 1 if QtWidgets.qApp.keyboardModifiers() & QtCore.Qt.ControlModifier and QtWidgets.qApp.keyboardModifiers() & QtCore.Qt.ShiftModifier: - steps *= 1000 + mod = 1000 elif QtWidgets.qApp.keyboardModifiers() & QtCore.Qt.ControlModifier: - steps *= 10 + mod = 10 elif QtWidgets.qApp.keyboardModifiers() & QtCore.Qt.ShiftModifier: - steps *= 100 + mod = 100 if QtWidgets.qApp.keyboardModifiers() & QtCore.Qt.AltModifier: - self.setValue(self.value() + self.singleStep() / steps) + self.setValue(self.value() + self.singleStep() * steps / mod) else: - self.setValue(self.value() + self.singleStep() * steps) + self.setValue(self.value() + self.singleStep() * mod * steps) def wheelEvent(self, event): + """ + The function for handling the scroll event on the spinbox. + + Parameters + ---------- + event : QWheelEvent + The event on the spinbox. + """ if QT == 4: delta = event.delta() else: @@ -310,17 +626,42 @@ def wheelEvent(self, event): self.stepBy(-step) event.accept() + class SsnakeSpinBox(QtWidgets.QSpinBox): - def stepBy(self,steps): + """ + A reimplementation of the QSpinBox. + """ + + def stepBy(self, steps): + """ + Increases or decreases the spinbox by a given value. + When the Control button is held the stepsize is multiplied 10. + When the Shift button is held the stepsize is multiplied 100. + When both the Control and Shift buttons are held the stepsize is multiplied 1000. + + Parameters + ---------- + steps : int + The value to increase the spinbox + """ + mod = 1 if QtWidgets.qApp.keyboardModifiers() & QtCore.Qt.ControlModifier and QtWidgets.qApp.keyboardModifiers() & QtCore.Qt.ShiftModifier: - steps *= 1000 + mod = 1000 elif QtWidgets.qApp.keyboardModifiers() & QtCore.Qt.ControlModifier: - steps *= 10 + mod = 10 elif QtWidgets.qApp.keyboardModifiers() & QtCore.Qt.ShiftModifier: - steps *= 100 - self.setValue(self.value() + self.singleStep() * steps) + mod = 100 + self.setValue(self.value() + self.singleStep() * mod * steps) def wheelEvent(self, event): + """ + The function for handling the scroll event on the spinbox. + + Parameters + ---------- + event : QWheelEvent + The event on the spinbox. + """ if QT == 4: delta = event.delta() else: @@ -332,19 +673,34 @@ def wheelEvent(self, event): self.stepBy(-step) event.accept() + class QLabel(QtWidgets.QLabel): + """ + A reimplementation of the QLabel. + The text is center aligned by default. + """ def __init__(self, parent, *args, **kwargs): super(QLabel, self).__init__(parent, *args, **kwargs) self.setAlignment(QtCore.Qt.AlignCenter) + class QSelectLabel(QtWidgets.QLabel): + """ + A reimplementation of the QLabel. + The text is selectable by default. + """ def __init__(self, parent, *args, **kwargs): super(QSelectLabel, self).__init__(parent, *args, **kwargs) self.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse) + class QLeftLabel(QtWidgets.QLabel): + """ + A reimplementation of the QLabel. + The text is left aligned by default. + """ def __init__(self, parent, *args, **kwargs): super(QLeftLabel, self).__init__(parent, *args, **kwargs) @@ -353,8 +709,27 @@ def __init__(self, parent, *args, **kwargs): class QLineEdit(QtWidgets.QLineEdit): + """ + A reimplementation of the QLineEdit. + The text is center aligned by default. + """ def __init__(self, text='', func=None, parent=None): + """ + Initializes QLineEdit. + + Parameters + ---------- + text : str, optional + The initial text, by default this is an empty string. + func : function, optional + The function that is called when return is pressed. + By default no function is called. + parent : QWidget, optional + The parent widget of the lineedit. + This object is passed to QLineEdit. + By default None is used. + """ super(QLineEdit, self).__init__(parent) self.setText(str(text)) self.setAlignment(QtCore.Qt.AlignCenter) @@ -363,18 +738,50 @@ def __init__(self, text='', func=None, parent=None): class FitQLineEdit(QLineEdit): + """ + A reimplementation of the QLineEdit designed for the fitting parameter frame. + Allows connecting parameters from the contextmenu. + """ def __init__(self, fitParent, paramName, *args): + """ + Initializes FitQLineEdit. + + Parameters + ---------- + parent : AbstractParamFrame + The fitting parameter frame of the lineedit. + paramName : str + The name of this lineedit. + *args + Additional arguments are passed to QLineEdit. + """ super(FitQLineEdit, self).__init__(*args) self.fitParent = fitParent self.paramName = paramName def contextMenuEvent(self, event): + """ + Creates the context menu, with the Connect Parameter option. + + Parameters + ---------- + event : QEvent + The event. + """ menu = self.createStandardContextMenu() menu.addAction('Connect Parameter', self.connectParams) menu.exec_(event.globalPos()) def connectParams(self, *args): + """ + Opens the ConnectParamsWindow. + + Parameters + ---------- + *args + Additional arguments are ignored. + """ parametertxtlist = self.fitParent.rootwindow.getParamTextList() ConnectParamsWindow(self, parametertxtlist, self.paramName, self.fitParent.rootwindow.getTabNames(), self.fitParent.rootwindow.getCurrentTabName(), self.setConnect) @@ -383,8 +790,29 @@ def setConnect(self, inpTuple): class ConnectParamsWindow(QtWidgets.QWidget): + """ + The window for connecting fitting parameters. + """ def __init__(self, parent, paramTextList, paramName, spectrumNames, currentSpectrum, returnFunc): + """ + Initializes the ConnectParamsWindow. + + Parameters + ---------- + parent : QWidget + The parent of the ConnectParamsWindow. This value is passed to QWidget. + paramTextList : list of str + List with the names of all parameters. + paramName : str + Name of the lineedit from which this window was opened. + spectrumNames : list of str + A list of the names of the spectra being fit. + currentSpectrum : str + The name of the spectrum that is currently open. + returnFunc : function + The function that should be called when the ok button is pressed. + """ super(ConnectParamsWindow, self).__init__(parent) self.setWindowFlags(QtCore.Qt.Window | QtCore.Qt.Tool) self.setWindowTitle("Connect Parameter") @@ -421,12 +849,15 @@ def __init__(self, parent, paramTextList, paramName, spectrumNames, currentSpect self.okButton.clicked.connect(self.applyAndClose) self.okButton.setFocus() self.box = QtWidgets.QDialogButtonBox() - self.box.addButton(self.cancelButton,QtWidgets.QDialogButtonBox.RejectRole) - self.box.addButton(self.okButton,QtWidgets.QDialogButtonBox.AcceptRole) + self.box.addButton(self.cancelButton, QtWidgets.QDialogButtonBox.RejectRole) + self.box.addButton(self.okButton, QtWidgets.QDialogButtonBox.AcceptRole) self.layout.addWidget(self.box, 2, 0) self.show() def applyAndClose(self): + """ + Runs the returnFunc and closes the window. + """ paramName = self.paramTextList[self.paramNameEntry.currentIndex()] returnTuple = (paramName, self.lineEntry.value(), safeEval(self.multEntry.text()), safeEval(self.addEntry.text()), self.spectrumNameEntry.currentIndex()) self.closeEvent() @@ -434,17 +865,3 @@ def applyAndClose(self): def closeEvent(self, *args): self.deleteLater() - - -class specialProgressBar(QtWidgets.QProgressBar): - - def __init__(self): - super(specialProgressBar, self).__init__() - self.setAlignment(QtCore.Qt.AlignCenter) - self._text = 'Inactive' - - def setText(self, text): - self._text = text - - def text(self): - return self._text